From f938da99d9f22e28e7641ec8d701f6317e99a758 Mon Sep 17 00:00:00 2001 From: Potuz Date: Mon, 29 Dec 2025 17:07:21 -0300 Subject: [PATCH] Use head to validate atts for previous epoch (#16109) In the event that the target checkpoint of an attestation is for the previous epoch, and the head state has the same dependent root at that epoch. The reason being that this guarantees that both seed and active validator indices are guaranteed to be the same at the checkpoint's epoch, from the point of view of the attester (even on a different branch) and the head view. --- .../blockchain/process_attestation_helpers.go | 10 ++++---- .../blockchain/process_attestation_test.go | 23 ++++++++++++------- changelog/potuz_use_head_previous_epoch.md | 3 +++ 3 files changed, 24 insertions(+), 12 deletions(-) create mode 100644 changelog/potuz_use_head_previous_epoch.md diff --git a/beacon-chain/blockchain/process_attestation_helpers.go b/beacon-chain/blockchain/process_attestation_helpers.go index 24185cc8db..ec9e86602c 100644 --- a/beacon-chain/blockchain/process_attestation_helpers.go +++ b/beacon-chain/blockchain/process_attestation_helpers.go @@ -22,7 +22,7 @@ import ( // The caller of this function must have a lock on forkchoice. func (s *Service) getRecentPreState(ctx context.Context, c *ethpb.Checkpoint) state.ReadOnlyBeaconState { headEpoch := slots.ToEpoch(s.HeadSlot()) - if c.Epoch < headEpoch || c.Epoch == 0 { + if c.Epoch+1 < headEpoch || c.Epoch == 0 { return nil } // Only use head state if the head state is compatible with the target checkpoint. @@ -30,11 +30,13 @@ func (s *Service) getRecentPreState(ctx context.Context, c *ethpb.Checkpoint) st if err != nil { return nil } - headDependent, err := s.cfg.ForkChoiceStore.DependentRootForEpoch([32]byte(headRoot), c.Epoch-1) + // headEpoch - 1 equals c.Epoch if c is from the previous epoch and equals c.Epoch - 1 if c is from the current epoch. + // We don't use the smaller c.Epoch - 1 because forkchoice would not have the data to answer that. + headDependent, err := s.cfg.ForkChoiceStore.DependentRootForEpoch([32]byte(headRoot), headEpoch-1) if err != nil { return nil } - targetDependent, err := s.cfg.ForkChoiceStore.DependentRootForEpoch([32]byte(c.Root), c.Epoch-1) + targetDependent, err := s.cfg.ForkChoiceStore.DependentRootForEpoch([32]byte(c.Root), headEpoch-1) if err != nil { return nil } @@ -43,7 +45,7 @@ func (s *Service) getRecentPreState(ctx context.Context, c *ethpb.Checkpoint) st } // If the head state alone is enough, we can return it directly read only. - if c.Epoch == headEpoch { + if c.Epoch <= headEpoch { st, err := s.HeadStateReadOnly(ctx) if err != nil { return nil diff --git a/beacon-chain/blockchain/process_attestation_test.go b/beacon-chain/blockchain/process_attestation_test.go index 8874de9700..1a3b7e0f52 100644 --- a/beacon-chain/blockchain/process_attestation_test.go +++ b/beacon-chain/blockchain/process_attestation_test.go @@ -170,12 +170,13 @@ func TestService_GetRecentPreState(t *testing.T) { err = s.SetFinalizedCheckpoint(cp0) require.NoError(t, err) - st, root, err := prepareForkchoiceState(ctx, 31, [32]byte(ckRoot), [32]byte{}, [32]byte{'R'}, cp0, cp0) + st, blk, err := prepareForkchoiceState(ctx, 31, [32]byte(ckRoot), [32]byte{}, [32]byte{'R'}, cp0, cp0) require.NoError(t, err) - require.NoError(t, service.cfg.ForkChoiceStore.InsertNode(ctx, st, root)) + require.NoError(t, service.cfg.ForkChoiceStore.InsertNode(ctx, st, blk)) service.head = &head{ root: [32]byte(ckRoot), state: s, + block: blk, slot: 31, } require.NotNil(t, service.getRecentPreState(ctx, ðpb.Checkpoint{Epoch: 1, Root: ckRoot})) @@ -197,12 +198,13 @@ func TestService_GetRecentPreState_Old_Checkpoint(t *testing.T) { err = s.SetFinalizedCheckpoint(cp0) require.NoError(t, err) - st, root, err := prepareForkchoiceState(ctx, 33, [32]byte(ckRoot), [32]byte{}, [32]byte{'R'}, cp0, cp0) + st, blk, err := prepareForkchoiceState(ctx, 33, [32]byte(ckRoot), [32]byte{}, [32]byte{'R'}, cp0, cp0) require.NoError(t, err) - require.NoError(t, service.cfg.ForkChoiceStore.InsertNode(ctx, st, root)) + require.NoError(t, service.cfg.ForkChoiceStore.InsertNode(ctx, st, blk)) service.head = &head{ root: [32]byte(ckRoot), state: s, + block: blk, slot: 33, } require.IsNil(t, service.getRecentPreState(ctx, ðpb.Checkpoint{})) @@ -227,6 +229,7 @@ func TestService_GetRecentPreState_Same_DependentRoots(t *testing.T) { require.NoError(t, service.cfg.ForkChoiceStore.InsertNode(ctx, st, blk)) st, blk, err = prepareForkchoiceState(ctx, 64, [32]byte{'T'}, blk.Root(), [32]byte{}, cp0, cp0) require.NoError(t, err) + headBlock := blk require.NoError(t, service.cfg.ForkChoiceStore.InsertNode(ctx, st, blk)) st, blk, err = prepareForkchoiceState(ctx, 33, [32]byte{'U'}, [32]byte(ckRoot), [32]byte{}, cp0, cp0) require.NoError(t, err) @@ -235,8 +238,9 @@ func TestService_GetRecentPreState_Same_DependentRoots(t *testing.T) { service.head = &head{ root: [32]byte{'T'}, - state: s, + block: headBlock, slot: 64, + state: s, } require.NotNil(t, service.getRecentPreState(ctx, ðpb.Checkpoint{Epoch: 2, Root: cpRoot[:]})) } @@ -263,6 +267,7 @@ func TestService_GetRecentPreState_Different_DependentRoots(t *testing.T) { require.NoError(t, service.cfg.ForkChoiceStore.InsertNode(ctx, st, blk)) st, blk, err = prepareForkchoiceState(ctx, 64, [32]byte{'U'}, blk.Root(), [32]byte{}, cp0, cp0) require.NoError(t, err) + headBlock := blk require.NoError(t, service.cfg.ForkChoiceStore.InsertNode(ctx, st, blk)) st, blk, err = prepareForkchoiceState(ctx, 33, [32]byte{'V'}, [32]byte(ckRoot), [32]byte{}, cp0, cp0) require.NoError(t, err) @@ -270,7 +275,8 @@ func TestService_GetRecentPreState_Different_DependentRoots(t *testing.T) { cpRoot := blk.Root() service.head = &head{ - root: [32]byte{'T'}, + root: [32]byte{'U'}, + block: headBlock, state: s, slot: 64, } @@ -287,12 +293,13 @@ func TestService_GetRecentPreState_Different(t *testing.T) { err = s.SetFinalizedCheckpoint(cp0) require.NoError(t, err) - st, root, err := prepareForkchoiceState(ctx, 33, [32]byte(ckRoot), [32]byte{}, [32]byte{'R'}, cp0, cp0) + st, blk, err := prepareForkchoiceState(ctx, 33, [32]byte(ckRoot), [32]byte{}, [32]byte{'R'}, cp0, cp0) require.NoError(t, err) - require.NoError(t, service.cfg.ForkChoiceStore.InsertNode(ctx, st, root)) + require.NoError(t, service.cfg.ForkChoiceStore.InsertNode(ctx, st, blk)) service.head = &head{ root: [32]byte(ckRoot), state: s, + block: blk, slot: 33, } require.IsNil(t, service.getRecentPreState(ctx, ðpb.Checkpoint{})) diff --git a/changelog/potuz_use_head_previous_epoch.md b/changelog/potuz_use_head_previous_epoch.md new file mode 100644 index 0000000000..278c652d3b --- /dev/null +++ b/changelog/potuz_use_head_previous_epoch.md @@ -0,0 +1,3 @@ +### Added + +- Use the head state to validate attestations for the previous epoch if head is compatible with the target checkpoint.