diff --git a/docs/docs-developers/docs/aztec-js/how_to_pay_fees.md b/docs/docs-developers/docs/aztec-js/how_to_pay_fees.md index 7209f9b2d98f..f90f3a9e52b3 100644 --- a/docs/docs-developers/docs/aztec-js/how_to_pay_fees.md +++ b/docs/docs-developers/docs/aztec-js/how_to_pay_fees.md @@ -23,13 +23,11 @@ This guide walks you through paying transaction fees on Aztec using various paym | Method | Use Case | Privacy | Requirements | | ------------------- | ----------------------------- | ------- | -------------------------- | | Fee Juice (default) | Account already has Fee Juice | Public | Funded account | -#if(testnet) -| Sponsored FPC | Testing, free transactions | Public | None (not on testnet) | -#else +#if(devnet) | Sponsored FPC | Testing, free transactions | Public | None | +#else +| Sponsored FPC | Testing, free transactions | Public | None (devnet and local only) | #endif -| Private FPC | Pay with tokens privately | Private | Token balance, FPC address | -| Public FPC | Pay with tokens publicly | Public | Token balance, FPC address | | Bridge + Claim | Bootstrap from L1 | Public | L1 ETH for gas | ## Mana and Fee Juice @@ -118,20 +116,22 @@ console.log("Transaction fee:", receipt.transactionFee); ## Use Fee Payment Contracts -Fee Payment Contracts (FPC) pay fees on your behalf, typically accepting a different token than Fee Juice. Since Fee Juice is non-transferable on L2, FPCs are the most common fee payment method. +Fee Payment Contracts (FPCs) pay Fee Juice on your behalf. FPCs must use Fee Juice exclusively on L2 during the setup phase; custom token contract functions cannot be called during setup on public networks. An FPC that accepts other tokens on L1 and bridges Fee Juice works on any network. ### Sponsored Fee Payment Contracts #if(testnet) -:::warning -The Sponsored FPC is **not** deployed on testnet. To pay fees, you must either [bridge Fee Juice from L1](#bridge-fee-juice-from-l1) or deploy your own fee-paying contract. +:::note +The Sponsored FPC is not available on testnet or mainnet. It is only available on devnet and local network. +::: +#elif(mainnet) +:::note +The Sponsored FPC is not available on mainnet. It is only available on devnet and local network. ::: - -The Sponsored FPC pays for fees unconditionally without requiring payment in return. It is available on the local network and devnet (deployed by Aztec Labs), but **not on testnet**. -#else -The Sponsored FPC pays for fees unconditionally without requiring payment in return. It is available on both the local network and devnet (deployed by Aztec Labs). #endif +The Sponsored FPC pays fees unconditionally. It is only available on devnet and local network. + You can derive the Sponsored FPC address from its deployment parameters, register it with your wallet, and use it to pay for transactions: #include_code deploy_sponsored_fpc_contract /docs/examples/ts/aztecjs_advanced/index.ts typescript @@ -140,39 +140,6 @@ Here's a simpler example from the test suite: #include_code sponsored_fpc_simple yarn-project/end-to-end/src/e2e_fees/sponsored_payments.test.ts typescript -### Use other Fee Paying Contracts - -Third-party FPCs can pay for your fees using custom logic, such as accepting different tokens instead of Fee Juice. - -#### Set gas settings - -```typescript -import { GasSettings } from "@aztec/stdlib/gas"; - -// node is from createAztecNodeClient() in the connection guide (see prerequisites) -const maxFeesPerGas = (await node.getCurrentMinFees()).mul(1.5); //adjust this to your needs -const gasSettings = GasSettings.default({ maxFeesPerGas }); -``` - -Private FPCs enable fee payments without revealing the payer's identity onchain: - -#include_code private_fpc_payment yarn-project/end-to-end/src/composed/e2e_local_network_example.test.ts typescript - -Public FPCs can be used in the same way: - -```typescript -import { PublicFeePaymentMethod } from "@aztec/aztec.js/fee"; - -// wallet is from the connection guide; fpcAddress is the FPC contract address -// senderAddress is the account paying; gasSettings is from the step above -const paymentMethod = new PublicFeePaymentMethod( - fpcAddress, - senderAddress, - wallet, - gasSettings, -); -``` - ## Bridge Fee Juice from L1 Fee Juice is non-transferable on L2, but you can bridge it from L1, claim it on L2, and use it. This involves a few components that are part of a running network's infrastructure: diff --git a/docs/docs-developers/docs/foundational-topics/fees.md b/docs/docs-developers/docs/foundational-topics/fees.md index 74992f1b03ba..884b211d6397 100644 --- a/docs/docs-developers/docs/foundational-topics/fees.md +++ b/docs/docs-developers/docs/foundational-topics/fees.md @@ -83,12 +83,9 @@ Fee Juice uses an enshrined `FeeJuicePortal` contract on Ethereum for bridging, An account with Fee Juice can pay for its transactions directly. A new account can even pay for its own deployment transaction, provided Fee Juice was bridged to its address before deployment. -Alternatively, accounts can use [fee-paying contracts (FPCs)](../aztec-js/how_to_pay_fees.md#use-fee-payment-contracts) to pay for transactions. FPCs accept tokens and pay fees in Fee Juice on behalf of users. Common patterns include: +Alternatively, accounts can use [fee-paying contracts (FPCs)](../aztec-js/how_to_pay_fees.md#use-fee-payment-contracts) to pay for transactions. FPCs must use Fee Juice exclusively on L2 during the setup phase, but can accept other tokens on L1 and bridge Fee Juice. -- **Sponsored FPCs**: Pay fees unconditionally, enabling free transactions for users -- **Token-accepting FPCs**: Accept a specific token in exchange for paying fees - -FPCs can contain arbitrary logic to authorize fee payments and can operate privately or publicly. +The **Sponsored FPC** pays fees unconditionally, enabling free transactions. It is only available on devnet and local network. ### Teardown phase diff --git a/docs/docs-developers/docs/resources/migration_notes.md b/docs/docs-developers/docs/resources/migration_notes.md index 40df1255046e..0017a6806288 100644 --- a/docs/docs-developers/docs/resources/migration_notes.md +++ b/docs/docs-developers/docs/resources/migration_notes.md @@ -9,6 +9,71 @@ Aztec is in active development. Each version may introduce breaking changes that ## TBD +### [PXE] `simulateTx`, `executeUtility`, `profileTx`, and `proveTx` no longer accept `scopes: 'ALL_SCOPES'` + +The `AccessScopes` type (`'ALL_SCOPES' | AztecAddress[]`) has been removed. The `scopes` field in `SimulateTxOpts`, +`ExecuteUtilityOpts`, and `ProfileTxOpts` now requires an explicit `AztecAddress[]`. Callers that previously passed +`'ALL_SCOPES'` must now specify which addresses will be in scope for the call. + +**Migration:** + +```diff ++ const accounts = await pxe.getRegisteredAccounts(); ++ const scopes = accounts.map(a => a.address); + + // simulateTx +- await pxe.simulateTx(txRequest, { simulatePublic: true, scopes: 'ALL_SCOPES' }); ++ await pxe.simulateTx(txRequest, { simulatePublic: true, scopes }); + + // executeUtility +- await pxe.executeUtility(call, { scopes: 'ALL_SCOPES' }); ++ await pxe.executeUtility(call, { scopes }); + + // profileTx +- await pxe.profileTx(txRequest, { profileMode: 'full', scopes: 'ALL_SCOPES' }); ++ await pxe.profileTx(txRequest, { profileMode: 'full', scopes }); + + // proveTx +- await pxe.proveTx(txRequest, 'ALL_SCOPES'); ++ await pxe.proveTx(txRequest, scopes); +``` + +**Impact**: Any code passing `'ALL_SCOPES'` to `simulateTx`, `executeUtility`, `profileTx`, or `proveTx` will fail to compile. Replace with an explicit array of account addresses. + +### [PXE] Capsule operations are now scope-enforced at the PXE level + +The PXE now enforces that capsule operations can only access scopes that were authorized for the current execution. If a contract attempts to access a capsule scope that is not in its allowed scopes list, the PXE will throw an error: + +``` +Scope 0x1234... is not in the allowed scopes list: [0xabcd...]. +``` + +The zero address (`AztecAddress::zero()`) is always allowed regardless of the scopes list, preserving backwards compatibility for contracts using the global scope. + +**Impact**: Contracts that access capsules scoped to addresses not included in the transaction's authorized scopes will now fail at runtime. Ensure the correct scopes are passed when executing transactions. + +## 4.2.0-aztecnr-rc.2 + +### Custom token FPCs removed from default public setup allowlist + +Token contract functions (like `transfer_in_public` and `_increase_public_balance`) have been removed from the default public setup allowlist. FPCs that accept custom tokens (like the reference `FPC` contract) will not work on public networks, because their setup-phase calls to these functions will be rejected. Token class IDs change with each aztec-nr release, making it impractical to maintain them in the allowlist. + +FPCs that use only Fee Juice still work on all networks, since FeeJuice is a protocol contract with a fixed address in the allowlist. Custom FPCs should only call protocol contract functions (AuthRegistry, FeeJuice) during setup. + +`PublicFeePaymentMethod` and `PrivateFeePaymentMethod` in aztec.js are affected, since they use the reference `FPC` contract which calls Token functions during setup. Switch to `FeeJuicePaymentMethodWithClaim` (after [bridging Fee Juice from L1](../aztec-js/how_to_pay_fees.md#bridge-fee-juice-from-l1)) or write an FPC that uses Fee Juice natively. + +**Migration:** + +```diff +- import { PublicFeePaymentMethod } from '@aztec/aztec.js/fee'; +- const paymentMethod = new PublicFeePaymentMethod(fpcAddress, senderAddress, wallet, gasSettings); ++ import { FeeJuicePaymentMethodWithClaim } from '@aztec/aztec.js/fee'; ++ const paymentMethod = new FeeJuicePaymentMethodWithClaim(senderAddress, claim); +``` + +Similarly, the `fpc-public` and `fpc-private` CLI wallet payment methods use the reference Token-based FPC and will not work on public networks. Use `fee_juice` for direct Fee Juice payment, or `fpc-sponsored` on devnet and local network. + + ### [Aztec.nr] Domain-separated tags on log emission All logs emitted through the Aztec.nr framework now include a domain-separated tag at `fields[0]`. Each log category uses its own domain separator via `compute_log_tag(raw_tag, dom_sep)`: @@ -118,6 +183,8 @@ The `DeployTxReceipt` and `DeployWaitOptions` types have been removed. + from: address, + }); ``` + + ### [aztec.js] `isContractInitialized` is now `initializationStatus` tri-state enum `ContractMetadata.isContractInitialized` has been renamed to `ContractMetadata.initializationStatus` and changed from `boolean | undefined` to a `ContractInitializationStatus` enum with values `INITIALIZED`, `UNINITIALIZED`, and `UNKNOWN`. diff --git a/docs/docs-developers/docs/tutorials/contract_tutorials/counter_contract.md b/docs/docs-developers/docs/tutorials/contract_tutorials/counter_contract.md index 0f8a7f672b99..77e26bf0afa9 100644 --- a/docs/docs-developers/docs/tutorials/contract_tutorials/counter_contract.md +++ b/docs/docs-developers/docs/tutorials/contract_tutorials/counter_contract.md @@ -35,9 +35,9 @@ Your structure should look like this: | |-Nargo.toml ``` -The file `main.nr` will soon turn into our smart contract! +The `aztec new` command creates a contract project with `Nargo.toml` and `src/main.nr`. The file `src/main.nr` will soon turn into our smart contract! -Add the following dependencies to `Nargo.toml` under the autogenerated content: +Add the following dependency to `Nargo.toml` under the existing `aztec` dependency: ```toml [dependencies] diff --git a/docs/docs-developers/docs/tutorials/contract_tutorials/recursive_verification.md b/docs/docs-developers/docs/tutorials/contract_tutorials/recursive_verification.md index 250b8fb90e1e..4d9be3222241 100644 --- a/docs/docs-developers/docs/tutorials/contract_tutorials/recursive_verification.md +++ b/docs/docs-developers/docs/tutorials/contract_tutorials/recursive_verification.md @@ -454,7 +454,10 @@ The proof generation script executes the circuit offchain and produces the proof Create `scripts/generate_data.ts`: -#include_code generate_data /docs/examples/ts/recursive_verification/scripts/generate_data.ts typescript +```js +import circuitJson from "../circuit/target/hello_circuit.json" with { type: "json" }; +#include_code generate_data /docs/examples/ts/recursive_verification/scripts/generate_data.ts raw +``` ### Understanding the Proof Generation Pipeline diff --git a/docs/docs-developers/docs/tutorials/contract_tutorials/token_contract.md b/docs/docs-developers/docs/tutorials/contract_tutorials/token_contract.md index 5e120d1c1f49..f13ced10715e 100644 --- a/docs/docs-developers/docs/tutorials/contract_tutorials/token_contract.md +++ b/docs/docs-developers/docs/tutorials/contract_tutorials/token_contract.md @@ -44,13 +44,13 @@ cd bob_token yarn init # This is to ensure yarn uses node_modules instead of pnp for dependency installation yarn config set nodeLinker node-modules -yarn add @aztec/aztec.js@#include_aztec_version @aztec/accounts@#include_aztec_version @aztec/test-wallet@#include_aztec_version @aztec/kv-store@#include_aztec_version +yarn add @aztec/aztec.js@#include_aztec_version @aztec/accounts@#include_aztec_version @aztec/kv-store@#include_aztec_version aztec init ``` ## Contract structure -The `aztec init` command created a workspace with two crates: a `bob_token_contract` crate for your smart contract code and a `bob_token_test` crate for Noir tests. In `bob_token_contract/src/main.nr` we even have a proto-contract. Let's replace it with a simple starting point: +The `aztec init` command created a contract project with `Nargo.toml` and `src/main.nr`. Let's replace the boilerplate in `src/main.nr` with a simple starting point: ```rust #include_code start /docs/examples/contracts/bob_token_contract/src/main.nr raw diff --git a/docs/docs-participate/basics/fees.md b/docs/docs-participate/basics/fees.md index 4b67f66c6209..82d9b51de0f0 100644 --- a/docs/docs-participate/basics/fees.md +++ b/docs/docs-participate/basics/fees.md @@ -62,10 +62,7 @@ Aztec offers flexible fee payment: If you have $AZTEC, pay for your own transactions directly from your account. ### Sponsored Transactions -Some applications pay fees on behalf of their users, enabling "free" transactions. The application covers the cost, not you. - -### Fee-Paying Contracts -Specialized contracts can accept other tokens and pay fees in $AZTEC for you. This is useful if you only hold other tokens. +Fee-paying contracts can pay fees on your behalf. For example, on devnet and local network, a sponsored fee-paying contract covers transaction costs for free. FPCs can also accept other tokens on L1 and bridge $AZTEC to pay fees. ## Understanding Your Fee diff --git a/docs/examples/ts/recursive_verification/scripts/generate_data.ts b/docs/examples/ts/recursive_verification/scripts/generate_data.ts index 46038617dd70..e948a761d2f0 100644 --- a/docs/examples/ts/recursive_verification/scripts/generate_data.ts +++ b/docs/examples/ts/recursive_verification/scripts/generate_data.ts @@ -1,6 +1,6 @@ +import circuitJson from "../../../../target/hello_circuit.json" with { type: "json" }; // docs:start:generate_data import { Noir } from "@aztec/noir-noir_js"; -import circuitJson from "../../../../target/hello_circuit.json" with { type: "json" }; import { Barretenberg, UltraHonkBackend, deflattenFields } from "@aztec/bb.js"; import fs from "fs"; import { exit } from "process"; @@ -56,14 +56,14 @@ if (proofAsFields.length === 0) { const vkAsFields = recursiveArtifacts.vkAsFields; console.log(`VK size: ${vkAsFields.length}`); // Should be 115 -console.log(`Proof size: ${proofAsFields.length}`); // Should be 508 +console.log(`Proof size: ${proofAsFields.length}`); // Should be ~500 console.log(`Public inputs: ${mainProofData.publicInputs.length}`); // Should be 1 // Step 9: Save all data to JSON for contract interaction const data = { vkAsFields: vkAsFields, // 115 field elements - the verification key vkHash: recursiveArtifacts.vkHash, // Hash of VK - stored in contract - proofAsFields: proofAsFields, // 508 field elements - the proof + proofAsFields: proofAsFields, // ~500 field elements - the proof publicInputs: mainProofData.publicInputs.map((p: string) => p.toString()), }; diff --git a/docs/examples/ts/token_bridge/index.ts b/docs/examples/ts/token_bridge/index.ts index 436a4be41dde..728f285164bc 100644 --- a/docs/examples/ts/token_bridge/index.ts +++ b/docs/examples/ts/token_bridge/index.ts @@ -162,7 +162,7 @@ const INBOX_ABI = [ type: "event", name: "MessageSent", inputs: [ - { name: "l2BlockNumber", type: "uint256", indexed: true }, + { name: "checkpointNumber", type: "uint256", indexed: true }, { name: "index", type: "uint256", indexed: false }, { name: "hash", type: "bytes32", indexed: true }, { name: "rollingHash", type: "bytes16", indexed: false }, diff --git a/docs/netlify.toml b/docs/netlify.toml index 9d3d41270b6e..a2908957123d 100644 --- a/docs/netlify.toml +++ b/docs/netlify.toml @@ -805,3 +805,13 @@ # PXE: incompatible oracle version between contract and PXE from = "/errors/8" to = "/developers/docs/foundational-topics/pxe" + +[[redirects]] + # CLI: aztec dep version in Nargo.toml does not match the CLI version + from = "/errors/9" + to = "/developers/docs/aztec-nr/framework-description/dependencies#updating-your-aztec-dependencies" + +[[redirects]] + # PXE: capsule operation attempted with a scope not in the allowed scopes list + from = "/errors/10" + to = "/developers/docs/aztec-nr/framework-description/advanced/how_to_use_capsules" diff --git a/yarn-project/archiver/src/modules/l1_synchronizer.ts b/yarn-project/archiver/src/modules/l1_synchronizer.ts index 802576d726b8..1861c8a67c8c 100644 --- a/yarn-project/archiver/src/modules/l1_synchronizer.ts +++ b/yarn-project/archiver/src/modules/l1_synchronizer.ts @@ -256,8 +256,11 @@ export class ArchiverL1Synchronizer implements Traceable { }, ); } - } catch (err) { - this.log.warn(`Failed to update finalized checkpoint: ${err}`); + } catch (err: any) { + // The rollup contract may not exist at the finalized L1 block right after deployment. + if (!err?.message?.includes('returned no data')) { + this.log.warn(`Failed to update finalized checkpoint: ${err}`); + } } } diff --git a/yarn-project/aztec/src/testing/anvil_test_watcher.ts b/yarn-project/aztec/src/testing/anvil_test_watcher.ts index 44ef6662a9c8..4eabc6583949 100644 --- a/yarn-project/aztec/src/testing/anvil_test_watcher.ts +++ b/yarn-project/aztec/src/testing/anvil_test_watcher.ts @@ -130,7 +130,7 @@ export class AnvilTestWatcher { return; } - const l1Time = (await this.cheatcodes.timestamp()) * 1000; + const l1Time = (await this.cheatcodes.lastBlockTimestamp()) * 1000; const wallTime = this.dateProvider.now(); if (l1Time > wallTime) { this.logger.warn(`L1 is ahead of wall time. Syncing wall time to L1 time`); diff --git a/yarn-project/aztec/src/testing/cheat_codes.ts b/yarn-project/aztec/src/testing/cheat_codes.ts index 94ebe0b87046..14413fd4ddd3 100644 --- a/yarn-project/aztec/src/testing/cheat_codes.ts +++ b/yarn-project/aztec/src/testing/cheat_codes.ts @@ -72,7 +72,7 @@ export class CheatCodes { * @param duration - The duration to advance time by (in seconds) */ async warpL2TimeAtLeastBy(sequencerClient: SequencerClient, node: AztecNode, duration: bigint | number) { - const currentTimestamp = await this.eth.timestamp(); + const currentTimestamp = await this.eth.lastBlockTimestamp(); const targetTimestamp = BigInt(currentTimestamp) + BigInt(duration); await this.warpL2TimeAtLeastTo(sequencerClient, node, targetTimestamp); } diff --git a/yarn-project/cli-wallet/src/cmds/check_tx.ts b/yarn-project/cli-wallet/src/cmds/check_tx.ts index 1ff5ef54dbfe..749633c9422b 100644 --- a/yarn-project/cli-wallet/src/cmds/check_tx.ts +++ b/yarn-project/cli-wallet/src/cmds/check_tx.ts @@ -1,5 +1,5 @@ import type { ContractArtifact } from '@aztec/aztec.js/abi'; -import type { AztecAddress } from '@aztec/aztec.js/addresses'; +import { AztecAddress } from '@aztec/aztec.js/addresses'; import { Fr } from '@aztec/aztec.js/fields'; import type { AztecNode } from '@aztec/aztec.js/node'; import { ProtocolContractAddress } from '@aztec/aztec.js/protocol'; @@ -87,12 +87,13 @@ async function inspectTx(wallet: CLIWallet, aztecNode: AztecNode, txHash: TxHash // Nullifiers const nullifierCount = effects.nullifiers.length; const { deployNullifiers, initNullifiers, classNullifiers } = await getKnownNullifiers(wallet, artifactMap); + const accounts = (await wallet.getAccounts()).map(a => a.item); if (nullifierCount > 0) { log(' Nullifiers:'); for (const nullifier of effects.nullifiers) { const deployed = deployNullifiers[nullifier.toString()]; const note = deployed - ? (await wallet.getNotes({ siloedNullifier: nullifier, contractAddress: deployed, scopes: 'ALL_SCOPES' }))[0] + ? (await wallet.getNotes({ siloedNullifier: nullifier, contractAddress: deployed, scopes: accounts }))[0] : undefined; const initialized = initNullifiers[nullifier.toString()]; const registered = classNullifiers[nullifier.toString()]; diff --git a/yarn-project/cli-wallet/src/utils/wallet.ts b/yarn-project/cli-wallet/src/utils/wallet.ts index 3768dc272267..d339a4d82026 100644 --- a/yarn-project/cli-wallet/src/utils/wallet.ts +++ b/yarn-project/cli-wallet/src/utils/wallet.ts @@ -56,7 +56,12 @@ export class CLIWallet extends BaseWallet { override async getAccounts(): Promise[]> { const accounts = (await this.db?.listAliases('accounts')) ?? []; - return Promise.resolve(accounts.map(({ key, value }) => ({ alias: value, item: AztecAddress.fromString(key) }))); + return Promise.resolve( + accounts.map(({ key, value }) => { + const alias = key.includes(':') ? key.slice(key.indexOf(':') + 1) : key; + return { alias, item: AztecAddress.fromString(value) }; + }), + ); } private async createCancellationTxExecutionRequest( diff --git a/yarn-project/end-to-end/src/e2e_cheat_codes.test.ts b/yarn-project/end-to-end/src/e2e_cheat_codes.test.ts index 27b0bf124dfc..5a4646542537 100644 --- a/yarn-project/end-to-end/src/e2e_cheat_codes.test.ts +++ b/yarn-project/end-to-end/src/e2e_cheat_codes.test.ts @@ -46,19 +46,19 @@ describe('e2e_cheat_codes', () => { it.each([100, 42, 99])(`setNextBlockTimestamp by %i`, async increment => { const blockNumber = await ethCheatCodes.blockNumber(); - const timestamp = await ethCheatCodes.timestamp(); + const timestamp = await ethCheatCodes.lastBlockTimestamp(); await ethCheatCodes.setNextBlockTimestamp(timestamp + increment); - expect(await ethCheatCodes.timestamp()).toBe(timestamp); + expect(await ethCheatCodes.lastBlockTimestamp()).toBe(timestamp); await ethCheatCodes.mine(); expect(await ethCheatCodes.blockNumber()).toBe(blockNumber + 1); - expect(await ethCheatCodes.timestamp()).toBe(timestamp + increment); + expect(await ethCheatCodes.lastBlockTimestamp()).toBe(timestamp + increment); }); it('setNextBlockTimestamp to a past timestamp throws', async () => { - const timestamp = await ethCheatCodes.timestamp(); + const timestamp = await ethCheatCodes.lastBlockTimestamp(); const pastTimestamp = timestamp - 1000; await expect(async () => await ethCheatCodes.setNextBlockTimestamp(pastTimestamp)).rejects.toThrow( 'Timestamp error', diff --git a/yarn-project/end-to-end/src/e2e_crowdfunding_and_claim.test.ts b/yarn-project/end-to-end/src/e2e_crowdfunding_and_claim.test.ts index 68711711b23b..5668ba4e3e77 100644 --- a/yarn-project/end-to-end/src/e2e_crowdfunding_and_claim.test.ts +++ b/yarn-project/end-to-end/src/e2e_crowdfunding_and_claim.test.ts @@ -60,7 +60,7 @@ describe('e2e_crowdfunding_and_claim', () => { } = await setup(3)); // We set the deadline to a week from now - deadline = (await cheatCodes.eth.timestamp()) + 7 * 24 * 60 * 60; + deadline = (await cheatCodes.eth.lastBlockTimestamp()) + 7 * 24 * 60 * 60; ({ contract: donationToken } = await TokenContract.deploy( wallet, diff --git a/yarn-project/end-to-end/src/e2e_epochs/epochs_mbps.parallel.test.ts b/yarn-project/end-to-end/src/e2e_epochs/epochs_mbps.parallel.test.ts index c4261f46420b..0f293e34e4aa 100644 --- a/yarn-project/end-to-end/src/e2e_epochs/epochs_mbps.parallel.test.ts +++ b/yarn-project/end-to-end/src/e2e_epochs/epochs_mbps.parallel.test.ts @@ -15,6 +15,7 @@ import { CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types'; import { times, timesAsync } from '@aztec/foundation/collection'; import { SecretValue } from '@aztec/foundation/config'; import { retryUntil } from '@aztec/foundation/retry'; +import { sleep } from '@aztec/foundation/sleep'; import { bufferToHex } from '@aztec/foundation/string'; import { executeTimeout } from '@aztec/foundation/timer'; import { TestContract } from '@aztec/noir-test-contracts.js/Test'; @@ -149,6 +150,20 @@ describe('e2e_epochs/epochs_mbps', () => { /** Retrieves all checkpoints from the archiver, checks that one has the target block count, and returns its number. */ async function assertMultipleBlocksPerSlot(targetBlockCount: number, logger: Logger): Promise { + // Wait for the first validator's archiver to index a checkpoint with the target block count. + // waitForTx polls the initial setup node, but this archiver belongs to nodes[0] (the first + // validator). They sync L1 independently, so there's a race window of ~200-400ms. + const waitTimeout = test.L2_SLOT_DURATION_IN_S * 3; + await retryUntil( + async () => { + const checkpoints = await archiver.getCheckpoints(CheckpointNumber(1), 50); + return checkpoints.some(pc => pc.checkpoint.blocks.length >= targetBlockCount) || undefined; + }, + `checkpoint with at least ${targetBlockCount} blocks`, + waitTimeout, + 0.5, + ); + const checkpoints = await archiver.getCheckpoints(CheckpointNumber(1), 50); logger.warn(`Retrieved ${checkpoints.length} checkpoints from archiver`, { checkpoints: checkpoints.map(pc => pc.checkpoint.getStats()), @@ -538,11 +553,20 @@ describe('e2e_epochs/epochs_mbps', () => { }); await waitUntilL1Timestamp(test.l1Client, targetTimestamp, undefined, test.L2_SLOT_DURATION_IN_S * 3); - // Send both pre-proved txs simultaneously, waiting for them to be checkpointed. + // Send the deploy tx first and give it time to propagate to all validators, + // then send the call tx. Priority fees are a safety net, but arrival ordering + // ensures the deploy tx is in the pool before the call tx regardless of gossip timing. const timeout = test.L2_SLOT_DURATION_IN_S * 5; - logger.warn(`Sending both txs and waiting for checkpointed receipts`); + logger.warn(`Sending deploy tx first, then call tx`); + const deployTxHash = await deployTx.send({ wait: NO_WAIT }); + await sleep(1000); + const callTxHash = await callTx.send({ wait: NO_WAIT }); const [deployReceipt, callReceipt] = await executeTimeout( - () => Promise.all([deployTx.send({ wait: { timeout } }), callTx.send({ wait: { timeout } })]), + () => + Promise.all([ + waitForTx(context.aztecNode, deployTxHash, { timeout }), + waitForTx(context.aztecNode, callTxHash, { timeout }), + ]), timeout * 1000, ); logger.warn(`Both txs checkpointed`, { diff --git a/yarn-project/end-to-end/src/e2e_epochs/epochs_missed_l1_slot.test.ts b/yarn-project/end-to-end/src/e2e_epochs/epochs_missed_l1_slot.test.ts index 1fe3b1a9a47f..3e863302f2c3 100644 --- a/yarn-project/end-to-end/src/e2e_epochs/epochs_missed_l1_slot.test.ts +++ b/yarn-project/end-to-end/src/e2e_epochs/epochs_missed_l1_slot.test.ts @@ -101,7 +101,7 @@ describe('e2e_epochs/epochs_missed_l1_slot', () => { await eth.setAutomine(false); await eth.setIntervalMining(0, { silent: true }); - const frozenL1Timestamp = await eth.timestamp(); + const frozenL1Timestamp = await eth.lastBlockTimestamp(); logger.info(`L1 mining paused at L1 timestamp ${frozenL1Timestamp}`); // Step 3: Wait until the sequencer reaches PUBLISHING_CHECKPOINT during the mining pause. diff --git a/yarn-project/end-to-end/src/e2e_l1_publisher/e2e_l1_publisher.test.ts b/yarn-project/end-to-end/src/e2e_l1_publisher/e2e_l1_publisher.test.ts index 7b1c8f050cce..7291a7ce8b78 100644 --- a/yarn-project/end-to-end/src/e2e_l1_publisher/e2e_l1_publisher.test.ts +++ b/yarn-project/end-to-end/src/e2e_l1_publisher/e2e_l1_publisher.test.ts @@ -274,6 +274,7 @@ describe('L1Publisher integration', () => { { l1ChainId: chainId, ethereumSlotDuration: config.ethereumSlotDuration, + aztecSlotDuration: config.aztecSlotDuration, }, { blobClient, @@ -785,7 +786,6 @@ describe('L1Publisher integration', () => { await publisher.enqueueGovernanceCastSignal( l1ContractAddresses.rollupAddress, block.slot, - block.timestamp, EthAddress.random(), (_payload: any) => Promise.resolve(Signature.random().toString()), ); diff --git a/yarn-project/end-to-end/src/e2e_p2p/add_rollup.test.ts b/yarn-project/end-to-end/src/e2e_p2p/add_rollup.test.ts index c885c8a4745c..0db4c4f28055 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/add_rollup.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/add_rollup.test.ts @@ -518,7 +518,7 @@ describe('e2e_p2p_add_rollup', () => { const futureEpoch = EpochNumber.fromBigInt(500n + BigInt(await newRollup.getCurrentEpochNumber())); const futureSlot = SlotNumber.fromBigInt(BigInt(futureEpoch) * BigInt(t.ctx.aztecNodeConfig.aztecEpochDuration)); const time = await newRollup.getTimestampForSlot(futureSlot); - if (time > BigInt(await t.ctx.cheatCodes.eth.timestamp())) { + if (time > BigInt(await t.ctx.cheatCodes.eth.lastBlockTimestamp())) { await t.ctx.cheatCodes.eth.warp(Number(time)); await waitL1Block(); } diff --git a/yarn-project/end-to-end/src/e2e_p2p/broadcasted_invalid_block_proposal_slash.test.ts b/yarn-project/end-to-end/src/e2e_p2p/broadcasted_invalid_block_proposal_slash.test.ts index ed3b62b21c65..96511ea5b511 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/broadcasted_invalid_block_proposal_slash.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/broadcasted_invalid_block_proposal_slash.test.ts @@ -56,6 +56,7 @@ describe('e2e_p2p_broadcasted_invalid_block_proposal_slash', () => { basePort: BOOT_NODE_UDP_PORT, metricsPort: shouldCollectMetrics(), initialConfig: { + anvilSlotsInAnEpoch: 4, listenAddress: '127.0.0.1', aztecEpochDuration, ethereumSlotDuration: ETHEREUM_SLOT_DURATION, diff --git a/yarn-project/end-to-end/src/e2e_p2p/data_withholding_slash.test.ts b/yarn-project/end-to-end/src/e2e_p2p/data_withholding_slash.test.ts index 917dba22df30..45cc1d03b11c 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/data_withholding_slash.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/data_withholding_slash.test.ts @@ -58,6 +58,7 @@ describe('e2e_p2p_data_withholding_slash', () => { basePort: BOOT_NODE_UDP_PORT, metricsPort: shouldCollectMetrics(), initialConfig: { + anvilSlotsInAnEpoch: 4, listenAddress: '127.0.0.1', aztecEpochDuration, ethereumSlotDuration: 4, diff --git a/yarn-project/end-to-end/src/e2e_p2p/duplicate_attestation_slash.test.ts b/yarn-project/end-to-end/src/e2e_p2p/duplicate_attestation_slash.test.ts index 3ebef6ac94da..eb8edad01bc1 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/duplicate_attestation_slash.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/duplicate_attestation_slash.test.ts @@ -68,6 +68,7 @@ describe('e2e_p2p_duplicate_attestation_slash', () => { basePort: BOOT_NODE_UDP_PORT, metricsPort: shouldCollectMetrics(), initialConfig: { + anvilSlotsInAnEpoch: 4, listenAddress: '127.0.0.1', aztecEpochDuration, ethereumSlotDuration: ETHEREUM_SLOT_DURATION, diff --git a/yarn-project/end-to-end/src/e2e_p2p/duplicate_proposal_slash.test.ts b/yarn-project/end-to-end/src/e2e_p2p/duplicate_proposal_slash.test.ts index c0b6062acac6..3f0ed6e6164b 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/duplicate_proposal_slash.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/duplicate_proposal_slash.test.ts @@ -60,6 +60,7 @@ describe('e2e_p2p_duplicate_proposal_slash', () => { basePort: BOOT_NODE_UDP_PORT, metricsPort: shouldCollectMetrics(), initialConfig: { + anvilSlotsInAnEpoch: 4, listenAddress: '127.0.0.1', aztecEpochDuration, ethereumSlotDuration: ETHEREUM_SLOT_DURATION, diff --git a/yarn-project/end-to-end/src/e2e_p2p/inactivity_slash_test.ts b/yarn-project/end-to-end/src/e2e_p2p/inactivity_slash_test.ts index 7897fb1269ad..614528ea757a 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/inactivity_slash_test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/inactivity_slash_test.ts @@ -58,6 +58,7 @@ export class P2PInactivityTest { basePort: BOOT_NODE_UDP_PORT, startProverNode: true, initialConfig: { + anvilSlotsInAnEpoch: 4, proverNodeConfig: { proverNodeEpochProvingDelayMs: AZTEC_SLOT_DURATION * 1000 }, aztecTargetCommitteeSize: COMMITTEE_SIZE, aztecSlotDuration: AZTEC_SLOT_DURATION, diff --git a/yarn-project/end-to-end/src/e2e_p2p/multiple_validators_sentinel.parallel.test.ts b/yarn-project/end-to-end/src/e2e_p2p/multiple_validators_sentinel.parallel.test.ts index 8747f2b7251d..cf778c18196f 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/multiple_validators_sentinel.parallel.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/multiple_validators_sentinel.parallel.test.ts @@ -44,6 +44,7 @@ describe('e2e_p2p_multiple_validators_sentinel', () => { basePort: BOOT_NODE_UDP_PORT, startProverNode: true, initialConfig: { + anvilSlotsInAnEpoch: 4, aztecTargetCommitteeSize: NUM_VALIDATORS, aztecSlotDuration: AZTEC_SLOT_DURATION, ethereumSlotDuration: ETHEREUM_SLOT_DURATION, diff --git a/yarn-project/end-to-end/src/e2e_p2p/slash_veto_demo.test.ts b/yarn-project/end-to-end/src/e2e_p2p/slash_veto_demo.test.ts index 532b94e0dd98..a4e352ab0fff 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/slash_veto_demo.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/slash_veto_demo.test.ts @@ -78,6 +78,7 @@ describe('veto slash', () => { basePort: BOOT_NODE_UDP_PORT, startProverNode: true, initialConfig: { + anvilSlotsInAnEpoch: 4, aztecSlotDuration: AZTEC_SLOT_DURATION, ethereumSlotDuration: ETHEREUM_SLOT_DURATION, aztecProofSubmissionEpochs: 1024, // effectively do not reorg diff --git a/yarn-project/end-to-end/src/e2e_p2p/valid_epoch_pruned_slash.test.ts b/yarn-project/end-to-end/src/e2e_p2p/valid_epoch_pruned_slash.test.ts index 8683138cb02a..6954c9640c3b 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/valid_epoch_pruned_slash.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/valid_epoch_pruned_slash.test.ts @@ -52,6 +52,7 @@ describe('e2e_p2p_valid_epoch_pruned_slash', () => { basePort: BOOT_NODE_UDP_PORT, metricsPort: shouldCollectMetrics(), initialConfig: { + anvilSlotsInAnEpoch: 4, enforceTimeTable: true, cancelTxOnTimeout: false, sequencerPublisherAllowInvalidStates: true, diff --git a/yarn-project/end-to-end/src/e2e_p2p/validators_sentinel.test.ts b/yarn-project/end-to-end/src/e2e_p2p/validators_sentinel.test.ts index 3a4e47afe387..43ddccff833f 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/validators_sentinel.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/validators_sentinel.test.ts @@ -39,6 +39,7 @@ describe('e2e_p2p_validators_sentinel', () => { basePort: BOOT_NODE_UDP_PORT, startProverNode: true, initialConfig: { + anvilSlotsInAnEpoch: 4, aztecTargetCommitteeSize: NUM_VALIDATORS, aztecSlotDuration: AZTEC_SLOT_DURATION, ethereumSlotDuration: ETHEREUM_SLOT_DURATION, diff --git a/yarn-project/end-to-end/src/e2e_sequencer/slasher_config.test.ts b/yarn-project/end-to-end/src/e2e_sequencer/slasher_config.test.ts index 57c72796020c..4a2ff9bdfab2 100644 --- a/yarn-project/end-to-end/src/e2e_sequencer/slasher_config.test.ts +++ b/yarn-project/end-to-end/src/e2e_sequencer/slasher_config.test.ts @@ -10,6 +10,7 @@ describe('e2e_slasher_config', () => { beforeAll(async () => { ({ aztecNodeAdmin, aztecNode } = await setup(0, { + anvilSlotsInAnEpoch: 4, slashInactivityTargetPercentage: 1, slashInactivityPenalty: 42n, })); diff --git a/yarn-project/end-to-end/src/e2e_synching.test.ts b/yarn-project/end-to-end/src/e2e_synching.test.ts index e03737b2669a..5f57afed4fd8 100644 --- a/yarn-project/end-to-end/src/e2e_synching.test.ts +++ b/yarn-project/end-to-end/src/e2e_synching.test.ts @@ -443,6 +443,7 @@ describe('e2e_synching', () => { { l1ChainId: 31337, ethereumSlotDuration: ETHEREUM_SLOT_DURATION, + aztecSlotDuration: getL1ContractsConfigEnvVars().aztecSlotDuration, }, { blobClient, @@ -466,7 +467,7 @@ describe('e2e_synching', () => { for (const checkpoint of checkpoints) { const lastBlock = checkpoint.blocks.at(-1)!; const targetTime = Number(lastBlock.header.globalVariables.timestamp) - ETHEREUM_SLOT_DURATION; - while ((await cheatCodes.eth.timestamp()) < targetTime) { + while ((await cheatCodes.eth.lastBlockTimestamp()) < targetTime) { await cheatCodes.eth.mine(); } // If it breaks here, first place you should look is the pruning. diff --git a/yarn-project/end-to-end/src/fixtures/setup.ts b/yarn-project/end-to-end/src/fixtures/setup.ts index 630f783bfca8..ffef7e66db10 100644 --- a/yarn-project/end-to-end/src/fixtures/setup.ts +++ b/yarn-project/end-to-end/src/fixtures/setup.ts @@ -302,6 +302,8 @@ export async function setup( config.dataDirectory = directoryToCleanup; } + const dateProvider = new TestDateProvider(); + if (!config.l1RpcUrls?.length) { if (!isAnvilTestChain(chain.id)) { throw new Error(`No ETHEREUM_HOSTS set but non anvil chain requested`); @@ -311,6 +313,7 @@ export async function setup( accounts: opts.anvilAccounts, port: opts.anvilPort ?? (process.env.ANVIL_PORT ? parseInt(process.env.ANVIL_PORT) : undefined), slotsInAnEpoch: opts.anvilSlotsInAnEpoch, + dateProvider, }); anvil = res.anvil; config.l1RpcUrls = [res.rpcUrl]; @@ -322,8 +325,6 @@ export async function setup( logger.info(`Logging metrics to ${filename}`); setupMetricsLogger(filename); } - - const dateProvider = new TestDateProvider(); const ethCheatCodes = new EthCheatCodesWithState(config.l1RpcUrls, dateProvider); if (opts.stateLoad) { @@ -419,11 +420,12 @@ export async function setup( await ethCheatCodes.setIntervalMining(config.ethereumSlotDuration); } - // Always sync dateProvider to L1 time after deploying L1 contracts, regardless of mining mode. - // In compose mode, L1 time may have drifted ahead of system time due to the local-network watcher - // warping time forward on each filled slot. Without this sync, the sequencer computes the wrong - // slot from its dateProvider and cannot propose blocks. - dateProvider.setTime((await ethCheatCodes.timestamp()) * 1000); + // In compose mode (no local anvil), sync dateProvider to L1 time since it may have drifted + // ahead of system time due to the local-network watcher warping time forward on each filled slot. + // When running with a local anvil, the dateProvider is kept in sync via the stdout listener. + if (!anvil) { + dateProvider.setTime((await ethCheatCodes.lastBlockTimestamp()) * 1000); + } if (opts.l2StartTime) { await ethCheatCodes.warp(opts.l2StartTime, { resetBlockInterval: true }); diff --git a/yarn-project/end-to-end/src/simulators/lending_simulator.ts b/yarn-project/end-to-end/src/simulators/lending_simulator.ts index 404bb3d5ad8d..ae299b31e249 100644 --- a/yarn-project/end-to-end/src/simulators/lending_simulator.ts +++ b/yarn-project/end-to-end/src/simulators/lending_simulator.ts @@ -94,7 +94,9 @@ export class LendingSimulator { async prepare() { this.accumulator = BASE; - const slot = await this.rollup.getSlotAt(BigInt(await this.cc.eth.timestamp()) + BigInt(this.ethereumSlotDuration)); + const slot = await this.rollup.getSlotAt( + BigInt(await this.cc.eth.lastBlockTimestamp()) + BigInt(this.ethereumSlotDuration), + ); this.time = Number(await this.rollup.getTimestampForSlot(slot)); } @@ -103,7 +105,7 @@ export class LendingSimulator { return; } - const slot = await this.rollup.getSlotAt(BigInt(await this.cc.eth.timestamp())); + const slot = await this.rollup.getSlotAt(BigInt(await this.cc.eth.lastBlockTimestamp())); const targetSlot = SlotNumber(slot + diff); const ts = Number(await this.rollup.getTimestampForSlot(targetSlot)); const timeDiff = ts - this.time; diff --git a/yarn-project/ethereum/src/test/eth_cheat_codes.ts b/yarn-project/ethereum/src/test/eth_cheat_codes.ts index d62b194c0241..b1cbdd199d6f 100644 --- a/yarn-project/ethereum/src/test/eth_cheat_codes.ts +++ b/yarn-project/ethereum/src/test/eth_cheat_codes.ts @@ -85,10 +85,12 @@ export class EthCheatCodes { } /** - * Get the current timestamp - * @returns The current timestamp + * Get the timestamp of the latest mined L1 block. + * Note: this is NOT the current time — it's the discrete timestamp of the last block. + * Between blocks, the actual chain time advances but no new block reflects it. + * @returns The latest block timestamp in seconds */ - public async timestamp(): Promise { + public async lastBlockTimestamp(): Promise { const res = await this.doRpcCall('eth_getBlockByNumber', ['latest', true]); return parseInt(res.timestamp, 16); } @@ -552,7 +554,7 @@ export class EthCheatCodes { } public async syncDateProvider() { - const timestamp = await this.timestamp(); + const timestamp = await this.lastBlockTimestamp(); if ('setTime' in this.dateProvider) { this.dateProvider.setTime(timestamp * 1000); } diff --git a/yarn-project/ethereum/src/test/start_anvil.test.ts b/yarn-project/ethereum/src/test/start_anvil.test.ts index 842733dd8cb5..cb0d70f61dbb 100644 --- a/yarn-project/ethereum/src/test/start_anvil.test.ts +++ b/yarn-project/ethereum/src/test/start_anvil.test.ts @@ -1,5 +1,6 @@ import { type Logger, createLogger } from '@aztec/foundation/log'; import { sleep } from '@aztec/foundation/sleep'; +import { TestDateProvider } from '@aztec/foundation/timer'; import { createPublicClient, http, parseAbiItem } from 'viem'; @@ -53,4 +54,40 @@ describe('start_anvil', () => { stopWatching(); }, 500); }); + + it('syncs dateProvider to anvil block time on each mined block', async () => { + // Stop the default anvil instance (no dateProvider). + await anvil.stop(); + + const dateProvider = new TestDateProvider(); + const res = await startAnvil({ dateProvider }); + anvil = res.anvil; + rpcUrl = res.rpcUrl; + + const publicClient = createPublicClient({ transport: http(rpcUrl, { batch: false }) }); + + // Mine a block so anvil emits a "Block Time" line. + await publicClient.request({ method: 'evm_mine', params: [] } as any); + // Give the stdout listener time to fire. + await sleep(200); + + const block = await publicClient.getBlock({ blockTag: 'latest' }); + const blockTimeMs = Number(block.timestamp) * 1000; + // The dateProvider should now be within 2 seconds of the anvil block time. + // TestDateProvider.now() = Date.now() + offset, and setTime sets offset = blockTimeMs - Date.now(), + // so subsequent now() calls return blockTimeMs + elapsed. We check the difference is small. + expect(Math.abs(dateProvider.now() - blockTimeMs)).toBeLessThan(2000); + + // Warp anvil forward by 1000 seconds and verify the dateProvider follows. + const futureTimestamp = Number(block.timestamp) + 1000; + await publicClient.request({ + method: 'evm_setNextBlockTimestamp', + params: [futureTimestamp], + } as any); + await publicClient.request({ method: 'evm_mine', params: [] } as any); + await sleep(200); + + const futureTimeMs = futureTimestamp * 1000; + expect(Math.abs(dateProvider.now() - futureTimeMs)).toBeLessThan(2000); + }); }); diff --git a/yarn-project/ethereum/src/test/start_anvil.ts b/yarn-project/ethereum/src/test/start_anvil.ts index ba2f8b573604..f5ba8609e15f 100644 --- a/yarn-project/ethereum/src/test/start_anvil.ts +++ b/yarn-project/ethereum/src/test/start_anvil.ts @@ -1,5 +1,6 @@ import { createLogger } from '@aztec/foundation/log'; import { makeBackoff, retry } from '@aztec/foundation/retry'; +import type { TestDateProvider } from '@aztec/foundation/timer'; import { fileURLToPath } from '@aztec/foundation/url'; import { type ChildProcess, spawn } from 'child_process'; @@ -33,6 +34,12 @@ export async function startAnvil( * L1-finality-based logic work without needing hundreds of mined blocks. */ slotsInAnEpoch?: number; + /** + * If provided, the date provider will be synced to anvil's block time on every mined block. + * This keeps the dateProvider in lockstep with anvil's chain time, avoiding drift between + * the wall clock and the L1 chain when computing L1 slot timestamps. + */ + dateProvider?: TestDateProvider; } = {}, ): Promise<{ anvil: Anvil; methodCalls?: string[]; rpcUrl: string; stop: () => Promise }> { const anvilBinary = resolve(dirname(fileURLToPath(import.meta.url)), '../../', 'scripts/anvil_kill_wrapper.sh'); @@ -108,12 +115,15 @@ export async function startAnvil( child.once('close', onClose); }); - // Continue piping for logging / method-call capture after startup. - if (logger || opts.captureMethodCalls) { + // Continue piping for logging, method-call capture, and/or dateProvider sync after startup. + if (logger || opts.captureMethodCalls || opts.dateProvider) { child.stdout?.on('data', (data: Buffer) => { const text = data.toString(); logger?.debug(text.trim()); methodCalls?.push(...(text.match(/eth_[^\s]+/g) || [])); + if (opts.dateProvider) { + syncDateProviderFromAnvilOutput(text, opts.dateProvider); + } }); child.stderr?.on('data', (data: Buffer) => { logger?.debug(data.toString().trim()); @@ -160,6 +170,19 @@ export async function startAnvil( return { anvil: anvilObj, methodCalls, stop, rpcUrl: `http://127.0.0.1:${port}` }; } +/** Extracts block time from anvil stdout and syncs the dateProvider. */ +function syncDateProviderFromAnvilOutput(text: string, dateProvider: TestDateProvider): void { + // Anvil logs mined blocks as: + // Block Time: "Fri, 20 Mar 2026 02:10:46 +0000" + const match = text.match(/Block Time:\s*"([^"]+)"/); + if (match) { + const blockTimeMs = new Date(match[1]).getTime(); + if (!isNaN(blockTimeMs)) { + dateProvider.setTime(blockTimeMs); + } + } +} + /** Send SIGTERM, wait up to 5 s, then SIGKILL. All timers are always cleared. */ function killChild(child: ChildProcess): Promise { return new Promise(resolve => { diff --git a/yarn-project/pxe/src/access_scopes.ts b/yarn-project/pxe/src/access_scopes.ts deleted file mode 100644 index 9ea570de8e13..000000000000 --- a/yarn-project/pxe/src/access_scopes.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { AztecAddress } from '@aztec/stdlib/aztec-address'; - -/** - * Controls which accounts' private state and keys are accessible during execution. - * - `'ALL_SCOPES'`: All registered accounts' private state and keys are accessible. - * - `AztecAddress[]` with entries: Only the specified accounts' private state and keys are accessible. - * - `[]` (empty array): Deny-all. No private state is visible and no keys are accessible. - */ -export type AccessScopes = 'ALL_SCOPES' | AztecAddress[]; diff --git a/yarn-project/pxe/src/contract_function_simulator/contract_function_simulator.ts b/yarn-project/pxe/src/contract_function_simulator/contract_function_simulator.ts index 5c64b0be5b27..5e3975e36d1e 100644 --- a/yarn-project/pxe/src/contract_function_simulator/contract_function_simulator.ts +++ b/yarn-project/pxe/src/contract_function_simulator/contract_function_simulator.ts @@ -89,10 +89,10 @@ import { getFinalMinRevertibleSideEffectCounter, } from '@aztec/stdlib/tx'; -import type { AccessScopes } from '../access_scopes.js'; import type { ContractSyncService } from '../contract_sync/contract_sync_service.js'; import type { MessageContextService } from '../messages/message_context_service.js'; import type { AddressStore } from '../storage/address_store/address_store.js'; +import { CapsuleService } from '../storage/capsule_store/capsule_service.js'; import type { CapsuleStore } from '../storage/capsule_store/capsule_store.js'; import type { ContractStore } from '../storage/contract_store/contract_store.js'; import type { NoteStore } from '../storage/note_store/note_store.js'; @@ -122,7 +122,7 @@ export type ContractSimulatorRunOpts = { /** The address used as a tagging sender when emitting private logs. */ senderForTags?: AztecAddress; /** The accounts whose notes we can access in this call. */ - scopes: AccessScopes; + scopes: AztecAddress[]; /** The job ID for staged writes. */ jobId: string; }; @@ -245,7 +245,7 @@ export class ContractFunctionSimulator { senderTaggingStore: this.senderTaggingStore, recipientTaggingStore: this.recipientTaggingStore, senderAddressBookStore: this.senderAddressBookStore, - capsuleStore: this.capsuleStore, + capsuleService: new CapsuleService(this.capsuleStore, scopes), privateEventStore: this.privateEventStore, messageContextService: this.messageContextService, contractSyncService: this.contractSyncService, @@ -319,7 +319,7 @@ export class ContractFunctionSimulator { call: FunctionCall, authwits: AuthWitness[], anchorBlockHeader: BlockHeader, - scopes: AccessScopes, + scopes: AztecAddress[], jobId: string, ): Promise<{ result: Fr[]; offchainEffects: OffchainEffect[] }> { const entryPointArtifact = await this.contractStore.getFunctionArtifactWithDebugMetadata(call.to, call.selector); @@ -340,7 +340,7 @@ export class ContractFunctionSimulator { aztecNode: this.aztecNode, recipientTaggingStore: this.recipientTaggingStore, senderAddressBookStore: this.senderAddressBookStore, - capsuleStore: this.capsuleStore, + capsuleService: new CapsuleService(this.capsuleStore, scopes), privateEventStore: this.privateEventStore, messageContextService: this.messageContextService, contractSyncService: this.contractSyncService, diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/legacy_oracle_mappings.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/legacy_oracle_mappings.ts index e241a3b0f1a7..7f0a32eb57d8 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/legacy_oracle_mappings.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/legacy_oracle_mappings.ts @@ -84,12 +84,9 @@ export function buildLegacyOracleCallbacks(oracle: Oracle): ACIRCallback { secret: ACVMField[], ): Promise<(ACVMField | ACVMField[])[]> => oracle.aztec_utl_getL1ToL2MembershipWitness(contractAddress, messageHash, secret), - utilityEmitOffchainEffect: (data: ACVMField[]): Promise => oracle.aztec_utl_emitOffchainEffect(data), // Renames (same signature, different oracle name) privateNotifySetMinRevertibleSideEffectCounter: (counter: ACVMField[]): Promise => oracle.aztec_prv_notifyRevertiblePhaseStart(counter), - privateIsSideEffectCounterRevertible: (sideEffectCounter: ACVMField[]): Promise => - oracle.aztec_prv_isExecutionInRevertiblePhase(sideEffectCounter), // Signature changes: old 4-param oracles → new 1-param validatePublicCalldata privateNotifyEnqueuedPublicFunctionCall: ( _contractAddress: ACVMField[], @@ -97,11 +94,5 @@ export function buildLegacyOracleCallbacks(oracle: Oracle): ACIRCallback { _sideEffectCounter: ACVMField[], _isStaticCall: ACVMField[], ): Promise => oracle.aztec_prv_assertValidPublicCalldata(calldataHash), - privateNotifySetPublicTeardownFunctionCall: ( - _contractAddress: ACVMField[], - calldataHash: ACVMField[], - _sideEffectCounter: ACVMField[], - _isStaticCall: ACVMField[], - ): Promise => oracle.aztec_prv_assertValidPublicCalldata(calldataHash), }; } diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/oracle_version_is_checked.test.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/oracle_version_is_checked.test.ts index 12534a67a729..983ad39a26cf 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/oracle_version_is_checked.test.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/oracle_version_is_checked.test.ts @@ -16,6 +16,7 @@ import type { ContractSyncService } from '../../contract_sync/contract_sync_serv import type { MessageContextService } from '../../messages/message_context_service.js'; import { ORACLE_VERSION } from '../../oracle_version.js'; import type { AddressStore } from '../../storage/address_store/address_store.js'; +import { CapsuleService } from '../../storage/capsule_store/capsule_service.js'; import type { CapsuleStore } from '../../storage/capsule_store/capsule_store.js'; import type { ContractStore } from '../../storage/contract_store/contract_store.js'; import type { NoteStore } from '../../storage/note_store/note_store.js'; @@ -148,7 +149,7 @@ describe('Oracle Version Check test suite', () => { anchorBlockHeader, senderForTags, jobId: 'test', - scopes: 'ALL_SCOPES', + scopes: [], }); expect(assertCompatibleOracleVersionSpy).toHaveBeenCalledTimes(1); @@ -200,12 +201,12 @@ describe('Oracle Version Check test suite', () => { aztecNode, recipientTaggingStore, senderAddressBookStore, - capsuleStore, + capsuleService: new CapsuleService(capsuleStore, []), privateEventStore, messageContextService, contractSyncService, jobId: 'test', - scopes: 'ALL_SCOPES', + scopes: [], }); }); diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution.test.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution.test.ts index 5650a7f13cde..9df391d1a020 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution.test.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution.test.ts @@ -219,7 +219,7 @@ describe('Private Execution test suite', () => { anchorBlockHeader, senderForTags, jobId: TEST_JOB_ID, - scopes: 'ALL_SCOPES', + scopes: [owner], }); }; @@ -324,8 +324,7 @@ describe('Private Execution test suite', () => { // Configure mock to actually perform sync_state calls (needed for nested call tests) contractSyncService.ensureContractSynced.mockImplementation( async (contractAddress, functionToInvokeAfterSync, utilityExecutor, anchorBlockHeader, jobId, scopes) => { - const scopeAddresses = scopes === 'ALL_SCOPES' ? [owner] : scopes; - for (const scope of scopeAddresses) { + for (const scope of scopes) { await syncState( contractAddress, contractStore, @@ -384,6 +383,19 @@ describe('Private Execution test suite', () => { keyStore.getAccounts.mockResolvedValue([owner, recipient, senderForTags]); + keyStore.accountHasKey.mockImplementation(async (account: AztecAddress, pkMHash: Fr) => { + if (account.equals(owner)) { + return pkMHash.equals(await ownerCompleteAddress.publicKeys.masterNullifierPublicKey.hash()); + } + if (account.equals(recipient)) { + return pkMHash.equals(await recipientCompleteAddress.publicKeys.masterNullifierPublicKey.hash()); + } + if (account.equals(senderForTags)) { + return pkMHash.equals(await senderForTagsCompleteAddress.publicKeys.masterNullifierPublicKey.hash()); + } + return false; + }); + keyStore.getKeyValidationRequest.mockImplementation(async (pkMHash: Fr, contractAddress: AztecAddress) => { if (pkMHash.equals(await ownerCompleteAddress.publicKeys.masterNullifierPublicKey.hash())) { return Promise.resolve( diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution_oracle.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution_oracle.ts index 54e5af2f7eb7..dcadd6ca3366 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution_oracle.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution_oracle.ts @@ -25,7 +25,6 @@ import { type TxContext, } from '@aztec/stdlib/tx'; -import type { AccessScopes } from '../../access_scopes.js'; import { NoteService } from '../../notes/note_service.js'; import type { SenderTaggingStore } from '../../storage/tagging_store/sender_tagging_store.js'; import { syncSenderTaggingIndexes } from '../../tagging/index.js'; @@ -43,7 +42,7 @@ export type PrivateExecutionOracleArgs = Omit Promise; + utilityExecutor: (call: FunctionCall, scopes: AztecAddress[]) => Promise; executionCache: HashedValuesCache; noteCache: ExecutionNoteCache; taggingIndexCache: ExecutionTaggingIndexCache; @@ -76,7 +75,7 @@ export class PrivateExecutionOracle extends UtilityExecutionOracle implements IP private readonly argsHash: Fr; private readonly txContext: TxContext; private readonly callContext: CallContext; - private readonly utilityExecutor: (call: FunctionCall, scopes: AccessScopes) => Promise; + private readonly utilityExecutor: (call: FunctionCall, scopes: AztecAddress[]) => Promise; private readonly executionCache: HashedValuesCache; private readonly noteCache: ExecutionNoteCache; private readonly taggingIndexCache: ExecutionTaggingIndexCache; @@ -555,7 +554,7 @@ export class PrivateExecutionOracle extends UtilityExecutionOracle implements IP senderTaggingStore: this.senderTaggingStore, recipientTaggingStore: this.recipientTaggingStore, senderAddressBookStore: this.senderAddressBookStore, - capsuleStore: this.capsuleStore, + capsuleService: this.capsuleService, privateEventStore: this.privateEventStore, messageContextService: this.messageContextService, contractSyncService: this.contractSyncService, diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution.test.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution.test.ts index 541327fae27a..c68a606f5c5e 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution.test.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution.test.ts @@ -26,6 +26,7 @@ import type { _MockProxy } from 'jest-mock-extended/lib/Mock.js'; import type { ContractSyncService } from '../../contract_sync/contract_sync_service.js'; import { MessageContextService } from '../../messages/message_context_service.js'; import type { AddressStore } from '../../storage/address_store/address_store.js'; +import { CapsuleService } from '../../storage/capsule_store/capsule_service.js'; import type { CapsuleStore } from '../../storage/capsule_store/capsule_store.js'; import type { ContractStore } from '../../storage/contract_store/contract_store.js'; import type { NoteStore } from '../../storage/note_store/note_store.js'; @@ -231,12 +232,16 @@ describe('Utility Execution test suite', () => { let utilityExecutionOracle: UtilityExecutionOracle; const syncedBlockNumber = 100; + let scope: AztecAddress; + beforeEach(async () => { contractAddress = await AztecAddress.random(); anchorBlockHeader = BlockHeader.empty({ globalVariables: GlobalVariables.empty({ blockNumber: BlockNumber(syncedBlockNumber) }), }); + scope = await AztecAddress.random(); + utilityExecutionOracle = new UtilityExecutionOracle({ contractAddress, authWitnesses: [], @@ -249,12 +254,12 @@ describe('Utility Execution test suite', () => { aztecNode, recipientTaggingStore, senderAddressBookStore, - capsuleStore, + capsuleService: new CapsuleService(capsuleStore, [scope]), privateEventStore, messageContextService, contractSyncService, jobId: 'test-job-id', - scopes: 'ALL_SCOPES', + scopes: [scope], }); }); @@ -267,8 +272,7 @@ describe('Utility Execution test suite', () => { }); describe('capsules', () => { - it('forwards scope to the capsule store', async () => { - const scope = await AztecAddress.random(); + it('forwards scope to the capsule service', async () => { const slot = Fr.random(); const srcSlot = Fr.random(); const dstSlot = Fr.random(); @@ -316,12 +320,12 @@ describe('Utility Execution test suite', () => { aztecNode, recipientTaggingStore, senderAddressBookStore, - capsuleStore, + capsuleService: new CapsuleService(capsuleStore, [scope]), privateEventStore, messageContextService, contractSyncService, jobId: 'test-job-id', - scopes: 'ALL_SCOPES', + scopes: [scope], }); capsuleStore.getCapsule.mockResolvedValueOnce(persisted); @@ -357,7 +361,6 @@ describe('Utility Execution test suite', () => { describe('resolveMessageContexts', () => { const requestSlot = Fr.random(); const responseSlot = Fr.random(); - const scope = AztecAddress.fromBigInt(42n); it('throws when contractAddress does not match', async () => { const wrongAddress = await AztecAddress.random(); @@ -497,12 +500,12 @@ describe('Utility Execution test suite', () => { aztecNode, recipientTaggingStore, senderAddressBookStore, - capsuleStore, + capsuleService: new CapsuleService(capsuleStore, []), privateEventStore, messageContextService, contractSyncService, jobId: 'test-job-id', - scopes: 'ALL_SCOPES', + scopes: [], }); const oracleA = makeOracle(contractAddressA); diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts index aac6d2d05675..512ca45ab9ec 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts @@ -21,7 +21,6 @@ import type { NoteStatus } from '@aztec/stdlib/note'; import { MerkleTreeId, type NullifierMembershipWitness, PublicDataWitness } from '@aztec/stdlib/trees'; import type { BlockHeader, Capsule, OffchainEffect } from '@aztec/stdlib/tx'; -import type { AccessScopes } from '../../access_scopes.js'; import { createContractLogger, logContractMessage, stripAztecnrLogPrefix } from '../../contract_logging.js'; import type { ContractSyncService } from '../../contract_sync/contract_sync_service.js'; import { EventService } from '../../events/event_service.js'; @@ -30,7 +29,7 @@ import { MessageContextService } from '../../messages/message_context_service.js import { NoteService } from '../../notes/note_service.js'; import { ORACLE_VERSION } from '../../oracle_version.js'; import type { AddressStore } from '../../storage/address_store/address_store.js'; -import type { CapsuleStore } from '../../storage/capsule_store/capsule_store.js'; +import type { CapsuleService } from '../../storage/capsule_store/capsule_service.js'; import type { ContractStore } from '../../storage/contract_store/contract_store.js'; import type { NoteStore } from '../../storage/note_store/note_store.js'; import type { PrivateEventStore } from '../../storage/private_event_store/private_event_store.js'; @@ -59,13 +58,13 @@ export type UtilityExecutionOracleArgs = { aztecNode: AztecNode; recipientTaggingStore: RecipientTaggingStore; senderAddressBookStore: SenderAddressBookStore; - capsuleStore: CapsuleStore; + capsuleService: CapsuleService; privateEventStore: PrivateEventStore; messageContextService: MessageContextService; contractSyncService: ContractSyncService; jobId: string; log?: ReturnType; - scopes: AccessScopes; + scopes: AztecAddress[]; }; /** @@ -90,13 +89,13 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra protected readonly aztecNode: AztecNode; protected readonly recipientTaggingStore: RecipientTaggingStore; protected readonly senderAddressBookStore: SenderAddressBookStore; - protected readonly capsuleStore: CapsuleStore; + protected readonly capsuleService: CapsuleService; protected readonly privateEventStore: PrivateEventStore; protected readonly messageContextService: MessageContextService; protected readonly contractSyncService: ContractSyncService; protected readonly jobId: string; protected logger: ReturnType; - protected readonly scopes: AccessScopes; + protected readonly scopes: AztecAddress[]; constructor(args: UtilityExecutionOracleArgs) { this.contractAddress = args.contractAddress; @@ -110,7 +109,7 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra this.aztecNode = args.aztecNode; this.recipientTaggingStore = args.recipientTaggingStore; this.senderAddressBookStore = args.senderAddressBookStore; - this.capsuleStore = args.capsuleStore; + this.capsuleService = args.capsuleService; this.privateEventStore = args.privateEventStore; this.messageContextService = args.messageContextService; this.contractSyncService = args.contractSyncService; @@ -166,18 +165,15 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra * @throws If scopes are defined and the account is not in the scopes. */ public async getKeyValidationRequest(pkMHash: Fr): Promise { - // If scopes are defined, check that the key belongs to an account in the scopes. - if (this.scopes !== 'ALL_SCOPES' && this.scopes.length > 0) { - let hasAccess = false; - for (let i = 0; i < this.scopes.length && !hasAccess; i++) { - if (await this.keyStore.accountHasKey(this.scopes[i], pkMHash)) { - hasAccess = true; - } - } - if (!hasAccess) { - throw new Error(`Key validation request denied: no scoped account has a key with hash ${pkMHash.toString()}.`); + let hasAccess = false; + for (let i = 0; i < this.scopes.length && !hasAccess; i++) { + if (await this.keyStore.accountHasKey(this.scopes[i], pkMHash)) { + hasAccess = true; } } + if (!hasAccess) { + throw new Error(`Key validation request denied: no scoped account has a key with hash ${pkMHash.toString()}.`); + } return this.keyStore.getKeyValidationRequest(pkMHash, this.contractAddress); } @@ -502,7 +498,7 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra this.aztecNode, this.anchorBlockHeader, this.keyStore, - this.capsuleStore, + this.capsuleService, this.recipientTaggingStore, this.senderAddressBookStore, this.addressStore, @@ -539,11 +535,21 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra // We read all note and event validation requests and process them all concurrently. This makes the process much // faster as we don't need to wait for the network round-trip. const noteValidationRequests = ( - await this.capsuleStore.readCapsuleArray(contractAddress, noteValidationRequestsArrayBaseSlot, this.jobId, scope) + await this.capsuleService.readCapsuleArray( + contractAddress, + noteValidationRequestsArrayBaseSlot, + this.jobId, + scope, + ) ).map(fields => NoteValidationRequest.fromFields(fields, maxNotePackedLen)); const eventValidationRequests = ( - await this.capsuleStore.readCapsuleArray(contractAddress, eventValidationRequestsArrayBaseSlot, this.jobId, scope) + await this.capsuleService.readCapsuleArray( + contractAddress, + eventValidationRequestsArrayBaseSlot, + this.jobId, + scope, + ) ).map(fields => EventValidationRequest.fromFields(fields, maxEventSerializedLen)); const noteService = new NoteService(this.noteStore, this.aztecNode, this.anchorBlockHeader, this.jobId); @@ -578,14 +584,14 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra await Promise.all([...noteStorePromises, ...eventStorePromises]); // Requests are cleared once we're done. - await this.capsuleStore.setCapsuleArray( + await this.capsuleService.setCapsuleArray( contractAddress, noteValidationRequestsArrayBaseSlot, [], this.jobId, scope, ); - await this.capsuleStore.setCapsuleArray( + await this.capsuleService.setCapsuleArray( contractAddress, eventValidationRequestsArrayBaseSlot, [], @@ -608,14 +614,14 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra // We read all log retrieval requests and process them all concurrently. This makes the process much faster as we // don't need to wait for the network round-trip. const logRetrievalRequests = ( - await this.capsuleStore.readCapsuleArray(contractAddress, logRetrievalRequestsArrayBaseSlot, this.jobId, scope) + await this.capsuleService.readCapsuleArray(contractAddress, logRetrievalRequestsArrayBaseSlot, this.jobId, scope) ).map(LogRetrievalRequest.fromFields); const logService = new LogService( this.aztecNode, this.anchorBlockHeader, this.keyStore, - this.capsuleStore, + this.capsuleService, this.recipientTaggingStore, this.senderAddressBookStore, this.addressStore, @@ -626,10 +632,16 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra const maybeLogRetrievalResponses = await logService.fetchLogsByTag(contractAddress, logRetrievalRequests); // Requests are cleared once we're done. - await this.capsuleStore.setCapsuleArray(contractAddress, logRetrievalRequestsArrayBaseSlot, [], this.jobId, scope); + await this.capsuleService.setCapsuleArray( + contractAddress, + logRetrievalRequestsArrayBaseSlot, + [], + this.jobId, + scope, + ); // The responses are stored as Option in a second CapsuleArray. - await this.capsuleStore.setCapsuleArray( + await this.capsuleService.setCapsuleArray( contractAddress, logRetrievalResponsesArrayBaseSlot, maybeLogRetrievalResponses.map(LogRetrievalResponse.toSerializedOption), @@ -653,7 +665,7 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra // need scopes here, we just need a bit of shared memory to cross boundaries between Noir and TS. // At the same time, we don't want to allow any global scope access other than where backwards compatibility // forces us to. Hence we need the scope here to be artificial. - const requestCapsules = await this.capsuleStore.readCapsuleArray( + const requestCapsules = await this.capsuleService.readCapsuleArray( contractAddress, messageContextRequestsArrayBaseSlot, this.jobId, @@ -675,7 +687,7 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra ); // Leave response in response capsule array. - await this.capsuleStore.setCapsuleArray( + await this.capsuleService.setCapsuleArray( contractAddress, messageContextResponsesArrayBaseSlot, maybeMessageContexts.map(MessageContext.toSerializedOption), @@ -683,7 +695,7 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra scope, ); } finally { - await this.capsuleStore.setCapsuleArray( + await this.capsuleService.setCapsuleArray( contractAddress, messageContextRequestsArrayBaseSlot, [], @@ -698,23 +710,15 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra // TODO(#10727): instead of this check that this.contractAddress is allowed to access the external DB throw new Error(`Contract ${contractAddress} is not allowed to access ${this.contractAddress}'s PXE DB`); } - this.capsuleStore.setCapsule(contractAddress, slot, capsule, this.jobId, scope); + this.capsuleService.setCapsule(contractAddress, slot, capsule, this.jobId, scope); } - public async getCapsule(contractAddress: AztecAddress, slot: Fr, scope: AztecAddress): Promise { + public getCapsule(contractAddress: AztecAddress, slot: Fr, scope: AztecAddress): Promise { if (!contractAddress.equals(this.contractAddress)) { // TODO(#10727): instead of this check that this.contractAddress is allowed to access the external DB throw new Error(`Contract ${contractAddress} is not allowed to access ${this.contractAddress}'s PXE DB`); } - const maybeTransientCapsule = this.capsules.find( - c => - c.contractAddress.equals(contractAddress) && - c.storageSlot.equals(slot) && - (c.scope ?? AztecAddress.ZERO).equals(scope), - )?.data; - - // TODO(#12425): On the following line, the pertinent capsule gets overshadowed by the transient one. Tackle this. - return maybeTransientCapsule ?? (await this.capsuleStore.getCapsule(contractAddress, slot, this.jobId, scope)); + return this.capsuleService.getCapsule(contractAddress, slot, this.jobId, scope, this.capsules); } public deleteCapsule(contractAddress: AztecAddress, slot: Fr, scope: AztecAddress): void { @@ -722,7 +726,7 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra // TODO(#10727): instead of this check that this.contractAddress is allowed to access the external DB throw new Error(`Contract ${contractAddress} is not allowed to access ${this.contractAddress}'s PXE DB`); } - this.capsuleStore.deleteCapsule(contractAddress, slot, this.jobId, scope); + this.capsuleService.deleteCapsule(contractAddress, slot, this.jobId, scope); } public copyCapsule( @@ -736,7 +740,7 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra // TODO(#10727): instead of this check that this.contractAddress is allowed to access the external DB throw new Error(`Contract ${contractAddress} is not allowed to access ${this.contractAddress}'s PXE DB`); } - return this.capsuleStore.copyCapsule(contractAddress, srcSlot, dstSlot, numEntries, this.jobId, scope); + return this.capsuleService.copyCapsule(contractAddress, srcSlot, dstSlot, numEntries, this.jobId, scope); } /** diff --git a/yarn-project/pxe/src/contract_sync/contract_sync_service.test.ts b/yarn-project/pxe/src/contract_sync/contract_sync_service.test.ts index 0ed395978b75..6e70a2b12896 100644 --- a/yarn-project/pxe/src/contract_sync/contract_sync_service.test.ts +++ b/yarn-project/pxe/src/contract_sync/contract_sync_service.test.ts @@ -9,7 +9,6 @@ import { makeBlockHeader } from '@aztec/stdlib/testing'; import { jest } from '@jest/globals'; import { mock } from 'jest-mock-extended'; -import type { AccessScopes } from '../access_scopes.js'; import type { ContractStore } from '../storage/contract_store/contract_store.js'; import type { NoteStore } from '../storage/note_store/note_store.js'; import { ContractSyncService } from './contract_sync_service.js'; @@ -19,7 +18,7 @@ describe('ContractSyncService', () => { let contractStore: ReturnType>; let noteStore: ReturnType>; let service: ContractSyncService; - let utilityExecutor: jest.Mock<(call: FunctionCall, scopes: AccessScopes) => Promise>; + let utilityExecutor: jest.Mock<(call: FunctionCall, scopes: AztecAddress[]) => Promise>; const contractAddress = AztecAddress.fromBigInt(100n); const scopeA = AztecAddress.fromBigInt(200n); @@ -30,7 +29,7 @@ describe('ContractSyncService', () => { beforeEach(() => { utilityExecutor = jest - .fn<(call: FunctionCall, scopes: AccessScopes) => Promise>() + .fn<(call: FunctionCall, scopes: AztecAddress[]) => Promise>() .mockResolvedValue(undefined); contractStore = mock(); @@ -62,13 +61,7 @@ describe('ContractSyncService', () => { // syncNoteNullifiers returns early when no notes noteStore.getNotes.mockResolvedValue([]); - service = new ContractSyncService( - aztecNode, - contractStore, - noteStore, - () => Promise.resolve([scopeA, scopeB]), - createLogger('test:contract-sync'), - ); + service = new ContractSyncService(aztecNode, contractStore, noteStore, createLogger('test:contract-sync')); }); describe('ensureContractSynced', () => { @@ -85,15 +78,11 @@ describe('ContractSyncService', () => { }); it('skips scope-specific syncs after syncing with all scopes', async () => { - await service.ensureContractSynced( - contractAddress, - null, - utilityExecutor, - anchorBlockHeader, - jobId, - 'ALL_SCOPES', - ); - // ALL_SCOPES resolves to [scopeA, scopeB] via getRegisteredAccounts, so syncState is called once per account + await service.ensureContractSynced(contractAddress, null, utilityExecutor, anchorBlockHeader, jobId, [ + scopeA, + scopeB, + ]); + // [scopeA, scopeB] syncs each scope individually expectSyncedScopes([scopeA], [scopeB]); // After syncing all scopes, scope-specific calls should be skipped @@ -102,18 +91,14 @@ describe('ContractSyncService', () => { expectSyncedScopes([scopeA], [scopeB]); }); - it('still syncs all scopes even after scope-specific sync', async () => { + it('only syncs unsynced scopes when requesting multiple', async () => { await service.ensureContractSynced(contractAddress, null, utilityExecutor, anchorBlockHeader, jobId, [scopeA]); - await service.ensureContractSynced( - contractAddress, - null, - utilityExecutor, - anchorBlockHeader, - jobId, - 'ALL_SCOPES', - ); - // ALL_SCOPES resolves to [scopeA, scopeB]; both are re-synced since ALL_SCOPES bypasses per-scope cache - expectSyncedScopes([scopeA], [scopeA], [scopeB]); + await service.ensureContractSynced(contractAddress, null, utilityExecutor, anchorBlockHeader, jobId, [ + scopeA, + scopeB, + ]); + // scopeA is already cached, so only scopeB is synced + expectSyncedScopes([scopeA], [scopeB]); }); it('empty scopes array skips sync entirely', async () => { @@ -277,63 +262,46 @@ describe('ContractSyncService', () => { expectSyncedScopes([scopeA], [scopeB], [scopeA], [scopeB]); }); - it('also invalidates the ALL_SCOPES entry', async () => { - // Sync ALL_SCOPES -- covers every account. Resolves to [scopeA, scopeB] via getRegisteredAccounts. - await service.ensureContractSynced( - contractAddress, - null, - utilityExecutor, - anchorBlockHeader, - jobId, - 'ALL_SCOPES', - ); + it('invalidating one scope does not affect the other', async () => { + await service.ensureContractSynced(contractAddress, null, utilityExecutor, anchorBlockHeader, jobId, [ + scopeA, + scopeB, + ]); expectSyncedScopes([scopeA], [scopeB]); - // Syncing scopeA is a no-op because ALL_SCOPES already covers it. + // Syncing scopeA is a no-op because it's already cached. await service.ensureContractSynced(contractAddress, null, utilityExecutor, anchorBlockHeader, jobId, [scopeA]); expectSyncedScopes([scopeA], [scopeB]); - // Invalidate scopeA -- this should also clear the ALL_SCOPES entry. + // Invalidate scopeA only. service.invalidateContractForScopes(contractAddress, [scopeA]); // Now syncing scopeA triggers a re-sync. await service.ensureContractSynced(contractAddress, null, utilityExecutor, anchorBlockHeader, jobId, [scopeA]); expectSyncedScopes([scopeA], [scopeB], [scopeA]); - // And syncing ALL_SCOPES also triggers a re-sync since it was invalidated too. - await service.ensureContractSynced( - contractAddress, - null, - utilityExecutor, - anchorBlockHeader, - jobId, - 'ALL_SCOPES', - ); - expectSyncedScopes([scopeA], [scopeB], [scopeA], [scopeA], [scopeB]); + // Syncing both scopes only re-syncs scopeA (already re-synced above is cached), scopeB is still cached. + await service.ensureContractSynced(contractAddress, null, utilityExecutor, anchorBlockHeader, jobId, [ + scopeA, + scopeB, + ]); + expectSyncedScopes([scopeA], [scopeB], [scopeA]); }); it('empty scopes is a no-op', async () => { - await service.ensureContractSynced( - contractAddress, - null, - utilityExecutor, - anchorBlockHeader, - jobId, - 'ALL_SCOPES', - ); + await service.ensureContractSynced(contractAddress, null, utilityExecutor, anchorBlockHeader, jobId, [ + scopeA, + scopeB, + ]); expectSyncedScopes([scopeA], [scopeB]); service.invalidateContractForScopes(contractAddress, []); - // ALL_SCOPES should still be cached since no scopes were invalidated. - await service.ensureContractSynced( - contractAddress, - null, - utilityExecutor, - anchorBlockHeader, - jobId, - 'ALL_SCOPES', - ); + // Both scopes should still be cached since no scopes were invalidated. + await service.ensureContractSynced(contractAddress, null, utilityExecutor, anchorBlockHeader, jobId, [ + scopeA, + scopeB, + ]); expectSyncedScopes([scopeA], [scopeB]); }); @@ -351,7 +319,7 @@ describe('ContractSyncService', () => { }); /** Asserts the utility executor was called exactly with the given sequence of scope arrays. */ - const expectSyncedScopes = (...expectedScopes: AccessScopes[]) => { + const expectSyncedScopes = (...expectedScopes: AztecAddress[][]) => { expect(utilityExecutor).toHaveBeenCalledTimes(expectedScopes.length); for (let i = 0; i < expectedScopes.length; i++) { const [, actualScopes] = utilityExecutor.mock.calls[i]; @@ -360,7 +328,7 @@ describe('ContractSyncService', () => { }; /** Asserts the utility executor was called exactly with the given sequence of [contractAddress, scopes] pairs. */ - const expectSyncedContracts = (...expected: [AztecAddress, AccessScopes][]) => { + const expectSyncedContracts = (...expected: [AztecAddress, AztecAddress[]][]) => { expect(utilityExecutor).toHaveBeenCalledTimes(expected.length); for (let i = 0; i < expected.length; i++) { const [call, actualScopes] = utilityExecutor.mock.calls[i]; diff --git a/yarn-project/pxe/src/contract_sync/contract_sync_service.ts b/yarn-project/pxe/src/contract_sync/contract_sync_service.ts index 8708010c62e2..147bd1eb65df 100644 --- a/yarn-project/pxe/src/contract_sync/contract_sync_service.ts +++ b/yarn-project/pxe/src/contract_sync/contract_sync_service.ts @@ -4,7 +4,6 @@ import type { AztecAddress } from '@aztec/stdlib/aztec-address'; import type { AztecNode } from '@aztec/stdlib/interfaces/client'; import type { BlockHeader } from '@aztec/stdlib/tx'; -import type { AccessScopes } from '../access_scopes.js'; import type { StagedStore } from '../job_coordinator/job_coordinator.js'; import type { ContractStore } from '../storage/contract_store/contract_store.js'; import type { NoteStore } from '../storage/note_store/note_store.js'; @@ -31,7 +30,6 @@ export class ContractSyncService implements StagedStore { private aztecNode: AztecNode, private contractStore: ContractStore, private noteStore: NoteStore, - private getRegisteredAccounts: () => Promise, private log: Logger, ) {} @@ -52,10 +50,10 @@ export class ContractSyncService implements StagedStore { async ensureContractSynced( contractAddress: AztecAddress, functionToInvokeAfterSync: FunctionSelector | null, - utilityExecutor: (call: FunctionCall, scopes: AccessScopes) => Promise, + utilityExecutor: (call: FunctionCall, scopes: AztecAddress[]) => Promise, anchorBlockHeader: BlockHeader, jobId: string, - scopes: AccessScopes, + scopes: AztecAddress[], ): Promise { if (this.#shouldSkipSync(jobId, contractAddress)) { return; @@ -75,33 +73,29 @@ export class ContractSyncService implements StagedStore { await this.#awaitSync(contractAddress, scopes); } - /** Clears sync cache entries for the given scopes of a contract. Also clears the ALL_SCOPES entry. */ + /** Clears sync cache entries for the given scopes of a contract. */ invalidateContractForScopes(contractAddress: AztecAddress, scopes: AztecAddress[]): void { if (scopes.length === 0) { return; } scopes.forEach(scope => this.syncedContracts.delete(toKey(contractAddress, scope))); - this.syncedContracts.delete(toKey(contractAddress, 'ALL_SCOPES')); } async #syncContract( contractAddress: AztecAddress, functionToInvokeAfterSync: FunctionSelector | null, - utilityExecutor: (call: FunctionCall, scopes: AccessScopes) => Promise, + utilityExecutor: (call: FunctionCall, scopes: AztecAddress[]) => Promise, anchorBlockHeader: BlockHeader, jobId: string, - scopes: AccessScopes, + scopes: AztecAddress[], ): Promise { this.log.debug(`Syncing contract ${contractAddress}`); - // Resolve ALL_SCOPES to actual registered accounts, since sync_state must be called once per account. - const scopeAddresses = scopes === 'ALL_SCOPES' ? await this.getRegisteredAccounts() : scopes; - await Promise.all([ // Call sync_state sequentially for each scope address — each invocation synchronizes one account's private // state using scoped capsule arrays. (async () => { - for (const scope of scopeAddresses) { + for (const scope of scopes) { await syncState( contractAddress, this.contractStore, @@ -147,11 +141,11 @@ export class ContractSyncService implements StagedStore { /** If there are unsynced scopes, starts sync and stores the promise in cache with error cleanup. */ #startSyncIfNeeded( contractAddress: AztecAddress, - scopes: AccessScopes, - syncFn: (scopesToSync: AccessScopes) => Promise, + scopes: AztecAddress[], + syncFn: (scopesToSync: AztecAddress[]) => Promise, ): void { - const scopesToSync = this.#getScopesToSync(contractAddress, scopes); - const keys = toKeys(contractAddress, scopesToSync); + const scopesToSync = scopes.filter(scope => !this.syncedContracts.has(toKey(contractAddress, scope))); + const keys = scopesToSync.map(scope => toKey(contractAddress, scope)); if (keys.length === 0) { return; } @@ -162,31 +156,15 @@ export class ContractSyncService implements StagedStore { keys.forEach(key => this.syncedContracts.set(key, promise)); } - /** Filters out scopes that are already cached, returning only those that still need syncing. */ - #getScopesToSync(contractAddress: AztecAddress, scopes: AccessScopes): AccessScopes { - if (this.syncedContracts.has(toKey(contractAddress, 'ALL_SCOPES'))) { - // If we are already syncing all scopes, then return an empty list - return []; - } - if (scopes === 'ALL_SCOPES') { - return 'ALL_SCOPES'; - } - return scopes.filter(scope => !this.syncedContracts.has(toKey(contractAddress, scope))); - } - /** Collects all relevant scope promises (including in-flight ones from concurrent calls) and awaits them. */ - async #awaitSync(contractAddress: AztecAddress, scopes: AccessScopes): Promise { - const promises = toKeys(contractAddress, scopes) - .map(key => this.syncedContracts.get(key)) + async #awaitSync(contractAddress: AztecAddress, scopes: AztecAddress[]): Promise { + const promises = scopes + .map(scope => this.syncedContracts.get(toKey(contractAddress, scope))) .filter(p => p !== undefined); await Promise.all(promises); } } -function toKeys(contract: AztecAddress, scopes: AccessScopes) { - return scopes === 'ALL_SCOPES' ? [toKey(contract, scopes)] : scopes.map(scope => toKey(contract, scope)); -} - -function toKey(contract: AztecAddress, scope: AztecAddress | 'ALL_SCOPES') { - return scope === 'ALL_SCOPES' ? `${contract.toString()}:*` : `${contract.toString()}:${scope.toString()}`; +function toKey(contract: AztecAddress, scope: AztecAddress) { + return `${contract.toString()}:${scope.toString()}`; } diff --git a/yarn-project/pxe/src/contract_sync/helpers.ts b/yarn-project/pxe/src/contract_sync/helpers.ts index 8f437d10dd7f..f794527a32f9 100644 --- a/yarn-project/pxe/src/contract_sync/helpers.ts +++ b/yarn-project/pxe/src/contract_sync/helpers.ts @@ -6,7 +6,6 @@ import { DelayedPublicMutableValues, DelayedPublicMutableValuesWithHash } from ' import type { AztecNode } from '@aztec/stdlib/interfaces/client'; import type { BlockHeader } from '@aztec/stdlib/tx'; -import type { AccessScopes } from '../access_scopes.js'; import { NoteService } from '../notes/note_service.js'; import type { ContractStore } from '../storage/contract_store/contract_store.js'; import type { NoteStore } from '../storage/note_store/note_store.js'; @@ -43,7 +42,7 @@ export async function syncState( contractAddress: AztecAddress, contractStore: ContractStore, functionToInvokeAfterSync: FunctionSelector | null, - utilityExecutor: (privateSyncCall: FunctionCall, scopes: AccessScopes) => Promise, + utilityExecutor: (privateSyncCall: FunctionCall, scopes: AztecAddress[]) => Promise, noteStore: NoteStore, aztecNode: AztecNode, anchorBlockHeader: BlockHeader, @@ -60,7 +59,7 @@ export async function syncState( } const noteService = new NoteService(noteStore, aztecNode, anchorBlockHeader, jobId); - const scopes: AccessScopes = [scope]; + const scopes: AztecAddress[] = [scope]; // Both sync_state and syncNoteNullifiers interact with the note store, but running them in parallel is safe // because note store is designed to handle concurrent operations. diff --git a/yarn-project/pxe/src/debug/pxe_debug_utils.ts b/yarn-project/pxe/src/debug/pxe_debug_utils.ts index e5504328a611..592074cd37fe 100644 --- a/yarn-project/pxe/src/debug/pxe_debug_utils.ts +++ b/yarn-project/pxe/src/debug/pxe_debug_utils.ts @@ -1,9 +1,9 @@ import type { FunctionCall } from '@aztec/stdlib/abi'; import type { AuthWitness } from '@aztec/stdlib/auth-witness'; +import type { AztecAddress } from '@aztec/stdlib/aztec-address'; import type { NoteDao } from '@aztec/stdlib/note'; import type { ContractOverrides } from '@aztec/stdlib/tx'; -import type { AccessScopes } from '../access_scopes.js'; import type { BlockSynchronizer } from '../block_synchronizer/block_synchronizer.js'; import type { ContractFunctionSimulator } from '../contract_function_simulator/contract_function_simulator.js'; import type { ContractSyncService } from '../contract_sync/contract_sync_service.js'; @@ -22,7 +22,7 @@ export class PXEDebugUtils { contractFunctionSimulator: ContractFunctionSimulator, call: FunctionCall, authWitnesses: AuthWitness[] | undefined, - scopes: AccessScopes, + scopes: AztecAddress[], jobId: string, ) => Promise; @@ -41,7 +41,7 @@ export class PXEDebugUtils { contractFunctionSimulator: ContractFunctionSimulator, call: FunctionCall, authWitnesses: AuthWitness[] | undefined, - scopes: AccessScopes, + scopes: AztecAddress[], jobId: string, ) => Promise, ) { diff --git a/yarn-project/pxe/src/entrypoints/client/bundle/index.ts b/yarn-project/pxe/src/entrypoints/client/bundle/index.ts index d854f0abf873..437bf2a74da3 100644 --- a/yarn-project/pxe/src/entrypoints/client/bundle/index.ts +++ b/yarn-project/pxe/src/entrypoints/client/bundle/index.ts @@ -1,4 +1,3 @@ -export * from '../../../access_scopes.js'; export * from '../../../notes_filter.js'; export * from '../../../pxe.js'; export * from '../../../config/index.js'; diff --git a/yarn-project/pxe/src/entrypoints/client/lazy/index.ts b/yarn-project/pxe/src/entrypoints/client/lazy/index.ts index 17b4025cbf74..3f417a8b5209 100644 --- a/yarn-project/pxe/src/entrypoints/client/lazy/index.ts +++ b/yarn-project/pxe/src/entrypoints/client/lazy/index.ts @@ -1,4 +1,3 @@ -export * from '../../../access_scopes.js'; export * from '../../../notes_filter.js'; export * from '../../../pxe.js'; export * from '../../../config/index.js'; diff --git a/yarn-project/pxe/src/entrypoints/server/index.ts b/yarn-project/pxe/src/entrypoints/server/index.ts index ffc0a3248e21..49a02ca88a4d 100644 --- a/yarn-project/pxe/src/entrypoints/server/index.ts +++ b/yarn-project/pxe/src/entrypoints/server/index.ts @@ -1,4 +1,3 @@ -export * from '../../access_scopes.js'; export * from '../../notes_filter.js'; export * from '../../pxe.js'; export * from '../../config/index.js'; diff --git a/yarn-project/pxe/src/logs/log_service.test.ts b/yarn-project/pxe/src/logs/log_service.test.ts index 01b1a2244195..68678dc94cfc 100644 --- a/yarn-project/pxe/src/logs/log_service.test.ts +++ b/yarn-project/pxe/src/logs/log_service.test.ts @@ -13,6 +13,7 @@ import { type MockProxy, mock } from 'jest-mock-extended'; import { LogRetrievalRequest } from '../contract_function_simulator/noir-structs/log_retrieval_request.js'; import { AddressStore } from '../storage/address_store/address_store.js'; +import { CapsuleService } from '../storage/capsule_store/capsule_service.js'; import { CapsuleStore } from '../storage/capsule_store/capsule_store.js'; import { RecipientTaggingStore } from '../storage/tagging_store/recipient_tagging_store.js'; import { SenderAddressBookStore } from '../storage/tagging_store/sender_address_book_store.js'; @@ -53,7 +54,7 @@ describe('LogService', () => { aztecNode, anchorBlockHeader, keyStore, - capsuleStore, + new CapsuleService(capsuleStore, []), recipientTaggingStore, senderAddressBookStore, addressStore, diff --git a/yarn-project/pxe/src/logs/log_service.ts b/yarn-project/pxe/src/logs/log_service.ts index df4645b37be5..0f830e018b3e 100644 --- a/yarn-project/pxe/src/logs/log_service.ts +++ b/yarn-project/pxe/src/logs/log_service.ts @@ -15,7 +15,7 @@ import type { BlockHeader } from '@aztec/stdlib/tx'; import type { LogRetrievalRequest } from '../contract_function_simulator/noir-structs/log_retrieval_request.js'; import { LogRetrievalResponse } from '../contract_function_simulator/noir-structs/log_retrieval_response.js'; import { AddressStore } from '../storage/address_store/address_store.js'; -import { CapsuleStore } from '../storage/capsule_store/capsule_store.js'; +import type { CapsuleService } from '../storage/capsule_store/capsule_service.js'; import type { RecipientTaggingStore } from '../storage/tagging_store/recipient_tagging_store.js'; import type { SenderAddressBookStore } from '../storage/tagging_store/sender_address_book_store.js'; import { @@ -31,7 +31,7 @@ export class LogService { private readonly aztecNode: AztecNode, private readonly anchorBlockHeader: BlockHeader, private readonly keyStore: KeyStore, - private readonly capsuleStore: CapsuleStore, + private readonly capsuleService: CapsuleService, private readonly recipientTaggingStore: RecipientTaggingStore, private readonly senderAddressBookStore: SenderAddressBookStore, private readonly addressStore: AddressStore, @@ -207,7 +207,7 @@ export class LogService { }); // TODO: This looks like it could belong more at the oracle interface level - return this.capsuleStore.appendToCapsuleArray( + return this.capsuleService.appendToCapsuleArray( contractAddress, capsuleArrayBaseSlot, pendingTaggedLogs, diff --git a/yarn-project/pxe/src/notes/note_service.test.ts b/yarn-project/pxe/src/notes/note_service.test.ts index d29529735c65..667fc26f2cea 100644 --- a/yarn-project/pxe/src/notes/note_service.test.ts +++ b/yarn-project/pxe/src/notes/note_service.test.ts @@ -41,13 +41,13 @@ describe('NoteService', () => { contractAddress = await AztecAddress.random(); - const notes = await noteStore.getNotes({ contractAddress, scopes: 'ALL_SCOPES' }, 'test'); + recipient = await keyStore.addAccount(new Fr(69), Fr.random()); + + const notes = await noteStore.getNotes({ contractAddress, scopes: [recipient.address] }, 'test'); expect(notes).toHaveLength(0); const accounts = await keyStore.getAccounts(); - expect(accounts).toHaveLength(0); - - recipient = await keyStore.addAccount(new Fr(69), Fr.random()); + expect(accounts).toHaveLength(1); setSyncedBlockNumber(BlockNumber(syncedBlockNumber)); }); @@ -65,7 +65,7 @@ describe('NoteService', () => { const nullifierIndex = randomDataInBlock(123n); aztecNode.findLeavesIndexes.mockResolvedValue([nullifierIndex]); - await noteService.syncNoteNullifiers(contractAddress, 'ALL_SCOPES'); + await noteService.syncNoteNullifiers(contractAddress, [recipient.address]); const remainingNotes = await noteStore.getNotes( { @@ -103,7 +103,7 @@ describe('NoteService', () => { // No nullifier found in merkle tree aztecNode.findLeavesIndexes.mockResolvedValue([undefined]); - await noteService.syncNoteNullifiers(contractAddress, 'ALL_SCOPES'); + await noteService.syncNoteNullifiers(contractAddress, [recipient.address]); const remainingNotes = await noteStore.getNotes( { @@ -148,7 +148,7 @@ describe('NoteService', () => { return Promise.resolve([undefined]); }); - await noteService.syncNoteNullifiers(contractAddress, 'ALL_SCOPES'); + await noteService.syncNoteNullifiers(contractAddress, [recipient.address]); // Verify note still exists const remainingNotes = await noteStore.getNotes( @@ -186,7 +186,7 @@ describe('NoteService', () => { const getNotesSpy = jest.spyOn(noteStore, 'getNotes'); - await noteService.syncNoteNullifiers(contractAddress, 'ALL_SCOPES'); + await noteService.syncNoteNullifiers(contractAddress, [recipient.address]); // Verify applyNullifiers was called once for all accounts expect(getNotesSpy).toHaveBeenCalledTimes(1); diff --git a/yarn-project/pxe/src/notes/note_service.ts b/yarn-project/pxe/src/notes/note_service.ts index dd50499c3ffd..bf839db370b8 100644 --- a/yarn-project/pxe/src/notes/note_service.ts +++ b/yarn-project/pxe/src/notes/note_service.ts @@ -7,7 +7,6 @@ import { Note, NoteDao, NoteStatus } from '@aztec/stdlib/note'; import { MerkleTreeId } from '@aztec/stdlib/trees'; import type { BlockHeader, TxHash } from '@aztec/stdlib/tx'; -import type { AccessScopes } from '../access_scopes.js'; import type { NoteStore } from '../storage/note_store/note_store.js'; export class NoteService { @@ -32,7 +31,7 @@ export class NoteService { owner: AztecAddress | undefined, storageSlot: Fr, status: NoteStatus, - scopes: AccessScopes, + scopes: AztecAddress[], ) { const noteDaos = await this.noteStore.getNotes( { @@ -71,7 +70,7 @@ export class NoteService { * * @param contractAddress - The contract whose notes should be checked and nullified. */ - public async syncNoteNullifiers(contractAddress: AztecAddress, scopes: AccessScopes): Promise { + public async syncNoteNullifiers(contractAddress: AztecAddress, scopes: AztecAddress[]): Promise { const anchorBlockHash = await this.anchorBlockHeader.hash(); const contractNotes = await this.noteStore.getNotes({ contractAddress, scopes }, this.jobId); diff --git a/yarn-project/pxe/src/notes_filter.ts b/yarn-project/pxe/src/notes_filter.ts index cdf6c3dc2bc1..18e2ab2ab9f5 100644 --- a/yarn-project/pxe/src/notes_filter.ts +++ b/yarn-project/pxe/src/notes_filter.ts @@ -2,8 +2,6 @@ import type { Fr } from '@aztec/foundation/curves/bn254'; import type { AztecAddress } from '@aztec/stdlib/aztec-address'; import type { NoteStatus } from '@aztec/stdlib/note'; -import type { AccessScopes } from './access_scopes.js'; - /** * A filter used to fetch notes. * @remarks This filter is applied as an intersection of all its params. @@ -22,5 +20,5 @@ export type NotesFilter = { status?: NoteStatus; /** The siloed nullifier for the note. */ siloedNullifier?: Fr; - scopes: AccessScopes; + scopes: AztecAddress[]; }; diff --git a/yarn-project/pxe/src/pxe.ts b/yarn-project/pxe/src/pxe.ts index 6d7b05ab88aa..39072be6823d 100644 --- a/yarn-project/pxe/src/pxe.ts +++ b/yarn-project/pxe/src/pxe.ts @@ -52,7 +52,6 @@ import { import { inspect } from 'util'; -import type { AccessScopes } from './access_scopes.js'; import { BlockSynchronizer } from './block_synchronizer/index.js'; import type { PXEConfig } from './config/index.js'; import { BenchmarkedNodeFactory } from './contract_function_simulator/benchmarked_node.js'; @@ -96,7 +95,7 @@ export type ProfileTxOpts = { /** If true, proof generation is skipped during profiling. Defaults to true. */ skipProofGeneration?: boolean; /** Addresses whose private state and keys are accessible during private execution. */ - scopes: AccessScopes; + scopes: AztecAddress[]; }; /** Options for PXE.simulateTx. */ @@ -112,7 +111,7 @@ export type SimulateTxOpts = { /** State overrides for the simulation, such as contract instances and artifacts. Requires skipKernels: true */ overrides?: SimulationOverrides; /** Addresses whose private state and keys are accessible during private execution */ - scopes: AccessScopes; + scopes: AztecAddress[]; }; /** Options for PXE.executeUtility. */ @@ -120,7 +119,7 @@ export type ExecuteUtilityOpts = { /** The authentication witnesses required for the function call. */ authwits?: AuthWitness[]; /** The accounts whose notes we can access in this call */ - scopes: AccessScopes; + scopes: AztecAddress[]; }; /** Args for PXE.create. */ @@ -214,7 +213,6 @@ export class PXE { node, contractStore, noteStore, - () => keyStore.getAccounts(), createLogger('pxe:contract_sync', bindings), ); const messageContextService = new MessageContextService(node); @@ -367,7 +365,7 @@ export class PXE { async #executePrivate( contractFunctionSimulator: ContractFunctionSimulator, txRequest: TxExecutionRequest, - scopes: AccessScopes, + scopes: AztecAddress[], jobId: string, ): Promise { const { origin: contractAddress, functionSelector } = txRequest; @@ -416,7 +414,7 @@ export class PXE { contractFunctionSimulator: ContractFunctionSimulator, call: FunctionCall, authWitnesses: AuthWitness[] | undefined, - scopes: AccessScopes, + scopes: AztecAddress[], jobId: string, ) { try { @@ -1041,7 +1039,7 @@ export class PXE { inspect(txRequest), `simulatePublic=${simulatePublic}`, `skipTxValidation=${skipTxValidation}`, - `scopes=${scopes === 'ALL_SCOPES' ? scopes : scopes.map(s => s.toString()).join(', ')}`, + `scopes=${scopes.map(s => s.toString()).join(', ')}`, ); } }); @@ -1053,7 +1051,7 @@ export class PXE { */ public executeUtility( call: FunctionCall, - { authwits, scopes }: ExecuteUtilityOpts = { scopes: 'ALL_SCOPES' }, + { authwits, scopes }: ExecuteUtilityOpts = { scopes: [] }, ): Promise { // We disable concurrent executions since those might execute oracles which read and write to the PXE stores (e.g. // to the capsules), and we need to prevent concurrent runs from interfering with one another (e.g. attempting to @@ -1111,7 +1109,7 @@ export class PXE { throw this.#contextualizeError( err, `executeUtility ${to}:${name}(${stringifiedArgs})`, - `scopes=${scopes === 'ALL_SCOPES' ? scopes : scopes.map(s => s.toString()).join(', ')}`, + `scopes=${scopes.map(s => s.toString()).join(', ')}`, ); } }); diff --git a/yarn-project/pxe/src/storage/capsule_store/capsule_service.test.ts b/yarn-project/pxe/src/storage/capsule_store/capsule_service.test.ts new file mode 100644 index 000000000000..616c907de454 --- /dev/null +++ b/yarn-project/pxe/src/storage/capsule_store/capsule_service.test.ts @@ -0,0 +1,139 @@ +import { Fr } from '@aztec/foundation/curves/bn254'; +import { openTmpStore } from '@aztec/kv-store/lmdb-v2'; +import { AztecAddress } from '@aztec/stdlib/aztec-address'; + +import { CapsuleService } from './capsule_service.js'; +import { CapsuleStore } from './capsule_store.js'; + +describe('CapsuleService', () => { + let contract: AztecAddress; + let allowedScope: AztecAddress; + let disallowedScope: AztecAddress; + let capsuleStore: CapsuleStore; + let capsuleService: CapsuleService; + + const jobId = 'test'; + + beforeEach(async () => { + contract = await AztecAddress.random(); + allowedScope = await AztecAddress.random(); + disallowedScope = await AztecAddress.random(); + capsuleStore = new CapsuleStore(await openTmpStore('capsule_service_test')); + capsuleService = new CapsuleService(capsuleStore, [allowedScope]); + }); + + describe('scope enforcement', () => { + const slot = new Fr(1); + const capsule = [new Fr(42)]; + + it('setCapsule rejects a disallowed scope', () => { + expect(() => capsuleService.setCapsule(contract, slot, capsule, jobId, disallowedScope)).toThrow( + 'is not in the allowed scopes list', + ); + }); + + it('getCapsule rejects a disallowed scope', async () => { + await expect(capsuleService.getCapsule(contract, slot, jobId, disallowedScope)).rejects.toThrow( + 'is not in the allowed scopes list', + ); + }); + + it('deleteCapsule rejects a disallowed scope', () => { + expect(() => capsuleService.deleteCapsule(contract, slot, jobId, disallowedScope)).toThrow( + 'is not in the allowed scopes list', + ); + }); + + it('copyCapsule rejects a disallowed scope', () => { + expect(() => capsuleService.copyCapsule(contract, slot, new Fr(5), 1, jobId, disallowedScope)).toThrow( + 'is not in the allowed scopes list', + ); + }); + + it('appendToCapsuleArray rejects a disallowed scope', () => { + expect(() => capsuleService.appendToCapsuleArray(contract, slot, [capsule], jobId, disallowedScope)).toThrow( + 'is not in the allowed scopes list', + ); + }); + + it('readCapsuleArray rejects a disallowed scope', () => { + expect(() => capsuleService.readCapsuleArray(contract, slot, jobId, disallowedScope)).toThrow( + 'is not in the allowed scopes list', + ); + }); + + it('setCapsuleArray rejects a disallowed scope', () => { + expect(() => capsuleService.setCapsuleArray(contract, slot, [capsule], jobId, disallowedScope)).toThrow( + 'is not in the allowed scopes list', + ); + }); + + it('allows operations with an allowed scope', async () => { + const scope = allowedScope; + + // setCapsule + getCapsule + capsuleService.setCapsule(contract, slot, capsule, jobId, scope); + expect(await capsuleService.getCapsule(contract, slot, jobId, scope)).toEqual(capsule); + + // deleteCapsule + capsuleService.deleteCapsule(contract, slot, jobId, scope); + expect(await capsuleService.getCapsule(contract, slot, jobId, scope)).toBeNull(); + + // copyCapsule + capsuleService.setCapsule(contract, slot, capsule, jobId, scope); + await capsuleService.copyCapsule(contract, slot, new Fr(5), 1, jobId, scope); + expect(await capsuleService.getCapsule(contract, new Fr(5), jobId, scope)).toEqual(capsule); + + // appendToCapsuleArray + readCapsuleArray + const baseSlot = new Fr(10); + await capsuleService.appendToCapsuleArray(contract, baseSlot, [capsule], jobId, scope); + expect(await capsuleService.readCapsuleArray(contract, baseSlot, jobId, scope)).toEqual([capsule]); + + // setCapsuleArray + const newArray = [capsule, capsule]; + await capsuleService.setCapsuleArray(contract, baseSlot, newArray, jobId, scope); + expect(await capsuleService.readCapsuleArray(contract, baseSlot, jobId, scope)).toEqual(newArray); + }); + + it('address zero is always allowed even if not in the scopes list', async () => { + const scope = AztecAddress.ZERO; + + // setCapsule + getCapsule + capsuleService.setCapsule(contract, slot, capsule, jobId, scope); + expect(await capsuleService.getCapsule(contract, slot, jobId, scope)).toEqual(capsule); + + // deleteCapsule + capsuleService.deleteCapsule(contract, slot, jobId, scope); + expect(await capsuleService.getCapsule(contract, slot, jobId, scope)).toBeNull(); + + // copyCapsule + capsuleService.setCapsule(contract, slot, capsule, jobId, scope); + await capsuleService.copyCapsule(contract, slot, new Fr(5), 1, jobId, scope); + expect(await capsuleService.getCapsule(contract, new Fr(5), jobId, scope)).toEqual(capsule); + + // appendToCapsuleArray + readCapsuleArray + const baseSlot = new Fr(10); + await capsuleService.appendToCapsuleArray(contract, baseSlot, [capsule], jobId, scope); + expect(await capsuleService.readCapsuleArray(contract, baseSlot, jobId, scope)).toEqual([capsule]); + + // setCapsuleArray + const newArray = [capsule, capsule]; + await capsuleService.setCapsuleArray(contract, baseSlot, newArray, jobId, scope); + expect(await capsuleService.readCapsuleArray(contract, baseSlot, jobId, scope)).toEqual(newArray); + }); + + it('empty allowed scopes rejects requests', async () => { + const noScopesService = new CapsuleService(capsuleStore, []); + const scope = allowedScope; + const err = 'is not in the allowed scopes list'; + + expect(() => noScopesService.setCapsule(contract, slot, capsule, jobId, scope)).toThrow(err); + await expect(noScopesService.getCapsule(contract, slot, jobId, scope)).rejects.toThrow(err); + expect(() => noScopesService.deleteCapsule(contract, slot, jobId, scope)).toThrow(err); + expect(() => noScopesService.copyCapsule(contract, slot, new Fr(5), 1, jobId, scope)).toThrow(err); + expect(() => noScopesService.appendToCapsuleArray(contract, slot, [capsule], jobId, scope)).toThrow(err); + expect(() => noScopesService.readCapsuleArray(contract, slot, jobId, scope)).toThrow(err); + expect(() => noScopesService.setCapsuleArray(contract, slot, [capsule], jobId, scope)).toThrow(err); + }); + }); +}); diff --git a/yarn-project/pxe/src/storage/capsule_store/capsule_service.ts b/yarn-project/pxe/src/storage/capsule_store/capsule_service.ts new file mode 100644 index 000000000000..cdb61859cb13 --- /dev/null +++ b/yarn-project/pxe/src/storage/capsule_store/capsule_service.ts @@ -0,0 +1,90 @@ +import type { Fr } from '@aztec/foundation/curves/bn254'; +import { AztecAddress } from '@aztec/stdlib/aztec-address'; +import type { Capsule } from '@aztec/stdlib/tx'; + +import type { CapsuleStore } from './capsule_store.js'; + +/** + * Wraps a CapsuleStore with scope-based access control. Each operation asserts that the requested scope is in the + * allowed scopes list before delegating to the underlying store. + */ +export class CapsuleService { + constructor( + private readonly capsuleStore: CapsuleStore, + private readonly allowedScopes: AztecAddress[], + ) {} + + setCapsule(contractAddress: AztecAddress, slot: Fr, capsule: Fr[], jobId: string, scope: AztecAddress) { + assertAllowedScope(scope, this.allowedScopes); + this.capsuleStore.setCapsule(contractAddress, slot, capsule, jobId, scope); + } + + async getCapsule( + contractAddress: AztecAddress, + slot: Fr, + jobId: string, + scope: AztecAddress, + transientCapsules?: Capsule[], + ): Promise { + assertAllowedScope(scope, this.allowedScopes); + + // TODO(#12425): On the following line, the pertinent capsule gets overshadowed by the transient one. Tackle this. + const maybeTransientCapsule = transientCapsules?.find( + c => + c.contractAddress.equals(contractAddress) && + c.storageSlot.equals(slot) && + (c.scope ?? AztecAddress.ZERO).equals(scope), + )?.data; + + return maybeTransientCapsule ?? (await this.capsuleStore.getCapsule(contractAddress, slot, jobId, scope)); + } + + deleteCapsule(contractAddress: AztecAddress, slot: Fr, jobId: string, scope: AztecAddress) { + assertAllowedScope(scope, this.allowedScopes); + this.capsuleStore.deleteCapsule(contractAddress, slot, jobId, scope); + } + + copyCapsule( + contractAddress: AztecAddress, + srcSlot: Fr, + dstSlot: Fr, + numEntries: number, + jobId: string, + scope: AztecAddress, + ): Promise { + assertAllowedScope(scope, this.allowedScopes); + return this.capsuleStore.copyCapsule(contractAddress, srcSlot, dstSlot, numEntries, jobId, scope); + } + + appendToCapsuleArray( + contractAddress: AztecAddress, + baseSlot: Fr, + content: Fr[][], + jobId: string, + scope: AztecAddress, + ): Promise { + assertAllowedScope(scope, this.allowedScopes); + return this.capsuleStore.appendToCapsuleArray(contractAddress, baseSlot, content, jobId, scope); + } + + readCapsuleArray(contractAddress: AztecAddress, baseSlot: Fr, jobId: string, scope: AztecAddress): Promise { + assertAllowedScope(scope, this.allowedScopes); + return this.capsuleStore.readCapsuleArray(contractAddress, baseSlot, jobId, scope); + } + + setCapsuleArray(contractAddress: AztecAddress, baseSlot: Fr, content: Fr[][], jobId: string, scope: AztecAddress) { + assertAllowedScope(scope, this.allowedScopes); + return this.capsuleStore.setCapsuleArray(contractAddress, baseSlot, content, jobId, scope); + } +} + +function assertAllowedScope(scope: AztecAddress, allowedScopes: AztecAddress[]) { + if (scope.equals(AztecAddress.ZERO)) { + return; + } + if (!allowedScopes.some((allowed: AztecAddress) => allowed.equals(scope))) { + throw new Error( + `Scope ${scope.toString()} is not in the allowed scopes list: [${allowedScopes.map((s: AztecAddress) => s.toString()).join(', ')}]. See https://docs.aztec.network/errors/10`, + ); + } +} diff --git a/yarn-project/pxe/src/storage/capsule_store/index.ts b/yarn-project/pxe/src/storage/capsule_store/index.ts index accfb15afdc9..ab6c6222a925 100644 --- a/yarn-project/pxe/src/storage/capsule_store/index.ts +++ b/yarn-project/pxe/src/storage/capsule_store/index.ts @@ -1 +1,2 @@ +export { CapsuleService } from './capsule_service.js'; export { CapsuleStore } from './capsule_store.js'; diff --git a/yarn-project/pxe/src/storage/note_store/note_store.test.ts b/yarn-project/pxe/src/storage/note_store/note_store.test.ts index 8fc1a287e518..01e38dee0a71 100644 --- a/yarn-project/pxe/src/storage/note_store/note_store.test.ts +++ b/yarn-project/pxe/src/storage/note_store/note_store.test.ts @@ -99,7 +99,7 @@ describe('NoteStore', () => { const noteStore = new NoteStore(store); await verifyAndCommitForEachJob(['pre-commit', 'post-commit'], noteStore, async (jobId: string) => { - const notes = await noteStore.getNotes({ contractAddress: CONTRACT_A, scopes: 'ALL_SCOPES' }, jobId); + const notes = await noteStore.getNotes({ contractAddress: CONTRACT_A, scopes: [SCOPE_1, SCOPE_2] }, jobId); expect(Array.isArray(notes)).toBe(true); expect(notes).toHaveLength(0); }); @@ -123,8 +123,8 @@ describe('NoteStore', () => { const noteStore2 = new NoteStore(store); await verifyAndCommitForEachJob(['second-store', 'fresh-job'], noteStore2, async (jobId: string) => { - const notesA = await noteStore2.getNotes({ contractAddress: CONTRACT_A, scopes: 'ALL_SCOPES' }, jobId); - const notesB = await noteStore2.getNotes({ contractAddress: CONTRACT_B, scopes: 'ALL_SCOPES' }, jobId); + const notesA = await noteStore2.getNotes({ contractAddress: CONTRACT_A, scopes: [FAKE_ADDRESS] }, jobId); + const notesB = await noteStore2.getNotes({ contractAddress: CONTRACT_B, scopes: [FAKE_ADDRESS] }, jobId); expect(nullifierSet(notesA)).toEqual(nullifierSet([SILOED_NULLIFIER_1])); expect(nullifierSet(notesB)).toEqual(nullifierSet([SILOED_NULLIFIER_2])); @@ -150,14 +150,14 @@ describe('NoteStore', () => { }); it('filters notes matching only the contractAddress', async () => { - const notes = await noteStore.getNotes({ contractAddress: CONTRACT_A, scopes: 'ALL_SCOPES' }, 'test'); + const notes = await noteStore.getNotes({ contractAddress: CONTRACT_A, scopes: [SCOPE_1, SCOPE_2] }, 'test'); // note1 and note2 match CONTRACT_A expect(nullifierSet(notes)).toEqual(nullifierSet([note1, note2])); }); it('filters notes matching contractAddress and storageSlot', async () => { const notes = await noteStore.getNotes( - { contractAddress: CONTRACT_A, storageSlot: SLOT_Y, scopes: 'ALL_SCOPES' }, + { contractAddress: CONTRACT_A, storageSlot: SLOT_Y, scopes: [SCOPE_1, SCOPE_2] }, 'test', ); expect(nullifierSet(notes)).toEqual(nullifierSet([note2])); @@ -210,14 +210,14 @@ describe('NoteStore', () => { const nullifiers = [mkNullifier(note2)]; await expect(noteStore.applyNullifiers(nullifiers, 'test')).resolves.toEqual([note2]); - const activeNotes = await noteStore.getNotes({ contractAddress: CONTRACT_A, scopes: 'ALL_SCOPES' }, 'test'); + const activeNotes = await noteStore.getNotes({ contractAddress: CONTRACT_A, scopes: [SCOPE_1, SCOPE_2] }, 'test'); expect(nullifierSet(activeNotes)).toEqual(nullifierSet([note1])); const allNotes = await noteStore.getNotes( { contractAddress: CONTRACT_A, status: NoteStatus.ACTIVE_OR_NULLIFIED, - scopes: 'ALL_SCOPES', + scopes: [SCOPE_1, SCOPE_2], }, 'test', ); @@ -270,7 +270,7 @@ describe('NoteStore', () => { const filter = { contractAddress: CONTRACT_A, siloedNullifier: note1.siloedNullifier, - scopes: 'ALL_SCOPES' as const, + scopes: [SCOPE_1, SCOPE_2], }; const notes = await noteStore.getNotes(filter, 'test'); @@ -281,7 +281,7 @@ describe('NoteStore', () => { { contractAddress: CONTRACT_A, siloedNullifier: note2.siloedNullifier, - scopes: 'ALL_SCOPES', + scopes: [SCOPE_1, SCOPE_2], }, 'test', ); @@ -303,13 +303,13 @@ describe('NoteStore', () => { }); it('returns no notes when filtering by non-existing contractAddress', async () => { - const notes = await noteStore.getNotes({ contractAddress: FAKE_ADDRESS, scopes: 'ALL_SCOPES' }, 'test'); + const notes = await noteStore.getNotes({ contractAddress: FAKE_ADDRESS, scopes: [SCOPE_1, SCOPE_2] }, 'test'); expect(notes).toHaveLength(0); }); it('returns no notes when filtering by non-existing storageSlot', async () => { const notes = await noteStore.getNotes( - { contractAddress: CONTRACT_A, storageSlot: NON_EXISTING_SLOT, scopes: 'ALL_SCOPES' }, + { contractAddress: CONTRACT_A, storageSlot: NON_EXISTING_SLOT, scopes: [SCOPE_1, SCOPE_2] }, 'test', ); expect(notes).toHaveLength(0); @@ -329,7 +329,7 @@ describe('NoteStore', () => { const filter = { contractAddress: CONTRACT_A, siloedNullifier: NON_EXISTING_SLOT, - scopes: 'ALL_SCOPES' as const, + scopes: [SCOPE_1, SCOPE_2], }; const notes = await noteStore.getNotes(filter, 'test'); @@ -340,7 +340,7 @@ describe('NoteStore', () => { const filter = { contractAddress: CONTRACT_B, siloedNullifier: note2.siloedNullifier, - scopes: 'ALL_SCOPES' as const, + scopes: [SCOPE_1, SCOPE_2], }; const notes = await noteStore.getNotes(filter, 'test'); @@ -372,12 +372,12 @@ describe('NoteStore', () => { const result = await noteStore.applyNullifiers([mkNullifier(note1)], 'test'); expect(result).toEqual([note1]); - const active = await noteStore.getNotes({ contractAddress: CONTRACT_A, scopes: 'ALL_SCOPES' }, 'test'); + const active = await noteStore.getNotes({ contractAddress: CONTRACT_A, scopes: [SCOPE_1, SCOPE_2] }, 'test'); const all = await noteStore.getNotes( { contractAddress: CONTRACT_A, status: NoteStatus.ACTIVE_OR_NULLIFIED, - scopes: 'ALL_SCOPES', + scopes: [SCOPE_1, SCOPE_2], }, 'test', ); @@ -390,8 +390,8 @@ describe('NoteStore', () => { const nullifiers = [mkNullifier(note1), mkNullifier(note3)]; const result = await noteStore.applyNullifiers(nullifiers, 'test'); - const activeA = await noteStore.getNotes({ contractAddress: CONTRACT_A, scopes: 'ALL_SCOPES' }, 'test'); - const activeB = await noteStore.getNotes({ contractAddress: CONTRACT_B, scopes: 'ALL_SCOPES' }, 'test'); + const activeA = await noteStore.getNotes({ contractAddress: CONTRACT_A, scopes: [SCOPE_1, SCOPE_2] }, 'test'); + const activeB = await noteStore.getNotes({ contractAddress: CONTRACT_B, scopes: [SCOPE_1, SCOPE_2] }, 'test'); expect(result).toEqual([note1, note3]); // returned nullified notes expect(nullifierSet(activeA)).toEqual(nullifierSet([note2])); // note2 remains active @@ -405,7 +405,7 @@ describe('NoteStore', () => { contractAddress: CONTRACT_A, siloedNullifier: note2.siloedNullifier, status: NoteStatus.ACTIVE_OR_NULLIFIED, - scopes: 'ALL_SCOPES' as const, + scopes: [SCOPE_1, SCOPE_2], }; const notes = await noteStore.getNotes(filter, 'test'); @@ -479,7 +479,10 @@ describe('NoteStore', () => { // Verify notes are still active (transaction rolled back) await verifyAndCommitForEachJob(['test', 'after-job-commit'], noteStore, async (jobId: string) => { - const activeNotes = await noteStore.getNotes({ contractAddress: CONTRACT_A, scopes: 'ALL_SCOPES' }, jobId); + const activeNotes = await noteStore.getNotes( + { contractAddress: CONTRACT_A, scopes: [SCOPE_1, SCOPE_2] }, + jobId, + ); expect(nullifierSet(activeNotes)).toEqual(nullifierSet([note1, note2])); }); }); @@ -495,7 +498,10 @@ describe('NoteStore', () => { expect(result).toEqual([]); await verifyAndCommitForEachJob(['test', 'after-job-commit'], noteStore, async (jobId: string) => { - const activeNotes = await noteStore.getNotes({ contractAddress: CONTRACT_A, scopes: 'ALL_SCOPES' }, jobId); + const activeNotes = await noteStore.getNotes( + { contractAddress: CONTRACT_A, scopes: [SCOPE_1, SCOPE_2] }, + jobId, + ); expect(nullifierSet(activeNotes)).toEqual(nullifierSet([note2])); }); }); @@ -519,11 +525,14 @@ describe('NoteStore', () => { // Verify note is now in nullified state await verifyAndCommitForEachJob(['fresh-job', 'after-job-commit'], noteStore, async (jobId: string) => { - const activeNotes = await noteStore.getNotes({ contractAddress: CONTRACT_A, scopes: 'ALL_SCOPES' }, jobId); + const activeNotes = await noteStore.getNotes( + { contractAddress: CONTRACT_A, scopes: [SCOPE_1, SCOPE_2] }, + jobId, + ); expect(nullifierSet(activeNotes)).not.toContain(freshNullifier.toBigInt()); const allNotes = await noteStore.getNotes( - { contractAddress: CONTRACT_A, status: NoteStatus.ACTIVE_OR_NULLIFIED, scopes: 'ALL_SCOPES' }, + { contractAddress: CONTRACT_A, status: NoteStatus.ACTIVE_OR_NULLIFIED, scopes: [SCOPE_1, SCOPE_2] }, jobId, ); expect(nullifierSet(allNotes)).toContain(freshNullifier.toBigInt()); @@ -553,14 +562,17 @@ describe('NoteStore', () => { // Verify all notes are nullified await verifyAndCommitForEachJob(['concurrent-job', 'after-job-commit'], noteStore, async (jobId: string) => { - const activeNotes = await noteStore.getNotes({ contractAddress: CONTRACT_A, scopes: 'ALL_SCOPES' }, jobId); + const activeNotes = await noteStore.getNotes( + { contractAddress: CONTRACT_A, scopes: [SCOPE_1, SCOPE_2] }, + jobId, + ); const activeNullifiers = nullifierSet(activeNotes); for (const nullifier of noteNullifiers) { expect(activeNullifiers).not.toContain(nullifier.toBigInt()); } const allNotes = await noteStore.getNotes( - { contractAddress: CONTRACT_A, status: NoteStatus.ACTIVE_OR_NULLIFIED, scopes: 'ALL_SCOPES' }, + { contractAddress: CONTRACT_A, status: NoteStatus.ACTIVE_OR_NULLIFIED, scopes: [SCOPE_1, SCOPE_2] }, jobId, ); expect(nullifierSet(allNotes)).toEqual(nullifierSet([note1, note2, ...noteNullifiers])); @@ -578,11 +590,14 @@ describe('NoteStore', () => { // Verify the note is in nullified state await verifyAndCommitForEachJob(['new-job', 'after-job-commit'], noteStore, async (jobId: string) => { - const activeNotes = await noteStore.getNotes({ contractAddress: CONTRACT_A, scopes: 'ALL_SCOPES' }, jobId); + const activeNotes = await noteStore.getNotes( + { contractAddress: CONTRACT_A, scopes: [SCOPE_1, SCOPE_2] }, + jobId, + ); expect(nullifierSet(activeNotes)).not.toContain(note1.siloedNullifier.toBigInt()); const allNotes = await noteStore.getNotes( - { contractAddress: CONTRACT_A, status: NoteStatus.ACTIVE_OR_NULLIFIED, scopes: 'ALL_SCOPES' }, + { contractAddress: CONTRACT_A, status: NoteStatus.ACTIVE_OR_NULLIFIED, scopes: [SCOPE_1, SCOPE_2] }, jobId, ); expect(nullifierSet(allNotes)).toContain(note1.siloedNullifier.toBigInt()); @@ -622,7 +637,7 @@ describe('NoteStore', () => { // Verify the note is nullified and has both scopes await verifyAndCommitForEachJob(['duplicate-job', 'after-job-commit'], noteStore, async (jobId: string) => { const allNotes = await noteStore.getNotes( - { contractAddress: CONTRACT_A, status: NoteStatus.ACTIVE_OR_NULLIFIED, scopes: 'ALL_SCOPES' }, + { contractAddress: CONTRACT_A, status: NoteStatus.ACTIVE_OR_NULLIFIED, scopes: [SCOPE_1, SCOPE_2] }, jobId, ); expect(nullifierSet(allNotes)).toContain(duplicateNullifier.toBigInt()); @@ -678,7 +693,10 @@ describe('NoteStore', () => { it('restores notes that were nullified after the rollback block', async () => { // noteBlock2 remains active, noteBlock3 was nullified at block 4 should be restored - const activeNotes = await noteStore.getNotes({ contractAddress: CONTRACT_A, scopes: 'ALL_SCOPES' }, 'test'); + const activeNotes = await noteStore.getNotes( + { contractAddress: CONTRACT_A, scopes: [SCOPE_1, SCOPE_2] }, + 'test', + ); expect(nullifierSet(activeNotes)).toEqual(nullifierSet([noteBlock2, noteBlock3])); }); @@ -687,7 +705,7 @@ describe('NoteStore', () => { { contractAddress: CONTRACT_A, status: NoteStatus.ACTIVE_OR_NULLIFIED, - scopes: 'ALL_SCOPES', + scopes: [SCOPE_1, SCOPE_2], }, 'test', ); @@ -696,13 +714,19 @@ describe('NoteStore', () => { expect(nullifierSet(allNotes)).toEqual(nullifierSet([noteBlock1, noteBlock2, noteBlock3])); // Verify noteBlock1 is not in active notes - const activeNotes = await noteStore.getNotes({ contractAddress: CONTRACT_A, scopes: 'ALL_SCOPES' }, 'test'); + const activeNotes = await noteStore.getNotes( + { contractAddress: CONTRACT_A, scopes: [SCOPE_1, SCOPE_2] }, + 'test', + ); expect(nullifierSet(activeNotes)).not.toContain(noteBlock1.siloedNullifier.toBigInt()); }); it('preserves active notes created before the rollback block that were never nullified', async () => { // noteBlock2 was created at block 2 (before rollback block 3) and never nullified - const activeNotes = await noteStore.getNotes({ contractAddress: CONTRACT_A, scopes: 'ALL_SCOPES' }, 'test'); + const activeNotes = await noteStore.getNotes( + { contractAddress: CONTRACT_A, scopes: [SCOPE_1, SCOPE_2] }, + 'test', + ); expect(nullifierSet(activeNotes)).toEqual(nullifierSet([noteBlock2, noteBlock3])); }); @@ -711,7 +735,7 @@ describe('NoteStore', () => { { contractAddress: CONTRACT_A, status: NoteStatus.ACTIVE_OR_NULLIFIED, - scopes: 'ALL_SCOPES', + scopes: [SCOPE_1, SCOPE_2], }, 'test', ); @@ -742,14 +766,17 @@ describe('NoteStore', () => { await noteStore.commit('test'); await noteStore.rollback(5, 5); - const activeNotes = await noteStore.getNotes({ contractAddress: CONTRACT_A, scopes: 'ALL_SCOPES' }, 'test'); + const activeNotes = await noteStore.getNotes( + { contractAddress: CONTRACT_A, scopes: [SCOPE_1, SCOPE_2] }, + 'test', + ); expect(activeNotes).toHaveLength(0); const allNotes = await noteStore.getNotes( { contractAddress: CONTRACT_A, status: NoteStatus.ACTIVE_OR_NULLIFIED, - scopes: 'ALL_SCOPES', + scopes: [SCOPE_1, SCOPE_2], }, 'test', ); @@ -774,14 +801,17 @@ describe('NoteStore', () => { await noteStore.commit('test'); await noteStore.rollback(6, 4); - const activeNotes = await noteStore.getNotes({ contractAddress: CONTRACT_A, scopes: 'ALL_SCOPES' }, 'test'); + const activeNotes = await noteStore.getNotes( + { contractAddress: CONTRACT_A, scopes: [SCOPE_1, SCOPE_2] }, + 'test', + ); expect(activeNotes).toHaveLength(0); const allNotes = await noteStore.getNotes( { contractAddress: CONTRACT_A, status: NoteStatus.ACTIVE_OR_NULLIFIED, - scopes: 'ALL_SCOPES', + scopes: [SCOPE_1, SCOPE_2], }, 'test', ); @@ -808,13 +838,16 @@ describe('NoteStore', () => { // note1 should be restored (nullified at block 7 > rollback block 5) // note2 should be deleted (created at block 10 > rollback block 5) - const activeNotes = await noteStore.getNotes({ contractAddress: CONTRACT_A, scopes: 'ALL_SCOPES' }, 'test'); + const activeNotes = await noteStore.getNotes( + { contractAddress: CONTRACT_A, scopes: [SCOPE_1, SCOPE_2] }, + 'test', + ); expect(nullifierSet(activeNotes)).toEqual(nullifierSet([note1Nullifier])); }); it('handles rollback on empty PXE database gracefully', async () => { await expect(noteStore.rollback(10, 20)).resolves.not.toThrow(); - const notes = await noteStore.getNotes({ contractAddress: CONTRACT_A, scopes: 'ALL_SCOPES' }, 'test'); + const notes = await noteStore.getNotes({ contractAddress: CONTRACT_A, scopes: [SCOPE_1, SCOPE_2] }, 'test'); expect(notes).toHaveLength(0); }); diff --git a/yarn-project/pxe/src/storage/note_store/note_store.ts b/yarn-project/pxe/src/storage/note_store/note_store.ts index 7d93c1e24a40..ae43b85e5b09 100644 --- a/yarn-project/pxe/src/storage/note_store/note_store.ts +++ b/yarn-project/pxe/src/storage/note_store/note_store.ts @@ -106,7 +106,7 @@ export class NoteStore implements StagedStore { * returned once if this is the case) */ getNotes(filter: NotesFilter, jobId: string): Promise { - if (filter.scopes !== 'ALL_SCOPES' && filter.scopes.length === 0) { + if (filter.scopes.length === 0) { return Promise.resolve([]); } @@ -180,10 +180,7 @@ export class NoteStore implements StagedStore { continue; } - if ( - filter.scopes !== 'ALL_SCOPES' && - note.scopes.intersection(new Set(filter.scopes.map(s => s.toString()))).size === 0 - ) { + if (note.scopes.intersection(new Set(filter.scopes.map(s => s.toString()))).size === 0) { continue; } diff --git a/yarn-project/sequencer-client/src/publisher/sequencer-publisher-factory.test.ts b/yarn-project/sequencer-client/src/publisher/sequencer-publisher-factory.test.ts index a06a0b62e2e7..4dff76d54deb 100644 --- a/yarn-project/sequencer-client/src/publisher/sequencer-publisher-factory.test.ts +++ b/yarn-project/sequencer-client/src/publisher/sequencer-publisher-factory.test.ts @@ -34,6 +34,7 @@ describe('SequencerPublisherFactory', () => { beforeEach(() => { mockConfig = { ethereumSlotDuration: 12, + aztecSlotDuration: 24, } as SequencerClientConfig; mockPublisherManager = mock>(); mockBlobClient = mock(); diff --git a/yarn-project/sequencer-client/src/publisher/sequencer-publisher.test.ts b/yarn-project/sequencer-client/src/publisher/sequencer-publisher.test.ts index 12592d5a1042..27a0e8f4cbe5 100644 --- a/yarn-project/sequencer-client/src/publisher/sequencer-publisher.test.ts +++ b/yarn-project/sequencer-client/src/publisher/sequencer-publisher.test.ts @@ -118,11 +118,12 @@ describe('SequencerPublisher', () => { rollupAddress: EthAddress.ZERO.toString(), governanceProposerAddress: mockGovernanceProposerAddress, }, - ...defaultL1TxUtilsConfig, + ethereumSlotDuration: 12, + aztecSlotDuration: 24, } as unknown as TxSenderConfig & PublisherConfig & - Pick & + Pick & L1TxUtilsConfig; rollup = mock(); @@ -220,7 +221,6 @@ describe('SequencerPublisher', () => { await publisher.enqueueGovernanceCastSignal( govPayload, SlotNumber(2), - 1n, EthAddress.fromString(testHarnessAttesterAccount.address), msg => testHarnessAttesterAccount.signTypedData(msg), ), @@ -448,7 +448,6 @@ describe('SequencerPublisher', () => { await publisher.enqueueGovernanceCastSignal( govPayload, SlotNumber(2), - 1n, EthAddress.fromString(testHarnessAttesterAccount.address), msg => testHarnessAttesterAccount.signTypedData(msg), ), @@ -463,7 +462,6 @@ describe('SequencerPublisher', () => { await publisher.enqueueGovernanceCastSignal( govPayload, SlotNumber(2), - 1n, EthAddress.fromString(testHarnessAttesterAccount.address), msg => testHarnessAttesterAccount.signTypedData(msg), ), @@ -478,7 +476,6 @@ describe('SequencerPublisher', () => { await publisher.enqueueGovernanceCastSignal( govPayload, SlotNumber(2), - 1n, EthAddress.fromString(testHarnessAttesterAccount.address), msg => testHarnessAttesterAccount.signTypedData(msg), ), @@ -493,7 +490,6 @@ describe('SequencerPublisher', () => { await publisher.enqueueGovernanceCastSignal( govPayload, SlotNumber(2), - 1n, EthAddress.fromString(testHarnessAttesterAccount.address), msg => testHarnessAttesterAccount.signTypedData(msg), ), @@ -507,7 +503,6 @@ describe('SequencerPublisher', () => { await publisher.enqueueGovernanceCastSignal( govPayload, SlotNumber(2), - 1n, EthAddress.fromString(testHarnessAttesterAccount.address), msg => testHarnessAttesterAccount.signTypedData(msg), ); @@ -515,7 +510,6 @@ describe('SequencerPublisher', () => { await publisher.enqueueGovernanceCastSignal( govPayload, SlotNumber(3), - 2n, EthAddress.fromString(testHarnessAttesterAccount.address), msg => testHarnessAttesterAccount.signTypedData(msg), ); @@ -534,7 +528,6 @@ describe('SequencerPublisher', () => { await publisher.enqueueGovernanceCastSignal( govPayload, SlotNumber(2), - 1n, EthAddress.fromString(testHarnessAttesterAccount.address), msg => testHarnessAttesterAccount.signTypedData(msg), ), @@ -549,7 +542,6 @@ describe('SequencerPublisher', () => { await publisher.enqueueGovernanceCastSignal( govPayload, SlotNumber(2), - 1n, EthAddress.fromString(testHarnessAttesterAccount.address), msg => testHarnessAttesterAccount.signTypedData(msg), ), @@ -565,7 +557,6 @@ describe('SequencerPublisher', () => { await publisher.enqueueGovernanceCastSignal( govPayload, SlotNumber(2), - 1n, EthAddress.fromString(testHarnessAttesterAccount.address), msg => testHarnessAttesterAccount.signTypedData(msg), ), @@ -576,7 +567,6 @@ describe('SequencerPublisher', () => { await publisher.enqueueGovernanceCastSignal( govPayload, SlotNumber(3), - 2n, EthAddress.fromString(testHarnessAttesterAccount.address), msg => testHarnessAttesterAccount.signTypedData(msg), ), diff --git a/yarn-project/sequencer-client/src/publisher/sequencer-publisher.ts b/yarn-project/sequencer-client/src/publisher/sequencer-publisher.ts index 3ff0e0d87893..e93c1f1a145f 100644 --- a/yarn-project/sequencer-client/src/publisher/sequencer-publisher.ts +++ b/yarn-project/sequencer-client/src/publisher/sequencer-publisher.ts @@ -41,7 +41,7 @@ import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts'; import { type ProposerSlashAction, encodeSlashConsensusVotes } from '@aztec/slasher'; import { CommitteeAttestationsAndSigners, type ValidateCheckpointResult } from '@aztec/stdlib/block'; import type { Checkpoint } from '@aztec/stdlib/checkpoint'; -import { getNextL1SlotTimestamp } from '@aztec/stdlib/epoch-helpers'; +import { getLastL1SlotTimestampForL2Slot, getNextL1SlotTimestamp } from '@aztec/stdlib/epoch-helpers'; import { SlashFactoryContract } from '@aztec/stdlib/l1-contracts'; import type { CheckpointHeader } from '@aztec/stdlib/rollup'; import type { L1PublishCheckpointStats } from '@aztec/stdlib/stats'; @@ -122,6 +122,7 @@ export class SequencerPublisher { protected log: Logger; protected ethereumSlotDuration: bigint; + protected aztecSlotDuration: bigint; private dateProvider: DateProvider; private blobClient: BlobClientInterface; @@ -153,7 +154,7 @@ export class SequencerPublisher { constructor( private config: Pick & - Pick & { l1ChainId: number }, + Pick & { l1ChainId: number }, deps: { telemetry?: TelemetryClient; blobClient: BlobClientInterface; @@ -171,6 +172,7 @@ export class SequencerPublisher { ) { this.log = deps.log ?? createLogger('sequencer:publisher'); this.ethereumSlotDuration = BigInt(config.ethereumSlotDuration); + this.aztecSlotDuration = BigInt(config.aztecSlotDuration); this.dateProvider = deps.dateProvider; this.epochCache = deps.epochCache; this.lastActions = deps.lastActions; @@ -457,15 +459,17 @@ export class SequencerPublisher { * @param tipArchive - The archive to check * @returns The slot and block number if it is possible to propose, undefined otherwise */ - public async canProposeAt( + public canProposeAt( tipArchive: Fr, msgSender: EthAddress, - opts: { forcePendingCheckpointNumber?: CheckpointNumber } = {}, + opts: { forcePendingCheckpointNumber?: CheckpointNumber; pipelined?: boolean } = {}, ) { // TODO: #14291 - should loop through multiple keys to check if any of them can propose const ignoredErrors = ['SlotAlreadyInChain', 'InvalidProposer', 'InvalidArchive']; - const nextL1SlotTs = await this.getNextL1SlotTimestampWithL1Floor(); + const pipelined = opts.pipelined ?? false; + const slotOffset = pipelined ? this.aztecSlotDuration : 0n; + const nextL1SlotTs = this.getNextL1SlotTimestamp() + slotOffset; return this.rollupContract .canProposeAt(tipArchive.toBuffer(), msgSender.toString(), nextL1SlotTs, { @@ -505,7 +509,7 @@ export class SequencerPublisher { flags, ] as const; - const ts = await this.getNextL1SlotTimestampWithL1Floor(); + const ts = this.getSimulationTimestamp(header.slotNumber); const stateOverrides = await this.rollupContract.makePendingCheckpointNumberOverride( opts?.forcePendingCheckpointNumber, ); @@ -528,7 +532,7 @@ export class SequencerPublisher { data: encodeFunctionData({ abi: RollupAbi, functionName: 'validateHeaderWithAttestations', args }), from: MULTI_CALL_3_ADDRESS, }, - { time: ts + 1n }, + { time: ts }, stateOverrides, ); this.log.debug(`Simulated validateHeader`); @@ -655,8 +659,7 @@ export class SequencerPublisher { attestationsAndSigners: CommitteeAttestationsAndSigners, attestationsAndSignersSignature: Signature, options: { forcePendingCheckpointNumber?: CheckpointNumber }, - ): Promise { - const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration); + ): Promise { const blobFields = checkpoint.toBlobFields(); const blobs = await getBlobsPerL1Block(blobFields); const blobInput = getPrefixedEthBlobCommitments(blobs); @@ -675,13 +678,11 @@ export class SequencerPublisher { blobInput, ] as const; - await this.simulateProposeTx(args, ts, options); - return ts; + await this.simulateProposeTx(args, options); } private async enqueueCastSignalHelper( slotNumber: SlotNumber, - timestamp: bigint, signalType: GovernanceSignalAction, payload: EthAddress, base: IEmpireBase, @@ -759,11 +760,16 @@ export class SequencerPublisher { lastValidL2Slot: slotNumber, }); + const timestamp = this.getSimulationTimestamp(slotNumber); + try { await this.l1TxUtils.simulate(request, { time: timestamp }, [], mergeAbis([request.abi ?? [], ErrorsAbi])); this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, { request }); } catch (err) { - this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, err); + const viemError = formatViemError(err); + this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, viemError, { + simulationTimestamp: timestamp, + }); // Yes, we enqueue the request anyway, in case there was a bug with the simulation itself } @@ -814,19 +820,16 @@ export class SequencerPublisher { /** * Enqueues a governance castSignal transaction to cast a signal for a given slot number. * @param slotNumber - The slot number to cast a signal for. - * @param timestamp - The timestamp of the slot to cast a signal for. * @returns True if the signal was successfully enqueued, false otherwise. */ public enqueueGovernanceCastSignal( governancePayload: EthAddress, slotNumber: SlotNumber, - timestamp: bigint, signerAddress: EthAddress, signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>, ): Promise { return this.enqueueCastSignalHelper( slotNumber, - timestamp, 'governance-signal', governancePayload, this.govProposerContract, @@ -839,7 +842,6 @@ export class SequencerPublisher { public async enqueueSlashingActions( actions: ProposerSlashAction[], slotNumber: SlotNumber, - timestamp: bigint, signerAddress: EthAddress, signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>, ): Promise { @@ -860,7 +862,6 @@ export class SequencerPublisher { }); await this.enqueueCastSignalHelper( slotNumber, - timestamp, 'empire-slashing-signal', action.payload, this.slashingProposerContract, @@ -879,7 +880,6 @@ export class SequencerPublisher { (receipt: TransactionReceipt) => !!this.slashFactoryContract.tryExtractSlashPayloadCreatedEvent(receipt.logs), slotNumber, - timestamp, ); break; } @@ -897,7 +897,6 @@ export class SequencerPublisher { request, (receipt: TransactionReceipt) => !!empireSlashingProposer.tryExtractPayloadSubmittedEvent(receipt.logs), slotNumber, - timestamp, ); break; } @@ -921,7 +920,6 @@ export class SequencerPublisher { request, (receipt: TransactionReceipt) => !!tallySlashingProposer.tryExtractVoteCastEvent(receipt.logs), slotNumber, - timestamp, ); break; } @@ -943,7 +941,6 @@ export class SequencerPublisher { request, (receipt: TransactionReceipt) => !!tallySlashingProposer.tryExtractRoundExecutedEvent(receipt.logs), slotNumber, - timestamp, ); break; } @@ -979,15 +976,13 @@ export class SequencerPublisher { feeAssetPriceModifier: checkpoint.feeAssetPriceModifier, }; - let ts: bigint; - try { // @note This will make sure that we are passing the checks for our header ASSUMING that the data is also made available // This means that we can avoid the simulation issues in later checks. // By simulation issue, I mean the fact that the block.timestamp is equal to the last block, not the next, which // make time consistency checks break. // TODO(palla): Check whether we're validating twice, once here and once within addProposeTx, since we call simulateProposeTx in both places. - ts = await this.validateCheckpointForSubmission( + await this.validateCheckpointForSubmission( checkpoint, attestationsAndSigners, attestationsAndSignersSignature, @@ -1003,7 +998,7 @@ export class SequencerPublisher { } this.log.verbose(`Enqueuing checkpoint propose transaction`, { ...checkpoint.toCheckpointInfo(), ...opts }); - await this.addProposeTx(checkpoint, proposeTxArgs, opts, ts); + await this.addProposeTx(checkpoint, proposeTxArgs, opts); } public enqueueInvalidateCheckpoint( @@ -1046,8 +1041,8 @@ export class SequencerPublisher { request: L1TxRequest, checkSuccess: (receipt: TransactionReceipt) => boolean | undefined, slotNumber: SlotNumber, - timestamp: bigint, ) { + const timestamp = this.getSimulationTimestamp(slotNumber); const logData = { slotNumber, timestamp, gasLimit: undefined as bigint | undefined }; if (this.lastActions[action] && this.lastActions[action] === slotNumber) { this.log.debug(`Skipping duplicate action ${action} for slot ${slotNumber}`); @@ -1061,8 +1056,9 @@ export class SequencerPublisher { let gasUsed: bigint; const simulateAbi = mergeAbis([request.abi ?? [], ErrorsAbi]); + try { - ({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [], simulateAbi)); // TODO(palla/slash): Check the timestamp logic + ({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [], simulateAbi)); this.log.verbose(`Simulation for ${action} succeeded`, { ...logData, request, gasUsed }); } catch (err) { const viemError = formatViemError(err, simulateAbi); @@ -1118,7 +1114,6 @@ export class SequencerPublisher { private async prepareProposeTx( encodedData: L1ProcessArgs, - timestamp: bigint, options: { forcePendingCheckpointNumber?: CheckpointNumber }, ) { const kzg = Blob.getViemKzgInstance(); @@ -1173,7 +1168,7 @@ export class SequencerPublisher { blobInput, ] as const; - const { rollupData, simulationResult } = await this.simulateProposeTx(args, timestamp, options); + const { rollupData, simulationResult } = await this.simulateProposeTx(args, options); return { args, blobEvaluationGas, rollupData, simulationResult }; } @@ -1181,7 +1176,6 @@ export class SequencerPublisher { /** * Simulates the propose tx with eth_simulateV1 * @param args - The propose tx args - * @param timestamp - The timestamp to simulate proposal at * @returns The simulation result */ private async simulateProposeTx( @@ -1198,7 +1192,6 @@ export class SequencerPublisher { ViemSignature, `0x${string}`, ], - timestamp: bigint, options: { forcePendingCheckpointNumber?: CheckpointNumber }, ) { const rollupData = encodeFunctionData({ @@ -1232,6 +1225,8 @@ export class SequencerPublisher { }); } + const simTs = this.getSimulationTimestamp(SlotNumber.fromBigInt(args[0].header.slotNumber)); + const simulationResult = await this.l1TxUtils .simulate( { @@ -1241,8 +1236,7 @@ export class SequencerPublisher { ...(this.proposerAddressForSimulation && { from: this.proposerAddressForSimulation.toString() }), }, { - // @note we add 1n to the timestamp because geth implementation doesn't like simulation timestamp to be equal to the current block timestamp - time: timestamp + 1n, + time: simTs, // @note reth should have a 30m gas limit per block but throws errors that this tx is beyond limit so we increase here gasLimit: MAX_L1_TX_LIMIT * 2n, }, @@ -1264,7 +1258,7 @@ export class SequencerPublisher { logs: [], }; } - this.log.error(`Failed to simulate propose tx`, viemError); + this.log.error(`Failed to simulate propose tx`, viemError, { simulationTimestamp: simTs }); throw err; }); @@ -1275,16 +1269,11 @@ export class SequencerPublisher { checkpoint: Checkpoint, encodedData: L1ProcessArgs, opts: { txTimeoutAt?: Date; forcePendingCheckpointNumber?: CheckpointNumber } = {}, - timestamp: bigint, ): Promise { const slot = checkpoint.header.slotNumber; const timer = new Timer(); const kzg = Blob.getViemKzgInstance(); - const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx( - encodedData, - timestamp, - opts, - ); + const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(encodedData, opts); const startBlock = await this.l1TxUtils.getBlockNumber(); const gasLimit = this.l1TxUtils.bumpGasLimit( BigInt(Math.ceil((Number(simulationResult.gasUsed) * 64) / 63)) + @@ -1361,20 +1350,16 @@ export class SequencerPublisher { }); } - /** - * Returns the timestamp to use when simulating L1 proposal calls. - * Uses the wall-clock-based next L1 slot boundary, but floors it with the latest L1 block timestamp - * plus one slot duration. This prevents the sequencer from targeting a future L2 slot when the L1 - * chain hasn't caught up to the wall clock yet (e.g., the dateProvider is one L1 slot ahead of the - * latest mined block), which would cause the propose tx to land in an L1 block with block.timestamp - * still in the previous L2 slot. - * TODO(palla): Properly fix by keeping dateProvider synced with anvil's chain time on every block. - */ - private async getNextL1SlotTimestampWithL1Floor(): Promise { + /** Returns the timestamp of the last L1 slot within a given L2 slot. Used as the simulation timestamp + * for eth_simulateV1 calls, since it's guaranteed to be greater than any L1 block produced during the slot. */ + private getSimulationTimestamp(slot: SlotNumber): bigint { + const l1Constants = this.epochCache.getL1Constants(); + return getLastL1SlotTimestampForL2Slot(slot, l1Constants); + } + + /** Returns the timestamp of the next L1 slot boundary after now. */ + private getNextL1SlotTimestamp(): bigint { const l1Constants = this.epochCache.getL1Constants(); - const fromWallClock = getNextL1SlotTimestamp(this.dateProvider.nowInSeconds(), l1Constants); - const latestBlock = await this.l1TxUtils.client.getBlock(); - const fromL1Block = latestBlock.timestamp + BigInt(l1Constants.ethereumSlotDuration); - return fromWallClock > fromL1Block ? fromWallClock : fromL1Block; + return getNextL1SlotTimestamp(this.dateProvider.nowInSeconds(), l1Constants); } } diff --git a/yarn-project/sequencer-client/src/sequencer/checkpoint_voter.ha.integration.test.ts b/yarn-project/sequencer-client/src/sequencer/checkpoint_voter.ha.integration.test.ts index e5c0c52dc7c1..6d75678c3fb0 100644 --- a/yarn-project/sequencer-client/src/sequencer/checkpoint_voter.ha.integration.test.ts +++ b/yarn-project/sequencer-client/src/sequencer/checkpoint_voter.ha.integration.test.ts @@ -277,6 +277,7 @@ describe('CheckpointVoter HA Integration', () => { requiredConfirmations: 1, maxL1TxInclusionWaitPulseSeconds: 60, ethereumSlotDuration: DefaultL1ContractsConfig.ethereumSlotDuration, + aztecSlotDuration: DefaultL1ContractsConfig.aztecSlotDuration, fishermanMode: false, l1ChainId: 1, }; @@ -291,6 +292,7 @@ describe('CheckpointVoter HA Integration', () => { ts: BigInt(Math.floor(Date.now() / 1000)), nowMs: BigInt(Date.now()), }); + epochCache.getL1Constants.mockReturnValue(TEST_L1_CONSTANTS as any); const slashFactoryContract = mock(); diff --git a/yarn-project/sequencer-client/src/sequencer/checkpoint_voter.ts b/yarn-project/sequencer-client/src/sequencer/checkpoint_voter.ts index 5a6ff07c03ee..0bf72881179d 100644 --- a/yarn-project/sequencer-client/src/sequencer/checkpoint_voter.ts +++ b/yarn-project/sequencer-client/src/sequencer/checkpoint_voter.ts @@ -2,7 +2,6 @@ import type { SlotNumber } from '@aztec/foundation/branded-types'; import type { EthAddress } from '@aztec/foundation/eth-address'; import type { Logger } from '@aztec/foundation/log'; import type { SlasherClientInterface } from '@aztec/slasher'; -import { getTimestampForSlot } from '@aztec/stdlib/epoch-helpers'; import type { ResolvedSequencerConfig } from '@aztec/stdlib/interfaces/server'; import type { ValidatorClient } from '@aztec/validator-client'; import { DutyAlreadySignedError } from '@aztec/validator-ha-signer/errors'; @@ -18,7 +17,6 @@ import type { SequencerRollupConstants } from './types.js'; * Handles governance and slashing voting for a given slot. */ export class CheckpointVoter { - private slotTimestamp: bigint; private governanceSigner: (msg: TypedDataDefinition) => Promise<`0x${string}`>; private slashingSigner: (msg: TypedDataDefinition) => Promise<`0x${string}`>; @@ -33,8 +31,6 @@ export class CheckpointVoter { private readonly metrics: SequencerMetrics, private readonly log: Logger, ) { - this.slotTimestamp = getTimestampForSlot(this.slot, this.l1Constants); - // Create separate signers with appropriate duty contexts for governance and slashing votes // These use HA protection to ensure only one node signs per slot/duty const governanceContext: SigningContext = { slot: this.slot, dutyType: DutyType.GOVERNANCE_VOTE }; @@ -77,7 +73,6 @@ export class CheckpointVoter { return await this.publisher.enqueueGovernanceCastSignal( governanceProposerPayload, this.slot, - this.slotTimestamp, this.attestorAddress, this.governanceSigner, ); @@ -108,13 +103,7 @@ export class CheckpointVoter { this.metrics.recordSlashingAttempt(actions.length); - return await this.publisher.enqueueSlashingActions( - actions, - this.slot, - this.slotTimestamp, - this.attestorAddress, - this.slashingSigner, - ); + return await this.publisher.enqueueSlashingActions(actions, this.slot, this.attestorAddress, this.slashingSigner); } catch (err) { if (err instanceof DutyAlreadySignedError) { this.log.info(`Slashing vote already signed by another node`, { diff --git a/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts b/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts index d6d6fcc80dce..0ec5e38f5439 100644 --- a/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts +++ b/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts @@ -630,7 +630,6 @@ describe('sequencer', () => { expect(publisher.enqueueGovernanceCastSignal).toHaveBeenCalledWith( governancePayload, SlotNumber(1), - expect.any(BigInt), expect.any(EthAddress), expect.any(Function), ); diff --git a/yarn-project/stdlib/src/epoch-helpers/index.test.ts b/yarn-project/stdlib/src/epoch-helpers/index.test.ts index 150d8216714d..4e89e3b127da 100644 --- a/yarn-project/stdlib/src/epoch-helpers/index.test.ts +++ b/yarn-project/stdlib/src/epoch-helpers/index.test.ts @@ -1,6 +1,11 @@ -import { EpochNumber } from '@aztec/foundation/branded-types'; +import { EpochNumber, SlotNumber } from '@aztec/foundation/branded-types'; -import { type L1RollupConstants, getProofSubmissionDeadlineTimestamp, getTimestampRangeForEpoch } from './index.js'; +import { + type L1RollupConstants, + getLastL1SlotTimestampForL2Slot, + getProofSubmissionDeadlineTimestamp, + getTimestampRangeForEpoch, +} from './index.js'; describe('EpochHelpers', () => { let constants: Omit; @@ -34,4 +39,14 @@ describe('EpochHelpers', () => { const deadline = getProofSubmissionDeadlineTimestamp(EpochNumber.fromBigInt(3n), constants); expect(deadline).toEqual(l1GenesisTime + BigInt(24 * 4 * 3) + BigInt(24 * 8)); }); + + it('returns last L1 slot timestamp for L2 slot', () => { + // L2 slot 0 starts at l1GenesisTime, lasts 24s with 12s L1 slots, so last L1 slot is at +12 + const ts = getLastL1SlotTimestampForL2Slot(SlotNumber(0), constants); + expect(ts).toEqual(l1GenesisTime + BigInt(24 - 12)); + + // L2 slot 5 starts at l1GenesisTime + 5*24 = +120, last L1 slot at +120+12 = +132 + const ts2 = getLastL1SlotTimestampForL2Slot(SlotNumber(5), constants); + expect(ts2).toEqual(l1GenesisTime + BigInt(5 * 24 + 24 - 12)); + }); }); diff --git a/yarn-project/stdlib/src/epoch-helpers/index.ts b/yarn-project/stdlib/src/epoch-helpers/index.ts index 0ae35f5461f4..3f4d8ccf4476 100644 --- a/yarn-project/stdlib/src/epoch-helpers/index.ts +++ b/yarn-project/stdlib/src/epoch-helpers/index.ts @@ -68,6 +68,14 @@ export function getNextL1SlotTimestamp( return constants.l1GenesisTime + (currentL1Slot + 1n) * BigInt(constants.ethereumSlotDuration); } +/** Returns the timestamp of the last L1 slot within a given L2 slot. */ +export function getLastL1SlotTimestampForL2Slot( + slot: SlotNumber, + constants: Pick, +): bigint { + return getTimestampForSlot(slot, constants) + BigInt(constants.slotDuration - constants.ethereumSlotDuration); +} + /** Returns the L2 slot number at the next L1 block based on the current timestamp. */ export function getSlotAtNextL1Block( currentL1Timestamp: bigint, diff --git a/yarn-project/txe/src/oracle/txe_oracle_top_level_context.ts b/yarn-project/txe/src/oracle/txe_oracle_top_level_context.ts index a03b02069353..d5b01754c8ec 100644 --- a/yarn-project/txe/src/oracle/txe_oracle_top_level_context.ts +++ b/yarn-project/txe/src/oracle/txe_oracle_top_level_context.ts @@ -12,9 +12,9 @@ import { Fr } from '@aztec/foundation/curves/bn254'; import { LogLevels, type Logger, applyStringFormatting, createLogger } from '@aztec/foundation/log'; import { TestDateProvider } from '@aztec/foundation/timer'; import type { KeyStore } from '@aztec/key-store'; -import type { AccessScopes } from '@aztec/pxe/client/lazy'; import { AddressStore, + CapsuleService, CapsuleStore, type ContractStore, type ContractSyncService, @@ -328,7 +328,7 @@ export class TXEOracleTopLevelContext implements IMiscOracle, ITxeExecutionOracl const effectiveScopes = from.isZero() ? [] : [from]; // Sync notes before executing private function to discover notes from previous transactions - const utilityExecutor = async (call: FunctionCall, execScopes: AccessScopes) => { + const utilityExecutor = async (call: FunctionCall, execScopes: AztecAddress[]) => { await this.executeUtilityCall(call, execScopes, jobId); }; @@ -382,7 +382,7 @@ export class TXEOracleTopLevelContext implements IMiscOracle, ITxeExecutionOracl senderTaggingStore: this.senderTaggingStore, recipientTaggingStore: this.recipientTaggingStore, senderAddressBookStore: this.senderAddressBookStore, - capsuleStore: this.capsuleStore, + capsuleService: new CapsuleService(this.capsuleStore, effectiveScopes), privateEventStore: this.privateEventStore, contractSyncService: this.stateMachine.contractSyncService, jobId, @@ -706,7 +706,7 @@ export class TXEOracleTopLevelContext implements IMiscOracle, ITxeExecutionOracl }, blockHeader, jobId, - 'ALL_SCOPES', + await this.keyStore.getAccounts(), ); const call = FunctionCall.from({ @@ -720,10 +720,10 @@ export class TXEOracleTopLevelContext implements IMiscOracle, ITxeExecutionOracl returnTypes: [], }); - return this.executeUtilityCall(call, 'ALL_SCOPES', jobId); + return this.executeUtilityCall(call, await this.keyStore.getAccounts(), jobId); } - private async executeUtilityCall(call: FunctionCall, scopes: AccessScopes, jobId: string): Promise { + private async executeUtilityCall(call: FunctionCall, scopes: AztecAddress[], jobId: string): Promise { const entryPointArtifact = await this.contractStore.getFunctionArtifactWithDebugMetadata(call.to, call.selector); if (entryPointArtifact.functionType !== FunctionType.UTILITY) { throw new Error(`Cannot run ${entryPointArtifact.functionType} function as utility`); @@ -748,7 +748,7 @@ export class TXEOracleTopLevelContext implements IMiscOracle, ITxeExecutionOracl aztecNode: this.stateMachine.node, recipientTaggingStore: this.recipientTaggingStore, senderAddressBookStore: this.senderAddressBookStore, - capsuleStore: this.capsuleStore, + capsuleService: new CapsuleService(this.capsuleStore, scopes), privateEventStore: this.privateEventStore, messageContextService: this.stateMachine.messageContextService, contractSyncService: this.contractSyncService, diff --git a/yarn-project/txe/src/state_machine/index.ts b/yarn-project/txe/src/state_machine/index.ts index db91399fb541..5976e9f346a6 100644 --- a/yarn-project/txe/src/state_machine/index.ts +++ b/yarn-project/txe/src/state_machine/index.ts @@ -3,7 +3,6 @@ import { TestCircuitVerifier } from '@aztec/bb-prover/test'; import { CheckpointNumber } from '@aztec/foundation/branded-types'; import { Fr } from '@aztec/foundation/curves/bn254'; import { createLogger } from '@aztec/foundation/log'; -import type { KeyStore } from '@aztec/key-store'; import { type AnchorBlockStore, type ContractStore, ContractSyncService, type NoteStore } from '@aztec/pxe/server'; import { MessageContextService } from '@aztec/pxe/simulator'; import { L2Block } from '@aztec/stdlib/block'; @@ -36,7 +35,6 @@ export class TXEStateMachine { anchorBlockStore: AnchorBlockStore, contractStore: ContractStore, noteStore: NoteStore, - keyStore: KeyStore, ) { const synchronizer = await TXESynchronizer.create(); const aztecNodeConfig = {} as AztecNodeConfig; @@ -69,7 +67,6 @@ export class TXEStateMachine { node, contractStore, noteStore, - () => keyStore.getAccounts(), createLogger('txe:contract_sync'), ); diff --git a/yarn-project/txe/src/txe_session.ts b/yarn-project/txe/src/txe_session.ts index 71958ef8740b..ffb8574597a4 100644 --- a/yarn-project/txe/src/txe_session.ts +++ b/yarn-project/txe/src/txe_session.ts @@ -3,10 +3,10 @@ import { Fr } from '@aztec/foundation/curves/bn254'; import { type Logger, createLogger } from '@aztec/foundation/log'; import { KeyStore } from '@aztec/key-store'; import { openTmpStore } from '@aztec/kv-store/lmdb-v2'; -import type { AccessScopes } from '@aztec/pxe/client/lazy'; import { AddressStore, AnchorBlockStore, + CapsuleService, CapsuleStore, ContractStore, ContractSyncService, @@ -179,7 +179,7 @@ export class TXESession implements TXESessionStateHandler { const archiver = new TXEArchiver(store); const anchorBlockStore = new AnchorBlockStore(store); - const stateMachine = await TXEStateMachine.create(archiver, anchorBlockStore, contractStore, noteStore, keyStore); + const stateMachine = await TXEStateMachine.create(archiver, anchorBlockStore, contractStore, noteStore); const nextBlockTimestamp = BigInt(Math.floor(new Date().getTime() / 1000)); const version = new Fr(await stateMachine.node.getVersion()); @@ -188,13 +188,7 @@ export class TXESession implements TXESessionStateHandler { const initialJobId = jobCoordinator.beginJob(); const logger = createLogger('txe:session'); - const contractSyncService = new ContractSyncService( - stateMachine.node, - contractStore, - noteStore, - () => keyStore.getAccounts(), - logger, - ); + const contractSyncService = new ContractSyncService(stateMachine.node, contractStore, noteStore, logger); const topLevelOracleHandler = new TXEOracleTopLevelContext( stateMachine, @@ -342,7 +336,7 @@ export class TXESession implements TXESessionStateHandler { await new NoteService(this.noteStore, this.stateMachine.node, anchorBlock!, this.currentJobId).syncNoteNullifiers( contractAddress, - 'ALL_SCOPES', + await this.keyStore.getAccounts(), ); const latestBlock = await this.stateMachine.node.getBlockHeader('latest'); @@ -378,11 +372,11 @@ export class TXESession implements TXESessionStateHandler { senderTaggingStore: this.senderTaggingStore, recipientTaggingStore: this.recipientTaggingStore, senderAddressBookStore: this.senderAddressBookStore, - capsuleStore: this.capsuleStore, + capsuleService: new CapsuleService(this.capsuleStore, await this.keyStore.getAccounts()), privateEventStore: this.privateEventStore, contractSyncService: this.stateMachine.contractSyncService, jobId: this.currentJobId, - scopes: 'ALL_SCOPES', + scopes: await this.keyStore.getAccounts(), messageContextService: this.stateMachine.messageContextService, }); @@ -436,7 +430,7 @@ export class TXESession implements TXESessionStateHandler { this.stateMachine.node, anchorBlockHeader, this.currentJobId, - ).syncNoteNullifiers(contractAddress, 'ALL_SCOPES'); + ).syncNoteNullifiers(contractAddress, await this.keyStore.getAccounts()); this.oracleHandler = new UtilityExecutionOracle({ contractAddress, @@ -450,12 +444,12 @@ export class TXESession implements TXESessionStateHandler { aztecNode: this.stateMachine.node, recipientTaggingStore: this.recipientTaggingStore, senderAddressBookStore: this.senderAddressBookStore, - capsuleStore: this.capsuleStore, + capsuleService: new CapsuleService(this.capsuleStore, await this.keyStore.getAccounts()), privateEventStore: this.privateEventStore, messageContextService: this.stateMachine.messageContextService, contractSyncService: this.contractSyncService, jobId: this.currentJobId, - scopes: 'ALL_SCOPES', + scopes: await this.keyStore.getAccounts(), }); this.state = { name: 'UTILITY' }; @@ -524,7 +518,7 @@ export class TXESession implements TXESessionStateHandler { } private utilityExecutorForContractSync(anchorBlock: any) { - return async (call: FunctionCall, scopes: AccessScopes) => { + return async (call: FunctionCall, scopes: AztecAddress[]) => { const entryPointArtifact = await this.contractStore.getFunctionArtifactWithDebugMetadata(call.to, call.selector); if (entryPointArtifact.functionType !== FunctionType.UTILITY) { throw new Error(`Cannot run ${entryPointArtifact.functionType} function as utility`); @@ -543,7 +537,7 @@ export class TXESession implements TXESessionStateHandler { aztecNode: this.stateMachine.node, recipientTaggingStore: this.recipientTaggingStore, senderAddressBookStore: this.senderAddressBookStore, - capsuleStore: this.capsuleStore, + capsuleService: new CapsuleService(this.capsuleStore, scopes), privateEventStore: this.privateEventStore, messageContextService: this.stateMachine.messageContextService, contractSyncService: this.contractSyncService, diff --git a/yarn-project/wallet-sdk/src/base-wallet/base_wallet.ts b/yarn-project/wallet-sdk/src/base-wallet/base_wallet.ts index dc21fe15ac84..9b11ae2e88a4 100644 --- a/yarn-project/wallet-sdk/src/base-wallet/base_wallet.ts +++ b/yarn-project/wallet-sdk/src/base-wallet/base_wallet.ts @@ -36,7 +36,7 @@ import type { ChainInfo } from '@aztec/entrypoints/interfaces'; import { Fr } from '@aztec/foundation/curves/bn254'; import { createLogger } from '@aztec/foundation/log'; import type { FieldsOf } from '@aztec/foundation/types'; -import { type AccessScopes, displayDebugLogs } from '@aztec/pxe/client/lazy'; +import { displayDebugLogs } from '@aztec/pxe/client/lazy'; import type { PXE, PackedPrivateEvent } from '@aztec/pxe/server'; import { type ContractArtifact, @@ -94,7 +94,7 @@ export type SimulateViaEntrypointOptions = Pick< /** Fee options for the entrypoint */ feeOptions: FeeOptions; /** Scopes to use for the simulation */ - scopes: AccessScopes; + scopes: AztecAddress[]; }; /** * A base class for Wallet implementations