Checkpoint Sync 4/5 - enable checkpoint sync to be used by beacon node (#10386)

* enable checkpoint sync in beacon node

* lint fix

* rm unused error

* addressing PR feedback from Radek

* consistent slice -> fixed conversion

Co-authored-by: kasey <kasey@users.noreply.github.com>
This commit is contained in:
kasey 2022-03-28 16:01:55 -05:00 committed by GitHub
parent 1af3c07ec5
commit 7920528ede
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 958 additions and 108 deletions

View File

@ -257,7 +257,7 @@ func (s *Service) StartFromSavedState(saved state.BeaconState) error {
func (s *Service) originRootFromSavedState(ctx context.Context) ([32]byte, error) {
// first check if we have started from checkpoint sync and have a root
originRoot, err := s.cfg.BeaconDB.OriginBlockRoot(ctx)
originRoot, err := s.cfg.BeaconDB.OriginCheckpointBlockRoot(ctx)
if err == nil {
return originRoot, nil
}
@ -265,7 +265,7 @@ func (s *Service) originRootFromSavedState(ctx context.Context) ([32]byte, error
return originRoot, errors.Wrap(err, "could not retrieve checkpoint sync chain origin data from db")
}
// we got here because OriginBlockRoot gave us an ErrNotFound. this means the node was started from a genesis state,
// we got here because OriginCheckpointBlockRoot gave us an ErrNotFound. this means the node was started from a genesis state,
// so we should have a value for GenesisBlock
genesisBlock, err := s.cfg.BeaconDB.GenesisBlock(ctx)
if err != nil {

View File

@ -36,6 +36,7 @@ func NewWeakSubjectivityVerifier(wsc *ethpb.Checkpoint, db weakSubjectivityDB) (
// per 7342, a nil checkpoint, zero-root or zero-epoch should all fail validation
// and return an error instead of creating a WeakSubjectivityVerifier that permits any chain history.
if wsc == nil || len(wsc.Root) == 0 || wsc.Epoch == 0 {
log.Warn("No valid weak subjectivity checkpoint specified, running without weak subjectivity verification")
return &WeakSubjectivityVerifier{
enabled: false,
}, nil
@ -79,15 +80,17 @@ func (v *WeakSubjectivityVerifier) VerifyWeakSubjectivity(ctx context.Context, f
if !v.db.HasBlock(ctx, v.root) {
return errors.Wrap(errWSBlockNotFound, fmt.Sprintf("missing root %#x", v.root))
}
filter := filters.NewFilter().SetStartSlot(v.slot).SetEndSlot(v.slot + params.BeaconConfig().SlotsPerEpoch)
endSlot := v.slot + params.BeaconConfig().SlotsPerEpoch
filter := filters.NewFilter().SetStartSlot(v.slot).SetEndSlot(endSlot)
// A node should have the weak subjectivity block corresponds to the correct epoch in the DB.
log.Infof("Searching block roots for weak subjectivity root=%#x, between slots %d-%d", v.root, v.slot, endSlot)
roots, err := v.db.BlockRoots(ctx, filter)
if err != nil {
return errors.Wrap(err, "error while retrieving block roots to verify weak subjectivity")
}
for _, root := range roots {
if v.root == root {
log.Info("Weak subjectivity check has passed")
log.Info("Weak subjectivity check has passed!!")
v.verified = true
return nil
}

View File

@ -14,60 +14,65 @@ import (
"github.com/prysmaticlabs/prysm/proto/prysm/v1alpha1/wrapper"
"github.com/prysmaticlabs/prysm/testing/require"
"github.com/prysmaticlabs/prysm/testing/util"
"github.com/prysmaticlabs/prysm/time/slots"
)
func TestService_VerifyWeakSubjectivityRoot(t *testing.T) {
beaconDB := testDB.SetupDB(t)
b := util.NewBeaconBlock()
b.Block.Slot = 32
b.Block.Slot = 1792480
wsb, err := wrapper.WrappedSignedBeaconBlock(b)
require.NoError(t, err)
require.NoError(t, beaconDB.SaveBlock(context.Background(), wsb))
r, err := b.Block.HashTreeRoot()
require.NoError(t, err)
blockEpoch := slots.ToEpoch(b.Block.Slot)
tests := []struct {
wsVerified bool
disabled bool
wantErr error
checkpt *ethpb.Checkpoint
finalizedEpoch types.Epoch
name string
}{
{
name: "nil root and epoch",
},
{
name: "already verified",
checkpt: &ethpb.Checkpoint{Epoch: 2},
finalizedEpoch: 2,
wsVerified: true,
name: "nil root and epoch",
disabled: true,
},
{
name: "not yet to verify, ws epoch higher than finalized epoch",
checkpt: &ethpb.Checkpoint{Epoch: 2},
finalizedEpoch: 1,
checkpt: &ethpb.Checkpoint{Root: bytesutil.PadTo([]byte{'a'}, 32), Epoch: blockEpoch},
finalizedEpoch: blockEpoch - 1,
},
{
name: "can't find the block in DB",
checkpt: &ethpb.Checkpoint{Root: bytesutil.PadTo([]byte{'a'}, fieldparams.RootLength), Epoch: 1},
finalizedEpoch: 3,
finalizedEpoch: blockEpoch + 1,
wantErr: errWSBlockNotFound,
},
{
name: "can't find the block corresponds to ws epoch in DB",
checkpt: &ethpb.Checkpoint{Root: r[:], Epoch: 2}, // Root belongs in epoch 1.
finalizedEpoch: 3,
checkpt: &ethpb.Checkpoint{Root: r[:], Epoch: blockEpoch - 2}, // Root belongs in epoch 1.
finalizedEpoch: blockEpoch - 1,
wantErr: errWSBlockNotFoundInEpoch,
},
{
name: "can verify and pass",
checkpt: &ethpb.Checkpoint{Root: r[:], Epoch: 1},
finalizedEpoch: 3,
checkpt: &ethpb.Checkpoint{Root: r[:], Epoch: blockEpoch},
finalizedEpoch: blockEpoch + 1,
},
{
name: "equal epoch",
checkpt: &ethpb.Checkpoint{Root: r[:], Epoch: blockEpoch},
finalizedEpoch: blockEpoch,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
wv, err := NewWeakSubjectivityVerifier(tt.checkpt, beaconDB)
require.Equal(t, !tt.disabled, wv.enabled)
require.NoError(t, err)
s := &Service{
cfg: &config{BeaconDB: beaconDB, WeakSubjectivityCheckpt: tt.checkpt},

View File

@ -13,3 +13,9 @@ var ErrNotFoundState = kv.ErrNotFoundState
// ErrNotFoundOriginBlockRoot wraps ErrNotFound for an error specific to the origin block root.
var ErrNotFoundOriginBlockRoot = kv.ErrNotFoundOriginBlockRoot
// ErrNotFoundOriginBlockRoot wraps ErrNotFound for an error specific to the origin block root.
var ErrNotFoundBackfillBlockRoot = kv.ErrNotFoundBackfillBlockRoot
// ErrNotFoundGenesisBlockRoot means no genesis block root was found, indicating the db was not initialized with genesis
var ErrNotFoundGenesisBlockRoot = kv.ErrNotFoundGenesisBlockRoot

View File

@ -27,6 +27,7 @@ type ReadOnlyDatabase interface {
BlockRootsBySlot(ctx context.Context, slot types.Slot) (bool, [][32]byte, error)
HasBlock(ctx context.Context, blockRoot [32]byte) bool
GenesisBlock(ctx context.Context) (block.SignedBeaconBlock, error)
GenesisBlockRoot(ctx context.Context) ([32]byte, error)
IsFinalizedBlock(ctx context.Context, blockRoot [32]byte) bool
FinalizedChildBlock(ctx context.Context, blockRoot [32]byte) (block.SignedBeaconBlock, error)
HighestSlotBlocksBelow(ctx context.Context, slot types.Slot) ([]block.SignedBeaconBlock, error)
@ -53,7 +54,8 @@ type ReadOnlyDatabase interface {
// Fee reicipients operations.
FeeRecipientByValidatorID(ctx context.Context, id types.ValidatorIndex) (common.Address, error)
// origin checkpoint sync support
OriginBlockRoot(ctx context.Context) ([32]byte, error)
OriginCheckpointBlockRoot(ctx context.Context) ([32]byte, error)
BackfillBlockRoot(ctx context.Context) ([32]byte, error)
}
// NoHeadAccessDatabase defines a struct without access to chain head data.
@ -102,7 +104,8 @@ type HeadAccessDatabase interface {
EnsureEmbeddedGenesis(ctx context.Context) error
// initialization method needed for origin checkpoint sync
SaveOrigin(ctx context.Context, state io.Reader, block io.Reader) error
SaveOrigin(ctx context.Context, serState, serBlock []byte) error
SaveBackfillBlockRoot(ctx context.Context, blockRoot [32]byte) error
}
// SlasherDatabase interface for persisting data related to detecting slashable offenses on Ethereum.

View File

@ -48,18 +48,18 @@ func (s *Store) Block(ctx context.Context, blockRoot [32]byte) (block.SignedBeac
return blk, err
}
// OriginBlockRoot returns the value written to the db in SaveOriginBlockRoot
// OriginCheckpointBlockRoot returns the value written to the db in SaveOriginCheckpointBlockRoot
// This is the root of a finalized block within the weak subjectivity period
// at the time the chain was started, used to initialize the database and chain
// without syncing from genesis.
func (s *Store) OriginBlockRoot(ctx context.Context) ([32]byte, error) {
_, span := trace.StartSpan(ctx, "BeaconDB.OriginBlockRoot")
func (s *Store) OriginCheckpointBlockRoot(ctx context.Context) ([32]byte, error) {
_, span := trace.StartSpan(ctx, "BeaconDB.OriginCheckpointBlockRoot")
defer span.End()
var root [32]byte
err := s.db.View(func(tx *bolt.Tx) error {
bkt := tx.Bucket(blocksBucket)
rootSlice := bkt.Get(originBlockRootKey)
rootSlice := bkt.Get(originCheckpointBlockRootKey)
if rootSlice == nil {
return ErrNotFoundOriginBlockRoot
}
@ -70,6 +70,25 @@ func (s *Store) OriginBlockRoot(ctx context.Context) ([32]byte, error) {
return root, err
}
// BackfillBlockRoot keeps track of the highest block available before the OriginCheckpointBlockRoot
func (s *Store) BackfillBlockRoot(ctx context.Context) ([32]byte, error) {
_, span := trace.StartSpan(ctx, "BeaconDB.BackfillBlockRoot")
defer span.End()
var root [32]byte
err := s.db.View(func(tx *bolt.Tx) error {
bkt := tx.Bucket(blocksBucket)
rootSlice := bkt.Get(backfillBlockRootKey)
if len(rootSlice) == 0 {
return ErrNotFoundBackfillBlockRoot
}
root = bytesutil.ToBytes32(rootSlice)
return nil
})
return root, err
}
// HeadBlock returns the latest canonical block in the Ethereum Beacon Chain.
func (s *Store) HeadBlock(ctx context.Context) (block.SignedBeaconBlock, error) {
ctx, span := trace.StartSpan(ctx, "BeaconDB.HeadBlock")
@ -325,6 +344,22 @@ func (s *Store) GenesisBlock(ctx context.Context) (block.SignedBeaconBlock, erro
return blk, err
}
func (s *Store) GenesisBlockRoot(ctx context.Context) ([32]byte, error) {
ctx, span := trace.StartSpan(ctx, "BeaconDB.GenesisBlockRoot")
defer span.End()
var root [32]byte
err := s.db.View(func(tx *bolt.Tx) error {
bkt := tx.Bucket(blocksBucket)
r := bkt.Get(genesisBlockRootKey)
if len(r) == 0 {
return ErrNotFoundGenesisBlockRoot
}
root = bytesutil.ToBytes32(r)
return nil
})
return root, err
}
// SaveGenesisBlockRoot to the db.
func (s *Store) SaveGenesisBlockRoot(ctx context.Context, blockRoot [32]byte) error {
_, span := trace.StartSpan(ctx, "BeaconDB.SaveGenesisBlockRoot")
@ -335,16 +370,27 @@ func (s *Store) SaveGenesisBlockRoot(ctx context.Context, blockRoot [32]byte) er
})
}
// SaveOriginBlockRoot is used to keep track of the block root used for origin sync.
// SaveOriginCheckpointBlockRoot is used to keep track of the block root used for syncing from a checkpoint origin.
// This should be a finalized block from within the current weak subjectivity period.
// This value is used by a running beacon chain node to locate the state at the beginning
// of the chain history, in places where genesis would typically be used.
func (s *Store) SaveOriginBlockRoot(ctx context.Context, blockRoot [32]byte) error {
_, span := trace.StartSpan(ctx, "BeaconDB.SaveOriginBlockRoot")
func (s *Store) SaveOriginCheckpointBlockRoot(ctx context.Context, blockRoot [32]byte) error {
_, span := trace.StartSpan(ctx, "BeaconDB.SaveOriginCheckpointBlockRoot")
defer span.End()
return s.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket(blocksBucket)
return bucket.Put(originBlockRootKey, blockRoot[:])
return bucket.Put(originCheckpointBlockRootKey, blockRoot[:])
})
}
// SaveBackfillBlockRoot is used to keep track of the most recently backfilled block root when
// the node was initialized via checkpoint sync.
func (s *Store) SaveBackfillBlockRoot(ctx context.Context, blockRoot [32]byte) error {
_, span := trace.StartSpan(ctx, "BeaconDB.SaveBackfillBlockRoot")
defer span.End()
return s.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket(blocksBucket)
return bucket.Put(backfillBlockRootKey, blockRoot[:])
})
}

View File

@ -58,6 +58,23 @@ var blockTests = []struct {
},
}
func TestStore_SaveBackfillBlockRoot(t *testing.T) {
db := setupDB(t)
ctx := context.Background()
_, err := db.BackfillBlockRoot(ctx)
require.ErrorIs(t, err, ErrNotFoundBackfillBlockRoot)
expected := [32]byte{}
copy(expected[:], []byte{0x23})
err = db.SaveBackfillBlockRoot(ctx, expected)
require.NoError(t, err)
actual, err := db.BackfillBlockRoot(ctx)
require.NoError(t, err)
require.Equal(t, expected, actual)
}
func TestStore_SaveBlock_NoDuplicates(t *testing.T) {
BlockCacheSize = 1
slot := types.Slot(20)

View File

@ -13,5 +13,11 @@ var ErrNotFoundState = errors.Wrap(ErrNotFound, "state not found")
// ErrNotFoundOriginBlockRoot is an error specifically for the origin block root getter
var ErrNotFoundOriginBlockRoot = errors.Wrap(ErrNotFound, "OriginBlockRoot")
// ErrNotFoundGenesisBlockRoot means no genesis block root was found, indicating the db was not initialized with genesis
var ErrNotFoundGenesisBlockRoot = errors.Wrap(ErrNotFound, "OriginGenesisRoot")
// ErrNotFoundOriginBlockRoot is an error specifically for the origin block root getter
var ErrNotFoundBackfillBlockRoot = errors.Wrap(ErrNotFound, "BackfillBlockRoot")
// ErrNotFoundFeeRecipient is a not found error specifically for the fee recipient getter
var ErrNotFoundFeeRecipient = errors.Wrap(ErrNotFound, "fee recipient")

View File

@ -32,7 +32,7 @@ var containerFinalizedButNotCanonical = []byte("recent block needs reindexing to
// - De-index all finalized beacon block roots from previous_finalized_epoch to
// new_finalized_epoch. (I.e. delete these roots from the index, to be re-indexed.)
// - Build the canonical finalized chain by walking up the ancestry chain from the finalized block
// root until a parent is found in the index or the parent is genesis.
// root until a parent is found in the index, or the parent is genesis or the origin checkpoint.
// - Add all block roots in the database where epoch(block.slot) == checkpoint.epoch.
//
// This method ensures that all blocks from the current finalized epoch are considered "final" while
@ -46,6 +46,7 @@ func (s *Store) updateFinalizedBlockRoots(ctx context.Context, tx *bolt.Tx, chec
root := checkpoint.Root
var previousRoot []byte
genesisRoot := tx.Bucket(blocksBucket).Get(genesisBlockRootKey)
initCheckpointRoot := tx.Bucket(blocksBucket).Get(originCheckpointBlockRootKey)
// De-index recent finalized block roots, to be re-indexed.
previousFinalizedCheckpoint := &ethpb.Checkpoint{}
@ -74,7 +75,7 @@ func (s *Store) updateFinalizedBlockRoots(ctx context.Context, tx *bolt.Tx, chec
// Walk up the ancestry chain until we reach a block root present in the finalized block roots
// index bucket or genesis block root.
for {
if bytes.Equal(root, genesisRoot) {
if bytes.Equal(root, genesisRoot) || bytes.Equal(root, initCheckpointRoot) {
break
}

View File

@ -51,7 +51,9 @@ var (
altairKey = []byte("altair")
bellatrixKey = []byte("merge")
// block root included in the beacon state used by weak subjectivity initial sync
originBlockRootKey = []byte("origin-block-root")
originCheckpointBlockRootKey = []byte("origin-checkpoint-block-root")
// block root tracking the progress of backfill, or pointing at genesis if backfill has not been initiated
backfillBlockRootKey = []byte("backfill-block-root")
// Deprecated: This index key was migrated in PR 6461. Do not use, except for migrations.
lastArchivedIndexKey = []byte("last-archived")

View File

@ -2,69 +2,76 @@ package kv
import (
"context"
"io"
"io/ioutil"
"fmt"
"github.com/pkg/errors"
types "github.com/prysmaticlabs/eth2-types"
"github.com/prysmaticlabs/prysm/config/params"
"github.com/prysmaticlabs/prysm/encoding/ssz/detect"
ethpb "github.com/prysmaticlabs/prysm/proto/prysm/v1alpha1"
"github.com/prysmaticlabs/prysm/runtime/version"
)
// SaveOrigin loads an ssz serialized Block & BeaconState from an io.Reader
// (ex: an open file) prepares the database so that the beacon node can begin
// syncing, using the provided values as their point of origin. This is an alternative
// to syncing from genesis, and should only be run on an empty database.
func (s *Store) SaveOrigin(ctx context.Context, stateReader, blockReader io.Reader) error {
sb, err := ioutil.ReadAll(stateReader)
func (s *Store) SaveOrigin(ctx context.Context, serState, serBlock []byte) error {
genesisRoot, err := s.GenesisBlockRoot(ctx)
if err != nil {
return errors.Wrap(err, "failed to read origin state bytes")
if errors.Is(err, ErrNotFoundGenesisBlockRoot) {
return errors.Wrap(err, "genesis block root not found: genesis must be provided for checkpoint sync")
}
return errors.Wrap(err, "genesis block root query error: checkpoint sync must verify genesis to proceed")
}
bb, err := ioutil.ReadAll(blockReader)
err = s.SaveBackfillBlockRoot(ctx, genesisRoot)
if err != nil {
return errors.Wrap(err, "error reading block given to SaveOrigin")
return errors.Wrap(err, "unable to save genesis root as initial backfill starting point for checkpoint sync")
}
cf, err := detect.FromState(sb)
cf, err := detect.FromState(serState)
if err != nil {
return errors.Wrap(err, "failed to detect config and fork for origin state")
return errors.Wrap(err, "could not sniff config+fork for origin state bytes")
}
bs, err := cf.UnmarshalBeaconState(sb)
if err != nil {
return errors.Wrap(err, "could not unmarshal origin state")
}
wblk, err := cf.UnmarshalBeaconBlock(bb)
if err != nil {
return errors.Wrap(err, "unable to unmarshal origin SignedBeaconBlock")
_, ok := params.BeaconConfig().ForkVersionSchedule[cf.Version]
if !ok {
return fmt.Errorf("config mismatch, beacon node configured to connect to %s, detected state is for %s", params.BeaconConfig().ConfigName, cf.Config.ConfigName)
}
blockRoot, err := wblk.Block().HashTreeRoot()
log.Infof("detected supported config for state & block version, config name=%s, fork name=%s", cf.Config.ConfigName, version.String(cf.Fork))
state, err := cf.UnmarshalBeaconState(serState)
if err != nil {
return errors.Wrap(err, "failed to initialize origin state w/ bytes + config+fork")
}
wblk, err := cf.UnmarshalBeaconBlock(serBlock)
if err != nil {
return errors.Wrap(err, "failed to initialize origin block w/ bytes + config+fork")
}
blk := wblk.Block()
// save block
blockRoot, err := blk.HashTreeRoot()
if err != nil {
return errors.Wrap(err, "could not compute HashTreeRoot of checkpoint block")
}
// save block
log.Infof("saving checkpoint block to db, w/ root=%#x", blockRoot)
if err := s.SaveBlock(ctx, wblk); err != nil {
return errors.Wrap(err, "could not save checkpoint block")
}
// save state
if err = s.SaveState(ctx, bs, blockRoot); err != nil {
log.Infof("calling SaveState w/ blockRoot=%x", blockRoot)
if err = s.SaveState(ctx, state, blockRoot); err != nil {
return errors.Wrap(err, "could not save state")
}
if err = s.SaveStateSummary(ctx, &ethpb.StateSummary{
Slot: bs.Slot(),
Slot: state.Slot(),
Root: blockRoot[:],
}); err != nil {
return errors.Wrap(err, "could not save state summary")
}
// save origin block root in special key, to be used when the canonical
// origin (start of chain, ie alternative to genesis) block or state is needed
if err = s.SaveOriginBlockRoot(ctx, blockRoot); err != nil {
return errors.Wrap(err, "could not save origin block root")
}
// mark block as head of chain, so that processing will pick up from this point
if err = s.SaveHeadBlockRoot(ctx, blockRoot); err != nil {
return errors.Wrap(err, "could not save head block root")
@ -87,5 +94,11 @@ func (s *Store) SaveOrigin(ctx context.Context, stateReader, blockReader io.Read
return errors.Wrap(err, "could not mark checkpoint sync block as finalized")
}
// save origin block root in a special key, to be used when the canonical
// origin (start of chain, ie alternative to genesis) block or state is needed
if err = s.SaveOriginCheckpointBlockRoot(ctx, blockRoot); err != nil {
return errors.Wrap(err, "could not save origin block root")
}
return nil
}

View File

@ -41,6 +41,8 @@ go_library(
"//beacon-chain/state:go_default_library",
"//beacon-chain/state/stategen:go_default_library",
"//beacon-chain/sync:go_default_library",
"//beacon-chain/sync/backfill:go_default_library",
"//beacon-chain/sync/checkpoint:go_default_library",
"//beacon-chain/sync/initial-sync:go_default_library",
"//cmd:go_default_library",
"//cmd/beacon-chain/flags:go_default_library",

View File

@ -44,6 +44,8 @@ import (
"github.com/prysmaticlabs/prysm/beacon-chain/state"
"github.com/prysmaticlabs/prysm/beacon-chain/state/stategen"
regularsync "github.com/prysmaticlabs/prysm/beacon-chain/sync"
"github.com/prysmaticlabs/prysm/beacon-chain/sync/backfill"
"github.com/prysmaticlabs/prysm/beacon-chain/sync/checkpoint"
initialsync "github.com/prysmaticlabs/prysm/beacon-chain/sync/initial-sync"
"github.com/prysmaticlabs/prysm/cmd"
"github.com/prysmaticlabs/prysm/cmd/beacon-chain/flags"
@ -101,6 +103,8 @@ type BeaconNode struct {
slasherAttestationsFeed *event.Feed
finalizedStateAtStartUp state.BeaconState
serviceFlagOpts *serviceFlagOpts
blockchainFlagOpts []blockchain.Option
CheckpointInitializer checkpoint.Initializer
}
// New creates a new node instance, sets up configuration options, and registers
@ -162,13 +166,19 @@ func New(cliCtx *cli.Context, opts ...Option) (*BeaconNode, error) {
if err := beacon.startDB(cliCtx, depositAddress); err != nil {
return nil, err
}
log.Debugln("Starting Slashing DB")
if err := beacon.startSlasherDB(cliCtx); err != nil {
return nil, err
}
bfs := backfill.NewStatus(beacon.db)
if err := bfs.Reload(ctx); err != nil {
return nil, errors.Wrap(err, "backfill status initialization error")
}
log.Debugln("Starting State Gen")
if err := beacon.startStateGen(); err != nil {
if err := beacon.startStateGen(ctx, bfs); err != nil {
return nil, err
}
@ -239,7 +249,7 @@ func New(cliCtx *cli.Context, opts ...Option) (*BeaconNode, error) {
// db.DatabasePath is the path to the containing directory
// db.NewDBFilename expands that to the canonical full path using
// the same constuction as NewDB()
// the same construction as NewDB()
c, err := newBeaconNodePromCollector(db.NewDBFilename(beacon.db.DatabasePath()))
if err != nil {
return nil, err
@ -396,6 +406,13 @@ func (b *BeaconNode) startDB(cliCtx *cli.Context, depositAddress string) error {
if err := b.db.EnsureEmbeddedGenesis(b.ctx); err != nil {
return err
}
if b.CheckpointInitializer != nil {
if err := b.CheckpointInitializer.Initialize(b.ctx, d); err != nil {
return err
}
}
knownContract, err := b.db.DepositContractAddress(b.ctx)
if err != nil {
return err
@ -463,10 +480,11 @@ func (b *BeaconNode) startSlasherDB(cliCtx *cli.Context) error {
return nil
}
func (b *BeaconNode) startStateGen() error {
b.stateGen = stategen.New(b.db)
func (b *BeaconNode) startStateGen(ctx context.Context, bfs *backfill.Status) error {
opts := []stategen.StateGenOption{stategen.WithBackfillStatus(bfs)}
sg := stategen.New(b.db, opts...)
cp, err := b.db.FinalizedCheckpoint(b.ctx)
cp, err := b.db.FinalizedCheckpoint(ctx)
if err != nil {
return err
}
@ -474,7 +492,7 @@ func (b *BeaconNode) startStateGen() error {
r := bytesutil.ToBytes32(cp.Root)
// Consider edge case where finalized root are zeros instead of genesis root hash.
if r == params.BeaconConfig().ZeroHash {
genesisBlock, err := b.db.GenesisBlock(b.ctx)
genesisBlock, err := b.db.GenesisBlock(ctx)
if err != nil {
return err
}
@ -486,10 +504,12 @@ func (b *BeaconNode) startStateGen() error {
}
}
b.finalizedStateAtStartUp, err = b.stateGen.StateByRoot(b.ctx, r)
b.finalizedStateAtStartUp, err = sg.StateByRoot(ctx, r)
if err != nil {
return err
}
b.stateGen = sg
return nil
}

View File

@ -12,7 +12,7 @@ import (
// PrepareStateFetchGRPCError returns an appropriate gRPC error based on the supplied argument.
// The argument error should be a result of fetching state.
func PrepareStateFetchGRPCError(err error) error {
if errors.Is(err, stategen.ErrSlotBeforeOrigin) {
if errors.Is(err, stategen.ErrNoDataForSlot) {
return status.Errorf(codes.NotFound, "lacking historical data needed to fulfill request")
}
if stateNotFoundErr, ok := err.(*statefetcher.StateNotFoundError); ok {

View File

@ -28,6 +28,7 @@ go_library(
"//beacon-chain/db:go_default_library",
"//beacon-chain/db/filters:go_default_library",
"//beacon-chain/state:go_default_library",
"//beacon-chain/sync/backfill:go_default_library",
"//cache/lru:go_default_library",
"//config/params:go_default_library",
"//encoding/bytesutil:go_default_library",

View File

@ -4,6 +4,7 @@ import (
"context"
"github.com/pkg/errors"
types "github.com/prysmaticlabs/eth2-types"
"github.com/prysmaticlabs/prysm/beacon-chain/core/helpers"
"github.com/prysmaticlabs/prysm/beacon-chain/state"
"github.com/prysmaticlabs/prysm/config/params"
@ -12,7 +13,7 @@ import (
"go.opencensus.io/trace"
)
var ErrSlotBeforeOrigin = errors.New("cannot retrieve data for slots before sync origin")
var ErrNoDataForSlot = errors.New("cannot retrieve data for slot")
// HasState returns true if the state exists in cache or in DB.
func (s *State) HasState(ctx context.Context, blockRoot [32]byte) (bool, error) {
@ -230,16 +231,17 @@ func (s *State) LastAncestorState(ctx context.Context, root [32]byte) (state.Bea
return nil, ctx.Err()
}
// return an error if we have rewound to before the checkpoint sync slot
if (b.Block().Slot() - 1) < s.minimumSlot() {
return nil, errors.Wrapf(ErrSlotBeforeOrigin, "no blocks in db prior to slot %d", s.minimumSlot())
}
// Is the state a genesis state.
parentRoot := bytesutil.ToBytes32(b.Block().ParentRoot())
if parentRoot == params.BeaconConfig().ZeroHash {
return s.beaconDB.GenesisState(ctx)
}
// return an error if slot hasn't been covered by checkpoint sync backfill
ps := b.Block().Slot() - 1
if !s.slotAvailable(ps) {
return nil, errors.Wrapf(ErrNoDataForSlot, "slot %d not in db due to checkpoint sync", ps)
}
// Does the state exist in the hot state cache.
if s.hotStateCache.has(parentRoot) {
return s.hotStateCache.get(parentRoot), nil
@ -283,3 +285,11 @@ func (s *State) CombinedCache() *CombinedCache {
}
return &CombinedCache{getters: getters}
}
func (s *State) slotAvailable(slot types.Slot) bool {
// default to assuming node was initialized from genesis - backfill only needs to be specified for checkpoint sync
if s.backfillStatus == nil {
return true
}
return s.backfillStatus.SlotCovered(slot)
}

View File

@ -11,6 +11,7 @@ import (
types "github.com/prysmaticlabs/eth2-types"
"github.com/prysmaticlabs/prysm/beacon-chain/db"
"github.com/prysmaticlabs/prysm/beacon-chain/state"
"github.com/prysmaticlabs/prysm/beacon-chain/sync/backfill"
"github.com/prysmaticlabs/prysm/config/params"
"github.com/prysmaticlabs/prysm/encoding/bytesutil"
ethpb "github.com/prysmaticlabs/prysm/proto/prysm/v1alpha1"
@ -48,8 +49,7 @@ type State struct {
finalizedInfo *finalizedInfo
epochBoundaryStateCache *epochBoundaryState
saveHotStateDB *saveHotStateDbConfig
minSlot types.Slot
beaconDBInitType BeaconDBInitType
backfillStatus *backfill.Status
}
// This tracks the config in the event of long non-finality,
@ -71,28 +71,15 @@ type finalizedInfo struct {
lock sync.RWMutex
}
func WithMinimumSlot(min types.Slot) StateGenOption {
return func(s *State) {
s.minSlot = min
}
}
type BeaconDBInitType uint
const (
BeaconDBInitTypeGenesisState = iota
BeaconDBInitTypeCheckpoint
)
func WithInitType(t BeaconDBInitType) StateGenOption {
return func(s *State) {
s.beaconDBInitType = t
}
}
// StateGenOption is a functional option for controlling the initialization of a *State value
type StateGenOption func(*State)
func WithBackfillStatus(bfs *backfill.Status) StateGenOption {
return func(sg *State) {
sg.backfillStatus = bfs
}
}
// New returns a new state management object.
func New(beaconDB db.NoHeadAccessDatabase, opts ...StateGenOption) *State {
s := &State{
@ -104,12 +91,11 @@ func New(beaconDB db.NoHeadAccessDatabase, opts ...StateGenOption) *State {
saveHotStateDB: &saveHotStateDbConfig{
duration: defaultHotStateDBInterval,
},
// defaults to minimumSlot of zero (genesis), overridden by checkpoint sync
minSlot: params.BeaconConfig().GenesisSlot,
}
for _, o := range opts {
o(s)
}
return s
}
@ -167,7 +153,3 @@ func (s *State) finalizedState() state.BeaconState {
defer s.finalizedInfo.lock.RUnlock()
return s.finalizedInfo.state.Copy()
}
func (s *State) minimumSlot() types.Slot {
return s.minSlot
}

View File

@ -0,0 +1,32 @@
load("@prysm//tools/go:def.bzl", "go_library", "go_test")
go_library(
name = "go_default_library",
srcs = ["status.go"],
importpath = "github.com/prysmaticlabs/prysm/beacon-chain/sync/backfill",
visibility = ["//visibility:public"],
deps = [
"//beacon-chain/core/helpers:go_default_library",
"//beacon-chain/db:go_default_library",
"//proto/prysm/v1alpha1/block:go_default_library",
"@com_github_pkg_errors//:go_default_library",
"@com_github_prysmaticlabs_eth2_types//:go_default_library",
],
)
go_test(
name = "go_default_test",
srcs = ["status_test.go"],
embed = [":go_default_library"],
deps = [
"//beacon-chain/core/helpers:go_default_library",
"//beacon-chain/db:go_default_library",
"//config/params:go_default_library",
"//proto/prysm/v1alpha1/block:go_default_library",
"//proto/prysm/v1alpha1/wrapper:go_default_library",
"//testing/require:go_default_library",
"//testing/util:go_default_library",
"@com_github_pkg_errors//:go_default_library",
"@com_github_prysmaticlabs_eth2_types//:go_default_library",
],
)

View File

@ -0,0 +1,122 @@
package backfill
import (
"context"
"github.com/pkg/errors"
types "github.com/prysmaticlabs/eth2-types"
"github.com/prysmaticlabs/prysm/beacon-chain/core/helpers"
"github.com/prysmaticlabs/prysm/beacon-chain/db"
"github.com/prysmaticlabs/prysm/proto/prysm/v1alpha1/block"
)
// NewStatus correctly initializes a Status value with the required database value.
func NewStatus(store BackfillDB) *Status {
return &Status{
store: store,
}
}
// Status provides a way to update and query the status of a backfill process that may be necessary to track when
// a node was initialized via checkpoint sync. With checkpoint sync, there will be a gap in node history from genesis
// until the checkpoint sync origin block. Status provides the means to update the value keeping track of the lower
// end of the missing block range via the Advance() method, to check whether a Slot is missing from the database
// via the SlotCovered() method, and to see the current StartGap() and EndGap().
type Status struct {
start types.Slot
end types.Slot
store BackfillDB
genesisSync bool
}
// SlotCovered uses StartGap() and EndGap() to determine if the given slot is covered by the current chain history.
// If the slot is <= StartGap(), or >= EndGap(), the result is true.
// If the slot is between StartGap() and EndGap(), the result is false.
func (s *Status) SlotCovered(sl types.Slot) bool {
// short circuit if the node was synced from genesis
if s.genesisSync {
return true
}
if s.StartGap() < sl && sl < s.EndGap() {
return false
}
return true
}
// StartGap returns the slot at the beginning of the range that needs to be backfilled.
func (s *Status) StartGap() types.Slot {
return s.start
}
// EndGap returns the slot at the end of the range that needs to be backfilled.
func (s *Status) EndGap() types.Slot {
return s.end
}
var ErrAdvancePastOrigin = errors.New("cannot advance backfill Status beyond the origin checkpoint slot")
// Advance advances the backfill position to the given slot & root.
// It updates the backfill block root entry in the database,
// and also updates the Status value's copy of the backfill position slot.
func (s *Status) Advance(ctx context.Context, upTo types.Slot, root [32]byte) error {
if upTo > s.end {
return errors.Wrapf(ErrAdvancePastOrigin, "advance slot=%d, origin slot=%d", upTo, s.end)
}
s.start = upTo
return s.store.SaveBackfillBlockRoot(ctx, root)
}
// Reload queries the database for backfill status, initializing the internal data and validating the database state.
func (s *Status) Reload(ctx context.Context) error {
cpRoot, err := s.store.OriginCheckpointBlockRoot(ctx)
if err != nil {
// mark genesis sync and short circuit further lookups
if errors.Is(err, db.ErrNotFoundOriginBlockRoot) {
s.genesisSync = true
return nil
}
return err
}
cpBlock, err := s.store.Block(ctx, cpRoot)
if err != nil {
return errors.Wrapf(err, "error retrieving block for origin checkpoint root=%#x", cpRoot)
}
if err := helpers.BeaconBlockIsNil(cpBlock); err != nil {
return err
}
s.end = cpBlock.Block().Slot()
_, err = s.store.GenesisBlockRoot(ctx)
if err != nil {
if errors.Is(err, db.ErrNotFoundGenesisBlockRoot) {
return errors.Wrap(err, "genesis block root required for checkpoint sync")
}
return err
}
bfRoot, err := s.store.BackfillBlockRoot(ctx)
if err != nil {
if errors.Is(err, db.ErrNotFoundBackfillBlockRoot) {
return errors.Wrap(err, "found origin checkpoint block root, but no backfill block root")
}
return err
}
bfBlock, err := s.store.Block(ctx, bfRoot)
if err != nil {
return errors.Wrapf(err, "error retrieving block for backfill root=%#x", bfRoot)
}
if err := helpers.BeaconBlockIsNil(bfBlock); err != nil {
return err
}
s.start = bfBlock.Block().Slot()
return nil
}
// BackfillDB describes the set of DB methods that the Status type needs to function.
type BackfillDB interface {
SaveBackfillBlockRoot(ctx context.Context, blockRoot [32]byte) error
GenesisBlockRoot(ctx context.Context) ([32]byte, error)
OriginCheckpointBlockRoot(ctx context.Context) ([32]byte, error)
BackfillBlockRoot(ctx context.Context) ([32]byte, error)
Block(ctx context.Context, blockRoot [32]byte) (block.SignedBeaconBlock, error)
}

View File

@ -0,0 +1,359 @@
package backfill
import (
"context"
"testing"
"github.com/prysmaticlabs/prysm/beacon-chain/core/helpers"
"github.com/prysmaticlabs/prysm/beacon-chain/db"
"github.com/pkg/errors"
types "github.com/prysmaticlabs/eth2-types"
"github.com/prysmaticlabs/prysm/config/params"
"github.com/prysmaticlabs/prysm/proto/prysm/v1alpha1/block"
"github.com/prysmaticlabs/prysm/proto/prysm/v1alpha1/wrapper"
"github.com/prysmaticlabs/prysm/testing/require"
"github.com/prysmaticlabs/prysm/testing/util"
)
var errEmptyMockDBMethod = errors.New("uninitialized mock db method called")
type mockBackfillDB struct {
saveBackfillBlockRoot func(ctx context.Context, blockRoot [32]byte) error
genesisBlockRoot func(ctx context.Context) ([32]byte, error)
originCheckpointBlockRoot func(ctx context.Context) ([32]byte, error)
backfillBlockRoot func(ctx context.Context) ([32]byte, error)
block func(ctx context.Context, blockRoot [32]byte) (block.SignedBeaconBlock, error)
}
var _ BackfillDB = &mockBackfillDB{}
func (db *mockBackfillDB) SaveBackfillBlockRoot(ctx context.Context, blockRoot [32]byte) error {
if db.saveBackfillBlockRoot != nil {
return db.saveBackfillBlockRoot(ctx, blockRoot)
}
return errEmptyMockDBMethod
}
func (db *mockBackfillDB) GenesisBlockRoot(ctx context.Context) ([32]byte, error) {
if db.genesisBlockRoot != nil {
return db.genesisBlockRoot(ctx)
}
return [32]byte{}, errEmptyMockDBMethod
}
func (db *mockBackfillDB) OriginCheckpointBlockRoot(ctx context.Context) ([32]byte, error) {
if db.originCheckpointBlockRoot != nil {
return db.originCheckpointBlockRoot(ctx)
}
return [32]byte{}, errEmptyMockDBMethod
}
func (db *mockBackfillDB) BackfillBlockRoot(ctx context.Context) ([32]byte, error) {
if db.backfillBlockRoot != nil {
return db.backfillBlockRoot(ctx)
}
return [32]byte{}, errEmptyMockDBMethod
}
func (db *mockBackfillDB) Block(ctx context.Context, blockRoot [32]byte) (block.SignedBeaconBlock, error) {
if db.block != nil {
return db.block(ctx, blockRoot)
}
return nil, errEmptyMockDBMethod
}
func TestSlotCovered(t *testing.T) {
cases := []struct {
name string
slot types.Slot
status *Status
result bool
}{
{
name: "below start true",
status: &Status{start: 1},
slot: 0,
result: true,
},
{
name: "above end true",
status: &Status{end: 1},
slot: 2,
result: true,
},
{
name: "equal end true",
status: &Status{end: 1},
slot: 1,
result: true,
},
{
name: "equal start true",
status: &Status{start: 2},
slot: 2,
result: true,
},
{
name: "between false",
status: &Status{start: 1, end: 3},
slot: 2,
result: false,
},
{
name: "genesisSync always true",
status: &Status{genesisSync: true},
slot: 100,
result: true,
},
}
for _, c := range cases {
result := c.status.SlotCovered(c.slot)
require.Equal(t, c.result, result)
}
}
func TestAdvance(t *testing.T) {
ctx := context.Background()
saveBackfillBuf := make([][32]byte, 0)
mdb := &mockBackfillDB{
saveBackfillBlockRoot: func(ctx context.Context, root [32]byte) error {
saveBackfillBuf = append(saveBackfillBuf, root)
return nil
},
}
s := &Status{end: 100, store: mdb}
var root [32]byte
copy(root[:], []byte{0x23, 0x23})
require.NoError(t, s.Advance(ctx, 90, root))
require.Equal(t, root, saveBackfillBuf[0])
not := s.SlotCovered(95)
require.Equal(t, false, not)
// this should still be len 1 after failing to advance
require.Equal(t, 1, len(saveBackfillBuf))
require.ErrorIs(t, s.Advance(ctx, s.end+1, root), ErrAdvancePastOrigin)
// this has an element in it from the previous test, there shouldn't be an additional one
require.Equal(t, 1, len(saveBackfillBuf))
}
func goodBlockRoot(root [32]byte) func(ctx context.Context) ([32]byte, error) {
return func(ctx context.Context) ([32]byte, error) {
return root, nil
}
}
func setupTestBlock(slot types.Slot) (block.SignedBeaconBlock, error) {
bRaw := util.NewBeaconBlock()
b, err := wrapper.WrappedSignedBeaconBlock(bRaw)
if err != nil {
return nil, err
}
return b, wrapper.SetBlockSlot(b, slot)
}
func TestReload(t *testing.T) {
ctx := context.Background()
derp := errors.New("derp")
originSlot := types.Slot(100)
var originRoot [32]byte
copy(originRoot[:], []byte{0x01})
originBlock, err := setupTestBlock(originSlot)
require.NoError(t, err)
backfillSlot := types.Slot(50)
var backfillRoot [32]byte
copy(originRoot[:], []byte{0x02})
backfillBlock, err := setupTestBlock(backfillSlot)
require.NoError(t, err)
cases := []struct {
name string
db BackfillDB
err error
expected *Status
}{
{
name: "origin not found, implying genesis sync ",
db: &mockBackfillDB{
genesisBlockRoot: goodBlockRoot(params.BeaconConfig().ZeroHash),
originCheckpointBlockRoot: func(ctx context.Context) ([32]byte, error) {
return [32]byte{}, db.ErrNotFoundOriginBlockRoot
}},
expected: &Status{genesisSync: true},
},
{
name: "genesis not found error",
err: db.ErrNotFoundGenesisBlockRoot,
db: &mockBackfillDB{
genesisBlockRoot: func(ctx context.Context) ([32]byte, error) {
return [32]byte{}, db.ErrNotFoundGenesisBlockRoot
},
originCheckpointBlockRoot: goodBlockRoot(originRoot),
block: func(ctx context.Context, root [32]byte) (block.SignedBeaconBlock, error) {
switch root {
case originRoot:
return originBlock, nil
}
return nil, nil
},
},
},
{
name: "other genesis error",
err: derp,
db: &mockBackfillDB{
genesisBlockRoot: func(ctx context.Context) ([32]byte, error) {
return [32]byte{}, derp
},
originCheckpointBlockRoot: goodBlockRoot(originRoot),
block: func(ctx context.Context, root [32]byte) (block.SignedBeaconBlock, error) {
switch root {
case originRoot:
return originBlock, nil
}
return nil, nil
},
},
},
{
name: "origin other error",
db: &mockBackfillDB{
genesisBlockRoot: goodBlockRoot(params.BeaconConfig().ZeroHash),
originCheckpointBlockRoot: func(ctx context.Context) ([32]byte, error) {
return [32]byte{}, derp
}},
err: derp,
},
{
name: "origin root found, block missing",
db: &mockBackfillDB{
genesisBlockRoot: goodBlockRoot(params.BeaconConfig().ZeroHash),
originCheckpointBlockRoot: goodBlockRoot(originRoot),
block: func(ctx context.Context, root [32]byte) (block.SignedBeaconBlock, error) {
return nil, nil
},
},
err: helpers.ErrNilSignedBeaconBlock,
},
{
name: "origin root found, block error",
db: &mockBackfillDB{
genesisBlockRoot: goodBlockRoot(params.BeaconConfig().ZeroHash),
originCheckpointBlockRoot: goodBlockRoot(originRoot),
block: func(ctx context.Context, root [32]byte) (block.SignedBeaconBlock, error) {
return nil, derp
},
},
err: derp,
},
{
name: "origin root found, block found, backfill root not found",
db: &mockBackfillDB{
genesisBlockRoot: goodBlockRoot(params.BeaconConfig().ZeroHash),
originCheckpointBlockRoot: goodBlockRoot(originRoot),
block: func(ctx context.Context, root [32]byte) (block.SignedBeaconBlock, error) {
return originBlock, nil
},
backfillBlockRoot: func(ctx context.Context) ([32]byte, error) {
return [32]byte{}, db.ErrNotFoundBackfillBlockRoot
},
},
err: db.ErrNotFoundBackfillBlockRoot,
},
{
name: "origin root found, block found, random backfill root err",
db: &mockBackfillDB{
genesisBlockRoot: goodBlockRoot(params.BeaconConfig().ZeroHash),
originCheckpointBlockRoot: goodBlockRoot(originRoot),
block: func(ctx context.Context, root [32]byte) (block.SignedBeaconBlock, error) {
switch root {
case originRoot:
return originBlock, nil
case backfillRoot:
return nil, nil
}
return nil, derp
},
backfillBlockRoot: func(ctx context.Context) ([32]byte, error) {
return [32]byte{}, derp
},
},
err: derp,
},
{
name: "origin root found, block found, backfill root found, backfill block not found",
db: &mockBackfillDB{
genesisBlockRoot: goodBlockRoot(params.BeaconConfig().ZeroHash),
originCheckpointBlockRoot: goodBlockRoot(originRoot),
block: func(ctx context.Context, root [32]byte) (block.SignedBeaconBlock, error) {
switch root {
case originRoot:
return originBlock, nil
case backfillRoot:
return nil, nil
}
return nil, derp
},
backfillBlockRoot: goodBlockRoot(backfillRoot),
},
err: helpers.ErrNilSignedBeaconBlock,
},
{
name: "origin root found, block found, backfill root found, backfill block random err",
db: &mockBackfillDB{
genesisBlockRoot: goodBlockRoot(params.BeaconConfig().ZeroHash),
originCheckpointBlockRoot: goodBlockRoot(originRoot),
block: func(ctx context.Context, root [32]byte) (block.SignedBeaconBlock, error) {
switch root {
case originRoot:
return originBlock, nil
case backfillRoot:
return nil, derp
}
return nil, errors.New("not derp")
},
backfillBlockRoot: goodBlockRoot(backfillRoot),
},
err: derp,
},
{
name: "complete happy path",
db: &mockBackfillDB{
genesisBlockRoot: goodBlockRoot(params.BeaconConfig().ZeroHash),
originCheckpointBlockRoot: goodBlockRoot(originRoot),
block: func(ctx context.Context, root [32]byte) (block.SignedBeaconBlock, error) {
switch root {
case originRoot:
return originBlock, nil
case backfillRoot:
return backfillBlock, nil
}
return nil, errors.New("not derp")
},
backfillBlockRoot: goodBlockRoot(backfillRoot),
},
err: derp,
expected: &Status{genesisSync: false, start: backfillSlot, end: originSlot},
},
}
for _, c := range cases {
s := &Status{
store: c.db,
}
err := s.Reload(ctx)
if err != nil {
require.ErrorIs(t, err, c.err)
continue
}
require.NoError(t, err)
if c.expected == nil {
continue
}
require.Equal(t, c.expected.genesisSync, s.genesisSync)
require.Equal(t, c.expected.start, s.start)
require.Equal(t, c.expected.end, s.end)
}
}

View File

@ -0,0 +1,17 @@
load("@prysm//tools/go:def.bzl", "go_library")
go_library(
name = "go_default_library",
srcs = [
"api.go",
"file.go",
],
importpath = "github.com/prysmaticlabs/prysm/beacon-chain/sync/checkpoint",
visibility = ["//visibility:public"],
deps = [
"//api/client/beacon:go_default_library",
"//beacon-chain/db:go_default_library",
"//io/file:go_default_library",
"@com_github_pkg_errors//:go_default_library",
],
)

View File

@ -0,0 +1,35 @@
package checkpoint
import (
"context"
"github.com/pkg/errors"
"github.com/prysmaticlabs/prysm/api/client/beacon"
"github.com/prysmaticlabs/prysm/beacon-chain/db"
)
// APIInitializer manages initializing the beacon node using checkpoint sync, retrieving the checkpoint state and root
// from the remote beacon node api.
type APIInitializer struct {
c *beacon.Client
}
// NewAPIInitializer creates an APIInitializer, handling the set up of a beacon node api client
// using the provided host string.
func NewAPIInitializer(beaconNodeHost string) (*APIInitializer, error) {
c, err := beacon.NewClient(beaconNodeHost)
if err != nil {
return nil, errors.Wrapf(err, "unable to parse beacon node url or hostname - %s", beaconNodeHost)
}
return &APIInitializer{c: c}, nil
}
// Initialize downloads origin state and block for checkpoint sync and initializes database records to
// prepare the node to begin syncing from that point.
func (dl *APIInitializer) Initialize(ctx context.Context, d db.Database) error {
od, err := beacon.DownloadOriginData(ctx, dl.c)
if err != nil {
return errors.Wrap(err, "Error retrieving checkpoint origin state and block")
}
return d.SaveOrigin(ctx, od.StateBytes(), od.BlockBytes())
}

View File

@ -0,0 +1,67 @@
package checkpoint
import (
"context"
"fmt"
"os"
"github.com/prysmaticlabs/prysm/io/file"
"github.com/pkg/errors"
"github.com/prysmaticlabs/prysm/beacon-chain/db"
)
// Initializer describes a type that is able to obtain the checkpoint sync data (BeaconState and SignedBeaconBlock)
// in some way and perform database setup to prepare the beacon node for syncing from the given checkpoint.
// See FileInitializer and APIInitializer.
type Initializer interface {
Initialize(ctx context.Context, d db.Database) error
}
// NewFileInitializer validates the given path information and creates an Initializer which will
// use the provided state and block files to prepare the node for checkpoint sync.
func NewFileInitializer(blockPath string, statePath string) (*FileInitializer, error) {
var err error
if err = existsAndIsFile(blockPath); err != nil {
return nil, err
}
if err = existsAndIsFile(statePath); err != nil {
return nil, err
}
// stat just to make sure it actually exists and is a file
return &FileInitializer{blockPath: blockPath, statePath: statePath}, nil
}
// FileInitializer initializes a beacon-node database to use checkpoint sync,
// using ssz-encoded block and state data stored in files on the local filesystem.
type FileInitializer struct {
blockPath string
statePath string
}
// Initialize is called in the BeaconNode db startup code if an Initializer is present.
// Initialize does what is needed to prepare the beacon node database for syncing from the weak subjectivity checkpoint.
func (fi *FileInitializer) Initialize(ctx context.Context, d db.Database) error {
serBlock, err := file.ReadFileAsBytes(fi.blockPath)
if err != nil {
return errors.Wrapf(err, "error reading block file %s for checkpoint sync init", fi.blockPath)
}
serState, err := file.ReadFileAsBytes(fi.statePath)
if err != nil {
return errors.Wrapf(err, "error reading state file %s for checkpoint sync init", fi.blockPath)
}
return d.SaveOrigin(ctx, serState, serBlock)
}
var _ Initializer = &FileInitializer{}
func existsAndIsFile(path string) error {
info, err := os.Stat(path)
if err != nil {
return errors.Wrapf(err, "error checking existence of ssz-encoded file %s for checkpoint sync init", path)
}
if info.IsDir() {
return fmt.Errorf("%s is a directory, please specify full path to file", path)
}
return nil
}

View File

@ -21,6 +21,7 @@ go_library(
"//cmd/beacon-chain/db:go_default_library",
"//cmd/beacon-chain/flags:go_default_library",
"//cmd/beacon-chain/powchain:go_default_library",
"//cmd/beacon-chain/sync/checkpoint:go_default_library",
"//config/features:go_default_library",
"//io/file:go_default_library",
"//io/logs:go_default_library",

View File

@ -10,7 +10,7 @@ import (
// FlagOptions for blockchain service flag configurations.
func FlagOptions(c *cli.Context) ([]blockchain.Option, error) {
wsp := c.String(flags.WeakSubjectivityCheckpt.Name)
wsp := c.String(flags.WeakSubjectivityCheckpoint.Name)
wsCheckpt, err := helpers.ParseWeakSubjectivityInputString(wsp)
if err != nil {
return nil, err

View File

@ -185,13 +185,6 @@ var (
Name: "network-id",
Usage: "Sets the network id of the beacon chain.",
}
// WeakSubjectivityCheckpt defines the weak subjectivity checkpoint the node must sync through to defend against long range attacks.
WeakSubjectivityCheckpt = &cli.StringFlag{
Name: "weak-subjectivity-checkpoint",
Usage: "Input in `block_root:epoch_number` format. This guarantees that syncing leads to the given Weak Subjectivity Checkpoint along the canonical chain. " +
"If such a sync is not possible, the node will treat it a critical and irrecoverable failure",
Value: "",
}
// Eth1HeaderReqLimit defines a flag to set the maximum number of headers that a deposit log query can fetch. If none is set, 1000 will be the limit.
Eth1HeaderReqLimit = &cli.Uint64Flag{
Name: "eth1-header-req-limit",
@ -204,6 +197,14 @@ var (
Usage: "Load a genesis state from ssz file. Testnet genesis files can be found in the " +
"eth2-clients/eth2-testnets repository on github.",
}
// WeakSubjectivityCheckpoint defines the weak subjectivity checkpoint the node must sync through to defend against long range attacks.
WeakSubjectivityCheckpoint = &cli.StringFlag{
Name: "weak-subjectivity-checkpoint",
Usage: "Input in `block_root:epoch_number` format." +
" This guarantees that syncing leads to the given Weak Subjectivity Checkpoint along the canonical chain. " +
"If such a sync is not possible, the node will treat it as a critical and irrecoverable failure",
Value: "",
}
// MinPeersPerSubnet defines a flag to set the minimum number of peers that a node will attempt to peer with for a subnet.
MinPeersPerSubnet = &cli.Uint64Flag{
Name: "minimum-peers-per-subnet",

View File

@ -17,6 +17,7 @@ import (
dbcommands "github.com/prysmaticlabs/prysm/cmd/beacon-chain/db"
"github.com/prysmaticlabs/prysm/cmd/beacon-chain/flags"
powchaincmd "github.com/prysmaticlabs/prysm/cmd/beacon-chain/powchain"
"github.com/prysmaticlabs/prysm/cmd/beacon-chain/sync/checkpoint"
"github.com/prysmaticlabs/prysm/config/features"
"github.com/prysmaticlabs/prysm/io/file"
"github.com/prysmaticlabs/prysm/io/logs"
@ -62,7 +63,7 @@ var appFlags = []cli.Flag{
flags.HistoricalSlasherNode,
flags.ChainID,
flags.NetworkID,
flags.WeakSubjectivityCheckpt,
flags.WeakSubjectivityCheckpoint,
flags.Eth1HeaderReqLimit,
flags.GenesisStatePath,
flags.MinPeersPerSubnet,
@ -118,6 +119,9 @@ var appFlags = []cli.Flag{
cmd.BoltMMapInitialSizeFlag,
cmd.ValidatorMonitorIndicesFlag,
cmd.ApiTimeoutFlag,
checkpoint.BlockPath,
checkpoint.StatePath,
checkpoint.RemoteURL,
}
func init() {
@ -242,6 +246,13 @@ func startNode(ctx *cli.Context) error {
node.WithBlockchainFlagOptions(blockchainFlagOpts),
node.WithPowchainFlagOptions(powchainFlagOpts),
}
cptOpts, err := checkpoint.BeaconNodeOptions(ctx)
if err != nil {
return err
}
if cptOpts != nil {
opts = append(opts, cptOpts)
}
beacon, err := node.New(ctx, opts...)
if err != nil {
return err

View File

@ -0,0 +1,14 @@
load("@prysm//tools/go:def.bzl", "go_library")
go_library(
name = "go_default_library",
srcs = ["options.go"],
importpath = "github.com/prysmaticlabs/prysm/cmd/beacon-chain/sync/checkpoint",
visibility = ["//visibility:public"],
deps = [
"//beacon-chain/node:go_default_library",
"//beacon-chain/sync/checkpoint:go_default_library",
"@com_github_pkg_errors//:go_default_library",
"@com_github_urfave_cli_v2//:go_default_library",
],
)

View File

@ -0,0 +1,68 @@
package checkpoint
import (
"fmt"
"github.com/pkg/errors"
"github.com/prysmaticlabs/prysm/beacon-chain/node"
"github.com/prysmaticlabs/prysm/beacon-chain/sync/checkpoint"
"github.com/urfave/cli/v2"
)
var (
// StatePath defines a flag to start the beacon chain from a give genesis state file.
StatePath = &cli.PathFlag{
Name: "checkpoint-state",
Usage: "Rather than syncing from genesis, you can start processing from a ssz-serialized BeaconState+Block." +
" This flag allows you to specify a local file containing the checkpoint BeaconState to load.",
}
// BlockPath is required when using StatePath to also provide the latest integrated block.
BlockPath = &cli.PathFlag{
Name: "checkpoint-block",
Usage: "Rather than syncing from genesis, you can start processing from a ssz-serialized BeaconState+Block." +
" This flag allows you to specify a local file containing the checkpoint Block to load.",
}
RemoteURL = &cli.StringFlag{
Name: "checkpoint-sync-url",
Usage: "URL of a synced beacon node to trust in obtaining checkpoint sync data. " +
"As an additional safety measure, it is strongly recommended to only use this option in conjunction with " +
"--weak-subjectivity-checkpoint flag",
}
)
// BeaconNodeOptions is responsible for determining if the checkpoint sync options have been used, and if so,
// reading the block and state ssz-serialized values from the filesystem locations specified and preparing a
// checkpoint.Initializer, which uses the provided io.ReadClosers to initialize the beacon node database.
func BeaconNodeOptions(c *cli.Context) (node.Option, error) {
blockPath := c.Path(BlockPath.Name)
statePath := c.Path(StatePath.Name)
remoteURL := c.String(RemoteURL.Name)
if remoteURL != "" {
return func(node *node.BeaconNode) error {
var err error
node.CheckpointInitializer, err = checkpoint.NewAPIInitializer(remoteURL)
if err != nil {
return errors.Wrap(err, "error while constructing beacon node api client for checkpoint sync")
}
return nil
}, nil
}
if blockPath == "" && statePath == "" {
return nil, nil
}
if blockPath != "" && statePath == "" {
return nil, fmt.Errorf("--checkpoint-block specified, but not --checkpoint-state. both are required")
}
if blockPath == "" && statePath != "" {
return nil, fmt.Errorf("--checkpoint-state specified, but not --checkpoint-block. both are required")
}
return func(node *node.BeaconNode) (err error) {
node.CheckpointInitializer, err = checkpoint.NewFileInitializer(blockPath, statePath)
if err != nil {
return errors.Wrap(err, "error preparing to initialize checkpoint from local ssz files")
}
return nil
}, nil
}

View File

@ -7,6 +7,7 @@ import (
"github.com/prysmaticlabs/prysm/cmd"
"github.com/prysmaticlabs/prysm/cmd/beacon-chain/flags"
"github.com/prysmaticlabs/prysm/cmd/beacon-chain/sync/checkpoint"
"github.com/prysmaticlabs/prysm/config/features"
"github.com/prysmaticlabs/prysm/runtime/debug"
"github.com/urfave/cli/v2"
@ -75,6 +76,9 @@ var appHelpFlagGroups = []flagGroup{
cmd.BoltMMapInitialSizeFlag,
cmd.ValidatorMonitorIndicesFlag,
cmd.ApiTimeoutFlag,
checkpoint.BlockPath,
checkpoint.StatePath,
checkpoint.RemoteURL,
},
},
{
@ -121,7 +125,7 @@ var appHelpFlagGroups = []flagGroup{
flags.HistoricalSlasherNode,
flags.ChainID,
flags.NetworkID,
flags.WeakSubjectivityCheckpt,
flags.WeakSubjectivityCheckpoint,
flags.Eth1HeaderReqLimit,
flags.GenesisStatePath,
flags.MinPeersPerSubnet,

View File

@ -30,6 +30,8 @@ func WrapFlags(flags []cli.Flag) []cli.Flag {
f = altsrc.NewUint64Flag(t)
case *cli.UintFlag:
f = altsrc.NewUintFlag(t)
case *cli.PathFlag:
f = altsrc.NewPathFlag(t)
case *cli.Int64Flag:
// Int64Flag does not work. See https://github.com/prysmaticlabs/prysm/issues/6478
panic(fmt.Sprintf("unsupported flag type type %T", f))