Skip to content
16 changes: 16 additions & 0 deletions yarn-project/aztec-node/src/aztec-node/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { compactArray } from '@aztec/foundation/collection';
import { EthAddress } from '@aztec/foundation/eth-address';
import { Fr } from '@aztec/foundation/fields';
import { type Logger, createLogger } from '@aztec/foundation/log';
import { SerialQueue } from '@aztec/foundation/queue';
import { DateProvider, Timer } from '@aztec/foundation/timer';
import { SiblingPath } from '@aztec/foundation/trees';
import type { AztecKVStore } from '@aztec/kv-store';
Expand Down Expand Up @@ -101,6 +102,9 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
private packageVersion: string;
private metrics: NodeMetrics;

// Serial queue to ensure that we only send one tx at a time
private txQueue: SerialQueue = new SerialQueue();

public readonly tracer: Tracer;

constructor(
Expand All @@ -123,6 +127,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
this.packageVersion = getPackageVersion();
this.metrics = new NodeMetrics(telemetry, 'AztecNodeService');
this.tracer = telemetry.getTracer('AztecNodeService');
this.txQueue.start();

this.log.info(`Aztec Node version: ${this.packageVersion}`);
this.log.info(`Aztec Node started on chain 0x${l1ChainId.toString(16)}`, config.l1Contracts);
Expand Down Expand Up @@ -419,6 +424,16 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
* @param tx - The transaction to be submitted.
*/
public async sendTx(tx: Tx) {
await this.txQueue
.put(async () => {
await this.#sendTx(tx);
})
.catch(error => {
this.log.error(`Error sending tx`, { error });
});
}

async #sendTx(tx: Tx) {
const timer = new Timer();
const txHash = (await tx.getTxHash()).toString();

Expand Down Expand Up @@ -464,6 +479,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
*/
public async stop() {
this.log.info(`Stopping`);
await this.txQueue.end();
await this.sequencer?.stop();
await this.p2pClient.stop();
await this.worldStateSynchronizer.stop();
Expand Down
1 change: 1 addition & 0 deletions yarn-project/aztec.js/src/api/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export { ContractFunctionInteraction, type SendMethodOptions } from '../contract

export { TxProfileResult } from '@aztec/stdlib/tx';
export { DefaultWaitOpts, SentTx, type WaitOpts } from '../contract/sent_tx.js';
export { ProvenTx } from '../contract/proven_tx.js';
export {
ContractBase,
type ContractMethod,
Expand Down
20 changes: 19 additions & 1 deletion yarn-project/bb-prover/src/bb/execute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ type BBExecResult = {
* @param command - The command to execute
* @param args - The arguments to pass
* @param logger - A log function
* @param timeout - An optional timeout before killing the BB process
* @param resultParser - An optional handler for detecting success or failure
* @returns The completed partial witness outputted from the circuit
*/
Expand All @@ -70,6 +71,7 @@ export function executeBB(
command: string,
args: string[],
logger: LogFn,
timeout?: number,
resultParser = (code: number) => code === 0,
): Promise<BBExecResult> {
return new Promise<BBExecResult>(resolve => {
Expand All @@ -80,6 +82,18 @@ export function executeBB(
const bb = proc.spawn(pathToBB, [command, ...args], {
env,
});

let timeoutId: NodeJS.Timeout | undefined;
if (timeout !== undefined) {
timeoutId = setTimeout(() => {
logger(`BB execution timed out after ${timeout}ms, killing process`);
if (bb.pid) {
bb.kill('SIGKILL');
}
resolve({ status: BB_RESULT.FAILURE, exitCode: -1, signal: 'TIMEOUT' });
}, timeout);
}

bb.stdout.on('data', data => {
const message = data.toString('utf-8').replace(/\n$/, '');
logger(message);
Expand All @@ -89,6 +103,9 @@ export function executeBB(
logger(message);
});
bb.on('close', (exitCode: number, signal?: string) => {
if (timeoutId) {
clearTimeout(timeoutId);
}
if (resultParser(exitCode)) {
resolve({ status: BB_RESULT.SUCCESS, exitCode, signal });
} else {
Expand Down Expand Up @@ -604,7 +621,8 @@ export async function verifyClientIvcProof(
const args = ['--scheme', 'client_ivc', '-p', proofPath, '-k', keyPath];
const timer = new Timer();
const command = 'verify';
const result = await executeBB(pathToBB, command, args, log);
const timeout = 1000; // 1s verification timeout for invalid proofs
const result = await executeBB(pathToBB, command, args, log, timeout);
const duration = timer.ms();
if (result.status == BB_RESULT.SUCCESS) {
return { status: BB_RESULT.SUCCESS, durationMs: duration };
Expand Down
84 changes: 83 additions & 1 deletion yarn-project/end-to-end/src/e2e_prover/full.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { type AztecAddress, EthAddress, waitForProven } from '@aztec/aztec.js';
import { type AztecAddress, EthAddress, ProvenTx, Tx, TxReceipt, TxStatus, waitForProven } from '@aztec/aztec.js';
import { RollupContract } from '@aztec/ethereum';
import { parseBooleanEnv } from '@aztec/foundation/config';
import { getTestData, isGenerateTestDataEnabled } from '@aztec/foundation/testing';
import { updateProtocolCircuitSampleInputs } from '@aztec/foundation/testing/files';
import type { FieldsOf } from '@aztec/foundation/types';
import { FeeJuicePortalAbi, RewardDistributorAbi, TestERC20Abi } from '@aztec/l1-artifacts';
import { Gas } from '@aztec/stdlib/gas';
import { PrivateKernelTailCircuitPublicInputs } from '@aztec/stdlib/kernel';
import { ClientIvcProof } from '@aztec/stdlib/proofs';

import TOML from '@iarna/toml';
import '@jest/globals';
Expand Down Expand Up @@ -284,4 +288,82 @@ describe('full_prover', () => {
expect(String((results[0] as PromiseRejectedResult).reason)).toMatch(/Tx dropped by P2P node/);
expect(String((results[1] as PromiseRejectedResult).reason)).toMatch(/Tx dropped by P2P node/);
});

it(
'should prevent large influxes of txs with invalid proofs from causing ddos attacks',
async () => {
if (!REAL_PROOFS) {
t.logger.warn(`Skipping test with fake proofs`);
return;
}

const NUM_INVALID_TXS = 20;

// Create and prove a tx
logger.info(`Creating and proving tx`);
const sendAmount = 1n;
const interaction = provenAssets[0].methods.transfer(recipient, sendAmount);
const provenTx = await interaction.prove({ skipPublicSimulation: true });
const wallet = (provenTx as any).wallet;

// Verify the tx proof
logger.info(`Verifying the valid tx proof`);
await expect(t.circuitProofVerifier?.verifyProof(provenTx)).resolves.toBeTrue();

// Spam node with invalid txs
logger.info(`Submitting ${NUM_INVALID_TXS} invalid transactions to simulate a ddos attack`);
const invalidTxPromises = [];
const data = provenTx.data;
for (let i = 0; i < NUM_INVALID_TXS; i++) {
// Use a random ClientIvcProof and alter the public tx data to generate a unique invalid tx hash
const invalidProvenTx = new ProvenTx(
wallet,
new Tx(
new PrivateKernelTailCircuitPublicInputs(
data.constants,
data.rollupValidationRequests,
data.gasUsed.add(new Gas(i + 1, 0)),
data.feePayer,
data.forPublic,
data.forRollup,
),
ClientIvcProof.random(),
provenTx.contractClassLogs,
provenTx.enqueuedPublicFunctionCalls,
provenTx.publicTeardownFunctionCall,
),
);

const sentTx = invalidProvenTx.send();
invalidTxPromises.push(sentTx.wait({ timeout: 10, interval: 0.1, dontThrowOnRevert: true }));
}

logger.info(`Sending proven tx`);
const validTx = provenTx.send();

// Flag the valid transfer on the token simulator
tokenSim.transferPrivate(sender, recipient, sendAmount);

// Warp to the next epoch
const epoch = await cheatCodes.rollup.getEpoch();
logger.info(`Advancing from epoch ${epoch} to next epoch`);
await cheatCodes.rollup.advanceToNextEpoch();

const results = await Promise.allSettled([...invalidTxPromises, validTx.wait({ timeout: 300, interval: 10 })]);

// Assert that the large influx of invalid txs are rejected and do not ddos the node
for (let i = 0; i < NUM_INVALID_TXS; i++) {
const invalidTxReceipt = (results[i] as PromiseFulfilledResult<FieldsOf<TxReceipt>>).value;
expect(invalidTxReceipt.status).toBe(TxStatus.DROPPED);
expect(invalidTxReceipt.error).toMatch(/Tx dropped by P2P node/);
}

// Assert that the valid tx is successfully sent and mined
const validTxReceipt = (results[NUM_INVALID_TXS] as PromiseFulfilledResult<FieldsOf<TxReceipt>>).value;
expect(validTxReceipt.status).toBe(TxStatus.SUCCESS);

logger.info(`Valid tx was mined and invalid txs were dropped by P2P node`);
},
TIMEOUT,
);
});