diff --git a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts index dfd2f2889dee..69a593bbd0f2 100644 --- a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts +++ b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts @@ -186,6 +186,8 @@ export function getBeaconBlockApi({ metrics?.registerBeaconBlock(OpSource.api, seenTimestampSec, signedBlock.message); + console.log("Publishing block to P2P (gossip) network", signedBlock); + await Promise.all([ // Send the block, regardless of whether or not it is valid. The API // specification is very clear that this is the desired behaviour. diff --git a/packages/beacon-node/src/network/gossip/gossipsub.ts b/packages/beacon-node/src/network/gossip/gossipsub.ts index 8d7998abcd3b..6eb966ffd2d4 100644 --- a/packages/beacon-node/src/network/gossip/gossipsub.ts +++ b/packages/beacon-node/src/network/gossip/gossipsub.ts @@ -197,6 +197,9 @@ export class Eth2Gossipsub extends GossipSub { async publishBeaconBlock(signedBlock: allForks.SignedBeaconBlock): Promise { const fork = this.config.getForkName(signedBlock.message.slot); + console.log(`Publishing beacon block for fork: ${fork}`); + + // TODO: For EIP-4844, switch this to GossipType.beacon_block_and_blobs_sidecar await this.publishObject({type: GossipType.beacon_block, fork}, signedBlock); } diff --git a/packages/beacon-node/src/network/gossip/handlers/index.ts b/packages/beacon-node/src/network/gossip/handlers/index.ts index 7b55b32c1e7e..b54547443046 100644 --- a/packages/beacon-node/src/network/gossip/handlers/index.ts +++ b/packages/beacon-node/src/network/gossip/handlers/index.ts @@ -3,6 +3,8 @@ import {toHexString} from "@chainsafe/ssz"; import {IBeaconConfig} from "@lodestar/config"; import {phase0, ssz} from "@lodestar/types"; import {ILogger, prettyBytes} from "@lodestar/utils"; +import {SignedBeaconBlock} from "@lodestar/types/lib/phase0/types.js"; +import {ForkName} from "@lodestar/params"; import {IMetrics} from "../../../metrics/index.js"; import {OpSource} from "../../../metrics/validatorMonitor.js"; import {IBeaconChain} from "../../../chain/index.js"; @@ -74,69 +76,85 @@ const MAX_UNKNOWN_BLOCK_ROOT_RETRIES = 1; export function getGossipHandlers(modules: ValidatorFnsModules, options: GossipHandlerOpts): GossipHandlers { const {chain, config, metrics, network, logger} = modules; - return { - [GossipType.beacon_block]: async (signedBlock, topic, peerIdStr, seenTimestampSec) => { - const slot = signedBlock.message.slot; - const forkTypes = config.getForkTypes(slot); - const blockHex = prettyBytes(forkTypes.BeaconBlock.hashTreeRoot(signedBlock.message)); - const delaySec = chain.clock.secFromSlot(slot, seenTimestampSec); - logger.verbose("Received gossip block", { - slot: slot, - root: blockHex, - curentSlot: chain.clock.currentSlot, - peerId: peerIdStr, - delaySec, - }); - - try { - await validateGossipBlock(config, chain, signedBlock, topic.fork); - } catch (e) { - if (e instanceof BlockGossipError) { - if (e instanceof BlockGossipError && e.type.code === BlockErrorCode.PARENT_UNKNOWN) { - logger.debug("Gossip block has error", {slot, root: blockHex, code: e.type.code}); - network.events.emit(NetworkEvent.unknownBlockParent, signedBlock, peerIdStr); - } - } + async function handleBeaconBlock( + signedBlock: SignedBeaconBlock, + fork: ForkName, + peerIdStr: string, + seenTimestampSec: number + ): Promise { + const slot = signedBlock.message.slot; + const forkTypes = config.getForkTypes(slot); + const blockHex = prettyBytes(forkTypes.BeaconBlock.hashTreeRoot(signedBlock.message)); + const delaySec = chain.clock.secFromSlot(slot, seenTimestampSec); + logger.verbose("Received gossip block", { + slot: slot, + root: blockHex, + curentSlot: chain.clock.currentSlot, + peerId: peerIdStr, + delaySec, + }); - if (e instanceof BlockGossipError && e.action === GossipAction.REJECT) { - chain.persistInvalidSszValue(forkTypes.SignedBeaconBlock, signedBlock, `gossip_reject_slot_${slot}`); + try { + await validateGossipBlock(config, chain, signedBlock, fork); + } catch (e) { + if (e instanceof BlockGossipError) { + if (e instanceof BlockGossipError && e.type.code === BlockErrorCode.PARENT_UNKNOWN) { + logger.debug("Gossip block has error", {slot, root: blockHex, code: e.type.code}); + network.events.emit(NetworkEvent.unknownBlockParent, signedBlock, peerIdStr); } + } - throw e; + if (e instanceof BlockGossipError && e.action === GossipAction.REJECT) { + chain.persistInvalidSszValue(forkTypes.SignedBeaconBlock, signedBlock, `gossip_reject_slot_${slot}`); } - // Handler - MUST NOT `await`, to allow validation result to be propagated - - metrics?.registerBeaconBlock(OpSource.gossip, seenTimestampSec, signedBlock.message); - - // `validProposerSignature = true`, in gossip validation the proposer signature is checked - // At gossip time, it's critical to keep a good number of mesh peers. - // To do that, the Gossip Job Wait Time should be consistently <3s to avoid the behavior penalties in gossip - // Gossip Job Wait Time depends on the BLS Job Wait Time - // so `blsVerifyOnMainThread = true`: we want to verify signatures immediately without affecting the bls thread pool. - // otherwise we can't utilize bls thread pool capacity and Gossip Job Wait Time can't be kept low consistently. - // See https://github.com/ChainSafe/lodestar/issues/3792 - chain - .processBlock(signedBlock, {validProposerSignature: true, blsVerifyOnMainThread: true}) - .then(() => { - // Returns the delay between the start of `block.slot` and `current time` - const delaySec = chain.clock.secFromSlot(slot); - metrics?.gossipBlock.elapsedTimeTillProcessed.observe(delaySec); - }) - .catch((e) => { - if (e instanceof BlockError) { - switch (e.type.code) { - case BlockErrorCode.ALREADY_KNOWN: - case BlockErrorCode.PARENT_UNKNOWN: - case BlockErrorCode.PRESTATE_MISSING: - case BlockErrorCode.EXECUTION_ENGINE_ERROR: - break; - default: - network.reportPeer(peerIdFromString(peerIdStr), PeerAction.LowToleranceError, "BadGossipBlock"); - } + throw e; + } + + // Handler - MUST NOT `await`, to allow validation result to be propagated + + metrics?.registerBeaconBlock(OpSource.gossip, seenTimestampSec, signedBlock.message); + + // `validProposerSignature = true`, in gossip validation the proposer signature is checked + // At gossip time, it's critical to keep a good number of mesh peers. + // To do that, the Gossip Job Wait Time should be consistently <3s to avoid the behavior penalties in gossip + // Gossip Job Wait Time depends on the BLS Job Wait Time + // so `blsVerifyOnMainThread = true`: we want to verify signatures immediately without affecting the bls thread pool. + // otherwise we can't utilize bls thread pool capacity and Gossip Job Wait Time can't be kept low consistently. + // See https://github.com/ChainSafe/lodestar/issues/3792 + chain + .processBlock(signedBlock, {validProposerSignature: true, blsVerifyOnMainThread: true}) + .then(() => { + // Returns the delay between the start of `block.slot` and `current time` + const delaySec = chain.clock.secFromSlot(slot); + metrics?.gossipBlock.elapsedTimeTillProcessed.observe(delaySec); + }) + .catch((e) => { + if (e instanceof BlockError) { + switch (e.type.code) { + case BlockErrorCode.ALREADY_KNOWN: + case BlockErrorCode.PARENT_UNKNOWN: + case BlockErrorCode.PRESTATE_MISSING: + case BlockErrorCode.EXECUTION_ENGINE_ERROR: + break; + default: + network.reportPeer(peerIdFromString(peerIdStr), PeerAction.LowToleranceError, "BadGossipBlock"); } - logger.error("Error receiving block", {slot, peer: peerIdStr}, e as Error); - }); + } + logger.error("Error receiving block", {slot, peer: peerIdStr}, e as Error); + }); + } + + return { + [GossipType.beacon_block_and_blobs_sidecar]: async (signedBlock, topic, peerIdStr, seenTimestampSec) => { + const {beaconBlock, blobsSidecar: _} = signedBlock; + // TODO EIP-4844: Validate blobs + + return handleBeaconBlock(beaconBlock, topic.fork, peerIdStr, seenTimestampSec); + }, + + [GossipType.beacon_block]: async (signedBlock, topic, peerIdStr, seenTimestampSec) => { + return handleBeaconBlock(signedBlock, topic.fork, peerIdStr, seenTimestampSec); }, [GossipType.beacon_aggregate_and_proof]: async (signedAggregateAndProof, _topic, _peer, seenTimestampSec) => { diff --git a/packages/beacon-node/src/network/gossip/interface.ts b/packages/beacon-node/src/network/gossip/interface.ts index df9b711c1a4d..ddf4a887f4c8 100644 --- a/packages/beacon-node/src/network/gossip/interface.ts +++ b/packages/beacon-node/src/network/gossip/interface.ts @@ -4,7 +4,7 @@ import {Message} from "@libp2p/interface-pubsub"; import StrictEventEmitter from "strict-event-emitter-types"; import {MessageAcceptance, PeerIdStr} from "@chainsafe/libp2p-gossipsub/types"; import {ForkName} from "@lodestar/params"; -import {allForks, altair, phase0} from "@lodestar/types"; +import {allForks, altair, eip4844, phase0} from "@lodestar/types"; import {IBeaconConfig} from "@lodestar/config"; import {ILogger} from "@lodestar/utils"; import {IBeaconChain} from "../../chain/index.js"; @@ -24,6 +24,8 @@ export enum GossipType { sync_committee = "sync_committee", light_client_finality_update = "light_client_finality_update", light_client_optimistic_update = "light_client_optimistic_update", + // eip4844 + beacon_block_and_blobs_sidecar = "beacon_block_and_blobs_sidecar", } export enum GossipEncoding { @@ -52,6 +54,7 @@ export type GossipTopicTypeMap = { [GossipType.sync_committee]: {type: GossipType.sync_committee; subnet: number}; [GossipType.light_client_finality_update]: {type: GossipType.light_client_finality_update}; [GossipType.light_client_optimistic_update]: {type: GossipType.light_client_optimistic_update}; + [GossipType.beacon_block_and_blobs_sidecar]: {type: GossipType.beacon_block_and_blobs_sidecar}; }; export type GossipTopicMap = { @@ -74,6 +77,7 @@ export type GossipTypeMap = { [GossipType.sync_committee]: altair.SyncCommitteeMessage; [GossipType.light_client_finality_update]: altair.LightClientFinalityUpdate; [GossipType.light_client_optimistic_update]: altair.LightClientOptimisticUpdate; + [GossipType.beacon_block_and_blobs_sidecar]: eip4844.SignedBeaconBlockAndBlobsSidecar; }; export type GossipFnByType = { diff --git a/packages/beacon-node/src/network/gossip/topic.ts b/packages/beacon-node/src/network/gossip/topic.ts index 97a962b40964..cf1e1f7a84c8 100644 --- a/packages/beacon-node/src/network/gossip/topic.ts +++ b/packages/beacon-node/src/network/gossip/topic.ts @@ -58,6 +58,7 @@ function stringifyGossipTopicType(topic: GossipTopic): string { case GossipType.sync_committee_contribution_and_proof: case GossipType.light_client_finality_update: case GossipType.light_client_optimistic_update: + case GossipType.beacon_block_and_blobs_sidecar: return topic.type; case GossipType.beacon_attestation: case GossipType.sync_committee: diff --git a/packages/beacon-node/src/network/gossip/validation/queue.ts b/packages/beacon-node/src/network/gossip/validation/queue.ts index a88b4c355f79..3a389da7823c 100644 --- a/packages/beacon-node/src/network/gossip/validation/queue.ts +++ b/packages/beacon-node/src/network/gossip/validation/queue.ts @@ -19,6 +19,7 @@ const gossipQueueOpts: {[K in GossipType]: Pick = (forkNext) return slotFns.upgradeStateToBellatrix(preState as CachedBeaconStateAltair); case ForkName.capella: return slotFns.upgradeStateToCapella(preState as CachedBeaconStateBellatrix); + case ForkName.eip4844: + return slotFns.upgradeStateTo4844(preState as CachedBeaconStateCapella); } }, options: { diff --git a/packages/state-transition/src/index.ts b/packages/state-transition/src/index.ts index 59b57729becd..20e7c1fff2a3 100644 --- a/packages/state-transition/src/index.ts +++ b/packages/state-transition/src/index.ts @@ -8,6 +8,8 @@ export { CachedBeaconStatePhase0, CachedBeaconStateAltair, CachedBeaconStateBellatrix, + CachedBeaconStateCapella, + CachedBeaconState4844, CachedBeaconStateAllForks, CachedBeaconStateExecutions, // Non-cached states diff --git a/packages/types/src/eip4844/sszTypes.ts b/packages/types/src/eip4844/sszTypes.ts index d0310eeb9859..63d8b7ce4c2a 100644 --- a/packages/types/src/eip4844/sszTypes.ts +++ b/packages/types/src/eip4844/sszTypes.ts @@ -5,13 +5,14 @@ import {ssz as phase0Ssz} from "../phase0/index.js"; import {ssz as altairSsz} from "../altair/index.js"; import {ssz as capellaSsz} from "../capella/index.js"; -const {UintNum64, Slot, ValidatorIndex, Root, BLSSignature, UintBn256, Bytes32, Bytes48} = primitiveSsz; +const {UintNum64, Slot, Root, BLSSignature, UintBn256, Bytes32, Bytes48} = primitiveSsz; // Custom Types // https://github.com/ethereum/consensus-specs/blob/dev/specs/eip4844/beacon-chain.md#custom-types export const VersionedHash = Bytes32; export const KZGCommitment = Bytes48; +export const KZGProof = Bytes48; export const BLSFieldElement = Bytes32; export const Blob = new ListCompositeType(BLSFieldElement, FIELD_ELEMENTS_PER_BLOB); export const BlobKzgCommitments = new ListCompositeType(KZGCommitment, MAX_BLOBS_PER_BLOCK); @@ -37,13 +38,13 @@ export const ExecutionPayloadHeader = new ContainerType( {typeName: "ExecutionPayloadHeader", jsonCase: "eth2"} ); -// Annoyingly, we have to preserve Fields ordering while changing the type of ExecutionPayload +// We have to preserve Fields ordering while changing the type of ExecutionPayload export const BeaconBlockBody = new ContainerType( { ...altairSsz.BeaconBlockBody.fields, executionPayload: ExecutionPayload, // Modified in EIP-4844 blsToExecutionChanges: capellaSsz.BeaconBlockBody.fields.blsToExecutionChanges, - blobKzgCommitments: BlobKzgCommitments, + blobKzgCommitments: BlobKzgCommitments, // New in EIP-4844 }, {typeName: "BeaconBlockBody", jsonCase: "eth2", cachePermanentRootStruct: true} ); @@ -64,7 +65,49 @@ export const SignedBeaconBlock = new ContainerType( {typeName: "SignedBeaconBlock", jsonCase: "eth2"} ); -// we don't reuse capella.BeaconState fields since we need to replace +export const BlobsSidecar = new ContainerType( + { + beaconBlockRoot: Root, + beaconBlockSlot: Slot, + blobs: new ListCompositeType(Blob, MAX_BLOBS_PER_BLOCK), + kzgAggregatedProof: KZGProof, + }, + {typeName: "BlobsSidecar", jsonCase: "eth2"} +); + +export const SignedBeaconBlockAndBlobsSidecar = new ContainerType( + { + beaconBlock: SignedBeaconBlock, + blobsSidecar: BlobsSidecar, + }, + {typeName: "SignedBeaconBlockAndBlobsSidecar", jsonCase: "eth2"} +); + +export const BlindedBeaconBlockBody = new ContainerType( + { + ...BeaconBlockBody.fields, + executionPayloadHeader: ExecutionPayloadHeader, // Modified in EIP-4844 + }, + {typeName: "BlindedBeaconBlockBody", jsonCase: "eth2", cachePermanentRootStruct: true} +); + +export const BlindedBeaconBlock = new ContainerType( + { + ...capellaSsz.BlindedBeaconBlock.fields, + body: BlindedBeaconBlockBody, // Modified in EIP-4844 + }, + {typeName: "BlindedBeaconBlock", jsonCase: "eth2", cachePermanentRootStruct: true} +); + +export const SignedBlindedBeaconBlock = new ContainerType( + { + message: BlindedBeaconBlock, // Modified in EIP-4844 + signature: BLSSignature, + }, + {typeName: "SignedBlindedBeaconBlock", jsonCase: "eth2"} +); + +// We don't spread capella.BeaconState fields since we need to replace // latestExecutionPayloadHeader and we cannot keep order doing that export const BeaconState = new ContainerType( { @@ -101,7 +144,7 @@ export const BeaconState = new ContainerType( currentSyncCommittee: altairSsz.SyncCommittee, nextSyncCommittee: altairSsz.SyncCommittee, // Execution - latestExecutionPayloadHeader: ExecutionPayloadHeader, // [Modified in EIP-4844] + latestExecutionPayloadHeader: ExecutionPayloadHeader, // Modified in EIP-4844 // Withdrawals withdrawalQueue: capellaSsz.WithdrawalQueue, nextWithdrawalIndex: capellaSsz.BeaconState.fields.nextWithdrawalIndex, @@ -109,31 +152,3 @@ export const BeaconState = new ContainerType( }, {typeName: "BeaconState", jsonCase: "eth2"} ); - -export const BlindedBeaconBlockBody = new ContainerType( - { - ...BeaconBlockBody.fields, - executionPayloadHeader: ExecutionPayloadHeader, // Modified in EIP-4844 - }, - {typeName: "BlindedBeaconBlockBody", jsonCase: "eth2", cachePermanentRootStruct: true} -); - -export const BlindedBeaconBlock = new ContainerType( - { - slot: Slot, - proposerIndex: ValidatorIndex, - // Reclare expandedType() with altair block and altair state - parentRoot: Root, - stateRoot: Root, - body: BlindedBeaconBlockBody, // Modified in EIP-4844 - }, - {typeName: "BlindedBeaconBlock", jsonCase: "eth2", cachePermanentRootStruct: true} -); - -export const SignedBlindedBeaconBlock = new ContainerType( - { - message: BlindedBeaconBlock, // Modified in EIP-4844 - signature: BLSSignature, - }, - {typeName: "SignedBlindedBeaconBlock", jsonCase: "eth2"} -); diff --git a/packages/types/src/eip4844/types.ts b/packages/types/src/eip4844/types.ts index 822557d3da12..e2bbd59ce868 100644 --- a/packages/types/src/eip4844/types.ts +++ b/packages/types/src/eip4844/types.ts @@ -10,6 +10,8 @@ export type ExecutionPayloadHeader = ValueOf; export type BeaconBlockBody = ValueOf; export type BeaconBlock = ValueOf; export type SignedBeaconBlock = ValueOf; +export type SignedBeaconBlockAndBlobsSidecar = ValueOf; + export type BeaconState = ValueOf; export type BlindedBeaconBlockBody = ValueOf;