Skip to content

Commit c3fec9e

Browse files
authored
Merge pull request #5346 from stacks-network/test/replay-block-naka
Test: Add replay block command for nakamoto blocks
2 parents a81469f + 98d3638 commit c3fec9e

File tree

4 files changed

+265
-13
lines changed

4 files changed

+265
-13
lines changed

stackslib/src/chainstate/nakamoto/mod.rs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2020,6 +2020,7 @@ impl NakamotoChainState {
20202020
commit_burn,
20212021
sortition_burn,
20222022
&active_reward_set,
2023+
false,
20232024
) {
20242025
Ok(next_chain_tip_info) => (Some(next_chain_tip_info), None),
20252026
Err(e) => (None, Some(e)),
@@ -3893,6 +3894,62 @@ impl NakamotoChainState {
38933894
Ok(())
38943895
}
38953896

3897+
pub(crate) fn make_non_advancing_receipt<'a>(
3898+
clarity_commit: PreCommitClarityBlock<'a>,
3899+
burn_dbconn: &SortitionHandleConn,
3900+
parent_ch: &ConsensusHash,
3901+
evaluated_epoch: StacksEpochId,
3902+
matured_rewards: Vec<MinerReward>,
3903+
tx_receipts: Vec<StacksTransactionReceipt>,
3904+
matured_rewards_info_opt: Option<MinerRewardInfo>,
3905+
block_execution_cost: ExecutionCost,
3906+
applied_epoch_transition: bool,
3907+
signers_updated: bool,
3908+
coinbase_height: u64,
3909+
) -> Result<
3910+
(
3911+
StacksEpochReceipt,
3912+
PreCommitClarityBlock<'a>,
3913+
Option<RewardSetData>,
3914+
),
3915+
ChainstateError,
3916+
> {
3917+
// get burn block stats, for the transaction receipt
3918+
3919+
let parent_sn = SortitionDB::get_block_snapshot_consensus(burn_dbconn, &parent_ch)?
3920+
.ok_or_else(|| {
3921+
// shouldn't happen
3922+
warn!(
3923+
"CORRUPTION: {} does not correspond to a burn block",
3924+
&parent_ch
3925+
);
3926+
ChainstateError::InvalidStacksBlock("No parent consensus hash".into())
3927+
})?;
3928+
let (parent_burn_block_hash, parent_burn_block_height, parent_burn_block_timestamp) = (
3929+
parent_sn.burn_header_hash,
3930+
parent_sn.block_height,
3931+
parent_sn.burn_header_timestamp,
3932+
);
3933+
3934+
let epoch_receipt = StacksEpochReceipt {
3935+
header: StacksHeaderInfo::regtest_genesis(),
3936+
tx_receipts,
3937+
matured_rewards,
3938+
matured_rewards_info: matured_rewards_info_opt,
3939+
parent_microblocks_cost: ExecutionCost::zero(),
3940+
anchored_block_cost: block_execution_cost,
3941+
parent_burn_block_hash,
3942+
parent_burn_block_height: u32::try_from(parent_burn_block_height).unwrap_or(0), // shouldn't be fatal
3943+
parent_burn_block_timestamp,
3944+
evaluated_epoch,
3945+
epoch_transition: applied_epoch_transition,
3946+
signers_updated,
3947+
coinbase_height,
3948+
};
3949+
3950+
return Ok((epoch_receipt, clarity_commit, None));
3951+
}
3952+
38963953
/// Append a Nakamoto Stacks block to the Stacks chain state.
38973954
/// NOTE: This does _not_ set the block as processed! The caller must do this.
38983955
pub(crate) fn append_block<'a>(
@@ -3910,6 +3967,7 @@ impl NakamotoChainState {
39103967
burnchain_commit_burn: u64,
39113968
burnchain_sortition_burn: u64,
39123969
active_reward_set: &RewardSet,
3970+
do_not_advance: bool,
39133971
) -> Result<
39143972
(
39153973
StacksEpochReceipt,
@@ -4104,6 +4162,7 @@ impl NakamotoChainState {
41044162
burn_dbconn,
41054163
block,
41064164
parent_coinbase_height,
4165+
do_not_advance,
41074166
)?;
41084167
if new_tenure {
41094168
// tenure height must have advanced
@@ -4284,6 +4343,24 @@ impl NakamotoChainState {
42844343
.as_ref()
42854344
.map(|rewards| rewards.reward_info.clone());
42864345

4346+
if do_not_advance {
4347+
// if we're performing a block replay, and we don't want to advance any
4348+
// of the db state, return a fake receipt
4349+
return Self::make_non_advancing_receipt(
4350+
clarity_commit,
4351+
burn_dbconn,
4352+
&parent_ch,
4353+
evaluated_epoch,
4354+
matured_rewards,
4355+
tx_receipts,
4356+
matured_rewards_info_opt,
4357+
block_execution_cost,
4358+
applied_epoch_transition,
4359+
signer_set_calc.is_some(),
4360+
coinbase_height,
4361+
);
4362+
}
4363+
42874364
let new_tip = Self::advance_tip(
42884365
&mut chainstate_tx.tx,
42894366
&parent_chain_tip.anchored_header,

stackslib/src/chainstate/nakamoto/tenure.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -840,6 +840,7 @@ impl NakamotoChainState {
840840
handle: &mut SH,
841841
block: &NakamotoBlock,
842842
parent_coinbase_height: u64,
843+
do_not_advance: bool,
843844
) -> Result<u64, ChainstateError> {
844845
let Some(tenure_payload) = block.get_tenure_tx_payload() else {
845846
// no new tenure
@@ -867,6 +868,9 @@ impl NakamotoChainState {
867868
));
868869
};
869870

871+
if do_not_advance {
872+
return Ok(coinbase_height);
873+
}
870874
Self::insert_nakamoto_tenure(headers_tx, &block.header, coinbase_height, tenure_payload)?;
871875
return Ok(coinbase_height);
872876
}

stackslib/src/cli.rs

Lines changed: 155 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ use crate::util_lib::db::IndexDBTx;
4747

4848
/// Can be used with CLI commands to support non-mainnet chainstate
4949
/// Allows integration testing of these functions
50+
#[derive(Deserialize)]
5051
pub struct StacksChainConfig {
5152
pub chain_id: u32,
5253
pub first_block_height: u64,
@@ -68,6 +69,44 @@ impl StacksChainConfig {
6869
epochs: (*STACKS_EPOCHS_MAINNET).clone(),
6970
}
7071
}
72+
73+
pub fn default_testnet() -> Self {
74+
let mut pox_constants = PoxConstants::regtest_default();
75+
pox_constants.prepare_length = 100;
76+
pox_constants.reward_cycle_length = 900;
77+
pox_constants.v1_unlock_height = 3;
78+
pox_constants.v2_unlock_height = 5;
79+
pox_constants.pox_3_activation_height = 5;
80+
pox_constants.pox_4_activation_height = 6;
81+
pox_constants.v3_unlock_height = 7;
82+
let mut epochs = EpochList::new(&*STACKS_EPOCHS_REGTEST);
83+
epochs[StacksEpochId::Epoch10].start_height = 0;
84+
epochs[StacksEpochId::Epoch10].end_height = 0;
85+
epochs[StacksEpochId::Epoch20].start_height = 0;
86+
epochs[StacksEpochId::Epoch20].end_height = 1;
87+
epochs[StacksEpochId::Epoch2_05].start_height = 1;
88+
epochs[StacksEpochId::Epoch2_05].end_height = 2;
89+
epochs[StacksEpochId::Epoch21].start_height = 2;
90+
epochs[StacksEpochId::Epoch21].end_height = 3;
91+
epochs[StacksEpochId::Epoch22].start_height = 3;
92+
epochs[StacksEpochId::Epoch22].end_height = 4;
93+
epochs[StacksEpochId::Epoch23].start_height = 4;
94+
epochs[StacksEpochId::Epoch23].end_height = 5;
95+
epochs[StacksEpochId::Epoch24].start_height = 5;
96+
epochs[StacksEpochId::Epoch24].end_height = 6;
97+
epochs[StacksEpochId::Epoch25].start_height = 6;
98+
epochs[StacksEpochId::Epoch25].end_height = 56_457;
99+
epochs[StacksEpochId::Epoch30].start_height = 56_457;
100+
Self {
101+
chain_id: CHAIN_ID_TESTNET,
102+
first_block_height: 0,
103+
first_burn_header_hash: BurnchainHeaderHash::from_hex(BITCOIN_REGTEST_FIRST_BLOCK_HASH)
104+
.unwrap(),
105+
first_burn_header_timestamp: BITCOIN_REGTEST_FIRST_BLOCK_TIMESTAMP.into(),
106+
pox_constants,
107+
epochs,
108+
}
109+
}
71110
}
72111

73112
const STACKS_CHAIN_CONFIG_DEFAULT_MAINNET: LazyCell<StacksChainConfig> =
@@ -151,6 +190,91 @@ pub fn command_replay_block(argv: &[String], conf: Option<&StacksChainConfig>) {
151190
println!("Finished. run_time_seconds = {}", start.elapsed().as_secs());
152191
}
153192

193+
/// Replay blocks from chainstate database
194+
/// Terminates on error using `process::exit()`
195+
///
196+
/// Arguments:
197+
/// - `argv`: Args in CLI format: `<command-name> [args...]`
198+
pub fn command_replay_block_nakamoto(argv: &[String], conf: Option<&StacksChainConfig>) {
199+
let print_help_and_exit = || -> ! {
200+
let n = &argv[0];
201+
eprintln!("Usage:");
202+
eprintln!(" {n} <database-path>");
203+
eprintln!(" {n} <database-path> prefix <index-block-hash-prefix>");
204+
eprintln!(" {n} <database-path> index-range <start-block> <end-block>");
205+
eprintln!(" {n} <database-path> range <start-block> <end-block>");
206+
eprintln!(" {n} <database-path> <first|last> <block-count>");
207+
process::exit(1);
208+
};
209+
let start = Instant::now();
210+
let db_path = argv.get(1).unwrap_or_else(|| print_help_and_exit());
211+
let mode = argv.get(2).map(String::as_str);
212+
213+
let chain_state_path = format!("{db_path}/chainstate/");
214+
215+
let default_conf = STACKS_CHAIN_CONFIG_DEFAULT_MAINNET;
216+
let conf = conf.unwrap_or(&default_conf);
217+
218+
let mainnet = conf.chain_id == CHAIN_ID_MAINNET;
219+
let (chainstate, _) =
220+
StacksChainState::open(mainnet, conf.chain_id, &chain_state_path, None).unwrap();
221+
222+
let conn = chainstate.nakamoto_blocks_db();
223+
224+
let query = match mode {
225+
Some("prefix") => format!(
226+
"SELECT index_block_hash FROM nakamoto_staging_blocks WHERE orphaned = 0 AND index_block_hash LIKE \"{}%\"",
227+
argv[3]
228+
),
229+
Some("first") => format!(
230+
"SELECT index_block_hash FROM nakamoto_staging_blocks WHERE orphaned = 0 ORDER BY height ASC LIMIT {}",
231+
argv[3]
232+
),
233+
Some("range") => {
234+
let arg4 = argv[3]
235+
.parse::<u64>()
236+
.expect("<start_block> not a valid u64");
237+
let arg5 = argv[4].parse::<u64>().expect("<end-block> not a valid u64");
238+
let start = arg4.saturating_sub(1);
239+
let blocks = arg5.saturating_sub(arg4);
240+
format!("SELECT index_block_hash FROM nakamoto_staging_blocks WHERE orphaned = 0 ORDER BY height ASC LIMIT {start}, {blocks}")
241+
}
242+
Some("index-range") => {
243+
let start = argv[3]
244+
.parse::<u64>()
245+
.expect("<start_block> not a valid u64");
246+
let end = argv[4].parse::<u64>().expect("<end-block> not a valid u64");
247+
let blocks = end.saturating_sub(start);
248+
format!("SELECT index_block_hash FROM nakamoto_staging_blocks WHERE orphaned = 0 ORDER BY index_block_hash ASC LIMIT {start}, {blocks}")
249+
}
250+
Some("last") => format!(
251+
"SELECT index_block_hash FROM nakamoto_staging_blocks WHERE orphaned = 0 ORDER BY height DESC LIMIT {}",
252+
argv[3]
253+
),
254+
Some(_) => print_help_and_exit(),
255+
// Default to ALL blocks
256+
None => "SELECT index_block_hash FROM nakamoto_staging_blocks WHERE orphaned = 0".into(),
257+
};
258+
259+
let mut stmt = conn.prepare(&query).unwrap();
260+
let mut hashes_set = stmt.query(NO_PARAMS).unwrap();
261+
262+
let mut index_block_hashes: Vec<String> = vec![];
263+
while let Ok(Some(row)) = hashes_set.next() {
264+
index_block_hashes.push(row.get(0).unwrap());
265+
}
266+
267+
let total = index_block_hashes.len();
268+
println!("Will check {total} blocks");
269+
for (i, index_block_hash) in index_block_hashes.iter().enumerate() {
270+
if i % 100 == 0 {
271+
println!("Checked {i}...");
272+
}
273+
replay_naka_staging_block(db_path, index_block_hash, &conf);
274+
}
275+
println!("Finished. run_time_seconds = {}", start.elapsed().as_secs());
276+
}
277+
154278
/// Replay mock mined blocks from JSON files
155279
/// Terminates on error using `process::exit()`
156280
///
@@ -525,6 +649,36 @@ fn replay_block(
525649
};
526650
}
527651

652+
/// Fetch and process a NakamotoBlock from database and call `replay_block_nakamoto()` to validate
653+
fn replay_naka_staging_block(db_path: &str, index_block_hash_hex: &str, conf: &StacksChainConfig) {
654+
let block_id = StacksBlockId::from_hex(index_block_hash_hex).unwrap();
655+
let chain_state_path = format!("{db_path}/chainstate/");
656+
let sort_db_path = format!("{db_path}/burnchain/sortition");
657+
658+
let mainnet = conf.chain_id == CHAIN_ID_MAINNET;
659+
let (mut chainstate, _) =
660+
StacksChainState::open(mainnet, conf.chain_id, &chain_state_path, None).unwrap();
661+
662+
let mut sortdb = SortitionDB::connect(
663+
&sort_db_path,
664+
conf.first_block_height,
665+
&conf.first_burn_header_hash,
666+
conf.first_burn_header_timestamp,
667+
&conf.epochs,
668+
conf.pox_constants.clone(),
669+
None,
670+
true,
671+
)
672+
.unwrap();
673+
674+
let (block, block_size) = chainstate
675+
.nakamoto_blocks_db()
676+
.get_nakamoto_block(&block_id)
677+
.unwrap()
678+
.unwrap();
679+
replay_block_nakamoto(&mut sortdb, &mut chainstate, &block, block_size).unwrap();
680+
}
681+
528682
fn replay_block_nakamoto(
529683
sort_db: &mut SortitionDB,
530684
stacks_chain_state: &mut StacksChainState,
@@ -756,6 +910,7 @@ fn replay_block_nakamoto(
756910
commit_burn,
757911
sortition_burn,
758912
&active_reward_set,
913+
true,
759914
) {
760915
Ok(next_chain_tip_info) => (Some(next_chain_tip_info), None),
761916
Err(e) => (None, Some(e)),
@@ -783,18 +938,5 @@ fn replay_block_nakamoto(
783938
return Err(e);
784939
};
785940

786-
let (receipt, _clarity_commit, _reward_set_data) = ok_opt.expect("FATAL: unreachable");
787-
788-
assert_eq!(
789-
receipt.header.anchored_header.block_hash(),
790-
block.header.block_hash()
791-
);
792-
assert_eq!(receipt.header.consensus_hash, block.header.consensus_hash);
793-
794-
info!(
795-
"Advanced to new tip! {}/{}",
796-
&receipt.header.consensus_hash,
797-
&receipt.header.anchored_header.block_hash()
798-
);
799941
Ok(())
800942
}

stackslib/src/main.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1470,6 +1470,35 @@ simulating a miner.
14701470
process::exit(0);
14711471
}
14721472

1473+
if argv[1] == "replay-naka-block" {
1474+
let chain_config =
1475+
if let Some(network_flag_ix) = argv.iter().position(|arg| arg == "--network") {
1476+
let Some(network_choice) = argv.get(network_flag_ix + 1) else {
1477+
eprintln!("Must supply network choice after `--network` option");
1478+
process::exit(1);
1479+
};
1480+
1481+
let network_config = match network_choice.to_lowercase().as_str() {
1482+
"testnet" => cli::StacksChainConfig::default_testnet(),
1483+
"mainnet" => cli::StacksChainConfig::default_mainnet(),
1484+
other => {
1485+
eprintln!("Unknown network choice `{other}`");
1486+
process::exit(1);
1487+
}
1488+
};
1489+
1490+
argv.remove(network_flag_ix + 1);
1491+
argv.remove(network_flag_ix);
1492+
1493+
Some(network_config)
1494+
} else {
1495+
None
1496+
};
1497+
1498+
cli::command_replay_block_nakamoto(&argv[1..], chain_config.as_ref());
1499+
process::exit(0);
1500+
}
1501+
14731502
if argv[1] == "replay-mock-mining" {
14741503
cli::command_replay_mock_mining(&argv[1..], None);
14751504
process::exit(0);

0 commit comments

Comments
 (0)