cmd/blsync, beacon/light: implement execBlockSyncer with request.Module

This commit is contained in:
Zsolt Felfoldi 2023-03-23 02:25:16 +01:00
parent 4f67f433c8
commit 4d3c2c45dc
17 changed files with 1697 additions and 555 deletions

108
beacon/light/api/light_api.go Normal file → Executable file
View file

@ -21,7 +21,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"math/big"
"net/http" "net/http"
"time" "time"
@ -32,9 +32,6 @@ import (
"github.com/ethereum/go-ethereum/beacon/params" "github.com/ethereum/go-ethereum/beacon/params"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/hexutil"
ctypes "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/trie"
"github.com/holiman/uint256"
"github.com/protolambda/zrnt/eth2/beacon/capella" "github.com/protolambda/zrnt/eth2/beacon/capella"
"github.com/protolambda/zrnt/eth2/configs" "github.com/protolambda/zrnt/eth2/configs"
"github.com/protolambda/ztyp/tree" "github.com/protolambda/ztyp/tree"
@ -211,36 +208,15 @@ func (api *BeaconLightApi) GetHeader(blockRoot common.Hash) (types.Header, error
} }
// does not verify state root // does not verify state root
func (api *BeaconLightApi) GetHeadStateProof(format merkle.ProofFormat) (merkle.MultiProof, error) { //TODO ...
encFormat, bitLength := EncodeCompactProofFormat(format) /*func (api *BeaconLightApi) GetHeadStateProof(format merkle.ProofFormat) (merkle.MultiProof, error) {
encFormat, bitLength := EncodeCompactProofFormat(format) //TODO cache encoding?
return api.getStateProof("head", format, encFormat, bitLength) return api.getStateProof("head", format, encFormat, bitLength)
} }*/
type StateProofSub struct { func (api *BeaconLightApi) GetStateProof(stateRoot common.Hash, format merkle.ProofFormat) (merkle.MultiProof, error) {
api *BeaconLightApi encFormat, bitLength := EncodeCompactProofFormat(format) //TODO cache encoding?
format merkle.ProofFormat proof, err := api.getStateProof(stateRoot.Hex(), format, encFormat, bitLength)
encFormat []byte
bitLength int
}
func (api *BeaconLightApi) SubscribeStateProof(format merkle.ProofFormat, first, period int) (*StateProofSub, error) {
encFormat, bitLength := EncodeCompactProofFormat(format)
_, err := api.httpGetf("/eth/v0/beacon/proof/subscribe/states?format=0x%x&first=%d&period=%d", encFormat, first, period)
if err != nil && err != ErrNotFound {
// if subscribe endpoint is missing then we expect proof endpoint to serve recent states without subscription
return nil, err
}
return &StateProofSub{
api: api,
format: format,
encFormat: encFormat,
bitLength: bitLength,
}, nil
}
// verifies state root
func (sub *StateProofSub) Get(stateRoot common.Hash) (merkle.MultiProof, error) {
proof, err := sub.api.getStateProof(stateRoot.Hex(), sub.format, sub.encFormat, sub.bitLength)
if err != nil { if err != nil {
return merkle.MultiProof{}, err return merkle.MultiProof{}, err
} }
@ -329,77 +305,27 @@ func (api *BeaconLightApi) GetCheckpointData(checkpointHash common.Hash) (*light
return checkpoint, nil return checkpoint, nil
} }
// GetExecutionPayload fetches the execution block belonging to the beacon block func (api *BeaconLightApi) GetBeaconBlock(blockRoot common.Hash) (*capella.BeaconBlock, error) {
// specified by beaconRoot and validates its block hash against the expected execRoot. resp, err := api.httpGetf("/eth/v2/beacon/blocks/0x%x", blockRoot)
func (api *BeaconLightApi) GetExecutionPayload(header types.Header) (*ctypes.Block, error) {
resp, err := api.httpGetf("/eth/v2/beacon/blocks/0x%x", header.Hash())
if err != nil { if err != nil {
return nil, err return nil, err
} }
spec := configs.Mainnet
// note: eth2 api endpoints serve bellatrix.SignedBeaconBlock instead
// also try github.com/protolambda/eth2api for api bindings
//var beaconBlock bellatrix.BeaconBlock
var beaconBlock capella.BeaconBlock
myJSONBlockData := resp
var beaconBlockMessage struct { var beaconBlockMessage struct {
Data struct { Data struct {
Message capella.BeaconBlock `json:"message"` Message capella.BeaconBlock `json:"message"`
} `json:"data"` } `json:"data"`
} }
if err := json.Unmarshal(myJSONBlockData, &beaconBlockMessage); err != nil { if err := json.Unmarshal(resp, &beaconBlockMessage); err != nil {
return nil, fmt.Errorf("invalid block json data: %v", err) return nil, fmt.Errorf("invalid block json data: %v", err)
} }
beaconBlock = beaconBlockMessage.Data.Message beaconBlock := new(capella.BeaconBlock)
beaconBodyRoot := common.Hash(beaconBlock.Body.HashTreeRoot(spec, tree.GetHashFn())) *beaconBlock = beaconBlockMessage.Data.Message
if beaconBodyRoot != header.BodyRoot { root := common.Hash(beaconBlock.HashTreeRoot(configs.Mainnet, tree.GetHashFn()))
return nil, fmt.Errorf("Beacon body root hash mismatch (expected: %x, got: %x)", header.BodyRoot.Bytes(), beaconBodyRoot.Bytes()) if root != blockRoot {
return nil, fmt.Errorf("Beacon block root hash mismatch (expected: %x, got: %x)", blockRoot, root)
} }
return beaconBlock, nil
payload := &beaconBlock.Body.ExecutionPayload
txs := make([]*ctypes.Transaction, len(payload.Transactions))
for i, opaqueTx := range payload.Transactions {
var tx ctypes.Transaction
if err := tx.UnmarshalBinary(opaqueTx); err != nil {
return nil, fmt.Errorf("failed to parse tx %d: %v", i, err)
}
txs[i] = &tx
}
withdrawals := make([]*ctypes.Withdrawal, len(payload.Withdrawals))
for i, w := range payload.Withdrawals {
withdrawals[i] = &ctypes.Withdrawal{
Index: uint64(w.Index),
Validator: uint64(w.ValidatorIndex),
Address: common.Address(w.Address),
Amount: uint64(w.Amount),
}
}
wroot := ctypes.DeriveSha(ctypes.Withdrawals(withdrawals), trie.NewStackTrie(nil))
execHeader := &ctypes.Header{
ParentHash: common.Hash(payload.ParentHash),
UncleHash: ctypes.EmptyUncleHash,
Coinbase: common.Address(payload.FeeRecipient),
Root: common.Hash(payload.StateRoot),
TxHash: ctypes.DeriveSha(ctypes.Transactions(txs), trie.NewStackTrie(nil)),
ReceiptHash: common.Hash(payload.ReceiptsRoot),
Bloom: ctypes.Bloom(payload.LogsBloom),
Difficulty: big.NewInt(0), // constant
Number: new(big.Int).SetUint64(uint64(payload.BlockNumber)),
GasLimit: uint64(payload.GasLimit),
GasUsed: uint64(payload.GasUsed),
Time: uint64(payload.Timestamp),
Extra: []byte(payload.ExtraData),
MixDigest: common.Hash(payload.PrevRandao), // reused in merge
Nonce: ctypes.BlockNonce{}, // zero
BaseFee: (*uint256.Int)(&payload.BaseFeePerGas).ToBig(),
WithdrawalsHash: &wroot,
}
execBlock := ctypes.NewBlockWithHeader(execHeader).WithBody(txs, nil).WithWithdrawals(withdrawals)
if execBlock.Hash() != common.Hash(payload.BlockHash) {
return nil, fmt.Errorf("Sanity check failed, payload hash does not match.")
}
return execBlock, nil
} }
func decodeHeadEvent(enc []byte) (uint64, common.Hash, error) { func decodeHeadEvent(enc []byte) (uint64, common.Hash, error) {

119
beacon/light/api/sync_server.go Normal file → Executable file
View file

@ -22,8 +22,10 @@ import (
"github.com/ethereum/go-ethereum/beacon/light" "github.com/ethereum/go-ethereum/beacon/light"
"github.com/ethereum/go-ethereum/beacon/light/types" "github.com/ethereum/go-ethereum/beacon/light/types"
"github.com/ethereum/go-ethereum/beacon/merkle"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/protolambda/zrnt/eth2/beacon/capella"
) )
const ( const (
@ -32,33 +34,41 @@ const (
type SyncServer struct { type SyncServer struct {
api *BeaconLightApi api *BeaconLightApi
Stop func()
lock sync.RWMutex lock sync.RWMutex
triggerCallback func() unsubscribe func()
latestHeadSlot uint64
latestHeadHash common.Hash
signedHeads []types.SignedHead
canRequestBootstrap bool canRequestBootstrap bool
firstUpdate uint64 //TODO ... firstUpdate, afterLastUpdate uint64
firstState uint64 //TODO ...
} }
func NewSyncServer(api *BeaconLightApi) *SyncServer { func NewSyncServer(api *BeaconLightApi) *SyncServer {
s := &SyncServer{ return &SyncServer{
api: api, api: api,
canRequestBootstrap: true, canRequestBootstrap: true,
} }
s.Stop = s.api.StartHeadListener(s.newHead, s.newSignedHead, func(err error) {
log.Warn("Head event stream error", "err", err)
})
return s
} }
func (s *SyncServer) SetTriggerCallback(cb func()) { func (s *SyncServer) SubscribeHeads(newHead func(uint64, common.Hash), newSignedHead func(signedHead types.SignedHead)) {
s.lock.Lock() s.lock.Lock()
defer s.lock.Unlock() s.unsubscribe = s.api.StartHeadListener(newHead, func(signedHead types.SignedHead) {
s.lock.Lock()
s.afterLastUpdate = types.PeriodOfSlot(signedHead.Header.Slot + 256)
s.lock.Unlock()
newSignedHead(signedHead)
}, func(err error) {
log.Warn("Head event stream error", "err", err)
})
s.lock.Unlock()
}
s.triggerCallback = cb func (s *SyncServer) UnsubscribeHeads() {
s.lock.Lock()
if s.unsubscribe != nil {
s.unsubscribe()
s.unsubscribe = nil
}
s.lock.Unlock()
} }
func (s *SyncServer) Delay() time.Duration { return 0 } //TODO func (s *SyncServer) Delay() time.Duration { return 0 } //TODO
@ -67,20 +77,6 @@ func (s *SyncServer) Fail(desc string) {
log.Warn("API endpoint failure", "URL", s.api.url, "error", desc) log.Warn("API endpoint failure", "URL", s.api.url, "error", desc)
} }
func (s *SyncServer) LatestHead() (uint64, common.Hash) {
s.lock.RLock()
defer s.lock.RUnlock()
return s.latestHeadSlot, s.latestHeadHash
}
func (s *SyncServer) SignedHeads() []types.SignedHead {
s.lock.RLock()
defer s.lock.RUnlock()
return s.signedHeads
}
func (s *SyncServer) CanRequestBootstrap() bool { func (s *SyncServer) CanRequestBootstrap() bool {
s.lock.RLock() s.lock.RLock()
defer s.lock.RUnlock() defer s.lock.RUnlock()
@ -105,10 +101,7 @@ func (s *SyncServer) UpdateRange() types.PeriodRange {
s.lock.RLock() s.lock.RLock()
defer s.lock.RUnlock() defer s.lock.RUnlock()
if len(s.signedHeads) == 0 { r := types.PeriodRange{First: s.firstUpdate, AfterLast: s.afterLastUpdate}
return types.PeriodRange{}
}
r := types.PeriodRange{First: s.firstUpdate, AfterLast: types.PeriodOfSlot(s.signedHeads[len(s.signedHeads)-1].Header.Slot + 256)}
if !r.IsEmpty() { if !r.IsEmpty() {
return r return r
} }
@ -125,32 +118,44 @@ func (s *SyncServer) RequestUpdates(first, count uint64, response func([]*types.
}() }()
} }
func (s *SyncServer) newHead(slot uint64, blockRoot common.Hash) { func (s *SyncServer) RequestBeaconBlock(blockRoot common.Hash, response func(*capella.BeaconBlock)) {
go func() {
if block, err := s.api.GetBeaconBlock(blockRoot); err == nil {
response(block)
} else {
response(nil)
}
}()
}
func (s *SyncServer) RequestBeaconHeader(blockRoot common.Hash, response func(*types.Header)) {
go func() {
if header, err := s.api.GetHeader(blockRoot); err == nil {
response(&header)
} else {
response(nil)
}
}()
}
func (s *SyncServer) BeaconStateTail() uint64 {
s.lock.RLock()
defer s.lock.RUnlock()
return s.firstState
}
func (s *SyncServer) RequestBeaconState(slot uint64, stateRoot common.Hash, format merkle.ProofFormat, response func(*merkle.MultiProof)) {
go func() {
if proof, err := s.api.GetStateProof(stateRoot, format); err == nil {
response(&proof)
} else {
s.lock.Lock() s.lock.Lock()
defer s.lock.Unlock() if slot >= s.firstState {
s.firstState = slot + 1
s.latestHeadSlot, s.latestHeadHash = slot, blockRoot
} }
s.lock.Unlock()
func (s *SyncServer) newSignedHead(signedHead types.SignedHead) { response(nil)
s.lock.Lock()
defer s.lock.Unlock()
if s.signedHeads == nil {
s.signedHeads = []types.SignedHead{signedHead}
s.triggerCallback()
return
} }
if lastHead := s.signedHeads[len(s.signedHeads)-1]; signedHead.Header.Slot < lastHead.Header.Slot || }()
(signedHead.Header.Slot == lastHead.Header.Slot && signedHead.SignerCount() <= lastHead.SignerCount()) {
return
}
if len(s.signedHeads) < maxHeadLength {
s.signedHeads = append(s.signedHeads, signedHead)
s.triggerCallback()
return
}
copy(s.signedHeads[:len(s.signedHeads)-1], s.signedHeads[1:])
s.signedHeads[len(s.signedHeads)-1] = signedHead
s.triggerCallback()
} }

View file

@ -25,14 +25,14 @@ import (
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
) )
type HeadTracker struct { type HeadValidator struct {
lock sync.Mutex lock sync.Mutex
committeeChain *CommitteeChain committeeChain *CommitteeChain
subs []*headSub subs []*headSub
} }
func NewHeadTracker(committeeChain *CommitteeChain) *HeadTracker { func NewHeadValidator(committeeChain *CommitteeChain) *HeadValidator {
return &HeadTracker{committeeChain: committeeChain} return &HeadValidator{committeeChain: committeeChain}
} }
type headSub struct { type headSub struct {
@ -41,7 +41,7 @@ type headSub struct {
callbacks []func(types.SignedHead) callbacks []func(types.SignedHead)
} }
func (h *HeadTracker) Subscribe(minSignerCount int, callback func(types.SignedHead)) { func (h *HeadValidator) Subscribe(minSignerCount int, callback func(types.SignedHead)) {
h.lock.Lock() h.lock.Lock()
defer h.lock.Unlock() defer h.lock.Unlock()
@ -64,7 +64,7 @@ func (h *HeadTracker) Subscribe(minSignerCount int, callback func(types.SignedHe
} }
} }
func (h *HeadTracker) Add(head types.SignedHead) error { func (h *HeadValidator) Add(head types.SignedHead) error {
h.lock.Lock() h.lock.Lock()
defer h.lock.Unlock() defer h.lock.Unlock()

268
beacon/light/light_chain.go Normal file
View file

@ -0,0 +1,268 @@
// Copyright 2023 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package light
import (
"errors"
"sync"
"github.com/ethereum/go-ethereum/beacon/light/types"
"github.com/ethereum/go-ethereum/beacon/merkle"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/lru"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/log"
//"github.com/ethereum/go-ethereum/rlp"
)
var (
ErrNotFound = errors.New("not found")
ErrEmptySlot = errors.New("empty slot")
ErrInvalidProofFormat = errors.New("invalid proof format")
ErrInvalidStateRoot = errors.New("invalid state root")
)
type LightChain struct {
lock sync.RWMutex
db ethdb.KeyValueStore //TODO implement database
chainHead, chainTail types.Header
chainInit bool
stateHead, stateTail types.Header
stateInit bool
headerCache *lru.Cache[slotAndHash, types.Header]
canonicalCache *lru.Cache[uint64, common.Hash]
slotCache *lru.Cache[common.Hash, uint64]
stateCache *lru.Cache[slotAndHash, merkle.Values]
stateProofFormat merkle.ProofFormat //TODO slot/parentSlot dependent format
}
func NewLightChain(db ethdb.KeyValueStore, stateProofFormat merkle.ProofFormat) *LightChain {
//TODO init from db
return &LightChain{
db: db,
stateProofFormat: stateProofFormat,
headerCache: lru.NewCache[slotAndHash, types.Header](10000), //TODO use smaller cache when db is implemented
canonicalCache: lru.NewCache[uint64, common.Hash](10000),
slotCache: lru.NewCache[common.Hash, uint64](10000),
stateCache: lru.NewCache[slotAndHash, merkle.Values](10000),
}
}
type slotAndHash struct {
slot uint64
hash common.Hash
}
func (lc *LightChain) AddHeader(header types.Header) {
lc.lock.Lock()
defer lc.lock.Unlock()
blockRoot := header.Hash()
lc.headerCache.Add(slotAndHash{header.Slot, blockRoot}, header)
lc.slotCache.Add(blockRoot, header.Slot)
if lc.chainInit && blockRoot == lc.chainTail.ParentRoot {
var err error
for err == nil {
lc.canonicalCache.Add(header.Slot, header.Hash())
for slot := header.Slot + 1; slot < lc.chainTail.Slot; slot++ {
lc.canonicalCache.Add(slot, common.Hash{})
}
lc.chainTail = header
header, err = lc.GetHeaderByHash(header.ParentRoot)
}
}
}
func (lc *LightChain) SetChainHead(head types.Header) {
lc.lock.Lock()
defer lc.lock.Unlock()
if !lc.chainInit {
lc.chainInit = true
lc.chainHead = head
lc.chainTail = head
}
for slot := head.Slot + 1; slot <= lc.chainHead.Slot; slot++ {
lc.canonicalCache.Remove(slot)
}
lc.chainHead = head
for !lc.IsCanonical(head) {
lc.canonicalCache.Add(head.Slot, head.Hash())
parent, err := lc.GetParent(head)
if err != nil {
for slot := lc.chainTail.Slot; slot < head.Slot; slot++ {
lc.canonicalCache.Remove(slot)
}
lc.chainTail = head
lc.stateInit = false
lc.reinitStateChain(head)
return
}
for slot := parent.Slot + 1; slot < head.Slot; slot++ {
lc.canonicalCache.Add(slot, common.Hash{})
}
head = parent
}
if lc.stateInit && lc.stateHead.Slot >= head.Slot {
if head.Slot >= lc.stateTail.Slot {
lc.stateHead = head
} else {
lc.stateInit = false
}
}
if lc.stateInit {
lc.extendStateHead()
} else {
lc.reinitStateChain(head)
}
}
func (lc *LightChain) extendStateHead() {
for slot := lc.stateHead.Slot + 1; slot <= lc.chainHead.Slot; slot++ {
if header, err := lc.GetHeaderBySlot(slot); err == nil {
if lc.HasStateProof(header) {
lc.stateHead = header
} else {
break
}
}
}
}
func (lc *LightChain) extendStateTail() {
if lc.stateTail.Slot == 0 {
return
}
for slot := lc.stateTail.Slot - 1; slot >= lc.chainTail.Slot; slot-- {
if header, err := lc.GetHeaderBySlot(slot); err == nil {
if lc.HasStateProof(header) {
lc.stateTail = header
} else {
break
}
}
}
}
func (lc *LightChain) reinitStateChain(header types.Header) {
for slot := header.Slot; slot <= lc.chainHead.Slot; slot++ {
if header, err := lc.GetHeaderBySlot(slot); err == nil && lc.HasStateProof(header) {
lc.stateInit = true
lc.stateHead = header
lc.stateTail = header
lc.extendStateHead()
return
}
}
}
func (lc *LightChain) HeaderRange() (head, tail types.Header, init bool) {
lc.lock.RLock()
defer lc.lock.RUnlock()
return lc.chainHead, lc.chainTail, lc.chainInit
}
func (lc *LightChain) HasHeader(blockRoot common.Hash) bool {
_, ok := lc.slotCache.Get(blockRoot)
return ok
}
func (lc *LightChain) GetHeaderByHash(blockRoot common.Hash) (types.Header, error) {
if slot, ok := lc.slotCache.Get(blockRoot); ok {
if header, ok := lc.headerCache.Get(slotAndHash{slot, blockRoot}); ok {
return header, nil
}
log.Error("LightChain slot -> blockRoot entry found but header is missing", "slot", slot, "blockRoot", blockRoot)
}
return types.Header{}, ErrNotFound
}
func (lc *LightChain) GetHeaderBySlot(slot uint64) (types.Header, error) {
if blockRoot, ok := lc.canonicalCache.Get(slot); ok {
if blockRoot == (common.Hash{}) {
return types.Header{}, ErrEmptySlot
}
if header, ok := lc.headerCache.Get(slotAndHash{slot, blockRoot}); ok {
return header, nil
}
log.Error("LightChain canonical blockRoot entry found but header is missing", "slot", slot, "blockRoot", blockRoot)
}
return types.Header{}, ErrNotFound
}
func (lc *LightChain) GetParent(header types.Header) (types.Header, error) {
return lc.GetHeaderByHash(header.ParentRoot)
}
func (lc *LightChain) IsCanonical(header types.Header) bool {
blockRoot, ok := lc.canonicalCache.Get(header.Slot)
return ok && blockRoot == header.Hash()
}
func (lc *LightChain) StateProofRange() (head, tail types.Header, init bool) {
lc.lock.RLock()
defer lc.lock.RUnlock()
return lc.stateHead, lc.stateTail, lc.stateInit
}
func (lc *LightChain) HasStateProof(header types.Header) bool {
_, ok := lc.stateCache.Get(slotAndHash{header.Slot, header.StateRoot})
return ok
}
func (lc *LightChain) GetStateProof(header types.Header) (merkle.MultiProof, error) {
values, ok := lc.stateCache.Get(slotAndHash{header.Slot, header.StateRoot})
if !ok {
return merkle.MultiProof{}, ErrNotFound
}
return merkle.MultiProof{Format: lc.stateProofFormat, Values: values}, nil
}
func (lc *LightChain) StateProofFormat(header types.Header) merkle.ProofFormat {
return lc.stateProofFormat
}
func (lc *LightChain) AddStateProof(header types.Header, proof merkle.MultiProof) error {
lc.lock.Lock()
defer lc.lock.Unlock()
if !merkle.IsEqual(proof.Format, lc.StateProofFormat(header)) {
return ErrInvalidProofFormat
}
if proof.RootHash() != header.StateRoot {
return ErrInvalidStateRoot
}
lc.stateCache.Add(slotAndHash{header.Slot, header.StateRoot}, proof.Values)
if !lc.IsCanonical(header) {
return nil
}
if !lc.stateInit {
lc.stateInit = true
lc.stateHead = header
lc.stateTail = header
return nil
}
if header.Slot > lc.stateHead.Slot && header.Slot <= lc.chainHead.Slot {
lc.extendStateHead()
} else if header.Slot < lc.stateTail.Slot && header.Slot >= lc.chainTail.Slot {
lc.extendStateTail()
}
return nil
}

View file

@ -0,0 +1,69 @@
// Copyright 2023 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package request
type request interface {
CanSendTo(server *Server) (canSend bool, priority uint64)
SendTo(server *Server)
}
// Environment allows Module.Process to send requests to a set of servers. The enabled server set can either be all servers that are not delayed or timed out (in case of a module trigger) or a subset of them that have been triggered by a server trigger.
type Environment struct {
*HeadTracker
scheduler *Scheduler
allServers []*Server
canRequestNow map[*Server]struct{}
}
func (s *Environment) TryRequest(req request) (sent, tryMore bool) {
var (
maxServerPriority, maxRequestPriority uint64
bestServer *Server
)
for server := range s.canRequestNow {
canRequest, serverPriority := server.CanRequestNow()
if !canRequest {
delete(s.canRequestNow, server)
continue
}
canSend, requestPriority := req.CanSendTo(server)
if !canSend || requestPriority < maxRequestPriority ||
(requestPriority == maxRequestPriority && serverPriority <= maxServerPriority) {
continue
}
maxServerPriority, maxRequestPriority = serverPriority, requestPriority
bestServer = server
}
if bestServer != nil {
req.SendTo(bestServer)
return true, true
}
return false, len(s.canRequestNow) > 0
}
func (s *Environment) CanRequestNow() bool {
return len(s.canRequestNow) > 0
}
func (s *Environment) CanRequestLater(req request) bool {
for _, server := range s.allServers {
if canSend, _ := req.CanSendTo(server); canSend {
return true
}
}
return false
}

View file

@ -0,0 +1,146 @@
// Copyright 2023 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package request
import (
"sync"
"github.com/ethereum/go-ethereum/beacon/light/types"
"github.com/ethereum/go-ethereum/common"
)
type HeadTracker struct {
newSignedHead func(server *Server, signedHead types.SignedHead)
validatedLock sync.RWMutex
validatedHead types.Header
validatedHeadTrigger *ModuleTrigger
prefetchLock sync.RWMutex
serverHeads map[*Server]common.Hash
headInfo map[common.Hash]serverHeadInfo
headCounter uint64
prefetchHead common.Hash
prefetchHeadTrigger *ModuleTrigger
}
type serverHeadInfo struct {
serverCount int
headCounter uint64
}
func NewHeadTracker(newSignedHead func(server *Server, signedHead types.SignedHead)) *HeadTracker {
return &HeadTracker{
serverHeads: make(map[*Server]common.Hash),
headInfo: make(map[common.Hash]serverHeadInfo),
newSignedHead: newSignedHead,
}
}
func (s *HeadTracker) SetupTriggers(trigger func(id string) *ModuleTrigger) {
s.validatedHeadTrigger = trigger("validatedHead")
s.prefetchHeadTrigger = trigger("prefetchHead")
}
func (s *HeadTracker) SetValidatedHead(head types.Header) {
s.validatedLock.Lock()
defer s.validatedLock.Unlock()
s.validatedHead = head
s.validatedHeadTrigger.Trigger()
}
func (s *HeadTracker) ValidatedHead() types.Header {
s.validatedLock.RLock()
defer s.validatedLock.RUnlock()
return s.validatedHead
}
func (s *HeadTracker) registerServer(server *Server) {
s.prefetchLock.Lock()
defer s.prefetchLock.Unlock()
server.SubscribeHeads(func(slot uint64, blockRoot common.Hash) {
s.prefetchLock.Lock()
defer s.prefetchLock.Unlock()
if server.unregistered {
return
}
server.setHead(slot, blockRoot)
s.setServerHead(server, blockRoot)
server.trigger()
}, func(signedHead types.SignedHead) {
s.newSignedHead(server, signedHead)
})
}
func (s *HeadTracker) unregisterServer(server *Server) {
s.prefetchLock.Lock()
defer s.prefetchLock.Unlock()
server.UnsubscribeHeads()
server.unregistered = true
s.setServerHead(server, common.Hash{})
}
func (s *HeadTracker) setServerHead(server *Server, head common.Hash) {
if oldHead, ok := s.serverHeads[server]; ok {
if head == oldHead {
return
}
h := s.headInfo[oldHead]
if h.serverCount--; h.serverCount > 0 {
s.headInfo[oldHead] = h
} else {
delete(s.headInfo, oldHead)
}
}
if head != (common.Hash{}) {
h, ok := s.headInfo[head]
if !ok {
s.headCounter++
h.headCounter = s.headCounter
}
h.serverCount++
s.headInfo[head] = h
}
var (
bestHead common.Hash
bestHeadInfo serverHeadInfo
)
for head, headInfo := range s.headInfo {
if headInfo.serverCount > bestHeadInfo.serverCount ||
(headInfo.serverCount == bestHeadInfo.serverCount && headInfo.headCounter > bestHeadInfo.headCounter) {
bestHead, bestHeadInfo = head, headInfo
}
}
if bestHead != s.prefetchHead {
s.prefetchHead = bestHead
s.prefetchHeadTrigger.Trigger()
} else if head == s.prefetchHead {
server.trigger()
}
}
func (s *HeadTracker) PrefetchHead() common.Hash {
s.prefetchLock.RLock()
defer s.prefetchLock.RUnlock()
return s.prefetchHead
}

View file

@ -16,32 +16,70 @@
package request package request
type sentRequest struct {
sentTo *Server
reqId uint64
}
type SingleLock struct { type SingleLock struct {
requestLock map[*Server]uint64 // servers where the request has been sent and not timed out yet sentRequest
Trigger *ModuleTrigger
} }
func (s *SingleLock) CanSend(server *Server) bool { func (s *SingleLock) CanRequest() bool {
reqId, ok := s.requestLock[server] if s.sentTo != nil && s.sentTo.hasTimedOut(s.reqId) {
if ok && server.Timeout(reqId) { s.sentTo = nil
delete(s.requestLock, server)
return false
} }
return !ok && server.CanSend() return s.sentTo == nil
} }
// assumes that canSend returned true (no request lock) func (s *SingleLock) Send(srv *Server) uint64 {
func (s *SingleLock) TrySend(srv *Server) (uint64, bool) { reqId := srv.sendRequest(s.Trigger)
if s.requestLock == nil { s.sentTo, s.reqId = srv, reqId
s.requestLock = make(map[*Server]uint64) return reqId
}
if reqId, ok := srv.TrySend(); ok {
s.requestLock[srv] = reqId
return reqId, true
}
return 0, false
} }
func (s *SingleLock) Returned(srv *Server, reqId uint64) { func (s *SingleLock) Returned(srv *Server, reqId uint64) {
delete(s.requestLock, srv) if srv == s.sentTo && reqId == s.reqId {
srv.Returned(reqId) s.sentTo = nil
}
srv.returned(reqId)
if s.Trigger != nil {
s.Trigger.Trigger()
}
}
type MultiLock struct {
locks map[interface{}]sentRequest // locks are only present in the map when sentTo != nil
Trigger *ModuleTrigger
}
func (s *MultiLock) CanRequest(id interface{}) bool {
if s.locks == nil {
s.locks = make(map[interface{}]sentRequest)
}
if sl, ok := s.locks[id]; ok {
if sl.sentTo.hasTimedOut(sl.reqId) {
delete(s.locks, id)
} else {
return false
}
}
return true
}
func (s *MultiLock) Send(srv *Server, id interface{}) uint64 {
reqId := srv.sendRequest(s.Trigger)
s.locks[id] = sentRequest{sentTo: srv, reqId: reqId}
return reqId
}
func (s *MultiLock) Returned(srv *Server, reqId uint64, id interface{}) {
if s.locks[id] == (sentRequest{sentTo: srv, reqId: reqId}) {
delete(s.locks, id)
}
srv.returned(reqId)
if s.Trigger != nil {
s.Trigger.Trigger()
}
} }

View file

@ -19,16 +19,21 @@ package request
import ( import (
"sync" "sync"
"time" "time"
"github.com/ethereum/go-ethereum/beacon/light/types"
"github.com/ethereum/go-ethereum/common"
) )
const softRequestTimeout = time.Second const softRequestTimeout = time.Second
type Module interface { type Module interface {
Process(servers []*Server) bool // removed if return value is false SetupTriggers(trigger func(id string, subscribe bool) *ModuleTrigger)
Process(env *Environment)
} }
type RequestServer interface { type RequestServer interface {
SetTriggerCallback(func()) SubscribeHeads(newHead func(uint64, common.Hash), newSignedHead func(types.SignedHead))
UnsubscribeHeads()
Delay() time.Duration Delay() time.Duration
Fail(string) Fail(string)
} }
@ -47,14 +52,17 @@ func (t *ModuleTrigger) Trigger() {
defer t.s.triggerLock.Unlock() defer t.s.triggerLock.Unlock()
for m := range t.triggers { for m := range t.triggers {
t.s.moduleTrigger(m) t.s.triggerModule(m)
} }
} }
type Scheduler struct { type Scheduler struct {
headTracker *HeadTracker
lock sync.Mutex lock sync.Mutex
modules []Module // first has highest priority modules []Module // first has highest priority
servers []*Server servers []*Server
triggers map[string]*ModuleTrigger
triggeredBy map[Module][]*ModuleTrigger triggeredBy map[Module][]*ModuleTrigger
stopCh chan chan struct{} stopCh chan chan struct{}
@ -65,10 +73,12 @@ type Scheduler struct {
trServers map[*Server]struct{} trServers map[*Server]struct{}
} }
func NewScheduler() *Scheduler { func NewScheduler(headTracker *HeadTracker) *Scheduler {
return &Scheduler{ return &Scheduler{
headTracker: headTracker,
stopCh: make(chan chan struct{}), stopCh: make(chan chan struct{}),
triggerCh: make(chan struct{}, 1), triggerCh: make(chan struct{}, 1),
triggers: make(map[string]*ModuleTrigger),
triggeredBy: make(map[Module][]*ModuleTrigger), triggeredBy: make(map[Module][]*ModuleTrigger),
} }
} }
@ -79,46 +89,51 @@ func (s *Scheduler) RegisterModule(m Module) {
defer s.lock.Unlock() defer s.lock.Unlock()
s.modules = append(s.modules, m) s.modules = append(s.modules, m)
m.SetupTriggers(func(id string, subscribe bool) *ModuleTrigger { return s.addTrigger(m, id, subscribe) })
} }
func (s *Scheduler) AddTriggers(m Module, triggeredBy []*ModuleTrigger) { func (s *Scheduler) addTrigger(m Module, id string, subscribe bool) *ModuleTrigger {
s.triggeredBy[m] = append(s.triggeredBy[m], triggeredBy...) t, ok := s.triggers[id]
for _, t := range triggeredBy { if !ok {
t = new(ModuleTrigger)
s.triggers[id] = t
}
if !subscribe {
return t
}
s.triggeredBy[m] = append(s.triggeredBy[m], t)
if t.triggers == nil { if t.triggers == nil {
t.s = s t.s = s
t.triggers = make(map[Module]struct{}) t.triggers = make(map[Module]struct{})
} }
t.triggers[m] = struct{}{} t.triggers[m] = struct{}{}
} return t
} }
func (s *Scheduler) unregisterModule(m Module, t []*ModuleTrigger) { // GetModuleTrigger returns the ModuleTrigger with the given id or creates a new one.
for i, module := range s.modules { func (s *Scheduler) GetModuleTrigger(id string) *ModuleTrigger {
if module == m {
copy(s.modules[i:len(s.modules)-1], s.modules[i+1:])
s.modules = s.modules[:len(s.modules)-1]
break
}
}
triggeredBy := s.triggeredBy[m]
delete(s.triggeredBy, m)
for _, t := range triggeredBy {
delete(t.triggers, m)
}
}
func (s *Scheduler) RegisterServer(RequestServer RequestServer) {
s.lock.Lock() s.lock.Lock()
defer s.lock.Unlock() defer s.lock.Unlock()
server := s.newServer(RequestServer) t, ok := s.triggers[id]
s.servers = append(s.servers, server) if !ok {
RequestServer.SetTriggerCallback(func() { t = new(ModuleTrigger)
s.ServerTrigger(server) s.triggers[id] = t
}) }
s.ServerTrigger(server) return t
} }
// RegisterServer registers a new server.
func (s *Scheduler) RegisterServer(requestServer RequestServer) {
s.lock.Lock()
defer s.lock.Unlock()
server := s.newServer(requestServer)
s.servers = append(s.servers, server)
s.headTracker.registerServer(server)
}
// UnregisterServer removes a registered server.
func (s *Scheduler) UnregisterServer(RequestServer RequestServer) { func (s *Scheduler) UnregisterServer(RequestServer RequestServer) {
s.lock.Lock() s.lock.Lock()
defer s.lock.Unlock() defer s.lock.Unlock()
@ -128,16 +143,18 @@ func (s *Scheduler) UnregisterServer(RequestServer RequestServer) {
s.servers[i] = s.servers[len(s.servers)-1] s.servers[i] = s.servers[len(s.servers)-1]
s.servers = s.servers[:len(s.servers)-1] s.servers = s.servers[:len(s.servers)-1]
server.stop() server.stop()
s.headTracker.unregisterServer(server)
return return
} }
} }
} }
// call before registering servers // Start starts the scheduler. It should be called after registering all modules and before registering any servers.
func (s *Scheduler) Start() { func (s *Scheduler) Start() {
go s.syncLoop() go s.syncLoop()
} }
// Stop stops the scheduler.
func (s *Scheduler) Stop() { func (s *Scheduler) Stop() {
s.lock.Lock() s.lock.Lock()
for _, server := range s.servers { for _, server := range s.servers {
@ -150,6 +167,7 @@ func (s *Scheduler) Stop() {
<-stop <-stop
} }
// syncLoop calls all processable modules in the order of their registration. A round of processing starts whenever there is at least one processable module. Triggers triggered during a processing round do not affect the current round but ensure that there is going to be a next round.
func (s *Scheduler) syncLoop() { func (s *Scheduler) syncLoop() {
s.lock.Lock() s.lock.Lock()
s.triggerLock.Lock() s.triggerLock.Lock()
@ -179,36 +197,42 @@ func (s *Scheduler) syncLoop() {
} }
} }
// processModules runs an entire processing round, calling processable modules with the appropriate Environment.
func (s *Scheduler) processModules(trModules map[Module]struct{}, trServers map[*Server]struct{}) { func (s *Scheduler) processModules(trModules map[Module]struct{}, trServers map[*Server]struct{}) {
trs := make([]*Server, 0, len(s.servers)) mtEnv := Environment{ // enables all servers for triggered modules
HeadTracker: s.headTracker,
scheduler: s,
allServers: s.servers,
canRequestNow: make(map[*Server]struct{}),
}
stEnv := Environment{ // enables triggered servers only for other modules
HeadTracker: s.headTracker,
scheduler: s,
allServers: s.servers,
canRequestNow: make(map[*Server]struct{}),
}
for _, server := range s.servers { for _, server := range s.servers {
if canRequest, _ := server.CanRequestNow(); !canRequest {
continue
}
mtEnv.canRequestNow[server] = struct{}{}
if _, ok := trServers[server]; ok { if _, ok := trServers[server]; ok {
trs = append(trs, server) stEnv.canRequestNow[server] = struct{}{}
} }
} }
var i int
for _, module := range s.modules { for _, module := range s.modules {
keep := true
if _, ok := trModules[module]; ok { if _, ok := trModules[module]; ok {
keep = module.Process(s.servers) module.Process(&mtEnv)
} else if len(trs) > 0 { } else if len(stEnv.canRequestNow) > 0 {
keep = module.Process(trs) module.Process(&stEnv)
}
if keep {
s.modules[i] = module
i++
} }
} }
s.modules = s.modules[:i]
} }
func (s *Scheduler) ServerTrigger(server *Server) { // triggerServer ensures that a next processing round is initiated as soon as possible and every module will be called with the given server enabled in its Environment. Should be called when the given server has become available (again) or when its range of servable requests has been expanded.
func (s *Scheduler) triggerServer(server *Server) {
s.triggerLock.Lock() s.triggerLock.Lock()
s.serverTrigger(server)
s.triggerLock.Unlock()
}
func (s *Scheduler) serverTrigger(server *Server) {
if s.trServers == nil { if s.trServers == nil {
s.trServers = make(map[*Server]struct{}) s.trServers = make(map[*Server]struct{})
} }
@ -217,15 +241,11 @@ func (s *Scheduler) serverTrigger(server *Server) {
s.triggerCh <- struct{}{} s.triggerCh <- struct{}{}
s.triggered = true s.triggered = true
} }
}
func (s *Scheduler) ModuleTrigger(module Module) {
s.triggerLock.Lock()
s.moduleTrigger(module)
s.triggerLock.Unlock() s.triggerLock.Unlock()
} }
func (s *Scheduler) moduleTrigger(module Module) { // triggerModule ensures that a next processing round is initiated as soon as possible and the given module will be called with all servers enabled in its Environment. Called by ModuleTrigger.Trigger when the range of possible requests or processable data might have been expanded.
func (s *Scheduler) triggerModule(module Module) {
if s.trModules == nil { if s.trModules == nil {
s.trModules = make(map[Module]struct{}) s.trModules = make(map[Module]struct{})
} }

View file

@ -20,36 +20,19 @@ import (
"math/rand" "math/rand"
"sync" "sync"
"time" "time"
"github.com/ethereum/go-ethereum/common"
) )
func SelectServer(servers []*Server, priority func(server *Server) uint64) *Server { type Server struct {
var (
maxPriority uint64
mpCount int
bestServer *Server
)
for _, server := range servers {
pri := priority(server)
if pri == 0 || pri < maxPriority { // 0 means it cannot serve the request at all
continue
}
if pri > maxPriority {
maxPriority = pri
mpCount = 1
bestServer = server
} else {
mpCount++
if rand.Intn(mpCount) == 0 {
bestServer = server
}
}
}
return bestServer
}
type Server struct { //TODO name?
RequestServer RequestServer
scheduler *Scheduler scheduler *Scheduler
headLock sync.RWMutex
latestHeadSlot uint64
latestHeadHash common.Hash
unregistered bool // accessed under HeadTracker.prefetchLock
lock sync.Mutex lock sync.Mutex
sent map[uint64]chan struct{} // closed when returned; nil when timed out sent map[uint64]chan struct{} // closed when returned; nil when timed out
timeoutCount int timeoutCount int
@ -69,51 +52,68 @@ func (s *Scheduler) newServer(server RequestServer) *Server {
} }
} }
func (s *Server) setHead(slot uint64, blockRoot common.Hash) {
s.headLock.Lock()
defer s.headLock.Unlock()
s.latestHeadSlot, s.latestHeadHash = slot, blockRoot
}
func (s *Server) trigger() {
s.scheduler.triggerServer(s)
}
func (s *Server) LatestHead() (uint64, common.Hash) {
s.headLock.RLock()
defer s.headLock.RUnlock()
return s.latestHeadSlot, s.latestHeadHash
}
// guarantees a server trigger later if the result is false // guarantees a server trigger later if the result is false
func (s *Server) CanSend() bool { func (s *Server) CanRequestNow() (bool, uint64) {
s.lock.Lock() s.lock.Lock()
defer s.lock.Unlock() defer s.lock.Unlock()
if s.isDelayed() || s.timeoutCount != 0 { if s.isDelayed() || s.timeoutCount != 0 {
s.needTrigger = true s.needTrigger = true
return false return false, 0
} }
return true return true, uint64(rand.Uint32() + 1) //TODO use priority based on in-flight requests
} }
// guarantees a server trigger later if the result is false func (s *Server) sendRequest(timeoutTrigger *ModuleTrigger) uint64 {
func (s *Server) TrySend() (uint64, bool) {
s.lock.Lock() s.lock.Lock()
defer s.lock.Unlock() defer s.lock.Unlock()
if s.isDelayed() || s.timeoutCount != 0 {
s.needTrigger = true
return 0, false
}
s.lastReqId++ s.lastReqId++
reqId := s.lastReqId
returnCh := make(chan struct{}) returnCh := make(chan struct{})
s.sent[s.lastReqId] = returnCh s.sent[reqId] = returnCh
s.delayChecked = false s.delayChecked = false
go func() { go func() {
timer := time.NewTimer(softRequestTimeout) timer := time.NewTimer(softRequestTimeout)
select { select {
case <-timer.C: case <-timer.C:
s.lock.Lock() s.lock.Lock()
if _, ok := s.sent[s.lastReqId]; ok { if _, ok := s.sent[reqId]; ok {
s.sent[s.lastReqId] = nil s.sent[reqId] = nil
s.timeoutCount++ s.timeoutCount++
} }
s.lock.Unlock() s.lock.Unlock()
if timeoutTrigger != nil {
timeoutTrigger.Trigger()
}
case <-returnCh: case <-returnCh:
timer.Stop() timer.Stop()
case <-s.stopCh: case <-s.stopCh:
timer.Stop() timer.Stop()
} }
}() }()
return s.lastReqId, true return reqId
} }
func (s *Server) Timeout(reqId uint64) bool { func (s *Server) hasTimedOut(reqId uint64) bool {
s.lock.Lock() s.lock.Lock()
defer s.lock.Unlock() defer s.lock.Unlock()
@ -121,7 +121,7 @@ func (s *Server) Timeout(reqId uint64) bool {
return ok && ch == nil return ok && ch == nil
} }
func (s *Server) Returned(reqId uint64) { func (s *Server) returned(reqId uint64) {
s.lock.Lock() s.lock.Lock()
defer s.lock.Unlock() defer s.lock.Unlock()
@ -130,6 +130,10 @@ func (s *Server) Returned(reqId uint64) {
close(ch) close(ch)
} else { } else {
s.timeoutCount-- s.timeoutCount--
if s.needTrigger && s.timeoutCount == 0 && !s.isDelayed() {
s.needTrigger = false
s.scheduler.triggerServer(s)
}
} }
delete(s.sent, reqId) delete(s.sent, reqId)
} }
@ -152,11 +156,11 @@ func (s *Server) isDelayed() bool {
case <-timer.C: case <-timer.C:
s.lock.Lock() s.lock.Lock()
s.delayed = false s.delayed = false
trigger := s.needTrigger && s.timeoutCount == 0 if s.needTrigger && s.timeoutCount == 0 {
s.lock.Unlock() s.needTrigger = false
if trigger { s.scheduler.triggerServer(s)
s.scheduler.serverTrigger(s)
} }
s.lock.Unlock()
case <-s.stopCh: case <-s.stopCh:
timer.Stop() timer.Stop()
} }

View file

@ -1,105 +0,0 @@
// Copyright 2023 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package sync
import (
"sync"
"github.com/ethereum/go-ethereum/beacon/light"
"github.com/ethereum/go-ethereum/beacon/light/request"
"github.com/ethereum/go-ethereum/beacon/light/types"
)
type signedHeadServer interface {
request.RequestServer
SignedHeads() []types.SignedHead
}
type latestHeads struct {
heads map[uint64]types.SignedHead
oldestSlot uint64
}
type HeadSyncer struct {
lock sync.Mutex
headTracker *light.HeadTracker
chain *light.CommitteeChain
added, queued latestHeads
SignedHeadTrigger request.ModuleTrigger
}
func NewHeadSyncer(headTracker *light.HeadTracker, chain *light.CommitteeChain) *HeadSyncer {
return &HeadSyncer{
headTracker: headTracker,
chain: chain,
}
}
func (s *HeadSyncer) Process(servers []*request.Server) bool {
s.lock.Lock()
defer s.lock.Unlock()
nextPeriod, ok := s.chain.NextSyncPeriod()
if !ok {
return true
}
for slot, head := range s.queued.heads {
if head.Header.SyncPeriod() <= nextPeriod {
delete(s.queued.heads, slot)
if s.added.add(head) && s.headTracker.Add(head) == nil {
s.SignedHeadTrigger.Trigger()
}
}
}
for _, server := range servers {
if hserver, ok := server.RequestServer.(signedHeadServer); ok {
heads := hserver.SignedHeads()
for _, head := range heads {
if head.Header.SyncPeriod() > nextPeriod {
s.queued.add(head)
} else if s.added.add(head) {
if s.headTracker.Add(head) == nil {
s.SignedHeadTrigger.Trigger()
} else {
hserver.Fail("received invalid signed head")
break
}
}
}
}
}
return true
}
func (l *latestHeads) add(head types.SignedHead) bool {
if l.heads == nil {
l.heads = make(map[uint64]types.SignedHead)
l.oldestSlot = head.Header.Slot
}
if oldHead, ok := l.heads[head.Header.Slot]; ok {
if head.SignerCount() <= oldHead.SignerCount() {
return false
}
}
l.heads[head.Header.Slot] = head
for len(l.heads) > 4 {
delete(l.heads, l.oldestSlot)
l.oldestSlot++
}
return true
}

View file

@ -0,0 +1,92 @@
// Copyright 2023 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package sync
import (
"fmt"
"math"
"sync"
"github.com/ethereum/go-ethereum/beacon/light"
"github.com/ethereum/go-ethereum/beacon/light/request"
"github.com/ethereum/go-ethereum/beacon/light/types"
)
type HeadUpdater struct {
headValidator *light.HeadValidator
chain *light.CommitteeChain
lock sync.Mutex
nextSyncPeriod uint64
queuedHeads map[*request.Server][]types.SignedHead
}
func NewHeadUpdater(headValidator *light.HeadValidator, chain *light.CommitteeChain) *HeadUpdater {
s := &HeadUpdater{
headValidator: headValidator,
chain: chain,
nextSyncPeriod: math.MaxUint64,
queuedHeads: make(map[*request.Server][]types.SignedHead),
}
return s
}
func (s *HeadUpdater) SetupTriggers(trigger func(id string, subscribe bool) *request.ModuleTrigger) {
trigger("newUpdate", true)
}
func (s *HeadUpdater) NewSignedHead(server *request.Server, signedHead types.SignedHead) {
nextPeriod, ok := s.chain.NextSyncPeriod()
if !ok || signedHead.Header.SyncPeriod() > nextPeriod {
s.lock.Lock()
s.queuedHeads[server] = append(s.queuedHeads[server], signedHead) //TODO protect against future period spam
s.lock.Unlock()
return
}
if err := s.headValidator.Add(signedHead); err != nil {
server.Fail(fmt.Sprintf("Invalid signed head: %v", err))
}
}
func (s *HeadUpdater) Process(env *request.Environment) {
s.lock.Lock()
defer s.lock.Unlock()
nextPeriod, ok := s.chain.NextSyncPeriod()
if !ok || nextPeriod == s.nextSyncPeriod {
return
}
s.nextSyncPeriod = nextPeriod
for server, queued := range s.queuedHeads {
j := len(queued)
for i := len(queued) - 1; i >= 0; i-- {
if signedHead := queued[i]; signedHead.Header.SyncPeriod() <= nextPeriod {
if err := s.headValidator.Add(signedHead); err != nil {
server.Fail(fmt.Sprintf("Invalid queued head: %v", err))
}
} else {
j--
if j != i {
queued[j] = queued[i]
}
}
}
if j != 0 {
s.queuedHeads[server] = queued[j:]
}
}
}

View file

@ -0,0 +1,182 @@
// Copyright 2023 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package sync
import (
"math"
"sync"
"github.com/ethereum/go-ethereum/beacon/light"
"github.com/ethereum/go-ethereum/beacon/light/request"
"github.com/ethereum/go-ethereum/beacon/light/types"
"github.com/ethereum/go-ethereum/common"
)
type beaconHeaderServer interface {
request.RequestServer
RequestBeaconHeader(blockRoot common.Hash, response func(*types.Header))
}
type HeaderSync struct {
lock sync.Mutex
reqLock request.MultiLock
chain *light.LightChain
prefetch bool
targetHead, syncPtr types.Header
targetTailSlot uint64
selfTrigger, chainTrigger *request.ModuleTrigger
}
func NewHeaderSync(chain *light.LightChain, prefetch bool) *HeaderSync {
return &HeaderSync{
chain: chain,
prefetch: prefetch,
targetTailSlot: math.MaxUint64,
}
}
func (s *HeaderSync) SetupTriggers(trigger func(id string, subscribe bool) *request.ModuleTrigger) {
s.selfTrigger = trigger("headerSync", true)
s.reqLock.Trigger = s.selfTrigger
trigger("validatedHead", true)
s.chainTrigger = trigger("headerChain", false)
}
func (s *HeaderSync) SetTailTarget(targetTailSlot uint64) {
s.lock.Lock()
defer s.lock.Unlock()
if targetTailSlot < s.targetTailSlot {
s.selfTrigger.Trigger()
}
s.targetTailSlot = targetTailSlot
}
func (s *HeaderSync) Process(env *request.Environment) {
s.lock.Lock()
defer s.lock.Unlock()
validatedHead := env.ValidatedHead()
if validatedHead != s.targetHead {
s.targetHead = validatedHead
s.syncPtr = validatedHead
s.chain.AddHeader(validatedHead)
}
if s.targetHead == (types.Header{}) {
return
}
chainHead, chainTail, chainInit := s.chain.HeaderRange()
if !chainInit {
s.chain.AddHeader(s.targetHead)
s.chain.SetChainHead(s.targetHead)
s.selfTrigger.Trigger()
s.chainTrigger.Trigger()
}
if s.prefetch {
if prefetchHead := env.PrefetchHead(); !s.chain.HasHeader(prefetchHead) {
s.tryPrefetchHead(env, prefetchHead)
}
}
if chainHead != s.targetHead && !s.trySyncHead(env, chainTail.Slot) {
// always prioritize syncing to the latest head, do not start tail sync until done
return
}
if s.targetTailSlot < chainTail.Slot {
s.trySyncTail(env, chainTail)
}
}
// returns true if targetHead has been reached
func (s *HeaderSync) trySyncHead(env *request.Environment, chainTailSlot uint64) bool {
for {
if s.syncPtr.Slot <= chainTailSlot || s.chain.IsCanonical(s.syncPtr) {
s.chain.SetChainHead(s.targetHead)
s.chainTrigger.Trigger()
return true
}
if parent, err := s.chain.GetHeaderByHash(s.syncPtr.ParentRoot); err == nil {
s.syncPtr = parent
} else {
s.tryRequestHeader(env, s.syncPtr.ParentRoot, false)
return false
}
}
}
func (s *HeaderSync) trySyncTail(env *request.Environment, syncTail types.Header) {
for {
if parent, err := s.chain.GetHeaderByHash(syncTail.ParentRoot); err == nil {
syncTail = parent
} else {
s.tryRequestHeader(env, syncTail.ParentRoot, false)
return
}
}
}
func (s *HeaderSync) tryPrefetchHead(env *request.Environment, head common.Hash) {
if head != (common.Hash{}) && !s.chain.HasHeader(head) {
s.tryRequestHeader(env, head, true)
}
}
func (s *HeaderSync) tryRequestHeader(env *request.Environment, blockRoot common.Hash, prefetch bool) {
if s.reqLock.CanRequest(blockRoot) {
env.TryRequest(headerRequest{
HeaderSync: s,
blockRoot: blockRoot,
prefetch: prefetch,
})
}
}
type headerRequest struct {
*HeaderSync
blockRoot common.Hash
prefetch bool
}
func (r headerRequest) CanSendTo(server *request.Server) (canSend bool, priority uint64) {
if _, ok := server.RequestServer.(beaconHeaderServer); !ok {
return false, 0
}
if !r.prefetch {
return true, 0
}
_, headRoot := server.LatestHead()
return r.blockRoot == headRoot, 0
}
func (r headerRequest) SendTo(server *request.Server) {
reqId := r.reqLock.Send(server, r.blockRoot)
server.RequestServer.(beaconHeaderServer).RequestBeaconHeader(r.blockRoot, func(header *types.Header) {
r.lock.Lock()
defer r.lock.Unlock()
r.reqLock.Returned(server, reqId, r.blockRoot)
if header == nil {
server.Fail("error retrieving beacon header")
return
}
_, oldChainTail, _ := r.chain.HeaderRange()
r.chain.AddHeader(*header)
_, chainTail, _ := r.chain.HeaderRange()
if chainTail != oldChainTail {
r.chainTrigger.Trigger() //TODO do this in a nicer way?
}
})
}

View file

@ -0,0 +1,222 @@
// Copyright 2023 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package sync
import (
"math"
"sync"
"sync/atomic"
"github.com/ethereum/go-ethereum/beacon/light"
"github.com/ethereum/go-ethereum/beacon/light/request"
"github.com/ethereum/go-ethereum/beacon/light/types"
"github.com/ethereum/go-ethereum/beacon/merkle"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
)
type beaconStateServer interface {
request.RequestServer
BeaconStateTail() uint64
RequestBeaconState(slot uint64, stateRoot common.Hash, format merkle.ProofFormat, response func(*merkle.MultiProof))
}
type StateSync struct {
lock sync.Mutex
reqLock request.MultiLock
chain *light.LightChain
prefetch bool
targetTailSlot uint64
headSyncPossible uint32
selfTrigger, headStateTrigger *request.ModuleTrigger
}
func NewStateSync(chain *light.LightChain, prefetch bool) *StateSync {
return &StateSync{
chain: chain,
prefetch: prefetch,
targetTailSlot: math.MaxUint64,
}
}
func (s *StateSync) SetupTriggers(trigger func(id string, subscribe bool) *request.ModuleTrigger) {
s.selfTrigger = trigger("stateSync", true)
s.reqLock.Trigger = s.selfTrigger
trigger("headerChain", true)
trigger("prefetchHeader", true)
s.headStateTrigger = trigger("headState", false)
}
func (s *StateSync) SetTailTarget(targetTailSlot uint64) {
s.lock.Lock()
defer s.lock.Unlock()
if targetTailSlot < s.targetTailSlot {
s.selfTrigger.Trigger()
}
s.targetTailSlot = targetTailSlot
}
func (s *StateSync) Process(env *request.Environment) {
s.lock.Lock()
defer s.lock.Unlock()
chainHead, chainTail, chainInit := s.chain.HeaderRange()
if !chainInit {
return
}
if s.prefetch {
if header, err := s.chain.GetHeaderByHash(env.PrefetchHead()); err == nil && !s.chain.HasStateProof(header) {
s.tryPrefetchHead(env, header)
}
}
stateHead, stateTail, stateInit := s.chain.StateProofRange()
if !stateInit {
s.tryRequestState(env, chainHead, false)
stateHead, stateTail = chainHead, chainHead
} else if stateHead != chainHead {
if !s.trySyncHead(env, stateHead) {
return
}
}
targetTailSlot := s.targetTailSlot
if chainTail.Slot > targetTailSlot {
targetTailSlot = chainTail.Slot
}
if targetTailSlot < stateTail.Slot {
s.trySyncTail(env, stateTail, targetTailSlot)
}
}
func (s *StateSync) trySyncHead(env *request.Environment, stateHead types.Header) bool {
slot, lastBlockRoot := stateHead.Slot, stateHead.Hash()
for {
slot++
header, err := s.chain.GetHeaderBySlot(slot)
if err == light.ErrEmptySlot {
continue
}
if err == light.ErrNotFound {
// no more canonical headers; head sync success
atomic.StoreUint32(&s.headSyncPossible, 1)
return true
}
if err != nil {
log.Error("Unexpected error during state head sync", "error", err)
return false
}
if header.ParentRoot != lastBlockRoot {
s.selfTrigger.Trigger() // reorg happened, stop and retry
return false
}
lastBlockRoot = header.Hash()
if !s.chain.HasStateProof(header) {
if sentOrLocked, tryLater := s.tryRequestState(env, header, false); !sentOrLocked {
if !tryLater {
atomic.StoreUint32(&s.headSyncPossible, 0)
}
return false
}
}
}
}
func (s *StateSync) HeadSyncPossible() bool {
return atomic.LoadUint32(&s.headSyncPossible) == 1
}
func (s *StateSync) trySyncTail(env *request.Environment, stateTail types.Header, targetTailSlot uint64) {
for stateTail.Slot > targetTailSlot {
var err error
stateTail, err = s.chain.GetParent(stateTail)
if err != nil {
log.Error("Unexpected error during state tail sync", "error", err)
return
}
if !s.chain.HasStateProof(stateTail) {
if sentOrLocked, _ := s.tryRequestState(env, stateTail, false); !sentOrLocked {
return
}
}
}
}
func (s *StateSync) tryPrefetchHead(env *request.Environment, head types.Header) {
s.tryRequestState(env, head, true)
}
// tryRequestState starts a request for the partial beacon state belonging to the
// specified header if possible. It returns true if further requests should be
// attempted (either starting this one was successful or unnecessary because it
// is already locked by a recent attempt).
func (s *StateSync) tryRequestState(env *request.Environment, header types.Header, prefetch bool) (sentOrLocked, tryLater bool) {
if !s.reqLock.CanRequest(header.StateRoot) {
return true, false
}
req := stateRequest{
StateSync: s,
header: header,
prefetch: prefetch,
}
sentOrLocked, _ = env.TryRequest(req)
if !sentOrLocked {
tryLater = env.CanRequestLater(req)
}
return
}
type stateRequest struct {
*StateSync
header types.Header
prefetch bool
}
func (r stateRequest) CanSendTo(server *request.Server) (canSend bool, priority uint64) {
if rs, ok := server.RequestServer.(beaconStateServer); !ok || r.header.Slot < rs.BeaconStateTail() {
return false, 0
}
if !r.prefetch {
return true, 0
}
_, headRoot := server.LatestHead()
return r.header.Hash() == headRoot, 0
}
func (r stateRequest) SendTo(server *request.Server) {
reqId := r.reqLock.Send(server, r.header.StateRoot)
server.RequestServer.(beaconStateServer).RequestBeaconState(r.header.Slot, r.header.StateRoot, r.chain.StateProofFormat(r.header), func(proof *merkle.MultiProof) {
r.lock.Lock()
defer r.lock.Unlock()
r.reqLock.Returned(server, reqId, r.header.StateRoot)
if proof == nil {
//server.Fail("error retrieving beacon state proof")
return
}
oldStateHead, _, _ := r.chain.StateProofRange()
if err := r.chain.AddStateProof(r.header, *proof); err != nil {
server.Fail("invalid beacon state proof: " + err.Error())
return
}
chainHead, _, _ := r.chain.HeaderRange()
stateHead, _, _ := r.chain.StateProofRange()
if stateHead == chainHead && oldStateHead != chainHead {
r.headStateTrigger.Trigger()
}
})
}

View file

@ -35,14 +35,14 @@ type checkpointInitServer interface {
} }
type CheckpointInit struct { type CheckpointInit struct {
request.SingleLock
lock sync.Mutex lock sync.Mutex
reqLock request.SingleLock
chain *light.CommitteeChain chain *light.CommitteeChain
cs *light.CheckpointStore cs *light.CheckpointStore
checkpointHash common.Hash checkpointHash common.Hash
initialized bool initialized bool
InitTrigger request.ModuleTrigger initTrigger *request.ModuleTrigger
} }
func NewCheckpointInit(chain *light.CommitteeChain, cs *light.CheckpointStore, checkpointHash common.Hash) *CheckpointInit { func NewCheckpointInit(chain *light.CommitteeChain, cs *light.CheckpointStore, checkpointHash common.Hash) *CheckpointInit {
@ -53,129 +53,149 @@ func NewCheckpointInit(chain *light.CommitteeChain, cs *light.CheckpointStore, c
} }
} }
func (s *CheckpointInit) Process(servers []*request.Server) bool { func (s *CheckpointInit) SetupTriggers(trigger func(id string, subscribe bool) *request.ModuleTrigger) {
s.reqLock.Trigger = trigger("checkpointInit", true)
s.initTrigger = trigger("committeeChainInit", false)
}
func (s *CheckpointInit) Process(env *request.Environment) {
s.lock.Lock() s.lock.Lock()
defer s.lock.Unlock() defer s.lock.Unlock()
if s.initialized { if s.initialized {
return false return
} }
if checkpoint := s.cs.Get(s.checkpointHash); checkpoint != nil { if checkpoint := s.cs.Get(s.checkpointHash); checkpoint != nil {
checkpoint.InitChain(s.chain) checkpoint.InitChain(s.chain)
s.initialized = true s.initialized = true
s.InitTrigger.Trigger() s.initTrigger.Trigger()
return false return
} }
srv := request.SelectServer(servers, func(server *request.Server) uint64 { if s.reqLock.CanRequest() {
if cserver, ok := server.RequestServer.(checkpointInitServer); ok && cserver.CanRequestBootstrap() && s.CanSend(server) { env.TryRequest(checkpointRequest{
return 1 CheckpointInit: s,
} checkpointHash: s.checkpointHash,
return 0
}) })
if srv == nil {
return true
} }
reqId, ok := s.TrySend(srv)
if !ok {
return true
} }
server := srv.RequestServer.(checkpointInitServer)
server.RequestBootstrap(s.checkpointHash, func(checkpoint *light.CheckpointData) {
s.lock.Lock()
defer s.lock.Unlock()
s.Returned(srv, reqId) type checkpointRequest struct {
*CheckpointInit
checkpointHash common.Hash
}
func (r checkpointRequest) CanSendTo(server *request.Server) (canSend bool, priority uint64) {
if cs, ok := server.RequestServer.(checkpointInitServer); !ok || !cs.CanRequestBootstrap() {
return false, 0
}
return true, 0
}
func (r checkpointRequest) SendTo(server *request.Server) {
reqId := r.reqLock.Send(server)
server.RequestServer.(checkpointInitServer).RequestBootstrap(r.checkpointHash, func(checkpoint *light.CheckpointData) {
r.lock.Lock()
defer r.lock.Unlock()
r.reqLock.Returned(server, reqId)
if checkpoint == nil || !checkpoint.Validate() { if checkpoint == nil || !checkpoint.Validate() {
server.Fail("error retrieving checkpoint data") server.Fail("error retrieving checkpoint data")
return return
} }
checkpoint.InitChain(s.chain) checkpoint.InitChain(r.chain)
s.cs.Store(checkpoint) r.cs.Store(checkpoint)
s.initialized = true r.initialized = true
s.InitTrigger.Trigger() r.initTrigger.Trigger()
}) })
return true
} }
type forwardUpdateServer interface { type updateServer interface {
request.RequestServer request.RequestServer
UpdateRange() types.PeriodRange UpdateRange() types.PeriodRange
RequestUpdates(first, count uint64, response func([]*types.LightClientUpdate, []*types.SerializedCommittee)) RequestUpdates(first, count uint64, response func([]*types.LightClientUpdate, []*types.SerializedCommittee))
} }
type ForwardUpdateSyncer struct { type ForwardUpdateSync struct {
request.SingleLock
lock sync.Mutex lock sync.Mutex
reqLock request.SingleLock
chain *light.CommitteeChain chain *light.CommitteeChain
NewUpdateTrigger request.ModuleTrigger newUpdateTrigger *request.ModuleTrigger
} }
func NewForwardUpdateSyncer(chain *light.CommitteeChain) *ForwardUpdateSyncer { func NewForwardUpdateSync(chain *light.CommitteeChain) *ForwardUpdateSync {
return &ForwardUpdateSyncer{chain: chain} return &ForwardUpdateSync{chain: chain}
} }
func (s *ForwardUpdateSyncer) Process(servers []*request.Server) bool { func (s *ForwardUpdateSync) SetupTriggers(trigger func(id string, subscribe bool) *request.ModuleTrigger) {
s.reqLock.Trigger = trigger("forwardUpdateSync", true)
trigger("committeeChainInit", true)
trigger("validatedHead", true)
s.newUpdateTrigger = trigger("newUpdate", true)
}
func (s *ForwardUpdateSync) Process(env *request.Environment) {
s.lock.Lock() s.lock.Lock()
defer s.lock.Unlock() defer s.lock.Unlock()
first, ok := s.chain.NextSyncPeriod() first, ok := s.chain.NextSyncPeriod()
if !ok { if !ok {
return true return
} }
srv := request.SelectServer(servers, func(server *request.Server) uint64 { env.TryRequest(updateRequest{
if fserver, ok := server.RequestServer.(forwardUpdateServer); ok && s.CanSend(server) { ForwardUpdateSync: s,
updateRange := fserver.UpdateRange() first: first,
if first < updateRange.First {
return 0
}
return updateRange.AfterLast
}
return 0
}) })
if srv == nil {
return true
} }
server := srv.RequestServer.(forwardUpdateServer)
updateRange := server.UpdateRange() type updateRequest struct {
if updateRange.AfterLast <= first { *ForwardUpdateSync
return true first uint64
} }
reqId, ok := s.TrySend(srv)
if !ok { func (r updateRequest) CanSendTo(server *request.Server) (canSend bool, priority uint64) {
return true if us, ok := server.RequestServer.(updateServer); ok {
if updateRange := us.UpdateRange(); updateRange.Includes(r.first) {
return true, updateRange.AfterLast
} }
count := updateRange.AfterLast - first }
if count > maxUpdateRequest { //TODO const return false, 0
}
func (r updateRequest) SendTo(server *request.Server) {
us := server.RequestServer.(updateServer)
updateRange := us.UpdateRange()
count := updateRange.AfterLast - r.first
if count > maxUpdateRequest {
count = maxUpdateRequest count = maxUpdateRequest
} }
server.RequestUpdates(first, count, func(updates []*types.LightClientUpdate, committees []*types.SerializedCommittee) { reqId := r.reqLock.Send(server)
s.lock.Lock() us.RequestUpdates(r.first, count, func(updates []*types.LightClientUpdate, committees []*types.SerializedCommittee) {
defer s.lock.Unlock() r.lock.Lock()
defer r.lock.Unlock()
s.Returned(srv, reqId) r.reqLock.Returned(server, reqId)
if len(updates) != int(count) || len(committees) != int(count) { if len(updates) != int(count) || len(committees) != int(count) {
server.Fail("wrong number of updates received") server.Fail("wrong number of updates received")
return return
} }
for i, update := range updates { for i, update := range updates {
if update.Header.SyncPeriod() != first+uint64(i) { if update.Header.SyncPeriod() != r.first+uint64(i) {
server.Fail("update with wrong sync period received") server.Fail("update with wrong sync period received")
return return
} }
if err := s.chain.InsertUpdate(update, committees[i]); err != nil { if err := r.chain.InsertUpdate(update, committees[i]); err != nil {
if err == light.ErrInvalidUpdate || err == light.ErrWrongCommitteeRoot || err == light.ErrCannotReorg { if err == light.ErrInvalidUpdate || err == light.ErrWrongCommitteeRoot || err == light.ErrCannotReorg {
server.Fail("invalid update received") server.Fail("invalid update received")
} else { } else {
log.Error("Unexpected InsertUpdate error", "error", err) log.Error("Unexpected InsertUpdate error", "error", err)
} }
if i != 0 { if i != 0 { // some updates were added
s.NewUpdateTrigger.Trigger() r.newUpdateTrigger.Trigger()
} }
return return
} }
} }
s.NewUpdateTrigger.Trigger() r.newUpdateTrigger.Trigger()
}) })
return true
} }

View file

@ -69,6 +69,16 @@ type ProofFormat interface {
Children() (left, right ProofFormat) // either both or neither should be nil Children() (left, right ProofFormat) // either both or neither should be nil
} }
// IsEqual returns true if the two formats are the same
func IsEqual(a, b ProofFormat) bool {
al, ar := a.Children()
bl, br := b.Children()
if al == nil || bl == nil {
return al == nil && bl == nil
}
return IsEqual(al, bl) && IsEqual(ar, br)
}
// ProofReader allows traversing and reading a tree structure or a subset of it. // ProofReader allows traversing and reading a tree structure or a subset of it.
// Note: the hash of each traversed node is always requested. If the internal // Note: the hash of each traversed node is always requested. If the internal
// hash is not available then subtrees are always traversed (first left, then right). // hash is not available then subtrees are always traversed (first left, then right).

307
cmd/blsync/block_sync.go Executable file
View file

@ -0,0 +1,307 @@
// Copyright 2023 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package main
import (
"fmt"
"math/big"
"sync"
"github.com/ethereum/go-ethereum/beacon/light"
"github.com/ethereum/go-ethereum/beacon/light/request"
lsync "github.com/ethereum/go-ethereum/beacon/light/sync"
"github.com/ethereum/go-ethereum/beacon/light/types"
"github.com/ethereum/go-ethereum/beacon/merkle"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/lru"
ctypes "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/rpc"
"github.com/ethereum/go-ethereum/trie"
"github.com/holiman/uint256"
"github.com/protolambda/zrnt/eth2/beacon/capella"
"github.com/protolambda/zrnt/eth2/configs"
"github.com/protolambda/ztyp/tree"
)
const reverseSyncHeaders = 128
type beaconBlockServer interface {
request.RequestServer
RequestBeaconBlock(blockRoot common.Hash, response func(*capella.BeaconBlock))
}
type beaconStateServer interface {
request.RequestServer
BeaconStateTail() uint64
RequestBeaconState(slot uint64, stateRoot common.Hash, format merkle.ProofFormat, response func(merkle.MultiProof))
}
type beaconBlockSync struct {
lock sync.Mutex
reqLock request.MultiLock
recentBlocks *lru.Cache[common.Hash, *capella.BeaconBlock]
headUpdater *lsync.HeadUpdater
lightChain *light.LightChain
validatedHead types.Header
headBlock *capella.BeaconBlock // belongs to validatedHead (or nil)
headBlockTrigger, prefetchHeaderTrigger *request.ModuleTrigger
}
func newBeaconBlockSyncer(lightChain *light.LightChain) *beaconBlockSync {
return &beaconBlockSync{
lightChain: lightChain,
recentBlocks: lru.NewCache[common.Hash, *capella.BeaconBlock](10),
}
}
func (s *beaconBlockSync) SetupTriggers(trigger func(id string, subscribe bool) *request.ModuleTrigger) {
s.reqLock.Trigger = trigger("beaconBlockSync", true)
trigger("validatedHead", true)
s.headBlockTrigger = trigger("headBlock", false)
s.prefetchHeaderTrigger = trigger("prefetchHeader", false)
}
func (s *beaconBlockSync) Process(env *request.Environment) {
s.lock.Lock()
defer s.lock.Unlock()
validatedHead := env.ValidatedHead()
if validatedHead == (types.Header{}) {
return
}
if validatedHead != s.validatedHead {
s.validatedHead = validatedHead
s.headBlock = nil
if block, ok := s.recentBlocks.Get(validatedHead.Hash()); ok {
s.headBlock = block
s.headBlockTrigger.Trigger()
}
}
if s.headBlock == nil && s.validatedHead != (types.Header{}) {
if !s.tryRequestBlock(env, s.validatedHead.Hash(), false) {
return
}
}
prefetchHead := env.PrefetchHead()
if _, ok := s.recentBlocks.Get(prefetchHead); !ok {
s.tryRequestBlock(env, prefetchHead, true)
}
}
func (s *beaconBlockSync) getHeadBlock() *capella.BeaconBlock {
s.lock.Lock()
defer s.lock.Unlock()
return s.headBlock
}
func (s *beaconBlockSync) tryRequestBlock(env *request.Environment, blockRoot common.Hash, prefetch bool) bool {
if !s.reqLock.CanRequest(blockRoot) {
return true
}
_, tryMore := env.TryRequest(blockRequest{
beaconBlockSync: s,
blockRoot: blockRoot,
prefetch: prefetch,
})
return tryMore
}
type blockRequest struct {
*beaconBlockSync
blockRoot common.Hash
prefetch bool
}
func (r blockRequest) CanSendTo(server *request.Server) (canSend bool, priority uint64) {
if _, ok := server.RequestServer.(beaconBlockServer); !ok {
return false, 0
}
if !r.prefetch {
return true, 0
}
_, headRoot := server.LatestHead()
return r.blockRoot == headRoot, 0
}
func (r blockRequest) SendTo(server *request.Server) {
reqId := r.reqLock.Send(server, r.blockRoot)
server.RequestServer.(beaconBlockServer).RequestBeaconBlock(r.blockRoot, func(block *capella.BeaconBlock) {
r.lock.Lock()
defer r.lock.Unlock()
r.reqLock.Returned(server, reqId, r.blockRoot)
if block == nil {
server.Fail("error retrieving beacon block")
return
}
r.recentBlocks.Add(r.blockRoot, block)
if !r.lightChain.HasHeader(r.blockRoot) {
r.lightChain.AddHeader(types.Header{
Slot: uint64(block.Slot),
ProposerIndex: uint64(block.ProposerIndex),
ParentRoot: common.Hash(block.ParentRoot),
StateRoot: common.Hash(block.StateRoot),
BodyRoot: common.Hash(block.Body.HashTreeRoot(configs.Mainnet, tree.GetHashFn())),
})
r.prefetchHeaderTrigger.Trigger()
}
if r.validatedHead.Hash() == r.blockRoot {
r.headBlock = block
r.headBlockTrigger.Trigger()
}
})
}
func getExecBlock(beaconBlock *capella.BeaconBlock) (*ctypes.Block, error) {
payload := &beaconBlock.Body.ExecutionPayload
txs := make([]*ctypes.Transaction, len(payload.Transactions))
for i, opaqueTx := range payload.Transactions {
var tx ctypes.Transaction
if err := tx.UnmarshalBinary(opaqueTx); err != nil {
return nil, fmt.Errorf("failed to parse tx %d: %v", i, err)
}
txs[i] = &tx
}
withdrawals := make([]*ctypes.Withdrawal, len(payload.Withdrawals))
for i, w := range payload.Withdrawals {
withdrawals[i] = &ctypes.Withdrawal{
Index: uint64(w.Index),
Validator: uint64(w.ValidatorIndex),
Address: common.Address(w.Address),
Amount: uint64(w.Amount),
}
}
wroot := ctypes.DeriveSha(ctypes.Withdrawals(withdrawals), trie.NewStackTrie(nil))
execHeader := &ctypes.Header{
ParentHash: common.Hash(payload.ParentHash),
UncleHash: ctypes.EmptyUncleHash,
Coinbase: common.Address(payload.FeeRecipient),
Root: common.Hash(payload.StateRoot),
TxHash: ctypes.DeriveSha(ctypes.Transactions(txs), trie.NewStackTrie(nil)),
ReceiptHash: common.Hash(payload.ReceiptsRoot),
Bloom: ctypes.Bloom(payload.LogsBloom),
Difficulty: common.Big0,
Number: new(big.Int).SetUint64(uint64(payload.BlockNumber)),
GasLimit: uint64(payload.GasLimit),
GasUsed: uint64(payload.GasUsed),
Time: uint64(payload.Timestamp),
Extra: []byte(payload.ExtraData),
MixDigest: common.Hash(payload.PrevRandao), // reused in merge
Nonce: ctypes.BlockNonce{}, // zero
BaseFee: (*uint256.Int)(&payload.BaseFeePerGas).ToBig(),
WithdrawalsHash: &wroot,
}
execBlock := ctypes.NewBlockWithHeader(execHeader).WithBody(txs, nil).WithWithdrawals(withdrawals)
if execBlockHash := execBlock.Hash(); execBlockHash != common.Hash(payload.BlockHash) {
return nil, fmt.Errorf("Sanity check failed, payload hash does not match (expected %x, got %x)", common.Hash(payload.BlockHash), execBlockHash)
}
return execBlock, nil
}
type engineApiUpdater struct {
client *rpc.Client
lock sync.Mutex
lastHead common.Hash
headerSync *lsync.HeaderSync
stateSync *lsync.StateSync
blockSync *beaconBlockSync
chain *light.LightChain
updating bool
selfTrigger *request.ModuleTrigger
}
func (s *engineApiUpdater) SetupTriggers(trigger func(id string, subscribe bool) *request.ModuleTrigger) {
trigger("headBlock", true)
trigger("headState", true)
s.selfTrigger = trigger("engineApiUpdater", true)
}
func (s *engineApiUpdater) Process(env *request.Environment) {
s.lock.Lock()
defer s.lock.Unlock()
if s.updating {
return
}
headBlock := s.blockSync.getHeadBlock()
if headBlock == nil {
return
}
headRoot := common.Hash(headBlock.HashTreeRoot(configs.Mainnet, tree.GetHashFn()))
if headRoot == s.lastHead {
return
}
if headBlock.Slot > reverseSyncHeaders {
s.headerSync.SetTailTarget(uint64(headBlock.Slot) - reverseSyncHeaders)
} else {
s.headerSync.SetTailTarget(0)
}
head, err := s.chain.GetHeaderByHash(headRoot)
if err != nil {
return
}
var finalizedExecRoot common.Hash
if state, err := s.chain.GetStateProof(head); err == nil {
finalizedRoot := common.Hash(state.Values[finalizedBlockIndex])
if finalized, err := s.chain.GetHeaderByHash(finalizedRoot); err == nil {
if finalizedState, err := s.chain.GetStateProof(finalized); err == nil {
finalizedExecRoot = common.Hash(finalizedState.Values[execBlockIndex])
}
}
} else {
if s.stateSync.HeadSyncPossible() {
return
}
}
s.lastHead = headRoot
execBlock, err := getExecBlock(headBlock)
if err != nil {
log.Error("Error extracting execution block from validated beacon block", "error", err)
return
}
execRoot := execBlock.Hash()
if s.client == nil { // dry run, no engine API specified
log.Info("New execution block retrieved", "block number", execBlock.NumberU64(), "block hash", execRoot, "finalized block hash", finalizedExecRoot)
} else {
s.updating = true
go func() {
if status, err := callNewPayloadV1(s.client, execBlock); err == nil {
log.Info("Successful NewPayload", "block number", execBlock.NumberU64(), "block hash", execRoot, "status", status)
} else {
log.Error("Failed NewPayload", "block number", execBlock.NumberU64(), "block hash", execRoot, "error", err)
}
if status, err := callForkchoiceUpdatedV1(s.client, execRoot, finalizedExecRoot); err == nil {
log.Info("Successful ForkchoiceUpdated", "head", execRoot, "finalized", finalizedExecRoot, "status", status)
} else {
log.Error("Failed ForkchoiceUpdated", "head", execRoot, "finalized", finalizedExecRoot, "error", err)
}
s.lock.Lock()
s.updating = false
s.selfTrigger.Trigger()
s.lock.Unlock()
}()
}
}

View file

@ -33,7 +33,6 @@ import (
"github.com/ethereum/go-ethereum/beacon/params" "github.com/ethereum/go-ethereum/beacon/params"
"github.com/ethereum/go-ethereum/cmd/utils" "github.com/ethereum/go-ethereum/cmd/utils"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/lru"
"github.com/ethereum/go-ethereum/common/mclock" "github.com/ethereum/go-ethereum/common/mclock"
ctypes "github.com/ethereum/go-ethereum/core/types" ctypes "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethdb/memorydb" "github.com/ethereum/go-ethereum/ethdb/memorydb"
@ -97,34 +96,55 @@ func blsync(ctx *cli.Context) error {
customHeader[strings.TrimSpace(kv[0])] = strings.TrimSpace(kv[1]) customHeader[strings.TrimSpace(kv[0])] = strings.TrimSpace(kv[1])
} }
// create data structures
var ( var (
beaconApi = api.NewBeaconLightApi(ctx.String(utils.BeaconApiFlag.Name), customHeader)
db = memorydb.New() db = memorydb.New()
threshold = ctx.Int(utils.BeaconThresholdFlag.Name) threshold = ctx.Int(utils.BeaconThresholdFlag.Name)
committeeChain = light.NewCommitteeChain(db, chainConfig.Forks, threshold, !ctx.Bool(utils.BeaconNoFilterFlag.Name), light.BLSVerifier{}, &mclock.System{}, func() int64 { return time.Now().UnixNano() }) committeeChain = light.NewCommitteeChain(db, chainConfig.Forks, threshold, !ctx.Bool(utils.BeaconNoFilterFlag.Name), light.BLSVerifier{}, &mclock.System{}, func() int64 { return time.Now().UnixNano() })
checkpointStore = light.NewCheckpointStore(db, committeeChain) checkpointStore = light.NewCheckpointStore(db, committeeChain)
headTracker = light.NewHeadTracker(committeeChain) headValidator = light.NewHeadValidator(committeeChain)
scheduler = request.NewScheduler() lightChain = light.NewLightChain(db, stateProofFormat)
) )
committeeChain.SetGenesisData(chainConfig.GenesisData) committeeChain.SetGenesisData(chainConfig.GenesisData)
headUpdater := sync.NewHeadUpdater(headValidator, committeeChain)
headTracker := request.NewHeadTracker(headUpdater.NewSignedHead)
headValidator.Subscribe(threshold, func(signedHead types.SignedHead) {
headTracker.SetValidatedHead(signedHead.Header)
})
// create sync modules
checkpointInit := sync.NewCheckpointInit(committeeChain, checkpointStore, chainConfig.Checkpoint) checkpointInit := sync.NewCheckpointInit(committeeChain, checkpointStore, chainConfig.Checkpoint)
forwardSync := sync.NewForwardUpdateSyncer(committeeChain) forwardSync := sync.NewForwardUpdateSync(committeeChain)
headSync := sync.NewHeadSyncer(headTracker, committeeChain) headerSync := sync.NewHeaderSync(lightChain, false)
stateSync := sync.NewStateSync(lightChain, true)
beaconBlockSync := newBeaconBlockSyncer(lightChain)
engineApiUpdater := &engineApiUpdater{ //TODO constructor
client: makeRPCClient(ctx),
headerSync: headerSync,
stateSync: stateSync,
blockSync: beaconBlockSync,
chain: lightChain,
}
// set up sync modules and triggers
scheduler := request.NewScheduler(headTracker)
headTracker.SetupTriggers(scheduler.GetModuleTrigger)
scheduler.RegisterModule(checkpointInit) scheduler.RegisterModule(checkpointInit)
scheduler.RegisterModule(forwardSync) scheduler.RegisterModule(forwardSync)
scheduler.RegisterModule(headSync) scheduler.RegisterModule(headUpdater)
scheduler.AddTriggers(forwardSync, []*request.ModuleTrigger{&checkpointInit.InitTrigger, &forwardSync.NewUpdateTrigger, &headSync.SignedHeadTrigger}) scheduler.RegisterModule(beaconBlockSync)
scheduler.AddTriggers(headSync, []*request.ModuleTrigger{&forwardSync.NewUpdateTrigger}) scheduler.RegisterModule(engineApiUpdater)
scheduler.RegisterModule(stateSync)
syncer := &execSyncer{ scheduler.RegisterModule(headerSync)
api: beaconApi, // start
client: makeRPCClient(ctx),
execRootCache: lru.NewCache[common.Hash, common.Hash](1000),
}
headTracker.Subscribe(threshold, syncer.newHead)
scheduler.Start() scheduler.Start()
stateSync.SetTailTarget(0)
// register server(s)
for _, url := range utils.SplitAndTrim(ctx.String(utils.BeaconApiFlag.Name)) {
beaconApi := api.NewBeaconLightApi(url, customHeader)
scheduler.RegisterServer(api.NewSyncServer(beaconApi)) scheduler.RegisterServer(api.NewSyncServer(beaconApi))
}
// run until stopped
<-ctx.Done() <-ctx.Done()
scheduler.Stop() scheduler.Stop()
return nil return nil
@ -150,85 +170,3 @@ func callForkchoiceUpdatedV1(client *rpc.Client, headHash, finalizedHash common.
cancel() cancel()
return resp.PayloadStatus.Status, err return resp.PayloadStatus.Status, err
} }
type execSyncer struct {
api *api.BeaconLightApi
sub *api.StateProofSub
client *rpc.Client
execRootCache *lru.Cache[common.Hash, common.Hash] // beacon block root -> execution block root
}
// newHead fetches state proofs to determine the execution block root and calls
// the engine API if specified
func (e *execSyncer) newHead(signedHead types.SignedHead) {
head := signedHead.Header
log.Info("Received new beacon head", "slot", head.Slot, "blockRoot", head.Hash())
block, err := e.api.GetExecutionPayload(head)
if err != nil {
log.Error("Error fetching execution payload from beacon API", "error", err)
return
}
blockRoot := block.Hash()
var finalizedExecRoot common.Hash
if e.sub == nil {
if sub, err := e.api.SubscribeStateProof(stateProofFormat, 0, 1); err == nil {
log.Info("Successfully created beacon state subscription")
e.sub = sub
} else {
log.Error("Failed to create beacon state subscription", "error", err)
return
}
}
proof, err := e.sub.Get(head.StateRoot)
if err == nil {
var (
execBlockRoot = common.Hash(proof.Values[execBlockIndex])
finalizedBeaconRoot = common.Hash(proof.Values[finalizedBlockIndex])
beaconRoot = head.Hash()
)
e.execRootCache.Add(beaconRoot, execBlockRoot)
if blockRoot != execBlockRoot {
log.Error("Execution payload block hash does not match value in beacon state", "expected", execBlockRoot, "got", block.Hash())
return
}
if _, ok := e.execRootCache.Get(head.ParentRoot); !ok {
e.fetchExecRoots(head.ParentRoot)
}
finalizedExecRoot, _ = e.execRootCache.Get(finalizedBeaconRoot)
} else if err != api.ErrNotFound {
log.Error("Error fetching state proof from beacon API", "error", err)
}
if e.client == nil { // dry run, no engine API specified
log.Info("New execution block retrieved", "block number", block.NumberU64(), "block hash", blockRoot, "finalized block hash", finalizedExecRoot)
return
}
if status, err := callNewPayloadV1(e.client, block); err == nil {
log.Info("Successful NewPayload", "block number", block.NumberU64(), "block hash", blockRoot, "status", status)
} else {
log.Error("Failed NewPayload", "block number", block.NumberU64(), "block hash", blockRoot, "error", err)
}
if status, err := callForkchoiceUpdatedV1(e.client, blockRoot, finalizedExecRoot); err == nil {
log.Info("Successful ForkchoiceUpdated", "head", blockRoot, "finalized", finalizedExecRoot, "status", status)
} else {
log.Error("Failed ForkchoiceUpdated", "head", blockRoot, "finalized", finalizedExecRoot, "error", err)
}
}
func (e *execSyncer) fetchExecRoots(blockRoot common.Hash) {
for maxFetch := 256; maxFetch > 0; maxFetch-- {
header, err := e.api.GetHeader(blockRoot)
if err != nil {
break
}
proof, err := e.sub.Get(header.StateRoot)
if err != nil {
// exit silently because we expect running into an error when parent is unknown
break
}
e.execRootCache.Add(header.Hash(), common.Hash(proof.Values[execBlockIndex]))
if _, ok := e.execRootCache.Get(header.ParentRoot); ok {
break
}
blockRoot = header.ParentRoot
}
}