@@ -29,6 +29,117 @@ use crate::v0::messages::{
29
29
} ;
30
30
use crate :: v0:: signer_state:: { GlobalStateEvaluator , ReplayTransactionSet , SignerStateMachine } ;
31
31
32
+ /// Test setup helper struct containing common test data
33
+ struct SignerStateTest {
34
+ global_eval : GlobalStateEvaluator ,
35
+ addresses : Vec < StacksAddress > ,
36
+ burn_block : ConsensusHash ,
37
+ burn_block_height : u64 ,
38
+ current_miner : StateMachineUpdateMinerState ,
39
+ local_supported_signer_protocol_version : u64 ,
40
+ active_signer_protocol_version : u64 ,
41
+ tx_a : StacksTransaction ,
42
+ tx_b : StacksTransaction ,
43
+ tx_c : StacksTransaction ,
44
+ tx_d : StacksTransaction ,
45
+ }
46
+
47
+ impl SignerStateTest {
48
+ fn new ( num_signers : u32 ) -> Self {
49
+ let global_eval = generate_global_state_evaluator ( num_signers) ;
50
+ let addresses: Vec < _ > = global_eval. address_weights . keys ( ) . cloned ( ) . collect ( ) ;
51
+ let local_address = addresses[ 0 ] . clone ( ) ;
52
+
53
+ let burn_block = ConsensusHash ( [ 20u8 ; 20 ] ) ;
54
+ let burn_block_height = 100 ;
55
+ let current_miner = StateMachineUpdateMinerState :: ActiveMiner {
56
+ current_miner_pkh : Hash160 ( [ 0xab ; 20 ] ) ,
57
+ tenure_id : ConsensusHash ( [ 0x44 ; 20 ] ) ,
58
+ parent_tenure_id : ConsensusHash ( [ 0x22 ; 20 ] ) ,
59
+ parent_tenure_last_block : StacksBlockId ( [ 0x33 ; 32 ] ) ,
60
+ parent_tenure_last_block_height : 1 ,
61
+ } ;
62
+
63
+ let local_supported_signer_protocol_version = 1 ;
64
+ let active_signer_protocol_version = 1 ;
65
+
66
+ // Create test transactions with different memos for uniqueness
67
+ let pk1 = StacksPrivateKey :: random ( ) ;
68
+ let pk2 = StacksPrivateKey :: random ( ) ;
69
+ let pk3 = StacksPrivateKey :: random ( ) ;
70
+ let pk4 = StacksPrivateKey :: random ( ) ;
71
+
72
+ let make_tx = |pk : & StacksPrivateKey , memo : [ u8 ; 34 ] | StacksTransaction {
73
+ version : TransactionVersion :: Testnet ,
74
+ chain_id : 0x80000000 ,
75
+ auth : TransactionAuth :: from_p2pkh ( pk) . unwrap ( ) ,
76
+ anchor_mode : TransactionAnchorMode :: Any ,
77
+ post_condition_mode : TransactionPostConditionMode :: Allow ,
78
+ post_conditions : vec ! [ ] ,
79
+ payload : TransactionPayload :: TokenTransfer (
80
+ local_address. clone ( ) . into ( ) ,
81
+ 100 ,
82
+ TokenTransferMemo ( memo) ,
83
+ ) ,
84
+ } ;
85
+
86
+ let tx_a = make_tx ( & pk1, [ 1u8 ; 34 ] ) ;
87
+ let tx_b = make_tx ( & pk2, [ 2u8 ; 34 ] ) ;
88
+ let tx_c = make_tx ( & pk3, [ 3u8 ; 34 ] ) ;
89
+ let tx_d = make_tx ( & pk4, [ 4u8 ; 34 ] ) ;
90
+
91
+ Self {
92
+ global_eval,
93
+ addresses,
94
+ burn_block,
95
+ burn_block_height,
96
+ current_miner,
97
+ local_supported_signer_protocol_version,
98
+ active_signer_protocol_version,
99
+ tx_a,
100
+ tx_b,
101
+ tx_c,
102
+ tx_d,
103
+ }
104
+ }
105
+
106
+ /// Create a replay transaction update message
107
+ fn create_replay_update (
108
+ & self ,
109
+ transactions : Vec < StacksTransaction > ,
110
+ ) -> StateMachineUpdateMessage {
111
+ StateMachineUpdateMessage :: new (
112
+ self . active_signer_protocol_version ,
113
+ self . local_supported_signer_protocol_version ,
114
+ StateMachineUpdateContent :: V1 {
115
+ burn_block : self . burn_block ,
116
+ burn_block_height : self . burn_block_height ,
117
+ current_miner : self . current_miner . clone ( ) ,
118
+ replay_transactions : transactions,
119
+ } ,
120
+ )
121
+ . unwrap ( )
122
+ }
123
+
124
+ /// Update multiple signers with the same replay transaction set
125
+ fn update_signers ( & mut self , signer_indices : & [ usize ] , transactions : Vec < StacksTransaction > ) {
126
+ let update = self . create_replay_update ( transactions) ;
127
+ for & index in signer_indices {
128
+ self . global_eval
129
+ . insert_update ( self . addresses [ index] . clone ( ) , update. clone ( ) ) ;
130
+ }
131
+ }
132
+
133
+ /// Get the global state replay set
134
+ fn get_global_replay_set ( & mut self ) -> Vec < StacksTransaction > {
135
+ self . global_eval
136
+ . determine_global_state ( )
137
+ . unwrap ( )
138
+ . tx_replay_set
139
+ . unwrap_or_default ( )
140
+ }
141
+ }
142
+
32
143
fn generate_global_state_evaluator ( num_addresses : u32 ) -> GlobalStateEvaluator {
33
144
let address_weights = generate_random_address_with_equal_weights ( num_addresses) ;
34
145
let active_protocol_version = 0 ;
@@ -417,3 +528,181 @@ fn determine_global_states_with_tx_replay_set() {
417
528
tx_replay_state_machine
418
529
) ;
419
530
}
531
+
532
+ #[ test]
533
+ /// Case: One signer has [A,B,C], another has [A,B] - should find common prefix [A,B]
534
+ fn test_replay_set_common_prefix_coalescing ( ) {
535
+ let mut state_test = SignerStateTest :: new ( 5 ) ;
536
+
537
+ // Signers 0, 1: [A,B,C] (40% weight)
538
+ state_test. update_signers (
539
+ & [ 0 , 1 ] ,
540
+ vec ! [
541
+ state_test. tx_a. clone( ) ,
542
+ state_test. tx_b. clone( ) ,
543
+ state_test. tx_c. clone( ) ,
544
+ ] ,
545
+ ) ;
546
+
547
+ // Signers 2, 3, 4: [A,B] (60% weight - should win)
548
+ state_test. update_signers (
549
+ & [ 2 , 3 , 4 ] ,
550
+ vec ! [ state_test. tx_a. clone( ) , state_test. tx_b. clone( ) ] ,
551
+ ) ;
552
+
553
+ let transactions = state_test. get_global_replay_set ( ) ;
554
+
555
+ // Should find common prefix [A,B] since it's the longest prefix with majority support
556
+ assert_eq ! ( transactions. len( ) , 2 ) ;
557
+ assert_eq ! ( transactions[ 0 ] , state_test. tx_a) ; // Order matters!
558
+ assert_eq ! ( transactions[ 1 ] , state_test. tx_b) ;
559
+ assert ! ( !transactions. contains( & state_test. tx_c) ) ;
560
+ }
561
+
562
+ #[ test]
563
+ /// Case: One sequence has clear majority - should use that sequence
564
+ fn test_replay_set_majority_prefix_selection ( ) {
565
+ let mut state_test = SignerStateTest :: new ( 5 ) ;
566
+
567
+ // Signer 0: [A] (20% weight)
568
+ state_test. update_signers ( & [ 0 ] , vec ! [ state_test. tx_a. clone( ) ] ) ;
569
+
570
+ // Signers 1, 2, 3, 4: [C] (80% weight - above threshold)
571
+ state_test. update_signers ( & [ 1 , 2 , 3 , 4 ] , vec ! [ state_test. tx_c. clone( ) ] ) ;
572
+
573
+ let transactions = state_test. get_global_replay_set ( ) ;
574
+
575
+ // Should use [C] since it has majority support (80% > 70%)
576
+ assert_eq ! ( transactions. len( ) , 1 ) ;
577
+ assert_eq ! ( transactions[ 0 ] , state_test. tx_c) ;
578
+ }
579
+
580
+ #[ test]
581
+ /// Case: Exact agreement should be prioritized over subset coalescing
582
+ fn test_replay_set_exact_agreement_prioritized ( ) {
583
+ let mut state_test = SignerStateTest :: new ( 5 ) ;
584
+
585
+ // 4 signers agree on [A,B] exactly (80% - above threshold)
586
+ state_test. update_signers (
587
+ & [ 0 , 1 , 2 , 3 ] ,
588
+ vec ! [ state_test. tx_a. clone( ) , state_test. tx_b. clone( ) ] ,
589
+ ) ;
590
+
591
+ // 1 signer has just [A] (20%)
592
+ state_test. update_signers ( & [ 4 ] , vec ! [ state_test. tx_a. clone( ) ] ) ;
593
+
594
+ let transactions = state_test. get_global_replay_set ( ) ;
595
+
596
+ // Should use exact agreement [A,B] rather than common prefix [A]
597
+ assert_eq ! ( transactions. len( ) , 2 ) ;
598
+ assert_eq ! ( transactions[ 0 ] , state_test. tx_a) ; // Order matters!
599
+ assert_eq ! ( transactions[ 1 ] , state_test. tx_b) ;
600
+ }
601
+
602
+ #[ test]
603
+ /// Case: Complete disagreement - no overlap and no majority
604
+ fn test_replay_set_no_agreement_returns_empty ( ) {
605
+ let mut state_test = SignerStateTest :: new ( 5 ) ;
606
+
607
+ // Signer 0: [A] (20% weight)
608
+ state_test. update_signers ( & [ 0 ] , vec ! [ state_test. tx_a. clone( ) ] ) ;
609
+
610
+ // Signer 1: [B] (20% weight)
611
+ state_test. update_signers ( & [ 1 ] , vec ! [ state_test. tx_b. clone( ) ] ) ;
612
+
613
+ // Signer 2: [C] (20% weight)
614
+ state_test. update_signers ( & [ 2 ] , vec ! [ state_test. tx_c. clone( ) ] ) ;
615
+
616
+ // Signers 3, 4: empty sets (40% weight)
617
+ state_test. update_signers ( & [ 3 , 4 ] , vec ! [ ] ) ;
618
+
619
+ let transactions = state_test. get_global_replay_set ( ) ;
620
+
621
+ // Should return empty set to prioritize liveness when no agreement
622
+ assert_eq ! ( transactions. len( ) , 0 ) ;
623
+ }
624
+
625
+ #[ test]
626
+ /// Case: Same transactions in different order have no common prefix
627
+ fn test_replay_set_order_matters_no_common_prefix ( ) {
628
+ let mut state_test = SignerStateTest :: new ( 4 ) ;
629
+
630
+ // Signers 0, 1: [A,B] (50% weight)
631
+ state_test. update_signers (
632
+ & [ 0 , 1 ] ,
633
+ vec ! [ state_test. tx_a. clone( ) , state_test. tx_b. clone( ) ] ,
634
+ ) ;
635
+
636
+ // Signers 2, 3: [B,A] (50% weight)
637
+ state_test. update_signers (
638
+ & [ 2 , 3 ] ,
639
+ vec ! [ state_test. tx_b. clone( ) , state_test. tx_a. clone( ) ] ,
640
+ ) ;
641
+
642
+ let transactions = state_test. get_global_replay_set ( ) ;
643
+
644
+ // Should return empty set since [A,B] and [B,A] have no common prefix
645
+ // Even though both contain the same transactions, order matters for replay
646
+ assert_eq ! ( transactions. len( ) , 0 ) ;
647
+ }
648
+
649
+ #[ test]
650
+ /// Case: [A,B,C] vs [A,B,D] should find common prefix [A,B]
651
+ fn test_replay_set_partial_prefix_match ( ) {
652
+ let mut state_test = SignerStateTest :: new ( 5 ) ;
653
+
654
+ // Signer 0, 1: [A,B,C] (40% weight)
655
+ state_test. update_signers (
656
+ & [ 0 , 1 ] ,
657
+ vec ! [
658
+ state_test. tx_a. clone( ) ,
659
+ state_test. tx_b. clone( ) ,
660
+ state_test. tx_c. clone( ) ,
661
+ ] ,
662
+ ) ;
663
+
664
+ // Signers 2, 3, 4: [A,B,D] (60% weight)
665
+ state_test. update_signers (
666
+ & [ 2 , 3 , 4 ] ,
667
+ vec ! [
668
+ state_test. tx_a. clone( ) ,
669
+ state_test. tx_b. clone( ) ,
670
+ state_test. tx_d. clone( ) ,
671
+ ] ,
672
+ ) ;
673
+
674
+ let transactions = state_test. get_global_replay_set ( ) ;
675
+
676
+ // Should find [A,B] as the longest common prefix with majority support
677
+ assert_eq ! ( transactions. len( ) , 2 ) ;
678
+ assert_eq ! ( transactions[ 0 ] , state_test. tx_a) ;
679
+ assert_eq ! ( transactions[ 1 ] , state_test. tx_b) ;
680
+ }
681
+
682
+ #[ test]
683
+ /// Edge case: Equal-weight competing prefixes should find common prefix
684
+ fn test_replay_set_equal_weight_competing_prefixes ( ) {
685
+ let mut state_test = SignerStateTest :: new ( 6 ) ;
686
+
687
+ // Signers 0, 1, 2: [A,B] (50% weight - not enough alone)
688
+ state_test. update_signers (
689
+ & [ 0 , 1 , 2 ] ,
690
+ vec ! [ state_test. tx_a. clone( ) , state_test. tx_b. clone( ) ] ,
691
+ ) ;
692
+
693
+ // Signers 3, 4, 5: [A,C] (50% weight - not enough alone)
694
+ state_test. update_signers (
695
+ & [ 3 , 4 , 5 ] ,
696
+ vec ! [ state_test. tx_a. clone( ) , state_test. tx_c. clone( ) ] ,
697
+ ) ;
698
+
699
+ let transactions = state_test. get_global_replay_set ( ) ;
700
+
701
+ // Should find common prefix [A] since both [A,B] and [A,C] start with [A]
702
+ // and [A] has 100% support (above the 70% threshold)
703
+ assert_eq ! ( transactions. len( ) , 1 , "Should find common prefix [A]" ) ;
704
+ assert_eq ! (
705
+ transactions[ 0 ] , state_test. tx_a,
706
+ "Should contain transaction A"
707
+ ) ;
708
+ }
0 commit comments