From 220c7dc98e39c9234dbb41715e273421c251ce9f Mon Sep 17 00:00:00 2001 From: dapplion <35266934+dapplion@users.noreply.github.com> Date: Mon, 5 Dec 2022 14:41:11 +0700 Subject: [PATCH] Add verifyKzgCommitmentsAgainstTransactions --- packages/state-transition/src/util/blobs.ts | 94 +++++++++++++++++++ packages/state-transition/src/util/index.ts | 1 + .../test/unit/util/blobs.test.ts | 53 +++++++++++ 3 files changed, 148 insertions(+) create mode 100644 packages/state-transition/src/util/blobs.ts create mode 100644 packages/state-transition/test/unit/util/blobs.test.ts diff --git a/packages/state-transition/src/util/blobs.ts b/packages/state-transition/src/util/blobs.ts new file mode 100644 index 000000000000..0ec5234a04e7 --- /dev/null +++ b/packages/state-transition/src/util/blobs.ts @@ -0,0 +1,94 @@ +import SHA256 from "@chainsafe/as-sha256"; +import {byteArrayEquals} from "@chainsafe/ssz"; +import {BLOB_TX_TYPE, VERSIONED_HASH_VERSION_KZG} from "@lodestar/params"; +import {bellatrix, eip4844} from "@lodestar/types"; +import {toHex} from "@lodestar/utils"; + +// TODO EIP-4844: Move to params +const BYTES_PER_HASH = 32; + +/** + * Blob transaction: + * - 1 byte prefix + * - class SignedBlobTransaction(Container): message starts at offset 69 + * - class BlobTransaction(Container): blob_versioned_hashes offset value in offset 188, last property in container + * So to read blob_versioned_hashes: + * - Read offset value at [70+188, 70+188+4] + * - Read chunks between offset value and EOF + * Reference: https://gist.github.com/protolambda/23bd106b66f6d4bb854ce46044aa3ca3 + */ +export const OPAQUE_TX_MESSAGE_OFFSET = 70; +export const OPAQUE_TX_BLOB_VERSIONED_HASHES_OFFSET = OPAQUE_TX_MESSAGE_OFFSET + 188; + +type VersionHash = Uint8Array; + +/** + * https://github.com/ethereum/consensus-specs/blob/11a037fd9227e29ee809c9397b09f8cc3383a8c0/specs/eip4844/beacon-chain.md#verify_kzg_commitments_against_transactions + * No expensive verification, just checks that the version hashes are consistent with the kzg commitments + */ +export function verifyKzgCommitmentsAgainstTransactions( + transactions: bellatrix.Transaction[], + blobKzgCommitments: eip4844.KZGCommitment[] +): boolean { + const allVersionedHashes: VersionHash[] = []; + for (const tx of transactions) { + if (tx[0] === BLOB_TX_TYPE) { + // TODO EIP-4844: Optimize array manipulation + allVersionedHashes.push(...txPeekBlobVersionedHashes(tx)); + } + } + + if (allVersionedHashes.length !== blobKzgCommitments.length) { + throw Error( + `allVersionedHashes len ${allVersionedHashes.length} != blobKzgCommitments len ${blobKzgCommitments.length}` + ); + } + + for (let i = 0; i < blobKzgCommitments.length; i++) { + const versionedHash = kzgCommitmentToVersionedHash(blobKzgCommitments[i]); + if (!byteArrayEquals(allVersionedHashes[i], versionedHash)) { + throw Error(`Wrong versionedHash ${i} ${toHex(allVersionedHashes[i])} != ${toHex(versionedHash)}`); + } + } + + // TODO EIP-4844: Use proper API, either throw error or return boolean + return true; +} + +function txPeekBlobVersionedHashes(opaqueTx: bellatrix.Transaction): VersionHash[] { + if (opaqueTx[0] !== BLOB_TX_TYPE) { + throw Error(`tx type ${opaqueTx[0]} != BLOB_TX_TYPE`); + } + + const opaqueTxDv = new DataView(opaqueTx.buffer, opaqueTx.byteOffset, opaqueTx.byteLength); + + const blobVersionedHashesOffset = + OPAQUE_TX_MESSAGE_OFFSET + opaqueTxDv.getUint32(OPAQUE_TX_BLOB_VERSIONED_HASHES_OFFSET, true); + + // Guard against offsets that go beyond end of bytes + if (blobVersionedHashesOffset > opaqueTx.length) { + throw Error(`blobVersionedHashesOffset ${blobVersionedHashesOffset} > EOF ${opaqueTx.length}`); + } + + // Guard against not multiple of BYTES_PER_HASH + const blobVersionedHashesByteLen = opaqueTx.length - blobVersionedHashesOffset; + if ((opaqueTx.length - blobVersionedHashesOffset) % BYTES_PER_HASH !== 0) { + throw Error(`Uneven blobVersionedHashesByteLen ${blobVersionedHashesByteLen}`); + } + + const versionedHashes: VersionHash[] = []; + + // iterate from x to end of data, in steps of 32, to get all hashes + for (let i = blobVersionedHashesOffset; i < opaqueTx.length; i += BYTES_PER_HASH) { + versionedHashes.push(opaqueTx.subarray(i, i + BYTES_PER_HASH)); + } + + return versionedHashes; +} + +export function kzgCommitmentToVersionedHash(kzgCommitment: eip4844.KZGCommitment): VersionHash { + const hash = SHA256.digest(kzgCommitment); + // Equivalent to `VERSIONED_HASH_VERSION_KZG + hash(kzg_commitment)[1:]` + hash[0] = VERSIONED_HASH_VERSION_KZG; + return hash; +} diff --git a/packages/state-transition/src/util/index.ts b/packages/state-transition/src/util/index.ts index 74a5db2b835a..24f4f3dd3707 100644 --- a/packages/state-transition/src/util/index.ts +++ b/packages/state-transition/src/util/index.ts @@ -3,6 +3,7 @@ export * from "./array.js"; export * from "./attestation.js"; export * from "./attesterStatus.js"; export * from "./balance.js"; +export * from "./blobs.js"; export * from "./capella.js"; export * from "./execution.js"; export * from "./blockRoot.js"; diff --git a/packages/state-transition/test/unit/util/blobs.test.ts b/packages/state-transition/test/unit/util/blobs.test.ts new file mode 100644 index 000000000000..9ce23fd7f85a --- /dev/null +++ b/packages/state-transition/test/unit/util/blobs.test.ts @@ -0,0 +1,53 @@ +import {fromHex} from "@lodestar/utils"; +import {verifyKzgCommitmentsAgainstTransactions} from "../../../src/index.js"; + +// Data from fee-merket test at Inphi/eip4844-interop +// Ref https://github.com/Inphi/eip4844-interop/blob/cad0dab50901cc1371a683388136fd56654d3bba/tests/fee-market/main.go#L67 +const tx = + "05450000000058aca83353114c8385ece4c18461dbb8d1ef7479c3640e812acd2ec488c56fcf43e305a508bc30bec9bff27ab367a1ed0b397b0299bb7f48b72884f5178e7c0a0100000000000000000000000000000000000000000000000000000000000000000000000000000000f2052a0100000000000000000000000000000000000000000000000000000000f2052a010000000000000000000000000000000000000000000000000000005034030000000000c00000004e61bc0000000000000000000000000000000000000000000000000000000000d5000000d5000000005ed0b200000000000000000000000000000000000000000000000000000000d500000001ffb38a7a99e3e2335be83fc74b7faa19d553124301134066927e042d676d93e523ef251e0b82bdcb72d2ca85e99c804f60ffa989"; +const blobKzgCommitment = + "a1a6202ee4a387c16ba23379b9c471a0cc48cb53793678faa92b2920c270867eebeac74351946859ffd199c6fe32385e"; + +describe("blobs", () => { + it("verifyKzgCommitmentsAgainstTransactions", () => { + verifyKzgCommitmentsAgainstTransactions([fromHex(tx)], [fromHex(blobKzgCommitment)]); + }); + + // 05 + // 45000000 + // 0058aca83353114c8385ece4c18461dbb8d1ef7479c3640e812acd2ec488c56fcf43e305a508bc30bec9bff27ab367a1ed0b397b0299bb7f48b72884f5178e7c0a + // 000 - 0100000000000000000000000000000000000000000000000000000000000000 // chain_id + // 032 - 0000000000000000 // nonce + // 040 - 00f2052a01000000000000000000000000000000000000000000000000000000 // max_priority_fee_per_gas + // 072 - 00f2052a01000000000000000000000000000000000000000000000000000000 // max_fee_per_gas + // 104 - 5034030000000000 // gas + // 112 - c0000000 // to - offset_value = 192 + // 116 - 4e61bc0000000000000000000000000000000000000000000000000000000000 // value + // 148 - d5000000 // data + // 152 - d5000000 // access_list + // 156 - 005ed0b200000000000000000000000000000000000000000000000000000000 // max_fee_per_data_gas + // 188 - d5000000 // blob_versioned_hashes - offset_value = 213 + // 192 - 01 ffb38a7a99e3e2335be83fc74b7faa19d5531243 // __value_of to + // 213 - 01134066927e042d676d93e523ef251e0b82bdcb72d2ca85e99c804f60ffa989 // __value_of blob_versioned_hashes + // + // field offset: 32 + 8 + 32 + 32 + 8 + 4 + 32 + 4 + 4 + 32 = 188 + + // class SignedBlobTransaction(Container): + // message: BlobTransaction // 4 bytes offset, continues on 69 + // signature: ECDSASignature // 65 bytes + + // class BlobTransaction(Container): + // chain_id: uint256 // 32 bytes + // nonce: uint64 // 8 bytes + // max_priority_fee_per_gas: uint256 // 32 bytes + // max_fee_per_gas: uint256 // 32 bytes + // gas: uint64 // 8 bytes + // to: Union[None, Address] # Address = Bytes20 // 4 bytes offset + // value: uint256 // 32 bytes + // data: ByteList[MAX_CALLDATA_SIZE] // 4 bytes offset + // access_list: List[AccessTuple, MAX_ACCESS_LIST_SIZE] // 4 bytes offset + // max_fee_per_data_gas: uint256 # new in PR 5707, a.k.a. fee market change of EIP-4844 // 32 bytes offset + // blob_versioned_hashes: List[VersionedHash, MAX_VERSIONED_HASHES_LIST_SIZE] // 4 bytes offset + + // field_offset = 32 + 8 + 32 + 32 + 8 + 4 + 32 + 4 + 4 + 32 = 188 +});