Skip to content

Commit 4ba7918

Browse files
*: v1.10.3-rc1 (#4567)
* Proposer duties v2 (#4564) * build(deps): Bump github.com/ethereum/go-ethereum (#4566) Bumps the go-dependencies group with 1 update: [github.com/ethereum/go-ethereum](https://github.com/ethereum/go-ethereum). Updates `github.com/ethereum/go-ethereum` from 1.17.3 to 1.17.4 - [Release notes](https://github.com/ethereum/go-ethereum/releases) - [Commits](ethereum/go-ethereum@v1.17.3...v1.17.4) --- updated-dependencies: - dependency-name: github.com/ethereum/go-ethereum dependency-version: 1.17.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: go-dependencies ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump libp2p (#4568) * build(deps): Bump the go-dependencies group across 1 directory with 6 updates (#4562) Bumps the go-dependencies group with 3 updates in the / directory: [golang.org/x/crypto](https://github.com/golang/crypto), [golang.org/x/net](https://github.com/golang/net) and [golang.org/x/tools](https://github.com/golang/tools). Updates `golang.org/x/crypto` from 0.52.0 to 0.53.0 - [Commits](golang/crypto@v0.52.0...v0.53.0) Updates `golang.org/x/net` from 0.55.0 to 0.56.0 - [Commits](golang/net@v0.55.0...v0.56.0) Updates `golang.org/x/sync` from 0.20.0 to 0.21.0 - [Commits](golang/sync@v0.20.0...v0.21.0) Updates `golang.org/x/term` from 0.43.0 to 0.44.0 - [Commits](golang/term@v0.43.0...v0.44.0) Updates `golang.org/x/text` from 0.37.0 to 0.38.0 - [Release notes](https://github.com/golang/text/releases) - [Commits](golang/text@v0.37.0...v0.38.0) Updates `golang.org/x/tools` from 0.45.0 to 0.46.0 - [Release notes](https://github.com/golang/tools/releases) - [Commits](golang/tools@v0.45.0...v0.46.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-version: 0.53.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-dependencies - dependency-name: golang.org/x/net dependency-version: 0.56.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-dependencies - dependency-name: golang.org/x/sync dependency-version: 0.21.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-dependencies - dependency-name: golang.org/x/term dependency-version: 0.44.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-dependencies - dependency-name: golang.org/x/text dependency-version: 0.38.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-dependencies - dependency-name: golang.org/x/tools dependency-version: 0.46.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-dependencies ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1 parent 6ad45d1 commit 4ba7918

12 files changed

Lines changed: 439 additions & 161 deletions

File tree

app/app.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -514,7 +514,7 @@ func wireCoreWorkflow(ctx context.Context, life *lifecycle.Manager, conf Config,
514514
}
515515

516516
sseListener.SubscribeChainReorgEvent(sched.HandleChainReorgEvent)
517-
sseListener.SubscribeBlockEvent(sched.HandleBlockEvent)
517+
sseListener.SubscribeHeadEvent(sched.HandleHeadEvent)
518518

519519
sched.SubscribeSlots(setFeeRecipient(eth2Cl, builderRegSvc.FeeRecipient))
520520

@@ -615,6 +615,12 @@ func wireCoreWorkflow(ctx context.Context, life *lifecycle.Manager, conf Config,
615615
return err
616616
}
617617

618+
// Invalidate early-fetched (head-event-triggered) attestation data on reorgs, since the cached
619+
// data was verified against a head that may no longer be canonical.
620+
if featureset.Enabled(featureset.FetchAttOnBlock) || featureset.Enabled(featureset.FetchAttOnBlockWithDelay) {
621+
sseListener.SubscribeChainReorgEvent(fetch.HandleChainReorg)
622+
}
623+
618624
dutyDB := dutydb.NewMemDB(deadlinerFunc("dutydb"))
619625

620626
vapi, err := validatorapi.NewComponent(eth2Cl, allPubSharesByKey, nodeIdx.ShareIdx, builderRegSvc.FeeRecipient, conf.BuilderAPI, lock.TargetGasLimit)

app/featureset/featureset.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,11 @@ const (
7171
// In case they differ, Charon does not sign the attestation.
7272
ChainSplitHalt = "chain_split_halt"
7373

74-
// FetchAttOnBlock enables fetching attestation data upon block processing event from beacon node via SSE.
75-
// Fallback to T=1/3 if block event is not received in time.
74+
// FetchAttOnBlock enables fetching attestation data early upon the SSE "head" event from the beacon node
75+
// (fork-choice head updated), rather than waiting for the scheduled deadline. Triggering on the head event
76+
// (instead of the "block" event) ensures the beacon node's head has settled onto the new block before
77+
// fetching, avoiding stale attestation data at epoch boundaries. Fetched data is dropped if it does not
78+
// vote for the head from the event. Falls back to T=1/3 if no head event is received in time.
7679
FetchAttOnBlock = "fetch_att_on_block"
7780

7881
// FetchAttOnBlockWithDelay enables fetching attestation data with 300ms delay.

app/sse/listener.go

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ package sse
44

55
import (
66
"context"
7+
"encoding/hex"
78
"encoding/json"
89
"math"
910
"net/http"
1011
"strconv"
12+
"strings"
1113
"sync"
1214
"time"
1315

@@ -22,19 +24,19 @@ import (
2224

2325
type (
2426
ChainReorgEventHandlerFunc func(ctx context.Context, epoch eth2p0.Epoch)
25-
BlockEventHandlerFunc func(ctx context.Context, slot eth2p0.Slot, bnAddr string)
27+
HeadEventHandlerFunc func(ctx context.Context, slot eth2p0.Slot, blockRoot eth2p0.Root, bnAddr string)
2628
)
2729

2830
type Listener interface {
2931
SubscribeChainReorgEvent(ChainReorgEventHandlerFunc)
30-
SubscribeBlockEvent(BlockEventHandlerFunc)
32+
SubscribeHeadEvent(HeadEventHandlerFunc)
3133
}
3234

3335
type listener struct {
3436
sync.Mutex
3537

3638
chainReorgSubs []ChainReorgEventHandlerFunc
37-
blockSubs []BlockEventHandlerFunc
39+
headSubs []HeadEventHandlerFunc
3840
lastReorgEpoch eth2p0.Epoch
3941

4042
// blockGossipTimes stores timestamps of block gossip events per slot and beacon node address
@@ -64,7 +66,7 @@ func StartListener(ctx context.Context, eth2Cl eth2wrap.Client, addresses, heade
6466

6567
l := &listener{
6668
chainReorgSubs: make([]ChainReorgEventHandlerFunc, 0),
67-
blockSubs: make([]BlockEventHandlerFunc, 0),
69+
headSubs: make([]HeadEventHandlerFunc, 0),
6870
blockGossipTimes: make(map[uint64]map[string]time.Time),
6971
genesisTime: genesisTime,
7072
slotDuration: slotDuration,
@@ -105,11 +107,11 @@ func (p *listener) SubscribeChainReorgEvent(handler ChainReorgEventHandlerFunc)
105107
p.chainReorgSubs = append(p.chainReorgSubs, handler)
106108
}
107109

108-
func (p *listener) SubscribeBlockEvent(handler BlockEventHandlerFunc) {
110+
func (p *listener) SubscribeHeadEvent(handler HeadEventHandlerFunc) {
109111
p.Lock()
110112
defer p.Unlock()
111113

112-
p.blockSubs = append(p.blockSubs, handler)
114+
p.headSubs = append(p.headSubs, handler)
113115
}
114116

115117
func (p *listener) eventHandler(ctx context.Context, event *event, addr string) error {
@@ -167,6 +169,13 @@ func (p *listener) handleHeadEvent(ctx context.Context, event *event, addr strin
167169
z.Str("prev_ddr", head.PreviousDutyDependentRoot),
168170
z.Str("curr_ddr", head.CurrentDutyDependentRoot))
169171

172+
blockRoot, err := parseRoot(head.Block)
173+
if err != nil {
174+
return errors.Wrap(err, "parse head block root", z.Str("addr", addr))
175+
}
176+
177+
p.notifyHeadEvent(ctx, eth2p0.Slot(slot), blockRoot, addr)
178+
170179
return nil
171180
}
172181

@@ -270,8 +279,6 @@ func (p *listener) handleBlockEvent(ctx context.Context, event *event, addr stri
270279

271280
sseBlockHistogram.WithLabelValues(addr).Observe(delay.Seconds())
272281

273-
p.notifyBlockEvent(ctx, eth2p0.Slot(slot), addr)
274-
275282
return nil
276283
}
277284

@@ -289,15 +296,32 @@ func (p *listener) notifyChainReorg(ctx context.Context, epoch eth2p0.Epoch) {
289296
}
290297
}
291298

292-
func (p *listener) notifyBlockEvent(ctx context.Context, slot eth2p0.Slot, bnAddr string) {
299+
func (p *listener) notifyHeadEvent(ctx context.Context, slot eth2p0.Slot, blockRoot eth2p0.Root, bnAddr string) {
293300
p.Lock()
294301
defer p.Unlock()
295302

296-
for _, sub := range p.blockSubs {
297-
sub(ctx, slot, bnAddr)
303+
for _, sub := range p.headSubs {
304+
sub(ctx, slot, blockRoot, bnAddr)
298305
}
299306
}
300307

308+
// parseRoot parses a 0x-prefixed hex string into an eth2p0.Root.
309+
func parseRoot(hexRoot string) (eth2p0.Root, error) {
310+
b, err := hex.DecodeString(strings.TrimPrefix(hexRoot, "0x"))
311+
if err != nil {
312+
return eth2p0.Root{}, errors.Wrap(err, "decode hex root")
313+
}
314+
315+
var root eth2p0.Root
316+
if len(b) != len(root) {
317+
return eth2p0.Root{}, errors.New("invalid root length", z.Int("length", len(b)))
318+
}
319+
320+
copy(root[:], b)
321+
322+
return root, nil
323+
}
324+
301325
// computeDelay computes the delay between start of the slot and receiving the event.
302326
func (p *listener) computeDelay(slot uint64, eventTS time.Time, delayOKFunc func(delay time.Duration) bool) (time.Duration, bool) {
303327
slotStartTime := p.genesisTime.Add(time.Duration(slot) * p.slotDuration)

app/sse/listener_internal_test.go

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ func TestHandleEvents(t *testing.T) {
116116
t.Run(test.name, func(t *testing.T) {
117117
l := &listener{
118118
chainReorgSubs: make([]ChainReorgEventHandlerFunc, 0),
119-
blockSubs: make([]BlockEventHandlerFunc, 0),
119+
headSubs: make([]HeadEventHandlerFunc, 0),
120120
slotDuration: 12 * time.Second,
121121
slotsPerEpoch: 32,
122122
genesisTime: time.Date(2020, 12, 1, 12, 0, 23, 0, time.UTC),
@@ -161,21 +161,22 @@ func TestSubscribeNotifyChainReorg(t *testing.T) {
161161
require.Equal(t, eth2p0.Epoch(10), reportedEpochs[1])
162162
}
163163

164-
func TestSubscribeNotifyBlockEvent(t *testing.T) {
164+
func TestSubscribeNotifyHeadEvent(t *testing.T) {
165165
ctx := t.Context()
166166
l := &listener{
167-
blockSubs: make([]BlockEventHandlerFunc, 0),
167+
headSubs: make([]HeadEventHandlerFunc, 0),
168168
}
169169

170170
reportedSlots := make([]eth2p0.Slot, 0)
171171

172-
l.SubscribeBlockEvent(func(_ context.Context, slot eth2p0.Slot, bnAddr string) {
172+
l.SubscribeHeadEvent(func(_ context.Context, slot eth2p0.Slot, _ eth2p0.Root, bnAddr string) {
173173
reportedSlots = append(reportedSlots, slot)
174174
})
175175

176-
l.notifyBlockEvent(ctx, eth2p0.Slot(100), "http://test-bn:5052")
177-
l.notifyBlockEvent(ctx, eth2p0.Slot(100), "http://test-bn:5052") // Duplicate should be reported (no dedup for block events)
178-
l.notifyBlockEvent(ctx, eth2p0.Slot(101), "http://test-bn:5052")
176+
root := eth2p0.Root{0x01}
177+
l.notifyHeadEvent(ctx, eth2p0.Slot(100), root, "http://test-bn:5052")
178+
l.notifyHeadEvent(ctx, eth2p0.Slot(100), root, "http://test-bn:5052") // Duplicate should be reported (no dedup for head events)
179+
l.notifyHeadEvent(ctx, eth2p0.Slot(101), root, "http://test-bn:5052")
179180

180181
require.Len(t, reportedSlots, 3)
181182
require.Equal(t, eth2p0.Slot(100), reportedSlots[0])

core/fetcher/fetcher.go

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,23 +55,64 @@ func (f *Fetcher) Subscribe(fn func(context.Context, core.Duty, core.UnsignedDat
5555
}
5656

5757
// FetchOnly fetches attestation data and caches it without triggering subscribers.
58-
// This allows early fetching on block events while deferring consensus to the scheduled time.
59-
func (f *Fetcher) FetchOnly(ctx context.Context, duty core.Duty, defSet core.DutyDefinitionSet, bnAddr string) error {
58+
// This allows early fetching on head events while deferring consensus to the scheduled time.
59+
// The data is only cached if it votes for headBlockRoot (the head from the SSE head event);
60+
// otherwise it is dropped so consensus re-fetches fresh data at the scheduled deadline.
61+
func (f *Fetcher) FetchOnly(ctx context.Context, duty core.Duty, defSet core.DutyDefinitionSet, bnAddr string, headBlockRoot eth2p0.Root) error {
6062
if duty.Type != core.DutyAttester {
6163
return errors.New("unsupported duty", z.Str("type", duty.Type.String()))
6264
}
6365

66+
// Evict stale cache entries: head events arrive in increasing slot order, so any cached slot
67+
// below the current one is past its consensus deadline and was either already consumed by Fetch
68+
// or permanently orphaned (e.g. a late head event arriving after consensus already ran). This
69+
// bounds the cache without relying on reorg events.
70+
f.attDataCache.Range(func(key, _ any) bool {
71+
if s, ok := key.(uint64); ok && s < duty.Slot {
72+
f.attDataCache.Delete(s)
73+
}
74+
75+
return true
76+
})
77+
6478
unsignedSet, err := f.fetchAttesterDataFrom(ctx, duty.Slot, defSet, bnAddr)
6579
if err != nil {
6680
return errors.Wrap(err, "fetch attester data for early cache")
6781
}
6882

83+
// Verify the fetched attestation data votes for the head block reported by the SSE head event.
84+
// If the beacon node served data for a different head (e.g. the head moved on between the event
85+
// and the request), skip caching so consensus re-fetches fresh data at the scheduled deadline.
86+
for _, data := range unsignedSet {
87+
attData, ok := data.(core.AttestationData)
88+
if !ok {
89+
return errors.New("invalid attestation data type")
90+
}
91+
92+
if attData.Data.BeaconBlockRoot != headBlockRoot {
93+
log.Debug(ctx, "Skipping early attestation cache: fetched head differs from head event",
94+
z.U64("slot", duty.Slot), z.Str("bn_addr", bnAddr),
95+
z.Str("head_event_root", headBlockRoot.String()),
96+
z.Str("fetched_root", attData.Data.BeaconBlockRoot.String()))
97+
98+
return nil
99+
}
100+
}
101+
69102
f.attDataCache.Store(duty.Slot, unsignedSet)
70103
log.Debug(ctx, "Early attestation data fetched and cached", z.U64("slot", duty.Slot), z.Str("bn_addr", bnAddr))
71104

72105
return nil
73106
}
74107

108+
// HandleChainReorg invalidates the early-fetch cache upon a chain reorg, since cached
109+
// attestation data was verified against a head that may no longer be canonical.
110+
// Consensus then re-fetches fresh data at the scheduled deadline.
111+
func (f *Fetcher) HandleChainReorg(ctx context.Context, epoch eth2p0.Epoch) {
112+
f.attDataCache.Clear()
113+
log.Debug(ctx, "Early attestation data cache invalidated due to chain reorg", z.U64("epoch", uint64(epoch)))
114+
}
115+
75116
// Fetch triggers fetching of a proposed duty data set.
76117
func (f *Fetcher) Fetch(ctx context.Context, duty core.Duty, defSet core.DutyDefinitionSet) error {
77118
var (

0 commit comments

Comments
 (0)