From 9d084bceb3aeac583a32f58761bbbfc91bce5bdb Mon Sep 17 00:00:00 2001 From: Potuz Date: Sun, 5 Apr 2026 17:12:21 -0300 Subject: [PATCH] Fix finalized and justified state endpoint to not advance the slot (#16635) Use StateByRoot with the checkpoint root instead of replaying to the epoch start slot. The previous approach incorrectly advanced the state beyond the checkpoint block's post-state. Co-authored-by: Claude Opus 4.6 (1M context) --- beacon-chain/rpc/lookup/stater.go | 20 ++------------------ beacon-chain/rpc/lookup/stater_test.go | 18 ++++++++---------- changelog/potuz_finalized_state_endpoint.md | 2 ++ 3 files changed, 12 insertions(+), 28 deletions(-) create mode 100644 changelog/potuz_finalized_state_endpoint.md diff --git a/beacon-chain/rpc/lookup/stater.go b/beacon-chain/rpc/lookup/stater.go index 8a9964392796..4f3b9a456ff7 100644 --- a/beacon-chain/rpc/lookup/stater.go +++ b/beacon-chain/rpc/lookup/stater.go @@ -140,29 +140,13 @@ func (p *BeaconDbStater) State(ctx context.Context, stateId []byte) (state.Beaco } case "finalized": checkpoint := p.ChainInfoFetcher.FinalizedCheckpt() - targetSlot, err := slots.EpochStart(checkpoint.Epoch) - if err != nil { - return nil, errors.Wrap(err, "could not get start slot") - } - // We use the stategen replayer to fetch the finalized state and then - // replay it to the start slot of our checkpoint's epoch. The replayer - // only ever accesses our canonical history, so the state retrieved will - // always be the finalized state at that epoch. - s, err = p.ReplayerBuilder.ReplayerForSlot(targetSlot).ReplayToSlot(ctx, targetSlot) + s, err = p.StateGenService.StateByRoot(ctx, bytesutil.ToBytes32(checkpoint.Root)) if err != nil { return nil, errors.Wrap(err, "could not get finalized state") } case "justified": checkpoint := p.ChainInfoFetcher.CurrentJustifiedCheckpt() - targetSlot, err := slots.EpochStart(checkpoint.Epoch) - if err != nil { - return nil, errors.Wrap(err, "could not get start slot") - } - // We use the stategen replayer to fetch the justified state and then - // replay it to the start slot of our checkpoint's epoch. The replayer - // only ever accesses our canonical history, so the state retrieved will - // always be the justified state at that epoch. - s, err = p.ReplayerBuilder.ReplayerForSlot(targetSlot).ReplayToSlot(ctx, targetSlot) + s, err = p.StateGenService.StateByRoot(ctx, bytesutil.ToBytes32(checkpoint.Root)) if err != nil { return nil, errors.Wrap(err, "could not get justified state") } diff --git a/beacon-chain/rpc/lookup/stater_test.go b/beacon-chain/rpc/lookup/stater_test.go index a2ffa9140bc8..1b8c800b7a13 100644 --- a/beacon-chain/rpc/lookup/stater_test.go +++ b/beacon-chain/rpc/lookup/stater_test.go @@ -91,20 +91,20 @@ func TestGetState(t *testing.T) { }) t.Run("finalized", func(t *testing.T) { + // Use a block root distinct from the state root to verify + // we look up by checkpoint root, not by state root. + blockRoot := bytesutil.ToBytes32([]byte("finalized-block-root")) stateGen := mockstategen.NewService() - replayer := mockstategen.NewReplayerBuilder() - replayer.SetMockStateForSlot(newBeaconState, params.BeaconConfig().SlotsPerEpoch*10) - stateGen.StatesByRoot[stateRoot] = newBeaconState + stateGen.StatesByRoot[blockRoot] = newBeaconState p := BeaconDbStater{ ChainInfoFetcher: &chainMock.ChainService{ FinalizedCheckPoint: ðpb.Checkpoint{ - Root: stateRoot[:], + Root: blockRoot[:], Epoch: 10, }, }, StateGenService: stateGen, - ReplayerBuilder: replayer, } s, err := p.State(ctx, []byte("finalized")) @@ -115,20 +115,18 @@ func TestGetState(t *testing.T) { }) t.Run("justified", func(t *testing.T) { + blockRoot := bytesutil.ToBytes32([]byte("justified-block-root")) stateGen := mockstategen.NewService() - replayer := mockstategen.NewReplayerBuilder() - replayer.SetMockStateForSlot(newBeaconState, params.BeaconConfig().SlotsPerEpoch*10) - stateGen.StatesByRoot[stateRoot] = newBeaconState + stateGen.StatesByRoot[blockRoot] = newBeaconState p := BeaconDbStater{ ChainInfoFetcher: &chainMock.ChainService{ CurrentJustifiedCheckPoint: ðpb.Checkpoint{ - Root: stateRoot[:], + Root: blockRoot[:], Epoch: 10, }, }, StateGenService: stateGen, - ReplayerBuilder: replayer, } s, err := p.State(ctx, []byte("justified")) diff --git a/changelog/potuz_finalized_state_endpoint.md b/changelog/potuz_finalized_state_endpoint.md new file mode 100644 index 000000000000..4d170b1f3b5d --- /dev/null +++ b/changelog/potuz_finalized_state_endpoint.md @@ -0,0 +1,2 @@ +### Fixed +- Fixed finalized and justified state endpoint to not advance the slot.