diff --git a/stackslib/src/cli.rs b/stackslib/src/cli.rs index 66e14d4b5d..1df033b4a4 100644 --- a/stackslib/src/cli.rs +++ b/stackslib/src/cli.rs @@ -31,6 +31,7 @@ use stacks_common::types::chainstate::{BlockHeaderHash, BurnchainHeaderHash, Sta use stacks_common::types::sqlite::NO_PARAMS; use stacks_common::util::get_epoch_time_ms; use stacks_common::util::hash::Hash160; +use stacks_common::util::secp256k1::Secp256k1PublicKey; use stacks_common::util::vrf::VRFProof; use crate::burnchains::db::BurnchainDB; @@ -53,25 +54,34 @@ use crate::cost_estimates::metrics::UnitMetric; use crate::cost_estimates::UnitEstimator; use crate::util_lib::db::IndexDBTx; -/// Options common to many `stacks-inspect` subcommands -/// Returned by `process_common_opts()` +/// Options for `stacks-inspect` command and used by many subcommands #[derive(Debug, Default)] -pub struct CommonOpts { +pub struct StacksInspectOpts { pub config: Option, } -/// Process arguments common to many `stacks-inspect` subcommands and drain them from `argv` +impl StacksInspectOpts { + #[inline(always)] + pub fn new_with_config(config: Config) -> Self { + Self { + config: Some(config), + ..Self::default() + } + } +} + +/// Process `stacks-inspect` options and drain them from `argv` /// /// Args: -/// - `argv`: Full CLI args `Vec` +/// - `argv`: Full or partial CLI args `Vec` /// - `start_at`: Position in args vec where to look for common options. /// For example, if `start_at` is `1`, then look for these options **before** the subcommand: /// ```console /// stacks-inspect --config testnet.toml replay-block path/to/chainstate /// ``` -pub fn drain_common_opts(argv: &mut Vec, start_at: usize) -> CommonOpts { +pub fn drain_stacks_inspect_opts(argv: &mut Vec, start_at: usize) -> StacksInspectOpts { let mut i = start_at; - let mut opts = CommonOpts::default(); + let mut opts = StacksInspectOpts::default(); while let Some(arg) = argv.get(i) { let (prefix, opt) = arg.split_at(2); if prefix != "--" { @@ -118,12 +128,74 @@ pub fn drain_common_opts(argv: &mut Vec, start_at: usize) -> CommonOpts opts } +/// Options for `try-mine` subcommand +#[derive(Debug, Default)] +pub struct TryMineOpts { + pub min_fee: Option, + pub max_time: Option, + pub max_blocks: Option, + pub reset_tenure: Option, +} + +/// Process `try-mine` options and drain them from `argv` +/// +/// Args: +/// - `argv`: Full or partial CLI args `Vec` +/// - `start_at`: Position in args vec where to look for common options. +/// For example, if `start_at` is `1`, then look for these options after the `try-mine` command: +/// ```console +/// try-mine --min-fee 10000 path/to/chainstate +/// ``` +pub fn drain_try_mine_opts(argv: &mut Vec, start_at: usize) -> TryMineOpts { + let mut i = start_at; + let mut opts = TryMineOpts::default(); + while let Some(arg) = argv.get(i) { + let (prefix, opt) = arg.split_at(2); + if prefix != "--" { + // No args left to take + break; + } + // "Take" arg + i += 1; + match opt { + "min-fee" => { + let fee = argv[i] + .parse() + .unwrap_or_else(|e| panic!("Failed to parse `{opt}` as `u64`: {e}")); + opts.min_fee.replace(fee); + i += 1; + } + "max-time" => { + let time = argv[i] + .parse() + .unwrap_or_else(|e| panic!("Failed to parse `{opt}` as `u64`: {e}")); + opts.max_time.replace(time); + i += 1; + } + "max-blocks" => { + let blocks = argv[i] + .parse() + .unwrap_or_else(|e| panic!("Failed to parse `{opt}` as `u64`: {e}")); + opts.max_blocks.replace(blocks); + i += 1; + } + "reset-tenure" => { + opts.reset_tenure.replace(true); + } + _ => panic!("Unrecognized option: {opt}"), + } + } + // Remove options processed + argv.drain(start_at..i); + opts +} + /// Replay blocks from chainstate database /// Terminates on error using `process::exit()` /// /// Arguments: /// - `argv`: Args in CLI format: ` [args...]` -pub fn command_replay_block(argv: &[String], conf: Option<&Config>) { +pub fn command_replay_block(argv: &[String], opts: &StacksInspectOpts) { let print_help_and_exit = || -> ! { let n = &argv[0]; eprintln!("Usage:"); @@ -186,6 +258,7 @@ pub fn command_replay_block(argv: &[String], conf: Option<&Config>) { } let total = index_block_hashes.len(); + let conf = opts.config.as_ref(); println!("Will check {total} blocks"); for (i, index_block_hash) in index_block_hashes.iter().enumerate() { if i % 100 == 0 { @@ -201,7 +274,7 @@ pub fn command_replay_block(argv: &[String], conf: Option<&Config>) { /// /// Arguments: /// - `argv`: Args in CLI format: ` [args...]` -pub fn command_replay_block_nakamoto(argv: &[String], conf: Option<&Config>) { +pub fn command_replay_block_nakamoto(argv: &[String], opts: &StacksInspectOpts) { let print_help_and_exit = || -> ! { let n = &argv[0]; eprintln!("Usage:"); @@ -218,7 +291,7 @@ pub fn command_replay_block_nakamoto(argv: &[String], conf: Option<&Config>) { let chain_state_path = format!("{db_path}/chainstate/"); - let conf = conf.unwrap_or(&DEFAULT_MAINNET_CONFIG); + let conf = opts.config.as_ref().unwrap_or(&DEFAULT_MAINNET_CONFIG); let (chainstate, _) = StacksChainState::open( conf.is_mainnet(), @@ -290,7 +363,7 @@ pub fn command_replay_block_nakamoto(argv: &[String], conf: Option<&Config>) { /// Arguments: /// - `argv`: Args in CLI format: ` [args...]` /// - `conf`: Optional config for running on non-mainnet chainstate -pub fn command_replay_mock_mining(argv: &[String], conf: Option<&Config>) { +pub fn command_replay_mock_mining(argv: &[String], opts: &StacksInspectOpts) { let print_help_and_exit = || -> ! { let n = &argv[0]; eprintln!("Usage:"); @@ -366,6 +439,7 @@ pub fn command_replay_mock_mining(argv: &[String], conf: Option<&Config>) { indexed_files[0].0 ); + let conf = opts.config.as_ref(); for (bh, filename) in indexed_files { let filepath = blocks_path.join(filename); let block = AssembledAnchorBlock::deserialize_from_file(&filepath) @@ -384,32 +458,38 @@ pub fn command_replay_mock_mining(argv: &[String], conf: Option<&Config>) { /// Arguments: /// - `argv`: Args in CLI format: ` [args...]` /// - `conf`: Optional config for running on non-mainnet chainstate -pub fn command_try_mine(argv: &[String], conf: Option<&Config>) { +pub fn command_try_mine(mut argv: Vec, opts: &StacksInspectOpts) { + // Parse subcommand-specific flags + let try_mine_opts = drain_try_mine_opts(&mut argv, 1); + let print_help_and_exit = || { - let n = &argv[0]; - eprintln!("Usage: {n} [min-fee [max-time]]"); + eprintln!("Usage: {n} [options...] ", n = &argv[0]); + eprintln!(""); + eprintln!("Options:"); + eprintln!(" --min-fee : Minimum fee for miner to include transaction"); + eprintln!(" --max-time : Max time to spend mining a block, in ms"); + eprintln!(" --max-blocks : Max blocks to mine"); + eprintln!(" --reset-tenure: Force a tenure reset (only matters post-Nakamoto)"); eprintln!(""); - eprintln!("Given a , try to ''mine'' an anchored block. This invokes the miner block"); - eprintln!("assembly, but does not attempt to broadcast a block commit. This is useful for determining"); - eprintln!("what transactions a given chain state would include in an anchor block,"); - eprintln!("or otherwise simulating a miner."); + eprintln!("Given a , try to \"mine\" an anchored block."); + eprintln!("This invokes the miner block assembly, but does not attempt to"); + eprintln!("broadcast a block commit. This is useful for determining"); + eprintln!("which transactions a given chain state would include in an"); + eprintln!("anchor block, or otherwise simulating a miner."); process::exit(1); }; - // Parse subcommand-specific args + // Parse subcommand-specific positional args let db_path = argv.get(1).unwrap_or_else(print_help_and_exit); - let min_fee = argv - .get(2) - .map(|arg| arg.parse().expect("Could not parse min_fee")) - .unwrap_or(u64::MAX); - let max_time = argv - .get(3) - .map(|arg| arg.parse().expect("Could not parse max_time")) - .unwrap_or(u64::MAX); + + let min_fee = try_mine_opts.min_fee.unwrap_or(0); + let max_time = try_mine_opts.max_time.unwrap_or(u64::MAX); + let _max_blocks = try_mine_opts.max_blocks.unwrap_or(1); + let reset_tenure = try_mine_opts.reset_tenure.unwrap_or(false); let start = Instant::now(); - let conf = conf.unwrap_or(&DEFAULT_MAINNET_CONFIG); + let conf = opts.config.as_ref().unwrap_or(&DEFAULT_MAINNET_CONFIG); let burnchain_path = format!("{db_path}/burnchain"); let sort_db_path = format!("{db_path}/burnchain/sortition"); @@ -445,17 +525,24 @@ pub fn command_try_mine(argv: &[String], conf: Option<&Config>) { NakamotoChainState::get_canonical_block_header(chainstate.db(), &sort_db) .unwrap_or_else(|e| panic!("Error looking up chain tip: {e}")) .expect("No chain tip found"); + let parent_consensus_hash = parent_stacks_header.consensus_hash; + let parent_block_id = parent_stacks_header.index_block_hash(); let burn_dbconn = sort_db.index_handle(&chain_tip.sortition_id); let mut settings = BlockBuilderSettings::limited(); settings.max_miner_time_ms = max_time; + // In case we need to submit transactions + let miner_privk = StacksPrivateKey::new(); + let miner_pubkey = Secp256k1PublicKey::from_private(&miner_privk); + let miner_pubkey_hash = Hash160::from_node_public_key(&miner_pubkey); + let miner_nonce = 0; + let result = match &parent_stacks_header.anchored_header { StacksBlockHeaderTypes::Epoch2(..) => { - let sk = StacksPrivateKey::new(); - let mut tx_auth = TransactionAuth::from_p2pkh(&sk).unwrap(); - tx_auth.set_origin_nonce(0); + let mut tx_auth = TransactionAuth::from_p2pkh(&miner_privk).unwrap(); + tx_auth.set_origin_nonce(miner_nonce); let mut coinbase_tx = StacksTransaction::new( TransactionVersion::Mainnet, @@ -466,7 +553,7 @@ pub fn command_try_mine(argv: &[String], conf: Option<&Config>) { coinbase_tx.chain_id = conf.burnchain.chain_id; coinbase_tx.anchor_mode = TransactionAnchorMode::OnChainOnly; let mut tx_signer = StacksTransactionSigner::new(&coinbase_tx); - tx_signer.sign_origin(&sk).unwrap(); + tx_signer.sign_origin(&miner_privk).unwrap(); let coinbase_tx = tx_signer.get_tx().unwrap(); StacksBlockBuilder::build_anchored_block( @@ -490,6 +577,49 @@ pub fn command_try_mine(argv: &[String], conf: Option<&Config>) { .map(|(block, cost, size)| (block.block_hash(), block.txs, cost, size)) } StacksBlockHeaderTypes::Nakamoto(..) => { + let tenure_info = if reset_tenure { + let num_blocks_so_far = NakamotoChainState::get_nakamoto_tenure_length( + chainstate.db(), + &parent_block_id, + ) + .unwrap_or_else(|e| panic!("Error getting tenure length: {e}")); + let payload = TenureChangePayload { + tenure_consensus_hash: parent_consensus_hash, + prev_tenure_consensus_hash: parent_consensus_hash, + burn_view_consensus_hash: parent_consensus_hash, + previous_tenure_end: parent_block_id, + previous_tenure_blocks: num_blocks_so_far, + cause: TenureChangeCause::Extended, + pubkey_hash: miner_pubkey_hash, + }; + let tenure_change_tx_payload = TransactionPayload::TenureChange(payload); + + let mut tx_auth = TransactionAuth::from_p2pkh(&miner_privk).unwrap(); + tx_auth.set_origin_nonce(miner_nonce); + + let version = if conf.is_mainnet() { + TransactionVersion::Mainnet + } else { + TransactionVersion::Testnet + }; + + let mut tx = StacksTransaction::new(version, tx_auth, tenure_change_tx_payload); + + tx.chain_id = conf.burnchain.chain_id; + tx.anchor_mode = TransactionAnchorMode::OnChainOnly; + let mut tx_signer = StacksTransactionSigner::new(&tx); + tx_signer + .sign_origin(&miner_privk) + .unwrap_or_else(|e| panic!("Failed to sign transaction: {e}")); + + let tenure_change_tx = Some(tx_signer.get_tx().expect("Failed to get tx")); + NakamotoTenureInfo { + coinbase_tx: None, + tenure_change_tx, + } + } else { + NakamotoTenureInfo::default() + }; NakamotoBlockBuilder::build_nakamoto_block( &chainstate, &burn_dbconn, @@ -499,7 +629,7 @@ pub fn command_try_mine(argv: &[String], conf: Option<&Config>) { &parent_stacks_header.consensus_hash, // the burn so far on the burnchain (i.e. from the last burnchain block) chain_tip.total_burn, - NakamotoTenureInfo::default(), + tenure_info, settings, None, 0, @@ -510,10 +640,8 @@ pub fn command_try_mine(argv: &[String], conf: Option<&Config>) { let elapsed = start.elapsed(); let summary = format!( - "block @ height = {h} off of {pid} ({pch}/{pbh}) in {t}ms. Min-fee: {min_fee}, Max-time: {max_time}", + "block @ height = {h} off of {parent_block_id} ({parent_consensus_hash}/{pbh}) in {t}ms. Min-fee: {min_fee}, Max-time: {max_time}", h=parent_stacks_header.stacks_block_height + 1, - pid=&parent_stacks_header.index_block_hash(), - pch=&parent_stacks_header.consensus_hash, pbh=&parent_stacks_header.anchored_header.block_hash(), t=elapsed.as_millis(), ); @@ -1131,8 +1259,8 @@ pub mod test { "stacks-inspect try-mine --config my_config.toml /tmp/chainstate/mainnet", ); let argv_init = argv.clone(); - let opts = drain_common_opts(&mut argv, 0); - let opts = drain_common_opts(&mut argv, 1); + let opts = drain_stacks_inspect_opts(&mut argv, 0); + let opts = drain_stacks_inspect_opts(&mut argv, 1); assert_eq!(argv, argv_init); assert!(opts.config.is_none()); @@ -1141,7 +1269,7 @@ pub mod test { let mut argv = parse_cli_command( "stacks-inspect --network mocknet --network mainnet try-mine /tmp/chainstate/mainnet", ); - let opts = drain_common_opts(&mut argv, 1); + let opts = drain_stacks_inspect_opts(&mut argv, 1); let argv_expected = parse_cli_command("stacks-inspect try-mine /tmp/chainstate/mainnet"); assert_eq!(argv, argv_expected); diff --git a/stackslib/src/main.rs b/stackslib/src/main.rs index af597808c0..1ee03cc35e 100644 --- a/stackslib/src/main.rs +++ b/stackslib/src/main.rs @@ -309,7 +309,7 @@ fn main() { process::exit(1); } - let common_opts = cli::drain_common_opts(&mut argv, 1); + let opts = cli::drain_stacks_inspect_opts(&mut argv, 1); if argv[1] == "--version" { println!( @@ -798,7 +798,7 @@ check if the associated microblocks can be downloaded } if argv[1] == "try-mine" { - cli::command_try_mine(&argv[1..], common_opts.config.as_ref()); + cli::command_try_mine(argv[1..].to_vec(), &opts); process::exit(0); } @@ -1600,17 +1600,17 @@ check if the associated microblocks can be downloaded } if argv[1] == "replay-block" { - cli::command_replay_block(&argv[1..], common_opts.config.as_ref()); + cli::command_replay_block(&argv[1..], &opts); process::exit(0); } if argv[1] == "replay-naka-block" { - cli::command_replay_block_nakamoto(&argv[1..], common_opts.config.as_ref()); + cli::command_replay_block_nakamoto(&argv[1..], &opts); process::exit(0); } if argv[1] == "replay-mock-mining" { - cli::command_replay_mock_mining(&argv[1..], common_opts.config.as_ref()); + cli::command_replay_mock_mining(&argv[1..], &opts); process::exit(0); } diff --git a/testnet/stacks-node/src/nakamoto_node/miner.rs b/testnet/stacks-node/src/nakamoto_node/miner.rs index d9edf97e90..a66cc45100 100644 --- a/testnet/stacks-node/src/nakamoto_node/miner.rs +++ b/testnet/stacks-node/src/nakamoto_node/miner.rs @@ -1113,14 +1113,13 @@ impl BlockMinerThread { Some(&self.event_dispatcher), signer_bitvec_len.unwrap_or(0), ) - .map_err(|e| { + .inspect_err(|e| { if !matches!( e, ChainstateError::MinerAborted | ChainstateError::NoTransactionsToMine ) { error!("Relayer: Failure mining anchored block: {e}"); } - e })?; if block.txs.is_empty() { diff --git a/testnet/stacks-node/src/tests/neon_integrations.rs b/testnet/stacks-node/src/tests/neon_integrations.rs index a3ce78eb24..f77ad96b3e 100644 --- a/testnet/stacks-node/src/tests/neon_integrations.rs +++ b/testnet/stacks-node/src/tests/neon_integrations.rs @@ -38,7 +38,7 @@ use stacks::chainstate::stacks::{ StacksPublicKey, StacksTransaction, TransactionContractCall, TransactionPayload, }; use stacks::clarity_cli::vm_execute as execute; -use stacks::cli; +use stacks::cli::{self, StacksInspectOpts}; use stacks::codec::StacksMessageCodec; use stacks::config::{EventKeyType, EventObserverConfig, FeeEstimatorName, InitialBalance}; use stacks::core::mempool::MemPoolWalkTxTypes; @@ -12694,10 +12694,11 @@ fn mock_miner_replay() { // Run `mock_miner_replay()` let blocks_dir = blocks_dir.into_os_string().into_string().unwrap(); let db_path = format!("{}/neon", conf.node.working_dir); - let args: Vec = vec!["replay-mock-mining".into(), db_path, blocks_dir]; info!("Replaying mock mined blocks..."); - cli::command_replay_mock_mining(&args, Some(&conf)); + let argv: Vec = vec!["replay-mock-mining".into(), db_path, blocks_dir]; + let opts = StacksInspectOpts::new_with_config(conf); + cli::command_replay_mock_mining(&argv, &opts); // ---------- Test finished, clean up ----------