Skip to content

Commit c92129e

Browse files
authored
feat: Validate block proposal txs iteratively (#10921)
Instead of loading all txs from the p2p pool and validating them all, we now get an iterator to the p2p pool and iteratively run through them and validate them as we go. This ensures we only load and validate strictly the txs we need. This also makes it easy to enforce new block constraints such as gas limits, which we add as two new env vars. As part of this PR, we also change the interface of validators. Since there is no point anymore in validating txs in bulk, we drop the `validateTxs` method in favor of just `validateTx`. And since we're at it, we enrich `validateTx` to return `valid/invalid/skip` and to include the failure reason. Fixes #10869
1 parent 1cb7cd7 commit c92129e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+911
-931
lines changed

yarn-project/aztec-node/src/aztec-node/server.test.ts

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
type WorldStateSynchronizer,
1111
mockTxForRollup,
1212
} from '@aztec/circuit-types';
13-
import { type ContractDataSource, EthAddress, Fr, MaxBlockNumber } from '@aztec/circuits.js';
13+
import { type ContractDataSource, EthAddress, Fr, GasFees, MaxBlockNumber } from '@aztec/circuits.js';
1414
import { type P2P } from '@aztec/p2p';
1515
import { type GlobalVariableBuilder } from '@aztec/sequencer-client';
1616
import { NoopTelemetryClient } from '@aztec/telemetry-client/noop';
@@ -37,8 +37,9 @@ describe('aztec node', () => {
3737
p2p = mock<P2P>();
3838

3939
globalVariablesBuilder = mock<GlobalVariableBuilder>();
40-
merkleTreeOps = mock<MerkleTreeReadOperations>();
40+
globalVariablesBuilder.getCurrentBaseFees.mockResolvedValue(new GasFees(0, 0));
4141

42+
merkleTreeOps = mock<MerkleTreeReadOperations>();
4243
merkleTreeOps.findLeafIndices.mockImplementation((_treeId: MerkleTreeId, _value: any[]) => {
4344
return Promise.resolve([undefined]);
4445
});
@@ -99,14 +100,14 @@ describe('aztec node', () => {
99100
const doubleSpendWithExistingTx = txs[1];
100101
lastBlockNumber += 1;
101102

102-
expect(await node.isValidTx(doubleSpendTx)).toBe(true);
103+
expect(await node.isValidTx(doubleSpendTx)).toEqual({ result: 'valid' });
103104

104105
// We push a duplicate nullifier that was created in the same transaction
105106
doubleSpendTx.data.forRollup!.end.nullifiers.push(doubleSpendTx.data.forRollup!.end.nullifiers[0]);
106107

107-
expect(await node.isValidTx(doubleSpendTx)).toBe(false);
108+
expect(await node.isValidTx(doubleSpendTx)).toEqual({ result: 'invalid', reason: ['Duplicate nullifier in tx'] });
108109

109-
expect(await node.isValidTx(doubleSpendWithExistingTx)).toBe(true);
110+
expect(await node.isValidTx(doubleSpendWithExistingTx)).toEqual({ result: 'valid' });
110111

111112
// We make a nullifier from `doubleSpendWithExistingTx` a part of the nullifier tree, so it gets rejected as double spend
112113
const doubleSpendNullifier = doubleSpendWithExistingTx.data.forRollup!.end.nullifiers[0].toBuffer();
@@ -116,20 +117,23 @@ describe('aztec node', () => {
116117
);
117118
});
118119

119-
expect(await node.isValidTx(doubleSpendWithExistingTx)).toBe(false);
120+
expect(await node.isValidTx(doubleSpendWithExistingTx)).toEqual({
121+
result: 'invalid',
122+
reason: ['Existing nullifier'],
123+
});
120124
lastBlockNumber = 0;
121125
});
122126

123127
it('tests that the node correctly validates chain id', async () => {
124128
const tx = mockTxForRollup(0x10000);
125129
tx.data.constants.txContext.chainId = chainId;
126130

127-
expect(await node.isValidTx(tx)).toBe(true);
131+
expect(await node.isValidTx(tx)).toEqual({ result: 'valid' });
128132

129133
// We make the chain id on the tx not equal to the configured chain id
130-
tx.data.constants.txContext.chainId = new Fr(1n + chainId.value);
134+
tx.data.constants.txContext.chainId = new Fr(1n + chainId.toBigInt());
131135

132-
expect(await node.isValidTx(tx)).toBe(false);
136+
expect(await node.isValidTx(tx)).toEqual({ result: 'invalid', reason: ['Incorrect chain id'] });
133137
});
134138

135139
it('tests that the node correctly validates max block numbers', async () => {
@@ -159,11 +163,14 @@ describe('aztec node', () => {
159163
lastBlockNumber = 3;
160164

161165
// Default tx with no max block number should be valid
162-
expect(await node.isValidTx(noMaxBlockNumberMetadata)).toBe(true);
166+
expect(await node.isValidTx(noMaxBlockNumberMetadata)).toEqual({ result: 'valid' });
163167
// Tx with max block number < current block number should be invalid
164-
expect(await node.isValidTx(invalidMaxBlockNumberMetadata)).toBe(false);
168+
expect(await node.isValidTx(invalidMaxBlockNumberMetadata)).toEqual({
169+
result: 'invalid',
170+
reason: ['Invalid block number'],
171+
});
165172
// Tx with max block number >= current block number should be valid
166-
expect(await node.isValidTx(validMaxBlockNumberMetadata)).toBe(true);
173+
expect(await node.isValidTx(validMaxBlockNumberMetadata)).toEqual({ result: 'valid' });
167174
});
168175
});
169176
});

yarn-project/aztec-node/src/aztec-node/server.ts

Lines changed: 28 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import {
1616
NullifierMembershipWitness,
1717
type NullifierWithBlockSource,
1818
P2PClientType,
19-
type ProcessedTx,
2019
type ProverConfig,
2120
PublicDataWitness,
2221
PublicSimulationOutput,
@@ -29,7 +28,7 @@ import {
2928
TxReceipt,
3029
type TxScopedL2Log,
3130
TxStatus,
32-
type TxValidator,
31+
type TxValidationResult,
3332
type WorldStateSynchronizer,
3433
tryStop,
3534
} from '@aztec/circuit-types';
@@ -64,17 +63,16 @@ import { DateProvider, Timer } from '@aztec/foundation/timer';
6463
import { type AztecKVStore } from '@aztec/kv-store';
6564
import { openTmpStore } from '@aztec/kv-store/lmdb';
6665
import { SHA256Trunc, StandardTree, UnbalancedTree } from '@aztec/merkle-tree';
67-
import {
68-
AggregateTxValidator,
69-
DataTxValidator,
70-
DoubleSpendTxValidator,
71-
MetadataTxValidator,
72-
type P2P,
73-
TxProofValidator,
74-
createP2PClient,
75-
} from '@aztec/p2p';
66+
import { type P2P, createP2PClient } from '@aztec/p2p';
7667
import { ProtocolContractAddress } from '@aztec/protocol-contracts';
77-
import { GlobalVariableBuilder, type L1Publisher, SequencerClient, createSlasherClient } from '@aztec/sequencer-client';
68+
import {
69+
GlobalVariableBuilder,
70+
type L1Publisher,
71+
SequencerClient,
72+
createSlasherClient,
73+
createValidatorForAcceptingTxs,
74+
getDefaultAllowedSetupFunctions,
75+
} from '@aztec/sequencer-client';
7876
import { PublicProcessorFactory } from '@aztec/simulator';
7977
import { Attributes, type TelemetryClient, type Traceable, type Tracer, trackSpan } from '@aztec/telemetry-client';
8078
import { NoopTelemetryClient } from '@aztec/telemetry-client/noop';
@@ -418,15 +416,21 @@ export class AztecNodeService implements AztecNode, Traceable {
418416
*/
419417
public async sendTx(tx: Tx) {
420418
const timer = new Timer();
421-
this.log.info(`Received tx ${tx.getTxHash()}`);
419+
const txHash = tx.getTxHash().toString();
422420

423-
if (!(await this.isValidTx(tx))) {
421+
const valid = await this.isValidTx(tx);
422+
if (valid.result !== 'valid') {
423+
const reason = valid.reason.join(', ');
424424
this.metrics.receivedTx(timer.ms(), false);
425+
this.log.warn(`Invalid tx ${txHash}: ${reason}`, { txHash });
426+
// TODO(#10967): Throw when receiving an invalid tx instead of just returning
427+
// throw new Error(`Invalid tx: ${reason}`);
425428
return;
426429
}
427430

428431
await this.p2pClient!.sendTx(tx);
429432
this.metrics.receivedTx(timer.ms(), true);
433+
this.log.info(`Received tx ${tx.getTxHash()}`, { txHash });
430434
}
431435

432436
public async getTxReceipt(txHash: TxHash): Promise<TxReceipt> {
@@ -878,34 +882,19 @@ export class AztecNodeService implements AztecNode, Traceable {
878882
}
879883
}
880884

881-
public async isValidTx(tx: Tx, isSimulation: boolean = false): Promise<boolean> {
885+
public async isValidTx(tx: Tx, isSimulation: boolean = false): Promise<TxValidationResult> {
882886
const blockNumber = (await this.blockSource.getBlockNumber()) + 1;
883887
const db = this.worldStateSynchronizer.getCommitted();
884-
// These validators are taken from the sequencer, and should match.
885-
// The reason why `phases` and `gas` tx validator is in the sequencer and not here is because
886-
// those tx validators are customizable by the sequencer.
887-
const txValidators: TxValidator<Tx | ProcessedTx>[] = [
888-
new DataTxValidator(),
889-
new MetadataTxValidator(new Fr(this.l1ChainId), new Fr(blockNumber)),
890-
new DoubleSpendTxValidator({
891-
getNullifierIndices: nullifiers => db.findLeafIndices(MerkleTreeId.NULLIFIER_TREE, nullifiers),
892-
}),
893-
];
894-
895-
if (!isSimulation) {
896-
txValidators.push(new TxProofValidator(this.proofVerifier));
897-
}
898-
899-
const txValidator = new AggregateTxValidator(...txValidators);
900-
901-
const [_, invalidTxs] = await txValidator.validateTxs([tx]);
902-
if (invalidTxs.length > 0) {
903-
this.log.warn(`Rejecting tx ${tx.getTxHash()} because of validation errors`);
904-
905-
return false;
906-
}
888+
const verifier = isSimulation ? undefined : this.proofVerifier;
889+
const validator = createValidatorForAcceptingTxs(db, this.contractDataSource, verifier, {
890+
blockNumber,
891+
l1ChainId: this.l1ChainId,
892+
enforceFees: !!this.config.enforceFees,
893+
setupAllowList: this.config.allowedInSetup ?? getDefaultAllowedSetupFunctions(),
894+
gasFees: await this.getCurrentBaseFees(),
895+
});
907896

908-
return true;
897+
return await validator.validateTx(tx);
909898
}
910899

911900
public async setConfig(config: Partial<SequencerConfig & ProverConfig>): Promise<void> {

yarn-project/circuit-types/src/interfaces/aztec-node.test.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import { MerkleTreeId } from '../merkle_tree_id.js';
4040
import { EpochProofQuote } from '../prover_coordination/epoch_proof_quote.js';
4141
import { PublicDataWitness } from '../public_data_witness.js';
4242
import { SiblingPath } from '../sibling_path/sibling_path.js';
43+
import { type TxValidationResult } from '../tx/index.js';
4344
import { PublicSimulationOutput } from '../tx/public_simulation_output.js';
4445
import { Tx } from '../tx/tx.js';
4546
import { TxHash } from '../tx/tx_hash.js';
@@ -293,9 +294,14 @@ describe('AztecNodeApiSchema', () => {
293294
expect(response).toBeInstanceOf(PublicSimulationOutput);
294295
});
295296

296-
it('isValidTx', async () => {
297+
it('isValidTx(valid)', async () => {
298+
const response = await context.client.isValidTx(Tx.random(), true);
299+
expect(response).toEqual({ result: 'valid' });
300+
});
301+
302+
it('isValidTx(invalid)', async () => {
297303
const response = await context.client.isValidTx(Tx.random());
298-
expect(response).toBe(true);
304+
expect(response).toEqual({ result: 'invalid', reason: ['Invalid'] });
299305
});
300306

301307
it('setConfig', async () => {
@@ -559,9 +565,9 @@ class MockAztecNode implements AztecNode {
559565
expect(tx).toBeInstanceOf(Tx);
560566
return Promise.resolve(PublicSimulationOutput.random());
561567
}
562-
isValidTx(tx: Tx, _isSimulation?: boolean | undefined): Promise<boolean> {
568+
isValidTx(tx: Tx, isSimulation?: boolean | undefined): Promise<TxValidationResult> {
563569
expect(tx).toBeInstanceOf(Tx);
564-
return Promise.resolve(true);
570+
return Promise.resolve(isSimulation ? { result: 'valid' } : { result: 'invalid', reason: ['Invalid'] });
565571
}
566572
setConfig(config: Partial<SequencerConfig & ProverConfig>): Promise<void> {
567573
expect(config.coinbase).toBeInstanceOf(EthAddress);

yarn-project/circuit-types/src/interfaces/aztec-node.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,14 @@ import { MerkleTreeId } from '../merkle_tree_id.js';
3838
import { EpochProofQuote } from '../prover_coordination/epoch_proof_quote.js';
3939
import { PublicDataWitness } from '../public_data_witness.js';
4040
import { SiblingPath } from '../sibling_path/index.js';
41-
import { PublicSimulationOutput, Tx, TxHash, TxReceipt } from '../tx/index.js';
41+
import {
42+
PublicSimulationOutput,
43+
Tx,
44+
TxHash,
45+
TxReceipt,
46+
type TxValidationResult,
47+
TxValidationResultSchema,
48+
} from '../tx/index.js';
4249
import { TxEffect } from '../tx_effect.js';
4350
import { type SequencerConfig, SequencerConfigSchema } from './configs.js';
4451
import { type L2BlockNumber, L2BlockNumberSchema } from './l2_block_number.js';
@@ -395,7 +402,7 @@ export interface AztecNode
395402
* @param tx - The transaction to validate for correctness.
396403
* @param isSimulation - True if the transaction is a simulated one without generated proofs. (Optional)
397404
*/
398-
isValidTx(tx: Tx, isSimulation?: boolean): Promise<boolean>;
405+
isValidTx(tx: Tx, isSimulation?: boolean): Promise<TxValidationResult>;
399406

400407
/**
401408
* Updates the configuration of this node.
@@ -567,7 +574,7 @@ export const AztecNodeApiSchema: ApiSchemaFor<AztecNode> = {
567574

568575
simulatePublicCalls: z.function().args(Tx.schema, optional(z.boolean())).returns(PublicSimulationOutput.schema),
569576

570-
isValidTx: z.function().args(Tx.schema, optional(z.boolean())).returns(z.boolean()),
577+
isValidTx: z.function().args(Tx.schema, optional(z.boolean())).returns(TxValidationResultSchema),
571578

572579
setConfig: z.function().args(SequencerConfigSchema.merge(ProverConfigSchema).partial()).returns(z.void()),
573580

yarn-project/circuit-types/src/interfaces/configs.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ export interface SequencerConfig {
2020
maxTxsPerBlock?: number;
2121
/** The minimum number of txs to include in a block. */
2222
minTxsPerBlock?: number;
23+
/** The maximum L2 block gas. */
24+
maxL2BlockGas?: number;
25+
/** The maximum DA block gas. */
26+
maxDABlockGas?: number;
2327
/** Recipient of block reward. */
2428
coinbase?: EthAddress;
2529
/** Address to receive fees. */
@@ -53,6 +57,8 @@ export const SequencerConfigSchema = z.object({
5357
transactionPollingIntervalMS: z.number().optional(),
5458
maxTxsPerBlock: z.number().optional(),
5559
minTxsPerBlock: z.number().optional(),
60+
maxL2BlockGas: z.number().optional(),
61+
maxDABlockGas: z.number().optional(),
5662
coinbase: schemas.EthAddress.optional(),
5763
feeRecipient: schemas.AztecAddress.optional(),
5864
acvmWorkingDirectory: z.string().optional(),

yarn-project/circuit-types/src/tx/tx.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import {
22
ClientIvcProof,
3+
Fr,
34
PrivateKernelTailCircuitPublicInputs,
5+
PrivateLog,
46
type PrivateToPublicAccumulatedData,
57
type ScopedLogHash,
68
} from '@aztec/circuits.js';
@@ -230,6 +232,20 @@ export class Tx extends Gossipable {
230232
);
231233
}
232234

235+
/**
236+
* Estimates the tx size based on its private effects. Note that the actual size of the tx
237+
* after processing will probably be larger, as public execution would generate more data.
238+
*/
239+
getEstimatedPrivateTxEffectsSize() {
240+
return (
241+
this.unencryptedLogs.getSerializedLength() +
242+
this.contractClassLogs.getSerializedLength() +
243+
this.data.getNonEmptyNoteHashes().length * Fr.SIZE_IN_BYTES +
244+
this.data.getNonEmptyNullifiers().length * Fr.SIZE_IN_BYTES +
245+
this.data.getNonEmptyPrivateLogs().length * PrivateLog.SIZE_IN_BYTES
246+
);
247+
}
248+
233249
/**
234250
* Convenience function to get a hash out of a tx or a tx-like.
235251
* @param tx - Tx-like object.
Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
1-
import { type AnyTx, type TxValidator } from './tx_validator.js';
1+
import { type AnyTx, type TxValidationResult, type TxValidator } from './tx_validator.js';
22

33
export class EmptyTxValidator<T extends AnyTx = AnyTx> implements TxValidator<T> {
4-
public validateTxs(txs: T[]): Promise<[validTxs: T[], invalidTxs: T[], skippedTxs: T[]]> {
5-
return Promise.resolve([txs, [], []]);
6-
}
7-
8-
public validateTx(_tx: T): Promise<boolean> {
9-
return Promise.resolve(true);
4+
public validateTx(_tx: T): Promise<TxValidationResult> {
5+
return Promise.resolve({ result: 'valid' });
106
}
117
}
Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,23 @@
1+
import { type ZodFor } from '@aztec/foundation/schemas';
2+
3+
import { z } from 'zod';
4+
15
import { type ProcessedTx } from '../processed_tx.js';
26
import { type Tx } from '../tx.js';
37

48
export type AnyTx = Tx | ProcessedTx;
59

10+
export type TxValidationResult =
11+
| { result: 'valid' }
12+
| { result: 'invalid'; reason: string[] }
13+
| { result: 'skipped'; reason: string[] };
14+
615
export interface TxValidator<T extends AnyTx = AnyTx> {
7-
validateTx(tx: T): Promise<boolean>;
8-
validateTxs(txs: T[]): Promise<[validTxs: T[], invalidTxs: T[], skippedTxs?: T[]]>;
16+
validateTx(tx: T): Promise<TxValidationResult>;
917
}
18+
19+
export const TxValidationResultSchema = z.discriminatedUnion('result', [
20+
z.object({ result: z.literal('valid'), reason: z.array(z.string()).optional() }),
21+
z.object({ result: z.literal('invalid'), reason: z.array(z.string()) }),
22+
z.object({ result: z.literal('skipped'), reason: z.array(z.string()) }),
23+
]) satisfies ZodFor<TxValidationResult>;

yarn-project/circuit-types/src/tx_effect.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,11 @@ export class TxEffect {
152152
]);
153153
}
154154

155+
/** Returns the size of this tx effect in bytes as serialized onto DA. */
156+
getDASize() {
157+
return this.toBlobFields().length * Fr.SIZE_IN_BYTES;
158+
}
159+
155160
/**
156161
* Deserializes the TxEffect object from a Buffer.
157162
* @param buffer - Buffer or BufferReader object to deserialize.

yarn-project/circuits.js/src/structs/gas.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,11 @@ export class Gas {
7878
return new Gas(Math.ceil(this.daGas * scalar), Math.ceil(this.l2Gas * scalar));
7979
}
8080

81+
/** Returns true if any of this instance's dimensions is greater than the corresponding on the other. */
82+
gtAny(other: Gas) {
83+
return this.daGas > other.daGas || this.l2Gas > other.l2Gas;
84+
}
85+
8186
computeFee(gasFees: GasFees) {
8287
return GasDimensions.reduce(
8388
(acc, dimension) => acc.add(gasFees.get(dimension).mul(new Fr(this.get(dimension)))),

yarn-project/end-to-end/src/e2e_block_building.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ describe('e2e_block_building', () => {
207207
// to pick up and validate the txs, so we may need to bump it to work on CI. Note that we need
208208
// at least 3s here so the archiver has time to loop once and sync, and the sequencer has at
209209
// least 1s to loop.
210-
sequencer.sequencer.timeTable[SequencerState.WAITING_FOR_TXS] = 4;
210+
sequencer.sequencer.timeTable[SequencerState.INITIALIZING_PROPOSAL] = 4;
211211
sequencer.sequencer.timeTable[SequencerState.CREATING_BLOCK] = 4;
212212
sequencer.sequencer.processTxTime = 1;
213213

0 commit comments

Comments
 (0)