diff --git a/.vscode/launch.json b/.vscode/launch.json index ea804a69ab2..64a883de0ad 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -6,13 +6,12 @@ "request": "launch", "name": "executable 'blockstack-core'", "cargo": { - "args": ["build", "--bin=stacks-inspect", "--package=blockstack-core"], + "args": ["build", "--bin=stacks-node"], "filter": { - "name": "stacks-inspect", "kind": "bin" } }, - "args": [], + "args": ["mockamoto"], "cwd": "${workspaceFolder}" }, { diff --git a/stackslib/src/chainstate/nakamoto/mod.rs b/stackslib/src/chainstate/nakamoto/mod.rs index 72c797cb027..a1822934242 100644 --- a/stackslib/src/chainstate/nakamoto/mod.rs +++ b/stackslib/src/chainstate/nakamoto/mod.rs @@ -387,6 +387,55 @@ impl NakamotoBlock { } impl NakamotoChainState { + pub fn get_chain_tip( + chainstate_conn: &Connection, + sortition_tip: &ConsensusHash, + sortition_db_conn: &Connection, + ) -> Result<(BlockHeaderHash, ConsensusHash), ChainstateError> { + let block_sql = + "SELECT block_hash, consensus_hash, block_height FROM nakamoto_block_headers + WHERE consensus_hash = ? + UNION + SELECT block_hash, consensus_hash, block_height FROM block_headers + WHERE consensus_hash = ? + ORDER BY block_height DESC LIMIT 1"; + let mut cur_sortition_tip = sortition_tip.clone(); + loop { + if let Some(tip_pair) = chainstate_conn + .query_row( + block_sql, + params![&cur_sortition_tip, &cur_sortition_tip], + |row| { + let block_header_hash = row.get(0)?; + let consensus_hash = row.get(1)?; + Ok((block_header_hash, consensus_hash)) + }, + ) + .optional()? + { + return Ok(tip_pair); + } else { + // no blocks are processed that were produced at `cur_sortition_tip`, check `cur_sortition_tip`'s parent. + let parent_sortition_id = SortitionDB::get_block_snapshot_consensus( + sortition_db_conn, + &cur_sortition_tip, + )? + .ok_or_else(|| ChainstateError::NoSuchBlockError)? + .parent_sortition_id; + + cur_sortition_tip = + SortitionDB::get_block_snapshot(sortition_db_conn, &parent_sortition_id)? + .ok_or_else(|| ChainstateError::NoSuchBlockError)? + .consensus_hash; + + // this is true at the "genesis" sortition: this means that there is no stacks block produced yet. + if cur_sortition_tip == FIRST_BURNCHAIN_CONSENSUS_HASH { + return Err(ChainstateError::NoSuchBlockError); + } + } + } + } + /// Notify the staging database that a given stacks block has been processed. /// This will update the attachable status for children blocks, as well as marking the stacks /// block itself as processed. @@ -798,7 +847,7 @@ impl NakamotoChainState { parent_block_id, tenure_height, tenure_changed) - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20, ?21, ?22, ?23, ?24, ?25)", + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20, ?21, ?22, ?23, ?24, ?25, ?26)", args )?; @@ -1000,11 +1049,12 @@ impl NakamotoChainState { clarity_instance: &'a mut ClarityInstance, sortition_dbconn: &'b dyn SortitionDBRef, pox_constants: &PoxConstants, - chain_tip: &StacksHeaderInfo, burn_view: BurnchainHeaderHash, burn_view_height: u32, parent_consensus_hash: ConsensusHash, parent_header_hash: BlockHeaderHash, + parent_stacks_height: u64, + parent_burn_height: u32, mainnet: bool, miner_id_opt: Option, tenure_changed: bool, @@ -1014,7 +1064,7 @@ impl NakamotoChainState { let parent_sortition_id = sortition_dbconn .get_sortition_id_from_consensus_hash(&parent_consensus_hash) .expect("Failed to get parent SortitionID from ConsensusHash"); - let tip_index_hash = chain_tip.index_block_hash(); + let tip_index_hash = StacksBlockId::new(&parent_consensus_hash, &parent_header_hash); // find matured miner rewards, so we can grant them within the Clarity DB tx. let matured_rewards_pair = if !tenure_changed { @@ -1091,7 +1141,7 @@ impl NakamotoChainState { StacksChainState::find_mature_miner_rewards( &mut clarity_tx, sortition_dbconn.sqlite_conn(), - &chain_tip, + parent_stacks_height, latest_matured_miners, matured_miner_parent, ) @@ -1134,9 +1184,9 @@ impl NakamotoChainState { StacksChainState::process_epoch_transition(&mut clarity_tx, burn_view_height)?; debug!( - "Setup block: Processed epoch transition at {}/{}", - &chain_tip.consensus_hash, - &chain_tip.anchored_header.block_hash() + "Setup block: Processed epoch transition"; + "parent_consensus_hash" => %parent_consensus_hash, + "parent_header_hash" => %parent_header_hash, ); let evaluated_epoch = clarity_tx.get_epoch(); @@ -1147,13 +1197,13 @@ impl NakamotoChainState { sortition_dbconn.as_burn_state_db(), sortition_dbconn, &mut clarity_tx, - chain_tip, + parent_burn_height, &parent_sortition_id, )?; debug!( - "Setup block: Processed unlock events at {}/{}", - &chain_tip.consensus_hash, - &chain_tip.anchored_header.block_hash() + "Setup block: Processed unlock events"; + "parent_consensus_hash" => %parent_consensus_hash, + "parent_header_hash" => %parent_header_hash, ); unlock_events } else { @@ -1168,20 +1218,16 @@ impl NakamotoChainState { stacking_burn_ops.clone(), active_pox_contract, )); - debug!( - "Setup block: Processed burnchain stacking ops for {}/{}", - &chain_tip.consensus_hash, - &chain_tip.anchored_header.block_hash() - ); tx_receipts.extend(StacksChainState::process_transfer_ops( &mut clarity_tx, transfer_burn_ops.clone(), )); debug!( - "Setup block: Processed burnchain transfer ops for {}/{}", - &chain_tip.consensus_hash, - &chain_tip.anchored_header.block_hash() + "Setup block: Processed burnchain stacking and transfer ops"; + "parent_consensus_hash" => %parent_consensus_hash, + "parent_header_hash" => %parent_header_hash, ); + // DelegateStx ops are allowed from epoch 2.1 onward. // The query for the delegate ops only returns anything in and after Epoch 2.1, // but we do a second check here just to be safe. @@ -1192,17 +1238,18 @@ impl NakamotoChainState { active_pox_contract, )); debug!( - "Setup block: Processed burnchain delegate ops for {}/{}", - &chain_tip.consensus_hash, - &chain_tip.anchored_header.block_hash() + "Setup block: Processed burnchain delegate ops"; + "parent_consensus_hash" => %parent_consensus_hash, + "parent_header_hash" => %parent_header_hash, ); } debug!( - "Setup block: ready to go for {}/{}", - &chain_tip.consensus_hash, - &chain_tip.anchored_header.block_hash() + "Setup block: completed setup"; + "parent_consensus_hash" => %parent_consensus_hash, + "parent_header_hash" => %parent_header_hash, ); + Ok(SetupBlockResult { clarity_tx, tx_receipts, @@ -1216,7 +1263,7 @@ impl NakamotoChainState { }) } - fn append_block<'a>( + pub fn append_block<'a>( chainstate_tx: &mut ChainstateTx, clarity_instance: &'a mut ClarityInstance, burn_dbconn: &mut SortitionHandleTx, @@ -1321,13 +1368,14 @@ impl NakamotoChainState { clarity_instance, burn_dbconn, pox_constants, - &parent_chain_tip, burn_view_hash, burn_view_height.try_into().map_err(|_| { ChainstateError::InvalidStacksBlock("Burn block height exceeded u32".into()) })?, parent_ch, parent_block_hash, + parent_chain_tip.stacks_block_height, + parent_chain_tip.burn_header_height, mainnet, None, tenure_changed, diff --git a/stackslib/src/chainstate/stacks/db/accounts.rs b/stackslib/src/chainstate/stacks/db/accounts.rs index bc97c20130e..294036ea901 100644 --- a/stackslib/src/chainstate/stacks/db/accounts.rs +++ b/stackslib/src/chainstate/stacks/db/accounts.rs @@ -991,17 +991,17 @@ impl StacksChainState { pub fn find_mature_miner_rewards( clarity_tx: &mut ClarityTx, sortdb_conn: &Connection, - tip: &StacksHeaderInfo, + tip_stacks_height: u64, mut latest_matured_miners: Vec, parent_miner: MinerPaymentSchedule, ) -> Result, MinerReward, MinerRewardInfo)>, Error> { let mainnet = clarity_tx.config.mainnet; - if tip.stacks_block_height <= MINER_REWARD_MATURITY { + if tip_stacks_height <= MINER_REWARD_MATURITY { // no mature rewards exist return Ok(None); } - let reward_height = tip.stacks_block_height - MINER_REWARD_MATURITY; + let reward_height = tip_stacks_height - MINER_REWARD_MATURITY; assert!(latest_matured_miners.len() > 0); assert!(latest_matured_miners[0].vtxindex == 0); diff --git a/stackslib/src/chainstate/stacks/db/blocks.rs b/stackslib/src/chainstate/stacks/db/blocks.rs index b2857231aec..abd565ebeb8 100644 --- a/stackslib/src/chainstate/stacks/db/blocks.rs +++ b/stackslib/src/chainstate/stacks/db/blocks.rs @@ -4607,6 +4607,7 @@ impl StacksChainState { current_epoch = StacksEpochId::Epoch24; } StacksEpochId::Epoch24 => { + receipts.append(&mut clarity_tx.block.initialize_epoch_3_0()?); current_epoch = StacksEpochId::Epoch30; } StacksEpochId::Epoch30 => { @@ -5226,7 +5227,7 @@ impl StacksChainState { burn_dbconn: &dyn BurnStateDB, sortition_dbconn: &dyn SortitionDBRef, clarity_tx: &mut ClarityTx, - chain_tip: &StacksHeaderInfo, + chain_tip_burn_header_height: u32, parent_sortition_id: &SortitionId, ) -> Result, Error> { let pox_reward_cycle = Burnchain::static_block_height_to_reward_cycle( @@ -5265,7 +5266,7 @@ impl StacksChainState { let pox_start_cycle_info = sortition_dbconn.get_pox_start_cycle_info( parent_sortition_id, - chain_tip.burn_header_height.into(), + chain_tip_burn_header_height.into(), pox_reward_cycle, )?; debug!("check_and_handle_reward_start: got pox reward cycle info"); @@ -5405,7 +5406,7 @@ impl StacksChainState { let matured_miner_rewards_opt = match StacksChainState::find_mature_miner_rewards( &mut clarity_tx, conn, - &chain_tip, + chain_tip.stacks_block_height, latest_matured_miners, matured_miner_parent, ) { @@ -5494,7 +5495,7 @@ impl StacksChainState { burn_dbconn, sortition_dbconn, &mut clarity_tx, - chain_tip, + chain_tip.burn_header_height, &parent_sortition_id, )?; debug!( diff --git a/stackslib/src/chainstate/stacks/mod.rs b/stackslib/src/chainstate/stacks/mod.rs index 282a229b39b..5e518d1e711 100644 --- a/stackslib/src/chainstate/stacks/mod.rs +++ b/stackslib/src/chainstate/stacks/mod.rs @@ -709,9 +709,7 @@ impl TransactionPayload { } } -#[repr(u8)] -#[derive(Debug, Clone, PartialEq, Copy, Serialize, Deserialize)] -pub enum TransactionPayloadID { +define_u8_enum!(TransactionPayloadID { TokenTransfer = 0, SmartContract = 1, ContractCall = 2, @@ -719,8 +717,8 @@ pub enum TransactionPayloadID { Coinbase = 4, CoinbaseToAltRecipient = 5, VersionedSmartContract = 6, - TenureChange = 7, -} + TenureChange = 7 +}); /// Encoding of an asset type identifier #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] diff --git a/stackslib/src/chainstate/stacks/transaction.rs b/stackslib/src/chainstate/stacks/transaction.rs index cc2ce7598bd..bf1e42150bc 100644 --- a/stackslib/src/chainstate/stacks/transaction.rs +++ b/stackslib/src/chainstate/stacks/transaction.rs @@ -25,8 +25,8 @@ use clarity::vm::types::{QualifiedContractIdentifier, StandardPrincipalData}; use clarity::vm::ClarityVersion; use clarity::vm::{SymbolicExpression, SymbolicExpressionType, Value}; use stacks_common::types::StacksPublicKeyBuffer; -use stacks_common::util::hash::to_hex; use stacks_common::util::hash::Sha512Trunc256Sum; +use stacks_common::util::hash::{to_hex, MerkleHashFunc, MerkleTree}; use stacks_common::util::retry::BoundReader; use stacks_common::util::secp256k1::MessageSignature; @@ -242,28 +242,33 @@ impl StacksMessageCodec for TransactionPayload { } fn consensus_deserialize(fd: &mut R) -> Result { - let type_id: u8 = read_next(fd)?; + let type_id_u8 = read_next(fd)?; + let type_id = TransactionPayloadID::from_u8(type_id_u8).ok_or_else(|| { + codec_error::DeserializeError(format!( + "Failed to parse transaction -- unknown payload ID {type_id_u8}" + )) + })?; let payload = match type_id { - x if x == TransactionPayloadID::TokenTransfer as u8 => { + TransactionPayloadID::TokenTransfer => { let principal = read_next(fd)?; let amount = read_next(fd)?; let memo = read_next(fd)?; TransactionPayload::TokenTransfer(principal, amount, memo) } - x if x == TransactionPayloadID::ContractCall as u8 => { + TransactionPayloadID::ContractCall => { let payload: TransactionContractCall = read_next(fd)?; TransactionPayload::ContractCall(payload) } - x if x == TransactionPayloadID::SmartContract as u8 => { + TransactionPayloadID::SmartContract => { let payload: TransactionSmartContract = read_next(fd)?; TransactionPayload::SmartContract(payload, None) } - x if x == TransactionPayloadID::VersionedSmartContract as u8 => { + TransactionPayloadID::VersionedSmartContract => { let version = ClarityVersion_consensus_deserialize(fd)?; let payload: TransactionSmartContract = read_next(fd)?; TransactionPayload::SmartContract(payload, Some(version)) } - x if x == TransactionPayloadID::PoisonMicroblock as u8 => { + TransactionPayloadID::PoisonMicroblock => { let h1: StacksMicroblockHeader = read_next(fd)?; let h2: StacksMicroblockHeader = read_next(fd)?; @@ -284,11 +289,11 @@ impl StacksMessageCodec for TransactionPayload { TransactionPayload::PoisonMicroblock(h1, h2) } - x if x == TransactionPayloadID::Coinbase as u8 => { + TransactionPayloadID::Coinbase => { let payload: CoinbasePayload = read_next(fd)?; TransactionPayload::Coinbase(payload, None) } - x if x == TransactionPayloadID::CoinbaseToAltRecipient as u8 => { + TransactionPayloadID::CoinbaseToAltRecipient => { let payload: CoinbasePayload = read_next(fd)?; let principal_value: Value = read_next(fd)?; let recipient = match principal_value { @@ -300,18 +305,26 @@ impl StacksMessageCodec for TransactionPayload { TransactionPayload::Coinbase(payload, Some(recipient)) } - _ => { - return Err(codec_error::DeserializeError(format!( - "Failed to parse transaction -- unknown payload ID {}", - type_id - ))); - } + TransactionPayloadID::TenureChange => TransactionPayload::TenureChange(read_next(fd)?), }; Ok(payload) } } +impl<'a, H> FromIterator<&'a StacksTransaction> for MerkleTree +where + H: MerkleHashFunc + Clone + PartialEq + fmt::Debug, +{ + fn from_iter>(iter: T) -> Self { + let txid_vec = iter + .into_iter() + .map(|x| x.txid().as_bytes().to_vec()) + .collect(); + MerkleTree::new(&txid_vec) + } +} + impl TransactionPayload { pub fn new_contract_call( contract_address: StacksAddress, diff --git a/stackslib/src/clarity_vm/clarity.rs b/stackslib/src/clarity_vm/clarity.rs index aa8a5052294..d6123e160a0 100644 --- a/stackslib/src/clarity_vm/clarity.rs +++ b/stackslib/src/clarity_vm/clarity.rs @@ -1277,6 +1277,32 @@ impl<'a, 'b> ClarityBlockConnection<'a, 'b> { }) } + pub fn initialize_epoch_3_0(&mut self) -> Result, Error> { + // use the `using!` statement to ensure that the old cost_tracker is placed + // back in all branches after initialization + using!(self.cost_track, "cost tracker", |old_cost_tracker| { + // epoch initialization is *free*. + // NOTE: this also means that cost functions won't be evaluated. + self.cost_track.replace(LimitedCostTracker::new_free()); + self.epoch = StacksEpochId::Epoch30; + self.as_transaction(|tx_conn| { + // bump the epoch in the Clarity DB + tx_conn + .with_clarity_db(|db| { + db.set_clarity_epoch_version(StacksEpochId::Epoch30); + Ok(()) + }) + .unwrap(); + + // require 3.0 rules henceforth in this connection as well + tx_conn.epoch = StacksEpochId::Epoch30; + }); + + debug!("Epoch 3.0 initialized"); + (old_cost_tracker, Ok(vec![])) + }) + } + pub fn start_transaction_processing<'c>(&'c mut self) -> ClarityTransactionConnection<'c, 'a> { let store = &mut self.datastore; let cost_track = &mut self.cost_track; diff --git a/testnet/stacks-node/Cargo.toml b/testnet/stacks-node/Cargo.toml index 84425e5a30c..5a5db4e0e63 100644 --- a/testnet/stacks-node/Cargo.toml +++ b/testnet/stacks-node/Cargo.toml @@ -43,7 +43,7 @@ tracing = "0.1.37" tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } wsts = { git = "https://github.com/Trust-Machines/wsts", tag = "4.0.0rc2" } -[dev-dependencies.rusqlite] +[dependencies.rusqlite] version = "=0.24.2" features = ["blob", "serde_json", "i128_blob", "bundled", "trace"] diff --git a/testnet/stacks-node/src/config.rs b/testnet/stacks-node/src/config.rs index 4420c363063..65327ed5a12 100644 --- a/testnet/stacks-node/src/config.rs +++ b/testnet/stacks-node/src/config.rs @@ -268,6 +268,39 @@ impl ConfigFile { } } + pub fn mockamoto() -> ConfigFile { + let burnchain = BurnchainConfigFile { + mode: Some("mockamoto".into()), + rpc_port: Some(8332), + peer_port: Some(8333), + peer_host: Some("localhost".into()), + username: Some("blockstack".into()), + password: Some("blockstacksystem".into()), + magic_bytes: Some("M3".into()), + ..BurnchainConfigFile::default() + }; + + let node = NodeConfigFile { + bootstrap_node: None, + miner: Some(true), + ..NodeConfigFile::default() + }; + + let mining_key = Secp256k1PrivateKey::new(); + let miner = MinerConfigFile { + mining_key: Some(mining_key.to_hex()), + ..MinerConfigFile::default() + }; + + ConfigFile { + burnchain: Some(burnchain), + node: Some(node), + ustx_balance: None, + miner: Some(miner), + ..ConfigFile::default() + } + } + pub fn helium() -> ConfigFile { // ## Settings for local testnet, relying on a local bitcoind server // ## running with the following bitcoin.conf: @@ -922,12 +955,24 @@ impl Config { unprocessed_block_deadline_secs: miner .unprocessed_block_deadline_secs .unwrap_or(miner_default_config.unprocessed_block_deadline_secs), + mining_key: miner + .mining_key + .as_ref() + .map(|x| Secp256k1PrivateKey::from_hex(x)) + .transpose()?, }, None => miner_default_config, }; let supported_modes = vec![ - "mocknet", "helium", "neon", "argon", "krypton", "xenon", "mainnet", + "mocknet", + "helium", + "neon", + "argon", + "krypton", + "xenon", + "mainnet", + "mockamoto", ]; if !supported_modes.contains(&burnchain.mode.as_str()) { @@ -1447,7 +1492,7 @@ impl BurnchainConfig { match self.mode.as_str() { "mainnet" => ("mainnet".to_string(), BitcoinNetworkType::Mainnet), "xenon" => ("testnet".to_string(), BitcoinNetworkType::Testnet), - "helium" | "neon" | "argon" | "krypton" | "mocknet" => { + "helium" | "neon" | "argon" | "krypton" | "mocknet" | "mockamoto" => { ("regtest".to_string(), BitcoinNetworkType::Regtest) } _ => panic!("Invalid bitcoin mode -- expected mainnet, testnet, or regtest"), @@ -1925,6 +1970,7 @@ pub struct MinerConfig { pub nonce_cache_size: u64, pub candidate_retry_cache_size: u64, pub unprocessed_block_deadline_secs: u64, + pub mining_key: Option, } impl MinerConfig { @@ -1941,6 +1987,7 @@ impl MinerConfig { nonce_cache_size: 10_000, candidate_retry_cache_size: 10_000, unprocessed_block_deadline_secs: 30, + mining_key: None, } } } @@ -2045,6 +2092,7 @@ pub struct MinerConfigFile { pub nonce_cache_size: Option, pub candidate_retry_cache_size: Option, pub unprocessed_block_deadline_secs: Option, + pub mining_key: Option, } #[derive(Clone, Deserialize, Default, Debug)] diff --git a/testnet/stacks-node/src/main.rs b/testnet/stacks-node/src/main.rs index d6e4503e368..60bd4be7ed8 100644 --- a/testnet/stacks-node/src/main.rs +++ b/testnet/stacks-node/src/main.rs @@ -20,6 +20,7 @@ pub mod config; pub mod event_dispatcher; pub mod genesis_data; pub mod keychain; +pub mod mockamoto; pub mod neon_node; pub mod node; pub mod operations; @@ -35,6 +36,8 @@ use std::process; use backtrace::Backtrace; use pico_args::Arguments; +use crate::mockamoto::MockamotoNode; + pub use self::burnchains::{ BitcoinRegtestController, BurnchainController, BurnchainTip, MocknetController, }; @@ -101,6 +104,10 @@ fn main() { args.finish().unwrap(); ConfigFile::mainnet() } + "mockamoto" => { + args.finish().unwrap(); + ConfigFile::mockamoto() + } "check-config" => { let config_path: String = args.value_from_str("--config").unwrap(); args.finish().unwrap(); @@ -202,6 +209,9 @@ fn main() { { let mut run_loop = neon::RunLoop::new(conf); run_loop.start(None, mine_start.unwrap_or(0)); + } else if conf.burnchain.mode == "mockamoto" { + let mut mockamoto = MockamotoNode::new(&conf).unwrap(); + mockamoto.run(); } else { println!("Burnchain mode '{}' not supported", conf.burnchain.mode); } diff --git a/testnet/stacks-node/src/mockamoto.rs b/testnet/stacks-node/src/mockamoto.rs new file mode 100644 index 00000000000..6b0e52cc7bc --- /dev/null +++ b/testnet/stacks-node/src/mockamoto.rs @@ -0,0 +1,584 @@ +use std::sync::atomic::AtomicBool; +use std::sync::mpsc::sync_channel; +use std::sync::mpsc::Receiver; +use std::sync::mpsc::SyncSender; +use std::sync::Arc; +use std::sync::Mutex; +use std::thread; +use std::thread::sleep; +use std::time::Duration; + +use clarity::vm::ast::ASTRules; +use stacks::burnchains::Txid; +use stacks::chainstate::burn::db::sortdb::SortitionDB; +use stacks::chainstate::burn::BlockSnapshot; +use stacks::chainstate::burn::OpsHash; +use stacks::chainstate::burn::SortitionHash; +use stacks::chainstate::coordinator::comm::CoordinatorChannels; +use stacks::chainstate::coordinator::comm::CoordinatorReceivers; +use stacks::chainstate::coordinator::CoordinatorCommunication; +use stacks::chainstate::nakamoto::NakamotoBlock; +use stacks::chainstate::nakamoto::NakamotoBlockHeader; +use stacks::chainstate::nakamoto::NakamotoChainState; +use stacks::chainstate::nakamoto::SetupBlockResult; +use stacks::chainstate::stacks::db::ChainStateBootData; +use stacks::chainstate::stacks::db::StacksChainState; +use stacks::chainstate::stacks::miner::MinerStatus; +use stacks::chainstate::stacks::CoinbasePayload; +use stacks::chainstate::stacks::Error as ChainstateError; +use stacks::chainstate::stacks::SchnorrThresholdSignature; +use stacks::chainstate::stacks::StacksTransaction; +use stacks::chainstate::stacks::StacksTransactionSigner; +use stacks::chainstate::stacks::TenureChangeCause; +use stacks::chainstate::stacks::TenureChangePayload; +use stacks::chainstate::stacks::TransactionAuth; +use stacks::chainstate::stacks::TransactionPayload; +use stacks::chainstate::stacks::TransactionVersion; +use stacks::chainstate::stacks::MINER_BLOCK_CONSENSUS_HASH; +use stacks::chainstate::stacks::MINER_BLOCK_HEADER_HASH; +use stacks::clarity_vm::database::SortitionDBRef; +use stacks::core::StacksEpoch; +use stacks::core::BLOCK_LIMIT_MAINNET_10; +use stacks::core::HELIUM_BLOCK_LIMIT_20; +use stacks::core::PEER_VERSION_EPOCH_1_0; +use stacks::core::PEER_VERSION_EPOCH_2_0; +use stacks::core::PEER_VERSION_EPOCH_2_05; +use stacks::core::PEER_VERSION_EPOCH_2_1; +use stacks::core::PEER_VERSION_EPOCH_2_2; +use stacks::core::PEER_VERSION_EPOCH_2_3; +use stacks::core::PEER_VERSION_EPOCH_2_4; +use stacks::net::relay::Relayer; +use stacks::net::stackerdb::StackerDBs; +use stacks_common::consts::FIRST_BURNCHAIN_CONSENSUS_HASH; +use stacks_common::consts::FIRST_STACKS_BLOCK_HASH; +use stacks_common::consts::STACKS_EPOCH_MAX; +use stacks_common::types::chainstate::BlockHeaderHash; +use stacks_common::types::chainstate::BurnchainHeaderHash; +use stacks_common::types::chainstate::ConsensusHash; +use stacks_common::types::chainstate::PoxId; +use stacks_common::types::chainstate::SortitionId; +use stacks_common::types::chainstate::StacksBlockId; +use stacks_common::types::chainstate::StacksPrivateKey; +use stacks_common::types::chainstate::TrieHash; +use stacks_common::types::PrivateKey; +use stacks_common::types::StacksEpochId; +use stacks_common::util::hash::Hash160; +use stacks_common::util::hash::MerkleTree; +use stacks_common::util::hash::Sha512Trunc256Sum; +use stacks_common::util::secp256k1::MessageSignature; +use stacks_common::util::secp256k1::Secp256k1PublicKey; + +use crate::event_dispatcher; +use crate::neon::Counters; +use crate::neon_node::Globals; +use crate::neon_node::PeerThread; +use crate::neon_node::RelayerDirective; +use crate::neon_node::StacksNode; +use crate::neon_node::BLOCK_PROCESSOR_STACK_SIZE; +use crate::syncctl::PoxSyncWatchdogComms; +use crate::Config; +use crate::EventDispatcher; +use lazy_static::lazy_static; + +lazy_static! { + pub static ref STACKS_EPOCHS_MOCKAMOTO: [StacksEpoch; 8] = [ + StacksEpoch { + epoch_id: StacksEpochId::Epoch10, + start_height: 0, + end_height: 0, + block_limit: BLOCK_LIMIT_MAINNET_10.clone(), + network_epoch: PEER_VERSION_EPOCH_1_0 + }, + StacksEpoch { + epoch_id: StacksEpochId::Epoch20, + start_height: 0, + end_height: 1, + block_limit: HELIUM_BLOCK_LIMIT_20.clone(), + network_epoch: PEER_VERSION_EPOCH_2_0 + }, + StacksEpoch { + epoch_id: StacksEpochId::Epoch2_05, + start_height: 1, + end_height: 2, + block_limit: HELIUM_BLOCK_LIMIT_20.clone(), + network_epoch: PEER_VERSION_EPOCH_2_05 + }, + StacksEpoch { + epoch_id: StacksEpochId::Epoch21, + start_height: 2, + end_height: 3, + block_limit: HELIUM_BLOCK_LIMIT_20.clone(), + network_epoch: PEER_VERSION_EPOCH_2_1 + }, + StacksEpoch { + epoch_id: StacksEpochId::Epoch22, + start_height: 3, + end_height: 4, + block_limit: HELIUM_BLOCK_LIMIT_20.clone(), + network_epoch: PEER_VERSION_EPOCH_2_2 + }, + StacksEpoch { + epoch_id: StacksEpochId::Epoch23, + start_height: 4, + end_height: 5, + block_limit: HELIUM_BLOCK_LIMIT_20.clone(), + network_epoch: PEER_VERSION_EPOCH_2_3 + }, + StacksEpoch { + epoch_id: StacksEpochId::Epoch24, + start_height: 5, + end_height: 6, + block_limit: HELIUM_BLOCK_LIMIT_20.clone(), + network_epoch: PEER_VERSION_EPOCH_2_4 + }, + StacksEpoch { + epoch_id: StacksEpochId::Epoch30, + start_height: 6, + end_height: STACKS_EPOCH_MAX, + block_limit: HELIUM_BLOCK_LIMIT_20.clone(), + network_epoch: PEER_VERSION_EPOCH_2_4 + }, + ]; +} + +fn make_snapshot( + parent_snapshot: &BlockSnapshot, + miner_pkh: &Hash160, +) -> Result { + let burn_height = parent_snapshot.block_height + 1; + let mut mock_burn_hash_contents = [0u8; 32]; + mock_burn_hash_contents[0..8].copy_from_slice((burn_height + 1).to_be_bytes().as_ref()); + let mut mock_consensus_hash_contents = [0u8; 20]; + mock_consensus_hash_contents[0..8].copy_from_slice((burn_height + 1).to_be_bytes().as_ref()); + + let new_bhh = BurnchainHeaderHash(mock_burn_hash_contents); + let new_ch = ConsensusHash(mock_consensus_hash_contents); + let new_sh = SortitionHash([1; 32]); + + let new_snapshot = BlockSnapshot { + block_height: burn_height, + burn_header_timestamp: 100 * u64::from(burn_height + 1), + burn_header_hash: new_bhh.clone(), + parent_burn_header_hash: parent_snapshot.burn_header_hash.clone(), + consensus_hash: new_ch.clone(), + ops_hash: OpsHash([0; 32]), + total_burn: 10, + sortition: true, + sortition_hash: new_sh, + winning_block_txid: Txid([0; 32]), + winning_stacks_block_hash: BlockHeaderHash([0; 32]), + index_root: TrieHash([0; 32]), + num_sortitions: parent_snapshot.num_sortitions + 1, + stacks_block_accepted: true, + stacks_block_height: 1, + arrival_index: parent_snapshot.arrival_index + 1, + canonical_stacks_tip_height: 1, + canonical_stacks_tip_hash: BlockHeaderHash([0; 32]), + canonical_stacks_tip_consensus_hash: new_ch.clone(), + sortition_id: SortitionId::new(&new_bhh.clone(), &PoxId::new(vec![true])), + parent_sortition_id: parent_snapshot.sortition_id.clone(), + pox_valid: true, + accumulated_coinbase_ustx: 0, + miner_pk_hash: Some(miner_pkh.clone()), + }; + Ok(new_snapshot) +} + +/// This struct wraps all the state required for operating a +/// stacks-node in `mockamoto` mode. +/// +/// This mode of operation is a single-node network in which bitcoin +/// blocks are simulated: no `bitcoind` is communicated with (either +/// operating as regtest, testnet or mainnet). This operation mode +/// is useful for testing the stacks-only operation of Nakamoto. +/// +/// The current implementation of the mockamoto node simply produces +/// Nakamoto blocks containing *only* coinbase and tenure-change +/// transactions. As the implementation of Nakamoto progresses, and +/// the mockamoto mode merges with changes to the chains coordinator, +/// the mockamoto node will support mining of transactions and event +/// emission. +/// +pub struct MockamotoNode { + sortdb: SortitionDB, + chainstate: StacksChainState, + miner_key: StacksPrivateKey, + relay_rcv: Receiver, + coord_rcv: CoordinatorReceivers, + globals: Globals, + config: Config, +} + +impl MockamotoNode { + pub fn new(config: &Config) -> Result { + let miner_key = config + .miner + .mining_key + .clone() + .ok_or("Mockamoto node must be configured with `miner.mining_key`")?; + + let burnchain = config.get_burnchain(); + let (sortdb, _burndb) = burnchain + .connect_db( + true, + BurnchainHeaderHash([0; 32]), + 100, + STACKS_EPOCHS_MOCKAMOTO.to_vec(), + ) + .map_err(|e| e.to_string())?; + + let mut boot_data = ChainStateBootData::new(&burnchain, vec![], None); + let (chainstate, _) = StacksChainState::open_and_exec( + config.is_mainnet(), + config.burnchain.chain_id, + &config.get_chainstate_path_str(), + Some(&mut boot_data), + Some(config.node.get_marf_opts()), + ) + .unwrap(); + + let (coord_rcv, coord_comms) = CoordinatorCommunication::instantiate(); + let miner_status = Arc::new(Mutex::new(MinerStatus::make_ready(100))); + let (relay_send, relay_rcv) = sync_channel(10); + let counters = Counters::new(); + let should_keep_running = Arc::new(AtomicBool::new(true)); + let sync_comms = PoxSyncWatchdogComms::new(should_keep_running.clone()); + + let globals = Globals::new( + coord_comms, + miner_status, + relay_send, + counters, + sync_comms, + should_keep_running, + ); + + Ok(MockamotoNode { + sortdb, + chainstate, + miner_key, + relay_rcv, + coord_rcv, + globals, + config: config.clone(), + }) + } + + pub fn run(&mut self) { + info!("Starting a burn cycle"); + self.produce_burnchain_block().unwrap(); + self.produce_burnchain_block().unwrap(); + self.produce_burnchain_block().unwrap(); + self.produce_burnchain_block().unwrap(); + + let mut p2p_net = StacksNode::setup_peer_network( + &self.config, + &self.config.atlas, + self.config.get_burnchain(), + ); + + let stackerdbs = StackerDBs::connect(&self.config.get_stacker_db_file_path(), true) + .expect("FATAL: failed to connect to stacker DB"); + + let relayer = Relayer::from_p2p(&mut p2p_net, stackerdbs); + + let peer_thread = PeerThread::new_all( + self.globals.clone(), + &self.config, + self.config.get_burnchain().pox_constants, + p2p_net, + ); + let ev_dispatcher = EventDispatcher::new(); + + let _peer_thread = thread::Builder::new() + .stack_size(BLOCK_PROCESSOR_STACK_SIZE) + .name("p2p".into()) + .spawn(move || { + StacksNode::p2p_main(peer_thread, ev_dispatcher); + }) + .expect("FATAL: failed to start p2p thread"); + + loop { + info!("Starting a burn cycle"); + self.produce_burnchain_block().unwrap(); + info!("Produced a burn block"); + sleep(Duration::from_millis(100)); + info!("Mining a staging block"); + self.mine_and_stage_block().unwrap(); + info!("Processing a staging block"); + self.process_staging_block().unwrap(); + info!("Cycle done"); + sleep(Duration::from_secs(5)); + } + } + + fn produce_burnchain_block(&mut self) -> Result<(), ChainstateError> { + let miner_pk = Secp256k1PublicKey::from_private(&self.miner_key); + let miner_pk_hash = Hash160::from_node_public_key(&miner_pk); + + let parent_snapshot = SortitionDB::get_canonical_burn_chain_tip(&self.sortdb.conn())?; + let new_snapshot = make_snapshot(&parent_snapshot, &miner_pk_hash)?; + let mut sortdb_tx = self.sortdb.tx_handle_begin(&parent_snapshot.sortition_id)?; + + sortdb_tx.append_chain_tip_snapshot( + &parent_snapshot, + &new_snapshot, + &vec![], + &vec![], + None, + None, + None, + )?; + + sortdb_tx.commit()?; + + let staging_db_tx = self.chainstate.db_tx_begin()?; + NakamotoChainState::set_burn_block_processed( + &staging_db_tx, + &new_snapshot.burn_header_hash, + )?; + staging_db_tx.commit()?; + + Ok(()) + } + + fn mine_stacks_block(&mut self) -> Result { + let sortition_tip = SortitionDB::get_canonical_burn_chain_tip(self.sortdb.conn())?; + let chain_id = self.chainstate.chain_id; + let (mut chainstate_tx, clarity_instance) = self.chainstate.chainstate_tx_begin().unwrap(); + + let (is_genesis, chain_tip_bh, chain_tip_ch) = match NakamotoChainState::get_chain_tip( + &chainstate_tx, + &sortition_tip.consensus_hash, + self.sortdb.conn(), + ) { + Ok(chain_tip) => (false, chain_tip.0, chain_tip.1), + Err(ChainstateError::NoSuchBlockError) => + // No stacks tip yet, parent should be genesis + { + ( + true, + FIRST_STACKS_BLOCK_HASH, + FIRST_BURNCHAIN_CONSENSUS_HASH, + ) + } + Err(e) => return Err(e), + }; + + let (parent_chain_length, parent_burn_height) = if is_genesis { + (0, 0) + } else { + let tip_block_id = StacksBlockId::new(&chain_tip_ch, &chain_tip_bh); + let tip_info = NakamotoChainState::get_block_header(&chainstate_tx, &tip_block_id)? + .ok_or(ChainstateError::NoSuchBlockError)?; + (tip_info.stacks_block_height, tip_info.burn_header_height) + }; + + let miner_nonce = 2 * parent_chain_length; + + let coinbase_tx_payload = TransactionPayload::Coinbase(CoinbasePayload([1; 32]), None); + let mut coinbase_tx = StacksTransaction::new( + TransactionVersion::Testnet, + TransactionAuth::from_p2pkh(&self.miner_key).unwrap(), + coinbase_tx_payload, + ); + coinbase_tx.chain_id = chain_id; + coinbase_tx.set_origin_nonce(miner_nonce); + let mut coinbase_tx_signer = StacksTransactionSigner::new(&coinbase_tx); + coinbase_tx_signer.sign_origin(&self.miner_key).unwrap(); + let coinbase_tx = coinbase_tx_signer.get_tx().unwrap(); + + let parent_block_id = StacksBlockId::new(&chain_tip_ch, &chain_tip_bh); + // Add a tenure change transaction to the block: + // as of now every mockamoto block is a tenure-change. + // If mockamoto mode changes to support non-tenure-changing blocks, this will have + // to be gated. + let tenure_change_tx_payload = TransactionPayload::TenureChange(TenureChangePayload { + previous_tenure_end: parent_block_id, + previous_tenure_blocks: 1, + cause: TenureChangeCause::BlockFound, + pubkey_hash: Hash160([0; 20]), + signature: SchnorrThresholdSignature {}, + signers: vec![], + }); + let mut tenure_tx = StacksTransaction::new( + TransactionVersion::Testnet, + TransactionAuth::from_p2pkh(&self.miner_key).unwrap(), + tenure_change_tx_payload, + ); + tenure_tx.chain_id = chain_id; + tenure_tx.set_origin_nonce(miner_nonce + 1); + let txid = tenure_tx.txid(); + let mut tenure_tx_signer = StacksTransactionSigner::new(&tenure_tx); + tenure_tx_signer.sign_origin(&self.miner_key).unwrap(); + let tenure_tx = tenure_tx_signer.get_tx().unwrap(); + + let sortdb_handle = self.sortdb.index_conn(); + let SetupBlockResult { + mut clarity_tx, + mut tx_receipts, + matured_miner_rewards_opt, + evaluated_epoch, + applied_epoch_transition, + burn_stack_stx_ops, + burn_transfer_stx_ops, + mut auto_unlock_events, + burn_delegate_stx_ops, + } = NakamotoChainState::setup_block( + &mut chainstate_tx, + clarity_instance, + &sortdb_handle, + &self.sortdb.pox_constants, + sortition_tip.burn_header_hash.clone(), + sortition_tip.block_height.try_into().map_err(|_| { + ChainstateError::InvalidStacksBlock("Burn block height exceeded u32".into()) + })?, + chain_tip_ch.clone(), + chain_tip_bh.clone(), + parent_chain_length, + parent_burn_height, + false, + None, + true, + parent_chain_length + 1, + )?; + + let txs = vec![coinbase_tx, tenure_tx]; + + let _ = match StacksChainState::process_block_transactions( + &mut clarity_tx, + &txs, + 0, + ASTRules::PrecheckSize, + ) { + Err(e) => { + let msg = format!("Mined invalid stacks block {e:?}"); + warn!("{msg}"); + + clarity_tx.rollback_block(); + return Err(ChainstateError::InvalidStacksBlock(msg)); + } + Ok((block_fees, _block_burns, txs_receipts)) => (block_fees, txs_receipts), + }; + + let mut lockup_events = match NakamotoChainState::finish_block( + &mut clarity_tx, + matured_miner_rewards_opt.as_ref(), + ) { + Err(ChainstateError::InvalidStacksBlock(e)) => { + clarity_tx.rollback_block(); + return Err(ChainstateError::InvalidStacksBlock(e)); + } + Err(e) => return Err(e), + Ok(lockup_events) => lockup_events, + }; + + let state_index_root = clarity_tx.seal(); + let tx_merkle_tree: MerkleTree = txs.iter().collect(); + clarity_tx.commit_mined_block(&StacksBlockId::new( + &MINER_BLOCK_CONSENSUS_HASH, + &MINER_BLOCK_HEADER_HASH, + )); + chainstate_tx.commit(); + + let mut block = NakamotoBlock { + header: NakamotoBlockHeader { + version: 100, + chain_length: parent_chain_length + 1, + burn_spent: 10, + parent: chain_tip_bh, + burn_view: sortition_tip.burn_header_hash.clone(), + tx_merkle_root: tx_merkle_tree.root(), + state_index_root, + stacker_signature: MessageSignature([0; 65]), + miner_signature: MessageSignature([0; 65]), + consensus_hash: sortition_tip.consensus_hash.clone(), + parent_consensus_hash: chain_tip_ch, + }, + txs, + }; + + let miner_signature = self + .miner_key + .sign(block.header.signature_hash().unwrap().as_bytes()) + .unwrap(); + + block.header.miner_signature = miner_signature; + + Ok(block) + } + + fn mine_and_stage_block(&mut self) -> Result<(), ChainstateError> { + let block = self.mine_stacks_block()?; + let chainstate_tx = self.chainstate.db_tx_begin()?; + let sortition_handle = self.sortdb.index_handle_at_tip(); + NakamotoChainState::accept_block(block, &sortition_handle, &chainstate_tx)?; + chainstate_tx.commit()?; + Ok(()) + } + + fn process_staging_block(&mut self) -> Result { + let (mut chainstate_tx, clarity_instance) = self.chainstate.chainstate_tx_begin()?; + let pox_constants = self.sortdb.pox_constants.clone(); + let mut sortdb_tx = self.sortdb.tx_begin_at_tip(); + let Some(next_block) = NakamotoChainState::next_ready_block(&chainstate_tx)? else { + return Ok(false); + }; + + let parent_block_id = StacksBlockId::new( + &next_block.header.parent_consensus_hash, + &next_block.header.parent, + ); + let parent_chain_tip = + NakamotoChainState::get_block_header(&chainstate_tx, &parent_block_id)?.ok_or_else( + || { + warn!( + "Tried to process next ready block, but its parent header cannot be found"; + "block_hash" => %next_block.header.block_hash(), + "consensus_hash" => %next_block.header.consensus_hash, + "parent_block_id" => %parent_block_id + ); + ChainstateError::NoSuchBlockError + }, + )?; + + let burnchain_tip_info = SortitionDB::get_block_snapshot_consensus( + &sortdb_tx, + &next_block.header.consensus_hash, + )?.ok_or_else(|| { + warn!( + "Tried to process next ready block, but the snapshot that elected it cannot be found"; + "block_hash" => %next_block.header.block_hash(), + "consensus_hash" => %next_block.header.consensus_hash, + ); + ChainstateError::NoSuchBlockError + })?; + + let burnchain_height = burnchain_tip_info.block_height.try_into().map_err(|_| { + error!("Burnchain height exceeds u32"); + ChainstateError::InvalidStacksBlock("Burnchain height exceeds u32".into()) + })?; + let block_size = 1; + + let (_receipt, clarity_tx) = NakamotoChainState::append_block( + &mut chainstate_tx, + clarity_instance, + &mut sortdb_tx, + &pox_constants, + &parent_chain_tip, + &burnchain_tip_info.burn_header_hash, + burnchain_height, + burnchain_tip_info.burn_header_timestamp, + &next_block, + block_size, + 1, + 1, + ) + .unwrap(); + + chainstate_tx.commit(); + clarity_tx.commit(); + + Ok(true) + } +} diff --git a/testnet/stacks-node/src/neon_node.rs b/testnet/stacks-node/src/neon_node.rs index 78e474ac70c..2b839f52991 100644 --- a/testnet/stacks-node/src/neon_node.rs +++ b/testnet/stacks-node/src/neon_node.rs @@ -153,6 +153,7 @@ use clarity::vm::ast::ASTRules; use clarity::vm::costs::ExecutionCost; use clarity::vm::types::PrincipalData; use clarity::vm::types::QualifiedContractIdentifier; +use stacks::burnchains::PoxConstants; use stacks::burnchains::{ db::BurnchainHeaderReader, Burnchain, BurnchainParameters, BurnchainSigner, Txid, }; @@ -219,6 +220,7 @@ use super::{BurnchainController, Config, EventDispatcher, Keychain}; use crate::burnchains::bitcoin_regtest_controller::OngoingBlockCommit; use crate::burnchains::bitcoin_regtest_controller::{addr2str, BitcoinRegtestController}; use crate::burnchains::make_bitcoin_indexer; +use crate::run_loop; use crate::run_loop::neon::Counters; use crate::run_loop::neon::RunLoop; use crate::run_loop::RegisteredKey; @@ -3593,12 +3595,26 @@ impl PeerThread { /// Binds the addresses in the config (which may panic if the port is blocked). /// This is so the node will crash "early" before any new threads start if there's going to be /// a bind error anyway. - pub fn new(runloop: &RunLoop, mut net: PeerNetwork) -> PeerThread { - let config = runloop.config().clone(); + pub fn new(runloop: &RunLoop, net: PeerNetwork) -> PeerThread { + Self::new_all( + runloop.get_globals(), + runloop.config(), + runloop.get_burnchain().pox_constants, + net, + ) + } + + pub fn new_all( + globals: Globals, + config: &Config, + pox_constants: PoxConstants, + mut net: PeerNetwork, + ) -> Self { + let config = config.clone(); let mempool = Self::connect_mempool_db(&config); let burn_db_path = config.get_burn_db_file_path(); - let sortdb = SortitionDB::open(&burn_db_path, false, runloop.get_burnchain().pox_constants) + let sortdb = SortitionDB::open(&burn_db_path, false, pox_constants) .expect("FATAL: could not open sortition DB"); let chainstate = @@ -3621,7 +3637,7 @@ impl PeerThread { PeerThread { config, net: Some(net), - globals: runloop.get_globals(), + globals, poll_timeout, sortdb: Some(sortdb), chainstate: Some(chainstate), @@ -4146,11 +4162,8 @@ impl StacksNode { /// Main loop of the p2p thread. /// Runs in a separate thread. /// Continuously receives, until told otherwise. - pub fn p2p_main( - mut p2p_thread: PeerThread, - event_dispatcher: EventDispatcher, - should_keep_running: Arc, - ) { + pub fn p2p_main(mut p2p_thread: PeerThread, event_dispatcher: EventDispatcher) { + let should_keep_running = p2p_thread.globals.should_keep_running.clone(); let (mut dns_resolver, mut dns_client) = DNSResolver::new(10); // spawn a daemon thread that runs the DNS resolver. @@ -4298,7 +4311,6 @@ impl StacksNode { }) .expect("FATAL: failed to start relayer thread"); - let should_keep_running_clone = globals.should_keep_running.clone(); let p2p_event_dispatcher = runloop.get_event_dispatcher(); let p2p_thread = PeerThread::new(runloop, p2p_net); let p2p_thread_handle = thread::Builder::new() @@ -4309,7 +4321,7 @@ impl StacksNode { )) .spawn(move || { debug!("p2p thread ID is {:?}", thread::current().id()); - Self::p2p_main(p2p_thread, p2p_event_dispatcher, should_keep_running_clone); + Self::p2p_main(p2p_thread, p2p_event_dispatcher); }) .expect("FATAL: failed to start p2p thread");