From 7920528ede01e38df40c14cfdd90c101e50851db Mon Sep 17 00:00:00 2001 From: kasey <489222+kasey@users.noreply.github.com> Date: Mon, 28 Mar 2022 16:01:55 -0500 Subject: [PATCH] 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 --- beacon-chain/blockchain/service.go | 4 +- .../blockchain/weak_subjectivity_checks.go | 7 +- .../weak_subjectivity_checks_test.go | 35 +- beacon-chain/db/errors.go | 6 + beacon-chain/db/iface/interface.go | 7 +- beacon-chain/db/kv/blocks.go | 62 ++- beacon-chain/db/kv/blocks_test.go | 17 + beacon-chain/db/kv/error.go | 6 + beacon-chain/db/kv/finalized_block_roots.go | 5 +- beacon-chain/db/kv/schema.go | 4 +- beacon-chain/db/kv/wss.go | 65 ++-- beacon-chain/node/BUILD.bazel | 2 + beacon-chain/node/node.go | 34 +- .../rpc/eth/helpers/error_handling.go | 2 +- beacon-chain/state/stategen/BUILD.bazel | 1 + beacon-chain/state/stategen/getter.go | 20 +- beacon-chain/state/stategen/service.go | 36 +- beacon-chain/sync/backfill/BUILD.bazel | 32 ++ beacon-chain/sync/backfill/status.go | 122 ++++++ beacon-chain/sync/backfill/status_test.go | 359 ++++++++++++++++++ beacon-chain/sync/checkpoint/BUILD.bazel | 17 + beacon-chain/sync/checkpoint/api.go | 35 ++ beacon-chain/sync/checkpoint/file.go | 67 ++++ cmd/beacon-chain/BUILD.bazel | 1 + cmd/beacon-chain/blockchain/options.go | 2 +- cmd/beacon-chain/flags/base.go | 15 +- cmd/beacon-chain/main.go | 13 +- cmd/beacon-chain/sync/checkpoint/BUILD.bazel | 14 + cmd/beacon-chain/sync/checkpoint/options.go | 68 ++++ cmd/beacon-chain/usage.go | 6 +- cmd/wrap_flags.go | 2 + 31 files changed, 958 insertions(+), 108 deletions(-) create mode 100644 beacon-chain/sync/backfill/BUILD.bazel create mode 100644 beacon-chain/sync/backfill/status.go create mode 100644 beacon-chain/sync/backfill/status_test.go create mode 100644 beacon-chain/sync/checkpoint/BUILD.bazel create mode 100644 beacon-chain/sync/checkpoint/api.go create mode 100644 beacon-chain/sync/checkpoint/file.go create mode 100644 cmd/beacon-chain/sync/checkpoint/BUILD.bazel create mode 100644 cmd/beacon-chain/sync/checkpoint/options.go diff --git a/beacon-chain/blockchain/service.go b/beacon-chain/blockchain/service.go index f6b7528fc..a36810c6e 100644 --- a/beacon-chain/blockchain/service.go +++ b/beacon-chain/blockchain/service.go @@ -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 { diff --git a/beacon-chain/blockchain/weak_subjectivity_checks.go b/beacon-chain/blockchain/weak_subjectivity_checks.go index d87a1e964..b92ecef01 100644 --- a/beacon-chain/blockchain/weak_subjectivity_checks.go +++ b/beacon-chain/blockchain/weak_subjectivity_checks.go @@ -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 } diff --git a/beacon-chain/blockchain/weak_subjectivity_checks_test.go b/beacon-chain/blockchain/weak_subjectivity_checks_test.go index 0df55b470..493931dbc 100644 --- a/beacon-chain/blockchain/weak_subjectivity_checks_test.go +++ b/beacon-chain/blockchain/weak_subjectivity_checks_test.go @@ -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: ðpb.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: ðpb.Checkpoint{Epoch: 2}, - finalizedEpoch: 1, + checkpt: ðpb.Checkpoint{Root: bytesutil.PadTo([]byte{'a'}, 32), Epoch: blockEpoch}, + finalizedEpoch: blockEpoch - 1, }, { name: "can't find the block in DB", checkpt: ðpb.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: ðpb.Checkpoint{Root: r[:], Epoch: 2}, // Root belongs in epoch 1. - finalizedEpoch: 3, + checkpt: ðpb.Checkpoint{Root: r[:], Epoch: blockEpoch - 2}, // Root belongs in epoch 1. + finalizedEpoch: blockEpoch - 1, wantErr: errWSBlockNotFoundInEpoch, }, { name: "can verify and pass", - checkpt: ðpb.Checkpoint{Root: r[:], Epoch: 1}, - finalizedEpoch: 3, + checkpt: ðpb.Checkpoint{Root: r[:], Epoch: blockEpoch}, + finalizedEpoch: blockEpoch + 1, + }, + { + name: "equal epoch", + checkpt: ðpb.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}, diff --git a/beacon-chain/db/errors.go b/beacon-chain/db/errors.go index 626060b2c..1b632b207 100644 --- a/beacon-chain/db/errors.go +++ b/beacon-chain/db/errors.go @@ -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 diff --git a/beacon-chain/db/iface/interface.go b/beacon-chain/db/iface/interface.go index 214e24f0e..de54f276a 100644 --- a/beacon-chain/db/iface/interface.go +++ b/beacon-chain/db/iface/interface.go @@ -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. diff --git a/beacon-chain/db/kv/blocks.go b/beacon-chain/db/kv/blocks.go index 5678efd15..d6d4499da 100644 --- a/beacon-chain/db/kv/blocks.go +++ b/beacon-chain/db/kv/blocks.go @@ -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[:]) }) } diff --git a/beacon-chain/db/kv/blocks_test.go b/beacon-chain/db/kv/blocks_test.go index 7fa53f63f..5d7583f20 100644 --- a/beacon-chain/db/kv/blocks_test.go +++ b/beacon-chain/db/kv/blocks_test.go @@ -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) diff --git a/beacon-chain/db/kv/error.go b/beacon-chain/db/kv/error.go index dac69d44a..7add6c66d 100644 --- a/beacon-chain/db/kv/error.go +++ b/beacon-chain/db/kv/error.go @@ -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") diff --git a/beacon-chain/db/kv/finalized_block_roots.go b/beacon-chain/db/kv/finalized_block_roots.go index 04dc89e0c..90a17024b 100644 --- a/beacon-chain/db/kv/finalized_block_roots.go +++ b/beacon-chain/db/kv/finalized_block_roots.go @@ -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 := ðpb.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 } diff --git a/beacon-chain/db/kv/schema.go b/beacon-chain/db/kv/schema.go index 8b276abb7..187d82d9b 100644 --- a/beacon-chain/db/kv/schema.go +++ b/beacon-chain/db/kv/schema.go @@ -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") diff --git a/beacon-chain/db/kv/wss.go b/beacon-chain/db/kv/wss.go index 228046811..6d860c2bc 100644 --- a/beacon-chain/db/kv/wss.go +++ b/beacon-chain/db/kv/wss.go @@ -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, ðpb.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 } diff --git a/beacon-chain/node/BUILD.bazel b/beacon-chain/node/BUILD.bazel index b55886168..96d307b84 100644 --- a/beacon-chain/node/BUILD.bazel +++ b/beacon-chain/node/BUILD.bazel @@ -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", diff --git a/beacon-chain/node/node.go b/beacon-chain/node/node.go index 298548c57..e30b5d30e 100644 --- a/beacon-chain/node/node.go +++ b/beacon-chain/node/node.go @@ -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 } diff --git a/beacon-chain/rpc/eth/helpers/error_handling.go b/beacon-chain/rpc/eth/helpers/error_handling.go index a41a35280..92bc40813 100644 --- a/beacon-chain/rpc/eth/helpers/error_handling.go +++ b/beacon-chain/rpc/eth/helpers/error_handling.go @@ -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 { diff --git a/beacon-chain/state/stategen/BUILD.bazel b/beacon-chain/state/stategen/BUILD.bazel index 571158336..aafaedcc2 100644 --- a/beacon-chain/state/stategen/BUILD.bazel +++ b/beacon-chain/state/stategen/BUILD.bazel @@ -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", diff --git a/beacon-chain/state/stategen/getter.go b/beacon-chain/state/stategen/getter.go index a4af3f9fc..16ffef396 100644 --- a/beacon-chain/state/stategen/getter.go +++ b/beacon-chain/state/stategen/getter.go @@ -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) +} diff --git a/beacon-chain/state/stategen/service.go b/beacon-chain/state/stategen/service.go index 654be19f6..a3f6f31b2 100644 --- a/beacon-chain/state/stategen/service.go +++ b/beacon-chain/state/stategen/service.go @@ -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 -} diff --git a/beacon-chain/sync/backfill/BUILD.bazel b/beacon-chain/sync/backfill/BUILD.bazel new file mode 100644 index 000000000..1fe721188 --- /dev/null +++ b/beacon-chain/sync/backfill/BUILD.bazel @@ -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", + ], +) diff --git a/beacon-chain/sync/backfill/status.go b/beacon-chain/sync/backfill/status.go new file mode 100644 index 000000000..8eaacdc99 --- /dev/null +++ b/beacon-chain/sync/backfill/status.go @@ -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) +} diff --git a/beacon-chain/sync/backfill/status_test.go b/beacon-chain/sync/backfill/status_test.go new file mode 100644 index 000000000..e438f13d6 --- /dev/null +++ b/beacon-chain/sync/backfill/status_test.go @@ -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) + } +} diff --git a/beacon-chain/sync/checkpoint/BUILD.bazel b/beacon-chain/sync/checkpoint/BUILD.bazel new file mode 100644 index 000000000..5672e362e --- /dev/null +++ b/beacon-chain/sync/checkpoint/BUILD.bazel @@ -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", + ], +) diff --git a/beacon-chain/sync/checkpoint/api.go b/beacon-chain/sync/checkpoint/api.go new file mode 100644 index 000000000..fb678c0bc --- /dev/null +++ b/beacon-chain/sync/checkpoint/api.go @@ -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()) +} diff --git a/beacon-chain/sync/checkpoint/file.go b/beacon-chain/sync/checkpoint/file.go new file mode 100644 index 000000000..346a0142a --- /dev/null +++ b/beacon-chain/sync/checkpoint/file.go @@ -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 +} diff --git a/cmd/beacon-chain/BUILD.bazel b/cmd/beacon-chain/BUILD.bazel index ee374d88a..f44a85971 100644 --- a/cmd/beacon-chain/BUILD.bazel +++ b/cmd/beacon-chain/BUILD.bazel @@ -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", diff --git a/cmd/beacon-chain/blockchain/options.go b/cmd/beacon-chain/blockchain/options.go index 1b367e2a6..30ca4767b 100644 --- a/cmd/beacon-chain/blockchain/options.go +++ b/cmd/beacon-chain/blockchain/options.go @@ -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 diff --git a/cmd/beacon-chain/flags/base.go b/cmd/beacon-chain/flags/base.go index bae11f76e..180160bd5 100644 --- a/cmd/beacon-chain/flags/base.go +++ b/cmd/beacon-chain/flags/base.go @@ -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", diff --git a/cmd/beacon-chain/main.go b/cmd/beacon-chain/main.go index f85c018fd..c9fb465c5 100644 --- a/cmd/beacon-chain/main.go +++ b/cmd/beacon-chain/main.go @@ -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 diff --git a/cmd/beacon-chain/sync/checkpoint/BUILD.bazel b/cmd/beacon-chain/sync/checkpoint/BUILD.bazel new file mode 100644 index 000000000..06da1f480 --- /dev/null +++ b/cmd/beacon-chain/sync/checkpoint/BUILD.bazel @@ -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", + ], +) diff --git a/cmd/beacon-chain/sync/checkpoint/options.go b/cmd/beacon-chain/sync/checkpoint/options.go new file mode 100644 index 000000000..4a453e3d2 --- /dev/null +++ b/cmd/beacon-chain/sync/checkpoint/options.go @@ -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 +} diff --git a/cmd/beacon-chain/usage.go b/cmd/beacon-chain/usage.go index c3e94aca9..4095e470b 100644 --- a/cmd/beacon-chain/usage.go +++ b/cmd/beacon-chain/usage.go @@ -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, diff --git a/cmd/wrap_flags.go b/cmd/wrap_flags.go index 166cd8f45..039df63cc 100644 --- a/cmd/wrap_flags.go +++ b/cmd/wrap_flags.go @@ -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))