mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-08-20 10:52:25 +00:00
beacon/light: add CommitteeChain
This commit is contained in:
parent
f265cc24b4
commit
574f085715
5 changed files with 1190 additions and 0 deletions
110
beacon/light/checkpoint.go
Normal file
110
beacon/light/checkpoint.go
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
// 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"
|
||||
|
||||
"github.com/ethereum/go-ethereum/beacon/merkle"
|
||||
"github.com/ethereum/go-ethereum/beacon/params"
|
||||
"github.com/ethereum/go-ethereum/beacon/types"
|
||||
"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.SerializedSyncCommittee `rlp:"-"`
|
||||
CommitteeBranch merkle.Values
|
||||
}
|
||||
|
||||
func (c *CheckpointData) Validate() error {
|
||||
if c.CommitteeRoot != c.Committee.Root() {
|
||||
return errors.New("wrong committee root")
|
||||
}
|
||||
return merkle.VerifyProof(c.Header.StateRoot, params.StateIndexSyncCommittee, c.CommitteeBranch, merkle.Value(c.CommitteeRoot))
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
521
beacon/light/committee_chain.go
Normal file
521
beacon/light/committee_chain.go
Normal file
|
|
@ -0,0 +1,521 @@
|
|||
// 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/params"
|
||||
"github.com/ethereum/go-ethereum/beacon/types"
|
||||
"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 (
|
||||
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
|
||||
)
|
||||
|
||||
// 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.SerializedSyncCommittee]
|
||||
fixedRoots *canonicalStore[common.Hash]
|
||||
syncCommitteeCache *lru.Cache[uint64, syncCommittee] // cache deserialized committees
|
||||
unixNano func() int64
|
||||
|
||||
config *types.ChainConfig
|
||||
signerThreshold int
|
||||
minimumUpdateScore types.UpdateScore
|
||||
enforceTime bool
|
||||
}
|
||||
|
||||
// NewCommitteeChain creates a new CommitteeChain
|
||||
func NewCommitteeChain(db ethdb.KeyValueStore, config *types.ChainConfig, 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.SerializedSyncCommittee](db, syncCommitteeKey, func(committee *types.SerializedSyncCommittee) ([]byte, error) {
|
||||
return committee[:], nil
|
||||
}, func(enc []byte) (*types.SerializedSyncCommittee, error) {
|
||||
if len(enc) == types.SerializedSyncCommitteeSize {
|
||||
committee := new(types.SerializedSyncCommittee)
|
||||
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,
|
||||
config: config,
|
||||
signerThreshold: signerThreshold,
|
||||
enforceTime: enforceTime,
|
||||
minimumUpdateScore: types.UpdateScore{
|
||||
SignerCount: uint32(signerThreshold),
|
||||
SubPeriodIndex: params.SyncPeriodLength / 16,
|
||||
},
|
||||
}
|
||||
|
||||
// check validity constraints
|
||||
if !s.updates.IsEmpty() {
|
||||
if s.fixedRoots.IsEmpty() || s.updates.First < s.fixedRoots.First ||
|
||||
s.updates.First >= s.fixedRoots.AfterLast {
|
||||
log.Crit("Inconsistent database error: first update is not in the fixed roots range")
|
||||
}
|
||||
if s.committees.First > s.updates.First || s.committees.AfterLast <= s.updates.AfterLast {
|
||||
log.Crit("Inconsistent database error: missing committees in update range")
|
||||
}
|
||||
}
|
||||
if !s.committees.IsEmpty() {
|
||||
if s.fixedRoots.IsEmpty() || s.committees.First < s.fixedRoots.First ||
|
||||
s.committees.First >= s.fixedRoots.AfterLast {
|
||||
log.Crit("Inconsistent database error: first committee is not in the fixed roots range")
|
||||
}
|
||||
if s.committees.AfterLast > s.fixedRoots.AfterLast && s.committees.AfterLast > s.updates.AfterLast+1 {
|
||||
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.First, "last period", s.committees.AfterLast-1)
|
||||
}
|
||||
// roll back invalid updates (might be necessary if forks have been changed since last time)
|
||||
var batch ethdb.Batch
|
||||
for !s.updates.IsEmpty() {
|
||||
if update := s.updates.get(s.updates.AfterLast - 1); update == nil || s.verifyUpdate(update) {
|
||||
if update == nil {
|
||||
log.Crit("Sync committee update missing", "period", s.updates.AfterLast-1)
|
||||
}
|
||||
break
|
||||
}
|
||||
if batch == nil {
|
||||
batch = s.db.NewBatch()
|
||||
}
|
||||
s.rollback(batch, s.updates.AfterLast)
|
||||
}
|
||||
if batch != nil {
|
||||
if err := batch.Write(); err != nil {
|
||||
log.Error("Error writing batch into chain database", "error", err)
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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.CanExpand(period) {
|
||||
if root != oldRoot {
|
||||
return ErrInvalidPeriod
|
||||
}
|
||||
for p := s.fixedRoots.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.AfterLast {
|
||||
return nil
|
||||
}
|
||||
batch := s.db.NewBatch()
|
||||
s.fixedRoots.deleteFrom(batch, period)
|
||||
if s.updates.IsEmpty() || period <= s.updates.First {
|
||||
s.updates.deleteFrom(batch, period)
|
||||
s.deleteCommitteesFrom(batch, period)
|
||||
} else {
|
||||
fromPeriod := s.updates.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) GetCommittee(period uint64) *types.SerializedSyncCommittee {
|
||||
return s.committees.get(period)
|
||||
}
|
||||
|
||||
func (s *CommitteeChain) AddCommittee(period uint64, committee *types.SerializedSyncCommittee) error {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
if !s.committees.CanExpand(period) {
|
||||
return ErrInvalidPeriod
|
||||
}
|
||||
root := s.getCommitteeRoot(period)
|
||||
if root == (common.Hash{}) {
|
||||
return ErrInvalidPeriod
|
||||
}
|
||||
if root != committee.Root() {
|
||||
return ErrWrongCommitteeRoot
|
||||
}
|
||||
if !s.committees.Includes(period) {
|
||||
s.committees.add(nil, period, committee)
|
||||
s.syncCommitteeCache.Remove(period)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *CommitteeChain) GetUpdate(period uint64) *types.LightClientUpdate {
|
||||
return s.updates.get(period)
|
||||
}
|
||||
|
||||
func (s *CommitteeChain) InsertUpdate(update *types.LightClientUpdate, nextCommittee *types.SerializedSyncCommittee) error {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
period := update.AttestedHeader.Header.SyncPeriod()
|
||||
if !s.updates.CanExpand(period) || !s.committees.Includes(period) {
|
||||
return ErrInvalidPeriod
|
||||
}
|
||||
if s.minimumUpdateScore.BetterThan(update.Score()) {
|
||||
return ErrInvalidUpdate
|
||||
}
|
||||
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.Includes(period+1) && reorg {
|
||||
return ErrCannotReorg
|
||||
}
|
||||
if !s.verifyUpdate(update) {
|
||||
return ErrInvalidUpdate
|
||||
}
|
||||
addCommittee := !s.committees.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.committees.IsEmpty() {
|
||||
return 0, false
|
||||
}
|
||||
if !s.updates.IsEmpty() {
|
||||
return s.updates.AfterLast, true
|
||||
}
|
||||
return s.committees.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
|
||||
}
|
||||
|
||||
// VerifySignedHeader 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) VerifySignedHeader(head types.SignedHeader) (bool, time.Duration) {
|
||||
s.lock.RLock()
|
||||
defer s.lock.RUnlock()
|
||||
|
||||
return s.verifySignedHeader(head)
|
||||
}
|
||||
|
||||
// (rlock required)
|
||||
func (s *CommitteeChain) verifySignedHeader(head types.SignedHeader) (bool, time.Duration) {
|
||||
var (
|
||||
slotTime = int64(time.Second) * int64(s.config.GenesisTime+head.Header.Slot*12)
|
||||
age = time.Duration(s.unixNano() - slotTime)
|
||||
)
|
||||
if s.enforceTime && age < 0 {
|
||||
return false, age
|
||||
}
|
||||
committee := s.getSyncCommittee(types.SyncPeriod(head.SignatureSlot))
|
||||
if committee == nil {
|
||||
return false, age
|
||||
}
|
||||
if signingRoot, err := s.config.Forks.SigningRoot(head.Header); err == nil {
|
||||
return s.sigVerifier.verifySignature(committee, signingRoot, &head.Signature), age
|
||||
}
|
||||
return false, 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.verifySignedHeader(update.AttestedHeader)
|
||||
if age < 0 {
|
||||
log.Warn("Future committee update received", "age", age)
|
||||
}
|
||||
return ok
|
||||
}
|
||||
|
||||
type canonicalStore[T any] struct {
|
||||
Range
|
||||
db ethdb.KeyValueStore
|
||||
keyPrefix []byte
|
||||
cache *lru.Cache[uint64, T]
|
||||
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.First == 0 {
|
||||
cs.First = period
|
||||
} else if cs.AfterLast != period {
|
||||
if iter.Next() {
|
||||
log.Error("Gap in the canonical chain database")
|
||||
}
|
||||
break // continuity guaranteed
|
||||
}
|
||||
cs.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.CanExpand(period) {
|
||||
log.Error("Cannot expand canonical store", "range.first", cs.First, "range.afterLast", cs.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.cache.Add(period, value)
|
||||
cs.Expand(period)
|
||||
}
|
||||
|
||||
// should only be used in batch mode
|
||||
func (cs *canonicalStore[T]) deleteFrom(batch ethdb.Batch, fromPeriod uint64) (deleted Range) {
|
||||
if fromPeriod >= cs.AfterLast {
|
||||
return
|
||||
}
|
||||
if fromPeriod < cs.First {
|
||||
fromPeriod = cs.First
|
||||
}
|
||||
deleted = Range{First: fromPeriod, AfterLast: cs.AfterLast}
|
||||
for period := fromPeriod; period < cs.AfterLast; period++ {
|
||||
batch.Delete(cs.getDbKey(period))
|
||||
cs.cache.Remove(period)
|
||||
}
|
||||
if fromPeriod > cs.First {
|
||||
cs.AfterLast = fromPeriod
|
||||
} else {
|
||||
cs.Range = Range{}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (cs *canonicalStore[T]) get(period uint64) T {
|
||||
if value, ok := cs.cache.Get(period); ok {
|
||||
return value
|
||||
}
|
||||
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
|
||||
}
|
||||
356
beacon/light/committee_chain_test.go
Normal file
356
beacon/light/committee_chain_test.go
Normal file
|
|
@ -0,0 +1,356 @@
|
|||
// 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"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/beacon/params"
|
||||
"github.com/ethereum/go-ethereum/beacon/types"
|
||||
"github.com/ethereum/go-ethereum/common/mclock"
|
||||
"github.com/ethereum/go-ethereum/ethdb/memorydb"
|
||||
)
|
||||
|
||||
var (
|
||||
testGenesis = newTestGenesis()
|
||||
testGenesis2 = newTestGenesis()
|
||||
|
||||
tfBase = newTestForks(testGenesis, types.Forks{
|
||||
&types.Fork{Epoch: 0, Version: []byte{0}},
|
||||
})
|
||||
tfAlternative = newTestForks(testGenesis, types.Forks{
|
||||
&types.Fork{Epoch: 0, Version: []byte{0}},
|
||||
&types.Fork{Epoch: 0x700, Version: []byte{1}},
|
||||
})
|
||||
tfAnotherGenesis = newTestForks(testGenesis2, types.Forks{
|
||||
&types.Fork{Epoch: 0, Version: []byte{0}},
|
||||
})
|
||||
|
||||
tcBase = newTestCommitteeChain(nil, tfBase, true, 0, 10, 400, false)
|
||||
tcBaseWithInvalidUpdates = newTestCommitteeChain(tcBase, tfBase, false, 5, 10, 200, false) // signer count too low
|
||||
tcBaseWithBetterUpdates = newTestCommitteeChain(tcBase, tfBase, false, 5, 10, 440, false)
|
||||
tcReorgWithWorseUpdates = newTestCommitteeChain(tcBase, tfBase, true, 5, 10, 400, false)
|
||||
tcReorgWithWorseUpdates2 = newTestCommitteeChain(tcBase, tfBase, true, 5, 10, 380, false)
|
||||
tcReorgWithBetterUpdates = newTestCommitteeChain(tcBase, tfBase, true, 5, 10, 420, false)
|
||||
tcReorgWithFinalizedUpdates = newTestCommitteeChain(tcBase, tfBase, true, 5, 10, 400, true)
|
||||
tcFork = newTestCommitteeChain(tcBase, tfAlternative, true, 7, 10, 400, false)
|
||||
tcAnotherGenesis = newTestCommitteeChain(nil, tfAnotherGenesis, true, 0, 10, 400, false)
|
||||
)
|
||||
|
||||
func TestCommitteeChainFixedRoots(t *testing.T) {
|
||||
for _, reload := range []bool{false, true} {
|
||||
c := newCommitteeChainTest(t, tfBase, 300, true)
|
||||
c.setClockPeriod(7)
|
||||
c.addFixedRoot(tcBase, 4, nil)
|
||||
c.addFixedRoot(tcBase, 5, nil)
|
||||
c.addFixedRoot(tcBase, 6, nil)
|
||||
c.addFixedRoot(tcBase, 8, ErrInvalidPeriod) // range has to be continuoous
|
||||
c.addFixedRoot(tcBase, 3, nil)
|
||||
c.addFixedRoot(tcBase, 2, nil)
|
||||
if reload {
|
||||
c.reloadChain()
|
||||
}
|
||||
c.addCommittee(tcBase, 4, nil)
|
||||
c.addCommittee(tcBase, 6, ErrInvalidPeriod) // range has to be continuoous
|
||||
c.addCommittee(tcBase, 5, nil)
|
||||
c.addCommittee(tcBase, 6, nil)
|
||||
c.addCommittee(tcAnotherGenesis, 3, ErrWrongCommitteeRoot)
|
||||
c.addCommittee(tcBase, 3, nil)
|
||||
if reload {
|
||||
c.reloadChain()
|
||||
}
|
||||
c.verifyRange(tcBase, 3, 6)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommitteeChainCheckpointSync(t *testing.T) {
|
||||
for _, enforceTime := range []bool{false, true} {
|
||||
for _, reload := range []bool{false, true} {
|
||||
c := newCommitteeChainTest(t, tfBase, 300, enforceTime)
|
||||
if enforceTime {
|
||||
c.setClockPeriod(6)
|
||||
}
|
||||
c.insertUpdate(tcBase, 3, true, ErrInvalidPeriod)
|
||||
c.addFixedRoot(tcBase, 3, nil)
|
||||
c.addFixedRoot(tcBase, 4, nil)
|
||||
c.insertUpdate(tcBase, 4, true, ErrInvalidPeriod) // still no committee
|
||||
c.addCommittee(tcBase, 3, nil)
|
||||
c.addCommittee(tcBase, 4, nil)
|
||||
if reload {
|
||||
c.reloadChain()
|
||||
}
|
||||
c.verifyRange(tcBase, 3, 4)
|
||||
c.insertUpdate(tcBase, 3, false, nil) // update can be added without committee here
|
||||
c.insertUpdate(tcBase, 4, false, ErrNeedCommittee) // but not here as committee 5 is not there yet
|
||||
c.insertUpdate(tcBase, 4, true, nil)
|
||||
c.verifyRange(tcBase, 3, 5)
|
||||
c.insertUpdate(tcBaseWithInvalidUpdates, 5, true, ErrInvalidUpdate) // signer count too low
|
||||
c.insertUpdate(tcBase, 5, true, nil)
|
||||
if reload {
|
||||
c.reloadChain()
|
||||
}
|
||||
if enforceTime {
|
||||
c.insertUpdate(tcBase, 6, true, ErrInvalidUpdate) // future update rejected
|
||||
c.setClockPeriod(7)
|
||||
}
|
||||
c.insertUpdate(tcBase, 6, true, nil) // when the time comes it's accepted
|
||||
if reload {
|
||||
c.reloadChain()
|
||||
}
|
||||
if enforceTime {
|
||||
c.verifyRange(tcBase, 3, 6) // committee 7 is there but still in the future
|
||||
c.setClockPeriod(8)
|
||||
}
|
||||
c.verifyRange(tcBase, 3, 7) // now period 7 can also be verified
|
||||
// try reverse syncing an update
|
||||
c.insertUpdate(tcBase, 2, false, ErrInvalidPeriod) // fixed committee is needed first
|
||||
c.addFixedRoot(tcBase, 2, nil)
|
||||
c.addCommittee(tcBase, 2, nil)
|
||||
c.insertUpdate(tcBase, 2, false, nil)
|
||||
c.verifyRange(tcBase, 2, 7)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommitteeChainReorg(t *testing.T) {
|
||||
for _, reload := range []bool{false, true} {
|
||||
for _, addBetterUpdates := range []bool{false, true} {
|
||||
c := newCommitteeChainTest(t, tfBase, 300, true)
|
||||
c.setClockPeriod(11)
|
||||
c.addFixedRoot(tcBase, 3, nil)
|
||||
c.addFixedRoot(tcBase, 4, nil)
|
||||
c.addCommittee(tcBase, 3, nil)
|
||||
for period := uint64(3); period < 10; period++ {
|
||||
c.insertUpdate(tcBase, period, true, nil)
|
||||
}
|
||||
if reload {
|
||||
c.reloadChain()
|
||||
}
|
||||
c.verifyRange(tcBase, 3, 10)
|
||||
c.insertUpdate(tcReorgWithWorseUpdates, 5, true, ErrCannotReorg)
|
||||
c.insertUpdate(tcReorgWithWorseUpdates2, 5, true, ErrCannotReorg)
|
||||
if addBetterUpdates {
|
||||
// add better updates for the base chain and expect first reorg to fail
|
||||
// (only add updates as committees should be the same)
|
||||
for period := uint64(5); period < 10; period++ {
|
||||
c.insertUpdate(tcBaseWithBetterUpdates, period, false, nil)
|
||||
}
|
||||
if reload {
|
||||
c.reloadChain()
|
||||
}
|
||||
c.verifyRange(tcBase, 3, 10) // still on the same chain
|
||||
c.insertUpdate(tcReorgWithBetterUpdates, 5, true, ErrCannotReorg)
|
||||
} else {
|
||||
// reorg with better updates
|
||||
c.insertUpdate(tcReorgWithBetterUpdates, 5, false, ErrNeedCommittee)
|
||||
c.verifyRange(tcBase, 3, 10) // no success yet, still on the base chain
|
||||
c.verifyRange(tcReorgWithBetterUpdates, 3, 5)
|
||||
c.insertUpdate(tcReorgWithBetterUpdates, 5, true, nil)
|
||||
// successful reorg, base chain should only match before the reorg period
|
||||
if reload {
|
||||
c.reloadChain()
|
||||
}
|
||||
c.verifyRange(tcBase, 3, 5)
|
||||
c.verifyRange(tcReorgWithBetterUpdates, 3, 6)
|
||||
for period := uint64(6); period < 10; period++ {
|
||||
c.insertUpdate(tcReorgWithBetterUpdates, period, true, nil)
|
||||
}
|
||||
c.verifyRange(tcReorgWithBetterUpdates, 3, 10)
|
||||
}
|
||||
// reorg with finalized updates; should succeed even if base chain updates
|
||||
// have been improved because a finalized update beats everything else
|
||||
c.insertUpdate(tcReorgWithFinalizedUpdates, 5, false, ErrNeedCommittee)
|
||||
c.insertUpdate(tcReorgWithFinalizedUpdates, 5, true, nil)
|
||||
if reload {
|
||||
c.reloadChain()
|
||||
}
|
||||
c.verifyRange(tcReorgWithFinalizedUpdates, 3, 6)
|
||||
for period := uint64(6); period < 10; period++ {
|
||||
c.insertUpdate(tcReorgWithFinalizedUpdates, period, true, nil)
|
||||
}
|
||||
c.verifyRange(tcReorgWithFinalizedUpdates, 3, 10)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommitteeChainFork(t *testing.T) {
|
||||
c := newCommitteeChainTest(t, tfAlternative, 300, true)
|
||||
c.setClockPeriod(11)
|
||||
// trying to sync a chain on an alternative fork with the base chain data
|
||||
c.addFixedRoot(tcBase, 0, nil)
|
||||
c.addFixedRoot(tcBase, 1, nil)
|
||||
c.addCommittee(tcBase, 0, nil)
|
||||
// shared section should sync without errors
|
||||
for period := uint64(0); period < 7; period++ {
|
||||
c.insertUpdate(tcBase, period, true, nil)
|
||||
}
|
||||
c.insertUpdate(tcBase, 7, true, ErrInvalidUpdate) // wrong fork
|
||||
// committee root #7 is still the same but signatures are already signed with
|
||||
// a different fork id so period 7 should only verify on the alternative fork
|
||||
c.verifyRange(tcBase, 0, 6)
|
||||
c.verifyRange(tcFork, 0, 7)
|
||||
for period := uint64(7); period < 10; period++ {
|
||||
c.insertUpdate(tcFork, period, true, nil)
|
||||
}
|
||||
c.verifyRange(tcFork, 0, 10)
|
||||
// reload the chain while switching to the base fork
|
||||
c.config = tfBase
|
||||
c.reloadChain()
|
||||
// updates 7..9 should be rolled back now
|
||||
c.verifyRange(tcFork, 0, 6) // again, period 7 only verifies on the right fork
|
||||
c.verifyRange(tcBase, 0, 7)
|
||||
c.insertUpdate(tcFork, 7, true, ErrInvalidUpdate) // wrong fork
|
||||
for period := uint64(7); period < 10; period++ {
|
||||
c.insertUpdate(tcBase, period, true, nil)
|
||||
}
|
||||
c.verifyRange(tcBase, 0, 10)
|
||||
}
|
||||
|
||||
type committeeChainTest struct {
|
||||
t *testing.T
|
||||
db *memorydb.Database
|
||||
clock *mclock.Simulated
|
||||
config types.ChainConfig
|
||||
signerThreshold int
|
||||
enforceTime bool
|
||||
chain *CommitteeChain
|
||||
}
|
||||
|
||||
func newCommitteeChainTest(t *testing.T, config types.ChainConfig, signerThreshold int, enforceTime bool) *committeeChainTest {
|
||||
c := &committeeChainTest{
|
||||
t: t,
|
||||
db: memorydb.New(),
|
||||
clock: &mclock.Simulated{},
|
||||
config: config,
|
||||
signerThreshold: signerThreshold,
|
||||
enforceTime: enforceTime,
|
||||
}
|
||||
c.chain = NewCommitteeChain(c.db, &config, signerThreshold, enforceTime, DummyVerifier{}, c.clock, func() int64 { return int64(c.clock.Now()) })
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *committeeChainTest) reloadChain() {
|
||||
c.chain = NewCommitteeChain(c.db, &c.config, c.signerThreshold, c.enforceTime, DummyVerifier{}, c.clock, func() int64 { return int64(c.clock.Now()) })
|
||||
}
|
||||
|
||||
func (c *committeeChainTest) setClockPeriod(period float64) {
|
||||
target := mclock.AbsTime(period * float64(time.Second*12*params.SyncPeriodLength))
|
||||
wait := time.Duration(target - c.clock.Now())
|
||||
if wait < 0 {
|
||||
c.t.Fatalf("Invalid setClockPeriod")
|
||||
}
|
||||
c.clock.Run(wait)
|
||||
}
|
||||
|
||||
func (c *committeeChainTest) addFixedRoot(tc *testCommitteeChain, period uint64, expErr error) {
|
||||
if err := c.chain.AddFixedRoot(period, tc.periods[period].committee.Root()); err != expErr {
|
||||
c.t.Errorf("Incorrect error output from AddFixedRoot at period %d (expected %v, got %v)", period, expErr, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *committeeChainTest) addCommittee(tc *testCommitteeChain, period uint64, expErr error) {
|
||||
if err := c.chain.AddCommittee(period, tc.periods[period].committee); err != expErr {
|
||||
c.t.Errorf("Incorrect error output from AddCommittee at period %d (expected %v, got %v)", period, expErr, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *committeeChainTest) insertUpdate(tc *testCommitteeChain, period uint64, addCommittee bool, expErr error) {
|
||||
var committee *types.SerializedSyncCommittee
|
||||
if addCommittee {
|
||||
committee = tc.periods[period+1].committee
|
||||
}
|
||||
if err := c.chain.InsertUpdate(tc.periods[period].update, committee); err != expErr {
|
||||
c.t.Errorf("Incorrect error output from InsertUpdate at period %d (expected %v, got %v)", period, expErr, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *committeeChainTest) verifySignedHeader(tc *testCommitteeChain, period float64, expOk bool) {
|
||||
slot := uint64(period * float64(params.SyncPeriodLength))
|
||||
signedHead := GenerateTestSignedHeader(types.Header{Slot: slot}, &tc.config, tc.periods[types.SyncPeriod(slot)].committee, slot+1, 400)
|
||||
if ok, _ := c.chain.VerifySignedHeader(signedHead); ok != expOk {
|
||||
c.t.Errorf("Incorrect output from VerifySignedHeader at period %f (expected %v, got %v)", period, expOk, ok)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *committeeChainTest) verifyRange(tc *testCommitteeChain, begin, end uint64) {
|
||||
if begin > 0 {
|
||||
c.verifySignedHeader(tc, float64(begin)-0.5, false)
|
||||
}
|
||||
for period := begin; period <= end; period++ {
|
||||
c.verifySignedHeader(tc, float64(period)+0.5, true)
|
||||
}
|
||||
c.verifySignedHeader(tc, float64(end)+1.5, false)
|
||||
}
|
||||
|
||||
func newTestGenesis() types.ChainConfig {
|
||||
var config types.ChainConfig
|
||||
rand.Read(config.GenesisValidatorsRoot[:])
|
||||
return config
|
||||
}
|
||||
|
||||
func newTestForks(config types.ChainConfig, forks types.Forks) types.ChainConfig {
|
||||
for _, fork := range forks {
|
||||
config.AddFork(fork.Name, fork.Epoch, fork.Version)
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
func newTestCommitteeChain(parent *testCommitteeChain, config types.ChainConfig, newCommittees bool, begin, end int, signerCount int, finalizedHeader bool) *testCommitteeChain {
|
||||
tc := &testCommitteeChain{
|
||||
config: config,
|
||||
}
|
||||
if parent != nil {
|
||||
tc.periods = make([]testPeriod, len(parent.periods))
|
||||
copy(tc.periods, parent.periods)
|
||||
}
|
||||
if newCommittees {
|
||||
if begin == 0 {
|
||||
tc.fillCommittees(begin, end+1)
|
||||
} else {
|
||||
tc.fillCommittees(begin+1, end+1)
|
||||
}
|
||||
}
|
||||
tc.fillUpdates(begin, end, signerCount, finalizedHeader)
|
||||
return tc
|
||||
}
|
||||
|
||||
type testPeriod struct {
|
||||
committee *types.SerializedSyncCommittee
|
||||
update *types.LightClientUpdate
|
||||
}
|
||||
|
||||
type testCommitteeChain struct {
|
||||
periods []testPeriod
|
||||
config types.ChainConfig
|
||||
}
|
||||
|
||||
func (tc *testCommitteeChain) fillCommittees(begin, end int) {
|
||||
if len(tc.periods) <= end {
|
||||
tc.periods = append(tc.periods, make([]testPeriod, end+1-len(tc.periods))...)
|
||||
}
|
||||
for i := begin; i <= end; i++ {
|
||||
tc.periods[i].committee = GenerateTestCommittee()
|
||||
}
|
||||
}
|
||||
|
||||
func (tc *testCommitteeChain) fillUpdates(begin, end int, signerCount int, finalizedHeader bool) {
|
||||
for i := begin; i <= end; i++ {
|
||||
tc.periods[i].update = GenerateTestUpdate(&tc.config, uint64(i), tc.periods[i].committee, tc.periods[i+1].committee, signerCount, finalizedHeader)
|
||||
}
|
||||
}
|
||||
52
beacon/light/range.go
Normal file
52
beacon/light/range.go
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
// 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
|
||||
|
||||
type Range struct {
|
||||
First uint64
|
||||
AfterLast uint64
|
||||
}
|
||||
|
||||
func (a Range) IsEmpty() bool {
|
||||
return a.AfterLast == a.First
|
||||
}
|
||||
|
||||
func (a Range) Includes(period uint64) bool {
|
||||
return period >= a.First && period < a.AfterLast
|
||||
}
|
||||
|
||||
func (a Range) CanExpand(period uint64) bool {
|
||||
return a.IsEmpty() || (period+1 >= a.First && period <= a.AfterLast)
|
||||
}
|
||||
|
||||
func (a *Range) 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
|
||||
}
|
||||
}
|
||||
151
beacon/light/test_helpers.go
Normal file
151
beacon/light/test_helpers.go
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
// 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 (
|
||||
"crypto/sha256"
|
||||
"math/rand"
|
||||
|
||||
"github.com/ethereum/go-ethereum/beacon/merkle"
|
||||
"github.com/ethereum/go-ethereum/beacon/params"
|
||||
"github.com/ethereum/go-ethereum/beacon/types"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
)
|
||||
|
||||
func GenerateTestCommittee() *types.SerializedSyncCommittee {
|
||||
s := new(types.SerializedSyncCommittee)
|
||||
rand.Read(s[:32])
|
||||
return s
|
||||
}
|
||||
|
||||
func GenerateTestUpdate(config *types.ChainConfig, period uint64, committee, nextCommittee *types.SerializedSyncCommittee, signerCount int, finalizedHeader bool) *types.LightClientUpdate {
|
||||
update := new(types.LightClientUpdate)
|
||||
update.NextSyncCommitteeRoot = nextCommittee.Root()
|
||||
var attestedHeader types.Header
|
||||
if finalizedHeader {
|
||||
update.FinalizedHeader = new(types.Header)
|
||||
*update.FinalizedHeader, update.NextSyncCommitteeBranch = makeTestHeaderWithMerkleProof(types.SyncPeriodStart(period)+100, params.StateIndexNextSyncCommittee, merkle.Value(update.NextSyncCommitteeRoot))
|
||||
attestedHeader, update.FinalityBranch = makeTestHeaderWithMerkleProof(types.SyncPeriodStart(period)+200, params.StateIndexFinalBlock, merkle.Value(update.FinalizedHeader.Hash()))
|
||||
} else {
|
||||
attestedHeader, update.NextSyncCommitteeBranch = makeTestHeaderWithMerkleProof(types.SyncPeriodStart(period)+2000, params.StateIndexNextSyncCommittee, merkle.Value(update.NextSyncCommitteeRoot))
|
||||
}
|
||||
update.AttestedHeader = GenerateTestSignedHeader(attestedHeader, config, committee, attestedHeader.Slot+1, signerCount)
|
||||
return update
|
||||
}
|
||||
|
||||
func GenerateTestSignedHeader(header types.Header, config *types.ChainConfig, committee *types.SerializedSyncCommittee, signatureSlot uint64, signerCount int) types.SignedHeader {
|
||||
bitmask := makeBitmask(signerCount)
|
||||
signingRoot, _ := config.Forks.SigningRoot(header)
|
||||
c, _ := DummyVerifier{}.deserializeSyncCommittee(committee)
|
||||
return types.SignedHeader{
|
||||
Header: header,
|
||||
Signature: types.SyncAggregate{
|
||||
Signers: bitmask,
|
||||
Signature: makeDummySignature(c.(dummySyncCommittee), signingRoot, bitmask),
|
||||
},
|
||||
SignatureSlot: signatureSlot,
|
||||
}
|
||||
}
|
||||
|
||||
func GenerateTestCheckpoint(period uint64, committee *types.SerializedSyncCommittee) *CheckpointData {
|
||||
header, branch := makeTestHeaderWithMerkleProof(types.SyncPeriodStart(period)+200, params.StateIndexSyncCommittee, merkle.Value(committee.Root()))
|
||||
return &CheckpointData{
|
||||
Header: header,
|
||||
Committee: committee,
|
||||
CommitteeRoot: committee.Root(),
|
||||
CommitteeBranch: branch,
|
||||
}
|
||||
}
|
||||
|
||||
func makeBitmask(signerCount int) (bitmask [params.SyncCommitteeBitmaskSize]byte) {
|
||||
for i := 0; i < params.SyncCommitteeSize; i++ {
|
||||
if rand.Intn(params.SyncCommitteeSize-i) < signerCount {
|
||||
bitmask[i/8] += byte(1) << (i & 7)
|
||||
signerCount--
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func makeTestHeaderWithMerkleProof(slot, index uint64, value merkle.Value) (types.Header, merkle.Values) {
|
||||
var branch merkle.Values
|
||||
hasher := sha256.New()
|
||||
for index > 1 {
|
||||
var proofHash merkle.Value
|
||||
rand.Read(proofHash[:])
|
||||
hasher.Reset()
|
||||
if index&1 == 0 {
|
||||
hasher.Write(value[:])
|
||||
hasher.Write(proofHash[:])
|
||||
} else {
|
||||
hasher.Write(proofHash[:])
|
||||
hasher.Write(value[:])
|
||||
}
|
||||
hasher.Sum(value[:0])
|
||||
index >>= 1
|
||||
branch = append(branch, proofHash)
|
||||
}
|
||||
return types.Header{Slot: slot, StateRoot: common.Hash(value)}, branch
|
||||
}
|
||||
|
||||
// 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.SerializedSyncCommittee) (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.SerializedSyncCommittee) (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.SerializedSyncCommittee) (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.Signers)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
Loading…
Reference in a new issue