Skip to content

Commit f334800

Browse files
Merge pull request #7386 from francesco-stacks/perf/validate-block-db-reuse
perf: speed up `stacks-inspect validate-block`: reuse DB connections, cache reward sets
2 parents a774d5a + dfd1641 commit f334800

3 files changed

Lines changed: 193 additions & 88 deletions

File tree

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
`stacks-inspect validate-block` now opens the chainstate and sortition databases once per run instead of once per block, and caches the Nakamoto reward set per reward cycle
2+

contrib/stacks-inspect/src/lib.rs

Lines changed: 182 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
pub mod cli;
1717

18+
use std::collections::HashMap;
1819
use std::io::Write;
1920
use std::time::Instant;
2021
use std::{fs, io, process};
@@ -42,6 +43,7 @@ use stackslib::chainstate::nakamoto::miner::{
4243
BlockMetadata, NakamotoBlockBuilder, NakamotoTenureInfo,
4344
};
4445
use stackslib::chainstate::nakamoto::{NakamotoBlock, NakamotoChainState};
46+
use stackslib::chainstate::stacks::boot::RewardSet;
4547
use stackslib::chainstate::stacks::db::blocks::DummyEventDispatcher;
4648
use stackslib::chainstate::stacks::db::{
4749
ChainstateTx, StacksBlockHeaderTypes, StacksChainState, StacksHeaderInfo,
@@ -347,8 +349,12 @@ pub fn command_validate_block(args: &ValidateBlockArgs, conf: Option<&Config>) {
347349
let mut completed = 0;
348350
let mut errors: Vec<(StacksBlockId, String)> = Vec::new();
349351

352+
let (mut chainstate, mut sortdb) = open_validation_dbs(db_path, conf);
353+
let mut reward_set_cache: HashMap<u64, CachedRewardSet> = HashMap::new();
354+
350355
for entry in work_items {
351-
if let Err(e) = validate_entry(db_path, conf, &entry) {
356+
if let Err(e) = validate_entry(&mut chainstate, &mut sortdb, &mut reward_set_cache, &entry)
357+
{
352358
if early_exit {
353359
print!("\r");
354360
io::stdout().flush().ok();
@@ -358,6 +364,14 @@ pub fn command_validate_block(args: &ValidateBlockArgs, conf: Option<&Config>) {
358364
print!("\r");
359365
io::stdout().flush().ok();
360366
errors.push((entry.index_block_hash.clone(), e));
367+
368+
// not every append_block error path rolls back the Clarity trie
369+
// it opened, and a trie left open panics the next block's begin
370+
chainstate.with_clarity_marf(|marf| {
371+
if marf.get_open_chain_tip().is_some() {
372+
marf.drop_current();
373+
}
374+
});
361375
}
362376
completed += 1;
363377
let pct = ((completed as f32 / total_blocks as f32) * 100.0).floor() as usize;
@@ -385,10 +399,58 @@ pub fn command_validate_block(args: &ValidateBlockArgs, conf: Option<&Config>) {
385399
);
386400
}
387401

388-
fn validate_entry(db_path: &str, conf: &Config, entry: &BlockScanEntry) -> Result<(), String> {
402+
/// Open the chainstate and sortition DBs used by block validation.
403+
/// Called once per run.
404+
fn open_validation_dbs(db_path: &str, conf: &Config) -> (StacksChainState, SortitionDB) {
405+
let chain_state_path = format!("{db_path}/chainstate/");
406+
let sort_db_path = format!("{db_path}/burnchain/sortition");
407+
408+
let (chainstate, _) = StacksChainState::open(
409+
conf.is_mainnet(),
410+
conf.burnchain.chain_id,
411+
&chain_state_path,
412+
None,
413+
)
414+
.unwrap_or_else(|e| {
415+
eprintln!("Failed to open chainstate at {chain_state_path}: {e:?}");
416+
process::exit(1);
417+
});
418+
419+
let burnchain = conf.get_burnchain();
420+
let epochs = conf.burnchain.get_epoch_list();
421+
let sortdb = SortitionDB::connect(
422+
&sort_db_path,
423+
burnchain.first_block_height,
424+
&burnchain.first_block_hash,
425+
u64::from(burnchain.first_block_timestamp),
426+
&epochs,
427+
burnchain.pox_constants.clone(),
428+
None,
429+
true,
430+
None,
431+
)
432+
.unwrap_or_else(|e| {
433+
eprintln!("Failed to open sortition DB at {sort_db_path}: {e:?}");
434+
process::exit(1);
435+
});
436+
437+
(chainstate, sortdb)
438+
}
439+
440+
fn validate_entry(
441+
chainstate: &mut StacksChainState,
442+
sortdb: &mut SortitionDB,
443+
reward_set_cache: &mut HashMap<u64, CachedRewardSet>,
444+
entry: &BlockScanEntry,
445+
) -> Result<(), String> {
389446
match entry.source {
390-
BlockSource::Nakamoto => replay_naka_staging_block(db_path, &entry.index_block_hash, conf),
391-
BlockSource::Epoch2 => replay_staging_block(db_path, &entry.index_block_hash, conf),
447+
BlockSource::Nakamoto => replay_naka_staging_block(
448+
chainstate,
449+
sortdb,
450+
reward_set_cache,
451+
&entry.index_block_hash,
452+
),
453+
BlockSource::Epoch2 => replay_staging_block(chainstate, sortdb, &entry.index_block_hash),
392454
}
393455
}
394456

@@ -660,36 +722,10 @@ pub fn command_contract_hash(args: &ContractHashArgs, _conf: Option<&Config>) {
660722

661723
/// Fetch and process a `StagingBlock` from database and call `replay_block()` to validate
662724
fn replay_staging_block(
663-
db_path: &str,
725+
chainstate: &mut StacksChainState,
726+
sortdb: &mut SortitionDB,
664727
block_id: &StacksBlockId,
665-
conf: &Config,
666728
) -> Result<(), String> {
667-
let chain_state_path = format!("{db_path}/chainstate/");
668-
let sort_db_path = format!("{db_path}/burnchain/sortition");
669-
670-
let (mut chainstate, _) = StacksChainState::open(
671-
conf.is_mainnet(),
672-
conf.burnchain.chain_id,
673-
&chain_state_path,
674-
None,
675-
)
676-
.map_err(|e| format!("Failed to open chainstate at {chain_state_path}: {e:?}"))?;
677-
678-
let burnchain = conf.get_burnchain();
679-
let epochs = conf.burnchain.get_epoch_list();
680-
let mut sortdb = SortitionDB::connect(
681-
&sort_db_path,
682-
burnchain.first_block_height,
683-
&burnchain.first_block_hash,
684-
u64::from(burnchain.first_block_timestamp),
685-
&epochs,
686-
burnchain.pox_constants.clone(),
687-
None,
688-
true,
689-
None,
690-
)
691-
.map_err(|e| format!("Failed to open sortition DB at {sort_db_path}: {e:?}"))?;
692-
693729
let sort_tx = sortdb.tx_begin_at_tip();
694730

695731
let blocks_path = chainstate.blocks_path.clone();
@@ -918,7 +954,10 @@ fn replay_block(
918954
block_sortition_burn,
919955
true,
920956
) {
921-
Ok((receipt, _, _)) => {
957+
Ok((receipt, clarity_commit, _)) => {
958+
// validation only: discard the block's uncommitted trie so the
959+
// long-lived ClarityInstance can begin the next block
960+
clarity_commit.rollback();
922961
if let Some(cost) = cost_opt {
923962
if receipt.anchored_block_cost != cost {
924963
return Err(format!(
@@ -940,50 +979,106 @@ fn replay_block(
940979

941980
/// Fetch and process a NakamotoBlock from database and call `replay_block_nakamoto()` to validate
942981
fn replay_naka_staging_block(
943-
db_path: &str,
982+
chainstate: &mut StacksChainState,
983+
sortdb: &mut SortitionDB,
984+
reward_set_cache: &mut HashMap<u64, CachedRewardSet>,
944985
block_id: &StacksBlockId,
945-
conf: &Config,
946986
) -> Result<(), String> {
947-
let chain_state_path = format!("{db_path}/chainstate/");
948-
let sort_db_path = format!("{db_path}/burnchain/sortition");
949-
950-
let (mut chainstate, _) = StacksChainState::open(
951-
conf.is_mainnet(),
952-
conf.burnchain.chain_id,
953-
&chain_state_path,
954-
None,
955-
)
956-
.map_err(|e| format!("Failed to open chainstate: {e:?}"))?;
957-
958-
let burnchain = conf.get_burnchain();
959-
let epochs = conf.burnchain.get_epoch_list();
960-
let mut sortdb = SortitionDB::connect(
961-
&sort_db_path,
962-
burnchain.first_block_height,
963-
&burnchain.first_block_hash,
964-
u64::from(burnchain.first_block_timestamp),
965-
&epochs,
966-
burnchain.pox_constants.clone(),
967-
None,
968-
true,
969-
None,
970-
)
971-
.map_err(|e| format!("Failed to open sortition DB: {e:?}"))?;
972-
973987
let (block, block_size) = chainstate
974988
.nakamoto_blocks_db()
975989
.get_nakamoto_block(block_id)
976990
.map_err(|e| format!("Failed to load Nakamoto block: {e:?}"))?
977991
.ok_or_else(|| "No block data found".to_string())?;
978992

979-
replay_block_nakamoto(&mut sortdb, &mut chainstate, &block, block_size)
993+
replay_block_nakamoto(sortdb, chainstate, reward_set_cache, &block, block_size)
980994
.map_err(|e| format!("Failed to validate Nakamoto block: {e:?}"))
981995
}
982996

997+
/// Reward set cached per cycle, tagged with the block that calculated it so
998+
/// hits can be verified fork-aware (see [`load_reward_set_cached`]).
999+
struct CachedRewardSet {
1000+
calc_coinbase_height: u64,
1001+
calc_block_id: StacksBlockId,
1002+
reward_set: RewardSet,
1003+
}
1004+
1005+
/// Load the reward set of `cycle` as seen from `parent_block_id`, caching per
1006+
/// cycle.
1007+
///
1008+
/// A reward set is identified by its *calculation block* (where `.signers`
1009+
/// was written for the cycle), resolved through the parent's fork. A cache
1010+
/// hit is used only after re-resolving the cached calculation height through
1011+
/// this block's parent and checking it lands on the same calculation block —
1012+
/// forks that diverged before the calculation block (and so may carry a
1013+
/// different reward set) recompute instead of reusing the wrong entry. The
1014+
/// expensive step (a read-only Clarity eval on `.signers`) runs once per
1015+
/// cycle-and-fork-lineage.
1016+
fn load_reward_set_cached<'a>(
1017+
cycle: u64,
1018+
reward_set_cache: &'a mut HashMap<u64, CachedRewardSet>,
1019+
stacks_chain_state: &mut StacksChainState,
1020+
sort_db: &SortitionDB,
1021+
parent_block_id: &StacksBlockId,
1022+
) -> Result<&'a RewardSet, String> {
1023+
let cached_ok = match reward_set_cache.get(&cycle) {
1024+
Some(entry) => NakamotoChainState::get_header_by_coinbase_height(
1025+
&mut stacks_chain_state.index_conn(),
1026+
parent_block_id,
1027+
entry.calc_coinbase_height,
1028+
)
1029+
.map_err(|e| format!("Failed to resolve cached calculation block: {e:?}"))?
1030+
.is_some_and(|hdr| hdr.index_block_hash() == entry.calc_block_id),
1031+
None => false,
1032+
};
1033+
1034+
if !cached_ok {
1035+
let provider = OnChainRewardSetProvider::<DummyEventDispatcher>(None);
1036+
// anchor the burn view at the block itself
1037+
let sort_handle = sort_db
1038+
.index_handle_at_block(stacks_chain_state, parent_block_id)
1039+
.map_err(|e| format!("Failed to open sortition handle at {parent_block_id}: {e:?}"))?;
1040+
let calc_coinbase_height = provider
1041+
.get_height_of_pox_calculation(cycle, stacks_chain_state, &sort_handle, parent_block_id)
1042+
.map_err(|e| {
1043+
format!("Failed to find reward-set calculation height of cycle {cycle}: {e:?}")
1044+
})?;
1045+
let calc_block_id = NakamotoChainState::get_header_by_coinbase_height(
1046+
&mut stacks_chain_state.index_conn(),
1047+
parent_block_id,
1048+
calc_coinbase_height,
1049+
)
1050+
.map_err(|e| format!("Failed to load calculation block header: {e:?}"))?
1051+
.ok_or_else(|| format!("No block at coinbase height {calc_coinbase_height}"))?
1052+
.index_block_hash();
1053+
let reward_set = provider
1054+
.read_reward_set_at_calculated_block(
1055+
calc_coinbase_height,
1056+
stacks_chain_state,
1057+
parent_block_id,
1058+
true,
1059+
)
1060+
.map_err(|e| format!("Failed to read reward set of cycle {cycle}: {e:?}"))?;
1061+
reward_set_cache.insert(
1062+
cycle,
1063+
CachedRewardSet {
1064+
calc_coinbase_height,
1065+
calc_block_id,
1066+
reward_set,
1067+
},
1068+
);
1069+
}
1070+
1071+
Ok(&reward_set_cache
1072+
.get(&cycle)
1073+
.expect("reward set just inserted")
1074+
.reward_set)
1075+
}
1076+
9831077
#[allow(clippy::result_large_err)]
9841078
fn replay_block_nakamoto(
9851079
sort_db: &mut SortitionDB,
9861080
stacks_chain_state: &mut StacksChainState,
1081+
reward_set_cache: &mut HashMap<u64, CachedRewardSet>,
9871082
block: &NakamotoBlock,
9881083
block_size: u64,
9891084
) -> Result<(), ChainstateError> {
@@ -1069,25 +1164,24 @@ fn replay_block_nakamoto(
10691164
"Elected in block height before first_block_height".into(),
10701165
)
10711166
})?;
1072-
let active_reward_set = OnChainRewardSetProvider::<DummyEventDispatcher>(None)
1073-
.read_reward_set_nakamoto_of_cycle(
1074-
elected_in_cycle,
1075-
stacks_chain_state,
1076-
sort_db,
1077-
&block.header.parent_block_id,
1078-
true,
1079-
)
1080-
.map_err(|e| {
1081-
warn!(
1082-
"Cannot process Nakamoto block: could not load reward set that elected the block";
1083-
"err" => ?e,
1084-
"consensus_hash" => %block.header.consensus_hash,
1085-
"stacks_block_hash" => %block.header.block_hash(),
1086-
"stacks_block_id" => %block.header.block_id(),
1087-
"parent_block_id" => %block.header.parent_block_id,
1088-
);
1089-
ChainstateError::NoSuchBlockError
1090-
})?;
1167+
let active_reward_set = load_reward_set_cached(
1168+
elected_in_cycle,
1169+
reward_set_cache,
1170+
stacks_chain_state,
1171+
sort_db,
1172+
&block.header.parent_block_id,
1173+
)
1174+
.map_err(|e| {
1175+
warn!(
1176+
"Cannot process Nakamoto block: could not load reward set that elected the block";
1177+
"err" => %e,
1178+
"consensus_hash" => %block.header.consensus_hash,
1179+
"stacks_block_hash" => %block.header.block_hash(),
1180+
"stacks_block_id" => %block.header.block_id(),
1181+
"parent_block_id" => %block.header.parent_block_id,
1182+
);
1183+
ChainstateError::NoSuchBlockError
1184+
})?;
10911185
let (mut chainstate_tx, clarity_instance) = stacks_chain_state.chainstate_tx_begin();
10921186

10931187
// find parent header
@@ -1110,8 +1204,6 @@ fn replay_block_nakamoto(
11101204
&parent_header_info.anchored_header.block_hash(),
11111205
);
11121206
if parent_block_id != block.header.parent_block_id {
1113-
drop(chainstate_tx);
1114-
11151207
let msg = "Discontinuous Nakamoto Stacks block";
11161208
warn!("{}", &msg;
11171209
"child parent_block_id" => %block.header.parent_block_id,
@@ -1259,10 +1351,15 @@ fn replay_block_nakamoto(
12591351
block_size,
12601352
commit_burn,
12611353
sortition_burn,
1262-
&active_reward_set,
1354+
active_reward_set,
12631355
true,
12641356
) {
1265-
Ok((receipt, _, _, _)) => (Some(receipt), None),
1357+
Ok((receipt, clarity_commit, _, _)) => {
1358+
// validation only: discard the block's uncommitted trie so the
1359+
// long-lived ClarityInstance can begin the next block
1360+
clarity_commit.rollback();
1361+
(Some(receipt), None)
1362+
}
12661363
Err(e) => (None, Some(e)),
12671364
};
12681365

@@ -1277,9 +1374,6 @@ fn replay_block_nakamoto(
12771374
}
12781375

12791376
if let Some(e) = err_opt {
1280-
// force rollback
1281-
drop(chainstate_tx);
1282-
12831377
warn!(
12841378
"Failed to append {}/{}: {:?}",
12851379
&block.header.consensus_hash,

stackslib/src/clarity_vm/clarity.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -925,6 +925,15 @@ impl PreCommitClarityBlock<'_> {
925925
.commit_to_processed_block(&self.commit_to)
926926
.expect("FATAL: failed to commit block");
927927
}
928+
929+
/// Discard the block's uncommitted trie instead of committing it.
930+
/// For replay/validation tooling that processes blocks against a
931+
/// long-lived `ClarityInstance` and throws the resulting state away:
932+
/// the instance cannot `begin` the next block while a trie is open.
933+
pub fn rollback(self) {
934+
debug!("Rolling back pre-commit Clarity block connection"; "index_block" => %self.commit_to);
935+
self.datastore.drop_current_trie();
936+
}
928937
}
929938

930939
impl<'a, 'b> ClarityBlockConnection<'a, 'b> {

0 commit comments

Comments
 (0)