1515
1616pub mod cli;
1717
18+ use std:: collections:: HashMap ;
1819use std:: io:: Write ;
1920use std:: time:: Instant ;
2021use std:: { fs, io, process} ;
@@ -42,6 +43,7 @@ use stackslib::chainstate::nakamoto::miner::{
4243 BlockMetadata , NakamotoBlockBuilder , NakamotoTenureInfo ,
4344} ;
4445use stackslib:: chainstate:: nakamoto:: { NakamotoBlock , NakamotoChainState } ;
46+ use stackslib:: chainstate:: stacks:: boot:: RewardSet ;
4547use stackslib:: chainstate:: stacks:: db:: blocks:: DummyEventDispatcher ;
4648use 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
662724fn 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
942981fn 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) ]
9841078fn 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,
0 commit comments