diff --git a/packages/beacon-node/src/chain/validation/dataColumnSidecar.ts b/packages/beacon-node/src/chain/validation/dataColumnSidecar.ts index c433e75c2e87..4c1452601778 100644 --- a/packages/beacon-node/src/chain/validation/dataColumnSidecar.ts +++ b/packages/beacon-node/src/chain/validation/dataColumnSidecar.ts @@ -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, @@ -45,14 +47,68 @@ export async function validateGossipDataColumnSidecar( } export function validateDataColumnsSidecars( - _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 { diff --git a/packages/beacon-node/test/unit/util/dataColumn.test.ts b/packages/beacon-node/test/unit/util/dataColumn.test.ts index bc088503cc0e..79ec181173a9 100644 --- a/packages/beacon-node/test/unit/util/dataColumn.test.ts +++ b/packages/beacon-node/test/unit/util/dataColumn.test.ts @@ -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]], [ @@ -26,3 +35,52 @@ describe("custody columns", () => { }); } }); +describe("data column sidecars", () => { + const afterEachCallbacks: (() => Promise | 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(); + }); +}); diff --git a/packages/beacon-node/test/unit/util/kzg.test.ts b/packages/beacon-node/test/unit/util/kzg.test.ts index 18b04f8e48a9..b75bb7a9f5f2 100644 --- a/packages/beacon-node/test/unit/util/kzg.test.ts +++ b/packages/beacon-node/test/unit/util/kzg.test.ts @@ -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", () => { @@ -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; -} diff --git a/packages/beacon-node/test/utils/kzg.ts b/packages/beacon-node/test/utils/kzg.ts new file mode 100644 index 000000000000..1d4ab28af800 --- /dev/null +++ b/packages/beacon-node/test/utils/kzg.ts @@ -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; +} diff --git a/yarn.lock b/yarn.lock index d1e7b050e298..61fb15c20f7c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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== @@ -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== @@ -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"