From da7f95e8c9d844521592ffcacda187470985e7c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isabel=20Sch=C3=B6ps=20Thiel?= <155141998+IST-Github@users.noreply.github.com> Date: Mon, 1 Jan 2024 00:35:39 +0100 Subject: [PATCH] Delete beacon/light directory --- beacon/light/canonical.go | 125 ------- beacon/light/committee_chain.go | 514 --------------------------- beacon/light/committee_chain_test.go | 356 ------------------- beacon/light/range.go | 78 ---- beacon/light/test_helpers.go | 152 -------- 5 files changed, 1225 deletions(-) delete mode 100644 beacon/light/canonical.go delete mode 100644 beacon/light/committee_chain.go delete mode 100644 beacon/light/committee_chain_test.go delete mode 100644 beacon/light/range.go delete mode 100644 beacon/light/test_helpers.go diff --git a/beacon/light/canonical.go b/beacon/light/canonical.go deleted file mode 100644 index b5371493b4..0000000000 --- a/beacon/light/canonical.go +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright 2023 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see . - -package light - -import ( - "encoding/binary" - "fmt" - - "github.com/ethereum/go-ethereum/common/lru" - "github.com/ethereum/go-ethereum/ethdb" - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/rlp" -) - -// canonicalStore stores instances of the given type in a database and caches -// them in memory, associated with a continuous range of period numbers. -// Note: canonicalStore is not thread safe and it is the caller's responsibility -// to avoid concurrent access. -type canonicalStore[T any] struct { - keyPrefix []byte - periods periodRange - cache *lru.Cache[uint64, T] -} - -// newCanonicalStore creates a new canonicalStore and loads all keys associated -// with the keyPrefix in order to determine the ranges available in the database. -func newCanonicalStore[T any](db ethdb.Iteratee, keyPrefix []byte) (*canonicalStore[T], error) { - cs := &canonicalStore[T]{ - keyPrefix: keyPrefix, - cache: lru.NewCache[uint64, T](100), - } - var ( - iter = db.NewIterator(keyPrefix, nil) - kl = len(keyPrefix) - first = true - ) - defer iter.Release() - - for iter.Next() { - if len(iter.Key()) != kl+8 { - log.Warn("Invalid key length in the canonical chain database", "key", fmt.Sprintf("%#x", iter.Key())) - continue - } - period := binary.BigEndian.Uint64(iter.Key()[kl : kl+8]) - if first { - cs.periods.Start = period - } else if cs.periods.End != period { - return nil, fmt.Errorf("gap in the canonical chain database between periods %d and %d", cs.periods.End, period-1) - } - first = false - cs.periods.End = period + 1 - } - return cs, nil -} - -// databaseKey returns the database key belonging to the given period. -func (cs *canonicalStore[T]) databaseKey(period uint64) []byte { - return binary.BigEndian.AppendUint64(append([]byte{}, cs.keyPrefix...), period) -} - -// add adds the given item to the database. It also ensures that the range remains -// continuous. Can be used either with a batch or database backend. -func (cs *canonicalStore[T]) add(backend ethdb.KeyValueWriter, period uint64, value T) error { - if !cs.periods.canExpand(period) { - return fmt.Errorf("period expansion is not allowed, first: %d, next: %d, period: %d", cs.periods.Start, cs.periods.End, period) - } - enc, err := rlp.EncodeToBytes(value) - if err != nil { - return err - } - if err := backend.Put(cs.databaseKey(period), enc); err != nil { - return err - } - cs.cache.Add(period, value) - cs.periods.expand(period) - return nil -} - -// deleteFrom removes items starting from the given period. -func (cs *canonicalStore[T]) deleteFrom(db ethdb.KeyValueWriter, fromPeriod uint64) (deleted periodRange) { - keepRange, deleteRange := cs.periods.split(fromPeriod) - deleteRange.each(func(period uint64) { - db.Delete(cs.databaseKey(period)) - cs.cache.Remove(period) - }) - cs.periods = keepRange - return deleteRange -} - -// get returns the item at the given period or the null value of the given type -// if no item is present. -func (cs *canonicalStore[T]) get(backend ethdb.KeyValueReader, period uint64) (T, bool) { - var null, value T - if !cs.periods.contains(period) { - return null, false - } - if value, ok := cs.cache.Get(period); ok { - return value, true - } - enc, err := backend.Get(cs.databaseKey(period)) - if err != nil { - log.Error("Canonical store value not found", "period", period, "start", cs.periods.Start, "end", cs.periods.End) - return null, false - } - if err := rlp.DecodeBytes(enc, &value); err != nil { - log.Error("Error decoding canonical store value", "error", err) - return null, false - } - cs.cache.Add(period, value) - return value, true -} diff --git a/beacon/light/committee_chain.go b/beacon/light/committee_chain.go deleted file mode 100644 index d707f8cc34..0000000000 --- a/beacon/light/committee_chain.go +++ /dev/null @@ -1,514 +0,0 @@ -// Copyright 2023 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see . - -package light - -import ( - "errors" - "fmt" - "math" - "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/core/rawdb" - "github.com/ethereum/go-ethereum/ethdb" - "github.com/ethereum/go-ethereum/log" -) - -var ( - 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") -) - -// CommitteeChain is a passive data structure that can validate, hold and update -// a chain of beacon light sync committees and updates. It requires at least one -// externally set fixed committee root at the beginning of the chain which can -// be set either based on a BootstrapData or a trusted source (a local beacon -// full node). This makes the structure useful for both light client and light -// server setups. -// -// It always maintains the following consistency constraints: -// - a committee can only be present if its root hash matches an existing fixed -// root or if it is proven by an update at the previous period -// - an update can only be present if a committee is present at the same period -// and the update signature is valid and has enough participants. -// The committee at the next period (proven by the update) should also be -// present (note that this means they can only be added together if neither -// is present yet). If a fixed root is present at the next period then the -// update can only be present if it proves the same committee root. -// -// Once synced to the current sync period, CommitteeChain can also validate -// signed beacon headers. -type CommitteeChain struct { - // chainmu guards against concurrent access to the canonicalStore structures - // (updates, committees, fixedCommitteeRoots) and ensures that they stay consistent - // with each other and with committeeCache. - chainmu sync.RWMutex - db ethdb.KeyValueStore - updates *canonicalStore[*types.LightClientUpdate] - committees *canonicalStore[*types.SerializedSyncCommittee] - fixedCommitteeRoots *canonicalStore[common.Hash] - committeeCache *lru.Cache[uint64, syncCommittee] // cache deserialized committees - - clock mclock.Clock // monotonic clock (simulated clock in tests) - unixNano func() int64 // system clock (simulated clock in tests) - sigVerifier committeeSigVerifier // BLS sig verifier (dummy verifier in tests) - - config *types.ChainConfig - signerThreshold int - minimumUpdateScore types.UpdateScore - enforceTime bool // enforceTime specifies whether the age of a signed header should be checked -} - -// NewCommitteeChain creates a new CommitteeChain. -func NewCommitteeChain(db ethdb.KeyValueStore, config *types.ChainConfig, signerThreshold int, enforceTime bool) *CommitteeChain { - return newCommitteeChain(db, config, signerThreshold, enforceTime, blsVerifier{}, &mclock.System{}, func() int64 { return time.Now().UnixNano() }) -} - -// newCommitteeChain creates a new CommitteeChain with the option of replacing the -// clock source and signature verification for testing purposes. -func newCommitteeChain(db ethdb.KeyValueStore, config *types.ChainConfig, signerThreshold int, enforceTime bool, sigVerifier committeeSigVerifier, clock mclock.Clock, unixNano func() int64) *CommitteeChain { - s := &CommitteeChain{ - committeeCache: 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, - }, - } - - var err1, err2, err3 error - if s.fixedCommitteeRoots, err1 = newCanonicalStore[common.Hash](db, rawdb.FixedCommitteeRootKey); err1 != nil { - log.Error("Error creating fixed committee root store", "error", err1) - } - if s.committees, err2 = newCanonicalStore[*types.SerializedSyncCommittee](db, rawdb.SyncCommitteeKey); err2 != nil { - log.Error("Error creating committee store", "error", err2) - } - if s.updates, err3 = newCanonicalStore[*types.LightClientUpdate](db, rawdb.BestUpdateKey); err3 != nil { - log.Error("Error creating update store", "error", err3) - } - if err1 != nil || err2 != nil || err3 != nil || !s.checkConstraints() { - log.Info("Resetting invalid committee chain") - s.Reset() - } - // roll back invalid updates (might be necessary if forks have been changed since last time) - for !s.updates.periods.isEmpty() { - update, ok := s.updates.get(s.db, s.updates.periods.End-1) - if !ok { - log.Error("Sync committee update missing", "period", s.updates.periods.End-1) - s.Reset() - break - } - if valid, err := s.verifyUpdate(update); err != nil { - log.Error("Error validating update", "period", s.updates.periods.End-1, "error", err) - } else if valid { - break - } - if err := s.rollback(s.updates.periods.End); err != nil { - log.Error("Error writing batch into chain database", "error", err) - } - } - if !s.committees.periods.isEmpty() { - log.Trace("Sync committee chain loaded", "first period", s.committees.periods.Start, "last period", s.committees.periods.End-1) - } - return s -} - -// checkConstraints checks committee chain validity constraints -func (s *CommitteeChain) checkConstraints() bool { - isNotInFixedCommitteeRootRange := func(r periodRange) bool { - return s.fixedCommitteeRoots.periods.isEmpty() || - r.Start < s.fixedCommitteeRoots.periods.Start || - r.Start >= s.fixedCommitteeRoots.periods.End - } - - valid := true - if !s.updates.periods.isEmpty() { - if isNotInFixedCommitteeRootRange(s.updates.periods) { - log.Error("Start update is not in the fixed roots range") - valid = false - } - if s.committees.periods.Start > s.updates.periods.Start || s.committees.periods.End <= s.updates.periods.End { - log.Error("Missing committees in update range") - valid = false - } - } - if !s.committees.periods.isEmpty() { - if isNotInFixedCommitteeRootRange(s.committees.periods) { - log.Error("Start committee is not in the fixed roots range") - valid = false - } - if s.committees.periods.End > s.fixedCommitteeRoots.periods.End && s.committees.periods.End > s.updates.periods.End+1 { - log.Error("Last committee is neither in the fixed roots range nor proven by updates") - valid = false - } - } - return valid -} - -// Reset resets the committee chain. -func (s *CommitteeChain) Reset() { - s.chainmu.Lock() - defer s.chainmu.Unlock() - - if err := s.rollback(0); err != nil { - log.Error("Error writing batch into chain database", "error", err) - } -} - -// CheckpointInit initializes a CommitteeChain based on the checkpoint. -// Note: if the chain is already initialized and the committees proven by the -// checkpoint do match the existing chain then the chain is retained and the -// new checkpoint becomes fixed. -func (s *CommitteeChain) CheckpointInit(bootstrap *types.BootstrapData) error { - s.chainmu.Lock() - defer s.chainmu.Unlock() - - if err := bootstrap.Validate(); err != nil { - return err - } - - period := bootstrap.Header.SyncPeriod() - if err := s.deleteFixedCommitteeRootsFrom(period + 2); err != nil { - s.Reset() - return err - } - if s.addFixedCommitteeRoot(period, bootstrap.CommitteeRoot) != nil { - s.Reset() - if err := s.addFixedCommitteeRoot(period, bootstrap.CommitteeRoot); err != nil { - s.Reset() - return err - } - } - if err := s.addFixedCommitteeRoot(period+1, common.Hash(bootstrap.CommitteeBranch[0])); err != nil { - s.Reset() - return err - } - if err := s.addCommittee(period, bootstrap.Committee); err != nil { - s.Reset() - return err - } - return nil -} - -// addFixedCommitteeRoot sets a fixed committee root at the given period. -// Note that the period where the first committee is added has to have a fixed -// root which can either come from a BootstrapData or a trusted source. -func (s *CommitteeChain) addFixedCommitteeRoot(period uint64, root common.Hash) error { - if root == (common.Hash{}) { - return ErrWrongCommitteeRoot - } - - batch := s.db.NewBatch() - oldRoot := s.getCommitteeRoot(period) - if !s.fixedCommitteeRoots.periods.canExpand(period) { - // Note: the fixed committee root range should always be continuous and - // therefore the expected syncing method is to forward sync and optionally - // backward sync periods one by one, starting from a checkpoint. The only - // case when a root that is not adjacent to the already fixed ones can be - // fixed is when the same root has already been proven by an update chain. - // In this case the all roots in between can and should be fixed. - // This scenario makes sense when a new trusted checkpoint is added to an - // existing chain, ensuring that it will not be rolled back (might be - // important in case of low signer participation rate). - if root != oldRoot { - return ErrInvalidPeriod - } - // if the old root exists and matches the new one then it is guaranteed - // that the given period is after the existing fixed range and the roots - // in between can also be fixed. - for p := s.fixedCommitteeRoots.periods.End; p < period; p++ { - if err := s.fixedCommitteeRoots.add(batch, p, s.getCommitteeRoot(p)); err != nil { - return err - } - } - } - if oldRoot != (common.Hash{}) && (oldRoot != root) { - // existing old root was different, we have to reorg the chain - if err := s.rollback(period); err != nil { - return err - } - } - if err := s.fixedCommitteeRoots.add(batch, period, root); err != nil { - return err - } - if err := batch.Write(); err != nil { - log.Error("Error writing batch into chain database", "error", err) - return err - } - return nil -} - -// deleteFixedCommitteeRootsFrom deletes fixed roots starting from the given period. -// It also maintains chain consistency, meaning that it also deletes updates and -// committees if they are no longer supported by a valid update chain. -func (s *CommitteeChain) deleteFixedCommitteeRootsFrom(period uint64) error { - if period >= s.fixedCommitteeRoots.periods.End { - return nil - } - batch := s.db.NewBatch() - s.fixedCommitteeRoots.deleteFrom(batch, period) - if s.updates.periods.isEmpty() || period <= s.updates.periods.Start { - // Note: the first period of the update chain should always be fixed so if - // the fixed root at the first update is removed then the entire update chain - // and the proven committees have to be removed. Earlier committees in the - // remaining fixed root range can stay. - s.updates.deleteFrom(batch, period) - s.deleteCommitteesFrom(batch, period) - } else { - // The update chain stays intact, some previously fixed committee roots might - // get unfixed but are still proven by the update chain. If there were - // committees present after the range proven by updates, those should be - // removed if the belonging fixed roots are also removed. - fromPeriod := s.updates.periods.End + 1 // not proven by updates - if period > fromPeriod { - fromPeriod = period // also not justified by fixed roots - } - s.deleteCommitteesFrom(batch, fromPeriod) - } - if err := batch.Write(); err != nil { - log.Error("Error writing batch into chain database", "error", err) - return err - } - return nil -} - -// deleteCommitteesFrom deletes committees starting from the given period. -func (s *CommitteeChain) deleteCommitteesFrom(batch ethdb.Batch, period uint64) { - deleted := s.committees.deleteFrom(batch, period) - for period := deleted.Start; period < deleted.End; period++ { - s.committeeCache.Remove(period) - } -} - -// addCommittee adds a committee at the given period if possible. -func (s *CommitteeChain) addCommittee(period uint64, committee *types.SerializedSyncCommittee) error { - if !s.committees.periods.canExpand(period) { - return ErrInvalidPeriod - } - root := s.getCommitteeRoot(period) - if root == (common.Hash{}) { - return ErrInvalidPeriod - } - if root != committee.Root() { - return ErrWrongCommitteeRoot - } - if !s.committees.periods.contains(period) { - if err := s.committees.add(s.db, period, committee); err != nil { - return err - } - s.committeeCache.Remove(period) - } - return nil -} - -// InsertUpdate adds a new update if possible. -func (s *CommitteeChain) InsertUpdate(update *types.LightClientUpdate, nextCommittee *types.SerializedSyncCommittee) error { - s.chainmu.Lock() - defer s.chainmu.Unlock() - - period := update.AttestedHeader.Header.SyncPeriod() - if !s.updates.periods.canExpand(period) || !s.committees.periods.contains(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, ok := s.updates.get(s.db, period); ok && !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.fixedCommitteeRoots.periods.contains(period+1) && reorg { - return ErrCannotReorg - } - if ok, err := s.verifyUpdate(update); err != nil { - return err - } else if !ok { - return ErrInvalidUpdate - } - addCommittee := !s.committees.periods.contains(period+1) || reorg - if addCommittee { - if nextCommittee == nil { - return ErrNeedCommittee - } - if nextCommittee.Root() != update.NextSyncCommitteeRoot { - return ErrWrongCommitteeRoot - } - } - if reorg { - if err := s.rollback(period + 1); err != nil { - return err - } - } - batch := s.db.NewBatch() - if addCommittee { - if err := s.committees.add(batch, period+1, nextCommittee); err != nil { - return err - } - s.committeeCache.Remove(period + 1) - } - if err := s.updates.add(batch, period, update); err != nil { - return err - } - 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 -} - -// NextSyncPeriod returns the next period where an update can be added and also -// whether the chain is initialized at all. -func (s *CommitteeChain) NextSyncPeriod() (uint64, bool) { - s.chainmu.RLock() - defer s.chainmu.RUnlock() - - if s.committees.periods.isEmpty() { - return 0, false - } - if !s.updates.periods.isEmpty() { - return s.updates.periods.End, true - } - return s.committees.periods.End - 1, true -} - -// rollback removes all committees and fixed roots from the given period and updates -// starting from the previous period. -func (s *CommitteeChain) rollback(period uint64) error { - max := s.updates.periods.End + 1 - if s.committees.periods.End > max { - max = s.committees.periods.End - } - if s.fixedCommitteeRoots.periods.End > max { - max = s.fixedCommitteeRoots.periods.End - } - for max > period { - max-- - batch := s.db.NewBatch() - s.deleteCommitteesFrom(batch, max) - s.fixedCommitteeRoots.deleteFrom(batch, max) - if max > 0 { - s.updates.deleteFrom(batch, max-1) - } - if err := batch.Write(); err != nil { - log.Error("Error writing batch into chain database", "error", err) - return err - } - } - return nil -} - -// getCommitteeRoot returns the committee root at the given period, either fixed, -// proven by a previous update or both. It returns an empty hash if the committee -// root is unknown. -func (s *CommitteeChain) getCommitteeRoot(period uint64) common.Hash { - if root, ok := s.fixedCommitteeRoots.get(s.db, period); ok || period == 0 { - return root - } - if update, ok := s.updates.get(s.db, period-1); ok { - return update.NextSyncCommitteeRoot - } - return common.Hash{} -} - -// getSyncCommittee returns the deserialized sync committee at the given period. -func (s *CommitteeChain) getSyncCommittee(period uint64) (syncCommittee, error) { - if c, ok := s.committeeCache.Get(period); ok { - return c, nil - } - if sc, ok := s.committees.get(s.db, period); ok { - c, err := s.sigVerifier.deserializeSyncCommittee(sc) - if err != nil { - return nil, fmt.Errorf("Sync committee #%d deserialization error: %v", period, err) - } - s.committeeCache.Add(period, c) - return c, nil - } - return nil, fmt.Errorf("Missing serialized sync committee #%d", period) -} - -// VerifySignedHeader returns true if the given signed header 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 header 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, error) { - s.chainmu.RLock() - defer s.chainmu.RUnlock() - - return s.verifySignedHeader(head) -} - -func (s *CommitteeChain) verifySignedHeader(head types.SignedHeader) (bool, time.Duration, error) { - var age time.Duration - now := s.unixNano() - if head.Header.Slot < (uint64(now-math.MinInt64)/uint64(time.Second)-s.config.GenesisTime)/12 { - age = time.Duration(now - int64(time.Second)*int64(s.config.GenesisTime+head.Header.Slot*12)) - } else { - age = time.Duration(math.MinInt64) - } - if s.enforceTime && age < 0 { - return false, age, nil - } - committee, err := s.getSyncCommittee(types.SyncPeriod(head.SignatureSlot)) - if err != nil { - return false, 0, err - } - if committee == nil { - return false, age, nil - } - if signingRoot, err := s.config.Forks.SigningRoot(head.Header); err == nil { - return s.sigVerifier.verifySignature(committee, signingRoot, &head.Signature), age, nil - } - return false, age, nil -} - -// 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) -func (s *CommitteeChain) verifyUpdate(update *types.LightClientUpdate) (bool, error) { - // 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, err := s.verifySignedHeader(update.AttestedHeader) - if age < 0 { - log.Warn("Future committee update received", "age", age) - } - return ok, err -} diff --git a/beacon/light/committee_chain_test.go b/beacon/light/committee_chain_test.go deleted file mode 100644 index 60ea2a0efd..0000000000 --- a/beacon/light/committee_chain_test.go +++ /dev/null @@ -1,356 +0,0 @@ -// Copyright 2022 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see . - -package light - -import ( - "crypto/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 TestCommitteeChainFixedCommitteeRoots(t *testing.T) { - for _, reload := range []bool{false, true} { - c := newCommitteeChainTest(t, tfBase, 300, true) - c.setClockPeriod(7) - c.addFixedCommitteeRoot(tcBase, 4, nil) - c.addFixedCommitteeRoot(tcBase, 5, nil) - c.addFixedCommitteeRoot(tcBase, 6, nil) - c.addFixedCommitteeRoot(tcBase, 8, ErrInvalidPeriod) // range has to be continuous - c.addFixedCommitteeRoot(tcBase, 3, nil) - c.addFixedCommitteeRoot(tcBase, 2, nil) - if reload { - c.reloadChain() - } - c.addCommittee(tcBase, 4, nil) - c.addCommittee(tcBase, 6, ErrInvalidPeriod) // range has to be continuous - 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.addFixedCommitteeRoot(tcBase, 3, nil) - c.addFixedCommitteeRoot(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.addFixedCommitteeRoot(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.addFixedCommitteeRoot(tcBase, 3, nil) - c.addFixedCommitteeRoot(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.addFixedCommitteeRoot(tcBase, 0, nil) - c.addFixedCommitteeRoot(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) addFixedCommitteeRoot(tc *testCommitteeChain, period uint64, expErr error) { - if err := c.chain.addFixedCommitteeRoot(period, tc.periods[period].committee.Root()); err != expErr { - c.t.Errorf("Incorrect error output from addFixedCommitteeRoot 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) - } -} diff --git a/beacon/light/range.go b/beacon/light/range.go deleted file mode 100644 index 76ebe2381a..0000000000 --- a/beacon/light/range.go +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright 2023 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see . - -package light - -// periodRange represents a (possibly zero-length) range of integers (sync periods). -type periodRange struct { - Start, End uint64 -} - -// isEmpty returns true if the length of the range is zero. -func (a periodRange) isEmpty() bool { - return a.End == a.Start -} - -// contains returns true if the range includes the given period. -func (a periodRange) contains(period uint64) bool { - return period >= a.Start && period < a.End -} - -// canExpand returns true if the range includes or can be expanded with the given -// period (either the range is empty or the given period is inside, right before or -// right after the range). -func (a periodRange) canExpand(period uint64) bool { - return a.isEmpty() || (period+1 >= a.Start && period <= a.End) -} - -// expand expands the range with the given period. -// This method assumes that canExpand returned true: otherwise this is a no-op. -func (a *periodRange) expand(period uint64) { - if a.isEmpty() { - a.Start, a.End = period, period+1 - return - } - if a.Start == period+1 { - a.Start-- - } - if a.End == period { - a.End++ - } -} - -// split splits the range into two ranges. The 'fromPeriod' will be the first -// element in the second range (if present). -// The original range is unchanged by this operation -func (a *periodRange) split(fromPeriod uint64) (periodRange, periodRange) { - if fromPeriod <= a.Start { - // First range empty, everything in second range, - return periodRange{}, *a - } - if fromPeriod >= a.End { - // Second range empty, everything in first range, - return *a, periodRange{} - } - x := periodRange{a.Start, fromPeriod} - y := periodRange{fromPeriod, a.End} - return x, y -} - -// each invokes the supplied function fn once per period in range -func (a *periodRange) each(fn func(uint64)) { - for p := a.Start; p < a.End; p++ { - fn(p) - } -} diff --git a/beacon/light/test_helpers.go b/beacon/light/test_helpers.go deleted file mode 100644 index f537d963a6..0000000000 --- a/beacon/light/test_helpers.go +++ /dev/null @@ -1,152 +0,0 @@ -// Copyright 2023 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see . - -package light - -import ( - "crypto/rand" - "crypto/sha256" - mrand "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) *types.BootstrapData { - header, branch := makeTestHeaderWithMerkleProof(types.SyncPeriodStart(period)+200, params.StateIndexSyncCommittee, merkle.Value(committee.Root())) - return &types.BootstrapData{ - 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 mrand.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 -}