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
26 changes: 19 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,12 @@ 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) {
const type = sszTypesFor(fork).AttesterSlashing;
rootHash = type.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 +297,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
49 changes: 45 additions & 4 deletions packages/beacon-node/src/db/repositories/attesterSlashing.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
import {ChainForkConfig} from "@lodestar/config";
import {Db, Repository} from "@lodestar/db";
import {ValidatorIndex, phase0, ssz} from "@lodestar/types";
import {ForkName, isForkPostElectra} from "@lodestar/params";
import {AttesterSlashing, ValidatorIndex, phase0, ssz, sszTypesFor} from "@lodestar/types";
import {Bucket, getBucketNameByValue} from "../buckets.js";

// We add a 1-byte prefix where 0 means pre-electra and 1 means post-electra
ensi321 marked this conversation as resolved.
Show resolved Hide resolved
enum PrefixByte {
PRE_ELECTRA = 0,
POST_ELECTRA = 1,
}

/**
* AttesterSlashing indexed by root
*
* 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;
const type = ssz.phase0.AttesterSlashing; // Pick some type. Will be overriden
super(config, db, bucket, type, getBucketNameByValue(bucket));
}

async hasAll(attesterIndices: ValidatorIndex[] = []): Promise<boolean> {
Expand All @@ -29,4 +37,37 @@ export class AttesterSlashingRepository extends Repository<Uint8Array, phase0.At
}
return true;
}

encodeValue(value: AttesterSlashing): Uint8Array {
const slot = Number(value.attestation1.data.slot);
const fork = this.config.getForkName(slot);

const type = isForkPostElectra(fork)
? sszTypesFor(ForkName.electra).AttesterSlashing
: sszTypesFor(ForkName.phase0).AttesterSlashing;
const valueBytes = type.serialize(value);

// We need to differentiate between post-electra and pre-electra attester slashing
// such that we can deserialize correctly
const prefixByte = new Uint8Array(1);
prefixByte[0] = isForkPostElectra(fork) ? PrefixByte.POST_ELECTRA : PrefixByte.PRE_ELECTRA;

const prefixedData = new Uint8Array(1 + valueBytes.length);
prefixedData.set(prefixByte, 0);
prefixedData.set(valueBytes, 1);

return prefixedData;
}

decodeValue(data: Buffer): AttesterSlashing {
// First byte is written
const prefix = data.subarray(0, 1);
const isPostElectra = prefix[0] === PrefixByte.POST_ELECTRA;

const type = isPostElectra
? sszTypesFor(ForkName.electra).AttesterSlashing
: sszTypesFor(ForkName.phase0).AttesterSlashing;

return type.deserialize(data.subarray(1));
}
}
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 @@ -470,13 +470,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
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
15 changes: 13 additions & 2 deletions packages/fork-choice/src/forkChoice/forkChoice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,18 @@ import {
isExecutionStateType,
} from "@lodestar/state-transition";
import {computeUnrealizedCheckpoints} from "@lodestar/state-transition/epoch";
import {BeaconBlock, Epoch, Root, RootHex, Slot, ValidatorIndex, bellatrix, phase0, ssz} from "@lodestar/types";
import {
AttesterSlashing,
BeaconBlock,
Epoch,
Root,
RootHex,
Slot,
ValidatorIndex,
bellatrix,
phase0,
ssz,
} from "@lodestar/types";
import {Logger, MapDef, fromHex, toRootHex} from "@lodestar/utils";

import {computeDeltas} from "../protoArray/computeDeltas.js";
Expand Down Expand Up @@ -738,7 +749,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, MAX_COMMITTEES_PER_SLOT, MAX_VALIDATORS_PER_COMMITTEE} from "@lodestar/params";
import {phase0} from "@lodestar/types";
import {IndexedAttestationBigint, phase0} from "@lodestar/types";
import {getIndexedAttestationBigintSignatureSet, getIndexedAttestationSignatureSet} from "../signatureSets/index.js";
import {CachedBeaconStateAllForks} from "../types.js";
import {verifySignatureSet} from "../util/index.js";
Expand All @@ -24,7 +24,7 @@ export function isValidIndexedAttestation(

export function isValidIndexedAttestationBigint(
state: CachedBeaconStateAllForks,
indexedAttestation: phase0.IndexedAttestationBigint,
indexedAttestation: IndexedAttestationBigint,
verifySignature: boolean
): boolean {
if (!isValidIndexedAttestationIndices(state, indexedAttestation.attestingIndices)) {
Expand Down
Loading
Loading