Skip to content

Commit

Permalink
feat: extend attestation validity till end of next epoch - eip7045 (#…
Browse files Browse the repository at this point in the history
…5731)

* feat: extend attestation validity till end of next epoch - eip7045

upgrade spec version

Review PR

update p2p validations

fix tests

* fix tests
  • Loading branch information
g11tech authored Jul 19, 2023
1 parent 4df3774 commit 29314c9
Show file tree
Hide file tree
Showing 16 changed files with 137 additions and 47 deletions.
3 changes: 2 additions & 1 deletion packages/beacon-node/src/api/impl/beacon/pool/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,9 @@ export function getBeaconPoolApi({
await Promise.all(
attestations.map(async (attestation, i) => {
try {
const fork = chain.config.getForkName(chain.clock.currentSlot);
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
const validateFn = () => validateApiAttestation(chain, {attestation, serializedData: null});
const validateFn = () => validateApiAttestation(fork, chain, {attestation, serializedData: null});
const {slot, beaconBlockRoot} = attestation.data;
// when a validator is configured with multiple beacon node urls, this attestation data may come from another beacon node
// and the block hasn't been in our forkchoice since we haven't seen / processing that block
Expand Down
3 changes: 2 additions & 1 deletion packages/beacon-node/src/api/impl/validator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -556,13 +556,14 @@ export function getValidatorApi({

const seenTimestampSec = Date.now() / 1000;
const errors: Error[] = [];
const fork = chain.config.getForkName(chain.clock.currentSlot);

await Promise.all(
signedAggregateAndProofs.map(async (signedAggregateAndProof, i) => {
try {
// TODO: Validate in batch
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
const validateFn = () => validateApiAggregateAndProof(chain, signedAggregateAndProof);
const validateFn = () => validateApiAggregateAndProof(fork, chain, signedAggregateAndProof);
const {slot, beaconBlockRoot} = signedAggregateAndProof.message.aggregate.data;
// when a validator is configured with multiple beacon node urls, this attestation may come from another beacon node
// and the block hasn't been in our forkchoice since we haven't seen / processing that block
Expand Down
2 changes: 1 addition & 1 deletion packages/beacon-node/src/chain/errors/attestationError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ export type AttestationErrorType =
| {code: AttestationErrorCode.NOT_EXACTLY_ONE_AGGREGATION_BIT_SET}
| {code: AttestationErrorCode.PRIOR_ATTESTATION_KNOWN; validatorIndex: ValidatorIndex; epoch: Epoch}
| {code: AttestationErrorCode.FUTURE_EPOCH; attestationEpoch: Epoch; currentEpoch: Epoch}
| {code: AttestationErrorCode.PAST_EPOCH; attestationEpoch: Epoch; currentEpoch: Epoch}
| {code: AttestationErrorCode.PAST_EPOCH; attestationEpoch: Epoch; previousEpoch: Epoch}
| {code: AttestationErrorCode.ATTESTS_TO_FUTURE_BLOCK; block: Slot; attestation: Slot}
| {code: AttestationErrorCode.INVALID_SUBNET_ID; received: number; expected: number}
| {code: AttestationErrorCode.WRONG_NUMBER_OF_AGGREGATION_BITS}
Expand Down
13 changes: 10 additions & 3 deletions packages/beacon-node/src/chain/validation/aggregateAndProof.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {toHexString} from "@chainsafe/ssz";
import {ForkName} from "@lodestar/params";
import {phase0, RootHex, ssz, ValidatorIndex} from "@lodestar/types";
import {
computeEpochAtSlot,
Expand Down Expand Up @@ -26,23 +27,29 @@ export type AggregateAndProofValidationResult = {
};

export async function validateApiAggregateAndProof(
fork: ForkName,
chain: IBeaconChain,
signedAggregateAndProof: phase0.SignedAggregateAndProof
): Promise<AggregateAndProofValidationResult> {
const skipValidationKnownAttesters = true;
const prioritizeBls = true;
return validateAggregateAndProof(chain, signedAggregateAndProof, null, {skipValidationKnownAttesters, prioritizeBls});
return validateAggregateAndProof(fork, chain, signedAggregateAndProof, null, {
skipValidationKnownAttesters,
prioritizeBls,
});
}

export async function validateGossipAggregateAndProof(
fork: ForkName,
chain: IBeaconChain,
signedAggregateAndProof: phase0.SignedAggregateAndProof,
serializedData: Uint8Array
): Promise<AggregateAndProofValidationResult> {
return validateAggregateAndProof(chain, signedAggregateAndProof, serializedData);
return validateAggregateAndProof(fork, chain, signedAggregateAndProof, serializedData);
}

async function validateAggregateAndProof(
fork: ForkName,
chain: IBeaconChain,
signedAggregateAndProof: phase0.SignedAggregateAndProof,
serializedData: Uint8Array | null = null,
Expand Down Expand Up @@ -87,7 +94,7 @@ async function validateAggregateAndProof(
// [IGNORE] aggregate.data.slot is within the last ATTESTATION_PROPAGATION_SLOT_RANGE slots (with a MAXIMUM_GOSSIP_CLOCK_DISPARITY allowance)
// -- i.e. aggregate.data.slot + ATTESTATION_PROPAGATION_SLOT_RANGE >= current_slot >= aggregate.data.slot
// (a client MAY queue future aggregates for processing at the appropriate slot).
verifyPropagationSlotRange(chain, attSlot);
verifyPropagationSlotRange(fork, chain, attSlot);
}

// [IGNORE] The aggregate is the first valid aggregate received for the aggregator with
Expand Down
69 changes: 51 additions & 18 deletions packages/beacon-node/src/chain/validation/attestation.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {toHexString} from "@chainsafe/ssz";
import {phase0, Epoch, Root, Slot, RootHex, ssz} from "@lodestar/types";
import {ProtoBlock} from "@lodestar/fork-choice";
import {ATTESTATION_SUBNET_COUNT, SLOTS_PER_EPOCH} from "@lodestar/params";
import {ATTESTATION_SUBNET_COUNT, SLOTS_PER_EPOCH, ForkName, ForkSeq} from "@lodestar/params";
import {
computeEpochAtSlot,
CachedBeaconStateAllForks,
Expand Down Expand Up @@ -56,12 +56,13 @@ const SHUFFLING_LOOK_AHEAD_EPOCHS = 1;
* - do not prioritize bls signature set
*/
export async function validateGossipAttestation(
fork: ForkName,
chain: IBeaconChain,
attestationOrBytes: GossipAttestation,
/** Optional, to allow verifying attestations through API with unknown subnet */
subnet: number
): Promise<AttestationValidationResult> {
return validateAttestation(chain, attestationOrBytes, subnet);
return validateAttestation(fork, chain, attestationOrBytes, subnet);
}

/**
Expand All @@ -71,18 +72,20 @@ export async function validateGossipAttestation(
* - prioritize bls signature set
*/
export async function validateApiAttestation(
fork: ForkName,
chain: IBeaconChain,
attestationOrBytes: ApiAttestation
): Promise<AttestationValidationResult> {
const prioritizeBls = true;
return validateAttestation(chain, attestationOrBytes, null, prioritizeBls);
return validateAttestation(fork, chain, attestationOrBytes, null, prioritizeBls);
}

/**
* Only deserialize the attestation if needed, use the cached AttestationData instead
* This is to avoid deserializing similar attestation multiple times which could help the gc
*/
async function validateAttestation(
fork: ForkName,
chain: IBeaconChain,
attestationOrBytes: AttestationOrBytes,
/** Optional, to allow verifying attestations through API with unknown subnet */
Expand Down Expand Up @@ -146,7 +149,7 @@ async function validateAttestation(
// [IGNORE] attestation.data.slot is within the last ATTESTATION_PROPAGATION_SLOT_RANGE slots (within a MAXIMUM_GOSSIP_CLOCK_DISPARITY allowance)
// -- i.e. attestation.data.slot + ATTESTATION_PROPAGATION_SLOT_RANGE >= current_slot >= attestation.data.slot
// (a client MAY queue future attestations for processing at the appropriate slot).
verifyPropagationSlotRange(chain, attestationOrCache.attestation.data.slot);
verifyPropagationSlotRange(fork, chain, attestationOrCache.attestation.data.slot);
}

// [REJECT] The attestation is unaggregated -- that is, it has exactly one participating validator
Expand Down Expand Up @@ -343,29 +346,59 @@ async function validateAttestation(
* Accounts for `MAXIMUM_GOSSIP_CLOCK_DISPARITY`.
* Note: We do not queue future attestations for later processing
*/
export function verifyPropagationSlotRange(chain: IBeaconChain, attestationSlot: Slot): void {
export function verifyPropagationSlotRange(fork: ForkName, chain: IBeaconChain, attestationSlot: Slot): void {
// slot with future tolerance of MAXIMUM_GOSSIP_CLOCK_DISPARITY_SEC
const latestPermissibleSlot = chain.clock.slotWithFutureTolerance(MAXIMUM_GOSSIP_CLOCK_DISPARITY_SEC);
const earliestPermissibleSlot = Math.max(
// slot with past tolerance of MAXIMUM_GOSSIP_CLOCK_DISPARITY_SEC
// ATTESTATION_PROPAGATION_SLOT_RANGE = SLOTS_PER_EPOCH
chain.clock.slotWithPastTolerance(MAXIMUM_GOSSIP_CLOCK_DISPARITY_SEC) - SLOTS_PER_EPOCH,
0
);
if (attestationSlot < earliestPermissibleSlot) {
throw new AttestationError(GossipAction.IGNORE, {
code: AttestationErrorCode.PAST_SLOT,
earliestPermissibleSlot,
attestationSlot,
});
}
if (attestationSlot > latestPermissibleSlot) {
throw new AttestationError(GossipAction.IGNORE, {
code: AttestationErrorCode.FUTURE_SLOT,
latestPermissibleSlot,
attestationSlot,
});
}

const earliestPermissibleSlot = Math.max(
// slot with past tolerance of MAXIMUM_GOSSIP_CLOCK_DISPARITY_SEC
// ATTESTATION_PROPAGATION_SLOT_RANGE = SLOTS_PER_EPOCH
chain.clock.slotWithPastTolerance(MAXIMUM_GOSSIP_CLOCK_DISPARITY_SEC) - SLOTS_PER_EPOCH,
0
);

// Post deneb the attestations are valid for current as well as previous epoch
// while pre deneb they are valid for ATTESTATION_PROPAGATION_SLOT_RANGE
//
// see: https://github.com/ethereum/consensus-specs/pull/3360
if (ForkSeq[fork] < ForkSeq.deneb) {
if (attestationSlot < earliestPermissibleSlot) {
throw new AttestationError(GossipAction.IGNORE, {
code: AttestationErrorCode.PAST_SLOT,
earliestPermissibleSlot,
attestationSlot,
});
}
} 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 earliestPermissiblePreviousEpoch = computeEpochAtSlot(earliestPermissibleSlot);
if (attestationEpoch < earliestPermissiblePreviousEpoch) {
throw new AttestationError(GossipAction.IGNORE, {
code: AttestationErrorCode.PAST_EPOCH,
previousEpoch: earliestPermissiblePreviousEpoch,
attestationEpoch,
});
}
}
}

/**
Expand Down
8 changes: 6 additions & 2 deletions packages/beacon-node/src/network/processor/gossipHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,9 +192,10 @@ export function getGossipHandlers(modules: ValidatorFnsModules, options: GossipH
[GossipType.beacon_aggregate_and_proof]: async ({serializedData}, topic, _peer, seenTimestampSec) => {
let validationResult: AggregateAndProofValidationResult;
const signedAggregateAndProof = sszDeserialize(topic, serializedData);
const {fork} = topic;

try {
validationResult = await validateGossipAggregateAndProof(chain, signedAggregateAndProof, serializedData);
validationResult = await validateGossipAggregateAndProof(fork, chain, signedAggregateAndProof, serializedData);
} catch (e) {
if (e instanceof AttestationError && e.action === GossipAction.REJECT) {
chain.persistInvalidSszValue(ssz.phase0.SignedAggregateAndProof, signedAggregateAndProof, "gossip_reject");
Expand Down Expand Up @@ -229,14 +230,17 @@ export function getGossipHandlers(modules: ValidatorFnsModules, options: GossipH
chain.emitter.emit(routes.events.EventType.attestation, signedAggregateAndProof.message.aggregate);
},

[GossipType.beacon_attestation]: async ({serializedData, msgSlot}, {subnet}, _peer, seenTimestampSec) => {
[GossipType.beacon_attestation]: async ({serializedData, msgSlot}, topic, _peer, seenTimestampSec) => {
if (msgSlot == undefined) {
throw Error("msgSlot is undefined for beacon_attestation topic");
}
const {subnet, fork} = topic;

// do not deserialize gossipSerializedData here, it's done in validateGossipAttestation only if needed
let validationResult: AttestationValidationResult;
try {
validationResult = await validateGossipAttestation(
fork,
chain,
{attestation: null, serializedData, attSlot: msgSlot},
subnet
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ describe("validate gossip signedAggregateAndProof", () => {
chain.seenAggregatedAttestations["aggregateRootsByEpoch"].clear();
},
fn: async () => {
await validateApiAggregateAndProof(chain, agg);
const fork = chain.config.getForkName(stateSlot);
await validateApiAggregateAndProof(fork, chain, agg);
},
});

Expand All @@ -37,7 +38,8 @@ describe("validate gossip signedAggregateAndProof", () => {
chain.seenAggregatedAttestations["aggregateRootsByEpoch"].clear();
},
fn: async () => {
await validateGossipAggregateAndProof(chain, agg, serializedData);
const fork = chain.config.getForkName(stateSlot);
await validateGossipAggregateAndProof(fork, chain, agg, serializedData);
},
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,17 @@ describe("validate attestation", () => {
id: `validate api attestation - ${id}`,
beforeEach: () => chain.seenAttesters["validatorIndexesByEpoch"].clear(),
fn: async () => {
await validateApiAttestation(chain, {attestation: att, serializedData: null});
const fork = chain.config.getForkName(stateSlot);
await validateApiAttestation(fork, chain, {attestation: att, serializedData: null});
},
});

itBench({
id: `validate gossip attestation - ${id}`,
beforeEach: () => chain.seenAttesters["validatorIndexesByEpoch"].clear(),
fn: async () => {
await validateGossipAttestation(chain, {attestation: null, serializedData, attSlot: slot}, subnet);
const fork = chain.config.getForkName(stateSlot);
await validateGossipAttestation(fork, chain, {attestation: null, serializedData, attSlot: slot}, subnet);
},
});
}
Expand Down
2 changes: 1 addition & 1 deletion packages/beacon-node/test/spec/specTestVersioning.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {DownloadTestsOptions} from "@lodestar/spec-test-util";
const __dirname = path.dirname(fileURLToPath(import.meta.url));

export const ethereumConsensusSpecsTests: DownloadTestsOptions = {
specVersion: "v1.4.0-alpha.3",
specVersion: "v1.4.0-beta.0",
// Target directory is the host package root: 'packages/*/spec-tests'
outputDir: path.join(__dirname, "../../spec-tests"),
specTestsRepoUrl: "https://github.com/ethereum/consensus-spec-tests",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ describe("chain / validation / aggregateAndProof", () => {
it("Valid", async () => {
const {chain, signedAggregateAndProof} = getValidData({});

await validateApiAggregateAndProof(chain, signedAggregateAndProof);
const fork = chain.config.getForkName(stateSlot);
await validateApiAggregateAndProof(fork, chain, signedAggregateAndProof);
});

it("BAD_TARGET_EPOCH", async () => {
Expand Down Expand Up @@ -188,9 +189,10 @@ describe("chain / validation / aggregateAndProof", () => {
signedAggregateAndProof: phase0.SignedAggregateAndProof,
errorCode: AttestationErrorCode
): Promise<void> {
const fork = chain.config.getForkName(stateSlot);
const serializedData = ssz.phase0.SignedAggregateAndProof.serialize(signedAggregateAndProof);
await expectRejectedWithLodestarError(
validateGossipAggregateAndProof(chain, signedAggregateAndProof, serializedData),
validateGossipAggregateAndProof(fork, chain, signedAggregateAndProof, serializedData),
errorCode
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ describe("chain / validation / attestation", () => {
it("Valid", async () => {
const {chain, attestation} = getValidData();

await validateApiAttestation(chain, {attestation, serializedData: null});
const fork = chain.config.getForkName(stateSlot);
await validateApiAttestation(fork, chain, {attestation, serializedData: null});
});

it("INVALID_SERIALIZED_BYTES_ERROR_CODE", async () => {
Expand Down Expand Up @@ -276,7 +277,8 @@ describe("chain / validation / attestation", () => {
attestationOrBytes: ApiAttestation,
errorCode: string
): Promise<void> {
await expectRejectedWithLodestarError(validateApiAttestation(chain, attestationOrBytes), errorCode);
const fork = chain.config.getForkName(stateSlot);
await expectRejectedWithLodestarError(validateApiAttestation(fork, chain, attestationOrBytes), errorCode);
}

async function expectGossipError(
Expand All @@ -285,7 +287,11 @@ describe("chain / validation / attestation", () => {
subnet: number,
errorCode: string
): Promise<void> {
await expectRejectedWithLodestarError(validateGossipAttestation(chain, attestationOrBytes, subnet), errorCode);
const fork = chain.config.getForkName(stateSlot);
await expectRejectedWithLodestarError(
validateGossipAttestation(fork, chain, attestationOrBytes, subnet),
errorCode
);
}
});

Expand Down
Loading

0 comments on commit 29314c9

Please sign in to comment.