diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/config/Constants.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/config/Constants.java index 46bf7afa7f6..e9c7a6498e8 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/config/Constants.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/config/Constants.java @@ -31,6 +31,9 @@ public class Constants { // sync committee size. public static final int VALID_CONTRIBUTION_AND_PROOF_SET_SIZE = 512; public static final int VALID_SYNC_COMMITTEE_MESSAGE_SET_SIZE = 512; + // When finalization is at its best case with 100% of votes we could have up to 3 full + // epochs of non-finalized blocks + public static final int BEST_CASE_NON_FINALIZED_EPOCHS = 3; public static final Duration ETH1_INDIVIDUAL_BLOCK_RETRY_TIMEOUT = Duration.ofMillis(500); public static final Duration ETH1_DEPOSIT_REQUEST_RETRY_TIMEOUT = Duration.ofSeconds(2); diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/forkchoice/ReadOnlyStore.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/forkchoice/ReadOnlyStore.java index e8e67c595d0..9e07bf30e31 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/forkchoice/ReadOnlyStore.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/forkchoice/ReadOnlyStore.java @@ -134,4 +134,13 @@ default SafeFuture> retrieveBlock(Bytes32 blockRoot) { SafeFuture> retrieveCheckpointState( Checkpoint checkpoint, BeaconState latestStateAtEpoch); + + // implements is_head_weak from fork-choice Consensus Spec + SafeFuture> isHeadWeak(final Bytes32 root); + + // implements is_parent_strong from fork-choice Consensus Spec + SafeFuture> isParentStrong(final Bytes32 parentRoot); + + // implements is_ffg_competitive from Consensus Spec + Optional isFfgCompetitive(final Bytes32 headRoot, final Bytes32 parentRoot); } diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/common/helpers/BeaconStateAccessors.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/common/helpers/BeaconStateAccessors.java index b6851c3259e..d37edf0327b 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/common/helpers/BeaconStateAccessors.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/common/helpers/BeaconStateAccessors.java @@ -170,7 +170,7 @@ public Bytes32 getSeed(BeaconState state, UInt64 epoch, Bytes4 domainType) * @return */ public UInt64 calculateCommitteeFraction( - final BeaconState beaconState, final UInt64 committeePercent) { + final BeaconState beaconState, final int committeePercent) { final UInt64 committeeWeight = getTotalActiveBalance(beaconState).dividedBy(config.getSlotsPerEpoch()); return committeeWeight.times(committeePercent).dividedBy(100); diff --git a/ethereum/spec/src/test/java/tech/pegasys/teku/spec/logic/common/helpers/BeaconStateAccessorsTest.java b/ethereum/spec/src/test/java/tech/pegasys/teku/spec/logic/common/helpers/BeaconStateAccessorsTest.java index 524aca45626..8fb520913fb 100644 --- a/ethereum/spec/src/test/java/tech/pegasys/teku/spec/logic/common/helpers/BeaconStateAccessorsTest.java +++ b/ethereum/spec/src/test/java/tech/pegasys/teku/spec/logic/common/helpers/BeaconStateAccessorsTest.java @@ -137,8 +137,7 @@ void calculateCommitteeFraction_full() { spec.atSlot(state.getSlot()).beaconStateAccessors().getTotalActiveBalance(state); final UInt64 totalActiveBalancePerSlot = totalActiveBalance.dividedBy(spec.getGenesisSpec().getSlotsPerEpoch()); - final UInt64 fraction = - beaconStateAccessors.calculateCommitteeFraction(state, UInt64.valueOf(100)); + final UInt64 fraction = beaconStateAccessors.calculateCommitteeFraction(state, 100); // at its simplest, if we've divided by slots in the function, this should be // totalActiveBalance/slots (because fraction is 100%) assertThat(fraction).isEqualTo(totalActiveBalancePerSlot); @@ -151,7 +150,7 @@ void calculateCommitteeFraction_minimal() { spec.atSlot(state.getSlot()).beaconStateAccessors().getTotalActiveBalance(state); final UInt64 totalActiveBalancePerSlot = totalActiveBalance.dividedBy(spec.getGenesisSpec().getSlotsPerEpoch()); - final UInt64 fraction = beaconStateAccessors.calculateCommitteeFraction(state, UInt64.ONE); + final UInt64 fraction = beaconStateAccessors.calculateCommitteeFraction(state, 1); // should be 1% of balance per slot... assertThat(fraction).isEqualTo(totalActiveBalancePerSlot.dividedBy(100)); } 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 7a47f4a3be1..574e0149882 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 @@ -246,6 +246,21 @@ public SafeFuture> retrieveCheckpointState( return SafeFuture.completedFuture(Optional.of(latestStateAtEpoch)); } + @Override + public SafeFuture> isHeadWeak(Bytes32 root) { + return SafeFuture.completedFuture(Optional.empty()); + } + + @Override + public SafeFuture> isParentStrong(Bytes32 parentRoot) { + return SafeFuture.completedFuture(Optional.empty()); + } + + @Override + public Optional isFfgCompetitive(Bytes32 headRoot, Bytes32 parentRoot) { + return Optional.empty(); + } + @Override public Optional> getBlobSidecarsIfAvailable( final SlotAndBlockRoot slotAndBlockRoot) { diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/validation/BlobSidecarGossipValidator.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/validation/BlobSidecarGossipValidator.java index 747b28648d6..8ad6f1a5ccd 100644 --- a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/validation/BlobSidecarGossipValidator.java +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/validation/BlobSidecarGossipValidator.java @@ -14,7 +14,9 @@ package tech.pegasys.teku.statetransition.validation; import static tech.pegasys.teku.infrastructure.async.SafeFuture.completedFuture; +import static tech.pegasys.teku.spec.config.Constants.BEST_CASE_NON_FINALIZED_EPOCHS; import static tech.pegasys.teku.spec.config.Constants.VALID_BLOCK_SET_SIZE; +import static tech.pegasys.teku.statetransition.validation.InternalValidationResult.ACCEPT; import static tech.pegasys.teku.statetransition.validation.InternalValidationResult.ignore; import static tech.pegasys.teku.statetransition.validation.InternalValidationResult.reject; @@ -48,6 +50,7 @@ public class BlobSidecarGossipValidator { private final Spec spec; private final Set receivedValidBlobSidecarInfoSet; + private final Set validSignedBlockHeaders; private final GossipValidationHelper gossipValidationHelper; private final Map invalidBlockRoots; private final MiscHelpersDeneb miscHelpersDeneb; @@ -63,6 +66,9 @@ public static BlobSidecarGossipValidator create( final Optional maybeMaxBlobsPerBlock = spec.getMaxBlobsPerBlock(); final int validInfoSize = VALID_BLOCK_SET_SIZE * maybeMaxBlobsPerBlock.orElse(1); + // It's not fatal if we miss something and we don't need finalized data + final int validSignedBlockHeadersSize = + spec.getGenesisSpec().getSlotsPerEpoch() * BEST_CASE_NON_FINALIZED_EPOCHS; return new BlobSidecarGossipValidator( spec, @@ -70,7 +76,8 @@ public static BlobSidecarGossipValidator create( validationHelper, miscHelpersDeneb, kzg, - LimitedSet.createSynchronized(validInfoSize)); + LimitedSet.createSynchronized(validInfoSize), + LimitedSet.createSynchronized(validSignedBlockHeadersSize)); } @VisibleForTesting @@ -84,13 +91,15 @@ private BlobSidecarGossipValidator( final GossipValidationHelper gossipValidationHelper, final MiscHelpersDeneb miscHelpersDeneb, final KZG kzg, - final Set receivedValidBlobSidecarInfoSet) { + final Set receivedValidBlobSidecarInfoSet, + final Set validSignedBlockHeaders) { this.spec = spec; this.invalidBlockRoots = invalidBlockRoots; this.gossipValidationHelper = gossipValidationHelper; this.miscHelpersDeneb = miscHelpersDeneb; this.kzg = kzg; this.receivedValidBlobSidecarInfoSet = receivedValidBlobSidecarInfoSet; + this.validSignedBlockHeaders = validSignedBlockHeaders; } public SafeFuture validate(final BlobSidecar blobSidecar) { @@ -134,6 +143,12 @@ public SafeFuture validate(final BlobSidecar blobSidec return completedFuture(InternalValidationResult.IGNORE); } + // Optimization: If we have already completely verified BlobSidecar with the same + // SignedBlockHeader, we can skip most steps and jump to shortened validation + if (validSignedBlockHeaders.contains(blobSidecar.getSignedBeaconBlockHeader().hashTreeRoot())) { + return validateBlobSidecarWithKnownValidHeader(blobSidecar, blockHeader); + } + /* * [REJECT] The proposer signature of `blob_sidecar.signed_block_header`, is valid with respect * to the `block_header.proposer_index` pubkey. @@ -241,6 +256,12 @@ public SafeFuture validate(final BlobSidecar blobSidec return reject("BlobSidecar block header signature is invalid."); } + /* + * Checking it again at the very end because whole method is not synchronized + * + * [IGNORE] The sidecar is the first sidecar for the tuple (block_header.slot, block_header.proposer_index, blob_sidecar.index) + * with valid header signature, sidecar inclusion proof, and kzg proof. + */ if (!receivedValidBlobSidecarInfoSet.add( new SlotProposerIndexAndBlobIndex( blockHeader.getSlot(), @@ -250,10 +271,55 @@ public SafeFuture validate(final BlobSidecar blobSidec "BlobSidecar is not the first valid for its slot and index. It will be dropped."); } - return InternalValidationResult.ACCEPT; + validSignedBlockHeaders.add(blobSidecar.getSignedBeaconBlockHeader().hashTreeRoot()); + + return ACCEPT; }); } + private SafeFuture validateBlobSidecarWithKnownValidHeader( + final BlobSidecar blobSidecar, final BeaconBlockHeader blockHeader) { + + /* + * [REJECT] The sidecar's inclusion proof is valid as verified by `verify_blob_sidecar_inclusion_proof(blob_sidecar)`. + */ + if (!miscHelpersDeneb.verifyBlobSidecarMerkleProof(blobSidecar)) { + return completedFuture(reject("BlobSidecar inclusion proof validation failed")); + } + + /* + * [REJECT] The sidecar's blob is valid as verified by + * `verify_blob_kzg_proof(blob_sidecar.blob, blob_sidecar.kzg_commitment, blob_sidecar.kzg_proof)`. + */ + if (!miscHelpersDeneb.verifyBlobKzgProof(kzg, blobSidecar)) { + return completedFuture(reject("BlobSidecar does not pass kzg validation")); + } + + // This can be changed between two received BlobSidecars from one block, so checking + /* + * [REJECT] The current finalized_checkpoint is an ancestor of the sidecar's block -- i.e. + * `get_checkpoint_block(store, block_header.parent_root, store.finalized_checkpoint.epoch) == store.finalized_checkpoint.root`. + */ + if (!gossipValidationHelper.currentFinalizedCheckpointIsAncestorOfBlock( + blockHeader.getSlot(), blockHeader.getParentRoot())) { + return completedFuture( + reject("BlobSidecar block header does not descend from finalized checkpoint")); + } + + /* + * [IGNORE] The sidecar is the first sidecar for the tuple (block_header.slot, block_header.proposer_index, blob_sidecar.index) + * with valid header signature, sidecar inclusion proof, and kzg proof. + */ + if (!receivedValidBlobSidecarInfoSet.add( + new SlotProposerIndexAndBlobIndex( + blockHeader.getSlot(), blockHeader.getProposerIndex(), blobSidecar.getIndex()))) { + return SafeFuture.completedFuture( + ignore("BlobSidecar is not the first valid for its slot and index. It will be dropped.")); + } + + return SafeFuture.completedFuture(ACCEPT); + } + private boolean verifyBlockHeaderSignature( final BeaconState state, final SignedBeaconBlockHeader signedBlockHeader) { final Bytes32 domain = diff --git a/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/validation/BlobSidecarGossipValidatorTest.java b/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/validation/BlobSidecarGossipValidatorTest.java index a414f4dfb64..47a32850fb0 100644 --- a/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/validation/BlobSidecarGossipValidatorTest.java +++ b/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/validation/BlobSidecarGossipValidatorTest.java @@ -15,7 +15,10 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.util.HashMap; @@ -30,6 +33,7 @@ import tech.pegasys.teku.kzg.KZG; import tech.pegasys.teku.spec.Spec; import tech.pegasys.teku.spec.SpecMilestone; +import tech.pegasys.teku.spec.SpecVersion; import tech.pegasys.teku.spec.TestSpecContext; import tech.pegasys.teku.spec.TestSpecInvocationContextProvider.SpecContext; import tech.pegasys.teku.spec.datastructures.blobs.versions.deneb.BlobSidecar; @@ -46,6 +50,7 @@ public class BlobSidecarGossipValidatorTest { private final GossipValidationHelper gossipValidationHelper = mock(GossipValidationHelper.class); private final MiscHelpersDeneb miscHelpersDeneb = mock(MiscHelpersDeneb.class); private final KZG kzg = mock(KZG.class); + private DataStructureUtil dataStructureUtil; private BlobSidecarGossipValidator blobSidecarValidator; private UInt64 parentSlot; @@ -61,7 +66,7 @@ public class BlobSidecarGossipValidatorTest { @BeforeEach void setup(final SpecContext specContext) { - final DataStructureUtil dataStructureUtil = specContext.getDataStructureUtil(); + this.dataStructureUtil = specContext.getDataStructureUtil(); blobSidecarValidator = BlobSidecarGossipValidator.create( @@ -135,6 +140,9 @@ void shouldRejectWhenIndexIsTooBig(final SpecContext specContext) { void shouldRejectWhenSlotIsNotDeneb() { final Spec mockedSpec = mock(Spec.class); when(mockedSpec.getMaxBlobsPerBlock(slot)).thenReturn(Optional.empty()); + final SpecVersion mockedSpecVersion = mock(SpecVersion.class); + when(mockedSpec.getGenesisSpec()).thenReturn(mockedSpecVersion); + when(mockedSpecVersion.getSlotsPerEpoch()).thenReturn(1); blobSidecarValidator = BlobSidecarGossipValidator.create( @@ -268,4 +276,162 @@ void shouldTrackValidInfoSet() { SafeFutureAssert.assertThatSafeFuture(blobSidecarValidator.validate(blobSidecar)) .isCompletedWithValueMatching(InternalValidationResult::isIgnore); } + + @TestTemplate + void shouldNotVerifyKnownValidSignedHeader() { + SafeFutureAssert.assertThatSafeFuture(blobSidecarValidator.validate(blobSidecar)) + .isCompletedWithValueMatching(InternalValidationResult::isAccept); + + verify(miscHelpersDeneb).verifyBlobSidecarMerkleProof(blobSidecar); + verify(miscHelpersDeneb).verifyBlobKzgProof(kzg, blobSidecar); + verify(gossipValidationHelper).getParentStateInBlockEpoch(any(), any(), any()); + verify(gossipValidationHelper).isProposerTheExpectedProposer(any(), any(), any()); + verify(gossipValidationHelper) + .isSignatureValidWithRespectToProposerIndex(any(), any(), any(), any()); + clearInvocations(gossipValidationHelper); + + // Other BlobSidecar from the same block + final BlobSidecar blobSidecar0 = + dataStructureUtil + .createRandomBlobSidecarBuilder() + .signedBeaconBlockHeader(blobSidecar.getSignedBeaconBlockHeader()) + .index(UInt64.ZERO) + .build(); + + SafeFutureAssert.assertThatSafeFuture(blobSidecarValidator.validate(blobSidecar0)) + .isCompletedWithValueMatching(InternalValidationResult::isAccept); + + verify(miscHelpersDeneb).verifyBlobSidecarMerkleProof(blobSidecar0); + verify(miscHelpersDeneb).verifyBlobKzgProof(kzg, blobSidecar0); + verify(gossipValidationHelper, never()).getParentStateInBlockEpoch(any(), any(), any()); + verify(gossipValidationHelper, never()).isProposerTheExpectedProposer(any(), any(), any()); + verify(gossipValidationHelper, never()) + .isSignatureValidWithRespectToProposerIndex(any(), any(), any(), any()); + clearInvocations(gossipValidationHelper); + + // BlobSidecar from the new block + final BlobSidecar blobSidecarNew = + dataStructureUtil.createRandomBlobSidecarBuilder().index(UInt64.ZERO).build(); + final Bytes32 parentRoot = + blobSidecarNew.getSignedBeaconBlockHeader().getMessage().getParentRoot(); + + when(gossipValidationHelper.isSlotFinalized(blobSidecarNew.getSlot())).thenReturn(false); + when(gossipValidationHelper.isSlotFromFuture(blobSidecarNew.getSlot())).thenReturn(false); + when(gossipValidationHelper.isBlockAvailable(parentRoot)).thenReturn(true); + when(gossipValidationHelper.getSlotForBlockRoot(parentRoot)) + .thenReturn(Optional.of(parentSlot)); + when(gossipValidationHelper.getParentStateInBlockEpoch( + parentSlot, parentRoot, blobSidecarNew.getSlot())) + .thenReturn(SafeFuture.completedFuture(Optional.empty())); + when(gossipValidationHelper.currentFinalizedCheckpointIsAncestorOfBlock( + blobSidecarNew.getSlot(), parentRoot)) + .thenReturn(true); + + SafeFutureAssert.assertThatSafeFuture(blobSidecarValidator.validate(blobSidecarNew)) + .isCompletedWithValueMatching(InternalValidationResult::isIgnore); + + verify(miscHelpersDeneb).verifyBlobSidecarMerkleProof(blobSidecarNew); + verify(miscHelpersDeneb).verifyBlobKzgProof(kzg, blobSidecarNew); + verify(gossipValidationHelper).getParentStateInBlockEpoch(any(), any(), any()); + } + + @TestTemplate + void shouldVerifySignedHeaderAgainAfterItDroppedFromCache() { + final Spec specMock = mock(Spec.class); + final SpecVersion specVersion = mock(SpecVersion.class); + when(specMock.getMaxBlobsPerBlock(any())).thenReturn(Optional.of(6)); + when(specMock.getGenesisSpec()).thenReturn(specVersion); + // This will make cache of size 3 + when(specVersion.getSlotsPerEpoch()).thenReturn(1); + this.blobSidecarValidator = + BlobSidecarGossipValidator.create( + specMock, invalidBlocks, gossipValidationHelper, miscHelpersDeneb, kzg); + // Accept everything + when(gossipValidationHelper.isSlotFinalized(any())).thenReturn(false); + when(gossipValidationHelper.isSlotFromFuture(any())).thenReturn(false); + when(gossipValidationHelper.isBlockAvailable(any())).thenReturn(true); + when(gossipValidationHelper.getParentStateInBlockEpoch(any(), any(), any())) + .thenReturn(SafeFuture.completedFuture(Optional.of(postState))); + when(gossipValidationHelper.isProposerTheExpectedProposer(any(), any(), any())) + .thenReturn(true); + when(gossipValidationHelper.currentFinalizedCheckpointIsAncestorOfBlock(any(), any())) + .thenReturn(true); + when(gossipValidationHelper.isSignatureValidWithRespectToProposerIndex( + any(), any(), any(), any())) + .thenReturn(true); + + // First blobSidecar + SafeFutureAssert.assertThatSafeFuture(blobSidecarValidator.validate(blobSidecar)) + .isCompletedWithValueMatching(InternalValidationResult::isAccept); + clearInvocations(gossipValidationHelper); + + // Other BlobSidecar from the same block, known valid block header is detected, so short + // validation is used + final BlobSidecar blobSidecar0 = + dataStructureUtil + .createRandomBlobSidecarBuilder() + .signedBeaconBlockHeader(blobSidecar.getSignedBeaconBlockHeader()) + .index(UInt64.ZERO) + .build(); + + SafeFutureAssert.assertThatSafeFuture(blobSidecarValidator.validate(blobSidecar0)) + .isCompletedWithValueMatching(InternalValidationResult::isAccept); + + verify(gossipValidationHelper, never()) + .isSignatureValidWithRespectToProposerIndex(any(), any(), any(), any()); + clearInvocations(gossipValidationHelper); + + // 2nd block BlobSidecar + final BlobSidecar blobSidecar2 = dataStructureUtil.randomBlobSidecar(); + when(gossipValidationHelper.getSlotForBlockRoot(any())) + .thenReturn(Optional.of(blobSidecar2.getSlot().decrement())); + + SafeFutureAssert.assertThatSafeFuture(blobSidecarValidator.validate(blobSidecar2)) + .isCompletedWithValueMatching(InternalValidationResult::isAccept); + + verify(gossipValidationHelper) + .isSignatureValidWithRespectToProposerIndex(any(), any(), any(), any()); + clearInvocations(gossipValidationHelper); + + // 3rd block BlobSidecar + final BlobSidecar blobSidecar3 = dataStructureUtil.randomBlobSidecar(); + when(gossipValidationHelper.getSlotForBlockRoot(any())) + .thenReturn(Optional.of(blobSidecar3.getSlot().decrement())); + + SafeFutureAssert.assertThatSafeFuture(blobSidecarValidator.validate(blobSidecar3)) + .isCompletedWithValueMatching(InternalValidationResult::isAccept); + + verify(gossipValidationHelper) + .isSignatureValidWithRespectToProposerIndex(any(), any(), any(), any()); + clearInvocations(gossipValidationHelper); + + // 4th block BlobSidecar, erasing block from blobSidecar0 from cache + final BlobSidecar blobSidecar4 = dataStructureUtil.randomBlobSidecar(); + when(gossipValidationHelper.getSlotForBlockRoot(any())) + .thenReturn(Optional.of(blobSidecar4.getSlot().decrement())); + + SafeFutureAssert.assertThatSafeFuture(blobSidecarValidator.validate(blobSidecar4)) + .isCompletedWithValueMatching(InternalValidationResult::isAccept); + + verify(gossipValidationHelper) + .isSignatureValidWithRespectToProposerIndex(any(), any(), any(), any()); + clearInvocations(gossipValidationHelper); + + // BlobSidecar from the same block as blobSidecar0 and blobSidecar + final BlobSidecar blobSidecar5 = + dataStructureUtil + .createRandomBlobSidecarBuilder() + .signedBeaconBlockHeader(blobSidecar.getSignedBeaconBlockHeader()) + .index(UInt64.valueOf(2)) + .build(); + when(gossipValidationHelper.getSlotForBlockRoot(any())) + .thenReturn(Optional.of(blobSidecar5.getSlot().decrement())); + + SafeFutureAssert.assertThatSafeFuture(blobSidecarValidator.validate(blobSidecar5)) + .isCompletedWithValueMatching(InternalValidationResult::isAccept); + + // Signature is validating again though header was known valid until dropped from cache + verify(gossipValidationHelper) + .isSignatureValidWithRespectToProposerIndex(any(), any(), any(), any()); + } } diff --git a/storage/src/main/java/tech/pegasys/teku/storage/client/BlockTimelinessTracker.java b/storage/src/main/java/tech/pegasys/teku/storage/client/BlockTimelinessTracker.java index e4a3a754797..cc106053cac 100644 --- a/storage/src/main/java/tech/pegasys/teku/storage/client/BlockTimelinessTracker.java +++ b/storage/src/main/java/tech/pegasys/teku/storage/client/BlockTimelinessTracker.java @@ -15,6 +15,8 @@ import static tech.pegasys.teku.spec.constants.NetworkConstants.INTERVALS_PER_SLOT; +import com.google.common.base.Supplier; +import com.google.common.base.Suppliers; import java.util.Map; import java.util.Optional; import org.apache.logging.log4j.LogManager; @@ -33,6 +35,9 @@ public class BlockTimelinessTracker { private final Spec spec; private final RecentChainData recentChainData; + private final Supplier genesisTimeSupplier; + private final Supplier genesisTimeMillisSupplier; + // implements is_timely from Consensus Spec public BlockTimelinessTracker( final Spec spec, final RecentChainData recentChainData, final TimeProvider timeProvider) { @@ -44,12 +49,14 @@ public BlockTimelinessTracker( spec.getGenesisSpec().getSlotsPerEpoch() * epochsForTimeliness); this.timeProvider = timeProvider; this.recentChainData = recentChainData; + this.genesisTimeSupplier = Suppliers.memoize(recentChainData::getGenesisTime); + this.genesisTimeMillisSupplier = Suppliers.memoize(recentChainData::getGenesisTimeMillis); } public void setBlockTimelinessFromArrivalTime( final SignedBeaconBlock block, final UInt64 arrivalTimeMillis) { - final UInt64 genesisTime = recentChainData.getGenesisTime(); - final UInt64 computedSlot = spec.getCurrentSlot(timeProvider.getTimeInSeconds(), genesisTime); + final UInt64 computedSlot = + spec.getCurrentSlot(timeProvider.getTimeInSeconds(), genesisTimeSupplier.get()); final Bytes32 root = block.getRoot(); if (computedSlot.isGreaterThan(block.getMessage().getSlot())) { LOG.debug( @@ -65,7 +72,7 @@ public void setBlockTimelinessFromArrivalTime( .ifPresent( slot -> { final UInt64 slotStartTimeMillis = - spec.getSlotStartTimeMillis(slot, genesisTime.times(1000)); + spec.getSlotStartTimeMillis(slot, genesisTimeMillisSupplier.get()); final int millisIntoSlot = arrivalTimeMillis.minusMinZero(slotStartTimeMillis).intValue(); @@ -87,7 +94,34 @@ public void setBlockTimelinessFromArrivalTime( }); } - public Optional isBlockTimely(final Bytes32 root) { + Optional isBlockTimely(final Bytes32 root) { return Optional.ofNullable(blockTimeliness.get(root)); } + + // is_proposing_on_time from consensus-spec + // 'on time' is before we're half-way to the attester time. logically, if the slot is 3 segments, + // then splitting into 6 segments is half-way to the attestation time. + public boolean isProposingOnTime(final UInt64 slot) { + final UInt64 slotStartTimeMillis = + spec.getSlotStartTimeMillis(slot, genesisTimeMillisSupplier.get()); + final UInt64 timelinessLimit = spec.getMillisPerSlot(slot).dividedBy(INTERVALS_PER_SLOT * 2); + final UInt64 currentTimeMillis = timeProvider.getTimeInMillis(); + final boolean isTimely = + currentTimeMillis.minusMinZero(slotStartTimeMillis).isLessThan(timelinessLimit); + LOG.debug( + "Check ProposingOnTime for slot {}, slot start time is {} ms and current time is {} ms, limit is {} ms result: {}", + slot, + slotStartTimeMillis, + currentTimeMillis, + timelinessLimit, + isTimely); + return isTimely; + } + + // Implements is_head_late form consensus-spec + // caveat: if the root was not found, will default to it being timely, + // on the basis that it's not safe to make choices about blocks we don't know about + public boolean isBlockLate(final Bytes32 root) { + return !isBlockTimely(root).orElse(true); + } } 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 d3f66a473e8..27654860a93 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 @@ -624,7 +624,12 @@ public void setBlockTimelinessFromArrivalTime( blockTimelinessTracker.setBlockTimelinessFromArrivalTime(block, arrivalTime); } - public Optional getBlockTimeliness(final Bytes32 blockRoot) { - return blockTimelinessTracker.isBlockTimely(blockRoot); + // implements is_head_late from consensus spec + public boolean isBlockLate(final Bytes32 blockRoot) { + return blockTimelinessTracker.isBlockLate(blockRoot); + } + + public boolean isProposingOnTime(final UInt64 slot) { + return blockTimelinessTracker.isProposingOnTime(slot); } } 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 964716bc86a..e9ae37ef04a 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 @@ -52,6 +52,7 @@ import tech.pegasys.teku.infrastructure.metrics.TekuMetricCategory; import tech.pegasys.teku.infrastructure.unsigned.UInt64; import tech.pegasys.teku.spec.Spec; +import tech.pegasys.teku.spec.SpecVersion; import tech.pegasys.teku.spec.datastructures.blobs.versions.deneb.BlobSidecar; import tech.pegasys.teku.spec.datastructures.blocks.BlockAndCheckpoints; import tech.pegasys.teku.spec.datastructures.blocks.SignedBeaconBlock; @@ -59,6 +60,7 @@ import tech.pegasys.teku.spec.datastructures.blocks.SlotAndBlockRoot; import tech.pegasys.teku.spec.datastructures.blocks.StateAndBlockSummary; import tech.pegasys.teku.spec.datastructures.execution.SlotAndExecutionPayloadSummary; +import tech.pegasys.teku.spec.datastructures.forkchoice.ProtoNodeData; import tech.pegasys.teku.spec.datastructures.forkchoice.VoteTracker; import tech.pegasys.teku.spec.datastructures.forkchoice.VoteUpdater; import tech.pegasys.teku.spec.datastructures.hashtree.HashTree; @@ -67,6 +69,7 @@ import tech.pegasys.teku.spec.datastructures.state.Checkpoint; import tech.pegasys.teku.spec.datastructures.state.CheckpointState; import tech.pegasys.teku.spec.datastructures.state.beaconstate.BeaconState; +import tech.pegasys.teku.spec.logic.common.helpers.BeaconStateAccessors; import tech.pegasys.teku.storage.api.StorageUpdateChannel; import tech.pegasys.teku.storage.api.StoredBlockMetadata; import tech.pegasys.teku.storage.api.VoteUpdateChannel; @@ -184,7 +187,7 @@ private Store( this.earliestBlobSidecarSlotProvider = earliestBlobSidecarSlotProvider; } - public static UpdatableStore create( + static UpdatableStore create( final AsyncRunner asyncRunner, final MetricsSystem metricsSystem, final Spec spec, @@ -200,9 +203,8 @@ public static UpdatableStore create( final Checkpoint bestJustifiedCheckpoint, final Map blockInfoByRoot, final Map votes, - final StoreConfig config) { - - // Create limited collections for non-final data + final StoreConfig config, + final ForkChoiceStrategy forkChoiceStrategy) { final Map blocks = LimitedMap.createSynchronizedNatural(config.getBlockCacheSize()); final CachingTaskQueue checkpointStateTaskQueue = @@ -221,6 +223,47 @@ public static UpdatableStore create( final Map> blobSidecars = LimitedMap.createSynchronizedNatural(config.getBlockCacheSize()); + return new Store( + asyncRunner, + metricsSystem, + spec, + config.getHotStatePersistenceFrequencyInEpochs(), + blockProvider, + stateAndBlockProvider, + earliestBlobSidecarSlotProvider, + stateTaskQueue, + initialCheckpoint, + time, + genesisTime, + finalizedAnchor, + finalizedOptimisticTransitionPayload, + justifiedCheckpoint, + bestJustifiedCheckpoint, + forkChoiceStrategy, + votes, + blocks, + checkpointStateTaskQueue, + maybeEpochStates, + blobSidecars); + } + + public static UpdatableStore create( + final AsyncRunner asyncRunner, + final MetricsSystem metricsSystem, + final Spec spec, + final BlockProvider blockProvider, + final StateAndBlockSummaryProvider stateAndBlockProvider, + final EarliestBlobSidecarSlotProvider earliestBlobSidecarSlotProvider, + final Optional initialCheckpoint, + final UInt64 time, + final UInt64 genesisTime, + final AnchorPoint finalizedAnchor, + final Optional finalizedOptimisticTransitionPayload, + final Checkpoint justifiedCheckpoint, + final Checkpoint bestJustifiedCheckpoint, + final Map blockInfoByRoot, + final Map votes, + final StoreConfig config) { final UInt64 currentEpoch = spec.computeEpochAtSlot(spec.getCurrentSlot(time, genesisTime)); final ForkChoiceStrategy forkChoiceStrategy = ForkChoiceStrategy.initialize( @@ -232,16 +275,13 @@ public static UpdatableStore create( currentEpoch, justifiedCheckpoint, finalizedAnchor)); - - return new Store( + return create( asyncRunner, metricsSystem, spec, - config.getHotStatePersistenceFrequencyInEpochs(), blockProvider, stateAndBlockProvider, earliestBlobSidecarSlotProvider, - stateTaskQueue, initialCheckpoint, time, genesisTime, @@ -249,12 +289,10 @@ public static UpdatableStore create( finalizedOptimisticTransitionPayload, justifiedCheckpoint, bestJustifiedCheckpoint, - forkChoiceStrategy, + blockInfoByRoot, votes, - blocks, - checkpointStateTaskQueue, - maybeEpochStates, - blobSidecars); + config, + forkChoiceStrategy); } private static ProtoArray buildProtoArray( @@ -496,6 +534,125 @@ public Optional getBlockIfAvailable(final Bytes32 blockRoot) } } + @Override + public SafeFuture> isHeadWeak(final Bytes32 root) { + + final Optional maybeBlockData = getBlockDataFromForkChoiceStrategy(root); + if (maybeBlockData.isEmpty()) { + LOG.trace("isHeadWeak {}: no block data in protoArray, returning false", root); + return SafeFuture.completedFuture(Optional.empty()); + } + + final UInt64 headWeight = maybeBlockData.get().getWeight(); + + final SafeFuture> futureMaybeJustifiedState = + retrieveCheckpointState(justifiedCheckpoint); + + return futureMaybeJustifiedState + .thenApply( + maybeJustifiedState -> + maybeJustifiedState.map( + justifiedState -> { + final SpecVersion specVersion = spec.atSlot(justifiedState.getSlot()); + final BeaconStateAccessors beaconStateAccessors = + specVersion.beaconStateAccessors(); + final UInt64 reorgThreashold = + beaconStateAccessors.calculateCommitteeFraction( + justifiedState, + specVersion.getConfig().getReorgHeadWeightThreshold()); + final boolean result = headWeight.isLessThan(reorgThreashold); + + LOG.trace( + "isHeadWeak {}: headWeight: {}, reorgThreshold: {}, result: {}", + root, + headWeight, + reorgThreashold, + result); + return result; + })) + .exceptionallyCompose( + err -> { + LOG.error( + "Failed to retrieve justified state when checking head weight for block {}.", + root, + err); + return SafeFuture.completedFuture(Optional.empty()); + }); + } + + @Override + public SafeFuture> isParentStrong(final Bytes32 parentRoot) { + final Optional maybeBlockData = getBlockDataFromForkChoiceStrategy(parentRoot); + if (maybeBlockData.isEmpty()) { + LOG.trace("isParentStrong {}: no block data in protoArray, returning false", parentRoot); + return SafeFuture.completedFuture(Optional.empty()); + } + + final UInt64 parentWeight = maybeBlockData.get().getWeight(); + + final SafeFuture> futureMaybeJustifiedState = + retrieveCheckpointState(justifiedCheckpoint); + + return futureMaybeJustifiedState + .thenApply( + maybeJustifiedState -> + maybeJustifiedState.map( + justifiedState -> { + final SpecVersion specVersion = spec.atSlot(justifiedState.getSlot()); + final BeaconStateAccessors beaconStateAccessors = + specVersion.beaconStateAccessors(); + final UInt64 parentThreshold = + beaconStateAccessors.calculateCommitteeFraction( + justifiedState, + specVersion.getConfig().getReorgParentWeightThreshold()); + final boolean result = parentWeight.isGreaterThan(parentThreshold); + + LOG.trace( + "isParentStrong {}: parentWeight: {}, parentThreshold: {}, result: {}", + parentRoot, + parentWeight, + parentThreshold, + result); + return result; + })) + .exceptionallyCompose( + err -> { + LOG.error( + "Failed to retrieve justified state when checking weight for parent {}.", + parentRoot, + err); + return SafeFuture.completedFuture(Optional.empty()); + }); + } + + @Override + public Optional isFfgCompetitive(Bytes32 headRoot, Bytes32 parentRoot) { + final Optional maybeHeadData = getBlockDataFromForkChoiceStrategy(headRoot); + final Optional maybeParentData = getBlockDataFromForkChoiceStrategy(parentRoot); + if (maybeParentData.isEmpty() || maybeHeadData.isEmpty()) { + return Optional.empty(); + } + final Checkpoint headUnrealizedJustifiedCheckpoint = + maybeHeadData.get().getCheckpoints().getUnrealizedJustifiedCheckpoint(); + final Checkpoint parentUnrealizedJustifiedCheckpoint = + maybeParentData.get().getCheckpoints().getUnrealizedJustifiedCheckpoint(); + LOG.trace( + "head {}, compared to parent {}", + headUnrealizedJustifiedCheckpoint, + parentUnrealizedJustifiedCheckpoint); + return Optional.of( + headUnrealizedJustifiedCheckpoint.equals(parentUnrealizedJustifiedCheckpoint)); + } + + private Optional getBlockDataFromForkChoiceStrategy(final Bytes32 root) { + readLock.lock(); + try { + return forkChoiceStrategy.getBlockData(root); + } finally { + readLock.unlock(); + } + } + @Override public SafeFuture> retrieveSignedBlock(final Bytes32 blockRoot) { if (!containsBlock(blockRoot)) { diff --git a/storage/src/main/java/tech/pegasys/teku/storage/store/StoreBuilder.java b/storage/src/main/java/tech/pegasys/teku/storage/store/StoreBuilder.java index f7a4c23a699..5c2354fa530 100644 --- a/storage/src/main/java/tech/pegasys/teku/storage/store/StoreBuilder.java +++ b/storage/src/main/java/tech/pegasys/teku/storage/store/StoreBuilder.java @@ -16,6 +16,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; +import com.google.common.annotations.VisibleForTesting; import java.util.HashMap; import java.util.Map; import java.util.Optional; @@ -33,6 +34,7 @@ import tech.pegasys.teku.spec.datastructures.state.Checkpoint; import tech.pegasys.teku.storage.api.OnDiskStoreData; import tech.pegasys.teku.storage.api.StoredBlockMetadata; +import tech.pegasys.teku.storage.protoarray.ForkChoiceStrategy; public class StoreBuilder { private AsyncRunner asyncRunner; @@ -53,6 +55,7 @@ public class StoreBuilder { private Map votes; private Optional finalizedOptimisticTransitionPayload = Optional.empty(); + private ForkChoiceStrategy forkChoiceStrategy = null; private StoreBuilder() {} @@ -104,6 +107,26 @@ public StoreBuilder onDiskStoreData(final OnDiskStoreData data) { public UpdatableStore build() { assertValid(); + if (forkChoiceStrategy != null) { + return Store.create( + asyncRunner, + metricsSystem, + spec, + blockProvider, + stateAndBlockProvider, + earliestBlobSidecarSlotProvider, + anchor, + time, + genesisTime, + latestFinalized, + finalizedOptimisticTransitionPayload, + justifiedCheckpoint, + bestJustifiedCheckpoint, + blockInfoByRoot, + votes, + storeConfig, + forkChoiceStrategy); + } return Store.create( asyncRunner, metricsSystem, @@ -233,6 +256,12 @@ public StoreBuilder blockInformation(final Map blo return this; } + @VisibleForTesting + StoreBuilder forkChoiceStrategy(final ForkChoiceStrategy forkChoiceStrategy) { + this.forkChoiceStrategy = forkChoiceStrategy; + return this; + } + public StoreBuilder votes(final Map votes) { checkNotNull(votes); this.votes = votes; diff --git a/storage/src/main/java/tech/pegasys/teku/storage/store/StoreTransaction.java b/storage/src/main/java/tech/pegasys/teku/storage/store/StoreTransaction.java index 66e11cf0912..595e502b1f5 100644 --- a/storage/src/main/java/tech/pegasys/teku/storage/store/StoreTransaction.java +++ b/storage/src/main/java/tech/pegasys/teku/storage/store/StoreTransaction.java @@ -451,6 +451,21 @@ public SafeFuture> retrieveCheckpointState( return store.retrieveCheckpointState(checkpoint, latestStateAtEpoch); } + @Override + public SafeFuture> isHeadWeak(Bytes32 root) { + return store.isHeadWeak(root); + } + + @Override + public SafeFuture> isParentStrong(Bytes32 parentRoot) { + return store.isParentStrong(parentRoot); + } + + @Override + public Optional isFfgCompetitive(Bytes32 headRoot, Bytes32 parentRoot) { + return store.isFfgCompetitive(headRoot, parentRoot); + } + @Override public Optional getBlockIfAvailable(final Bytes32 blockRoot) { return Optional.ofNullable(blockData.get(blockRoot)) diff --git a/storage/src/test/java/tech/pegasys/teku/storage/client/BlockTimelinessTrackerTest.java b/storage/src/test/java/tech/pegasys/teku/storage/client/BlockTimelinessTrackerTest.java index e32045276b8..9b01eff1bf5 100644 --- a/storage/src/test/java/tech/pegasys/teku/storage/client/BlockTimelinessTrackerTest.java +++ b/storage/src/test/java/tech/pegasys/teku/storage/client/BlockTimelinessTrackerTest.java @@ -22,7 +22,6 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import tech.pegasys.teku.infrastructure.time.StubTimeProvider; -import tech.pegasys.teku.infrastructure.time.TimeProvider; import tech.pegasys.teku.infrastructure.unsigned.UInt64; import tech.pegasys.teku.spec.Spec; import tech.pegasys.teku.spec.TestSpecFactory; @@ -36,7 +35,7 @@ class BlockTimelinessTrackerTest { private final RecentChainData recentChainData = mock(RecentChainData.class); private final UInt64 slot = UInt64.ONE; - private TimeProvider timeProvider; + private StubTimeProvider timeProvider; private Bytes32 blockRoot; private SignedBlockAndState signedBlockAndState; private BlockTimelinessTracker tracker; @@ -51,6 +50,8 @@ void setup() { when(recentChainData.getGenesisTime()) .thenReturn(signedBlockAndState.getState().getGenesisTime()); + when(recentChainData.getGenesisTimeMillis()) + .thenReturn(signedBlockAndState.getState().getGenesisTime().times(1000)); when(recentChainData.getCurrentSlot()).thenReturn(Optional.of(UInt64.ONE)); } @@ -60,6 +61,7 @@ void blockTimeliness_shouldReportTimelinessIfSet() { tracker.setBlockTimelinessFromArrivalTime(signedBlockAndState.getBlock(), computedTime); assertThat(tracker.isBlockTimely(blockRoot)).contains(true); + assertThat(tracker.isBlockLate(blockRoot)).isFalse(); } @Test @@ -68,6 +70,7 @@ void blockTimeliness_shouldReportFalseIfLate() { tracker.setBlockTimelinessFromArrivalTime(signedBlockAndState.getBlock(), computedTime); assertThat(tracker.isBlockTimely(blockRoot)).contains(false); + assertThat(tracker.isBlockLate(blockRoot)).isTrue(); } @Test @@ -76,6 +79,7 @@ void blockTimeliness_shouldReportFalseIfAtLimit() { tracker.setBlockTimelinessFromArrivalTime(signedBlockAndState.getBlock(), computedTime); assertThat(tracker.isBlockTimely(blockRoot)).contains(false); + assertThat(tracker.isBlockLate(blockRoot)).isTrue(); } @Test @@ -85,11 +89,47 @@ void blockTimeliness_ifBlockFromFuture() { tracker.setBlockTimelinessFromArrivalTime( dataStructureUtil.randomSignedBeaconBlock(0), computedTime); assertThat(tracker.isBlockTimely(blockRoot)).isEmpty(); + assertThat(tracker.isBlockLate(blockRoot)).isFalse(); } @Test void blockTimeliness_shouldReportEmptyIfNotSet() { assertThat(tracker.isBlockTimely(blockRoot)).isEmpty(); + assertThat(tracker.isBlockLate(blockRoot)).isFalse(); + } + + @Test + void isProposingOnTime_shouldDetectBeforeSlotStartAsOk() { + // Advance time to 500ms before slot start + timeProvider.advanceTimeByMillis(millisPerSlot - 500); + assertThat(tracker.isProposingOnTime(slot)).isTrue(); + } + + @Test + void isProposingOnTime_shouldDetectSlotStartAsOnTime() { + // Advance time by 1 slot, leaving us at exactly slot time + timeProvider.advanceTimeByMillis(millisPerSlot); + assertThat(tracker.isProposingOnTime(slot)).isTrue(); + } + + @Test + void isProposingOnTime_shouldDetectLateIfAttestationsDue() { + // attestation is due 2 seconds into slot + timeProvider.advanceTimeByMillis(millisPerSlot + 2000); + assertThat(tracker.isProposingOnTime(slot)).isFalse(); + } + + @Test + void isProposingOnTime_shouldDetectOnTimeBeforeCutoff() { + /// 999 ms into slot, cutoff is 1000ms + timeProvider.advanceTimeByMillis(millisPerSlot + 999); + assertThat(tracker.isProposingOnTime(slot)).isTrue(); + } + + @Test + void isProposingOnTime_shouldDetectLateIfHalfWayToAttestationDue() { + timeProvider.advanceTimeByMillis(millisPerSlot + 1000); + assertThat(tracker.isProposingOnTime(slot)).isFalse(); } private UInt64 computeTime(final UInt64 slot, final long timeIntoSlot) { diff --git a/storage/src/test/java/tech/pegasys/teku/storage/store/AbstractStoreTest.java b/storage/src/test/java/tech/pegasys/teku/storage/store/AbstractStoreTest.java index 9d44544a06b..587f66b89dd 100644 --- a/storage/src/test/java/tech/pegasys/teku/storage/store/AbstractStoreTest.java +++ b/storage/src/test/java/tech/pegasys/teku/storage/store/AbstractStoreTest.java @@ -15,6 +15,7 @@ import static java.util.Collections.emptyMap; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; import static tech.pegasys.teku.infrastructure.async.SyncAsyncRunner.SYNC_RUNNER; import java.util.ArrayList; @@ -41,6 +42,7 @@ import tech.pegasys.teku.storage.api.StorageUpdateChannel; import tech.pegasys.teku.storage.api.StoredBlockMetadata; import tech.pegasys.teku.storage.api.StubStorageUpdateChannel; +import tech.pegasys.teku.storage.protoarray.ForkChoiceStrategy; public abstract class AbstractStoreTest { protected final Spec spec = TestSpecFactory.createMinimalDeneb(); @@ -48,6 +50,8 @@ public abstract class AbstractStoreTest { protected final ChainBuilder chainBuilder = ChainBuilder.create(spec); protected final StoreConfig defaultStoreConfig = StoreConfig.createDefault(); + protected final ForkChoiceStrategy dummyForkChoiceStrategy = mock(ForkChoiceStrategy.class); + protected void processChainWithLimitedCache( BiConsumer chainProcessor) { final int cacheSize = 10; @@ -117,6 +121,18 @@ protected void processCheckpointsWithLimitedCache( allCheckpoints.forEach(c -> chainProcessor.accept(store, c)); } + protected void processChainHeadWithMockForkChoiceStrategy( + BiConsumer chainProcessor) { + final StoreConfig pruningOptions = StoreConfig.builder().build(); + + final UpdatableStore store = createGenesisStoreWithMockForkChoiceStrategy(pruningOptions); + final List blocks = chainBuilder.generateBlocksUpToSlot(29); + + addBlocks(store, blocks); + + chainProcessor.accept(store, blocks.get(blocks.size() - 1)); + } + protected void addBlock(final UpdatableStore store, final SignedBlockAndState block) { addBlocks(store, List.of(block)); } @@ -134,7 +150,7 @@ protected UpdatableStore createGenesisStore() { return createGenesisStore(defaultStoreConfig); } - protected UpdatableStore createGenesisStore(final StoreConfig pruningOptions) { + protected StoreBuilder createStoreBuilder(final StoreConfig pruningOptions) { final SignedBlockAndState genesis = chainBuilder.generateGenesis(); final Checkpoint genesisCheckpoint = chainBuilder.getCurrentCheckpointForEpoch(0); return StoreBuilder.create() @@ -161,8 +177,16 @@ protected UpdatableStore createGenesisStore(final StoreConfig pruningOptions) { genesis.getExecutionBlockHash(), Optional.of(spec.calculateBlockCheckpoints(genesis.getState()))))) .storeConfig(pruningOptions) - .votes(emptyMap()) - .build(); + .votes(emptyMap()); + } + + protected UpdatableStore createGenesisStore(final StoreConfig pruningOptions) { + return createStoreBuilder(pruningOptions).build(); + } + + protected UpdatableStore createGenesisStoreWithMockForkChoiceStrategy( + final StoreConfig pruningOptions) { + return createStoreBuilder(pruningOptions).forkChoiceStrategy(dummyForkChoiceStrategy).build(); } protected BlockProvider blockProviderFromChainBuilder() { diff --git a/storage/src/test/java/tech/pegasys/teku/storage/store/StoreTest.java b/storage/src/test/java/tech/pegasys/teku/storage/store/StoreTest.java index 1c6eb07e0b5..3e2170c0458 100644 --- a/storage/src/test/java/tech/pegasys/teku/storage/store/StoreTest.java +++ b/storage/src/test/java/tech/pegasys/teku/storage/store/StoreTest.java @@ -15,6 +15,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import static tech.pegasys.teku.infrastructure.async.SafeFutureAssert.assertThatSafeFuture; import static tech.pegasys.teku.infrastructure.async.SafeFutureAssert.safeJoin; import static tech.pegasys.teku.infrastructure.async.SyncAsyncRunner.SYNC_RUNNER; @@ -32,11 +34,14 @@ import tech.pegasys.teku.infrastructure.metrics.StubMetricsSystem; import tech.pegasys.teku.infrastructure.unsigned.UInt64; import tech.pegasys.teku.spec.datastructures.blocks.BeaconBlock; +import tech.pegasys.teku.spec.datastructures.blocks.BlockCheckpoints; import tech.pegasys.teku.spec.datastructures.blocks.SignedBeaconBlock; import tech.pegasys.teku.spec.datastructures.blocks.SignedBlockAndState; import tech.pegasys.teku.spec.datastructures.blocks.SlotAndBlockRoot; import tech.pegasys.teku.spec.datastructures.blocks.StateAndBlockSummary; import tech.pegasys.teku.spec.datastructures.forkchoice.InvalidCheckpointException; +import tech.pegasys.teku.spec.datastructures.forkchoice.ProtoNodeData; +import tech.pegasys.teku.spec.datastructures.forkchoice.ProtoNodeValidationStatus; import tech.pegasys.teku.spec.datastructures.state.AnchorPoint; import tech.pegasys.teku.spec.datastructures.state.Checkpoint; import tech.pegasys.teku.spec.datastructures.state.CheckpointState; @@ -90,6 +95,196 @@ public void retrieveSignedBlock_withLimitedCache() { }); } + @Test + public void isHeadWeak_withoutNodeData() { + processChainHeadWithMockForkChoiceStrategy( + (store, blockAndState) -> { + final Bytes32 root = blockAndState.getRoot(); + final SafeFuture> isHeadWeakFuture = store.isHeadWeak(root); + assertThat(safeJoin(isHeadWeakFuture)).isEmpty(); + }); + } + + @Test + public void isHeadWeak_withSufficientWeightIsFalse() { + processChainHeadWithMockForkChoiceStrategy( + (store, blockAndState) -> { + final Bytes32 root = blockAndState.getRoot(); + setProtoNodeDataForBlock(blockAndState, UInt64.valueOf("2400000001"), UInt64.MAX_VALUE); + + final SafeFuture> isHeadWeakFuture = store.isHeadWeak(root); + assertThat(safeJoin(isHeadWeakFuture)).contains(false); + }); + } + + @Test + public void isHeadWeak_Boundary() { + processChainHeadWithMockForkChoiceStrategy( + (store, blockAndState) -> { + final Bytes32 root = blockAndState.getRoot(); + setProtoNodeDataForBlock(blockAndState, UInt64.valueOf("2399999999"), UInt64.MAX_VALUE); + + final SafeFuture> isHeadWeakFuture = store.isHeadWeak(root); + assertThat(safeJoin(isHeadWeakFuture)).contains(true); + }); + } + + @Test + public void isHeadWeak_withLowWeightIsTrue() { + processChainHeadWithMockForkChoiceStrategy( + (store, blockAndState) -> { + final Bytes32 root = blockAndState.getRoot(); + setProtoNodeDataForBlock(blockAndState, UInt64.valueOf("1000000000"), UInt64.MAX_VALUE); + + final SafeFuture> isHeadWeakFuture = store.isHeadWeak(root); + assertThat(safeJoin(isHeadWeakFuture)).contains(true); + }); + } + + @Test + public void isParentStrong_withoutNodeData() { + processChainHeadWithMockForkChoiceStrategy( + (store, blockAndState) -> { + final Bytes32 root = blockAndState.getBlock().getParentRoot(); + final SafeFuture> isParentStrongFuture = store.isParentStrong(root); + assertThat(safeJoin(isParentStrongFuture)).isEmpty(); + }); + } + + @Test + public void isParentStrong_withSufficientWeight() { + processChainHeadWithMockForkChoiceStrategy( + (store, blockAndState) -> { + final Bytes32 root = blockAndState.getBlock().getParentRoot(); + setProtoNodeDataForBlock(blockAndState, UInt64.ZERO, UInt64.valueOf("19200000001")); + final SafeFuture> isParentStrongFuture = store.isParentStrong(root); + assertThat(safeJoin(isParentStrongFuture)).contains(true); + }); + } + + @Test + public void isParentStrong_wityBoundaryWeight() { + processChainHeadWithMockForkChoiceStrategy( + (store, blockAndState) -> { + final Bytes32 root = blockAndState.getBlock().getParentRoot(); + setProtoNodeDataForBlock(blockAndState, UInt64.ZERO, UInt64.valueOf("19200000000")); + final SafeFuture> isParentStrongFuture = store.isParentStrong(root); + assertThat(safeJoin(isParentStrongFuture)).contains(false); + }); + } + + @Test + public void isParentStrong_wityZeroWeight() { + processChainHeadWithMockForkChoiceStrategy( + (store, blockAndState) -> { + final Bytes32 root = blockAndState.getBlock().getParentRoot(); + setProtoNodeDataForBlock(blockAndState, UInt64.ZERO, UInt64.ZERO); + final SafeFuture> isParentStrongFuture = store.isParentStrong(root); + assertThat(safeJoin(isParentStrongFuture)).contains(false); + }); + } + + @Test + public void isFfgCompetitive_checkpointMatches() { + final BlockCheckpoints headBlockCheckpoint = mock(BlockCheckpoints.class); + final BlockCheckpoints parentBlockCheckpoint = mock(BlockCheckpoints.class); + final Checkpoint checkpoint = new Checkpoint(UInt64.ZERO, Bytes32.random()); + when(headBlockCheckpoint.getUnrealizedJustifiedCheckpoint()).thenReturn(checkpoint); + when(parentBlockCheckpoint.getUnrealizedJustifiedCheckpoint()).thenReturn(checkpoint); + processChainHeadWithMockForkChoiceStrategy( + (store, blockAndState) -> { + final Bytes32 root = blockAndState.getRoot(); + final Bytes32 parentRoot = blockAndState.getBlock().getParentRoot(); + setProtoNodeDataForBlock(blockAndState, headBlockCheckpoint, parentBlockCheckpoint); + assertThat(store.isFfgCompetitive(root, parentRoot)).contains(true); + }); + } + + @Test + public void isFfgCompetitive_checkpointDifferent() { + final BlockCheckpoints headBlockCheckpoint = mock(BlockCheckpoints.class); + final BlockCheckpoints parentBlockCheckpoint = mock(BlockCheckpoints.class); + final Checkpoint checkpoint = new Checkpoint(UInt64.ZERO, Bytes32.random()); + final Checkpoint checkpointParent = new Checkpoint(UInt64.ONE, Bytes32.random()); + when(headBlockCheckpoint.getUnrealizedJustifiedCheckpoint()).thenReturn(checkpoint); + when(parentBlockCheckpoint.getUnrealizedJustifiedCheckpoint()).thenReturn(checkpointParent); + processChainHeadWithMockForkChoiceStrategy( + (store, blockAndState) -> { + final Bytes32 root = blockAndState.getRoot(); + final Bytes32 parentRoot = blockAndState.getBlock().getParentRoot(); + setProtoNodeDataForBlock(blockAndState, headBlockCheckpoint, parentBlockCheckpoint); + assertThat(store.isFfgCompetitive(root, parentRoot)).contains(false); + }); + } + + @Test + public void isFfgCompetitive_missingProtoNodeEntries() { + processChainHeadWithMockForkChoiceStrategy( + (store, blockAndState) -> { + final Bytes32 root = blockAndState.getRoot(); + final Bytes32 parentRoot = blockAndState.getBlock().getParentRoot(); + assertThat(store.isFfgCompetitive(root, parentRoot)).isEmpty(); + }); + } + + private void setProtoNodeDataForBlock( + SignedBlockAndState blockAndState, + BlockCheckpoints headCheckpoint, + BlockCheckpoints parentCheckpoint) { + final Bytes32 root = blockAndState.getRoot(); + final Bytes32 parentRoot = blockAndState.getParentRoot(); + final ProtoNodeData protoNodeData = + new ProtoNodeData( + UInt64.ONE, + root, + blockAndState.getParentRoot(), + blockAndState.getStateRoot(), + Bytes32.random(), + ProtoNodeValidationStatus.VALID, + headCheckpoint, + UInt64.ZERO); + final ProtoNodeData parentNodeData = + new ProtoNodeData( + UInt64.ZERO, + parentRoot, + Bytes32.random(), + blockAndState.getStateRoot(), + Bytes32.random(), + ProtoNodeValidationStatus.VALID, + parentCheckpoint, + UInt64.ZERO); + when(dummyForkChoiceStrategy.getBlockData(root)).thenReturn(Optional.of(protoNodeData)); + when(dummyForkChoiceStrategy.getBlockData(parentRoot)).thenReturn(Optional.of(parentNodeData)); + } + + private void setProtoNodeDataForBlock( + SignedBlockAndState blockAndState, final UInt64 headValue, final UInt64 parentValue) { + final Bytes32 root = blockAndState.getRoot(); + final Bytes32 parentRoot = blockAndState.getParentRoot(); + final ProtoNodeData protoNodeData = + new ProtoNodeData( + UInt64.ONE, + root, + blockAndState.getParentRoot(), + blockAndState.getStateRoot(), + Bytes32.random(), + ProtoNodeValidationStatus.VALID, + null, + headValue); + final ProtoNodeData parentNodeData = + new ProtoNodeData( + UInt64.ZERO, + parentRoot, + Bytes32.random(), + blockAndState.getStateRoot(), + Bytes32.random(), + ProtoNodeValidationStatus.VALID, + null, + parentValue); + when(dummyForkChoiceStrategy.getBlockData(root)).thenReturn(Optional.of(protoNodeData)); + when(dummyForkChoiceStrategy.getBlockData(parentRoot)).thenReturn(Optional.of(parentNodeData)); + } + @Test public void epochStatesCacheMostRecentlyAddedStates() { final int cacheSize = 2;