From cdd0d2bc5274038bd3e281ad0e62a59e6b7f038b Mon Sep 17 00:00:00 2001 From: Mitchell Tracy Date: Thu, 21 Mar 2024 19:28:28 -0400 Subject: [PATCH] wip: [skip ci] --- .../core/libraries/decoders/TxsDecoder.sol | 65 +-- .../crates/rollup-lib/src/base.nr | 1 + .../rollup-lib/src/base/calldata_gas.nr | 62 +++ .../combined_accumulated_data.nr | 8 +- .../combined_accumulated_data_builder.nr | 2 + yarn-project/circuit-types/jest.config.ts | 12 + yarn-project/circuit-types/package.json | 11 - .../src/calldata_tx_effect_factory.test.ts | 34 ++ .../src/calldata_tx_effect_factory.ts | 78 ++++ yarn-project/circuit-types/src/index.ts | 21 +- .../circuit-types/src/tx/processed_tx.ts | 39 +- yarn-project/circuit-types/src/tx_effect.ts | 61 ++- .../__snapshots__/gas_used.test.ts.snap | 393 ++++++++++++++++++ .../circuits.js/src/structs/gas_used.test.ts | 38 ++ .../circuits.js/src/structs/gas_used.ts | 62 +++ yarn-project/circuits.js/src/structs/index.ts | 1 + ...vate_kernel_tail_circuit_private_inputs.ts | 4 +- .../circuits.js/src/structs/revert_code.ts | 11 +- .../src/type_conversion.ts | 12 + .../src/note_processor/note_processor.test.ts | 25 +- 20 files changed, 862 insertions(+), 78 deletions(-) create mode 100644 noir-projects/noir-protocol-circuits/crates/rollup-lib/src/base/calldata_gas.nr create mode 100644 yarn-project/circuit-types/jest.config.ts create mode 100644 yarn-project/circuit-types/src/calldata_tx_effect_factory.test.ts create mode 100644 yarn-project/circuit-types/src/calldata_tx_effect_factory.ts create mode 100644 yarn-project/circuits.js/src/structs/__snapshots__/gas_used.test.ts.snap create mode 100644 yarn-project/circuits.js/src/structs/gas_used.test.ts create mode 100644 yarn-project/circuits.js/src/structs/gas_used.ts diff --git a/l1-contracts/src/core/libraries/decoders/TxsDecoder.sol b/l1-contracts/src/core/libraries/decoders/TxsDecoder.sol index 587197b8088..e7dcdd22713 100644 --- a/l1-contracts/src/core/libraries/decoders/TxsDecoder.sol +++ b/l1-contracts/src/core/libraries/decoders/TxsDecoder.sol @@ -18,33 +18,38 @@ import {Hash} from "../Hash.sol"; * ------------------- * L2 Body Data Specification * ------------------- - * | byte start | num bytes | name - * | --- | --- | --- - * | 0x0 | 0x4 | len(numTxs) (denoted t) - * | | | TxEffect 0 { - * | 0x4 | 0x1 | len(newNoteHashes) (denoted b) - * | 0x4 + 0x1 | b * 0x20 | newNoteHashes - * | 0x4 + 0x1 + b * 0x20 | 0x1 | len(newNullifiers) (denoted c) - * | 0x4 + 0x1 + b * 0x20 + 0x1 | c * 0x20 | newNullifiers - * | 0x4 + 0x1 + b * 0x20 + 0x1 + c * 0x20 | 0x1 | len(newL2ToL1Msgs) (denoted d) - * | 0x4 + 0x1 + b * 0x20 + 0x1 + c * 0x20 + 0x1 | d * 0x20 | newL2ToL1Msgs - * | 0x4 + 0x1 + b * 0x20 + 0x1 + c * 0x20 + 0x1 + d * 0x20 | 0x1 | len(newPublicDataWrites) (denoted e) - * | 0x4 + 0x1 + b * 0x20 + 0x1 + c * 0x20 + 0x1 + d * 0x20 + 0x01 | e * 0x40 | newPublicDataWrites - * | 0x4 + 0x1 + b * 0x20 + 0x1 + c * 0x20 + 0x1 + d * 0x20 + 0x01 + e * 0x40 | 0x04 | byteLen(newEncryptedLogs) (denoted f) - * | 0x4 + 0x1 + b * 0x20 + 0x1 + c * 0x20 + 0x1 + d * 0x20 + 0x01 + e * 0x40 + 0x4 | f | newEncryptedLogs - * | 0x4 + 0x1 + b * 0x20 + 0x1 + c * 0x20 + 0x1 + d * 0x20 + 0x01 + e * 0x40 + 0x4 + f | 0x04 | byteLen(newUnencryptedLogs) (denoted g) - * | 0x4 + 0x1 + b * 0x20 + 0x1 + c * 0x20 + 0x1 + d * 0x20 + 0x01 + e * 0x40 + 0x4 + f + 0x4 | g | newUnencryptedLogs - * | | | }, - * | | | TxEffect 1 { - * | | | ... - * | | | }, - * | | | ... - * | | | TxEffect (t - 1) { - * | | | ... - * | | | }, + * | byte start | num bytes | name + * | --- | --- | --- + * | 0x0 | 0x4 | len(numTxs) (denoted t) + * | | | TxEffect 0 { + * | 0x4 | 0x8 | daGasUsed + * | 0x4 + 0x8 | 0x8 | computeGasUsed + * | 0x4 + 0x8 + 0x8 | 0x1 | revertCode + * | 0x4 + 0x8 + 0x8 + 0x1 | 0x1 | len(newNoteHashes) (denoted b) + * | 0x4 + 0x8 + 0x8 + 0x1 + 0x1 | b * 0x20 | newNoteHashes + * | 0x4 + 0x8 + 0x8 + 0x1 + 0x1 + b * 0x20 | 0x1 | len(newNullifiers) (denoted c) + * | 0x4 + 0x8 + 0x8 + 0x1 + 0x1 + b * 0x20 + 0x1 | c * 0x20 | newNullifiers + * | 0x4 + 0x8 + 0x8 + 0x1 + 0x1 + b * 0x20 + 0x1 + c * 0x20 | 0x1 | len(newL2ToL1Msgs) (denoted d) + * | 0x4 + 0x8 + 0x8 + 0x1 + 0x1 + b * 0x20 + 0x1 + c * 0x20 + 0x1 | d * 0x20 | newL2ToL1Msgs + * | 0x4 + 0x8 + 0x8 + 0x1 + 0x1 + b * 0x20 + 0x1 + c * 0x20 + 0x1 + d * 0x20 | 0x1 | len(newPublicDataWrites) (denoted e) + * | 0x4 + 0x8 + 0x8 + 0x1 + 0x1 + b * 0x20 + 0x1 + c * 0x20 + 0x1 + d * 0x20 + 0x01 | e * 0x40 | newPublicDataWrites + * | 0x4 + 0x8 + 0x8 + 0x1 + 0x1 + b * 0x20 + 0x1 + c * 0x20 + 0x1 + d * 0x20 + 0x01 + e * 0x40 | 0x04 | byteLen(newEncryptedLogs) (denoted f) + * | 0x4 + 0x8 + 0x8 + 0x1 + 0x1 + b * 0x20 + 0x1 + c * 0x20 + 0x1 + d * 0x20 + 0x01 + e * 0x40 + 0x4 | f | newEncryptedLogs + * | 0x4 + 0x8 + 0x8 + 0x1 + 0x1 + b * 0x20 + 0x1 + c * 0x20 + 0x1 + d * 0x20 + 0x01 + e * 0x40 + 0x4 + f | 0x04 | byteLen(newUnencryptedLogs) (denoted g) + * | 0x4 + 0x8 + 0x8 + 0x1 + 0x1 + b * 0x20 + 0x1 + c * 0x20 + 0x1 + d * 0x20 + 0x01 + e * 0x40 + 0x4 + f + 0x4 | g | newUnencryptedLogs + * | | | }, + * | | | TxEffect 1 { + * | | | ... + * | | | }, + * | | | ... + * | | | TxEffect (t - 1) { + * | | | ... + * | | | }, */ library TxsDecoder { struct ArrayOffsets { + uint256 daGasUsed; + uint256 computeGasUsed; uint256 revertCode; uint256 noteHash; uint256 nullifier; @@ -105,6 +110,14 @@ library TxsDecoder { * Zero values. */ + // daGasUsed + offsets.daGasUsed = offset; + offset += 0x8; + + // computeGasUsed + offsets.computeGasUsed = offset; + offset += 0x8; + // Revert Code offsets.revertCode = offset; offset += 0x1; @@ -146,7 +159,9 @@ library TxsDecoder { // Insertions are split into multiple `bytes.concat` to work around stack too deep. vars.baseLeaf = bytes.concat( - // pad the revert code to 32 bytes to match the hash preimage + // pad these values to 32 bytes to match the hash preimage + sliceAndPadLeft(_body, offsets.daGasUsed, 0x8, 0x20), + sliceAndPadLeft(_body, offsets.computeGasUsed, 0x8, 0x20), sliceAndPadLeft(_body, offsets.revertCode, 0x1, 0x20), bytes.concat( sliceAndPadRight( diff --git a/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/base.nr b/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/base.nr index 26987302495..04d0ad96728 100644 --- a/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/base.nr +++ b/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/base.nr @@ -1,5 +1,6 @@ mod base_rollup_inputs; mod state_diff_hints; +mod calldata_gas; use base_rollup_inputs::BaseRollupInputs; use crate::abis::base_or_merge_rollup_public_inputs::BaseOrMergeRollupPublicInputs; diff --git a/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/base/calldata_gas.nr b/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/base/calldata_gas.nr new file mode 100644 index 00000000000..56e8c42b273 --- /dev/null +++ b/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/base/calldata_gas.nr @@ -0,0 +1,62 @@ +use dep::types::abis::accumulated_data::CombinedAccumulatedData; +use dep::types::abis::side_effect::{SideEffect, SideEffectLinkedToNoteHash}; +use dep::types::traits::is_empty; +use dep::types::constants::{ + MAX_NEW_NULLIFIERS_PER_TX, MAX_NEW_NOTE_HASHES_PER_TX, MAX_NEW_L2_TO_L1_MSGS_PER_TX, + MAX_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX +}; + +global FIXED_DA_GAS: u64 = 272; +global DA_GAS_PER_BYTE: u64 = 16; + +pub fn compute_calldata_da_gas(data: CombinedAccumulatedData) -> u64 { + let mut non_zero_bytes = (data.unencrypted_log_preimages_length + data.encrypted_log_preimages_length) as u64; + + for i in 0..MAX_NEW_NOTE_HASHES_PER_TX { + if !is_empty(data.new_note_hashes[i]) { + non_zero_bytes += 32; + } + } + + for i in 0..MAX_NEW_NULLIFIERS_PER_TX { + if !is_empty(data.new_nullifiers[i]) { + non_zero_bytes += 32; + } + } + + for i in 0..MAX_NEW_L2_TO_L1_MSGS_PER_TX { + if !is_empty(data.new_l2_to_l1_msgs[i]) { + non_zero_bytes += 32; + } + } + + for i in 0..MAX_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX { + if !is_empty(data.public_data_update_requests[i]) { + non_zero_bytes += 64; + } + } + + non_zero_bytes * DA_GAS_PER_BYTE + FIXED_DA_GAS +} + +pub fn compute_calldata_compute_gas(data: CombinedAccumulatedData) -> u64 { + data.compute_gas_used +} + +mod tests { + + use dep::types::abis::accumulated_data::{CombinedAccumulatedDataBuilder, CombinedAccumulatedData}; + + use crate::base::calldata_gas::compute_calldata_da_gas; + + #[test] + fn test_compute_calldata_gas() { + let mut builder: CombinedAccumulatedDataBuilder = dep::std::unsafe::zeroed(); + builder.unencrypted_log_preimages_length = 4; + builder.encrypted_log_preimages_length = 4; + let data = builder.finish(); + // see calldata_tx_effect_factory.test.ts for the expected value + assert_eq(compute_calldata_da_gas(data), 400); + } +} + diff --git a/noir-projects/noir-protocol-circuits/crates/types/src/abis/accumulated_data/combined_accumulated_data.nr b/noir-projects/noir-protocol-circuits/crates/types/src/abis/accumulated_data/combined_accumulated_data.nr index 76f7e00531f..a52ed1c3daf 100644 --- a/noir-projects/noir-protocol-circuits/crates/types/src/abis/accumulated_data/combined_accumulated_data.nr +++ b/noir-projects/noir-protocol-circuits/crates/types/src/abis/accumulated_data/combined_accumulated_data.nr @@ -7,7 +7,8 @@ use crate::{ call_request::CallRequest, caller_context::CallerContext, public_data_update_request::PublicDataUpdateRequest, side_effect::{SideEffect, SideEffectLinkedToNoteHash} -} +}, + traits::Empty }; use crate::constants::{ MAX_NEW_NOTE_HASHES_PER_TX, MAX_NEW_NULLIFIERS_PER_TX, MAX_PRIVATE_CALL_STACK_LENGTH_PER_TX, @@ -21,6 +22,9 @@ use crate::traits::is_empty; use crate::utils::arrays::{array_cp, array_concat, array_to_bounded_vec}; struct CombinedAccumulatedData { + da_gas_used: u64, + compute_gas_used: u64, + revert_code: u8, new_note_hashes: [SideEffect; MAX_NEW_NOTE_HASHES_PER_TX], @@ -52,6 +56,8 @@ impl CombinedAccumulatedData { revertible: PublicAccumulatedRevertibleData ) -> CombinedAccumulatedData { CombinedAccumulatedData { + da_gas_used: 0, + compute_gas_used: 1, revert_code: non_revertible.revert_code, new_note_hashes: array_concat(non_revertible.new_note_hashes, revertible.new_note_hashes), new_nullifiers: array_concat(non_revertible.new_nullifiers, revertible.new_nullifiers), diff --git a/noir-projects/noir-protocol-circuits/crates/types/src/abis/accumulated_data/combined_accumulated_data_builder.nr b/noir-projects/noir-protocol-circuits/crates/types/src/abis/accumulated_data/combined_accumulated_data_builder.nr index f72d34c9565..43fe1a972ea 100644 --- a/noir-projects/noir-protocol-circuits/crates/types/src/abis/accumulated_data/combined_accumulated_data_builder.nr +++ b/noir-projects/noir-protocol-circuits/crates/types/src/abis/accumulated_data/combined_accumulated_data_builder.nr @@ -77,6 +77,8 @@ impl CombinedAccumulatedDataBuilder { pub fn finish(self) -> CombinedAccumulatedData { CombinedAccumulatedData { + da_gas_used: 0, + compute_gas_used: 1, revert_code: self.revert_code, new_note_hashes: self.new_note_hashes.storage, new_nullifiers: self.new_nullifiers.storage, diff --git a/yarn-project/circuit-types/jest.config.ts b/yarn-project/circuit-types/jest.config.ts new file mode 100644 index 00000000000..83d85d85f9b --- /dev/null +++ b/yarn-project/circuit-types/jest.config.ts @@ -0,0 +1,12 @@ +import type { Config } from 'jest'; + +const config: Config = { + preset: 'ts-jest/presets/default-esm', + moduleNameMapper: { + '^(\\.{1,2}/.*)\\.[cm]?js$': '$1', + }, + testRegex: './src/.*\\.test\\.(js|mjs|ts)$', + rootDir: './src', +}; + +export default config; diff --git a/yarn-project/circuit-types/package.json b/yarn-project/circuit-types/package.json index fbf2b25be3b..96f082fe961 100644 --- a/yarn-project/circuit-types/package.json +++ b/yarn-project/circuit-types/package.json @@ -25,17 +25,6 @@ "formatting:fix": "run -T eslint --fix ./src && run -T prettier -w ./src", "test": "NODE_NO_WARNINGS=1 node --experimental-vm-modules $(yarn bin jest) --passWithNoTests" }, - "inherits": [ - "../package.common.json" - ], - "jest": { - "preset": "ts-jest/presets/default-esm", - "moduleNameMapper": { - "^(\\.{1,2}/.*)\\.[cm]?js$": "$1" - }, - "testRegex": "./src/.*\\.test\\.(js|mjs|ts)$", - "rootDir": "./src" - }, "dependencies": { "@aztec/circuits.js": "workspace:^", "@aztec/ethereum": "workspace:^", diff --git a/yarn-project/circuit-types/src/calldata_tx_effect_factory.test.ts b/yarn-project/circuit-types/src/calldata_tx_effect_factory.test.ts new file mode 100644 index 00000000000..df85575fb96 --- /dev/null +++ b/yarn-project/circuit-types/src/calldata_tx_effect_factory.test.ts @@ -0,0 +1,34 @@ +import { + Fr, + MAX_NEW_L2_TO_L1_MSGS_PER_TX, + MAX_NEW_NOTE_HASHES_PER_TX, + MAX_NEW_NULLIFIERS_PER_TX, + MAX_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX, + RevertCode, +} from '@aztec/circuits.js'; +import { makeTuple } from '@aztec/foundation/array'; + +import { CalldataTxEffectFactory, DA_BYTE_GAS, FIXED_DA_GAS, TxL2Logs } from './index.js'; +import { PublicDataWrite } from './public_data_write.js'; +import { ITxEffectWithoutGasUsed } from './tx_effect.js'; + +describe('calldata_tx_effect_factory', () => { + it('correctly calculates DA gas for empty TxEffect', () => { + const effect: ITxEffectWithoutGasUsed = { + revertCode: RevertCode.OK, + noteHashes: makeTuple(MAX_NEW_NOTE_HASHES_PER_TX, Fr.zero), + nullifiers: makeTuple(MAX_NEW_NULLIFIERS_PER_TX, Fr.zero), + l2ToL1Msgs: makeTuple(MAX_NEW_L2_TO_L1_MSGS_PER_TX, Fr.zero), + publicDataWrites: makeTuple(MAX_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX, PublicDataWrite.empty), + encryptedLogs: TxL2Logs.empty(), + unencryptedLogs: TxL2Logs.empty(), + }; + + const gasUsed = CalldataTxEffectFactory.build(effect).daGasUsed; + + // 4n for each log, due to encoding of the length of the logs + expect(gasUsed.value).toEqual(FIXED_DA_GAS + 4n * DA_BYTE_GAS + 4n * DA_BYTE_GAS); + }); + + // TODO(@just-mitch) more tests please +}); diff --git a/yarn-project/circuit-types/src/calldata_tx_effect_factory.ts b/yarn-project/circuit-types/src/calldata_tx_effect_factory.ts new file mode 100644 index 00000000000..ec0e01e9581 --- /dev/null +++ b/yarn-project/circuit-types/src/calldata_tx_effect_factory.ts @@ -0,0 +1,78 @@ +import { Fr, GasUsed, RevertCode } from '@aztec/circuits.js'; +import { arrayNonEmptyLength } from '@aztec/foundation/collection'; + +import { PublicDataWrite } from './public_data_write.js'; +import { GasProfiler, GasType, ITxEffectWithoutGasUsed, TxEffect, TxEffectFactory } from './tx_effect.js'; + +export const DA_BYTE_GAS = 16n; + +export const FIXED_BYTES = // 17 bytes + GasUsed.PACKED_SIZE_IN_BYTES + // da_gas_used + GasUsed.PACKED_SIZE_IN_BYTES + // compute_gas_used + RevertCode.PACKED_SIZE_IN_BYTES; // revert_code + +export const FIXED_DA_GAS = FIXED_BYTES * DA_BYTE_GAS; // 272n + +const getComputeGasUsed = (_effect: ITxEffectWithoutGasUsed) => { + // Just a dummy for now + return new GasUsed(1n); +}; + +/** + * Note. This does not exactly match ethereum calldata cost. + * It is correlated, but simplified to ease circuit calculations: + * We don't want to bitwise deconstruct the calldata to count the non-zero bytes in the circuit. + * + * We overcompensate by + * - assuming our FIXED_BYTE "header" is always non-zero. + * - assuming there is no zero byte in any non-zero field + * + * We undercompensate by + * - not counting the bytes used to store the lengths of the various arrays + * + * @param effect the TxEffect to calculate the DA gas used for + * @returns our interpretation of the DA gas used + */ +const getDAGasUsed = (effect: ITxEffectWithoutGasUsed) => { + const nonEmptyFields = + arrayNonEmptyLength(effect.noteHashes, Fr.isZero) + + arrayNonEmptyLength(effect.nullifiers, Fr.isZero) + + arrayNonEmptyLength(effect.l2ToL1Msgs, Fr.isZero) + + 2 * arrayNonEmptyLength(effect.publicDataWrites, PublicDataWrite.isEmpty); + + const gasUsed = + FIXED_DA_GAS + + DA_BYTE_GAS * + BigInt( + Fr.SIZE_IN_BYTES * nonEmptyFields + + effect.encryptedLogs.getSerializedLength() + + effect.unencryptedLogs.getSerializedLength(), + ); + + return new GasUsed(gasUsed); +}; + +const gasProfiler: GasProfiler = (effect: ITxEffectWithoutGasUsed) => { + return { + [GasType.DA]: getDAGasUsed(effect), + [GasType.COMPUTE]: getComputeGasUsed(effect), + }; +}; + +export const CalldataTxEffectFactory: TxEffectFactory = { + gasProfiler, + build(effect: ITxEffectWithoutGasUsed) { + const { [GasType.DA]: daGasUsed, [GasType.COMPUTE]: computeGasUsed } = this.gasProfiler(effect); + return new TxEffect( + daGasUsed, + computeGasUsed, + effect.revertCode, + effect.noteHashes, + effect.nullifiers, + effect.l2ToL1Msgs, + effect.publicDataWrites, + effect.encryptedLogs, + effect.unencryptedLogs, + ); + }, +}; diff --git a/yarn-project/circuit-types/src/index.ts b/yarn-project/circuit-types/src/index.ts index 31213c9cb9b..8bc35953cc4 100644 --- a/yarn-project/circuit-types/src/index.ts +++ b/yarn-project/circuit-types/src/index.ts @@ -1,23 +1,24 @@ +export { CompleteAddress, GrumpkinPrivateKey, PartialAddress, PublicKey } from '@aztec/circuits.js'; +export * from './auth_witness.js'; +export * from './aztec_node/rpc/index.js'; +export * from './body.js'; +export * from './calldata_tx_effect_factory.js'; export * from './function_call.js'; +export * from './interfaces/index.js'; export * from './keys/index.js'; -export * from './notes/index.js'; -export * from './messaging/index.js'; export * from './l2_block.js'; -export * from './body.js'; export * from './l2_block_context.js'; export * from './l2_block_downloader/index.js'; export * from './l2_block_source.js'; -export * from './tx_effect.js'; export * from './logs/index.js'; export * from './merkle_tree_id.js'; +export * from './messaging/index.js'; export * from './mocks.js'; +export * from './notes/index.js'; +export * from './packed_arguments.js'; export * from './public_data_write.js'; -export * from './simulation_error.js'; export * from './sibling_path/index.js'; +export * from './simulation_error.js'; export * from './tx/index.js'; +export * from './tx_effect.js'; export * from './tx_execution_request.js'; -export * from './packed_arguments.js'; -export * from './interfaces/index.js'; -export * from './auth_witness.js'; -export * from './aztec_node/rpc/index.js'; -export { CompleteAddress, PublicKey, PartialAddress, GrumpkinPrivateKey } from '@aztec/circuits.js'; diff --git a/yarn-project/circuit-types/src/tx/processed_tx.ts b/yarn-project/circuit-types/src/tx/processed_tx.ts index 9e321fb0cee..27541614f65 100644 --- a/yarn-project/circuit-types/src/tx/processed_tx.ts +++ b/yarn-project/circuit-types/src/tx/processed_tx.ts @@ -1,4 +1,12 @@ -import { PublicDataWrite, SimulationError, Tx, TxEffect, TxHash, TxL2Logs } from '@aztec/circuit-types'; +import { + CalldataTxEffectFactory, + PublicDataWrite, + SimulationError, + Tx, + TxEffect, + TxHash, + TxL2Logs, +} from '@aztec/circuit-types'; import { Fr, Header, @@ -173,22 +181,25 @@ export function makeEmptyProcessedTx(header: Header, chainId: Fr, version: Fr): }; } -export function toTxEffect(tx: ProcessedTx): TxEffect { - return new TxEffect( - tx.data.combinedData.revertCode, - tx.data.combinedData.newNoteHashes.map((c: SideEffect) => c.value) as Tuple, - tx.data.combinedData.newNullifiers.map((n: SideEffectLinkedToNoteHash) => n.value) as Tuple< +// TODO(@just-mitch) need to have gas used on `combinedData` +export function toTxEffect(tx: ProcessedTx, factory = CalldataTxEffectFactory): TxEffect { + return factory.build({ + revertCode: tx.data.combinedData.revertCode, + noteHashes: tx.data.combinedData.newNoteHashes.map((c: SideEffect) => c.value) as Tuple< Fr, - typeof MAX_NEW_NULLIFIERS_PER_TX + typeof MAX_NEW_NOTE_HASHES_PER_TX >, - tx.data.combinedData.newL2ToL1Msgs, - tx.data.combinedData.publicDataUpdateRequests.map(t => new PublicDataWrite(t.leafSlot, t.newValue)) as Tuple< - PublicDataWrite, - typeof MAX_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX + nullifiers: tx.data.combinedData.newNullifiers.map((n: SideEffectLinkedToNoteHash) => n.value) as Tuple< + Fr, + typeof MAX_NEW_NULLIFIERS_PER_TX >, - tx.encryptedLogs || new TxL2Logs([]), - tx.unencryptedLogs || new TxL2Logs([]), - ); + l2ToL1Msgs: tx.data.combinedData.newL2ToL1Msgs, + publicDataWrites: tx.data.combinedData.publicDataUpdateRequests.map( + t => new PublicDataWrite(t.leafSlot, t.newValue), + ) as Tuple, + encryptedLogs: tx.encryptedLogs || new TxL2Logs([]), + unencryptedLogs: tx.unencryptedLogs || new TxL2Logs([]), + }); } function validateProcessedTxLogs(tx: ProcessedTx): void { diff --git a/yarn-project/circuit-types/src/tx_effect.ts b/yarn-project/circuit-types/src/tx_effect.ts index c80d0cee28d..3487b10a6a7 100644 --- a/yarn-project/circuit-types/src/tx_effect.ts +++ b/yarn-project/circuit-types/src/tx_effect.ts @@ -1,6 +1,7 @@ import { LogType, PublicDataWrite, TxHash, TxL2Logs } from '@aztec/circuit-types'; import { Fr, + GasUsed, MAX_NEW_L2_TO_L1_MSGS_PER_TX, MAX_NEW_NOTE_HASHES_PER_TX, MAX_NEW_NULLIFIERS_PER_TX, @@ -20,33 +21,63 @@ import { import { inspect } from 'util'; -export class TxEffect { +export enum GasType { + DA = 'da', + COMPUTE = 'compute', +} +export interface ITxEffect { + daGasUsed: GasUsed; + computeGasUsed: GasUsed; + revertCode: RevertCode; + noteHashes: Tuple; + nullifiers: Tuple; + l2ToL1Msgs: Tuple; + publicDataWrites: Tuple; + encryptedLogs: TxL2Logs; + unencryptedLogs: TxL2Logs; +} +export type ITxEffectWithoutGasUsed = Omit; +export type GasProfiler = (effect: ITxEffectWithoutGasUsed) => Record; +export interface TxEffectFactory { + readonly gasProfiler: GasProfiler; + build: (effect: ITxEffectWithoutGasUsed) => TxEffect; +} + +export class TxEffect implements ITxEffect { constructor( + /** + * The amount of da gas used during the transaction. + */ + public readonly daGasUsed: GasUsed, + /** + * The amount of compute gas used during the transaction. + */ + public readonly computeGasUsed: GasUsed, /** * Whether the transaction reverted during public app logic. */ - public revertCode: RevertCode, + public readonly revertCode: RevertCode, /** * The note hashes to be inserted into the note hash tree. */ - public noteHashes: Tuple, + public readonly noteHashes: Tuple, /** * The nullifiers to be inserted into the nullifier tree. */ - public nullifiers: Tuple, + public readonly nullifiers: Tuple, /** * The L2 to L1 messages to be inserted into the messagebox on L1. */ - public l2ToL1Msgs: Tuple, + public readonly l2ToL1Msgs: Tuple, /** * The public data writes to be inserted into the public data tree. */ - public publicDataWrites: Tuple, + public readonly publicDataWrites: Tuple, /** * The logs of the txEffect */ - public encryptedLogs: TxL2Logs, - public unencryptedLogs: TxL2Logs, + public readonly encryptedLogs: TxL2Logs, + public readonly unencryptedLogs: TxL2Logs, ) {} toBuffer(): Buffer { @@ -56,6 +87,8 @@ export class TxEffect { const nonZeroPublicDataWrites = this.publicDataWrites.filter(h => !h.isEmpty()); return Buffer.concat([ + this.daGasUsed.toBuffer(), + this.computeGasUsed.toBuffer(), this.revertCode.toBuffer(), serializeArrayOfBufferableToVector(nonZeroNoteHashes, 1), serializeArrayOfBufferableToVector(nonZeroNullifiers, 1), @@ -74,6 +107,8 @@ export class TxEffect { static fromBuffer(buffer: Buffer | BufferReader): TxEffect { const reader = BufferReader.asReader(buffer); + const daGasUsed = GasUsed.fromBuffer(reader); + const computeGasUsed = GasUsed.fromBuffer(reader); const revertCode = RevertCode.fromBuffer(reader); const nonZeroNoteHashes = reader.readVectorUint8Prefix(Fr); const nonZeroNullifiers = reader.readVectorUint8Prefix(Fr); @@ -81,6 +116,8 @@ export class TxEffect { const nonZeroPublicDataWrites = reader.readVectorUint8Prefix(PublicDataWrite); return new TxEffect( + daGasUsed, + computeGasUsed, revertCode, padArrayEnd(nonZeroNoteHashes, Fr.ZERO, MAX_NEW_NOTE_HASHES_PER_TX), padArrayEnd(nonZeroNullifiers, Fr.ZERO, MAX_NEW_NULLIFIERS_PER_TX), @@ -113,6 +150,8 @@ export class TxEffect { const unencryptedLogsHashKernel0 = this.unencryptedLogs.hash(); const inputValue = Buffer.concat([ + this.daGasUsed.toHashPreimage(), + this.computeGasUsed.toHashPreimage(), this.revertCode.toHashPreimage(), noteHashesBuffer, nullifiersBuffer, @@ -132,6 +171,8 @@ export class TxEffect { numUnencryptedLogsPerCall = 1, ): TxEffect { return new TxEffect( + GasUsed.random(), + GasUsed.random(), RevertCode.random(), makeTuple(MAX_NEW_NOTE_HASHES_PER_TX, Fr.random), makeTuple(MAX_NEW_NULLIFIERS_PER_TX, Fr.random), @@ -165,7 +206,9 @@ export class TxEffect { // print out the non-empty fields return `TxEffect { - revertCode: ${this.revertCode}, + daGasUsed: ${inspect(this.daGasUsed)}, + computeGasUsed: ${inspect(this.computeGasUsed)}, + revertCode: ${inspect(this.revertCode)}, note hashes: [${this.noteHashes.map(h => h.toString()).join(', ')}], nullifiers: [${this.nullifiers.map(h => h.toString()).join(', ')}], l2ToL1Msgs: [${this.l2ToL1Msgs.map(h => h.toString()).join(', ')}], diff --git a/yarn-project/circuits.js/src/structs/__snapshots__/gas_used.test.ts.snap b/yarn-project/circuits.js/src/structs/__snapshots__/gas_used.test.ts.snap new file mode 100644 index 00000000000..b932c7782cc --- /dev/null +++ b/yarn-project/circuits.js/src/structs/__snapshots__/gas_used.test.ts.snap @@ -0,0 +1,393 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`gas_used accepts valid values 1`] = ` +{ + "data": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + "type": "Buffer", +} +`; + +exports[`gas_used accepts valid values 2`] = ` +{ + "data": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + "type": "Buffer", +} +`; + +exports[`gas_used accepts valid values 3`] = ` +{ + "data": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + ], + "type": "Buffer", +} +`; + +exports[`gas_used accepts valid values 4`] = ` +{ + "data": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + ], + "type": "Buffer", +} +`; + +exports[`gas_used accepts valid values 5`] = ` +{ + "data": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 2, + ], + "type": "Buffer", +} +`; + +exports[`gas_used accepts valid values 6`] = ` +{ + "data": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 2, + ], + "type": "Buffer", +} +`; + +exports[`gas_used accepts valid values 7`] = ` +{ + "data": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 3, + ], + "type": "Buffer", +} +`; + +exports[`gas_used accepts valid values 8`] = ` +{ + "data": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 3, + ], + "type": "Buffer", +} +`; + +exports[`gas_used accepts valid values 9`] = ` +{ + "data": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 42, + ], + "type": "Buffer", +} +`; + +exports[`gas_used accepts valid values 10`] = ` +{ + "data": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 42, + ], + "type": "Buffer", +} +`; + +exports[`gas_used accepts valid values 11`] = ` +{ + "data": [ + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + ], + "type": "Buffer", +} +`; + +exports[`gas_used accepts valid values 12`] = ` +{ + "data": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + ], + "type": "Buffer", +} +`; + +exports[`gas_used accepts valid values 13`] = ` +{ + "data": [ + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + ], + "type": "Buffer", +} +`; + +exports[`gas_used accepts valid values 14`] = ` +{ + "data": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + ], + "type": "Buffer", +} +`; diff --git a/yarn-project/circuits.js/src/structs/gas_used.test.ts b/yarn-project/circuits.js/src/structs/gas_used.test.ts new file mode 100644 index 00000000000..4b970277040 --- /dev/null +++ b/yarn-project/circuits.js/src/structs/gas_used.test.ts @@ -0,0 +1,38 @@ +import { GasUsed } from './gas_used.js'; + +describe('gas_used', () => { + it.each([0n, 1n, 2n, 3n, 42n, 2n ** 32n, 2n ** 64n - 1n])('accepts valid values', value => { + const gasUsed = new GasUsed(value); + expect(gasUsed.value).toEqual(BigInt(value)); + + const b = gasUsed.toBuffer(); + expect(b).toMatchSnapshot(); + expect(GasUsed.fromBuffer(b)).toEqual(gasUsed); + + expect(gasUsed.toHashPreimage()).toMatchSnapshot(); + }); + + it.each([-1n, -2n, -42n])('rejects negative values', value => { + expect(() => new GasUsed(value)).toThrow(); + }); + + it('rejects too large values', () => { + expect(() => new GasUsed(2n ** 64n)).toThrow(); + }); + + it('serde retains value', () => { + const a = GasUsed.fromBuffer(new GasUsed(42n).toBuffer()); + const b = GasUsed.fromBuffer(new GasUsed(43n).toBuffer()); + + expect(a.value).toEqual(42n); + expect(b.value).toEqual(43n); + + expect(a.equals(a)).toBeTruthy(); + expect(a.equals(b)).toBeFalsy(); + }); + + it('random', () => { + const g = GasUsed.random(); + expect(g.value).toBeLessThanOrEqual(GasUsed.MAX_VALUE); + }); +}); diff --git a/yarn-project/circuits.js/src/structs/gas_used.ts b/yarn-project/circuits.js/src/structs/gas_used.ts new file mode 100644 index 00000000000..ded2941f2f7 --- /dev/null +++ b/yarn-project/circuits.js/src/structs/gas_used.ts @@ -0,0 +1,62 @@ +import { BufferReader } from '@aztec/foundation/serialize'; + +import { inspect } from 'util'; + +export class GasUsed { + public static readonly PACKED_SIZE_IN_BYTES = 8n; + public static readonly MAX_VALUE = 2n ** (GasUsed.PACKED_SIZE_IN_BYTES * 8n) - 1n; + private static readonly PREIMAGE_SIZE_IN_BYTES = 32n; + + public readonly value: bigint; + + constructor(gas: bigint) { + if (gas < 0) { + throw new Error('Gas used cannot be negative'); + } else if (gas > GasUsed.MAX_VALUE) { + throw new Error(`Gas used is too large: [${gas}] does not fit in a ${GasUsed.PACKED_SIZE_IN_BYTES} byte field`); + } + + this.value = BigInt(gas); + } + + public toHashPreimage(): Buffer { + const padding = Buffer.alloc(Number(GasUsed.PREIMAGE_SIZE_IN_BYTES - GasUsed.PACKED_SIZE_IN_BYTES)); + return Buffer.concat([padding, this.toBuffer()]); + } + + public toBuffer(): Buffer { + const b = Buffer.alloc(Number(GasUsed.PACKED_SIZE_IN_BYTES)); + b.writeBigUInt64BE(this.value); + return b; + } + + public equals(other: GasUsed): boolean { + return this.value === other.value; + } + + static empty(): GasUsed { + return new GasUsed(0n); + } + + static fromBuffer(buffer: Buffer | BufferReader): GasUsed { + const reader = BufferReader.asReader(buffer); + const gas = reader.readBytes(Number(GasUsed.PACKED_SIZE_IN_BYTES)).readBigUInt64BE(); + return new GasUsed(gas); + } + + /** + * + * @returns A barely random instance of GasUsed. Not suitable for cryptographic use. + */ + static random(): GasUsed { + let g = GasUsed.MAX_VALUE + 1n; + while (g > GasUsed.MAX_VALUE) { + g = BigInt(Math.floor(Math.random() * Number(GasUsed.MAX_VALUE))); + } + return new GasUsed(g); + } + + [inspect.custom]() { + return `GasUsed<${this.value}>`; + } +} diff --git a/yarn-project/circuits.js/src/structs/index.ts b/yarn-project/circuits.js/src/structs/index.ts index 02984792090..9f69f5fc324 100644 --- a/yarn-project/circuits.js/src/structs/index.ts +++ b/yarn-project/circuits.js/src/structs/index.ts @@ -7,6 +7,7 @@ export * from './content_commitment.js'; export * from './contract_storage_read.js'; export * from './contract_storage_update_request.js'; export * from './function_data.js'; +export * from './gas_used.js'; export * from './global_variables.js'; export * from './header.js'; export * from './kernel/combined_accumulated_data.js'; diff --git a/yarn-project/circuits.js/src/structs/kernel/private_kernel_tail_circuit_private_inputs.ts b/yarn-project/circuits.js/src/structs/kernel/private_kernel_tail_circuit_private_inputs.ts index 2f90462d3bd..556a2c0ec57 100644 --- a/yarn-project/circuits.js/src/structs/kernel/private_kernel_tail_circuit_private_inputs.ts +++ b/yarn-project/circuits.js/src/structs/kernel/private_kernel_tail_circuit_private_inputs.ts @@ -1,3 +1,4 @@ +import { Fr, GrumpkinScalar } from '@aztec/foundation/fields'; import { BufferReader, Tuple, serializeToBuffer } from '@aztec/foundation/serialize'; import { @@ -6,8 +7,7 @@ import { MAX_NOTE_HASH_READ_REQUESTS_PER_TX, MAX_NULLIFIER_KEY_VALIDATION_REQUESTS_PER_TX, } from '../../constants.gen.js'; -import { GrumpkinPrivateKey } from '../../index.js'; -import { Fr, GrumpkinScalar } from '../index.js'; +import { GrumpkinPrivateKey } from '../../types/grumpkin_private_key.js'; import { NullifierReadRequestHints, nullifierReadRequestHintsFromBuffer } from '../read_request_hints.js'; import { SideEffect, SideEffectLinkedToNoteHash } from '../side_effects.js'; import { PrivateKernelInnerData } from './private_kernel_inner_data.js'; diff --git a/yarn-project/circuits.js/src/structs/revert_code.ts b/yarn-project/circuits.js/src/structs/revert_code.ts index f88cd1f0c53..38f1b285980 100644 --- a/yarn-project/circuits.js/src/structs/revert_code.ts +++ b/yarn-project/circuits.js/src/structs/revert_code.ts @@ -16,6 +16,9 @@ function isRevertCodeEnum(value: number): value is RevertCodeEnum { * Wrapper class over a field to safely represent a revert code. */ export class RevertCode { + public static readonly PACKED_SIZE_IN_BYTES = 1n; + private static readonly PREIMAGE_SIZE_IN_BYTES = 32n; + private code: number; private constructor(e: RevertCodeEnum) { this.code = e.valueOf(); @@ -38,15 +41,13 @@ export class RevertCode { * from serialization for transmitting the data. */ - private static readonly PREIMAGE_SIZE_IN_BYTES = 32; public toHashPreimage(): Buffer { - const padding = Buffer.alloc(RevertCode.PREIMAGE_SIZE_IN_BYTES - RevertCode.PACKED_SIZE_IN_BYTES); + const padding = Buffer.alloc(Number(RevertCode.PREIMAGE_SIZE_IN_BYTES - RevertCode.PACKED_SIZE_IN_BYTES)); return Buffer.concat([padding, this.toBuffer()]); } - private static readonly PACKED_SIZE_IN_BYTES = 1; public toBuffer(): Buffer { - const b = Buffer.alloc(RevertCode.PACKED_SIZE_IN_BYTES); + const b = Buffer.alloc(Number(RevertCode.PACKED_SIZE_IN_BYTES)); b.writeUInt8(this.code, 0); return b; } @@ -73,7 +74,7 @@ export class RevertCode { public static fromBuffer(buffer: Buffer | BufferReader): RevertCode { const reader = BufferReader.asReader(buffer); - const code = reader.readBytes(RevertCode.PACKED_SIZE_IN_BYTES).readUInt8(0); + const code = reader.readBytes(Number(RevertCode.PACKED_SIZE_IN_BYTES)).readUInt8(0); if (!isRevertCodeEnum(code)) { throw new Error(`Invalid RevertCode: ${code}`); } diff --git a/yarn-project/noir-protocol-circuits-types/src/type_conversion.ts b/yarn-project/noir-protocol-circuits-types/src/type_conversion.ts index 62622e993a0..38683872370 100644 --- a/yarn-project/noir-protocol-circuits-types/src/type_conversion.ts +++ b/yarn-project/noir-protocol-circuits-types/src/type_conversion.ts @@ -20,6 +20,7 @@ import { Fr, FunctionData, FunctionSelector, + GasUsed, GlobalVariables, GrumpkinPrivateKey, GrumpkinScalar, @@ -790,6 +791,14 @@ export function mapRevertCodeToNoir(revertCode: RevertCode): NoirField { return mapFieldToNoir(revertCode.toField()); } +export function mapGasUsedFromNoir(gasUsed: NoirField): GasUsed { + return new GasUsed(BigInt(mapNumberFromNoir(gasUsed))); +} + +export function mapGasUsedToNoir(gasUsed: GasUsed): NoirField { + return mapFieldToNoir(new Fr(gasUsed.value)); +} + /** * Maps an array from noir types to a tuple of parsed types. * @param noirArray - The noir array. @@ -1127,6 +1136,9 @@ export function mapCombinedAccumulatedDataToNoir( combinedAccumulatedData: CombinedAccumulatedData, ): CombinedAccumulatedDataNoir { return { + // TODO(@just-mitch): fill in gas used + da_gas_used: mapGasUsedToNoir(GasUsed.empty()), + compute_gas_used: mapGasUsedToNoir(GasUsed.empty()), revert_code: mapRevertCodeToNoir(combinedAccumulatedData.revertCode), new_note_hashes: mapTuple(combinedAccumulatedData.newNoteHashes, mapSideEffectToNoir), new_nullifiers: mapTuple(combinedAccumulatedData.newNullifiers, mapSideEffectLinkedToNoir), diff --git a/yarn-project/pxe/src/note_processor/note_processor.test.ts b/yarn-project/pxe/src/note_processor/note_processor.test.ts index e7de9c66bf8..23b4081fa7a 100644 --- a/yarn-project/pxe/src/note_processor/note_processor.test.ts +++ b/yarn-project/pxe/src/note_processor/note_processor.test.ts @@ -9,6 +9,7 @@ import { L2BlockL2Logs, Note, TaggedNote, + TxEffect, TxL2Logs, } from '@aztec/circuit-types'; import { Fr, INITIAL_L2_BLOCK_NUM, MAX_NEW_NOTE_HASHES_PER_TX } from '@aztec/circuits.js'; @@ -107,16 +108,38 @@ describe('Note Processor', () => { } = createEncryptedLogsAndOwnedL1NotePayloads(isTargetBlock ? ownedData : [], isTargetBlock ? ownedNotes : []); encryptedLogsArr.push(encryptedLogs); ownedL1NotePayloads.push(...payloads); + const noteHashesList: Tuple[] = []; for (let i = 0; i < TXS_PER_BLOCK; i++) { - block.body.txEffects[i].noteHashes = newNotes + const noteHashes = newNotes .map(n => computeMockNoteHash(n.notePayload.note)) .slice(i * MAX_NEW_NOTE_HASHES_PER_TX, (i + 1) * MAX_NEW_NOTE_HASHES_PER_TX) as Tuple< Fr, typeof MAX_NEW_NOTE_HASHES_PER_TX >; + noteHashesList.push(noteHashes); } + // Reconstruct TxEffects because their fields are readonly + const txEffects: TxEffect[] = noteHashesList.map((noteHashes, index) => { + // Create a new TxEffect instance with the correct noteHashes + const existingTxEffect = block.body.txEffects[index]; + return new TxEffect( + existingTxEffect.daGasUsed, + existingTxEffect.computeGasUsed, + existingTxEffect.revertCode, + noteHashes, + existingTxEffect.nullifiers, + existingTxEffect.l2ToL1Msgs, + existingTxEffect.publicDataWrites, + existingTxEffect.encryptedLogs, + existingTxEffect.unencryptedLogs, + ); + }); + + block.body.txEffects = txEffects; + const randomBlockContext = new L2BlockContext(block); + blockContexts.push(randomBlockContext); } return { blockContexts, encryptedLogsArr, ownedL1NotePayloads };