mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-08-20 10:52:25 +00:00
cmd/blsync, beacon/light: standalone beacon light sync tool
This commit is contained in:
parent
2169fa343a
commit
4f67f433c8
27 changed files with 4362 additions and 9 deletions
489
beacon/light/api/light_api.go
Normal file
489
beacon/light/api/light_api.go
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
156
beacon/light/api/sync_server.go
Normal file
156
beacon/light/api/sync_server.go
Normal 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
109
beacon/light/checkpoint.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
548
beacon/light/committee_chain.go
Normal file
548
beacon/light/committee_chain.go
Normal 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
|
||||||
|
}
|
||||||
95
beacon/light/head_tracker.go
Normal file
95
beacon/light/head_tracker.go
Normal 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
|
||||||
|
}
|
||||||
47
beacon/light/request/lock.go
Normal file
47
beacon/light/request/lock.go
Normal 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)
|
||||||
|
}
|
||||||
237
beacon/light/request/scheduler.go
Normal file
237
beacon/light/request/scheduler.go
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
166
beacon/light/request/server.go
Normal file
166
beacon/light/request/server.go
Normal 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
85
beacon/light/signature.go
Normal 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
|
||||||
|
}
|
||||||
105
beacon/light/sync/head_sync.go
Normal file
105
beacon/light/sync/head_sync.go
Normal 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
|
||||||
|
}
|
||||||
181
beacon/light/sync/update_sync.go
Normal file
181
beacon/light/sync/update_sync.go
Normal 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
|
||||||
|
}
|
||||||
204
beacon/light/types/committee.go
Normal file
204
beacon/light/types/committee.go
Normal 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
180
beacon/light/types/forks.go
Normal 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
|
||||||
|
}
|
||||||
158
beacon/light/types/header.go
Normal file
158
beacon/light/types/header.go
Normal 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()
|
||||||
|
}
|
||||||
242
beacon/light/types/protocol.go
Normal file
242
beacon/light/types/protocol.go
Normal 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, "")
|
||||||
|
}
|
||||||
494
beacon/merkle/binary_merkle.go
Normal file
494
beacon/merkle/binary_merkle.go
Normal 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)
|
||||||
|
}
|
||||||
259
beacon/merkle/binary_merkle_test.go
Normal file
259
beacon/merkle/binary_merkle_test.go
Normal 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])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
29
beacon/params/constants.go
Normal file
29
beacon/params/constants.go
Normal 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
|
||||||
|
)
|
||||||
46
beacon/params/tree_indices.go
Normal file
46
beacon/params/tree_indices.go
Normal 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
183
cmd/blsync/config.go
Normal 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
234
cmd/blsync/main.go
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -36,6 +36,7 @@ import (
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/accounts"
|
"github.com/ethereum/go-ethereum/accounts"
|
||||||
"github.com/ethereum/go-ethereum/accounts/keystore"
|
"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"
|
||||||
"github.com/ethereum/go-ethereum/common/fdlimit"
|
"github.com/ethereum/go-ethereum/common/fdlimit"
|
||||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||||
|
|
@ -273,6 +274,58 @@ var (
|
||||||
Usage: "Manually specify the Cancun fork timestamp, overriding the bundled setting",
|
Usage: "Manually specify the Cancun fork timestamp, overriding the bundled setting",
|
||||||
Category: flags.EthCategory,
|
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
|
// Light server and client settings
|
||||||
LightServeFlag = &cli.IntFlag{
|
LightServeFlag = &cli.IntFlag{
|
||||||
Name: "light.serve",
|
Name: "light.serve",
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import (
|
||||||
"math/big"
|
"math/big"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||||
|
|
@ -429,3 +430,22 @@ func (ma *MixedcaseAddress) ValidChecksum() bool {
|
||||||
func (ma *MixedcaseAddress) Original() string {
|
func (ma *MixedcaseAddress) Original() string {
|
||||||
return ma.original
|
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
7
go.mod
|
|
@ -18,6 +18,7 @@ require (
|
||||||
github.com/davecgh/go-spew v1.1.1
|
github.com/davecgh/go-spew v1.1.1
|
||||||
github.com/deckarep/golang-set/v2 v2.1.0
|
github.com/deckarep/golang-set/v2 v2.1.0
|
||||||
github.com/docker/docker v1.6.2
|
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/dop251/goja v0.0.0-20230122112309-96b1610dd4f7
|
||||||
github.com/edsrzf/mmap-go v1.0.0
|
github.com/edsrzf/mmap-go v1.0.0
|
||||||
github.com/ethereum/c-kzg-4844 v0.1.0
|
github.com/ethereum/c-kzg-4844 v0.1.0
|
||||||
|
|
@ -49,9 +50,13 @@ require (
|
||||||
github.com/kylelemons/godebug v1.1.0
|
github.com/kylelemons/godebug v1.1.0
|
||||||
github.com/mattn/go-colorable v0.1.13
|
github.com/mattn/go-colorable v0.1.13
|
||||||
github.com/mattn/go-isatty v0.0.16
|
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/naoina/toml v0.1.2-0.20170918210437-9fafd6967416
|
||||||
github.com/olekukonko/tablewriter v0.0.5
|
github.com/olekukonko/tablewriter v0.0.5
|
||||||
github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7
|
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/rs/cors v1.7.0
|
||||||
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible
|
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible
|
||||||
github.com/status-im/keycard-go v0.2.0
|
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/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
|
||||||
github.com/gogo/protobuf v1.3.2 // indirect
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
github.com/influxdata/line-protocol v0.0.0-20210311194329-9aa0e372d097 // 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/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/pretty v0.3.1 // indirect
|
||||||
github.com/kr/text v0.2.0 // indirect
|
github.com/kr/text v0.2.0 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.9 // indirect
|
github.com/mattn/go-runewidth v0.0.9 // indirect
|
||||||
|
|
|
||||||
19
go.sum
19
go.sum
|
|
@ -114,6 +114,8 @@ github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI=
|
||||||
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
|
github.com/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 h1:HlFGsy+9/xrgMmhmN+NGhCc5SHGJ7I+kHosRR1xc/aI=
|
||||||
github.com/docker/docker v1.6.2/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
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-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 h1:kgvzE5wLsLa7XKfV85VZl40QXaMCaeFtHpPwJ8fhotY=
|
||||||
github.com/dop251/goja v0.0.0-20230122112309-96b1610dd4f7/go.mod h1:yRkwfj0CBpOGre+TwBsqPV0IH0Pk73e4PXJOeNDboGs=
|
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/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 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao=
|
||||||
github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA=
|
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 h1:DZfsyhDK1hnSS5lH8l+JggqzEleHteTYfutAiVlSUM8=
|
||||||
github.com/holiman/uint256 v1.2.2-0.20230321075855-87b91420868c/go.mod h1:SC8Ryt4n+UBbPbIBKaG9zbbDlp4jOru9xFZmPzLUTxw=
|
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=
|
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/neffos v0.0.14/go.mod h1:8lqADm8PnbeFfL7CLXh1WHw53dG27MC3pgi2R1rmoTE=
|
||||||
github.com/kataras/pio v0.0.2/go.mod h1:hAoW0t9UmXi4R5Oyq5Z4irTbaTsOemSrDGUtaTl7Dro=
|
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/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/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
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=
|
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 h1:EF27CXIuDsYJ6mmvtBRlEuB2UVOqHG1tAXgZ7yIO+lw=
|
||||||
github.com/klauspost/compress v1.15.15/go.mod h1:ZcK2JAFqKOpnBlxcLsJzYfrS9X1akm9fHZNnD9+Vo/4=
|
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 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.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
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/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/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/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/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.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||||
github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag=
|
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/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 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=
|
||||||
github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY=
|
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.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.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
|
||||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
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-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-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-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-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-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210316164454-77fc1eacc6aa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210316164454-77fc1eacc6aa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import "github.com/urfave/cli/v2"
|
||||||
|
|
||||||
const (
|
const (
|
||||||
EthCategory = "ETHEREUM"
|
EthCategory = "ETHEREUM"
|
||||||
|
BeaconCategory = "BEACON CHAIN"
|
||||||
LightCategory = "LIGHT CLIENT"
|
LightCategory = "LIGHT CLIENT"
|
||||||
DevCategory = "DEVELOPER CHAIN"
|
DevCategory = "DEVELOPER CHAIN"
|
||||||
EthashCategory = "ETHASH"
|
EthashCategory = "ETHASH"
|
||||||
|
|
|
||||||
24
node/node.go
24
node/node.go
|
|
@ -338,15 +338,9 @@ func (n *Node) closeDataDir() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// obtainJWTSecret loads the jwt-secret, either from the provided config,
|
// ObtainJWTSecret loads the jwt-secret from the provided config. If the file is not
|
||||||
// or from the default location. If neither of those are present, it generates
|
// present, it generates a new secret and stores to the given location.
|
||||||
// a new secret and stores to the default location.
|
func ObtainJWTSecret(fileName string) ([]byte, error) {
|
||||||
func (n *Node) obtainJWTSecret(cliParam string) ([]byte, error) {
|
|
||||||
fileName := cliParam
|
|
||||||
if len(fileName) == 0 {
|
|
||||||
// no path provided, use default
|
|
||||||
fileName = n.ResolvePath(datadirJWTKey)
|
|
||||||
}
|
|
||||||
// try reading from file
|
// try reading from file
|
||||||
if data, err := os.ReadFile(fileName); err == nil {
|
if data, err := os.ReadFile(fileName); err == nil {
|
||||||
jwtSecret := common.FromHex(strings.TrimSpace(string(data)))
|
jwtSecret := common.FromHex(strings.TrimSpace(string(data)))
|
||||||
|
|
@ -372,6 +366,18 @@ func (n *Node) obtainJWTSecret(cliParam string) ([]byte, error) {
|
||||||
return jwtSecret, nil
|
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
|
// 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
|
// startup. It's not meant to be called at any time afterwards as it makes certain
|
||||||
// assumptions about the state of the node.
|
// assumptions about the state of the node.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue