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