From 290245ebaf55a2522b2bb92bb2bbe7d5923f2878 Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Wed, 8 Jul 2026 07:31:52 -0300 Subject: [PATCH 1/7] perf(bot): parallelize independent bot factory setup steps (#24581) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Parallelizes the independent setup steps in the bot factory (`@aztec/bot`, `bot/src/factory.ts`). This is production bot code that runs against live networks, so the changes are strictly behavior-preserving: same contracts at the same (salt-derived) addresses, same amounts, same final state. Only the ordering/concurrency of provably-independent setup steps changes. The bot factory is the single largest cost in the `single-node/bot` e2e suite: each `*.create` runs the production sequencer at 12s L2 slots (`PIPELINING_SETUP_OPTS`) and deploys/mints one tx per slot, fully serial. `AmmBot.create` alone was ~74s (7 serial txs). ## Dependency graph per bot type Edges below are "must be mined before", verified against the Noir contract source (`token_contract`, `amm_contract`). ### Transaction bot -- `setup()` (unchanged) `setupAccount` -> register recipient -> `ensureFeeJuiceBalance` -> deploy token -> mint. This chain is fully data-dependent: funding gates the deploy, the deploy gates the mint. The recipient (`createSchnorrAccount`) is register-only (no tx), and the private+public mints are already batched into one tx. Nothing is independent, so `setup()` is left serial. ### AMM bot -- `setupAmm()` Steps: deploy token0 (D0), token1 (D1), LP token (DL), AMM (DA); `set_minter` granting the AMM rights over the LP token (SM); mint token0+token1 to the provider (M, one batched tx); `add_liquidity` (AL). - D0, D1, DL: independent -- distinct contracts, no cross-references. - DA: the AMM constructor only stores the three token addresses (no calls into the tokens). Those addresses are salt-derived, so DA needs only the (pre-derived) addresses, not the token deploys mined. - SM: `Token::set_minter` just writes `minters[amm] = true`; it does not call or validate the AMM contract. It needs the LP token deployed and the (derivable) AMM address only -- not the AMM deployed. The deployer is authorized because the Token constructor sets `minters[admin] = true`. - M: `mint_to_private`/`mint_to_public` require the caller be a minter; the deployer is admin=minter from the constructor, so the token0/token1 mints need only those tokens deployed (no `set_minter`). - AL: `add_liquidity` transfers token0/token1 from the provider (needs M for balances), mints LP tokens (needs SM so the AMM is an LP minter), and targets the AMM (needs DA). Restructured from 7 serial txs into 3 slot-phases: - Phase 1: `Promise.all([D0, D1, DL])` - Phase 2: `Promise.all([DA, SM, M])` - Phase 3: `AL` ### Cross-chain bot -- `setupCrossChain()` Steps: deploy TestContract (an L2 tx), seed N L1->L2 messages (L1 txs), wait for the first message ready. - The seeds reference only the L2 recipient (TestContract) address, which is salt-derived. L1->L2 messages are queued on L1 and do not require the L2 contract to exist yet; they are consumed later in `bot.run()`, after setup completes. So the L2 deploy and the L1 seeding are independent. - The L2 deploy pays via the L2 wallet/PXE account; the seeds use the bot's L1 client -- different accounts, so there is no L1 nonce interaction between them. Restructured to overlap the deploy with the seed loop: `Promise.all([deploy TestContract, seed loop])`, then the message-ready wait. ## What stayed serial, and why - The whole transaction-bot `setup()` (data-dependent chain, nothing independent). - `ensureFeeJuiceBalance` before every deploy (funding must precede spending). - `add_liquidity` after its mints + minter grant + AMM deploy. - The individual L1->L2 seeds inside the loop: they share the bot's single L1 account, so concurrent sends would race on the L1 nonce. The suite already documents this nonce hazard. Only the loop as a whole overlaps the (L2) TestContract deploy. ## Production safety / idempotent re-entry - Every deploy still goes through `registerOrDeployContract`, which checks `getContractMetadata(address).isContractPublished` per contract and only registers (no tx) if already deployed. If one parallel deploy fails and the others succeed, the next `*.create` recovers: the succeeded contracts register-only, the failed one redeploys. Addresses are salt-derived and identical across runs. - `set_minter` is idempotent (writes `true` again). The AMM path's mint and `add_liquidity` always run (no balance guard) -- unchanged from the previous `fundAmm`. - Same-sender concurrent `.send()`s are safe by construction: the PXE serializes simulate/prove through its `SerialQueue` and setup txs pay with random tx nonces. The bot runs the same `EmbeddedWallet` -> PXE path in the e2e suite and in production. - Deliberate, benign failure-path change: because Phase 2 runs DA/SM/M concurrently, a failure of one no longer prevents the others from being sent. Their effects (a deployed-but-unused AMM, a minter grant, an extra mint) are all idempotent-safe, and `add_liquidity` uses fixed amounts, so re-entry converges to the same final state. - `withNoMinTxsPerBlock` (the `flushSetupTransactions` save/zero/restore wrapper around each setup tx) was not safe to re-enter concurrently: interleaved save/zero/restore could read the already-zeroed value and "restore" `minTxsPerBlock` to 0 permanently. It now reference-counts entrants -- the first saves and zeroes, the last restores -- with unit tests covering the overlapping and failure paths (`bot/src/factory.test.ts`). Serial callers see the exact same RPC sequence as before. Final state is identical in every path: same contract addresses, same minter grant, same minted amounts, same liquidity. ## Local evidence Two full local runs of `single-node/bot/bot.test.ts` (production sequencer, 12s L2 slots), all 10 tests passing in both. Hook durations from factory log timestamps, against the round-4 local baseline measured on the same machine at the same cadence (findings from #24534): - `Bot.create` (transaction-bot hook, untouched): 31.8s vs 32.2s baseline -- unchanged, as intended. - `AmmBot.create`: 57.2s vs 73.9s baseline (-16.7s). The three token deploy txs go out within 300ms of each other; the AMM deploy, `set_minter` and the mint batch all go out within the following slot. - `CrossChainBot.create`: 24.0s vs 43.5s baseline (-19.5s). The TestContract deploy overlaps both L1 seeds; the first message is ready almost immediately after the deploy is mined. - Suite-level `beforeHooksMs`: 116.9s vs 153.7s baseline (-36.8s), matching the per-hook deltas. - Unit: `bot/src/factory.test.ts` (4 tests) covering the `withNoMinTxsPerBlock` reentrancy fix. ## Expected effect on CI - `AmmBot.create`: 7 serial txs -> 3 slot-phases (~87s on CI per #24534's spans -> ~55-60s). - `CrossChainBot.create`: deploy overlaps the seed pipeline (~43s -> ~25s). - Transaction bot: unchanged. - Aggregate: roughly -35 to -45s on the bot suite's beforeAll wall time. - Fix-phase proof metric (once #24534's `setup:bot` instrumentation is on this base): the amm-bot and cross-chain `setup:bot` occurrences drop per the numbers above. Measured CI numbers to be appended when a full run lands. ## Measured impact The bot suite (a single shard) drops 216.3s → 118.3s total (**−98.0s, −45%**); beforeHooks 215.9s → 118.0s. - No `setup:bot` spans exist on this base (that instrumentation lives in the unmerged #24534), so the metric is the suite-level hook fold, which covers all three nested bot factory setups (Bot, AmmBot, CrossChainBot). - Noise context from the same run pair: untouched light suites move −2 to −3s per shard (private_initialization −2.9s/shard over 20 shards, deploy_method −2.1s/shard over 14), and the largest counter-move is validators_sentinel +24s (multi-node variance). A −98s single-shard delta is far outside that envelope. - The CI delta exceeds the local −36.8s because the CI baseline pays more missed-slot penalties per serial tx (baseline beforeHooks 215.9s on CI vs 153.7s locally at the same 12s cadence); collapsing 7 serial txs into 3 slot-phases removes proportionally more where slots are more often missed. Baseline CI run 1783393028773172 (merge-base 30966a45, full). PR CI run 1783427250198215 (x-fast). Fixes A-1408 (cherry picked from commit 5d2b6dfbafb09b6c76ba5fdb4a09f327ddde132c) --- yarn-project/bot/src/factory.test.ts | 87 ++++++++++ yarn-project/bot/src/factory.ts | 231 ++++++++++++++------------- 2 files changed, 211 insertions(+), 107 deletions(-) create mode 100644 yarn-project/bot/src/factory.test.ts diff --git a/yarn-project/bot/src/factory.test.ts b/yarn-project/bot/src/factory.test.ts new file mode 100644 index 000000000000..936cfc6ed233 --- /dev/null +++ b/yarn-project/bot/src/factory.test.ts @@ -0,0 +1,87 @@ +import { promiseWithResolvers } from '@aztec/foundation/promise'; +import type { AztecNode, AztecNodeAdmin, AztecNodeAdminConfig } from '@aztec/stdlib/interfaces/client'; +import type { EmbeddedWallet } from '@aztec/wallets/embedded'; + +import { mock } from 'jest-mock-extended'; + +import { type BotConfig, getBotDefaultConfig } from './config.js'; +import { BotFactory } from './factory.js'; +import type { BotStore } from './store/index.js'; + +class TestBotFactory extends BotFactory { + public override withNoMinTxsPerBlock(fn: () => Promise): Promise { + return super.withNoMinTxsPerBlock(fn); + } +} + +describe('BotFactory.withNoMinTxsPerBlock', () => { + let minTxsPerBlock: number | undefined; + let setConfigCalls: (number | undefined)[]; + let factory: TestBotFactory; + + beforeEach(() => { + minTxsPerBlock = 3; + setConfigCalls = []; + + const aztecNodeAdmin = mock(); + aztecNodeAdmin.getConfig.mockImplementation(() => Promise.resolve({ minTxsPerBlock } as AztecNodeAdminConfig)); + aztecNodeAdmin.setConfig.mockImplementation(config => { + setConfigCalls.push(config.minTxsPerBlock); + minTxsPerBlock = config.minTxsPerBlock; + return Promise.resolve(); + }); + + const config: BotConfig = { ...getBotDefaultConfig(), flushSetupTransactions: true }; + const wallet = mock(); + factory = new TestBotFactory(config, wallet, mock(), mock(), aztecNodeAdmin); + }); + + it('zeroes minTxsPerBlock around a call and restores it after', async () => { + await factory.withNoMinTxsPerBlock(() => { + expect(minTxsPerBlock).toBe(0); + return Promise.resolve(); + }); + + expect(setConfigCalls).toEqual([0, 3]); + expect(minTxsPerBlock).toBe(3); + }); + + it('zeroes once and restores once across overlapping calls', async () => { + const gates = [promiseWithResolvers(), promiseWithResolvers(), promiseWithResolvers()]; + const calls = gates.map(gate => factory.withNoMinTxsPerBlock(() => gate.promise)); + + gates[0].resolve(); + await calls[0]; + expect(minTxsPerBlock).toBe(0); + + gates[1].resolve(); + await calls[1]; + expect(minTxsPerBlock).toBe(0); + + gates[2].resolve(); + await calls[2]; + expect(setConfigCalls).toEqual([0, 3]); + expect(minTxsPerBlock).toBe(3); + }); + + it('restores minTxsPerBlock when the wrapped call fails', async () => { + await expect(factory.withNoMinTxsPerBlock(() => Promise.reject(new Error('boom')))).rejects.toThrow('boom'); + + expect(setConfigCalls).toEqual([0, 3]); + expect(minTxsPerBlock).toBe(3); + }); + + it('restores minTxsPerBlock when one of several overlapping calls fails', async () => { + const gate = promiseWithResolvers(); + const failing = factory.withNoMinTxsPerBlock(() => Promise.reject(new Error('boom'))); + const succeeding = factory.withNoMinTxsPerBlock(() => gate.promise); + + await expect(failing).rejects.toThrow('boom'); + expect(minTxsPerBlock).toBe(0); + + gate.resolve(); + await succeeding; + expect(setConfigCalls).toEqual([0, 3]); + expect(minTxsPerBlock).toBe(3); + }); +}); diff --git a/yarn-project/bot/src/factory.ts b/yarn-project/bot/src/factory.ts index d720ccdb83a2..19eac4f95e0b 100644 --- a/yarn-project/bot/src/factory.ts +++ b/yarn-project/bot/src/factory.ts @@ -44,6 +44,11 @@ const FEE_JUICE_TOP_UP_THRESHOLD = 100n * 10n ** 18n; export class BotFactory { private log = createLogger('bot'); + /** Number of in-flight withNoMinTxsPerBlock calls; see that method for why they are counted. */ + private noMinTxsPerBlockDepth = 0; + /** Set by the first withNoMinTxsPerBlock entrant; resolves to the minTxsPerBlock value to restore. */ + private savedMinTxsPerBlock?: Promise<{ minTxsPerBlock?: number }>; + constructor( private readonly config: BotConfig, private readonly wallet: EmbeddedWallet, @@ -86,23 +91,35 @@ export class BotFactory { }> { const defaultAccountAddress = await this.setupAccount(); await this.ensureFeeJuiceBalance(defaultAccountAddress); - const token0 = await this.setupTokenContract(defaultAccountAddress, this.config.tokenSalt, 'BotToken0', 'BOT0'); - const token1 = await this.setupTokenContract(defaultAccountAddress, this.config.tokenSalt, 'BotToken1', 'BOT1'); - const liquidityToken = await this.setupTokenContract( - defaultAccountAddress, - this.config.tokenSalt, - 'BotLPToken', - 'BOTLP', - ); - const amm = await this.setupAmmContract( - defaultAccountAddress, - this.config.tokenSalt, - token0, - token1, - liquidityToken, - ); - await this.fundAmm(defaultAccountAddress, defaultAccountAddress, amm, token0, token1, liquidityToken); + const salt = this.config.tokenSalt; + + // token0, token1 and the LP token are independent contracts with no shared state, so deploy them + // concurrently rather than one slot at a time. + const [token0, token1, liquidityToken] = await Promise.all([ + this.setupTokenContract(defaultAccountAddress, salt, 'BotToken0', 'BOT0'), + this.setupTokenContract(defaultAccountAddress, salt, 'BotToken1', 'BOT1'), + this.setupTokenContract(defaultAccountAddress, salt, 'BotLPToken', 'BOTLP'), + ]); + + const ammDeploy = AMMContract.deploy(this.wallet, token0.address, token1.address, liquidityToken.address, { + salt, + universalDeploy: true, + }); + const ammAddress = (await ammDeploy.getInstance()).address; + + // The AMM constructor only stores the (already-derived) token addresses, and set_minter only records + // the AMM address on the LP token: neither reads the other's on-chain state, so the AMM deploy, the + // LP-minter grant, and the token0/token1 mints are mutually independent and run concurrently. + const [amm] = await Promise.all([ + this.deployAmmContract(defaultAccountAddress, ammDeploy), + this.grantLpTokenMinter(defaultAccountAddress, liquidityToken, ammAddress), + this.mintAmmLiquidity(defaultAccountAddress, token0, token1), + ]); + + // add_liquidity spends the minted token0/token1 balances and mints LP tokens, so it must follow both + // the mints and the minter grant, and target the deployed AMM. + await this.addAmmLiquidity(defaultAccountAddress, defaultAccountAddress, amm, token0, token1, liquidityToken); this.log.info(`AMM initialized and funded`); return { wallet: this.wallet, defaultAccountAddress, amm, token0, token1, node: this.aztecNode }; @@ -140,25 +157,32 @@ export class BotFactory { const rollupContract = new RollupContract(l1Client, l1ContractAddresses.rollupAddress.toString()); const rollupVersion = await rollupContract.getVersion(); - // Deploy TestContract (pays from the standing balance funded above). - const contract = await this.setupTestContract(defaultAccountAddress); + // Derive the TestContract address up front (deterministic from the salt). Seeding L1→L2 messages only + // needs the L2 recipient address — the messages are queued on L1 and don't require the L2 contract to + // exist yet (they're consumed later, after setup completes) — so the deploy (an L2 tx paying from the + // standing balance funded above) and the L1 seeding run concurrently. + const testContractDeploy = TestContract.deploy(this.wallet, { + salt: this.config.tokenSalt, + universalDeploy: true, + }); + const contractAddress = (await testContractDeploy.getInstance()).address; // Recover any pending messages from store (clean up stale ones first) await this.store.cleanupOldPendingMessages(); const pendingMessages = await this.store.getUnconsumedL1ToL2Messages(); - // Seed initial L1→L2 messages if pipeline is empty + // Seed initial L1→L2 messages if pipeline is empty. The seeds are sent one at a time: they share the + // bot's L1 account, so concurrent sends would race on the L1 nonce. const seedCount = Math.max(0, this.config.l1ToL2SeedCount - pendingMessages.length); - for (let i = 0; i < seedCount; i++) { - await seedL1ToL2Message( - l1Client, - EthAddress.fromString(l1ContractAddresses.inboxAddress.toString()), - contract.address, - rollupVersion, - this.store, - this.log, - ); - } + const inboxAddress = EthAddress.fromString(l1ContractAddresses.inboxAddress.toString()); + const [contract] = await Promise.all([ + this.deployTestContract(defaultAccountAddress, testContractDeploy), + (async () => { + for (let i = 0; i < seedCount; i++) { + await seedL1ToL2Message(l1Client, inboxAddress, contractAddress, rollupVersion, this.store, this.log); + } + })(), + ]); // Block until at least one message is ready const allMessages = await this.store.getUnconsumedL1ToL2Messages(); @@ -182,10 +206,8 @@ export class BotFactory { }; } - private async setupTestContract(deployer: AztecAddress): Promise { - const deployOpts: DeployOptions = { from: deployer }; - const deploy = TestContract.deploy(this.wallet, { salt: this.config.tokenSalt, universalDeploy: true }); - const instance = await this.registerOrDeployContract('TestContract', deploy, deployOpts); + private async deployTestContract(deployer: AztecAddress, deploy: DeployMethod): Promise { + const instance = await this.registerOrDeployContract('TestContract', deploy, { from: deployer }); return TestContract.at(instance.address, this.wallet); } @@ -289,34 +311,37 @@ export class BotFactory { return TokenContract.at(instance.address, this.wallet); } - private async setupAmmContract( - deployer: AztecAddress, - salt: Fr, - token0: TokenContract, - token1: TokenContract, - lpToken: TokenContract, - ): Promise { - const deployOpts: DeployOptions = { from: deployer }; - const deploy = AMMContract.deploy(this.wallet, token0.address, token1.address, lpToken.address, { - salt, - universalDeploy: true, - }); - const instance = await this.registerOrDeployContract('AMM', deploy, deployOpts); + private async deployAmmContract(deployer: AztecAddress, deploy: DeployMethod): Promise { + const instance = await this.registerOrDeployContract('AMM', deploy, { from: deployer }); const amm = AMMContract.at(instance.address, this.wallet); - this.log.info(`AMM deployed at ${amm.address}`); - const setMinterInteraction = lpToken.methods.set_minter(amm.address, true); - const { receipt: minterReceipt } = await setMinterInteraction.send({ + return amm; + } + + /** Grants the AMM minting rights over the LP token. set_minter only records the address, so it does not + * require the AMM contract to be deployed first. */ + private async grantLpTokenMinter(deployer: AztecAddress, lpToken: TokenContract, amm: AztecAddress): Promise { + const { receipt } = await lpToken.methods.set_minter(amm, true).send({ from: deployer, wait: { timeout: this.config.txMinedWaitSeconds }, }); - this.log.info(`Set LP token minter to AMM txHash=${minterReceipt.txHash.toString()}`); - this.log.info(`Liquidity token initialized`); + this.log.info(`Set LP token minter to AMM txHash=${receipt.txHash.toString()}`); + } - return amm; + private async mintAmmLiquidity(minter: AztecAddress, token0: TokenContract, token1: TokenContract): Promise { + this.log.info(`Minting ${MINT_BALANCE} tokens of each BotToken0 and BotToken1 for ${minter}`); + const mintBatch = new BatchCall(this.wallet, [ + token0.methods.mint_to_private(minter, MINT_BALANCE), + token1.methods.mint_to_private(minter, MINT_BALANCE), + ]); + const { receipt } = await mintBatch.send({ + from: minter, + wait: { timeout: this.config.txMinedWaitSeconds }, + }); + this.log.info(`Sent mint tx: ${receipt.txHash.toString()}`); } - private async fundAmm( + private async addAmmLiquidity( defaultAccountAddress: AztecAddress, liquidityProvider: AztecAddress, amm: AMMContract, @@ -324,22 +349,6 @@ export class BotFactory { token1: TokenContract, lpToken: TokenContract, ): Promise { - const getPrivateBalances = () => - Promise.all([ - token0.methods - .balance_of_private(liquidityProvider) - .simulate({ from: liquidityProvider }) - .then(r => r.result), - token1.methods - .balance_of_private(liquidityProvider) - .simulate({ from: liquidityProvider }) - .then(r => r.result), - lpToken.methods - .balance_of_private(liquidityProvider) - .simulate({ from: liquidityProvider }) - .then(r => r.result), - ]); - const authwitNonce = Fr.random(); // keep some tokens for swapping @@ -348,12 +357,6 @@ export class BotFactory { const amount1Max = MINT_BALANCE / 2; const amount1Min = MINT_BALANCE / 4; - const [t0Bal, t1Bal, lpBal] = await getPrivateBalances(); - - this.log.info( - `Minting ${MINT_BALANCE} tokens of each BotToken0 and BotToken1. Current private balances of ${liquidityProvider}: token0=${t0Bal}, token1=${t1Bal}, lp=${lpBal}`, - ); - // Add authwitnesses for the transfers in AMM::add_liquidity function const token0Authwit = await this.wallet.createAuthWit(defaultAccountAddress, { caller: amm.address, @@ -378,36 +381,33 @@ export class BotFactory { .getFunctionCall(), }); - const mintBatch = new BatchCall(this.wallet, [ - token0.methods.mint_to_private(liquidityProvider, MINT_BALANCE), - token1.methods.mint_to_private(liquidityProvider, MINT_BALANCE), - ]); - const { receipt: mintReceipt } = await mintBatch.send({ - from: liquidityProvider, - wait: { timeout: this.config.txMinedWaitSeconds }, - }); - - this.log.info(`Sent mint tx: ${mintReceipt.txHash.toString()}`); - - const addLiquidityInteraction = amm.methods.add_liquidity( - amount0Max, - amount1Max, - amount0Min, - amount1Min, - authwitNonce, - ); - const { receipt: addLiquidityReceipt } = await addLiquidityInteraction.send({ - from: liquidityProvider, - authWitnesses: [token0Authwit, token1Authwit], - wait: { timeout: this.config.txMinedWaitSeconds }, - }); + const { receipt } = await amm.methods + .add_liquidity(amount0Max, amount1Max, amount0Min, amount1Min, authwitNonce) + .send({ + from: liquidityProvider, + authWitnesses: [token0Authwit, token1Authwit], + wait: { timeout: this.config.txMinedWaitSeconds }, + }); - this.log.info(`Sent tx to add liquidity to the AMM: ${addLiquidityReceipt.txHash.toString()}`); + this.log.info(`Sent tx to add liquidity to the AMM: ${receipt.txHash.toString()}`); this.log.info(`Liquidity added`); - const [newT0Bal, newT1Bal, newLPBal] = await getPrivateBalances(); + const [t0Bal, t1Bal, lpBal] = await Promise.all([ + token0.methods + .balance_of_private(liquidityProvider) + .simulate({ from: liquidityProvider }) + .then(r => r.result), + token1.methods + .balance_of_private(liquidityProvider) + .simulate({ from: liquidityProvider }) + .then(r => r.result), + lpToken.methods + .balance_of_private(liquidityProvider) + .simulate({ from: liquidityProvider }) + .then(r => r.result), + ]); this.log.info( - `Updated private balances of ${defaultAccountAddress} after minting and funding AMM: token0=${newT0Bal}, token1=${newT1Bal}, lp=${newLPBal}`, + `Updated private balances of ${defaultAccountAddress} after minting and funding AMM: token0=${t0Bal}, token1=${t1Bal}, lp=${lpBal}`, ); } @@ -590,19 +590,36 @@ export class BotFactory { return claim as L2AmountClaim; } - private async withNoMinTxsPerBlock(fn: () => Promise): Promise { + protected async withNoMinTxsPerBlock(fn: () => Promise): Promise { if (!this.aztecNodeAdmin || !this.config.flushSetupTransactions) { this.log.verbose(`No node admin client or flushing not requested (not setting minTxsPerBlock to 0)`); return fn(); } - const { minTxsPerBlock } = await this.aztecNodeAdmin.getConfig(); - this.log.warn(`Setting sequencer minTxsPerBlock to 0 from ${minTxsPerBlock} to flush setup transactions`); - await this.aztecNodeAdmin.setConfig({ minTxsPerBlock: 0 }); + const aztecNodeAdmin = this.aztecNodeAdmin; + // Setup steps run concurrently, so this wrapper can be re-entered while another call is in flight. + // Reference-count the entrants: the first saves the current value and zeroes it, the last restores it. + // A naive save/zero/restore per call could interleave, with a late entrant reading the already-zeroed + // value and "restoring" 0 at the end. + if (this.noMinTxsPerBlockDepth++ === 0) { + this.savedMinTxsPerBlock = (async () => { + const { minTxsPerBlock } = await aztecNodeAdmin.getConfig(); + this.log.warn(`Setting sequencer minTxsPerBlock to 0 from ${minTxsPerBlock} to flush setup transactions`); + await aztecNodeAdmin.setConfig({ minTxsPerBlock: 0 }); + return { minTxsPerBlock }; + })(); + } try { + await this.savedMinTxsPerBlock; return await fn(); } finally { - this.log.warn(`Restoring sequencer minTxsPerBlock to ${minTxsPerBlock}`); - await this.aztecNodeAdmin.setConfig({ minTxsPerBlock }); + if (--this.noMinTxsPerBlockDepth === 0) { + // If saving/zeroing itself failed there is nothing to restore. + const saved = await this.savedMinTxsPerBlock!.catch(() => undefined); + if (saved) { + this.log.warn(`Restoring sequencer minTxsPerBlock to ${saved.minTxsPerBlock}`); + await aztecNodeAdmin.setConfig({ minTxsPerBlock: saved.minTxsPerBlock }); + } + } } } } From 5b761980fa639e840f6148f102a5bfffe60e28a7 Mon Sep 17 00:00:00 2001 From: Aztec Bot <49558828+AztecBot@users.noreply.github.com> Date: Wed, 8 Jul 2026 14:01:00 -0400 Subject: [PATCH 2/7] docs: revert threat model for node (#24465) (#24610) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removes `yarn-project/THREAT_MODEL.md`, added in [#24465](https://github.com/AztecProtocol/aztec-packages/pull/24465). Per discussion in #team-alpha: detailed threat models and invariants may make it easier for attackers to find bugs, so the team wants this content pulled from the public repo for now. It will be re-added to LabsBox/ClaudeBox (private/internal) — see [claudebox#1357](https://github.com/AztecProtocol/claudebox/pull/1357) — and can come back here once the team is finding fewer bugs. Only the threat model doc itself is removed here — the incidental doc cleanups bundled into the same squashed commit (stale BLOCK protocol references in `yarn-project/p2p/README.md` and `yarn-project/p2p/src/services/reqresp/README.md`, and the missing `DUPLICATE_ATTESTATION` entry in `yarn-project/slasher/README.md`) are kept as-is. (cherry picked from commit 4d026929bc2f97638332f1bd19595de0c92f2828) --- yarn-project/THREAT_MODEL.md | 341 ----------------------------------- 1 file changed, 341 deletions(-) delete mode 100644 yarn-project/THREAT_MODEL.md diff --git a/yarn-project/THREAT_MODEL.md b/yarn-project/THREAT_MODEL.md deleted file mode 100644 index 1dee4b96cba8..000000000000 --- a/yarn-project/THREAT_MODEL.md +++ /dev/null @@ -1,341 +0,0 @@ -# Aztec Network Threat Model - -This document describes the threat model of the Aztec L2 network: how transactions flow from users into the proven -chain, what each participant can and cannot do, and the properties the implementation must uphold. It is intended as a -guideline for the security of the node implementation (`yarn-project`) and its interaction with the L1 rollup -contracts (`l1-contracts`). - -**In scope**: transaction dissemination and the mempool, the p2p layer, block and checkpoint production, committee -attestation, L1 checkpoint submission and sync, epoch proving, slashing, and the escape hatch. - -**Out of scope**: client-side private execution and proving (PXE, wallets), hardening of a node's public RPC interface, -the cryptographic soundness of the proving system itself (treated as an assumption below), and L1 governance internals. - -## 1. System overview - -### Actors - -| Actor | Role | -| --- | --- | -| User | Executes private functions locally, produces a client-side proof, submits the tx to a node via RPC. | -| Node | Syncs L2 state from L1 and p2p, maintains a mempool, serves RPC. Every other server-side actor runs one. | -| Proposer | The validator elected for a slot. Builds blocks, collects attestations, submits the checkpoint to L1. | -| Committee | Per-epoch sample of validators. Re-executes proposals and attests to checkpoints; its attestations gate proof acceptance (training wheels for the proving system, A2) and back data availability (A8). | -| Prover | Generates the epoch validity proof and submits it to L1. Permissionless. | -| Escape hatch proposer | Bonded candidate, randomly selected, may propose without a committee during periodic windows. | -| Vetoer | Designated L1 role that can block slash payloads during the execution delay. | -| L1 contracts | Rollup (checkpoints, proofs, pruning, invalidation), Inbox/Outbox (cross-chain messages), slashing and governance periphery. | - -### Time and validator selection - -Time is divided into **slots** (fixed windows, e.g. 72 s): each slot has exactly one elected proposer and produces at -most one checkpoint, built as several blocks in fixed sub-slots. Slots group into **epochs**: each epoch has one -committee, and epochs are the unit of proving and pruning. - -The committee for epoch N is sampled (Fisher–Yates, without replacement) from the registered validator set, seeded from -Ethereum's RANDAO. Both inputs are taken from the past: the validator set is snapshotted `lagInEpochsForValidatorSet` -epochs before N and the seed `lagInEpochsForRandao` epochs before N, with set lag ≥ seed lag — by the time the -randomness is known, the population it samples from can no longer be changed. L1 stores a commitment to each epoch's -committee to prevent substitution (`ValidatorSelectionLib`); nodes mirror the computation locally -([epoch-cache README](epoch-cache/README.md)). The proposer for a slot is -`keccak(epoch, slot, seed) % committeeSize` — deterministic and computable by anyone. - -Selection bias is an audit surface in its own right: seed grinding via L1 `prevrandao`, and timing games against the -validator-set snapshot. The lag scheme above and the escape hatch's snapshot-before-seed ordering are the existing -defenses. Edge case: an empty committee (target size 0) means anyone may propose, and nodes accept checkpoints without -attestation validation. - -### Transaction lifecycle - -1. **Submission.** A user sends a tx (with its client-side proof) to a node via JSON-RPC `sendTx`. The node fully - validates it — proof verification plus protocol rules (double-spend, fees, gas limits, expiration, metadata) — - before admitting it to the mempool ([server.ts](aztec-node/src/aztec-node/server.ts), - [tx validators](p2p/src/msg_validators/tx_validator/)). -2. **Propagation.** The node gossips the tx on the p2p `tx` topic. Every receiving node runs the same validation - pipeline *before* the message is re-propagated: gossipsub only forwards messages the local node accepted. Peers that - originate invalid data are penalized; validation outcomes that could be another node's fault are dropped without - penalty (see §6). -3. **Mempool.** The pool ([tx_pool_v2](p2p/src/mem_pools/tx_pool_v2/)) admits txs subject to nullifier-conflict, - fee-payer-balance, and priority rules, and evicts txs that become ineligible (nullifiers mined by a block, expired - timestamps, insufficient fee-payer balance, invalid anchor block after a reorg, lowest priority when full). -4. **Block building.** The proposer for a slot builds several blocks back-to-back (sub-slots), pulling txs from its - mempool and executing public calls against a fork of world state. Each block is signed and broadcast as a - `BlockProposal`; the last block ships inside the `CheckpointProposal` that closes the slot. Production runs - pipelined: blocks for slot N are built during slot N−1 ([sequencer-client README](sequencer-client/README.md)). -5. **Attestation.** Committee members re-execute every block in the proposal and, if the result matches, sign a - `CheckpointAttestation` over the checkpoint header and archive root. Attestations are checkpoint-only; individual - blocks are never attested ([validator-client README](validator-client/README.md)). -6. **L1 submission.** Once the proposer holds a quorum of attestations — ⌊2n/3⌋+1 of the committee — it submits the - checkpoint to the rollup contract in a single Multicall3 tx (invalidations first, then propose, then - governance/slashing votes). At propose time L1 validates the header, blob commitments, and the proposer signature, - but **not** the attestations (posted as calldata) and **not** tx validity (`ProposeLib`). -7. **Sync.** Nodes track two chains. The **proposed chain** comes from p2p: proposals are re-executed locally and - pushed into the archiver as provisional blocks. The **checkpointed (pending) chain** comes from L1 - `CheckpointProposed` events: each node verifies the posted attestations from calldata (committee membership and - quorum, per *delayed attestation verification*) before fetching and decoding blobs; checkpointed blocks are **not** - re-executed ([archiver README](archiver/README.md), [validation.ts](archiver/src/modules/validation.ts)). -8. **Proving.** Prover nodes prove epochs optimistically, starting sub-tree work as checkpoints land on L1. The rollup - accepts an epoch proof only if the proof verifies **and** the last checkpoint in the range carries valid committee - attestations (`EpochProofLib`). The proven tip then advances. If no proof - lands within the proof submission window, all unproven checkpoints are pruned — the pending chain reorgs back to the - proven tip; nodes unwind preemptively and the mempool resurrects the affected txs. -9. **Finality.** Proven state enables L2→L1 message consumption via the Outbox, and fees/rewards are distributed from - the committee-attested checkpoint headers. Once the L1 block containing the verified proof is itself finalized on - L1, the state can no longer be reorged out. - -### Chain states - -| Chain | Source | Trust | -| --- | --- | --- | -| Proposed | p2p proposals | Re-executed locally; trustless but reorgs freely within/across slots. | -| Checkpointed (pending) | L1 events + blobs | Attestation-gated; contents trusted from the committee (not re-executed). Reorgs on prune or invalidation. | -| Proven | L1 verified proof | Trust reduces to circuit soundness + L1 verifier. Still subject to L1 reorgs. | -| Finalized | Proof's L1 block finalized | The L1 block containing the verified proof is finalized; cannot be reorged out of L1. | - -### Where the details live - -| Topic | Document | -| --- | --- | -| Proposer flow, pipelining, timetable, L1 publisher | [sequencer-client README](sequencer-client/README.md) | -| Proposal validation, attestation creation, building limits | [validator-client README](validator-client/README.md) | -| Committee/proposer selection, RANDAO seed, epoch caching | [epoch-cache README](epoch-cache/README.md) | -| Gossip topics, message validation, peer scoring, req/resp | [p2p README](p2p/README.md) and sub-READMEs | -| Mempool state machine and eviction | [tx_pool_v2 README](p2p/src/mem_pools/tx_pool_v2/README.md) | -| L1 sync, reorgs, invalid checkpoints, pruning | [archiver README](archiver/README.md) | -| Epoch proving pipeline | [prover-node README](prover-node/README.md) | -| Slashing architecture and offenses | [slasher README](slasher/README.md) | -| Operator-facing slashing guide (amounts, veto, ejection) | [slashing configuration guide](../docs/docs-operate/operators/sequencer-management/slashing-configuration.md) | - -## 2. Trust assumptions - -- **A1 — Committee quorum.** Safety of the *pending* chain assumes fewer than ⌊2n/3⌋+1 members of any epoch committee - are malicious; liveness assumes at least ⌊2n/3⌋+1 are honest and online (`computeQuorum` in - [epoch-helpers](stdlib/src/epoch-helpers/), mirrored in `InvalidateLib`). -- **A2 — Proven-chain soundness.** It must be impossible to prove an invalid state transition: the *proven* chain - assumes the protocol circuits are sound and the L1 verifier is correct, without relying on committee honesty. The - committee is nevertheless a second, independent gate: proof submission requires committee attestations on the proven - range (V4), so it acts as training wheels for the proving system — exploiting a soundness bug also requires a - colluding quorum (or an escape-hatch bond, §7). Invalid state reaches the proven chain only if *both* layers fail. -- **A3 — Completeness.** Every state transition the network considers valid must be provable. An unprovable-but-attested - checkpoint forces a prune, so a completeness bug converts into a liveness attack. -- **A4 — Prover liveness.** At least one prover is willing and able to prove each epoch; otherwise the chain reorgs at - the proof submission deadline. -- **A5 — L1 liveness.** Ethereum remains live and censorship-resistant enough for time-windowed actions to land: - checkpoint proposal, invalidation, proof submission, slashing votes and execution. -- **A6 — Blind checkpoint trust.** Nodes follow attested checkpoints without re-executing them. A committee quorum can - therefore feed nodes invalid pending state (bounded by A2 + pruning). This assumption may be revisited. -- **A7 — Uniform validation config.** Slashing relies on all honest validators making identical deterministic validity - decisions; operators must not run divergent block/checkpoint validation limits. -- **A8 — Data availability.** Checkpoint effects are published as L1 blobs; tx preimages needed for proving are served - over p2p. Committees are slashed if the data behind their attested checkpoints is not made available. - -## 3. Threat actors and worst-case impact - -| Actor | Can do (accepted worst case) | Cannot do (must hold) | Mitigations | -| --- | --- | --- | --- | -| Malicious user | Submit txs that later become ineligible (e.g. nullifier races); attempt mempool flooding | Saturate mempools with unincludable txs; get an invalid tx included; force an unprovable state transition | Full validation incl. proof verification at every hop; eviction rules; priority fees + price-bump replacement; per-tx gas floors | -| Malicious peer (non-validator) | Send garbage, replay, or spam over gossip/req-resp | Get invalid data re-broadcast by honest nodes; cause honest nodes to be penalized by their peers; eclipse a node cheaply | Validate-before-forward; peer scoring, rate limits, bans (§6); AUTH handshake on nodes that only accept validator peers | -| Malicious proposer | Censor txs; waste its own slot and the next one (pipelining); post an unattested/badly-attested checkpoint to L1; equivocate | Convince honest nodes of an invalid state transition; corrupt state beyond slot N+1; brick other nodes' sync | Re-execution before attestation/adoption; pipeline depth capped at 2; delayed attestation verification + permissionless invalidation; slashing (§5) | -| Committee minority (< quorum, not proposer) | Withhold their own attestations; equivocate (slashable) | Prevent a slot from being attested; get honest members slashed | Quorum only needs ⌊2n/3⌋+1 of n; equivocation slashing | -| Committee quorum (≥ ⌊2n/3⌋+1 malicious) | Post invalid-but-attested checkpoints that nodes follow blindly; withhold tx data; halt the pending chain until the epoch prune | Get invalid state proven or exited on L1 (A2); avoid slashing for data withholding / attested-invalid | Unprovable state → prune at proof-window expiry; archiver preemptive unwind; slashing incl. `DATA_WITHHOLDING` | -| Malicious prover | Nothing by proving (proofs are verified); grief by *not* proving | Prove an invalid transition (A2); steal fees/rewards via forged proof calldata | Proof verification; attestation gate at proof submission (A2); fees bound to attested headers (see §4.4); A4 for liveness | -| Escape hatch proposer | Same as a malicious committee during its hatch window: post arbitrary unattested checkpoints, halt pending chain until prune | Exceed malicious-committee damage; escape its bond | Bond + punishment for failing to propose-and-prove; hatch windows are bounded (`ACTIVE_DURATION` out of every `FREQUENCY` epochs) | -| Malicious slashing majority | Vote through an unfair slash (requires >50% of a round's proposers) | Execute it silently or instantly | Execution delay + vetoer; offense votes are public on L1 | - -## 4. Security properties - -Properties are numbered for reference from audit findings. Each states the requirement, the enforcing mechanism, and -notable caveats. - -### 4.1 P2P and mempool - -- **P1 — Validate before forward.** No node re-broadcasts a gossip message it has not fully validated (including tx - proof verification). Enforced by running validation inside the gossipsub message-validation hook; only `Accept` - results propagate. -- **P2 — No penalty for relayed faults.** A peer must not be penalized for forwarding data that was plausibly valid - from its point of view. Encoded three ways: (a) tx validation splits outcomes into `REJECT` + penalty for - sender-attributable faults (malformed data, invalid proof) vs `IGNORE`/low-severity for state-divergence-explainable - ones (recent double-spend, timestamp expiry); (b) proposal gossip validators only run shallow, syntactic checks - (signature, slot window, proposer identity, tx-hash consistency) — deep re-execution failures are attributed to the - *proposer* via slashing and never penalize the relaying peer; (c) equivocating or oversized proposals are - deliberately `Accept`ed and re-broadcast as slashing evidence so the network can witness the offense, again without - penalizing relayers. A malicious node must not be able to craft data that honest nodes accept, re-broadcast, and are - then penalized for. -- **P3 — Mempool inclusion-eligibility.** Every tx in the pending pool must be includable in a future block; txs - invalidated by chain progress are evicted (mined nullifiers, expiry, fee-payer balance, reorged anchor blocks). - Accepted residual: executing a tx inside a block can invalidate sibling txs mid-slot (nullifier collision); at least - one tx of any colliding set is includable, and the builder drops the rest during execution. -- **P4 — Flood resistance.** Pool size is capped with priority-fee eviction; replacement requires a configurable price - bump; gossip messages have per-topic size caps; req/resp protocols have per-peer and global rate limits (§6). -- **P5 — Sybil/eclipse resistance.** Peer discovery (discv5), connection gating, auth handshakes on nodes that only - accept validator peers (`p2pAllowOnlyValidators`, off by default; no such nodes are deployed today), failed-auth - lockouts, and IP colocation penalties raise the cost of surrounding a node. This is a defense-in-depth area, not an - absolute guarantee. - -### 4.2 Block proposal and attestation - -- **B1 — Proposer exclusivity.** Only the slot's elected proposer can land a checkpoint: L1 verifies the proposer - signature over the payload digest at propose time (`ValidatorSelectionLib.verifyProposer`); validators independently - check proposal provenance before processing. During hatch windows, only the designated escape hatch proposer may - propose. -- **B2 — Re-execution gate.** Honest validators attest only after re-executing all blocks in the checkpoint and - matching the resulting state (archive root, header hash). Honest full nodes adopt proposed blocks into their view - only via the same re-execution path. A proposal that fails validation is rejected and triggers a slash vote - (`BROADCASTED_INVALID_BLOCK_PROPOSAL` / `BROADCASTED_INVALID_CHECKPOINT_PROPOSAL`). -- **B3 — Equivocation detection.** Two proposals for the same position with different content, or two attestations by - the same signer for the same slot, are slashable (`DUPLICATE_PROPOSAL`, `DUPLICATE_ATTESTATION`). The first duplicate - proposal is deliberately propagated so other validators can witness the offense. Checkpoint-vs-L1 equivocation is - caught by comparing signed p2p proposals against L1-confirmed checkpoints. -- **B4 — Bounded proposer blast radius.** Pipelining lets a proposer build on an in-flight parent, so a failed or - malicious slot can waste the *next* proposer's work, but no more: pipeline depth is capped at 2 checkpoints beyond the - confirmed tip, and a parent that fails to land cleanly causes the child's work to be discarded and an invalidation to - be enqueued ([checkpoint_proposal_job.ts](sequencer-client/src/sequencer/checkpoint_proposal_job.ts)). -- **B5 — Attestation quorum.** A checkpoint is valid only with ⌊2n/3⌋+1 committee signatures over the consensus payload - (EIP-712, slot-bound). Enforced off-chain by every syncing node, and on-chain at proof submission and in - `invalidateInsufficientAttestations`. - -### 4.3 Checkpointed chain and delayed attestation verification - -L1 does **not** verify committee attestations at propose time (gas optimization). The security burden moves to: - -- **C1 — Nodes never follow bad-attestation checkpoints.** Every node validates attestations (signatures, committee - membership at the correct index, quorum) from L1 **calldata** before fetching or decoding blobs, so a checkpoint with - invalid attestations is rejected without touching potentially malformed blob data - ([validation.ts](archiver/src/modules/validation.ts)). -- **C2 — Sync never bricks.** The archiver skips invalid checkpoints, advances its syncpoint past them, and keeps - processing. The `inHash` check cannot be weaponized: L1 enforces `header.inHash == inbox.consume(...)` at propose - time, so a local mismatch indicates a node bug, not attacker-controlled input (`ProposeLib`). -- **C3 — Bad-attestation checkpoints are removable and unprovable.** Anyone can call `invalidateBadAttestation` / - `invalidateInsufficientAttestations` to purge them (no rebate; expected callers: next proposer, then committee, then - any validator, with timed fallbacks in the sequencer). They cannot enter the proven chain: `submitEpochRootProof` - re-verifies the last checkpoint's attestations (`InvalidateLib`, `EpochProofLib`). -- **C4 — Posting bad attestations is punished.** `PROPOSED_INSUFFICIENT_ATTESTATIONS`, - `PROPOSED_INCORRECT_ATTESTATIONS`, and `PROPOSED_DESCENDANT_OF_CHECKPOINT_WITH_INVALID_ATTESTATIONS` target the - publishing proposer (only the one who publishes to L1 — a pipelined builder that discarded its work is not at fault). -- **C5 — Malicious-quorum damage is time-bounded.** Invalid-but-attested state persists at most until the epoch's proof - submission window expires, at which point the rollup prunes on the next propose and nodes preemptively unwind - (`canPruneAtTime`). Honest checkpoints built on top are collateral damage of the prune (accepted). - -### 4.4 Proving - -- **V1 — Soundness (A2).** No prover input may yield an accepted proof of an invalid transition. Circuit-level; out of - scope here but the anchor for everything above. -- **V2 — Completeness (A3).** Anything the network accepts as valid must be provable. Consequence for node config: - restrictions enforced only in validator software (e.g. block/tx caps) must never make circuit-valid state unprovable - or unsyncable — see S1. -- **V3 — Prune on missing proof.** Epochs unproven past the window are removed; the chain falls back to the proven tip. - This is the designed recovery from both prover outages and malicious-quorum garbage. -- **V4 — Unsound-verifier blast radius.** Defense in depth for a hypothetical L1 verifier bug: (a) epoch-proof fee - data is not free calldata — fees and rewards derive from checkpoint headers that L1 re-hashes against the - committee-attested header hashes, so a forged proof cannot redirect or inflate fees; (b) proof submission - requires valid committee attestations on the range's last checkpoint, so a lone prover cannot promote arbitrary state - — it needs a colluding quorum (or a hatch window, see §7). This is the committee's training-wheels role from A2. - Residual exposure: Outbox withdrawals from a maliciously-proven state; tracked as a known gap in §7. - -### 4.5 L1 sync completeness - -- **S1 — Sync everything provable.** Nodes must be able to sync from L1 any checkpoint that provers can prove, even if - local validator policy would have refused to attest to it. Validator-side caps (`VALIDATOR_MAX_*`) apply only to p2p - proposal validation; the archiver's L1 path enforces only attestation validity, `inHash` consistency, and structural - blob decoding. Rationale: escape-hatch and malicious-quorum checkpoints bypass validator policy but can still reach - the proven chain, and a node that refuses to sync them forks itself off. -- **S2 — L1 reorg resilience.** Message and checkpoint sync detect L1 reorgs (rolling hashes, archive root comparison), - unwind to the common ancestor, and re-fetch — including the subtle case of checkpoints added *behind* the syncpoint. - See [archiver README](archiver/README.md) § Edge Cases. - -### 4.6 Slashing fairness - -- **F1 — Honest validators are never slashable.** Every offense requires either the offender's own signature - (proposals, attestations — EIP-712 slot-bound, so replay across slots is impossible) or sustained, locally-observable - inactivity. An honest validator signs only what it built or successfully re-executed, so no message from a malicious - peer can route it into slashable behavior. HA deployments coordinate signing via a shared store to prevent - self-equivocation ([validator-ha-signer](validator-ha-signer/)). -- **F2 — Individual accountability.** Offenses target individuals: an honest committee member is not slashed because - the majority misbehaved. Note `DATA_WITHHOLDING` targets *all attesters* of the checkpoint — making the data - available is part of the attester's duty, and any single honest attester publishing the txs clears the whole - committee. -- **F3 — Governance backstop.** Slashes need >50% of a round's proposers to vote, wait out an execution delay, and can - be vetoed. Protects against correlated false positives from software bugs (and from A7 violations). - -## 5. Slashing - -Consensus-based: proposers vote (2 bits per validator: 0–3 slash units) during round N on offenses from round N−2; the -L1 `SlashingProposer` tallies and executes after the delay unless vetoed. Full mechanics in -[slasher README](slasher/README.md); offense rationale in AZIP-7. - -| Offense | Target | Trigger | -| --- | --- | --- | -| `DATA_WITHHOLDING` | All attesters of the checkpoint | Checkpoint txs not available on p2p within the tolerance window | -| `INACTIVITY` | Individual validator | Missed proposals/attestations beyond threshold for consecutive committee epochs (Sentinel) | -| `BROADCASTED_INVALID_BLOCK_PROPOSAL` | Proposer | Block proposal fails validation/re-execution | -| `BROADCASTED_INVALID_CHECKPOINT_PROPOSAL` | Proposer | Checkpoint proposal invalid: truncates its own later block, header mismatch on recomputation, malformed fee modifier | -| `PROPOSED_INSUFFICIENT_ATTESTATIONS` | Publishing proposer | L1 checkpoint with < ⌊2n/3⌋+1 signatures | -| `PROPOSED_INCORRECT_ATTESTATIONS` | Publishing proposer | L1 checkpoint with non-committee or invalid signatures | -| `PROPOSED_DESCENDANT_OF_CHECKPOINT_WITH_INVALID_ATTESTATIONS` | Publishing proposer | Built on an invalid-attestation checkpoint | -| `DUPLICATE_PROPOSAL` | Proposer | Conflicting proposals for the same position (p2p or p2p-vs-L1) | -| `DUPLICATE_ATTESTATION` | Attester | Conflicting attestations for the same slot | -| `ATTESTED_TO_INVALID_CHECKPOINT_PROPOSAL` | Attesters | Attested in a slot where the node detected (via re-execution) a slashable invalid proposal | - -Amounts map to three L1-configured tiers. On the v5 testnet (AZIP-16 preset) any offense effectively ejects the -validator (slash drops stake below the local ejection threshold, forcing full exit); mainnet uses smaller amounts. -Grace period after genesis; own validators are auto-protected from a node's own votes; `SLASH_VALIDATORS_NEVER/ALWAYS` -allow operator overrides. - -Audit angles: offense detection soundness under A7 (config divergence), vote encoding and tally correctness on L1, -veto/delay windows vs validator exit timing, and whether any honest behavior can satisfy an offense predicate. - -## 6. P2P peer penalization - -Peer standing combines two layers. The **application-level score** -([peer_scoring.ts](p2p/src/services/peer-manager/peer_scoring.ts)) -accumulates penalties only (there is no reward path), decays by ×0.9 per minute, and drives the peer manager: score -below −50 → GOODBYE + disconnect on the next heartbeat (~30s); below −100 → 24h ban. While banned, the score is frozen -(no decaying out early) and fresh inbound connections are refused; a mere "disconnect"-scored peer may reconnect. The -**gossipsub score** independently combines the app score (weight 10, `-Infinity` for unauthenticated peers when the -node only accepts validator peers via `p2pAllowOnlyValidators`), per-topic delivery scoring (rewards up to +33/topic on predictable-rate topics; the `tx` -topic has no delivery scoring), an invalid-message penalty (P4, weight −20, ~4-slot decay, incremented by every gossip -`REJECT`), and an IP-colocation penalty (−5). Tuning math: [gossipsub README](p2p/src/services/gossipsub/README.md). - -Application-level severities (penalty points against the −50/−100 thresholds; boundaries are strict, so e.g. two -`LowTolerance` strikes reach exactly −100 and a third is needed to ban): - -| Severity | Points | Example conditions | -| --- | --- | --- | -| `LowToleranceError` | 50 | Invalid tx proof, undeserializable message, double-spend of a long-settled nullifier, malformed req/resp payload | -| `MidToleranceError` | 10 | Most tx validation failures (metadata, gas, phases, size), inconsistent req/resp batch responses | -| `HighToleranceError` | 2 | Conditions plausibly caused by state lag or transient faults: recent double-spend, timestamp expiry, per-peer rate-limit breach, connection resets/timeouts | - -Req/resp (PING, STATUS, AUTH, GOODBYE, TX, BLOCK_TXS): per-peer and global rate limits per protocol (handshake -protocols 5/s per peer, 10/s global; TX and BLOCK_TXS 10/s per peer, 200/s global). The per-peer check runs before the -global one, so a single peer cannot exhaust the shared budget and have others blamed; exceeding a per-peer limit costs -a `HighToleranceError`, exceeding the global limit returns `RATE_LIMIT_EXCEEDED` with no penalty. Malformed -requests/responses penalize the sender (`LowToleranceError`); transport errors and timeouts are treated as transient -(`HighToleranceError`); self-inflicted aborts are never penalized. - -Handshake failures are a separate subsystem that bypasses scoring: failed AUTH attempts (nodes running with -`p2pAllowOnlyValidators`) get -exponential dial backoff (5 min doubling to 160 min) and, past `p2pMaxFailedAuthAttemptsAllowed` (default 3), inbound -denial until 1h passes without failures. Trusted/private/preferred peers are exempt from manager-initiated -disconnection and capacity eviction, but **not** from scoring or gossipsub graylisting. - -The reject-vs-ignore mapping per message type (which failures penalize the sender vs get silently dropped) is the -enforcement of P2 and is catalogued per validator in [message validator READMEs](p2p/src/msg_validators/). - -## 7. Known gaps - -1. **Descendants of invalid checkpoints.** If a malicious quorum attests to a *descendant* of an invalid-attestation - checkpoint, nodes currently follow it (they assume an honest majority) instead of ignoring it until proven. Flagged - in [archiver README](archiver/README.md); the corresponding slash (`PROPOSED_DESCENDANT_…`) exists, but node-side - chain-selection does not. -2. **Escape hatch × unsound verifier.** `submitEpochRootProof` skips attestation verification for hatch epochs (there - is no committee), so during a hatch window the attestation gate of V4(b) is absent: a malicious hatch proposer plus - an L1 verifier bug could prove invalid state. Fee theft is still blocked (V4(a)); Outbox withdrawals are the - remaining exposure. Compensating controls today: bond, random selection among bonded candidates, bounded window. -3. **Validation config divergence (A7).** Divergent `VALIDATOR_MAX_*` limits across honest validators can produce - divergent invalidity verdicts and therefore divergent slash votes against honest proposers. Mitigated by the >50% - vote quorum and the vetoer; no protocol-level enforcement of config uniformity exists. -4. **Committee trust is a standing decision (A6).** Following attested checkpoints without re-execution is deliberate - and may be revisited (e.g. stateless validation of checkpointed data); until then, C1–C5 are the whole story for - pending-chain integrity. -5. **Blob retention.** Nodes syncing later than the L1 blob retention window depend on out-of-protocol blob archives to - reconstruct the checkpointed chain; availability of that path is operational, not protocol-enforced. From 801b0b8c49e70d5f8c296c81c239092271d8b064 Mon Sep 17 00:00:00 2001 From: Amin Sammara <84764772+aminsammara@users.noreply.github.com> Date: Tue, 14 Jul 2026 00:52:50 +0300 Subject: [PATCH 3/7] feat(cli): support funding accounts in validator-keys new/set-funding-account (#24476) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Wires a real `--funding-account` option into `aztec validator-keys new` (previously commented out with a TODO, so operators had to hand-edit the keystore JSON), and adds a dedicated `set-funding-account` subcommand for existing keystores. Per review feedback, `add` does **not** take `--funding-account`: the funding account is a keystore-level field (the only one `KeystoreManager.createFundingSigner` reads), so setting it from `add` would be a global mutation disguised as a per-validator flag. Use `set-funding-account` instead: ``` aztec validator-keys set-funding-account [--remote-signer ] [--password ] ``` ## Behavior - The funding account value accepts either a 32-byte private key or a 20-byte address. - An address requires `--remote-signer` (a local funder needs its private key to sign funding txs); it is stored as `{ address, remoteSignerUrl }`. - With `--password`, a plaintext funder key is encrypted to a JSON V3 file and replaced with a `{ path, password }` reference, mirroring how attester/publisher keys are handled. - The value is written at the keystore top level (`keystore.fundingAccount`). Validator-level funding exists in the schema but is never consumed at runtime, so it is intentionally not emitted. - `set-funding-account` replaces an existing funding account with a warning. ## Changes - `index.ts` — real `--funding-account` option on `new`; new `set-funding-account` subcommand. - `set_funding_account.ts` — new command: validate, resolve, optionally encrypt, write `keystore.fundingAccount`. - `utils.ts` — `validateFundingAccountOptions` (normalize + validate key/address, require remote-signer for address form). - `shared.ts` — `resolveFundingAccount` and `encryptFundingAccountToFile`; extracted the ETH JSON V3 encryption helper for reuse. - `new.ts` — validate, resolve, optionally encrypt, set `keystore.fundingAccount`. - `valkeys.test.ts` — 17 new tests covering validation, private-key/address/password paths on `new`, and set/replace on `set-funding-account`. ## Testing - `yarn workspace @aztec/cli test src/cmds/validator_keys/valkeys.test.ts` — 60 passed. - `yarn build`, `yarn format cli`, `yarn lint cli` — all clean. (cherry picked from commit 3aa68f6d748f199e323b5882bd3121b3a4451201) --- .../cli/src/cmds/validator_keys/index.ts | 34 ++- .../cli/src/cmds/validator_keys/new.ts | 23 +- .../validator_keys/set_funding_account.ts | 55 +++++ .../cli/src/cmds/validator_keys/shared.ts | 38 ++- .../cli/src/cmds/validator_keys/utils.ts | 45 +++- .../src/cmds/validator_keys/valkeys.test.ts | 228 +++++++++++++++++- .../src/keystore_manager.test.ts | 38 +++ 7 files changed, 453 insertions(+), 8 deletions(-) create mode 100644 yarn-project/cli/src/cmds/validator_keys/set_funding_account.ts diff --git a/yarn-project/cli/src/cmds/validator_keys/index.ts b/yarn-project/cli/src/cmds/validator_keys/index.ts index 8588f40c55a7..0c6088a6cbf7 100644 --- a/yarn-project/cli/src/cmds/validator_keys/index.ts +++ b/yarn-project/cli/src/cmds/validator_keys/index.ts @@ -33,8 +33,10 @@ export function injectCommands(program: Command, log: LogFn) { 'Coinbase ETH address to use when proposing. Defaults to attester address.', parseEthereumAddress, ) - // TODO: add funding account back in when implemented - // .option('--funding-account ', 'ETH private key (or address for remote signer setup) to fund publishers') + .option( + '--funding-account ', + 'ETH funding account used to top up publisher EOAs. Provide a private key, or an address together with --remote-signer.', + ) .option('--remote-signer ', 'Default remote signer URL for accounts in this file') .option('--ikm ', 'Initial keying material for BLS (alternative to mnemonic)', value => parseHex(value, 32)) .option('--bls-path ', `EIP-2334 path (default ${defaultBlsPath})`) @@ -99,8 +101,6 @@ export function injectCommands(program: Command, log: LogFn) { 'Coinbase ETH address to use when proposing. Defaults to attester address.', parseEthereumAddress, ) - // TODO: add funding account back in when implemented - // .option('--funding-account ', 'ETH private key (or address for remote signer setup) to fund publishers') .option('--remote-signer ', 'Default remote signer URL for accounts in this file') .option('--ikm ', 'Initial keying material for BLS (alternative to mnemonic)', value => parseHex(value, 32)) .option('--bls-path ', `EIP-2334 path (default ${defaultBlsPath})`) @@ -117,6 +117,32 @@ export function injectCommands(program: Command, log: LogFn) { await addValidatorKeys(existing, options, log); }); + group + .command('set-funding-account') + .summary('Set the funding account of an existing keystore') + .description( + 'Sets the keystore-level ETH funding account used to top up publisher EOAs, replacing any existing one', + ) + .argument('', 'Path to existing keystore JSON') + .argument( + '', + 'Funding account: a private key, or an address (needs --remote-signer unless the keystore already defines one)', + ) + .option( + '--remote-signer ', + 'Remote signer URL for the funding account (required with an address unless the keystore already defines one)', + ) + .option( + '--password ', + 'Password for writing the funding key as an encrypted ETH JSON V3 file. Empty string allowed', + ) + .option('--encrypted-keystore-dir ', 'Output directory for the encrypted funding key file') + .option('--json', 'Echo resulting JSON to stdout') + .action(async (existing: string, fundingAccount: string, options) => { + const { setFundingAccount } = await import('./set_funding_account.js'); + await setFundingAccount(existing, fundingAccount, options, log); + }); + group .command('staker') .summary('Generate staking JSON from keystore') diff --git a/yarn-project/cli/src/cmds/validator_keys/new.ts b/yarn-project/cli/src/cmds/validator_keys/new.ts index 208ebc7a8748..bc8ebc9826bd 100644 --- a/yarn-project/cli/src/cmds/validator_keys/new.ts +++ b/yarn-project/cli/src/cmds/validator_keys/new.ts @@ -9,12 +9,14 @@ import { wordlist } from '@scure/bip39/wordlists/english.js'; import { readFile, writeFile } from 'fs/promises'; import { basename, dirname, join } from 'path'; import { createPublicClient, fallback, http } from 'viem'; -import { generateMnemonic, mnemonicToAccount } from 'viem/accounts'; +import { generateMnemonic, mnemonicToAccount, privateKeyToAccount } from 'viem/accounts'; import { buildValidatorEntries, + encryptFundingAccountToFile, logValidatorSummaries, maybePrintJson, + resolveFundingAccount, resolveKeystoreOutputPath, writeBlsBn254ToFile, writeEthJsonV3ToFile, @@ -23,6 +25,7 @@ import { import { processAttesterAccounts } from './staker.js'; import { validateBlsPathOptions, + validateFundingAccountOptions, validatePublisherOptions, validateRemoteSignerOptions, validateStakerOutputOptions, @@ -51,6 +54,7 @@ export type NewValidatorKeystoreOptions = { json?: boolean; feeRecipient: AztecAddress; coinbase?: EthAddress; + fundingAccount?: string; remoteSigner?: string; stakerOutput?: boolean; gseAddress?: EthAddress; @@ -147,6 +151,8 @@ export async function newValidatorKeystore(options: NewValidatorKeystoreOptions, validatePublisherOptions(options); // validate remote signer options validateRemoteSignerOptions(options); + // validate funding account option + validateFundingAccountOptions(options); const { dataDir, @@ -156,6 +162,7 @@ export async function newValidatorKeystore(options: NewValidatorKeystoreOptions, publishers, json, coinbase, + fundingAccount, accountIndex = 0, addressIndex = 0, feeRecipient, @@ -213,17 +220,26 @@ export async function newValidatorKeystore(options: NewValidatorKeystoreOptions, remoteSigner, }); + let resolvedFundingAccount = fundingAccount ? resolveFundingAccount(fundingAccount, remoteSigner) : undefined; + // If password provided, write ETH JSON V3 and BLS BN254 keystores and replace plaintext if (shouldEncryptKeystores) { const encryptedKeystoreOutDir = encryptedKeystoreDir && encryptedKeystoreDir.length > 0 ? encryptedKeystoreDir : keystoreOutDir; await writeEthJsonV3ToFile(validators, { outDir: encryptedKeystoreOutDir, password: ethPassword }); await writeBlsBn254ToFile(validators, { outDir: encryptedKeystoreOutDir, password: blsPassword }); + if (resolvedFundingAccount) { + resolvedFundingAccount = await encryptFundingAccountToFile(resolvedFundingAccount, { + outDir: encryptedKeystoreOutDir, + password: ethPassword, + }); + } } const keystore = { schemaVersion: 1, validators, + ...(resolvedFundingAccount ? { fundingAccount: resolvedFundingAccount } : {}), }; await writeKeystoreFile(outputPath, keystore); @@ -285,6 +301,11 @@ export async function newValidatorKeystore(options: NewValidatorKeystoreOptions, // print a concise summary of public keys (addresses and BLS pubkeys) if no --json options was selected if (!json) { logValidatorSummaries(log, summaries); + if (fundingAccount) { + const funderAddress = + fundingAccount.length === 66 ? privateKeyToAccount(fundingAccount as `0x${string}`).address : fundingAccount; + log(`funding account: ${funderAddress}`); + } } if (mnemonic && remoteSigner && !json) { diff --git a/yarn-project/cli/src/cmds/validator_keys/set_funding_account.ts b/yarn-project/cli/src/cmds/validator_keys/set_funding_account.ts new file mode 100644 index 000000000000..21258abb56b1 --- /dev/null +++ b/yarn-project/cli/src/cmds/validator_keys/set_funding_account.ts @@ -0,0 +1,55 @@ +import type { LogFn } from '@aztec/foundation/log'; +import { loadKeystoreFile } from '@aztec/node-keystore/loader'; +import type { KeyStore } from '@aztec/node-keystore/types'; + +import { dirname } from 'path'; +import { privateKeyToAccount } from 'viem/accounts'; + +import { encryptFundingAccountToFile, maybePrintJson, resolveFundingAccount, writeKeystoreFile } from './shared.js'; +import { validateFundingAccountOptions } from './utils.js'; + +export type SetFundingAccountOptions = { + remoteSigner?: string; + password?: string; + encryptedKeystoreDir?: string; + json?: boolean; +}; + +/** + * Sets the top-level funding account of an existing keystore, replacing any previous one. The + * account may be a private key, or an address paired with a remote signer URL. With a password, + * a plaintext key is encrypted to an ETH JSON V3 file and stored as a { path, password } reference. + */ +export async function setFundingAccount( + existing: string, + fundingAccount: string, + options: SetFundingAccountOptions, + log: LogFn, +) { + const { remoteSigner, password, encryptedKeystoreDir, json } = options; + + const keystore: KeyStore = loadKeystoreFile(existing); + + const validated = { fundingAccount, remoteSigner }; + validateFundingAccountOptions(validated, !!keystore.remoteSigner); + + let resolved = resolveFundingAccount(validated.fundingAccount!, remoteSigner); + if (password !== undefined) { + const outDir = encryptedKeystoreDir && encryptedKeystoreDir.length > 0 ? encryptedKeystoreDir : dirname(existing); + resolved = await encryptFundingAccountToFile(resolved, { outDir, password }); + } + + if (keystore.fundingAccount) { + log('Replacing existing funding account in keystore'); + } + keystore.fundingAccount = resolved; + + await writeKeystoreFile(existing, keystore); + + if (!json) { + const value = validated.fundingAccount!; + const funderAddress = value.length === 66 ? privateKeyToAccount(value as `0x${string}`).address : value; + log(`Set funding account ${funderAddress} in ${existing}`); + } + maybePrintJson(log, !!json, keystore as unknown as Record); +} diff --git a/yarn-project/cli/src/cmds/validator_keys/shared.ts b/yarn-project/cli/src/cmds/validator_keys/shared.ts index 981acc155e1b..64d162454505 100644 --- a/yarn-project/cli/src/cmds/validator_keys/shared.ts +++ b/yarn-project/cli/src/cmds/validator_keys/shared.ts @@ -3,7 +3,7 @@ import { asyncPool } from '@aztec/foundation/async-pool'; import { deriveBlsPrivateKey } from '@aztec/foundation/crypto/bls'; import { createBn254Keystore } from '@aztec/foundation/crypto/bls/bn254_keystore'; import { computeBn254G1PublicKeyCompressed } from '@aztec/foundation/crypto/bn254'; -import type { EthAddress } from '@aztec/foundation/eth-address'; +import { EthAddress } from '@aztec/foundation/eth-address'; import type { LogFn } from '@aztec/foundation/log'; import type { EthAccount, EthPrivateKey, ValidatorKeyStore } from '@aztec/node-keystore/types'; import type { AztecAddress } from '@aztec/stdlib/aztec-address'; @@ -125,6 +125,21 @@ export function deriveEthAttester( : (('0x' + Buffer.from(acct.getHdKey().privateKey!).toString('hex')) as EthPrivateKey); } +/** + * Resolve a `--funding-account` value into a keystore `EthAccount`. A 66-char value is a private key + * (used verbatim). A 42-char value is an address: with an explicit `remoteSigner` URL it becomes an + * `{ address, remoteSignerUrl }` pair; without one it is stored as a bare address that falls back to + * the keystore-level remote signer at runtime. Callers must validate the value first (see + * `validateFundingAccountOptions`). + */ +export function resolveFundingAccount(fundingAccount: string, remoteSigner?: string): EthAccount { + if (fundingAccount.length === 66) { + return fundingAccount as EthPrivateKey; + } + const address = EthAddress.fromString(fundingAccount); + return remoteSigner ? ({ address, remoteSignerUrl: remoteSigner } as EthAccount) : address; +} + export async function buildValidatorEntries(input: BuildValidatorsInput) { const { validatorCount, @@ -330,6 +345,27 @@ export async function writeEthJsonV3Keystore( return outPath; } +/** + * If `account` is a plaintext ETH private key, encrypt it to a JSON V3 file and return a + * { path, password } reference; otherwise return it unchanged. + */ +async function maybeEncryptEthAccount(account: any, label: string, options: { outDir: string; password: string }) { + if (typeof account === 'string' && account.startsWith('0x') && account.length === 66) { + const fileBase = `${label}_${account.slice(2, 10)}`; + const p = await writeEthJsonV3Keystore(options.outDir, fileBase, options.password, account); + return { path: p, password: options.password }; + } + return account; +} + +/** Encrypt a plaintext funding-account key to a JSON V3 file, replacing it with a { path, password } reference. */ +export async function encryptFundingAccountToFile( + account: EthAccount, + options: { outDir: string; password: string }, +): Promise { + return (await maybeEncryptEthAccount(account, 'funding', options)) as EthAccount; +} + /** Replace plaintext ETH keys in validators with { path, password } pointing to JSON V3 files. */ export async function writeEthJsonV3ToFile( validators: ValidatorKeyStore[], diff --git a/yarn-project/cli/src/cmds/validator_keys/utils.ts b/yarn-project/cli/src/cmds/validator_keys/utils.ts index 8b8dca71f191..1e83594f9b6a 100644 --- a/yarn-project/cli/src/cmds/validator_keys/utils.ts +++ b/yarn-project/cli/src/cmds/validator_keys/utils.ts @@ -1,4 +1,4 @@ -import type { EthAddress } from '@aztec/foundation/eth-address'; +import { EthAddress } from '@aztec/foundation/eth-address'; import { ethPrivateKeySchema } from '@aztec/node-keystore/schemas'; import type { EthPrivateKey } from '@aztec/node-keystore/types'; @@ -79,3 +79,46 @@ export function validatePublisherOptions(options: { publishers?: string[]; publi options.publishers = normalizedKeys as EthPrivateKey[]; } } + +/** + * Validates and normalizes the `--funding-account` option in place. The value may be a private key + * (used as a local signer) or an ETH address. An address needs a remote signer to sign funding txs: + * either `--remote-signer`, or a keystore that already defines one (pass `hasKeystoreRemoteSigner`), + * which a bare address inherits at runtime. + */ +export function validateFundingAccountOptions( + options: { fundingAccount?: string; remoteSigner?: string }, + hasKeystoreRemoteSigner = false, +) { + if (!options.fundingAccount) { + return; + } + + let value = options.fundingAccount.trim(); + if (!value.startsWith('0x')) { + value = '0x' + value; + } + + if (value.length === 66) { + try { + ethPrivateKeySchema.parse(value); + } catch (error) { + throw new Error(`Invalid funding account private key: ${error instanceof Error ? error.message : String(error)}`); + } + } else if (value.length === 42) { + try { + EthAddress.fromString(value); + } catch (error) { + throw new Error(`Invalid funding account address: ${error instanceof Error ? error.message : String(error)}`); + } + if (!options.remoteSigner && !hasKeystoreRemoteSigner) { + throw new Error( + '--funding-account as an address requires --remote-signer, or a keystore that already defines a remote signer', + ); + } + } else { + throw new Error('Invalid funding account: expected a 32-byte private key or a 20-byte address'); + } + + options.fundingAccount = value; +} diff --git a/yarn-project/cli/src/cmds/validator_keys/valkeys.test.ts b/yarn-project/cli/src/cmds/validator_keys/valkeys.test.ts index 24abb2c06169..e647115f7597 100644 --- a/yarn-project/cli/src/cmds/validator_keys/valkeys.test.ts +++ b/yarn-project/cli/src/cmds/validator_keys/valkeys.test.ts @@ -13,6 +13,7 @@ import { mnemonicToAccount } from 'viem/accounts'; import { addValidatorKeys } from './add.js'; import { generateBlsKeypair } from './generate_bls_keypair.js'; import { newValidatorKeystore } from './new.js'; +import { setFundingAccount } from './set_funding_account.js'; import { buildValidatorEntries, computeBlsPublicKeyCompressed, @@ -24,7 +25,7 @@ import { writeEthJsonV3ToFile, writeKeystoreFile, } from './shared.js'; -import { validatePublisherOptions } from './utils.js'; +import { validateFundingAccountOptions, validatePublisherOptions } from './utils.js'; const TEST_MNEMONIC = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'; @@ -402,6 +403,44 @@ describe('validator keys utilities', () => { }); }); + describe('validateFundingAccountOptions', () => { + const validPrivateKey = '0x' + '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'; + const validAddress = '0x' + '01'.repeat(20); + + it('accepts a private key and leaves it normalized', () => { + const options = { fundingAccount: validPrivateKey }; + expect(() => validateFundingAccountOptions(options)).not.toThrow(); + expect(options.fundingAccount).toBe(validPrivateKey); + }); + + it('adds a missing 0x prefix', () => { + const options = { fundingAccount: validPrivateKey.slice(2) }; + expect(() => validateFundingAccountOptions(options)).not.toThrow(); + expect(options.fundingAccount).toBe(validPrivateKey); + }); + + it('accepts an address when a remote signer is set', () => { + const options = { fundingAccount: validAddress, remoteSigner: 'http://localhost:9000' }; + expect(() => validateFundingAccountOptions(options)).not.toThrow(); + expect(options.fundingAccount).toBe(validAddress); + }); + + it('throws for an address without a remote signer', () => { + const options = { fundingAccount: validAddress }; + expect(() => validateFundingAccountOptions(options)).toThrow(/requires --remote-signer/); + }); + + it('throws for a malformed value', () => { + const options = { fundingAccount: '0x1234' }; + expect(() => validateFundingAccountOptions(options)).toThrow(/Invalid funding account/); + }); + + it('is a no-op when unset', () => { + const options = {}; + expect(() => validateFundingAccountOptions(options)).not.toThrow(); + }); + }); + describe('newValidatorKeystore', () => { it('creates a keystore file and logs a summary', async () => { const path = join(tmp, 'created.json'); @@ -946,6 +985,99 @@ describe('validator keys utilities', () => { expect(Array.isArray(validator.publisher)).toBe(true); expect(validator.publisher).toEqual([publisherKey1, publisherKey2]); }); + + it('writes a top-level funding account from a private key', async () => { + const path = join(tmp, 'with-funding-key.json'); + const fundingKey = '0x' + 'ab'.repeat(32); + await newValidatorKeystore( + { + dataDir: tmp, + file: 'with-funding-key.json', + count: 1, + mnemonic: TEST_MNEMONIC, + fundingAccount: fundingKey, + feeRecipient: ('0x' + '11'.repeat(32)) as unknown as AztecAddress, + }, + s => s, + ); + const keystore: KeyStore = loadKeystoreFile(path); + expect(keystore.fundingAccount).toBe(fundingKey); + }); + + it('writes a remote-signer funding account from an address', async () => { + const path = join(tmp, 'with-funding-address.json'); + const fundingAddress = '0x' + '02'.repeat(20); + const remoteSigner = 'http://localhost:9000'; + await newValidatorKeystore( + { + dataDir: tmp, + file: 'with-funding-address.json', + count: 1, + mnemonic: TEST_MNEMONIC, + fundingAccount: fundingAddress, + remoteSigner, + feeRecipient: ('0x' + '12'.repeat(32)) as unknown as AztecAddress, + }, + s => s, + ); + const keystore: KeyStore = loadKeystoreFile(path); + const funder = keystore.fundingAccount as any; + expect(funder.remoteSignerUrl).toBe(remoteSigner); + expect(funder.address.toString().toLowerCase()).toBe(fundingAddress); + }); + + it('rejects a funding-account address without a remote signer', async () => { + await expect( + newValidatorKeystore( + { + dataDir: tmp, + file: 'funding-address-no-signer.json', + count: 1, + mnemonic: TEST_MNEMONIC, + fundingAccount: '0x' + '03'.repeat(20), + feeRecipient: ('0x' + '13'.repeat(32)) as unknown as AztecAddress, + }, + s => s, + ), + ).rejects.toThrow(/requires --remote-signer/); + }); + + it('rejects a malformed funding account', async () => { + await expect( + newValidatorKeystore( + { + dataDir: tmp, + file: 'funding-malformed.json', + count: 1, + mnemonic: TEST_MNEMONIC, + fundingAccount: '0xdead', + feeRecipient: ('0x' + '14'.repeat(32)) as unknown as AztecAddress, + }, + s => s, + ), + ).rejects.toThrow(/Invalid funding account/); + }); + + it('encrypts a plaintext funding account when a password is provided', async () => { + const path = join(tmp, 'with-funding-encrypted.json'); + await newValidatorKeystore( + { + dataDir: tmp, + file: 'with-funding-encrypted.json', + count: 1, + mnemonic: TEST_MNEMONIC, + fundingAccount: '0x' + 'cd'.repeat(32), + password: 'funding-test-pw', + encryptedKeystoreDir: tmp, + feeRecipient: ('0x' + '15'.repeat(32)) as unknown as AztecAddress, + }, + s => s, + ); + const keystore: KeyStore = loadKeystoreFile(path); + const funder = keystore.fundingAccount as any; + expect(typeof funder.path).toBe('string'); + expect(existsSync(funder.path)).toBe(true); + }); }); describe('materialization helpers (invoked directly)', () => { @@ -1021,6 +1153,100 @@ describe('validator keys utilities', () => { }); }); + describe('setFundingAccount', () => { + const writeBaseKeystore = (path: string, extra: Record = {}) => { + const baseKeystore = { + schemaVersion: 1, + validators: [{ attester: '0x' + '0a'.repeat(32), feeRecipient: ('0x' + '06'.repeat(32)) as unknown as string }], + ...extra, + }; + writeFileSync(path, JSON.stringify(baseKeystore, null, 2), 'utf-8'); + }; + + it('sets a funding account on a keystore that has none', async () => { + const existing = join(tmp, 'set-funding.json'); + writeBaseKeystore(existing); + const fundingKey = '0x' + 'ab'.repeat(32); + + const logs: string[] = []; + await setFundingAccount(existing, fundingKey, {}, s => logs.push(s)); + + const updated: KeyStore = loadKeystoreFile(existing); + expect(updated.fundingAccount).toBe(fundingKey); + expect(logs.some(l => l.includes('Replacing existing funding account'))).toBe(false); + expect(logs.some(l => l.includes('Set funding account'))).toBe(true); + }); + + it('replaces an existing funding account and warns', async () => { + const existing = join(tmp, 'replace-funding.json'); + writeBaseKeystore(existing, { fundingAccount: '0x' + 'aa'.repeat(32) }); + const newFundingKey = '0x' + 'bb'.repeat(32); + + const logs: string[] = []; + await setFundingAccount(existing, newFundingKey, {}, s => logs.push(s)); + + const updated: KeyStore = loadKeystoreFile(existing); + expect(updated.fundingAccount).toBe(newFundingKey); + expect(logs.some(l => l.includes('Replacing existing funding account'))).toBe(true); + }); + + it('sets a remote-signer funding account from an address', async () => { + const existing = join(tmp, 'set-funding-address.json'); + writeBaseKeystore(existing); + const fundingAddress = '0x' + '02'.repeat(20); + const remoteSigner = 'http://localhost:9000'; + + await setFundingAccount(existing, fundingAddress, { remoteSigner }, s => s); + + const updated: KeyStore = loadKeystoreFile(existing); + const funder = updated.fundingAccount as any; + expect(funder.remoteSignerUrl).toBe(remoteSigner); + expect(funder.address.toString().toLowerCase()).toBe(fundingAddress); + }); + + it('inherits the keystore remote signer for an address when --remote-signer is omitted', async () => { + const existing = join(tmp, 'set-funding-inherit-signer.json'); + writeBaseKeystore(existing, { remoteSigner: 'http://localhost:9000' }); + const fundingAddress = '0x' + '02'.repeat(20); + + await setFundingAccount(existing, fundingAddress, {}, s => s); + + const updated: KeyStore = loadKeystoreFile(existing); + // Stored as a bare address (no inline remoteSignerUrl); resolved via the keystore-level signer at runtime. + const funder = updated.fundingAccount as any; + expect(funder.remoteSignerUrl).toBeUndefined(); + expect(funder.toString().toLowerCase()).toBe(fundingAddress); + }); + + it('rejects an address without a remote signer', async () => { + const existing = join(tmp, 'set-funding-no-signer.json'); + writeBaseKeystore(existing); + + await expect(setFundingAccount(existing, '0x' + '03'.repeat(20), {}, s => s)).rejects.toThrow( + /requires --remote-signer/, + ); + }); + + it('rejects a malformed funding account', async () => { + const existing = join(tmp, 'set-funding-malformed.json'); + writeBaseKeystore(existing); + + await expect(setFundingAccount(existing, '0xdead', {}, s => s)).rejects.toThrow(/Invalid funding account/); + }); + + it('encrypts a plaintext funding key when a password is provided', async () => { + const existing = join(tmp, 'set-funding-encrypted.json'); + writeBaseKeystore(existing); + + await setFundingAccount(existing, '0x' + 'cd'.repeat(32), { password: '', encryptedKeystoreDir: tmp }, s => s); + + const updated: KeyStore = loadKeystoreFile(existing); + const funder = updated.fundingAccount as any; + expect(typeof funder.path).toBe('string'); + expect(existsSync(funder.path)).toBe(true); + }); + }); + describe('generateBlsKeypair', () => { it('writes to file and logs a write message when out is provided', async () => { const out = join(tmp, 'bls.json'); diff --git a/yarn-project/node-keystore/src/keystore_manager.test.ts b/yarn-project/node-keystore/src/keystore_manager.test.ts index a0bcf268d5cf..8452f1dbe0ac 100644 --- a/yarn-project/node-keystore/src/keystore_manager.test.ts +++ b/yarn-project/node-keystore/src/keystore_manager.test.ts @@ -1605,5 +1605,43 @@ describe('KeystoreManager', () => { expect(signer).toBeUndefined(); }); + + it('resolves a bare-address fundingAccount via the keystore-level remote signer', async () => { + const fundingAddress = EthAddress.random(); + const keystore: KeyStore = { + schemaVersion: 1, + validators: [ + { + attester: EthAddress.random(), + feeRecipient: await AztecAddress.random(), + }, + ], + remoteSigner: 'http://localhost:9000', + fundingAccount: fundingAddress, + }; + + const manager = new KeystoreManager(keystore); + const signer = manager.createFundingSigner(); + + expect(signer).toBeInstanceOf(RemoteSigner); + expect(signer!.address.equals(fundingAddress)).toBeTruthy(); + }); + + it('throws for a bare-address fundingAccount when no remote signer is configured', async () => { + const keystore: KeyStore = { + schemaVersion: 1, + validators: [ + { + attester: EthAddress.random(), + feeRecipient: await AztecAddress.random(), + }, + ], + fundingAccount: EthAddress.random(), + }; + + const manager = new KeystoreManager(keystore); + + expect(() => manager.createFundingSigner()).toThrow(/No remote signer configuration found/); + }); }); }); From a0fff3b4d9ee3517017b3b5c6df33a6a23e1a2ce Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Thu, 23 Jul 2026 16:32:50 -0300 Subject: [PATCH 4/7] chore(spartan): restore 7 day mainnet slash grace period (#24804) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restores the mainnet `SLASH_GRACE_PERIOD_L2_SLOTS` default from `1200` to `8400` slots in `spartan/environments/network-defaults.yml`. At the mainnet `AZTEC_SLOT_DURATION` of 72s: - `1200 × 72s = 86,400s` = **exactly 24 hours** (the value v5 shipped with) - `8400 × 72s = 604,800s` = **exactly 7 days** (the intended value) This value was already raised to 8400 in #21451 ("tune mainnet slasher penalties and sequencer allocation"), but that commit landed directly on the v4 release branch and never made it onto `next`. `v5` therefore inherited the stale 1200 from `next`. Confirmed by diffing the branches: | branch | mainnet grace | at 72s slot | | --- | --- | --- | | `v4-next` | 8400 | 7 days | | `next` / `v5-next` / `merge-train/spartan` | 1200 | 24 hours | - **Only the `networks.mainnet` preset is changed.** The top-level `slasher` anchor (`0`) and the `devnet` preset (`0`) are unchanged. - **`testnet` (64 slots) is deliberately left alone.** `v4-next` has 3600 there, but that value traces back to the original `refactor: centralize network defaults in YAML` commit on the `next` line rather than to the lost #21451 change — so it is a separate long-standing divergence, not part of this regression. Worth a follow-up decision, but out of scope here. - No generated files are committed: `yarn-project/slasher/src/generated/` and the CLI network presets are gitignored and produced at build time from this YAML. - Operator docs updated to match (8,400 slots / ~7 days). The `network_versioned_docs/version-v5.0.1` snapshot is intentionally left untouched, since it documents what v5.0.1 actually shipped. This changes a **baked-in default for future builds/deployments**. It does not retroactively alter the grace period on the already-running v5 network — nodes there need `SLASH_GRACE_PERIOD_L2_SLOTS` set explicitly or a redeploy. - [x] YAML parses; mainnet resolves to exactly 7.00 days at a 72s slot - [x] No test or `spartan/environments/*.env` file pins the old `1200` value --- *Created by [claudebox](https://claudebox.work/v2/sessions/849a625af4f2c309/jobs/1) · group: `slackbot` · [Slack thread](https://aztecprotocol.slack.com/archives/C0AU8BULZHC/p1784543473010009?thread_ts=1784543473.010009&cid=C0AU8BULZHC)* (cherry picked from commit 38202d9de8cdee768d738f41d5ea5f130009ebeb) # Conflicts: # spartan/environments/network-defaults.yml --- spartan/environments/network-defaults.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/spartan/environments/network-defaults.yml b/spartan/environments/network-defaults.yml index 6b6bb4bf127b..f55c68e87e5c 100644 --- a/spartan/environments/network-defaults.yml +++ b/spartan/environments/network-defaults.yml @@ -375,5 +375,9 @@ networks: SLASH_ATTEST_INVALID_CHECKPOINT_PROPOSAL_PENALTY: 0 # AZIP-16: deferred to subsequent release SLASH_UNKNOWN_PENALTY: 2000e18 SLASH_INVALID_BLOCK_PENALTY: 2000e18 +<<<<<<< HEAD SLASH_INVALID_CHECKPOINT_PROPOSAL_PENALTY: 2000e18 # AZIP-16: activated at SMALL +======= + SLASH_INVALID_CHECKPOINT_PROPOSAL_PENALTY: 2000e18 # AZIP-16: activated at SMALL +>>>>>>> 38202d9de8c (chore(spartan): restore 7 day mainnet slash grace period (#24804)) SLASH_GRACE_PERIOD_L2_SLOTS: 8400 # 7 days at a 72s slot From 6b815ba37bfc6d0230b155084ddae18b223171e8 Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Thu, 23 Jul 2026 16:33:12 -0300 Subject: [PATCH 5/7] chore(spartan): resolve port conflict --- spartan/environments/network-defaults.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/spartan/environments/network-defaults.yml b/spartan/environments/network-defaults.yml index f55c68e87e5c..6b6bb4bf127b 100644 --- a/spartan/environments/network-defaults.yml +++ b/spartan/environments/network-defaults.yml @@ -375,9 +375,5 @@ networks: SLASH_ATTEST_INVALID_CHECKPOINT_PROPOSAL_PENALTY: 0 # AZIP-16: deferred to subsequent release SLASH_UNKNOWN_PENALTY: 2000e18 SLASH_INVALID_BLOCK_PENALTY: 2000e18 -<<<<<<< HEAD SLASH_INVALID_CHECKPOINT_PROPOSAL_PENALTY: 2000e18 # AZIP-16: activated at SMALL -======= - SLASH_INVALID_CHECKPOINT_PROPOSAL_PENALTY: 2000e18 # AZIP-16: activated at SMALL ->>>>>>> 38202d9de8c (chore(spartan): restore 7 day mainnet slash grace period (#24804)) SLASH_GRACE_PERIOD_L2_SLOTS: 8400 # 7 days at a 72s slot From 7bb1526b9d2d5c896ef97822c12ee92adbfe9386 Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Thu, 23 Jul 2026 16:34:20 -0300 Subject: [PATCH 6/7] chore: re-pin handshake registry with owner-bound nullifiers (#24893) [FWD-PORT CONFLICTS] --- .../docs/resources/migration_notes.md | 18 ++++++++++++++ .../aztec-nr/aztec/src/standard_addresses.nr | 4 ++++ .../src/standard_contract_data.ts | 24 +++++++++++++++++++ 3 files changed, 46 insertions(+) diff --git a/docs/docs-developers/docs/resources/migration_notes.md b/docs/docs-developers/docs/resources/migration_notes.md index dbb29701e344..1ffdc5b07466 100644 --- a/docs/docs-developers/docs/resources/migration_notes.md +++ b/docs/docs-developers/docs/resources/migration_notes.md @@ -9,7 +9,25 @@ Aztec is in active development. Each version may introduce breaking changes that ## TBD +<<<<<<< HEAD ## 5.0.1 +======= +### [Aztec.nr] Canonical HandshakeRegistry re-pinned at a new address + +The canonical `HandshakeRegistry` has been re-pinned so that it includes the owner's address in its `PrivateMutable` initialization nullifiers, keeping the handshake state of accounts that share keys independent. The registry moves to a new address. Handshakes established with the previous registry instance are not visible to the new one and must be re-established. The other standard contracts keep their addresses. + +### [Aztec.nr] Note property selectors are typed and use packed-layout indices + +The selectors in the generated `properties()` used the field's position in the note struct declaration, which pointed at the wrong packed field for any note with an earlier field packing to more than one `Field` (a `Point`, an array, a nested struct). Selector indices are now the field's offset in the note's packed representation, so `select`/`sort` criteria constrain the field they name. + +Breaking changes: + +- `PropertySelector` carries the selected property's type. Hand-constructed literals need a type annotation, e.g. `let selector: PropertySelector = PropertySelector { index: 0, offset: 0, length: 32 };`. +- `select`/`sort` reject properties that pack to more than one `Field` at compile time. +- `select` takes its value typed as the property's type. Cast the value if a mixed-type comparison was intentional. +- `properties()` cannot be used with a custom `Packable` layout. Define property selectors manually for such notes. +- Every note field type must implement `Packable`, even when the note's own `Packable` is hand-written. +>>>>>>> c3a2a8512c6 (chore: re-pin handshake registry with owner-bound nullifiers (#24893)) ### [Aztec.nr] History note nullification helpers renamed and restricted to own-contract notes diff --git a/noir-projects/aztec-nr/aztec/src/standard_addresses.nr b/noir-projects/aztec-nr/aztec/src/standard_addresses.nr index f1c67723e7f5..dfe3a2f5c15f 100644 --- a/noir-projects/aztec-nr/aztec/src/standard_addresses.nr +++ b/noir-projects/aztec-nr/aztec/src/standard_addresses.nr @@ -14,5 +14,9 @@ pub global STANDARD_PUBLIC_CHECKS_ADDRESS: AztecAddress = AztecAddress::from_fie ); pub global STANDARD_HANDSHAKE_REGISTRY_ADDRESS: AztecAddress = AztecAddress::from_field( +<<<<<<< HEAD 0x090d0f9525c99184e3741290b2695c5e3af23f51e96061b74b953111b0d4f59d, +======= + 0x06127814dca78709650de6629637194f7381d2e05b35eb9d53a4746636c9aa9d, +>>>>>>> c3a2a8512c6 (chore: re-pin handshake registry with owner-bound nullifiers (#24893)) ); diff --git a/yarn-project/standard-contracts/src/standard_contract_data.ts b/yarn-project/standard-contracts/src/standard_contract_data.ts index 07c017457a9f..997a3aa6564f 100644 --- a/yarn-project/standard-contracts/src/standard_contract_data.ts +++ b/yarn-project/standard-contracts/src/standard_contract_data.ts @@ -26,15 +26,26 @@ export const StandardContractAddress: Record ), PublicChecks: AztecAddress.fromStringUnsafe('0x2f96c5a68fc1d04b13b76167a254f0a1a906c2dc0b571fcbca107254f064b045'), HandshakeRegistry: AztecAddress.fromStringUnsafe( +<<<<<<< HEAD '0x090d0f9525c99184e3741290b2695c5e3af23f51e96061b74b953111b0d4f59d', +======= + '0x06127814dca78709650de6629637194f7381d2e05b35eb9d53a4746636c9aa9d', +>>>>>>> c3a2a8512c6 (chore: re-pin handshake registry with owner-bound nullifiers (#24893)) ), }; export const StandardContractClassId: Record = { +<<<<<<< HEAD AuthRegistry: Fr.fromString('0x1d203dfe82143b105c236b42c42ede91728d4590e7c541f7d2423edeb68c31ae'), MultiCallEntrypoint: Fr.fromString('0x127ebfb260048794535198012df8e2f7599a46651efbf738c406f4baa3b960ca'), PublicChecks: Fr.fromString('0x087cada0973aa4e162f0830a61aed5a3f03d1e9578ce953aba213f2508d579a7'), HandshakeRegistry: Fr.fromString('0x041a88db4c2b65a224ad9c9d96c8365d972a98f47e2549a1841ac9897b8b7dc0'), +======= + AuthRegistry: Fr.fromString('0x04182c4b482c8e60c386e473f6dbb6bb3e2ef18e5156f7c9db5ac46af81abdcf'), + MultiCallEntrypoint: Fr.fromString('0x2f20566eaff9091697f8f5c43a19041267c7591d54074b57043eae90fca8ea64'), + PublicChecks: Fr.fromString('0x04bc93d415c4d48acab6063b6410f74e1cc257ba8e519020f8773b4ce8ccd31e'), + HandshakeRegistry: Fr.fromString('0x2e04c07c83ee8107e921c3ae4ade010ee183860a89b7534ea3367efb561d2c3b'), +>>>>>>> c3a2a8512c6 (chore: re-pin handshake registry with owner-bound nullifiers (#24893)) }; export const StandardContractClassIdPreimage: Record< @@ -57,8 +68,13 @@ export const StandardContractClassIdPreimage: Record< publicBytecodeCommitment: Fr.fromString('0x013c4f854a5c87c9daf86c5f9bc07a42c2a061f1d924a5b3564ec7edc8e18cb7'), }, HandshakeRegistry: { +<<<<<<< HEAD artifactHash: Fr.fromString('0x062da141c4114bcc93ac6cbe2fe30f0e8cbf820780b2225e958fe806d0e347a9'), privateFunctionsRoot: Fr.fromString('0x16c4666c93705b44b4a164ec6bcbfd7ec0ff593922f4a0e3bc158b9e3a1f95c1'), +======= + artifactHash: Fr.fromString('0x005a9c5db229999895a873e4c2bcaf6ca2523522b5c520c52c39b1bdb1b5faee'), + privateFunctionsRoot: Fr.fromString('0x1bb83fa540ba6654ffe4a4c75ec9349180a2f0ecd0d3851ca28d665ab2926c73'), +>>>>>>> c3a2a8512c6 (chore: re-pin handshake registry with owner-bound nullifiers (#24893)) publicBytecodeCommitment: Fr.fromString('0x0ce4c618c3ed7f3a20410e618c06bb701e150af7fe28a3e92f68e7733809f33e'), }, }; @@ -96,13 +112,21 @@ export const StandardContractPrivateFunctions: Record< selector: FunctionSelector.fromField( Fr.fromString('0x0000000000000000000000000000000000000000000000000000000019f8b409'), ), +<<<<<<< HEAD vkHash: Fr.fromString('0x0f230529c03e4877eeabfc0c659a1133e53f2d954b1d0f6092558cb02952da3e'), +======= + vkHash: Fr.fromString('0x195d78c4fa1f3f8accaa2dcc45115c5b0536e68cdd4c06f5999185554f7b73e4'), +>>>>>>> c3a2a8512c6 (chore: re-pin handshake registry with owner-bound nullifiers (#24893)) }, { selector: FunctionSelector.fromField( Fr.fromString('0x00000000000000000000000000000000000000000000000000000000db548fcf'), ), +<<<<<<< HEAD vkHash: Fr.fromString('0x2aa20cf767e6af6f99f4ceedcbdda34c2cd8a8e0763fb273810cf5abd7873b70'), +======= + vkHash: Fr.fromString('0x06ea812f1c792a864a36003312327d78995fdd894fbf48d54928a315e84df342'), +>>>>>>> c3a2a8512c6 (chore: re-pin handshake registry with owner-bound nullifiers (#24893)) }, { selector: FunctionSelector.fromField( From 093aa33fa3541a82adf75ca1bf747a172a4c5cc8 Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Thu, 23 Jul 2026 16:34:50 -0300 Subject: [PATCH 7/7] chore: resolve handshake registry port conflicts --- .../docs/resources/migration_notes.md | 18 -------------- .../aztec-nr/aztec/src/standard_addresses.nr | 4 ---- .../src/standard_contract_data.ts | 24 ------------------- 3 files changed, 46 deletions(-) diff --git a/docs/docs-developers/docs/resources/migration_notes.md b/docs/docs-developers/docs/resources/migration_notes.md index 1ffdc5b07466..dbb29701e344 100644 --- a/docs/docs-developers/docs/resources/migration_notes.md +++ b/docs/docs-developers/docs/resources/migration_notes.md @@ -9,25 +9,7 @@ Aztec is in active development. Each version may introduce breaking changes that ## TBD -<<<<<<< HEAD ## 5.0.1 -======= -### [Aztec.nr] Canonical HandshakeRegistry re-pinned at a new address - -The canonical `HandshakeRegistry` has been re-pinned so that it includes the owner's address in its `PrivateMutable` initialization nullifiers, keeping the handshake state of accounts that share keys independent. The registry moves to a new address. Handshakes established with the previous registry instance are not visible to the new one and must be re-established. The other standard contracts keep their addresses. - -### [Aztec.nr] Note property selectors are typed and use packed-layout indices - -The selectors in the generated `properties()` used the field's position in the note struct declaration, which pointed at the wrong packed field for any note with an earlier field packing to more than one `Field` (a `Point`, an array, a nested struct). Selector indices are now the field's offset in the note's packed representation, so `select`/`sort` criteria constrain the field they name. - -Breaking changes: - -- `PropertySelector` carries the selected property's type. Hand-constructed literals need a type annotation, e.g. `let selector: PropertySelector = PropertySelector { index: 0, offset: 0, length: 32 };`. -- `select`/`sort` reject properties that pack to more than one `Field` at compile time. -- `select` takes its value typed as the property's type. Cast the value if a mixed-type comparison was intentional. -- `properties()` cannot be used with a custom `Packable` layout. Define property selectors manually for such notes. -- Every note field type must implement `Packable`, even when the note's own `Packable` is hand-written. ->>>>>>> c3a2a8512c6 (chore: re-pin handshake registry with owner-bound nullifiers (#24893)) ### [Aztec.nr] History note nullification helpers renamed and restricted to own-contract notes diff --git a/noir-projects/aztec-nr/aztec/src/standard_addresses.nr b/noir-projects/aztec-nr/aztec/src/standard_addresses.nr index dfe3a2f5c15f..f1c67723e7f5 100644 --- a/noir-projects/aztec-nr/aztec/src/standard_addresses.nr +++ b/noir-projects/aztec-nr/aztec/src/standard_addresses.nr @@ -14,9 +14,5 @@ pub global STANDARD_PUBLIC_CHECKS_ADDRESS: AztecAddress = AztecAddress::from_fie ); pub global STANDARD_HANDSHAKE_REGISTRY_ADDRESS: AztecAddress = AztecAddress::from_field( -<<<<<<< HEAD 0x090d0f9525c99184e3741290b2695c5e3af23f51e96061b74b953111b0d4f59d, -======= - 0x06127814dca78709650de6629637194f7381d2e05b35eb9d53a4746636c9aa9d, ->>>>>>> c3a2a8512c6 (chore: re-pin handshake registry with owner-bound nullifiers (#24893)) ); diff --git a/yarn-project/standard-contracts/src/standard_contract_data.ts b/yarn-project/standard-contracts/src/standard_contract_data.ts index 997a3aa6564f..07c017457a9f 100644 --- a/yarn-project/standard-contracts/src/standard_contract_data.ts +++ b/yarn-project/standard-contracts/src/standard_contract_data.ts @@ -26,26 +26,15 @@ export const StandardContractAddress: Record ), PublicChecks: AztecAddress.fromStringUnsafe('0x2f96c5a68fc1d04b13b76167a254f0a1a906c2dc0b571fcbca107254f064b045'), HandshakeRegistry: AztecAddress.fromStringUnsafe( -<<<<<<< HEAD '0x090d0f9525c99184e3741290b2695c5e3af23f51e96061b74b953111b0d4f59d', -======= - '0x06127814dca78709650de6629637194f7381d2e05b35eb9d53a4746636c9aa9d', ->>>>>>> c3a2a8512c6 (chore: re-pin handshake registry with owner-bound nullifiers (#24893)) ), }; export const StandardContractClassId: Record = { -<<<<<<< HEAD AuthRegistry: Fr.fromString('0x1d203dfe82143b105c236b42c42ede91728d4590e7c541f7d2423edeb68c31ae'), MultiCallEntrypoint: Fr.fromString('0x127ebfb260048794535198012df8e2f7599a46651efbf738c406f4baa3b960ca'), PublicChecks: Fr.fromString('0x087cada0973aa4e162f0830a61aed5a3f03d1e9578ce953aba213f2508d579a7'), HandshakeRegistry: Fr.fromString('0x041a88db4c2b65a224ad9c9d96c8365d972a98f47e2549a1841ac9897b8b7dc0'), -======= - AuthRegistry: Fr.fromString('0x04182c4b482c8e60c386e473f6dbb6bb3e2ef18e5156f7c9db5ac46af81abdcf'), - MultiCallEntrypoint: Fr.fromString('0x2f20566eaff9091697f8f5c43a19041267c7591d54074b57043eae90fca8ea64'), - PublicChecks: Fr.fromString('0x04bc93d415c4d48acab6063b6410f74e1cc257ba8e519020f8773b4ce8ccd31e'), - HandshakeRegistry: Fr.fromString('0x2e04c07c83ee8107e921c3ae4ade010ee183860a89b7534ea3367efb561d2c3b'), ->>>>>>> c3a2a8512c6 (chore: re-pin handshake registry with owner-bound nullifiers (#24893)) }; export const StandardContractClassIdPreimage: Record< @@ -68,13 +57,8 @@ export const StandardContractClassIdPreimage: Record< publicBytecodeCommitment: Fr.fromString('0x013c4f854a5c87c9daf86c5f9bc07a42c2a061f1d924a5b3564ec7edc8e18cb7'), }, HandshakeRegistry: { -<<<<<<< HEAD artifactHash: Fr.fromString('0x062da141c4114bcc93ac6cbe2fe30f0e8cbf820780b2225e958fe806d0e347a9'), privateFunctionsRoot: Fr.fromString('0x16c4666c93705b44b4a164ec6bcbfd7ec0ff593922f4a0e3bc158b9e3a1f95c1'), -======= - artifactHash: Fr.fromString('0x005a9c5db229999895a873e4c2bcaf6ca2523522b5c520c52c39b1bdb1b5faee'), - privateFunctionsRoot: Fr.fromString('0x1bb83fa540ba6654ffe4a4c75ec9349180a2f0ecd0d3851ca28d665ab2926c73'), ->>>>>>> c3a2a8512c6 (chore: re-pin handshake registry with owner-bound nullifiers (#24893)) publicBytecodeCommitment: Fr.fromString('0x0ce4c618c3ed7f3a20410e618c06bb701e150af7fe28a3e92f68e7733809f33e'), }, }; @@ -112,21 +96,13 @@ export const StandardContractPrivateFunctions: Record< selector: FunctionSelector.fromField( Fr.fromString('0x0000000000000000000000000000000000000000000000000000000019f8b409'), ), -<<<<<<< HEAD vkHash: Fr.fromString('0x0f230529c03e4877eeabfc0c659a1133e53f2d954b1d0f6092558cb02952da3e'), -======= - vkHash: Fr.fromString('0x195d78c4fa1f3f8accaa2dcc45115c5b0536e68cdd4c06f5999185554f7b73e4'), ->>>>>>> c3a2a8512c6 (chore: re-pin handshake registry with owner-bound nullifiers (#24893)) }, { selector: FunctionSelector.fromField( Fr.fromString('0x00000000000000000000000000000000000000000000000000000000db548fcf'), ), -<<<<<<< HEAD vkHash: Fr.fromString('0x2aa20cf767e6af6f99f4ceedcbdda34c2cd8a8e0763fb273810cf5abd7873b70'), -======= - vkHash: Fr.fromString('0x06ea812f1c792a864a36003312327d78995fdd894fbf48d54928a315e84df342'), ->>>>>>> c3a2a8512c6 (chore: re-pin handshake registry with owner-bound nullifiers (#24893)) }, { selector: FunctionSelector.fromField(