Skip to content

Commit 894a3b2

Browse files
committed
get replay blocks working with nakamoto blocks
1 parent f245115 commit 894a3b2

File tree

4 files changed

+208
-16
lines changed

4 files changed

+208
-16
lines changed

stackslib/src/chainstate/nakamoto/mod.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2012,6 +2012,7 @@ impl NakamotoChainState {
20122012
commit_burn,
20132013
sortition_burn,
20142014
&active_reward_set,
2015+
false,
20152016
) {
20162017
Ok(next_chain_tip_info) => (Some(next_chain_tip_info), None),
20172018
Err(e) => (None, Some(e)),
@@ -3901,6 +3902,7 @@ impl NakamotoChainState {
39013902
burnchain_commit_burn: u64,
39023903
burnchain_sortition_burn: u64,
39033904
active_reward_set: &RewardSet,
3905+
do_not_advance: bool,
39043906
) -> Result<
39053907
(
39063908
StacksEpochReceipt,
@@ -4095,6 +4097,7 @@ impl NakamotoChainState {
40954097
burn_dbconn,
40964098
block,
40974099
parent_coinbase_height,
4100+
do_not_advance,
40984101
)?;
40994102
if new_tenure {
41004103
// tenure height must have advanced
@@ -4275,6 +4278,46 @@ impl NakamotoChainState {
42754278
.as_ref()
42764279
.map(|rewards| rewards.reward_info.clone());
42774280

4281+
if do_not_advance {
4282+
// get burn block stats, for the transaction receipt
4283+
let (parent_burn_block_hash, parent_burn_block_height, parent_burn_block_timestamp) =
4284+
if block.is_first_mined() {
4285+
(BurnchainHeaderHash([0; 32]), 0, 0)
4286+
} else {
4287+
let sn = SortitionDB::get_block_snapshot_consensus(burn_dbconn, &parent_ch)?
4288+
.ok_or_else(|| {
4289+
// shouldn't happen
4290+
warn!(
4291+
"CORRUPTION: {} does not correspond to a burn block",
4292+
&parent_ch
4293+
);
4294+
ChainstateError::InvalidStacksBlock("No parent consensus hash".into())
4295+
})?;
4296+
(
4297+
sn.burn_header_hash,
4298+
sn.block_height,
4299+
sn.burn_header_timestamp,
4300+
)
4301+
};
4302+
4303+
let epoch_receipt = StacksEpochReceipt {
4304+
header: StacksHeaderInfo::regtest_genesis(),
4305+
tx_receipts,
4306+
matured_rewards,
4307+
matured_rewards_info: matured_rewards_info_opt,
4308+
parent_microblocks_cost: ExecutionCost::zero(),
4309+
anchored_block_cost: block_execution_cost,
4310+
parent_burn_block_hash,
4311+
parent_burn_block_height: u32::try_from(parent_burn_block_height).unwrap_or(0), // shouldn't be fatal
4312+
parent_burn_block_timestamp,
4313+
evaluated_epoch,
4314+
epoch_transition: applied_epoch_transition,
4315+
signers_updated: signer_set_calc.is_some(),
4316+
};
4317+
4318+
return Ok((epoch_receipt, clarity_commit, None));
4319+
}
4320+
42784321
let new_tip = Self::advance_tip(
42794322
&mut chainstate_tx.tx,
42804323
&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: 156 additions & 16 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,10 +69,48 @@ impl StacksChainConfig {
6869
epochs: STACKS_EPOCHS_MAINNET.to_vec(),
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 = STACKS_EPOCHS_REGTEST.to_vec();
83+
epochs[0].start_height = 0;
84+
epochs[0].end_height = 0;
85+
epochs[1].start_height = 0;
86+
epochs[1].end_height = 1;
87+
epochs[2].start_height = 1;
88+
epochs[2].end_height = 2;
89+
epochs[3].start_height = 2;
90+
epochs[3].end_height = 3;
91+
epochs[4].start_height = 3;
92+
epochs[4].end_height = 4;
93+
epochs[5].start_height = 4;
94+
epochs[5].end_height = 5;
95+
epochs[6].start_height = 5;
96+
epochs[6].end_height = 6;
97+
epochs[7].start_height = 6;
98+
epochs[7].end_height = 56_457;
99+
epochs[8].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> =
74-
LazyCell::new(StacksChainConfig::default_mainnet);
113+
LazyCell::new(StacksChainConfig::default_testnet);
75114

76115
/// Replay blocks from chainstate database
77116
/// Terminates on error using `process::exit()`
@@ -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,11 +649,39 @@ 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,
531-
mut chainstate_tx: ChainstateTx,
532-
clarity_instance: &mut ClarityInstance,
533685
block: &NakamotoBlock,
534686
block_size: u64,
535687
) -> Result<(), ChainstateError> {
@@ -758,6 +910,7 @@ fn replay_block_nakamoto(
758910
commit_burn,
759911
sortition_burn,
760912
&active_reward_set,
913+
true,
761914
) {
762915
Ok(next_chain_tip_info) => (Some(next_chain_tip_info), None),
763916
Err(e) => (None, Some(e)),
@@ -785,18 +938,5 @@ fn replay_block_nakamoto(
785938
return Err(e);
786939
};
787940

788-
let (receipt, clarity_commit, reward_set_data) = ok_opt.expect("FATAL: unreachable");
789-
790-
assert_eq!(
791-
receipt.header.anchored_header.block_hash(),
792-
block.header.block_hash()
793-
);
794-
assert_eq!(receipt.header.consensus_hash, block.header.consensus_hash);
795-
796-
info!(
797-
"Advanced to new tip! {}/{}",
798-
&receipt.header.consensus_hash,
799-
&receipt.header.anchored_header.block_hash()
800-
);
801941
Ok(())
802942
}

stackslib/src/main.rs

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

1473+
if argv[1] == "replay-naka-block" {
1474+
cli::command_replay_block_nakamoto(&argv[1..], None);
1475+
process::exit(0);
1476+
}
1477+
14731478
if argv[1] == "replay-mock-mining" {
14741479
cli::command_replay_mock_mining(&argv[1..], None);
14751480
process::exit(0);

0 commit comments

Comments
 (0)