Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Test: Add replay block command for nakamoto blocks #5346

Merged
merged 7 commits into from
Nov 19, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions stackslib/src/chainstate/nakamoto/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2012,6 +2012,7 @@ impl NakamotoChainState {
commit_burn,
sortition_burn,
&active_reward_set,
false,
) {
Ok(next_chain_tip_info) => (Some(next_chain_tip_info), None),
Err(e) => (None, Some(e)),
Expand Down Expand Up @@ -3901,6 +3902,7 @@ impl NakamotoChainState {
burnchain_commit_burn: u64,
burnchain_sortition_burn: u64,
active_reward_set: &RewardSet,
do_not_advance: bool,
) -> Result<
(
StacksEpochReceipt,
Expand Down Expand Up @@ -4095,6 +4097,7 @@ impl NakamotoChainState {
burn_dbconn,
block,
parent_coinbase_height,
do_not_advance,
)?;
if new_tenure {
// tenure height must have advanced
Expand Down Expand Up @@ -4275,6 +4278,46 @@ impl NakamotoChainState {
.as_ref()
.map(|rewards| rewards.reward_info.clone());

if do_not_advance {
kantai marked this conversation as resolved.
Show resolved Hide resolved
// get burn block stats, for the transaction receipt
let (parent_burn_block_hash, parent_burn_block_height, parent_burn_block_timestamp) =
if block.is_first_mined() {
(BurnchainHeaderHash([0; 32]), 0, 0)
kantai marked this conversation as resolved.
Show resolved Hide resolved
} else {
let sn = SortitionDB::get_block_snapshot_consensus(burn_dbconn, &parent_ch)?
.ok_or_else(|| {
// shouldn't happen
warn!(
"CORRUPTION: {} does not correspond to a burn block",
&parent_ch
);
ChainstateError::InvalidStacksBlock("No parent consensus hash".into())
})?;
(
sn.burn_header_hash,
sn.block_height,
sn.burn_header_timestamp,
)
};

let epoch_receipt = StacksEpochReceipt {
header: StacksHeaderInfo::regtest_genesis(),
tx_receipts,
matured_rewards,
matured_rewards_info: matured_rewards_info_opt,
parent_microblocks_cost: ExecutionCost::zero(),
anchored_block_cost: block_execution_cost,
parent_burn_block_hash,
parent_burn_block_height: u32::try_from(parent_burn_block_height).unwrap_or(0), // shouldn't be fatal
parent_burn_block_timestamp,
evaluated_epoch,
epoch_transition: applied_epoch_transition,
signers_updated: signer_set_calc.is_some(),
};

return Ok((epoch_receipt, clarity_commit, None));
}

let new_tip = Self::advance_tip(
&mut chainstate_tx.tx,
&parent_chain_tip.anchored_header,
Expand Down
4 changes: 4 additions & 0 deletions stackslib/src/chainstate/nakamoto/tenure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -840,6 +840,7 @@ impl NakamotoChainState {
handle: &mut SH,
block: &NakamotoBlock,
parent_coinbase_height: u64,
do_not_advance: bool,
) -> Result<u64, ChainstateError> {
let Some(tenure_payload) = block.get_tenure_tx_payload() else {
// no new tenure
Expand Down Expand Up @@ -867,6 +868,9 @@ impl NakamotoChainState {
));
};

if do_not_advance {
return Ok(coinbase_height);
}
Self::insert_nakamoto_tenure(headers_tx, &block.header, coinbase_height, tenure_payload)?;
return Ok(coinbase_height);
}
Expand Down
170 changes: 155 additions & 15 deletions stackslib/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ use crate::util_lib::db::IndexDBTx;

/// Can be used with CLI commands to support non-mainnet chainstate
/// Allows integration testing of these functions
#[derive(Deserialize)]
pub struct StacksChainConfig {
pub chain_id: u32,
pub first_block_height: u64,
Expand All @@ -68,6 +69,44 @@ impl StacksChainConfig {
epochs: STACKS_EPOCHS_MAINNET.to_vec(),
}
}

pub fn default_testnet() -> Self {
let mut pox_constants = PoxConstants::regtest_default();
pox_constants.prepare_length = 100;
pox_constants.reward_cycle_length = 900;
pox_constants.v1_unlock_height = 3;
pox_constants.v2_unlock_height = 5;
pox_constants.pox_3_activation_height = 5;
pox_constants.pox_4_activation_height = 6;
pox_constants.v3_unlock_height = 7;
let mut epochs = STACKS_EPOCHS_REGTEST.to_vec();
epochs[0].start_height = 0;
obycode marked this conversation as resolved.
Show resolved Hide resolved
epochs[0].end_height = 0;
epochs[1].start_height = 0;
epochs[1].end_height = 1;
epochs[2].start_height = 1;
epochs[2].end_height = 2;
epochs[3].start_height = 2;
epochs[3].end_height = 3;
epochs[4].start_height = 3;
epochs[4].end_height = 4;
epochs[5].start_height = 4;
epochs[5].end_height = 5;
epochs[6].start_height = 5;
epochs[6].end_height = 6;
epochs[7].start_height = 6;
epochs[7].end_height = 56_457;
epochs[8].start_height = 56_457;
Self {
chain_id: CHAIN_ID_TESTNET,
first_block_height: 0,
first_burn_header_hash: BurnchainHeaderHash::from_hex(BITCOIN_REGTEST_FIRST_BLOCK_HASH)
.unwrap(),
first_burn_header_timestamp: BITCOIN_REGTEST_FIRST_BLOCK_TIMESTAMP.into(),
pox_constants,
epochs,
}
}
}

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

/// Replay blocks from chainstate database
/// Terminates on error using `process::exit()`
///
/// Arguments:
/// - `argv`: Args in CLI format: `<command-name> [args...]`
pub fn command_replay_block_nakamoto(argv: &[String], conf: Option<&StacksChainConfig>) {
let print_help_and_exit = || -> ! {
let n = &argv[0];
eprintln!("Usage:");
eprintln!(" {n} <database-path>");
eprintln!(" {n} <database-path> prefix <index-block-hash-prefix>");
eprintln!(" {n} <database-path> index-range <start-block> <end-block>");
eprintln!(" {n} <database-path> range <start-block> <end-block>");
eprintln!(" {n} <database-path> <first|last> <block-count>");
process::exit(1);
};
let start = Instant::now();
let db_path = argv.get(1).unwrap_or_else(|| print_help_and_exit());
let mode = argv.get(2).map(String::as_str);

let chain_state_path = format!("{db_path}/chainstate/");

let default_conf = STACKS_CHAIN_CONFIG_DEFAULT_MAINNET;
let conf = conf.unwrap_or(&default_conf);

let mainnet = conf.chain_id == CHAIN_ID_MAINNET;
let (chainstate, _) =
StacksChainState::open(mainnet, conf.chain_id, &chain_state_path, None).unwrap();

let conn = chainstate.nakamoto_blocks_db();

let query = match mode {
Some("prefix") => format!(
"SELECT index_block_hash FROM nakamoto_staging_blocks WHERE orphaned = 0 AND index_block_hash LIKE \"{}%\"",
argv[3]
),
Some("first") => format!(
"SELECT index_block_hash FROM nakamoto_staging_blocks WHERE orphaned = 0 ORDER BY height ASC LIMIT {}",
argv[3]
),
Some("range") => {
let arg4 = argv[3]
.parse::<u64>()
.expect("<start_block> not a valid u64");
let arg5 = argv[4].parse::<u64>().expect("<end-block> not a valid u64");
let start = arg4.saturating_sub(1);
let blocks = arg5.saturating_sub(arg4);
format!("SELECT index_block_hash FROM nakamoto_staging_blocks WHERE orphaned = 0 ORDER BY height ASC LIMIT {start}, {blocks}")
}
Some("index-range") => {
let start = argv[3]
.parse::<u64>()
.expect("<start_block> not a valid u64");
let end = argv[4].parse::<u64>().expect("<end-block> not a valid u64");
let blocks = end.saturating_sub(start);
format!("SELECT index_block_hash FROM nakamoto_staging_blocks WHERE orphaned = 0 ORDER BY index_block_hash ASC LIMIT {start}, {blocks}")
}
Some("last") => format!(
"SELECT index_block_hash FROM nakamoto_staging_blocks WHERE orphaned = 0 ORDER BY height DESC LIMIT {}",
argv[3]
),
Some(_) => print_help_and_exit(),
// Default to ALL blocks
None => "SELECT index_block_hash FROM nakamoto_staging_blocks WHERE orphaned = 0".into(),
};

let mut stmt = conn.prepare(&query).unwrap();
let mut hashes_set = stmt.query(NO_PARAMS).unwrap();

let mut index_block_hashes: Vec<String> = vec![];
while let Ok(Some(row)) = hashes_set.next() {
index_block_hashes.push(row.get(0).unwrap());
}

let total = index_block_hashes.len();
println!("Will check {total} blocks");
for (i, index_block_hash) in index_block_hashes.iter().enumerate() {
if i % 100 == 0 {
println!("Checked {i}...");
}
replay_naka_staging_block(db_path, index_block_hash, &conf);
}
println!("Finished. run_time_seconds = {}", start.elapsed().as_secs());
}

/// Replay mock mined blocks from JSON files
/// Terminates on error using `process::exit()`
///
Expand Down Expand Up @@ -525,11 +649,39 @@ fn replay_block(
};
}

/// Fetch and process a NakamotoBlock from database and call `replay_block_nakamoto()` to validate
fn replay_naka_staging_block(db_path: &str, index_block_hash_hex: &str, conf: &StacksChainConfig) {
let block_id = StacksBlockId::from_hex(index_block_hash_hex).unwrap();
let chain_state_path = format!("{db_path}/chainstate/");
let sort_db_path = format!("{db_path}/burnchain/sortition");

let mainnet = conf.chain_id == CHAIN_ID_MAINNET;
let (mut chainstate, _) =
StacksChainState::open(mainnet, conf.chain_id, &chain_state_path, None).unwrap();

let mut sortdb = SortitionDB::connect(
&sort_db_path,
conf.first_block_height,
&conf.first_burn_header_hash,
conf.first_burn_header_timestamp,
&conf.epochs,
conf.pox_constants.clone(),
None,
true,
)
.unwrap();

let (block, block_size) = chainstate
.nakamoto_blocks_db()
.get_nakamoto_block(&block_id)
.unwrap()
.unwrap();
replay_block_nakamoto(&mut sortdb, &mut chainstate, &block, block_size).unwrap();
}

fn replay_block_nakamoto(
sort_db: &mut SortitionDB,
stacks_chain_state: &mut StacksChainState,
mut chainstate_tx: ChainstateTx,
clarity_instance: &mut ClarityInstance,
block: &NakamotoBlock,
block_size: u64,
) -> Result<(), ChainstateError> {
Expand Down Expand Up @@ -758,6 +910,7 @@ fn replay_block_nakamoto(
commit_burn,
sortition_burn,
&active_reward_set,
true,
) {
Ok(next_chain_tip_info) => (Some(next_chain_tip_info), None),
Err(e) => (None, Some(e)),
Expand Down Expand Up @@ -785,18 +938,5 @@ fn replay_block_nakamoto(
return Err(e);
};

let (receipt, clarity_commit, reward_set_data) = ok_opt.expect("FATAL: unreachable");

assert_eq!(
receipt.header.anchored_header.block_hash(),
block.header.block_hash()
);
assert_eq!(receipt.header.consensus_hash, block.header.consensus_hash);

info!(
"Advanced to new tip! {}/{}",
&receipt.header.consensus_hash,
&receipt.header.anchored_header.block_hash()
);
Ok(())
}
29 changes: 29 additions & 0 deletions stackslib/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1470,6 +1470,35 @@ simulating a miner.
process::exit(0);
}

if argv[1] == "replay-naka-block" {
let chain_config =
if let Some(network_flag_ix) = argv.iter().position(|arg| arg == "--network") {
let Some(network_choice) = argv.get(network_flag_ix + 1) else {
eprintln!("Must supply network choice after `--network` option");
process::exit(1);
};

let network_config = match network_choice.as_str() {
kantai marked this conversation as resolved.
Show resolved Hide resolved
"testnet" => cli::StacksChainConfig::default_testnet(),
"mainnet" => cli::StacksChainConfig::default_mainnet(),
other => {
eprintln!("Unknown network choice `{other}`");
process::exit(1);
}
};

argv.remove(network_flag_ix + 1);
argv.remove(network_flag_ix);

Some(network_config)
} else {
None
};

cli::command_replay_block_nakamoto(&argv[1..], chain_config.as_ref());
process::exit(0);
}

if argv[1] == "replay-mock-mining" {
cli::command_replay_mock_mining(&argv[1..], None);
process::exit(0);
Expand Down