diff --git a/yarn-project/aztec-node/src/aztec-node/server.ts b/yarn-project/aztec-node/src/aztec-node/server.ts index 88bb1404b59e..96b30ee28dfa 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.ts @@ -70,7 +70,8 @@ import { type WorldStateSynchronizer, tryStop, } from '@aztec/stdlib/interfaces/server'; -import type { LogFilter, SiloedTag, Tag, TxScopedL2Log } from '@aztec/stdlib/logs'; +import type { DebugLogStore, LogFilter, SiloedTag, Tag, TxScopedL2Log } from '@aztec/stdlib/logs'; +import { InMemoryDebugLogStore, NullDebugLogStore } from '@aztec/stdlib/logs'; import { InboxLeaf, type L1ToL2MessageSource } from '@aztec/stdlib/messaging'; import { P2PClientType } from '@aztec/stdlib/p2p'; import type { Offense, SlashPayloadRound } from '@aztec/stdlib/slashing'; @@ -151,12 +152,20 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { private blobClient?: BlobClientInterface, private validatorClient?: ValidatorClient, private keyStoreManager?: KeystoreManager, + private debugLogStore: DebugLogStore = new NullDebugLogStore(), ) { this.metrics = new NodeMetrics(telemetry, 'AztecNodeService'); this.tracer = telemetry.getTracer('AztecNodeService'); this.log.info(`Aztec Node version: ${this.packageVersion}`); this.log.info(`Aztec Node started on chain 0x${l1ChainId.toString(16)}`, config.l1Contracts); + + // A defensive check that protects us against introducing a bug in the complex `createAndSync` function. We must + // never have debugLogStore enabled when not in test mode because then we would be accumulating debug logs in + // memory which could be a DoS vector on the sequencer (since no fees are paid for debug logs). + if (debugLogStore.isEnabled && config.realProofs) { + throw new Error('debugLogStore should never be enabled when realProofs are set'); + } } public async getWorldStateSyncStatus(): Promise { @@ -296,9 +305,19 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { config.realProofs || config.debugForceTxProofVerification ? await BBCircuitVerifier.new(config) : new TestCircuitVerifier(config.proverTestVerificationDelayMs); + + let debugLogStore: DebugLogStore; if (!config.realProofs) { log.warn(`Aztec node is accepting fake proofs`); + + debugLogStore = new InMemoryDebugLogStore(); + log.info( + 'Aztec node started in test mode (realProofs set to false) hence debug logs from public functions will be collected and served', + ); + } else { + debugLogStore = new NullDebugLogStore(); } + const proofVerifier = new QueuedIVCVerifier(config, circuitVerifier); // create the tx pool and the p2p client, which will need the l2 block source @@ -457,6 +476,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { archiver, dateProvider, telemetry, + debugLogStore, ); sequencer = await SequencerClient.new(config, { @@ -538,6 +558,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { blobClient, validatorClient, keyStoreManager, + debugLogStore, ); return node; @@ -831,18 +852,22 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { // Then get the actual tx from the archiver, which tracks every tx in a mined block. const settledTxReceipt = await this.blockSource.getSettledTxReceipt(txHash); + let receipt: TxReceipt; if (settledTxReceipt) { - // If the archiver has the receipt then return it. - return settledTxReceipt; + receipt = settledTxReceipt; } else if (isKnownToPool) { // If the tx is in the pool but not in the archiver, it's pending. // This handles race conditions between archiver and p2p, where the archiver // has pruned the block in which a tx was mined, but p2p has not caught up yet. - return new TxReceipt(txHash, TxStatus.PENDING, undefined, undefined); + receipt = new TxReceipt(txHash, TxStatus.PENDING, undefined, undefined); } else { // Otherwise, if we don't know the tx, we consider it dropped. - return new TxReceipt(txHash, TxStatus.DROPPED, undefined, 'Tx dropped by P2P node'); + receipt = new TxReceipt(txHash, TxStatus.DROPPED, undefined, 'Tx dropped by P2P node'); } + + this.debugLogStore.decorateReceiptWithLogs(txHash.toString(), receipt); + + return receipt; } public getTxEffect(txHash: TxHash): Promise { 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 74b83fdb9f96..e3a776edac02 100644 --- a/yarn-project/simulator/src/public/public_processor/public_processor.ts +++ b/yarn-project/simulator/src/public/public_processor/public_processor.ts @@ -25,7 +25,7 @@ import type { PublicProcessorValidator, SequencerConfig, } from '@aztec/stdlib/interfaces/server'; -import type { DebugLog } from '@aztec/stdlib/logs'; +import { type DebugLog, type DebugLogStore, NullDebugLogStore } from '@aztec/stdlib/logs'; import { ProvingRequestType } from '@aztec/stdlib/proofs'; import { MerkleTreeId } from '@aztec/stdlib/trees'; import { @@ -140,6 +140,7 @@ export class PublicProcessor implements Traceable { telemetryClient: TelemetryClient = getTelemetryClient(), private log: Logger, private opts: Pick = {}, + private debugLogStore: DebugLogStore = new NullDebugLogStore(), ) { this.metrics = new PublicProcessorMetrics(telemetryClient, 'PublicProcessor'); } @@ -293,6 +294,8 @@ export class PublicProcessor implements Traceable { returns = returns.concat(returnValues); debugLogs.push(...txDebugLogs); + this.debugLogStore.storeLogs(processedTx.hash.toString(), txDebugLogs); + totalPublicGas = totalPublicGas.add(processedTx.gasUsed.publicGas); totalBlockGas = totalBlockGas.add(processedTx.gasUsed.totalGas); totalSizeInBytes += txSize; diff --git a/yarn-project/simulator/src/public/public_tx_simulator/factories.ts b/yarn-project/simulator/src/public/public_tx_simulator/factories.ts index 646317a3d94a..8d1c29746334 100644 --- a/yarn-project/simulator/src/public/public_tx_simulator/factories.ts +++ b/yarn-project/simulator/src/public/public_tx_simulator/factories.ts @@ -19,10 +19,11 @@ export function createPublicTxSimulatorForBlockBuilding( globalVariables: GlobalVariables, telemetryClient: TelemetryClient, bindings?: LoggerBindings, + collectDebugLogs = false, ) { const config = PublicSimulatorConfig.from({ skipFeeEnforcement: false, - collectDebugLogs: false, + collectDebugLogs, collectHints: false, collectPublicInputs: false, collectStatistics: false, diff --git a/yarn-project/stdlib/src/logs/debug_log_store.ts b/yarn-project/stdlib/src/logs/debug_log_store.ts new file mode 100644 index 000000000000..f671c4967bca --- /dev/null +++ b/yarn-project/stdlib/src/logs/debug_log_store.ts @@ -0,0 +1,54 @@ +import type { TxReceipt } from '../tx/tx_receipt.js'; +import type { DebugLog } from './debug_log.js'; + +/** + * Store for debug logs emitted by public functions during transaction execution. + * + * Uses the Null Object pattern: production code uses NullDebugLogStore (no-op), while test mode uses + * InMemoryDebugLogStore (stores and serves logs). + */ +export interface DebugLogStore { + /** Store debug logs for a processed transaction. */ + storeLogs(txHash: string, logs: DebugLog[]): void; + /** Decorate a TxReceipt with any stored debug logs for the given tx. */ + decorateReceiptWithLogs(txHash: string, receipt: TxReceipt): void; + /** Whether debug log collection is enabled. */ + readonly isEnabled: boolean; +} + +/** No-op implementation for production mode. */ +export class NullDebugLogStore implements DebugLogStore { + storeLogs(_txHash: string, _logs: DebugLog[]): void { + return; + } + decorateReceiptWithLogs(_txHash: string, _receipt: TxReceipt): void { + return; + } + get isEnabled(): boolean { + return false; + } +} + +/** In-memory implementation for test mode that stores and serves debug logs. */ +export class InMemoryDebugLogStore implements DebugLogStore { + private map = new Map(); + + storeLogs(txHash: string, logs: DebugLog[]): void { + if (logs.length > 0) { + this.map.set(txHash, logs); + } + } + + decorateReceiptWithLogs(txHash: string, receipt: TxReceipt): void { + if (receipt.isMined()) { + const debugLogs = this.map.get(txHash); + if (debugLogs) { + receipt.debugLogs = debugLogs; + } + } + } + + get isEnabled(): boolean { + return true; + } +} diff --git a/yarn-project/stdlib/src/logs/index.ts b/yarn-project/stdlib/src/logs/index.ts index dafe33e376db..aba30077041d 100644 --- a/yarn-project/stdlib/src/logs/index.ts +++ b/yarn-project/stdlib/src/logs/index.ts @@ -12,5 +12,6 @@ export * from './shared_secret_derivation.js'; export * from './tx_scoped_l2_log.js'; export * from './message_context.js'; export * from './debug_log.js'; +export * from './debug_log_store.js'; export * from './tag.js'; export * from './siloed_tag.js'; diff --git a/yarn-project/stdlib/src/tx/tx_receipt.ts b/yarn-project/stdlib/src/tx/tx_receipt.ts index ec54694712d5..3b67b0057ba5 100644 --- a/yarn-project/stdlib/src/tx/tx_receipt.ts +++ b/yarn-project/stdlib/src/tx/tx_receipt.ts @@ -4,6 +4,7 @@ import { z } from 'zod'; import { RevertCode } from '../avm/revert_code.js'; import { BlockHash } from '../block/block_hash.js'; +import { DebugLog } from '../logs/debug_log.js'; import { type ZodFor, schemas } from '../schemas/schemas.js'; import { TxHash } from './tx_hash.js'; @@ -57,6 +58,12 @@ export class TxReceipt { public blockHash?: BlockHash, /** The block number in which the transaction was included. */ public blockNumber?: BlockNumber, + /** + * Debug logs collected during public function execution. Served only when the node is in test mode and placed on + * the receipt only because it's a convenient place for it (the logs are printed out by the wallet when a mined + * tx receipt is obtained). + */ + public debugLogs?: DebugLog[], ) {} /** Returns true if the transaction was executed successfully. */ @@ -103,6 +110,7 @@ export class TxReceipt { blockHash: BlockHash.schema.optional(), blockNumber: BlockNumberSchema.optional(), transactionFee: schemas.BigInt.optional(), + debugLogs: z.array(DebugLog.schema).optional(), }) .transform(fields => TxReceipt.from(fields)); } @@ -115,6 +123,7 @@ export class TxReceipt { transactionFee?: bigint; blockHash?: BlockHash; blockNumber?: BlockNumber; + debugLogs?: DebugLog[]; }) { return new TxReceipt( fields.txHash, @@ -124,6 +133,7 @@ export class TxReceipt { fields.transactionFee, fields.blockHash, fields.blockNumber, + fields.debugLogs, ); } diff --git a/yarn-project/validator-client/src/checkpoint_builder.ts b/yarn-project/validator-client/src/checkpoint_builder.ts index 7805f431610b..04ffeaf33fd8 100644 --- a/yarn-project/validator-client/src/checkpoint_builder.ts +++ b/yarn-project/validator-client/src/checkpoint_builder.ts @@ -28,6 +28,7 @@ import { type PublicProcessorLimits, type WorldStateSynchronizer, } from '@aztec/stdlib/interfaces/server'; +import { type DebugLogStore, NullDebugLogStore } from '@aztec/stdlib/logs'; import { MerkleTreeId } from '@aztec/stdlib/trees'; import { type CheckpointGlobalVariables, GlobalVariables, StateReference, Tx } from '@aztec/stdlib/tx'; import { type TelemetryClient, getTelemetryClient } from '@aztec/telemetry-client'; @@ -52,6 +53,7 @@ export class CheckpointBuilder implements ICheckpointBlockBuilder { private dateProvider: DateProvider, private telemetryClient: TelemetryClient, bindings?: LoggerBindings, + private debugLogStore: DebugLogStore = new NullDebugLogStore(), ) { this.log = createLogger('checkpoint-builder', { ...bindings, @@ -152,6 +154,8 @@ export class CheckpointBuilder implements ICheckpointBlockBuilder { const contractsDB = new PublicContractsDB(this.contractDataSource, this.log.getBindings()); const guardedFork = new GuardedMerkleTreeOperations(fork); + const collectDebugLogs = this.debugLogStore.isEnabled; + const bindings = this.log.getBindings(); const publicTxSimulator = createPublicTxSimulatorForBlockBuilding( guardedFork, @@ -159,6 +163,7 @@ export class CheckpointBuilder implements ICheckpointBlockBuilder { globalVariables, this.telemetryClient, bindings, + collectDebugLogs, ); const processor = new PublicProcessor( @@ -170,6 +175,7 @@ export class CheckpointBuilder implements ICheckpointBlockBuilder { this.telemetryClient, createLogger('simulator:public-processor', bindings), this.config, + this.debugLogStore, ); const validator = createValidatorForBlockBuilding( @@ -197,6 +203,7 @@ export class FullNodeCheckpointsBuilder implements ICheckpointsBuilder { private contractDataSource: ContractDataSource, private dateProvider: DateProvider, private telemetryClient: TelemetryClient = getTelemetryClient(), + private debugLogStore: DebugLogStore = new NullDebugLogStore(), ) { this.log = createLogger('checkpoint-builder'); } @@ -251,6 +258,7 @@ export class FullNodeCheckpointsBuilder implements ICheckpointsBuilder { this.dateProvider, this.telemetryClient, bindings, + this.debugLogStore, ); } @@ -311,6 +319,7 @@ export class FullNodeCheckpointsBuilder implements ICheckpointsBuilder { this.dateProvider, this.telemetryClient, bindings, + this.debugLogStore, ); } diff --git a/yarn-project/wallet-sdk/src/base-wallet/base_wallet.ts b/yarn-project/wallet-sdk/src/base-wallet/base_wallet.ts index d6ac9462f8be..f45118ed2773 100644 --- a/yarn-project/wallet-sdk/src/base-wallet/base_wallet.ts +++ b/yarn-project/wallet-sdk/src/base-wallet/base_wallet.ts @@ -28,7 +28,7 @@ import type { ChainInfo } from '@aztec/entrypoints/interfaces'; import { Fr } from '@aztec/foundation/curves/bn254'; import { createLogger } from '@aztec/foundation/log'; import type { FieldsOf } from '@aztec/foundation/types'; -import type { AccessScopes, ContractNameResolver } from '@aztec/pxe/client/lazy'; +import { type AccessScopes, displayDebugLogs } from '@aztec/pxe/client/lazy'; import type { PXE, PackedPrivateEvent } from '@aztec/pxe/server'; import { type ContractArtifact, @@ -338,15 +338,6 @@ export abstract class BaseWallet implements Wallet { blockHeader = (await this.aztecNode.getBlockHeader())!; } - const getContractName: ContractNameResolver = async address => { - const instance = await this.pxe.getContractInstance(address); - if (!instance) { - return undefined; - } - const artifact = await this.pxe.getContractArtifact(instance.currentContractClassId); - return artifact?.name; - }; - const [optimizedResults, normalResult] = await Promise.all([ optimizableCalls.length > 0 ? simulateViaNode( @@ -357,7 +348,7 @@ export abstract class BaseWallet implements Wallet { feeOptions.gasSettings, blockHeader, opts.skipFeeEnforcement ?? true, - getContractName, + this.getContractName.bind(this), ) : Promise.resolve([]), remainingCalls.length > 0 @@ -410,7 +401,27 @@ export abstract class BaseWallet implements Wallet { // Otherwise, wait for the full receipt (default behavior on wait: undefined) const waitOpts = typeof opts.wait === 'object' ? opts.wait : undefined; - return (await waitForTx(this.aztecNode, txHash, waitOpts)) as SendReturn; + const receipt = await waitForTx(this.aztecNode, txHash, waitOpts); + + // Display debug logs from public execution if present (served in test mode only) + if (receipt.debugLogs?.length) { + await displayDebugLogs(receipt.debugLogs, this.getContractName.bind(this)); + } + + return receipt as SendReturn; + } + + /** + * Resolves a contract address to a human-readable name via PXE, if available. + * @param address - The contract address to resolve. + */ + protected async getContractName(address: AztecAddress): Promise { + const instance = await this.pxe.getContractInstance(address); + if (!instance) { + return undefined; + } + const artifact = await this.pxe.getContractArtifact(instance.currentContractClassId); + return artifact?.name; } protected contextualizeError(err: Error, ...context: string[]): Error {