Skip to content

Commit 3db7992

Browse files
authored
chore(port): forward-port v5-next prover + node/world-state backlog to next (#24933)
Forward-ports the **prover** and **node / world-state / barretenberg** slices of the v5-next → next backlog (work merged to `v5-next` after the ~2026-07-08 cut that reshaped `next`). ## Applied (clean cherry-picks, chronological) - feat(prover): revive cancelled proving jobs from a persisted aborted state (#24578) - fix(prover-node): rebuild pruned checkpoint provers and recover the epoch (#24436) - feat: exported in-process testing network (#24629) - fix(validator): sync world state before forking in checkpoint proposal validation (#24694) ## ⚠️ Needs owner conflict-resolution (conflict against reshaped `next`; not included here) Cherry-pick onto this branch and resolve: - [ ] `git cherry-pick -x a4e3a44` — feat(world-state): support prefilled nullifiers in genesis state (#24567) **[RESHAPE — v6 subsystem rewritten; re-implement, do not merge]** - [ ] `git cherry-pick -x c44e74e` — fix(prover-node): do not abort in-flight proving jobs on a clean shutdown (#24579) - [ ] `git cherry-pick -x 1039d38` — feat: allow custom proof submission target address (#24270) Part of the manual v5-next → next backlog sweep. Draft until conflicts are resolved and CI is green.
2 parents ea54ad4 + af8b489 commit 3db7992

45 files changed

Lines changed: 2551 additions & 798 deletions

Some content is hidden

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

l1-contracts/scripts/forge_broadcast.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,11 @@ const timeoutMs =
5959
(isAnvil ? 120_000 : 1_200_000);
6060

6161
const proc = spawn(
62-
"forge",
62+
process.env.FORGE_BIN || "forge",
6363
["script", ...args, "--broadcast", "--batch-size", batchSize],
6464
{
6565
stdio: ["ignore", "pipe", "inherit"],
66-
},
66+
}
6767
);
6868

6969
const stdout = [];
@@ -85,14 +85,14 @@ const exitCode = await new Promise((resolve) => {
8585
});
8686
proc.on("close", (code) => {
8787
clearTimeout(timeout);
88-
resolve(timedOut ? 1 : (code ?? 1));
88+
resolve(timedOut ? 1 : code ?? 1);
8989
});
9090
});
9191

9292
log(
9393
exitCode === 0
9494
? "Broadcast succeeded."
95-
: `Broadcast failed (exit ${exitCode}).`,
95+
: `Broadcast failed (exit ${exitCode}).`
9696
);
9797
const data = Buffer.concat(stdout);
9898
if (data.length > 0) writeSync(1, data);

yarn-project/aztec/bootstrap.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ function test_cmds {
1212
# All CLI tests share test/mixed-workspace/target so they must run sequentially
1313
# in a single jest invocation (--runInBand is set by run_test.sh).
1414
echo "$hash:ISOLATE=1:NAME=aztec/cli NARGO=$NARGO BB=$BB PROFILER_PATH=$PROFILER_PATH yarn-project/scripts/run_test.sh aztec/src/cli"
15+
# setupLocalNetwork smoke test: spins up anvil + an in-process node (network usage ⇒ ISOLATE).
16+
echo "$hash:ISOLATE=1:NAME=aztec/testing yarn-project/scripts/run_test.sh aztec/src/testing/local-network.test.ts"
1517
}
1618

1719
case "$cmd" in

yarn-project/aztec/src/local-network/local-network.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ export type LocalNetworkConfig = AztecNodeConfig & {
8888
l1Mnemonic: string;
8989
/** Whether to deploy test accounts on local network start.*/
9090
testAccounts: boolean;
91+
/** Override the default per-address fee juice granted at genesis to funded addresses. */
92+
initialAccountFeeJuice?: Fr;
9193
};
9294

9395
/**
@@ -176,7 +178,10 @@ export async function createLocalNetwork(config: Partial<LocalNetworkConfig> = {
176178
...(initialAccounts.length ? [bananaFPC, sponsoredFPC] : []),
177179
...prefundAddresses,
178180
];
179-
const { genesisArchiveRoot, genesis, fundingNeeded } = await getGenesisValues(fundedAddresses);
181+
const { genesisArchiveRoot, genesis, fundingNeeded } = await getGenesisValues(
182+
fundedAddresses,
183+
config.initialAccountFeeJuice,
184+
);
180185

181186
const dateProvider = new TestDateProvider();
182187

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export { EthCheatCodes, RollupCheatCodes } from '@aztec/ethereum/test';
22
export { CheatCodes } from './cheat_codes.js';
33
export { EpochTestSettler } from './epoch_test_settler.js';
4+
export { setupLocalNetwork, TEST_FEE_PADDING, type LocalNetwork, type LocalNetworkOptions } from './local-network.js';
45
export { getTokenAllowedSetupFunctions } from './token_allowed_setup.js';
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { getInitialTestAccountsData } from '@aztec/accounts/testing';
2+
import { TokenContract } from '@aztec/noir-contracts.js/Token';
3+
import { EmbeddedWallet } from '@aztec/wallets/embedded';
4+
5+
import { TEST_FEE_PADDING, setupLocalNetwork } from './local-network.js';
6+
7+
describe('setupLocalNetwork', () => {
8+
it('serves a live node on a random L1 port and tears down cleanly', async () => {
9+
await using net = await setupLocalNetwork();
10+
const info = await net.node.getNodeInfo();
11+
expect(info.l1ContractAddresses.rollupAddress).toBeDefined();
12+
expect(await net.node.getBlockNumber()).toBeGreaterThanOrEqual(0);
13+
expect(net.l1ChainId).toBe(31337);
14+
// OS-assigned ephemeral port, never the fixed default 8545.
15+
expect(net.l1RpcUrl).toMatch(/^http:\/\/127\.0\.0\.1:\d+$/);
16+
expect(net.l1RpcUrl).not.toContain(':8545');
17+
}, 300_000);
18+
19+
it('runs two networks in parallel on distinct ports', async () => {
20+
const [a, b] = await Promise.all([setupLocalNetwork(), setupLocalNetwork()]);
21+
try {
22+
expect(a.l1RpcUrl).not.toEqual(b.l1RpcUrl);
23+
expect(await a.node.getBlockNumber()).toBeGreaterThanOrEqual(0);
24+
expect(await b.node.getBlockNumber()).toBeGreaterThanOrEqual(0);
25+
} finally {
26+
await Promise.all([a.stop(), b.stop()]);
27+
}
28+
}, 300_000);
29+
30+
it('pre-funds addresses at genesis so they can pay for their own txs', async () => {
31+
const [alice] = await getInitialTestAccountsData();
32+
const net = await setupLocalNetwork({ fundedAddresses: [alice.address] });
33+
try {
34+
const wallet = await EmbeddedWallet.create(net.node, {
35+
ephemeral: true,
36+
pxeConfig: { proverEnabled: false },
37+
});
38+
await wallet.createSchnorrInitializerlessAccount(alice.secret, alice.salt, alice.signingKey);
39+
wallet.setMinFeePadding(TEST_FEE_PADDING);
40+
41+
const { contract } = await TokenContract.deploy(wallet, alice.address, 'TokenName', 'TKN', 18).send({
42+
from: alice.address,
43+
});
44+
expect(contract.address).toBeDefined();
45+
expect(await net.node.getBlockNumber()).toBeGreaterThan(0);
46+
47+
await wallet.stop();
48+
} finally {
49+
await net.stop();
50+
}
51+
}, 300_000);
52+
});
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import type { AztecNodeService } from '@aztec/aztec-node';
2+
import type { AztecNodeConfig } from '@aztec/aztec-node/config';
3+
import type { Fr } from '@aztec/aztec.js/fields';
4+
import { startAnvil } from '@aztec/ethereum/test';
5+
import type { AztecAddress } from '@aztec/stdlib/aztec-address';
6+
7+
import { foundry } from 'viem/chains';
8+
9+
import { createLocalNetwork } from '../local-network/local-network.js';
10+
11+
/** A running in-process local network: an inline Aztec node backed by its own anvil L1. */
12+
export interface LocalNetwork extends AsyncDisposable {
13+
/** Fully-synced Aztec node, ready to serve client requests. */
14+
node: AztecNodeService;
15+
/** RPC URL of the spawned anvil instance. */
16+
l1RpcUrl: string;
17+
/** Chain id used on L1 (foundry's default 31337). */
18+
l1ChainId: number;
19+
/** Stops every process started by the fixture: node and anvil. Also invoked by `await using`. */
20+
stop: () => Promise<void>;
21+
}
22+
23+
/** Options for {@link setupLocalNetwork}. */
24+
export interface LocalNetworkOptions {
25+
/**
26+
* Addresses that should hold fee juice at genesis. Saves each of these the round-trip of bridging
27+
* + claiming fee juice before they can pay for gas.
28+
*/
29+
fundedAddresses?: AztecAddress[];
30+
/** Override the default per-address genesis fee juice granted to {@link fundedAddresses}. */
31+
initialAccountFeeJuice?: Fr;
32+
/** Node config overrides, e.g. `realProofs`, `aztecEpochDuration`, `p2pEnabled`. */
33+
config?: Partial<AztecNodeConfig>;
34+
}
35+
36+
/**
37+
* Spin up an in-process local network with the given addresses pre-funded.
38+
*
39+
* Each call spawns its own anvil on an OS-assigned random port and runs the Aztec node inline via
40+
* the same {@link createLocalNetwork} codepath that backs `aztec start --local-network` (with the
41+
* sandbox account/FPC/token setup skipped). Distinct ports let independent suites run in parallel.
42+
* The caller must `await result.stop()` in its teardown (or hold the result with `await using`).
43+
*
44+
* Requires a Foundry toolchain (`anvil`/`forge`), installed via `aztec-up` or `foundryup`. Binaries
45+
* are located in the standard install directories or on `PATH`; set `$ANVIL_BIN` / `$FORGE_BIN` to
46+
* pin specific ones.
47+
*/
48+
export async function setupLocalNetwork(opts: LocalNetworkOptions = {}): Promise<LocalNetwork> {
49+
// `--port 0` → anvil binds an ephemeral port that `startAnvil` reads back, so parallel suites
50+
// never collide on a fixed port.
51+
const { rpcUrl, stop: stopAnvil } = await startAnvil({ port: 0 });
52+
53+
try {
54+
const { node, stop: stopNode } = await createLocalNetwork(
55+
{
56+
...opts.config,
57+
l1RpcUrls: [rpcUrl],
58+
testAccounts: false,
59+
prefundAddresses: (opts.fundedAddresses ?? []).map(a => a.toString()),
60+
initialAccountFeeJuice: opts.initialAccountFeeJuice,
61+
},
62+
() => {},
63+
);
64+
65+
// Stop the node before anvil (its teardown still talks to L1); the finally guarantees anvil is
66+
// reaped even if node shutdown throws.
67+
const stop = async () => {
68+
try {
69+
await stopNode();
70+
} finally {
71+
await stopAnvil();
72+
}
73+
};
74+
75+
return {
76+
node,
77+
l1RpcUrl: rpcUrl,
78+
l1ChainId: foundry.id,
79+
stop,
80+
[Symbol.asyncDispose]: stop,
81+
};
82+
} catch (err) {
83+
await stopAnvil();
84+
throw err;
85+
}
86+
}
87+
88+
/**
89+
* Min-fee padding multiplier for test wallets whose txs may mine well after their fee estimate.
90+
* The automine sequencer builds one block per tx and advances L1 time in big jumps, and proposer
91+
* pipelining evolves the fee-asset price across the build/publish gap (~20x observed in CI), so the
92+
* network's congestion base fee can swing sharply between the wallet's fee estimate and the block
93+
* the tx actually lands in. The default wallet padding isn't enough and trips
94+
* `maxFeesPerGas.feePerL2Gas must be >= gasFees.feePerL2Gas`. Apply via
95+
* `wallet.setMinFeePadding(TEST_FEE_PADDING)` on every test wallet that sends txs.
96+
*/
97+
export const TEST_FEE_PADDING = 30;

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { AztecNode } from '@aztec/aztec.js/node';
2+
import { TEST_FEE_PADDING } from '@aztec/aztec/testing';
23
import type { GasFees } from '@aztec/stdlib/gas';
34

45
export const METRICS_PORT = 4318;
@@ -17,9 +18,10 @@ export const LARGE_MIN_FEE_PADDING = 15;
1718
* price modifier evolves faster across the build/publish gap, so client-set maxFeesPerGas (sized
1819
* for the default 5x padding) was getting bumped past by the time the tx mined a few slots later.
1920
* Observed worst case in CI: fee evolved ~20x between PXE snapshot and inclusion, exceeding even
20-
* LARGE_MIN_FEE_PADDING (15x).
21+
* LARGE_MIN_FEE_PADDING (15x). Same multiplier and same class of problem as the published
22+
* {@link TEST_FEE_PADDING}, re-exported under a name that captures the pipelining rationale.
2123
*/
22-
export const PIPELINED_FEE_PADDING = 30;
24+
export const PIPELINED_FEE_PADDING = TEST_FEE_PADDING;
2325

2426
/**
2527
* Setup option preset that opts a test into proposer pipelining. Use with `setup()`:
@@ -77,7 +79,7 @@ export const AUTOMINE_E2E_OPTS = {
7779
minTxsPerBlock: 0,
7880
aztecSlotDuration: 12,
7981
ethereumSlotDuration: 4,
80-
walletMinFeePadding: PIPELINED_FEE_PADDING,
82+
walletMinFeePadding: TEST_FEE_PADDING,
8183
} as const;
8284

8385
/** Returns worst-case predicted min fees with padding applied, mirroring the BaseWallet pattern. */

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ import { getVKTreeRoot } from '@aztec/noir-protocol-circuits-types/vk-tree';
3737
import type { P2PClientDeps } from '@aztec/p2p';
3838
import { MockGossipSubNetwork, getMockPubSubP2PServiceFactory } from '@aztec/p2p/test-helpers';
3939
import { protocolContractsHash } from '@aztec/protocol-contracts';
40-
import type { ProverNodeConfig } from '@aztec/prover-node';
40+
import type { ProverNodeConfig, ProverNodeDeps } from '@aztec/prover-node';
4141
import { type PXEConfig, type PXECreationOptions, getPXEConfig } from '@aztec/pxe/server';
4242
import type { SequencerClient } from '@aztec/sequencer-client';
4343
import { AuthRegistryArtifact, getStandardAuthRegistry } from '@aztec/standard-contracts/auth-registry';
@@ -872,6 +872,7 @@ export function createAndSyncProverNode(
872872
telemetry?: TelemetryClient;
873873
dateProvider: DateProvider;
874874
p2pClientDeps?: P2PClientDeps;
875+
proverNodeDeps?: Partial<ProverNodeDeps>;
875876
},
876877
options: { genesis?: GenesisData; dontStart?: boolean },
877878
): Promise<{ proverNode: AztecNodeService }> {

yarn-project/end-to-end/src/single-node/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ top-level `it`; CI splits each `it` into its own job.
5858
| `cross-chain/` | L1↔L2 messaging on a single node, on the `CrossChainMessagingTest` harness (`cross_chain_messaging_test.ts`), which extends `SingleNodeTestContext` and owns a `CrossChainTestHarness` plus the L1 inbox/outbox handles (it auto-proves via an `EpochTestSettler` when no prover node runs). The shared L1→L2 message helpers live in `message_test_helpers.ts` (`createL1ToL2MessageHelpers`, whose injected `markAsProven` lets each suite plug in its own proving policy). `l1_to_l2` (L1→L2 message readiness and duplicate messages, over private and public scope), `l1_to_l2_inbox_drift` (inbox checkpoint drift after a rollup reorg, over private and public scope), `l2_to_l1` (L2→L1 message inclusion across single/multi-message txs and multi-block checkpoints, subtree-root balancing, and a reorg-and-remine case), `token_bridge` (private and public L1→L2 deposits and L2→L1 withdrawals via the TokenBridge — including mint-on-behalf — plus the withdrawal/claim failure cases, merged from the former three `token_bridge_*` files). |
5959
| `bot/` | Transaction bot implementations. `bot` (transfer bot, AMM bot, and cross-chain bot; exercises fee-juice portal deposits, L2→L1 messages, and bot contract reuse). |
6060
| `sync/` | World-state sync stress and snapshot sync. `synching` builds fixture block data (env-gated, slow) and replays it for sync benchmarks and prune/reorg scenarios (only the outer `it.each` runs in CI); `snapshot_sync` exercises the node snapshot upload/download path, syncing fresh nodes from one or multiple snapshot URLs, including fallback from a corrupted snapshot. |
61-
| `proving/` | Epoch and proof lifecycle. `default_node` (basic proving/node coverage that shares one context-default node across three suites: consecutive epochs prove and finalized blocks are purged from world state beyond the checkpoint-history window, a proof is submitted even with no txs, and the node returns initial genesis-block data — the former `world_state_pruning`, `empty_blocks`, and `node_block_api` files), `long_proving_time` (a prover delay spanning multiple epochs), `multi_proof` (multiple prover nodes prove one epoch), `optimistic.parallel` (checkpoint-driven proving across the happy path and several mid-epoch / last-slot / during-proving reorg cases), `proof_fails.parallel` (proof not accepted after epoch end; proving aborts when the next epoch ends), `cross_chain_public_message` (an epoch with a public tx that consumes an L1→L2 message in the block it lands, guarding against a sequencer/prover state-root mismatch), `upload_failed_proof` (a failed proving job's state is uploaded and re-run on a fresh instance). |
61+
| `proving/` | Epoch and proof lifecycle. `default_node` (basic proving/node coverage that shares one context-default node across three suites: consecutive epochs prove and finalized blocks are purged from world state beyond the checkpoint-history window, a proof is submitted even with no txs, and the node returns initial genesis-block data — the former `world_state_pruning`, `empty_blocks`, and `node_block_api` files), `long_proving_time` (a prover delay spanning multiple epochs), `multi_proof` (multiple prover nodes prove one epoch), `optimistic.parallel` (checkpoint-driven proving across the happy path and several mid-epoch / last-slot / during-proving reorg cases), `proof_fails.parallel` (proof not accepted after epoch end; proving aborts when the next epoch ends), `cross_chain_public_message` (an epoch with a public tx that consumes an L1→L2 message in the block it lands, guarding against a sequencer/prover state-root mismatch), `upload_failed_proof` (a failed proving job's state is uploaded and re-run on a fresh instance), `prover_restart.parallel` (a clean prover-node shutdown leaves its in-flight broker jobs untouched, and a restart against the same shared broker resumes proving from them rather than aborting and re-proving — one case gates the top tree and withholds the checkpoint-root proofs so those top-tree jobs are the in-flight, revived ones, the other starves agents from the start so real transaction base-rollup proofs are the in-flight, revived jobs). |
6262
| `prover/` | Real-proof exercises on the `FullProverTest` harness (real Barretenberg when `FAKE_PROOFS=0`, fake otherwise), split by where the proving runs so CI can size their containers independently: `client/client` (client-side proof generation and `verifyProof` for private and public transfers, no on-chain submission) and `server/full` (the end-to-end pipeline: client proves, node builds blocks, prover node generates epoch proofs, L1 verifies them). |
6363
| `partial-proofs/` | Manually driven partial-proof submission. `single_root` (the prover node's `startProof` path on a single root) and `multi_root` (three partial-proof roots are staged and messages consume against any covering root, exercising the multi-root Outbox semantics). |
6464
| `l1-reorgs/` | Behavior under L1 reorgs, split by what reorgs. `blocks.parallel` (prune L2 blocks when a reorg drops a proof, hold when a replacement proof lands in the window, restore blocks when a proof reappears, prune pending-chain blocks, and see new blocks added by a reorg) and `messages.parallel` (L1→L2 messages updated by a reorg, and a missed message inserted by one). `setup.ts` holds the shared `FAST_REORG_TIMING` profile and delayer wiring. |

yarn-project/end-to-end/src/single-node/proving/optimistic.parallel.test.ts

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -338,22 +338,20 @@ describe('single-node/proving/optimistic', () => {
338338
timeout: 30,
339339
});
340340

341-
// Verify the prover-node observes the prune. `markPruned()` fires reactively when
342-
// the L2BlockStream emits the prune; the SlotWatcher then reaps the (now pruned)
343-
// prover on its next tick (default 1s), so checking strictly for `isPruned()` would
344-
// race against the reap. Identify the original by `(checkpointNumber, slot)` —
345-
// checkpoint numbers refill sequentially after a reorg, so the replacement reuses
346-
// the same number but lives at a different slot. Accept either state for the
347-
// original: still in the store and pruned, or already reaped.
341+
// Verify the prover-node observes the prune. The prune reactively cancels and removes the
342+
// orphaned prover from the store when the L2BlockStream emits it, so the original should drop
343+
// out of the store (or, if observed mid-race, be cancelled). Identify the original by
344+
// `(checkpointNumber, slot)` — checkpoint numbers refill sequentially after a reorg, so the
345+
// replacement reuses the same number but lives at a different slot.
348346
await retryUntil(
349347
() => {
350348
const prover = proverNode
351349
.getCheckpointStore()
352350
.listAll()
353351
.find(p => p.checkpoint.number === checkpointBeforeReorg && p.slotNumber === originalSlot);
354-
return Promise.resolve(!prover || prover.isPruned());
352+
return Promise.resolve(!prover || prover.isCancelled());
355353
},
356-
`prover marks original checkpoint ${checkpointBeforeReorg} (slot ${originalSlot}) as pruned (or reaps it)`,
354+
`prover cancels and removes original checkpoint ${checkpointBeforeReorg} (slot ${originalSlot})`,
357355
30,
358356
0.2,
359357
);
@@ -383,7 +381,7 @@ describe('single-node/proving/optimistic', () => {
383381
proverNode
384382
.getCheckpointStore()
385383
.listAll()
386-
.some(p => p.checkpoint.number === replacementCheckpoint && !p.isPruned()),
384+
.some(p => p.checkpoint.number === replacementCheckpoint),
387385
),
388386
`prover re-creates sub-tree for replacement checkpoint ${replacementCheckpoint}`,
389387
30,
@@ -875,7 +873,7 @@ describe('single-node/proving/optimistic', () => {
875873
// The session manager constructs a full session over the canonical content for the
876874
// anchored epoch when it completes, then proves it; the store retains the provers
877875
// until expiry.
878-
const epochCheckpointsInStore = await proverNode.getCheckpointStore().listCanonicalForEpoch(epoch);
876+
const epochCheckpointsInStore = await proverNode.getCheckpointStore().listForEpoch(epoch);
879877
const storedNumbers = new Set(epochCheckpointsInStore.map(p => p.checkpoint.number));
880878
for (const n of preSpawnCheckpointNumbers) {
881879
expect(storedNumbers.has(n)).toBe(true);

0 commit comments

Comments
 (0)