diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/payloadattestation/PayloadAttestationMessageGossipValidator.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/payloadattestation/PayloadAttestationMessageGossipValidator.java index d9ef242b66b..37136c72fc9 100644 --- a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/payloadattestation/PayloadAttestationMessageGossipValidator.java +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/payloadattestation/PayloadAttestationMessageGossipValidator.java @@ -20,6 +20,7 @@ import static tech.pegasys.teku.statetransition.validation.InternalValidationResult.ignore; import static tech.pegasys.teku.statetransition.validation.InternalValidationResult.reject; +import com.google.errorprone.annotations.FormatMethod; import it.unimi.dsi.fastutil.ints.IntSet; import java.util.Map; import java.util.Optional; @@ -69,77 +70,71 @@ public SafeFuture validate( final PayloadAttestationData data = validatablePayloadAttestationMessage.getData(); /* - * [IGNORE] The message's slot is for the current slot (with a MAXIMUM_GOSSIP_CLOCK_DISPARITY allowance), - * i.e. data.slot == current_slot + * [IGNORE] The payload attestation's slot is for the current slot */ if (!gossipValidationHelper.isSlotCurrent(data.getSlot())) { - LOG.trace( - "Ignoring payload attestation with slot {} from validator with index {} because it's not from the current slot", - data.getSlot(), - payloadAttestationMessage.getValidatorIndex()); return completedFuture( - ignore( + ignorePayloadAttestation( + payloadAttestationMessage, "Ignoring payload attestation with slot %s from validator with index %s because it's not from the current slot", - data.getSlot(), payloadAttestationMessage.getValidatorIndex())); + data.getSlot(), + payloadAttestationMessage.getValidatorIndex())); } /* - * [IGNORE] The payload_attestation_message is the first valid message received from the validator - * with index payload_attestation_message.validate_index + * [IGNORE] This is the first valid payload attestation from this validator index */ final ValidatorIndexAndSlot key = new ValidatorIndexAndSlot(payloadAttestationMessage.getValidatorIndex(), data.getSlot()); if (seenPayloadAttestations.contains(key)) { - return completedFuture(ignoreAttestationAlreadySeenValidationResult(key)); + return completedFuture( + ignorePayloadAttestationAlreadySeenValidationResult(payloadAttestationMessage)); } /* - * [REJECT] The message's block data.beacon_block_root passes validation. - * Check this before the availability check so that a known-invalid block root is rejected - * immediately rather than treated as an unseen block and queued for future processing. + * [REJECT] The payload attestation's block passes validation */ if (invalidBlockRoots.containsKey(data.getBeaconBlockRoot())) { - LOG.trace("Payload attestations's block with root {} is invalid", data.getBeaconBlockRoot()); return completedFuture( - reject( - "Payload attestations's block with root %s is invalid", data.getBeaconBlockRoot())); + rejectPayloadAttestation( + payloadAttestationMessage, + "Payload attestations's block with root %s is invalid", + data.getBeaconBlockRoot())); } /* - * [IGNORE] The message's block data.beacon_block_root has been seen (via gossip or non-gossip sources) - * (a client MAY queue attestation for processing once the block is retrieved. - * Note a client might want to request payload after). + * [IGNORE] The payload attestation's block has been seen (via gossip or non-gossip sources) + * (MAY be queued until block is retrieved) */ if (!gossipValidationHelper.isBlockAvailable(data.getBeaconBlockRoot())) { - LOG.trace( - "Payload attestations's block with root {} is not available. Saving for future processing", - data.getBeaconBlockRoot()); - return completedFuture(SAVE_FOR_FUTURE); + return completedFuture( + savePayloadAttestationForFuture( + payloadAttestationMessage, + "Payload attestations's block with root %s is not available", + data.getBeaconBlockRoot())); } /* - * [IGNORE] The block referenced by data.beacon_block_root is at slot data.slot, - * i.e. the block has block.slot == data.slot. + * [IGNORE] The payload attestation's block is at the assigned slot */ final Optional maybeBlockSlot = gossipValidationHelper.getSlotForBlockRoot(data.getBeaconBlockRoot()); if (maybeBlockSlot.isEmpty()) { - LOG.trace( - "Payload attestations's block with root {} has no known slot. Saving for future processing", - data.getBeaconBlockRoot()); - return completedFuture(SAVE_FOR_FUTURE); + return completedFuture( + savePayloadAttestationForFuture( + payloadAttestationMessage, + "Payload attestations's block with root %s has no known slot", + data.getBeaconBlockRoot())); } final UInt64 blockSlot = maybeBlockSlot.get(); if (!blockSlot.equals(data.getSlot())) { - LOG.trace( - "Payload attestations's block with root {} is at slot {} but attestation is for slot {}", - data.getBeaconBlockRoot(), - blockSlot, - data.getSlot()); return completedFuture( - ignore( + ignorePayloadAttestation( + payloadAttestationMessage, "Payload attestations's block with root %s is at slot %s but attestation is for slot %s", - data.getBeaconBlockRoot(), blockSlot, data.getSlot())); + data.getBeaconBlockRoot(), + blockSlot, + data.getSlot())); } // The block has just been checked to be at data.slot, so the state to validate against is its @@ -150,54 +145,118 @@ public SafeFuture validate( .thenApply( maybeState -> { if (maybeState.isEmpty()) { - LOG.trace( - "State for block root {} and slot {} is unavailable", + return savePayloadAttestationForFuture( + payloadAttestationMessage, + "State for block root %s and slot %s is unavailable", data.getBeaconBlockRoot(), data.getSlot()); - return SAVE_FOR_FUTURE; } final BeaconState state = maybeState.get(); + final UInt64 validatorIndex = payloadAttestationMessage.getValidatorIndex(); + /* - * [REJECT] The message's validator index is within the payload committee in get_ptc(state, data.slot). - * The state is the head state corresponding to processing the block up to the current slot as determined - * by the fork choice. + * [REJECT] The validator index is valid + */ + if (validatorIndex.isGreaterThanOrEqualTo(state.getValidators().size())) { + return rejectPayloadAttestation( + payloadAttestationMessage, + "Payload attestation's validator index %s is out of range for the %s validators in the state", + validatorIndex, + state.getValidators().size()); + } + + /* + * [REJECT] The validator is a member of the payload timeliness committee */ final IntSet ptcPositions = validatablePayloadAttestationMessage.calculatePtcPositions(spec, state); if (ptcPositions.isEmpty()) { - LOG.trace( - "Payload attestation's validator index {} is not in the payload committee for slot {}", - payloadAttestationMessage.getValidatorIndex(), - data.getSlot()); - return reject( + return rejectPayloadAttestation( + payloadAttestationMessage, "Payload attestation's validator index %s is not in the payload committee", - payloadAttestationMessage.getValidatorIndex()); + validatorIndex); } /* - * [REJECT] payload_attestation_message.signature is valid with respect to the validator's public key. + * [REJECT] The signature is valid */ if (!isSignatureValid(payloadAttestationMessage, state)) { - return reject("Invalid payload attestation signature"); + return rejectPayloadAttestation( + payloadAttestationMessage, "Invalid payload attestation signature"); } if (!seenPayloadAttestations.add(key)) { - return ignoreAttestationAlreadySeenValidationResult(key); + return ignorePayloadAttestationAlreadySeenValidationResult( + payloadAttestationMessage); } else { - return ACCEPT; + return acceptPayloadAttestation(payloadAttestationMessage); } }); } - private InternalValidationResult ignoreAttestationAlreadySeenValidationResult( - final ValidatorIndexAndSlot key) { - LOG.trace( - "Payload attestation for slot {} and validator index {} already seen", - key.slot(), - key.validatorIndex()); - return ignore( + private InternalValidationResult ignorePayloadAttestationAlreadySeenValidationResult( + final PayloadAttestationMessage payloadAttestationMessage) { + return ignorePayloadAttestation( + payloadAttestationMessage, "Payload attestation for slot %s and validator index %s already seen", - key.slot(), key.validatorIndex()); + payloadAttestationMessage.getData().getSlot(), + payloadAttestationMessage.getValidatorIndex()); + } + + private InternalValidationResult acceptPayloadAttestation( + final PayloadAttestationMessage payloadAttestationMessage) { + LOG.trace( + "PayloadAttestation Gossip Validation Result: ACCEPT, context: {}", + formatPayloadAttestationContext(payloadAttestationMessage)); + return ACCEPT; + } + + @FormatMethod + private InternalValidationResult rejectPayloadAttestation( + final PayloadAttestationMessage payloadAttestationMessage, + final String descriptionTemplate, + final Object... args) { + final String message = String.format(descriptionTemplate, args); + LOG.trace( + "PayloadAttestation Gossip Validation Result: REJECT, context: {}, reason: {}", + formatPayloadAttestationContext(payloadAttestationMessage), + message); + return reject("%s", message); + } + + @FormatMethod + private InternalValidationResult ignorePayloadAttestation( + final PayloadAttestationMessage payloadAttestationMessage, + final String descriptionTemplate, + final Object... args) { + final String message = String.format(descriptionTemplate, args); + LOG.trace( + "PayloadAttestation Gossip Validation Result: IGNORE, context: {}, reason: {}", + formatPayloadAttestationContext(payloadAttestationMessage), + message); + return ignore("%s", message); + } + + @FormatMethod + private InternalValidationResult savePayloadAttestationForFuture( + final PayloadAttestationMessage payloadAttestationMessage, + final String descriptionTemplate, + final Object... args) { + final String message = String.format(descriptionTemplate, args); + LOG.trace( + "PayloadAttestation Gossip Validation Result: SAVE_FOR_FUTURE, context: {}, reason: {}", + formatPayloadAttestationContext(payloadAttestationMessage), + message); + return SAVE_FOR_FUTURE; + } + + private String formatPayloadAttestationContext( + final PayloadAttestationMessage payloadAttestationMessage) { + return String.format( + "validator index %s, slot %s, block root %s", + payloadAttestationMessage.getValidatorIndex(), + payloadAttestationMessage.getData().getSlot(), + payloadAttestationMessage.getData().getBeaconBlockRoot()); } private boolean isSignatureValid( diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/validation/ExecutionPayloadBidGossipValidator.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/validation/ExecutionPayloadBidGossipValidator.java index 805afac54c1..9bc19fb8be5 100644 --- a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/validation/ExecutionPayloadBidGossipValidator.java +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/validation/ExecutionPayloadBidGossipValidator.java @@ -81,7 +81,7 @@ public SafeFuture validate( final ExecutionPayloadBid bid = signedExecutionPayloadBid.getMessage(); /* - * [REJECT] bid.execution_payment is zero. + * [REJECT] The bid's execution payment is zero */ final UInt64 executionPayment = bid.getExecutionPayment(); if (!executionPayment.isZero()) { @@ -98,7 +98,7 @@ public SafeFuture validate( } /* - * [IGNORE] bid.slot is the current slot or the next slot. + * [IGNORE] The bid's slot is the current slot or the next slot */ if (!gossipValidationHelper.isSlotCurrentOrNext(bid.getSlot())) { return completedFuture( @@ -106,8 +106,7 @@ public SafeFuture validate( } /* - * [REJECT] the number of bid.blob_kzg_commitments is within the limit for the bid's epoch - * -- i.e. len(bid.blob_kzg_commitments) <= get_blob_parameters(proposal_epoch).max_blobs_per_block. + * [REJECT] The bid's blob KZG commitment count is within the per-epoch limit */ final Optional maybeMaxBlobsPerBlock = spec.getMaxBlobsPerBlockAtSlot(bid.getSlot()); if (maybeMaxBlobsPerBlock.isPresent()) { @@ -124,7 +123,8 @@ public SafeFuture validate( } /* - * Retrieve the bid's parent block slot for the remaining validation rules. + * [IGNORE] The bid's parent block root is a known beacon block + * (MAY be queued until parent is imported) */ final Optional maybeParentBlockSlot = gossipValidationHelper.getSlotForBlockRoot(bid.getParentBlockRoot()); @@ -138,7 +138,14 @@ public SafeFuture validate( final UInt64 parentBlockSlot = maybeParentBlockSlot.get(); /* - * [REJECT] The bid is for a higher slot than its parent block. + * [IGNORE] The bid's slot is within the parent's proposer lookahead + */ + if (!gossipValidationHelper.isWithinParentProposerLookahead(bid.getSlot(), parentBlockSlot)) { + return completedFuture(ignoreBid(bid, "bid's slot is past the parent's proposer lookahead")); + } + + /* + * [REJECT] The bid is for a higher slot than its parent block */ if (!bid.getSlot().isGreaterThan(parentBlockSlot)) { return completedFuture( @@ -153,6 +160,10 @@ public SafeFuture validate( .getParentStateInBlockEpoch(parentBlockSlot, bid.getParentBlockRoot(), bid.getSlot()) .thenApply( maybeState -> { + /* + * [IGNORE] The bid's parent block has been imported + * (MAY be queued until parent is imported) + */ if (maybeState.isEmpty()) { return saveBidForFuture( bid, @@ -181,8 +192,7 @@ public SafeFuture validate( } /* - * [REJECT] bid.fee_recipient matches the fee_recipient from the proposer's - * SignedProposerPreferences associated with bid.slot + * [IGNORE] The bid's fee recipient matches the proposer's preference */ if (!bid.getFeeRecipient().equals(proposerPreferences.get().getFeeRecipient())) { return ignoreBid( @@ -193,8 +203,7 @@ public SafeFuture validate( } /* - * [IGNORE] this is the first signed bid seen with a valid signature from the given builder for the tuple - * (bid.slot, bid.parent_block_hash, bid.parent_block_root). + * [IGNORE] This is the first bid for this slot, parent, and builder */ final BuilderAndParent builderAndParent = new BuilderAndParent( @@ -204,19 +213,15 @@ public SafeFuture validate( .contains(builderAndParent)) { return ignoreBid( bid, - "already received for parent block hash %s and parent block root %s", + "already received valid bid for slot %s, parent block hash %s, parent block root %s, and builder index %s", + bid.getSlot(), bid.getParentBlockHash(), - bid.getParentBlockRoot()); + bid.getParentBlockRoot(), + bid.getBuilderIndex()); } /* - * [IGNORE] this bid is the highest value bid seen for the tuple - * (bid.slot, bid.parent_block_hash, bid.parent_block_root). - * - * Note: Implementations SHOULD include DoS prevention measures to - * mitigate spam from malicious builders submitting numerous bids with minimal value increments. - * Possible strategies include: (1) only forwarding bids that exceed the current highest bid by a - * minimum threshold, or (2) forwarding only the highest observed bid at regular time intervals. + * [IGNORE] This is the highest value bid seen for the slot and parent * */ final BidParent bidValueKey = @@ -236,9 +241,7 @@ public SafeFuture validate( } /* - * [IGNORE] bid.parent_block_hash is the block hash of a known execution payload in fork choice - * and is_gas_limit_target_compatible(parent_gas_limit, bid.gas_limit, proposer_preferences.target_gas_limit) - * is True where parent_gas_limit is the gas_limit of that execution payload. + * [IGNORE] The bid's parent block hash is the hash of a known execution payload */ final Optional maybeParentGasLimit = gossipValidationHelper.getGasLimitForExecutionPayload( @@ -249,6 +252,10 @@ public SafeFuture validate( "parent execution payload gas limit is unavailable for parent block hash %s; saving for future processing", bid.getParentBlockHash()); } + + /* + * [IGNORE] The bid's gas limit is compatible with the proposer's target gas limit + */ final UInt64 parentGasLimit = maybeParentGasLimit.get(); final UInt64 targetGasLimit = proposerPreferences.get().getTargetGasLimit(); if (!isGasLimitTargetCompatible(parentGasLimit, bid.getGasLimit(), targetGasLimit)) { @@ -261,8 +268,7 @@ public SafeFuture validate( } /* - * [IGNORE] The bid is compatible with the current head branch, i.e. - * is_bid_compatible_with_head(store, bid) returns True. + * [IGNORE] The bid is compatible with the current head branch */ if (!gossipValidationHelper.isBidCompatibleWithHead(bid)) { return ignoreBid( @@ -273,8 +279,7 @@ public SafeFuture validate( } /* - * [REJECT] bid.prev_randao is the correct RANDAO mix -- i.e. validate that - * bid.prev_randao == get_randao_mix(parent_state, get_current_epoch(parent_state)). + * [REJECT] The bid's previous randao is correct */ final Bytes32 expectedRandaoMix = gossipValidationHelper.getRandaoMixForCurrentEpoch(state, bid.getSlot()); @@ -286,15 +291,11 @@ public SafeFuture validate( expectedRandaoMix); } + final SszList builders = BeaconStateGloas.required(state).getBuilders(); + /* - * [REJECT] bid.builder_index is a valid/active builder index -- i.e. is_active_builder(state, bid.builder_index) returns True - */ - /* - * [REJECT] bid.builder_index is within range -- i.e. - * bid.builder_index < len(state.builders). Checked explicitly rather than relying on - * isActiveBuilder so that the builder lookups below are always in bounds. + * [REJECT] The builder index is valid */ - final SszList builders = BeaconStateGloas.required(state).getBuilders(); if (bid.getBuilderIndex().isGreaterThanOrEqualTo(builders.size())) { return rejectBid( bid, @@ -303,6 +304,10 @@ public SafeFuture validate( builders.size()); } + /* + * [REJECT] The builder is active + */ + if (!gossipValidationHelper.isActiveBuilder( bid.getBuilderIndex(), state, bid.getSlot())) { return rejectBid( @@ -312,8 +317,7 @@ public SafeFuture validate( } /* - * [REJECT] the builder is a payload builder -- i.e. - * state.builders[bid.builder_index].version == PAYLOAD_BUILDER_VERSION. + * [REJECT] The builder is a payload builder */ final Builder builder = builders.get(bid.getBuilderIndex().intValue()); final int builderVersion = builder.getVersion(); @@ -327,8 +331,7 @@ public SafeFuture validate( } /* - * [IGNORE] bid.value is less or equal than the builder's excess balance - * -- i.e. MIN_ACTIVATION_BALANCE + bid.value <= state.balances[bid.builder_index]. + * [IGNORE] The builder can cover the bid */ if (!gossipValidationHelper.builderHasEnoughBalanceForBid( bid.getValue(), bid.getBuilderIndex(), state, bid.getSlot())) { @@ -336,7 +339,7 @@ public SafeFuture validate( } /* - * [IGNORE] The parent's payload does not try to exit the builder. + * [IGNORE] The parent's payload does not try to exit the builder */ if (bid.getParentBlockHash() .equals( @@ -372,7 +375,7 @@ public SafeFuture validate( } /* - * [REJECT] signed_execution_payload_bid.signature is valid with respect to the bid.builder_index. + * [REJECT] The bid signature is valid */ if (!isSignatureValid(signedExecutionPayloadBid, state)) { return rejectBid(bid, "invalid execution payload bid signature"); diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/validation/ExecutionPayloadGossipValidator.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/validation/ExecutionPayloadGossipValidator.java index 311d68c045d..2098d10ab90 100644 --- a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/validation/ExecutionPayloadGossipValidator.java +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/validation/ExecutionPayloadGossipValidator.java @@ -107,8 +107,7 @@ public SafeFuture validate( && broadcastValidationLevel .map(CONSENSUS_AND_EQUIVOCATION::equals) .orElse(false)) { - // consensus_and_equivocation: reject if the envelope's beacon block is an - // equivocation, before it is broadcast + // Extra broadcast-level equivocation check return performEquivocationCheck(envelope); } return SafeFuture.completedFuture(markAsSeen(result, envelope)); @@ -178,7 +177,7 @@ private SafeFuture> performWithBlockValidatio final ExecutionPayloadBid bid = maybeExecutionPayloadBid.get(); /* - * [REJECT] envelope.builder_index == bid.builder_index + * [REJECT] The envelope is from the builder committed to by the bid */ if (!envelope.getBuilderIndex().equals(bid.getBuilderIndex())) { LOG.trace( @@ -191,7 +190,7 @@ private SafeFuture> performWithBlockValidatio envelope.getBuilderIndex(), bid.getBuilderIndex())); } /* - * [REJECT] payload.block_hash == bid.block_hash + * [REJECT] The payload's block hash matches the bid's block hash */ final ExecutionPayload payload = envelope.getPayload(); final Bytes32 payloadBlockHash = payload.getBlockHash(); @@ -208,7 +207,7 @@ private SafeFuture> performWithBlockValidatio } /* - * [REJECT] hash_tree_root(envelope.execution_requests) == bid.execution_requests_root + * [REJECT] The envelope's execution requests root matches the bid's execution requests root */ final Bytes32 executionRequestsRoot = envelope.getExecutionRequests().hashTreeRoot(); final Bytes32 bidExecutionRequestsRoot = bid.getExecutionRequestsRoot(); @@ -230,14 +229,14 @@ private SafeFuture> performWithBlockValidatio private Optional performPreBlockValidation( final ExecutionPayloadEnvelope envelope) { /* - * [IGNORE] The node has not seen another valid SignedExecutionPayloadEnvelope for this block root from this builder. + * [IGNORE] The node has not seen another valid envelope for this block root from this builder */ if (seenPayloads.contains(envelope.getBlockRootAndBuilderIndex())) { return Optional.of(ignoreExecutionPayloadAlreadySeen(envelope)); } /* - * [REJECT] block passes validation + * [REJECT] The envelope's block passes validation */ if (invalidBlockRoots.containsKey(envelope.getBeaconBlockRoot())) { LOG.trace( @@ -253,8 +252,8 @@ private Optional performPreBlockValidation( gossipValidationHelper.getSlotForBlockRoot(envelope.getBeaconBlockRoot()); /* - * [SAVE_FOR_FUTURE] The envelope's block root envelope.block_root has been seen (via gossip or non-gossip sources) - * (a client MAY queue payload for processing once the block is retrieved) + * [IGNORE] The envelope's block root has been seen (via gossip or non-gossip sources) + * (MAY be queued until block is retrieved) */ if (maybeBeaconBlockSlot.isEmpty()) { LOG.trace( @@ -265,7 +264,6 @@ private Optional performPreBlockValidation( /* * [IGNORE] The envelope is from a slot greater than or equal to the latest finalized slot - * -- i.e. validate that envelope.slot >= compute_start_slot_at_epoch(store.finalized_checkpoint.epoch) */ if (gossipValidationHelper.isBeforeFinalizedSlot(envelope.getSlot())) { LOG.trace( @@ -278,7 +276,7 @@ private Optional performPreBlockValidation( } /* - * [REJECT] block.slot equals envelope.slot + * [REJECT] The block's slot matches the payload's slot number */ final UInt64 beaconBlockSlot = maybeBeaconBlockSlot.get(); if (!envelope.getSlot().equals(beaconBlockSlot)) { @@ -295,12 +293,6 @@ private Optional performPreBlockValidation( return verifyRequestAndWithdrawalLimits(envelope); } - /** - * [REJECT] Each execution request count, and the withdrawal count, is within its configured limit - * -- i.e. spec {@code verify_execution_requests_limits} plus the MAX_WITHDRAWALS_PER_PAYLOAD - * check. These bound the work an envelope can impose before it is propagated, so they are checked - * ahead of the expensive state lookup and signature verification. - */ private Optional verifyRequestAndWithdrawalLimits( final ExecutionPayloadEnvelope envelope) { final SpecConfigGloas config = @@ -308,6 +300,9 @@ private Optional verifyRequestAndWithdrawalLimits( final ExecutionRequestsGloas executionRequests = ExecutionRequestsGloas.required(envelope.getExecutionRequests()); + /* + * [REJECT] The execution request counts are within their limits + */ final Optional requestLimitResult = Stream.of( rejectIfOverLimit( @@ -332,6 +327,9 @@ private Optional verifyRequestAndWithdrawalLimits( return requestLimitResult; } + /* + * [REJECT] The number of withdrawals is within the limit + */ return rejectIfOverLimit( "withdrawals", ExecutionPayloadCapella.required(envelope.getPayload()).getWithdrawals().size(), @@ -354,12 +352,6 @@ private Optional rejectIfOverLimit( count, description, limit)); } - /** - * The envelope's slot has already been checked to equal the slot of its beacon block, so the - * state to validate against is the block's own post state. It is looked up by block root rather - * than by slot and block root to keep the checkpoint state task queue off the path a payload has - * to travel before it can be propagated. - */ private SafeFuture performWithStateValidation( final SignedExecutionPayloadEnvelope envelope) { return gossipValidationHelper @@ -373,7 +365,7 @@ private SafeFuture performWithStateValidation( return SAVE_FOR_FUTURE; } /* - * [REJECT] signed_execution_payload_envelope.signature is valid with respect to the builder's public key + * [REJECT] The envelope signature is valid */ if (!isSignatureValid(envelope, maybeState.get())) { LOG.trace("Invalid signed execution payload envelope signature. Rejecting"); diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/validation/GossipValidationHelper.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/validation/GossipValidationHelper.java index 2352de56590..5d0f533cbed 100644 --- a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/validation/GossipValidationHelper.java +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/validation/GossipValidationHelper.java @@ -83,6 +83,27 @@ public boolean isSlotFromFuture(final UInt64 slot) { return slot.isGreaterThan(maxCurrSlot); } + /** + * Returns true when the proposer for {@code proposalSlot} is known, which happens once the + * lookahead epoch has started. + */ + public boolean isWithinProposerLookahead(final UInt64 proposalSlot) { + final int minSeedLookahead = spec.atSlot(proposalSlot).getConfig().getMinSeedLookahead(); + final UInt64 lookaheadEpoch = + spec.computeEpochAtSlot(proposalSlot).minusMinZero(minSeedLookahead); + final UInt64 lookaheadEpochStartSlot = spec.computeStartSlotAtEpoch(lookaheadEpoch); + return !isSlotFromFuture(lookaheadEpochStartSlot); + } + + /** Returns true when the proposal slot is within the parent block's proposer lookahead. */ + public boolean isWithinParentProposerLookahead( + final UInt64 proposalSlot, final UInt64 parentBlockSlot) { + final UInt64 proposalEpoch = spec.computeEpochAtSlot(proposalSlot); + final UInt64 parentEpoch = spec.computeEpochAtSlot(parentBlockSlot); + final int minSeedLookahead = spec.getSpecConfig(proposalEpoch).getMinSeedLookahead(); + return proposalEpoch.isLessThanOrEqualTo(parentEpoch.plus(minSeedLookahead)); + } + public boolean isEpochFromFuture(final UInt64 epoch) { return isSlotFromFuture(spec.computeStartSlotAtEpoch(epoch)); } diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/validation/ProposerPreferencesGossipValidator.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/validation/ProposerPreferencesGossipValidator.java index 2eacb27b6ad..53b2c37c043 100644 --- a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/validation/ProposerPreferencesGossipValidator.java +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/validation/ProposerPreferencesGossipValidator.java @@ -70,17 +70,24 @@ public SafeFuture validate( final Bytes32 dependentRoot = proposerPreferences.getDependentRoot(); /* - * The proposer lookahead for proposal_slot is fixed as of the start of - * compute_epoch_at_slot(proposal_slot) - MIN_SEED_LOOKAHEAD, so that epoch and its start slot - * anchor the remaining rules. + * [IGNORE] These are the first valid preferences seen for this dependent root and slot */ - final int minSeedLookahead = spec.atSlot(proposalSlot).getConfig().getMinSeedLookahead(); - final UInt64 lookaheadEpoch = - spec.computeEpochAtSlot(proposalSlot).minusMinZero(minSeedLookahead); - final UInt64 lookaheadEpochStartSlot = spec.computeStartSlotAtEpoch(lookaheadEpoch); + final ProposerPreferencesDedupKey dedupKey = + new ProposerPreferencesDedupKey(proposalSlot, dependentRoot); + if (seenProposerPreferences.contains(dedupKey)) { + return completedFuture(ignoreAlreadySeen(proposerPreferences)); + } + + final UInt64 proposalEpoch = spec.computeEpochAtSlot(proposalSlot); + /* + * [IGNORE] The proposal epoch is after the Gloas upgrade + */ + if (!spec.isProposerPreferencesAvailableAtEpoch(proposalEpoch)) { + return completedFuture(ignorePreferences(proposerPreferences, "proposal epoch is pre-gloas")); + } /* - * [IGNORE] The proposal slot has not started yet. + * [IGNORE] The proposal slot has not started yet */ if (gossipValidationHelper.hasSlotStarted(proposalSlot)) { return completedFuture( @@ -88,18 +95,17 @@ public SafeFuture validate( } /* - * [IGNORE] The proposer for the proposal slot is known -- i.e. the lookahead epoch has started, - * so its proposer lookahead can be computed. + * [IGNORE] The proposer for the proposal slot is known */ - if (gossipValidationHelper.isSlotFromFuture(lookaheadEpochStartSlot)) { + if (!gossipValidationHelper.isWithinProposerLookahead(proposalSlot)) { return completedFuture( ignorePreferences( proposerPreferences, "proposer for the proposal slot is not yet known")); } /* - * [IGNORE] The block with root preferences.dependent_root has been seen - * (a client MAY queue preferences for processing once the block is retrieved). + * [IGNORE] The dependent block has been seen (via gossip or non-gossip sources) + * (MAY be queued until block is retrieved) */ if (!gossipValidationHelper.isBlockAvailable(dependentRoot)) { return completedFuture( @@ -108,21 +114,12 @@ public SafeFuture validate( "dependent root has not been seen; saving for future processing")); } - /* - * [IGNORE] The signed_proposer_preferences is the first valid message for the tuple - * (preferences.proposal_slot, preferences.dependent_root). The validator index is deliberately - * excluded: only one validator can be the proposer for that pair, so keying on it as well would - * let a peer force full validation of arbitrarily many messages for the same slot. - */ - final ProposerPreferencesDedupKey dedupKey = - new ProposerPreferencesDedupKey(proposalSlot, dependentRoot); - if (seenProposerPreferences.contains(dedupKey)) { - return completedFuture(ignoreAlreadySeen(proposerPreferences)); - } + final int minSeedLookahead = spec.atSlot(proposalSlot).getConfig().getMinSeedLookahead(); + final UInt64 lookaheadEpoch = proposalEpoch.minusMinZero(minSeedLookahead); + final UInt64 lookaheadEpochStartSlot = spec.computeStartSlotAtEpoch(lookaheadEpoch); /* - * [REJECT] The dependent root is before the proposer lookahead epoch. A dependent root at or - * after that boundary cannot be the latest block preceding the epoch. + * [REJECT] The dependent block's slot is not after the shuffling dependent slot */ final Optional maybeDependentRootSlot = recentChainData.getSlotForBlockRoot(dependentRoot); @@ -139,9 +136,7 @@ public SafeFuture validate( } /* - * [IGNORE] The dependent root is a possible dependent block for the lookahead epoch. Without - * this the checkpoint state below would be built by skipping slots that already contain blocks, - * yielding a proposer lookahead that no branch actually agrees with. + * [IGNORE] The dependent block is a possible dependent block for the proposer lookahead */ if (!gossipValidationHelper.isPossibleDependentRoot(dependentRoot, lookaheadEpochStartSlot)) { return completedFuture( @@ -149,55 +144,69 @@ public SafeFuture validate( proposerPreferences, "dependent root is not a possible dependent block")); } - return recentChainData - .retrieveCheckpointState(new Checkpoint(lookaheadEpoch, dependentRoot)) - .thenApply( - maybeState -> { - if (maybeState.isEmpty()) { - return savePreferencesForFuture( - proposerPreferences, - "checkpoint state for lookahead epoch %s is unavailable; saving for future processing", - lookaheadEpoch); - } - final BeaconState state = maybeState.get(); - + return gossipValidationHelper + .getStateAtBlockRoot(dependentRoot) + .thenCompose( + maybeDependentState -> { /* - * [REJECT] The validator is the proposer for the given slot in the proposer lookahead - * of the checkpoint state at (lookahead_epoch, preferences.dependent_root). + * [IGNORE] The dependent block passes validation */ - final int lookaheadIndex = - proposalSlot.minusMinZero(lookaheadEpochStartSlot).intValue(); - final UInt64 expectedValidatorIndex = - BeaconStateFulu.required(state).getProposerLookahead().getElement(lookaheadIndex); - if (!expectedValidatorIndex.equals(proposerPreferences.getValidatorIndex())) { - return rejectPreferences( - proposerPreferences, - "validator index does not match expected proposer %s", - expectedValidatorIndex); + if (maybeDependentState.isEmpty()) { + return completedFuture( + ignorePreferences( + proposerPreferences, "dependent root has not passed validation")); } - /* - * [REJECT] signed_proposer_preferences.signature is valid with respect to - * the validator's public key - */ - if (!isSignatureValid(signedProposerPreferences, state)) { - return rejectPreferences(proposerPreferences, "invalid signature"); - } - - if (!seenProposerPreferences.add(dedupKey)) { - return ignoreAlreadySeen(proposerPreferences); - } - - return acceptPreferences(proposerPreferences); + return recentChainData + .retrieveCheckpointState(new Checkpoint(lookaheadEpoch, dependentRoot)) + .thenApply( + maybeState -> { + if (maybeState.isEmpty()) { + return savePreferencesForFuture( + proposerPreferences, + "checkpoint state for lookahead epoch %s is unavailable; saving for future processing", + lookaheadEpoch); + } + final BeaconState state = maybeState.get(); + + /* + * [REJECT] The validator is the proposer for the given slot in the proposer lookahead + */ + final int lookaheadIndex = + proposalSlot.minusMinZero(lookaheadEpochStartSlot).intValue(); + final UInt64 expectedValidatorIndex = + BeaconStateFulu.required(state) + .getProposerLookahead() + .getElement(lookaheadIndex); + if (!expectedValidatorIndex.equals( + proposerPreferences.getValidatorIndex())) { + return rejectPreferences( + proposerPreferences, + "validator index does not match expected proposer %s", + expectedValidatorIndex); + } + + /* + * [REJECT] The signature is valid + */ + if (!isSignatureValid(signedProposerPreferences, state)) { + return rejectPreferences(proposerPreferences, "invalid signature"); + } + + if (!seenProposerPreferences.add(dedupKey)) { + return ignoreAlreadySeen(proposerPreferences); + } + + return acceptPreferences(proposerPreferences); + }); }) .exceptionally( - error -> { - return rejectPreferencesWithError( - proposerPreferences, - error, - "unable to generate checkpoint state for lookahead epoch %s", - lookaheadEpoch); - }); + error -> + rejectPreferencesWithError( + proposerPreferences, + error, + "unable to generate checkpoint state for lookahead epoch %s", + lookaheadEpoch)); } private InternalValidationResult acceptPreferences( diff --git a/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/payloadattestation/PayloadAttestationMessageGossipValidatorTest.java b/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/payloadattestation/PayloadAttestationMessageGossipValidatorTest.java index d61e3ee28f1..d2d5fe67961 100644 --- a/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/payloadattestation/PayloadAttestationMessageGossipValidatorTest.java +++ b/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/payloadattestation/PayloadAttestationMessageGossipValidatorTest.java @@ -72,11 +72,19 @@ void setup(final TestSpecInvocationContextProvider.SpecContext specContext) { new PayloadAttestationMessageGossipValidator( spec, gossipValidationHelper, invalidBlockRoots); - payloadAttestationMessage = dataStructureUtil.randomPayloadAttestationMessage(); + final PayloadAttestationMessage randomPayloadAttestationMessage = + dataStructureUtil.randomPayloadAttestationMessage(); + postState = dataStructureUtil.randomBeaconState(); + validatorIndex = UInt64.ZERO; + payloadAttestationMessage = + randomPayloadAttestationMessage + .getSchema() + .create( + validatorIndex, + randomPayloadAttestationMessage.getData(), + randomPayloadAttestationMessage.getSignature()); slot = payloadAttestationMessage.getData().getSlot(); - validatorIndex = payloadAttestationMessage.getValidatorIndex(); blockRoot = payloadAttestationMessage.getData().getBeaconBlockRoot(); - postState = dataStructureUtil.randomBeaconState(); when(gossipValidationHelper.isSlotCurrent(slot)).thenReturn(true); when(gossipValidationHelper.isBlockAvailable(blockRoot)).thenReturn(true); @@ -220,6 +228,26 @@ void shouldSaveForFuture_whenStateIsUnavailable() { .isCompletedWithValue(SAVE_FOR_FUTURE); } + @TestTemplate + void shouldReject_whenValidatorIndexIsOutOfRange() { + validatorIndex = UInt64.valueOf(postState.getValidators().size()); + payloadAttestationMessage = + payloadAttestationMessage + .getSchema() + .create( + validatorIndex, + payloadAttestationMessage.getData(), + payloadAttestationMessage.getSignature()); + + assertThatSafeFuture( + payloadAttestationMessageGossipValidator.validate( + validatablePayloadAttestationMessage())) + .isCompletedWithValue( + reject( + "Payload attestation's validator index %s is out of range for the %s validators in the state", + validatorIndex, postState.getValidators().size())); + } + @TestTemplate void shouldReject_whenValidatorNotInPtcCommittee() { when(spec.getPtc(postState, slot)).thenReturn(IntList.of(validatorIndex.intValue() + 1)); diff --git a/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/validation/ExecutionPayloadBidGossipValidatorTest.java b/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/validation/ExecutionPayloadBidGossipValidatorTest.java index dc155c6ae6c..6983eeb8fb1 100644 --- a/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/validation/ExecutionPayloadBidGossipValidatorTest.java +++ b/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/validation/ExecutionPayloadBidGossipValidatorTest.java @@ -144,6 +144,7 @@ void setup(final TestSpecInvocationContextProvider.SpecContext specContext) { .thenReturn(Optional.of(proposerPreferences)); when(gossipValidationHelper.isSlotCurrentOrNext(slot)).thenReturn(true); + when(gossipValidationHelper.isWithinParentProposerLookahead(any(), any())).thenReturn(true); when(gossipValidationHelper.getShufflingDependentRoot(parentBlockRoot, slot)) .thenReturn(Optional.of(dependentRoot)); when(gossipValidationHelper.getGasLimitForExecutionPayload(parentBlockRoot, parentBlockHash)) @@ -205,6 +206,69 @@ void shouldIgnore_whenSlotIsNotCurrentOrNext() { ignoreBid(signedBid, "must be for current or next slot but was for slot %s", slot)); } + @TestTemplate + void shouldIgnore_whenBidSlotIsPastParentProposerLookahead() { + final UInt64 parentBlockSlot = slot.decrement(); + when(gossipValidationHelper.isWithinParentProposerLookahead(slot, parentBlockSlot)) + .thenReturn(false); + + assertThatSafeFuture(bidValidator.validate(signedBid)) + .isCompletedWithValue( + ignoreBid(signedBid, "bid's slot is past the parent's proposer lookahead")); + } + + @TestTemplate + void shouldReject_whenBlobKzgCommitmentCountExceedsLimit() { + final int maxBlobsPerBlock = 0; + when(spec.getMaxBlobsPerBlockAtSlot(slot)).thenReturn(Optional.of(maxBlobsPerBlock)); + final SignedExecutionPayloadBid bidWithTooManyBlobCommitments = + signedBidWithBlobKzgCommitments(maxBlobsPerBlock + 1); + + assertThatSafeFuture(bidValidator.validate(bidWithTooManyBlobCommitments)) + .isCompletedWithValue( + rejectBid( + bidWithTooManyBlobCommitments, + "has %s blob kzg commitments which exceeds the maximum of %s for the slot", + maxBlobsPerBlock + 1, + maxBlobsPerBlock)); + } + + @TestTemplate + void shouldReject_whenBuilderIndexIsOutOfRange() { + final UInt64 invalidBuilderIndex = UInt64.valueOf(8); + final SignedExecutionPayloadBid bidWithInvalidBuilderIndex = + signedBidForParent(parentBlockHash, parentBlockRoot, invalidBuilderIndex, bid.getValue()); + + assertThatSafeFuture(bidValidator.validate(bidWithInvalidBuilderIndex)) + .isCompletedWithValue( + rejectBid( + bidWithInvalidBuilderIndex, + "builder index %s is out of range for the %s builders in the state", + invalidBuilderIndex, + 8)); + } + + @TestTemplate + void shouldReject_whenBuilderIsNotPayloadBuilder() { + final BeaconState stateWithNonPayloadBuilder = + stateWithBuilderVersion(PAYLOAD_BUILDER_VERSION + 1); + when(gossipValidationHelper.getParentStateInBlockEpoch(slot.decrement(), parentBlockRoot, slot)) + .thenReturn(SafeFuture.completedFuture(Optional.of(stateWithNonPayloadBuilder))); + when(gossipValidationHelper.isActiveBuilder(builderIndex, stateWithNonPayloadBuilder, slot)) + .thenReturn(true); + when(gossipValidationHelper.getRandaoMixForCurrentEpoch(stateWithNonPayloadBuilder, slot)) + .thenReturn(bid.getPrevRandao()); + + assertThatSafeFuture(bidValidator.validate(signedBid)) + .isCompletedWithValue( + rejectBid( + signedBid, + "builder index %s has version %s but only payload builder version %s may bid", + builderIndex, + PAYLOAD_BUILDER_VERSION + 1, + PAYLOAD_BUILDER_VERSION)); + } + @TestTemplate void shouldSaveForFuture_whenProposerPreferencesNotSeen() { when(proposerPreferencesManager.getProposerPreferences(slot, dependentRoot)) @@ -346,9 +410,11 @@ void shouldIgnore_whenAlreadySeen() { .isCompletedWithValue( ignoreBid( signedBid, - "already received for parent block hash %s and parent block root %s", + "already received valid bid for slot %s, parent block hash %s, parent block root %s, and builder index %s", + slot, parentBlockHash, - parentBlockRoot)); + parentBlockRoot, + builderIndex)); } @TestTemplate @@ -652,9 +718,11 @@ void shouldIgnoreSeenBid() { .isCompletedWithValue( ignoreBid( signedBid, - "already received for parent block hash %s and parent block root %s", + "already received valid bid for slot %s, parent block hash %s, parent block root %s, and builder index %s", + slot, parentBlockHash, - parentBlockRoot)); + parentBlockRoot, + builderIndex)); } @TestTemplate @@ -707,9 +775,11 @@ void shouldIgnoreBidWhenBuilderAddedDuringValidation() { .isCompletedWithValue( ignoreBid( signedBid, - "already received for parent block hash %s and parent block root %s", + "already received valid bid for slot %s, parent block hash %s, parent block root %s, and builder index %s", + slot, parentBlockHash, - parentBlockRoot)); + parentBlockRoot, + builderIndex)); } @TestTemplate @@ -751,9 +821,11 @@ void shouldEvictOldestSlotAfterMaxSlotsTracked() { .isCompletedWithValue( ignoreBid( bidForCachedSlot, - "already received for parent block hash %s and parent block root %s", + "already received valid bid for slot %s, parent block hash %s, parent block root %s, and builder index %s", + cachedSlot, parentBlockHash, - parentBlockRoot)); + parentBlockRoot, + sameBuilder)); } @TestTemplate @@ -949,6 +1021,32 @@ private SignedExecutionPayloadBid signedBidWithBlockHash(final Bytes32 blockHash return dataStructureUtil.randomSignedExecutionPayloadBid(bidWithBlockHash); } + private SignedExecutionPayloadBid signedBidWithBlobKzgCommitments( + final int blobKzgCommitmentsCount) { + final var bidBlobKzgCommitments = + schemaDefinitions + .getExecutionPayloadBidSchema() + .getBlobKzgCommitmentsSchema() + .createFromElements( + dataStructureUtil.randomBlobKzgCommitments(blobKzgCommitmentsCount).asList()); + final ExecutionPayloadBid bidWithBlobKzgCommitments = + bid.getSchema() + .create( + bid.getParentBlockHash(), + bid.getParentBlockRoot(), + bid.getBlockHash(), + bid.getPrevRandao(), + bid.getFeeRecipient(), + bid.getGasLimit(), + bid.getBuilderIndex(), + bid.getSlot(), + bid.getValue(), + bid.getExecutionPayment(), + bidBlobKzgCommitments, + bid.getExecutionRequestsRoot()); + return dataStructureUtil.randomSignedExecutionPayloadBid(bidWithBlobKzgCommitments); + } + private SignedExecutionPayloadBid signedBidForParent( final Bytes32 parentHash, final Bytes32 parentRoot, @@ -981,6 +1079,17 @@ private SignedExecutionPayloadBid signedBidForParent( return dataStructureUtil.randomSignedExecutionPayloadBid(bidForParent); } + private BeaconState stateWithBuilderVersion(final int builderVersion) { + return postState.updated( + mutableState -> { + final SszMutableList builders = + MutableBeaconStateGloas.required(mutableState).getBuilders(); + builders.set( + builderIndex.intValue(), + dataStructureUtil.builderBuilder().version(builderVersion).build()); + }); + } + private void mockProposerPreferences( final SignedExecutionPayloadBid signedBid, final UInt64 targetGasLimit) { final ProposerPreferences proposerPreferences = mock(ProposerPreferences.class); diff --git a/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/validation/GossipValidationHelperTest.java b/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/validation/GossipValidationHelperTest.java index 0fce3667fa9..8293d23481e 100644 --- a/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/validation/GossipValidationHelperTest.java +++ b/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/validation/GossipValidationHelperTest.java @@ -146,6 +146,53 @@ void isSlotFromFuture_shouldComputeCorrectly() { assertThat(gossipValidationHelper.isSlotFromFuture(slot2)).isFalse(); } + @TestTemplate + void isWithinProposerLookahead_shouldComputeCorrectly() { + final UInt64 proposalSlot = UInt64.valueOf(100); + final int minSeedLookahead = spec.atSlot(proposalSlot).getConfig().getMinSeedLookahead(); + final UInt64 lookaheadEpoch = + spec.computeEpochAtSlot(proposalSlot).minusMinZero(minSeedLookahead); + final UInt64 lookaheadEpochStartSlot = spec.computeStartSlotAtEpoch(lookaheadEpoch); + final UInt64 lookaheadEpochStartTimeMillis = + spec.computeTimeMillisAtSlot( + lookaheadEpochStartSlot, + secondsToMillis( + recentChainData.getBestState().orElseThrow().getImmediately().getGenesisTime())); + + final UInt64 notYetInsideTolerance = + lookaheadEpochStartTimeMillis + .minusMinZero(gossipValidationHelper.getMaxOffsetTimeInMillis()) + .decrement(); + storageSystem.chainUpdater().setTimeMillis(notYetInsideTolerance); + assertThat(gossipValidationHelper.isWithinProposerLookahead(proposalSlot)).isFalse(); + + final UInt64 insideTolerance = + lookaheadEpochStartTimeMillis.minusMinZero( + gossipValidationHelper.getMaxOffsetTimeInMillis()); + storageSystem.chainUpdater().setTimeMillis(insideTolerance); + assertThat(gossipValidationHelper.isWithinProposerLookahead(proposalSlot)).isTrue(); + } + + @TestTemplate + void isWithinParentProposerLookahead_shouldUseParentEpoch() { + final UInt64 parentEpoch = UInt64.valueOf(2); + final UInt64 parentBlockSlot = spec.computeStartSlotAtEpoch(parentEpoch); + final int minSeedLookahead = spec.getSpecConfig(parentEpoch).getMinSeedLookahead(); + final UInt64 lastAllowedEpoch = parentEpoch.plus(minSeedLookahead); + final UInt64 lastAllowedProposalSlot = spec.computeStartSlotAtEpoch(lastAllowedEpoch); + final UInt64 outsideLookaheadProposalSlot = + spec.computeStartSlotAtEpoch(lastAllowedEpoch.plus(1)); + + assertThat( + gossipValidationHelper.isWithinParentProposerLookahead( + lastAllowedProposalSlot, parentBlockSlot)) + .isTrue(); + assertThat( + gossipValidationHelper.isWithinParentProposerLookahead( + outsideLookaheadProposalSlot, parentBlockSlot)) + .isFalse(); + } + @TestTemplate void isEpochFromFuture_shouldComputeCorrectly() { final UInt64 epoch2 = UInt64.valueOf(2); diff --git a/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/validation/ProposerPreferencesGossipValidatorTest.java b/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/validation/ProposerPreferencesGossipValidatorTest.java index 56f47f6f90b..240731e6d2d 100644 --- a/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/validation/ProposerPreferencesGossipValidatorTest.java +++ b/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/validation/ProposerPreferencesGossipValidatorTest.java @@ -96,8 +96,11 @@ void setUp(final SpecContext specContext) { lookaheadEpochStartSlot = spec.computeStartSlotAtEpoch(lookaheadEpoch); when(gossipValidationHelper.hasSlotStarted(proposalSlot)).thenReturn(false); + when(gossipValidationHelper.isWithinProposerLookahead(proposalSlot)).thenReturn(true); when(gossipValidationHelper.isSlotFromFuture(lookaheadEpochStartSlot)).thenReturn(false); when(gossipValidationHelper.isBlockAvailable(dependentRoot)).thenReturn(true); + when(gossipValidationHelper.getStateAtBlockRoot(dependentRoot)) + .thenReturn(SafeFuture.completedFuture(Optional.of(state))); when(gossipValidationHelper.isPossibleDependentRoot(dependentRoot, lookaheadEpochStartSlot)) .thenReturn(true); when(gossipValidationHelper.isSignatureValidWithRespectToProposerIndex( @@ -131,7 +134,7 @@ void shouldIgnore_whenProposalSlotHasAlreadyStarted() { @TestTemplate void shouldIgnore_whenLookaheadEpochHasNotStarted() { - when(gossipValidationHelper.isSlotFromFuture(lookaheadEpochStartSlot)).thenReturn(true); + when(gossipValidationHelper.isWithinProposerLookahead(proposalSlot)).thenReturn(false); assertThatSafeFuture(validator.validate(signedProposerPreferences)) .isCompletedWithValue( ignorePreferences( @@ -139,6 +142,23 @@ void shouldIgnore_whenLookaheadEpochHasNotStarted() { verify(recentChainData, never()).retrieveCheckpointState(any(Checkpoint.class)); } + @TestTemplate + void shouldIgnore_whenProposalEpochIsPreGloas() { + final Spec preGloasSpec = mock(Spec.class); + when(preGloasSpec.computeEpochAtSlot(proposalSlot)).thenReturn(lookaheadEpoch); + when(preGloasSpec.isProposerPreferencesAvailableAtEpoch(lookaheadEpoch)).thenReturn(false); + + final ProposerPreferencesGossipValidator preGloasValidator = + new ProposerPreferencesGossipValidator( + preGloasSpec, gossipValidationHelper, recentChainData); + + assertThatSafeFuture(preGloasValidator.validate(signedProposerPreferences)) + .isCompletedWithValue( + ignorePreferences(signedProposerPreferences, "proposal epoch is pre-gloas")); + verify(gossipValidationHelper, never()).getStateAtBlockRoot(any()); + verify(recentChainData, never()).retrieveCheckpointState(any(Checkpoint.class)); + } + @TestTemplate void shouldIgnore_whenDependentRootIsNotAPossibleDependentBlock() { when(gossipValidationHelper.isPossibleDependentRoot(dependentRoot, lookaheadEpochStartSlot)) @@ -150,6 +170,18 @@ void shouldIgnore_whenDependentRootIsNotAPossibleDependentBlock() { verify(recentChainData, never()).retrieveCheckpointState(any(Checkpoint.class)); } + @TestTemplate + void shouldIgnore_whenDependentBlockHasNotPassedValidation() { + when(gossipValidationHelper.getStateAtBlockRoot(dependentRoot)) + .thenReturn(SafeFuture.completedFuture(Optional.empty())); + + assertThatSafeFuture(validator.validate(signedProposerPreferences)) + .isCompletedWithValue( + ignorePreferences( + signedProposerPreferences, "dependent root has not passed validation")); + verify(recentChainData, never()).retrieveCheckpointState(any(Checkpoint.class)); + } + @TestTemplate void shouldIgnoreDuplicate_whenOnlyTheValidatorIndexDiffers() { assertThatSafeFuture(validator.validate(signedProposerPreferences))