Skip to content

Commit 8b2dab2

Browse files
spalladinoaztec-bot
authored andcommitted
perf(e2e): overlap and batch setup txs in e2e harnesses (#24569)
Stacked on #24564; retarget to `merge-train/spartan-v5` after it merges. (Unifies former #24584 into this PR — the two touched the same mint path in `fees_test.ts`, so they now land together.) Restructures the serial setup-tx chains in the fees and cross-chain e2e harnesses so independent setup transactions stop paying one production slot each: independent txs run concurrently, and same-sender calls are batched into a single tx (one proof, one tx, one slot). Part of the round-4/5 e2e speedup effort. ## Commits - **perf(e2e): overlap fees harness setup txs** — after `FeesTest.setup` deploys BananaCoin, the BananaFPC deploy, the SponsoredFPC registration (a PXE registration, no L2 tx), and Alice's banana mints each depend only on the token, so the fees suites (`account_init`, `gas_estimation`, `private_payments`) now run them under a `Promise.all`. The cross-chain harness (`deployAndInitializeTokenAndBridgeContracts`) deploys the L2 token and its bridge concurrently: the bridge takes the token address as a constructor arg, but that address is deterministic before the deploy tx mines (the deploy is now constructed with an explicit `deployer`, same address derivation as the previous send-time lock), and the bridge constructor only stores it without calling into the token. - **perf(e2e): parallelize dual token mints** + **perf(e2e): batch same-sender setup txs into single txs** — `applyFundAliceWithBananas` sent Alice's private and public banana mints back-to-back (measured 100% serial: `tx:mint` busyMs == totalMs on the base). The first commit made them concurrent; the second replaces the concurrency with a single `BatchCall([mint_to_private, mint_to_public])` tx, because measurement showed concurrent same-sender txs still pay *consecutive* slots: the PXE serializes `simulateTx`/`proveTx` on a single queue, so the second tx is only proven after the first and misses the first's slot. Overlapping cannot remove whole slots — only batching can. Both mints are Alice sends on `BananaCoin` touching disjoint balances; the private-balance before/after assertion is preserved, and the public mint's effect is checked by the downstream tests that spend Alice's public bananas. Precedent for the pattern already lives in this tree (`mintNotes` in `fixtures/token_utils.ts`). ## Dependency analysis - FPC deploy needs the token address (constructor arg) → runs after the token deploy, concurrent with the mint batch. - Mints need the token → concurrent with the FPC deploy, and the private/public mints are mutually independent (disjoint balances), so they share one tx. - SponsoredFPC setup is a wallet registration only → free to join the `Promise.all`. - TokenBridge's initializer stores `{token, portal}` in its own storage and never calls the token, so the bridge deploy does not need the token to be mined first. - #24564 invariants preserved: `BANANA_COIN_SALT`/`BANANA_FPC_SALT` + `deployer: alice` are untouched, and the genesis-funded-address assertion in `applyFPCSetup` still runs (and passed locally). ## Same-sender concurrency Concurrent `.send()` calls from the same sender through the same TestWallet are safe: the PXE serializes `simulateTx`/`proveTx` through a `SerialQueue` (`#putInJobQueue`), so simulations never interleave and only the block-inclusion waits overlap. The setup txs pay fees from Alice's public fee-juice balance (`PREEXISTING_FEE_JUICE`) with random tx nonces, so there are no nullifier or note conflicts between the concurrent txs. Verified locally with full `Promise.all` of the send+wait calls; no NO_WAIT fallback or cross-sender split was needed. ## Surveyed and skipped - Batching token class+instance publication: not applicable on this base — `DeployMethod.request()` already merges class publication, instance publication, and the constructor call into a single execution payload sent as one tx. Measured locally, `deploy:token` is a single ~12s (1 slot) occurrence; the ~24s occurrences in CI span data are inclusion latency at CI cadence, not a second tx. - `applyFPCSetup`: a single `deploy:fpc` tx — no same-sender pair inside the phase. Folding the FPC deploy into the mint batch (one class publication + app calls is within the class-log limit) would remove the remaining consecutive slot, but it merges two harness phases that four test files compose differently via `Promise.all`, and deploy-inside-BatchCall is not a proven pattern here — a restructure, not a mechanical merge. Left as a possible follow-up. - `applySponsoredFPCSetup`: no on-chain tx — it only calls `wallet.registerContract` locally; the sponsored FPC is genesis-funded and its class preloaded. Nothing to batch. - Two different contract deploys cannot share a tx (`MAX_CONTRACT_CLASS_LOGS_PER_TX = 1`), which rules out any two-deploy batch (e.g. cross-chain token+bridge) — those stay concurrent rather than batched. ## Local verification (12s slots; CI runs at its own cadence) Overlap commits (fees + cross-chain): - Base (`account_init`, spans on): 5/5 passed; beforeAll 83.5s = `setup:auth-registry` 32.0s + `deploy:token` 12.0s + `tx:mint` 23.9s (busyMs == totalMs, fully serial) + `deploy:fpc` 11.8s. - After overlap: 5/5 passed; beforeAll 72.2s (−11.3s); `deploy:fpc` fully overlapped with the mint window. - `gas_estimation`: 3/3 passed; `private_payments`: 8/8 passed; `token_bridge` (cross-chain overlap): 7/7 passed — `deploy:token` 12.1s and `deploy:bridge` 23.7s ran concurrently (union ≈ 23.7s vs ~48s serial on the CI baseline); `l2_to_l1`: 6/6 passed (regression check on the untouched path). Batch commit (`account_init`, 5/5 green on all runs): | run | tx:mint count | tx:mint busyMs | deploy:fpc busyMs | beforeHooksMs | |-----|---------------|----------------|-------------------|---------------| | before batch | 2 | 23618 | 23713 | 71607 | | run 1 (after) | 1 | 11834 | 24065 | 72372 | | run 2 (after) | 1 | 11807 | 23743 | 71678 | The mint phase drops from two consecutive slots (~23.6s) to one (~11.8s), one proof and one tx fewer per shard. No intermittent failures across any run. ## Measured impact Measured on CI in two steps (each commit group had its own PR run before unification). **Overlap** (PR run 1783394912998666 vs #24564's run 1783375878950264): −162s summed across shards. Per-suite beforeHooks: private_payments.parallel −16.0s (×8), gas_estimation.parallel −15.5s (×3), token_bridge −12.0s, fee_juice_payments −12.0s, account_init −11.5s. The overlap is real but partial: `tx:mint` busyMs/totalMs = 0.68 run-wide — concurrent txs land in consecutive slots because the PXE serializes proving (the finding that motivated the batch commit). Cross-chain `token_bridge` collapses its deploy phase from serial 35.9s to concurrent ~23.4s (union ≈ max, as designed). **Batch** (PR run 1783432285938536 vs 1783394912998666): `tx:mint` 42 → 29 occurrences (one merged per fees shard); busyMs 523s → 431s (−92s of tx/proving work; busyMs == totalMs in the PR run — no overlapping mint txs left to union). Wall-clock lands where the mint is the setup critical path: `failures` −12.0s, `fee_juice_payments` −11.5s (≈ one 12s slot each). Suites where the mint overlaps `applyFPCSetup` are flat (`account_init` −0.4s, `private_payments.parallel` +2s, `gas_estimation.parallel` +3s): `deploy:fpc` is unchanged (13 occurrences, ~2 slots each) and remains their critical path, so removing a slot from the mint branch of the `Promise.all` doesn't move the union. Run-wide sum over common suites: −8s; noise envelope on untouched suites ±20–37s. Net vs #24564's baseline: roughly −185s summed across shards, plus 13 fewer txs/proofs per run (less proving contention through the sequencer). The remaining ~12s/shard lever on the overlapped fees suites is folding the FPC deploy into the same batch, noted above as a follow-up. Fixes A-1406 Fixes A-1409 (cherry picked from commit 1d280af)
1 parent 0cd9ab1 commit 8b2dab2

5 files changed

Lines changed: 38 additions & 26 deletions

File tree

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

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -76,19 +76,17 @@ export async function deployAndInitializeTokenAndBridgeContracts(
7676
client: l1Client,
7777
});
7878

79-
// deploy l2 token
80-
const { contract: token } = await testSpan('deploy:token', () =>
81-
TokenContract.deploy(wallet, owner, 'TokenName', 'TokenSymbol', 18).send({
82-
from: owner,
83-
}),
84-
);
85-
86-
// deploy l2 token bridge and attach to the portal
87-
const { contract: bridge } = await testSpan('deploy:bridge', () =>
88-
TokenBridgeContract.deploy(wallet, token.address, tokenPortalAddress).send({
89-
from: owner,
90-
}),
91-
);
79+
// Deploy the L2 token and its bridge concurrently so they share a slot: the bridge takes the token
80+
// address as a constructor arg, but that address is known deterministically before the token deploy
81+
// mines, and the bridge's constructor only stores it without calling into the token.
82+
const tokenDeploy = TokenContract.deploy(wallet, owner, 'TokenName', 'TokenSymbol', 18, { deployer: owner });
83+
const tokenAddress = await tokenDeploy.getAddress();
84+
const [{ contract: token }, { contract: bridge }] = await Promise.all([
85+
testSpan('deploy:token', () => tokenDeploy.send({ from: owner })),
86+
testSpan('deploy:bridge', () =>
87+
TokenBridgeContract.deploy(wallet, tokenAddress, tokenPortalAddress).send({ from: owner }),
88+
),
89+
]);
9290

9391
if ((await token.methods.get_admin().simulate({ from: owner })).result !== owner.toBigInt()) {
9492
throw new Error(`Token admin is not ${owner}`);

yarn-project/end-to-end/src/single-node/fees/account_init.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,9 @@ describe('single-node/fees/account_init', () => {
3333

3434
beforeAll(async () => {
3535
await t.setup({ ...PIPELINING_SETUP_OPTS });
36-
await t.applyFundAliceWithBananas();
37-
await t.applyFPCSetup();
36+
// Alice's banana mints and the BananaFPC deploy each depend only on the BananaCoin deployed
37+
// during setup, so they run concurrently and share slots.
38+
await Promise.all([t.applyFundAliceWithBananas(), t.applyFPCSetup()]);
3839
({ aliceAddress, wallet, bananaCoin, bananaFPC, logger, aztecNode } = t);
3940
});
4041

yarn-project/end-to-end/src/single-node/fees/fees_test.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { AztecAddress } from '@aztec/aztec.js/addresses';
2+
import { BatchCall } from '@aztec/aztec.js/contracts';
23
import { Fr } from '@aztec/aztec.js/fields';
34
import { createLogger } from '@aztec/aztec.js/log';
45
import type { AztecNode } from '@aztec/aztec.js/node';
@@ -408,12 +409,24 @@ export class FeesTest extends SingleNodeTestContext {
408409
public async applyFundAliceWithBananas() {
409410
this.logger.info('Applying fund Alice with bananas setup');
410411

411-
await this.mintPrivateBananas(this.ALICE_INITIAL_BANANAS, this.aliceAddress);
412+
// Both mints are alice sends on BananaCoin touching disjoint balances, so they ride a single
413+
// BatchCall tx (one proof, one slot) rather than two concurrent txs. The PXE serializes proving,
414+
// so two concurrent sends would still be proven back-to-back and land in consecutive slots.
415+
const { result: privateBalanceBefore } = await this.bananaCoin.methods
416+
.balance_of_private(this.aliceAddress)
417+
.simulate({ from: this.aliceAddress });
418+
412419
await testSpan('tx:mint', () =>
413-
this.bananaCoin.methods.mint_to_public(this.aliceAddress, this.ALICE_INITIAL_BANANAS).send({
414-
from: this.aliceAddress,
415-
}),
420+
new BatchCall(this.wallet, [
421+
this.bananaCoin.methods.mint_to_private(this.aliceAddress, this.ALICE_INITIAL_BANANAS),
422+
this.bananaCoin.methods.mint_to_public(this.aliceAddress, this.ALICE_INITIAL_BANANAS),
423+
]).send({ from: this.aliceAddress }),
416424
);
425+
426+
const { result: privateBalanceAfter } = await this.bananaCoin.methods
427+
.balance_of_private(this.aliceAddress)
428+
.simulate({ from: this.aliceAddress });
429+
expect(privateBalanceAfter).toEqual(privateBalanceBefore + this.ALICE_INITIAL_BANANAS);
417430
}
418431

419432
public async applyFundAliceWithPrivateBananas() {

yarn-project/end-to-end/src/single-node/fees/gas_estimation.parallel.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,9 @@ describe('single-node/fees/gas_estimation', () => {
4646

4747
beforeAll(async () => {
4848
await t.setup({ ...PIPELINING_SETUP_OPTS });
49-
await t.applyFPCSetup();
50-
await t.applyFundAliceWithBananas();
49+
// Alice's banana mints and the BananaFPC deploy each depend only on the BananaCoin deployed
50+
// during setup, so they run concurrently and share slots.
51+
await Promise.all([t.applyFPCSetup(), t.applyFundAliceWithBananas()]);
5152
({ wallet, aliceAddress, bobAddress, bananaCoin, bananaFPC, gasSettings, logger, aztecNode } = t);
5253
});
5354

yarn-project/end-to-end/src/single-node/fees/private_payments.parallel.test.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,10 @@ describe('single-node/fees/private_payments', () => {
4646
// cycle: the prover-node submits a proof as soon as the epoch is complete, so ~8x shorter
4747
// epochs ≈ ~8x faster proof cadence per cycle. Setup itself stays slot-bound.
4848
await t.setup({ ...PIPELINING_SETUP_OPTS, aztecProofSubmissionEpochs: 640, aztecEpochDuration: 4 });
49-
await t.applyFPCSetup();
50-
// Register the SponsoredFPC (funded at genesis via FeesTest's fundSponsoredFPC) so the folded
51-
// sponsored-payment it can use it; this is a PXE registration, not an L2 tx.
52-
await t.applySponsoredFPCSetup();
53-
await t.applyFundAliceWithBananas();
49+
// The BananaFPC deploy, the SponsoredFPC registration (funded at genesis via FeesTest's
50+
// fundSponsoredFPC; a PXE registration, not an L2 tx), and Alice's banana mints each depend only
51+
// on the BananaCoin deployed during setup, so they run concurrently and share slots.
52+
await Promise.all([t.applyFPCSetup(), t.applySponsoredFPCSetup(), t.applyFundAliceWithBananas()]);
5453
({
5554
wallet,
5655
aliceAddress,

0 commit comments

Comments
 (0)