diff --git a/api/server/structs/conversions_lightclient.go b/api/server/structs/conversions_lightclient.go index 2fc3d70e0c22..4566ff69d7e3 100644 --- a/api/server/structs/conversions_lightclient.go +++ b/api/server/structs/conversions_lightclient.go @@ -11,6 +11,40 @@ import ( "github.com/prysmaticlabs/prysm/v5/runtime/version" ) +func LightClientBootsrapFromConsensus(bootstrap interfaces.LightClientBootstrap) (*LightClientBootstrap, error) { + header, err := lightClientHeaderToJSON(bootstrap.Header()) + if err != nil { + return nil, errors.Wrap(err, "could not marshal light client header") + } + + var jsonBranch []string + if bootstrap.Version() >= version.Electra { + branch, err := bootstrap.CurrentSyncCommitteeBranchElectra() + if err != nil { + return nil, err + } + jsonBranch = make([]string, len(branch)) + for i, item := range branch { + jsonBranch[i] = hexutil.Encode(item[:]) + } + } else { + branch, err := bootstrap.CurrentSyncCommitteeBranch() + if err != nil { + return nil, err + } + jsonBranch = make([]string, len(branch)) + for i, item := range branch { + jsonBranch[i] = hexutil.Encode(item[:]) + } + } + + return &LightClientBootstrap{ + Header: header, + CurrentSyncCommittee: SyncCommitteeFromConsensus(bootstrap.CurrentSyncCommittee()), + CurrentSyncCommitteeBranch: jsonBranch, + }, nil +} + func LightClientUpdateFromConsensus(update interfaces.LightClientUpdate) (*LightClientUpdate, error) { attestedHeader, err := lightClientHeaderToJSON(update.AttestedHeader()) if err != nil { @@ -171,9 +205,59 @@ func lightClientHeaderToJSON(header interfaces.LightClientHeader) (json.RawMessa Execution: execution, ExecutionBranch: branchToJSON(executionBranch[:]), } + case version.Electra: + exInterface, err := header.Execution() + if err != nil { + return nil, err + } + ex, ok := exInterface.Proto().(*enginev1.ExecutionPayloadHeaderDeneb) + if !ok { + return nil, fmt.Errorf("execution data is not %T", &enginev1.ExecutionPayloadHeaderDeneb{}) + } + execution, err := ExecutionPayloadHeaderDenebFromConsensus(ex) + if err != nil { + return nil, err + } + executionBranch, err := header.ExecutionBranch() + if err != nil { + return nil, err + } + result = &LightClientHeaderDeneb{ + Beacon: BeaconBlockHeaderFromConsensus(header.Beacon()), + Execution: execution, + ExecutionBranch: branchToJSON(executionBranch[:]), + } default: return nil, fmt.Errorf("unsupported header version %s", version.String(v)) } return json.Marshal(result) } + +func LightClientBootstrapFromConsensus(bootstrap interfaces.LightClientBootstrap) (*LightClientBootstrap, error) { + header, err := lightClientHeaderToJSON(bootstrap.Header()) + if err != nil { + return nil, errors.Wrap(err, "could not marshal light client header") + } + + var scBranch [][32]byte + if bootstrap.Version() >= version.Electra { + b, err := bootstrap.CurrentSyncCommitteeBranchElectra() + if err != nil { + return nil, err + } + scBranch = b[:] + } else { + b, err := bootstrap.CurrentSyncCommitteeBranch() + if err != nil { + return nil, err + } + scBranch = b[:] + } + + return &LightClientBootstrap{ + Header: header, + CurrentSyncCommittee: SyncCommitteeFromConsensus(bootstrap.CurrentSyncCommittee()), + CurrentSyncCommitteeBranch: branchToJSON(scBranch), + }, nil +} diff --git a/beacon-chain/blockchain/BUILD.bazel b/beacon-chain/blockchain/BUILD.bazel index 2dc6f11a87f9..bb7d4c728d62 100644 --- a/beacon-chain/blockchain/BUILD.bazel +++ b/beacon-chain/blockchain/BUILD.bazel @@ -59,6 +59,7 @@ go_library( "//beacon-chain/forkchoice:go_default_library", "//beacon-chain/forkchoice/doubly-linked-tree:go_default_library", "//beacon-chain/forkchoice/types:go_default_library", + "//beacon-chain/light-client:go_default_library", "//beacon-chain/operations/attestations:go_default_library", "//beacon-chain/operations/blstoexec:go_default_library", "//beacon-chain/operations/slashings:go_default_library", diff --git a/beacon-chain/blockchain/options.go b/beacon-chain/blockchain/options.go index 38492502a1f9..62c6115562b3 100644 --- a/beacon-chain/blockchain/options.go +++ b/beacon-chain/blockchain/options.go @@ -8,6 +8,7 @@ import ( "github.com/prysmaticlabs/prysm/v5/beacon-chain/db/filesystem" "github.com/prysmaticlabs/prysm/v5/beacon-chain/execution" "github.com/prysmaticlabs/prysm/v5/beacon-chain/forkchoice" + light_client "github.com/prysmaticlabs/prysm/v5/beacon-chain/light-client" "github.com/prysmaticlabs/prysm/v5/beacon-chain/operations/attestations" "github.com/prysmaticlabs/prysm/v5/beacon-chain/operations/blstoexec" "github.com/prysmaticlabs/prysm/v5/beacon-chain/operations/slashings" @@ -205,3 +206,10 @@ func WithSyncChecker(checker Checker) Option { return nil } } + +func WithLightClientStore(lcs *light_client.Store) Option { + return func(s *Service) error { + s.lcStore = lcs + return nil + } +} diff --git a/beacon-chain/blockchain/process_block.go b/beacon-chain/blockchain/process_block.go index 7a55c3f83fba..5f529a3e433a 100644 --- a/beacon-chain/blockchain/process_block.go +++ b/beacon-chain/blockchain/process_block.go @@ -69,7 +69,10 @@ func (s *Service) postBlockProcess(cfg *postBlockProcessConfig) error { if s.inRegularSync() { defer s.handleSecondFCUCall(cfg, fcuArgs) } - defer s.sendLightClientFeeds(cfg) + if features.Get().EnableLightClient && slots.ToEpoch(s.CurrentSlot()) >= params.BeaconConfig().AltairForkEpoch { + defer s.saveLightClientUpdates(cfg) + defer s.processLightClientUpdates(cfg) + } defer s.sendStateFeedOnBlock(cfg) defer reportProcessingTime(startTime) defer reportAttestationInclusion(cfg.roblock.Block()) diff --git a/beacon-chain/blockchain/process_block_helpers.go b/beacon-chain/blockchain/process_block_helpers.go index 04289145e079..d2dabdcd29ad 100644 --- a/beacon-chain/blockchain/process_block_helpers.go +++ b/beacon-chain/blockchain/process_block_helpers.go @@ -5,17 +5,15 @@ import ( "fmt" "time" - lightclient "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/light-client" - "github.com/ethereum/go-ethereum/common" "github.com/pkg/errors" "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/feed" statefeed "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/feed/state" + lightclient "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/light-client" "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/transition" doublylinkedtree "github.com/prysmaticlabs/prysm/v5/beacon-chain/forkchoice/doubly-linked-tree" forkchoicetypes "github.com/prysmaticlabs/prysm/v5/beacon-chain/forkchoice/types" "github.com/prysmaticlabs/prysm/v5/beacon-chain/state" - "github.com/prysmaticlabs/prysm/v5/config/features" field_params "github.com/prysmaticlabs/prysm/v5/config/fieldparams" "github.com/prysmaticlabs/prysm/v5/config/params" consensus_blocks "github.com/prysmaticlabs/prysm/v5/consensus-types/blocks" @@ -114,69 +112,121 @@ func (s *Service) sendStateFeedOnBlock(cfg *postBlockProcessConfig) { }) } -// sendLightClientFeeds sends the light client feeds when feature flag is enabled. -func (s *Service) sendLightClientFeeds(cfg *postBlockProcessConfig) { - if features.Get().EnableLightClient { - if _, err := s.sendLightClientOptimisticUpdate(cfg.ctx, cfg.roblock, cfg.postState); err != nil { - log.WithError(err).Error("Failed to send light client optimistic update") - } - - // Get the finalized checkpoint - finalized := s.ForkChoicer().FinalizedCheckpoint() - - // LightClientFinalityUpdate needs super majority - s.tryPublishLightClientFinalityUpdate(cfg.ctx, cfg.roblock, finalized, cfg.postState) +func (s *Service) processLightClientUpdates(cfg *postBlockProcessConfig) { + if err := s.processLightClientOptimisticUpdate(cfg.ctx, cfg.roblock, cfg.postState); err != nil { + log.WithError(err).Error("Failed to process light client optimistic update") + } + if err := s.processLightClientFinalityUpdate(cfg.ctx, cfg.roblock, cfg.postState); err != nil { + log.WithError(err).Error("Failed to process light client finality update") } } -func (s *Service) tryPublishLightClientFinalityUpdate( - ctx context.Context, - signed interfaces.ReadOnlySignedBeaconBlock, - finalized *forkchoicetypes.Checkpoint, - postState state.BeaconState, -) { - if finalized.Epoch <= s.lastPublishedLightClientEpoch { - return +func (s *Service) saveLightClientUpdates(cfg *postBlockProcessConfig) { + if err := s.saveLightClientBootstrap(cfg); err != nil { + log.WithError(err).Error("Failed to save light client bootstrap") } - - config := params.BeaconConfig() - if finalized.Epoch < config.AltairForkEpoch { - return + if err := s.saveLightClientUpdate(cfg); err != nil { + log.WithError(err).Error("Failed to save light client update") } +} - syncAggregate, err := signed.Block().Body().SyncAggregate() - if err != nil || syncAggregate == nil { - return +// saveLightClientUpdate saves the light client update for a block +// if it's better than the already saved one. +func (s *Service) saveLightClientUpdate(cfg *postBlockProcessConfig) error { + log.Info("LC: saving light client update") + + attestedRoot := cfg.roblock.Block().ParentRoot() + attestedBlock, err := s.getBlock(cfg.ctx, attestedRoot) + if err != nil { + return errors.Wrap(err, "could not get attested block") + } + if attestedBlock == nil || attestedBlock.IsNil() { + return errors.New("attested block is nil") + } + attestedState, err := s.cfg.StateGen.StateByRoot(cfg.ctx, attestedRoot) + if err != nil { + return errors.Wrap(err, "could not get attested state") + } + if attestedState == nil || attestedState.IsNil() { + return errors.New("attested state is nil") + } + finalizedRoot := cfg.postState.FinalizedCheckpoint().Root + finalizedBlock, err := s.getBlock(cfg.ctx, [32]byte(finalizedRoot)) + if err != nil { + return errors.Wrap(err, "could not get finalized block") } - // LightClientFinalityUpdate needs super majority - if syncAggregate.SyncCommitteeBits.Count()*3 < config.SyncCommitteeSize*2 { - return + update, err := lightclient.NewLightClientUpdateFromBeaconState( + cfg.ctx, + s.CurrentSlot(), + cfg.postState, + cfg.roblock, + attestedState, + attestedBlock, + finalizedBlock, + ) + if err != nil { + return errors.Wrap(err, "could not create light client update") } - _, err = s.sendLightClientFinalityUpdate(ctx, signed, postState) + period := uint64(attestedState.Slot()) / (uint64(params.BeaconConfig().SlotsPerEpoch) * uint64(params.BeaconConfig().EpochsPerSyncCommitteePeriod)) + + oldUpdate, err := s.cfg.BeaconDB.LightClientUpdate(cfg.ctx, period) if err != nil { - log.WithError(err).Error("Failed to send light client finality update") + return errors.Wrap(err, "could not get current light client update") + } + if oldUpdate == nil { + if err = s.cfg.BeaconDB.SaveLightClientUpdate(cfg.ctx, period, update); err != nil { + return errors.Wrap(err, "could not save light client update") + } } else { - s.lastPublishedLightClientEpoch = finalized.Epoch + isNewUpdateBetter, err := lightclient.IsBetterUpdate(update, oldUpdate) + if err != nil { + return errors.Wrap(err, "could not compare light client updates") + } + if isNewUpdateBetter { + if err = s.cfg.BeaconDB.SaveLightClientUpdate(cfg.ctx, period, update); err != nil { + return errors.Wrap(err, "could not save light client update") + } + log.Info("LC: saved light client update") + } else { + log.Info("LC: skipped saving light client update") + } + } + + return nil +} + +// saveLightClientBootstrap saves a light client bootstrap for a block. +func (s *Service) saveLightClientBootstrap(cfg *postBlockProcessConfig) error { + log.Info("LC: saving light client bootstrap") + blockRoot := cfg.roblock.Root() + bootstrap, err := lightclient.CreateLightClientBootstrap(cfg.ctx, s.CurrentSlot(), cfg.postState, cfg.roblock) + if err != nil { + return errors.Wrap(err, "could not create light client bootstrap") } + if err = s.cfg.BeaconDB.SaveLightClientBootstrap(cfg.ctx, blockRoot[:], bootstrap); err != nil { + return errors.Wrap(err, "could not save light client bootstrap") + } + log.Infof("LC: saved light client bootstrap for root %#x", blockRoot) + return nil } -// sendLightClientFinalityUpdate sends a light client finality update notification to the state feed. -func (s *Service) sendLightClientFinalityUpdate(ctx context.Context, signed interfaces.ReadOnlySignedBeaconBlock, - postState state.BeaconState) (int, error) { - // Get attested state +func (s *Service) processLightClientFinalityUpdate( + ctx context.Context, + signed interfaces.ReadOnlySignedBeaconBlock, + postState state.BeaconState, +) error { attestedRoot := signed.Block().ParentRoot() attestedBlock, err := s.cfg.BeaconDB.Block(ctx, attestedRoot) if err != nil { - return 0, errors.Wrap(err, "could not get attested block") + return errors.Wrap(err, "could not get attested block") } attestedState, err := s.cfg.StateGen.StateByRoot(ctx, attestedRoot) if err != nil { - return 0, errors.Wrap(err, "could not get attested state") + return errors.Wrap(err, "could not get attested state") } - // Get finalized block var finalizedBlock interfaces.ReadOnlySignedBeaconBlock finalizedCheckPoint := attestedState.FinalizedCheckpoint() if finalizedCheckPoint != nil { @@ -197,28 +247,52 @@ func (s *Service) sendLightClientFinalityUpdate(ctx context.Context, signed inte finalizedBlock, ) if err != nil { - return 0, errors.Wrap(err, "could not create light client update") + return errors.Wrap(err, "could not create light client update") + } + + maxActiveParticipants := update.SyncAggregate().SyncCommitteeBits.Len() + numActiveParticipants := update.SyncAggregate().SyncCommitteeBits.Count() + hasSupermajority := numActiveParticipants*3 >= maxActiveParticipants*2 + + last := s.lcStore.LastLCFinalityUpdate + if last != nil { + // The finalized_header.beacon.slot is greater than that of all previously forwarded finality_updates, + // or it matches the highest previously forwarded slot and also has a sync_aggregate indicating supermajority (> 2/3) + // sync committee participation while the previously forwarded finality_update for that slot did not indicate supermajority + slot := last.FinalizedHeader().Beacon().Slot + lastMaxActiveParticipants := last.SyncAggregate().SyncCommitteeBits.Len() + lastNumActiveParticipants := last.SyncAggregate().SyncCommitteeBits.Count() + lastHasSupermajority := lastNumActiveParticipants*3 >= lastMaxActiveParticipants*2 + + if update.FinalizedHeader().Beacon().Slot < slot { + return nil + } + if update.FinalizedHeader().Beacon().Slot == slot && (lastHasSupermajority || !hasSupermajority) { + return nil + } } - // Send event - return s.cfg.StateNotifier.StateFeed().Send(&feed.Event{ + log.Info("LC: storing new finality update post-block") + s.lcStore.LastLCFinalityUpdate = update + + s.cfg.StateNotifier.StateFeed().Send(&feed.Event{ Type: statefeed.LightClientFinalityUpdate, Data: update, - }), nil + }) + + return nil } -// sendLightClientOptimisticUpdate sends a light client optimistic update notification to the state feed. -func (s *Service) sendLightClientOptimisticUpdate(ctx context.Context, signed interfaces.ReadOnlySignedBeaconBlock, - postState state.BeaconState) (int, error) { - // Get attested state +func (s *Service) processLightClientOptimisticUpdate(ctx context.Context, signed interfaces.ReadOnlySignedBeaconBlock, + postState state.BeaconState) error { attestedRoot := signed.Block().ParentRoot() attestedBlock, err := s.cfg.BeaconDB.Block(ctx, attestedRoot) if err != nil { - return 0, errors.Wrap(err, "could not get attested block") + return errors.Wrap(err, "could not get attested block") } attestedState, err := s.cfg.StateGen.StateByRoot(ctx, attestedRoot) if err != nil { - return 0, errors.Wrap(err, "could not get attested state") + return errors.Wrap(err, "could not get attested state") } update, err := lightclient.NewLightClientOptimisticUpdateFromBeaconState( @@ -230,13 +304,26 @@ func (s *Service) sendLightClientOptimisticUpdate(ctx context.Context, signed in attestedBlock, ) if err != nil { - return 0, errors.Wrap(err, "could not create light client update") + return errors.Wrap(err, "could not create light client update") } - return s.cfg.StateNotifier.StateFeed().Send(&feed.Event{ + last := s.lcStore.LastLCOptimisticUpdate + if last != nil { + // The attested_header.beacon.slot is greater than that of all previously forwarded optimistic_updates + if update.AttestedHeader().Beacon().Slot <= last.AttestedHeader().Beacon().Slot { + return nil + } + } + + log.Info("LC: storing new optimistic update post-block") + s.lcStore.LastLCOptimisticUpdate = update + + s.cfg.StateNotifier.StateFeed().Send(&feed.Event{ Type: statefeed.LightClientOptimisticUpdate, Data: update, - }), nil + }) + + return nil } // updateCachesPostBlockProcessing updates the next slot cache and handles the epoch diff --git a/beacon-chain/blockchain/service.go b/beacon-chain/blockchain/service.go index c984a2f79750..6494b80a746d 100644 --- a/beacon-chain/blockchain/service.go +++ b/beacon-chain/blockchain/service.go @@ -24,6 +24,7 @@ import ( "github.com/prysmaticlabs/prysm/v5/beacon-chain/execution" f "github.com/prysmaticlabs/prysm/v5/beacon-chain/forkchoice" forkchoicetypes "github.com/prysmaticlabs/prysm/v5/beacon-chain/forkchoice/types" + light_client "github.com/prysmaticlabs/prysm/v5/beacon-chain/light-client" "github.com/prysmaticlabs/prysm/v5/beacon-chain/operations/attestations" "github.com/prysmaticlabs/prysm/v5/beacon-chain/operations/blstoexec" "github.com/prysmaticlabs/prysm/v5/beacon-chain/operations/slashings" @@ -38,7 +39,6 @@ import ( "github.com/prysmaticlabs/prysm/v5/consensus-types/blocks" consensus_blocks "github.com/prysmaticlabs/prysm/v5/consensus-types/blocks" "github.com/prysmaticlabs/prysm/v5/consensus-types/interfaces" - "github.com/prysmaticlabs/prysm/v5/consensus-types/primitives" "github.com/prysmaticlabs/prysm/v5/encoding/bytesutil" "github.com/prysmaticlabs/prysm/v5/monitoring/tracing/trace" ethpb "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1" @@ -49,25 +49,25 @@ import ( // Service represents a service that handles the internal // logic of managing the full PoS beacon chain. type Service struct { - cfg *config - ctx context.Context - cancel context.CancelFunc - genesisTime time.Time - head *head - headLock sync.RWMutex - originBlockRoot [32]byte // genesis root, or weak subjectivity checkpoint root, depending on how the node is initialized - boundaryRoots [][32]byte - checkpointStateCache *cache.CheckpointStateCache - initSyncBlocks map[[32]byte]interfaces.ReadOnlySignedBeaconBlock - initSyncBlocksLock sync.RWMutex - wsVerifier *WeakSubjectivityVerifier - clockSetter startup.ClockSetter - clockWaiter startup.ClockWaiter - syncComplete chan struct{} - blobNotifiers *blobNotifierMap - blockBeingSynced *currentlySyncingBlock - blobStorage *filesystem.BlobStorage - lastPublishedLightClientEpoch primitives.Epoch + cfg *config + ctx context.Context + cancel context.CancelFunc + genesisTime time.Time + head *head + headLock sync.RWMutex + originBlockRoot [32]byte // genesis root, or weak subjectivity checkpoint root, depending on how the node is initialized + boundaryRoots [][32]byte + checkpointStateCache *cache.CheckpointStateCache + initSyncBlocks map[[32]byte]interfaces.ReadOnlySignedBeaconBlock + initSyncBlocksLock sync.RWMutex + wsVerifier *WeakSubjectivityVerifier + clockSetter startup.ClockSetter + clockWaiter startup.ClockWaiter + syncComplete chan struct{} + blobNotifiers *blobNotifierMap + blockBeingSynced *currentlySyncingBlock + blobStorage *filesystem.BlobStorage + lcStore *light_client.Store } // config options for the service. diff --git a/beacon-chain/core/light-client/lightclient.go b/beacon-chain/core/light-client/lightclient.go index d07813fc7a5d..e2933430ccdd 100644 --- a/beacon-chain/core/light-client/lightclient.go +++ b/beacon-chain/core/light-client/lightclient.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "fmt" + "reflect" "github.com/pkg/errors" "github.com/prysmaticlabs/prysm/v5/beacon-chain/execution" @@ -23,6 +24,104 @@ import ( "google.golang.org/protobuf/proto" ) +func NewLightClientBootstrapFromBeaconState( + ctx context.Context, + currentSlot primitives.Slot, + state state.BeaconState, + block interfaces.ReadOnlySignedBeaconBlock, +) (interfaces.LightClientBootstrap, error) { + // assert compute_epoch_at_slot(state.slot) >= ALTAIR_FORK_EPOCH + if slots.ToEpoch(state.Slot()) < params.BeaconConfig().AltairForkEpoch { + return nil, fmt.Errorf("light client bootstrap is not supported before Altair, invalid slot %d", state.Slot()) + } + + // assert state.slot == state.latest_block_header.slot + latestBlockHeader := state.LatestBlockHeader() + if state.Slot() != latestBlockHeader.Slot { + return nil, fmt.Errorf("state slot %d not equal to latest block header slot %d", state.Slot(), latestBlockHeader.Slot) + } + + // header.state_root = hash_tree_root(state) + stateRoot, err := state.HashTreeRoot(ctx) + if err != nil { + return nil, errors.Wrap(err, "could not get state root") + } + latestBlockHeader.StateRoot = stateRoot[:] + + // assert hash_tree_root(header) == hash_tree_root(block.message) + latestBlockHeaderRoot, err := latestBlockHeader.HashTreeRoot() + if err != nil { + return nil, errors.Wrap(err, "could not get latest block header root") + } + beaconBlockRoot, err := block.Block().HashTreeRoot() + if err != nil { + return nil, errors.Wrap(err, "could not get block root") + } + if latestBlockHeaderRoot != beaconBlockRoot { + return nil, fmt.Errorf("latest block header root %#x not equal to block root %#x", latestBlockHeaderRoot, beaconBlockRoot) + } + + lightClientHeader, err := BlockToLightClientHeader(ctx, currentSlot, block) + if err != nil { + return nil, errors.Wrap(err, "could not convert block to light client header") + } + currentSyncCommittee, err := state.CurrentSyncCommittee() + if err != nil { + return nil, errors.Wrap(err, "could not get current sync committee") + } + currentSyncCommitteeProof, err := state.CurrentSyncCommitteeProof(ctx) + if err != nil { + return nil, errors.Wrap(err, "could not get current sync committee proof") + } + + currentEpoch := slots.ToEpoch(currentSlot) + + var m proto.Message + if currentEpoch >= params.BeaconConfig().ElectraForkEpoch { + h, ok := lightClientHeader.Proto().(*pb.LightClientHeaderDeneb) + if !ok { + return nil, fmt.Errorf("light client header type %T is not %T", h, &pb.LightClientHeaderDeneb{}) + } + m = &pb.LightClientBootstrapElectra{ + Header: h, + CurrentSyncCommittee: currentSyncCommittee, + CurrentSyncCommitteeBranch: currentSyncCommitteeProof, + } + } else if currentEpoch >= params.BeaconConfig().DenebForkEpoch { + h, ok := lightClientHeader.Proto().(*pb.LightClientHeaderDeneb) + if !ok { + return nil, fmt.Errorf("light client header type %T is not %T", h, &pb.LightClientHeaderDeneb{}) + } + m = &pb.LightClientBootstrapDeneb{ + Header: h, + CurrentSyncCommittee: currentSyncCommittee, + CurrentSyncCommitteeBranch: currentSyncCommitteeProof, + } + } else if currentEpoch >= params.BeaconConfig().CapellaForkEpoch { + h, ok := lightClientHeader.Proto().(*pb.LightClientHeaderCapella) + if !ok { + return nil, fmt.Errorf("light client header type %T is not %T", h, &pb.LightClientHeaderCapella{}) + } + m = &pb.LightClientBootstrapCapella{ + Header: h, + CurrentSyncCommittee: currentSyncCommittee, + CurrentSyncCommitteeBranch: currentSyncCommitteeProof, + } + } else { + h, ok := lightClientHeader.Proto().(*pb.LightClientHeaderAltair) + if !ok { + return nil, fmt.Errorf("light client header type %T is not %T", h, &pb.LightClientHeaderAltair{}) + } + m = &pb.LightClientBootstrapAltair{ + Header: h, + CurrentSyncCommittee: currentSyncCommittee, + CurrentSyncCommitteeBranch: currentSyncCommitteeProof, + } + } + + return light_client.NewWrappedBootstrap(m) +} + func NewLightClientFinalityUpdateFromBeaconState( ctx context.Context, currentSlot primitives.Slot, @@ -562,3 +661,236 @@ func emptyPayloadProof() [][]byte { } return proof } + +func HasRelevantSyncCommittee(update interfaces.LightClientUpdate) (bool, error) { + if update.Version() >= version.Electra { + branch, err := update.NextSyncCommitteeBranchElectra() + if err != nil { + return false, err + } + return !reflect.DeepEqual(branch, interfaces.LightClientSyncCommitteeBranchElectra{}), nil + } + branch, err := update.NextSyncCommitteeBranch() + if err != nil { + return false, err + } + return !reflect.DeepEqual(branch, interfaces.LightClientSyncCommitteeBranch{}), nil +} + +func HasFinality(update interfaces.LightClientUpdate) (bool, error) { + if update.Version() >= version.Electra { + b, err := update.FinalityBranchElectra() + if err != nil { + return false, err + } + return !reflect.DeepEqual(b, interfaces.LightClientFinalityBranchElectra{}), nil + } + + b, err := update.FinalityBranch() + if err != nil { + return false, err + } + return !reflect.DeepEqual(b, interfaces.LightClientFinalityBranch{}), nil +} + +func IsBetterUpdate(newUpdate, oldUpdate interfaces.LightClientUpdate) (bool, error) { + maxActiveParticipants := newUpdate.SyncAggregate().SyncCommitteeBits.Len() + newNumActiveParticipants := newUpdate.SyncAggregate().SyncCommitteeBits.Count() + oldNumActiveParticipants := oldUpdate.SyncAggregate().SyncCommitteeBits.Count() + newHasSupermajority := newNumActiveParticipants*3 >= maxActiveParticipants*2 + oldHasSupermajority := oldNumActiveParticipants*3 >= maxActiveParticipants*2 + + if newHasSupermajority != oldHasSupermajority { + return newHasSupermajority, nil + } + if !newHasSupermajority && newNumActiveParticipants != oldNumActiveParticipants { + return newNumActiveParticipants > oldNumActiveParticipants, nil + } + + newUpdateAttestedHeaderBeacon := newUpdate.AttestedHeader().Beacon() + oldUpdateAttestedHeaderBeacon := oldUpdate.AttestedHeader().Beacon() + + // Compare presence of relevant sync committee + newHasRelevantSyncCommittee, err := HasRelevantSyncCommittee(newUpdate) + if err != nil { + return false, err + } + newHasRelevantSyncCommittee = newHasRelevantSyncCommittee && + (slots.SyncCommitteePeriod(slots.ToEpoch(newUpdateAttestedHeaderBeacon.Slot)) == slots.SyncCommitteePeriod(slots.ToEpoch(newUpdate.SignatureSlot()))) + oldHasRelevantSyncCommittee, err := HasRelevantSyncCommittee(oldUpdate) + if err != nil { + return false, err + } + oldHasRelevantSyncCommittee = oldHasRelevantSyncCommittee && + (slots.SyncCommitteePeriod(slots.ToEpoch(oldUpdateAttestedHeaderBeacon.Slot)) == slots.SyncCommitteePeriod(slots.ToEpoch(oldUpdate.SignatureSlot()))) + + if newHasRelevantSyncCommittee != oldHasRelevantSyncCommittee { + return newHasRelevantSyncCommittee, nil + } + + // Compare indication of any finality + newHasFinality, err := HasFinality(newUpdate) + if err != nil { + return false, err + } + oldHasFinality, err := HasFinality(oldUpdate) + if err != nil { + return false, err + } + if newHasFinality != oldHasFinality { + return newHasFinality, nil + } + + newUpdateFinalizedHeaderBeacon := newUpdate.FinalizedHeader().Beacon() + oldUpdateFinalizedHeaderBeacon := oldUpdate.FinalizedHeader().Beacon() + + // Compare sync committee finality + if newHasFinality { + newHasSyncCommitteeFinality := + slots.SyncCommitteePeriod(slots.ToEpoch(newUpdateFinalizedHeaderBeacon.Slot)) == + slots.SyncCommitteePeriod(slots.ToEpoch(newUpdateAttestedHeaderBeacon.Slot)) + oldHasSyncCommitteeFinality := + slots.SyncCommitteePeriod(slots.ToEpoch(oldUpdateFinalizedHeaderBeacon.Slot)) == + slots.SyncCommitteePeriod(slots.ToEpoch(oldUpdateAttestedHeaderBeacon.Slot)) + + if newHasSyncCommitteeFinality != oldHasSyncCommitteeFinality { + return newHasSyncCommitteeFinality, nil + } + } + + // Tiebreaker 1: Sync committee participation beyond supermajority + if newNumActiveParticipants != oldNumActiveParticipants { + return newNumActiveParticipants > oldNumActiveParticipants, nil + } + + // Tiebreaker 2: Prefer older data (fewer changes to best) + if newUpdateAttestedHeaderBeacon.Slot != oldUpdateAttestedHeaderBeacon.Slot { + return newUpdateAttestedHeaderBeacon.Slot < oldUpdateAttestedHeaderBeacon.Slot, nil + } + + return newUpdate.SignatureSlot() < oldUpdate.SignatureSlot(), nil +} + +func CreateLightClientBootstrap( + ctx context.Context, + currentSlot primitives.Slot, + state state.BeaconState, + block interfaces.ReadOnlySignedBeaconBlock, +) (interfaces.LightClientBootstrap, error) { + // assert compute_epoch_at_slot(state.slot) >= ALTAIR_FORK_EPOCH + if slots.ToEpoch(state.Slot()) < params.BeaconConfig().AltairForkEpoch { + return nil, fmt.Errorf("light client bootstrap is not supported before Altair, invalid slot %d", state.Slot()) + } + + // assert state.slot == state.latest_block_header.slot + latestBlockHeader := state.LatestBlockHeader() + if state.Slot() != latestBlockHeader.Slot { + return nil, fmt.Errorf("state slot %d not equal to latest block header slot %d", state.Slot(), latestBlockHeader.Slot) + } + + // header.state_root = hash_tree_root(state) + stateRoot, err := state.HashTreeRoot(ctx) + if err != nil { + return nil, errors.Wrap(err, "could not get state root") + } + latestBlockHeader.StateRoot = stateRoot[:] + + // assert hash_tree_root(header) == hash_tree_root(block.message) + latestBlockHeaderRoot, err := latestBlockHeader.HashTreeRoot() + if err != nil { + return nil, errors.Wrap(err, "could not get latest block header root") + } + beaconBlockRoot, err := block.Block().HashTreeRoot() + if err != nil { + return nil, errors.Wrap(err, "could not get block root") + } + if latestBlockHeaderRoot != beaconBlockRoot { + return nil, fmt.Errorf("latest block header root %#x not equal to block root %#x", latestBlockHeaderRoot, beaconBlockRoot) + } + + bootstrap, err := createDefaultLightClientBootstrap(currentSlot) + if err != nil { + return nil, errors.Wrap(err, "could not create default light client bootstrap") + } + + lightClientHeader, err := BlockToLightClientHeader(ctx, currentSlot, block) + if err != nil { + return nil, errors.Wrap(err, "could not convert block to light client header") + } + + err = bootstrap.SetHeader(lightClientHeader) + if err != nil { + return nil, errors.Wrap(err, "could not set header") + } + + currentSyncCommittee, err := state.CurrentSyncCommittee() + if err != nil { + return nil, errors.Wrap(err, "could not get current sync committee") + } + + err = bootstrap.SetCurrentSyncCommittee(currentSyncCommittee) + if err != nil { + return nil, errors.Wrap(err, "could not set current sync committee") + } + + currentSyncCommitteeProof, err := state.CurrentSyncCommitteeProof(ctx) + if err != nil { + return nil, errors.Wrap(err, "could not get current sync committee proof") + } + + err = bootstrap.SetCurrentSyncCommitteeBranch(currentSyncCommitteeProof) + if err != nil { + return nil, errors.Wrap(err, "could not set current sync committee proof") + } + + return bootstrap, nil +} + +func createDefaultLightClientBootstrap(currentSlot primitives.Slot) (interfaces.LightClientBootstrap, error) { + currentEpoch := slots.ToEpoch(currentSlot) + syncCommitteeSize := params.BeaconConfig().SyncCommitteeSize + pubKeys := make([][]byte, syncCommitteeSize) + for i := uint64(0); i < syncCommitteeSize; i++ { + pubKeys[i] = make([]byte, fieldparams.BLSPubkeyLength) + } + currentSyncCommittee := &pb.SyncCommittee{ + Pubkeys: pubKeys, + AggregatePubkey: make([]byte, fieldparams.BLSPubkeyLength), + } + + var currentSyncCommitteeBranch [][]byte + if currentEpoch >= params.BeaconConfig().ElectraForkEpoch { + currentSyncCommitteeBranch = make([][]byte, fieldparams.SyncCommitteeBranchDepthElectra) + } else { + currentSyncCommitteeBranch = make([][]byte, fieldparams.SyncCommitteeBranchDepth) + } + for i := 0; i < len(currentSyncCommitteeBranch); i++ { + currentSyncCommitteeBranch[i] = make([]byte, fieldparams.RootLength) + } + + // TODO: can this be based on the current epoch? + var m proto.Message + if currentEpoch < params.BeaconConfig().CapellaForkEpoch { + m = &pb.LightClientBootstrapAltair{ + CurrentSyncCommittee: currentSyncCommittee, + CurrentSyncCommitteeBranch: currentSyncCommitteeBranch, + } + } else if currentEpoch < params.BeaconConfig().DenebForkEpoch { + m = &pb.LightClientBootstrapCapella{ + CurrentSyncCommittee: currentSyncCommittee, + CurrentSyncCommitteeBranch: currentSyncCommitteeBranch, + } + } else if currentEpoch < params.BeaconConfig().ElectraForkEpoch { + m = &pb.LightClientBootstrapDeneb{ + CurrentSyncCommittee: currentSyncCommittee, + CurrentSyncCommitteeBranch: currentSyncCommitteeBranch, + } + } else { + m = &pb.LightClientBootstrapElectra{ + CurrentSyncCommittee: currentSyncCommittee, + CurrentSyncCommitteeBranch: currentSyncCommitteeBranch, + } + } + + return light_client.NewWrappedBootstrap(m) +} diff --git a/beacon-chain/db/iface/interface.go b/beacon-chain/db/iface/interface.go index fc2ef7af4faa..544a9d093afc 100644 --- a/beacon-chain/db/iface/interface.go +++ b/beacon-chain/db/iface/interface.go @@ -57,6 +57,7 @@ type ReadOnlyDatabase interface { FeeRecipientByValidatorID(ctx context.Context, id primitives.ValidatorIndex) (common.Address, error) RegistrationByValidatorID(ctx context.Context, id primitives.ValidatorIndex) (*ethpb.ValidatorRegistrationV1, error) // light client operations + LightClientBootstrap(ctx context.Context, root [32]byte) (interfaces.LightClientBootstrap, error) LightClientUpdates(ctx context.Context, startPeriod, endPeriod uint64) (map[uint64]interfaces.LightClientUpdate, error) LightClientUpdate(ctx context.Context, period uint64) (interfaces.LightClientUpdate, error) @@ -96,6 +97,7 @@ type NoHeadAccessDatabase interface { SaveFeeRecipientsByValidatorIDs(ctx context.Context, ids []primitives.ValidatorIndex, addrs []common.Address) error SaveRegistrationsByValidatorIDs(ctx context.Context, ids []primitives.ValidatorIndex, regs []*ethpb.ValidatorRegistrationV1) error // light client operations + SaveLightClientBootstrap(ctx context.Context, blockRoot []byte, bootstrap interfaces.LightClientBootstrap) error SaveLightClientUpdate(ctx context.Context, period uint64, update interfaces.LightClientUpdate) error CleanUpDirtyStates(ctx context.Context, slotsPerArchivedPoint primitives.Slot) error diff --git a/beacon-chain/db/kv/kv.go b/beacon-chain/db/kv/kv.go index 63e49e30485d..128b17eddf24 100644 --- a/beacon-chain/db/kv/kv.go +++ b/beacon-chain/db/kv/kv.go @@ -108,6 +108,7 @@ var Buckets = [][]byte{ stateSummaryBucket, stateValidatorsBucket, lightClientUpdatesBucket, + lightClientBootstrapBucket, // Indices buckets. blockSlotIndicesBucket, stateSlotIndicesBucket, diff --git a/beacon-chain/db/kv/lightclient.go b/beacon-chain/db/kv/lightclient.go index f4cf94668d30..01bd15136200 100644 --- a/beacon-chain/db/kv/lightclient.go +++ b/beacon-chain/db/kv/lightclient.go @@ -31,6 +31,91 @@ func (s *Store) SaveLightClientUpdate(ctx context.Context, period uint64, update }) } +func (s *Store) SaveLightClientBootstrap(ctx context.Context, blockRoot []byte, bootstrap interfaces.LightClientBootstrap) error { + _, span := trace.StartSpan(ctx, "BeaconDB.SaveLightClientBootstrap") + defer span.End() + + return s.db.Update(func(tx *bolt.Tx) error { + bkt := tx.Bucket(lightClientBootstrapBucket) + enc, err := encodeLightClientBootstrap(bootstrap) + if err != nil { + return err + } + return bkt.Put(blockRoot, enc) + }) +} + +func (s *Store) LightClientBootstrap(ctx context.Context, blockRoot [32]byte) (interfaces.LightClientBootstrap, error) { + _, span := trace.StartSpan(ctx, "BeaconDB.LightClientBootstrap") + defer span.End() + + var bootstrap interfaces.LightClientBootstrap + err := s.db.View(func(tx *bolt.Tx) error { + bkt := tx.Bucket(lightClientBootstrapBucket) + enc := bkt.Get(blockRoot[:]) + if enc == nil { + return nil + } + var err error + bootstrap, err = decodeLightClientBootstrap(enc) + return err + }) + return bootstrap, err +} + +func encodeLightClientBootstrap(bootstrap interfaces.LightClientBootstrap) ([]byte, error) { + key, err := keyForLightClientUpdate(bootstrap.Version()) + if err != nil { + return nil, err + } + enc, err := bootstrap.MarshalSSZ() + if err != nil { + return nil, errors.Wrap(err, "could not marshal light client bootstrap") + } + fullEnc := make([]byte, len(key)+len(enc)) + copy(fullEnc, key) + copy(fullEnc[len(key):], enc) + return snappy.Encode(nil, fullEnc), nil +} + +func decodeLightClientBootstrap(enc []byte) (interfaces.LightClientBootstrap, error) { + var err error + enc, err = snappy.Decode(nil, enc) + if err != nil { + return nil, errors.Wrap(err, "could not snappy decode light client bootstrap") + } + var m proto.Message + switch { + case hasAltairKey(enc): + bootstrap := ðpb.LightClientBootstrapAltair{} + if err := bootstrap.UnmarshalSSZ(enc[len(altairKey):]); err != nil { + return nil, errors.Wrap(err, "could not unmarshal Altair light client bootstrap") + } + m = bootstrap + case hasCapellaKey(enc): + bootstrap := ðpb.LightClientBootstrapCapella{} + if err := bootstrap.UnmarshalSSZ(enc[len(capellaKey):]); err != nil { + return nil, errors.Wrap(err, "could not unmarshal Capella light client bootstrap") + } + m = bootstrap + case hasDenebKey(enc): + bootstrap := ðpb.LightClientBootstrapDeneb{} + if err := bootstrap.UnmarshalSSZ(enc[len(denebKey):]); err != nil { + return nil, errors.Wrap(err, "could not unmarshal Deneb light client bootstrap") + } + m = bootstrap + case hasElectraKey(enc): + bootstrap := ðpb.LightClientBootstrapElectra{} + if err := bootstrap.UnmarshalSSZ(enc[len(electraKey):]); err != nil { + return nil, errors.Wrap(err, "could not unmarshal Electra light client bootstrap") + } + m = bootstrap + default: + return nil, errors.New("decoding of saved light client bootstrap is unsupported") + } + return light_client.NewWrappedBootstrap(m) +} + func (s *Store) LightClientUpdates(ctx context.Context, startPeriod, endPeriod uint64) (map[uint64]interfaces.LightClientUpdate, error) { _, span := trace.StartSpan(ctx, "BeaconDB.LightClientUpdates") defer span.End() @@ -87,7 +172,7 @@ func (s *Store) LightClientUpdate(ctx context.Context, period uint64) (interface } func encodeLightClientUpdate(update interfaces.LightClientUpdate) ([]byte, error) { - key, err := keyForLightClientUpdate(update) + key, err := keyForLightClientUpdate(update.Version()) if err != nil { return nil, err } @@ -139,8 +224,8 @@ func decodeLightClientUpdate(enc []byte) (interfaces.LightClientUpdate, error) { return light_client.NewWrappedUpdate(m) } -func keyForLightClientUpdate(update interfaces.LightClientUpdate) ([]byte, error) { - switch v := update.Version(); v { +func keyForLightClientUpdate(v int) ([]byte, error) { + switch v { case version.Electra: return electraKey, nil case version.Deneb: diff --git a/beacon-chain/db/kv/schema.go b/beacon-chain/db/kv/schema.go index 30d950514ca2..f6648a8f928a 100644 --- a/beacon-chain/db/kv/schema.go +++ b/beacon-chain/db/kv/schema.go @@ -18,7 +18,8 @@ var ( registrationBucket = []byte("registration") // Light Client Updates Bucket - lightClientUpdatesBucket = []byte("light-client-updates") + lightClientUpdatesBucket = []byte("light-client-updates") + lightClientBootstrapBucket = []byte("light-client-bootstrap") // Deprecated: This bucket was migrated in PR 6461. Do not use, except for migrations. slotsHasObjectBucket = []byte("slots-has-objects") diff --git a/beacon-chain/light-client/BUILD.bazel b/beacon-chain/light-client/BUILD.bazel new file mode 100644 index 000000000000..4c36f9363a8d --- /dev/null +++ b/beacon-chain/light-client/BUILD.bazel @@ -0,0 +1,9 @@ +load("@prysm//tools/go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = ["store.go"], + importpath = "github.com/prysmaticlabs/prysm/v5/beacon-chain/light-client", + visibility = ["//visibility:public"], + deps = ["//consensus-types/interfaces:go_default_library"], +) diff --git a/beacon-chain/light-client/store.go b/beacon-chain/light-client/store.go new file mode 100644 index 000000000000..203758d3ebc7 --- /dev/null +++ b/beacon-chain/light-client/store.go @@ -0,0 +1,12 @@ +package light_client + +import ( + "github.com/prysmaticlabs/prysm/v5/consensus-types/interfaces" +) + +type Store struct { + // lcFinalityUpdateLock sync.Mutex + LastLCFinalityUpdate interfaces.LightClientFinalityUpdate + // lcOptimisticUpdateLock sync.Mutex + LastLCOptimisticUpdate interfaces.LightClientOptimisticUpdate +} diff --git a/beacon-chain/node/BUILD.bazel b/beacon-chain/node/BUILD.bazel index a310daf40b26..e21526b857b6 100644 --- a/beacon-chain/node/BUILD.bazel +++ b/beacon-chain/node/BUILD.bazel @@ -30,6 +30,7 @@ go_library( "//beacon-chain/execution:go_default_library", "//beacon-chain/forkchoice:go_default_library", "//beacon-chain/forkchoice/doubly-linked-tree:go_default_library", + "//beacon-chain/light-client:go_default_library", "//beacon-chain/monitor:go_default_library", "//beacon-chain/node/registration:go_default_library", "//beacon-chain/operations/attestations:go_default_library", diff --git a/beacon-chain/node/node.go b/beacon-chain/node/node.go index fce4a4e56afd..673cbdd7ada3 100644 --- a/beacon-chain/node/node.go +++ b/beacon-chain/node/node.go @@ -34,6 +34,7 @@ import ( "github.com/prysmaticlabs/prysm/v5/beacon-chain/execution" "github.com/prysmaticlabs/prysm/v5/beacon-chain/forkchoice" doublylinkedtree "github.com/prysmaticlabs/prysm/v5/beacon-chain/forkchoice/doubly-linked-tree" + light_client "github.com/prysmaticlabs/prysm/v5/beacon-chain/light-client" "github.com/prysmaticlabs/prysm/v5/beacon-chain/monitor" "github.com/prysmaticlabs/prysm/v5/beacon-chain/node/registration" "github.com/prysmaticlabs/prysm/v5/beacon-chain/operations/attestations" @@ -121,6 +122,7 @@ type BeaconNode struct { BlobStorageOptions []filesystem.BlobStorageOption verifyInitWaiter *verification.InitializerWaiter syncChecker *initialsync.SyncChecker + lcStore *light_client.Store } // New creates a new node instance, sets up configuration options, and registers @@ -157,6 +159,7 @@ func New(cliCtx *cli.Context, cancel context.CancelFunc, opts ...Option) (*Beaco serviceFlagOpts: &serviceFlagOpts{}, initialSyncComplete: make(chan struct{}), syncChecker: &initialsync.SyncChecker{}, + lcStore: &light_client.Store{}, } for _, opt := range opts { @@ -763,6 +766,7 @@ func (b *BeaconNode) registerBlockchainService(fc forkchoice.ForkChoicer, gs *st blockchain.WithTrackedValidatorsCache(b.trackedValidatorsCache), blockchain.WithPayloadIDCache(b.payloadIDCache), blockchain.WithSyncChecker(b.syncChecker), + blockchain.WithLightClientStore(b.lcStore), ) blockchainService, err := blockchain.NewService(b.ctx, opts...) @@ -846,6 +850,7 @@ func (b *BeaconNode) registerSyncService(initialSyncComplete chan struct{}, bFil regularsync.WithBlobStorage(b.BlobStorage), regularsync.WithVerifierWaiter(b.verifyInitWaiter), regularsync.WithAvailableBlocker(bFillStore), + regularsync.WithLightClientStore(b.lcStore), ) return b.services.RegisterService(rs) } @@ -1003,6 +1008,7 @@ func (b *BeaconNode) registerRPCService(router *http.ServeMux) error { BlobStorage: b.BlobStorage, TrackedValidatorsCache: b.trackedValidatorsCache, PayloadIDCache: b.payloadIDCache, + LCStore: b.lcStore, }) return b.services.RegisterService(rpcService) diff --git a/beacon-chain/p2p/gossip_scoring_params.go b/beacon-chain/p2p/gossip_scoring_params.go index afe667283435..2e522a31c5b3 100644 --- a/beacon-chain/p2p/gossip_scoring_params.go +++ b/beacon-chain/p2p/gossip_scoring_params.go @@ -44,6 +44,12 @@ const ( // blsToExecutionChangeWeight specifies the scoring weight that we apply to // our bls to execution topic. blsToExecutionChangeWeight = 0.05 + // lightClientFinalityUpdateWeight specifies the scoring weight that we apply to + // our light client finality update topic. + lightClientFinalityUpdateWeight = 0.05 + // lightClientOptimisticUpdateWeight specifies the scoring weight that we apply to + // our light client optimistic update topic. + lightClientOptimisticUpdateWeight = 0.05 // maxInMeshScore describes the max score a peer can attain from being in the mesh. maxInMeshScore = 10 @@ -124,6 +130,10 @@ func (s *Service) topicScoreParams(topic string) (*pubsub.TopicScoreParams, erro case strings.Contains(topic, GossipBlobSidecarMessage): // TODO(Deneb): Using the default block scoring. But this should be updated. return defaultBlockTopicParams(), nil + case strings.Contains(topic, GossipLightClientFinalityUpdateMessage): + return defaultLightClientFinalityUpdateTopicParams(), nil + case strings.Contains(topic, GossipLightClientOptimisticUpdateMessage): + return defaultLightClientOptimisticUpdateTopicParams(), nil default: return nil, errors.Errorf("unrecognized topic provided for parameter registration: %s", topic) } @@ -503,6 +513,50 @@ func defaultBlsToExecutionChangeTopicParams() *pubsub.TopicScoreParams { } } +func defaultLightClientFinalityUpdateTopicParams() *pubsub.TopicScoreParams { + return &pubsub.TopicScoreParams{ + TopicWeight: lightClientFinalityUpdateWeight, + TimeInMeshWeight: maxInMeshScore / inMeshCap(), + TimeInMeshQuantum: inMeshTime(), + TimeInMeshCap: inMeshCap(), + FirstMessageDeliveriesWeight: 2, + FirstMessageDeliveriesDecay: scoreDecay(oneHundredEpochs), + FirstMessageDeliveriesCap: 5, + MeshMessageDeliveriesWeight: 0, + MeshMessageDeliveriesDecay: 0, + MeshMessageDeliveriesCap: 0, + MeshMessageDeliveriesThreshold: 0, + MeshMessageDeliveriesWindow: 0, + MeshMessageDeliveriesActivation: 0, + MeshFailurePenaltyWeight: 0, + MeshFailurePenaltyDecay: 0, + InvalidMessageDeliveriesWeight: -2000, + InvalidMessageDeliveriesDecay: scoreDecay(invalidDecayPeriod), + } +} + +func defaultLightClientOptimisticUpdateTopicParams() *pubsub.TopicScoreParams { + return &pubsub.TopicScoreParams{ + TopicWeight: lightClientOptimisticUpdateWeight, + TimeInMeshWeight: maxInMeshScore / inMeshCap(), + TimeInMeshQuantum: inMeshTime(), + TimeInMeshCap: inMeshCap(), + FirstMessageDeliveriesWeight: 2, + FirstMessageDeliveriesDecay: scoreDecay(oneHundredEpochs), + FirstMessageDeliveriesCap: 5, + MeshMessageDeliveriesWeight: 0, + MeshMessageDeliveriesDecay: 0, + MeshMessageDeliveriesCap: 0, + MeshMessageDeliveriesThreshold: 0, + MeshMessageDeliveriesWindow: 0, + MeshMessageDeliveriesActivation: 0, + MeshFailurePenaltyWeight: 0, + MeshFailurePenaltyDecay: 0, + InvalidMessageDeliveriesWeight: -2000, + InvalidMessageDeliveriesDecay: scoreDecay(invalidDecayPeriod), + } +} + func oneSlotDuration() time.Duration { return time.Duration(params.BeaconConfig().SecondsPerSlot) * time.Second } diff --git a/beacon-chain/p2p/gossip_topic_mappings.go b/beacon-chain/p2p/gossip_topic_mappings.go index d88a4499ce2b..6eb9922fe907 100644 --- a/beacon-chain/p2p/gossip_topic_mappings.go +++ b/beacon-chain/p2p/gossip_topic_mappings.go @@ -22,6 +22,8 @@ var gossipTopicMappings = map[string]func() proto.Message{ SyncCommitteeSubnetTopicFormat: func() proto.Message { return ðpb.SyncCommitteeMessage{} }, BlsToExecutionChangeSubnetTopicFormat: func() proto.Message { return ðpb.SignedBLSToExecutionChange{} }, BlobSubnetTopicFormat: func() proto.Message { return ðpb.BlobSidecar{} }, + LightClientFinalityUpdateTopicFormat: func() proto.Message { return ðpb.LightClientFinalityUpdateAltair{} }, + LightClientOptimisticUpdateTopicFormat: func() proto.Message { return ðpb.LightClientOptimisticUpdateAltair{} }, } // GossipTopicMappings is a function to return the assigned data type @@ -60,6 +62,25 @@ func GossipTopicMappings(topic string, epoch primitives.Epoch) proto.Message { return ðpb.SignedAggregateAttestationAndProofElectra{} } return gossipMessage(topic) + case LightClientFinalityUpdateTopicFormat: + if epoch >= params.BeaconConfig().ElectraForkEpoch { + return ðpb.LightClientFinalityUpdateElectra{} + } + if epoch >= params.BeaconConfig().DenebForkEpoch { + return ðpb.LightClientFinalityUpdateDeneb{} + } + if epoch >= params.BeaconConfig().CapellaForkEpoch { + return ðpb.LightClientFinalityUpdateCapella{} + } + return gossipMessage(topic) + case LightClientOptimisticUpdateTopicFormat: + if epoch >= params.BeaconConfig().DenebForkEpoch { + return ðpb.LightClientOptimisticUpdateDeneb{} + } + if epoch >= params.BeaconConfig().CapellaForkEpoch { + return ðpb.LightClientOptimisticUpdateCapella{} + } + return gossipMessage(topic) default: return gossipMessage(topic) } diff --git a/beacon-chain/p2p/rpc_topic_mappings.go b/beacon-chain/p2p/rpc_topic_mappings.go index 7d9c5ebdff0a..cd1f811b6476 100644 --- a/beacon-chain/p2p/rpc_topic_mappings.go +++ b/beacon-chain/p2p/rpc_topic_mappings.go @@ -43,6 +43,18 @@ const BlobSidecarsByRangeName = "/blob_sidecars_by_range" // BlobSidecarsByRootName is the name for the BlobSidecarsByRoot v1 message topic. const BlobSidecarsByRootName = "/blob_sidecars_by_root" +// LightClientBootstrapName is the name for the LightClientBootstrap message topic, +const LightClientBootstrapName = "/light_client_bootstrap" + +// LightClientUpdatesByRangeName is the name for the LightClientUpdatesByRange topic. +const LightClientUpdatesByRangeName = "/light_client_updates_by_range" + +// LightClientFinalityUpdateName is the name for the LightClientFinalityUpdate topic. +const LightClientFinalityUpdateName = "/light_client_finality_update" + +// LightClientOptimisticUpdateName is the name for the LightClientOptimisticUpdate topic. +const LightClientOptimisticUpdateName = "/light_client_optimistic_update" + const ( // V1 RPC Topics // RPCStatusTopicV1 defines the v1 topic for the status rpc method. @@ -66,6 +78,18 @@ const ( // /eth2/beacon_chain/req/blob_sidecars_by_root/1/ RPCBlobSidecarsByRootTopicV1 = protocolPrefix + BlobSidecarsByRootName + SchemaVersionV1 + // RPCLightClientBootstrapTopicV1 is a topic for requesting a light client bootstrap. + RPCLightClientBootstrapTopicV1 = protocolPrefix + LightClientBootstrapName + SchemaVersionV1 + + // RPCLightClientUpdatesByRangeTopicV1 is a topic for requesting light client updates by range. + RPCLightClientUpdatesByRangeTopicV1 = protocolPrefix + LightClientUpdatesByRangeName + SchemaVersionV1 + + // RPCLightClientFinalityUpdateTopicV1 is a topic for requesting a light client finality update. + RPCLightClientFinalityUpdateTopicV1 = protocolPrefix + LightClientFinalityUpdateName + SchemaVersionV1 + + // RPCLightClientOptimisticUpdateTopicV1 is a topic for requesting a light client Optimistic update. + RPCLightClientOptimisticUpdateTopicV1 = protocolPrefix + LightClientOptimisticUpdateName + SchemaVersionV1 + // V2 RPC Topics // RPCBlocksByRangeTopicV2 defines v2 the topic for the blocks by range rpc method. RPCBlocksByRangeTopicV2 = protocolPrefix + BeaconBlocksByRangeMessageName + SchemaVersionV2 @@ -101,6 +125,11 @@ var RPCTopicMappings = map[string]interface{}{ RPCBlobSidecarsByRangeTopicV1: new(pb.BlobSidecarsByRangeRequest), // BlobSidecarsByRoot v1 Message RPCBlobSidecarsByRootTopicV1: new(p2ptypes.BlobSidecarsByRootReq), + // Light client + RPCLightClientBootstrapTopicV1: new(p2ptypes.LightClientBootstrapReq), + RPCLightClientUpdatesByRangeTopicV1: new(pb.LightClientUpdatesByRangeReq), + RPCLightClientFinalityUpdateTopicV1: new(interface{}), + RPCLightClientOptimisticUpdateTopicV1: new(interface{}), } // Maps all registered protocol prefixes. @@ -111,14 +140,18 @@ var protocolMapping = map[string]bool{ // Maps all the protocol message names for the different rpc // topics. var messageMapping = map[string]bool{ - StatusMessageName: true, - GoodbyeMessageName: true, - BeaconBlocksByRangeMessageName: true, - BeaconBlocksByRootsMessageName: true, - PingMessageName: true, - MetadataMessageName: true, - BlobSidecarsByRangeName: true, - BlobSidecarsByRootName: true, + StatusMessageName: true, + GoodbyeMessageName: true, + BeaconBlocksByRangeMessageName: true, + BeaconBlocksByRootsMessageName: true, + PingMessageName: true, + MetadataMessageName: true, + BlobSidecarsByRangeName: true, + BlobSidecarsByRootName: true, + LightClientBootstrapName: true, + LightClientUpdatesByRangeName: true, + LightClientFinalityUpdateName: true, + LightClientOptimisticUpdateName: true, } // Maps all the RPC messages which are to updated in altair. diff --git a/beacon-chain/p2p/topics.go b/beacon-chain/p2p/topics.go index 3187e36a5cfe..966c5c4183be 100644 --- a/beacon-chain/p2p/topics.go +++ b/beacon-chain/p2p/topics.go @@ -30,6 +30,10 @@ const ( GossipBlsToExecutionChangeMessage = "bls_to_execution_change" // GossipBlobSidecarMessage is the name for the blob sidecar message type. GossipBlobSidecarMessage = "blob_sidecar" + // GossipLightClientFinalityUpdateMessage is the name for the light client finality update message type. + GossipLightClientFinalityUpdateMessage = "light_client_finality_update" + // GossipLightClientOptimisticUpdateMessage is the name for the light client optimistic update message type. + GossipLightClientOptimisticUpdateMessage = "light_client_optimistic_update" // Topic Formats // // AttestationSubnetTopicFormat is the topic format for the attestation subnet. @@ -52,4 +56,8 @@ const ( BlsToExecutionChangeSubnetTopicFormat = GossipProtocolAndDigest + GossipBlsToExecutionChangeMessage // BlobSubnetTopicFormat is the topic format for the blob subnet. BlobSubnetTopicFormat = GossipProtocolAndDigest + GossipBlobSidecarMessage + "_%d" + // LightClientFinalityUpdateTopicFormat is the topic format for the light client finality update subnet. + LightClientFinalityUpdateTopicFormat = GossipProtocolAndDigest + GossipLightClientFinalityUpdateMessage + // LightClientOptimisticUpdateTopicFormat is the topic format for the light client optimistic update subnet. + LightClientOptimisticUpdateTopicFormat = GossipProtocolAndDigest + GossipLightClientOptimisticUpdateMessage ) diff --git a/beacon-chain/p2p/types/BUILD.bazel b/beacon-chain/p2p/types/BUILD.bazel index a66fcefd9e09..2ecf4d8166ee 100644 --- a/beacon-chain/p2p/types/BUILD.bazel +++ b/beacon-chain/p2p/types/BUILD.bazel @@ -17,6 +17,7 @@ go_library( "//validator/client:__pkg__", ], deps = [ + "//config/fieldparams:go_default_library", "//config/params:go_default_library", "//consensus-types/blocks:go_default_library", "//consensus-types/interfaces:go_default_library", diff --git a/beacon-chain/p2p/types/rpc_errors.go b/beacon-chain/p2p/types/rpc_errors.go index 46381876c118..2ca23d120e91 100644 --- a/beacon-chain/p2p/types/rpc_errors.go +++ b/beacon-chain/p2p/types/rpc_errors.go @@ -5,6 +5,7 @@ import "errors" var ( ErrWrongForkDigestVersion = errors.New("wrong fork digest version") ErrInvalidEpoch = errors.New("invalid epoch") + ErrInvalidRoot = errors.New("invalid root") ErrInvalidFinalizedRoot = errors.New("invalid finalized root") ErrInvalidSequenceNum = errors.New("invalid sequence number provided") ErrGeneric = errors.New("internal service error") diff --git a/beacon-chain/p2p/types/types.go b/beacon-chain/p2p/types/types.go index 2ccda62f326d..537050a71409 100644 --- a/beacon-chain/p2p/types/types.go +++ b/beacon-chain/p2p/types/types.go @@ -9,13 +9,15 @@ import ( "github.com/pkg/errors" ssz "github.com/prysmaticlabs/fastssz" + fieldparams "github.com/prysmaticlabs/prysm/v5/config/fieldparams" "github.com/prysmaticlabs/prysm/v5/config/params" eth "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1" ) -const rootLength = 32 - -const maxErrorLength = 256 +const ( + maxErrorLength = 256 + maxLightClientUpdates = 16 +) // SSZBytes is a bytes slice that satisfies the fast-ssz interface. type SSZBytes []byte @@ -34,7 +36,7 @@ func (b *SSZBytes) HashTreeRootWith(hh *ssz.Hasher) error { } // BeaconBlockByRootsReq specifies the block by roots request type. -type BeaconBlockByRootsReq [][rootLength]byte +type BeaconBlockByRootsReq [][fieldparams.RootLength]byte // MarshalSSZTo marshals the block by roots request with the provided byte slice. func (r *BeaconBlockByRootsReq) MarshalSSZTo(dst []byte) ([]byte, error) { @@ -59,25 +61,25 @@ func (r *BeaconBlockByRootsReq) MarshalSSZ() ([]byte, error) { // SizeSSZ returns the size of the serialized representation. func (r *BeaconBlockByRootsReq) SizeSSZ() int { - return len(*r) * rootLength + return len(*r) * fieldparams.RootLength } // UnmarshalSSZ unmarshals the provided bytes buffer into the // block by roots request object. func (r *BeaconBlockByRootsReq) UnmarshalSSZ(buf []byte) error { bufLen := len(buf) - maxLength := int(params.BeaconConfig().MaxRequestBlocks * rootLength) + maxLength := int(params.BeaconConfig().MaxRequestBlocks * fieldparams.RootLength) if bufLen > maxLength { return errors.Errorf("expected buffer with length of up to %d but received length %d", maxLength, bufLen) } - if bufLen%rootLength != 0 { + if bufLen%fieldparams.RootLength != 0 { return ssz.ErrIncorrectByteSize } - numOfRoots := bufLen / rootLength - roots := make([][rootLength]byte, 0, numOfRoots) + numOfRoots := bufLen / fieldparams.RootLength + roots := make([][fieldparams.RootLength]byte, 0, numOfRoots) for i := 0; i < numOfRoots; i++ { - var rt [rootLength]byte - copy(rt[:], buf[i*rootLength:(i+1)*rootLength]) + var rt [fieldparams.RootLength]byte + copy(rt[:], buf[i*fieldparams.RootLength:(i+1)*fieldparams.RootLength]) roots = append(roots, rt) } *r = roots @@ -211,3 +213,36 @@ func init() { sizer := ð.BlobIdentifier{} blobIdSize = sizer.SizeSSZ() } + +// LightClientBootstrapReq specifies the light client bootstrap request type. +type LightClientBootstrapReq [fieldparams.RootLength]byte + +// MarshalSSZTo marshals the block by roots request with the provided byte slice. +func (r *LightClientBootstrapReq) MarshalSSZTo(dst []byte) ([]byte, error) { + marshalledObj, err := r.MarshalSSZ() + if err != nil { + return nil, err + } + return append(dst, marshalledObj...), nil +} + +// MarshalSSZ Marshals the block by roots request type into the serialized object. +func (r *LightClientBootstrapReq) MarshalSSZ() ([]byte, error) { + return r[:], nil +} + +// SizeSSZ returns the size of the serialized representation. +func (r *LightClientBootstrapReq) SizeSSZ() int { + return fieldparams.RootLength +} + +// UnmarshalSSZ unmarshals the provided bytes buffer into the +// block by roots request object. +func (r *LightClientBootstrapReq) UnmarshalSSZ(buf []byte) error { + bufLen := len(buf) + if bufLen != fieldparams.RootLength { + return errors.Errorf("expected buffer with length of %d but received length %d", fieldparams.RootLength, bufLen) + } + copy(r[:], buf) + return nil +} diff --git a/beacon-chain/rpc/BUILD.bazel b/beacon-chain/rpc/BUILD.bazel index dbb213a65597..19cc6384ed66 100644 --- a/beacon-chain/rpc/BUILD.bazel +++ b/beacon-chain/rpc/BUILD.bazel @@ -22,6 +22,7 @@ go_library( "//beacon-chain/db:go_default_library", "//beacon-chain/db/filesystem:go_default_library", "//beacon-chain/execution:go_default_library", + "//beacon-chain/light-client:go_default_library", "//beacon-chain/operations/attestations:go_default_library", "//beacon-chain/operations/blstoexec:go_default_library", "//beacon-chain/operations/slashings:go_default_library", diff --git a/beacon-chain/rpc/endpoints.go b/beacon-chain/rpc/endpoints.go index 9dae9c032a31..c87068e0d49f 100644 --- a/beacon-chain/rpc/endpoints.go +++ b/beacon-chain/rpc/endpoints.go @@ -872,6 +872,7 @@ func (s *Service) lightClientEndpoints(blocker lookup.Blocker, stater lookup.Sta HeadFetcher: s.cfg.HeadFetcher, ChainInfoFetcher: s.cfg.ChainInfoFetcher, BeaconDB: s.cfg.BeaconDB, + LCStore: s.cfg.LCStore, } const namespace = "lightclient" diff --git a/beacon-chain/rpc/eth/events/BUILD.bazel b/beacon-chain/rpc/eth/events/BUILD.bazel index da308f797853..44f2f7116e50 100644 --- a/beacon-chain/rpc/eth/events/BUILD.bazel +++ b/beacon-chain/rpc/eth/events/BUILD.bazel @@ -26,7 +26,6 @@ go_library( "//monitoring/tracing/trace:go_default_library", "//network/httputil:go_default_library", "//proto/eth/v1:go_default_library", - "//proto/eth/v2:go_default_library", "//proto/prysm/v1alpha1:go_default_library", "//runtime/version:go_default_library", "//time/slots:go_default_library", diff --git a/beacon-chain/rpc/eth/events/events.go b/beacon-chain/rpc/eth/events/events.go index 173a2017681d..5d9a825fabc3 100644 --- a/beacon-chain/rpc/eth/events/events.go +++ b/beacon-chain/rpc/eth/events/events.go @@ -25,7 +25,6 @@ import ( "github.com/prysmaticlabs/prysm/v5/monitoring/tracing/trace" "github.com/prysmaticlabs/prysm/v5/network/httputil" ethpb "github.com/prysmaticlabs/prysm/v5/proto/eth/v1" - ethpbv2 "github.com/prysmaticlabs/prysm/v5/proto/eth/v2" eth "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1" "github.com/prysmaticlabs/prysm/v5/runtime/version" "github.com/prysmaticlabs/prysm/v5/time/slots" @@ -411,9 +410,9 @@ func topicForEvent(event *feed.Event) string { return HeadTopic case *ethpb.EventFinalizedCheckpoint: return FinalizedCheckpointTopic - case *ethpbv2.LightClientFinalityUpdateWithVersion: + case interfaces.LightClientFinalityUpdate: return LightClientFinalityUpdateTopic - case *ethpbv2.LightClientOptimisticUpdateWithVersion: + case interfaces.LightClientOptimisticUpdate: return LightClientOptimisticUpdateTopic case *ethpb.EventChainReorg: return ChainReorgTopic diff --git a/beacon-chain/rpc/eth/light-client/BUILD.bazel b/beacon-chain/rpc/eth/light-client/BUILD.bazel index f4bf65404a28..c756fa3e2abf 100644 --- a/beacon-chain/rpc/eth/light-client/BUILD.bazel +++ b/beacon-chain/rpc/eth/light-client/BUILD.bazel @@ -15,17 +15,16 @@ go_library( "//beacon-chain/blockchain:go_default_library", "//beacon-chain/core/light-client:go_default_library", "//beacon-chain/db:go_default_library", + "//beacon-chain/light-client:go_default_library", "//beacon-chain/rpc/eth/shared:go_default_library", "//beacon-chain/rpc/lookup:go_default_library", "//beacon-chain/state:go_default_library", - "//config/fieldparams:go_default_library", "//config/params:go_default_library", "//consensus-types/interfaces:go_default_library", "//consensus-types/primitives:go_default_library", "//monitoring/tracing/trace:go_default_library", "//network/httputil:go_default_library", "//runtime/version:go_default_library", - "//time/slots:go_default_library", "@com_github_ethereum_go_ethereum//common/hexutil:go_default_library", "@com_github_pkg_errors//:go_default_library", "@com_github_wealdtech_go_bytesutil//:go_default_library", diff --git a/beacon-chain/rpc/eth/light-client/handlers.go b/beacon-chain/rpc/eth/light-client/handlers.go index 91746c5db135..b61fa9cde413 100644 --- a/beacon-chain/rpc/eth/light-client/handlers.go +++ b/beacon-chain/rpc/eth/light-client/handlers.go @@ -3,13 +3,13 @@ package lightclient import ( "context" "fmt" - "math" "net/http" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/pkg/errors" "github.com/prysmaticlabs/prysm/v5/api" "github.com/prysmaticlabs/prysm/v5/api/server/structs" + lightclient "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/light-client" "github.com/prysmaticlabs/prysm/v5/beacon-chain/rpc/eth/shared" "github.com/prysmaticlabs/prysm/v5/config/params" "github.com/prysmaticlabs/prysm/v5/consensus-types/interfaces" @@ -45,18 +45,33 @@ func (s *Server) GetLightClientBootstrap(w http.ResponseWriter, req *http.Reques return } - bootstrap, err := createLightClientBootstrap(ctx, s.ChainInfoFetcher.CurrentSlot(), state, blk) + bootstrap, err := lightclient.NewLightClientBootstrapFromBeaconState(ctx, s.ChainInfoFetcher.CurrentSlot(), state, blk) if err != nil { httputil.HandleError(w, "could not get light client bootstrap: "+err.Error(), http.StatusInternalServerError) return } - response := &structs.LightClientBootstrapResponse{ - Version: version.String(blk.Version()), - Data: bootstrap, - } - w.Header().Set(api.VersionHeader, version.String(version.Deneb)) - httputil.WriteJson(w, response) + w.Header().Set(api.VersionHeader, version.String(bootstrap.Version())) + + if httputil.RespondWithSsz(req) { + ssz, err := bootstrap.MarshalSSZ() + if err != nil { + httputil.HandleError(w, "could not marshal bootstrap to SSZ: "+err.Error(), http.StatusInternalServerError) + return + } + httputil.WriteSsz(w, ssz, "light_client_bootstrap.ssz") + } else { + data, err := structs.LightClientBootsrapFromConsensus(bootstrap) + if err != nil { + httputil.HandleError(w, "could not marshal bootstrap to JSON: "+err.Error(), http.StatusInternalServerError) + return + } + response := &structs.LightClientBootstrapResponse{ + Version: version.String(bootstrap.Version()), + Data: data, + } + httputil.WriteJson(w, response) + } } // GetLightClientUpdatesByRange - implements https://github.com/ethereum/beacon-APIs/blob/263f4ed6c263c967f13279c7a9f5629b51c5fc55/apis/beacon/light_client/updates.yaml @@ -150,105 +165,70 @@ func (s *Server) GetLightClientUpdatesByRange(w http.ResponseWriter, req *http.R // GetLightClientFinalityUpdate - implements https://github.com/ethereum/beacon-APIs/blob/263f4ed6c263c967f13279c7a9f5629b51c5fc55/apis/beacon/light_client/finality_update.yaml func (s *Server) GetLightClientFinalityUpdate(w http.ResponseWriter, req *http.Request) { - ctx, span := trace.StartSpan(req.Context(), "beacon.GetLightClientFinalityUpdate") + _, span := trace.StartSpan(req.Context(), "beacon.GetLightClientFinalityUpdate") defer span.End() - // Finality update needs super majority of sync committee signatures - minSyncCommitteeParticipants := float64(params.BeaconConfig().MinSyncCommitteeParticipants) - minSignatures := uint64(math.Ceil(minSyncCommitteeParticipants * 2 / 3)) - - block, err := s.suitableBlock(ctx, minSignatures) - if !shared.WriteBlockFetchError(w, block, err) { + update := s.LCStore.LastLCFinalityUpdate + if update == nil { + httputil.HandleError(w, "No light client finality update available", http.StatusNotFound) return } - st, err := s.Stater.StateBySlot(ctx, block.Block().Slot()) - if err != nil { - httputil.HandleError(w, "Could not get state: "+err.Error(), http.StatusInternalServerError) - return - } + w.Header().Set(api.VersionHeader, version.String(update.Version())) - attestedRoot := block.Block().ParentRoot() - attestedBlock, err := s.Blocker.Block(ctx, attestedRoot[:]) - if !shared.WriteBlockFetchError(w, block, errors.Wrap(err, "could not get attested block")) { - return - } - attestedSlot := attestedBlock.Block().Slot() - attestedState, err := s.Stater.StateBySlot(ctx, attestedSlot) - if err != nil { - httputil.HandleError(w, "Could not get attested state: "+err.Error(), http.StatusInternalServerError) - return - } - - var finalizedBlock interfaces.ReadOnlySignedBeaconBlock - finalizedCheckpoint := attestedState.FinalizedCheckpoint() - if finalizedCheckpoint == nil { - httputil.HandleError(w, "Attested state does not have a finalized checkpoint", http.StatusInternalServerError) - return - } - finalizedRoot := bytesutil.ToBytes32(finalizedCheckpoint.Root) - finalizedBlock, err = s.Blocker.Block(ctx, finalizedRoot[:]) - if !shared.WriteBlockFetchError(w, block, errors.Wrap(err, "could not get finalized block")) { - return - } - - update, err := newLightClientFinalityUpdateFromBeaconState(ctx, s.ChainInfoFetcher.CurrentSlot(), st, block, attestedState, attestedBlock, finalizedBlock) - if err != nil { - httputil.HandleError(w, "Could not get light client finality update: "+err.Error(), http.StatusInternalServerError) - return - } - - response := &structs.LightClientFinalityUpdateResponse{ - Version: version.String(attestedState.Version()), - Data: update, + if httputil.RespondWithSsz(req) { + ssz, err := update.MarshalSSZ() + if err != nil { + httputil.HandleError(w, "could not marshal update to SSZ: "+err.Error(), http.StatusInternalServerError) + return + } + httputil.WriteSsz(w, ssz, "light_client_finality_update.ssz") + } else { + data, err := structs.LightClientFinalityUpdateFromConsensus(update) + if err != nil { + httputil.HandleError(w, "could not marshal update to JSON: "+err.Error(), http.StatusInternalServerError) + return + } + response := &structs.LightClientFinalityUpdateResponse{ + Version: version.String(update.Version()), + Data: data, + } + httputil.WriteJson(w, response) } - - httputil.WriteJson(w, response) } // GetLightClientOptimisticUpdate - implements https://github.com/ethereum/beacon-APIs/blob/263f4ed6c263c967f13279c7a9f5629b51c5fc55/apis/beacon/light_client/optimistic_update.yaml func (s *Server) GetLightClientOptimisticUpdate(w http.ResponseWriter, req *http.Request) { - ctx, span := trace.StartSpan(req.Context(), "beacon.GetLightClientOptimisticUpdate") + _, span := trace.StartSpan(req.Context(), "beacon.GetLightClientOptimisticUpdate") defer span.End() - block, err := s.suitableBlock(ctx, params.BeaconConfig().MinSyncCommitteeParticipants) - if !shared.WriteBlockFetchError(w, block, err) { - return - } - st, err := s.Stater.StateBySlot(ctx, block.Block().Slot()) - if err != nil { - httputil.HandleError(w, "could not get state: "+err.Error(), http.StatusInternalServerError) - return - } - attestedRoot := block.Block().ParentRoot() - attestedBlock, err := s.Blocker.Block(ctx, attestedRoot[:]) - if err != nil { - httputil.HandleError(w, "Could not get attested block: "+err.Error(), http.StatusInternalServerError) - return - } - if attestedBlock == nil { - httputil.HandleError(w, "Attested block is nil", http.StatusInternalServerError) - return - } - attestedSlot := attestedBlock.Block().Slot() - attestedState, err := s.Stater.StateBySlot(ctx, attestedSlot) - if err != nil { - httputil.HandleError(w, "Could not get attested state: "+err.Error(), http.StatusInternalServerError) + update := s.LCStore.LastLCOptimisticUpdate + if update == nil { + httputil.HandleError(w, "No light client optimistic update available", http.StatusNotFound) return } - update, err := newLightClientOptimisticUpdateFromBeaconState(ctx, s.ChainInfoFetcher.CurrentSlot(), st, block, attestedState, attestedBlock) - if err != nil { - httputil.HandleError(w, "Could not get light client optimistic update: "+err.Error(), http.StatusInternalServerError) - return - } + w.Header().Set(api.VersionHeader, version.String(update.Version())) - response := &structs.LightClientOptimisticUpdateResponse{ - Version: version.String(attestedState.Version()), - Data: update, + if httputil.RespondWithSsz(req) { + ssz, err := update.MarshalSSZ() + if err != nil { + httputil.HandleError(w, "could not marshal update to SSZ: "+err.Error(), http.StatusInternalServerError) + return + } + httputil.WriteSsz(w, ssz, "light_client_optimistic_update.ssz") + } else { + data, err := structs.LightClientOptimisticUpdateFromConsensus(update) + if err != nil { + httputil.HandleError(w, "could not marshal update to JSON: "+err.Error(), http.StatusInternalServerError) + return + } + response := &structs.LightClientOptimisticUpdateResponse{ + Version: version.String(update.Version()), + Data: data, + } + httputil.WriteJson(w, response) } - - httputil.WriteJson(w, response) } // suitableBlock returns the latest block that satisfies all criteria required for creating a new update diff --git a/beacon-chain/rpc/eth/light-client/handlers_test.go b/beacon-chain/rpc/eth/light-client/handlers_test.go index 0e91f38e4031..984cd7add016 100644 --- a/beacon-chain/rpc/eth/light-client/handlers_test.go +++ b/beacon-chain/rpc/eth/light-client/handlers_test.go @@ -32,214 +32,221 @@ import ( "github.com/prysmaticlabs/prysm/v5/testing/util" ) -func TestLightClientHandler_GetLightClientBootstrap_Altair(t *testing.T) { - l := util.NewTestLightClient(t).SetupTestAltair() - - slot := l.State.Slot() - stateRoot, err := l.State.HashTreeRoot(l.Ctx) - require.NoError(t, err) - - mockBlocker := &testutil.MockBlocker{BlockToReturn: l.Block} - mockChainService := &mock.ChainService{Optimistic: true, Slot: &slot} - mockChainInfoFetcher := &mock.ChainService{Slot: &slot} - s := &Server{ - Stater: &testutil.MockStater{StatesBySlot: map[primitives.Slot]state.BeaconState{ - slot: l.State, - }}, - Blocker: mockBlocker, - HeadFetcher: mockChainService, - ChainInfoFetcher: mockChainInfoFetcher, - } - request := httptest.NewRequest("GET", "http://foo.com/", nil) - request.SetPathValue("block_root", hexutil.Encode(stateRoot[:])) - writer := httptest.NewRecorder() - writer.Body = &bytes.Buffer{} - - s.GetLightClientBootstrap(writer, request) - require.Equal(t, http.StatusOK, writer.Code) - var resp structs.LightClientBootstrapResponse - err = json.Unmarshal(writer.Body.Bytes(), &resp) - require.NoError(t, err) - var respHeader structs.LightClientHeader - err = json.Unmarshal(resp.Data.Header, &respHeader) - require.NoError(t, err) - require.Equal(t, "altair", resp.Version) - - blockHeader, err := l.Block.Header() - require.NoError(t, err) - require.Equal(t, hexutil.Encode(blockHeader.Header.BodyRoot), respHeader.Beacon.BodyRoot) - require.Equal(t, strconv.FormatUint(uint64(blockHeader.Header.Slot), 10), respHeader.Beacon.Slot) - - require.NotNil(t, resp.Data.CurrentSyncCommittee) - require.NotNil(t, resp.Data.CurrentSyncCommitteeBranch) -} +func TestLightClientHandler_GetLightClientBootstrap(t *testing.T) { + params.SetupTestConfigCleanup(t) + cfg := params.BeaconConfig() + cfg.AltairForkEpoch = 0 + cfg.BellatrixForkEpoch = 1 + cfg.CapellaForkEpoch = 2 + cfg.DenebForkEpoch = 3 + cfg.ElectraForkEpoch = 4 + params.OverrideBeaconConfig(cfg) + + t.Run("altair", func(t *testing.T) { + l := util.NewTestLightClient(t).SetupTestAltair() + + slot := primitives.Slot(params.BeaconConfig().AltairForkEpoch * primitives.Epoch(params.BeaconConfig().SlotsPerEpoch)).Add(1) + stateRoot, err := l.State.HashTreeRoot(l.Ctx) + require.NoError(t, err) -func TestLightClientHandler_GetLightClientBootstrap_Bellatrix(t *testing.T) { - l := util.NewTestLightClient(t).SetupTestBellatrix() + mockBlocker := &testutil.MockBlocker{BlockToReturn: l.Block} + mockChainService := &mock.ChainService{Optimistic: true, Slot: &slot} + mockChainInfoFetcher := &mock.ChainService{Slot: &slot} + s := &Server{ + Stater: &testutil.MockStater{StatesBySlot: map[primitives.Slot]state.BeaconState{ + slot: l.State, + }}, + Blocker: mockBlocker, + HeadFetcher: mockChainService, + ChainInfoFetcher: mockChainInfoFetcher, + } + request := httptest.NewRequest("GET", "http://foo.com/", nil) + request.SetPathValue("block_root", hexutil.Encode(stateRoot[:])) + writer := httptest.NewRecorder() + writer.Body = &bytes.Buffer{} - slot := l.State.Slot() - stateRoot, err := l.State.HashTreeRoot(l.Ctx) - require.NoError(t, err) + s.GetLightClientBootstrap(writer, request) + require.Equal(t, http.StatusOK, writer.Code) + var resp structs.LightClientBootstrapResponse + err = json.Unmarshal(writer.Body.Bytes(), &resp) + require.NoError(t, err) + var respHeader structs.LightClientHeader + err = json.Unmarshal(resp.Data.Header, &respHeader) + require.NoError(t, err) + require.Equal(t, "altair", resp.Version) - mockBlocker := &testutil.MockBlocker{BlockToReturn: l.Block} - mockChainService := &mock.ChainService{Optimistic: true, Slot: &slot} - mockChainInfoFetcher := &mock.ChainService{Slot: &slot} - s := &Server{ - Stater: &testutil.MockStater{StatesBySlot: map[primitives.Slot]state.BeaconState{ - slot: l.State, - }}, - Blocker: mockBlocker, - HeadFetcher: mockChainService, - ChainInfoFetcher: mockChainInfoFetcher, - } - request := httptest.NewRequest("GET", "http://foo.com/", nil) - request.SetPathValue("block_root", hexutil.Encode(stateRoot[:])) - writer := httptest.NewRecorder() - writer.Body = &bytes.Buffer{} + blockHeader, err := l.Block.Header() + require.NoError(t, err) + require.Equal(t, hexutil.Encode(blockHeader.Header.BodyRoot), respHeader.Beacon.BodyRoot) + require.Equal(t, strconv.FormatUint(uint64(blockHeader.Header.Slot), 10), respHeader.Beacon.Slot) - s.GetLightClientBootstrap(writer, request) - require.Equal(t, http.StatusOK, writer.Code) - var resp structs.LightClientBootstrapResponse - err = json.Unmarshal(writer.Body.Bytes(), &resp) - require.NoError(t, err) - var respHeader structs.LightClientHeader - err = json.Unmarshal(resp.Data.Header, &respHeader) - require.NoError(t, err) - require.Equal(t, "bellatrix", resp.Version) + require.NotNil(t, resp.Data.CurrentSyncCommittee) + require.NotNil(t, resp.Data.CurrentSyncCommitteeBranch) + }) + t.Run("bellatrix", func(t *testing.T) { + l := util.NewTestLightClient(t).SetupTestBellatrix() - blockHeader, err := l.Block.Header() - require.NoError(t, err) - require.Equal(t, hexutil.Encode(blockHeader.Header.BodyRoot), respHeader.Beacon.BodyRoot) - require.Equal(t, strconv.FormatUint(uint64(blockHeader.Header.Slot), 10), respHeader.Beacon.Slot) + slot := primitives.Slot(params.BeaconConfig().BellatrixForkEpoch * primitives.Epoch(params.BeaconConfig().SlotsPerEpoch)).Add(1) + blockRoot, err := l.Block.Block().HashTreeRoot() + require.NoError(t, err) - require.NotNil(t, resp.Data.CurrentSyncCommittee) - require.NotNil(t, resp.Data.CurrentSyncCommitteeBranch) -} + mockBlocker := &testutil.MockBlocker{BlockToReturn: l.Block} + mockChainService := &mock.ChainService{Optimistic: true, Slot: &slot} + mockChainInfoFetcher := &mock.ChainService{Slot: &slot} + s := &Server{ + Stater: &testutil.MockStater{StatesBySlot: map[primitives.Slot]state.BeaconState{ + slot: l.State, + }}, + Blocker: mockBlocker, + HeadFetcher: mockChainService, + ChainInfoFetcher: mockChainInfoFetcher, + } + request := httptest.NewRequest("GET", "http://foo.com/", nil) + request.SetPathValue("block_root", hexutil.Encode(blockRoot[:])) + writer := httptest.NewRecorder() + writer.Body = &bytes.Buffer{} -func TestLightClientHandler_GetLightClientBootstrap_Capella(t *testing.T) { - l := util.NewTestLightClient(t).SetupTestCapella(false) // result is same for true and false + s.GetLightClientBootstrap(writer, request) + require.Equal(t, http.StatusOK, writer.Code) + var resp structs.LightClientBootstrapResponse + err = json.Unmarshal(writer.Body.Bytes(), &resp) + require.NoError(t, err) + var respHeader structs.LightClientHeader + err = json.Unmarshal(resp.Data.Header, &respHeader) + require.NoError(t, err) + require.Equal(t, "altair", resp.Version) - slot := l.State.Slot() - stateRoot, err := l.State.HashTreeRoot(l.Ctx) - require.NoError(t, err) + blockHeader, err := l.Block.Header() + require.NoError(t, err) + require.Equal(t, hexutil.Encode(blockHeader.Header.BodyRoot), respHeader.Beacon.BodyRoot) + require.Equal(t, strconv.FormatUint(uint64(blockHeader.Header.Slot), 10), respHeader.Beacon.Slot) - mockBlocker := &testutil.MockBlocker{BlockToReturn: l.Block} - mockChainService := &mock.ChainService{Optimistic: true, Slot: &slot} - mockChainInfoFetcher := &mock.ChainService{Slot: &slot} - s := &Server{ - Stater: &testutil.MockStater{StatesBySlot: map[primitives.Slot]state.BeaconState{ - slot: l.State, - }}, - Blocker: mockBlocker, - HeadFetcher: mockChainService, - ChainInfoFetcher: mockChainInfoFetcher, - } - request := httptest.NewRequest("GET", "http://foo.com/", nil) - request.SetPathValue("block_root", hexutil.Encode(stateRoot[:])) - writer := httptest.NewRecorder() - writer.Body = &bytes.Buffer{} + require.NotNil(t, resp.Data.CurrentSyncCommittee) + require.NotNil(t, resp.Data.CurrentSyncCommitteeBranch) + }) + t.Run("capella", func(t *testing.T) { + l := util.NewTestLightClient(t).SetupTestCapella(false) // result is same for true and false - s.GetLightClientBootstrap(writer, request) - require.Equal(t, http.StatusOK, writer.Code) - var resp structs.LightClientBootstrapResponse - err = json.Unmarshal(writer.Body.Bytes(), &resp) - require.NoError(t, err) - var respHeader structs.LightClientHeader - err = json.Unmarshal(resp.Data.Header, &respHeader) - require.NoError(t, err) - require.Equal(t, "capella", resp.Version) + slot := primitives.Slot(params.BeaconConfig().CapellaForkEpoch * primitives.Epoch(params.BeaconConfig().SlotsPerEpoch)).Add(1) + blockRoot, err := l.Block.Block().HashTreeRoot() + require.NoError(t, err) - blockHeader, err := l.Block.Header() - require.NoError(t, err) - require.Equal(t, hexutil.Encode(blockHeader.Header.BodyRoot), respHeader.Beacon.BodyRoot) - require.Equal(t, strconv.FormatUint(uint64(blockHeader.Header.Slot), 10), respHeader.Beacon.Slot) + mockBlocker := &testutil.MockBlocker{BlockToReturn: l.Block} + mockChainService := &mock.ChainService{Optimistic: true, Slot: &slot} + mockChainInfoFetcher := &mock.ChainService{Slot: &slot} + s := &Server{ + Stater: &testutil.MockStater{StatesBySlot: map[primitives.Slot]state.BeaconState{ + slot: l.State, + }}, + Blocker: mockBlocker, + HeadFetcher: mockChainService, + ChainInfoFetcher: mockChainInfoFetcher, + } + request := httptest.NewRequest("GET", "http://foo.com/", nil) + request.SetPathValue("block_root", hexutil.Encode(blockRoot[:])) + writer := httptest.NewRecorder() + writer.Body = &bytes.Buffer{} - require.NotNil(t, resp.Data.CurrentSyncCommittee) - require.NotNil(t, resp.Data.CurrentSyncCommitteeBranch) -} + s.GetLightClientBootstrap(writer, request) + require.Equal(t, http.StatusOK, writer.Code) + var resp structs.LightClientBootstrapResponse + err = json.Unmarshal(writer.Body.Bytes(), &resp) + require.NoError(t, err) + var respHeader structs.LightClientHeader + err = json.Unmarshal(resp.Data.Header, &respHeader) + require.NoError(t, err) + require.Equal(t, "capella", resp.Version) -func TestLightClientHandler_GetLightClientBootstrap_Deneb(t *testing.T) { - l := util.NewTestLightClient(t).SetupTestDeneb(false) // result is same for true and false + blockHeader, err := l.Block.Header() + require.NoError(t, err) + require.Equal(t, hexutil.Encode(blockHeader.Header.BodyRoot), respHeader.Beacon.BodyRoot) + require.Equal(t, strconv.FormatUint(uint64(blockHeader.Header.Slot), 10), respHeader.Beacon.Slot) - slot := l.State.Slot() - stateRoot, err := l.State.HashTreeRoot(l.Ctx) - require.NoError(t, err) + require.NotNil(t, resp.Data.CurrentSyncCommittee) + require.NotNil(t, resp.Data.CurrentSyncCommitteeBranch) + }) + t.Run("deneb", func(t *testing.T) { + l := util.NewTestLightClient(t).SetupTestDeneb(false) // result is same for true and false - mockBlocker := &testutil.MockBlocker{BlockToReturn: l.Block} - mockChainService := &mock.ChainService{Optimistic: true, Slot: &slot} - mockChainInfoFetcher := &mock.ChainService{Slot: &slot} - s := &Server{ - Stater: &testutil.MockStater{StatesBySlot: map[primitives.Slot]state.BeaconState{ - slot: l.State, - }}, - Blocker: mockBlocker, - HeadFetcher: mockChainService, - ChainInfoFetcher: mockChainInfoFetcher, - } - request := httptest.NewRequest("GET", "http://foo.com/", nil) - request.SetPathValue("block_root", hexutil.Encode(stateRoot[:])) - writer := httptest.NewRecorder() - writer.Body = &bytes.Buffer{} + slot := primitives.Slot(params.BeaconConfig().DenebForkEpoch * primitives.Epoch(params.BeaconConfig().SlotsPerEpoch)).Add(1) + blockRoot, err := l.Block.Block().HashTreeRoot() + require.NoError(t, err) - s.GetLightClientBootstrap(writer, request) - require.Equal(t, http.StatusOK, writer.Code) - var resp structs.LightClientBootstrapResponse - err = json.Unmarshal(writer.Body.Bytes(), &resp) - require.NoError(t, err) - var respHeader structs.LightClientHeader - err = json.Unmarshal(resp.Data.Header, &respHeader) - require.NoError(t, err) - require.Equal(t, "deneb", resp.Version) + mockBlocker := &testutil.MockBlocker{BlockToReturn: l.Block} + mockChainService := &mock.ChainService{Optimistic: true, Slot: &slot} + mockChainInfoFetcher := &mock.ChainService{Slot: &slot} + s := &Server{ + Stater: &testutil.MockStater{StatesBySlot: map[primitives.Slot]state.BeaconState{ + slot: l.State, + }}, + Blocker: mockBlocker, + HeadFetcher: mockChainService, + ChainInfoFetcher: mockChainInfoFetcher, + } + request := httptest.NewRequest("GET", "http://foo.com/", nil) + request.SetPathValue("block_root", hexutil.Encode(blockRoot[:])) + writer := httptest.NewRecorder() + writer.Body = &bytes.Buffer{} - blockHeader, err := l.Block.Header() - require.NoError(t, err) - require.Equal(t, hexutil.Encode(blockHeader.Header.BodyRoot), respHeader.Beacon.BodyRoot) - require.Equal(t, strconv.FormatUint(uint64(blockHeader.Header.Slot), 10), respHeader.Beacon.Slot) + s.GetLightClientBootstrap(writer, request) + require.Equal(t, http.StatusOK, writer.Code) + var resp structs.LightClientBootstrapResponse + err = json.Unmarshal(writer.Body.Bytes(), &resp) + require.NoError(t, err) + var respHeader structs.LightClientHeader + err = json.Unmarshal(resp.Data.Header, &respHeader) + require.NoError(t, err) + require.Equal(t, "deneb", resp.Version) - require.NotNil(t, resp.Data.CurrentSyncCommittee) - require.NotNil(t, resp.Data.CurrentSyncCommitteeBranch) -} + blockHeader, err := l.Block.Header() + require.NoError(t, err) + require.Equal(t, hexutil.Encode(blockHeader.Header.BodyRoot), respHeader.Beacon.BodyRoot) + require.Equal(t, strconv.FormatUint(uint64(blockHeader.Header.Slot), 10), respHeader.Beacon.Slot) -func TestLightClientHandler_GetLightClientBootstrap_Electra(t *testing.T) { - l := util.NewTestLightClient(t).SetupTestElectra(false) // result is same for true and false + require.NotNil(t, resp.Data.CurrentSyncCommittee) + require.NotNil(t, resp.Data.CurrentSyncCommitteeBranch) + }) + t.Run("electra", func(t *testing.T) { + l := util.NewTestLightClient(t).SetupTestElectra(false) // result is same for true and false - slot := l.State.Slot() - stateRoot, err := l.State.HashTreeRoot(l.Ctx) - require.NoError(t, err) + slot := primitives.Slot(params.BeaconConfig().ElectraForkEpoch * primitives.Epoch(params.BeaconConfig().SlotsPerEpoch)).Add(1) + blockRoot, err := l.Block.Block().HashTreeRoot() + require.NoError(t, err) - mockBlocker := &testutil.MockBlocker{BlockToReturn: l.Block} - mockChainService := &mock.ChainService{Optimistic: true, Slot: &slot} - mockChainInfoFetcher := &mock.ChainService{Slot: &slot} - s := &Server{ - Stater: &testutil.MockStater{StatesBySlot: map[primitives.Slot]state.BeaconState{ - slot: l.State, - }}, - Blocker: mockBlocker, - HeadFetcher: mockChainService, - ChainInfoFetcher: mockChainInfoFetcher, - } - request := httptest.NewRequest("GET", "http://foo.com/", nil) - request.SetPathValue("block_root", hexutil.Encode(stateRoot[:])) - writer := httptest.NewRecorder() - writer.Body = &bytes.Buffer{} + mockBlocker := &testutil.MockBlocker{BlockToReturn: l.Block} + mockChainService := &mock.ChainService{Optimistic: true, Slot: &slot} + mockChainInfoFetcher := &mock.ChainService{Slot: &slot} + s := &Server{ + Stater: &testutil.MockStater{StatesBySlot: map[primitives.Slot]state.BeaconState{ + slot: l.State, + }}, + Blocker: mockBlocker, + HeadFetcher: mockChainService, + ChainInfoFetcher: mockChainInfoFetcher, + } + request := httptest.NewRequest("GET", "http://foo.com/", nil) + request.SetPathValue("block_root", hexutil.Encode(blockRoot[:])) + writer := httptest.NewRecorder() + writer.Body = &bytes.Buffer{} - s.GetLightClientBootstrap(writer, request) - require.Equal(t, http.StatusOK, writer.Code) - var resp structs.LightClientBootstrapResponse - err = json.Unmarshal(writer.Body.Bytes(), &resp) - require.NoError(t, err) - var respHeader structs.LightClientHeader - err = json.Unmarshal(resp.Data.Header, &respHeader) - require.NoError(t, err) - require.Equal(t, "electra", resp.Version) + s.GetLightClientBootstrap(writer, request) + require.Equal(t, http.StatusOK, writer.Code) + var resp structs.LightClientBootstrapResponse + err = json.Unmarshal(writer.Body.Bytes(), &resp) + require.NoError(t, err) + var respHeader structs.LightClientHeader + err = json.Unmarshal(resp.Data.Header, &respHeader) + require.NoError(t, err) + require.Equal(t, "electra", resp.Version) - blockHeader, err := l.Block.Header() - require.NoError(t, err) - require.Equal(t, hexutil.Encode(blockHeader.Header.BodyRoot), respHeader.Beacon.BodyRoot) - require.Equal(t, strconv.FormatUint(uint64(blockHeader.Header.Slot), 10), respHeader.Beacon.Slot) + blockHeader, err := l.Block.Header() + require.NoError(t, err) + require.Equal(t, hexutil.Encode(blockHeader.Header.BodyRoot), respHeader.Beacon.BodyRoot) + require.Equal(t, strconv.FormatUint(uint64(blockHeader.Header.Slot), 10), respHeader.Beacon.Slot) - require.NotNil(t, resp.Data.CurrentSyncCommittee) - require.NotNil(t, resp.Data.CurrentSyncCommitteeBranch) + require.NotNil(t, resp.Data.CurrentSyncCommittee) + require.NotNil(t, resp.Data.CurrentSyncCommitteeBranch) + }) } // GetLightClientByRange tests @@ -988,9 +995,7 @@ func TestLightClientHandler_GetLightClientUpdatesByRangeMissingUpdates(t *testin } -// // TestLightClientHandler_GetLightClientFinalityUpdate tests - -func TestLightClientHandler_GetLightClientFinalityUpdateAltair(t *testing.T) { +func TestLightClientHandler_GetLightClientFinalityUpdate(t *testing.T) { helpers.ClearCache() ctx := context.Background() config := params.BeaconConfig() @@ -1102,230 +1107,6 @@ func TestLightClientHandler_GetLightClientFinalityUpdateAltair(t *testing.T) { require.NotNil(t, resp.Data) } -func TestLightClientHandler_GetLightClientFinalityUpdateCapella(t *testing.T) { - helpers.ClearCache() - ctx := context.Background() - config := params.BeaconConfig() - slot := primitives.Slot(config.CapellaForkEpoch * primitives.Epoch(config.SlotsPerEpoch)).Add(1) - - attestedState, err := util.NewBeaconStateCapella() - require.NoError(t, err) - err = attestedState.SetSlot(slot.Sub(1)) - require.NoError(t, err) - - require.NoError(t, attestedState.SetFinalizedCheckpoint(&pb.Checkpoint{ - Epoch: config.AltairForkEpoch - 10, - Root: make([]byte, 32), - })) - - parent := util.NewBeaconBlockCapella() - parent.Block.Slot = slot.Sub(1) - - signedParent, err := blocks.NewSignedBeaconBlock(parent) - require.NoError(t, err) - - parentHeader, err := signedParent.Header() - require.NoError(t, err) - attestedHeader := parentHeader.Header - - err = attestedState.SetLatestBlockHeader(attestedHeader) - require.NoError(t, err) - attestedStateRoot, err := attestedState.HashTreeRoot(ctx) - require.NoError(t, err) - - // get a new signed block so the root is updated with the new state root - parent.Block.StateRoot = attestedStateRoot[:] - signedParent, err = blocks.NewSignedBeaconBlock(parent) - require.NoError(t, err) - - st, err := util.NewBeaconStateCapella() - require.NoError(t, err) - err = st.SetSlot(slot) - require.NoError(t, err) - - parentRoot, err := signedParent.Block().HashTreeRoot() - require.NoError(t, err) - - block := util.NewBeaconBlockCapella() - block.Block.Slot = slot - block.Block.ParentRoot = parentRoot[:] - - for i := uint64(0); i < config.SyncCommitteeSize; i++ { - block.Block.Body.SyncAggregate.SyncCommitteeBits.SetBitAt(i, true) - } - - signedBlock, err := blocks.NewSignedBeaconBlock(block) - require.NoError(t, err) - - h, err := signedBlock.Header() - require.NoError(t, err) - - err = st.SetLatestBlockHeader(h.Header) - require.NoError(t, err) - stateRoot, err := st.HashTreeRoot(ctx) - require.NoError(t, err) - - // get a new signed block so the root is updated with the new state root - block.Block.StateRoot = stateRoot[:] - signedBlock, err = blocks.NewSignedBeaconBlock(block) - require.NoError(t, err) - - root, err := block.Block.HashTreeRoot() - require.NoError(t, err) - - mockBlocker := &testutil.MockBlocker{ - RootBlockMap: map[[32]byte]interfaces.ReadOnlySignedBeaconBlock{ - parentRoot: signedParent, - root: signedBlock, - }, - SlotBlockMap: map[primitives.Slot]interfaces.ReadOnlySignedBeaconBlock{ - slot.Sub(1): signedParent, - slot: signedBlock, - }, - } - mockChainService := &mock.ChainService{Optimistic: true, Slot: &slot, State: st, FinalizedRoots: map[[32]byte]bool{ - root: true, - }} - mockChainInfoFetcher := &mock.ChainService{Slot: &slot} - s := &Server{ - Stater: &testutil.MockStater{StatesBySlot: map[primitives.Slot]state.BeaconState{ - slot.Sub(1): attestedState, - slot: st, - }}, - Blocker: mockBlocker, - HeadFetcher: mockChainService, - ChainInfoFetcher: mockChainInfoFetcher, - } - request := httptest.NewRequest("GET", "http://foo.com", nil) - writer := httptest.NewRecorder() - writer.Body = &bytes.Buffer{} - - s.GetLightClientFinalityUpdate(writer, request) - - require.Equal(t, http.StatusOK, writer.Code) - var resp *structs.LightClientUpdateResponse - err = json.Unmarshal(writer.Body.Bytes(), &resp) - require.NoError(t, err) - var respHeader structs.LightClientHeader - err = json.Unmarshal(resp.Data.AttestedHeader, &respHeader) - require.NoError(t, err) - require.Equal(t, "capella", resp.Version) - require.Equal(t, hexutil.Encode(attestedHeader.BodyRoot), respHeader.Beacon.BodyRoot) - require.NotNil(t, resp.Data) -} - -func TestLightClientHandler_GetLightClientFinalityUpdateDeneb(t *testing.T) { - helpers.ClearCache() - ctx := context.Background() - config := params.BeaconConfig() - slot := primitives.Slot(config.DenebForkEpoch * primitives.Epoch(config.SlotsPerEpoch)).Add(1) - - attestedState, err := util.NewBeaconStateDeneb() - require.NoError(t, err) - err = attestedState.SetSlot(slot.Sub(1)) - require.NoError(t, err) - - require.NoError(t, attestedState.SetFinalizedCheckpoint(&pb.Checkpoint{ - Epoch: config.AltairForkEpoch - 10, - Root: make([]byte, 32), - })) - - parent := util.NewBeaconBlockDeneb() - parent.Block.Slot = slot.Sub(1) - - signedParent, err := blocks.NewSignedBeaconBlock(parent) - require.NoError(t, err) - - parentHeader, err := signedParent.Header() - require.NoError(t, err) - attestedHeader := parentHeader.Header - - err = attestedState.SetLatestBlockHeader(attestedHeader) - require.NoError(t, err) - attestedStateRoot, err := attestedState.HashTreeRoot(ctx) - require.NoError(t, err) - - // get a new signed block so the root is updated with the new state root - parent.Block.StateRoot = attestedStateRoot[:] - signedParent, err = blocks.NewSignedBeaconBlock(parent) - require.NoError(t, err) - - st, err := util.NewBeaconStateDeneb() - require.NoError(t, err) - err = st.SetSlot(slot) - require.NoError(t, err) - - parentRoot, err := signedParent.Block().HashTreeRoot() - require.NoError(t, err) - - block := util.NewBeaconBlockDeneb() - block.Block.Slot = slot - block.Block.ParentRoot = parentRoot[:] - - for i := uint64(0); i < config.SyncCommitteeSize; i++ { - block.Block.Body.SyncAggregate.SyncCommitteeBits.SetBitAt(i, true) - } - - signedBlock, err := blocks.NewSignedBeaconBlock(block) - require.NoError(t, err) - - h, err := signedBlock.Header() - require.NoError(t, err) - - err = st.SetLatestBlockHeader(h.Header) - require.NoError(t, err) - stateRoot, err := st.HashTreeRoot(ctx) - require.NoError(t, err) - - // get a new signed block so the root is updated with the new state root - block.Block.StateRoot = stateRoot[:] - signedBlock, err = blocks.NewSignedBeaconBlock(block) - require.NoError(t, err) - - root, err := block.Block.HashTreeRoot() - require.NoError(t, err) - - mockBlocker := &testutil.MockBlocker{ - RootBlockMap: map[[32]byte]interfaces.ReadOnlySignedBeaconBlock{ - parentRoot: signedParent, - root: signedBlock, - }, - SlotBlockMap: map[primitives.Slot]interfaces.ReadOnlySignedBeaconBlock{ - slot.Sub(1): signedParent, - slot: signedBlock, - }, - } - mockChainService := &mock.ChainService{Optimistic: true, Slot: &slot, State: st, FinalizedRoots: map[[32]byte]bool{ - root: true, - }} - mockChainInfoFetcher := &mock.ChainService{Slot: &slot} - s := &Server{ - Stater: &testutil.MockStater{StatesBySlot: map[primitives.Slot]state.BeaconState{ - slot.Sub(1): attestedState, - slot: st, - }}, - Blocker: mockBlocker, - HeadFetcher: mockChainService, - ChainInfoFetcher: mockChainInfoFetcher, - } - request := httptest.NewRequest("GET", "http://foo.com", nil) - writer := httptest.NewRecorder() - writer.Body = &bytes.Buffer{} - - s.GetLightClientFinalityUpdate(writer, request) - - require.Equal(t, http.StatusOK, writer.Code) - var resp *structs.LightClientUpdateResponse - err = json.Unmarshal(writer.Body.Bytes(), &resp) - require.NoError(t, err) - var respHeader structs.LightClientHeaderDeneb - err = json.Unmarshal(resp.Data.AttestedHeader, &respHeader) - require.NoError(t, err) - require.Equal(t, "deneb", resp.Version) - require.Equal(t, hexutil.Encode(attestedHeader.BodyRoot), respHeader.Beacon.BodyRoot) - require.NotNil(t, resp.Data) -} - func TestLightClientHandler_GetLightClientOptimisticUpdateAltair(t *testing.T) { helpers.ClearCache() ctx := context.Background() diff --git a/beacon-chain/rpc/eth/light-client/helpers.go b/beacon-chain/rpc/eth/light-client/helpers.go index 39779735bca1..28809391247a 100644 --- a/beacon-chain/rpc/eth/light-client/helpers.go +++ b/beacon-chain/rpc/eth/light-client/helpers.go @@ -2,23 +2,13 @@ package lightclient import ( "context" - "encoding/json" - "fmt" - "reflect" - "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/pkg/errors" - fieldparams "github.com/prysmaticlabs/prysm/v5/config/fieldparams" - "github.com/prysmaticlabs/prysm/v5/config/params" "github.com/prysmaticlabs/prysm/v5/consensus-types/primitives" - lightclient "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/light-client" - "github.com/prysmaticlabs/prysm/v5/runtime/version" - "github.com/prysmaticlabs/prysm/v5/api/server/structs" + lightclient "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/light-client" "github.com/prysmaticlabs/prysm/v5/beacon-chain/state" "github.com/prysmaticlabs/prysm/v5/consensus-types/interfaces" - "github.com/prysmaticlabs/prysm/v5/time/slots" ) func createLightClientBootstrap( @@ -27,76 +17,13 @@ func createLightClientBootstrap( state state.BeaconState, block interfaces.ReadOnlySignedBeaconBlock, ) (*structs.LightClientBootstrap, error) { - // assert compute_epoch_at_slot(state.slot) >= ALTAIR_FORK_EPOCH - if slots.ToEpoch(state.Slot()) < params.BeaconConfig().AltairForkEpoch { - return nil, fmt.Errorf("light client bootstrap is not supported before Altair, invalid slot %d", state.Slot()) - } - - // assert state.slot == state.latest_block_header.slot - latestBlockHeader := state.LatestBlockHeader() - if state.Slot() != latestBlockHeader.Slot { - return nil, fmt.Errorf("state slot %d not equal to latest block header slot %d", state.Slot(), latestBlockHeader.Slot) - } - - // header.state_root = hash_tree_root(state) - stateRoot, err := state.HashTreeRoot(ctx) - if err != nil { - return nil, errors.Wrap(err, "could not get state root") - } - latestBlockHeader.StateRoot = stateRoot[:] - // assert hash_tree_root(header) == hash_tree_root(block.message) - latestBlockHeaderRoot, err := latestBlockHeader.HashTreeRoot() + bootstrap, err := lightclient.CreateLightClientBootstrap(ctx, currentSlot, state, block) if err != nil { - return nil, errors.Wrap(err, "could not get latest block header root") - } - beaconBlockRoot, err := block.Block().HashTreeRoot() - if err != nil { - return nil, errors.Wrap(err, "could not get block root") - } - if latestBlockHeaderRoot != beaconBlockRoot { - return nil, fmt.Errorf("latest block header root %#x not equal to block root %#x", latestBlockHeaderRoot, beaconBlockRoot) - } - - lightClientHeader, err := lightclient.BlockToLightClientHeader(ctx, currentSlot, block) - if err != nil { - return nil, errors.Wrap(err, "could not convert block to light client header") - } - - apiLightClientHeader := &structs.LightClientHeader{ - Beacon: structs.BeaconBlockHeaderFromConsensus(lightClientHeader.Beacon()), - } - - headerJSON, err := json.Marshal(apiLightClientHeader) - if err != nil { - return nil, errors.Wrap(err, "could not convert header to raw message") - } - currentSyncCommittee, err := state.CurrentSyncCommittee() - if err != nil { - return nil, errors.Wrap(err, "could not get current sync committee") - } - currentSyncCommitteeProof, err := state.CurrentSyncCommitteeProof(ctx) - if err != nil { - return nil, errors.Wrap(err, "could not get current sync committee proof") - } - - var branch []string - if state.Version() >= version.Electra { - branch = make([]string, fieldparams.SyncCommitteeBranchDepthElectra) - } else { - branch = make([]string, fieldparams.SyncCommitteeBranchDepth) - } - for i, proof := range currentSyncCommitteeProof { - branch[i] = hexutil.Encode(proof) - } - - result := &structs.LightClientBootstrap{ - Header: headerJSON, - CurrentSyncCommittee: structs.SyncCommitteeFromConsensus(currentSyncCommittee), - CurrentSyncCommitteeBranch: branch, + return nil, err } - return result, nil + return structs.LightClientBootstrapFromConsensus(bootstrap) } func newLightClientFinalityUpdateFromBeaconState( @@ -131,112 +58,3 @@ func newLightClientOptimisticUpdateFromBeaconState( return structs.LightClientOptimisticUpdateFromConsensus(result) } - -func HasRelevantSyncCommittee(update interfaces.LightClientUpdate) (bool, error) { - if update.Version() >= version.Electra { - branch, err := update.NextSyncCommitteeBranchElectra() - if err != nil { - return false, err - } - return !reflect.DeepEqual(branch, interfaces.LightClientSyncCommitteeBranchElectra{}), nil - } - branch, err := update.NextSyncCommitteeBranch() - if err != nil { - return false, err - } - return !reflect.DeepEqual(branch, interfaces.LightClientSyncCommitteeBranch{}), nil -} - -func HasFinality(update interfaces.LightClientUpdate) (bool, error) { - if update.Version() >= version.Electra { - b, err := update.FinalityBranchElectra() - if err != nil { - return false, err - } - return !reflect.DeepEqual(b, interfaces.LightClientFinalityBranchElectra{}), nil - } - - b, err := update.FinalityBranch() - if err != nil { - return false, err - } - return !reflect.DeepEqual(b, interfaces.LightClientFinalityBranch{}), nil -} - -func IsBetterUpdate(newUpdate, oldUpdate interfaces.LightClientUpdate) (bool, error) { - maxActiveParticipants := newUpdate.SyncAggregate().SyncCommitteeBits.Len() - newNumActiveParticipants := newUpdate.SyncAggregate().SyncCommitteeBits.Count() - oldNumActiveParticipants := oldUpdate.SyncAggregate().SyncCommitteeBits.Count() - newHasSupermajority := newNumActiveParticipants*3 >= maxActiveParticipants*2 - oldHasSupermajority := oldNumActiveParticipants*3 >= maxActiveParticipants*2 - - if newHasSupermajority != oldHasSupermajority { - return newHasSupermajority, nil - } - if !newHasSupermajority && newNumActiveParticipants != oldNumActiveParticipants { - return newNumActiveParticipants > oldNumActiveParticipants, nil - } - - newUpdateAttestedHeaderBeacon := newUpdate.AttestedHeader().Beacon() - oldUpdateAttestedHeaderBeacon := oldUpdate.AttestedHeader().Beacon() - - // Compare presence of relevant sync committee - newHasRelevantSyncCommittee, err := HasRelevantSyncCommittee(newUpdate) - if err != nil { - return false, err - } - newHasRelevantSyncCommittee = newHasRelevantSyncCommittee && - (slots.SyncCommitteePeriod(slots.ToEpoch(newUpdateAttestedHeaderBeacon.Slot)) == slots.SyncCommitteePeriod(slots.ToEpoch(newUpdate.SignatureSlot()))) - oldHasRelevantSyncCommittee, err := HasRelevantSyncCommittee(oldUpdate) - if err != nil { - return false, err - } - oldHasRelevantSyncCommittee = oldHasRelevantSyncCommittee && - (slots.SyncCommitteePeriod(slots.ToEpoch(oldUpdateAttestedHeaderBeacon.Slot)) == slots.SyncCommitteePeriod(slots.ToEpoch(oldUpdate.SignatureSlot()))) - - if newHasRelevantSyncCommittee != oldHasRelevantSyncCommittee { - return newHasRelevantSyncCommittee, nil - } - - // Compare indication of any finality - newHasFinality, err := HasFinality(newUpdate) - if err != nil { - return false, err - } - oldHasFinality, err := HasFinality(oldUpdate) - if err != nil { - return false, err - } - if newHasFinality != oldHasFinality { - return newHasFinality, nil - } - - newUpdateFinalizedHeaderBeacon := newUpdate.FinalizedHeader().Beacon() - oldUpdateFinalizedHeaderBeacon := oldUpdate.FinalizedHeader().Beacon() - - // Compare sync committee finality - if newHasFinality { - newHasSyncCommitteeFinality := - slots.SyncCommitteePeriod(slots.ToEpoch(newUpdateFinalizedHeaderBeacon.Slot)) == - slots.SyncCommitteePeriod(slots.ToEpoch(newUpdateAttestedHeaderBeacon.Slot)) - oldHasSyncCommitteeFinality := - slots.SyncCommitteePeriod(slots.ToEpoch(oldUpdateFinalizedHeaderBeacon.Slot)) == - slots.SyncCommitteePeriod(slots.ToEpoch(oldUpdateAttestedHeaderBeacon.Slot)) - - if newHasSyncCommitteeFinality != oldHasSyncCommitteeFinality { - return newHasSyncCommitteeFinality, nil - } - } - - // Tiebreaker 1: Sync committee participation beyond supermajority - if newNumActiveParticipants != oldNumActiveParticipants { - return newNumActiveParticipants > oldNumActiveParticipants, nil - } - - // Tiebreaker 2: Prefer older data (fewer changes to best) - if newUpdateAttestedHeaderBeacon.Slot != oldUpdateAttestedHeaderBeacon.Slot { - return newUpdateAttestedHeaderBeacon.Slot < oldUpdateAttestedHeaderBeacon.Slot, nil - } - - return newUpdate.SignatureSlot() < oldUpdate.SignatureSlot(), nil -} diff --git a/beacon-chain/rpc/eth/light-client/helpers_test.go b/beacon-chain/rpc/eth/light-client/helpers_test.go index e5ac6e11ce6c..6d60fca8185a 100644 --- a/beacon-chain/rpc/eth/light-client/helpers_test.go +++ b/beacon-chain/rpc/eth/light-client/helpers_test.go @@ -53,7 +53,7 @@ func TestIsBetterUpdate(t *testing.T) { SyncCommitteeBits: []byte{0b11111100, 0b1}, // [0,0,1,1,1,1,1,1] }) - result, err := IsBetterUpdate(newUpdate, oldUpdate) + result, err := lightclient.IsBetterUpdate(newUpdate, oldUpdate) assert.NoError(t, err) assert.Equal(t, true, result) }) @@ -71,7 +71,7 @@ func TestIsBetterUpdate(t *testing.T) { SyncCommitteeBits: []byte{0b01111100, 0b1}, // [0,0,1,1,1,1,1,0] }) - result, err := IsBetterUpdate(newUpdate, oldUpdate) + result, err := lightclient.IsBetterUpdate(newUpdate, oldUpdate) assert.NoError(t, err) assert.Equal(t, false, result) }) @@ -89,7 +89,7 @@ func TestIsBetterUpdate(t *testing.T) { SyncCommitteeBits: []byte{0b01111100, 0b1}, // [0,0,1,1,1,1,1,0] }) - result, err := IsBetterUpdate(newUpdate, oldUpdate) + result, err := lightclient.IsBetterUpdate(newUpdate, oldUpdate) assert.NoError(t, err) assert.Equal(t, true, result) }) @@ -107,7 +107,7 @@ func TestIsBetterUpdate(t *testing.T) { SyncCommitteeBits: []byte{0b00111100, 0b1}, // [0,0,1,1,1,1,0,0] }) - result, err := IsBetterUpdate(newUpdate, oldUpdate) + result, err := lightclient.IsBetterUpdate(newUpdate, oldUpdate) assert.NoError(t, err) assert.Equal(t, false, result) }) @@ -146,7 +146,7 @@ func TestIsBetterUpdate(t *testing.T) { assert.NoError(t, err) newUpdate.SetSignatureSlot(1000000) - result, err := IsBetterUpdate(newUpdate, oldUpdate) + result, err := lightclient.IsBetterUpdate(newUpdate, oldUpdate) assert.NoError(t, err) assert.Equal(t, true, result) }) @@ -185,7 +185,7 @@ func TestIsBetterUpdate(t *testing.T) { assert.NoError(t, err) newUpdate.SetSignatureSlot(9999) - result, err := IsBetterUpdate(newUpdate, oldUpdate) + result, err := lightclient.IsBetterUpdate(newUpdate, oldUpdate) assert.NoError(t, err) assert.Equal(t, false, result) }) @@ -228,7 +228,7 @@ func TestIsBetterUpdate(t *testing.T) { err = newUpdate.SetFinalityBranch(createNonEmptyFinalityBranch()) assert.NoError(t, err) - result, err := IsBetterUpdate(newUpdate, oldUpdate) + result, err := lightclient.IsBetterUpdate(newUpdate, oldUpdate) assert.NoError(t, err) assert.Equal(t, true, result) }) @@ -271,7 +271,7 @@ func TestIsBetterUpdate(t *testing.T) { assert.NoError(t, err) newUpdate.SetSignatureSlot(9999) - result, err := IsBetterUpdate(newUpdate, oldUpdate) + result, err := lightclient.IsBetterUpdate(newUpdate, oldUpdate) assert.NoError(t, err) assert.Equal(t, false, result) }) @@ -332,7 +332,7 @@ func TestIsBetterUpdate(t *testing.T) { err = newUpdate.SetFinalizedHeader(newFinalizedHeader) assert.NoError(t, err) - result, err := IsBetterUpdate(newUpdate, oldUpdate) + result, err := lightclient.IsBetterUpdate(newUpdate, oldUpdate) assert.NoError(t, err) assert.Equal(t, true, result) }) @@ -393,7 +393,7 @@ func TestIsBetterUpdate(t *testing.T) { err = newUpdate.SetFinalizedHeader(newFinalizedHeader) assert.NoError(t, err) - result, err := IsBetterUpdate(newUpdate, oldUpdate) + result, err := lightclient.IsBetterUpdate(newUpdate, oldUpdate) assert.NoError(t, err) assert.Equal(t, false, result) }) @@ -411,7 +411,7 @@ func TestIsBetterUpdate(t *testing.T) { SyncCommitteeBits: []byte{0b01111100, 0b1}, // [0,1,1,1,1,1,0,0] }) - result, err := IsBetterUpdate(newUpdate, oldUpdate) + result, err := lightclient.IsBetterUpdate(newUpdate, oldUpdate) assert.NoError(t, err) assert.Equal(t, true, result) }) @@ -429,7 +429,7 @@ func TestIsBetterUpdate(t *testing.T) { SyncCommitteeBits: []byte{0b00111100, 0b1}, // [0,0,1,1,1,1,0,0] }) - result, err := IsBetterUpdate(newUpdate, oldUpdate) + result, err := lightclient.IsBetterUpdate(newUpdate, oldUpdate) assert.NoError(t, err) assert.Equal(t, false, result) }) @@ -490,7 +490,7 @@ func TestIsBetterUpdate(t *testing.T) { err = newUpdate.SetFinalizedHeader(newFinalizedHeader) assert.NoError(t, err) - result, err := IsBetterUpdate(newUpdate, oldUpdate) + result, err := lightclient.IsBetterUpdate(newUpdate, oldUpdate) assert.NoError(t, err) assert.Equal(t, true, result) }) @@ -551,7 +551,7 @@ func TestIsBetterUpdate(t *testing.T) { err = newUpdate.SetFinalizedHeader(newFinalizedHeader) assert.NoError(t, err) - result, err := IsBetterUpdate(newUpdate, oldUpdate) + result, err := lightclient.IsBetterUpdate(newUpdate, oldUpdate) assert.NoError(t, err) assert.Equal(t, false, result) }) @@ -612,7 +612,7 @@ func TestIsBetterUpdate(t *testing.T) { err = newUpdate.SetFinalizedHeader(newFinalizedHeader) assert.NoError(t, err) - result, err := IsBetterUpdate(newUpdate, oldUpdate) + result, err := lightclient.IsBetterUpdate(newUpdate, oldUpdate) assert.NoError(t, err) assert.Equal(t, true, result) }) @@ -673,7 +673,7 @@ func TestIsBetterUpdate(t *testing.T) { err = newUpdate.SetFinalizedHeader(newFinalizedHeader) assert.NoError(t, err) - result, err := IsBetterUpdate(newUpdate, oldUpdate) + result, err := lightclient.IsBetterUpdate(newUpdate, oldUpdate) assert.NoError(t, err) assert.Equal(t, false, result) }) diff --git a/beacon-chain/rpc/eth/light-client/server.go b/beacon-chain/rpc/eth/light-client/server.go index 84b061379fc1..2157f2d0ce44 100644 --- a/beacon-chain/rpc/eth/light-client/server.go +++ b/beacon-chain/rpc/eth/light-client/server.go @@ -3,6 +3,7 @@ package lightclient import ( "github.com/prysmaticlabs/prysm/v5/beacon-chain/blockchain" "github.com/prysmaticlabs/prysm/v5/beacon-chain/db" + light_client "github.com/prysmaticlabs/prysm/v5/beacon-chain/light-client" "github.com/prysmaticlabs/prysm/v5/beacon-chain/rpc/lookup" ) @@ -12,4 +13,5 @@ type Server struct { HeadFetcher blockchain.HeadFetcher ChainInfoFetcher blockchain.ChainInfoFetcher BeaconDB db.HeadAccessDatabase + LCStore *light_client.Store } diff --git a/beacon-chain/rpc/service.go b/beacon-chain/rpc/service.go index 4a56c0d4162e..f9c86fac9bba 100644 --- a/beacon-chain/rpc/service.go +++ b/beacon-chain/rpc/service.go @@ -26,6 +26,7 @@ import ( "github.com/prysmaticlabs/prysm/v5/beacon-chain/db" "github.com/prysmaticlabs/prysm/v5/beacon-chain/db/filesystem" "github.com/prysmaticlabs/prysm/v5/beacon-chain/execution" + light_client "github.com/prysmaticlabs/prysm/v5/beacon-chain/light-client" "github.com/prysmaticlabs/prysm/v5/beacon-chain/operations/attestations" "github.com/prysmaticlabs/prysm/v5/beacon-chain/operations/blstoexec" "github.com/prysmaticlabs/prysm/v5/beacon-chain/operations/slashings" @@ -140,6 +141,7 @@ type Config struct { BlobStorage *filesystem.BlobStorage TrackedValidatorsCache *cache.TrackedValidatorsCache PayloadIDCache *cache.PayloadIDCache + LCStore *light_client.Store } // NewService instantiates a new RPC service instance that will diff --git a/beacon-chain/sync/BUILD.bazel b/beacon-chain/sync/BUILD.bazel index 1d83ac98ad6f..abcb2cd1f3bd 100644 --- a/beacon-chain/sync/BUILD.bazel +++ b/beacon-chain/sync/BUILD.bazel @@ -26,6 +26,7 @@ go_library( "rpc_blob_sidecars_by_root.go", "rpc_chunked_response.go", "rpc_goodbye.go", + "rpc_light_client.go", "rpc_metadata.go", "rpc_ping.go", "rpc_send_request.go", @@ -38,6 +39,7 @@ go_library( "subscriber_blob_sidecar.go", "subscriber_bls_to_execution_change.go", "subscriber_handlers.go", + "subscriber_light_client.go", "subscriber_sync_committee_message.go", "subscriber_sync_contribution_proof.go", "subscription_topic_handler.go", @@ -48,6 +50,7 @@ go_library( "validate_beacon_blocks.go", "validate_blob.go", "validate_bls_to_execution_change.go", + "validate_light_client.go", "validate_proposer_slashing.go", "validate_sync_committee_message.go", "validate_sync_contribution_proof.go", @@ -79,6 +82,7 @@ go_library( "//beacon-chain/db/filesystem:go_default_library", "//beacon-chain/db/filters:go_default_library", "//beacon-chain/execution:go_default_library", + "//beacon-chain/light-client:go_default_library", "//beacon-chain/operations/attestations:go_default_library", "//beacon-chain/operations/blstoexec:go_default_library", "//beacon-chain/operations/slashings:go_default_library", diff --git a/beacon-chain/sync/options.go b/beacon-chain/sync/options.go index ff20b8b81212..2eaa5325a9a2 100644 --- a/beacon-chain/sync/options.go +++ b/beacon-chain/sync/options.go @@ -8,6 +8,7 @@ import ( "github.com/prysmaticlabs/prysm/v5/beacon-chain/db" "github.com/prysmaticlabs/prysm/v5/beacon-chain/db/filesystem" "github.com/prysmaticlabs/prysm/v5/beacon-chain/execution" + light_client "github.com/prysmaticlabs/prysm/v5/beacon-chain/light-client" "github.com/prysmaticlabs/prysm/v5/beacon-chain/operations/attestations" "github.com/prysmaticlabs/prysm/v5/beacon-chain/operations/blstoexec" "github.com/prysmaticlabs/prysm/v5/beacon-chain/operations/slashings" @@ -180,3 +181,11 @@ func WithAvailableBlocker(avb coverage.AvailableBlocker) Option { return nil } } + +// WithLightClientStore allows the sync package to access light client data. +func WithLightClientStore(lcs *light_client.Store) Option { + return func(s *Service) error { + s.lcStore = lcs + return nil + } +} diff --git a/beacon-chain/sync/rate_limiter.go b/beacon-chain/sync/rate_limiter.go index 636fe54c685e..60052b018651 100644 --- a/beacon-chain/sync/rate_limiter.go +++ b/beacon-chain/sync/rate_limiter.go @@ -79,6 +79,12 @@ func newRateLimiter(p2pProvider p2p.P2P) *limiter { // BlobSidecarsByRangeV1 topicMap[addEncoding(p2p.RPCBlobSidecarsByRangeTopicV1)] = blobCollector + // Light client requests + topicMap[addEncoding(p2p.RPCLightClientBootstrapTopicV1)] = leakybucket.NewCollector(1, defaultBurstLimit, leakyBucketPeriod, false /* deleteEmptyBuckets */) + topicMap[addEncoding(p2p.RPCLightClientUpdatesByRangeTopicV1)] = leakybucket.NewCollector(1, defaultBurstLimit, leakyBucketPeriod, false /* deleteEmptyBuckets */) + topicMap[addEncoding(p2p.RPCLightClientOptimisticUpdateTopicV1)] = leakybucket.NewCollector(1, defaultBurstLimit, leakyBucketPeriod, false /* deleteEmptyBuckets */) + topicMap[addEncoding(p2p.RPCLightClientFinalityUpdateTopicV1)] = leakybucket.NewCollector(1, defaultBurstLimit, leakyBucketPeriod, false /* deleteEmptyBuckets */) + // General topic for all rpc requests. topicMap[rpcLimiterTopic] = leakybucket.NewCollector(5, defaultBurstLimit*2, leakyBucketPeriod, false /* deleteEmptyBuckets */) diff --git a/beacon-chain/sync/rpc.go b/beacon-chain/sync/rpc.go index 1c067036f5ee..e08eb555c943 100644 --- a/beacon-chain/sync/rpc.go +++ b/beacon-chain/sync/rpc.go @@ -96,6 +96,22 @@ func (s *Service) registerRPCHandlersAltair() { p2p.RPCMetaDataTopicV2, s.metaDataHandler, ) + s.registerRPC( + p2p.RPCLightClientBootstrapTopicV1, + s.lightClientBootstrapRPCHandler, + ) + s.registerRPC( + p2p.RPCLightClientUpdatesByRangeTopicV1, + s.lightClientUpdatesByRangeRPCHandler, + ) + s.registerRPC( + p2p.RPCLightClientFinalityUpdateTopicV1, + s.lightClientFinalityUpdateRPCHandler, + ) + s.registerRPC( + p2p.RPCLightClientOptimisticUpdateTopicV1, + s.lightClientOptimisticUpdateRPCHandler, + ) } func (s *Service) registerRPCHandlersDeneb() { @@ -191,9 +207,12 @@ func (s *Service) registerRPC(baseTopic string, handle rpcHandler) { // Increment message received counter. messageReceivedCounter.WithLabelValues(topic).Inc() - // since metadata requests do not have any data in the payload, we + // since some requests do not have any data in the payload, we // do not decode anything. - if baseTopic == p2p.RPCMetaDataTopicV1 || baseTopic == p2p.RPCMetaDataTopicV2 { + if baseTopic == p2p.RPCMetaDataTopicV1 || + baseTopic == p2p.RPCMetaDataTopicV2 || + baseTopic == p2p.RPCLightClientFinalityUpdateTopicV1 || + baseTopic == p2p.RPCLightClientOptimisticUpdateTopicV1 { if err := handle(ctx, base, stream); err != nil { messageFailedProcessingCounter.WithLabelValues(topic).Inc() if !errors.Is(err, p2ptypes.ErrWrongForkDigestVersion) { diff --git a/beacon-chain/sync/rpc_chunked_response.go b/beacon-chain/sync/rpc_chunked_response.go index 6eac6fc8ff3d..bc452a7f62d5 100644 --- a/beacon-chain/sync/rpc_chunked_response.go +++ b/beacon-chain/sync/rpc_chunked_response.go @@ -155,3 +155,194 @@ func WriteBlobSidecarChunk(stream libp2pcore.Stream, tor blockchain.TemporalOrac _, err = encoding.EncodeWithMaxLength(stream, sidecar) return err } + +func WriteLightClientBootstrapChunk( + stream libp2pcore.Stream, + tor blockchain.TemporalOracle, + encoding encoder.NetworkEncoding, + bootstrap interfaces.LightClientBootstrap, +) error { + if _, err := stream.Write([]byte{responseCodeSuccess}); err != nil { + return err + } + + var obtainedCtx []byte + valRoot := tor.GenesisValidatorsRoot() + switch v := bootstrap.Version(); v { + case version.Altair: + digest, err := forks.ForkDigestFromEpoch(params.BeaconConfig().AltairForkEpoch, valRoot[:]) + if err != nil { + return err + } + obtainedCtx = digest[:] + case version.Capella: + digest, err := forks.ForkDigestFromEpoch(params.BeaconConfig().CapellaForkEpoch, valRoot[:]) + if err != nil { + return err + } + obtainedCtx = digest[:] + case version.Deneb: + digest, err := forks.ForkDigestFromEpoch(params.BeaconConfig().DenebForkEpoch, valRoot[:]) + if err != nil { + return err + } + obtainedCtx = digest[:] + case version.Electra: + digest, err := forks.ForkDigestFromEpoch(params.BeaconConfig().ElectraForkEpoch, valRoot[:]) + if err != nil { + return err + } + obtainedCtx = digest[:] + default: + return errors.Wrapf(ErrUnrecognizedVersion, "light client boostrap version %s is not recognized", version.String(v)) + } + + if err := writeContextToStream(obtainedCtx, stream); err != nil { + return err + } + _, err := encoding.EncodeWithMaxLength(stream, bootstrap) + return err +} + +func WriteLightClientUpdateChunk( + stream libp2pcore.Stream, + tor blockchain.TemporalOracle, + encoding encoder.NetworkEncoding, + update interfaces.LightClientUpdate, +) error { + if _, err := stream.Write([]byte{responseCodeSuccess}); err != nil { + return err + } + + var obtainedCtx []byte + valRoot := tor.GenesisValidatorsRoot() + switch v := update.Version(); v { + case version.Altair: + digest, err := forks.ForkDigestFromEpoch(params.BeaconConfig().AltairForkEpoch, valRoot[:]) + if err != nil { + return err + } + obtainedCtx = digest[:] + case version.Capella: + digest, err := forks.ForkDigestFromEpoch(params.BeaconConfig().CapellaForkEpoch, valRoot[:]) + if err != nil { + return err + } + obtainedCtx = digest[:] + case version.Deneb: + digest, err := forks.ForkDigestFromEpoch(params.BeaconConfig().DenebForkEpoch, valRoot[:]) + if err != nil { + return err + } + obtainedCtx = digest[:] + case version.Electra: + digest, err := forks.ForkDigestFromEpoch(params.BeaconConfig().ElectraForkEpoch, valRoot[:]) + if err != nil { + return err + } + obtainedCtx = digest[:] + default: + return errors.Wrapf(ErrUnrecognizedVersion, "light client boostrap version %s is not recognized", version.String(v)) + } + + if err := writeContextToStream(obtainedCtx, stream); err != nil { + return err + } + _, err := encoding.EncodeWithMaxLength(stream, update) + return err +} + +func WriteLightClientFinalityUpdateChunk( + stream libp2pcore.Stream, + tor blockchain.TemporalOracle, + encoding encoder.NetworkEncoding, + update interfaces.LightClientFinalityUpdate, +) error { + if _, err := stream.Write([]byte{responseCodeSuccess}); err != nil { + return err + } + + var obtainedCtx []byte + valRoot := tor.GenesisValidatorsRoot() + switch v := update.Version(); v { + case version.Altair: + digest, err := forks.ForkDigestFromEpoch(params.BeaconConfig().AltairForkEpoch, valRoot[:]) + if err != nil { + return err + } + obtainedCtx = digest[:] + case version.Capella: + digest, err := forks.ForkDigestFromEpoch(params.BeaconConfig().CapellaForkEpoch, valRoot[:]) + if err != nil { + return err + } + obtainedCtx = digest[:] + case version.Deneb: + digest, err := forks.ForkDigestFromEpoch(params.BeaconConfig().DenebForkEpoch, valRoot[:]) + if err != nil { + return err + } + obtainedCtx = digest[:] + case version.Electra: + digest, err := forks.ForkDigestFromEpoch(params.BeaconConfig().ElectraForkEpoch, valRoot[:]) + if err != nil { + return err + } + obtainedCtx = digest[:] + default: + return errors.Wrapf(ErrUnrecognizedVersion, "light client boostrap version %s is not recognized", version.String(v)) + } + + if err := writeContextToStream(obtainedCtx, stream); err != nil { + return err + } + _, err := encoding.EncodeWithMaxLength(stream, update) + return err +} +func WriteLightClientOptimisticUpdateChunk( + stream libp2pcore.Stream, + tor blockchain.TemporalOracle, + encoding encoder.NetworkEncoding, + update interfaces.LightClientOptimisticUpdate, +) error { + if _, err := stream.Write([]byte{responseCodeSuccess}); err != nil { + return err + } + + var obtainedCtx []byte + valRoot := tor.GenesisValidatorsRoot() + switch v := update.Version(); v { + case version.Altair: + digest, err := forks.ForkDigestFromEpoch(params.BeaconConfig().AltairForkEpoch, valRoot[:]) + if err != nil { + return err + } + obtainedCtx = digest[:] + case version.Capella: + digest, err := forks.ForkDigestFromEpoch(params.BeaconConfig().CapellaForkEpoch, valRoot[:]) + if err != nil { + return err + } + obtainedCtx = digest[:] + case version.Deneb: + digest, err := forks.ForkDigestFromEpoch(params.BeaconConfig().DenebForkEpoch, valRoot[:]) + if err != nil { + return err + } + obtainedCtx = digest[:] + case version.Electra: + digest, err := forks.ForkDigestFromEpoch(params.BeaconConfig().ElectraForkEpoch, valRoot[:]) + if err != nil { + return err + } + obtainedCtx = digest[:] + default: + return errors.Wrapf(ErrUnrecognizedVersion, "light client boostrap version %s is not recognized", version.String(v)) + } + + if err := writeContextToStream(obtainedCtx, stream); err != nil { + return err + } + _, err := encoding.EncodeWithMaxLength(stream, update) + return err +} diff --git a/beacon-chain/sync/rpc_light_client.go b/beacon-chain/sync/rpc_light_client.go new file mode 100644 index 000000000000..626a8aaf036a --- /dev/null +++ b/beacon-chain/sync/rpc_light_client.go @@ -0,0 +1,206 @@ +package sync + +import ( + "context" + "fmt" + + libp2pcore "github.com/libp2p/go-libp2p/core" + "github.com/prysmaticlabs/prysm/v5/beacon-chain/p2p" + "github.com/prysmaticlabs/prysm/v5/beacon-chain/p2p/types" + "github.com/prysmaticlabs/prysm/v5/config/params" + "github.com/prysmaticlabs/prysm/v5/monitoring/tracing" + "github.com/prysmaticlabs/prysm/v5/monitoring/tracing/trace" + eth "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1" +) + +// lightClientBootstrapRPCHandler handles the /eth2/beacon_chain/req/light_client_bootstrap/1/ RPC request. +func (s *Service) lightClientBootstrapRPCHandler(ctx context.Context, msg interface{}, stream libp2pcore.Stream) error { + ctx, span := trace.StartSpan(ctx, "sync.lightClientBootstrapRPCHandler") + defer span.End() + ctx, cancel := context.WithTimeout(ctx, ttfbTimeout) + defer cancel() + + // TODO: What should we log? + log := log.WithField("handler", p2p.LightClientBootstrapName[1:]) // slice the leading slash off the name var + + log.Info("LC: lightClientBootstrapRPCHandler invoked") + + SetRPCStreamDeadlines(stream) + if err := s.rateLimiter.validateRequest(stream, 1); err != nil { + log.WithError(err).Error("s.rateLimiter.validateRequest") + return err + } + s.rateLimiter.add(stream, 1) + + rawMsg, ok := msg.(*types.LightClientBootstrapReq) + if !ok { + log.Error("Message is not *types.LightClientBootstrapReq") + return fmt.Errorf("message is not type %T", &types.LightClientBootstrapReq{}) + } + blkRoot := *rawMsg + + bootstrap, err := s.cfg.beaconDB.LightClientBootstrap(ctx, blkRoot) + if err != nil { + s.writeErrorResponseToStream(responseCodeServerError, types.ErrGeneric.Error(), stream) + tracing.AnnotateError(span, err) + log.WithError(err).Error("s.cfg.beaconDB.LightClientBootstrap") + return err + } + if bootstrap == nil { + s.writeErrorResponseToStream(responseCodeResourceUnavailable, types.ErrResourceUnavailable.Error(), stream) + tracing.AnnotateError(span, err) + log.WithError(err).Error(fmt.Sprintf("nil bootstrap for root %#x", blkRoot)) + return err + } + + SetStreamWriteDeadline(stream, defaultWriteDuration) + if err = WriteLightClientBootstrapChunk(stream, s.cfg.clock, s.cfg.p2p.Encoding(), bootstrap); err != nil { + s.writeErrorResponseToStream(responseCodeServerError, types.ErrGeneric.Error(), stream) + tracing.AnnotateError(span, err) + log.WithError(err).Error("WriteLightClientBootstrapChunk") + return err + } + + log.Info("LC: lightClientBootstrapRPCHandler completed") + + closeStream(stream, log) + return nil +} + +// lightClientUpdatesByRangeRPCHandler handles the /eth2/beacon_chain/req/light_client_updates_by_range/1/ RPC request. +func (s *Service) lightClientUpdatesByRangeRPCHandler(ctx context.Context, msg interface{}, stream libp2pcore.Stream) error { + ctx, span := trace.StartSpan(ctx, "sync.lightClientUpdatesByRangeRPCHandler") + defer span.End() + ctx, cancel := context.WithTimeout(ctx, ttfbTimeout) + defer cancel() + + // TODO: What should we log? + log := log.WithField("handler", p2p.LightClientUpdatesByRangeName[1:]) // slice the leading slash off the name var + + log.Info("LC: lightClientUpdatesByRangeRPCHandler invoked") + + SetRPCStreamDeadlines(stream) + if err := s.rateLimiter.validateRequest(stream, 1); err != nil { + log.WithError(err).Error("s.rateLimiter.validateRequest") + return err + } + s.rateLimiter.add(stream, 1) + + r, ok := msg.(*eth.LightClientUpdatesByRangeReq) + if !ok { + log.Error("Message is not *eth.LightClientUpdatesByRangeReq") + return fmt.Errorf("message is not type %T", ð.LightClientUpdatesByRangeReq{}) + } + + if r.Count > params.BeaconConfig().MaxRequestLightClientUpdates { + r.Count = params.BeaconConfig().MaxRequestLightClientUpdates + } + + // TODO: check for overflow + /*endPeriod,ok := math.SafeAdd(r.StartPeriod, r.Count-1) + if !ok { + err := errors.Wrap(types.ErrInvalidRequest, "end period overflows") + s.writeErrorResponseToStream(responseCodeInvalidRequest, err.Error(), stream) + s.cfg.p2p.Peers().Scorers().BadResponsesScorer().Increment(stream.Conn().RemotePeer()) + tracing.AnnotateError(span, err) + log.WithError(err).Error("end period overflows") + return err + }*/ + + log.Infof("LC: requesting updates by range (StartPeriod: %d, EndPeriod: %d)", r.StartPeriod, r.StartPeriod+r.Count-1) + + updates, err := s.cfg.beaconDB.LightClientUpdates(ctx, r.StartPeriod, r.StartPeriod+r.Count-1) + if err != nil { + s.writeErrorResponseToStream(responseCodeServerError, types.ErrGeneric.Error(), stream) + tracing.AnnotateError(span, err) + log.WithError(err).Error("s.cfg.beaconDB.LightClientUpdates") + return err + } + for _, u := range updates { + SetStreamWriteDeadline(stream, defaultWriteDuration) + if err = WriteLightClientUpdateChunk(stream, s.cfg.clock, s.cfg.p2p.Encoding(), u); err != nil { + s.writeErrorResponseToStream(responseCodeServerError, types.ErrGeneric.Error(), stream) + tracing.AnnotateError(span, err) + log.WithError(err).Error("WriteLightClientUpdateChunk") + return err + } + s.rateLimiter.add(stream, 1) + } + + log.Info("LC: lightClientUpdatesByRangeRPCHandler completed") + + closeStream(stream, log) + return nil +} + +// lightClientFinalityUpdateRPCHandler handles the /eth2/beacon_chain/req/light_client_finality_update/1/ RPC request. +func (s *Service) lightClientFinalityUpdateRPCHandler(ctx context.Context, _ interface{}, stream libp2pcore.Stream) error { + ctx, span := trace.StartSpan(ctx, "sync.lightClientFinalityUpdateRPCHandler") + defer span.End() + _, cancel := context.WithTimeout(ctx, ttfbTimeout) + defer cancel() + + // TODO: What should we log? + log := log.WithField("handler", p2p.LightClientFinalityUpdateName[1:]) // slice the leading slash off the name var + + log.Info("LC: lightClientFinalityUpdateRPCHandler invoked") + + SetRPCStreamDeadlines(stream) + if err := s.rateLimiter.validateRequest(stream, 1); err != nil { + return err + } + s.rateLimiter.add(stream, 1) + + if s.lcStore.LastLCFinalityUpdate == nil { + s.writeErrorResponseToStream(responseCodeResourceUnavailable, types.ErrResourceUnavailable.Error(), stream) + return nil + } + + SetStreamWriteDeadline(stream, defaultWriteDuration) + if err := WriteLightClientFinalityUpdateChunk(stream, s.cfg.clock, s.cfg.p2p.Encoding(), s.lcStore.LastLCFinalityUpdate); err != nil { + s.writeErrorResponseToStream(responseCodeServerError, types.ErrGeneric.Error(), stream) + tracing.AnnotateError(span, err) + return err + } + + log.Info("LC: lightClientFinalityUpdateRPCHandler completed") + + closeStream(stream, log) + return nil +} + +// lightClientOptimisticUpdateRPCHandler handles the /eth2/beacon_chain/req/light_client_optimistic_update/1/ RPC request. +func (s *Service) lightClientOptimisticUpdateRPCHandler(ctx context.Context, _ interface{}, stream libp2pcore.Stream) error { + ctx, span := trace.StartSpan(ctx, "sync.lightClientOptimisticUpdateRPCHandler") + defer span.End() + _, cancel := context.WithTimeout(ctx, ttfbTimeout) + defer cancel() + + // TODO: What should we log? + log := log.WithField("handler", p2p.LightClientOptimisticUpdateName[1:]) // slice the leading slash off the name var + + log.Info("LC: lightClientOptimisticUpdateRPCHandler invoked") + + SetRPCStreamDeadlines(stream) + if err := s.rateLimiter.validateRequest(stream, 1); err != nil { + return err + } + s.rateLimiter.add(stream, 1) + + if s.lcStore.LastLCOptimisticUpdate == nil { + s.writeErrorResponseToStream(responseCodeResourceUnavailable, types.ErrResourceUnavailable.Error(), stream) + return nil + } + + SetStreamWriteDeadline(stream, defaultWriteDuration) + if err := WriteLightClientOptimisticUpdateChunk(stream, s.cfg.clock, s.cfg.p2p.Encoding(), s.lcStore.LastLCOptimisticUpdate); err != nil { + s.writeErrorResponseToStream(responseCodeServerError, types.ErrGeneric.Error(), stream) + tracing.AnnotateError(span, err) + return err + } + + log.Info("LC: lightClientOptimisticUpdateRPCHandler completed") + + closeStream(stream, log) + return nil +} diff --git a/beacon-chain/sync/rpc_send_request.go b/beacon-chain/sync/rpc_send_request.go index 30edbb09cf51..8572d94a02af 100644 --- a/beacon-chain/sync/rpc_send_request.go +++ b/beacon-chain/sync/rpc_send_request.go @@ -25,6 +25,8 @@ import ( "github.com/sirupsen/logrus" ) +// TODO + var errBlobChunkedReadFailure = errors.New("failed to read stream of chunk-encoded blobs") var errBlobUnmarshal = errors.New("Could not unmarshal chunk-encoded blob") diff --git a/beacon-chain/sync/service.go b/beacon-chain/sync/service.go index 473d3d9709ff..f69f8c8b3a64 100644 --- a/beacon-chain/sync/service.go +++ b/beacon-chain/sync/service.go @@ -25,6 +25,7 @@ import ( "github.com/prysmaticlabs/prysm/v5/beacon-chain/db" "github.com/prysmaticlabs/prysm/v5/beacon-chain/db/filesystem" "github.com/prysmaticlabs/prysm/v5/beacon-chain/execution" + light_client "github.com/prysmaticlabs/prysm/v5/beacon-chain/light-client" "github.com/prysmaticlabs/prysm/v5/beacon-chain/operations/attestations" "github.com/prysmaticlabs/prysm/v5/beacon-chain/operations/blstoexec" "github.com/prysmaticlabs/prysm/v5/beacon-chain/operations/slashings" @@ -158,6 +159,7 @@ type Service struct { newBlobVerifier verification.NewBlobVerifier availableBlocker coverage.AvailableBlocker ctxMap ContextByteVersions + lcStore *light_client.Store } // NewService initializes new regular sync service. diff --git a/beacon-chain/sync/subscriber.go b/beacon-chain/sync/subscriber.go index 5d2f054b83d6..3406bd171832 100644 --- a/beacon-chain/sync/subscriber.go +++ b/beacon-chain/sync/subscriber.go @@ -123,6 +123,18 @@ func (s *Service) registerSubscribers(epoch primitives.Epoch, digest [4]byte) { digest, ) } + s.subscribe( + p2p.LightClientFinalityUpdateTopicFormat, + s.validateLightClientFinalityUpdate, + s.lightClientFinalityUpdateSubscriber, + digest, + ) + s.subscribe( + p2p.LightClientOptimisticUpdateTopicFormat, + s.validateLightClientOptimisticUpdate, + s.lightClientOptimisticUpdateSubscriber, + digest, + ) } // New Gossip Topic in Capella diff --git a/beacon-chain/sync/subscriber_light_client.go b/beacon-chain/sync/subscriber_light_client.go new file mode 100644 index 000000000000..8f7546c8f4b8 --- /dev/null +++ b/beacon-chain/sync/subscriber_light_client.go @@ -0,0 +1,45 @@ +package sync + +import ( + "context" + "fmt" + + "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/feed" + statefeed "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/feed/state" + "github.com/prysmaticlabs/prysm/v5/consensus-types/interfaces" + "google.golang.org/protobuf/proto" +) + +func (s *Service) lightClientFinalityUpdateSubscriber(_ context.Context, msg proto.Message) error { + update, ok := msg.(interfaces.LightClientFinalityUpdate) + if !ok { + return fmt.Errorf("message type %T is not a light client finality update", msg) + } + + log.Info("LC: storing new finality update in p2p subscriber") + s.lcStore.LastLCFinalityUpdate = update + + s.cfg.stateNotifier.StateFeed().Send(&feed.Event{ + Type: statefeed.LightClientFinalityUpdate, + Data: update, + }) + + return nil +} + +func (s *Service) lightClientOptimisticUpdateSubscriber(_ context.Context, msg proto.Message) error { + update, ok := msg.(interfaces.LightClientOptimisticUpdate) + if !ok { + return fmt.Errorf("message type %T is not a light client optimistic update", msg) + } + + log.Info("LC: storing new optimistic update in p2p subscriber") + s.lcStore.LastLCOptimisticUpdate = update + + s.cfg.stateNotifier.StateFeed().Send(&feed.Event{ + Type: statefeed.LightClientOptimisticUpdate, + Data: update, + }) + + return nil +} diff --git a/beacon-chain/sync/validate_light_client.go b/beacon-chain/sync/validate_light_client.go new file mode 100644 index 000000000000..5634c43b7f53 --- /dev/null +++ b/beacon-chain/sync/validate_light_client.go @@ -0,0 +1,128 @@ +package sync + +import ( + "context" + "time" + + pubsub "github.com/libp2p/go-libp2p-pubsub" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/prysmaticlabs/prysm/v5/config/params" + "github.com/prysmaticlabs/prysm/v5/consensus-types/interfaces" + "github.com/prysmaticlabs/prysm/v5/monitoring/tracing" + "github.com/prysmaticlabs/prysm/v5/monitoring/tracing/trace" + "github.com/prysmaticlabs/prysm/v5/time/slots" +) + +func (s *Service) validateLightClientFinalityUpdate(ctx context.Context, pid peer.ID, msg *pubsub.Message) (pubsub.ValidationResult, error) { + // Validation runs on publish (not just subscriptions), so we should approve any message from + // ourselves. + if pid == s.cfg.p2p.PeerID() { + return pubsub.ValidationAccept, nil + } + + // TODO keep? + // The head state will be too far away to validate any execution change. + if s.cfg.initialSync.Syncing() { + return pubsub.ValidationIgnore, nil + } + + _, span := trace.StartSpan(ctx, "sync.validateLightClientFinalityUpdate") + defer span.End() + + log.Info("LC: p2p validateLightClientFinalityUpdate invoked") + + m, err := s.decodePubsubMessage(msg) + if err != nil { + tracing.AnnotateError(span, err) + return pubsub.ValidationReject, err + } + + update, ok := m.(interfaces.LightClientFinalityUpdate) + if !ok { + return pubsub.ValidationReject, errWrongMessage + } + + maxActiveParticipants := update.SyncAggregate().SyncCommitteeBits.Len() + numActiveParticipants := update.SyncAggregate().SyncCommitteeBits.Count() + hasSupermajority := numActiveParticipants*3 >= maxActiveParticipants*2 + + last := s.lcStore.LastLCFinalityUpdate + if last != nil { + // [IGNORE] The finalized_header.beacon.slot is greater than that of all previously forwarded finality_updates, + // or it matches the highest previously forwarded slot and also has a sync_aggregate indicating supermajority (> 2/3) + // sync committee participation while the previously forwarded finality_update for that slot did not indicate supermajority + slot := last.FinalizedHeader().Beacon().Slot + lastMaxActiveParticipants := last.SyncAggregate().SyncCommitteeBits.Len() + lastNumActiveParticipants := last.SyncAggregate().SyncCommitteeBits.Count() + lastHasSupermajority := lastNumActiveParticipants*3 >= lastMaxActiveParticipants*2 + + if update.FinalizedHeader().Beacon().Slot < slot { + return pubsub.ValidationIgnore, nil + } + if update.FinalizedHeader().Beacon().Slot == slot && (lastHasSupermajority || !hasSupermajority) { + return pubsub.ValidationIgnore, nil + } + } + // [IGNORE] The finality_update is received after the block at signature_slot was given enough time + // to propagate through the network -- i.e. validate that one-third of finality_update.signature_slot + // has transpired (SECONDS_PER_SLOT / INTERVALS_PER_SLOT seconds after the start of the slot, + // with a MAXIMUM_GOSSIP_CLOCK_DISPARITY allowance) + earliestValidTime := slots.StartTime(uint64(s.cfg.clock.GenesisTime().Unix()), update.FinalizedHeader().Beacon().Slot). + Add(time.Second * time.Duration(params.BeaconConfig().SecondsPerSlot/params.BeaconConfig().IntervalsPerSlot)). + Add(-params.BeaconConfig().MaximumGossipClockDisparityDuration()) + if s.cfg.clock.Now().Before(earliestValidTime) { + return pubsub.ValidationIgnore, nil + } + + return pubsub.ValidationAccept, nil +} + +func (s *Service) validateLightClientOptimisticUpdate(ctx context.Context, pid peer.ID, msg *pubsub.Message) (pubsub.ValidationResult, error) { + // Validation runs on publish (not just subscriptions), so we should approve any message from + // ourselves. + if pid == s.cfg.p2p.PeerID() { + return pubsub.ValidationAccept, nil + } + + // TODO keep? + // The head state will be too far away to validate any execution change. + if s.cfg.initialSync.Syncing() { + return pubsub.ValidationIgnore, nil + } + + _, span := trace.StartSpan(ctx, "sync.validateLightClientOptimisticUpdate") + defer span.End() + + log.Info("LC: p2p validateLightClientOptimisticUpdate invoked") + + m, err := s.decodePubsubMessage(msg) + if err != nil { + tracing.AnnotateError(span, err) + return pubsub.ValidationReject, err + } + + update, ok := m.(interfaces.LightClientOptimisticUpdate) + if !ok { + return pubsub.ValidationReject, errWrongMessage + } + + last := s.lcStore.LastLCOptimisticUpdate + if last != nil { + // [IGNORE] The attested_header.beacon.slot is greater than that of all previously forwarded optimistic_updates + if update.AttestedHeader().Beacon().Slot <= last.AttestedHeader().Beacon().Slot { + return pubsub.ValidationIgnore, nil + } + } + // [IGNORE] The optimistic_update is received after the block at signature_slot was given enough time + // to propagate through the network -- i.e. validate that one-third of optimistic_update.signature_slot + // has transpired (SECONDS_PER_SLOT / INTERVALS_PER_SLOT seconds after the start of the slot, + // with a MAXIMUM_GOSSIP_CLOCK_DISPARITY allowance) + earliestValidTime := slots.StartTime(uint64(s.cfg.clock.GenesisTime().Unix()), update.AttestedHeader().Beacon().Slot). + Add(time.Second * time.Duration(params.BeaconConfig().SecondsPerSlot/params.BeaconConfig().IntervalsPerSlot)). + Add(-params.BeaconConfig().MaximumGossipClockDisparityDuration()) + if s.cfg.clock.Now().Before(earliestValidTime) { + return pubsub.ValidationIgnore, nil + } + + return pubsub.ValidationAccept, nil +} diff --git a/consensus-types/interfaces/light_client.go b/consensus-types/interfaces/light_client.go index b0d0dadaa2e4..ccc9cbbcadca 100644 --- a/consensus-types/interfaces/light_client.go +++ b/consensus-types/interfaces/light_client.go @@ -27,8 +27,11 @@ type LightClientBootstrap interface { ssz.Marshaler Version() int Header() LightClientHeader + SetHeader(header LightClientHeader) error CurrentSyncCommittee() *pb.SyncCommittee + SetCurrentSyncCommittee(sc *pb.SyncCommittee) error CurrentSyncCommitteeBranch() (LightClientSyncCommitteeBranch, error) + SetCurrentSyncCommitteeBranch(branch [][]byte) error CurrentSyncCommitteeBranchElectra() (LightClientSyncCommitteeBranchElectra, error) } diff --git a/consensus-types/light-client/bootstrap.go b/consensus-types/light-client/bootstrap.go index 1de819266d03..be943d4ddc1f 100644 --- a/consensus-types/light-client/bootstrap.go +++ b/consensus-types/light-client/bootstrap.go @@ -41,10 +41,16 @@ func NewWrappedBootstrapAltair(p *pb.LightClientBootstrapAltair) (interfaces.Lig if p == nil { return nil, consensustypes.ErrNilObjectWrapped } - header, err := NewWrappedHeader(p.Header) - if err != nil { - return nil, err + + var header interfaces.LightClientHeader + var err error + if p.Header != nil { + header, err = NewWrappedHeader(p.Header) + if err != nil { + return nil, err + } } + branch, err := createBranch[interfaces.LightClientSyncCommitteeBranch]( "sync committee", p.CurrentSyncCommitteeBranch, @@ -81,14 +87,42 @@ func (h *bootstrapAltair) Header() interfaces.LightClientHeader { return h.header } +func (h *bootstrapAltair) SetHeader(header interfaces.LightClientHeader) error { + p, ok := (header.Proto()).(*pb.LightClientHeaderAltair) + if !ok { + return fmt.Errorf("header type %T is not %T", p, &pb.LightClientHeaderAltair{}) + } + h.p.Header = p + h.header = header + return nil +} + func (h *bootstrapAltair) CurrentSyncCommittee() *pb.SyncCommittee { return h.p.CurrentSyncCommittee } +func (h *bootstrapAltair) SetCurrentSyncCommittee(sc *pb.SyncCommittee) error { + h.p.CurrentSyncCommittee = sc + return nil +} + func (h *bootstrapAltair) CurrentSyncCommitteeBranch() (interfaces.LightClientSyncCommitteeBranch, error) { return h.currentSyncCommitteeBranch, nil } +func (h *bootstrapAltair) SetCurrentSyncCommitteeBranch(branch [][]byte) error { + if len(branch) != fieldparams.SyncCommitteeBranchDepth { + return fmt.Errorf("branch length %d is not %d", len(branch), fieldparams.SyncCommitteeBranchDepth) + } + newBranch := [fieldparams.SyncCommitteeBranchDepth][32]byte{} + for i, root := range branch { + copy(newBranch[i][:], root) + } + h.currentSyncCommitteeBranch = newBranch + h.p.CurrentSyncCommitteeBranch = branch + return nil +} + func (h *bootstrapAltair) CurrentSyncCommitteeBranchElectra() (interfaces.LightClientSyncCommitteeBranchElectra, error) { return [6][32]byte{}, consensustypes.ErrNotSupported("CurrentSyncCommitteeBranchElectra", version.Altair) } @@ -105,10 +139,16 @@ func NewWrappedBootstrapCapella(p *pb.LightClientBootstrapCapella) (interfaces.L if p == nil { return nil, consensustypes.ErrNilObjectWrapped } - header, err := NewWrappedHeader(p.Header) - if err != nil { - return nil, err + + var header interfaces.LightClientHeader + var err error + if p.Header != nil { + header, err = NewWrappedHeader(p.Header) + if err != nil { + return nil, err + } } + branch, err := createBranch[interfaces.LightClientSyncCommitteeBranch]( "sync committee", p.CurrentSyncCommitteeBranch, @@ -145,14 +185,42 @@ func (h *bootstrapCapella) Header() interfaces.LightClientHeader { return h.header } +func (h *bootstrapCapella) SetHeader(header interfaces.LightClientHeader) error { + p, ok := (header.Proto()).(*pb.LightClientHeaderCapella) + if !ok { + return fmt.Errorf("header type %T is not %T", p, &pb.LightClientHeaderCapella{}) + } + h.p.Header = p + h.header = header + return nil +} + func (h *bootstrapCapella) CurrentSyncCommittee() *pb.SyncCommittee { return h.p.CurrentSyncCommittee } +func (h *bootstrapCapella) SetCurrentSyncCommittee(sc *pb.SyncCommittee) error { + h.p.CurrentSyncCommittee = sc + return nil +} + func (h *bootstrapCapella) CurrentSyncCommitteeBranch() (interfaces.LightClientSyncCommitteeBranch, error) { return h.currentSyncCommitteeBranch, nil } +func (h *bootstrapCapella) SetCurrentSyncCommitteeBranch(branch [][]byte) error { + if len(branch) != fieldparams.SyncCommitteeBranchDepth { + return fmt.Errorf("branch length %d is not %d", len(branch), fieldparams.SyncCommitteeBranchDepth) + } + newBranch := [fieldparams.SyncCommitteeBranchDepth][32]byte{} + for i, root := range branch { + copy(newBranch[i][:], root) + } + h.currentSyncCommitteeBranch = newBranch + h.p.CurrentSyncCommitteeBranch = branch + return nil +} + func (h *bootstrapCapella) CurrentSyncCommitteeBranchElectra() (interfaces.LightClientSyncCommitteeBranchElectra, error) { return [6][32]byte{}, consensustypes.ErrNotSupported("CurrentSyncCommitteeBranchElectra", version.Capella) } @@ -169,10 +237,16 @@ func NewWrappedBootstrapDeneb(p *pb.LightClientBootstrapDeneb) (interfaces.Light if p == nil { return nil, consensustypes.ErrNilObjectWrapped } - header, err := NewWrappedHeader(p.Header) - if err != nil { - return nil, err + + var header interfaces.LightClientHeader + var err error + if p.Header != nil { + header, err = NewWrappedHeader(p.Header) + if err != nil { + return nil, err + } } + branch, err := createBranch[interfaces.LightClientSyncCommitteeBranch]( "sync committee", p.CurrentSyncCommitteeBranch, @@ -209,14 +283,42 @@ func (h *bootstrapDeneb) Header() interfaces.LightClientHeader { return h.header } +func (h *bootstrapDeneb) SetHeader(header interfaces.LightClientHeader) error { + p, ok := (header.Proto()).(*pb.LightClientHeaderDeneb) + if !ok { + return fmt.Errorf("header type %T is not %T", p, &pb.LightClientHeaderDeneb{}) + } + h.p.Header = p + h.header = header + return nil +} + func (h *bootstrapDeneb) CurrentSyncCommittee() *pb.SyncCommittee { return h.p.CurrentSyncCommittee } +func (h *bootstrapDeneb) SetCurrentSyncCommittee(sc *pb.SyncCommittee) error { + h.p.CurrentSyncCommittee = sc + return nil +} + func (h *bootstrapDeneb) CurrentSyncCommitteeBranch() (interfaces.LightClientSyncCommitteeBranch, error) { return h.currentSyncCommitteeBranch, nil } +func (h *bootstrapDeneb) SetCurrentSyncCommitteeBranch(branch [][]byte) error { + if len(branch) != fieldparams.SyncCommitteeBranchDepth { + return fmt.Errorf("branch length %d is not %d", len(branch), fieldparams.SyncCommitteeBranchDepth) + } + newBranch := [fieldparams.SyncCommitteeBranchDepth][32]byte{} + for i, root := range branch { + copy(newBranch[i][:], root) + } + h.currentSyncCommitteeBranch = newBranch + h.p.CurrentSyncCommitteeBranch = branch + return nil +} + func (h *bootstrapDeneb) CurrentSyncCommitteeBranchElectra() (interfaces.LightClientSyncCommitteeBranchElectra, error) { return [6][32]byte{}, consensustypes.ErrNotSupported("CurrentSyncCommitteeBranchElectra", version.Deneb) } @@ -233,10 +335,16 @@ func NewWrappedBootstrapElectra(p *pb.LightClientBootstrapElectra) (interfaces.L if p == nil { return nil, consensustypes.ErrNilObjectWrapped } - header, err := NewWrappedHeader(p.Header) - if err != nil { - return nil, err + + var header interfaces.LightClientHeader + var err error + if p.Header != nil { + header, err = NewWrappedHeader(p.Header) + if err != nil { + return nil, err + } } + branch, err := createBranch[interfaces.LightClientSyncCommitteeBranchElectra]( "sync committee", p.CurrentSyncCommitteeBranch, @@ -273,14 +381,42 @@ func (h *bootstrapElectra) Header() interfaces.LightClientHeader { return h.header } +func (h *bootstrapElectra) SetHeader(header interfaces.LightClientHeader) error { + p, ok := (header.Proto()).(*pb.LightClientHeaderDeneb) + if !ok { + return fmt.Errorf("header type %T is not %T", p, &pb.LightClientHeaderDeneb{}) + } + h.p.Header = p + h.header = header + return nil +} + func (h *bootstrapElectra) CurrentSyncCommittee() *pb.SyncCommittee { return h.p.CurrentSyncCommittee } +func (h *bootstrapElectra) SetCurrentSyncCommittee(sc *pb.SyncCommittee) error { + h.p.CurrentSyncCommittee = sc + return nil +} + func (h *bootstrapElectra) CurrentSyncCommitteeBranch() (interfaces.LightClientSyncCommitteeBranch, error) { return [5][32]byte{}, consensustypes.ErrNotSupported("CurrentSyncCommitteeBranch", version.Electra) } +func (h *bootstrapElectra) SetCurrentSyncCommitteeBranch(branch [][]byte) error { + if len(branch) != fieldparams.SyncCommitteeBranchDepthElectra { + return fmt.Errorf("branch length %d is not %d", len(branch), fieldparams.SyncCommitteeBranchDepthElectra) + } + newBranch := [fieldparams.SyncCommitteeBranchDepthElectra][32]byte{} + for i, root := range branch { + copy(newBranch[i][:], root) + } + h.currentSyncCommitteeBranch = newBranch + h.p.CurrentSyncCommitteeBranch = branch + return nil +} + func (h *bootstrapElectra) CurrentSyncCommitteeBranchElectra() (interfaces.LightClientSyncCommitteeBranchElectra, error) { return h.currentSyncCommitteeBranch, nil } diff --git a/proto/prysm/v1alpha1/BUILD.bazel b/proto/prysm/v1alpha1/BUILD.bazel index 51ec7d961645..8d0ca5acf80e 100644 --- a/proto/prysm/v1alpha1/BUILD.bazel +++ b/proto/prysm/v1alpha1/BUILD.bazel @@ -263,6 +263,7 @@ ssz_gen_marshal( objs = [ "BeaconBlocksByRangeRequest", "BlobSidecarsByRangeRequest", + "LightClientUpdatesByRangeReq", "MetaDataV0", "MetaDataV1", "SignedValidatorRegistrationV1", diff --git a/proto/prysm/v1alpha1/electra.ssz.go b/proto/prysm/v1alpha1/electra.ssz.go index 95a8c2fe87e0..a7aa9f14d626 100644 --- a/proto/prysm/v1alpha1/electra.ssz.go +++ b/proto/prysm/v1alpha1/electra.ssz.go @@ -1,5 +1,5 @@ // Code generated by fastssz. DO NOT EDIT. -// Hash: 734bb55036e8fc3dc75bec7202483b74acd4f33b48e3e9956ffd5cd8de0a07f1 +// Hash: 3fab62924eab85198e9eb6083c1f069c358eb6a246f290d56ed86fdffad76c95 package eth import ( diff --git a/proto/prysm/v1alpha1/non-core.ssz.go b/proto/prysm/v1alpha1/non-core.ssz.go index 2630eec1ccb2..859a9dc1b3e2 100644 --- a/proto/prysm/v1alpha1/non-core.ssz.go +++ b/proto/prysm/v1alpha1/non-core.ssz.go @@ -1,5 +1,5 @@ // Code generated by fastssz. DO NOT EDIT. -// Hash: bfd7d6b556134c3bd236b880245717aa01ae79573b33f2746a08c165ba5dcedb +// Hash: 61016ffa736065f82cfed43813dfb5f1b43b775b91b71470c66da266ac9fd837 package eth import ( @@ -611,6 +611,66 @@ func (b *BlobSidecarsByRangeRequest) HashTreeRootWith(hh *ssz.Hasher) (err error return } +// MarshalSSZ ssz marshals the LightClientUpdatesByRangeReq object +func (l *LightClientUpdatesByRangeReq) MarshalSSZ() ([]byte, error) { + return ssz.MarshalSSZ(l) +} + +// MarshalSSZTo ssz marshals the LightClientUpdatesByRangeReq object to a target array +func (l *LightClientUpdatesByRangeReq) MarshalSSZTo(buf []byte) (dst []byte, err error) { + dst = buf + + // Field (0) 'StartPeriod' + dst = ssz.MarshalUint64(dst, l.StartPeriod) + + // Field (1) 'Count' + dst = ssz.MarshalUint64(dst, l.Count) + + return +} + +// UnmarshalSSZ ssz unmarshals the LightClientUpdatesByRangeReq object +func (l *LightClientUpdatesByRangeReq) UnmarshalSSZ(buf []byte) error { + var err error + size := uint64(len(buf)) + if size != 16 { + return ssz.ErrSize + } + + // Field (0) 'StartPeriod' + l.StartPeriod = ssz.UnmarshallUint64(buf[0:8]) + + // Field (1) 'Count' + l.Count = ssz.UnmarshallUint64(buf[8:16]) + + return err +} + +// SizeSSZ returns the ssz encoded size in bytes for the LightClientUpdatesByRangeReq object +func (l *LightClientUpdatesByRangeReq) SizeSSZ() (size int) { + size = 16 + return +} + +// HashTreeRoot ssz hashes the LightClientUpdatesByRangeReq object +func (l *LightClientUpdatesByRangeReq) HashTreeRoot() ([32]byte, error) { + return ssz.HashWithDefaultHasher(l) +} + +// HashTreeRootWith ssz hashes the LightClientUpdatesByRangeReq object with a hasher +func (l *LightClientUpdatesByRangeReq) HashTreeRootWith(hh *ssz.Hasher) (err error) { + indx := hh.Index() + + // Field (0) 'StartPeriod' + hh.PutUint64(l.StartPeriod) + + // Field (1) 'Count' + hh.PutUint64(l.Count) + + hh.Merkleize(indx) + return +} + // MarshalSSZ ssz marshals the DepositSnapshot object func (d *DepositSnapshot) MarshalSSZ() ([]byte, error) { return ssz.MarshalSSZ(d) diff --git a/proto/prysm/v1alpha1/p2p_messages.pb.go b/proto/prysm/v1alpha1/p2p_messages.pb.go index 5afd437c2f71..97bb81dfab31 100755 --- a/proto/prysm/v1alpha1/p2p_messages.pb.go +++ b/proto/prysm/v1alpha1/p2p_messages.pb.go @@ -403,6 +403,61 @@ func (x *BlobSidecarsByRangeRequest) GetCount() uint64 { return 0 } +type LightClientUpdatesByRangeReq struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + StartPeriod uint64 `protobuf:"varint,1,opt,name=start_period,json=startPeriod,proto3" json:"start_period,omitempty"` + Count uint64 `protobuf:"varint,2,opt,name=count,proto3" json:"count,omitempty"` +} + +func (x *LightClientUpdatesByRangeReq) Reset() { + *x = LightClientUpdatesByRangeReq{} + if protoimpl.UnsafeEnabled { + mi := &file_proto_prysm_v1alpha1_p2p_messages_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *LightClientUpdatesByRangeReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LightClientUpdatesByRangeReq) ProtoMessage() {} + +func (x *LightClientUpdatesByRangeReq) ProtoReflect() protoreflect.Message { + mi := &file_proto_prysm_v1alpha1_p2p_messages_proto_msgTypes[6] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LightClientUpdatesByRangeReq.ProtoReflect.Descriptor instead. +func (*LightClientUpdatesByRangeReq) Descriptor() ([]byte, []int) { + return file_proto_prysm_v1alpha1_p2p_messages_proto_rawDescGZIP(), []int{6} +} + +func (x *LightClientUpdatesByRangeReq) GetStartPeriod() uint64 { + if x != nil { + return x.StartPeriod + } + return 0 +} + +func (x *LightClientUpdatesByRangeReq) GetCount() uint64 { + if x != nil { + return x.Count + } + return 0 +} + var File_proto_prysm_v1alpha1_p2p_messages_proto protoreflect.FileDescriptor var file_proto_prysm_v1alpha1_p2p_messages_proto_rawDesc = []byte{ @@ -492,17 +547,23 @@ var file_proto_prysm_v1alpha1_p2p_messages_proto_rawDesc = []byte{ 0x75, 0x73, 0x2d, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2f, 0x70, 0x72, 0x69, 0x6d, 0x69, 0x74, 0x69, 0x76, 0x65, 0x73, 0x2e, 0x53, 0x6c, 0x6f, 0x74, 0x52, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x53, 0x6c, 0x6f, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x04, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x42, 0x9b, 0x01, 0x0a, 0x19, 0x6f, 0x72, - 0x67, 0x2e, 0x65, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x2e, 0x65, 0x74, 0x68, 0x2e, 0x76, - 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x42, 0x10, 0x50, 0x32, 0x50, 0x4d, 0x65, 0x73, 0x73, - 0x61, 0x67, 0x65, 0x73, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x3a, 0x67, 0x69, 0x74, - 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x72, 0x79, 0x73, 0x6d, 0x61, 0x74, 0x69, - 0x63, 0x6c, 0x61, 0x62, 0x73, 0x2f, 0x70, 0x72, 0x79, 0x73, 0x6d, 0x2f, 0x76, 0x35, 0x2f, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x70, 0x72, 0x79, 0x73, 0x6d, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70, - 0x68, 0x61, 0x31, 0x3b, 0x65, 0x74, 0x68, 0xaa, 0x02, 0x15, 0x45, 0x74, 0x68, 0x65, 0x72, 0x65, - 0x75, 0x6d, 0x2e, 0x45, 0x74, 0x68, 0x2e, 0x56, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0xca, - 0x02, 0x15, 0x45, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x5c, 0x45, 0x74, 0x68, 0x5c, 0x76, - 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x28, 0x04, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x57, 0x0a, 0x1c, 0x4c, 0x69, 0x67, + 0x68, 0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x73, 0x42, + 0x79, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x65, 0x71, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x74, 0x61, + 0x72, 0x74, 0x5f, 0x70, 0x65, 0x72, 0x69, 0x6f, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, + 0x0b, 0x73, 0x74, 0x61, 0x72, 0x74, 0x50, 0x65, 0x72, 0x69, 0x6f, 0x64, 0x12, 0x14, 0x0a, 0x05, + 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x63, 0x6f, 0x75, + 0x6e, 0x74, 0x42, 0x9b, 0x01, 0x0a, 0x19, 0x6f, 0x72, 0x67, 0x2e, 0x65, 0x74, 0x68, 0x65, 0x72, + 0x65, 0x75, 0x6d, 0x2e, 0x65, 0x74, 0x68, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, + 0x42, 0x10, 0x50, 0x32, 0x50, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x50, 0x72, 0x6f, + 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x3a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, + 0x2f, 0x70, 0x72, 0x79, 0x73, 0x6d, 0x61, 0x74, 0x69, 0x63, 0x6c, 0x61, 0x62, 0x73, 0x2f, 0x70, + 0x72, 0x79, 0x73, 0x6d, 0x2f, 0x76, 0x35, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x70, 0x72, + 0x79, 0x73, 0x6d, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x3b, 0x65, 0x74, 0x68, + 0xaa, 0x02, 0x15, 0x45, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x2e, 0x45, 0x74, 0x68, 0x2e, + 0x56, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0xca, 0x02, 0x15, 0x45, 0x74, 0x68, 0x65, 0x72, + 0x65, 0x75, 0x6d, 0x5c, 0x45, 0x74, 0x68, 0x5c, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, + 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -517,14 +578,15 @@ func file_proto_prysm_v1alpha1_p2p_messages_proto_rawDescGZIP() []byte { return file_proto_prysm_v1alpha1_p2p_messages_proto_rawDescData } -var file_proto_prysm_v1alpha1_p2p_messages_proto_msgTypes = make([]protoimpl.MessageInfo, 6) +var file_proto_prysm_v1alpha1_p2p_messages_proto_msgTypes = make([]protoimpl.MessageInfo, 7) var file_proto_prysm_v1alpha1_p2p_messages_proto_goTypes = []interface{}{ - (*Status)(nil), // 0: ethereum.eth.v1alpha1.Status - (*BeaconBlocksByRangeRequest)(nil), // 1: ethereum.eth.v1alpha1.BeaconBlocksByRangeRequest - (*ENRForkID)(nil), // 2: ethereum.eth.v1alpha1.ENRForkID - (*MetaDataV0)(nil), // 3: ethereum.eth.v1alpha1.MetaDataV0 - (*MetaDataV1)(nil), // 4: ethereum.eth.v1alpha1.MetaDataV1 - (*BlobSidecarsByRangeRequest)(nil), // 5: ethereum.eth.v1alpha1.BlobSidecarsByRangeRequest + (*Status)(nil), // 0: ethereum.eth.v1alpha1.Status + (*BeaconBlocksByRangeRequest)(nil), // 1: ethereum.eth.v1alpha1.BeaconBlocksByRangeRequest + (*ENRForkID)(nil), // 2: ethereum.eth.v1alpha1.ENRForkID + (*MetaDataV0)(nil), // 3: ethereum.eth.v1alpha1.MetaDataV0 + (*MetaDataV1)(nil), // 4: ethereum.eth.v1alpha1.MetaDataV1 + (*BlobSidecarsByRangeRequest)(nil), // 5: ethereum.eth.v1alpha1.BlobSidecarsByRangeRequest + (*LightClientUpdatesByRangeReq)(nil), // 6: ethereum.eth.v1alpha1.LightClientUpdatesByRangeReq } var file_proto_prysm_v1alpha1_p2p_messages_proto_depIdxs = []int32{ 0, // [0:0] is the sub-list for method output_type @@ -612,6 +674,18 @@ func file_proto_prysm_v1alpha1_p2p_messages_proto_init() { return nil } } + file_proto_prysm_v1alpha1_p2p_messages_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*LightClientUpdatesByRangeReq); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } type x struct{} out := protoimpl.TypeBuilder{ @@ -619,7 +693,7 @@ func file_proto_prysm_v1alpha1_p2p_messages_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_proto_prysm_v1alpha1_p2p_messages_proto_rawDesc, NumEnums: 0, - NumMessages: 6, + NumMessages: 7, NumExtensions: 0, NumServices: 0, }, diff --git a/proto/prysm/v1alpha1/p2p_messages.proto b/proto/prysm/v1alpha1/p2p_messages.proto index 8ba1d36c88cd..4ede0604566f 100644 --- a/proto/prysm/v1alpha1/p2p_messages.proto +++ b/proto/prysm/v1alpha1/p2p_messages.proto @@ -70,3 +70,16 @@ message BlobSidecarsByRangeRequest { uint64 start_slot = 1 [(ethereum.eth.ext.cast_type) = "github.com/prysmaticlabs/prysm/v5/consensus-types/primitives.Slot"]; uint64 count = 2; } + +/* + Spec Definition: + ( + start_period: uint64 + count: uint64 + ) +*/ +message LightClientUpdatesByRangeReq { + uint64 start_period = 1; + uint64 count = 2; +} +