From 0fb01c31a9492e4c401ab2d1e18dcf4bcf3ecaad Mon Sep 17 00:00:00 2001 From: Paul Harris Date: Mon, 29 Jan 2024 09:40:48 +1000 Subject: [PATCH] implement getStateForBlockProduction for late block reorg Currently late block reorg is disabled so this defers to `getStateAtSlotExact` This functionality diverges from `getStateAtSlotExact` with late block reorg, as sometimes the state we're wanting to use is the parent of the head. In this scenario we actually need to check the proposer head, and get the appropriate state based on that instead. The fallback given all things being equal currently is to call getStateAtSlotExact, which is basically falling back to the old functionality. - if late block reorg is disabled - if the head root can't be read - if the headRoot is the same as the root we're told to use from getProposerHead. In late block reorg case where we're using parent, we need to call `regenerateBeaconState` to apply empty slots. partially addresses #6595 Signed-off-by: Paul Harris --- .../coordinator/ValidatorApiHandler.java | 5 +- .../coordinator/ValidatorApiHandlerTest.java | 4 +- .../forkchoice/ForkChoiceTrigger.java | 4 + .../client/CombinedChainDataClient.java | 24 ++++++ .../client/CombinedChainDataClientTest.java | 77 +++++++++++++++++++ 5 files changed, 111 insertions(+), 3 deletions(-) diff --git a/beacon/validator/src/main/java/tech/pegasys/teku/validator/coordinator/ValidatorApiHandler.java b/beacon/validator/src/main/java/tech/pegasys/teku/validator/coordinator/ValidatorApiHandler.java index 593eabbb42e..198f3ebc81e 100644 --- a/beacon/validator/src/main/java/tech/pegasys/teku/validator/coordinator/ValidatorApiHandler.java +++ b/beacon/validator/src/main/java/tech/pegasys/teku/validator/coordinator/ValidatorApiHandler.java @@ -323,7 +323,10 @@ public SafeFuture> createUnsignedBlock( blockProductionPerformanceFactory.create(slot); return forkChoiceTrigger .prepareForBlockProduction(slot, blockProductionPerformance) - .thenCompose(__ -> combinedChainDataClient.getStateAtSlotExact(slot)) + .thenCompose( + __ -> + combinedChainDataClient.getStateForBlockProduction( + slot, forkChoiceTrigger.isForkChoiceOverrideLateBlockEnabled())) .thenPeek( maybeState -> { maybeState.ifPresent( diff --git a/beacon/validator/src/test/java/tech/pegasys/teku/validator/coordinator/ValidatorApiHandlerTest.java b/beacon/validator/src/test/java/tech/pegasys/teku/validator/coordinator/ValidatorApiHandlerTest.java index faf25e85b6b..a27339c89fd 100644 --- a/beacon/validator/src/test/java/tech/pegasys/teku/validator/coordinator/ValidatorApiHandlerTest.java +++ b/beacon/validator/src/test/java/tech/pegasys/teku/validator/coordinator/ValidatorApiHandlerTest.java @@ -500,7 +500,7 @@ public void createUnsignedBlock_shouldFailWhenNodeIsSyncing() { public void createUnsignedBlock_shouldFailWhenParentBlockIsOptimistic() { final UInt64 newSlot = UInt64.valueOf(25); final BeaconState blockSlotState = dataStructureUtil.randomBeaconState(newSlot); - when(chainDataClient.getStateAtSlotExact(newSlot)) + when(chainDataClient.getStateForBlockProduction(newSlot, false)) .thenReturn(SafeFuture.completedFuture(Optional.of(blockSlotState))); final Bytes32 parentRoot = spec.getBlockRootAtSlot(blockSlotState, newSlot.minus(1)); when(chainDataClient.isOptimisticBlock(parentRoot)).thenReturn(true); @@ -521,7 +521,7 @@ public void createUnsignedBlock_shouldCreateBlock() { final BLSSignature randaoReveal = dataStructureUtil.randomSignature(); final BeaconBlock createdBlock = dataStructureUtil.randomBeaconBlock(newSlot.longValue()); - when(chainDataClient.getStateAtSlotExact(newSlot)) + when(chainDataClient.getStateForBlockProduction(newSlot, false)) .thenReturn(SafeFuture.completedFuture(Optional.of(blockSlotState))); when(blockFactory.createUnsignedBlock( blockSlotState, diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/forkchoice/ForkChoiceTrigger.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/forkchoice/ForkChoiceTrigger.java index 9536490a024..204dfaa1e35 100644 --- a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/forkchoice/ForkChoiceTrigger.java +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/forkchoice/ForkChoiceTrigger.java @@ -40,6 +40,10 @@ public SafeFuture prepareForBlockProduction( return forkChoice.prepareForBlockProduction(slot, blockProductionPerformance); } + public boolean isForkChoiceOverrideLateBlockEnabled() { + return forkChoice.isForkChoiceLateBlockReorgEnabled(); + } + public SafeFuture prepareForAttestationProduction(final UInt64 slot) { return forkChoiceRatchet.ensureForkChoiceCompleteForSlot(slot); } diff --git a/storage/src/main/java/tech/pegasys/teku/storage/client/CombinedChainDataClient.java b/storage/src/main/java/tech/pegasys/teku/storage/client/CombinedChainDataClient.java index 2e2d0007ac2..6d328b35bc3 100644 --- a/storage/src/main/java/tech/pegasys/teku/storage/client/CombinedChainDataClient.java +++ b/storage/src/main/java/tech/pegasys/teku/storage/client/CombinedChainDataClient.java @@ -240,6 +240,30 @@ public SafeFuture> getStateAtSlotExact(final UInt64 slot) return regenerateStateAndSlotExact(slot); } + public SafeFuture> getStateForBlockProduction( + final UInt64 slot, final boolean isForkChoiceLateBlockReorgEnabled) { + if (!isForkChoiceLateBlockReorgEnabled) { + return getStateAtSlotExact(slot); + } + final Optional headRoot = getBestBlockRoot(); + if (headRoot.isEmpty()) { + return getStateAtSlotExact(slot); + } + final Bytes32 root = recentChainData.getProposerHead(headRoot.get(), slot); + if (root.equals(headRoot.get())) { + return getStateAtSlotExact(slot); + } + // otherwise we're looking for the parent slot + return getStateByBlockRoot(root) + .thenCompose( + maybeState -> + maybeState + .map( + beaconState -> + SafeFuture.completedFuture(regenerateBeaconState(beaconState, slot))) + .orElseGet(() -> getStateAtSlotExact(slot))); + } + public SafeFuture> getStateAtSlotExact( final UInt64 slot, final Bytes32 chainHead) { final Optional recentBlockRoot = diff --git a/storage/src/test/java/tech/pegasys/teku/storage/client/CombinedChainDataClientTest.java b/storage/src/test/java/tech/pegasys/teku/storage/client/CombinedChainDataClientTest.java index 8a09eeafcb4..a02c855fc0c 100644 --- a/storage/src/test/java/tech/pegasys/teku/storage/client/CombinedChainDataClientTest.java +++ b/storage/src/test/java/tech/pegasys/teku/storage/client/CombinedChainDataClientTest.java @@ -21,6 +21,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Optional; +import java.util.concurrent.ExecutionException; import org.apache.tuweni.bytes.Bytes32; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -40,6 +41,7 @@ import tech.pegasys.teku.spec.util.DataStructureUtil; import tech.pegasys.teku.storage.api.StorageQueryChannel; import tech.pegasys.teku.storage.protoarray.ForkChoiceStrategy; +import tech.pegasys.teku.storage.store.UpdatableStore; /** Note: Most tests should be added to the integration-test directory */ class CombinedChainDataClientTest { @@ -48,6 +50,8 @@ class CombinedChainDataClientTest { private final RecentChainData recentChainData = mock(RecentChainData.class); private final ForkChoiceStrategy forkChoiceStrategy = mock(ForkChoiceStrategy.class); private final StorageQueryChannel historicalChainData = mock(StorageQueryChannel.class); + + private final UpdatableStore store = mock(UpdatableStore.class); private final CombinedChainDataClient client = new CombinedChainDataClient( recentChainData, @@ -146,6 +150,79 @@ void getsEarliestAvailableBlobSidecarSlot() { assertThat(result).hasValue(UInt64.ONE); } + @Test + void getStateForBlockProduction_directsToStateAtSlotExact() + throws ExecutionException, InterruptedException { + final BeaconState state = dataStructureUtil.randomBeaconState(UInt64.valueOf(2)); + final Optional recentBlockRoot = + Optional.of(spec.getBlockRootAtSlot(state, UInt64.ONE)); + when(recentChainData.getBlockRootInEffectBySlot(UInt64.ONE)).thenReturn(recentBlockRoot); + when(recentChainData.getStore()).thenReturn(store); + when(store.retrieveStateAtSlot(any())) + .thenReturn(SafeFuture.completedFuture(Optional.of(state))); + final SafeFuture> future = + client.getStateForBlockProduction(UInt64.ONE, false); + assertThat(future.get()).contains(state); + } + + @Test + void getStateForBlockProduction_whenEnabeldAndHaveNoBestBlockRoot() + throws ExecutionException, InterruptedException { + final BeaconState state = dataStructureUtil.randomBeaconState(UInt64.valueOf(2)); + final Optional recentBlockRoot = + Optional.of(spec.getBlockRootAtSlot(state, UInt64.ONE)); + when(recentChainData.getStore()).thenReturn(store); + + when(recentChainData.getBestBlockRoot()).thenReturn(Optional.empty()); + when(recentChainData.getBlockRootInEffectBySlot(UInt64.ONE)).thenReturn(recentBlockRoot); + when(store.retrieveStateAtSlot(any())) + .thenReturn(SafeFuture.completedFuture(Optional.of(state))); + + final SafeFuture> future = + client.getStateForBlockProduction(UInt64.ONE, true); + assertThat(future.get()).contains(state); + } + + @Test + void getStateForBlockProduction_whenEnabeldAndBestBlockRootMatches() + throws ExecutionException, InterruptedException { + final BeaconState state = dataStructureUtil.randomBeaconState(UInt64.valueOf(2)); + final Optional recentBlockRoot = + Optional.of(spec.getBlockRootAtSlot(state, UInt64.ONE)); + when(recentChainData.getStore()).thenReturn(store); + + when(recentChainData.getBestBlockRoot()).thenReturn(recentBlockRoot); + when(recentChainData.getProposerHead(any(), any())).thenReturn(recentBlockRoot.get()); + when(recentChainData.getBlockRootInEffectBySlot(UInt64.ONE)).thenReturn(recentBlockRoot); + when(store.retrieveStateAtSlot(any())) + .thenReturn(SafeFuture.completedFuture(Optional.of(state))); + + final SafeFuture> future = + client.getStateForBlockProduction(UInt64.ONE, true); + assertThat(future.get()).contains(state); + } + + @Test + void getStateForBlockProduction_whenEnabledAndBestBlockRootDifferent() + throws ExecutionException, InterruptedException { + final BeaconState state = dataStructureUtil.randomBeaconState(UInt64.valueOf(2)); + final Bytes32 proposerHead = dataStructureUtil.randomBytes32(); + final BeaconState proposerState = dataStructureUtil.randomBeaconState(UInt64.ONE); + final Optional recentBlockRoot = + Optional.of(spec.getBlockRootAtSlot(state, UInt64.ONE)); + when(recentChainData.getStore()).thenReturn(store); + + when(recentChainData.getBestBlockRoot()).thenReturn(recentBlockRoot); + when(recentChainData.getProposerHead(any(), any())).thenReturn(proposerHead); + when(recentChainData.getBlockRootInEffectBySlot(UInt64.ONE)).thenReturn(recentBlockRoot); + when(store.retrieveBlockState(proposerHead)) + .thenReturn(SafeFuture.completedFuture(Optional.of(proposerState))); + + final SafeFuture> future = + client.getStateForBlockProduction(UInt64.ONE, true); + assertThat(future.get()).contains(proposerState); + } + @Test void getsBlobSidecarBySlotAndBlockRootAndBlobIndex() { final SlotAndBlockRootAndBlobIndex correctKey =