Skip to content

Commit d038752

Browse files
authored
Merge pull request #5362 from stacks-network/feat/shadow-block-tooling
Feat/shadow block tooling
2 parents 82c36d7 + a3cdab6 commit d038752

File tree

15 files changed

+712
-45
lines changed

15 files changed

+712
-45
lines changed

.github/actions/dockerfiles/Dockerfile.debian-source

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,5 @@ RUN --mount=type=tmpfs,target=${BUILD_DIR} cp -R /src/. ${BUILD_DIR}/ \
2424
&& cp -R ${BUILD_DIR}/target/${TARGET}/release/. /out
2525

2626
FROM --platform=${TARGETPLATFORM} debian:bookworm
27-
COPY --from=build /out/stacks-node /out/stacks-signer /bin/
27+
COPY --from=build /out/stacks-node /out/stacks-signer /out/stacks-inspect /bin/
2828
CMD ["stacks-node", "mainnet"]

.github/workflows/bitcoin-tests.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ jobs:
139139
- tests::nakamoto_integrations::utxo_check_on_startup_panic
140140
- tests::nakamoto_integrations::utxo_check_on_startup_recover
141141
- tests::nakamoto_integrations::v3_signer_api_endpoint
142+
- tests::nakamoto_integrations::test_shadow_recovery
142143
- tests::nakamoto_integrations::signer_chainstate
143144
- tests::nakamoto_integrations::clarity_cost_spend_down
144145
# TODO: enable these once v1 signer is supported by a new nakamoto epoch

stackslib/src/burnchains/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,7 @@ impl PoxConstants {
450450
)
451451
}
452452

453+
// NOTE: this is the *old* pre-Nakamoto testnet
453454
pub fn testnet_default() -> PoxConstants {
454455
PoxConstants::new(
455456
POX_REWARD_CYCLE_LENGTH / 2, // 1050

stackslib/src/chainstate/nakamoto/coordinator/mod.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ use crate::monitoring::increment_stx_blocks_processed_counter;
5858
use crate::net::Error as NetError;
5959
use crate::util_lib::db::Error as DBError;
6060

61+
#[cfg(any(test, feature = "testing"))]
62+
pub static TEST_COORDINATOR_STALL: std::sync::Mutex<Option<bool>> = std::sync::Mutex::new(None);
63+
6164
#[cfg(test)]
6265
pub mod tests;
6366

@@ -764,6 +767,21 @@ impl<
764767
true
765768
}
766769

770+
#[cfg(any(test, feature = "testing"))]
771+
fn fault_injection_pause_nakamoto_block_processing() {
772+
if *TEST_COORDINATOR_STALL.lock().unwrap() == Some(true) {
773+
// Do an extra check just so we don't log EVERY time.
774+
warn!("Coordinator is stalled due to testing directive");
775+
while *TEST_COORDINATOR_STALL.lock().unwrap() == Some(true) {
776+
std::thread::sleep(std::time::Duration::from_millis(10));
777+
}
778+
warn!("Coordinator is no longer stalled due to testing directive. Continuing...");
779+
}
780+
}
781+
782+
#[cfg(not(any(test, feature = "testing")))]
783+
fn fault_injection_pause_nakamoto_block_processing() {}
784+
767785
/// Handle one or more new Nakamoto Stacks blocks.
768786
/// If we process a PoX anchor block, then return its block hash. This unblocks processing the
769787
/// next reward cycle's burnchain blocks. Subsequent calls to this function will terminate
@@ -776,6 +794,8 @@ impl<
776794
);
777795

778796
loop {
797+
Self::fault_injection_pause_nakamoto_block_processing();
798+
779799
// process at most one block per loop pass
780800
let mut processed_block_receipt = match NakamotoChainState::process_next_nakamoto_block(
781801
&mut self.chain_state_db,

stackslib/src/chainstate/nakamoto/miner.rs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -410,11 +410,11 @@ impl NakamotoBlockBuilder {
410410
burn_dbconn: &'a SortitionHandleConn,
411411
info: &'b mut MinerTenureInfo<'a>,
412412
) -> Result<ClarityTx<'b, 'b>, Error> {
413-
if info.tenure_block_commit_opt.is_none() {
413+
let Some(block_commit) = info.tenure_block_commit_opt.as_ref() else {
414414
return Err(Error::InvalidStacksBlock(
415415
"Block-commit is required; cannot mine a shadow block".into(),
416416
));
417-
}
417+
};
418418

419419
let SetupBlockResult {
420420
clarity_tx,
@@ -435,10 +435,7 @@ impl NakamotoBlockBuilder {
435435
info.coinbase_height,
436436
info.cause == Some(TenureChangeCause::Extended),
437437
&self.header.pox_treatment,
438-
// safety: checked above
439-
info.tenure_block_commit_opt
440-
.as_ref()
441-
.unwrap_or_else(|| panic!("FATAL: no block-commit for normal Nakamoto block")),
438+
block_commit,
442439
&info.active_reward_set,
443440
)?;
444441
self.matured_miner_rewards_opt = matured_miner_rewards_opt;

stackslib/src/chainstate/nakamoto/shadow.rs

Lines changed: 141 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ use crate::chainstate::nakamoto::{
5555
SortitionHandleConn, StacksDBIndexed,
5656
};
5757
use crate::chainstate::stacks::boot::RewardSet;
58+
use crate::chainstate::stacks::db::blocks::DummyEventDispatcher;
5859
use crate::chainstate::stacks::db::{
5960
ChainstateTx, ClarityTx, StacksAccount, StacksChainState, StacksHeaderInfo,
6061
};
@@ -70,6 +71,7 @@ use crate::chainstate::stacks::{
7071
use crate::clarity::vm::types::StacksAddressExtensions;
7172
use crate::clarity_vm::clarity::ClarityInstance;
7273
use crate::clarity_vm::database::SortitionDBRef;
74+
use crate::net::Error as NetError;
7375
use crate::util_lib::db::{query_row, u64_to_sql, Error as DBError};
7476

7577
impl NakamotoBlockHeader {
@@ -461,7 +463,7 @@ impl NakamotoBlockBuilder {
461463
}
462464

463465
/// Get an address's account
464-
fn get_account(
466+
pub fn get_account(
465467
chainstate: &mut StacksChainState,
466468
sortdb: &SortitionDB,
467469
addr: &StacksAddress,
@@ -561,13 +563,17 @@ impl NakamotoBlockBuilder {
561563
}
562564
let block = builder.mine_nakamoto_block(&mut tenure_tx);
563565
let size = builder.bytes_so_far;
564-
let cost = builder.tenure_finish(tenure_tx).unwrap();
566+
let cost = builder.tenure_finish(tenure_tx)?;
565567
Ok((block, size, cost))
566568
}
567569

568570
/// Produce a single-block shadow tenure.
569571
/// Used by tooling to synthesize shadow blocks in case of an emergency.
570-
/// The details and circumstances will be recorded in an accompanying SIP.
572+
/// The details and circumatances will be recorded in an accompanying SIP.
573+
///
574+
/// `naka_tip_id` is the Stacks chain tip on top of which the shadow block will be built.
575+
/// `tenure_id_consensus_hash` is the sortition in which the shadow block will be built.
576+
/// `txs` are transactions to include, beyond a coinbase and tenure-change
571577
pub fn make_shadow_tenure(
572578
chainstate: &mut StacksChainState,
573579
sortdb: &SortitionDB,
@@ -705,8 +711,7 @@ impl NakamotoBlockBuilder {
705711
Some(&coinbase_tx),
706712
1,
707713
None,
708-
)
709-
.unwrap();
714+
)?;
710715

711716
let mut block_txs = vec![tenure_change_tx, coinbase_tx];
712717
block_txs.append(&mut txs);
@@ -853,3 +858,134 @@ impl<'a> NakamotoStagingBlocksTx<'a> {
853858
Ok(())
854859
}
855860
}
861+
862+
/// DO NOT RUN ON A RUNNING NODE (unless you're testing).
863+
///
864+
/// Insert and process a shadow block into the Stacks chainstate.
865+
pub fn process_shadow_block(
866+
chain_state: &mut StacksChainState,
867+
sort_db: &mut SortitionDB,
868+
shadow_block: NakamotoBlock,
869+
) -> Result<(), ChainstateError> {
870+
let tx = chain_state.staging_db_tx_begin()?;
871+
tx.add_shadow_block(&shadow_block)?;
872+
tx.commit()?;
873+
874+
let no_dispatch: Option<DummyEventDispatcher> = None;
875+
loop {
876+
let sort_tip = SortitionDB::get_canonical_burn_chain_tip(sort_db.conn())?;
877+
878+
// process at most one block per loop pass
879+
let processed_block_receipt = match NakamotoChainState::process_next_nakamoto_block(
880+
chain_state,
881+
sort_db,
882+
&sort_tip.sortition_id,
883+
no_dispatch.as_ref(),
884+
) {
885+
Ok(receipt_opt) => receipt_opt,
886+
Err(ChainstateError::InvalidStacksBlock(msg)) => {
887+
warn!("Encountered invalid block: {}", &msg);
888+
continue;
889+
}
890+
Err(ChainstateError::NetError(NetError::DeserializeError(msg))) => {
891+
// happens if we load a zero-sized block (i.e. an invalid block)
892+
warn!("Encountered invalid block (codec error): {}", &msg);
893+
continue;
894+
}
895+
Err(e) => {
896+
// something else happened
897+
return Err(e.into());
898+
}
899+
};
900+
901+
if processed_block_receipt.is_none() {
902+
// out of blocks
903+
info!("No more blocks to process (no receipts)");
904+
break;
905+
};
906+
907+
let Some((_, processed, orphaned, _)) = chain_state
908+
.nakamoto_blocks_db()
909+
.get_block_processed_and_signed_weight(
910+
&shadow_block.header.consensus_hash,
911+
&shadow_block.header.block_hash(),
912+
)?
913+
else {
914+
return Err(ChainstateError::InvalidStacksBlock(format!(
915+
"Shadow block {} for tenure {} not store",
916+
&shadow_block.block_id(),
917+
&shadow_block.header.consensus_hash
918+
)));
919+
};
920+
921+
if orphaned {
922+
return Err(ChainstateError::InvalidStacksBlock(format!(
923+
"Shadow block {} for tenure {} was orphaned",
924+
&shadow_block.block_id(),
925+
&shadow_block.header.consensus_hash
926+
)));
927+
}
928+
929+
if processed {
930+
break;
931+
}
932+
}
933+
Ok(())
934+
}
935+
936+
/// DO NOT RUN ON A RUNNING NODE (unless you're testing).
937+
///
938+
/// Automatically repair a node that has been stalled due to an empty prepare phase.
939+
/// Works by synthesizing, inserting, and processing shadow tenures in-between the last sortition
940+
/// with a winner and the burnchain tip.
941+
///
942+
/// This is meant to be accessed by the tooling. Once the blocks are synthesized, they would be
943+
/// added into other broken nodes' chainstates by the same tooling. Ultimately, a patched node
944+
/// would be released with these shadow blocks added in as part of the chainstate schema.
945+
///
946+
/// Returns the syntheisized shadow blocks on success.
947+
/// Returns error on failure.
948+
pub fn shadow_chainstate_repair(
949+
chain_state: &mut StacksChainState,
950+
sort_db: &mut SortitionDB,
951+
) -> Result<Vec<NakamotoBlock>, ChainstateError> {
952+
let sort_tip = SortitionDB::get_canonical_burn_chain_tip(sort_db.conn())?;
953+
954+
let header = NakamotoChainState::get_canonical_block_header(chain_state.db(), &sort_db)?
955+
.ok_or_else(|| ChainstateError::NoSuchBlockError)?;
956+
957+
let header_sn =
958+
SortitionDB::get_block_snapshot_consensus(sort_db.conn(), &header.consensus_hash)?
959+
.ok_or_else(|| {
960+
ChainstateError::InvalidStacksBlock(
961+
"Canonical stacks header does not have a sortition".into(),
962+
)
963+
})?;
964+
965+
let mut shadow_blocks = vec![];
966+
for burn_height in (header_sn.block_height + 1)..sort_tip.block_height {
967+
let sort_tip = SortitionDB::get_canonical_burn_chain_tip(sort_db.conn())?;
968+
let sort_handle = sort_db.index_handle(&sort_tip.sortition_id);
969+
let sn = sort_handle
970+
.get_block_snapshot_by_height(burn_height)?
971+
.ok_or_else(|| ChainstateError::InvalidStacksBlock("No sortition at height".into()))?;
972+
973+
let header = NakamotoChainState::get_canonical_block_header(chain_state.db(), &sort_db)?
974+
.ok_or_else(|| ChainstateError::NoSuchBlockError)?;
975+
976+
let chain_tip = header.index_block_hash();
977+
let shadow_block = NakamotoBlockBuilder::make_shadow_tenure(
978+
chain_state,
979+
sort_db,
980+
chain_tip.clone(),
981+
sn.consensus_hash,
982+
vec![],
983+
)?;
984+
985+
shadow_blocks.push(shadow_block.clone());
986+
987+
process_shadow_block(chain_state, sort_db, shadow_block)?;
988+
}
989+
990+
Ok(shadow_blocks)
991+
}

stackslib/src/chainstate/nakamoto/staging_blocks.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,7 @@ impl<'a> NakamotoStagingBlocksConnRef<'a> {
278278
/// There will be at most one such block.
279279
///
280280
/// NOTE: for Nakamoto blocks, the sighash is the same as the block hash.
281-
pub(crate) fn get_block_processed_and_signed_weight(
281+
pub fn get_block_processed_and_signed_weight(
282282
&self,
283283
consensus_hash: &ConsensusHash,
284284
block_hash: &BlockHeaderHash,

0 commit comments

Comments
 (0)