Skip to content

Commit c7eaf92

Browse files
sklppy88nventuro
andauthored
fix: enforce parity of sequencer tx validation and node tx validation (#7951)
Part of #4781 by having parity between sequencer tx validation and node tx validation. Note that we are using the validators from the sequencer, and they should match. We are omitting `phases` and `gas` tx validator which is in the sequencer and not here is because those tx validators are customizable by the sequencer and not uniform between all sequencers. --------- Co-authored-by: Nicolás Venturo <nicolas.venturo@gmail.com>
1 parent 4c6fe1a commit c7eaf92

File tree

8 files changed

+262
-96
lines changed

8 files changed

+262
-96
lines changed

yarn-project/aztec-node/package.json

+9-8
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,9 @@
2525
"../package.common.json"
2626
],
2727
"jest": {
28-
"moduleNameMapper": {
29-
"^(\\.{1,2}/.*)\\.[cm]?js$": "$1"
30-
},
31-
"testRegex": "./src/.*\\.test\\.(js|mjs|ts)$",
32-
"rootDir": "./src",
28+
"extensionsToTreatAsEsm": [
29+
".ts"
30+
],
3331
"transform": {
3432
"^.+\\.tsx?$": [
3533
"@swc/jest",
@@ -43,9 +41,11 @@
4341
}
4442
]
4543
},
46-
"extensionsToTreatAsEsm": [
47-
".ts"
48-
],
44+
"moduleNameMapper": {
45+
"^(\\.{1,2}/.*)\\.[cm]?js$": "$1"
46+
},
47+
"testRegex": "./src/.*\\.test\\.(js|mjs|ts)$",
48+
"rootDir": "./src",
4949
"reporters": [
5050
[
5151
"default",
@@ -83,6 +83,7 @@
8383
"@types/jest": "^29.5.0",
8484
"@types/node": "^18.7.23",
8585
"jest": "^29.5.0",
86+
"jest-mock-extended": "^3.0.3",
8687
"ts-node": "^10.9.1",
8788
"typescript": "^5.0.4"
8889
},

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { readFileSync } from 'fs';
1010
import { dirname, resolve } from 'path';
1111
import { fileURLToPath } from 'url';
1212

13-
export { sequencerClientConfigMappings, SequencerClientConfig } from '@aztec/sequencer-client';
13+
export { sequencerClientConfigMappings, SequencerClientConfig };
1414

1515
/**
1616
* The configuration the aztec node.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
import { TestCircuitVerifier } from '@aztec/bb-prover';
2+
import {
3+
type AztecNode,
4+
type L1ToL2MessageSource,
5+
type L2BlockSource,
6+
type L2LogsSource,
7+
MerkleTreeId,
8+
type MerkleTreeOperations,
9+
mockTxForRollup,
10+
} from '@aztec/circuit-types';
11+
import { AztecAddress, EthAddress, Fr, GasFees, GlobalVariables, MaxBlockNumber } from '@aztec/circuits.js';
12+
import { type AztecLmdbStore } from '@aztec/kv-store/lmdb';
13+
import { type P2P } from '@aztec/p2p';
14+
import { type GlobalVariableBuilder } from '@aztec/sequencer-client';
15+
import { NoopTelemetryClient } from '@aztec/telemetry-client/noop';
16+
import { type ContractDataSource } from '@aztec/types/contracts';
17+
import { type WorldStateSynchronizer } from '@aztec/world-state';
18+
19+
import { type MockProxy, mock, mockFn } from 'jest-mock-extended';
20+
21+
import { type AztecNodeConfig, getConfigEnvVars } from './config.js';
22+
import { AztecNodeService } from './server.js';
23+
24+
describe('aztec node', () => {
25+
let p2p: MockProxy<P2P>;
26+
let globalVariablesBuilder: MockProxy<GlobalVariableBuilder>;
27+
let merkleTreeOps: MockProxy<MerkleTreeOperations>;
28+
29+
let lastBlockNumber: number;
30+
31+
let node: AztecNode;
32+
33+
const chainId = new Fr(12345);
34+
const version = Fr.ZERO;
35+
const coinbase = EthAddress.random();
36+
const feeRecipient = AztecAddress.random();
37+
const gasFees = GasFees.empty();
38+
39+
beforeEach(() => {
40+
lastBlockNumber = 0;
41+
42+
p2p = mock<P2P>();
43+
44+
globalVariablesBuilder = mock<GlobalVariableBuilder>();
45+
merkleTreeOps = mock<MerkleTreeOperations>();
46+
47+
const worldState = mock<WorldStateSynchronizer>({
48+
getLatest: () => merkleTreeOps,
49+
});
50+
51+
const l2BlockSource = mock<L2BlockSource>({
52+
getBlockNumber: mockFn().mockResolvedValue(lastBlockNumber),
53+
});
54+
55+
const l2LogsSource = mock<L2LogsSource>();
56+
57+
const l1ToL2MessageSource = mock<L1ToL2MessageSource>();
58+
59+
// all txs use the same allowed FPC class
60+
const contractSource = mock<ContractDataSource>();
61+
62+
const store = mock<AztecLmdbStore>();
63+
64+
const aztecNodeConfig: AztecNodeConfig = getConfigEnvVars();
65+
66+
node = new AztecNodeService(
67+
{
68+
...aztecNodeConfig,
69+
l1Contracts: {
70+
...aztecNodeConfig.l1Contracts,
71+
rollupAddress: EthAddress.ZERO,
72+
registryAddress: EthAddress.ZERO,
73+
inboxAddress: EthAddress.ZERO,
74+
outboxAddress: EthAddress.ZERO,
75+
availabilityOracleAddress: EthAddress.ZERO,
76+
},
77+
},
78+
p2p,
79+
l2BlockSource,
80+
l2LogsSource,
81+
l2LogsSource,
82+
contractSource,
83+
l1ToL2MessageSource,
84+
worldState,
85+
undefined,
86+
31337,
87+
1,
88+
globalVariablesBuilder,
89+
store,
90+
new TestCircuitVerifier(),
91+
new NoopTelemetryClient(),
92+
);
93+
});
94+
95+
describe('tx validation', () => {
96+
it('tests that the node correctly validates double spends', async () => {
97+
const txs = [mockTxForRollup(0x10000), mockTxForRollup(0x20000)];
98+
txs.forEach(tx => {
99+
tx.data.constants.txContext.chainId = chainId;
100+
});
101+
const doubleSpendTx = txs[0];
102+
const doubleSpendWithExistingTx = txs[1];
103+
104+
const mockedGlobalVariables = new GlobalVariables(
105+
chainId,
106+
version,
107+
new Fr(lastBlockNumber + 1),
108+
new Fr(1),
109+
Fr.ZERO,
110+
coinbase,
111+
feeRecipient,
112+
gasFees,
113+
);
114+
115+
globalVariablesBuilder.buildGlobalVariables
116+
.mockResolvedValueOnce(mockedGlobalVariables)
117+
.mockResolvedValueOnce(mockedGlobalVariables);
118+
119+
expect(await node.isValidTx(doubleSpendTx)).toBe(true);
120+
121+
// We push a duplicate nullifier that was created in the same transaction
122+
doubleSpendTx.data.forRollup!.end.nullifiers.push(doubleSpendTx.data.forRollup!.end.nullifiers[0]);
123+
124+
expect(await node.isValidTx(doubleSpendTx)).toBe(false);
125+
126+
globalVariablesBuilder.buildGlobalVariables
127+
.mockResolvedValueOnce(mockedGlobalVariables)
128+
.mockResolvedValueOnce(mockedGlobalVariables);
129+
130+
expect(await node.isValidTx(doubleSpendWithExistingTx)).toBe(true);
131+
132+
// We make a nullifier from `doubleSpendWithExistingTx` a part of the nullifier tree, so it gets rejected as double spend
133+
const doubleSpendNullifier = doubleSpendWithExistingTx.data.forRollup!.end.nullifiers[0].toBuffer();
134+
merkleTreeOps.findLeafIndex.mockImplementation((treeId: MerkleTreeId, value: any) => {
135+
return Promise.resolve(
136+
treeId === MerkleTreeId.NULLIFIER_TREE && value.equals(doubleSpendNullifier) ? 1n : undefined,
137+
);
138+
});
139+
140+
expect(await node.isValidTx(doubleSpendWithExistingTx)).toBe(false);
141+
});
142+
143+
it('tests that the node correctly validates chain id', async () => {
144+
const tx = mockTxForRollup(0x10000);
145+
tx.data.constants.txContext.chainId = chainId;
146+
147+
const mockedGlobalVariables = new GlobalVariables(
148+
chainId,
149+
version,
150+
new Fr(lastBlockNumber + 1),
151+
new Fr(1),
152+
Fr.ZERO,
153+
coinbase,
154+
feeRecipient,
155+
gasFees,
156+
);
157+
158+
globalVariablesBuilder.buildGlobalVariables
159+
.mockResolvedValueOnce(mockedGlobalVariables)
160+
.mockResolvedValueOnce(mockedGlobalVariables);
161+
162+
expect(await node.isValidTx(tx)).toBe(true);
163+
164+
// We make the chain id on the tx not equal to the configured chain id
165+
tx.data.constants.txContext.chainId = new Fr(1n + chainId.value);
166+
167+
expect(await node.isValidTx(tx)).toBe(false);
168+
});
169+
170+
it('tests that the node correctly validates max block numbers', async () => {
171+
const txs = [mockTxForRollup(0x10000), mockTxForRollup(0x20000), mockTxForRollup(0x30000)];
172+
txs.forEach(tx => {
173+
tx.data.constants.txContext.chainId = chainId;
174+
});
175+
176+
const noMaxBlockNumberMetadata = txs[0];
177+
const invalidMaxBlockNumberMetadata = txs[1];
178+
const validMaxBlockNumberMetadata = txs[2];
179+
180+
invalidMaxBlockNumberMetadata.data.forRollup!.rollupValidationRequests = {
181+
maxBlockNumber: new MaxBlockNumber(true, new Fr(1)),
182+
getSize: () => 1,
183+
toBuffer: () => Fr.ZERO.toBuffer(),
184+
};
185+
186+
validMaxBlockNumberMetadata.data.forRollup!.rollupValidationRequests = {
187+
maxBlockNumber: new MaxBlockNumber(true, new Fr(5)),
188+
getSize: () => 1,
189+
toBuffer: () => Fr.ZERO.toBuffer(),
190+
};
191+
192+
const mockedGlobalVariables = new GlobalVariables(
193+
chainId,
194+
version,
195+
new Fr(lastBlockNumber + 5),
196+
new Fr(1),
197+
Fr.ZERO,
198+
coinbase,
199+
feeRecipient,
200+
gasFees,
201+
);
202+
203+
globalVariablesBuilder.buildGlobalVariables
204+
.mockResolvedValueOnce(mockedGlobalVariables)
205+
.mockResolvedValueOnce(mockedGlobalVariables)
206+
.mockResolvedValueOnce(mockedGlobalVariables);
207+
208+
// Default tx with no max block number should be valid
209+
expect(await node.isValidTx(noMaxBlockNumberMetadata)).toBe(true);
210+
// Tx with max block number < current block number should be invalid
211+
expect(await node.isValidTx(invalidMaxBlockNumberMetadata)).toBe(false);
212+
// Tx with max block number >= current block number should be valid
213+
expect(await node.isValidTx(validMaxBlockNumberMetadata)).toBe(true);
214+
});
215+
});
216+
});

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

+33-18
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { createArchiver } from '@aztec/archiver';
22
import { BBCircuitVerifier, TestCircuitVerifier } from '@aztec/bb-prover';
33
import {
44
type AztecNode,
5+
type ClientProtocolCircuitVerifier,
56
type FromLogType,
67
type GetUnencryptedLogsResponse,
78
type L1ToL2MessageSource,
@@ -24,7 +25,6 @@ import {
2425
type TxHash,
2526
TxReceipt,
2627
TxStatus,
27-
type TxValidator,
2828
partitionReverts,
2929
} from '@aztec/circuit-types';
3030
import {
@@ -57,8 +57,15 @@ import { getCanonicalFeeJuice } from '@aztec/protocol-contracts/fee-juice';
5757
import { getCanonicalInstanceDeployer } from '@aztec/protocol-contracts/instance-deployer';
5858
import { getCanonicalKeyRegistryAddress } from '@aztec/protocol-contracts/key-registry';
5959
import { getCanonicalMultiCallEntrypointAddress } from '@aztec/protocol-contracts/multi-call-entrypoint';
60-
import { AggregateTxValidator, DataTxValidator, GlobalVariableBuilder, SequencerClient } from '@aztec/sequencer-client';
61-
import { PublicProcessorFactory, WASMSimulator, createSimulationProvider } from '@aztec/simulator';
60+
import {
61+
AggregateTxValidator,
62+
DataTxValidator,
63+
DoubleSpendTxValidator,
64+
GlobalVariableBuilder,
65+
MetadataTxValidator,
66+
SequencerClient,
67+
} from '@aztec/sequencer-client';
68+
import { PublicProcessorFactory, WASMSimulator, WorldStateDB, createSimulationProvider } from '@aztec/simulator';
6269
import { type TelemetryClient } from '@aztec/telemetry-client';
6370
import { NoopTelemetryClient } from '@aztec/telemetry-client/noop';
6471
import {
@@ -72,7 +79,6 @@ import { MerkleTrees, type WorldStateSynchronizer, createWorldStateSynchronizer
7279

7380
import { type AztecNodeConfig, getPackageInfo } from './config.js';
7481
import { NodeMetrics } from './node_metrics.js';
75-
import { MetadataTxValidator } from './tx_validator/tx_metadata_validator.js';
7682
import { TxProofValidator } from './tx_validator/tx_proof_validator.js';
7783

7884
/**
@@ -97,7 +103,7 @@ export class AztecNodeService implements AztecNode {
97103
protected readonly version: number,
98104
protected readonly globalVariableBuilder: GlobalVariableBuilder,
99105
protected readonly merkleTreesDb: AztecKVStore,
100-
private txValidator: TxValidator,
106+
private proofVerifier: ClientProtocolCircuitVerifier,
101107
private telemetry: TelemetryClient,
102108
private log = createDebugLogger('aztec:node'),
103109
) {
@@ -158,11 +164,6 @@ export class AztecNodeService implements AztecNode {
158164
await Promise.all([p2pClient.start(), worldStateSynchronizer.start()]);
159165

160166
const proofVerifier = config.realProofs ? await BBCircuitVerifier.new(config) : new TestCircuitVerifier();
161-
const txValidator = new AggregateTxValidator(
162-
new DataTxValidator(),
163-
new MetadataTxValidator(config.l1ChainId),
164-
new TxProofValidator(proofVerifier),
165-
);
166167

167168
const simulationProvider = await createSimulationProvider(config, log);
168169

@@ -197,7 +198,7 @@ export class AztecNodeService implements AztecNode {
197198
config.version,
198199
new GlobalVariableBuilder(config),
199200
store,
200-
txValidator,
201+
proofVerifier,
201202
telemetry,
202203
log,
203204
);
@@ -762,7 +763,26 @@ export class AztecNodeService implements AztecNode {
762763
}
763764

764765
public async isValidTx(tx: Tx): Promise<boolean> {
765-
const [_, invalidTxs] = await this.txValidator.validateTxs([tx]);
766+
const blockNumber = (await this.blockSource.getBlockNumber()) + 1;
767+
768+
const newGlobalVariables = await this.globalVariableBuilder.buildGlobalVariables(
769+
new Fr(blockNumber),
770+
// We only need chainId and block number, thus coinbase and fee recipient can be set to 0.
771+
EthAddress.ZERO,
772+
AztecAddress.ZERO,
773+
);
774+
775+
// These validators are taken from the sequencer, and should match.
776+
// The reason why `phases` and `gas` tx validator is in the sequencer and not here is because
777+
// those tx validators are customizable by the sequencer.
778+
const txValidator = new AggregateTxValidator(
779+
new DataTxValidator(),
780+
new MetadataTxValidator(newGlobalVariables),
781+
new DoubleSpendTxValidator(new WorldStateDB(this.worldStateSynchronizer.getLatest())),
782+
new TxProofValidator(this.proofVerifier),
783+
);
784+
785+
const [_, invalidTxs] = await txValidator.validateTxs([tx]);
766786
if (invalidTxs.length > 0) {
767787
this.log.warn(`Rejecting tx ${tx.getTxHash()} because of validation errors`);
768788

@@ -777,12 +797,7 @@ export class AztecNodeService implements AztecNode {
777797
this.sequencer?.updateSequencerConfig(config);
778798

779799
if (newConfig.realProofs !== this.config.realProofs) {
780-
const proofVerifier = config.realProofs ? await BBCircuitVerifier.new(newConfig) : new TestCircuitVerifier();
781-
782-
this.txValidator = new AggregateTxValidator(
783-
new MetadataTxValidator(this.l1ChainId),
784-
new TxProofValidator(proofVerifier),
785-
);
800+
this.proofVerifier = config.realProofs ? await BBCircuitVerifier.new(newConfig) : new TestCircuitVerifier();
786801
}
787802

788803
this.config = newConfig;

0 commit comments

Comments
 (0)