beacon/light: new Module and Requester interfaces

This commit is contained in:
Zsolt Felfoldi 2024-01-29 02:46:24 +01:00 committed by Felix Lange
parent 084f23cfa0
commit c249d129b6
9 changed files with 316 additions and 346 deletions

View file

@ -17,7 +17,6 @@
package request package request
import ( import (
"math"
"sync" "sync"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
@ -45,19 +44,13 @@ type Module interface {
// 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
// registered in. // registered in.
Process([]Event) Process(Requester, []Event)
// MakeRequest generates a request candidate based on the state of the target }
// structure(s) and the internal state of the module. This candidate is
// typically the next obtainable item (or range of items) of the target type Requester interface {
// structure that is assumed to be available at the given server and has not CanSendTo() []Server
// been requested yet (or has been requested but already timed out and should Send(Server, Request) ID
// be resent). Fail(Server, string)
// MakeRequest is always called after Process. Note that it is the Scheduler's
// job to select the best possible requests and actually send them. If a
// request has been sent, the module is notified through an EvRequest event
// which also immediately triggers a next processing round, allowing modules
// to send more requests if possible and necessary.
MakeRequest(Server) (Request, float32)
} }
// Scheduler is a modular network data retrieval framework that coordinates multiple // Scheduler is a modular network data retrieval framework that coordinates multiple
@ -72,7 +65,10 @@ type Scheduler struct {
servers map[server]struct{} servers map[server]struct{}
targets map[targetData]uint64 targets map[targetData]uint64
pending map[ServerAndID]pendingRequest requesterLock sync.RWMutex
serverOrder []server
pending map[ServerAndID]pendingRequest
// eventLock guards access to the events list. Note that eventLock can be // eventLock guards access to the events list. Note that eventLock can be
// locked either while lock is locked or unlocked but lock cannot be locked // locked either while lock is locked or unlocked but lock cannot be locked
// while eventLock is locked. // while eventLock is locked.
@ -87,15 +83,12 @@ type Scheduler struct {
} }
type ( type (
// Server identifies a server without allowing any direct interaction except // Server identifies a server without allowing any direct interaction.
// for reporting a server side failure.
// Note: server interface is used by Scheduler and Tracker but not used by // Note: server interface is used by Scheduler and Tracker but not used by
// the modules that do not interact with them directly. // the modules that do not interact with them directly.
// In order to make module testing easier, Server interface is used in // In order to make module testing easier, Server interface is used in
// events and modules. // events and modules.
Server interface { Server any
Fail(desc string)
}
Request any Request any
Response any Response any
ID uint64 ID uint64
@ -238,91 +231,16 @@ func (s *Scheduler) targetChanged() (changed bool) {
// requests are generated and sent if necessary and possible. // requests are generated and sent if necessary and possible.
func (s *Scheduler) processRound() { func (s *Scheduler) processRound() {
for { for {
filteredEvents := s.filterEvents()
log.Debug("Processing modules") log.Debug("Processing modules")
filteredEvents := s.filterEvents()
for _, module := range s.modules { for _, module := range s.modules {
log.Debug("Processing module", "name", s.names[module], "events", len(filteredEvents[module])) log.Debug("Processing module", "name", s.names[module], "events", len(filteredEvents[module]))
module.Process(filteredEvents[module]) module.Process(requester{s, module}, filteredEvents[module])
} }
if !s.targetChanged() { if !s.targetChanged() {
break break
} }
} }
s.sendRequests()
}
// sendRequests lets each module generate a request if necessary and sends it to
// a suitable server if possible.
// Note that if a request is sent, an EvRequest event will immediately trigger a
// next processing round, thereby allowing modules to create any number of requests
// in any suitable moment as long as there is a server that can accept them.
func (s *Scheduler) sendRequests() {
servers := make(map[server]struct{})
for server := range s.servers {
if ok, _ := server.canRequestNow(); ok {
servers[server] = struct{}{}
}
}
log.Debug("Generating request candidates", "servers", len(servers))
for _, module := range s.modules {
if len(servers) == 0 {
return
}
if s.tryRequest(module, servers) {
log.Debug("Sent request", "module", s.names[module])
}
}
}
// tryRequest tries to generate request candidates for a given module and a given
// set of servers, then selects the best candidate if there is one and sends the
// request to the server it was generated for.
// The candidates are primarily ranked based on "request priority", a number that
// Module.MakeRequest has returned along with the request candidate. This ranking
// may or may not be used depending on the type of the request, identical requests
// typically have the same priority while multiple item requests may have a
// priority based on the number of items requested.
// If there are multiple candidates with identical request priority then they are
// ranked based on "server priority" which is determined by the server. This value
// is typically higher is the server is expected to respond quicker or with a
// higher chance (typically a lower number of pending requests).
// Note that tryRequest can also remove items from the set of available servers
// if they are no longer able to accept requests in the current processing round.
func (s *Scheduler) tryRequest(module Module, servers map[server]struct{}) bool {
var (
maxServerPriority, maxRequestPriority float32
bestServer server
bestRequest Request
)
maxServerPriority, maxRequestPriority = -math.MaxFloat32, -math.MaxFloat32
serverCount := len(servers)
var removed, candidates int
for server := range servers {
canRequest, serverPriority := server.canRequestNow()
if !canRequest {
delete(servers, server)
removed++
continue
}
request, requestPriority := module.MakeRequest(server)
if request != nil {
candidates++
}
if request == nil || requestPriority < maxRequestPriority ||
(requestPriority == maxRequestPriority && serverPriority <= maxServerPriority) {
continue
}
maxServerPriority, maxRequestPriority = serverPriority, requestPriority
bestServer, bestRequest = server, request
}
log.Debug("Request attempt", "serverCount", serverCount, "removedServers", removed, "requestCandidates", candidates)
if bestServer == nil {
return false
}
sid := ServerAndID{Server: bestServer, ID: bestServer.sendRequest(bestRequest)}
s.pending[sid] = pendingRequest{request: bestRequest, module: module}
return true
} }
// Trigger starts a new processing round. If fired during processing, it ensures // Trigger starts a new processing round. If fired during processing, it ensures
@ -358,6 +276,9 @@ func (s *Scheduler) filterEvents() map[Module][]Event {
s.events = nil s.events = nil
s.eventLock.Unlock() s.eventLock.Unlock()
s.requesterLock.Lock()
defer s.requesterLock.Unlock()
filteredEvents := make(map[Module][]Event) filteredEvents := make(map[Module][]Event)
for _, event := range events { for _, event := range events {
server := event.Server.(server) server := event.Server.(server)
@ -379,9 +300,19 @@ func (s *Scheduler) filterEvents() map[Module][]Event {
switch event.Type { switch event.Type {
case EvRegistered: case EvRegistered:
s.servers[server] = struct{}{} s.servers[server] = struct{}{}
s.serverOrder = append(s.serverOrder, nil)
copy(s.serverOrder[1:], s.serverOrder[:len(s.serverOrder)-1])
s.serverOrder[0] = server
case EvUnregistered: case EvUnregistered:
s.closePending(event.Server, filteredEvents) s.closePending(event.Server, filteredEvents)
delete(s.servers, server) delete(s.servers, server)
for i, srv := range s.serverOrder {
if srv == server {
copy(s.serverOrder[i:len(s.serverOrder)-1], s.serverOrder[i+1:])
s.serverOrder = s.serverOrder[:len(s.serverOrder)-1]
break
}
}
} }
for _, module := range s.modules { for _, module := range s.modules {
filteredEvents[module] = append(filteredEvents[module], event) filteredEvents[module] = append(filteredEvents[module], event)
@ -408,3 +339,44 @@ func (s *Scheduler) closePending(server Server, filteredEvents map[Module][]Even
} }
} }
} }
type requester struct {
*Scheduler
module Module
}
func (s requester) CanSendTo() []Server {
s.requesterLock.RLock()
defer s.requesterLock.RUnlock()
list := make([]Server, 0, len(s.serverOrder))
for _, server := range s.serverOrder {
if server.canRequestNow() {
list = append(list, server)
}
}
return list
}
func (s requester) Send(srv Server, req Request) ID {
s.requesterLock.Lock()
defer s.requesterLock.Unlock()
server := srv.(server)
id := server.sendRequest(req)
sid := ServerAndID{Server: srv, ID: id}
s.pending[sid] = pendingRequest{request: req, module: s.module}
for i, ss := range s.serverOrder {
if ss == server {
copy(s.serverOrder[i:len(s.serverOrder)-1], s.serverOrder[i+1:])
s.serverOrder[len(s.serverOrder)-1] = server
return id
}
}
log.Error("Target server not found in ordered list of registered servers")
return id
}
func (s requester) Fail(srv Server, desc string) {
srv.(server).fail(desc)
}

View file

@ -18,7 +18,6 @@ package request
import ( import (
"math" "math"
"math/rand"
"sync" "sync"
"time" "time"
@ -28,7 +27,6 @@ import (
var ( var (
// request events // request events
EvRequest = &EventType{Name: "request", requestEvent: true} // data: RequestResponse; sent by Scheduler
EvResponse = &EventType{Name: "response", requestEvent: true} // data: RequestResponse; sent by requestServer EvResponse = &EventType{Name: "response", requestEvent: true} // data: RequestResponse; sent by requestServer
EvFail = &EventType{Name: "fail", requestEvent: true} // data: RequestResponse; sent by requestServer EvFail = &EventType{Name: "fail", requestEvent: true} // data: RequestResponse; sent by requestServer
EvTimeout = &EventType{Name: "timeout", requestEvent: true} // data: RequestResponse; sent by serverWithTimeout EvTimeout = &EventType{Name: "timeout", requestEvent: true} // data: RequestResponse; sent by serverWithTimeout
@ -70,9 +68,9 @@ type requestServer interface {
// new requests based on timeouts and response failures. // new requests based on timeouts and response failures.
type server interface { type server interface {
subscribe(eventCallback func(Event)) subscribe(eventCallback func(Event))
canRequestNow() (bool, float32) canRequestNow() bool
sendRequest(Request) ID sendRequest(Request) ID
Fail(string) fail(string)
unsubscribe() unsubscribe()
} }
@ -121,7 +119,7 @@ type RequestResponse struct {
Response Response Response Response
} }
// serverWithTimeout wraps a requestServer and introduces two new request event //TODO serverWithTimeout wraps a requestServer and introduces two new request event
// types: EvRequest and EvTimeout. Whenever a request is successfully sent, an // types: EvRequest and EvTimeout. Whenever a request is successfully sent, an
// EvRequest event is emitted first. The request's lifecycle is concluded if // EvRequest event is emitted first. The request's lifecycle is concluded if
// EvResponse or EvFail emitted by the parent requestServer. If this does not // EvResponse or EvFail emitted by the parent requestServer. If this does not
@ -161,9 +159,7 @@ func (s *serverWithTimeout) sendRequest(request Request) (reqId ID) {
s.lock.Lock() s.lock.Lock()
s.lastID++ s.lastID++
id := s.lastID id := s.lastID
reqData := RequestResponse{ID: id, Request: request} s.startTimeout(RequestResponse{ID: id, Request: request})
s.childEventCb(Event{Type: EvRequest, Data: reqData})
s.startTimeout(reqData)
s.lock.Unlock() s.lock.Unlock()
s.parent.SendRequest(id, request) s.parent.SendRequest(id, request)
return id return id
@ -293,12 +289,12 @@ func (s *serverWithLimits) eventCallback(event Event) {
s.parallelLimit += parallelAdjustUp s.parallelLimit += parallelAdjustUp
} }
s.pendingCount-- s.pendingCount--
if canRequest, _ := s.canRequest(); canRequest { if s.canRequest() {
sendCanRequestAgain = s.sendEvent sendCanRequestAgain = s.sendEvent
s.sendEvent = false s.sendEvent = false
} }
if event.Type == EvFail { if event.Type == EvFail {
s.fail("failed request") s.failLocked("failed request")
} }
} }
childEventCb := s.childEventCb childEventCb := s.childEventCb
@ -331,14 +327,14 @@ func (s *serverWithLimits) unsubscribe() {
} }
// canRequest checks whether a new request can be started. // canRequest checks whether a new request can be started.
func (s *serverWithLimits) canRequest() (bool, float32) { func (s *serverWithLimits) canRequest() bool {
if s.delayTimer != nil || s.pendingCount >= int(s.parallelLimit) || s.timeoutCount > 0 { if s.delayTimer != nil || s.pendingCount >= int(s.parallelLimit) || s.timeoutCount > 0 {
return false, 0 return false
} }
if s.parallelLimit < minParallelLimit { if s.parallelLimit < minParallelLimit {
s.parallelLimit = minParallelLimit s.parallelLimit = minParallelLimit
} }
return true, -(float32(s.pendingCount) + rand.Float32()) / s.parallelLimit return true
} }
// canRequestNow checks whether a new request can be started, according to the // canRequestNow checks whether a new request can be started, according to the
@ -349,10 +345,10 @@ func (s *serverWithLimits) canRequest() (bool, float32) {
// set of servers. // set of servers.
// If it returns false then it is guaranteed that an EvCanRequestAgain will be // If it returns false then it is guaranteed that an EvCanRequestAgain will be
// sent whenever the server becomes available for requesting again. // sent whenever the server becomes available for requesting again.
func (s *serverWithLimits) canRequestNow() (bool, float32) { func (s *serverWithLimits) canRequestNow() bool {
var sendCanRequestAgain bool var sendCanRequestAgain bool
s.lock.Lock() s.lock.Lock()
canRequest, priority := s.canRequest() canRequest := s.canRequest()
if canRequest { if canRequest {
sendCanRequestAgain = s.sendEvent sendCanRequestAgain = s.sendEvent
s.sendEvent = false s.sendEvent = false
@ -362,7 +358,7 @@ func (s *serverWithLimits) canRequestNow() (bool, float32) {
if sendCanRequestAgain { if sendCanRequestAgain {
childEventCb(Event{Type: EvCanRequestAgain}) childEventCb(Event{Type: EvCanRequestAgain})
} }
return canRequest, priority return canRequest
} }
// delay sets the delay timer to the given duration, disabling new requests for // delay sets the delay timer to the given duration, disabling new requests for
@ -384,7 +380,7 @@ func (s *serverWithLimits) delay(delay time.Duration) {
s.lock.Lock() s.lock.Lock()
if s.delayTimer != nil && s.delayCounter == delayCounter { // do nothing if there is a new timer now if s.delayTimer != nil && s.delayCounter == delayCounter { // do nothing if there is a new timer now
s.delayTimer = nil s.delayTimer = nil
if canRequest, _ := s.canRequest(); canRequest { if s.canRequest() {
sendCanRequestAgain = s.sendEvent sendCanRequestAgain = s.sendEvent
s.sendEvent = false s.sendEvent = false
} }
@ -399,15 +395,15 @@ func (s *serverWithLimits) delay(delay time.Duration) {
// fail reports that a response from the server was found invalid by the processing // fail reports that a response from the server was found invalid by the processing
// Module, disabling new requests for a dynamically adjused time period. // 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()
s.fail(desc) s.failLocked(desc)
} }
// fail calculates the dynamic failure delay and applies it. // failLocked calculates the dynamic failure delay and applies it.
func (s *serverWithLimits) fail(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
now := s.clock.Now() now := s.clock.Now()

View file

@ -67,7 +67,7 @@ func NewHeadSync(headTracker headTracker, chain committeeChain) *HeadSync {
return s return s
} }
func (s *HeadSync) Process(events []request.Event) { func (s *HeadSync) Process(requester request.Requester, events []request.Event) {
for _, event := range events { for _, event := range events {
switch event.Type { switch event.Type {
case EvNewHead: case EvNewHead:
@ -90,10 +90,6 @@ func (s *HeadSync) Process(events []request.Event) {
} }
} }
func (s *HeadSync) MakeRequest(server request.Server) (request.Request, float32) {
return nil, 0
}
// newSignedHead handles received signed head; either validates it if the chain // newSignedHead handles received signed head; either validates it if the chain
// is properly synced or stores it for further validation. // is properly synced or stores it for further validation.
func (s *HeadSync) newSignedHead(server request.Server, signedHead types.SignedHeader) { func (s *HeadSync) newSignedHead(server request.Server, signedHead types.SignedHeader) {

View file

@ -24,10 +24,10 @@ import (
) )
var ( var (
testServer1 = &TestServer{ID: 1} testServer1 = "testServer1"
testServer2 = &TestServer{ID: 2} testServer2 = "testServer2"
testServer3 = &TestServer{ID: 3} testServer3 = "testServer3"
testServer4 = &TestServer{ID: 4} testServer4 = "testServer4"
testHead0 = types.HeadInfo{} testHead0 = types.HeadInfo{}
testHead1 = types.HeadInfo{Slot: 123, BlockRoot: common.Hash{1}} testHead1 = types.HeadInfo{Slot: 123, BlockRoot: common.Hash{1}}
@ -52,12 +52,12 @@ func TestValidatedHead(t *testing.T) {
ts.AddServer(testServer1, 1) ts.AddServer(testServer1, 1)
ts.ServerEvent(EvNewSignedHead, testServer1, testSHead1) ts.ServerEvent(EvNewSignedHead, testServer1, testSHead1)
ts.Run(1, nil, nil) 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)
chain.SetNextSyncPeriod(0) // initialize chain chain.SetNextSyncPeriod(0) // initialize chain
ts.Run(2, nil, nil) 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.SignedHeader{testSHead1})
@ -65,34 +65,34 @@ func TestValidatedHead(t *testing.T) {
ts.ServerEvent(EvNewSignedHead, testServer1, testSHead2) ts.ServerEvent(EvNewSignedHead, testServer1, testSHead2)
ts.AddServer(testServer2, 1) ts.AddServer(testServer2, 1)
ts.ServerEvent(EvNewSignedHead, testServer2, testSHead2) ts.ServerEvent(EvNewSignedHead, testServer2, testSHead2)
ts.Run(3, nil, nil) 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.SignedHeader{testSHead2, testSHead2})
ts.ServerEvent(EvNewSignedHead, testServer1, testSHead3) ts.ServerEvent(EvNewSignedHead, testServer1, testSHead3)
ts.AddServer(testServer3, 1) ts.AddServer(testServer3, 1)
ts.ServerEvent(EvNewSignedHead, testServer3, testSHead4) ts.ServerEvent(EvNewSignedHead, testServer3, testSHead4)
ts.Run(4, nil, nil) ts.Run(4)
// future period annonced 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, nil, nil) ts.Run(5)
// testSHead3 can be validated now but not testSHead4 // testSHead3 can be validated now but not testSHead4
ht.ExpValidated(t, 5, []types.SignedHeader{testSHead3}) ht.ExpValidated(t, 5, []types.SignedHeader{testSHead3})
// 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)
ts.Run(6, nil, nil) ts.Run(6)
ht.ExpValidated(t, 6, nil) ht.ExpValidated(t, 6, nil)
chain.SetNextSyncPeriod(3) chain.SetNextSyncPeriod(3)
ts.Run(7, nil, nil) ts.Run(7)
// testSHead4 could be validated now but it's not queued by any registered server // testSHead4 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(EvNewSignedHead, testServer2, testSHead4)
ts.Run(8, nil, nil) ts.Run(8)
// now testSHead4 should be validated // now testSHead4 should be validated
ht.ExpValidated(t, 8, []types.SignedHeader{testSHead4}) ht.ExpValidated(t, 8, []types.SignedHeader{testSHead4})
} }
@ -107,45 +107,45 @@ func TestPrefetchHead(t *testing.T) {
ts.AddServer(testServer1, 1) ts.AddServer(testServer1, 1)
ts.ServerEvent(EvNewHead, testServer1, testHead1) ts.ServerEvent(EvNewHead, testServer1, testHead1)
ts.Run(1, nil, nil) ts.Run(1)
ht.ExpPrefetch(t, 1, testHead1) // s1: h1 ht.ExpPrefetch(t, 1, testHead1) // s1: h1
ts.AddServer(testServer2, 1) ts.AddServer(testServer2, 1)
ts.ServerEvent(EvNewHead, testServer2, testHead2) ts.ServerEvent(EvNewHead, testServer2, testHead2)
ts.Run(2, nil, nil) ts.Run(2)
ht.ExpPrefetch(t, 2, testHead2) // s1: h1, s2: h2 ht.ExpPrefetch(t, 2, testHead2) // s1: h1, s2: h2
ts.ServerEvent(EvNewHead, testServer1, testHead2) ts.ServerEvent(EvNewHead, testServer1, testHead2)
ts.Run(3, nil, nil) ts.Run(3)
ht.ExpPrefetch(t, 3, testHead2) // s1: h2, s2: h2 ht.ExpPrefetch(t, 3, testHead2) // s1: h2, s2: h2
ts.AddServer(testServer3, 1) ts.AddServer(testServer3, 1)
ts.ServerEvent(EvNewHead, testServer3, testHead3) ts.ServerEvent(EvNewHead, testServer3, testHead3)
ts.Run(4, nil, nil) ts.Run(4)
ht.ExpPrefetch(t, 4, testHead2) // s1: h2, s2: h2, s3: h3 ht.ExpPrefetch(t, 4, testHead2) // s1: h2, s2: h2, s3: h3
ts.AddServer(testServer4, 1) ts.AddServer(testServer4, 1)
ts.ServerEvent(EvNewHead, testServer4, testHead4) ts.ServerEvent(EvNewHead, testServer4, testHead4)
ts.Run(5, nil, nil) ts.Run(5)
ht.ExpPrefetch(t, 5, testHead2) // s1: h2, s2: h2, s3: h3, s4: h4 ht.ExpPrefetch(t, 5, testHead2) // s1: h2, s2: h2, s3: h3, s4: h4
ts.ServerEvent(EvNewHead, testServer2, testHead3) ts.ServerEvent(EvNewHead, testServer2, testHead3)
ts.Run(6, nil, nil) ts.Run(6)
ht.ExpPrefetch(t, 6, testHead3) // s1: h2, s2: h3, s3: h3, s4: h4 ht.ExpPrefetch(t, 6, testHead3) // s1: h2, s2: h3, s3: h3, s4: h4
ts.RemoveServer(testServer3) ts.RemoveServer(testServer3)
ts.Run(7, nil, nil) ts.Run(7)
ht.ExpPrefetch(t, 7, testHead4) // s1: h2, s2: h3, s4: h4 ht.ExpPrefetch(t, 7, testHead4) // s1: h2, s2: h3, s4: h4
ts.RemoveServer(testServer1) ts.RemoveServer(testServer1)
ts.Run(8, nil, nil) ts.Run(8)
ht.ExpPrefetch(t, 8, testHead4) // s2: h3, s4: h4 ht.ExpPrefetch(t, 8, testHead4) // s2: h3, s4: h4
ts.RemoveServer(testServer4) ts.RemoveServer(testServer4)
ts.Run(9, nil, nil) ts.Run(9)
ht.ExpPrefetch(t, 9, testHead3) // s2: h3 ht.ExpPrefetch(t, 9, testHead3) // s2: h3
ts.RemoveServer(testServer2) ts.RemoveServer(testServer2)
ts.Run(10, nil, nil) ts.Run(10)
ht.ExpPrefetch(t, 10, testHead0) // no servers registered ht.ExpPrefetch(t, 10, testHead0) // no servers registered
} }

View file

@ -17,6 +17,7 @@
package sync package sync
import ( import (
"reflect"
"testing" "testing"
"github.com/ethereum/go-ethereum/beacon/light" "github.com/ethereum/go-ethereum/beacon/light"
@ -24,15 +25,6 @@ import (
"github.com/ethereum/go-ethereum/beacon/types" "github.com/ethereum/go-ethereum/beacon/types"
) )
type TestServer struct {
ts *TestScheduler
ID int
}
func (s *TestServer) Fail(desc string) {
s.ts.serverFail(s)
}
type requestWithID struct { type requestWithID struct {
sid request.ServerAndID sid request.ServerAndID
request request.Request request request.Request
@ -44,7 +36,7 @@ type TestScheduler struct {
events []request.Event events []request.Event
servers []request.Server servers []request.Server
allowance map[request.Server]int allowance map[request.Server]int
sent map[int]requestWithID sent map[int][]requestWithID
testIndex int testIndex int
expFail map[request.Server]int // expected Server.Fail calls during next Run expFail map[request.Server]int // expected Server.Fail calls during next Run
lastId request.ID lastId request.ID
@ -56,13 +48,26 @@ func NewTestScheduler(t *testing.T, module request.Module) *TestScheduler {
module: module, module: module,
allowance: make(map[request.Server]int), allowance: make(map[request.Server]int),
expFail: make(map[request.Server]int), expFail: make(map[request.Server]int),
sent: make(map[int]requestWithID), sent: make(map[int][]requestWithID),
} }
} }
func (ts *TestScheduler) Run(testIndex int, expServer request.Server, expReq request.Request) { func (ts *TestScheduler) Run(testIndex int, exp ...any) {
expReqs := make([]requestWithID, len(exp)/2)
id := ts.lastId
for i := range expReqs {
id++
expReqs[i] = requestWithID{
sid: request.ServerAndID{Server: exp[i*2].(request.Server), ID: id},
request: exp[i*2+1].(request.Request),
}
}
if len(expReqs) == 0 {
expReqs = nil
}
ts.testIndex = testIndex ts.testIndex = testIndex
ts.module.Process(ts.events) ts.module.Process(ts, ts.events)
ts.events = nil ts.events = nil
for server, count := range ts.expFail { for server, count := range ts.expFail {
@ -70,31 +75,47 @@ func (ts *TestScheduler) Run(testIndex int, expServer request.Server, expReq req
if count == 0 { if count == 0 {
continue continue
} }
ts.t.Errorf("Missing %d Server.Fail(s) from server %d in test case #%d", count, server.(*TestServer).ID, testIndex) ts.t.Errorf("Missing %d Server.Fail(s) from server %s in test case #%d", count, server.(string), testIndex)
} }
expReqWithID := requestWithID{ if !reflect.DeepEqual(ts.sent[testIndex], expReqs) {
sid: request.ServerAndID{Server: expServer, ID: ts.lastId + 1}, ts.t.Errorf("Wrong sent requests in test case #%d (expected %v, got %v)", testIndex, expReqs, ts.sent[testIndex])
request: expReq,
}
req, ok := ts.tryRequest(testIndex, ts.module.MakeRequest)
if expReq == nil {
if ok {
ts.t.Errorf("Unexpected request in test case #%d (expected none, got %v)", testIndex, req)
}
return
}
if !ok {
ts.t.Errorf("Missing request in test case #%d (expected %v, got none)", testIndex, expReqWithID)
return
}
if req != expReqWithID {
ts.t.Errorf("Wrong request in test case #%d (expected %v, got %v)", testIndex, expReqWithID, req)
} }
} }
func (ts *TestScheduler) Request(testIndex int) request.Request { func (ts *TestScheduler) CanSendTo() (cs []request.Server) {
return ts.sent[testIndex].request for _, server := range ts.servers {
if ts.allowance[server] > 0 {
cs = append(cs, server)
}
}
return
}
func (ts *TestScheduler) Send(server request.Server, req request.Request) request.ID {
ts.lastId++
ts.sent[ts.testIndex] = append(ts.sent[ts.testIndex], requestWithID{
sid: request.ServerAndID{Server: server, ID: ts.lastId},
request: req,
})
ts.allowance[server]--
return ts.lastId
}
func (ts *TestScheduler) Fail(server request.Server, desc string) {
if ts.expFail[server] == 0 {
ts.t.Errorf("Unexpected Fail from server %s in test case #%d: %s", server.(string), ts.testIndex, desc)
return
}
ts.expFail[server]--
}
func (ts *TestScheduler) Request(testIndex, reqIndex int) requestWithID {
if len(ts.sent[testIndex]) < reqIndex {
ts.t.Errorf("Missing request from test case %d index %d", testIndex, reqIndex)
return requestWithID{}
}
return ts.sent[testIndex][reqIndex-1]
} }
func (ts *TestScheduler) ServerEvent(evType *request.EventType, server request.Server, data any) { func (ts *TestScheduler) ServerEvent(evType *request.EventType, server request.Server, data any) {
@ -105,10 +126,8 @@ func (ts *TestScheduler) ServerEvent(evType *request.EventType, server request.S
}) })
} }
func (ts *TestScheduler) RequestEvent(evType *request.EventType, testIndex int, resp request.Response) { func (ts *TestScheduler) RequestEvent(evType *request.EventType, req requestWithID, resp request.Response) {
req, ok := ts.sent[testIndex] if req.request == nil {
if !ok {
ts.t.Errorf("Missing request from test case %v", testIndex)
return return
} }
ts.events = append(ts.events, request.Event{ ts.events = append(ts.events, request.Event{
@ -123,7 +142,6 @@ func (ts *TestScheduler) RequestEvent(evType *request.EventType, testIndex int,
} }
func (ts *TestScheduler) AddServer(server request.Server, allowance int) { func (ts *TestScheduler) AddServer(server request.Server, allowance int) {
server.(*TestServer).ts = ts
ts.servers = append(ts.servers, server) ts.servers = append(ts.servers, server)
ts.allowance[server] = allowance ts.allowance[server] = allowance
ts.ServerEvent(request.EvRegistered, server, nil) ts.ServerEvent(request.EvRegistered, server, nil)
@ -150,43 +168,6 @@ func (ts *TestScheduler) ExpFail(server request.Server) {
ts.expFail[server]++ ts.expFail[server]++
} }
func (ts *TestScheduler) serverFail(server request.Server) {
if ts.expFail[server] == 0 {
ts.t.Errorf("Unexpected Server.Fail from server %d in test case #%d", server.(*TestServer).ID, ts.testIndex)
return
}
ts.expFail[server]--
}
func (ts *TestScheduler) tryRequest(testIndex int, requestFn func(server request.Server) (request.Request, float32)) (requestWithID, bool) {
var (
bestServer request.Server
bestReq request.Request
bestPri float32
)
for _, server := range ts.servers {
if ts.allowance[server] == 0 {
continue
}
req, pri := requestFn(server)
if req != nil && (bestReq == nil || pri > bestPri) {
bestServer, bestReq, bestPri = server, req, pri
}
}
if bestServer == nil {
return requestWithID{}, false
}
ts.allowance[bestServer]--
ts.lastId++
req := requestWithID{
sid: request.ServerAndID{Server: bestServer, ID: ts.lastId},
request: bestReq,
}
ts.sent[testIndex] = req
ts.RequestEvent(request.EvRequest, testIndex, nil)
return req, true
}
type TestCommitteeChain struct { type TestCommitteeChain struct {
fsp, nsp uint64 fsp, nsp uint64
init bool init bool

View file

@ -52,16 +52,12 @@ func NewCheckpointInit(chain committeeChain, checkpointHash common.Hash) *Checkp
} }
} }
func (s *CheckpointInit) Process(events []request.Event) { func (s *CheckpointInit) Process(requester request.Requester, events []request.Event) {
for _, event := range events { for _, event := range events {
if !event.IsRequestEvent() { if !event.IsRequestEvent() {
continue continue
} }
sid, req, resp := event.RequestInfo() sid, req, resp := event.RequestInfo()
if event.Type == request.EvRequest {
s.locked = sid
continue
}
if s.locked == sid { if s.locked == sid {
s.locked = request.ServerAndID{} s.locked = request.ServerAndID{}
} }
@ -71,16 +67,21 @@ func (s *CheckpointInit) Process(events []request.Event) {
s.initialized = true s.initialized = true
return return
} }
event.Server.Fail("invalid checkpoint data")
requester.Fail(event.Server, "invalid checkpoint data")
} }
} }
} // start a request if possible
func (s *CheckpointInit) MakeRequest(server request.Server) (request.Request, float32) {
if s.initialized || s.locked != (request.ServerAndID{}) { if s.initialized || s.locked != (request.ServerAndID{}) {
return nil, 0 return
} }
return ReqCheckpointData(s.checkpointHash), 0 cs := requester.CanSendTo()
if len(cs) == 0 {
return
}
server := cs[0]
id := requester.Send(server, ReqCheckpointData(s.checkpointHash))
s.locked = request.ServerAndID{Server: server, ID: id}
} }
// ForwardUpdateSync implements request.Module; it fetches updates between the // ForwardUpdateSync implements request.Module; it fetches updates between the
@ -194,12 +195,9 @@ func (u updateResponseList) Less(i, j int) bool {
return u[i].request.FirstPeriod < u[j].request.FirstPeriod return u[i].request.FirstPeriod < u[j].request.FirstPeriod
} }
func (s *ForwardUpdateSync) Process(events []request.Event) { func (s *ForwardUpdateSync) Process(requester request.Requester, events []request.Event) {
for _, event := range events { for _, event := range events {
switch event.Type { switch event.Type {
case request.EvRequest:
sid, req, _ := event.RequestInfo()
s.lockRange(sid, req.(ReqUpdates))
case request.EvResponse, request.EvFail, request.EvTimeout: case request.EvResponse, request.EvFail, request.EvTimeout:
sid, rq, rs := event.RequestInfo() sid, rq, rs := event.RequestInfo()
req := rq.(ReqUpdates) req := rq.(ReqUpdates)
@ -212,7 +210,7 @@ func (s *ForwardUpdateSync) Process(events []request.Event) {
s.lockRange(sid, req) s.lockRange(sid, req)
queued = true queued = true
} else { } else {
event.Server.Fail("invalid update range") requester.Fail(event.Server, "invalid update range")
} }
} }
if !queued { if !queued {
@ -230,8 +228,8 @@ func (s *ForwardUpdateSync) Process(events []request.Event) {
sort.Sort(updateResponseList(s.processQueue)) sort.Sort(updateResponseList(s.processQueue))
for s.processQueue != nil { for s.processQueue != nil {
u := s.processQueue[0] u := s.processQueue[0]
if !s.processResponse(u) { if !s.processResponse(requester, u) {
return break
} }
s.unlockRange(u.sid, u.request) s.unlockRange(u.sid, u.request)
s.processQueue = s.processQueue[1:] s.processQueue = s.processQueue[1:]
@ -239,11 +237,43 @@ func (s *ForwardUpdateSync) Process(events []request.Event) {
s.processQueue = nil s.processQueue = nil
} }
} }
// start new requests if possible
startPeriod, chainInit := s.chain.NextSyncPeriod()
if !chainInit {
return
}
for {
firstPeriod, maxCount := s.rangeLock.firstUnlocked(startPeriod, maxUpdateRequest)
var (
sendTo request.Server
bestCount uint64
)
for _, server := range requester.CanSendTo() {
nextPeriod := s.nextSyncPeriod[server]
if nextPeriod <= firstPeriod {
continue
}
count := maxCount
if nextPeriod < firstPeriod+maxCount {
count = nextPeriod - firstPeriod
}
if count > bestCount {
sendTo, bestCount = server, count
}
}
if sendTo == nil {
return
}
req := ReqUpdates{FirstPeriod: firstPeriod, Count: bestCount}
id := requester.Send(sendTo, req)
s.lockRange(request.ServerAndID{Server: sendTo, ID: id}, req)
}
} }
// processResponse adds the fetched updates and committees to the committee chain. // processResponse adds the fetched updates and committees to the committee chain.
// Returns true in case of full or partial success. // Returns true in case of full or partial success.
func (s *ForwardUpdateSync) processResponse(u updateResponse) (success bool) { func (s *ForwardUpdateSync) processResponse(requester request.Requester, u updateResponse) (success bool) {
for i, update := range u.response.Updates { for i, update := range u.response.Updates {
if err := s.chain.InsertUpdate(update, u.response.Committees[i]); err != nil { if err := s.chain.InsertUpdate(update, u.response.Committees[i]); err != nil {
if err == light.ErrInvalidPeriod { if err == light.ErrInvalidPeriod {
@ -252,7 +282,7 @@ func (s *ForwardUpdateSync) processResponse(u updateResponse) (success bool) {
return return
} }
if err == light.ErrInvalidUpdate || err == light.ErrWrongCommitteeRoot || err == light.ErrCannotReorg { if err == light.ErrInvalidUpdate || err == light.ErrWrongCommitteeRoot || err == light.ErrCannotReorg {
u.sid.Server.Fail("invalid update received") requester.Fail(u.sid.Server, "invalid update received")
} else { } else {
log.Error("Unexpected InsertUpdate error", "error", err) log.Error("Unexpected InsertUpdate error", "error", err)
} }
@ -262,20 +292,3 @@ func (s *ForwardUpdateSync) processResponse(u updateResponse) (success bool) {
} }
return return
} }
func (s *ForwardUpdateSync) MakeRequest(server request.Server) (request.Request, float32) {
startPeriod, chainInit := s.chain.NextSyncPeriod()
if !chainInit {
return nil, 0
}
firstPeriod, maxCount := s.rangeLock.firstUnlocked(startPeriod, maxUpdateRequest)
nextPeriod := s.nextSyncPeriod[server]
if nextPeriod <= firstPeriod {
return nil, 0
}
count := maxCount
if nextPeriod < firstPeriod+maxCount {
count = nextPeriod - firstPeriod
}
return ReqUpdates{FirstPeriod: firstPeriod, Count: count}, float32(count)
}

View file

@ -37,18 +37,18 @@ func TestCheckpointInit(t *testing.T) {
ts.Run(1, testServer1, ReqCheckpointData(checkpointHash)) ts.Run(1, testServer1, ReqCheckpointData(checkpointHash))
// server 1 times out; expect request to server 2 // server 1 times out; expect request to server 2
ts.RequestEvent(request.EvTimeout, 1, nil) ts.RequestEvent(request.EvTimeout, ts.Request(1, 1), nil)
ts.Run(2, testServer2, ReqCheckpointData(checkpointHash)) ts.Run(2, testServer2, ReqCheckpointData(checkpointHash))
// invalid response from server 2; expect init state to still be false // invalid response from server 2; expect init state to still be false
ts.RequestEvent(request.EvResponse, 2, &types.BootstrapData{Header: types.Header{Slot: 123456}}) ts.RequestEvent(request.EvResponse, ts.Request(2, 1), &types.BootstrapData{Header: types.Header{Slot: 123456}})
ts.ExpFail(testServer2) ts.ExpFail(testServer2)
ts.Run(3, nil, nil) ts.Run(3)
chain.ExpInit(t, false) chain.ExpInit(t, false)
// server 1 fails (hard timeout) // server 1 fails (hard timeout)
ts.RequestEvent(request.EvFail, 1, nil) ts.RequestEvent(request.EvFail, ts.Request(1, 1), nil)
ts.Run(4, nil, nil) ts.Run(4)
chain.ExpInit(t, false) chain.ExpInit(t, false)
// server 3 is registered; expect bootstrap request to server 3 // server 3 is registered; expect bootstrap request to server 3
@ -56,8 +56,8 @@ func TestCheckpointInit(t *testing.T) {
ts.Run(5, testServer3, ReqCheckpointData(checkpointHash)) ts.Run(5, testServer3, ReqCheckpointData(checkpointHash))
// valid response from server 3; expect chain to be initialized // valid response from server 3; expect chain to be initialized
ts.RequestEvent(request.EvResponse, 5, checkpoint) ts.RequestEvent(request.EvResponse, ts.Request(5, 1), checkpoint)
ts.Run(6, nil, nil) ts.Run(6)
chain.ExpInit(t, true) chain.ExpInit(t, true)
} }
@ -73,68 +73,72 @@ func TestUpdateSyncParallel(t *testing.T) {
ts.ServerEvent(EvNewSignedHead, testServer2, types.SignedHeader{SignatureSlot: 0x2000*100 + 0x1000}) ts.ServerEvent(EvNewSignedHead, testServer2, types.SignedHeader{SignatureSlot: 0x2000*100 + 0x1000})
// expect 6 requests to be sent // expect 6 requests to be sent
ts.Run(1, testServer1, ReqUpdates{FirstPeriod: 0, Count: 8}) ts.Run(1,
ts.Run(2, testServer1, ReqUpdates{FirstPeriod: 8, Count: 8}) testServer1, ReqUpdates{FirstPeriod: 0, Count: 8},
ts.Run(3, testServer1, ReqUpdates{FirstPeriod: 16, Count: 8}) testServer1, ReqUpdates{FirstPeriod: 8, Count: 8},
ts.Run(4, testServer2, ReqUpdates{FirstPeriod: 24, Count: 8}) testServer1, ReqUpdates{FirstPeriod: 16, Count: 8},
ts.Run(5, testServer2, ReqUpdates{FirstPeriod: 32, Count: 8}) testServer2, ReqUpdates{FirstPeriod: 24, Count: 8},
ts.Run(6, testServer2, ReqUpdates{FirstPeriod: 40, Count: 8}) testServer2, ReqUpdates{FirstPeriod: 32, Count: 8},
testServer2, ReqUpdates{FirstPeriod: 40, Count: 8})
// valid response to request 1; expect 8 periods synced and a new request started // valid response to request 1; expect 8 periods synced and a new request started
ts.RequestEvent(request.EvResponse, 1, testRespUpdate(ts.Request(1))) ts.RequestEvent(request.EvResponse, ts.Request(1, 1), testRespUpdate(ts.Request(1, 1)))
ts.AddAllowance(testServer1, 1) ts.AddAllowance(testServer1, 1)
ts.Run(7, testServer1, ReqUpdates{FirstPeriod: 48, Count: 8}) ts.Run(7, testServer1, ReqUpdates{FirstPeriod: 48, Count: 8})
chain.ExpNextSyncPeriod(t, 8) chain.ExpNextSyncPeriod(t, 8)
// valid response to requests 4 and 5 // valid response to requests 4 and 5
ts.RequestEvent(request.EvResponse, 4, testRespUpdate(ts.Request(4))) ts.RequestEvent(request.EvResponse, ts.Request(1, 4), testRespUpdate(ts.Request(1, 4)))
ts.RequestEvent(request.EvResponse, 5, testRespUpdate(ts.Request(5))) ts.RequestEvent(request.EvResponse, ts.Request(1, 5), testRespUpdate(ts.Request(1, 5)))
ts.AddAllowance(testServer2, 2) ts.AddAllowance(testServer2, 2)
// expect 2 more requests but no sync progress (responses 4 and 5 cannot be added before 2 and 3) // expect 2 more requests but no sync progress (responses 4 and 5 cannot be added before 2 and 3)
ts.Run(8, testServer2, ReqUpdates{FirstPeriod: 56, Count: 8}) ts.Run(8,
ts.Run(9, testServer2, ReqUpdates{FirstPeriod: 64, Count: 8}) testServer2, ReqUpdates{FirstPeriod: 56, Count: 8},
testServer2, ReqUpdates{FirstPeriod: 64, Count: 8})
chain.ExpNextSyncPeriod(t, 8) chain.ExpNextSyncPeriod(t, 8)
// soft timeout for requests 2 and 3 (server 1 is overloaded) // soft timeout for requests 2 and 3 (server 1 is overloaded)
ts.RequestEvent(request.EvTimeout, 2, nil) ts.RequestEvent(request.EvTimeout, ts.Request(1, 2), nil)
ts.RequestEvent(request.EvTimeout, 3, nil) ts.RequestEvent(request.EvTimeout, ts.Request(1, 3), nil)
// no allowance, no more requests // no allowance, no more requests
ts.Run(10, nil, nil) ts.Run(10)
// valid response to requests 6 and 8 and 9 // valid response to requests 6 and 8 and 9
ts.RequestEvent(request.EvResponse, 6, testRespUpdate(ts.Request(6))) ts.RequestEvent(request.EvResponse, ts.Request(1, 6), testRespUpdate(ts.Request(1, 6)))
ts.RequestEvent(request.EvResponse, 8, testRespUpdate(ts.Request(8))) ts.RequestEvent(request.EvResponse, ts.Request(8, 1), testRespUpdate(ts.Request(8, 1)))
ts.RequestEvent(request.EvResponse, 9, testRespUpdate(ts.Request(9))) ts.RequestEvent(request.EvResponse, ts.Request(8, 2), testRespUpdate(ts.Request(8, 2)))
ts.AddAllowance(testServer2, 3) ts.AddAllowance(testServer2, 3)
// server 2 can now resend requests 2 and 3 (timed out by server 1) and also send a new one // server 2 can now resend requests 2 and 3 (timed out by server 1) and also send a new one
ts.Run(11, testServer2, ReqUpdates{FirstPeriod: 8, Count: 8}) ts.Run(11,
ts.Run(12, testServer2, ReqUpdates{FirstPeriod: 16, Count: 8}) testServer2, ReqUpdates{FirstPeriod: 8, Count: 8},
ts.Run(13, testServer2, ReqUpdates{FirstPeriod: 72, Count: 8}) testServer2, ReqUpdates{FirstPeriod: 16, Count: 8},
testServer2, ReqUpdates{FirstPeriod: 72, Count: 8})
// server 1 finally answers timed out request 2 // server 1 finally answers timed out request 2
ts.RequestEvent(request.EvResponse, 2, testRespUpdate(ts.Request(2))) ts.RequestEvent(request.EvResponse, ts.Request(1, 2), testRespUpdate(ts.Request(1, 2)))
ts.AddAllowance(testServer1, 1) ts.AddAllowance(testServer1, 1)
// expect sync progress and one new request // expect sync progress and one new request
ts.Run(14, testServer1, ReqUpdates{FirstPeriod: 80, Count: 8}) ts.Run(14, testServer1, ReqUpdates{FirstPeriod: 80, Count: 8})
chain.ExpNextSyncPeriod(t, 16) chain.ExpNextSyncPeriod(t, 16)
// server 2 answers requests 11 and 12 (resends of requests 2 and 3) // server 2 answers requests 11 and 12 (resends of requests 2 and 3)
ts.RequestEvent(request.EvResponse, 11, testRespUpdate(ts.Request(11))) ts.RequestEvent(request.EvResponse, ts.Request(11, 1), testRespUpdate(ts.Request(11, 1)))
ts.RequestEvent(request.EvResponse, 12, testRespUpdate(ts.Request(12))) ts.RequestEvent(request.EvResponse, ts.Request(11, 2), testRespUpdate(ts.Request(11, 2)))
ts.AddAllowance(testServer2, 2) ts.AddAllowance(testServer2, 2)
ts.Run(15, testServer2, ReqUpdates{FirstPeriod: 88, Count: 8}) ts.Run(15,
ts.Run(16, testServer2, ReqUpdates{FirstPeriod: 96, Count: 4}) testServer2, ReqUpdates{FirstPeriod: 88, Count: 8},
testServer2, ReqUpdates{FirstPeriod: 96, Count: 4})
// finally the gap is filled, update can process responses up to req6 // finally the gap is filled, update can process responses up to req6
chain.ExpNextSyncPeriod(t, 48) chain.ExpNextSyncPeriod(t, 48)
// all remaining requests are answered // all remaining requests are answered
ts.RequestEvent(request.EvResponse, 3, testRespUpdate(ts.Request(3))) ts.RequestEvent(request.EvResponse, ts.Request(1, 3), testRespUpdate(ts.Request(1, 3)))
ts.RequestEvent(request.EvResponse, 7, testRespUpdate(ts.Request(7))) ts.RequestEvent(request.EvResponse, ts.Request(7, 1), testRespUpdate(ts.Request(7, 1)))
ts.RequestEvent(request.EvResponse, 13, testRespUpdate(ts.Request(13))) ts.RequestEvent(request.EvResponse, ts.Request(11, 3), testRespUpdate(ts.Request(11, 3)))
ts.RequestEvent(request.EvResponse, 14, testRespUpdate(ts.Request(14))) ts.RequestEvent(request.EvResponse, ts.Request(14, 1), testRespUpdate(ts.Request(14, 1)))
ts.RequestEvent(request.EvResponse, 15, testRespUpdate(ts.Request(15))) ts.RequestEvent(request.EvResponse, ts.Request(15, 1), testRespUpdate(ts.Request(15, 1)))
ts.RequestEvent(request.EvResponse, 16, testRespUpdate(ts.Request(16))) ts.RequestEvent(request.EvResponse, ts.Request(15, 2), testRespUpdate(ts.Request(15, 2)))
ts.Run(17, nil, nil) ts.Run(17)
// expect chain to be fully synced // expect chain to be fully synced
chain.ExpNextSyncPeriod(t, 100) chain.ExpNextSyncPeriod(t, 100)
} }
@ -156,30 +160,32 @@ func TestUpdateSyncDifferentHeads(t *testing.T) {
ts.Run(1, testServer3, ReqUpdates{FirstPeriod: 10, Count: 7}) ts.Run(1, testServer3, ReqUpdates{FirstPeriod: 10, Count: 7})
// request times out, expect request to the next best head // request times out, expect request to the next best head
ts.RequestEvent(request.EvTimeout, 1, nil) ts.RequestEvent(request.EvTimeout, ts.Request(1, 1), nil)
ts.Run(2, testServer2, ReqUpdates{FirstPeriod: 10, Count: 6}) ts.Run(2, testServer2, ReqUpdates{FirstPeriod: 10, Count: 6})
// request times out, expect request to the last available server // request times out, expect request to the last available server
ts.RequestEvent(request.EvTimeout, 2, nil) ts.RequestEvent(request.EvTimeout, ts.Request(2, 1), nil)
ts.Run(3, testServer1, ReqUpdates{FirstPeriod: 10, Count: 5}) ts.Run(3, testServer1, ReqUpdates{FirstPeriod: 10, Count: 5})
// valid response to request 3, expect chain synced to period 15 // valid response to request 3, expect chain synced to period 15
ts.RequestEvent(request.EvResponse, 3, testRespUpdate(ts.Request(3))) ts.RequestEvent(request.EvResponse, ts.Request(3, 1), testRespUpdate(ts.Request(3, 1)))
ts.AddAllowance(testServer1, 1) ts.AddAllowance(testServer1, 1)
ts.Run(4, nil, nil) ts.Run(4)
chain.ExpNextSyncPeriod(t, 15) chain.ExpNextSyncPeriod(t, 15)
// invalid response to request 1, server can only deliver updates up to period 15 despite announced head // invalid response to request 1, server can only deliver updates up to period 15 despite announced head
ts.RequestEvent(request.EvResponse, 1, testRespUpdate(ReqUpdates{FirstPeriod: 10, Count: 5})) truncated := ts.Request(1, 1)
truncated.request = ReqUpdates{FirstPeriod: 10, Count: 5}
ts.RequestEvent(request.EvResponse, ts.Request(1, 1), testRespUpdate(truncated))
ts.ExpFail(testServer3) ts.ExpFail(testServer3)
ts.Run(5, nil, nil) ts.Run(5)
// expect no progress of chain head // expect no progress of chain head
chain.ExpNextSyncPeriod(t, 15) chain.ExpNextSyncPeriod(t, 15)
// valid response to request 2, expect chain synced to period 16 // valid response to request 2, expect chain synced to period 16
ts.RequestEvent(request.EvResponse, 2, testRespUpdate(ts.Request(2))) ts.RequestEvent(request.EvResponse, ts.Request(2, 1), testRespUpdate(ts.Request(2, 1)))
ts.AddAllowance(testServer2, 1) ts.AddAllowance(testServer2, 1)
ts.Run(6, nil, nil) ts.Run(6)
chain.ExpNextSyncPeriod(t, 16) chain.ExpNextSyncPeriod(t, 16)
// a new server is registered with announced head period 17 // a new server is registered with announced head period 17
@ -189,15 +195,18 @@ func TestUpdateSyncDifferentHeads(t *testing.T) {
ts.Run(7, testServer4, ReqUpdates{FirstPeriod: 16, Count: 1}) ts.Run(7, testServer4, ReqUpdates{FirstPeriod: 16, Count: 1})
// valid response, expect chain synced to period 17 // valid response, expect chain synced to period 17
ts.RequestEvent(request.EvResponse, 7, testRespUpdate(ts.Request(7))) ts.RequestEvent(request.EvResponse, ts.Request(7, 1), testRespUpdate(ts.Request(7, 1)))
ts.AddAllowance(testServer4, 1) ts.AddAllowance(testServer4, 1)
ts.Run(8, nil, nil) ts.Run(8)
chain.ExpNextSyncPeriod(t, 17) chain.ExpNextSyncPeriod(t, 17)
} }
func testRespUpdate(request request.Request) request.Response { func testRespUpdate(request requestWithID) request.Response {
var resp RespUpdates var resp RespUpdates
req := request.(ReqUpdates) if request.request == nil {
return resp
}
req := request.request.(ReqUpdates)
resp.Updates = make([]*types.LightClientUpdate, int(req.Count)) resp.Updates = make([]*types.LightClientUpdate, int(req.Count))
resp.Committees = make([]*types.SerializedSyncCommittee, int(req.Count)) resp.Committees = make([]*types.SerializedSyncCommittee, int(req.Count))
period := req.FirstPeriod period := req.FirstPeriod

View file

@ -29,7 +29,7 @@ import (
// to the validated and prefetch heads. // 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]
locked map[common.Hash]struct{} locked map[common.Hash]request.ServerAndID
serverHeads map[request.Server]common.Hash serverHeads map[request.Server]common.Hash
headTracker headTracker headTracker headTracker
@ -53,34 +53,42 @@ 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),
locked: make(map[common.Hash]struct{}), locked: make(map[common.Hash]request.ServerAndID),
serverHeads: make(map[request.Server]common.Hash), serverHeads: make(map[request.Server]common.Hash),
headCh: make(chan headData, 1), headCh: make(chan headData, 1),
} }
} }
func (s *beaconBlockSync) Process(events []request.Event) { func (s *beaconBlockSync) Process(requester request.Requester, events []request.Event) {
for _, event := range events { for _, event := range events {
switch event.Type { switch event.Type {
case request.EvRequest:
_, req, _ := event.RequestInfo()
blockRoot := common.Hash(req.(sync.ReqBeaconBlock))
s.locked[blockRoot] = struct{}{}
case request.EvResponse, request.EvFail, request.EvTimeout: case request.EvResponse, request.EvFail, request.EvTimeout:
_, req, resp := event.RequestInfo() sid, req, resp := event.RequestInfo()
blockRoot := common.Hash(req.(sync.ReqBeaconBlock)) blockRoot := common.Hash(req.(sync.ReqBeaconBlock))
if resp != nil { if resp != nil {
s.recentBlocks.Add(blockRoot, resp.(*capella.BeaconBlock)) s.recentBlocks.Add(blockRoot, resp.(*capella.BeaconBlock))
} }
delete(s.locked, blockRoot) if s.locked[blockRoot] == sid {
delete(s.locked, blockRoot)
}
case sync.EvNewHead: case sync.EvNewHead:
s.serverHeads[event.Server] = event.Data.(types.HeadInfo).BlockRoot s.serverHeads[event.Server] = event.Data.(types.HeadInfo).BlockRoot
case request.EvUnregistered: case request.EvUnregistered:
delete(s.serverHeads, event.Server) delete(s.serverHeads, event.Server)
} }
} }
s.updateValidatedHead()
// request validated head block if unavailable and not yet requested
if vh, ok := s.headTracker.ValidatedHead(); ok {
s.tryRequestBlock(requester, vh.Header.Hash(), false)
}
// request prefetch head if the given server has announced it
if prefetchHead := s.headTracker.PrefetchHead().BlockRoot; prefetchHead != (common.Hash{}) {
s.tryRequestBlock(requester, prefetchHead, true)
}
}
// send validated head block func (s *beaconBlockSync) updateValidatedHead() {
head, ok := s.headTracker.ValidatedHead() head, ok := s.headTracker.ValidatedHead()
if !ok { if !ok {
return return
@ -105,27 +113,22 @@ func (s *beaconBlockSync) Process(events []request.Event) {
} }
} }
func (s *beaconBlockSync) MakeRequest(server request.Server) (request.Request, float32) { func (s *beaconBlockSync) tryRequestBlock(requester request.Requester, blockRoot common.Hash, needSameHead bool) {
// request validated head block if unavailable and not yet requested if _, ok := s.recentBlocks.Get(blockRoot); ok {
if vh, ok := s.headTracker.ValidatedHead(); ok { return
validatedHead := vh.Header.Hash()
if _, ok := s.recentBlocks.Get(validatedHead); !ok {
if _, ok := s.locked[validatedHead]; !ok {
return sync.ReqBeaconBlock(validatedHead), 1
}
}
} }
// request prefetch head if the given server has announced it if _, ok := s.locked[blockRoot]; ok {
if prefetchHead := s.headTracker.PrefetchHead().BlockRoot; prefetchHead != (common.Hash{}) && prefetchHead == s.serverHeads[server] { return
if _, ok := s.recentBlocks.Get(prefetchHead); !ok { }
if _, ok := s.locked[prefetchHead]; !ok { for _, server := range requester.CanSendTo() {
return sync.ReqBeaconBlock(prefetchHead), 0 if needSameHead && (s.serverHeads[server] != blockRoot) {
} continue
} }
id := requester.Send(server, sync.ReqBeaconBlock(blockRoot))
s.locked[blockRoot] = request.ServerAndID{Server: server, ID: id}
return
} }
return nil, 0
} }
func blockHeadInfo(block *capella.BeaconBlock) types.HeadInfo { func blockHeadInfo(block *capella.BeaconBlock) types.HeadInfo {
if block == nil { if block == nil {
return types.HeadInfo{} return types.HeadInfo{}

View file

@ -29,8 +29,8 @@ import (
) )
var ( var (
testServer1 = &sync.TestServer{ID: 1} testServer1 = "testServer1"
testServer2 = &sync.TestServer{ID: 2} testServer2 = "testServer2"
testBlock1 = &capella.BeaconBlock{Slot: 123} testBlock1 = &capella.BeaconBlock{Slot: 123}
testBlock2 = &capella.BeaconBlock{Slot: 124} testBlock2 = &capella.BeaconBlock{Slot: 124}
@ -57,7 +57,7 @@ func TestBlockSync(t *testing.T) {
} }
// no block requests expected until head tracker knows about a head // no block requests expected until head tracker knows about a head
ts.Run(1, nil, nil) ts.Run(1)
expHeadBlock(1, nil) expHeadBlock(1, nil)
// set block 1 as prefetch head, announced by server 2 // set block 1 as prefetch head, announced by server 2
@ -68,15 +68,15 @@ func TestBlockSync(t *testing.T) {
ts.Run(2, testServer2, sync.ReqBeaconBlock(head1.BlockRoot)) ts.Run(2, testServer2, sync.ReqBeaconBlock(head1.BlockRoot))
// valid response // valid response
ts.RequestEvent(request.EvResponse, 2, testBlock1) ts.RequestEvent(request.EvResponse, ts.Request(2, 1), testBlock1)
ts.AddAllowance(testServer2, 1) ts.AddAllowance(testServer2, 1)
ts.Run(3, nil, nil) ts.Run(3)
// head block still not expected as the fetched block is not the validated head yet // head block still not expected as the fetched block is not the validated head yet
expHeadBlock(3, nil) expHeadBlock(3, nil)
// set as validated head, expect no further requests but block 1 set as head block // set as validated head, expect no further requests but block 1 set as head block
ht.validated.Header = blockHeader(testBlock1) ht.validated.Header = blockHeader(testBlock1)
ts.Run(4, nil, nil) ts.Run(4)
expHeadBlock(4, testBlock1) expHeadBlock(4, testBlock1)
// set block 2 as prefetch head, announced by server 1 // set block 2 as prefetch head, announced by server 1
@ -87,8 +87,8 @@ func TestBlockSync(t *testing.T) {
ts.Run(5, testServer1, sync.ReqBeaconBlock(head2.BlockRoot)) ts.Run(5, testServer1, sync.ReqBeaconBlock(head2.BlockRoot))
// req2 fails, no further requests expected because server 2 has not announced it // req2 fails, no further requests expected because server 2 has not announced it
ts.RequestEvent(request.EvFail, 5, nil) ts.RequestEvent(request.EvFail, ts.Request(5, 1), nil)
ts.Run(6, nil, nil) ts.Run(6)
// set as validated head before retrieving block; now it's assumed to be available from server 2 too // set as validated head before retrieving block; now it's assumed to be available from server 2 too
ht.validated.Header = blockHeader(testBlock2) ht.validated.Header = blockHeader(testBlock2)
@ -98,8 +98,8 @@ func TestBlockSync(t *testing.T) {
expHeadBlock(4, nil) expHeadBlock(4, nil)
// valid response, now head block should be block 2 immediately as it is already validated // valid response, now head block should be block 2 immediately as it is already validated
ts.RequestEvent(request.EvResponse, 7, testBlock2) ts.RequestEvent(request.EvResponse, ts.Request(7, 1), testBlock2)
ts.Run(8, nil, nil) ts.Run(8)
expHeadBlock(5, testBlock2) expHeadBlock(5, testBlock2)
} }