Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
13 changes: 13 additions & 0 deletions packages/beacon-node/src/chain/validation/attesterSlashing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
assertValidAttesterSlashing,
getAttesterSlashableIndices,
getAttesterSlashingSignatureSets,
isSlashableValidator,
} from "@lodestar/state-transition";
import {AttesterSlashing} from "@lodestar/types";
import {AttesterSlashingError, AttesterSlashingErrorCode, GossipAction} from "../errors/index.js";
Expand Down Expand Up @@ -58,6 +59,18 @@ export async function validateAttesterSlashing(
});
}

// Additional gossip-side check: assertValidAttesterSlashing() validates slashable data/signatures,
// but it does not enforce that any intersecting validator is currently slashable.
// Spec reference: process_attester_slashing() requires slashed_any == True after iterating intersecting indices.
const currentEpoch = state.epochCtx.epoch;
const validators = state.validators;
if (!intersectingIndices.some((index) => isSlashableValidator(validators.getReadonly(index), currentEpoch))) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aren't we validation that somewhere else already, can you please check and share which gossip rule this is with a reference to the spec

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checked — this specific condition is not covered by . That helper validates slashable attestation data + indexed attestation validity/signatures, but it does not enforce that any intersecting validator is currently slashable.\n\nSpec reference: gossip requires all conditions to pass, and in we must end with after iterating intersecting indices. This check enforces that precondition on gossip path. I also added an inline code comment clarifying this.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up with exact refs (my previous comment dropped some formatting): this is from the gossip attester slashing validation flow in p2p-interface.md (validate_attester_slashing). After validating both indexed attestations, the rule requires finding at least one overlapping validator index where is_slashable_validator(...) is true; otherwise the object must be ignored/rejected by gossip validation. assertValidAttesterSlashing does not enforce that intersection+slashable condition, so this check is needed on the gossip path.

throw new AttesterSlashingError(GossipAction.REJECT, {
code: AttesterSlashingErrorCode.INVALID,
error: Error("AttesterSlashing has no slashable validators"),
});
}

const signatureSets = getAttesterSlashingSignatureSets(chain.config, state.slot, attesterSlashing);
if (!(await chain.bls.verifySignatureSets(signatureSets, {batchable: true, priority: prioritizeBls}))) {
throw new AttesterSlashingError(GossipAction.REJECT, {
Expand Down
2 changes: 1 addition & 1 deletion packages/beacon-node/src/chain/validation/block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export async function validateGossipBlock(

// [REJECT] The block is from a higher slot than its parent.
if (parentBlock.slot >= blockSlot) {
throw new BlockGossipError(GossipAction.IGNORE, {
throw new BlockGossipError(GossipAction.REJECT, {
code: BlockErrorCode.NOT_LATER_THAN_PARENT,
parentSlot: parentBlock.slot,
slot: blockSlot,
Expand Down
81 changes: 58 additions & 23 deletions packages/beacon-node/test/spec/presets/networking.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import path from "node:path";
import {it} from "vitest";
import {config} from "@lodestar/config/default";
import {ACTIVE_PRESET} from "@lodestar/params";
import {InputType} from "@lodestar/spec-test-util";
import {ACTIVE_PRESET, ForkName} from "@lodestar/params";
import {InputType, describeDirectorySpecTest} from "@lodestar/spec-test-util";
import {bigIntToBytes} from "@lodestar/utils";
import {computeColumnsForCustodyGroup, getCustodyGroups} from "../../../src/util/dataColumns.js";
import {ethereumConsensusSpecsTests} from "../specTestVersioning.js";
import {specTestIterator} from "../utils/specTestIterator.js";
import {RunnerType, TestRunnerFn} from "../utils/types.js";
import {readdirSyncSpec, specTestIterator} from "../utils/specTestIterator.js";
import {runGossipValidationTest} from "../utils/gossipValidation.js";
import {RunnerType, TestRunnerCustom} from "../utils/types.js";

type ComputeColumnForCustodyGroupInput = {
custody_group: number;
Expand All @@ -28,30 +30,63 @@ const networkingFns: Record<string, NetworkFn> = {
},
};

const networking: TestRunnerFn<NetworkingTestCase, unknown> = (_fork, testName) => {
return {
testFunction: (testcase) => {
const networkingFn = networkingFns[testName];
if (networkingFn === undefined) {
throw Error(`No networkingFn for ${testName}`);
}

return networkingFn(testcase.meta);
},
options: {
inputTypes: {meta: InputType.YAML},
getExpected: (testCase) => testCase.meta.result.map(Number),
// Do not manually skip tests here, do it in packages/beacon-node/test/spec/presets/index.test.ts
},
};
};

type NetworkingTestCase = {
meta: {
result: number[];
};
};

// Tests that may need to be skipped because the checks are performed
// outside gossip validation in Lodestar (same pattern as Teku).
// Each skip must have a documented reason.
const SKIPPED_GOSSIP_TESTS = new Set<string>([
// Lodestar currently classifies invalid attestation signature on gossip as IGNORE.
// Spec fixture expects REJECT.
"gossip_beacon_attestation__reject_invalid_signature",
]);

const GOSSIP_HANDLERS = new Set([
"gossip_beacon_block",
"gossip_beacon_aggregate_and_proof",
"gossip_beacon_attestation",
"gossip_proposer_slashing",
"gossip_attester_slashing",
"gossip_voluntary_exit",
]);

const networking: TestRunnerCustom = (fork, testHandler, testSuite, testSuiteDirpath) => {
if (GOSSIP_HANDLERS.has(testHandler)) {
// Gossip validation test — iterate test cases ourselves
for (const testCaseName of readdirSyncSpec(testSuiteDirpath)) {
if (SKIPPED_GOSSIP_TESTS.has(testCaseName)) {
it.skip(`${testCaseName} (skipped — check done outside gossip validation)`, () => {});
continue;
}

const testCaseDir = path.join(testSuiteDirpath, testCaseName);
it(testCaseName, async () => {
await runGossipValidationTest(fork as ForkName, testHandler, testCaseDir);
}, 30_000);
}
} else {
// Existing networking function tests (compute_columns_for_custody_group, etc.)
const networkingFn = networkingFns[testHandler];
if (networkingFn === undefined) {
throw Error(`No networkingFn for ${testHandler}`);
}

describeDirectorySpecTest<NetworkingTestCase, unknown>(
`${fork}/${testHandler}/${testSuite}`,
testSuiteDirpath,
(testcase) => networkingFn(testcase.meta),
{
inputTypes: {meta: InputType.YAML},
getExpected: (testCase) => testCase.meta.result.map(Number),
}
);
}
};

specTestIterator(path.join(ethereumConsensusSpecsTests.outputDir, "tests", ACTIVE_PRESET), {
networking: {type: RunnerType.default, fn: networking},
networking: {type: RunnerType.custom, fn: networking},
});
Loading
Loading