@@ -43,7 +43,7 @@ use stacks::net::api::postblock_proposal::{ValidateRejectCode, TEST_VALIDATE_STA
43
43
use stacks:: net:: relay:: fault_injection:: set_ignore_block;
44
44
use stacks:: types:: chainstate:: { StacksAddress , StacksBlockId , StacksPrivateKey , StacksPublicKey } ;
45
45
use stacks:: types:: PublicKey ;
46
- use stacks:: util:: hash:: { hex_bytes, Hash160 , MerkleHashFunc } ;
46
+ use stacks:: util:: hash:: { hex_bytes, Hash160 , MerkleHashFunc , MerkleTree , Sha512Trunc256Sum } ;
47
47
use stacks:: util:: secp256k1:: { Secp256k1PrivateKey , Secp256k1PublicKey } ;
48
48
use stacks:: util_lib:: boot:: boot_code_id;
49
49
use stacks:: util_lib:: signed_structured_data:: pox4:: {
@@ -56,16 +56,16 @@ use stacks_signer::chainstate::{ProposalEvalConfig, SortitionsView};
56
56
use stacks_signer:: client:: { SignerSlotID , StackerDB } ;
57
57
use stacks_signer:: config:: { build_signer_config_tomls, GlobalConfig as SignerConfig , Network } ;
58
58
use stacks_signer:: v0:: signer:: {
59
- TEST_IGNORE_ALL_BLOCK_PROPOSALS , TEST_PAUSE_BLOCK_BROADCAST , TEST_REJECT_ALL_BLOCK_PROPOSAL ,
60
- TEST_SKIP_BLOCK_BROADCAST ,
59
+ TEST_IGNORE_ALL_BLOCK_PROPOSALS , TEST_IGNORE_BLOCK_RESPONSES , TEST_PAUSE_BLOCK_BROADCAST ,
60
+ TEST_REJECT_ALL_BLOCK_PROPOSAL , TEST_SKIP_BLOCK_BROADCAST ,
61
61
} ;
62
62
use stacks_signer:: v0:: SpawnedSigner ;
63
63
use tracing_subscriber:: prelude:: * ;
64
64
use tracing_subscriber:: { fmt, EnvFilter } ;
65
65
66
66
use super :: SignerTest ;
67
67
use crate :: config:: { EventKeyType , EventObserverConfig } ;
68
- use crate :: event_dispatcher:: MinedNakamotoBlockEvent ;
68
+ use crate :: event_dispatcher:: { MinedNakamotoBlockEvent , TEST_SKIP_BLOCK_ANNOUNCEMENT } ;
69
69
use crate :: nakamoto_node:: miner:: {
70
70
TEST_BLOCK_ANNOUNCE_STALL , TEST_BROADCAST_STALL , TEST_MINE_STALL ,
71
71
} ;
@@ -375,7 +375,7 @@ impl SignerTest<SpawnedSigner> {
375
375
}
376
376
}
377
377
378
- /// Propose an invalid block to the signers
378
+ /// Propose a block to the signers
379
379
fn propose_block ( & mut self , block : NakamotoBlock , timeout : Duration ) {
380
380
let miners_contract_id = boot_code_id ( MINERS_NAME , false ) ;
381
381
let mut session =
@@ -385,6 +385,7 @@ impl SignerTest<SpawnedSigner> {
385
385
. btc_regtest_controller
386
386
. get_headers_height ( ) ;
387
387
let reward_cycle = self . get_current_reward_cycle ( ) ;
388
+ let signer_signature_hash = block. header . signer_signature_hash ( ) ;
388
389
let message = SignerMessage :: BlockProposal ( BlockProposal {
389
390
block,
390
391
burn_height,
@@ -401,7 +402,7 @@ impl SignerTest<SpawnedSigner> {
401
402
let mut version = 0 ;
402
403
let slot_id = MinerSlotID :: BlockProposal . to_u8 ( ) as u32 ;
403
404
let start = Instant :: now ( ) ;
404
- debug ! ( "Proposing invalid block to signers" ) ;
405
+ debug ! ( "Proposing block to signers: {signer_signature_hash} " ) ;
405
406
while !accepted {
406
407
let mut chunk =
407
408
StackerDBChunkData :: new ( slot_id * 2 , version, message. serialize_to_vec ( ) ) ;
@@ -8557,3 +8558,252 @@ fn tenure_extend_after_2_bad_commits() {
8557
8558
run_loop_2_thread. join ( ) . unwrap ( ) ;
8558
8559
signer_test. shutdown ( ) ;
8559
8560
}
8561
+
8562
+ #[ test]
8563
+ #[ ignore]
8564
+ /// Test that signers that reject a block locally, but that was accepted globally will accept
8565
+ /// only accept a block built upon it when they receive the new block event confirming their prior
8566
+ /// rejected block.
8567
+ ///
8568
+ /// Test Setup:
8569
+ /// The test spins up five stacks signers, one miner Nakamoto node, and a corresponding bitcoind.
8570
+ /// The stacks node is then advanced to Epoch 3.0 boundary to allow block signing.
8571
+ ///
8572
+ /// Test Execution:
8573
+ /// The node mines 1 stacks block N (all signers sign it). <30% of signers are configured to auto reject
8574
+ /// any block proposals, announcement of new blocks are skipped, and signatures ignored by signers.
8575
+ /// The subsequent block N+1 is proposed, triggering one of the <30% signers submit the block to the node
8576
+ /// for validation. The node will fail due to a bad block header hash mismatch (passes height checks)
8577
+ ///
8578
+ /// Test Assertion:
8579
+ /// - All signers accepted block N.
8580
+ /// - Less than 30% of the signers rejected block N+1.
8581
+ /// - The 30% of signers that rejected block N+1, will submit the block for validation
8582
+ /// as it passes preliminary checks (even though its a sister block, it is a sister block to a locally rejected block)
8583
+ fn global_acceptance_depends_on_block_announcement ( ) {
8584
+ if env:: var ( "BITCOIND_TEST" ) != Ok ( "1" . into ( ) ) {
8585
+ return ;
8586
+ }
8587
+
8588
+ tracing_subscriber:: registry ( )
8589
+ . with ( fmt:: layer ( ) )
8590
+ . with ( EnvFilter :: from_default_env ( ) )
8591
+ . init ( ) ;
8592
+
8593
+ info ! ( "------------------------- Test Setup -------------------------" ) ;
8594
+ let num_signers = 5 ;
8595
+ let sender_sk = Secp256k1PrivateKey :: new ( ) ;
8596
+ let sender_addr = tests:: to_addr ( & sender_sk) ;
8597
+ let send_amt = 100 ;
8598
+ let send_fee = 180 ;
8599
+ let nmb_txs = 4 ;
8600
+
8601
+ let recipient = PrincipalData :: from ( StacksAddress :: burn_address ( false ) ) ;
8602
+ let mut signer_test: SignerTest < SpawnedSigner > = SignerTest :: new (
8603
+ num_signers,
8604
+ vec ! [ ( sender_addr, ( send_amt + send_fee) * nmb_txs) ] ,
8605
+ ) ;
8606
+
8607
+ let all_signers: Vec < _ > = signer_test
8608
+ . signer_stacks_private_keys
8609
+ . iter ( )
8610
+ . map ( StacksPublicKey :: from_private)
8611
+ . collect ( ) ;
8612
+
8613
+ let http_origin = format ! ( "http://{}" , & signer_test. running_nodes. conf. node. rpc_bind) ;
8614
+ let short_timeout = 30 ;
8615
+ signer_test. boot_to_epoch_3 ( ) ;
8616
+
8617
+ info ! ( "------------------------- Test Mine Nakamoto Block N -------------------------" ) ;
8618
+ let info_before = signer_test
8619
+ . stacks_client
8620
+ . get_peer_info ( )
8621
+ . expect ( "Failed to get peer info" ) ;
8622
+
8623
+ test_observer:: clear ( ) ;
8624
+ // submit a tx so that the miner will mine a stacks block N
8625
+ let mut sender_nonce = 0 ;
8626
+ let transfer_tx = make_stacks_transfer (
8627
+ & sender_sk,
8628
+ sender_nonce,
8629
+ send_fee,
8630
+ signer_test. running_nodes . conf . burnchain . chain_id ,
8631
+ & recipient,
8632
+ send_amt,
8633
+ ) ;
8634
+ let tx = submit_tx ( & http_origin, & transfer_tx) ;
8635
+ sender_nonce += 1 ;
8636
+ info ! ( "Submitted tx {tx} in to mine block N" ) ;
8637
+
8638
+ wait_for ( short_timeout, || {
8639
+ Ok ( signer_test
8640
+ . stacks_client
8641
+ . get_peer_info ( )
8642
+ . expect ( "Failed to get peer info" )
8643
+ . stacks_tip_height
8644
+ > info_before. stacks_tip_height )
8645
+ } )
8646
+ . expect ( "Timed out waiting for N to be mined and processed" ) ;
8647
+
8648
+ let info_after = signer_test
8649
+ . stacks_client
8650
+ . get_peer_info ( )
8651
+ . expect ( "Failed to get peer info" ) ;
8652
+ assert_eq ! (
8653
+ info_before. stacks_tip_height + 1 ,
8654
+ info_after. stacks_tip_height
8655
+ ) ;
8656
+
8657
+ // Ensure that the block was accepted globally so the stacks tip has advanced to N
8658
+ let nakamoto_blocks = test_observer:: get_mined_nakamoto_blocks ( ) ;
8659
+ let block_n = nakamoto_blocks. last ( ) . unwrap ( ) ;
8660
+ assert_eq ! ( info_after. stacks_tip. to_string( ) , block_n. block_hash) ;
8661
+
8662
+ // Make sure that ALL signers accepted the block proposal
8663
+ signer_test
8664
+ . wait_for_block_acceptance ( short_timeout, & block_n. signer_signature_hash , & all_signers)
8665
+ . expect ( "Timed out waiting for block acceptance of N" ) ;
8666
+
8667
+ info ! ( "------------------------- Mine Nakamoto Block N+1 -------------------------" ) ;
8668
+ // Make less than 30% of the signers reject the block and ensure it is accepted by the node, but not announced.
8669
+ let rejecting_signers: Vec < _ > = all_signers
8670
+ . iter ( )
8671
+ . cloned ( )
8672
+ . take ( num_signers * 3 / 10 )
8673
+ . collect ( ) ;
8674
+ let non_rejecting_signers = all_signers[ num_signers * 3 / 10 ..] . to_vec ( ) ;
8675
+ TEST_REJECT_ALL_BLOCK_PROPOSAL
8676
+ . lock ( )
8677
+ . unwrap ( )
8678
+ . replace ( rejecting_signers. clone ( ) ) ;
8679
+ TEST_SKIP_BLOCK_ANNOUNCEMENT . lock ( ) . unwrap ( ) . replace ( true ) ;
8680
+ TEST_IGNORE_BLOCK_RESPONSES . lock ( ) . unwrap ( ) . replace ( true ) ;
8681
+ TEST_IGNORE_SIGNERS . lock ( ) . unwrap ( ) . replace ( true ) ;
8682
+ test_observer:: clear ( ) ;
8683
+
8684
+ // submit a tx so that the miner will mine a stacks block N+1
8685
+ let info_before = signer_test
8686
+ . stacks_client
8687
+ . get_peer_info ( )
8688
+ . expect ( "Failed to get peer info" ) ;
8689
+ let transfer_tx = make_stacks_transfer (
8690
+ & sender_sk,
8691
+ sender_nonce,
8692
+ send_fee,
8693
+ signer_test. running_nodes . conf . burnchain . chain_id ,
8694
+ & recipient,
8695
+ send_amt,
8696
+ ) ;
8697
+ let tx = submit_tx ( & http_origin, & transfer_tx) ;
8698
+ info ! ( "Submitted tx {tx} in to mine block N+1" ) ;
8699
+
8700
+ let mut proposed_block = None ;
8701
+ let start_time = Instant :: now ( ) ;
8702
+ while proposed_block. is_none ( ) && start_time. elapsed ( ) < Duration :: from_secs ( 30 ) {
8703
+ proposed_block = test_observer:: get_stackerdb_chunks ( )
8704
+ . into_iter ( )
8705
+ . flat_map ( |chunk| chunk. modified_slots )
8706
+ . find_map ( |chunk| {
8707
+ let message = SignerMessage :: consensus_deserialize ( & mut chunk. data . as_slice ( ) )
8708
+ . expect ( "Failed to deserialize SignerMessage" ) ;
8709
+ match message {
8710
+ SignerMessage :: BlockProposal ( proposal) => {
8711
+ if proposal. block . header . consensus_hash
8712
+ == info_before. stacks_tip_consensus_hash
8713
+ {
8714
+ Some ( proposal. block )
8715
+ } else {
8716
+ None
8717
+ }
8718
+ }
8719
+ _ => None ,
8720
+ }
8721
+ } ) ;
8722
+ }
8723
+ let proposed_block = proposed_block. expect ( "Failed to find proposed block within 30s" ) ;
8724
+
8725
+ signer_test
8726
+ . wait_for_block_acceptance (
8727
+ short_timeout,
8728
+ & proposed_block. header . signer_signature_hash ( ) ,
8729
+ & non_rejecting_signers,
8730
+ )
8731
+ . expect ( "Timed out waiting for block acceptance of N+1 by non rejecting signers" ) ;
8732
+
8733
+ signer_test
8734
+ . wait_for_block_rejections ( short_timeout, & rejecting_signers)
8735
+ . expect ( "Timed out waiting for block rejection of N+1' from rejecting signers" ) ;
8736
+
8737
+ info ! (
8738
+ "------------------------- Attempt to Mine Nakamoto Block N+1' -------------------------"
8739
+ ) ;
8740
+ TEST_REJECT_ALL_BLOCK_PROPOSAL
8741
+ . lock ( )
8742
+ . unwrap ( )
8743
+ . replace ( Vec :: new ( ) ) ;
8744
+ test_observer:: clear ( ) ;
8745
+
8746
+ let mut sister_block = proposed_block;
8747
+
8748
+ let transfer_tx_bytes = make_stacks_transfer (
8749
+ & sender_sk,
8750
+ sender_nonce,
8751
+ send_fee,
8752
+ signer_test. running_nodes . conf . burnchain . chain_id ,
8753
+ & recipient,
8754
+ send_amt * 2 ,
8755
+ ) ;
8756
+ let tx = StacksTransaction :: consensus_deserialize ( & mut & transfer_tx_bytes[ ..] ) . unwrap ( ) ;
8757
+ let txs = vec ! [ tx] ;
8758
+ let txid_vecs = txs. iter ( ) . map ( |tx| tx. txid ( ) . as_bytes ( ) . to_vec ( ) ) . collect ( ) ;
8759
+
8760
+ let merkle_tree = MerkleTree :: < Sha512Trunc256Sum > :: new ( & txid_vecs) ;
8761
+ let tx_merkle_root = merkle_tree. root ( ) ;
8762
+ sister_block. txs = txs;
8763
+ sister_block. header . tx_merkle_root = tx_merkle_root;
8764
+ sister_block
8765
+ . header
8766
+ . sign_miner ( & signer_test. running_nodes . conf . miner . mining_key . unwrap ( ) )
8767
+ . unwrap ( ) ;
8768
+ signer_test. propose_block ( sister_block. clone ( ) , Duration :: from_secs ( 30 ) ) ;
8769
+
8770
+ wait_for ( 30 , || {
8771
+ let stackerdb_events = test_observer:: get_stackerdb_chunks ( ) ;
8772
+ let block_rejections: HashSet < _ > = stackerdb_events
8773
+ . into_iter ( )
8774
+ . flat_map ( |chunk| chunk. modified_slots )
8775
+ . filter_map ( |chunk| {
8776
+ let message = SignerMessage :: consensus_deserialize ( & mut chunk. data . as_slice ( ) )
8777
+ . expect ( "Failed to deserialize SignerMessage" ) ;
8778
+ match message {
8779
+ SignerMessage :: BlockResponse ( BlockResponse :: Rejected ( rejection) ) => {
8780
+ let rejected_pubkey = rejection
8781
+ . recover_public_key ( )
8782
+ . expect ( "Failed to recover public key from rejection" ) ;
8783
+ // Proves that one of the rejecting signers actually submitted the block for validation as it passed its preliminary checks about chain length
8784
+ assert_eq ! (
8785
+ rejection. reason_code,
8786
+ RejectCode :: ValidationFailed ( ValidateRejectCode :: BadBlockHash )
8787
+ ) ;
8788
+ Some ( rejected_pubkey)
8789
+ }
8790
+ _ => None ,
8791
+ }
8792
+ } )
8793
+ . collect :: < HashSet < _ > > ( ) ;
8794
+ Ok ( block_rejections. len ( ) == all_signers. len ( ) )
8795
+ } )
8796
+ . expect ( "Timed out waiting for block rejections for N+1'" ) ;
8797
+ // Assert the block was NOT mined and the tip has not changed.
8798
+ let info_after = signer_test
8799
+ . stacks_client
8800
+ . get_peer_info ( )
8801
+ . expect ( "Failed to get peer info" ) ;
8802
+ assert_eq ! (
8803
+ info_after,
8804
+ signer_test
8805
+ . stacks_client
8806
+ . get_peer_info( )
8807
+ . expect( "Failed to get peer info" )
8808
+ ) ;
8809
+ }
0 commit comments