beacon/light: added more comments and func/struct descriptions

This commit is contained in:
Zsolt Felfoldi 2024-01-10 07:03:33 +01:00 committed by Felix Lange
parent 26813d2a6c
commit 7dd8190f63
10 changed files with 173 additions and 47 deletions

View file

@ -26,6 +26,7 @@ import (
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
) )
// ApiServer is a wrapper around BeaconLightApi that implements request.requestServer.
type ApiServer struct { type ApiServer struct {
api *BeaconLightApi api *BeaconLightApi
eventCallback func(event request.Event) eventCallback func(event request.Event)
@ -33,10 +34,12 @@ type ApiServer struct {
lastId uint64 lastId uint64
} }
// NewApiServer creates a new ApiServer.
func NewApiServer(api *BeaconLightApi) *ApiServer { func NewApiServer(api *BeaconLightApi) *ApiServer {
return &ApiServer{api: api} return &ApiServer{api: api}
} }
// Subscribe implements request.requestServer.
func (s *ApiServer) Subscribe(eventCallback func(event request.Event)) { func (s *ApiServer) Subscribe(eventCallback func(event request.Event)) {
s.eventCallback = eventCallback s.eventCallback = eventCallback
s.unsubscribe = s.api.StartHeadListener(func(slot uint64, blockRoot common.Hash) { s.unsubscribe = s.api.StartHeadListener(func(slot uint64, blockRoot common.Hash) {
@ -50,6 +53,7 @@ func (s *ApiServer) Subscribe(eventCallback func(event request.Event)) {
}) })
} }
// SendRequest implements request.requestServer.
func (s *ApiServer) SendRequest(req request.Request) request.ID { func (s *ApiServer) SendRequest(req request.Request) request.ID {
id := request.ID(atomic.AddUint64(&s.lastId, 1)) id := request.ID(atomic.AddUint64(&s.lastId, 1))
go func() { go func() {
@ -86,7 +90,8 @@ func (s *ApiServer) SendRequest(req request.Request) request.ID {
return id return id
} }
// Note: UnsubscribeHeads should not be called concurrently with SubscribeHeads // Unsubscribe implements request.requestServer.
// Note: Unsubscribe should not be called concurrently with Subscribe.
func (s *ApiServer) Unsubscribe() { func (s *ApiServer) Unsubscribe() {
if s.unsubscribe != nil { if s.unsubscribe != nil {
s.unsubscribe() s.unsubscribe()

View file

@ -25,6 +25,9 @@ import (
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
) )
// HeadTracker keeps track of the latest validated head and the "prefetch" head
// which is the (not necessarily validated) head announced by the majority of
// servers.
type HeadTracker struct { type HeadTracker struct {
lock sync.RWMutex lock sync.RWMutex
committeeChain *CommitteeChain committeeChain *CommitteeChain
@ -34,6 +37,7 @@ type HeadTracker struct {
prefetchHead types.HeadInfo prefetchHead types.HeadInfo
} }
// NewHeadTracker creates a new HeadTracker.
func NewHeadTracker(committeeChain *CommitteeChain, minSignerCount int) *HeadTracker { func NewHeadTracker(committeeChain *CommitteeChain, minSignerCount int) *HeadTracker {
return &HeadTracker{ return &HeadTracker{
committeeChain: committeeChain, committeeChain: committeeChain,
@ -41,6 +45,7 @@ func NewHeadTracker(committeeChain *CommitteeChain, minSignerCount int) *HeadTra
} }
} }
// ValidatedHead returns the latest validated head.
func (h *HeadTracker) ValidatedHead() types.SignedHeader { func (h *HeadTracker) ValidatedHead() types.SignedHeader {
h.lock.RLock() h.lock.RLock()
defer h.lock.RUnlock() defer h.lock.RUnlock()
@ -48,6 +53,10 @@ func (h *HeadTracker) ValidatedHead() types.SignedHeader {
return h.signedHead return h.signedHead
} }
// Validate validates the given signed head. If the head is successfully validated
// and it is better than the old validated head (higher slot or same slot and more
// signers) then ValidatedHead is updated. The boolean return flag signals if
// ValidatedHead has been changed.
func (h *HeadTracker) Validate(head types.SignedHeader) (bool, error) { func (h *HeadTracker) Validate(head types.SignedHeader) (bool, error) {
h.lock.Lock() h.lock.Lock()
defer h.lock.Unlock() defer h.lock.Unlock()
@ -76,6 +85,11 @@ func (h *HeadTracker) Validate(head types.SignedHeader) (bool, error) {
return true, nil return true, nil
} }
// PrefetchHead returns the latest known prefetch head's head info.
// This head can be used to start fetching related data hoping that it will be
// validated soon.
// Note that the prefetch head cannot be validated cryptographically so it should
// only be used as a performance optimization hint.
func (h *HeadTracker) PrefetchHead() types.HeadInfo { func (h *HeadTracker) PrefetchHead() types.HeadInfo {
h.lock.RLock() h.lock.RLock()
defer h.lock.RUnlock() defer h.lock.RUnlock()
@ -83,6 +97,9 @@ func (h *HeadTracker) PrefetchHead() types.HeadInfo {
return h.prefetchHead return h.prefetchHead
} }
// SetPrefetchHead sets the prefetch head info.
// Note that HeadTracker does not verify the prefetch head, just acts as a thread
// safe bulletin board.
func (h *HeadTracker) SetPrefetchHead(head types.HeadInfo) { func (h *HeadTracker) SetPrefetchHead(head types.HeadInfo) {
h.lock.Lock() h.lock.Lock()
defer h.lock.Unlock() defer h.lock.Unlock()

View file

@ -31,19 +31,18 @@ import (
// with servers but may keep track of certain parameters of registered servers, // with servers but may keep track of certain parameters of registered servers,
// based on the received server events. These server parameters may affect the // based on the received server events. These server parameters may affect the
// possible range of requests to be sent to a given server. // possible range of requests to be sent to a given server.
// Modules are called by Scheduler whenever a global trigger is fired. All request // Modules are called by Scheduler whenever a global trigger is fired. All events
// and server events fire the trigger. Modules themselves can also self-trigger, // fire the trigger. Modules themselves can also self-trigger, ensuring an
// ensuring an immediate next processing round after the target data structure has // immediate next processing round after the target data structure has been
// been changed in a way that could make further actions possible either by the // changed in a way that could make further actions possible either by the same
// same or another Module. // or another Module.
type Module interface { type Module interface {
// Process is a non-blocking function that is called on each Module whenever // Process is a non-blocking function that is called on each Module whenever
// a processing round is triggered. It can start new requests through the // a processing round is triggered. It can start new requests through the
// received Tracker, process events related to servers and previosly sent // received Tracker, process events and/or do other data processing tasks.
// requests and/or do other data processing tasks. Note that request events // Note that request events are only passed to the module that made the given
// are only passed to the module that made the given request while server // request while server events are passed to every module. Process can also
// events are passed to every module. Process can also trigger a next // trigger a next processing round by returning true.
// processing round by returning true.
// //
// Note: Process functions of different modules are never called concurrently; // Note: Process functions of different modules are never called concurrently;
// they are called by Scheduler in the same order of priority as they were // they are called by Scheduler in the same order of priority as they were
@ -54,7 +53,8 @@ type Module interface {
// Scheduler is a modular network data retrieval framework that coordinates multiple // Scheduler is a modular network data retrieval framework that coordinates multiple
// servers and retrieval mechanisms (modules). It implements a trigger mechanism // servers and retrieval mechanisms (modules). It implements a trigger mechanism
// that calls the Process function of registered modules whenever either the state // that calls the Process function of registered modules whenever either the state
// of existing data structures or connected servers could allow new operations. // of existing data structures or events coming from registered servers could
// allow new operations.
type Scheduler struct { type Scheduler struct {
lock sync.Mutex lock sync.Mutex
clock mclock.Clock clock mclock.Clock
@ -71,8 +71,8 @@ type Scheduler struct {
// testTimerResults []bool // true is appended when simulated timer is processed; false when stopped // testTimerResults []bool // true is appended when simulated timer is processed; false when stopped
} }
// pendingRequest keeps track of sent and not finalized requests and their sender // pendingRequest keeps track of sent and not yet finalized requests and their
// modules and whether a soft timeout has already happened. // sender modules.
type pendingRequest struct { type pendingRequest struct {
request Request request Request
module Module module Module
@ -185,7 +185,7 @@ func (s *Scheduler) syncLoop() {
} }
} }
// processModules runs an entire processing round, calling the process functions // processModules runs an entire processing round, calling the Process functions
// of all modules, passing all relevant events. // of all modules, passing all relevant events.
func (s *Scheduler) processModules() { func (s *Scheduler) processModules() {
s.lock.Lock() s.lock.Lock()
@ -241,7 +241,7 @@ func (s *Scheduler) Trigger() {
} }
} }
// addRequestEvent adds a request event to the sender module's Tracker, ensuring // addRequestEvent adds a request event to the sender module's tracker, ensuring
// that the module receives it in the next processing round. // that the module receives it in the next processing round.
func (s *Scheduler) addRequestEvent(event Event) { func (s *Scheduler) addRequestEvent(event Event) {
sid, _, _ := event.RequestInfo() sid, _, _ := event.RequestInfo()
@ -263,8 +263,8 @@ func (s *Scheduler) addServerEvent(event Event) {
// handleEvent processes an Event and adds it either as a request event or a // handleEvent processes an Event and adds it either as a request event or a
// server event, depending on its type. In case of an EvUnregistered server event // server event, depending on its type. In case of an EvUnregistered server event
// it also closes all pending requests to the given server by emitting a failed // it also closes all pending requests to the given server by emitting a failed
// request event (Finalized without Response), ensuring that all requests get // request event (EvFail), ensuring that all requests get finalized and thereby
// finalized and thereby allowing the module logic to be safe and simple. // allowing the module logic to be safe and simple.
func (s *Scheduler) handleEvent(event Event) { func (s *Scheduler) handleEvent(event Event) {
s.Trigger() s.Trigger()
if event.IsRequestEvent() { if event.IsRequestEvent() {

View file

@ -28,9 +28,9 @@ import (
var ( var (
// request events // request events
EvResponse = &EventType{Name: "response", requestEvent: true} // data: RequestResponse EvResponse = &EventType{Name: "response", requestEvent: true} // data: RequestResponse; sent by requestServer
EvFail = &EventType{Name: "fail", requestEvent: true} // data: RequestResponse EvFail = &EventType{Name: "fail", requestEvent: true} // data: RequestResponse; sent by requestServer
EvTimeout = &EventType{Name: "timeout", requestEvent: true} // data: RequestResponse EvTimeout = &EventType{Name: "timeout", requestEvent: true} // data: RequestResponse; sent by serverWithTimeout
// server events // server events
EvRegistered = &EventType{Name: "registered"} // data: nil; sent by Scheduler EvRegistered = &EventType{Name: "registered"} // data: nil; sent by Scheduler
EvUnregistered = &EventType{Name: "unregistered"} // data: nil; sent by Scheduler EvUnregistered = &EventType{Name: "unregistered"} // data: nil; sent by Scheduler
@ -43,26 +43,30 @@ const (
) )
const ( const (
parallelAdjustUp = 0.1 // serverWithLimits parameters
parallelAdjustDown = 1 parallelAdjustUp = 0.1 // adjust parallelLimit up in case of success under full load
minParallelLimit = 1 parallelAdjustDown = 1 // adjust parallelLimit down in case of timeout/failure
defaultParallelLimit = 3 minParallelLimit = 1 // parallelLimit lower bound
minFailureDelay = time.Millisecond * 100 defaultParallelLimit = 3 // parallelLimit initial value
maxFailureDelay = time.Minute minFailureDelay = time.Millisecond * 100 // minimum disable time in case of request failure
maxFailureDelay = time.Minute // maximum disable time in case of request failure
) )
// requestServer can send a set of requests pre-defined by the application and // requestServer can send requests in a non-blocking way and feed back events
// signal events through the event callback. After each request, it should send // through the event callback. After each request, it should send back either
// back either EvResponse or EvFail. Additionally, it may also send application- // EvResponse or EvFail. Additionally, it may also send application-defined
// defined events that the Modules can interpret. // events that the Modules can interpret.
//
//TODO ?separate request and server events here?
type requestServer interface { type requestServer interface {
Subscribe(eventCallback func(event Event)) Subscribe(eventCallback func(event Event))
SendRequest(request Request) ID SendRequest(request Request) ID
Unsubscribe() Unsubscribe()
} }
// server is implemented by a requestServer wrapped into serverWithTimeout and
// serverWithLimits and is used by Scheduler.
// In addition to requestServer functionality, server can also handle timeouts,
// limit the number of parallel in-flight requests and temporarily disable
// new requests based on timeouts and response failures.
type server interface { type server interface {
subscribe(eventCallback func(event Event)) subscribe(eventCallback func(event Event))
canRequestNow() (bool, float32) canRequestNow() (bool, float32)
@ -71,6 +75,7 @@ type server interface {
unsubscribe() unsubscribe()
} }
// newServer wraps a requestServer and returns a server
func newServer(rs requestServer, clock mclock.Clock) server { func newServer(rs requestServer, clock mclock.Clock) server {
s := &serverWithLimits{} s := &serverWithLimits{}
s.parent = rs s.parent = rs
@ -81,26 +86,36 @@ func newServer(rs requestServer, clock mclock.Clock) server {
type serverSet map[server]struct{} type serverSet map[server]struct{}
// EventType identifies an event type, either related to a request or the server
// in general. Server events can also be externally defined.
type EventType struct { type EventType struct {
Name string Name string
requestEvent bool requestEvent bool // all request events are pre-defined in request package
} }
// Event describes an event where the type of Data depends on Type.
// Server field is not required when sent through the event callback; it is filled
// out when processed by the Scheduler. Note that the Scheduler can also create
// and send events (EvRegistered, EvUnregistered) directly.
type Event struct { type Event struct {
Type *EventType Type *EventType
Server Server // filled by Scheduler Server Server // filled by Scheduler
Data any Data any
} }
// IsRequestEvent returns true if the event is a request event
func (e *Event) IsRequestEvent() bool { func (e *Event) IsRequestEvent() bool {
return e.Type.requestEvent return e.Type.requestEvent
} }
// RequestInfo assumes that the event is a request event and returns its contents
// in a convenient form.
func (e *Event) RequestInfo() (ServerAndID, Request, Response) { func (e *Event) RequestInfo() (ServerAndID, Request, Response) {
data := e.Data.(RequestResponse) data := e.Data.(RequestResponse)
return ServerAndID{Server: e.Server, ID: data.ID}, data.Request, data.Response return ServerAndID{Server: e.Server, ID: data.ID}, data.Request, data.Response
} }
// RequestResponse is the Data type of request events.
type RequestResponse struct { type RequestResponse struct {
ID ID ID ID
Request Request Request Request
@ -120,11 +135,14 @@ type serverWithTimeout struct {
timeouts map[ID]mclock.Timer timeouts map[ID]mclock.Timer
} }
// init initializes serverWithTimeout
func (s *serverWithTimeout) init(clock mclock.Clock) { func (s *serverWithTimeout) init(clock mclock.Clock) {
s.clock = clock s.clock = clock
s.timeouts = make(map[ID]mclock.Timer) s.timeouts = make(map[ID]mclock.Timer)
} }
// subscribe subscribes to events which include parent (requestServer) events
// plus EvTimeout.
func (s *serverWithTimeout) subscribe(eventCallback func(event Event)) { func (s *serverWithTimeout) subscribe(eventCallback func(event Event)) {
s.lock.Lock() s.lock.Lock()
defer s.lock.Unlock() defer s.lock.Unlock()
@ -133,6 +151,7 @@ func (s *serverWithTimeout) subscribe(eventCallback func(event Event)) {
s.parent.Subscribe(s.eventCallback) s.parent.Subscribe(s.eventCallback)
} }
// eventCallback is called by parent (requestServer) event subscription.
func (s *serverWithTimeout) eventCallback(event Event) { func (s *serverWithTimeout) eventCallback(event Event) {
s.lock.Lock() s.lock.Lock()
defer s.lock.Unlock() defer s.lock.Unlock()
@ -152,6 +171,8 @@ func (s *serverWithTimeout) eventCallback(event Event) {
} }
} }
// sendRequest sends a request through the parent (requestServer) and starts a
// timer for request timeout.
func (s *serverWithTimeout) sendRequest(request Request) (reqId ID) { func (s *serverWithTimeout) sendRequest(request Request) (reqId ID) {
s.lock.Lock() s.lock.Lock()
defer s.lock.Unlock() defer s.lock.Unlock()
@ -201,6 +222,7 @@ func (s *serverWithTimeout) unsubscribe() {
s.parent.Unsubscribe() s.parent.Unsubscribe()
} }
// stopTimer stops the given timer
func (s *serverWithTimeout) stopTimer(timer mclock.Timer) { func (s *serverWithTimeout) stopTimer(timer mclock.Timer) {
timer.Stop() timer.Stop()
/*if timer.Stop() && s.scheduler.testTimerResults != nil { /*if timer.Stop() && s.scheduler.testTimerResults != nil {
@ -231,11 +253,14 @@ type serverWithLimits struct {
failureDelay float64 failureDelay float64
} }
// init initializes serverWithLimits
func (s *serverWithLimits) init() { func (s *serverWithLimits) init() {
s.softTimeouts = make(map[ID]struct{}) s.softTimeouts = make(map[ID]struct{})
s.parallelLimit = defaultParallelLimit s.parallelLimit = defaultParallelLimit
} }
// subscribe subscribes to events which include parent (serverWithTimeout) events
// plus EvCanRequstAgain.
func (s *serverWithLimits) subscribe(eventCallback func(event Event)) { func (s *serverWithLimits) subscribe(eventCallback func(event Event)) {
s.lock.Lock() s.lock.Lock()
defer s.lock.Unlock() defer s.lock.Unlock()
@ -244,6 +269,7 @@ func (s *serverWithLimits) subscribe(eventCallback func(event Event)) {
s.serverWithTimeout.subscribe(s.eventCallback) s.serverWithTimeout.subscribe(s.eventCallback)
} }
// eventCallback is called by parent (serverWithTimeout) event subscription.
func (s *serverWithLimits) eventCallback(event Event) { func (s *serverWithLimits) eventCallback(event Event) {
s.lock.Lock() s.lock.Lock()
var sendCanRequestAgain bool var sendCanRequestAgain bool
@ -284,13 +310,13 @@ func (s *serverWithLimits) eventCallback(event Event) {
} }
} }
// sendRequest sends a request through the parent (serverWithTimeout).
func (s *serverWithLimits) sendRequest(request Request) (reqId ID) { func (s *serverWithLimits) sendRequest(request Request) (reqId ID) {
s.lock.Lock() s.lock.Lock()
defer s.lock.Unlock() defer s.lock.Unlock()
s.pendingCount++ s.pendingCount++
id := s.serverWithTimeout.sendRequest(request) return s.serverWithTimeout.sendRequest(request)
return id
} }
// stop stops all goroutines associated with the server. // stop stops all goroutines associated with the server.
@ -306,6 +332,7 @@ func (s *serverWithLimits) unsubscribe() {
s.serverWithTimeout.unsubscribe() s.serverWithTimeout.unsubscribe()
} }
// canRequest checks whether a new request can be started.
func (s *serverWithLimits) canRequest() (bool, float32) { func (s *serverWithLimits) canRequest() (bool, float32) {
if s.delayTimer != nil || s.pendingCount >= int(s.parallelLimit) { if s.delayTimer != nil || s.pendingCount >= int(s.parallelLimit) {
return false, 0 return false, 0
@ -316,7 +343,14 @@ func (s *serverWithLimits) canRequest() (bool, float32) {
return true, -(float32(s.pendingCount) + rand.Float32()) / s.parallelLimit return true, -(float32(s.pendingCount) + rand.Float32()) / s.parallelLimit
} }
// EvCanRequestAgain guaranteed if it returns false // canRequestNow checks whether a new request can be started, according to the
// current in-flight request count and parallelLimit, and also the failure delay
// timer.
// If a new request is allowed then it also returns a priority value that can be
// used to select the least overloaded server from an otherwise equally suitable
// set of servers.
// If it returns false then it is guaranteed that an EvCanRequestAgain will be
// sent whenever the server becomes available for requesting again.
func (s *serverWithLimits) canRequestNow() (bool, float32) { func (s *serverWithLimits) canRequestNow() (bool, float32) {
var sendCanRequestAgain bool var sendCanRequestAgain bool
s.lock.Lock() s.lock.Lock()
@ -333,6 +367,8 @@ func (s *serverWithLimits) canRequestNow() (bool, float32) {
return canRequest, priority return canRequest, priority
} }
// delay sets the delay timer to the given duration, disabling new requests for
// the given period.
func (s *serverWithLimits) delay(delay time.Duration) { func (s *serverWithLimits) delay(delay time.Duration) {
if s.delayTimer != nil { if s.delayTimer != nil {
// Note: if stopping the timer is unsuccessful then the resulting AfterFunc // Note: if stopping the timer is unsuccessful then the resulting AfterFunc
@ -366,6 +402,8 @@ func (s *serverWithLimits) delay(delay time.Duration) {
}) })
} }
// fail reports that a response from the server was found invalid by the processing
// Module, disabling new requests for a dynamically adjused time period.
func (s *serverWithLimits) fail(desc string) { func (s *serverWithLimits) fail(desc string) {
s.lock.Lock() s.lock.Lock()
defer s.lock.Unlock() defer s.lock.Unlock()
@ -373,6 +411,7 @@ func (s *serverWithLimits) fail(desc string) {
s.failLocked(desc) s.failLocked(desc)
} }
// failLocked calculates the dynamic failure delay and applies it.
func (s *serverWithLimits) failLocked(desc string) { func (s *serverWithLimits) failLocked(desc string) {
log.Debug("Server error", "description", desc) log.Debug("Server error", "description", desc)
s.failureDelay *= 2 s.failureDelay *= 2

View file

@ -23,6 +23,11 @@ import (
) )
type ( type (
// Server identifies a server without allowing any direct interaction.
// Note: server interface is used by Scheduler and Tracker but not used by
// the modules that do not interact with them directly.
// In order to make module testing easier, Server interface is used in
// events and modules.
Server any Server any
Request any Request any
Response any Response any
@ -60,14 +65,20 @@ type Tracker interface {
InvalidResponse(id ServerAndID, desc string) InvalidResponse(id ServerAndID, desc string)
} }
// one per sync process // tracker implements Tracker. A separate instance is created for each Module.
type tracker struct { type tracker struct {
servers serverSet // one per trigger // servers is a set of currently available servers; it is recreated at every
// processModule round.
servers serverSet
scheduler *Scheduler scheduler *Scheduler
module Module module Module
// requestEvents is a list of events related to requests sent by the given
// module. It is reset before module processing and the previous contents are
// passed to Module.Process along with the globally collected server events.
requestEvents []Event requestEvents []Event
} }
// TryRequest implements Tracker.
func (p *tracker) TryRequest(requestFn func(server Server) (Request, float32)) (RequestWithID, bool) { func (p *tracker) TryRequest(requestFn func(server Server) (Request, float32)) (RequestWithID, bool) {
var ( var (
maxServerPriority, maxRequestPriority float32 maxServerPriority, maxRequestPriority float32
@ -104,6 +115,7 @@ func (p *tracker) TryRequest(requestFn func(server Server) (Request, float32)) (
return RequestWithID{ServerAndID: id, Request: bestRequest}, true return RequestWithID{ServerAndID: id, Request: bestRequest}, true
} }
// InvalidResponse implements Tracker.
func (p *tracker) InvalidResponse(id ServerAndID, desc string) { func (p *tracker) InvalidResponse(id ServerAndID, desc string) {
id.Server.(server).fail(desc) id.Server.(server).fail(desc)
} }

View file

@ -28,6 +28,11 @@ type headTracker interface {
SetPrefetchHead(head types.HeadInfo) SetPrefetchHead(head types.HeadInfo)
} }
// HeadSync implements request.Module; it updates the validated and prefetch
// heads of HeadTracker based on the EvHead and EvSignedHead events coming from
// registered servers.
// 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.
type HeadSync struct { type HeadSync struct {
headTracker headTracker headTracker headTracker
chain committeeChain chain committeeChain
@ -40,11 +45,16 @@ type HeadSync struct {
prefetchHead types.HeadInfo prefetchHead types.HeadInfo
} }
// headServerCount is associated with most recently seen head infos; it counts
// the number of servers currently having the given head info as their announced
// head and a counter signaling how recent that head is.
// This data is used for selecting the prefetch head.
type headServerCount struct { type headServerCount struct {
serverCount int serverCount int
headCounter uint64 headCounter uint64
} }
// 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,
@ -85,6 +95,8 @@ func (s *HeadSync) Process(tracker request.Tracker, events []request.Event) (tri
return return
} }
// newSignedHead handles received signed head; either validates it if the chain
// is properly synced or stores it for further validation.
func (s *HeadSync) newSignedHead(server request.Server, signedHead types.SignedHeader) (trigger bool) { func (s *HeadSync) newSignedHead(server request.Server, signedHead types.SignedHeader) (trigger bool) {
if !s.chainInit || types.SyncPeriod(signedHead.SignatureSlot) > s.nextSyncPeriod { if !s.chainInit || types.SyncPeriod(signedHead.SignatureSlot) > s.nextSyncPeriod {
s.unvalidatedHeads[server] = signedHead s.unvalidatedHeads[server] = signedHead
@ -94,6 +106,8 @@ func (s *HeadSync) newSignedHead(server request.Server, signedHead types.SignedH
return updated return updated
} }
// processUnvalidatedHeads iterates the list of unvalidated heads and validates
// those which can be validated.
func (s *HeadSync) processUnvalidatedHeads() (trigger bool) { func (s *HeadSync) processUnvalidatedHeads() (trigger bool) {
if !s.chainInit { if !s.chainInit {
return false return false

View file

@ -26,7 +26,7 @@ import (
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
) )
const maxUpdateRequest = 8 const maxUpdateRequest = 8 // maximum number of updates requested in a single request
type committeeChain interface { type committeeChain interface {
CheckpointInit(bootstrap types.BootstrapData) error CheckpointInit(bootstrap types.BootstrapData) error
@ -34,6 +34,9 @@ type committeeChain interface {
NextSyncPeriod() (uint64, bool) NextSyncPeriod() (uint64, bool)
} }
// CheckpointInit implements request.Module; it fetches the light client bootstrap
// data belonging to the given checkpoint hash and initializes the committee chain
// if successful.
type CheckpointInit struct { type CheckpointInit struct {
chain committeeChain chain committeeChain
checkpointHash common.Hash checkpointHash common.Hash
@ -41,6 +44,7 @@ type CheckpointInit struct {
initialized bool initialized bool
} }
// NewCheckpointInit creates a new CheckpointInit.
func NewCheckpointInit(chain committeeChain, checkpointHash common.Hash) *CheckpointInit { func NewCheckpointInit(chain committeeChain, checkpointHash common.Hash) *CheckpointInit {
return &CheckpointInit{ return &CheckpointInit{
chain: chain, chain: chain,
@ -78,6 +82,12 @@ func (s *CheckpointInit) Process(tracker request.Tracker, events []request.Event
return false return false
} }
// ForwardUpdateSync implements request.Module; it fetches updates between the
// committee chain head and each server's announced head. Updates are fetched
// in batches and multiple batches can also be requested in parallel.
// Out of order responses are also handled; if a batch of updates cannot be added
// to the chain immediately because of a gap then the future updates are
// remembered until they can be processed.
type ForwardUpdateSync struct { type ForwardUpdateSync struct {
chain committeeChain chain committeeChain
rangeLock rangeLock rangeLock rangeLock
@ -86,6 +96,7 @@ type ForwardUpdateSync struct {
nextSyncPeriod map[request.Server]uint64 nextSyncPeriod map[request.Server]uint64
} }
// NewForwardUpdateSync creates a new ForwardUpdateSync.
func NewForwardUpdateSync(chain committeeChain) *ForwardUpdateSync { func NewForwardUpdateSync(chain committeeChain) *ForwardUpdateSync {
return &ForwardUpdateSync{ return &ForwardUpdateSync{
chain: chain, chain: chain,
@ -95,8 +106,12 @@ func NewForwardUpdateSync(chain committeeChain) *ForwardUpdateSync {
} }
} }
// rangeLock allows locking sections of an integer space, preventing the syncing
// mechanism from making requests again for sections where a not timed out request
// is already pending or where already fetched and unprocessed data is available.
type rangeLock map[uint64]int type rangeLock map[uint64]int
// lock locks or unlocks the given section, depending on the sign of the add parameter.
func (r rangeLock) lock(first, count uint64, add int) { func (r rangeLock) lock(first, count uint64, add int) {
for i := first; i < first+count; i++ { for i := first; i < first+count; i++ {
if v := r[i] + add; v > 0 { if v := r[i] + add; v > 0 {
@ -107,6 +122,8 @@ func (r rangeLock) lock(first, count uint64, add int) {
} }
} }
// firstUnlocked returns the first unlocked section starting at or after start
// and not longer than maxCount.
func (r rangeLock) firstUnlocked(start, maxCount uint64) (first, count uint64) { func (r rangeLock) firstUnlocked(start, maxCount uint64) (first, count uint64) {
first = start first = start
for { for {
@ -127,6 +144,8 @@ func (r rangeLock) firstUnlocked(start, maxCount uint64) (first, count uint64) {
return return
} }
// lockRange locks the range belonging to the given update request, unless the
// same request has already been locked
func (s *ForwardUpdateSync) lockRange(sid request.ServerAndID, req request.Request) { func (s *ForwardUpdateSync) lockRange(sid request.ServerAndID, req request.Request) {
if _, ok := s.lockedIDs[sid]; ok { if _, ok := s.lockedIDs[sid]; ok {
return return
@ -136,6 +155,8 @@ func (s *ForwardUpdateSync) lockRange(sid request.ServerAndID, req request.Reque
s.rangeLock.lock(r.FirstPeriod, r.Count, 1) s.rangeLock.lock(r.FirstPeriod, r.Count, 1)
} }
// unlockRange unlocks the range belonging to the given update request, unless
// same request has already been unlocked
func (s *ForwardUpdateSync) unlockRange(sid request.ServerAndID, req request.Request) { func (s *ForwardUpdateSync) unlockRange(sid request.ServerAndID, req request.Request) {
if _, ok := s.lockedIDs[sid]; !ok { if _, ok := s.lockedIDs[sid]; !ok {
return return
@ -145,6 +166,8 @@ func (s *ForwardUpdateSync) unlockRange(sid request.ServerAndID, req request.Req
s.rangeLock.lock(r.FirstPeriod, r.Count, -1) s.rangeLock.lock(r.FirstPeriod, r.Count, -1)
} }
// verifyRange returns true if the number of updates and the individual update
// periods in the response match the requested section.
func (s *ForwardUpdateSync) verifyRange(req request.Request, resp request.Response) bool { func (s *ForwardUpdateSync) verifyRange(req request.Request, resp request.Response) bool {
request, ok := req.(ReqUpdates) request, ok := req.(ReqUpdates)
if !ok { if !ok {
@ -165,7 +188,8 @@ func (s *ForwardUpdateSync) verifyRange(req request.Request, resp request.Respon
return true return true
} }
// returns true for partial success // processResponse adds the fetched updates and committees to the committee chain.
// Returns true in case of full or partial success.
func (s *ForwardUpdateSync) processResponse(tracker request.Tracker, event request.Event) (success bool) { func (s *ForwardUpdateSync) processResponse(tracker request.Tracker, event request.Event) (success bool) {
sid, _, resp := event.RequestInfo() sid, _, resp := event.RequestInfo()
response, ok := resp.(RespUpdates) response, ok := resp.(RespUpdates)
@ -191,6 +215,7 @@ func (s *ForwardUpdateSync) processResponse(tracker request.Tracker, event reque
return return
} }
// updateResponseList implements sort.Sort and sorts update request/response events by FirstPeriod.
type updateResponseList []request.Event type updateResponseList []request.Event
func (u updateResponseList) Len() int { return len(u) } func (u updateResponseList) Len() int { return len(u) }

View file

@ -37,6 +37,8 @@ import (
"github.com/protolambda/ztyp/tree" "github.com/protolambda/ztyp/tree"
) )
// beaconBlockSync implements request.Module; it fetches the beacon blocks belonging
// to the validated and prefetch heads.
type beaconBlockSync struct { type beaconBlockSync struct {
recentBlocks *lru.Cache[common.Hash, *capella.BeaconBlock] recentBlocks *lru.Cache[common.Hash, *capella.BeaconBlock]
validatedHead common.Hash validatedHead common.Hash
@ -50,7 +52,8 @@ type headTracker interface {
ValidatedHead() types.SignedHeader ValidatedHead() types.SignedHeader
} }
func newBeaconBlockSyncer(headTracker headTracker) *beaconBlockSync { // newBeaconBlockSync returns a new beaconBlockSync.
func newBeaconBlockSync(headTracker headTracker) *beaconBlockSync {
return &beaconBlockSync{ return &beaconBlockSync{
headTracker: headTracker, headTracker: headTracker,
recentBlocks: lru.NewCache[common.Hash, *capella.BeaconBlock](10), recentBlocks: lru.NewCache[common.Hash, *capella.BeaconBlock](10),
@ -96,12 +99,18 @@ func (s *beaconBlockSync) Process(tracker request.Tracker, events []request.Even
return return
} }
// belongs to validatedHead (or nil) // getHeadBlock returns the beacon block belonging to ValidatedHead or nil if not available.
func (s *beaconBlockSync) getHeadBlock() *capella.BeaconBlock { func (s *beaconBlockSync) getHeadBlock() *capella.BeaconBlock {
block, _ := s.recentBlocks.Get(s.validatedHead) block, _ := s.recentBlocks.Get(s.validatedHead)
return block return block
} }
// tryRequestBlock tries to send a block request for the given root if the block
// is not available and the root is not locked by another pending request.
// If prefetch is true then the request is only sent to a server whose latest
// announced head has the same block root. If prefetch is false then a validated
// block is requested which is expected to be available at every properly synced
// server, therefore no such restriction is applied.
func (s *beaconBlockSync) tryRequestBlock(tracker request.Tracker, blockRoot common.Hash, prefetch bool) { func (s *beaconBlockSync) tryRequestBlock(tracker request.Tracker, blockRoot common.Hash, prefetch bool) {
if _, ok := s.recentBlocks.Get(blockRoot); ok { if _, ok := s.recentBlocks.Get(blockRoot); ok {
return return
@ -121,6 +130,7 @@ func (s *beaconBlockSync) tryRequestBlock(tracker request.Tracker, blockRoot com
} }
} }
// getExecBlock extracts the execution block from the beacon block's payload.
func getExecBlock(beaconBlock *capella.BeaconBlock) (*ctypes.Block, error) { func getExecBlock(beaconBlock *capella.BeaconBlock) (*ctypes.Block, error) {
payload := &beaconBlock.Body.ExecutionPayload payload := &beaconBlock.Body.ExecutionPayload
txs := make([]*ctypes.Transaction, len(payload.Transactions)) txs := make([]*ctypes.Transaction, len(payload.Transactions))
@ -167,10 +177,14 @@ func getExecBlock(beaconBlock *capella.BeaconBlock) (*ctypes.Block, error) {
return execBlock, nil return execBlock, nil
} }
// beaconBlockHash calculates the hash of a beacon block.
func beaconBlockHash(beaconBlock *capella.BeaconBlock) common.Hash { func beaconBlockHash(beaconBlock *capella.BeaconBlock) common.Hash {
return common.Hash(beaconBlock.HashTreeRoot(configs.Mainnet, tree.GetHashFn())) return common.Hash(beaconBlock.HashTreeRoot(configs.Mainnet, tree.GetHashFn()))
} }
// engineApiUpdater implements request.Module. This module does not start requests,
// it is only implemented as a module in order to easily trigger it by successful
// head block retrieval.
type engineApiUpdater struct { type engineApiUpdater struct {
client *rpc.Client client *rpc.Client
trigger func() trigger func()

View file

@ -41,7 +41,7 @@ func TestBlockSync(t *testing.T) {
tracker.AddServer(testServer1, 1) tracker.AddServer(testServer1, 1)
tracker.AddServer(testServer2, 1) tracker.AddServer(testServer2, 1)
ht := &testHeadTracker{} ht := &testHeadTracker{}
blockSync := newBeaconBlockSyncer(ht) blockSync := newBeaconBlockSync(ht)
expHeadBlock := func(tci int, expHead *capella.BeaconBlock) { expHeadBlock := func(tci int, expHead *capella.BeaconBlock) {
expInfo := blockHeadInfo(expHead) expInfo := blockHeadInfo(expHead)

View file

@ -125,7 +125,7 @@ func blsync(ctx *cli.Context) error {
checkpointInit := sync.NewCheckpointInit(committeeChain, chainConfig.Checkpoint) checkpointInit := sync.NewCheckpointInit(committeeChain, chainConfig.Checkpoint)
forwardSync := sync.NewForwardUpdateSync(committeeChain) forwardSync := sync.NewForwardUpdateSync(committeeChain)
beaconBlockSync := newBeaconBlockSyncer(headTracker) beaconBlockSync := newBeaconBlockSync(headTracker)
engineApiUpdater := &engineApiUpdater{ //TODO constructor engineApiUpdater := &engineApiUpdater{ //TODO constructor
client: makeRPCClient(ctx), client: makeRPCClient(ctx),
blockSync: beaconBlockSync, blockSync: beaconBlockSync,