diff --git a/labs b/labs index 703af8fbdc18..a488630bd484 160000 --- a/labs +++ b/labs @@ -1 +1 @@ -Subproject commit 703af8fbdc184f229944b807b51d463e3d630dc2 +Subproject commit a488630bd4847653dbb0e30eff0afb23f12c6587 diff --git a/labs-patches/0001-fix-link-epoch-proof-library-when-deploying-rollup.patch b/labs-patches/0001-fix-link-epoch-proof-library-when-deploying-rollup.patch deleted file mode 100644 index 6ca4c5618d04..000000000000 --- a/labs-patches/0001-fix-link-epoch-proof-library-when-deploying-rollup.patch +++ /dev/null @@ -1,51 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Alex Gherghisan -Date: Mon, 7 Sep 2026 09:54:14 +0000 -Subject: [PATCH] fix: link epoch proof library when deploying rollup - -Register EpochProofExtLib with the Rollup deployment artifacts and verify that every link reference has deployable bytecode. Requires regenerated foundation L1 artifacts exporting the epoch proof library. - -diff --git a/yarn-project/ethereum/src/l1_artifacts.test.ts b/yarn-project/ethereum/src/l1_artifacts.test.ts -new file mode 100644 -index 0000000000000000000000000000000000000000..570d2852ce95b796c9614638303dc6708e8a0dd7 ---- /dev/null -+++ b/yarn-project/ethereum/src/l1_artifacts.test.ts -@@ -0,0 +1,13 @@ -+import { RollupArtifact } from './l1_artifacts.js'; -+ -+describe('Rollup deployment artifacts', () => { -+ it('supplies deployable bytecode for every linked library', () => { -+ const libraries: Record = RollupArtifact.libraries.libraryCode; -+ for (const references of Object.values(RollupArtifact.libraries.linkReferences)) { -+ for (const name of Object.keys(references)) { -+ expect(libraries[name]).toBeDefined(); -+ expect(libraries[name]?.contractBytecode).toMatch(/^0x[0-9a-f]+$/i); -+ } -+ } -+ }); -+}); -diff --git a/yarn-project/ethereum/src/l1_artifacts.ts b/yarn-project/ethereum/src/l1_artifacts.ts -index d404aac730012d52c0c095fdddbf6a816b8ec705..2e6979daa1c516ddb8741772f705985674bf0f4c 100644 ---- a/yarn-project/ethereum/src/l1_artifacts.ts -+++ b/yarn-project/ethereum/src/l1_artifacts.ts -@@ -3,6 +3,8 @@ import { - CoinIssuerBytecode, - DateGatedRelayerAbi, - DateGatedRelayerBytecode, -+ EpochProofExtLibAbi, -+ EpochProofExtLibBytecode, - FeeAssetHandlerAbi, - FeeAssetHandlerBytecode, - FeeJuicePortalAbi, -@@ -91,6 +93,11 @@ export const RollupArtifact = { - contractAbi: RollupOperationsExtLibAbi, - contractBytecode: RollupOperationsExtLibBytecode as Hex, - }, -+ EpochProofExtLib: { -+ name: 'EpochProofExtLib', -+ contractAbi: EpochProofExtLibAbi, -+ contractBytecode: EpochProofExtLibBytecode as Hex, -+ }, - ValidatorOperationsExtLib: { - name: 'ValidatorOperationsExtLib', - contractAbi: ValidatorOperationsExtLibAbi, diff --git a/labs-patches/0002-feat-submit-compact-fees-for-proven-checkpoints.patch b/labs-patches/0002-feat-submit-compact-fees-for-proven-checkpoints.patch deleted file mode 100644 index eaefe1e98d4f..000000000000 --- a/labs-patches/0002-feat-submit-compact-fees-for-proven-checkpoints.patch +++ /dev/null @@ -1,255 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Alex Gherghisan -Date: Mon, 7 Sep 2026 09:55:13 +0000 -Subject: [PATCH] feat: submit compact fees for proven checkpoints - -Use the proven tip observed during validation to send only coinbase and accumulated fees for the proven prefix. Keep full headers for the suffix and for public-input validation. Apply the same encoding to gas estimation and test fresh, partial, and fully proven prefixes. - -diff --git a/yarn-project/prover-node/src/prover-node-publisher.test.ts b/yarn-project/prover-node/src/prover-node-publisher.test.ts -index 390a9d84f0082ae241c9a38dc2b37b75f98579b8..8d78e336101d82bfe1ad2ccc3c5dd9339c01c1cb 100644 ---- a/yarn-project/prover-node/src/prover-node-publisher.test.ts -+++ b/yarn-project/prover-node/src/prover-node-publisher.test.ts -@@ -1,3 +1,5 @@ -+import { RollupAbi } from '@aztec-foundation/l1-artifacts'; -+ - import { BatchedBlob } from '@aztec-labs/blob-lib/types'; - import type { RollupContract } from '@aztec-labs/ethereum/contracts'; - import { randomL1ContractAddresses } from '@aztec-labs/ethereum/l1-contract-addresses'; -@@ -13,6 +15,7 @@ import { Proof } from '@aztec-labs/stdlib/proofs'; - import { CheckpointHeader, RootRollupPublicInputs } from '@aztec-labs/stdlib/rollup'; - import { jest } from '@jest/globals'; - import { type MockProxy, mock } from 'jest-mock-extended'; -+import { decodeFunctionData, getAddress } from 'viem'; - - import { ProverNodePublisher } from './prover-node-publisher.js'; - -@@ -197,6 +200,30 @@ describe('prover-node-publisher', () => { - }, - ); - -+ describe('compact checkpoint headers', () => { -+ it.each([ -+ { proven: 32, end: 48, prefixLength: 0 }, -+ { proven: 40, end: 48, prefixLength: 8 }, -+ { proven: 48, end: 48, prefixLength: 16 }, -+ ])('encodes $prefixLength compact entries when proven is $proven', async ({ proven, end, prefixLength }) => { -+ const args = setupPublishData(64, proven, 33, end); -+ await publisher.submitEpochProof(args); -+ const [{ data }] = l1Utils.sendAndMonitorTransaction.mock.calls[0]; -+ const decoded = decodeFunctionData({ abi: RollupAbi, data: data! }); -+ if (decoded.functionName !== 'submitEpochRootProof') { -+ throw new Error(`Unexpected function ${decoded.functionName}`); -+ } -+ const [submission] = decoded.args; -+ const fullHeaders = args.headers.map(header => header.toViem()); -+ const headers = fullHeaders.map(header => ({ ...header, coinbase: getAddress(header.coinbase) })); -+ expect(submission.provenCheckpointFees).toEqual( -+ headers.slice(0, prefixLength).map(({ coinbase, accumulatedFees }) => ({ coinbase, accumulatedFees })), -+ ); -+ expect(submission.headers).toEqual(headers.slice(prefixLength)); -+ expect(rollup.getEpochProofPublicInputs.mock.calls[0][0][3]).toEqual(fullHeaders); -+ }); -+ }); -+ - describe('proof submission target', () => { - it('defaults the submit tx target to the rollup address', async () => { - const rollupAddress = EthAddress.random().toString(); -@@ -227,11 +254,11 @@ describe('prover-node-publisher', () => { - }); - }); - -- it('analyzeEpochProofSubmission validates, estimates, and does not send tx', async () => { -+ it.each([32, 40, 64])('estimates compact calldata without sending when proven is %i', async proven => { - const fromCheckpoint = 33; - const toCheckpoint = 64; - -- rollup.getTips.mockResolvedValue({ pending: CheckpointNumber(65), proven: CheckpointNumber(32) }); -+ rollup.getTips.mockResolvedValue({ pending: CheckpointNumber(65), proven: CheckpointNumber(proven) }); - - const checkpoints = Array.from({ length: 100 }, () => RootRollupPublicInputs.random()); - rollup.getCheckpoint.mockImplementation((n: CheckpointNumber) => -@@ -271,18 +298,33 @@ describe('prover-node-publisher', () => { - ourPublicInputs.blobPublicInputs.c.negate(), - ); - -+ const headers = makeHeadersForRange(fromCheckpoint, toCheckpoint); - await publisher.analyzeEpochProofSubmission({ - epochNumber: EpochNumber(2), - fromCheckpoint: CheckpointNumber(fromCheckpoint), - toCheckpoint: CheckpointNumber(toCheckpoint), - publicInputs: ourPublicInputs, -- headers: makeHeadersForRange(fromCheckpoint, toCheckpoint), -+ headers, - proof: Proof.empty(), - batchedBlobInputs: batchedBlob, - attestations: [], - }); - -- expect(l1Utils.estimateGas).toHaveBeenCalled(); -+ const [, { data }] = l1Utils.estimateGas.mock.calls[0]; -+ const decoded = decodeFunctionData({ abi: RollupAbi, data: data! }); -+ if (decoded.functionName !== 'submitEpochRootProof') { -+ throw new Error(`Unexpected function ${decoded.functionName}`); -+ } -+ const [submission] = decoded.args; -+ const prefixLength = proven - fromCheckpoint + 1; -+ const expectedHeaders = headers.map(header => { -+ const viem = header.toViem(); -+ return { ...viem, coinbase: getAddress(viem.coinbase) }; -+ }); -+ expect(submission.provenCheckpointFees).toEqual( -+ expectedHeaders.slice(0, prefixLength).map(({ coinbase, accumulatedFees }) => ({ coinbase, accumulatedFees })), -+ ); -+ expect(submission.headers).toEqual(expectedHeaders.slice(prefixLength)); - expect(l1Utils.getFeesPerGas).toHaveBeenCalled(); - expect(l1Utils.sendAndMonitorTransaction).not.toHaveBeenCalled(); - }); -diff --git a/yarn-project/prover-node/src/prover-node-publisher.ts b/yarn-project/prover-node/src/prover-node-publisher.ts -index 3b66d55bcfff92c4d79498c691e76d001d2be749..086ab3340b18fb88489444723153c6a5f8846dec 100644 ---- a/yarn-project/prover-node/src/prover-node-publisher.ts -+++ b/yarn-project/prover-node/src/prover-node-publisher.ts -@@ -89,9 +89,9 @@ export class ProverNodePublisher { - - const timer = new Timer(); - // Validate epoch proof range and hashes are correct before submitting -- await this.validateEpochProofSubmission(args); -+ const provenPrefixLength = await this.validateEpochProofSubmission(args); - -- const txReceipt = await this.sendSubmitEpochProofTx(args); -+ const txReceipt = await this.sendSubmitEpochProofTx(args, provenPrefixLength); - if (!txReceipt) { - this.log.error(`Failed to mine submitEpochProof tx`, undefined, ctx); - return false; -@@ -138,7 +138,7 @@ export class ProverNodePublisher { - batchedBlobInputs: BatchedBlob; - attestations: ViemCommitteeAttestation[]; - headers: CheckpointHeader[]; -- }) { -+ }): Promise { - const { fromCheckpoint, toCheckpoint, publicInputs, batchedBlobInputs } = args; - - // Check that the checkpoint numbers match the expected epoch to be proven -@@ -196,6 +196,10 @@ export class ProverNodePublisher { - log: this.log, - }); - } -+ -+ // The production rollup advances the proven tip and accounts for rewards atomically. A later proof may -+ // advance it further before inclusion; the contract accepts any already-proven prefix, including a shorter one. -+ return Math.max(0, proven - fromCheckpoint + 1); - } - - /** -@@ -215,9 +219,9 @@ export class ProverNodePublisher { - }): Promise { - const { epochNumber, fromCheckpoint, toCheckpoint } = args; - -- await this.validateEpochProofSubmission(args); -+ const provenPrefixLength = await this.validateEpochProofSubmission(args); - -- const data = this.encodeSubmitEpochProofCalldata(args); -+ const data = this.encodeSubmitEpochProofCalldata(args, provenPrefixLength); - const senderAddress = this.l1TxUtils.getSenderAddress(); - - const [gasLimit, feesPerGas, latestBlock] = await Promise.all([ -@@ -252,33 +256,39 @@ export class ProverNodePublisher { - this.metrics.recordEstimatedSubmitProof(stats); - } - -- private encodeSubmitEpochProofCalldata(args: { -- fromCheckpoint: CheckpointNumber; -- toCheckpoint: CheckpointNumber; -- publicInputs: RootRollupPublicInputs; -- proof: Proof; -- batchedBlobInputs: BatchedBlob; -- attestations: ViemCommitteeAttestation[]; -- headers: CheckpointHeader[]; -- }): Hex { -+ private encodeSubmitEpochProofCalldata( -+ args: { -+ fromCheckpoint: CheckpointNumber; -+ toCheckpoint: CheckpointNumber; -+ publicInputs: RootRollupPublicInputs; -+ proof: Proof; -+ batchedBlobInputs: BatchedBlob; -+ attestations: ViemCommitteeAttestation[]; -+ headers: CheckpointHeader[]; -+ }, -+ provenPrefixLength: number, -+ ): Hex { - return encodeFunctionData({ - abi: RollupAbi, - functionName: 'submitEpochRootProof', -- args: [this.getSubmitEpochProofArgs(args)], -+ args: [this.getSubmitEpochProofArgs(args, provenPrefixLength)], - }); - } - -- private async sendSubmitEpochProofTx(args: { -- fromCheckpoint: CheckpointNumber; -- toCheckpoint: CheckpointNumber; -- deadline?: Date; -- publicInputs: RootRollupPublicInputs; -- proof: Proof; -- batchedBlobInputs: BatchedBlob; -- attestations: ViemCommitteeAttestation[]; -- headers: CheckpointHeader[]; -- }): Promise { -- const txArgs = [this.getSubmitEpochProofArgs(args)] as const; -+ private async sendSubmitEpochProofTx( -+ args: { -+ fromCheckpoint: CheckpointNumber; -+ toCheckpoint: CheckpointNumber; -+ deadline?: Date; -+ publicInputs: RootRollupPublicInputs; -+ proof: Proof; -+ batchedBlobInputs: BatchedBlob; -+ attestations: ViemCommitteeAttestation[]; -+ headers: CheckpointHeader[]; -+ }, -+ provenPrefixLength: number, -+ ): Promise { -+ const txArgs = [this.getSubmitEpochProofArgs(args, provenPrefixLength)] as const; - - this.log.info(`Submitting epoch proof to L1 rollup contract`, { - proofSize: args.proof.withoutPublicInputs().length, -@@ -342,15 +352,18 @@ export class ProverNodePublisher { - ] as const; - } - -- private getSubmitEpochProofArgs(args: { -- fromCheckpoint: CheckpointNumber; -- toCheckpoint: CheckpointNumber; -- publicInputs: RootRollupPublicInputs; -- proof: Proof; -- batchedBlobInputs: BatchedBlob; -- attestations: ViemCommitteeAttestation[]; -- headers: CheckpointHeader[]; -- }) { -+ private getSubmitEpochProofArgs( -+ args: { -+ fromCheckpoint: CheckpointNumber; -+ toCheckpoint: CheckpointNumber; -+ publicInputs: RootRollupPublicInputs; -+ proof: Proof; -+ batchedBlobInputs: BatchedBlob; -+ attestations: ViemCommitteeAttestation[]; -+ headers: CheckpointHeader[]; -+ }, -+ provenPrefixLength: number, -+ ) { - // Returns arguments for EpochProofLib.sol -> submitEpochRootProof() - const proofHex: Hex = `0x${args.proof.withoutPublicInputs().toString('hex')}`; - const argsArray = this.getEpochProofPublicInputsArgs(args); -@@ -358,7 +371,10 @@ export class ProverNodePublisher { - start: argsArray[0], - end: argsArray[1], - args: argsArray[2], -- headers: argsArray[3], -+ provenCheckpointFees: argsArray[3] -+ .slice(0, provenPrefixLength) -+ .map(({ coinbase, accumulatedFees }) => ({ coinbase, accumulatedFees })), -+ headers: argsArray[3].slice(provenPrefixLength), - attestations: CommitteeAttestationsAndSigners.packAttestations( - args.attestations.map(a => CommitteeAttestation.fromViem(a)), - ), diff --git a/labs-patches/0003-feat-introduce-a-protocol-fee-margin-AZIP-23.patch b/labs-patches/0003-feat-introduce-a-protocol-fee-margin-AZIP-23.patch deleted file mode 100644 index 6ea19a3fb7fb..000000000000 --- a/labs-patches/0003-feat-introduce-a-protocol-fee-margin-AZIP-23.patch +++ /dev/null @@ -1,717 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: aminsammara -Date: Fri, 28 Aug 2026 00:00:00 +0000 -Subject: [PATCH] feat: introduce a protocol fee margin (AZIP-23) - - -diff --git a/yarn-project/aztec-node/src/aztec-node/node_public_calls_simulator.test.ts b/yarn-project/aztec-node/src/aztec-node/node_public_calls_simulator.test.ts -index 3e3e2adc5e26b329a00eae741910f6f7cc1eb1fa..49ab85d4e8d054112add643c99e87104a5f08f0a 100644 ---- a/yarn-project/aztec-node/src/aztec-node/node_public_calls_simulator.test.ts -+++ b/yarn-project/aztec-node/src/aztec-node/node_public_calls_simulator.test.ts -@@ -444,7 +444,7 @@ describe('NodePublicCallsSimulator', () => { - }); - - function makeFeeHeader(): FeeHeader { -- return { excessMana: 0n, manaUsed: 0n, ethPerFeeAsset: 0n, congestionCost: 0n, proverCost: 0n }; -+ return { excessMana: 0n, manaUsed: 0n, ethPerFeeAsset: 0n, protocolFee: 0n, proverCost: 0n }; - } - - function makeProposedCheckpointData(args: { -diff --git a/yarn-project/end-to-end/src/single-node/fees/fees_test.ts b/yarn-project/end-to-end/src/single-node/fees/fees_test.ts -index b7daa424c26980a069ee2620483ea5c4317b96b4..c1713e320beb40c957579d0707d3d750adb380bb 100644 ---- a/yarn-project/end-to-end/src/single-node/fees/fees_test.ts -+++ b/yarn-project/end-to-end/src/single-node/fees/fees_test.ts -@@ -381,13 +381,14 @@ export class FeesTest extends SingleNodeTestContext { - return feeHeader.manaUsed * feeHeader.proverCost; - }; - -- // RewardLib computes sequencerFee = checkpointFee - burn - proverFee where burn = manaUsed * congestionCost. -- // The fixture's typical case keeps congestionCost at zero, but reading it explicitly avoids latent bugs -- // when test load changes excess mana. -+ // RewardLib computes sequencerFee = checkpointFee - protocolFee - proverFee where -+ // protocolFee = manaUsed * feeHeader.protocolFee. The fixture's typical case keeps the -+ // protocol fee at zero, but reading it explicitly avoids latent bugs when test load changes -+ // excess mana. - this.getCommittedBurn = async (blockNumber: BlockNumber) => { - const block = await this.aztecNode.getBlock(blockNumber); - const feeHeader = await this.rollupContract.getFeeHeader(BigInt(block!.checkpointNumber)); -- return feeHeader.manaUsed * feeHeader.congestionCost; -+ return feeHeader.manaUsed * feeHeader.protocolFee; - }; - } - -diff --git a/yarn-project/ethereum/src/contracts/fee_asset_price_oracle.test.ts b/yarn-project/ethereum/src/contracts/fee_asset_price_oracle.test.ts -index 4e56319a2a5b7d47f10a4011e8794d4fdd772907..a9066db9710099da5ea776f723d830dba7b52a90 100644 ---- a/yarn-project/ethereum/src/contracts/fee_asset_price_oracle.test.ts -+++ b/yarn-project/ethereum/src/contracts/fee_asset_price_oracle.test.ts -@@ -112,7 +112,7 @@ describe('Uniswap Price Oracle', () => { - expect(modifier).toBe(49n); - - const child = RollupContract.computeChildFeeHeader( -- { excessMana: 0n, manaUsed: 0n, ethPerFeeAsset: predictedParentE12, congestionCost: 0n, proverCost: 0n }, -+ { excessMana: 0n, manaUsed: 0n, ethPerFeeAsset: predictedParentE12, protocolFee: 0n, proverCost: 0n }, - 0n, - modifier, - 100n, -diff --git a/yarn-project/ethereum/src/contracts/rollup.test.ts b/yarn-project/ethereum/src/contracts/rollup.test.ts -index 31f7d71141d6d8d51707ff7c87f9f8d6f52eb048..2262d7755c2938fd1cf066743c63196e77fab7a9 100644 ---- a/yarn-project/ethereum/src/contracts/rollup.test.ts -+++ b/yarn-project/ethereum/src/contracts/rollup.test.ts -@@ -22,7 +22,7 @@ import { type FeeHeader, RollupContract, TempCheckpointLogField } from './rollup - describe('compressFeeHeader', () => { - /** Creates a zero fee header with the given overrides. */ - function makeFeeHeader(overrides: Partial = {}): FeeHeader { -- return { manaUsed: 0n, excessMana: 0n, ethPerFeeAsset: 0n, congestionCost: 0n, proverCost: 0n, ...overrides }; -+ return { manaUsed: 0n, excessMana: 0n, ethPerFeeAsset: 0n, protocolFee: 0n, proverCost: 0n, ...overrides }; - } - - it('sets the preheat flag (bit 255)', () => { -@@ -62,15 +62,15 @@ describe('compressFeeHeader', () => { - expect((result >> 80n) & ((1n << 48n) - 1n)).toBe(999n); - }); - -- it('packs congestionCost into bits [128:191]', () => { -- const header = makeFeeHeader({ congestionCost: 42n }); -+ it('packs protocolFee into bits [128:191]', () => { -+ const header = makeFeeHeader({ protocolFee: 42n }); - const result = RollupContract.compressFeeHeader(header); - expect((result >> 128n) & ((1n << 64n) - 1n)).toBe(42n); - }); - -- it('clamps congestionCost to 64 bits', () => { -+ it('clamps protocolFee to 64 bits', () => { - const maxValue = (1n << 64n) - 1n; -- const header = makeFeeHeader({ congestionCost: maxValue + 1n }); -+ const header = makeFeeHeader({ protocolFee: maxValue + 1n }); - const result = RollupContract.compressFeeHeader(header); - expect((result >> 128n) & maxValue).toBe(maxValue); - }); -@@ -93,7 +93,7 @@ describe('compressFeeHeader', () => { - manaUsed: 1000n, - excessMana: 2000n, - ethPerFeeAsset: 3000n, -- congestionCost: 4000n, -+ protocolFee: 4000n, - proverCost: 5000n, - }); - const result = RollupContract.compressFeeHeader(header); -@@ -122,7 +122,7 @@ describe('computeChildFeeHeader', () => { - manaUsed: 5000n, - excessMana: 3000n, - ethPerFeeAsset: 1000n, -- congestionCost: 100n, -+ protocolFee: 100n, - proverCost: 200n, - }; - -@@ -144,9 +144,9 @@ describe('computeChildFeeHeader', () => { - expect(result.manaUsed).toBe(7777n); - }); - -- it('always sets congestionCost and proverCost to zero', () => { -+ it('always sets protocolFee and proverCost to zero', () => { - const result = RollupContract.computeChildFeeHeader(baseFeeHeader, 0n, 0n, manaTarget); -- expect(result.congestionCost).toBe(0n); -+ expect(result.protocolFee).toBe(0n); - expect(result.proverCost).toBe(0n); - }); - -@@ -217,14 +217,14 @@ describe('computeChildFeeHeader', () => { - manaUsed: 8000n, - excessMana: 15000n, - ethPerFeeAsset: 5000n, -- congestionCost: 999n, -+ protocolFee: 999n, - proverCost: 888n, - }; - const result = RollupContract.computeChildFeeHeader(parent, 42n, 250n, manaTarget); - expect(result.excessMana).toBe(13000n); - expect(result.manaUsed).toBe(42n); - expect(result.ethPerFeeAsset).toBe(5125n); -- expect(result.congestionCost).toBe(0n); -+ expect(result.protocolFee).toBe(0n); - expect(result.proverCost).toBe(0n); - }); - }); -@@ -425,7 +425,7 @@ describe('Rollup', () => { - manaUsed: 12345n, - excessMana: 67890n, - ethPerFeeAsset: 1_000_000_000_000n, -- congestionCost: 99999n, -+ protocolFee: 99999n, - proverCost: 55555n, - } as FeeHeader, - }; -@@ -561,7 +561,7 @@ describe('Rollup', () => { - manaUsed: 12345n, - excessMana: 67890n, - ethPerFeeAsset: 1_000_000_000_000n, -- congestionCost: 99999n, -+ protocolFee: 99999n, - proverCost: 55555n, - }; - -@@ -580,7 +580,7 @@ describe('Rollup', () => { - expect(result.manaUsed).toBe(feeHeader.manaUsed); - expect(result.excessMana).toBe(feeHeader.excessMana); - expect(result.ethPerFeeAsset).toBe(feeHeader.ethPerFeeAsset); -- expect(result.congestionCost).toBe(feeHeader.congestionCost); -+ expect(result.protocolFee).toBe(feeHeader.protocolFee); - expect(result.proverCost).toBe(feeHeader.proverCost); - }); - }); -diff --git a/yarn-project/ethereum/src/contracts/rollup.ts b/yarn-project/ethereum/src/contracts/rollup.ts -index af4ac517775b024a0bc1fa86f9ac8de0d3a2a6f7..cd27a307a7c8c8182b9f0e19647914424a20f5e6 100644 ---- a/yarn-project/ethereum/src/contracts/rollup.ts -+++ b/yarn-project/ethereum/src/contracts/rollup.ts -@@ -114,7 +114,7 @@ export type FeeHeader = { - excessMana: bigint; - manaUsed: bigint; - ethPerFeeAsset: bigint; -- congestionCost: bigint; -+ protocolFee: bigint; - proverCost: bigint; - }; - -@@ -177,7 +177,7 @@ export type TempCheckpointLogOverrideFields = { - export type ManaMinFeeComponents = { - sequencerCost: bigint; - proverCost: bigint; -- congestionCost: bigint; -+ protocolFee: bigint; - congestionMultiplier: bigint; - }; - -@@ -432,6 +432,16 @@ export class RollupContract { - return this.rollup.read.getProvingCostPerManaInFeeAsset(); - } - -+ /** Returns the current protocol fee margin in basis points. Not memoized: governance can change it. */ -+ getProtocolFeeMargin(): Promise { -+ return this.rollup.read.getProtocolFeeMargin(); -+ } -+ -+ /** Returns the current recipient of the protocol fee tranche. Not memoized: governance can change it. */ -+ async getProtocolFeeRecipient(): Promise { -+ return EthAddress.fromString(await this.rollup.read.getProtocolFeeRecipient()); -+ } -+ - @memoize - getManaLimit(): Promise { - return this.rollup.read.getManaLimit(); -@@ -636,7 +646,7 @@ export class RollupContract { - excessMana: result.excessMana, - manaUsed: result.manaUsed, - ethPerFeeAsset: result.ethPerFeeAsset, -- congestionCost: result.congestionCost, -+ protocolFee: result.protocolFee, - proverCost: result.proverCost, - }; - } -@@ -729,7 +739,7 @@ export class RollupContract { - excessMana: result.feeHeader.excessMana, - manaUsed: result.feeHeader.manaUsed, - ethPerFeeAsset: result.feeHeader.ethPerFeeAsset, -- congestionCost: result.feeHeader.congestionCost, -+ protocolFee: result.feeHeader.protocolFee, - proverCost: result.feeHeader.proverCost, - }, - }; -@@ -1079,7 +1089,7 @@ export class RollupContract { - let value = BigInt(feeHeader.manaUsed) & ((1n << 32n) - 1n); // bits [0:31] - value |= (feeHeader.excessMana < MASK_48_BITS ? feeHeader.excessMana : MASK_48_BITS) << 32n; // bits [32:79] - value |= (BigInt(feeHeader.ethPerFeeAsset) & MASK_48_BITS) << 80n; // bits [80:127] -- value |= (feeHeader.congestionCost < MASK_64_BITS ? feeHeader.congestionCost : MASK_64_BITS) << 128n; // bits [128:191] -+ value |= (feeHeader.protocolFee < MASK_64_BITS ? feeHeader.protocolFee : MASK_64_BITS) << 128n; // bits [128:191] - value |= (feeHeader.proverCost < MASK_63_BITS ? feeHeader.proverCost : MASK_63_BITS) << 192n; // bits [192:254] - value |= 1n << 255n; // preheat flag - return value; -@@ -1121,7 +1131,7 @@ export class RollupContract { - excessMana, - manaUsed: childManaUsed, - ethPerFeeAsset: newPrice, -- congestionCost: 0n, -+ protocolFee: 0n, - proverCost: 0n, - }; - } -@@ -1183,7 +1193,7 @@ export class RollupContract { - return { - sequencerCost: result.sequencerCost, - proverCost: result.proverCost, -- congestionCost: result.congestionCost, -+ protocolFee: result.protocolFee, - congestionMultiplier: result.congestionMultiplier, - }; - } -diff --git a/yarn-project/ethereum/src/test/chain_monitor.ts b/yarn-project/ethereum/src/test/chain_monitor.ts -index 272bf7fae3e907674b0b70f34b6cbcc67d90f95a..07539fdac4b7d7fb86ea1f06833ad1641731f88b 100644 ---- a/yarn-project/ethereum/src/test/chain_monitor.ts -+++ b/yarn-project/ethereum/src/test/chain_monitor.ts -@@ -11,7 +11,7 @@ import type { ViemClient } from '../types.js'; - - /** L2 fee data reported by the chain monitor. */ - export type L2FeeData = ManaMinFeeComponents & { -- /** Total minimum fee per mana in Fee Juice (sum of sequencerCost + proverCost + congestionCost). */ -+ /** Total minimum fee per mana in Fee Juice (sum of sequencerCost + proverCost + protocolFee). */ - minFeePerMana: bigint; - /** L1 base fee observed by the oracle. */ - l1BaseFee: bigint; -@@ -380,7 +380,7 @@ export class ChainMonitor extends EventEmitter { - return ( - this.l2FeeData.sequencerCost !== newData.sequencerCost || - this.l2FeeData.proverCost !== newData.proverCost || -- this.l2FeeData.congestionCost !== newData.congestionCost || -+ this.l2FeeData.protocolFee !== newData.protocolFee || - this.l2FeeData.l1BaseFee !== newData.l1BaseFee || - this.l2FeeData.l1BlobFee !== newData.l1BlobFee || - this.l2FeeData.ethPerFeeAsset !== newData.ethPerFeeAsset -diff --git a/yarn-project/ethereum/src/test/rollup_cheat_codes.ts b/yarn-project/ethereum/src/test/rollup_cheat_codes.ts -index ee6523711676e1014f2f502b2fa5324c52188db0..8f8d34fbdb4af3b7370b408b08764dd411665749 100644 ---- a/yarn-project/ethereum/src/test/rollup_cheat_codes.ts -+++ b/yarn-project/ethereum/src/test/rollup_cheat_codes.ts -@@ -374,6 +374,30 @@ export class RollupCheatCodes { - }); - } - -+ /** -+ * Sets the protocol fee margin (in basis points). Throws if the on-chain tx reverts -+ * (e.g. rate-limit cooldown or step cap) instead of silently succeeding. -+ * @param bps - The new protocol fee margin in basis points -+ */ -+ public async setProtocolFeeMargin(bps: number) { -+ await this.asOwner(async (account, rollup) => { -+ const hash = await rollup.write.setProtocolFeeMargin([bps], { -+ account, -+ chain: this.client.chain, -+ gasLimit: 1000000n, -+ }); -+ const receipt = await this.client.waitForTransactionReceipt({ hash }); -+ if (receipt.status !== 'success') { -+ throw new Error( -+ `setProtocolFeeMargin(${bps}) reverted on L1 (tx ${hash}). ` + -+ `Likely FeeLib rate-limit (30-day cooldown or x3/2 step cap on the fee multiplier); ` + -+ `use clearProvingCostCooldown() between successive updates (it clears both cooldowns).`, -+ ); -+ } -+ this.logger.warn(`Updated protocol fee margin to ${bps} bps`); -+ }); -+ } -+ - /** - * Resets the 30-day proving-cost update cooldown enforced by FeeLib.updateProvingCostPerMana - * by zeroing `FeeStore.provingCostLastUpdate` directly in contract storage. Use between -@@ -384,7 +408,8 @@ export class RollupCheatCodes { - * l1-contracts/src/core/libraries/rollup/FeeLib.sol: - * slot + 0: CompressedFeeConfig config (uint256) - * slot + 1: L1GasOracleValues l1GasOracleValues (14+14+4 bytes, packed) -- * slot + 2: uint64 provingCostLastUpdate (only member — zeroing the slot is safe) -+ * slot + 2: uint64 provingCostLastUpdate + uint64 protocolMarginLastUpdate (packed -- -+ * zeroing the slot clears BOTH the proving-cost and protocol-fee-margin cooldowns) - * If the struct layout changes, update the offset below. - */ - public async clearProvingCostCooldown() { -diff --git a/yarn-project/prover-node/src/prover-node-publisher.test.ts b/yarn-project/prover-node/src/prover-node-publisher.test.ts -index 8d78e336101d82bfe1ad2ccc3c5dd9339c01c1cb..837be0bf558db9548361431980a74f3579ce2cc7 100644 ---- a/yarn-project/prover-node/src/prover-node-publisher.test.ts -+++ b/yarn-project/prover-node/src/prover-node-publisher.test.ts -@@ -75,7 +75,7 @@ describe('prover-node-publisher', () => { - excessMana: 0n, // unused - manaUsed: 0n, // unused - ethPerFeeAsset: 0n, // unused -- congestionCost: 0n, // unused -+ protocolFee: 0n, // unused - proverCost: 0n, // unused - }, - }), -@@ -270,7 +270,7 @@ describe('prover-node-publisher', () => { - blobCommitmentsHash: Buffer32.ZERO, - outHash: '0x', - slotNumber: SlotNumber(0), -- feeHeader: { excessMana: 0n, manaUsed: 0n, ethPerFeeAsset: 0n, congestionCost: 0n, proverCost: 0n }, -+ feeHeader: { excessMana: 0n, manaUsed: 0n, ethPerFeeAsset: 0n, protocolFee: 0n, proverCost: 0n }, - }), - ); - -@@ -352,7 +352,7 @@ describe('prover-node-publisher', () => { - excessMana: 0n, // unused - manaUsed: 0n, // unused - ethPerFeeAsset: 0n, // unused -- congestionCost: 0n, // unused -+ protocolFee: 0n, // unused - proverCost: 0n, // unused - }, - }), -diff --git a/yarn-project/sequencer-client/src/global_variable_builder/fee_predictor.test.ts b/yarn-project/sequencer-client/src/global_variable_builder/fee_predictor.test.ts -index 7260dd2046fae0828284f7608982b59a7b3f6655..89548b5f6a8aa878ff147f9a05a2e17cc35867f6 100644 ---- a/yarn-project/sequencer-client/src/global_variable_builder/fee_predictor.test.ts -+++ b/yarn-project/sequencer-client/src/global_variable_builder/fee_predictor.test.ts -@@ -267,7 +267,7 @@ describe('FeePredictor', () => { - excessMana: newExcessMana, - manaUsed: assumedManaUsed, - ethPerFeeAsset: decayEthPerFeeAsset(currentFeeHeader.ethPerFeeAsset, i + 1), -- congestionCost: 0n, -+ protocolFee: 0n, - proverCost: 0n, - }; - -@@ -344,7 +344,7 @@ describe('FeePredictor', () => { - excessMana: newExcessMana, - manaUsed: 0n, - ethPerFeeAsset: decayedEthPerFeeAsset, -- congestionCost: 0n, -+ protocolFee: 0n, - proverCost: 0n, - }; - -@@ -362,6 +362,33 @@ describe('FeePredictor', () => { - nextCheckpointOffset++; - } - }, 60_000); -+ -+ it('slot 0 matches L1 getManaMinFeeAt with a nonzero protocol fee margin', async () => { -+ // Pin the comparison timestamp so before/after fees differ only by the margin. -+ const startSlot = await getPredictionStartSlot(); -+ const timestamp = getTimestamp(startSlot + 1n); -+ const feeBefore = await rollup.getManaMinFeeAt(timestamp, true); -+ -+ // The first-ever margin update bypasses the 30-day cooldown; from 0 the x3/2 step cap on the -+ // fee multiplier permits up to 5000 bps. -+ await rollupCheatCodes.setProtocolFeeMargin(5000); -+ expect(await rollup.getProtocolFeeMargin()).toBe(5000); -+ -+ // The margin must move the pinned fee. If L1 scaled both the fakeExponential factor and the -+ // mulDiv divisor, mu would cancel silently and this catches it. -+ const l1Fee = await rollup.getManaMinFeeAt(timestamp, true); -+ expect(l1Fee).toBeGreaterThan(feeBefore); -+ -+ // The predictor must agree with L1 exactly at mu != 0. This is the only check that catches -+ // the same factor+divisor double-scaling on the TS side of the mirror. -+ for (const manaUsage of Object.values(ManaUsageEstimate)) { -+ const predictor = await makeRefreshedPredictor(); -+ const predicted = predictor.getPredictedMinFees(manaUsage); -+ const predictionStartSlot = await getPredictionStartSlot(); -+ const l1FeeAtStart = await rollup.getManaMinFeeAt(getTimestamp(predictionStartSlot), true); -+ expect(predicted[0].feePerL2Gas).toBe(l1FeeAtStart); -+ } -+ }, 60_000); - }); - - describe('FeePredictor state caching', () => { -diff --git a/yarn-project/sequencer-client/src/global_variable_builder/fee_predictor.ts b/yarn-project/sequencer-client/src/global_variable_builder/fee_predictor.ts -index 9ab61df8c8690d3ff8591d0ef0d08be66db07071..bd80340dce6c23b80a4e44d35da3db237708e000 100644 ---- a/yarn-project/sequencer-client/src/global_variable_builder/fee_predictor.ts -+++ b/yarn-project/sequencer-client/src/global_variable_builder/fee_predictor.ts -@@ -21,6 +21,7 @@ type FeeOracleState = { - manaLimit: bigint; - provingCostPerManaEth: bigint; - epochDuration: bigint; -+ protocolFeeMarginBps: bigint; - /** Pre-resolved L1 fees for each slot in the prediction window. */ - l1FeesBySlot: L1FeeData[]; - }; -@@ -75,11 +76,12 @@ export class FeePredictor { - const opts = { blockNumber }; - - // Cached constants don't need pinning -- const [manaTarget, manaLimit, provingCostPerManaEth, epochDuration] = await Promise.all([ -+ const [manaTarget, manaLimit, provingCostPerManaEth, epochDuration, protocolFeeMarginBps] = await Promise.all([ - this.rollupContract.getManaTarget(), - this.rollupContract.getManaLimit(), - this.rollupContract.getProvingCostPerMana(), - this.rollupContract.getEpochDuration(), -+ this.rollupContract.getProtocolFeeMargin(), - ]); - - // First, compute the earliest possible nextSlot independently of the checkpoint, so we can -@@ -115,6 +117,7 @@ export class FeePredictor { - manaLimit, - provingCostPerManaEth, - epochDuration: BigInt(epochDuration), -+ protocolFeeMarginBps: BigInt(protocolFeeMarginBps), - l1FeesBySlot, - }; - } -@@ -170,6 +173,7 @@ export class FeePredictor { - provingCostPerManaEth: state.provingCostPerManaEth, - excessMana, - ethPerFeeAsset, -+ protocolFeeMarginBps: state.protocolFeeMarginBps, - }), - ); - } -diff --git a/yarn-project/sequencer-client/src/sequencer/checkpoint_proposal_job.test.ts b/yarn-project/sequencer-client/src/sequencer/checkpoint_proposal_job.test.ts -index 79b59d646d0bbc0996af22914c33430b02ec46c9..016425746093a3a6506e59634e29cd3504493f78 100644 ---- a/yarn-project/sequencer-client/src/sequencer/checkpoint_proposal_job.test.ts -+++ b/yarn-project/sequencer-client/src/sequencer/checkpoint_proposal_job.test.ts -@@ -192,7 +192,7 @@ describe('CheckpointProposalJob', () => { - // Default rollup contract reads used by pipelined fee-header derivation. Tests that exercise - // the failure modes override these via jest.spyOn. - jest.spyOn(publisher.rollupContract, 'getCheckpoint').mockResolvedValue({ -- feeHeader: { manaUsed: 0n, excessMana: 0n, ethPerFeeAsset: 1n, congestionCost: 0n, proverCost: 0n }, -+ feeHeader: { manaUsed: 0n, excessMana: 0n, ethPerFeeAsset: 1n, protocolFee: 0n, proverCost: 0n }, - } as any); - jest.spyOn(publisher.rollupContract, 'getManaTarget').mockResolvedValue(10_000n); - publisher.sendRequestsAt.mockResolvedValue({ -diff --git a/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts b/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts -index 65cb6e9158332e2f2f1079c66788e0f260c7bd16..be876254b1a8fbbd11f60cd38dd956ef4d52cd83 100644 ---- a/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts -+++ b/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts -@@ -260,7 +260,7 @@ describe('sequencer', () => { - rollupContract.isEscapeHatchOpen.mockResolvedValue(false); - // Default rollup reads used by pipelined fee-header derivation. - rollupContract.getCheckpoint.mockResolvedValue({ -- feeHeader: { manaUsed: 0n, excessMana: 0n, ethPerFeeAsset: 1n, congestionCost: 0n, proverCost: 0n }, -+ feeHeader: { manaUsed: 0n, excessMana: 0n, ethPerFeeAsset: 1n, protocolFee: 0n, proverCost: 0n }, - } as any); - rollupContract.getManaTarget.mockResolvedValue(10_000n); - -diff --git a/yarn-project/stdlib/src/checkpoint/simulation_overrides.test.ts b/yarn-project/stdlib/src/checkpoint/simulation_overrides.test.ts -index 26f80c803be718f9f998b8c5ec56459485ffcea8..24d78ae2a0445d75bc3fadad1d20524bdb594240 100644 ---- a/yarn-project/stdlib/src/checkpoint/simulation_overrides.test.ts -+++ b/yarn-project/stdlib/src/checkpoint/simulation_overrides.test.ts -@@ -37,7 +37,7 @@ describe('computePipelinedParentFeeHeader', () => { - manaUsed: 3000n, - excessMana: 1000n, - ethPerFeeAsset: 500n, -- congestionCost: 50n, -+ protocolFee: 50n, - proverCost: 10n, - }; - -@@ -138,7 +138,7 @@ describe('buildCheckpointSimulationOverridesPlan', () => { - manaUsed: 3000n, - excessMana: 1000n, - ethPerFeeAsset: 500n, -- congestionCost: 50n, -+ protocolFee: 50n, - proverCost: 10n, - }; - -diff --git a/yarn-project/stdlib/src/gas/README.md b/yarn-project/stdlib/src/gas/README.md -index d0980089673a78730755c400b66ff64cf99ce202..427fed74dbb37f87156db6bae35965f6b0053700 100644 ---- a/yarn-project/stdlib/src/gas/README.md -+++ b/yarn-project/stdlib/src/gas/README.md -@@ -56,28 +56,35 @@ Updates to `provingCostPerMana` are rate-limited on L1 (`FeeLib.updateProvingCos - at most one update every 30 days, each moving the value by at most ×1.5 (or ÷1.5), with a - floor of 2 wei per mana. - --### Congestion Cost -+### Protocol Fee - --An exponential surcharge when the network is congested (inspired by EIP-1559; the --implementation uses the `fakeExponential` Taylor series approximation from EIP-4844): -+The markup above operator cost: the governance-set protocol fee margin (basis points, applied -+through the congestion multiplier's factor) plus an exponential congestion surcharge when the -+network is congested (inspired by EIP-1559; the implementation uses the `fakeExponential` -+Taylor series approximation from EIP-4844): - - ``` --baseCost = sequencerCost + proverCost --congestionCost = floor(baseCost * congestionMultiplier / MINIMUM_CONGESTION_MULTIPLIER) - baseCost -+baseCost = sequencerCost + proverCost -+protocolFee = floor(baseCost * congestionMultiplier / MINIMUM_CONGESTION_MULTIPLIER) - baseCost - ``` - --When there is no congestion the multiplier equals `MINIMUM_CONGESTION_MULTIPLIER` (1e9) --and congestion cost is zero. -+At a zero margin and no congestion the multiplier equals `MINIMUM_CONGESTION_MULTIPLIER` (1e9) -+and the protocol fee is zero. - - ### Congestion Multiplier - - ``` - excessMana = max(0, prevExcessMana + prevManaUsed - manaTarget) - denominator = manaTarget * 854,700,854 / 1e8 ≈ 8.547 * manaTarget --congestionMultiplier = fakeExponential(MINIMUM_CONGESTION_MULTIPLIER, -+congestionMultiplier = fakeExponential((10,000 + protocolFeeMarginBps) * 1e5, - min(excessMana, 100 * denominator), denominator) - ``` - -+The factor `(10,000 + protocolFeeMarginBps) * 1e5` equals `(1 + mu) * 1e9`, so the -+uncongested baseline is `(1 + mu) * MINIMUM_CONGESTION_MULTIPLIER` — exactly 1e9 at a zero -+margin. Only the factor scales with the margin; the divisor in the protocol fee formula stays -+`MINIMUM_CONGESTION_MULTIPLIER` (scaling both would cancel the margin). -+ - Each additional `manaTarget` of excess mana multiplies the fee by `e^(1/8.547) ≈ 1.124`, - i.e. ~12.5%. The exponent is capped at 100 (multiplier ≤ ~2.7e43 × the minimum) to keep - the Taylor series from overflowing. -@@ -85,7 +92,7 @@ the Taylor series from overflowing. - ### Total - - ``` --minFeePerMana = sequencerCost + proverCost + congestionCost -+minFeePerMana = sequencerCost + proverCost + protocolFee - ``` - - Each component is converted from ETH to the fee asset individually (rounding up) before -diff --git a/yarn-project/stdlib/src/gas/fee_math.test.ts b/yarn-project/stdlib/src/gas/fee_math.test.ts -index e77dfb8a2ddd8fabb4027cbeb34d0fc6d8f254e5..a501fe652e640b2442ac9d6c70d81bb5285bd33e 100644 ---- a/yarn-project/stdlib/src/gas/fee_math.test.ts -+++ b/yarn-project/stdlib/src/gas/fee_math.test.ts -@@ -65,12 +65,12 @@ describe('computeExcessMana', () => { - - describe('computeCongestionMultiplier', () => { - it('returns MINIMUM_CONGESTION_MULTIPLIER when excess is zero', () => { -- expect(computeCongestionMultiplier(0n, 100_000_000n)).toBe(MINIMUM_CONGESTION_MULTIPLIER); -+ expect(computeCongestionMultiplier(0n, 100_000_000n, 0n)).toBe(MINIMUM_CONGESTION_MULTIPLIER); - }); - - it('increases with excess mana', () => { -- const low = computeCongestionMultiplier(100_000n, 100_000_000n); -- const high = computeCongestionMultiplier(200_000n, 100_000_000n); -+ const low = computeCongestionMultiplier(100_000n, 100_000_000n, 0n); -+ const high = computeCongestionMultiplier(200_000n, 100_000_000n, 0n); - expect(high).toBeGreaterThan(low); - expect(low).toBeGreaterThan(MINIMUM_CONGESTION_MULTIPLIER); - }); -@@ -78,11 +78,16 @@ describe('computeCongestionMultiplier', () => { - it('increases by ~12.5% per manaTarget of excess', () => { - // When excessMana = manaTarget, multiplier ≈ 1.125 * MINIMUM_CONGESTION_MULTIPLIER - const manaTarget = 100_000_000n; -- const multiplier = computeCongestionMultiplier(manaTarget, manaTarget); -+ const multiplier = computeCongestionMultiplier(manaTarget, manaTarget, 0n); - const ratio = Number(multiplier) / Number(MINIMUM_CONGESTION_MULTIPLIER); - expect(ratio).toBeGreaterThan(1.12); - expect(ratio).toBeLessThan(1.13); - }); -+ -+ it('scales the zero-excess baseline by exactly (10000 + bps) * 1e5', () => { -+ expect(computeCongestionMultiplier(0n, 100_000_000n, 5000n)).toBe(15_000n * 100_000n); -+ expect(computeCongestionMultiplier(0n, 100_000_000n, 65_535n)).toBe(75_535n * 100_000n); -+ }); - }); - - describe('computeManaMinFee', () => { -@@ -94,6 +99,7 @@ describe('computeManaMinFee', () => { - provingCostPerManaEth: 0n, - excessMana: 0n, - ethPerFeeAsset: 1_000_000_000_000n, // 1:1 ETH:FeeAsset -+ protocolFeeMarginBps: 0n, - }; - - it('returns zero when manaTarget is zero', () => { -@@ -123,12 +129,36 @@ describe('computeManaMinFee', () => { - expect(high).toBeGreaterThan(low); - }); - -- it('has zero congestion cost when excess mana is zero', () => { -- // With zero excess, congestionMultiplier = MINIMUM_CONGESTION_MULTIPLIER, -- // so congestionCost = total * 1 - total = 0 -+ it('has zero protocol fee when excess mana and margin are zero', () => { -+ // With zero excess and zero margin, congestionMultiplier = MINIMUM_CONGESTION_MULTIPLIER, -+ // so protocolFee = total * 1 - total = 0 - const fee = computeManaMinFee(baseParams); - // The fee should equal just sequencer + prover costs - const feeWithExcess = computeManaMinFee({ ...baseParams, excessMana: baseParams.manaTarget }); - expect(feeWithExcess).toBeGreaterThan(fee); - }); -+ -+ it('scales the fee by ~(1 + mu) at zero excess', () => { -+ const base = computeManaMinFee(baseParams); -+ const withMargin = computeManaMinFee({ ...baseParams, protocolFeeMarginBps: 5000n }); -+ // fee = cost + (floor(cost * 3 / 2) - cost) converted per component; allow 1 wei of Ceil slack. -+ const expected = (base * 15_000n) / 10_000n; -+ expect(withMargin).toBeGreaterThanOrEqual(expected - 1n); -+ expect(withMargin).toBeLessThanOrEqual(expected + 1n); -+ expect(withMargin).toBeGreaterThan(base); -+ }); -+ -+ it('applies the margin on top of congestion multiplicatively', () => { -+ const congested = computeManaMinFee({ ...baseParams, excessMana: baseParams.manaTarget * 3n }); -+ const congestedWithMargin = computeManaMinFee({ -+ ...baseParams, -+ excessMana: baseParams.manaTarget * 3n, -+ protocolFeeMarginBps: 5000n, -+ }); -+ expect(congestedWithMargin).toBeGreaterThan(congested); -+ // The margin scales the multiplier's factor, so the marked-up congested fee is ~1.5x. -+ const ratio = Number(congestedWithMargin) / Number(congested); -+ expect(ratio).toBeGreaterThan(1.49); -+ expect(ratio).toBeLessThan(1.51); -+ }); - }); -diff --git a/yarn-project/stdlib/src/gas/fee_math.ts b/yarn-project/stdlib/src/gas/fee_math.ts -index 35ff2842c97a88356c9664503ed16c2e1bcfb871..6d1ffa8fa73dbbee67e9e9334a9ba2563f6b6e84 100644 ---- a/yarn-project/stdlib/src/gas/fee_math.ts -+++ b/yarn-project/stdlib/src/gas/fee_math.ts -@@ -36,6 +36,7 @@ export type ManaMinFeeParams = { - provingCostPerManaEth: bigint; - excessMana: bigint; - ethPerFeeAsset: bigint; -+ protocolFeeMarginBps: bigint; - }; - - /** -@@ -60,11 +61,21 @@ export function computeExcessMana(prevExcessMana: bigint, prevManaUsed: bigint, - return sum > manaTarget ? sum - manaTarget : 0n; - } - --/** Computes the congestion multiplier from excess mana (1e9 = no congestion). */ --export function computeCongestionMultiplier(excessMana: bigint, manaTarget: bigint): bigint { -+/** -+ * Computes the congestion multiplier from excess mana. -+ * The protocol fee margin scales only the fakeExponential factor: (10_000 + bps) * 1e5, which is -+ * (1 + mu) * 1e9 and exactly 1e9 at mu = 0. The uncongested baseline is therefore (1 + mu) * 1e9. -+ * The MINIMUM_CONGESTION_MULTIPLIER divisor in computeManaMinFee MUST NOT be scaled -- scaling -+ * both sites cancels the margin. -+ */ -+export function computeCongestionMultiplier( -+ excessMana: bigint, -+ manaTarget: bigint, -+ protocolFeeMarginBps: bigint, -+): bigint { - const denominator = (manaTarget * MAGIC_CONGESTION_VALUE_MULTIPLIER) / MAGIC_CONGESTION_VALUE_DIVISOR; - const cappedNumerator = excessMana < denominator * 100n ? excessMana : denominator * 100n; -- return fakeExponential(MINIMUM_CONGESTION_MULTIPLIER, cappedNumerator, denominator); -+ return fakeExponential((10_000n + protocolFeeMarginBps) * 100_000n, cappedNumerator, denominator); - } - - /** Ceiling division for positive bigints. */ -@@ -81,11 +92,20 @@ function toFeeAsset(ethValue: bigint, ethPerFeeAsset: bigint): bigint { - } - - /** -- * Computes the full mana min fee (sequencer + prover + congestion) in fee asset terms. -+ * Computes the full mana min fee (sequencer + prover + protocol fee) in fee asset terms. - * Mirrors FeeLib.getManaMinFeeComponentsAt + summedMinFee. - */ - export function computeManaMinFee(params: ManaMinFeeParams): bigint { -- const { l1BaseFee, l1BlobFee, manaTarget, epochDuration, provingCostPerManaEth, excessMana, ethPerFeeAsset } = params; -+ const { -+ l1BaseFee, -+ l1BlobFee, -+ manaTarget, -+ epochDuration, -+ provingCostPerManaEth, -+ excessMana, -+ ethPerFeeAsset, -+ protocolFeeMarginBps, -+ } = params; - - if (manaTarget === 0n) { - return 0n; -@@ -102,17 +122,18 @@ export function computeManaMinFee(params: ManaMinFeeParams): bigint { - // Total base cost in ETH (congestion is computed on this) - const totalEth = sequencerCostEth + proverCostEth; - -- // Congestion multiplier and cost (in ETH) -- const congestionMul = computeCongestionMultiplier(excessMana, manaTarget); -- const congestionCostEth = (totalEth * congestionMul) / MINIMUM_CONGESTION_MULTIPLIER - totalEth; -+ // Congestion multiplier (scaled by the protocol fee margin) and protocol fee (in ETH). -+ // The divisor here stays MINIMUM_CONGESTION_MULTIPLIER; only the multiplier's factor scales. -+ const congestionMul = computeCongestionMultiplier(excessMana, manaTarget, protocolFeeMarginBps); -+ const protocolFeeEth = (totalEth * congestionMul) / MINIMUM_CONGESTION_MULTIPLIER - totalEth; - - // Convert all components to fee asset - const clampedEthPerFeeAsset = ethPerFeeAsset < MIN_ETH_PER_FEE_ASSET ? MIN_ETH_PER_FEE_ASSET : ethPerFeeAsset; - const sequencerCost = toFeeAsset(sequencerCostEth, clampedEthPerFeeAsset); - const proverCost = toFeeAsset(proverCostEth, clampedEthPerFeeAsset); -- const congestionCost = toFeeAsset(congestionCostEth, clampedEthPerFeeAsset); -+ const protocolFee = toFeeAsset(protocolFeeEth, clampedEthPerFeeAsset); - -- const total = sequencerCost + proverCost + congestionCost; -+ const total = sequencerCost + proverCost + protocolFee; - - // Cap at uint128 max (matching FeeLib.summedMinFee) - const UINT128_MAX = (1n << 128n) - 1n; diff --git a/labs-patches/0004-refactor-ethereum-read-vkTreeRoot-and-protocolContra.patch b/labs-patches/0004-refactor-ethereum-read-vkTreeRoot-and-protocolContra.patch deleted file mode 100644 index 395ed117e172..000000000000 --- a/labs-patches/0004-refactor-ethereum-read-vkTreeRoot-and-protocolContra.patch +++ /dev/null @@ -1,49 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Santiago Palladino -Date: Tue, 8 Sep 2026 17:46:57 -0300 -Subject: [PATCH] refactor(ethereum): read vkTreeRoot and protocolContractsHash - via their getters - - -diff --git a/yarn-project/ethereum/src/contracts/rollup.test.ts b/yarn-project/ethereum/src/contracts/rollup.test.ts -index 2262d7755c2938fd1cf066743c63196e77fab7a9..54744f5474fa00832e9eb0abc510bea8a90bbaec 100644 ---- a/yarn-project/ethereum/src/contracts/rollup.test.ts -+++ b/yarn-project/ethereum/src/contracts/rollup.test.ts -@@ -362,12 +362,12 @@ describe('Rollup', () => { - }); - - describe('getVkTreeRoot and getProtocolContractsHash', () => { -- it('reads vkTreeRoot from storage', async () => { -+ it('reads vkTreeRoot', async () => { - const result = await rollup.getVkTreeRoot(); - expect(result).toEqual(vkTreeRoot); - }); - -- it('reads protocolContractsHash from storage', async () => { -+ it('reads protocolContractsHash', async () => { - const result = await rollup.getProtocolContractsHash(); - expect(result).toEqual(protocolContractsHash); - }); -diff --git a/yarn-project/ethereum/src/contracts/rollup.ts b/yarn-project/ethereum/src/contracts/rollup.ts -index cd27a307a7c8c8182b9f0e19647914424a20f5e6..6ac1639751d082d633487d7dcf04fa58714f82d1 100644 ---- a/yarn-project/ethereum/src/contracts/rollup.ts -+++ b/yarn-project/ethereum/src/contracts/rollup.ts -@@ -459,16 +459,12 @@ export class RollupContract { - - @memoize - async getVkTreeRoot(): Promise { -- const slot = BigInt(RollupContract.stfStorageSlot) + 3n; -- const value = await this.client.getStorageAt({ address: this.address, slot: `0x${slot.toString(16)}` }); -- return Fr.fromString(value ?? '0x0'); -+ return Fr.fromString(await this.rollup.read.getVkTreeRoot()); - } - - @memoize - async getProtocolContractsHash(): Promise { -- const slot = BigInt(RollupContract.stfStorageSlot) + 4n; -- const value = await this.client.getStorageAt({ address: this.address, slot: `0x${slot.toString(16)}` }); -- return Fr.fromString(value ?? '0x0'); -+ return Fr.fromString(await this.rollup.read.getProtocolContractsHash()); - } - - /** diff --git a/labs-patches/0005-feat-ethereum-carry-the-consumed-inbox-bucket-in-che.patch b/labs-patches/0005-feat-ethereum-carry-the-consumed-inbox-bucket-in-che.patch deleted file mode 100644 index 1d1ce792aa6b..000000000000 --- a/labs-patches/0005-feat-ethereum-carry-the-consumed-inbox-bucket-in-che.patch +++ /dev/null @@ -1,129 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Santiago Palladino -Date: Thu, 10 Sep 2026 12:23:21 -0300 -Subject: [PATCH] feat(ethereum): carry the consumed inbox bucket in checkpoint - log overrides - -The L1 `TempCheckpointLog` gains an `inboxConsumedBucket` member, packed into -the same storage word as the slot number and the cumulative inbox message -total. The override builders have to write it, otherwise supplying either of -the other two zeroes it out and a simulation reads a checkpoint that consumed -bucket 0. - -The packed-word diff now fires when any of the three fields is supplied and -writes all three, with the new field width-checked at 64 bits like the -message total. - -diff --git a/yarn-project/ethereum/src/contracts/chain_state_override.ts b/yarn-project/ethereum/src/contracts/chain_state_override.ts -index b080b8056fafbe4cf45d47c482488beb56eb7631..eb93049c680f49e5896c590a8e9195634a33f91e 100644 ---- a/yarn-project/ethereum/src/contracts/chain_state_override.ts -+++ b/yarn-project/ethereum/src/contracts/chain_state_override.ts -@@ -21,6 +21,7 @@ export type PendingCheckpointOverrideState = { - payloadDigest?: Buffer32; - slotNumber?: SlotNumber; - inboxMsgTotal?: bigint; -+ inboxConsumedBucket?: bigint; - }; - - export type ChainTipsOverride = { -@@ -95,8 +96,8 @@ export class SimulationOverridesBuilder { - * Overrides one or more `tempCheckpointLogs` cell fields for the configured pending checkpoint. - * Any subset can be provided. The translator (`makeTempCheckpointLogOverride`) emits a stateDiff - * entry per storage word touched, so fields in untouched words stay at their on-chain values; -- * `slotNumber` and `inboxMsgTotal` share a word, so setting either zeroes the other unless it is -- * supplied too. -+ * `slotNumber`, `inboxMsgTotal` and `inboxConsumedBucket` share a word, so setting any of them -+ * zeroes the others unless they are supplied too. - * - * `slotNumber` is required for `STFLib.canPruneAtTime`: when the simulation overrides `pending` - * to a checkpoint that has no on-chain `tempCheckpointLogs` entry yet, the missing slotNumber falls -@@ -109,6 +110,7 @@ export class SimulationOverridesBuilder { - payloadDigest?: Buffer32; - slotNumber?: SlotNumber; - inboxMsgTotal?: bigint; -+ inboxConsumedBucket?: bigint; - }): this { - this.assertPendingCheckpointNumber(); - this.pendingCheckpointState = { ...(this.pendingCheckpointState ?? {}), ...fields }; -@@ -177,6 +179,7 @@ export async function buildSimulationOverridesStateOverride( - payloadDigest: plan.pendingCheckpointState.payloadDigest, - slotNumber: plan.pendingCheckpointState.slotNumber, - inboxMsgTotal: plan.pendingCheckpointState.inboxMsgTotal, -+ inboxConsumedBucket: plan.pendingCheckpointState.inboxConsumedBucket, - feeHeader: plan.pendingCheckpointState.feeHeader, - }), - ), -diff --git a/yarn-project/ethereum/src/contracts/rollup.test.ts b/yarn-project/ethereum/src/contracts/rollup.test.ts -index 54744f5474fa00832e9eb0abc510bea8a90bbaec..7c770fabeac2c4a296c348524d106fc0dc047997 100644 ---- a/yarn-project/ethereum/src/contracts/rollup.test.ts -+++ b/yarn-project/ethereum/src/contracts/rollup.test.ts -@@ -487,16 +487,17 @@ describe('Rollup', () => { - ); - }); - -- it('packs the inbox consumption count into the slot-number word', async () => { -+ it('packs the inbox consumption counts into the slot-number word', async () => { - const checkpointNumber = CheckpointNumber(13); - const override = await rollup.makeTempCheckpointLogOverride(checkpointNumber, { - slotNumber: SlotNumber(7), - inboxMsgTotal: 300n, -+ inboxConsumedBucket: 5n, - }); - const { map, slotFor } = getDiffMap(checkpointNumber, override); - expect(override[0].stateDiff).toHaveLength(1); - expect(map.get(await slotFor(TempCheckpointLogField.SlotNumber))).toBe( -- `0x${(7n | (300n << 32n)).toString(16).padStart(64, '0')}`.toLowerCase(), -+ `0x${(7n | (300n << 32n) | (5n << 96n)).toString(16).padStart(64, '0')}`.toLowerCase(), - ); - }); - -diff --git a/yarn-project/ethereum/src/contracts/rollup.ts b/yarn-project/ethereum/src/contracts/rollup.ts -index 6ac1639751d082d633487d7dcf04fa58714f82d1..b72cc405e81aee557a54df55a165666884e939b9 100644 ---- a/yarn-project/ethereum/src/contracts/rollup.ts -+++ b/yarn-project/ethereum/src/contracts/rollup.ts -@@ -160,8 +160,8 @@ export enum TempCheckpointLogField { - * arbitrary `bytes32` value rather than a BN254 scalar. `slotNumber` carries the uint32 portion - * of the on-chain `CompressedSlot`. - * -- * `slotNumber` and `inboxMsgTotal` share a single storage word, so supplying either rewrites both; -- * the one left out lands as zero. -+ * `slotNumber`, `inboxMsgTotal` and `inboxConsumedBucket` share a single storage word, so supplying -+ * any one of them rewrites all three; the ones left out land as zero. - */ - export type TempCheckpointLogOverrideFields = { - headerHash?: Fr; -@@ -170,6 +170,8 @@ export type TempCheckpointLogOverrideFields = { - slotNumber?: SlotNumber; - /** Cumulative Inbox message count consumed as of this checkpoint. */ - inboxMsgTotal?: bigint; -+ /** Inbox bucket sequence number this checkpoint's rolling hash corresponds to. */ -+ inboxConsumedBucket?: bigint; - feeHeader?: FeeHeader; - }; - -@@ -998,16 +1000,21 @@ export class RollupContract { - value: fields.payloadDigest.toString() as `0x${string}`, - }); - } -- if (fields.slotNumber !== undefined || fields.inboxMsgTotal !== undefined) { -- // The L1 struct packs the slot number and the inbox consumption count into one word, so this -- // diff always writes both. Widths are enforced here because the L1 writers cast through -+ if ( -+ fields.slotNumber !== undefined || -+ fields.inboxMsgTotal !== undefined || -+ fields.inboxConsumedBucket !== undefined -+ ) { -+ // The L1 struct packs the slot number and the two inbox consumption counts into one word, so this -+ // diff always writes all three. Widths are enforced here because the L1 writers cast through - // SafeCast and revert on overflow; a malformed override must surface rather than silently truncate - // into a neighbouring field. - const slotNumber = requireUintFits(BigInt(fields.slotNumber ?? 0), 32, 'slotNumber'); - const inboxMsgTotal = requireUintFits(fields.inboxMsgTotal ?? 0n, 64, 'inboxMsgTotal'); -+ const inboxConsumedBucket = requireUintFits(fields.inboxConsumedBucket ?? 0n, 64, 'inboxConsumedBucket'); - stateDiff.push({ - slot: slotAt(TempCheckpointLogField.SlotNumber), -- value: word(slotNumber | (inboxMsgTotal << 32n)), -+ value: word(slotNumber | (inboxMsgTotal << 32n) | (inboxConsumedBucket << 96n)), - }); - } - if (fields.feeHeader) { diff --git a/labs-patches/0006-feat-ethereum-wrap-inbox-endpoint-resolution-and-che.patch b/labs-patches/0006-feat-ethereum-wrap-inbox-endpoint-resolution-and-che.patch deleted file mode 100644 index f90bed882465..000000000000 --- a/labs-patches/0006-feat-ethereum-wrap-inbox-endpoint-resolution-and-che.patch +++ /dev/null @@ -1,640 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Santiago Palladino -Date: Thu, 10 Sep 2026 12:50:44 -0300 -Subject: [PATCH] feat(ethereum): wrap inbox endpoint resolution and checkpoint - preflight - -`InboxContract.getBucketAtOrBeforeTotal` resolves a locally known cumulative -message count to the newest live bucket boundary at or below it, returning -`undefined` on the not-found revert so a caller whose target has already been -evicted from the ring sees an absent endpoint rather than an error. - -`RollupContract.validateCheckpointHeaderAndInbox` simulates the Rollup's -integrated header and Inbox preflight over `eth_simulateV1` with a block-time -override and the caller's state overrides, and returns the bucket sequence to -submit to `propose` as `bucketHint`. - -`getRevertedErrorName` decodes the custom error a viem contract call reverted -with, which is how the Inbox wrapper tells the not-found revert apart from -every other failure. - -diff --git a/yarn-project/ethereum/src/contracts/inbox.test.ts b/yarn-project/ethereum/src/contracts/inbox.test.ts -new file mode 100644 -index 0000000000000000000000000000000000000000..9736cb997e90d039777e11d69d56185939f11134 ---- /dev/null -+++ b/yarn-project/ethereum/src/contracts/inbox.test.ts -@@ -0,0 +1,179 @@ -+import { InboxAbi } from '@aztec-foundation/l1-artifacts/InboxAbi'; -+ -+import { Fr } from '@aztec-labs/foundation/curves/bn254'; -+import { createLogger } from '@aztec-labs/foundation/log'; -+import { DateProvider } from '@aztec-labs/foundation/timer'; -+import { mock } from 'jest-mock-extended'; -+import { -+ ContractFunctionExecutionError, -+ ContractFunctionRevertedError, -+ type Hex, -+ encodeAbiParameters, -+ encodeErrorResult, -+ getContract, -+ keccak256, -+} from 'viem'; -+import { foundry } from 'viem/chains'; -+ -+import { L1RpcError } from '../client.js'; -+import { DefaultL1ContractsConfig } from '../config.js'; -+import { type DeployAztecL1ContractsReturnType, deployAztecL1Contracts } from '../deploy_aztec_l1_contracts.js'; -+import { EthCheatCodes } from '../test/eth_cheat_codes.js'; -+import type { Anvil } from '../test/start_anvil.js'; -+import { startAnvil } from '../test/start_anvil.js'; -+import type { ViemClient } from '../types.js'; -+import { InboxContract } from './inbox.js'; -+import { RollupContract } from './rollup.js'; -+ -+describe('InboxContract', () => { -+ let anvil: Anvil; -+ let rpcUrl: string; -+ let cheatCodes: EthCheatCodes; -+ let deployed: DeployAztecL1ContractsReturnType; -+ let inbox: InboxContract; -+ let version: bigint; -+ -+ beforeAll(async () => { -+ const privateKeyRaw = '0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba'; -+ ({ anvil, rpcUrl } = await startAnvil()); -+ cheatCodes = new EthCheatCodes([rpcUrl], new DateProvider()); -+ -+ deployed = await deployAztecL1Contracts(rpcUrl, privateKeyRaw, foundry.id, { -+ ...DefaultL1ContractsConfig, -+ vkTreeRoot: Fr.random(), -+ protocolContractsHash: Fr.random(), -+ genesisArchiveRoot: Fr.random(), -+ realVerifier: false, -+ }); -+ inbox = InboxContract.getFromL1ContractsValues(deployed); -+ version = await new RollupContract(deployed.l1Client, deployed.l1ContractAddresses.rollupAddress).getVersion(); -+ }); -+ -+ afterAll(async () => { -+ await anvil?.stop().catch(err => createLogger('cleanup').error(err)); -+ }); -+ -+ /** -+ * Sends `count` messages in one fresh L1 block, 12 seconds after the previous one so they open a new bucket, and -+ * returns that bucket's sequence. -+ */ -+ async function sendMessages(count: number): Promise { -+ const contract = getContract({ -+ address: deployed.l1ContractAddresses.inboxAddress.toString(), -+ abi: InboxAbi, -+ client: deployed.l1Client, -+ }); -+ const { timestamp } = await deployed.l1Client.getBlock(); -+ await cheatCodes.warp(timestamp + 12n, { silent: true }); -+ await cheatCodes.setAutomine(false); -+ const hashes: Hex[] = []; -+ for (let i = 0; i < count; i++) { -+ hashes.push( -+ await contract.write.sendL2Message( -+ [{ actor: Fr.random().toString(), version }, Fr.random().toString(), Fr.random().toString()], -+ { gas: 1_000_000n }, -+ ), -+ ); -+ } -+ await cheatCodes.mine(1); -+ await cheatCodes.setAutomine(true); -+ for (const hash of hashes) { -+ const receipt = await deployed.l1Client.waitForTransactionReceipt({ hash }); -+ expect(receipt.status).toBe('success'); -+ } -+ return inbox.getCurrentBucketSeq(); -+ } -+ -+ describe('getBucketAtOrBeforeTotal', () => { -+ it('resolves to the genesis bucket on an empty inbox', async () => { -+ const found = await inbox.getBucketAtOrBeforeTotal(0n); -+ expect(found?.seq).toBe(0n); -+ expect(found?.bucket).toEqual(await inbox.getBucket(0n)); -+ -+ const loose = await inbox.getBucketAtOrBeforeTotal(100n); -+ expect(loose?.seq).toBe(0n); -+ }); -+ -+ it('resolves exact and interior bounds to the newest bucket at or below them', async () => { -+ const first = await sendMessages(3); -+ const second = await sendMessages(2); -+ expect([first, second]).toEqual([1n, 2n]); -+ -+ const exact = await inbox.getBucketAtOrBeforeTotal(3n); -+ expect(exact?.seq).toBe(1n); -+ expect(exact?.bucket).toEqual(await inbox.getBucket(1n)); -+ expect(exact?.bucket.totalMsgCount).toBe(3n); -+ -+ const interior = await inbox.getBucketAtOrBeforeTotal(4n); -+ expect(interior?.seq).toBe(1n); -+ -+ const newest = await inbox.getBucketAtOrBeforeTotal(5n); -+ expect(newest?.seq).toBe(2n); -+ expect(newest?.bucket.totalMsgCount).toBe(5n); -+ -+ const belowFirst = await inbox.getBucketAtOrBeforeTotal(2n); -+ expect(belowFirst?.seq).toBe(0n); -+ }); -+ -+ // The ring has to wrap for a bound to fall below every live bucket, which no test can reach by sending messages. -+ // Rewrite the Inbox's storage instead: a wrapped sequence counter with the newest probed entries and the oldest -+ // live entry all ending past the bound, exactly the state the contract reverts on. -+ it('resolves to undefined when every live bucket ends past the bound', async () => { -+ const inboxAddress = deployed.l1ContractAddresses.inboxAddress; -+ const current = await inbox.getContract().read.BUCKET_RING_SIZE(); -+ const oldest = 1n; -+ // Slot 1 packs (currentBucketSeq, provenConsumedBucketSeq); a bucket's packed word sits one slot after its -+ // `buckets` mapping entry and holds (totalMsgCount, timestamp, msgCount). -+ const seqSlot = 1n; -+ const totalsSlotOf = (seq: bigint) => -+ BigInt(keccak256(encodeAbiParameters([{ type: 'uint256' }, { type: 'uint256' }], [seq % current, 0n]))) + 1n; -+ const touched = [seqSlot, ...[current, current - 1n, current - 2n, current - 3n, oldest].map(totalsSlotOf)]; -+ const saved = await Promise.all(touched.map(slot => cheatCodes.load(inboxAddress, slot))); -+ -+ try { -+ await cheatCodes.store(inboxAddress, seqSlot, current); -+ for (const slot of touched.slice(1)) { -+ await cheatCodes.store(inboxAddress, slot, 10n); -+ } -+ -+ await expect(inbox.getBucketAtOrBeforeTotal(9n)).resolves.toBeUndefined(); -+ const hit = await inbox.getBucketAtOrBeforeTotal(10n); -+ expect(hit?.seq).toBe(current); -+ expect(hit?.bucket.totalMsgCount).toBe(10n); -+ } finally { -+ for (const [i, slot] of touched.entries()) { -+ await cheatCodes.store(inboxAddress, slot, saved[i]); -+ } -+ } -+ expect((await inbox.getBucketAtOrBeforeTotal(0n))?.seq).toBe(0n); -+ }); -+ -+ it('propagates reverts other than not-found and transport failures', async () => { -+ const address = deployed.l1ContractAddresses.inboxAddress.toString(); -+ const revertWith = (errorName: 'Inbox__NoBucketAtOrBeforeTotal' | 'Inbox__BucketOutOfWindow') => -+ new ContractFunctionExecutionError( -+ new ContractFunctionRevertedError({ -+ abi: InboxAbi, -+ data: encodeErrorResult({ abi: InboxAbi, errorName, args: [3n, 7n] }), -+ functionName: 'getBucketAtOrBeforeTotal', -+ }), -+ { abi: InboxAbi, functionName: 'getBucketAtOrBeforeTotal', args: [3n], contractAddress: address }, -+ ); -+ const failing = (err: Error) => { -+ const client = mock(); -+ client.readContract.mockRejectedValue(err); -+ return new InboxContract(client, address); -+ }; -+ -+ await expect(failing(revertWith('Inbox__NoBucketAtOrBeforeTotal')).getBucketAtOrBeforeTotal(3n)).resolves.toBe( -+ undefined, -+ ); -+ await expect(failing(revertWith('Inbox__BucketOutOfWindow')).getBucketAtOrBeforeTotal(3n)).rejects.toThrow( -+ /Inbox__BucketOutOfWindow/, -+ ); -+ await expect(failing(new L1RpcError('L1 RPC request failed')).getBucketAtOrBeforeTotal(3n)).rejects.toThrow( -+ /L1 RPC request failed/, -+ ); -+ }); -+ }); -+}); -diff --git a/yarn-project/ethereum/src/contracts/inbox.ts b/yarn-project/ethereum/src/contracts/inbox.ts -index 0cd7471ceca7db19152f8ddd3c07dac57ec20f79..83515b23a951327c99c0b6838c231b00ad05591f 100644 ---- a/yarn-project/ethereum/src/contracts/inbox.ts -+++ b/yarn-project/ethereum/src/contracts/inbox.ts -@@ -11,8 +11,9 @@ import { getPublicClient } from '../client.js'; - import type { DeployAztecL1ContractsReturnType } from '../deploy_aztec_l1_contracts.js'; - import type { L1ReaderConfig } from '../l1_reader.js'; - import type { ViemClient } from '../types.js'; -+import { formatViemError } from '../utils.js'; - import type { L1EventLog } from './log.js'; --import { checkBlockTag } from './utils.js'; -+import { checkBlockTag, getRevertedErrorName } from './utils.js'; - - /** The full L1-to-L2 message emitted by the Inbox, decoded from the event. Hashing it yields `leaf`. */ - export type MessageSentMessage = { -@@ -105,12 +106,29 @@ export class InboxContract { - ): Promise { - await checkBlockTag(opts.blockNumber, this.client); - const bucket = await this.inbox.read.getBucket([seq], opts); -- return { -- rollingHash: Fr.fromString(bucket.rollingHash), -- totalMsgCount: bucket.totalMsgCount, -- timestamp: bucket.timestamp, -- msgCount: bucket.msgCount, -- }; -+ return toInboxContractBucket(bucket); -+ } -+ -+ /** -+ * Returns the live Inbox bucket with the greatest cumulative message total at or below `upperBound`, together with -+ * its sequence number. Resolves to `undefined` when even the oldest retained bucket ends past the bound, i.e. the -+ * ring has already evicted every bucket that could have matched. Totals strictly increase per bucket, so the result -+ * is the newest bucket boundary reachable from a local message count. -+ */ -+ public async getBucketAtOrBeforeTotal( -+ upperBound: bigint, -+ opts: { blockTag?: BlockTag; blockNumber?: bigint } = {}, -+ ): Promise<{ seq: bigint; bucket: InboxContractBucket } | undefined> { -+ await checkBlockTag(opts.blockNumber, this.client); -+ try { -+ const [seq, bucket] = await this.inbox.read.getBucketAtOrBeforeTotal([upperBound], opts); -+ return { seq, bucket: toInboxContractBucket(bucket) }; -+ } catch (err) { -+ if (getRevertedErrorName(err) === 'Inbox__NoBucketAtOrBeforeTotal') { -+ return undefined; -+ } -+ throw formatViemError(err); -+ } - } - - /** Fetches MessageSent events within the given block range. */ -@@ -210,6 +228,20 @@ export type InboxContractState = { - currentBucketSeq: bigint; - }; - -+function toInboxContractBucket(bucket: { -+ rollingHash: Hex; -+ totalMsgCount: bigint; -+ timestamp: bigint; -+ msgCount: number; -+}): InboxContractBucket { -+ return { -+ rollingHash: Fr.fromString(bucket.rollingHash), -+ totalMsgCount: bucket.totalMsgCount, -+ timestamp: bucket.timestamp, -+ msgCount: bucket.msgCount, -+ }; -+} -+ - /** A snapshot of an on-chain Inbox rolling-hash bucket. */ - export type InboxContractBucket = { - /** Consensus rolling hash (truncated sha256 chain) after the last message absorbed into this bucket. */ -diff --git a/yarn-project/ethereum/src/contracts/rollup.test.ts b/yarn-project/ethereum/src/contracts/rollup.test.ts -index 7c770fabeac2c4a296c348524d106fc0dc047997..cf222eccafe55ff9b3615f3ad1c28d8ab4631e20 100644 ---- a/yarn-project/ethereum/src/contracts/rollup.test.ts -+++ b/yarn-project/ethereum/src/contracts/rollup.test.ts -@@ -1,3 +1,4 @@ -+import { InboxAbi } from '@aztec-foundation/l1-artifacts/InboxAbi'; - import { RollupAbi } from '@aztec-foundation/l1-artifacts/RollupAbi'; - - import { L1RpcError, getPublicClient } from '@aztec-labs/ethereum/client'; -@@ -8,16 +9,19 @@ import { EthAddress } from '@aztec-labs/foundation/eth-address'; - import { createLogger } from '@aztec-labs/foundation/log'; - import { DateProvider } from '@aztec-labs/foundation/timer'; - import { jest } from '@jest/globals'; --import { type Abi, RpcRequestError, encodeErrorResult } from 'viem'; -+import { type Abi, RpcRequestError, encodeErrorResult, getContract } from 'viem'; - import { foundry } from 'viem/chains'; - - import { DefaultL1ContractsConfig } from '../config.js'; --import { deployAztecL1Contracts } from '../deploy_aztec_l1_contracts.js'; -+import { type DeployAztecL1ContractsReturnType, deployAztecL1Contracts } from '../deploy_aztec_l1_contracts.js'; -+import { ReadOnlyL1TxUtils } from '../l1_tx_utils/index.js'; - import { EthCheatCodes } from '../test/eth_cheat_codes.js'; - import type { Anvil } from '../test/start_anvil.js'; - import { startAnvil } from '../test/start_anvil.js'; - import type { ViemClient } from '../types.js'; --import { type FeeHeader, RollupContract, TempCheckpointLogField } from './rollup.js'; -+import { buildSimulationOverridesStateOverride } from './chain_state_override.js'; -+import { InboxContract } from './inbox.js'; -+import { type CheckpointPreflightArgs, type FeeHeader, RollupContract, TempCheckpointLogField } from './rollup.js'; - - describe('compressFeeHeader', () => { - /** Creates a zero fee header with the given overrides. */ -@@ -237,25 +241,28 @@ describe('Rollup', () => { - - let vkTreeRoot: Fr; - let protocolContractsHash: Fr; -+ let genesisArchiveRoot: Fr; - let rollupAddress: `0x${string}`; - let rollup: RollupContract; -+ let deployed: DeployAztecL1ContractsReturnType; - - beforeAll(async () => { - // this is the 6th address that gets funded by the junk mnemonic - const privateKeyRaw = '0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba'; - vkTreeRoot = Fr.random(); - protocolContractsHash = Fr.random(); -+ genesisArchiveRoot = Fr.random(); - - ({ anvil, rpcUrl } = await startAnvil()); - - publicClient = getPublicClient({ l1RpcUrls: [rpcUrl], l1ChainId: 31337 }); - cheatCodes = new EthCheatCodes([rpcUrl], new DateProvider()); - -- const deployed = await deployAztecL1Contracts(rpcUrl, privateKeyRaw, foundry.id, { -+ deployed = await deployAztecL1Contracts(rpcUrl, privateKeyRaw, foundry.id, { - ...DefaultL1ContractsConfig, - vkTreeRoot, - protocolContractsHash, -- genesisArchiveRoot: Fr.random(), -+ genesisArchiveRoot, - realVerifier: false, - }); - -@@ -361,6 +368,185 @@ describe('Rollup', () => { - }); - }); - -+ describe('validateCheckpointHeaderAndInbox', () => { -+ let l1TxUtils: ReadOnlyL1TxUtils; -+ let inbox: InboxContract; -+ let slotDuration: bigint; -+ let l1GenesisTime: bigint; -+ let liveTips: bigint; -+ -+ beforeAll(async () => { -+ l1TxUtils = new ReadOnlyL1TxUtils(publicClient, undefined, new DateProvider()); -+ inbox = InboxContract.getFromL1ContractsValues(deployed); -+ slotDuration = BigInt(await rollup.getSlotDuration()); -+ l1GenesisTime = await rollup.getL1GenesisTime(); -+ // Earlier tests rewrote the chain tips; run against the fresh chain (effective parent 0) and put them back after. -+ liveTips = await cheatCodes.load(EthAddress.fromString(rollupAddress), BigInt(RollupContract.stfStorageSlot)); -+ await cheatCodes.store(EthAddress.fromString(rollupAddress), BigInt(RollupContract.stfStorageSlot), 0n); -+ }); -+ -+ afterAll(async () => { -+ await cheatCodes.store(EthAddress.fromString(rollupAddress), BigInt(RollupContract.stfStorageSlot), liveTips); -+ }); -+ -+ /** Builds an otherwise valid header for the second L2 slot after the current L1 time, and the time to simulate at. */ -+ async function buildHeader(inboxRollingHash: Fr): Promise<{ args: CheckpointPreflightArgs; time: bigint }> { -+ const now = (await publicClient.getBlock()).timestamp; -+ const slot = SlotNumber(Number((now - l1GenesisTime) / slotDuration) + 2); -+ const time = await rollup.getTimestampForSlot(slot); -+ const minFee = await rollup.getManaMinFeeAt(time, true); -+ const zero = Fr.ZERO.toString(); -+ const args: CheckpointPreflightArgs = { -+ header: { -+ lastArchiveRoot: genesisArchiveRoot.toString(), -+ blockHeadersHash: zero, -+ blobsHash: zero, -+ inboxRollingHash: inboxRollingHash.toString(), -+ outHash: zero, -+ slotNumber: BigInt(slot), -+ timestamp: time, -+ coinbase: EthAddress.random().toString(), -+ feeRecipient: zero, -+ gasFees: { feePerDaGas: 0n, feePerL2Gas: minFee }, -+ totalManaUsed: 0n, -+ accumulatedFees: 0n, -+ }, -+ attestations: { signatureIndices: '0x', signaturesOrAddresses: '0x' }, -+ signers: [], -+ attestationsAndSignersSignature: { r: zero, s: zero, v: 0 }, -+ digest: zero, -+ blobsHash: zero, -+ flags: { ignoreDA: true }, -+ expectedTotal: 0n, -+ expectedParentCheckpointNumber: 0n, -+ }; -+ return { args, time }; -+ } -+ -+ it('returns the genesis bucket for a checkpoint consuming nothing on a fresh chain', async () => { -+ const { args, time } = await buildHeader(Fr.ZERO); -+ await expect(rollup.validateCheckpointHeaderAndInbox(l1TxUtils, args, { time })).resolves.toBe(0n); -+ }); -+ -+ it('rejects a parent claim that differs from the parent propose would use', async () => { -+ const { args, time } = await buildHeader(Fr.ZERO); -+ await expect( -+ rollup.validateCheckpointHeaderAndInbox(l1TxUtils, { ...args, expectedParentCheckpointNumber: 1n }, { time }), -+ ).rejects.toThrow(/Rollup__UnexpectedParentCheckpoint/); -+ }); -+ -+ it('rejects a consumed total that is not a bucket boundary', async () => { -+ const { args, time } = await buildHeader(Fr.ZERO); -+ await expect( -+ rollup.validateCheckpointHeaderAndInbox(l1TxUtils, { ...args, expectedTotal: 1n }, { time }), -+ ).rejects.toThrow(/Rollup__InboxTotalNotAtBucketBoundary/); -+ }); -+ -+ it('runs the shared header checks against the derived parent', async () => { -+ const { args, time } = await buildHeader(Fr.ZERO); -+ const header = { ...args.header, lastArchiveRoot: Fr.random().toString() }; -+ await expect(rollup.validateCheckpointHeaderAndInbox(l1TxUtils, { ...args, header }, { time })).rejects.toThrow( -+ /Rollup__InvalidArchive/, -+ ); -+ }); -+ -+ /** Sends one message in a fresh L1 block 12 seconds after the previous one, so it opens a new bucket. */ -+ async function sendMessageInNewBucket(): Promise { -+ const inboxContract = getContract({ -+ address: deployed.l1ContractAddresses.inboxAddress.toString(), -+ abi: InboxAbi, -+ client: deployed.l1Client, -+ }); -+ const version = await rollup.getVersion(); -+ const { timestamp } = await publicClient.getBlock(); -+ await cheatCodes.warp(timestamp + 12n, { silent: true }); -+ const hash = await inboxContract.write.sendL2Message( -+ [{ actor: Fr.random().toString(), version }, Fr.random().toString(), Fr.random().toString()], -+ { gas: 1_000_000n }, -+ ); -+ await deployed.l1Client.waitForTransactionReceipt({ hash }); -+ } -+ -+ it('resolves a consumed bucket to its sequence and rejects a stale hash for it', async () => { -+ await sendMessageInNewBucket(); -+ const bucket = await inbox.getBucket(1n); -+ expect(bucket.totalMsgCount).toBe(1n); -+ -+ const { args, time } = await buildHeader(bucket.rollingHash); -+ await expect( -+ rollup.validateCheckpointHeaderAndInbox(l1TxUtils, { ...args, expectedTotal: 1n }, { time }), -+ ).resolves.toBe(1n); -+ -+ const stale = { ...args.header, inboxRollingHash: Fr.random().toString() }; -+ await expect( -+ rollup.validateCheckpointHeaderAndInbox(l1TxUtils, { ...args, header: stale, expectedTotal: 1n }, { time }), -+ ).rejects.toThrow(/Rollup__InvalidInboxRollingHash/); -+ }); -+ -+ // A pipelined checkpoint is validated against a parent that has not landed: the state overrides describe that -+ // parent (tips, archive, slot, consumed total) and the call reads the parent's total from them, so the same call -+ // without the overrides has no such parent and a parent total ahead of the child rejects the child. -+ it('validates against an unpublished parent supplied through state overrides', async () => { -+ // Two fresh buckets: the parent consumed through the first, the child consumes the second. -+ await sendMessageInNewBucket(); -+ const parentSeq = await inbox.getCurrentBucketSeq(); -+ await sendMessageInNewBucket(); -+ const childSeq = await inbox.getCurrentBucketSeq(); -+ expect(childSeq).toBe(parentSeq + 1n); -+ const parentBucket = await inbox.getBucket(parentSeq); -+ const childBucket = await inbox.getBucket(childSeq); -+ expect(childBucket.totalMsgCount).toBe(parentBucket.totalMsgCount + 1n); -+ -+ const { args, time } = await buildHeader(childBucket.rollingHash); -+ const parentArchive = Fr.random(); -+ const parentSlot = SlotNumber(Number(args.header.slotNumber) - 1); -+ // The mana fee derivation reads the parent's fee header; the genesis one stands in for a real parent's. -+ const parentFeeHeader = await rollup.getFeeHeader(0n); -+ const overridesFor = (parentInboxMsgTotal: bigint) => -+ buildSimulationOverridesStateOverride(rollup, { -+ chainTipsOverride: { pending: CheckpointNumber(1), proven: CheckpointNumber(0) }, -+ pendingCheckpointState: { -+ archive: parentArchive, -+ slotNumber: parentSlot, -+ inboxMsgTotal: parentInboxMsgTotal, -+ inboxConsumedBucket: parentSeq, -+ feeHeader: parentFeeHeader, -+ }, -+ }); -+ -+ const stateOverrides = await overridesFor(parentBucket.totalMsgCount); -+ const minFee = await rollup.getManaMinFeeAt(time, true, stateOverrides); -+ const header = { -+ ...args.header, -+ lastArchiveRoot: parentArchive.toString(), -+ gasFees: { ...args.header.gasFees, feePerL2Gas: minFee }, -+ }; -+ const childArgs = { -+ ...args, -+ header, -+ expectedTotal: childBucket.totalMsgCount, -+ expectedParentCheckpointNumber: 1n, -+ }; -+ -+ await expect( -+ rollup.validateCheckpointHeaderAndInbox(l1TxUtils, childArgs, { time, stateOverrides }), -+ ).resolves.toBe(childSeq); -+ -+ // Without the parent's state the effective parent is still checkpoint 0. -+ await expect(rollup.validateCheckpointHeaderAndInbox(l1TxUtils, childArgs, { time })).rejects.toThrow( -+ /Rollup__UnexpectedParentCheckpoint/, -+ ); -+ -+ // The parent's stored total, not the caller, is the consumption floor. -+ await expect( -+ rollup.validateCheckpointHeaderAndInbox(l1TxUtils, childArgs, { -+ time, -+ stateOverrides: await overridesFor(childBucket.totalMsgCount + 3n), -+ }), -+ ).rejects.toThrow(/Rollup__InboxConsumptionBehindParent/); -+ }); -+ }); -+ - describe('getVkTreeRoot and getProtocolContractsHash', () => { - it('reads vkTreeRoot', async () => { - const result = await rollup.getVkTreeRoot(); -diff --git a/yarn-project/ethereum/src/contracts/rollup.ts b/yarn-project/ethereum/src/contracts/rollup.ts -index b72cc405e81aee557a54df55a165666884e939b9..e8c5da3881bb37da8f712d67a0a22a6cf564d3be 100644 ---- a/yarn-project/ethereum/src/contracts/rollup.ts -+++ b/yarn-project/ethereum/src/contracts/rollup.ts -@@ -1,3 +1,4 @@ -+import { ErrorsAbi } from '@aztec-foundation/l1-artifacts/ErrorsAbi'; - import { EscapeHatchAbi } from '@aztec-foundation/l1-artifacts/EscapeHatchAbi'; - import { RollupAbi } from '@aztec-foundation/l1-artifacts/RollupAbi'; - import { RollupStorage } from '@aztec-foundation/l1-artifacts/RollupStorage'; -@@ -22,6 +23,7 @@ import { - type StateOverride, - type WatchContractEventReturnType, - decodeErrorResult, -+ decodeFunctionResult, - encodeAbiParameters, - encodeFunctionData, - getContract, -@@ -33,9 +35,9 @@ import { getPublicClient } from '../client.js'; - import type { DeployAztecL1ContractsReturnType } from '../deploy_aztec_l1_contracts.js'; - import type { L1ContractAddresses } from '../l1_contract_addresses.js'; - import type { L1ReaderConfig } from '../l1_reader.js'; --import type { L1TxRequest, L1TxUtils } from '../l1_tx_utils/index.js'; -+import type { L1TxRequest, L1TxUtils, ReadOnlyL1TxUtils } from '../l1_tx_utils/index.js'; - import type { ViemClient } from '../types.js'; --import { formatViemError } from '../utils.js'; -+import { formatViemError, mergeAbis } from '../utils.js'; - import { GSEContract } from './gse.js'; - import type { L1EventLog } from './log.js'; - import { SlasherContract } from './slasher_contract.js'; -@@ -96,6 +98,21 @@ export type ViemGasFees = { - feePerL2Gas: bigint; - }; - -+/** Inputs of the Rollup's integrated header and Inbox preflight, mirroring the contract's `CheckpointPreflightArgs`. */ -+export type CheckpointPreflightArgs = { -+ header: ViemHeader; -+ attestations: ViemCommitteeAttestations; -+ signers: `0x${string}`[]; -+ attestationsAndSignersSignature: ViemSignature; -+ digest: `0x${string}`; -+ blobsHash: `0x${string}`; -+ flags: { ignoreDA: boolean }; -+ /** Cumulative Inbox message count the checkpoint consumed up to; must be a live bucket boundary. */ -+ expectedTotal: bigint; -+ /** Checkpoint number the header was built on; the call derives the real parent and rejects any other. */ -+ expectedParentCheckpointNumber: bigint; -+}; -+ - /** - * Status of a validator/attester in the staking system. - * Matches the Status enum in StakingLib.sol -@@ -890,6 +907,39 @@ export class RollupContract { - } - } - -+ /** -+ * Simulates `validateCheckpointHeaderAndInbox` at the intended execution time and state, and returns the Inbox -+ * bucket sequence to submit to `propose` as `bucketHint`. -+ * -+ * The call derives the parent checkpoint from the simulated Rollup storage the way `propose` does (the proven tip if -+ * the pending chain is prunable at `time`), so `stateOverrides` must describe the state the real transaction will -+ * see: a pipelined parent, or the tips after a bundled invalidation. It runs over `eth_simulateV1` with a block time -+ * override, the same transport as the header-only preflight, and throws a formatted error naming the contract -+ * revert (`Rollup__UnexpectedParentCheckpoint`, `Rollup__InboxTotalNotAtBucketBoundary`, -+ * `Inbox__NoBucketAtOrBeforeTotal`, or any header/Inbox consumption error `propose` raises) when the checkpoint is -+ * not publishable in that context. -+ * @param l1TxUtils - The simulation transport -+ * @param args - The header validation inputs plus the consumed Inbox total and the expected parent -+ * @param opts - The block timestamp to simulate at, the state overrides to apply, and the simulated sender -+ */ -+ public async validateCheckpointHeaderAndInbox( -+ l1TxUtils: Pick, -+ args: CheckpointPreflightArgs, -+ opts: { time: bigint; stateOverrides?: StateOverride; from?: `0x${string}` }, -+ ): Promise { -+ const { result } = await l1TxUtils.simulate( -+ { -+ to: this.address, -+ data: encodeFunctionData({ abi: RollupAbi, functionName: 'validateCheckpointHeaderAndInbox', args: [args] }), -+ from: opts.from, -+ }, -+ { time: opts.time }, -+ opts.stateOverrides ?? [], -+ mergeAbis([RollupAbi, ErrorsAbi]), -+ ); -+ return decodeFunctionResult({ abi: RollupAbi, functionName: 'validateCheckpointHeaderAndInbox', data: result }); -+ } -+ - /** - * @notice Calls `canProposeAtTime` with the time of the next Ethereum block and the sender address - * -diff --git a/yarn-project/ethereum/src/contracts/utils.ts b/yarn-project/ethereum/src/contracts/utils.ts -index 5877ba44392da9e77b9198a69c0c40951ae6bde4..b3a061a615e653939ac6c5432659485a408b9b19 100644 ---- a/yarn-project/ethereum/src/contracts/utils.ts -+++ b/yarn-project/ethereum/src/contracts/utils.ts -@@ -1,3 +1,5 @@ -+import { BaseError, ContractFunctionRevertedError } from 'viem'; -+ - import type { ViemClient } from '../types.js'; - import { BlockTagTooOldError } from './errors.js'; - -@@ -12,3 +14,12 @@ export async function checkBlockTag(block: bigint | undefined, publicClient: Vie - throw new BlockTagTooOldError(block, latestBlock); - } - } -+ -+/** Returns the name of the custom error a viem contract call reverted with, if the revert data could be decoded. */ -+export function getRevertedErrorName(err: unknown): string | undefined { -+ if (!(err instanceof BaseError)) { -+ return undefined; -+ } -+ const revertError = err.walk(e => e instanceof ContractFunctionRevertedError); -+ return revertError instanceof ContractFunctionRevertedError ? revertError.data?.errorName : undefined; -+} diff --git a/labs-patches/0007-fix-ethereum-drop-the-dead-ignoreSignatures-header-v.patch b/labs-patches/0007-fix-ethereum-drop-the-dead-ignoreSignatures-header-v.patch deleted file mode 100644 index f51a72cc4f45..000000000000 --- a/labs-patches/0007-fix-ethereum-drop-the-dead-ignoreSignatures-header-v.patch +++ /dev/null @@ -1,52 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Santiago Palladino -Date: Thu, 10 Sep 2026 18:38:20 -0300 -Subject: [PATCH] fix(ethereum): drop the dead ignoreSignatures header - validation flag - -`CheckpointHeaderValidationFlags` on the Rollup carries a single field, -`ignoreDA`. The `ignoreSignatures` field the TypeScript declared alongside it -is a leftover from `BlockHeaderValidationFlags`, which lost that flag when -signature verification moved out of the header check and became a function of -the attestations passed in. - -Because the flags argument reaches viem as a typed variable rather than an -inline literal, viem encoded it against the ABI and discarded the unknown key -with no type error and no runtime error. The flag has therefore had no effect -for as long as it has been declared, and removing it changes no encoded -calldata. - -Nor does it change intent: signature checks are skipped by passing empty -attestations, which the one caller that set `ignoreSignatures: true` -(`SequencerPublisher.validateCheckpointHeader`) already does, so the header -preflight it simulates keeps skipping them. - -diff --git a/yarn-project/ethereum/src/contracts/rollup.ts b/yarn-project/ethereum/src/contracts/rollup.ts -index e8c5da3881bb37da8f712d67a0a22a6cf564d3be..0b04e06b0845647877173fcd4fe723b509c40707 100644 ---- a/yarn-project/ethereum/src/contracts/rollup.ts -+++ b/yarn-project/ethereum/src/contracts/rollup.ts -@@ -887,10 +887,7 @@ export class RollupContract { - ViemSignature, - `0x${string}`, - `0x${string}`, -- { -- ignoreDA: boolean; -- ignoreSignatures: boolean; -- }, -+ { ignoreDA: boolean }, - ], - account: `0x${string}` | Account, - ): Promise { -diff --git a/yarn-project/sequencer-client/src/publisher/sequencer-publisher.ts b/yarn-project/sequencer-client/src/publisher/sequencer-publisher.ts -index 948f14d655618eb2c9516689074a18dd9efdc2e1..f20fbb70727827cbfc3f6eab2dd9ccd5b1571723 100644 ---- a/yarn-project/sequencer-client/src/publisher/sequencer-publisher.ts -+++ b/yarn-project/sequencer-client/src/publisher/sequencer-publisher.ts -@@ -983,7 +983,7 @@ export class SequencerPublisher { - header: CheckpointHeader, - simulationOverridesPlan?: SimulationOverridesPlan, - ): Promise { -- const flags = { ignoreDA: true, ignoreSignatures: true }; -+ const flags = { ignoreDA: true }; - - const args = [ - header.toViem(), diff --git a/labs-patches/0008-feat-stdlib-add-InboxParity-rungs-for-4-and-16-messa.patch b/labs-patches/0008-feat-stdlib-add-InboxParity-rungs-for-4-and-16-messa.patch deleted file mode 100644 index 49a647a797b0..000000000000 --- a/labs-patches/0008-feat-stdlib-add-InboxParity-rungs-for-4-and-16-messa.patch +++ /dev/null @@ -1,220 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Santiago Palladino -Date: Thu, 10 Sep 2026 14:22:47 -0300 -Subject: [PATCH] feat(stdlib): add InboxParity rungs for 4 and 16 messages - -The InboxParity ladder gains two smaller rungs in the protocol circuits, so the -node side has to know about them: `INBOX_PARITY_SIZES` grows to five entries -(ascending, so `pickInboxParitySize` keeps picking the smallest that fits), the -two artifacts join the server artifact union, the size-to-artifact map, the VK -index map and the plain-UltraHonk list in the bb-prover, and the codegen script -emits their input types. - -diff --git a/yarn-project/bb-prover/src/honk.ts b/yarn-project/bb-prover/src/honk.ts -index 5cac2075281bfe55bbfc2d8c73ae9026822d0ef4..6ed3dae2f0ff6e0edd55494795584359d938242d 100644 ---- a/yarn-project/bb-prover/src/honk.ts -+++ b/yarn-project/bb-prover/src/honk.ts -@@ -4,6 +4,8 @@ export type UltraHonkFlavor = 'ultra_honk' | 'ultra_keccak_honk' | 'ultra_starkn - - const UltraKeccakHonkCircuits = ['RootRollupArtifact'] as const satisfies ServerProtocolArtifact[]; - const UltraHonkCircuits = [ -+ 'InboxParity4Artifact', -+ 'InboxParity16Artifact', - 'InboxParity64Artifact', - 'InboxParity256Artifact', - 'InboxParity1024Artifact', -diff --git a/yarn-project/noir-protocol-circuits-types/src/artifacts/server.ts b/yarn-project/noir-protocol-circuits-types/src/artifacts/server.ts -index 2e8c9b9b695e3b94825d95208e0fb2143c38ae9a..86e3e6dfc20603c54ed758f678dd847a445ee4ac 100644 ---- a/yarn-project/noir-protocol-circuits-types/src/artifacts/server.ts -+++ b/yarn-project/noir-protocol-circuits-types/src/artifacts/server.ts -@@ -1,6 +1,8 @@ - import type { NoirCompiledCircuit, NoirCompiledCircuitWithName } from '@aztec-labs/stdlib/noir'; - - import PublicChonkVerifierJson from '../../artifacts/chonk_verifier_public.json' with { type: 'json' }; -+import InboxParity4Json from '../../artifacts/inbox_parity_4.json' with { type: 'json' }; -+import InboxParity16Json from '../../artifacts/inbox_parity_16.json' with { type: 'json' }; - import InboxParity64Json from '../../artifacts/inbox_parity_64.json' with { type: 'json' }; - import InboxParity256Json from '../../artifacts/inbox_parity_256.json' with { type: 'json' }; - import InboxParity1024Json from '../../artifacts/inbox_parity_1024.json' with { type: 'json' }; -@@ -25,6 +27,8 @@ import TxMergeRollupJson from '../../artifacts/rollup_tx_merge.json' with { type - import type { ServerProtocolArtifact } from './types.js'; - - export const ServerCircuitArtifacts: Record = { -+ InboxParity4Artifact: InboxParity4Json as NoirCompiledCircuit, -+ InboxParity16Artifact: InboxParity16Json as NoirCompiledCircuit, - InboxParity64Artifact: InboxParity64Json as NoirCompiledCircuit, - InboxParity256Artifact: InboxParity256Json as NoirCompiledCircuit, - InboxParity1024Artifact: InboxParity1024Json as NoirCompiledCircuit, -@@ -46,6 +50,8 @@ export const ServerCircuitArtifacts: Record = { - // No separate simulated build for the inbox parity ladder: they verify no child proofs, so the constrained - // artifacts are used for simulation too. -+ InboxParity4Artifact: InboxParity4Json as NoirCompiledCircuit, -+ InboxParity16Artifact: InboxParity16Json as NoirCompiledCircuit, - InboxParity64Artifact: InboxParity64Json as NoirCompiledCircuit, - InboxParity256Artifact: InboxParity256Json as NoirCompiledCircuit, - InboxParity1024Artifact: InboxParity1024Json as NoirCompiledCircuit, -diff --git a/yarn-project/noir-protocol-circuits-types/src/artifacts/types.ts b/yarn-project/noir-protocol-circuits-types/src/artifacts/types.ts -index 5809ad8add7ca1de37d88c5ab61d954ccaf9a018..f8d1febc568f130ab387d337c6e2974c5d4e6290 100644 ---- a/yarn-project/noir-protocol-circuits-types/src/artifacts/types.ts -+++ b/yarn-project/noir-protocol-circuits-types/src/artifacts/types.ts -@@ -27,6 +27,8 @@ export type ClientProtocolArtifact = - - // These are all circuits that should generate proofs with the `recursive` flag. - export type ServerProtocolArtifact = -+ | 'InboxParity4Artifact' -+ | 'InboxParity16Artifact' - | 'InboxParity64Artifact' - | 'InboxParity256Artifact' - | 'InboxParity1024Artifact' -@@ -58,6 +60,10 @@ export interface ArtifactProvider { - - export function mapProtocolArtifactNameToCircuitName(artifact: ProtocolArtifact): CircuitName { - switch (artifact) { -+ case 'InboxParity4Artifact': -+ return 'inbox-parity-4'; -+ case 'InboxParity16Artifact': -+ return 'inbox-parity-16'; - case 'InboxParity64Artifact': - return 'inbox-parity-64'; - case 'InboxParity256Artifact': -diff --git a/yarn-project/noir-protocol-circuits-types/src/artifacts/vks/server.ts b/yarn-project/noir-protocol-circuits-types/src/artifacts/vks/server.ts -index 99f2f9d1e34558f1aa0d804f788f0442b321e9e7..9bcb5592bac74317399a089cc0a826ade0ba156f 100644 ---- a/yarn-project/noir-protocol-circuits-types/src/artifacts/vks/server.ts -+++ b/yarn-project/noir-protocol-circuits-types/src/artifacts/vks/server.ts -@@ -9,6 +9,8 @@ import { - CHECKPOINT_ROOT_SINGLE_BLOCK_ROLLUP_VK_INDEX, - HIDING_KERNEL_TO_PUBLIC_VK_INDEX, - HIDING_KERNEL_TO_ROLLUP_VK_INDEX, -+ INBOX_PARITY_4_VK_INDEX, -+ INBOX_PARITY_16_VK_INDEX, - INBOX_PARITY_64_VK_INDEX, - INBOX_PARITY_256_VK_INDEX, - INBOX_PARITY_1024_VK_INDEX, -@@ -31,6 +33,8 @@ import { - import { VerificationKeyData } from '@aztec-labs/stdlib/vks'; - - import PublicChonkVerifier from '../../../artifacts/chonk_verifier_public.json' with { type: 'json' }; -+import InboxParity4 from '../../../artifacts/inbox_parity_4.json' with { type: 'json' }; -+import InboxParity16 from '../../../artifacts/inbox_parity_16.json' with { type: 'json' }; - import InboxParity64 from '../../../artifacts/inbox_parity_64.json' with { type: 'json' }; - import InboxParity256 from '../../../artifacts/inbox_parity_256.json' with { type: 'json' }; - import InboxParity1024 from '../../../artifacts/inbox_parity_1024.json' with { type: 'json' }; -@@ -55,6 +59,8 @@ import { abiToVKData } from '../../utils/vk_json.js'; - import type { ProtocolCircuitName, ServerProtocolCircuitName } from '../types.js'; - - export const ServerCircuitVks: Record = { -+ InboxParity4Artifact: abiToVKData(InboxParity4), -+ InboxParity16Artifact: abiToVKData(InboxParity16), - InboxParity64Artifact: abiToVKData(InboxParity64), - InboxParity256Artifact: abiToVKData(InboxParity256), - InboxParity1024Artifact: abiToVKData(InboxParity1024), -@@ -89,6 +95,8 @@ export const ProtocolCircuitVkIndexes: Record = { - HidingKernelToRollup: HIDING_KERNEL_TO_ROLLUP_VK_INDEX, - HidingKernelToPublic: HIDING_KERNEL_TO_PUBLIC_VK_INDEX, - PublicChonkVerifier: PUBLIC_CHONK_VERIFIER_VK_INDEX, -+ InboxParity4Artifact: INBOX_PARITY_4_VK_INDEX, -+ InboxParity16Artifact: INBOX_PARITY_16_VK_INDEX, - InboxParity64Artifact: INBOX_PARITY_64_VK_INDEX, - InboxParity256Artifact: INBOX_PARITY_256_VK_INDEX, - InboxParity1024Artifact: INBOX_PARITY_1024_VK_INDEX, -diff --git a/yarn-project/noir-protocol-circuits-types/src/execution/server.ts b/yarn-project/noir-protocol-circuits-types/src/execution/server.ts -index 9d3c870eba333ababe207d81b44ce7cb2ac522ad..a8c414261eba957920186b5b394113b9f25ece4c 100644 ---- a/yarn-project/noir-protocol-circuits-types/src/execution/server.ts -+++ b/yarn-project/noir-protocol-circuits-types/src/execution/server.ts -@@ -1,7 +1,13 @@ - import type { InputValue } from '@aztec-foundation/noir-noirc_abi'; - import type { InputMap } from '@aztec-foundation/noir-types'; - --import { INBOX_PARITY_SIZE_LARGE, INBOX_PARITY_SIZE_MEDIUM, INBOX_PARITY_SIZE_SMALL } from '@aztec-labs/constants'; -+import { -+ INBOX_PARITY_SIZE_LARGE, -+ INBOX_PARITY_SIZE_MEDIUM, -+ INBOX_PARITY_SIZE_SMALL, -+ INBOX_PARITY_SIZE_XSMALL, -+ INBOX_PARITY_SIZE_XXSMALL, -+} from '@aztec-labs/constants'; - import { pushTestData } from '@aztec-labs/foundation/testing'; - import type { InboxParityPrivateInputs } from '@aztec-labs/stdlib/parity'; - import type { -@@ -63,6 +69,10 @@ export function convertInboxParityPrivateInputsToNoir(inputs: InboxParityPrivate - /** Maps an InboxParity ladder size to its server artifact. */ - export function inboxParityArtifactForSize(size: number): ServerProtocolArtifact { - switch (size) { -+ case INBOX_PARITY_SIZE_XXSMALL: -+ return 'InboxParity4Artifact'; -+ case INBOX_PARITY_SIZE_XSMALL: -+ return 'InboxParity16Artifact'; - case INBOX_PARITY_SIZE_SMALL: - return 'InboxParity64Artifact'; - case INBOX_PARITY_SIZE_MEDIUM: -diff --git a/yarn-project/noir-protocol-circuits-types/src/scripts/generate_ts_from_abi.ts b/yarn-project/noir-protocol-circuits-types/src/scripts/generate_ts_from_abi.ts -index 21911887c6d4623e3259441209264a0a66fad50b..1035e60c8fb2687bb028027f8c7b22414479f475 100644 ---- a/yarn-project/noir-protocol-circuits-types/src/scripts/generate_ts_from_abi.ts -+++ b/yarn-project/noir-protocol-circuits-types/src/scripts/generate_ts_from_abi.ts -@@ -8,6 +8,8 @@ import { promises as fs } from 'fs'; - const log = createConsoleLogger('autogenerate'); - - const circuits = [ -+ 'inbox_parity_4', -+ 'inbox_parity_16', - 'inbox_parity_64', - 'inbox_parity_256', - 'inbox_parity_1024', -diff --git a/yarn-project/prover-client/src/test/bb_prover_parity.test.ts b/yarn-project/prover-client/src/test/bb_prover_parity.test.ts -index 91f467c362a75c1d4d8dd035c9dba27ff39bb425..062571a4300529c54f5a3a54caba92d73512e237 100644 ---- a/yarn-project/prover-client/src/test/bb_prover_parity.test.ts -+++ b/yarn-project/prover-client/src/test/bb_prover_parity.test.ts -@@ -12,6 +12,8 @@ const logger = createLogger('prover-client:test:bb-prover-parity'); - - // Maps a ladder size to its server artifact name, mirroring the bb-prover's own selection. - const artifactForSize: Record = { -+ 4: 'InboxParity4Artifact', -+ 16: 'InboxParity16Artifact', - 64: 'InboxParity64Artifact', - 256: 'InboxParity256Artifact', - 1024: 'InboxParity1024Artifact', -diff --git a/yarn-project/stdlib/src/parity/inbox_parity_private_inputs.ts b/yarn-project/stdlib/src/parity/inbox_parity_private_inputs.ts -index cf0687ef9f7c3b6183b596b4299e854d79dc0064..dd96d2660fabb0afc7b61f2522fd22e75a5060d7 100644 ---- a/yarn-project/stdlib/src/parity/inbox_parity_private_inputs.ts -+++ b/yarn-project/stdlib/src/parity/inbox_parity_private_inputs.ts -@@ -1,4 +1,10 @@ --import { INBOX_PARITY_SIZE_LARGE, INBOX_PARITY_SIZE_MEDIUM, INBOX_PARITY_SIZE_SMALL } from '@aztec-labs/constants'; -+import { -+ INBOX_PARITY_SIZE_LARGE, -+ INBOX_PARITY_SIZE_MEDIUM, -+ INBOX_PARITY_SIZE_SMALL, -+ INBOX_PARITY_SIZE_XSMALL, -+ INBOX_PARITY_SIZE_XXSMALL, -+} from '@aztec-labs/constants'; - import { padArrayEnd } from '@aztec-labs/foundation/collection'; - import { Fr } from '@aztec-labs/foundation/curves/bn254'; - import { bufferSchemaFor } from '@aztec-labs/foundation/schemas'; -@@ -6,7 +12,13 @@ import { BufferReader, serializeToBuffer } from '@aztec-labs/foundation/serializ - import { bufferToHex, hexToBuffer } from '@aztec-labs/foundation/string'; - - /** The InboxParity size ladder, ascending. One VK per size; the prover proves the smallest that fits. */ --export const INBOX_PARITY_SIZES = [INBOX_PARITY_SIZE_SMALL, INBOX_PARITY_SIZE_MEDIUM, INBOX_PARITY_SIZE_LARGE] as const; -+export const INBOX_PARITY_SIZES = [ -+ INBOX_PARITY_SIZE_XXSMALL, -+ INBOX_PARITY_SIZE_XSMALL, -+ INBOX_PARITY_SIZE_SMALL, -+ INBOX_PARITY_SIZE_MEDIUM, -+ INBOX_PARITY_SIZE_LARGE, -+] as const; - - /** A valid InboxParity ladder size. */ - export type InboxParitySize = (typeof INBOX_PARITY_SIZES)[number]; -diff --git a/yarn-project/stdlib/src/stats/stats.ts b/yarn-project/stdlib/src/stats/stats.ts -index 276c1a70dbc6f8d87a55f6a1727f526e6fa51c0f..e1627a488af61e35e8846abf22b65f5e5333b4a7 100644 ---- a/yarn-project/stdlib/src/stats/stats.ts -+++ b/yarn-project/stdlib/src/stats/stats.ts -@@ -101,6 +101,8 @@ export type ClientCircuitName = - | 'app-circuit'; - - export type ServerCircuitName = -+ | 'inbox-parity-4' -+ | 'inbox-parity-16' - | 'inbox-parity-64' - | 'inbox-parity-256' - | 'inbox-parity-1024' diff --git a/labs-patches/0009-feat-world-state-seed-protocol-contract-registration.patch b/labs-patches/0009-feat-world-state-seed-protocol-contract-registration.patch deleted file mode 100644 index 8f49fc990a54..000000000000 --- a/labs-patches/0009-feat-world-state-seed-protocol-contract-registration.patch +++ /dev/null @@ -1,880 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Santiago Palladino -Date: Wed, 16 Sep 2026 09:44:36 -0300 -Subject: [PATCH] feat(world-state)!: seed protocol contract registration - nullifiers at genesis - -Generate the siloed class-registration and magic-instance nullifiers for every -bundled protocol contract and seed them into the genesis nullifier tree by -default (DEFAULT_GENESIS_DATA in @aztec-labs/protocol-contracts), so that an -on-chain re-publish of a bundled protocol class is rejected as a duplicate -nullifier instead of reaching the archiver. - -GenesisData.prefilledNullifiers becomes required, so every explicit genesis -literal has to choose between the canonical default and a deliberately empty -tree. getGenesisValues' fifth argument now means *additional* nullifiers on top -of the protocol baseline, and duplicates are rejected rather than discarded. - -This moves the genesis nullifier-tree root, block-header hash and archive root, -so it only applies to a network initialized from the new genesis. - -diff --git a/yarn-project/aztec/src/mainnet_compatibility.test.ts b/yarn-project/aztec/src/mainnet_compatibility.test.ts -index 7f1ee58760e8c8f7823a16d91d73afc04e05fd1d..7fbd7e1587aa3cd6e7fd686e4ad145e29f949c3e 100644 ---- a/yarn-project/aztec/src/mainnet_compatibility.test.ts -+++ b/yarn-project/aztec/src/mainnet_compatibility.test.ts -@@ -1,7 +1,10 @@ - import { Fr } from '@aztec-labs/aztec.js/fields'; - import { getVKTreeRoot } from '@aztec-labs/noir-protocol-circuits-types/vk-tree'; - import { protocolContractsHash } from '@aztec-labs/protocol-contracts'; --import { getGenesisValues } from '@aztec-labs/world-state/testing'; -+import { computeFeePayerBalanceLeafSlot } from '@aztec-labs/protocol-contracts/fee-juice'; -+import type { AztecAddress } from '@aztec-labs/stdlib/aztec-address'; -+import { MerkleTreeId, PublicDataTreeLeaf } from '@aztec-labs/stdlib/trees'; -+import { NativeWorldStateService } from '@aztec-labs/world-state'; - - /** - * This test suit makes sure that the code in the monorepo is still compatible with the latest version of mainnet -@@ -17,15 +20,37 @@ describe('Mainnet compatibility', () => { - Fr.fromHexString('0x04b0ccfafec7ff9a4a31c67c21377f73a8c01dcadc8710ee2a8e7751565db7f4'), - ); - }); -+ // Mainnet was initialized before the protocol contract registration nullifiers were seeded at genesis, so its root -+ // is rebuilt from an empty nullifier tree rather than from today's default genesis. - it('has expected Genesis tree roots', async () => { -- // initial accounts get initial fee juice added to their balance -- const { genesisArchiveRoot } = await getGenesisValues( -- /* initial accounts */ [], -- /* initial fee juice */ Fr.ZERO, -- /* initial public data leaves */ [], -- ); -+ const genesisArchiveRoot = await historicalGenesisArchiveRoot(/* funded accounts */ [], Fr.ZERO); - expect(genesisArchiveRoot).toEqual( - Fr.fromHexString('0x0a0877e7fa8646252b46122976de111ece73d0cc054da8d780e3f12ec1709305'), - ); - }); - }); -+ -+/** -+ * Rebuilds the genesis archive root of a network that was initialized before the protocol contract registration -+ * nullifiers were seeded at genesis: an empty nullifier tree, only the deployment's fee-juice prefunding, timestamp 0. -+ * `getGenesisValues` cannot express this any more, because it always seeds the canonical protocol baseline. -+ */ -+async function historicalGenesisArchiveRoot(fundedAccounts: AztecAddress[], initialAccountFeeJuice: Fr) { -+ const prefilledPublicData = await Promise.all( -+ fundedAccounts.map( -+ async address => new PublicDataTreeLeaf(await computeFeePayerBalanceLeafSlot(address), initialAccountFeeJuice), -+ ), -+ ); -+ prefilledPublicData.sort((a, b) => (b.slot.lt(a.slot) ? 1 : -1)); -+ -+ const ws = await NativeWorldStateService.ephemeral({ -+ prefilledPublicData, -+ prefilledNullifiers: [], -+ genesisTimestamp: 0n, -+ }); -+ try { -+ return new Fr((await ws.getCommitted().getTreeInfo(MerkleTreeId.ARCHIVE)).root); -+ } finally { -+ await ws.close(); -+ } -+} -diff --git a/yarn-project/aztec/src/testnet_compatibility.test.ts b/yarn-project/aztec/src/testnet_compatibility.test.ts -index f8ed746df39e3822533d1173352383f7a50c0eb7..ba864ad5e12c93f392b5650de599f16d119bb25a 100644 ---- a/yarn-project/aztec/src/testnet_compatibility.test.ts -+++ b/yarn-project/aztec/src/testnet_compatibility.test.ts -@@ -3,7 +3,11 @@ import { Fr } from '@aztec-labs/aztec.js/fields'; - import { getSponsoredFPCAddress } from '@aztec-labs/cli/cli-utils'; - import { getVKTreeRoot } from '@aztec-labs/noir-protocol-circuits-types/vk-tree'; - import { protocolContractsHash } from '@aztec-labs/protocol-contracts'; --import { getGenesisValues } from '@aztec-labs/world-state/testing'; -+import { computeFeePayerBalanceLeafSlot } from '@aztec-labs/protocol-contracts/fee-juice'; -+import type { AztecAddress } from '@aztec-labs/stdlib/aztec-address'; -+import { MerkleTreeId, PublicDataTreeLeaf } from '@aztec-labs/stdlib/trees'; -+import { NativeWorldStateService } from '@aztec-labs/world-state'; -+import { defaultInitialAccountFeeJuice } from '@aztec-labs/world-state/testing'; - - /** - * This test suit makes sure that the code in the monorepo is still compatible with the latest version of testnet -@@ -19,14 +23,41 @@ describe('Testnet compatibility', () => { - Fr.fromHexString('0x04b0ccfafec7ff9a4a31c67c21377f73a8c01dcadc8710ee2a8e7751565db7f4'), - ); - }); -+ // Testnet was initialized before the protocol contract registration nullifiers were seeded at genesis, so its root -+ // is rebuilt from an empty nullifier tree rather than from today's default genesis. - it('has expected Genesis tree roots', async () => { - const initialAccounts: InitialAccountData[] = []; - const sponsoredFPCAddress = await getSponsoredFPCAddress(); - const initialFundedAccounts = initialAccounts.map(a => a.address).concat(sponsoredFPCAddress); -- const { genesisArchiveRoot } = await getGenesisValues(initialFundedAccounts); -+ const genesisArchiveRoot = await historicalGenesisArchiveRoot(initialFundedAccounts, defaultInitialAccountFeeJuice); - - expect(genesisArchiveRoot).toEqual( - Fr.fromHexString('0x271f7321aa0cb733bdee9ddd1b0497bb1b4b97eea2182cee0f516c19005ab2bf'), - ); - }); - }); -+ -+/** -+ * Rebuilds the genesis archive root of a network that was initialized before the protocol contract registration -+ * nullifiers were seeded at genesis: an empty nullifier tree, only the deployment's fee-juice prefunding, timestamp 0. -+ * `getGenesisValues` cannot express this any more, because it always seeds the canonical protocol baseline. -+ */ -+async function historicalGenesisArchiveRoot(fundedAccounts: AztecAddress[], initialAccountFeeJuice: Fr) { -+ const prefilledPublicData = await Promise.all( -+ fundedAccounts.map( -+ async address => new PublicDataTreeLeaf(await computeFeePayerBalanceLeafSlot(address), initialAccountFeeJuice), -+ ), -+ ); -+ prefilledPublicData.sort((a, b) => (b.slot.lt(a.slot) ? 1 : -1)); -+ -+ const ws = await NativeWorldStateService.ephemeral({ -+ prefilledPublicData, -+ prefilledNullifiers: [], -+ genesisTimestamp: 0n, -+ }); -+ try { -+ return new Fr((await ws.getCommitted().getTreeInfo(MerkleTreeId.ARCHIVE)).root); -+ } finally { -+ await ws.close(); -+ } -+} -diff --git a/yarn-project/end-to-end/src/automine/contracts/deploy/contract_class_registration.test.ts b/yarn-project/end-to-end/src/automine/contracts/deploy/contract_class_registration.test.ts -index e2331fe921859216e180585cc81892368751175b..b54135a72057559327a22254a74889e1dbb0dc27 100644 ---- a/yarn-project/end-to-end/src/automine/contracts/deploy/contract_class_registration.test.ts -+++ b/yarn-project/end-to-end/src/automine/contracts/deploy/contract_class_registration.test.ts -@@ -16,6 +16,7 @@ import type { BlockNumber } from '@aztec-labs/foundation/branded-types'; - import { writeTestData } from '@aztec-labs/foundation/testing/files'; - import { StatefulTestContract } from '@aztec-labs/noir-test-contracts.js/StatefulTest'; - import { TestContract } from '@aztec-labs/noir-test-contracts.js/Test'; -+import { BundledProtocolContractsProvider } from '@aztec-labs/protocol-contracts/providers/bundle'; - import type { ContractClassIdPreimage } from '@aztec-labs/stdlib/contract'; - import { PublicKeys } from '@aztec-labs/stdlib/keys'; - import { jest } from '@jest/globals'; -@@ -80,6 +81,21 @@ describe('automine/contracts/deploy/contract_class_registration', () => { - expect(registeredClass!.privateFunctionsRoot.toString()).toEqual(contractClass.privateFunctionsRoot.toString()); - expect(registeredClass!.packedBytecode.toString('hex')).toEqual(contractClass.packedBytecode.toString('hex')); - }); -+ -+ it('rejects re-publishing a bundled protocol contract class as a duplicate nullifier', async () => { -+ // The genesis nullifier tree seeds the class-registration nullifier of every bundled protocol contract, so -+ // publishing one through the normal path recomputes the same class id and pushes a nullifier that already -+ // exists. Without that seeding the publish succeeds and the node ends up with a second registration of a class -+ // it already preloaded at block 0. -+ const provider = new BundledProtocolContractsProvider(); -+ const { artifact: feeJuiceArtifact } = await provider.getProtocolContractArtifact('FeeJuice'); -+ const feeJuiceClass = await getContractClassFromArtifact(feeJuiceArtifact); -+ expect(await aztecNode.getContractClass(feeJuiceClass.id)).toBeDefined(); -+ -+ logger.info(`Attempting to re-publish bundled protocol class ${feeJuiceClass.id.toString()}`); -+ const interaction = await publishContractClass(wallet, feeJuiceArtifact); -+ await expect(interaction.send({ from: defaultAccountAddress })).rejects.toThrow(DUPLICATE_NULLIFIER_ERROR); -+ }); - }); - - const testDeployingAnInstance = ( -diff --git a/yarn-project/end-to-end/src/fixtures/standard_contracts_genesis.ts b/yarn-project/end-to-end/src/fixtures/standard_contracts_genesis.ts -index 81d0509d97e648d4a06e220920e73c1f9e00a74b..30a49ac0927a7fc89b7fa6ed6baea7b960f1bd7f 100644 ---- a/yarn-project/end-to-end/src/fixtures/standard_contracts_genesis.ts -+++ b/yarn-project/end-to-end/src/fixtures/standard_contracts_genesis.ts -@@ -11,7 +11,8 @@ import { siloNullifier } from '@aztec-labs/stdlib/hash'; - * `ContractInstanceRegistry.publish_for_public_execution` pushes, using the contract's real derived address (standard - * contracts are deployed at artifact-derived addresses, not magic protocol addresses). - * -- * Seed these into the genesis nullifier tree (as the 5th `getGenesisValues` arg) alongside the archiver's -+ * Seed these into the genesis nullifier tree (as the 5th `getGenesisValues` arg, which adds them on top of the -+ * canonical protocol contract nullifiers that every genesis carries) alongside the archiver's - * `testPreloadStandardContracts` preload: the store preload makes the `ensure*Published` guards short-circuit, and these - * nullifiers make the AVM's deployment-nullifier check pass when the contracts are called. Every e2e node genesis that - * feeds an L1 `genesisArchiveRoot` must seed the same set, or the world-state root diverges from the deployed rollup. -diff --git a/yarn-project/foundation/src/testing/files/index.ts b/yarn-project/foundation/src/testing/files/index.ts -index 1ef8387ecf7f1fd8d77f3b813b1f4948b8ff6faf..27689a32f44b9cf52acdf1fbdf231a82db4aa37d 100644 ---- a/yarn-project/foundation/src/testing/files/index.ts -+++ b/yarn-project/foundation/src/testing/files/index.ts -@@ -64,6 +64,16 @@ export function updateProtocolCircuitSampleInputs(circuitName: string, value: st - logger(`Updated test data in ${targetFile} for ${circuitName}`); - } - -+/** -+ * Resolves the output path for an l1-contracts test fixture. The `l1-contracts` project lives in the foundation -+ * repository, which is not an ancestor of this checkout, so regenerating fixtures from here requires pointing -+ * `AZTEC_L1_FIXTURES_DIR` at `l1-contracts/test/fixtures` in the foundation tree. The fallback is the historical path -+ * relative to the running package's working directory. -+ */ -+export function getL1FixturePath(fileName: string) { -+ return join(process.env.AZTEC_L1_FIXTURES_DIR ?? '../../l1-contracts/test/fixtures', fileName); -+} -+ - export function getPathToFile(targetFileFromRepoRoot: string) { - const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../../../../../'); - if (!existsSync(join(repoRoot, 'CODEOWNERS'))) { -diff --git a/yarn-project/p2p/src/msg_validators/tx_validator/tx_validator_bench.test.ts b/yarn-project/p2p/src/msg_validators/tx_validator/tx_validator_bench.test.ts -index ebde5f89463df3699ff203aca7cb6d8d12e86258..8d1aeb5507dba7c016354f1a28db8e52b33ecd39 100644 ---- a/yarn-project/p2p/src/msg_validators/tx_validator/tx_validator_bench.test.ts -+++ b/yarn-project/p2p/src/msg_validators/tx_validator/tx_validator_bench.test.ts -@@ -7,7 +7,7 @@ import { BlockNumber } from '@aztec-labs/foundation/branded-types'; - import { padArrayEnd, times } from '@aztec-labs/foundation/collection'; - import { Fr } from '@aztec-labs/foundation/curves/bn254'; - import { Timer } from '@aztec-labs/foundation/timer'; --import { ProtocolContractAddress } from '@aztec-labs/protocol-contracts'; -+import { DEFAULT_GENESIS_DATA, ProtocolContractAddress } from '@aztec-labs/protocol-contracts'; - import { computeFeePayerBalanceLeafSlot } from '@aztec-labs/protocol-contracts/fee-juice'; - import { AztecAddress } from '@aztec-labs/stdlib/aztec-address'; - import { BlockHash } from '@aztec-labs/stdlib/block'; -@@ -150,6 +150,7 @@ describe('TxValidator: Benchmarks', () => { - // Create real LMDB-backed world state with fee payer balance - const feePayerLeafSlot = await computeFeePayerBalanceLeafSlot(gasTx.data.feePayer); - const genesis: GenesisData = { -+ ...DEFAULT_GENESIS_DATA, - prefilledPublicData: [new PublicDataTreeLeaf(feePayerLeafSlot, new Fr(10n ** 18n))], - genesisTimestamp: 0n, - }; -@@ -372,6 +373,7 @@ describe('TxValidator: Benchmarks', () => { - // Create world state with fee payer balance only (initial tree size limits prefilled data) - const feePayerLeafSlot = await computeFeePayerBalanceLeafSlot(gasTx.data.feePayer); - localWs = await NativeWorldStateService.tmp(true, { -+ ...DEFAULT_GENESIS_DATA, - prefilledPublicData: [new PublicDataTreeLeaf(feePayerLeafSlot, new Fr(10n ** 18n))], - genesisTimestamp: 0n, - }); -diff --git a/yarn-project/protocol-contracts/src/genesis_data.test.ts b/yarn-project/protocol-contracts/src/genesis_data.test.ts -new file mode 100644 -index 0000000000000000000000000000000000000000..17aab4167420709743263d54ffdd7ff5a8f3647c ---- /dev/null -+++ b/yarn-project/protocol-contracts/src/genesis_data.test.ts -@@ -0,0 +1,36 @@ -+import { Fr } from '@aztec-labs/foundation/curves/bn254'; -+import { getContractClassFromArtifact } from '@aztec-labs/stdlib/contract'; -+import { siloNullifier } from '@aztec-labs/stdlib/hash'; -+ -+import { DEFAULT_GENESIS_DATA } from './genesis_data.js'; -+import { ProtocolContractAddress, protocolContractNames } from './protocol_contract_data.js'; -+import { ProtocolContractArtifact } from './provider/bundle.js'; -+ -+describe('DEFAULT_GENESIS_DATA', () => { -+ // Recomputed from the bundled artifacts rather than read back from the generator's output, so a stale -+ // `protocol_contract_data.ts` or a mistake in its generator fails here. This does not catch a class id rotation: -+ // the generated file is rebuilt from the same artifacts, so both sides move together. What catches that is the -+ // pinned-root assertion in world-state's `testing.test.ts`, which compares a live genesis against -+ // `GENESIS_ARCHIVE_ROOT`. -+ it('seeds exactly the registration nullifiers the bundled protocol artifacts imply', async () => { -+ const expected: Fr[] = []; -+ for (const name of protocolContractNames) { -+ const { id: classId } = await getContractClassFromArtifact(ProtocolContractArtifact[name]); -+ expected.push(await siloNullifier(ProtocolContractAddress.ContractClassRegistry, classId)); -+ expected.push( -+ await siloNullifier(ProtocolContractAddress.ContractInstanceRegistry, ProtocolContractAddress[name].toField()), -+ ); -+ } -+ expected.sort((a, b) => (a.toBigInt() < b.toBigInt() ? -1 : 1)); -+ -+ expect(DEFAULT_GENESIS_DATA.prefilledNullifiers.map(n => n.toString())).toEqual(expected.map(n => n.toString())); -+ }); -+ -+ it('is strictly increasing, as the indexed nullifier tree requires', () => { -+ const nullifiers = DEFAULT_GENESIS_DATA.prefilledNullifiers; -+ expect(nullifiers.length).toBeGreaterThan(0); -+ for (let i = 1; i < nullifiers.length; i++) { -+ expect(nullifiers[i - 1].toBigInt() < nullifiers[i].toBigInt()).toBe(true); -+ } -+ }); -+}); -diff --git a/yarn-project/protocol-contracts/src/genesis_data.ts b/yarn-project/protocol-contracts/src/genesis_data.ts -new file mode 100644 -index 0000000000000000000000000000000000000000..ebaf09433576bc298b0c8ab3869dc6f23b3a919b ---- /dev/null -+++ b/yarn-project/protocol-contracts/src/genesis_data.ts -@@ -0,0 +1,17 @@ -+import type { GenesisData } from '@aztec-labs/stdlib/world-state'; -+ -+import { ProtocolContractGenesisNullifiers } from './protocol_contract_data.js'; -+ -+/** -+ * Canonical genesis data for a production network. Seeds the protocol contract registration nullifiers -+ * ({@link ProtocolContractGenesisNullifiers}) into the genesis nullifier tree, so that an on-chain re-publish of a -+ * bundled protocol class id pushes an already-existing nullifier and the transaction is rejected as a duplicate -+ * nullifier before it ever reaches the archiver. The `GENESIS_BLOCK_HEADER_HASH` and `GENESIS_ARCHIVE_ROOT` constants -+ * are derived from this genesis, so production world-state callers must build on top of it rather than on -+ * `EMPTY_GENESIS_DATA`, or their roots diverge from the deployed rollup. -+ */ -+export const DEFAULT_GENESIS_DATA: GenesisData = { -+ prefilledPublicData: [], -+ prefilledNullifiers: ProtocolContractGenesisNullifiers, -+ genesisTimestamp: 0n, -+}; -diff --git a/yarn-project/protocol-contracts/src/index.ts b/yarn-project/protocol-contracts/src/index.ts -index b47cfa12fb8d1d8522dc877acfb52b197bc4e9d3..7b9fc23cfb9f989038038629b7374f4d80e32d28 100644 ---- a/yarn-project/protocol-contracts/src/index.ts -+++ b/yarn-project/protocol-contracts/src/index.ts -@@ -1,2 +1,3 @@ -+export * from './genesis_data.js'; - export * from './protocol_contract.js'; - export * from './protocol_contract_data.js'; -diff --git a/yarn-project/protocol-contracts/src/scripts/generate_data.ts b/yarn-project/protocol-contracts/src/scripts/generate_data.ts -index 0901a731d94705d762235cbc6a44dc93e920ad02..8af98302da42b6da2e26f274e4571984f628af33 100644 ---- a/yarn-project/protocol-contracts/src/scripts/generate_data.ts -+++ b/yarn-project/protocol-contracts/src/scripts/generate_data.ts -@@ -20,7 +20,7 @@ import { - computeInitializationHash, - getContractClassFromArtifact, - } from '@aztec-labs/stdlib/contract'; --import { computeSiloedPrivateLogFirstField } from '@aztec-labs/stdlib/hash'; -+import { computeSiloedPrivateLogFirstField, siloNullifier } from '@aztec-labs/stdlib/hash'; - import { PublicKeys } from '@aztec-labs/stdlib/keys'; - import { type NoirCompiledContract } from '@aztec-labs/stdlib/noir'; - import { ProtocolContracts } from '@aztec-labs/stdlib/tx'; -@@ -211,6 +211,42 @@ async function generateProtocolContractsList(names: string[], contractData: Cont - `; - } - -+// Generates the siloed registration nullifiers that the protocol contracts would emit if they were published on-chain, -+// so that world-state genesis can pre-insert them into the nullifier tree. With these present from genesis, an on-chain -+// re-publish of a bundled protocol class id pushes an already-existing nullifier, making that transaction invalid -+// (duplicate nullifier) before it ever reaches the archiver. For each protocol contract we seed: -+// - the class nullifier: siloNullifier(ContractClassRegistry, classId) — what ContractClassRegistry.publish emits; -+// - the instance nullifier: siloNullifier(ContractInstanceRegistry, magicAddress) — the magic protocol address -+// (1/2/3) rather than the derived address, matching the node's block-0 protocol contract preload. No on-chain -+// deployment can produce a magic address, so this entry is unreachable from a publish; it exists so that genesis -+// reflects the canonical protocol deployment. -+// The list is sorted ascending because the indexed nullifier tree requires its prefilled leaves to be unique and -+// strictly increasing. -+async function generateGenesisNullifiers(names: string[], contractData: ContractData[]) { -+ const classRegistry = AztecAddress.fromBigIntUnsafe(BigInt(CONTRACT_CLASS_REGISTRY_CONTRACT_ADDRESS)); -+ const instanceRegistry = AztecAddress.fromBigIntUnsafe(BigInt(CONTRACT_INSTANCE_REGISTRY_CONTRACT_ADDRESS)); -+ -+ const nullifiers: Fr[] = []; -+ for (let i = 0; i < names.length; i++) { -+ const magicAddress = AztecAddress.fromBigIntUnsafe(BigInt(contractAddressMapping[names[i]])); -+ nullifiers.push(await siloNullifier(classRegistry, contractData[i].classId)); -+ nullifiers.push(await siloNullifier(instanceRegistry, magicAddress.toField())); -+ } -+ nullifiers.sort((a, b) => (a.toBigInt() < b.toBigInt() ? -1 : 1)); -+ -+ for (let i = 1; i < nullifiers.length; i++) { -+ if (nullifiers[i].toBigInt() === nullifiers[i - 1].toBigInt()) { -+ throw new Error(`Duplicate protocol contract genesis nullifier: ${nullifiers[i].toString()}`); -+ } -+ } -+ -+ return ` -+ export const ProtocolContractGenesisNullifiers: Fr[] = [ -+ ${nullifiers.map(n => `Fr.fromString('${n.toString()}')`).join(',\n')} -+ ]; -+ `; -+} -+ - // Generate the siloed log tags for events emitted via private logs. - async function generateLogTags() { - return ` -@@ -241,6 +277,8 @@ async function generateOutputFile(names: string[], contractData: ContractData[]) - - ${await generateProtocolContractsList(names, contractData)} - -+ ${await generateGenesisNullifiers(names, contractData)} -+ - ${await generateLogTags()} - `; - await fs.writeFile(outputFilePath, content); -diff --git a/yarn-project/prover-client/src/light/lightweight_checkpoint_builder.bench.test.ts b/yarn-project/prover-client/src/light/lightweight_checkpoint_builder.bench.test.ts -index c169436fc2a841b0d12aaa1cdb628b7d2c61fcca..b085bdab524ed53839236ae76e2343cc3ac58af7 100644 ---- a/yarn-project/prover-client/src/light/lightweight_checkpoint_builder.bench.test.ts -+++ b/yarn-project/prover-client/src/light/lightweight_checkpoint_builder.bench.test.ts -@@ -4,7 +4,7 @@ import { timesAsync } from '@aztec-labs/foundation/collection'; - import { Fr } from '@aztec-labs/foundation/curves/bn254'; - import { createLogger } from '@aztec-labs/foundation/log'; - import { getVKTreeRoot } from '@aztec-labs/noir-protocol-circuits-types/vk-tree'; --import { ProtocolContractsList } from '@aztec-labs/protocol-contracts'; -+import { DEFAULT_GENESIS_DATA, ProtocolContractsList } from '@aztec-labs/protocol-contracts'; - import { computeFeePayerBalanceLeafSlot } from '@aztec-labs/protocol-contracts/fee-juice'; - import { PublicDataWrite } from '@aztec-labs/stdlib/avm'; - import { AztecAddress } from '@aztec-labs/stdlib/aztec-address'; -@@ -44,6 +44,7 @@ describe('LightweightCheckpointBuilder benchmarks', () => { - feePayerBalance = new Fr(10n ** 20n); - const feePayerSlot = await computeFeePayerBalanceLeafSlot(feePayer); - const genesis: GenesisData = { -+ ...DEFAULT_GENESIS_DATA, - prefilledPublicData: [new PublicDataTreeLeaf(feePayerSlot, feePayerBalance)], - genesisTimestamp: 0n, - }; -diff --git a/yarn-project/prover-client/src/light/lightweight_checkpoint_builder.test.ts b/yarn-project/prover-client/src/light/lightweight_checkpoint_builder.test.ts -index 6fa99499c08051bc8897ac3838c6c3bb9d5dcf59..b4bf688b1c9dc360b4b154ab84160bbb07bbd12b 100644 ---- a/yarn-project/prover-client/src/light/lightweight_checkpoint_builder.test.ts -+++ b/yarn-project/prover-client/src/light/lightweight_checkpoint_builder.test.ts -@@ -3,7 +3,7 @@ import { BlockNumber, CheckpointNumber, SlotNumber } from '@aztec-labs/foundatio - import { timesAsync } from '@aztec-labs/foundation/collection'; - import { Fr } from '@aztec-labs/foundation/curves/bn254'; - import { getVKTreeRoot } from '@aztec-labs/noir-protocol-circuits-types/vk-tree'; --import { ProtocolContractsList } from '@aztec-labs/protocol-contracts'; -+import { DEFAULT_GENESIS_DATA, ProtocolContractsList } from '@aztec-labs/protocol-contracts'; - import { computeFeePayerBalanceLeafSlot } from '@aztec-labs/protocol-contracts/fee-juice'; - import { PublicDataWrite } from '@aztec-labs/stdlib/avm'; - import { AztecAddress } from '@aztec-labs/stdlib/aztec-address'; -@@ -31,6 +31,7 @@ describe('LightweightCheckpointBuilder', () => { - feePayerBalance = new Fr(10n ** 20n); - const feePayerSlot = await computeFeePayerBalanceLeafSlot(feePayer); - const genesis: GenesisData = { -+ ...DEFAULT_GENESIS_DATA, - prefilledPublicData: [new PublicDataTreeLeaf(feePayerSlot, feePayerBalance)], - genesisTimestamp: 0n, - }; -diff --git a/yarn-project/prover-client/src/mocks/test_context.ts b/yarn-project/prover-client/src/mocks/test_context.ts -index 78227407f77c67b7cf16f0d95078ea310dd004e9..02347cb79d4aa06891bb816e3820cc369a4fa6ac 100644 ---- a/yarn-project/prover-client/src/mocks/test_context.ts -+++ b/yarn-project/prover-client/src/mocks/test_context.ts -@@ -7,7 +7,7 @@ import type { Logger } from '@aztec-labs/foundation/log'; - import { SerialQueue } from '@aztec-labs/foundation/queue'; - import type { FieldsOf } from '@aztec-labs/foundation/types'; - import { getVKTreeRoot } from '@aztec-labs/noir-protocol-circuits-types/vk-tree'; --import { ProtocolContractsList } from '@aztec-labs/protocol-contracts'; -+import { DEFAULT_GENESIS_DATA, ProtocolContractsList } from '@aztec-labs/protocol-contracts'; - import { computeFeePayerBalanceLeafSlot } from '@aztec-labs/protocol-contracts/fee-juice'; - import { PublicDataWrite } from '@aztec-labs/stdlib/avm'; - import { AztecAddress } from '@aztec-labs/stdlib/aztec-address'; -@@ -92,6 +92,7 @@ export class TestContext { - const initialFeePayerBalance = new Fr(10n ** 20n); - const feePayerSlot = await computeFeePayerBalanceLeafSlot(feePayer); - const genesis: GenesisData = { -+ ...DEFAULT_GENESIS_DATA, - prefilledPublicData: [new PublicDataTreeLeaf(feePayerSlot, initialFeePayerBalance)], - genesisTimestamp: 0n, - }; -diff --git a/yarn-project/sequencer-client/src/publisher/write_json.ts b/yarn-project/sequencer-client/src/publisher/write_json.ts -index 3ea5c6875adafaaa1362db66e6f17c197d358955..3d72050fb673a2efc4f5a7b2d26d2dc4510709c7 100644 ---- a/yarn-project/sequencer-client/src/publisher/write_json.ts -+++ b/yarn-project/sequencer-client/src/publisher/write_json.ts -@@ -1,6 +1,7 @@ - import { BatchedBlob, Blob, getEthBlobEvaluationInputs, getPrefixedEthBlobCommitments } from '@aztec-labs/blob-lib'; - import { Fr } from '@aztec-labs/foundation/curves/bn254'; - import { EthAddress } from '@aztec-labs/foundation/eth-address'; -+import { getL1FixturePath } from '@aztec-labs/foundation/testing/files'; - import { AztecAddress } from '@aztec-labs/stdlib/aztec-address'; - import { L2Block } from '@aztec-labs/stdlib/block'; - import { CheckpointHeader } from '@aztec-labs/stdlib/rollup'; -@@ -25,8 +26,7 @@ export async function writeJson( - if (!AZTEC_GENERATE_TEST_DATA) { - return; - } -- // Path relative to the package.json in the end-to-end folder -- const path = `../../l1-contracts/test/fixtures/${fileName}.json`; -+ const path = getL1FixturePath(`${fileName}.json`); - - const asHex = (value: Fr | Buffer | EthAddress | AztecAddress, size = 64) => { - const buffer = Buffer.isBuffer(value) ? value : value.toBuffer(); -diff --git a/yarn-project/stdlib/src/block/l2_block.test.ts b/yarn-project/stdlib/src/block/l2_block.test.ts -index e6e82c1f920cb41f1c0805ec52dd07b455881005..c21982836752c3bce482ac3041ce644c29565b48 100644 ---- a/yarn-project/stdlib/src/block/l2_block.test.ts -+++ b/yarn-project/stdlib/src/block/l2_block.test.ts -@@ -32,7 +32,7 @@ describe('L2Block', () => { - '0x2590f2aab19dd791700b4a43d3f52bb88ef2409a3731da8e848663559202e4c6', - ); - emptyBlockHeader.state.partial.nullifierTree.root = Fr.fromString( -- '0x18935581a8ed73d08ffd00386fba55ba6c89f3ab848a76b8fedfa9034cee0454', -+ '0x21a19fe6f636fb24d9f63edb7b807613492cc0001c91e531917a2539f57e2ba8', - ); - emptyBlockHeader.state.partial.nullifierTree.nextAvailableLeafIndex = 128; - emptyBlockHeader.state.partial.publicDataTree.root = Fr.fromString( -diff --git a/yarn-project/stdlib/src/world-state/genesis_data.ts b/yarn-project/stdlib/src/world-state/genesis_data.ts -index 0e6488b96cb3aba3dc1186c9add7bb0006f9c621..d6f80eba64c7f1f2710f194285952b29f24f473d 100644 ---- a/yarn-project/stdlib/src/world-state/genesis_data.ts -+++ b/yarn-project/stdlib/src/world-state/genesis_data.ts -@@ -7,17 +7,24 @@ export type GenesisData = { - /** Public data tree leaves to pre-populate in the genesis state (e.g. fee juice balances). */ - prefilledPublicData: PublicDataTreeLeaf[]; - /** -- * Nullifiers to pre-insert into the genesis nullifier tree. Optional; defaults to an empty list, which leaves the -- * nullifier tree at its canonical empty-genesis state so that production genesis roots are unchanged. When non-empty, -- * the leaves must be unique and strictly increasing in field value (the native world state enforces this before -- * construction). Test networks pass a non-empty list to seed e.g. standard-contract registration nullifiers. -+ * Nullifiers to pre-insert into the genesis nullifier tree. Must be unique and strictly increasing in field value -+ * (the native world state enforces this before construction). Production callers pass `DEFAULT_GENESIS_DATA` from -+ * `@aztec-labs/protocol-contracts`, whose list is the canonical protocol contract registration nullifiers that the -+ * `GENESIS_ARCHIVE_ROOT` and `GENESIS_BLOCK_HEADER_HASH` constants are derived from; this cannot be defaulted here -+ * because `@aztec-labs/stdlib` does not depend on `@aztec-labs/protocol-contracts`. Test networks add e.g. the -+ * standard-contract registration nullifiers on top. Pass an explicit empty array only for low-level tree tests that -+ * want a truly empty nullifier tree. - */ -- prefilledNullifiers?: Fr[]; -+ prefilledNullifiers: Fr[]; - /** Timestamp for the genesis block header. Defaults to 0 (canonical empty genesis) in production. */ - genesisTimestamp: bigint; - }; - --/** An empty genesis data with no prefilled state and a zero timestamp. */ -+/** -+ * An empty genesis data with no prefilled state and a zero timestamp. Its nullifier tree is empty, so the resulting -+ * genesis roots do NOT match the canonical production roots; use `DEFAULT_GENESIS_DATA` from -+ * `@aztec-labs/protocol-contracts` for anything that has to agree with a deployed rollup. -+ */ - export const EMPTY_GENESIS_DATA: GenesisData = { - prefilledPublicData: [], - prefilledNullifiers: [], -@@ -31,6 +38,8 @@ export function isGenesisData(obj: any): obj is GenesisData { - typeof obj === 'object' && - 'prefilledPublicData' in obj && - Array.isArray(obj.prefilledPublicData) && -+ 'prefilledNullifiers' in obj && -+ Array.isArray(obj.prefilledNullifiers) && - 'genesisTimestamp' in obj && - typeof obj.genesisTimestamp === 'bigint' - ); -diff --git a/yarn-project/world-state/src/native/ipc_world_state_instance.ts b/yarn-project/world-state/src/native/ipc_world_state_instance.ts -index 74fc7584c696070ddb63c56250378ea18e78fb2f..6cd36e2816616de6d9014b2adf224acfab12f14e 100644 ---- a/yarn-project/world-state/src/native/ipc_world_state_instance.ts -+++ b/yarn-project/world-state/src/native/ipc_world_state_instance.ts -@@ -207,7 +207,7 @@ function getWsdbExtraArgs( - args.push('--prefilled-public-data', JSON.stringify(pairs)); - } - -- const prefilledNullifiers = genesis.prefilledNullifiers ?? []; -+ const prefilledNullifiers = genesis.prefilledNullifiers; - for (let i = 1; i < prefilledNullifiers.length; i++) { - if (prefilledNullifiers[i].toBigInt() <= prefilledNullifiers[i - 1].toBigInt()) { - throw new Error('Prefilled genesis nullifiers must be unique and strictly increasing'); -diff --git a/yarn-project/world-state/src/native/native_world_state.test.ts b/yarn-project/world-state/src/native/native_world_state.test.ts -index a60a3fd7af2acef898ec527762b8cc6997b5c351..d17ba712b4e4c0604496112d293ca05778779fcc 100644 ---- a/yarn-project/world-state/src/native/native_world_state.test.ts -+++ b/yarn-project/world-state/src/native/native_world_state.test.ts -@@ -16,6 +16,7 @@ import { Fr } from '@aztec-labs/foundation/curves/bn254'; - import { EthAddress } from '@aztec-labs/foundation/eth-address'; - import { retryUntil } from '@aztec-labs/foundation/retry'; - import type { SiblingPath } from '@aztec-labs/foundation/trees'; -+import { DEFAULT_GENESIS_DATA } from '@aztec-labs/protocol-contracts'; - import { PublicDataWrite } from '@aztec-labs/stdlib/avm'; - import { L2Block } from '@aztec-labs/stdlib/block'; - import { DatabaseVersionManager } from '@aztec-labs/stdlib/database-version/manager'; -@@ -1567,8 +1568,10 @@ describe('NativeWorldState', () => { - const ws = await NativeWorldStateService.new(EthAddress.random(), dataDir, wsTreeMapSizes); - const { state: initialState, ...initialRest } = ws.getInitialHeader(); - -- // With prefilled. -+ // With prefilled. Spread DEFAULT_GENESIS_DATA so the nullifier tree matches the default-genesis baseline above -+ // (which seeds the canonical protocol contract registration nullifiers); only the public data differs. - const genesis: GenesisData = { -+ ...DEFAULT_GENESIS_DATA, - prefilledPublicData: [ - new PublicDataTreeLeaf(new Fr(1000), new Fr(2000)), - new PublicDataTreeLeaf(new Fr(3000), new Fr(4000)), -diff --git a/yarn-project/world-state/src/native/native_world_state.ts b/yarn-project/world-state/src/native/native_world_state.ts -index e99c6fa0566c031c22338935b7f76d1bc4fedb59..a308cf3dc2e0de04a8451e3c169e8e78a05ace23 100644 ---- a/yarn-project/world-state/src/native/native_world_state.ts -+++ b/yarn-project/world-state/src/native/native_world_state.ts -@@ -4,6 +4,7 @@ import { fromEntries, padArrayEnd } from '@aztec-labs/foundation/collection'; - import { Fr } from '@aztec-labs/foundation/curves/bn254'; - import { EthAddress } from '@aztec-labs/foundation/eth-address'; - import { type Logger, type LoggerBindings, createLogger } from '@aztec-labs/foundation/log'; -+import { DEFAULT_GENESIS_DATA } from '@aztec-labs/protocol-contracts'; - import type { L2Block } from '@aztec-labs/stdlib/block'; - import { DatabaseVersionManager } from '@aztec-labs/stdlib/database-version/manager'; - import type { -@@ -14,7 +15,7 @@ import type { - import type { SnapshotDataKeys } from '@aztec-labs/stdlib/snapshots'; - import { MerkleTreeId, NullifierLeaf, type NullifierLeafPreimage, PublicDataTreeLeaf } from '@aztec-labs/stdlib/trees'; - import { BlockHeader, GlobalVariables, PartialStateReference, StateReference } from '@aztec-labs/stdlib/tx'; --import { EMPTY_GENESIS_DATA, type GenesisData, WorldStateRevision } from '@aztec-labs/stdlib/world-state'; -+import { type GenesisData, WorldStateRevision } from '@aztec-labs/stdlib/world-state'; - import { getTelemetryClient } from '@aztec-labs/telemetry-client'; - import assert from 'assert/strict'; - import { mkdir, mkdtemp, rm } from 'fs/promises'; -@@ -51,7 +52,7 @@ export class NativeWorldStateService implements MerkleTreeDatabase { - protected instance: NativeWorldStateInstance, - protected readonly worldStateInstrumentation: WorldStateInstrumentation, - protected readonly log: Logger, -- private readonly genesis: GenesisData = EMPTY_GENESIS_DATA, -+ private readonly genesis: GenesisData = DEFAULT_GENESIS_DATA, - private readonly cleanup = () => Promise.resolve(), - /** Factory to recreate a fresh IpcWorldState after clear(). */ - private readonly recreateInstance?: () => Promise, -@@ -61,7 +62,7 @@ export class NativeWorldStateService implements MerkleTreeDatabase { - rollupAddress: EthAddress, - dataDir: string, - wsTreeMapSizes: WorldStateTreeMapSizes, -- genesis: GenesisData = EMPTY_GENESIS_DATA, -+ genesis: GenesisData = DEFAULT_GENESIS_DATA, - instrumentation = new WorldStateInstrumentation(getTelemetryClient()), - bindings?: LoggerBindings, - cleanup = () => Promise.resolve(), -@@ -103,7 +104,7 @@ export class NativeWorldStateService implements MerkleTreeDatabase { - - static async tmp( - cleanupTmpDir = true, -- genesis: GenesisData = EMPTY_GENESIS_DATA, -+ genesis: GenesisData = DEFAULT_GENESIS_DATA, - instrumentation = new WorldStateInstrumentation(getTelemetryClient()), - bindings?: LoggerBindings, - threads?: number, -@@ -159,7 +160,7 @@ export class NativeWorldStateService implements MerkleTreeDatabase { - } - - static ephemeral( -- genesis: GenesisData = EMPTY_GENESIS_DATA, -+ genesis: GenesisData = DEFAULT_GENESIS_DATA, - instrumentation = new WorldStateInstrumentation(getTelemetryClient()), - bindings?: LoggerBindings, - ): Promise { -@@ -174,7 +175,7 @@ export class NativeWorldStateService implements MerkleTreeDatabase { - wsdbBackend: ConstructorParameters[0], - instrumentation = new WorldStateInstrumentation(getTelemetryClient()), - bindings?: LoggerBindings, -- genesis: GenesisData = EMPTY_GENESIS_DATA, -+ genesis: GenesisData = DEFAULT_GENESIS_DATA, - cleanup = () => Promise.resolve(), - recreateInstance?: () => Promise, - ): Promise { -diff --git a/yarn-project/world-state/src/synchronizer/factory.ts b/yarn-project/world-state/src/synchronizer/factory.ts -index 1e3b4cd65d10a10e7ebc5274220024acb546baba..d84fea9ac057d2c5f055b228ed040dcabf98e520 100644 ---- a/yarn-project/world-state/src/synchronizer/factory.ts -+++ b/yarn-project/world-state/src/synchronizer/factory.ts -@@ -1,8 +1,9 @@ - import type { LoggerBindings } from '@aztec-labs/foundation/log'; -+import { DEFAULT_GENESIS_DATA } from '@aztec-labs/protocol-contracts'; - import type { L2BlockSource } from '@aztec-labs/stdlib/block'; - import type { DataStoreConfig } from '@aztec-labs/stdlib/kv-store'; - import type { L1ToL2MessageSource } from '@aztec-labs/stdlib/messaging'; --import { EMPTY_GENESIS_DATA, type GenesisData, isGenesisData } from '@aztec-labs/stdlib/world-state'; -+import { type GenesisData, isGenesisData } from '@aztec-labs/stdlib/world-state'; - import { type TelemetryClient, getTelemetryClient } from '@aztec-labs/telemetry-client'; - - import { WorldStateInstrumentation } from '../instrumentation/instrumentation.js'; -@@ -44,7 +45,7 @@ export async function createWorldState( - | 'publicDataTreeMapSizeKb' - > & - Pick, -- genesis: GenesisData = EMPTY_GENESIS_DATA, -+ genesis: GenesisData = DEFAULT_GENESIS_DATA, - instrumentation: WorldStateInstrumentation = new WorldStateInstrumentation(getTelemetryClient()), - bindings?: LoggerBindings, - ) { -diff --git a/yarn-project/world-state/src/testing.test.ts b/yarn-project/world-state/src/testing.test.ts -index da1d2cf779eba93995fdeb513a31e9bc85242ce7..55db72edd6ab2bb59b091d4245e057c5075f5cb8 100644 ---- a/yarn-project/world-state/src/testing.test.ts -+++ b/yarn-project/world-state/src/testing.test.ts -@@ -1,5 +1,6 @@ - import { GENESIS_ARCHIVE_ROOT } from '@aztec-labs/constants'; - import { Fr } from '@aztec-labs/foundation/curves/bn254'; -+import { DEFAULT_GENESIS_DATA, ProtocolContractGenesisNullifiers } from '@aztec-labs/protocol-contracts'; - import { MerkleTreeId, PublicDataTreeLeaf } from '@aztec-labs/stdlib/trees'; - import { EMPTY_GENESIS_DATA, type GenesisData } from '@aztec-labs/stdlib/world-state'; - import { jest } from '@jest/globals'; -@@ -13,10 +14,8 @@ const archiveRoot = async (ws: NativeWorldStateService) => - new Fr((await ws.getCommitted().getTreeInfo(MerkleTreeId.ARCHIVE)).root); - - describe('generateGenesisValues world state backend equivalence', () => { -- // A genesis with both non-empty prefilled public data and a non-zero timestamp, so the -- // fast-return branch in generateGenesisValues is not taken and the archive root is computed -- // from an actual world state. - const genesis: GenesisData = { -+ ...DEFAULT_GENESIS_DATA, - prefilledPublicData: [ - new PublicDataTreeLeaf(new Fr(1000), new Fr(2000)), - new PublicDataTreeLeaf(new Fr(3000), new Fr(4000)), -@@ -41,76 +40,76 @@ describe('generateGenesisValues world state backend equivalence', () => { - }); - - describe('genesis prefilled nullifiers', () => { -- // (a) With no public data, no timestamp and no prefilled nullifiers, generateGenesisValues takes its fast path and -- // returns the pinned GENESIS_ARCHIVE_ROOT constant without spinning up a world state. -- it('empty genesis returns the canonical GENESIS_ARCHIVE_ROOT via the fast path', async () => { -- const { genesisArchiveRoot } = await getGenesisValues([]); -- expect(genesisArchiveRoot).toEqual(new Fr(GENESIS_ARCHIVE_ROOT)); -- }); -+ const nullifierIndices = (ws: NativeWorldStateService, nullifiers: Fr[]) => -+ ws.getCommitted().findLeafIndices( -+ MerkleTreeId.NULLIFIER_TREE, -+ nullifiers.map(n => n.toBuffer()), -+ ); - -- // (a) The fast-path constant must equal the root actually computed from an empty-nullifier genesis, i.e. the -- // GENESIS_ARCHIVE_ROOT constant is correct for the default (empty) prefilled-nullifiers list on this branch. -- it('the empty-genesis archive root matches a freshly computed empty world state', async () => { -- const ws = await NativeWorldStateService.ephemeral(EMPTY_GENESIS_DATA); -+ it('the default world state seeds every protocol contract registration nullifier', async () => { -+ const ws = await NativeWorldStateService.ephemeral(); - try { -- expect(await archiveRoot(ws)).toEqual(new Fr(GENESIS_ARCHIVE_ROOT)); -+ const indices = await nullifierIndices(ws, ProtocolContractGenesisNullifiers); -+ expect(ProtocolContractGenesisNullifiers.length).toBeGreaterThan(0); -+ expect(indices.every(index => index !== undefined)).toBe(true); - } finally { - await ws.close(); - } - }); - -- // (b) An explicit empty prefilled-nullifiers list must be behaviour-neutral: threading it through the non-fast path -- // (public data present) yields exactly the same root as a genesis that omits the field entirely. -- it('an explicit empty prefilledNullifiers produces the same root as omitting the field', async () => { -- const publicData = [new PublicDataTreeLeaf(new Fr(1000), new Fr(2000))]; -- const withoutField: GenesisData = { prefilledPublicData: publicData, genesisTimestamp: 42n }; -- const withEmpty: GenesisData = { prefilledPublicData: publicData, genesisTimestamp: 42n, prefilledNullifiers: [] }; -- const wsWithout = await NativeWorldStateService.ephemeral(withoutField); -- const wsEmpty = await NativeWorldStateService.ephemeral(withEmpty); -+ it('the default genesis archive root matches the pinned GENESIS_ARCHIVE_ROOT constant', async () => { -+ const ws = await NativeWorldStateService.ephemeral(DEFAULT_GENESIS_DATA); - try { -- expect(await archiveRoot(wsEmpty)).toEqual(await archiveRoot(wsWithout)); -+ expect(await archiveRoot(ws)).toEqual(new Fr(GENESIS_ARCHIVE_ROOT)); - } finally { -- await wsWithout.close(); -- await wsEmpty.close(); -+ await ws.close(); - } -+ const { genesisArchiveRoot } = await getGenesisValues([]); -+ expect(genesisArchiveRoot).toEqual(new Fr(GENESIS_ARCHIVE_ROOT)); - }); - -- // (c) A non-empty, sorted, unique nullifier list produces a deterministic root that differs from the empty genesis. -- it('a non-empty prefilledNullifiers list yields a deterministic root that differs from the empty genesis', async () => { -- // Values must exceed the padding leaves that fill the initial prefill region and be strictly increasing. -- const nullifiers = [new Fr(1000n), new Fr(2000n), new Fr(3000n)]; -- const genesisWith: GenesisData = { prefilledPublicData: [], genesisTimestamp: 0n, prefilledNullifiers: nullifiers }; -- const genesisEmpty: GenesisData = { prefilledPublicData: [], genesisTimestamp: 0n, prefilledNullifiers: [] }; -- const wsWith1 = await NativeWorldStateService.ephemeral(genesisWith); -- const wsWith2 = await NativeWorldStateService.ephemeral(genesisWith); -- const wsEmpty = await NativeWorldStateService.ephemeral(genesisEmpty); -+ it('an explicitly empty genesis seeds no protocol nullifiers and yields a different root', async () => { -+ const ws = await NativeWorldStateService.ephemeral(EMPTY_GENESIS_DATA); - try { -- const rootWith1 = await archiveRoot(wsWith1); -- const rootWith2 = await archiveRoot(wsWith2); -- const rootEmpty = await archiveRoot(wsEmpty); -- expect(rootWith1).toEqual(rootWith2); -- expect(rootWith1).not.toEqual(rootEmpty); -+ const indices = await nullifierIndices(ws, ProtocolContractGenesisNullifiers); -+ expect(indices.every(index => index === undefined)).toBe(true); -+ expect(await archiveRoot(ws)).not.toEqual(new Fr(GENESIS_ARCHIVE_ROOT)); - } finally { -- await wsWith1.close(); -- await wsWith2.close(); -- await wsEmpty.close(); -+ await ws.close(); - } - }); - -- // (c) getGenesisValues sorts an unsorted list ascending and produces a genesis whose root matches direct construction. -- it('getGenesisValues sorts prefilled nullifiers and seeds them into the genesis', async () => { -- const unsorted = [new Fr(3000n), new Fr(1000n), new Fr(2000n)]; -- const { genesis, genesisArchiveRoot } = await getGenesisValues([], undefined, [], 0n, unsorted); -- expect(genesis.prefilledNullifiers).toEqual([new Fr(1000n), new Fr(2000n), new Fr(3000n)]); -+ it('getGenesisValues seeds the protocol baseline plus the additional nullifiers, sorted', async () => { -+ // Values must exceed the padding leaves that fill the initial prefill region. -+ const additional = [new Fr(3000n), new Fr(1000n), new Fr(2000n)]; -+ const additionalCopy = [...additional]; -+ const { genesis, genesisArchiveRoot } = await getGenesisValues([], undefined, [], 0n, additional); -+ -+ expect(additional).toEqual(additionalCopy); -+ expect(genesis.prefilledNullifiers).toEqual( -+ [...ProtocolContractGenesisNullifiers, ...additional].sort((a, b) => (a.toBigInt() < b.toBigInt() ? -1 : 1)), -+ ); -+ - const ws = await NativeWorldStateService.ephemeral(genesis); - try { - expect(await archiveRoot(ws)).toEqual(genesisArchiveRoot); -+ const indices = await nullifierIndices(ws, [...ProtocolContractGenesisNullifiers, ...additional]); -+ expect(indices.every(index => index !== undefined)).toBe(true); - } finally { - await ws.close(); - } - }); - -- // (d) The defensive TS-side check rejects prefilled nullifiers that are not unique and strictly increasing before -+ it('getGenesisValues rejects additional nullifiers that duplicate the protocol baseline', async () => { -+ await expect(getGenesisValues([], undefined, [], 0n, [ProtocolContractGenesisNullifiers[0]])).rejects.toThrow( -+ 'Duplicate genesis nullifier', -+ ); -+ await expect(getGenesisValues([], undefined, [], 0n, [new Fr(1000n), new Fr(1000n)])).rejects.toThrow( -+ 'Duplicate genesis nullifier', -+ ); -+ }); -+ -+ // The defensive TS-side check rejects prefilled nullifiers that are not unique and strictly increasing before - // handing them to the native tree. - it('rejects prefilled nullifiers that are not strictly increasing', async () => { - const descending: GenesisData = { -diff --git a/yarn-project/world-state/src/testing.ts b/yarn-project/world-state/src/testing.ts -index bd04b32845f14bfe5f5b384a51d3e5f821420dfd..5cd9a352e684c11b1004a3dea76ae971e10ee267 100644 ---- a/yarn-project/world-state/src/testing.ts -+++ b/yarn-project/world-state/src/testing.ts -@@ -1,5 +1,5 @@ --import { GENESIS_ARCHIVE_ROOT } from '@aztec-labs/constants'; - import { Fr } from '@aztec-labs/foundation/curves/bn254'; -+import { DEFAULT_GENESIS_DATA } from '@aztec-labs/protocol-contracts'; - import { computeFeePayerBalanceLeafSlot } from '@aztec-labs/protocol-contracts/fee-juice'; - import type { AztecAddress } from '@aztec-labs/stdlib/aztec-address'; - import { MerkleTreeId, PublicDataTreeLeaf } from '@aztec-labs/stdlib/trees'; -@@ -8,35 +8,37 @@ import type { GenesisData } from '@aztec-labs/stdlib/world-state'; - import { NativeWorldStateService } from './native/index.js'; - - async function generateGenesisValues(genesis: GenesisData) { -- // The GENESIS_ARCHIVE_ROOT constant reflects the canonical empty genesis (no public data, no prefilled nullifiers, -- // timestamp 0), so we can only short-circuit when this genesis adds none of those on top. -- if (!genesis.prefilledPublicData.length && genesis.genesisTimestamp === 0n && !genesis.prefilledNullifiers?.length) { -- return { -- genesisArchiveRoot: new Fr(GENESIS_ARCHIVE_ROOT), -- }; -- } -- - // Compute the genesis values on a throwaway world state. The archive root derives deterministically from the - // prefilled public data, the prefilled nullifiers, and the genesis timestamp, so the fsync-off ephemeral store (no - // version manager, no crash-recoverability) produces an identical root while skipping the fsync overhead that `tmp` - // pays. close() removes the tmpdir. - const ws = await NativeWorldStateService.ephemeral(genesis); -- const genesisArchiveRoot = new Fr((await ws.getCommitted().getTreeInfo(MerkleTreeId.ARCHIVE)).root); -- await ws.close(); -- -- return { -- genesisArchiveRoot, -- }; -+ try { -+ return { -+ genesisArchiveRoot: new Fr((await ws.getCommitted().getTreeInfo(MerkleTreeId.ARCHIVE)).root), -+ }; -+ } finally { -+ await ws.close(); -+ } - } - - export const defaultInitialAccountFeeJuice = new Fr(10n ** 22n); - -+/** -+ * Builds the genesis data and the resulting genesis archive root for a deployment. -+ * -+ * @param additionalNullifiers - Nullifiers to seed *in addition to* the canonical protocol contract registration -+ * nullifiers that {@link DEFAULT_GENESIS_DATA} always contributes. Test networks pass e.g. the standard-contract -+ * registration nullifiers here. Passing an empty list still yields the canonical protocol baseline, and passing the -+ * full list of an already-resolved `GenesisData` is an error (duplicates are rejected rather than discarded) — such a -+ * caller should hand the resolved object straight to the world state instead. -+ */ - export async function getGenesisValues( - initialAccounts: AztecAddress[], - initialAccountFeeJuice = defaultInitialAccountFeeJuice, - genesisPublicData: PublicDataTreeLeaf[] = [], - genesisTimestamp: bigint = 0n, -- prefilledNullifiers: Fr[] = [], -+ additionalNullifiers: Fr[] = [], - ) { - // Top up the accounts with fee juice. - let prefilledPublicData = await Promise.all( -@@ -50,11 +52,21 @@ export async function getGenesisValues( - - prefilledPublicData.sort((a, b) => (b.slot.lt(a.slot) ? 1 : -1)); - -- // The indexed nullifier tree requires its prefilled leaves to be unique and strictly increasing, so sort ascending -- // here (a copy, to avoid mutating the caller's array) rather than relying on the caller's ordering. -- const sortedNullifiers = [...prefilledNullifiers].sort((a, b) => (a.toBigInt() < b.toBigInt() ? -1 : 1)); -+ // The indexed nullifier tree requires its prefilled leaves to be unique and strictly increasing, so sort a copy -+ // ascending rather than relying on the caller's ordering. -+ const prefilledNullifiers = [...DEFAULT_GENESIS_DATA.prefilledNullifiers, ...additionalNullifiers].sort((a, b) => -+ a.toBigInt() < b.toBigInt() ? -1 : 1, -+ ); -+ for (let i = 1; i < prefilledNullifiers.length; i++) { -+ if (prefilledNullifiers[i].equals(prefilledNullifiers[i - 1])) { -+ throw new Error( -+ `Duplicate genesis nullifier ${prefilledNullifiers[i].toString()}: the protocol contract registration ` + -+ `nullifiers are always seeded, so they must not be passed again as additional nullifiers.`, -+ ); -+ } -+ } - -- const genesis: GenesisData = { prefilledPublicData, prefilledNullifiers: sortedNullifiers, genesisTimestamp }; -+ const genesis: GenesisData = { prefilledPublicData, prefilledNullifiers, genesisTimestamp }; - const { genesisArchiveRoot } = await generateGenesisValues(genesis); - - return { diff --git a/labs-patches/0010-feat-world-state-expose-the-canonical-genesis-measur.patch b/labs-patches/0010-feat-world-state-expose-the-canonical-genesis-measur.patch deleted file mode 100644 index 86abfc83e86b..000000000000 --- a/labs-patches/0010-feat-world-state-expose-the-canonical-genesis-measur.patch +++ /dev/null @@ -1,122 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Santiago Palladino -Date: Thu, 17 Sep 2026 14:37:34 -0300 -Subject: [PATCH] feat(world-state): expose the canonical genesis measurement - -`measureCanonicalGenesis` returns the seeded nullifiers, the genesis -nullifier-tree root, the block-header hash and the archive root from one -ephemeral world state, so the foundation's regeneration script and the -`compute-genesis-values` CLI read the same numbers rather than each -deriving their own. - -The l2_block genesis test now reads GENESIS_NULLIFIER_TREE_ROOT from the -generated constants instead of repeating the literal, so a regeneration -no longer has to reach into the patch series to update it. - -diff --git a/yarn-project/cli/src/cmds/l1/compute_genesis_values.ts b/yarn-project/cli/src/cmds/l1/compute_genesis_values.ts -index 10da6b1a717c14ab30ce6e721259d67d61a176c2..f6ca0913d128ffaa4e7cb7c29b14bfd0a3ea4a0c 100644 ---- a/yarn-project/cli/src/cmds/l1/compute_genesis_values.ts -+++ b/yarn-project/cli/src/cmds/l1/compute_genesis_values.ts -@@ -1,16 +1,23 @@ - import { getInitialTestAccountsData } from '@aztec-labs/accounts/testing'; - import type { LogFn } from '@aztec-labs/foundation/log'; - import { protocolContractsHash } from '@aztec-labs/protocol-contracts'; --import { getGenesisValues } from '@aztec-labs/world-state/testing'; -+import { getGenesisValues, measureCanonicalGenesis } from '@aztec-labs/world-state/testing'; - - import { getSponsoredFPCAddress } from '../../utils/setup_contracts.js'; - --/** Computes and prints genesis values needed for L1 contract deployment. */ -+/** -+ * Computes and prints genesis values needed for L1 contract deployment. -+ * -+ * `genesisArchiveRoot` is this deployment's root, which depends on the accounts it prefunds. The `canonical` block is -+ * the prefunding-free genesis that the `GENESIS_*` protocol constants record, and is what -+ * `noir-projects/fnd/scripts/regenerate_genesis_constants.sh` reads to refresh them. -+ */ - export async function computeGenesisValuesCmd(testAccounts: boolean, sponsoredFPC: boolean, log: LogFn) { - const initialAccounts = testAccounts ? await getInitialTestAccountsData() : []; - const sponsoredFPCAddresses = sponsoredFPC ? await getSponsoredFPCAddress() : []; - const initialFundedAccounts = initialAccounts.map(a => a.address).concat(sponsoredFPCAddresses); - const { genesisArchiveRoot } = await getGenesisValues(initialFundedAccounts); -+ const canonical = await measureCanonicalGenesis(); - - const { getVKTreeRoot } = await import('@aztec-labs/noir-protocol-circuits-types/vk-tree'); - const vkTreeRoot = getVKTreeRoot(); -@@ -21,6 +28,12 @@ export async function computeGenesisValuesCmd(testAccounts: boolean, sponsoredFP - vkTreeRoot: vkTreeRoot.toString(), - protocolContractsHash: protocolContractsHash.toString(), - genesisArchiveRoot: genesisArchiveRoot.toString(), -+ canonical: { -+ prefilledNullifiers: canonical.prefilledNullifiers.map(n => n.toString()), -+ nullifierTreeRoot: canonical.nullifierTreeRoot.toString(), -+ blockHeaderHash: canonical.blockHeaderHash.toString(), -+ archiveRoot: canonical.archiveRoot.toString(), -+ }, - }, - null, - 2, -diff --git a/yarn-project/stdlib/src/block/l2_block.test.ts b/yarn-project/stdlib/src/block/l2_block.test.ts -index c21982836752c3bce482ac3041ce644c29565b48..c140359a0e44de41431d5bc943037ba9f0d6ca72 100644 ---- a/yarn-project/stdlib/src/block/l2_block.test.ts -+++ b/yarn-project/stdlib/src/block/l2_block.test.ts -@@ -1,3 +1,4 @@ -+import { GENESIS_NULLIFIER_TREE_ROOT } from '@aztec-labs/constants'; - import { BlockNumber } from '@aztec-labs/foundation/branded-types'; - import { Fr } from '@aztec-labs/foundation/curves/bn254'; - import { jsonStringify } from '@aztec-labs/foundation/json-rpc'; -@@ -23,7 +24,7 @@ describe('L2Block', () => { - }); - - it('can create an initial block', async () => { -- // Values taken from world_state.test.cpp WorldStateTest.GetInitialTreeInfoForAllTrees -+ // The non-nullifier roots are those of an empty world state; only the nullifier tree is seeded at genesis. - const emptyBlockHeader = BlockHeader.empty(); - emptyBlockHeader.state.l1ToL2MessageTree.root = Fr.fromString( - '0x0fef6d80d31109ddb56d6b3f607cbc9c0af0bff3ea0d43e8f278983c64c11f7a', -@@ -31,9 +32,7 @@ describe('L2Block', () => { - emptyBlockHeader.state.partial.noteHashTree.root = Fr.fromString( - '0x2590f2aab19dd791700b4a43d3f52bb88ef2409a3731da8e848663559202e4c6', - ); -- emptyBlockHeader.state.partial.nullifierTree.root = Fr.fromString( -- '0x21a19fe6f636fb24d9f63edb7b807613492cc0001c91e531917a2539f57e2ba8', -- ); -+ emptyBlockHeader.state.partial.nullifierTree.root = new Fr(GENESIS_NULLIFIER_TREE_ROOT); - emptyBlockHeader.state.partial.nullifierTree.nextAvailableLeafIndex = 128; - emptyBlockHeader.state.partial.publicDataTree.root = Fr.fromString( - '0x1bef38b621017d3c7416663d0cd81369424560710526a3fbaaec13e356b9d084', -diff --git a/yarn-project/world-state/src/testing.ts b/yarn-project/world-state/src/testing.ts -index 5cd9a352e684c11b1004a3dea76ae971e10ee267..be193f30666200500d199805ccd86c759aa93666 100644 ---- a/yarn-project/world-state/src/testing.ts -+++ b/yarn-project/world-state/src/testing.ts -@@ -22,6 +22,30 @@ async function generateGenesisValues(genesis: GenesisData) { - } - } - -+/** -+ * Measures every value the canonical genesis pins, from one world state. -+ * -+ * Canonical means {@link DEFAULT_GENESIS_DATA} exactly: the protocol contracts' registration nullifiers, no prefunded -+ * public data and timestamp 0. That is what `GENESIS_NULLIFIER_TREE_ROOT`, `GENESIS_BLOCK_HEADER_HASH` and -+ * `GENESIS_ARCHIVE_ROOT` in `constants.nr` record, so this is the measurement -+ * `noir-projects/fnd/scripts/regenerate_genesis_constants.sh` writes back into them. A deployment that prefunds -+ * accounts gets a different archive root and must use {@link getGenesisValues} instead. -+ */ -+export async function measureCanonicalGenesis() { -+ const ws = await NativeWorldStateService.ephemeral(DEFAULT_GENESIS_DATA); -+ try { -+ const committed = ws.getCommitted(); -+ return { -+ prefilledNullifiers: DEFAULT_GENESIS_DATA.prefilledNullifiers, -+ nullifierTreeRoot: new Fr((await committed.getTreeInfo(MerkleTreeId.NULLIFIER_TREE)).root), -+ blockHeaderHash: await ws.getInitialHeader().hash(), -+ archiveRoot: new Fr((await committed.getTreeInfo(MerkleTreeId.ARCHIVE)).root), -+ }; -+ } finally { -+ await ws.close(); -+ } -+} -+ - export const defaultInitialAccountFeeJuice = new Fr(10n ** 22n); - - /** diff --git a/labs-patches/0016-test-refresh-compile_failure-snapshots-for-the-rc.2-.patch b/labs-patches/0016-test-refresh-compile_failure-snapshots-for-the-rc.2-.patch deleted file mode 100644 index e8d3501d856f..000000000000 --- a/labs-patches/0016-test-refresh-compile_failure-snapshots-for-the-rc.2-.patch +++ /dev/null @@ -1,131 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: AztecBot -Date: Wed, 16 Sep 2026 18:54:22 +0000 -Subject: [PATCH] test: refresh compile_failure snapshots for the rc.2 - associated-item diagnostic - -noir-lang/noir#13666 replaces "Could not resolve 'X' in path" with -"no associated item named \`X\` found for \`T\`" plus a secondary label on -the path segment. Six compile_failure stderr snapshots capture that -diagnostic; the assertions themselves are unchanged. - -diff --git a/noir-projects/contract-snapshots/tests/snapshots/compile_failure/panic_on_incorrectly_performed_private_call/snapshots__stderr.snap b/noir-projects/contract-snapshots/tests/snapshots/compile_failure/panic_on_incorrectly_performed_private_call/snapshots__stderr.snap -index de1c268866c5bf74027273d894f9de3660391ede..4df93ed799613e2a7afe89a4f769f63334dcdedb 100644 ---- a/noir-projects/contract-snapshots/tests/snapshots/compile_failure/panic_on_incorrectly_performed_private_call/snapshots__stderr.snap -+++ b/noir-projects/contract-snapshots/tests/snapshots/compile_failure/panic_on_incorrectly_performed_private_call/snapshots__stderr.snap -@@ -2,14 +2,14 @@ - source: tests/snapshots.rs - expression: stderr - --- --error: Could not resolve 'ZERO' in path -+error: no associated item named `ZERO` found for `AztecAddress` - ┌─ src/main.nr:3:1 - │ - 3 │ #[aztec] - │ -------- While running this function attribute - · - 9 │ PanicOnIncorrectlyPerformedPrivateCall::at(AztecAddress::ZERO).arbitrary_external_function(); -- │ ---- -+ │ ---- a path can only name an associated type, constant or function of `AztecAddress` - │ - - error: Your private call needs to be passed into the `self.call(...)` method to be executed (e.g. `self.call(MyContract::at(address).my_private_function(...args))` -diff --git a/noir-projects/contract-snapshots/tests/snapshots/compile_failure/panic_on_incorrectly_performed_private_static_call/snapshots__stderr.snap b/noir-projects/contract-snapshots/tests/snapshots/compile_failure/panic_on_incorrectly_performed_private_static_call/snapshots__stderr.snap -index 7bd6bfee98f3185185322df150ed1f70ca4570a2..6806ee35d9344176c951671ffdc2834d91577471 100644 ---- a/noir-projects/contract-snapshots/tests/snapshots/compile_failure/panic_on_incorrectly_performed_private_static_call/snapshots__stderr.snap -+++ b/noir-projects/contract-snapshots/tests/snapshots/compile_failure/panic_on_incorrectly_performed_private_static_call/snapshots__stderr.snap -@@ -2,14 +2,14 @@ - source: tests/snapshots.rs - expression: stderr - --- --error: Could not resolve 'ZERO' in path -+error: no associated item named `ZERO` found for `AztecAddress` - ┌─ src/main.nr:3:1 - │ - 3 │ #[aztec] - │ -------- While running this function attribute - · - 18 │ PanicOnIncorrectlyPerformedPrivateStaticCall::at(AztecAddress::ZERO) -- │ ---- -+ │ ---- a path can only name an associated type, constant or function of `AztecAddress` - │ - - error: Your private static call needs to be passed into the `self.view(...)` method to be executed (e.g. `self.view(MyContract::at(address).my_private_static_function(...args))` -diff --git a/noir-projects/contract-snapshots/tests/snapshots/compile_failure/panic_on_incorrectly_performed_public_call/snapshots__stderr.snap b/noir-projects/contract-snapshots/tests/snapshots/compile_failure/panic_on_incorrectly_performed_public_call/snapshots__stderr.snap -index 3361a44915c4167462c28865683a28f7b6f6920e..79bbb3a59b4b0620070fd7768927d2c8a3e9d649 100644 ---- a/noir-projects/contract-snapshots/tests/snapshots/compile_failure/panic_on_incorrectly_performed_public_call/snapshots__stderr.snap -+++ b/noir-projects/contract-snapshots/tests/snapshots/compile_failure/panic_on_incorrectly_performed_public_call/snapshots__stderr.snap -@@ -2,14 +2,14 @@ - source: tests/snapshots.rs - expression: stderr - --- --error: Could not resolve 'ZERO' in path -+error: no associated item named `ZERO` found for `AztecAddress` - ┌─ src/main.nr:3:1 - │ - 3 │ #[aztec] - │ -------- While running this function attribute - · - 12 │ PanicOnIncorrectlyPerformedPublicCall::at(AztecAddress::ZERO).arbitrary_public_function(); -- │ ---- -+ │ ---- a path can only name an associated type, constant or function of `AztecAddress` - │ - - error: Your public call needs to be passed into the `self.call(...)`, `self.enqueue(...)` or `self.enqueue_incognito(...)` method to be executed (e.g. `self.call(MyContract::at(address).my_public_function(...args))` -diff --git a/noir-projects/contract-snapshots/tests/snapshots/compile_failure/panic_on_incorrectly_performed_public_static_call/snapshots__stderr.snap b/noir-projects/contract-snapshots/tests/snapshots/compile_failure/panic_on_incorrectly_performed_public_static_call/snapshots__stderr.snap -index b426ebb70c1c16f4aa364af828cc940961a4387c..4e92d1c22b1b8cd2807938e8e5b892aa455d06b1 100644 ---- a/noir-projects/contract-snapshots/tests/snapshots/compile_failure/panic_on_incorrectly_performed_public_static_call/snapshots__stderr.snap -+++ b/noir-projects/contract-snapshots/tests/snapshots/compile_failure/panic_on_incorrectly_performed_public_static_call/snapshots__stderr.snap -@@ -2,14 +2,14 @@ - source: tests/snapshots.rs - expression: stderr - --- --error: Could not resolve 'ZERO' in path -+error: no associated item named `ZERO` found for `AztecAddress` - ┌─ src/main.nr:3:1 - │ - 3 │ #[aztec] - │ -------- While running this function attribute - · - 18 │ PanicOnIncorrectlyPerformedPublicStaticCall::at(AztecAddress::ZERO) -- │ ---- -+ │ ---- a path can only name an associated type, constant or function of `AztecAddress` - │ - - error: Your public static call needs to be passed into the `self.view(...)`, `self.enqueue_view(...)` or `self.enqueue_view_incognito(...)` method to be executed (e.g. `self.view(MyContract::at(address).my_public_static_function(...args))` -diff --git a/noir-projects/contract-snapshots/tests/snapshots/compile_failure/panic_on_non_state_var_in_storage/snapshots__stderr.snap b/noir-projects/contract-snapshots/tests/snapshots/compile_failure/panic_on_non_state_var_in_storage/snapshots__stderr.snap -index 57e8e6b2c8f6498116fe25c7d255af957fda4dc0..d9ed998f8e63d5166db0e08328851901a0df55d4 100644 ---- a/noir-projects/contract-snapshots/tests/snapshots/compile_failure/panic_on_non_state_var_in_storage/snapshots__stderr.snap -+++ b/noir-projects/contract-snapshots/tests/snapshots/compile_failure/panic_on_non_state_var_in_storage/snapshots__stderr.snap -@@ -14,11 +14,11 @@ error: Type NonStateVar does not implement StateVariable and hence cannot be pla - 2: storage - at /noir-projects/aztec-nr/aztec/src/macros/storage.nr:: - --error: Could not resolve 'init' in path -+error: no associated item named `init` found for `Storage<_>` - ┌─ /noir-projects/aztec-nr/aztec/src/macros/internals_functions_generation/external/private.nr:: - │ - │ let storage = Storage::init(&mut context); -- │ ---- -+ │ ---- a path can only name an associated type, constant or function of `Storage<_>` - │ - ┌─ src/main.nr:4:1 - │ -diff --git a/noir-projects/contract-snapshots/tests/snapshots/compile_failure/panic_on_owned_state_var_in_storage/snapshots__stderr.snap b/noir-projects/contract-snapshots/tests/snapshots/compile_failure/panic_on_owned_state_var_in_storage/snapshots__stderr.snap -index dfdf6e7bf30dce77c6bcfc671ba71ca18fcc638c..a28ab7d1b215c22c88baeeaed608739459b4a597 100644 ---- a/noir-projects/contract-snapshots/tests/snapshots/compile_failure/panic_on_owned_state_var_in_storage/snapshots__stderr.snap -+++ b/noir-projects/contract-snapshots/tests/snapshots/compile_failure/panic_on_owned_state_var_in_storage/snapshots__stderr.snap -@@ -14,11 +14,11 @@ error: Type PrivateImmutable implements OwnedStateVariable and h - 2: storage - at /noir-projects/aztec-nr/aztec/src/macros/storage.nr:: - --error: Could not resolve 'init' in path -+error: no associated item named `init` found for `Storage<_>` - ┌─ /noir-projects/aztec-nr/aztec/src/macros/internals_functions_generation/external/private.nr:: - │ - │ let storage = Storage::init(&mut context); -- │ ---- -+ │ ---- a path can only name an associated type, constant or function of `Storage<_>` - │ - ┌─ src/main.nr:4:1 - │ diff --git a/labs-patches/0017-feat-stdlib-size-the-fee-model-s-L1-gas-constants-fo.patch b/labs-patches/0017-feat-stdlib-size-the-fee-model-s-L1-gas-constants-fo.patch deleted file mode 100644 index 60458976fa73..000000000000 --- a/labs-patches/0017-feat-stdlib-size-the-fee-model-s-L1-gas-constants-fo.patch +++ /dev/null @@ -1,61 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Santiago Palladino -Date: Thu, 17 Sep 2026 18:35:07 -0300 -Subject: [PATCH] feat(stdlib)!: size the fee model's L1 gas constants for - Glamsterdam - -`L1_GAS_PER_CHECKPOINT_PROPOSED` and `L1_GAS_PER_EPOCH_VERIFIED` are the -TypeScript port of the constants in `FeeLib.sol`, used to predict the minimum -fee per mana a few slots ahead. Follow the L1 change that resizes them for the -Glamsterdam gas schedule (EIP-8037 state gas, EIP-8038 state access repricing): -300k -> 500k and 3.6M -> 4M. A stale port would predict fees that no longer -match what the rollup charges. - -Both values are placeholders on the L1 side too: they come from the -`BenchmarkRollupTest` gas report run under `forge --evm-version amsterdam`, -which uses a mock proof verifier and an empty proof, and will be replaced once -the fork is live and the costs can be measured on testnet. - -diff --git a/yarn-project/end-to-end/src/single-node/fees/fees_test.ts b/yarn-project/end-to-end/src/single-node/fees/fees_test.ts -index c1713e320beb40c957579d0707d3d750adb380bb..0392920433bb3ef8db468ee94b6a6466a4389ed3 100644 ---- a/yarn-project/end-to-end/src/single-node/fees/fees_test.ts -+++ b/yarn-project/end-to-end/src/single-node/fees/fees_test.ts -@@ -350,7 +350,7 @@ export class FeesTest extends SingleNodeTestContext { - - // @todo @lherskind As we deal with #13601 - // Right now the value is from `FeeLib.sol` -- const L1_GAS_PER_EPOCH_VERIFIED = 3600000n; -+ const L1_GAS_PER_EPOCH_VERIFIED = 4000000n; - - // We round up - const mulDiv = (a: bigint, b: bigint, c: bigint) => (a * b) / c + ((a * b) % c > 0n ? 1n : 0n); -diff --git a/yarn-project/stdlib/src/gas/README.md b/yarn-project/stdlib/src/gas/README.md -index 427fed74dbb37f87156db6bae35965f6b0053700..283e45e2a4628b43393ba477ec0f781fdf8a62ec 100644 ---- a/yarn-project/stdlib/src/gas/README.md -+++ b/yarn-project/stdlib/src/gas/README.md -@@ -208,8 +208,8 @@ itself, before actual teardown consumption is known. - - | Constant | Value | - | ------------------------------- | -------------- | --| `L1_GAS_PER_CHECKPOINT_PROPOSED` | 300,000 | --| `L1_GAS_PER_EPOCH_VERIFIED` | 3,600,000 | -+| `L1_GAS_PER_CHECKPOINT_PROPOSED` | 500,000 | -+| `L1_GAS_PER_EPOCH_VERIFIED` | 4,000,000 | - | `BLOBS_PER_CHECKPOINT` (FeeLib) | 3 | - | `BLOB_GAS_PER_BLOB` | 2^17 | - | `MINIMUM_CONGESTION_MULTIPLIER` | 1e9 | -diff --git a/yarn-project/stdlib/src/gas/fee_math.ts b/yarn-project/stdlib/src/gas/fee_math.ts -index 6d1ffa8fa73dbbee67e9e9334a9ba2563f6b6e84..16ae8d035b13202e4965d433dccb18536b13cc18 100644 ---- a/yarn-project/stdlib/src/gas/fee_math.ts -+++ b/yarn-project/stdlib/src/gas/fee_math.ts -@@ -5,8 +5,8 @@ - - // Constants matching FeeLib.sol - export const MINIMUM_CONGESTION_MULTIPLIER = 1_000_000_000n; --export const L1_GAS_PER_CHECKPOINT_PROPOSED = 300_000n; --export const L1_GAS_PER_EPOCH_VERIFIED = 3_600_000n; -+export const L1_GAS_PER_CHECKPOINT_PROPOSED = 500_000n; -+export const L1_GAS_PER_EPOCH_VERIFIED = 4_000_000n; - export const BLOBS_PER_CHECKPOINT = 3n; - export const BLOB_GAS_PER_BLOB = 2n ** 17n; - export const MAGIC_CONGESTION_VALUE_MULTIPLIER = 854_700_854n; diff --git a/labs-patches/0017-feat-support-attester-signed-provider-exits.patch b/labs-patches/0017-feat-support-attester-signed-provider-exits.patch deleted file mode 100644 index 0bdf18014984..000000000000 --- a/labs-patches/0017-feat-support-attester-signed-provider-exits.patch +++ /dev/null @@ -1,360 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Rumata888 -Date: Tue, 15 Sep 2026 03:00:59 +0000 -Subject: [PATCH] feat: support attester-signed provider exits - - -diff --git a/yarn-project/cli/src/cmds/l1/index.ts b/yarn-project/cli/src/cmds/l1/index.ts -index 5d6c1d72274e9061cf1cc00f3a992b0212038883..7739e64f38c63cbe9afb32fd78544fac74df48f2 100644 ---- a/yarn-project/cli/src/cmds/l1/index.ts -+++ b/yarn-project/cli/src/cmds/l1/index.ts -@@ -337,6 +337,29 @@ export function injectCommands(program: Command, log: LogFn, debugLogger: Logger - }); - }); - -+ program -+ .command('initiate-provider-exit') -+ .description("Initiates a staking-provider exit, signed by the position's attester.") -+ .addOption(l1RpcUrlsOption) -+ .addOption(l1ChainIdOption) -+ .option('-pk, --private-key ', 'The attester private key', PRIVATE_KEY) -+ .option('-m, --mnemonic ', 'Mnemonic for the attester account', MNEMONIC) -+ .requiredOption('--attester
', 'Attester address of the position to exit', parseEthereumAddress) -+ .requiredOption('--rollup
', 'Rollup holding the position', parseEthereumAddress) -+ .action(async options => { -+ const { initiateProviderExit } = await import('./update_l1_validators.js'); -+ await initiateProviderExit({ -+ rpcUrls: options.l1RpcUrls, -+ chainId: options.l1ChainId, -+ privateKey: options.privateKey, -+ mnemonic: options.mnemonic, -+ attesterAddress: options.attester, -+ rollupAddress: options.rollup, -+ log, -+ debugLogger, -+ }); -+ }); -+ - program - .command('remove-l1-validator') - .description('Removes a validator to the L1 rollup contract.') -diff --git a/yarn-project/cli/src/cmds/l1/provider_exit.test.ts b/yarn-project/cli/src/cmds/l1/provider_exit.test.ts -new file mode 100644 -index 0000000000000000000000000000000000000000..114bd5fa1f85023385e3136cca0d6c8792ca1079 ---- /dev/null -+++ b/yarn-project/cli/src/cmds/l1/provider_exit.test.ts -@@ -0,0 +1,169 @@ -+import { GovernanceAbi, RollupAbi, TestERC20Abi } from '@aztec-foundation/l1-artifacts'; -+ -+import { createExtendedL1Client } from '@aztec-labs/ethereum/client'; -+import { DefaultL1ContractsConfig } from '@aztec-labs/ethereum/config'; -+import { AttesterStatus, RollupContract } from '@aztec-labs/ethereum/contracts'; -+import { deployAztecL1Contracts } from '@aztec-labs/ethereum/deploy-aztec-l1-contracts'; -+import { type Anvil, EthCheatCodes, startAnvil } from '@aztec-labs/ethereum/test'; -+import { SecretValue } from '@aztec-labs/foundation/config'; -+import { Fr } from '@aztec-labs/foundation/curves/bn254'; -+import { EthAddress } from '@aztec-labs/foundation/eth-address'; -+import { createLogger } from '@aztec-labs/foundation/log'; -+import { DateProvider } from '@aztec-labs/foundation/timer'; -+import { Command } from 'commander'; -+import { getContract } from 'viem'; -+import { mnemonicToAccount } from 'viem/accounts'; -+import { foundry } from 'viem/chains'; -+ -+import { injectCommands } from './index.js'; -+import { initiateProviderExit } from './update_l1_validators.js'; -+ -+const mnemonic = 'test test test test test test test test test test test junk'; -+const attester = mnemonicToAccount(mnemonic); -+const withdrawer = mnemonicToAccount(mnemonic, { addressIndex: 1 }); -+const logger = createLogger('cli:test:provider-exit'); -+ -+describe('initiate-provider-exit command', () => { -+ it('requires a rollup and attester and accepts no recipient', () => { -+ const program = new Command(); -+ injectCommands(program, () => {}, logger); -+ const command = program.commands.find(command => command.name() === 'initiate-provider-exit'); -+ expect(command).toBeDefined(); -+ expect(command?.options.find(option => option.long === '--rollup')?.mandatory).toBe(true); -+ expect(command?.options.find(option => option.long === '--attester')?.mandatory).toBe(true); -+ expect(command?.options.some(option => /recipient|withdrawer/.test(option.long ?? ''))).toBe(false); -+ }); -+ -+ it('rejects a signer that does not match the attester before contacting L1', async () => { -+ await expect( -+ initiateProviderExit({ -+ rpcUrls: ['http://127.0.0.1:1'], -+ chainId: foundry.id, -+ mnemonic, -+ attesterAddress: EthAddress.fromString(withdrawer.address), -+ rollupAddress: EthAddress.ZERO, -+ log: () => {}, -+ debugLogger: logger, -+ }), -+ ).rejects.toThrow('The transaction signer must match the attester address'); -+ }); -+}); -+ -+describe('provider exit through the client and CLI', () => { -+ let anvil: Anvil; -+ let rpcUrl: string; -+ -+ afterAll(async () => { -+ await anvil?.stop(); -+ }); -+ -+ it('deploys, exits as the attester, and pays only the withdrawer-selected recipient after both delays', async () => { -+ ({ anvil, rpcUrl } = await startAnvil()); -+ const { l1ContractAddresses } = await deployAztecL1Contracts( -+ rpcUrl, -+ '0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba', -+ foundry.id, -+ { -+ ...DefaultL1ContractsConfig, -+ vkTreeRoot: Fr.random(), -+ protocolContractsHash: Fr.random(), -+ genesisArchiveRoot: Fr.random(), -+ realVerifier: false, -+ aztecTargetCommitteeSize: 4, -+ entryQueueBootstrapValidatorSetSize: 21, -+ entryQueueBootstrapFlushSize: 21, -+ entryQueueMaxFlushSize: 21, -+ initialValidators: Array.from({ length: 21 }, (_, index) => ({ -+ attester: -+ index === 0 -+ ? EthAddress.fromString(attester.address) -+ : EthAddress.fromString(`0x${(1000 + index).toString(16).padStart(40, '0')}`), -+ withdrawer: EthAddress.fromString(withdrawer.address), -+ bn254SecretKey: new SecretValue(BigInt(index + 1)), -+ })), -+ }, -+ ); -+ const attesterClient = createExtendedL1Client([rpcUrl], attester, foundry); -+ const withdrawerClient = createExtendedL1Client([rpcUrl], withdrawer, foundry); -+ const rollup = new RollupContract(attesterClient, l1ContractAddresses.rollupAddress); -+ const target = EthAddress.fromString(attester.address); -+ const recipient = mnemonicToAccount(mnemonic, { addressIndex: 2 }).address; -+ const token = getContract({ -+ address: (await rollup.getStakingAsset()).toString(), -+ abi: TestERC20Abi, -+ client: attesterClient, -+ }); -+ const governance = getContract({ -+ address: l1ContractAddresses.governanceAddress.toString(), -+ abi: GovernanceAbi, -+ client: attesterClient, -+ }); -+ const ownerRollup = getContract({ address: rollup.address, abi: RollupAbi, client: withdrawerClient }); -+ const original = await rollup.getAttesterView(target); -+ const recipientBalance = await token.read.balanceOf([recipient]); -+ -+ const before = await rollup.getProviderExitLimitState(); -+ expect(before).toMatchObject({ validatorCount: 21n, committeeSize: 4n, used: 0n, allowance: 1n, canExit: true }); -+ expect(await rollup.getProviderExitWindow()).toBe(before.window); -+ -+ const receipt = await initiateProviderExit({ -+ rpcUrls: [rpcUrl], -+ chainId: foundry.id, -+ mnemonic, -+ attesterAddress: target, -+ rollupAddress: l1ContractAddresses.rollupAddress, -+ log: () => {}, -+ debugLogger: logger, -+ }); -+ expect(receipt.status).toBe('success'); -+ const pending = await rollup.getAttesterView(target); -+ expect(pending.status).toBe(AttesterStatus.ZOMBIE); -+ expect(pending.exit.exists).toBe(true); -+ expect(pending.exit.isRecipient).toBe(false); -+ expect(pending.exit.recipientOrWithdrawer).toEqual(EthAddress.fromString(withdrawer.address)); -+ expect(pending.exit.amount).toBe(original.effectiveBalance); -+ expect(await token.read.balanceOf([recipient])).toBe(recipientBalance); -+ expect(await rollup.getProviderExitLimitState()).toMatchObject({ -+ validatorCount: 20n, -+ used: 1n, -+ allowance: 0n, -+ canExit: false, -+ }); -+ const withdrawal = await governance.read.getWithdrawal([pending.exit.withdrawalId]); -+ -+ await expect( -+ attesterClient.simulateContract({ -+ address: rollup.address, -+ abi: RollupAbi, -+ functionName: 'initiateWithdraw', -+ args: [attester.address, recipient], -+ }), -+ ).rejects.toThrow(); -+ -+ const selection = await ownerRollup.write.initiateWithdraw([attester.address, recipient]); -+ expect((await withdrawerClient.waitForTransactionReceipt({ hash: selection })).status).toBe('success'); -+ const selected = await rollup.getAttesterView(target); -+ expect(selected.exit.withdrawalId).toBe(pending.exit.withdrawalId); -+ expect(selected.exit.exitableAt).toBe(pending.exit.exitableAt); -+ expect(selected.exit.recipientOrWithdrawer).toEqual(EthAddress.fromString(recipient)); -+ expect(selected.exit.isRecipient).toBe(true); -+ expect(await governance.read.getWithdrawal([pending.exit.withdrawalId])).toEqual(withdrawal); -+ -+ const unlock = pending.exit.exitableAt > withdrawal.unlocksAt ? pending.exit.exitableAt : withdrawal.unlocksAt; -+ const cheatCodes = new EthCheatCodes([rpcUrl], new DateProvider()); -+ await cheatCodes.warp(unlock - 1n); -+ await expect( -+ attesterClient.simulateContract({ -+ address: rollup.address, -+ abi: RollupAbi, -+ functionName: 'finalizeWithdraw', -+ args: [attester.address], -+ }), -+ ).rejects.toThrow(); -+ await cheatCodes.warp(unlock); -+ const finalized = await ownerRollup.write.finalizeWithdraw([attester.address]); -+ expect((await withdrawerClient.waitForTransactionReceipt({ hash: finalized })).status).toBe('success'); -+ expect(await token.read.balanceOf([recipient])).toBe(recipientBalance + pending.exit.amount); -+ expect((await rollup.getAttesterView(target)).exit.exists).toBe(false); -+ }, 240_000); -+}); -diff --git a/yarn-project/cli/src/cmds/l1/update_l1_validators.ts b/yarn-project/cli/src/cmds/l1/update_l1_validators.ts -index 5b889d49d1b232b1d2b3593f77d34df7faa3306b..9fa4469702311c4d73c208b5ba16a4362cf0acb9 100644 ---- a/yarn-project/cli/src/cmds/l1/update_l1_validators.ts -+++ b/yarn-project/cli/src/cmds/l1/update_l1_validators.ts -@@ -254,6 +254,39 @@ export async function removeL1Validator({ - dualLog(`Transaction hash: ${receipt.transactionHash}`); - } - -+/** Initiates a provider exit without changing the registered withdrawer's control over the payout. */ -+export async function initiateProviderExit({ -+ rpcUrls, -+ chainId, -+ privateKey, -+ mnemonic, -+ attesterAddress, -+ rollupAddress, -+ log, -+ debugLogger, -+}: Omit & LoggerArgs & { attesterAddress: EthAddress }) { -+ const account = getAccount(privateKey, mnemonic); -+ if (account.address.toLowerCase() !== attesterAddress.toString().toLowerCase()) { -+ throw new Error('The transaction signer must match the attester address'); -+ } -+ const chain = createEthereumChain(rpcUrls, chainId); -+ const client = createExtendedL1Client(rpcUrls, account, chain.chainInfo); -+ const rollup = new RollupContract(client, rollupAddress); -+ const l1TxUtils = createL1TxUtils(client, { logger: debugLogger }); -+ const { receipt } = await rollup.initiateProviderExit(l1TxUtils, attesterAddress); -+ if (receipt.status !== 'success') { -+ throw new Error(`Provider exit reverted: ${receipt.transactionHash}`); -+ } -+ log(`Provider exit initiated for ${attesterAddress}. Transaction hash: ${receipt.transactionHash}`); -+ log('The registered withdrawer must select a recipient using initiateWithdraw before finalization.'); -+ debugLogger.info('Provider exit initiated', { -+ attester: attesterAddress.toString(), -+ rollup: rollupAddress.toString(), -+ transactionHash: receipt.transactionHash, -+ }); -+ return receipt; -+} -+ - export async function pruneRollup({ - rpcUrls, - chainId, -diff --git a/yarn-project/ethereum/src/contracts/rollup.ts b/yarn-project/ethereum/src/contracts/rollup.ts -index 0b04e06b0845647877173fcd4fe723b509c40707..99e313209a9903d2e3bf29a39fab41e98f84e5d2 100644 ---- a/yarn-project/ethereum/src/contracts/rollup.ts -+++ b/yarn-project/ethereum/src/contracts/rollup.ts -@@ -210,6 +210,18 @@ export type RewardConfig = { - checkpointReward: bigint; - }; - -+/** Current provider-exit capacity. All durations are in seconds and counts are positions, not token amounts. */ -+export type ProviderExitLimitState = { -+ window: bigint; -+ validatorCount: bigint; -+ committeeSize: bigint; -+ used: bigint; -+ /** Limit after one proposed removal; allowance minus used is not a guaranteed batch size. */ -+ allowance: bigint; -+ /** Rollup-wide capacity only; the transaction still checks the caller and position. */ -+ canExit: boolean; -+}; -+ - /** - * Exit information for a validator - */ -@@ -1350,6 +1362,30 @@ export class RollupContract { - return Buffer32.fromString(await this.rollup.read.getCurrentBlobCommitmentsHash()); - } - -+ public getProviderExitWindow(): Promise { -+ return this.rollup.read.getProviderExitWindow(); -+ } -+ -+ public getProviderExitLimitState(): Promise { -+ return this.rollup.read.getProviderExitLimitState(); -+ } -+ -+ /** Initiates a provider exit. The transaction signer must be the position's attester. */ -+ public initiateProviderExit( -+ l1TxUtils: L1TxUtils, -+ attester: EthAddress, -+ ): ReturnType { -+ return l1TxUtils.sendAndMonitorTransaction({ -+ to: this.address, -+ abi: RollupAbi, -+ data: encodeFunctionData({ -+ abi: RollupAbi, -+ functionName: 'initiateProviderExit', -+ args: [attester.toString()], -+ }), -+ }); -+ } -+ - async getStakingAsset(): Promise { - return EthAddress.fromString(await this.rollup.read.getStakingAsset()); - } -diff --git a/yarn-project/ethereum/src/l1_artifacts.ts b/yarn-project/ethereum/src/l1_artifacts.ts -index 2e6979daa1c516ddb8741772f705985674bf0f4c..d1d574bd048ddc3418d8d49e4567b1b4c51940dd 100644 ---- a/yarn-project/ethereum/src/l1_artifacts.ts -+++ b/yarn-project/ethereum/src/l1_artifacts.ts -@@ -27,6 +27,8 @@ import { - MultiAdderBytecode, - OutboxAbi, - OutboxBytecode, -+ ProviderExitExtLibAbi, -+ ProviderExitExtLibBytecode, - RegisterNewRollupVersionPayloadAbi, - RegisterNewRollupVersionPayloadBytecode, - RegistryAbi, -@@ -103,6 +105,11 @@ export const RollupArtifact = { - contractAbi: ValidatorOperationsExtLibAbi, - contractBytecode: ValidatorOperationsExtLibBytecode as Hex, - }, -+ ProviderExitExtLib: { -+ name: 'ProviderExitExtLib', -+ contractAbi: ProviderExitExtLibAbi, -+ contractBytecode: ProviderExitExtLibBytecode as Hex, -+ }, - RewardExtLib: { - name: 'RewardExtLib', - contractAbi: RewardExtLibAbi, -diff --git a/yarn-project/ethereum/src/provider_exit_artifacts.test.ts b/yarn-project/ethereum/src/provider_exit_artifacts.test.ts -new file mode 100644 -index 0000000000000000000000000000000000000000..58022cca58f9f7ba8ab7fa005cdba99ec16fc536 ---- /dev/null -+++ b/yarn-project/ethereum/src/provider_exit_artifacts.test.ts -@@ -0,0 +1,16 @@ -+import { RollupArtifact } from './l1_artifacts.js'; -+ -+describe('provider-exit deployment artifacts', () => { -+ it('provides deployable bytecode for the provider library required by the rollup', () => { -+ const references = Object.values(RollupArtifact.libraries.linkReferences).flatMap(Object.keys); -+ expect(references).toContain('ProviderExitExtLib'); -+ const library = RollupArtifact.libraries.libraryCode.ProviderExitExtLib; -+ expect(library.contractBytecode).toMatch(/^0x[0-9a-f]+$/i); -+ expect( -+ library.contractAbi -+ .filter(item => item.type === 'function') -+ .map(item => item.name) -+ .sort(), -+ ).toEqual(['getProviderExitLimitState', 'getProviderExitWindow', 'initiateProviderExit']); -+ }); -+}); diff --git a/labs-patches/0018-test-check-provider-entry-points-on-the-rollup-ABI.patch b/labs-patches/0018-test-check-provider-entry-points-on-the-rollup-ABI.patch deleted file mode 100644 index 5cddaa9887f0..000000000000 --- a/labs-patches/0018-test-check-provider-entry-points-on-the-rollup-ABI.patch +++ /dev/null @@ -1,29 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Rumata888 -Date: Tue, 15 Sep 2026 03:03:41 +0000 -Subject: [PATCH] test: check provider entry points on the rollup ABI - - -diff --git a/yarn-project/ethereum/src/provider_exit_artifacts.test.ts b/yarn-project/ethereum/src/provider_exit_artifacts.test.ts -index 58022cca58f9f7ba8ab7fa005cdba99ec16fc536..d62471f1745358da605d8b94af22a4e7afa8832f 100644 ---- a/yarn-project/ethereum/src/provider_exit_artifacts.test.ts -+++ b/yarn-project/ethereum/src/provider_exit_artifacts.test.ts -@@ -6,11 +6,12 @@ describe('provider-exit deployment artifacts', () => { - expect(references).toContain('ProviderExitExtLib'); - const library = RollupArtifact.libraries.libraryCode.ProviderExitExtLib; - expect(library.contractBytecode).toMatch(/^0x[0-9a-f]+$/i); -- expect( -- library.contractAbi -- .filter(item => item.type === 'function') -- .map(item => item.name) -- .sort(), -- ).toEqual(['getProviderExitLimitState', 'getProviderExitWindow', 'initiateProviderExit']); -+ expect(RollupArtifact.contractAbi).toEqual( -+ expect.arrayContaining([ -+ expect.objectContaining({ type: 'function', name: 'getProviderExitLimitState' }), -+ expect.objectContaining({ type: 'function', name: 'getProviderExitWindow' }), -+ expect.objectContaining({ type: 'function', name: 'initiateProviderExit' }), -+ ]), -+ ); - }); - }); diff --git a/labs-patches/0018-test-derive-stale-fee-snapshot-from-padding-ceiling.patch b/labs-patches/0018-test-derive-stale-fee-snapshot-from-padding-ceiling.patch deleted file mode 100644 index d2c63df6460d..000000000000 --- a/labs-patches/0018-test-derive-stale-fee-snapshot-from-padding-ceiling.patch +++ /dev/null @@ -1,24 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Santiago Palladino -Date: Fri, 18 Sep 2026 12:57:42 -0300 -Subject: [PATCH] test: derive stale fee snapshot from padding ceiling - - -diff --git a/yarn-project/end-to-end/src/single-node/fees/fee_settings.test.ts b/yarn-project/end-to-end/src/single-node/fees/fee_settings.test.ts -index 5268662b0ab779007d8eafd6cc03a7951dd80b08..212cf024f4d7b7213462abe15de0a652abf23358 100644 ---- a/yarn-project/end-to-end/src/single-node/fees/fee_settings.test.ts -+++ b/yarn-project/end-to-end/src/single-node/fees/fee_settings.test.ts -@@ -239,10 +239,9 @@ describe('single-node/fees/fee_settings', () => { - - const lowerMinFees = await getCurrentMinFeesAfterCheckpoint(testContractDeployBlock); - // `higherMinFees` is the synthetic "stale" snapshot the wallet supposedly took before the -- // real L2 fee bumped — it only needs to stay above the realized `bumpedMinFees` so that -- // `txWithNoPadding` is still mineable after the bump. Use `4x` for unambiguous headroom -- // while keeping the snapshot below the 6x default-padding cap. -- const higherMinFees = lowerMinFees.mul(4); -+ // real L2 fee bumped. Match the default-padded ceiling, which this test already requires to -+ // exceed the realized bump, so both fee-selection paths have the same guaranteed headroom. -+ const higherMinFees = lowerMinFees.mul(1 + DEFAULT_MIN_FEE_PADDING); - - const { txWithNoPadding, txWithDefaultPadding } = await prepareTxsWithMockedMinFees(higherMinFees, lowerMinFees); - diff --git a/labs-patches/0019-refactor-name-attester-initiated-withdrawals-consist.patch b/labs-patches/0019-refactor-name-attester-initiated-withdrawals-consist.patch deleted file mode 100644 index f453f6d68000..000000000000 --- a/labs-patches/0019-refactor-name-attester-initiated-withdrawals-consist.patch +++ /dev/null @@ -1,543 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Rumata888 -Date: Wed, 16 Sep 2026 15:36:33 +0000 -Subject: [PATCH] refactor: name attester-initiated withdrawals consistently - - -diff --git a/yarn-project/cli/src/cmds/l1/attester_exit.test.ts b/yarn-project/cli/src/cmds/l1/attester_exit.test.ts -new file mode 100644 -index 0000000000000000000000000000000000000000..0ac624dff45d66c40ffc7f3285f0a9ab1d66d811 ---- /dev/null -+++ b/yarn-project/cli/src/cmds/l1/attester_exit.test.ts -@@ -0,0 +1,169 @@ -+import { GovernanceAbi, RollupAbi, TestERC20Abi } from '@aztec-foundation/l1-artifacts'; -+ -+import { createExtendedL1Client } from '@aztec-labs/ethereum/client'; -+import { DefaultL1ContractsConfig } from '@aztec-labs/ethereum/config'; -+import { AttesterStatus, RollupContract } from '@aztec-labs/ethereum/contracts'; -+import { deployAztecL1Contracts } from '@aztec-labs/ethereum/deploy-aztec-l1-contracts'; -+import { type Anvil, EthCheatCodes, startAnvil } from '@aztec-labs/ethereum/test'; -+import { SecretValue } from '@aztec-labs/foundation/config'; -+import { Fr } from '@aztec-labs/foundation/curves/bn254'; -+import { EthAddress } from '@aztec-labs/foundation/eth-address'; -+import { createLogger } from '@aztec-labs/foundation/log'; -+import { DateProvider } from '@aztec-labs/foundation/timer'; -+import { Command } from 'commander'; -+import { getContract } from 'viem'; -+import { mnemonicToAccount } from 'viem/accounts'; -+import { foundry } from 'viem/chains'; -+ -+import { injectCommands } from './index.js'; -+import { initiateWithdrawByAttester } from './update_l1_validators.js'; -+ -+const mnemonic = 'test test test test test test test test test test test junk'; -+const attester = mnemonicToAccount(mnemonic); -+const withdrawer = mnemonicToAccount(mnemonic, { addressIndex: 1 }); -+const logger = createLogger('cli:test:attester-exit'); -+ -+describe('initiate-withdraw-by-attester command', () => { -+ it('requires a rollup and attester and accepts no recipient', () => { -+ const program = new Command(); -+ injectCommands(program, () => {}, logger); -+ const command = program.commands.find(command => command.name() === 'initiate-withdraw-by-attester'); -+ expect(command).toBeDefined(); -+ expect(command?.options.find(option => option.long === '--rollup')?.mandatory).toBe(true); -+ expect(command?.options.find(option => option.long === '--attester')?.mandatory).toBe(true); -+ expect(command?.options.some(option => /recipient|withdrawer/.test(option.long ?? ''))).toBe(false); -+ }); -+ -+ it('rejects a signer that does not match the attester before contacting L1', async () => { -+ await expect( -+ initiateWithdrawByAttester({ -+ rpcUrls: ['http://127.0.0.1:1'], -+ chainId: foundry.id, -+ mnemonic, -+ attesterAddress: EthAddress.fromString(withdrawer.address), -+ rollupAddress: EthAddress.ZERO, -+ log: () => {}, -+ debugLogger: logger, -+ }), -+ ).rejects.toThrow('The transaction signer must match the attester address'); -+ }); -+}); -+ -+describe('attester exit through the client and CLI', () => { -+ let anvil: Anvil; -+ let rpcUrl: string; -+ -+ afterAll(async () => { -+ await anvil?.stop(); -+ }); -+ -+ it('deploys, exits as the attester, and pays only the withdrawer-selected recipient after both delays', async () => { -+ ({ anvil, rpcUrl } = await startAnvil()); -+ const { l1ContractAddresses } = await deployAztecL1Contracts( -+ rpcUrl, -+ '0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba', -+ foundry.id, -+ { -+ ...DefaultL1ContractsConfig, -+ vkTreeRoot: Fr.random(), -+ protocolContractsHash: Fr.random(), -+ genesisArchiveRoot: Fr.random(), -+ realVerifier: false, -+ aztecTargetCommitteeSize: 4, -+ entryQueueBootstrapValidatorSetSize: 21, -+ entryQueueBootstrapFlushSize: 21, -+ entryQueueMaxFlushSize: 21, -+ initialValidators: Array.from({ length: 21 }, (_, index) => ({ -+ attester: -+ index === 0 -+ ? EthAddress.fromString(attester.address) -+ : EthAddress.fromString(`0x${(1000 + index).toString(16).padStart(40, '0')}`), -+ withdrawer: EthAddress.fromString(withdrawer.address), -+ bn254SecretKey: new SecretValue(BigInt(index + 1)), -+ })), -+ }, -+ ); -+ const attesterClient = createExtendedL1Client([rpcUrl], attester, foundry); -+ const withdrawerClient = createExtendedL1Client([rpcUrl], withdrawer, foundry); -+ const rollup = new RollupContract(attesterClient, l1ContractAddresses.rollupAddress); -+ const target = EthAddress.fromString(attester.address); -+ const recipient = mnemonicToAccount(mnemonic, { addressIndex: 2 }).address; -+ const token = getContract({ -+ address: (await rollup.getStakingAsset()).toString(), -+ abi: TestERC20Abi, -+ client: attesterClient, -+ }); -+ const governance = getContract({ -+ address: l1ContractAddresses.governanceAddress.toString(), -+ abi: GovernanceAbi, -+ client: attesterClient, -+ }); -+ const ownerRollup = getContract({ address: rollup.address, abi: RollupAbi, client: withdrawerClient }); -+ const original = await rollup.getAttesterView(target); -+ const recipientBalance = await token.read.balanceOf([recipient]); -+ -+ const before = await rollup.getAttesterExitLimitState(); -+ expect(before).toMatchObject({ validatorCount: 21n, committeeSize: 4n, used: 0n, allowance: 1n, canExit: true }); -+ expect(await rollup.getAttesterExitWindow()).toBe(before.window); -+ -+ const receipt = await initiateWithdrawByAttester({ -+ rpcUrls: [rpcUrl], -+ chainId: foundry.id, -+ mnemonic, -+ attesterAddress: target, -+ rollupAddress: l1ContractAddresses.rollupAddress, -+ log: () => {}, -+ debugLogger: logger, -+ }); -+ expect(receipt.status).toBe('success'); -+ const pending = await rollup.getAttesterView(target); -+ expect(pending.status).toBe(AttesterStatus.ZOMBIE); -+ expect(pending.exit.exists).toBe(true); -+ expect(pending.exit.isRecipient).toBe(false); -+ expect(pending.exit.recipientOrWithdrawer).toEqual(EthAddress.fromString(withdrawer.address)); -+ expect(pending.exit.amount).toBe(original.effectiveBalance); -+ expect(await token.read.balanceOf([recipient])).toBe(recipientBalance); -+ expect(await rollup.getAttesterExitLimitState()).toMatchObject({ -+ validatorCount: 20n, -+ used: 1n, -+ allowance: 0n, -+ canExit: false, -+ }); -+ const withdrawal = await governance.read.getWithdrawal([pending.exit.withdrawalId]); -+ -+ await expect( -+ attesterClient.simulateContract({ -+ address: rollup.address, -+ abi: RollupAbi, -+ functionName: 'initiateWithdraw', -+ args: [attester.address, recipient], -+ }), -+ ).rejects.toThrow(); -+ -+ const selection = await ownerRollup.write.initiateWithdraw([attester.address, recipient]); -+ expect((await withdrawerClient.waitForTransactionReceipt({ hash: selection })).status).toBe('success'); -+ const selected = await rollup.getAttesterView(target); -+ expect(selected.exit.withdrawalId).toBe(pending.exit.withdrawalId); -+ expect(selected.exit.exitableAt).toBe(pending.exit.exitableAt); -+ expect(selected.exit.recipientOrWithdrawer).toEqual(EthAddress.fromString(recipient)); -+ expect(selected.exit.isRecipient).toBe(true); -+ expect(await governance.read.getWithdrawal([pending.exit.withdrawalId])).toEqual(withdrawal); -+ -+ const unlock = pending.exit.exitableAt > withdrawal.unlocksAt ? pending.exit.exitableAt : withdrawal.unlocksAt; -+ const cheatCodes = new EthCheatCodes([rpcUrl], new DateProvider()); -+ await cheatCodes.warp(unlock - 1n); -+ await expect( -+ attesterClient.simulateContract({ -+ address: rollup.address, -+ abi: RollupAbi, -+ functionName: 'finalizeWithdraw', -+ args: [attester.address], -+ }), -+ ).rejects.toThrow(); -+ await cheatCodes.warp(unlock); -+ const finalized = await ownerRollup.write.finalizeWithdraw([attester.address]); -+ expect((await withdrawerClient.waitForTransactionReceipt({ hash: finalized })).status).toBe('success'); -+ expect(await token.read.balanceOf([recipient])).toBe(recipientBalance + pending.exit.amount); -+ expect((await rollup.getAttesterView(target)).exit.exists).toBe(false); -+ }, 240_000); -+}); -diff --git a/yarn-project/cli/src/cmds/l1/index.ts b/yarn-project/cli/src/cmds/l1/index.ts -index 7739e64f38c63cbe9afb32fd78544fac74df48f2..00b3a92999f82dfad8970af5c167ed902d50384e 100644 ---- a/yarn-project/cli/src/cmds/l1/index.ts -+++ b/yarn-project/cli/src/cmds/l1/index.ts -@@ -338,8 +338,8 @@ export function injectCommands(program: Command, log: LogFn, debugLogger: Logger - }); - - program -- .command('initiate-provider-exit') -- .description("Initiates a staking-provider exit, signed by the position's attester.") -+ .command('initiate-withdraw-by-attester') -+ .description("Initiates a withdrawal signed by the position's attester.") - .addOption(l1RpcUrlsOption) - .addOption(l1ChainIdOption) - .option('-pk, --private-key ', 'The attester private key', PRIVATE_KEY) -@@ -347,8 +347,8 @@ export function injectCommands(program: Command, log: LogFn, debugLogger: Logger - .requiredOption('--attester
', 'Attester address of the position to exit', parseEthereumAddress) - .requiredOption('--rollup
', 'Rollup holding the position', parseEthereumAddress) - .action(async options => { -- const { initiateProviderExit } = await import('./update_l1_validators.js'); -- await initiateProviderExit({ -+ const { initiateWithdrawByAttester } = await import('./update_l1_validators.js'); -+ await initiateWithdrawByAttester({ - rpcUrls: options.l1RpcUrls, - chainId: options.l1ChainId, - privateKey: options.privateKey, -diff --git a/yarn-project/cli/src/cmds/l1/provider_exit.test.ts b/yarn-project/cli/src/cmds/l1/provider_exit.test.ts -deleted file mode 100644 -index 114bd5fa1f85023385e3136cca0d6c8792ca1079..0000000000000000000000000000000000000000 ---- a/yarn-project/cli/src/cmds/l1/provider_exit.test.ts -+++ /dev/null -@@ -1,169 +0,0 @@ --import { GovernanceAbi, RollupAbi, TestERC20Abi } from '@aztec-foundation/l1-artifacts'; -- --import { createExtendedL1Client } from '@aztec-labs/ethereum/client'; --import { DefaultL1ContractsConfig } from '@aztec-labs/ethereum/config'; --import { AttesterStatus, RollupContract } from '@aztec-labs/ethereum/contracts'; --import { deployAztecL1Contracts } from '@aztec-labs/ethereum/deploy-aztec-l1-contracts'; --import { type Anvil, EthCheatCodes, startAnvil } from '@aztec-labs/ethereum/test'; --import { SecretValue } from '@aztec-labs/foundation/config'; --import { Fr } from '@aztec-labs/foundation/curves/bn254'; --import { EthAddress } from '@aztec-labs/foundation/eth-address'; --import { createLogger } from '@aztec-labs/foundation/log'; --import { DateProvider } from '@aztec-labs/foundation/timer'; --import { Command } from 'commander'; --import { getContract } from 'viem'; --import { mnemonicToAccount } from 'viem/accounts'; --import { foundry } from 'viem/chains'; -- --import { injectCommands } from './index.js'; --import { initiateProviderExit } from './update_l1_validators.js'; -- --const mnemonic = 'test test test test test test test test test test test junk'; --const attester = mnemonicToAccount(mnemonic); --const withdrawer = mnemonicToAccount(mnemonic, { addressIndex: 1 }); --const logger = createLogger('cli:test:provider-exit'); -- --describe('initiate-provider-exit command', () => { -- it('requires a rollup and attester and accepts no recipient', () => { -- const program = new Command(); -- injectCommands(program, () => {}, logger); -- const command = program.commands.find(command => command.name() === 'initiate-provider-exit'); -- expect(command).toBeDefined(); -- expect(command?.options.find(option => option.long === '--rollup')?.mandatory).toBe(true); -- expect(command?.options.find(option => option.long === '--attester')?.mandatory).toBe(true); -- expect(command?.options.some(option => /recipient|withdrawer/.test(option.long ?? ''))).toBe(false); -- }); -- -- it('rejects a signer that does not match the attester before contacting L1', async () => { -- await expect( -- initiateProviderExit({ -- rpcUrls: ['http://127.0.0.1:1'], -- chainId: foundry.id, -- mnemonic, -- attesterAddress: EthAddress.fromString(withdrawer.address), -- rollupAddress: EthAddress.ZERO, -- log: () => {}, -- debugLogger: logger, -- }), -- ).rejects.toThrow('The transaction signer must match the attester address'); -- }); --}); -- --describe('provider exit through the client and CLI', () => { -- let anvil: Anvil; -- let rpcUrl: string; -- -- afterAll(async () => { -- await anvil?.stop(); -- }); -- -- it('deploys, exits as the attester, and pays only the withdrawer-selected recipient after both delays', async () => { -- ({ anvil, rpcUrl } = await startAnvil()); -- const { l1ContractAddresses } = await deployAztecL1Contracts( -- rpcUrl, -- '0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba', -- foundry.id, -- { -- ...DefaultL1ContractsConfig, -- vkTreeRoot: Fr.random(), -- protocolContractsHash: Fr.random(), -- genesisArchiveRoot: Fr.random(), -- realVerifier: false, -- aztecTargetCommitteeSize: 4, -- entryQueueBootstrapValidatorSetSize: 21, -- entryQueueBootstrapFlushSize: 21, -- entryQueueMaxFlushSize: 21, -- initialValidators: Array.from({ length: 21 }, (_, index) => ({ -- attester: -- index === 0 -- ? EthAddress.fromString(attester.address) -- : EthAddress.fromString(`0x${(1000 + index).toString(16).padStart(40, '0')}`), -- withdrawer: EthAddress.fromString(withdrawer.address), -- bn254SecretKey: new SecretValue(BigInt(index + 1)), -- })), -- }, -- ); -- const attesterClient = createExtendedL1Client([rpcUrl], attester, foundry); -- const withdrawerClient = createExtendedL1Client([rpcUrl], withdrawer, foundry); -- const rollup = new RollupContract(attesterClient, l1ContractAddresses.rollupAddress); -- const target = EthAddress.fromString(attester.address); -- const recipient = mnemonicToAccount(mnemonic, { addressIndex: 2 }).address; -- const token = getContract({ -- address: (await rollup.getStakingAsset()).toString(), -- abi: TestERC20Abi, -- client: attesterClient, -- }); -- const governance = getContract({ -- address: l1ContractAddresses.governanceAddress.toString(), -- abi: GovernanceAbi, -- client: attesterClient, -- }); -- const ownerRollup = getContract({ address: rollup.address, abi: RollupAbi, client: withdrawerClient }); -- const original = await rollup.getAttesterView(target); -- const recipientBalance = await token.read.balanceOf([recipient]); -- -- const before = await rollup.getProviderExitLimitState(); -- expect(before).toMatchObject({ validatorCount: 21n, committeeSize: 4n, used: 0n, allowance: 1n, canExit: true }); -- expect(await rollup.getProviderExitWindow()).toBe(before.window); -- -- const receipt = await initiateProviderExit({ -- rpcUrls: [rpcUrl], -- chainId: foundry.id, -- mnemonic, -- attesterAddress: target, -- rollupAddress: l1ContractAddresses.rollupAddress, -- log: () => {}, -- debugLogger: logger, -- }); -- expect(receipt.status).toBe('success'); -- const pending = await rollup.getAttesterView(target); -- expect(pending.status).toBe(AttesterStatus.ZOMBIE); -- expect(pending.exit.exists).toBe(true); -- expect(pending.exit.isRecipient).toBe(false); -- expect(pending.exit.recipientOrWithdrawer).toEqual(EthAddress.fromString(withdrawer.address)); -- expect(pending.exit.amount).toBe(original.effectiveBalance); -- expect(await token.read.balanceOf([recipient])).toBe(recipientBalance); -- expect(await rollup.getProviderExitLimitState()).toMatchObject({ -- validatorCount: 20n, -- used: 1n, -- allowance: 0n, -- canExit: false, -- }); -- const withdrawal = await governance.read.getWithdrawal([pending.exit.withdrawalId]); -- -- await expect( -- attesterClient.simulateContract({ -- address: rollup.address, -- abi: RollupAbi, -- functionName: 'initiateWithdraw', -- args: [attester.address, recipient], -- }), -- ).rejects.toThrow(); -- -- const selection = await ownerRollup.write.initiateWithdraw([attester.address, recipient]); -- expect((await withdrawerClient.waitForTransactionReceipt({ hash: selection })).status).toBe('success'); -- const selected = await rollup.getAttesterView(target); -- expect(selected.exit.withdrawalId).toBe(pending.exit.withdrawalId); -- expect(selected.exit.exitableAt).toBe(pending.exit.exitableAt); -- expect(selected.exit.recipientOrWithdrawer).toEqual(EthAddress.fromString(recipient)); -- expect(selected.exit.isRecipient).toBe(true); -- expect(await governance.read.getWithdrawal([pending.exit.withdrawalId])).toEqual(withdrawal); -- -- const unlock = pending.exit.exitableAt > withdrawal.unlocksAt ? pending.exit.exitableAt : withdrawal.unlocksAt; -- const cheatCodes = new EthCheatCodes([rpcUrl], new DateProvider()); -- await cheatCodes.warp(unlock - 1n); -- await expect( -- attesterClient.simulateContract({ -- address: rollup.address, -- abi: RollupAbi, -- functionName: 'finalizeWithdraw', -- args: [attester.address], -- }), -- ).rejects.toThrow(); -- await cheatCodes.warp(unlock); -- const finalized = await ownerRollup.write.finalizeWithdraw([attester.address]); -- expect((await withdrawerClient.waitForTransactionReceipt({ hash: finalized })).status).toBe('success'); -- expect(await token.read.balanceOf([recipient])).toBe(recipientBalance + pending.exit.amount); -- expect((await rollup.getAttesterView(target)).exit.exists).toBe(false); -- }, 240_000); --}); -diff --git a/yarn-project/cli/src/cmds/l1/update_l1_validators.ts b/yarn-project/cli/src/cmds/l1/update_l1_validators.ts -index 9fa4469702311c4d73c208b5ba16a4362cf0acb9..61f08708e7cee9057de9bd2c1889ec0a512c90d8 100644 ---- a/yarn-project/cli/src/cmds/l1/update_l1_validators.ts -+++ b/yarn-project/cli/src/cmds/l1/update_l1_validators.ts -@@ -254,8 +254,8 @@ export async function removeL1Validator({ - dualLog(`Transaction hash: ${receipt.transactionHash}`); - } - --/** Initiates a provider exit without changing the registered withdrawer's control over the payout. */ --export async function initiateProviderExit({ -+/** Initiates a attester exit without changing the registered withdrawer's control over the payout. */ -+export async function initiateWithdrawByAttester({ - rpcUrls, - chainId, - privateKey, -@@ -273,13 +273,13 @@ export async function initiateProviderExit({ - const client = createExtendedL1Client(rpcUrls, account, chain.chainInfo); - const rollup = new RollupContract(client, rollupAddress); - const l1TxUtils = createL1TxUtils(client, { logger: debugLogger }); -- const { receipt } = await rollup.initiateProviderExit(l1TxUtils, attesterAddress); -+ const { receipt } = await rollup.initiateWithdrawByAttester(l1TxUtils, attesterAddress); - if (receipt.status !== 'success') { -- throw new Error(`Provider exit reverted: ${receipt.transactionHash}`); -+ throw new Error(`Attester exit reverted: ${receipt.transactionHash}`); - } -- log(`Provider exit initiated for ${attesterAddress}. Transaction hash: ${receipt.transactionHash}`); -+ log(`Attester exit initiated for ${attesterAddress}. Transaction hash: ${receipt.transactionHash}`); - log('The registered withdrawer must select a recipient using initiateWithdraw before finalization.'); -- debugLogger.info('Provider exit initiated', { -+ debugLogger.info('Attester exit initiated', { - attester: attesterAddress.toString(), - rollup: rollupAddress.toString(), - transactionHash: receipt.transactionHash, -diff --git a/yarn-project/ethereum/src/attester_exit_artifacts.test.ts b/yarn-project/ethereum/src/attester_exit_artifacts.test.ts -new file mode 100644 -index 0000000000000000000000000000000000000000..b26c56843b44e7015fd85c2fa9e4f556ff55f929 ---- /dev/null -+++ b/yarn-project/ethereum/src/attester_exit_artifacts.test.ts -@@ -0,0 +1,17 @@ -+import { RollupArtifact } from './l1_artifacts.js'; -+ -+describe('attester-exit deployment artifacts', () => { -+ it('provides deployable bytecode for the provider library required by the rollup', () => { -+ const references = Object.values(RollupArtifact.libraries.linkReferences).flatMap(Object.keys); -+ expect(references).toContain('AttesterExitExtLib'); -+ const library = RollupArtifact.libraries.libraryCode.AttesterExitExtLib; -+ expect(library.contractBytecode).toMatch(/^0x[0-9a-f]+$/i); -+ expect(RollupArtifact.contractAbi).toEqual( -+ expect.arrayContaining([ -+ expect.objectContaining({ type: 'function', name: 'getAttesterExitLimitState' }), -+ expect.objectContaining({ type: 'function', name: 'getAttesterExitWindow' }), -+ expect.objectContaining({ type: 'function', name: 'initiateWithdrawByAttester' }), -+ ]), -+ ); -+ }); -+}); -diff --git a/yarn-project/ethereum/src/contracts/rollup.ts b/yarn-project/ethereum/src/contracts/rollup.ts -index 99e313209a9903d2e3bf29a39fab41e98f84e5d2..1ec153c00024998952168bd1df2e6b4c949560c5 100644 ---- a/yarn-project/ethereum/src/contracts/rollup.ts -+++ b/yarn-project/ethereum/src/contracts/rollup.ts -@@ -210,8 +210,8 @@ export type RewardConfig = { - checkpointReward: bigint; - }; - --/** Current provider-exit capacity. All durations are in seconds and counts are positions, not token amounts. */ --export type ProviderExitLimitState = { -+/** Current attester-exit capacity. All durations are in seconds and counts are positions, not token amounts. */ -+export type AttesterExitLimitState = { - window: bigint; - validatorCount: bigint; - committeeSize: bigint; -@@ -1362,16 +1362,16 @@ export class RollupContract { - return Buffer32.fromString(await this.rollup.read.getCurrentBlobCommitmentsHash()); - } - -- public getProviderExitWindow(): Promise { -- return this.rollup.read.getProviderExitWindow(); -+ public getAttesterExitWindow(): Promise { -+ return this.rollup.read.getAttesterExitWindow(); - } - -- public getProviderExitLimitState(): Promise { -- return this.rollup.read.getProviderExitLimitState(); -+ public getAttesterExitLimitState(): Promise { -+ return this.rollup.read.getAttesterExitLimitState(); - } - -- /** Initiates a provider exit. The transaction signer must be the position's attester. */ -- public initiateProviderExit( -+ /** Initiates a attester exit. The transaction signer must be the position's attester. */ -+ public initiateWithdrawByAttester( - l1TxUtils: L1TxUtils, - attester: EthAddress, - ): ReturnType { -@@ -1380,7 +1380,7 @@ export class RollupContract { - abi: RollupAbi, - data: encodeFunctionData({ - abi: RollupAbi, -- functionName: 'initiateProviderExit', -+ functionName: 'initiateWithdrawByAttester', - args: [attester.toString()], - }), - }); -diff --git a/yarn-project/ethereum/src/l1_artifacts.ts b/yarn-project/ethereum/src/l1_artifacts.ts -index d1d574bd048ddc3418d8d49e4567b1b4c51940dd..81ea059bc5331930775c871d336e30d0abb84a3e 100644 ---- a/yarn-project/ethereum/src/l1_artifacts.ts -+++ b/yarn-project/ethereum/src/l1_artifacts.ts -@@ -1,4 +1,6 @@ - import { -+ AttesterExitExtLibAbi, -+ AttesterExitExtLibBytecode, - CoinIssuerAbi, - CoinIssuerBytecode, - DateGatedRelayerAbi, -@@ -27,8 +29,6 @@ import { - MultiAdderBytecode, - OutboxAbi, - OutboxBytecode, -- ProviderExitExtLibAbi, -- ProviderExitExtLibBytecode, - RegisterNewRollupVersionPayloadAbi, - RegisterNewRollupVersionPayloadBytecode, - RegistryAbi, -@@ -105,10 +105,10 @@ export const RollupArtifact = { - contractAbi: ValidatorOperationsExtLibAbi, - contractBytecode: ValidatorOperationsExtLibBytecode as Hex, - }, -- ProviderExitExtLib: { -- name: 'ProviderExitExtLib', -- contractAbi: ProviderExitExtLibAbi, -- contractBytecode: ProviderExitExtLibBytecode as Hex, -+ AttesterExitExtLib: { -+ name: 'AttesterExitExtLib', -+ contractAbi: AttesterExitExtLibAbi, -+ contractBytecode: AttesterExitExtLibBytecode as Hex, - }, - RewardExtLib: { - name: 'RewardExtLib', -diff --git a/yarn-project/ethereum/src/provider_exit_artifacts.test.ts b/yarn-project/ethereum/src/provider_exit_artifacts.test.ts -deleted file mode 100644 -index d62471f1745358da605d8b94af22a4e7afa8832f..0000000000000000000000000000000000000000 ---- a/yarn-project/ethereum/src/provider_exit_artifacts.test.ts -+++ /dev/null -@@ -1,17 +0,0 @@ --import { RollupArtifact } from './l1_artifacts.js'; -- --describe('provider-exit deployment artifacts', () => { -- it('provides deployable bytecode for the provider library required by the rollup', () => { -- const references = Object.values(RollupArtifact.libraries.linkReferences).flatMap(Object.keys); -- expect(references).toContain('ProviderExitExtLib'); -- const library = RollupArtifact.libraries.libraryCode.ProviderExitExtLib; -- expect(library.contractBytecode).toMatch(/^0x[0-9a-f]+$/i); -- expect(RollupArtifact.contractAbi).toEqual( -- expect.arrayContaining([ -- expect.objectContaining({ type: 'function', name: 'getProviderExitLimitState' }), -- expect.objectContaining({ type: 'function', name: 'getProviderExitWindow' }), -- expect.objectContaining({ type: 'function', name: 'initiateProviderExit' }), -- ]), -- ); -- }); --}); diff --git a/labs-patches/0020-feat-add-batched-attester-exit-CLI.patch b/labs-patches/0020-feat-add-batched-attester-exit-CLI.patch deleted file mode 100644 index 7a7202935683..000000000000 --- a/labs-patches/0020-feat-add-batched-attester-exit-CLI.patch +++ /dev/null @@ -1,376 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Rumata888 -Date: Fri, 18 Sep 2026 00:39:31 +0000 -Subject: [PATCH] feat: add batched attester exit CLI - - -diff --git a/yarn-project/cli/src/cmds/l1/attester_exit.test.ts b/yarn-project/cli/src/cmds/l1/attester_exit.test.ts -index 0ac624dff45d66c40ffc7f3285f0a9ab1d66d811..d3181a049e98b29e66a1857dc6ab9f46ca3e3cf9 100644 ---- a/yarn-project/cli/src/cmds/l1/attester_exit.test.ts -+++ b/yarn-project/cli/src/cmds/l1/attester_exit.test.ts -@@ -8,15 +8,19 @@ import { type Anvil, EthCheatCodes, startAnvil } from '@aztec-labs/ethereum/test - import { SecretValue } from '@aztec-labs/foundation/config'; - import { Fr } from '@aztec-labs/foundation/curves/bn254'; - import { EthAddress } from '@aztec-labs/foundation/eth-address'; -+import { Signature } from '@aztec-labs/foundation/eth-signature'; - import { createLogger } from '@aztec-labs/foundation/log'; - import { DateProvider } from '@aztec-labs/foundation/timer'; - import { Command } from 'commander'; -+import { mkdtemp, rm, writeFile } from 'node:fs/promises'; -+import { tmpdir } from 'node:os'; -+import { join } from 'node:path'; - import { getContract } from 'viem'; - import { mnemonicToAccount } from 'viem/accounts'; - import { foundry } from 'viem/chains'; - - import { injectCommands } from './index.js'; --import { initiateWithdrawByAttester } from './update_l1_validators.js'; -+import { initiateWithdrawByAttester, readAttesterExitAuthorizations } from './update_l1_validators.js'; - - const mnemonic = 'test test test test test test test test test test test junk'; - const attester = mnemonicToAccount(mnemonic); -@@ -49,6 +53,40 @@ describe('initiate-withdraw-by-attester command', () => { - }); - }); - -+describe('initiate-withdraw-by-attester-batch command', () => { -+ it('requires a rollup and authorization file and exposes the up-to-limit mode', () => { -+ const program = new Command(); -+ injectCommands(program, () => {}, logger); -+ const command = program.commands.find(command => command.name() === 'initiate-withdraw-by-attester-batch'); -+ expect(command).toBeDefined(); -+ expect(command?.options.find(option => option.long === '--rollup')?.mandatory).toBe(true); -+ expect(command?.options.find(option => option.long === '--authorizations')?.mandatory).toBe(true); -+ expect(command?.options.find(option => option.long === '--up-to-limit')).toBeDefined(); -+ }); -+ -+ it('reads the JSON authorization format used by the batch command', async () => { -+ const directory = await mkdtemp(join(tmpdir(), 'attester-exit-')); -+ const path = join(directory, 'authorizations.json'); -+ const signature = Signature.random(); -+ await writeFile( -+ path, -+ JSON.stringify([{ attester: attester.address, deadline: '123456789', signature: signature.toString() }]), -+ ); -+ -+ try { -+ await expect(readAttesterExitAuthorizations(path)).resolves.toEqual([ -+ { -+ attester: EthAddress.fromString(attester.address), -+ deadline: 123456789n, -+ signature: signature.toViemSignature(), -+ }, -+ ]); -+ } finally { -+ await rm(directory, { recursive: true }); -+ } -+ }); -+}); -+ - describe('attester exit through the client and CLI', () => { - let anvil: Anvil; - let rpcUrl: string; -diff --git a/yarn-project/cli/src/cmds/l1/index.ts b/yarn-project/cli/src/cmds/l1/index.ts -index 00b3a92999f82dfad8970af5c167ed902d50384e..1eab26c5686872c6c6ce72630f163623f1e83603 100644 ---- a/yarn-project/cli/src/cmds/l1/index.ts -+++ b/yarn-project/cli/src/cmds/l1/index.ts -@@ -360,6 +360,36 @@ export function injectCommands(program: Command, log: LogFn, debugLogger: Logger - }); - }); - -+ program -+ .command('initiate-withdraw-by-attester-batch') -+ .description('Relays a JSON array of attester-signed withdrawal authorizations.') -+ .addOption(l1RpcUrlsOption) -+ .addOption(l1ChainIdOption) -+ .option('-pk, --private-key ', 'The relayer private key', PRIVATE_KEY) -+ .option('-m, --mnemonic ', 'Mnemonic for the relayer account', MNEMONIC) -+ .requiredOption('--authorizations ', 'JSON file containing attester, decimal deadline, and signature fields') -+ .requiredOption('--rollup
', 'Rollup holding the positions', parseEthereumAddress) -+ .option( -+ '--up-to-limit', -+ 'Process the largest permitted prefix instead of reverting when the whole batch is too large', -+ ) -+ .action(async options => { -+ const { initiateWithdrawByAttesterBatch, readAttesterExitAuthorizations } = await import( -+ './update_l1_validators.js' -+ ); -+ await initiateWithdrawByAttesterBatch({ -+ rpcUrls: options.l1RpcUrls, -+ chainId: options.l1ChainId, -+ privateKey: options.privateKey, -+ mnemonic: options.mnemonic, -+ authorizations: await readAttesterExitAuthorizations(options.authorizations), -+ upToLimit: options.upToLimit, -+ rollupAddress: options.rollup, -+ log, -+ debugLogger, -+ }); -+ }); -+ - program - .command('remove-l1-validator') - .description('Removes a validator to the L1 rollup contract.') -diff --git a/yarn-project/cli/src/cmds/l1/update_l1_validators.ts b/yarn-project/cli/src/cmds/l1/update_l1_validators.ts -index 61f08708e7cee9057de9bd2c1889ec0a512c90d8..0f9ad9711bbe196008c2d9c44da77a3b276e4e56 100644 ---- a/yarn-project/cli/src/cmds/l1/update_l1_validators.ts -+++ b/yarn-project/cli/src/cmds/l1/update_l1_validators.ts -@@ -3,14 +3,16 @@ import { RollupAbi, StakingAssetHandlerAbi, TestERC20Abi } from '@aztec-foundati - import { createEthereumChain, isAnvilTestChain } from '@aztec-labs/ethereum/chain'; - import { createExtendedL1Client, getPublicClient } from '@aztec-labs/ethereum/client'; - import { getL1ContractsConfigEnvVars } from '@aztec-labs/ethereum/config'; --import { GSEContract, RollupContract } from '@aztec-labs/ethereum/contracts'; -+import { type AttesterExitAuthorization, GSEContract, RollupContract } from '@aztec-labs/ethereum/contracts'; - import { createL1TxUtils } from '@aztec-labs/ethereum/l1-tx-utils'; - import { EthCheatCodes } from '@aztec-labs/ethereum/test'; --import type { EthAddress } from '@aztec-labs/foundation/eth-address'; -+import { EthAddress } from '@aztec-labs/foundation/eth-address'; -+import { Signature } from '@aztec-labs/foundation/eth-signature'; - import type { LogFn, Logger } from '@aztec-labs/foundation/log'; - import { DateProvider } from '@aztec-labs/foundation/timer'; - import { ZkPassportProofParams } from '@aztec-labs/stdlib/zkpassport'; --import { encodeFunctionData, formatEther, getContract, maxUint256 } from 'viem'; -+import { readFile } from 'node:fs/promises'; -+import { encodeFunctionData, formatEther, getContract, isHex, maxUint256 } from 'viem'; - import { generatePrivateKey, mnemonicToAccount, privateKeyToAccount } from 'viem/accounts'; - - export interface RollupCommandArgs { -@@ -254,7 +256,86 @@ export async function removeL1Validator({ - dualLog(`Transaction hash: ${receipt.transactionHash}`); - } - --/** Initiates a attester exit without changing the registered withdrawer's control over the payout. */ -+/** Reads relayed attester exit authorizations from a JSON array. */ -+export async function readAttesterExitAuthorizations(path: string): Promise { -+ const parsed: unknown = JSON.parse(await readFile(path, 'utf-8')); -+ if (!Array.isArray(parsed) || parsed.length === 0) { -+ throw new Error('Attester exit authorization file must contain a non-empty JSON array'); -+ } -+ -+ return parsed.map((value, index) => parseAttesterExitAuthorization(value, index)); -+} -+ -+function isRecord(value: unknown): value is Record { -+ return typeof value === 'object' && value !== null && !Array.isArray(value); -+} -+ -+function parseAttesterExitAuthorization(value: unknown, index: number): AttesterExitAuthorization { -+ if (!isRecord(value)) { -+ throw new Error(`Attester exit authorization ${index} must be an object`); -+ } -+ -+ const authorization = value; -+ if (typeof authorization.attester !== 'string') { -+ throw new Error(`Attester exit authorization ${index} has an invalid attester`); -+ } -+ if (typeof authorization.deadline !== 'string' || !/^\d+$/.test(authorization.deadline)) { -+ throw new Error(`Attester exit authorization ${index} deadline must be a decimal string`); -+ } -+ if (typeof authorization.signature !== 'string' || !isHex(authorization.signature)) { -+ throw new Error(`Attester exit authorization ${index} has an invalid signature`); -+ } -+ -+ return { -+ attester: EthAddress.fromString(authorization.attester), -+ deadline: BigInt(authorization.deadline), -+ signature: Signature.fromString(authorization.signature).toViemSignature(), -+ }; -+} -+ -+/** Relays a batch of attester-signed exits. */ -+export async function initiateWithdrawByAttesterBatch({ -+ rpcUrls, -+ chainId, -+ privateKey, -+ mnemonic, -+ authorizations, -+ upToLimit, -+ rollupAddress, -+ log, -+ debugLogger, -+}: Omit & -+ LoggerArgs & { -+ authorizations: AttesterExitAuthorization[]; -+ upToLimit: boolean; -+ }) { -+ const account = getAccount(privateKey, mnemonic); -+ const chain = createEthereumChain(rpcUrls, chainId); -+ const client = createExtendedL1Client(rpcUrls, account, chain.chainInfo); -+ const rollup = new RollupContract(client, rollupAddress); -+ const l1TxUtils = createL1TxUtils(client, { logger: debugLogger }); -+ const { receipt } = upToLimit -+ ? await rollup.initiateWithdrawByAttesterBatchUpToLimit(l1TxUtils, authorizations) -+ : await rollup.initiateWithdrawByAttesterBatch(l1TxUtils, authorizations); -+ -+ if (receipt.status !== 'success') { -+ throw new Error(`Attester exit batch reverted: ${receipt.transactionHash}`); -+ } -+ -+ log(`Submitted ${authorizations.length} attester exit authorizations. Transaction hash: ${receipt.transactionHash}`); -+ if (upToLimit) { -+ log('The rollup processed the largest permitted prefix of the authorization list.'); -+ } -+ debugLogger.info('Attester exit batch submitted', { -+ authorizationCount: authorizations.length, -+ upToLimit, -+ rollup: rollupAddress.toString(), -+ transactionHash: receipt.transactionHash, -+ }); -+ return receipt; -+} -+ -+/** Initiates an attester exit without changing the registered withdrawer's control over the payout. */ - export async function initiateWithdrawByAttester({ - rpcUrls, - chainId, -diff --git a/yarn-project/ethereum/src/attester_exit_artifacts.test.ts b/yarn-project/ethereum/src/attester_exit_artifacts.test.ts -index b26c56843b44e7015fd85c2fa9e4f556ff55f929..e3717fc759f9e1e3f5a2f056afedcc4ac42c8349 100644 ---- a/yarn-project/ethereum/src/attester_exit_artifacts.test.ts -+++ b/yarn-project/ethereum/src/attester_exit_artifacts.test.ts -@@ -11,6 +11,9 @@ describe('attester-exit deployment artifacts', () => { - expect.objectContaining({ type: 'function', name: 'getAttesterExitLimitState' }), - expect.objectContaining({ type: 'function', name: 'getAttesterExitWindow' }), - expect.objectContaining({ type: 'function', name: 'initiateWithdrawByAttester' }), -+ expect.objectContaining({ type: 'function', name: 'initiateWithdrawByAttesterWithSignature' }), -+ expect.objectContaining({ type: 'function', name: 'initiateWithdrawByAttesterBatch' }), -+ expect.objectContaining({ type: 'function', name: 'initiateWithdrawByAttesterBatchUpToLimit' }), - ]), - ); - }); -diff --git a/yarn-project/ethereum/src/contracts/rollup.ts b/yarn-project/ethereum/src/contracts/rollup.ts -index 1ec153c00024998952168bd1df2e6b4c949560c5..e13a0ed767dcbfa214954e139dac726e7354af02 100644 ---- a/yarn-project/ethereum/src/contracts/rollup.ts -+++ b/yarn-project/ethereum/src/contracts/rollup.ts -@@ -8,7 +8,7 @@ import { Buffer32 } from '@aztec-labs/foundation/buffer'; - import { Fr } from '@aztec-labs/foundation/curves/bn254'; - import { memoize } from '@aztec-labs/foundation/decorators'; - import { EthAddress } from '@aztec-labs/foundation/eth-address'; --import type { ViemSignature } from '@aztec-labs/foundation/eth-signature'; -+import { Signature, type ViemSignature } from '@aztec-labs/foundation/eth-signature'; - import { createLogger } from '@aztec-labs/foundation/log'; - import { makeBackoff, retry } from '@aztec-labs/foundation/retry'; - import { getErrorCause } from '@aztec-labs/foundation/types'; -@@ -21,6 +21,7 @@ import { - type Log, - RpcRequestError, - type StateOverride, -+ type TypedDataDefinition, - type WatchContractEventReturnType, - decodeErrorResult, - decodeFunctionResult, -@@ -222,6 +223,21 @@ export type AttesterExitLimitState = { - canExit: boolean; - }; - -+/** An attester-signed authorization that anyone may relay to initiate the position's withdrawal. */ -+export type AttesterExitAuthorization = { -+ attester: EthAddress; -+ deadline: bigint; -+ signature: ViemSignature; -+}; -+ -+function toViemAttesterExitAuthorization(authorization: AttesterExitAuthorization) { -+ return { -+ attester: authorization.attester.toString(), -+ deadline: authorization.deadline, -+ signature: authorization.signature, -+ }; -+} -+ - /** - * Exit information for a validator - */ -@@ -1370,7 +1386,37 @@ export class RollupContract { - return this.rollup.read.getAttesterExitLimitState(); - } - -- /** Initiates a attester exit. The transaction signer must be the position's attester. */ -+ /** Builds the EIP-712 data an attester signs to authorize a relayed exit. */ -+ public buildAttesterExitTypedData(attester: EthAddress, deadline: bigint): TypedDataDefinition { -+ return { -+ domain: { -+ name: 'Aztec Rollup', -+ version: '1', -+ chainId: this.client.chain.id, -+ verifyingContract: this.address, -+ }, -+ types: { -+ AttesterExit: [ -+ { name: 'attester', type: 'address' }, -+ { name: 'deadline', type: 'uint256' }, -+ ], -+ }, -+ primaryType: 'AttesterExit', -+ message: { attester: attester.toString(), deadline }, -+ }; -+ } -+ -+ /** Creates an attester exit authorization that can be submitted by another account. */ -+ public async createAttesterExitAuthorization( -+ attester: EthAddress, -+ deadline: bigint, -+ signer: (typedData: TypedDataDefinition) => Promise, -+ ): Promise { -+ const signature = Signature.fromString(await signer(this.buildAttesterExitTypedData(attester, deadline))); -+ return { attester, deadline, signature: signature.toViemSignature() }; -+ } -+ -+ /** Initiates an attester exit. The transaction signer must be the position's attester. */ - public initiateWithdrawByAttester( - l1TxUtils: L1TxUtils, - attester: EthAddress, -@@ -1386,6 +1432,54 @@ export class RollupContract { - }); - } - -+ /** Relays one signed attester exit authorization. */ -+ public initiateWithdrawByAttesterWithSignature( -+ l1TxUtils: L1TxUtils, -+ authorization: AttesterExitAuthorization, -+ ): ReturnType { -+ return l1TxUtils.sendAndMonitorTransaction({ -+ to: this.address, -+ abi: RollupAbi, -+ data: encodeFunctionData({ -+ abi: RollupAbi, -+ functionName: 'initiateWithdrawByAttesterWithSignature', -+ args: [toViemAttesterExitAuthorization(authorization)], -+ }), -+ }); -+ } -+ -+ /** Relays a signed attester exit batch that reverts unless every authorization can be processed. */ -+ public initiateWithdrawByAttesterBatch( -+ l1TxUtils: L1TxUtils, -+ authorizations: AttesterExitAuthorization[], -+ ): ReturnType { -+ return l1TxUtils.sendAndMonitorTransaction({ -+ to: this.address, -+ abi: RollupAbi, -+ data: encodeFunctionData({ -+ abi: RollupAbi, -+ functionName: 'initiateWithdrawByAttesterBatch', -+ args: [authorizations.map(toViemAttesterExitAuthorization)], -+ }), -+ }); -+ } -+ -+ /** Relays the largest permitted prefix of a signed attester exit batch. */ -+ public initiateWithdrawByAttesterBatchUpToLimit( -+ l1TxUtils: L1TxUtils, -+ authorizations: AttesterExitAuthorization[], -+ ): ReturnType { -+ return l1TxUtils.sendAndMonitorTransaction({ -+ to: this.address, -+ abi: RollupAbi, -+ data: encodeFunctionData({ -+ abi: RollupAbi, -+ functionName: 'initiateWithdrawByAttesterBatchUpToLimit', -+ args: [authorizations.map(toViemAttesterExitAuthorization)], -+ }), -+ }); -+ } -+ - async getStakingAsset(): Promise { - return EthAddress.fromString(await this.rollup.read.getStakingAsset()); - }