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"] 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 diff --git a/stackslib/src/burnchains/mod.rs b/stackslib/src/burnchains/mod.rs index e541a2062c..3e153df53b 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 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; diff --git a/stackslib/src/chainstate/nakamoto/shadow.rs b/stackslib/src/chainstate/nakamoto/shadow.rs index 9ce296a975..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 { @@ -461,7 +463,7 @@ impl NakamotoBlockBuilder { } /// Get an address's account - fn get_account( + pub fn get_account( chainstate: &mut StacksChainState, sortdb: &SortitionDB, addr: &StacksAddress, @@ -561,13 +563,17 @@ 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)) } /// Produce a single-block shadow tenure. /// Used by tooling to synthesize shadow blocks in case of an emergency. - /// The details and circumstances will be recorded in an accompanying SIP. + /// 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, @@ -704,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); @@ -852,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) +} 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, diff --git a/stackslib/src/main.rs b/stackslib/src/main.rs index 98315cffa8..f112f8b18c 100644 --- a/stackslib/src/main.rs +++ b/stackslib/src/main.rs @@ -49,6 +49,10 @@ 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::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::{ @@ -247,6 +251,56 @@ impl P2PSession { } } +fn open_nakamoto_chainstate_dbs( + chainstate_dir: &str, + network: &str, +) -> (SortitionDB, StacksChainState) { + 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, dirname); + let sort_db_path = format!("{}/{}/burnchain/sortition/", chainstate_dir, dirname); + + 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) + .expect("Failed to open stacks chain state"); + + (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(); @@ -1166,6 +1220,204 @@ simulating a miner. println!("{:?}", inv); } + if argv[1] == "get-nakamoto-tip" { + if argv.len() < 4 { + eprintln!( + "Usage: {} get-nakamoto-tip 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 (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()); + + check_shadow_network(network); + 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(); + + 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() + .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); + } + + // 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_hex).unwrap()); + 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-patch CHAINSTATE_DIR NETWORK SHADOW_BLOCKS_PATH.JSON", + &argv[0] + ); + process::exit(1); + } + + let chainstate_dir = argv[2].as_str(); + let network = argv[3].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: 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!( + "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()); + + check_shadow_network(network); + 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]); 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) 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)) = diff --git a/testnet/stacks-node/src/tests/nakamoto_integrations.rs b/testnet/stacks-node/src/tests/nakamoto_integrations.rs index 90334cce9b..08dbea3f31 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}; @@ -90,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}; @@ -104,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, @@ -9594,3 +9597,173 @@ 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 signer_test: SignerTest = SignerTest::new(1, vec![]); + signer_test.boot_to_epoch_3(); + + 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(); + + // make another tenure + next_block_and_mine_commit( + 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 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( + 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); +} 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,