@@ -134,9 +134,189 @@ func TestBlockSync(t *testing.T) {
134
134
expHeadBlock (testBlock2 )
135
135
}
136
136
137
+ // TestBlockSyncFinality tests the beacon block sync's handling of finality updates.
138
+ //
139
+ // Beacon chain finality works as follows:
140
+ // - An "attested" header is the latest block that has been attested to by validators
141
+ // - A "finalized" header is a block that has been finalized (cannot be reverted)
142
+ // - ChainHeadEvents should include the finalized block hash when finality data is available
143
+ // - This enables the execution client to know which blocks are safe from reorgs
144
+ func TestBlockSyncFinality (t * testing.T ) {
145
+ ht := & testHeadTracker {}
146
+ blockSync := newBeaconBlockSync (ht )
147
+ headCh := make (chan types.ChainHeadEvent , 16 )
148
+ blockSync .SubscribeChainHead (headCh )
149
+ ts := sync .NewTestScheduler (t , blockSync )
150
+ ts .AddServer (testServer1 , 1 )
151
+ ts .AddServer (testServer2 , 1 )
152
+
153
+ // expChainHeadEvent is a helper function that validates ChainHeadEvent emissions.
154
+ // It checks that:
155
+ // 1. An event is emitted when expected (or not emitted when expHead is nil)
156
+ // 2. The event contains the correct execution block number
157
+ // 3. The event contains the expected finalized hash (or empty hash when no finality)
158
+ expChainHeadEvent := func (expHead * types.BeaconBlock , expFinalizedHash common.Hash ) {
159
+ t .Helper ()
160
+ var event types.ChainHeadEvent
161
+ var hasEvent bool
162
+ select {
163
+ case event = <- headCh :
164
+ hasEvent = true
165
+ default :
166
+ }
167
+
168
+ if expHead == nil {
169
+ if hasEvent {
170
+ t .Errorf ("Expected no chain head event, but got one with block number %d" , event .Block .NumberU64 ())
171
+ }
172
+ return
173
+ }
174
+
175
+ if ! hasEvent {
176
+ t .Errorf ("Expected chain head event with block number %d, but got none" , expHead .Header ().Slot )
177
+ return
178
+ }
179
+
180
+ expPayload , err := expHead .ExecutionPayload ()
181
+ if err != nil {
182
+ t .Fatalf ("expHead.ExecutionPayload() failed: %v" , err )
183
+ }
184
+
185
+ if event .Block .NumberU64 () != expPayload .NumberU64 () {
186
+ t .Errorf ("Wrong head block number, expected %d, got %d" , expPayload .NumberU64 (), event .Block .NumberU64 ())
187
+ }
188
+
189
+ if event .Finalized != expFinalizedHash {
190
+ t .Errorf ("Wrong finalized hash, expected %x, got %x" , expFinalizedHash , event .Finalized )
191
+ }
192
+ }
193
+
194
+ // ═══════════════════════════════════════════════════════════════════════════════════
195
+ // Test Scenario 1: Basic finality with proper finality update
196
+ // ═══════════════════════════════════════════════════════════════════════════════════
197
+ // This tests the normal case where we have both an attested block (testBlock1) and
198
+ // a finalized block (testBlock2). The ChainHeadEvent should include the finalized
199
+ // block's execution hash, indicating to the execution client that testBlock2 is safe.
200
+
201
+ head1 := blockHeadInfo (testBlock1 )
202
+ ht .prefetch = head1
203
+ ht .validated .Header = testBlock1 .Header ()
204
+
205
+ // Configure finality update: testBlock1 is attested, testBlock2 is finalized
206
+ ht .finalized .Attested .Header = testBlock1 .Header ()
207
+ ht .finalized .Finalized .Header = testBlock2 .Header ()
208
+ ht .finalized .Finalized .PayloadHeader = createTestExecutionHeader (testBlock2 )
209
+
210
+ // Simulate the block sync process
211
+ ts .ServerEvent (sync .EvNewHead , testServer1 , head1 )
212
+ ts .Run (1 , testServer1 , sync .ReqBeaconBlock (head1 .BlockRoot ))
213
+ ts .RequestEvent (request .EvResponse , ts .Request (1 , 1 ), testBlock1 )
214
+ ts .AddAllowance (testServer1 , 1 )
215
+ ts .Run (2 )
216
+
217
+ // Verify that ChainHeadEvent includes the finalized block's execution hash
218
+ finalizedPayload , err := testBlock2 .ExecutionPayload ()
219
+ if err != nil {
220
+ t .Fatalf ("Failed to get finalized payload: %v" , err )
221
+ }
222
+ expFinalizedHash := finalizedPayload .Hash ()
223
+ expChainHeadEvent (testBlock1 , expFinalizedHash )
224
+
225
+ // ═══════════════════════════════════════════════════════════════════════════════════
226
+ // Test Scenario 2: No finality update available
227
+ // ═══════════════════════════════════════════════════════════════════════════════════
228
+ // This tests the case where we have a new head block but no finality information.
229
+ // The ChainHeadEvent should be emitted but with an empty finalized hash.
230
+
231
+ // Clear any pending events from the previous test
232
+ select {
233
+ case <- headCh :
234
+ default :
235
+ }
236
+
237
+ // Set up scenario: new head (testBlock2) but no finality update
238
+ ht .validated .Header = testBlock2 .Header ()
239
+ ht .finalized = types.FinalityUpdate {} // Explicitly clear finality data
240
+ head2 := blockHeadInfo (testBlock2 )
241
+ ht .prefetch = head2
242
+
243
+ // Simulate block sync process
244
+ ts .ServerEvent (sync .EvNewHead , testServer1 , head2 )
245
+ ts .Run (3 , testServer1 , sync .ReqBeaconBlock (head2 .BlockRoot ))
246
+ ts .RequestEvent (request .EvResponse , ts .Request (3 , 1 ), testBlock2 )
247
+ ts .AddAllowance (testServer1 , 1 )
248
+ ts .Run (4 )
249
+
250
+ // Verify ChainHeadEvent is emitted but with empty finalized hash
251
+ expChainHeadEvent (testBlock2 , common.Hash {})
252
+
253
+ // ═══════════════════════════════════════════════════════════════════════════════════
254
+ // Test Scenario 3: Direct ValidatedFinality method testing
255
+ // ═══════════════════════════════════════════════════════════════════════════════════
256
+ // This tests the ValidatedFinality method directly to ensure it returns the correct
257
+ // finality update structure and availability flag.
258
+
259
+ // Clear any pending events
260
+ select {
261
+ case <- headCh :
262
+ default :
263
+ }
264
+
265
+ // Set up a proper finality update structure
266
+ ht .validated .Header = testBlock1 .Header ()
267
+ ht .finalized .Attested .Header = testBlock1 .Header ()
268
+ ht .finalized .Finalized .Header = testBlock2 .Header ()
269
+ ht .finalized .Finalized .PayloadHeader = createTestExecutionHeader (testBlock2 )
270
+
271
+ // Test the ValidatedFinality method directly
272
+ finalityUpdate , hasFinalityUpdate := ht .ValidatedFinality ()
273
+ if ! hasFinalityUpdate {
274
+ t .Error ("Expected finality update to be available" )
275
+ }
276
+
277
+ if finalityUpdate .Attested .Header != testBlock1 .Header () {
278
+ t .Error ("Finality update attested header doesn't match expected testBlock1" )
279
+ }
280
+
281
+ if finalityUpdate .Finalized .Header != testBlock2 .Header () {
282
+ t .Error ("Finality update finalized header doesn't match expected testBlock2" )
283
+ }
284
+
285
+ // Test that the sync logic properly uses this finality update
286
+ // Since testBlock1 is already in cache, we can just run the sync logic
287
+ ts .Run (5 )
288
+
289
+ // Verify that the finality information is properly included in the ChainHeadEvent
290
+ expChainHeadEvent (testBlock1 , expFinalizedHash )
291
+ }
292
+
293
+ // createTestExecutionHeader creates a minimal ExecutionHeader for testing purposes.
294
+ //
295
+ // In production, ExecutionHeaders contain many fields (parent hash, state root, receipts root, etc.)
296
+ // but for testing beacon chain finality logic, we only need the block hash to verify that
297
+ // the correct finalized block is referenced in ChainHeadEvents.
298
+ //
299
+ // This simplified approach allows us to test the finality propagation logic without
300
+ // dealing with the complexity of constructing full execution payloads.
301
+ func createTestExecutionHeader (block * types.BeaconBlock ) * types.ExecutionHeader {
302
+ payload , err := block .ExecutionPayload ()
303
+ if err != nil {
304
+ panic (err )
305
+ }
306
+ // Create a minimal ExecutionHeader with only the block hash populated
307
+ // This is sufficient for testing finality hash propagation
308
+ execHeader := & deneb.ExecutionPayloadHeader {
309
+ BlockHash : [32 ]byte (payload .Hash ()),
310
+ }
311
+ return types .NewExecutionHeader (execHeader )
312
+ }
313
+
314
+ // testHeadTracker is a mock implementation of the HeadTracker interface for testing.
315
+ // It allows tests to simulate different beacon chain states and finality conditions.
137
316
type testHeadTracker struct {
138
- prefetch types.HeadInfo
139
- validated types.SignedHeader
317
+ prefetch types.HeadInfo // The head info to return from PrefetchHead()
318
+ validated types.SignedHeader // The validated header for optimistic updates
319
+ finalized types.FinalityUpdate // The finality update data for comprehensive finality testing
140
320
}
141
321
142
322
func (h * testHeadTracker ) PrefetchHead () types.HeadInfo {
@@ -151,13 +331,33 @@ func (h *testHeadTracker) ValidatedOptimistic() (types.OptimisticUpdate, bool) {
151
331
}, h .validated .Header != (types.Header {})
152
332
}
153
333
154
- // TODO add test case for finality
334
+ // ValidatedFinality returns the most recent finality update if available.
335
+ //
336
+ // This method implements a two-tier approach:
337
+ // 1. Primary: If explicit finality data is set (h.finalized), return it directly
338
+ // 2. Fallback: For backward compatibility with existing tests, create a minimal
339
+ // finality update from the validated header
340
+ //
341
+ // The fallback ensures that existing tests continue to work while new tests
342
+ // can take advantage of the more comprehensive finality testing capabilities.
155
343
func (h * testHeadTracker ) ValidatedFinality () (types.FinalityUpdate , bool ) {
156
- finalized := types .NewExecutionHeader (new (deneb.ExecutionPayloadHeader ))
157
- return types.FinalityUpdate {
158
- Attested : types.HeaderWithExecProof {Header : h .validated .Header },
159
- Finalized : types.HeaderWithExecProof {PayloadHeader : finalized },
160
- Signature : h .validated .Signature ,
161
- SignatureSlot : h .validated .SignatureSlot ,
162
- }, h .validated .Header != (types.Header {})
344
+ // Primary path: Return explicit finality data if available
345
+ if h .finalized .Attested .Header != (types.Header {}) {
346
+ return h .finalized , true
347
+ }
348
+
349
+ // Fallback path: Create minimal finality update for backward compatibility
350
+ // This ensures existing tests continue to work without modification
351
+ if h .validated .Header != (types.Header {}) {
352
+ finalized := types .NewExecutionHeader (new (deneb.ExecutionPayloadHeader ))
353
+ return types.FinalityUpdate {
354
+ Attested : types.HeaderWithExecProof {Header : h .validated .Header },
355
+ Finalized : types.HeaderWithExecProof {PayloadHeader : finalized },
356
+ Signature : h .validated .Signature ,
357
+ SignatureSlot : h .validated .SignatureSlot ,
358
+ }, true
359
+ }
360
+
361
+ // No finality data available
362
+ return types.FinalityUpdate {}, false
163
363
}
0 commit comments