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 770ecbad932..eceb599460b 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 @@ -598,10 +598,8 @@ public void createAttestationData_shouldFailWhenHeadIsOptimistic() { final UInt64 slot = spec.computeStartSlotAtEpoch(EPOCH).plus(ONE); when(chainDataClient.getCurrentSlot()).thenReturn(slot); - final BeaconState state = createStateWithActiveValidators(epochStartSlot); - final SignedBeaconBlock block = - dataStructureUtil.randomSignedBeaconBlock(state.getSlot(), state); - final SignedBlockAndState blockAndState = new SignedBlockAndState(block, state); + final SignedBlockAndState blockAndState = + dataStructureUtil.randomSignedBlockAndState(epochStartSlot); final SafeFuture> blockAndStateResult = completedFuture(Optional.of(blockAndState)); @@ -624,10 +622,9 @@ public void createAttestationData_shouldCreateAttestation() { final UInt64 slot = spec.computeStartSlotAtEpoch(EPOCH).plus(ONE); when(chainDataClient.getCurrentSlot()).thenReturn(slot); - final BeaconState state = createStateWithActiveValidators(epochStartSlot); - final SignedBeaconBlock block = - dataStructureUtil.randomSignedBeaconBlock(state.getSlot(), state); - final SignedBlockAndState blockAndState = new SignedBlockAndState(block, state); + dataStructureUtil.randomBlockAndState(epochStartSlot); + final SignedBlockAndState blockAndState = + dataStructureUtil.randomSignedBlockAndState(epochStartSlot); final SafeFuture> blockAndStateResult = completedFuture(Optional.of(blockAndState)); @@ -646,7 +643,10 @@ public void createAttestationData_shouldCreateAttestation() { assertThat(attestationData) .isEqualTo( spec.getGenericAttestationData( - slot, state, block.getMessage(), UInt64.valueOf(committeeIndex))); + slot, + blockAndState.getState(), + blockAndState.getBlock().getMessage(), + UInt64.valueOf(committeeIndex))); assertThat(attestationData.getSlot()).isEqualTo(slot); final InOrder inOrder = inOrder(forkChoiceTrigger, chainDataClient); @@ -674,11 +674,10 @@ public void createAttestationData_shouldUseCorrectSourceWhenEpochTransitionRequi // Slot is from before the current epoch, so we need to ensure we process the epoch transition final UInt64 blockSlot = slot.minus(1); - final BeaconState wrongState = createStateWithActiveValidators(blockSlot); final BeaconState rightState = createStateWithActiveValidators(slot); - final SignedBeaconBlock block = - dataStructureUtil.randomSignedBeaconBlock(wrongState.getSlot(), wrongState); - final SignedBlockAndState blockAndState = new SignedBlockAndState(block, wrongState); + final SignedBlockAndState blockAndState = + dataStructureUtil.randomSignedBlockAndState(blockSlot); + final SignedBeaconBlock block = blockAndState.getBlock(); final SafeFuture> blockAndStateResult = completedFuture(Optional.of(blockAndState)); diff --git a/data/provider/src/main/java/tech/pegasys/teku/api/stateselector/StateSelectorFactory.java b/data/provider/src/main/java/tech/pegasys/teku/api/stateselector/StateSelectorFactory.java index d72b4993993..11eb8d3d668 100644 --- a/data/provider/src/main/java/tech/pegasys/teku/api/stateselector/StateSelectorFactory.java +++ b/data/provider/src/main/java/tech/pegasys/teku/api/stateselector/StateSelectorFactory.java @@ -80,19 +80,13 @@ public StateSelector genesisSelector() { @Override public StateSelector finalizedSelector() { return () -> - SafeFuture.completedFuture( - client - .getLatestFinalized() - .map( - finalized -> - addMetaData( - finalized.getState(), - // The finalized checkpoint may change because of optimistically - // imported blocks at the head and if the head isn't optimistic, the - // finalized block can't be optimistic. - client.isChainHeadOptimistic(), - true, - true))); + client + .getBestFinalizedState() + .thenApply( + maybeFinalized -> + maybeFinalized.map( + finalized -> + addMetaData(finalized, client.isChainHeadOptimistic(), true, true))); } @Override diff --git a/data/provider/src/test/java/tech/pegasys/teku/api/stateselector/StateSelectorFactoryTest.java b/data/provider/src/test/java/tech/pegasys/teku/api/stateselector/StateSelectorFactoryTest.java index be8ce53de44..62fec6da9a1 100644 --- a/data/provider/src/test/java/tech/pegasys/teku/api/stateselector/StateSelectorFactoryTest.java +++ b/data/provider/src/test/java/tech/pegasys/teku/api/stateselector/StateSelectorFactoryTest.java @@ -36,7 +36,6 @@ import tech.pegasys.teku.spec.datastructures.blocks.BeaconBlockHeader; import tech.pegasys.teku.spec.datastructures.blocks.SignedBlockAndState; import tech.pegasys.teku.spec.datastructures.metadata.StateAndMetaData; -import tech.pegasys.teku.spec.datastructures.state.AnchorPoint; import tech.pegasys.teku.spec.datastructures.state.beaconstate.BeaconState; import tech.pegasys.teku.spec.util.DataStructureUtil; import tech.pegasys.teku.storage.api.StorageQueryChannel; @@ -66,8 +65,7 @@ public void headSelector_shouldGetBestState() { @Test public void finalizedSelector_shouldGetFinalizedState() { - when(client.getLatestFinalized()) - .thenReturn(Optional.of(AnchorPoint.fromInitialState(spec, state))); + when(client.getBestFinalizedState()).thenReturn(SafeFuture.completedFuture(Optional.of(state))); Optional result = safeJoin(factory.finalizedSelector().getState()); assertThat(result).contains(withMetaData(state, true)); } diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/blocks/StateAndBlockSummary.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/blocks/StateAndBlockSummary.java index 5dbeabc20a8..a53969ec888 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/blocks/StateAndBlockSummary.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/blocks/StateAndBlockSummary.java @@ -33,9 +33,20 @@ public class StateAndBlockSummary implements BeaconBlockSummary { protected StateAndBlockSummary(final BeaconBlockSummary blockSummary, final BeaconState state) { checkNotNull(blockSummary); checkNotNull(state); - checkArgument( - blockSummary.getStateRoot().equals(state.hashTreeRoot()), - "Block state root must match the supplied state"); + final Bytes32 latestBlockHeaderBodyRoot = state.getLatestBlockHeader().getBodyRoot(); + // if the state slot is 0, we're either at genesis or testing + if (!state.getSlot().isZero()) { + // This check would allow a state to have an empty slot, and still be a valid block and state. + checkArgument( + latestBlockHeaderBodyRoot.equals(blockSummary.getBodyRoot()), + String.format( + "Latest Block body root %s in state at slot %s must match the block summary root. " + + "Block slot %s, block body root %s", + latestBlockHeaderBodyRoot, + state.getSlot(), + blockSummary.getSlot(), + blockSummary.getBodyRoot())); + } this.blockSummary = blockSummary; this.state = state; } @@ -128,7 +139,10 @@ public int hashCode() { @Override public String toString() { - return MoreObjects.toStringHelper(this).add("blockSummary", blockSummary).toString(); + return MoreObjects.toStringHelper(this) + .add("blockSummary", blockSummary) + .add("state", state) + .toString(); } private Optional getLatestExecutionPayloadHeader() { diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/state/AnchorPoint.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/state/AnchorPoint.java index 97ab43e0244..e257709acad 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/state/AnchorPoint.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/state/AnchorPoint.java @@ -33,6 +33,11 @@ /** * Represents an "anchor" - a trusted, finalized (block, state, checkpoint) tuple from which we can * sync. + * + *

NOTE: Anchor point is a teku concept.
+ * - state needs to be the anchor_state as used in fork choice
+ * - checkpoint is an actual checkpoint as defined in fork choice, is the first slot of epoch
+ * - blockSummary is the block from the finalized anchor state */ public class AnchorPoint extends StateAndBlockSummary { private final Spec spec; @@ -66,14 +71,6 @@ public static AnchorPoint create( return new AnchorPoint(spec, checkpoint, state, blockSummary); } - public static AnchorPoint create( - final Spec spec, - final Checkpoint checkpoint, - final SignedBeaconBlock block, - final BeaconState state) { - return new AnchorPoint(spec, checkpoint, state, block); - } - public static AnchorPoint create( final Spec spec, final Checkpoint checkpoint, final SignedBlockAndState blockAndState) { return new AnchorPoint(spec, checkpoint, blockAndState.getState(), blockAndState.getBlock()); @@ -107,16 +104,16 @@ public static AnchorPoint fromInitialState(final Spec spec, final BeaconState st } } - private static boolean isGenesisState(final BeaconState state) { - return state.getSlot().equals(SpecConfig.GENESIS_SLOT); - } - public static AnchorPoint fromInitialBlockAndState( final Spec spec, final SignedBlockAndState blockAndState) { return fromInitialBlockAndState(spec, blockAndState.getBlock(), blockAndState.getState()); } - public static AnchorPoint fromInitialBlockAndState( + private static boolean isGenesisState(final BeaconState state) { + return state.getSlot().equals(SpecConfig.GENESIS_SLOT); + } + + private static AnchorPoint fromInitialBlockAndState( final Spec spec, final SignedBeaconBlock block, final BeaconState state) { checkArgument( Objects.equals(block.getStateRoot(), state.hashTreeRoot()), diff --git a/ethereum/spec/src/testFixtures/java/tech/pegasys/teku/spec/datastructures/forkchoice/TestStoreImpl.java b/ethereum/spec/src/testFixtures/java/tech/pegasys/teku/spec/datastructures/forkchoice/TestStoreImpl.java index 38f733281ab..50526ec355e 100644 --- a/ethereum/spec/src/testFixtures/java/tech/pegasys/teku/spec/datastructures/forkchoice/TestStoreImpl.java +++ b/ethereum/spec/src/testFixtures/java/tech/pegasys/teku/spec/datastructures/forkchoice/TestStoreImpl.java @@ -124,7 +124,7 @@ public UInt64 getLatestFinalizedBlockSlot() { public AnchorPoint getLatestFinalized() { final SignedBeaconBlock block = getSignedBlock(finalizedCheckpoint.getRoot()); final BeaconState state = getBlockState(finalizedCheckpoint.getRoot()); - return AnchorPoint.create(spec, finalizedCheckpoint, block, state); + return AnchorPoint.create(spec, finalizedCheckpoint, state, Optional.of(block)); } @Override diff --git a/ethereum/spec/src/testFixtures/java/tech/pegasys/teku/spec/generator/BlockProposalTestUtil.java b/ethereum/spec/src/testFixtures/java/tech/pegasys/teku/spec/generator/BlockProposalTestUtil.java index e7cd6850ac3..6f3ed2358b4 100644 --- a/ethereum/spec/src/testFixtures/java/tech/pegasys/teku/spec/generator/BlockProposalTestUtil.java +++ b/ethereum/spec/src/testFixtures/java/tech/pegasys/teku/spec/generator/BlockProposalTestUtil.java @@ -34,6 +34,7 @@ import tech.pegasys.teku.spec.config.SpecConfig; import tech.pegasys.teku.spec.datastructures.blobs.versions.deneb.Blob; import tech.pegasys.teku.spec.datastructures.blocks.BeaconBlock; +import tech.pegasys.teku.spec.datastructures.blocks.BeaconBlockHeader; import tech.pegasys.teku.spec.datastructures.blocks.Eth1Data; import tech.pegasys.teku.spec.datastructures.blocks.SignedBeaconBlock; import tech.pegasys.teku.spec.datastructures.blocks.SignedBlockAndState; @@ -223,11 +224,14 @@ public SafeFuture createNewBlockSkippingStateTransition( blockBody); // Sign block and set block signature - BLSSignature blockSignature = signer.signBlock(block, state.getForkInfo()).join(); + final BLSSignature blockSignature = + signer.signBlock(block, state.getForkInfo()).join(); final SignedBeaconBlock signedBlock = SignedBeaconBlock.create(spec, block, blockSignature); - return new SignedBlockAndState(signedBlock, blockSlotState); + final BeaconState updatedState = + state.updated(w -> w.setLatestBlockHeader(BeaconBlockHeader.fromBlock(block))); + return new SignedBlockAndState(signedBlock, updatedState); }); } diff --git a/ethereum/spec/src/testFixtures/java/tech/pegasys/teku/spec/util/DataStructureUtil.java b/ethereum/spec/src/testFixtures/java/tech/pegasys/teku/spec/util/DataStructureUtil.java index 13349943cf1..c64ed60f946 100644 --- a/ethereum/spec/src/testFixtures/java/tech/pegasys/teku/spec/util/DataStructureUtil.java +++ b/ethereum/spec/src/testFixtures/java/tech/pegasys/teku/spec/util/DataStructureUtil.java @@ -966,7 +966,10 @@ public List randomSignedBlockAndStateSequence( final Bytes32 stateRoot = state.hashTreeRoot(); final SignedBeaconBlock block = signedBlock(randomBeaconBlock(nextSlot, parentRoot, stateRoot, full)); - blocks.add(new SignedBlockAndState(block, state)); + final BeaconState updatedState = + state.updated( + w -> w.setLatestBlockHeader(BeaconBlockHeader.fromBlock(block.getMessage()))); + blocks.add(new SignedBlockAndState(block, updatedState)); parentBlock = block; } return blocks; @@ -2027,8 +2030,11 @@ public AnchorPoint createAnchorFromState(final BeaconState anchorState) { final Bytes32 anchorRoot = anchorBlock.hashTreeRoot(); final UInt64 anchorEpoch = spec.getCurrentEpoch(anchorState); final Checkpoint anchorCheckpoint = new Checkpoint(anchorEpoch, anchorRoot); + final BeaconState updatedAnchorState = + anchorState.updated(w -> w.setLatestBlockHeader(BeaconBlockHeader.fromBlock(anchorBlock))); - return AnchorPoint.create(spec, anchorCheckpoint, signedAnchorBlock, anchorState); + return AnchorPoint.create( + spec, anchorCheckpoint, updatedAnchorState, Optional.of(signedAnchorBlock)); } public SignedContributionAndProof randomSignedContributionAndProof() { diff --git a/infrastructure/logging/src/main/java/tech/pegasys/teku/infrastructure/logging/StatusLogger.java b/infrastructure/logging/src/main/java/tech/pegasys/teku/infrastructure/logging/StatusLogger.java index 83fddd5a560..ee86cd6406d 100644 --- a/infrastructure/logging/src/main/java/tech/pegasys/teku/infrastructure/logging/StatusLogger.java +++ b/infrastructure/logging/src/main/java/tech/pegasys/teku/infrastructure/logging/StatusLogger.java @@ -325,12 +325,15 @@ public void loadedInitialStateResource( } } - public void errorIncompatibleInitialState(final UInt64 epoch) { - log.error( - "Cannot start with provided initial state for the epoch {}, " - + "checkpoint occurred on the empty slot, which is not yet supported.\n" - + "If you are using remote checkpoint source, " - + "please wait for the next epoch to finalize and retry.", + public void warningUnexpectedInitialState( + final UInt64 epoch, final UInt64 stateSlot, final UInt64 blockSlot) { + log.warn( + "Starting from state at slot {}, with block slot {}, which is not an anchor state. " + + "Ensure that slots from {} - {} are empty. At epoch {}.", + stateSlot, + blockSlot, + stateSlot, + blockSlot, epoch); } diff --git a/services/beaconchain/src/main/java/tech/pegasys/teku/services/beaconchain/WeakSubjectivityInitializer.java b/services/beaconchain/src/main/java/tech/pegasys/teku/services/beaconchain/WeakSubjectivityInitializer.java index 9520ca17711..a9f3c5b240c 100644 --- a/services/beaconchain/src/main/java/tech/pegasys/teku/services/beaconchain/WeakSubjectivityInitializer.java +++ b/services/beaconchain/src/main/java/tech/pegasys/teku/services/beaconchain/WeakSubjectivityInitializer.java @@ -13,7 +13,6 @@ package tech.pegasys.teku.services.beaconchain; -import static tech.pegasys.teku.infrastructure.exceptions.ExitConstants.ERROR_EXIT_CODE; import static tech.pegasys.teku.infrastructure.logging.StatusLogger.STATUS_LOG; import static tech.pegasys.teku.networks.Eth2NetworkConfiguration.FINALIZED_STATE_URL_PATH; @@ -88,8 +87,10 @@ private AnchorPoint getAnchorPoint( STATUS_LOG.loadingInitialStateResource(sanitizedResource); final BeaconState state = ChainDataLoader.loadState(spec, stateResource); if (state.getSlot().isGreaterThan(state.getLatestBlockHeader().getSlot())) { - STATUS_LOG.errorIncompatibleInitialState(spec.computeEpochAtSlot(state.getSlot())); - System.exit(ERROR_EXIT_CODE); + STATUS_LOG.warningUnexpectedInitialState( + spec.computeEpochAtSlot(state.getSlot()), + state.getSlot(), + state.getLatestBlockHeader().getSlot()); } final AnchorPoint anchor = AnchorPoint.fromInitialState(spec, state); STATUS_LOG.loadedInitialStateResource( 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 7836c60745c..a3f2eb4e75b 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 @@ -33,7 +33,6 @@ import tech.pegasys.teku.spec.datastructures.blobs.versions.deneb.BlobSidecar; import tech.pegasys.teku.spec.datastructures.blocks.BeaconBlockAndState; import tech.pegasys.teku.spec.datastructures.blocks.BeaconBlockSummary; -import tech.pegasys.teku.spec.datastructures.blocks.MinimalBeaconBlockSummary; import tech.pegasys.teku.spec.datastructures.blocks.SignedBeaconBlock; import tech.pegasys.teku.spec.datastructures.blocks.SignedBlockAndState; import tech.pegasys.teku.spec.datastructures.blocks.SlotAndBlockRoot; @@ -197,34 +196,6 @@ public SafeFuture> getAllBlobSidecars( .thenCompose(this::getAllBlobSidecars); } - private List filterBlobSidecars( - final List blobSidecars, final List indices) { - if (indices.isEmpty()) { - return blobSidecars; - } - return blobSidecars.stream().filter(key -> indices.contains(key.getIndex())).toList(); - } - - private Stream filterBlobSidecarKeys( - final List keys, final List indices) { - if (indices.isEmpty()) { - return keys.stream(); - } - return keys.stream().filter(key -> indices.contains(key.getBlobIndex())); - } - - private SafeFuture> getBlobSidecars( - final Stream keys) { - return SafeFuture.collectAll(keys.map(this::getAllBlobSidecarByKey)) - .thenApply(blobSidecars -> blobSidecars.stream().flatMap(Optional::stream).toList()); - } - - private SafeFuture> getAllBlobSidecars( - final Stream keys) { - return SafeFuture.collectAll(keys.map(this::getAllBlobSidecarByKey)) - .thenApply(blobSidecars -> blobSidecars.stream().flatMap(Optional::stream).toList()); - } - public SafeFuture> getStateAtSlotExact(final UInt64 slot) { final Optional recentBlockRoot = recentChainData.getBlockRootInEffectBySlot(slot); if (recentBlockRoot.isPresent()) { @@ -285,14 +256,6 @@ public SafeFuture> getStateAtSlotExact( } } - private SafeFuture> regenerateStateAndSlotExact(final UInt64 slot) { - return getBlockAndStateInEffectAtSlot(slot) - .thenApplyChecked( - maybeBlockAndState -> - maybeBlockAndState.flatMap( - blockAndState -> regenerateBeaconState(blockAndState.getState(), slot))); - } - public SafeFuture> getCheckpointStateAtEpoch(final UInt64 epoch) { final UInt64 epochSlot = spec.computeStartSlotAtEpoch(epoch); return getSignedBlockAndStateInEffectAtSlot(epochSlot) @@ -318,12 +281,6 @@ public SafeFuture getCheckpointState( }); } - private SafeFuture> getStateForBlock( - final SignedBeaconBlock block) { - return getStateByBlockRoot(block.getRoot()) - .thenApply(maybeState -> maybeState.map(state -> new SignedBlockAndState(block, state))); - } - public boolean isFinalized(final UInt64 slot) { final UInt64 finalizedEpoch = recentChainData.getFinalizedEpoch(); final UInt64 finalizedSlot = spec.computeStartSlotAtEpoch(finalizedEpoch); @@ -413,71 +370,10 @@ public SafeFuture> getStateByStateRoot(final Bytes32 state .orElseGet(() -> getFinalizedStateFromStateRoot(stateRoot))); } - private SafeFuture> getFinalizedStateFromStateRoot( - final Bytes32 stateRoot) { - return historicalChainData - .getFinalizedSlotByStateRoot(stateRoot) - .thenCompose( - maybeSlot -> maybeSlot.map(this::getStateAtSlotExact).orElse(STATE_NOT_AVAILABLE)); - } - public SafeFuture> getFinalizedSlotByBlockRoot(final Bytes32 blockRoot) { return historicalChainData.getFinalizedSlotByBlockRoot(blockRoot); } - private SafeFuture> getStateFromSlotAndBlock( - final SlotAndBlockRoot slotAndBlockRoot) { - final UpdatableStore store = getStore(); - if (store == null) { - LOG.trace( - "No state at slot and block root {} because the store is not set", slotAndBlockRoot); - return STATE_NOT_AVAILABLE; - } - return store - .retrieveStateAtSlot(slotAndBlockRoot) - .thenCompose( - maybeState -> { - if (maybeState.isPresent()) { - return SafeFuture.completedFuture(maybeState); - } - LOG.debug( - "State not found in store, querying historicalData for {}:{}", - slotAndBlockRoot::getSlot, - slotAndBlockRoot::getBlockRoot); - return getFinalizedStateFromSlotAndBlock(slotAndBlockRoot); - }); - } - - private SafeFuture> getFinalizedStateFromSlotAndBlock( - final SlotAndBlockRoot slotAndBlockRoot) { - return historicalChainData - .getFinalizedStateByBlockRoot(slotAndBlockRoot.getBlockRoot()) - .thenApply( - maybeState -> - maybeState.flatMap( - preState -> regenerateBeaconState(preState, slotAndBlockRoot.getSlot()))); - } - - private Optional regenerateBeaconState( - final BeaconState preState, final UInt64 slot) { - if (preState.getSlot().equals(slot)) { - return Optional.of(preState); - } else if (slot.compareTo(getCurrentSlot()) > 0) { - LOG.debug("Attempted to wind forward to a future state: {}", slot.toString()); - return Optional.empty(); - } - try { - LOG.debug( - "Processing slots to regenerate state from slot {}, target slot {}", - preState::getSlot, - () -> slot); - return Optional.of(spec.processSlots(preState, slot)); - } catch (SlotProcessingException | EpochProcessingException | IllegalArgumentException e) { - LOG.debug("State Transition error", e); - return Optional.empty(); - } - } - public Optional> getBestState() { return recentChainData.getBestState(); } @@ -486,10 +382,6 @@ public Optional getBestBlockRoot() { return recentChainData.getBestBlockRoot(); } - public Optional getBestBlock() { - return recentChainData.getHeadBlock(); - } - public Optional getChainHead() { return recentChainData.getChainHead(); } @@ -502,7 +394,7 @@ public boolean isStoreAvailable() { return recentChainData != null && recentChainData.getStore() != null; } - public boolean isChainDataFullyAvailable() { + private boolean isChainDataFullyAvailable() { return !recentChainData.isPreGenesis() && !recentChainData.isPreForkChoice(); } @@ -523,10 +415,6 @@ public List getCommitteesFromState( return result; } - public Optional getGenesisTime() { - return Optional.ofNullable(recentChainData.getGenesisTime()); - } - /** * @return The slot at which the chain head block was proposed */ @@ -662,6 +550,18 @@ public Optional getLatestFinalized() { return Optional.ofNullable(getStore()).map(ReadOnlyStore::getLatestFinalized); } + // in line with spec on_block, computing finalized slot as the + // first slot of the epoch stored in store.finalized_checkpoint + public SafeFuture> getBestFinalizedState() { + final Optional maybeFinalizedCheckpoint = getFinalizedCheckpoint(); + if (maybeFinalizedCheckpoint.isEmpty()) { + return SafeFuture.completedFuture(Optional.empty()); + } + final UInt64 finalizedSlot = + spec.computeStartSlotAtEpoch(maybeFinalizedCheckpoint.get().getEpoch()); + return getStateAtSlotExact(finalizedSlot); + } + public SafeFuture> getJustifiedState() { if (recentChainData.isPreGenesis()) { return SafeFuture.completedFuture(Optional.empty()); @@ -669,10 +569,6 @@ public SafeFuture> getJustifiedState() { return getStore().retrieveCheckpointState(getStore().getJustifiedCheckpoint()); } - public Optional getJustifiedCheckpoint() { - return Optional.ofNullable(getStore()).map(ReadOnlyStore::getJustifiedCheckpoint); - } - public Optional getGenesisData() { return recentChainData.getGenesisData(); } @@ -823,4 +719,111 @@ private boolean isOptimistic( public SafeFuture> getInitialAnchor() { return historicalChainData.getAnchor(); } + + private SafeFuture> getStateFromSlotAndBlock( + final SlotAndBlockRoot slotAndBlockRoot) { + final UpdatableStore store = getStore(); + if (store == null) { + LOG.trace( + "No state at slot and block root {} because the store is not set", slotAndBlockRoot); + return STATE_NOT_AVAILABLE; + } + return store + .retrieveStateAtSlot(slotAndBlockRoot) + .thenCompose( + maybeState -> { + if (maybeState.isPresent()) { + return SafeFuture.completedFuture(maybeState); + } + LOG.debug( + "State not found in store, querying historicalData for {}:{}", + slotAndBlockRoot::getSlot, + slotAndBlockRoot::getBlockRoot); + return getFinalizedStateFromSlotAndBlock(slotAndBlockRoot); + }); + } + + private SafeFuture> getFinalizedStateFromSlotAndBlock( + final SlotAndBlockRoot slotAndBlockRoot) { + return historicalChainData + .getFinalizedStateByBlockRoot(slotAndBlockRoot.getBlockRoot()) + .thenApply( + maybeState -> + maybeState.flatMap( + preState -> regenerateBeaconState(preState, slotAndBlockRoot.getSlot()))); + } + + private Optional regenerateBeaconState( + final BeaconState preState, final UInt64 slot) { + if (preState.getSlot().equals(slot)) { + return Optional.of(preState); + } else if (slot.compareTo(getCurrentSlot()) > 0) { + LOG.debug("Attempted to wind forward to a future state: {}", slot.toString()); + return Optional.empty(); + } + try { + LOG.debug( + "Processing slots to regenerate state from slot {}, target slot {}", + preState::getSlot, + () -> slot); + return Optional.of(spec.processSlots(preState, slot)); + } catch (SlotProcessingException | EpochProcessingException | IllegalArgumentException e) { + LOG.debug("State Transition error", e); + return Optional.empty(); + } + } + + private SafeFuture> getStateForBlock( + final SignedBeaconBlock block) { + return getStateByBlockRoot(block.getRoot()) + .thenApply(maybeState -> maybeState.map(state -> new SignedBlockAndState(block, state))); + } + + private SafeFuture> getFinalizedStateFromStateRoot( + final Bytes32 stateRoot) { + return historicalChainData + .getFinalizedSlotByStateRoot(stateRoot) + .thenCompose( + maybeSlot -> maybeSlot.map(this::getStateAtSlotExact).orElse(STATE_NOT_AVAILABLE)); + } + + private SafeFuture> regenerateStateAndSlotExact(final UInt64 slot) { + return getBlockAndStateInEffectAtSlot(slot) + .thenApplyChecked( + maybeBlockAndState -> + maybeBlockAndState.flatMap( + blockAndState -> regenerateBeaconState(blockAndState.getState(), slot))); + } + + private List filterBlobSidecars( + final List blobSidecars, final List indices) { + if (indices.isEmpty()) { + return blobSidecars; + } + return blobSidecars.stream().filter(key -> indices.contains(key.getIndex())).toList(); + } + + private Stream filterBlobSidecarKeys( + final List keys, final List indices) { + if (indices.isEmpty()) { + return keys.stream(); + } + return keys.stream().filter(key -> indices.contains(key.getBlobIndex())); + } + + private SafeFuture> getBlobSidecars( + final Stream keys) { + return SafeFuture.collectAll(keys.map(this::getAllBlobSidecarByKey)) + .thenApply(blobSidecars -> blobSidecars.stream().flatMap(Optional::stream).toList()); + } + + private SafeFuture> getAllBlobSidecars( + final Stream keys) { + return SafeFuture.collectAll(keys.map(this::getAllBlobSidecarByKey)) + .thenApply(blobSidecars -> blobSidecars.stream().flatMap(Optional::stream).toList()); + } + + private Optional getFinalizedCheckpoint() { + return Optional.ofNullable(getStore()).map(ReadOnlyStore::getFinalizedCheckpoint); + } } 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 f9169921db7..56d9f508858 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 @@ -17,6 +17,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -290,6 +291,33 @@ void getsBlobSidecarBySlotAndBlobIndex() { assertThat(incorrectResult).isEmpty(); } + @Test + void getBestFinalizedState_fetchesFinalizedState() + throws ExecutionException, InterruptedException { + final Checkpoint finalized = dataStructureUtil.randomCheckpoint(1024); + when(recentChainData.getStore()).thenReturn(store); + final UInt64 slot = spec.computeStartSlotAtEpoch(UInt64.valueOf(1024)); + final BeaconState state = dataStructureUtil.randomBeaconState(slot); + when(store.getFinalizedCheckpoint()).thenReturn(finalized); + when(recentChainData.getBlockRootInEffectBySlot(any())) + .thenReturn(Optional.of(state.getLatestBlockHeader().getBodyRoot())); + when(store.retrieveStateAtSlot(any())) + .thenReturn(SafeFuture.completedFuture(Optional.of(state))); + + final SafeFuture> maybeFinalizedState = client.getBestFinalizedState(); + // don't care about result, checking we called the right function with the right slot + + assertThat(maybeFinalizedState.get().orElseThrow().getSlot()).isEqualTo(slot); + verify(recentChainData, times(1)).getBlockRootInEffectBySlot(slot); + } + + @Test + void getBestFinalizedState_gracefulIfNotReady() throws ExecutionException, InterruptedException { + when(recentChainData.getStore()).thenReturn(null); + final SafeFuture> maybeFinalizedState = client.getBestFinalizedState(); + assertThat(maybeFinalizedState.get()).isEmpty(); + } + private void setupGetBlobSidecar( final SlotAndBlockRootAndBlobIndex key, final BlobSidecar result) { when(historicalChainData.getBlobSidecar(any())) diff --git a/storage/src/test/java/tech/pegasys/teku/storage/client/RecentChainDataTest.java b/storage/src/test/java/tech/pegasys/teku/storage/client/RecentChainDataTest.java index 9dd62c870a6..77d52944a23 100644 --- a/storage/src/test/java/tech/pegasys/teku/storage/client/RecentChainDataTest.java +++ b/storage/src/test/java/tech/pegasys/teku/storage/client/RecentChainDataTest.java @@ -206,6 +206,41 @@ public void initializeFromAnchorPoint_withTimeGreaterThanAnchorBlockTime() { assertThat(recentChainData.getStore().getTimeSeconds()).isEqualTo(time); } + @Test + void initializeFromAnchorPoint_withEmptySlotAtEndOfFinalizedEpoch() { + initPreGenesis(); + final ChainBuilder chainBuilder = ChainBuilder.create(spec); + final UInt64 genesisTime = UInt64.valueOf(5000); + chainBuilder.generateGenesis(genesisTime, true); + final List chain = chainBuilder.generateBlocksUpToSlot(15); + chainBuilder.generateBlockAtSlot(17); + final SignedBlockAndState anchor = chain.getLast(); + + final AnchorPoint anchorPoint = AnchorPoint.fromInitialState(spec, anchor.getState()); + final UInt64 anchorBlockTime = + anchorPoint.getBlockSlot().times(genesisSpecConfig.getSecondsPerSlot()).plus(genesisTime); + final UInt64 time = anchorBlockTime.plus(100); + recentChainData.initializeFromAnchorPoint(anchorPoint, time); + assertThat(recentChainData.getStore().getTimeSeconds()).isEqualTo(time); + } + + @Test + void initializeFromAnchorPoint_withEmptySlotAtStartOEpoch() { + initPreGenesis(); + final ChainBuilder chainBuilder = ChainBuilder.create(spec); + final UInt64 genesisTime = UInt64.valueOf(5000); + chainBuilder.generateGenesis(genesisTime, true); + chainBuilder.generateBlocksUpToSlot(15); + final SignedBlockAndState anchor = chainBuilder.generateBlockAtSlot(18); + + final AnchorPoint anchorPoint = AnchorPoint.fromInitialState(spec, anchor.getState()); + final UInt64 anchorBlockTime = + anchorPoint.getBlockSlot().times(genesisSpecConfig.getSecondsPerSlot()).plus(genesisTime); + final UInt64 time = anchorBlockTime.plus(100); + recentChainData.initializeFromAnchorPoint(anchorPoint, time); + assertThat(recentChainData.getStore().getTimeSeconds()).isEqualTo(time); + } + @Test void getGenesisData_shouldBeEmptyPreGenesis() { initPreGenesis();