Skip to content

Commit 2e58f0a

Browse files
authored
feat(val): reex (#9768)
1 parent ceaeda5 commit 2e58f0a

File tree

24 files changed

+525
-80
lines changed

24 files changed

+525
-80
lines changed

scripts/ci/get_e2e_jobs.sh

-1
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,6 @@ done
6969
# Add the input labels and expanded matches to allow_list
7070
allow_list+=("${input_labels[@]}" "${expanded_allow_list[@]}")
7171

72-
7372
# Generate full list of targets, excluding specific entries, on one line
7473
test_list=$(echo "${full_list[@]}" | grep -v 'base' | grep -v 'bench' | grep -v "network" | grep -v 'devnet' | xargs echo)
7574

spartan/aztec-network/templates/validator.yaml

+2
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,8 @@ spec:
151151
value: "{{ .Values.validator.p2p.enabled }}"
152152
- name: VALIDATOR_DISABLED
153153
value: "{{ .Values.validator.validator.disabled }}"
154+
- name: VALIDATOR_REEXECUTE
155+
value: "{{ .Values.validator.validator.reexecute }}"
154156
- name: SEQ_MAX_SECONDS_BETWEEN_BLOCKS
155157
value: "{{ .Values.validator.sequencer.maxSecondsBetweenBlocks }}"
156158
- name: SEQ_MIN_TX_PER_BLOCK

spartan/aztec-network/values.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ validator:
9494
enforceTimeTable: true
9595
validator:
9696
disabled: false
97+
reexecute: true
9798
p2p:
9899
enabled: "true"
99100
startupProbe:

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

+10
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1+
import { type AztecAddress } from '@aztec/foundation/aztec-address';
12
import { Fr } from '@aztec/foundation/fields';
23
import { BufferReader, FieldReader, serializeToBuffer } from '@aztec/foundation/serialize';
34

45
import { inspect } from 'util';
56

7+
import { computePublicDataTreeLeafSlot } from '../hash/hash.js';
8+
import { type ContractStorageUpdateRequest } from './contract_storage_update_request.js';
9+
610
// TO BE REMOVED.
711
/**
812
* Write operations on the public data tree including the previous value.
@@ -75,6 +79,12 @@ export class PublicDataUpdateRequest {
7579
return new PublicDataUpdateRequest(Fr.fromBuffer(reader), Fr.fromBuffer(reader), reader.readNumber());
7680
}
7781

82+
static fromContractStorageUpdateRequest(contractAddress: AztecAddress, updateRequest: ContractStorageUpdateRequest) {
83+
const leafSlot = computePublicDataTreeLeafSlot(contractAddress, updateRequest.storageSlot);
84+
85+
return new PublicDataUpdateRequest(leafSlot, updateRequest.newValue, updateRequest.counter);
86+
}
87+
7888
static empty() {
7989
return new PublicDataUpdateRequest(Fr.ZERO, Fr.ZERO, 0);
8090
}

yarn-project/end-to-end/scripts/e2e_test_config.yml

+2
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ tests:
9090
test_path: 'e2e_p2p/rediscovery.test.ts'
9191
e2e_p2p_reqresp:
9292
test_path: 'e2e_p2p/reqresp.test.ts'
93+
e2e_p2p_reex:
94+
test_path: 'e2e_p2p/reex.test.ts'
9395
flakey_e2e_tests:
9496
test_path: './src/flakey'
9597
ignore_failures: true

yarn-project/end-to-end/src/e2e_p2p/gossip_network.test.ts

+2
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ describe('e2e_p2p_network', () => {
4545
throw new Error('Bootstrap node ENR is not available');
4646
}
4747

48+
t.ctx.aztecNodeConfig.validatorReexecute = true;
49+
4850
// create our network of nodes and submit txs into each of them
4951
// the number of txs per node and the number of txs per rollup
5052
// should be set so that the only way for rollups to be built

yarn-project/end-to-end/src/e2e_p2p/p2p_network.ts

+50-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
import { getSchnorrAccount } from '@aztec/accounts/schnorr';
12
import { type AztecNodeConfig, type AztecNodeService } from '@aztec/aztec-node';
2-
import { EthCheatCodes } from '@aztec/aztec.js';
3+
import { type AccountWalletWithSecretKey, EthCheatCodes } from '@aztec/aztec.js';
34
import { EthAddress } from '@aztec/circuits.js';
45
import { getL1ContractsConfigEnvVars } from '@aztec/ethereum';
56
import { type DebugLogger, createDebugLogger } from '@aztec/foundation/log';
67
import { RollupAbi } from '@aztec/l1-artifacts';
8+
import { SpamContract } from '@aztec/noir-contracts.js';
79
import { type BootstrapNode } from '@aztec/p2p';
810
import { createBootstrapNodeFromPrivateKey } from '@aztec/p2p/mocks';
911

@@ -17,7 +19,12 @@ import {
1719
generateNodePrivateKeys,
1820
generatePeerIdPrivateKeys,
1921
} from '../fixtures/setup_p2p_test.js';
20-
import { type ISnapshotManager, type SubsystemsContext, createSnapshotManager } from '../fixtures/snapshot_manager.js';
22+
import {
23+
type ISnapshotManager,
24+
type SubsystemsContext,
25+
addAccounts,
26+
createSnapshotManager,
27+
} from '../fixtures/snapshot_manager.js';
2128
import { getPrivateKeyFromIndex } from '../fixtures/utils.js';
2229
import { getEndToEndTestTelemetryClient } from '../fixtures/with_telemetry_utils.js';
2330

@@ -39,6 +46,10 @@ export class P2PNetworkTest {
3946

4047
public bootstrapNodeEnr: string = '';
4148

49+
// The re-execution test needs a wallet and a spam contract
50+
public wallet?: AccountWalletWithSecretKey;
51+
public spamContract?: SpamContract;
52+
4253
constructor(
4354
testName: string,
4455
public bootstrapNode: BootstrapNode,
@@ -108,12 +119,16 @@ export class P2PNetworkTest {
108119
client: deployL1ContractsValues.walletClient,
109120
});
110121

122+
this.logger.verbose(`Adding ${this.numberOfNodes} validators`);
123+
111124
const txHashes: `0x${string}`[] = [];
112125
for (let i = 0; i < this.numberOfNodes; i++) {
113126
const account = privateKeyToAccount(this.nodePrivateKeys[i]!);
114127
this.logger.debug(`Adding ${account.address} as validator`);
115128
const txHash = await rollup.write.addValidator([account.address]);
116129
txHashes.push(txHash);
130+
131+
this.logger.debug(`Adding ${account.address} as validator`);
117132
}
118133

119134
// Wait for all the transactions adding validators to be mined
@@ -148,6 +163,39 @@ export class P2PNetworkTest {
148163
});
149164
}
150165

166+
async setupAccount() {
167+
await this.snapshotManager.snapshot(
168+
'setup-account',
169+
addAccounts(1, this.logger, false),
170+
async ({ accountKeys }, ctx) => {
171+
const accountManagers = accountKeys.map(ak => getSchnorrAccount(ctx.pxe, ak[0], ak[1], 1));
172+
await Promise.all(accountManagers.map(a => a.register()));
173+
const wallets = await Promise.all(accountManagers.map(a => a.getWallet()));
174+
this.wallet = wallets[0];
175+
},
176+
);
177+
}
178+
179+
async deploySpamContract() {
180+
await this.snapshotManager.snapshot(
181+
'add-spam-contract',
182+
async () => {
183+
if (!this.wallet) {
184+
throw new Error('Call snapshot t.setupAccount before deploying account contract');
185+
}
186+
187+
const spamContract = await SpamContract.deploy(this.wallet).send().deployed();
188+
return { contractAddress: spamContract.address };
189+
},
190+
async ({ contractAddress }) => {
191+
if (!this.wallet) {
192+
throw new Error('Call snapshot t.setupAccount before deploying account contract');
193+
}
194+
this.spamContract = await SpamContract.at(contractAddress, this.wallet);
195+
},
196+
);
197+
}
198+
151199
async removeInitialNode() {
152200
await this.snapshotManager.snapshot(
153201
'remove-inital-validator',
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import { type AztecNodeService } from '@aztec/aztec-node';
2+
import { type SentTx, sleep } from '@aztec/aztec.js';
3+
4+
/* eslint-disable-next-line no-restricted-imports */
5+
import { BlockProposal, SignatureDomainSeperator, getHashedSignaturePayload } from '@aztec/circuit-types';
6+
7+
import { beforeAll, describe, it, jest } from '@jest/globals';
8+
import fs from 'fs';
9+
10+
import { createNodes } from '../fixtures/setup_p2p_test.js';
11+
import { P2PNetworkTest } from './p2p_network.js';
12+
import { submitComplexTxsTo } from './shared.js';
13+
14+
const NUM_NODES = 4;
15+
const NUM_TXS_PER_NODE = 1;
16+
const BOOT_NODE_UDP_PORT = 41000;
17+
18+
const DATA_DIR = './data/re-ex';
19+
20+
describe('e2e_p2p_reex', () => {
21+
let t: P2PNetworkTest;
22+
let nodes: AztecNodeService[];
23+
24+
beforeAll(async () => {
25+
nodes = [];
26+
27+
t = await P2PNetworkTest.create({
28+
testName: 'e2e_p2p_reex',
29+
numberOfNodes: NUM_NODES,
30+
basePort: BOOT_NODE_UDP_PORT,
31+
});
32+
33+
t.logger.verbose('Setup account');
34+
await t.setupAccount();
35+
36+
t.logger.verbose('Deploy spam contract');
37+
await t.deploySpamContract();
38+
39+
t.logger.verbose('Apply base snapshots');
40+
await t.applyBaseSnapshots();
41+
42+
t.logger.verbose('Setup nodes');
43+
await t.setup();
44+
});
45+
46+
afterAll(async () => {
47+
// shutdown all nodes.
48+
await t.stopNodes(nodes);
49+
await t.teardown();
50+
for (let i = 0; i < NUM_NODES; i++) {
51+
fs.rmSync(`${DATA_DIR}-${i}`, { recursive: true, force: true });
52+
}
53+
});
54+
55+
it('validators should re-execute transactions before attesting', async () => {
56+
// create the bootstrap node for the network
57+
if (!t.bootstrapNodeEnr) {
58+
throw new Error('Bootstrap node ENR is not available');
59+
}
60+
61+
t.ctx.aztecNodeConfig.validatorReexecute = true;
62+
63+
nodes = await createNodes(
64+
t.ctx.aztecNodeConfig,
65+
t.peerIdPrivateKeys,
66+
t.bootstrapNodeEnr,
67+
NUM_NODES,
68+
BOOT_NODE_UDP_PORT,
69+
);
70+
71+
// Hook into the node and intercept re-execution logic, ensuring that it was infact called
72+
const reExecutionSpies = [];
73+
for (const node of nodes) {
74+
// Make sure the nodes submit faulty proposals, in this case a faulty proposal is one where we remove one of the transactions
75+
// Such that the calculated archive will be different!
76+
jest.spyOn((node as any).p2pClient, 'broadcastProposal').mockImplementation(async (...args: unknown[]) => {
77+
// We remove one of the transactions, therefore the block root will be different!
78+
const proposal = args[0] as BlockProposal;
79+
const { txHashes } = proposal.payload;
80+
81+
// We need to mutate the proposal, so we cast to any
82+
(proposal.payload as any).txHashes = txHashes.slice(0, txHashes.length - 1);
83+
84+
// We sign over the proposal using the node's signing key
85+
// Abusing javascript to access the nodes signing key
86+
const signer = (node as any).sequencer.sequencer.validatorClient.validationService.keyStore;
87+
const newProposal = new BlockProposal(
88+
proposal.payload,
89+
await signer.signMessage(getHashedSignaturePayload(proposal.payload, SignatureDomainSeperator.blockProposal)),
90+
);
91+
92+
return (node as any).p2pClient.p2pService.propagate(newProposal);
93+
});
94+
95+
// Store re-execution spys node -> sequencer Client -> seqeuncer -> validator
96+
const spy = jest.spyOn((node as any).sequencer.sequencer.validatorClient, 'reExecuteTransactions');
97+
reExecutionSpies.push(spy);
98+
}
99+
100+
// wait a bit for peers to discover each other
101+
await sleep(4000);
102+
103+
nodes.forEach(node => {
104+
node.getSequencer()?.updateSequencerConfig({
105+
minTxsPerBlock: NUM_TXS_PER_NODE,
106+
maxTxsPerBlock: NUM_TXS_PER_NODE,
107+
});
108+
});
109+
const txs = await submitComplexTxsTo(t.logger, t.spamContract!, NUM_TXS_PER_NODE);
110+
111+
// We ensure that the transactions are NOT mined
112+
try {
113+
await Promise.all(
114+
txs.map(async (tx: SentTx, i: number) => {
115+
t.logger.info(`Waiting for tx ${i}: ${await tx.getTxHash()} to be mined`);
116+
return tx.wait();
117+
}),
118+
);
119+
} catch (e) {
120+
t.logger.info('Failed to mine all txs, as planned');
121+
}
122+
123+
// Expect that all of the re-execution attempts failed with an invalid root
124+
for (const spy of reExecutionSpies) {
125+
for (const result of spy.mock.results) {
126+
await expect(result.value).rejects.toThrow('Validator Error: Re-execution state mismatch');
127+
}
128+
}
129+
});
130+
});

yarn-project/end-to-end/src/e2e_p2p/shared.ts

+26-1
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,37 @@
11
import { getSchnorrAccount } from '@aztec/accounts/schnorr';
22
import { type AztecNodeService } from '@aztec/aztec-node';
3-
import { type DebugLogger } from '@aztec/aztec.js';
3+
import { type DebugLogger, type SentTx } from '@aztec/aztec.js';
44
import { CompleteAddress, TxStatus } from '@aztec/aztec.js';
55
import { Fr, GrumpkinScalar } from '@aztec/foundation/fields';
6+
import { type SpamContract } from '@aztec/noir-contracts.js';
67
import { type PXEService, createPXEService, getPXEServiceConfig as getRpcConfig } from '@aztec/pxe';
78

89
import { type NodeContext } from '../fixtures/setup_p2p_test.js';
910

11+
// submits a set of transactions to the provided Private eXecution Environment (PXE)
12+
export const submitComplexTxsTo = async (logger: DebugLogger, spamContract: SpamContract, numTxs: number) => {
13+
const txs: SentTx[] = [];
14+
15+
const seed = 1234n;
16+
const spamCount = 15;
17+
for (let i = 0; i < numTxs; i++) {
18+
const tx = spamContract.methods.spam(seed + BigInt(i * spamCount), spamCount, false).send();
19+
const txHash = await tx.getTxHash();
20+
21+
logger.info(`Tx sent with hash ${txHash}`);
22+
const receipt = await tx.getReceipt();
23+
expect(receipt).toEqual(
24+
expect.objectContaining({
25+
status: TxStatus.PENDING,
26+
error: '',
27+
}),
28+
);
29+
logger.info(`Receipt received for ${txHash}`);
30+
txs.push(tx);
31+
}
32+
return txs;
33+
};
34+
1035
// creates an instance of the PXE and submit a given number of transactions to it.
1136
export const createPXEServiceAndSubmitTransactions = async (
1237
logger: DebugLogger,

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

-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
*
1212
* To run the Setup run with the `AZTEC_GENERATE_TEST_DATA=1` flag. Without
1313
* this flag, we will run in execution.
14-
*
1514
* There is functionality to store the `stats` of a sync, but currently we
1615
* will simply be writing it to the log instead.
1716
*

yarn-project/end-to-end/src/fixtures/snapshot_manager.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,7 @@ async function setupFromFresh(
260260
opts: SetupOptions = {},
261261
deployL1ContractsArgs: Partial<DeployL1ContractsArgs> = {
262262
assumeProvenThrough: Number.MAX_SAFE_INTEGER,
263+
initialValidators: [],
263264
},
264265
): Promise<SubsystemsContext> {
265266
logger.verbose(`Initializing state...`);
@@ -390,7 +391,6 @@ async function setupFromFresh(
390391
async function setupFromState(statePath: string, logger: Logger): Promise<SubsystemsContext> {
391392
logger.verbose(`Initializing with saved state at ${statePath}...`);
392393

393-
// Load config.
394394
// TODO: For some reason this is currently the union of a bunch of subsystems. That needs fixing.
395395
const aztecNodeConfig: AztecNodeConfig & SetupOptions = JSON.parse(
396396
readFileSync(`${statePath}/aztec_node_config.json`, 'utf-8'),

yarn-project/foundation/src/config/env_var.ts

+1
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ export type EnvVar =
144144
| 'VALIDATOR_ATTESTATIONS_WAIT_TIMEOUT_MS'
145145
| 'VALIDATOR_DISABLED'
146146
| 'VALIDATOR_PRIVATE_KEY'
147+
| 'VALIDATOR_REEXECUTE'
147148
| 'VERSION'
148149
| 'WS_BLOCK_CHECK_INTERVAL_MS'
149150
| 'WS_PROVEN_BLOCKS_ONLY'

yarn-project/prover-client/src/orchestrator/block-building-helpers.ts

+1
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ export async function buildBaseRollupHints(
122122
padArrayEnd(tx.txEffect.nullifiers, Fr.ZERO, MAX_NULLIFIERS_PER_TX).map(n => n.toBuffer()),
123123
NULLIFIER_SUBTREE_HEIGHT,
124124
);
125+
125126
if (nullifierWitnessLeaves === undefined) {
126127
throw new Error(`Could not craft nullifier batch insertion proofs`);
127128
}

yarn-project/sequencer-client/src/block_builder/light.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export class LightweightBlockBuilder implements BlockBuilder {
3232
constructor(private db: MerkleTreeWriteOperations, private telemetry: TelemetryClient) {}
3333

3434
async startNewBlock(numTxs: number, globalVariables: GlobalVariables, l1ToL2Messages: Fr[]): Promise<void> {
35-
this.logger.verbose('Starting new block', { numTxs, globalVariables, l1ToL2Messages });
35+
this.logger.verbose('Starting new block', { numTxs, globalVariables: globalVariables.toJSON(), l1ToL2Messages });
3636
this.numTxs = numTxs;
3737
this.globalVariables = globalVariables;
3838
this.l1ToL2Messages = padArrayEnd(l1ToL2Messages, Fr.ZERO, NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP);

yarn-project/sequencer-client/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ export * from './publisher/index.js';
44
export * from './sequencer/index.js';
55

66
// Used by the node to simulate public parts of transactions. Should these be moved to a shared library?
7+
// ISSUE(#9832)
78
export * from './global_variable_builder/index.js';

0 commit comments

Comments
 (0)