cmd/blsync, beacon/light: standalone beacon light sync tool

This commit is contained in:
Zsolt Felfoldi 2022-09-20 21:36:31 +02:00
parent 2169fa343a
commit 4f67f433c8
27 changed files with 4362 additions and 9 deletions

View file

@ -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 <http://www.gnu.org/licenses/>.
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
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
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()
}

109
beacon/light/checkpoint.go Normal file
View file

@ -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 <http://www.gnu.org/licenses/>.
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)
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
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
}

View file

@ -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 <http://www.gnu.org/licenses/>.
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
}

View file

@ -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 <http://www.gnu.org/licenses/>.
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)
}

View file

@ -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 <http://www.gnu.org/licenses/>.
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
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
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
}

85
beacon/light/signature.go Normal file
View file

@ -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 <http://www.gnu.org/licenses/>.
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
}

View file

@ -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 <http://www.gnu.org/licenses/>.
package sync
import (
"sync"
"github.com/ethereum/go-ethereum/beacon/light"
"github.com/ethereum/go-ethereum/beacon/light/request"
"github.com/ethereum/go-ethereum/beacon/light/types"
)
type signedHeadServer interface {
request.RequestServer
SignedHeads() []types.SignedHead
}
type latestHeads struct {
heads map[uint64]types.SignedHead
oldestSlot uint64
}
type HeadSyncer struct {
lock sync.Mutex
headTracker *light.HeadTracker
chain *light.CommitteeChain
added, queued latestHeads
SignedHeadTrigger request.ModuleTrigger
}
func NewHeadSyncer(headTracker *light.HeadTracker, chain *light.CommitteeChain) *HeadSyncer {
return &HeadSyncer{
headTracker: headTracker,
chain: chain,
}
}
func (s *HeadSyncer) Process(servers []*request.Server) bool {
s.lock.Lock()
defer s.lock.Unlock()
nextPeriod, ok := s.chain.NextSyncPeriod()
if !ok {
return true
}
for slot, head := range s.queued.heads {
if head.Header.SyncPeriod() <= nextPeriod {
delete(s.queued.heads, slot)
if s.added.add(head) && s.headTracker.Add(head) == nil {
s.SignedHeadTrigger.Trigger()
}
}
}
for _, server := range servers {
if hserver, ok := server.RequestServer.(signedHeadServer); ok {
heads := hserver.SignedHeads()
for _, head := range heads {
if head.Header.SyncPeriod() > nextPeriod {
s.queued.add(head)
} else if s.added.add(head) {
if s.headTracker.Add(head) == nil {
s.SignedHeadTrigger.Trigger()
} else {
hserver.Fail("received invalid signed head")
break
}
}
}
}
}
return true
}
func (l *latestHeads) add(head types.SignedHead) bool {
if l.heads == nil {
l.heads = make(map[uint64]types.SignedHead)
l.oldestSlot = head.Header.Slot
}
if oldHead, ok := l.heads[head.Header.Slot]; ok {
if head.SignerCount() <= oldHead.SignerCount() {
return false
}
}
l.heads[head.Header.Slot] = head
for len(l.heads) > 4 {
delete(l.heads, l.oldestSlot)
l.oldestSlot++
}
return true
}

View file

@ -0,0 +1,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 <http://www.gnu.org/licenses/>.
package sync
import (
"sync"
"github.com/ethereum/go-ethereum/beacon/light"
"github.com/ethereum/go-ethereum/beacon/light/request"
"github.com/ethereum/go-ethereum/beacon/light/types"
"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
}

View file

@ -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 <http://www.gnu.org/licenses/>.
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
}

180
beacon/light/types/forks.go Normal file
View file

@ -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 <http://www.gnu.org/licenses/>.
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
}

View file

@ -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 <http://www.gnu.org/licenses/>.
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()
}

View file

@ -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 <http://www.gnu.org/licenses/>.
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, "")
}

View file

@ -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 <http://www.gnu.org/licenses/>.
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)
}

View file

@ -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 <http://www.gnu.org/licenses/>.
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])
}
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
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
)

View file

@ -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 <http://www.gnu.org/licenses/>.
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)

183
cmd/blsync/config.go Normal file
View file

@ -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 <http://www.gnu.org/licenses/>.
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
}

234
cmd/blsync/main.go Normal file
View file

@ -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 <http://www.gnu.org/licenses/>.
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
}
}

View file

@ -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",

View file

@ -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
}
}

7
go.mod
View file

@ -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

19
go.sum
View file

@ -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=

View file

@ -20,6 +20,7 @@ import "github.com/urfave/cli/v2"
const (
EthCategory = "ETHEREUM"
BeaconCategory = "BEACON CHAIN"
LightCategory = "LIGHT CLIENT"
DevCategory = "DEVELOPER CHAIN"
EthashCategory = "ETHASH"

View file

@ -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.