1- use std:: sync:: Arc ;
1+ use std:: { collections :: BTreeSet , sync:: Arc } ;
22
3+ use nimiq_account:: Account ;
34use nimiq_block:: BlockError ;
45use nimiq_blockchain:: { interface:: HistoryInterface , BlockProducer , Blockchain , BlockchainConfig } ;
56use nimiq_blockchain_interface:: { AbstractBlockchain , BlockchainEvent , PushError , PushResult } ;
6- use nimiq_database:: mdbx:: MdbxDatabase ;
7+ use nimiq_database:: { mdbx:: MdbxDatabase , traits :: WriteTransaction } ;
78use nimiq_genesis:: NetworkId ;
8- use nimiq_primitives:: policy:: Policy ;
9+ use nimiq_primitives:: { key_nibbles :: KeyNibbles , policy:: Policy } ;
910use nimiq_serde:: { Deserialize , Serialize } ;
1011use nimiq_test_log:: test;
1112use nimiq_test_utils:: {
@@ -288,12 +289,10 @@ fn history_sync_emits_protocol_upgrade_event_when_version_changes() {
288289 ) ) ;
289290
290291 let producer = BlockProducer :: new ( signing_key ( ) , voting_key ( ) ) ;
291- produce_macro_blocks_with_txns (
292+ produce_macro_blocks (
292293 & producer,
293294 & blockchain1,
294295 ( Policy :: batches_per_epoch ( ) - 1 ) as usize ,
295- 1 ,
296- 0 ,
297296 ) ;
298297 let upgrade_version = signal_next_protocol_version_via_tx ( & producer, & blockchain1) ;
299298 fill_micro_blocks_with_txns ( & producer, & blockchain1, 1 , 0 ) ;
@@ -308,7 +307,7 @@ fn history_sync_emits_protocol_upgrade_event_when_version_changes() {
308307 let upgrade_history = blockchain1
309308 . read ( )
310309 . history_store
311- . get_epoch_transactions ( 1 , None ) ;
310+ . get_epoch_transactions ( upgrade_block . epoch_number ( ) , None ) ;
312311
313312 let time2 = Arc :: new ( OffsetTime :: new ( ) ) ;
314313 let env2 = MdbxDatabase :: new_volatile ( Default :: default ( ) ) . unwrap ( ) ;
@@ -341,6 +340,253 @@ fn history_sync_emits_protocol_upgrade_event_when_version_changes() {
341340 ) ;
342341}
343342
343+ #[ test]
344+ fn history_sync_replays_empty_first_batch_when_it_changes_state ( ) {
345+ fn seed_current_batch_punished_slot ( blockchain : & Arc < RwLock < Blockchain > > ) {
346+ let blockchain = blockchain. write ( ) ;
347+ let mut staking_contract = blockchain. get_staking_contract ( ) ;
348+ let validator_address = staking_contract
349+ . active_validators
350+ . keys ( )
351+ . next ( )
352+ . expect ( "missing active validator" )
353+ . clone ( ) ;
354+
355+ staking_contract
356+ . punished_slots
357+ . current_batch_punished_slots
358+ . clear ( ) ;
359+ staking_contract
360+ . punished_slots
361+ . previous_batch_punished_slots
362+ . clear ( ) ;
363+
364+ let mut punished_slots = BTreeSet :: new ( ) ;
365+ punished_slots. insert ( 0 ) ;
366+ staking_contract
367+ . punished_slots
368+ . current_batch_punished_slots
369+ . insert ( validator_address, punished_slots) ;
370+
371+ let mut txn = blockchain. write_transaction ( ) ;
372+ blockchain
373+ . state
374+ . accounts
375+ . tree
376+ . put (
377+ & mut ( & mut txn) . into ( ) ,
378+ & KeyNibbles :: from ( & Policy :: STAKING_CONTRACT_ADDRESS ) ,
379+ Account :: Staking ( staking_contract) ,
380+ )
381+ . unwrap ( ) ;
382+ txn. commit ( ) ;
383+ }
384+
385+ let genesis_block_number = Policy :: genesis_block_number ( ) ;
386+ let num_macro_blocks = 2 ;
387+
388+ // Create a blockchain to produce the macro blocks.
389+ let time = Arc :: new ( OffsetTime :: new ( ) ) ;
390+ let env = MdbxDatabase :: new_volatile ( Default :: default ( ) ) . unwrap ( ) ;
391+ let blockchain = Arc :: new ( RwLock :: new (
392+ Blockchain :: new (
393+ env,
394+ BlockchainConfig :: default ( ) ,
395+ NetworkId :: UnitAlbatross ,
396+ time,
397+ )
398+ . unwrap ( ) ,
399+ ) ) ;
400+ seed_current_batch_punished_slot ( & blockchain) ;
401+
402+ // Create a second blockchain to push blocks to.
403+ let time = Arc :: new ( OffsetTime :: new ( ) ) ;
404+ let env2 = MdbxDatabase :: new_volatile ( Default :: default ( ) ) . unwrap ( ) ;
405+ let blockchain2 = Arc :: new ( RwLock :: new (
406+ Blockchain :: new (
407+ env2,
408+ BlockchainConfig :: default ( ) ,
409+ NetworkId :: UnitAlbatross ,
410+ time,
411+ )
412+ . unwrap ( ) ,
413+ ) ) ;
414+ seed_current_batch_punished_slot ( & blockchain2) ;
415+
416+ // Produce the blocks on blockchain1.
417+ let producer = BlockProducer :: new ( signing_key ( ) , voting_key ( ) ) ;
418+ produce_macro_blocks ( & producer, & blockchain, num_macro_blocks) ;
419+
420+ // Get the first checkpoint and corresponding history tree transactions.
421+ let blockchain_rg = blockchain. read ( ) ;
422+ let election_txs_1 = blockchain_rg. history_store . get_epoch_transactions ( 1 , None ) ;
423+
424+ let checkpoint_block_1_2 = blockchain_rg
425+ . chain_store
426+ . get_block_at (
427+ Policy :: blocks_per_batch ( ) * 2 + genesis_block_number,
428+ true ,
429+ None ,
430+ )
431+ . unwrap ( ) ;
432+ let mut checkpoint_txs_1_2 = vec ! [ ] ;
433+
434+ for hist_tx in & election_txs_1 {
435+ if hist_tx. block_number > Policy :: blocks_per_batch ( ) * 2 + genesis_block_number {
436+ break ;
437+ }
438+ checkpoint_txs_1_2. push ( hist_tx. clone ( ) ) ;
439+ }
440+
441+ // The first checkpoint of the first epoch has no history items of its own. Replaying it still
442+ // matters here because its FinalizeBatch clears the seeded punished slots before the next
443+ // checkpoint reward is applied.
444+ assert_eq ! (
445+ Blockchain :: push_history_sync(
446+ blockchain2. upgradable_read( ) ,
447+ checkpoint_block_1_2,
448+ & checkpoint_txs_1_2
449+ ) ,
450+ Ok ( PushResult :: Extended )
451+ ) ;
452+ }
453+
454+ #[ test]
455+ fn history_sync_replays_missing_first_checkpoint_before_second_batch_history ( ) {
456+ let genesis_block_number = Policy :: genesis_block_number ( ) ;
457+
458+ let time = Arc :: new ( OffsetTime :: new ( ) ) ;
459+ let env = MdbxDatabase :: new_volatile ( Default :: default ( ) ) . unwrap ( ) ;
460+ let blockchain1 = Arc :: new ( RwLock :: new (
461+ Blockchain :: new (
462+ env,
463+ BlockchainConfig :: default ( ) ,
464+ NetworkId :: UnitAlbatross ,
465+ time,
466+ )
467+ . unwrap ( ) ,
468+ ) ) ;
469+
470+ let producer = BlockProducer :: new ( signing_key ( ) , voting_key ( ) ) ;
471+
472+ // Leave the first batch empty so the first checkpoint is missing from history, then add
473+ // transactions in the second batch before producing the target checkpoint.
474+ produce_macro_blocks ( & producer, & blockchain1, 1 ) ;
475+ fill_micro_blocks_with_txns ( & producer, & blockchain1, 1 , 0 ) ;
476+ produce_macro_blocks ( & producer, & blockchain1, 1 ) ;
477+
478+ let blockchain_rg = blockchain1. read ( ) ;
479+ let checkpoint_block_1_2 = blockchain_rg
480+ . chain_store
481+ . get_block_at (
482+ Policy :: blocks_per_batch ( ) * 2 + genesis_block_number,
483+ true ,
484+ None ,
485+ )
486+ . unwrap ( ) ;
487+ let checkpoint_txs_1_2 = blockchain_rg. history_store . get_epoch_transactions ( 1 , None ) ;
488+
489+ assert ! (
490+ checkpoint_txs_1_2
491+ . iter( )
492+ . all( |hist_tx| hist_tx. block_number > Policy :: blocks_per_batch( ) + genesis_block_number) ,
493+ "test setup must keep the first batch empty" ,
494+ ) ;
495+ assert ! (
496+ checkpoint_txs_1_2
497+ . iter( )
498+ . any( |hist_tx| Policy :: batch_at( hist_tx. block_number) == 2 ) ,
499+ "test setup must retain second-batch history" ,
500+ ) ;
501+
502+ let time = Arc :: new ( OffsetTime :: new ( ) ) ;
503+ let env2 = MdbxDatabase :: new_volatile ( Default :: default ( ) ) . unwrap ( ) ;
504+ let blockchain2 = Arc :: new ( RwLock :: new (
505+ Blockchain :: new (
506+ env2,
507+ BlockchainConfig :: default ( ) ,
508+ NetworkId :: UnitAlbatross ,
509+ time,
510+ )
511+ . unwrap ( ) ,
512+ ) ) ;
513+
514+ assert_eq ! (
515+ Blockchain :: push_history_sync(
516+ blockchain2. upgradable_read( ) ,
517+ checkpoint_block_1_2,
518+ & checkpoint_txs_1_2
519+ ) ,
520+ Ok ( PushResult :: Extended )
521+ ) ;
522+ assert_eq ! ( blockchain_rg. head( ) , blockchain2. read( ) . head( ) ) ;
523+ }
524+
525+ #[ test]
526+ fn history_sync_rejects_missing_non_first_batch_history ( ) {
527+ let genesis_block_number = Policy :: genesis_block_number ( ) ;
528+ let num_macro_blocks = 3 ;
529+
530+ let time = Arc :: new ( OffsetTime :: new ( ) ) ;
531+ let env = MdbxDatabase :: new_volatile ( Default :: default ( ) ) . unwrap ( ) ;
532+ let blockchain = Arc :: new ( RwLock :: new (
533+ Blockchain :: new (
534+ env,
535+ BlockchainConfig :: default ( ) ,
536+ NetworkId :: UnitAlbatross ,
537+ time,
538+ )
539+ . unwrap ( ) ,
540+ ) ) ;
541+
542+ let producer = BlockProducer :: new ( signing_key ( ) , voting_key ( ) ) ;
543+ produce_macro_blocks_with_txns ( & producer, & blockchain, num_macro_blocks, 1 , 0 ) ;
544+
545+ let blockchain_rg = blockchain. read ( ) ;
546+ let election_txs_1 = blockchain_rg. history_store . get_epoch_transactions ( 1 , None ) ;
547+ let checkpoint_block_1_3 = blockchain_rg
548+ . chain_store
549+ . get_block_at (
550+ Policy :: blocks_per_batch ( ) * 3 + genesis_block_number,
551+ true ,
552+ None ,
553+ )
554+ . unwrap ( ) ;
555+
556+ let checkpoint_txs_1_3: Vec < _ > = election_txs_1
557+ . into_iter ( )
558+ . filter ( |hist_tx| {
559+ hist_tx. block_number > Policy :: blocks_per_batch ( ) * 2 + genesis_block_number
560+ } )
561+ . collect ( ) ;
562+
563+ assert ! (
564+ !checkpoint_txs_1_3. is_empty( ) ,
565+ "test setup must retain history after the skipped batch" ,
566+ ) ;
567+
568+ let time = Arc :: new ( OffsetTime :: new ( ) ) ;
569+ let env2 = MdbxDatabase :: new_volatile ( Default :: default ( ) ) . unwrap ( ) ;
570+ let blockchain2 = Arc :: new ( RwLock :: new (
571+ Blockchain :: new (
572+ env2,
573+ BlockchainConfig :: default ( ) ,
574+ NetworkId :: UnitAlbatross ,
575+ time,
576+ )
577+ . unwrap ( ) ,
578+ ) ) ;
579+
580+ assert_eq ! (
581+ Blockchain :: push_history_sync(
582+ blockchain2. upgradable_read( ) ,
583+ checkpoint_block_1_3,
584+ & checkpoint_txs_1_3
585+ ) ,
586+ Err ( PushError :: InvalidBlock ( BlockError :: InvalidHistoryRoot ) )
587+ ) ;
588+ }
589+
344590// Tests if the history sync works when micro blocks have already been pushed in the blockchain.
345591// This basically tests if we can go from the history sync to the normal follow mode and back.
346592#[ test]
0 commit comments