diff --git a/.claude/skills/local-node-validation/SKILL.md b/.claude/skills/local-node-validation/SKILL.md index fda6cfa..fb17c87 100644 --- a/.claude/skills/local-node-validation/SKILL.md +++ b/.claude/skills/local-node-validation/SKILL.md @@ -12,51 +12,112 @@ Validates that contracts working in MockChain also work against a real Miden nod MockChain simplifies execution in ways that hide real-world failures: 1. **No automatic block production** -- MockChain requires explicit `prove_next_block()`. A live node produces blocks on its own schedule. -2. **No network transport** -- MockChain does not simulate the network transaction builder that handles network notes. +2. **No network transport** -- MockChain does not simulate the network transaction builder (ntx-builder) that handles network notes. 3. **No RPC latency or timeouts** -- MockChain executes locally and instantly. Live nodes have gRPC round-trips with configurable timeouts. -4. **No version/genesis validation** -- MockChain skips the `Accept` header version check that live nodes enforce. -5. **Account update block numbers not tracked** -- MockChain returns chain tip instead of actual update block number. -6. **No mempool or batching** -- MockChain does not simulate transaction queuing, batch formation, or block inclusion delays. -7. **Genesis hash is cached in the client store** -- after the first sync, the client persists the network's genesis commitment in its SQLite store (default path `store.sqlite3` per `integration/src/helpers.rs`; `local-store.sqlite3` for the local-validation variant introduced in Step 2) and ships it on every Accept header. Switching networks (local node to testnet, or vice versa) without wiping the active store fails with `accept header validation failed`. -8. **`NoteTag(0)` notes are not delivered by default subscriptions** -- live nodes filter `SyncNotes` responses by the tag list each client has subscribed to. `NoteTag::new(0)` has all-zero routing bits, so the default account-derived subscription that `add_account` registers (`NoteTagRecord::with_account_source(NoteTag::with_account_target(id), id)`) does not match it. Such notes still exist on-chain and remain queryable, but a client only receives them during sync if it explicitly tracks tag 0 via `Client::add_note_tag(...)`. MockChain bypasses sync filtering and surfaces these notes anyway, hiding the gap until live validation. Prefer `NoteTag::with_account_target(account_id)`, a use-case tag constructor, or an explicit `add_note_tag` subscription when notes must reach the recipient via sync. +4. **No version/genesis validation** -- MockChain skips the protocol-version check that a live node negotiates at connect. The live negotiation rides on an `accept` header of the form `application/vnd.miden; version=; genesis=`; a rejection surfaces as `RpcError::AcceptHeaderError`. +5. **Different scheduling** -- MockChain has no block-production cadence, no RPC round-trip, and no rate limiting, so timing-dependent bugs do not reproduce. ## Prerequisites -- [ ] MockChain integration tests pass: `cargo test -p integration --release` -- [ ] `miden-node` installed and version-matched with the client. Check the `miden-client` version in `integration/Cargo.toml`; the node binary must be on the same minor release. `cargo install miden-node --locked` may pin an older published crate; if so, install from source: `cargo install miden-node --locked --git https://github.com/0xMiden/miden-node --tag v`. `midenup` manages matched toolchains. -- [ ] Working integration binary exists in `integration/src/bin/` +- [ ] Your MockChain integration tests pass (e.g. `cargo test -p --release`) +- [ ] A Miden node available locally. The node is **not** a single binary -- it is composed of standalone executables (validator, sequencer, ntx-builder, transaction prover). The client's own test infra installs the full set: `miden-validator`, `miden-node`, `miden-ntx-builder`, `miden-remote-prover` (see `scripts/start-test-node.sh` in the `miden-client` repo), with `cargo install --locked`. The node version the client is built against resolves from crates.io via your `Cargo.lock`, not from a git source. +- [ ] A working network/integration validation binary exists in your project that you can use as the starting template for the localhost variant + +> The local-node launch CLI lives in the 0xMiden/node repo, not in `miden-client`. The commands below are the topology the client's `make start-node` target (`scripts/start-test-node.sh`) drives; confirm exact flags against your installed node's `--help` for the version you run. ## Step 1: Clean State and Start Local Node -**Every node session must start from clean state.** Stale store files and keystore directories cause conflicts, deserialization errors, and misleading test results. Always wipe before starting. +**Every node session must start from clean state.** Stale store files and keystore directories cause conflicts, deserialization errors, and misleading test results. Always wipe before starting. Serialized artifacts do not round-trip across protocol versions -- the MAST wire format is `[0, 0, 4]` and the package format is `[6, 0, 0]` -- so a fresh store is required after any version change. The helper script does `rm -rf "$DATA"` on every start for exactly this reason. + +The simplest path is the client's `make start-node` target, which runs the bundled `scripts/start-test-node.sh` helper: it installs the node binaries (pinned to your `Cargo.lock`), generates genesis, bootstraps each component, and starts the split topology for you: + +```bash +# From a checkout of the miden-client repo pinned to your client version: +make start-node # foreground, streams logs; Ctrl+C stops +# or +make start-node-background # returns once RPC is ready (used by CI) +``` + +This brings up the four-component topology and exposes the RPC on `127.0.0.1:57291` (the client default, `MIDEN_NODE_PORT`). + +If you run the node binaries directly instead of via `make start-node`, the shape is below. Treat it as a reference skeleton, not a copy-paste recipe: it omits details the script handles for you (it does not show generating the genesis config the validator bootstraps from, and it leaves out the shared network-tx auth header that the sequencer and ntx-builder must agree on or the sequencer rejects the ntx-builder's transactions). Verify every subcommand and flag against `--help` for your node version, or just use `make start-node`. ```bash -# 1. Wipe all state from previous runs -rm -rf local-node-data/ local-keystore/ local-store.sqlite3 - -# 2. Bootstrap fresh node -mkdir -p local-node-data -miden-node bundled bootstrap \ - --data-directory local-node-data \ - --accounts-directory . - -# 3. Start node (keep running in separate terminal) -miden-node bundled start \ - --data-directory local-node-data \ - --rpc.url http://0.0.0.0:57291 +# 0. Build the genesis block ONCE with the dedicated `genesis` subcommand. +# (The genesis.toml it consumes is produced by the client's `gen-genesis` binary: +# cargo build --release -p test-node-genesis --bin gen-genesis +# ./target/release/gen-genesis /genesis-config) +miden-validator genesis \ + --genesis-block-directory /genesis --accounts-directory /accounts \ + --config /genesis-config/genesis.toml + +# 1. Bootstrap each component from that genesis block. All three take --genesis. +# Create each component's data directory first -- they open their SQLite DB +# directly and do not mkdir it for you. +miden-validator bootstrap --data-directory /validator --genesis /genesis/genesis.dat +miden-node bootstrap --data-directory /node --genesis /genesis/genesis.dat +miden-ntx-builder bootstrap --data-directory /ntx-builder --genesis /genesis/genesis.dat + +# 2. Start the components in order: validator, then sequencer (which carries the RPC) +# and prover, then ntx-builder. The sequencer and ntx-builder must agree on the +# network-tx auth header or the sequencer rejects the ntx-builder's transactions. +# The validator requires threshold storage-key material to start at all. +miden-validator start --listen 127.0.0.1:50101 --data-directory /validator \ + --storage-key.epoch <64 hex chars> \ + --storage-key.setup-context /setup-context.wire \ + --storage-key.public-key-set /public-key-set.wire \ + --storage-key.secret-share /secret-share.wire + +miden-node sequencer --rpc.listen 127.0.0.1:57291 --data-directory /node \ + --validator.url http://127.0.0.1:50101 --ntx-builder.url http://127.0.0.1:50301 \ + --block.interval 3s --batch.interval 1s \ + --rpc.network-tx-auth-header-value "$NETWORK_TX_AUTH" \ + --rpc.rate-limit.burst-size 10000 --rpc.rate-limit.replenish-per-second 10000 + +miden-remote-prover --kind=transaction --port=50051 + +miden-ntx-builder start --listen 127.0.0.1:50301 --rpc.url http://127.0.0.1:57291 \ + --tx-prover.url http://127.0.0.1:50051 --data-directory /ntx-builder \ + --rpc.auth-header-value "$NETWORK_TX_AUTH" --max-cycles $((1 << 18)) +``` + +Three things here bite hard if you skip them: + +- **The validator will not start without threshold storage-key material.** The client vendors insecure development fixtures for exactly this at `scripts/testdata/insecure-golden-storage-key/`. Never use those outside a local test node. +- **Without the rate-limit bump, an integration run gets throttled** by the sequencer's default limiter and starts failing in ways that look like network flakiness. +- **Start ordering matters.** The script sleeps ~2s after the validator and again after the sequencer, then polls the RPC socket for up to 60 seconds. + +Tear down with `make stop-node` (`scripts/stop-test-node.sh`), which kills by pid file and falls back to `pkill` on the installed binary paths. + +### Private notes need a separate service + +`ClientBuilder::for_localhost()` configures **no note transport**. Private-note flows against a local node therefore silently do nothing until you both run the transport service (`make start-note-transport`, which installs `miden-note-transport-node` from `0xMiden/miden-note-transport`) and point the client at it: + +```rust +.note_transport(Arc::new(GrpcNoteTransportClient::new(url, timeout_ms))) ``` **This clean-start sequence is mandatory every time.** Do not attempt to reuse state from a previous session. -## Step 2: Adapt helpers.rs for Localhost +## Step 2: Add a localhost client helper + +Add a `setup_local_client()` function to whichever helper module in your integration / test harness already hosts the network `setup_client()` equivalent. Name and location are up to you -- adjust to your repo's layout. -In `integration/src/helpers.rs`, add a `setup_local_client()` alongside the existing `setup_client()`: +`.sqlite_store(..)` is **not** an inherent `ClientBuilder` method -- it comes from an extension trait in the `miden-client-sqlite-store` crate. You must bring it into scope or the call fails to compile (method not found): + +```rust +use miden_client_sqlite_store::ClientBuilderSqliteExt; // required for .sqlite_store(..) +``` ```rust pub async fn setup_local_client() -> Result { - let endpoint = Endpoint::new("http".into(), "localhost".into(), Some(57291)); - let timeout_ms = 10_000; - let rpc_client = Arc::new(GrpcClient::new(&endpoint, timeout_ms)); + let endpoint = Endpoint::localhost(); // http://localhost:57291 + let timeout_ms = 10_000; // DEFAULT_GRPC_TIMEOUT_MS + + // `.rpc()` uses the client AS PROVIDED. Wrap it, or you silently lose + // response verification. (`.grpc_client(&endpoint, Some(timeout_ms))` and the + // `for_*` constructors wrap for you.) + let rpc_client = Arc::new(VerifyingRpcClient::new(GrpcClient::new(&endpoint, timeout_ms))); let keystore_path = std::path::PathBuf::from("../local-keystore"); let keystore = Arc::new(FilesystemKeyStore::new(keystore_path) @@ -68,7 +129,6 @@ pub async fn setup_local_client() -> Result { .rpc(rpc_client) .sqlite_store(store_path) .authenticator(keystore.clone()) - .in_debug_mode(true.into()) .build() .await .context("Failed to build local Miden client")?; @@ -77,35 +137,39 @@ pub async fn setup_local_client() -> Result { } ``` +Imports: `use miden_client::rpc::{Endpoint, GrpcClient, VerifyingRpcClient};`. + +**There is no debug-mode switch.** `ClientBuilder::in_debug_mode`, `Client::in_debug_mode`, the `DebugMode` type, the CLI `--debug` flag and the `MIDEN_DEBUG` environment variable do not exist — a `.in_debug_mode(..)` line will not compile. What replaced them is a debug adapter, and it is narrower than it sounds: the CLI has its own `dap` feature that is **not** enabled by default, so a stock `miden-client` binary exposes nothing. Built with it, `--start-debug-adapter ` (optionally `--record `) is accepted by exactly two commands — `exec` and `consume-notes`. Passing it to `mint`, `transfer`, `swap` or a PSWAP command is an unknown-argument error. MASM print-style debugging goes through the `miden::core::debug` procedures, which print unconditionally, so there is nothing to gate. + +`build()` fails with `ClientInitializationError` if **either** the RPC client or the store is missing — they are two independent checks, so supplying only one is not enough. + Use separate paths (`local-keystore/`, `local-store.sqlite3`) to avoid contaminating testnet state. -## Step 3: Create Local Validation Binary +## Step 3: Create a local validation binary -Create `integration/src/bin/validate_local.rs` mirroring the existing testnet binary (`increment_count.rs`) but using `setup_local_client()`. +Add a local validation binary alongside your existing network/testnet validation binary, mirroring its structure but swapping in `setup_local_client()`. Pick any conventional name for it (for example `validate_local`) -- adjust to your repo's binary layout. The binary must: -1. Call `setup_local_client()` instead of `setup_client()` -2. Sync state: `client.sync_state().await?` -3. Build contracts (same as existing binary) +1. Call `setup_local_client()` instead of the network setup function +2. Sync state: `client.sync_state().await?`. This is **mandatory before the first submit**, not just good hygiene: transaction inputs are sealed against chain state, and a client that has not synced genesis and the chain tip cannot resolve the encryption key. +3. Build contracts (same as the existing binary) 4. Create accounts, create notes, submit transactions 5. Sync again after each transaction submission 6. Wait for transaction inclusion (poll `sync_state` until account state updates) 7. Verify final state matches MockChain test expectations 8. Print clear pass/fail for each verification step -Key differences from testnet binary: +Key differences from the network binary: - Localhost endpoint (port 57291) - Separate keystore and store paths - Must handle block production timing (sync + wait between submissions) -**Account deployment**: `Client::add_account()` only writes the account to the local client store; it does not register the account on-chain. To make a public or network account discoverable by other clients, submit a transaction involving the account (typically the account's first transaction). Until that transaction is included in a block, `get_account_details(id)` from any other client returns "not found". - ## Step 4: Run and Verify Ensure clean client state before running (the node should already be clean from Step 1): ```bash rm -rf local-keystore/ local-store.sqlite3 -cargo run --bin validate_local --release +cargo run --bin --release ``` ### Verification Checklist @@ -120,11 +184,14 @@ cargo run --bin validate_local --release ## Step 5: Inspect Node Logs -Run the node with verbose logging: +Run the node with verbose logging. The helper script honors `RUST_LOG` and writes a per-component log file per service; if you launch the binaries directly, set it on the process you want to inspect (the sequencer carries the RPC): + ```bash -RUST_LOG=info miden-node bundled start \ - --data-directory local-node-data \ - --rpc.url http://0.0.0.0:57291 +RUST_LOG=info make start-node +# or, running the sequencer directly: +RUST_LOG=info miden-node sequencer --rpc.listen 127.0.0.1:57291 --data-directory /node \ + --validator.url http://127.0.0.1:50101 --ntx-builder.url http://127.0.0.1:50301 \ + --block.interval 3s --batch.interval 1s ``` Look for: @@ -136,11 +203,14 @@ Look for: | Symptom | Cause | Fix | |---------|-------|-----| -| `Unavailable` RPC error | Node not running or wrong port | Start node, verify port 57291 | -| `accept header validation failed` after switching networks | Client store cached the genesis commitment from a different network | Delete the active client store (`store.sqlite3` by default; `local-store.sqlite3` for local validation) and re-sync | -| Version mismatch error | Node binary lags client crate (`cargo install miden-node` may pin an older published crate) | Reinstall to match `miden-client` from `integration/Cargo.toml`: `cargo install miden-node --locked --git https://github.com/0xMiden/miden-node --tag v`, or use `midenup` | -| Vite or proxy returns 404 on RPC calls from frontend | Proxy targets the wrong path prefix | gRPC paths are `/rpc.Api/` (e.g. `/rpc.Api/Status`, `/rpc.Api/SyncNotes`, `/rpc.Api/GetAccount`); forward the `/rpc.Api` prefix in the proxy config | +| `Unavailable` RPC error | Node not running or wrong port | Start node, verify the sequencer's RPC is listening on 57291 | +| `RpcError::AcceptHeaderError` / "The node rejected the request due to a version mismatch." | Node and client crate versions differ | Run the node version resolved by your client's `Cargo.lock`. The version and genesis commitment are negotiated at connect via the `accept` header and a mismatch is rejected; there is no mixed-version mode | +| `miden-validator start` exits immediately | Missing threshold storage-key material | Pass all four `--storage-key.*` flags; for a local test node use the vendored `scripts/testdata/insecure-golden-storage-key/` fixtures | +| Unknown-argument error on `bootstrap` | Using the old flag names | Genesis is its own `miden-validator genesis --config ` step, and all three `bootstrap` commands take `--genesis ` (not `--file`, not `--genesis-config-file`) | +| Requests throttled / intermittent failures under load | Sequencer rate limiter at its default | Start the sequencer with `--rpc.rate-limit.burst-size 10000 --rpc.rate-limit.replenish-per-second 10000` | +| Private notes never arrive | No note transport configured | `ClientBuilder::for_localhost()` sets none — run `make start-note-transport` and pass `.note_transport(..)` | | Transaction rejected | Invalid proof or state | Check contract code, reset node data, try again | -| Account not found after `add_account()` | `add_account()` is local-only; it does not register the account on-chain | Submit a transaction involving the account to deploy it on-chain, then `sync_state()` | -| Store errors or deserialization failures | Stale state from a previous session, or cached genesis from a different network | Wipe everything: `rm -rf local-node-data/ local-keystore/ local-store.sqlite3` and re-bootstrap | -| Block not produced | Node produces blocks when transactions arrive | Submit a transaction; check `--block-producer.block-interval` setting | +| Account not found after creation | Haven't synced | Call `sync_state()` after account creation | +| Store errors or deserialization failures | Stale state from previous session (or artifacts from an earlier protocol version, which do not round-trip) | Wipe the node data, keystore, and client store, then re-bootstrap from a fresh genesis | +| `.sqlite_store(..)` does not compile | Extension trait not in scope | `use miden_client_sqlite_store::ClientBuilderSqliteExt;` | +| Block not produced | Node produces blocks on the sequencer's configured cadence | Submit a transaction; check the sequencer's `--block.interval` (and `--batch.interval`) settings, or consult `miden-node sequencer --help` | diff --git a/.claude/skills/miden-client-cli/SKILL.md b/.claude/skills/miden-client-cli/SKILL.md index c4f6682..4c5c7b9 100644 --- a/.claude/skills/miden-client-cli/SKILL.md +++ b/.claude/skills/miden-client-cli/SKILL.md @@ -1,58 +1,60 @@ --- name: miden-client-cli -description: Map to the official Miden client CLI. The recommended path is via midenup, which installs a managed `client` toolchain component and is invoked as `miden client ...` (component invocation through the midenup `miden` wrapper). The underlying / direct-install path is the `miden-client-cli` crate (`cargo install miden-client-cli --locked`), which exposes the binary `miden-client` and is invoked as `miden-client ...`. Both paths run the same upstream binary. Covers install, init, network selection, and where to find the canonical command reference and configuration docs. Use when an agent needs to create accounts, query state, mint, send, or consume notes against a running Miden node from the command line; pair with the local-node-validation skill for localhost workflows. +description: Map to the official Miden client CLI for the current project-template v16 line. Use midenup's managed `miden client ...` component when it matches the intended workflow; use direct `miden-client-cli` installation when exact parity with this repository's resolved `Cargo.lock` client is required. Covers install, init, network selection, and canonical command/configuration references. Use when an agent needs to create accounts, query state, mint, transfer, or consume notes against a running Miden node; pair with the local-node-validation skill for localhost workflows. --- # Miden Client CLI -The Miden client CLI is the command-line wrapper around the `miden-client` library. It creates accounts, syncs state, mints assets, sends transactions, and consumes notes against a running Miden node. +The Miden client CLI is the command-line wrapper around the `miden-client` library. It creates accounts, syncs state, mints assets, submits transactions, and consumes notes against a running Miden node. -This skill maps agents to the canonical install paths and upstream command reference. Do not memorize commands or config keys here. Follow the links. +This skill maps agents to the managed midenup invocation and the exact client version currently resolved by this repository's lockfile. Do not mix a store or command set from another client release into the v0.16 workflow. When to reach for this skill: the user wants to interact with a node from the shell. For Rust-library work against a localhost node from a binary, see the `local-node-validation` skill. For test-side note construction, see `rust-sdk-testing-patterns`. -## Two Invocation Paths +## Installation and Version Check -There are two ways to invoke the same upstream binary. +The repository README uses midenup for the managed Miden toolchain. After installing and initializing midenup, the `miden` wrapper delegates `miden client ` to the toolchain's `client` component, whose installed executable is `miden-client`. Always check the installed component before using an existing store: -**Through midenup (recommended).** Run `miden client `. This is component invocation: the midenup `miden` wrapper looks up the `client` component in the active toolchain and runs its installed executable. `miden client` is **not** an alias; the midenup README alias table only documents short-hand aliases (such as `miden account`, `miden faucet`, `miden new-wallet`, `miden send`), and component invocation is independent of the alias map. The toolchain manifest declares `installed_executable: miden-client` for the `client` component. - -**Direct install.** Run `cargo install miden-client-cli --locked`, then invoke `miden-client `. This installs the same upstream binary that midenup delegates to. - -Both paths execute identically. - -References: -- midenup install, init, toolchain delegation, and alias docs: [github.com/0xMiden/midenup](https://github.com/0xMiden/midenup). midenup is unpublished; the canonical install command is `cargo install --path .` or `cargo install --git `. -- miden-client CLI install and setup: [miden-client `bin/miden-cli/README.md`](https://github.com/0xMiden/miden-client/blob/main/bin/miden-cli/README.md). +```sh +miden client --version +``` -## Install via midenup +Midenup components are independent of this repository's `Cargo.lock`, so use the managed component only after the version check matches the workflow you are validating. When exact parity with the current lockfile is required, install and verify the resolved client directly: ```sh -cargo install midenup && midenup init -midenup install stable +cargo install miden-client-cli --version 0.16.0-rc.2 --locked +test "$(miden-client --version)" = "miden-client 0.16.0-rc.2" ``` -`midenup init` creates a `miden` symlink in `$CARGO_HOME/bin` (default `~/.cargo/bin`). If `miden` is not found after init, ensure `$CARGO_HOME/bin` is on your `PATH`. +`integration/Cargo.toml` lower-bounds `miden-client` and `miden-client-sqlite-store` at `0.16.0-rc.1`; `Cargo.lock` currently resolves both to `0.16.0-rc.2`, backed by protocol/standards/testing `0.16.0-rc.6`. + +References: +- midenup install, init, toolchain delegation, and component docs: [github.com/0xMiden/midenup](https://github.com/0xMiden/midenup). +- Exact direct CLI install and setup: [miden-client v0.16.0-rc.2 `bin/miden-cli/README.md`](https://github.com/0xMiden/miden-client/blob/v0.16.0-rc.2/bin/miden-cli/README.md). ## First-Time Initialization -`init` writes a `miden-client.toml` config in the current directory: +`init` is optional because the CLI can self-initialize. Explicit initialization is safer when selecting a network and a fresh store. By default it creates global configuration at `~/.miden/miden-client.toml`; pass `--local` to create `./.miden/miden-client.toml`. A local config takes precedence over the global config. + +For this repository's Testnet runtime, use a fresh v0.16 local configuration and store. With direct install: ```sh -miden client init --network localhost # or testnet | devnet | http://[:port] +miden-client init --local --network testnet --store-path store.sqlite3 ``` -Subsequent commands operate against that config. For localhost workflows, pair this skill with `local-node-validation`: it boots a local node on `http://0.0.0.0:57291` and prepares a clean keystore. +With midenup, the equivalent shape is `miden client init --local --network testnet --store-path store.sqlite3`. Omitting `--network` selects Testnet. Other accurate choices are `localhost`, `testnet`, or a full custom HTTP(S) RPC URL. Do not open a pre-v0.16 or different-network SQLite store with this client; archive it and initialize a fresh path. For localhost workflows, pair this skill with `local-node-validation`. + +Current v0.16 command changes include `transfer` in place of the removed `send` subcommand, and `account --inspect --verbose` in place of the removed `account --show --with-code`. Use `miden-client --help` for the exact installed command surface. The old client/CLI debug-mode toggle and `--debug` flag are removed. ## Canonical Command Reference -Follow the canonical in-repo references on `0xMiden/miden-client` (the active line, which matches project-template's pinned `miden-client = "0.14"`). The `miden-docs` site (`0xMiden.github.io/miden-docs/...`) is not used here because those URLs are not stable. +Follow the canonical references at the exact `v0.16.0-rc.2` tag, which matches the client currently resolved in project-template's `Cargo.lock`. -- CLI Reference: [`docs/external/src/rust-client/cli/index.md`](https://github.com/0xMiden/miden-client/blob/main/docs/external/src/rust-client/cli/index.md) -- CLI Configuration: [`docs/external/src/rust-client/cli/cli-config.md`](https://github.com/0xMiden/miden-client/blob/main/docs/external/src/rust-client/cli/cli-config.md) -- Repo overview and recent release notes: [`0xMiden/miden-client`](https://github.com/0xMiden/miden-client) (browse the `CHANGELOG.md` on `main` for the latest behavioral changes; pin to a release tag if you need a snapshot). +- CLI Reference: [`docs/external/src/rust-client/cli/index.md`](https://github.com/0xMiden/miden-client/blob/v0.16.0-rc.2/docs/external/src/rust-client/cli/index.md) +- CLI Configuration: [`docs/external/src/rust-client/cli/cli-config.md`](https://github.com/0xMiden/miden-client/blob/v0.16.0-rc.2/docs/external/src/rust-client/cli/cli-config.md) +- Release behavior: [`CHANGELOG.md`](https://github.com/0xMiden/miden-client/blob/v0.16.0-rc.2/CHANGELOG.md). -For the live command list and flags on the installed binary, run `miden client --help` (midenup path) or `miden-client --help` (direct-install path). Drill into a specific command with `miden client --help`. The canonical references above remain authoritative for deeper documentation. +For the live command list and flags, run `miden-client --help` and drill into a command with `miden-client --help`. If an exact-version midenup component is deliberately selected, the equivalent invocation is `miden client ...`. ## Cross-References diff --git a/.claude/skills/miden-concepts/SKILL.md b/.claude/skills/miden-concepts/SKILL.md index 7ba0c54..b3bf0c7 100644 --- a/.claude/skills/miden-concepts/SKILL.md +++ b/.claude/skills/miden-concepts/SKILL.md @@ -21,7 +21,7 @@ Key properties: | Transactions involve sender + receiver | Transactions involve **one account only** | | Public state by default | **Private by default** | | Validators execute transactions | **Client executes and proves** locally | -| Gas metering | No gas (computational bounds exist) | +| Gas metering | **Fees are paid by the account's own auth procedure**, which funds a public `TX_FEE` note out of the vault — not burned by the kernel. Execution is separately bounded by `MAX_TX_EXECUTION_CYCLES` | | Synchronous contract calls | **Asynchronous** communication via notes | | Accounts are balances + storage | Accounts are **full smart contracts** with code, storage, and vault | @@ -29,48 +29,73 @@ Key properties: ### Accounts Each account is an independent smart contract containing: -- **Code** — Immutable logic compiled from Rust components -- **Storage** — Up to 255 slots exposed in Rust as `StorageValue` or `StorageMap` +- **Code** — Logic compiled from Rust components +- **Storage** — Up to 255 slots (`AccountStorage::MAX_NUM_STORAGE_SLOTS`), exposed in guest Rust as `StorageValue` or `StorageMap`. Slots are **named, not positional**: each is a `StorageSlotName` paired with its content, kept sorted by name, and a duplicate name is rejected (`AccountError::DuplicateStorageSlotName`). The slot id is derived from the name, which is why a component reading its own named slot is portable across accounts without taking the slot as a parameter - **Vault** — Holds fungible and non-fungible assets -- **Nonce** — Incremented with each state change -- **ID** — Unique identifier (prefix + suffix, 2 Felts) +- **Nonce** — Must increase whenever the account's state changes; an account update records the amount it increased by, not just the fact that it did +- **ID** — Unique identifier (prefix + suffix, 2 Felts). `AccountId` does **not** convert into `[Felt; 2]`; reach the parts with `id.prefix().as_felt()` and `id.suffix()` + +Account state changes reach the network as an **`AccountPatch`** (`miden_protocol::account::AccountPatch`), which describes the account's new state. `AccountDelta` still exists and is still *relative* — it records changes rather than final values — and is what a `TransactionSummary` commits to. Don't assume the two are interchangeable: `TransactionSummary::account_delta()` deliberately returns the relative `AccountDelta`. Accounts are composed from **components** — reusable Rust modules annotated with `#[component]`. ### Notes Notes are **UTXO-like messages** for asynchronous inter-account communication. A note contains: - **Script** — Logic that executes when the note is consumed -- **Inputs** — Data passed to the script (Vec) -- **Assets** — Fungible/non-fungible tokens attached to the note -- **Metadata** — Sender, tag, note type (public/private) +- **Storage** — Data accessible to the script during execution (`NoteStorage`, backed by `Vec`), capped at `MAX_NOTE_STORAGE_ITEMS = 1024` +- **Assets** — Fungible/non-fungible tokens attached to the note, capped at `MAX_ASSETS_PER_NOTE = 16` +- **Metadata** — Sender, tag, note type (public/private). That trio is the *partial* metadata; the full `NoteMetadata` also carries the attachment headers and their commitment +- **Attachments** — Up to `NoteAttachments::MAX_COUNT = 4` attachments, addressed by scheme rather than position. Each holds 1–`NoteAttachment::MAX_NUM_WORDS` (256) words, capped at 512 words per note across all of them — so this is a real payload channel, not a four-word field Notes are created as **output notes** by one transaction and consumed as **input notes** by another. ### Transactions -A transaction is a **single-account state transition** with 4 phases: -1. Consume input notes (execute their scripts against the account) -2. Execute transaction script (optional, for one-off logic) -3. Update account state (storage, vault, nonce) -4. Produce output notes (for other accounts to consume later) +A transaction is a **single-account state transition**. The kernel runs four phases: +1. **Prologue** — prepare the root context from the transaction inputs +2. **Note processing** — run every input note's script against the account +3. **Transaction script** — optional one-off logic +4. **Epilogue** — run the account's **authentication procedure** (this is where the fee is paid), then compute and validate the final state + +Updating account state and producing output notes are effects of phases 2-3, not phases of their own. The thing people most often leave out of this list is that **authentication and fee payment happen in the epilogue**, after all scripts have run. + +**Transaction summaries are six words.** A `TransactionSummary` is what an account's authentication procedure signs, and its commitment preimage is laid out as: + +```text +[ACCOUNT_DELTA_COMMITMENT, INPUT_NOTES_COMMITMENT, OUTPUT_NOTES_COMMITMENT, + BLOCK_COMMITMENT, [expiration_delta, user_param0, user_param1, user_param2], + [user_param3, user_param4, user_param5, user_param6]] +``` + +The trailing user parameters give an auth procedure a way to bind extra data (a replay-protection salt, a maximum fee) into the same signature. A component that hashes a shorter layout **compiles and fails at runtime**; the MASM side of this constant is `TX_SUMMARY_NUM_ELEMENTS = 24` in the standard auth library. + +**Fees are paid from the authentication procedure.** The auth procedure computes the fee and funds a public `TX_FEE` note out of the account's vault before the summary is created. Callers supply the conversion data with `TransactionRequestBuilder::fee_conversion_info(conversion_info, salt)`; network accounts need a fee policy of their own. **Important**: A two-party transfer (Alice sends Bob tokens) requires TWO transactions: 1. Alice's transaction creates a P2ID note with tokens attached 2. Bob's transaction consumes that note, receiving the tokens ### Assets -- **SDK shape**: `Asset { key: Word, value: Word }` + +An asset is **two words**: an identifier word and a value word. On the operand stack and in MASM doc comments they appear as `ASSET_ID` followed by `ASSET_VALUE`; the protocol type alias is `Asset = struct { id: word, value: word }`. + - **Fungible**: asset amount lives in `asset.value[0]` - **Non-fungible**: Unique token tied to a faucet account - Assets live in account **vaults** and move between accounts via notes -- Created by **faucet accounts** using `faucet::create_fungible_asset()` or `faucet::mint()` +- Minted and destroyed by **faucet accounts** via `faucet::mint(asset)` / `faucet::burn(asset)`, which take an already-built `Asset`. There is no in-transaction asset construction: the kernel exposes no `create_fungible_asset` / `create_non_fungible_asset`. +- A note may carry at most **`MAX_ASSETS_PER_NOTE` = 16** assets. + +**`AssetId` and `AssetClass` are different things, and the names are a trap.** `AssetId` is the *unique identifier of an asset in the vault*; its Word layout is `[asset_class_suffix, asset_class_prefix, faucet_id_suffix|reserved|composition, faucet_id_prefix]`, and `AssetId::hash()` produces the `AssetIdHash` used as the vault SMT key. `AssetClass` is the narrower thing that *distinguishes different assets issued by the same faucet* — two felts, and one component of an `AssetId`. Code that treats an `AssetId` as if it were a per-faucet class (or vice versa) type-checks and is wrong. + +> **Layer note.** The Rust *contract* SDK (the guest `miden` crate) builds against an earlier protocol snapshot than the client/protocol line, and there the guest type is still `Asset { key: Word, value: Word }` with `asset.value[0]` as the fungible amount. The field is named `key`, not `id`, in guest contract code. Read the layer you are actually writing for rather than renaming across the boundary. ### Felt and Word - **Felt**: Field element in the Goldilocks prime field (p = 2^64 - 2^32 + 1). The fundamental data unit. - **Word**: Array of 4 Felts (32 bytes). Used for cryptographic hashes, storage keys, account IDs. -- **Current constructors**: `Felt::new`, `Felt::from_u8` / `from_u16` / `from_u32`, `Felt::from_canonical_checked`, `Word::new`, `Word::from([u32; 4])`, `Word::from([Felt; 4])`, `Word::try_from([u64; 4])` +- **Felt constructors** (Rust `miden_field::Felt` — the same type used host-side in clients/tests *and* guest-side inside `#[component]`/`#[note]` contract code, which re-exports it): `Felt::new(u64)` is **fallible** — it returns `Result` and rejects values at or above `Felt::ORDER` (it delegates to `from_canonical_checked`), so callers must `?`/match it (guest code typically `Felt::new(0).unwrap()`). `Felt::new_unchecked(u64)` is the raw, non-reducing constructor (any `u64`, no validation) — an out-of-range value yields a non-canonical `Felt`. The field order constant is `Felt::ORDER`; there is no `Felt::MODULUS`. Always-succeed constructors (return a bare `Felt`): `Felt::from_u8` / `from_u16` / `from_u32`. Non-panicking but fallible: `Felt::from_canonical_checked(u64) -> Option` (returns `None` when out of range). Note the JS/React SDK's `Felt` is a *different* type whose `Felt.new(u64)` is infallible and whose accessor is `.asInt()`. +- **Word constructors**: `Word::new`, `Word::from([u32; 4])`, `Word::from([Felt; 4])`, `Word::try_from([u64; 4])` - **Current accessors**: `felt.as_canonical_u64()`, `word.as_elements()`, `word.into_elements()`, `word.as_bytes()`, `word.to_hex()` -**WARNING**: Felt arithmetic is **modular**. Subtraction wraps around the prime. Always validate with `.as_canonical_u64()` before subtracting. See the rust-sdk-pitfalls skill for details. +**WARNING**: Felt arithmetic is **modular**. Subtraction wraps around the prime. Always validate with `.as_canonical_u64()` before subtracting (`.asInt()` in the JS/React SDK). See the rust-sdk-pitfalls skill (or frontend-pitfalls for the JS side) for details. ## Standard Note Patterns @@ -79,6 +104,30 @@ A transaction is a **single-account state transition** with 4 phases: | **P2ID** | Send assets to a specific account | Note script checks consumer's ID matches target | | **P2IDE** | P2ID with expiration | Adds block-height timelock; sender can reclaim after expiry | | **SWAP** | Atomic asset exchange | Note offers asset A, requests asset B; consumer provides B | +| **PSWAP** | Partial-fill swap | A SWAP that can be consumed for part of the offered amount, leaving a remainder note | + +Those are the ones you write by hand. The full `StandardNote` set is larger — it also covers `MINT`, `BURN`, `FEE_SPONSORSHIP`, `TX_FEE`, and the component-configuration notes (`OWNER_CONFIG`, `RBAC_CONFIG`, `PAUSE_CONFIG`, `ALLOWLIST_CONFIG`, `BLOCKLIST_CONFIG`, `NETWORK_ACCOUNT_CONFIG`, `FAUCET_POLICY_CONFIG`, `FAUCET_METADATA_CONFIG`, `CONSTANT_FEE_POLICY_CONFIG`, `MIN_BURN_AMOUNT_CONFIG`). + +Standard notes are built with typed builders rather than a `create(..)` constructor: `P2idNote::builder()…build()?`, with fluent `.asset(..)` / `.assets(..)` / `.attachment(..)` / `.attachments(..)`. `MINT` and `BURN` are unified across faucet kinds — one `MintNote` / `BurnNote` rather than per-faucet-kind scripts. + +## Standard Components (miden-standards) + +| Component | Purpose | +|-----------|---------| +| `BasicWallet` | Standard wallet. Three interface procedures: `receive_asset`, `move_asset_to_note`, `create_note` (roots via `receive_asset_root()`, `move_asset_to_note_root()`, `create_note_root()`) | +| `FungibleFaucet` | Mint/burn fungible tokens (`mint_and_send`, `receive_and_burn`, plus metadata accessors and owner-gated setters); built via `FungibleFaucet::builder()` | +| `NoAuth` | No authentication (for testing) — but it still pays the transaction fee | +| `AuthSingleSig` | Production signature authentication — one component covering both Falcon-512 and ECDSA-K256 key types | + +`output_note::create` is account-context only, so a transaction or note script cannot create a note directly — it goes through an account component wrapper such as `BasicWallet::create_note`. + +**Auth**: `AuthSingleSig` dispatches on the key type, so one component handles both Falcon-512 and ECDSA-K256 keys. The Falcon-512 scheme uses Poseidon2 as its hash function and is named `Falcon512Poseidon2`. Construct it with `AuthSingleSig::new(approver)` or the typed helpers `falcon512_poseidon2(pk)` / `ecdsa_k256_keccak(pk)` / `from_public_key(pk)`. + +Keys are wrapped in `Approver { pub_key, auth_scheme }`, and multi-signature setups use `ApproverSet { approvers, threshold }`. There is no `AccountBuilder::with_auth_component` and no `AuthMethod` or `AuthSingleSigAcl`: auth components are added with `with_component(s)` like any other component. + +The auth roster is wider than `NoAuth` + `AuthSingleSig` — `miden_standards::account::auth` also exports `AuthMultisig`, `AuthMultisigSmart`, `AuthGuardedMultisig` (each with a matching `*Config` type), and `AuthNetworkAccount`, which takes its parts directly rather than a config struct. + +**Fungible faucet**: `FungibleFaucet` is the fungible-faucet component, constructed with the `bon`-generated `FungibleFaucet::builder()` (required setters `.name(TokenName::new(..)?)`, `.symbol(TokenSymbol::new(..)?)`, `.decimals(n)`, `.max_supply(AssetAmount)`, then `.build()?`). ## Development Model @@ -91,7 +140,9 @@ Three contract types: - `#[note]` — Note script (executes when consumed) - `#[tx_script]` — One-off transaction logic -Contracts are tested locally with **MockChain** (no network needed) and deployed via **miden-client**. +Contracts are tested locally with **MockChain** (no network needed) and deployed via the Miden Rust client. That client lives in the **`0xMiden/miden-client`** repository and is published as the crate `miden-client`; the browser client is a separate repository, `0xMiden/web-sdk`. + +A component's methods are not implicitly part of the account interface. In Rust, mark each callable method with `#[account_procedure]` on the `#[component]` **trait**; in hand-written component MASM, annotate the exported procedure with `@account_procedure` (or `@auth_script` for an authentication component). An unmarked procedure still compiles and is still exported by the package, but is not reachable as an account procedure. ## Key Design Decisions for App Architects diff --git a/.claude/skills/rust-sdk-patterns/SKILL.md b/.claude/skills/rust-sdk-patterns/SKILL.md index 8ce7ed4..76c779e 100644 --- a/.claude/skills/rust-sdk-patterns/SKILL.md +++ b/.claude/skills/rust-sdk-patterns/SKILL.md @@ -1,42 +1,198 @@ --- name: rust-sdk-patterns -description: Complete guide to writing Miden smart contracts with the Rust SDK. Covers #[component], #[note], #[tx_script] macros, storage patterns, native functions, asset handling, cross-component calls, P2ID note creation, and asset receiving via component methods. Use when writing, editing, or reviewing Miden Rust contract code. +description: Complete guide to writing Miden smart contracts with the Rust SDK. Covers the three-part #[component_storage]/#[component] account-component pattern, the #[account_procedure] interface marker, #[note]/#[note_script] notes, #[tx_script] scripts, the #[account(...)] wrapper and its generated traits, storage patterns, native functions, asset handling, cross-component calls, P2ID note creation, and asset receiving via component methods. Use when writing, editing, or reviewing Miden Rust contract code. --- # Miden Rust SDK Patterns ## Three Contract Types -### Account Component (`#[component]`) +### Account Component (three-part pattern) Defines reusable logic and storage for accounts. Accounts are composed of one or more components. -See [counter-account/src/lib.rs](../../../contracts/counter-account/src/lib.rs) for a working example demonstrating `#[component]`, typed `StorageMap`, `get()`/`set()`, and felt arithmetic. +An account component is written as **three parts** — the storage struct is annotated `#[component_storage]`, and `#[component]` applies to the API trait and the impl block: -**Cargo.toml for accounts:** See [counter-account/Cargo.toml](../../../contracts/counter-account/Cargo.toml) for the required `crate-type`, `miden` dependency, `component` metadata, and `project-kind`. +1. `#[component_storage]` on the **storage struct** — declares typed `#[storage(...)]` fields and derives slot names. +2. `#[component]` on a **trait** — the component's exported API (this is the source of the generated WIT interface). +3. `#[component]` on the **`impl Trait for Storage`** block — the behavior, wired to the guest bindings. -### Note Script (`#[note]`) +```rust +#![no_std] +#![feature(alloc_error_handler)] +#[macro_use] +extern crate alloc; +use miden::*; + +#[component_storage] +struct BankStorage { + #[storage(description = "initialized")] + initialized: StorageValue, + #[storage(description = "balances")] + balances: StorageMap, +} + +#[component] +trait Bank { + #[account_procedure] + fn initialize(&mut self); + #[account_procedure] + fn deposit(&mut self, depositor: AccountId, deposit_asset: Asset); +} + +#[component] +impl Bank for BankStorage { + fn initialize(&mut self) { /* read/write self.initialized, etc. */ } + fn deposit(&mut self, depositor: AccountId, deposit_asset: Asset) { /* ... */ } +} +``` + +Only the trait's methods are exported to WIT. Inherent (`impl BankStorage`) methods stay private to the contract — use them for helpers like key derivation. + +Reference: `examples/basic-wallet/src/lib.rs` and `examples/counter-contract/src/lib.rs` in the compiler repo. + +### `#[account_procedure]`: being exported is not the same as being callable + +A component's methods are **not** implicitly part of the account interface. Mark every method that must be reachable from a transaction script, a note script, foreign procedure invocation (FPI), or a sibling component with `#[account_procedure]`. + +Three rules that catch people out: + +- **It goes on the `#[component]` trait declaration, not on the impl block.** Putting it on the impl does nothing. +- **An unmarked method still compiles and is still exported by the package** — it simply is not an account procedure. There is no error at build time; the call site fails later. +- **`#[account_procedure]` and `#[auth_script]` cannot be combined in one component.** They belong to different component kinds: an ordinary account component versus an authentication component. An authentication component uses `#[auth_script]` alone, and its single method is the interface implicitly. + +`#[account_procedure]` needs no import — the enclosing `#[component]` macro recognises it, exactly as it does `#[auth_script]`. + +Any number of methods may be marked. + +**Project metadata for accounts:** `[lib]` needs `kind`, `namespace`, and an explicit `path`; `[dependencies]` needs `miden-core` and `miden-protocol`: + +```toml +# miden-project.toml, matching contracts/counter-account/miden-project.toml +[package] +name = "counter-account" +version = "0.1.0" + +[lib] +path = "src/lib.rs" +kind = "account-component" +namespace = "miden:counter-account/counter-contract@0.1.0" + +[dependencies] +miden-core = "*" +miden-protocol = "*" + +[package.metadata.miden] +supported-types = ["RegularAccountImmutableCode"] +``` + +`supported-types` also accepts `"RegularAccountUpdatableCode"` and the faucet kinds `["FungibleFaucet", "NonFungibleFaucet"]`. + +The project-template contract `Cargo.toml` files currently use `edition = "2021"`, `crate-type = ["cdylib"]`, and published `miden` / `miden-sdk-build-script-support` `0.14` dependencies. Copy the local manifests unless intentionally changing the template line: + +```toml +[package] +name = "counter-account" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +miden = { version = "0.14" } + +[build-dependencies] +miden-sdk-build-script-support = { version = "0.14" } +``` + +Contracts build on `nightly-2026-09-01` with target `wasm32-wasip2`. Use the midenup / cargo-miden command documented in this repository's `README.md` and `CLAUDE.md`; the local `build.rs` calls `miden_sdk_build_script_support::prepare_package_cache()` so source dependencies are available to the SDK macros during Cargo checks and IDE analysis. + +### Note Script (`#[note]` / `#[note_script]`) Executes when a note is consumed by an account. Can call component methods on the consuming account. -See [increment-note/src/lib.rs](../../../contracts/increment-note/src/lib.rs) for a working example demonstrating `#[note]`, `#[note_script]`, and cross-component calls. +A note is two parts: a `#[note]` struct (the note inputs type) and a `#[note]` `impl` block containing exactly one `#[note_script]` entrypoint. The entrypoint takes `self` **by value**, exactly one `Word` argument, and optionally a single reference to an `#[account(...)]` wrapper (`&MyAccount` or `&mut MyAccount`). The consuming account is declared separately with `#[account(...)]`. + +```rust +#![no_std] +#![feature(alloc_error_handler)] + +use miden::*; -**Cargo.toml for notes:** See [increment-note/Cargo.toml](../../../contracts/increment-note/Cargo.toml) for the required `miden` deps, cross-component dependencies, wit deps, and `project-kind = "note-script"`. +#[account(counter_account::CounterContract)] +pub struct Wallet; + +#[note] +struct IncrementNote; + +#[note] +impl IncrementNote { + #[note_script] + fn run(self, _arg: Word, account: &mut Wallet) { + let initial_value = account.get_count(); + account.increment_count(); + let expected_value = initial_value + Felt::from_u32(1); + let final_value = account.get_count(); + assert_eq(final_value, expected_value); + } +} +``` + +A `#[note]` struct with fields is auto-decoded from `active_note::get_storage()`. The decoder is strict: it calls `ensure_eof()`, so surplus felts in the note's storage fail with `FeltReprError::TrailingData`. A zero-sized note type skips `get_storage()` entirely. + +Reference: `examples/p2id-note/src/lib.rs`, `examples/p2ide-note/src/lib.rs`, `examples/counter-note/src/lib.rs`. + +**Project metadata for notes:** `[lib] kind = "note"`, plus `namespace` and `path`. Conventional namespace shape is `miden:/miden-@0.1.0`. ### Transaction Script (`#[tx_script]`) One-off logic executed in the context of an account. Used for initialization, admin operations, etc. +`#[tx_script]` annotates a free `fn run`. Its signature is `fn run(arg: Word)` or `fn run(arg: Word, account: &mut MyAccount)` where `MyAccount` is an `#[account(...)]` wrapper. There is no `crate::bindings::Account` — you declare the account wrapper yourself and the macro instantiates it as the active account. + ```rust #![no_std] #![feature(alloc_error_handler)] use miden::*; -use crate::bindings::Account; + +#[account(bank_account::Bank)] +pub struct Wallet; #[tx_script] -fn run(_arg: Word, account: &mut Account) { +fn run(_arg: Word, account: &mut Wallet) { account.initialize(); } ``` -**Cargo.toml:** Same as account but with `project-kind = "tx-script"`. +Reference: `examples/basic-wallet-tx-script/src/lib.rs`. + +**Project metadata for tx scripts:** like a note, but `[lib] kind = "tx-script"` and `namespace = "miden:base/transaction-script@1.0.0"`, plus `path = "src/lib.rs"`. + +## `#[account(...)]` generates one trait per interface + +`#[account(pkg::Interface)]` does **not** generate inherent methods on the wrapper struct. It generates **one trait per referenced interface**, named after the interface and carrying the wrapper's visibility, and implements it for the wrapper. Single-component accounts still call `account.method(..)` unchanged — as long as the generated trait is in scope. + +The consequences worth knowing before you hit them: + +- **The wrapper struct must not share a name with any generated trait.** `#[account(counter_contract::CounterContract)] struct CounterContract;` is a hard error. Rename the struct (e.g. `Counter`). +- **A call site in a different module than the wrapper must `use` the generated trait.** A same-module `#[note]` / `#[tx_script]` entrypoint sees it automatically. +- **A referenced interface must export at least one method**, or `#[account(...)]` errors. +- **Wrappers are module-scope only.** +- **Clashing method names are disambiguated with UFCS**: `::deposit(account, asset)`. Generated traits are same-module so they need no import — but disambiguating against an `ActiveAccount` built-in does: `use miden::active_account::ActiveAccount;`. +- **Clashing trait names are renamed with `as`**: `#[account(counter_contract::CounterContract as RemoteCounter)]`. The path still selects the interface. +- **A component method named `new` is now legal.** `Wallet::new(id)` resolves to the inherent constructor, `wallet.new()` to the component method. + +## Storage Slot Naming + +Storage slot names are part of the on-chain storage ABI and are derived as: + +``` +:::: +``` + +The first segment is `[package] name` from **`miden-project.toml`** (character-sanitised, not re-snake-cased). The **middle segment is the interface segment of the `[lib].namespace`** (the part between the last `/` and `@`), snake-cased — **not** the snake-cased struct name. This deliberately decouples slot names from private Rust renames. The version suffix (`@0.1.0`) is ignored so the slot name stays stable, and there is no `slot(...)` attribute. + +Live values from the pinned examples: `counter_contract::counter_contract::count_map`, `auth_component_rpo_falcon512::auth_component::owner_public_key`. + +See the rust-sdk-pitfalls skill (P5) for more on slot naming. ## Storage Types @@ -45,26 +201,38 @@ fn run(_arg: Word, account: &mut Account) { | `StorageValue` | Single typed slot (flags, counters, IDs) | `.get() -> T` | `.set(T) -> T` | | `StorageMap` | Typed key-value mapping (balances, records) | `.get(K) -> V` | `.set(K, V) -> V` | +`K: WordKey`, and `T`/`V`: `WordValue`. `WordValue` is implemented for `Word`, `Felt`, `AssetAmount`, `Digest`, `AccountId`, `Recipient`, `Tag`, `NoteIdx`, `NoteType`; `WordKey` for the same set minus `Digest` and `Recipient`. + ## Native Function Modules | Module | Key Functions | Purpose | |--------|--------------|---------| -| `native_account::` | `add_asset(Asset)`, `remove_asset(Asset)`, `incr_nonce()` | Modify account vault/nonce | -| `active_account::` | `get_id() -> AccountId`, `get_balance(AccountId) -> Felt` | Query current account | -| `active_note::` | `get_assets() -> Vec`, `get_sender() -> AccountId` | Query note being consumed (typed note storage arrives as `self` in the `#[note_script]` method; see "Cross-Component Note Pattern" below) | +| `native_account::` | `add_asset(Asset) -> Word`, `remove_asset(Asset) -> Word`, `incr_nonce() -> Nonce`, `get_id() -> AccountId`, `get_initial_asset(Word) -> Word`, `get_initial_commitment() -> Word`, `was_procedure_called(Word) -> bool`, `compute_delta_commitment() -> Word` | Modify / read the native account | +| `active_account::` | `get_id() -> AccountId`, `get_nonce() -> Nonce`, `get_asset(asset_key: Word) -> Word`, `has_asset(asset_id: Word) -> bool`, `get_vault_root() -> Word`, `get_num_procedures() -> u32`, `get_procedure_root(u32) -> Word`, `has_procedure(Word) -> bool` | Query the active account | +| `active_note::` | `get_storage() -> Vec`, `get_initial_assets() -> Vec`, `get_sender() -> AccountId`, `get_recipient() -> Recipient`, `get_metadata() -> NoteMetadata`, `find_attachment(Felt) -> Option`, `write_attachment_to_memory(u32) -> Vec` | Query the note being consumed | | `note::` | `build_recipient(Word, Word, Vec) -> Recipient` | Build note recipients from serial number, script root, and note storage | -| `output_note::` | `create(Tag, NoteType, Recipient) -> NoteIdx`, `add_asset(Asset, NoteIdx)` | Create output notes | -| `faucet::` | `create_fungible_asset(Felt) -> Asset`, `mint(Asset)`, `burn(Asset)` | Asset minting | -| `tx::` | `get_block_number() -> Felt`, `get_block_timestamp() -> Felt` | Transaction context | -| Intrinsics | `assert(bool)`, `assertz(Felt)`, `assert_eq(Felt, Felt)` | Validation | +| `output_note::` | `create(Tag, NoteType, Recipient) -> NoteIdx`, `add_asset(Asset, NoteIdx)`, the `*_attachment` family | Create output notes | +| `faucet::` | `mint(Asset)`, `burn(Asset)` | Move assets in and out of existence | +| `tx::` | `get_block_number() -> BlockNumber`, `get_block_timestamp() -> u32`, `get_num_input_notes() -> u32`, `get_num_output_notes() -> u32`, `get_expiration_block_delta() -> u16`, `update_expiration_block_delta(u16)`, `execute_foreign_procedure(..)` | Transaction context and FPI | +| Intrinsics | `assert(Felt)`, `assertz(Felt)`, `assert_eq(Felt, Felt)` | Validation (`assert` fails unless the felt equals 1; `assertz` fails unless it equals 0) | -## Asset Handling +`add_asset`, `remove_asset` and the `active_account` queries are also trait methods auto-implemented on the `#[component_storage]` struct, so the idiomatic body is `self.add_asset(asset)` rather than the free function. + +### Three context restrictions the compiler will not catch -`Asset` is now a two-word value: +- **`native_account::incr_nonce()` may only be called from the account's authentication procedure.** The kernel asserts the caller's origin; calling it from an ordinary component method panics at runtime. +- **`output_note::create` is account-component context only.** A transaction or note script must go through a component method that wraps it — see `create_note` in `examples/basic-wallet/src/lib.rs`. +- **`native_account::add_asset` / `remove_asset` are likewise account-context only** (see pitfall P11). -**Constructor**: `Asset::new(word)` creates an Asset from a Word. +### Balances and asset construction -See [miden-bank bank-account](https://github.com/0xMiden/tutorials/blob/main/examples/miden-bank/contracts/bank-account/src/lib.rs) for complete asset handling patterns including deposit, withdrawal, and balance tracking. +There is no `active_account::get_balance`. Read the asset value word with `active_account::get_asset(asset_key)` (or `native_account::get_initial_asset(asset_key)` for the pre-transaction value) and take the fungible amount from it; test membership with `active_account::has_asset(asset_id)`. + +There is also no in-transaction asset construction: `faucet::create_fungible_asset`, `create_non_fungible_asset`, `has_callbacks` and the whole `asset` module are gone. `faucet::mint` and `faucet::burn` take an already-built `Asset`. + +## Asset Handling + +`Asset` is a two-word value: ```rust pub struct Asset { @@ -73,40 +241,66 @@ pub struct Asset { } ``` -For fungible assets, the amount lives in `asset.value[0]`. The asset class / vault identity lives in `asset.key`. +**Constructor**: `Asset::new(key, value)` builds an Asset from its two words (the arguments are `impl Into`). + +The guest field is literally named `key`, but the word it holds is the protocol's **asset ID** — the vault's unique identifier for the asset. Read `asset.key` as "the asset-ID word". + +For fungible assets the amount lives in `asset.value[0]`. Prefer the typed accessors over raw felt maths: ```rust -// Access fungible amount -let amount = asset.value[0]; +// Typed amount: panics if the asset is non-fungible or the amount is out of range +let amount: AssetAmount = asset.amount(); +let fungible: bool = asset.is_fungible(); -// Keep the asset key if you need to persist or compare the asset class -let asset_key = asset.key; +// Raw form, if you need the felt +let amount_felt = asset.value[0]; -// Add asset to account vault (only from component methods, not note scripts; see pitfall P11) -native_account::add_asset(asset); +// Keep the asset-ID word if you need to persist or compare the asset +let asset_id = asset.key; -// Remove asset from account vault -native_account::remove_asset(asset.clone()); +// Vault operations (component methods only — see pitfall P11) +self.add_asset(asset); +self.remove_asset(asset); // Asset is Copy, no clone needed ``` +`AssetAmount` is a validated newtype (`MAX_U64 = 2^63 - 2^31`) with integer ordering and add/sub that panic on over/underflow. It is usable in exported signatures and as a storage value type. + ## P2ID Output Note Creation -To send assets to another account, create a P2ID (Pay-to-ID) output note. See [miden-bank bank-account](https://github.com/0xMiden/tutorials/blob/main/examples/miden-bank/contracts/bank-account/src/lib.rs) `create_p2id_note()` for a complete working implementation. +To send assets to another account, create a P2ID output note **from an account-component method** — both `output_note::create` and `native_account::remove_asset` are account-context only, so a note or tx script cannot do this inline. + +The sequence is `note::build_recipient` → `output_note::create` → `remove_asset` + `output_note::add_asset`. `examples/basic-wallet/src/lib.rs` is the reference: `create_note` wraps `output_note::create`, and `move_asset_to_note` wraps the remove-then-add pair. + +`note::build_recipient` panics if the note storage exceeds `MAX_NOTE_STORAGE_ITEMS` (1024 felts). A note may carry at most `MAX_ASSETS_PER_NOTE` = **16** assets. ## Cross-Component Dependencies -To call another component's methods from a note or tx script, two Cargo.toml sections are needed. See [increment-note/Cargo.toml](../../../contracts/increment-note/Cargo.toml) for a working example showing both `[package.metadata.miden.dependencies]` and `[package.metadata.component.target.dependencies]`. +To call another component's methods from a note or tx script, declare the dependency in your **`miden-project.toml`** — never in `Cargo.toml`, which the macros read only for `[package] name` and `description`: + +```toml +[dependencies] +miden-core = "*" +miden-protocol = "*" +basic-wallet = { path = "../basic-wallet" } +``` -Then import the bindings in your Rust code. See [increment-note/src/lib.rs](../../../contracts/increment-note/src/lib.rs) line 13 for the import pattern: `use crate::bindings::miden::target_component::target_component;` +The `[dependencies]` entry is required. Do not add a `[package.metadata.miden.dependencies]..wit` override for normal source or `.masp` dependencies whose packages already embed WIT; current SDK macros read the embedded package WIT and reject an explicit WIT override when embedded WIT exists. For source dependencies, make sure the dependency crate has a `build.rs` that calls `miden_sdk_build_script_support::prepare_package_cache()` with a matching `miden-sdk-build-script-support` dependency so Cargo checks and IDE analysis populate `MIDENC_PACKAGE_CACHE`. + +Then expose the dependency's methods on the consuming account by declaring an `#[account(package::Interface)]` wrapper (e.g. `#[account(basic_wallet::BasicWallet)] pub struct Wallet;`) and calling methods on the injected `account` parameter. The package name is the dependency's Rust-style name (`-` replaced with `_`) and `Interface` is its exported WIT interface in UpperCamelCase. + +A component can also declare siblings it calls with `#[component(pkg::Interface)]` on its own trait. The generated traits attach through a blanket impl bound on `NativeAccount`, so only the native account can make intra-account sibling calls. + +There is a second, wrapper-free form: the generated bindings expose free functions, which `examples/counter-note/src/lib.rs` uses directly (`use crate::bindings::miden::counter_contract::counter_contract; counter_contract::get_count();`). ## Common Type Conversions ```rust // Felt from integer let f = felt!(42); // preferred for literals in contract code -let f = Felt::new(42); // construct a Felt from a u64 -let f = Felt::from_u32(42); -let f = Felt::from_canonical_checked(42).unwrap(); +let f = Felt::new(42).unwrap(); // fallible: Felt::new returns Result +let f = Felt::new_unchecked(42); // infallible, non-reducing form +let f = Felt::from_u32(42); // infallible (u32 always fits) +let f = Felt::from_canonical_checked(42).unwrap(); // returns Option // Word from Felts let w = Word::from([f0, f1, f2, f3]); @@ -123,9 +317,21 @@ let hex = w.to_hex(); let n: u64 = f.as_canonical_u64(); ``` +### Kernel scalars are typed, not felts + +Counts are `u32` (`tx::get_num_input_notes`, `tx::get_num_output_notes`, `active_account::get_num_procedures`, and the `num_assets` / `num_storage_items` fields of the note-info structs), so count-driven loops index directly. Block heights are `BlockNumber` (comparable as integers; `BlockNumber::try_from(felt)` validates a height read out of note storage). Block timestamps are `u32` seconds and expiration deltas are `u16`. Account nonces are `Nonce` — use `as_felt()` / `as_u64()` or `Felt::from(nonce)` where a raw value is needed, e.g. when packing a nonce into a `Word`. Attachment lookups return `Option`. + +## Debug Printing + +```rust +miden::println!("checkpoint"); // string literal / &str only — format args are a compile error +miden::debug::println(some_str); +miden::intrinsics::debug::breakpoint(); +``` + ## No-std Requirements -Every contract file must start with `#![no_std]` and `#![feature(alloc_error_handler)]`. See any contract in [contracts/](../../../contracts/) for the pattern. +Every contract file must start with `#![no_std]` and `#![feature(alloc_error_handler)]`. If you need heap allocation (Vec, String, etc.): ```rust @@ -133,58 +339,62 @@ extern crate alloc; use alloc::vec::Vec; ``` -## Cross-Component Note Pattern +Use `#[macro_use] extern crate alloc;` when you want the `vec!` macro available (e.g. for building note-recipient inputs). -A note script reads from `active_note::*` and forwards work to a public account-component method via generated bindings. This is the canonical pattern for any note that updates account state, because note scripts cannot call `native_account::*` directly (see `rust-sdk-pitfalls` skill, P11). +## Cross-Component Note Pattern -The `#[note]` macro generates `TryFrom<&[Felt]>` for the note struct, so the note's serialized storage is deserialized into typed fields before the script runs. The `#[note_script]` method receives the deserialized note as `self` (by value) and never indexes a raw Felt slice manually. Alongside the required `Word` arg, the method may optionally accept a `&Account` or `&mut Account` parameter. See [compiler/sdk/base-macros/src/lib.rs](https://github.com/0xMiden/compiler/blob/main/sdk/base-macros/src/lib.rs) for the macro contract and [compiler/sdk/base-macros/src/note.rs](https://github.com/0xMiden/compiler/blob/main/sdk/base-macros/src/note.rs) for the generated deserialization (each named field is read via `::from_felt_repr(...)` and EOF is asserted at the end). +A note script reads from `active_note::*` or from typed `#[note]` fields and forwards work to a public account-component method via generated bindings. This is the canonical pattern for any note that updates account state, because note scripts cannot call `native_account::*` directly (see `rust-sdk-pitfalls` skill, P11). -Supported field types include `Felt`, the unsigned integer scalars (`u64`, `u32`, `u8`), `bool`, `Option`, and `Vec` via the `FromFeltRepr` trait (`compiler/sdk/field-repr/repr/src/lib.rs`), plus any user type that opts in with `#[derive(FromFeltRepr)]` (this is how `AccountId` supports the macro - see `compiler/sdk/base-sys/src/bindings/types.rs`). Do **not** use `Asset` or `Word` directly as note struct fields; those types do not currently derive `FromFeltRepr`. If you need asset-shaped data inside the note, flatten it into supported scalar fields and reconstruct inside the script, or keep it on the side as a separate `active_note::get_assets()` read. +The `#[note]` macro generates `TryFrom<&[Felt]>` for the note struct, so the note's serialized inputs are deserialized into typed fields before the script runs. The `#[note_script]` method receives the deserialized note as `self` (by value) and never has to index a raw Felt slice manually for new typed note scripts. Alongside the required `Word` arg, the method may optionally accept a `&Account` or `&mut Account` parameter. See [compiler/sdk/base-macros/src/lib.rs](https://github.com/0xMiden/compiler/blob/main/sdk/base-macros/src/lib.rs) for the macro contract and [compiler/sdk/base-macros/src/note.rs](https://github.com/0xMiden/compiler/blob/main/sdk/base-macros/src/note.rs) for the generated deserialization. -For Cargo.toml wiring (cross-component dependencies + bindings import), see "Cross-Component Dependencies" above. See [increment-note/src/lib.rs](../../../contracts/increment-note/src/lib.rs) for the project-template's local example of the `#[note] struct + #[note] impl` macro form. +Supported field types include `Felt`, the unsigned integer scalars (`u64`, `u32`, `u8`), `bool`, `Option`, and `Vec` via the `FromFeltRepr` trait (`compiler/sdk/field-repr/repr/src/lib.rs`), plus any user type that opts in with `#[derive(FromFeltRepr)]`. Do not use `Asset` or `Word` directly as note struct fields unless source inspection confirms those types implement `FromFeltRepr` for the SDK line you are using. If you need asset-shaped data inside the note, flatten it into supported scalar fields and reconstruct inside the script, or keep assets attached to the note and read them from `active_note`. -**Storage-free case** (sender + assets, single component call per asset): declare a unit struct (`#[note] struct DepositNote;`). The script reads `active_note::get_sender()` and iterates `active_note::get_assets()`, calling the component method per asset. The macro still generates the deserialization wrapper; for a unit struct it only asserts the storage Felt slice is empty. +For dependency wiring, see "Cross-Component Dependencies" above. See [increment-note/src/lib.rs](../../../contracts/increment-note/src/lib.rs) for the project-template's local example of the `#[note] struct + #[note] impl` macro form. -**Typed-storage case** (note carries scripted data): declare named fields on the note struct. The macro deserializes them in declaration order, and the script accesses them via `self.`. Illustrative shape: +**Storage-free case** (sender + assets, single component call per asset): declare a unit struct. The script reads the sender and attached assets from `active_note`, then calls the component method per asset. The macro still generates the deserialization wrapper; for a unit struct it only asserts the input Felt slice is empty. The miden-bank [deposit-note](https://github.com/0xMiden/tutorials/blob/main/examples/miden-bank/contracts/deposit-note/src/lib.rs) shows this flow. -```rust -#[note] -struct DepositNote { - depositor: AccountId, -} +**Input-bearing case** (note carries scripted data): declare named fields on the note struct when possible. The macro deserializes them in declaration order, and the script accesses them via `self.`. The miden-bank [withdraw-request-note](https://github.com/0xMiden/tutorials/blob/main/examples/miden-bank/contracts/withdraw-request-note/src/lib.rs) is the canonical older raw-input example: it reads 11 Felts from the note inputs, reconstructs the requested asset, serial number, tag, aux, and note type, then calls `bank_account::withdraw(...)`. For new typed notes, preserve that layout but prefer typed fields and let the macro perform the deserialization. -#[note] -impl DepositNote { - #[note_script] - pub fn run(self, _arg: Word) { - let assets = active_note::get_assets(); - for asset in assets { - bank_account::deposit(self.depositor, asset); - } - } -} -``` +**Component side that absorbs the call**: see [miden-bank bank-account](https://github.com/0xMiden/tutorials/blob/main/examples/miden-bank/contracts/bank-account/src/lib.rs) for `deposit(...)` and `withdraw(...)` in a fuller example. The component method validates balances, updates storage, and, for `withdraw`, creates a P2ID output note via the existing P2ID pattern. -(`use` statements and crate attributes elided; see [increment-note/src/lib.rs](../../../contracts/increment-note/src/lib.rs) for a complete file.) For a verified working example with an `&mut Account` parameter, see [compiler/examples/p2id-note/src/lib.rs](https://github.com/0xMiden/compiler/blob/main/examples/p2id-note/src/lib.rs) (`#[note] struct P2idNote { target_account_id: AccountId }`, where the script asserts `account.get_id() == self.target_account_id` and calls `account.receive_asset(asset)` for each attached asset). +**Test wiring**: tests pass the serialized Felt representation of the note fields through `NoteCreationConfig.inputs`, in declaration order. See `rust-sdk-testing-patterns` skill, "Note Construction", for the helper that builds a note from a compiled `.masp` package and a populated `NoteCreationConfig`. -**Component side that absorbs the call**: see [miden-bank bank-account](https://github.com/0xMiden/tutorials/blob/main/examples/miden-bank/contracts/bank-account/src/lib.rs) for `deposit(...)` and `withdraw(...)` in a fuller example. The component method validates (felt-arithmetic safety, see `rust-sdk-pitfalls` P1), updates storage, and (for `withdraw`) creates a P2ID output note via the existing P2ID pattern. Note: miden-bank currently demonstrates an older raw-indexing variant for its withdraw-request note; treat the typed pattern shown above as the preferred shape for new note scripts. +## Asset Receiving via Component Methods -**Test wiring**: tests pass the serialized Felt representation of the note struct's fields through `NoteCreationConfig.storage`, in declaration order. See `rust-sdk-testing-patterns` skill, "Note Construction" section, for the helper that builds a note from a compiled `.masp` package and a populated `NoteCreationConfig`. +Note scripts cannot call `native_account::add_asset()` directly (see pitfall P11). The canonical pattern is for an account component to expose a trait method — marked `#[account_procedure]` — that wraps `add_asset`, and for the note script to call that method through the `#[account(...)]` wrapper. -## Asset Receiving via Component Methods +Component side (`examples/basic-wallet/src/lib.rs`): -Note scripts cannot call `native_account::add_asset()` directly (see pitfall P11). The canonical pattern is for an account component to expose a public method that wraps `native_account::add_asset()`, and note scripts call that method via cross-component bindings. +```rust +#[component] +trait BasicWallet { + #[account_procedure] + fn receive_asset(&mut self, asset: Asset); +} -See [miden-bank bank-account deposit()](https://github.com/0xMiden/tutorials/blob/main/examples/miden-bank/contracts/bank-account/src/lib.rs) for the component side: the `deposit()` method validates the deposit, updates storage, and calls `native_account::add_asset()`. +#[component] +impl BasicWallet for BasicWalletStorage { + fn receive_asset(&mut self, asset: Asset) { + self.add_asset(asset); + } +} +``` -See [miden-bank deposit-note](https://github.com/0xMiden/tutorials/blob/main/examples/miden-bank/contracts/deposit-note/src/lib.rs) for the note side: the note script calls `bank_account::deposit()` via generated bindings. +Note side (`examples/p2id-note/src/lib.rs`): the note declares `#[account(basic_wallet::BasicWallet)] pub struct Wallet;` and, inside `#[note_script] fn script(self, _arg: Word, account: &mut Wallet)`, calls `account.receive_asset(asset)` on that wrapper. It is **not** a free `basic_wallet::receive_asset()` call. ## Validation Checklist - [ ] `#![no_std]` and `#![feature(alloc_error_handler)]` at top of every contract -- [ ] `crate-type = ["cdylib"]` in Cargo.toml -- [ ] Correct `project-kind` in `[package.metadata.miden]` -- [ ] Typed storage uses `StorageValue` / `StorageMap` with `get()` / `set()` -- [ ] Cross-component deps in both `[package.metadata.miden.dependencies]` and `[package.metadata.component.target.dependencies]` +- [ ] Account components use the three-part pattern: `#[component_storage]` struct + `#[component]` trait + `#[component]` impl (never `#[component]` on a struct) +- [ ] Every externally-callable trait method carries `#[account_procedure]`, on the **trait**, not the impl +- [ ] `#[account_procedure]` and `#[auth_script]` are not combined in one component +- [ ] The `#[account(...)]` wrapper struct name differs from every generated trait name +- [ ] Contract `Cargo.toml` matches the local template shape: `edition = "2021"`, `crate-type = ["cdylib"]`, `miden = { version = "0.14" }`, and matching `miden-sdk-build-script-support = { version = "0.14" }` +- [ ] `[lib]` in `miden-project.toml` has `kind` (`account-component` / `note` / `tx-script`), `namespace`, **and `path`** +- [ ] `[dependencies]` in `miden-project.toml` carries `miden-core = "*"` and `miden-protocol = "*"` +- [ ] Typed storage uses `StorageValue` / `StorageMap` with `get()` / `set()`; slot names derive from `::::` +- [ ] Notes/tx-scripts that call a component declare an `#[account(package::Interface)]` wrapper and call methods on the injected `account` +- [ ] Cross-component deps declared in `miden-project.toml` (never `Cargo.toml`) under `[dependencies]`; rely on embedded WIT and the package cache unless source inspection proves an override is required +- [ ] `incr_nonce()` is called only from an authentication procedure; `output_note::create` and the vault operations only from account-component context - [ ] Felt arithmetic validated before subtraction (see rust-sdk-pitfalls skill) - [ ] Felt comparisons use `.as_canonical_u64()` (see rust-sdk-pitfalls skill) diff --git a/.claude/skills/rust-sdk-pitfalls/SKILL.md b/.claude/skills/rust-sdk-pitfalls/SKILL.md index 4d044ef..77cd9b4 100644 --- a/.claude/skills/rust-sdk-pitfalls/SKILL.md +++ b/.claude/skills/rust-sdk-pitfalls/SKILL.md @@ -5,6 +5,12 @@ description: Critical pitfalls and safety rules for Miden Rust SDK development. # Miden SDK Pitfalls +Verified against project-template's current v16 line: contract SDK `miden` and +`miden-sdk-build-script-support` `0.14` (published `0.14.0`) on `nightly-2026-09-01`, +`cargo-miden` `0.10.0` through midenup / cargo-miden, `Cargo.lock` resolving +protocol/standards/testing `0.16.0-rc.6`, `miden-client` `0.16.0-rc.2`, and VM/package crates +`0.29.4`. + ## P1: Felt Arithmetic is Modular (SECURITY CRITICAL) **Severity**: Critical — can cause loss of funds @@ -28,6 +34,14 @@ let new_balance = current_balance - withdraw_amount; **Max Felt value**: The maximum valid Felt is `p - 1 = 18446744069414584320`, not `u64::MAX` (`18446744073709551615`). Using `u64::MAX` as a sentinel or boundary value causes silent wraparound. +**Prefer `AssetAmount` for token quantities.** `miden::AssetAmount` is a validated newtype over +`Felt` (`AssetAmount::MAX_U64 = (1 << 63) - (1 << 31)`) whose `Add` / `Sub` impls **panic** on +overflow / underflow rather than wrapping, and whose ordering is canonical-integer ordering. +Constructors and accessors: `AssetAmount::new(u64) -> Result`, +`AssetAmount::max()`, `AssetAmount::ZERO`, `as_u64()`, `as_felt()`. It is a valid `WordKey` and +`WordValue`, so it can be stored directly in `StorageValue` / `StorageMap` and used in exported +signatures. + ## P2: Felt Comparison Operators Are Misleading for Quantity Logic **Severity**: High — silently produces incorrect results @@ -44,42 +58,199 @@ if balance.as_canonical_u64() > threshold.as_canonical_u64() { ... } **Rule**: For quantity/business logic, ALWAYS convert to `.as_canonical_u64()` before using comparison operators. -## P3: Function Argument Limit (4 Words / 16 Felts) +**Exception — typed scalars already compare as integers.** `BlockNumber`, `Nonce` and +`AssetAmount` derive integer ordering, so no conversion is needed. The `p2ide-note` example relies +on this directly: + +```rust +let block_number = tx::get_block_number(); // BlockNumber +let timelock_height = BlockNumber::try_from(inputs[3]).unwrap(); +assert!(block_number >= timelock_height); // integer comparison, correct as written +``` + +## P3: The 16-Felt Cross-Context Boundary — Two Limits, and Exports Differ From FPI Imports + +**Severity**: High — the wrong mental model makes you refactor a signature that would have compiled, +and miss the one that will not + +A cross-context call passes its parameters on the MASM operand stack, whose addressable window is 16 +felts (4 Words, counting the canonical-ABI result pointer when present). Two *different* limits +govern that boundary, and conflating them is the actual pitfall: + +- **`MAX_FLAT_PARAMS = 16`** — a **count** of canonical-ABI flat values. +- **`MAX_DIRECT_STACK_FELTS = 16`** — a **felt budget**, measured after `u64` values expand to two + felts each and any result pointer is added. -**Severity**: Medium — causes compilation errors +Both live in `compiler:sdk/v0.14.0:frontend/wasm/src/component/types/mod.rs:44-62`, whose own +doc comment spells out the distinction: the felt budget "is a Miden VM constraint, distinct from the +spec's count-based `MAX_FLAT_PARAMS`: a signature can stay within 16 flat values while 64-bit values +expand it past 16 stack felts." -Functions can receive at most 4 Words (16 Felts) as arguments. +Exceeding **either** limit makes canonical-ABI flattening replace the whole parameter list with a +single pointer to a tuple in linear memory — `flat_params_need_tuple` is an **OR** +(`compiler:sdk/v0.14.0:frontend/wasm/src/component/flat.rs:212-217,263-270`): ```rust -// PROBLEM — too many arguments -fn process(a: Word, b: Word, c: Word, d: Word, e: Word) { ... } // > 4 Words! +flat_params.len() > MAX_FLAT_PARAMS + || flat_params.iter().map(|param| param.ty.size_in_felts()).sum::() + > MAX_DIRECT_STACK_FELTS +``` + +What happens to that pointer is where the two sides of the boundary part ways. + +### FPI imports: the count decides the path, the felt budget can still reject it -// SOLUTION — pass fat types by reference -fn process(a: &Word, b: &Word, c: &Word, d: &Word, e: &Word) { ... } +The critical subtlety: `plan_fpi_call` does **not** reuse the OR above. It re-derives the call shape +from the flat-value **count alone** +(`compiler:sdk/v0.14.0:frontend/wasm/src/component/lower_imports.rs:330-351`): + +```rust +let has_arg_ptr = flattened_params.len() > MAX_FLAT_PARAMS; ``` -## P4: Storage API Is Typed +So the two disagree in exactly one band — **count ≤ 16 but felts > 16**. Flattening *has* tupled +such a signature, but `plan_fpi_call` still believes it is a direct call, so it takes the +`!has_arg_ptr` path, sums the operand felts, and rejects: -**Severity**: Medium — old examples no longer compile +``` +FPI import `{path}` lowers to {n} operand stack felts after expanding 64-bit +values and result pointers, but direct FPI calls support at most 16 +``` -The old `Value` / untyped `StorageMap` API is gone. Account storage is now: +The source says this check is deliberately ordered first, because "over-budget direct shapes are +tupled by canonical ABI flattening, which would otherwise surface as a confusing shape mismatch." + +```rust +// REJECTED — 13 flat values, so the count-based `has_arg_ptr` is false, but six +// `u64`s expand to two felts each: 6 prefix felts + 12 + 1 + 1 result pointer +// = 20 operand felts. +struct SixU64Record { a: u64, b: u64, c: u64, d: u64, e: u64, f: u64, tag: Felt } +fn echo_six_u64_record(&self, input: SixU64Record) -> SixU64Record; +``` + +That is the compiler's own negative test, +`compiler:sdk/v0.14.0:tests/integration-network/src/mockchain/fpi/note/six_u64_struct.rs:5-13` +(`#[should_panic(expected = "direct FPI calls support at most 16")]`). + +**Above 16 flat values the indirect path is supported** — `has_arg_ptr` is true, the felt-budget +check is skipped entirely, and the wrapper reloads the tuple so the backend still sees a direct, +felt-only call +(`compiler:sdk/v0.14.0:frontend/wasm/src/component/lower_imports.rs:439-508`). A +22-flat-parameter FPI import is a **passing** test +(`compiler:sdk/v0.14.0:tests/integration-network/src/mockchain/fpi/note/sixteen_flattened_params_struct.rs`). + +**Indirect is not unbounded, though.** The FPI executor imposes its own caps, checked after the +shape is settled (`compiler:sdk/v0.14.0:frontend/wasm/src/component/lower_imports.rs:381-399`), +with values from `ExecFpi` (`compiler:sdk/v0.14.0:dialects/hir/src/ops/invoke.rs:160-169`): + +| bound | value | diagnostic | +| --- | --- | --- | +| `PREFIX_FELTS` — account id + procedure root, subtracted before the input check | 6 | `must pass account id and procedure root` | +| `MAX_INPUT_FELTS` — flattened *procedure input* felts, after the prefix | 16 | ``passes {n} flattened procedure input felts, but `execute_foreign_procedure` supports at most 16`` | +| `EXECUTOR_RESULT_FELTS` — result felts | 16 | ``returns {n} result felts, but `execute_foreign_procedure` supports at most 16`` | + +So the tuple pointer buys you past the *stack window*, not past the *protocol*: the payload the +foreign procedure actually receives is still capped at 16 felts. + +**Do not "fix" the felt-budget rejection by padding the signature until the count exceeds 16** just +to trigger the indirect path. It does flip `has_arg_ptr` and skip the stack-window check, but the +executor's 16-felt input cap then rejects the same payload — you have only moved which diagnostic +fires. Reduce what crosses the boundary instead: split the call, or hand over an identifier (a +storage key, a note index, a commitment) and let the callee load the rest itself. + +### Component exports: indirect parameters are not implemented yet + +On the export side the tuple pointer is produced the same way but then refused, so **either** an +over-16 flat-value count **or** an over-16 felt budget fails +(`compiler:sdk/v0.14.0:frontend/wasm/src/component/lift_exports.rs:68-74`): + +``` +component export lifting for '{path}' is not yet implemented for passing the +parameters using the advice provider in the cross-context `call`; +``` + +```rust +// REJECTED as a component export — flattens to 20 felt params. +fn process(a: Word, b: Word, c: Word, d: Word, e: Word) { ... } + +// STILL REJECTED — a wrapper struct compresses nothing. Flattening recurses +// into struct fields and concatenates them, so a WordBatch holding those same +// five Words is still 20 felts. +fn process(batch: WordBatch) { ... } + +// OK — reduce what actually crosses the boundary. +fn process(batch_commitment: Word) { ... } +``` + +Export **return** values are capped separately, at 16 loaded *values* (a count, with no felt-budget +check at all — a record of nine `u64` fields is 9 values but 18 felts and is not caught): +`compiler:sdk/v0.14.0:frontend/wasm/src/component/lift_exports.rs:281-286`. + +### Unrelated, but adjacent + +`&T` parameters are refused before any of this, by the `#[component]` macro rather than the +compiler frontend: `references are not supported in component interfaces or exported types` +(`compiler:sdk/v0.14.0:sdk/base-macros/src/types.rs:102-106`). It applies to exported method +parameters, return types, and exported struct/enum fields alike — `&self` receivers are fine. + +## P4: Storage API Is Typed, and a Component Is Three Parts + +**Severity**: Medium — the wrong component shape does not compile + +Account storage uses typed slots: - `StorageValue` for a single typed slot - `StorageMap` for typed maps - `get()` / `set()` methods instead of `.read()` / `.write()` - `K: WordKey`, `T: WordValue`, `V: WordValue` +`WordValue` is implemented for `Word`, `Felt`, `AssetAmount`, `Digest`, `AccountId`, `Recipient`, +`Tag`, `NoteIdx`, `NoteType`. `WordKey` is implemented for `Word`, `Felt`, `AssetAmount`, +`AccountId`, `Tag`, `NoteIdx`, `NoteType`. + +An account component is written in **three parts**: annotate the storage struct with `#[component_storage]`, the API `trait` with `#[component]`, and the `impl Trait for Storage` block with `#[component]`. Every trait method that must be callable from outside the component also needs `#[account_procedure]` (see P13): + ```rust +// 1. Storage struct — annotated #[component_storage], NOT #[component]. +// Applying #[component] to a struct is a hard compile error. +#[component_storage] +struct CounterContractStorage { + #[storage(description = "counter contract storage map")] + count_map: StorageMap, +} + +// 2. API trait — defines the exported interface. #[component] -struct CounterContract { - #[storage(description = "single typed slot")] - counter: StorageValue, +trait CounterContract { + #[account_procedure] + fn get_count(&self) -> Felt; + #[account_procedure] + fn increment_count(&mut self) -> Felt; +} - #[storage(description = "typed map")] - balances: StorageMap, +// 3. Implementation — the behavior, wired to the storage struct. +// #[account_procedure] goes on the trait declaration only, never here. +#[component] +impl CounterContract for CounterContractStorage { + fn get_count(&self) -> Felt { + let key = Word::new([felt!(0), felt!(0), felt!(0), felt!(1)]); + self.count_map.get(key) + } + + fn increment_count(&mut self) -> Felt { + let key = Word::new([felt!(0), felt!(0), felt!(0), felt!(1)]); + let current_value: Felt = self.count_map.get(key); + let new_value = current_value + felt!(1); + self.count_map.set(key, new_value); + new_value + } } ``` +`#[component_storage]` accepts only unit structs and structs with named fields — a tuple struct +fails with `` `#[component_storage]` only supports unit structs or structs with named fields. `` +Generic storage structs are rejected. + If you need custom keys or values, implement `WordKey` / `WordValue` by converting to and from a single `Word`. ## P5: Storage Slot Naming Convention @@ -88,15 +259,38 @@ If you need custom keys or values, implement `WordKey` / `WordValue` by converti Storage slot names follow a strict pattern. Getting it wrong often returns the default value silently. -**Pattern**: `[component_package_or_name]::[snake_case(component_struct)]::[field_name]` +**Pattern**: `[package_name]::[namespace_interface_segment]::[field_name]` + +**Where the segments come from**: The `#[component_storage]` macro (NOT `#[component]`) processes the `#[storage]` fields and derives slot names. It loads `miden-project.toml` (next to your `Cargo.toml`, NOT `Cargo.toml` itself): + +- **First segment** = `[package] name` from `miden-project.toml`. +- **Middle segment** = the *interface segment* of the `[lib] namespace` value. The namespace is a fully-qualified component id `namespace:package/interface@version`; the interface segment sits between the last `/` and the `@`. This is deliberately decoupled from the Rust storage-struct name, so renaming the private struct cannot change deployed slot names. The struct name (`CounterContractStorage`, `AuthComponentStorage`, …) does NOT appear in the slot name. +- **Last segment** = the `#[storage]` field name. + +**Conversion rule**: All three segments are character-sanitized — any `@version` suffix is stripped, characters outside `[A-Za-z0-9_]` are replaced with `_`, and an empty or leading-`_` segment is prefixed with `x`. Only the **middle** segment additionally goes through `to_snake_case()`; the package name is *not* snake-cased, it is only character-sanitized (which is why the conventional kebab-case `counter-contract` still lands as `counter_contract`). + +| `[package] name` | `[lib] namespace` | Field | Storage Slot Name | +|------------------|-------------------|-------|-------------------| +| `counter-contract` | `miden:counter-contract/counter-contract@0.1.0` | `count_map` | `counter_contract::counter_contract::count_map` | +| `auth-component-rpo-falcon512` | `miden:auth-component-rpo-falcon512/auth-component@0.1.0` | `owner_public_key` | `auth_component_rpo_falcon512::auth_component::owner_public_key` | +| `storage-example` | `miden:storage-example/foo@1.0.0` | `asset_qty_map` | `storage_example::foo::asset_qty_map` | -**Conversion rule**: Replace characters outside `[A-Za-z0-9_]` with `_` in the package or component name. The package comes from `[package.metadata.component] package = "..."`, with any `@version` suffix ignored. +The first two rows are the exact strings the compiler's own MockChain tests assert against, so use +them as the ground truth for the algorithm. -| Package in Cargo.toml | Component Struct | Field | Storage Slot Name | -|----------------------|------------------|-------|-------------------| -| `miden:counter-account` | `CounterContract` | `count_map` | `miden_counter_account::counter_contract::count_map` | -| `miden:bank-account` | `BankAccount` | `balances` | `miden_bank_account::bank_account::balances` | -| `miden:bank-account` | `BankAccount` | `initialized` | `miden_bank_account::bank_account::initialized` | +Omitting the manifest is an error, not a fallback: a `#[component_storage]` struct with `#[storage]` +fields and no `miden-project.toml` fails with `` `#[component_storage]` with `#[storage]` fields +requires a `miden-project.toml` next to the crate's `Cargo.toml`: storage slot names derive from the +`[lib].namespace` interface segment. `` + +**Caveat (toolchain-version dependent)**: this naming is a property of the Rust SDK contract macros +in the `miden-base-macros` crate, which ships at published `0.14.0` alongside `miden`, `miden-base`, +`miden-base-sys`, `miden-stdlib-sys` and `miden-sdk-alloc`. The separate compiler / `midenc` / +`cargo-miden` workspace is `0.10.0`. Neither is the protocol/network version: protocol, +`miden-standards` and `miden-testing` resolve to `0.16.0-rc.6`, `miden-client` resolves to +`0.16.0-rc.2`, and the VM/package crates resolve to `0.29.4`. See P20 for the full version matrix. +Verify slot names against your installed toolchain rather than assuming a protocol version implies a +macro behavior. ## P6: No-std Environment @@ -104,7 +298,18 @@ Storage slot names follow a strict pattern. Getting it wrong often returns the d All contract code must be `#![no_std]`. Forgetting this or using std types causes build failures. -**Required at the top of every contract file:** See any contract in [contracts/](../../../contracts/) for the correct pattern (`#![no_std]` + `#![feature(alloc_error_handler)]`). +**Required at the top of every contract file:** + +```rust +#![no_std] +#![feature(alloc_error_handler)] +``` + +Both lines appear before any code in the SDK examples — see +`compiler:sdk/v0.14.0:examples/counter-contract/src/lib.rs`, +`compiler:sdk/v0.14.0:examples/basic-wallet/src/lib.rs` and +`compiler:sdk/v0.14.0:examples/p2id-note/src/lib.rs`. Most of them lead with an explanatory +`// Do not link against libstd ...` comment first, so match the two attributes, not the first line. **For heap allocation (Vec, String, Box):** ```rust @@ -112,11 +317,16 @@ extern crate alloc; use alloc::vec::Vec; ``` -## P7: Asset ABI Is Two Words, Not One +**Toolchain**: current project-template contract crates use `nightly-2026-09-01` with target +`wasm32-wasip2`; the published SDK and `cargo-miden` line require Rust 1.99. Local contract +`Cargo.toml` files use `edition = "2021"`, `crate-type = ["cdylib"]`, `miden = { version = "0.14" }`, +and matching `miden-sdk-build-script-support = { version = "0.14" }`. -**Severity**: Medium — old `asset.inner[...]` code is stale +## P7: Rust SDK `Asset` Is Two Words (ID + Value) -`Asset` is now: +**Severity**: Medium — reconstructing an asset from raw `asset.inner[...]` offsets is wrong + +In the Rust SDK (`miden::Asset` / `miden_base_sys::bindings::Asset`), an `Asset` is encoded as two words: ```rust pub struct Asset { @@ -125,21 +335,34 @@ pub struct Asset { } ``` +The field is literally named `key`, but the word it holds is the **asset-ID word** at the protocol +layer (see P17). Construct with `Asset::new(key: impl Into, value: impl Into)`. + ```rust -// Reading the amount from a fungible asset -let amount = asset.value[0]; +// Preferred accessors — validated, and integer-ordered +let amount: AssetAmount = asset.amount(); // panics if non-fungible or out of range +let fungible: bool = asset.is_fungible(); -// Persisting or comparing the asset class -let asset_key = asset.key; +// Raw access when you need the words themselves +let raw_amount: Felt = asset.value[0]; // fungible amount lives here +let asset_id_word: Word = asset.key; // persist or compare the asset class ``` -Do not assume the old single-word asset layout. Use `asset.key` and `asset.value`, or protocol helpers, instead of reconstructing from old `asset.inner[...]` offsets. +Use `asset.key` / `asset.value` (or the accessors above) rather than reconstructing an asset from raw `asset.inner[...]` offsets. -## P8: `Recipient::compute` Was Removed +**SDK vs protocol `Asset`**: the two-word `{key, value}` form is the Rust SDK ABI type. At the +protocol layer, `Asset` is an enum `{ Fungible(FungibleAsset), NonFungible(NonFungibleAsset) }` and +the vault words come from `Asset::to_id_word()` and `Asset::to_value_word()`. There is no +`to_key_word()` — that name does not exist anywhere in the protocol source. Related protocol +accessors: `Asset::id() -> AssetId`, `Asset::from_id_and_value(AssetId, Word)`, +`Asset::from_id_and_value_words(Word, Word)`, `Asset::as_elements() -> [Felt; 8]`. +`FungibleAsset::amount()` returns `AssetAmount`, not `u64`. -**Severity**: Medium — causes compilation errors after upgrading +## P8: Build Recipients with `note::build_recipient` (no `Recipient::compute`) -Building recipients now goes through the note binding: +**Severity**: Medium — calling a nonexistent `Recipient::compute` fails to compile + +Build recipients through the note binding: ```rust extern crate alloc; @@ -152,58 +375,418 @@ let recipient = note::build_recipient( ); ``` -## P9: P2ID Note Root Hardcoding +`note::build_recipient(serial_num: Word, script_root: Word, storage: Vec) -> Recipient` is the +Rust SDK alias for the host function `miden::protocol::note::compute_and_store_recipient`, which +computes and stores the recipient in one step. You can call either name. + +**Storage cap**: note storage is limited to 1024 felts (`MAX_NOTE_STORAGE_ITEMS`). Both +`build_recipient` / `compute_and_store_recipient` and `note::compute_storage_commitment` assert on +it and panic with `note storage cannot contain more than 1024 items`. + +## P9: P2ID Note Root — Prefer `script_root()`, Do Not Hardcode **Severity**: Low-Medium — breaks after miden-standards updates -Creating P2ID output notes requires the MAST root digest of the P2ID script. This is typically hardcoded as a constant. +Creating P2ID output notes requires the MAST root of the P2ID script. The root changes whenever the P2ID script or the assembler/hashing changes, so a hardcoded literal is fragile and unverifiable. -For any note that is being created within the compiler code, the MAST root digest is needed. Below you find the example of a P2ID note +**Source of truth**: Use the associated function `P2idNote::script_root() -> NoteScriptRoot` from +`miden-standards` (`NoteScriptRoot` is a `Word` newtype). The same associated function exists on +`P2ideNote`, `SwapNote`, `PswapNote`, `MintNote` and `BurnNote`. From the client, both types are +re-exported as `miden_client::note::P2idNote` and `miden_client::note::NoteScriptRoot`. Derive the +root from the dependency rather than embedding a literal, and re-derive after any dependency bump. ```rust -fn p2id_note_root() -> Digest { - Digest::from_word( - Word::try_from([ - 13362761878458161062_u64, - 15090726097241769395_u64, - 444910447169617901_u64, - 3558201871398422326_u64, - ]) - .unwrap(), - ) -} +use miden_standards::note::P2idNote; + +// script_root() returns a NoteScriptRoot (a Word newtype); convert to Word when needed. +let p2id_root: Word = P2idNote::script_root().into(); ``` -**Risk**: If miden-standards updates the P2ID script, this digest becomes invalid and withdrawals silently fail. +**If you must embed a constant** (e.g., inside compiler/contract code that cannot call into miden-standards), regenerate it from the current `miden-standards` version and verify it after every update. The four-limb literal below is ILLUSTRATIVE only — it will not match your build and must not be copied as-is: + +```rust +// ILLUSTRATIVE ONLY — will not match your build. Regenerate from +// P2idNote::script_root() for your pinned miden-standards version. +fn p2id_note_root() -> Word { + Word::try_from([ + 13362761878458161062_u64, + 15090726097241769395_u64, + 444910447169617901_u64, + 3558201871398422326_u64, + ]) + .unwrap() +} +``` -**Mitigation**: Use `P2idNote::script_root()` from miden-standards if available, or verify the hardcoded root matches the current version after dependency updates. +**Risk**: If miden-standards updates the P2ID script, any hardcoded digest becomes invalid and withdrawals silently fail. -**NoteType for P2ID**: P2ID output notes created in contract code should use the private note type value via `NoteType::from(felt!(2))` (see P10). Using the public note type triggers an opaque "missing details in advice provider" error at execution time. See [miden-bank withdraw](https://github.com/0xMiden/tutorials/blob/main/examples/miden-bank/contracts/bank-account/src/lib.rs) for the working pattern. +**NoteType for P2ID**: P2ID output notes created in contract code are constructed with `NoteType::from(felt!(...))` — `felt!(0)` for private, `felt!(1)` for public (see P10). The kernel rejects any other note type with `ERR_NOTE_INVALID_TYPE` ("invalid note type"). ## P10: NoteType Variants Unavailable in Compiler SDK -**Severity**: Medium -- causes compilation errors +**Severity**: Critical -- wrong values panic at runtime, named variants cause compilation errors -Named enum variants (`NoteType::Private`, `NoteType::Public`, `NoteType::Encrypted`) don't exist in contract code. Construct via `NoteType::from()`: +Named enum variants (`NoteType::Private`, `NoteType::Public`) don't exist in contract code — the guest-side SDK `NoteType` is an unvalidated `#[repr(transparent)]` wrapper around `Felt` with `From`, `From for Word` and `TryFrom`, and no named variants. Construct via `NoteType::from()`: | NoteType | Value | |----------|-------| +| Private (default) | `NoteType::from(felt!(0))` | | Public | `NoteType::from(felt!(1))` | -| Private | `NoteType::from(felt!(2))` | -| Encrypted | `NoteType::from(felt!(3))` | -See [miden-bank bank-account](https://github.com/0xMiden/tutorials/blob/main/examples/miden-bank/contracts/bank-account/src/lib.rs) for `NoteType::from(note_type)` usage. +**Note-type encoding**: the note type is 1-bit — `Private = 0` (the protocol default) and `Public = 1`. Only these two values exist; there is no `Encrypted` type. The SDK wrapper does no validation, so an out-of-range value (e.g. `felt!(2)` or `felt!(3)`) is not caught at compile time — the kernel rejects it at execution time with `ERR_NOTE_INVALID_TYPE` (the output-note builder does `u32assert.err=ERR_NOTE_INVALID_TYPE u32lte.NOTE_TYPE_PUBLIC`). + +For a working conversion site, see `compiler:sdk/v0.14.0:examples/basic-wallet-tx-script/src/lib.rs`, +which turns a raw input felt into a note type with `note_type.into()` before calling the wallet's +`create_note`. ## P11: Note Scripts Cannot Call Native Account Functions **Severity**: High -- causes runtime failures -Note scripts cannot call `native_account::add_asset()` or other `native_account::` functions directly. The kernel's `authenticate_account_origin` check rejects these calls from a note context. Instead, note scripts must call an account component method, which then calls `native_account::add_asset()` internally. +Note scripts cannot call `native_account::add_asset()` or other `native_account::` functions +directly. The kernel's `authenticate_account_origin` check rejects these calls from a note context +(`pub proc account_add_asset` runs `exec.memory::assert_native_account` then +`exec.authenticate_account_origin`; `account_remove_asset` does the same). Instead, note scripts +must call an account component method, which then calls `native_account::add_asset()` internally. + +The pattern, split across two pinned examples: + +```rust +// Note side — compiler:sdk/v0.14.0:examples/p2id-note/src/lib.rs +#[account(basic_wallet::BasicWallet)] +pub struct Wallet; + +#[note] +impl P2idNote { + #[note_script] + pub fn script(self, _arg: Word, account: &mut Wallet) { + for asset in active_note::get_initial_assets() { + account.receive_asset(asset); // component method, not a free function + } + } +} + +// Component side — compiler:sdk/v0.14.0:examples/basic-wallet/src/lib.rs +#[component] +trait BasicWallet { + #[account_procedure] + fn receive_asset(&mut self, asset: Asset); +} + +#[component] +impl BasicWallet for BasicWalletStorage { + fn receive_asset(&mut self, asset: Asset) { + self.add_asset(asset); // NativeAccount trait method, the idiomatic form + } +} +``` -See [miden-bank deposit-note](https://github.com/0xMiden/tutorials/blob/main/examples/miden-bank/contracts/deposit-note/src/lib.rs) for the correct pattern: the note script calls `bank_account::deposit()`, which internally calls `native_account::add_asset()`. +`self.add_asset(asset)` / `self.remove_asset(asset)` are `NativeAccount` trait methods +auto-implemented on the `#[component_storage]` struct; the free functions +`native_account::add_asset(asset)` / `native_account::remove_asset(asset)` are equivalent. + +The alternative to an `#[account(..)]` wrapper is the generated-bindings free-function form, used by +`compiler:sdk/v0.14.0:examples/counter-note/src/lib.rs`: + +```rust +use crate::bindings::miden::counter_contract::counter_contract; +counter_contract::increment_count(); +``` ## P12: Note Inputs Are Immutable After Creation **Severity**: Low -- causes incorrect architecture -The Felt slice that the `#[note]` macro deserializes into `self` is baked at note creation time and cannot be modified after creation. Design the typed note struct's field set and field order carefully before deployment; any later change is a breaking change for existing notes. +Note inputs (`active_note::get_storage()`) are baked at note creation time and cannot be modified after creation. Design note input layouts carefully before deployment. + +A `#[note]` struct **with fields** is auto-decoded from that storage: the macro generates a +`TryFrom<&[Felt]>` that decodes each field via `FromFeltRepr` and then calls `ensure_eof()`, so +extra trailing felts are a decode failure (`FeltReprError::TrailingData`), not ignored padding. A +zero-sized `#[note]` struct skips `get_storage()` entirely. Manual slicing is still available — +`compiler:sdk/v0.14.0:examples/p2ide-note/src/lib.rs` reads `active_note::get_storage()` +directly and asserts `inputs.len() == 4` — but the typed form in +`compiler:sdk/v0.14.0:examples/p2id-note/src/lib.rs` (`#[note] struct P2idNote { +target_account_id: AccountId }`) is the shape to prefer. + +## P13: Externally-Callable Methods Must Be Marked `#[account_procedure]` + +**Severity**: Critical — omitting it compiles clean and fails only when something calls the method + +A `#[component]` trait method is part of the account's **interface** only if it carries +`#[account_procedure]`. Without it the method still compiles and is still exported to WIT, but it is +not an account procedure, so any note script, transaction script, FPI call or sibling-component call +that targets it fails. There is no compile-time warning. + +```rust +#[component] +trait Bank { + #[account_procedure] + fn initialize(&mut self); + #[account_procedure] + fn deposit(&mut self, depositor: AccountId, deposit_asset: Asset); +} +``` + +Rules: + +- Placement is on the **trait declaration**, never on the `impl` block. The `impl` methods stay bare. +- No import is needed — the enclosing `#[component]` macro recognises the attribute. Applying it + outside a `#[component]` trait errors with `` `#[account_procedure]` must be applied to a method + inside a `#[component]` `trait` ``, and it takes no arguments. +- `#[account_procedure]` and `#[auth_script]` are **mutually exclusive within one component**: + `a component cannot combine #[auth_script] and #[account_procedure]`. +- Inherent (`impl BankStorage`) methods are not exported at all, and "exported to WIT" is not the + same thing as "is an account procedure". +- The `cargo miden new` scaffolding under `compiler:sdk/v0.14.0:extra/templates/` omits + `#[account_procedure]`, so freshly-generated code is wrong out of the box. Use + `compiler:sdk/v0.14.0:examples/counter-contract/src/lib.rs` and + `compiler:sdk/v0.14.0:examples/basic-wallet/src/lib.rs` as the reference instead. + +**MASM equivalent**: a hand-written or standards MASM component marks its exports with the +`@account_procedure` / `@auth_script` attributes — "a procedure is part of the component interface +if it has either the `@account_procedure` or `@auth_script` attributes". See +`protocol:v0.16.0-rc.6:crates/miden-standards/asm/standards/wallets/basic.masm`. + +## P14: Some Kernel Calls Are Legal Only in a Specific Runtime Context + +**Severity**: Critical — these compile anywhere and panic at execution time + +Three restrictions enforced by the kernel, not the type system: + +| Call | Allowed only from | Kernel enforcement | +|---|---|---| +| `output_note::create(tag, note_type, recipient)` | an account-component procedure | `exec.authenticate_account_origin` + `exec.memory::assert_native_account` | +| `native_account::{add_asset, remove_asset}` | an account-component procedure | `exec.memory::assert_native_account` + `exec.authenticate_account_origin` | +| `native_account::incr_nonce()` / `self.incr_nonce()` | the account's `#[auth_script]` authentication procedure | `exec.memory::assert_native_account` + `exec.assert_auth_procedure_origin` | + +All three are in `protocol:v0.16.0-rc.6:crates/miden-protocol/asm/kernels/transaction/lib/api.masm` +(`pub proc output_note_create`, `pub proc account_add_asset`, `pub proc account_incr_nonce`). + +Consequences: + +- A transaction script or note script that calls `output_note::create` directly compiles and then + fails at execution. Route it through an account-component method — `basic-wallet` exposes + `#[account_procedure] fn create_note(&mut self, tag: Tag, note_type: NoteType, recipient: + Recipient) -> NoteIdx` for exactly this reason. +- Calling `incr_nonce()` from an ordinary component method panics. Only the authentication + component's single `#[auth_script]` method may do it — see + `compiler:sdk/v0.14.0:examples/auth-component-no-auth/src/lib.rs`. + +**Auth components**: exactly one `#[auth_script]` method per `#[component]` trait, and a crate whose +`miden-project.toml` sets `[package.metadata.miden] project-kind = "authentication-component"` must +have exactly one (`authentication components require exactly one #[auth_script] method`); +`#[auth_script]` on a non-account-component target is rejected outright. + +## P15: Bindings That No Longer Exist + +**Severity**: High — a contract carried forward from an earlier SDK will not compile, or will compile against the wrong name + +| Gone | Use instead | +|---|---| +| `active_note::get_assets()` | `active_note::get_initial_assets() -> Vec` | +| `input_note::get_assets(idx)` | `input_note::get_initial_assets(idx)` | +| `input_note::get_assets_info(idx)` | `input_note::get_initial_assets_info(idx)` | +| `active_account::get_balance` / `get_initial_balance` | `active_account::get_asset(asset_key: Word) -> Word` (or `native_account::get_initial_asset(asset_key: Word) -> Word`), then read the amount out of the value word | +| `active_account::has_non_fungible_asset(asset)` | `active_account::has_asset(asset_id: Word) -> bool` | +| `faucet::create_fungible_asset` / `create_non_fungible_asset` / `has_callbacks`, and the whole `asset` module | build the `Asset` outside the transaction; only `faucet::mint(Asset)` and `faucet::burn(Asset)` remain | +| `AttachmentLocation` | `Option` from `find_attachment` | +| `output_note::set_attachment` | append with `output_note::add_word_attachment`, `output_note::add_attachment`, or `output_note::add_attachment_from_memory` | + +The output-note attachment APIs append attachment entries; they do not replace an existing attachment in place. + +The current `active_account` surface is `get_id() -> AccountId`, `get_nonce() -> Nonce`, +`compute_commitment() -> Word`, `get_code_commitment() -> Word`, `compute_storage_commitment() -> +Word`, `get_asset(Word) -> Word`, `has_asset(Word) -> bool`, `get_vault_root() -> Word`, +`get_num_procedures() -> u32`, `get_procedure_root(u32) -> Word`, `has_procedure(Word) -> bool` — +all also available on the `ActiveAccount` trait. + +Initial-state getters live on `native_account` as free functions: `get_initial_commitment()`, +`get_initial_storage_commitment()`, `get_initial_vault_root()`, `get_initial_asset(Word) -> Word`, +plus `compute_delta_commitment()` and `was_procedure_called(Word) -> bool`. + +## P16: Kernel Scalars Are Typed, Not `Felt` + +**Severity**: Medium — arithmetic and `Word` packing that assumed `Felt` no longer type-checks + +| Binding | Return type | +|---|---| +| `tx::get_block_number()` | `BlockNumber` | +| `tx::get_block_timestamp()` | `u32` (seconds) | +| `tx::get_num_input_notes()` / `get_num_output_notes()` | `u32` | +| `tx::get_expiration_block_delta()` | `u16` (and `update_expiration_block_delta(delta: u16)`) | +| `active_account::get_num_procedures()` | `u32` (and `get_procedure_root(index: u32)`) | +| `active_account::get_nonce()`, `native_account::incr_nonce()` | `Nonce` | +| `active_note::find_attachment(..)`, `output_note::find_attachment(..)` | `Option` | + +`BlockNumber` offers `try_from(Felt)`, `as_u32()`, `as_felt()`, `From` and integer comparison; +`as_u32()` **panics** rather than truncating if the value exceeds the u32 block-height range. +`Nonce` offers `as_u64()`, `as_felt()` and `From for Felt`. + +Packing them back into a `Word` needs an explicit conversion: + +```rust +let ref_block_num = tx::get_block_number(); +let final_nonce = self.incr_nonce(); +let w = Word::from([felt!(0), felt!(0), ref_block_num.into(), final_nonce.into()]); +``` + +## P17: `AssetId` at the Protocol Layer Is the Vault Key, Not the Asset Class + +**Severity**: High — a naive find-and-replace compiles and is silently wrong + +At the protocol layer the vault key type is `AssetId`: + +```rust +pub struct AssetId { + asset_class: AssetClass, // {suffix, prefix}; both zero for fungible assets + faucet_id: AccountId, + composition: AssetComposition, +} +``` + +Word layout: `[asset_class_suffix, asset_class_prefix, [faucet_id_suffix | reserved | composition], +faucet_id_prefix]`. The actual SMT key is `AssetId::hash() -> AssetIdHash`. + +`AssetClass` is a *component of* `AssetId` — it distinguishes assets issued by the same +faucet — not the asset id itself. Treating `AssetId` as the per-faucet class compiles and is +silently wrong. The vault-key accessors are `Asset::id()` and `Asset::to_id_word()`, and the client +re-exports `AssetId` (not `AssetClass`) from `miden_client::asset`. + +There is **no `AssetVaultKey` type** in either the protocol or the client — searching for one is a +dead end, and a type of that name in your code or in generated bindings is stale. The vault-key type +is `AssetId`, declared at +`protocol:v0.16.0-rc.6:crates/miden-protocol/src/asset/vault/asset_id.rs:42` and re-exported by the +client at `miden-client:v0.16.0-rc.2:crates/rust-client/src/lib.rs:195`. + +On the guest side nothing renamed: `miden::Asset` still has a field literally named `key`, and that +word is the asset-ID word (P7). + +## P18: `MAX_ASSETS_PER_NOTE` Is 16 + +**Severity**: Medium — a loop or note builder sized for a larger bound fails + +`pub const MAX_ASSETS_PER_NOTE: usize = 16;` (mirrored by `NoteAssets::MAX_NUM_ASSETS` and by the +kernel's `constants.masm`). Any code that assumed 64 assets per note — fixed-size buffers, batching +logic, test fixtures — needs resizing. + +## P19: A Transaction Summary Is Six Words (24 Felts) + +**Severity**: High — an auth procedure hashing a four-word layout compiles and fails at runtime + +`TransactionSummary::NUM_ELEMENTS` covers six words. The standards MASM matches with +`const TX_SUMMARY_NUM_ELEMENTS = 24` and six word-sized locals +(`SUMMARY_ACCOUNT_DELTA_LOC = 0`, `SUMMARY_INPUT_NOTES_LOC = 4`, `SUMMARY_OUTPUT_NOTES_LOC = 8`, +`SUMMARY_BLOCK_COMMITMENT_LOC = 12`, `SUMMARY_PARAMS_HEAD_LOC = 16`, +`SUMMARY_PARAMS_TAIL_LOC = 20`), and +`pub proc create_tx_summary(user_params: [felt; 7]) -> (word, word, word, word, word, word)`. + +Preimage order: + +```text +[ACCOUNT_DELTA_COMMITMENT, INPUT_NOTES_COMMITMENT, OUTPUT_NOTES_COMMITMENT, + BLOCK_COMMITMENT, [expiration_delta, user_param0..2], [user_param3..6]] +``` + +Sources: `protocol:v0.16.0-rc.6:crates/miden-protocol/src/transaction/tx_summary.rs` and +`protocol:v0.16.0-rc.6:crates/miden-standards/asm/standards/auth/mod.masm`. + +## P20: Match the Project Version Line and Keep Build Tools Separate + +**Severity**: High - mixing lower bounds, resolved lockfile versions, and build tools creates false migrations + +The current project-template uses published final contract SDK crates and release-candidate host crates. Copy the local manifests and lockfile before changing versions: + +```toml +# contracts//Cargo.toml +miden = { version = "0.14" } +miden-sdk-build-script-support = { version = "0.14" } + +# integration/Cargo.toml +miden-client = { version = "0.16.0-rc.1", features = ["tonic"] } +miden-client-sqlite-store = { version = "0.16.0-rc.1", package = "miden-client-sqlite-store" } +miden-standards = { version = "0.16.0-rc.4", features = ["testing"] } +miden-testing = "0.16.0-rc.4" +miden-mast-package = { version = "0.29", default-features = false } +``` + +`Cargo.lock` currently resolves `miden-client` / `miden-client-sqlite-store` to `0.16.0-rc.2`, protocol/standards/testing to `0.16.0-rc.6`, and VM/package crates such as `miden-mast-package` to `0.29.4`. Treat the manifest requirements as lower bounds and the lockfile as the exact local graph. + +**MSRV split**: use the highest applicable toolchain. The published SDK and `cargo-miden` line require Rust 1.99, and this project pins `nightly-2026-09-01` with target `wasm32-wasip2` for contract builds. + +**Build tool separation**: `cargo-miden` is an out-of-process build tool, not an `integration` library dependency. The test helper calls `miden` / `cargo miden build` through `build_project_in_dir(...)`, then tests the produced `.masp` packages with the client, standards, and `miden-testing` dependencies. + +## P21: `miden-project.toml` Requires `[lib] path` + +**Severity**: Medium — a missing key is a parse error, an unknown key is also a parse error + +`[lib]` and every `[[bin]]` target carry a **mandatory** `path` (`Span`, no `serde(default)`), +and both target structs are `deny_unknown_fields` — every other key (`kind`, `namespace`, `name`) is +optional. `kind` exists on `[lib]` only; `[[bin]]` targets are always executables. Accepted `kind` +spellings: `lib` / `library`, `kernel`, `account` / `account-component`, `note`, `tx-script` / +`transaction-script`; an executable kind on `[lib]` errors with `this is not a valid target type for +a library`. + +Full account-component manifest, matching +`compiler:sdk/v0.14.0:examples/counter-contract/miden-project.toml`: + +```toml +[package] +name = "counter-contract" +version = "0.1.0" + +[lib] +kind = "account-component" +namespace = "miden:counter-contract/counter-contract@0.1.0" +path = "src/lib.rs" # mandatory + +[dependencies] +miden-core = "*" +miden-protocol = "*" + +[package.metadata.miden] +supported-types = ["RegularAccountUpdatableCode"] +``` + +`[package.metadata]` is a free-form bag as far as the `miden-project` parser is concerned, but the +SDK macros read `[package.metadata.miden] project-kind` out of it and treat +`project-kind = "authentication-component"` as the switch that requires exactly one `#[auth_script]` +method (P14). Other observed values of `supported-types`: `"RegularAccountImmutableCode"`, and +`["FungibleFaucet", "NonFungibleFaucet"]` for faucets. + +Cross-component dependencies go in `miden-project.toml`'s `[dependencies]` - never in `Cargo.toml`, +which the macros read only for `[package] name` / `description`. The macro reads embedded WIT from +dependency packages and uses `MIDENC_PACKAGE_CACHE` for source dependencies prepared by `build.rs`. +Do not add a `wit` override for a dependency whose package already embeds WIT; current macros reject +that override. For source dependencies, plain Cargo checks, builds, and IDE analysis require the +dependency crate's `build.rs` to call `miden_sdk_build_script_support::prepare_package_cache()` with +a matching `miden-sdk-build-script-support` dependency. + +## P22: MASM-Side Facts That Bite Rust SDK Developers + +**Severity**: Medium — relevant when hand-writing component MASM, or reading the standards / kernel MASM + +- **An undeclared `.masm` file is silently dropped.** A file is compiled only if its parent module + declares it with `mod` / `pub mod`. Module discovery is seeded exclusively from the root module's + `submodules()` and extended from each child's — there is no directory walk, so an undeclared file + is never opened. The *error* direction is a declaration with no matching `.masm` or + `/mod.masm` (`ParsingError::UndefinedSubmodule`; both present is + `AmbiguousSubmoduleLocation`), and a module reaching the linker without a parent declaration is + `LinkerError::UndeclaredSubmodule`. +- **Import syntax**: `use x -> y` is rejected — the parser says *import aliases use `as`; `->` is no + longer supported*. Item imports are `use {a, b} from path::to::module`, with per-item `as` aliases. + `pub use` is legal only for braced item imports — write `pub use {c} from a::b`, not + `pub use a::b::c`. Modules cannot be re-exported at all (use `pub mod`); wildcard and digest + imports are rejected. +- **`debug.*` decorators are gone.** `debug.stack.4`, `debug.mem`, `debug.local.0.2` and + `debug.adv_stack.4` are rejected by the parser. The replacement is the `miden::core::debug` + module (`miden-vm:v0.29.4:crates/lib/core/asm/debug.masm`), exporting `print_stack`, `print_mem`, + `print_mem_addr`, `print_mem_all`, `print_adv_stack`, `print_adv_stack_all`, `print_adv_map_all`, + `print_adv_map_item`. These are **ordinary procedure calls that print unconditionally**, + regardless of debug mode, and the ones taking stack inputs consume them — strip them from + production code. The advice-stack / advice-map printers additionally need host handlers + registered. +- **`.masl` is gone.** The artefact is a `Package` with extension `.masp` (magic `b"MASP\0"`); + `Library` and `KernelLibrary` were deleted. `Assembler::link_package(Arc, Linkage)` and + `Assembler::with_package(..)` are the linking entry points, kernels come in via + `Assembler::with_kernel(source_manager, Arc)`, and `assemble_library` / + `assemble_kernel` / `assemble_program` all return `Box`. diff --git a/.claude/skills/rust-sdk-source-guide/SKILL.md b/.claude/skills/rust-sdk-source-guide/SKILL.md index 2dd39d2..067d21c 100644 --- a/.claude/skills/rust-sdk-source-guide/SKILL.md +++ b/.claude/skills/rust-sdk-source-guide/SKILL.md @@ -22,20 +22,22 @@ Rule of thumb: if the task involves more than one contract or a pattern not cove This is the single highest-leverage practice for AI-assisted Miden development. -**Build loop**: After every contract edit, run `cargo miden build --manifest-path contracts//Cargo.toml --release`. The project's build hook does this automatically. If the build fails: +**Build loop**: After every contract edit, run the midenup / cargo-miden command from this project's `README.md` and `CLAUDE.md`, e.g. `miden build --manifest-path contracts//Cargo.toml --release` or `cargo miden build --manifest-path contracts//Cargo.toml --release` (adjust the path to your project's contract layout). `build` forwards its arguments to `midenc`'s compiler parser, so `--manifest-path` and the profile flags (`--release` / `--debug`) are understood. The output is a `.masp` package written under `//`. If your project has a build hook configured, it may run this automatically. If the build fails: 1. Read the error message -2. Translate obvious SDK migration errors first: +2. Translate obvious SDK/compiler errors first: - `.as_u64()` -> `.as_canonical_u64()` - `Recipient::compute(...)` -> `note::build_recipient(...)` - `Value` -> `StorageValue` - `StorageMap` -> `StorageMap` + - `active_note::get_assets()` -> `active_note::get_initial_assets()` + - a trait method that a note or tx script cannot reach -> it is missing `#[account_procedure]` 3. Search the source repos for a working example of the pattern that failed 4. Adapt the working pattern to your use case 5. Rebuild -**Test loop**: Write tests alongside contracts. Run full repo checks with `cargo make test` (or `cargo test -p integration --release` for a faster integration-only loop). When tests fail: +**Test loop**: Write tests alongside contracts. Run full repo checks with `cargo test` — or `make test` if the repo ships a GNU `Makefile`. In the protocol repo the `test` target runs `cargo nextest run --profile default --cargo-profile test-dev --features concurrent,testing,std` (siblings: `test-dev`, `test-release` / `testr`, `test-docs`, `lint`, `clippy`, `format`, `doc`). `cargo make test` needs a `Makefile.toml` configuring `cargo-make`; among the referenced repos only the compiler ships one — protocol, the client and the VM each ship a GNU `Makefile` and no `Makefile.toml`, while the compiler ships a `Makefile.toml` and no GNU `Makefile`. When tests fail: 1. Check the error — is it a build error, a runtime assertion, or a proof failure? -2. For assertion failures: check felt arithmetic (modular wrapping) and storage slot naming +2. For assertion failures: check felt arithmetic (modular wrapping), storage slot naming, and whether the call is legal in its runtime context (`output_note::create` is account-context-only; `incr_nonce()` is auth-procedure-only) 3. For unexpected behavior: compare your code against the closest working example in source repos Never submit code that doesn't compile and pass tests. The verification loop is your quality guarantee. @@ -49,9 +51,10 @@ The basic skills (rust-sdk-patterns, rust-sdk-testing-patterns, miden-concepts, - Read source files only when you need a specific answer (progressive disclosure) - Look for working examples first, then adapt. Working code that compiles is more reliable than documentation. - When you find a useful pattern in source, extract just what you need — the exact API call, the exact data layout, the exact test setup. +- Start API questions at `compiler/sdk/sdk/MIGRATION.md`. Its `## Unreleased` section is the authoritative, hand-written list of what changed on the Rust contract surface, with before/after code for each break. `compiler/sdk/CHANGELOG.md` is the companion. For an exact signature, go to `compiler/sdk/base-sys/src/bindings/*.rs`. **Using sub-agents for exploration**: -- Launch an explore sub-agent with a specific question: "Find how P2ID output notes are created in the miden-bank repository" +- Launch an explore sub-agent with a specific question: "Find how the basic-wallet component creates an output note and moves an asset into it (`compiler/examples/basic-wallet/src/lib.rs`)" - The sub-agent searches, reads the relevant files, and returns a focused summary - Your main context stays clean for implementation @@ -70,84 +73,189 @@ When stuck at any stage: search the source repos for a similar working pattern. --- -## Miden Source Repository Map - -Clone these repos alongside your project for reference. Claude will explore them when needed for advanced patterns. - -```bash -# Required: contains standard note types and account components -git clone --depth 1 --branch main https://github.com/0xMiden/miden-base.git ../miden-base - -# Required: contains SDK, compiler, and 12 working examples -git clone --depth 1 --branch main https://github.com/0xMiden/compiler.git ../compiler +## Which Version Is Which -# Required: contains client API for deployment and chain interaction -git clone --depth 1 --branch main https://github.com/0xMiden/miden-client.git ../miden-client +The three-way version skew is the most confusing thing about this stack. These are four independent +release lines: -# Recommended: complete working banking app with advanced patterns in the `examples/miden-bank` folder of the tutorials repo -git clone --branch main https://github.com/0xMiden/tutorials.git ../tutorials -``` +| Line | Crates | Version | MSRV | +|---|---|---|---| +| Contract SDK (guest) | `miden`, `miden-base`, `miden-base-macros`, `miden-base-sys`, `miden-stdlib-sys`, `miden-sdk-alloc` | project manifests use `0.14`, resolving to published `0.14.0` | Rust 1.99 + nightly `2026-09-01`, target `wasm32-wasip2` | +| Compiler / build tool | compiler workspace, `midenc`, `cargo-miden` | `cargo-miden` `0.10.0`, usually installed through midenup / cargo-miden | Rust 1.99 | +| Protocol | `miden-protocol`, `miden-standards`, `miden-testing`, `miden-tx`, `miden-tx-batch`, `miden-block-prover`, `miden-agglayer` | integration manifests lower-bound standards/testing at `0.16.0-rc.4`; `Cargo.lock` resolves protocol/standards/testing to `0.16.0-rc.6` | see each crate | +| Client | `miden-client`, `miden-client-sqlite-store` | integration manifests lower-bound at `0.16.0-rc.1`; `Cargo.lock` resolves both to `0.16.0-rc.2` | see each crate | +| VM / package crates | `miden-assembly`, `miden-assembly-syntax`, `miden-core`, `miden-core-lib`, `miden-crypto`, `miden-mast-package`, `miden-processor`, `miden-project`, `miden-prover` | `Cargo.lock` resolves package crates such as `miden-mast-package` to `0.29.4` | see each crate | -**Note**: These commands clone the stable `main` branch. Only use `--branch next` if the user explicitly requests the experimental/upcoming version of the compiler or source repos. +For published final crates, follow the local manifests: contract crates use `miden = { version = "0.14" }` and matching `miden-sdk-build-script-support = { version = "0.14" }`. Full pre-release strings matter for release-candidate dependencies such as `miden-client = { version = "0.16.0-rc.1" }`, `miden-standards = { version = "0.16.0-rc.4" }`, and `miden-testing = "0.16.0-rc.4"`; read both `integration/Cargo.toml` and `Cargo.lock` before changing version requirements. -### `compiler/` — The Rust-to-MASM Compiler +Keep the build tool out of the integration crate's dependency graph. The current project builds contracts out of process with midenup / cargo-miden via `build_project_in_dir(...)`, then tests those packages with the client, standards, and `miden-testing` libraries resolved in `Cargo.lock`. -Contains the SDK that powers `#[component]`, `#[note]`, and `#[tx_script]` macros. +--- -- **`examples/`** — 12 working examples covering every SDK pattern: account components, note scripts, transaction scripts, authentication components, wallets, faucets, storage. These are the most reliable reference for "how to write X" questions. +## Miden Source Repository Map -**WARNING**: Stay in `examples/` only. Do NOT explore compiler internals (`sdk/`, `codegen/`, etc.) — they are implementation details that will confuse the agent and lead to incorrect code. +Clone these repos alongside your project for reference. Claude will explore them when needed for advanced patterns. -**Explore when**: Writing any new contract type, finding working code examples for patterns not covered by skills. +```bash +# Required: protocol layer, standard note types and account components (crate: miden-protocol) +git clone --branch v0.16.0-rc.6 https://github.com/0xMiden/protocol.git ../protocol -### `miden-base/` — Protocol Layer and Standard Library +# Required: client API for deployment and chain interaction (crate: miden-client) +git clone --branch v0.16.0-rc.2 https://github.com/0xMiden/miden-client.git ../miden-client -Contains the protocol specification, standard components, and standard note types. +# Required: the Rust SDK macros and compiler; the sdk tag names the guest SDK version. +git clone --branch sdk/v0.14.0 https://github.com/0xMiden/compiler.git ../compiler -- **`crates/miden-standards/`** — Standard note types (P2ID, P2IDE, SWAP, BURN, MINT) and standard account components (BasicWallet, BasicFungibleFaucet, authentication components). Explore to understand note flow patterns and data layouts. -- **`crates/miden-protocol/asm/kernels/transaction/`** — The MASM transaction kernel. Every Rust SDK function (e.g., `native_account::add_asset`, `output_note::create`, `faucet::mint`) maps to a procedure defined here. Start with `api.masm` to find the procedure signature and stack contract, then read the implementation in `lib/` (e.g., `lib/output_note.masm`, `lib/account.masm`, `lib/epilogue.masm`). Useful for understanding exactly what happens under the hood -- for example, whether a function touches the vault, what the conservation check compares, or how note assets are tracked. -- **`crates/miden-tx/`** — Rust execution engine (executor, prover, host). Orchestrates transaction execution but rarely needed for understanding contract behavior. Explore only if debugging execution infrastructure or host-level behavior. -- **`crates/miden-testing/`** — MockChain implementation internals. Explore when you need to understand testing infrastructure beyond what the testing-patterns skill covers. +# Optional: the VM, assembler, and package format, when you need MASM or `.masp` internals +git clone --branch v0.29.4 https://github.com/0xMiden/miden-vm.git ../miden-vm +``` -**Note**: Standard components (BasicWallet, etc.) are MASM-only and not callable from Rust SDK (see [compiler#936](https://github.com/0xMiden/compiler/issues/936)). Explore miden-standards to understand note flows and data layouts, not for finding callable Rust APIs. +`--depth 1` is intentionally omitted so you can check out other refs later if needed. + +### `compiler/` — The Rust-to-MASM Compiler and the Guest SDK + +Contains the SDK that powers the `#[component_storage]`, `#[component]`, `#[account_procedure]`, `#[auth_script]`, `#[account]`, `#[note]`, `#[note_script]` and `#[tx_script]` macros. + +- **`compiler/examples/`** — exactly 12 working example projects, and the most reliable reference for + "how do I write X": + + | Example | Shows | + |---|---| + | `compiler/examples/counter-contract/` | account component with `StorageMap`, `#[account_procedure]`, full `miden-project.toml` | + | `compiler/examples/basic-wallet/` | asset in/out, `output_note::create` wrapped as an account procedure | + | `compiler/examples/storage-example/` | `StorageValue` + `StorageMap`, `miden::generate!()` + `bindings::export!` over a hand-written WIT interface | + | `compiler/examples/auth-component-no-auth/` | minimal `#[auth_script]` auth component, `incr_nonce()` | + | `compiler/examples/auth-component-rpo-falcon512/` | signature-checking auth component, transaction-summary hashing | + | `compiler/examples/p2id-note/` | typed `#[note]` struct decoded from note storage, `#[account(basic_wallet::BasicWallet)]` wrapper | + | `compiler/examples/p2ide-note/` | manual `active_note::get_storage()` parsing, `BlockNumber` timelock comparison | + | `compiler/examples/counter-note/` | note script calling a component through generated bindings, with no `#[account(..)]` wrapper | + | `compiler/examples/basic-wallet-tx-script/` | `#[tx_script]`, advice-provider input loading, calling wallet procedures | + | `compiler/examples/collatz/`, `compiler/examples/fibonacci/`, `compiler/examples/is-prime/` | plain compute programs, no account context | + + There is no faucet example here. For faucet reference use + `protocol/crates/miden-standards/src/account/faucets/fungible/mod.rs` (the `FungibleFaucet` + component) or the compiler's own `compiler/tests/integration/src/sdk/base/faucet.rs` binding test. + +- **`compiler/sdk/`** — the guest SDK, and at v0.16 the single most authoritative API reference. Whitelisted + for exploration: + - `compiler/sdk/sdk/MIGRATION.md` — start here; the `## Unreleased` section is the v0.16 contract-surface + change list with before/after code + - `compiler/sdk/CHANGELOG.md` + - `compiler/sdk/base-sys/src/bindings/` — the exact signature of every kernel binding + (`active_account.rs`, `native_account.rs`, `active_note.rs`, `input_note.rs`, `output_note.rs`, + `note.rs`, `faucet.rs`, `tx.rs`, `types.rs`) + - `compiler/sdk/base/src/types/storage.rs` — `StorageValue` / `StorageMap` / `WordKey` / `WordValue` + - `compiler/sdk/base-macros/src/` — macro behaviour and, importantly, the exact error messages + (`component_macro/mod.rs`, `component_macro/storage.rs`, `component_macro/sibling.rs`, + `foreign_account.rs`, `note.rs`, `script.rs`, `wit_world.rs`) + +- **`compiler/tests/integration-network/src/mockchain/`** — end-to-end multi-contract MockChain flows + (counter contract under three auth components, FPI in many shapes, asset transfer), including + the live storage-slot-name strings in `compiler/tests/integration-network/src/mockchain/support/helpers.rs`. + +**WARNING**: Do NOT explore the compiler's own internals — `compiler/codegen/`, `compiler/hir/`, `compiler/hir-analysis/`, `compiler/hir-transform/`, `compiler/frontend/`, `compiler/midenc-compile/`, `compiler/midenc-session/` — they are implementation details that will confuse the agent and lead to incorrect code. +The one narrow exception is `compiler/frontend/wasm/src/component/` when you need the exact wording of a +call-boundary (16-felt) diagnostic. + +**Explore when**: Writing any new contract type, checking an exact binding signature, or finding working code for a pattern not covered by skills. + +### `protocol/` — Protocol Layer and Standard Library + +The protocol repo (`github.com/0xMiden/protocol`; primary crate `miden-protocol`). Contains the protocol specification, standard components, and standard note types. Workspace crates: `miden-agglayer`, `miden-block-prover`, `miden-protocol`, `miden-protocol-build-utils`, `miden-standards`, `miden-testing`, `miden-tx`, `miden-tx-batch`. + +- **`protocol/crates/miden-standards/`** — Standard note types (`P2idNote`, `P2ideNote`, `SwapNote`, + `PswapNote`, `BurnNote`, `MintNote` under `protocol/crates/miden-standards/src/note/`) and standard account + components (BasicWallet, FungibleFaucet, authentication components). Standard notes are built with + typed `bon` builders — e.g. + `P2idNote::builder().sender(..).target(..).serial_number(..).asset(..).build()?` — and convert via + `impl From for Note`; there is no `XNote::create(..)`. Each type also exposes the + associated function `script_root() -> NoteScriptRoot`. +- **`protocol/crates/miden-protocol/asm/`** — the MASM. Four things live here, and the split matters: + - `protocol/crates/miden-protocol/asm/kernels/transaction/lib/api.masm` — **the syscall surface**. This + directory contains `api.masm` and nothing else. Start here for a procedure's signature, stack + contract, and its context assertions (`exec.memory::assert_native_account`, + `exec.authenticate_account_origin`, `exec.assert_auth_procedure_origin`). The kernel binaries + are `protocol/crates/miden-protocol/asm/kernels/transaction/bin/main.masm` and + `protocol/crates/miden-protocol/asm/kernels/transaction/bin/tx_script_main.masm`. + - `protocol/crates/miden-protocol/asm/kernels/transaction-core/src/` — **the implementations**: + `mod.masm`, `account.masm`, `account_update.masm`, `asset.masm`, `asset_vault.masm`, + `callbacks.masm`, `constants.masm`, `epilogue.masm`, `faucet.masm`, `fungible_asset.masm`, + `input_note.masm`, `link_map.masm`, `memory.masm`, `non_fungible_asset.masm`, `note.masm`, + `output_note.masm`, `prologue.masm`, `tx.masm`. + - `protocol/crates/miden-protocol/asm/protocol/src/` — the userspace `miden::protocol::*` modules that the + Rust SDK bindings actually map onto: `active_account.masm`, `active_note.masm`, + `native_account.masm`, `note.masm`, `output_note.masm`, `input_note.masm`, `faucet.masm`, + `tx.masm`, `asset.masm`, `auth.masm`, `account_id.masm`, `kernel_proc_offsets.masm`, + `types.masm`, `constants.masm`, `mod.masm`. + - `protocol/crates/miden-protocol/asm/protocol_utils/src/` — shared helpers (`account_id.masm`, + `asset.masm`, `constants.masm`, `mem.masm`, `note.masm`, `types.masm`, `mod.masm`). + + The batch kernel is separate: `protocol/crates/miden-protocol/asm/kernels/batch/src/main.masm`. +- **`protocol/crates/miden-tx/`** — Rust execution engine (executor, prover, host). Orchestrates transaction execution but rarely needed for understanding contract behavior. Explore only if debugging execution infrastructure or host-level behavior. +- **`protocol/crates/miden-testing/`** — MockChain implementation internals: + `protocol/crates/miden-testing/src/mock_chain/chain.rs` (`MockChain`), + `protocol/crates/miden-testing/src/mock_chain/chain_builder.rs` (`MockChainBuilder`), + `protocol/crates/miden-testing/src/mock_transaction/builder.rs` (`MockTransactionBuilder`). + `protocol/crates/miden-testing/src/kernel_tests/tx/` is the + canonical worked usage of the rc.6 API against a MockChain. + +**Note on standard components**: `miden-standards` ships them as MASM +(`protocol/crates/miden-standards/asm/standards/wallets/basic.masm`, +`protocol/crates/miden-standards/asm/standards/notes/p2id.masm`, +`protocol/crates/miden-standards/asm/components/auth/singlesig/singlesig.masm`). A MASM +component participates in the account interface by annotating its exports `@account_procedure` or +`@auth_script`. Separately, the compiler now ships a **Rust** `basic-wallet` account component in +`compiler/examples/basic-wallet/`, and `compiler/examples/p2id-note/`, `compiler/examples/p2ide-note/` and +`compiler/examples/basic-wallet-tx-script/` call it through `#[account(basic_wallet::BasicWallet)]` — so a +Rust-compiled wallet component is callable from Rust. Explore `miden-standards` for note flows, +data layouts, and the canonical MASM shape. **Explore when**: Understanding note flows, P2ID/SWAP/faucet data layouts, or what SDK functions actually do under the hood (via the kernel MASM). -### `miden-client/` — Client Library +### `miden-client/` - Client Library (crate `miden-client`) -Contains the Rust API for deploying contracts and interacting with the Miden network. +The client repo is `https://github.com/0xMiden/miden-client`; the published crate is `miden-client`, +and the CLI source lives under `bin/miden-cli/`. - Rust client for building transactions, syncing state, managing accounts and notes -- CLI tool source code for reference on client usage patterns +- Re-exports the protocol types you need at the boundary, e.g. `miden_client::note::P2idNote`, + `miden_client::note::NoteScriptRoot`, `miden_client::asset::AssetId` +- `miden-client/bin/miden-cli/` is CLI tool source, useful as a reference for client usage patterns **Explore when**: Deploying contracts to testnet, submitting transactions, syncing state, managing notes on-chain. -### `miden-bank/` — Working Example Application +### `miden-vm/` — VM, Assembler, and Package Format -A complete banking application built with the Rust SDK. Demonstrates advanced patterns that go beyond the basic skills. +Only needed for MASM or artefact-level questions, but authoritative for them: -- Multiple contract types working together (account, deposit note, withdraw note, tx script) -- Advanced patterns: `StorageMap` + `StorageValue` composition, felt arithmetic safety, cross-component calls, P2ID output note creation from within contracts -- Multi-step integration tests with output note verification - -**Explore when**: Building multi-contract applications, understanding how pieces fit together, seeing a complete working app end-to-end. +- `miden-vm/crates/project/src/ast/target.rs` — the `miden-project.toml` schema (`[lib]` / `[[bin]]` + targets, mandatory `path`, `deny_unknown_fields`) +- `miden-vm/crates/mast-package/src/package/` — the `.masp` package format (`Package`, magic `b"MASP\0"`) +- `miden-vm/crates/assembly/src/assembler.rs` — `Assembler::link_package` / `with_package` / `with_kernel`; + `assemble_library` / `assemble_kernel` / `assemble_program` all return `Box`. `.masl`, + `Library` and `KernelLibrary` no longer exist. +- `miden-vm/crates/lib/core/asm/debug.masm` — the `miden::core::debug` printers that replaced the removed + `debug.*` decorators +- `miden-vm/docs/src/user_docs/assembly/code_organization.md` — module tree, `mod` / `pub mod` declarations, + and the `use {a, b} from path` / `pub use {a} from path` import syntax --- ## What to Explore for Each Contract Type -| Building This | Explore These Repos | What to Look For | +| Building This | Explore These Paths | What to Look For | |---|---|---| -| Account component with storage | `compiler/` examples, `miden-bank/` contracts | `StorageMap` / `StorageValue` patterns, pub method signatures | -| Note script | `compiler/` examples, `miden-bank/` contracts | `#[note_script]` pattern, cross-component calls, note storage parsing | -| Transaction script | `compiler/` examples, `miden-bank/` contracts | `#[tx_script]` pattern, Account binding import | -| Authentication component | `compiler/` examples | Auth component patterns (NoAuth, Falcon512, ECDSA) | -| Faucet (token minting) | `compiler/` examples | BasicFungibleFaucet example, mint/burn pattern | -| P2ID output notes | `miden-bank/` contracts, `miden-base/` standards (data layouts) | `note::build_recipient`, script root, `output_note` creation | -| Swap notes | `miden-base/` standards (data layouts) | SwapNote data layout, tag construction, payback flow | -| Multi-step tests | `miden-bank/` integration tests | Init → operate → verify flow, output note verification | +| Account component with storage | `compiler/examples/counter-contract/`, `compiler/examples/storage-example/` | `StorageMap` / `StorageValue` patterns, `#[account_procedure]` on the trait, `miden-project.toml` shape | +| Note script | `compiler/examples/p2id-note/`, `compiler/examples/p2ide-note/`, `compiler/examples/counter-note/` | `#[note]` + `#[note_script]`, typed vs manual note-storage parsing, cross-component calls | +| Transaction script | `compiler/examples/basic-wallet-tx-script/` | `#[tx_script]`, `#[account(..)]` wrapper, advice-provider inputs | +| Authentication component | `compiler/examples/auth-component-no-auth/`, `compiler/examples/auth-component-rpo-falcon512/` | exactly one `#[auth_script]`, `project-kind = "authentication-component"`, `incr_nonce()`, tx-summary hashing | +| Faucet (token minting) | `protocol/crates/miden-standards/src/account/faucets/fungible/mod.rs`, `compiler/tests/integration/src/sdk/base/faucet.rs` | `FungibleFaucet::builder()`, `faucet::mint` / `faucet::burn` bindings, `supported-types = ["FungibleFaucet", "NonFungibleFaucet"]` | +| P2ID output notes | `compiler/examples/basic-wallet/src/lib.rs`, `protocol/crates/miden-standards/src/note/p2id.rs` | `note::build_recipient`, `P2idNote::script_root()`, `output_note::create` wrapped as an account procedure | +| Swap notes | `protocol/crates/miden-standards/src/note/swap.rs` | SwapNote data layout, tag construction, payback flow | +| Multi-step / multi-contract tests | `compiler/tests/integration-network/src/mockchain/`, `protocol/crates/miden-testing/src/kernel_tests/tx/` | MockChain setup, init → operate → verify flow, output-note verification, storage-slot names | | Client deployment | `miden-client/` | TransactionRequestBuilder, sync, submit patterns | -| SDK function internals | `miden-base/` kernel (`crates/miden-protocol/asm/kernels/transaction/`) | `api.masm` for procedure signatures, `lib/*.masm` for implementations | +| SDK binding signatures | `compiler/sdk/base-sys/src/bindings/*.rs`, `compiler/sdk/sdk/MIGRATION.md` | exact Rust signature and return type of every kernel binding | +| SDK function internals | `protocol/crates/miden-protocol/asm/kernels/transaction/lib/api.masm` → `protocol/crates/miden-protocol/asm/kernels/transaction-core/src/*.masm` → `protocol/crates/miden-protocol/asm/protocol/src/*.masm` | `api.masm` for signatures + context assertions, `protocol/crates/miden-protocol/asm/kernels/transaction-core/src/` for implementations, `protocol/crates/miden-protocol/asm/protocol/src/` for the userspace wrappers the bindings call | --- @@ -156,23 +264,54 @@ A complete banking application built with the Rust SDK. Demonstrates advanced pa These patterns go beyond what the basic skills cover. For each, the source repos contain working implementations. ### Multi-Component Accounts -Accounts can include standard components (BasicWallet, authentication) alongside custom logic at account creation time. Standard components are MASM-only (not callable from Rust), but they are composed into accounts via the testing/deployment infrastructure. The `compiler/` examples show how to compose accounts with multiple components. +Accounts compose several components at creation time: custom logic plus an authentication component, and optionally a standard component. Every account needs exactly one authentication component, whose single `#[auth_script]` procedure is the only place `incr_nonce()` may be called. `compiler/tests/integration-network/src/mockchain/counter/` builds the same counter account against three different auth components and is the clearest worked example. Standard components can be MASM (`miden-standards`) or Rust (`compiler/examples/basic-wallet/`); in both cases the interface is defined by `@account_procedure` / `#[account_procedure]` annotations on the exported procedures. ### Output Note Creation from Contracts -Create output notes (like P2ID) from within contract code. Requires building a recipient with `note::build_recipient(serial_num, script_root, storage)` and then using `output_note::create(...)`. The `miden-bank/` withdraw pattern demonstrates this end-to-end. +Create output notes (like P2ID) from within contract code: build a recipient with `note::build_recipient(serial_num, script_root, storage)`, call `output_note::create(tag, note_type, recipient)` for the index, then move assets in with `native_account::remove_asset(asset)` + `output_note::add_asset(asset, note_idx)`. + +Both `output_note::create` and `native_account::remove_asset` are **account-context-only at +runtime**, so the whole sequence has to live inside an account-component procedure. A tx script or +note script that calls them directly compiles and then fails during execution. +`compiler/examples/basic-wallet/src/lib.rs` is the reference: it exposes +`#[account_procedure] fn create_note(..) -> NoteIdx` and +`#[account_procedure] fn move_asset_to_note(asset, note_idx)`, and +`compiler/examples/basic-wallet-tx-script/src/lib.rs` drives them from the script side. ### Note Storage Protocol -Notes carry storage data as a `Vec` baked at creation time. The `#[note]` macro generates a `TryFrom<&[Felt]>` for the note struct via the `FromFeltRepr` trait, so the `#[note_script]` method receives the deserialized note as `self` (by value); the script reads typed fields with `self.` and never indexes the raw Felt slice. -Attached assets are separate and should be read with `active_note::get_assets()`. +A note's storage is exposed to its `#[note_script]` as a `Vec` via `active_note::get_storage()`. Two styles: + +- **Typed (preferred)** — give the `#[note]` struct fields and the macro auto-decodes them: it + generates a `TryFrom<&[Felt]>` that reads each field via `FromFeltRepr` and then calls + `ensure_eof()`, so trailing felts are a decode error rather than ignored padding. A zero-sized + `#[note]` struct skips `get_storage()` entirely. See `compiler/examples/p2id-note/src/lib.rs` + (`#[note] struct P2idNote { target_account_id: AccountId }`). +- **Manual** — read `active_note::get_storage()` and index it yourself, asserting the length. See + `compiler/examples/p2ide-note/src/lib.rs`, which asserts `inputs.len() == 4` and then builds + `AccountId` and `BlockNumber` values out of the felts. + +Attached assets are separate from storage and are read with `active_note::get_initial_assets()`. ### Atomic Swaps -The standard SwapNote in `miden-base/` creates a payback P2ID note automatically when consumed. Explore the SwapNote builder to understand tag construction, storage layout, and the payback mechanism. +The standard SwapNote in `protocol/crates/miden-standards/src/note/swap.rs` creates a payback P2ID note automatically when consumed. Explore the `SwapNote` `bon` builder and `SwapNote::script_root()` to understand tag construction, storage layout, and the payback mechanism. ### Account Initialization -Use `#[tx_script]` to initialize accounts before they accept operations. The `miden-bank/` init-tx-script calls `account.initialize()` to set an initialization flag, which is checked before every operation. +Use `#[tx_script]` to run setup or admin operations against an account before or between note flows. The signature is validated by the macro: at most two parameters, the first literally typed `Word`, the second a reference to an `#[account(...)]` wrapper type. `compiler/examples/basic-wallet-tx-script/src/lib.rs` is the reference — `fn run(arg: Word, account: &mut Wallet)`, loading its inputs from the advice provider and then calling account procedures. ### Token Creation (Faucets) -Faucet accounts mint and burn fungible or non-fungible tokens. The `compiler/` fungible-faucet example and `miden-base/` BasicFungibleFaucet standard component show how to create and manage tokens. +Faucet accounts mint and burn tokens. The `FungibleFaucet` standard component +(`protocol/crates/miden-standards/src/account/faucets/fungible/mod.rs`) is built with +`FungibleFaucet::builder().name(..).symbol(..).decimals(..).max_supply(..).build()?`, where the +required setters take `TokenName`, `TokenSymbol`, `u8` and `AssetAmount`, `build()` returns +`Result`, and `MAX_DECIMALS = 12`. Optional setters: +`token_supply` (`AssetAmount`), `description`, `logo_uri`, `external_link`, +`is_description_mutable`, `is_logo_uri_mutable`, `is_external_link_mutable`, +`is_max_supply_mutable`. + +On the contract side only `faucet::mint(Asset)` and `faucet::burn(Asset)` exist — in-transaction +asset construction was removed, so build the `Asset` outside the transaction. There is no faucet +example in `compiler/examples/`; use `compiler/tests/integration/src/sdk/base/faucet.rs`, a +compile-only binding test (it ends at `test.compile_package()`) that also shows the faucet manifest +(`supported-types = ["FungibleFaucet", "NonFungibleFaucet"]`). ### P2ID with Expiration (P2IDE) -Send assets with a deadline — the sender can reclaim after the block height passes. The `compiler/` p2ide-note example and `miden-base/` P2IDE standard show the timelock pattern. +Send assets with a deadline — the sender can reclaim after the block height passes. `compiler/examples/p2ide-note/src/lib.rs` and `protocol/crates/miden-standards/src/note/p2ide.rs` show the timelock pattern. Note that the example works in typed `BlockNumber` values (`BlockNumber::try_from(inputs[n]).unwrap()`, then plain `>=` comparison), not raw felts, because `BlockNumber` orders as an integer. diff --git a/.claude/skills/rust-sdk-testing-patterns/SKILL.md b/.claude/skills/rust-sdk-testing-patterns/SKILL.md index b838ab0..1157896 100644 --- a/.claude/skills/rust-sdk-testing-patterns/SKILL.md +++ b/.claude/skills/rust-sdk-testing-patterns/SKILL.md @@ -5,21 +5,62 @@ description: Guide to testing Miden smart contracts with MockChain. Covers test # Miden Testing Patterns (MockChain) +These patterns target this repository's current v16 host line. `integration/Cargo.toml` lower-bounds +`miden-client` / `miden-client-sqlite-store` at `0.16.0-rc.1`, `miden-standards` / `miden-testing` at +`0.16.0-rc.4`, and `miden-mast-package` at `0.29`; `Cargo.lock` currently resolves client/sqlite to +`0.16.0-rc.2`, protocol/standards/testing to `0.16.0-rc.6`, and package crates such as +`miden-mast-package` to `0.29.4`. + +`MockChain` and its builders live in `miden-testing`. This project depends on `miden-testing` +directly; other repos may alternatively use the `miden_client::testing` re-export behind the client's +optional `testing` feature. + +The canonical worked examples are in the protocol repo itself: +`crates/miden-testing/src/kernel_tests/tx/test_note.rs` for the end-to-end MockChain flow, and +`crates/miden-testing/src/mock_chain/{chain,chain_builder}.rs` for the full builder surface. + ## Test File Setup -Tests go in `integration/tests/`. All tests are async and use MockChain for local execution without a network. +All tests are async and use MockChain for local execution without a network. -See [counter_test.rs](../../../integration/tests/counter_test.rs) for a complete working test covering imports, MockChain setup, contract building, account creation with storage, note creation, transaction execution, and storage verification. +```rust +use miden_client::{ + account::{ + component::{InitStorageData, StorageValueName}, + AccountBuilder, AccountComponent, AccountType, StorageSlotName, + }, + auth::AuthSchemeId, + note::NoteAssets, + transaction::RawOutputNote, + Felt, Word, +}; +use miden_client::asset::{Asset, FungibleAsset}; +use miden_testing::{AccountState, Auth, MockChain}; +``` + +`StorageValueName` lives under `account::component`, never under `account::` directly. ## Step-by-Step Test Pattern ### 1. Initialize MockChain Builder -See [counter_test.rs](../../../integration/tests/counter_test.rs) line 21 for the pattern: `let mut builder = MockChain::builder();` +`let mut builder = MockChain::builder();` ### 2. Create Sender/Wallet Accounts -See [counter_test.rs](../../../integration/tests/counter_test.rs) lines 24-26 for the basic wallet pattern. For wallets with pre-funded assets, use `builder.add_existing_wallet_with_assets(Auth::BasicAuth { auth_scheme: AuthSchemeId::Falcon512Poseidon2 }, [FungibleAsset::new(faucet.id(), 100)?.into()])`. +For a bare wallet, `builder.add_existing_wallet(Auth::BasicAuth { auth_scheme: AuthSchemeId::Falcon512Poseidon2 })`. +For a pre-funded one, `builder.add_existing_wallet_with_assets(auth, [FungibleAsset::new(faucet.id(), 100)?.into()])`. +Both return `anyhow::Result`. + +`Auth` also offers `Multisig`, `GuardedMultisig`, `MultisigSmart`, `IncrNonce`, `Noop`, +`Conditional` and `NetworkAccount` variants, plus the shorthands `Auth::default()`, +`Auth::basic_falcon()` and `Auth::basic_ecdsa()`. + +> Auth-scheme naming: `miden_client::auth` re-exports the **same** protocol enum under two names — +> `AuthScheme` (the protocol name) and `AuthSchemeId` (an alias). Both compile; the field is +> `Auth::BasicAuth { auth_scheme }`. The enum is `#[non_exhaustive]` with `Falcon512Poseidon2 = 2` +> and `EcdsaK256Keccak = 1`, and the constants `RPO_FALCON_SCHEME_ID` / +> `ECDSA_K256_KECCAK_SCHEME_ID` name them. ### 3. Set Up Faucets (for fungible assets) ```rust @@ -29,95 +70,200 @@ let faucet = builder.add_existing_basic_faucet( }, "TOKEN", // token symbol 1000, // max supply - Some(10), // total_issuance (None for 0) + Some(10), // token_supply (None defaults to 0) )?; ``` +The 4th argument is `token_supply: Option` (an explicit `None` is treated as `0`). + ### 4. Build Contracts -See [counter_test.rs](../../../integration/tests/counter_test.rs) lines 29-35 for the pattern using `build_project_in_dir`. +Build contracts **out of process** with the midenup / cargo-miden build command and load the resulting +`.masp` package in the test. In this repository, `build_project_in_dir(...)` uses `miden` when +`MIDENUP_HOME` is set, then `CARGO_MIDEN`, then `cargo miden`. + +Do not add `cargo-miden` as a library dependency of a crate that also depends on `miden-client`. The +build tool is intentionally outside the integration crate's Cargo graph; tests consume the compiled +package through the client, standards, and `miden-testing` dependencies. + +Package artefacts: the extension is `.masp` (`Package::EXTENSION`), magic `MASP\0`, package format +version `[6, 0, 0]`, MAST wire version `[0, 0, 4]`. There is no `.masl`. ### 5. Create Account with Storage **Storage slot naming convention** (CRITICAL): ``` -[component_package_or_name]::[snake_case(component_struct)]::[field_name] +:::: ``` -Examples: -- Package `miden:counter-account`, component `CounterContract`, field `count_map` -> `miden_counter_account::counter_contract::count_map` -- Package `miden:bank-account`, component `BankAccount`, field `balances` -> `miden_bank_account::bank_account::balances` +The slot name is part of the on-chain storage ABI and is derived by the compiler's +`#[component_storage]` macro, **not** from the Rust struct name: +- `` is the **bare** package name (`[package].name` in `miden-project.toml`), with no + `miden:` org prefix. +- `` is the `[lib].namespace` **interface** segment — the text between the last + `/` and the `@` in the namespace — snake_cased. Because it comes from the declared namespace, + renaming the Rust struct cannot change the deployed slot name. +- `` is the Rust storage field's identifier (not its `description`). -Rule: Replace characters outside `[A-Za-z0-9_]` with `_` in the package or component name. +Characters outside `[A-Za-z0-9_]` are replaced with `_` in each segment. -See [counter_test.rs](../../../integration/tests/counter_test.rs) lines 38-54 for the current pattern: populate `InitStorageData`, build the component from the compiled package, then register the account with `builder.add_account_from_builder(...)`. +Live values from the pinned compiler examples: `counter_contract::counter_contract::count_map`, +`auth_component_rpo_falcon512::auth_component::owner_public_key`. -```rust -let counter_storage_slot = counter_storage_slot()?; -let mut init_storage_data = InitStorageData::default(); -init_storage_data.insert_map_entry(counter_storage_slot.clone(), COUNTER_STORAGE_KEY, 0_u64)?; +The component's storage is declared with the three-part component macro (`#[component_storage]` +struct + `#[component]` trait + `#[component]` impl); the storage struct, not the trait, carries the +`#[storage]` fields the slot names derive from. Externally-callable trait methods additionally need +`#[account_procedure]`. See the `rust-sdk-patterns` skill for the contract side. -let counter_component = - AccountComponent::from_package(&contract_package, &init_storage_data)?; -let counter_account = builder.add_account_from_builder( - Auth::BasicAuth { - auth_scheme: AuthSchemeId::Falcon512Poseidon2, - }, - AccountBuilder::new([3_u8; 32]) - .account_type(AccountType::RegularAccountImmutableCode) - .storage_mode(AccountStorageMode::Public) - .with_component(counter_component), - AccountState::Exists, -)?; -``` +Seed any value slot that has no schema default, then register the account: -For a single-value contract slot (paired with `StorageValue` on-chain) instead of a map: ```rust +// A `StorageValue` slot with no schema default must be seeded, or +// `AccountComponent::from_package` errors with `InitValueNotProvided`. +// A map slot defaults to empty and needs no entry. +let initialized_slot = StorageSlotName::new("bank_account::bank::initialized")?; + let mut init_storage_data = InitStorageData::default(); init_storage_data.insert_value( - "miden_bank_account::bank_account::initialized", - 0_u64, + StorageValueName::from_slot_name(&initialized_slot), + Word::default(), +)?; + +let component = AccountComponent::from_package(&bank_package, &init_storage_data)?; + +let account_builder = AccountBuilder::new([3u8; 32]) + .account_type(AccountType::Public) + .with_component(component); + +let bank_account = builder.add_account_from_builder( + Auth::BasicAuth { auth_scheme: AuthSchemeId::Falcon512Poseidon2 }, + account_builder, + AccountState::Exists, )?; ``` +**There is no `AccountBuilder::with_auth_component`.** Auth components are installed like any other +component, via `.with_component(..)` / `.with_components(..)`. The builder's surface is +`new([u8; 32])`, `version`, `account_type`, `with_asset_callbacks`, `with_component(s)`, +`with_assets`, `nonce`, `storage_schemas`, `build`, `build_existing`. + +For map slots, seed entries with `init_storage_data.insert_map_entry(slot_name, key, value)?`. + +> Storage-seeding footgun: `InitStorageData::insert_value(name, value)` takes +> `value: impl Into`. The numeric `From` impls (`u8`/`u16`/`u32`/`u64`) produce a +> `WordValue::Atomic(string)` that the slot's schema parses — **not** a felt-positioned `Word`. Only +> `From` yields `[felt, 0, 0, 0]`, and `From` / `From<[Felt; 4]>` / `From<[u32; 4]>` are +> fully-typed words. For a `StorageValue` slot whose contract reads index `[0]`, seed a `Word` +> (`Word::default()` for zero). The `insert_value` doc comment claiming `u64` becomes `[0,0,0,felt]` +> is inaccurate; the code produces an atomic string. + +> Account model: +> - `AccountType` is the visibility enum `{ Private (default), Public }`, with `as_u8()`, +> `is_public()`, `is_private()`. +> - Set visibility via `.account_type(..)`. There is no `.storage_mode(..)` and no +> `AccountStorageMode` on this builder. +> - Faucet-ness is determined by the installed components. + +`MockChainBuilder` also ships ready-made account helpers that remove most of the above: +`create_new_wallet`, `add_existing_note_creator`, `add_existing_non_fungible_faucet`, +`add_existing_network_faucet`, `create_new_faucet`, `add_existing_mock_account` (and its +`_with_storage` / `_with_assets` / `_with_storage_and_assets` variants), +`add_existing_account_from_components`. + ### 6. Create Notes -See [counter_test.rs](../../../integration/tests/counter_test.rs) lines 56-64 for basic note creation with `RandomCoin`, `NoteScript::from_package`, and `NoteBuilder`. +Build a note with `NoteBuilder`, seeding the `RandomCoin` from the note-script root: -For notes with assets and inputs: ```rust -use miden_client::{asset::FungibleAsset, crypto::RandomCoin, note::NoteScript, Felt}; +use miden_client::{asset::FungibleAsset, crypto::RandomCoin, note::NoteScript, Felt, Word}; use miden_standards::testing::note::NoteBuilder; -let mut note_rng = RandomCoin::new(NoteScript::from_package(note_package.as_ref())?.root()); +let note_script = NoteScript::from_package(note_package.as_ref())?; +let mut note_rng = RandomCoin::new(Word::from(note_script.root())); let note = NoteBuilder::new(sender.id(), &mut note_rng) .package((*note_package).clone()) .add_assets([FungibleAsset::new(faucet.id(), 50)?.into()]) - .note_storage([Felt::new(42), Felt::new(0)])? + .note_storage([Felt::from(42_u32), Felt::from(0_u32)])? .build()?; ``` +`NoteBuilder::new(sender: AccountId, rng: T)` takes the RNG **by value**; `&mut RandomCoin` works +because `&mut T: Rng`. Other builder methods: `package`, `script`, `code`, `note_type`, `tag`, +`add_assets`, `note_storage`, `serial_number`, `attachment`, `advice_map`, +`dynamically_linked_packages`, `source_manager`, `build`. + +> `.tag(..)` takes a **`u32`**, not a `NoteTag`. The idiom is +> `.tag(NoteTag::with_account_target(account.id()).into())`. + +> `NoteScript::from_package(&Package)` requires the package to have exactly one `@note_script` +> export. `NoteScript::root()` returns a `NoteScriptRoot` newtype, and `RandomCoin::new` needs a +> `Word`, so convert explicitly with `Word::from(...root())`. + +> `Felt::new(u64)` is **fallible** — it returns `Result`. `note_storage` +> takes `impl IntoIterator`, so build each felt with the infallible +> `Felt::from(42_u32)` for in-range literals (`From//` are infallible); for a `u64` +> use `Felt::new(n)?` or `Felt::new_unchecked(n)`. + +> A note carries at most `MAX_ASSETS_PER_NOTE` = **16** assets. + +`MockChainBuilder` also has ready-made note constructors that skip `NoteBuilder` entirely: +`add_p2any_note`, `add_p2id_note`, `add_p2ide_note`, `add_swap_note`, `add_spawn_note`, +`add_tx_fee_note`, `add_p2id_note_with_fee`. + +Standard notes built outside the chain builder use typed `bon` builders — +`P2idNote::builder().sender(..).target(..).serial_number(..).asset(..).build()?`, then +`Note::from(p2id)`. There is no `XNote::create(..)`. + ### 7. Add to MockChain and Build -See [counter_test.rs](../../../integration/tests/counter_test.rs) lines 66-70 for seeding the note and building the mock chain. `add_account_from_builder(...)` has already registered the account in the builder, so at this stage you usually only need to add notes. +Register accounts (`builder.add_account(account)?`) and seed notes +(`builder.add_output_note(RawOutputNote::Full(note.clone()))`), then `let mut mock_chain = builder.build()?;`. + +> `add_output_note` returns `()`, not a `Result` — no `?`. ### 8. Execute Transaction -See [counter_test.rs](../../../integration/tests/counter_test.rs) lines 73-82 for the full execution flow: `build_tx_context` -> `execute()` -> `add_pending_executed_transaction()` -> `prove_next_block()`. The single-transaction counter test does not call `apply_delta()` because `counter_account` is not reused after the build; final state is read from `mock_chain.committed_account(...)` after the block is proven. Multi-transaction tests that keep using the in-memory `Account` variable across steps should call `account.apply_delta(&executed.account_delta())?` after each `execute()` (see "Multi-Transaction Test Pattern" below). +```rust +let executed = mock_chain + .build_transaction(account.id()) + .authenticated_input_note(note.id()) + .build()? + .execute() + .await?; + +mock_chain.add_pending_executed_transaction(&executed)?; +mock_chain.prove_next_block()?; +``` + +`build_transaction(input)` takes `impl Into`, which has `From` and +`From`. Pass an `Account` (rather than an id) when chaining transactions against evolving +in-memory state, and for private accounts. + +Input notes are **not** positional arguments. Attach them with `.authenticated_input_note(NoteId)`, +`.authenticated_input_notes(..)`, `.unauthenticated_input_note(Note)` or +`.unauthenticated_input_notes(..)`. + +`build()` is sync and returns `anyhow::Result`; `execute()` is on `MockTransaction`, +is `async`, and returns `Result`. + +Other `MockTransactionBuilder` methods: `tx_script`, `tx_script_args`, `auth_args`, +`extend_note_args`, `reference_block`, `foreign_accounts`, `extend_advice_inputs`, +`add_advice_map_entry`, `authenticator`, `add_signature`, `add_note_script`, `send_notes_script`, +`expected_output_note(s)`, `with_source_manager`. ### 9. Execute with Transaction Script + +`TransactionScript::from_package(&package)?` handles a `kind = "tx-script"` package directly: if the +package is a program it uses the entrypoint, otherwise it looks for the single procedure carrying +the `transaction_script` attribute, which the compiler emits on tx-script exports. + ```rust use miden_client::transaction::TransactionScript; -let tx_script_package = Arc::new(build_project_in_dir( - Path::new("../contracts/my-tx-script"), - true, -)?); -let program = tx_script_package.unwrap_program(); -let tx_script = TransactionScript::new((*program).clone()); +let tx_script = TransactionScript::from_package(&tx_script_package)?; let executed = mock_chain - .build_tx_context(account.clone(), &[], &[])? + .build_transaction(account.id()) .tx_script(tx_script) .build()? .execute() @@ -129,96 +275,168 @@ mock_chain.prove_next_block()?; let updated_account = mock_chain.committed_account(account.id())?; ``` +`TransactionScript::from_parts(Arc, MastNodeId)` exists, but it is not the path for +compiler-produced tx-script packages — use the package-based construction shown above. + ### 10. Verify Storage State -See [counter_test.rs](../../../integration/tests/counter_test.rs) lines 84-96 for reading the committed account state and asserting on the result. +Read state with `account.storage().get_item(&slot)` or +`account.storage().get_map_item(&slot, key)` on an in-memory `Account` you keep patch-current, or +re-fetch the committed account with `mock_chain.committed_account(account.id())?` after +`prove_next_block()`. + +> `get_map_item(&self, slot_name: &StorageSlotName, key: StorageMapKey)` takes a **`StorageMapKey` +> by value**, not a `Word`. Build one with `StorageMapKey::new(word)`, or `StorageMapKey::empty()` / +> `StorageMapKey::from_index(idx)` for the degenerate cases. + +`AccountStorage` exposes scalar felt values in `[felt, 0, 0, 0]` layout. + +`FungibleAsset::amount()` returns an **`AssetAmount`**, not a `u64` — an assertion comparing it to a +bare integer will not compile. Use `AssetAmount::new(expected)?` or `.as_u64()`. ### 11. Verify Output Notes -**Important**: `add_output_note()` is only available on `MockChainBuilder` (before `build()`); use it to seed the chain with existing notes. To verify output notes from a transaction, use `extend_expected_output_notes()` on `TxContextBuilder`: +`add_output_note()` is only on `MockChainBuilder` (before `build()`) — use it to seed the chain with +existing notes. To assert on notes a transaction *produces*, use `expected_output_note(..)` / +`expected_output_notes(..)` on `MockTransactionBuilder`: ```rust -use miden_client::{note::{Note, NoteAssets, NoteMetadata, NoteRecipient}, transaction::RawOutputNote}; +use miden_client::{ + note::{Note, NoteType, PartialNoteMetadata}, + transaction::RawOutputNote, +}; -let expected_note = Note::new(expected_assets, expected_metadata, expected_recipient); +let partial_metadata = PartialNoteMetadata::new(sender, NoteType::Public).with_tag(tag); +let expected_note = Note::new(expected_assets, partial_metadata, expected_recipient); -let tx_context = mock_chain - .build_tx_context(account.id(), &[note.id()], &[])? - .extend_expected_output_notes(vec![RawOutputNote::Full(expected_note)]) - .build()?; - -// execute() will verify output notes match -let executed = tx_context.execute().await?; +let executed = mock_chain + .build_transaction(account.id()) + .authenticated_input_note(note.id()) + .expected_output_note(RawOutputNote::Full(expected_note)) + .build()? + .execute() + .await?; ``` +> Both `expected_output_note(s)` silently **drop** `RawOutputNote::Partial` entries — only `Full` +> notes are retained and checked. + +> Note metadata: +> - `Note::new(assets, partial_metadata, recipient)` takes a `PartialNoteMetadata` (sender/type/tag +> only), is infallible, and has no `Into` conversion on that parameter. +> - `PartialNoteMetadata::new(sender, note_type)` defaults the tag to `NoteTag::default()`; set one +> with `.with_tag(tag)` or `set_tag(..)`. +> - For attachment-bearing notes use +> `Note::with_attachments(assets, partial_metadata, recipient, attachments)`. + ## MockChain Note Interaction Notes flow through MockChain in four steps: 1. **Build** the note from a compiled `.masp` package (see "Note Construction" below) or via `NoteBuilder`. -2. **Seed** with `MockChainBuilder::add_output_note(RawOutputNote::Full(note.clone()))` BEFORE `builder.build()`. This places the note on the chain so a later transaction can consume it. `add_output_note(...)` is only available on the builder; once `builder.build()` returns the `MockChain`, output notes can only appear as the result of executing a transaction. `RawOutputNote` is re-exported from `miden_client::transaction`. -3. **Consume** by passing the note ID to `mock_chain.build_tx_context(account, &[note.id()], &[])`. The transaction's note-script execution reads the consumed note's storage and assets. -4. **Verify** expected output notes with `.extend_expected_output_notes(vec![RawOutputNote::Full(expected.clone())])` on the `TxContextBuilder`. `tx_context.execute().await?` will assert the produced output notes match. +2. **Seed** with `MockChainBuilder::add_output_note(RawOutputNote::Full(note.clone()))` BEFORE `builder.build()`. This places the note on the chain so a later transaction can consume it. `add_output_note(...)` is only available on the builder; once `builder.build()` returns the `MockChain`, output notes appear as transaction results. +3. **Consume** through `mock_chain.build_transaction(account).authenticated_input_note(note.id())` for chain-known notes, or `.unauthenticated_input_note(note.clone())` / `.unauthenticated_input_notes(..)` when the full note must be supplied. +4. **Verify** expected output notes with `.expected_output_note(RawOutputNote::Full(expected_note))` or `.expected_output_notes(..)` on the `MockTransactionBuilder`. `execute().await?` asserts that the produced output notes match. -After `execute()` and before `add_pending_executed_transaction(...) + prove_next_block()`: if a later step will keep using the in-memory `Account` variable (for example, to build another `tx_context` or assert account state directly), call `account.apply_delta(&executed.account_delta())?` to keep the variable in sync with the chain. Post-block reads should use `mock_chain.committed_account(account.id())?` (see Step 8 above and "Multi-Transaction Test Pattern" below). For block advancement and reference-block semantics, see "MockChain Block Numbering" below. +After `execute()` and before `add_pending_executed_transaction(...) + prove_next_block()`: if a later step will keep using the same in-memory `Account` variable, call `account.apply_patch(executed.account_patch())?` to keep the variable in sync with the chain. Post-block reads should use `mock_chain.committed_account(account.id())?`. For block advancement and reference-block semantics, see "MockChain Block Numbering" below. -End-to-end multi-note example: see [miden-bank withdraw_test.rs](https://github.com/0xMiden/tutorials/blob/main/examples/miden-bank/integration/tests/withdraw_test.rs) for seeding deposit + withdraw-request notes via `add_output_note(RawOutputNote::Full(...))` before `builder.build()`, then consuming the withdraw-request note and asserting an expected P2ID output note via `extend_expected_output_notes(...)` plus `prove_next_block()`. See [miden-bank deposit_test.rs](https://github.com/0xMiden/tutorials/blob/main/examples/miden-bank/integration/tests/deposit_test.rs) for the simpler single-note consume + prove cycle. +See [counter_test.rs](../../../integration/tests/counter_test.rs) for the current project-template note consume + prove cycle. The miden-bank [withdraw_test.rs](https://github.com/0xMiden/tutorials/blob/main/examples/miden-bank/integration/tests/withdraw_test.rs) and [deposit_test.rs](https://github.com/0xMiden/tutorials/blob/main/examples/miden-bank/integration/tests/deposit_test.rs) remain useful for multi-note flow and note-input layout, but they use older `build_tx_context(...)` / `OutputNote` naming; translate those examples to the current `build_transaction(...)` / `RawOutputNote` APIs shown above. ## Multi-Transaction Test Pattern -For contracts requiring initialization before use, each step usually needs its own `execute()` → `add_pending_executed_transaction()` → `prove_next_block()` cycle. Fetch the committed account or note state from `mock_chain` between steps before building the next context. +For contracts requiring initialization before use, each step usually needs its own `execute()` → +`add_pending_executed_transaction()` → `prove_next_block()` cycle. + +When you keep reading from and reusing the **same in-memory `Account`** across transactions, apply +the account patch after every `execute()` so later local reads see the new state: -Call `account.apply_delta(&executed.account_delta())?` after each `execute()` to keep the in-memory `Account` variable in sync with the chain whenever later steps reuse it (for `build_tx_context(...)`, asserting account state directly, etc.). The miden-bank tutorial tests follow this pattern between every `execute()` and `prove_next_block()`. If the test only reads final state via `mock_chain.committed_account(...)` after the last `prove_next_block()` and never reuses the in-memory variable, `apply_delta` is unnecessary; see [counter_test.rs](../../../integration/tests/counter_test.rs). +```rust +bank_account.apply_patch(executed.account_patch())?; +``` -See [miden-bank withdraw_test.rs](https://github.com/0xMiden/tutorials/blob/main/examples/miden-bank/integration/tests/withdraw_test.rs) for a complete multi-transaction test demonstrating: initialize bank → deposit assets → withdraw assets (3 sequential transactions with state verification between each step). +`Account::apply_patch(&AccountPatch)` and `ExecutedTransaction::account_patch() -> &AccountPatch` +are the account-update path. If you instead re-fetch via `mock_chain.committed_account(..)` after +`prove_next_block()`, you can skip the patch entirely. -See [miden-bank deposit_test.rs](https://github.com/0xMiden/tutorials/blob/main/examples/miden-bank/integration/tests/deposit_test.rs) for an end-to-end asset-bearing note test. +> **The one exception that catches people out:** `TransactionSummary::account_delta()` still returns +> a relative `AccountDelta` and is deliberately *not* a patch. A blanket rename of `account_delta` +> to `account_patch` breaks that call site. ## MockChain Block Numbering -Genesis is block 0. Each `prove_next_block()` advances the block number by 1. In contract code, `tx::get_block_number()` returns the **reference block**: the last proven block at the time the transaction started, not the block the transaction will be included in. +Genesis is block 0. Each `prove_next_block()` advances the block number by 1; `prove_next_block_at(timestamp)` +does the same at a chosen timestamp. In contract code, `tx::get_block_number()` returns the +**reference block** — the last proven block at the time the transaction started, not the block the +transaction will be included in. + +> `tx::get_block_number()` returns a **`BlockNumber`**, not a `Felt`. Compare it directly against +> another `BlockNumber`; convert a felt read out of note storage with `BlockNumber::try_from(felt)`. ## Note Construction -Prefer `NoteBuilder` (or mirror its logic with compiled `.masp` package files) for creating notes in tests. Start from `NoteBuilder::new(sender.id(), &mut note_rng)`, then configure `.package(...)`, optional `.add_assets(...)`, optional `.note_storage(...)`, and finally `.build()`. See [counter_test.rs](../../../integration/tests/counter_test.rs) for the working pattern. +Prefer `NoteBuilder` for creating notes in tests. Start from `NoteBuilder::new(sender.id(), &mut note_rng)`, then configure `.package(...)`, optional `.add_assets(...)`, optional `.note_storage(...)`, and finally `.build()`. See [counter_test.rs](../../../integration/tests/counter_test.rs) and Step 6 above for the working local pattern. ### Building Notes from `.masp` Packages -When a test or binary needs full control over the note (custom storage Felts, deterministic serial number, P2ID-style metadata, or a real-client publish + consume flow), build directly from a compiled `.masp` package. The canonical pipeline is `NoteScript::from_package(package.as_ref())` paired with `NoteBuilder::new(sender_id, &mut RandomCoin::new(note_script.root())).package((*package).clone()).note_type(...).tag(...).add_assets(...).note_storage(...).serial_number(...).build()`. Project-template's own [counter_test.rs](../../../integration/tests/counter_test.rs) follows this same shape with `NoteBuilder` directly. +When a test or binary needs full control over the note, start with the compiled `.masp` package and construct the note script from the package. The current project-template path is `NoteScript::from_package(package.as_ref())`, seed a `RandomCoin` from `Word::from(note_script.root())`, and pass the package into `NoteBuilder::package((*package).clone())` before adding assets, storage inputs, type, tag, serial number, or attachments. -The miden-bank tutorial codifies this as two helpers built on top of `NoteScript::from_package` + `NoteBuilder`: +The miden-bank tutorial codifies a lower-level variant in helpers built around `NoteScript::from_parts(...)`, `NoteInputs::new(config.inputs)`, `NoteRecipient::new(...)`, `NoteMetadata::new(...)`, and `Note::new(...)`: -- **Real-client path** (`create_note_from_package`): calls `client.rng().draw_word()` and threads it through `NoteBuilder::serial_number(...)` for a fresh per-note serial. Used when the note will be published via a real `TransactionRequestBuilder`. See [miden-bank helpers.rs](https://github.com/0xMiden/tutorials/blob/main/examples/miden-bank/integration/src/helpers.rs) (`create_note_from_package`). -- **Deterministic test path** (`create_testing_note_from_package`): omits `serial_number(...)`, letting `NoteBuilder` derive the serial deterministically from `RandomCoin::new(note_script.root())`. Used when seeding `MockChainBuilder` with a freshly-built note. See [miden-bank helpers.rs](https://github.com/0xMiden/tutorials/blob/main/examples/miden-bank/integration/src/helpers.rs) (`create_testing_note_from_package`). +- **Real-client path** (`create_note_from_package`): calls `client.rng().draw_word()` for a fresh per-note serial. Used when the note will be published via a real `TransactionRequestBuilder`. See [miden-bank helpers.rs](https://github.com/0xMiden/tutorials/blob/main/examples/miden-bank/integration/src/helpers.rs) (`create_note_from_package`). +- **Deterministic test path** (`create_testing_note_from_package`): uses a deterministic serial so tests can seed `MockChainBuilder` with a freshly built note. See [miden-bank helpers.rs](https://github.com/0xMiden/tutorials/blob/main/examples/miden-bank/integration/src/helpers.rs) (`create_testing_note_from_package`). -Both helpers take a `NoteCreationConfig` with four fields: `note_type: NoteType`, `tag: NoteTag`, `assets: NoteAssets`, `storage: Vec`. See [miden-bank helpers.rs](https://github.com/0xMiden/tutorials/blob/main/examples/miden-bank/integration/src/helpers.rs) (`NoteCreationConfig` struct + `Default` impl). To drive a cross-component note (see `rust-sdk-patterns` "Cross-Component Note Pattern"), populate `NoteCreationConfig.storage` with the serialized Felt representation of the typed note struct's fields in declaration order; the `#[note]` macro deserializes that slice into `self` before the script runs. +Both helpers take a `NoteCreationConfig` with `note_type`, `tag`, `assets`, `inputs`, `execution_hint`, and `aux`. See [miden-bank helpers.rs](https://github.com/0xMiden/tutorials/blob/main/examples/miden-bank/integration/src/helpers.rs) (`NoteCreationConfig` struct + `Default` impl). To drive a cross-component note (see `rust-sdk-patterns` "Cross-Component Note Pattern"), populate `NoteCreationConfig.inputs` with the serialized Felt representation expected by the note script. -Test-side example: see [miden-bank withdraw_test.rs](https://github.com/0xMiden/tutorials/blob/main/examples/miden-bank/integration/tests/withdraw_test.rs) for a storage vector reaching the note via `NoteCreationConfig { storage, ..Default::default() }` and the seeded `MockChainBuilder.add_output_note(RawOutputNote::Full(...))` call before `builder.build()`. +Test-side example: see [miden-bank withdraw_test.rs](https://github.com/0xMiden/tutorials/blob/main/examples/miden-bank/integration/tests/withdraw_test.rs) for an input vector reaching the note via `NoteCreationConfig { inputs, ..Default::default() }` and the seeded `MockChainBuilder.add_output_note(OutputNote::Full(...))` call before `builder.build()`. -Binary-side example: see [miden-bank deposit.rs](https://github.com/0xMiden/tutorials/blob/main/examples/miden-bank/integration/src/bin/deposit.rs) for `build_project_in_dir(...)` to produce the `.masp` package, `create_note_from_package(...)` to assemble the note, then `TransactionRequestBuilder::own_output_notes(vec![note.clone()])` and `input_notes([(note.clone(), None)])` to publish and consume. For the surrounding client setup (CLI side), see the `miden-client-cli` skill. +Binary-side example: see [miden-bank deposit.rs](https://github.com/0xMiden/tutorials/blob/main/examples/miden-bank/integration/src/bin/deposit.rs) for `build_project_in_dir(...)` to produce the `.masp` package, `create_note_from_package(...)` to assemble the note, then `TransactionRequestBuilder::own_output_notes(vec![OutputNote::Full(note.clone())])` and `unauthenticated_input_notes([(note.clone(), None)])` to publish and consume. For the surrounding client setup (CLI side), see the `miden-client-cli` skill. ## Asset-Bearing Note Example -To create a note that carries fungible assets in tests: - -1. Create a `FungibleAsset` from a faucet ID and amount. -2. Seed a `RandomCoin` from `NoteScript::from_package(note_package.as_ref())?.root()`. -3. Pass the asset into `NoteBuilder::add_assets(...)` and any note inputs into `note_storage(...)`. +1. Create a `FungibleAsset` from a faucet ID and amount, e.g. `FungibleAsset::new(faucet.id(), 50)?` + (the amount parameter is still `u64`), and wrap it into `NoteAssets::new(vec![Asset::Fungible(asset)])?` + — or pass it via `NoteBuilder::add_assets`. +2. Seed a `RandomCoin` from `Word::from(NoteScript::from_package(note_package.as_ref())?.root())`. +3. Pass any note inputs into `note_storage(...)?`, building each felt with the infallible + `Felt::from(_u32)` for in-range literals or `Felt::new_unchecked(n)` for `u64` inputs. 4. Finish with `.package((*note_package).clone()).build()?`. -The faucet must be set up first (see Step 3) and the sender wallet must hold sufficient assets (see Step 2). +The faucet must be set up first (see Step 3) and the sender wallet must hold sufficient assets +(see Step 2). ## Key Dependencies -See [integration/Cargo.toml](../../../integration/Cargo.toml) for the current dependency versions used in this project. +```toml +miden-client = { version = "0.16.0-rc.1", features = ["tonic"] } +miden-client-sqlite-store = { version = "0.16.0-rc.1", package = "miden-client-sqlite-store" } +miden-standards = { version = "0.16.0-rc.4", features = ["testing"] } +miden-testing = "0.16.0-rc.4" +miden-mast-package = { version = "0.29", default-features = false } +``` + +The contracts a test builds depend on the guest SDK `miden = { version = "0.14" }` and matching +`miden-sdk-build-script-support = { version = "0.14" }`, built on `nightly-2026-09-01` with target +`wasm32-wasip2`. See Step 4 for why `cargo-miden` must not be a library dependency of the test crate; +`build_project_in_dir(...)` invokes midenup / `CARGO_MIDEN` / `cargo miden` out of process. ## Validation Checklist - [ ] Test function is `async` and uses `#[tokio::test]` -- [ ] Storage slot names follow `package_or_name::component_struct::field_name` pattern -- [ ] All contracts built before account/note creation -- [ ] Account storage seeded via `InitStorageData` +- [ ] `miden-testing` is available as a direct dependency, or through `miden_client::testing` only when that optional client feature is enabled +- [ ] Auth uses `AuthSchemeId::Falcon512Poseidon2` (or the equivalent `AuthScheme::Falcon512Poseidon2`) +- [ ] `AccountBuilder` uses `.account_type(..)`, and no `.storage_mode(..)` and no `.with_auth_component(..)` — auth goes through `.with_component(..)` +- [ ] Storage slot names follow `::::` +- [ ] Value slots without a schema default are seeded via `InitStorageData::insert_value(StorageValueName::from_slot_name(&slot), ..)`; `StorageValue` slots get a `Word`, not a bare integer +- [ ] Contracts are built out of process with `build_project_in_dir(...)` / midenup / `CARGO_MIDEN` / `cargo miden`, not by depending on `cargo-miden` +- [ ] `NoteScript::root()` converted with `Word::from(..)` before seeding `RandomCoin` +- [ ] `NoteBuilder::tag(..)` is passed a `u32` +- [ ] Note-storage felts built with infallible `Felt::from(_u32)` or `Felt::new_unchecked(_u64)` +- [ ] `Note::new(..)` is passed a `PartialNoteMetadata` (not `NoteMetadata`) +- [ ] Transaction scripts built with `TransactionScript::from_package(&package)?` +- [ ] Execution goes through `chain.build_transaction(..)` with `.authenticated_input_note(..)` / `.unauthenticated_input_note(..)`, then `.build()?.execute().await?` - [ ] `prove_next_block()` called after `add_pending_executed_transaction()` -- [ ] Post-block assertions read state from `mock_chain.committed_account(...)` or other committed chain views -- [ ] Notes added to `MockChainBuilder` via `add_output_note(RawOutputNote::Full(...))` before `build()` +- [ ] In-memory accounts refreshed with `account.apply_patch(executed.account_patch())?`, and `TransactionSummary::account_delta()` left alone +- [ ] Map reads pass a `StorageMapKey`, not a `Word` +- [ ] `FungibleAsset::amount()` compared as `AssetAmount`, not a bare integer +- [ ] Notes added to `MockChainBuilder` via `add_output_note(RawOutputNote::Full(..))` before `build()` (no `?` — it returns `()`) - [ ] Faucet set up before creating assets