beacon/light: added comments and removed unused code

This commit is contained in:
Zsolt Felfoldi 2023-07-25 14:37:07 +02:00 committed by zsfelfoldi
parent 574f085715
commit 8ff38966c4
5 changed files with 68 additions and 74 deletions

View file

@ -23,13 +23,12 @@ import (
"github.com/ethereum/go-ethereum/beacon/params" "github.com/ethereum/go-ethereum/beacon/params"
"github.com/ethereum/go-ethereum/beacon/types" "github.com/ethereum/go-ethereum/beacon/types"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/rlp"
) )
var checkpointKey = []byte("checkpoint-") // block root -> RLP(CheckpointData) // CheckpointData contains a sync committee where light sync can be started,
// together with a proof through a beacon header and corresponding state.
// Note: CheckpointData is fetched from a server based on a known checkpoint hash.
type CheckpointData struct { type CheckpointData struct {
Header types.Header Header types.Header
CommitteeRoot common.Hash CommitteeRoot common.Hash
@ -37,6 +36,7 @@ type CheckpointData struct {
CommitteeBranch merkle.Values CommitteeBranch merkle.Values
} }
// Validate verifies the proof included in CheckpointData.
func (c *CheckpointData) Validate() error { func (c *CheckpointData) Validate() error {
if c.CommitteeRoot != c.Committee.Root() { if c.CommitteeRoot != c.Committee.Root() {
return errors.New("wrong committee root") return errors.New("wrong committee root")
@ -44,7 +44,8 @@ func (c *CheckpointData) Validate() error {
return merkle.VerifyProof(c.Header.StateRoot, params.StateIndexSyncCommittee, c.CommitteeBranch, merkle.Value(c.CommitteeRoot)) return merkle.VerifyProof(c.Header.StateRoot, params.StateIndexSyncCommittee, c.CommitteeBranch, merkle.Value(c.CommitteeRoot))
} }
// expected to be validated already // InitChain initializes a CommitteeChain based on the checkpoint.
// Note that the checkpoint is expected to be already validated.
func (c *CheckpointData) InitChain(chain *CommitteeChain) { func (c *CheckpointData) InitChain(chain *CommitteeChain) {
must := func(err error) { must := func(err error) {
if err != nil { if err != nil {
@ -60,51 +61,3 @@ func (c *CheckpointData) InitChain(chain *CommitteeChain) {
must(chain.AddFixedRoot(period+1, common.Hash(c.CommitteeBranch[0]))) must(chain.AddFixedRoot(period+1, common.Hash(c.CommitteeBranch[0])))
must(chain.AddCommittee(period, c.Committee)) 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

@ -47,16 +47,25 @@ var (
syncCommitteeKey = []byte("committee-") // bigEndian64(syncPeriod) -> serialized committee syncCommitteeKey = []byte("committee-") // bigEndian64(syncPeriod) -> serialized committee
) )
// CommitteeChain maintains a chain of sync committee updates and a small // CommitteeChain is a passive data structure that can validate, hold and update
// set of best known signed heads. It is used in all client configurations // a chain of beacon light sync committees and updates. It requires at least one
// operating on a beacon chain. It can sync its update chain and receive signed // externally set fixed committee root at the beginning of the chain which can
// heads from either an ODR or beacon node API backend and propagate/serve this // be set either based on a CheckpointData or a trusted source (a local beacon
// data to subscribed peers. Received signed heads are validated based on the // full node). This makes the structure useful for both light client and light
// known sync committee chain and added to the local set if valid or placed in a // server setups.
// deferred queue if the committees are not synced up to the period of the new //
// head yet. // It always maintains the following consistency constraints:
// Sync committee chain is either initialized from a weak subjectivity checkpoint // - a committee can only be present if its root hash matches an existing fixed
// or controlled by a BeaconChain that is driven by a trusted source (beacon node API). // 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 { type CommitteeChain struct {
lock sync.RWMutex lock sync.RWMutex
db ethdb.KeyValueStore db ethdb.KeyValueStore
@ -74,7 +83,7 @@ type CommitteeChain struct {
enforceTime bool enforceTime bool
} }
// NewCommitteeChain creates a new CommitteeChain // 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 { func NewCommitteeChain(db ethdb.KeyValueStore, config *types.ChainConfig, signerThreshold int, enforceTime bool, sigVerifier committeeSigVerifier, clock mclock.Clock, unixNano func() int64) *CommitteeChain {
s := &CommitteeChain{ s := &CommitteeChain{
fixedRoots: newCanonicalStore[common.Hash](db, fixedRootKey, func(root common.Hash) ([]byte, error) { fixedRoots: newCanonicalStore[common.Hash](db, fixedRootKey, func(root common.Hash) ([]byte, error) {
@ -162,6 +171,7 @@ func NewCommitteeChain(db ethdb.KeyValueStore, config *types.ChainConfig, signer
return s return s
} }
// Reset resets the committee chain.
func (s *CommitteeChain) Reset() { func (s *CommitteeChain) Reset() {
s.lock.Lock() s.lock.Lock()
defer s.lock.Unlock() defer s.lock.Unlock()
@ -173,6 +183,9 @@ func (s *CommitteeChain) Reset() {
} }
} }
// AddFixedRoot 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 CheckpointData or a trusted source.
func (s *CommitteeChain) AddFixedRoot(period uint64, root common.Hash) error { func (s *CommitteeChain) AddFixedRoot(period uint64, root common.Hash) error {
s.lock.Lock() s.lock.Lock()
defer s.lock.Unlock() defer s.lock.Unlock()
@ -199,6 +212,9 @@ func (s *CommitteeChain) AddFixedRoot(period uint64, root common.Hash) error {
return nil return nil
} }
// DeleteFixedRootsFrom 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) DeleteFixedRootsFrom(period uint64) error { func (s *CommitteeChain) DeleteFixedRootsFrom(period uint64) error {
s.lock.Lock() s.lock.Lock()
defer s.lock.Unlock() defer s.lock.Unlock()
@ -225,6 +241,7 @@ func (s *CommitteeChain) DeleteFixedRootsFrom(period uint64) error {
return nil return nil
} }
// deleteCommitteesFrom deletes committees starting from the given period.
func (s *CommitteeChain) deleteCommitteesFrom(batch ethdb.Batch, period uint64) { func (s *CommitteeChain) deleteCommitteesFrom(batch ethdb.Batch, period uint64) {
deleted := s.committees.deleteFrom(batch, period) deleted := s.committees.deleteFrom(batch, period)
for period := deleted.First; period < deleted.AfterLast; period++ { for period := deleted.First; period < deleted.AfterLast; period++ {
@ -232,10 +249,12 @@ func (s *CommitteeChain) deleteCommitteesFrom(batch ethdb.Batch, period uint64)
} }
} }
// GetCommittee returns the committee at the given period.
func (s *CommitteeChain) GetCommittee(period uint64) *types.SerializedSyncCommittee { func (s *CommitteeChain) GetCommittee(period uint64) *types.SerializedSyncCommittee {
return s.committees.get(period) return s.committees.get(period)
} }
// AddCommittee adds a committee at the given period if possible.
func (s *CommitteeChain) AddCommittee(period uint64, committee *types.SerializedSyncCommittee) error { func (s *CommitteeChain) AddCommittee(period uint64, committee *types.SerializedSyncCommittee) error {
s.lock.Lock() s.lock.Lock()
defer s.lock.Unlock() defer s.lock.Unlock()
@ -257,10 +276,12 @@ func (s *CommitteeChain) AddCommittee(period uint64, committee *types.Serialized
return nil return nil
} }
// GetUpdate returns the update at the given period.
func (s *CommitteeChain) GetUpdate(period uint64) *types.LightClientUpdate { func (s *CommitteeChain) GetUpdate(period uint64) *types.LightClientUpdate {
return s.updates.get(period) return s.updates.get(period)
} }
// InsertUpdate adds a new update if possible.
func (s *CommitteeChain) InsertUpdate(update *types.LightClientUpdate, nextCommittee *types.SerializedSyncCommittee) error { func (s *CommitteeChain) InsertUpdate(update *types.LightClientUpdate, nextCommittee *types.SerializedSyncCommittee) error {
s.lock.Lock() s.lock.Lock()
defer s.lock.Unlock() defer s.lock.Unlock()
@ -313,6 +334,8 @@ func (s *CommitteeChain) InsertUpdate(update *types.LightClientUpdate, nextCommi
return nil 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) { func (s *CommitteeChain) NextSyncPeriod() (uint64, bool) {
s.lock.RLock() s.lock.RLock()
defer s.lock.RUnlock() defer s.lock.RUnlock()
@ -326,6 +349,8 @@ func (s *CommitteeChain) NextSyncPeriod() (uint64, bool) {
return s.committees.AfterLast - 1, true return s.committees.AfterLast - 1, true
} }
// rollback removes all committees and fixed roots from the given period and updates
// starting from the previous period.
func (s *CommitteeChain) rollback(batch ethdb.Batch, period uint64) { func (s *CommitteeChain) rollback(batch ethdb.Batch, period uint64) {
s.deleteCommitteesFrom(batch, period) s.deleteCommitteesFrom(batch, period)
s.fixedRoots.deleteFrom(batch, period) s.fixedRoots.deleteFrom(batch, period)
@ -335,6 +360,9 @@ func (s *CommitteeChain) rollback(batch ethdb.Batch, period uint64) {
s.updates.deleteFrom(batch, period) s.updates.deleteFrom(batch, period)
} }
// 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 { func (s *CommitteeChain) getCommitteeRoot(period uint64) common.Hash {
if root := s.fixedRoots.get(period); root != (common.Hash{}) || period == 0 { if root := s.fixedRoots.get(period); root != (common.Hash{}) || period == 0 {
return root return root
@ -345,8 +373,7 @@ func (s *CommitteeChain) getCommitteeRoot(period uint64) common.Hash {
return common.Hash{} return common.Hash{}
} }
// getSyncCommittee returns the deserialized sync committee at the given period // 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 { func (s *CommitteeChain) getSyncCommittee(period uint64) syncCommittee {
if c, ok := s.syncCommitteeCache.Get(period); ok { if c, ok := s.syncCommitteeCache.Get(period); ok {
return c return c
@ -364,9 +391,9 @@ func (s *CommitteeChain) getSyncCommittee(period uint64) syncCommittee {
return nil return nil
} }
// VerifySignedHeader returns true if the given signed head has a valid signature // VerifySignedHeader returns true if the given signed header has a valid signature
// according to the local committee chain. The caller should ensure that the // 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 // committees advertised by the same source where the signed header came from are
// synced before verifying the signature. // synced before verifying the signature.
// The age of the header is also returned (the time elapsed since the beginning // 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 // of the given slot, according to the local system clock). If enforceTime is
@ -378,7 +405,6 @@ func (s *CommitteeChain) VerifySignedHeader(head types.SignedHeader) (bool, time
return s.verifySignedHeader(head) return s.verifySignedHeader(head)
} }
// (rlock required)
func (s *CommitteeChain) verifySignedHeader(head types.SignedHeader) (bool, time.Duration) { func (s *CommitteeChain) verifySignedHeader(head types.SignedHeader) (bool, time.Duration) {
var ( var (
slotTime = int64(time.Second) * int64(s.config.GenesisTime+head.Header.Slot*12) slotTime = int64(time.Second) * int64(s.config.GenesisTime+head.Header.Slot*12)
@ -400,7 +426,6 @@ func (s *CommitteeChain) verifySignedHeader(head types.SignedHeader) (bool, time
// verifyUpdate checks whether the header signature is correct and the update // verifyUpdate checks whether the header signature is correct and the update
// fits into the specified constraints (assumes that the update has been // fits into the specified constraints (assumes that the update has been
// successfully validated previously) // successfully validated previously)
// (rlock required)
func (s *CommitteeChain) verifyUpdate(update *types.LightClientUpdate) bool { func (s *CommitteeChain) verifyUpdate(update *types.LightClientUpdate) bool {
// Note: SignatureSlot determines the sync period of the committee used for signature // Note: SignatureSlot determines the sync period of the committee used for signature
// verification. Though in reality SignatureSlot is always bigger than update.Header.Slot, // verification. Though in reality SignatureSlot is always bigger than update.Header.Slot,
@ -413,6 +438,8 @@ func (s *CommitteeChain) verifyUpdate(update *types.LightClientUpdate) bool {
return ok return ok
} }
// canonicalStore stores instances of the given type in a database and caches
// them in memory, associated with a continuous range of period numbers.
type canonicalStore[T any] struct { type canonicalStore[T any] struct {
Range Range
db ethdb.KeyValueStore db ethdb.KeyValueStore
@ -422,6 +449,7 @@ type canonicalStore[T any] struct {
decode func([]byte) (T, error) decode func([]byte) (T, error)
} }
// newCanonicalStore creates a new canonicalStore.
func newCanonicalStore[T any](db ethdb.KeyValueStore, keyPrefix []byte, func newCanonicalStore[T any](db ethdb.KeyValueStore, keyPrefix []byte,
encode func(T) ([]byte, error), decode func([]byte) (T, error)) *canonicalStore[T] { encode func(T) ([]byte, error), decode func([]byte) (T, error)) *canonicalStore[T] {
cs := &canonicalStore[T]{ cs := &canonicalStore[T]{
@ -451,6 +479,7 @@ func newCanonicalStore[T any](db ethdb.KeyValueStore, keyPrefix []byte,
return cs return cs
} }
// getDbKey returns the database key belonging to the given period.
func (cs *canonicalStore[T]) getDbKey(period uint64) []byte { func (cs *canonicalStore[T]) getDbKey(period uint64) []byte {
var ( var (
kl = len(cs.keyPrefix) kl = len(cs.keyPrefix)
@ -461,6 +490,8 @@ func (cs *canonicalStore[T]) getDbKey(period uint64) []byte {
return key return key
} }
// add adds the given item to the database. It also ensures that the range remains
// continuous. Can be used both in batch mode and as a standalone operation.
func (cs *canonicalStore[T]) add(batch ethdb.Batch, period uint64, value T) { func (cs *canonicalStore[T]) add(batch ethdb.Batch, period uint64, value T) {
if !cs.CanExpand(period) { if !cs.CanExpand(period) {
log.Error("Cannot expand canonical store", "range.first", cs.First, "range.afterLast", cs.AfterLast, "new period", period) log.Error("Cannot expand canonical store", "range.first", cs.First, "range.afterLast", cs.AfterLast, "new period", period)
@ -484,7 +515,8 @@ func (cs *canonicalStore[T]) add(batch ethdb.Batch, period uint64, value T) {
cs.Expand(period) cs.Expand(period)
} }
// should only be used in batch mode // deleteFrom removes items starting from the given period. Can only be used in
// batch mode.
func (cs *canonicalStore[T]) deleteFrom(batch ethdb.Batch, fromPeriod uint64) (deleted Range) { func (cs *canonicalStore[T]) deleteFrom(batch ethdb.Batch, fromPeriod uint64) (deleted Range) {
if fromPeriod >= cs.AfterLast { if fromPeriod >= cs.AfterLast {
return return
@ -505,6 +537,8 @@ func (cs *canonicalStore[T]) deleteFrom(batch ethdb.Batch, fromPeriod uint64) (d
return return
} }
// 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(period uint64) T { func (cs *canonicalStore[T]) get(period uint64) T {
if value, ok := cs.cache.Get(period); ok { if value, ok := cs.cache.Get(period); ok {
return value return value

View file

@ -17,7 +17,7 @@
package light package light
import ( import (
"math/rand" "crypto/rand"
"testing" "testing"
"time" "time"

View file

@ -16,23 +16,29 @@
package light package light
// Range represents a (possibly zero-length) range of integers (sync periods).
type Range struct { type Range struct {
First uint64 First uint64
AfterLast uint64 AfterLast uint64
} }
// IsEmpty returns true if the length of the range is zero.
func (a Range) IsEmpty() bool { func (a Range) IsEmpty() bool {
return a.AfterLast == a.First return a.AfterLast == a.First
} }
// Includes returns true if the range includes the given period.
func (a Range) Includes(period uint64) bool { func (a Range) Includes(period uint64) bool {
return period >= a.First && period < a.AfterLast return period >= a.First && period < a.AfterLast
} }
// CanExpand returns true if the range can be expanded with the given period
// (either the range is empty or the new period is right before or after the range).
func (a Range) CanExpand(period uint64) bool { func (a Range) CanExpand(period uint64) bool {
return a.IsEmpty() || (period+1 >= a.First && period <= a.AfterLast) return a.IsEmpty() || (period+1 >= a.First && period <= a.AfterLast)
} }
// Expand expands the range with the given period (assumes that CanExpand returned true).
func (a *Range) Expand(period uint64) { func (a *Range) Expand(period uint64) {
if a.IsEmpty() { if a.IsEmpty() {
a.First, a.AfterLast = period, period+1 a.First, a.AfterLast = period, period+1

View file

@ -17,8 +17,9 @@
package light package light
import ( import (
"crypto/rand"
"crypto/sha256" "crypto/sha256"
"math/rand" mrand "math/rand"
"github.com/ethereum/go-ethereum/beacon/merkle" "github.com/ethereum/go-ethereum/beacon/merkle"
"github.com/ethereum/go-ethereum/beacon/params" "github.com/ethereum/go-ethereum/beacon/params"
@ -73,7 +74,7 @@ func GenerateTestCheckpoint(period uint64, committee *types.SerializedSyncCommit
func makeBitmask(signerCount int) (bitmask [params.SyncCommitteeBitmaskSize]byte) { func makeBitmask(signerCount int) (bitmask [params.SyncCommitteeBitmaskSize]byte) {
for i := 0; i < params.SyncCommitteeSize; i++ { for i := 0; i < params.SyncCommitteeSize; i++ {
if rand.Intn(params.SyncCommitteeSize-i) < signerCount { if mrand.Intn(params.SyncCommitteeSize-i) < signerCount {
bitmask[i/8] += byte(1) << (i & 7) bitmask[i/8] += byte(1) << (i & 7)
signerCount-- signerCount--
} }