diff --git a/beacon-chain/blockchain/chain_info.go b/beacon-chain/blockchain/chain_info.go index 1c8e38c055e7..1e4558ee49cc 100644 --- a/beacon-chain/blockchain/chain_info.go +++ b/beacon-chain/blockchain/chain_info.go @@ -1,6 +1,7 @@ package blockchain import ( + "bytes" "context" "time" @@ -299,13 +300,14 @@ func (s *Service) ForkChoicer() forkchoice.ForkChoicer { // IsOptimistic returns true if the current head is optimistic. func (s *Service) IsOptimistic(ctx context.Context) (bool, error) { - s.headLock.RLock() - defer s.headLock.RUnlock() if slots.ToEpoch(s.CurrentSlot()) < params.BeaconConfig().BellatrixForkEpoch { return false, nil } + s.headLock.RLock() + headRoot := s.head.root + s.headLock.RUnlock() - return s.IsOptimisticForRoot(ctx, s.head.root) + return s.IsOptimisticForRoot(ctx, headRoot) } // IsFinalized returns true if the input root is finalized. @@ -332,7 +334,17 @@ func (s *Service) IsOptimisticForRoot(ctx context.Context, root [32]byte) (bool, return false, err } if ss == nil { - return false, errInvalidNilSummary + // if the requested root is the headroot we should treat the + // node as optimistic. This can happen if we pruned INVALID + // nodes and no viable head is available. + headRoot, err := s.HeadRoot(ctx) + if err != nil { + return true, err + } + if bytes.Equal(headRoot, root[:]) { + return true, nil + } + return true, errInvalidNilSummary } validatedCheckpoint, err := s.cfg.BeaconDB.LastValidatedCheckpoint(ctx) diff --git a/beacon-chain/blockchain/execution_engine.go b/beacon-chain/blockchain/execution_engine.go index 950520a80fea..e269071d2ec7 100644 --- a/beacon-chain/blockchain/execution_engine.go +++ b/beacon-chain/blockchain/execution_engine.go @@ -106,8 +106,12 @@ func (s *Service) notifyForkchoiceUpdate(ctx context.Context, arg *notifyForkcho r, err := s.cfg.ForkChoiceStore.Head(ctx, s.justifiedBalances.balances) if err != nil { - log.WithError(err).Error("Could not get head root") - return nil, nil + log.WithFields(logrus.Fields{ + "slot": headBlk.Slot(), + "blockRoot": fmt.Sprintf("%#x", bytesutil.Trunc(headRoot[:])), + "invalidCount": len(invalidRoots), + }).Warn("Pruned invalid blocks, could not update head root") + return nil, invalidBlock{error: ErrInvalidPayload, root: arg.headRoot, invalidAncestorRoots: invalidRoots} } b, err := s.getBlock(ctx, r) if err != nil { diff --git a/beacon-chain/blockchain/process_block_test.go b/beacon-chain/blockchain/process_block_test.go index 069c1069d6d5..53160938c289 100644 --- a/beacon-chain/blockchain/process_block_test.go +++ b/beacon-chain/blockchain/process_block_test.go @@ -20,6 +20,7 @@ import ( "github.com/prysmaticlabs/prysm/beacon-chain/core/transition" "github.com/prysmaticlabs/prysm/beacon-chain/db" testDB "github.com/prysmaticlabs/prysm/beacon-chain/db/testing" + "github.com/prysmaticlabs/prysm/beacon-chain/execution" mockExecution "github.com/prysmaticlabs/prysm/beacon-chain/execution/testing" doublylinkedtree "github.com/prysmaticlabs/prysm/beacon-chain/forkchoice/doubly-linked-tree" "github.com/prysmaticlabs/prysm/beacon-chain/forkchoice/protoarray" @@ -1800,3 +1801,1058 @@ func Test_verifyBlkFinalizedSlot_invalidBlock(t *testing.T) { err = service.verifyBlkFinalizedSlot(wb) require.Equal(t, true, IsInvalidBlock(err)) } + +// See the description in #10777 and #10782 for the full setup +// We sync optimistically a chain of blocks. Block 17 is the last block in Epoch +// 2. Block 18 justifies block 12 (the first in Epoch 2) and Block 19 returns +// INVALID from FCU, with LVH block 17. No head is viable. We check +// that the node is optimistic and that we can actually import a block on top of +// 17 and recover. +func TestStore_NoViableHead_FCU_Protoarray(t *testing.T) { + params.SetupTestConfigCleanup(t) + config := params.BeaconConfig() + config.SlotsPerEpoch = 6 + config.AltairForkEpoch = 1 + config.BellatrixForkEpoch = 2 + config.SafeSlotsToImportOptimistically = 0 + params.OverrideBeaconConfig(config) + + ctx := context.Background() + beaconDB := testDB.SetupDB(t) + + mockEngine := &mockExecution.EngineClient{ErrNewPayload: execution.ErrAcceptedSyncingPayloadStatus, ErrForkchoiceUpdated: execution.ErrAcceptedSyncingPayloadStatus} + opts := []Option{ + WithDatabase(beaconDB), + WithAttestationPool(attestations.NewPool()), + WithStateGen(stategen.New(beaconDB)), + WithForkChoiceStore(protoarray.New()), + WithStateNotifier(&mock.MockStateNotifier{}), + WithExecutionEngineCaller(mockEngine), + WithProposerIdsCache(cache.NewProposerPayloadIDsCache()), + } + service, err := NewService(ctx, opts...) + require.NoError(t, err) + + st, keys := util.DeterministicGenesisState(t, 64) + stateRoot, err := st.HashTreeRoot(ctx) + require.NoError(t, err, "Could not hash genesis state") + + require.NoError(t, service.saveGenesisData(ctx, st)) + + genesis := blocks.NewGenesisBlock(stateRoot[:]) + wsb, err := wrapper.WrappedSignedBeaconBlock(genesis) + require.NoError(t, err) + require.NoError(t, service.cfg.BeaconDB.SaveBlock(ctx, wsb), "Could not save genesis block") + + parentRoot, err := genesis.Block.HashTreeRoot() + require.NoError(t, err, "Could not get signing root") + require.NoError(t, service.cfg.BeaconDB.SaveState(ctx, st, parentRoot), "Could not save genesis state") + require.NoError(t, service.cfg.BeaconDB.SaveHeadBlockRoot(ctx, parentRoot), "Could not save genesis state") + + for i := 1; i < 6; i++ { + driftGenesisTime(service, int64(i), 0) + st, err := service.HeadState(ctx) + require.NoError(t, err) + b, err := util.GenerateFullBlock(st, keys, util.DefaultBlockGenConfig(), types.Slot(i)) + require.NoError(t, err) + wsb, err := wrapper.WrappedSignedBeaconBlock(b) + require.NoError(t, err) + root, err := b.Block.HashTreeRoot() + require.NoError(t, err) + require.NoError(t, service.onBlock(ctx, wsb, root)) + } + + for i := 6; i < 12; i++ { + driftGenesisTime(service, int64(i), 0) + st, err := service.HeadState(ctx) + require.NoError(t, err) + b, err := util.GenerateFullBlockAltair(st, keys, util.DefaultBlockGenConfig(), types.Slot(i)) + require.NoError(t, err) + wsb, err := wrapper.WrappedSignedBeaconBlock(b) + require.NoError(t, err) + root, err := b.Block.HashTreeRoot() + require.NoError(t, err) + err = service.onBlock(ctx, wsb, root) + require.NoError(t, err) + } + + for i := 12; i < 18; i++ { + driftGenesisTime(service, int64(i), 0) + st, err := service.HeadState(ctx) + require.NoError(t, err) + b, err := util.GenerateFullBlockBellatrix(st, keys, util.DefaultBlockGenConfig(), types.Slot(i)) + require.NoError(t, err) + wsb, err := wrapper.WrappedSignedBeaconBlock(b) + require.NoError(t, err) + root, err := b.Block.HashTreeRoot() + require.NoError(t, err) + err = service.onBlock(ctx, wsb, root) + require.NoError(t, err) + } + // Check that we haven't justified the second epoch yet + jc := service.ForkChoicer().JustifiedCheckpoint() + require.Equal(t, types.Epoch(0), jc.Epoch) + + // import a block that justifies the second epoch + driftGenesisTime(service, 18, 0) + validHeadState, err := service.HeadState(ctx) + require.NoError(t, err) + b, err := util.GenerateFullBlockBellatrix(validHeadState, keys, util.DefaultBlockGenConfig(), 18) + require.NoError(t, err) + wsb, err = wrapper.WrappedSignedBeaconBlock(b) + require.NoError(t, err) + firstInvalidRoot, err := b.Block.HashTreeRoot() + require.NoError(t, err) + err = service.onBlock(ctx, wsb, firstInvalidRoot) + require.NoError(t, err) + jc = service.ForkChoicer().JustifiedCheckpoint() + require.Equal(t, types.Epoch(2), jc.Epoch) + + sjc := validHeadState.CurrentJustifiedCheckpoint() + require.Equal(t, types.Epoch(0), sjc.Epoch) + lvh := b.Block.Body.ExecutionPayload.ParentHash + // check our head + require.Equal(t, firstInvalidRoot, service.ForkChoicer().CachedHeadRoot()) + + // import another block to find out that it was invalid + mockEngine = &mockExecution.EngineClient{ErrNewPayload: execution.ErrAcceptedSyncingPayloadStatus, ErrForkchoiceUpdated: execution.ErrInvalidPayloadStatus, ForkChoiceUpdatedResp: lvh} + service.cfg.ExecutionEngineCaller = mockEngine + driftGenesisTime(service, 19, 0) + st, err = service.HeadState(ctx) + require.NoError(t, err) + b, err = util.GenerateFullBlockBellatrix(st, keys, util.DefaultBlockGenConfig(), 19) + require.NoError(t, err) + wsb, err = wrapper.WrappedSignedBeaconBlock(b) + require.NoError(t, err) + root, err := b.Block.HashTreeRoot() + require.NoError(t, err) + err = service.onBlock(ctx, wsb, root) + require.ErrorContains(t, "received an INVALID payload from execution engine", err) + // Check that forkchoice's head is the last invalid block imported. The + // store's headroot is the previous head (since the invalid block did + // not finish importing) one and that the node is optimistic + require.Equal(t, root, service.ForkChoicer().CachedHeadRoot()) + headRoot, err := service.HeadRoot(ctx) + require.NoError(t, err) + require.Equal(t, firstInvalidRoot, bytesutil.ToBytes32(headRoot)) + optimistic, err := service.IsOptimistic(ctx) + require.NoError(t, err) + require.Equal(t, true, optimistic) + + // import another block based on the last valid head state + mockEngine = &mockExecution.EngineClient{} + service.cfg.ExecutionEngineCaller = mockEngine + driftGenesisTime(service, 20, 0) + b, err = util.GenerateFullBlockBellatrix(validHeadState, keys, &util.BlockGenConfig{}, 20) + require.NoError(t, err) + wsb, err = wrapper.WrappedSignedBeaconBlock(b) + require.NoError(t, err) + root, err = b.Block.HashTreeRoot() + require.NoError(t, err) + err = service.onBlock(ctx, wsb, root) + require.NoError(t, err) + // Check the newly imported block is head, it justified the right + // checkpoint and the node is no longer optimistic + require.Equal(t, root, service.ForkChoicer().CachedHeadRoot()) + sjc = service.CurrentJustifiedCheckpt() + require.Equal(t, jc.Epoch, sjc.Epoch) + require.Equal(t, jc.Root, bytesutil.ToBytes32(sjc.Root)) + optimistic, err = service.IsOptimistic(ctx) + require.NoError(t, err) + require.Equal(t, false, optimistic) +} + +// See the description in #10777 and #10782 for the full setup +// We sync optimistically a chain of blocks. Block 17 is the last block in Epoch +// 2. Block 18 justifies block 12 (the first in Epoch 2) and Block 19 returns +// INVALID from FCU, with LVH block 17. No head is viable. We check +// that the node is optimistic and that we can actually import a block on top of +// 17 and recover. +func TestStore_NoViableHead_FCU_DoublyLinkedTree(t *testing.T) { + params.SetupTestConfigCleanup(t) + config := params.BeaconConfig() + config.SlotsPerEpoch = 6 + config.AltairForkEpoch = 1 + config.BellatrixForkEpoch = 2 + config.SafeSlotsToImportOptimistically = 0 + params.OverrideBeaconConfig(config) + + ctx := context.Background() + beaconDB := testDB.SetupDB(t) + + mockEngine := &mockExecution.EngineClient{ErrNewPayload: execution.ErrAcceptedSyncingPayloadStatus, ErrForkchoiceUpdated: execution.ErrAcceptedSyncingPayloadStatus} + opts := []Option{ + WithDatabase(beaconDB), + WithAttestationPool(attestations.NewPool()), + WithStateGen(stategen.New(beaconDB)), + WithForkChoiceStore(doublylinkedtree.New()), + WithStateNotifier(&mock.MockStateNotifier{}), + WithExecutionEngineCaller(mockEngine), + WithProposerIdsCache(cache.NewProposerPayloadIDsCache()), + } + service, err := NewService(ctx, opts...) + require.NoError(t, err) + + st, keys := util.DeterministicGenesisState(t, 64) + stateRoot, err := st.HashTreeRoot(ctx) + require.NoError(t, err, "Could not hash genesis state") + + require.NoError(t, service.saveGenesisData(ctx, st)) + + genesis := blocks.NewGenesisBlock(stateRoot[:]) + wsb, err := wrapper.WrappedSignedBeaconBlock(genesis) + require.NoError(t, err) + require.NoError(t, service.cfg.BeaconDB.SaveBlock(ctx, wsb), "Could not save genesis block") + + parentRoot, err := genesis.Block.HashTreeRoot() + require.NoError(t, err, "Could not get signing root") + require.NoError(t, service.cfg.BeaconDB.SaveState(ctx, st, parentRoot), "Could not save genesis state") + require.NoError(t, service.cfg.BeaconDB.SaveHeadBlockRoot(ctx, parentRoot), "Could not save genesis state") + + for i := 1; i < 6; i++ { + driftGenesisTime(service, int64(i), 0) + st, err := service.HeadState(ctx) + require.NoError(t, err) + b, err := util.GenerateFullBlock(st, keys, util.DefaultBlockGenConfig(), types.Slot(i)) + require.NoError(t, err) + wsb, err := wrapper.WrappedSignedBeaconBlock(b) + require.NoError(t, err) + root, err := b.Block.HashTreeRoot() + require.NoError(t, err) + require.NoError(t, service.onBlock(ctx, wsb, root)) + } + + for i := 6; i < 12; i++ { + driftGenesisTime(service, int64(i), 0) + st, err := service.HeadState(ctx) + require.NoError(t, err) + b, err := util.GenerateFullBlockAltair(st, keys, util.DefaultBlockGenConfig(), types.Slot(i)) + require.NoError(t, err) + wsb, err := wrapper.WrappedSignedBeaconBlock(b) + require.NoError(t, err) + root, err := b.Block.HashTreeRoot() + require.NoError(t, err) + err = service.onBlock(ctx, wsb, root) + require.NoError(t, err) + } + + for i := 12; i < 18; i++ { + driftGenesisTime(service, int64(i), 0) + st, err := service.HeadState(ctx) + require.NoError(t, err) + b, err := util.GenerateFullBlockBellatrix(st, keys, util.DefaultBlockGenConfig(), types.Slot(i)) + require.NoError(t, err) + wsb, err := wrapper.WrappedSignedBeaconBlock(b) + require.NoError(t, err) + root, err := b.Block.HashTreeRoot() + require.NoError(t, err) + err = service.onBlock(ctx, wsb, root) + require.NoError(t, err) + } + // Check that we haven't justified the second epoch yet + jc := service.ForkChoicer().JustifiedCheckpoint() + require.Equal(t, types.Epoch(0), jc.Epoch) + + // import a block that justifies the second epoch + driftGenesisTime(service, 18, 0) + validHeadState, err := service.HeadState(ctx) + require.NoError(t, err) + b, err := util.GenerateFullBlockBellatrix(validHeadState, keys, util.DefaultBlockGenConfig(), 18) + require.NoError(t, err) + wsb, err = wrapper.WrappedSignedBeaconBlock(b) + require.NoError(t, err) + firstInvalidRoot, err := b.Block.HashTreeRoot() + require.NoError(t, err) + err = service.onBlock(ctx, wsb, firstInvalidRoot) + require.NoError(t, err) + jc = service.ForkChoicer().JustifiedCheckpoint() + require.Equal(t, types.Epoch(2), jc.Epoch) + + sjc := validHeadState.CurrentJustifiedCheckpoint() + require.Equal(t, types.Epoch(0), sjc.Epoch) + lvh := b.Block.Body.ExecutionPayload.ParentHash + // check our head + require.Equal(t, firstInvalidRoot, service.ForkChoicer().CachedHeadRoot()) + + // import another block to find out that it was invalid + mockEngine = &mockExecution.EngineClient{ErrNewPayload: execution.ErrAcceptedSyncingPayloadStatus, ErrForkchoiceUpdated: execution.ErrInvalidPayloadStatus, ForkChoiceUpdatedResp: lvh} + service.cfg.ExecutionEngineCaller = mockEngine + driftGenesisTime(service, 19, 0) + st, err = service.HeadState(ctx) + require.NoError(t, err) + b, err = util.GenerateFullBlockBellatrix(st, keys, util.DefaultBlockGenConfig(), 19) + require.NoError(t, err) + wsb, err = wrapper.WrappedSignedBeaconBlock(b) + require.NoError(t, err) + root, err := b.Block.HashTreeRoot() + require.NoError(t, err) + err = service.onBlock(ctx, wsb, root) + require.ErrorContains(t, "received an INVALID payload from execution engine", err) + // Check that forkchoice's head is the last invalid block imported. The + // store's headroot is the previous head (since the invalid block did + // not finish importing) one and that the node is optimistic + require.Equal(t, root, service.ForkChoicer().CachedHeadRoot()) + headRoot, err := service.HeadRoot(ctx) + require.NoError(t, err) + require.Equal(t, firstInvalidRoot, bytesutil.ToBytes32(headRoot)) + optimistic, err := service.IsOptimistic(ctx) + require.NoError(t, err) + require.Equal(t, true, optimistic) + + // import another block based on the last valid head state + mockEngine = &mockExecution.EngineClient{} + service.cfg.ExecutionEngineCaller = mockEngine + driftGenesisTime(service, 20, 0) + b, err = util.GenerateFullBlockBellatrix(validHeadState, keys, &util.BlockGenConfig{}, 20) + require.NoError(t, err) + wsb, err = wrapper.WrappedSignedBeaconBlock(b) + require.NoError(t, err) + root, err = b.Block.HashTreeRoot() + require.NoError(t, err) + err = service.onBlock(ctx, wsb, root) + require.NoError(t, err) + // Check the newly imported block is head, it justified the right + // checkpoint and the node is no longer optimistic + require.Equal(t, root, service.ForkChoicer().CachedHeadRoot()) + sjc = service.CurrentJustifiedCheckpt() + require.Equal(t, jc.Epoch, sjc.Epoch) + require.Equal(t, jc.Root, bytesutil.ToBytes32(sjc.Root)) + optimistic, err = service.IsOptimistic(ctx) + require.NoError(t, err) + require.Equal(t, false, optimistic) +} + +// See the description in #10777 and #10782 for the full setup +// We sync optimistically a chain of blocks. Block 17 is the last block in Epoch +// 2. Block 18 justifies block 12 (the first in Epoch 2) and Block 19 returns +// INVALID from NewPayload, with LVH block 17. No head is viable. We check +// that the node is optimistic and that we can actually import a block on top of +// 17 and recover. +func TestStore_NoViableHead_NewPayload_DoublyLinkedTree(t *testing.T) { + params.SetupTestConfigCleanup(t) + config := params.BeaconConfig() + config.SlotsPerEpoch = 6 + config.AltairForkEpoch = 1 + config.BellatrixForkEpoch = 2 + config.SafeSlotsToImportOptimistically = 0 + params.OverrideBeaconConfig(config) + + ctx := context.Background() + beaconDB := testDB.SetupDB(t) + + mockEngine := &mockExecution.EngineClient{ErrNewPayload: execution.ErrAcceptedSyncingPayloadStatus, ErrForkchoiceUpdated: execution.ErrAcceptedSyncingPayloadStatus} + opts := []Option{ + WithDatabase(beaconDB), + WithAttestationPool(attestations.NewPool()), + WithStateGen(stategen.New(beaconDB)), + WithForkChoiceStore(doublylinkedtree.New()), + WithStateNotifier(&mock.MockStateNotifier{}), + WithExecutionEngineCaller(mockEngine), + WithProposerIdsCache(cache.NewProposerPayloadIDsCache()), + } + service, err := NewService(ctx, opts...) + require.NoError(t, err) + + st, keys := util.DeterministicGenesisState(t, 64) + stateRoot, err := st.HashTreeRoot(ctx) + require.NoError(t, err, "Could not hash genesis state") + + require.NoError(t, service.saveGenesisData(ctx, st)) + + genesis := blocks.NewGenesisBlock(stateRoot[:]) + wsb, err := wrapper.WrappedSignedBeaconBlock(genesis) + require.NoError(t, err) + require.NoError(t, service.cfg.BeaconDB.SaveBlock(ctx, wsb), "Could not save genesis block") + + parentRoot, err := genesis.Block.HashTreeRoot() + require.NoError(t, err, "Could not get signing root") + require.NoError(t, service.cfg.BeaconDB.SaveState(ctx, st, parentRoot), "Could not save genesis state") + require.NoError(t, service.cfg.BeaconDB.SaveHeadBlockRoot(ctx, parentRoot), "Could not save genesis state") + + for i := 1; i < 6; i++ { + driftGenesisTime(service, int64(i), 0) + st, err := service.HeadState(ctx) + require.NoError(t, err) + b, err := util.GenerateFullBlock(st, keys, util.DefaultBlockGenConfig(), types.Slot(i)) + require.NoError(t, err) + wsb, err := wrapper.WrappedSignedBeaconBlock(b) + require.NoError(t, err) + root, err := b.Block.HashTreeRoot() + require.NoError(t, err) + require.NoError(t, service.onBlock(ctx, wsb, root)) + } + + for i := 6; i < 12; i++ { + driftGenesisTime(service, int64(i), 0) + st, err := service.HeadState(ctx) + require.NoError(t, err) + b, err := util.GenerateFullBlockAltair(st, keys, util.DefaultBlockGenConfig(), types.Slot(i)) + require.NoError(t, err) + wsb, err := wrapper.WrappedSignedBeaconBlock(b) + require.NoError(t, err) + root, err := b.Block.HashTreeRoot() + require.NoError(t, err) + err = service.onBlock(ctx, wsb, root) + require.NoError(t, err) + } + + for i := 12; i < 18; i++ { + driftGenesisTime(service, int64(i), 0) + st, err := service.HeadState(ctx) + require.NoError(t, err) + b, err := util.GenerateFullBlockBellatrix(st, keys, util.DefaultBlockGenConfig(), types.Slot(i)) + require.NoError(t, err) + wsb, err := wrapper.WrappedSignedBeaconBlock(b) + require.NoError(t, err) + root, err := b.Block.HashTreeRoot() + require.NoError(t, err) + err = service.onBlock(ctx, wsb, root) + require.NoError(t, err) + } + // Check that we haven't justified the second epoch yet + jc := service.ForkChoicer().JustifiedCheckpoint() + require.Equal(t, types.Epoch(0), jc.Epoch) + + // import a block that justifies the second epoch + driftGenesisTime(service, 18, 0) + validHeadState, err := service.HeadState(ctx) + require.NoError(t, err) + b, err := util.GenerateFullBlockBellatrix(validHeadState, keys, util.DefaultBlockGenConfig(), 18) + require.NoError(t, err) + wsb, err = wrapper.WrappedSignedBeaconBlock(b) + require.NoError(t, err) + firstInvalidRoot, err := b.Block.HashTreeRoot() + require.NoError(t, err) + err = service.onBlock(ctx, wsb, firstInvalidRoot) + require.NoError(t, err) + jc = service.ForkChoicer().JustifiedCheckpoint() + require.Equal(t, types.Epoch(2), jc.Epoch) + + sjc := validHeadState.CurrentJustifiedCheckpoint() + require.Equal(t, types.Epoch(0), sjc.Epoch) + lvh := b.Block.Body.ExecutionPayload.ParentHash + // check our head + require.Equal(t, firstInvalidRoot, service.ForkChoicer().CachedHeadRoot()) + + // import another block to find out that it was invalid + mockEngine = &mockExecution.EngineClient{ErrNewPayload: execution.ErrInvalidPayloadStatus, NewPayloadResp: lvh} + service.cfg.ExecutionEngineCaller = mockEngine + driftGenesisTime(service, 19, 0) + st, err = service.HeadState(ctx) + require.NoError(t, err) + b, err = util.GenerateFullBlockBellatrix(st, keys, util.DefaultBlockGenConfig(), 19) + require.NoError(t, err) + wsb, err = wrapper.WrappedSignedBeaconBlock(b) + require.NoError(t, err) + root, err := b.Block.HashTreeRoot() + require.NoError(t, err) + err = service.onBlock(ctx, wsb, root) + require.ErrorContains(t, "received an INVALID payload from execution engine", err) + // Check that forkchoice's head and store's headroot are the previous head (since the invalid block did + // not finish importing and it was never imported to forkchoice). Cehck + // also that the node is optimistic + require.Equal(t, firstInvalidRoot, service.ForkChoicer().CachedHeadRoot()) + headRoot, err := service.HeadRoot(ctx) + require.NoError(t, err) + require.Equal(t, firstInvalidRoot, bytesutil.ToBytes32(headRoot)) + optimistic, err := service.IsOptimistic(ctx) + require.NoError(t, err) + require.Equal(t, true, optimistic) + + // import another block based on the last valid head state + mockEngine = &mockExecution.EngineClient{} + service.cfg.ExecutionEngineCaller = mockEngine + driftGenesisTime(service, 20, 0) + b, err = util.GenerateFullBlockBellatrix(validHeadState, keys, &util.BlockGenConfig{}, 20) + require.NoError(t, err) + wsb, err = wrapper.WrappedSignedBeaconBlock(b) + require.NoError(t, err) + root, err = b.Block.HashTreeRoot() + require.NoError(t, err) + err = service.onBlock(ctx, wsb, root) + require.NoError(t, err) + // Check the newly imported block is head, it justified the right + // checkpoint and the node is no longer optimistic + require.Equal(t, root, service.ForkChoicer().CachedHeadRoot()) + sjc = service.CurrentJustifiedCheckpt() + require.Equal(t, jc.Epoch, sjc.Epoch) + require.Equal(t, jc.Root, bytesutil.ToBytes32(sjc.Root)) + optimistic, err = service.IsOptimistic(ctx) + require.NoError(t, err) + require.Equal(t, false, optimistic) +} + +// See the description in #10777 and #10782 for the full setup +// We sync optimistically a chain of blocks. Block 17 is the last block in Epoch +// 2. Block 18 justifies block 12 (the first in Epoch 2) and Block 19 returns +// INVALID from NewPayload, with LVH block 17. No head is viable. We check +// that the node is optimistic and that we can actually import a block on top of +// 17 and recover. +func TestStore_NoViableHead_NewPayload_Protoarray(t *testing.T) { + params.SetupTestConfigCleanup(t) + config := params.BeaconConfig() + config.SlotsPerEpoch = 6 + config.AltairForkEpoch = 1 + config.BellatrixForkEpoch = 2 + config.SafeSlotsToImportOptimistically = 0 + params.OverrideBeaconConfig(config) + + ctx := context.Background() + beaconDB := testDB.SetupDB(t) + + mockEngine := &mockExecution.EngineClient{ErrNewPayload: execution.ErrAcceptedSyncingPayloadStatus, ErrForkchoiceUpdated: execution.ErrAcceptedSyncingPayloadStatus} + opts := []Option{ + WithDatabase(beaconDB), + WithAttestationPool(attestations.NewPool()), + WithStateGen(stategen.New(beaconDB)), + WithForkChoiceStore(protoarray.New()), + WithStateNotifier(&mock.MockStateNotifier{}), + WithExecutionEngineCaller(mockEngine), + WithProposerIdsCache(cache.NewProposerPayloadIDsCache()), + } + service, err := NewService(ctx, opts...) + require.NoError(t, err) + + st, keys := util.DeterministicGenesisState(t, 64) + stateRoot, err := st.HashTreeRoot(ctx) + require.NoError(t, err, "Could not hash genesis state") + + require.NoError(t, service.saveGenesisData(ctx, st)) + + genesis := blocks.NewGenesisBlock(stateRoot[:]) + wsb, err := wrapper.WrappedSignedBeaconBlock(genesis) + require.NoError(t, err) + require.NoError(t, service.cfg.BeaconDB.SaveBlock(ctx, wsb), "Could not save genesis block") + + parentRoot, err := genesis.Block.HashTreeRoot() + require.NoError(t, err, "Could not get signing root") + require.NoError(t, service.cfg.BeaconDB.SaveState(ctx, st, parentRoot), "Could not save genesis state") + require.NoError(t, service.cfg.BeaconDB.SaveHeadBlockRoot(ctx, parentRoot), "Could not save genesis state") + + for i := 1; i < 6; i++ { + driftGenesisTime(service, int64(i), 0) + st, err := service.HeadState(ctx) + require.NoError(t, err) + b, err := util.GenerateFullBlock(st, keys, util.DefaultBlockGenConfig(), types.Slot(i)) + require.NoError(t, err) + wsb, err := wrapper.WrappedSignedBeaconBlock(b) + require.NoError(t, err) + root, err := b.Block.HashTreeRoot() + require.NoError(t, err) + require.NoError(t, service.onBlock(ctx, wsb, root)) + } + + for i := 6; i < 12; i++ { + driftGenesisTime(service, int64(i), 0) + st, err := service.HeadState(ctx) + require.NoError(t, err) + b, err := util.GenerateFullBlockAltair(st, keys, util.DefaultBlockGenConfig(), types.Slot(i)) + require.NoError(t, err) + wsb, err := wrapper.WrappedSignedBeaconBlock(b) + require.NoError(t, err) + root, err := b.Block.HashTreeRoot() + require.NoError(t, err) + err = service.onBlock(ctx, wsb, root) + require.NoError(t, err) + } + + for i := 12; i < 18; i++ { + driftGenesisTime(service, int64(i), 0) + st, err := service.HeadState(ctx) + require.NoError(t, err) + b, err := util.GenerateFullBlockBellatrix(st, keys, util.DefaultBlockGenConfig(), types.Slot(i)) + require.NoError(t, err) + wsb, err := wrapper.WrappedSignedBeaconBlock(b) + require.NoError(t, err) + root, err := b.Block.HashTreeRoot() + require.NoError(t, err) + err = service.onBlock(ctx, wsb, root) + require.NoError(t, err) + } + // Check that we haven't justified the second epoch yet + jc := service.ForkChoicer().JustifiedCheckpoint() + require.Equal(t, types.Epoch(0), jc.Epoch) + + // import a block that justifies the second epoch + driftGenesisTime(service, 18, 0) + validHeadState, err := service.HeadState(ctx) + require.NoError(t, err) + b, err := util.GenerateFullBlockBellatrix(validHeadState, keys, util.DefaultBlockGenConfig(), 18) + require.NoError(t, err) + wsb, err = wrapper.WrappedSignedBeaconBlock(b) + require.NoError(t, err) + firstInvalidRoot, err := b.Block.HashTreeRoot() + require.NoError(t, err) + err = service.onBlock(ctx, wsb, firstInvalidRoot) + require.NoError(t, err) + jc = service.ForkChoicer().JustifiedCheckpoint() + require.Equal(t, types.Epoch(2), jc.Epoch) + + sjc := validHeadState.CurrentJustifiedCheckpoint() + require.Equal(t, types.Epoch(0), sjc.Epoch) + lvh := b.Block.Body.ExecutionPayload.ParentHash + // check our head + require.Equal(t, firstInvalidRoot, service.ForkChoicer().CachedHeadRoot()) + + // import another block to find out that it was invalid + mockEngine = &mockExecution.EngineClient{ErrNewPayload: execution.ErrInvalidPayloadStatus, NewPayloadResp: lvh} + service.cfg.ExecutionEngineCaller = mockEngine + driftGenesisTime(service, 19, 0) + st, err = service.HeadState(ctx) + require.NoError(t, err) + b, err = util.GenerateFullBlockBellatrix(st, keys, util.DefaultBlockGenConfig(), 19) + require.NoError(t, err) + wsb, err = wrapper.WrappedSignedBeaconBlock(b) + require.NoError(t, err) + root, err := b.Block.HashTreeRoot() + require.NoError(t, err) + err = service.onBlock(ctx, wsb, root) + require.ErrorContains(t, "received an INVALID payload from execution engine", err) + // Check that forkchoice's head and store's headroot are the previous head (since the invalid block did + // not finish importing and it was never imported to forkchoice). Cehck + // also that the node is optimistic + require.Equal(t, firstInvalidRoot, service.ForkChoicer().CachedHeadRoot()) + headRoot, err := service.HeadRoot(ctx) + require.NoError(t, err) + require.Equal(t, firstInvalidRoot, bytesutil.ToBytes32(headRoot)) + optimistic, err := service.IsOptimistic(ctx) + require.NoError(t, err) + require.Equal(t, true, optimistic) + + // import another block based on the last valid head state + mockEngine = &mockExecution.EngineClient{} + service.cfg.ExecutionEngineCaller = mockEngine + driftGenesisTime(service, 20, 0) + b, err = util.GenerateFullBlockBellatrix(validHeadState, keys, &util.BlockGenConfig{}, 20) + require.NoError(t, err) + wsb, err = wrapper.WrappedSignedBeaconBlock(b) + require.NoError(t, err) + root, err = b.Block.HashTreeRoot() + require.NoError(t, err) + err = service.onBlock(ctx, wsb, root) + require.NoError(t, err) + // Check the newly imported block is head, it justified the right + // checkpoint and the node is no longer optimistic + require.Equal(t, root, service.ForkChoicer().CachedHeadRoot()) + sjc = service.CurrentJustifiedCheckpt() + require.Equal(t, jc.Epoch, sjc.Epoch) + require.Equal(t, jc.Root, bytesutil.ToBytes32(sjc.Root)) + optimistic, err = service.IsOptimistic(ctx) + require.NoError(t, err) + require.Equal(t, false, optimistic) +} + +// See the description in #10777 and #10782 for the full setup +// We sync optimistically a chain of blocks. Block 12 is the first block in Epoch +// 2 (and the merge block in this sequence). Block 18 justifies it and Block 19 returns +// INVALID from NewPayload, with LVH block 12. No head is viable. We check +// that the node is optimistic and that we can actually import a chain of blocks on top of +// 12 and recover. Notice that it takes two epochs to fully recover, and we stay +// optimistic for the whole time. +func TestStore_NoViableHead_Liveness_DoublyLinkedTree(t *testing.T) { + params.SetupTestConfigCleanup(t) + config := params.BeaconConfig() + config.SlotsPerEpoch = 6 + config.AltairForkEpoch = 1 + config.BellatrixForkEpoch = 2 + config.SafeSlotsToImportOptimistically = 0 + params.OverrideBeaconConfig(config) + + ctx := context.Background() + beaconDB := testDB.SetupDB(t) + + mockEngine := &mockExecution.EngineClient{ErrNewPayload: execution.ErrAcceptedSyncingPayloadStatus, ErrForkchoiceUpdated: execution.ErrAcceptedSyncingPayloadStatus} + opts := []Option{ + WithDatabase(beaconDB), + WithAttestationPool(attestations.NewPool()), + WithStateGen(stategen.New(beaconDB)), + WithForkChoiceStore(doublylinkedtree.New()), + WithStateNotifier(&mock.MockStateNotifier{}), + WithExecutionEngineCaller(mockEngine), + WithProposerIdsCache(cache.NewProposerPayloadIDsCache()), + } + service, err := NewService(ctx, opts...) + require.NoError(t, err) + + st, keys := util.DeterministicGenesisState(t, 64) + stateRoot, err := st.HashTreeRoot(ctx) + require.NoError(t, err, "Could not hash genesis state") + + require.NoError(t, service.saveGenesisData(ctx, st)) + + genesis := blocks.NewGenesisBlock(stateRoot[:]) + wsb, err := wrapper.WrappedSignedBeaconBlock(genesis) + require.NoError(t, err) + require.NoError(t, service.cfg.BeaconDB.SaveBlock(ctx, wsb), "Could not save genesis block") + + parentRoot, err := genesis.Block.HashTreeRoot() + require.NoError(t, err, "Could not get signing root") + require.NoError(t, service.cfg.BeaconDB.SaveState(ctx, st, parentRoot), "Could not save genesis state") + require.NoError(t, service.cfg.BeaconDB.SaveHeadBlockRoot(ctx, parentRoot), "Could not save genesis state") + + for i := 1; i < 6; i++ { + driftGenesisTime(service, int64(i), 0) + st, err := service.HeadState(ctx) + require.NoError(t, err) + b, err := util.GenerateFullBlock(st, keys, util.DefaultBlockGenConfig(), types.Slot(i)) + require.NoError(t, err) + wsb, err := wrapper.WrappedSignedBeaconBlock(b) + require.NoError(t, err) + root, err := b.Block.HashTreeRoot() + require.NoError(t, err) + require.NoError(t, service.onBlock(ctx, wsb, root)) + } + + for i := 6; i < 12; i++ { + driftGenesisTime(service, int64(i), 0) + st, err := service.HeadState(ctx) + require.NoError(t, err) + b, err := util.GenerateFullBlockAltair(st, keys, util.DefaultBlockGenConfig(), types.Slot(i)) + require.NoError(t, err) + wsb, err := wrapper.WrappedSignedBeaconBlock(b) + require.NoError(t, err) + root, err := b.Block.HashTreeRoot() + require.NoError(t, err) + err = service.onBlock(ctx, wsb, root) + require.NoError(t, err) + } + + // import the merge block + driftGenesisTime(service, 12, 0) + st, err = service.HeadState(ctx) + require.NoError(t, err) + b, err := util.GenerateFullBlockBellatrix(st, keys, util.DefaultBlockGenConfig(), 12) + require.NoError(t, err) + wsb, err = wrapper.WrappedSignedBeaconBlock(b) + require.NoError(t, err) + lastValidRoot, err := b.Block.HashTreeRoot() + require.NoError(t, err) + err = service.onBlock(ctx, wsb, lastValidRoot) + require.NoError(t, err) + // save the post state and the payload Hash of this block since it will + // be the LVH + validHeadState, err := service.HeadState(ctx) + require.NoError(t, err) + lvh := b.Block.Body.ExecutionPayload.BlockHash + validjc := validHeadState.CurrentJustifiedCheckpoint() + require.Equal(t, types.Epoch(0), validjc.Epoch) + + // import blocks 13 through 18 to justify 12 + for i := 13; i < 19; i++ { + driftGenesisTime(service, int64(i), 0) + st, err := service.HeadState(ctx) + require.NoError(t, err) + b, err := util.GenerateFullBlockBellatrix(st, keys, util.DefaultBlockGenConfig(), types.Slot(i)) + require.NoError(t, err) + wsb, err := wrapper.WrappedSignedBeaconBlock(b) + require.NoError(t, err) + root, err := b.Block.HashTreeRoot() + require.NoError(t, err) + err = service.onBlock(ctx, wsb, root) + require.NoError(t, err) + } + // Check that we have justified the second epoch + jc := service.ForkChoicer().JustifiedCheckpoint() + require.Equal(t, types.Epoch(2), jc.Epoch) + invalidHeadRoot := service.ForkChoicer().CachedHeadRoot() + + // import block 19 to find out that the whole chain 13--18 was in fact + // invalid + mockEngine = &mockExecution.EngineClient{ErrNewPayload: execution.ErrInvalidPayloadStatus, NewPayloadResp: lvh} + service.cfg.ExecutionEngineCaller = mockEngine + driftGenesisTime(service, 19, 0) + st, err = service.HeadState(ctx) + require.NoError(t, err) + b, err = util.GenerateFullBlockBellatrix(st, keys, util.DefaultBlockGenConfig(), 19) + require.NoError(t, err) + wsb, err = wrapper.WrappedSignedBeaconBlock(b) + require.NoError(t, err) + root, err := b.Block.HashTreeRoot() + require.NoError(t, err) + err = service.onBlock(ctx, wsb, root) + require.ErrorContains(t, "received an INVALID payload from execution engine", err) + + // Check that forkchoice's head and store's headroot are the previous head (since the invalid block did + // not finish importing and it was never imported to forkchoice). Cehck + // also that the node is optimistic + require.Equal(t, invalidHeadRoot, service.ForkChoicer().CachedHeadRoot()) + headRoot, err := service.HeadRoot(ctx) + require.NoError(t, err) + require.Equal(t, invalidHeadRoot, bytesutil.ToBytes32(headRoot)) + optimistic, err := service.IsOptimistic(ctx) + require.NoError(t, err) + require.Equal(t, true, optimistic) + + // Check that the node's justified checkpoint does not agree with the + // last valid state's justified checkpoint + sjc := service.CurrentJustifiedCheckpt() + require.Equal(t, types.Epoch(2), sjc.Epoch) + + // import another block based on the last valid head state + mockEngine = &mockExecution.EngineClient{} + service.cfg.ExecutionEngineCaller = mockEngine + driftGenesisTime(service, 20, 0) + b, err = util.GenerateFullBlockBellatrix(validHeadState, keys, &util.BlockGenConfig{}, 20) + require.NoError(t, err) + wsb, err = wrapper.WrappedSignedBeaconBlock(b) + require.NoError(t, err) + root, err = b.Block.HashTreeRoot() + require.NoError(t, err) + require.NoError(t, service.onBlock(ctx, wsb, root)) + // Check that the head is still INVALID and the node is still optimistic + require.Equal(t, invalidHeadRoot, service.ForkChoicer().CachedHeadRoot()) + optimistic, err = service.IsOptimistic(ctx) + require.NoError(t, err) + require.Equal(t, true, optimistic) + st, err = service.cfg.StateGen.StateByRoot(ctx, root) + require.NoError(t, err) + // Import blocks 21--30 (Epoch 3 was not enough to justify 2) + for i := 21; i < 30; i++ { + driftGenesisTime(service, int64(i), 0) + require.NoError(t, err) + b, err := util.GenerateFullBlockBellatrix(st, keys, util.DefaultBlockGenConfig(), types.Slot(i)) + require.NoError(t, err) + wsb, err := wrapper.WrappedSignedBeaconBlock(b) + require.NoError(t, err) + root, err := b.Block.HashTreeRoot() + require.NoError(t, err) + err = service.onBlock(ctx, wsb, root) + require.NoError(t, err) + st, err = service.cfg.StateGen.StateByRoot(ctx, root) + require.NoError(t, err) + } + // Head should still be INVALID and the node optimistic + require.Equal(t, invalidHeadRoot, service.ForkChoicer().CachedHeadRoot()) + optimistic, err = service.IsOptimistic(ctx) + require.NoError(t, err) + require.Equal(t, true, optimistic) + + // Import block 30, it should justify Epoch 4 and become HEAD, the node + // recovers + driftGenesisTime(service, 30, 0) + b, err = util.GenerateFullBlockBellatrix(st, keys, util.DefaultBlockGenConfig(), 30) + require.NoError(t, err) + wsb, err = wrapper.WrappedSignedBeaconBlock(b) + require.NoError(t, err) + root, err = b.Block.HashTreeRoot() + require.NoError(t, err) + err = service.onBlock(ctx, wsb, root) + require.NoError(t, err) + require.Equal(t, root, service.ForkChoicer().CachedHeadRoot()) + sjc = service.CurrentJustifiedCheckpt() + require.Equal(t, types.Epoch(4), sjc.Epoch) + optimistic, err = service.IsOptimistic(ctx) + require.NoError(t, err) + require.Equal(t, false, optimistic) +} + +// See the description in #10777 and #10782 for the full setup +// We sync optimistically a chain of blocks. Block 12 is the first block in Epoch +// 2 (and the merge block in this sequence). Block 18 justifies it and Block 19 returns +// INVALID from NewPayload, with LVH block 12. No head is viable. We check +// that the node is optimistic and that we can actually import a chain of blocks on top of +// 12 and recover. Notice that it takes two epochs to fully recover, and we stay +// optimistic for the whole time. +func TestStore_NoViableHead_Liveness_Protoarray(t *testing.T) { + params.SetupTestConfigCleanup(t) + config := params.BeaconConfig() + config.SlotsPerEpoch = 6 + config.AltairForkEpoch = 1 + config.BellatrixForkEpoch = 2 + config.SafeSlotsToImportOptimistically = 0 + params.OverrideBeaconConfig(config) + + ctx := context.Background() + beaconDB := testDB.SetupDB(t) + + mockEngine := &mockExecution.EngineClient{ErrNewPayload: execution.ErrAcceptedSyncingPayloadStatus, ErrForkchoiceUpdated: execution.ErrAcceptedSyncingPayloadStatus} + opts := []Option{ + WithDatabase(beaconDB), + WithAttestationPool(attestations.NewPool()), + WithStateGen(stategen.New(beaconDB)), + WithForkChoiceStore(doublylinkedtree.New()), + WithStateNotifier(&mock.MockStateNotifier{}), + WithExecutionEngineCaller(mockEngine), + WithProposerIdsCache(cache.NewProposerPayloadIDsCache()), + } + service, err := NewService(ctx, opts...) + require.NoError(t, err) + + st, keys := util.DeterministicGenesisState(t, 64) + stateRoot, err := st.HashTreeRoot(ctx) + require.NoError(t, err, "Could not hash genesis state") + + require.NoError(t, service.saveGenesisData(ctx, st)) + + genesis := blocks.NewGenesisBlock(stateRoot[:]) + wsb, err := wrapper.WrappedSignedBeaconBlock(genesis) + require.NoError(t, err) + require.NoError(t, service.cfg.BeaconDB.SaveBlock(ctx, wsb), "Could not save genesis block") + + parentRoot, err := genesis.Block.HashTreeRoot() + require.NoError(t, err, "Could not get signing root") + require.NoError(t, service.cfg.BeaconDB.SaveState(ctx, st, parentRoot), "Could not save genesis state") + require.NoError(t, service.cfg.BeaconDB.SaveHeadBlockRoot(ctx, parentRoot), "Could not save genesis state") + + for i := 1; i < 6; i++ { + driftGenesisTime(service, int64(i), 0) + st, err := service.HeadState(ctx) + require.NoError(t, err) + b, err := util.GenerateFullBlock(st, keys, util.DefaultBlockGenConfig(), types.Slot(i)) + require.NoError(t, err) + wsb, err := wrapper.WrappedSignedBeaconBlock(b) + require.NoError(t, err) + root, err := b.Block.HashTreeRoot() + require.NoError(t, err) + require.NoError(t, service.onBlock(ctx, wsb, root)) + } + + for i := 6; i < 12; i++ { + driftGenesisTime(service, int64(i), 0) + st, err := service.HeadState(ctx) + require.NoError(t, err) + b, err := util.GenerateFullBlockAltair(st, keys, util.DefaultBlockGenConfig(), types.Slot(i)) + require.NoError(t, err) + wsb, err := wrapper.WrappedSignedBeaconBlock(b) + require.NoError(t, err) + root, err := b.Block.HashTreeRoot() + require.NoError(t, err) + err = service.onBlock(ctx, wsb, root) + require.NoError(t, err) + } + + // import the merge block + driftGenesisTime(service, 12, 0) + st, err = service.HeadState(ctx) + require.NoError(t, err) + b, err := util.GenerateFullBlockBellatrix(st, keys, util.DefaultBlockGenConfig(), 12) + require.NoError(t, err) + wsb, err = wrapper.WrappedSignedBeaconBlock(b) + require.NoError(t, err) + lastValidRoot, err := b.Block.HashTreeRoot() + require.NoError(t, err) + err = service.onBlock(ctx, wsb, lastValidRoot) + require.NoError(t, err) + // save the post state and the payload Hash of this block since it will + // be the LVH + validHeadState, err := service.HeadState(ctx) + require.NoError(t, err) + lvh := b.Block.Body.ExecutionPayload.BlockHash + validjc := validHeadState.CurrentJustifiedCheckpoint() + require.Equal(t, types.Epoch(0), validjc.Epoch) + + // import blocks 13 through 18 to justify 12 + for i := 13; i < 19; i++ { + driftGenesisTime(service, int64(i), 0) + st, err := service.HeadState(ctx) + require.NoError(t, err) + b, err := util.GenerateFullBlockBellatrix(st, keys, util.DefaultBlockGenConfig(), types.Slot(i)) + require.NoError(t, err) + wsb, err := wrapper.WrappedSignedBeaconBlock(b) + require.NoError(t, err) + root, err := b.Block.HashTreeRoot() + require.NoError(t, err) + err = service.onBlock(ctx, wsb, root) + require.NoError(t, err) + } + // Check that we have justified the second epoch + jc := service.ForkChoicer().JustifiedCheckpoint() + require.Equal(t, types.Epoch(2), jc.Epoch) + invalidHeadRoot := service.ForkChoicer().CachedHeadRoot() + + // import block 19 to find out that the whole chain 13--18 was in fact + // invalid + mockEngine = &mockExecution.EngineClient{ErrNewPayload: execution.ErrInvalidPayloadStatus, NewPayloadResp: lvh} + service.cfg.ExecutionEngineCaller = mockEngine + driftGenesisTime(service, 19, 0) + st, err = service.HeadState(ctx) + require.NoError(t, err) + b, err = util.GenerateFullBlockBellatrix(st, keys, util.DefaultBlockGenConfig(), 19) + require.NoError(t, err) + wsb, err = wrapper.WrappedSignedBeaconBlock(b) + require.NoError(t, err) + root, err := b.Block.HashTreeRoot() + require.NoError(t, err) + err = service.onBlock(ctx, wsb, root) + require.ErrorContains(t, "received an INVALID payload from execution engine", err) + + // Check that forkchoice's head and store's headroot are the previous head (since the invalid block did + // not finish importing and it was never imported to forkchoice). Cehck + // also that the node is optimistic + require.Equal(t, invalidHeadRoot, service.ForkChoicer().CachedHeadRoot()) + headRoot, err := service.HeadRoot(ctx) + require.NoError(t, err) + require.Equal(t, invalidHeadRoot, bytesutil.ToBytes32(headRoot)) + optimistic, err := service.IsOptimistic(ctx) + require.NoError(t, err) + require.Equal(t, true, optimistic) + + // Check that the node's justified checkpoint does not agree with the + // last valid state's justified checkpoint + sjc := service.CurrentJustifiedCheckpt() + require.Equal(t, types.Epoch(2), sjc.Epoch) + + // import another block based on the last valid head state + mockEngine = &mockExecution.EngineClient{} + service.cfg.ExecutionEngineCaller = mockEngine + driftGenesisTime(service, 20, 0) + b, err = util.GenerateFullBlockBellatrix(validHeadState, keys, &util.BlockGenConfig{}, 20) + require.NoError(t, err) + wsb, err = wrapper.WrappedSignedBeaconBlock(b) + require.NoError(t, err) + root, err = b.Block.HashTreeRoot() + require.NoError(t, err) + require.NoError(t, service.onBlock(ctx, wsb, root)) + // Check that the head is still INVALID and the node is still optimistic + require.Equal(t, invalidHeadRoot, service.ForkChoicer().CachedHeadRoot()) + optimistic, err = service.IsOptimistic(ctx) + require.NoError(t, err) + require.Equal(t, true, optimistic) + st, err = service.cfg.StateGen.StateByRoot(ctx, root) + require.NoError(t, err) + // Import blocks 21--30 (Epoch 3 was not enough to justify 2) + for i := 21; i < 30; i++ { + driftGenesisTime(service, int64(i), 0) + require.NoError(t, err) + b, err := util.GenerateFullBlockBellatrix(st, keys, util.DefaultBlockGenConfig(), types.Slot(i)) + require.NoError(t, err) + wsb, err := wrapper.WrappedSignedBeaconBlock(b) + require.NoError(t, err) + root, err := b.Block.HashTreeRoot() + require.NoError(t, err) + err = service.onBlock(ctx, wsb, root) + require.NoError(t, err) + st, err = service.cfg.StateGen.StateByRoot(ctx, root) + require.NoError(t, err) + } + // Head should still be INVALID and the node optimistic + require.Equal(t, invalidHeadRoot, service.ForkChoicer().CachedHeadRoot()) + optimistic, err = service.IsOptimistic(ctx) + require.NoError(t, err) + require.Equal(t, true, optimistic) + + // Import block 30, it should justify Epoch 4 and become HEAD, the node + // recovers + driftGenesisTime(service, 30, 0) + b, err = util.GenerateFullBlockBellatrix(st, keys, util.DefaultBlockGenConfig(), 30) + require.NoError(t, err) + wsb, err = wrapper.WrappedSignedBeaconBlock(b) + require.NoError(t, err) + root, err = b.Block.HashTreeRoot() + require.NoError(t, err) + err = service.onBlock(ctx, wsb, root) + require.NoError(t, err) + require.Equal(t, root, service.ForkChoicer().CachedHeadRoot()) + sjc = service.CurrentJustifiedCheckpt() + require.Equal(t, types.Epoch(4), sjc.Epoch) + optimistic, err = service.IsOptimistic(ctx) + require.NoError(t, err) + require.Equal(t, false, optimistic) +} + +// Helper function to simulate the block being on time or delayed for proposer +// boost. It alters the genesisTime tracked by the store. +func driftGenesisTime(s *Service, slot int64, delay int64) { + offset := int64(slot*int64(params.BeaconConfig().SecondsPerSlot) - delay) + s.SetGenesisTime(time.Unix(time.Now().Unix()-offset, 0)) +} diff --git a/beacon-chain/core/altair/block.go b/beacon-chain/core/altair/block.go index 20def229c33f..3be31371d01b 100644 --- a/beacon-chain/core/altair/block.go +++ b/beacon-chain/core/altair/block.go @@ -2,8 +2,8 @@ package altair import ( "context" - "errors" + "github.com/pkg/errors" "github.com/prysmaticlabs/prysm/beacon-chain/core/helpers" "github.com/prysmaticlabs/prysm/beacon-chain/core/signing" p2pType "github.com/prysmaticlabs/prysm/beacon-chain/p2p/types" @@ -47,11 +47,11 @@ import ( func ProcessSyncAggregate(ctx context.Context, s state.BeaconState, sync *ethpb.SyncAggregate) (state.BeaconState, error) { votedKeys, votedIndices, didntVoteIndices, err := FilterSyncCommitteeVotes(s, sync) if err != nil { - return nil, err + return nil, errors.Wrap(err, "could not filter sync committee votes") } if err := VerifySyncCommitteeSig(s, votedKeys, sync.SyncCommitteeSignature); err != nil { - return nil, err + return nil, errors.Wrap(err, "could not verify sync committee signature") } return ApplySyncRewardsPenalties(ctx, s, votedIndices, didntVoteIndices) diff --git a/beacon-chain/core/transition/bellatrix_transition_no_verify_sig_test.go b/beacon-chain/core/transition/bellatrix_transition_no_verify_sig_test.go index 2e3107f0b7df..c535d73e77d9 100644 --- a/beacon-chain/core/transition/bellatrix_transition_no_verify_sig_test.go +++ b/beacon-chain/core/transition/bellatrix_transition_no_verify_sig_test.go @@ -99,7 +99,7 @@ func TestExecuteBellatrixStateTransitionNoVerify_FullProcess(t *testing.T) { block.Block.StateRoot = stateRoot[:] c := beaconState.Copy() - sig, err := util.BlockSignatureBellatrix(c, block.Block, privKeys) + sig, err := util.BlockSignature(c, block.Block, privKeys) require.NoError(t, err) block.Signature = sig.Marshal() @@ -187,7 +187,7 @@ func TestExecuteBellatrixStateTransitionNoVerifySignature_CouldNotVerifyStateRoo block.Block.StateRoot = stateRoot[:] c := beaconState.Copy() - sig, err := util.BlockSignatureBellatrix(c, block.Block, privKeys) + sig, err := util.BlockSignature(c, block.Block, privKeys) require.NoError(t, err) block.Signature = sig.Marshal() diff --git a/beacon-chain/monitor/process_block_test.go b/beacon-chain/monitor/process_block_test.go index 50c1ed38c597..a5e14c8e8ff0 100644 --- a/beacon-chain/monitor/process_block_test.go +++ b/beacon-chain/monitor/process_block_test.go @@ -194,6 +194,7 @@ func TestProcessBlock_AllEventsTrackedVals(t *testing.T) { genConfig := util.DefaultBlockGenConfig() genConfig.NumProposerSlashings = 1 + genConfig.FullSyncAggregate = true b, err := util.GenerateFullBlockAltair(genesis, keys, genConfig, 1) require.NoError(t, err) s := setupService(t) diff --git a/testing/util/altair.go b/testing/util/altair.go index 8a814e65f6d1..89ed9c3581cd 100644 --- a/testing/util/altair.go +++ b/testing/util/altair.go @@ -311,7 +311,7 @@ func BlockSignatureAltair( return privKeys[proposerIdx].Sign(blockRoot[:]), nil } -// GenerateFullBlockAltair generates a fully valid block with the requested parameters. +// GenerateFullBlockAltair generates a fully valid Altair block with the requested parameters. // Use BlockGenConfig to declare the conditions you would like the block generated under. func GenerateFullBlockAltair( bState state.BeaconState, @@ -388,26 +388,44 @@ func GenerateFullBlockAltair( return nil, err } + var newSyncAggregate *ethpb.SyncAggregate + if conf.FullSyncAggregate { + newSyncAggregate, err = generateSyncAggregate(bState, privs, parentRoot) + if err != nil { + return nil, errors.Wrap(err, "failed generating syncAggregate") + } + } else { + var syncCommitteeBits []byte + currSize := new(ethpb.SyncAggregate).SyncCommitteeBits.Len() + switch currSize { + case 512: + syncCommitteeBits = bitfield.NewBitvector512() + case 32: + syncCommitteeBits = bitfield.NewBitvector32() + default: + return nil, errors.New("invalid bit vector size") + } + newSyncAggregate = ðpb.SyncAggregate{ + SyncCommitteeBits: syncCommitteeBits, + SyncCommitteeSignature: append([]byte{0xC0}, make([]byte, 95)...), + } + } + if slot == currentSlot { slot = currentSlot + 1 } - syncAgg, err := generateSyncAggregate(bState, privs, parentRoot) + stCopy := bState.Copy() + stCopy, err = transition.ProcessSlots(context.Background(), stCopy, slot) if err != nil { return nil, err } - - // Temporarily incrementing the beacon state slot here since BeaconProposerIndex is a - // function deterministic on beacon state slot. - if err := bState.SetSlot(slot); err != nil { - return nil, err - } - reveal, err := RandaoReveal(bState, time.CurrentEpoch(bState), privs) + reveal, err := RandaoReveal(stCopy, time.CurrentEpoch(stCopy), privs) if err != nil { return nil, err } - idx, err := helpers.BeaconProposerIndex(ctx, bState) + idx, err := helpers.BeaconProposerIndex(ctx, stCopy) if err != nil { return nil, err } @@ -424,15 +442,12 @@ func GenerateFullBlockAltair( Attestations: atts, VoluntaryExits: exits, Deposits: newDeposits, - Graffiti: make([]byte, 32), - SyncAggregate: syncAgg, + Graffiti: make([]byte, fieldparams.RootLength), + SyncAggregate: newSyncAggregate, }, } - if err := bState.SetSlot(currentSlot); err != nil { - return nil, err - } - signature, err := BlockSignatureAltair(bState, block, privs) + signature, err := BlockSignature(bState, block, privs) if err != nil { return nil, err } diff --git a/testing/util/attestation.go b/testing/util/attestation.go index c34de8971d3e..4c58ca5d2a22 100644 --- a/testing/util/attestation.go +++ b/testing/util/attestation.go @@ -13,6 +13,7 @@ import ( "github.com/prysmaticlabs/prysm/beacon-chain/state" v1 "github.com/prysmaticlabs/prysm/beacon-chain/state/v1" v2 "github.com/prysmaticlabs/prysm/beacon-chain/state/v2" + v3 "github.com/prysmaticlabs/prysm/beacon-chain/state/v3" fieldparams "github.com/prysmaticlabs/prysm/config/fieldparams" "github.com/prysmaticlabs/prysm/config/params" types "github.com/prysmaticlabs/prysm/consensus-types/primitives" @@ -89,6 +90,16 @@ func GenerateAttestations( return nil, err } headState = genState + case version.Bellatrix: + pbState, err := v3.ProtobufBeaconState(bState.CloneInnerState()) + if err != nil { + return nil, err + } + genState, err := v3.InitializeFromProtoUnsafe(pbState) + if err != nil { + return nil, err + } + headState = genState default: return nil, errors.New("state type isn't supported") } diff --git a/testing/util/bellatrix.go b/testing/util/bellatrix.go index 49121c929560..f86795ac1b89 100644 --- a/testing/util/bellatrix.go +++ b/testing/util/bellatrix.go @@ -2,54 +2,203 @@ package util import ( "context" + "encoding/binary" + "fmt" + "github.com/pkg/errors" + "github.com/prysmaticlabs/go-bitfield" "github.com/prysmaticlabs/prysm/beacon-chain/core/helpers" - "github.com/prysmaticlabs/prysm/beacon-chain/core/signing" "github.com/prysmaticlabs/prysm/beacon-chain/core/time" "github.com/prysmaticlabs/prysm/beacon-chain/core/transition" "github.com/prysmaticlabs/prysm/beacon-chain/state" + fieldparams "github.com/prysmaticlabs/prysm/config/fieldparams" "github.com/prysmaticlabs/prysm/config/params" - "github.com/prysmaticlabs/prysm/consensus-types/wrapper" + types "github.com/prysmaticlabs/prysm/consensus-types/primitives" "github.com/prysmaticlabs/prysm/crypto/bls" + "github.com/prysmaticlabs/prysm/crypto/hash" + "github.com/prysmaticlabs/prysm/encoding/bytesutil" + enginev1 "github.com/prysmaticlabs/prysm/proto/engine/v1" ethpb "github.com/prysmaticlabs/prysm/proto/prysm/v1alpha1" + "github.com/prysmaticlabs/prysm/time/slots" ) -// BlockSignatureBellatrix calculates the post-state root of the block and returns the signature. -func BlockSignatureBellatrix( +// GenerateFullBlockBellatrix generates a fully valid Bellatrix block with the requested parameters. +// Use BlockGenConfig to declare the conditions you would like the block generated under. +// This function modifies the passed state as follows: + +func GenerateFullBlockBellatrix( bState state.BeaconState, - block *ethpb.BeaconBlockBellatrix, - privKeys []bls.SecretKey, -) (bls.Signature, error) { + privs []bls.SecretKey, + conf *BlockGenConfig, + slot types.Slot, +) (*ethpb.SignedBeaconBlockBellatrix, error) { + ctx := context.Background() + currentSlot := bState.Slot() + if currentSlot > slot { + return nil, fmt.Errorf("current slot in state is larger than given slot. %d > %d", currentSlot, slot) + } + bState = bState.Copy() + + if conf == nil { + conf = &BlockGenConfig{} + } + var err error - wsb, err := wrapper.WrappedSignedBeaconBlock(ðpb.SignedBeaconBlockBellatrix{Block: block}) + var pSlashings []*ethpb.ProposerSlashing + numToGen := conf.NumProposerSlashings + if numToGen > 0 { + pSlashings, err = generateProposerSlashings(bState, privs, numToGen) + if err != nil { + return nil, errors.Wrapf(err, "failed generating %d proposer slashings:", numToGen) + } + } + + numToGen = conf.NumAttesterSlashings + var aSlashings []*ethpb.AttesterSlashing + if numToGen > 0 { + aSlashings, err = generateAttesterSlashings(bState, privs, numToGen) + if err != nil { + return nil, errors.Wrapf(err, "failed generating %d attester slashings:", numToGen) + } + } + + numToGen = conf.NumAttestations + var atts []*ethpb.Attestation + if numToGen > 0 { + atts, err = GenerateAttestations(bState, privs, numToGen, slot, false) + if err != nil { + return nil, errors.Wrapf(err, "failed generating %d attestations:", numToGen) + } + } + + numToGen = conf.NumDeposits + var newDeposits []*ethpb.Deposit + eth1Data := bState.Eth1Data() + if numToGen > 0 { + newDeposits, eth1Data, err = generateDepositsAndEth1Data(bState, numToGen) + if err != nil { + return nil, errors.Wrapf(err, "failed generating %d deposits:", numToGen) + } + } + + numToGen = conf.NumVoluntaryExits + var exits []*ethpb.SignedVoluntaryExit + if numToGen > 0 { + exits, err = generateVoluntaryExits(bState, privs, numToGen) + if err != nil { + return nil, errors.Wrapf(err, "failed generating %d attester slashings:", numToGen) + } + } + + numToGen = conf.NumTransactions + newTransactions := make([][]byte, numToGen) + for i := uint64(0); i < numToGen; i++ { + newTransactions[i] = bytesutil.Uint64ToBytesLittleEndian(i) + } + random, err := helpers.RandaoMix(bState, time.CurrentEpoch(bState)) if err != nil { - return nil, err + return nil, errors.Wrap(err, "could not process randao mix") } - s, err := transition.CalculateStateRoot(context.Background(), bState, wsb) + + timestamp, err := slots.ToTime(bState.GenesisTime(), slot) if err != nil { - return nil, err + return nil, errors.Wrap(err, "could not get current timestamp") } - block.StateRoot = s[:] - domain, err := signing.Domain(bState.Fork(), time.CurrentEpoch(bState), params.BeaconConfig().DomainBeaconProposer, bState.GenesisValidatorsRoot()) + + stCopy := bState.Copy() + stCopy, err = transition.ProcessSlots(context.Background(), stCopy, slot) if err != nil { return nil, err } - blockRoot, err := signing.ComputeSigningRoot(block, domain) + + parentExecution, err := stCopy.LatestExecutionPayloadHeader() if err != nil { return nil, err } - // Temporarily increasing the beacon state slot here since BeaconProposerIndex is a - // function deterministic on beacon state slot. - currentSlot := bState.Slot() - if err := bState.SetSlot(block.Slot); err != nil { - return nil, err + blockHash := indexToHash(uint64(slot)) + newExecutionPayload := &enginev1.ExecutionPayload{ + ParentHash: parentExecution.BlockHash, + FeeRecipient: make([]byte, 20), + StateRoot: params.BeaconConfig().ZeroHash[:], + ReceiptsRoot: params.BeaconConfig().ZeroHash[:], + LogsBloom: make([]byte, 256), + PrevRandao: random, + BlockNumber: uint64(slot), + ExtraData: params.BeaconConfig().ZeroHash[:], + BaseFeePerGas: params.BeaconConfig().ZeroHash[:], + BlockHash: blockHash[:], + Timestamp: uint64(timestamp.Unix()), + Transactions: newTransactions, + } + var syncCommitteeBits []byte + currSize := new(ethpb.SyncAggregate).SyncCommitteeBits.Len() + switch currSize { + case 512: + syncCommitteeBits = bitfield.NewBitvector512() + case 32: + syncCommitteeBits = bitfield.NewBitvector32() + default: + return nil, errors.New("invalid bit vector size") + } + newSyncAggregate := ðpb.SyncAggregate{ + SyncCommitteeBits: syncCommitteeBits, + SyncCommitteeSignature: append([]byte{0xC0}, make([]byte, 95)...), } - proposerIdx, err := helpers.BeaconProposerIndex(context.Background(), bState) + + newHeader := bState.LatestBlockHeader() + prevStateRoot, err := bState.HashTreeRoot(ctx) if err != nil { - return nil, err + return nil, errors.Wrap(err, "could not hash state") } - if err := bState.SetSlot(currentSlot); err != nil { - return nil, err + newHeader.StateRoot = prevStateRoot[:] + parentRoot, err := newHeader.HashTreeRoot() + if err != nil { + return nil, errors.Wrap(err, "could not hash the new header") + } + + if slot == currentSlot { + slot = currentSlot + 1 + } + + reveal, err := RandaoReveal(stCopy, time.CurrentEpoch(stCopy), privs) + if err != nil { + return nil, errors.Wrap(err, "could not compute randao reveal") } - return privKeys[proposerIdx].Sign(blockRoot[:]), nil + + idx, err := helpers.BeaconProposerIndex(ctx, stCopy) + if err != nil { + return nil, errors.Wrap(err, "could not compute beacon proposer index") + } + + block := ðpb.BeaconBlockBellatrix{ + Slot: slot, + ParentRoot: parentRoot[:], + ProposerIndex: idx, + Body: ðpb.BeaconBlockBodyBellatrix{ + Eth1Data: eth1Data, + RandaoReveal: reveal, + ProposerSlashings: pSlashings, + AttesterSlashings: aSlashings, + Attestations: atts, + VoluntaryExits: exits, + Deposits: newDeposits, + Graffiti: make([]byte, fieldparams.RootLength), + SyncAggregate: newSyncAggregate, + ExecutionPayload: newExecutionPayload, + }, + } + + // The fork can change after processing the state + signature, err := BlockSignature(bState, block, privs) + if err != nil { + return nil, errors.Wrap(err, "could not compute block signature") + } + + return ðpb.SignedBeaconBlockBellatrix{Block: block, Signature: signature.Marshal()}, nil +} + +func indexToHash(i uint64) [32]byte { + var b [8]byte + binary.LittleEndian.PutUint64(b[:], i) + return hash.Hash(b[:]) } diff --git a/testing/util/block.go b/testing/util/block.go index ffcf822aab69..7585b4053ab2 100644 --- a/testing/util/block.go +++ b/testing/util/block.go @@ -34,6 +34,8 @@ type BlockGenConfig struct { NumAttestations uint64 NumDeposits uint64 NumVoluntaryExits uint64 + NumTransactions uint64 // Only for post Bellatrix blocks + FullSyncAggregate bool } // DefaultBlockGenConfig returns the block config that utilizes the @@ -45,6 +47,7 @@ func DefaultBlockGenConfig() *BlockGenConfig { NumAttestations: 1, NumDeposits: 0, NumVoluntaryExits: 0, + NumTransactions: 0, } } diff --git a/testing/util/helpers.go b/testing/util/helpers.go index c9b9ee9bef38..555a3f73531d 100644 --- a/testing/util/helpers.go +++ b/testing/util/helpers.go @@ -12,6 +12,7 @@ import ( "github.com/prysmaticlabs/prysm/beacon-chain/core/transition" "github.com/prysmaticlabs/prysm/beacon-chain/state" "github.com/prysmaticlabs/prysm/config/params" + "github.com/prysmaticlabs/prysm/consensus-types/interfaces" types "github.com/prysmaticlabs/prysm/consensus-types/primitives" "github.com/prysmaticlabs/prysm/consensus-types/wrapper" "github.com/prysmaticlabs/prysm/crypto/bls" @@ -37,37 +38,78 @@ func RandaoReveal(beaconState state.ReadOnlyBeaconState, epoch types.Epoch, priv // BlockSignature calculates the post-state root of the block and returns the signature. func BlockSignature( bState state.BeaconState, - block *ethpb.BeaconBlock, + block interface{}, privKeys []bls.SecretKey, ) (bls.Signature, error) { - wsb, err := wrapper.WrappedSignedBeaconBlock(ðpb.SignedBeaconBlock{Block: block}) + var wsb interfaces.SignedBeaconBlock + var err error + // copy the state since we need to process slots + bState = bState.Copy() + switch b := block.(type) { + case *ethpb.BeaconBlock: + wsb, err = wrapper.WrappedSignedBeaconBlock(ðpb.SignedBeaconBlock{Block: b}) + case *ethpb.BeaconBlockAltair: + wsb, err = wrapper.WrappedSignedBeaconBlock(ðpb.SignedBeaconBlockAltair{Block: b}) + case *ethpb.BeaconBlockBellatrix: + wsb, err = wrapper.WrappedSignedBeaconBlock(ðpb.SignedBeaconBlockBellatrix{Block: b}) + default: + return nil, errors.New("unsupported block type") + } if err != nil { return nil, errors.Wrap(err, "could not wrap block") } s, err := transition.CalculateStateRoot(context.Background(), bState, wsb) if err != nil { - return nil, err + return nil, errors.Wrap(err, "could not calculate state root") } - block.StateRoot = s[:] - domain, err := signing.Domain(bState.Fork(), time.CurrentEpoch(bState), params.BeaconConfig().DomainBeaconProposer, bState.GenesisValidatorsRoot()) + + switch b := block.(type) { + case *ethpb.BeaconBlock: + b.StateRoot = s[:] + case *ethpb.BeaconBlockAltair: + b.StateRoot = s[:] + case *ethpb.BeaconBlockBellatrix: + b.StateRoot = s[:] + } + + // Temporarily increasing the beacon state slot here since BeaconProposerIndex is a + // function deterministic on beacon state slot. + var blockSlot types.Slot + switch b := block.(type) { + case *ethpb.BeaconBlock: + blockSlot = b.Slot + case *ethpb.BeaconBlockAltair: + blockSlot = b.Slot + case *ethpb.BeaconBlockBellatrix: + blockSlot = b.Slot + } + + // process slots to get the right fork + bState, err = transition.ProcessSlots(context.Background(), bState, blockSlot) if err != nil { return nil, err } - blockRoot, err := signing.ComputeSigningRoot(block, domain) + + domain, err := signing.Domain(bState.Fork(), time.CurrentEpoch(bState), params.BeaconConfig().DomainBeaconProposer, bState.GenesisValidatorsRoot()) if err != nil { return nil, err } - // Temporarily increasing the beacon state slot here since BeaconProposerIndex is a - // function deterministic on beacon state slot. - currentSlot := bState.Slot() - if err := bState.SetSlot(block.Slot); err != nil { - return nil, err + + var blockRoot [32]byte + switch b := block.(type) { + case *ethpb.BeaconBlock: + blockRoot, err = signing.ComputeSigningRoot(b, domain) + case *ethpb.BeaconBlockAltair: + blockRoot, err = signing.ComputeSigningRoot(b, domain) + case *ethpb.BeaconBlockBellatrix: + blockRoot, err = signing.ComputeSigningRoot(b, domain) } - proposerIdx, err := helpers.BeaconProposerIndex(context.Background(), bState) if err != nil { return nil, err } - if err := bState.SetSlot(currentSlot); err != nil { + + proposerIdx, err := helpers.BeaconProposerIndex(context.Background(), bState) + if err != nil { return nil, err } return privKeys[proposerIdx].Sign(blockRoot[:]), nil