Skip to content
72 changes: 64 additions & 8 deletions packages/beacon-node/src/chain/validation/dataColumnSidecar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import {
NUMBER_OF_COLUMNS,
} from "@lodestar/params";
import {ssz, deneb, peerdas, Slot, Root} from "@lodestar/types";
import {verifyMerkleBranch} from "@lodestar/utils";
import {toHex, verifyMerkleBranch} from "@lodestar/utils";

import {DataColumnSidecarGossipError, DataColumnSidecarErrorCode} from "../errors/dataColumnSidecarError.js";
import {GossipAction} from "../errors/gossipValidation.js";
import {IBeaconChain} from "../interface.js";
import {ckzg} from "../../util/kzg.js";
import {byteArrayEquals} from "../../util/bytes.js";

export async function validateGossipDataColumnSidecar(
chain: IBeaconChain,
Expand Down Expand Up @@ -45,14 +47,68 @@ export async function validateGossipDataColumnSidecar(
}

export function validateDataColumnsSidecars(
Comment thread
matthewkeil marked this conversation as resolved.
_blockSlot: Slot,
_blockRoot: Root,
_expectedKzgCommitments: deneb.BlobKzgCommitments,
_dataColumnSidecars: peerdas.DataColumnSidecars,
_opts: {skipProofsCheck: boolean} = {skipProofsCheck: false}
blockSlot: Slot,
blockRoot: Root,
blockKzgCommitments: deneb.BlobKzgCommitments,
dataColumnSidecars: peerdas.DataColumnSidecars,
opts: {skipProofsCheck: boolean} = {skipProofsCheck: false}
): void {
// stubbed
return;
const commitmentBytes: Uint8Array[] = [];
const cellIndices: number[] = [];
const cells: Uint8Array[] = [];
const proofBytes: Uint8Array[] = [];

for (let sidecarsIndex = 0; sidecarsIndex < dataColumnSidecars.length; sidecarsIndex++) {
const columnSidecar = dataColumnSidecars[sidecarsIndex];
const {index: columnIndex, column, kzgCommitments, kzgProofs} = columnSidecar;
const columnBlockHeader = columnSidecar.signedBlockHeader.message;
const columnBlockRoot = ssz.phase0.BeaconBlockHeader.hashTreeRoot(columnBlockHeader);
if (
columnBlockHeader.slot !== blockSlot ||
!byteArrayEquals(columnBlockRoot, blockRoot) ||
blockKzgCommitments.length !== kzgCommitments.length ||
blockKzgCommitments
.map((commitment, i) => byteArrayEquals(commitment, kzgCommitments[i]))
.filter((result) => result === false).length
) {
throw new Error(
`Invalid data column sidecar slot=${columnBlockHeader.slot} columnBlockRoot=${toHex(columnBlockRoot)} columnIndex=${columnIndex} for the block blockRoot=${toHex(blockRoot)} slot=${blockSlot} sidecarsIndex=${sidecarsIndex}`
);
}

if (columnIndex >= NUMBER_OF_COLUMNS) {
throw new Error(
`Invalid data sidecar columnIndex=${columnIndex} in slot=${blockSlot} blockRoot=${toHex(blockRoot)} sidecarsIndex=${sidecarsIndex}`
);
}

if (column.length !== kzgCommitments.length || column.length !== kzgProofs.length) {
throw new Error(
`Invalid data sidecar array lengths for columnIndex=${columnIndex} in slot=${blockSlot} blockRoot=${toHex(blockRoot)}`
);
}

commitmentBytes.push(...kzgCommitments);
cellIndices.push(...Array.from({length: column.length}, () => columnIndex));
cells.push(...column);
proofBytes.push(...kzgProofs);
}

if (opts.skipProofsCheck) {
return;
}

let valid: boolean;
try {
valid = ckzg.verifyCellKzgProofBatch(commitmentBytes, cellIndices, cells, proofBytes);
} catch (err) {
(err as Error).message = `Error in verifyCellKzgProofBatch for slot=${blockSlot} blockRoot=${toHex(blockRoot)}`;
throw err;
}

if (!valid) {
throw new Error(`Invalid data column sidecars in slot=${blockSlot} blockRoot=${toHex(blockRoot)}`);
}
}

function validateInclusionProof(dataColumnSidecar: peerdas.DataColumnSidecar): boolean {
Expand Down
62 changes: 60 additions & 2 deletions packages/beacon-node/test/unit/util/dataColumn.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
import {describe, it, expect} from "vitest";
/* eslint-disable @typescript-eslint/naming-convention */
import {describe, it, expect, beforeAll, afterEach} from "vitest";
import {fromHexString} from "@chainsafe/ssz";
import {ssz} from "@lodestar/types";
import {createBeaconConfig, createChainForkConfig, defaultChainConfig} from "@lodestar/config";
import {NUMBER_OF_COLUMNS} from "@lodestar/params";
import {bigIntToBytes} from "@lodestar/utils";

import {getCustodyColumns} from "../../../src/util/dataColumns.js";
import {getMockedBeaconChain} from "../../mocks/mockedBeaconChain.js";
import {ckzg, initCKZG, loadEthereumTrustedSetup} from "../../../src/util/kzg.js";
import {generateRandomBlob, transactionForKzgCommitment} from "../../utils/kzg.js";
import {computeDataColumnSidecars} from "../../../src/util/blobs.js";
import {validateDataColumnsSidecars} from "../../../src/chain/validation/dataColumnSidecar.js";

describe("custody columns", () => {
describe("getCustodyColumns", () => {
const testCases = [
["cdbee32dc3c50e9711d22be5565c7e44ff6108af663b2dc5abd2df573d2fa83f", 4, [2, 80, 89, 118]],
[
Expand All @@ -26,3 +35,52 @@ describe("custody columns", () => {
});
}
});
describe("data column sidecars", () => {
const afterEachCallbacks: (() => Promise<unknown> | void)[] = [];
afterEach(async () => {
while (afterEachCallbacks.length > 0) {
const callback = afterEachCallbacks.pop();
if (callback) await callback();
}
});

beforeAll(async function () {
await initCKZG();
loadEthereumTrustedSetup();
});

it("validateDataColumnsSidecars", () => {
const chainConfig = createChainForkConfig({
...defaultChainConfig,
ALTAIR_FORK_EPOCH: 0,
BELLATRIX_FORK_EPOCH: 0,
DENEB_FORK_EPOCH: 0,
ELECTRA_FORK_EPOCH: 0,
});
const genesisValidatorsRoot = Buffer.alloc(32, 0xaa);
const config = createBeaconConfig(chainConfig, genesisValidatorsRoot);

const chain = getMockedBeaconChain({config});
afterEachCallbacks.push(() => chain.close());

const slot = 0;
const blobs = [generateRandomBlob(), generateRandomBlob()];
const kzgCommitments = blobs.map((blob) => ckzg.blobToKzgCommitment(blob));

const signedBeaconBlock = ssz.deneb.SignedBeaconBlock.defaultValue();

for (const kzgCommitment of kzgCommitments) {
signedBeaconBlock.message.body.executionPayload.transactions.push(transactionForKzgCommitment(kzgCommitment));
signedBeaconBlock.message.body.blobKzgCommitments.push(kzgCommitment);
}
const blockRoot = ssz.deneb.BeaconBlock.hashTreeRoot(signedBeaconBlock.message);
const columnSidecars = computeDataColumnSidecars(config, signedBeaconBlock, {
blobs,
});

expect(columnSidecars.length).toEqual(NUMBER_OF_COLUMNS);
expect(columnSidecars[0].column.length).toEqual(blobs.length);

expect(validateDataColumnsSidecars(slot, blockRoot, kzgCommitments, columnSidecars)).toBeUndefined();
});
});
30 changes: 4 additions & 26 deletions packages/beacon-node/test/unit/util/kzg.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import {describe, it, expect, afterEach, beforeAll} from "vitest";
import {bellatrix, deneb, ssz} from "@lodestar/types";
import {BYTES_PER_FIELD_ELEMENT, BLOB_TX_TYPE} from "@lodestar/params";
import {deneb, ssz} from "@lodestar/types";
import {createBeaconConfig, createChainForkConfig, defaultChainConfig} from "@lodestar/config";
import {signedBlockToSignedHeader} from "@lodestar/state-transition";
import {getMockedBeaconChain} from "../../mocks/mockedBeaconChain.js";
import {computeBlobSidecars, computeDataColumnSidecars, kzgCommitmentToVersionedHash} from "../../../src/util/blobs.js";
import {loadEthereumTrustedSetup, initCKZG, ckzg, FIELD_ELEMENTS_PER_BLOB_MAINNET} from "../../../src/util/kzg.js";
import {loadEthereumTrustedSetup, initCKZG, ckzg} from "../../../src/util/kzg.js";
import {validateBlobSidecars, validateGossipBlobSidecar} from "../../../src/chain/validation/blobSidecar.js";
import {generateRandomBlob, transactionForKzgCommitment} from "../../utils/kzg.js";
import {computeBlobSidecars, computeDataColumnSidecars} from "../../../src/util/blobs.js";
import {getBlobCellAndProofs} from "../../utils/getBlobCellAndProofs.js";

describe("C-KZG", () => {
Expand Down Expand Up @@ -121,25 +121,3 @@ describe("C-KZG", () => {
});
});
});

function transactionForKzgCommitment(kzgCommitment: deneb.KZGCommitment): bellatrix.Transaction {
// Just use versionedHash as the transaction encoding to mock newPayloadV3 verification
// prefixed with BLOB_TX_TYPE
const transaction = new Uint8Array(33);
const versionedHash = kzgCommitmentToVersionedHash(kzgCommitment);
transaction[0] = BLOB_TX_TYPE;
transaction.set(versionedHash, 1);
return transaction;
}

/**
* Generate random blob of sequential integers such that each element is < BLS_MODULUS
*/
function generateRandomBlob(): deneb.Blob {
const blob = new Uint8Array(FIELD_ELEMENTS_PER_BLOB_MAINNET * BYTES_PER_FIELD_ELEMENT);
const dv = new DataView(blob.buffer, blob.byteOffset, blob.byteLength);
for (let i = 0; i < FIELD_ELEMENTS_PER_BLOB_MAINNET; i++) {
dv.setUint32(i * BYTES_PER_FIELD_ELEMENT, i);
}
return blob;
}
26 changes: 26 additions & 0 deletions packages/beacon-node/test/utils/kzg.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import {bellatrix, deneb} from "@lodestar/types";
import {BLOB_TX_TYPE, BYTES_PER_FIELD_ELEMENT} from "@lodestar/params";
import {kzgCommitmentToVersionedHash} from "../../src/util/blobs.js";
import {FIELD_ELEMENTS_PER_BLOB_MAINNET} from "../../src/util/kzg.js";

export function transactionForKzgCommitment(kzgCommitment: deneb.KZGCommitment): bellatrix.Transaction {
// Just use versionedHash as the transaction encoding to mock newPayloadV3 verification
// prefixed with BLOB_TX_TYPE
const transaction = new Uint8Array(33);
const versionedHash = kzgCommitmentToVersionedHash(kzgCommitment);
transaction[0] = BLOB_TX_TYPE;
transaction.set(versionedHash, 1);
return transaction;
}

/**
* Generate random blob of sequential integers such that each element is < BLS_MODULUS
*/
export function generateRandomBlob(): deneb.Blob {
const blob = new Uint8Array(FIELD_ELEMENTS_PER_BLOB_MAINNET * BYTES_PER_FIELD_ELEMENT);
const dv = new DataView(blob.buffer, blob.byteOffset, blob.byteLength);
for (let i = 0; i < FIELD_ELEMENTS_PER_BLOB_MAINNET; i++) {
dv.setUint32(i * BYTES_PER_FIELD_ELEMENT, i);
}
return blob;
}
22 changes: 2 additions & 20 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -12085,16 +12085,7 @@ string-argv@~0.3.1:
resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.2.tgz#2b6d0ef24b656274d957d54e0a4bbf6153dc02b6"
integrity sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==

"string-width-cjs@npm:string-width@^4.2.0":
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"

"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
Expand Down Expand Up @@ -13678,7 +13669,7 @@ workerpool@6.2.1:
resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343"
integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==

"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
Expand All @@ -13696,15 +13687,6 @@ wrap-ansi@^6.2.0:
string-width "^4.1.0"
strip-ansi "^6.0.0"

wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"

wrap-ansi@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
Expand Down