diff --git a/beacon-chain/blockchain/BUILD.bazel b/beacon-chain/blockchain/BUILD.bazel index dcb8ffc8b9d6..522eb0643892 100644 --- a/beacon-chain/blockchain/BUILD.bazel +++ b/beacon-chain/blockchain/BUILD.bazel @@ -182,6 +182,7 @@ go_test( "//time:go_default_library", "//time/slots:go_default_library", "@com_github_ethereum_go_ethereum//common:go_default_library", + "@com_github_ethereum_go_ethereum//common/hexutil:go_default_library", "@com_github_ethereum_go_ethereum//core/types:go_default_library", "@com_github_holiman_uint256//:go_default_library", "@com_github_pkg_errors//:go_default_library", diff --git a/beacon-chain/blockchain/receive_block.go b/beacon-chain/blockchain/receive_block.go index db2de5f1ff4e..8a3996025327 100644 --- a/beacon-chain/blockchain/receive_block.go +++ b/beacon-chain/blockchain/receive_block.go @@ -169,13 +169,8 @@ func (s *Service) ReceiveBlock(ctx context.Context, block interfaces.ReadOnlySig } // Send finalized events and finalized deposits in the background if newFinalized { - finalized := s.cfg.ForkChoiceStore.FinalizedCheckpoint() - go s.sendNewFinalizedEvent(ctx, postState) - depCtx, cancel := context.WithTimeout(context.Background(), depositDeadline) - go func() { - s.insertFinalizedDeposits(depCtx, finalized.Root) - cancel() - }() + // hook to process all post state finalization tasks + s.executePostFinalizationTasks(ctx, postState) } // If slasher is configured, forward the attestations in the block via an event feed for processing. @@ -224,6 +219,19 @@ func (s *Service) ReceiveBlock(ctx context.Context, block interfaces.ReadOnlySig return nil } +func (s *Service) executePostFinalizationTasks(ctx context.Context, finalizedState state.BeaconState) { + finalized := s.cfg.ForkChoiceStore.FinalizedCheckpoint() + go func() { + finalizedState.SaveValidatorIndices() // used to handle Validator index invariant from EIP6110 + s.sendNewFinalizedEvent(ctx, finalizedState) + }() + depCtx, cancel := context.WithTimeout(context.Background(), depositDeadline) + go func() { + s.insertFinalizedDeposits(depCtx, finalized.Root) + cancel() + }() +} + // ReceiveBlockBatch processes the whole block batch at once, assuming the block batch is linear ,transitioning // the state, performing batch verification of all collected signatures and then performing the appropriate // actions for a block post-transition. diff --git a/beacon-chain/blockchain/receive_block_test.go b/beacon-chain/blockchain/receive_block_test.go index 9c065d767506..f6ec1745c9fe 100644 --- a/beacon-chain/blockchain/receive_block_test.go +++ b/beacon-chain/blockchain/receive_block_test.go @@ -6,11 +6,14 @@ import ( "testing" "time" + "github.com/ethereum/go-ethereum/common/hexutil" blockchainTesting "github.com/prysmaticlabs/prysm/v5/beacon-chain/blockchain/testing" "github.com/prysmaticlabs/prysm/v5/beacon-chain/cache" statefeed "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/feed/state" "github.com/prysmaticlabs/prysm/v5/beacon-chain/das" "github.com/prysmaticlabs/prysm/v5/beacon-chain/operations/voluntaryexits" + "github.com/prysmaticlabs/prysm/v5/config/features" + fieldparams "github.com/prysmaticlabs/prysm/v5/config/fieldparams" "github.com/prysmaticlabs/prysm/v5/config/params" "github.com/prysmaticlabs/prysm/v5/consensus-types/blocks" "github.com/prysmaticlabs/prysm/v5/consensus-types/primitives" @@ -20,6 +23,7 @@ import ( "github.com/prysmaticlabs/prysm/v5/testing/assert" "github.com/prysmaticlabs/prysm/v5/testing/require" "github.com/prysmaticlabs/prysm/v5/testing/util" + "github.com/prysmaticlabs/prysm/v5/time/slots" logTest "github.com/sirupsen/logrus/hooks/test" ) @@ -415,3 +419,81 @@ func Test_sendNewFinalizedEvent(t *testing.T) { assert.DeepEqual(t, finalizedStRoot[:], fc.State) assert.Equal(t, false, fc.ExecutionOptimistic) } + +func Test_executePostFinalizationTasks(t *testing.T) { + resetFn := features.InitWithReset(&features.Flags{ + EIP6110ValidatorIndexCache: true, + }) + defer resetFn() + + logHook := logTest.NewGlobal() + + headState, err := util.NewBeaconState() + require.NoError(t, err) + finalizedStRoot, err := headState.HashTreeRoot(context.Background()) + require.NoError(t, err) + + genesis := util.NewBeaconBlock() + genesisRoot, err := genesis.Block.HashTreeRoot() + require.NoError(t, err) + finalizedSlot := params.BeaconConfig().SlotsPerEpoch*122 + 1 + headBlock := util.NewBeaconBlock() + headBlock.Block.Slot = finalizedSlot + headBlock.Block.StateRoot = finalizedStRoot[:] + headBlock.Block.ParentRoot = bytesutil.PadTo(genesisRoot[:], 32) + headRoot, err := headBlock.Block.HashTreeRoot() + require.NoError(t, err) + + hexKey := "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a" + key, err := hexutil.Decode(hexKey) + require.NoError(t, err) + require.NoError(t, headState.SetValidators([]*ethpb.Validator{ + { + PublicKey: key, + WithdrawalCredentials: make([]byte, fieldparams.RootLength), + }, + })) + require.NoError(t, headState.SetSlot(finalizedSlot)) + require.NoError(t, headState.SetFinalizedCheckpoint(ðpb.Checkpoint{ + Epoch: 123, + Root: headRoot[:], + })) + require.NoError(t, headState.SetGenesisValidatorsRoot(params.BeaconConfig().ZeroHash[:])) + + s, tr := minimalTestService(t, WithFinalizedStateAtStartUp(headState)) + ctx, beaconDB, stateGen := tr.ctx, tr.db, tr.sg + + require.NoError(t, beaconDB.SaveGenesisBlockRoot(ctx, genesisRoot)) + util.SaveBlock(t, ctx, beaconDB, genesis) + require.NoError(t, beaconDB.SaveState(ctx, headState, headRoot)) + require.NoError(t, beaconDB.SaveState(ctx, headState, genesisRoot)) + util.SaveBlock(t, ctx, beaconDB, headBlock) + require.NoError(t, beaconDB.SaveFinalizedCheckpoint(ctx, ðpb.Checkpoint{Epoch: slots.ToEpoch(finalizedSlot), Root: headRoot[:]})) + + require.NoError(t, err) + require.NoError(t, stateGen.SaveState(ctx, headRoot, headState)) + require.NoError(t, beaconDB.SaveLastValidatedCheckpoint(ctx, ðpb.Checkpoint{Epoch: slots.ToEpoch(finalizedSlot), Root: headRoot[:]})) + + notifier := &blockchainTesting.MockStateNotifier{RecordEvents: true} + s.cfg.StateNotifier = notifier + s.executePostFinalizationTasks(s.ctx, headState) + + time.Sleep(1 * time.Second) // sleep for a second because event is in a separate go routine + require.Equal(t, 1, len(notifier.ReceivedEvents())) + e := notifier.ReceivedEvents()[0] + assert.Equal(t, statefeed.FinalizedCheckpoint, int(e.Type)) + fc, ok := e.Data.(*ethpbv1.EventFinalizedCheckpoint) + require.Equal(t, true, ok, "event has wrong data type") + assert.Equal(t, primitives.Epoch(123), fc.Epoch) + assert.DeepEqual(t, headRoot[:], fc.Block) + assert.DeepEqual(t, finalizedStRoot[:], fc.State) + assert.Equal(t, false, fc.ExecutionOptimistic) + + // check the cache + index, ok := headState.ValidatorIndexByPubkey(bytesutil.ToBytes48(key)) + require.Equal(t, true, ok) + require.Equal(t, primitives.ValidatorIndex(0), index) // first index + + // check deposit + require.LogsContain(t, logHook, "Finalized deposit insertion completed at index") +} diff --git a/beacon-chain/blockchain/service.go b/beacon-chain/blockchain/service.go index 09d0cce7906d..52dea5958bb8 100644 --- a/beacon-chain/blockchain/service.go +++ b/beacon-chain/blockchain/service.go @@ -330,6 +330,8 @@ func (s *Service) StartFromSavedState(saved state.BeaconState) error { return errors.Wrap(err, "failed to initialize blockchain service") } + saved.SaveValidatorIndices() // used to handle Validator index invariant from EIP6110 + return nil } diff --git a/beacon-chain/blockchain/service_test.go b/beacon-chain/blockchain/service_test.go index 3eba33e170b7..73be76f80e4d 100644 --- a/beacon-chain/blockchain/service_test.go +++ b/beacon-chain/blockchain/service_test.go @@ -8,6 +8,7 @@ import ( "time" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" "github.com/prysmaticlabs/prysm/v5/beacon-chain/cache" "github.com/prysmaticlabs/prysm/v5/beacon-chain/cache/depositsnapshot" "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/altair" @@ -29,6 +30,7 @@ import ( "github.com/prysmaticlabs/prysm/v5/config/params" consensusblocks "github.com/prysmaticlabs/prysm/v5/consensus-types/blocks" "github.com/prysmaticlabs/prysm/v5/consensus-types/interfaces" + "github.com/prysmaticlabs/prysm/v5/consensus-types/primitives" "github.com/prysmaticlabs/prysm/v5/container/trie" "github.com/prysmaticlabs/prysm/v5/encoding/bytesutil" ethpb "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1" @@ -499,6 +501,66 @@ func TestChainService_EverythingOptimistic(t *testing.T) { require.Equal(t, true, op) } +func TestStartFromSavedState_ValidatorIndexCacheUpdated(t *testing.T) { + resetFn := features.InitWithReset(&features.Flags{ + EnableStartOptimistic: true, + EIP6110ValidatorIndexCache: true, + }) + defer resetFn() + + genesis := util.NewBeaconBlock() + genesisRoot, err := genesis.Block.HashTreeRoot() + require.NoError(t, err) + finalizedSlot := params.BeaconConfig().SlotsPerEpoch*2 + 1 + headBlock := util.NewBeaconBlock() + headBlock.Block.Slot = finalizedSlot + headBlock.Block.ParentRoot = bytesutil.PadTo(genesisRoot[:], 32) + headState, err := util.NewBeaconState() + require.NoError(t, err) + hexKey := "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a" + key, err := hexutil.Decode(hexKey) + require.NoError(t, err) + hexKey2 := "0x42247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a" + key2, err := hexutil.Decode(hexKey2) + require.NoError(t, err) + require.NoError(t, headState.SetValidators([]*ethpb.Validator{ + { + PublicKey: key, + WithdrawalCredentials: make([]byte, fieldparams.RootLength), + }, + { + PublicKey: key2, + WithdrawalCredentials: make([]byte, fieldparams.RootLength), + }, + })) + require.NoError(t, headState.SetSlot(finalizedSlot)) + require.NoError(t, headState.SetGenesisValidatorsRoot(params.BeaconConfig().ZeroHash[:])) + headRoot, err := headBlock.Block.HashTreeRoot() + require.NoError(t, err) + + c, tr := minimalTestService(t, WithFinalizedStateAtStartUp(headState)) + ctx, beaconDB, stateGen := tr.ctx, tr.db, tr.sg + + require.NoError(t, beaconDB.SaveGenesisBlockRoot(ctx, genesisRoot)) + util.SaveBlock(t, ctx, beaconDB, genesis) + require.NoError(t, beaconDB.SaveState(ctx, headState, headRoot)) + require.NoError(t, beaconDB.SaveState(ctx, headState, genesisRoot)) + util.SaveBlock(t, ctx, beaconDB, headBlock) + require.NoError(t, beaconDB.SaveFinalizedCheckpoint(ctx, ðpb.Checkpoint{Epoch: slots.ToEpoch(finalizedSlot), Root: headRoot[:]})) + + require.NoError(t, err) + require.NoError(t, stateGen.SaveState(ctx, headRoot, headState)) + require.NoError(t, beaconDB.SaveLastValidatedCheckpoint(ctx, ðpb.Checkpoint{Epoch: slots.ToEpoch(finalizedSlot), Root: headRoot[:]})) + require.NoError(t, c.StartFromSavedState(headState)) + + index, ok := headState.ValidatorIndexByPubkey(bytesutil.ToBytes48(key)) + require.Equal(t, true, ok) + require.Equal(t, primitives.ValidatorIndex(0), index) // first index + index2, ok := headState.ValidatorIndexByPubkey(bytesutil.ToBytes48(key2)) + require.Equal(t, true, ok) + require.Equal(t, primitives.ValidatorIndex(1), index2) // first index +} + // MockClockSetter satisfies the ClockSetter interface for testing the conditions where blockchain.Service should // call SetGenesis. type MockClockSetter struct { diff --git a/beacon-chain/state/state-native/setters_misc.go b/beacon-chain/state/state-native/setters_misc.go index 661aa070961c..77d346fb4fc9 100644 --- a/beacon-chain/state/state-native/setters_misc.go +++ b/beacon-chain/state/state-native/setters_misc.go @@ -117,6 +117,10 @@ func (b *BeaconState) SaveValidatorIndices() { b.lock.Lock() defer b.lock.Unlock() + if b.validatorIndexCache == nil { + b.validatorIndexCache = newFinalizedValidatorIndexCache() + } + b.saveValidatorIndices() }