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 }