Skip to content

Commit

Permalink
save light client updates (#14618)
Browse files Browse the repository at this point in the history
* Light Client - use the new consensus types (#14549)

* in progress

* completed logic

* var name

* additional logic changes

* fix createDefaultLightClientUpdate

* empty fields

* unused context

* Return the correct light client payload proof (#14565)

* Return the correct payload proof

* changelog <3

* Set fields of wrapped proto object in light client setters (#14573)

* Set fields of wrapped proto object in light client setters

* changelog <3

* fix TODOs for events (#14570)

* fix TODOs for events

* address review comments

* Update beacon-chain/rpc/eth/events/events.go

Co-authored-by: Radosław Kapka <radoslaw.kapka@gmail.com>

* Update beacon-chain/rpc/eth/events/events.go

Co-authored-by: Radosław Kapka <radoslaw.kapka@gmail.com>

* nits

---------

Co-authored-by: Radosław Kapka <radoslaw.kapka@gmail.com>

* align changelog with develop branch

* bzl

* LC Updates by Range to read from DB (#14531)

* change updatebyrange

* lcupdateresponse from consensus

* range altair test

* range forks tests

* finish tests

* changelog

* remove unused functions

* Update beacon-chain/rpc/eth/light-client/handlers.go

Co-authored-by: Radosław Kapka <radoslaw.kapka@gmail.com>

* Update beacon-chain/rpc/eth/light-client/handlers.go

Co-authored-by: Radosław Kapka <radoslaw.kapka@gmail.com>

* use slice instead of array

* refactor code

* refactor tests

* refactor tests

* refactor tests

* add configCleanup in tests

* refactor missing updates testcase

* Light Client - use the new consensus types (#14549)

* in progress

* completed logic

* var name

* additional logic changes

* fix createDefaultLightClientUpdate

* empty fields

* unused context

* change updatesByRange to use new structs

* Light Client - use the new consensus types (#14549)

* in progress

* completed logic

* var name

* additional logic changes

* fix createDefaultLightClientUpdate

* empty fields

* unused context

* fix rpc/helpers_test

* Return the correct light client payload proof (#14565)

* Return the correct payload proof

* changelog <3

* merge

* Set fields of wrapped proto object in light client setters (#14573)

* Set fields of wrapped proto object in light client setters

* changelog <3

* fixing tests...

* core tests fixed

* kv tests fixed

* fix TODOs for events (#14570)

* fix TODOs for events

* address review comments

* Update beacon-chain/rpc/eth/events/events.go

Co-authored-by: Radosław Kapka <radoslaw.kapka@gmail.com>

* Update beacon-chain/rpc/eth/events/events.go

Co-authored-by: Radosław Kapka <radoslaw.kapka@gmail.com>

* nits

---------

Co-authored-by: Radosław Kapka <radoslaw.kapka@gmail.com>

* tests fixed

* remove unused function

* fix slice capacity

* address issues

* address issues

* fix circular import error

* remove unused func

* fix changelog

---------

Co-authored-by: Radosław Kapka <radoslaw.kapka@gmail.com>
Co-authored-by: Radosław Kapka <rkapka@wp.pl>
Co-authored-by: Rupam Dey <rpmdey2004@gmail.com>

* Light Client: Electra finality branch (#14597)

* extract from lc-p2p branch

* generate code

* trixy's review

* test fixes

* fix issue in `CreateDefaultLightClientUpdate` function (#14585)

* use state in `CreateDefaultLightClientUpdate`

* lint

* add `stateSlot` to `update.go` structs

* Revert "add `stateSlot` to `update.go` structs"

This reverts commit 84468ae.

* set sync committee based on attestedHeader in updateElectra

* dependencies

* add check to `SetNextSyncCommitteeBranchElectra`

* add detailed error messages to `update.go`

* dependencies

* fix `createDefaultLightClientUpdate`

* deps

* fix errors

* deps

* revert error messages

* deps

* save update

* save update

* move create lc bootstrap to core

* bootstrap db

* save bootstrap

* testing

* testing progress

* testing

* testing

* checkpoint working

* set genesis time manually

* savebootstrap tests

* conflicts resolved

* fix lint issues

* fix lint issues

* address reviews

* revert changes to consensus-types/lc/updates.go

* add lightClientHeaderToJSON support for Electra

---------

Co-authored-by: Radosław Kapka <rkapka@wp.pl>
Co-authored-by: Rupam Dey <rpmdey2004@gmail.com>
Co-authored-by: Radosław Kapka <radoslaw.kapka@gmail.com>
Co-authored-by: Inspector-Butters <mohamadbastin@gmail.com>
5 people committed Nov 29, 2024
1 parent fe9e5de commit 6436a08
Showing 19 changed files with 1,601 additions and 919 deletions.
76 changes: 38 additions & 38 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -34,7 +34,7 @@ The format is based on Keep a Changelog, and this project adheres to Semantic Ve
- Updated the default `scrape-interval` in `Client-stats` to 2 minutes to accommodate Beaconcha.in API rate limits.
- Switch to compounding when consolidating with source==target.
- Revert block db save when saving state fails.
- Return false from HasBlock if the block is being synced.
- Return false from HasBlock if the block is being synced.
- Cleanup forkchoice on failed insertions.
- Use read only validator for core processing to avoid unnecessary copying.
- Use ROBlock across block processing pipeline.
@@ -88,9 +88,9 @@ The format is based on Keep a Changelog, and this project adheres to Semantic Ve

### Security

## [v5.1.2](https://github.com/prysmaticlabs/prysm/compare/v5.1.1...v5.1.2) - 2024-10-16
## [v5.1.2](https://github.com/prysmaticlabs/prysm/compare/v5.1.1...v5.1.2) - 2024-10-16

This is a hotfix release with one change.
This is a hotfix release with one change.

Prysm v5.1.1 contains an updated implementation of the beacon api streaming events endpoint. This
new implementation contains a bug that can cause a panic in certain conditions. The issue is
@@ -102,20 +102,20 @@ prysm REST mode validator (a feature which requires the validator to be configur
api intead of prysm's stock grpc endpoints) or accessory software that connects to the events api,
like https://github.com/ethpandaops/ethereum-metrics-exporter

### Fixed
### Fixed

- Recover from panics when writing the event stream [#14545](https://github.com/prysmaticlabs/prysm/pull/14545)

## [v5.1.1](https://github.com/prysmaticlabs/prysm/compare/v5.1.0...v5.1.1) - 2024-10-15

This release has a number of features and improvements. Most notably, the feature flag
`--enable-experimental-state` has been flipped to "opt out" via `--disable-experimental-state`.
This release has a number of features and improvements. Most notably, the feature flag
`--enable-experimental-state` has been flipped to "opt out" via `--disable-experimental-state`.
The experimental state management design has shown significant improvements in memory usage at
runtime. Updates to libp2p's gossipsub have some bandwidith stability improvements with support for
IDONTWANT control messages.
IDONTWANT control messages.

The gRPC gateway has been deprecated from Prysm in this release. If you need JSON data, consider the
standardized beacon-APIs.
standardized beacon-APIs.

Updating to this release is recommended at your convenience.

@@ -157,7 +157,7 @@ Updating to this release is recommended at your convenience.
- `grpc-gateway-corsdomain` is renamed to http-cors-domain. The old name can still be used as an alias.
- `api-timeout` is changed from int flag to duration flag, default value updated.
- Light client support: abstracted out the light client headers with different versions.
- `ApplyToEveryValidator` has been changed to prevent misuse bugs, it takes a closure that takes a `ReadOnlyValidator` and returns a raw pointer to a `Validator`.
- `ApplyToEveryValidator` has been changed to prevent misuse bugs, it takes a closure that takes a `ReadOnlyValidator` and returns a raw pointer to a `Validator`.
- Removed gorilla mux library and replaced it with net/http updates in go 1.22.
- Clean up `ProposeBlock` for validator client to reduce cognitive scoring and enable further changes.
- Updated k8s-io/client-go to v0.30.4 and k8s-io/apimachinery to v0.30.4
@@ -168,7 +168,7 @@ Updating to this release is recommended at your convenience.
- Updated Sepolia bootnodes.
- Make committee aware packing the default by deprecating `--enable-committee-aware-packing`.
- Moved `ConvertKzgCommitmentToVersionedHash` to the `primitives` package.
- Updated correlation penalty for EIP-7251.
- Updated correlation penalty for EIP-7251.

### Deprecated
- `--disable-grpc-gateway` flag is deprecated due to grpc gateway removal.
@@ -641,34 +641,34 @@ AVX support (eg Celeron) after the Deneb fork. This is not an issue for mainnet.

- Linter: Wastedassign linter enabled to improve code quality.
- API Enhancements:
- Added payload return in Wei for /eth/v3/validator/blocks.
- Added Holesky Deneb Epoch for better epoch management.
- Added payload return in Wei for /eth/v3/validator/blocks.
- Added Holesky Deneb Epoch for better epoch management.
- Testing Enhancements:
- Clear cache in tests of core helpers to ensure test reliability.
- Added Debug State Transition Method for improved debugging.
- Backfilling test: Enabled backfill in E2E tests for more comprehensive coverage.
- Clear cache in tests of core helpers to ensure test reliability.
- Added Debug State Transition Method for improved debugging.
- Backfilling test: Enabled backfill in E2E tests for more comprehensive coverage.
- API Updates: Re-enabled jwt on keymanager API for enhanced security.
- Logging Improvements: Enhanced block by root log for better traceability.
- Validator Client Improvements:
- Added Spans to Core Validator Methods for enhanced monitoring.
- Improved readability in validator client code for better maintenance (various commits).
- Added Spans to Core Validator Methods for enhanced monitoring.
- Improved readability in validator client code for better maintenance (various commits).

### Changed

- Optimizations and Refinements:
- Lowered resource usage in certain processes for efficiency.
- Moved blob rpc validation closer to peer read for optimized processing.
- Cleaned up validate beacon block code for clarity and efficiency.
- Updated Sepolia Deneb fork epoch for alignment with network changes.
- Changed blob latency metrics to milliseconds for more precise measurement.
- Altered getLegacyDatabaseLocation message for better clarity.
- Improved wait for activation method for enhanced performance.
- Capitalized Aggregated Unaggregated Attestations Log for consistency.
- Modified HistoricalRoots usage for accuracy.
- Adjusted checking of attribute emptiness for efficiency.
- Lowered resource usage in certain processes for efficiency.
- Moved blob rpc validation closer to peer read for optimized processing.
- Cleaned up validate beacon block code for clarity and efficiency.
- Updated Sepolia Deneb fork epoch for alignment with network changes.
- Changed blob latency metrics to milliseconds for more precise measurement.
- Altered getLegacyDatabaseLocation message for better clarity.
- Improved wait for activation method for enhanced performance.
- Capitalized Aggregated Unaggregated Attestations Log for consistency.
- Modified HistoricalRoots usage for accuracy.
- Adjusted checking of attribute emptiness for efficiency.
- Database Management:
- Moved --db-backup-output-dir as a deprecated flag for database management simplification.
- Added the Ability to Defragment the Beacon State for improved database performance.
- Moved --db-backup-output-dir as a deprecated flag for database management simplification.
- Added the Ability to Defragment the Beacon State for improved database performance.
- Dependency Update: Bumped quic-go version from 0.39.3 to 0.39.4 for up-to-date dependencies.

### Removed
@@ -679,12 +679,12 @@ AVX support (eg Celeron) after the Deneb fork. This is not an issue for mainnet.
### Fixed

- Bug Fixes:
- Fixed off by one error for improved accuracy.
- Resolved small typo in error messages for clarity.
- Addressed minor issue in blsToExecChange validator for better validation.
- Corrected blobsidecar json tag for commitment inclusion proof.
- Fixed ssz post-requests content type check.
- Resolved issue with port logging in bootnode.
- Fixed off by one error for improved accuracy.
- Resolved small typo in error messages for clarity.
- Addressed minor issue in blsToExecChange validator for better validation.
- Corrected blobsidecar json tag for commitment inclusion proof.
- Fixed ssz post-requests content type check.
- Resolved issue with port logging in bootnode.
- Test Fixes: Re-enabled Slasher E2E Test for more comprehensive testing.

### Security
@@ -1111,9 +1111,9 @@ No security issues in thsi release.
now features runtime detection, automatically enabling optimized code paths if your CPU supports it.
- **Multiarch Containers Preview Available**: multiarch (:wave: arm64 support :wave:) containers will be offered for
preview at the following locations:
- Beacon Chain: [gcr.io/prylabs-dev/prysm/beacon-chain:v4.1.0](gcr.io/prylabs-dev/prysm/beacon-chain:v4.1.0)
- Validator: [gcr.io/prylabs-dev/prysm/validator:v4.1.0](gcr.io/prylabs-dev/prysm/validator:v4.1.0)
- Please note that in the next cycle, we will exclusively use these containers at the canonical URLs.
- Beacon Chain: [gcr.io/prylabs-dev/prysm/beacon-chain:v4.1.0](gcr.io/prylabs-dev/prysm/beacon-chain:v4.1.0)
- Validator: [gcr.io/prylabs-dev/prysm/validator:v4.1.0](gcr.io/prylabs-dev/prysm/validator:v4.1.0)
- Please note that in the next cycle, we will exclusively use these containers at the canonical URLs.

### Added

50 changes: 50 additions & 0 deletions api/server/structs/conversions_lightclient.go
Original file line number Diff line number Diff line change
@@ -171,9 +171,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.ExecutionPayloadHeaderElectra)
if !ok {
return nil, fmt.Errorf("execution data is not %T", &enginev1.ExecutionPayloadHeaderElectra{})
}
execution, err := ExecutionPayloadHeaderElectraFromConsensus(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
}
1 change: 1 addition & 0 deletions beacon-chain/blockchain/process_block.go
Original file line number Diff line number Diff line change
@@ -68,6 +68,7 @@ func (s *Service) postBlockProcess(cfg *postBlockProcessConfig) error {
defer s.handleSecondFCUCall(cfg, fcuArgs)
}
defer s.sendLightClientFeeds(cfg)
defer s.saveLightClientUpdate(cfg)
defer s.sendStateFeedOnBlock(cfg)
defer reportProcessingTime(startTime)
defer reportAttestationInclusion(cfg.roblock.Block())
104 changes: 102 additions & 2 deletions beacon-chain/blockchain/process_block_helpers.go
Original file line number Diff line number Diff line change
@@ -5,12 +5,11 @@ 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"
@@ -129,6 +128,107 @@ func (s *Service) sendLightClientFeeds(cfg *postBlockProcessConfig) {
}
}

// saveLightClientUpdate saves the light client update for this block
// if it's better than the already saved one, when feature flag is enabled.
func (s *Service) saveLightClientUpdate(cfg *postBlockProcessConfig) {
//if !features.Get().EnableLightClient {
// return
//}

attestedRoot := cfg.roblock.Block().ParentRoot()
attestedBlock, err := s.getBlock(cfg.ctx, attestedRoot)
if err != nil {
log.WithError(err).Error("Saving light client update failed: Could not get attested block")
return
}
if attestedBlock == nil || attestedBlock.IsNil() {
log.Error("Saving light client update failed: Attested block is nil")
return
}
attestedState, err := s.cfg.StateGen.StateByRoot(cfg.ctx, attestedRoot)
if err != nil {
log.WithError(err).Error("Saving light client update failed: Could not get attested state")
return
}
if attestedState == nil || attestedState.IsNil() {
log.Error("Saving light client update failed: Attested state is nil")
return
}

finalizedRoot := attestedState.FinalizedCheckpoint().Root
finalizedBlock, err := s.getBlock(cfg.ctx, [32]byte(finalizedRoot))
if err != nil {
log.WithError(err).Error("Saving light client update failed: Could not get finalized block")
return
}

update, err := lightclient.NewLightClientUpdateFromBeaconState(
cfg.ctx,
s.CurrentSlot(),
cfg.postState,
cfg.roblock,
attestedState,
attestedBlock,
finalizedBlock,
)
if err != nil {
log.WithError(err).Error("Saving light client update failed: Could not create light client update")
return
}

period := slots.SyncCommitteePeriod(slots.ToEpoch(attestedState.Slot()))

oldUpdate, err := s.cfg.BeaconDB.LightClientUpdate(cfg.ctx, period)
if err != nil {
log.WithError(err).Error("Saving light client update failed: Could not get current light client update")
return
}

if oldUpdate == nil {
if err := s.cfg.BeaconDB.SaveLightClientUpdate(cfg.ctx, period, update); err != nil {
log.WithError(err).Error("Saving light client update failed: Could not save light client update")
} else {
log.WithField("period", period).Debug("Saving light client update: Saved new update")
}
return
}

isNewUpdateBetter, err := lightclient.IsBetterUpdate(update, oldUpdate)
if err != nil {
log.WithError(err).Error("Saving light client update failed: Could not compare light client updates")
return
}

if isNewUpdateBetter {
if err := s.cfg.BeaconDB.SaveLightClientUpdate(cfg.ctx, period, update); err != nil {
log.WithError(err).Error("Saving light client update failed: Could not save light client update")
} else {
log.WithField("period", period).Debug("Saving light client update: Saved new update")
}
} else {
log.WithField("period", period).Debug("Saving light client update: New update is not better than the current one. Skipping save.")
}
}

// saveLightClientBootstrap saves a light client bootstrap for this block
// when feature flag is enabled.
func (s *Service) saveLightClientBootstrap(cfg *postBlockProcessConfig) {
//if !features.Get().EnableLightClient {
// return
//}

blockRoot := cfg.roblock.Root()
bootstrap, err := lightclient.CreateLightClientBootstrap(cfg.ctx, s.CurrentSlot(), cfg.postState, cfg.roblock)
if err != nil {
log.WithError(err).Error("Saving light client bootstrap failed: Could not create light client bootstrap")
return
}
err = s.cfg.BeaconDB.SaveLightClientBootstrap(cfg.ctx, blockRoot[:], bootstrap)
if err != nil {
log.WithError(err).Error("Saving light client bootstrap failed: Could not save light client bootstrap in DB")
}
}

func (s *Service) tryPublishLightClientFinalityUpdate(
ctx context.Context,
signed interfaces.ReadOnlySignedBeaconBlock,
252 changes: 252 additions & 0 deletions beacon-chain/blockchain/process_block_test.go
Original file line number Diff line number Diff line change
@@ -40,6 +40,7 @@ import (
"github.com/prysmaticlabs/prysm/v5/testing/require"
"github.com/prysmaticlabs/prysm/v5/testing/util"
prysmTime "github.com/prysmaticlabs/prysm/v5/time"
"github.com/prysmaticlabs/prysm/v5/time/slots"
logTest "github.com/sirupsen/logrus/hooks/test"
)

@@ -2446,3 +2447,254 @@ func fakeResult(missing []uint64) map[uint64]struct{} {
}
return r
}

func TestSaveLightClientUpdate(t *testing.T) {
s, tr := minimalTestService(t)
ctx := tr.ctx

t.Run("Altair", func(t *testing.T) {
l := util.NewTestLightClient(t).SetupTestAltair()

s.genesisTime = time.Unix(time.Now().Unix()-(int64(params.BeaconConfig().AltairForkEpoch)*int64(params.BeaconConfig().SlotsPerEpoch)*int64(params.BeaconConfig().SecondsPerSlot)), 0)

err := s.cfg.BeaconDB.SaveBlock(ctx, l.AttestedBlock)
require.NoError(t, err)
attestedBlockRoot, err := l.AttestedBlock.Block().HashTreeRoot()
require.NoError(t, err)
err = s.cfg.BeaconDB.SaveState(ctx, l.AttestedState, attestedBlockRoot)
require.NoError(t, err)

currentBlockRoot, err := l.Block.Block().HashTreeRoot()
require.NoError(t, err)
roblock, err := consensusblocks.NewROBlockWithRoot(l.Block, currentBlockRoot)
require.NoError(t, err)

err = s.cfg.BeaconDB.SaveBlock(ctx, roblock)
require.NoError(t, err)
err = s.cfg.BeaconDB.SaveState(ctx, l.State, currentBlockRoot)
require.NoError(t, err)

err = s.cfg.BeaconDB.SaveBlock(ctx, l.FinalizedBlock)
require.NoError(t, err)

cfg := &postBlockProcessConfig{
ctx: ctx,
roblock: roblock,
postState: l.State,
isValidPayload: true,
}

s.saveLightClientUpdate(cfg)

// Check that the light client update is saved
period := slots.SyncCommitteePeriod(slots.ToEpoch(l.AttestedState.Slot()))

u, err := s.cfg.BeaconDB.LightClientUpdate(ctx, period)
require.NoError(t, err)
require.NotNil(t, u)
attestedStateRoot, err := l.AttestedState.HashTreeRoot(ctx)
require.NoError(t, err)
require.Equal(t, attestedStateRoot, [32]byte(u.AttestedHeader().Beacon().StateRoot))
require.Equal(t, u.Version(), version.Altair)
})

t.Run("Capella", func(t *testing.T) {
l := util.NewTestLightClient(t).SetupTestCapella(false)

s.genesisTime = time.Unix(time.Now().Unix()-(int64(params.BeaconConfig().CapellaForkEpoch)*int64(params.BeaconConfig().SlotsPerEpoch)*int64(params.BeaconConfig().SecondsPerSlot)), 0)

err := s.cfg.BeaconDB.SaveBlock(ctx, l.AttestedBlock)
require.NoError(t, err)
attestedBlockRoot, err := l.AttestedBlock.Block().HashTreeRoot()
require.NoError(t, err)
err = s.cfg.BeaconDB.SaveState(ctx, l.AttestedState, attestedBlockRoot)
require.NoError(t, err)

currentBlockRoot, err := l.Block.Block().HashTreeRoot()
require.NoError(t, err)
roblock, err := consensusblocks.NewROBlockWithRoot(l.Block, currentBlockRoot)
require.NoError(t, err)

err = s.cfg.BeaconDB.SaveBlock(ctx, roblock)
require.NoError(t, err)
err = s.cfg.BeaconDB.SaveState(ctx, l.State, currentBlockRoot)
require.NoError(t, err)

err = s.cfg.BeaconDB.SaveBlock(ctx, l.FinalizedBlock)
require.NoError(t, err)

cfg := &postBlockProcessConfig{
ctx: ctx,
roblock: roblock,
postState: l.State,
isValidPayload: true,
}

s.saveLightClientUpdate(cfg)

// Check that the light client update is saved
period := slots.SyncCommitteePeriod(slots.ToEpoch(l.AttestedState.Slot()))
u, err := s.cfg.BeaconDB.LightClientUpdate(ctx, period)
require.NoError(t, err)
require.NotNil(t, u)
attestedStateRoot, err := l.AttestedState.HashTreeRoot(ctx)
require.NoError(t, err)
require.Equal(t, attestedStateRoot, [32]byte(u.AttestedHeader().Beacon().StateRoot))
require.Equal(t, u.Version(), version.Capella)
})

t.Run("Deneb", func(t *testing.T) {
l := util.NewTestLightClient(t).SetupTestDeneb(false)

s.genesisTime = time.Unix(time.Now().Unix()-(int64(params.BeaconConfig().DenebForkEpoch)*int64(params.BeaconConfig().SlotsPerEpoch)*int64(params.BeaconConfig().SecondsPerSlot)), 0)

err := s.cfg.BeaconDB.SaveBlock(ctx, l.AttestedBlock)
require.NoError(t, err)
attestedBlockRoot, err := l.AttestedBlock.Block().HashTreeRoot()
require.NoError(t, err)
err = s.cfg.BeaconDB.SaveState(ctx, l.AttestedState, attestedBlockRoot)
require.NoError(t, err)

currentBlockRoot, err := l.Block.Block().HashTreeRoot()
require.NoError(t, err)
roblock, err := consensusblocks.NewROBlockWithRoot(l.Block, currentBlockRoot)
require.NoError(t, err)

err = s.cfg.BeaconDB.SaveBlock(ctx, roblock)
require.NoError(t, err)
err = s.cfg.BeaconDB.SaveState(ctx, l.State, currentBlockRoot)
require.NoError(t, err)

err = s.cfg.BeaconDB.SaveBlock(ctx, l.FinalizedBlock)
require.NoError(t, err)

cfg := &postBlockProcessConfig{
ctx: ctx,
roblock: roblock,
postState: l.State,
isValidPayload: true,
}

s.saveLightClientUpdate(cfg)

// Check that the light client update is saved
period := slots.SyncCommitteePeriod(slots.ToEpoch(l.AttestedState.Slot()))
u, err := s.cfg.BeaconDB.LightClientUpdate(ctx, period)
require.NoError(t, err)
require.NotNil(t, u)
attestedStateRoot, err := l.AttestedState.HashTreeRoot(ctx)
require.NoError(t, err)
require.Equal(t, attestedStateRoot, [32]byte(u.AttestedHeader().Beacon().StateRoot))
require.Equal(t, u.Version(), version.Deneb)
})
}

func TestSaveLightClientBootstrap(t *testing.T) {
s, tr := minimalTestService(t)
ctx := tr.ctx

t.Run("Altair", func(t *testing.T) {
l := util.NewTestLightClient(t).SetupTestAltair()

s.genesisTime = time.Unix(time.Now().Unix()-(int64(params.BeaconConfig().AltairForkEpoch)*int64(params.BeaconConfig().SlotsPerEpoch)*int64(params.BeaconConfig().SecondsPerSlot)), 0)

currentBlockRoot, err := l.Block.Block().HashTreeRoot()
require.NoError(t, err)
roblock, err := consensusblocks.NewROBlockWithRoot(l.Block, currentBlockRoot)
require.NoError(t, err)

err = s.cfg.BeaconDB.SaveBlock(ctx, roblock)
require.NoError(t, err)
err = s.cfg.BeaconDB.SaveState(ctx, l.State, currentBlockRoot)
require.NoError(t, err)

cfg := &postBlockProcessConfig{
ctx: ctx,
roblock: roblock,
postState: l.State,
isValidPayload: true,
}

s.saveLightClientBootstrap(cfg)

// Check that the light client bootstrap is saved
b, err := s.cfg.BeaconDB.LightClientBootstrap(ctx, currentBlockRoot[:])
require.NoError(t, err)
require.NotNil(t, b)

stateRoot, err := l.State.HashTreeRoot(ctx)
require.NoError(t, err)
require.Equal(t, stateRoot, [32]byte(b.Header().Beacon().StateRoot))
require.Equal(t, b.Version(), version.Altair)
})

t.Run("Capella", func(t *testing.T) {
l := util.NewTestLightClient(t).SetupTestCapella(false)

s.genesisTime = time.Unix(time.Now().Unix()-(int64(params.BeaconConfig().CapellaForkEpoch)*int64(params.BeaconConfig().SlotsPerEpoch)*int64(params.BeaconConfig().SecondsPerSlot)), 0)

currentBlockRoot, err := l.Block.Block().HashTreeRoot()
require.NoError(t, err)
roblock, err := consensusblocks.NewROBlockWithRoot(l.Block, currentBlockRoot)
require.NoError(t, err)

err = s.cfg.BeaconDB.SaveBlock(ctx, roblock)
require.NoError(t, err)
err = s.cfg.BeaconDB.SaveState(ctx, l.State, currentBlockRoot)
require.NoError(t, err)

cfg := &postBlockProcessConfig{
ctx: ctx,
roblock: roblock,
postState: l.State,
isValidPayload: true,
}

s.saveLightClientBootstrap(cfg)

// Check that the light client bootstrap is saved
b, err := s.cfg.BeaconDB.LightClientBootstrap(ctx, currentBlockRoot[:])
require.NoError(t, err)
require.NotNil(t, b)

stateRoot, err := l.State.HashTreeRoot(ctx)
require.NoError(t, err)
require.Equal(t, stateRoot, [32]byte(b.Header().Beacon().StateRoot))
require.Equal(t, b.Version(), version.Capella)
})

t.Run("Deneb", func(t *testing.T) {
l := util.NewTestLightClient(t).SetupTestDeneb(false)

s.genesisTime = time.Unix(time.Now().Unix()-(int64(params.BeaconConfig().DenebForkEpoch)*int64(params.BeaconConfig().SlotsPerEpoch)*int64(params.BeaconConfig().SecondsPerSlot)), 0)

currentBlockRoot, err := l.Block.Block().HashTreeRoot()
require.NoError(t, err)
roblock, err := consensusblocks.NewROBlockWithRoot(l.Block, currentBlockRoot)
require.NoError(t, err)

err = s.cfg.BeaconDB.SaveBlock(ctx, roblock)
require.NoError(t, err)
err = s.cfg.BeaconDB.SaveState(ctx, l.State, currentBlockRoot)
require.NoError(t, err)

cfg := &postBlockProcessConfig{
ctx: ctx,
roblock: roblock,
postState: l.State,
isValidPayload: true,
}

s.saveLightClientBootstrap(cfg)

// Check that the light client bootstrap is saved
b, err := s.cfg.BeaconDB.LightClientBootstrap(ctx, currentBlockRoot[:])
require.NoError(t, err)
require.NotNil(t, b)

stateRoot, err := l.State.HashTreeRoot(ctx)
require.NoError(t, err)
require.Equal(t, stateRoot, [32]byte(b.Header().Beacon().StateRoot))
require.Equal(t, b.Version(), version.Deneb)
})
}
3 changes: 3 additions & 0 deletions beacon-chain/core/light-client/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -31,8 +31,11 @@ go_test(
deps = [
":go_default_library",
"//config/fieldparams:go_default_library",
"//config/params:go_default_library",
"//consensus-types:go_default_library",
"//consensus-types/blocks:go_default_library",
"//consensus-types/light-client:go_default_library",
"//consensus-types/primitives:go_default_library",
"//encoding/ssz:go_default_library",
"//proto/engine/v1:go_default_library",
"//proto/prysm/v1alpha1:go_default_library",
259 changes: 258 additions & 1 deletion beacon-chain/core/light-client/lightclient.go
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@ import (
"bytes"
"context"
"fmt"
"reflect"

"github.com/pkg/errors"
"github.com/prysmaticlabs/prysm/v5/beacon-chain/execution"
@@ -382,7 +383,6 @@ func BlockToLightClientHeader(
var m proto.Message
currentEpoch := slots.ToEpoch(currentSlot)
blockEpoch := slots.ToEpoch(block.Block().Slot())

parentRoot := block.Block().ParentRoot()
stateRoot := block.Block().StateRoot()
bodyRoot, err := block.Block().Body().HashTreeRoot()
@@ -562,3 +562,260 @@ 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.SyncCommitteeBranchDepth)
for i := 0; i < len(currentSyncCommitteeBranch); i++ {
currentSyncCommitteeBranch[i] = make([]byte, fieldparams.RootLength)
}
} else {
currentSyncCommitteeBranch = make([][]byte, fieldparams.SyncCommitteeBranchDepthElectra)
for i := 0; i < len(currentSyncCommitteeBranch); i++ {
currentSyncCommitteeBranch[i] = make([]byte, fieldparams.RootLength)
}
}

executionBranch := make([][]byte, fieldparams.ExecutionBranchDepth)
for i := 0; i < fieldparams.ExecutionBranchDepth; i++ {
executionBranch[i] = make([]byte, 32)
}

var m proto.Message
if currentEpoch < params.BeaconConfig().CapellaForkEpoch {
m = &pb.LightClientBootstrapAltair{
Header: &pb.LightClientHeaderAltair{
Beacon: &pb.BeaconBlockHeader{},
},
CurrentSyncCommittee: currentSyncCommittee,
CurrentSyncCommitteeBranch: currentSyncCommitteeBranch,
}
} else if currentEpoch < params.BeaconConfig().DenebForkEpoch {
m = &pb.LightClientBootstrapCapella{
Header: &pb.LightClientHeaderCapella{
Beacon: &pb.BeaconBlockHeader{},
Execution: &enginev1.ExecutionPayloadHeaderCapella{},
ExecutionBranch: executionBranch,
},
CurrentSyncCommittee: currentSyncCommittee,
CurrentSyncCommitteeBranch: currentSyncCommitteeBranch,
}
} else if currentEpoch < params.BeaconConfig().ElectraForkEpoch {
m = &pb.LightClientBootstrapDeneb{
Header: &pb.LightClientHeaderDeneb{
Beacon: &pb.BeaconBlockHeader{},
Execution: &enginev1.ExecutionPayloadHeaderDeneb{},
ExecutionBranch: executionBranch,
},
CurrentSyncCommittee: currentSyncCommittee,
CurrentSyncCommitteeBranch: currentSyncCommitteeBranch,
}
} else {
m = &pb.LightClientBootstrapElectra{
Header: &pb.LightClientHeaderDeneb{
Beacon: &pb.BeaconBlockHeader{},
Execution: &enginev1.ExecutionPayloadHeaderDeneb{},
ExecutionBranch: executionBranch,
},
CurrentSyncCommittee: currentSyncCommittee,
CurrentSyncCommitteeBranch: currentSyncCommitteeBranch,
}
}
return light_client.NewWrappedBootstrap(m)
}
666 changes: 666 additions & 0 deletions beacon-chain/core/light-client/lightclient_test.go

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions beacon-chain/db/iface/interface.go
Original file line number Diff line number Diff line change
@@ -59,6 +59,7 @@ type ReadOnlyDatabase interface {
// light client operations
LightClientUpdates(ctx context.Context, startPeriod, endPeriod uint64) (map[uint64]interfaces.LightClientUpdate, error)
LightClientUpdate(ctx context.Context, period uint64) (interfaces.LightClientUpdate, error)
LightClientBootstrap(ctx context.Context, blockRoot []byte) (interfaces.LightClientBootstrap, error)

// origin checkpoint sync support
OriginCheckpointBlockRoot(ctx context.Context) ([32]byte, error)
@@ -97,6 +98,7 @@ type NoHeadAccessDatabase interface {
SaveRegistrationsByValidatorIDs(ctx context.Context, ids []primitives.ValidatorIndex, regs []*ethpb.ValidatorRegistrationV1) error
// light client operations
SaveLightClientUpdate(ctx context.Context, period uint64, update interfaces.LightClientUpdate) error
SaveLightClientBootstrap(ctx context.Context, blockRoot []byte, bootstrap interfaces.LightClientBootstrap) error

CleanUpDirtyStates(ctx context.Context, slotsPerArchivedPoint primitives.Slot) error
}
1 change: 1 addition & 0 deletions beacon-chain/db/kv/kv.go
Original file line number Diff line number Diff line change
@@ -108,6 +108,7 @@ var Buckets = [][]byte{
stateSummaryBucket,
stateValidatorsBucket,
lightClientUpdatesBucket,
lightclientBootstrapBucket,
// Indices buckets.
blockSlotIndicesBucket,
stateSlotIndicesBucket,
91 changes: 88 additions & 3 deletions beacon-chain/db/kv/lightclient.go
Original file line number Diff line number Diff line change
@@ -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 []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 := &ethpb.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 := &ethpb.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 := &ethpb.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 := &ethpb.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:
3 changes: 2 additions & 1 deletion beacon-chain/db/kv/schema.go
Original file line number Diff line number Diff line change
@@ -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")
3 changes: 0 additions & 3 deletions beacon-chain/rpc/eth/light-client/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -18,14 +18,12 @@ go_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",
@@ -56,7 +54,6 @@ go_test(
"//proto/engine/v1:go_default_library",
"//proto/prysm/v1alpha1:go_default_library",
"//runtime/version:go_default_library",
"//testing/assert:go_default_library",
"//testing/require:go_default_library",
"//testing/util:go_default_library",
"@com_github_ethereum_go_ethereum//common/hexutil:go_default_library",
4 changes: 4 additions & 0 deletions beacon-chain/rpc/eth/light-client/handlers_test.go
Original file line number Diff line number Diff line change
@@ -201,6 +201,10 @@ func TestLightClientHandler_GetLightClientBootstrap_Deneb(t *testing.T) {
}

func TestLightClientHandler_GetLightClientBootstrap_Electra(t *testing.T) {
config := params.BeaconConfig()
config.ElectraForkEpoch = config.DenebForkEpoch + 1
params.OverrideBeaconConfig(config)

l := util.NewTestLightClient(t).SetupTestElectra(false) // result is same for true and false

slot := l.State.Slot()
191 changes: 4 additions & 187 deletions beacon-chain/rpc/eth/light-client/helpers.go
Original file line number Diff line number Diff line change
@@ -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,12 @@ 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()
if err != nil {
return nil, errors.Wrap(err, "could not get latest block header root")
}
beaconBlockRoot, err := block.Block().HashTreeRoot()
bootstrap, err := lightclient.CreateLightClientBootstrap(ctx, currentSlot, state, block)
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 +57,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
}
677 changes: 0 additions & 677 deletions beacon-chain/rpc/eth/light-client/helpers_test.go

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions consensus-types/interfaces/light_client.go
Original file line number Diff line number Diff line change
@@ -27,9 +27,12 @@ type LightClientBootstrap interface {
ssz.Marshaler
Version() int
Header() LightClientHeader
SetHeader(header LightClientHeader) error
CurrentSyncCommittee() *pb.SyncCommittee
SetCurrentSyncCommittee(sc *pb.SyncCommittee) error
CurrentSyncCommitteeBranch() (LightClientSyncCommitteeBranch, error)
CurrentSyncCommitteeBranchElectra() (LightClientSyncCommitteeBranchElectra, error)
SetCurrentSyncCommitteeBranch(branch [][]byte) error
}

type LightClientUpdate interface {
112 changes: 112 additions & 0 deletions consensus-types/light-client/bootstrap.go
Original file line number Diff line number Diff line change
@@ -81,14 +81,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)
}
@@ -145,14 +173,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)
}
@@ -209,14 +265,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)
}
@@ -273,14 +357,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
}
22 changes: 15 additions & 7 deletions testing/util/lightclient.go
Original file line number Diff line number Diff line change
@@ -261,25 +261,33 @@ func (l *TestLightClient) SetupTestCapellaFinalizedBlockAltair(blinded bool) *Te
func (l *TestLightClient) SetupTestAltair() *TestLightClient {
ctx := context.Background()

slot := primitives.Slot(params.BeaconConfig().AltairForkEpoch * primitives.Epoch(params.BeaconConfig().SlotsPerEpoch)).Add(1)
slot := primitives.Slot(uint64(params.BeaconConfig().AltairForkEpoch) * uint64(params.BeaconConfig().SlotsPerEpoch)).Add(1)

attestedState, err := NewBeaconStateAltair()
require.NoError(l.T, err)
err = attestedState.SetSlot(slot)
require.NoError(l.T, err)

finalizedBlock, err := blocks.NewSignedBeaconBlock(NewBeaconBlockAltair())
finalizedState, err := NewBeaconStateAltair()
require.NoError(l.T, err)
finalizedBlock.SetSlot(1)
finalizedHeader, err := finalizedBlock.Header()
err = finalizedState.SetSlot(1)
require.NoError(l.T, err)
finalizedStateRoot, err := finalizedState.HashTreeRoot(ctx)
require.NoError(l.T, err)
SignedFinalizedBlock, err := blocks.NewSignedBeaconBlock(NewBeaconBlockAltair())
require.NoError(l.T, err)
SignedFinalizedBlock.SetSlot(1)
SignedFinalizedBlock.SetStateRoot(finalizedStateRoot[:])
finalizedHeader, err := SignedFinalizedBlock.Header()
require.NoError(l.T, err)
finalizedRoot, err := finalizedHeader.Header.HashTreeRoot()
require.NoError(l.T, err)

require.NoError(l.T, attestedState.SetFinalizedCheckpoint(&ethpb.Checkpoint{
finalizedCheckpoint := &ethpb.Checkpoint{
Epoch: params.BeaconConfig().AltairForkEpoch - 10,
Root: finalizedRoot[:],
}))
}
require.NoError(l.T, attestedState.SetFinalizedCheckpoint(finalizedCheckpoint))

parent := NewBeaconBlockAltair()
parent.Block.Slot = slot
@@ -337,7 +345,7 @@ func (l *TestLightClient) SetupTestAltair() *TestLightClient {
l.AttestedState = attestedState
l.Block = signedBlock
l.Ctx = ctx
l.FinalizedBlock = finalizedBlock
l.FinalizedBlock = SignedFinalizedBlock
l.AttestedBlock = signedParent

return l

0 comments on commit 6436a08

Please sign in to comment.