Skip to content

Commit

Permalink
Save LC Bootstrap DB Space Optimization (#14775)
Browse files Browse the repository at this point in the history
* add db optimization altair

* add db optimization

* add tests

* fix tests

* fix changelog

* remove debug code

* remove unused code

* fix comment

* remove unused code
  • Loading branch information
Inspector-Butters authored Jan 9, 2025
1 parent 393e63e commit e99df5e
Show file tree
Hide file tree
Showing 8 changed files with 257 additions and 24 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ The format is based on Keep a Changelog, and this project adheres to Semantic Ve
- Add field param placeholder for Electra blob target and max to pass spec tests.
- Add EIP-7691: Blob throughput increase.
- SSZ files generation: Remove the `// Hash: ...` header.
- DB optimization for saving light client bootstraps (save unique sync committees only).
- Trace IDONTWANT Messages in Pubsub.
- Add Fulu fork boilerplate.

Expand Down
1 change: 1 addition & 0 deletions beacon-chain/db/kv/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ go_library(
"//beacon-chain/state/genesis:go_default_library",
"//beacon-chain/state/state-native:go_default_library",
"//config/features:go_default_library",
"//config/fieldparams:go_default_library",
"//config/params:go_default_library",
"//consensus-types/blocks:go_default_library",
"//consensus-types/interfaces:go_default_library",
Expand Down
1 change: 1 addition & 0 deletions beacon-chain/db/kv/kv.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ var Buckets = [][]byte{
stateValidatorsBucket,
lightClientUpdatesBucket,
lightClientBootstrapBucket,
lightClientSyncCommitteeBucket,
// Indices buckets.
blockSlotIndicesBucket,
stateSlotIndicesBucket,
Expand Down
101 changes: 83 additions & 18 deletions beacon-chain/db/kv/lightclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (

"github.com/golang/snappy"
"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/interfaces"
light_client "github.com/prysmaticlabs/prysm/v5/consensus-types/light-client"
"github.com/prysmaticlabs/prysm/v5/encoding/bytesutil"
Expand Down Expand Up @@ -35,9 +37,35 @@ func (s *Store) SaveLightClientBootstrap(ctx context.Context, blockRoot []byte,
_, span := trace.StartSpan(ctx, "BeaconDB.SaveLightClientBootstrap")
defer span.End()

bootstrapCopy, err := light_client.NewWrappedBootstrap(proto.Clone(bootstrap.Proto()))
if err != nil {
return errors.Wrap(err, "could not clone light client bootstrap")
}
syncCommitteeHash, err := bootstrapCopy.CurrentSyncCommittee().HashTreeRoot()
if err != nil {
return errors.Wrap(err, "could not hash current sync committee")
}

return s.db.Update(func(tx *bolt.Tx) error {
syncCommitteeBucket := tx.Bucket(lightClientSyncCommitteeBucket)
syncCommitteeAlreadyExists := syncCommitteeBucket.Get(syncCommitteeHash[:]) != nil
if !syncCommitteeAlreadyExists {
enc, err := bootstrapCopy.CurrentSyncCommittee().MarshalSSZ()
if err != nil {
return errors.Wrap(err, "could not marshal current sync committee")
}
if err := syncCommitteeBucket.Put(syncCommitteeHash[:], enc); err != nil {
return errors.Wrap(err, "could not save current sync committee")
}
}

err = bootstrapCopy.SetCurrentSyncCommittee(createEmptySyncCommittee())
if err != nil {
return errors.Wrap(err, "could not set current sync committee to zero while saving")
}

bkt := tx.Bucket(lightClientBootstrapBucket)
enc, err := encodeLightClientBootstrap(bootstrap)
enc, err := encodeLightClientBootstrap(bootstrapCopy, syncCommitteeHash)
if err != nil {
return err
}
Expand All @@ -50,20 +78,49 @@ func (s *Store) LightClientBootstrap(ctx context.Context, blockRoot []byte) (int
defer span.End()

var bootstrap interfaces.LightClientBootstrap
var syncCommitteeHash []byte
err := s.db.View(func(tx *bolt.Tx) error {
bkt := tx.Bucket(lightClientBootstrapBucket)
syncCommitteeBucket := tx.Bucket(lightClientSyncCommitteeBucket)
enc := bkt.Get(blockRoot)
if enc == nil {
return nil
}
var err error
bootstrap, err = decodeLightClientBootstrap(enc)
bootstrap, syncCommitteeHash, err = decodeLightClientBootstrap(enc)
if err != nil {
return errors.Wrap(err, "could not decode light client bootstrap")
}
var syncCommitteeBytes = syncCommitteeBucket.Get(syncCommitteeHash)
if syncCommitteeBytes == nil {
return errors.New("sync committee not found")
}
syncCommittee := &ethpb.SyncCommittee{}
if err := syncCommittee.UnmarshalSSZ(syncCommitteeBytes); err != nil {
return errors.Wrap(err, "could not unmarshal sync committee")
}
err = bootstrap.SetCurrentSyncCommittee(syncCommittee)
if err != nil {
return errors.Wrap(err, "could not set current sync committee while retrieving")
}
return err
})
return bootstrap, err
}

func encodeLightClientBootstrap(bootstrap interfaces.LightClientBootstrap) ([]byte, error) {
func createEmptySyncCommittee() *ethpb.SyncCommittee {
syncCom := make([][]byte, params.BeaconConfig().SyncCommitteeSize)
for i := 0; uint64(i) < params.BeaconConfig().SyncCommitteeSize; i++ {
syncCom[i] = make([]byte, fieldparams.BLSPubkeyLength)
}

return &ethpb.SyncCommittee{
Pubkeys: syncCom,
AggregatePubkey: make([]byte, fieldparams.BLSPubkeyLength),
}
}

func encodeLightClientBootstrap(bootstrap interfaces.LightClientBootstrap, syncCommitteeHash [32]byte) ([]byte, error) {
key, err := keyForLightClientUpdate(bootstrap.Version())
if err != nil {
return nil, err
Expand All @@ -72,48 +129,56 @@ func encodeLightClientBootstrap(bootstrap interfaces.LightClientBootstrap) ([]by
if err != nil {
return nil, errors.Wrap(err, "could not marshal light client bootstrap")
}
fullEnc := make([]byte, len(key)+len(enc))
fullEnc := make([]byte, len(key)+32+len(enc))
copy(fullEnc, key)
copy(fullEnc[len(key):], enc)
return snappy.Encode(nil, fullEnc), nil
copy(fullEnc[len(key):len(key)+32], syncCommitteeHash[:])
copy(fullEnc[len(key)+32:], enc)
compressedEnc := snappy.Encode(nil, fullEnc)
return compressedEnc, nil
}

func decodeLightClientBootstrap(enc []byte) (interfaces.LightClientBootstrap, error) {
func decodeLightClientBootstrap(enc []byte) (interfaces.LightClientBootstrap, []byte, 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")
return nil, nil, errors.Wrap(err, "could not snappy decode light client bootstrap")
}
var m proto.Message
var syncCommitteeHash []byte
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")
if err := bootstrap.UnmarshalSSZ(enc[len(altairKey)+32:]); err != nil {
return nil, nil, errors.Wrap(err, "could not unmarshal Altair light client bootstrap")
}
m = bootstrap
syncCommitteeHash = enc[len(altairKey) : len(altairKey)+32]
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")
if err := bootstrap.UnmarshalSSZ(enc[len(capellaKey)+32:]); err != nil {
return nil, nil, errors.Wrap(err, "could not unmarshal Capella light client bootstrap")
}
m = bootstrap
syncCommitteeHash = enc[len(capellaKey) : len(capellaKey)+32]
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")
if err := bootstrap.UnmarshalSSZ(enc[len(denebKey)+32:]); err != nil {
return nil, nil, errors.Wrap(err, "could not unmarshal Deneb light client bootstrap")
}
m = bootstrap
syncCommitteeHash = enc[len(denebKey) : len(denebKey)+32]
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")
if err := bootstrap.UnmarshalSSZ(enc[len(electraKey)+32:]); err != nil {
return nil, nil, errors.Wrap(err, "could not unmarshal Electra light client bootstrap")
}
m = bootstrap
syncCommitteeHash = enc[len(electraKey) : len(electraKey)+32]
default:
return nil, errors.New("decoding of saved light client bootstrap is unsupported")
return nil, nil, errors.New("decoding of saved light client bootstrap is unsupported")
}
return light_client.NewWrappedBootstrap(m)
bootstrap, err := light_client.NewWrappedBootstrap(m)
return bootstrap, syncCommitteeHash, err
}

func (s *Store) LightClientUpdates(ctx context.Context, startPeriod, endPeriod uint64) (map[uint64]interfaces.LightClientUpdate, error) {
Expand Down
155 changes: 151 additions & 4 deletions beacon-chain/db/kv/lightclient_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/prysmaticlabs/prysm/v5/testing/require"
"github.com/prysmaticlabs/prysm/v5/testing/util"
"github.com/prysmaticlabs/prysm/v5/time/slots"
bolt "go.etcd.io/bbolt"
"google.golang.org/protobuf/proto"
)

Expand Down Expand Up @@ -611,7 +612,13 @@ func TestStore_LightClientBootstrap_CanSaveRetrieve(t *testing.T) {

retrievedBootstrap, err := db.LightClientBootstrap(ctx, []byte("blockRootAltair"))
require.NoError(t, err)
require.DeepEqual(t, bootstrap, retrievedBootstrap, "retrieved bootstrap does not match saved bootstrap")
require.DeepEqual(t, bootstrap.Header(), retrievedBootstrap.Header(), "retrieved bootstrap header does not match saved bootstrap header")
require.DeepEqual(t, bootstrap.CurrentSyncCommittee(), retrievedBootstrap.CurrentSyncCommittee(), "retrieved bootstrap sync committee does not match saved bootstrap sync committee")
savedBranch, err := bootstrap.CurrentSyncCommitteeBranch()
require.NoError(t, err)
retrievedBranch, err := retrievedBootstrap.CurrentSyncCommitteeBranch()
require.NoError(t, err)
require.DeepEqual(t, savedBranch, retrievedBranch, "retrieved bootstrap sync committee branch does not match saved bootstrap sync committee branch")
})

t.Run("Capella", func(t *testing.T) {
Expand All @@ -626,7 +633,13 @@ func TestStore_LightClientBootstrap_CanSaveRetrieve(t *testing.T) {

retrievedBootstrap, err := db.LightClientBootstrap(ctx, []byte("blockRootCapella"))
require.NoError(t, err)
require.DeepEqual(t, bootstrap, retrievedBootstrap, "retrieved bootstrap does not match saved bootstrap")
require.DeepEqual(t, bootstrap.Header(), retrievedBootstrap.Header(), "retrieved bootstrap header does not match saved bootstrap header")
require.DeepEqual(t, bootstrap.CurrentSyncCommittee(), retrievedBootstrap.CurrentSyncCommittee(), "retrieved bootstrap sync committee does not match saved bootstrap sync committee")
savedBranch, err := bootstrap.CurrentSyncCommitteeBranch()
require.NoError(t, err)
retrievedBranch, err := retrievedBootstrap.CurrentSyncCommitteeBranch()
require.NoError(t, err)
require.DeepEqual(t, savedBranch, retrievedBranch, "retrieved bootstrap sync committee branch does not match saved bootstrap sync committee branch")
})

t.Run("Deneb", func(t *testing.T) {
Expand All @@ -641,7 +654,13 @@ func TestStore_LightClientBootstrap_CanSaveRetrieve(t *testing.T) {

retrievedBootstrap, err := db.LightClientBootstrap(ctx, []byte("blockRootDeneb"))
require.NoError(t, err)
require.DeepEqual(t, bootstrap, retrievedBootstrap, "retrieved bootstrap does not match saved bootstrap")
require.DeepEqual(t, bootstrap.Header(), retrievedBootstrap.Header(), "retrieved bootstrap header does not match saved bootstrap header")
require.DeepEqual(t, bootstrap.CurrentSyncCommittee(), retrievedBootstrap.CurrentSyncCommittee(), "retrieved bootstrap sync committee does not match saved bootstrap sync committee")
savedBranch, err := bootstrap.CurrentSyncCommitteeBranch()
require.NoError(t, err)
retrievedBranch, err := retrievedBootstrap.CurrentSyncCommitteeBranch()
require.NoError(t, err)
require.DeepEqual(t, savedBranch, retrievedBranch, "retrieved bootstrap sync committee branch does not match saved bootstrap sync committee branch")
})

t.Run("Electra", func(t *testing.T) {
Expand All @@ -656,10 +675,138 @@ func TestStore_LightClientBootstrap_CanSaveRetrieve(t *testing.T) {

retrievedBootstrap, err := db.LightClientBootstrap(ctx, []byte("blockRootElectra"))
require.NoError(t, err)
require.DeepEqual(t, bootstrap, retrievedBootstrap, "retrieved bootstrap does not match saved bootstrap")
require.DeepEqual(t, bootstrap.Header(), retrievedBootstrap.Header(), "retrieved bootstrap header does not match saved bootstrap header")
require.DeepEqual(t, bootstrap.CurrentSyncCommittee(), retrievedBootstrap.CurrentSyncCommittee(), "retrieved bootstrap sync committee does not match saved bootstrap sync committee")
savedBranch, err := bootstrap.CurrentSyncCommitteeBranchElectra()
require.NoError(t, err)
retrievedBranch, err := retrievedBootstrap.CurrentSyncCommitteeBranchElectra()
require.NoError(t, err)
require.DeepEqual(t, savedBranch, retrievedBranch, "retrieved bootstrap sync committee branch does not match saved bootstrap sync committee branch")
})
}

func TestStore_LightClientBootstrap_MultipleBootstrapsWithSameSyncCommittee(t *testing.T) {
params.SetupTestConfigCleanup(t)
cfg := params.BeaconConfig()
cfg.AltairForkEpoch = 0
cfg.CapellaForkEpoch = 1
cfg.DenebForkEpoch = 2
cfg.ElectraForkEpoch = 3
cfg.EpochsPerSyncCommitteePeriod = 1
params.OverrideBeaconConfig(cfg)

db := setupDB(t)
ctx := context.Background()

bootstrap1, err := createDefaultLightClientBootstrap(primitives.Slot(uint64(params.BeaconConfig().AltairForkEpoch) * uint64(params.BeaconConfig().SlotsPerEpoch)))
require.NoError(t, err)
bootstrap2, err := createDefaultLightClientBootstrap(primitives.Slot(uint64(params.BeaconConfig().AltairForkEpoch) * uint64(params.BeaconConfig().SlotsPerEpoch)))
require.NoError(t, err)

randomSyncCommittee := createRandomSyncCommittee()

err = bootstrap1.SetCurrentSyncCommittee(randomSyncCommittee)
require.NoError(t, err)
err = bootstrap2.SetCurrentSyncCommittee(randomSyncCommittee)
require.NoError(t, err)

err = db.SaveLightClientBootstrap(ctx, []byte("blockRootAltair1"), bootstrap1)
require.NoError(t, err)
err = db.SaveLightClientBootstrap(ctx, []byte("blockRootAltair2"), bootstrap2)
require.NoError(t, err)

retrievedBootstrap1, err := db.LightClientBootstrap(ctx, []byte("blockRootAltair1"))
require.NoError(t, err)
retrievedBootstrap2, err := db.LightClientBootstrap(ctx, []byte("blockRootAltair2"))
require.NoError(t, err)

require.DeepEqual(t, bootstrap1.Header(), retrievedBootstrap1.Header(), "retrieved bootstrap1 header does not match saved bootstrap1 header")
require.DeepEqual(t, randomSyncCommittee, retrievedBootstrap1.CurrentSyncCommittee(), "retrieved bootstrap1 sync committee does not match saved bootstrap1 sync committee")
savedBranch, err := bootstrap1.CurrentSyncCommitteeBranch()
require.NoError(t, err)
retrievedBranch, err := retrievedBootstrap1.CurrentSyncCommitteeBranch()
require.NoError(t, err)
require.DeepEqual(t, savedBranch, retrievedBranch, "retrieved bootstrap1 sync committee branch does not match saved bootstrap1 sync committee branch")

require.DeepEqual(t, bootstrap2.Header(), retrievedBootstrap2.Header(), "retrieved bootstrap1 header does not match saved bootstrap1 header")
require.DeepEqual(t, randomSyncCommittee, retrievedBootstrap2.CurrentSyncCommittee(), "retrieved bootstrap1 sync committee does not match saved bootstrap1 sync committee")
savedBranch2, err := bootstrap2.CurrentSyncCommitteeBranch()
require.NoError(t, err)
retrievedBranch2, err := retrievedBootstrap2.CurrentSyncCommitteeBranch()
require.NoError(t, err)
require.DeepEqual(t, savedBranch2, retrievedBranch2, "retrieved bootstrap1 sync committee branch does not match saved bootstrap1 sync committee branch")

// Ensure that the sync committee is only stored once
err = db.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket(lightClientSyncCommitteeBucket)
require.NotNil(t, bucket)
count := bucket.Stats().KeyN
require.Equal(t, 1, count)
return nil
})
require.NoError(t, err)
}

func TestStore_LightClientBootstrap_MultipleBootstrapsWithDifferentSyncCommittees(t *testing.T) {
params.SetupTestConfigCleanup(t)
cfg := params.BeaconConfig()
cfg.AltairForkEpoch = 0
cfg.CapellaForkEpoch = 1
cfg.DenebForkEpoch = 2
cfg.ElectraForkEpoch = 3
cfg.EpochsPerSyncCommitteePeriod = 1
params.OverrideBeaconConfig(cfg)

db := setupDB(t)
ctx := context.Background()

bootstrap1, err := createDefaultLightClientBootstrap(primitives.Slot(uint64(params.BeaconConfig().AltairForkEpoch) * uint64(params.BeaconConfig().SlotsPerEpoch)))
require.NoError(t, err)
bootstrap2, err := createDefaultLightClientBootstrap(primitives.Slot(uint64(params.BeaconConfig().AltairForkEpoch) * uint64(params.BeaconConfig().SlotsPerEpoch)))
require.NoError(t, err)

err = bootstrap1.SetCurrentSyncCommittee(createRandomSyncCommittee())
require.NoError(t, err)
err = bootstrap2.SetCurrentSyncCommittee(createRandomSyncCommittee())
require.NoError(t, err)

err = db.SaveLightClientBootstrap(ctx, []byte("blockRootAltair1"), bootstrap1)
require.NoError(t, err)
err = db.SaveLightClientBootstrap(ctx, []byte("blockRootAltair2"), bootstrap2)
require.NoError(t, err)

retrievedBootstrap1, err := db.LightClientBootstrap(ctx, []byte("blockRootAltair1"))
require.NoError(t, err)
retrievedBootstrap2, err := db.LightClientBootstrap(ctx, []byte("blockRootAltair2"))
require.NoError(t, err)

require.DeepEqual(t, bootstrap1.Header(), retrievedBootstrap1.Header(), "retrieved bootstrap1 header does not match saved bootstrap1 header")
require.DeepEqual(t, bootstrap1.CurrentSyncCommittee(), retrievedBootstrap1.CurrentSyncCommittee(), "retrieved bootstrap1 sync committee does not match saved bootstrap1 sync committee")
savedBranch, err := bootstrap1.CurrentSyncCommitteeBranch()
require.NoError(t, err)
retrievedBranch, err := retrievedBootstrap1.CurrentSyncCommitteeBranch()
require.NoError(t, err)
require.DeepEqual(t, savedBranch, retrievedBranch, "retrieved bootstrap1 sync committee branch does not match saved bootstrap1 sync committee branch")

require.DeepEqual(t, bootstrap2.Header(), retrievedBootstrap2.Header(), "retrieved bootstrap1 header does not match saved bootstrap1 header")
require.DeepEqual(t, bootstrap2.CurrentSyncCommittee(), retrievedBootstrap2.CurrentSyncCommittee(), "retrieved bootstrap1 sync committee does not match saved bootstrap1 sync committee")
savedBranch2, err := bootstrap2.CurrentSyncCommitteeBranch()
require.NoError(t, err)
retrievedBranch2, err := retrievedBootstrap2.CurrentSyncCommitteeBranch()
require.NoError(t, err)
require.DeepEqual(t, savedBranch2, retrievedBranch2, "retrieved bootstrap1 sync committee branch does not match saved bootstrap1 sync committee branch")

// Ensure that the sync committee is stored twice
err = db.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket(lightClientSyncCommitteeBucket)
require.NotNil(t, bucket)
count := bucket.Stats().KeyN
require.Equal(t, 2, count)
return nil
})
require.NoError(t, err)
}

func createDefaultLightClientBootstrap(currentSlot primitives.Slot) (interfaces.LightClientBootstrap, error) {
currentEpoch := slots.ToEpoch(currentSlot)
syncCommitteeSize := params.BeaconConfig().SyncCommitteeSize
Expand Down
5 changes: 3 additions & 2 deletions beacon-chain/db/kv/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ var (
registrationBucket = []byte("registration")

// Light Client Updates Bucket
lightClientUpdatesBucket = []byte("light-client-updates")
lightClientBootstrapBucket = []byte("light-client-bootstrap")
lightClientUpdatesBucket = []byte("light-client-updates")
lightClientBootstrapBucket = []byte("light-client-bootstrap")
lightClientSyncCommitteeBucket = []byte("light-client-sync-committee")

// Deprecated: This bucket was migrated in PR 6461. Do not use, except for migrations.
slotsHasObjectBucket = []byte("slots-has-objects")
Expand Down
Loading

0 comments on commit e99df5e

Please sign in to comment.