Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: handle electra attester slashing #7397

Merged
merged 11 commits into from
Jan 30, 2025
23 changes: 16 additions & 7 deletions packages/beacon-node/src/api/impl/beacon/pool/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,21 @@ export function getBeaconPoolApi({
},

async getPoolAttesterSlashings() {
const fork = chain.config.getForkName(chain.clock.currentSlot);

if (isForkPostElectra(fork)) {
ensi321 marked this conversation as resolved.
Show resolved Hide resolved
throw new ApiError(
400,
`Use getPoolAttesterSlashingsV2 to retrieve pool attester slashings for post-electra fork=${fork}`
);
}

return {data: chain.opPool.getAllAttesterSlashings()};
},

async getPoolAttesterSlashingsV2() {
// TODO Electra: Determine fork based on data returned by api
return {data: chain.opPool.getAllAttesterSlashings(), meta: {version: ForkName.phase0}};
const fork = chain.config.getForkName(chain.clock.currentSlot);
ensi321 marked this conversation as resolved.
Show resolved Hide resolved
return {data: chain.opPool.getAllAttesterSlashings(), meta: {version: fork}};
},

async getPoolProposerSlashings() {
Expand Down Expand Up @@ -162,14 +171,14 @@ export function getBeaconPoolApi({
},

async submitPoolAttesterSlashings({attesterSlashing}) {
await validateApiAttesterSlashing(chain, attesterSlashing);
chain.opPool.insertAttesterSlashing(attesterSlashing);
await network.publishAttesterSlashing(attesterSlashing);
await this.submitPoolAttesterSlashingsV2({attesterSlashing});
},

async submitPoolAttesterSlashingsV2({attesterSlashing}) {
// TODO Electra: Refactor submitPoolAttesterSlashings and submitPoolAttesterSlashingsV2
await this.submitPoolAttesterSlashings({attesterSlashing});
await validateApiAttesterSlashing(chain, attesterSlashing);
const fork = chain.config.getForkName(Number(attesterSlashing.attestation1.data.slot));
chain.opPool.insertAttesterSlashing(fork, attesterSlashing);
await network.publishAttesterSlashing(attesterSlashing);
},

async submitPoolProposerSlashings({proposerSlashing}) {
Expand Down
25 changes: 18 additions & 7 deletions packages/beacon-node/src/chain/opPools/opPool.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {Id, Repository} from "@lodestar/db";
import {
BLS_WITHDRAWAL_PREFIX,
ForkName,
ForkSeq,
MAX_ATTESTER_SLASHINGS,
MAX_ATTESTER_SLASHINGS_ELECTRA,
Expand All @@ -15,7 +16,15 @@ import {
getAttesterSlashableIndices,
isValidVoluntaryExit,
} from "@lodestar/state-transition";
import {AttesterSlashing, Epoch, SignedBeaconBlock, ValidatorIndex, capella, phase0, ssz} from "@lodestar/types";
import {
AttesterSlashing,
Epoch,
SignedBeaconBlock,
ValidatorIndex,
capella,
phase0,
sszTypesFor,
} from "@lodestar/types";
import {fromHex, toHex, toRootHex} from "@lodestar/utils";
import {IBeaconDb} from "../../db/index.js";
import {Metrics} from "../../metrics/metrics.js";
Expand All @@ -26,7 +35,7 @@ import {isValidBlsToExecutionChangeForBlockInclusion} from "./utils.js";

type HexRoot = string;
type AttesterSlashingCached = {
attesterSlashing: phase0.AttesterSlashing;
attesterSlashing: AttesterSlashing;
intersectingIndices: number[];
};

Expand Down Expand Up @@ -66,7 +75,7 @@ export class OpPool {
]);

for (const attesterSlashing of attesterSlashings) {
this.insertAttesterSlashing(attesterSlashing.value, attesterSlashing.key);
this.insertAttesterSlashing(ForkName.electra, attesterSlashing.value, attesterSlashing.key);
ensi321 marked this conversation as resolved.
Show resolved Hide resolved
}
for (const proposerSlashing of proposerSlashings) {
this.insertProposerSlashing(proposerSlashing);
Expand Down Expand Up @@ -132,8 +141,11 @@ export class OpPool {
}

/** Must be validated beforehand */
insertAttesterSlashing(attesterSlashing: phase0.AttesterSlashing, rootHash?: Uint8Array): void {
if (!rootHash) rootHash = ssz.phase0.AttesterSlashing.hashTreeRoot(attesterSlashing);
insertAttesterSlashing(fork: ForkName, attesterSlashing: AttesterSlashing, rootHash?: Uint8Array): void {
if (!rootHash) {
rootHash = sszTypesFor(fork).AttesterSlashing.hashTreeRoot(attesterSlashing);
}

// TODO: Do once and cache attached to the AttesterSlashing object
const intersectingIndices = getAttesterSlashableIndices(attesterSlashing);
this.attesterSlashings.set(toRootHex(rootHash), {
Expand Down Expand Up @@ -284,8 +296,7 @@ export class OpPool {
}

/** For beacon pool API */
// TODO Electra: Update to adapt electra.AttesterSlashing
getAllAttesterSlashings(): phase0.AttesterSlashing[] {
getAllAttesterSlashings(): AttesterSlashing[] {
return Array.from(this.attesterSlashings.values()).map((attesterSlashings) => attesterSlashings.attesterSlashing);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,28 @@ import {
getAttesterSlashableIndices,
getAttesterSlashingSignatureSets,
} from "@lodestar/state-transition";
import {phase0} from "@lodestar/types";
import {AttesterSlashing} from "@lodestar/types";
import {AttesterSlashingError, AttesterSlashingErrorCode, GossipAction} from "../errors/index.js";
import {IBeaconChain} from "../index.js";

export async function validateApiAttesterSlashing(
chain: IBeaconChain,
attesterSlashing: phase0.AttesterSlashing // TODO Electra: Handle electra.AttesterSlashing
attesterSlashing: AttesterSlashing
): Promise<void> {
const prioritizeBls = true;
return validateAttesterSlashing(chain, attesterSlashing, prioritizeBls);
}

export async function validateGossipAttesterSlashing(
chain: IBeaconChain,
attesterSlashing: phase0.AttesterSlashing
attesterSlashing: AttesterSlashing
): Promise<void> {
return validateAttesterSlashing(chain, attesterSlashing);
}

export async function validateAttesterSlashing(
chain: IBeaconChain,
attesterSlashing: phase0.AttesterSlashing,
attesterSlashing: AttesterSlashing,
prioritizeBls = false
): Promise<void> {
// [IGNORE] At least one index in the intersection of the attesting indices of each attestation has not yet been seen
Expand Down
2 changes: 1 addition & 1 deletion packages/beacon-node/src/db/buckets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export enum Bucket {
phase0_depositData = 12, // [DEPRECATED] index -> DepositData
phase0_exit = 13, // ValidatorIndex -> VoluntaryExit
phase0_proposerSlashing = 14, // ValidatorIndex -> ProposerSlashing
phase0_attesterSlashing = 15, // Root -> AttesterSlashing
allForks_attesterSlashing = 15, // Root -> AttesterSlashing
ensi321 marked this conversation as resolved.
Show resolved Hide resolved
capella_blsToExecutionChange = 16, // ValidatorIndex -> SignedBLSToExecutionChange
// checkpoint states
allForks_checkpointState = 17, // Root -> BeaconState
Expand Down
14 changes: 10 additions & 4 deletions packages/beacon-node/src/db/repositories/attesterSlashing.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {ChainForkConfig} from "@lodestar/config";
import {Db, Repository} from "@lodestar/db";
import {ValidatorIndex, phase0, ssz} from "@lodestar/types";
import {AttesterSlashing, ValidatorIndex, ssz} from "@lodestar/types";
import {Bucket, getBucketNameByValue} from "../buckets.js";

/**
Expand All @@ -9,10 +9,16 @@ import {Bucket, getBucketNameByValue} from "../buckets.js";
* Added via gossip or api
* Removed when included on chain or old
*/
export class AttesterSlashingRepository extends Repository<Uint8Array, phase0.AttesterSlashing> {
export class AttesterSlashingRepository extends Repository<Uint8Array, AttesterSlashing> {
constructor(config: ChainForkConfig, db: Db) {
const bucket = Bucket.phase0_attesterSlashing;
super(config, db, bucket, ssz.phase0.AttesterSlashing, getBucketNameByValue(bucket));
const bucket = Bucket.allForks_attesterSlashing;
/**
* We are using `ssz.electra.AttesterSlashing` type since it is backward compatible with `ssz.phase0.AttesterSlashing`
* But this also means the length of `attestingIndices` is not checked/enforced here. Need to make sure length
* is correct before writing to db.
*/
const type = ssz.electra.AttesterSlashing;
super(config, db, bucket, type, getBucketNameByValue(bucket));
}

async hasAll(attesterIndices: ValidatorIndex[] = []): Promise<boolean> {
Expand Down
5 changes: 3 additions & 2 deletions packages/beacon-node/src/network/gossip/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {Message, TopicValidatorResult} from "@libp2p/interface";
import {BeaconConfig} from "@lodestar/config";
import {ForkName} from "@lodestar/params";
import {
AttesterSlashing,
LightClientFinalityUpdate,
LightClientOptimisticUpdate,
SignedAggregateAndProof,
Expand Down Expand Up @@ -90,7 +91,7 @@ export type GossipTypeMap = {
[GossipType.beacon_attestation]: SingleAttestation;
[GossipType.voluntary_exit]: phase0.SignedVoluntaryExit;
[GossipType.proposer_slashing]: phase0.ProposerSlashing;
[GossipType.attester_slashing]: phase0.AttesterSlashing;
[GossipType.attester_slashing]: AttesterSlashing;
[GossipType.sync_committee_contribution_and_proof]: altair.SignedContributionAndProof;
[GossipType.sync_committee]: altair.SyncCommitteeMessage;
[GossipType.light_client_finality_update]: LightClientFinalityUpdate;
Expand All @@ -105,7 +106,7 @@ export type GossipFnByType = {
[GossipType.beacon_attestation]: (attestation: SingleAttestation) => Promise<void> | void;
[GossipType.voluntary_exit]: (voluntaryExit: phase0.SignedVoluntaryExit) => Promise<void> | void;
[GossipType.proposer_slashing]: (proposerSlashing: phase0.ProposerSlashing) => Promise<void> | void;
[GossipType.attester_slashing]: (attesterSlashing: phase0.AttesterSlashing) => Promise<void> | void;
[GossipType.attester_slashing]: (attesterSlashing: AttesterSlashing) => Promise<void> | void;
[GossipType.sync_committee_contribution_and_proof]: (
signedContributionAndProof: altair.SignedContributionAndProof
) => Promise<void> | void;
Expand Down
3 changes: 2 additions & 1 deletion packages/beacon-node/src/network/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
} from "@libp2p/interface";
import type {AddressManager, ConnectionManager, Registrar, TransportManager} from "@libp2p/interface-internal";
import {
AttesterSlashing,
LightClientFinalityUpdate,
LightClientOptimisticUpdate,
SignedAggregateAndProof,
Expand Down Expand Up @@ -79,7 +80,7 @@ export interface INetwork extends INetworkCorePublic {
publishVoluntaryExit(voluntaryExit: phase0.SignedVoluntaryExit): Promise<number>;
publishBlsToExecutionChange(blsToExecutionChange: capella.SignedBLSToExecutionChange): Promise<number>;
publishProposerSlashing(proposerSlashing: phase0.ProposerSlashing): Promise<number>;
publishAttesterSlashing(attesterSlashing: phase0.AttesterSlashing): Promise<number>;
publishAttesterSlashing(attesterSlashing: AttesterSlashing): Promise<number>;
publishSyncCommitteeSignature(signature: altair.SyncCommitteeMessage, subnet: SubnetID): Promise<number>;
publishContributionAndProof(contributionAndProof: altair.SignedContributionAndProof): Promise<number>;
publishLightClientFinalityUpdate(update: LightClientFinalityUpdate): Promise<number>;
Expand Down
3 changes: 2 additions & 1 deletion packages/beacon-node/src/network/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {ForkSeq} from "@lodestar/params";
import {ResponseIncoming} from "@lodestar/reqresp";
import {computeStartSlotAtEpoch, computeTimeAtSlot} from "@lodestar/state-transition";
import {
AttesterSlashing,
LightClientBootstrap,
LightClientFinalityUpdate,
LightClientOptimisticUpdate,
Expand Down Expand Up @@ -373,7 +374,7 @@ export class Network implements INetwork {
);
}

async publishAttesterSlashing(attesterSlashing: phase0.AttesterSlashing): Promise<number> {
async publishAttesterSlashing(attesterSlashing: AttesterSlashing): Promise<number> {
const fork = this.config.getForkName(Number(attesterSlashing.attestation1.data.slot as bigint));
return this.publishGossip<GossipType.attester_slashing>(
{type: GossipType.attester_slashing, fork},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -473,13 +473,14 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
topic,
}: GossipHandlerParamGeneric<GossipType.attester_slashing>) => {
const {serializedData} = gossipData;
const {fork} = topic;
const attesterSlashing = sszDeserialize(topic, serializedData);
await validateGossipAttesterSlashing(chain, attesterSlashing);

// Handler

try {
chain.opPool.insertAttesterSlashing(attesterSlashing);
chain.opPool.insertAttesterSlashing(fork, attesterSlashing);
chain.forkChoice.onAttesterSlashing(attesterSlashing);
} catch (e) {
logger.error("Error adding attesterSlashing to pool", {}, e as Error);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {itBench} from "@dapplion/benchmark";
import {
ForkName,
MAX_ATTESTER_SLASHINGS,
MAX_BLS_TO_EXECUTION_CHANGES,
MAX_PROPOSER_SLASHINGS,
Expand Down Expand Up @@ -63,7 +64,7 @@ describe("opPool", () => {

function fillAttesterSlashing(pool: OpPool, state: CachedBeaconStateAltair, count: number): OpPool {
for (const attestation of generateIndexedAttestations(state, count)) {
pool.insertAttesterSlashing({
pool.insertAttesterSlashing(ForkName.phase0, {
attestation1: ssz.phase0.IndexedAttestationBigint.fromJson(ssz.phase0.IndexedAttestation.toJson(attestation)),
attestation2: ssz.phase0.IndexedAttestationBigint.fromJson(ssz.phase0.IndexedAttestation.toJson(attestation)),
});
Expand Down
4 changes: 2 additions & 2 deletions packages/beacon-node/test/spec/presets/operations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
getBlockRootAtSlot,
} from "@lodestar/state-transition";
import * as blockFns from "@lodestar/state-transition/block";
import {altair, bellatrix, capella, electra, phase0, ssz, sszTypesFor} from "@lodestar/types";
import {AttesterSlashing, altair, bellatrix, capella, electra, phase0, ssz, sszTypesFor} from "@lodestar/types";

import {createCachedBeaconStateTest} from "../../utils/cachedBeaconState.js";
import {getConfig} from "../../utils/config.js";
Expand Down Expand Up @@ -41,7 +41,7 @@ const operationFns: Record<string, BlockProcessFn<CachedBeaconStateAllForks>> =
blockFns.processAttestations(fork, state, [testCase.attestation]);
},

attester_slashing: (state, testCase: BaseSpecTest & {attester_slashing: phase0.AttesterSlashing}) => {
attester_slashing: (state, testCase: BaseSpecTest & {attester_slashing: AttesterSlashing}) => {
const fork = state.config.getForkSeq(state.slot);
blockFns.processAttesterSlashing(fork, state, testCase.attester_slashing, shouldVerify(testCase));
},
Expand Down
3 changes: 2 additions & 1 deletion packages/fork-choice/src/forkChoice/forkChoice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
} from "@lodestar/state-transition";
import {computeUnrealizedCheckpoints} from "@lodestar/state-transition/epoch";
import {
AttesterSlashing,
BeaconBlock,
Epoch,
IndexedAttestation,
Expand Down Expand Up @@ -749,7 +750,7 @@ export class ForkChoice implements IForkChoice {
* We already call is_slashable_attestation_data() and is_valid_indexed_attestation
* in state transition so no need to do it again
*/
onAttesterSlashing(attesterSlashing: phase0.AttesterSlashing): void {
onAttesterSlashing(attesterSlashing: AttesterSlashing): void {
// TODO: we already call in in state-transition, find a way not to recompute it again
const intersectingIndices = getAttesterSlashableIndices(attesterSlashing);
for (const validatorIndex of intersectingIndices) {
Expand Down
14 changes: 12 additions & 2 deletions packages/fork-choice/src/forkChoice/interface.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
import {EffectiveBalanceIncrements} from "@lodestar/state-transition";
import {CachedBeaconStateAllForks} from "@lodestar/state-transition";
import {BeaconBlock, Epoch, IndexedAttestation, Root, RootHex, Slot, ValidatorIndex, phase0} from "@lodestar/types";
import {
AttesterSlashing,
BeaconBlock,
Epoch,
IndexedAttestation,
Root,
RootHex,
Slot,
ValidatorIndex,
phase0,
} from "@lodestar/types";
import {
DataAvailabilityStatus,
LVHExecResponse,
Expand Down Expand Up @@ -164,7 +174,7 @@ export interface IForkChoice {
*
* https://github.com/ethereum/consensus-specs/blob/v1.2.0-rc.3/specs/phase0/fork-choice.md#on_attester_slashing
*/
onAttesterSlashing(slashing: phase0.AttesterSlashing): void;
onAttesterSlashing(slashing: AttesterSlashing): void;
getLatestMessage(validatorIndex: ValidatorIndex): LatestMessage | undefined;
/**
* Call `onTick` for all slots between `fcStore.getCurrentSlot()` and the provided `currentSlot`.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {ForkSeq} from "@lodestar/params";
import {phase0} from "@lodestar/types";
import {AttesterSlashing} from "@lodestar/types";

import {CachedBeaconStateAllForks} from "../types.js";
import {getAttesterSlashableIndices, isSlashableAttestationData, isSlashableValidator} from "../util/index.js";
Expand All @@ -15,7 +15,7 @@ import {slashValidator} from "./slashValidator.js";
export function processAttesterSlashing(
fork: ForkSeq,
state: CachedBeaconStateAllForks,
attesterSlashing: phase0.AttesterSlashing,
attesterSlashing: AttesterSlashing,
verifySignatures = true
): void {
assertValidAttesterSlashing(state, attesterSlashing, verifySignatures);
Expand All @@ -39,7 +39,7 @@ export function processAttesterSlashing(

export function assertValidAttesterSlashing(
state: CachedBeaconStateAllForks,
attesterSlashing: phase0.AttesterSlashing,
attesterSlashing: AttesterSlashing,
verifySignatures = true
): void {
const attestation1 = attesterSlashing.attestation1;
Expand Down
4 changes: 2 additions & 2 deletions packages/state-transition/src/util/attestation.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {MIN_ATTESTATION_INCLUSION_DELAY, SLOTS_PER_EPOCH} from "@lodestar/params";
import {Slot, ValidatorIndex, phase0, ssz} from "@lodestar/types";
import {AttesterSlashing, Slot, ValidatorIndex, phase0, ssz} from "@lodestar/types";

/**
* Check if [[data1]] and [[data2]] are slashable according to Casper FFG rules.
Expand All @@ -22,7 +22,7 @@ export function isValidAttestationSlot(attestationSlot: Slot, currentSlot: Slot)
);
}

export function getAttesterSlashableIndices(attesterSlashing: phase0.AttesterSlashing): ValidatorIndex[] {
export function getAttesterSlashableIndices(attesterSlashing: AttesterSlashing): ValidatorIndex[] {
const indices: ValidatorIndex[] = [];
const attSet1 = new Set(attesterSlashing.attestation1.attestingIndices);
const attArr2 = attesterSlashing.attestation2.attestingIndices;
Expand Down
Loading