diff --git a/beacon-chain/lightclient/BUILD.bazel b/beacon-chain/lightclient/BUILD.bazel new file mode 100644 index 000000000000..9e294e00c099 --- /dev/null +++ b/beacon-chain/lightclient/BUILD.bazel @@ -0,0 +1,13 @@ +load("@prysm//tools/go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = [ + "config.go", + "helpers.go", + "store.go", + "update.go", + ], + importpath = "github.com/prysmaticlabs/prysm/v4/beacon-chain/lightclient", + visibility = ["//visibility:public"], +) diff --git a/beacon-chain/lightclient/config.go b/beacon-chain/lightclient/config.go new file mode 100644 index 000000000000..8b17bfdfed26 --- /dev/null +++ b/beacon-chain/lightclient/config.go @@ -0,0 +1,147 @@ +package lightclient + +import ( + "encoding/json" + "strconv" + + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/prysmaticlabs/prysm/v4/config/params" + types "github.com/prysmaticlabs/prysm/v4/consensus-types/primitives" + "github.com/prysmaticlabs/prysm/v4/encoding/bytesutil" +) + +// ConfigJSON is the JSON representation of the light client config. +type ConfigJSON struct { + CapellaForkEpoch string `json:"capella_fork_epoch"` + CapellaForkVersion string `json:"capella_fork_version" hex:"true"` + BellatrixForkEpoch string `json:"bellatrix_fork_epoch"` + BellatrixForkVersion string `json:"bellatrix_fork_version" hex:"true"` + AltairForkEpoch string `json:"altair_fork_epoch"` + AltairForkVersion string `json:"altair_fork_version" hex:"true"` + GenesisForkVersion string `json:"genesis_fork_version" hex:"true"` + MinSyncCommitteeParticipants string `json:"min_sync_committee_participants"` + GenesisSlot string `json:"genesis_slot"` + DomainSyncCommittee string `json:"domain_sync_committee" hex:"true"` + SlotsPerEpoch string `json:"slots_per_epoch"` + EpochsPerSyncCommitteePeriod string `json:"epochs_per_sync_committee_period"` + SecondsPerSlot string `json:"seconds_per_slot"` +} + +// Config is the light client configuration. It consists of the subset of the beacon chain configuration relevant to the +// light client. Unlike the beacon chain configuration it is serializable to JSON, hence it's a separate object. +type Config struct { + CapellaForkEpoch types.Epoch + CapellaForkVersion []byte + BellatrixForkEpoch types.Epoch + BellatrixForkVersion []byte + AltairForkEpoch types.Epoch + AltairForkVersion []byte + GenesisForkVersion []byte + MinSyncCommitteeParticipants uint64 + GenesisSlot types.Slot + DomainSyncCommittee [4]byte + SlotsPerEpoch types.Slot + EpochsPerSyncCommitteePeriod types.Epoch + SecondsPerSlot uint64 +} + +// NewConfig creates a new light client configuration from a beacon chain configuration. +func NewConfig(chainConfig *params.BeaconChainConfig) *Config { + return &Config{ + CapellaForkEpoch: chainConfig.CapellaForkEpoch, + CapellaForkVersion: chainConfig.CapellaForkVersion, + BellatrixForkEpoch: chainConfig.BellatrixForkEpoch, + BellatrixForkVersion: chainConfig.BellatrixForkVersion, + AltairForkEpoch: chainConfig.AltairForkEpoch, + AltairForkVersion: chainConfig.AltairForkVersion, + GenesisForkVersion: chainConfig.GenesisForkVersion, + MinSyncCommitteeParticipants: chainConfig.MinSyncCommitteeParticipants, + GenesisSlot: chainConfig.GenesisSlot, + DomainSyncCommittee: chainConfig.DomainSyncCommittee, + SlotsPerEpoch: chainConfig.SlotsPerEpoch, + EpochsPerSyncCommitteePeriod: chainConfig.EpochsPerSyncCommitteePeriod, + SecondsPerSlot: chainConfig.SecondsPerSlot, + } +} + +func (c *Config) MarshalJSON() ([]byte, error) { + return json.Marshal(&ConfigJSON{ + CapellaForkEpoch: strconv.FormatUint(uint64(c.CapellaForkEpoch), 10), + CapellaForkVersion: hexutil.Encode(c.CapellaForkVersion), + BellatrixForkEpoch: strconv.FormatUint(uint64(c.BellatrixForkEpoch), 10), + BellatrixForkVersion: hexutil.Encode(c.BellatrixForkVersion), + AltairForkEpoch: strconv.FormatUint(uint64(c.AltairForkEpoch), 10), + AltairForkVersion: hexutil.Encode(c.AltairForkVersion), + GenesisForkVersion: hexutil.Encode(c.GenesisForkVersion), + MinSyncCommitteeParticipants: strconv.FormatUint(c.MinSyncCommitteeParticipants, 10), + GenesisSlot: strconv.FormatUint(uint64(c.GenesisSlot), 10), + DomainSyncCommittee: hexutil.Encode(c.DomainSyncCommittee[:]), + SlotsPerEpoch: strconv.FormatUint(uint64(c.SlotsPerEpoch), 10), + EpochsPerSyncCommitteePeriod: strconv.FormatUint(uint64(c.EpochsPerSyncCommitteePeriod), 10), + SecondsPerSlot: strconv.FormatUint(c.SecondsPerSlot, 10), + }) +} + +func (c *Config) UnmarshalJSON(input []byte) error { + var configJSON ConfigJSON + if err := json.Unmarshal(input, &configJSON); err != nil { + return err + } + var config Config + capellaForkEpoch, err := strconv.ParseUint(configJSON.CapellaForkEpoch, 10, 64) + if err != nil { + return err + } + config.CapellaForkEpoch = types.Epoch(capellaForkEpoch) + if config.CapellaForkVersion, err = hexutil.Decode(configJSON.CapellaForkVersion); err != nil { + return err + } + bellatrixForkEpoch, err := strconv.ParseUint(configJSON.BellatrixForkEpoch, 10, 64) + if err != nil { + return err + } + config.BellatrixForkEpoch = types.Epoch(bellatrixForkEpoch) + if config.BellatrixForkVersion, err = hexutil.Decode(configJSON.BellatrixForkVersion); err != nil { + return err + } + altairForkEpoch, err := strconv.ParseUint(configJSON.AltairForkEpoch, 10, 64) + if err != nil { + return err + } + config.AltairForkEpoch = types.Epoch(altairForkEpoch) + if config.AltairForkVersion, err = hexutil.Decode(configJSON.AltairForkVersion); err != nil { + return err + } + if config.GenesisForkVersion, err = hexutil.Decode(configJSON.GenesisForkVersion); err != nil { + return err + } + if config.MinSyncCommitteeParticipants, err = strconv.ParseUint(configJSON.MinSyncCommitteeParticipants, 10, + 64); err != nil { + return err + } + genesisSlot, err := strconv.ParseUint(configJSON.GenesisSlot, 10, 64) + if err != nil { + return err + } + config.GenesisSlot = types.Slot(genesisSlot) + domainSyncCommittee, err := hexutil.Decode(configJSON.DomainSyncCommittee) + if err != nil { + return err + } + config.DomainSyncCommittee = bytesutil.ToBytes4(domainSyncCommittee) + slotsPerEpoch, err := strconv.ParseUint(configJSON.SlotsPerEpoch, 10, 64) + if err != nil { + return err + } + config.SlotsPerEpoch = types.Slot(slotsPerEpoch) + epochsPerSyncCommitteePeriod, err := strconv.ParseUint(configJSON.EpochsPerSyncCommitteePeriod, 10, 64) + if err != nil { + return err + } + config.EpochsPerSyncCommitteePeriod = types.Epoch(epochsPerSyncCommitteePeriod) + if config.SecondsPerSlot, err = strconv.ParseUint(configJSON.SecondsPerSlot, 10, 64); err != nil { + return err + } + *c = config + return nil +} diff --git a/beacon-chain/lightclient/helpers.go b/beacon-chain/lightclient/helpers.go new file mode 100644 index 000000000000..b1573c377652 --- /dev/null +++ b/beacon-chain/lightclient/helpers.go @@ -0,0 +1,20 @@ +package lightclient + +import ( + types "github.com/prysmaticlabs/prysm/v4/consensus-types/primitives" +) + +// computeEpochAtSlot implements compute_epoch_at_slot from the spec. +func computeEpochAtSlot(config *Config, slot types.Slot) types.Epoch { + return types.Epoch(slot / config.SlotsPerEpoch) +} + +// computeSyncCommitteePeriod implements compute_sync_committee_period from the spec. +func computeSyncCommitteePeriod(config *Config, epoch types.Epoch) uint64 { + return uint64(epoch / config.EpochsPerSyncCommitteePeriod) +} + +// computeSyncCommitteePeriodAtSlot implements compute_sync_committee_period_at_slot from the spec. +func computeSyncCommitteePeriodAtSlot(config *Config, slot types.Slot) uint64 { + return computeSyncCommitteePeriod(config, computeEpochAtSlot(config, slot)) +} diff --git a/beacon-chain/lightclient/store.go b/beacon-chain/lightclient/store.go new file mode 100644 index 000000000000..754ac76a1784 --- /dev/null +++ b/beacon-chain/lightclient/store.go @@ -0,0 +1,426 @@ +// Package lightclient implements the light client for the Ethereum 2.0 Beacon Chain. +// It is based on the Altair light client spec at this revision: +// https://github.com/ethereum/consensus-specs/tree/208da34ac4e75337baf79adebf036ab595e39f15/specs/altair/light-client +package lightclient + +import ( + "encoding/json" + "errors" + "fmt" + + ethrpc "github.com/prysmaticlabs/prysm/v4/beacon-chain/rpc/apimiddleware" + "github.com/prysmaticlabs/prysm/v4/beacon-chain/rpc/apimiddleware/helpers" + "github.com/prysmaticlabs/prysm/v4/beacon-chain/rpc/eth/helpers/lightclient" + + "github.com/prysmaticlabs/prysm/v4/beacon-chain/core/signing" + types "github.com/prysmaticlabs/prysm/v4/consensus-types/primitives" + "github.com/prysmaticlabs/prysm/v4/container/trie" + "github.com/prysmaticlabs/prysm/v4/crypto/bls/blst" + "github.com/prysmaticlabs/prysm/v4/crypto/bls/common" + ethpbv1 "github.com/prysmaticlabs/prysm/v4/proto/eth/v1" + ethpbv2 "github.com/prysmaticlabs/prysm/v4/proto/eth/v2" +) + +const ( + currentSyncCommitteeIndex = uint64(54) +) + +// Store implements LightClientStore from the spec. +type Store struct { + Config *Config `json:"config,omitempty"` + // FinalizedHeader is a header that is finalized + FinalizedHeader *ethpbv1.BeaconBlockHeader `json:"finalized_header,omitempty"` + // CurrentSyncCommittee is the sync committees corresponding to the finalized header + CurrentSyncCommittee *ethpbv2.SyncCommittee `json:"current_sync_committeeu,omitempty"` + // NextSyncCommittee is the next sync committees corresponding to the finalized header + NextSyncCommittee *ethpbv2.SyncCommittee `json:"next_sync_committee,omitempty"` + // BestValidUpdate is the best available header to switch finalized head to if we see nothing else + BestValidUpdate *ethpbv2.LightClientUpdate `json:"best_valid_update,omitempty"` + // OptimisticHeader is the most recent available reasonably-safe header + OptimisticHeader *ethpbv1.BeaconBlockHeader `json:"optimistic_header,omitempty"` + // PreviousMaxActiveParticipants is the previous max number of active participants in a sync committee (used to + // calculate safety threshold) + PreviousMaxActiveParticipants uint64 `json:"previous_max_active_participants,omitempty"` + // CurrentMaxActiveParticipants is the max number of active participants in a sync committee (used to calculate + // safety threshold) + CurrentMaxActiveParticipants uint64 `json:"current_max_active_participants,omitempty"` +} + +// UnmarshalUpdateFromJSON allows to go from a strictly typed JSON update to a generic light client update. It can be used by +// callers to propagate updates of different types over a single endpoint, and receivers to process it as a generic +// update (e.g., by calling Store.ProcessUpdate()). +func UnmarshalUpdateFromJSON(typedUpdate *ethrpc.TypedLightClientUpdateJson) (*ethpbv2.LightClientUpdate, error) { + var update *ethpbv2.LightClientUpdate + switch typedUpdate.TypeName { + case ethrpc.LightClientUpdateTypeName: + var fullUpdate ethrpc.LightClientUpdateJson + err := json.Unmarshal([]byte(typedUpdate.Data), &fullUpdate) + if err != nil { + return nil, err + } + update, err = helpers.NewLightClientUpdateFromJSON(&fullUpdate) + if err != nil { + return nil, err + } + case ethrpc.LightClientFinalityUpdateTypeName: + var finalityUpdate ethrpc.LightClientFinalityUpdateJson + err := json.Unmarshal([]byte(typedUpdate.Data), &finalityUpdate) + if err != nil { + return nil, err + } + update, err = helpers.NewLightClientUpdateFromFinalityUpdateJSON(&finalityUpdate) + if err != nil { + return nil, err + } + case ethrpc.LightClientOptimisticUpdateTypeName: + var optimisticUpdate ethrpc.LightClientOptimisticUpdateJson + err := json.Unmarshal([]byte(typedUpdate.Data), &optimisticUpdate) + if err != nil { + return nil, err + } + update, err = helpers.NewLightClientUpdateFromOptimisticUpdateJSON(&optimisticUpdate) + if err != nil { + return nil, err + } + default: + return nil, fmt.Errorf("unknown update type %q", typedUpdate.TypeName) + } + return update, nil +} + +// getSubtreeIndex implements get_subtree_index from the spec. +func getSubtreeIndex(index uint64) uint64 { + return index % (uint64(1) << ethpbv2.FloorLog2(index-1)) +} + +// NewStore implements initialize_light_client_store from the spec. +func NewStore(config *Config, trustedBlockRoot [32]byte, + bootstrap *ethpbv2.LightClientBootstrap) (*Store, error) { + if config == nil { + return nil, errors.New("light client config cannot be nil") + } + if bootstrap.Header == nil || bootstrap.CurrentSyncCommittee == nil { + return nil, errors.New("malformed bootstrap") + } + bootstrapRoot, err := bootstrap.Header.HashTreeRoot() + if err != nil { + panic(err) + } + if trustedBlockRoot != bootstrapRoot { + return nil, errors.New("trusted block root does not match bootstrap header") + } + root, err := bootstrap.CurrentSyncCommittee.HashTreeRoot() + if err != nil { + return nil, err + } + if !trie.VerifyMerkleProof( + bootstrap.Header.StateRoot, + root[:], + getSubtreeIndex(currentSyncCommitteeIndex), + bootstrap.CurrentSyncCommitteeBranch) { + return nil, errors.New("current sync committee merkle proof is invalid") + } + return &Store{ + Config: config, + FinalizedHeader: bootstrap.Header, + CurrentSyncCommittee: bootstrap.CurrentSyncCommittee, + NextSyncCommittee: nil, + OptimisticHeader: bootstrap.Header, + }, nil +} + +func (s *Store) Clone() *Store { + return &Store{ + Config: s.Config, + FinalizedHeader: s.FinalizedHeader, + CurrentSyncCommittee: s.CurrentSyncCommittee, + NextSyncCommittee: s.NextSyncCommittee, + BestValidUpdate: s.BestValidUpdate, + OptimisticHeader: s.OptimisticHeader, + PreviousMaxActiveParticipants: s.PreviousMaxActiveParticipants, + CurrentMaxActiveParticipants: s.CurrentMaxActiveParticipants, + } +} + +// isNextSyncCommitteeKnown implements is_next_sync_committee_known from the spec. +func (s *Store) isNextSyncCommitteeKnown() bool { + return s.NextSyncCommittee != nil +} + +func max(a, b uint64) uint64 { + if a > b { + return a + } + return b +} + +// getSafetyThreshold implements get_safety_threshold from the spec. +func (s *Store) getSafetyThreshold() uint64 { + return max(s.PreviousMaxActiveParticipants, s.CurrentMaxActiveParticipants) / 2 +} + +// computeForkVersion implements compute_fork_version from the spec. +func (s *Store) computeForkVersion(epoch types.Epoch) []byte { + if epoch >= s.Config.CapellaForkEpoch { + return s.Config.CapellaForkVersion + } + if epoch >= s.Config.BellatrixForkEpoch { + return s.Config.BellatrixForkVersion + } + if epoch >= s.Config.AltairForkEpoch { + return s.Config.AltairForkVersion + } + return s.Config.GenesisForkVersion +} + +// validateWrappedUpdate implements validate_light_client_update from the spec. +func (s *Store) validateWrappedUpdate(update *update, currentSlot types.Slot, genesisValidatorsRoot []byte) error { + // Verify sync committee has sufficient participants + syncAggregate := update.SyncAggregate + if syncAggregate == nil || syncAggregate.SyncCommitteeBits == nil { + return errors.New("sync aggregate in update is invalid") + } + if syncAggregate.SyncCommitteeBits.Count() < s.Config.MinSyncCommitteeParticipants { + return errors.New("sync committee does not have sufficient participants") + } + + if update.AttestedHeader == nil { + return errors.New("attested header in update is not set") + } + // Verify update does not skip a sync committee period + if !(currentSlot >= update.SignatureSlot && + update.SignatureSlot > update.AttestedHeader.Slot && + (update.FinalizedHeader == nil || update.AttestedHeader.Slot >= update.FinalizedHeader.Slot)) { + return errors.New("update skips a sync committee period") + } + storePeriod := computeSyncCommitteePeriodAtSlot(s.Config, s.FinalizedHeader.Slot) + updateSignaturePeriod := computeSyncCommitteePeriodAtSlot(s.Config, update.SignatureSlot) + if s.isNextSyncCommitteeKnown() { + if !(updateSignaturePeriod == storePeriod || updateSignaturePeriod == storePeriod+1) { + return errors.New("update skips a sync committee period") + } + } else { + if updateSignaturePeriod != storePeriod { + return errors.New("update skips a sync committee period") + } + } + + // Verify update is relevant + updateAttestedPeriod := computeSyncCommitteePeriodAtSlot(s.Config, update.AttestedHeader.Slot) + updateHasNextSyncCommittee := !s.isNextSyncCommitteeKnown() && (update.IsSyncCommiteeUpdate() && updateAttestedPeriod == storePeriod) + if !(update.AttestedHeader.Slot > s.FinalizedHeader.Slot || updateHasNextSyncCommittee) { + return errors.New("update is not relevant") + } + + // Verify that the finality branch, if present, confirms finalized header to match the finalized checkpoint root + // saved in the state of attested header. Note that the genesis finalized checkpoint root is represented as a zero + // hash. + if !update.IsFinalityUpdate() { + if update.FinalizedHeader != nil { + return errors.New("finality branch is present but update is not finality") + } + } else { + var finalizedRoot [32]byte + if update.FinalizedHeader.Slot == s.Config.GenesisSlot { + if update.FinalizedHeader.String() != (ðpbv1.BeaconBlockHeader{}).String() { + return errors.New("genesis finalized checkpoint root is not represented as a zero hash") + } + finalizedRoot = [32]byte{} + } else { + var err error + if finalizedRoot, err = update.FinalizedHeader.HashTreeRoot(); err != nil { + return err + } + } + if !trie.VerifyMerkleProof( + update.AttestedHeader.StateRoot, + finalizedRoot[:], + getSubtreeIndex(ethpbv2.FinalizedRootIndex), + update.FinalityBranch) { + return errors.New("finality branch is invalid") + } + } + + // Verify that the next sync committee, if present, actually is the next sync committee saved in the state of the + // attested header + if !update.IsSyncCommiteeUpdate() { + if update.NextSyncCommittee != nil { + return errors.New("sync committee branch is present but update is not sync committee") + } + } else { + if updateAttestedPeriod == storePeriod && s.isNextSyncCommitteeKnown() { + if !update.NextSyncCommittee.Equals(s.NextSyncCommittee) { + return errors.New("next sync committee is not known") + } + } + root, err := update.NextSyncCommittee.HashTreeRoot() + if err != nil { + return err + } + if !trie.VerifyMerkleProof( + update.AttestedHeader.StateRoot, + root[:], + getSubtreeIndex(ethpbv2.NextSyncCommitteeIndex), + update.NextSyncCommitteeBranch) { + return errors.New("sync committee branch is invalid") + } + } + + var syncCommittee *ethpbv2.SyncCommittee + // Verify sync committee aggregate signature + if updateSignaturePeriod == storePeriod { + syncCommittee = s.CurrentSyncCommittee + } else { + syncCommittee = s.NextSyncCommittee + } + var participantPubkeys []common.PublicKey + for i := uint64(0); i < syncAggregate.SyncCommitteeBits.Len(); i++ { + bit := syncAggregate.SyncCommitteeBits.BitAt(i) + if bit { + publicKey, err := blst.PublicKeyFromBytes(syncCommittee.Pubkeys[i]) + if err != nil { + return err + } + participantPubkeys = append(participantPubkeys, publicKey) + } + } + forkVersion := s.computeForkVersion(computeEpochAtSlot(s.Config, update.SignatureSlot)) + domain, err := signing.ComputeDomain(s.Config.DomainSyncCommittee, forkVersion, genesisValidatorsRoot) + if err != nil { + return err + } + signingRoot, err := signing.ComputeSigningRoot(update.AttestedHeader, domain) + if err != nil { + return err + } + signature, err := blst.SignatureFromBytes(syncAggregate.SyncCommitteeSignature) + if err != nil { + return err + } + if !signature.FastAggregateVerify(participantPubkeys, signingRoot) { + return errors.New("sync committee signature is invalid") + } + + return nil +} + +// applyUpdate implements apply_light_client_update from the spec. +func (s *Store) applyUpdate(update *ethpbv2.LightClientUpdate) error { + storePeriod := computeSyncCommitteePeriodAtSlot(s.Config, s.FinalizedHeader.Slot) + updateFinalizedPeriod := computeSyncCommitteePeriodAtSlot(s.Config, update.FinalizedHeader.Slot) + if !s.isNextSyncCommitteeKnown() { + if updateFinalizedPeriod != storePeriod { + return errors.New("update finalized period does not match store period") + } + s.NextSyncCommittee = update.NextSyncCommittee + } else if updateFinalizedPeriod == storePeriod+1 { + s.CurrentSyncCommittee = s.NextSyncCommittee + s.NextSyncCommittee = update.NextSyncCommittee + s.PreviousMaxActiveParticipants = s.CurrentMaxActiveParticipants + s.CurrentMaxActiveParticipants = 0 + } + if update.FinalizedHeader.Slot > s.FinalizedHeader.Slot { + s.FinalizedHeader = update.FinalizedHeader + if s.FinalizedHeader.Slot > s.OptimisticHeader.Slot { + s.OptimisticHeader = s.FinalizedHeader + } + } + return nil +} + +// ProcessForceUpdate implements process_light_client_store_force_update from the spec. +func (s *Store) ProcessForceUpdate(currentSlot types.Slot) error { + if currentSlot > s.FinalizedHeader.Slot+s.Config.SlotsPerEpoch+types.Slot(s.Config.EpochsPerSyncCommitteePeriod) && + s.BestValidUpdate != nil { + // Forced best update when the update timeout has elapsed. + // Because the apply logic waits for `finalized_header.slot` to indicate sync committee finality, + // the `attested_header` may be treated as `finalized_header` in extended periods of non-finality + // to guarantee progression into later sync committee periods according to `is_better_update`. + if s.BestValidUpdate.FinalizedHeader.Slot <= s.FinalizedHeader.Slot { + s.BestValidUpdate.FinalizedHeader = s.BestValidUpdate.AttestedHeader + } + if err := s.applyUpdate(s.BestValidUpdate); err != nil { + return err + } + s.BestValidUpdate = nil + } + return nil +} + +// ValidateUpdate provides a wrapper around validateUpdate() for callers that want to separate validate and apply. +func (s *Store) ValidateUpdate(lightClientUpdate *ethpbv2.LightClientUpdate, + currentSlot types.Slot, genesisValidatorsRoot []byte) error { + update := &update{ + LightClientUpdate: lightClientUpdate, + config: s.Config, + } + return s.validateWrappedUpdate(update, currentSlot, genesisValidatorsRoot) +} + +func (s *Store) maybeValidateAndProcessUpdate(lightClientUpdate *ethpbv2.LightClientUpdate, + currentSlot types.Slot, genesisValidatorsRoot []byte, validated bool) error { + update := &update{ + LightClientUpdate: lightClientUpdate, + config: s.Config, + } + if !validated { + if err := s.validateWrappedUpdate(update, currentSlot, genesisValidatorsRoot); err != nil { + return err + } + } + syncCommiteeBits := update.SyncAggregate.SyncCommitteeBits + + // Update the best update in case we have to force-update to it if the timeout elapses + if s.BestValidUpdate == nil || update.isBetterUpdate(s.BestValidUpdate) { + s.BestValidUpdate = update.LightClientUpdate + } + + // Track the maximum number of active participants in the committee signature + s.CurrentMaxActiveParticipants = max(s.CurrentMaxActiveParticipants, syncCommiteeBits.Count()) + + // Update the optimistic header + if syncCommiteeBits.Count() > s.getSafetyThreshold() && update.AttestedHeader.Slot > s.OptimisticHeader.Slot { + s.OptimisticHeader = update.AttestedHeader + } + + // Update finalized header + updateHasFinalizedNextSyncCommittee := !s.isNextSyncCommitteeKnown() && update.IsSyncCommiteeUpdate() && + update.IsFinalityUpdate() && computeSyncCommitteePeriodAtSlot(s.Config, update.FinalizedHeader. + Slot) == computeSyncCommitteePeriodAtSlot(s.Config, update.AttestedHeader.Slot) + if syncCommiteeBits.Count()*3 >= syncCommiteeBits.Len()*2 && + ((update.FinalizedHeader != nil && update.FinalizedHeader.Slot > s.FinalizedHeader.Slot) || + updateHasFinalizedNextSyncCommittee) { + // Normal update through 2/3 threshold + if err := s.applyUpdate(update.LightClientUpdate); err != nil { + return err + } + s.BestValidUpdate = nil + } + return nil +} + +// ProcessUpdate implements process_light_client_update from the spec. +func (s *Store) ProcessUpdate(lightClientUpdate *ethpbv2.LightClientUpdate, + currentSlot types.Slot, genesisValidatorsRoot []byte) error { + return s.maybeValidateAndProcessUpdate(lightClientUpdate, currentSlot, genesisValidatorsRoot, false) +} + +// ProcessValidatedUpdate processes a pre-validated update. +func (s *Store) ProcessValidatedUpdate(lightClientUpdate *ethpbv2.LightClientUpdate, + currentSlot types.Slot, genesisValidatorsRoot []byte) error { + return s.maybeValidateAndProcessUpdate(lightClientUpdate, currentSlot, genesisValidatorsRoot, true) +} + +// ProcessFinalityUpdate implements process_light_client_finality_update from the spec. +func (s *Store) ProcessFinalityUpdate(update *ethpbv2.LightClientFinalityUpdate, currentSlot types.Slot, + genesisValidatorsRoot []byte) error { + return s.ProcessUpdate(lightclient.NewLightClientUpdateFromFinalityUpdate(update), currentSlot, + genesisValidatorsRoot) +} + +// ProcessOptimisticUpdate implements process_light_client_optimistic_update from the spec. +func (s *Store) ProcessOptimisticUpdate(update *ethpbv2.LightClientOptimisticUpdate, currentSlot types.Slot, + genesisValidatorsRoot []byte) error { + return s.ProcessUpdate(lightclient.NewLightClientUpdateFromOptimisticUpdate(update), currentSlot, + genesisValidatorsRoot) +} diff --git a/beacon-chain/lightclient/update.go b/beacon-chain/lightclient/update.go new file mode 100644 index 000000000000..c71fd5ca848e --- /dev/null +++ b/beacon-chain/lightclient/update.go @@ -0,0 +1,78 @@ +package lightclient + +import ( + ethpbv2 "github.com/prysmaticlabs/prysm/v4/proto/eth/v2" +) + +// update is a convenience wrapper for a LightClientUpdate to feed config parameters into misc utils. +type update struct { + config *Config + *ethpbv2.LightClientUpdate +} + +// hasRelevantSyncCommittee implements has_relevant_sync_committee from the spec. +func (u *update) hasRelevantSyncCommittee() bool { + return u.IsSyncCommiteeUpdate() && + computeSyncCommitteePeriodAtSlot(u.config, u.AttestedHeader.Slot) == + computeSyncCommitteePeriodAtSlot(u.config, u.SignatureSlot) +} + +// hasSyncCommitteeFinality implements has_sync_committee_finality from the spec. +func (u *update) hasSyncCommitteeFinality() bool { + return computeSyncCommitteePeriodAtSlot(u.config, u.FinalizedHeader.Slot) == + computeSyncCommitteePeriodAtSlot(u.config, u.AttestedHeader.Slot) +} + +// isBetterUpdate implements is_better_update from the spec. +func (u *update) isBetterUpdate(newUpdatePb *ethpbv2.LightClientUpdate) bool { + newUpdate := &update{ + config: u.config, + LightClientUpdate: newUpdatePb, + } + // Compare supermajority (> 2/3) sync committee participation + maxActiveParticipants := newUpdate.SyncAggregate.SyncCommitteeBits.Len() + newNumActiveParticipants := newUpdate.SyncAggregate.SyncCommitteeBits.Count() + oldNumActiveParticipants := u.SyncAggregate.SyncCommitteeBits.Count() + newHasSupermajority := newNumActiveParticipants*3 >= maxActiveParticipants*2 + oldHasSupermajority := oldNumActiveParticipants*3 >= maxActiveParticipants*2 + if newHasSupermajority != oldHasSupermajority { + return newHasSupermajority && !oldHasSupermajority + } + if !newHasSupermajority && newNumActiveParticipants != oldNumActiveParticipants { + return newNumActiveParticipants > oldNumActiveParticipants + } + + // Compare presence of relevant sync committee + newHasRelevantSyncCommittee := newUpdate.hasRelevantSyncCommittee() + oldHasRelevantSyncCommittee := u.hasRelevantSyncCommittee() + if newHasRelevantSyncCommittee != oldHasRelevantSyncCommittee { + return newHasRelevantSyncCommittee + } + + // Compare indication of any finality + newHasFinality := newUpdate.IsFinalityUpdate() + oldHasFinality := u.IsFinalityUpdate() + if newHasFinality != oldHasFinality { + return newHasFinality + } + + // Compare sync committee finality + if newHasFinality { + newHasSyncCommitteeFinality := newUpdate.hasSyncCommitteeFinality() + oldHasSyncCommitteeFinality := u.hasSyncCommitteeFinality() + if newHasSyncCommitteeFinality != oldHasSyncCommitteeFinality { + return newHasSyncCommitteeFinality + } + } + + // Tiebreaker 1: Sync committee participation beyond supermajority + if newNumActiveParticipants != oldNumActiveParticipants { + return newNumActiveParticipants > oldNumActiveParticipants + } + + // Tiebreaker 2: Prefer older data (fewer changes to best) + if newUpdate.AttestedHeader.Slot != u.AttestedHeader.Slot { + return newUpdate.AttestedHeader.Slot < u.AttestedHeader.Slot + } + return newUpdate.SignatureSlot < u.SignatureSlot +} diff --git a/proto/eth/v2/custom.go b/proto/eth/v2/custom.go new file mode 100644 index 000000000000..86132bd84e2b --- /dev/null +++ b/proto/eth/v2/custom.go @@ -0,0 +1,51 @@ +package eth + +import ( + "bytes" + "math/bits" +) + +const ( + NextSyncCommitteeIndex = uint64(55) + FinalizedRootIndex = uint64(105) +) + +func (x *SyncCommittee) Equals(other *SyncCommittee) bool { + if len(x.Pubkeys) != len(other.Pubkeys) { + return false + } + for i := range x.Pubkeys { + if !bytes.Equal(x.Pubkeys[i], other.Pubkeys[i]) { + return false + } + } + return bytes.Equal(x.AggregatePubkey, other.AggregatePubkey) +} + +func FloorLog2(x uint64) int { + return bits.Len64(uint64(x - 1)) +} + +func isEmptyWithLength(bb [][]byte, length uint64) bool { + if len(bb) == 0 { + return true + } + l := FloorLog2(length) + if len(bb) != l { + return false + } + for _, b := range bb { + if !bytes.Equal(b, []byte{}) { + return false + } + } + return true +} + +func (x *LightClientUpdate) IsSyncCommiteeUpdate() bool { + return !isEmptyWithLength(x.GetNextSyncCommitteeBranch(), NextSyncCommitteeIndex) +} + +func (x *LightClientUpdate) IsFinalityUpdate() bool { + return !isEmptyWithLength(x.GetFinalityBranch(), FinalizedRootIndex) +}