Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions packages/beacon-node/src/chain/errors/blobsSidecarError.ts
Original file line number Diff line number Diff line change
@@ -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<BlobsSidecarErrorType> {}
1 change: 1 addition & 0 deletions packages/beacon-node/src/chain/errors/index.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
150 changes: 150 additions & 0 deletions packages/beacon-node/src/chain/validation/blobsSidecar.ts
Original file line number Diff line number Diff line change
@@ -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;
}
14 changes: 14 additions & 0 deletions packages/beacon-node/src/network/gossip/handlers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -13,6 +14,7 @@ import {
BlockErrorCode,
BlockGossipError,
GossipAction,
GossipActionError,
SyncCommitteeError,
} from "../../../chain/errors/index.js";
import {GossipHandlers, GossipType} from "../interface.js";
Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
10 changes: 7 additions & 3 deletions packages/beacon-node/src/network/gossip/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,21 @@ 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";
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",
Expand All @@ -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};
Expand All @@ -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;
Expand All @@ -78,6 +79,9 @@ export type GossipTypeMap = {

export type GossipFnByType = {
[GossipType.beacon_block]: (signedBlock: allForks.SignedBeaconBlock) => Promise<void> | void;
[GossipType.beacon_block_and_blobs_sidecar]: (
signedBeaconBlockAndBlobsSidecar: eip4844.SignedBeaconBlockAndBlobsSidecar
) => Promise<void> | void;
[GossipType.beacon_aggregate_and_proof]: (aggregateAndProof: phase0.SignedAggregateAndProof) => Promise<void> | void;
[GossipType.beacon_attestation]: (attestation: phase0.Attestation) => Promise<void> | void;
[GossipType.voluntary_exit]: (voluntaryExit: phase0.SignedVoluntaryExit) => Promise<void> | void;
Expand Down
4 changes: 4 additions & 0 deletions packages/beacon-node/src/network/gossip/topic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions packages/beacon-node/src/network/gossip/validation/queue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions packages/beacon-node/test/unit/network/gossip/topic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down
Loading