From 38595f797d5b0aed1b2097ef06ab68967287e37f Mon Sep 17 00:00:00 2001 From: dbanks12 Date: Sat, 30 Aug 2025 18:23:28 +0000 Subject: [PATCH] feat: avm ts simulator accumulates debuglogs into ProcessedTx --- .../src/public/avm/opcodes/misc.test.ts | 12 +++- .../simulator/src/public/avm/opcodes/misc.ts | 19 ++++--- .../public_processor/public_processor.ts | 11 +++- .../public_tx_simulator/public_tx_context.ts | 7 +++ .../public_tx_simulator.ts | 4 ++ .../src/public/side_effect_trace.test.ts | 12 +++- .../simulator/src/public/side_effect_trace.ts | 14 ++++- .../src/public/side_effect_trace_interface.ts | 1 + .../src/public/state_manager/state_manager.ts | 9 +++ yarn-project/stdlib/src/logs/index.ts | 1 + .../stdlib/src/logs/public_debugged_log.ts | 55 +++++++++++++++++++ yarn-project/stdlib/src/tests/factories.ts | 3 +- yarn-project/stdlib/src/tx/processed_tx.ts | 8 +++ 13 files changed, 142 insertions(+), 14 deletions(-) create mode 100644 yarn-project/stdlib/src/logs/public_debugged_log.ts diff --git a/yarn-project/simulator/src/public/avm/opcodes/misc.test.ts b/yarn-project/simulator/src/public/avm/opcodes/misc.test.ts index b3b6eadfa2c1..269b313677a5 100644 --- a/yarn-project/simulator/src/public/avm/opcodes/misc.test.ts +++ b/yarn-project/simulator/src/public/avm/opcodes/misc.test.ts @@ -1,7 +1,9 @@ import { jest } from '@jest/globals'; +import { mock } from 'jest-mock-extended'; +import type { PublicSideEffectTraceInterface } from '../../side_effect_trace_interface.js'; import { Field, Uint8, Uint32 } from '../avm_memory_types.js'; -import { initContext, initExecutionEnvironment } from '../fixtures/initializers.js'; +import { initContext, initExecutionEnvironment, initPersistableStateManager } from '../fixtures/initializers.js'; import { Opcode } from '../serialization/instruction_serialization.js'; import { DebugLog } from './misc.js'; @@ -30,7 +32,9 @@ describe('Misc Instructions', () => { it('Should execute DebugLog in client-initiated simulation mode', async () => { const env = initExecutionEnvironment({ clientInitiatedSimulation: true }); - const context = initContext({ env }); + const trace = mock(); + const persistableState = initPersistableStateManager({ trace }); + const context = initContext({ env, persistableState }); // Set up memory with message and fields const messageOffset = 10; const fieldsOffset = 100; @@ -64,6 +68,10 @@ describe('Misc Instructions', () => { // Check that logger.verbose was called with formatted message expect(mockVerbose).toHaveBeenCalledWith(`Hello ${fieldValue.toFr()}!`); + + // expect debug log to be traced + const msgId = 0; // TODO(dbanks12): implement messageId + expect(trace.traceDebugLog).toHaveBeenCalledWith(msgId, [fieldValue.toFr()]); } finally { // Restore the mock mockIsVerbose.mockRestore(); diff --git a/yarn-project/simulator/src/public/avm/opcodes/misc.ts b/yarn-project/simulator/src/public/avm/opcodes/misc.ts index a4fb9ce34b9f..e6e756569c5c 100644 --- a/yarn-project/simulator/src/public/avm/opcodes/misc.ts +++ b/yarn-project/simulator/src/public/avm/opcodes/misc.ts @@ -55,14 +55,19 @@ export class DebugLog extends Instruction { memory.checkTagsRange(TypeTag.UINT8, messageOffset, this.messageSize); memory.checkTagsRange(TypeTag.FIELD, fieldsOffset, fieldsSize); - // Interpret str = [u8; N] to string. - const messageAsStr = rawMessage.map(field => String.fromCharCode(field.toNumber())).join(''); - const formattedStr = applyStringFormatting( - messageAsStr, - fields.map(field => field.toFr()), - ); + // Convert fields to Fr array for the side effect trace + const fieldsAsFr = fields.map(field => field.toFr()); - DebugLog.logger.verbose(formattedStr); + const messageId = 0; // TODO(dbanks12): implement messageId as operand + context.persistableState.writeDebugLog(messageId, fieldsAsFr); + + if (DebugLog.logger.isLevelEnabled('verbose')) { + // Interpret str = [u8; N] to string. + const messageAsStr = rawMessage.map(field => String.fromCharCode(field.toNumber())).join(''); + const formattedStr = applyStringFormatting(messageAsStr, fieldsAsFr); + + DebugLog.logger.verbose(formattedStr); + } } } } diff --git a/yarn-project/simulator/src/public/public_processor/public_processor.ts b/yarn-project/simulator/src/public/public_processor/public_processor.ts index 6173a8183b1b..eb6bc2a8663e 100644 --- a/yarn-project/simulator/src/public/public_processor/public_processor.ts +++ b/yarn-project/simulator/src/public/public_processor/public_processor.ts @@ -513,7 +513,7 @@ export class PublicProcessor implements Traceable { private async processTxWithPublicCalls(tx: Tx): Promise<[ProcessedTx, NestedProcessReturnValues[]]> { const timer = new Timer(); - const { avmProvingRequest, gasUsed, revertCode, revertReason, processedPhases } = + const { avmProvingRequest, gasUsed, revertCode, revertReason, processedPhases, publicDebuggedLogs } = await this.publicTxSimulator.simulate(tx); if (!avmProvingRequest) { @@ -542,7 +542,14 @@ export class PublicProcessor implements Traceable { const durationMs = timer.ms(); this.metrics.recordTx(phaseCount, durationMs, gasUsed.publicGas); - const processedTx = makeProcessedTxFromTxWithPublicCalls(tx, avmProvingRequest, gasUsed, revertCode, revertReason); + const processedTx = makeProcessedTxFromTxWithPublicCalls( + tx, + avmProvingRequest, + gasUsed, + revertCode, + revertReason, + publicDebuggedLogs, + ); const returnValues = processedPhases.find(({ phase }) => phase === TxExecutionPhase.APP_LOGIC)?.returnValues ?? []; diff --git a/yarn-project/simulator/src/public/public_tx_simulator/public_tx_context.ts b/yarn-project/simulator/src/public/public_tx_simulator/public_tx_context.ts index 481107912f2f..c1347e6829b1 100644 --- a/yarn-project/simulator/src/public/public_tx_simulator/public_tx_context.ts +++ b/yarn-project/simulator/src/public/public_tx_simulator/public_tx_context.ts @@ -289,6 +289,13 @@ export class PublicTxContext { return txFee; } + /** + * Get the public debugged logs accumulated during this transaction. + */ + public getDebugLogs() { + return this.trace.getSideEffects().publicDebuggedLogs; + } + /** * Generate the public inputs for the AVM circuit. */ diff --git a/yarn-project/simulator/src/public/public_tx_simulator/public_tx_simulator.ts b/yarn-project/simulator/src/public/public_tx_simulator/public_tx_simulator.ts index a677a38e45e3..edfec8e10d04 100644 --- a/yarn-project/simulator/src/public/public_tx_simulator/public_tx_simulator.ts +++ b/yarn-project/simulator/src/public/public_tx_simulator/public_tx_simulator.ts @@ -13,6 +13,7 @@ import { } from '@aztec/stdlib/avm'; import { SimulationError } from '@aztec/stdlib/errors'; import type { Gas, GasUsed } from '@aztec/stdlib/gas'; +import type { PublicDebuggedLog } from '@aztec/stdlib/logs'; import { ProvingRequestType } from '@aztec/stdlib/proofs'; import type { MerkleTreeWriteOperations } from '@aztec/stdlib/trees'; import { @@ -47,6 +48,8 @@ export type PublicTxResult = { /** Revert reason, if any */ revertReason?: SimulationError; processedPhases: ProcessedPhase[]; + /** Debug logs emitted during public execution */ + publicDebuggedLogs: PublicDebuggedLog[]; }; export class PublicTxSimulator { @@ -176,6 +179,7 @@ export class PublicTxSimulator { revertCode, revertReason: context.revertReason, processedPhases: processedPhases, + publicDebuggedLogs: context.getDebugLogs(), }; } finally { // Make sure there are no new contracts in the tx-level cache. diff --git a/yarn-project/simulator/src/public/side_effect_trace.test.ts b/yarn-project/simulator/src/public/side_effect_trace.test.ts index 117e6eea746e..331487e6f7d6 100644 --- a/yarn-project/simulator/src/public/side_effect_trace.test.ts +++ b/yarn-project/simulator/src/public/side_effect_trace.test.ts @@ -15,7 +15,7 @@ import { PublicDataUpdateRequest } from '@aztec/stdlib/avm'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; import { computePublicDataTreeLeafSlot } from '@aztec/stdlib/hash'; import { NoteHash, Nullifier } from '@aztec/stdlib/kernel'; -import { PublicLog } from '@aztec/stdlib/logs'; +import { PublicDebuggedLog, PublicLog } from '@aztec/stdlib/logs'; import { L2ToL1Message } from '@aztec/stdlib/messaging'; import { makeContractClassPublic } from '@aztec/stdlib/testing'; @@ -31,6 +31,8 @@ describe('Public Side Effect Trace', () => { const recipient = Fr.random(); const content = Fr.random(); const log = [Fr.random(), Fr.random(), Fr.random()]; + const debugLogMessageId = 42; + const debugLog = [new Fr(1), new Fr(2)]; let startCounter: number; let startCounterPlus1: number; @@ -88,6 +90,11 @@ describe('Public Side Effect Trace', () => { expect(trace.getSideEffects().publicLogs).toEqual([expectedLog]); }); + it('Should trace debug logs', () => { + trace.traceDebugLog(debugLogMessageId, debugLog); + expect(trace.getSideEffects().publicDebuggedLogs).toEqual([new PublicDebuggedLog(debugLogMessageId, debugLog)]); + }); + describe('Maximum accesses', () => { it('Should enforce maximum number of user public storage writes', async () => { for (let i = 0; i < MAX_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX; i++) { @@ -228,6 +235,7 @@ describe('Public Side Effect Trace', () => { testCounter++; nestedTrace.tracePublicLog(address, log); testCounter++; + nestedTrace.traceDebugLog(debugLogMessageId, debugLog); // no counter incr trace.merge(nestedTrace, reverted); @@ -244,6 +252,8 @@ describe('Public Side Effect Trace', () => { expect(parentSideEffects.nullifiers).toEqual([]); expect(parentSideEffects.l2ToL1Msgs).toEqual([]); expect(parentSideEffects.publicLogs).toEqual([]); + // debug logs don't get dropped on revert + expect(parentSideEffects.publicDebuggedLogs).toEqual([new PublicDebuggedLog(debugLogMessageId, debugLog)]); // parent trace does not adopt nested call's writtenPublicDataSlots expect(trace.isStorageCold(address, slot)).toBe(true); } else { diff --git a/yarn-project/simulator/src/public/side_effect_trace.ts b/yarn-project/simulator/src/public/side_effect_trace.ts index 98fdef6be94b..f655dea4e1dd 100644 --- a/yarn-project/simulator/src/public/side_effect_trace.ts +++ b/yarn-project/simulator/src/public/side_effect_trace.ts @@ -16,7 +16,7 @@ import { PublicDataUpdateRequest } from '@aztec/stdlib/avm'; import type { AztecAddress } from '@aztec/stdlib/aztec-address'; import { computePublicDataTreeLeafSlot } from '@aztec/stdlib/hash'; import { NoteHash, Nullifier } from '@aztec/stdlib/kernel'; -import { PublicLog } from '@aztec/stdlib/logs'; +import { PublicDebuggedLog, PublicLog } from '@aztec/stdlib/logs'; import { L2ToL1Message, ScopedL2ToL1Message } from '@aztec/stdlib/messaging'; import { strict as assert } from 'assert'; @@ -36,6 +36,7 @@ export type SideEffects = { nullifiers: Nullifier[]; l2ToL1Msgs: ScopedL2ToL1Message[]; publicLogs: PublicLog[]; + publicDebuggedLogs: PublicDebuggedLog[]; }; export class SideEffectArrayLengths { @@ -69,6 +70,7 @@ export class SideEffectTrace implements PublicSideEffectTraceInterface { private nullifiers: Nullifier[] = []; private l2ToL1Messages: ScopedL2ToL1Message[] = []; private publicLogs: PublicLog[] = []; + private publicDebuggedLogs: PublicDebuggedLog[] = []; /** Make sure a forked trace is never merged twice. */ private alreadyMergedIntoParent = false; @@ -114,6 +116,9 @@ export class SideEffectTrace implements PublicSideEffectTraceInterface { this.sideEffectCounter = forkedTrace.sideEffectCounter; this.uniqueClassIds.acceptAndMerge(forkedTrace.uniqueClassIds); + // Debug logs are always merged, even on revert + this.publicDebuggedLogs.push(...forkedTrace.publicDebuggedLogs); + if (!reverted) { this.publicDataWrites.push(...forkedTrace.publicDataWrites); this.noteHashes.push(...forkedTrace.noteHashes); @@ -237,6 +242,12 @@ export class SideEffectTrace implements PublicSideEffectTraceInterface { this.incrementSideEffectCounter(); } + public traceDebugLog(messageId: number, fields: Fr[]) { + // Debug logs don't have a limit like other side effects + const debugLog = new PublicDebuggedLog(messageId, fields); + this.publicDebuggedLogs.push(debugLog); + } + public traceGetContractClass(contractClassId: Fr, exists: boolean) { // We limit the number of unique contract class IDs due to hashing and the trace length limit. if (exists && !this.uniqueClassIds.has(contractClassId.toString())) { @@ -260,6 +271,7 @@ export class SideEffectTrace implements PublicSideEffectTraceInterface { nullifiers: this.nullifiers, l2ToL1Msgs: this.l2ToL1Messages, publicLogs: this.publicLogs, + publicDebuggedLogs: this.publicDebuggedLogs, }; } } diff --git a/yarn-project/simulator/src/public/side_effect_trace_interface.ts b/yarn-project/simulator/src/public/side_effect_trace_interface.ts index deefef6f8171..ace0dfdbbe30 100644 --- a/yarn-project/simulator/src/public/side_effect_trace_interface.ts +++ b/yarn-project/simulator/src/public/side_effect_trace_interface.ts @@ -18,5 +18,6 @@ export interface PublicSideEffectTraceInterface { traceNewNullifier(siloedNullifier: Fr): void; traceNewL2ToL1Message(contractAddress: AztecAddress, recipient: Fr, content: Fr): void; tracePublicLog(contractAddress: AztecAddress, log: Fr[]): void; + traceDebugLog(messageId: number, fields: Fr[]): void; traceGetContractClass(contractClassId: Fr, exists: boolean): void; } diff --git a/yarn-project/simulator/src/public/state_manager/state_manager.ts b/yarn-project/simulator/src/public/state_manager/state_manager.ts index 58f647e12db3..6884e69b3efb 100644 --- a/yarn-project/simulator/src/public/state_manager/state_manager.ts +++ b/yarn-project/simulator/src/public/state_manager/state_manager.ts @@ -329,6 +329,15 @@ export class PublicPersistableStateManager { this.trace.tracePublicLog(contractAddress, log); } + /** + * Write a debug log + * @param messageId - message ID (currently always 0) + * @param fields - debug log fields + */ + public writeDebugLog(messageId: number, fields: Fr[]) { + this.trace.traceDebugLog(messageId, fields); + } + /** * Get a contract instance. * @param contractAddress - address of the contract instance to retrieve. diff --git a/yarn-project/stdlib/src/logs/index.ts b/yarn-project/stdlib/src/logs/index.ts index 92bbe7dcd517..a029b4837565 100644 --- a/yarn-project/stdlib/src/logs/index.ts +++ b/yarn-project/stdlib/src/logs/index.ts @@ -2,6 +2,7 @@ export * from './log_with_tx_data.js'; export * from './indexed_tagging_secret.js'; export * from './contract_class_log.js'; export * from './public_log.js'; +export * from './public_debugged_log.js'; export * from './private_log.js'; export * from './pending_tagged_log.js'; export * from './log_id.js'; diff --git a/yarn-project/stdlib/src/logs/public_debugged_log.ts b/yarn-project/stdlib/src/logs/public_debugged_log.ts new file mode 100644 index 000000000000..992a618ab1e3 --- /dev/null +++ b/yarn-project/stdlib/src/logs/public_debugged_log.ts @@ -0,0 +1,55 @@ +import type { FieldsOf } from '@aztec/foundation/array'; +import { Fr } from '@aztec/foundation/fields'; +import { BufferReader, FieldReader, serializeToBuffer, serializeToFields } from '@aztec/foundation/serialize'; + +/** + * Represents a debug log emitted during public execution. + */ +export class PublicDebuggedLog { + constructor( + /** Message ID (currently always 0, reserved for future use) */ + public messageId: number, + /** The fields array from the DebugLog opcode */ + public fields: Fr[], + ) {} + + static from(fields: FieldsOf) { + return new PublicDebuggedLog(fields.messageId, fields.fields); + } + + toBuffer(): Buffer { + return serializeToBuffer([this.messageId, this.fields.length, this.fields]); + } + + static fromBuffer(buffer: Buffer | BufferReader): PublicDebuggedLog { + const reader = BufferReader.asReader(buffer); + const messageId = reader.readNumber(); + const fieldsLength = reader.readNumber(); + const fields = reader.readArray(fieldsLength, Fr); + return new PublicDebuggedLog(messageId, fields); + } + + toFields(): Fr[] { + return serializeToFields([this.messageId, this.fields.length, this.fields]); + } + + static fromFields(fields: Fr[] | FieldReader): PublicDebuggedLog { + const reader = FieldReader.asReader(fields); + const messageId = reader.readField().toNumber(); + const fieldsLength = reader.readField().toNumber(); + const fieldsArray = reader.readFieldArray(fieldsLength); + return new PublicDebuggedLog(messageId, fieldsArray); + } + + toString(): string { + return `PublicDebuggedLog { messageId: ${this.messageId}, fields: [${this.fields.map(f => f.toString()).join(', ')}] }`; + } + + equals(other: PublicDebuggedLog): boolean { + return ( + this.messageId === other.messageId && + this.fields.length === other.fields.length && + this.fields.every((field, index) => field.equals(other.fields[index])) + ); + } +} diff --git a/yarn-project/stdlib/src/tests/factories.ts b/yarn-project/stdlib/src/tests/factories.ts index 44381a47131b..2213bb1c19a5 100644 --- a/yarn-project/stdlib/src/tests/factories.ts +++ b/yarn-project/stdlib/src/tests/factories.ts @@ -1732,7 +1732,8 @@ export async function makeBloatedProcessedTx({ }, gasUsed, RevertCode.OK, - undefined /* revertReason */, + /*revertReason=*/ undefined, + /*publicDebuggedLogs=*/ [], ); } } diff --git a/yarn-project/stdlib/src/tx/processed_tx.ts b/yarn-project/stdlib/src/tx/processed_tx.ts index aab5ec9594bb..d9e37d36dd9b 100644 --- a/yarn-project/stdlib/src/tx/processed_tx.ts +++ b/yarn-project/stdlib/src/tx/processed_tx.ts @@ -8,6 +8,7 @@ import { Gas } from '../gas/gas.js'; import type { GasUsed } from '../gas/gas_used.js'; import { computeL2ToL1MessageHash } from '../hash/hash.js'; import type { PrivateKernelTailCircuitPublicInputs } from '../kernel/private_kernel_tail_circuit_public_inputs.js'; +import type { PublicDebuggedLog } from '../logs/public_debugged_log.js'; import type { ClientIvcProof } from '../proofs/client_ivc_proof.js'; import type { GlobalVariables } from './global_variables.js'; import type { Tx } from './tx.js'; @@ -61,6 +62,10 @@ export type ProcessedTx = { * Reason the tx was reverted. */ revertReason: SimulationError | undefined; + /** + * Debug logs emitted during public execution. + */ + publicDebuggedLogs: PublicDebuggedLog[]; }; /** @@ -125,6 +130,7 @@ export function makeProcessedTxFromPrivateOnlyTx( gasUsed, revertCode: RevertCode.OK, revertReason: undefined, + publicDebuggedLogs: [], }; } @@ -140,6 +146,7 @@ export function makeProcessedTxFromTxWithPublicCalls( gasUsed: GasUsed, revertCode: RevertCode, revertReason: SimulationError | undefined, + publicDebuggedLogs: PublicDebuggedLog[] = [], ): ProcessedTx { const avmPublicInputs = avmProvingRequest.inputs.publicInputs; @@ -189,5 +196,6 @@ export function makeProcessedTxFromTxWithPublicCalls( gasUsed, revertCode, revertReason, + publicDebuggedLogs, }; }