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
2 changes: 1 addition & 1 deletion yarn-project/end-to-end/bootstrap.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ function test_cmds {
local prefix="$hash $run_test_script"

if [ "$CI_FULL" -eq 1 ]; then
echo "$hash timeout -v 900s bash -c 'CPUS=16 MEM=96g $run_test_script simple e2e_prover/full real'"
echo "$hash timeout -v 1200s bash -c 'CPUS=16 MEM=96g $run_test_script simple e2e_prover/full real'"
Comment thread
natebeauregard marked this conversation as resolved.
Outdated
else
echo "$hash FAKE_PROOFS=1 $run_test_script simple e2e_prover/full fake"
fi
Expand Down
94 changes: 93 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,13 +1,20 @@
import { type AztecAddress, EthAddress, waitForProven } from '@aztec/aztec.js';
import { type AztecAddress, EthAddress, Tx, TxReceipt, TxStatus, waitForProven } from '@aztec/aztec.js';
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, RollupAbi, 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';
import { type Chain, type GetContractReturnType, type HttpTransport, type PublicClient, getContract } from 'viem';

// TODO(#12613)
// eslint-disable-next-line import/no-relative-packages
import { ProvenTx } from '../../../aztec.js/src/contract/proven_tx.js';
Comment thread
natebeauregard marked this conversation as resolved.
Outdated
import { FullProverTest } from './e2e_prover_test.js';

// Set a very long 20 minute timeout.
Expand Down Expand Up @@ -294,4 +301,89 @@ 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 = 50;

// 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();

const validReceipt = await validTx.wait({ timeout: 300, interval: 10 });
logger.info(`Valid tx has been mined`);

// 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 }),
await waitForProven(t.aztecNode, validReceipt, { provenTimeout: 1500 }),
]);

// 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 proven
const validTxReceipt = (results[NUM_INVALID_TXS] as PromiseFulfilledResult<FieldsOf<TxReceipt>>).value;
expect(validTxReceipt.status).toBe(TxStatus.SUCCESS);

logger.info(`Valid tx was proven and invalid txs were dropped by P2P node`);
},
TIMEOUT,
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,32 @@ import { Tx, type TxValidationResult, type TxValidator } from '@aztec/stdlib/tx'

export class TxProofValidator implements TxValidator<Tx> {
#log = createLogger('p2p:tx_validator:private_proof');
private static readonly PROOF_TIMEOUT_MS = 1000;

constructor(private verifier: ClientProtocolCircuitVerifier) {}

async validateTx(tx: Tx): Promise<TxValidationResult> {
if (!(await this.verifier.verifyProof(tx))) {
this.#log.warn(`Rejecting tx ${await Tx.getHash(tx)} for invalid proof`);
try {
const proofTimeout = new Promise<boolean>((_, reject) => {
setTimeout(
() => reject(new Error(`Proof verification timed out after ${TxProofValidator.PROOF_TIMEOUT_MS}ms`)),
TxProofValidator.PROOF_TIMEOUT_MS,
);
});

// Consider the proof invalid if verification takes longer than 1s. Valid proofs take ~300ms to validate.
const isValid = await Promise.race([this.verifier.verifyProof(tx), proofTimeout]);
Comment thread
natebeauregard marked this conversation as resolved.
Outdated

if (!isValid) {
this.#log.warn(`Rejecting tx ${await Tx.getHash(tx)} for invalid proof`);
return { result: 'invalid', reason: ['Invalid proof'] };
}

this.#log.trace(`Accepted ${await Tx.getHash(tx)} with valid proof`);
return { result: 'valid' };
} catch (error) {
this.#log.warn(`Rejecting tx ${await Tx.getHash(tx)}: ${String(error)}`);
return { result: 'invalid', reason: ['Invalid proof'] };
}
this.#log.trace(`Accepted ${await Tx.getHash(tx)} with valid proof`);
return { result: 'valid' };
}
}