mirror of
https://github.com/ChainSafe/lodestar.git
synced 2026-01-10 08:08:16 -05:00
feat: offload cell proof computation from beacon node (#7686)
**Motivation**
Uses cell proofs from EL `getPayloadsV5` to offload cell proof
construction
Refer to [EIP-7594
spec](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-7594.md#networking)
for more on requiring blob transaction senders to compute cell proofs.
[Pending spec
changes](cad4194e3f/src/engine/osaka.md (engine_getpayloadv5))
define updates for `getPayloadsV5` in the execution engine API
**Description**
* add engine_getPayloadV5 to engine API
* avoid computing cell proofs in computeDataColumnSidecars
* rename CELLS_PER_BLOB to CELLS_PER_EXT_BLOB to match spec
* update sszTypes for Fulu BlockContents, SignedBlockContents
Closes #7669
**Other notes**
* implement blobsBundle validation
I've added a validation function for validating `BlobsBundleV2` by
computing cells and batch verifying the cell proofs, but don't currently
call this function. We could validate this data on receiving responses
from the EL or when producing the block body, but we might consider data
from the EL trustworthy and skip costly verification.
---------
Co-authored-by: Matthew Keil <github@mail.matthewkeil.com>
Co-authored-by: matthewkeil <me@matthewkeil.com>
This commit is contained in:
@@ -87,7 +87,9 @@ export function getBeaconBlockApi({
|
||||
const fork = config.getForkName(signedBlock.message.slot);
|
||||
let blockData: BlockInputAvailableData;
|
||||
if (isForkPostFulu(fork)) {
|
||||
dataColumnSidecars = computeDataColumnSidecars(config, signedBlock, signedBlockOrContents);
|
||||
const cachedContents = chain.getContents(signedBlock.message as deneb.BeaconBlock);
|
||||
|
||||
dataColumnSidecars = computeDataColumnSidecars(config, signedBlock, cachedContents ?? signedBlockOrContents);
|
||||
blockData = {
|
||||
fork,
|
||||
dataColumnsLen: dataColumnSidecars.length,
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
Wei,
|
||||
bellatrix,
|
||||
deneb,
|
||||
fulu,
|
||||
isBlindedBeaconBlock,
|
||||
phase0,
|
||||
} from "@lodestar/types";
|
||||
@@ -164,7 +165,7 @@ export class BeaconChain implements IBeaconChain {
|
||||
readonly checkpointBalancesCache: CheckpointBalancesCache;
|
||||
readonly shufflingCache: ShufflingCache;
|
||||
/** Map keyed by executionPayload.blockHash of the block for those blobs */
|
||||
readonly producedContentsCache = new Map<BlockHash, deneb.Contents>();
|
||||
readonly producedContentsCache = new Map<BlockHash, deneb.Contents & {cells?: fulu.Cell[][]}>();
|
||||
|
||||
// Cache payload from the local execution so that produceBlindedBlock or produceBlockV3 and
|
||||
// send and get signed/published blinded versions which beacon can assemble into full before
|
||||
@@ -773,7 +774,7 @@ export class BeaconChain implements IBeaconChain {
|
||||
* kzg_aggregated_proof=compute_proof_from_blobs(blobs),
|
||||
* )
|
||||
*/
|
||||
getContents(beaconBlock: deneb.BeaconBlock): deneb.Contents {
|
||||
getContents(beaconBlock: deneb.BeaconBlock): deneb.Contents & {cells?: fulu.Cell[][]} {
|
||||
const blockHash = toRootHex(beaconBlock.body.executionPayload.blockHash);
|
||||
const contents = this.producedContentsCache.get(blockHash);
|
||||
if (!contents) {
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
altair,
|
||||
capella,
|
||||
deneb,
|
||||
fulu,
|
||||
phase0,
|
||||
} from "@lodestar/types";
|
||||
import {Logger} from "@lodestar/utils";
|
||||
@@ -129,7 +130,7 @@ export interface IBeaconChain {
|
||||
|
||||
readonly beaconProposerCache: BeaconProposerCache;
|
||||
readonly checkpointBalancesCache: CheckpointBalancesCache;
|
||||
readonly producedContentsCache: Map<BlockHash, deneb.Contents>;
|
||||
readonly producedContentsCache: Map<BlockHash, deneb.Contents & {cells?: fulu.Cell[][]}>;
|
||||
readonly producedBlockRoot: Map<RootHex, ExecutionPayload | null>;
|
||||
readonly shufflingCache: ShufflingCache;
|
||||
readonly producedBlindedBlockRoot: Set<RootHex>;
|
||||
@@ -194,7 +195,7 @@ export interface IBeaconChain {
|
||||
root: RootHex
|
||||
): Promise<{block: SignedBeaconBlock; executionOptimistic: boolean; finalized: boolean} | null>;
|
||||
|
||||
getContents(beaconBlock: deneb.BeaconBlock): deneb.Contents;
|
||||
getContents(beaconBlock: deneb.BeaconBlock): deneb.Contents & {cells?: fulu.Cell[][]};
|
||||
|
||||
produceCommonBlockBody(blockAttributes: BlockAttributes): Promise<CommonBlockBody>;
|
||||
produceBlock(blockAttributes: BlockAttributes & {commonBlockBody?: CommonBlockBody}): Promise<{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {ChainForkConfig} from "@lodestar/config";
|
||||
import {ForkPostBellatrix, ForkSeq, isForkPostAltair, isForkPostBellatrix} from "@lodestar/params";
|
||||
import {ForkPostBellatrix, ForkPostDeneb, ForkSeq, isForkPostAltair, isForkPostBellatrix} from "@lodestar/params";
|
||||
import {
|
||||
CachedBeaconStateAllForks,
|
||||
CachedBeaconStateBellatrix,
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
capella,
|
||||
deneb,
|
||||
electra,
|
||||
fulu,
|
||||
ssz,
|
||||
sszTypesFor,
|
||||
} from "@lodestar/types";
|
||||
@@ -44,6 +45,7 @@ import {
|
||||
getExpectedGasLimit,
|
||||
} from "../../execution/index.js";
|
||||
import {fromGraffitiBuffer} from "../../util/graffiti.js";
|
||||
import {ckzg} from "../../util/kzg.js";
|
||||
import type {BeaconChain} from "../chain.js";
|
||||
import {CommonBlockBody} from "../interface.js";
|
||||
import {validateBlobsAndKzgCommitments} from "./validateBlobsAndKzgCommitments.js";
|
||||
@@ -97,7 +99,7 @@ export enum BlobsResultType {
|
||||
|
||||
export type BlobsResult =
|
||||
| {type: BlobsResultType.preDeneb}
|
||||
| {type: BlobsResultType.produced; contents: deneb.Contents; blockHash: RootHex}
|
||||
| {type: BlobsResultType.produced; contents: deneb.Contents & {cells?: fulu.Cell[][]}; blockHash: RootHex}
|
||||
| {type: BlobsResultType.blinded};
|
||||
|
||||
export async function produceBlockBody<T extends BlockType>(
|
||||
@@ -347,13 +349,22 @@ export async function produceBlockBody<T extends BlockType>(
|
||||
throw Error(`Missing blobsBundle response from getPayload at fork=${fork}`);
|
||||
}
|
||||
|
||||
let cells: fulu.Cell[][] | undefined;
|
||||
if (ForkSeq[fork] >= ForkSeq.fulu) {
|
||||
cells = blobsBundle.blobs.map((blob) => ckzg.computeCells(blob));
|
||||
}
|
||||
|
||||
if (this.opts.sanityCheckExecutionEngineBlobs) {
|
||||
validateBlobsAndKzgCommitments(executionPayload, blobsBundle);
|
||||
validateBlobsAndKzgCommitments(fork as ForkPostDeneb, executionPayload, blobsBundle, cells);
|
||||
}
|
||||
|
||||
(blockBody as deneb.BeaconBlockBody).blobKzgCommitments = blobsBundle.commitments;
|
||||
const blockHash = toRootHex(executionPayload.blockHash);
|
||||
const contents = {kzgProofs: blobsBundle.proofs, blobs: blobsBundle.blobs};
|
||||
const contents = {
|
||||
kzgProofs: blobsBundle.proofs,
|
||||
blobs: blobsBundle.blobs,
|
||||
cells,
|
||||
};
|
||||
blobsResult = {type: BlobsResultType.produced, contents, blockHash};
|
||||
|
||||
Object.assign(logMeta, {blobs: blobsBundle.commitments.length});
|
||||
|
||||
@@ -1,16 +1,61 @@
|
||||
import {ExecutionPayload} from "@lodestar/types";
|
||||
import {CELLS_PER_EXT_BLOB, ForkPostDeneb, ForkSeq} from "@lodestar/params";
|
||||
import {ExecutionPayload, fulu} from "@lodestar/types";
|
||||
import {BlobsBundle} from "../../execution/index.js";
|
||||
import {ckzg} from "../../util/kzg.js";
|
||||
|
||||
/**
|
||||
* Optionally sanity-check that the KZG commitments match the versioned hashes in the transactions
|
||||
* https://github.com/ethereum/consensus-specs/blob/11a037fd9227e29ee809c9397b09f8cc3383a8c0/specs/eip4844/validator.md#blob-kzg-commitments
|
||||
*/
|
||||
|
||||
export function validateBlobsAndKzgCommitments(_payload: ExecutionPayload, blobsBundle: BlobsBundle): void {
|
||||
// sanity-check that the KZG commitments match the blobs (as produced by the execution engine)
|
||||
export function validateBlobsAndKzgCommitments(
|
||||
fork: ForkPostDeneb,
|
||||
_payload: ExecutionPayload,
|
||||
blobsBundle: BlobsBundle,
|
||||
cells?: fulu.Cell[][]
|
||||
): void {
|
||||
if (blobsBundle.blobs.length !== blobsBundle.commitments.length) {
|
||||
throw Error(
|
||||
`Blobs bundle blobs len ${blobsBundle.blobs.length} != commitments len ${blobsBundle.commitments.length}`
|
||||
);
|
||||
}
|
||||
|
||||
if (ForkSeq[fork] < ForkSeq.fulu) {
|
||||
if (blobsBundle.proofs.length !== blobsBundle.blobs.length) {
|
||||
throw new Error(
|
||||
`Invalid proofs length for BlobsBundleV1 format: expected ${blobsBundle.blobs.length}, got ${blobsBundle.proofs.length}`
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
ckzg.verifyBlobKzgProofBatch(blobsBundle.blobs, blobsBundle.commitments, blobsBundle.proofs);
|
||||
} catch {
|
||||
throw new Error("Error in verifyBlobKzgProofBatch");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!cells) {
|
||||
cells = blobsBundle.blobs.map((blob) => ckzg.computeCells(blob));
|
||||
}
|
||||
|
||||
const expectedProofsLength = blobsBundle.blobs.length * CELLS_PER_EXT_BLOB;
|
||||
if (blobsBundle.proofs.length !== expectedProofsLength) {
|
||||
throw Error(
|
||||
`Invalid proofs length for BlobsBundleV2 format: expected ${expectedProofsLength}, got ${blobsBundle.proofs.length}`
|
||||
);
|
||||
}
|
||||
|
||||
const commitmentBytes = blobsBundle.commitments.flatMap((commitment) => Array(CELLS_PER_EXT_BLOB).fill(commitment));
|
||||
const cellIndices = Array.from({length: blobsBundle.blobs.length}).flatMap(() =>
|
||||
Array.from({length: CELLS_PER_EXT_BLOB}, (_, i) => i)
|
||||
);
|
||||
const proofBytes = blobsBundle.proofs.flat();
|
||||
|
||||
try {
|
||||
ckzg.verifyCellKzgProofBatch(commitmentBytes, cellIndices, cells.flat(), proofBytes);
|
||||
} catch {
|
||||
throw new Error("Error in verifyCellKzgProofBatch");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -424,14 +424,26 @@ export class ExecutionEngineHttp implements IExecutionEngine {
|
||||
executionRequests?: ExecutionRequests;
|
||||
shouldOverrideBuilder?: boolean;
|
||||
}> {
|
||||
const method =
|
||||
ForkSeq[fork] >= ForkSeq.electra
|
||||
? "engine_getPayloadV4"
|
||||
: ForkSeq[fork] >= ForkSeq.deneb
|
||||
? "engine_getPayloadV3"
|
||||
: ForkSeq[fork] >= ForkSeq.capella
|
||||
? "engine_getPayloadV2"
|
||||
: "engine_getPayloadV1";
|
||||
let method: keyof EngineApiRpcReturnTypes;
|
||||
switch (fork) {
|
||||
case ForkName.phase0:
|
||||
case ForkName.altair:
|
||||
case ForkName.bellatrix:
|
||||
method = "engine_getPayloadV1";
|
||||
break;
|
||||
case ForkName.capella:
|
||||
method = "engine_getPayloadV2";
|
||||
break;
|
||||
case ForkName.deneb:
|
||||
method = "engine_getPayloadV3";
|
||||
break;
|
||||
case ForkName.electra:
|
||||
method = "engine_getPayloadV4";
|
||||
break;
|
||||
default:
|
||||
method = "engine_getPayloadV5";
|
||||
break;
|
||||
}
|
||||
const payloadResponse = await this.rpc.fetchWithRetries<
|
||||
EngineApiRpcReturnTypes[typeof method],
|
||||
EngineApiRpcParamTypes[typeof method]
|
||||
|
||||
@@ -96,6 +96,7 @@ export class ExecutionEngineMockBackend implements JsonRpcBackend {
|
||||
engine_getPayloadV2: this.getPayload.bind(this),
|
||||
engine_getPayloadV3: this.getPayload.bind(this),
|
||||
engine_getPayloadV4: this.getPayload.bind(this),
|
||||
engine_getPayloadV5: this.getPayload.bind(this),
|
||||
engine_getPayloadBodiesByHashV1: this.getPayloadBodiesByHash.bind(this),
|
||||
engine_getPayloadBodiesByRangeV1: this.getPayloadBodiesByRange.bind(this),
|
||||
engine_getClientVersionV1: this.getClientVersionV1.bind(this),
|
||||
|
||||
@@ -63,6 +63,7 @@ export type EngineApiRpcParamTypes = {
|
||||
engine_getPayloadV2: [QUANTITY];
|
||||
engine_getPayloadV3: [QUANTITY];
|
||||
engine_getPayloadV4: [QUANTITY];
|
||||
engine_getPayloadV5: [QUANTITY];
|
||||
|
||||
/**
|
||||
* 1. Array of DATA - Array of block_hash field values of the ExecutionPayload structure
|
||||
@@ -118,6 +119,7 @@ export type EngineApiRpcReturnTypes = {
|
||||
engine_getPayloadV2: ExecutionPayloadResponse;
|
||||
engine_getPayloadV3: ExecutionPayloadResponse;
|
||||
engine_getPayloadV4: ExecutionPayloadResponse;
|
||||
engine_getPayloadV5: ExecutionPayloadResponse;
|
||||
|
||||
engine_getPayloadBodiesByHashV1: (ExecutionPayloadBodyRpc | null)[];
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ export function computeBlobSidecars(
|
||||
export function computeDataColumnSidecars(
|
||||
config: ChainForkConfig,
|
||||
signedBlock: SignedBeaconBlock,
|
||||
contents: deneb.Contents & {kzgCommitmentsInclusionProof?: fulu.KzgCommitmentsInclusionProof}
|
||||
contents: fulu.Contents & {kzgCommitmentsInclusionProof?: fulu.KzgCommitmentsInclusionProof; cells?: fulu.Cell[][]}
|
||||
): fulu.DataColumnSidecars {
|
||||
const blobKzgCommitments = (signedBlock as deneb.SignedBeaconBlock).message.body.blobKzgCommitments;
|
||||
if (blobKzgCommitments === undefined) {
|
||||
@@ -83,17 +83,24 @@ export function computeDataColumnSidecars(
|
||||
if (blobKzgCommitments.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const {blobs} = contents;
|
||||
const fork = config.getForkName(signedBlock.message.slot);
|
||||
const signedBlockHeader = signedBlockToSignedHeader(config, signedBlock);
|
||||
const kzgCommitmentsInclusionProof =
|
||||
contents.kzgCommitmentsInclusionProof ?? computeKzgCommitmentsInclusionProof(fork, signedBlock.message.body);
|
||||
const cellsAndProofs = blobs.map((blob) => ckzg.computeCellsAndKzgProofs(blob));
|
||||
const {blobs, kzgProofs} = contents;
|
||||
const cellsAndProofs = Array.from({length: blobs.length}, (_, rowNumber) => {
|
||||
const cells = contents.cells?.[rowNumber] ?? ckzg.computeCells(blobs[rowNumber]);
|
||||
const proofs = kzgProofs.slice(rowNumber * NUMBER_OF_COLUMNS, (rowNumber + 1) * NUMBER_OF_COLUMNS);
|
||||
return {cells, proofs};
|
||||
});
|
||||
|
||||
return Array.from({length: NUMBER_OF_COLUMNS}, (_, columnIndex) => {
|
||||
// columnIndex'th column
|
||||
const column = Array.from({length: blobs.length}, (_, rowNumber) => cellsAndProofs[rowNumber][0][columnIndex]);
|
||||
const kzgProofs = Array.from({length: blobs.length}, (_, rowNumber) => cellsAndProofs[rowNumber][1][columnIndex]);
|
||||
const column = Array.from({length: blobs.length}, (_, rowNumber) => cellsAndProofs[rowNumber].cells[columnIndex]);
|
||||
const kzgProofs = Array.from(
|
||||
{length: blobs.length},
|
||||
(_, rowNumber) => cellsAndProofs[rowNumber].proofs[columnIndex]
|
||||
);
|
||||
return {
|
||||
index: columnIndex,
|
||||
column,
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
import {CELLS_PER_EXT_BLOB, ForkName} from "@lodestar/params";
|
||||
import {ExecutionPayload, deneb, fulu} from "@lodestar/types";
|
||||
import {beforeAll, describe, expect, it} from "vitest";
|
||||
import {validateBlobsAndKzgCommitments} from "../../../../src/chain/produceBlock/validateBlobsAndKzgCommitments.js";
|
||||
import {BlobsBundle} from "../../../../src/execution/index.js";
|
||||
import {ckzg, initCKZG, loadEthereumTrustedSetup} from "../../../../src/util/kzg.js";
|
||||
import {generateRandomBlob} from "../../../utils/kzg.js";
|
||||
|
||||
describe("validateBlobsAndKzgCommitments", () => {
|
||||
beforeAll(async () => {
|
||||
await initCKZG();
|
||||
loadEthereumTrustedSetup();
|
||||
});
|
||||
|
||||
it("should validate a valid V1 blobs bundle", () => {
|
||||
const blobs = [generateRandomBlob(), generateRandomBlob()];
|
||||
const commitments = [];
|
||||
const proofs = [];
|
||||
|
||||
for (const blob of blobs) {
|
||||
const commitment = ckzg.blobToKzgCommitment(blob);
|
||||
const proof = ckzg.computeBlobKzgProof(blob, commitment);
|
||||
|
||||
commitments.push(commitment);
|
||||
proofs.push(proof);
|
||||
}
|
||||
|
||||
const blobsBundle: BlobsBundle = {
|
||||
commitments,
|
||||
blobs,
|
||||
proofs,
|
||||
};
|
||||
|
||||
// Create a mock ExecutionPayload
|
||||
const mockPayload = {
|
||||
blockNumber: 1,
|
||||
blockHash: new Uint8Array(32),
|
||||
parentHash: new Uint8Array(32),
|
||||
} as ExecutionPayload;
|
||||
|
||||
expect(() => validateBlobsAndKzgCommitments(ForkName.deneb, mockPayload, blobsBundle)).not.toThrow();
|
||||
});
|
||||
|
||||
it("should throw if V1 blobs bundle proof verification fails", () => {
|
||||
const blobs = [generateRandomBlob(), generateRandomBlob()];
|
||||
const commitments = blobs.map((blob) => ckzg.blobToKzgCommitment(blob));
|
||||
const proofs = blobs.map(() => new Uint8Array(48).fill(1)); // filled with all ones which should fail verification
|
||||
|
||||
const blobsBundle: BlobsBundle = {
|
||||
commitments,
|
||||
blobs,
|
||||
proofs,
|
||||
};
|
||||
|
||||
// Create a mock ExecutionPayload
|
||||
const mockPayload = {
|
||||
blockNumber: 1,
|
||||
blockHash: new Uint8Array(32),
|
||||
parentHash: new Uint8Array(32),
|
||||
} as ExecutionPayload;
|
||||
|
||||
expect(() => validateBlobsAndKzgCommitments(ForkName.deneb, mockPayload, blobsBundle)).toThrow(
|
||||
"Error in verifyBlobKzgProofBatch"
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw if commitments and blobs lengths don't match", () => {
|
||||
const blobsBundle: BlobsBundle = {
|
||||
commitments: [new Uint8Array(48).fill(1)],
|
||||
blobs: [generateRandomBlob(), generateRandomBlob()],
|
||||
proofs: [],
|
||||
};
|
||||
|
||||
// Create a mock ExecutionPayload
|
||||
const mockPayload = {
|
||||
blockNumber: 1,
|
||||
blockHash: new Uint8Array(32),
|
||||
parentHash: new Uint8Array(32),
|
||||
} as ExecutionPayload;
|
||||
|
||||
expect(() => validateBlobsAndKzgCommitments(ForkName.deneb, mockPayload, blobsBundle)).toThrow(
|
||||
"Blobs bundle blobs len 2 != commitments len 1"
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw if V1 proofs length is incorrect", () => {
|
||||
const blobs = [generateRandomBlob()];
|
||||
const commitments = blobs.map((blob) => ckzg.blobToKzgCommitment(blob));
|
||||
const blobsBundle: BlobsBundle = {
|
||||
commitments,
|
||||
blobs,
|
||||
proofs: [], // No proofs when we need one per blob
|
||||
};
|
||||
|
||||
// Create a mock ExecutionPayload
|
||||
const mockPayload = {
|
||||
blockNumber: 1,
|
||||
blockHash: new Uint8Array(32),
|
||||
parentHash: new Uint8Array(32),
|
||||
} as ExecutionPayload;
|
||||
|
||||
expect(() => validateBlobsAndKzgCommitments(ForkName.deneb, mockPayload, blobsBundle)).toThrow(
|
||||
"Invalid proofs length for BlobsBundleV1 format: expected 1, got 0"
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw if V2 proofs length is incorrect", () => {
|
||||
const blobs = [generateRandomBlob()];
|
||||
const commitments = blobs.map((blob) => ckzg.blobToKzgCommitment(blob));
|
||||
const blobsBundle: BlobsBundle = {
|
||||
commitments,
|
||||
blobs,
|
||||
proofs: [new Uint8Array(48).fill(1)], // Only one proof when we need CELLS_PER_EXT_BLOB
|
||||
};
|
||||
|
||||
// Create a mock ExecutionPayload
|
||||
const mockPayload = {
|
||||
blockNumber: 1,
|
||||
blockHash: new Uint8Array(32),
|
||||
parentHash: new Uint8Array(32),
|
||||
} as ExecutionPayload;
|
||||
|
||||
expect(() => validateBlobsAndKzgCommitments(ForkName.fulu, mockPayload, blobsBundle)).toThrow(
|
||||
`Invalid proofs length for BlobsBundleV2 format: expected ${CELLS_PER_EXT_BLOB}, got 1`
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw if V2 cell proofs verification fails", () => {
|
||||
const blobs = [generateRandomBlob()];
|
||||
const commitments = blobs.map((blob) => ckzg.blobToKzgCommitment(blob));
|
||||
const cells = blobs.flatMap((blob) => ckzg.computeCells(blob));
|
||||
const proofs = cells.map(() => new Uint8Array(48).fill(0)); // filled with all zeros which should fail verification
|
||||
|
||||
const blobsBundle: BlobsBundle = {
|
||||
commitments,
|
||||
blobs,
|
||||
proofs,
|
||||
};
|
||||
|
||||
// Create a mock ExecutionPayload
|
||||
const mockPayload = {
|
||||
blockNumber: 1,
|
||||
blockHash: new Uint8Array(32),
|
||||
parentHash: new Uint8Array(32),
|
||||
} as ExecutionPayload;
|
||||
|
||||
expect(() => validateBlobsAndKzgCommitments(ForkName.fulu, mockPayload, blobsBundle)).toThrow(
|
||||
"Error in verifyCellKzgProofBatch"
|
||||
);
|
||||
});
|
||||
|
||||
it("should validate BlobsBundleV2 when cells are passed", () => {
|
||||
const blobs = [generateRandomBlob()];
|
||||
|
||||
// Compute commitments, cells, and proofs for each blob
|
||||
const commitments: deneb.KZGCommitment[] = [];
|
||||
const cells: fulu.Cell[][] = [];
|
||||
const proofs: deneb.KZGProof[] = [];
|
||||
blobs.map((blob) => {
|
||||
commitments.push(ckzg.blobToKzgCommitment(blob));
|
||||
|
||||
const [blobCells, blobProofs] = ckzg.computeCellsAndKzgProofs(blob);
|
||||
cells.push(blobCells);
|
||||
proofs.push(...blobProofs);
|
||||
});
|
||||
|
||||
const blobsBundle: BlobsBundle = {
|
||||
commitments,
|
||||
blobs,
|
||||
proofs,
|
||||
};
|
||||
|
||||
// Create a mock ExecutionPayload
|
||||
const mockPayload = {
|
||||
blockNumber: 1,
|
||||
blockHash: new Uint8Array(32),
|
||||
parentHash: new Uint8Array(32),
|
||||
} as ExecutionPayload;
|
||||
|
||||
expect(() => validateBlobsAndKzgCommitments(ForkName.fulu, mockPayload, blobsBundle, cells)).not.toThrow();
|
||||
});
|
||||
|
||||
it("should validate V2 blobs bundle when cells are not passed", () => {
|
||||
const blobs = [generateRandomBlob()];
|
||||
|
||||
// Compute commitments and proofs for each blob
|
||||
const commitments: deneb.KZGCommitment[] = [];
|
||||
const proofs: deneb.KZGProof[] = [];
|
||||
blobs.map((blob) => {
|
||||
commitments.push(ckzg.blobToKzgCommitment(blob));
|
||||
|
||||
const [_, blobProofs] = ckzg.computeCellsAndKzgProofs(blob);
|
||||
proofs.push(...blobProofs);
|
||||
});
|
||||
|
||||
const blobsBundle: BlobsBundle = {
|
||||
commitments,
|
||||
blobs,
|
||||
proofs,
|
||||
};
|
||||
|
||||
// Create a mock ExecutionPayload
|
||||
const mockPayload = {
|
||||
blockNumber: 1,
|
||||
blockHash: new Uint8Array(32),
|
||||
parentHash: new Uint8Array(32),
|
||||
} as ExecutionPayload;
|
||||
|
||||
expect(() => validateBlobsAndKzgCommitments(ForkName.fulu, mockPayload, blobsBundle)).not.toThrow();
|
||||
});
|
||||
});
|
||||
@@ -50,6 +50,7 @@ describe("block archive repository", () => {
|
||||
const slot = dataColumn.signedBlockHeader.message.slot;
|
||||
const blob = ssz.deneb.Blob.defaultValue();
|
||||
const commitment = ssz.deneb.KZGCommitment.defaultValue();
|
||||
const kzgProof = ssz.deneb.KZGProof.defaultValue();
|
||||
const singedBlock = ssz.fulu.SignedBeaconBlock.defaultValue();
|
||||
|
||||
singedBlock.message.body.blobKzgCommitments.push(commitment);
|
||||
@@ -57,7 +58,7 @@ describe("block archive repository", () => {
|
||||
singedBlock.message.body.blobKzgCommitments.push(commitment);
|
||||
const allDataColumnSidecars = computeDataColumnSidecars(config, singedBlock, {
|
||||
blobs: [blob, blob, blob],
|
||||
kzgProofs: [commitment, commitment, commitment],
|
||||
kzgProofs: Array.from({length: 3 * NUMBER_OF_COLUMNS}, () => kzgProof),
|
||||
});
|
||||
for (let j = 0; j < allDataColumnSidecars.length; j++) {
|
||||
allDataColumnSidecars[j].index = j;
|
||||
|
||||
109
packages/beacon-node/test/unit/util/blobs.test.ts
Normal file
109
packages/beacon-node/test/unit/util/blobs.test.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import {createChainForkConfig} from "@lodestar/config";
|
||||
import {NUMBER_OF_COLUMNS} from "@lodestar/params";
|
||||
import {deneb, fulu, ssz} from "@lodestar/types";
|
||||
import {beforeAll, describe, expect, it} from "vitest";
|
||||
import {computeDataColumnSidecars} from "../../../src/util/blobs.js";
|
||||
import {ckzg, initCKZG, loadEthereumTrustedSetup} from "../../../src/util/kzg.js";
|
||||
import {generateRandomBlob} from "../../utils/kzg.js";
|
||||
|
||||
describe("computeDataColumnSidecars", () => {
|
||||
beforeAll(async () => {
|
||||
await initCKZG();
|
||||
loadEthereumTrustedSetup();
|
||||
});
|
||||
|
||||
const config = createChainForkConfig({
|
||||
ALTAIR_FORK_EPOCH: 0,
|
||||
BELLATRIX_FORK_EPOCH: 0,
|
||||
CAPELLA_FORK_EPOCH: 0,
|
||||
DENEB_FORK_EPOCH: 0,
|
||||
ELECTRA_FORK_EPOCH: 0,
|
||||
FULU_FORK_EPOCH: 0,
|
||||
});
|
||||
|
||||
it("should compute DataColumnSidecars when cells are not provided", () => {
|
||||
// Generate test data
|
||||
const blobs = [generateRandomBlob(), generateRandomBlob()];
|
||||
|
||||
// Compute commitments, cells, and proofs for each blob
|
||||
const kzgCommitments: deneb.KZGCommitment[] = [];
|
||||
const cells: fulu.Cell[][] = [];
|
||||
const kzgProofs: deneb.KZGProof[] = [];
|
||||
blobs.map((blob) => {
|
||||
kzgCommitments.push(ckzg.blobToKzgCommitment(blob));
|
||||
|
||||
const [blobCells, blobProofs] = ckzg.computeCellsAndKzgProofs(blob);
|
||||
cells.push(blobCells);
|
||||
kzgProofs.push(...blobProofs);
|
||||
});
|
||||
|
||||
// Create a test block with the commitments
|
||||
const signedBeaconBlock = ssz.fulu.SignedBeaconBlock.defaultValue();
|
||||
signedBeaconBlock.message.body.blobKzgCommitments = kzgCommitments;
|
||||
|
||||
// Compute sidecars without providing cells
|
||||
const sidecars = computeDataColumnSidecars(config, signedBeaconBlock, {
|
||||
blobs,
|
||||
kzgProofs,
|
||||
});
|
||||
|
||||
// Verify the results
|
||||
expect(sidecars.length).toBe(NUMBER_OF_COLUMNS);
|
||||
expect(sidecars[0].column.length).toBe(blobs.length);
|
||||
for (let i = 0; i < blobs.length; i++) {
|
||||
for (let j = 0; j < NUMBER_OF_COLUMNS; j++) {
|
||||
expect(sidecars[j].column[i]).toEqual(cells[i][j]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("should use provided cells when available", () => {
|
||||
// Generate test data
|
||||
const blobs = [generateRandomBlob(), generateRandomBlob()];
|
||||
|
||||
// Compute commitments, cells, and proofs for each blob
|
||||
const kzgCommitments: deneb.KZGCommitment[] = [];
|
||||
const cells: fulu.Cell[][] = [];
|
||||
const kzgProofs: deneb.KZGProof[] = [];
|
||||
blobs.map((blob) => {
|
||||
kzgCommitments.push(ckzg.blobToKzgCommitment(blob));
|
||||
|
||||
const [blobCells, blobProofs] = ckzg.computeCellsAndKzgProofs(blob);
|
||||
cells.push(blobCells);
|
||||
kzgProofs.push(...blobProofs);
|
||||
});
|
||||
|
||||
// Create a test block with the commitments
|
||||
const signedBeaconBlock = ssz.fulu.SignedBeaconBlock.defaultValue();
|
||||
signedBeaconBlock.message.body.blobKzgCommitments = kzgCommitments;
|
||||
|
||||
// Compute sidecars with provided cells
|
||||
const sidecars = computeDataColumnSidecars(config, signedBeaconBlock, {
|
||||
blobs,
|
||||
kzgProofs,
|
||||
cells,
|
||||
});
|
||||
|
||||
// Verify the results
|
||||
expect(sidecars.length).toBe(NUMBER_OF_COLUMNS);
|
||||
expect(sidecars[0].column.length).toBe(blobs.length);
|
||||
for (let i = 0; i < blobs.length; i++) {
|
||||
for (let j = 0; j < NUMBER_OF_COLUMNS; j++) {
|
||||
expect(sidecars[j].column[i]).toEqual(cells[i][j]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("should throw error when block is missing blobKzgCommitments", () => {
|
||||
const signedBeaconBlock = ssz.phase0.SignedBeaconBlock.defaultValue() as any;
|
||||
const blobs = [generateRandomBlob()];
|
||||
const kzgProofs = blobs.flatMap((blob) => ckzg.computeCellsAndKzgProofs(blob)[1]);
|
||||
|
||||
expect(() =>
|
||||
computeDataColumnSidecars(config, signedBeaconBlock, {
|
||||
blobs,
|
||||
kzgProofs,
|
||||
})
|
||||
).toThrow("Invalid block with missing blobKzgCommitments for computeBlobSidecars");
|
||||
});
|
||||
});
|
||||
@@ -191,7 +191,7 @@ describe("data column sidecars", () => {
|
||||
const slot = 0;
|
||||
const blobs = [generateRandomBlob(), generateRandomBlob()];
|
||||
const kzgCommitments = blobs.map((blob) => ckzg.blobToKzgCommitment(blob));
|
||||
const kzgProofs = blobs.map((blob, i) => ckzg.computeBlobKzgProof(blob, kzgCommitments[i]));
|
||||
const kzgProofs = blobs.flatMap((blob) => ckzg.computeCellsAndKzgProofs(blob)[1]);
|
||||
|
||||
const signedBeaconBlock = ssz.fulu.SignedBeaconBlock.defaultValue();
|
||||
|
||||
@@ -229,7 +229,7 @@ describe("data column sidecars", () => {
|
||||
const slot = 0;
|
||||
const blobs = [generateRandomBlob(), generateRandomBlob()];
|
||||
const kzgCommitments = blobs.map((blob) => ckzg.blobToKzgCommitment(blob));
|
||||
const kzgProofs = blobs.map((blob, i) => ckzg.computeBlobKzgProof(blob, kzgCommitments[i]));
|
||||
const kzgProofs = blobs.flatMap((blob) => ckzg.computeCellsAndKzgProofs(blob)[1]);
|
||||
|
||||
const signedBeaconBlock = ssz.fulu.SignedBeaconBlock.defaultValue();
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ describe("C-KZG", () => {
|
||||
const mocks = getBlobCellAndProofs();
|
||||
const blobs = mocks.map(({blob}) => blob);
|
||||
const kzgCommitments = blobs.map(ckzg.blobToKzgCommitment);
|
||||
const kzgProofs = blobs.map((blob, index) => ckzg.computeBlobKzgProof(blob, kzgCommitments[index]));
|
||||
const kzgProofs = blobs.flatMap((blob) => ckzg.computeCellsAndKzgProofs(blob)[1]);
|
||||
for (const commitment of kzgCommitments) {
|
||||
signedBeaconBlock.message.body.executionPayload.transactions.push(transactionForKzgCommitment(commitment));
|
||||
signedBeaconBlock.message.body.blobKzgCommitments.push(commitment);
|
||||
|
||||
@@ -281,7 +281,7 @@ export const CONSOLIDATION_REQUEST_TYPE = 0x02;
|
||||
// 128
|
||||
export const NUMBER_OF_COLUMNS = (FIELD_ELEMENTS_PER_BLOB * 2) / FIELD_ELEMENTS_PER_CELL;
|
||||
export const BYTES_PER_CELL = FIELD_ELEMENTS_PER_CELL * BYTES_PER_FIELD_ELEMENT;
|
||||
export const CELLS_PER_BLOB = FIELD_ELEMENTS_PER_EXT_BLOB / FIELD_ELEMENTS_PER_CELL;
|
||||
export const CELLS_PER_EXT_BLOB = FIELD_ELEMENTS_PER_EXT_BLOB / FIELD_ELEMENTS_PER_CELL;
|
||||
|
||||
// ssz.fulu.BeaconBlockBody.getPathInfo(['blobKzgCommitments']).gindex
|
||||
export const KZG_COMMITMENTS_GINDEX = 27;
|
||||
|
||||
@@ -2,6 +2,7 @@ import {ByteVectorType, ContainerType, ListBasicType, ListCompositeType, VectorC
|
||||
import {
|
||||
BYTES_PER_FIELD_ELEMENT,
|
||||
FIELD_ELEMENTS_PER_CELL,
|
||||
FIELD_ELEMENTS_PER_EXT_BLOB,
|
||||
KZG_COMMITMENTS_INCLUSION_PROOF_DEPTH,
|
||||
MAX_BLOB_COMMITMENTS_PER_BLOCK,
|
||||
MAX_REQUEST_DATA_COLUMN_SIDECARS,
|
||||
@@ -31,6 +32,10 @@ export const Cell = new ByteVectorType(BYTES_PER_FIELD_ELEMENT * FIELD_ELEMENTS_
|
||||
export const DataColumn = new ListCompositeType(Cell, MAX_BLOB_COMMITMENTS_PER_BLOCK);
|
||||
export const ExtendedMatrix = new ListCompositeType(Cell, MAX_BLOB_COMMITMENTS_PER_BLOCK * NUMBER_OF_COLUMNS);
|
||||
export const KzgCommitmentsInclusionProof = new VectorCompositeType(Bytes32, KZG_COMMITMENTS_INCLUSION_PROOF_DEPTH);
|
||||
export const KZGProofs = new ListCompositeType(
|
||||
denebSsz.KZGProof,
|
||||
FIELD_ELEMENTS_PER_EXT_BLOB * MAX_BLOB_COMMITMENTS_PER_BLOCK
|
||||
);
|
||||
|
||||
export const DataColumnSidecar = new ContainerType(
|
||||
{
|
||||
@@ -124,6 +129,15 @@ export const BlobSidecar = new ContainerType(
|
||||
{typeName: "BlobSidecar", jsonCase: "eth2"}
|
||||
);
|
||||
|
||||
export const BlobsBundle = new ContainerType(
|
||||
{
|
||||
commitments: denebSsz.BlobKzgCommitments,
|
||||
proofs: KZGProofs,
|
||||
blobs: denebSsz.Blobs,
|
||||
},
|
||||
{typeName: "BlobsBundle", jsonCase: "eth2"}
|
||||
);
|
||||
|
||||
export const BlindedBeaconBlockBody = new ContainerType(
|
||||
{
|
||||
...electraSsz.BlindedBeaconBlockBody.fields,
|
||||
@@ -163,7 +177,8 @@ export const SignedBuilderBid = new ContainerType(
|
||||
|
||||
export const ExecutionPayloadAndBlobsBundle = new ContainerType(
|
||||
{
|
||||
...denebSsz.ExecutionPayloadAndBlobsBundle.fields,
|
||||
executionPayload: ExecutionPayload,
|
||||
blobsBundle: BlobsBundle,
|
||||
},
|
||||
{typeName: "ExecutionPayloadAndBlobsBundle", jsonCase: "eth2"}
|
||||
);
|
||||
@@ -226,14 +241,18 @@ export const SSEPayloadAttributes = new ContainerType(
|
||||
|
||||
export const BlockContents = new ContainerType(
|
||||
{
|
||||
...electraSsz.BlockContents.fields,
|
||||
block: BeaconBlock,
|
||||
kzgProofs: KZGProofs,
|
||||
blobs: denebSsz.Blobs,
|
||||
},
|
||||
{typeName: "BlockContents", jsonCase: "eth2"}
|
||||
);
|
||||
|
||||
export const SignedBlockContents = new ContainerType(
|
||||
{
|
||||
...electraSsz.SignedBlockContents.fields,
|
||||
signedBlock: SignedBeaconBlock,
|
||||
kzgProofs: KZGProofs,
|
||||
blobs: denebSsz.Blobs,
|
||||
},
|
||||
{typeName: "SignedBlockContents", jsonCase: "eth2"}
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user