diff --git a/packages/api/src/beacon/routes/events.ts b/packages/api/src/beacon/routes/events.ts index 41a8e2e9cad9..ddcc57104523 100644 --- a/packages/api/src/beacon/routes/events.ts +++ b/packages/api/src/beacon/routes/events.ts @@ -35,6 +35,10 @@ export enum EventType { attestation = "attestation", /** The node has received a valid voluntary exit (from P2P or API) */ voluntaryExit = "voluntary_exit", + /** The node has received a valid proposer slashing (from P2P or API) */ + proposerSlashing = "proposer_slashing", + /** The node has received a valid attester slashing (from P2P or API) */ + attesterSlashing = "attester_slashing", /** The node has received a valid blsToExecutionChange (from P2P or API) */ blsToExecutionChange = "bls_to_execution_change", /** Finalized checkpoint has been updated */ @@ -58,6 +62,8 @@ export const eventTypes: {[K in EventType]: K} = { [EventType.block]: EventType.block, [EventType.attestation]: EventType.attestation, [EventType.voluntaryExit]: EventType.voluntaryExit, + [EventType.proposerSlashing]: EventType.proposerSlashing, + [EventType.attesterSlashing]: EventType.attesterSlashing, [EventType.blsToExecutionChange]: EventType.blsToExecutionChange, [EventType.finalizedCheckpoint]: EventType.finalizedCheckpoint, [EventType.chainReorg]: EventType.chainReorg, @@ -85,6 +91,8 @@ export type EventData = { }; [EventType.attestation]: phase0.Attestation; [EventType.voluntaryExit]: phase0.SignedVoluntaryExit; + [EventType.proposerSlashing]: phase0.ProposerSlashing; + [EventType.attesterSlashing]: phase0.AttesterSlashing; [EventType.blsToExecutionChange]: capella.SignedBLSToExecutionChange; [EventType.finalizedCheckpoint]: { block: RootHex; @@ -174,6 +182,8 @@ export function getTypeByEvent(): {[K in EventType]: TypeJson} { [EventType.attestation]: ssz.phase0.Attestation, [EventType.voluntaryExit]: ssz.phase0.SignedVoluntaryExit, + [EventType.proposerSlashing]: ssz.phase0.ProposerSlashing, + [EventType.attesterSlashing]: ssz.phase0.AttesterSlashing, [EventType.blsToExecutionChange]: ssz.capella.SignedBLSToExecutionChange, [EventType.finalizedCheckpoint]: new ContainerType( diff --git a/packages/api/test/unit/beacon/testData/events.ts b/packages/api/test/unit/beacon/testData/events.ts index 7bfac9a59a88..08dd27e09d76 100644 --- a/packages/api/test/unit/beacon/testData/events.ts +++ b/packages/api/test/unit/beacon/testData/events.ts @@ -48,6 +48,68 @@ export const eventTestData: EventData = { signature: "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505", }), + [EventType.proposerSlashing]: ssz.phase0.ProposerSlashing.fromJson({ + signed_header_1: { + message: { + slot: "0", + proposer_index: "0", + parent_root: "0x0000000000000000000000000000000000000000000000000000000000000000", + state_root: "0x0000000000000000000000000000000000000000000000000000000000000000", + body_root: "0x0000000000000000000000000000000000000000000000000000000000000000", + }, + signature: + "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + }, + signed_header_2: { + message: { + slot: "0", + proposer_index: "0", + parent_root: "0x0000000000000000000000000000000000000000000000000000000000000000", + state_root: "0x0000000000000000000000000000000000000000000000000000000000000000", + body_root: "0x0000000000000000000000000000000000000000000000000000000000000000", + }, + signature: + "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + }, + }), + [EventType.attesterSlashing]: ssz.phase0.AttesterSlashing.fromJson({ + attestation_1: { + attesting_indices: ["0", "1"], + data: { + slot: "0", + index: "0", + beacon_block_root: "0x0000000000000000000000000000000000000000000000000000000000000000", + source: { + epoch: "0", + root: "0x0000000000000000000000000000000000000000000000000000000000000000", + }, + target: { + epoch: "0", + root: "0x0000000000000000000000000000000000000000000000000000000000000000", + }, + }, + signature: + "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + }, + attestation_2: { + attesting_indices: ["0", "1"], + data: { + slot: "0", + index: "0", + beacon_block_root: "0x0000000000000000000000000000000000000000000000000000000000000000", + source: { + epoch: "0", + root: "0x0000000000000000000000000000000000000000000000000000000000000000", + }, + target: { + epoch: "0", + root: "0x0000000000000000000000000000000000000000000000000000000000000000", + }, + }, + signature: + "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + }, + }), [EventType.blsToExecutionChange]: ssz.capella.SignedBLSToExecutionChange.fromJson({ message: { validator_index: "1", diff --git a/packages/beacon-node/src/chain/blocks/importBlock.ts b/packages/beacon-node/src/chain/blocks/importBlock.ts index 89ed52b66750..c8fc62eaa622 100644 --- a/packages/beacon-node/src/chain/blocks/importBlock.ts +++ b/packages/beacon-node/src/chain/blocks/importBlock.ts @@ -414,6 +414,16 @@ export async function importBlock( this.emitter.emit(routes.events.EventType.attestation, attestation); } } + if (this.emitter.listenerCount(routes.events.EventType.attesterSlashing)) { + for (const attesterSlashing of block.message.body.attesterSlashings) { + this.emitter.emit(routes.events.EventType.attesterSlashing, attesterSlashing); + } + } + if (this.emitter.listenerCount(routes.events.EventType.proposerSlashing)) { + for (const proposerSlashing of block.message.body.proposerSlashings) { + this.emitter.emit(routes.events.EventType.proposerSlashing, proposerSlashing); + } + } } // Register stat metrics about the block after importing it diff --git a/packages/beacon-node/src/network/processor/gossipHandlers.ts b/packages/beacon-node/src/network/processor/gossipHandlers.ts index 9073a204b785..bdd5f1917dc5 100644 --- a/packages/beacon-node/src/network/processor/gossipHandlers.ts +++ b/packages/beacon-node/src/network/processor/gossipHandlers.ts @@ -453,6 +453,8 @@ function getDefaultHandlers(modules: ValidatorFnsModules, options: GossipHandler } catch (e) { logger.error("Error adding attesterSlashing to pool", {}, e as Error); } + + chain.emitter.emit(routes.events.EventType.attesterSlashing, attesterSlashing); }, [GossipType.proposer_slashing]: async ({ @@ -470,6 +472,8 @@ function getDefaultHandlers(modules: ValidatorFnsModules, options: GossipHandler } catch (e) { logger.error("Error adding attesterSlashing to pool", {}, e as Error); } + + chain.emitter.emit(routes.events.EventType.proposerSlashing, proposerSlashing); }, [GossipType.voluntary_exit]: async ({gossipData, topic}: GossipHandlerParamGeneric) => { diff --git a/packages/beacon-node/test/e2e/network/gossipsub.test.ts b/packages/beacon-node/test/e2e/network/gossipsub.test.ts index c7b3dbefbe77..fd1794bf549d 100644 --- a/packages/beacon-node/test/e2e/network/gossipsub.test.ts +++ b/packages/beacon-node/test/e2e/network/gossipsub.test.ts @@ -138,6 +138,70 @@ function runTests({useWorker}: {useWorker: boolean}): void { ); }); + it("Publish and receive an attesterSlashing", async function () { + let onAttesterSlashingChange: (payload: Uint8Array) => void; + const onAttesterSlashingChangePromise = new Promise((resolve) => (onAttesterSlashingChange = resolve)); + + const {netA, netB} = await mockModules({ + [GossipType.attester_slashing]: async ({gossipData}: GossipHandlerParamGeneric) => { + onAttesterSlashingChange(gossipData.serializedData); + }, + }); + + await Promise.all([onPeerConnect(netA), onPeerConnect(netB), connect(netA, netB)]); + expect(netA.getConnectedPeerCount()).toBe(1); + expect(netB.getConnectedPeerCount()).toBe(1); + + await netA.subscribeGossipCoreTopics(); + await netB.subscribeGossipCoreTopics(); + + // Wait to have a peer connected to a topic + while (!netA.closed) { + await sleep(500); + if (await hasSomeMeshPeer(netA)) { + break; + } + } + + const attesterSlashing = ssz.phase0.AttesterSlashing.defaultValue(); + await netA.publishAttesterSlashing(attesterSlashing); + + const received = await onAttesterSlashingChangePromise; + expect(Buffer.from(received)).toEqual(Buffer.from(ssz.phase0.AttesterSlashing.serialize(attesterSlashing))); + }); + + it("Publish and receive a proposerSlashing", async function () { + let onProposerSlashingChange: (payload: Uint8Array) => void; + const onProposerSlashingChangePromise = new Promise((resolve) => (onProposerSlashingChange = resolve)); + + const {netA, netB} = await mockModules({ + [GossipType.proposer_slashing]: async ({gossipData}: GossipHandlerParamGeneric) => { + onProposerSlashingChange(gossipData.serializedData); + }, + }); + + await Promise.all([onPeerConnect(netA), onPeerConnect(netB), connect(netA, netB)]); + expect(netA.getConnectedPeerCount()).toBe(1); + expect(netB.getConnectedPeerCount()).toBe(1); + + await netA.subscribeGossipCoreTopics(); + await netB.subscribeGossipCoreTopics(); + + // Wait to have a peer connected to a topic + while (!netA.closed) { + await sleep(500); + if (await hasSomeMeshPeer(netA)) { + break; + } + } + + const proposerSlashing = ssz.phase0.ProposerSlashing.defaultValue(); + await netA.publishProposerSlashing(proposerSlashing); + + const received = await onProposerSlashingChangePromise; + expect(Buffer.from(received)).toEqual(Buffer.from(ssz.phase0.ProposerSlashing.serialize(proposerSlashing))); + }); + it("Publish and receive a LightClientOptimisticUpdate", async function () { let onLightClientOptimisticUpdate: (ou: Uint8Array) => void; const onLightClientOptimisticUpdatePromise = new Promise(