Skip to content

Commit e033909

Browse files
potuznisdas
authored andcommitted
Add feature flag to start from any beacon block in db (#15000)
* Add feature flag to start from any beacon block in db The new feature flag called --sync-from takes a string that can take values: - `head` or - a 0x-prefixed hex encoded beacon block root. The beacon block root or the head block root has to be known in db and has to be a descendant of the current justified checkpoint. * Fix Bugs In Sync From Head (#15006) * Fix Bugs * Remove log * missing save * add tests * Kasey review #1 * Kasey's review #2 * Kasey's review #3 --------- Co-authored-by: Nishant Das <[email protected]>
1 parent f9236f0 commit e033909

File tree

14 files changed

+301
-19
lines changed

14 files changed

+301
-19
lines changed

beacon-chain/blockchain/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ go_test(
128128
"receive_block_test.go",
129129
"service_norace_test.go",
130130
"service_test.go",
131+
"setup_forkchoice_test.go",
131132
"setup_test.go",
132133
"weak_subjectivity_checks_test.go",
133134
],

beacon-chain/blockchain/service.go

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import (
3939
ethpb "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1"
4040
prysmTime "github.com/prysmaticlabs/prysm/v5/time"
4141
"github.com/prysmaticlabs/prysm/v5/time/slots"
42+
"github.com/sirupsen/logrus"
4243
)
4344

4445
// Service represents a service that handles the internal
@@ -316,32 +317,36 @@ func (s *Service) originRootFromSavedState(ctx context.Context) ([32]byte, error
316317
return genesisBlkRoot, nil
317318
}
318319

319-
// initializeHeadFromDB uses the finalized checkpoint and head block found in the database to set the current head.
320+
// initializeHeadFromDB uses the finalized checkpoint and head block root from forkchoice to set the current head.
320321
// Note that this may block until stategen replays blocks between the finalized and head blocks
321322
// if the head sync flag was specified and the gap between the finalized and head blocks is at least 128 epochs long.
322-
func (s *Service) initializeHeadFromDB(ctx context.Context, finalizedState state.BeaconState) error {
323+
func (s *Service) initializeHead(ctx context.Context, st state.BeaconState) error {
323324
cp := s.FinalizedCheckpt()
324-
fRoot := [32]byte(cp.Root)
325-
finalizedRoot := s.ensureRootNotZeros(fRoot)
326-
327-
if finalizedState == nil || finalizedState.IsNil() {
325+
fRoot := s.ensureRootNotZeros([32]byte(cp.Root))
326+
if st == nil || st.IsNil() {
328327
return errors.New("finalized state can't be nil")
329328
}
330329

331-
finalizedBlock, err := s.getBlock(ctx, finalizedRoot)
330+
s.cfg.ForkChoiceStore.RLock()
331+
root := s.cfg.ForkChoiceStore.HighestReceivedBlockRoot()
332+
s.cfg.ForkChoiceStore.RUnlock()
333+
blk, err := s.cfg.BeaconDB.Block(ctx, root)
332334
if err != nil {
333-
return errors.Wrap(err, "could not get finalized block")
335+
return errors.Wrap(err, "could not get head block")
334336
}
335-
if err := s.setHead(&head{
336-
finalizedRoot,
337-
finalizedBlock,
338-
finalizedState,
339-
finalizedBlock.Block().Slot(),
340-
false,
341-
}); err != nil {
337+
if root != fRoot {
338+
st, err = s.cfg.StateGen.StateByRoot(ctx, root)
339+
if err != nil {
340+
return errors.Wrap(err, "could not get head state")
341+
}
342+
}
343+
if err := s.setHead(&head{root, blk, st, blk.Block().Slot(), false}); err != nil {
342344
return errors.Wrap(err, "could not set head")
343345
}
344-
346+
log.WithFields(logrus.Fields{
347+
"root": fmt.Sprintf("%#x", root),
348+
"slot": blk.Block().Slot(),
349+
}).Info("Initialized head block from DB")
345350
return nil
346351
}
347352

beacon-chain/blockchain/setup_forchoice.go

Lines changed: 93 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,119 @@ package blockchain
22

33
import (
44
"bytes"
5+
"context"
6+
"fmt"
57

68
"github.com/pkg/errors"
79
forkchoicetypes "github.com/prysmaticlabs/prysm/v5/beacon-chain/forkchoice/types"
810
"github.com/prysmaticlabs/prysm/v5/beacon-chain/state"
911
"github.com/prysmaticlabs/prysm/v5/config/features"
1012
"github.com/prysmaticlabs/prysm/v5/consensus-types/blocks"
13+
"github.com/prysmaticlabs/prysm/v5/consensus-types/interfaces"
1114
"github.com/prysmaticlabs/prysm/v5/encoding/bytesutil"
15+
"github.com/prysmaticlabs/prysm/v5/time/slots"
1216
)
1317

1418
func (s *Service) setupForkchoice(st state.BeaconState) error {
1519
if err := s.setupForkchoiceCheckpoints(); err != nil {
1620
return errors.Wrap(err, "could not set up forkchoice checkpoints")
1721
}
18-
if err := s.setupForkchoiceRoot(st); err != nil {
22+
if err := s.setupForkchoiceTree(st); err != nil {
1923
return errors.Wrap(err, "could not set up forkchoice root")
2024
}
21-
if err := s.initializeHeadFromDB(s.ctx, st); err != nil {
25+
if err := s.initializeHead(s.ctx, st); err != nil {
2226
return errors.Wrap(err, "could not initialize head from db")
2327
}
2428
return nil
2529
}
2630

31+
func (s *Service) startupHeadRoot() [32]byte {
32+
headStr := features.Get().ForceHead
33+
cp := s.FinalizedCheckpt()
34+
fRoot := s.ensureRootNotZeros([32]byte(cp.Root))
35+
if headStr == "" {
36+
return fRoot
37+
}
38+
if headStr == "head" {
39+
root, err := s.cfg.BeaconDB.HeadBlockRoot()
40+
if err != nil {
41+
log.WithError(err).Error("could not get head block root, starting with finalized block as head")
42+
return fRoot
43+
}
44+
log.Infof("Using Head root of %#x", root)
45+
return root
46+
}
47+
root, err := bytesutil.DecodeHexWithLength(headStr, 32)
48+
if err != nil {
49+
log.WithError(err).Error("could not parse head root, starting with finalized block as head")
50+
return fRoot
51+
}
52+
return [32]byte(root)
53+
}
54+
55+
func (s *Service) setupForkchoiceTree(st state.BeaconState) error {
56+
headRoot := s.startupHeadRoot()
57+
cp := s.FinalizedCheckpt()
58+
fRoot := s.ensureRootNotZeros([32]byte(cp.Root))
59+
if err := s.setupForkchoiceRoot(st); err != nil {
60+
return errors.Wrap(err, "could not set up forkchoice root")
61+
}
62+
if headRoot == fRoot {
63+
return nil
64+
}
65+
blk, err := s.cfg.BeaconDB.Block(s.ctx, headRoot)
66+
if err != nil {
67+
log.WithError(err).Error("could not get head block, starting with finalized block as head")
68+
return nil
69+
}
70+
if slots.ToEpoch(blk.Block().Slot()) < cp.Epoch {
71+
log.WithField("headRoot", fmt.Sprintf("%#x", headRoot)).Error("head block is older than finalized block, starting with finalized block as head")
72+
return nil
73+
}
74+
chain, err := s.buildForkchoiceChain(s.ctx, blk)
75+
if err != nil {
76+
log.WithError(err).Error("could not build forkchoice chain, starting with finalized block as head")
77+
return nil
78+
}
79+
s.cfg.ForkChoiceStore.Lock()
80+
defer s.cfg.ForkChoiceStore.Unlock()
81+
return s.cfg.ForkChoiceStore.InsertChain(s.ctx, chain)
82+
}
83+
84+
func (s *Service) buildForkchoiceChain(ctx context.Context, head interfaces.ReadOnlySignedBeaconBlock) ([]*forkchoicetypes.BlockAndCheckpoints, error) {
85+
chain := []*forkchoicetypes.BlockAndCheckpoints{}
86+
cp := s.FinalizedCheckpt()
87+
fRoot := s.ensureRootNotZeros([32]byte(cp.Root))
88+
jp := s.CurrentJustifiedCheckpt()
89+
root, err := head.Block().HashTreeRoot()
90+
if err != nil {
91+
return nil, errors.Wrap(err, "could not get head block root")
92+
}
93+
for {
94+
roblock, err := blocks.NewROBlockWithRoot(head, root)
95+
if err != nil {
96+
return nil, err
97+
}
98+
// This chain sets the justified checkpoint for every block, including some that are older than jp.
99+
// This should be however safe for forkchoice at startup. An alternative would be to hook during the
100+
// block processing pipeline when setting the head state, to compute the right states for the justified
101+
// checkpoint.
102+
chain = append(chain, &forkchoicetypes.BlockAndCheckpoints{Block: roblock, JustifiedCheckpoint: jp, FinalizedCheckpoint: cp})
103+
root = head.Block().ParentRoot()
104+
if root == fRoot {
105+
break
106+
}
107+
head, err = s.cfg.BeaconDB.Block(s.ctx, root)
108+
if err != nil {
109+
return nil, errors.Wrap(err, "could not get block")
110+
}
111+
if slots.ToEpoch(head.Block().Slot()) < cp.Epoch {
112+
return nil, errors.New("head block is not a descendant of the finalized checkpoint")
113+
}
114+
}
115+
return chain, nil
116+
}
117+
27118
func (s *Service) setupForkchoiceRoot(st state.BeaconState) error {
28119
cp := s.FinalizedCheckpt()
29120
fRoot := s.ensureRootNotZeros([32]byte(cp.Root))
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
package blockchain
2+
3+
import (
4+
"testing"
5+
6+
"github.com/prysmaticlabs/prysm/v5/beacon-chain/core/blocks"
7+
"github.com/prysmaticlabs/prysm/v5/config/features"
8+
"github.com/prysmaticlabs/prysm/v5/config/params"
9+
consensusblocks "github.com/prysmaticlabs/prysm/v5/consensus-types/blocks"
10+
"github.com/prysmaticlabs/prysm/v5/consensus-types/primitives"
11+
ethpb "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1"
12+
"github.com/prysmaticlabs/prysm/v5/testing/require"
13+
"github.com/prysmaticlabs/prysm/v5/testing/util"
14+
logTest "github.com/sirupsen/logrus/hooks/test"
15+
)
16+
17+
func Test_startupHeadRoot(t *testing.T) {
18+
service, tr := minimalTestService(t)
19+
ctx := tr.ctx
20+
hook := logTest.NewGlobal()
21+
cp := service.FinalizedCheckpt()
22+
require.DeepEqual(t, cp.Root, params.BeaconConfig().ZeroHash[:])
23+
gr := [32]byte{'r', 'o', 'o', 't'}
24+
service.originBlockRoot = gr
25+
require.NoError(t, service.cfg.BeaconDB.SaveGenesisBlockRoot(ctx, gr))
26+
t.Run("start from finalized", func(t *testing.T) {
27+
require.Equal(t, service.startupHeadRoot(), gr)
28+
})
29+
t.Run("head requested, error path", func(t *testing.T) {
30+
resetCfg := features.InitWithReset(&features.Flags{
31+
ForceHead: "head",
32+
})
33+
defer resetCfg()
34+
require.Equal(t, service.startupHeadRoot(), gr)
35+
require.LogsContain(t, hook, "could not get head block root, starting with finalized block as head")
36+
})
37+
38+
st, _ := util.DeterministicGenesisState(t, 64)
39+
hr := [32]byte{'h', 'e', 'a', 'd'}
40+
require.NoError(t, service.cfg.BeaconDB.SaveState(ctx, st, hr), "Could not save genesis state")
41+
require.NoError(t, service.cfg.BeaconDB.SaveHeadBlockRoot(ctx, hr), "Could not save genesis state")
42+
require.NoError(t, service.cfg.BeaconDB.SaveHeadBlockRoot(ctx, hr))
43+
44+
t.Run("start from head", func(t *testing.T) {
45+
resetCfg := features.InitWithReset(&features.Flags{
46+
ForceHead: "head",
47+
})
48+
defer resetCfg()
49+
require.Equal(t, service.startupHeadRoot(), hr)
50+
})
51+
}
52+
53+
func Test_setupForkchoiceTree_Finalized(t *testing.T) {
54+
service, tr := minimalTestService(t)
55+
ctx := tr.ctx
56+
57+
st, _ := util.DeterministicGenesisState(t, 64)
58+
stateRoot, err := st.HashTreeRoot(ctx)
59+
require.NoError(t, err, "Could not hash genesis state")
60+
61+
require.NoError(t, service.saveGenesisData(ctx, st))
62+
63+
genesis := blocks.NewGenesisBlock(stateRoot[:])
64+
wsb, err := consensusblocks.NewSignedBeaconBlock(genesis)
65+
require.NoError(t, err)
66+
require.NoError(t, service.cfg.BeaconDB.SaveBlock(ctx, wsb), "Could not save genesis block")
67+
parentRoot, err := genesis.Block.HashTreeRoot()
68+
require.NoError(t, err, "Could not get signing root")
69+
require.NoError(t, service.cfg.BeaconDB.SaveState(ctx, st, parentRoot), "Could not save genesis state")
70+
require.NoError(t, service.cfg.BeaconDB.SaveHeadBlockRoot(ctx, parentRoot), "Could not save genesis state")
71+
require.NoError(t, service.cfg.BeaconDB.SaveJustifiedCheckpoint(ctx, &ethpb.Checkpoint{Root: parentRoot[:]}))
72+
require.NoError(t, service.cfg.BeaconDB.SaveFinalizedCheckpoint(ctx, &ethpb.Checkpoint{Root: parentRoot[:]}))
73+
require.NoError(t, service.setupForkchoiceTree(st))
74+
require.Equal(t, 1, service.cfg.ForkChoiceStore.NodeCount())
75+
}
76+
77+
func Test_setupForkchoiceTree_Head(t *testing.T) {
78+
service, tr := minimalTestService(t)
79+
ctx := tr.ctx
80+
resetCfg := features.InitWithReset(&features.Flags{
81+
ForceHead: "head",
82+
})
83+
defer resetCfg()
84+
85+
genesisState, keys := util.DeterministicGenesisState(t, 64)
86+
stateRoot, err := genesisState.HashTreeRoot(ctx)
87+
require.NoError(t, err, "Could not hash genesis state")
88+
genesis := blocks.NewGenesisBlock(stateRoot[:])
89+
wsb, err := consensusblocks.NewSignedBeaconBlock(genesis)
90+
require.NoError(t, err)
91+
genesisRoot, err := genesis.Block.HashTreeRoot()
92+
require.NoError(t, err, "Could not get signing root")
93+
require.NoError(t, service.cfg.BeaconDB.SaveBlock(ctx, wsb), "Could not save genesis block")
94+
require.NoError(t, service.saveGenesisData(ctx, genesisState))
95+
96+
require.NoError(t, service.cfg.BeaconDB.SaveState(ctx, genesisState, genesisRoot), "Could not save genesis state")
97+
require.NoError(t, service.cfg.BeaconDB.SaveHeadBlockRoot(ctx, genesisRoot), "Could not save genesis state")
98+
99+
st, err := service.HeadState(ctx)
100+
require.NoError(t, err)
101+
b, err := util.GenerateFullBlock(st, keys, util.DefaultBlockGenConfig(), primitives.Slot(1))
102+
require.NoError(t, err)
103+
wsb, err = consensusblocks.NewSignedBeaconBlock(b)
104+
require.NoError(t, err)
105+
root, err := b.Block.HashTreeRoot()
106+
require.NoError(t, err)
107+
preState, err := service.getBlockPreState(ctx, wsb.Block())
108+
require.NoError(t, err)
109+
postState, err := service.validateStateTransition(ctx, preState, wsb)
110+
require.NoError(t, err)
111+
require.NoError(t, service.savePostStateInfo(ctx, root, wsb, postState))
112+
113+
b, err = util.GenerateFullBlock(postState, keys, util.DefaultBlockGenConfig(), primitives.Slot(2))
114+
require.NoError(t, err)
115+
wsb, err = consensusblocks.NewSignedBeaconBlock(b)
116+
require.NoError(t, err)
117+
root, err = b.Block.HashTreeRoot()
118+
require.NoError(t, err)
119+
require.NoError(t, service.savePostStateInfo(ctx, root, wsb, preState))
120+
121+
require.NoError(t, service.cfg.BeaconDB.SaveHeadBlockRoot(ctx, root))
122+
cp := service.FinalizedCheckpt()
123+
fRoot := service.ensureRootNotZeros([32]byte(cp.Root))
124+
require.NotEqual(t, fRoot, root)
125+
require.Equal(t, root, service.startupHeadRoot())
126+
require.NoError(t, service.setupForkchoiceTree(st))
127+
require.Equal(t, 2, service.cfg.ForkChoiceStore.NodeCount())
128+
}

beacon-chain/db/iface/interface.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ type HeadAccessDatabase interface {
110110

111111
// Block related methods.
112112
HeadBlock(ctx context.Context) (interfaces.ReadOnlySignedBeaconBlock, error)
113+
HeadBlockRoot() ([32]byte, error)
113114
SaveHeadBlockRoot(ctx context.Context, blockRoot [32]byte) error
114115

115116
// Genesis operations.

beacon-chain/db/kv/blocks.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,21 @@ func (s *Store) OriginCheckpointBlockRoot(ctx context.Context) ([32]byte, error)
8080
return root, err
8181
}
8282

83+
// HeadBlockRoot returns the latest canonical block root in the Ethereum Beacon Chain.
84+
func (s *Store) HeadBlockRoot() ([32]byte, error) {
85+
var root [32]byte
86+
err := s.db.View(func(tx *bolt.Tx) error {
87+
bkt := tx.Bucket(blocksBucket)
88+
headRoot := bkt.Get(headBlockRootKey)
89+
if len(headRoot) == 0 {
90+
return errors.New("no head block root found")
91+
}
92+
copy(root[:], headRoot)
93+
return nil
94+
})
95+
return root, err
96+
}
97+
8398
// HeadBlock returns the latest canonical block in the Ethereum Beacon Chain.
8499
func (s *Store) HeadBlock(ctx context.Context) (interfaces.ReadOnlySignedBeaconBlock, error) {
85100
ctx, span := trace.StartSpan(ctx, "BeaconDB.HeadBlock")

beacon-chain/forkchoice/doubly-linked-tree/store.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,13 @@ func (s *Store) tips() ([][32]byte, []primitives.Slot) {
252252
return roots, slots
253253
}
254254

255+
func (f *ForkChoice) HighestReceivedBlockRoot() [32]byte {
256+
if f.store.highestReceivedNode == nil {
257+
return [32]byte{}
258+
}
259+
return f.store.highestReceivedNode.root
260+
}
261+
255262
// HighestReceivedBlockSlot returns the highest slot received by the forkchoice
256263
func (f *ForkChoice) HighestReceivedBlockSlot() primitives.Slot {
257264
if f.store.highestReceivedNode == nil {

beacon-chain/forkchoice/interfaces.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ type FastGetter interface {
6565
FinalizedPayloadBlockHash() [32]byte
6666
HasNode([32]byte) bool
6767
HighestReceivedBlockSlot() primitives.Slot
68+
HighestReceivedBlockRoot() [32]byte
6869
HighestReceivedBlockDelay() primitives.Slot
6970
IsCanonical(root [32]byte) bool
7071
IsOptimistic(root [32]byte) (bool, error)

beacon-chain/forkchoice/ro.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,13 @@ func (ro *ROForkChoice) HighestReceivedBlockSlot() primitives.Slot {
114114
return ro.getter.HighestReceivedBlockSlot()
115115
}
116116

117+
// HighestReceivedBlockRoot delegates to the underlying forkchoice call, under a lock.
118+
func (ro *ROForkChoice) HighestReceivedBlockRoot() [32]byte {
119+
ro.l.RLock()
120+
defer ro.l.RUnlock()
121+
return ro.getter.HighestReceivedBlockRoot()
122+
}
123+
117124
// HighestReceivedBlockDelay delegates to the underlying forkchoice call, under a lock.
118125
func (ro *ROForkChoice) HighestReceivedBlockDelay() primitives.Slot {
119126
ro.l.RLock()

0 commit comments

Comments
 (0)