diff --git a/CHANGELOG.md b/CHANGELOG.md index cf5b8029b84..47552dab18e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,3 +20,5 @@ the [releases page](https://github.com/Consensys/teku/releases). - Added hidden option `--Xp2p-dumps-to-file-enabled` to enable saving p2p dumps to file. ### Bug Fixes + +- Fixed a checkpoint sync issue where Teku couldn't start when the finalized state has been transitioned with empty slot(s) diff --git a/acceptance-tests/src/acceptance-test/java/tech/pegasys/teku/test/acceptance/TransitionedAnchorAcceptanceTest.java b/acceptance-tests/src/acceptance-test/java/tech/pegasys/teku/test/acceptance/TransitionedAnchorAcceptanceTest.java new file mode 100644 index 00000000000..e41c4035542 --- /dev/null +++ b/acceptance-tests/src/acceptance-test/java/tech/pegasys/teku/test/acceptance/TransitionedAnchorAcceptanceTest.java @@ -0,0 +1,143 @@ +/* + * Copyright Consensys Software Inc., 2022 + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package tech.pegasys.teku.test.acceptance; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.google.common.io.Resources; +import java.net.URL; +import java.security.SecureRandom; +import java.util.List; +import java.util.stream.IntStream; +import org.apache.tuweni.bytes.Bytes32; +import org.junit.jupiter.api.Test; +import tech.pegasys.teku.api.schema.bellatrix.SignedBeaconBlockBellatrix; +import tech.pegasys.teku.bls.BLSKeyPair; +import tech.pegasys.teku.infrastructure.bytes.Bytes20; +import tech.pegasys.teku.infrastructure.unsigned.UInt64; +import tech.pegasys.teku.spec.datastructures.state.beaconstate.BeaconState; +import tech.pegasys.teku.test.acceptance.dsl.AcceptanceTestBase; +import tech.pegasys.teku.test.acceptance.dsl.GenesisGenerator.InitialStateData; +import tech.pegasys.teku.test.acceptance.dsl.TekuBeaconNode; +import tech.pegasys.teku.test.acceptance.dsl.TekuNodeConfigBuilder; +import tech.pegasys.teku.test.acceptance.dsl.tools.deposits.ValidatorKeys; +import tech.pegasys.teku.test.acceptance.dsl.tools.deposits.ValidatorKeystores; + +public class TransitionedAnchorAcceptanceTest extends AcceptanceTestBase { + private static final URL JWT_FILE = Resources.getResource("auth/ee-jwt-secret.hex"); + // SEED is chosen to have empty slots in the end of 3rd epoch. + // TODO: Good to find one with earlier end epoch empty slots + private static final byte[] SEED = new byte[] {0x11, 0x03, 0x04}; + + @Test + @SuppressWarnings("DoNotCreateSecureRandomDirectly") + void shouldMaintainValidatorsInMutableClient() throws Exception { + final SecureRandom rnd = SecureRandom.getInstance("SHA1PRNG"); + rnd.setSeed(SEED); + final String networkName = "swift"; + + final List node1Validators = + IntStream.range(0, 16).mapToObj(__ -> BLSKeyPair.random(rnd)).toList(); + final List node2Validators = + IntStream.range(0, 3).mapToObj(__ -> BLSKeyPair.random(rnd)).toList(); + + final ValidatorKeystores node1ValidatorKeystores = + new ValidatorKeystores( + node1Validators.stream() + .map(keyPair -> new ValidatorKeys(keyPair, Bytes32.random(rnd), false)) + .toList()); + // will be non-active in the beginning + final ValidatorKeystores node2ValidatorKeystores = + new ValidatorKeystores( + node2Validators.stream() + .map(keyPair -> new ValidatorKeys(keyPair, Bytes32.random(rnd), false)) + .toList()); + + final InitialStateData genesis = + createGenesisGenerator() + .network(networkName) + .withAltairEpoch(UInt64.ZERO) + .withBellatrixEpoch(UInt64.ZERO) + .validatorKeys(node1ValidatorKeystores, node2ValidatorKeystores) + .generate(); + + final String node1FeeRecipient = "0xFE3B557E8Fb62b89F4916B721be55cEb828dBd73"; + final TekuBeaconNode beaconNode1 = + createTekuBeaconNode( + TekuNodeConfigBuilder.createBeaconNode() + .withStubExecutionEngine() + .withJwtSecretFile(JWT_FILE) + .withNetwork(networkName) + .withRealNetwork() + .withInitialState(genesis) + .withAltairEpoch(UInt64.ZERO) + .withBellatrixEpoch(UInt64.ZERO) + .withValidatorProposerDefaultFeeRecipient(node1FeeRecipient) + .withReadOnlyKeystorePath(node1ValidatorKeystores) + .build()); + + beaconNode1.start(); + beaconNode1.waitForFinalizationAfter(UInt64.valueOf(3)); + + final BeaconState finalizedState = beaconNode1.fetchFinalizedState(); + final UInt64 finalizedSlot = finalizedState.getSlot(); + final UInt64 finalizedEpoch = beaconNode1.getSpec().computeEpochAtSlot(finalizedSlot); + final UInt64 finalizedEpochFirstSlot = + beaconNode1.getSpec().computeStartSlotAtEpoch(finalizedEpoch); + assertNotEquals(finalizedSlot, finalizedEpochFirstSlot); + final UInt64 maxFinalizedSlot = + beaconNode1.getSpec().computeStartSlotAtEpoch(finalizedEpoch.plus(1)); + System.out.println( + "State slot: " + + finalizedState.getSlot() + + ", maxSlot: " + + maxFinalizedSlot + + ", finalized Epoch: " + + finalizedEpoch); + assertTrue(finalizedState.getSlot().isLessThan(maxFinalizedSlot)); + final BeaconState transitionedFinalizedState = + beaconNode1.getSpec().processSlots(finalizedState, maxFinalizedSlot); + + final String node2FeeRecipient = "0xfe3b557E8Fb62B89F4916B721be55ceB828DBd55"; + final TekuBeaconNode beaconNode2 = + createTekuBeaconNode( + TekuNodeConfigBuilder.createBeaconNode() + .withStubExecutionEngine() + .withJwtSecretFile(JWT_FILE) + .withNetwork(networkName) + .withRealNetwork() + .withInitialState(new InitialStateData(transitionedFinalizedState)) + .withAltairEpoch(UInt64.ZERO) + .withBellatrixEpoch(UInt64.ZERO) + .withValidatorProposerDefaultFeeRecipient(node2FeeRecipient) + .withReadOnlyKeystorePath(node2ValidatorKeystores) + .withPeers(beaconNode1) + .build()); + + beaconNode2.start(); + beaconNode2.waitUntilInSyncWith(beaconNode1); + beaconNode2.waitForBlockSatisfying( + block -> { + assertThat(block).isInstanceOf(SignedBeaconBlockBellatrix.class); + final SignedBeaconBlockBellatrix bellatrixBlock = (SignedBeaconBlockBellatrix) block; + assertEquals( + Bytes20.fromHexString(node2FeeRecipient), + bellatrixBlock.getMessage().getBody().executionPayload.feeRecipient); + }); + beaconNode2.waitForNewFinalization(); + } +} diff --git a/acceptance-tests/src/testFixtures/java/tech/pegasys/teku/test/acceptance/dsl/TekuBeaconNode.java b/acceptance-tests/src/testFixtures/java/tech/pegasys/teku/test/acceptance/dsl/TekuBeaconNode.java index cec4c8294f7..1574606dc47 100644 --- a/acceptance-tests/src/testFixtures/java/tech/pegasys/teku/test/acceptance/dsl/TekuBeaconNode.java +++ b/acceptance-tests/src/testFixtures/java/tech/pegasys/teku/test/acceptance/dsl/TekuBeaconNode.java @@ -456,6 +456,16 @@ public void waitForNewFinalization() { MINUTES); } + public void waitForFinalizationAfter(final UInt64 newFinalizedEpoch) { + LOG.debug("Wait for finalized block"); + waitFor( + () -> + assertThat(fetchStateFinalityCheckpoints().orElseThrow().finalized.epoch) + .isGreaterThanOrEqualTo(newFinalizedEpoch), + 9, + MINUTES); + } + public void waitForMilestone(final SpecMilestone expectedMilestone) { waitForLogMessageContaining("Activating network upgrade: " + expectedMilestone.name()); } @@ -779,4 +789,13 @@ public void expectElOffline() throws IOException { public void expectElOnline() throws IOException { assertThat(fetchSyncStatus().data.elOffline).isFalse(); } + + public BeaconState fetchFinalizedState() throws IOException { + final Bytes beaconStateBytes = + httpClient.getAsBytes( + getRestApiUrl(), + "eth/v2/debug/beacon/states/finalized", + Map.of("Accept", "application/octet-stream")); + return spec.deserializeBeaconState(Bytes.wrap(beaconStateBytes)); + } } diff --git a/beacon/validator/src/test/java/tech/pegasys/teku/validator/coordinator/DepositProviderTest.java b/beacon/validator/src/test/java/tech/pegasys/teku/validator/coordinator/DepositProviderTest.java index 47d27a2c759..2fed118cd78 100644 --- a/beacon/validator/src/test/java/tech/pegasys/teku/validator/coordinator/DepositProviderTest.java +++ b/beacon/validator/src/test/java/tech/pegasys/teku/validator/coordinator/DepositProviderTest.java @@ -482,6 +482,33 @@ void whenCallingForFinalizedSnapshotAndSnapshotAvailable_SnapshotReturned() { .isEqualTo(UInt64.valueOf(30)); } + @Test + void whenUsingTransitionedAnchorPoint_FinalizationIsNotFailed() throws Exception { + setup(16); + Bytes32 finalizedBlockRoot = Bytes32.fromHexString("0x01"); + updateStateEth1DepositIndex(10); + updateStateEth1DataDepositCount(10); + mockDepositsFromEth1Block(0, 20); + final AnchorPoint anchorPoint = mock(AnchorPoint.class); + final UpdatableStore store = mock(UpdatableStore.class); + when(recentChainData.getStore()).thenReturn(store); + when(store.getLatestFinalized()).thenReturn(anchorPoint); + final BeaconState transitionedState = spec.processSlots(state, state.getSlot().plus(2)); + when(anchorPoint.getState()).thenAnswer(__ -> transitionedState); + when(eth1DataCache.getEth1DataAndHeight(eq(transitionedState.getEth1Data()))) + .thenReturn(Optional.empty()); + + assertThat(depositProvider.getDepositMapSize()).isEqualTo(20); + + depositProvider.onNewFinalizedCheckpoint(new Checkpoint(UInt64.ONE, finalizedBlockRoot), false); + + assertThat(depositProvider.getDepositMapSize()).isEqualTo(10); + List availableDeposits = depositProvider.getAvailableDeposits(); + assertThat(availableDeposits.size()).isEqualTo(10); + + verify(eth1DataCache).getEth1DataAndHeight(eq(transitionedState.getEth1Data())); + } + private void checkThatDepositProofIsValid(SszList deposits) { final SpecVersion genesisSpec = spec.getGenesisSpec(); deposits.forEach( diff --git a/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/tekuv1/beacon/GetStateByBlockRoot.java b/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/tekuv1/beacon/GetStateByBlockRoot.java index 1303ad2454d..6ad3c6392e5 100644 --- a/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/tekuv1/beacon/GetStateByBlockRoot.java +++ b/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/tekuv1/beacon/GetStateByBlockRoot.java @@ -69,6 +69,7 @@ public void handleRequest(RestApiRequest request) throws JsonProcessingException request.header(Header.CACHE_CONTROL, CACHE_NONE); final String blockId = request.getPathParameter(PARAMETER_BLOCK_ID); + // FIXME: not sure. Are we ok to return here transitioned state on keyword finalized? SafeFuture> future = chainDataProvider.getBeaconStateByBlockId(blockId); request.respondAsync( diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/Spec.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/Spec.java index 2638f8b21f7..ad4699ab8cf 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/Spec.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/Spec.java @@ -424,6 +424,10 @@ public UInt64 computeStartSlotAtEpoch(final UInt64 epoch) { return atEpoch(epoch).miscHelpers().computeStartSlotAtEpoch(epoch); } + public UInt64 computeEndSlotAtEpoch(final UInt64 epoch) { + return atEpoch(epoch).miscHelpers().computeEndSlotAtEpoch(epoch); + } + public UInt64 computeEpochAtSlot(final UInt64 slot) { return atSlot(slot).miscHelpers().computeEpochAtSlot(slot); } 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..6a40e3058f6 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,11 +33,15 @@ public class StateAndBlockSummary implements BeaconBlockSummary { protected StateAndBlockSummary(final BeaconBlockSummary blockSummary, final BeaconState state) { checkNotNull(blockSummary); checkNotNull(state); + this.blockSummary = blockSummary; + this.state = state; + verifyStateAndBlockConsistency(); + } + + protected void verifyStateAndBlockConsistency() { checkArgument( blockSummary.getStateRoot().equals(state.hashTreeRoot()), "Block state root must match the supplied state"); - this.blockSummary = blockSummary; - this.state = state; } public static StateAndBlockSummary create(final BeaconState state) { 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 69046334b66..959f0e92b5f 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 @@ -38,6 +38,7 @@ public class AnchorPoint extends StateAndBlockSummary { private final Spec spec; private final Checkpoint checkpoint; private final boolean isGenesis; + private final boolean isTransitioned; private AnchorPoint( final Spec spec, @@ -45,15 +46,11 @@ private AnchorPoint( final BeaconState state, final BeaconBlockSummary blockSummary) { super(blockSummary, state); - checkArgument( - checkpoint.getRoot().equals(blockSummary.getRoot()), "Checkpoint and block must match"); - checkArgument( - checkpoint.getEpochStartSlot(spec).isGreaterThanOrEqualTo(blockSummary.getSlot()), - "Block must be at or prior to the start of the checkpoint epoch"); - this.spec = spec; this.checkpoint = checkpoint; this.isGenesis = checkpoint.getEpoch().equals(SpecConfig.GENESIS_EPOCH); + this.isTransitioned = state.getSlot().isGreaterThan(blockSummary.getSlot()); + verifyAnchor(); } public static AnchorPoint create( @@ -126,6 +123,42 @@ public static AnchorPoint fromInitialBlockAndState( return new AnchorPoint(spec, checkpoint, state, block); } + /** + * Skipping verification in the super class. All checks are made in {@link #verifyAnchor()} + * instead + */ + @Override + protected void verifyStateAndBlockConsistency() {} + + private void verifyAnchor() { + final UInt64 blockSlot = blockSummary.getSlot(); + if (state.getSlot().isGreaterThan(blockSlot)) { + // the finalized state is transitioned with empty slot(s) + checkArgument( + blockSummary.getStateRoot().equals(state.getLatestBlockHeader().getStateRoot()), + "Block state root must match the latest block header state root in the state"); + final int stateAndBlockRootsIndex = + blockSlot.mod(spec.getSlotsPerHistoricalRoot(blockSlot)).intValue(); + checkArgument( + blockSummary + .getStateRoot() + .equals(state.getStateRoots().get(stateAndBlockRootsIndex).get()), + "Block state root must match the state root for the block slot %s in the state roots", + blockSlot); + checkArgument( + blockSummary.getRoot().equals(state.getBlockRoots().get(stateAndBlockRootsIndex).get()), + "Block root must match the root for the block slot %s in the block roots in the state", + blockSlot); + } else { + super.verifyStateAndBlockConsistency(); + } + checkArgument( + checkpoint.getRoot().equals(blockSummary.getRoot()), "Checkpoint and block must match"); + checkArgument( + checkpoint.getEpochStartSlot(spec).isGreaterThanOrEqualTo(blockSlot), + "Block must be at or prior to the start of the checkpoint epoch"); + } + public boolean isGenesis() { return isGenesis; } @@ -146,6 +179,10 @@ public UInt64 getEpochStartSlot() { return checkpoint.getEpochStartSlot(spec); } + public boolean isTransitioned() { + return isTransitioned; + } + @Override public boolean equals(final Object o) { if (this == o) { diff --git a/ethereum/spec/src/test/java/tech/pegasys/teku/spec/datastructures/state/AnchorPointTest.java b/ethereum/spec/src/test/java/tech/pegasys/teku/spec/datastructures/state/AnchorPointTest.java index e7b5dd7c6f6..aed77ec2d19 100644 --- a/ethereum/spec/src/test/java/tech/pegasys/teku/spec/datastructures/state/AnchorPointTest.java +++ b/ethereum/spec/src/test/java/tech/pegasys/teku/spec/datastructures/state/AnchorPointTest.java @@ -13,6 +13,7 @@ package tech.pegasys.teku.spec.datastructures.state; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; import java.util.Optional; @@ -21,11 +22,19 @@ import tech.pegasys.teku.spec.Spec; import tech.pegasys.teku.spec.TestSpecFactory; import tech.pegasys.teku.spec.datastructures.blocks.BeaconBlockAndState; +import tech.pegasys.teku.spec.datastructures.blocks.SignedBlockAndState; +import tech.pegasys.teku.spec.datastructures.state.beaconstate.BeaconState; +import tech.pegasys.teku.spec.logic.common.statetransition.exceptions.EpochProcessingException; +import tech.pegasys.teku.spec.logic.common.statetransition.exceptions.SlotProcessingException; import tech.pegasys.teku.spec.util.DataStructureUtil; +import tech.pegasys.teku.storage.storageSystem.InMemoryStorageSystemBuilder; +import tech.pegasys.teku.storage.storageSystem.StorageSystem; public class AnchorPointTest { private final Spec spec = TestSpecFactory.createDefault(); private final DataStructureUtil dataStructureUtil = new DataStructureUtil(spec); + private final StorageSystem storageSystem = + InMemoryStorageSystemBuilder.create().specProvider(spec).build(); @Test public void create_withCheckpointPriorToState() { @@ -41,4 +50,30 @@ public void create_withCheckpointPriorToState() { .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("Block must be at or prior to the start of the checkpoint epoch"); } + + @Test + public void createFromInitialState_WhenFinalizedStateTransitionedWithAnEmptySlot() + throws SlotProcessingException, EpochProcessingException { + storageSystem.chainUpdater().initializeGenesis(); + + final UInt64 latestBlockHeaderEpoch = UInt64.valueOf(4); + final UInt64 latestBlockHeaderSlot = spec.computeEndSlotAtEpoch(latestBlockHeaderEpoch); + final SignedBlockAndState blockAndState = + storageSystem.chainUpdater().advanceChainUntil(latestBlockHeaderSlot); + + // empty slot transition + final BeaconState postState = + spec.processSlots(blockAndState.getState(), latestBlockHeaderSlot.increment()); + + final AnchorPoint anchor = AnchorPoint.fromInitialState(spec, postState); + + // verify finalized anchor + assertThat(anchor.getBlockSlot()).isEqualTo(latestBlockHeaderSlot); + assertThat(anchor.getStateRoot()).isEqualTo(blockAndState.getBlock().getStateRoot()); + final Checkpoint expectedCheckpoint = + new Checkpoint( + latestBlockHeaderEpoch.plus(1), + blockAndState.getBlock().asHeader().getMessage().hashTreeRoot()); + assertThat(anchor.getCheckpoint()).isEqualTo(expectedCheckpoint); + } } diff --git a/ethereum/spec/src/test/java/tech/pegasys/teku/spec/logic/versions/bellatrix/helpers/MiscHelpersBellatrixTest.java b/ethereum/spec/src/test/java/tech/pegasys/teku/spec/logic/versions/bellatrix/helpers/MiscHelpersBellatrixTest.java new file mode 100644 index 00000000000..d6bf06fb1a5 --- /dev/null +++ b/ethereum/spec/src/test/java/tech/pegasys/teku/spec/logic/versions/bellatrix/helpers/MiscHelpersBellatrixTest.java @@ -0,0 +1,59 @@ +/* + * Copyright Consensys Software Inc., 2022 + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package tech.pegasys.teku.spec.logic.versions.bellatrix.helpers; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; +import tech.pegasys.teku.spec.Spec; +import tech.pegasys.teku.spec.TestSpecFactory; +import tech.pegasys.teku.spec.datastructures.execution.ExecutionPayloadHeader; +import tech.pegasys.teku.spec.datastructures.state.beaconstate.BeaconState; +import tech.pegasys.teku.spec.datastructures.state.beaconstate.versions.bellatrix.BeaconStateBellatrix; +import tech.pegasys.teku.spec.schemas.SchemaDefinitionsBellatrix; +import tech.pegasys.teku.spec.util.DataStructureUtil; + +class MiscHelpersBellatrixTest { + private final Spec spec = TestSpecFactory.createMinimalBellatrix(); + private final MiscHelpersBellatrix miscHelpersBellatrix = + new MiscHelpersBellatrix(spec.getGenesisSpecConfig().toVersionBellatrix().orElseThrow()); + private final DataStructureUtil dataStructureUtil = new DataStructureUtil(spec); + + @Test + public void shouldHaveCorrectTransitionCompleteWithTransitionedState() throws Exception { + final ExecutionPayloadHeader defaultPayloadHeader = + SchemaDefinitionsBellatrix.required(spec.getGenesisSchemaDefinitions()) + .getExecutionPayloadHeaderSchema() + .getDefault(); + final BeaconStateBellatrix preMerge = + dataStructureUtil + .stateBuilderBellatrix(32, 5) + .latestExecutionPayloadHeader(defaultPayloadHeader) + .build(); + final BeaconStateBellatrix postMerge = + dataStructureUtil + .stateBuilderBellatrix(32, 5) + .latestExecutionPayloadHeader(dataStructureUtil.randomExecutionPayloadHeader()) + .build(); + final BeaconState preMergeTransitioned = + spec.processSlots(preMerge, preMerge.getSlot().plus(2)); + final BeaconState postMergeTransitioned = + spec.processSlots(postMerge, postMerge.getSlot().plus(2)); + + assertThat(miscHelpersBellatrix.isMergeTransitionComplete(preMerge)).isFalse(); + assertThat(miscHelpersBellatrix.isMergeTransitionComplete(preMergeTransitioned)).isFalse(); + assertThat(miscHelpersBellatrix.isMergeTransitionComplete(postMerge)).isTrue(); + assertThat(miscHelpersBellatrix.isMergeTransitionComplete(postMergeTransitioned)).isTrue(); + } +} diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/block/BlockImporter.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/block/BlockImporter.java index ae1b8b794fa..e78a10f2c14 100644 --- a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/block/BlockImporter.java +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/block/BlockImporter.java @@ -172,9 +172,8 @@ SafeFuture getLatestCheckpointState() { if (finalizedCheckpoint != null && recentChainData .getStore() - .getLatestFinalized() - .getRoot() - .equals(finalizedCheckpoint.getRoot())) { + .getFinalizedCheckpoint() + .equals(finalizedCheckpoint.getCheckpoint())) { return SafeFuture.completedFuture(finalizedCheckpoint); } diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/validation/AttestationStateSelector.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/validation/AttestationStateSelector.java index 1304931c687..4988d368613 100644 --- a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/validation/AttestationStateSelector.java +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/validation/AttestationStateSelector.java @@ -88,6 +88,7 @@ public SafeFuture> getStateToValidate( // If the attestation is within the lookahead period for the finalized state, use that // If the target block doesn't descend from finalized the attestation is invalid final BeaconState finalizedState = recentChainData.getStore().getLatestFinalized().getState(); + // TODO: Test if (finalizedState.getSlot().isGreaterThanOrEqualTo(earliestSlot)) { appliedSelectorRule.labels("attestation_within_lookahead").inc(); return completedFuture(Optional.of(finalizedState)); 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 dab59faf64c..c2d519d5df6 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,21 +325,12 @@ 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.", - epoch); - } - public void warnInitialStateIgnored() { log.warn("Not loading specified initial state as chain data already exists."); } - public void warnFailedToLoadInitialState(final String message) { - log.warn(message); + public void warnFailedToLoadInitialState(final Throwable throwable) { + log.warn("Failed to load initial state", throwable); } public void warnOnInitialStateWithSkippedSlots( diff --git a/services/beaconchain/src/main/java/tech/pegasys/teku/services/beaconchain/BeaconChainController.java b/services/beaconchain/src/main/java/tech/pegasys/teku/services/beaconchain/BeaconChainController.java index d950b816fb7..c35b594444d 100644 --- a/services/beaconchain/src/main/java/tech/pegasys/teku/services/beaconchain/BeaconChainController.java +++ b/services/beaconchain/src/main/java/tech/pegasys/teku/services/beaconchain/BeaconChainController.java @@ -1409,14 +1409,14 @@ private Optional tryLoadingAnchorPointFromInitialState( initialAnchor = attemptToLoadAnchorPoint( networkConfiguration.getNetworkBoostrapConfig().getInitialState()); - } catch (final InvalidConfigurationException e) { + } catch (final InvalidConfigurationException ex) { final StateBoostrapConfig stateBoostrapConfig = networkConfiguration.getNetworkBoostrapConfig(); if (stateBoostrapConfig.isUsingCustomInitialState() && !stateBoostrapConfig.isUsingCheckpointSync()) { - throw e; + throw ex; } - STATUS_LOG.warnFailedToLoadInitialState(e.getMessage()); + STATUS_LOG.warnFailedToLoadInitialState(ex); } return initialAnchor; 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 e1d0c9d35a7..564e23d7eec 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; @@ -86,10 +85,6 @@ private AnchorPoint getAnchorPoint(Spec spec, String stateResource, String sanit throws IOException { 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); - } final AnchorPoint anchor = AnchorPoint.fromInitialState(spec, state); STATUS_LOG.loadedInitialStateResource( state.hashTreeRoot(), diff --git a/storage/api/src/main/java/tech/pegasys/teku/storage/api/FinalizedChainData.java b/storage/api/src/main/java/tech/pegasys/teku/storage/api/FinalizedChainData.java index 83ba9723161..6e64531ecae 100644 --- a/storage/api/src/main/java/tech/pegasys/teku/storage/api/FinalizedChainData.java +++ b/storage/api/src/main/java/tech/pegasys/teku/storage/api/FinalizedChainData.java @@ -98,7 +98,9 @@ public Builder latestFinalized(final AnchorPoint latestFinalized) { latestFinalized .getSignedBeaconBlock() .orElseThrow(() -> new IllegalStateException("Missing newly finalized block"))); - this.finalizedStates.put(latestFinalized.getRoot(), latestFinalized.getState()); + if (!latestFinalized.isTransitioned()) { + this.finalizedStates.put(latestFinalized.getRoot(), latestFinalized.getState()); + } finalizedChildAndParent(latestFinalized.getRoot(), latestFinalized.getParentRoot()); return this; } diff --git a/storage/src/main/java/tech/pegasys/teku/storage/client/RecentChainData.java b/storage/src/main/java/tech/pegasys/teku/storage/client/RecentChainData.java index 5f8a6b9ddf3..a5e3dd19d11 100644 --- a/storage/src/main/java/tech/pegasys/teku/storage/client/RecentChainData.java +++ b/storage/src/main/java/tech/pegasys/teku/storage/client/RecentChainData.java @@ -301,6 +301,7 @@ public void updateHead(Bytes32 root, UInt64 currentSlot) { final Optional originalChainHead = chainHead; final ReadOnlyForkChoiceStrategy forkChoiceStrategy = store.getForkChoiceStrategy(); + // FIXME: with transitioned stateRoot already final Optional maybeBlockData = forkChoiceStrategy.getBlockData(root); if (maybeBlockData.isEmpty()) { LOG.error( @@ -375,6 +376,7 @@ private Optional computeReorgContext( return optionalReorgContext; } + // TODO: Test the consequences of having transitioned chainHead private ChainHead createNewChainHead( final Bytes32 root, final UInt64 currentSlot, final ProtoNodeData blockData) { final SafeFuture chainHeadStateFuture = diff --git a/storage/src/main/java/tech/pegasys/teku/storage/store/Store.java b/storage/src/main/java/tech/pegasys/teku/storage/store/Store.java index fd69c162f1b..b336e5b2ad2 100644 --- a/storage/src/main/java/tech/pegasys/teku/storage/store/Store.java +++ b/storage/src/main/java/tech/pegasys/teku/storage/store/Store.java @@ -173,7 +173,9 @@ private Store( // Track latest finalized block this.finalizedAnchor = finalizedAnchor; this.maybeEpochStates = maybeEpochStates; - states.cache(finalizedAnchor.getRoot(), finalizedAnchor); + if (!finalizedAnchor.isTransitioned()) { + states.cache(finalizedAnchor.getRoot(), finalizedAnchor); + } this.finalizedOptimisticTransitionPayload = finalizedOptimisticTransitionPayload; // Set up block provider to draw from in-memory blocks @@ -466,6 +468,7 @@ public Checkpoint getFinalizedCheckpoint() { } } + // TODO: Test me @Override public AnchorPoint getLatestFinalized() { readLock.lock(); @@ -674,6 +677,7 @@ public SafeFuture> retrieveStateAtSlot( new StateAtSlotTask(spec, slotAndBlockRoot, this::retrieveBlockState)); } + // TODO: Test me @Override public SafeFuture retrieveFinalizedCheckpointAndState() { final AnchorPoint finalized; @@ -842,6 +846,7 @@ private SafeFuture> getAndCacheBlockAndState( }); } + // TODO: Test me, will return transitioned state private SafeFuture> getOrRegenerateBlockAndState( final Bytes32 blockRoot) { // Avoid generating the hash tree to rebuild if the state is already available. @@ -858,13 +863,10 @@ private SafeFuture> getOrRegenerateBlockAndState( return SafeFuture.completedFuture(maybeEpochState); } - // if finalized is gone from cache we can still reconstruct that without regenerating + // if finalized is gone from cache we can use the finalized anchor without regenerating if (finalizedAnchor.getRoot().equals(blockRoot)) { LOG.trace("epochCache GET finalizedAnchor {}", finalizedAnchor::getSlot); - return SafeFuture.completedFuture( - Optional.of( - StateAndBlockSummary.create( - finalizedAnchor.getBlockSummary(), finalizedAnchor.getState()))); + return SafeFuture.completedFuture(Optional.of(finalizedAnchor)); } maybeEpochStates.ifPresent( @@ -993,7 +995,7 @@ private Optional getClosestAvailableBlockRootAndState( // If we haven't found a base state yet, we must have walked back to the latest finalized // block, check here for the base state final AnchorPoint finalized = getLatestFinalized(); - if (!treeBuilder.contains(finalized.getRoot())) { + if (!treeBuilder.contains(finalized.getRoot()) || finalized.isTransitioned()) { // We must have finalized a new block while processing and moved past our target root return Optional.empty(); }