@@ -68,3 +68,131 @@ pub fn initiateValidatorExit(
6868 try std .math .add (u64 , try validator .get ("exit_epoch" ), config .chain .MIN_VALIDATOR_WITHDRAWABILITY_DELAY ),
6969 );
7070}
71+
72+ // ─── Tests ───────────────────────────────────────────────────────────
73+
74+ const std_testing = std .testing ;
75+ const TestCachedBeaconState = @import ("../test_utils/root.zig" ).TestCachedBeaconState ;
76+ const Node = @import ("persistent_merkle_tree" ).Node ;
77+ const preset = @import ("preset" ).preset ;
78+
79+ test "initiateValidatorExit - no-op if already exited" {
80+ const allocator = std_testing .allocator ;
81+ const num_validators = 256 ;
82+ const pool_size = num_validators * 5 ;
83+ var pool = try Node .Pool .init (allocator , pool_size );
84+ defer pool .deinit ();
85+
86+ var test_state = try TestCachedBeaconState .init (allocator , & pool , num_validators );
87+ defer test_state .deinit ();
88+
89+ const state = test_state .cached_state .state .castToFork (.electra );
90+ const epoch_cache = test_state .cached_state .epoch_cache ;
91+
92+ // Set validator 0's exit_epoch to something other than FAR_FUTURE_EPOCH
93+ var validators = try state .validators ();
94+ var validator = try validators .get (0 );
95+ try validator .set ("exit_epoch" , 10 );
96+ try validator .set ("withdrawable_epoch" , 10 + test_state .config .chain .MIN_VALIDATOR_WITHDRAWABILITY_DELAY );
97+
98+ // Call initiateValidatorExit — should be a no-op
99+ try initiateValidatorExit (.electra , test_state .config , epoch_cache , state , validator );
100+
101+ // exit_epoch should remain unchanged
102+ try std_testing .expectEqual (@as (u64 , 10 ), try validator .get ("exit_epoch" ));
103+ }
104+
105+ test "initiateValidatorExit - sets exit and withdrawable epochs" {
106+ const allocator = std_testing .allocator ;
107+ const num_validators = 256 ;
108+ const pool_size = num_validators * 5 ;
109+ var pool = try Node .Pool .init (allocator , pool_size );
110+ defer pool .deinit ();
111+
112+ var test_state = try TestCachedBeaconState .init (allocator , & pool , num_validators );
113+ defer test_state .deinit ();
114+
115+ const state = test_state .cached_state .state .castToFork (.electra );
116+ const epoch_cache = test_state .cached_state .epoch_cache ;
117+
118+ var validators = try state .validators ();
119+ var validator = try validators .get (0 );
120+
121+ // Validator should start with FAR_FUTURE_EPOCH
122+ try std_testing .expectEqual (FAR_FUTURE_EPOCH , try validator .get ("exit_epoch" ));
123+
124+ try initiateValidatorExit (.electra , test_state .config , epoch_cache , state , validator );
125+
126+ const exit_epoch = try validator .get ("exit_epoch" );
127+ try std_testing .expect (exit_epoch != FAR_FUTURE_EPOCH );
128+ try std_testing .expectEqual (
129+ exit_epoch + test_state .config .chain .MIN_VALIDATOR_WITHDRAWABILITY_DELAY ,
130+ try validator .get ("withdrawable_epoch" ),
131+ );
132+ }
133+
134+ test "initiateValidatorExit - multiple exits" {
135+ const allocator = std_testing .allocator ;
136+ const num_validators = 256 ;
137+ const pool_size = num_validators * 5 ;
138+ var pool = try Node .Pool .init (allocator , pool_size );
139+ defer pool .deinit ();
140+
141+ var test_state = try TestCachedBeaconState .init (allocator , & pool , num_validators );
142+ defer test_state .deinit ();
143+
144+ const state = test_state .cached_state .state .castToFork (.electra );
145+ const epoch_cache = test_state .cached_state .epoch_cache ;
146+
147+ var validators = try state .validators ();
148+
149+ // Exit multiple validators and verify each gets a valid exit epoch
150+ var v0 = try validators .get (0 );
151+ var v1 = try validators .get (1 );
152+ var v2 = try validators .get (2 );
153+
154+ try initiateValidatorExit (.electra , test_state .config , epoch_cache , state , v0 );
155+ try initiateValidatorExit (.electra , test_state .config , epoch_cache , state , v1 );
156+ try initiateValidatorExit (.electra , test_state .config , epoch_cache , state , v2 );
157+
158+ const exit0 = try v0 .get ("exit_epoch" );
159+ const exit1 = try v1 .get ("exit_epoch" );
160+ const exit2 = try v2 .get ("exit_epoch" );
161+
162+ // All should have valid exit epochs
163+ try std_testing .expect (exit0 != FAR_FUTURE_EPOCH );
164+ try std_testing .expect (exit1 != FAR_FUTURE_EPOCH );
165+ try std_testing .expect (exit2 != FAR_FUTURE_EPOCH );
166+
167+ // All should have valid withdrawable epochs
168+ const delay = test_state .config .chain .MIN_VALIDATOR_WITHDRAWABILITY_DELAY ;
169+ try std_testing .expectEqual (exit0 + delay , try v0 .get ("withdrawable_epoch" ));
170+ try std_testing .expectEqual (exit1 + delay , try v1 .get ("withdrawable_epoch" ));
171+ try std_testing .expectEqual (exit2 + delay , try v2 .get ("withdrawable_epoch" ));
172+ }
173+
174+ test "initiateValidatorExit - second call is no-op" {
175+ const allocator = std_testing .allocator ;
176+ const num_validators = 256 ;
177+ const pool_size = num_validators * 5 ;
178+ var pool = try Node .Pool .init (allocator , pool_size );
179+ defer pool .deinit ();
180+
181+ var test_state = try TestCachedBeaconState .init (allocator , & pool , num_validators );
182+ defer test_state .deinit ();
183+
184+ const state = test_state .cached_state .state .castToFork (.electra );
185+ const epoch_cache = test_state .cached_state .epoch_cache ;
186+
187+ var validators = try state .validators ();
188+ var validator = try validators .get (0 );
189+
190+ // First call — should set exit epoch
191+ try initiateValidatorExit (.electra , test_state .config , epoch_cache , state , validator );
192+ const exit_epoch = try validator .get ("exit_epoch" );
193+ try std_testing .expect (exit_epoch != FAR_FUTURE_EPOCH );
194+
195+ // Second call — should be a no-op
196+ try initiateValidatorExit (.electra , test_state .config , epoch_cache , state , validator );
197+ try std_testing .expectEqual (exit_epoch , try validator .get ("exit_epoch" ));
198+ }
0 commit comments