Skip to content

Commit 27dc374

Browse files
committed
beacon/blsync: add comprehensive finality test coverage
1 parent b992b10 commit 27dc374

File tree

1 file changed

+210
-10
lines changed

1 file changed

+210
-10
lines changed

beacon/blsync/block_sync_test.go

Lines changed: 210 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -134,9 +134,189 @@ func TestBlockSync(t *testing.T) {
134134
expHeadBlock(testBlock2)
135135
}
136136

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.
137316
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
140320
}
141321

142322
func (h *testHeadTracker) PrefetchHead() types.HeadInfo {
@@ -151,13 +331,33 @@ func (h *testHeadTracker) ValidatedOptimistic() (types.OptimisticUpdate, bool) {
151331
}, h.validated.Header != (types.Header{})
152332
}
153333

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.
155343
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
163363
}

0 commit comments

Comments
 (0)