Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion packages/beacon-node/src/chain/errors/blobSidecarError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,16 @@ export enum BlobSidecarErrorCode {
FUTURE_SLOT = "BLOB_SIDECAR_ERROR_FUTURE_SLOT",
WOULD_REVERT_FINALIZED_SLOT = "BLOB_SIDECAR_ERROR_WOULD_REVERT_FINALIZED_SLOT",
ALREADY_KNOWN = "BLOB_SIDECAR_ERROR_ALREADY_KNOWN",
/** Already accepted a sidecar for this (block_root, index) tuple via gossip */
Comment thread
nflaig marked this conversation as resolved.
Outdated
ALREADY_SEEN_TUPLE = "BLOB_SIDECAR_ERROR_ALREADY_SEEN_TUPLE",
PARENT_UNKNOWN = "BLOB_SIDECAR_ERROR_PARENT_UNKNOWN",
PARENT_EXECUTION_INVALID = "BLOB_SIDECAR_ERROR_PARENT_EXECUTION_INVALID",
FINALIZED_NOT_ANCESTOR = "BLOB_SIDECAR_ERROR_FINALIZED_NOT_ANCESTOR",
NOT_LATER_THAN_PARENT = "BLOB_SIDECAR_ERROR_NOT_LATER_THAN_PARENT",
PROPOSAL_SIGNATURE_INVALID = "BLOB_SIDECAR_ERROR_PROPOSAL_SIGNATURE_INVALID",
INCLUSION_PROOF_INVALID = "BLOB_SIDECAR_ERROR_INCLUSION_PROOF_INVALID",
INCORRECT_PROPOSER = "BLOB_SIDECAR_ERROR_INCORRECT_PROPOSER",
PROPOSER_INDEX_OUT_OF_RANGE = "BLOB_SIDECAR_ERROR_PROPOSER_INDEX_OUT_OF_RANGE",
}

export type BlobSidecarErrorType =
Expand All @@ -50,12 +55,15 @@ export type BlobSidecarErrorType =
| {code: BlobSidecarErrorCode.FUTURE_SLOT; blockSlot: Slot; currentSlot: Slot}
| {code: BlobSidecarErrorCode.WOULD_REVERT_FINALIZED_SLOT; blockSlot: Slot; finalizedSlot: Slot}
| {code: BlobSidecarErrorCode.ALREADY_KNOWN; root: RootHex}
| {code: BlobSidecarErrorCode.ALREADY_SEEN_TUPLE; root: RootHex; blobIdx: number}
| {
code: BlobSidecarErrorCode.PARENT_UNKNOWN;
parentRoot: RootHex;
slot: Slot;
blockRoot: RootHex;
}
| {code: BlobSidecarErrorCode.PARENT_EXECUTION_INVALID; parentRoot: RootHex}
| {code: BlobSidecarErrorCode.FINALIZED_NOT_ANCESTOR; parentRoot: RootHex; finalizedRoot: RootHex}
| {code: BlobSidecarErrorCode.NOT_LATER_THAN_PARENT; parentSlot: Slot; slot: Slot}
| {
code: BlobSidecarErrorCode.PROPOSAL_SIGNATURE_INVALID;
Expand All @@ -64,7 +72,12 @@ export type BlobSidecarErrorType =
index: number;
}
| {code: BlobSidecarErrorCode.INCLUSION_PROOF_INVALID; slot: Slot; blobIdx: number}
| {code: BlobSidecarErrorCode.INCORRECT_PROPOSER; proposerIndex: ValidatorIndex};
| {code: BlobSidecarErrorCode.INCORRECT_PROPOSER; proposerIndex: ValidatorIndex}
| {
code: BlobSidecarErrorCode.PROPOSER_INDEX_OUT_OF_RANGE;
proposerIndex: ValidatorIndex;
validatorCount: number;
};

export class BlobSidecarGossipError extends GossipActionError<BlobSidecarErrorType> {}
export class BlobSidecarValidationError extends LodestarError<BlobSidecarErrorType> {}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ export enum BlsToExecutionChangeErrorCode {
ALREADY_EXISTS = "BLS_TO_EXECUTION_CHANGE_ERROR_ALREADY_EXISTS",
INVALID = "BLS_TO_EXECUTION_CHANGE_ERROR_INVALID",
INVALID_SIGNATURE = "BLS_TO_EXECUTION_CHANGE_ERROR_INVALID_SIGNATURE",
PRE_CAPELLA = "BLS_TO_EXECUTION_CHANGE_ERROR_PRE_CAPELLA",
}
export type BlsToExecutionChangeErrorType =
| {code: BlsToExecutionChangeErrorCode.ALREADY_EXISTS}
| {code: BlsToExecutionChangeErrorCode.INVALID}
| {code: BlsToExecutionChangeErrorCode.INVALID_SIGNATURE};
| {code: BlsToExecutionChangeErrorCode.INVALID_SIGNATURE}
| {code: BlsToExecutionChangeErrorCode.PRE_CAPELLA};

export class BlsToExecutionChangeError extends GossipActionError<BlsToExecutionChangeErrorType> {}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
isForkPostGloas,
} from "@lodestar/params";
import {computeStartSlotAtEpoch} from "@lodestar/state-transition";
import {BLSSignature, RootHex, SignedBeaconBlock, Slot, deneb, fulu} from "@lodestar/types";
import {BLSSignature, RootHex, SignedBeaconBlock, Slot, ValidatorIndex, deneb, fulu} from "@lodestar/types";
import {LodestarError, Logger, byteArrayEquals, pruneSetToMax} from "@lodestar/utils";
import {Metrics} from "../../metrics/metrics.js";
import {MAX_LOOK_AHEAD_EPOCHS} from "../../sync/constants.js";
Expand Down Expand Up @@ -111,6 +111,10 @@ export class SeenBlockInput {
// there should only 1 block root per slot but we need to always compare against rootHex
// and the signature to ensure we only skip verification if both match
private verifiedProposerSignatures = new Map<Slot, Map<RootHex, BLSSignature>>();
// Tracks blob sidecar tuples `(slot, proposer_index, blob_index)` already seen via
// gossip validation. Spec requires `[IGNORE]` for a subsequent sidecar matching the
// same tuple.
private blobSidecarTuples = new Map<Slot, Set<string>>();

constructor({
config,
Expand Down Expand Up @@ -411,6 +415,23 @@ export class SeenBlockInput {
seenMap.set(blockRootHex, signature);
}

/**
* Same proposer signing two different blocks at the same slot (equivocation)
* yields the same tuple, and only the first valid sidecar should propagate.
*/
isSeenBlobSidecar(slot: Slot, proposerIndex: ValidatorIndex, blobIndex: number): boolean {
return this.blobSidecarTuples.get(slot)?.has(`${proposerIndex}:${blobIndex}`) ?? false;
}

markSeenBlobSidecar(slot: Slot, proposerIndex: ValidatorIndex, blobIndex: number): void {
let seenSet = this.blobSidecarTuples.get(slot);
if (!seenSet) {
seenSet = new Set<string>();
this.blobSidecarTuples.set(slot, seenSet);
}
seenSet.add(`${proposerIndex}:${blobIndex}`);
}

private buildCommonProps(slot: Slot): {
daOutOfRange: boolean;
forkName: ForkName;
Expand Down Expand Up @@ -438,6 +459,7 @@ export class SeenBlockInput {
}
}
pruneSetToMax(this.verifiedProposerSignatures, MAX_BLOCK_INPUT_CACHE_SIZE);
pruneSetToMax(this.blobSidecarTuples, MAX_BLOCK_INPUT_CACHE_SIZE);
}

private evictBlockInput(blockInput: IBlockInput): void {
Expand Down
42 changes: 24 additions & 18 deletions packages/beacon-node/src/chain/validation/attestation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -638,26 +638,32 @@ export function verifyPropagationSlotRange(fork: ForkName, chain: IBeaconChain,
} else {
const attestationEpoch = computeEpochAtSlot(attestationSlot);

// upper bound for current epoch is same as epoch of latestPermissibleSlot
const latestPermissibleCurrentEpoch = computeEpochAtSlot(latestPermissibleSlot);
if (attestationEpoch > latestPermissibleCurrentEpoch) {
throw new AttestationError(GossipAction.IGNORE, {
code: AttestationErrorCode.FUTURE_EPOCH,
currentEpoch: latestPermissibleCurrentEpoch,
attestationEpoch,
});
}

// lower bound for previous epoch is same as epoch of earliestPermissibleSlot
const currentEpochWithPastTolerance = computeEpochAtSlot(
chain.clock.slotWithPastTolerance(chain.config.MAXIMUM_GOSSIP_CLOCK_DISPARITY / 1000)
);

const earliestPermissiblePreviousEpoch = Math.max(currentEpochWithPastTolerance - 1, 0);
if (attestationEpoch < earliestPermissiblePreviousEpoch) {
// EIP-7045: an attestation is valid for the current or previous epoch with
// MAXIMUM_GOSSIP_CLOCK_DISPARITY tolerance on each side of the epoch's slot range.
// We mirror is_within_slot_range from the spec by comparing milliseconds to the
// start/end of the epoch's slot range, so messages exactly at an epoch boundary plus
// disparity remain in-range.
const disparityMs = chain.config.MAXIMUM_GOSSIP_CLOCK_DISPARITY;
const isWithinEpochSlotRange = (epoch: Epoch): boolean => {
const epochStartSlot = computeStartSlotAtEpoch(epoch);
const epochEndSlot = epochStartSlot + SLOTS_PER_EPOCH;
return (
chain.clock.msFromSlot(epochStartSlot) >= -disparityMs && chain.clock.msFromSlot(epochEndSlot) <= disparityMs
);
};

if (!isWithinEpochSlotRange(attestationEpoch) && !isWithinEpochSlotRange(attestationEpoch + 1)) {
const latestPermissibleCurrentEpoch = computeEpochAtSlot(latestPermissibleSlot);
if (attestationEpoch > latestPermissibleCurrentEpoch) {
throw new AttestationError(GossipAction.IGNORE, {
code: AttestationErrorCode.FUTURE_EPOCH,
currentEpoch: latestPermissibleCurrentEpoch,
attestationEpoch,
});
}
throw new AttestationError(GossipAction.IGNORE, {
code: AttestationErrorCode.PAST_EPOCH,
previousEpoch: earliestPermissiblePreviousEpoch,
previousEpoch: Math.max(latestPermissibleCurrentEpoch - 1, 0),
attestationEpoch,
});
}
Expand Down
Loading
Loading