github.1485827954.workers.dev/ethereum/go-ethereum@v1.14.3/beacon/light/committee_chain.go (about)

     1  // Copyright 2023 The go-ethereum Authors
     2  // This file is part of the go-ethereum library.
     3  //
     4  // The go-ethereum library is free software: you can redistribute it and/or modify
     5  // it under the terms of the GNU Lesser General Public License as published by
     6  // the Free Software Foundation, either version 3 of the License, or
     7  // (at your option) any later version.
     8  //
     9  // The go-ethereum library is distributed in the hope that it will be useful,
    10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    12  // GNU Lesser General Public License for more details.
    13  //
    14  // You should have received a copy of the GNU Lesser General Public License
    15  // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
    16  
    17  package light
    18  
    19  import (
    20  	"errors"
    21  	"fmt"
    22  	"math"
    23  	"sync"
    24  	"time"
    25  
    26  	"github.com/ethereum/go-ethereum/beacon/params"
    27  	"github.com/ethereum/go-ethereum/beacon/types"
    28  	"github.com/ethereum/go-ethereum/common"
    29  	"github.com/ethereum/go-ethereum/common/lru"
    30  	"github.com/ethereum/go-ethereum/common/mclock"
    31  	"github.com/ethereum/go-ethereum/core/rawdb"
    32  	"github.com/ethereum/go-ethereum/ethdb"
    33  	"github.com/ethereum/go-ethereum/log"
    34  )
    35  
    36  var (
    37  	ErrNeedCommittee      = errors.New("sync committee required")
    38  	ErrInvalidUpdate      = errors.New("invalid committee update")
    39  	ErrInvalidPeriod      = errors.New("invalid update period")
    40  	ErrWrongCommitteeRoot = errors.New("wrong committee root")
    41  	ErrCannotReorg        = errors.New("can not reorg committee chain")
    42  )
    43  
    44  // CommitteeChain is a passive data structure that can validate, hold and update
    45  // a chain of beacon light sync committees and updates. It requires at least one
    46  // externally set fixed committee root at the beginning of the chain which can
    47  // be set either based on a BootstrapData or a trusted source (a local beacon
    48  // full node). This makes the structure useful for both light client and light
    49  // server setups.
    50  //
    51  // It always maintains the following consistency constraints:
    52  //   - a committee can only be present if its root hash matches an existing fixed
    53  //     root or if it is proven by an update at the previous period
    54  //   - an update can only be present if a committee is present at the same period
    55  //     and the update signature is valid and has enough participants.
    56  //     The committee at the next period (proven by the update) should also be
    57  //     present (note that this means they can only be added together if neither
    58  //     is present yet). If a fixed root is present at the next period then the
    59  //     update can only be present if it proves the same committee root.
    60  //
    61  // Once synced to the current sync period, CommitteeChain can also validate
    62  // signed beacon headers.
    63  type CommitteeChain struct {
    64  	// chainmu guards against concurrent access to the canonicalStore structures
    65  	// (updates, committees, fixedCommitteeRoots) and ensures that they stay consistent
    66  	// with each other and with committeeCache.
    67  	chainmu             sync.RWMutex
    68  	db                  ethdb.KeyValueStore
    69  	updates             *canonicalStore[*types.LightClientUpdate]
    70  	committees          *canonicalStore[*types.SerializedSyncCommittee]
    71  	fixedCommitteeRoots *canonicalStore[common.Hash]
    72  	committeeCache      *lru.Cache[uint64, syncCommittee] // cache deserialized committees
    73  	changeCounter       uint64
    74  
    75  	clock       mclock.Clock         // monotonic clock (simulated clock in tests)
    76  	unixNano    func() int64         // system clock (simulated clock in tests)
    77  	sigVerifier committeeSigVerifier // BLS sig verifier (dummy verifier in tests)
    78  
    79  	config             *types.ChainConfig
    80  	signerThreshold    int
    81  	minimumUpdateScore types.UpdateScore
    82  	enforceTime        bool // enforceTime specifies whether the age of a signed header should be checked
    83  }
    84  
    85  // NewCommitteeChain creates a new CommitteeChain.
    86  func NewCommitteeChain(db ethdb.KeyValueStore, config *types.ChainConfig, signerThreshold int, enforceTime bool) *CommitteeChain {
    87  	return newCommitteeChain(db, config, signerThreshold, enforceTime, blsVerifier{}, &mclock.System{}, func() int64 { return time.Now().UnixNano() })
    88  }
    89  
    90  // NewTestCommitteeChain creates a new CommitteeChain for testing.
    91  func NewTestCommitteeChain(db ethdb.KeyValueStore, config *types.ChainConfig, signerThreshold int, enforceTime bool, clock *mclock.Simulated) *CommitteeChain {
    92  	return newCommitteeChain(db, config, signerThreshold, enforceTime, dummyVerifier{}, clock, func() int64 { return int64(clock.Now()) })
    93  }
    94  
    95  // newCommitteeChain creates a new CommitteeChain with the option of replacing the
    96  // clock source and signature verification for testing purposes.
    97  func newCommitteeChain(db ethdb.KeyValueStore, config *types.ChainConfig, signerThreshold int, enforceTime bool, sigVerifier committeeSigVerifier, clock mclock.Clock, unixNano func() int64) *CommitteeChain {
    98  	s := &CommitteeChain{
    99  		committeeCache:  lru.NewCache[uint64, syncCommittee](10),
   100  		db:              db,
   101  		sigVerifier:     sigVerifier,
   102  		clock:           clock,
   103  		unixNano:        unixNano,
   104  		config:          config,
   105  		signerThreshold: signerThreshold,
   106  		enforceTime:     enforceTime,
   107  		minimumUpdateScore: types.UpdateScore{
   108  			SignerCount:    uint32(signerThreshold),
   109  			SubPeriodIndex: params.SyncPeriodLength / 16,
   110  		},
   111  	}
   112  
   113  	var err1, err2, err3 error
   114  	if s.fixedCommitteeRoots, err1 = newCanonicalStore[common.Hash](db, rawdb.FixedCommitteeRootKey); err1 != nil {
   115  		log.Error("Error creating fixed committee root store", "error", err1)
   116  	}
   117  	if s.committees, err2 = newCanonicalStore[*types.SerializedSyncCommittee](db, rawdb.SyncCommitteeKey); err2 != nil {
   118  		log.Error("Error creating committee store", "error", err2)
   119  	}
   120  	if s.updates, err3 = newCanonicalStore[*types.LightClientUpdate](db, rawdb.BestUpdateKey); err3 != nil {
   121  		log.Error("Error creating update store", "error", err3)
   122  	}
   123  	if err1 != nil || err2 != nil || err3 != nil || !s.checkConstraints() {
   124  		log.Info("Resetting invalid committee chain")
   125  		s.Reset()
   126  	}
   127  	// roll back invalid updates (might be necessary if forks have been changed since last time)
   128  	for !s.updates.periods.isEmpty() {
   129  		update, ok := s.updates.get(s.db, s.updates.periods.End-1)
   130  		if !ok {
   131  			log.Error("Sync committee update missing", "period", s.updates.periods.End-1)
   132  			s.Reset()
   133  			break
   134  		}
   135  		if valid, err := s.verifyUpdate(update); err != nil {
   136  			log.Error("Error validating update", "period", s.updates.periods.End-1, "error", err)
   137  		} else if valid {
   138  			break
   139  		}
   140  		if err := s.rollback(s.updates.periods.End); err != nil {
   141  			log.Error("Error writing batch into chain database", "error", err)
   142  		}
   143  	}
   144  	if !s.committees.periods.isEmpty() {
   145  		log.Trace("Sync committee chain loaded", "first period", s.committees.periods.Start, "last period", s.committees.periods.End-1)
   146  	}
   147  	return s
   148  }
   149  
   150  // checkConstraints checks committee chain validity constraints
   151  func (s *CommitteeChain) checkConstraints() bool {
   152  	isNotInFixedCommitteeRootRange := func(r periodRange) bool {
   153  		return s.fixedCommitteeRoots.periods.isEmpty() ||
   154  			r.Start < s.fixedCommitteeRoots.periods.Start ||
   155  			r.Start >= s.fixedCommitteeRoots.periods.End
   156  	}
   157  
   158  	valid := true
   159  	if !s.updates.periods.isEmpty() {
   160  		if isNotInFixedCommitteeRootRange(s.updates.periods) {
   161  			log.Error("Start update is not in the fixed roots range")
   162  			valid = false
   163  		}
   164  		if s.committees.periods.Start > s.updates.periods.Start || s.committees.periods.End <= s.updates.periods.End {
   165  			log.Error("Missing committees in update range")
   166  			valid = false
   167  		}
   168  	}
   169  	if !s.committees.periods.isEmpty() {
   170  		if isNotInFixedCommitteeRootRange(s.committees.periods) {
   171  			log.Error("Start committee is not in the fixed roots range")
   172  			valid = false
   173  		}
   174  		if s.committees.periods.End > s.fixedCommitteeRoots.periods.End && s.committees.periods.End > s.updates.periods.End+1 {
   175  			log.Error("Last committee is neither in the fixed roots range nor proven by updates")
   176  			valid = false
   177  		}
   178  	}
   179  	return valid
   180  }
   181  
   182  // Reset resets the committee chain.
   183  func (s *CommitteeChain) Reset() {
   184  	s.chainmu.Lock()
   185  	defer s.chainmu.Unlock()
   186  
   187  	if err := s.rollback(0); err != nil {
   188  		log.Error("Error writing batch into chain database", "error", err)
   189  	}
   190  	s.changeCounter++
   191  }
   192  
   193  // CheckpointInit initializes a CommitteeChain based on a checkpoint.
   194  // Note: if the chain is already initialized and the committees proven by the
   195  // checkpoint do match the existing chain then the chain is retained and the
   196  // new checkpoint becomes fixed.
   197  func (s *CommitteeChain) CheckpointInit(bootstrap types.BootstrapData) error {
   198  	s.chainmu.Lock()
   199  	defer s.chainmu.Unlock()
   200  
   201  	if err := bootstrap.Validate(); err != nil {
   202  		return err
   203  	}
   204  	period := bootstrap.Header.SyncPeriod()
   205  	if err := s.deleteFixedCommitteeRootsFrom(period + 2); err != nil {
   206  		s.Reset()
   207  		return err
   208  	}
   209  	if s.addFixedCommitteeRoot(period, bootstrap.CommitteeRoot) != nil {
   210  		s.Reset()
   211  		if err := s.addFixedCommitteeRoot(period, bootstrap.CommitteeRoot); err != nil {
   212  			s.Reset()
   213  			return err
   214  		}
   215  	}
   216  	if err := s.addFixedCommitteeRoot(period+1, common.Hash(bootstrap.CommitteeBranch[0])); err != nil {
   217  		s.Reset()
   218  		return err
   219  	}
   220  	if err := s.addCommittee(period, bootstrap.Committee); err != nil {
   221  		s.Reset()
   222  		return err
   223  	}
   224  	s.changeCounter++
   225  	return nil
   226  }
   227  
   228  // addFixedCommitteeRoot sets a fixed committee root at the given period.
   229  // Note that the period where the first committee is added has to have a fixed
   230  // root which can either come from a BootstrapData or a trusted source.
   231  func (s *CommitteeChain) addFixedCommitteeRoot(period uint64, root common.Hash) error {
   232  	if root == (common.Hash{}) {
   233  		return ErrWrongCommitteeRoot
   234  	}
   235  
   236  	batch := s.db.NewBatch()
   237  	oldRoot := s.getCommitteeRoot(period)
   238  	if !s.fixedCommitteeRoots.periods.canExpand(period) {
   239  		// Note: the fixed committee root range should always be continuous and
   240  		// therefore the expected syncing method is to forward sync and optionally
   241  		// backward sync periods one by one, starting from a checkpoint. The only
   242  		// case when a root that is not adjacent to the already fixed ones can be
   243  		// fixed is when the same root has already been proven by an update chain.
   244  		// In this case the all roots in between can and should be fixed.
   245  		// This scenario makes sense when a new trusted checkpoint is added to an
   246  		// existing chain, ensuring that it will not be rolled back (might be
   247  		// important in case of low signer participation rate).
   248  		if root != oldRoot {
   249  			return ErrInvalidPeriod
   250  		}
   251  		// if the old root exists and matches the new one then it is guaranteed
   252  		// that the given period is after the existing fixed range and the roots
   253  		// in between can also be fixed.
   254  		for p := s.fixedCommitteeRoots.periods.End; p < period; p++ {
   255  			if err := s.fixedCommitteeRoots.add(batch, p, s.getCommitteeRoot(p)); err != nil {
   256  				return err
   257  			}
   258  		}
   259  	}
   260  	if oldRoot != (common.Hash{}) && (oldRoot != root) {
   261  		// existing old root was different, we have to reorg the chain
   262  		if err := s.rollback(period); err != nil {
   263  			return err
   264  		}
   265  	}
   266  	if err := s.fixedCommitteeRoots.add(batch, period, root); err != nil {
   267  		return err
   268  	}
   269  	if err := batch.Write(); err != nil {
   270  		log.Error("Error writing batch into chain database", "error", err)
   271  		return err
   272  	}
   273  	return nil
   274  }
   275  
   276  // deleteFixedCommitteeRootsFrom deletes fixed roots starting from the given period.
   277  // It also maintains chain consistency, meaning that it also deletes updates and
   278  // committees if they are no longer supported by a valid update chain.
   279  func (s *CommitteeChain) deleteFixedCommitteeRootsFrom(period uint64) error {
   280  	if period >= s.fixedCommitteeRoots.periods.End {
   281  		return nil
   282  	}
   283  	batch := s.db.NewBatch()
   284  	s.fixedCommitteeRoots.deleteFrom(batch, period)
   285  	if s.updates.periods.isEmpty() || period <= s.updates.periods.Start {
   286  		// Note: the first period of the update chain should always be fixed so if
   287  		// the fixed root at the first update is removed then the entire update chain
   288  		// and the proven committees have to be removed. Earlier committees in the
   289  		// remaining fixed root range can stay.
   290  		s.updates.deleteFrom(batch, period)
   291  		s.deleteCommitteesFrom(batch, period)
   292  	} else {
   293  		// The update chain stays intact, some previously fixed committee roots might
   294  		// get unfixed but are still proven by the update chain. If there were
   295  		// committees present after the range proven by updates, those should be
   296  		// removed if the belonging fixed roots are also removed.
   297  		fromPeriod := s.updates.periods.End + 1 // not proven by updates
   298  		if period > fromPeriod {
   299  			fromPeriod = period // also not justified by fixed roots
   300  		}
   301  		s.deleteCommitteesFrom(batch, fromPeriod)
   302  	}
   303  	if err := batch.Write(); err != nil {
   304  		log.Error("Error writing batch into chain database", "error", err)
   305  		return err
   306  	}
   307  	return nil
   308  }
   309  
   310  // deleteCommitteesFrom deletes committees starting from the given period.
   311  func (s *CommitteeChain) deleteCommitteesFrom(batch ethdb.Batch, period uint64) {
   312  	deleted := s.committees.deleteFrom(batch, period)
   313  	for period := deleted.Start; period < deleted.End; period++ {
   314  		s.committeeCache.Remove(period)
   315  	}
   316  }
   317  
   318  // addCommittee adds a committee at the given period if possible.
   319  func (s *CommitteeChain) addCommittee(period uint64, committee *types.SerializedSyncCommittee) error {
   320  	if !s.committees.periods.canExpand(period) {
   321  		return ErrInvalidPeriod
   322  	}
   323  	root := s.getCommitteeRoot(period)
   324  	if root == (common.Hash{}) {
   325  		return ErrInvalidPeriod
   326  	}
   327  	if root != committee.Root() {
   328  		return ErrWrongCommitteeRoot
   329  	}
   330  	if !s.committees.periods.contains(period) {
   331  		if err := s.committees.add(s.db, period, committee); err != nil {
   332  			return err
   333  		}
   334  		s.committeeCache.Remove(period)
   335  	}
   336  	return nil
   337  }
   338  
   339  // InsertUpdate adds a new update if possible.
   340  func (s *CommitteeChain) InsertUpdate(update *types.LightClientUpdate, nextCommittee *types.SerializedSyncCommittee) error {
   341  	s.chainmu.Lock()
   342  	defer s.chainmu.Unlock()
   343  
   344  	period := update.AttestedHeader.Header.SyncPeriod()
   345  	if !s.updates.periods.canExpand(period) || !s.committees.periods.contains(period) {
   346  		return ErrInvalidPeriod
   347  	}
   348  	if s.minimumUpdateScore.BetterThan(update.Score()) {
   349  		return ErrInvalidUpdate
   350  	}
   351  	oldRoot := s.getCommitteeRoot(period + 1)
   352  	reorg := oldRoot != (common.Hash{}) && oldRoot != update.NextSyncCommitteeRoot
   353  	if oldUpdate, ok := s.updates.get(s.db, period); ok && !update.Score().BetterThan(oldUpdate.Score()) {
   354  		// a better or equal update already exists; no changes, only fail if new one tried to reorg
   355  		if reorg {
   356  			return ErrCannotReorg
   357  		}
   358  		return nil
   359  	}
   360  	if s.fixedCommitteeRoots.periods.contains(period+1) && reorg {
   361  		return ErrCannotReorg
   362  	}
   363  	if ok, err := s.verifyUpdate(update); err != nil {
   364  		return err
   365  	} else if !ok {
   366  		return ErrInvalidUpdate
   367  	}
   368  	addCommittee := !s.committees.periods.contains(period+1) || reorg
   369  	if addCommittee {
   370  		if nextCommittee == nil {
   371  			return ErrNeedCommittee
   372  		}
   373  		if nextCommittee.Root() != update.NextSyncCommitteeRoot {
   374  			return ErrWrongCommitteeRoot
   375  		}
   376  	}
   377  	s.changeCounter++
   378  	if reorg {
   379  		if err := s.rollback(period + 1); err != nil {
   380  			return err
   381  		}
   382  	}
   383  	batch := s.db.NewBatch()
   384  	if addCommittee {
   385  		if err := s.committees.add(batch, period+1, nextCommittee); err != nil {
   386  			return err
   387  		}
   388  		s.committeeCache.Remove(period + 1)
   389  	}
   390  	if err := s.updates.add(batch, period, update); err != nil {
   391  		return err
   392  	}
   393  	if err := batch.Write(); err != nil {
   394  		log.Error("Error writing batch into chain database", "error", err)
   395  		return err
   396  	}
   397  	log.Info("Inserted new committee update", "period", period, "next committee root", update.NextSyncCommitteeRoot)
   398  	return nil
   399  }
   400  
   401  // NextSyncPeriod returns the next period where an update can be added and also
   402  // whether the chain is initialized at all.
   403  func (s *CommitteeChain) NextSyncPeriod() (uint64, bool) {
   404  	s.chainmu.RLock()
   405  	defer s.chainmu.RUnlock()
   406  
   407  	if s.committees.periods.isEmpty() {
   408  		return 0, false
   409  	}
   410  	if !s.updates.periods.isEmpty() {
   411  		return s.updates.periods.End, true
   412  	}
   413  	return s.committees.periods.End - 1, true
   414  }
   415  
   416  func (s *CommitteeChain) ChangeCounter() uint64 {
   417  	s.chainmu.RLock()
   418  	defer s.chainmu.RUnlock()
   419  
   420  	return s.changeCounter
   421  }
   422  
   423  // rollback removes all committees and fixed roots from the given period and updates
   424  // starting from the previous period.
   425  func (s *CommitteeChain) rollback(period uint64) error {
   426  	max := s.updates.periods.End + 1
   427  	if s.committees.periods.End > max {
   428  		max = s.committees.periods.End
   429  	}
   430  	if s.fixedCommitteeRoots.periods.End > max {
   431  		max = s.fixedCommitteeRoots.periods.End
   432  	}
   433  	for max > period {
   434  		max--
   435  		batch := s.db.NewBatch()
   436  		s.deleteCommitteesFrom(batch, max)
   437  		s.fixedCommitteeRoots.deleteFrom(batch, max)
   438  		if max > 0 {
   439  			s.updates.deleteFrom(batch, max-1)
   440  		}
   441  		if err := batch.Write(); err != nil {
   442  			log.Error("Error writing batch into chain database", "error", err)
   443  			return err
   444  		}
   445  	}
   446  	return nil
   447  }
   448  
   449  // getCommitteeRoot returns the committee root at the given period, either fixed,
   450  // proven by a previous update or both. It returns an empty hash if the committee
   451  // root is unknown.
   452  func (s *CommitteeChain) getCommitteeRoot(period uint64) common.Hash {
   453  	if root, ok := s.fixedCommitteeRoots.get(s.db, period); ok || period == 0 {
   454  		return root
   455  	}
   456  	if update, ok := s.updates.get(s.db, period-1); ok {
   457  		return update.NextSyncCommitteeRoot
   458  	}
   459  	return common.Hash{}
   460  }
   461  
   462  // getSyncCommittee returns the deserialized sync committee at the given period.
   463  func (s *CommitteeChain) getSyncCommittee(period uint64) (syncCommittee, error) {
   464  	if c, ok := s.committeeCache.Get(period); ok {
   465  		return c, nil
   466  	}
   467  	if sc, ok := s.committees.get(s.db, period); ok {
   468  		c, err := s.sigVerifier.deserializeSyncCommittee(sc)
   469  		if err != nil {
   470  			return nil, fmt.Errorf("sync committee #%d deserialization error: %v", period, err)
   471  		}
   472  		s.committeeCache.Add(period, c)
   473  		return c, nil
   474  	}
   475  	return nil, fmt.Errorf("missing serialized sync committee #%d", period)
   476  }
   477  
   478  // VerifySignedHeader returns true if the given signed header has a valid signature
   479  // according to the local committee chain. The caller should ensure that the
   480  // committees advertised by the same source where the signed header came from are
   481  // synced before verifying the signature.
   482  // The age of the header is also returned (the time elapsed since the beginning
   483  // of the given slot, according to the local system clock). If enforceTime is
   484  // true then negative age (future) headers are rejected.
   485  func (s *CommitteeChain) VerifySignedHeader(head types.SignedHeader) (bool, time.Duration, error) {
   486  	s.chainmu.RLock()
   487  	defer s.chainmu.RUnlock()
   488  
   489  	return s.verifySignedHeader(head)
   490  }
   491  
   492  func (s *CommitteeChain) verifySignedHeader(head types.SignedHeader) (bool, time.Duration, error) {
   493  	var age time.Duration
   494  	now := s.unixNano()
   495  	if head.Header.Slot < (uint64(now-math.MinInt64)/uint64(time.Second)-s.config.GenesisTime)/12 {
   496  		age = time.Duration(now - int64(time.Second)*int64(s.config.GenesisTime+head.Header.Slot*12))
   497  	} else {
   498  		age = time.Duration(math.MinInt64)
   499  	}
   500  	if s.enforceTime && age < 0 {
   501  		return false, age, nil
   502  	}
   503  	committee, err := s.getSyncCommittee(types.SyncPeriod(head.SignatureSlot))
   504  	if err != nil {
   505  		return false, 0, err
   506  	}
   507  	if committee == nil {
   508  		return false, age, nil
   509  	}
   510  	if signingRoot, err := s.config.Forks.SigningRoot(head.Header); err == nil {
   511  		return s.sigVerifier.verifySignature(committee, signingRoot, &head.Signature), age, nil
   512  	}
   513  	return false, age, nil
   514  }
   515  
   516  // verifyUpdate checks whether the header signature is correct and the update
   517  // fits into the specified constraints (assumes that the update has been
   518  // successfully validated previously)
   519  func (s *CommitteeChain) verifyUpdate(update *types.LightClientUpdate) (bool, error) {
   520  	// Note: SignatureSlot determines the sync period of the committee used for signature
   521  	// verification. Though in reality SignatureSlot is always bigger than update.Header.Slot,
   522  	// setting them as equal here enforces the rule that they have to be in the same sync
   523  	// period in order for the light client update proof to be meaningful.
   524  	ok, age, err := s.verifySignedHeader(update.AttestedHeader)
   525  	if age < 0 {
   526  		log.Warn("Future committee update received", "age", age)
   527  	}
   528  	return ok, err
   529  }