Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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))) {
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