From 15972099a539b472d46934f235b20d5ce440fcf8 Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Tue, 22 Oct 2024 23:08:00 -0400 Subject: [PATCH 01/19] chore: add nakamoto testnet pox settings --- stackslib/src/burnchains/mod.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/stackslib/src/burnchains/mod.rs b/stackslib/src/burnchains/mod.rs index 0bc68897cb..f67fca2dd0 100644 --- a/stackslib/src/burnchains/mod.rs +++ b/stackslib/src/burnchains/mod.rs @@ -450,6 +450,7 @@ impl PoxConstants { ) } + // NOTE: this is the *old* pre-Nakamoto testnet pub fn testnet_default() -> PoxConstants { PoxConstants::new( POX_REWARD_CYCLE_LENGTH / 2, // 1050 @@ -468,6 +469,10 @@ impl PoxConstants { ) // total liquid supply is 40000000000000000 µSTX } + pub fn nakamoto_testnet_default() -> PoxConstants { + PoxConstants::new(900, 100, 0, 100, 0, u64::MAX, u64::MAX, 242, 243, 246, 244) + } + // TODO: add tests from mutation testing results #4838 #[cfg_attr(test, mutants::skip)] pub fn regtest_default() -> PoxConstants { From fbec882c562aa3c8558a67cec7fb2a89db0430b6 Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Tue, 22 Oct 2024 23:08:17 -0400 Subject: [PATCH 02/19] chore: expose NakamotoBlockBuilder::get_account() --- stackslib/src/chainstate/nakamoto/shadow.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/stackslib/src/chainstate/nakamoto/shadow.rs b/stackslib/src/chainstate/nakamoto/shadow.rs index 4e4b4f9741..914fc0e5e0 100644 --- a/stackslib/src/chainstate/nakamoto/shadow.rs +++ b/stackslib/src/chainstate/nakamoto/shadow.rs @@ -461,7 +461,7 @@ impl NakamotoBlockBuilder { } /// Get an address's account - fn get_account( + pub fn get_account( chainstate: &mut StacksChainState, sortdb: &SortitionDB, addr: &StacksAddress, @@ -568,6 +568,10 @@ impl NakamotoBlockBuilder { /// Produce a single-block shadow tenure. /// Used by tooling to synthesize shadow blocks in case of an emergency. /// The details and circumatances will be recorded in an accompanying SIP. + /// + /// `naka_tip_id` is the Stacks chain tip on top of which the shadow block will be built. + /// `tenure_id_consensus_hash` is the sortition in which the shadow block will be built. + /// `txs` are transactions to include, beyond a coinbase and tenure-change pub fn make_shadow_tenure( chainstate: &mut StacksChainState, sortdb: &SortitionDB, From 7e26c018e72e55a5518da275c7b9a0ed67f88510 Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Tue, 22 Oct 2024 23:08:32 -0400 Subject: [PATCH 03/19] feat: add `get-account`, `get-nakamoto-tip`, `make-shadow-block`, and `add-shadow-block` commands --- stackslib/src/main.rs | 181 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 181 insertions(+) diff --git a/stackslib/src/main.rs b/stackslib/src/main.rs index 98315cffa8..2e55cd722f 100644 --- a/stackslib/src/main.rs +++ b/stackslib/src/main.rs @@ -49,6 +49,7 @@ use blockstack_lib::chainstate::burn::db::sortdb::{ use blockstack_lib::chainstate::burn::operations::BlockstackOperationType; use blockstack_lib::chainstate::burn::{BlockSnapshot, ConsensusHash}; use blockstack_lib::chainstate::coordinator::{get_reward_cycle_info, OnChainRewardSetProvider}; +use blockstack_lib::chainstate::nakamoto::miner::NakamotoBlockBuilder; use blockstack_lib::chainstate::nakamoto::{NakamotoBlock, NakamotoChainState}; use blockstack_lib::chainstate::stacks::db::blocks::{DummyEventDispatcher, StagingBlock}; use blockstack_lib::chainstate::stacks::db::{ @@ -247,6 +248,30 @@ impl P2PSession { } } +fn open_nakamoto_chainstate_dbs( + chainstate_dir: &str, + network: &str, +) -> (SortitionDB, StacksChainState) { + let (mainnet, chain_id, pox_constants) = match network { + "mainnet" => (true, CHAIN_ID_MAINNET, PoxConstants::mainnet_default()), + "krypton" => (false, 0x80000100, PoxConstants::nakamoto_testnet_default()), + _ => { + panic!("Unrecognized network name '{}'", network); + } + }; + + let chain_state_path = format!("{}/{}/chainstate/", chainstate_dir, network); + let sort_db_path = format!("{}/{}/burnchain/sortition/", chainstate_dir, network); + + let sort_db = SortitionDB::open(&sort_db_path, false, pox_constants) + .unwrap_or_else(|_| panic!("Failed to open {sort_db_path}")); + + let (chain_state, _) = StacksChainState::open(mainnet, chain_id, &chain_state_path, None) + .expect("Failed to open stacks chain state"); + + (sort_db, chain_state) +} + #[cfg_attr(test, mutants::skip)] fn main() { let mut argv: Vec = env::args().collect(); @@ -1166,6 +1191,162 @@ simulating a miner. println!("{:?}", inv); } + if argv[1] == "get-nakamoto-tip" { + if argv.len() < 4 { + eprintln!( + "Usage: {} get-nakamoto-tip CHAINSTATAE_DIR NETWORK", + &argv[0] + ); + process::exit(1); + } + + let chainstate_dir = argv[2].as_str(); + let network = argv[3].as_str(); + + if network != "mainnet" && network != "krypton" { + eprintln!( + "Unknown network '{}': only support 'mainnet' and 'krypton'", + &network + ); + process::exit(1); + } + + let (sort_db, chain_state) = open_nakamoto_chainstate_dbs(chainstate_dir, network); + + let header = NakamotoChainState::get_canonical_block_header(chain_state.db(), &sort_db) + .unwrap() + .unwrap(); + println!("{}", &header.index_block_hash()); + process::exit(0); + } + + if argv[1] == "get-account" { + if argv.len() < 5 { + eprintln!( + "Usage: {} get-account CHAINSTATE_DIR mainnet|krypton ADDRESS [CHAIN_TIP]", + &argv[0] + ); + process::exit(1); + } + + let chainstate_dir = argv[2].as_str(); + let network = argv[3].as_str(); + let addr = StacksAddress::from_string(&argv[4]).unwrap(); + let chain_tip: Option = + argv.get(5).map(|tip| StacksBlockId::from_hex(tip).unwrap()); + + if network != "mainnet" && network != "krypton" { + eprintln!( + "Unknown network '{}': only support 'mainnet' and 'krypton'", + &network + ); + process::exit(1); + } + + let (sort_db, mut chain_state) = open_nakamoto_chainstate_dbs(chainstate_dir, network); + + let chain_tip_header = chain_tip + .map(|tip| { + let header = NakamotoChainState::get_block_header_nakamoto(chain_state.db(), &tip) + .unwrap() + .unwrap(); + header + }) + .unwrap_or_else(|| { + let header = + NakamotoChainState::get_canonical_block_header(chain_state.db(), &sort_db) + .unwrap() + .unwrap(); + header + }); + + let account = + NakamotoBlockBuilder::get_account(&mut chain_state, &sort_db, &addr, &chain_tip_header) + .unwrap(); + println!("{:#?}", &account); + process::exit(0); + } + + if argv[1] == "make-shadow-block" { + if argv.len() < 5 { + eprintln!( + "Usage: {} make-shadow-block CHAINSTATE_DIR NETWORK CHAIN_TIP_HASH [TX...]", + &argv[0] + ); + process::exit(1); + } + let chainstate_dir = argv[2].as_str(); + let network = argv[3].as_str(); + let chain_tip = StacksBlockId::from_hex(argv[4].as_str()).unwrap(); + let txs = argv[5..] + .iter() + .map(|tx_str| { + let tx_bytes = hex_bytes(&tx_str).unwrap(); + let tx = StacksTransaction::consensus_deserialize(&mut &tx_bytes[..]).unwrap(); + tx + }) + .collect(); + + if network != "mainnet" && network != "krypton" { + eprintln!( + "Unknown network '{}': only support 'mainnet' and 'krypton'", + &network + ); + process::exit(1); + } + + let (sort_db, mut chain_state) = open_nakamoto_chainstate_dbs(chainstate_dir, network); + let header = NakamotoChainState::get_block_header(chain_state.db(), &chain_tip) + .unwrap() + .unwrap(); + + let shadow_block = NakamotoBlockBuilder::make_shadow_tenure( + &mut chain_state, + &sort_db, + chain_tip, + header.consensus_hash, + txs, + ) + .unwrap(); + + println!("{}", to_hex(&shadow_block.serialize_to_vec())); + process::exit(0); + } + + if argv[1] == "add-shadow-block" { + if argv.len() < 5 { + eprintln!( + "Usage: {} add-shadow-block CHAINSTATE_DIR NETWORK SHADOW_BLOCK_HEX", + &argv[0] + ); + process::exit(1); + } + let chainstate_dir = argv[2].as_str(); + let network = argv[3].as_str(); + let block_hex = argv[4].as_str(); + let shadow_block = + NakamotoBlock::consensus_deserialize(&mut hex_bytes(block_hex).unwrap().as_slice()) + .unwrap(); + + assert!(shadow_block.is_shadow_block()); + + if network != "mainnet" && network != "krypton" { + eprintln!( + "Unknown network '{}': only support 'mainnet' and 'krypton'", + &network + ); + process::exit(1); + } + + let (_, mut chain_state) = open_nakamoto_chainstate_dbs(chainstate_dir, network); + + let tx = chain_state.staging_db_tx_begin().unwrap(); + tx.add_shadow_block(&shadow_block).unwrap(); + tx.commit().unwrap(); + + process::exit(0); + } + if argv[1] == "replay-chainstate" { if argv.len() < 7 { eprintln!("Usage: {} OLD_CHAINSTATE_PATH OLD_SORTITION_DB_PATH OLD_BURNCHAIN_DB_PATH NEW_CHAINSTATE_PATH NEW_BURNCHAIN_DB_PATH", &argv[0]); From 9bcee645e47f3553f3dd09db2a9dbe74e6f8475c Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Thu, 24 Oct 2024 17:47:09 -0400 Subject: [PATCH 04/19] fix: use correct pox params --- stackslib/src/burnchains/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stackslib/src/burnchains/mod.rs b/stackslib/src/burnchains/mod.rs index f67fca2dd0..3e153df53b 100644 --- a/stackslib/src/burnchains/mod.rs +++ b/stackslib/src/burnchains/mod.rs @@ -470,7 +470,7 @@ impl PoxConstants { } pub fn nakamoto_testnet_default() -> PoxConstants { - PoxConstants::new(900, 100, 0, 100, 0, u64::MAX, u64::MAX, 242, 243, 246, 244) + PoxConstants::new(900, 100, 51, 100, 0, u64::MAX, u64::MAX, 242, 243, 246, 244) } // TODO: add tests from mutation testing results #4838 From 850d00bec7f49ac35afff3f6e1c7d75813774e7f Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Thu, 24 Oct 2024 17:48:14 -0400 Subject: [PATCH 05/19] chore: move shadow block creation and replay into separate functions so we can use them for testing --- stackslib/src/chainstate/nakamoto/shadow.rs | 138 +++++++++++++++++++- 1 file changed, 135 insertions(+), 3 deletions(-) diff --git a/stackslib/src/chainstate/nakamoto/shadow.rs b/stackslib/src/chainstate/nakamoto/shadow.rs index 914fc0e5e0..f45b3f69a4 100644 --- a/stackslib/src/chainstate/nakamoto/shadow.rs +++ b/stackslib/src/chainstate/nakamoto/shadow.rs @@ -55,6 +55,7 @@ use crate::chainstate::nakamoto::{ SortitionHandleConn, StacksDBIndexed, }; use crate::chainstate::stacks::boot::RewardSet; +use crate::chainstate::stacks::db::blocks::DummyEventDispatcher; use crate::chainstate::stacks::db::{ ChainstateTx, ClarityTx, StacksAccount, StacksChainState, StacksHeaderInfo, }; @@ -70,6 +71,7 @@ use crate::chainstate::stacks::{ use crate::clarity::vm::types::StacksAddressExtensions; use crate::clarity_vm::clarity::ClarityInstance; use crate::clarity_vm::database::SortitionDBRef; +use crate::net::Error as NetError; use crate::util_lib::db::{query_row, u64_to_sql, Error as DBError}; impl NakamotoBlockHeader { @@ -561,7 +563,7 @@ impl NakamotoBlockBuilder { } let block = builder.mine_nakamoto_block(&mut tenure_tx); let size = builder.bytes_so_far; - let cost = builder.tenure_finish(tenure_tx).unwrap(); + let cost = builder.tenure_finish(tenure_tx)?; Ok((block, size, cost)) } @@ -708,8 +710,7 @@ impl NakamotoBlockBuilder { Some(&tenure_change_tx), Some(&coinbase_tx), 1, - ) - .unwrap(); + )?; let mut block_txs = vec![tenure_change_tx, coinbase_tx]; block_txs.append(&mut txs); @@ -856,3 +857,134 @@ impl<'a> NakamotoStagingBlocksTx<'a> { Ok(()) } } + +/// DO NOT RUN ON A RUNNING NODE (unless you're testing). +/// +/// Insert and process a shadow block into the Stacks chainstate. +pub fn process_shadow_block( + chain_state: &mut StacksChainState, + sort_db: &mut SortitionDB, + shadow_block: NakamotoBlock, +) -> Result<(), ChainstateError> { + let tx = chain_state.staging_db_tx_begin()?; + tx.add_shadow_block(&shadow_block)?; + tx.commit()?; + + let no_dispatch: Option = None; + loop { + let sort_tip = SortitionDB::get_canonical_burn_chain_tip(sort_db.conn())?; + + // process at most one block per loop pass + let processed_block_receipt = match NakamotoChainState::process_next_nakamoto_block( + chain_state, + sort_db, + &sort_tip.sortition_id, + no_dispatch.as_ref(), + ) { + Ok(receipt_opt) => receipt_opt, + Err(ChainstateError::InvalidStacksBlock(msg)) => { + warn!("Encountered invalid block: {}", &msg); + continue; + } + Err(ChainstateError::NetError(NetError::DeserializeError(msg))) => { + // happens if we load a zero-sized block (i.e. an invalid block) + warn!("Encountered invalid block (codec error): {}", &msg); + continue; + } + Err(e) => { + // something else happened + return Err(e.into()); + } + }; + + if processed_block_receipt.is_none() { + // out of blocks + info!("No more blocks to process (no receipts)"); + break; + }; + + let Some((_, processed, orphaned, _)) = chain_state + .nakamoto_blocks_db() + .get_block_processed_and_signed_weight( + &shadow_block.header.consensus_hash, + &shadow_block.header.block_hash(), + )? + else { + return Err(ChainstateError::InvalidStacksBlock(format!( + "Shadow block {} for tenure {} not store", + &shadow_block.block_id(), + &shadow_block.header.consensus_hash + ))); + }; + + if orphaned { + return Err(ChainstateError::InvalidStacksBlock(format!( + "Shadow block {} for tenure {} was orphaned", + &shadow_block.block_id(), + &shadow_block.header.consensus_hash + ))); + } + + if processed { + break; + } + } + Ok(()) +} + +/// DO NOT RUN ON A RUNNING NODE (unless you're testing). +/// +/// Automatically repair a node that has been stalled due to an empty prepare phase. +/// Works by synthesizing, inserting, and processing shadow tenures in-between the last sortition +/// with a winner and the burnchain tip. +/// +/// This is meant to be accessed by the tooling. Once the blocks are synthesized, they would be +/// added into other broken nodes' chainstates by the same tooling. Ultimately, a patched node +/// would be released with these shadow blocks added in as part of the chainstate schema. +/// +/// Returns the syntheisized shadow blocks on success. +/// Returns error on failure. +pub fn shadow_chainstate_repair( + chain_state: &mut StacksChainState, + sort_db: &mut SortitionDB, +) -> Result, ChainstateError> { + let sort_tip = SortitionDB::get_canonical_burn_chain_tip(sort_db.conn())?; + + let header = NakamotoChainState::get_canonical_block_header(chain_state.db(), &sort_db)? + .ok_or_else(|| ChainstateError::NoSuchBlockError)?; + + let header_sn = + SortitionDB::get_block_snapshot_consensus(sort_db.conn(), &header.consensus_hash)? + .ok_or_else(|| { + ChainstateError::InvalidStacksBlock( + "Canonical stacks header does not have a sortition".into(), + ) + })?; + + let mut shadow_blocks = vec![]; + for burn_height in (header_sn.block_height + 1)..sort_tip.block_height { + let sort_tip = SortitionDB::get_canonical_burn_chain_tip(sort_db.conn())?; + let sort_handle = sort_db.index_handle(&sort_tip.sortition_id); + let sn = sort_handle + .get_block_snapshot_by_height(burn_height)? + .ok_or_else(|| ChainstateError::InvalidStacksBlock("No sortition at height".into()))?; + + let header = NakamotoChainState::get_canonical_block_header(chain_state.db(), &sort_db)? + .ok_or_else(|| ChainstateError::NoSuchBlockError)?; + + let chain_tip = header.index_block_hash(); + let shadow_block = NakamotoBlockBuilder::make_shadow_tenure( + chain_state, + sort_db, + chain_tip.clone(), + sn.consensus_hash, + vec![], + )?; + + shadow_blocks.push(shadow_block.clone()); + + process_shadow_block(chain_state, sort_db, shadow_block)?; + } + + Ok(shadow_blocks) +} From fb391fc9e345355f1125deb9757a0bb4eb5063c5 Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Thu, 24 Oct 2024 17:48:35 -0400 Subject: [PATCH 06/19] chore: make function public --- stackslib/src/chainstate/nakamoto/staging_blocks.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stackslib/src/chainstate/nakamoto/staging_blocks.rs b/stackslib/src/chainstate/nakamoto/staging_blocks.rs index e5de323ce5..c3e8432878 100644 --- a/stackslib/src/chainstate/nakamoto/staging_blocks.rs +++ b/stackslib/src/chainstate/nakamoto/staging_blocks.rs @@ -278,7 +278,7 @@ impl<'a> NakamotoStagingBlocksConnRef<'a> { /// There will be at most one such block. /// /// NOTE: for Nakamoto blocks, the sighash is the same as the block hash. - pub(crate) fn get_block_processed_and_signed_weight( + pub fn get_block_processed_and_signed_weight( &self, consensus_hash: &ConsensusHash, block_hash: &BlockHeaderHash, From 0f29c68fab28e90a19ee5dde76bc465ae961bb48 Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Thu, 24 Oct 2024 17:49:12 -0400 Subject: [PATCH 07/19] chore: shadow-chainstate-repair and shadow-chainstate-patch --- stackslib/src/main.rs | 137 ++++++++++++++++++++++++++++++------------ 1 file changed, 100 insertions(+), 37 deletions(-) diff --git a/stackslib/src/main.rs b/stackslib/src/main.rs index 2e55cd722f..95e684162b 100644 --- a/stackslib/src/main.rs +++ b/stackslib/src/main.rs @@ -50,6 +50,9 @@ use blockstack_lib::chainstate::burn::operations::BlockstackOperationType; use blockstack_lib::chainstate::burn::{BlockSnapshot, ConsensusHash}; use blockstack_lib::chainstate::coordinator::{get_reward_cycle_info, OnChainRewardSetProvider}; use blockstack_lib::chainstate::nakamoto::miner::NakamotoBlockBuilder; +use blockstack_lib::chainstate::nakamoto::shadow::{ + process_shadow_block, shadow_chainstate_repair, +}; use blockstack_lib::chainstate::nakamoto::{NakamotoBlock, NakamotoChainState}; use blockstack_lib::chainstate::stacks::db::blocks::{DummyEventDispatcher, StagingBlock}; use blockstack_lib::chainstate::stacks::db::{ @@ -252,16 +255,32 @@ fn open_nakamoto_chainstate_dbs( chainstate_dir: &str, network: &str, ) -> (SortitionDB, StacksChainState) { - let (mainnet, chain_id, pox_constants) = match network { - "mainnet" => (true, CHAIN_ID_MAINNET, PoxConstants::mainnet_default()), - "krypton" => (false, 0x80000100, PoxConstants::nakamoto_testnet_default()), + let (mainnet, chain_id, pox_constants, dirname) = match network { + "mainnet" => ( + true, + CHAIN_ID_MAINNET, + PoxConstants::mainnet_default(), + network, + ), + "krypton" => ( + false, + 0x80000100, + PoxConstants::nakamoto_testnet_default(), + network, + ), + "naka3" => ( + false, + 0x80000000, + PoxConstants::new(20, 5, 3, 100, 0, u64::MAX, u64::MAX, 104, 105, 106, 107), + "nakamoto-neon", + ), _ => { panic!("Unrecognized network name '{}'", network); } }; - let chain_state_path = format!("{}/{}/chainstate/", chainstate_dir, network); - let sort_db_path = format!("{}/{}/burnchain/sortition/", chainstate_dir, network); + let chain_state_path = format!("{}/{}/chainstate/", chainstate_dir, dirname); + let sort_db_path = format!("{}/{}/burnchain/sortition/", chainstate_dir, dirname); let sort_db = SortitionDB::open(&sort_db_path, false, pox_constants) .unwrap_or_else(|_| panic!("Failed to open {sort_db_path}")); @@ -272,6 +291,16 @@ fn open_nakamoto_chainstate_dbs( (sort_db, chain_state) } +fn check_shadow_network(network: &str) { + if network != "mainnet" && network != "krypton" && network != "naka3" { + eprintln!( + "Unknown network '{}': only support 'mainnet', 'krypton', or 'naka3'", + &network + ); + process::exit(1); + } +} + #[cfg_attr(test, mutants::skip)] fn main() { let mut argv: Vec = env::args().collect(); @@ -1203,14 +1232,7 @@ simulating a miner. let chainstate_dir = argv[2].as_str(); let network = argv[3].as_str(); - if network != "mainnet" && network != "krypton" { - eprintln!( - "Unknown network '{}': only support 'mainnet' and 'krypton'", - &network - ); - process::exit(1); - } - + check_shadow_network(network); let (sort_db, chain_state) = open_nakamoto_chainstate_dbs(chainstate_dir, network); let header = NakamotoChainState::get_canonical_block_header(chain_state.db(), &sort_db) @@ -1235,14 +1257,7 @@ simulating a miner. let chain_tip: Option = argv.get(5).map(|tip| StacksBlockId::from_hex(tip).unwrap()); - if network != "mainnet" && network != "krypton" { - eprintln!( - "Unknown network '{}': only support 'mainnet' and 'krypton'", - &network - ); - process::exit(1); - } - + check_shadow_network(network); let (sort_db, mut chain_state) = open_nakamoto_chainstate_dbs(chainstate_dir, network); let chain_tip_header = chain_tip @@ -1287,14 +1302,7 @@ simulating a miner. }) .collect(); - if network != "mainnet" && network != "krypton" { - eprintln!( - "Unknown network '{}': only support 'mainnet' and 'krypton'", - &network - ); - process::exit(1); - } - + check_shadow_network(network); let (sort_db, mut chain_state) = open_nakamoto_chainstate_dbs(chainstate_dir, network); let header = NakamotoChainState::get_block_header(chain_state.db(), &chain_tip) .unwrap() @@ -1313,6 +1321,68 @@ simulating a miner. process::exit(0); } + /// Generates the shadow blocks needed to restore this node to working order. + /// Automatically inserts and processes them as well. + /// Prints out the generated shadow blocks (as JSON) + if argv[1] == "shadow-chainstate-repair" { + if argv.len() < 4 { + eprintln!( + "Usage: {} shadow-chainstate-repair CHAINSTATE_DIR NETWORK", + &argv[0] + ); + process::exit(1); + } + + let chainstate_dir = argv[2].as_str(); + let network = argv[3].as_str(); + + check_shadow_network(network); + + let (mut sort_db, mut chain_state) = open_nakamoto_chainstate_dbs(chainstate_dir, network); + let shadow_blocks = shadow_chainstate_repair(&mut chain_state, &mut sort_db).unwrap(); + + let shadow_blocks_hex: Vec<_> = shadow_blocks + .into_iter() + .map(|blk| to_hex(&blk.serialize_to_vec())) + .collect(); + + println!("{}", serde_json::to_string(&shadow_blocks)); + process::exit(0); + } + + /// Inserts and processes shadow blocks generated from `shadow-chainstate-repair` + if argv[1] == "shadow-chainstate-patch" { + if argv.len() < 5 { + eprintln!( + "Usage: {} shadow-chainstate-repair CHAINSTATE_DIR NETWORK SHADOW_BLOCKS_JSON", + &argv[0] + ); + process::exit(1); + } + + let chainstate_dir = argv[2].as_str(); + let network = argv[3].as_str(); + let shadow_blocks_json = argv[4].as_str(); + + let shadow_blocks_hex: Vec = serde_json::from_str(shadow_blocks_json).unwrap(); + let shadow_blocks: Vec<_> = shadow_blocks_hex + .into_iter() + .map(|blk_hex| { + NakamotoBlock::consensus_deserialize(&mut hex_bytes(&blk_hex).unwrap().as_slice()) + .unwrap() + }) + .collect(); + + check_shadow_network(network); + + let (mut sort_db, mut chain_state) = open_nakamoto_chainstate_dbs(chainstate_dir, network); + for shadow_block in shadow_blocks.into_iter() { + process_shadow_block(&mut chain_state, &mut sort_db, shadow_block).unwrap(); + } + + process::exit(0); + } + if argv[1] == "add-shadow-block" { if argv.len() < 5 { eprintln!( @@ -1330,14 +1400,7 @@ simulating a miner. assert!(shadow_block.is_shadow_block()); - if network != "mainnet" && network != "krypton" { - eprintln!( - "Unknown network '{}': only support 'mainnet' and 'krypton'", - &network - ); - process::exit(1); - } - + check_shadow_network(network); let (_, mut chain_state) = open_nakamoto_chainstate_dbs(chainstate_dir, network); let tx = chain_state.staging_db_tx_begin().unwrap(); From e8da15183cc4b31ffd4b831f8a50e6de29764759 Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Thu, 24 Oct 2024 17:49:29 -0400 Subject: [PATCH 08/19] fix: the parent can be a shadow block and thus not have a commit --- .../stacks-node/src/nakamoto_node/relayer.rs | 46 ++++++++++++++----- 1 file changed, 35 insertions(+), 11 deletions(-) diff --git a/testnet/stacks-node/src/nakamoto_node/relayer.rs b/testnet/stacks-node/src/nakamoto_node/relayer.rs index ef01f67f4b..9d380d9ac1 100644 --- a/testnet/stacks-node/src/nakamoto_node/relayer.rs +++ b/testnet/stacks-node/src/nakamoto_node/relayer.rs @@ -31,7 +31,7 @@ use stacks::chainstate::burn::operations::{ }; use stacks::chainstate::burn::{BlockSnapshot, ConsensusHash}; use stacks::chainstate::nakamoto::coordinator::get_nakamoto_next_recipients; -use stacks::chainstate::nakamoto::NakamotoChainState; +use stacks::chainstate::nakamoto::{NakamotoBlockHeader, NakamotoChainState}; use stacks::chainstate::stacks::address::PoxAddress; use stacks::chainstate::stacks::db::StacksChainState; use stacks::chainstate::stacks::miner::{ @@ -555,6 +555,7 @@ impl RelayerThread { tip_block_ch: &ConsensusHash, tip_block_bh: &BlockHeaderHash, ) -> Result { + let tip_block_id = StacksBlockId::new(&tip_block_ch, &tip_block_bh); let sort_tip = SortitionDB::get_canonical_burn_chain_tip(self.sortdb.conn()) .map_err(|_| NakamotoNodeError::SnapshotNotFoundForChainTip)?; @@ -636,18 +637,41 @@ impl RelayerThread { return Err(NakamotoNodeError::ParentNotFound); }; - // find the parent block-commit of this commit + // find the parent block-commit of this commit, so we can find the parent vtxindex + // if the parent is a shadow block, then the vtxindex would be 0. let commit_parent_block_burn_height = tip_tenure_sortition.block_height; - let Ok(Some(parent_winning_tx)) = SortitionDB::get_block_commit( - self.sortdb.conn(), - &tip_tenure_sortition.winning_block_txid, - &tip_tenure_sortition.sortition_id, - ) else { - error!("Relayer: Failed to lookup the block commit of parent tenure ID"; "tenure_consensus_hash" => %tip_block_ch); - return Err(NakamotoNodeError::SnapshotNotFoundForChainTip); - }; + let commit_parent_winning_vtxindex = if let Ok(Some(parent_winning_tx)) = + SortitionDB::get_block_commit( + self.sortdb.conn(), + &tip_tenure_sortition.winning_block_txid, + &tip_tenure_sortition.sortition_id, + ) { + parent_winning_tx.vtxindex + } else { + debug!( + "{}/{} ({}) must be a shadow block, since it has no block-commit", + &tip_block_bh, &tip_block_ch, &tip_block_id + ); + let Ok(Some(parent_version)) = + NakamotoChainState::get_nakamoto_block_version(self.chainstate.db(), &tip_block_id) + else { + error!( + "Relayer: Failed to lookup block version of {}", + &tip_block_id + ); + return Err(NakamotoNodeError::ParentNotFound); + }; + + if !NakamotoBlockHeader::is_shadow_block_version(parent_version) { + error!( + "Relayer: parent block-commit of {} not found, and it is not a shadow block", + &tip_block_id + ); + return Err(NakamotoNodeError::ParentNotFound); + } - let commit_parent_winning_vtxindex = parent_winning_tx.vtxindex; + 0 + }; // epoch in which this commit will be sent (affects how the burnchain client processes it) let Ok(Some(target_epoch)) = From 4851c3c1e720f7353b2308d5b59d0cb9508a943e Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Thu, 24 Oct 2024 17:49:49 -0400 Subject: [PATCH 09/19] chore: integration test --- .../src/tests/nakamoto_integrations.rs | 240 ++++++++++++++++++ 1 file changed, 240 insertions(+) diff --git a/testnet/stacks-node/src/tests/nakamoto_integrations.rs b/testnet/stacks-node/src/tests/nakamoto_integrations.rs index 90334cce9b..bb0be6c1ec 100644 --- a/testnet/stacks-node/src/tests/nakamoto_integrations.rs +++ b/testnet/stacks-node/src/tests/nakamoto_integrations.rs @@ -39,6 +39,7 @@ use stacks::chainstate::coordinator::comm::CoordinatorChannels; use stacks::chainstate::coordinator::OnChainRewardSetProvider; use stacks::chainstate::nakamoto::coordinator::load_nakamoto_reward_set; use stacks::chainstate::nakamoto::miner::NakamotoBlockBuilder; +use stacks::chainstate::nakamoto::shadow::shadow_chainstate_repair; use stacks::chainstate::nakamoto::test_signers::TestSigners; use stacks::chainstate::nakamoto::{NakamotoBlock, NakamotoBlockHeader, NakamotoChainState}; use stacks::chainstate::stacks::address::{PoxAddress, StacksAddressExtensions}; @@ -9594,3 +9595,242 @@ fn skip_mining_long_tx() { run_loop_thread.join().unwrap(); } + +/// Verify that a node in which there is no prepare-phase block can be recovered by +/// live-instantiating shadow tenures in the prepare phase +#[test] +#[ignore] +fn test_shadow_recovery() { + if env::var("BITCOIND_TEST") != Ok("1".into()) { + return; + } + + let (mut naka_conf, _miner_account) = naka_neon_integration_conf(None); + let http_origin = format!("http://{}", &naka_conf.node.rpc_bind); + naka_conf.miner.wait_on_interim_blocks = Duration::from_secs(1); + let sender_sk = Secp256k1PrivateKey::new(); + let sender_signer_sk = Secp256k1PrivateKey::new(); + let sender_signer_addr = tests::to_addr(&sender_signer_sk); + // setup sender + recipient for some test stx transfers + // these are necessary for the interim blocks to get mined at all + let sender_addr = tests::to_addr(&sender_sk); + let send_amt = 100; + let send_fee = 180; + naka_conf.add_initial_balance(PrincipalData::from(sender_addr.clone()).to_string(), 100000); + naka_conf.add_initial_balance( + PrincipalData::from(sender_signer_addr.clone()).to_string(), + 100000, + ); + let recipient = PrincipalData::from(StacksAddress::burn_address(false)); + let stacker_sk = setup_stacker(&mut naka_conf); + + test_observer::spawn(); + test_observer::register_any(&mut naka_conf); + + let mut btcd_controller = BitcoinCoreController::new(naka_conf.clone()); + btcd_controller + .start_bitcoind() + .expect("Failed starting bitcoind"); + let mut btc_regtest_controller = BitcoinRegtestController::new(naka_conf.clone(), None); + btc_regtest_controller.bootstrap_chain(201); + + let mut run_loop = boot_nakamoto::BootRunLoop::new(naka_conf.clone()).unwrap(); + let run_loop_stopper = run_loop.get_termination_switch(); + let Counters { + blocks_processed, + naka_submitted_commits: commits_submitted, + naka_proposed_blocks: proposals_submitted, + .. + } = run_loop.counters(); + + let coord_channel = run_loop.coordinator_channels(); + + let run_loop_thread = thread::Builder::new() + .name("run_loop".into()) + .spawn(move || run_loop.start(None, 0)) + .unwrap(); + wait_for_runloop(&blocks_processed); + let mut signers = TestSigners::new(vec![sender_signer_sk.clone()]); + boot_to_epoch_3( + &naka_conf, + &blocks_processed, + &[stacker_sk], + &[sender_signer_sk], + &mut Some(&mut signers), + &mut btc_regtest_controller, + ); + + info!("Bootstrapped to Epoch-3.0 boundary, starting nakamoto miner"); + + let burnchain = naka_conf.get_burnchain(); + let sortdb = burnchain.open_sortition_db(true).unwrap(); + let (chainstate, _) = StacksChainState::open( + naka_conf.is_mainnet(), + naka_conf.burnchain.chain_id, + &naka_conf.get_chainstate_path_str(), + None, + ) + .unwrap(); + + let block_height_pre_3_0 = + NakamotoChainState::get_canonical_block_header(chainstate.db(), &sortdb) + .unwrap() + .unwrap() + .stacks_block_height; + + info!("Nakamoto miner started..."); + blind_signer(&naka_conf, &signers, proposals_submitted); + + wait_for_first_naka_block_commit(60, &commits_submitted); + + // make another tenure + next_block_and_mine_commit( + &mut btc_regtest_controller, + 60, + &coord_channel, + &commits_submitted, + ) + .unwrap(); + + let block_height = btc_regtest_controller.get_headers_height(); + let reward_cycle = btc_regtest_controller + .get_burnchain() + .block_height_to_reward_cycle(block_height) + .unwrap(); + let prepare_phase_start = btc_regtest_controller + .get_burnchain() + .pox_constants + .prepare_phase_start( + btc_regtest_controller.get_burnchain().first_block_height, + reward_cycle, + ); + + let blocks_until_next_rc = prepare_phase_start + 1 - block_height + + (btc_regtest_controller + .get_burnchain() + .pox_constants + .prepare_length as u64) + + 1; + + // kill the chain by blowing through a prepare phase + btc_regtest_controller.bootstrap_chain(blocks_until_next_rc); + let target_burn_height = btc_regtest_controller.get_headers_height(); + + let burnchain = naka_conf.get_burnchain(); + let mut sortdb = burnchain.open_sortition_db(true).unwrap(); + let (mut chainstate, _) = StacksChainState::open( + false, + CHAIN_ID_TESTNET, + &naka_conf.get_chainstate_path_str(), + None, + ) + .unwrap(); + + wait_for(30, || { + let burn_height = get_chain_info(&naka_conf).burn_block_height; + if burn_height >= target_burn_height { + return Ok(true); + } + sleep_ms(500); + Ok(false) + }) + .unwrap(); + + let burn_height_after = get_chain_info(&naka_conf).burn_block_height; + let stacks_height_before = get_chain_info(&naka_conf).stacks_tip_height; + + // fix node + let shadow_blocks = shadow_chainstate_repair(&mut chainstate, &mut sortdb).unwrap(); + assert!(shadow_blocks.len() > 0); + + wait_for(30, || { + let Some(info) = get_chain_info_opt(&naka_conf) else { + sleep_ms(500); + return Ok(false); + }; + Ok(info.stacks_tip_height >= stacks_height_before) + }) + .unwrap(); + + // revive ATC-C by waiting for commits + for i in 0..4 { + btc_regtest_controller.bootstrap_chain(1); + sleep_ms(30_000); + } + + // make another tenure + next_block_and_mine_commit( + &mut btc_regtest_controller, + 60, + &coord_channel, + &commits_submitted, + ) + .unwrap(); + + // all shadow blocks are present and processed + let mut shadow_ids = HashSet::new(); + for sb in shadow_blocks { + let (_, processed, orphaned, _) = chainstate + .nakamoto_blocks_db() + .get_block_processed_and_signed_weight( + &sb.header.consensus_hash, + &sb.header.block_hash(), + ) + .unwrap() + .unwrap(); + assert!(processed); + assert!(!orphaned); + shadow_ids.insert(sb.block_id()); + } + + let tip = NakamotoChainState::get_canonical_block_header(chainstate.db(), &sortdb) + .unwrap() + .unwrap(); + let mut cursor = tip.index_block_hash(); + + // the chainstate has four parts: + // * epoch 2 + // * epoch 3 prior to failure + // * shadow blocks + // * epoch 3 after recovery + // Make sure they're all there + + let mut has_epoch_3_recovery = false; + let mut has_shadow_blocks = false; + let mut has_epoch_3_failure = false; + + loop { + let header = NakamotoChainState::get_block_header(chainstate.db(), &cursor) + .unwrap() + .unwrap(); + if header.anchored_header.as_stacks_epoch2().is_some() { + break; + } + + let header = header.anchored_header.as_stacks_nakamoto().clone().unwrap(); + + if header.is_shadow_block() { + assert!(shadow_ids.contains(&header.block_id())); + } else { + assert!(!shadow_ids.contains(&header.block_id())); + } + + if !header.is_shadow_block() && !has_epoch_3_recovery { + has_epoch_3_recovery = true; + } else if header.is_shadow_block() && has_epoch_3_recovery && !has_shadow_blocks { + has_shadow_blocks = true; + } else if !header.is_shadow_block() + && has_epoch_3_recovery + && has_shadow_blocks + && !has_epoch_3_failure + { + has_epoch_3_failure = true; + } + + cursor = header.parent_block_id; + } + + assert!(has_epoch_3_recovery); + assert!(has_shadow_blocks); + assert!(has_epoch_3_failure); +} From 8fff6bdeb428f2791376a9291c1828ac6876515b Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Thu, 24 Oct 2024 17:50:16 -0400 Subject: [PATCH 10/19] chore: run integration test in CI --- .github/workflows/bitcoin-tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/bitcoin-tests.yml b/.github/workflows/bitcoin-tests.yml index 23eed46f1e..dc6efee4ee 100644 --- a/.github/workflows/bitcoin-tests.yml +++ b/.github/workflows/bitcoin-tests.yml @@ -133,6 +133,7 @@ jobs: - tests::nakamoto_integrations::utxo_check_on_startup_panic - tests::nakamoto_integrations::utxo_check_on_startup_recover - tests::nakamoto_integrations::v3_signer_api_endpoint + - tests::nakamoto_integrations::test_shadow_recovery # TODO: enable these once v1 signer is supported by a new nakamoto epoch # - tests::signer::v1::dkg # - tests::signer::v1::sign_request_rejected From 05a99fa87fcdde324d44ab07e5b1e188f82848f0 Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Thu, 24 Oct 2024 21:42:26 +0000 Subject: [PATCH 11/19] chore: typo Co-authored-by: Brice Dobry --- stackslib/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stackslib/src/main.rs b/stackslib/src/main.rs index 95e684162b..150c2b007b 100644 --- a/stackslib/src/main.rs +++ b/stackslib/src/main.rs @@ -1223,7 +1223,7 @@ simulating a miner. if argv[1] == "get-nakamoto-tip" { if argv.len() < 4 { eprintln!( - "Usage: {} get-nakamoto-tip CHAINSTATAE_DIR NETWORK", + "Usage: {} get-nakamoto-tip CHAINSTATE_DIR NETWORK", &argv[0] ); process::exit(1); From bfbfbf767b4270886d97c6f4e56ad59507c08b23 Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Thu, 24 Oct 2024 18:01:18 -0400 Subject: [PATCH 12/19] chore: cargo fmt --- stackslib/src/chainstate/nakamoto/miner.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/stackslib/src/chainstate/nakamoto/miner.rs b/stackslib/src/chainstate/nakamoto/miner.rs index 7ffbbc3be9..9dba4561f5 100644 --- a/stackslib/src/chainstate/nakamoto/miner.rs +++ b/stackslib/src/chainstate/nakamoto/miner.rs @@ -401,11 +401,11 @@ impl NakamotoBlockBuilder { burn_dbconn: &'a SortitionHandleConn, info: &'b mut MinerTenureInfo<'a>, ) -> Result, Error> { - if info.tenure_block_commit_opt.is_none() { + let Some(block_commit) = info.tenure_block_commit_opt.as_ref() else { return Err(Error::InvalidStacksBlock( "Block-commit is required; cannot mine a shadow block".into(), )); - } + }; let SetupBlockResult { clarity_tx, @@ -426,10 +426,7 @@ impl NakamotoBlockBuilder { info.coinbase_height, info.cause == Some(TenureChangeCause::Extended), &self.header.pox_treatment, - // safety: checked above - info.tenure_block_commit_opt - .as_ref() - .unwrap_or_else(|| panic!("FATAL: no block-commit for normal Nakamoto block")), + block_commit, &info.active_reward_set, )?; self.matured_miner_rewards_opt = matured_miner_rewards_opt; From 8743b0b3ac95d492280b23ebd7eab95e09914bb4 Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Fri, 25 Oct 2024 12:46:06 -0400 Subject: [PATCH 13/19] chore: fix compile issue --- stackslib/src/main.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stackslib/src/main.rs b/stackslib/src/main.rs index 150c2b007b..046c8c71a2 100644 --- a/stackslib/src/main.rs +++ b/stackslib/src/main.rs @@ -1346,7 +1346,7 @@ simulating a miner. .map(|blk| to_hex(&blk.serialize_to_vec())) .collect(); - println!("{}", serde_json::to_string(&shadow_blocks)); + println!("{}", serde_json::to_string(&shadow_blocks_hex).unwrap()); process::exit(0); } @@ -1354,7 +1354,7 @@ simulating a miner. if argv[1] == "shadow-chainstate-patch" { if argv.len() < 5 { eprintln!( - "Usage: {} shadow-chainstate-repair CHAINSTATE_DIR NETWORK SHADOW_BLOCKS_JSON", + "Usage: {} shadow-chainstate-patch CHAINSTATE_DIR NETWORK SHADOW_BLOCKS_JSON", &argv[0] ); process::exit(1); From 84c982ccfa0eb5b7eb610bb6b17cb542938b065f Mon Sep 17 00:00:00 2001 From: wileyj <2847772+wileyj@users.noreply.github.com> Date: Fri, 25 Oct 2024 10:56:10 -0700 Subject: [PATCH 14/19] Adding stacks-signer binary to image --- .github/actions/dockerfiles/Dockerfile.debian-source | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/dockerfiles/Dockerfile.debian-source b/.github/actions/dockerfiles/Dockerfile.debian-source index b8da585fe2..80c434e8d5 100644 --- a/.github/actions/dockerfiles/Dockerfile.debian-source +++ b/.github/actions/dockerfiles/Dockerfile.debian-source @@ -24,5 +24,5 @@ RUN --mount=type=tmpfs,target=${BUILD_DIR} cp -R /src/. ${BUILD_DIR}/ \ && cp -R ${BUILD_DIR}/target/${TARGET}/release/. /out FROM --platform=${TARGETPLATFORM} debian:bookworm -COPY --from=build /out/stacks-node /out/stacks-signer /bin/ +COPY --from=build /out/stacks-node /out/stacks-signer /out/stacks-inspect /bin/ CMD ["stacks-node", "mainnet"] From c75ed395d289e026961591b06b44126df01e034c Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Fri, 25 Oct 2024 14:04:59 -0400 Subject: [PATCH 15/19] chore: read/write --- stackslib/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stackslib/src/main.rs b/stackslib/src/main.rs index 046c8c71a2..f62b248168 100644 --- a/stackslib/src/main.rs +++ b/stackslib/src/main.rs @@ -282,7 +282,7 @@ fn open_nakamoto_chainstate_dbs( let chain_state_path = format!("{}/{}/chainstate/", chainstate_dir, dirname); let sort_db_path = format!("{}/{}/burnchain/sortition/", chainstate_dir, dirname); - let sort_db = SortitionDB::open(&sort_db_path, false, pox_constants) + let sort_db = SortitionDB::open(&sort_db_path, true, pox_constants) .unwrap_or_else(|_| panic!("Failed to open {sort_db_path}")); let (chain_state, _) = StacksChainState::open(mainnet, chain_id, &chain_state_path, None) From b98239daf3e3729c9d89c6f27f593f2a1b6fbe2c Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Fri, 25 Oct 2024 16:03:40 -0400 Subject: [PATCH 16/19] fix: load blocks json from file --- stackslib/src/main.rs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/stackslib/src/main.rs b/stackslib/src/main.rs index f62b248168..d79d8a37ce 100644 --- a/stackslib/src/main.rs +++ b/stackslib/src/main.rs @@ -1354,7 +1354,7 @@ simulating a miner. if argv[1] == "shadow-chainstate-patch" { if argv.len() < 5 { eprintln!( - "Usage: {} shadow-chainstate-patch CHAINSTATE_DIR NETWORK SHADOW_BLOCKS_JSON", + "Usage: {} shadow-chainstate-patch CHAINSTATE_DIR NETWORK SHADOW_BLOCKS_PATH.JSON", &argv[0] ); process::exit(1); @@ -1362,9 +1362,17 @@ simulating a miner. let chainstate_dir = argv[2].as_str(); let network = argv[3].as_str(); - let shadow_blocks_json = argv[4].as_str(); + let shadow_blocks_json_path = argv[4].as_str(); + + let shadow_blocks_hex = { + let mut blocks_json_file = + File::open(shadow_blocks_json_path).expect("Unable to open file"); + let mut buffer = vec![]; + blocks_json_file.read_to_end(&mut buffer).unwrap(); + let shadow_blocks_hex: Vec = serde_json::from_slice(&buffer).unwrap(); + shadow_blocks_hex + }; - let shadow_blocks_hex: Vec = serde_json::from_str(shadow_blocks_json).unwrap(); let shadow_blocks: Vec<_> = shadow_blocks_hex .into_iter() .map(|blk_hex| { From ca17ede4da6320d1abf0f87a41d59dab277d78a4 Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Fri, 25 Oct 2024 19:31:41 -0400 Subject: [PATCH 17/19] chore: don't use docstring --- stackslib/src/main.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/stackslib/src/main.rs b/stackslib/src/main.rs index d79d8a37ce..f112f8b18c 100644 --- a/stackslib/src/main.rs +++ b/stackslib/src/main.rs @@ -1321,9 +1321,9 @@ simulating a miner. process::exit(0); } - /// Generates the shadow blocks needed to restore this node to working order. - /// Automatically inserts and processes them as well. - /// Prints out the generated shadow blocks (as JSON) + // Generates the shadow blocks needed to restore this node to working order. + // Automatically inserts and processes them as well. + // Prints out the generated shadow blocks (as JSON) if argv[1] == "shadow-chainstate-repair" { if argv.len() < 4 { eprintln!( @@ -1350,7 +1350,7 @@ simulating a miner. process::exit(0); } - /// Inserts and processes shadow blocks generated from `shadow-chainstate-repair` + // Inserts and processes shadow blocks generated from `shadow-chainstate-repair` if argv[1] == "shadow-chainstate-patch" { if argv.len() < 5 { eprintln!( From 439abdb12a4ea83a3724e6d17c0c07581482d359 Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Fri, 25 Oct 2024 19:31:54 -0400 Subject: [PATCH 18/19] fix: accomodate shadow blocks which have no sortition but do have a shadow tenure entry --- .../src/net/api/get_tenures_fork_info.rs | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/stackslib/src/net/api/get_tenures_fork_info.rs b/stackslib/src/net/api/get_tenures_fork_info.rs index 8bcf32ce1d..a225d07fcd 100644 --- a/stackslib/src/net/api/get_tenures_fork_info.rs +++ b/stackslib/src/net/api/get_tenures_fork_info.rs @@ -231,21 +231,29 @@ impl RPCRequestHandler for GetTenuresForkInfo { chainstate, &network.stacks_tip.block_id(), )?); - let handle = sortdb.index_handle(&cursor.sortition_id); let mut depth = 0; while depth < DEPTH_LIMIT && cursor.consensus_hash != recurse_end { depth += 1; if height_bound >= cursor.block_height { return Err(ChainError::NotInSameFork); } - cursor = handle - .get_last_snapshot_with_sortition(cursor.block_height.saturating_sub(1))?; - results.push(TenureForkingInfo::from_snapshot( - &cursor, - sortdb, - chainstate, - &network.stacks_tip.block_id(), - )?); + + if cursor.sortition + || chainstate + .nakamoto_blocks_db() + .is_shadow_tenure(&cursor.consensus_hash)? + { + results.push(TenureForkingInfo::from_snapshot( + &cursor, + sortdb, + chainstate, + &network.stacks_tip.block_id(), + )?); + } + + cursor = + SortitionDB::get_block_snapshot(sortdb.conn(), &cursor.parent_sortition_id)? + .ok_or_else(|| ChainError::NoSuchBlockError)?; } Ok(results) From 69eb7aca98fb735da47cf471e418ced3dd083747 Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Fri, 25 Oct 2024 19:32:15 -0400 Subject: [PATCH 19/19] fix: use a real signer to verify that we can resume mining atop shadow blocks --- .../src/tests/nakamoto_integrations.rs | 89 +++---------------- testnet/stacks-node/src/tests/signer/mod.rs | 4 +- testnet/stacks-node/src/tests/signer/v0.rs | 2 +- 3 files changed, 14 insertions(+), 81 deletions(-) diff --git a/testnet/stacks-node/src/tests/nakamoto_integrations.rs b/testnet/stacks-node/src/tests/nakamoto_integrations.rs index bb0be6c1ec..08dbea3f31 100644 --- a/testnet/stacks-node/src/tests/nakamoto_integrations.rs +++ b/testnet/stacks-node/src/tests/nakamoto_integrations.rs @@ -91,6 +91,7 @@ use stacks_common::util::secp256k1::{MessageSignature, Secp256k1PrivateKey, Secp use stacks_common::util::{get_epoch_time_secs, sleep_ms}; use stacks_signer::chainstate::{ProposalEvalConfig, SortitionsView}; use stacks_signer::signerdb::{BlockInfo, BlockState, ExtraBlockInfo, SignerDb}; +use stacks_signer::v0::SpawnedSigner; use super::bitcoin_regtest::BitcoinCoreController; use crate::config::{EventKeyType, InitialBalance}; @@ -105,6 +106,7 @@ use crate::tests::neon_integrations::{ get_neighbors, get_pox_info, next_block_and_wait, run_until_burnchain_height, submit_tx, test_observer, wait_for_runloop, }; +use crate::tests::signer::SignerTest; use crate::tests::{ gen_random_port, get_chain_info, make_contract_publish, make_contract_publish_versioned, make_stacks_transfer, to_addr, @@ -9605,87 +9607,19 @@ fn test_shadow_recovery() { return; } - let (mut naka_conf, _miner_account) = naka_neon_integration_conf(None); - let http_origin = format!("http://{}", &naka_conf.node.rpc_bind); - naka_conf.miner.wait_on_interim_blocks = Duration::from_secs(1); - let sender_sk = Secp256k1PrivateKey::new(); - let sender_signer_sk = Secp256k1PrivateKey::new(); - let sender_signer_addr = tests::to_addr(&sender_signer_sk); - // setup sender + recipient for some test stx transfers - // these are necessary for the interim blocks to get mined at all - let sender_addr = tests::to_addr(&sender_sk); - let send_amt = 100; - let send_fee = 180; - naka_conf.add_initial_balance(PrincipalData::from(sender_addr.clone()).to_string(), 100000); - naka_conf.add_initial_balance( - PrincipalData::from(sender_signer_addr.clone()).to_string(), - 100000, - ); - let recipient = PrincipalData::from(StacksAddress::burn_address(false)); - let stacker_sk = setup_stacker(&mut naka_conf); + let mut signer_test: SignerTest = SignerTest::new(1, vec![]); + signer_test.boot_to_epoch_3(); - test_observer::spawn(); - test_observer::register_any(&mut naka_conf); - - let mut btcd_controller = BitcoinCoreController::new(naka_conf.clone()); - btcd_controller - .start_bitcoind() - .expect("Failed starting bitcoind"); - let mut btc_regtest_controller = BitcoinRegtestController::new(naka_conf.clone(), None); - btc_regtest_controller.bootstrap_chain(201); - - let mut run_loop = boot_nakamoto::BootRunLoop::new(naka_conf.clone()).unwrap(); - let run_loop_stopper = run_loop.get_termination_switch(); - let Counters { - blocks_processed, - naka_submitted_commits: commits_submitted, - naka_proposed_blocks: proposals_submitted, - .. - } = run_loop.counters(); - - let coord_channel = run_loop.coordinator_channels(); - - let run_loop_thread = thread::Builder::new() - .name("run_loop".into()) - .spawn(move || run_loop.start(None, 0)) - .unwrap(); - wait_for_runloop(&blocks_processed); - let mut signers = TestSigners::new(vec![sender_signer_sk.clone()]); - boot_to_epoch_3( - &naka_conf, - &blocks_processed, - &[stacker_sk], - &[sender_signer_sk], - &mut Some(&mut signers), - &mut btc_regtest_controller, - ); - - info!("Bootstrapped to Epoch-3.0 boundary, starting nakamoto miner"); + let naka_conf = signer_test.running_nodes.conf.clone(); + let btc_regtest_controller = &mut signer_test.running_nodes.btc_regtest_controller; + let coord_channel = signer_test.running_nodes.coord_channel.clone(); + let commits_submitted = signer_test.running_nodes.commits_submitted.clone(); let burnchain = naka_conf.get_burnchain(); - let sortdb = burnchain.open_sortition_db(true).unwrap(); - let (chainstate, _) = StacksChainState::open( - naka_conf.is_mainnet(), - naka_conf.burnchain.chain_id, - &naka_conf.get_chainstate_path_str(), - None, - ) - .unwrap(); - - let block_height_pre_3_0 = - NakamotoChainState::get_canonical_block_header(chainstate.db(), &sortdb) - .unwrap() - .unwrap() - .stacks_block_height; - - info!("Nakamoto miner started..."); - blind_signer(&naka_conf, &signers, proposals_submitted); - - wait_for_first_naka_block_commit(60, &commits_submitted); // make another tenure next_block_and_mine_commit( - &mut btc_regtest_controller, + btc_regtest_controller, 60, &coord_channel, &commits_submitted, @@ -9736,7 +9670,6 @@ fn test_shadow_recovery() { }) .unwrap(); - let burn_height_after = get_chain_info(&naka_conf).burn_block_height; let stacks_height_before = get_chain_info(&naka_conf).stacks_tip_height; // fix node @@ -9753,14 +9686,14 @@ fn test_shadow_recovery() { .unwrap(); // revive ATC-C by waiting for commits - for i in 0..4 { + for _i in 0..4 { btc_regtest_controller.bootstrap_chain(1); sleep_ms(30_000); } // make another tenure next_block_and_mine_commit( - &mut btc_regtest_controller, + btc_regtest_controller, 60, &coord_channel, &commits_submitted, diff --git a/testnet/stacks-node/src/tests/signer/mod.rs b/testnet/stacks-node/src/tests/signer/mod.rs index 42b894398d..4fa441582e 100644 --- a/testnet/stacks-node/src/tests/signer/mod.rs +++ b/testnet/stacks-node/src/tests/signer/mod.rs @@ -112,7 +112,7 @@ pub struct SignerTest { } impl + Send + 'static, T: SignerEventTrait + 'static> SignerTest> { - fn new(num_signers: usize, initial_balances: Vec<(StacksAddress, u64)>) -> Self { + pub fn new(num_signers: usize, initial_balances: Vec<(StacksAddress, u64)>) -> Self { Self::new_with_config_modifications( num_signers, initial_balances, @@ -123,7 +123,7 @@ impl + Send + 'static, T: SignerEventTrait + 'static> SignerTest (), G: FnMut(&mut NeonConfig) -> (), >( diff --git a/testnet/stacks-node/src/tests/signer/v0.rs b/testnet/stacks-node/src/tests/signer/v0.rs index a8e64decc7..e1f0458aa5 100644 --- a/testnet/stacks-node/src/tests/signer/v0.rs +++ b/testnet/stacks-node/src/tests/signer/v0.rs @@ -227,7 +227,7 @@ impl SignerTest { } /// Run the test until the epoch 3 boundary - fn boot_to_epoch_3(&mut self) { + pub fn boot_to_epoch_3(&mut self) { boot_to_epoch_3_reward_set( &self.running_nodes.conf, &self.running_nodes.blocks_processed,