diff --git a/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/components.nr b/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/components.nr index 776ee6e8851e..5a7aaaf6e17c 100644 --- a/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/components.nr +++ b/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/components.nr @@ -5,7 +5,7 @@ use crate::abis::{ use super::abis::tx_effect::TxEffect; use dep::types::{ abis::{ - log::Log, log_hash::ScopedLogHash, public_data_write::PublicDataWrite, + log_hash::ScopedLogHash, private_log::PrivateLog, public_data_write::PublicDataWrite, public_log::PublicLog, sponge_blob::SpongeBlob, }, constants::{ @@ -84,7 +84,6 @@ pub fn accumulate_blocks_fees( // TODO(Miranda): combine fees with same recipient depending on rollup structure // Assuming that the final rollup tree (block root -> block merge -> root) has max 32 leaves (TODO: constrain in root), then // in the worst case, we would be checking the left 16 values (left_len = 16) against the right 16 (right_len = 16). - // Either way, construct arr in unconstrained and make use of hints to point to merged fee array. array_merge(left.fees, right.fees) } @@ -101,7 +100,7 @@ pub fn accumulate_blob_public_inputs( left_len + right_len <= AZTEC_MAX_EPOCH_DURATION, "too many blob public input structs accumulated in rollup", ); - // NB: For some reason, the below is around 150k gates cheaper than array_merge + // NB: The below is cheaper than array_merge because assigning BlockBlobPublicInputs is cheaper than calling .equals let mut add_from_left = true; let mut result = [BlockBlobPublicInputs::empty(); AZTEC_MAX_EPOCH_DURATION]; for i in 0..result.len() { @@ -246,12 +245,14 @@ fn get_tx_effects_hash_input( // NOTE HASHES array_len = array_length(note_hashes); if array_len != 0 { + let mut check_elt = true; let notes_prefix = encode_blob_prefix(NOTES_PREFIX, array_len); assert_eq(tx_effects_hash_input[offset], notes_prefix); offset += 1; for j in 0..MAX_NOTE_HASHES_PER_TX { - if j < array_len { + check_elt &= j != array_len; + if check_elt { assert_eq(tx_effects_hash_input[offset + j], note_hashes[j]); } } @@ -261,12 +262,14 @@ fn get_tx_effects_hash_input( // NULLIFIERS array_len = array_length(nullifiers); if array_len != 0 { + let mut check_elt = true; let nullifiers_prefix = encode_blob_prefix(NULLIFIERS_PREFIX, array_len); assert_eq(tx_effects_hash_input[offset], nullifiers_prefix); offset += 1; for j in 0..MAX_NULLIFIERS_PER_TX { - if j < array_len { + check_elt &= j != array_len; + if check_elt { assert_eq(tx_effects_hash_input[offset + j], nullifiers[j]); } } @@ -276,12 +279,14 @@ fn get_tx_effects_hash_input( // L2 TO L1 MESSAGES array_len = array_length(tx_effect.l2_to_l1_msgs); if array_len != 0 { + let mut check_elt = true; let l2_to_l1_msgs_prefix = encode_blob_prefix(L2_L1_MSGS_PREFIX, array_len); assert_eq(tx_effects_hash_input[offset], l2_to_l1_msgs_prefix); offset += 1; for j in 0..MAX_L2_TO_L1_MSGS_PER_TX { - if j < array_len { + check_elt &= j != array_len; + if check_elt { assert_eq(tx_effects_hash_input[offset + j], tx_effect.l2_to_l1_msgs[j]); } } @@ -291,12 +296,14 @@ fn get_tx_effects_hash_input( // PUBLIC DATA UPDATE REQUESTS array_len = array_length(public_data_update_requests); if array_len != 0 { + let mut check_elt = true; let public_data_update_requests_prefix = encode_blob_prefix(PUBLIC_DATA_UPDATE_REQUESTS_PREFIX, array_len * 2); assert_eq(tx_effects_hash_input[offset], public_data_update_requests_prefix); offset += 1; for j in 0..MAX_TOTAL_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX { - if j < array_len { + check_elt &= j != array_len; + if check_elt { assert_eq( tx_effects_hash_input[offset + j * 2], public_data_update_requests[j].leaf_slot, @@ -314,6 +321,7 @@ fn get_tx_effects_hash_input( // PRIVATE_LOGS array_len = array_length(private_logs) * PRIVATE_LOG_SIZE_IN_FIELDS; if array_len != 0 { + let mut check_elt = true; let private_logs_prefix = encode_blob_prefix(PRIVATE_LOGS_PREFIX, array_len); assert_eq(tx_effects_hash_input[offset], private_logs_prefix); offset += 1; @@ -321,7 +329,8 @@ fn get_tx_effects_hash_input( for j in 0..MAX_PRIVATE_LOGS_PER_TX { for k in 0..PRIVATE_LOG_SIZE_IN_FIELDS { let index = offset + j * PRIVATE_LOG_SIZE_IN_FIELDS + k; - if index < array_len { + check_elt &= j * PRIVATE_LOG_SIZE_IN_FIELDS + k != array_len; + if check_elt { assert_eq(tx_effects_hash_input[index], private_logs[j].fields[k]); } } @@ -332,6 +341,7 @@ fn get_tx_effects_hash_input( // PUBLIC LOGS array_len = array_length(public_logs) * PUBLIC_LOG_SIZE_IN_FIELDS; if array_len != 0 { + let mut check_elt = true; let public_logs_prefix = encode_blob_prefix(PUBLIC_LOGS_PREFIX, array_len); assert_eq(tx_effects_hash_input[offset], public_logs_prefix); offset += 1; @@ -339,7 +349,8 @@ fn get_tx_effects_hash_input( let log = public_logs[j].serialize(); for k in 0..PUBLIC_LOG_SIZE_IN_FIELDS { let index = offset + j * PUBLIC_LOG_SIZE_IN_FIELDS + k; - if index < array_len { + check_elt &= j * PUBLIC_LOG_SIZE_IN_FIELDS + k != array_len; + if check_elt { assert_eq(tx_effects_hash_input[index], log[k]); } } @@ -352,12 +363,14 @@ fn get_tx_effects_hash_input( // CONTRACT CLASS LOGS array_len = array_length(contract_class_logs); if array_len != 0 { + let mut check_elt = true; let contract_class_logs_prefix = encode_blob_prefix(CONTRACT_CLASS_LOGS_PREFIX, array_len); assert_eq(tx_effects_hash_input[offset], contract_class_logs_prefix); offset += 1; for j in 0..MAX_CONTRACT_CLASS_LOGS_PER_TX { - if j < array_len { + check_elt &= j != array_len; + if check_elt { assert_eq(tx_effects_hash_input[offset + j], contract_class_logs[j]); } } @@ -390,7 +403,7 @@ unconstrained fn get_tx_effects_hash_input_helper( nullifiers: [Field; MAX_NULLIFIERS_PER_TX], l2_to_l1_msgs: [Field; MAX_L2_TO_L1_MSGS_PER_TX], public_data_update_requests: [PublicDataWrite; MAX_TOTAL_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX], - private_logs: [Log; MAX_PRIVATE_LOGS_PER_TX], + private_logs: [PrivateLog; MAX_PRIVATE_LOGS_PER_TX], public_logs: [PublicLog; MAX_PUBLIC_LOGS_PER_TX], contract_class_logs: [Field; MAX_CONTRACT_CLASS_LOGS_PER_TX], revert_code: Field, diff --git a/noir-projects/noir-protocol-circuits/crates/types/src/utils/arrays.nr b/noir-projects/noir-protocol-circuits/crates/types/src/utils/arrays.nr index 44a76b077121..67b27f858c09 100644 --- a/noir-projects/noir-protocol-circuits/crates/types/src/utils/arrays.nr +++ b/noir-projects/noir-protocol-circuits/crates/types/src/utils/arrays.nr @@ -135,8 +135,29 @@ pub fn array_concat(array1: [T; N], array2: [T; M]) - } result } - +/// This function assumes that `array1` and `array2` contain no more than N non-empty elements between them, +/// if this is not the case then elements from the end of `array2` will be dropped. pub fn array_merge(array1: [T; N], array2: [T; N]) -> [T; N] +where + T: Empty + Eq, +{ + /// Safety: we constrain this array below + let result = unsafe { array_merge_helper(array1, array2) }; + // We assume arrays have been validated. The only use cases so far are with previously validated arrays. + let array1_len = array_length(array1); + let mut add_from_left = true; + for i in 0..N { + add_from_left &= i != array1_len; + if add_from_left { + assert_eq(result[i], array1[i]); + } else { + assert_eq(result[i], array2[i - array1_len]); + } + } + result +} + +unconstrained fn array_merge_helper(array1: [T; N], array2: [T; N]) -> [T; N] where T: Empty + Eq, { diff --git a/yarn-project/archiver/src/archiver/archiver.test.ts b/yarn-project/archiver/src/archiver/archiver.test.ts index c1adc87376a5..1120d7c0dc67 100644 --- a/yarn-project/archiver/src/archiver/archiver.test.ts +++ b/yarn-project/archiver/src/archiver/archiver.test.ts @@ -183,7 +183,7 @@ describe('Archiver', () => { (b.header.globalVariables.timestamp = new Fr(now + DefaultL1ContractsConfig.ethereumSlotDuration * (i + 1))), ); const rollupTxs = await Promise.all(blocks.map(makeRollupTx)); - const blobHashes = await Promise.all(blocks.map(makeVersionedBlobHash)); + const blobHashes = await Promise.all(blocks.map(makeVersionedBlobHashes)); publicClient.getBlockNumber.mockResolvedValueOnce(2500n).mockResolvedValueOnce(2600n).mockResolvedValueOnce(2700n); @@ -199,19 +199,19 @@ describe('Archiver', () => { mockInbox.read.totalMessagesInserted.mockResolvedValueOnce(2n).mockResolvedValueOnce(6n); - const blobsFromBlocks = await Promise.all(blocks.map(b => makeBlobFromBlock(b))); - blobsFromBlocks.forEach(blob => blobSinkClient.getBlobSidecar.mockResolvedValueOnce([blob])); + const blobsFromBlocks = await Promise.all(blocks.map(b => makeBlobsFromBlock(b))); + blobsFromBlocks.forEach(blobs => blobSinkClient.getBlobSidecar.mockResolvedValueOnce(blobs)); makeMessageSentEvent(98n, 1n, 0n); makeMessageSentEvent(99n, 1n, 1n); - makeL2BlockProposedEvent(101n, 1n, blocks[0].archive.root.toString(), [blobHashes[0]]); + makeL2BlockProposedEvent(101n, 1n, blocks[0].archive.root.toString(), blobHashes[0]); makeMessageSentEvent(2504n, 2n, 0n); makeMessageSentEvent(2505n, 2n, 1n); makeMessageSentEvent(2505n, 2n, 2n); makeMessageSentEvent(2506n, 3n, 1n); - makeL2BlockProposedEvent(2510n, 2n, blocks[1].archive.root.toString(), [blobHashes[1]]); - makeL2BlockProposedEvent(2520n, 3n, blocks[2].archive.root.toString(), [blobHashes[2]]); + makeL2BlockProposedEvent(2510n, 2n, blocks[1].archive.root.toString(), blobHashes[1]); + makeL2BlockProposedEvent(2520n, 3n, blocks[2].archive.root.toString(), blobHashes[2]); publicClient.getTransaction.mockResolvedValueOnce(rollupTxs[0]); rollupTxs.slice(1).forEach(tx => publicClient.getTransaction.mockResolvedValueOnce(tx)); @@ -281,7 +281,7 @@ describe('Archiver', () => { const numL2BlocksInTest = 2; const rollupTxs = await Promise.all(blocks.map(makeRollupTx)); - const blobHashes = await Promise.all(blocks.map(makeVersionedBlobHash)); + const blobHashes = await Promise.all(blocks.map(makeVersionedBlobHashes)); // Here we set the current L1 block number to 102. L1 to L2 messages after this should not be read. publicClient.getBlockNumber.mockResolvedValue(102n); @@ -295,13 +295,13 @@ describe('Archiver', () => { makeMessageSentEvent(66n, 1n, 0n); makeMessageSentEvent(68n, 1n, 1n); - makeL2BlockProposedEvent(70n, 1n, blocks[0].archive.root.toString(), [blobHashes[0]]); - makeL2BlockProposedEvent(80n, 2n, blocks[1].archive.root.toString(), [blobHashes[1]]); + makeL2BlockProposedEvent(70n, 1n, blocks[0].archive.root.toString(), blobHashes[0]); + makeL2BlockProposedEvent(80n, 2n, blocks[1].archive.root.toString(), blobHashes[1]); makeL2BlockProposedEvent(90n, 3n, badArchive, [badBlobHash]); rollupTxs.forEach(tx => publicClient.getTransaction.mockResolvedValueOnce(tx)); - const blobsFromBlocks = await Promise.all(blocks.map(b => makeBlobFromBlock(b))); - blobsFromBlocks.forEach(blob => blobSinkClient.getBlobSidecar.mockResolvedValueOnce([blob])); + const blobsFromBlocks = await Promise.all(blocks.map(b => makeBlobsFromBlock(b))); + blobsFromBlocks.forEach(blobs => blobSinkClient.getBlobSidecar.mockResolvedValueOnce(blobs)); await archiver.start(false); @@ -326,7 +326,7 @@ describe('Archiver', () => { const numL2BlocksInTest = 2; const rollupTxs = await Promise.all(blocks.map(makeRollupTx)); - const blobHashes = await Promise.all(blocks.map(makeVersionedBlobHash)); + const blobHashes = await Promise.all(blocks.map(makeVersionedBlobHashes)); publicClient.getBlockNumber.mockResolvedValueOnce(50n).mockResolvedValueOnce(100n); mockRollup.read.status @@ -337,12 +337,12 @@ describe('Archiver', () => { makeMessageSentEvent(66n, 1n, 0n); makeMessageSentEvent(68n, 1n, 1n); - makeL2BlockProposedEvent(70n, 1n, blocks[0].archive.root.toString(), [blobHashes[0]]); - makeL2BlockProposedEvent(80n, 2n, blocks[1].archive.root.toString(), [blobHashes[1]]); + makeL2BlockProposedEvent(70n, 1n, blocks[0].archive.root.toString(), blobHashes[0]); + makeL2BlockProposedEvent(80n, 2n, blocks[1].archive.root.toString(), blobHashes[1]); rollupTxs.forEach(tx => publicClient.getTransaction.mockResolvedValueOnce(tx)); - const blobsFromBlocks = await Promise.all(blocks.map(b => makeBlobFromBlock(b))); - blobsFromBlocks.forEach(blob => blobSinkClient.getBlobSidecar.mockResolvedValueOnce([blob])); + const blobsFromBlocks = await Promise.all(blocks.map(b => makeBlobsFromBlock(b))); + blobsFromBlocks.forEach(blobs => blobSinkClient.getBlobSidecar.mockResolvedValueOnce(blobs)); await archiver.start(false); @@ -364,7 +364,7 @@ describe('Archiver', () => { const numL2BlocksInTest = 2; const rollupTxs = await Promise.all(blocks.map(makeRollupTx)); - const blobHashes = await Promise.all(blocks.map(makeVersionedBlobHash)); + const blobHashes = await Promise.all(blocks.map(makeVersionedBlobHashes)); publicClient.getBlockNumber.mockResolvedValueOnce(50n).mockResolvedValueOnce(100n).mockResolvedValueOnce(150n); @@ -388,12 +388,12 @@ describe('Archiver', () => { makeMessageSentEvent(66n, 1n, 0n); makeMessageSentEvent(68n, 1n, 1n); - makeL2BlockProposedEvent(70n, 1n, blocks[0].archive.root.toString(), [blobHashes[0]]); - makeL2BlockProposedEvent(80n, 2n, blocks[1].archive.root.toString(), [blobHashes[1]]); + makeL2BlockProposedEvent(70n, 1n, blocks[0].archive.root.toString(), blobHashes[0]); + makeL2BlockProposedEvent(80n, 2n, blocks[1].archive.root.toString(), blobHashes[1]); rollupTxs.forEach(tx => publicClient.getTransaction.mockResolvedValueOnce(tx)); - const blobsFromBlocks = await Promise.all(blocks.map(b => makeBlobFromBlock(b))); - blobsFromBlocks.forEach(blob => blobSinkClient.getBlobSidecar.mockResolvedValueOnce([blob])); + const blobsFromBlocks = await Promise.all(blocks.map(b => makeBlobsFromBlock(b))); + blobsFromBlocks.forEach(blobs => blobSinkClient.getBlobSidecar.mockResolvedValueOnce(blobs)); await archiver.start(false); @@ -434,15 +434,15 @@ describe('Archiver', () => { const l2Block = blocks[0]; l2Block.header.globalVariables.slotNumber = new Fr(notLastL2SlotInEpoch); blocks = [l2Block]; - const blobHashes = [await makeVersionedBlobHash(l2Block)]; + const blobHashes = await makeVersionedBlobHashes(l2Block); const rollupTxs = await Promise.all(blocks.map(makeRollupTx)); publicClient.getBlockNumber.mockResolvedValueOnce(l1BlockForL2Block); mockRollup.read.status.mockResolvedValueOnce([0n, GENESIS_ROOT, 1n, l2Block.archive.root.toString(), GENESIS_ROOT]); makeL2BlockProposedEvent(l1BlockForL2Block, 1n, l2Block.archive.root.toString(), blobHashes); rollupTxs.forEach(tx => publicClient.getTransaction.mockResolvedValueOnce(tx)); - const blobsFromBlocks = await Promise.all(blocks.map(b => makeBlobFromBlock(b))); - blobsFromBlocks.forEach(blob => blobSinkClient.getBlobSidecar.mockResolvedValueOnce([blob])); + const blobsFromBlocks = await Promise.all(blocks.map(b => makeBlobsFromBlock(b))); + blobsFromBlocks.forEach(blobs => blobSinkClient.getBlobSidecar.mockResolvedValueOnce(blobs)); await archiver.start(false); @@ -468,7 +468,7 @@ describe('Archiver', () => { const l2Block = blocks[0]; l2Block.header.globalVariables.slotNumber = new Fr(lastL2SlotInEpoch); blocks = [l2Block]; - const blobHashes = [await makeVersionedBlobHash(l2Block)]; + const blobHashes = await makeVersionedBlobHashes(l2Block); const rollupTxs = await Promise.all(blocks.map(makeRollupTx)); publicClient.getBlockNumber.mockResolvedValueOnce(l1BlockForL2Block); @@ -476,8 +476,8 @@ describe('Archiver', () => { makeL2BlockProposedEvent(l1BlockForL2Block, 1n, l2Block.archive.root.toString(), blobHashes); rollupTxs.forEach(tx => publicClient.getTransaction.mockResolvedValueOnce(tx)); - const blobsFromBlocks = await Promise.all(blocks.map(b => makeBlobFromBlock(b))); - blobsFromBlocks.forEach(blob => blobSinkClient.getBlobSidecar.mockResolvedValueOnce([blob])); + const blobsFromBlocks = await Promise.all(blocks.map(b => makeBlobsFromBlock(b))); + blobsFromBlocks.forEach(blobs => blobSinkClient.getBlobSidecar.mockResolvedValueOnce(blobs)); await archiver.start(false); @@ -594,22 +594,20 @@ async function makeRollupTx(l2Block: L2Block) { } /** - * Makes a versioned blob hash for testing purposes. + * Makes versioned blob hashes for testing purposes. * @param l2Block - The L2 block. - * @returns A versioned blob hash. + * @returns Versioned blob hashes. */ -async function makeVersionedBlobHash(l2Block: L2Block): Promise<`0x${string}`> { - return `0x${(await Blob.fromFields(l2Block.body.toBlobFields())) - .getEthVersionedBlobHash() - .toString('hex')}` as `0x${string}`; +async function makeVersionedBlobHashes(l2Block: L2Block): Promise<`0x${string}`[]> { + const blobHashes = (await Blob.getBlobs(l2Block.body.toBlobFields())).map(b => b.getEthVersionedBlobHash()); + return blobHashes.map(h => `0x${h.toString('hex')}` as `0x${string})`); } /** * Blob response to be returned from the blob sink based on the expected block. * @param block - The block. - * @returns The blob. + * @returns The blobs. */ -function makeBlobFromBlock(block: L2Block) { - const blob = block.body.toBlobFields(); - return Blob.fromFields(blob); +async function makeBlobsFromBlock(block: L2Block) { + return await Blob.getBlobs(block.body.toBlobFields()); } diff --git a/yarn-project/archiver/src/archiver/data_retrieval.ts b/yarn-project/archiver/src/archiver/data_retrieval.ts index 7cd24a8e9cc2..d34cec485f03 100644 --- a/yarn-project/archiver/src/archiver/data_retrieval.ts +++ b/yarn-project/archiver/src/archiver/data_retrieval.ts @@ -243,7 +243,7 @@ async function getBlockFromRollupTx( // Body.fromBlobFields to accept blob buffers directly let blockFields: Fr[]; try { - blockFields = blobBodies.flatMap(b => b.toEncodedFields()); + blockFields = Blob.toEncodedFields(blobBodies); } catch (err: any) { if (err instanceof BlobDeserializationError) { logger.fatal(err.message); diff --git a/yarn-project/blob-lib/src/blob.ts b/yarn-project/blob-lib/src/blob.ts index b9c0f5359f8b..4f0810b26da5 100644 --- a/yarn-project/blob-lib/src/blob.ts +++ b/yarn-project/blob-lib/src/blob.ts @@ -1,8 +1,8 @@ -// Importing directly from 'c-kzg' does not work, ignoring import/no-named-as-default-member err: import { poseidon2Hash, sha256 } from '@aztec/foundation/crypto'; import { Fr } from '@aztec/foundation/fields'; import { BufferReader, serializeToBuffer } from '@aztec/foundation/serialize'; +// Importing directly from 'c-kzg' does not work, ignoring import/no-named-as-default-member err: import cKzg from 'c-kzg'; import type { Blob as BlobBuffer } from 'c-kzg'; @@ -164,6 +164,23 @@ export class Blob { } } + /** + * Get the encoded fields from multiple blobs. + * + * @dev This method takes into account trailing zeros + * + * @returns The encoded fields from the blobs. + */ + static toEncodedFields(blobs: Blob[]): Fr[] { + try { + return deserializeEncodedBlobToFields(Buffer.concat(blobs.map(b => b.data))); + } catch (err) { + throw new BlobDeserializationError( + `Failed to deserialize encoded blob fields, this blob was likely not created by us`, + ); + } + } + /** * Get the commitment fields from the blob. *