diff --git a/docs/docs-developers/docs/aztec-js/how_to_read_data.md b/docs/docs-developers/docs/aztec-js/how_to_read_data.md index 2fd4e438921d..2cc3b2ecca88 100644 --- a/docs/docs-developers/docs/aztec-js/how_to_read_data.md +++ b/docs/docs-developers/docs/aztec-js/how_to_read_data.md @@ -49,6 +49,10 @@ When simulating private functions, the caller must have access to any private st If the caller doesn't have access to another address's notes, the simulation will fail with an error. +:::tip +If `.simulate()` is prompting the user to sign every call, or failing with `min_revertible_side_effect_counter must not be 0` when you pass `from: AztecAddress.ZERO`, see [Simulate without signing prompts](./how_to_simulate_without_signing.md). +::: + :::warning Simulation runs locally without generating proofs. No correctness guarantees are provided on the result. See [Call Types](../foundational-topics/call_types.md#simulate) for more details. ::: diff --git a/docs/docs-developers/docs/aztec-js/how_to_simulate_without_signing.md b/docs/docs-developers/docs/aztec-js/how_to_simulate_without_signing.md new file mode 100644 index 000000000000..72daff7d9c0f --- /dev/null +++ b/docs/docs-developers/docs/aztec-js/how_to_simulate_without_signing.md @@ -0,0 +1,112 @@ +--- +title: Simulate without signing prompts +tags: [simulation, authwit, wallet] +sidebar_position: 6 +description: How to call .simulate() on a view function or estimate gas without prompting the user to sign authentication witnesses. +--- + +You want to call `.simulate()` from an app and not have the user's wallet pop up a signing prompt. This page covers the symptoms that lead to that prompt, why the obvious workarounds do not work, and the right fix. + +For the conceptual model of what kernelless simulation is, see [Kernelless simulations](../foundational-topics/pxe/kernelless_simulations.md). + +## Symptoms + +You are probably here because of one of these: + +- The wallet prompts the user for a signature on every `.simulate()` call, including reads of view-style functions. +- `.simulate()` fails with one of: + - `Account "0x0000…0000" does not exist on this wallet.` (from `EmbeddedWallet`) + - `Account not found in wallet for address: 0x0000…0000` (from other wallets built on `BaseWallet`) + - `Circuit execution failed: min_revertible_side_effect_counter must not be 0 for tail_to_public` (from a custom wallet that does not intercept the zero address, where the call reaches the kernel) + +All three are the same root cause: passing `from: AztecAddress.ZERO`. + +- A custom fee payment method breaks during simulation because `from` is `AztecAddress.ZERO`. +- Simulations take long enough that you want to skip the kernel circuits entirely. + +## The wrong fix + +Do not pass `from: AztecAddress.ZERO`. That value was the old way to express "no account context," and it is no longer a supported input for `.simulate()`. The replacement is `NO_FROM` (from `@aztec/aztec.js/account`), which tells the wallet to execute the payload through the default entrypoint with no account contract mediation. `NO_FROM` is still only appropriate for calls that genuinely have no sender; use a real account address for everything else. + +## The right fix + +A simulation uses a **stub account contract override**: the wallet provides a `SimulationOverrides` payload whose `contracts` map swaps the caller's account contract for a stub whose `is_valid` always returns true, and the PXE applies that override during the kernelless simulation. With the stub in place, authwit validity checks pass without a signature, and the wallet collects any `CallAuthorizationRequest` offchain effects emitted during the run to turn them into real authentication witnesses for the eventual `.send()`. + +`EmbeddedWallet` installs this override automatically on every `.simulate()` call, so most apps do not need to construct overrides themselves. The two ways to wire this up below correspond to "use the default" and "implement it in your own wallet." + +### Calling .simulate() from an app + +For a normal `.simulate()` you do not need to pass overrides yourself. The default simulation path is already kernelless, and wallets such as `EmbeddedWallet` install the stub-account override internally for you. Three things to remember: + +- Pass a real account address as `from`, or `NO_FROM` if the call genuinely has no sender. Do not use `AztecAddress.ZERO`. +- For simple reads, you can omit the `fee` block. +- For a transaction whose real fee path uses a fee payment contract (FPC) with private side effects (the FPC emits notes during fee payment), include that FPC in the simulation's fee options so gas estimation accounts for the FPC's side effects. Kernelless still applies; the gas number stays accurate. +- If you have a stale call site that uses `from: AztecAddress.ZERO` plus a no-op fee payment method as a workaround, replace it with a real `from` (or `NO_FROM`) and drop the no-op fee contract. + +#include_code simulate-view-without-signing /docs/examples/ts/aztecjs_kernelless_simulation/index.ts typescript + +If you genuinely need to construct your own `SimulationOverrides` (for example, to combine a contract-instance swap with a `fastForwardContractUpdate` for upgrade testing), you can pass them through `.simulate()`: + +```typescript +import { SimulationOverrides } from "@aztec/aztec.js/wallet"; + +const { result } = await contract.methods + .transfer_in_private(sender, recipient, amount, nonce) + .simulate({ + from: sender, + overrides: new SimulationOverrides({ + /* contracts and/or publicStorage */ + }), + }); +``` + +The override map itself has to be built by code that knows the contract class id and live contract instance. That is normally the wallet, not the app. Note that `overrides` does not apply to [utility functions](../foundational-topics/pxe/kernelless_simulations.md#where-kernelless-does-not-apply), those are simulated through `wallet.executeUtility`, which rejects `SimulationOverrides`. If your wallet does not handle the override path for you and you are tempted to reimplement it in app code, read the next section instead. + +### Implementing the override in a custom wallet + +`EmbeddedWallet` (`yarn-project/wallets/src/embedded/embedded_wallet.ts`, in `@aztec/wallets`) is the canonical implementation of the override pattern and the reference any custom wallet should follow. The three pieces it wires up are: + +1. **Register the stub contract class with the PXE at wallet startup.** Inside `initStubClasses`, `EmbeddedWallet` calls `pxe.registerContractClass` for each supported account type's stub artifact and caches the resulting class id by account type. +2. **Build an override map for every account in scope.** Inside `buildAccountOverrides`, it fetches the live contract instance for each scoped address and returns a `ContractOverrides` map that copies the instance with `currentContractClassId` rewritten to the stub class id. The map covers every account in scope, not only `from`. +3. **Use the stub entrypoint and pass the override to `pxe.simulateTx`.** Inside the overridden `simulateViaEntrypoint`, it constructs the `TxExecutionRequest` through the stub account's `DefaultAccountEntrypoint` (so the request is signed by the stub's empty-signature provider) and calls `pxe.simulateTx` with the resulting `SimulationOverrides`. + +The key constraints on this path: + +- `skipKernels` must be `true` to use `contracts` overrides. The PXE rejects the combination otherwise. `pxe.simulateTx` already defaults `skipKernels` to `true`. +- The stub contract class must be registered with the PXE before you reference it in an override. +- The override map must cover every scoped account, not only `from`. + +## Collecting authwit requests + +A simulation with the stub override active will reach `#[authorize_once]` call sites in app and token contracts without prompting for signatures. Each such site emits a `CallAuthorizationRequest` as an offchain effect, which the wallet can collect and turn into a real authentication witness for the eventual `.send()`. + +The example below uses the canonical contract-mediated pattern: Alice calls `Crowdfunding.donate(amount)`, which internally calls `transfer_in_private(alice, crowdfunding, amount, 0)` with `msg_sender = crowdfunding`. The token's `#[authorize_once]` macro requires an authwit from Alice authorizing the crowdfunding contract. Because Alice is the transaction sender, her PXE already has her notes and nullifier key, so the simulation can run end-to-end against her own state without any cross-wallet state sharing. + +Run the simulation and filter the offchain effects by the `CallAuthorizationRequest` selector: + +#include_code simulate-and-collect-effects /docs/examples/ts/aztecjs_kernelless_simulation/index.ts typescript + +Decode each effect into a `CallAuthorizationRequest`. The `innerHash` field is the piece the authorizing account needs to sign: + +#include_code decode-call-authorization /docs/examples/ts/aztecjs_kernelless_simulation/index.ts typescript + +Build a real authentication witness from each inner hash and send the transaction with the collected witnesses attached: + +#include_code build-authwits-and-send /docs/examples/ts/aztecjs_kernelless_simulation/index.ts typescript + +The app does not need to know which calls require authwits ahead of time. The simulation discovers them; the wallet signs them at send time. + +`EmbeddedWallet.sendTx` runs this same simulate-then-collect flow internally before delegating to `BaseWallet.sendTx`, so an app that uses `EmbeddedWallet` does not need to call `.simulate()` manually and pass `authWitnesses` to `.send()`. The explicit pattern above is the one a wallet that does not auto-collect must implement, either inside its `sendTx` (as `EmbeddedWallet` does) or inside the app call site. + +## Things to watch out for + +- **`AztecAddress.ZERO` is not "no sender".** Use `NO_FROM` (from `@aztec/aztec.js/account`) for calls that genuinely have no account context, and a real account address otherwise. +- **A private FPC needs to be included in fee options for accurate gas.** Kernelless simulation matches full simulation on gas, but only if the simulation sees the same fee path as the real transaction. If the user will pay through a fee payment contract (FPC) that emits private notes, pass that FPC in the simulation's fee options so its side effects are accounted for. Kernelless plus a private FPC is the supported path; you do not need a full simulation to get accurate gas. +- **`profile()` is not kernelless.** If you call `.profile()` to count circuit gates, the kernels run regardless. Use `.simulate()` if you only need return values, offchain effects, or gas estimates. +- **Utility functions reject overrides.** `FunctionType.UTILITY` calls go through `wallet.executeUtility`, and `ContractFunctionInteraction.simulate` throws `overrides are not supported for utility function simulation` if you pass non-empty `overrides.publicStorage` or `overrides.contracts`. Utility functions do not need an override anyway, since they do not run through an account contract. + +## Related + +- [Kernelless simulations](../foundational-topics/pxe/kernelless_simulations.md) for the conceptual model. +- [Reading contract data](./how_to_read_data.md) for the basic `.simulate()` API. +- [Authentication witnesses](../foundational-topics/advanced/authwit.md) for what `CallAuthorizationRequest` represents. diff --git a/docs/docs-developers/docs/foundational-topics/pxe/index.md b/docs/docs-developers/docs/foundational-topics/pxe/index.md index 93bbbb4e3efb..3323efa88a2f 100644 --- a/docs/docs-developers/docs/foundational-topics/pxe/index.md +++ b/docs/docs-developers/docs/foundational-topics/pxe/index.md @@ -40,7 +40,7 @@ flowchart TB ``` :::note[Privacy consideration] -When the PXE queries the node for world state (e.g., to check if a nullifier exists), the node learns which data the user is interested in. This is a known tradeoff—users can mitigate this by running their own node. +When the PXE queries the node for world state (e.g., to check if a nullifier exists), the node learns which data the user is interested in. This is a known tradeoff. Users can mitigate this by running their own node. ::: ## Components @@ -51,7 +51,7 @@ An application prompts the user's PXE to execute a transaction (e.g. execute fun The contract function simulator handles execution of smart contract functions by simulating transactions. It generates the required data and inputs for these functions, including partial witnesses and public inputs. -Until simulated simulations are implemented ([#9133](https://github.com/AztecProtocol/aztec-packages/issues/9133)), authentication witnesses are required for simulation before proving. +By default, the simulator runs in a [kernelless mode](./kernelless_simulations.md): it executes the private bytecode and computes the values the private kernels would have produced in TypeScript, instead of running the kernel circuits themselves. This is faster than a full simulation and lets the wallet capture authentication witness requests as offchain effects without prompting the user to sign during simulation. ### Proof Generation @@ -93,7 +93,7 @@ The set of oracles that the PXE exposes to private and utility functions is vers The version uses two components, `major.minor`, with the following compatibility rules: -- **`major`** must match exactly. A major bump is a breaking change — oracles were removed or their signatures changed — and a PXE on a different major cannot safely run the contract. +- **`major`** must match exactly. A major bump is a breaking change: oracles were removed or their signatures changed, and a PXE on a different major cannot safely run the contract. - **`minor`** indicates additive changes (new oracles). The PXE uses a best-effort approach here: a contract compiled against a higher `minor` than the PXE supports is still allowed to run, and an error is only thrown if the contract actually invokes an oracle the PXE does not know about. In practice, a contract built with a newer Aztec.nr may not use any of the newly added oracles at all, in which case it runs fine on an older PXE. The canonical version constants live in the PXE (`ORACLE_VERSION_MAJOR` / `ORACLE_VERSION_MINOR` in `yarn-project/pxe/src/oracle_version.ts`) and in Aztec.nr (`noir-projects/aztec-nr/aztec/src/oracle/version.nr`). The two are kept in lockstep as part of each release. diff --git a/docs/docs-developers/docs/foundational-topics/pxe/kernelless_simulations.md b/docs/docs-developers/docs/foundational-topics/pxe/kernelless_simulations.md new file mode 100644 index 000000000000..380ef3366348 --- /dev/null +++ b/docs/docs-developers/docs/foundational-topics/pxe/kernelless_simulations.md @@ -0,0 +1,98 @@ +--- +title: Kernelless simulations +sidebar_position: 2 +tags: [pxe, simulation, gas estimation] +description: How the PXE simulates transactions without running the private kernel circuits, why it is the default for .simulate(), and what it skips. +--- + +This page explains what kernelless simulation is in the Private eXecution Environment (PXE), how it differs from a full simulation, and where it does and does not apply. If you are looking for the recipe to make `.simulate()` succeed without signing prompts, see [Simulate without signing prompts](../../aztec-js/how_to_simulate_without_signing.md). + +## Overview + +A "full" simulation in the PXE runs the user's private function bytecode, then runs every private kernel circuit (init, inner, reset, tail) over the resulting execution trace. The kernels enforce protocol rules such as side-effect counter sequencing. + +A **kernelless simulation** runs the same private bytecode, but skips the kernel circuits. Instead, the PXE computes the values the kernel would have produced in TypeScript via `generateSimulatedProvingResult`. The output of a kernelless simulation is the same shape as a full simulation, so callers can read return values, offchain effects, and gas estimates from it without caring which path produced them. + +Kernelless simulation is the **default** for `PXE.simulateTx`. The `skipKernels` option in `SimulateTxOpts` defaults to `true`, and `BaseWallet` inherits that default. In normal use, every call to `.simulate()` on a contract method already runs without the kernels. + +The main consequence is speed. Skipping the kernels removes the most expensive part of simulation, so a kernelless run is faster than a full run on typical transactions. + +## What the PXE still does + +A kernelless simulation is not a partial execution. The PXE still: + +- runs the real ACIR bytecode for every private function in the call chain +- executes oracles, decrypts notes, builds nullifiers, and captures offchain effects +- simulates public calls against an ephemeral fork of public state +- runs `node.isValidTx` against the resulting transaction, unless `skipTxValidation` is set +- at the raw `PXE.simulateTx` level, enforces fee payer presence unless `skipFeeEnforcement` is set. Contract `.simulate()` calls through `BaseWallet` already pass `skipFeeEnforcement: true` for estimation, so you do not need to provide a fee block for normal read simulations + +What it skips with `skipKernels: true`: + +- the private kernel init, inner, reset, and tail circuits +- the proof generation associated with those kernels + +The kernels themselves do not check authentication witnesses. Authwit validity is checked by user-contract code (the `is_valid` call that the `#[authorize_once]` macro injects into the called function). What lets a kernelless simulation skip the signing prompt is the **stub-account override**, not the absence of the kernels: replacing the caller's account contract with a stub whose `is_valid` always returns true lets that user-contract check pass without a signature. + +## Simulation overrides + +A kernelless simulation accepts an optional `SimulationOverrides` payload that lets you replace pieces of the state the PXE would otherwise read from chain. The shape (in `yarn-project/stdlib/src/tx/simulated_tx.ts`) is: + +```typescript +type ContractOverrides = Record< + string, + { instance: ContractInstanceWithAddress } +>; + +class SimulationOverrides { + publicStorage?: PublicStorageOverride[]; + contracts?: ContractOverrides; +} +``` + +Two parts: + +- `publicStorage` rewrites slots in the ephemeral public-data fork before simulation. This is compatible with kernel execution; you can use it with or without `skipKernels`. +- `contracts` swaps a contract instance in the simulator's contract DB by replacing its `currentContractClassId`. There is no `artifact` field; the new class id must already be registered with the PXE via `pxe.registerContractClass(...)` so the simulator can resolve the bytecode. This requires `skipKernels: true`: PXE explicitly rejects contract overrides combined with kernel execution, because the kernels would fail validations against the swapped class. + +The `contracts` override path is what makes "simulate without signing" possible. Replacing the caller's account contract with a stub whose `is_valid` always returns true lets the simulation reach `#[authorize_once]` call sites without prompting the user to sign anything. + +For the cheat that simulates a contract as if it had already been upgraded to a new class, see [`fastForwardContractUpdate`](../../aztec-js/how_to_test.md#fast-forwarding-a-contract-update), which returns a `SimulationOverrides` covering both the registry storage rewrite and the upgraded instance entry. + +## Stub account contracts + +The stub-account pattern is the standard way to drive a kernelless simulation without authwit prompts. + +The Noir sources live at `noir-projects/noir-contracts/contracts/account/simulated_schnorr_account_contract/` and `simulated_ecdsa_account_contract/`. Both implement `is_valid` to always return `IS_VALID_SELECTOR`, so authwit validity checks pass without a real signature. Their constructors deliberately emit the same shape of side effects as the real account contracts (one nullifier for the contract init, one nullifier for the `SinglePrivateImmutable` signing-key state, one note hash for the key note, and a private log) so that gas estimation against the stub produces the same numbers as the real account. + +## Authwit requests come from the app, not the stub + +The `CallAuthorizationRequest` offchain effects you see during a kernelless simulation are emitted by the **app or token contract's `#[authorize_once]` macro** during private execution. The stub account's only job is to let the validity check pass so the simulation can reach those call sites in the first place. The wallet then collects the requests via `collectOffchainEffects(privateExecutionResult)`, filters them by `CallAuthorizationRequest.getSelector()`, and decodes each one to build a real `AuthWitness`. + +## Where kernelless does not apply + +The default applies to `simulateTx`, but not to every entry point that looks like simulation: + +- **`profileTx`** is not kernelless. `PXE.profileTx` always runs the private kernels after private execution; `skipProofGeneration` controls only whether a proof is produced, not whether kernel logic runs. If you call `.profile()` to measure circuit gates, expect the full kernel cost. +- **Public-only fast path**. When `BaseWallet.simulateTx` detects a leading run of public static calls, it sends them straight to `node.simulatePublicCalls` through `simulateViaNode`, bypassing the PXE private path entirely. There is no kernel to skip. `SimulationOverrides` still applies on that path. +- **Utility functions**. `FunctionType.UTILITY` calls go through `wallet.executeUtility`, not `pxe.simulateTx`. `ContractFunctionInteraction.simulate` rejects `overrides.publicStorage` and `overrides.contracts` for utility functions. + +## Gas estimation parity + +If the real transaction will pay through a fee payment contract (FPC) with private side effects (the FPC emits notes during fee payment), include that FPC in the simulation's fee options. The FPC's side effects feed into gas estimation, and you can run kernelless with the FPC attached to get both the speed benefit and accurate gas numbers. The default of "omit the fee block" only produces accurate gas for transactions whose fee path has no private side effects. + +## Multi-account scopes + +A simulation can run with multiple scoped accounts via `additionalScopes`. If you build a stub-account override for the sender only, the simulation will still prompt for authwits from any other in-scope account it touches. The override map must cover every account in scope, not just `from`. + +The canonical implementation is `EmbeddedWallet.buildAccountOverrides` in `yarn-project/wallets/src/embedded/embedded_wallet.ts`: for each scoped address, fetch the live contract instance from the PXE, copy it, and rewrite `currentContractClassId` to point at the stub class id registered at wallet startup. When implementing overrides in your own wallet, follow this pattern and make sure the scope list you build against matches the one the simulation will run with. + +## When you might still want a full simulation + +Kernelless is the right default. Reach for `skipKernels: false` only when you are validating kernel-level behavior itself. For everything else, including accurate gas estimation through a fee payment contract with private side effects, run kernelless with the appropriate fee options. + +## Related + +- [Simulate without signing prompts](../../aztec-js/how_to_simulate_without_signing.md) for the recipe-oriented version of this page. +- [Reading contract data](../../aztec-js/how_to_read_data.md) for the basic `.simulate()` API. +- [Wallets](../wallets.md) for the wallet's role in capturing private authorizations during simulation. diff --git a/docs/docs-developers/docs/foundational-topics/wallets.md b/docs/docs-developers/docs/foundational-topics/wallets.md index 4c0bc46c4457..895570670b5c 100644 --- a/docs/docs-developers/docs/foundational-topics/wallets.md +++ b/docs/docs-developers/docs/foundational-topics/wallets.md @@ -35,7 +35,7 @@ Private functions use a UTXO model, so their execution trace is determined entir Public functions use an account model (like Ethereum), so their execution trace depends on chain state at inclusion time, which may differ from simulation. ::: -Before sending, the wallet may run a **simulation** — a lightweight execution using a stub account contract that avoids expensive kernel circuit execution. This simulation estimates gas limits for the transaction and captures any required private authorization data (see [Authorizing actions](#authorizing-actions) below). The `EmbeddedWallet` runs this step automatically on every send. +Before sending, the wallet may run a **simulation**, a lightweight execution using a stub account contract that avoids expensive kernel circuit execution. This simulation estimates gas limits for the transaction and captures any required private authorization data (see [Authorizing actions](#authorizing-actions) below). The `EmbeddedWallet` runs this step automatically on every send. For details on what the PXE skips in this mode and how to wire it up in your own wallet, see [Kernelless simulations](./pxe/kernelless_simulations.md). Finally, the wallet **sends** the resulting _transaction_ object, which includes the proof of execution, to an Aztec Node. The transaction is then broadcasted through the peer-to-peer network, to be eventually picked up by a sequencer and included in a block. diff --git a/docs/docs-words.txt b/docs/docs-words.txt index 9bd7144a966b..90f48ff9bcc7 100644 --- a/docs/docs-words.txt +++ b/docs/docs-words.txt @@ -182,6 +182,7 @@ jumpi JUMPI kalypso Keccakf +kernelless keypair knwon lbwop diff --git a/docs/examples/ts/aztecjs_kernelless_simulation/config.yaml b/docs/examples/ts/aztecjs_kernelless_simulation/config.yaml new file mode 100644 index 000000000000..103036595a84 --- /dev/null +++ b/docs/examples/ts/aztecjs_kernelless_simulation/config.yaml @@ -0,0 +1,16 @@ +# Configuration for aztecjs_kernelless_simulation example +# Demonstrates kernelless simulation with stub account overrides: +# - Calling .simulate() on a view function without signing prompts +# - Harvesting CallAuthorizationRequest offchain effects to build authwits + +# No custom contracts needed - using pre-built contracts from @aztec/noir-contracts.js +contracts: [] + +# Dependencies: +# - @aztec/* packages are auto-linked from yarn-project/ +# - Use npm:package-name for external npm packages +dependencies: + - "@aztec/aztec.js" + - "@aztec/accounts" + - "@aztec/wallets" + - "@aztec/noir-contracts.js" diff --git a/docs/examples/ts/aztecjs_kernelless_simulation/index.ts b/docs/examples/ts/aztecjs_kernelless_simulation/index.ts new file mode 100644 index 000000000000..c23a6222ebb8 --- /dev/null +++ b/docs/examples/ts/aztecjs_kernelless_simulation/index.ts @@ -0,0 +1,167 @@ +import { createAztecNodeClient, waitForNode } from "@aztec/aztec.js/node"; +import { EmbeddedWallet } from "@aztec/wallets/embedded"; +import { getInitialTestAccountsData } from "@aztec/accounts/testing"; +import { TokenContract } from "@aztec/noir-contracts.js/Token"; +import { CrowdfundingContract } from "@aztec/noir-contracts.js/Crowdfunding"; +import { Fr } from "@aztec/aztec.js/fields"; +import { deriveKeys } from "@aztec/aztec.js/keys"; +import { CallAuthorizationRequest } from "@aztec/aztec.js/authorization"; + +// Setup: connect to network, create alice and bob, deploy a token, mint to alice. +// EmbeddedWallet runs every .simulate() call in kernelless mode with a stub-account +// override applied automatically. The examples below rely on that default. +const node = createAztecNodeClient( + process.env.AZTEC_NODE_URL ?? "http://localhost:8080", +); +await waitForNode(node); +const wallet = await EmbeddedWallet.create(node, { ephemeral: true }); + +const testAccounts = await getInitialTestAccountsData(); +const [aliceAddress, bobAddress] = await Promise.all( + testAccounts.slice(0, 2).map(async (account) => { + return ( + await wallet.createSchnorrAccount( + account.secret, + account.salt, + account.signingKey, + ) + ).address; + }), +); + +const { contract: tokenContract } = await TokenContract.deploy( + wallet, + aliceAddress, + "TestToken", + "TST", + 18, +).send({ from: aliceAddress }); + +await tokenContract.methods + .mint_to_private(aliceAddress, 1000n) + .send({ from: aliceAddress }); + +// docs:start:simulate-view-without-signing +// Reading a private view function would normally route through the account +// contract's entrypoint, whose is_valid check would prompt the user to sign. +// With EmbeddedWallet, .simulate() runs kernelless with a stub-account +// override applied to alice's account, so no signing prompt is triggered. +const { result: decimals } = await tokenContract.methods + .private_get_decimals() + .simulate({ from: aliceAddress }); + +console.log("Token decimals (read via private view):", decimals); +// docs:end:simulate-view-without-signing + +// Deploy a Crowdfunding contract with Bob as the operator and the previously +// deployed token as the donation token. The contract receives donation notes +// for donors, so it needs its own keys (a contract-account-style deployment). +const crowdfundingSecretKey = Fr.random(); +const crowdfundingPublicKeys = (await deriveKeys(crowdfundingSecretKey)) + .publicKeys; +const deadline = Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60; + +const crowdfundingDeployment = CrowdfundingContract.deploy( + wallet, + tokenContract.address, + bobAddress, + deadline, + { publicKeys: crowdfundingPublicKeys, deployer: aliceAddress }, +); +const crowdfundingInstance = await crowdfundingDeployment.getInstance(); +await wallet.registerContract( + crowdfundingInstance, + CrowdfundingContract.artifact, + crowdfundingSecretKey, +); +const { contract: crowdfundingContract } = await crowdfundingDeployment.send({ + from: aliceAddress, + additionalScopes: [crowdfundingInstance.address], +}); + +// docs:start:simulate-and-collect-effects +// Alice calls Crowdfunding.donate(amount). Internally the contract calls +// transfer_in_private(alice, crowdfunding, amount, 0) with msg_sender = +// crowdfunding, so the token's #[authorize_once] macro requires an authwit +// from Alice authorizing the crowdfunding contract. Alice is the sender, so +// her PXE has her notes and nullifier key — no cross-wallet state sharing +// is required. +// +// With kernelless + stub override (default in EmbeddedWallet), the simulation +// runs without prompting Alice to sign; the macro emits a +// CallAuthorizationRequest as an offchain effect that the wallet can turn +// into a real authwit before .send(). +const donationAmount = 100n; +const donateAction = crowdfundingContract.methods.donate(donationAmount); + +const { offchainEffects } = await donateAction.simulate({ + from: aliceAddress, + includeMetadata: true, +}); + +// Filter offchain effects for authwit requests by selector. +const authwitSelector = await CallAuthorizationRequest.getSelector(); +const authwitEffects = offchainEffects.filter( + (effect) => + effect.data.length > 0 && effect.data[0].equals(authwitSelector.toField()), +); + +if (authwitEffects.length !== 1) { + throw new Error( + `Expected exactly one CallAuthorizationRequest, got ${authwitEffects.length}`, + ); +} +// docs:end:simulate-and-collect-effects + +// docs:start:decode-call-authorization +// Decode each effect into a CallAuthorizationRequest. The inner hash is the +// piece the authorizing account (Alice) needs to sign. +const authorizationRequests = await Promise.all( + authwitEffects.map((effect) => + CallAuthorizationRequest.fromFields(effect.data), + ), +); + +for (const request of authorizationRequests) { + console.log("Authwit needed:", { + onBehalfOf: request.onBehalfOf.toString(), + msgSender: request.msgSender.toString(), + functionSelector: request.functionSelector.toString(), + }); +} + +if (!authorizationRequests[0].onBehalfOf.equals(aliceAddress)) { + throw new Error( + `Expected onBehalfOf to be alice (${aliceAddress.toString()}), got ${authorizationRequests[0].onBehalfOf.toString()}`, + ); +} +// docs:end:decode-call-authorization + +// docs:start:build-authwits-and-send +// Alice creates a real authentication witness from each inner hash. The +// `consumer` is the contract that consumes the authwit — here, the token, +// because that's where the inner transfer_in_private (and its #[authorize_once] +// site) runs. +const authWitnesses = await Promise.all( + authorizationRequests.map((request) => + wallet.createAuthWit(request.onBehalfOf, { + consumer: tokenContract.address, + innerHash: request.innerHash, + }), + ), +); + +// Alice now sends the real donate transaction with the collected authwits +// attached. +// +// Note: EmbeddedWallet.sendTx already runs this pre-simulation + authwit +// collection internally, so passing `authWitnesses` here is redundant for +// EmbeddedWallet. We pass them explicitly anyway because this is the pattern +// a wallet that does not auto-collect needs to follow. +await donateAction.send({ + from: aliceAddress, + authWitnesses, +}); +// docs:end:build-authwits-and-send + +console.log("Kernelless simulation example completed successfully"); diff --git a/docs/examples/ts/aztecjs_kernelless_simulation/yarn.lock b/docs/examples/ts/aztecjs_kernelless_simulation/yarn.lock new file mode 100644 index 000000000000..e69de29bb2d1