beacon/light: add CommitteeChain

This commit is contained in:
Zsolt Felfoldi 2023-07-24 16:47:32 +02:00 committed by zsfelfoldi
parent f265cc24b4
commit 574f085715
5 changed files with 1190 additions and 0 deletions

110
beacon/light/checkpoint.go Normal file
View 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)
}
}

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

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

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