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