diff --git a/beacon/light/api/light_api.go b/beacon/light/api/light_api.go
new file mode 100644
index 0000000000..0c4fc14a3c
--- /dev/null
+++ b/beacon/light/api/light_api.go
@@ -0,0 +1,489 @@
+// Copyright 2022 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 detaiapi.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package api
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "math/big"
+ "net/http"
+ "time"
+
+ "github.com/donovanhide/eventsource"
+ "github.com/ethereum/go-ethereum/beacon/light"
+ "github.com/ethereum/go-ethereum/beacon/light/types"
+ "github.com/ethereum/go-ethereum/beacon/merkle"
+ "github.com/ethereum/go-ethereum/beacon/params"
+ "github.com/ethereum/go-ethereum/common"
+ "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/configs"
+ "github.com/protolambda/ztyp/tree"
+)
+
+var (
+ ErrNotFound = errors.New("404 Not Found")
+ ErrInternal = errors.New("500 Internal Server Error")
+)
+
+// BeaconLightApi requests light client information from a beacon node REST API.
+// Note: all required API endpoints are currently only implemented by Lodestar.
+type BeaconLightApi struct {
+ url string
+ client *http.Client
+ customHeaders map[string]string
+}
+
+func NewBeaconLightApi(url string, customHeaders map[string]string) *BeaconLightApi {
+ return &BeaconLightApi{
+ url: url,
+ client: &http.Client{
+ Timeout: time.Second * 10,
+ },
+ customHeaders: customHeaders,
+ }
+}
+
+func (api *BeaconLightApi) httpGet(path string) ([]byte, error) {
+ req, err := http.NewRequest("GET", api.url+path, nil)
+ if err != nil {
+ return nil, err
+ }
+ for k, v := range api.customHeaders {
+ req.Header.Set(k, v)
+ }
+ resp, err := api.client.Do(req)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ switch resp.StatusCode {
+ case 200:
+ return io.ReadAll(resp.Body)
+ case 404:
+ return nil, ErrNotFound
+ case 500:
+ return nil, ErrInternal
+ default:
+ return nil, fmt.Errorf("Unexpected error from API endpoint \"%s\": status code %d", path, resp.StatusCode)
+ }
+}
+
+func (api *BeaconLightApi) httpGetf(format string, params ...any) ([]byte, error) {
+ return api.httpGet(fmt.Sprintf(format, params...))
+}
+
+// GetBestUpdateAndCommittee fetches and validates LightClientUpdate for given
+// period and full serialized committee for the next period (committee root hash
+// equals update.NextSyncCommitteeRoot).
+// Note that the results are validated but the update signature should be verified
+// by the caller as its validity depends on the update chain.
+//TODO handle valid partial results
+func (api *BeaconLightApi) GetBestUpdatesAndCommittees(firstPeriod, count uint64) ([]*types.LightClientUpdate, []*types.SerializedCommittee, error) {
+ resp, err := api.httpGetf("/eth/v1/beacon/light_client/updates?start_period=%d&count=%d", firstPeriod, count)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ var data []types.CommitteeUpdate
+ if err := json.Unmarshal(resp, &data); err != nil {
+ return nil, nil, err
+ }
+ if len(data) != int(count) {
+ return nil, nil, errors.New("invalid number of committee updates")
+ }
+ updates := make([]*types.LightClientUpdate, int(count))
+ committees := make([]*types.SerializedCommittee, int(count))
+ for i, d := range data {
+ if d.Update.Header.SyncPeriod() != firstPeriod+uint64(i) {
+ return nil, nil, errors.New("wrong committee update header period")
+ }
+ if err := d.Update.Validate(); err != nil {
+ return nil, nil, err
+ }
+ if d.NextSyncCommittee.Root() != d.Update.NextSyncCommitteeRoot {
+ return nil, nil, errors.New("wrong sync committee root")
+ }
+ updates[i], committees[i] = d.Update, d.NextSyncCommittee
+ }
+ return updates, committees, nil
+}
+
+// GetOptimisticHeadUpdate fetches a signed header based on the latest available
+// optimistic update. Note that the signature should be verified by the caller
+// as its validity depends on the update chain.
+//
+// See data structure definition here:
+// https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/sync-protocol.md#lightclientoptimisticupdate
+func (api *BeaconLightApi) GetOptimisticHeadUpdate() (types.SignedHead, error) {
+ resp, err := api.httpGet("/eth/v1/beacon/light_client/optimistic_update")
+ if err != nil {
+ return types.SignedHead{}, err
+ }
+ return decodeOptimisticHeadUpdate(resp)
+}
+
+func decodeOptimisticHeadUpdate(enc []byte) (types.SignedHead, error) {
+ var data struct {
+ Data struct {
+ Header types.JsonBeaconHeader `json:"attested_header"`
+ Aggregate types.SyncAggregate `json:"sync_aggregate"`
+ SignatureSlot common.Decimal `json:"signature_slot"`
+ } `json:"data"`
+ }
+ if err := json.Unmarshal(enc, &data); err != nil {
+ return types.SignedHead{}, err
+ }
+ if data.Data.Header.Beacon.StateRoot == (common.Hash{}) {
+ // workaround for different event encoding format in Lodestar
+ if err := json.Unmarshal(enc, &data.Data); err != nil {
+ return types.SignedHead{}, err
+ }
+ }
+
+ if len(data.Data.Aggregate.BitMask) != params.SyncCommitteeBitmaskSize {
+ return types.SignedHead{}, errors.New("invalid sync_committee_bits length")
+ }
+ if len(data.Data.Aggregate.Signature) != params.BlsSignatureSize {
+ return types.SignedHead{}, errors.New("invalid sync_committee_signature length")
+ }
+ return types.SignedHead{
+ Header: data.Data.Header.Beacon,
+ SyncAggregate: data.Data.Aggregate,
+ SignatureSlot: uint64(data.Data.SignatureSlot),
+ }, nil
+}
+
+// GetHead fetches and validates the beacon header with the given blockRoot.
+// If blockRoot is null hash then the latest head header is fetched.
+func (api *BeaconLightApi) GetHeader(blockRoot common.Hash) (types.Header, error) {
+ var blockId string
+ if blockRoot == (common.Hash{}) {
+ blockId = "head"
+ } else {
+ blockId = blockRoot.Hex()
+ }
+ resp, err := api.httpGetf("/eth/v1/beacon/headers/%s", blockId)
+ if err != nil {
+ return types.Header{}, err
+ }
+
+ var data struct {
+ Data struct {
+ Root common.Hash `json:"root"`
+ Canonical bool `json:"canonical"`
+ Header struct {
+ Message types.Header `json:"message"`
+ Signature hexutil.Bytes `json:"signature"`
+ } `json:"header"`
+ } `json:"data"`
+ }
+ if err := json.Unmarshal(resp, &data); err != nil {
+ return types.Header{}, err
+ }
+ header := data.Data.Header.Message
+ if blockRoot == (common.Hash{}) {
+ blockRoot = data.Data.Root
+ }
+ if header.Hash() != blockRoot {
+ return types.Header{}, errors.New("retrieved beacon header root does not match")
+ }
+ return header, nil
+}
+
+// does not verify state root
+func (api *BeaconLightApi) GetHeadStateProof(format merkle.ProofFormat) (merkle.MultiProof, error) {
+ encFormat, bitLength := EncodeCompactProofFormat(format)
+ return api.getStateProof("head", format, encFormat, bitLength)
+}
+
+type StateProofSub struct {
+ api *BeaconLightApi
+ format merkle.ProofFormat
+ 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 {
+ return merkle.MultiProof{}, err
+ }
+ if proof.RootHash() != stateRoot {
+ return merkle.MultiProof{}, errors.New("Received proof has incorrect state root")
+ }
+ return proof, nil
+}
+
+func (api *BeaconLightApi) getStateProof(stateId string, format merkle.ProofFormat, encFormat []byte, bitLength int) (merkle.MultiProof, error) {
+ resp, err := api.httpGetf("/eth/v0/beacon/proof/state/%s?format=0x%x", stateId, encFormat)
+ if err != nil {
+ return merkle.MultiProof{}, err
+ }
+ valueCount := (bitLength + 1) / 2
+ if len(resp) != valueCount*32 {
+ return merkle.MultiProof{}, errors.New("Invalid state proof length")
+ }
+ values := make(merkle.Values, valueCount)
+ for i := range values {
+ copy(values[i][:], resp[i*32:(i+1)*32])
+ }
+ return merkle.MultiProof{Format: format, Values: values}, nil
+}
+
+// EncodeCompactProofFormat encodes a merkle.ProofFormat into a binary compact
+// proof format. See description here:
+// https://github.com/ChainSafe/consensus-specs/blob/feat/multiproof/ssz/merkle-proofs.md#compact-multiproofs
+func EncodeCompactProofFormat(format merkle.ProofFormat) ([]byte, int) {
+ target := make([]byte, 0, 64)
+ var bitLength int
+ encodeProofFormatSubtree(format, &target, &bitLength)
+ return target, bitLength
+}
+
+// encodeProofFormatSubtree recursively encodes a subtree of a proof format into
+// binary compact format.
+func encodeProofFormatSubtree(format merkle.ProofFormat, target *[]byte, bitLength *int) {
+ bytePtr, bitMask := *bitLength>>3, byte(128)>>(*bitLength&7)
+ *bitLength++
+ if bytePtr == len(*target) {
+ *target = append(*target, byte(0))
+ }
+ if left, right := format.Children(); left == nil {
+ (*target)[bytePtr] += bitMask
+ } else {
+ encodeProofFormatSubtree(left, target, bitLength)
+ encodeProofFormatSubtree(right, target, bitLength)
+ }
+}
+
+// GetCheckpointData fetches and validates bootstrap data belonging to the given checkpoint.
+func (api *BeaconLightApi) GetCheckpointData(checkpointHash common.Hash) (*light.CheckpointData, error) {
+ resp, err := api.httpGetf("/eth/v1/beacon/light_client/bootstrap/0x%x", checkpointHash[:])
+ if err != nil {
+ return nil, err
+ }
+
+ // See data structure definition here:
+ // https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/sync-protocol.md#lightclientbootstrap
+ type bootstrapData struct {
+ Data struct {
+ Header types.JsonBeaconHeader `json:"header"`
+ Committee *types.SerializedCommittee `json:"current_sync_committee"`
+ CommitteeBranch merkle.Values `json:"current_sync_committee_branch"`
+ } `json:"data"`
+ }
+
+ var data bootstrapData
+ if err := json.Unmarshal(resp, &data); err != nil {
+ return nil, err
+ }
+ header := data.Data.Header.Beacon
+ if header.Hash() != checkpointHash {
+ return nil, errors.New("invalid checkpoint block header")
+ }
+ checkpoint := &light.CheckpointData{
+ Header: header,
+ CommitteeBranch: data.Data.CommitteeBranch,
+ CommitteeRoot: data.Data.Committee.Root(),
+ Committee: data.Data.Committee,
+ }
+ if !checkpoint.Validate() {
+ return nil, errors.New("invalid sync committee Merkle proof")
+ }
+ return checkpoint, nil
+}
+
+// GetExecutionPayload fetches the execution block belonging to the beacon block
+// specified by beaconRoot and validates its block hash against the expected execRoot.
+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 {
+ 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 {
+ Data struct {
+ Message capella.BeaconBlock `json:"message"`
+ } `json:"data"`
+ }
+ if err := json.Unmarshal(myJSONBlockData, &beaconBlockMessage); err != nil {
+ return nil, fmt.Errorf("invalid block json data: %v", err)
+ }
+ beaconBlock = beaconBlockMessage.Data.Message
+ beaconBodyRoot := common.Hash(beaconBlock.Body.HashTreeRoot(spec, tree.GetHashFn()))
+ if beaconBodyRoot != header.BodyRoot {
+ return nil, fmt.Errorf("Beacon body root hash mismatch (expected: %x, got: %x)", header.BodyRoot.Bytes(), beaconBodyRoot.Bytes())
+ }
+
+ 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) {
+ var data struct {
+ Slot common.Decimal `json:"slot"`
+ Block common.Hash `json:"block"`
+ }
+ if err := json.Unmarshal(enc, &data); err != nil {
+ return 0, common.Hash{}, err
+ }
+ return uint64(data.Slot), data.Block, nil
+}
+
+// StartHeadListener creates an event subscription for heads and signed (optimistic)
+// head updates and calls the specified callback functions when they are received.
+// The callbacks are also called for the current head and optimistic head at startup.
+// They are never called concurrently.
+func (api *BeaconLightApi) StartHeadListener(headFn func(slot uint64, blockRoot common.Hash), signedFn func(head types.SignedHead), errFn func(err error)) func() {
+ closeCh := make(chan struct{}) // initiate closing the stream
+ closedCh := make(chan struct{}) // stream closed (or failed to create)
+ stoppedCh := make(chan struct{}) // sync loop stopped
+ streamCh := make(chan *eventsource.Stream, 1)
+ go func() {
+ defer close(closedCh)
+ // when connected to a Lodestar node the subscription blocks until the
+ // first actual event arrives; therefore we create the subscription in
+ // a separate goroutine while letting the main goroutine sync up to the
+ // current head
+ stream, err := eventsource.Subscribe(api.url+"/eth/v1/events?topics=head&topics=light_client_optimistic_update", "")
+ if err != nil {
+ errFn(fmt.Errorf("Error creating event subscription: %v", err))
+ close(streamCh)
+ return
+ }
+ streamCh <- stream
+ <-closeCh
+ stream.Close()
+ }()
+ go func() {
+ defer close(stoppedCh)
+
+ if head, err := api.GetHeader(common.Hash{}); err == nil {
+ headFn(head.Slot, head.Hash())
+ }
+ if signedHead, err := api.GetOptimisticHeadUpdate(); err == nil {
+ signedFn(signedHead)
+ }
+ stream := <-streamCh
+ if stream == nil {
+ return
+ }
+ for {
+ select {
+ case event, ok := <-stream.Events:
+ if !ok {
+ break
+ }
+ switch event.Event() {
+ case "head":
+ if slot, blockRoot, err := decodeHeadEvent([]byte(event.Data())); err == nil {
+ headFn(slot, blockRoot)
+ } else {
+ errFn(fmt.Errorf("Error decoding head event: %v", err))
+ }
+ case "light_client_optimistic_update":
+ if signedHead, err := decodeOptimisticHeadUpdate([]byte(event.Data())); err == nil {
+ signedFn(signedHead)
+ } else {
+ errFn(fmt.Errorf("Error decoding optimistic update event: %v", err))
+ }
+ default:
+ errFn(fmt.Errorf("Unexpected event: %s", event.Event()))
+ }
+ case err, ok := <-stream.Errors:
+ if !ok {
+ break
+ }
+ errFn(err)
+ }
+ }
+ }()
+ return func() {
+ close(closeCh)
+ <-closedCh
+ <-stoppedCh
+ }
+}
diff --git a/beacon/light/api/sync_server.go b/beacon/light/api/sync_server.go
new file mode 100644
index 0000000000..d107ab2ae4
--- /dev/null
+++ b/beacon/light/api/sync_server.go
@@ -0,0 +1,156 @@
+// 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 .
+
+package api
+
+import (
+ "sync"
+ "time"
+
+ "github.com/ethereum/go-ethereum/beacon/light"
+ "github.com/ethereum/go-ethereum/beacon/light/types"
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/log"
+)
+
+const (
+ maxHeadLength = 4
+)
+
+type SyncServer struct {
+ api *BeaconLightApi
+ Stop func()
+ lock sync.RWMutex
+
+ triggerCallback func()
+ latestHeadSlot uint64
+ latestHeadHash common.Hash
+ signedHeads []types.SignedHead
+ canRequestBootstrap bool
+ firstUpdate uint64 //TODO ...
+}
+
+func NewSyncServer(api *BeaconLightApi) *SyncServer {
+ s := &SyncServer{
+ api: api,
+ 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()) {
+ s.lock.Lock()
+ defer s.lock.Unlock()
+
+ s.triggerCallback = cb
+}
+
+func (s *SyncServer) Delay() time.Duration { return 0 } //TODO
+
+func (s *SyncServer) Fail(desc string) {
+ 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 {
+ s.lock.RLock()
+ defer s.lock.RUnlock()
+
+ return s.canRequestBootstrap
+}
+
+func (s *SyncServer) RequestBootstrap(checkpointHash common.Hash, response func(*light.CheckpointData)) {
+ go func() {
+ if checkpoint, err := s.api.GetCheckpointData(checkpointHash); err == nil {
+ response(checkpoint)
+ } else {
+ s.lock.Lock()
+ s.canRequestBootstrap = false
+ s.lock.Unlock()
+ response(nil)
+ }
+ }()
+}
+
+func (s *SyncServer) UpdateRange() types.PeriodRange {
+ s.lock.RLock()
+ defer s.lock.RUnlock()
+
+ if len(s.signedHeads) == 0 {
+ return types.PeriodRange{}
+ }
+ r := types.PeriodRange{First: s.firstUpdate, AfterLast: types.PeriodOfSlot(s.signedHeads[len(s.signedHeads)-1].Header.Slot + 256)}
+ if !r.IsEmpty() {
+ return r
+ }
+ return types.PeriodRange{}
+}
+
+func (s *SyncServer) RequestUpdates(first, count uint64, response func([]*types.LightClientUpdate, []*types.SerializedCommittee)) {
+ go func() {
+ if updates, committees, err := s.api.GetBestUpdatesAndCommittees(first, count); err == nil {
+ response(updates, committees)
+ } else {
+ response(nil, nil)
+ }
+ }()
+}
+
+func (s *SyncServer) newHead(slot uint64, blockRoot common.Hash) {
+ s.lock.Lock()
+ defer s.lock.Unlock()
+
+ s.latestHeadSlot, s.latestHeadHash = slot, blockRoot
+}
+
+func (s *SyncServer) newSignedHead(signedHead types.SignedHead) {
+ 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()
+}
diff --git a/beacon/light/checkpoint.go b/beacon/light/checkpoint.go
new file mode 100644
index 0000000000..79946ae74e
--- /dev/null
+++ b/beacon/light/checkpoint.go
@@ -0,0 +1,109 @@
+// 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 .
+
+package light
+
+import (
+ "github.com/ethereum/go-ethereum/beacon/light/types"
+ "github.com/ethereum/go-ethereum/beacon/merkle"
+ "github.com/ethereum/go-ethereum/beacon/params"
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/ethdb"
+ "github.com/ethereum/go-ethereum/log"
+ "github.com/ethereum/go-ethereum/rlp"
+)
+
+var checkpointKey = []byte("checkpoint-") // block root -> RLP(CheckpointData)
+
+type CheckpointData struct {
+ Header types.Header
+ CommitteeRoot common.Hash
+ Committee *types.SerializedCommittee `rlp:"-"`
+ CommitteeBranch merkle.Values
+}
+
+func (c *CheckpointData) Validate() bool {
+ if c.CommitteeRoot != c.Committee.Root() {
+ return false
+ }
+ expStateRoot, ok := merkle.VerifySingleProof(c.CommitteeBranch, params.BsiSyncCommittee, merkle.Value(c.CommitteeRoot))
+ return ok && expStateRoot == c.Header.StateRoot
+}
+
+// expected to be validated already
+func (c *CheckpointData) InitChain(chain *CommitteeChain) {
+ must := func(err error) {
+ if err != nil {
+ log.Crit("Error initializing committee chain with checkpoint", "error", err)
+ }
+ }
+ period := c.Header.SyncPeriod()
+ must(chain.DeleteFixedRootsFrom(period + 2))
+ if chain.AddFixedRoot(period, c.CommitteeRoot) != nil {
+ chain.Reset()
+ must(chain.AddFixedRoot(period, c.CommitteeRoot))
+ }
+ must(chain.AddFixedRoot(period+1, common.Hash(c.CommitteeBranch[0])))
+ must(chain.AddCommittee(period, c.Committee))
+}
+
+type CheckpointStore struct {
+ chain *CommitteeChain
+ db ethdb.KeyValueStore
+}
+
+func NewCheckpointStore(db ethdb.KeyValueStore, chain *CommitteeChain) *CheckpointStore {
+ return &CheckpointStore{
+ db: db,
+ chain: chain,
+ }
+}
+
+func getCheckpointKey(checkpoint common.Hash) []byte {
+ var (
+ kl = len(checkpointKey)
+ key = make([]byte, kl+32)
+ )
+ copy(key[:kl], checkpointKey)
+ copy(key[kl:], checkpoint[:])
+ return key
+}
+
+func (cs *CheckpointStore) Get(checkpoint common.Hash) *CheckpointData {
+ if enc, err := cs.db.Get(getCheckpointKey(checkpoint)); err == nil {
+ c := new(CheckpointData)
+ if err := rlp.DecodeBytes(enc, c); err != nil {
+ log.Error("Error decoding stored checkpoint", "error", err)
+ return nil
+ }
+ if committee := cs.chain.committees.get(c.Header.SyncPeriod()); committee != nil && committee.Root() == c.CommitteeRoot {
+ c.Committee = committee
+ return c
+ }
+ log.Error("Missing committee for stored checkpoint", "period", c.Header.SyncPeriod())
+ }
+ return nil
+}
+
+func (cs *CheckpointStore) Store(c *CheckpointData) {
+ enc, err := rlp.EncodeToBytes(c)
+ if err != nil {
+ log.Error("Error encoding checkpoint for storage", "error", err)
+ }
+ if err := cs.db.Put(getCheckpointKey(c.Header.Hash()), enc); err != nil {
+ log.Error("Error storing checkpoint in database", "error", err)
+ }
+}
diff --git a/beacon/light/committee_chain.go b/beacon/light/committee_chain.go
new file mode 100644
index 0000000000..54ecece342
--- /dev/null
+++ b/beacon/light/committee_chain.go
@@ -0,0 +1,548 @@
+// 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 .
+
+package light
+
+import (
+ "encoding/binary"
+ "errors"
+ "sync"
+ "time"
+
+ "github.com/ethereum/go-ethereum/beacon/light/types"
+ "github.com/ethereum/go-ethereum/beacon/params"
+ "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/ethdb"
+ "github.com/ethereum/go-ethereum/log"
+ "github.com/ethereum/go-ethereum/rlp"
+)
+
+var (
+ ErrNotInitialized = errors.New("sync committee chain not initialized")
+ ErrNeedCommittee = errors.New("sync committee required")
+ ErrInvalidUpdate = errors.New("invalid committee update")
+ ErrInvalidPeriod = errors.New("invalid update period")
+ ErrWrongCommitteeRoot = errors.New("wrong committee root")
+ ErrCannotReorg = errors.New("can not reorg committee chain")
+)
+
+var (
+ genesisDataKey = []byte("genesis") // RLP(GenesisData)
+ bestUpdateKey = []byte("update-") // bigEndian64(syncPeriod) -> RLP(types.LightClientUpdate) (nextCommittee only referenced by root hash)
+ fixedRootKey = []byte("fixedRoot-") // bigEndian64(syncPeriod) -> committee root hash
+ syncCommitteeKey = []byte("committee-") // bigEndian64(syncPeriod) -> serialized committee
+)
+
+// ChainConfig contains built-in chain configuration presets for certain networks
+type ChainConfig struct {
+ GenesisData
+ Forks types.Forks
+ Checkpoint common.Hash
+}
+
+// GenesisData is required for signature verification and is set by the
+// CommitteeChain.Init function.
+type GenesisData struct {
+ GenesisTime uint64 // unix time (in seconds) of slot 0
+ GenesisValidatorsRoot common.Hash // root hash of the genesis validator set, used for signature domain calculation
+}
+
+// CommitteeChain maintains a chain of sync committee updates and a small
+// set of best known signed heads. It is used in all client configurations
+// operating on a beacon chain. It can sync its update chain and receive signed
+// heads from either an ODR or beacon node API backend and propagate/serve this
+// data to subscribed peers. Received signed heads are validated based on the
+// known sync committee chain and added to the local set if valid or placed in a
+// deferred queue if the committees are not synced up to the period of the new
+// head yet.
+// Sync committee chain is either initialized from a weak subjectivity checkpoint
+// or controlled by a BeaconChain that is driven by a trusted source (beacon node API).
+type CommitteeChain struct {
+ lock sync.RWMutex
+ db ethdb.KeyValueStore
+ sigVerifier committeeSigVerifier
+ clock mclock.Clock
+ updates *canonicalStore[*types.LightClientUpdate]
+ committees *canonicalStore[*types.SerializedCommittee]
+ fixedRoots *canonicalStore[common.Hash]
+ syncCommitteeCache *lru.Cache[uint64, syncCommittee] // cache deserialized committees
+ unixNano func() int64
+
+ forks types.Forks
+ signerThreshold int
+ minimumUpdateScore types.UpdateScore
+ enforceTime bool
+
+ genesisData GenesisData
+ genesisInit bool // genesis data initialized (signature check possible)
+}
+
+// NewCommitteeChain creates a new CommitteeChain
+func NewCommitteeChain(db ethdb.KeyValueStore, forks types.Forks, signerThreshold int, enforceTime bool, sigVerifier committeeSigVerifier, clock mclock.Clock, unixNano func() int64) *CommitteeChain {
+ s := &CommitteeChain{
+ fixedRoots: newCanonicalStore[common.Hash](db, fixedRootKey, func(root common.Hash) ([]byte, error) {
+ return root[:], nil
+ }, func(enc []byte) (root common.Hash, err error) {
+ if len(enc) == len(root) {
+ copy(root[:], enc)
+ } else {
+ err = errors.New("Incorrect length for committee root entry in the database")
+ }
+ return
+ }),
+ committees: newCanonicalStore[*types.SerializedCommittee](db, syncCommitteeKey, func(committee *types.SerializedCommittee) ([]byte, error) {
+ return committee[:], nil
+ }, func(enc []byte) (*types.SerializedCommittee, error) {
+ if len(enc) == types.SerializedCommitteeSize {
+ committee := new(types.SerializedCommittee)
+ copy(committee[:], enc)
+ return committee, nil
+ }
+ return nil, errors.New("Incorrect length for serialized committee entry in the database")
+ }),
+ updates: newCanonicalStore[*types.LightClientUpdate](db, bestUpdateKey, func(update *types.LightClientUpdate) ([]byte, error) {
+ return rlp.EncodeToBytes(update)
+ }, func(enc []byte) (*types.LightClientUpdate, error) {
+ update := new(types.LightClientUpdate)
+ if err := rlp.DecodeBytes(enc, update); err != nil {
+ return nil, err
+ }
+ return update, nil
+ }),
+ syncCommitteeCache: lru.NewCache[uint64, syncCommittee](10),
+ db: db,
+ sigVerifier: sigVerifier,
+ clock: clock,
+ unixNano: unixNano,
+ forks: forks,
+ signerThreshold: signerThreshold,
+ enforceTime: enforceTime,
+ minimumUpdateScore: types.UpdateScore{
+ SignerCount: uint32(signerThreshold),
+ SubPeriodIndex: params.SyncPeriodLength / 16,
+ },
+ }
+ if enc, err := s.db.Get(genesisDataKey); err == nil {
+ var genesisData GenesisData
+ if err := rlp.DecodeBytes(enc, &genesisData); err == nil {
+ s.setGenesisData(genesisData)
+ log.Trace("Beacon chain genesis data loaded")
+ } else {
+ log.Error("Error decoding genesis data", "error", err)
+ }
+ }
+
+ // check validity constraints
+ if !s.updates.periodRange.IsEmpty() {
+ if !s.genesisInit {
+ log.Crit("Inconsistent database error: updates present but genesis data is not initialized")
+ }
+ if s.fixedRoots.periodRange.IsEmpty() || s.updates.periodRange.First < s.fixedRoots.periodRange.First ||
+ s.updates.periodRange.First >= s.fixedRoots.periodRange.AfterLast {
+ log.Crit("Inconsistent database error: first update is not in the fixed roots range")
+ }
+ if s.committees.periodRange.First > s.updates.periodRange.First || s.committees.periodRange.AfterLast <= s.updates.periodRange.AfterLast {
+ log.Crit("Inconsistent database error: missing committees in update range")
+ }
+ }
+ if !s.committees.periodRange.IsEmpty() {
+ if s.fixedRoots.periodRange.IsEmpty() || s.committees.periodRange.First < s.fixedRoots.periodRange.First ||
+ s.committees.periodRange.First >= s.fixedRoots.periodRange.AfterLast {
+ log.Crit("Inconsistent database error: first committee is not in the fixed roots range")
+ }
+ if s.committees.periodRange.AfterLast > s.fixedRoots.periodRange.AfterLast && s.committees.periodRange.AfterLast >= s.updates.periodRange.AfterLast {
+ log.Crit("Inconsistent database error: last committee is neither in the fixed roots range nor proven by updates")
+ }
+ log.Trace("Sync committee chain loaded", "first period", s.committees.periodRange.First, "last period", s.committees.periodRange.AfterLast-1)
+ }
+ return s
+}
+
+func (s *CommitteeChain) Reset() {
+ s.lock.Lock()
+ defer s.lock.Unlock()
+
+ batch := s.db.NewBatch()
+ s.rollback(batch, 0)
+ if err := batch.Write(); err != nil {
+ log.Error("Error writing batch into chain database", "error", err)
+ }
+}
+
+// InitConfig initializes the tracker with the given GenesisData and starts the update
+// syncing process.
+// Note that Init may be called either at startup or later if it has to be
+// fetched from the network based on a checkpoint hash.
+func (s *CommitteeChain) SetGenesisData(genesisData GenesisData) {
+ s.lock.Lock()
+ defer s.lock.Unlock()
+
+ if s.genesisInit {
+ if s.genesisData != genesisData {
+ log.Crit("Beacon chain genesis data already initialized with different values")
+ }
+ return
+ }
+ s.setGenesisData(genesisData)
+ if enc, err := rlp.EncodeToBytes(&genesisData); err == nil {
+ if err := s.db.Put(genesisDataKey, enc); err != nil {
+ log.Error("Error storing genesis data", "error", err)
+ }
+ } else {
+ log.Error("Error encoding genesis data", "error", err)
+ }
+ log.Trace("Beacon chain genesis data stored and initialized")
+}
+
+// (lock required)
+func (s *CommitteeChain) setGenesisData(genesisData GenesisData) {
+ s.genesisData = genesisData
+ s.forks.ComputeDomains(genesisData.GenesisValidatorsRoot)
+ s.genesisInit = true
+}
+
+func (s *CommitteeChain) AddFixedRoot(period uint64, root common.Hash) error {
+ s.lock.Lock()
+ defer s.lock.Unlock()
+
+ batch := s.db.NewBatch()
+ oldRoot := s.getCommitteeRoot(period)
+ if !s.fixedRoots.periodRange.CanExpand(period) {
+ if root != oldRoot {
+ return ErrInvalidPeriod
+ }
+ for p := s.fixedRoots.periodRange.AfterLast; p <= period; p++ {
+ s.fixedRoots.add(batch, p, s.getCommitteeRoot(p))
+ }
+ }
+ if oldRoot != (common.Hash{}) && (oldRoot != root) {
+ // existing old root was different, we have to reorg the chain
+ s.rollback(batch, period)
+ }
+ s.fixedRoots.add(batch, period, root)
+ if err := batch.Write(); err != nil {
+ log.Error("Error writing batch into chain database", "error", err)
+ return err
+ }
+ return nil
+}
+
+func (s *CommitteeChain) DeleteFixedRootsFrom(period uint64) error {
+ s.lock.Lock()
+ defer s.lock.Unlock()
+
+ if period >= s.fixedRoots.periodRange.AfterLast {
+ return nil
+ }
+ batch := s.db.NewBatch()
+ s.fixedRoots.deleteFrom(batch, period)
+ if s.updates.periodRange.IsEmpty() || period <= s.updates.periodRange.First {
+ s.updates.deleteFrom(batch, period)
+ s.deleteCommitteesFrom(batch, period)
+ } else {
+ fromPeriod := s.updates.periodRange.AfterLast + 1
+ if period > fromPeriod {
+ fromPeriod = period
+ }
+ s.deleteCommitteesFrom(batch, fromPeriod)
+ }
+ if err := batch.Write(); err != nil {
+ log.Error("Error writing batch into chain database", "error", err)
+ return err
+ }
+ return nil
+}
+
+func (s *CommitteeChain) deleteCommitteesFrom(batch ethdb.Batch, period uint64) {
+ deleted := s.committees.deleteFrom(batch, period)
+ for period := deleted.First; period < deleted.AfterLast; period++ {
+ s.syncCommitteeCache.Remove(period)
+ }
+}
+
+func (s *CommitteeChain) AddCommittee(period uint64, committee *types.SerializedCommittee) error {
+ s.lock.Lock()
+ defer s.lock.Unlock()
+
+ if !s.committees.periodRange.CanExpand(period) {
+ return ErrInvalidPeriod
+ }
+ root := s.getCommitteeRoot(period)
+ if root == (common.Hash{}) {
+ return ErrInvalidPeriod
+ }
+ if root != committee.Root() {
+ return ErrWrongCommitteeRoot
+ }
+ if !s.committees.periodRange.Includes(period) {
+ s.committees.add(nil, period, committee)
+ s.syncCommitteeCache.Remove(period)
+ }
+ return nil
+}
+
+func (s *CommitteeChain) InsertUpdate(update *types.LightClientUpdate, nextCommittee *types.SerializedCommittee) error {
+ s.lock.Lock()
+ defer s.lock.Unlock()
+
+ if !s.genesisInit {
+ return ErrNotInitialized
+ }
+ period := update.Header.SyncPeriod()
+ if !s.updates.periodRange.CanExpand(period) || !s.committees.periodRange.Includes(period) {
+ return ErrInvalidPeriod
+ }
+ oldRoot := s.getCommitteeRoot(period + 1)
+ reorg := oldRoot != (common.Hash{}) && oldRoot != update.NextSyncCommitteeRoot
+ if oldUpdate := s.updates.get(period); oldUpdate != nil && !update.Score().BetterThan(oldUpdate.Score()) {
+ // a better or equal update already exists; no changes, only fail if new one tried to reorg
+ if reorg {
+ return ErrCannotReorg
+ }
+ return nil
+ }
+ if s.fixedRoots.periodRange.Includes(period+1) && reorg {
+ return ErrCannotReorg
+ }
+ if !s.verifyUpdate(update) {
+ return ErrInvalidUpdate
+ }
+ addCommittee := !s.committees.periodRange.Includes(period+1) || reorg
+ if addCommittee {
+ if nextCommittee == nil {
+ return ErrNeedCommittee
+ }
+ if nextCommittee.Root() != update.NextSyncCommitteeRoot {
+ return ErrWrongCommitteeRoot
+ }
+ }
+ batch := s.db.NewBatch()
+ if reorg {
+ s.rollback(batch, period+1)
+ }
+ if addCommittee {
+ s.committees.add(batch, period+1, nextCommittee)
+ s.syncCommitteeCache.Remove(period + 1)
+ }
+ s.updates.add(batch, period, update)
+ if err := batch.Write(); err != nil {
+ log.Error("Error writing batch into chain database", "error", err)
+ return err
+ }
+ log.Info("Inserted new committee update", "period", period, "next committee root", update.NextSyncCommitteeRoot)
+ return nil
+}
+
+func (s *CommitteeChain) NextSyncPeriod() (uint64, bool) {
+ s.lock.RLock()
+ defer s.lock.RUnlock()
+
+ if !s.genesisInit || s.committees.periodRange.IsEmpty() {
+ return 0, false
+ }
+ if !s.updates.periodRange.IsEmpty() {
+ return s.updates.periodRange.AfterLast, true
+ }
+ return s.committees.periodRange.AfterLast - 1, true
+}
+
+func (s *CommitteeChain) rollback(batch ethdb.Batch, period uint64) {
+ s.deleteCommitteesFrom(batch, period)
+ s.fixedRoots.deleteFrom(batch, period)
+ if period > 0 {
+ period--
+ }
+ s.updates.deleteFrom(batch, period)
+}
+
+func (s *CommitteeChain) getCommitteeRoot(period uint64) common.Hash {
+ if root := s.fixedRoots.get(period); root != (common.Hash{}) || period == 0 {
+ return root
+ }
+ if update := s.updates.get(period - 1); update != nil {
+ return update.NextSyncCommitteeRoot
+ }
+ return common.Hash{}
+}
+
+// getSyncCommittee returns the deserialized sync committee at the given period
+// of the current local committee chain (tracker mutex lock expected).
+func (s *CommitteeChain) getSyncCommittee(period uint64) syncCommittee {
+ if c, ok := s.syncCommitteeCache.Get(period); ok {
+ return c
+ }
+ if sc := s.committees.get(period); sc != nil {
+ c, err := s.sigVerifier.deserializeSyncCommittee(sc)
+ if err != nil {
+ log.Error("Sync committee deserialization error", "error", err)
+ return nil
+ }
+ s.syncCommitteeCache.Add(period, c)
+ return c
+ }
+ log.Error("Missing serialized sync committee", "period", period)
+ return nil
+}
+
+// VerifySignedHead returns true if the given signed head has a valid signature
+// according to the local committee chain. The caller should ensure that the
+// committees advertised by the same source where the signed head came from are
+// synced before verifying the signature.
+// The age of the header is also returned (the time elapsed since the beginning
+// of the given slot, according to the local system clock). If enforceTime is
+// true then negative age (future) headers are rejected.
+func (s *CommitteeChain) VerifySignedHead(head types.SignedHead) (bool, time.Duration) {
+ s.lock.RLock()
+ defer s.lock.RUnlock()
+
+ return s.verifySignedHead(head)
+}
+
+// (rlock required)
+func (s *CommitteeChain) verifySignedHead(head types.SignedHead) (bool, time.Duration) {
+ var (
+ slotTime = int64(time.Second) * int64(s.genesisData.GenesisTime+head.Header.Slot*12)
+ age = time.Duration(s.unixNano() - slotTime)
+ )
+ if s.enforceTime && age < 0 {
+ return false, age
+ }
+ committee := s.getSyncCommittee(types.PeriodOfSlot(head.SignatureSlot))
+ if committee == nil {
+ return false, age
+ }
+ return s.sigVerifier.verifySignature(committee, s.forks.SigningRoot(head.Header), &head.SyncAggregate), age
+}
+
+// verifyUpdate checks whether the header signature is correct and the update
+// fits into the specified constraints (assumes that the update has been
+// successfully validated previously)
+// (rlock required)
+func (s *CommitteeChain) verifyUpdate(update *types.LightClientUpdate) bool {
+ // Note: SignatureSlot determines the sync period of the committee used for signature
+ // verification. Though in reality SignatureSlot is always bigger than update.Header.Slot,
+ // setting them as equal here enforces the rule that they have to be in the same sync
+ // period in order for the light client update proof to be meaningful.
+ ok, age := s.verifySignedHead(types.SignedHead{Header: update.Header, SyncAggregate: update.SyncAggregate, SignatureSlot: update.Header.Slot})
+ if age < 0 {
+ log.Warn("Future committee update received", "age", age)
+ }
+ return ok
+}
+
+type canonicalStore[T any] struct {
+ db ethdb.KeyValueStore
+ keyPrefix []byte
+ cache *lru.Cache[uint64, T]
+ periodRange types.PeriodRange //TODO make it a parent struct?
+ encode func(T) ([]byte, error)
+ decode func([]byte) (T, error)
+}
+
+func newCanonicalStore[T any](db ethdb.KeyValueStore, keyPrefix []byte,
+ encode func(T) ([]byte, error), decode func([]byte) (T, error)) *canonicalStore[T] {
+ cs := &canonicalStore[T]{
+ db: db,
+ keyPrefix: keyPrefix,
+ encode: encode,
+ decode: decode,
+ cache: lru.NewCache[uint64, T](100),
+ }
+ var (
+ iter = db.NewIterator(keyPrefix, nil)
+ kl = len(keyPrefix)
+ )
+ for iter.Next() {
+ period := binary.BigEndian.Uint64(iter.Key()[kl : kl+8])
+ if cs.periodRange.First == 0 {
+ cs.periodRange.First = period
+ } else if cs.periodRange.AfterLast != period {
+ if iter.Next() {
+ log.Error("Gap in the canonical chain database")
+ }
+ break // continuity guaranteed
+ }
+ cs.periodRange.AfterLast = period + 1
+ }
+ iter.Release()
+ return cs
+}
+
+func (cs *canonicalStore[T]) getDbKey(period uint64) []byte {
+ var (
+ kl = len(cs.keyPrefix)
+ key = make([]byte, kl+8)
+ )
+ copy(key[:kl], cs.keyPrefix)
+ binary.BigEndian.PutUint64(key[kl:], period)
+ return key
+}
+
+func (cs *canonicalStore[T]) add(batch ethdb.Batch, period uint64, value T) {
+ if !cs.periodRange.CanExpand(period) {
+ log.Error("Cannot expand canonical store", "range.first", cs.periodRange.First, "range.afterLast", cs.periodRange.AfterLast, "new period", period)
+ return
+ }
+ enc, err := cs.encode(value)
+ if err != nil {
+ log.Error("Error encoding canonical store value", "error", err)
+ return
+ }
+ key := cs.getDbKey(period)
+ if batch != nil {
+ err = batch.Put(key, enc)
+ } else {
+ err = cs.db.Put(key, enc)
+ }
+ if err != nil {
+ log.Error("Error writing into canonical store value database", "error", err)
+ }
+ cs.periodRange.Expand(period)
+}
+
+// should only be used in batch mode
+func (cs *canonicalStore[T]) deleteFrom(batch ethdb.Batch, fromPeriod uint64) (deleted types.PeriodRange) {
+ if fromPeriod >= cs.periodRange.AfterLast {
+ return
+ }
+ if fromPeriod < cs.periodRange.First {
+ fromPeriod = cs.periodRange.First
+ }
+ deleted = types.PeriodRange{First: fromPeriod, AfterLast: cs.periodRange.AfterLast}
+ for period := fromPeriod; period < cs.periodRange.AfterLast; period++ {
+ batch.Delete(cs.getDbKey(period))
+ }
+ if fromPeriod > cs.periodRange.First {
+ cs.periodRange.AfterLast = fromPeriod
+ } else {
+ cs.periodRange = types.PeriodRange{}
+ }
+ return
+}
+
+func (cs *canonicalStore[T]) get(period uint64) T {
+ var value T
+ if enc, err := cs.db.Get(cs.getDbKey(period)); err == nil {
+ if v, err := cs.decode(enc); err == nil {
+ value = v
+ } else {
+ log.Error("Error decoding canonical store value", "error", err)
+ }
+ }
+ return value
+}
diff --git a/beacon/light/head_tracker.go b/beacon/light/head_tracker.go
new file mode 100644
index 0000000000..74392af583
--- /dev/null
+++ b/beacon/light/head_tracker.go
@@ -0,0 +1,95 @@
+// 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 .
+
+package light
+
+import (
+ "errors"
+ "sync"
+ "time"
+
+ "github.com/ethereum/go-ethereum/beacon/light/types"
+ "github.com/ethereum/go-ethereum/log"
+)
+
+type HeadTracker struct {
+ lock sync.Mutex
+ committeeChain *CommitteeChain
+ subs []*headSub
+}
+
+func NewHeadTracker(committeeChain *CommitteeChain) *HeadTracker {
+ return &HeadTracker{committeeChain: committeeChain}
+}
+
+type headSub struct {
+ minSignerCount int
+ nextSlot uint64
+ callbacks []func(types.SignedHead)
+}
+
+func (h *HeadTracker) Subscribe(minSignerCount int, callback func(types.SignedHead)) {
+ h.lock.Lock()
+ defer h.lock.Unlock()
+
+ insertAt := len(h.subs)
+ for i, sub := range h.subs {
+ if sub.minSignerCount == minSignerCount {
+ sub.callbacks = append(sub.callbacks, callback)
+ return
+ }
+ if sub.minSignerCount > minSignerCount {
+ insertAt = i
+ break
+ }
+ }
+ h.subs = append(h.subs, nil)
+ copy(h.subs[insertAt+1:], h.subs[insertAt:len(h.subs)-1])
+ h.subs[insertAt] = &headSub{
+ minSignerCount: minSignerCount,
+ callbacks: []func(types.SignedHead){callback},
+ }
+}
+
+func (h *HeadTracker) Add(head types.SignedHead) error {
+ h.lock.Lock()
+ defer h.lock.Unlock()
+
+ sigOk, age := h.committeeChain.VerifySignedHead(head)
+ if age < 0 {
+ log.Warn("Future signed head received", "age", age)
+ }
+ if age > time.Minute*2 {
+ log.Warn("Old signed head received", "age", age)
+ }
+ if !sigOk {
+ return errors.New("invalid header signature")
+ }
+
+ signerCount := head.SignerCount()
+ for _, sub := range h.subs {
+ if sub.minSignerCount > signerCount {
+ break
+ }
+ if head.Header.Slot >= sub.nextSlot {
+ for _, cb := range sub.callbacks {
+ cb(head)
+ }
+ sub.nextSlot = head.Header.Slot + 1
+ }
+ }
+ return nil
+}
diff --git a/beacon/light/request/lock.go b/beacon/light/request/lock.go
new file mode 100644
index 0000000000..758522176c
--- /dev/null
+++ b/beacon/light/request/lock.go
@@ -0,0 +1,47 @@
+// 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 .
+
+package request
+
+type SingleLock struct {
+ requestLock map[*Server]uint64 // servers where the request has been sent and not timed out yet
+}
+
+func (s *SingleLock) CanSend(server *Server) bool {
+ reqId, ok := s.requestLock[server]
+ if ok && server.Timeout(reqId) {
+ delete(s.requestLock, server)
+ return false
+ }
+ return !ok && server.CanSend()
+}
+
+// assumes that canSend returned true (no request lock)
+func (s *SingleLock) TrySend(srv *Server) (uint64, bool) {
+ if s.requestLock == nil {
+ s.requestLock = make(map[*Server]uint64)
+ }
+ if reqId, ok := srv.TrySend(); ok {
+ s.requestLock[srv] = reqId
+ return reqId, true
+ }
+ return 0, false
+}
+
+func (s *SingleLock) Returned(srv *Server, reqId uint64) {
+ delete(s.requestLock, srv)
+ srv.Returned(reqId)
+}
diff --git a/beacon/light/request/scheduler.go b/beacon/light/request/scheduler.go
new file mode 100644
index 0000000000..96400df2e9
--- /dev/null
+++ b/beacon/light/request/scheduler.go
@@ -0,0 +1,237 @@
+// 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 .
+
+package request
+
+import (
+ "sync"
+ "time"
+)
+
+const softRequestTimeout = time.Second
+
+type Module interface {
+ Process(servers []*Server) bool // removed if return value is false
+}
+
+type RequestServer interface {
+ SetTriggerCallback(func())
+ Delay() time.Duration
+ Fail(string)
+}
+
+// initialized when first trigger is added
+type ModuleTrigger struct { // Scheduler lock
+ s *Scheduler
+ triggers map[Module]struct{}
+}
+
+func (t *ModuleTrigger) Trigger() {
+ if t.triggers == nil {
+ return
+ }
+ t.s.triggerLock.Lock()
+ defer t.s.triggerLock.Unlock()
+
+ for m := range t.triggers {
+ t.s.moduleTrigger(m)
+ }
+}
+
+type Scheduler struct {
+ lock sync.Mutex
+ modules []Module // first has highest priority
+ servers []*Server
+ triggeredBy map[Module][]*ModuleTrigger
+ stopCh chan chan struct{}
+
+ triggerCh chan struct{}
+ triggerLock sync.Mutex
+ processing, triggered bool
+ trModules map[Module]struct{}
+ trServers map[*Server]struct{}
+}
+
+func NewScheduler() *Scheduler {
+ return &Scheduler{
+ stopCh: make(chan chan struct{}),
+ triggerCh: make(chan struct{}, 1),
+ triggeredBy: make(map[Module][]*ModuleTrigger),
+ }
+}
+
+// call before starting the scheduler
+func (s *Scheduler) RegisterModule(m Module) {
+ s.lock.Lock()
+ defer s.lock.Unlock()
+
+ s.modules = append(s.modules, m)
+}
+
+func (s *Scheduler) AddTriggers(m Module, triggeredBy []*ModuleTrigger) {
+ s.triggeredBy[m] = append(s.triggeredBy[m], triggeredBy...)
+ for _, t := range triggeredBy {
+ if t.triggers == nil {
+ t.s = s
+ t.triggers = make(map[Module]struct{})
+ }
+ t.triggers[m] = struct{}{}
+ }
+}
+
+func (s *Scheduler) unregisterModule(m Module, t []*ModuleTrigger) {
+ for i, module := range s.modules {
+ 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()
+ defer s.lock.Unlock()
+
+ server := s.newServer(RequestServer)
+ s.servers = append(s.servers, server)
+ RequestServer.SetTriggerCallback(func() {
+ s.ServerTrigger(server)
+ })
+ s.ServerTrigger(server)
+}
+
+func (s *Scheduler) UnregisterServer(RequestServer RequestServer) {
+ s.lock.Lock()
+ defer s.lock.Unlock()
+
+ for i, server := range s.servers {
+ if server.RequestServer == RequestServer {
+ s.servers[i] = s.servers[len(s.servers)-1]
+ s.servers = s.servers[:len(s.servers)-1]
+ server.stop()
+ return
+ }
+ }
+}
+
+// call before registering servers
+func (s *Scheduler) Start() {
+ go s.syncLoop()
+}
+
+func (s *Scheduler) Stop() {
+ s.lock.Lock()
+ for _, server := range s.servers {
+ server.stop()
+ }
+ s.servers = nil
+ s.lock.Unlock()
+ stop := make(chan struct{})
+ s.stopCh <- stop
+ <-stop
+}
+
+func (s *Scheduler) syncLoop() {
+ s.lock.Lock()
+ s.triggerLock.Lock()
+ s.processing = true
+ for {
+ trModules, trServers := s.trModules, s.trServers
+ s.trModules, s.trServers = nil, nil
+ if trModules != nil || trServers != nil {
+ s.triggerLock.Unlock()
+ s.processModules(trModules, trServers)
+ s.triggerLock.Lock()
+ } else {
+ s.processing = false
+ s.triggerLock.Unlock()
+ s.lock.Unlock()
+ select {
+ case stop := <-s.stopCh:
+ close(stop)
+ return
+ case <-s.triggerCh:
+ }
+ s.lock.Lock()
+ s.triggerLock.Lock()
+ s.triggered = false
+ s.processing = true
+ }
+ }
+}
+
+func (s *Scheduler) processModules(trModules map[Module]struct{}, trServers map[*Server]struct{}) {
+ trs := make([]*Server, 0, len(s.servers))
+ for _, server := range s.servers {
+ if _, ok := trServers[server]; ok {
+ trs = append(trs, server)
+ }
+ }
+ var i int
+ for _, module := range s.modules {
+ keep := true
+ if _, ok := trModules[module]; ok {
+ keep = module.Process(s.servers)
+ } else if len(trs) > 0 {
+ keep = module.Process(trs)
+ }
+ if keep {
+ s.modules[i] = module
+ i++
+ }
+ }
+ s.modules = s.modules[:i]
+}
+
+func (s *Scheduler) ServerTrigger(server *Server) {
+ s.triggerLock.Lock()
+ s.serverTrigger(server)
+ s.triggerLock.Unlock()
+}
+
+func (s *Scheduler) serverTrigger(server *Server) {
+ if s.trServers == nil {
+ s.trServers = make(map[*Server]struct{})
+ }
+ s.trServers[server] = struct{}{}
+ if !s.processing && !s.triggered {
+ s.triggerCh <- struct{}{}
+ s.triggered = true
+ }
+}
+
+func (s *Scheduler) ModuleTrigger(module Module) {
+ s.triggerLock.Lock()
+ s.moduleTrigger(module)
+ s.triggerLock.Unlock()
+}
+
+func (s *Scheduler) moduleTrigger(module Module) {
+ if s.trModules == nil {
+ s.trModules = make(map[Module]struct{})
+ }
+ s.trModules[module] = struct{}{}
+ if !s.processing && !s.triggered {
+ s.triggerCh <- struct{}{}
+ s.triggered = true
+ }
+}
diff --git a/beacon/light/request/server.go b/beacon/light/request/server.go
new file mode 100644
index 0000000000..326df2eed2
--- /dev/null
+++ b/beacon/light/request/server.go
@@ -0,0 +1,166 @@
+// 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 .
+
+package request
+
+import (
+ "math/rand"
+ "sync"
+ "time"
+)
+
+func SelectServer(servers []*Server, priority func(server *Server) uint64) *Server {
+ 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
+ scheduler *Scheduler
+ lock sync.Mutex
+ sent map[uint64]chan struct{} // closed when returned; nil when timed out
+ timeoutCount int
+ delayed bool
+ delayChecked bool
+ needTrigger bool
+ lastReqId uint64
+ stopCh chan struct{}
+}
+
+func (s *Scheduler) newServer(server RequestServer) *Server {
+ return &Server{
+ RequestServer: server,
+ scheduler: s,
+ sent: make(map[uint64]chan struct{}),
+ stopCh: make(chan struct{}),
+ }
+}
+
+// guarantees a server trigger later if the result is false
+func (s *Server) CanSend() bool {
+ s.lock.Lock()
+ defer s.lock.Unlock()
+
+ if s.isDelayed() || s.timeoutCount != 0 {
+ s.needTrigger = true
+ return false
+ }
+ return true
+}
+
+// guarantees a server trigger later if the result is false
+func (s *Server) TrySend() (uint64, bool) {
+ s.lock.Lock()
+ defer s.lock.Unlock()
+
+ if s.isDelayed() || s.timeoutCount != 0 {
+ s.needTrigger = true
+ return 0, false
+ }
+ s.lastReqId++
+ returnCh := make(chan struct{})
+ s.sent[s.lastReqId] = returnCh
+ s.delayChecked = false
+ go func() {
+ timer := time.NewTimer(softRequestTimeout)
+ select {
+ case <-timer.C:
+ s.lock.Lock()
+ if _, ok := s.sent[s.lastReqId]; ok {
+ s.sent[s.lastReqId] = nil
+ s.timeoutCount++
+ }
+ s.lock.Unlock()
+ case <-returnCh:
+ timer.Stop()
+ case <-s.stopCh:
+ timer.Stop()
+ }
+ }()
+ return s.lastReqId, true
+}
+
+func (s *Server) Timeout(reqId uint64) bool {
+ s.lock.Lock()
+ defer s.lock.Unlock()
+
+ ch, ok := s.sent[reqId]
+ return ok && ch == nil
+}
+
+func (s *Server) Returned(reqId uint64) {
+ s.lock.Lock()
+ defer s.lock.Unlock()
+
+ if ch, ok := s.sent[reqId]; ok {
+ if ch != nil {
+ close(ch)
+ } else {
+ s.timeoutCount--
+ }
+ delete(s.sent, reqId)
+ }
+}
+
+func (s *Server) stop() {
+ close(s.stopCh)
+}
+
+func (s *Server) isDelayed() bool {
+ if s.delayChecked {
+ return s.delayed
+ }
+ s.delayChecked = true
+ delay := s.RequestServer.Delay()
+ if s.delayed = delay > 0; s.delayed {
+ go func() {
+ timer := time.NewTimer(delay)
+ select {
+ case <-timer.C:
+ s.lock.Lock()
+ s.delayed = false
+ trigger := s.needTrigger && s.timeoutCount == 0
+ s.lock.Unlock()
+ if trigger {
+ s.scheduler.serverTrigger(s)
+ }
+ case <-s.stopCh:
+ timer.Stop()
+ }
+ }()
+ }
+ return s.delayed
+}
diff --git a/beacon/light/signature.go b/beacon/light/signature.go
new file mode 100644
index 0000000000..27f8af3c75
--- /dev/null
+++ b/beacon/light/signature.go
@@ -0,0 +1,85 @@
+// Copyright 2022 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 .
+
+package light
+
+import (
+ "math/rand"
+
+ "github.com/ethereum/go-ethereum/beacon/light/types"
+ "github.com/ethereum/go-ethereum/beacon/params"
+ "github.com/ethereum/go-ethereum/common"
+)
+
+// syncCommittee holds either a blsSyncCommittee or a fake dummySyncCommittee used for testing
+type syncCommittee interface{}
+
+// committeeSigVerifier verifies sync committee signatures (either proper BLS
+// signatures or fake signatures used for testing)
+type committeeSigVerifier interface {
+ deserializeSyncCommittee(s *types.SerializedCommittee) (syncCommittee, error)
+ verifySignature(committee syncCommittee, signedRoot common.Hash, aggregate *types.SyncAggregate) bool
+}
+
+// BLSVerifier implements committeeSigVerifier
+type BLSVerifier struct{}
+
+// deserializeSyncCommittee implements committeeSigVerifier
+func (BLSVerifier) deserializeSyncCommittee(s *types.SerializedCommittee) (syncCommittee, error) {
+ return s.Deserialize()
+}
+
+// verifySignature implements committeeSigVerifier
+func (BLSVerifier) verifySignature(committee syncCommittee, signingRoot common.Hash, aggregate *types.SyncAggregate) bool {
+ return committee.(*types.SyncCommittee).VerifySignature(signingRoot, aggregate)
+}
+
+type dummySyncCommittee [32]byte
+
+// dummyVerifier implements committeeSigVerifier
+type dummyVerifier struct{}
+
+// deserializeSyncCommittee implements committeeSigVerifier
+func (dummyVerifier) deserializeSyncCommittee(s *types.SerializedCommittee) (syncCommittee, error) {
+ var sc dummySyncCommittee
+ copy(sc[:], s[:32])
+ return sc, nil
+}
+
+// verifySignature implements committeeSigVerifier
+func (dummyVerifier) verifySignature(committee syncCommittee, signingRoot common.Hash, aggregate *types.SyncAggregate) bool {
+ return aggregate.Signature == makeDummySignature(committee.(dummySyncCommittee), signingRoot, aggregate.BitMask)
+}
+
+func randomDummySyncCommittee() dummySyncCommittee {
+ var sc dummySyncCommittee
+ rand.Read(sc[:])
+ return sc
+}
+
+func serializeDummySyncCommittee(sc dummySyncCommittee) *types.SerializedCommittee {
+ s := new(types.SerializedCommittee)
+ copy(s[:32], sc[:])
+ return s
+}
+
+func makeDummySignature(committee dummySyncCommittee, signingRoot common.Hash, bitmask [params.SyncCommitteeBitmaskSize]byte) (sig [params.BlsSignatureSize]byte) {
+ for i, b := range committee[:] {
+ sig[i] = b ^ signingRoot[i]
+ }
+ copy(sig[32:], bitmask[:])
+ return
+}
diff --git a/beacon/light/sync/head_sync.go b/beacon/light/sync/head_sync.go
new file mode 100644
index 0000000000..2fd901130c
--- /dev/null
+++ b/beacon/light/sync/head_sync.go
@@ -0,0 +1,105 @@
+// 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 .
+
+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
+}
diff --git a/beacon/light/sync/update_sync.go b/beacon/light/sync/update_sync.go
new file mode 100644
index 0000000000..5d00c76829
--- /dev/null
+++ b/beacon/light/sync/update_sync.go
@@ -0,0 +1,181 @@
+// 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 .
+
+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"
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/log"
+)
+
+const maxUpdateRequest = 8
+
+type checkpointInitServer interface {
+ request.RequestServer
+ CanRequestBootstrap() bool
+ RequestBootstrap(checkpointHash common.Hash, response func(*light.CheckpointData))
+}
+
+type CheckpointInit struct {
+ request.SingleLock
+ lock sync.Mutex
+ chain *light.CommitteeChain
+ cs *light.CheckpointStore
+ checkpointHash common.Hash
+ initialized bool
+
+ InitTrigger request.ModuleTrigger
+}
+
+func NewCheckpointInit(chain *light.CommitteeChain, cs *light.CheckpointStore, checkpointHash common.Hash) *CheckpointInit {
+ return &CheckpointInit{
+ chain: chain,
+ cs: cs,
+ checkpointHash: checkpointHash,
+ }
+}
+
+func (s *CheckpointInit) Process(servers []*request.Server) bool {
+ s.lock.Lock()
+ defer s.lock.Unlock()
+
+ if s.initialized {
+ return false
+ }
+ if checkpoint := s.cs.Get(s.checkpointHash); checkpoint != nil {
+ checkpoint.InitChain(s.chain)
+ s.initialized = true
+ s.InitTrigger.Trigger()
+ return false
+ }
+ srv := request.SelectServer(servers, func(server *request.Server) uint64 {
+ if cserver, ok := server.RequestServer.(checkpointInitServer); ok && cserver.CanRequestBootstrap() && s.CanSend(server) {
+ return 1
+ }
+ 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)
+ if checkpoint == nil || !checkpoint.Validate() {
+ server.Fail("error retrieving checkpoint data")
+ return
+ }
+ checkpoint.InitChain(s.chain)
+ s.cs.Store(checkpoint)
+ s.initialized = true
+ s.InitTrigger.Trigger()
+ })
+ return true
+}
+
+type forwardUpdateServer interface {
+ request.RequestServer
+ UpdateRange() types.PeriodRange
+ RequestUpdates(first, count uint64, response func([]*types.LightClientUpdate, []*types.SerializedCommittee))
+}
+
+type ForwardUpdateSyncer struct {
+ request.SingleLock
+ lock sync.Mutex
+ chain *light.CommitteeChain
+
+ NewUpdateTrigger request.ModuleTrigger
+}
+
+func NewForwardUpdateSyncer(chain *light.CommitteeChain) *ForwardUpdateSyncer {
+ return &ForwardUpdateSyncer{chain: chain}
+}
+
+func (s *ForwardUpdateSyncer) Process(servers []*request.Server) bool {
+ s.lock.Lock()
+ defer s.lock.Unlock()
+
+ first, ok := s.chain.NextSyncPeriod()
+ if !ok {
+ return true
+ }
+ srv := request.SelectServer(servers, func(server *request.Server) uint64 {
+ if fserver, ok := server.RequestServer.(forwardUpdateServer); ok && s.CanSend(server) {
+ updateRange := fserver.UpdateRange()
+ if first < updateRange.First {
+ return 0
+ }
+ return updateRange.AfterLast
+ }
+ return 0
+ })
+ if srv == nil {
+ return true
+ }
+ server := srv.RequestServer.(forwardUpdateServer)
+ updateRange := server.UpdateRange()
+ if updateRange.AfterLast <= first {
+ return true
+ }
+ reqId, ok := s.TrySend(srv)
+ if !ok {
+ return true
+ }
+ count := updateRange.AfterLast - first
+ if count > maxUpdateRequest { //TODO const
+ count = maxUpdateRequest
+ }
+ server.RequestUpdates(first, count, func(updates []*types.LightClientUpdate, committees []*types.SerializedCommittee) {
+ s.lock.Lock()
+ defer s.lock.Unlock()
+
+ s.Returned(srv, reqId)
+ if len(updates) != int(count) || len(committees) != int(count) {
+ server.Fail("wrong number of updates received")
+ return
+ }
+ for i, update := range updates {
+ if update.Header.SyncPeriod() != first+uint64(i) {
+ server.Fail("update with wrong sync period received")
+ return
+ }
+ if err := s.chain.InsertUpdate(update, committees[i]); err != nil {
+ if err == light.ErrInvalidUpdate || err == light.ErrWrongCommitteeRoot || err == light.ErrCannotReorg {
+ server.Fail("invalid update received")
+ } else {
+ log.Error("Unexpected InsertUpdate error", "error", err)
+ }
+ if i != 0 {
+ s.NewUpdateTrigger.Trigger()
+ }
+ return
+ }
+ }
+ s.NewUpdateTrigger.Trigger()
+ })
+ return true
+}
diff --git a/beacon/light/types/committee.go b/beacon/light/types/committee.go
new file mode 100644
index 0000000000..08beb61d7f
--- /dev/null
+++ b/beacon/light/types/committee.go
@@ -0,0 +1,204 @@
+// 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 .
+
+package types
+
+import (
+ "encoding/json"
+ "errors"
+ "math/bits"
+
+ "github.com/ethereum/go-ethereum/beacon/params"
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/common/hexutil"
+ "github.com/minio/sha256-simd"
+ bls "github.com/protolambda/bls12-381-util"
+)
+
+const SerializedCommitteeSize = (params.SyncCommitteeSize + 1) * params.BlsPubkeySize
+
+type SerializedCommittee [SerializedCommitteeSize]byte
+
+// jsonSyncCommittee is the JSON representation of a sync committee
+//
+// See data structure definition here:
+// https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/beacon-chain.md#syncaggregate
+type jsonSyncCommittee struct {
+ Pubkeys []hexutil.Bytes `json:"pubkeys"`
+ Aggregate hexutil.Bytes `json:"aggregate_pubkey"`
+}
+
+// MarshalJSON marshals as JSON.
+func (s *SerializedCommittee) MarshalJSON() ([]byte, error) {
+ sc := jsonSyncCommittee{Pubkeys: make([]hexutil.Bytes, params.SyncCommitteeSize)}
+ for i := range sc.Pubkeys {
+ sc.Pubkeys[i] = make(hexutil.Bytes, params.BlsPubkeySize)
+ copy(sc.Pubkeys[i][:], s[i*params.BlsPubkeySize:(i+1)*params.BlsPubkeySize])
+ }
+ sc.Aggregate = make(hexutil.Bytes, params.BlsPubkeySize)
+ copy(sc.Aggregate[:], s[params.SyncCommitteeSize*params.BlsPubkeySize:])
+ return json.Marshal(&sc)
+}
+
+// UnmarshalJSON unmarshals from JSON.
+func (s *SerializedCommittee) UnmarshalJSON(input []byte) error {
+ var sc jsonSyncCommittee
+ if err := json.Unmarshal(input, &sc); err != nil {
+ return err
+ }
+ if len(sc.Pubkeys) != params.SyncCommitteeSize {
+ return errors.New("Invalid number of pubkeys")
+ }
+ for i, key := range sc.Pubkeys {
+ if len(key) != params.BlsPubkeySize {
+ return errors.New("Invalid pubkey size")
+ }
+ copy(s[i*params.BlsPubkeySize:(i+1)*params.BlsPubkeySize], key[:])
+ }
+ if len(sc.Aggregate) != params.BlsPubkeySize {
+ return errors.New("Invalid pubkey size")
+ }
+ copy(s[params.SyncCommitteeSize*params.BlsPubkeySize:], sc.Aggregate[:])
+ return nil
+}
+
+// SerializedCommitteeRoot calculates the root hash of the binary tree representation
+// of a sync committee provided in serialized format
+func (s *SerializedCommittee) Root() common.Hash {
+ var (
+ hasher = sha256.New()
+ padding [64 - params.BlsPubkeySize]byte
+ data [params.SyncCommitteeSize]common.Hash
+ l = params.SyncCommitteeSize
+ )
+ for i := range data {
+ hasher.Reset()
+ hasher.Write(s[i*params.BlsPubkeySize : (i+1)*params.BlsPubkeySize])
+ hasher.Write(padding[:])
+ hasher.Sum(data[i][:0])
+ }
+ for l > 1 {
+ for i := 0; i < l/2; i++ {
+ hasher.Reset()
+ hasher.Write(data[i*2][:])
+ hasher.Write(data[i*2+1][:])
+ hasher.Sum(data[i][:0])
+ }
+ l /= 2
+ }
+ hasher.Reset()
+ hasher.Write(s[SerializedCommitteeSize-params.BlsPubkeySize : SerializedCommitteeSize])
+ hasher.Write(padding[:])
+ hasher.Sum(data[1][:0])
+ hasher.Reset()
+ hasher.Write(data[0][:])
+ hasher.Write(data[1][:])
+ hasher.Sum(data[0][:0])
+ return data[0]
+}
+
+func (s *SerializedCommittee) Deserialize() (*SyncCommittee, error) {
+ sc := new(SyncCommittee)
+ for i := 0; i <= params.SyncCommitteeSize; i++ {
+ pk := new(bls.Pubkey)
+ var sk [params.BlsPubkeySize]byte
+ copy(sk[:], s[i*params.BlsPubkeySize:(i+1)*params.BlsPubkeySize])
+ if err := pk.Deserialize(&sk); err != nil {
+ return nil, err
+ }
+ if i < params.SyncCommitteeSize {
+ sc.keys[i] = pk
+ } else {
+ sc.aggregate = pk
+ }
+ }
+ return sc, nil
+}
+
+// SyncCommittee is a set of sync committee signer pubkeys
+//
+// See data structure definition here:
+// https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/beacon-chain.md#syncaggregate
+type SyncCommittee struct {
+ keys [params.SyncCommitteeSize]*bls.Pubkey
+ aggregate *bls.Pubkey
+}
+
+func (sc *SyncCommittee) VerifySignature(signingRoot common.Hash, aggregate *SyncAggregate) bool {
+ var (
+ sig bls.Signature
+ signerKeys [params.SyncCommitteeSize]*bls.Pubkey
+ signerCount int
+ )
+ if err := sig.Deserialize(&aggregate.Signature); err != nil {
+ return false
+ }
+ for i, key := range sc.keys {
+ if aggregate.BitMask[i/8]&(byte(1)<<(i%8)) != 0 {
+ signerKeys[signerCount] = key
+ signerCount++
+ }
+ }
+ return bls.FastAggregateVerify(signerKeys[:signerCount], signingRoot[:], &sig)
+}
+
+// SyncAggregate represents an aggregated BLS signature with BitMask referring
+// to a subset of the corresponding sync committee
+//
+// See data structure definition here:
+// https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/beacon-chain.md#syncaggregate
+type SyncAggregate struct {
+ BitMask [params.SyncCommitteeBitmaskSize]byte
+ Signature [params.BlsSignatureSize]byte
+}
+
+type jsonSyncAggregate struct {
+ BitMask hexutil.Bytes `json:"sync_committee_bits"`
+ Signature hexutil.Bytes `json:"sync_committee_signature"`
+}
+
+// MarshalJSON marshals as JSON.
+func (s *SyncAggregate) MarshalJSON() ([]byte, error) {
+ return json.Marshal(&jsonSyncAggregate{
+ BitMask: hexutil.Bytes(s.BitMask[:]),
+ Signature: hexutil.Bytes(s.Signature[:]),
+ })
+}
+
+// UnmarshalJSON unmarshals from JSON.
+func (s *SyncAggregate) UnmarshalJSON(input []byte) error {
+ var sc jsonSyncAggregate
+ if err := json.Unmarshal(input, &sc); err != nil {
+ return err
+ }
+ if len(sc.BitMask) != params.SyncCommitteeBitmaskSize {
+ return errors.New("Invalid aggregate bitmask size")
+ }
+ if len(sc.Signature) != params.BlsSignatureSize {
+ return errors.New("Invalid signature size")
+ }
+ copy(s.BitMask[:], []byte(sc.BitMask))
+ copy(s.Signature[:], []byte(sc.Signature))
+ return nil
+}
+
+func (s *SyncAggregate) SignerCount() int {
+ var count int
+ for _, v := range s.BitMask {
+ count += bits.OnesCount8(v)
+ }
+ return count
+}
diff --git a/beacon/light/types/forks.go b/beacon/light/types/forks.go
new file mode 100644
index 0000000000..9cd4963e40
--- /dev/null
+++ b/beacon/light/types/forks.go
@@ -0,0 +1,180 @@
+// Copyright 2022 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 .
+
+package types
+
+import (
+ "bufio"
+ "fmt"
+ "io"
+ "os"
+ "sort"
+ "strconv"
+ "strings"
+
+ "github.com/ethereum/go-ethereum/beacon/merkle"
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/common/hexutil"
+ "github.com/ethereum/go-ethereum/log"
+ "github.com/minio/sha256-simd"
+)
+
+// Fork describes a single beacon chain fork and also stores the calculated
+// signature domain used after this fork.
+type Fork struct {
+ Epoch uint64 // epoch when given fork version is activated
+ Name string // name of the fork in the chain config (config.yaml) file
+ // See fork version definition here:
+ // https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#custom-types
+ Version []byte // fork version
+ domain merkle.Value // calculated by computeDomain, based on fork version and genesis validators root
+}
+
+// Forks is the list of all beacon chain forks in the chain configuration.
+type Forks []Fork
+
+// Fork returns the fork belonging to the given epoch
+func (bf Forks) Fork(epoch uint64) (Fork, bool) {
+ for i := len(bf) - 1; i >= 0; i-- {
+ if epoch >= bf[i].Epoch {
+ return bf[i], true
+ }
+ }
+ return Fork{}, false
+}
+
+// domain returns the signature domain for the given epoch (assumes that domains
+// have already been calculated).
+func (bf Forks) domain(epoch uint64) merkle.Value {
+ fork, ok := bf.Fork(epoch)
+ if !ok {
+ log.Error("Fork domain unknown", "epoch", epoch)
+ }
+ return fork.domain
+}
+
+// computeDomain returns the signature domain based on the given fork version
+// and genesis validator set root
+func computeDomain(forkVersion []byte, genesisValidatorsRoot common.Hash) merkle.Value {
+ var (
+ hasher = sha256.New()
+ forkVersion32 merkle.Value
+ forkDataRoot merkle.Value
+ domain merkle.Value
+ )
+ copy(forkVersion32[:len(forkVersion)], forkVersion)
+ hasher.Write(forkVersion32[:])
+ hasher.Write(genesisValidatorsRoot[:])
+ hasher.Sum(forkDataRoot[:0])
+ domain[0] = 7
+ copy(domain[4:], forkDataRoot[:28])
+ return domain
+}
+
+// computeDomains calculates and stores signature domains for each fork in the list.
+func (bf Forks) ComputeDomains(genesisValidatorsRoot common.Hash) {
+ for i := range bf {
+ bf[i].domain = computeDomain(bf[i].Version, genesisValidatorsRoot)
+ }
+}
+
+// signingRoot calculates the signing root of the given header.
+func (bf Forks) SigningRoot(header Header) common.Hash {
+ var (
+ signingRoot common.Hash
+ headerHash = header.Hash()
+ hasher = sha256.New()
+ domain = bf.domain(header.Epoch())
+ )
+ hasher.Write(headerHash[:])
+ hasher.Write(domain[:])
+ hasher.Sum(signingRoot[:0])
+ return signingRoot
+}
+
+func (f Forks) Len() int { return len(f) }
+func (f Forks) Swap(i, j int) { f[i], f[j] = f[j], f[i] }
+func (f Forks) Less(i, j int) bool { return f[i].Epoch < f[j].Epoch }
+
+// fieldValue checks if the given fork parameter field is present in the given line
+// and if it is then returns the field value and the name of the fork it belongs to.
+func fieldValue(line, field string) (name, value string, ok bool) {
+ if pos := strings.Index(line, field); pos >= 0 {
+ cutFrom := strings.Index(line, "#") // cut in-line comments
+ if cutFrom < 0 {
+ cutFrom = len(line)
+ }
+ return line[:pos], strings.TrimSpace(line[pos+len(field) : cutFrom]), true
+ }
+ return "", "", false
+}
+
+// LoadForks parses the beacon chain configuration file (config.yaml) and extracts
+// the list of forks
+func LoadForks(fileName string) (Forks, error) {
+ file, err := os.Open(fileName)
+ if err != nil {
+ return nil, fmt.Errorf("Error opening beacon chain config file: %v", err)
+ }
+ defer file.Close()
+ var (
+ forks Forks
+ forkVersions = make(map[string][]byte)
+ forkEpochs = make(map[string]uint64)
+ reader = bufio.NewReader(file)
+ )
+ forkEpochs["GENESIS"] = 0
+
+ for {
+ l, _, err := reader.ReadLine()
+ if err == io.EOF {
+ break
+ }
+ if err != nil {
+ return nil, fmt.Errorf("Error reading beacon chain config file: %v", err)
+ }
+ line := string(l)
+ if name, value, ok := fieldValue(line, "_FORK_VERSION:"); ok {
+ if v, err := hexutil.Decode(value); err == nil {
+ forkVersions[name] = v
+ } else {
+ return nil, fmt.Errorf("Error decoding hex fork id \"%s\" in beacon chain config file: %v", value, err)
+ }
+ }
+ if name, value, ok := fieldValue(line, "_FORK_EPOCH:"); ok {
+ if v, err := strconv.ParseUint(value, 10, 64); err == nil {
+ forkEpochs[name] = v
+ } else {
+ return nil, fmt.Errorf("Error parsing epoch number \"%s\" in beacon chain config file: %v", value, err)
+ }
+ }
+ }
+
+ for name, epoch := range forkEpochs {
+ if version, ok := forkVersions[name]; ok {
+ delete(forkVersions, name)
+ forks = append(forks, Fork{Epoch: epoch, Name: name, Version: version})
+ } else {
+ return nil, fmt.Errorf("Fork id missing for \"%s\" in beacon chain config file", name)
+ }
+ }
+
+ for name := range forkVersions {
+ return nil, fmt.Errorf("Epoch number missing for fork \"%s\" in beacon chain config file", name)
+ }
+ sort.Sort(forks)
+ return forks, nil
+}
diff --git a/beacon/light/types/header.go b/beacon/light/types/header.go
new file mode 100644
index 0000000000..cd4731ffb9
--- /dev/null
+++ b/beacon/light/types/header.go
@@ -0,0 +1,158 @@
+// Copyright 2022 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 .
+
+package types
+
+import (
+ "encoding/binary"
+ "encoding/json"
+
+ "github.com/ethereum/go-ethereum/beacon/merkle"
+ "github.com/ethereum/go-ethereum/beacon/params"
+ "github.com/ethereum/go-ethereum/common"
+)
+
+// Header defines a beacon header
+//
+// See data structure definition here:
+// https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#beaconblockheader
+type Header struct {
+ Slot uint64
+ ProposerIndex uint64
+ ParentRoot common.Hash
+ StateRoot common.Hash
+ BodyRoot common.Hash
+}
+
+// Header defines a beacon header and supports JSON encoding according to the
+// standard beacon API format
+//
+// See data structure definition here:
+// https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#beaconblockheader
+type jsonHeader struct {
+ Slot common.Decimal `json:"slot"`
+ ProposerIndex common.Decimal `json:"proposer_index"`
+ ParentRoot common.Hash `json:"parent_root"`
+ StateRoot common.Hash `json:"state_root"`
+ BodyRoot common.Hash `json:"body_root"`
+}
+
+// MarshalJSON marshals as JSON.
+func (bh *Header) MarshalJSON() ([]byte, error) {
+ return json.Marshal(&jsonHeader{
+ Slot: common.Decimal(bh.Slot),
+ ProposerIndex: common.Decimal(bh.ProposerIndex),
+ ParentRoot: bh.ParentRoot,
+ StateRoot: bh.StateRoot,
+ BodyRoot: bh.BodyRoot,
+ })
+}
+
+// UnmarshalJSON unmarshals from JSON.
+func (bh *Header) UnmarshalJSON(input []byte) error {
+ var dec jsonHeader
+ if err := json.Unmarshal(input, &dec); err != nil {
+ return err
+ }
+ bh.Slot = uint64(dec.Slot)
+ bh.ProposerIndex = uint64(dec.ProposerIndex)
+ bh.ParentRoot = dec.ParentRoot
+ bh.StateRoot = dec.StateRoot
+ bh.BodyRoot = dec.BodyRoot
+ return nil
+}
+
+// Hash calculates the block root of the header
+func (bh *Header) Hash() common.Hash {
+ var values [8]merkle.Value // values corresponding to indices 8 to 15 of the beacon header tree
+ binary.LittleEndian.PutUint64(values[params.BhiSlot-8][:8], bh.Slot)
+ binary.LittleEndian.PutUint64(values[params.BhiProposerIndex-8][:8], bh.ProposerIndex)
+ values[params.BhiParentRoot-8] = merkle.Value(bh.ParentRoot)
+ values[params.BhiStateRoot-8] = merkle.Value(bh.StateRoot)
+ values[params.BhiBodyRoot-8] = merkle.Value(bh.BodyRoot)
+ return merkle.MultiProof{Format: merkle.NewRangeFormat(8, 15, nil), Values: values[:]}.RootHash()
+}
+
+// Epoch returns the epoch the header belongs to
+func (bh *Header) Epoch() uint64 {
+ return bh.Slot >> params.Log2EpochLength
+}
+
+// SyncPeriod returns the sync period the header belongs to
+func (bh *Header) SyncPeriod() uint64 {
+ return bh.Slot >> params.Log2SyncPeriodLength
+}
+
+// PeriodStart returns the first slot of the given period
+func PeriodStart(period uint64) uint64 {
+ return period << params.Log2SyncPeriodLength
+}
+
+// PeriodOfSlot returns the sync period that the given slot belongs to
+func PeriodOfSlot(slot uint64) uint64 {
+ return slot >> params.Log2SyncPeriodLength
+}
+
+// HeaderWithoutState stores beacon header fields except the state root which can
+// be reconstructed from a partial beacon state proof stored alongside the header
+type HeaderWithoutState struct {
+ Slot uint64
+ ProposerIndex uint64
+ ParentRoot, BodyRoot common.Hash
+}
+
+// Hash calculates the block root of the header
+func (bh *HeaderWithoutState) Hash(stateRoot common.Hash) common.Hash {
+ return bh.Proof(stateRoot).RootHash()
+}
+
+// Proof returns a MultiProof of the header
+func (bh *HeaderWithoutState) Proof(stateRoot common.Hash) merkle.MultiProof {
+ var values [8]merkle.Value // values corresponding to indices 8 to 15 of the beacon header tree
+ binary.LittleEndian.PutUint64(values[params.BhiSlot-8][:8], bh.Slot)
+ binary.LittleEndian.PutUint64(values[params.BhiProposerIndex-8][:8], bh.ProposerIndex)
+ values[params.BhiParentRoot-8] = merkle.Value(bh.ParentRoot)
+ values[params.BhiStateRoot-8] = merkle.Value(stateRoot)
+ values[params.BhiBodyRoot-8] = merkle.Value(bh.BodyRoot)
+ return merkle.MultiProof{Format: merkle.NewRangeFormat(8, 15, nil), Values: values[:]}
+}
+
+// FullHeader reconstructs a full Header from a HeaderWithoutState and a state root
+func (bh *HeaderWithoutState) FullHeader(stateRoot common.Hash) Header {
+ return Header{
+ Slot: bh.Slot,
+ ProposerIndex: bh.ProposerIndex,
+ ParentRoot: bh.ParentRoot,
+ StateRoot: stateRoot,
+ BodyRoot: bh.BodyRoot,
+ }
+}
+
+// SignedHead represents a beacon header signed by a sync committee
+//
+// Note: this structure is created from either an optimistic update or an instant update:
+// https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/sync-protocol.md#lightclientoptimisticupdate
+// https://github.com/zsfelfoldi/beacon-APIs/blob/instant_update/apis/beacon/light_client/instant_update.yaml
+type SignedHead struct {
+ Header Header // signed beacon header
+ SyncAggregate SyncAggregate // sync committee signature aggregate
+ SignatureSlot uint64 // slot in which the signature has been created (newer than Header.Slot, determines the signing sync committee)
+}
+
+// SignerCount returns the number of individual signers in the signature aggregate
+func (s *SignedHead) SignerCount() int {
+ return s.SyncAggregate.SignerCount()
+}
diff --git a/beacon/light/types/protocol.go b/beacon/light/types/protocol.go
new file mode 100644
index 0000000000..37feb755f2
--- /dev/null
+++ b/beacon/light/types/protocol.go
@@ -0,0 +1,242 @@
+// Copyright 2022 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 .
+
+package types
+
+import (
+ "encoding/json"
+ "errors"
+
+ "github.com/ethereum/go-ethereum/beacon/merkle"
+ "github.com/ethereum/go-ethereum/beacon/params"
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/log"
+)
+
+const MaxUpdateScoresLength = 128 // max number of advertised update scores of most recent periods
+
+// LightClientUpdate is a proof of the next sync committee root based on a header
+// signed by the sync committee of the given period. Optionally the update can
+// prove quasi-finality by the signed header referring to a previous, finalized
+// header from the same period, and the finalized header referring to the next
+// sync committee root.
+//
+// See data structure definition here:
+// https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/sync-protocol.md#lightclientupdate
+type LightClientUpdate struct {
+ Header Header
+ SyncAggregate SyncAggregate
+ SignatureSlot uint64
+ NextSyncCommitteeRoot common.Hash
+ NextSyncCommitteeBranch merkle.Values
+ FinalizedHeader Header
+ FinalityBranch merkle.Values
+ score UpdateScore // not part of the encoding, calculated after decoding
+ scoreCalculated bool
+}
+
+type CommitteeUpdate struct {
+ Version string
+ Update *LightClientUpdate
+ NextSyncCommittee *SerializedCommittee
+}
+
+// See data structure definition here:
+// https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/sync-protocol.md#lightclientupdate
+type committeeUpdateJson struct {
+ Version string `json:"version"`
+ Data committeeUpdateData `json:"data"`
+}
+
+type committeeUpdateData struct {
+ Header JsonBeaconHeader `json:"attested_header"`
+ NextSyncCommittee *SerializedCommittee `json:"next_sync_committee"`
+ NextSyncCommitteeBranch merkle.Values `json:"next_sync_committee_branch"`
+ FinalizedHeader JsonBeaconHeader `json:"finalized_header"`
+ FinalityBranch merkle.Values `json:"finality_branch"`
+ SyncAggregate SyncAggregate `json:"sync_aggregate"`
+ SignatureSlot common.Decimal `json:"signature_slot"`
+}
+
+type JsonBeaconHeader struct {
+ Beacon Header `json:"beacon"`
+}
+
+// MarshalJSON marshals as JSON.
+func (u *CommitteeUpdate) MarshalJSON() ([]byte, error) {
+ return json.Marshal(&committeeUpdateJson{
+ Version: u.Version,
+ Data: committeeUpdateData{
+ Header: JsonBeaconHeader{Beacon: u.Update.Header},
+ NextSyncCommittee: u.NextSyncCommittee,
+ NextSyncCommitteeBranch: u.Update.NextSyncCommitteeBranch,
+ FinalizedHeader: JsonBeaconHeader{Beacon: u.Update.FinalizedHeader}, //TODO should we encode it when not present?
+ FinalityBranch: u.Update.FinalityBranch,
+ SyncAggregate: u.Update.SyncAggregate,
+ SignatureSlot: common.Decimal(u.Update.SignatureSlot),
+ },
+ })
+}
+
+// UnmarshalJSON unmarshals from JSON.
+func (u *CommitteeUpdate) UnmarshalJSON(input []byte) error {
+ var dec committeeUpdateJson
+ if err := json.Unmarshal(input, &dec); err != nil {
+ return err
+ }
+ u.Version = dec.Version
+ u.NextSyncCommittee = dec.Data.NextSyncCommittee
+ u.Update = &LightClientUpdate{
+ Header: dec.Data.Header.Beacon,
+ SyncAggregate: dec.Data.SyncAggregate,
+ SignatureSlot: uint64(dec.Data.SignatureSlot),
+ NextSyncCommitteeRoot: u.NextSyncCommittee.Root(),
+ NextSyncCommitteeBranch: dec.Data.NextSyncCommitteeBranch,
+ FinalizedHeader: dec.Data.FinalizedHeader.Beacon,
+ FinalityBranch: dec.Data.FinalityBranch,
+ }
+ return nil
+}
+
+// Validate verifies the validity of the update
+func (update *LightClientUpdate) Validate() error {
+ period := update.Header.SyncPeriod()
+ if PeriodOfSlot(update.SignatureSlot) != period {
+ return errors.New("signature slot and signed header are from different periods")
+ }
+ if update.hasFinalizedHeader() {
+ if update.FinalizedHeader.SyncPeriod() != period {
+ return errors.New("finalizedHeader is from previous period") // proves the same committee it is signed by
+ }
+ if root, ok := merkle.VerifySingleProof(update.FinalityBranch, params.BsiFinalBlock, merkle.Value(update.FinalizedHeader.Hash())); !ok || root != update.Header.StateRoot {
+ return errors.New("invalid FinalizedHeader merkle proof")
+ }
+ }
+ if root, ok := merkle.VerifySingleProof(update.NextSyncCommitteeBranch, params.BsiNextSyncCommittee, merkle.Value(update.NextSyncCommitteeRoot)); !ok || root != update.Header.StateRoot {
+ return errors.New("invalid NextSyncCommittee merkle proof")
+ }
+ return nil
+}
+
+// hasFinalizedHeader returns true if the update has a finalized header referred
+// by the signed header and referring to the next sync committee.
+// Note that in addition to this, a sufficient signer participation is also needed
+// in order to fulfill the quasi-finality condition (see UpdateScore.isFinalized).
+func (l *LightClientUpdate) hasFinalizedHeader() bool {
+ return l.FinalizedHeader.BodyRoot != (common.Hash{}) && l.FinalizedHeader.SyncPeriod() == l.Header.SyncPeriod()
+}
+
+// Score returns the UpdateScore describing the proof strength of the update
+// Note: thread safety can be ensured by always calling Score on a newly received
+// or decoded update before making it potentially available for other threads
+func (l *LightClientUpdate) Score() UpdateScore {
+ if l.scoreCalculated {
+ return l.score
+ }
+ l.score.SignerCount = uint32(l.SyncAggregate.SignerCount())
+ l.score.SubPeriodIndex = uint32(l.Header.Slot & 0x1fff)
+ l.score.FinalizedHeader = l.hasFinalizedHeader()
+ l.scoreCalculated = true
+ return l.score
+}
+
+// UpdateScore allows the comparison between updates at the same period in order
+// to find the best update chain that provides the strongest proof of being canonical.
+//
+// UpdateScores have a tightly packed binary encoding format for efficient p2p
+// protocol transmission. Each UpdateScore is encoded in 3 bytes.
+// When interpreted as a 24 bit little indian unsigned integer:
+// - the lowest 10 bits contain the number of signers in the header signature aggregate
+// - the next 13 bits contain the "sub-period index" which is he signed header's
+// slot modulo params.SyncPeriodLength (which is correlated with the risk of the chain being
+// re-orged before the previous period boundary in case of non-finalized updates)
+// - the highest bit is set when the update is finalized (meaning that the finality
+// header referenced by the signed header is in the same period as the signed
+// header, making reorgs before the period boundary impossible
+type UpdateScore struct {
+ SignerCount uint32 // number of signers in the header signature aggregate
+ SubPeriodIndex uint32 // signed header's slot modulo params.SyncPeriodLength
+ FinalizedHeader bool // update is considered finalized if has finalized header from the same period and 2/3 signatures
+}
+
+// isFinalized returns true if the update has a header signed by at least 2/3 of
+// the committee, referring to a finalized header that refers to the next sync
+// committee. This condition is a close approximation of the actual finality
+// condition that can only be verified by full beacon nodes.
+func (u *UpdateScore) isFinalized() bool {
+ return u.FinalizedHeader && u.SignerCount >= params.SyncCommitteeSupermajority
+}
+
+// BetterThan returns true if update u is considered better than w.
+func (u UpdateScore) BetterThan(w UpdateScore) bool {
+ var (
+ uFinalized = u.isFinalized()
+ wFinalized = w.isFinalized()
+ )
+ if uFinalized != wFinalized {
+ return uFinalized
+ }
+ return u.SignerCount > w.SignerCount
+}
+
+type PeriodRange struct {
+ First, AfterLast uint64
+}
+
+/*func (a PeriodRange) Shared(b PeriodRange) PeriodRange {
+ if b.First > a.First {
+ a.First = b.First
+ }
+ if b.AfterLast < a.AfterLast {
+ a.AfterLast = b.AfterLast
+ }
+ return a
+}
+
+func (a PeriodRange) IsValid() bool {
+ return a.AfterLast >= a.First
+}*/
+
+func (a PeriodRange) IsEmpty() bool {
+ return a.AfterLast == a.First
+}
+
+func (a PeriodRange) Includes(period uint64) bool {
+ return period >= a.First && period < a.AfterLast
+}
+
+func (a PeriodRange) CanExpand(period uint64) bool {
+ return a.IsEmpty() || (period+1 >= a.First && period <= a.AfterLast)
+}
+
+func (a *PeriodRange) Expand(period uint64) {
+ if a.IsEmpty() {
+ a.First, a.AfterLast = period, period+1
+ return
+ }
+ if a.Includes(period) {
+ return
+ }
+ if a.First == period+1 {
+ a.First--
+ return
+ }
+ if a.AfterLast == period {
+ a.AfterLast++
+ return
+ }
+ log.Error("Could not expand period range", "first", a.First, "")
+}
diff --git a/beacon/merkle/binary_merkle.go b/beacon/merkle/binary_merkle.go
new file mode 100644
index 0000000000..cf54a3a2e2
--- /dev/null
+++ b/beacon/merkle/binary_merkle.go
@@ -0,0 +1,494 @@
+// Copyright 2022 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 .
+
+package merkle
+
+import (
+ "math/bits"
+ "reflect"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/common/hexutil"
+ "github.com/ethereum/go-ethereum/log"
+ "github.com/minio/sha256-simd"
+)
+
+// Value represents either a 32 byte value or hash node in a binary merkle tree/partial proof
+type (
+ Value [32]byte
+ Values []Value
+)
+
+var ValueT = reflect.TypeOf(Value{})
+
+// UnmarshalJSON parses a merkle value in hex syntax.
+func (m *Value) UnmarshalJSON(input []byte) error {
+ return hexutil.UnmarshalFixedJSON(ValueT, input, m[:])
+}
+
+// VerifySingleProof verifies a Merkle proof branch for a single value in a
+// binary Merkle tree (index is a generalized tree index).
+func VerifySingleProof(proof Values, index uint64, value Value) (common.Hash, bool) {
+ hasher := sha256.New()
+ for _, proofHash := range proof {
+ hasher.Reset()
+ if index&1 == 0 {
+ hasher.Write(value[:])
+ hasher.Write(proofHash[:])
+ } else {
+ hasher.Write(proofHash[:])
+ hasher.Write(value[:])
+ }
+ hasher.Sum(value[:0])
+ index /= 2
+ if index == 0 {
+ return common.Hash{}, false
+ }
+ }
+ if index != 1 {
+ return common.Hash{}, false
+ }
+ return common.Hash(value), true
+}
+
+// ProofFormat defines the shape of a partial proof and allows traversing a subset of a tree
+type ProofFormat interface {
+ Children() (left, right ProofFormat) // either both or neither should be nil
+}
+
+// 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
+// hash is not available then subtrees are always traversed (first left, then right).
+// If internal hash is available then subtrees are only traversed if needed by the writer.
+type ProofReader interface {
+ Children() (left, right ProofReader) // subtrees accessible if not nil
+ ReadNode() (Value, bool) // hash should be available if children are nil (leaf node), optional otherwise (internal node)
+}
+
+// ProofWriter allow collecting data for a partial proof while a subset of a tree is traversed.
+type ProofWriter interface {
+ Children() (left, right ProofWriter) // all non-nil subtrees are traversed
+ WriteNode(Value) // called for every traversed tree node (both leaf and internal)
+}
+
+// TraverseProof traverses a reader and a writer defined on the same tree
+// simultaneously, copies data from the reader to the writer (if writer is not nil)
+// and returns the root hash. At least the shape defined by the writer is traversed;
+// subtrees not required by the writer are only traversed (with writer == nil)
+// if the hash of the internal tree node is not provided by the reader.
+func TraverseProof(reader ProofReader, writer ProofWriter) (common.Hash, bool) {
+ var (
+ wl ProofWriter
+ wr ProofWriter
+ )
+ if writer != nil {
+ wl, wr = writer.Children()
+ }
+ node, nodeAvailable := reader.ReadNode()
+ if nodeAvailable && wl == nil {
+ if writer != nil {
+ writer.WriteNode(node)
+ }
+ return common.Hash(node), true
+ }
+ rl, rr := reader.Children()
+ if rl == nil {
+ return common.Hash{}, false
+ }
+ lhash, ok := TraverseProof(rl, wl)
+ if !ok {
+ return common.Hash{}, false
+ }
+ rhash, ok := TraverseProof(rr, wr)
+ if !ok {
+ return common.Hash{}, false
+ }
+ if !nodeAvailable {
+ hasher := sha256.New()
+ hasher.Write(lhash[:])
+ hasher.Write(rhash[:])
+ hasher.Sum(node[:0])
+ }
+ if writer != nil {
+ writer.WriteNode(node)
+ }
+ return common.Hash(node), true
+}
+
+// MultiProof stores a partial Merkle tree proof
+type MultiProof struct {
+ Format ProofFormat
+ Values Values
+}
+
+// multiProofReader implements ProofReader based on a MultiProof and also allows
+// attaching further subtree readers at certain indices
+// Note: valuePtr is stored and copied as a reference because child readers read
+// from the same value list as the tree is traversed
+type multiProofReader struct {
+ format ProofFormat // corresponding proof format
+ values Values // proof values
+ valuePtr *int // next index to be read from values
+ index uint64 // generalized tree index
+ subtrees func(uint64) ProofReader // attached subtrees
+}
+
+// children implements ProofReader
+func (mpr multiProofReader) Children() (left, right ProofReader) {
+ lf, rf := mpr.format.Children()
+ if lf == nil {
+ if mpr.subtrees != nil {
+ if subtree := mpr.subtrees(mpr.index); subtree != nil {
+ return subtree.Children()
+ }
+ }
+ return nil, nil
+ }
+ return multiProofReader{format: lf, values: mpr.values, valuePtr: mpr.valuePtr, index: mpr.index * 2, subtrees: mpr.subtrees},
+ multiProofReader{format: rf, values: mpr.values, valuePtr: mpr.valuePtr, index: mpr.index*2 + 1, subtrees: mpr.subtrees}
+}
+
+// readNode implements ProofReader
+func (mpr multiProofReader) ReadNode() (Value, bool) {
+ if l, _ := mpr.format.Children(); l == nil && len(mpr.values) > *mpr.valuePtr {
+ hash := mpr.values[*mpr.valuePtr]
+ (*mpr.valuePtr)++
+ return hash, true
+ }
+ return Value{}, false
+}
+
+// Reader creates a multiProofReader for the given proof; if subtrees != nil
+// then also attaches subtree readers at indices where the function returns a
+// non-nil reader.
+// Note that the reader can only be traversed once as the values slice is
+// sequentially consumed.
+func (mp MultiProof) Reader(subtrees func(uint64) ProofReader) multiProofReader {
+ return multiProofReader{format: mp.Format, values: mp.Values, valuePtr: new(int), index: 1, subtrees: subtrees}
+}
+
+// Finished returns true if all values have been consumed by the traversal.
+// Should be checked after TraverseProof if received from an untrusted source in
+// order to prevent DoS attacks by excess proof values.
+func (mpr multiProofReader) Finished() bool {
+ return len(mpr.values) == *mpr.valuePtr
+}
+
+// rootHash returns the root hash of the proven structure.
+func (mp MultiProof) RootHash() common.Hash {
+ reader := mp.Reader(nil)
+ hash, ok := TraverseProof(reader, nil)
+ if !ok || !reader.Finished() {
+ log.Error("MultiProof.rootHash: invalid proof format")
+ }
+ return hash
+}
+
+// multiProofWriter implements ProofWriter and creates a MultiProof with the
+// previously specified format. Also allows attaching further subtree writers at
+// certain indices.
+// Note: values is stored and copied as a reference because child writers append
+// to the same value list as the tree is traversed
+type multiProofWriter struct {
+ format ProofFormat // target proof format
+ values *Values // target proof value list
+ index uint64 // generalized tree index
+ subtrees func(uint64) ProofWriter // attached subtrees
+}
+
+// NewMultiProofWriter creates a new multiproof writer with the specified format.
+// If subtrees != nil then further subtree writers are attached at indices where
+// the function returns a non-nil writer.
+// Note that the specified format should not include these attached subtrees;
+// they should be attached at leaf indices of the given format.
+// Also note that target can be nil in which case the nodes specified by the format
+// are traversed but not stored; subtree writers might still store tree data.
+func NewMultiProofWriter(format ProofFormat, target *Values, subtrees func(uint64) ProofWriter) multiProofWriter {
+ return multiProofWriter{format: format, values: target, index: 1, subtrees: subtrees}
+}
+
+// children implements ProofWriter
+func (mpw multiProofWriter) Children() (left, right ProofWriter) {
+ if mpw.subtrees != nil {
+ if subtree := mpw.subtrees(mpw.index); subtree != nil {
+ return subtree.Children()
+ }
+ }
+ lf, rf := mpw.format.Children()
+ if lf == nil {
+ return nil, nil
+ }
+ return multiProofWriter{format: lf, values: mpw.values, index: mpw.index * 2, subtrees: mpw.subtrees},
+ multiProofWriter{format: rf, values: mpw.values, index: mpw.index*2 + 1, subtrees: mpw.subtrees}
+}
+
+// writeNode implements ProofWriter
+func (mpw multiProofWriter) WriteNode(node Value) {
+ if mpw.values != nil {
+ if lf, _ := mpw.format.Children(); lf == nil {
+ *mpw.values = append(*mpw.values, node)
+ }
+ }
+ if mpw.subtrees != nil {
+ if subtree := mpw.subtrees(mpw.index); subtree != nil {
+ subtree.WriteNode(node)
+ }
+ }
+}
+
+// ProofFormatIndexMap creates a generalized tree index -> MultiProof value
+// slice index association map based on the given proof format.
+func ProofFormatIndexMap(f ProofFormat) map[uint64]int {
+ var (
+ m = make(map[uint64]int)
+ pos int
+ )
+ addToIndexMap(m, f, &pos, 1)
+ return m
+}
+
+// addToIndexMap recursively creates index associations for a given proof format subtree.
+func addToIndexMap(m map[uint64]int, f ProofFormat, pos *int, index uint64) {
+ l, r := f.Children()
+ if l == nil {
+ m[index] = *pos
+ (*pos)++
+ } else {
+ addToIndexMap(m, l, pos, index*2)
+ addToIndexMap(m, r, pos, index*2+1)
+ }
+}
+
+// ChildIndex returns the generalized tree index of a subtree node in terms of
+// the main tree where a is the main tree index of the subtree root and b is the
+// subtree index of the node in question.
+func ChildIndex(a, b uint64) uint64 {
+ return (a-1)<<(63-bits.LeadingZeros64(b)) + b
+}
+
+// IndexMapFormat implements ProofFormat based on an index map filled with
+// AddLeaf calls. Subtree formats can also be attached at certain indices.
+type IndexMapFormat struct {
+ leaves map[uint64]ProofFormat
+ index uint64
+}
+
+// NewIndexMapFormat returns an empty format.
+func NewIndexMapFormat() IndexMapFormat {
+ return IndexMapFormat{leaves: make(map[uint64]ProofFormat), index: 1}
+}
+
+// AddLeaf adds either a single leaf or attaches a subtree at the given tree index.
+func (f IndexMapFormat) AddLeaf(index uint64, subtree ProofFormat) IndexMapFormat {
+ if subtree != nil {
+ f.leaves[index] = subtree
+ }
+ for index > 1 {
+ index /= 2
+ f.leaves[index] = nil
+ }
+ return f
+}
+
+// children implements ProofFormat
+func (f IndexMapFormat) Children() (left, right ProofFormat) {
+ if st, ok := f.leaves[f.index]; ok {
+ if st != nil {
+ return st.Children()
+ }
+ return IndexMapFormat{leaves: f.leaves, index: f.index * 2}, IndexMapFormat{leaves: f.leaves, index: f.index*2 + 1}
+ }
+ return nil, nil
+}
+
+// rangeFormat defined a proof format with a continuous range of leaf indices.
+// Attaching subtree formats is also possible.
+type rangeFormat struct {
+ begin, end, index uint64 // begin and end should be on the same level
+ subtree func(uint64) ProofFormat
+}
+
+// NewRangeFormat creates a new rangeFormat with leafs in the begin..end range.
+// If subtrees != nil then further subtree formats are attached at indices where
+// the function returns a non-nil format.
+func NewRangeFormat(begin, end uint64, subtree func(uint64) ProofFormat) rangeFormat {
+ return rangeFormat{
+ begin: begin,
+ end: end,
+ index: 1,
+ subtree: subtree,
+ }
+}
+
+// children implements ProofFormat
+func (rf rangeFormat) Children() (left, right ProofFormat) {
+ var (
+ lzr = bits.LeadingZeros64(rf.begin)
+ lzi = bits.LeadingZeros64(rf.index)
+ )
+ if lzi < lzr {
+ return nil, nil
+ }
+ if lzi == lzr {
+ if rf.subtree != nil && rf.index >= rf.begin && rf.index <= rf.end {
+ if st := rf.subtree(rf.index); st != nil {
+ return st.Children()
+ }
+ }
+ return nil, nil
+ }
+ var (
+ // i1..i2 are the descendants of rf.index at the tree level where begin and end are located
+ i1 = rf.index << (lzi - lzr)
+ i2 = ((rf.index + 1) << (lzi - lzr)) - 1
+ )
+ if i1 <= rf.end && i2 >= rf.begin {
+ // Return child formats if there is an overlap (rf.index has any descendants
+ // in the begin..end range).
+ // Note that if begin..end only touches one of the returned child subtrees,
+ // we still return a rangeFormat for both branches and the other one will
+ // not have any further children (that child of rf.index will be stored
+ // in the proof as a single sibling node).
+ return rangeFormat{begin: rf.begin, end: rf.end, index: rf.index * 2, subtree: rf.subtree},
+ rangeFormat{begin: rf.begin, end: rf.end, index: rf.index*2 + 1, subtree: rf.subtree}
+ }
+ return nil, nil
+}
+
+// MergedFormat implements ProofFormat and realizes the union of the included
+// individual formats.
+type MergedFormat []ProofFormat
+
+// children implements ProofFormat
+func (m MergedFormat) Children() (left, right ProofFormat) {
+ var (
+ l = make(MergedFormat, 0, len(m))
+ r = make(MergedFormat, 0, len(m))
+ )
+ for _, f := range m {
+ if left, right := f.Children(); left != nil {
+ l = append(l, left)
+ r = append(r, right)
+ }
+ }
+ if len(l) > 0 {
+ return l, r
+ }
+ return nil, nil
+}
+
+// MergedReader implements ProofReader and realizes the union of the included
+// individual readers.
+// Note that the readers belonging to the same structure (having the same root)
+// is not checked by MergedReader.
+// Also note that fully consuming underlying sequential readers is not guaranteed
+// (MultiProofReader.Finalized will not necessarily return true so if necessary
+// then the well-formedness of individual multiproofs should be checked separately).
+type MergedReader []ProofReader
+
+// children implements ProofReader
+func (m MergedReader) Children() (left, right ProofReader) {
+ var (
+ l = make(MergedReader, 0, len(m))
+ r = make(MergedReader, 0, len(m))
+ )
+ for _, reader := range m {
+ if left, right := reader.Children(); left != nil {
+ l = append(l, left)
+ r = append(r, right)
+ }
+ }
+ if len(l) > 0 {
+ return l, r
+ }
+ return nil, nil
+}
+
+// readNode implements ProofReader
+func (m MergedReader) ReadNode() (value Value, ok bool) {
+ var hasChildren bool
+ for _, reader := range m {
+ if left, _ := reader.Children(); left != nil {
+ // ensure that all readers are fully traversed
+ hasChildren = true
+ }
+ if v, o := reader.ReadNode(); o {
+ value, ok = v, o
+ }
+ }
+ if hasChildren {
+ return Value{}, false
+ }
+ return
+}
+
+// MergedWriter implements ProofWriter and realizes the union of the included
+// individual writers. The shape traversed by MergedWriter is the union of the
+// shapes traversed by individual writers.
+type MergedWriter []ProofWriter
+
+// children implements ProofWriter
+func (m MergedWriter) Children() (left, right ProofWriter) {
+ var (
+ l = make(MergedWriter, 0, len(m))
+ r = make(MergedWriter, 0, len(m))
+ )
+ for _, w := range m {
+ if left, right := w.Children(); left != nil {
+ l = append(l, left)
+ r = append(r, right)
+ }
+ }
+ if len(l) > 0 {
+ return l, r
+ }
+ return nil, nil
+}
+
+// writeNode implements ProofWriter
+func (m MergedWriter) WriteNode(value Value) {
+ for _, w := range m {
+ w.WriteNode(value)
+ }
+}
+
+// callbackWriter implements ProofWriter with a simple callback mechanism
+type callbackWriter struct {
+ format ProofFormat
+ index uint64
+ storeCallback func(uint64, Value)
+}
+
+// NewCallbackWriter creates a callbackWriter that traverses the tree subset
+// defined by the given proof format and calls callbackWriter for each traversed node
+func NewCallbackWriter(format ProofFormat, storeCallback func(uint64, Value)) callbackWriter {
+ return callbackWriter{format: format, index: 1, storeCallback: storeCallback}
+}
+
+// children implements ProofWriter
+func (cw callbackWriter) Children() (left, right ProofWriter) {
+ lf, rf := cw.format.Children()
+ if lf == nil {
+ return nil, nil
+ }
+ return callbackWriter{format: lf, index: cw.index * 2, storeCallback: cw.storeCallback},
+ callbackWriter{format: rf, index: cw.index*2 + 1, storeCallback: cw.storeCallback}
+}
+
+// writeNode implements ProofWriter
+func (cw callbackWriter) WriteNode(node Value) {
+ cw.storeCallback(cw.index, node)
+}
diff --git a/beacon/merkle/binary_merkle_test.go b/beacon/merkle/binary_merkle_test.go
new file mode 100644
index 0000000000..9dab56ba32
--- /dev/null
+++ b/beacon/merkle/binary_merkle_test.go
@@ -0,0 +1,259 @@
+// Copyright 2022 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 .
+
+package merkle
+
+import (
+ "math/bits"
+ "math/rand"
+ "testing"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/minio/sha256-simd"
+)
+
+func TestMergedFormat(t *testing.T) {
+ for count := 0; count < 1000; count++ {
+ single := NewIndexMapFormat()
+ merged := MergedFormat{}
+ for {
+ if rand.Intn(5) == 0 {
+ break
+ }
+ f := NewIndexMapFormat()
+ for {
+ if rand.Intn(5) == 0 {
+ break
+ }
+ index := uint64(rand.Intn(255) + 1)
+ single.AddLeaf(index, nil)
+ f.AddLeaf(index, nil)
+ }
+ merged = append(merged, f)
+ }
+ if !formatsEqual(single, merged) {
+ t.Errorf("Single and merged formats do not match")
+ }
+ }
+}
+
+func TestIndexMapSubtrees(t *testing.T) {
+ for count := 0; count < 1000; count++ {
+ single := NewIndexMapFormat()
+ withSubtrees := NewIndexMapFormat()
+ // put single leaves and subtrees randomly into a single row in order to avoid collisions
+ for index := uint64(256); index < 512; index++ {
+ switch rand.Intn(100) {
+ case 0: // put single leaf at index
+ single.AddLeaf(index, nil)
+ withSubtrees.AddLeaf(index, nil)
+ case 1: // put subtree at index
+ subtree := NewIndexMapFormat()
+ for {
+ subindex := uint64(rand.Intn(255) + 1)
+ single.AddLeaf(ChildIndex(index, subindex), nil)
+ subtree.AddLeaf(subindex, nil)
+ if rand.Intn(5) == 0 { // exit here in order to avoid empty subtrees
+ break
+ }
+ }
+ withSubtrees.AddLeaf(index, subtree)
+ }
+ }
+ if !formatsEqual(single, withSubtrees) {
+ t.Errorf("Single and subtree formats do not match")
+ }
+ }
+}
+
+func TestRangeFormat(t *testing.T) {
+ for count := 0; count < 1000; count++ {
+ single := NewIndexMapFormat()
+ begin := uint64(rand.Intn(255) + 1)
+ nextLevel := uint64(1)
+ for nextLevel <= begin {
+ nextLevel += nextLevel
+ }
+ end := begin + uint64(rand.Intn(int(nextLevel-begin)))
+ for i := begin; i <= end; i++ {
+ single.AddLeaf(i, nil)
+ }
+
+ var subFn func(index uint64) ProofFormat
+ if rand.Intn(2) == 0 {
+ subroot := begin + uint64(rand.Intn(int(end+1-begin)))
+ subBegin := uint64(rand.Intn(255) + 1)
+ nextLevel = uint64(1)
+ for nextLevel <= subBegin {
+ nextLevel += nextLevel
+ }
+ subEnd := subBegin + uint64(rand.Intn(int(nextLevel-subBegin)))
+ for i := subBegin; i <= subEnd; i++ {
+ single.AddLeaf(ChildIndex(subroot, i), nil)
+ }
+ subtree := NewRangeFormat(subBegin, subEnd, nil)
+ subFn = func(index uint64) ProofFormat {
+ if index == subroot {
+ return subtree
+ }
+ return nil
+ }
+ }
+
+ rangeFormat := NewRangeFormat(begin, end, subFn)
+ if !formatsEqual(single, rangeFormat) {
+ t.Errorf("Single and range formats do not match")
+ }
+ }
+}
+
+func TestSingleProof(t *testing.T) {
+ for index := uint64(1); index < 256; index++ {
+ proof := make(Values, 63-bits.LeadingZeros64(index))
+ writer := NewCallbackWriter(NewIndexMapFormat().AddLeaf(index, nil), func(i uint64, v Value) {
+ shift := bits.LeadingZeros64(i) - bits.LeadingZeros64(index)
+ if i^(index>>shift) == 1 {
+ proof[shift] = v
+ }
+ })
+ testTraverseProof(t, testProofReader, writer, true)
+ root, ok := VerifySingleProof(proof, index, testMerkleTree[index])
+ if root != common.Hash(testMerkleTree[1]) {
+ t.Errorf("VerifySingleProof root hash mismatch (index = %d)", index)
+ }
+ if !ok {
+ t.Errorf("VerifySingleProof length invalid (index = %d)", index)
+ }
+ }
+}
+
+func TestMultiProof(t *testing.T) {
+ for count := 0; count < 300; count++ {
+ failIndex := uint64(128 + rand.Intn(128))
+ indexList := make([]uint64, 10)
+ for i := range indexList {
+ for {
+ indexList[i] = uint64(128 + rand.Intn(128))
+ if indexList[i]^failIndex > 1 {
+ // failIndex should not be available in proofs so it should not be equal to or sibling of a stored index
+ break
+ }
+ }
+ }
+
+ readers := make([]ProofReader, len(indexList))
+ for i, index := range indexList {
+ var mp MultiProof
+ mp.Format = NewIndexMapFormat().AddLeaf(index, nil)
+ writer := NewMultiProofWriter(mp.Format, &mp.Values, nil)
+ testTraverseProof(t, testProofReader, writer, true)
+ readers[i] = mp.Reader(nil)
+ }
+
+ // create a single multiproof from the merged reader using a subset of indices
+ var mp MultiProof
+ format := NewIndexMapFormat()
+ mpCount := rand.Intn(11) // add a subset of indices to the created multiproof format
+ for i := 0; i < mpCount; i++ {
+ format.AddLeaf(indexList[i], nil)
+ }
+ mp.Format = format
+ expSuccess := rand.Intn(2) == 0
+ if !expSuccess {
+ // add an index that should not be available in the merged reader, expect the traversal to fail
+ format.AddLeaf(failIndex, nil)
+ }
+ testTraverseProof(t, MergedReader(readers), NewMultiProofWriter(format, &mp.Values, nil), expSuccess)
+
+ if expSuccess {
+ mpwCount := rand.Intn(mpCount + 1) // create writers for a subset of the previously selected indices (available in mp)
+ mps := make([]MultiProof, mpwCount)
+ writers := make([]ProofWriter, mpwCount)
+ for i := range mps {
+ mps[i].Format = NewIndexMapFormat().AddLeaf(indexList[i], nil)
+ writers[i] = NewMultiProofWriter(mps[i].Format, &mps[i].Values, nil)
+ }
+ reader := mp.Reader(nil)
+ testTraverseProof(t, reader, MergedWriter(writers), true)
+ if !reader.Finished() {
+ t.Errorf("MultiProofReader not finished")
+ }
+ // test individual single-value multiproofs
+ for i, mp := range mps {
+ if valueIndex, ok := ProofFormatIndexMap(mp.Format)[indexList[i]]; !ok || mp.Values[valueIndex] != testMerkleTree[indexList[i]] {
+ t.Errorf("Could not find tree index %d in single-value multiproof", indexList[i])
+ }
+ }
+ }
+ }
+}
+
+func testTraverseProof(t *testing.T, reader ProofReader, writer ProofWriter, expSuccess bool) {
+ root, ok := TraverseProof(reader, writer)
+ if expSuccess {
+ if root != common.Hash(testMerkleTree[1]) {
+ t.Errorf("TraverseProof root hash mismatch")
+ }
+ if !ok {
+ t.Errorf("TraverseProof insufficient reader data")
+ }
+ } else if ok {
+ t.Errorf("TraverseProof succeeded (expected to fail)")
+ }
+}
+
+func formatsEqual(f1, f2 ProofFormat) bool {
+ if f1 == nil && f2 == nil {
+ return true
+ }
+ if f1 == nil || f2 == nil {
+ return false
+ }
+ c1l, c1r := f1.Children()
+ c2l, c2r := f2.Children()
+ return formatsEqual(c1l, c2l) && formatsEqual(c1r, c2r)
+}
+
+type testReader byte
+
+var testProofReader = testReader(1)
+
+func (r testReader) Children() (left, right ProofReader) {
+ if r >= 128 {
+ return nil, nil
+ }
+ return r * 2, r*2 + 1
+}
+
+func (r testReader) ReadNode() (Value, bool) {
+ return testMerkleTree[r], true
+}
+
+var testMerkleTree [256]Value
+
+func init() {
+ hasher := sha256.New()
+ for i := byte(255); i >= 1; i-- {
+ if i >= 128 {
+ testMerkleTree[i][0] = i
+ } else {
+ hasher.Reset()
+ hasher.Write(testMerkleTree[i*2][:])
+ hasher.Write(testMerkleTree[i*2+1][:])
+ hasher.Sum(testMerkleTree[i][:0])
+ }
+ }
+}
diff --git a/beacon/params/constants.go b/beacon/params/constants.go
new file mode 100644
index 0000000000..3546b0fd90
--- /dev/null
+++ b/beacon/params/constants.go
@@ -0,0 +1,29 @@
+// Copyright 2022 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 .
+
+package params
+
+const (
+ SyncCommitteeSize = 512
+ SyncCommitteeBitmaskSize = SyncCommitteeSize / 8
+ SyncCommitteeSupermajority = (SyncCommitteeSize*2 + 2) / 3
+ BlsSignatureSize = 96
+ BlsPubkeySize = 48
+ SyncPeriodLength = 8192
+ Log2SyncPeriodLength = 13
+ EpochLength = 32
+ Log2EpochLength = 5
+)
diff --git a/beacon/params/tree_indices.go b/beacon/params/tree_indices.go
new file mode 100644
index 0000000000..9ca906e525
--- /dev/null
+++ b/beacon/params/tree_indices.go
@@ -0,0 +1,46 @@
+// Copyright 2022 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 .
+
+package params
+
+import (
+ "github.com/ethereum/go-ethereum/beacon/merkle"
+)
+
+const (
+ // beacon header fields
+ BhiSlot = 8
+ BhiProposerIndex = 9
+ BhiParentRoot = 10
+ BhiStateRoot = 11
+ BhiBodyRoot = 12
+
+ // beacon state fields
+ BsiGenesisTime = 32
+ BsiGenesisValidators = 33
+ BsiForkVersion = 141
+ BsiLatestHeader = 36
+ BsiBlockRoots = 37
+ BsiStateRoots = 38
+ BsiHistoricRoots = 39
+ BsiFinalBlock = 105
+ BsiSyncCommittee = 54
+ BsiNextSyncCommittee = 55
+ BsiExecPayload = 56
+ BsiExecHead = 908
+)
+
+var BsiFinalExecHash = merkle.ChildIndex(merkle.ChildIndex(BsiFinalBlock, BhiStateRoot), BsiExecHead)
diff --git a/cmd/blsync/config.go b/cmd/blsync/config.go
new file mode 100644
index 0000000000..cf5d22969a
--- /dev/null
+++ b/cmd/blsync/config.go
@@ -0,0 +1,183 @@
+// Copyright 2022 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 .
+
+package main
+
+import (
+ "context"
+
+ "github.com/ethereum/go-ethereum/beacon/light"
+ "github.com/ethereum/go-ethereum/beacon/light/types"
+ "github.com/ethereum/go-ethereum/cmd/utils"
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/common/hexutil"
+ "github.com/ethereum/go-ethereum/log"
+ "github.com/ethereum/go-ethereum/node"
+ "github.com/ethereum/go-ethereum/rpc"
+ "github.com/urfave/cli/v2"
+)
+
+var (
+ MainnetConfig = light.ChainConfig{
+ GenesisData: light.GenesisData{
+ GenesisValidatorsRoot: common.HexToHash("0x4b363db94e286120d76eb905340fdd4e54bfe9f06bf33ff6cf5ad27f511bfe95"),
+ GenesisTime: 1606824023,
+ },
+ Forks: types.Forks{
+ types.Fork{
+ Epoch: 0,
+ Name: "GENESIS",
+ Version: []byte{0, 0, 0, 0},
+ },
+ types.Fork{
+ Epoch: 74240,
+ Name: "ALTAIR",
+ Version: []byte{1, 0, 0, 0},
+ },
+ types.Fork{
+ Epoch: 144896,
+ Name: "BELLATRIX",
+ Version: []byte{2, 0, 0, 0},
+ },
+ },
+ Checkpoint: common.HexToHash("0x388be41594ec7d6a6894f18c73f3469f07e2c19a803de4755d335817ed8e2e5a"),
+ }
+
+ SepoliaConfig = light.ChainConfig{
+ GenesisData: light.GenesisData{
+ GenesisValidatorsRoot: common.HexToHash("0xd8ea171f3c94aea21ebc42a1ed61052acf3f9209c00e4efbaaddac09ed9b8078"),
+ GenesisTime: 1655733600,
+ },
+ Forks: types.Forks{
+ types.Fork{
+ Epoch: 0,
+ Name: "GENESIS",
+ Version: []byte{144, 0, 0, 105},
+ },
+ types.Fork{
+ Epoch: 50,
+ Name: "ALTAIR",
+ Version: []byte{144, 0, 0, 112},
+ },
+ types.Fork{
+ Epoch: 100,
+ Name: "BELLATRIX",
+ Version: []byte{144, 0, 0, 113},
+ },
+ types.Fork{
+ Epoch: 56832,
+ Name: "CAPELLA",
+ Version: []byte{144, 0, 0, 114},
+ },
+ },
+ Checkpoint: common.HexToHash("0x1005a6d9175e96bfbce4d35b80f468e9bff0b674e1e861d16e09e10005a58e81"),
+ }
+
+ GoerliConfig = light.ChainConfig{
+ GenesisData: light.GenesisData{
+ GenesisValidatorsRoot: common.HexToHash("0x043db0d9a83813551ee2f33450d23797757d430911a9320530ad8a0eabc43efb"),
+ GenesisTime: 1614588812,
+ },
+ Forks: types.Forks{
+ types.Fork{
+ Epoch: 0,
+ Name: "GENESIS",
+ Version: []byte{0, 0, 16, 32},
+ },
+ types.Fork{
+ Epoch: 36660,
+ Name: "ALTAIR",
+ Version: []byte{1, 0, 16, 32},
+ },
+ types.Fork{
+ Epoch: 112260,
+ Name: "BELLATRIX",
+ Version: []byte{2, 0, 16, 32},
+ },
+ },
+ Checkpoint: common.HexToHash("0x53a0f4f0a378e2c4ae0a9ee97407eb69d0d737d8d8cd0a5fb1093f42f7b81c49"),
+ }
+)
+
+func makeChainConfig(ctx *cli.Context) light.ChainConfig {
+ utils.CheckExclusive(ctx, utils.MainnetFlag, utils.GoerliFlag, utils.SepoliaFlag)
+ customConfig := ctx.IsSet(utils.BeaconConfigFlag.Name) || ctx.IsSet(utils.BeaconGenesisRootFlag.Name) || ctx.IsSet(utils.BeaconGenesisTimeFlag.Name)
+ var config light.ChainConfig
+ switch {
+ case ctx.Bool(utils.MainnetFlag.Name):
+ config = MainnetConfig
+ case ctx.Bool(utils.SepoliaFlag.Name):
+ config = SepoliaConfig
+ case ctx.Bool(utils.GoerliFlag.Name):
+ config = GoerliConfig
+ default:
+ if !customConfig {
+ config = MainnetConfig
+ }
+ }
+ if customConfig && config.Forks != nil {
+ utils.Fatalf("Cannot use custom beacon chain config flags in combination with pre-defined network config")
+ }
+ if ctx.IsSet(utils.BeaconConfigFlag.Name) {
+ forks, err := types.LoadForks(ctx.String(utils.BeaconConfigFlag.Name))
+ if err != nil {
+ utils.Fatalf("Could not load beacon chain config file", "file name", ctx.String(utils.BeaconConfigFlag.Name), "error", err)
+ }
+ config.Forks = forks
+ }
+ if ctx.IsSet(utils.BeaconGenesisRootFlag.Name) {
+ if c, err := hexutil.Decode(ctx.String(utils.BeaconGenesisRootFlag.Name)); err == nil && len(c) <= 32 {
+ copy(config.GenesisValidatorsRoot[:len(c)], c)
+ } else {
+ utils.Fatalf("Invalid hex string", "beacon.genesis.gvroot", ctx.String(utils.BeaconGenesisRootFlag.Name), "error", err)
+ }
+ }
+ if ctx.IsSet(utils.BeaconGenesisTimeFlag.Name) {
+ config.GenesisTime = ctx.Uint64(utils.BeaconGenesisTimeFlag.Name)
+ }
+ if ctx.IsSet(utils.BeaconCheckpointFlag.Name) {
+ if c, err := hexutil.Decode(ctx.String(utils.BeaconCheckpointFlag.Name)); err == nil && len(c) <= 32 {
+ copy(config.Checkpoint[:len(c)], c)
+ } else {
+ utils.Fatalf("Invalid hex string", "beacon.checkpoint", ctx.String(utils.BeaconCheckpointFlag.Name), "error", err)
+ }
+ }
+ return config
+}
+
+func makeRPCClient(ctx *cli.Context) *rpc.Client {
+ if !ctx.IsSet(utils.BlsyncApiFlag.Name) {
+ log.Warn("No engine API target specified, performing a dry run")
+ return nil
+ }
+ if !ctx.IsSet(utils.BlsyncJWTSecretFlag.Name) {
+ utils.Fatalf("JWT secret parameter missing") //TODO use default if datadir is specified
+ }
+
+ engineApiUrl, jwtFileName := ctx.String(utils.BlsyncApiFlag.Name), ctx.String(utils.BlsyncJWTSecretFlag.Name)
+ var jwtSecret [32]byte
+ if jwt, err := node.ObtainJWTSecret(jwtFileName); err == nil {
+ copy(jwtSecret[:], jwt)
+ } else {
+ utils.Fatalf("Error loading or generating JWT secret: %v", err)
+ }
+ auth := node.NewJWTAuth(jwtSecret)
+ cl, err := rpc.DialOptions(context.Background(), engineApiUrl, rpc.WithHTTPAuth(auth))
+ if err != nil {
+ utils.Fatalf("Could not create RPC client: %v", err)
+ }
+ return cl
+}
diff --git a/cmd/blsync/main.go b/cmd/blsync/main.go
new file mode 100644
index 0000000000..69176c7f25
--- /dev/null
+++ b/cmd/blsync/main.go
@@ -0,0 +1,234 @@
+// Copyright 2022 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 .
+
+package main
+
+import (
+ "context"
+ "fmt"
+ "os"
+ "strings"
+ "time"
+
+ "github.com/ethereum/go-ethereum/beacon/engine"
+ "github.com/ethereum/go-ethereum/beacon/light"
+ "github.com/ethereum/go-ethereum/beacon/light/api"
+ "github.com/ethereum/go-ethereum/beacon/light/request"
+ "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/beacon/params"
+ "github.com/ethereum/go-ethereum/cmd/utils"
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/common/lru"
+ "github.com/ethereum/go-ethereum/common/mclock"
+ ctypes "github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/ethdb/memorydb"
+ "github.com/ethereum/go-ethereum/internal/flags"
+ "github.com/ethereum/go-ethereum/log"
+ "github.com/ethereum/go-ethereum/rpc"
+ "github.com/urfave/cli/v2"
+)
+
+func main() {
+ log.Root().SetHandler(log.LvlFilterHandler(log.LvlInfo, log.StreamHandler(os.Stderr, log.TerminalFormat(true))))
+ app := flags.NewApp("beacon light syncer tool")
+ app.Flags = []cli.Flag{
+ utils.BeaconApiFlag,
+ utils.BeaconApiHeaderFlag,
+ utils.BeaconThresholdFlag,
+ utils.BeaconNoFilterFlag,
+ utils.BeaconConfigFlag,
+ utils.BeaconGenesisRootFlag,
+ utils.BeaconGenesisTimeFlag,
+ utils.BeaconCheckpointFlag,
+ //TODO datadir for optional permanent database
+ utils.MainnetFlag,
+ utils.SepoliaFlag,
+ utils.GoerliFlag,
+ utils.BlsyncApiFlag,
+ utils.BlsyncJWTSecretFlag,
+ }
+ app.Action = blsync
+
+ if err := app.Run(os.Args); err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }
+}
+
+var (
+ stateProofFormat merkle.ProofFormat // requested multiproof format
+ execBlockIndex int // index of execution block root in proof.Values where proof.Format == stateProofFormat
+ finalizedBlockIndex int // index of finalized block root in proof.Values where proof.Format == stateProofFormat
+)
+
+func blsync(ctx *cli.Context) error {
+ if !ctx.IsSet(utils.BeaconApiFlag.Name) {
+ utils.Fatalf("Beacon node light client API URL not specified")
+ }
+ stateProofFormat = merkle.NewIndexMapFormat().AddLeaf(params.BsiExecHead, nil).AddLeaf(params.BsiFinalBlock, nil)
+ var (
+ stateIndexMap = merkle.ProofFormatIndexMap(stateProofFormat)
+ chainConfig = makeChainConfig(ctx)
+ customHeader = make(map[string]string)
+ )
+ execBlockIndex = stateIndexMap[params.BsiExecHead]
+ finalizedBlockIndex = stateIndexMap[params.BsiFinalBlock]
+
+ for _, s := range utils.SplitAndTrim(ctx.String(utils.BeaconApiHeaderFlag.Name)) {
+ kv := strings.Split(s, ":")
+ if len(kv) != 2 {
+ utils.Fatalf("Invalid custom API header entry: %s", s)
+ }
+ customHeader[strings.TrimSpace(kv[0])] = strings.TrimSpace(kv[1])
+ }
+
+ var (
+ beaconApi = api.NewBeaconLightApi(ctx.String(utils.BeaconApiFlag.Name), customHeader)
+ db = memorydb.New()
+ 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() })
+ checkpointStore = light.NewCheckpointStore(db, committeeChain)
+ headTracker = light.NewHeadTracker(committeeChain)
+ scheduler = request.NewScheduler()
+ )
+ committeeChain.SetGenesisData(chainConfig.GenesisData)
+
+ checkpointInit := sync.NewCheckpointInit(committeeChain, checkpointStore, chainConfig.Checkpoint)
+ forwardSync := sync.NewForwardUpdateSyncer(committeeChain)
+ headSync := sync.NewHeadSyncer(headTracker, committeeChain)
+ scheduler.RegisterModule(checkpointInit)
+ scheduler.RegisterModule(forwardSync)
+ scheduler.RegisterModule(headSync)
+ scheduler.AddTriggers(forwardSync, []*request.ModuleTrigger{&checkpointInit.InitTrigger, &forwardSync.NewUpdateTrigger, &headSync.SignedHeadTrigger})
+ scheduler.AddTriggers(headSync, []*request.ModuleTrigger{&forwardSync.NewUpdateTrigger})
+
+ syncer := &execSyncer{
+ api: beaconApi,
+ client: makeRPCClient(ctx),
+ execRootCache: lru.NewCache[common.Hash, common.Hash](1000),
+ }
+ headTracker.Subscribe(threshold, syncer.newHead)
+ scheduler.Start()
+ scheduler.RegisterServer(api.NewSyncServer(beaconApi))
+ <-ctx.Done()
+ scheduler.Stop()
+ return nil
+}
+
+func callNewPayloadV1(client *rpc.Client, block *ctypes.Block) (string, error) {
+ var resp engine.PayloadStatusV1
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
+ err := client.CallContext(ctx, &resp, "engine_newPayloadV1", *engine.BlockToExecutableData(block, nil).ExecutionPayload)
+ cancel()
+ return resp.Status, err
+}
+
+func callForkchoiceUpdatedV1(client *rpc.Client, headHash, finalizedHash common.Hash) (string, error) {
+ var resp engine.ForkChoiceResponse
+ update := engine.ForkchoiceStateV1{
+ HeadBlockHash: headHash,
+ SafeBlockHash: finalizedHash,
+ FinalizedBlockHash: finalizedHash,
+ }
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
+ err := client.CallContext(ctx, &resp, "engine_forkchoiceUpdatedV1", update, nil)
+ cancel()
+ 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
+ }
+}
diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go
index b67671e03b..05fdced469 100644
--- a/cmd/utils/flags.go
+++ b/cmd/utils/flags.go
@@ -36,6 +36,7 @@ import (
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/accounts/keystore"
+ bparams "github.com/ethereum/go-ethereum/beacon/params"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/fdlimit"
"github.com/ethereum/go-ethereum/common/hexutil"
@@ -273,6 +274,58 @@ var (
Usage: "Manually specify the Cancun fork timestamp, overriding the bundled setting",
Category: flags.EthCategory,
}
+ // Beacon client light sync settings
+ BeaconApiFlag = &cli.StringFlag{
+ Name: "beacon.api",
+ Usage: "Beacon node (CL) light client API URL (currently only supports LodeStar)",
+ Category: flags.BeaconCategory,
+ }
+ BeaconApiHeaderFlag = &cli.StringFlag{
+ Name: "beacon.api.header",
+ Usage: "Remote beacon node API custom HTTP header fields (\"key:value,key:value\")",
+ Category: flags.BeaconCategory,
+ }
+ BeaconThresholdFlag = &cli.IntFlag{
+ Name: "beacon.threshold",
+ Usage: "Beacon sync committee participation threshold",
+ Value: bparams.SyncCommitteeSupermajority,
+ Category: flags.BeaconCategory,
+ }
+ BeaconNoFilterFlag = &cli.BoolFlag{
+ Name: "beacon.nofilter",
+ Usage: "Disable future slot signature filter",
+ Category: flags.BeaconCategory,
+ }
+ BeaconConfigFlag = &cli.StringFlag{
+ Name: "beacon.config",
+ Usage: "Beacon chain config YAML file",
+ Category: flags.BeaconCategory,
+ }
+ BeaconGenesisRootFlag = &cli.StringFlag{
+ Name: "beacon.genesis.gvroot",
+ Usage: "Beacon chain genesis validators root",
+ Category: flags.BeaconCategory,
+ }
+ BeaconGenesisTimeFlag = &cli.Uint64Flag{
+ Name: "beacon.genesis.time",
+ Usage: "Beacon chain genesis time",
+ Category: flags.BeaconCategory,
+ }
+ BeaconCheckpointFlag = &cli.StringFlag{
+ Name: "beacon.checkpoint",
+ Usage: "Beacon chain weak subjectivity checkpoint block hash",
+ Category: flags.BeaconCategory,
+ }
+ BlsyncApiFlag = &cli.StringFlag{
+ Name: "blsync.engine.api",
+ Usage: "Target EL engine API URL",
+ Category: flags.BeaconCategory,
+ }
+ BlsyncJWTSecretFlag = &cli.StringFlag{
+ Name: "blsync.jwtsecret",
+ Usage: "Path to a JWT secret to use for target engine API endpoint",
+ Category: flags.BeaconCategory,
+ }
// Light server and client settings
LightServeFlag = &cli.IntFlag{
Name: "light.serve",
diff --git a/common/types.go b/common/types.go
index 0b68a19dd3..b3397e1ec9 100644
--- a/common/types.go
+++ b/common/types.go
@@ -26,6 +26,7 @@ import (
"math/big"
"math/rand"
"reflect"
+ "strconv"
"strings"
"github.com/ethereum/go-ethereum/common/hexutil"
@@ -429,3 +430,22 @@ func (ma *MixedcaseAddress) ValidChecksum() bool {
func (ma *MixedcaseAddress) Original() string {
return ma.original
}
+
+type Decimal uint64
+
+func isString(input []byte) bool {
+ return len(input) >= 2 && input[0] == '"' && input[len(input)-1] == '"'
+}
+
+// UnmarshalJSON parses a hash in hex syntax.
+func (d *Decimal) UnmarshalJSON(input []byte) error {
+ if !isString(input) {
+ return &json.UnmarshalTypeError{Value: "non-string", Type: reflect.TypeOf(uint64(0))}
+ }
+ if i, err := strconv.ParseInt(string(input[1:len(input)-1]), 10, 64); err == nil {
+ *d = Decimal(i)
+ return nil
+ } else {
+ return err
+ }
+}
diff --git a/go.mod b/go.mod
index 68929fb8af..f515efbeca 100644
--- a/go.mod
+++ b/go.mod
@@ -18,6 +18,7 @@ require (
github.com/davecgh/go-spew v1.1.1
github.com/deckarep/golang-set/v2 v2.1.0
github.com/docker/docker v1.6.2
+ github.com/donovanhide/eventsource v0.0.0-20210830082556-c59027999da0
github.com/dop251/goja v0.0.0-20230122112309-96b1610dd4f7
github.com/edsrzf/mmap-go v1.0.0
github.com/ethereum/c-kzg-4844 v0.1.0
@@ -49,9 +50,13 @@ require (
github.com/kylelemons/godebug v1.1.0
github.com/mattn/go-colorable v0.1.13
github.com/mattn/go-isatty v0.0.16
+ github.com/minio/sha256-simd v1.0.0
github.com/naoina/toml v0.1.2-0.20170918210437-9fafd6967416
github.com/olekukonko/tablewriter v0.0.5
github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7
+ github.com/protolambda/bls12-381-util v0.0.0-20220416220906-d8552aa452c7
+ github.com/protolambda/zrnt v0.30.0
+ github.com/protolambda/ztyp v0.2.2
github.com/rs/cors v1.7.0
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible
github.com/status-im/keycard-go v0.2.0
@@ -99,7 +104,9 @@ require (
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/influxdata/line-protocol v0.0.0-20210311194329-9aa0e372d097 // indirect
+ github.com/kilic/bls12-381 v0.1.0 // indirect
github.com/klauspost/compress v1.15.15 // indirect
+ github.com/klauspost/cpuid/v2 v2.0.4 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/mattn/go-runewidth v0.0.9 // indirect
diff --git a/go.sum b/go.sum
index 7f463184be..9b8a6bc724 100644
--- a/go.sum
+++ b/go.sum
@@ -114,6 +114,8 @@ github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI=
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
github.com/docker/docker v1.6.2 h1:HlFGsy+9/xrgMmhmN+NGhCc5SHGJ7I+kHosRR1xc/aI=
github.com/docker/docker v1.6.2/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/donovanhide/eventsource v0.0.0-20210830082556-c59027999da0 h1:C7t6eeMaEQVy6e8CarIhscYQlNmw5e3G36y7l7Y21Ao=
+github.com/donovanhide/eventsource v0.0.0-20210830082556-c59027999da0/go.mod h1:56wL82FO0bfMU5RvfXoIwSOP2ggqqxT+tAfNEIyxuHw=
github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk=
github.com/dop251/goja v0.0.0-20230122112309-96b1610dd4f7 h1:kgvzE5wLsLa7XKfV85VZl40QXaMCaeFtHpPwJ8fhotY=
github.com/dop251/goja v0.0.0-20230122112309-96b1610dd4f7/go.mod h1:yRkwfj0CBpOGre+TwBsqPV0IH0Pk73e4PXJOeNDboGs=
@@ -236,6 +238,7 @@ github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao=
github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA=
+github.com/holiman/uint256 v1.2.0/go.mod h1:y4ga/t+u+Xwd7CpDgZESaRcWy0I7XMlTMA25ApIH5Jw=
github.com/holiman/uint256 v1.2.2-0.20230321075855-87b91420868c h1:DZfsyhDK1hnSS5lH8l+JggqzEleHteTYfutAiVlSUM8=
github.com/holiman/uint256 v1.2.2-0.20230321075855-87b91420868c/go.mod h1:SC8Ryt4n+UBbPbIBKaG9zbbDlp4jOru9xFZmPzLUTxw=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
@@ -276,6 +279,8 @@ github.com/kataras/iris/v12 v12.1.8/go.mod h1:LMYy4VlP67TQ3Zgriz8RE2h2kMZV2SgMYb
github.com/kataras/neffos v0.0.14/go.mod h1:8lqADm8PnbeFfL7CLXh1WHw53dG27MC3pgi2R1rmoTE=
github.com/kataras/pio v0.0.2/go.mod h1:hAoW0t9UmXi4R5Oyq5Z4irTbaTsOemSrDGUtaTl7Dro=
github.com/kataras/sitemap v0.0.5/go.mod h1:KY2eugMKiPwsJgx7+U103YZehfvNGOXURubcGyk0Bz8=
+github.com/kilic/bls12-381 v0.1.0 h1:encrdjqKMEvabVQ7qYOKu1OvhqpK4s47wDYtNiPtlp4=
+github.com/kilic/bls12-381 v0.1.0/go.mod h1:vDTTHJONJ6G+P2R74EhnyotQDTliQDnFEwhdmfzw1ig=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
@@ -283,6 +288,8 @@ github.com/klauspost/compress v1.9.7/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0
github.com/klauspost/compress v1.15.15 h1:EF27CXIuDsYJ6mmvtBRlEuB2UVOqHG1tAXgZ7yIO+lw=
github.com/klauspost/compress v1.15.15/go.mod h1:ZcK2JAFqKOpnBlxcLsJzYfrS9X1akm9fHZNnD9+Vo/4=
github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
+github.com/klauspost/cpuid/v2 v2.0.4 h1:g0I61F2K2DjRHz1cnxlkNSBIaePVoJIjjnHui8QHbiw=
+github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
@@ -324,6 +331,9 @@ github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zk
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/mediocregopher/radix/v3 v3.4.2/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8=
github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc=
+github.com/minio/sha256-simd v0.1.0/go.mod h1:2FMWW+8GMoPweT6+pI63m9YE3Lmw4J71hV56Chs1E/U=
+github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g=
+github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag=
@@ -381,6 +391,14 @@ github.com/prometheus/common v0.39.0 h1:oOyhkDq05hPZKItWVBkJ6g6AtGxi+fy7F4JvUV8u
github.com/prometheus/common v0.39.0/go.mod h1:6XBZ7lYdLCbkAVhwRsWTZn+IN5AB9F/NXd5w0BbEX0Y=
github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=
github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY=
+github.com/protolambda/bls12-381-util v0.0.0-20210720105258-a772f2aac13e/go.mod h1:MPZvj2Pr0N8/dXyTPS5REeg2sdLG7t8DRzC1rLv925w=
+github.com/protolambda/bls12-381-util v0.0.0-20220416220906-d8552aa452c7 h1:cZC+usqsYgHtlBaGulVnZ1hfKAi8iWtujBnRLQE698c=
+github.com/protolambda/bls12-381-util v0.0.0-20220416220906-d8552aa452c7/go.mod h1:IToEjHuttnUzwZI5KBSM/LOOW3qLbbrHOEfp3SbECGY=
+github.com/protolambda/messagediff v1.4.0/go.mod h1:LboJp0EwIbJsePYpzh5Op/9G1/4mIztMRYzzwR0dR2M=
+github.com/protolambda/zrnt v0.30.0 h1:pHEn69ZgaDFGpLGGYG1oD7DvYI7RDirbMBPfbC+8p4g=
+github.com/protolambda/zrnt v0.30.0/go.mod h1:qcdX9CXFeVNCQK/q0nswpzhd+31RHMk2Ax/2lMsJ4Jw=
+github.com/protolambda/ztyp v0.2.2 h1:rVcL3vBu9W/aV646zF6caLS/dyn9BN8NYiuJzicLNyY=
+github.com/protolambda/ztyp v0.2.2/go.mod h1:9bYgKGqg3wJqT9ac1gI2hnVb0STQq7p/1lapqrqY1dU=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
@@ -538,6 +556,7 @@ golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201101102859-da207088b7d1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210316164454-77fc1eacc6aa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
diff --git a/internal/flags/categories.go b/internal/flags/categories.go
index c2db6c6c1d..7b976e19a2 100644
--- a/internal/flags/categories.go
+++ b/internal/flags/categories.go
@@ -20,6 +20,7 @@ import "github.com/urfave/cli/v2"
const (
EthCategory = "ETHEREUM"
+ BeaconCategory = "BEACON CHAIN"
LightCategory = "LIGHT CLIENT"
DevCategory = "DEVELOPER CHAIN"
EthashCategory = "ETHASH"
diff --git a/node/node.go b/node/node.go
index e8494ac3b2..bc91c34ccb 100644
--- a/node/node.go
+++ b/node/node.go
@@ -338,15 +338,9 @@ func (n *Node) closeDataDir() {
}
}
-// obtainJWTSecret loads the jwt-secret, either from the provided config,
-// or from the default location. If neither of those are present, it generates
-// a new secret and stores to the default location.
-func (n *Node) obtainJWTSecret(cliParam string) ([]byte, error) {
- fileName := cliParam
- if len(fileName) == 0 {
- // no path provided, use default
- fileName = n.ResolvePath(datadirJWTKey)
- }
+// ObtainJWTSecret loads the jwt-secret from the provided config. If the file is not
+// present, it generates a new secret and stores to the given location.
+func ObtainJWTSecret(fileName string) ([]byte, error) {
// try reading from file
if data, err := os.ReadFile(fileName); err == nil {
jwtSecret := common.FromHex(strings.TrimSpace(string(data)))
@@ -372,6 +366,18 @@ func (n *Node) obtainJWTSecret(cliParam string) ([]byte, error) {
return jwtSecret, nil
}
+// obtainJWTSecret loads the jwt-secret, either from the provided config,
+// or from the default location. If neither of those are present, it generates
+// a new secret and stores to the default location.
+func (n *Node) obtainJWTSecret(cliParam string) ([]byte, error) {
+ fileName := cliParam
+ if len(fileName) == 0 {
+ // no path provided, use default
+ fileName = n.ResolvePath(datadirJWTKey)
+ }
+ return ObtainJWTSecret(fileName)
+}
+
// startRPC is a helper method to configure all the various RPC endpoints during node
// startup. It's not meant to be called at any time afterwards as it makes certain
// assumptions about the state of the node.