Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 30 additions & 5 deletions yarn-project/aztec-node/src/aztec-node/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<WorldStateSyncStatus> {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -457,6 +476,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
archiver,
dateProvider,
telemetry,
debugLogStore,
);

sequencer = await SequencerClient.new(config, {
Expand Down Expand Up @@ -538,6 +558,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
blobClient,
validatorClient,
keyStoreManager,
debugLogStore,
);

return node;
Expand Down Expand Up @@ -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<IndexedTxEffect | undefined> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -140,6 +140,7 @@ export class PublicProcessor implements Traceable {
telemetryClient: TelemetryClient = getTelemetryClient(),
private log: Logger,
private opts: Pick<SequencerConfig, 'fakeProcessingDelayPerTxMs' | 'fakeThrowAfterProcessingTxCount'> = {},
private debugLogStore: DebugLogStore = new NullDebugLogStore(),
) {
this.metrics = new PublicProcessorMetrics(telemetryClient, 'PublicProcessor');
}
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
54 changes: 54 additions & 0 deletions yarn-project/stdlib/src/logs/debug_log_store.ts
Original file line number Diff line number Diff line change
@@ -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<string, DebugLog[]>();

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;
}
}
1 change: 1 addition & 0 deletions yarn-project/stdlib/src/logs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
10 changes: 10 additions & 0 deletions yarn-project/stdlib/src/tx/tx_receipt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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. */
Expand Down Expand Up @@ -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));
}
Expand All @@ -115,6 +123,7 @@ export class TxReceipt {
transactionFee?: bigint;
blockHash?: BlockHash;
blockNumber?: BlockNumber;
debugLogs?: DebugLog[];
}) {
return new TxReceipt(
fields.txHash,
Expand All @@ -124,6 +133,7 @@ export class TxReceipt {
fields.transactionFee,
fields.blockHash,
fields.blockNumber,
fields.debugLogs,
);
}

Expand Down
9 changes: 9 additions & 0 deletions yarn-project/validator-client/src/checkpoint_builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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,
Expand Down Expand Up @@ -152,13 +154,16 @@ 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,
contractsDB,
globalVariables,
this.telemetryClient,
bindings,
collectDebugLogs,
);

const processor = new PublicProcessor(
Expand All @@ -170,6 +175,7 @@ export class CheckpointBuilder implements ICheckpointBlockBuilder {
this.telemetryClient,
createLogger('simulator:public-processor', bindings),
this.config,
this.debugLogStore,
);

const validator = createValidatorForBlockBuilding(
Expand Down Expand Up @@ -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');
}
Expand Down Expand Up @@ -251,6 +258,7 @@ export class FullNodeCheckpointsBuilder implements ICheckpointsBuilder {
this.dateProvider,
this.telemetryClient,
bindings,
this.debugLogStore,
);
}

Expand Down Expand Up @@ -311,6 +319,7 @@ export class FullNodeCheckpointsBuilder implements ICheckpointsBuilder {
this.dateProvider,
this.telemetryClient,
bindings,
this.debugLogStore,
);
}

Expand Down
35 changes: 23 additions & 12 deletions yarn-project/wallet-sdk/src/base-wallet/base_wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand All @@ -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
Expand Down Expand Up @@ -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<W>;
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<W>;
}

/**
* 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<string | undefined> {
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 {
Expand Down
Loading