import { CONTRACT_CLASS_PUBLISHED_MAGIC_VALUE, CONTRACT_CLASS_REGISTRY_CONTRACT_ADDRESS, CONTRACT_INSTANCE_REGISTRY_CONTRACT_ADDRESS, MAX_PROCESSABLE_DA_GAS_PER_CHECKPOINT, MAX_PROCESSABLE_L2_GAS, PRIVATE_LOG_SIZE_IN_FIELDS, } from '@aztec/constants'; import { padArrayEnd } from '@aztec/foundation/collection'; import { Fr } from '@aztec/foundation/curves/bn254'; import { CONTRACT_INSTANCE_PUBLISHED_EVENT_TAG } from '@aztec/protocol-contracts'; import { bufferAsFields } from '@aztec/stdlib/abi'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; import type { ContractClassPublic, ContractInstanceWithAddress } from '@aztec/stdlib/contract'; import { Gas, GasFees, GasSettings } from '@aztec/stdlib/gas'; import { siloNullifier } from '@aztec/stdlib/hash'; import { LogHash, PartialPrivateTailPublicInputsForPublic, PartialPrivateTailPublicInputsForRollup, PrivateKernelTailCircuitPublicInputs, countAccumulatedItems, } from '@aztec/stdlib/kernel'; import { ContractClassLogFields, PrivateLog } from '@aztec/stdlib/logs'; import type { ScopedL2ToL1Message } from '@aztec/stdlib/messaging'; import { ChonkProof } from '@aztec/stdlib/proofs'; import { BlockHeader, GlobalVariables, HashedValues, PublicCallRequestWithCalldata, Tx, TxConstantData, TxContext, } from '@aztec/stdlib/tx'; import { strict as assert } from 'assert'; const TEARDOWN_DA_GAS_LIMIT = 98_304; const TEARDOWN_L2_GAS_LIMIT = 817_500; export type TestPrivateInsertions = { revertible?: { nullifiers?: Fr[]; noteHashes?: Fr[]; l2ToL1Msgs?: ScopedL2ToL1Message[]; }; nonRevertible?: { nullifiers?: Fr[]; noteHashes?: Fr[]; l2ToL1Msgs?: ScopedL2ToL1Message[]; }; }; /** * Craft a carrier transaction for some public calls for simulation by PublicTxSimulator. */ export async function createTxForPublicCalls( privateInsertions: TestPrivateInsertions, setupCallRequests: PublicCallRequestWithCalldata[], appCallRequests: PublicCallRequestWithCalldata[], teardownCallRequest?: PublicCallRequestWithCalldata, feePayer = AztecAddress.zero(), gasUsedByPrivate: Gas = Gas.empty(), globals: GlobalVariables = GlobalVariables.empty(), gasLimits?: Gas, ): Promise { assert( setupCallRequests.length > 0 || appCallRequests.length > 0 || teardownCallRequest !== undefined, "Can't create public tx with no enqueued calls", ); // use max limits gasLimits = gasLimits ?? new Gas(MAX_PROCESSABLE_DA_GAS_PER_CHECKPOINT, MAX_PROCESSABLE_L2_GAS); const forPublic = PartialPrivateTailPublicInputsForPublic.empty(); // Non revertible private insertions if (!privateInsertions.nonRevertible?.nullifiers?.length) { throw new Error('At least one non-revertible nullifier is required'); } for (let i = 0; i < privateInsertions.nonRevertible.nullifiers.length; i++) { assert(i < forPublic.nonRevertibleAccumulatedData.nullifiers.length, 'Nullifier index out of bounds'); forPublic.nonRevertibleAccumulatedData.nullifiers[i] = privateInsertions.nonRevertible.nullifiers[i]; } if (privateInsertions.nonRevertible.noteHashes) { for (let i = 0; i < privateInsertions.nonRevertible.noteHashes.length; i++) { assert(i < forPublic.nonRevertibleAccumulatedData.noteHashes.length, 'Note hash index out of bounds'); forPublic.nonRevertibleAccumulatedData.noteHashes[i] = privateInsertions.nonRevertible.noteHashes[i]; } } if (privateInsertions.nonRevertible.l2ToL1Msgs) { for (let i = 0; i < privateInsertions.nonRevertible.l2ToL1Msgs.length; i++) { assert(i < forPublic.nonRevertibleAccumulatedData.l2ToL1Msgs.length, 'L2 to L1 message index out of bounds'); forPublic.nonRevertibleAccumulatedData.l2ToL1Msgs[i] = privateInsertions.nonRevertible.l2ToL1Msgs[i]; } } // Revertible private insertions if (privateInsertions.revertible) { if (privateInsertions.revertible.noteHashes) { for (let i = 0; i < privateInsertions.revertible.noteHashes.length; i++) { assert(i < forPublic.revertibleAccumulatedData.noteHashes.length, 'Note hash index out of bounds'); forPublic.revertibleAccumulatedData.noteHashes[i] = privateInsertions.revertible.noteHashes[i]; } } if (privateInsertions.revertible.nullifiers) { for (let i = 0; i < privateInsertions.revertible.nullifiers.length; i++) { assert(i < forPublic.revertibleAccumulatedData.nullifiers.length, 'Nullifier index out of bounds'); forPublic.revertibleAccumulatedData.nullifiers[i] = privateInsertions.revertible.nullifiers[i]; } } if (privateInsertions.revertible.l2ToL1Msgs) { for (let i = 0; i < privateInsertions.revertible.l2ToL1Msgs.length; i++) { assert(i < forPublic.revertibleAccumulatedData.l2ToL1Msgs.length, 'L2 to L1 message index out of bounds'); forPublic.revertibleAccumulatedData.l2ToL1Msgs[i] = privateInsertions.revertible.l2ToL1Msgs[i]; } } } for (let i = 0; i < setupCallRequests.length; i++) { forPublic.nonRevertibleAccumulatedData.publicCallRequests[i] = setupCallRequests[i].request; } for (let i = 0; i < appCallRequests.length; i++) { forPublic.revertibleAccumulatedData.publicCallRequests[i] = appCallRequests[i].request; } if (teardownCallRequest) { forPublic.publicTeardownCallRequest = teardownCallRequest.request; } const maxFeesPerGas = feePayer.isZero() ? GasFees.empty() : new GasFees(10, 10); const teardownGasLimits = teardownCallRequest ? new Gas(TEARDOWN_DA_GAS_LIMIT, TEARDOWN_L2_GAS_LIMIT) : Gas.empty(); const gasSettings = new GasSettings(gasLimits, teardownGasLimits, maxFeesPerGas, GasFees.empty()); const txContext = new TxContext(Fr.zero(), Fr.zero(), gasSettings); const header = BlockHeader.empty({ globalVariables: globals }); const constantData = new TxConstantData(header, txContext, Fr.zero(), Fr.zero()); const expirationTimestamp = 0n; // Not used in the simulator. const txData = new PrivateKernelTailCircuitPublicInputs( constantData, /*gasUsed=*/ gasUsedByPrivate, feePayer, expirationTimestamp, forPublic, ); const calldata = [ ...setupCallRequests, ...appCallRequests, ...(teardownCallRequest ? [teardownCallRequest] : []), ].map(r => new HashedValues(r.calldata, r.request.calldataHash)); return await Tx.create({ data: txData, chonkProof: ChonkProof.random(), contractClassLogFields: [], publicFunctionCalldata: calldata, }); } export async function createTxForPrivateOnly( feePayer = AztecAddress.zero(), gasUsedByPrivate: Gas = new Gas(10, 10), ): Promise { // use max limits const gasLimits = new Gas(MAX_PROCESSABLE_DA_GAS_PER_CHECKPOINT, MAX_PROCESSABLE_L2_GAS); const forRollup = PartialPrivateTailPublicInputsForRollup.empty(); const maxFeesPerGas = feePayer.isZero() ? GasFees.empty() : new GasFees(10, 10); const gasSettings = new GasSettings(gasLimits, Gas.empty(), maxFeesPerGas, GasFees.empty()); const txContext = new TxContext(Fr.zero(), Fr.zero(), gasSettings); const constantData = new TxConstantData(BlockHeader.empty(), txContext, Fr.zero(), Fr.zero()); const expirationTimestamp = 0n; // Not used in the simulator. const txData = new PrivateKernelTailCircuitPublicInputs( constantData, /*gasUsed=*/ gasUsedByPrivate, feePayer, expirationTimestamp, /*forPublic=*/ undefined, forRollup, ); return await Tx.create({ data: txData, chonkProof: ChonkProof.empty(), contractClassLogFields: [], publicFunctionCalldata: [], }); } export async function addNewContractClassToTx( tx: Tx, contractClass: ContractClassPublic, skipNullifierInsertion = false, ) { const contractClassLogFields = [ new Fr(CONTRACT_CLASS_PUBLISHED_MAGIC_VALUE), contractClass.id, new Fr(contractClass.version), new Fr(contractClass.artifactHash), new Fr(contractClass.privateFunctionsRoot), ...bufferAsFields(contractClass.packedBytecode, Math.ceil(contractClass.packedBytecode.length / 31) + 1), ]; const contractAddress = new AztecAddress(new Fr(CONTRACT_CLASS_REGISTRY_CONTRACT_ADDRESS)); const emittedLength = contractClassLogFields.length; const logFields = ContractClassLogFields.fromEmittedFields(contractClassLogFields); const contractClassLogHash = LogHash.from({ value: await logFields.hash(), length: emittedLength, }).scope(contractAddress); const accumulatedData = tx.data.forPublic ? tx.data.forPublic!.revertibleAccumulatedData : tx.data.forRollup!.end; if (!skipNullifierInsertion) { const nextNullifierIndex = countAccumulatedItems(accumulatedData.nullifiers); accumulatedData.nullifiers[nextNullifierIndex] = contractClass.id; } const nextLogIndex = countAccumulatedItems(accumulatedData.contractClassLogsHashes); accumulatedData.contractClassLogsHashes[nextLogIndex] = contractClassLogHash; tx.contractClassLogFields.push(logFields); } export async function addNewContractInstanceToTx( tx: Tx, contractInstance: ContractInstanceWithAddress, skipNullifierInsertion = false, ) { // Only ivpk_m is broadcast as a point (x, y); the other five keys are hashes. const publicKeysAsFields = [ contractInstance.publicKeys.npkMHash, contractInstance.publicKeys.ivpkM.x, contractInstance.publicKeys.ivpkM.y, contractInstance.publicKeys.ovpkMHash, contractInstance.publicKeys.tpkMHash, contractInstance.publicKeys.mspkMHash, contractInstance.publicKeys.fbpkMHash, ]; const logFields = [ CONTRACT_INSTANCE_PUBLISHED_EVENT_TAG, contractInstance.address.toField(), new Fr(contractInstance.version), new Fr(contractInstance.salt), contractInstance.currentContractClassId, contractInstance.initializationHash, contractInstance.immutablesHash, ...publicKeysAsFields, contractInstance.deployer.toField(), ]; const contractInstanceLog = new PrivateLog( padArrayEnd(logFields, Fr.ZERO, PRIVATE_LOG_SIZE_IN_FIELDS), logFields.length, ); const contractAddressNullifier = await siloNullifier( AztecAddress.fromNumberUnsafe(CONTRACT_INSTANCE_REGISTRY_CONTRACT_ADDRESS), contractInstance.address.toField(), ); const accumulatedData = tx.data.forPublic ? tx.data.forPublic!.revertibleAccumulatedData : tx.data.forRollup!.end; if (!skipNullifierInsertion) { const nextNullifierIndex = countAccumulatedItems(accumulatedData.nullifiers); accumulatedData.nullifiers[nextNullifierIndex] = contractAddressNullifier; } const nextLogIndex = countAccumulatedItems(accumulatedData.privateLogs); accumulatedData.privateLogs[nextLogIndex] = contractInstanceLog; }