diff --git a/packages/beacon-node/src/chain/errors/blobsSidecarError.ts b/packages/beacon-node/src/chain/errors/blobsSidecarError.ts new file mode 100644 index 000000000000..628a2cdc0640 --- /dev/null +++ b/packages/beacon-node/src/chain/errors/blobsSidecarError.ts @@ -0,0 +1,24 @@ +import {Slot} from "@lodestar/types"; +import {GossipActionError} from "./gossipValidation.js"; + +export enum BlobsSidecarErrorCode { + /** !bls.KeyValidate(block.body.blob_kzg_commitments[i]) */ + INVALID_KZG = "BLOBS_SIDECAR_ERROR_INVALID_KZG", + /** !verify_kzg_commitments_against_transactions(block.body.execution_payload.transactions, block.body.blob_kzg_commitments) */ + INVALID_KZG_TXS = "BLOBS_SIDECAR_ERROR_INVALID_KZG_TXS", + /** sidecar.beacon_block_slot != block.slot */ + INCORRECT_SLOT = "BLOBS_SIDECAR_ERROR_INCORRECT_SLOT", + /** BLSFieldElement in valid range (x < BLS_MODULUS) */ + INVALID_BLOB = "BLOBS_SIDECAR_ERROR_INVALID_BLOB", + /** !bls.KeyValidate(blobs_sidecar.kzg_aggregated_proof) */ + INVALID_KZG_PROOF = "BLOBS_SIDECAR_ERROR_INVALID_KZG_PROOF", +} + +export type BlobsSidecarErrorType = + | {code: BlobsSidecarErrorCode.INVALID_KZG; kzgIdx: number} + | {code: BlobsSidecarErrorCode.INVALID_KZG_TXS} + | {code: BlobsSidecarErrorCode.INCORRECT_SLOT; blockSlot: Slot; blobSlot: Slot} + | {code: BlobsSidecarErrorCode.INVALID_BLOB; blobIdx: number} + | {code: BlobsSidecarErrorCode.INVALID_KZG_PROOF}; + +export class BlobsSidecarError extends GossipActionError {} diff --git a/packages/beacon-node/src/chain/errors/index.ts b/packages/beacon-node/src/chain/errors/index.ts index c08f8ce1aebe..8326b35354f7 100644 --- a/packages/beacon-node/src/chain/errors/index.ts +++ b/packages/beacon-node/src/chain/errors/index.ts @@ -1,5 +1,6 @@ export * from "./attestationError.js"; export * from "./attesterSlashingError.js"; +export * from "./blobsSidecarError.js"; export * from "./blockError.js"; export * from "./gossipValidation.js"; export * from "./proposerSlashingError.js"; diff --git a/packages/beacon-node/src/chain/validation/blobsSidecar.ts b/packages/beacon-node/src/chain/validation/blobsSidecar.ts new file mode 100644 index 000000000000..4d566281cce3 --- /dev/null +++ b/packages/beacon-node/src/chain/validation/blobsSidecar.ts @@ -0,0 +1,150 @@ +import {BYTES_PER_FIELD_ELEMENT, verifyAggregateKzgProof} from "c-kzg"; +import bls from "@chainsafe/bls"; +import {CoordType} from "@chainsafe/bls/types"; +import {eip4844, Root, ssz} from "@lodestar/types"; +import {bytesToBigInt} from "@lodestar/utils"; +import {FIELD_ELEMENTS_PER_BLOB} from "@lodestar/params"; +import {verifyKzgCommitmentsAgainstTransactions} from "@lodestar/state-transition"; +import {BlobsSidecarError, BlobsSidecarErrorCode} from "../errors/blobsSidecarError.js"; +import {GossipAction} from "../errors/gossipValidation.js"; +import {byteArrayEquals} from "../../util/bytes.js"; + +const BLS_MODULUS = BigInt("52435875175126190479447740508185965837690552500527637822603658699938581184513"); + +export function validateGossipBlobsSidecar( + signedBlock: eip4844.SignedBeaconBlock, + blobsSidecar: eip4844.BlobsSidecar +): void { + const block = signedBlock.message; + + // Spec: https://github.com/ethereum/consensus-specs/blob/4cb6fd1c8c8f190d147d15b182c2510d0423ec61/specs/eip4844/p2p-interface.md#beacon_block_and_blobs_sidecar + // [REJECT] The KZG commitments of the blobs are all correctly encoded compressed BLS G1 Points. + // -- i.e. all(bls.KeyValidate(commitment) for commitment in block.body.blob_kzg_commitments) + const {blobKzgCommitments} = block.body; + for (let i = 0; i < blobKzgCommitments.length; i++) { + if (!blsKeyValidate(blobKzgCommitments[i])) { + throw new BlobsSidecarError(GossipAction.REJECT, {code: BlobsSidecarErrorCode.INVALID_KZG, kzgIdx: i}); + } + } + + // [REJECT] The KZG commitments correspond to the versioned hashes in the transactions list. + // -- i.e. verify_kzg_commitments_against_transactions(block.body.execution_payload.transactions, block.body.blob_kzg_commitments) + if ( + !verifyKzgCommitmentsAgainstTransactions(block.body.executionPayload.transactions, block.body.blobKzgCommitments) + ) { + throw new BlobsSidecarError(GossipAction.REJECT, {code: BlobsSidecarErrorCode.INVALID_KZG_TXS}); + } + + // [IGNORE] the sidecar.beacon_block_slot is for the current slot (with a MAXIMUM_GOSSIP_CLOCK_DISPARITY allowance) + // -- i.e. sidecar.beacon_block_slot == block.slot. + if (blobsSidecar.beaconBlockSlot !== block.slot) { + throw new BlobsSidecarError(GossipAction.IGNORE, { + code: BlobsSidecarErrorCode.INCORRECT_SLOT, + blobSlot: blobsSidecar.beaconBlockSlot, + blockSlot: block.slot, + }); + } + + // [REJECT] the sidecar.blobs are all well formatted, i.e. the BLSFieldElement in valid range (x < BLS_MODULUS). + for (let i = 0; i < blobsSidecar.blobs.length; i++) { + if (!blobIsValidRange(blobsSidecar.blobs[i])) { + throw new BlobsSidecarError(GossipAction.REJECT, {code: BlobsSidecarErrorCode.INVALID_BLOB, blobIdx: i}); + } + } + + // [REJECT] The KZG proof is a correctly encoded compressed BLS G1 Point + // -- i.e. blsKeyValidate(blobs_sidecar.kzg_aggregated_proof) + if (!blsKeyValidate(blobsSidecar.kzgAggregatedProof)) { + throw new BlobsSidecarError(GossipAction.REJECT, {code: BlobsSidecarErrorCode.INVALID_KZG_PROOF}); + } + + // [REJECT] The KZG commitments in the block are valid against the provided blobs sidecar. -- i.e. + // validate_blobs_sidecar(block.slot, hash_tree_root(block), block.body.blob_kzg_commitments, sidecar) + validateBlobsSidecar( + block.slot, + ssz.bellatrix.BeaconBlock.hashTreeRoot(block), + block.body.blobKzgCommitments, + blobsSidecar + ); +} + +// https://github.com/ethereum/consensus-specs/blob/dev/specs/eip4844/beacon-chain.md#validate_blobs_sidecar +export function validateBlobsSidecar( + slot: number, + beaconBlockRoot: Root, + expectedKzgCommitments: eip4844.KZGCommitment[], + blobsSidecar: eip4844.BlobsSidecar +): void { + // assert slot == blobs_sidecar.beacon_block_slot + if (slot != blobsSidecar.beaconBlockSlot) { + throw new Error(`slot mismatch. Block slot: ${slot}, Blob slot ${blobsSidecar.beaconBlockSlot}`); + } + + // assert beacon_block_root == blobs_sidecar.beacon_block_root + if (!byteArrayEquals(beaconBlockRoot, blobsSidecar.beaconBlockRoot)) { + throw new Error( + `beacon block root mismatch. Block root: ${beaconBlockRoot}, Blob root ${blobsSidecar.beaconBlockRoot}` + ); + } + + // blobs = blobs_sidecar.blobs + // kzg_aggregated_proof = blobs_sidecar.kzg_aggregated_proof + const {blobs, kzgAggregatedProof} = blobsSidecar; + + // assert len(expected_kzg_commitments) == len(blobs) + if (expectedKzgCommitments.length !== blobs.length) { + throw new Error( + `blobs length to commitments length mismatch. Blob length: ${blobs.length}, Expected commitments length ${expectedKzgCommitments.length}` + ); + } + + // No need to verify the aggregate proof of zero blobs. Also c-kzg throws. + // https://github.com/dankrad/c-kzg/pull/12/files#r1025851956 + if (blobs.length > 0) { + // assert verify_aggregate_kzg_proof(blobs, expected_kzg_commitments, kzg_aggregated_proof) + let isProofValid: boolean; + try { + isProofValid = verifyAggregateKzgProof(blobs, expectedKzgCommitments, kzgAggregatedProof); + } catch (e) { + // TODO EIP-4844: TEMP Nov17: May always throw error -- we need to fix Geth's KZG to match C-KZG and the trusted setup used here + (e as Error).message = `Error on verifyAggregateKzgProof: ${(e as Error).message}`; + throw e; + } + + // TODO EIP-4844: TEMP Nov17: May always throw error -- we need to fix Geth's KZG to match C-KZG and the trusted setup used here + if (!isProofValid) { + throw Error("Invalid AggregateKzgProof"); + } + } +} + +/** + * From https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-bls-signature-04#section-2.5 + * KeyValidate = valid, non-identity point that is in the correct subgroup + */ +function blsKeyValidate(g1Point: Uint8Array): boolean { + try { + bls.PublicKey.fromBytes(g1Point, CoordType.jacobian, true); + return true; + } catch (e) { + return false; + } +} + +/** + * ``` + * Blob = new ByteVectorType(BYTES_PER_FIELD_ELEMENT * FIELD_ELEMENTS_PER_BLOB); + * ``` + * Check that each FIELD_ELEMENT as a uint256 < BLS_MODULUS + */ +function blobIsValidRange(blob: eip4844.Blob): boolean { + for (let i = 0; i < FIELD_ELEMENTS_PER_BLOB; i++) { + const fieldElement = blob.subarray(i * BYTES_PER_FIELD_ELEMENT, (i + 1) * BYTES_PER_FIELD_ELEMENT); + const fieldElementBN = bytesToBigInt(fieldElement, "be"); + if (fieldElementBN >= BLS_MODULUS) { + return false; + } + } + + return true; +} diff --git a/packages/beacon-node/src/network/gossip/handlers/index.ts b/packages/beacon-node/src/network/gossip/handlers/index.ts index ece27a6ccf93..b6138352ad93 100644 --- a/packages/beacon-node/src/network/gossip/handlers/index.ts +++ b/packages/beacon-node/src/network/gossip/handlers/index.ts @@ -3,6 +3,7 @@ import {toHexString} from "@chainsafe/ssz"; import {IBeaconConfig} from "@lodestar/config"; import {phase0, ssz} from "@lodestar/types"; import {ILogger, prettyBytes} from "@lodestar/utils"; +import {ForkSeq} from "@lodestar/params"; import {IMetrics} from "../../../metrics/index.js"; import {OpSource} from "../../../metrics/validatorMonitor.js"; import {IBeaconChain} from "../../../chain/index.js"; @@ -13,6 +14,7 @@ import { BlockErrorCode, BlockGossipError, GossipAction, + GossipActionError, SyncCommitteeError, } from "../../../chain/errors/index.js"; import {GossipHandlers, GossipType} from "../interface.js"; @@ -32,6 +34,7 @@ import {PeerAction} from "../../peers/index.js"; import {validateLightClientFinalityUpdate} from "../../../chain/validation/lightClientFinalityUpdate.js"; import {validateLightClientOptimisticUpdate} from "../../../chain/validation/lightClientOptimisticUpdate.js"; import {getBlockInput} from "../../../chain/blocks/types.js"; +import {validateGossipBlobsSidecar} from "../../../chain/validation/blobsSidecar.js"; /** * Gossip handler options as part of network options @@ -146,6 +149,17 @@ export function getGossipHandlers(modules: ValidatorFnsModules, options: GossipH }); }, + [GossipType.beacon_block_and_blobs_sidecar]: async (blockAndBlocks) => { + const {beaconBlock, blobsSidecar} = blockAndBlocks; + // TODO EIP-4844: Should throw for pre fork blocks? + if (config.getForkSeq(beaconBlock.message.slot) < ForkSeq.eip4844) { + throw new GossipActionError(GossipAction.REJECT, {code: "PRE_EIP4844_BLOCK"}); + } + + // TODO EIP-4844: Handle beacon_block_and_blobs_sidecar + validateGossipBlobsSidecar(beaconBlock, blobsSidecar); + }, + [GossipType.beacon_aggregate_and_proof]: async (signedAggregateAndProof, _topic, _peer, seenTimestampSec) => { let validationResult: {indexedAttestation: phase0.IndexedAttestation; committeeIndices: number[]}; try { diff --git a/packages/beacon-node/src/network/gossip/interface.ts b/packages/beacon-node/src/network/gossip/interface.ts index df9b711c1a4d..4d994731820f 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"; @@ -12,14 +12,13 @@ import {NetworkEvent} from "../events.js"; import {JobItemQueue} from "../../util/queue/index.js"; export enum GossipType { - // phase0 beacon_block = "beacon_block", + beacon_block_and_blobs_sidecar = "beacon_block_and_blobs_sidecar", beacon_aggregate_and_proof = "beacon_aggregate_and_proof", beacon_attestation = "beacon_attestation", voluntary_exit = "voluntary_exit", proposer_slashing = "proposer_slashing", attester_slashing = "attester_slashing", - // altair sync_committee_contribution_and_proof = "sync_committee_contribution_and_proof", sync_committee = "sync_committee", light_client_finality_update = "light_client_finality_update", @@ -41,6 +40,7 @@ export interface IGossipTopic { export type GossipTopicTypeMap = { [GossipType.beacon_block]: {type: GossipType.beacon_block}; + [GossipType.beacon_block_and_blobs_sidecar]: {type: GossipType.beacon_block_and_blobs_sidecar}; [GossipType.beacon_aggregate_and_proof]: {type: GossipType.beacon_aggregate_and_proof}; [GossipType.beacon_attestation]: {type: GossipType.beacon_attestation; subnet: number}; [GossipType.voluntary_exit]: {type: GossipType.voluntary_exit}; @@ -65,6 +65,7 @@ export type GossipTopic = GossipTopicMap[keyof GossipTopicMap]; export type GossipTypeMap = { [GossipType.beacon_block]: allForks.SignedBeaconBlock; + [GossipType.beacon_block_and_blobs_sidecar]: eip4844.SignedBeaconBlockAndBlobsSidecar; [GossipType.beacon_aggregate_and_proof]: phase0.SignedAggregateAndProof; [GossipType.beacon_attestation]: phase0.Attestation; [GossipType.voluntary_exit]: phase0.SignedVoluntaryExit; @@ -78,6 +79,9 @@ export type GossipTypeMap = { export type GossipFnByType = { [GossipType.beacon_block]: (signedBlock: allForks.SignedBeaconBlock) => Promise | void; + [GossipType.beacon_block_and_blobs_sidecar]: ( + signedBeaconBlockAndBlobsSidecar: eip4844.SignedBeaconBlockAndBlobsSidecar + ) => Promise | void; [GossipType.beacon_aggregate_and_proof]: (aggregateAndProof: phase0.SignedAggregateAndProof) => Promise | void; [GossipType.beacon_attestation]: (attestation: phase0.Attestation) => Promise | void; [GossipType.voluntary_exit]: (voluntaryExit: phase0.SignedVoluntaryExit) => Promise | void; diff --git a/packages/beacon-node/src/network/gossip/topic.ts b/packages/beacon-node/src/network/gossip/topic.ts index 97a962b40964..41307bcf96aa 100644 --- a/packages/beacon-node/src/network/gossip/topic.ts +++ b/packages/beacon-node/src/network/gossip/topic.ts @@ -51,6 +51,7 @@ export function stringifyGossipTopic(forkDigestContext: IForkDigestContext, topi function stringifyGossipTopicType(topic: GossipTopic): string { switch (topic.type) { case GossipType.beacon_block: + case GossipType.beacon_block_and_blobs_sidecar: case GossipType.beacon_aggregate_and_proof: case GossipType.voluntary_exit: case GossipType.proposer_slashing: @@ -71,6 +72,8 @@ export function getGossipSSZType(topic: GossipTopic) { case GossipType.beacon_block: // beacon_block is updated in altair to support the updated SignedBeaconBlock type return ssz[topic.fork].SignedBeaconBlock; + case GossipType.beacon_block_and_blobs_sidecar: + return ssz.eip4844.SignedBeaconBlockAndBlobsSidecar; case GossipType.beacon_aggregate_and_proof: return ssz.phase0.SignedAggregateAndProof; case GossipType.beacon_attestation: @@ -118,6 +121,7 @@ export function parseGossipTopic(forkDigestContext: IForkDigestContext, topicStr // Inline-d the parseGossipTopicType() function since spreading the resulting object x4 the time to parse a topicStr switch (gossipTypeStr) { case GossipType.beacon_block: + case GossipType.beacon_block_and_blobs_sidecar: case GossipType.beacon_aggregate_and_proof: case GossipType.voluntary_exit: case GossipType.proposer_slashing: diff --git a/packages/beacon-node/src/network/gossip/validation/queue.ts b/packages/beacon-node/src/network/gossip/validation/queue.ts index 5984d0260d84..26c1cf930ba8 100644 --- a/packages/beacon-node/src/network/gossip/validation/queue.ts +++ b/packages/beacon-node/src/network/gossip/validation/queue.ts @@ -11,6 +11,8 @@ const gossipQueueOpts: { } = { // validation gossip block asap [GossipType.beacon_block]: {maxLength: 1024, type: QueueType.FIFO, noYieldIfOneItem: true}, + // TODO EIP-4844: What's a good queue max given that now blocks are much bigger? + [GossipType.beacon_block_and_blobs_sidecar]: {maxLength: 32, type: QueueType.FIFO, noYieldIfOneItem: true}, // lighthoue has aggregate_queue 4096 and unknown_block_aggregate_queue 1024, we use single queue [GossipType.beacon_aggregate_and_proof]: {maxLength: 5120, type: QueueType.LIFO, maxConcurrency: 16}, // lighthouse has attestation_queue 16384 and unknown_block_attestation_queue 8192, we use single queue diff --git a/packages/beacon-node/test/unit/network/gossip/topic.test.ts b/packages/beacon-node/test/unit/network/gossip/topic.test.ts index f5e7f627baf1..1c4ec9282f6c 100644 --- a/packages/beacon-node/test/unit/network/gossip/topic.test.ts +++ b/packages/beacon-node/test/unit/network/gossip/topic.test.ts @@ -15,6 +15,12 @@ describe("network / gossip / topic", function () { topicStr: "/eth2/18ae4ccb/beacon_block/ssz_snappy", }, ], + [GossipType.beacon_block_and_blobs_sidecar]: [ + { + topic: {type: GossipType.beacon_block_and_blobs_sidecar, fork: ForkName.eip4844, encoding}, + topicStr: "/eth2/46acb19a/beacon_block_and_blobs_sidecar/ssz_snappy", + }, + ], [GossipType.beacon_aggregate_and_proof]: [ { topic: {type: GossipType.beacon_aggregate_and_proof, fork: ForkName.phase0, encoding}, diff --git a/packages/beacon-node/test/unit/util/kzg.test.ts b/packages/beacon-node/test/unit/util/kzg.test.ts index 7020258eeb15..1a17be837f6e 100644 --- a/packages/beacon-node/test/unit/util/kzg.test.ts +++ b/packages/beacon-node/test/unit/util/kzg.test.ts @@ -1,4 +1,3 @@ -import crypto from "node:crypto"; import {expect} from "chai"; import { freeTrustedSetup, @@ -8,10 +7,15 @@ import { BYTES_PER_FIELD_ELEMENT, FIELD_ELEMENTS_PER_BLOB, } from "c-kzg"; -import {eip4844} from "@lodestar/types"; +import {bellatrix, eip4844, ssz} from "@lodestar/types"; +import {BLOB_TX_TYPE} from "@lodestar/params"; +import { + kzgCommitmentToVersionedHash, + OPAQUE_TX_BLOB_VERSIONED_HASHES_OFFSET, + OPAQUE_TX_MESSAGE_OFFSET, +} from "@lodestar/state-transition"; import {loadEthereumTrustedSetup} from "../../../src/util/kzg.js"; - -const BLOB_BYTE_COUNT = FIELD_ELEMENTS_PER_BLOB * BYTES_PER_FIELD_ELEMENT; +import {validateBlobsSidecar, validateGossipBlobsSidecar} from "../../../src/chain/validation/blobsSidecar.js"; describe("C-KZG", () => { before(async function () { @@ -32,11 +36,65 @@ describe("C-KZG", () => { const proof = computeAggregateKzgProof(blobs); expect(verifyAggregateKzgProof(blobs, commitments, proof)).to.equal(true); }); + + it("BlobsSidecar", () => { + const slot = 0; + const blobs = [generateRandomBlob(), generateRandomBlob()]; + const kzgCommitments = blobs.map(blobToKzgCommitment); + + const signedBeaconBlock = ssz.eip4844.SignedBeaconBlock.defaultValue(); + for (const kzgCommitment of kzgCommitments) { + signedBeaconBlock.message.body.executionPayload.transactions.push(transactionForKzgCommitment(kzgCommitment)); + signedBeaconBlock.message.body.blobKzgCommitments.push(kzgCommitment); + } + const beaconBlockRoot = ssz.eip4844.BeaconBlock.hashTreeRoot(signedBeaconBlock.message); + + const blobsSidecar: eip4844.BlobsSidecar = { + beaconBlockRoot, + beaconBlockSlot: 0, + blobs, + kzgAggregatedProof: computeAggregateKzgProof(blobs), + }; + + // Full validation + validateBlobsSidecar(slot, beaconBlockRoot, kzgCommitments, blobsSidecar); + + // Gossip validation + validateGossipBlobsSidecar(signedBeaconBlock, blobsSidecar); + }); }); +function transactionForKzgCommitment(kzgCommitment: eip4844.KZGCommitment): bellatrix.Transaction { + // Some random value that after the offset's position + const blobVersionedHashesOffset = OPAQUE_TX_BLOB_VERSIONED_HASHES_OFFSET + 64; + + // +32 for the size of versionedHash + const ab = new ArrayBuffer(blobVersionedHashesOffset + 32); + const dv = new DataView(ab); + const ua = new Uint8Array(ab); + + // Set tx type + dv.setUint8(0, BLOB_TX_TYPE); + + // Set offset to hashes array + // const blobVersionedHashesOffset = + // OPAQUE_TX_MESSAGE_OFFSET + opaqueTxDv.getUint32(OPAQUE_TX_BLOB_VERSIONED_HASHES_OFFSET, true); + dv.setUint32(OPAQUE_TX_BLOB_VERSIONED_HASHES_OFFSET, blobVersionedHashesOffset - OPAQUE_TX_MESSAGE_OFFSET, true); + + const versionedHash = kzgCommitmentToVersionedHash(kzgCommitment); + ua.set(versionedHash, blobVersionedHashesOffset); + + return ua; +} + /** * Generate random blob of sequential integers such that each element is < BLS_MODULUS */ function generateRandomBlob(): eip4844.Blob { - return new Uint8Array(crypto.randomBytes(BLOB_BYTE_COUNT)); + const blob = new Uint8Array(FIELD_ELEMENTS_PER_BLOB * BYTES_PER_FIELD_ELEMENT); + const dv = new DataView(blob.buffer, blob.byteOffset, blob.byteLength); + for (let i = 0; i < FIELD_ELEMENTS_PER_BLOB; i++) { + dv.setUint32(i * BYTES_PER_FIELD_ELEMENT, i); + } + return blob; }