beacon/types, beacon/light: use OptimisticUpdate instead of SignedHeader

This commit is contained in:
Zsolt Felfoldi 2024-04-07 23:25:15 +02:00
parent 0da69e84c0
commit 27618fcac1
13 changed files with 225 additions and 146 deletions

View file

@ -41,7 +41,7 @@ type beaconBlockSync struct {
type headTracker interface { type headTracker interface {
PrefetchHead() types.HeadInfo PrefetchHead() types.HeadInfo
ValidatedHead() (types.SignedHeader, bool) ValidatedOptimistic() (types.OptimisticUpdate, bool)
ValidatedFinality() (types.FinalityUpdate, bool) ValidatedFinality() (types.FinalityUpdate, bool)
} }
@ -80,8 +80,8 @@ func (s *beaconBlockSync) Process(requester request.Requester, events []request.
} }
s.updateEventFeed() s.updateEventFeed()
// request validated head block if unavailable and not yet requested // request validated head block if unavailable and not yet requested
if vh, ok := s.headTracker.ValidatedHead(); ok { if vh, ok := s.headTracker.ValidatedOptimistic(); ok {
s.tryRequestBlock(requester, vh.Header.Hash(), false) s.tryRequestBlock(requester, vh.Attested.Hash(), false)
} }
// request prefetch head if the given server has announced it // request prefetch head if the given server has announced it
if prefetchHead := s.headTracker.PrefetchHead().BlockRoot; prefetchHead != (common.Hash{}) { if prefetchHead := s.headTracker.PrefetchHead().BlockRoot; prefetchHead != (common.Hash{}) {
@ -114,12 +114,12 @@ func blockHeadInfo(block *types.BeaconBlock) types.HeadInfo {
} }
func (s *beaconBlockSync) updateEventFeed() { func (s *beaconBlockSync) updateEventFeed() {
head, ok := s.headTracker.ValidatedHead() optimistic, ok := s.headTracker.ValidatedOptimistic()
if !ok { if !ok {
return return
} }
validatedHead := head.Header.Hash() validatedHead := optimistic.Attested.Hash()
headBlock, ok := s.recentBlocks.Get(validatedHead) headBlock, ok := s.recentBlocks.Get(validatedHead)
if !ok { if !ok {
return return
@ -127,7 +127,7 @@ func (s *beaconBlockSync) updateEventFeed() {
var finalizedHash common.Hash var finalizedHash common.Hash
if finality, ok := s.headTracker.ValidatedFinality(); ok { if finality, ok := s.headTracker.ValidatedFinality(); ok {
he := head.Header.Epoch() he := optimistic.Attested.Epoch()
fe := finality.Attested.Header.Epoch() fe := finality.Attested.Header.Epoch()
switch { switch {
case he == fe: case he == fe:
@ -135,7 +135,7 @@ func (s *beaconBlockSync) updateEventFeed() {
case he < fe: case he < fe:
return return
case he == fe+1: case he == fe+1:
parent, ok := s.recentBlocks.Get(head.Header.ParentRoot) parent, ok := s.recentBlocks.Get(optimistic.Attested.ParentRoot)
if !ok || parent.Slot()/params.EpochLength == fe { if !ok || parent.Slot()/params.EpochLength == fe {
return // head is at first slot of next epoch, wait for finality update return // head is at first slot of next epoch, wait for finality update
//TODO: try to fetch finality update directly if subscription does not deliver //TODO: try to fetch finality update directly if subscription does not deliver
@ -156,7 +156,7 @@ func (s *beaconBlockSync) updateEventFeed() {
return return
} }
s.chainHeadFeed.Send(types.ChainHeadEvent{ s.chainHeadFeed.Send(types.ChainHeadEvent{
BeaconHead: head.Header, BeaconHead: optimistic.Attested.Header,
Block: execBlock, Block: execBlock,
Finalized: finalizedHash, Finalized: finalizedHash,
}) })

View file

@ -134,8 +134,12 @@ func (h *testHeadTracker) PrefetchHead() types.HeadInfo {
return h.prefetch return h.prefetch
} }
func (h *testHeadTracker) ValidatedHead() (types.SignedHeader, bool) { func (h *testHeadTracker) ValidatedOptimistic() (types.OptimisticUpdate, bool) {
return h.validated, h.validated.Header != (types.Header{}) return types.OptimisticUpdate{
Attested: types.HeaderWithExecProof{Header: h.validated.Header},
Signature: h.validated.Signature,
SignatureSlot: h.validated.SignatureSlot,
}, h.validated.Header != (types.Header{})
} }
// TODO add test case for finality // TODO add test case for finality

View file

@ -46,13 +46,13 @@ func (s *ApiServer) Subscribe(eventCallback func(event request.Event)) {
log.Debug("New head received", "slot", slot, "blockRoot", blockRoot) log.Debug("New head received", "slot", slot, "blockRoot", blockRoot)
eventCallback(request.Event{Type: sync.EvNewHead, Data: types.HeadInfo{Slot: slot, BlockRoot: blockRoot}}) eventCallback(request.Event{Type: sync.EvNewHead, Data: types.HeadInfo{Slot: slot, BlockRoot: blockRoot}})
}, },
OnSignedHead: func(head types.SignedHeader) { OnOptimistic: func(update types.OptimisticUpdate) {
log.Debug("New signed head received", "slot", head.Header.Slot, "blockRoot", head.Header.Hash(), "signerCount", head.Signature.SignerCount()) log.Debug("New optimistic update received", "slot", update.Attested.Slot, "blockRoot", update.Attested.Hash(), "signerCount", update.Signature.SignerCount())
eventCallback(request.Event{Type: sync.EvNewSignedHead, Data: head}) eventCallback(request.Event{Type: sync.EvNewOptimisticUpdate, Data: update})
}, },
OnFinality: func(head types.FinalityUpdate) { OnFinality: func(update types.FinalityUpdate) {
log.Debug("New finality update received", "slot", head.Attested.Slot, "blockRoot", head.Attested.Hash(), "signerCount", head.Signature.SignerCount()) log.Debug("New finality update received", "slot", update.Attested.Slot, "blockRoot", update.Attested.Hash(), "signerCount", update.Signature.SignerCount())
eventCallback(request.Event{Type: sync.EvNewFinalityUpdate, Data: head}) eventCallback(request.Event{Type: sync.EvNewFinalityUpdate, Data: update})
}, },
OnError: func(err error) { OnError: func(err error) {
log.Warn("Head event stream error", "err", err) log.Warn("Head event stream error", "err", err)

View file

@ -184,46 +184,56 @@ func (api *BeaconLightApi) GetBestUpdatesAndCommittees(firstPeriod, count uint64
return updates, committees, nil return updates, committees, nil
} }
// GetOptimisticHeadUpdate fetches a signed header based on the latest available // GetOptimisticUpdate fetches the latest available optimistic update.
// optimistic update. Note that the signature should be verified by the caller // Note that the signature should be verified by the caller as its validity
// as its validity depends on the update chain. // depends on the update chain.
// //
// See data structure definition here: // See data structure definition here:
// https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/sync-protocol.md#lightclientoptimisticupdate // https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/sync-protocol.md#lightclientoptimisticupdate
func (api *BeaconLightApi) GetOptimisticHeadUpdate() (types.SignedHeader, error) { func (api *BeaconLightApi) GetOptimisticUpdate() (types.OptimisticUpdate, error) {
resp, err := api.httpGet("/eth/v1/beacon/light_client/optimistic_update") resp, err := api.httpGet("/eth/v1/beacon/light_client/optimistic_update")
if err != nil { if err != nil {
return types.SignedHeader{}, err return types.OptimisticUpdate{}, err
} }
return decodeOptimisticHeadUpdate(resp) return decodeOptimisticUpdate(resp)
} }
func decodeOptimisticHeadUpdate(enc []byte) (types.SignedHeader, error) { func decodeOptimisticUpdate(enc []byte) (types.OptimisticUpdate, error) {
var data struct { var data struct {
Data struct { Version string
Header jsonBeaconHeader `json:"attested_header"` Data struct {
Aggregate types.SyncAggregate `json:"sync_aggregate"` Attested jsonHeaderWithExecProof `json:"attested_header"`
SignatureSlot common.Decimal `json:"signature_slot"` Aggregate types.SyncAggregate `json:"sync_aggregate"`
SignatureSlot common.Decimal `json:"signature_slot"`
} `json:"data"` } `json:"data"`
} }
if err := json.Unmarshal(enc, &data); err != nil { if err := json.Unmarshal(enc, &data); err != nil {
return types.SignedHeader{}, err return types.OptimisticUpdate{}, err
} }
if data.Data.Header.Beacon.StateRoot == (common.Hash{}) { // Decode the execution payload headers.
attestedExecHeader, err := types.ExecutionHeaderFromJSON(data.Version, data.Data.Attested.Execution)
if err != nil {
return types.OptimisticUpdate{}, fmt.Errorf("invalid attested header: %v", err)
}
if data.Data.Attested.Beacon.StateRoot == (common.Hash{}) {
// workaround for different event encoding format in Lodestar // workaround for different event encoding format in Lodestar
if err := json.Unmarshal(enc, &data.Data); err != nil { if err := json.Unmarshal(enc, &data.Data); err != nil {
return types.SignedHeader{}, err return types.OptimisticUpdate{}, err
} }
} }
if len(data.Data.Aggregate.Signers) != params.SyncCommitteeBitmaskSize { if len(data.Data.Aggregate.Signers) != params.SyncCommitteeBitmaskSize {
return types.SignedHeader{}, errors.New("invalid sync_committee_bits length") return types.OptimisticUpdate{}, errors.New("invalid sync_committee_bits length")
} }
if len(data.Data.Aggregate.Signature) != params.BLSSignatureSize { if len(data.Data.Aggregate.Signature) != params.BLSSignatureSize {
return types.SignedHeader{}, errors.New("invalid sync_committee_signature length") return types.OptimisticUpdate{}, errors.New("invalid sync_committee_signature length")
} }
return types.SignedHeader{ return types.OptimisticUpdate{
Header: data.Data.Header.Beacon, Attested: types.HeaderWithExecProof{
Header: data.Data.Attested.Beacon,
PayloadHeader: attestedExecHeader,
PayloadBranch: data.Data.Attested.ExecutionBranch,
},
Signature: data.Data.Aggregate, Signature: data.Data.Aggregate,
SignatureSlot: uint64(data.Data.SignatureSlot), SignatureSlot: uint64(data.Data.SignatureSlot),
}, nil }, nil
@ -408,7 +418,7 @@ func decodeHeadEvent(enc []byte) (uint64, common.Hash, error) {
type HeadEventListener struct { type HeadEventListener struct {
OnNewHead func(slot uint64, blockRoot common.Hash) OnNewHead func(slot uint64, blockRoot common.Hash)
OnSignedHead func(head types.SignedHeader) OnOptimistic func(head types.OptimisticUpdate)
OnFinality func(head types.FinalityUpdate) OnFinality func(head types.FinalityUpdate)
OnError func(err error) OnError func(err error)
} }
@ -449,8 +459,8 @@ func (api *BeaconLightApi) StartHeadListener(listener HeadEventListener) func()
if head, err := api.GetHeader(common.Hash{}); err == nil { if head, err := api.GetHeader(common.Hash{}); err == nil {
listener.OnNewHead(head.Slot, head.Hash()) listener.OnNewHead(head.Slot, head.Hash())
} }
if signedHead, err := api.GetOptimisticHeadUpdate(); err == nil { if optimisticUpdate, err := api.GetOptimisticUpdate(); err == nil {
listener.OnSignedHead(signedHead) listener.OnOptimistic(optimisticUpdate)
} }
if finalityUpdate, err := api.GetFinalityUpdate(); err == nil { if finalityUpdate, err := api.GetFinalityUpdate(); err == nil {
listener.OnFinality(finalityUpdate) listener.OnFinality(finalityUpdate)
@ -482,9 +492,9 @@ func (api *BeaconLightApi) StartHeadListener(listener HeadEventListener) func()
listener.OnError(fmt.Errorf("error decoding head event: %v", err)) listener.OnError(fmt.Errorf("error decoding head event: %v", err))
} }
case "light_client_optimistic_update": case "light_client_optimistic_update":
signedHead, err := decodeOptimisticHeadUpdate([]byte(event.Data())) optimisticUpdate, err := decodeOptimisticUpdate([]byte(event.Data()))
if err == nil { if err == nil {
listener.OnSignedHead(signedHead) listener.OnOptimistic(optimisticUpdate)
} else { } else {
listener.OnError(fmt.Errorf("error decoding optimistic update event: %v", err)) listener.OnError(fmt.Errorf("error decoding optimistic update event: %v", err))
} }

View file

@ -29,15 +29,15 @@ import (
// which is the (not necessarily validated) head announced by the majority of // which is the (not necessarily validated) head announced by the majority of
// servers. // servers.
type HeadTracker struct { type HeadTracker struct {
lock sync.RWMutex lock sync.RWMutex
committeeChain *CommitteeChain committeeChain *CommitteeChain
minSignerCount int minSignerCount int
signedHead types.SignedHeader optimisticUpdate types.OptimisticUpdate
hasSignedHead bool hasOptimisticUpdate bool
finalityUpdate types.FinalityUpdate finalityUpdate types.FinalityUpdate
hasFinalityUpdate bool hasFinalityUpdate bool
prefetchHead types.HeadInfo prefetchHead types.HeadInfo
changeCounter uint64 changeCounter uint64
} }
// NewHeadTracker creates a new HeadTracker. // NewHeadTracker creates a new HeadTracker.
@ -48,15 +48,15 @@ func NewHeadTracker(committeeChain *CommitteeChain, minSignerCount int) *HeadTra
} }
} }
// ValidatedHead returns the latest validated head. // ValidatedOptimistic returns the latest validated optimistic update.
func (h *HeadTracker) ValidatedHead() (types.SignedHeader, bool) { func (h *HeadTracker) ValidatedOptimistic() (types.OptimisticUpdate, bool) {
h.lock.RLock() h.lock.RLock()
defer h.lock.RUnlock() defer h.lock.RUnlock()
return h.signedHead, h.hasSignedHead return h.optimisticUpdate, h.hasOptimisticUpdate
} }
// ValidatedFinality returns the latest validated finality. // ValidatedFinality returns the latest validated finality update.
func (h *HeadTracker) ValidatedFinality() (types.FinalityUpdate, bool) { func (h *HeadTracker) ValidatedFinality() (types.FinalityUpdate, bool) {
h.lock.RLock() h.lock.RLock()
defer h.lock.RUnlock() defer h.lock.RUnlock()
@ -64,26 +64,36 @@ func (h *HeadTracker) ValidatedFinality() (types.FinalityUpdate, bool) {
return h.finalityUpdate, h.hasFinalityUpdate return h.finalityUpdate, h.hasFinalityUpdate
} }
// ValidateHead validates the given signed head. If the head is successfully validated // ValidateOptimistic validates the given optimistic update. If the update is
// and it is better than the old validated head (higher slot or same slot and more // successfully validated and it is better than the old validated update (higher
// signers) then ValidatedHead is updated. The boolean return flag signals if // slot or same slot and more signers) then ValidatedOptimistic is updated.
// ValidatedHead has been changed. // The boolean return flag signals if ValidatedOptimistic has been changed.
func (h *HeadTracker) ValidateHead(head types.SignedHeader) (bool, error) { func (h *HeadTracker) ValidateOptimistic(update types.OptimisticUpdate) (bool, error) {
h.lock.Lock() h.lock.Lock()
defer h.lock.Unlock() defer h.lock.Unlock()
replace, err := h.validate(head, h.signedHead) if err := update.Validate(); err != nil {
return false, err
}
replace, err := h.validate(update.SignedHeader(), h.optimisticUpdate.SignedHeader())
if replace { if replace {
h.signedHead, h.hasSignedHead = head, true h.optimisticUpdate, h.hasOptimisticUpdate = update, true
h.changeCounter++ h.changeCounter++
} }
return replace, err return replace, err
} }
// ValidateFinality validates the given finality update. If the update is
// successfully validated and it is better than the old validated update (higher
// slot or same slot and more signers) then ValidatedFinality is updated.
// The boolean return flag signals if ValidatedFinality has been changed.
func (h *HeadTracker) ValidateFinality(update types.FinalityUpdate) (bool, error) { func (h *HeadTracker) ValidateFinality(update types.FinalityUpdate) (bool, error) {
h.lock.Lock() h.lock.Lock()
defer h.lock.Unlock() defer h.lock.Unlock()
if err := update.Validate(); err != nil {
return false, err
}
replace, err := h.validate(update.SignedHeader(), h.finalityUpdate.SignedHeader()) replace, err := h.validate(update.SignedHeader(), h.finalityUpdate.SignedHeader())
if replace { if replace {
h.finalityUpdate, h.hasFinalityUpdate = update, true h.finalityUpdate, h.hasFinalityUpdate = update, true
@ -142,6 +152,7 @@ func (h *HeadTracker) SetPrefetchHead(head types.HeadInfo) {
h.changeCounter++ h.changeCounter++
} }
// ChangeCounter implements request.targetData
func (h *HeadTracker) ChangeCounter() uint64 { func (h *HeadTracker) ChangeCounter() uint64 {
h.lock.RLock() h.lock.RLock()
defer h.lock.RUnlock() defer h.lock.RUnlock()

View file

@ -19,11 +19,12 @@ package sync
import ( import (
"github.com/ethereum/go-ethereum/beacon/light/request" "github.com/ethereum/go-ethereum/beacon/light/request"
"github.com/ethereum/go-ethereum/beacon/types" "github.com/ethereum/go-ethereum/beacon/types"
"github.com/ethereum/go-ethereum/log"
) )
type headTracker interface { type headTracker interface {
ValidateHead(head types.SignedHeader) (bool, error) ValidateOptimistic(update types.OptimisticUpdate) (bool, error)
ValidateFinality(head types.FinalityUpdate) (bool, error) ValidateFinality(update types.FinalityUpdate) (bool, error)
SetPrefetchHead(head types.HeadInfo) SetPrefetchHead(head types.HeadInfo)
} }
@ -33,16 +34,16 @@ type headTracker interface {
// It can also postpone the validation of the latest announced signed head // It can also postpone the validation of the latest announced signed head
// until the committee chain is synced up to at least the required period. // until the committee chain is synced up to at least the required period.
type HeadSync struct { type HeadSync struct {
headTracker headTracker headTracker headTracker
chain committeeChain chain committeeChain
nextSyncPeriod uint64 nextSyncPeriod uint64
chainInit bool chainInit bool
unvalidatedHeads map[request.Server]types.SignedHeader unvalidatedOptimistic map[request.Server]types.OptimisticUpdate
unvalidatedFinality map[request.Server]types.FinalityUpdate unvalidatedFinality map[request.Server]types.FinalityUpdate
serverHeads map[request.Server]types.HeadInfo serverHeads map[request.Server]types.HeadInfo
headServerCount map[types.HeadInfo]headServerCount headServerCount map[types.HeadInfo]headServerCount
headCounter uint64 headCounter uint64
prefetchHead types.HeadInfo prefetchHead types.HeadInfo
} }
// headServerCount is associated with most recently seen head infos; it counts // headServerCount is associated with most recently seen head infos; it counts
@ -57,12 +58,12 @@ type headServerCount struct {
// NewHeadSync creates a new HeadSync. // NewHeadSync creates a new HeadSync.
func NewHeadSync(headTracker headTracker, chain committeeChain) *HeadSync { func NewHeadSync(headTracker headTracker, chain committeeChain) *HeadSync {
s := &HeadSync{ s := &HeadSync{
headTracker: headTracker, headTracker: headTracker,
chain: chain, chain: chain,
unvalidatedHeads: make(map[request.Server]types.SignedHeader), unvalidatedOptimistic: make(map[request.Server]types.OptimisticUpdate),
unvalidatedFinality: make(map[request.Server]types.FinalityUpdate), unvalidatedFinality: make(map[request.Server]types.FinalityUpdate),
serverHeads: make(map[request.Server]types.HeadInfo), serverHeads: make(map[request.Server]types.HeadInfo),
headServerCount: make(map[types.HeadInfo]headServerCount), headServerCount: make(map[types.HeadInfo]headServerCount),
} }
return s return s
} }
@ -73,59 +74,68 @@ func (s *HeadSync) Process(requester request.Requester, events []request.Event)
switch event.Type { switch event.Type {
case EvNewHead: case EvNewHead:
s.setServerHead(event.Server, event.Data.(types.HeadInfo)) s.setServerHead(event.Server, event.Data.(types.HeadInfo))
case EvNewSignedHead: case EvNewOptimisticUpdate:
s.newSignedHead(event.Server, event.Data.(types.SignedHeader)) s.newOptimisticUpdate(event.Server, event.Data.(types.OptimisticUpdate))
case EvNewFinalityUpdate: case EvNewFinalityUpdate:
s.newFinalityUpdate(event.Server, event.Data.(types.FinalityUpdate)) s.newFinalityUpdate(event.Server, event.Data.(types.FinalityUpdate))
case request.EvUnregistered: case request.EvUnregistered:
s.setServerHead(event.Server, types.HeadInfo{}) s.setServerHead(event.Server, types.HeadInfo{})
delete(s.serverHeads, event.Server) delete(s.serverHeads, event.Server)
delete(s.unvalidatedHeads, event.Server) delete(s.unvalidatedOptimistic, event.Server)
delete(s.unvalidatedFinality, event.Server)
} }
} }
nextPeriod, chainInit := s.chain.NextSyncPeriod() nextPeriod, chainInit := s.chain.NextSyncPeriod()
if nextPeriod != s.nextSyncPeriod || chainInit != s.chainInit { if nextPeriod != s.nextSyncPeriod || chainInit != s.chainInit {
s.nextSyncPeriod, s.chainInit = nextPeriod, chainInit s.nextSyncPeriod, s.chainInit = nextPeriod, chainInit
s.processUnvalidated() s.processUnvalidatedUpdates()
} }
} }
// newSignedHead handles received signed head; either validates it if the chain // newOptimisticUpdate handles received optimistic update; either validates it if
// is properly synced or stores it for further validation. // the chain is properly synced or stores it for further validation.
func (s *HeadSync) newSignedHead(server request.Server, signedHead types.SignedHeader) { func (s *HeadSync) newOptimisticUpdate(server request.Server, optimisticUpdate types.OptimisticUpdate) {
if !s.chainInit || types.SyncPeriod(signedHead.SignatureSlot) > s.nextSyncPeriod { if !s.chainInit || types.SyncPeriod(optimisticUpdate.SignatureSlot) > s.nextSyncPeriod {
s.unvalidatedHeads[server] = signedHead s.unvalidatedOptimistic[server] = optimisticUpdate
return return
} }
s.headTracker.ValidateHead(signedHead) if _, err := s.headTracker.ValidateOptimistic(optimisticUpdate); err != nil {
log.Debug("Error validating optimistic update", "error", err)
}
} }
// newFinalityUpdate handles received finality update; either validates it if the chain // newFinalityUpdate handles received finality update; either validates it if
// is properly synced or stores it for further validation. // the chain is properly synced or stores it for further validation.
func (s *HeadSync) newFinalityUpdate(server request.Server, finalityUpdate types.FinalityUpdate) { func (s *HeadSync) newFinalityUpdate(server request.Server, finalityUpdate types.FinalityUpdate) {
if !s.chainInit || types.SyncPeriod(finalityUpdate.SignatureSlot) > s.nextSyncPeriod { if !s.chainInit || types.SyncPeriod(finalityUpdate.SignatureSlot) > s.nextSyncPeriod {
s.unvalidatedFinality[server] = finalityUpdate s.unvalidatedFinality[server] = finalityUpdate
return return
} }
s.headTracker.ValidateFinality(finalityUpdate) if _, err := s.headTracker.ValidateFinality(finalityUpdate); err != nil {
log.Debug("Error validating finality update", "error", err)
}
} }
// processUnvalidated iterates the list of unvalidated heads and validates // processUnvalidatedUpdates iterates the list of unvalidated updates and validates
// those which can be validated. // those which can be validated.
func (s *HeadSync) processUnvalidated() { func (s *HeadSync) processUnvalidatedUpdates() {
if !s.chainInit { if !s.chainInit {
return return
} }
for server, signedHead := range s.unvalidatedHeads { for server, optimisticUpdate := range s.unvalidatedOptimistic {
if types.SyncPeriod(signedHead.SignatureSlot) <= s.nextSyncPeriod { if types.SyncPeriod(optimisticUpdate.SignatureSlot) <= s.nextSyncPeriod {
s.headTracker.ValidateHead(signedHead) if _, err := s.headTracker.ValidateOptimistic(optimisticUpdate); err != nil {
delete(s.unvalidatedHeads, server) log.Debug("Error validating deferred optimistic update", "error", err)
}
delete(s.unvalidatedOptimistic, server)
} }
} }
for server, finalityUpdate := range s.unvalidatedFinality { for server, finalityUpdate := range s.unvalidatedFinality {
if types.SyncPeriod(finalityUpdate.SignatureSlot) <= s.nextSyncPeriod { if types.SyncPeriod(finalityUpdate.SignatureSlot) <= s.nextSyncPeriod {
s.headTracker.ValidateFinality(finalityUpdate) if _, err := s.headTracker.ValidateFinality(finalityUpdate); err != nil {
log.Debug("Error validating deferred finality update", "error", err)
}
delete(s.unvalidatedFinality, server) delete(s.unvalidatedFinality, server)
} }
} }

View file

@ -35,11 +35,11 @@ var (
testHead3 = types.HeadInfo{Slot: 124, BlockRoot: common.Hash{3}} testHead3 = types.HeadInfo{Slot: 124, BlockRoot: common.Hash{3}}
testHead4 = types.HeadInfo{Slot: 125, BlockRoot: common.Hash{4}} testHead4 = types.HeadInfo{Slot: 125, BlockRoot: common.Hash{4}}
testSHead1 = types.SignedHeader{SignatureSlot: 0x0124, Header: types.Header{Slot: 0x0123, StateRoot: common.Hash{1}}} testOptUpdate1 = types.OptimisticUpdate{SignatureSlot: 0x0124, Attested: types.HeaderWithExecProof{Header: types.Header{Slot: 0x0123, StateRoot: common.Hash{1}}}}
testSHead2 = types.SignedHeader{SignatureSlot: 0x2010, Header: types.Header{Slot: 0x200e, StateRoot: common.Hash{2}}} testOptUpdate2 = types.OptimisticUpdate{SignatureSlot: 0x2010, Attested: types.HeaderWithExecProof{Header: types.Header{Slot: 0x200e, StateRoot: common.Hash{2}}}}
// testSHead3 is at the end of period 1 but signed in period 2 // testOptUpdate3 is at the end of period 1 but signed in period 2
testSHead3 = types.SignedHeader{SignatureSlot: 0x4000, Header: types.Header{Slot: 0x3fff, StateRoot: common.Hash{3}}} testOptUpdate3 = types.OptimisticUpdate{SignatureSlot: 0x4000, Attested: types.HeaderWithExecProof{Header: types.Header{Slot: 0x3fff, StateRoot: common.Hash{3}}}}
testSHead4 = types.SignedHeader{SignatureSlot: 0x6444, Header: types.Header{Slot: 0x6443, StateRoot: common.Hash{4}}} testOptUpdate4 = types.OptimisticUpdate{SignatureSlot: 0x6444, Attested: types.HeaderWithExecProof{Header: types.Header{Slot: 0x6443, StateRoot: common.Hash{4}}}}
) )
func TestValidatedHead(t *testing.T) { func TestValidatedHead(t *testing.T) {
@ -51,7 +51,7 @@ func TestValidatedHead(t *testing.T) {
ht.ExpValidated(t, 0, nil) ht.ExpValidated(t, 0, nil)
ts.AddServer(testServer1, 1) ts.AddServer(testServer1, 1)
ts.ServerEvent(EvNewSignedHead, testServer1, testSHead1) ts.ServerEvent(EvNewOptimisticUpdate, testServer1, testOptUpdate1)
ts.Run(1) ts.Run(1)
// announced head should be queued because of uninitialized chain // announced head should be queued because of uninitialized chain
ht.ExpValidated(t, 1, nil) ht.ExpValidated(t, 1, nil)
@ -59,27 +59,27 @@ func TestValidatedHead(t *testing.T) {
chain.SetNextSyncPeriod(0) // initialize chain chain.SetNextSyncPeriod(0) // initialize chain
ts.Run(2) ts.Run(2)
// expect previously queued head to be validated // expect previously queued head to be validated
ht.ExpValidated(t, 2, []types.SignedHeader{testSHead1}) ht.ExpValidated(t, 2, []types.OptimisticUpdate{testOptUpdate1})
chain.SetNextSyncPeriod(1) chain.SetNextSyncPeriod(1)
ts.ServerEvent(EvNewSignedHead, testServer1, testSHead2) ts.ServerEvent(EvNewOptimisticUpdate, testServer1, testOptUpdate2)
ts.AddServer(testServer2, 1) ts.AddServer(testServer2, 1)
ts.ServerEvent(EvNewSignedHead, testServer2, testSHead2) ts.ServerEvent(EvNewOptimisticUpdate, testServer2, testOptUpdate2)
ts.Run(3) ts.Run(3)
// expect both head announcements to be validated instantly // expect both head announcements to be validated instantly
ht.ExpValidated(t, 3, []types.SignedHeader{testSHead2, testSHead2}) ht.ExpValidated(t, 3, []types.OptimisticUpdate{testOptUpdate2, testOptUpdate2})
ts.ServerEvent(EvNewSignedHead, testServer1, testSHead3) ts.ServerEvent(EvNewOptimisticUpdate, testServer1, testOptUpdate3)
ts.AddServer(testServer3, 1) ts.AddServer(testServer3, 1)
ts.ServerEvent(EvNewSignedHead, testServer3, testSHead4) ts.ServerEvent(EvNewOptimisticUpdate, testServer3, testOptUpdate4)
ts.Run(4) ts.Run(4)
// future period announced heads should be queued // future period annonced heads should be queued
ht.ExpValidated(t, 4, nil) ht.ExpValidated(t, 4, nil)
chain.SetNextSyncPeriod(2) chain.SetNextSyncPeriod(2)
ts.Run(5) ts.Run(5)
// testSHead3 can be validated now but not testSHead4 // testOptUpdate3 can be validated now but not testOptUpdate4
ht.ExpValidated(t, 5, []types.SignedHeader{testSHead3}) ht.ExpValidated(t, 5, []types.OptimisticUpdate{testOptUpdate3})
// server 3 disconnected without proving period 3, its announced head should be dropped // server 3 disconnected without proving period 3, its announced head should be dropped
ts.RemoveServer(testServer3) ts.RemoveServer(testServer3)
@ -88,13 +88,13 @@ func TestValidatedHead(t *testing.T) {
chain.SetNextSyncPeriod(3) chain.SetNextSyncPeriod(3)
ts.Run(7) ts.Run(7)
// testSHead4 could be validated now but it's not queued by any registered server // testOptUpdate4 could be validated now but it's not queued by any registered server
ht.ExpValidated(t, 7, nil) ht.ExpValidated(t, 7, nil)
ts.ServerEvent(EvNewSignedHead, testServer2, testSHead4) ts.ServerEvent(EvNewOptimisticUpdate, testServer2, testOptUpdate4)
ts.Run(8) ts.Run(8)
// now testSHead4 should be validated // now testOptUpdate4 should be validated
ht.ExpValidated(t, 8, []types.SignedHeader{testSHead4}) ht.ExpValidated(t, 8, []types.OptimisticUpdate{testOptUpdate4})
} }
func TestPrefetchHead(t *testing.T) { func TestPrefetchHead(t *testing.T) {

View file

@ -212,32 +212,32 @@ func (tc *TestCommitteeChain) ExpNextSyncPeriod(t *testing.T, expNsp uint64) {
type TestHeadTracker struct { type TestHeadTracker struct {
phead types.HeadInfo phead types.HeadInfo
validated []types.SignedHeader validated []types.OptimisticUpdate
} }
func (ht *TestHeadTracker) ValidateHead(head types.SignedHeader) (bool, error) { func (ht *TestHeadTracker) ValidateOptimistic(update types.OptimisticUpdate) (bool, error) {
ht.validated = append(ht.validated, head) ht.validated = append(ht.validated, update)
return true, nil return true, nil
} }
// TODO add test case for finality //TODO add test case for finality
func (ht *TestHeadTracker) ValidateFinality(head types.FinalityUpdate) (bool, error) { func (ht *TestHeadTracker) ValidateFinality(update types.FinalityUpdate) (bool, error) {
return true, nil return true, nil
} }
func (ht *TestHeadTracker) ExpValidated(t *testing.T, tci int, expHeads []types.SignedHeader) { func (ht *TestHeadTracker) ExpValidated(t *testing.T, tci int, expHeads []types.OptimisticUpdate) {
for i, expHead := range expHeads { for i, expHead := range expHeads {
if i >= len(ht.validated) { if i >= len(ht.validated) {
t.Errorf("Missing validated head in test case #%d index #%d (expected {slot %d blockRoot %x}, got none)", tci, i, expHead.Header.Slot, expHead.Header.Hash()) t.Errorf("Missing validated head in test case #%d index #%d (expected {slot %d blockRoot %x}, got none)", tci, i, expHead.Attested.Header.Slot, expHead.Attested.Header.Hash())
continue continue
} }
if ht.validated[i] != expHead { if !reflect.DeepEqual(ht.validated[i], expHead) {
vhead := ht.validated[i].Header vhead := ht.validated[i].Attested.Header
t.Errorf("Wrong validated head in test case #%d index #%d (expected {slot %d blockRoot %x}, got {slot %d blockRoot %x})", tci, i, expHead.Header.Slot, expHead.Header.Hash(), vhead.Slot, vhead.Hash()) t.Errorf("Wrong validated head in test case #%d index #%d (expected {slot %d blockRoot %x}, got {slot %d blockRoot %x})", tci, i, expHead.Attested.Header.Slot, expHead.Attested.Header.Hash(), vhead.Slot, vhead.Hash())
} }
} }
for i := len(expHeads); i < len(ht.validated); i++ { for i := len(expHeads); i < len(ht.validated); i++ {
vhead := ht.validated[i].Header vhead := ht.validated[i].Attested.Header
t.Errorf("Unexpected validated head in test case #%d index #%d (expected none, got {slot %d blockRoot %x})", tci, i, vhead.Slot, vhead.Hash()) t.Errorf("Unexpected validated head in test case #%d index #%d (expected none, got {slot %d blockRoot %x})", tci, i, vhead.Slot, vhead.Hash())
} }
ht.validated = nil ht.validated = nil

View file

@ -23,9 +23,9 @@ import (
) )
var ( var (
EvNewHead = &request.EventType{Name: "newHead"} // data: types.HeadInfo EvNewHead = &request.EventType{Name: "newHead"} // data: types.HeadInfo
EvNewSignedHead = &request.EventType{Name: "newSignedHead"} // data: types.SignedHeader EvNewOptimisticUpdate = &request.EventType{Name: "newOptimisticUpdate"} // data: types.OptimisticUpdate
EvNewFinalityUpdate = &request.EventType{Name: "newFinalityUpdate"} // data: types.FinalityUpdate EvNewFinalityUpdate = &request.EventType{Name: "newFinalityUpdate"} // data: types.FinalityUpdate
) )
type ( type (

View file

@ -221,9 +221,9 @@ func (s *ForwardUpdateSync) Process(requester request.Requester, events []reques
if !queued { if !queued {
s.unlockRange(sid, req) s.unlockRange(sid, req)
} }
case EvNewSignedHead: case EvNewOptimisticUpdate:
signedHead := event.Data.(types.SignedHeader) update := event.Data.(types.OptimisticUpdate)
s.nextSyncPeriod[event.Server] = types.SyncPeriod(signedHead.SignatureSlot + 256) s.nextSyncPeriod[event.Server] = types.SyncPeriod(update.SignatureSlot + 256)
case request.EvUnregistered: case request.EvUnregistered:
delete(s.nextSyncPeriod, event.Server) delete(s.nextSyncPeriod, event.Server)
} }

View file

@ -68,9 +68,9 @@ func TestUpdateSyncParallel(t *testing.T) {
ts := NewTestScheduler(t, updateSync) ts := NewTestScheduler(t, updateSync)
// add 2 servers, head at period 100; allow 3-3 parallel requests for each // add 2 servers, head at period 100; allow 3-3 parallel requests for each
ts.AddServer(testServer1, 3) ts.AddServer(testServer1, 3)
ts.ServerEvent(EvNewSignedHead, testServer1, types.SignedHeader{SignatureSlot: 0x2000*100 + 0x1000}) ts.ServerEvent(EvNewOptimisticUpdate, testServer1, types.OptimisticUpdate{SignatureSlot: 0x2000*100 + 0x1000})
ts.AddServer(testServer2, 3) ts.AddServer(testServer2, 3)
ts.ServerEvent(EvNewSignedHead, testServer2, types.SignedHeader{SignatureSlot: 0x2000*100 + 0x1000}) ts.ServerEvent(EvNewOptimisticUpdate, testServer2, types.OptimisticUpdate{SignatureSlot: 0x2000*100 + 0x1000})
// expect 6 requests to be sent // expect 6 requests to be sent
ts.Run(1, ts.Run(1,
@ -150,11 +150,11 @@ func TestUpdateSyncDifferentHeads(t *testing.T) {
ts := NewTestScheduler(t, updateSync) ts := NewTestScheduler(t, updateSync)
// add 3 servers with different announced head periods // add 3 servers with different announced head periods
ts.AddServer(testServer1, 1) ts.AddServer(testServer1, 1)
ts.ServerEvent(EvNewSignedHead, testServer1, types.SignedHeader{SignatureSlot: 0x2000*15 + 0x1000}) ts.ServerEvent(EvNewOptimisticUpdate, testServer1, types.OptimisticUpdate{SignatureSlot: 0x2000*15 + 0x1000})
ts.AddServer(testServer2, 1) ts.AddServer(testServer2, 1)
ts.ServerEvent(EvNewSignedHead, testServer2, types.SignedHeader{SignatureSlot: 0x2000*16 + 0x1000}) ts.ServerEvent(EvNewOptimisticUpdate, testServer2, types.OptimisticUpdate{SignatureSlot: 0x2000*16 + 0x1000})
ts.AddServer(testServer3, 1) ts.AddServer(testServer3, 1)
ts.ServerEvent(EvNewSignedHead, testServer3, types.SignedHeader{SignatureSlot: 0x2000*17 + 0x1000}) ts.ServerEvent(EvNewOptimisticUpdate, testServer3, types.OptimisticUpdate{SignatureSlot: 0x2000*17 + 0x1000})
// expect request to the best announced head // expect request to the best announced head
ts.Run(1, testServer3, ReqUpdates{FirstPeriod: 10, Count: 7}) ts.Run(1, testServer3, ReqUpdates{FirstPeriod: 10, Count: 7})
@ -190,7 +190,7 @@ func TestUpdateSyncDifferentHeads(t *testing.T) {
// a new server is registered with announced head period 17 // a new server is registered with announced head period 17
ts.AddServer(testServer4, 1) ts.AddServer(testServer4, 1)
ts.ServerEvent(EvNewSignedHead, testServer4, types.SignedHeader{SignatureSlot: 0x2000*17 + 0x1000}) ts.ServerEvent(EvNewOptimisticUpdate, testServer4, types.OptimisticUpdate{SignatureSlot: 0x2000*17 + 0x1000})
// expect request to sync one more period // expect request to sync one more period
ts.Run(7, testServer4, ReqUpdates{FirstPeriod: 16, Count: 1}) ts.Run(7, testServer4, ReqUpdates{FirstPeriod: 16, Count: 1})

View file

@ -66,9 +66,8 @@ func convertPayload[T payloadType](payload T, parentRoot *zrntcommon.Root) (*typ
block := types.NewBlockWithHeader(&header) block := types.NewBlockWithHeader(&header)
block = block.WithBody(transactions, nil) block = block.WithBody(transactions, nil)
block = block.WithWithdrawals(withdrawals) block = block.WithWithdrawals(withdrawals)
hash := block.Hash() if hash := block.Hash(); hash != expectedHash {
if hash != expectedHash { return nil, fmt.Errorf("Sanity check failed, payload hash does not match (expected %x, got %x)", expectedHash, hash)
return block, fmt.Errorf("Sanity check failed, payload hash does not match (expected %x, got %x)", expectedHash, hash)
} }
return block, nil return block, nil
} }

View file

@ -23,7 +23,7 @@ import (
"github.com/ethereum/go-ethereum/beacon/merkle" "github.com/ethereum/go-ethereum/beacon/merkle"
"github.com/ethereum/go-ethereum/beacon/params" "github.com/ethereum/go-ethereum/beacon/params"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types" ctypes "github.com/ethereum/go-ethereum/core/types"
) )
// HeadInfo represents an unvalidated new head announcement. // HeadInfo represents an unvalidated new head announcement.
@ -142,17 +142,57 @@ func (u UpdateScore) BetterThan(w UpdateScore) bool {
return u.SignerCount > w.SignerCount return u.SignerCount > w.SignerCount
} }
// HeaderWithExecProof contains a beacon header and proves the belonging execution
// payload header with a Merkle proof.
type HeaderWithExecProof struct { type HeaderWithExecProof struct {
Header Header
PayloadHeader *ExecutionHeader PayloadHeader *ExecutionHeader
PayloadBranch merkle.Values PayloadBranch merkle.Values
} }
// Validate verifies the Merkle proof of the execution payload header.
func (h *HeaderWithExecProof) Validate() error { func (h *HeaderWithExecProof) Validate() error {
payloadRoot := h.PayloadHeader.PayloadRoot() return merkle.VerifyProof(h.BodyRoot, params.BodyIndexExecPayload, h.PayloadBranch, h.PayloadHeader.PayloadRoot())
return merkle.VerifyProof(h.BodyRoot, params.BodyIndexExecPayload, h.PayloadBranch, payloadRoot)
} }
// OptimisticUpdate proves sync committee commitment on the attested beacon header.
// It also proves the belonging execution payload header with a Merkle proof.
//
// See data structure definition here:
// https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/sync-protocol.md#lightclientoptimisticupdate
type OptimisticUpdate struct {
Attested HeaderWithExecProof
// Sync committee BLS signature aggregate
Signature SyncAggregate
// Slot in which the signature has been created (newer than Header.Slot,
// determines the signing sync committee)
SignatureSlot uint64
}
// SignedHeader returns the signed attested header of the update.
func (u *OptimisticUpdate) SignedHeader() SignedHeader {
return SignedHeader{
Header: u.Attested.Header,
Signature: u.Signature,
SignatureSlot: u.SignatureSlot,
}
}
// Validate verifies the Merkle proof proving the execution payload header.
// Note that the sync committee signature of the attested header should be
// verified separately by a synced committee chain.
func (u *OptimisticUpdate) Validate() error {
return u.Attested.Validate()
}
// FinalityUpdate proves a finalized beacon header by a sync committee commitment
// on an attested beacon header, referring to the latest finalized header with a
// Merkle proof.
// It also proves the execution payload header belonging to both the attested and
// the finalized beacon header with Merkle proofs.
//
// See data structure definition here:
// https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/sync-protocol.md#lightclientfinalityupdate
type FinalityUpdate struct { type FinalityUpdate struct {
Attested, Finalized HeaderWithExecProof Attested, Finalized HeaderWithExecProof
FinalityBranch merkle.Values FinalityBranch merkle.Values
@ -163,6 +203,7 @@ type FinalityUpdate struct {
SignatureSlot uint64 SignatureSlot uint64
} }
// SignedHeader returns the signed attested header of the update.
func (u *FinalityUpdate) SignedHeader() SignedHeader { func (u *FinalityUpdate) SignedHeader() SignedHeader {
return SignedHeader{ return SignedHeader{
Header: u.Attested.Header, Header: u.Attested.Header,
@ -171,6 +212,10 @@ func (u *FinalityUpdate) SignedHeader() SignedHeader {
} }
} }
// Validate verifies the Merkle proofs proving the finalized beacon header and
// the execution payload headers belonging to the attested and finalized headers.
// Note that the sync committee signature of the attested header should be
// verified separately by a synced committee chain.
func (u *FinalityUpdate) Validate() error { func (u *FinalityUpdate) Validate() error {
if err := u.Attested.Validate(); err != nil { if err := u.Attested.Validate(); err != nil {
return err return err
@ -186,6 +231,6 @@ func (u *FinalityUpdate) Validate() error {
// finalized execution block. // finalized execution block.
type ChainHeadEvent struct { type ChainHeadEvent struct {
BeaconHead Header BeaconHead Header
Block *types.Block Block *ctypes.Block
Finalized common.Hash Finalized common.Hash
} }