diff --git a/.github/workflows/cut-versions.yml b/.github/workflows/cut-versions.yml index f94fbc11..6cad2933 100644 --- a/.github/workflows/cut-versions.yml +++ b/.github/workflows/cut-versions.yml @@ -71,9 +71,21 @@ jobs: - name: Install deps run: npm ci + - name: Test snapshot workflow + run: node --test scripts/cut-versions.test.mjs + - name: Parse release manifest (and merge with manual inputs) id: manifest env: + INPUT_VERSION_LABEL: ${{ inputs.version_label }} + INPUT_MIDEN_NODE_REF: ${{ inputs.miden_node_ref }} + INPUT_MIDEN_VM_REF: ${{ inputs.miden_vm_ref }} + INPUT_MIDEN_BASE_REF: ${{ inputs.miden_base_ref }} + INPUT_MIDEN_CLIENT_REF: ${{ inputs.miden_client_ref }} + INPUT_COMPILER_REF: ${{ inputs.compiler_ref }} + INPUT_MIDEN_TUTORIALS_REF: ${{ inputs.miden_tutorials_ref }} + INPUT_GUARDIAN_REF: ${{ inputs.guardian_ref }} + INPUT_NOTE_TRANSPORT_REF: ${{ inputs.note_transport_ref }} INPUT_BRIDGE_PORTAL_REF: ${{ inputs.bridge_portal_ref }} run: | node - <<'NODE' @@ -113,22 +125,45 @@ jobs: process.exit(1); } - // Emit outputs - for (const [k,v] of Object.entries(out)) { - console.log(`::set-output name=${k}::${v}`); + // A locally generated snapshot may already accompany the manifest push. + // Skip generation only when all three snapshot artifacts are present. + const versions = JSON.parse(read('versions.json') || '[]'); + const existing = [ + versions.includes(out.version_label), + fs.existsSync(`versioned_docs/version-${out.version_label}`), + fs.existsSync(`versioned_sidebars/version-${out.version_label}-sidebars.json`), + ]; + if (existing.some(Boolean) && !existing.every(Boolean)) { + console.error(`Incomplete snapshot for ${out.version_label}; refusing to overwrite it.`); + process.exit(1); } + out.snapshot_exists = String(existing.every(Boolean)); + + // Emit simple, single-line outputs through the supported Actions file. + const output = Object.entries(out).map(([key, value]) => `${key}=${value}`).join('\n'); + fs.appendFileSync(process.env.GITHUB_OUTPUT, `${output}\n`); NODE - name: Validate refs exist + env: + GH_TOKEN: ${{ github.token }} run: | set -e validate() { repo="$1"; ref="$2" echo "Validating $repo @ $ref" - git ls-remote "https://github.com/$repo" "$ref" | grep -q . || { - echo "ERROR: Ref '$ref' not found in $repo" - exit 1 - } + if [[ "$ref" =~ ^[0-9a-f]{40}$ ]]; then + resolved=$(gh api "repos/$repo/git/commits/$ref" --jq '.sha') || exit 1 + [[ "$resolved" == "$ref" ]] || { + echo "ERROR: Commit '$ref' did not resolve exactly in $repo" + exit 1 + } + else + git ls-remote "https://github.com/$repo" "$ref" | grep -q . || { + echo "ERROR: Ref '$ref' not found in $repo" + exit 1 + } + fi } validate "${REPO_NODE}" "${{ steps.manifest.outputs.miden_node_ref }}" validate "${REPO_VM}" "${{ steps.manifest.outputs.miden_vm_ref }}" @@ -141,6 +176,7 @@ jobs: validate "${REPO_BRIDGE_PORTAL}" "${{ steps.manifest.outputs.bridge_portal_ref }}" - name: Checkout sources at validated refs + if: steps.manifest.outputs.snapshot_exists != 'true' run: | set -e checkout_ref() { @@ -167,6 +203,7 @@ jobs: # This content is cleaned up after versioning (see "Clean up" step below) # ============================================================ - name: Aggregate docs into v0.4 IA structure + if: steps.manifest.outputs.snapshot_exists != 'true' run: | echo "Aggregating vendor docs into v0.4 IA structure..." @@ -333,6 +370,7 @@ jobs: ls -la docs/builder/tutorials/ || true - name: Cut version snapshot + if: steps.manifest.outputs.snapshot_exists != 'true' run: | VERSION="${{ steps.manifest.outputs.version_label }}" echo "Creating Docusaurus version: $VERSION" @@ -346,6 +384,7 @@ jobs: # docs/ must only contain authored content (builder/, reference/ landing pages) # All ingested content lives in versioned_docs/ after versioning - name: Clean up (delete vendor and ingested docs) + if: steps.manifest.outputs.snapshot_exists != 'true' run: | rm -rf vendor # Guardian: remove ingested subdirs (keep authored index.md); note-transport (fully ingested) @@ -379,6 +418,7 @@ jobs: rm -rf docs/builder/tutorials/recipes/img - name: Commit snapshots + if: steps.manifest.outputs.snapshot_exists != 'true' run: | if ! git diff --quiet; then git config user.name "github-actions[bot]" diff --git a/.release/release-manifest.yml b/.release/release-manifest.yml index 17cad613..bbea29b4 100644 --- a/.release/release-manifest.yml +++ b/.release/release-manifest.yml @@ -1,11 +1,12 @@ -version: "0.15" # the label to snapshot (used if no manual override) -refs: # exact, immutable refs per source repo (tags or release branches) - protocol: "refs/tags/v0.15.3" - node: "refs/tags/v0.15.0" - miden-client: "refs/tags/v0.15.2" - tutorials: "refs/heads/main" - miden-vm: "refs/tags/v0.23.4" - compiler: "refs/tags/v0.8.1" - guardian: "refs/tags/v0.15.0" - note-transport: "refs/tags/v0.4.1" -next_version: "0.16" +version: "0.16" +refs: # Immutable documentation source commits, resolved on 2026-09-14. + protocol: "fb1e40da0f7cec156963e5d815a51a18a8313f25" # v0.16.1 + node: "d6ce8b14d4680e0187b877c1de5c1cdeea16e7e2" # v0.16.0 + miden-client: "5f36964d2655549e600f55ab6f65caea943d1130" # rust-sdk v0.16.1 + tutorials: "965739b626acf3c63a0e547d313859079ee03c18" # v0.16 migration + NFT recipe (#249, #250) + miden-vm: "c1c2ddcb9cabc3d5c2071f8baf68c7930917cad8" # v0.29.2, the protocol v0.16 VM line + compiler: "f2e0b4274f4f13caeceb215645bd3f1f03a3c6f5" # v0.10.1; still pins protocol v0.16.0-rc.4 + guardian: "be66822395551c630a164f7bdafbb67771c83442" # v0.17.0, built for Miden v0.16 + note-transport: "9736b39d64cfb4d77bb2d777a9ad1a4f2ea3f2a5" # v0.5.0-rc.2, used by rust-sdk v0.16 + bridge-portal: "5d8f327a8adea0e0d671b798145b6092f5e23d6a" # Developer docs #117; portal still uses SDK v0.15 +next_version: "0.17" diff --git a/docs/builder/get-started/setup/installation.md b/docs/builder/get-started/setup/installation.md index 23be8a67..e153951a 100644 --- a/docs/builder/get-started/setup/installation.md +++ b/docs/builder/get-started/setup/installation.md @@ -128,13 +128,13 @@ toolchain for the network you use. `midenup` resolves network names through the [published channel manifest](https://0xmiden.github.io/midenup/channel-manifest.json). :::warning Release prerequisite -The published manifest currently maps `testnet` to the v0.15 channel. The -explicit `0.16.0` channel is not a substitute: it still mixes prerelease client -and protocol components. Neither channel is a verified setup for these v0.16 -testnet guides, and `devnet` is a different network. +The stable v0.16 toolchain update is ready on midenup's `next` branch, but it +has not yet been promoted to the published manifest. The published `testnet` +channel still points to v0.15, while its explicit `0.16.0` channel is the older +prerelease stack. -Do not continue with the network-dependent guides until `testnet` points to a -coherent v0.16 release channel. +Wait for the updated manifest to be published before using these +network-dependent v0.16 guides against testnet. ::: After the manifest meets that requirement, install the public testnet toolchain diff --git a/scripts/cut-versions.test.mjs b/scripts/cut-versions.test.mjs new file mode 100644 index 00000000..74057a75 --- /dev/null +++ b/scripts/cut-versions.test.mjs @@ -0,0 +1,105 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { spawnSync } from "node:child_process"; +import { createRequire } from "node:module"; +import vm from "node:vm"; +import test from "node:test"; +import YAML from "yaml"; + +const require = createRequire(import.meta.url); +const workflow = YAML.parse(readFileSync(new URL("../.github/workflows/cut-versions.yml", import.meta.url), "utf8")); +const steps = workflow.jobs.cut.steps; +const parser = steps.find((step) => step.id === "manifest"); +const parseCode = parser.run.match(/node - <<'NODE'\n([\s\S]*?)\nNODE/)[1]; + +function parse({ inputs = {}, present = [] } = {}) { + const outputs = {}; + const env = Object.fromEntries(Object.entries(parser.env || {}).map(([key, value]) => { + const input = value.match(/inputs\.([a-z_]+)/)?.[1]; + return [key, inputs[input] || ""]; + })); + env.GITHUB_OUTPUT = "__outputs__"; + const files = { + ".release/release-manifest.yml": "version: '0.16'\nrefs:\n tutorials: pinned-tutorial-commit\n", + "versions.json": JSON.stringify(present.includes("version-list") ? ["0.16", "0.15"] : ["0.15"]), + }; + const paths = new Set(Object.keys(files)); + if (present.includes("directory")) paths.add("versioned_docs/version-0.16"); + if (present.includes("sidebar")) paths.add("versioned_sidebars/version-0.16-sidebars.json"); + vm.runInNewContext(parseCode, { + require: (name) => name === "fs" ? { + existsSync: (path) => paths.has(path), + readFileSync: (path) => { assert.ok(path in files, `unexpected read: ${path}`); return files[path]; }, + appendFileSync: (path, contents) => { + assert.equal(path, env.GITHUB_OUTPUT); + for (const line of contents.trimEnd().split("\n")) { + const separator = line.indexOf("="); + assert.ok(separator > 0, `invalid output line: ${line}`); + outputs[line.slice(0, separator)] = line.slice(separator + 1); + } + }, + } : require(name), + process: { env, exit: (code) => { throw new Error(`exit ${code}`); } }, + console: { error() {}, log() {} }, + }); + return outputs; +} + +test("manifest outputs use GITHUB_OUTPUT instead of the deprecated command", () => { + assert.match(parseCode, /GITHUB_OUTPUT/); + assert.doesNotMatch(parseCode, /::set-output/); +}); + +test("manual version and source overrides reach the parser instead of silently using the manifest", () => { + const outputs = parse({ inputs: { + version_label: "0.99", miden_node_ref: "node-pin", miden_vm_ref: "vm-pin", + miden_base_ref: "protocol-pin", miden_client_ref: "client-pin", compiler_ref: "compiler-pin", + miden_tutorials_ref: "tutorial-pin", guardian_ref: "guardian-pin", + note_transport_ref: "transport-pin", bridge_portal_ref: "bridge-pin", + } }); + assert.equal(outputs.version_label, "0.99"); + for (const [key, expected] of Object.entries({ miden_node_ref: "node-pin", miden_vm_ref: "vm-pin", + miden_base_ref: "protocol-pin", miden_client_ref: "client-pin", compiler_ref: "compiler-pin", + miden_tutorials_ref: "tutorial-pin", guardian_ref: "guardian-pin", note_transport_ref: "transport-pin", + bridge_portal_ref: "bridge-pin" })) assert.equal(outputs[key], expected); +}); + +test("absent snapshot is eligible for generation and uses manifest defaults", () => { + const outputs = parse(); + assert.equal(outputs.version_label, "0.16"); + assert.equal(outputs.miden_tutorials_ref, "pinned-tutorial-commit"); + assert.equal(outputs.snapshot_exists, "false"); +}); + +test("complete checked-in snapshot validates refs but skips generation and cleanup", () => { + const outputs = parse({ present: ["version-list", "directory", "sidebar"] }); + assert.equal(outputs.snapshot_exists, "true"); + const validationStep = steps.find((step) => step.name === "Validate refs exist"); + assert.equal(validationStep.if, undefined, "source refs must still be validated"); + for (const step of steps.slice(steps.indexOf(validationStep) + 1)) { + const condition = (step.if || "true").replace(/\$\{\{|\}\}/g, "") + .replaceAll("steps.manifest.outputs.snapshot_exists", JSON.stringify(outputs.snapshot_exists)); + assert.equal(vm.runInNewContext(condition), false, `${step.name} would run on an existing snapshot`); + } +}); + +for (const present of [["directory"], ["version-list"], ["sidebar"], ["version-list", "directory"]]) { + test(`partial snapshot is rejected: ${present.join("+")}`, () => assert.throws(() => parse({ present }))); +} + +const validation = steps.find((step) => step.name === "Validate refs exist").run.split('\nvalidate "${REPO_NODE}"')[0]; +const sha = "965739b626acf3c63a0e547d313859079ee03c18"; +function validate(ref, apiSha = sha) { + const boundary = ` +git() { if [[ "$3" == "refs/tags/v0.16.0" ]]; then echo "${sha} refs/tags/v0.16.0"; fi; } +gh() { [[ "$1" == "api" && "$2" == "repos/0xMiden/tutorials/git/commits/${sha}" ]] || return 1; [[ -n "$API_SHA" ]] || return 1; echo "$API_SHA"; } +`; + return spawnSync("bash", ["-c", `${boundary}\n${validation}\nvalidate 0xMiden/tutorials "$TEST_REF"`], { + env: { ...process.env, TEST_REF: ref, API_SHA: apiSha }, encoding: "utf8", + }); +} +test("an existing exact commit pin is accepted", () => assert.equal(validate(sha).status, 0)); +test("a missing exact commit is rejected", () => assert.notEqual(validate(sha, "").status, 0)); +test("a mismatched commit response is rejected", () => assert.notEqual(validate(sha, "0".repeat(40)).status, 0)); +test("normal tag validation still works", () => assert.equal(validate("refs/tags/v0.16.0").status, 0)); +test("an unknown tag is rejected", () => assert.notEqual(validate("refs/tags/missing").status, 0)); diff --git a/sidebars.ts b/sidebars.ts index 86fb741f..0ed69b85 100644 --- a/sidebars.ts +++ b/sidebars.ts @@ -149,6 +149,7 @@ const sidebars: SidebarsConfig = { "builder/tutorials/recipes/rust/counter_contract_tutorial", "builder/tutorials/recipes/rust/create_deploy_tutorial", "builder/tutorials/recipes/rust/mint_consume_create_tutorial", + "builder/tutorials/recipes/rust/nft_mint_transfer", "builder/tutorials/recipes/rust/creating_notes_in_masm_tutorial", "builder/tutorials/recipes/rust/custom_note_how_to", "builder/tutorials/recipes/rust/unauthenticated_note_how_to", diff --git a/versioned_docs/version-0.16/builder/_category_.json b/versioned_docs/version-0.16/builder/_category_.json new file mode 100644 index 00000000..34d2480b --- /dev/null +++ b/versioned_docs/version-0.16/builder/_category_.json @@ -0,0 +1,6 @@ +{ + "label": "Builder", + "position": 1, + "collapsible": false, + "collapsed": false +} diff --git a/versioned_docs/version-0.16/builder/faq.md b/versioned_docs/version-0.16/builder/faq.md new file mode 100644 index 00000000..e007a20c --- /dev/null +++ b/versioned_docs/version-0.16/builder/faq.md @@ -0,0 +1,95 @@ +# FAQ + +## How is privacy implemented in Miden? + +Miden leverages zero-knowledge proofs and client side execution and proving to provide security and privacy. + +## Does Miden support encrypted notes? + +At the moment, Miden does not have support for encrypted notes but it is a planned feature. + +## Why does Miden have delegated proving? + +Miden leverages delegated proving for a few technical and practical reasons: + +1. **Computational:** Generating zero-knowledge proofs is a computationally intensive work. The proving process requires significant processing power and memory, making it impractical for some end-user devices (like smartphones) to generate. +2. **Technical architecture**: +Miden's architecture separates concerns between: + - **Transaction Creation**: End users create and sign transactions + - **Proof Generation**: Specialized provers generate validity proofs + - **Verification**: The network verifies these proofs +3. **Proving efficiency**: +Delegated provers can use optimized hardware that wouldn't be available to end-user devices, specifically designed for the mathematical operations needed in STARK proof generation. + +## What is the lifecycle of a transaction? + +For a client-executed transaction: + +### 1. Transaction Creation + +- User creates a transaction specifying the operations to perform (transfers, contract interactions, etc.) +- Client loads the account state and input notes needed for execution + +### 2. Transaction Execution + +- The Miden VM executes the note and transaction scripts locally +- The resulting state transitions and execution trace are computed +- During authentication, the account pays the protocol fee through a public `TX_FEE` note and authorizes the transaction summary + +### 3. Proof Generation + +- The client or a delegated prover generates a cryptographic proof attesting to the correctness of the execution + +### 4. Transaction Submission + +- The proven transaction, sealed inputs, and public state updates are submitted to Miden network nodes +- The node verifies the proof and admission rules before accepting the transaction into the mempool + +### 5. Transaction Selection + +- Accepted transactions are selected from the mempool +- Transactions are grouped into batches for inclusion in a block + +### 6. Block Production + +- Transaction batches and their proofs are assembled into a block +- The client synchronizes to observe the transaction's inclusion and updated state + +### 7. L1 Submission + +- Block proofs are aggregated for settlement +- The resulting proof and state commitments are submitted for L1 verification + +### 8. Finalization + +- L1 settlement establishes finality for the submitted state +- Inclusion in a Miden block does not by itself establish L1 finality + +## Do notes in Miden support recency conditions? + +Yes, Miden enables consumption of notes based on time conditions, such as: + +- A specific block height being reached +- A timestamp threshold being passed +- An oracle providing specific data +- Another transaction being confirmed + +## What does a Miden operator do in Miden? + +A Miden operator is an entity that maintains the infrastructure necessary for the functioning of the Miden layer 2. Their roles may involve: + +1. Running Sequencer Nodes +2. Operating the Prover Infrastructure +3. Submitting Proofs to L1 +4. Maintaining Data Availability +5. Participating in the Consensus Mechanism + +## How does bridging work in Miden? + +Miden testnet applications can integrate [Agglayer or Epoch](./tools/bridging/). +Agglayer provides canonical bridge semantics for ETH, while Epoch provides a +faster quote-and-solve flow for test USDC. Both integrations are testnet-only. + +## What does the gas fee model of Miden look like? + +Miden does not meter gas linearly. Its transaction fee grows logarithmically with the transaction's estimated VM cycles. During authentication, the account pays by creating a public `TX_FEE` note that the batch builder can collect. See [Transaction Fees](./smart-contracts/transactions/fees) for the formula, payment assets, and network-account sponsorship fees. diff --git a/versioned_docs/version-0.16/builder/get-started/_category_.json b/versioned_docs/version-0.16/builder/get-started/_category_.json new file mode 100644 index 00000000..f230021f --- /dev/null +++ b/versioned_docs/version-0.16/builder/get-started/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "Get Started", + "position": 1, + "link": { + "type": "doc", + "id": "builder/get-started/index" + } +} diff --git a/versioned_docs/version-0.16/builder/get-started/accounts.md b/versioned_docs/version-0.16/builder/get-started/accounts.md new file mode 100644 index 00000000..bce7e4ac --- /dev/null +++ b/versioned_docs/version-0.16/builder/get-started/accounts.md @@ -0,0 +1,400 @@ +--- +sidebar_position: 2 +title: Accounts +description: Learn how to create and manage Miden accounts programmatically using Rust and TypeScript. +--- + +# Accounts + +Miden's account model is fundamentally different from traditional blockchains. Let's explore how to create and manage accounts programmatically. + +## Understanding Miden Accounts + +Before diving into account creation, it's essential to understand what makes Miden accounts unique compared to traditional blockchain addresses. + +**What Makes Miden Accounts Special:** + +- **Smart Contract Wallets**: Every account is a programmable smart contract that can hold assets and execute custom logic +- **Modular Design**: Accounts are composed of reusable components (authentication, wallet functionality, etc.) +- **Privacy Levels**: Choose whether the full account state is public or privately held + +Miden accounts differ from traditional blockchain addresses in fundamental ways. + +**Account Architecture:** + +- Every account is a **smart contract** with programmable logic +- Accounts can store **assets** (fungible and non-fungible tokens) in their vault +- Each account has **storage slots** for custom data +- Accounts are composed of **modular components** for different functionalities + +**Account-state visibility:** + +- **Public**: All state visible onchain (transparent operations) +- **Private**: Only commitments onchain, full state held privately + +### Account Structure + +Every Miden account contains these core components: + +- **Vault**: Secure asset storage +- **Storage**: Key-value data store (up to 255 slots) +- **Code**: Smart contract logic +- **Nonce**: Anti-replay counter +- **Components**: Modular functionality (authentication, wallet, etc.) + +
+Account Struct + +```rust +pub struct MidenAccount { + /// Immutable, 120-bit ID encoding account visibility and version. + pub id: [u8; 15], + + /// Determines whether account state is public or private. + pub account_type: AccountType, + + /// Root commitment of the account CODE (MAST root). + pub code_commitment: [u8; 32], + + /// Root commitment of the account STORAGE (slots / maps). + /// Think of this as the "root hash" of the Account's storage merkle tree. + pub storage_commitment: [u8; 32], + + /// Vault commitment. For compact headers we keep only an optional aggregate commitment. + /// Indexers can materialize a richer view (e.g., list of assets) offchain. + pub vault_commitment: Option<[u8; 32]>, + + /// Monotonically increasing counter; must increment exactly once when state changes. + pub nonce: u64, + + /// Merged set of account components that defined this account's interface and storage. + /// Accounts are composed by merging components (e.g. wallet component + an auth component). + pub components: Vec, + + /// Authentication procedure metadata (e.g. "Falcon512Poseidon2"). + pub authentication: AuthenticationDescriptor, +} +``` + +**Account Types:** + +```rust +pub enum AccountType { + /// Account state is held privately by the owner. + Private, + /// Account state is visible onchain. + Public, +} +``` + +Wallet, faucet, and custom-contract roles come from the account's components and creation options, not from the account ID. + +
+ +## Set Up Development Environment + +To run the code examples in this guide, you'll need to set up a development environment for either Rust or TypeScript. + +### Rust Environment + +If you already created `my-test-project` during [installation](./setup/installation#rust-project), you can reuse it. Otherwise, create a new project: + +```bash title=">_ Terminal" +miden new my-project +cd my-project/integration/ +``` + +For each code example, create a new binary file: + +```bash title=">_ Terminal" +touch src/bin/demo.rs +``` + +Copy the Rust code example into the file, then run: + +```bash title=">_ Terminal" +cargo run --bin demo --release +``` + +### TypeScript Environment + +If you already created `miden-app` during [installation](./setup/installation#typescript-project), you can reuse it. Otherwise, scaffold a new Vite vanilla-ts project: + +```bash title=">_ Terminal" +npm create vite@latest miden-app -- --template vanilla-ts +cd miden-app +npm install @miden-sdk/miden-sdk@^0.16.0 +``` + +For each code example, save the TypeScript snippet as `src/demo.ts` (overwriting the previous one as you progress): + +```bash title=">_ Terminal" +touch src/demo.ts +``` + +Wire it into the entry point once (`src/main.ts`): + +```ts title="src/main.ts" +import { demo } from "./demo"; + +demo().catch(console.error); +``` + +Run the dev server: + +```bash title=">_ Terminal" +npm run dev +``` + +Open the dev-server URL in the browser and watch the devtools console for output. + +:::tip +For detailed frontend setup guidance (React, wallets, UI), see the [Tutorials section](../tutorials/). +::: + +## Creating Accounts Programmatically + +Let's start by creating accounts using the Miden client libraries: + +```rust title="integration/src/bin/account.rs" +use miden_client::{ + account::{ + component::{AuthScheme, AuthSingleSig, BasicWallet}, + AccountBuilder, AccountType, + }, + auth::{Approver, AuthSecretKey}, + builder::ClientBuilder, + keystore::{FilesystemKeyStore, Keystore}, + rpc::Endpoint, +}; +use miden_client_sqlite_store::ClientBuilderSqliteExt; +use rand::Rng; +use std::sync::Arc; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + // Initialize RPC connection + let endpoint = Endpoint::testnet(); + let timeout_ms = 10_000; + + // Initialize keystore + let keystore_path = std::path::PathBuf::from("./keystore"); + let keystore = + Arc::new(FilesystemKeyStore::new(keystore_path).unwrap()); + + let store_path = std::path::PathBuf::from("./store.sqlite3"); + + // Initialize client to connect with the Miden Testnet. + // NOTE: The client is our entry point to the Miden network. + // All interactions with the network go through the client. + let mut client = ClientBuilder::new() + .grpc_client(&endpoint, Some(timeout_ms)) + .sqlite_store(store_path) + .authenticator(keystore.clone()) + .build() + .await?; + + client.sync_state().await?; + + let mut init_seed = [0_u8; 32]; + client.rng().fill_bytes(&mut init_seed); + + let key_pair = AuthSecretKey::new_falcon512_poseidon2(); + + let builder = AccountBuilder::new(init_seed) + .account_type(AccountType::Public) + .with_component(AuthSingleSig::new(Approver::new( + key_pair.public_key().to_commitment(), + AuthScheme::Falcon512Poseidon2, + ))) + .with_component(BasicWallet); + + let account = builder.build()?; + + client.add_account(&account, false).await?; + + keystore.add_key(&key_pair, account.id()).await?; + + println!("Account ID: {}", account.id()); + println!("No Assets in Vault: {:?}", account.vault().is_empty()); + + Ok(()) +} +``` + +```typescript title="src/demo.ts" +import { MidenClient } from "@miden-sdk/miden-sdk"; + +export async function demo() { + // Initialize client to connect with the Miden Testnet. + // NOTE: The client is our entry point to the Miden network. + // All interactions with the network go through the client. + const client = await MidenClient.createTestnet(); + + // Create a new wallet account. + const wallet = await client.accounts.create({ + storage: "public", // Public: account state is visible onchain + }); + + console.log("Account ID:", wallet.id().toString()); + console.log( + "No Assets in Vault:", + wallet.vault().fungibleAssets().length === 0, + ); +} +``` + +
+Expected output + +```text +Account ID: 0x94733054a40d1610178320cc0c8060 +No Assets in Vault: true +``` + +
+ +## Creating a Token Faucet + +Before we can work with tokens, we need a source of tokens. Let's create a fungible token faucet: + +```rust title="integration/src/bin/faucet.rs" +use miden_client::{ + account::{ + component::{ + AuthScheme, AuthSingleSig, BurnPolicy, FungibleFaucet, MintPolicy, TokenName, + TokenPolicyManager, TransferPolicy, create_singlesig_user_fungible_faucet, + }, + AccountType, + }, + asset::{AssetAmount, TokenSymbol}, + auth::{Approver, AuthSecretKey}, + builder::ClientBuilder, + keystore::{FilesystemKeyStore, Keystore}, + rpc::Endpoint, +}; +use miden_client_sqlite_store::ClientBuilderSqliteExt; +use rand::Rng; +use std::sync::Arc; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + // Initialize RPC connection + let endpoint = Endpoint::testnet(); + let timeout_ms = 10_000; + + // Initialize keystore + let keystore_path = std::path::PathBuf::from("./keystore"); + let keystore = + Arc::new(FilesystemKeyStore::new(keystore_path).unwrap()); + + let store_path = std::path::PathBuf::from("./store.sqlite3"); + + // Initialize client to connect with the Miden Testnet. + // NOTE: The client is our entry point to the Miden network. + // All interactions with the network go through the client. + let mut client = ClientBuilder::new() + .grpc_client(&endpoint, Some(timeout_ms)) + .sqlite_store(store_path) + .authenticator(keystore.clone()) + .build() + .await?; + + client.sync_state().await?; + + // Faucet seed + let mut init_seed = [0u8; 32]; + client.rng().fill_bytes(&mut init_seed); + + // Faucet parameters + let symbol = TokenSymbol::new("TEST")?; + let decimals = 8; + let max_supply = AssetAmount::from(1_000_000u32); + + // Generate key pair + let key_pair = AuthSecretKey::new_falcon512_poseidon2(); + + // Build the faucet account + let faucet = FungibleFaucet::builder() + .name(TokenName::new("Test Token")?) + .symbol(symbol) + .decimals(decimals) + .max_supply(max_supply) + .build()?; + let policies = TokenPolicyManager::builder() + .active_mint_policy(MintPolicy::allow_all()) + .active_burn_policy(BurnPolicy::allow_all()) + .active_send_policy(TransferPolicy::allow_all()) + .active_receive_policy(TransferPolicy::allow_all()) + .build(); + let auth = AuthSingleSig::new(Approver::new( + key_pair.public_key().to_commitment(), + AuthScheme::Falcon512Poseidon2, + )); + let faucet_account = create_singlesig_user_fungible_faucet( + init_seed, + faucet, + auth, + policies, + AccountType::Public, + )?; + + client.add_account(&faucet_account, false).await?; + keystore.add_key(&key_pair, faucet_account.id()).await?; + + println!("Faucet account ID: {}", faucet_account.id()); + + Ok(()) +} +``` + +```typescript title="src/demo.ts" +import { MidenClient } from "@miden-sdk/miden-sdk"; + +export async function demo() { + // Initialize client to connect with the Miden Testnet. + const client = await MidenClient.createTestnet(); + + // Faucet parameters + const decimals = 8; + const maxSupply = 10_000_000n * 10n ** BigInt(decimals); + + // Create a fungible token faucet. + const faucet = await client.accounts.create({ + type: 0, // Fungible faucet + symbol: "TEST", + decimals, + maxSupply, + storage: "public", + }); + + console.log("Faucet account ID:", faucet.id().toString()); +} +``` + +
+Expected output + +```text +Faucet account ID: 0xde0ba31282f7522046d3d4af40722b +``` + +
+ +## Key Takeaways + +**Account Types:** + +- **Public Accounts**: Account state is fully transparent and visible onchain +- **Private Accounts**: Only cryptographic commitments are stored onchain, with full state maintained privately + +**Modular Components:** + +- **BasicWallet**: Provides asset management functionality +- **FungibleFaucet**: Provides token metadata and minting behavior +- **TokenPolicyManager**: Configures mint, burn, send, and receive policies +- **AuthSingleSig**: Handles cryptographic authentication (Falcon512 or ECDSA via the `AuthScheme` enum) + +Now that you understand how to create accounts and faucets, you're ready to learn about Miden's unique transaction model. Continue to [Notes & Transactions](./notes) to explore how assets move between accounts using notes. + +--- diff --git a/versioned_docs/version-0.16/builder/get-started/index.md b/versioned_docs/version-0.16/builder/get-started/index.md new file mode 100644 index 00000000..51c7550f --- /dev/null +++ b/versioned_docs/version-0.16/builder/get-started/index.md @@ -0,0 +1,46 @@ +--- +sidebar_position: 0 +title: Quick Start +description: Get started with Miden by installing Miden tools using the `midenup` toolchain, creating your first wallet, performing basic operations, and building your first smart contract! +pagination_prev: null +--- + +# Quick Start + +Welcome to Miden! This guide gets you up and running with the Miden blockchain by walking through the essential setup and core operations. + +## What is Miden? + +Miden is a privacy-focused, ZK-based blockchain that uses an actor model where each account is a smart contract. Unlike traditional blockchains where accounts simply hold balances, Miden accounts are programmable entities that can execute custom logic, store data, and manage assets autonomously. + +Key concepts you'll encounter: + +- **Accounts**: smart contracts that hold assets and execute code +- **Notes**: messages that exchange data and assets between accounts — also programmable +- **Assets**: tokens that can be fungible or non-fungible +- **Privacy**: accounts and notes can be public or private. Private accounts keep their state offchain, while private notes hide their details behind commitments + +## Getting started + +Follow these guides in order: + + + + Install the Miden toolchain with `midenup`. + + + Essential Miden CLI commands — create a wallet and mint your first tokens. + + + Create and manage Miden accounts programmatically in Rust and TypeScript. + + + Miden's note-based transaction model for private asset transfers. + + + Query account storage data and interact with deployed smart contracts. + + + Build, test, and deploy a smart contract on Miden using Rust. + + diff --git a/versioned_docs/version-0.16/builder/get-started/notes.md b/versioned_docs/version-0.16/builder/get-started/notes.md new file mode 100644 index 00000000..b0308ae1 --- /dev/null +++ b/versioned_docs/version-0.16/builder/get-started/notes.md @@ -0,0 +1,1137 @@ +--- +sidebar_position: 3 +title: Notes & Transactions +description: Learn Miden's unique note-based transaction model for asset transfers between accounts. +--- + +# Notes & Transactions + +Miden's transaction model is uniquely powerful, combining private asset transfers through notes with zero-knowledge proofs. Let's explore how to mint, consume, and send tokens using this innovative approach. + +## Understanding Miden's Transaction Model + +Traditional blockchains move tokens directly between account balances. Miden uses a more sophisticated **note-based system** that provides enhanced privacy and flexibility. + +**Think of Notes Like Sealed Envelopes:** + +- Alice puts 100 tokens in a sealed envelope (note) addressed to Bob +- She posts the envelope to the public board (network) +- Only Bob can open envelopes addressed to him +- When Bob opens it, the 100 tokens move to his vault + +**Key Components:** + +- **Notes**: Sealed containers that carry data and assets between accounts +- **P2ID (Pay-To-ID) Notes**: Notes addressed to a specific account ID (like Bob's address) +- **Nullifiers**: Prevent someone from opening the same envelope twice +- **Zero-Knowledge Proofs**: Prove transactions are valid without revealing private details + +## The Two-Transaction Model + +A core principle of Miden is that **a transaction is the state transition of a single account**. This means each transaction only modifies one account's state, which enables parallel execution and strong privacy guarantees. + +Miden uses a **two-transaction model** for asset transfers that provides enhanced privacy and scalability: + +### Transaction 1: Sender Creates Note + +- **Alice's account** creates a P2ID (Pay-To-ID) note containing 100 tokens +- The note specifies **Bob** as the only valid consumer +- Alice's balance decreases, note is available for consumption +- Alice's transaction is complete and final + +### Transaction 2: Recipient Consumes Note + +- **Bob's client** discovers the note (addressed to his ID) +- Bob creates a transaction to consume the note +- Tokens move from the note into Bob's vault +- Bob's balance increases, note is nullified + +### Benefits + +This approach provides several advantages over direct transfers: + +1. **Privacy**: Alice and Bob's transactions are unlinkable +2. **Parallelization**: Multiple transactions can be processed concurrently, enabling simultaneous creation of notes. +3. **Flexibility**: Notes can include complex conditions (time locks, multi-sig, etc.) +4. **Scalability**: No global state synchronization required + +## Set Up Development Environment + +To run the code examples in this guide, you'll need to set up a development environment. If you haven't already, follow the setup instructions in the [Accounts](./accounts#set-up-development-environment) guide. + +:::note Transaction fees +Each account that submits a transaction needs native fee tokens, including a faucet that mints `TEST`. The bootstrap below pauses each program so you can fund a newly created account before its first transaction. +::: + +### Bootstrap native fee funding + +Add the following shared helpers once, then use them with each program on this page. In the Rust project, add `pub mod funding;` to the existing `integration/src/lib.rs` file. + +Start a program and keep its terminal or browser tab running after it prints the account IDs. For each prompt, request a **public** native-fee funding note for that displayed account from a trusted faucet on the same network, then paste only the funding note ID. Do not consume the note with the CLI or another client store: the running program must discover and consume it. Request enough native tokens to cover this bootstrap transaction and the account's later transactions in the example; the required amount depends on network fees. + +```rust title="integration/src/funding.rs" +use miden_client::{ + account::AccountId, + auth::TransactionAuthenticator, + note::{Note, NoteId}, + store::TransactionFilter, + transaction::{TransactionRequestBuilder, TransactionStatus}, + Client, +}; +use std::io::{self, Write}; +use tokio::time::{sleep, Duration, Instant}; + +const WAIT_TIMEOUT: Duration = Duration::from_secs(300); + +pub async fn fund_account( + client: &mut Client, + account_id: AccountId, +) -> anyhow::Result<()> +where + AUTH: TransactionAuthenticator + Sync + 'static, +{ + println!("Request a public native-fee funding note for {account_id}."); + print!("Paste its note ID here: "); + io::stdout().flush()?; + + let mut note_id = String::new(); + io::stdin().read_line(&mut note_id)?; + let note_id = NoteId::try_from_hex(note_id.trim())?; + + let deadline = Instant::now() + WAIT_TIMEOUT; + let funding_note: Note = loop { + client.sync_state().await?; + let notes = client.get_consumable_notes(Some(account_id)).await?; + + if let Some((record, _)) = notes + .into_iter() + .find(|(record, _)| record.id() == Some(note_id)) + { + break record.try_into()?; + } + + if Instant::now() >= deadline { + anyhow::bail!( + "Timed out waiting for funding note {note_id}; verify its ID, visibility, and network" + ); + } + + sleep(Duration::from_secs(2)).await; + }; + + let request = TransactionRequestBuilder::new().build_consume_notes(vec![funding_note])?; + let tx_id = client.submit_new_transaction(account_id, request).await?; + println!("Native funding transaction submitted, ID: {}", tx_id.to_hex()); + + let deadline = Instant::now() + WAIT_TIMEOUT; + loop { + client.sync_state().await?; + let records = client + .get_transactions(TransactionFilter::Ids(vec![tx_id])) + .await?; + + if let Some(record) = records.first() { + match &record.status { + TransactionStatus::Committed { .. } => { + println!("Native funding confirmed for {account_id}"); + return Ok(()); + }, + TransactionStatus::Discarded(cause) => { + anyhow::bail!("Native funding transaction was discarded: {cause}"); + }, + TransactionStatus::Pending => {}, + } + } + + if Instant::now() >= deadline { + anyhow::bail!( + "Timed out waiting for transaction {} to commit; check this ID before retrying", + tx_id.to_hex() + ); + } + + sleep(Duration::from_secs(2)).await; + } +} +``` + +```typescript title="src/funding.ts" +import { + type AccountId, + MidenClient, + NoteId, +} from "@miden-sdk/miden-sdk"; + +const WAIT_TIMEOUT_MS = 300_000; + +export async function fundAccount( + client: MidenClient, + accountId: AccountId, +): Promise { + console.log( + `Request a public native-fee funding note for ${accountId.toString()}.`, + ); + const enteredId = window.prompt("Paste its note ID here:"); + if (!enteredId?.trim()) { + throw new Error("A native funding note ID is required"); + } + + const noteId = NoteId.fromHex(enteredId.trim()); + const normalizedId = noteId.toString(); + const deadline = Date.now() + WAIT_TIMEOUT_MS; + + while (Date.now() < deadline) { + await client.sync(); + const notes = await client.notes.listAvailable({ account: accountId }); + const fundingNote = notes.find( + (note) => note.id()?.toString() === normalizedId, + ); + + if (fundingNote) { + const { txId } = await client.transactions.consume({ + account: accountId, + notes: [fundingNote], + }); + console.log( + "Native funding transaction submitted, ID:", + txId.toHex(), + ); + await client.transactions.waitFor(txId, { + timeout: WAIT_TIMEOUT_MS, + }); + await client.sync(); + console.log("Native funding confirmed for", accountId.toString()); + return; + } + + await new Promise((resolve) => window.setTimeout(resolve, 2_000)); + } + + throw new Error( + `Timed out waiting for funding note ${normalizedId}; verify its ID, visibility, and network`, + ); +} +``` + +## Minting Tokens + +**What is Minting?** +Minting in Miden creates new tokens and packages them into a **P2ID note** (Pay-to-ID note) addressed to a specific account. Unlike traditional blockchains where tokens appear directly in your balance, Miden uses a two-step process: + +1. **Faucet mints tokens** → Creates a P2ID note containing the tokens +2. **Recipient consumes the note** → Tokens move into their account vault + +**Key Concepts:** + +- **P2ID Note**: A note that can only be consumed by the account it's addressed to +- **NoteType**: Determines visibility. `Public` note details are stored onchain. `Private` note details must reach the consumer separately; their commitments, metadata, and attachments remain public. +- **FungibleAsset**: Represents tokens that can be divided and exchanged (like currencies) + +Let's see this in action: + +```rust title="integration/src/bin/mint.rs" +use miden_client::{ + account::{ + component::{ + AuthScheme, AuthSingleSig, BasicWallet, BurnPolicy, FungibleFaucet, MintPolicy, + TokenName, TokenPolicyManager, TransferPolicy, + create_singlesig_user_fungible_faucet, + }, + AccountBuilder, AccountType, + }, + asset::{AssetAmount, FungibleAsset, TokenSymbol}, + auth::{Approver, AuthSecretKey}, + builder::ClientBuilder, + keystore::{FilesystemKeyStore, Keystore}, + note::NoteType, + rpc::Endpoint, + transaction::TransactionRequestBuilder, +}; +use miden_client_sqlite_store::ClientBuilderSqliteExt; +use rand::Rng; +use std::sync::Arc; +use integration::funding::fund_account; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + // Initialize RPC connection + let endpoint = Endpoint::testnet(); + let timeout_ms = 10_000; + + // Initialize keystore + let keystore_path = std::path::PathBuf::from("./keystore"); + let keystore = + Arc::new(FilesystemKeyStore::new(keystore_path).unwrap()); + + let store_path = std::path::PathBuf::from("./store.sqlite3"); + + // Initialize client to connect with the Miden Testnet. + // NOTE: The client is our entry point to the Miden network. + // All interactions with the network go through the client. + let mut client = ClientBuilder::new() + .grpc_client(&endpoint, Some(timeout_ms)) + .sqlite_store(store_path) + .authenticator(keystore.clone()) + .build() + .await?; + + client.sync_state().await?; + + //------------------------------------------------------------ + // CREATING A FAUCET AND MINTING TOKENS + //------------------------------------------------------------ + + // Account seeds + let mut alice_seed = [0u8; 32]; + client.rng().fill_bytes(&mut alice_seed); + let mut faucet_seed = [0u8; 32]; + client.rng().fill_bytes(&mut faucet_seed); + + // Faucet parameters + let symbol = TokenSymbol::new("TEST")?; + let decimals = 8; + let max_supply = AssetAmount::from(1_000_000u32); + + // Generate key pair + let alice_key_pair = AuthSecretKey::new_falcon512_poseidon2(); + let faucet_key_pair = AuthSecretKey::new_falcon512_poseidon2(); + + // Build the account + let account_builder = AccountBuilder::new(alice_seed) + .account_type(AccountType::Public) + .with_component(AuthSingleSig::new(Approver::new( + alice_key_pair.public_key().to_commitment(), + AuthScheme::Falcon512Poseidon2, + ))) + .with_component(BasicWallet); + + // Build the faucet + let faucet = FungibleFaucet::builder() + .name(TokenName::new("Test Token")?) + .symbol(symbol) + .decimals(decimals) + .max_supply(max_supply) + .build()?; + let policies = TokenPolicyManager::builder() + .active_mint_policy(MintPolicy::allow_all()) + .active_burn_policy(BurnPolicy::allow_all()) + .active_send_policy(TransferPolicy::allow_all()) + .active_receive_policy(TransferPolicy::allow_all()) + .build(); + + let alice_account = account_builder.build()?; + let faucet_auth = AuthSingleSig::new(Approver::new( + faucet_key_pair.public_key().to_commitment(), + AuthScheme::Falcon512Poseidon2, + )); + let faucet_account = create_singlesig_user_fungible_faucet( + faucet_seed, + faucet, + faucet_auth, + policies, + AccountType::Public, + )?; + + println!("Alice's account ID: {}", alice_account.id().to_hex()); + println!("Faucet account ID: {}", faucet_account.id().to_hex()); + + // Add accounts to client + client.add_account(&alice_account, false).await?; + client.add_account(&faucet_account, false).await?; + + // Add keys to keystore + keystore.add_key(&alice_key_pair, alice_account.id()).await?; + keystore.add_key(&faucet_key_pair, faucet_account.id()).await?; + + // Keep this program running while you fund the faucet at the prompt. + fund_account(&mut client, faucet_account.id()).await?; + + let amount: u64 = 1000; + // The faucet account ID encodes callback support for the transfer policies above. + let fungible_asset = FungibleAsset::new(faucet_account.id(), amount)?; + + // Build transaction request to mint fungible asset to Alice's account + // NOTE: This transaction will create a P2ID note (a Miden note containing the minted asset) + // for Alice's account. Alice will be able to consume these notes to get the fungible asset in her vault + println!("Minting 1000 tokens to Alice..."); + let transaction_request = TransactionRequestBuilder::new().build_mint_fungible_asset( + fungible_asset, + alice_account.id(), + NoteType::Public, + client.rng(), + )?; + + // Create transaction and submit it to create P2ID notes for Alice's account + let tx_id = client + .submit_new_transaction(faucet_account.id(), transaction_request) + .await?; + client.sync_state().await?; + + println!( + "Mint transaction submitted successfully, ID: {}", + tx_id.to_hex() + ); + + Ok(()) +} +``` + +```typescript title="src/demo.ts" +import { MidenClient } from "@miden-sdk/miden-sdk"; +import { fundAccount } from "./funding"; + +export async function demo() { + // Initialize client to connect with the Miden Testnet. + const client = await MidenClient.createTestnet(); + + // Creating Alice's account + const alice = await client.accounts.create({ + storage: "public", // Public: account state is visible onchain + }); + console.log("Alice's account ID:", alice.id().toString()); + + // Creating a faucet account + const decimals = 8; + const maxSupply = 10_000_000n * 10n ** BigInt(decimals); + const faucet = await client.accounts.create({ + type: 0, // Fungible faucet + symbol: "TEST", + decimals, + maxSupply, + storage: "public", + }); + console.log("Faucet account ID:", faucet.id().toString()); + + // Keep this browser tab running while you fund the faucet at the prompt. + await fundAccount(client, faucet.id()); + + // Mint 1000 tokens to Alice. + // This creates a P2ID note containing the asset; Alice consumes it + // to actually receive the tokens in her vault (see the next section). + console.log("Minting 1000 tokens to Alice..."); + const { txId } = await client.transactions.mint({ + account: faucet.id(), // faucet is the executing account + to: alice.id(), + amount: 1000n, + type: "public", // note visibility + }); + console.log("Mint transaction submitted successfully, ID:", txId.toHex()); +} +``` + +
+Expected output + +```text +Alice's account ID: 0x5b2840a923dedc102ea67e0c1eba3c +Faucet account ID: 0x29dd1dc628d2842032e751ed1b5da7 +Minting 1000 tokens to Alice... +Mint transaction submitted successfully, ID: 0x7a2dbde87ea2f4d41b396d6d3f6bdb9a8d7e2a51555fa57064a1657ad70fca06 +``` + +
+ +## Consuming Notes + +**Why Consume Notes?** +After minting creates a P2ID note containing tokens, the recipient must **consume** the note to actually receive the tokens in their account vault. This two-step process provides several benefits: + +- **Privacy**: The mint transaction and consume transaction are unlinkable +- **Flexibility**: Recipients can consume notes when they choose +- **Atomic Operations**: Each step either succeeds completely or fails safely + +**The Process:** + +1. **Find consumable notes** addressed to your account +2. **Create a consume transaction** referencing the note IDs +3. **Submit the transaction** to move tokens into your vault + +Here's how to consume notes programmatically: + +:::tip +This program includes the setup and minting steps from the previous section and requires the native fee funding described above. **The new consume logic starts at the `CONSUMING P2ID NOTES` comment.** +::: + +```rust title="integration/src/bin/consume.rs" +use miden_client::{ + account::{ + component::{ + AuthScheme, AuthSingleSig, BasicWallet, BurnPolicy, FungibleFaucet, MintPolicy, + TokenName, TokenPolicyManager, TransferPolicy, + create_singlesig_user_fungible_faucet, + }, + Account, AccountBuilder, AccountType, + }, + asset::{AssetAmount, AssetId, FungibleAsset, TokenSymbol}, + auth::{Approver, AuthSecretKey}, + builder::ClientBuilder, + keystore::{FilesystemKeyStore, Keystore}, + note::NoteType, + rpc::Endpoint, + transaction::TransactionRequestBuilder, +}; +use miden_client_sqlite_store::ClientBuilderSqliteExt; +use rand::Rng; +use std::sync::Arc; +use tokio::time::Duration; +use integration::funding::fund_account; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + // Initialize RPC connection + let endpoint = Endpoint::testnet(); + let timeout_ms = 10_000; + + // Initialize keystore + let keystore_path = std::path::PathBuf::from("./keystore"); + let keystore = + Arc::new(FilesystemKeyStore::new(keystore_path).unwrap()); + + let store_path = std::path::PathBuf::from("./store.sqlite3"); + + // Initialize client to connect with the Miden Testnet. + // NOTE: The client is our entry point to the Miden network. + // All interactions with the network go through the client. + let mut client = ClientBuilder::new() + .grpc_client(&endpoint, Some(timeout_ms)) + .sqlite_store(store_path) + .authenticator(keystore.clone()) + .build() + .await?; + + client.sync_state().await?; + + //------------------------------------------------------------ + // CREATING A FAUCET AND MINTING TOKENS + //------------------------------------------------------------ + + // Account seeds + let mut alice_seed = [0u8; 32]; + client.rng().fill_bytes(&mut alice_seed); + let mut faucet_seed = [0u8; 32]; + client.rng().fill_bytes(&mut faucet_seed); + + // Faucet parameters + let symbol = TokenSymbol::new("TEST")?; + let decimals = 8; + let max_supply = AssetAmount::from(1_000_000u32); + + // Generate key pair + let alice_key_pair = AuthSecretKey::new_falcon512_poseidon2(); + let faucet_key_pair = AuthSecretKey::new_falcon512_poseidon2(); + + // Build the account + let account_builder = AccountBuilder::new(alice_seed) + .account_type(AccountType::Public) + .with_component(AuthSingleSig::new(Approver::new( + alice_key_pair.public_key().to_commitment(), + AuthScheme::Falcon512Poseidon2, + ))) + .with_component(BasicWallet); + + // Build the faucet + let faucet = FungibleFaucet::builder() + .name(TokenName::new("Test Token")?) + .symbol(symbol) + .decimals(decimals) + .max_supply(max_supply) + .build()?; + let policies = TokenPolicyManager::builder() + .active_mint_policy(MintPolicy::allow_all()) + .active_burn_policy(BurnPolicy::allow_all()) + .active_send_policy(TransferPolicy::allow_all()) + .active_receive_policy(TransferPolicy::allow_all()) + .build(); + + let alice_account = account_builder.build()?; + let faucet_auth = AuthSingleSig::new(Approver::new( + faucet_key_pair.public_key().to_commitment(), + AuthScheme::Falcon512Poseidon2, + )); + let faucet_account = create_singlesig_user_fungible_faucet( + faucet_seed, + faucet, + faucet_auth, + policies, + AccountType::Public, + )?; + + println!("Alice's account ID: {}", alice_account.id().to_hex()); + println!("Faucet account ID: {}", faucet_account.id().to_hex()); + + // Add accounts to client + client.add_account(&alice_account, false).await?; + client.add_account(&faucet_account, false).await?; + + // Add keys to keystore + keystore.add_key(&alice_key_pair, alice_account.id()).await?; + keystore.add_key(&faucet_key_pair, faucet_account.id()).await?; + + // The faucet mints; Alice later consumes. Fund both before either transaction. + fund_account(&mut client, faucet_account.id()).await?; + fund_account(&mut client, alice_account.id()).await?; + + let amount: u64 = 1000; + // The faucet account ID encodes callback support for the transfer policies above. + let fungible_asset = FungibleAsset::new(faucet_account.id(), amount)?; + + // Build transaction request to mint fungible asset to Alice's account + // NOTE: This transaction will create a P2ID note (a Miden note containing the minted asset) + // for Alice's account. Alice will be able to consume these notes to get the fungible asset in her vault + println!("Minting 1000 tokens to Alice..."); + let transaction_request = TransactionRequestBuilder::new().build_mint_fungible_asset( + fungible_asset, + alice_account.id(), + NoteType::Public, + client.rng(), + )?; + + // Create transaction and submit it to create P2ID notes for Alice's account + let tx_id = client + .submit_new_transaction(faucet_account.id(), transaction_request) + .await?; + client.sync_state().await?; + + println!( + "Mint transaction submitted successfully, ID: {}", + tx_id.to_hex() + ); + + //------------------------------------------------------------ + // CONSUMING P2ID NOTES + //------------------------------------------------------------ + + // Public notes must be committed to a block before they can be consumed. + // Poll until the network includes our mint note in a block. + println!("Waiting for note to be consumable..."); + loop { + // Sync state to get the latest block + client.sync_state().await?; + + let consumable_notes = client + .get_consumable_notes(Some(alice_account.id())) + .await?; + + if consumable_notes.is_empty() { + tokio::time::sleep(Duration::from_secs(2)).await; + continue; + } + + let notes: Vec = consumable_notes + .into_iter() + .map(|(record, _)| record.try_into().expect("Failed to convert to Note")) + .collect(); + + let consume_tx_request = TransactionRequestBuilder::new().build_consume_notes(notes)?; + + // Create transaction and submit it to consume notes + let consume_tx_id = client + .submit_new_transaction(alice_account.id(), consume_tx_request) + .await?; + + println!( + "Consume transaction submitted successfully, ID: {}", + consume_tx_id.to_hex() + ); + + client.sync_state().await?; + + let alice_account: Account = client + .get_account(alice_account.id()) + .await? + .ok_or_else(|| anyhow::anyhow!("Account not found"))? + .try_into()?; + let vault = alice_account.vault(); + let asset_id = AssetId::new_fungible(faucet_account.id()); + println!( + "Alice's TEST token balance: {}", + vault.get_balance(asset_id)? + ); + + break; // Exit the loop after consuming the note + } + + Ok(()) +} +``` + +```typescript title="src/demo.ts" +import { MidenClient } from "@miden-sdk/miden-sdk"; +import { fundAccount } from "./funding"; + +export async function demo() { + // Initialize client to connect with the Miden Testnet. + const client = await MidenClient.createTestnet(); + + // Creating Alice's account + const alice = await client.accounts.create({ + storage: "public", + }); + console.log("Alice's account ID:", alice.id().toString()); + + // Creating a faucet account + const decimals = 8; + const maxSupply = 10_000_000n * 10n ** BigInt(decimals); + const faucet = await client.accounts.create({ + type: 0, // Fungible faucet + symbol: "TEST", + decimals, + maxSupply, + storage: "public", + }); + console.log("Faucet account ID:", faucet.id().toString()); + + // The faucet mints; Alice later consumes. Fund both before either transaction. + await fundAccount(client, faucet.id()); + await fundAccount(client, alice.id()); + + // Mint 1000 tokens to Alice. Creates a P2ID note that she'll consume. + console.log("Minting 1000 tokens to Alice..."); + const mintResult = await client.transactions.mint({ + account: faucet.id(), + to: alice.id(), + amount: 1000n, + type: "public", + waitForConfirmation: true, + }); + console.log( + "Mint transaction submitted successfully, ID:", + mintResult.txId.toHex(), + ); + + // List notes available to Alice and consume them — tokens move into her vault. + console.log("Waiting for note to be consumable..."); + const notes = await client.notes.listAvailable({ account: alice.id() }); + const consumeResult = await client.transactions.consume({ + account: alice.id(), + notes: [notes[0]], + waitForConfirmation: true, + }); + console.log( + "Consume transaction submitted successfully, ID:", + consumeResult.txId.toHex(), + ); + + // Fetch Alice again so her vault reflects the consumed note. + const updatedAlice = await client.accounts.get(alice.id()); + if (!updatedAlice) { + throw new Error("Alice's account was not found"); + } + + const balance = updatedAlice.vault().getBalance(faucet.id()); + console.log("Alice's TEST token balance:", Number(balance)); +} +``` + +
+Expected output + +```text +Alice's account ID: 0x5b2840a923dedc102ea67e0c1eba3c +Faucet account ID: 0x29dd1dc628d2842032e751ed1b5da7 +Minting 1000 tokens to Alice... +Mint transaction submitted successfully, ID: 0x7a2dbde87ea2f4d41b396d6d3f6bdb9a8d7e2a51555fa57064a1657ad70fca06 +Waiting for note to be consumable... +Consume transaction submitted successfully, ID: 0xa75872c498ee71cd6725aef9411d2559094cec1e1e89670dbf99c60bb8843481 +Alice's TEST token balance: 1000 +``` + +
+ +## Sending Tokens Between Accounts + +**How Sending Works in Miden** +Sending tokens between accounts follows the same note-based pattern. The sender creates a new P2ID note containing tokens from their vault and addresses it to the recipient: + +**The Flow:** + +1. **Sender creates P2ID note** containing tokens and recipient's account ID +2. **Sender submits transaction** - their balance decreases, note is published +3. **Recipient discovers note** addressed to their account ID +4. **Recipient consumes note** - tokens move into their vault + +Alice and Bob execute separate transactions. Their visibility depends on the +account and note types they use; public notes expose their details. + +Let's implement the complete flow - mint, consume, then send: + +:::tip +This program includes all previous steps and requires the native fee funding described above. **The new send logic starts at the `SENDING TOKENS TO BOB` comment.** +::: + +```rust title="integration/src/bin/send.rs" +use miden_client::{ + account::{ + component::{ + AuthScheme, AuthSingleSig, BasicWallet, BurnPolicy, FungibleFaucet, MintPolicy, + TokenName, TokenPolicyManager, TransferPolicy, + create_singlesig_user_fungible_faucet, + }, + Account, AccountBuilder, AccountType, + }, + asset::{AssetAmount, AssetId, FungibleAsset, TokenSymbol}, + auth::{Approver, AuthSecretKey}, + builder::ClientBuilder, + keystore::{FilesystemKeyStore, Keystore}, + note::{NoteType, P2idNote}, + rpc::Endpoint, + transaction::TransactionRequestBuilder, +}; +use miden_client_sqlite_store::ClientBuilderSqliteExt; +use rand::Rng; +use std::sync::Arc; +use tokio::time::Duration; +use integration::funding::fund_account; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + // Initialize RPC connection + let endpoint = Endpoint::testnet(); + let timeout_ms = 10_000; + + // Initialize keystore + let keystore_path = std::path::PathBuf::from("./keystore"); + let keystore = + Arc::new(FilesystemKeyStore::new(keystore_path).unwrap()); + + let store_path = std::path::PathBuf::from("./store.sqlite3"); + + // Initialize client to connect with the Miden Testnet. + // NOTE: The client is our entry point to the Miden network. + // All interactions with the network go through the client. + let mut client = ClientBuilder::new() + .grpc_client(&endpoint, Some(timeout_ms)) + .sqlite_store(store_path) + .authenticator(keystore.clone()) + .build() + .await?; + + client.sync_state().await?; + + //------------------------------------------------------------ + // CREATING A FAUCET AND MINTING TOKENS + //------------------------------------------------------------ + + // Account seeds + let mut alice_seed = [0u8; 32]; + client.rng().fill_bytes(&mut alice_seed); + let mut faucet_seed = [0u8; 32]; + client.rng().fill_bytes(&mut faucet_seed); + + // Faucet parameters + let symbol = TokenSymbol::new("TEST")?; + let decimals = 8; + let max_supply = AssetAmount::from(1_000_000u32); + + // Generate key pair + let alice_key_pair = AuthSecretKey::new_falcon512_poseidon2(); + let faucet_key_pair = AuthSecretKey::new_falcon512_poseidon2(); + + // Build the account + let account_builder = AccountBuilder::new(alice_seed) + .account_type(AccountType::Public) + .with_component(AuthSingleSig::new(Approver::new( + alice_key_pair.public_key().to_commitment(), + AuthScheme::Falcon512Poseidon2, + ))) + .with_component(BasicWallet); + + // Build the faucet + let faucet = FungibleFaucet::builder() + .name(TokenName::new("Test Token")?) + .symbol(symbol) + .decimals(decimals) + .max_supply(max_supply) + .build()?; + let policies = TokenPolicyManager::builder() + .active_mint_policy(MintPolicy::allow_all()) + .active_burn_policy(BurnPolicy::allow_all()) + .active_send_policy(TransferPolicy::allow_all()) + .active_receive_policy(TransferPolicy::allow_all()) + .build(); + + let alice_account = account_builder.build()?; + let faucet_auth = AuthSingleSig::new(Approver::new( + faucet_key_pair.public_key().to_commitment(), + AuthScheme::Falcon512Poseidon2, + )); + let faucet_account = create_singlesig_user_fungible_faucet( + faucet_seed, + faucet, + faucet_auth, + policies, + AccountType::Public, + )?; + + println!("Alice's account ID: {}", alice_account.id().to_hex()); + println!("Faucet account ID: {}", faucet_account.id().to_hex()); + + // Add accounts to client + client.add_account(&alice_account, false).await?; + client.add_account(&faucet_account, false).await?; + + // Add keys to keystore + keystore.add_key(&alice_key_pair, alice_account.id()).await?; + keystore.add_key(&faucet_key_pair, faucet_account.id()).await?; + + // The faucet mints; Alice later consumes and sends. Fund both first. + fund_account(&mut client, faucet_account.id()).await?; + fund_account(&mut client, alice_account.id()).await?; + + let amount: u64 = 1000; + // The faucet account ID encodes callback support for the transfer policies above. + let fungible_asset = FungibleAsset::new(faucet_account.id(), amount)?; + + // Build transaction request to mint fungible asset to Alice's account + // NOTE: This transaction will create a P2ID note (a Miden note containing the minted asset) + // for Alice's account. Alice will be able to consume these notes to get the fungible asset in her vault + println!("Minting 1000 tokens to Alice..."); + let transaction_request = TransactionRequestBuilder::new().build_mint_fungible_asset( + fungible_asset, + alice_account.id(), + NoteType::Public, + client.rng(), + )?; + + // Create transaction and submit it to create P2ID notes for Alice's account + let tx_id = client + .submit_new_transaction(faucet_account.id(), transaction_request) + .await?; + client.sync_state().await?; + + println!( + "Mint transaction submitted successfully, ID: {}", + tx_id.to_hex() + ); + + //------------------------------------------------------------ + // CONSUMING P2ID NOTES + //------------------------------------------------------------ + + // Public notes must be committed to a block before they can be consumed. + // Poll until the network includes our mint note in a block. + println!("Waiting for note to be consumable..."); + loop { + // Sync state to get the latest block + client.sync_state().await?; + + let consumable_notes = client + .get_consumable_notes(Some(alice_account.id())) + .await?; + + if consumable_notes.is_empty() { + tokio::time::sleep(Duration::from_secs(2)).await; + continue; + } + + let notes: Vec = consumable_notes + .into_iter() + .map(|(record, _)| record.try_into().expect("Failed to convert to Note")) + .collect(); + + let consume_tx_request = TransactionRequestBuilder::new().build_consume_notes(notes)?; + + // Create transaction and submit it to consume notes + let consume_tx_id = client + .submit_new_transaction(alice_account.id(), consume_tx_request) + .await?; + + println!( + "Consume transaction submitted successfully, ID: {}", + consume_tx_id.to_hex() + ); + + client.sync_state().await?; + + let alice_account: Account = client + .get_account(alice_account.id()) + .await? + .ok_or_else(|| anyhow::anyhow!("Account not found"))? + .try_into()?; + let vault = alice_account.vault(); + let asset_id = AssetId::new_fungible(faucet_account.id()); + println!( + "Alice's TEST token balance: {}", + vault.get_balance(asset_id)? + ); + + break; // Exit the loop after consuming the note + } + + //------------------------------------------------------------ + // SENDING TOKENS TO BOB + //------------------------------------------------------------ + + // Create Bob's account so this example is self-contained. + let mut bob_seed = [0u8; 32]; + client.rng().fill_bytes(&mut bob_seed); + let bob_key_pair = AuthSecretKey::new_falcon512_poseidon2(); + let bob_account = AccountBuilder::new(bob_seed) + .account_type(AccountType::Public) + .with_component(AuthSingleSig::new(Approver::new( + bob_key_pair.public_key().to_commitment(), + AuthScheme::Falcon512Poseidon2, + ))) + .with_component(BasicWallet) + .build()?; + + client.add_account(&bob_account, false).await?; + keystore.add_key(&bob_key_pair, bob_account.id()).await?; + + println!("Bob's account ID: {}", bob_account.id().to_hex()); + + let bob_account_id = bob_account.id(); + let send_amount = 100; + let fungible_asset_to_send = FungibleAsset::new(faucet_account.id(), send_amount)?; + + println!("Sending 100 tokens to Bob..."); + let p2id_note = P2idNote::builder() + .sender(alice_account.id()) + .target(bob_account_id) + .asset(fungible_asset_to_send) + .note_type(NoteType::Public) + .generate_serial_number(client.rng()) + .build()? + .into(); + + // Create transaction request to send P2ID note to Bob + let send_p2id_note_transaction_request = TransactionRequestBuilder::new() + .own_output_notes(vec![p2id_note]) + .build()?; + + // Create transaction and submit it to send P2ID note to Bob + let send_p2id_note_tx_id = client + .submit_new_transaction(alice_account.id(), send_p2id_note_transaction_request) + .await?; + client.sync_state().await?; + + println!( + "Send transaction submitted successfully, ID: {}", + send_p2id_note_tx_id.to_hex() + ); + + Ok(()) +} +``` + +```typescript title="src/demo.ts" +import { MidenClient } from "@miden-sdk/miden-sdk"; +import { fundAccount } from "./funding"; + +export async function demo() { + // Initialize client to connect with the Miden Testnet. + const client = await MidenClient.createTestnet(); + + // Create Alice's account and a faucet. + const alice = await client.accounts.create({ + storage: "public", + }); + console.log("Alice's account ID:", alice.id().toString()); + + const decimals = 8; + const maxSupply = 10_000_000n * 10n ** BigInt(decimals); + const faucet = await client.accounts.create({ + type: 0, // Fungible faucet + symbol: "TEST", + decimals, + maxSupply, + storage: "public", + }); + console.log("Faucet account ID:", faucet.id().toString()); + + // The faucet mints; Alice later consumes and sends. Fund both first. + await fundAccount(client, faucet.id()); + await fundAccount(client, alice.id()); + + // Mint 1000 tokens to Alice and consume the resulting P2ID note. + console.log("Minting 1000 tokens to Alice..."); + const mintResult = await client.transactions.mint({ + account: faucet.id(), + to: alice.id(), + amount: 1000n, + type: "public", + waitForConfirmation: true, + }); + console.log( + "Mint transaction submitted successfully, ID:", + mintResult.txId.toHex(), + ); + + console.log("Waiting for note to be consumable..."); + const notes = await client.notes.listAvailable({ account: alice.id() }); + const consumeResult = await client.transactions.consume({ + account: alice.id(), + notes: [notes[0]], + waitForConfirmation: true, + }); + console.log( + "Consume transaction submitted successfully, ID:", + consumeResult.txId.toHex(), + ); + + // Fetch Alice again so her vault reflects the consumed note. + const updatedAlice = await client.accounts.get(alice.id()); + if (!updatedAlice) { + throw new Error("Alice's account was not found"); + } + + const balance = updatedAlice.vault().getBalance(faucet.id()); + console.log("Alice's TEST token balance:", Number(balance)); + + // Create Bob's account so this example is self-contained. + const bob = await client.accounts.create({ + storage: "public", + }); + console.log("Bob's account ID:", bob.id().toString()); + + // Send 100 tokens from Alice to Bob. + console.log("Sending 100 tokens to Bob..."); + const { txId } = await client.transactions.send({ + account: alice.id(), + to: bob.id(), + token: faucet.id(), + amount: 100n, + type: "public", + waitForConfirmation: true, + }); + console.log("Send transaction submitted successfully, ID:", txId.toHex()); +} +``` + +
+Expected output + +```text +Alice's account ID: 0xd6b8bb0ed10b1610282c513501778a +Faucet account ID: 0xe48c43d6ad6496201bcfa585a5a4b6 +Minting 1000 tokens to Alice... +Mint transaction submitted successfully, ID: 0x948a0eef754068b3126dd3261b6b54214fa5608fb13c5e5953faf59bad79c75f +Waiting for note to be consumable... +Consume transaction submitted successfully, ID: 0xc69ab84b784120abe858bb536aebda90bd2067695f11d5da93ab0b704f39ad78 +Alice's TEST token balance: 1000 +Bob's account ID: 0x103f8a1ad4b983104aec0412ab0b0d +Sending 100 tokens to Bob... +Send transaction submitted successfully, ID: 0x51ac27474ade3a54adadd50db6c2b9a2ede254c5f9137f93d7a970f0bc7d66d5 +``` + +
+ +## Key Takeaways + +**Miden's Note-Based Transaction Model:** + +- **Notes** enable asset transfers between accounts (both public and private) +- **Two-transaction model** provides privacy and parallelization benefits +- **Zero-knowledge proofs** validate transaction execution without revealing details +- **P2ID notes** target specific recipients using their account IDs + +**Transaction Flow:** + +1. **Mint** tokens to create notes containing assets +2. **Consume** notes to add assets to account vaults +3. **Send** tokens using P2ID notes targeted to recipients +4. **Nullify** consumed notes to prevent double-spending + +This innovative approach provides unprecedented privacy and flexibility while maintaining the security guarantees of blockchain technology. The note-based model enables scalable, private transactions that can be processed in parallel without global state synchronization. + +--- diff --git a/versioned_docs/version-0.16/builder/get-started/read-storage.md b/versioned_docs/version-0.16/builder/get-started/read-storage.md new file mode 100644 index 00000000..eca9f106 --- /dev/null +++ b/versioned_docs/version-0.16/builder/get-started/read-storage.md @@ -0,0 +1,400 @@ +--- +sidebar_position: 4 +title: Read Storage Values +description: Learn how to query account storage data and interact with deployed smart contracts. +--- + +# Read Storage Values + +Let's explore how to interact with public accounts and retrieve their storage data. + +## Understanding Account Storage + +Miden accounts contain several types of data you can read. + +**Account Components:** + +- **Vault**: Contains the account's assets (tokens) +- **Storage**: Key-value data store with up to 255 slots +- **Code**: The account's smart contract logic (MAST root) +- **Nonce**: Nonce that increments with each state change to prevent double spend + +**Storage Visibility:** + +- **Public accounts**: All data is publicly accessible and can be read by anyone +- **Private accounts**: Only commitments are public; full data is held privately + +## Set Up Development Environment + +To run the code examples in this guide, you'll need to set up a development environment. If you haven't already, follow the setup instructions in the [Accounts](./accounts#set-up-development-environment) guide. + +## Reading from a Public Smart Contract + +Let's read a public counter contract already deployed on the Miden testnet. The examples below include its account ID, so you can query its storage without deploying a contract. This counter stores its value in a named storage map slot. + +### Reading the Count of a Counter contract + +Run `cargo run --bin read-count` for Rust, or call `demo()` for TypeScript. + +```rust title="integration/src/bin/read-count.rs" +use miden_client::{ + account::{Account, AccountId, StorageMapKey, StorageSlotName}, + builder::ClientBuilder, + keystore::FilesystemKeyStore, + rpc::Endpoint, + Felt, Word, +}; +use miden_client_sqlite_store::ClientBuilderSqliteExt; +use std::sync::Arc; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + // Initialize RPC connection + let endpoint = Endpoint::testnet(); + let timeout_ms = 10_000; + + // Initialize keystore + let keystore_path = std::path::PathBuf::from("./keystore"); + let keystore = + Arc::new(FilesystemKeyStore::new(keystore_path).unwrap()); + + let store_path = std::path::PathBuf::from("./store.sqlite3"); + + // Initialize client to connect with the Miden Testnet. + // NOTE: The client is our entry point to the Miden network. + // All interactions with the network go through the client. + let mut client = ClientBuilder::new() + .grpc_client(&endpoint, Some(timeout_ms)) + .sqlite_store(store_path) + .authenticator(keystore.clone()) + .build() + .await?; + + client.sync_state().await?; + + //------------------------------------------------------------ + // READ PUBLIC STATE OF THE COUNTER ACCOUNT + //------------------------------------------------------------ + + // A counter contract deployed on the Miden testnet. It is a public fixture and + // may need updating after a new release. + let counter_account_id = AccountId::from_hex("0x78ccf49bd142f8917edf7743e3e718")?; + + client.import_account_by_id(counter_account_id).await?; + + let counter_account: Account = client + .get_account(counter_account_id) + .await? + .ok_or_else(|| anyhow::anyhow!("Account not found"))? + .try_into()?; + + // Read the count using the contract's storage slot name and map key. + let slot_name = StorageSlotName::new("counter_account::counter_contract::count_map")?; + let counter_key = Word::new([Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::ONE]); + let count = counter_account + .storage() + .get_map_item( + &slot_name, + StorageMapKey::new(counter_key), + )?; + + println!("Count: {}", count[0].as_canonical_u64()); + + Ok(()) +} +``` + +```typescript title="src/demo.ts" +import { MidenClient, Word } from "@miden-sdk/miden-sdk"; + +export async function demo() { + // Initialize client to connect with the Miden Testnet. + const client = await MidenClient.createTestnet(); + + // A counter contract deployed on the Miden testnet. It is a public fixture and + // may need updating after a new release. + const counterAccountId = "0x78ccf49bd142f8917edf7743e3e718"; + + // Fetch the counter account (imports it into the local store if needed). + const counter = await client.accounts.getOrImport(counterAccountId); + + // Get the count from the counter account by querying its storage map + // using the named storage slot and counter key. + const slotName = "counter_account::counter_contract::count_map"; + const counterKey = new Word(BigUint64Array.from([0n, 0n, 0n, 1n])); + const count = counter.storage().getMapItem(slotName, counterKey); + + // The count value is a WORD (array of 4 u64 values). + // The counter number is the first element. + console.log("Count:", Number(count?.toU64s()[0])); +} +``` + +
+Expected output + +```text +Count: 1 +``` + +
+ +## Reading Account Token Balances + +You can also query the assets (tokens) held by an account. This example uses the native-fee funding helpers from [Notes & Transactions](./notes#bootstrap-native-fee-funding). Add those helpers first, then keep the program running while you fund the faucet and Alice at their prompts. + +```rust title="integration/src/bin/token-balance.rs" +use miden_client::{ + account::{ + component::{ + AuthScheme, AuthSingleSig, BasicWallet, BurnPolicy, FungibleFaucet, MintPolicy, + TokenName, TokenPolicyManager, TransferPolicy, + create_singlesig_user_fungible_faucet, + }, + Account, AccountBuilder, AccountType, + }, + asset::{AssetAmount, AssetId, FungibleAsset, TokenSymbol}, + auth::{Approver, AuthSecretKey}, + builder::ClientBuilder, + keystore::{FilesystemKeyStore, Keystore}, + note::NoteType, + rpc::Endpoint, + transaction::TransactionRequestBuilder, +}; +use miden_client_sqlite_store::ClientBuilderSqliteExt; +use rand::Rng; +use std::sync::Arc; +use tokio::time::Duration; +use integration::funding::fund_account; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + // Initialize RPC connection + let endpoint = Endpoint::testnet(); + let timeout_ms = 10_000; + + // Initialize keystore + let keystore_path = std::path::PathBuf::from("./keystore"); + let keystore = + Arc::new(FilesystemKeyStore::new(keystore_path).unwrap()); + + let store_path = std::path::PathBuf::from("./store.sqlite3"); + + // Initialize client to connect with the Miden Testnet. + // NOTE: The client is our entry point to the Miden network. + // All interactions with the network go through the client. + let mut client = ClientBuilder::new() + .grpc_client(&endpoint, Some(timeout_ms)) + .sqlite_store(store_path) + .authenticator(keystore.clone()) + .build() + .await?; + + client.sync_state().await?; + + //------------------------------------------------------------ + // CREATING A FAUCET, MINTING AND CONSUMING TOKENS + //------------------------------------------------------------ + + // Account seeds + let mut alice_seed = [0u8; 32]; + client.rng().fill_bytes(&mut alice_seed); + let mut faucet_seed = [0u8; 32]; + client.rng().fill_bytes(&mut faucet_seed); + + // Faucet parameters + let symbol = TokenSymbol::new("TEST")?; + let decimals = 8; + let max_supply = AssetAmount::from(1_000_000u32); + + // Generate key pair + let alice_key_pair = AuthSecretKey::new_falcon512_poseidon2(); + let faucet_key_pair = AuthSecretKey::new_falcon512_poseidon2(); + + // Build the account + let account_builder = AccountBuilder::new(alice_seed) + .account_type(AccountType::Public) + .with_component(AuthSingleSig::new(Approver::new( + alice_key_pair.public_key().to_commitment(), + AuthScheme::Falcon512Poseidon2, + ))) + .with_component(BasicWallet); + + // Build the faucet + let faucet = FungibleFaucet::builder() + .name(TokenName::new("Test Token")?) + .symbol(symbol) + .decimals(decimals) + .max_supply(max_supply) + .build()?; + let policies = TokenPolicyManager::builder() + .active_mint_policy(MintPolicy::allow_all()) + .active_burn_policy(BurnPolicy::allow_all()) + .active_send_policy(TransferPolicy::allow_all()) + .active_receive_policy(TransferPolicy::allow_all()) + .build(); + + let alice_account = account_builder.build()?; + let faucet_auth = AuthSingleSig::new(Approver::new( + faucet_key_pair.public_key().to_commitment(), + AuthScheme::Falcon512Poseidon2, + )); + let faucet_account = create_singlesig_user_fungible_faucet( + faucet_seed, + faucet, + faucet_auth, + policies, + AccountType::Public, + )?; + + println!("Alice's account ID: {}", alice_account.id().to_hex()); + println!("Faucet account ID: {}", faucet_account.id().to_hex()); + + // Add accounts to client + client.add_account(&alice_account, false).await?; + client.add_account(&faucet_account, false).await?; + + // Add keys to keystore + keystore.add_key(&alice_key_pair, alice_account.id()).await?; + keystore.add_key(&faucet_key_pair, faucet_account.id()).await?; + + // The faucet mints; Alice later consumes. Fund both before either transaction. + fund_account(&mut client, faucet_account.id()).await?; + fund_account(&mut client, alice_account.id()).await?; + + let amount: u64 = 1000; + // The faucet factory encodes callback support in the faucet account ID because + // transfer policies are configured above. + let fungible_asset = FungibleAsset::new(faucet_account.id(), amount)?; + + // Mint the asset to Alice — this creates a P2ID note she can consume. + let transaction_request = TransactionRequestBuilder::new().build_mint_fungible_asset( + fungible_asset, + alice_account.id(), + NoteType::Public, + client.rng(), + )?; + client + .submit_new_transaction(faucet_account.id(), transaction_request) + .await?; + client.sync_state().await?; + + // Public notes must be committed to a block before they can be consumed. + // Poll until the network includes our mint note in a block. + println!("Waiting for note to be consumable..."); + loop { + client.sync_state().await?; + + let consumable_notes = client + .get_consumable_notes(Some(alice_account.id())) + .await?; + + if consumable_notes.is_empty() { + tokio::time::sleep(Duration::from_secs(2)).await; + continue; + } + + let notes: Vec = consumable_notes + .into_iter() + .map(|(record, _)| record.try_into().expect("Failed to convert to Note")) + .collect(); + + let consume_tx_request = TransactionRequestBuilder::new().build_consume_notes(notes)?; + client + .submit_new_transaction(alice_account.id(), consume_tx_request) + .await?; + client.sync_state().await?; + + break; + } + + //------------------------------------------------------------ + // READ TOKEN BALANCE OF AN ACCOUNT + //------------------------------------------------------------ + + // Fetch the account again so the vault reflects the consumed note. + let alice_account: Account = client + .get_account(alice_account.id()) + .await? + .ok_or_else(|| anyhow::anyhow!("Account not found"))? + .try_into()?; + + let asset_id = AssetId::new_fungible(faucet_account.id()); + let balance = alice_account.vault().get_balance(asset_id)?; + + println!("Alice's TEST token balance: {}", balance); + + Ok(()) +} +``` + +```typescript title="src/demo.ts" +import { MidenClient } from "@miden-sdk/miden-sdk"; +import { fundAccount } from "./funding"; + +export async function demo() { + // Initialize client to connect with the Miden Testnet. + const client = await MidenClient.createTestnet(); + + // Create Alice's account and a faucet. + const alice = await client.accounts.create({ + storage: "public", + }); + console.log("Alice's account ID:", alice.id().toString()); + + const decimals = 8; + const maxSupply = 10_000_000n * 10n ** BigInt(decimals); + const faucet = await client.accounts.create({ + type: 0, // Fungible faucet + symbol: "TEST", + decimals, + maxSupply, + storage: "public", + }); + console.log("Faucet account ID:", faucet.id().toString()); + + // The faucet mints; Alice later consumes. Fund both before either transaction. + await fundAccount(client, faucet.id()); + await fundAccount(client, alice.id()); + + // Mint 1000 tokens to Alice and consume the resulting P2ID note. + await client.transactions.mint({ + account: faucet.id(), + to: alice.id(), + amount: 1000n, + type: "public", + waitForConfirmation: true, + }); + + console.log("Waiting for note to be consumable..."); + const notes = await client.notes.listAvailable({ account: alice.id() }); + await client.transactions.consume({ + account: alice.id(), + notes: [notes[0]], + waitForConfirmation: true, + }); + + // Fetch Alice again so the vault reflects the consumed note. + const updatedAlice = await client.accounts.get(alice.id()); + if (!updatedAlice) { + throw new Error("Alice's account was not found"); + } + + const balance = updatedAlice.vault().getBalance(faucet.id()); + console.log("Alice's TEST token balance:", Number(balance)); +} +``` + +
+Expected output + +```text +Alice's account ID: 0x5b2840a923dedc102ea67e0c1eba3c +Faucet account ID: 0x29dd1dc628d2842032e751ed1b5da7 +Waiting for note to be consumable... +Alice's TEST token balance: 1000 +``` + +
+ +--- diff --git a/versioned_docs/version-0.16/builder/get-started/setup/_category_.json b/versioned_docs/version-0.16/builder/get-started/setup/_category_.json new file mode 100644 index 00000000..103ee735 --- /dev/null +++ b/versioned_docs/version-0.16/builder/get-started/setup/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Set Up", + "position": 1 +} diff --git a/versioned_docs/version-0.16/builder/get-started/setup/cli-basics.md b/versioned_docs/version-0.16/builder/get-started/setup/cli-basics.md new file mode 100644 index 00000000..b8ff9af6 --- /dev/null +++ b/versioned_docs/version-0.16/builder/get-started/setup/cli-basics.md @@ -0,0 +1,220 @@ +--- +sidebar_position: 2 +title: CLI Basics +description: Learn essential Miden CLI commands to create your wallet and mint your first tokens. +--- + +This guide covers essential Miden CLI commands for creating accounts, minting and managing tokens. Make sure to have [installed Miden development tools](./installation.md) using the `midenup` toolchain. + +## Create Your First Account + +### Generate a New Wallet + +Create a new Miden wallet account: + +```bash title=">_ Terminal" +miden client sync +miden client new-wallet +``` + +
+Expected output + +```text +State synced to block +... +Generated and stored Falcon512 authentication key in keystore. +Successfully created new wallet. +To view account details execute miden client account -s 0x05bd1f642cd368800cc95956b2696a +Setting account 0x05bd1f642cd368800cc95956b2696a as the default account ID. +You can unset it with `miden client account --default none`. +``` + +
+ +The first command synchronizes the client with the latest network state. The second creates a basic wallet account with **private** storage locally. Its first successful transaction publishes the account onchain. + +### View Your Account + +List all your accounts: + +```bash title=">_ Terminal" +miden client account +``` + +
+Expected output + +```text +| Account ID | Kind | Type | Nonce | Status | +|------------|------|------|-------|--------| +| 0x970e3e4dbcd09b8035532edaa87bc9 | Regular | private | 0 | New | +``` + +
+ +View detailed information about your account: + +```bash title=">_ Terminal" +miden client account -s +``` + +
+Expected output + +```text +Account Information +================== + +| Field | Value | +|-------------------|--------------------------------------------------------------------------| +| Address | mtst1qztsu0jdhngfhqp42vhd42rme9cqzkzy89e | +| Account ID (hex) | 0x970e3e4dbcd09b8035532edaa87bc9 | +| Account Commitment| 0x404a762b9a19e70bc8752381b17f909bc0bbab02c0b4636d8923d088ac8ebc04 | +| Kind | Regular | +| Type | private | +| Code Commitment | 0x6a11161925930dae89cc24cbddf0d161cead39b0fe88c262d4e790cff35be01d | +| Vault Root | 0x3e128c57f6cfa0d44ab1308994171af13cb513422add28d1916b3ff254fef82d | +| Storage Root | 0x5f95d38174f10c8ce91a0202763b0813fdcbb2714704cda411af6483ebc8d012 | +| Nonce | 0 | + +Assets: + +| Asset Type | Faucet | Amount | +|------------|---------|---------| +| | | | + +Storage: + +| Slot Name | Slot Type | Value/Commitment | +|----------------------------------------------------------|-----------|--------------------------------------------------------------------| +| miden::standards::auth::singlesig::scheme | Value | 0x0200000000000000000000000000000000000000000000000000000000000000 | +| miden::standards::auth::singlesig::pub_key | Value | 0x113697002c3061328fce8c1e26dc433c536e967c8b91f30d81517e47f5980b3c | +| miden::standards::inspection::storage_schema::commitment | Value | 0xb5724e35b8267d3be6bfc7d0ce50bfd6cce52de6da9f9e847ab24ac1bf7770f1 | +``` + +
+ +**Key Account Components:** + +- **Account ID**: Unique 120-bit identifier encoding the account visibility and version +- **Vault**: Secure storage for your assets +- **Storage**: Key-value store for account data (255 slots available) +- **Code Commitment**: Hash of the account's smart contract logic +- **Nonce**: Counter that increments with each state change + +## Account Management + +### Switch Between Accounts + +If you have multiple accounts, set which one to use as default: + +```bash title=">_ Terminal" +miden client account --default +``` + +## Mint Your First Tokens + +Request test tokens for the wallet you just created. Replace `` with its account ID: + +```bash title=">_ Terminal" +miden mint --target-account --amount 1000 --no-consume +``` + +The [testnet faucet](https://faucet.testnet.miden.io/) sends the tokens in a public note. The amount is in base units. `--no-consume` leaves the note for you to consume with `miden client` in the next step. + +After the faucet transaction is confirmed, sync and consume the note: + +```bash title=">_ Terminal" +miden client sync +miden client consume-notes --account +``` + +With no note IDs specified, `consume-notes` consumes the notes available to that account. Public funding notes are discovered by syncing; you do not need to import a file. If the note is not available yet, wait a few seconds and sync again. + +This first transaction publishes your wallet onchain and adds the tokens to its vault. Its protocol fee is paid from the native test tokens in the funding note, so the remaining balance is less than the requested amount. + +Once the transaction is confirmed, check your account and balance: + +```bash title=">_ Terminal" +miden client sync +miden client account -s +``` + +The account now has a nonzero nonce and a funded vault. + +
+Using a downloaded funding note + +If you request tokens through the web faucet and download a note file, import it before running the sync and consume commands above: + +```bash title=">_ Terminal" +miden client import +``` + +
+ +## Create a New Project + +If you already created a project during [installation](./installation.md) (e.g., `my-test-project`), you can continue using it. Otherwise, create a new one: + +**Rust Workspace:** + +```bash title=">_ Terminal" +miden new my-project +``` + +Creates a **Rust workspace** for developing, testing, and deploying Miden smart contracts using Rust. + +**Vite Frontend Project:** + +```bash title=">_ Terminal" +# Using Yarn +yarn create miden-app +# Using NPM +npx create-miden-app +``` + +Creates a minimal **Vite example project with Miden integration**, built on the standard Vite React TypeScript template. + +## Custom client configuration + +Initialize the client in your working directory when you want to test against a custom network endpoint or use different keys without touching your global config: + +```bash title=">_ Terminal" +miden client init --local --network devnet +``` + +Available networks: + +- `testnet` - Miden's public test network +- `devnet` - Development network +- `localhost` - Local node for testing + +### Important Files Created + +When you initialize the Miden client with `--local`, a `.miden/` directory is created in your working directory with the following files: + +- **`.miden/miden-client.toml`**: Configuration file with network settings +- **`.miden/store.sqlite3`**: Database storing your account data and transaction history +- **`.miden/keystore/`**: Directory containing your private keys (keep secure!) +- **`.miden/packages/`**: Pre-built account component packages + +:::danger +Private keys in the `.miden/keystore/` directory are **not encrypted**. Keep these files secure and never share them. +::: + +To use your global client configuration, run `miden client` commands from a +directory that does not contain `./.miden/miden-client.toml`. Local lookup is +limited to the current working directory, and the CLI falls back to the global +configuration when that local file is absent. You do not need to delete the +local directory to switch. + +`miden client clear-config` is destructive cleanup, not a configuration +switch. It recursively removes the entire local `.miden/` directory, including +the store and private keys; when no local `.miden/` directory exists, it falls +back to removing the global directory. Its confirmation prompt does not +enumerate that state. Use it only for confirmed disposable client state after +preserving anything you need. + +--- diff --git a/versioned_docs/version-0.16/builder/get-started/setup/installation.md b/versioned_docs/version-0.16/builder/get-started/setup/installation.md new file mode 100644 index 00000000..e153951a --- /dev/null +++ b/versioned_docs/version-0.16/builder/get-started/setup/installation.md @@ -0,0 +1,257 @@ +--- +sidebar_position: 1 +title: Installation +description: Get started with Miden development by installing Miden tools using the `midenup` toolchain. +--- + +This guide walks you through installing the Miden development tools using the `midenup` toolchain manager. + +## Prerequisites + +### Install Rust + +Miden development requires Rust. Install it using rustup: + +```bash title=">_ Terminal" +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y +``` + +Reload your PATH environment variable: + +```bash title=">_ Terminal" +. "$HOME/.cargo/env" +``` + +Verify the installation: + +```bash title=">_ Terminal" +rustc --version +``` + +
+Expected output + +```text +rustc 1.98.1 (...) # or newer for client/protocol code +``` + +
+ +### Install Node.js & Yarn + +For TypeScript development with the Miden Web Client, you'll need Node.js and Yarn. + +**Install Node.js:** + +On macOS with Homebrew: + +```bash title=">_ macOS" +brew install node +``` + +On Ubuntu/Debian: + +```bash title=">_ Ubuntu/Debian" +curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash - +sudo apt-get install -y nodejs +``` + +On Windows, use the Node.js installer from nodejs.org. + +**Install Yarn:** + +```bash title=">_ Terminal" +# Install Yarn globally via npm +npm install -g yarn +``` + +**Verify installations:** + +```bash title=">_ Terminal" +node --version && yarn --version +``` + +
+Expected output + +```text +v22.x.x # or higher +1.22.x # or higher +``` + +
+ +### Install Miden CLI + +**Install midenup** + +The Miden toolchain installer makes it easy to manage Miden components: + +```bash title=">_ Terminal" +cargo install midenup +``` + +This guide is verified with midenup **1.0.0**. To install that exact release, use `cargo install midenup --version 1.0.0`. + +:::info +To install from source instead, name the package explicitly — the repository contains more than one binary: `cargo install --git https://github.com/0xMiden/midenup.git midenup` +::: + +**Initialize midenup** + +```bash title=">_ Terminal" +midenup init +``` + +This creates the `$MIDENUP_HOME` directory and sets up the `miden` command by creating a symlink in your Cargo bin directory (`$CARGO_HOME/bin/`, typically `~/.cargo/bin/`). Since Rust users already have this directory in their PATH, no additional PATH configuration is needed. + +Verify it works: + +```bash title=">_ Terminal" +which miden +``` + +
+Expected output + +```text +/Users//.cargo/bin/miden # macOS +/home//.cargo/bin/miden # Linux +``` + +
+ +**Install Miden Toolchain** + +These v0.16 guides require a coherent v0.16 client, compiler, and protocol +toolchain for the network you use. `midenup` resolves network names through the +[published channel manifest](https://0xmiden.github.io/midenup/channel-manifest.json). + +:::warning Release prerequisite +The stable v0.16 toolchain update is ready on midenup's `next` branch, but it +has not yet been promoted to the published manifest. The published `testnet` +channel still points to v0.15, while its explicit `0.16.0` channel is the older +prerelease stack. + +Wait for the updated manifest to be published before using these +network-dependent v0.16 guides against testnet. +::: + +After the manifest meets that requirement, install the public testnet toolchain +and make it the default: + +```bash title=">_ Terminal" +midenup install testnet && midenup override testnet +``` + +:::note +You may see `No artifact found. Proceeding to install from source` during installation. This is expected — it means pre-built binaries aren't available for your platform, so midenup compiles components from source. This can take 15-30 minutes. +::: + +### Verify Installation + +Check that everything is working correctly: + +```bash title=">_ Terminal" +midenup show active-toolchain +miden client --help +``` + +
+Expected output + +```text +testnet +CLI actions +Usage: miden client +... +``` + +
+ +### Troubleshooting + +**"miden: command not found"** + +Ensure `$CARGO_HOME/bin` (typically `~/.cargo/bin/`) is in your PATH. This should already be configured if you installed Rust via rustup. Verify with: + +```bash title=">_ Terminal" +echo $PATH | tr ':' '\n' | grep cargo +``` + +**"config error: missing field" when running `miden client` commands** + +If a previous `miden-client.toml` is incompatible with the current client, move +only that file aside and re-initialize the same scope and network. The CLI loads +`./.miden/miden-client.toml` from the current directory first, then falls back to +the global configuration (`$MIDEN_CLIENT_HOME/miden-client.toml` when that +variable is set, otherwise `~/.miden/miden-client.toml`). + +For example, to regenerate a local configuration without deleting its database +or keys, replace `` with that configuration's network. Keep any existing +backup rather than overwriting it: + +```bash title=">_ Terminal" +mv -i .miden/miden-client.toml .miden/miden-client.toml.previous +miden client init --local --network +``` + +For a global configuration, move the corresponding global +`miden-client.toml` aside and omit `--local`. Review the regenerated store and +keystore paths before making transactions, especially if the previous file used +custom paths. + +Regenerating this file does not migrate an older database or make state from one +network usable on another. Keep the old files until you have confirmed the +release's storage compatibility and recovered the accounts you need. + +Do not use `miden client clear-config` for migration or configuration switching. +It recursively removes the entire local `.miden/` directory, including the +store and private keys; when no local `.miden/` directory exists, it falls back +to removing the global directory. Its confirmation prompt does not enumerate +that state. Use it only for confirmed disposable client state after preserving +anything you need. + +## Set Up a Project + +The Quick Start guides let you follow along in either Rust or TypeScript. Scaffold whichever language you prefer — the two tabs in every later code example map 1:1 to the files below. + +### Rust Project + +```bash title=">_ Terminal" +miden new my-test-project +cd my-test-project +``` + +If successful, you'll see a new directory with Miden project files. The generated `rust-toolchain.toml` selects the Rust toolchain and components required by the project. + +For each Rust code example in the following pages, add a new binary under +`integration/src/bin/` and run it with `cargo run --bin --release`. + +### TypeScript Project + +The TypeScript examples use the [`@miden-sdk/miden-sdk`](https://www.npmjs.com/package/@miden-sdk/miden-sdk) package and its `MidenClient` API. The SDK ships WebAssembly that runs in the browser, so the simplest runnable setup is a minimal Vite project: + +```bash title=">_ Terminal" +npm create vite@latest miden-app -- --template vanilla-ts +cd miden-app +npm install @miden-sdk/miden-sdk@^0.16.0 +``` + +Open `src/main.ts` and replace its contents with a simple entry point that calls your demo: + +```ts title="src/main.ts" +import { demo } from "./demo"; + +demo().catch(console.error); +``` + +For each TypeScript snippet in the following pages, save it as `src/demo.ts` (or another name imported from `main.ts`) and run: + +```bash title=">_ Terminal" +npm run dev +``` + +The SDK initialises WebAssembly on first use; open the Vite dev server URL in your browser and watch the devtools console for output. + +--- diff --git a/versioned_docs/version-0.16/builder/get-started/your-first-smart-contract/_category_.json b/versioned_docs/version-0.16/builder/get-started/your-first-smart-contract/_category_.json new file mode 100644 index 00000000..9041f571 --- /dev/null +++ b/versioned_docs/version-0.16/builder/get-started/your-first-smart-contract/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Your First Smart Contract", + "position": 5 +} diff --git a/versioned_docs/version-0.16/builder/get-started/your-first-smart-contract/create.md b/versioned_docs/version-0.16/builder/get-started/your-first-smart-contract/create.md new file mode 100644 index 00000000..e94837d6 --- /dev/null +++ b/versioned_docs/version-0.16/builder/get-started/your-first-smart-contract/create.md @@ -0,0 +1,339 @@ +--- +sidebar_position: 2 +title: Create Your Project +description: Set up a new Miden project and understand the counter contract implementation. +--- + +In this section, you'll set up a new Miden project and understand the structure and implementation of both the counter account contract and increment note script. + +## Setting Up Your Project + +Create a new Miden project using the CLI: + +```bash title=">_ Terminal" +miden new counter-project +cd counter-project +``` + +This creates a workspace with the following structure: + +```text +counter-project/ +├── contracts/ # Each contract as individual crate +│ ├── counter-account/ # Example: Counter account contract +│ │ ├── miden-project.toml # Miden package manifest +│ │ └── src/lib.rs +│ └── increment-note/ # Example: Increment note contract +│ ├── miden-project.toml # Miden package manifest +│ └── src/lib.rs +├── integration/ # Integration crate (scripts + tests) +│ ├── src/ +│ │ ├── bin/ # Rust binaries for onchain interactions +│ │ ├── lib.rs +│ │ └── helpers.rs # Temporary helper file +│ └── tests/ # Test files +├── Cargo.toml # Workspace root +└── rust-toolchain.toml # Rust toolchain specification +``` + +The project follows Miden's design philosophy of clean separation: + +- **`contracts/`**: Your primary working directory for writing Miden smart contract code +- **`integration/`**: All onchain interactions, deployment scripts, and tests + +Each contract is organized as its own individual crate, providing independent versioning, dependencies, and clear isolation between different contracts. Each contract crate also includes a `miden-project.toml` file next to `Cargo.toml`; the Miden compiler uses it to identify the project kind, WIT namespace, and compiled package dependencies. + +### Project Manifests + +The Miden-specific manifest is required for account components, notes, and transaction scripts. The counter account manifest identifies the exported account component interface: + +```toml title="contracts/counter-account/miden-project.toml" +[package] +name = "counter-account" +version = "0.1.0" + +[lib] +kind = "account-component" +# Full `miden:/@` id. The interface segment is the +# kebab-cased component trait name (`CounterContract` -> `counter-contract`). +namespace = "miden:counter-account/counter-contract@0.1.0" +path = "src/lib.rs" + +[dependencies] +miden-core = "*" +miden-protocol = "*" + +[package.metadata.miden] +supported-types = ["RegularAccountImmutableCode"] +``` + +The increment note depends on the counter account package and the generated WIT +that describes its callable interface: + +```toml title="contracts/increment-note/miden-project.toml" +[package] +name = "increment-note" +version = "0.1.0" + +[lib] +kind = "note" +# Notes export a package-derived interface (`miden-`), matching the `#[note]` macro. +namespace = "miden:increment-note/miden-increment-note@0.1.0" +path = "src/lib.rs" + +[dependencies] +miden-core = "*" +miden-protocol = "*" +counter-account = { path = "../counter-account" } + +``` + +Build the contracts with `miden build` in dependency order, as shown below. +Building the account first produces both its `.masp` package and the generated +WIT consumed by the note's `#[account(...)]` wrapper. Plain `cargo check`, +`cargo build`, and IDE analysis do not automatically build or stage those +cross-component dependencies in the published SDK. + +## Building Your Contracts + +You can build individual contracts by navigating to their directory and running the Miden build command: + +```bash title=">_ Terminal" +# Build the counter account contract +cd contracts/counter-account +miden build + +# Build the increment note contract +cd ../increment-note +miden build +``` + +This compiles the Rust contract code into a Miden package (`.masp` file), making it ready for deployment and interaction. + +## Understanding the Counter Account Contract + +Let's examine the counter account contract that comes with the project template. Open `contracts/counter-account/src/lib.rs`: + +```rust title="contracts/counter-account/src/lib.rs" +// Do not link against libstd (i.e. anything defined in `std::`) +#![no_std] +#![feature(alloc_error_handler)] + +// However, we could still use some standard library types while +// remaining no-std compatible, if we uncommented the following lines: +// +// extern crate alloc; + +use miden::{component, component_storage, felt, Felt, StorageMap, Word}; + +/// Storage layout for the counter example. +#[component_storage] +struct CounterContractStorage { + /// Storage map holding the counter value. + #[storage(description = "counter contract storage map")] + count_map: StorageMap, +} + +/// API of the counter contract account component. +#[component] +trait CounterContract { + /// Returns the current counter value stored in the contract's storage map. + #[account_procedure] + fn get_count(&self) -> Felt; + /// Increments the counter value stored in the contract's storage map by one. + #[account_procedure] + fn increment_count(&mut self) -> Felt; +} + +#[component] +impl CounterContract for CounterContractStorage { + fn get_count(&self) -> Felt { + // Define a fixed key for the counter value within the map + let key = Word::new([felt!(0), felt!(0), felt!(0), felt!(1)]); + // Read the value associated with the key from the storage map + self.count_map.get(key) + } + + fn increment_count(&mut self) -> Felt { + // Define the same fixed key + let key = Word::new([felt!(0), felt!(0), felt!(0), felt!(1)]); + // Read the current value + let current_value: Felt = self.count_map.get(key); + // Increment the value by one + let new_value = current_value + felt!(1); + // Write the new value back to the storage map + self.count_map.set(key, new_value); + new_value + } +} +``` + +### Counter Contract Walkthrough + +#### No-std Environment + +```rust +#![no_std] +``` + +Miden contracts run in a `no_std` environment, meaning they don't link against Rust's standard library. This is essential for blockchain execution where contracts need to be deterministic and lightweight. + +#### Miden Library Imports + +```rust +use miden::{component, component_storage, felt, Felt, StorageMap, Word}; +``` + +These imports provide: + +- **`component`**: Macro for defining contract components +- **`component_storage`**: Macro for defining a component's storage struct +- **`felt`**: Macro for creating `Felt` literals (e.g., `felt!(1)`) +- **`Felt`/`Word`**: Miden's native field element and word types +- **`StorageMap`**: Key-value storage within account storage slots + +:::note[`felt` vs `Felt`] +`Felt` is the field element type representing values in the Goldilocks prime field (p = 2^64 - 2^32 + 1). `felt!(1)` creates a `Felt` from an integer literal. A literal outside the field range causes a panic when evaluated. For runtime values, use the fallible `Felt::new(value)` and handle its `Result`. +::: + +#### Contract Structure Definition + +```rust +#[component_storage] +struct CounterContractStorage { + /// Storage map holding the counter value. + #[storage(description = "counter contract storage map")] + count_map: StorageMap, +} + +#[component] +trait CounterContract { + /// Returns the current counter value stored in the contract's storage map. + #[account_procedure] + fn get_count(&self) -> Felt; + /// Increments the counter value stored in the contract's storage map by one. + #[account_procedure] + fn increment_count(&mut self) -> Felt; +} +``` + +The `#[component_storage]` attribute marks the storage struct for this Miden [Account component](../../../reference/protocol/account/index.md), while the `#[component]` trait defines the component's interface. Every callable trait method must carry `#[account_procedure]`; an unmarked method is not exported. The `count_map` field is a `StorageMap` stored in a named storage slot of the account. Storage slots are identified by name rather than explicit index numbers — the slot name is derived automatically from the component's manifest namespace and field name (e.g., `counter_account::counter_contract::count_map`). + +**Important**: Miden account storage is organized into named slots. Each slot holds either a single typed value or a key-value map. Here, `StorageMap` provides typed access to a map-backed slot, converting its keys and values to and from `Word`. Each `Word` consists of four field elements (`Felt`), and each `Felt` belongs to the Goldilocks prime field and is represented using 64 bits. + +#### Contract Implementation + +```rust +impl CounterContract for CounterContractStorage { + // Function implementations... +} +``` + +The `CounterContract` trait defines the external interface that other contracts and notes can call. The `impl CounterContract for CounterContractStorage` block provides the behavior over the component's storage. + +#### Storage Key Strategy + +```rust +let key = Word::new([felt!(0), felt!(0), felt!(0), felt!(1)]); +``` + +Both functions in the counter contract use the same fixed key `[0, 0, 0, 1]` to store and retrieve the counter value within the storage map. This demonstrates a simple but effective storage pattern. + +## Understanding the Increment Note Script + +Now let's examine the increment note script at `contracts/increment-note/src/lib.rs`: + +```rust title="contracts/increment-note/src/lib.rs" +// Do not link against libstd (i.e. anything defined in `std::`) +#![no_std] +#![feature(alloc_error_handler)] + +// However, we could still use some standard library types while +// remaining no-std compatible, if we uncommented the following lines: +// +// extern crate alloc; +// use alloc::vec::Vec; + +use miden::*; + +/// Native account of the note: exposes the `counter-contract` component methods gathered from the `counter-contract` package. +#[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); + } +} +``` + +### Increment Note Script Walkthrough + +#### No-std Setup + +Like the account contract, the note script uses `#![no_std]` and enables the `alloc_error_handler` language feature. + +#### Miden Imports + +```rust +use miden::*; +``` + +The note script glob-imports the `miden` prelude: the `#[note]` and `#[account]` macros, the basic field/word types (`Felt`, `Word`), and free functions such as `assert_eq`. Listing the imports individually is easy to get wrong — `assert_eq` here is a function from the prelude, not Rust's `assert_eq!` macro, so omitting it fails to compile. + +#### Note Script Structure + +Note scripts use a struct-based pattern. The `#[note]` attribute on both the struct and `impl` block marks this as a Miden note script component: + +```rust +#[note] +struct IncrementNote; + +#[note] +impl IncrementNote { + #[note_script] + fn run(self, _arg: Word, account: &mut Wallet) { ... } +} +``` + +The struct definition (`IncrementNote`) provides a named type for the note script. Unlike account contracts, note scripts don't store persistent data — the struct serves as the entry point container. The `Wallet` type is declared with `#[account(counter_account::CounterContract)]`, which binds the note to the counter account interface generated from `miden-project.toml`. + +Learn more about [note scripts in the Miden documentation](../../../reference/protocol/note.md). + +#### The Note Script Function + +```rust +#[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); +} +``` + +The `#[note_script]` attribute marks this method as the entry point for note execution. The `self` parameter is required for methods in the `impl` block. The function: + +1. **Gets the initial counter value** using the bound counter account interface +2. **Calls increment_count()** to increment the counter on the target account +3. **Verifies the operation succeeded** by checking the final value matches expectations + +This demonstrates how note scripts interact with account contracts through their public interfaces, calling functions to change state. + +The counter example demonstrates a complete interaction pattern: the account contract manages persistent state, while the note script provides a mechanism to trigger state changes through note consumption. + +## Next Steps + +Now that you understand the contract code structure, let's move on to [deploying your contract](./deploy) and learn how the integration folder enables interaction with your contracts on the Miden network. + +--- diff --git a/versioned_docs/version-0.16/builder/get-started/your-first-smart-contract/deploy.md b/versioned_docs/version-0.16/builder/get-started/your-first-smart-contract/deploy.md new file mode 100644 index 00000000..69443976 --- /dev/null +++ b/versioned_docs/version-0.16/builder/get-started/your-first-smart-contract/deploy.md @@ -0,0 +1,246 @@ +--- +sidebar_position: 3 +title: Deploy Your Contract +description: Learn about the integration folder and deploy your counter contract to the Miden testnet. +--- + +# Deploy Your Contract + +In this section, you'll learn about how to deploy and interact with your counter contract using the included "increment-count" scripts. + +## Understanding the Integration Folder + +The `integration/` folder is a crucial part of your Miden project workspace. It serves as the command center for all interactions with your smart contracts. Let's explore its structure and purpose. + +From the workspace root, inspect your project's integration folder: + +```bash title=">_ Terminal" +ls -la integration +``` + +You'll see a structure like: + +```text +integration/ +├── Cargo.toml # Integration crate configuration +├── src/ +│ ├── bin/ # Executable scripts for onchain interactions +│ │ └── increment_count.rs # Script to deploy and increment counter +│ ├── helpers.rs # Temporary helper file +│ └── lib.rs # Exports helpers +└── tests/ # Integration tests + └── counter_test.rs # Tests for counter contract +``` + +## Purpose of the Integration Folder + +The integration folder serves two essential functions in Miden development: + +### 1. Contract Interaction Scripts (Binary Executables) + +Think of the scripts in `src/bin/` as Miden's equivalent to [**Foundry scripts**](https://www.getfoundry.sh/forge/scripting). These are executable Rust binaries that handle all your contract interactions: + +- **Contract Deployment**: Scripts that create and deploy accounts to the network +- **Function/Procedure Calls**: Scripts that interact with deployed contracts through notes or [transaction scripts](../../../reference/protocol/transaction.md#transaction-lifecycle) +- **State Queries**: Scripts that read contract state from the network +- **Operations**: Scripts for contract upgrades, configuration changes, etc. + +Each binary is designed to handle a specific task. + +### 2. Testing Infrastructure + +All testing logic for your smart contracts lives here: + +- **Integration Tests**: End-to-end tests that verify contract behavior on Testnet +- **Mockchain Tests**: Local testing using Miden's testing framework + +This separation ensures your contract logic in `contracts/` remains clean and focused while all interaction complexity is managed in the integration layer. + +## The Increment Count Script + +Let's examine the `increment_count.rs` script located at `integration/src/bin/increment_count.rs`. This script demonstrates the complete lifecycle of deploying and interacting with your counter contract. + +The script performs these key operations: + +1. **Sets up a Miden client** connected to the testnet +2. **Builds both contract packages** (counter account and increment note) +3. **Creates the counter account** with initial storage configuration +4. **Creates a sender account** for publishing notes +5. **Creates and publishes the increment note** +6. **Consumes the note** to trigger the counter increment + +### Running the Script + +From the workspace root, run the increment script: + +```bash title=">_ Terminal" +cd integration +cargo run --bin increment_count --release +``` + +
+Expected Output + +```text +Latest block: 1238402 +Account ID: V1(AccountIdV1 { suffix: 6091438912547090176, prefix: 14314767458661568337 }) +Sender account ID: "0x7d9c7007a5773c116bc58f9f28762b" +Counter note hash: "0xa3642c5eb08d8298a2fb8ed7f268f13c4139771c77540768a7347c882f47ab4f" +Note publish transaction ID: "0xfc190c8edbf972115cd5f00b22c8eac06c9515d0403b84ff697efa7093d6b8a7" +Consume transaction ID: "0xa1aa7971e146710df74a674b7e99d1968bec8d2db1c4da6077a7e22843c92045" +``` + +
+ +Congratulations, you have successfully deployed the Counter Contract to the Miden Testnet, and incremented its count by one! You can verify your transaction on [MidenScan](https://testnet.midenscan.com) by searching for your transaction ID. + +### What Happens During Execution + +The script demonstrates Miden's deployment flow: + +1. **Contract Building**: The script compiles both the counter account and increment note contracts +2. **Account Creation**: Creates a counter account with initial storage (counter value = 0) +3. **Note Publishing**: Creates an increment note and publishes it to the network +4. **Note Consumption**: The counter account consumes the note, executing the increment logic +5. **State Update**: The counter value increases and the change is recorded onchain + +This process shows how Miden contracts are deployed through state changes rather than separate deployment transactions. + +**Miden's Deployment Flow**: Creating an account locally does not deploy it. Its first committed state-changing transaction publishes it onchain. If the account first consumes a funding note, that transaction publishes it with the counter still at zero. Consuming the increment note then changes the count to one. + +## How the Scripts Work + +The integration scripts connect to the Miden client and compile each contract by invoking `miden build` as a separate process. After each build, the helper loads the generated Miden package into the native client. You don't need to build the contracts manually before running the script. + +Next, we look into how the scripts convert your Rust contract code into deployable Miden contracts. + +## Script Breakdown + +Let's examine key parts of the increment script: + +### Client Setup + +```rust +let ClientSetup { mut client, keystore } = setup_client().await?; +let sync_summary = client.sync_state().await?; +``` + +This establishes a connection to the Miden testnet and synchronizes with the latest network state. + +### Building Contracts from Source + +The first step is building the Rust contracts into Miden packages: + +```rust +// Build the counter account contract from source +let counter_package = Arc::new( + build_project_in_dir(Path::new("../contracts/counter-account"), true) + .context("Failed to build counter account contract")? +); + +// Build the increment note script from source +let note_package = Arc::new( + build_project_in_dir(Path::new("../contracts/increment-note"), true) + .context("Failed to build increment note contract")? +); +``` + +The `build_project_in_dir()` function: + +- Takes the path to a contract project +- Invokes `miden build` in a separate process, using `--release` when requested +- Resolves the generated `.masp` artifact under the project's `target/miden/` directory +- Reads and deserializes the compiled package for the native client + +These packages contain all the information needed to deploy and interact with your contracts on the Miden network. + +### Converting Packages to Deployable Accounts + +Once we have the compiled packages, we convert them into deployable accounts and notes: + +```rust +// Configure initial storage for the counter account. +let counter_storage_slot = counter_storage_slot()?; +let mut init_storage_data = InitStorageData::default(); +init_storage_data + .insert_map_entry(counter_storage_slot, COUNTER_STORAGE_KEY, 0_u64) + .context("Failed to seed counter storage")?; +let counter_cfg = AccountCreationConfig { + init_storage_data, + ..Default::default() +}; + +// Convert the counter package into a deployable account +let counter_account = create_account_from_package( + &mut client, + counter_package.clone(), + counter_cfg +) +.await +.context("Failed to create counter account")?; +``` + +The `create_account_from_package()` function: + +- Takes the compiled contract package +- Combines it with the provided configuration (storage, settings, etc.) +- Creates a deployable Miden account that can be used in transactions + +**Important**: Accounts that use storage must have that storage seeded when instantiating the account. Storage slots are identified by name rather than index. The slot name follows the pattern `::::`, derived from the component's manifest namespace. We seed the storage with: + +- A named `StorageMap` slot, returned by the `counter_storage_slot()` helper (`counter_account::counter_contract::count_map`) +- The counter key `COUNTER_STORAGE_KEY` (`[0, 0, 0, 1]`), mapped to the initial count `0` + +`InitStorageData` carries these seed values into `AccountComponent::from_package()`, which the helper calls for you. + +This pre-initialization ensures the account's storage is properly configured before deployment. + +### Converting Packages to Executable Notes + +Similarly, we convert the note package into an executable note: + +```rust +// Build the increment note directly from the compiled package. +let counter_note = NoteBuilder::new(sender_account.id(), client.rng()) + .package((*note_package).clone()) + .tag(0) + .build() + .context("Failed to create counter note from package")?; + +// Publish the note to the network +let note_publish_request = TransactionRequestBuilder::new() + .own_output_notes(vec![counter_note.clone()]) + .build() + .context("Failed to build note publish transaction request")?; +``` + +`NoteBuilder` (from `miden_standards::testing::note`): + +- Takes the sender account ID and the client's RNG +- Accepts the compiled note script package via `.package()` +- Produces an executable note containing the increment script logic +- The note can then be published to the network and consumed by the target (counter) account + +This demonstrates the complete workflow: Rust source code → compiled packages → deployable accounts/notes → network transactions. + +### Note Consumption + +```rust +let consume_note_request = TransactionRequestBuilder::new() + .input_notes([(counter_note.clone(), None)]) + .build() + .context("Failed to build consume note transaction request")?; + +let consume_tx_id = client + .submit_new_transaction(counter_account.id(), consume_note_request) + .await + .context("Failed to create consume note transaction")?; +``` + +The counter account consumes the increment note, executing the note script which calls the counter's increment function. + +## Next Steps + +Congratulations! You've successfully deployed and interacted with your first Miden smart contract. The integration folder provides the foundation for managing all aspects of your contract lifecycle. + +This completes the core smart contract development workflow on Miden. You're now equipped to build and deploy your own smart contracts using these patterns and tools! diff --git a/versioned_docs/version-0.16/builder/get-started/your-first-smart-contract/index.md b/versioned_docs/version-0.16/builder/get-started/your-first-smart-contract/index.md new file mode 100644 index 00000000..826cec03 --- /dev/null +++ b/versioned_docs/version-0.16/builder/get-started/your-first-smart-contract/index.md @@ -0,0 +1,71 @@ +--- +sidebar_position: 1 +title: Your First Smart Contract +description: Learn to build, test, and deploy smart contracts on Miden using Rust. +--- + +# Your First Smart Contract + +Welcome to the **Your First Smart Contract** guide! This tutorial will walk you through building, testing, and deploying your first Miden smart contract using Rust. + +## What You'll Learn + +By the end of this tutorial, you will have: + +- **Set up a Miden project** with the proper workspace structure +- **Written and understood** a counter smart contract and increment note +- **Deployed your contract** to the Miden testnet using integration scripts +- **Mastered the fundamentals** of Miden's account-based smart contract model + +This guide focuses on practical, hands-on learning. You'll work with real code and deploy to a live network, giving you everything needed to start building on Miden. + +## What We'll Build + +You'll create a **counter contract system** consisting of: + +- **Counter Account**: A smart contract that stores and manages a counter value in its storage +- **Increment Note**: A note script that increments the counter when executed +- **Integration Scripts**: Deployment and interaction scripts for managing the contract lifecycle + +The counter example is designed to teach core Miden concepts through a simple, understandable use case that you can extend for your own projects. + +## Prerequisites + +Before starting this guide, ensure you have completed the [Installation](../setup/installation) tutorial and have: + +- **Rust toolchain** installed and configured +- **midenup toolchain** installed with Miden CLI tools + +:::tip Prerequisites Required +You need those development tools installed for this guide. If you haven't set up your environment yet, please complete the [installation](../setup/installation) guide first. +::: + +## No Prior Experience Required + +This tutorial is designed for developers new to Miden. You don't need prior experience with: + +- Miden's account model or note-based transactions +- Smart contract development on Miden +- The specifics of Rust development for blockchain + +We'll explain these concepts as we encounter them in the tutorial. + +## Getting Help + +If you get stuck during this tutorial: + +- Check the rest of the Miden Docs for detailed technical references +- Join the [Build On Miden](https://t.me/BuildOnMiden) Telegram community for support +- Review the code examples in your project's `contracts` and `integration/` folder + +## Guide Structure + +This tutorial is divided into focused sections: + +1. **[Create Your Project](./create)** - Set up your workspace and understand the counter contract code +2. **[Deploy Your Contract](./deploy)** - Learn the integration folder and deploy to testnet +3. **[Test Your Contract](./test)** - Learn how to effectively test your contracts + +Each section builds on the previous one, so we recommend following them in order. + +Ready to build your first Miden smart contract? Let's get started! diff --git a/versioned_docs/version-0.16/builder/get-started/your-first-smart-contract/test.md b/versioned_docs/version-0.16/builder/get-started/your-first-smart-contract/test.md new file mode 100644 index 00000000..62f386f3 --- /dev/null +++ b/versioned_docs/version-0.16/builder/get-started/your-first-smart-contract/test.md @@ -0,0 +1,314 @@ +--- +sidebar_position: 4 +title: Test Your Contract +description: Learn how to write and run tests for your Miden smart contracts using the integration testing framework. +--- + +# Test Your Contract + +In this final section, you'll learn how to test your counter contract using Miden's **Mockchain** - a purpose-built testing framework that enables fast, local testing without network dependencies. + +## Test Structure and Organization + +All tests for your smart contracts should be placed in the `integration/tests/` folder. This follows the same separation of concerns we've seen throughout the project: + +- **`contracts/`**: Contains your contract source code +- **`integration/src/bin/`**: Contains deployment and interaction scripts +- **`integration/tests/`**: Contains all test files for your contracts + +This structure keeps your contract logic clean while providing a dedicated space for comprehensive testing. + +## Local Testing with Mockchain + +For most testing scenarios, we use Miden's **Mockchain** - a local, mocked blockchain instance specifically designed for testing. While you can also create tests that use the Miden client for end-to-end testing and onchain interactions, the Mockchain provides the best developer experience for unit and integration testing. + +### What is the Mockchain? + +The Mockchain is Miden's purpose-built testing framework that provides several key advantages over testing against a live network: + +- **Blazing Fast Tests**: Run tests locally without network latency or external dependencies +- **Full State Control**: Manipulate blockchain state precisely to create specific test scenarios +- **Simpler Code**: Cleaner, more focused test logic without network complexity +- **Deterministic Results**: Consistent test outcomes independent of network conditions +- **Debugging Capabilities**: Detailed inspection of transaction execution and state changes + +This makes testing faster, more reliable, and easier to debug than testing against the testnet. + +## Running the Tests + +Execute your tests from the integration directory using the standard Cargo test command: + +```bash title="Terminal" +cd integration +cargo test --release +``` + +You should see output confirming the test passes: + +```text title="Expected Output" +running 1 test +test counter_test ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out +``` + +## Understanding the Mockchain Test + +Your project includes a comprehensive test file at `integration/tests/counter_test.rs` that demonstrates how to test the counter contract using the Mockchain. Let's walk through this test to understand the testing patterns: + +
+Test File + +```rust title="integration/tests/counter_test.rs" +use std::{path::Path, sync::Arc}; + +use anyhow::Context; +use integration::helpers::{build_project_in_dir, counter_storage_slot, COUNTER_STORAGE_KEY}; +use miden_client::{ + account::{ + component::InitStorageData, AccountBuilder, AccountComponent, AccountType, StorageMapKey, + }, + auth::AuthSchemeId, + crypto::RandomCoin, + note::NoteScript, + transaction::RawOutputNote, + Word, +}; +use miden_standards::testing::note::NoteBuilder; +use miden_testing::{AccountState, Auth, MockChain}; + +#[tokio::test] +async fn counter_test() -> anyhow::Result<()> { + // Test that after executing the increment note, the counter value is incremented by 1 + let mut builder = MockChain::builder(); + + // Create note sender account + let sender = builder.add_existing_wallet(Auth::BasicAuth { + auth_scheme: AuthSchemeId::Falcon512Poseidon2, + })?; + + // Build contracts + let contract_package = Arc::new(build_project_in_dir( + Path::new("../contracts/counter-account"), + true, + )?); + let note_package = Arc::new(build_project_in_dir( + Path::new("../contracts/increment-note"), + true, + )?); + + // Create the counter account with its initial storage through the component schema. + 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)?; + + let counter_component = AccountComponent::from_package(&contract_package, &init_storage_data) + .context("failed to build account component from counter package")?; + let counter_account = builder.add_account_from_builder( + Auth::BasicAuth { + auth_scheme: AuthSchemeId::Falcon512Poseidon2, + }, + AccountBuilder::new([3_u8; 32]) + .account_type(AccountType::Public) + .with_component(counter_component), + AccountState::Exists, + )?; + + let mut note_rng = RandomCoin::new(Word::from( + NoteScript::from_package(note_package.as_ref()) + .context("failed to build note script from package")? + .root(), + )); + let counter_note = NoteBuilder::new(sender.id(), &mut note_rng) + .package((*note_package).clone()) + .build() + .context("failed to build counter note from package")?; + + // add counter account and note to mockchain + builder.add_output_note(RawOutputNote::Full(counter_note.clone())); + + // Build the mock chain + let mut mock_chain = builder.build()?; + + // Build the transaction from the committed account and input note + let transaction = mock_chain + .build_transaction(counter_account.id()) + .authenticated_input_note(counter_note.id()) + .build()?; + + // Execute the transaction + let executed_transaction = transaction.execute().await?; + + // Add the executed transaction to the mockchain + mock_chain.add_pending_executed_transaction(&executed_transaction)?; + mock_chain.prove_next_block()?; + + // Get the count from the updated counter account + let count = mock_chain + .committed_account(counter_account.id())? + .storage() + .get_map_item( + &counter_storage_slot, + StorageMapKey::new(COUNTER_STORAGE_KEY), + ) + .expect("Failed to get counter value from storage slot"); + + assert_eq!( + count[0].as_canonical_u64(), + 1, + "Count value is not equal to 1" + ); + Ok(()) +} +``` + +
+ +## Test Code Walkthrough + +Let's break down this test step by step to understand how Mockchain testing works. + +### 1. Setting Up the Mockchain Builder + +```rust +let mut builder = MockChain::builder(); +let sender = builder.add_existing_wallet(Auth::BasicAuth { + auth_scheme: AuthSchemeId::Falcon512Poseidon2, +})?; +``` + +**What's happening:** + +- We instantiate the **Mockchain builder**, which is used to configure our testing environment +- We create a **sender account** using basic authentication - this account will publish the increment note +- The builder pattern allows us to incrementally add all the components needed for our test + +### 2. Building the Contract Packages + +```rust +let contract_package = Arc::new(build_project_in_dir( + Path::new("../contracts/counter-account"), + true, +)?); +let note_package = Arc::new(build_project_in_dir( + Path::new("../contracts/increment-note"), + true, +)?); +``` + +**What's happening:** + +- Just like in the deployment script, we **build both contract packages** (counter account and increment note) +- The `build_project_in_dir()` function compiles the Rust contracts into Miden packages +- We wrap them in `Arc` for efficient memory sharing across the test + +### 3. Creating the Test Account and Note + +```rust +// Create the counter account with its initial storage through the component schema. +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)?; + +let counter_component = AccountComponent::from_package(&contract_package, &init_storage_data) + .context("failed to build account component from counter package")?; +let counter_account = builder.add_account_from_builder( + Auth::BasicAuth { + auth_scheme: AuthSchemeId::Falcon512Poseidon2, + }, + AccountBuilder::new([3_u8; 32]) + .account_type(AccountType::Public) + .with_component(counter_component), + AccountState::Exists, +)?; + +let mut note_rng = RandomCoin::new(Word::from( + NoteScript::from_package(note_package.as_ref()) + .context("failed to build note script from package")? + .root(), +)); +let counter_note = NoteBuilder::new(sender.id(), &mut note_rng) + .package((*note_package).clone()) + .build() + .context("failed to build counter note from package")?; +``` + +**What's happening:** + +- We seed the **counter account's initial storage** through `InitStorageData`, mapping `COUNTER_STORAGE_KEY` to `0` inside the named slot returned by `counter_storage_slot()` +- `AccountComponent::from_package()` turns the compiled package plus that storage seed into an account component +- `builder.add_account_from_builder()` registers the account with the mockchain in the `AccountState::Exists` state, so it behaves like an already-deployed account +- The note is built with `NoteBuilder`, seeded from a `RandomCoin` derived from the note script's MAST root — this keeps note generation deterministic across test runs + +### 4. Adding the Note to the Mockchain + +```rust +builder.add_output_note(RawOutputNote::Full(counter_note.clone())); +let mut mock_chain = builder.build()?; +``` + +**What's happening:** + +- We **add the increment note** as a full output note to the mockchain +- We **build the mockchain** - now we have a complete testing environment ready to use + +The counter account does not need a separate `add_account()` call: `add_account_from_builder()` already registered it in the previous step. + +### 5. Creating and Executing the Transaction + +```rust +let transaction = mock_chain + .build_transaction(counter_account.id()) + .authenticated_input_note(counter_note.id()) + .build()?; + +let executed_transaction = transaction.execute().await?; +``` + +**What's happening:** + +- We **build the transaction** against the committed counter account and add the counter note as an authenticated input +- We **execute the transaction** - this runs the increment logic locally in the mockchain + +### 6. Verifying the Results + +```rust +// Add the executed transaction to the mockchain +mock_chain.add_pending_executed_transaction(&executed_transaction)?; +mock_chain.prove_next_block()?; + +// Get the count from the updated counter account +let count = mock_chain + .committed_account(counter_account.id())? + .storage() + .get_map_item( + &counter_storage_slot, + StorageMapKey::new(COUNTER_STORAGE_KEY), + ) + .expect("Failed to get counter value from storage slot"); + +assert_eq!( + count[0].as_canonical_u64(), + 1, + "Count value is not equal to 1" +); +``` + +**What's happening:** + +- We **add the executed transaction** to the mockchain and prove the next block, which commits the new account state +- We **read the counter value** back from `mock_chain.committed_account()` — the committed state already reflects the transaction, so there is no need to apply the account delta by hand +- We **assert that the count equals 1** - verifying the increment operation worked correctly + +The test verifies the complete flow: the increment note successfully increments the counter from 0 to 1, proving our smart contract works as expected. + +## Next Steps + +Congratulations! You've successfully completed the Miden smart contract quick start guide. You're now equipped to build more sophisticated smart contracts on Miden. Consider exploring: + +To deepen your knowledge, we recommend exploring the following resources: + +- Visit the [Tutorials section](../../tutorials/) for detailed, hands-on guides on topics such as contract interactions, advanced storage, custom note scripting, and integrating with external applications. +- For in-depth technical explanations, consult the [Reference section](../../../reference/) of the documentation. Here you'll find comprehensive information on Miden's architecture, account model, transaction lifecycle, and the underlying zero-knowledge technology that powers the network. + +The foundational patterns and concepts you've practiced in this Quick Start will enable you to build complex, privacy-preserving applications on the Miden network. Continue with the resources above to take your development further! diff --git a/versioned_docs/version-0.16/builder/glossary.md b/versioned_docs/version-0.16/builder/glossary.md new file mode 100644 index 00000000..dc20cb0b --- /dev/null +++ b/versioned_docs/version-0.16/builder/glossary.md @@ -0,0 +1,144 @@ +--- +title: Glossary +description: "Key terms and definitions used throughout the Miden docs — grouped by area (accounts, notes, protocol, Guardian, cryptography)." +pagination_next: null +--- + +# Glossary + +Key terms and definitions used throughout the Miden docs. Grouped thematically — if you're not sure where something lives, use your browser's find (⌘F / Ctrl+F). + + + + Account, AccountCode, AccountComponent, AccountId, AccountStorage, MultiSig, AccountBuilder. + + + Note, Note script, Note tag, Note ID, Nullifier, Asset, AssetVault. + + + Block, Batch, Kernel, Prover, Miden Assembly, Felt, Word. + + + Miden Guardian, Canonicalization, Delta, Delta Proposal, Threshold Signature. + + + +## Accounts + +### Account + +An account is a data structure that represents an entity (user account, smart contract) on the Miden blockchain — analogous to smart contracts. + +### Account builder + +Account builder provides a structured way to create and initialize new accounts on the Miden network with specific properties, permissions, and initial state. + +### AccountCode + +The executable code associated with an account. + +### AccountComponent + +A modular unit of code representing a piece of an account's functionality. Each `AccountCode` is composed of multiple `AccountComponent`s. + +### AccountId + +A value that uniquely identifies each account on Miden. + +### AccountIdVersion + +Represents the different versions of account identifier formats supported by Miden. + +### AccountStorage + +A key-value store associated with an account. Made up of storage slots. + +### MultiSig + +A multi-signature account on Miden that requires a configurable threshold (N-of-M) of authorized signers to approve transactions before execution. MultiSig workflows are coordinated through [Miden Guardian](./miden-guardian/). + +## Notes & assets + +### Note + +A fundamental data structure that represents an offchain asset or a piece of information that can be transferred between accounts. Miden's UTXO-like model is designed around notes. **Output notes** are new notes created by a transaction; **input notes** are those consumed (spent) by a transaction. + +### Note script + +A program that defines the rules and conditions under which a note can be consumed. + +### Note tag + +An identifier or metadata associated with notes that provides additional filtering capabilities. + +### Note ID + +A unique identifier assigned to each note to distinguish it from other notes. + +### Nullifier + +A cryptographic commitment that marks a note as spent, preventing it from being consumed again. + +### Asset + +A digital resource with value that can be owned, transferred, and managed within the Miden blockchain. + +### AssetVault + +The container used for managing assets within accounts. Provides a way to store and transfer assets associated with each account. + +## Protocol & VM + +### Block + +A fundamental data structure that groups multiple batches together and forms the blockchain's state. + +### Batch + +A collection of transactions grouped together, to be aggregated into blocks — improves network throughput. + +### Kernel + +A fundamental module of the Miden VM that acts as a base layer, providing core functionality and security guarantees for the protocol. + +### Prover + +Responsible for generating zero-knowledge proofs that attest to the correctness of program execution without revealing the underlying data. + +### Miden Operator + +An entity that runs the node infrastructure powering the Miden network. Operators verify transaction proofs, record created notes and consumed-note nullifiers in the state databases, build the batches and blocks that progress the chain, and execute network transactions on behalf of users (for example, consuming a note against a public DEX contract). + +### Miden Assembly + +An assembly language specifically designed for the Miden VM — a low-level language with specialized instructions optimized for zero-knowledge proof generation. + +### Felt + +A Felt (Field Element) is the primitive cryptographic data type used by the Miden VM. It represents an element in the finite (Goldilocks) field: `p = 2^64 − 2^32 + 1`. + +### Word + +A data structure that represents the basic unit of computation and storage in Miden. Composed of four `Felt`s. + +## Guardian & multisig + +### Miden Guardian + +Infrastructure built by OpenZeppelin for managing private account state on Miden. Guardian provides a server and client SDKs for backing up, syncing, and coordinating state across devices and parties without trust assumptions. See the [Miden Guardian documentation](./miden-guardian/). + +### Canonicalization + +The background process by which [Miden Guardian](./miden-guardian/) promotes candidate deltas to canonical status by verifying them against the Miden network. + +### Delta + +A Delta represents the changes between two states `s` and `s'`. Applying a Delta `d` to `s` produces `s'`. + +### Delta Proposal + +A coordination mechanism in [Miden Guardian](./miden-guardian/) that allows multiple signers to propose, review, and co-sign state changes before they are promoted to a canonical delta. + +### Threshold Signature + +A cryptographic scheme where a minimum number of signers (the threshold) out of a total group must sign for a transaction to be valid. Used in Miden's MultiSig accounts. diff --git a/versioned_docs/version-0.16/builder/index.md b/versioned_docs/version-0.16/builder/index.md new file mode 100644 index 00000000..e2a97710 --- /dev/null +++ b/versioned_docs/version-0.16/builder/index.md @@ -0,0 +1,89 @@ +--- +sidebar_label: Introduction +sidebar_position: 0 +pagination_next: null +--- + +# Build on Miden + +Miden is a zero-knowledge layer 2 where every account is an independent state machine and state is private by default. You author accounts, notes, and transactions in Rust, compile them to Miden Assembly (MASM), and prove them client-side — the network verifies each proof without ever seeing your private state. + +This is the developer's entry point. **New to Miden?** Start with [Get started](./get-started) to install the toolchain and send your first transaction, then build [Your first smart contract](./get-started/your-first-smart-contract), and dip into the [Reference](../reference) when you want to know how it all works underneath. Already oriented? Jump straight to any section below. + +## Start here + + + + Install midenup, create a wallet, and send your first transaction — in under ten minutes. + + + Walk through writing, proving, and deploying a counter contract in Rust. + + + + +Building against the public testnet? Grab free test assets from the [faucet](https://faucet.testnet.miden.io/), and find every public endpoint — RPC, block explorer, status — on the [Network](./tools/network) page. + + +## Build + + + + Accounts, notes, storage, components, transactions — the full Rust SDK surface. + + + Real-world examples: the Miden Bank, private multisig, custom note scripts. + + + Testing, debugging, and common pitfalls when writing Miden programs. + + + Rust, Web, and React SDKs · playground · block explorer · CLI. + + + +## Ship + + + + Breaking changes, renames, and new features across accounts, notes, transactions, MASM, and the client. + + + Backup, sync, and coordinate private account state across devices. + + + Multi-party threshold signature workflows powered by Guardian. + + + +## Reference + + + + Frequently asked questions about Miden. + + + Key terms and definitions used throughout the docs. + + + +## Community + +- [Telegram](https://t.me/BuildOnMiden) — technical discussion +- [GitHub](https://github.com/0xMiden) — source code +- [Roadmap](https://miden.xyz/roadmap) — what's coming next + +import SectionLinks from '@site/src/components/SectionLinks'; + + + +--- + +Licensed under the [MIT License](http://opensource.org/licenses/MIT). diff --git a/versioned_docs/version-0.16/builder/miden-guardian/architecture/_category_.json b/versioned_docs/version-0.16/builder/miden-guardian/architecture/_category_.json new file mode 100644 index 00000000..b7a3ae9d --- /dev/null +++ b/versioned_docs/version-0.16/builder/miden-guardian/architecture/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Architecture", + "position": 2 +} diff --git a/versioned_docs/version-0.16/builder/miden-guardian/architecture/infra.md b/versioned_docs/version-0.16/builder/miden-guardian/architecture/infra.md new file mode 100644 index 00000000..cd357a3e --- /dev/null +++ b/versioned_docs/version-0.16/builder/miden-guardian/architecture/infra.md @@ -0,0 +1,296 @@ +--- +title: "Guardian AWS Deployment Architecture" +sidebar_position: 2 +--- +This document explains how the Guardian server is deployed on AWS and how the +resources in [`infra/`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/infra/) fit together. It is a map for engineers +who already know the application and want to understand the runtime topology, +or for operators who need to know which Terraform file owns which AWS resource. + +Companion docs: +- [`infra/README.md`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/infra/README.md) — Terraform variables reference and + raw `terraform apply` workflow. +- [`docs/SERVER_AWS_DEPLOY.md`](../operations/server-aws-deploy.md) — end-to-end deploy + guide via `scripts/aws-deploy.sh`. +- [`docs/architecture/services.md`](./services.md) — logical service + decomposition inside the server process. + +## TL;DR + +The stack is a single Fargate service behind an Application Load Balancer, +backed by RDS PostgreSQL, with secrets in AWS Secrets Manager and DNS managed +through Route 53 and/or Cloudflare. The same Terraform configuration deploys +both `dev` and `prod` profiles — `prod` adds autoscaling, RDS Proxy, and +storage autoscaling on top of the same base topology. + +## Topology + +```mermaid +flowchart LR + Client["Client
(SDK / curl / grpcurl)"] + DNS["Route 53 / Cloudflare
guardian.openzeppelin.com"] + ACM["ACM certificate"] + + subgraph VPC["AWS VPC"] + direction LR + + subgraph PublicSubnets["Public subnets (≥2 AZs)"] + ALB["Application Load Balancer
:80 → redirect
:443 HTTPS → server:3000
:443 /guardian.Guardian/* → server:50051"] + end + + subgraph TaskSubnets["ECS task subnets"] + Fargate["ECS Fargate Service
guardian server
:3000 HTTP · :50051 gRPC"] + end + + subgraph DataSubnets["DB subnets (≥2 AZs)"] + Proxy["RDS Proxy
(prod only)"] + RDS[("RDS PostgreSQL")] + end + + Fargate -- "TLS, prod" --> Proxy + Proxy --> RDS + Fargate -- "dev: direct" --> RDS + end + + subgraph AWSServices["AWS managed services"] + SM["Secrets Manager
DATABASE_URL · ack keys ·
operator pubkeys · EVM config"] + CW["CloudWatch Logs"] + ECR["ECR
guardian-server image"] + IAM["IAM
task / execution / proxy roles"] + end + + Client --> DNS --> ALB + ACM -. cert .-> ALB + Fargate -. pull image .-> ECR + Fargate -- "stdout / stderr" --> CW + Fargate -- "GetSecretValue" --> SM + Proxy -- "GetSecretValue (creds)" --> SM + IAM -. assume role .-> Fargate + IAM -. assume role .-> Proxy +``` + +### Why this shape + +- **One service, one image.** Guardian is a single Rust binary that exposes + HTTP and gRPC from the same process. There is no API gateway, no sidecar. + The ALB does layer-7 routing on path so HTTPS clients and gRPC clients + share port `443` on the public hostname. +- **State lives in Postgres.** Authentication, account state, deltas, + proposals, audit logs all persist in RDS. The server itself is stateless; + scaling is "add more tasks". +- **Identity lives in Secrets Manager.** ACK signing keys (Falcon + ECDSA) + used to authenticate Guardian's responses are stored in Secrets Manager in + `prod` and bootstrapped into the container's filesystem keystore at + startup. In `dev` the server auto-generates ephemeral keys. +- **Stage profile toggles capacity, not topology.** `dev` and `prod` deploy + the same set of resource types; `prod` flips autoscaling on, sizes RDS up, + and inserts RDS Proxy between ECS and RDS. There is no separate `prod` + Terraform module. + +## Request flow + +```mermaid +sequenceDiagram + participant C as Client + participant DNS as Route53/Cloudflare + participant ALB as ALB (:443) + participant S as Server task (Fargate) + participant P as RDS Proxy + participant DB as RDS Postgres + participant SM as Secrets Manager + + C->>DNS: resolve guardian.openzeppelin.com + DNS-->>C: ALB DNS / Cloudflare IP + C->>ALB: HTTPS or gRPC over :443 + ALB->>ALB: ACM TLS termination + ALB->>ALB: path match
/guardian.Guardian/* → gRPC TG :50051
else → HTTP TG :3000 + ALB->>S: forward (HTTP1.1 or HTTP2/gRPC) + S->>SM: GetSecretValue
(at startup: DATABASE_URL, ack keys) + Note over S,DB: dev path + S->>DB: SQL on :5432 + Note over S,DB: prod path + S->>P: SQL on :5432 (TLS required) + P->>DB: pooled connection + DB-->>S: rows + S-->>ALB: response + ALB-->>C: response +``` + +Health checks: the HTTP target group probes `GET /` ([`alb.tf:59`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/infra/alb.tf#L59)); +the gRPC target group probes `/guardian.Guardian/GetPubkey` with matcher `0` +([`alb.tf:81`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/infra/alb.tf#L81)). + +## Resource inventory + +Mapping AWS resources to the Terraform files that own them: + +| AWS resource | Terraform file | Notes | +|---|---|---| +| ECS cluster | [`ecs.tf:2`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/infra/ecs.tf#L2) | Container Insights enabled, ECS Exec logging to CloudWatch. | +| ECS service `guardian server` | [`ecs.tf:152`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/infra/ecs.tf#L152) | Fargate, public IP, two target group attachments when HTTPS is on. | +| ECS task definition | [`ecs.tf:32`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/infra/ecs.tf#L32) | One container, ports `3000` + `50051`, env + secret env from Secrets Manager. | +| ECS autoscaling target + policies | [`ecs_autoscaling.tf`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/infra/ecs_autoscaling.tf) | CPU + memory target-tracking, only created when `effective_server_autoscaling_enabled`. | +| ALB | [`alb.tf:2`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/infra/alb.tf#L2) | Internet-facing, at least two subnets enforced as precondition. | +| HTTP target group (`:3000`) | [`alb.tf:46`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/infra/alb.tf#L46) | Health check `GET /`. | +| gRPC target group (`:50051`) | [`alb.tf:65`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/infra/alb.tf#L65) | Created only when an ACM cert is present. | +| HTTP listener `:80` | [`alb.tf:87`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/infra/alb.tf#L87) | Forwards when no cert, redirects to HTTPS when cert is present. | +| HTTPS listener `:443` | [`alb.tf:121`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/infra/alb.tf#L121) | TLS 1.3-1.2 policy; default action → HTTP target group. A migration can temporarily attach a second certificate through [`alb.tf:138`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/infra/alb.tf#L138). | +| gRPC listener rule | [`alb.tf:151`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/infra/alb.tf#L151) | Path `/guardian.Guardian/*` → gRPC target group, priority `10`. | +| RDS Postgres instance | [`rds.tf:20`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/infra/rds.tf#L20) | Storage encrypted, backups retained per `rds_backup_retention_days`; prod defaults enable deletion protection and a final snapshot on destroy. | +| RDS subnet group | [`rds.tf:8`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/infra/rds.tf#L8) | Requires ≥2 subnets. | +| `DATABASE_URL` secret | [`rds.tf:45`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/infra/rds.tf#L45) | Always created; consumed by the server task. | +| RDS Proxy + credentials secret | [`rds.tf:50`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/infra/rds.tf#L50), [`rds.tf:72`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/infra/rds.tf#L72) | Prod-only via `effective_rds_proxy_enabled`. | +| RDS Proxy target / pool config | [`rds.tf:108`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/infra/rds.tf#L108), [`rds.tf:120`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/infra/rds.tf#L120) | 80% max connections, 50% max idle. | +| Operator public keys secret | [`operator_secrets.tf`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/infra/operator_secrets.tf) | Optional dashboard operator Falcon pubkey list. | +| ACK Falcon/ECDSA secrets (existing) | [`data.tf`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/infra/data.tf) | Looked up via `data` in `prod`; created out-of-band by `aws-deploy.sh bootstrap-ack-keys`. | +| EVM allowed chains + RPC URLs secrets | [`data.tf`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/infra/data.tf) | Optional; populated by deploy script from `config/evm/chains.json`. | +| ALB SG | [`security_groups.tf:2`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/infra/security_groups.tf#L2) | Ingress `80/443` from `alb_ingress_cidrs`. | +| Server SG | [`security_groups.tf:35`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/infra/security_groups.tf#L35) | Ingress `3000`/`50051` only from ALB SG; egress all. | +| RDS Proxy SG | [`security_groups.tf:67`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/infra/security_groups.tf#L67) | Prod-only; ingress `5432` from server SG. | +| Postgres SG | [`security_groups.tf:92`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/infra/security_groups.tf#L92) | Ingress `5432` from server SG, plus RDS Proxy SG in prod. | +| ECS task execution role | [`iam.tf:2`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/infra/iam.tf#L2) | Pulls images, reads DB / EVM secrets at task start. | +| ECS task runtime role | [`iam.tf:53`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/infra/iam.tf#L53) | App-level `GetSecretValue` for ACK + operator secrets; SSM channels for ECS Exec. | +| ACK secrets policy | [`iam.tf:70`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/infra/iam.tf#L70) | Gated on `local.is_prod` — dev never reads ACK secrets. | +| Operator pubkeys policy | [`iam.tf:93`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/infra/iam.tf#L93) | Created if user supplies an existing ARN or a managed list. | +| RDS Proxy role | [`iam.tf:136`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/infra/iam.tf#L136) | Reads the proxy's credentials secret. | +| CloudWatch log groups | [`logs.tf`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/infra/logs.tf) | `server` group, `cluster` (ECS Exec) group, and the EMF metrics group when CloudWatch metrics are enabled. | +| ADOT metrics sidecar + collector config | [`ecs.tf`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/infra/ecs.tf), [`observability.tf`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/infra/observability.tf) | Non-essential container in the server task; config injected via `AOT_CONFIG_CONTENT`. | +| CloudWatch dashboard + alarms | [`observability.tf`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/infra/observability.tf) | `-server` dashboard; error-rate, latency, canonicalization, metrics-pipeline, and ECS saturation alarms. | +| ADOT EMF log-write policy | [`iam.tf`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/infra/iam.tf) | Task-role, stream-level writes on the EMF group only. | +| Route 53 alias | [`dns.tf:12`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/infra/dns.tf#L12) | Created when `route53_zone_id` is set; hostname migrations may temporarily add a second record. | +| Cloudflare CNAME | [`dns.tf:27`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/infra/dns.tf#L27) | Created when `cloudflare_zone_id` is set; can be proxied, with the same temporary migration support. | +| Variables / locals | [`variables.tf`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/infra/variables.tf), [`data.tf`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/infra/data.tf) | `local.is_prod`, `effective_*` locals derive the stage profile. | + +ECR is **not** managed by Terraform — it is created and pushed to by +[`scripts/aws-deploy.sh`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/scripts/aws-deploy.sh) before +`terraform apply`. + +## Stage profiles + +Both stages run from the same Terraform; `deployment_stage` flips a small set +of `effective_*` locals. + +```mermaid +flowchart TB + subgraph Dev["deployment_stage = dev"] + DevA["1 ECS task, no autoscaling"] + DevB["ECS → RDS direct"] + DevC["db.t3.micro, 20 GiB, no autoscale ceiling"] + DevD["ACK keys: ephemeral, in-task"] + DevE["Conservative rate limits / pool sizes"] + end + + subgraph Prod["deployment_stage = prod"] + ProdA["≥2 ECS tasks, target-tracking autoscaling"] + ProdB["ECS → RDS Proxy → RDS"] + ProdC["db.t3.medium, 50 GiB, 200 GiB ceiling"] + ProdD["ACK keys: Secrets Manager, imported at startup"] + ProdE["Benchmark-grade rate limits / pool sizes"] + end +``` + +Concrete defaults are in +[`infra/README.md`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/infra/README.md#variables-reference) and +[`docs/SERVER_AWS_DEPLOY.md`](../operations/server-aws-deploy.md#stage-profiles). + +## Identity and secrets + +Five categories of secret participate in a deploy: + +1. **`DATABASE_URL`** — written by Terraform from RDS connection details + ([`rds.tf:57`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/infra/rds.tf#L57)). Server task reads it via the + execution role at task start; injected as the `DATABASE_URL` env var + ([`ecs.tf:121`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/infra/ecs.tf#L121)). +2. **RDS Proxy credentials** (prod) — separate JSON secret consumed by the + proxy's IAM role ([`rds.tf:62`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/infra/rds.tf#L62), + [`iam.tf:155`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/infra/iam.tf#L155)). +3. **ACK signing keys** (prod) — Falcon + ECDSA secret keys for Guardian's + own response signing. Created out-of-band by + `aws-deploy.sh bootstrap-ack-keys`, referenced via `data` blocks, + read by the runtime role ([`iam.tf:70`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/infra/iam.tf#L70)) and + imported into the filesystem keystore at process start. +4. **Operator public keys** — Falcon public keys allowed to authenticate to + the dashboard. Either Terraform-managed (from a variable list) or an + existing ARN; either way exposed as + `GUARDIAN_OPERATOR_PUBLIC_KEYS_SECRET_ID` to the task + ([`ecs.tf:100`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/infra/ecs.tf#L100)). +5. **EVM allowed chains + RPC URLs** — Secrets Manager entries optionally + populated from `config/evm/chains.json`, exposed to the task as + `GUARDIAN_EVM_ALLOWED_CHAIN_IDS` and `GUARDIAN_EVM_RPC_URLS` + ([`ecs.tf:125`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/infra/ecs.tf#L125)). + +The IAM split is deliberate: +- The **execution role** only reads secrets the AWS-ECS agent needs *before* + the container starts (DB URL, EVM secrets surfaced as env). +- The **task role** owns secret reads the *application* performs at runtime + (ACK keys, operator pubkeys) and SSM channels for ECS Exec. + +## Networking + +The stack uses the default VPC by default and picks subnets from +[`data.tf`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/infra/data.tf) unless `vpc_id` / `subnet_ids` are set. +Public-subnet selection for the ALB sorts subnets lexicographically, which +can surface a private subnet ahead of a public one if subnet naming differs +across AZs — pin `subnet_ids` explicitly when you hit AZ surprises. RDS +Proxy additionally requires explicit opt-in for unsupported AZs via +`rds_proxy_subnet_ids` (e.g. `us-east-1e` / `use1-az3` in `us-east-1`). + +Security-group chain: +```text +internet → alb SG (80/443) → server SG (3000, 50051) +server SG ─┬─→ postgres SG (5432) (dev path) + └─→ rds_proxy SG (5432) → postgres SG (5432) (prod path) +``` + +Egress is wide-open from the server SG so the task can pull images, talk to +Secrets Manager, talk to Miden RPC, and emit CloudWatch logs. + +## Deploy lifecycle + +```mermaid +flowchart LR + A["scripts/aws-deploy.sh deploy"] --> B["docker build & push → ECR"] + B --> C["build tfvars from env"] + C --> D["terraform init/apply (split state per stack+stage)"] + D --> E["ECS service rolls task definition revision"] + E --> F["task pulls image, reads secrets,
starts HTTP+gRPC servers"] + F --> G["ALB health checks pass → traffic shifts"] +``` + +State is kept **locally** per stack+stage at +`infra/terraform...tfstate`. There is no remote backend +configured; the deploy script is the source of truth for which state file is +in use. + +## Observability surface + +Today: CloudWatch container logs ([`logs.tf`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/infra/logs.tf)), +Container Insights metrics ([`ecs.tf:6`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/infra/ecs.tf#L6)), +ECS Exec session logging ([`ecs.tf:10`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/infra/ecs.tf#L10)), and +Terraform-managed application metrics +([`observability.tf`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/infra/observability.tf)): an ADOT Collector +sidecar in the server task scrapes Guardian's loopback-only Prometheus +endpoint and exports selected metrics to CloudWatch via EMF under a +per-stack namespace, with a `-server` dashboard and alarms for +error rate, latency, canonicalization failures, metrics-pipeline health, +and ECS saturation. Gated by `guardian_metrics_enabled` (the endpoint) +and `cloudwatch_metrics_enabled` (the export pipeline), both on by +default; enablement and verification live in +[`SERVER_AWS_DEPLOY.md`](../operations/server-aws-deploy.md#metrics-dashboard-and-alarms). +Tracing exporters remain an open gap. + +## Things that are deliberately not here + +- **No remote Terraform backend.** State files are local; the deploy script + treats them as authoritative. Switch to S3+DynamoDB before multiple + operators apply concurrently. +- **No WAF, no Shield Advanced.** The ALB is reachable from + `alb_ingress_cidrs`, default `0.0.0.0/0`. +- **No RDS read replica, no automated DR drill.** Backups are + configured via `rds_backup_retention_days` (default 7) and the manual + restore procedure is documented in + [`runbooks/backup-restore.md`](../runbooks/backup-restore.md), but there + is no rehearsed, automated DR path. Multi-AZ standby failover is opt-in + via `rds_multi_az` (off by default). +- **No KMS-managed Secrets Manager keys.** Secrets use the default AWS-owned + key. Rotation is manual. diff --git a/versioned_docs/version-0.16/builder/miden-guardian/architecture/services.md b/versioned_docs/version-0.16/builder/miden-guardian/architecture/services.md new file mode 100644 index 00000000..8eb93946 --- /dev/null +++ b/versioned_docs/version-0.16/builder/miden-guardian/architecture/services.md @@ -0,0 +1,371 @@ +--- +title: "Guardian Service Architecture" +sidebar_position: 1 +--- +This document describes the logical decomposition of the Guardian system — +the modules inside the server process and the clients/SDKs that talk to it. +It complements [`docs/architecture/infra.md`](./infra.md), which covers how +that same process is packaged and deployed on AWS. + +## TL;DR + +Guardian is one Rust binary +([`crates/server`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/server)) that fronts two transports +(HTTP + gRPC) over a shared service layer, persists Miden account state and +deltas in Postgres, signs its responses with ACK keys, and is consumed by +Rust and TypeScript clients plus the multisig SDKs that build on top. + +## Layered view + +```mermaid +flowchart TB + subgraph Clients["Consumers"] + RC["crates/client
Rust client"] + TC["packages/guardian-client
TS client (HTTP REST)"] + OC["packages/guardian-operator-client
TS operator client"] + EC["packages/guardian-evm-client
TS EVM client"] + RM["crates/miden-multisig-client
Rust multisig SDK"] + TM["packages/miden-multisig-client
TS multisig SDK"] + end + + subgraph Server["crates/server (Guardian)"] + direction TB + + subgraph API["api/ — transports"] + HTTP["http.rs
axum + REST + dashboard"] + GRPC["grpc.rs
tonic + guardian.proto"] + DASH["dashboard.rs + dashboard_feeds.rs"] + EVMAPI["evm.rs"] + end + + subgraph MW["middleware/"] + AUTH["request auth, rate limit, CORS,
request ID, audit"] + end + + subgraph SVC["services/ — request handlers"] + DELTA["push_delta · get_delta · get_delta_since
delta_commit"] + PROP["push/get/sign_delta_proposal"] + STATE["get_state · lookup_account · configure_account"] + DASHSVC["dashboard_account_* · dashboard_global_*
dashboard_info · pagination"] + end + + subgraph DOM["domain"] + DO["delta_object.rs"] + SO["state_object.rs"] + NET["network/ — Miden RPC client glue"] + EVMDOM["evm/ — proposals, sessions, contracts"] + JOBS["jobs/ — canonicalization, async tasks"] + AUDIT["audit/ — append-only event log"] + end + + subgraph PERSIST["Persistence + identity"] + STG["storage/ — state + deltas
(postgres / filesystem)"] + META["metadata/ — accounts, auth, network
(postgres / filesystem)"] + ACK["ack/ — Falcon + ECDSA signing
(keystore + Secrets Manager)"] + end + + HTTP --> AUTH + GRPC --> AUTH + DASH --> AUTH + EVMAPI --> AUTH + AUTH --> SVC + SVC --> DOM + SVC --> PERSIST + DOM --> PERSIST + SVC --> ACK + end + + RC --> GRPC + TC --> HTTP + OC --> HTTP + EC --> EVMAPI + RM --> RC + TM --> TC + TM --> OC + + PERSIST <--> DB[("Postgres")] + NET <--> MIDEN[("Miden RPC node")] + ACK <--> SM[("AWS Secrets Manager
(prod)")] +``` + +Sources for the boxes: server module declarations in +[`crates/server/src/lib.rs:3-30`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/server/src/lib.rs#L3), +API submodules in +[`crates/server/src/api/mod.rs`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/server/src/api/mod.rs), +service handlers in +[`crates/server/src/services/mod.rs`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/server/src/services/mod.rs). + +## Components + +### Transports — `api/` + +Two transports, one set of service handlers. Default ports are `:50051` +(gRPC) and `:3000` (HTTP/dashboard); both are configurable via the server +builder. + +- **gRPC** ([`api/grpc.rs`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/server/src/api/grpc.rs)) is the + primary surface for the Rust client. Schema lives in + `crates/server/proto/guardian.proto`. Default bind: `:50051`. +- **HTTP** ([`api/http.rs`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/server/src/api/http.rs)) hosts the + REST routes for the TypeScript clients **and** the operator dashboard. + Default bind: `:3000`. + `axum` based. The TS Guardian client (`@openzeppelin/guardian-client`) + is HTTP-only — it does **not** speak gRPC. This Rust-vs-TS transport + split is the most common integration pitfall in the codebase. +- **Dashboard** ([`api/dashboard.rs`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/server/src/api/dashboard.rs), + [`api/dashboard_feeds.rs`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/server/src/api/dashboard_feeds.rs)) + carries operator endpoints: accounts list, account detail, proposals, + deltas, snapshot, info. Uses session auth distinct from Guardian's + per-account auth. +- **EVM** ([`api/evm.rs`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/server/src/api/evm.rs)) is the + feature-gated EVM proposal API exposed when the server is built with the + `evm` feature. + +Both transports share the **middleware** stack +([`crates/server/src/middleware`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/server/src/middleware)): +request authentication, rate limiting, CORS, request IDs, audit-log +emission. + +### Service handlers — `services/` + +Per-RPC handlers. Each file owns one verb of the Guardian contract. + +| Service file | Purpose | +|---|---| +| `push_delta.rs` | Apply a signed delta to an account's state. | +| `get_delta.rs`, `get_delta_since.rs` | Read deltas by id / by cursor. | +| `delta_commit.rs` | Final commit step turning a proposal into a delta. | +| `push_delta_proposal.rs`, `get_delta_proposal*.rs`, `sign_delta_proposal.rs` | Multisig proposal lifecycle. | +| `get_state.rs`, `lookup_account.rs`, `configure_account.rs` | State and account lifecycle. | +| `dashboard_account_*.rs`, `dashboard_global_*.rs`, `dashboard_info.rs`, `dashboard_pagination.rs` | Operator dashboard queries. | + +The transport layer (gRPC or HTTP) decodes the request, the middleware +authenticates and audits it, then dispatches into one of these handlers. + +### Domain types + +- **`state_object.rs` + `delta_object.rs`** ([`crates/server/src/state_object.rs`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/server/src/state_object.rs), + [`crates/server/src/delta_object.rs`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/server/src/delta_object.rs)) + — canonical in-memory shapes for an account's state and the deltas applied + to it. The proto-level shapes are normalized into these before any + persistence touches them. +- **`network/`** — wraps the Miden RPC client used to verify on-chain + proofs / submit transactions. +- **`evm/`** ([`crates/server/src/evm`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/server/src/evm)) — + EVM proposal lifecycle, session auth, on-chain contract interaction. + Feature-gated. +- **`jobs/`** ([`crates/server/src/jobs`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/server/src/jobs)) — + background work, currently canonicalization tasks that normalize proposals + before commit. +- **`audit/`** ([`crates/server/src/audit`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/server/src/audit)) + — append-only structured event log written alongside mutating operations. + +### Persistence — `storage/` and `metadata/` + +Two parallel module trees, each with a `postgres.rs` and `filesystem.rs` +backend selected at startup: + +- **`storage/`** ([`crates/server/src/storage`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/server/src/storage)) + owns the heavy data: account state objects and delta history. +- **`metadata/`** ([`crates/server/src/metadata`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/server/src/metadata)) + owns accounts, auth credentials + ([`metadata/auth/`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/server/src/metadata/auth)), and + network/peer info. + +In production both back ends are Postgres on RDS, sharing pool sizing +controlled by `GUARDIAN_DB_POOL_MAX_SIZE` and +`GUARDIAN_METADATA_DB_POOL_MAX_SIZE` (see +[`infra.md`](./infra.md#stage-profiles)). The filesystem backends are kept +for local development and tests. + +### Storage modes + +The backend is selected at **compile time** via Cargo features in +[`crates/server/src/builder/storage.rs`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/server/src/builder/storage.rs) — +not at runtime, not via env vars. Pick the right feature set for your build +target. + +| Mode | Feature | Storage | Metadata | Audit | Required env | +|---|---|---|---|---|---| +| **Postgres** (prod, staging) | `postgres` | `PostgresService` | `PostgresMetadataStore` | `PostgresAuditor` — durable rows in `admin_actions` | `DATABASE_URL` | +| **Filesystem** (dev, tests) | _none_ | `FilesystemService` | `FilesystemMetadataStore` | `LogAuditor` — structured logs only, **no durable audit** | `GUARDIAN_STORAGE_PATH`, `GUARDIAN_METADATA_PATH` | + +```mermaid +flowchart LR + Build["cargo build
--features ?"] + + Build -->|"--features postgres"| PG + Build -->|"no features"| FS + + subgraph PG["Postgres mode (prod)"] + PGS["PostgresService
(state, deltas)"] + PGM["PostgresMetadataStore
(accounts, auth, network)"] + PGA["PostgresAuditor
(durable admin_actions)"] + PGS --> RDS[("RDS PostgreSQL")] + PGM --> RDS + PGA --> RDS + end + + subgraph FS["Filesystem mode (dev/test)"] + FSS["FilesystemService"] + FSM["FilesystemMetadataStore"] + FSA["LogAuditor
⚠ warns at startup:
'audit events will not be persisted'"] + FSS --> Disk[("local filesystem")] + FSM --> Disk + end +``` + +#### Production must use Postgres + +Filesystem mode is unsafe for production for three reasons: + +1. **No durable audit trail.** The filesystem build wires up `LogAuditor` + and emits a one-shot startup warning that audit events flow only to + structured logs ([`builder/storage.rs:129-137`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/server/src/builder/storage.rs#L129)). + Postgres mode persists them in the `admin_actions` table. +2. **No migrations, no schema guarantees.** The Postgres path runs + `postgres::run_migrations` at startup + ([`builder/storage.rs:109`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/server/src/builder/storage.rs#L109)); + the filesystem path has none. +3. **No horizontal scaling.** Multiple ECS tasks behind the ALB cannot + share filesystem state — each task would diverge. Postgres on RDS is the + only shared substrate in the deployment topology + ([`infra.md`](./infra.md#topology)). + +The AWS deploy path therefore builds with `GUARDIAN_SERVER_FEATURES=postgres` +(plus `evm` when needed). See +[`SERVER_AWS_DEPLOY.md`](../operations/server-aws-deploy.md#quick-start). + +#### When filesystem mode is fine + +- Single-process examples under [`examples/`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/examples). +- Unit and integration tests that need a real backend but not a real DB. +- Quick local iteration on non-storage code paths before spinning up Postgres. + +For local development that exercises Postgres behavior, run a local +Postgres (e.g. `docker run -e POSTGRES_PASSWORD=… postgres`) and build with +`--features postgres` rather than relying on the filesystem backend. + +### Identity — `ack/` + +Guardian signs its responses so that clients (and the multisig SDKs in +particular) can verify the server is the same Guardian they trust. + +- [`ack/mod.rs`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/server/src/ack/mod.rs) wires the signer. +- [`ack/miden_falcon_rpo/`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/server/src/ack/miden_falcon_rpo) + and [`ack/miden_ecdsa/`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/server/src/ack/miden_ecdsa) hold the + two scheme implementations. +- [`ack/secrets_manager.rs`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/server/src/ack/secrets_manager.rs) + pulls secret payloads into the filesystem keystore at startup. In `dev` + keys are auto-generated; in `prod` they are loaded from the Secrets + Manager IDs in `GUARDIAN_ACK_FALCON_SECRET_ID` / + `GUARDIAN_ACK_ECDSA_SECRET_ID`, falling back to + `guardian-prod/server/ack-{falcon,ecdsa}-secret-key` when unset + ([`secrets_manager.rs:10-13`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/server/src/ack/secrets_manager.rs#L10)). + Terraform sets these env vars per stack so multi-stack deployments use + scoped IDs. + +### Dashboard subsystem + +The dashboard layer is a small system in its own right inside +[`crates/server/src/dashboard`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/server/src/dashboard): +its own session/auth ([`authz.rs`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/server/src/dashboard/authz.rs), +[`middleware.rs`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/server/src/dashboard/middleware.rs)), +allowlist of operator public keys ([`allowlist.rs`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/server/src/dashboard/allowlist.rs)), +permission model ([`permissions.rs`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/server/src/dashboard/permissions.rs)), +shared state ([`state.rs`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/server/src/dashboard/state.rs)), +and pagination cursor logic +([`cursor.rs`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/server/src/dashboard/cursor.rs)). + +It piggybacks on the same Postgres backend as the rest of the server but +authenticates operators through Falcon-signed challenges rather than the +per-account credentials used by Guardian's primary API. + +## Consumers + +```mermaid +flowchart LR + subgraph Rust["Rust SDKs"] + GC["crates/client
Guardian client (gRPC)"] + MM["crates/miden-multisig-client
multisig SDK"] + KS["crates/miden-keystore
local key storage"] + MR["crates/miden-rpc-client
Miden node client"] + end + + subgraph TS["TS SDKs (packages/)"] + GTS["guardian-client"] + GTOP["guardian-operator-client"] + GTEVM["guardian-evm-client"] + MTS["miden-multisig-client"] + end + + subgraph Apps["Examples / harnesses"] + DEMO["examples/demo (Rust)"] + SMOKE["examples/smoke-web,
operator-smoke-web,
evm-smoke-web"] + WEB["examples/web"] + end + + MM --> GC + MM --> KS + MM --> MR + MTS --> GTS + GTOP -. operator HTTP .-> Server + GTS -. HTTP/REST .-> Server + GTEVM -. EVM HTTP .-> Server + GC -. gRPC .-> Server + DEMO --> MM + SMOKE --> MTS + SMOKE --> GTOP + SMOKE --> GTEVM + WEB --> MTS + + Server[(crates/server)] +``` + +- **Rust client** ([`crates/client`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/client)) — gRPC client with + auth ([`crates/client/src/auth`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/client/src/auth)) and a + pluggable keystore ([`crates/client/src/keystore`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/client/src/keystore)). +- **Rust multisig SDK** ([`crates/miden-multisig-client`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/miden-multisig-client)) + — builds on the Rust client; owns proposal creation, signing, execution, + export/import, and the `SwitchGuardian` flow. +- **TS Guardian client** ([`packages/guardian-client`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/packages/guardian-client)) + — **HTTP REST** client (`GuardianHttpClient`) used directly by browser + apps and by the TS multisig SDK. Targets the server's HTTP routes on + `:3000`, **not** gRPC. This is the common transport split to be aware + of: the Rust client speaks gRPC; the TS client speaks HTTP. +- **TS multisig SDK** ([`packages/miden-multisig-client`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/packages/miden-multisig-client)) + — TS counterpart of the Rust multisig SDK; browser-friendly. +- **Operator client** ([`packages/guardian-operator-client`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/packages/guardian-operator-client)) + — talks to the dashboard endpoints (HTTP, not gRPC). +- **EVM client** ([`packages/guardian-evm-client`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/packages/guardian-evm-client)) + — talks to `api/evm.rs` when the server is built with EVM support. + +Smoke harnesses under [`examples/`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/examples) drive each SDK +end-to-end; see the matching `smoke-test-*` skills for how to run them. + +## Authentication shape + +Two distinct auth domains: + +1. **Per-account auth** — every mutating Guardian RPC is signed by the + account's Falcon or ECDSA key. Verified in the auth middleware via + [`metadata/auth/`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/server/src/metadata/auth) which loads + credentials from the metadata store and dispatches to the right scheme + ([`miden_falcon_rpo.rs`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/server/src/metadata/auth/miden_falcon_rpo.rs), + [`miden_ecdsa.rs`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/server/src/metadata/auth/miden_ecdsa.rs)). +2. **Operator auth** — dashboard endpoints use Falcon-signed challenges + against an allowlist of operator public keys, producing session cookies. + Lives entirely in + [`dashboard/authz.rs`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/server/src/dashboard/authz.rs). + +Both paths share the same ACK signer when emitting *responses*; only the +*incoming* credentials differ. + +## Where to look next + +- For build-and-deploy: [`docs/SERVER_AWS_DEPLOY.md`](../operations/server-aws-deploy.md). +- For the deployment topology: [`docs/architecture/infra.md`](./infra.md). +- For the multisig SDK: [`docs/MULTISIG_SDK.md`](../reference/multisig-sdk.md). +- For protocol-level contracts: `crates/server/proto/guardian.proto`. When + changing the proto, regenerate language bindings and re-run the SDK smoke + tests in `examples/` before publishing. diff --git a/versioned_docs/version-0.16/builder/miden-guardian/getting-started/_category_.json b/versioned_docs/version-0.16/builder/miden-guardian/getting-started/_category_.json new file mode 100644 index 00000000..3562d433 --- /dev/null +++ b/versioned_docs/version-0.16/builder/miden-guardian/getting-started/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Getting Started", + "position": 1 +} diff --git a/versioned_docs/version-0.16/builder/miden-guardian/getting-started/concepts.md b/versioned_docs/version-0.16/builder/miden-guardian/getting-started/concepts.md new file mode 100644 index 00000000..be75d4fb --- /dev/null +++ b/versioned_docs/version-0.16/builder/miden-guardian/getting-started/concepts.md @@ -0,0 +1,282 @@ +--- +title: "Guardian Concepts" +sidebar_position: 1 +--- +A conceptual introduction to Guardian. Read this **before** the architecture +docs if you are new to the system — it explains what Guardian is, what it is +*not*, and the trust model that informs every other decision in the +codebase. + +For formal definitions and wire shapes, see [`spec/`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/spec/index.md). For +the module-level decomposition, see +[`docs/architecture/services.md`](../architecture/services.md). + +## What Guardian is + +Guardian is an **off-chain coordination service** for Miden accounts. It +stores account state snapshots and the deltas that mutate them, signs +accepted deltas with an acknowledgement key, and helps multiple clients of +the same account stay in sync. + +Guardian is: + +- **Non-custodial.** It never holds an account's spending key. The account + owner does. +- **Not the source of truth.** The Miden network remains authoritative for + account commitments. Guardian's database is a *coordination cache*, not a + ledger. +- **Pluggable.** Server, clients, and the multisig SDK are all in this + repository; operators can run Guardian themselves, and users can rotate + away from any operator at any time. + +## The custody spectrum + +Traditional crypto custody is binary — either a full custodian holds the +key, or the user does. Guardian creates a third position — but **not** by +holding a seat in the user's multisig. The account component enforces two +independent checks on every ordinary transaction: + +```mermaid +flowchart LR + subgraph S["User signer set"] + H["Hot key
(daily transactions)"] + C["Cold key
(recovery, offline)"] + end + T["M-of-N threshold
(user keys only)"] + G["Guardian check
(one ACK signature —
never counted toward M)"] + B{"Both must pass"} + A["Transaction authorized"] + H --> T + C --> T + T --> B + G --> B + B --> A +``` + +- **The user threshold** counts only the user's signer set. The Guardian + key is stored in a separate storage slot and can never satisfy or + contribute to M. +- **The Guardian check** verifies exactly one Guardian signature over the + same transaction summary the cosigners sign. It is a pass/fail gate: + the Guardian can veto a transaction by withholding its ACK, but its + signature alone authorizes nothing — **Guardian alone can never move + funds**. +- **The rotation exception.** The one transaction the account component + executes without the Guardian check is `SwitchGuardian` (rotating the + Guardian key), which needs only the user threshold. Guardian's blocking + power is therefore temporary and defeasible: the user's own keys can + always remove it. + +So the Guardian participates in every ordinary transaction, but as a +separate authentication input — not as one of the M counted signatures. +Describing the account as "2-of-3" (user hot, user cold, Guardian) +understates the user threshold's independence and overstates Guardian's +authority; the accurate description is *M-of-N over user keys, plus a +removable Guardian gate*. + +## State and Delta + +The two primitives Guardian works with: + +- **State** — a snapshot of an account at a point in time. Guardian + tracks the account ID, current commitment, nonce, and authentication + scheme. The full state payload is opaque to Guardian; the client + supplies it. +- **Delta** — an append-only change to that state. Every delta + references the previous commitment (`prev_commitment`) and produces a + new one, forming an unbroken chain. + +These mirror the definitions in +[`spec/index.md`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/spec/index.md#definitions). The supporting concepts +are: + +| Term | Role | +|---|---| +| **Commitment** | Hash uniquely identifying a state version. Lets clients detect tampering. | +| **Nonce** | Monotonically increasing counter — orders deltas in the chain. | +| **Account ID** | Unique identifier; one Guardian hosts many accounts. | +| **Delta proposal** | Multi-party coordination object — sits in `pending` until threshold cosigners have signed. | +| **Acknowledgement (ACK)** | Guardian's signature over an accepted delta's transaction summary commitment — the same message the cosigners sign. Issued only after Guardian has validated the delta against the stored state. Clients verify the ACK to confirm a delta was actually accepted by the Guardian they expected. | + +## Transaction lifecycle + +A single transaction touches Guardian and the Miden network in five steps: + +```mermaid +sequenceDiagram + participant U as User device + participant G as Guardian + participant M as Miden network + + U->>U: 1. Execute transaction locally, compute delta + U->>G: 2. Submit signed delta (prev_commitment, nonce, payload) + G->>G: validate against stored state + G-->>U: 3. ACK signature over the transaction summary commitment
(delta status: candidate) + U->>M: 4. Submit proven account update + M-->>U: account commitment accepted + G->>M: poll for canonical commitment + G->>G: 5. If commitments match, mark canonical — otherwise park as
retained and keep reconciling until it matches or the TTL expires +``` + +The status transitions for a delta: + +| Status | Meaning | +|---|---| +| `candidate` | Guardian accepted and signed it, but the matching Miden update has not yet been observed. | +| `canonical` | Guardian observed the matching commitment on Miden. The delta is now durable for other clients of the same account. | +| `retained` | Guardian stopped actively verifying the candidate and released the account slot. The on-chain outcome remains **uncertain**; background reconciliation may still promote it to `canonical` until its retention TTL expires (default 24 h). A new submission at the same nonce supersedes it. Never read `retained` as "the transaction did not land" — it means *unlocked but unresolved*. | +| `discarded` | Canonicalization was abandoned by the client (`client_abandoned`), or retention is disabled and verification failed terminally. The client must rebuild from the latest canonical state. | + +This single definition of `retained` holds across every surface — +canonicalization, account locking, the abandon APIs (which report a +distinct `retained` result, never `abandoned`), the SDKs, and the +operator dashboard. At a glance: + +| Status | Account locked? | Outcome known? | Client action | +|---|---|---|---| +| `candidate` | Yes | No | Wait, or request abandonment | +| `retained` | No | No | Sync and check the chain before replacing (a resubmission supersedes the row and forfeits automatic recovery) | +| `discarded` (`client_abandoned`) | No | Probably not landed, but late reconciliation remains possible within the TTL | Continue cautiously | +| `canonical` | No | Yes — landed | Sync account state | + +This is the **canonicalization** process. The default `candidate` mode runs +a background worker that polls Miden and promotes or discards each +candidate. An `optimistic` mode promotes immediately and is appropriate +only when the client and operator trust each other absolutely (e.g. +single-tenant dev setups). See [`spec/processes.md`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/spec/processes.md#canonicalization) +for the formal state machine. + +## Trust model + +Guardian's trust boundaries layer up like this: + +```mermaid +flowchart LR + subgraph L1["1. Client → Guardian"] + L1d["signed delta, signed request,
timestamp, payload digest"] + end + subgraph L2["2. Guardian → Client"] + L2d["ACK-signed delta,
commitment chain"] + end + subgraph L3["3. Client → Miden"] + L3d["ZK-proven account update"] + end + subgraph L4["4. Guardian → Miden"] + L4d["read-only commitment queries"] + end + subgraph L5["5. Inside Guardian"] + L5d["snapshots, deltas, proposals,
metadata, audit log"] + end + + Client --> L1 --> L2 --> Client + Client --> L3 --> Miden + Guardian --> L4 --> Miden + Guardian --> L5 +``` + +| Boundary | Protected by | +|---|---| +| Client → Guardian | Per-account Falcon/ECDSA signatures, replay protection (±5 min timestamp window + monotonic per-key timestamps), rate limits, request size limits. | +| Guardian → Client | The ACK signature on every accepted delta. Clients verify the ACK and the commitment chain before trusting returned state. | +| Client → Miden | Miden's own ZK proof verification; Guardian is not in this path. | +| Guardian → Miden | Read-only RPC; Miden does not trust Guardian for anything. | +| Inside Guardian | Backend access control, IAM scoping, infrastructure hardening — see [`docs/runbooks/secrets.md`](../runbooks/secrets.md) and [`docs/architecture/infra.md`](../architecture/infra.md). | + +What this means in practice: + +- **A compromised Guardian cannot steal funds.** It can refuse service or + serve stale data, but it cannot produce a Miden-accepted update without + the user's spending key. +- **A compromised Guardian can withhold or lie about state.** Clients are + expected to compare against Miden before signing anything important. +- **An offline Guardian halts coordination.** Users can still execute + locally; they just cannot sync with their other devices, and other + cosigners cannot see their proposals. + +## Client verification checklist + +When integrating an SDK against a Guardian, clients **must**: + +1. **Pin Guardian's pubkey** — fetch `/pubkey` once over a trusted channel + and refuse to talk to any Guardian that returns a different key. +2. **Verify the ACK signature** on every accepted delta before treating it + as confirmed. +3. **Validate the commitment chain** — `delta_n.new_commitment` must match + `delta_{n+1}.prev_commitment`. A break means Guardian is lying or + corrupted. +4. **Check freshness against Miden** before signing high-value + transactions — match the latest canonical commitment against the + account's commitment on-chain. +5. **Treat unexpected pubkey changes as security events.** The Guardian + you connected to last week should be the same Guardian today. If it + isn't, halt operations until you can confirm intentional rotation. + +The Rust and TypeScript multisig SDKs perform 1–4 automatically; #5 is an +application-level decision. + +## Failure and recovery + +```mermaid +flowchart TB + F1["Guardian unavailable"] --> R1["Use local state · retry · rotate provider"] + F2["Candidate fails canonicalization"] --> R2["Resync from latest canonical · rebuild transaction"] + F3["Stale delta submission
(prev_commitment mismatch)"] --> R3["Fetch /delta/since · replay canonical deltas · retry"] + F4["Operator withholds updates"] --> R4["Compare against Miden · rotate Guardian
(user threshold)"] + F5["Guardian database corruption"] --> R5["Reject unverifiable data · recover from another device or operator"] +``` + +| Failure | What you see | Recovery | +|---|---|---| +| Guardian unreachable | gRPC `Unavailable` / HTTP 5xx, no ACK | Continue locally, retry; rotate operator if persistent. | +| Stale delta (`commitment_mismatch`) | `400` with `code: commitment_mismatch` | `GET /delta/since` → replay canonical chain → retry the local transaction. | +| Candidate parked (`retained`) | Delta status flips `candidate` → `retained`; the account is released | Usually means the Miden proof was never submitted, the on-chain commitment diverged, or the guardian's RPC view lagged. No action is strictly required: if the transaction actually landed, the guardian reconciles and promotes it automatically. To move on immediately, refetch state, rebuild and resubmit — a new submission at the same nonce supersedes the retained delta. Because superseding forfeits that automatic recovery, check the delta's status once before resubmitting: if it already flipped to `canonical`, the original transaction landed and there is nothing to redo. | +| Transaction died after approval (stranded candidate) | New proposals answered `409 conflict_pending_delta` while the candidate waits out the grace + retry window | Call `POST /delta/candidate/abandon` (SDKs: `abandonCandidate` / `abandon_candidate`). The worker confirms over a short quarantine that the transaction did not land, flips the delta to `discarded` with reason `client_abandoned`, and releases the account — typically well under a minute. Poll via `abandonStatus` / `abandon_status`. | +| Operator censors / withholds | Other cosigners see stale state | Rotate Guardian by meeting the applicable user threshold (the cold key can participate); the new operator inherits canonical state from Miden. | +| Guardian database corruption (or restore from an older backup) | Accounts whose on-chain commitment advanced past the stored state fail state verification; accounts onboarded after the restore point fail with `account_not_found` because no guardian record remains | For an advanced account, a device holding the newer state re-syncs it, or rotate to another operator. For a missing account, a device holding it re-onboards via `/configure`, which re-registers the account with the state that device holds. The guardian cannot regenerate lost deltas because guarded accounts are private. | +| Account paused by operator | State-transition, proposal, and EVM mutation paths return `409 GUARDIAN_ACCOUNT_PAUSED` with `paused_reason` (reads and `ConfigureAccount` keep working) | Operator-driven safety lever, not a fault. An operator with `accounts:pause` clears it via `POST /dashboard/accounts/{id}/unpause`. See [`DASHBOARD.md`](../operations/dashboard.md#account-pausing). | +| Account switched to another guardian | After the `switch_guardian` delta canonicalizes on this server, mutation paths return `409 GUARDIAN_ACCOUNT_RELEASED` with `released_at` (reads and `ConfigureAccount` keep working); the dashboard shows `released_at` | Expected outcome of a guardian switch, not a fault. Terminal until the wallet re-onboards via `/configure`, which re-validates the guardian binding. An operator unpause never reactivates a released account. | +| Pubkey changed unexpectedly | `/pubkey` returns a key your client doesn't pin | Treat as compromise. Halt, verify rotation through an out-of-band channel. | + +## Provider rotation + +Because Guardian is non-custodial and Miden is the source of truth, users +who can meet their account's signing threshold can switch from one Guardian +operator to another without the current operator's cooperation: + +1. Stand up (or contract with) a new Guardian instance. +2. Meet the applicable user threshold to execute the `SwitchGuardian` + transaction, which installs the new Guardian service key in the + account's Guardian slot. This is the rotation exception + described above: the account component accepts it without the current + Guardian's signature. +3. Point clients at the new endpoint and pubkey. + +The multisig SDK's `SwitchGuardian` flow implements this. See +[`docs/MULTISIG_SDK.md`](../reference/multisig-sdk.md). + +## What Guardian is *not* + +To avoid confusion when reading the architecture docs: + +- **Not a custodian.** Cannot move funds. +- **Not a node.** Does not validate Miden transactions. It signs *deltas* + (off-chain state changes); Miden verifies the on-chain proof. +- **Not authoritative.** If Guardian and Miden disagree, Miden wins. + Guardian discards mismatched candidates by design. +- **Not a TEE-only system.** Reproducible builds make TEE deployment + *possible* but the reference deployment runs on ECS/Fargate, not in a + TEE. +- **Not a privacy layer.** Guardian operators can see metadata (account + IDs, timestamps, delta size, frequency). Payload privacy comes from + Miden's ZK execution, not from Guardian. + +## Where to read next + +- [Local development](./local-dev.md) — get a Guardian running locally. +- [Service architecture](../architecture/services.md) — the modules that + implement everything above. +- [AWS deployment](../architecture/infra.md) — the reference production + topology. +- [Troubleshooting](../operations/troubleshooting.md) — error codes and recovery + playbooks. +- [`spec/`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/spec/index.md) — formal specification. diff --git a/versioned_docs/version-0.16/builder/miden-guardian/getting-started/local-dev.md b/versioned_docs/version-0.16/builder/miden-guardian/getting-started/local-dev.md new file mode 100644 index 00000000..0a7c82ab --- /dev/null +++ b/versioned_docs/version-0.16/builder/miden-guardian/getting-started/local-dev.md @@ -0,0 +1,275 @@ +--- +title: "Local Development" +sidebar_position: 3 +--- +How to run Guardian on a developer machine, what choices you have, and which +example to reach for once it's up. + +For protocol concepts (State, Delta, Nonce, Commitment) read +[`spec/index.md`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/spec/index.md) first — this guide assumes you know them. +For the deployed AWS topology see +[`docs/architecture/infra.md`](../architecture/infra.md). + +## What you're choosing + +Three decisions when running Guardian locally: + +1. **Storage backend** — filesystem (default) or Postgres. See + [Storage modes](../architecture/services.md#storage-modes). Filesystem is + fine for most local work; pick Postgres only if you are testing + migrations, audit persistence, or multi-replica behavior. +2. **Cargo features** — `postgres` and/or `evm`. Default builds do + **not** include EVM routes and do **not** include the Postgres backend. +3. **How to launch** — `cargo run` (fastest iteration) or + `docker compose` (closer to the deployed shape). + +## Prerequisites + +- Rust toolchain pinned by [`rust-toolchain.toml`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/rust-toolchain.toml). +- Node 24+ (bundles npm 11, which the `packages/` workspace lockfile requires) if you will run any TS examples or packages. +- Docker if you will use `docker-compose.*.yml`. +- A Miden node — required for almost every flow. Either point at a + Miden Devnet endpoint or run one locally; configure via + `GUARDIAN_NETWORK_TYPE`. A node on a non-default host or port (for + example a sidecar container) is reachable via + `GUARDIAN_MIDEN_RPC_ENDPOINT` without changing the network type; see + [CONFIGURATION.md](../reference/configuration.md) for that and the optional + `GUARDIAN_MIDEN_RPC_TIMEOUT_MS` / `GUARDIAN_MIDEN_RPC_MAX_ATTEMPTS` + read-retry knobs. The node's Miden line must match the workspace + baseline (currently the 0.16 pre-release line; devnet already runs + the 0.16 node) — a mismatched node is rejected at the RPC boundary + (see + [Troubleshooting](../operations/troubleshooting.md#client-and-node-disagree-about-the-network-version)). + +## Environment file + +The server calls `dotenvy::dotenv()` on startup, so `cargo run --bin server` +automatically reads a root `.env` file when one exists. In practice a `.env` +file (or exported variables) is required: `GUARDIAN_NETWORK_TYPE` has no +default and the server refuses to start without it, and the built-in +filesystem paths live under `/var/guardian`, which may not exist or be +writable on a developer machine. + +Minimal local `.env`: + +```bash +GUARDIAN_STORAGE_PATH=.guardian/storage +GUARDIAN_METADATA_PATH=.guardian/metadata +GUARDIAN_KEYSTORE_PATH=.guardian/keystore +GUARDIAN_NETWORK_TYPE=MidenDevnet +RUST_LOG=info +``` + +Create the directories once before the first run: + +```bash +mkdir -p .guardian/storage .guardian/metadata .guardian/keystore +``` + +Use `.env.example` as a broader template when you need deploy variables, +Postgres, dashboard operators, or EVM settings. Docker Compose does not inject +the root `.env` into the server container by default; the checked-in compose +file already sets the container filesystem paths. + +## Path A — `cargo run` with filesystem (fastest) + +```bash +cargo run --bin server +``` + +This builds with no extra features and uses the **filesystem** backend. +Useful env: + +| Variable | Notes | +|---|---| +| `GUARDIAN_STORAGE_PATH` | Local path for state + deltas. Defaults to `/var/guardian/storage`. | +| `GUARDIAN_METADATA_PATH` | Local path for accounts, auth, network. Defaults to `/var/guardian/metadata`. | +| `GUARDIAN_KEYSTORE_PATH` | ACK key files, auto-generated on first run. Defaults to `/var/guardian/keystore`. | +| `RUST_LOG` (`info`) | `info`, `debug`, or e.g. `server::jobs::canonicalization=debug`. | +| `GUARDIAN_NETWORK_TYPE` (**required**) | Miden network name: `MidenLocal`, `MidenTestnet`, or `MidenDevnet`. The server refuses to start when it is unset or unrecognized. | + +At startup the server emits a warning that audit events will **not** be +persisted — that's expected for filesystem mode +([`builder/storage.rs:133`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/server/src/builder/storage.rs#L133)). + +The HTTP server binds on `:3000`, gRPC on `:50051`. + +## Path B — `cargo run` with Postgres + +```bash +docker compose -f docker-compose.postgres.yml up -d + +DATABASE_URL=postgres://guardian:guardian@localhost:5432/guardian \ + cargo run -p guardian-server --features postgres --bin server +``` + +The Postgres path runs SQL migrations on startup +([`builder/storage.rs`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/server/src/builder/storage.rs)) and +wires `PostgresAuditor` so admin actions land in the `admin_actions` table. +Pool sizing is controlled by `GUARDIAN_DB_POOL_MAX_SIZE` and +`GUARDIAN_METADATA_DB_POOL_MAX_SIZE`. + +### Postgres-backed tests + +The `guardian-server` tests that need a live database (metadata replay state, +delta storage fencing, the audit append-only trigger, and the lease, session, +and challenge coordination stores) stay `#[ignore]` and run through one script: + +```bash +POSTGRES_PASSWORD=guardian docker compose -f docker-compose.postgres.yml up -d postgres +./scripts/test-postgres.sh +``` + +Name the `postgres` service explicitly: an unqualified `up -d` also builds and +starts the server image, which these tests do not use. `POSTGRES_PASSWORD` is +required even when only the database service is selected, because Compose +interpolates the whole file; the script's default connection URL expects +`guardian`. If your Compose volume was initialised with a different password, +pass a matching `DATABASE_URL` instead of relying on the default. + +The suite drops and recreates the `public` schema before the first test and +re-applies every migration from empty, so no run inherits state from an earlier +one and there is no cleanup step. It creates `guardian_test` if it is missing, +and it refuses to run against a database whose name does not end in `_test`, so +a `DATABASE_URL` still exported from a Path B session cannot lose your +development data. Point it elsewhere with: + +```bash +DATABASE_URL=postgres://user:pass@host:5432/guardian_test ./scripts/test-postgres.sh +``` + +Run one suite at a time per server: the reset wipes the shared test database. +Execution is serial because one test reverts the newest migration for the +duration of its own run. + +The local compose Postgres uses no TLS, so omit `sslmode` (plaintext). To +exercise certificate verification locally, run a TLS-enabled Postgres with a +self-signed CA and point the server at it: + +```bash +# Generate a CA and a server cert whose SAN matches the connection host +openssl req -x509 -newkey rsa:2048 -nodes -keyout ca.key -out ca.pem \ + -subj "/CN=Test CA" -days 3650 +openssl req -newkey rsa:2048 -nodes -keyout server.key -out server.csr \ + -subj "/CN=localhost" -addext "subjectAltName=DNS:localhost" +openssl x509 -req -in server.csr -CA ca.pem -CAkey ca.key -CAcreateserial \ + -out server.crt -days 3650 -copy_extensions copyall +# start postgres with ssl=on using server.crt/server.key, then: + +# verify-full: chain + hostname (SAN must be localhost) +DATABASE_URL="postgres://guardian:guardian@localhost:5432/guardian?sslmode=verify-full&sslrootcert=$PWD/ca.pem" \ + cargo run -p guardian-server --features postgres --bin server + +# verify-ca: chain only (hostname not checked) +DATABASE_URL="postgres://guardian:guardian@localhost:5432/guardian?sslmode=verify-ca&sslrootcert=$PWD/ca.pem" \ + cargo run -p guardian-server --features postgres --bin server +``` + +Expected: correct CA + matching SAN → starts; wrong/empty CA file → fails fast +with a certificate error; under `verify-full` a SAN ≠ `localhost` is refused +while the same cert is accepted under `verify-ca`. See the full matrix in +[CONFIGURATION.md → Database TLS](../reference/configuration.md#database-tls). + +## Path C — `cargo run` with EVM support + +```bash +GUARDIAN_EVM_RPC_URLS=31337=http://127.0.0.1:8545 \ +GUARDIAN_EVM_ENTRYPOINT_ADDRESS=0x... \ + cargo run -p guardian-server --features evm --bin server +``` + +EVM routes (`/evm/auth/*`, `/evm/accounts`, `/evm/proposals*`) only register +when the `evm` feature is on. Combine with `postgres` for prod-like local +setups: `--features postgres,evm`. Pair with an Anvil node — the +`smoke-test-evm-proposal-support` skill walks through the full flow. + +## Path D — Docker Compose + +```bash +docker compose up --build -d +docker compose logs -f +``` + +This is the default Compose flow — **filesystem backend**, no Postgres, no +root `.env` required. For a Postgres-backed compose stack use +`docker-compose.postgres.yml`. Endpoints are the same as Path A (`:3000`, +`:50051`). + +## Choosing a feature flag combo + +| Goal | Features | Backend | +|---|---|---| +| Hack on a service handler quickly | _none_ | filesystem | +| Touch migrations, audit, or multi-replica behavior | `postgres` | Postgres | +| Exercise the EVM proposal flow | `evm` | filesystem | +| Reproduce a prod issue locally | `postgres,evm` | Postgres | +| Run the operator dashboard | _any_ | either (Postgres for durable history) | + +The deploy script builds with `postgres,evm` when the EVM stack is requested +— see [`SERVER_AWS_DEPLOY.md`](../operations/server-aws-deploy.md#quick-start). + +## Verifying the server is up + +```bash +curl http://localhost:3000/ # liveness (alias of /status) +curl http://localhost:3000/pubkey # ACK key commitment +grpcurl -plaintext \ + -import-path crates/server/proto -proto guardian.proto \ + -d '{}' localhost:50051 guardian.Guardian/GetPubkey +``` + +If `GetPubkey` returns a key, the server is wired correctly and the gRPC +target group's health check +([`infra/alb.tf:55`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/infra/alb.tf#L55)) would pass in production. + +## Reaching for an example + +| Example | What it exercises | SDK | +|---|---|---| +| [`examples/demo`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/examples/demo/README.md) | End-to-end multisig flow in a Rust TUI — recommended starting point. | Rust multisig client | +| [`examples/rust`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/examples/rust/README.md) | Low-level Rust binaries for both local-node and mockchain flows. | Rust client | +| [`examples/smoke-web`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/examples/smoke-web/README.md) | Browser harness for multisig + wallet integrations. | TS multisig client | +| [`examples/operator-smoke-web`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/examples/operator-smoke-web/README.md) | Local Falcon operator login + dashboard account APIs. | `@openzeppelin/guardian-operator-client` | +| [`examples/evm-smoke-web`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/examples/evm-smoke-web/README.md) | EVM proposal lifecycle against Anvil + an EVM-enabled server. | `@openzeppelin/guardian-evm-client` | +| [`examples/web`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/examples/web/README.md) | Reference web integration. | TS multisig client | + +Follow each example's README to drive it manually. Agents in this repo +have matching skills (`smoke-test-rust-multisig-sdk`, +`smoke-test-ts-multisig-sdk`, `smoke-test-operator-dashboard`, +`smoke-test-evm-proposal-support`) that automate the same flows. + +## Running tests + +```bash +cargo test --workspace +cargo test -p guardian-server --features integration +cargo test -p guardian-server --features e2e +``` + +TypeScript packages live in an npm workspace under `packages/`. Install +once from that directory with `npm ci`, then run tests with +`npm test -w @openzeppelin/`. `miden-multisig-client` depends +on the in-repo `guardian-client` workspace package — build that first. +See the root [`README.md`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/README.md#typescript-tests). + +Cargo feature gates (`integration`, `e2e`) document what each suite +needs at the top of the relevant test modules under +[`crates/server/src/testing`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/server/src/testing). Agents in +this repo have a `guardian-validation-matrix` skill that picks the +smallest meaningful set for a given change. + +## Common gotchas + +- **`DATABASE_URL` missing under `--features postgres`** — the builder + fails fast with `"DATABASE_URL environment variable is required"` + ([`builder/storage.rs:97`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/server/src/builder/storage.rs#L97)). +- **Filesystem dirs don't exist** — the server creates them on demand, but + the parent path must be writable. +- **ACK keypair changes between runs in filesystem mode** — keys + auto-generate on first start. If clients pinned an old pubkey, point them + at the new one or persist the keystore directory. +- **gRPC reflection over Cloudflare** — works locally but Cloudflare's + free tier rejects gRPC unless explicitly enabled on the zone. +- **Apple Silicon + `--platform linux/amd64`** — slow but works. For + faster local Docker builds, build `linux/arm64` images and deploy with + `cpu_architecture = "ARM64"`. diff --git a/versioned_docs/version-0.16/builder/miden-guardian/getting-started/quickstart.md b/versioned_docs/version-0.16/builder/miden-guardian/getting-started/quickstart.md new file mode 100644 index 00000000..4c21cb57 --- /dev/null +++ b/versioned_docs/version-0.16/builder/miden-guardian/getting-started/quickstart.md @@ -0,0 +1,78 @@ +--- +title: "Quickstart" +sidebar_position: 2 +--- +A Guardian running on your machine in under 60 seconds, with one command +to confirm it's alive. + +This is the fast path. For depth (Postgres, EVM, feature flags, examples) +read [`docs/LOCAL_DEV.md`](./local-dev.md). For the *why* before the *how*, +read [`docs/CONCEPTS.md`](./concepts.md). For production readiness, start +with [`docs/PRODUCTION.md`](../operations/production.md). + +The compose file sets the container paths it needs, but you must choose a +network explicitly — `GUARDIAN_NETWORK_TYPE` has no default and the server +refuses to start without it (there is no fallback network). Set it in a +local `.env` file or export it in your shell; accepted values are +`MidenLocal`, `MidenTestnet`, and `MidenDevnet`. The same requirement +applies when you run the server directly with `cargo run`; see +[`LOCAL_DEV.md`](./local-dev.md#environment-file). + +## Run + +```bash +echo "GUARDIAN_NETWORK_TYPE=MidenTestnet" > .env +docker compose up --build -d +``` + +This launches the server with the filesystem backend. HTTP binds on +`:3000`, gRPC on `:50051`. + +## Verify + +```bash +curl http://localhost:3000/ # liveness — same body as /status, expect 200 OK +curl http://localhost:3000/pubkey # expect { "commitment": "0x..." } +``` + +If both succeed, Guardian is running. The commitment you see is the ACK +key commitment clients will pin. + +## Stop + +```bash +docker compose down +``` + +## What you just got + +- A filesystem-backed Guardian server (no Postgres, no EVM). +- Auto-generated ACK keypair persisted in the container's keystore. +- HTTP + gRPC on default ports. +- No operator dashboard (allowlist is empty by default). + +This is enough to point an example SDK at: + +```bash +# Rust TUI demo against your local Guardian +cd examples/demo && cargo run --release +``` + +The demo also needs a Miden RPC endpoint (Devnet works out of the box +and runs the Miden 0.16 node this workspace targets). If you upgraded +from an older checkout, wipe stale local state first (`store.sqlite3`, +`~/.guardian`) — Miden 0.15 state does not load under 0.16 (see +[`MIDEN_COMPATIBILITY.md`](../miden-compatibility.md)). See +[`docs/LOCAL_DEV.md`](./local-dev.md#prerequisites) if your network +choice differs. + +## Where to next + +| Goal | Read | +|---|---| +| Understand what Guardian *is* before going further | [`CONCEPTS.md`](./concepts.md) | +| Switch to Postgres, enable EVM, run without Docker | [`LOCAL_DEV.md`](./local-dev.md) | +| Set every available env var deliberately | [`CONFIGURATION.md`](../reference/configuration.md) | +| Enable the operator dashboard locally | [`DASHBOARD.md`](../operations/dashboard.md) | +| Deploy or operate in production | [`PRODUCTION.md`](../operations/production.md) | +| Something broke | [`TROUBLESHOOTING.md`](../operations/troubleshooting.md) | diff --git a/versioned_docs/version-0.16/builder/miden-guardian/guides/_category_.json b/versioned_docs/version-0.16/builder/miden-guardian/guides/_category_.json new file mode 100644 index 00000000..c59a630d --- /dev/null +++ b/versioned_docs/version-0.16/builder/miden-guardian/guides/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Guides", + "position": 3 +} diff --git a/versioned_docs/version-0.16/builder/miden-guardian/guides/aws-signers.md b/versioned_docs/version-0.16/builder/miden-guardian/guides/aws-signers.md new file mode 100644 index 00000000..87ad8858 --- /dev/null +++ b/versioned_docs/version-0.16/builder/miden-guardian/guides/aws-signers.md @@ -0,0 +1,115 @@ +--- +title: "Self-hosted Docker Compose with AWS-managed ACK signers" +sidebar_position: 2 +--- +Run the Guardian server with Docker Compose where: + +- **state** lives in Postgres (bundled in the Compose stack), +- the **Falcon** ACK key lives in **AWS Secrets Manager**, and +- the **ECDSA** ACK key lives in **AWS KMS** — its private key never enters the + server process. + +This is a self-hosted alternative to the ECS reference deployment +([`SERVER_AWS_DEPLOY.md`](../operations/server-aws-deploy.md)): you run the published +image on your own host but use the same AWS-managed signer backends. It is +distinct from [`LOCAL_DEV.md`](../getting-started/local-dev.md), which keeps keys on the +filesystem. + +Every setting is in one place — [`.env.example`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/docs/guides/aws-signers/.env.example) and +[`docker-compose.yml`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/docs/guides/aws-signers/docker-compose.yml) in this directory. For the +authoritative meaning of any variable, see +[`CONFIGURATION.md`](../reference/configuration.md). + +## Prerequisites + +- Docker and the AWS CLI, with credentials that can create a KMS key and a + Secrets Manager secret. +- The repo checked out (for this Compose file and the `scripts/aws-deploy.sh` + bootstrap helpers). + +## 1. Create the KMS ECDSA key + +```bash +STACK_NAME=guardian ./scripts/aws-deploy.sh bootstrap-kms-ecdsa-key +``` + +This creates an `ECC_SECG_P256K1` / `SIGN_VERIFY` key plus an +`alias/guardian-ack-ecdsa` alias and prints the key ARN. The alias is a stable +handle you can use as `GUARDIAN_ACK_ECDSA_KMS_KEY_ID`. + +## 2. Create the Falcon secret in Secrets Manager + +```bash +export TF_VAR_guardian_ack_ecdsa_kms_key_arn="" +STACK_NAME=guardian ./scripts/aws-deploy.sh bootstrap-ack-keys +``` + +With the KMS ARN exported, `bootstrap-ack-keys` generates and stores **only the +Falcon** secret (default name `guardian/server/ack-falcon-secret-key`) and skips +ECDSA, since ECDSA is KMS-backed. See +[`runbooks/secrets.md`](../runbooks/secrets.md#hosted-ecdsa-backend-aws-kms) +for key lifecycle and the immutable-spec caveat. + +## 3. Configure the environment + +From this directory: + +```bash +cp .env.example .env +``` + +Set in `.env`: + +- `POSTGRES_PASSWORD` — a strong, stable, URL-safe value. +- `AWS_REGION` — the region holding the secret and key. +- `GUARDIAN_ACK_FALCON_SECRET_ID` — the Falcon secret name from step 2. +- `GUARDIAN_ACK_ECDSA_KMS_KEY_ID` — the alias or ARN from step 1. + +The Compose file already pins `GUARDIAN_ENV=prod` (the switch that makes the +server load ACK keys from Secrets Manager) and `GUARDIAN_ACK_ECDSA_BACKEND=aws-kms`. + +### AWS credentials for the container + +`GUARDIAN_ENV=prod` means the container calls Secrets Manager and KMS, so it +needs credentials. The Compose file passes them through from your shell: + +```bash +export AWS_ACCESS_KEY_ID=... AWS_SECRET_ACCESS_KEY=... AWS_SESSION_TOKEN=... +``` + +For a long-lived host, prefer an EC2 instance role or container credentials over +static keys (leave the `AWS_*` keys unset and the SDK picks up the role). + +## 4. Run + +From this directory (Compose auto-discovers `docker-compose.yml` and `.env`): + +```bash +docker compose up +``` + +At startup the server imports the Falcon key into its keystore and runs a KMS +sign probe to confirm `kms:Sign` works — it fails fast on a misconfigured key or +missing permission rather than at first acknowledgement. The logs include +`ECDSA ACK signer ready` with the active backend. + +## 5. Validate + +```bash +curl -s localhost:3000/pubkey | jq . +``` + +You should see the Falcon and ECDSA public keys and commitments. The ECDSA +commitment is derived from the KMS key; record it — moving to a different KMS +key later is a Guardian identity change (a `SwitchGuardian` migration for +existing accounts), not a routine rotation. + +## Troubleshooting + +| Symptom | Likely cause | +|---|---| +| `configuration_error` mentioning the sign probe at startup | KMS key wrong spec (must be `ECC_SECG_P256K1` / `SIGN_VERIFY`), or the task lacks `kms:Sign` | +| Startup fails resolving the Falcon secret | `GUARDIAN_ACK_FALCON_SECRET_ID` wrong, or credentials can't read it / wrong `AWS_REGION` | +| `AWS_REGION` errors at boot | `GUARDIAN_ENV=prod` requires `AWS_REGION` to be set | + +See [`TROUBLESHOOTING.md`](../operations/troubleshooting.md) for the full error-code playbook. diff --git a/versioned_docs/version-0.16/builder/miden-guardian/guides/horizontal-scaling.md b/versioned_docs/version-0.16/builder/miden-guardian/guides/horizontal-scaling.md new file mode 100644 index 00000000..52e474c6 --- /dev/null +++ b/versioned_docs/version-0.16/builder/miden-guardian/guides/horizontal-scaling.md @@ -0,0 +1,317 @@ +--- +title: "Horizontal scaling: two replicas behind a proxy" +--- +Run two Guardian replicas behind a round-robin proxy, sharing one Postgres, and +watch the coordination layer (issue #242) work end to end on your laptop. This +mirrors the prod topology — 2–6 ECS tasks behind a load balancer — in miniature. + +```text + :8080 (HTTP) ┌─────────────┐ server-a :3000 (HTTP) + client ──▶ │ proxy/Caddy │ ──round-robin──┬──▶ :50052 (gRPC) + :50051 (gRPC) └─────────────┘ └──▶ server-b :3010 (HTTP) + :50053 (gRPC) + │ │ + └────┬────┘ + ▼ + postgres :5432 + (sessions · challenges · worker lease) +``` + +Everything that must be shared for a correct multi-replica deployment lives in +Postgres, so a session minted on one replica is honored on the other, and only +one replica ever canonicalizes. The variable meanings live in +[`../../CONFIGURATION.md`](../reference/configuration.md); the operational contract is in +[`../../runbooks/horizontal-scaling.md`](../runbooks/horizontal-scaling.md). + +## Prerequisites + +- Docker with Compose v2 (`docker compose`). +- The Rust toolchain and `jq`, used once to generate the shared ACK signing + keys (next section). +- No AWS anywhere. The replicas share one Guardian identity through two local + key files (`GUARDIAN_ACK_SECRET_PROVIDER=file`) — the non-AWS stable identity + from [issue #289](https://github.com/OpenZeppelin/guardian/issues/289). + +## Configure and run + +```sh +cp .env.example .env +# Set a real cursor secret — it MUST be identical on every replica: +# openssl rand -hex 32 → paste into GUARDIAN_DASHBOARD_CURSOR_SECRET +cp operators.example.json operators.json # empty allowlist `[]`; add a key for the login walkthrough (step 8) +``` + +Generate **one** ACK keypair that both replicas mount (this is the fleet's +Guardian identity — see [What is shared](#what-is-shared-and-why)). The files +must be owner-only or the server refuses to start: + +```sh +mkdir -p ack-keys +cargo run --quiet -p guardian-server --bin ack-keygen \ + | { read -r json; \ + jq -rj '.falcon_secret_key' <<<"$json" > ack-keys/ack-falcon-secret-key; \ + jq -rj '.ecdsa_secret_key' <<<"$json" > ack-keys/ack-ecdsa-secret-key; } +chmod 600 ack-keys/ack-falcon-secret-key ack-keys/ack-ecdsa-secret-key +``` + +`ack-keys/` is git-ignored. Treat it like any private key material — and note +that regenerating it changes the Guardian's identity, freezing any multisig +account that pinned the old one (see the +[secrets runbook](../runbooks/secrets.md#self-hosted-stable-identity-without-aws)). + +> **Building an unreleased version?** The published `latest` image does not yet +> contain these coordination changes (issue #242). Drop in the local-build +> override so the replicas build from the repo-root `Dockerfile` instead of +> pulling from the registry — Compose auto-merges it, so no extra flags are +> needed on any command: +> +> ```sh +> cp docker-compose.override.yml.example docker-compose.override.yml +> ``` +> +> Once the change ships in a published image, delete +> `docker-compose.override.yml` to go back to the registry image. + +```sh +docker compose up -d --build +``` + +The proxy is at [http://localhost:8080](http://localhost:8080). Each replica is also exposed directly — +`server-a` on `:3000`, `server-b` on `:3010` — so you can target a specific +replica during the walkthrough. Postgres is on `:5432`. + +gRPC mirrors the HTTP layout: the proxy round-robins plaintext gRPC (h2c) on +`:50051` — which is the Rust demo CLI's default endpoint, so its `[1] Local +gRPC` choice goes through the load balancer — with the replicas reachable +directly on `:50052` (A) and `:50053` (B). + +## What is shared (and why) + +| Shared in Postgres | Table | Effect across replicas | +|---|---|---| +| Operator/EVM sessions | `auth_sessions` | Log in on A, your cookie works on B; logout is honored fleet-wide. | +| Login challenges | `auth_challenges` | A challenge is single-use even if issued on A and verified on B. | +| Canonicalization lease | `worker_leases` | Exactly one replica promotes candidates; the others stand by. | +| Replay protection | `account_auth_state` | A request timestamp accepted on A cannot be replayed to B; each per-account-and-signer timestamp is usable exactly once fleet-wide. A retained migration floor also protects signers first authorized after an account-scoped upgrade. | + +Coordination is **backend-derived**: it is on because the backend is Postgres. +No environment variable enables or disables it. + +**Upgrading across schema migrations**: migrations run automatically at +startup, and the first replica to boot a new binary migrates the shared +database for the whole fleet. Migrations that move authentication state lock +the affected table for their transaction, so replay timestamps accepted by +old replicas are never lost mid-migration — old-binary writes either land +before the migration's snapshot or fail on the schema change. A replica still +running the previous binary therefore fails closed — its queries name columns +the migration removed, so it serves errors instead of authenticating against +stale state — until it is replaced. Plan rolling deploys accordingly: old +replicas may error (never misbehave) during the window between the first +new-binary boot and the last replica replacement. + +One shared thing lives **outside** Postgres: the ACK signing keys. Both +replicas mount the same `./ack-keys` files and load them via +`GUARDIAN_ACK_SECRET_PROVIDER=file`, so the fleet presents a single, +restart-stable Guardian identity at `/pubkey`. That matters for multisig: each +account pins the guardian's `/pubkey` commitment into its +`openzeppelin::guardian::public_key` slot at configure time, and a replica +whose identity doesn't match rejects co-signing with `invalid GUARDIAN public +key binding`. With per-replica ephemeral keys (the non-prod default), routing +multisig through the proxy would fail on every request the "wrong" replica +answered. + +## Validation walkthrough + +### 1. Both replicas report shared coordination + +```sh +docker compose logs server-a server-b | grep -i coordination +``` + +Each replica prints one line; both must read `mode=shared backend=postgres`. If +you ever see `mode=single-process backend=filesystem`, that replica is **not** +safe to run alongside others. + +### 2. One Guardian identity across the fleet + +```sh +diff <(curl -s http://localhost:3000/pubkey) <(curl -s http://localhost:3010/pubkey) \ + && echo "identical" +``` + +Both replicas serve a byte-identical `/pubkey` response because they load the +same `./ack-keys` files, and the identity survives restarts: `docker compose +restart server-a` and run the diff again — still identical. (Remove the +`GUARDIAN_ACK_*` variables from `docker-compose.yml` and each replica mints its +own ephemeral key per boot; the diff then fails and multisig through the proxy +breaks — see [What is shared](#what-is-shared-and-why).) + +### 3. Exactly one canonicalization lease holder + +```sh +docker compose exec postgres \ + psql -U guardian -d guardian \ + -c "select lease_name, holder_id, fence_token from worker_leases;" +``` + +You get a single `canonicalization` row with one `holder_id` (formatted +`{pid}-{random}`) — never two. Both replicas run the worker loop, but only the +lease holder does work; the other keeps trying to acquire and backs off. + +### 4. Lease failover with a fencing-token bump + +Stop the current holder and watch a different replica take over within the lease +TTL (~30s, i.e. 3× the 10s canonicalization interval): + +```sh +docker compose stop server-a # if A wasn't the holder, stop server-b instead +watch -n2 'docker compose exec -T postgres \ + psql -U guardian -d guardian \ + -c "select holder_id, fence_token, expires_at from worker_leases;"' +``` + +`holder_id` changes to the surviving replica and `fence_token` **increments** — +the increment is the steal signal a superseded holder uses to fence itself off +at its next write. Bring the replica back with `docker compose start server-a`; +the lease does not bounce back (the current holder keeps renewing). + +### 5. Proxy request failover + +The lease failover above is server-side; the proxy also has to stop routing +*client* requests to a dead replica. That is what the `health_uri` / `lb_*` +directives in the [`Caddyfile`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/docs/guides/horizontal-scaling/Caddyfile) do — a bare `round_robin` (no health +checks) keeps sending half the traffic to the dead replica and returns `502`. +Kill a replica and hit the proxy: + +```sh +docker compose stop server-b +for i in $(seq 1 4); do curl -s -o /dev/null -w "%{http_code} " \ + http://localhost:8080/pubkey; done; echo +``` + +Every response stays `200` — Caddy health-checks each replica and routes only to +the survivor. Bring it back with `docker compose start server-b`; Caddy re-adds +it within one health interval (~5s). (Strip the health directives from the +`Caddyfile` and the same loop returns alternating `502`s.) + +### 6. Auth fails closed when the shared store is down + +Pause Postgres and watch the holder step down rather than barrel ahead: + +```sh +docker compose pause postgres +docker compose logs -f server-a server-b # Ctrl-C after a few seconds +``` + +You will see lease renew/acquire failures and storage errors — the worker +**cancels its pass** instead of canonicalizing blind. If you have completed the +login walkthrough below, an authenticated request fails rather than silently +succeeding: authentication is **fail-closed**. + +> `docker compose pause` freezes Postgres mid-connection (SIGSTOP), so an +> in-flight request *hangs until it times out* rather than getting a prompt +> `5xx`. Either way it never succeeds. To see a fast `5xx` instead (socket +> closed → connection refused), use `docker compose stop postgres` and +> `docker compose start postgres` to recover. + +Recover with: + +```sh +docker compose unpause postgres +``` + +Coordination resumes automatically; no manual intervention. + +### 7. Rate-limit partitioning and `X-Forwarded-For` + +Each replica enforces `global / GUARDIAN_MAX_REPLICAS`. With the default global +burst of 10 and `GUARDIAN_MAX_REPLICAS=2`, a single replica caps at ~5 req/s. +Hammer one replica directly (the challenge endpoint is unauthenticated and +rate-limited): + +```sh +for i in $(seq 1 12); do + curl -s -o /dev/null -w "%{http_code} " \ + "http://localhost:3000/auth/challenge?commitment=0xdemo" +done; echo +``` + +After the per-replica burst is spent you see `429`s. Through the proxy +(`:8080`), Caddy sets `X-Forwarded-For`, so the server keys the limit on your +real client IP rather than the proxy address — confirm by repeating the loop +against `http://localhost:8080/...` and seeing the same per-IP behavior. + +### 8. (End-to-end) An operator session survives losing its replica + +This is the headline, and it needs a real operator key to sign the challenge. +Use the [`examples/operator-smoke-web`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/examples/operator-smoke-web) +harness (or the operator client) pointed at the **proxy** URL +`http://localhost:8080`: + +1. Generate a Falcon operator key with the harness and add its public key to + `operators.json` (replacing the empty `[]`); the allowlist hot-reloads, so no + restart is needed: + + ```json + [{ "public_key": "0x", "permissions": ["dashboard:read"] }] + ``` +2. Complete the login (`GET /auth/challenge` → sign → `POST + /auth/verify`). The proxy round-robins, so this may land on either + replica; the session row is written to `auth_sessions`. +3. Make an authenticated request (e.g. `GET /dashboard/accounts`) a few times — + each may be served by a different replica, and all succeed: the cookie is + validated against the shared store, not per-process memory. +4. Now `docker compose stop` the replica that handled your login and repeat — + **your session still works** on the survivor. Then `POST + /auth/logout`; the revocation is honored on every replica. + +## Cleanup + +```sh +docker compose down -v # -v also drops the Postgres + keystore volumes +``` + +`./ack-keys` is a host directory, so `down -v` leaves it alone — keep it and the +Guardian identity survives a full teardown. Delete it only to discard that +identity for good (any account configured against it would need a +`SwitchGuardian` migration). + +## From this demo to production + +This guide stays AWS-free to be runnable; a real prod deployment differs in two +ways that do not change the coordination behavior shown above: + +- **`GUARDIAN_ENV=prod`** activates the prod-stage startup guards — a filesystem + storage backend and a rate limit that partitions to 0 req/replica are each + refused at startup. (An unset `GUARDIAN_DASHBOARD_CURSOR_SECRET` only *warns* — + it degrades cross-replica pagination on the dashboard feeds and the client `/delta/history` endpoint, not custody, so a + single-replica prod server still boots.) Note these guards live behind the ACK + registry init, which in prod requires AWS first: set `GUARDIAN_ENV=prod` + without `AWS_REGION` and the server refuses to start with `AWS_REGION is + required when GUARDIAN_ENV=prod` before it ever reaches the storage or + rate-limit checks — so observing those two specifically needs AWS configured. +- **Where the shared ACK key lives.** Both setups give the fleet one guardian + identity — the property multisig needs, since each account pins the + guardian's `/pubkey` commitment at configure time. This guide sources it from + two local files (`GUARDIAN_ACK_SECRET_PROVIDER=file`, the `./ack-keys` bind + mount); prod sources the same key material from AWS Secrets Manager + (`GUARDIAN_ENV=prod` defaults the provider to `aws`), so no secret sits on a + task's disk. The formats are identical — the hex strings `ack-keygen` emits — + so keys are portable between the two providers; see the + [aws-signers guide](./aws-signers.md) and the + [secrets runbook](../runbooks/secrets.md#ack-signing-keys). Per-account + state already lives in Postgres and needs nothing extra. + +> **Smoke-testing multisig against this demo?** The shared `file`-provider +> identity makes the round-robin proxy safe for the full multisig flow — +> configure, co-sign, and execute can each land on a different replica. Point +> HTTP clients at `:8080` and gRPC clients (the Rust demo CLI) at `:50051`, +> its default. Just make the client's Miden RPC network match the server's +> `GUARDIAN_NETWORK_TYPE` (e.g. devnet RPC ↔ `MidenDevnet`), or +> canonicalization will loop on an `on_chain=0x00…0` commitment because the +> account was deployed to a different network than the guardian verifies +> against. + +The managed path (published Postgres image + the prod Terraform profile) sets +all of this for you; see [`../../SERVER_AWS_DEPLOY.md`](../operations/server-aws-deploy.md) +and the [horizontal-scaling runbook](../runbooks/horizontal-scaling.md). diff --git a/versioned_docs/version-0.16/builder/miden-guardian/guides/miden-dashboard.md b/versioned_docs/version-0.16/builder/miden-guardian/guides/miden-dashboard.md new file mode 100644 index 00000000..990b98e8 --- /dev/null +++ b/versioned_docs/version-0.16/builder/miden-guardian/guides/miden-dashboard.md @@ -0,0 +1,151 @@ +--- +title: "Run the Miden Dashboard UI with Guardian" +sidebar_position: 3 +--- +Stand up a local Guardian server and the operator dashboard UI +([`0xMiden/guardian-dashboard`](https://github.com/0xMiden/guardian-dashboard)) +side by side with Docker Compose, then log in as an operator and browse +accounts end to end. + +This is the assembly layer that ties together the existing reference docs — it +does not re-explain them. For the dashboard's trust model, the +challenge→session auth flow, the allowlist format, and the permission +vocabulary, see [`DASHBOARD.md`](../operations/dashboard.md). For the authoritative +meaning of any server variable, see [`CONFIGURATION.md`](../reference/configuration.md). + +The stack bundles **Postgres** for Guardian's state and metadata — the published +Guardian image is built with the `postgres` feature, so a database is required +(this guide is a realistic self-hosted base, not a throwaway filesystem toy). The +ACK signer uses an in-memory key held in a local keystore — fine for a single +host; production deployments move it to AWS Secrets Manager / KMS (see the +[AWS-managed signers guide](./aws-signers.md)). The dashboard is a +Next.js app the upstream repo ships without an image, so Compose runs it from a +source clone on the stock `node` image. + +How the pieces talk: the browser only talks to the dashboard; the dashboard's +**Next.js backend** holds the operator's Falcon private key and signs the +challenge→session flow against the Guardian server itself. The server is +reached over the Compose network at `http://server:3000`, so it needs **no CORS +configuration** here. + +## Prerequisites + +- Docker. +- A clone of the dashboard inside this guide directory. From here: + ```bash + git clone https://github.com/0xMiden/guardian-dashboard + ``` + By default this guide expects it at `./guardian-dashboard`. Point elsewhere + with `GUARDIAN_DASHBOARD_PATH` in `.env`. +- A free [Clerk](https://dashboard.clerk.com) application — the dashboard uses + Clerk for human sign-in and cannot start without its keys. + +## 1. Generate an operator key + +The dashboard ships a helper that emits everything you need — the **private +key** and **commitment** for the dashboard, and the **public key** for the +Guardian allowlist. From your `guardian-dashboard` clone: + +```bash +npm install +npx tsx scripts/generate-operator-key.ts +``` + +It prints two labelled blocks: a `GUARDIAN_OPERATOR_PRIVATE_KEY` / +`GUARDIAN_OPERATOR_COMMITMENT` pair, and a `["0x…"]` public key. These are not +env vars you set directly — in step 3 the private key and commitment go into the +`privateKey` and `commitment` fields of the `GUARDIAN_ENDPOINTS` entry in `.env`, +and the public key goes into `operators.json`. Keep the private key secret. +(Falcon keys generated by the multisig SDK or the +[`operator-smoke-web`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/examples/operator-smoke-web) UI work too, as +long as you can export all three values.) + +## 2. Configure Clerk + +In your Clerk app, copy the **test** publishable and secret keys for step 3. +Then create the admin user (or use your own) and set its **public metadata** +so the dashboard authorises it for this node: + +```json +{ "role": "admin", "endpointIds": ["local"] } +``` + +`endpointIds` must include the endpoint `id` you use in `GUARDIAN_ENDPOINTS` +below (`local` here). Without it the dashboard signs you in but shows no nodes. + +## 3. Configure the environment + +From this directory: + +```bash +cp .env.example .env +cp operators.example.json operators.json +``` + +In `.env` set: + +- `POSTGRES_PASSWORD` — a strong, stable, URL-safe value for the bundled Postgres. +- `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` and `CLERK_SECRET_KEY` — the test keys + from step 2. +- `GUARDIAN_ENDPOINTS` — fill `commitment` and `privateKey` from step 1. Leave + `url` as `http://server:3000` (the server's address on the Compose network) + and keep `network` equal to `GUARDIAN_NETWORK_TYPE`. + +In `operators.json` replace `0x` with the public +key from step 1. The default entry grants `dashboard:read` and `accounts:pause`; +trim the permission set if you only need read access (see the permission +vocabulary in [`DASHBOARD.md`](../operations/dashboard.md#permission-vocabulary)). The server +hot-reloads this file on every challenge and authenticated request, so you can +edit operators without restarting. + +## 4. Run + +From this directory (Compose auto-discovers `docker-compose.yml` and `.env`): + +```bash +docker compose up +``` + +The first dashboard boot runs `npm install` inside the container and is slow; +subsequent boots reuse the cached `node_modules` volume. The server starts +first; the dashboard only calls it once you sign in, by which point it is ready. + +## 5. Validate + +Confirm the server is live and serving its ACK keys: + +```bash +curl -s localhost:3000/pubkey | jq . +``` + +Then open the dashboard at [http://localhost:3001](http://localhost:3001), sign in through Clerk, +select the **Local Guardian** endpoint, and confirm the account list loads. A +successful list proves the full path: Clerk sign-in → operator +challenge→session against the server → an authenticated `/dashboard/accounts` +call. + +## Troubleshooting + +| Symptom | Likely cause | +|---|---| +| Sign-in works but no endpoint is selectable | Clerk public metadata missing `endpointIds`, or it omits the `id` from `GUARDIAN_ENDPOINTS` | +| Endpoint selected but accounts list is empty or 401s | Operator public key not in `operators.json`, or `network` in `GUARDIAN_ENDPOINTS` ≠ `GUARDIAN_NETWORK_TYPE` | +| Dashboard fails to start citing Clerk keys | `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` / `CLERK_SECRET_KEY` unset in `.env` | +| Dashboard container exits / `/app` is empty | `GUARDIAN_DASHBOARD_PATH` doesn't point at your `guardian-dashboard` clone | +| First `docker compose up` hangs on the dashboard | `npm install` running on first boot — wait it out; later boots are fast | +| Port 3001 already in use | Stop the conflicting process, or remap the dashboard port in `docker-compose.yml` | + +See [`TROUBLESHOOTING.md`](../operations/troubleshooting.md) for the full server +error-code playbook. + +## Operations checklist + +When standing up the dashboard against a stack: + +- [ ] At least one operator with `dashboard:read` in the allowlist — otherwise + the dashboard is unreachable. +- [ ] `network` in `GUARDIAN_ENDPOINTS` matches `GUARDIAN_NETWORK_TYPE`. +- [ ] Clerk operator metadata `endpointIds` includes every endpoint `id` the + user should see. +- [ ] A fresh sign-in → endpoint select → account list round trip succeeds + before considering the stack live. diff --git a/versioned_docs/version-0.16/builder/miden-guardian/guides/observability.md b/versioned_docs/version-0.16/builder/miden-guardian/guides/observability.md new file mode 100644 index 00000000..0d676dbe --- /dev/null +++ b/versioned_docs/version-0.16/builder/miden-guardian/guides/observability.md @@ -0,0 +1,143 @@ +--- +title: "Observability with Prometheus + Grafana" +sidebar_position: 4 +--- +Run a Guardian server with Prometheus metrics enabled, scraped by Prometheus +and visualized in a pre-provisioned **Guardian — Server Overview** Grafana +dashboard — all from one `docker compose up`. Use it to see Guardian's metric +surface, or as a starting point for your own dashboards. + +This is a local, single-host stack (filesystem storage, anonymous Grafana). It +is distinct from a production setup, where you would scrape your real server +from an existing Prometheus and protect the endpoint as below. Every setting is +in [`.env.example`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/docs/guides/observability/.env.example) and [`docker-compose.yml`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/docs/guides/observability/docker-compose.yml) +in this directory; for the authoritative meaning of any variable, see +[`CONFIGURATION.md`](../reference/configuration.md) (“Runtime — metrics”). + +## What you get + +The stack runs three services on the internal Compose network: + +- **server** — Guardian with `GUARDIAN_METRICS_ENABLED=true`, serving the + Prometheus exposition on a dedicated listener (`:9464`, not published to the + host). +- **prometheus** — scrapes `server:9464` every 15s with a bearer token. +- **grafana** — auto-provisioned with the Prometheus datasource and the + dashboard, anonymous access for zero-friction local viewing. + +The server instruments, end to end: HTTP and gRPC request paths (rate, latency, +in-flight, status/code), Miden RPC (the upstream chain node), storage +operations and per-pool (`storage`/`metadata`) DB-pool saturation, +canonicalization, the delta & proposal lifecycle, account growth, operator auth, +rate limiting, refresher health, and process metrics. + +Two properties worth knowing as an operator: + +- **Scrapes are cheap.** Expensive cross-account aggregates (delta counts, + in-flight proposals, account totals, pool status) are computed by a background + refresher every `GUARDIAN_METRICS_REFRESH_INTERVAL_SECS` and published as + gauges — a scrape never touches the database. Staleness is observable as + `time() - guardian_metrics_refresh_timestamp_seconds`. +- **Cardinality is bounded by construction.** Every label value comes from a + closed set (route templates, a gRPC method allowlist, small enums). No account + IDs, nonces, keys, IPs, or error strings become labels. + +## Prerequisites + +- Docker (with Compose) and outbound internet — the server connects to its Miden + network's RPC at startup. + +## 1. Start the stack + +```bash +cd docs/guides/observability +cp .env.example .env # optional — defaults work as-is +docker compose up +``` + +The first run builds the Guardian server image from this repo — several minutes +on a cold build, since it compiles the server in release mode (it must include +the current metrics code, so it can't use the published Postgres-only image). +Later runs reuse the cached image. Prometheus and Grafana are pulled as images. + +## 2. Open the dashboard + +- **Grafana** → [http://localhost:3001](http://localhost:3001) — anonymous access is enabled, so you + land directly on **Dashboards → Guardian → Guardian — Server Overview**. +- **Prometheus** → [http://localhost:9090](http://localhost:9090) — check **Status → Targets**: the + `guardian` target should be `UP`. + +## 3. Generate traffic + +The panels move once the server sees requests: + +```bash +curl -s http://127.0.0.1:3000/pubkey >/dev/null +curl -s "http://127.0.0.1:3000/auth/challenge?commitment=0xdeadbeef" >/dev/null +``` + +## 4. Tear down + +```bash +docker compose down # add -v to also drop the volumes +``` + +## The dashboard + +Panels are grouped by subsystem and map 1:1 onto the metric taxonomy in +[`spec/api.md`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/spec/api.md) (“Metrics Endpoint”): + +| Section | Covers | +|---|---| +| Overview | build info, account count, in-flight proposals, refresh staleness, HTTP rate & error % | +| HTTP / gRPC request path | rate by route/method, status/code breakdown, p50/p95/p99 latency, in-flight | +| Miden RPC | upstream chain-node call rate, errors, p95 latency | +| Storage & DB pools | operation rate/latency, per-pool (`storage`/`metadata`) connection saturation | +| Canonicalization | run rate & duration, candidate outcomes, retries | +| Delta & proposal lifecycle | submissions, proposal events, deltas-by-status, in-flight | +| Accounts | total + creation rate by network kind | +| Auth & rate limiting | operator auth outcomes, sessions, rate-limit rejections, refresh failures | +| Process / runtime | CPU, RSS, file descriptors | + +An `Instance` variable (top-left) filters to a single replica or aggregates +across all of them. Some panels stay empty in this local filesystem stack — +**DB pool** needs a Postgres build, and **Miden RPC / deltas / proposals / +account creation** need a real signed multisig flow. Copy +[`grafana/dashboards/guardian.json`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/docs/guides/observability/grafana/dashboards/guardian.json) into +your own Grafana and adapt it. + +## Protecting the endpoint (production) + +This stack keeps the metrics listener on the internal network and gates it with +a throwaway `devtoken`. In production, defense is layered (see +[`spec/api.md`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/spec/api.md) and the +[security note in Configuration](../reference/configuration.md#runtime--metrics-prometheus)): + +1. **Network isolation first** — the listener binds loopback by default; keep + `9464` reachable only from the scraper's network (private subnet / security + group / sidecar). +2. **Bearer token second** — `GUARDIAN_METRICS_BEARER_TOKEN` gates scrapes with + a constant-time check (`401` otherwise). Point Prometheus at a mounted secret + with `authorization.credentials_file:` rather than an inline value. +3. **TLS** — terminate at a reverse proxy or sidecar where transport encryption + is required. + +Never expose `/metrics`, Prometheus, or this Grafana to a public network — the +ports here are bound to `127.0.0.1` and Grafana runs anonymous-admin precisely +because it is local-only. + +## AWS deployment + +The Terraform reference deployment ships this metric surface to **CloudWatch** +instead of Prometheus/Grafana: an ADOT Collector sidecar in the ECS task scrapes +the loopback-only endpoint and exports selected metrics via EMF, with a +Terraform-managed dashboard and alarms. See +[`SERVER_AWS_DEPLOY.md`](../operations/server-aws-deploy.md#metrics-dashboard-and-alarms). + +## Out of scope + +Alert and recording rules and runbooks are not shipped here. The +[metric taxonomy in `spec/api.md`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/spec/api.md) is the reference for +writing your own — e.g. alert on `guardian_db_pool_pending_acquires` sustained +above zero, on `time() - guardian_metrics_refresh_timestamp_seconds` exceeding a +few refresh intervals, or on gRPC/HTTP error ratios. diff --git a/versioned_docs/version-0.16/builder/miden-guardian/guides/overview.md b/versioned_docs/version-0.16/builder/miden-guardian/guides/overview.md new file mode 100644 index 00000000..910483c6 --- /dev/null +++ b/versioned_docs/version-0.16/builder/miden-guardian/guides/overview.md @@ -0,0 +1,36 @@ +--- +title: "Guides" +sidebar_position: 1 +--- +Task-oriented, end-to-end walkthroughs for running Guardian in a specific mode. +Each guide assembles a complete, copy-pasteable configuration in one place and +links to [`CONFIGURATION.md`](../reference/configuration.md) for the authoritative meaning +of each variable. + +These differ from the other docs by intent: + +- **Guides** (here) — "how do I run it set up like *this*?" end to end. +- [`CONFIGURATION.md`](../reference/configuration.md) — flat reference for every env var. +- [`SERVER_AWS_DEPLOY.md`](../operations/server-aws-deploy.md) — the ECS/Terraform deploy procedure. +- [`runbooks/`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/docs/runbooks/) — operational procedures (secrets, incidents). + +Guides use Docker Compose unless a guide says otherwise, so directory names +describe the *configuration* a guide demonstrates rather than repeating the +runner. Name a guide after what makes it distinct (its signer backends, +storage, or network), not after Compose. + +## Available guides + +| Guide | Mode | +|---|---| +| [AWS-managed ACK signers](./aws-signers.md) | Self-hosted Compose: Postgres + Secrets Manager (Falcon) + KMS (ECDSA) | +| [Miden Dashboard UI](./miden-dashboard.md) | Self-hosted Compose: Postgres + Guardian server + the Miden Dashboard operator UI | +| [Observability](./observability.md) | Local Compose: server + Prometheus + pre-provisioned Grafana dashboard | +| [Horizontal scaling](./horizontal-scaling.md) | Local Compose: two replicas + round-robin proxy + shared Postgres (sessions, lease failover, fail-closed auth) | + +## Adding a guide + +Give each guide its own subdirectory holding a `README.md` and its committed, +runnable artifacts (e.g. `docker-compose.yml` + `.env.example`), so the guide +and the config you copy live together and the config can be smoke-tested. Keep +variable explanations in `CONFIGURATION.md` rather than restating them here. diff --git a/versioned_docs/version-0.16/builder/miden-guardian/guides/postgres-tls.md b/versioned_docs/version-0.16/builder/miden-guardian/guides/postgres-tls.md new file mode 100644 index 00000000..42529137 --- /dev/null +++ b/versioned_docs/version-0.16/builder/miden-guardian/guides/postgres-tls.md @@ -0,0 +1,133 @@ +--- +title: "Verified database TLS with Docker Compose" +sidebar_position: 5 +--- +Run the Guardian server against a **TLS-enabled Postgres** and have the server +**verify the database certificate** — not just encrypt the connection. The +bundled Postgres terminates TLS with a self-signed certificate; the server +connects with `sslmode=verify-full` and validates that certificate against a CA +you generate. + +This guide is the local, self-contained way to see Guardian's database TLS +verification accept a good certificate and reject a bad one. It complements +[`LOCAL_DEV.md`](../getting-started/local-dev.md) (which uses a plaintext Postgres) and the +AWS reference deployment in +[`SERVER_AWS_DEPLOY.md`](../operations/server-aws-deploy.md) (which verifies the RDS +certificate the same way). For the authoritative meaning of `sslmode` / +`sslrootcert`, see [Database TLS](../reference/configuration.md#database-tls). + +Everything is in this directory: [`docker-compose.yml`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/docs/guides/postgres-tls/docker-compose.yml), +[`.env.example`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/docs/guides/postgres-tls/.env.example), and [`generate-certs.sh`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/docs/guides/postgres-tls/generate-certs.sh). + +> **Image version:** verified database TLS ships in the server image; run a +> `GUARDIAN_VERSION` that includes it. Older images don't parse `verify-full` / +> `sslrootcert` and fail to connect (or connect without verifying), so this guide +> only works on an image that includes the feature. Before it's released, build +> one locally — see [Run against a local build](#run-against-a-local-build). + +## How it fits together + +- `generate-certs.sh` creates a throwaway CA and a Postgres server cert whose + **SAN is `postgres`** — the Compose service name the server dials — so + `verify-full` hostname matching succeeds. +- A one-shot `db-cert-init` service fixes the key's owner/permissions into a + shared volume (Postgres rejects a world-readable or wrong-owner key), then + exits; Postgres starts only after it succeeds. This mirrors the init-container + pattern the ECS deployment uses to deliver the CA bundle. +- The server mounts only `ca.pem` (the public trust anchor) and connects with + `sslmode=verify-full&sslrootcert=/etc/guardian/tls/ca.pem`. + +## Prerequisites + +- Docker (with Compose) and OpenSSL. +- The repo checked out (for this Compose file and the cert script). + +## 1. Generate the certificates + +```bash +./generate-certs.sh +``` + +Writes `certs/ca.pem`, `certs/server.crt`, `certs/server.key` (gitignored). + +## 2. Configure the environment + +```bash +cp .env.example .env +``` + +Set `POSTGRES_PASSWORD` to a strong, URL-safe value. Optionally pin +`GUARDIAN_VERSION` and set `DB_SSLMODE` (default `verify-full`). + +## 3. Run + +```bash +docker compose up +``` + +Order is enforced by Compose: `db-cert-init` writes the key → Postgres starts +with `ssl=on` and becomes healthy → the server starts, runs migrations, and +opens its connection pool. The server verifies the Postgres certificate on both +the migration (libpq) and pool (rustls) connections. + +## 4. Validate + +A clean startup (no certificate error in the logs, migrations applied, pool +ready) means verification passed. Confirm the server is up: + +```bash +curl -s localhost:3000/pubkey | jq . +``` + +## 5. Experiment with the verification levels + +Stop the stack (`docker compose down`) and change `.env`, then `up` again: + +- **`DB_SSLMODE=verify-ca`** — the chain is still verified, but the hostname is + not. Connection still succeeds (the CA is trusted). +- **Untrusted CA** — point the trust anchor at an unrelated CA to see a refusal: + generate a second CA (`openssl req -x509 -newkey rsa:2048 -nodes -keyout other.key -out other.pem -subj "/CN=Other" -days 1`) + and bind-mount `other.pem` over `/etc/guardian/tls/ca.pem`. The server **fails + fast** at startup with a certificate-verification error — it does **not** fall + back to an unverified connection. +- **Hostname mismatch** — re-run `generate-certs.sh` after editing it to use a + different SAN (e.g. `DNS:not-postgres`). Under `verify-full` the connection is + **refused**; switch to `DB_SSLMODE=verify-ca` and the same cert is **accepted** + (hostname isn't checked at that level). + +## Run against a local build + +Until the feature is in a published image, build the server image from the repo +and point the guide at it. From the repo root: + +```bash +docker build --target server-runner -t guardian:dbtls-local . +``` + +Then in `.env` (the `postgres` feature is compiled in by default): + +```bash +GUARDIAN_IMAGE=guardian:dbtls-local +GUARDIAN_PULL_POLICY=missing +``` + +`docker compose up` now runs your local image. (`GUARDIAN_PULL_POLICY=missing` +stops Compose from trying to pull the local-only tag.) + +## Cleanup + +```bash +docker compose down -v # also removes the Postgres + cert volumes +``` + +## Troubleshooting + +| Symptom | Likely cause | +|---|---| +| Postgres exits citing key permissions | `generate-certs.sh` not run, or `certs/` not readable by `db-cert-init`; re-run step 1 | +| Server error naming `sslrootcert` | `certs/ca.pem` missing or not mounted; re-run step 1 | +| Server refuses with a certificate-verification error | trust anchor doesn't match the server cert, or (under `verify-full`) the cert SAN ≠ `postgres` | +| Works under `verify-ca`, fails under `verify-full` | hostname/SAN mismatch — the server cert's SAN must be `postgres` | + +See [`TROUBLESHOOTING.md`](../operations/troubleshooting.md#server-fails-to-start) for the +database-TLS failure-to-cause mapping. diff --git a/versioned_docs/version-0.16/builder/miden-guardian/index.md b/versioned_docs/version-0.16/builder/miden-guardian/index.md new file mode 100644 index 00000000..f99be753 --- /dev/null +++ b/versioned_docs/version-0.16/builder/miden-guardian/index.md @@ -0,0 +1,80 @@ +--- +title: Miden Guardian +sidebar_position: 0 +--- + +# Miden Guardian + +Miden Guardian is an offchain coordination service built by [OpenZeppelin](https://www.openzeppelin.com/) for Miden accounts. It helps clients back up private account state, synchronize that state across devices, and coordinate multi-signer workflows without giving the Guardian provider unilateral control over the account. + +:::warning +Guardian is a work in progress. Use the docs and the [OpenZeppelin Guardian repository](https://github.com/OpenZeppelin/guardian) as the current operator and integration reference, and expect interfaces and operational defaults to evolve. +::: + +## The problem + +Miden's execution model requires clients to manage their own private state - accounts, notes, storage - locally on-device. While this provides strong privacy and scalability, it introduces real challenges: + +- **Solo-account users** risk losing access if local state is not backed up. Losing any part of the account state means losing access to the account itself. +- **Shared-account users** risk having stale state due to a faulty or malicious participant withholding updates. +- **Multi-device users** need all devices to see the same account state, but there is no public ledger to read from. + +On a public chain, the ledger is a universally readable source of truth. Every device and every signer can independently observe the latest state. In Miden's private account model, the canonical state is defined by the onchain commitment, but the full private state is not publicly readable. The coordination surface moves offchain. + +## What Guardian provides + +Guardian addresses these challenges by acting as an offchain coordination layer: + +- **Backup and recovery** - Account state is stored on Guardian, recoverable even if a device is lost. +- **Multi-device sync** - Multiple devices push and pull state through Guardian, staying in sync with the latest canonical state. +- **Multi-party coordination** - Shared accounts use delta proposals to coordinate threshold signing across participants. +- **Integrity verification** - State changes are linked by commitments, validated against the Miden network, and acknowledged with a cryptographic signature. + +Guardian is not a private execution environment, a sequencer, or a rollup. Clients still execute transactions locally and submit proofs to Miden. Guardian stores and coordinates the state data that authorized clients submit to it. + +## Architecture at a glance + +| Layer | Responsibility | What remains authoritative | +|---|---|---| +| **Miden network** | Stores commitments and verifies account updates. | The canonical account commitment. | +| **Miden client** | Executes transactions, manages local state, proves updates, and signs Guardian requests. | The user's keys and local verification logic. | +| **Guardian server** | Stores snapshots, stores deltas, authenticates readers/writers, acknowledges accepted deltas, and coordinates proposals. | Only the data it has accepted and signed. | +| **Storage backend** | Persists metadata, state snapshots, deltas, and pending proposals. | The append-only commitment chain, as verified by clients and the network. | + +Guardian is non-custodial. The provider cannot move funds unilaterally - it stores state and coordinates changes, but users retain cryptographic control over their accounts at all times. + +## Trust model summary + +Guardian has an explicit trust boundary: + +- **Safety**: The provider cannot forge account state, silently rewrite the commitment chain, or move assets without the required account signatures. +- **Liveness**: The provider can delay, censor, or refuse requests. Users should keep recovery keys and provider-rotation paths available. +- **Privacy**: Guardian protects state from unauthorized API callers, but the server stores the account state and delta payloads it receives. Treat the operator as able to observe submitted payloads unless your deployment adds a separate encryption layer or you operate the service yourself. +- **Freshness**: Clients should verify acknowledgments and compare local state against the latest onchain commitment when freshness matters. + +## Learn more + + + + What Guardian is and is not, the custody model, and the trust boundary. + + + A Guardian running locally in under 60 seconds with one command. + + + The server modules, storage modes, and the dashboard subsystem. + + + Readiness, configuration, and the path to a production deployment. + + + End-to-end Compose walkthroughs: signers, dashboard, observability, TLS. + + + Multi-party threshold signature workflows powered by Guardian. + + + +## Repository + +- [Miden Guardian](https://github.com/OpenZeppelin/guardian) - Guardian server, client SDKs, multisig client libraries, and specification diff --git a/versioned_docs/version-0.16/builder/miden-guardian/miden-compatibility.md b/versioned_docs/version-0.16/builder/miden-guardian/miden-compatibility.md new file mode 100644 index 00000000..a4bb1a08 --- /dev/null +++ b/versioned_docs/version-0.16/builder/miden-guardian/miden-compatibility.md @@ -0,0 +1,178 @@ +--- +title: "Miden compatibility" +--- +Which Miden protocol line each Guardian release targets, what changed between +lines, and what each upgrade does to stored data. + +> Guardian's own version and Miden's are **not** aligned. Guardian 0.16.x runs on +> Miden 0.15; Miden 0.16 arrives in Guardian 0.17.x. Read the matrix rather than +> matching the numbers. + +This page is the single source of truth for those facts. Procedures live +elsewhere and link here: + +| For | Read | +|---|---| +| Operator upgrade steps | [`PRODUCTION.md`](./operations/production.md) | +| Diagnosing a version-mismatch symptom | [`TROUBLESHOOTING.md`](./operations/troubleshooting.md) | +| SDK contract pinning and release policy | [`MULTISIG_SDK.md`](./reference/multisig-sdk.md#contract-version-pinning) | + +## Support matrix + +| Guardian | Miden protocol | `miden-protocol` / `miden-standards` | `miden-client` (Rust) | `@miden-sdk/miden-sdk` (npm) | +|---|---|---|---|---| +| 0.17.0 | 0.16 | `=0.16.1` | `=0.16.0` | `0.16.0` (exact) | +| 0.16.x | 0.15 | `0.15.3` | `0.15.0` | `^0.15.8` | +| 0.15.x | 0.15 | `0.15.x` | `0.15.0` | `^0.15.0` | +| 0.14.x | 0.14 | n/a | `0.14.x` | `^0.14.0` | +| 0.13.x | 0.13 | n/a | `0.13.0` | `^0.13.0` | +| 0.12.x | 0.12 | n/a | `0.12.5` | `^0.12.5` | + +0.17.0 builds on the stable Miden 0.16 release. `@miden-sdk/miden-sdk` 0.16.0 embeds +`miden-client` 0.16.0 and `miden-protocol` / `miden-standards` 0.16.1, which is why the +Rust pins are 0.16.1 for the protocol crates and 0.16.0 for the client crates. + +Pins are exact on the 0.16 line, and the Rust and npm pins must move together: nothing +at build time verifies that the npm SDK's embedded `miden-standards` matches the Rust +pin, so the CI parity gates are what catch drift. See +[`MULTISIG_SDK.md`](./reference/multisig-sdk.md#contract-version-pinning). + +**Upgrading from 0.16.x (Miden 0.15) to 0.17.0 (Miden 0.16)** is a protocol-line change: +the guarded-multisig auth component now pays the transaction fee and transaction summaries +bind the reference block, so nothing signed or stored on 0.15 verifies on 0.16. Stored Miden account data is +reset by the embedded migration listed below, accounts must be recreated, and the Rust +SDK's local `miden-client` SQLite store must be recreated (the browser IndexedDB store +migrates in place). `miden-client` 0.16.0 also raises the MSRV to 1.98.1. + +**0.17.0-rc.1 to rc.3 were pre-releases on the Miden 0.16 release candidates**, published +to npm under the `rc` dist-tag. They are not supported. Every rc pinned a different +`auth_tx` procedure root than 0.17.0 (0.16.1 factored the fee payment into +`miden::standards::auth::multisig::pay_bounded_fee`), so an account created on an rc is +rejected with `UnsupportedContractVersion`, and a proposal still pending from an rc cannot +be reproduced: `TransactionRequest` serialization changed and the auth arg commitment moved. +Execute or cancel every pending proposal on the rc version, have GUARDIAN drop any that +cannot be executed, then recreate the account on 0.17.0. Recreating the account does not +clear proposals served for the old one. + +A Guardian server or SDK built on one protocol line rejects a node from another. +Run a node matching the **Miden protocol** column. + +## Data resets + +Guardian has twice been unable to migrate stored account data across a Miden +line. Both resets are embedded migrations that run automatically at server +startup, both are irreversible, and both scope the purge to Miden rows using +`account_metadata.network_config->>'kind'` so EVM accounts survive. + +| Migration | Introduced in | Deletes | Preserves | +|---|---|---|---| +| `2026-08-24-000001_miden_016_irreversible_reset` | Guardian 0.17.x | Miden rows in `delta_proposals`, `deltas`, `states`, `account_metadata`; `account_auth_state` by cascade | EVM rows, `admin_actions`, `auth_sessions`, `auth_challenges`, `storage_encryption_marker`, `worker_leases`, keystore | +| `2026-06-14-000001_v015_account_id_cutover` | Guardian 0.15.x | pre-0.15 (v0 account ID) Miden rows in the same four tables | EVM rows, `admin_actions` | + +Both are Postgres-only. Filesystem-backed deployments reset by starting from +empty storage and metadata directories, preserving the keystore directory. + +A deployment upgrading across more than one line runs both migrations in the same +startup; the newer reset subsumes the older one. + +## Guardian 0.17.x on Miden 0.16 + +Nothing stored under Miden 0.15 survives, because the account's on-chain surface +moved in several independent ways: + +- **Procedure roots changed**, so stored proposals no longer address the + procedures they were signed against, and root-keyed storage reads + (`procedure_thresholds`) miss. +- **ECDSA-k256 public-key commitments changed** in `miden-crypto` 0.28 to hash + native affine-coordinate limbs (`qx || qy` as little-endian `u32` limbs) + instead of the compressed SEC1 bytes, so stored approver commitments no longer + match their keys. Compressed SEC1 *serialization* is unchanged, which is why + this fails as a commitment mismatch rather than a decode error. +- **The signature advice ABI changed** in `miden-vm` 0.29 to + `QX[8] || QY[8] || SIG_R[8] || SIG_S[8]`, and the recovery byte is no longer + part of it, so stored signatures cannot be replayed into a transaction. +- **Storage slot names moved** from `openzeppelin::*` to `miden::standards::*`, + so stored state cannot be read back by name. +- **The transaction summary layout changed** and now binds a chain anchor, so + stored summaries cannot be recomputed or re-verified. Proposals carry a + serialized `ChainAnchor` (wire field `chain_anchor`) and verification and + execution pin to it. +- **The custody account is now the upstream `miden-standards` + `AuthGuardedMultisig` component** rather than Guardian's local MASM, and + `guardianEnabled` is gone: the guardian is always present. +- **Transaction fees became the auth component's responsibility**, and + `AuthGuardedMultisig` now pays them. Its auth procedure calls + `miden::standards::fee::pay_fee` *before* building the transaction summary, so + the fee note and the vault withdrawal funding it fall inside what the cosigners + sign rather than being appended afterwards. + + That makes the auth arg carry double duty. `fee::load_conversion_info` reads it + as the commitment `hash(CONVERSION_INFO || SALT)` and looks the preimage up in + the advice map; the same word then serves as the transaction summary salt. A + bare salt still satisfies the salt role but not the fee role: the lookup + misses, conversion info comes back empty, and `pay_fee` aborts with + `ERR_FEE_CONVERSION_INFO_MISSING` — though only once the computed fee is + non-zero, so a zero-`verification_base_fee` chain never notices. + + **Every typed `create*Proposal` path in both SDKs therefore commits native + conversion info**, at rate 1/1 under the chain's own fee faucet. This is the + invariant change: a proposal's auth arg is no longer its salt. + + Cross-SDK reconstruction survives because the committed value is *derived*, not + chosen. The faucet is read from the block the proposal is anchored at — the + anchor travels with the proposal and is checked against the summary's block + commitment before use — and the rate is fixed at 1/1. Both SDKs declare the + stored proposal salt on their transaction request builders: + `fee_conversion_salt(salt)` in Rust and `withFeeConversionSalt(salt)` in + TypeScript. The pinned Miden clients then derive and commit the same native + conversion info from the reference header used for execution. + + Two consequences worth knowing: + + - The pinned Miden clients classify `AuthGuardedMultisig` as + `CallerChosenSalt`. A request declares a salt, and the client commits the + chain-native conversion info under that salt. Components that do not read + fee conversion info still fail with + `TransactionRequestError::FeeConversionInfoUnsupported`. + - `pay_fee` spends the faucet and rate the committed conversion info names, so + what a guarded account must hold follows from what it commits. The built-in + typed proposal paths always commit the chain-native asset at rate 1/1, so on + a fee-charging chain an account driving them needs that native asset in its + vault or `pay_fee` aborts before the summary exists — and guardian-assisted + recovery cannot route around it, since it takes the same path. A custom + request that commits a different fee asset must instead fund *that* asset: + holding only it is enough to execute through fee payment, provided the + request needs no other assets. Whether the resulting transaction is then + *included* is a separate question — the batch builder decides what fee + asset and rate it accepts. + + The exported builders always declare the fee conversion salt. A caller + assembling a raw custom request can omit it, but the resulting request works + only on a zero-fee chain. Typed proposal reconstruction always declares the + stored `salt_hex`. + +Data effect: full reset, see above. Operator steps: +[`PRODUCTION.md`](./operations/production.md#upgrading-to-miden-016). + +## Guardian 0.15.x and 0.16.x on Miden 0.15 + +Miden 0.15 invalidated account ID version 0: encoded version `0` is rejected, and +every serialized `AccountDelta` or `TransactionSummary` embedding a v0 ID fails to +deserialize. A v0 ID is a proof-of-work-derived commitment with no v1 equivalent, +so there is no in-place migration. Addresses also moved to bech32m. + +Guardian 0.16.x stayed on Miden 0.15 and required no reset; the changes in that +release were Guardian-side only. + +Data effect: the 0.15 cutover above, on the first 0.15 deploy. + +## Adding a line + +When Guardian adopts a new Miden line: + +1. Add a matrix row with the exact pins. +2. Add a per-line section stating what broke and what it does to stored data. +3. If data cannot be migrated, add the migration to the reset table and write the + operator steps in [`PRODUCTION.md`](./operations/production.md). +4. Leave the procedural and symptom docs pointing here rather than restating the + version facts, so there is one place to update. diff --git a/versioned_docs/version-0.16/builder/miden-guardian/operations/_category_.json b/versioned_docs/version-0.16/builder/miden-guardian/operations/_category_.json new file mode 100644 index 00000000..4e65b67b --- /dev/null +++ b/versioned_docs/version-0.16/builder/miden-guardian/operations/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Operations", + "position": 4 +} diff --git a/versioned_docs/version-0.16/builder/miden-guardian/operations/dashboard.md b/versioned_docs/version-0.16/builder/miden-guardian/operations/dashboard.md new file mode 100644 index 00000000..af42ed26 --- /dev/null +++ b/versioned_docs/version-0.16/builder/miden-guardian/operations/dashboard.md @@ -0,0 +1,335 @@ +--- +title: "Operator Dashboard" +sidebar_position: 2 +--- +The operator dashboard is Guardian's read-and-administer surface for +humans. It lives in the same Guardian server process as the gRPC API but +uses a **separate auth domain** and a **separate set of HTTP routes** under +`/dashboard/*`. This doc explains the trust model, how operators enroll, +how the local smoke example wires it up, and how the permission vocabulary +is structured. + +Companion docs: +- [Service architecture — Dashboard subsystem](../architecture/services.md#dashboard-subsystem) +- [Secrets runbook — Operator public keys](../runbooks/secrets.md#operator-public-keys) + +## What it is + +A small HTTP API + browser UI that lets a known set of operators: +- list and inspect accounts known to this Guardian +- read per-account and global delta / proposal feeds +- read account snapshots and metadata +- (gated by permission) pause/unpause accounts + +It is **not** part of the per-account gRPC contract — clients of Guardian +don't talk to it. Only operators do. + +## Trust model + +Two auth domains coexist in the same server process: + +```mermaid +flowchart LR + subgraph PerAccount["Per-account API (gRPC + HTTP)"] + A1["Account-owned Falcon/ECDSA key signs each request"] + A2["Credentials verified via metadata/auth"] + end + + subgraph Operator["Operator dashboard (/dashboard/*)"] + B1["Operator Falcon key signs a challenge"] + B2["Verified against the allowlist"] + B3["Session cookie issued, used for follow-up calls"] + end + + PerAccount -. shares ACK signer for responses .-> Operator +``` + +Code references: +- Allowlist loader: + [`crates/server/src/dashboard/allowlist.rs`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/server/src/dashboard/allowlist.rs) +- Challenge/session issuance: + [`crates/server/src/dashboard/authz.rs`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/server/src/dashboard/authz.rs) +- Middleware that gates `/dashboard/*`: + [`crates/server/src/dashboard/middleware.rs`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/server/src/dashboard/middleware.rs) +- Permission vocabulary: + [`crates/server/src/dashboard/permissions.rs`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/server/src/dashboard/permissions.rs) + +The dashboard never sees an account's key material and an account's +client never sees an operator's. They are separate by construction. + +## Session flow + +```mermaid +sequenceDiagram + participant O as Operator (browser) + participant S as Guardian server + O->>S: GET /dashboard/auth/challenge
(public key commitment) + S->>S: Check pubkey is in allowlist + S-->>O: Challenge nonce (TTL 5m) + O->>O: Sign challenge with Falcon SK + O->>S: POST /dashboard/auth/session
(signed challenge) + S->>S: Verify signature, mint session + S-->>O: Set-Cookie: guardian_operator_session (TTL 8h) + O->>S: GET /dashboard/accounts (cookie attached) + S->>S: middleware: require_dashboard_session + S-->>O: paginated accounts +``` + +Defaults +([`crates/server/src/dashboard/config.rs`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/server/src/dashboard/config.rs)): +- Challenge TTL: 5 minutes +- Session TTL: 8 hours +- Max outstanding challenges per operator: 8 +- Cookie name: `guardian_operator_session` +- Per-commitment auth budget: 6 burst / 30 per minute, partitioned by + `GUARDIAN_MAX_REPLICAS` in multi-replica deployments + +Session records use the configured coordination backend. Postgres-backed +deployments share sessions across replicas, so ALB stickiness is not required +and task replacement does not invalidate an unexpired session. Filesystem-backed +development uses the in-memory coordination backend and remains single-process. + +For multi-replica deployments where you want cursors to validate across +replicas, set `GUARDIAN_DASHBOARD_CURSOR_SECRET` to a 32-byte hex value +shared by every task ([`config.rs:38`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/server/src/dashboard/config.rs#L38)). + +## Pagination + +All list endpoints +([`services/dashboard_pagination.rs`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/server/src/services/dashboard_pagination.rs)) +share the same query-parameter shape: + +- `limit` — integer in `[1, 500]`. Default `50` when omitted or empty. + Out-of-range or non-integer values return HTTP 400 `invalid_limit`. +- `cursor` — opaque, HMAC-signed token returned by the previous page. + Signed with the cursor secret (see above); tampered, expired, or + wrong-kind cursors return HTTP 400 `invalid_cursor`. Omit to start + from the first page. + +When `GUARDIAN_DASHBOARD_CURSOR_SECRET` is unset, each task generates a +random secret at startup. Cursors then become invalid the moment a +client is routed to a different task — set the env var on any +multi-replica deployment. + +The global-delta feed +([`GET /dashboard/deltas`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/server/src/api/dashboard_feeds.rs)) +also accepts a `status` filter +([`services/dashboard_global_deltas.rs`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/server/src/services/dashboard_global_deltas.rs)): + +- Allowed values: `candidate`, `canonical`, `retained`, `discarded` + (comma-separated to combine, e.g. `?status=candidate,canonical`). +- Omitted or empty → all statuses. +- Duplicates within the filter are silently coalesced. +- Any other token returns HTTP 400 `invalid_status_filter`. + +Present `retained` rows as **"Unresolved / account unlocked"**, never as +failed: the guardian stopped actively verifying and released the account +slot, but the on-chain outcome is still uncertain and background +reconciliation may promote the row to `canonical` until its retention +TTL expires. The triage fields: + +- `status_reason` (feed + detail): why the row left the active candidate + path — `retry_exhausted` / `diverged` on `retained` rows (a `diverged` + row that later reconciles is direct evidence the divergence verdict + was spurious), `client_abandoned` on `discarded` rows. +- `retained_expires_at` (detail): when the recovery net gives up for + good. Retained age is `now − status_timestamp`. +- `base_matches_stored_state` (detail): whether the row still chains + from the stored account state; `false` means it is structurally + obsolete and can only age out. +- The latest reconciliation activity is in the worker logs as stable + `event=reconcile_*` records (see TROUBLESHOOTING.md). + +`GET /dashboard/info` exposes the reconciliation settings +(`retained_ttl_seconds`, `reconcile_interval_seconds`, +`reconcile_page_size`) so operators can tell why retained rows are or +are not being reconsidered — note that individual accounts back off as +their recoverable rows age, so a retained row being probed less often +than the configured interval is expected. + +## Permission vocabulary + +Permissions are server-defined; unknown strings are rejected at allowlist +load time so a typo surfaces explicitly +([`permissions.rs`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/server/src/dashboard/permissions.rs)). + +| Permission | Grants | +|---|---| +| `dashboard:read` | Read access to all `/dashboard/*` read endpoints. | +| `accounts:pause` | Pause/unpause accounts. | +| `policies:write` | Reserved for future policy writes — no endpoint currently requires it. | + +Wire strings are **case-sensitive** and **must not contain whitespace** — +the parser rejects both. + +> **Current scope:** `dashboard:read` gates all read endpoints. +> `accounts:pause` gates the pause/unpause endpoints (see [Account +> pausing](#account-pausing) below). `policies:write` is reserved +> vocabulary — accepted by the allowlist parser but no endpoint requires +> it yet. + +## Account pausing + +Operators holding `accounts:pause` can halt and resume an account's +state-changing operations. While paused, the server rejects calls on +the state-transition, proposal, and EVM mutation paths +(`PushDelta`, `PushDeltaProposal`, `SignDeltaProposal`, and the +matching EVM proposal/session operations) with `409 GUARDIAN_ACCOUNT_PAUSED` +and gRPC `FailedPrecondition` +([`error.rs:97-101`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/server/src/error.rs#L97)). Read endpoints +and `ConfigureAccount` remain available so an account can be +reconfigured while paused. + +| Route | Permission | Body | +|---|---|---| +| `POST /dashboard/accounts/{id}/pause` | `accounts:pause` | `{ "reason": "" }` — required and validated. | +| `POST /dashboard/accounts/{id}/unpause` | `accounts:pause` | `{ "reason": "" }` — optional. | + +Both endpoints are **idempotent**: pausing an already-paused account or +unpausing a not-paused account succeeds without state change. Each +transition is recorded in the audit log with the operator's commitment, +the timestamp, and the supplied reason +([`services/pause_account.rs`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/server/src/services/pause_account.rs), +[`services/unpause_account.rs`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/server/src/services/unpause_account.rs)). + +Pause state lives in the account metadata (`paused_at`, +`paused_reason`) — survives task restarts and follows the account across +multi-stack deploys that share metadata storage. + +When a paused account is touched by a write path (`PushDelta`, +`SignDeltaProposal`, `PushDeltaProposal`, …), the server returns +`GUARDIAN_ACCOUNT_PAUSED` with the original `paused_reason` in the +response body. See +[`TROUBLESHOOTING.md`](./troubleshooting.md#error-code-reference). + +## Allowlist payload + +The operator allowlist is a Secrets Manager entry whose payload is one of: + +**Legacy array form** — every key implicitly gets `dashboard:read`: +```json +["0x", "0x"] +``` + +**Object array form** (recommended) — explicit permission sets: +```json +[ + { + "public_key": "0x", + "permissions": ["dashboard:read", "accounts:pause"] + }, + { + "public_key": "0x", + "permissions": ["dashboard:read"] + } +] +``` + +Mixed arrays of bare strings and objects are accepted; duplicate +`public_key` entries across the file are rejected. + +> **Terraform-managed allowlists are limited to the legacy array form.** +> The `guardian_operator_public_keys` variable is typed `list(string)` +> and Terraform writes `jsonencode(...)` of the list verbatim +> ([`infra/operator_secrets.tf:12`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/infra/operator_secrets.tf#L12)), +> so every entry implicitly gets `dashboard:read` only. To grant +> `accounts:pause` you must use the object form via a file +> (`GUARDIAN_OPERATOR_PUBLIC_KEYS_FILE`) or an externally managed +> secret referenced by `guardian_operator_public_keys_secret_arn`. + +The server resolves the allowlist *source* from one of these env vars +at startup ([`allowlist.rs:70`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/server/src/dashboard/allowlist.rs#L70)); +the contents are re-read per authenticated request, so adding or +removing operators does not require a task restart: + +| Env var | Source | +|---|---| +| `GUARDIAN_OPERATOR_PUBLIC_KEYS_SECRET_ID` | Secrets Manager secret name or ARN (set by Terraform on the ECS task). | +| `GUARDIAN_OPERATOR_PUBLIC_KEYS_FILE` | Local JSON file path — local development only. | + +## Enrolling an operator + +End-to-end procedure for adding operator Alice to a deployed Guardian. + +1. **Alice generates a Falcon keypair** on a trusted device (the same + keypair format the multisig SDK and the smoke example use; the + `examples/operator-smoke-web` README has a UI for this). +2. Alice gives the public key (hex `0x…`) to the deploying operator. +3. **Deployer updates the allowlist**: + - Terraform-managed: append the bare key to + `guardian_operator_public_keys` and redeploy (see [Secrets + runbook](../runbooks/secrets.md#adding-or-removing-an-operator)). + This path grants `dashboard:read` only. + - Externally-managed: `aws secretsmanager update-secret` with the new + payload — no ECS restart required. +4. **Alice logs in** — challenge → sign → session. The change takes + effect on her next request; the server refreshes the allowlist on + every challenge issuance and every authenticated `/dashboard/*` call + ([`dashboard/state.rs:103-108`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/server/src/dashboard/state.rs#L103), + [`dashboard/state.rs:284-324`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/server/src/dashboard/state.rs#L284)). + +### Removing or revoking an operator + +Same shape, no restart: +1. Update the secret payload to drop or change Alice's entry. +2. Effect is immediate — the next challenge or authenticated request + from any task reloads the allowlist and rejects the removed key. + Currently-active sessions for the revoked operator are rejected at + their next authenticated call (the per-request reload catches them). + +## Local development + +To run the real operator UI ([`0xMiden/guardian-dashboard`](https://github.com/0xMiden/guardian-dashboard)) +against a local server with Docker Compose, follow +[`guides/miden-dashboard`](../guides/miden-dashboard.md). + +For a lightweight check of the API itself, use +[`examples/operator-smoke-web`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/examples/operator-smoke-web) — it +runs a browser harness that exercises challenge issuance, signed-session +issuance, and the account listing endpoints against either a local server +or a remote Guardian. + +Run a local Guardian with a file-based allowlist: +```bash +cat > /tmp/operators.json <<'EOF' +[{ "public_key": "0x", + "permissions": ["dashboard:read", "accounts:pause"] }] +EOF + +GUARDIAN_NETWORK_TYPE=MidenLocal \ +GUARDIAN_OPERATOR_PUBLIC_KEYS_FILE=/tmp/operators.json \ +GUARDIAN_STORAGE_PATH=.guardian/storage \ +GUARDIAN_METADATA_PATH=.guardian/metadata \ + cargo run --bin server +``` + +Then in another shell, follow the +[`examples/operator-smoke-web`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/examples/operator-smoke-web) README to +point the harness at `http://localhost:3000`. The +`smoke-test-operator-dashboard` skill drives this end-to-end. + +## Storage-mode caveats + +The dashboard surfaces a few aggregates (account counts, global feeds) +that are cheap on Postgres but expensive on the filesystem backend. The +server has a defensive cap: above +`DEFAULT_FILESYSTEM_AGGREGATE_THRESHOLD` (1,000 accounts by default, +[`config.rs:16`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/server/src/dashboard/config.rs#L16)), +cross-account aggregates on filesystem deployments may return a degraded +marker rather than a count. This is intentional — filesystem mode is a +dev convenience, not a production backend. See +[Storage modes](../architecture/services.md#storage-modes). + +## Operations checklist + +When standing up the dashboard for a new stack: + +- [ ] Decide Terraform-managed or externally-managed allowlist. +- [ ] Add at least one operator with `dashboard:read` before shipping — + otherwise the dashboard is unreachable. +- [ ] Set `GUARDIAN_NETWORK_TYPE` for the stack; the dashboard + environment reported by `GET /dashboard/info` is derived from it. +- [ ] If running ≥2 ECS tasks, pin + `GUARDIAN_DASHBOARD_CURSOR_SECRET` to a shared 32-byte hex value. +- [ ] Verify a fresh challenge → session round trip from the smoke + example before considering the deploy live. diff --git a/versioned_docs/version-0.16/builder/miden-guardian/operations/production.md b/versioned_docs/version-0.16/builder/miden-guardian/operations/production.md new file mode 100644 index 00000000..5ea20e72 --- /dev/null +++ b/versioned_docs/version-0.16/builder/miden-guardian/operations/production.md @@ -0,0 +1,305 @@ +--- +title: "Production Guide" +sidebar_position: 1 +--- +This is the production entry point for Guardian operators. It summarizes the +supported production shape and points to the detailed deploy, architecture, +configuration, and runbook docs. + +## Supported shape + +The reference production deployment is AWS ECS/Fargate running the Guardian +server with the Postgres backend, RDS for durable state, and AWS Secrets +Manager for deployment secrets. + +Production deployments should use: + +- `DEPLOY_STAGE=prod` for the Terraform stage profile. +- `GUARDIAN_SERVER_FEATURES=postgres` for Miden-only deployments. +- `GUARDIAN_SERVER_FEATURES=postgres,evm` when EVM proposal support is + required. +- Amazon RDS for state, deltas, proposals, account metadata, and audit rows. +- AWS Secrets Manager for ACK signing keys and deploy-time secrets. +- Explicit `GUARDIAN_CORS_ALLOWED_ORIGINS` for browser clients. + +### ECDSA ACK signer: Secrets Manager or KMS + +The Falcon and ECDSA ACK keys default to AWS Secrets Manager, which is the +path existing deployments use and remains fully supported. For the ECDSA signer +specifically, new production deployments should prefer **AWS KMS**: the private +key is generated in and never leaves KMS, so it is never resident in the +Guardian process. Set `guardian_ack_ecdsa_kms_key_arn` and the server uses the +KMS backend instead of the Secrets Manager secret (Falcon is unaffected). + +This is opt-in, not the default, because the KMS key is a distinct keypair: +switching an existing deployment changes Guardian's ECDSA identity and requires +the `SwitchGuardian` migration for existing accounts. Create the key and read +the trade-offs in [`runbooks/secrets.md`](../runbooks/secrets.md#hosted-ecdsa-backend-aws-kms). + +Filesystem mode is a local development backend only. It has no durable admin +audit table, no schema migrations, and cannot safely back multiple ECS tasks. + +### Running behind your own ingress (non-AWS) + +The rate limiter keys clients by IP, and that identity comes from the +ingress in front of the server. The reference ALB deployment handles +this automatically; if you run Guardian behind your own proxy or load +balancer instead, these must hold: + +- Your ingress must **append or overwrite** `X-Forwarded-For` on **both** + listeners: the HTTP port and the gRPC port. Guardian keys on the + rightmost `X-Forwarded-For` entry, the one your ingress appended. Note + that proxies often need separate configuration for gRPC (for example + nginx `grpc_pass` does not add forwarding headers unless + `grpc_set_header` is configured explicitly). +- If your ingress identifies callers with `X-Real-IP` instead, it must + **strip** any client-supplied `X-Forwarded-For`. `X-Forwarded-For` is + read first, so a caller that sends one wins over the `X-Real-IP` your + proxy set and picks its own rate-limit identity. +- Only the ingress may reach the server ports (3000/50051). Forwarding + headers are trusted whenever present, so a client that can connect + directly can choose its own rate-limit identity. +- If the ingress does not forward the client address at all (an + unconfigured proxy, Kubernetes `externalTrafficPolicy: Cluster` SNAT, + an L4 balancer without client-IP preservation), every client collapses + into one shared budget keyed on the proxy's address: one noisy client + then throttles everyone, and the sustained limit caps the whole + deployment's throughput. + +To check which case you are in, restart with +`RUST_LOG=info,server::middleware::rate_limit=debug` (the per-rejection +lines are `debug`, since refusals are expected traffic), trip the limiter +and read the `Request rate limited` lines: `client_ip` must show real +client addresses: not your proxy's address, not `unknown`, and not a +value you forged in a test request. Two probes settle it: exhaust the +budget from one machine and confirm a second machine on a different +address still succeeds, then retry from the exhausted machine with a +forged `X-Forwarded-For` prefix and confirm it stays throttled. + +## Production checklist + +Before treating a deployment as production-ready: + +- Set `DEPLOY_STAGE=prod`. +- Build with `postgres`, plus `evm` if the EVM API must be served. +- Bootstrap ACK secrets once with + `DEPLOY_STAGE=prod ./scripts/aws-deploy.sh bootstrap-ack-keys`. +- For the ECDSA signer, decide between Secrets Manager (default) and KMS + (preferred for new deployments); if using KMS, create the key and set + `guardian_ack_ecdsa_kms_key_arn` per + [`runbooks/secrets.md`](../runbooks/secrets.md#hosted-ecdsa-backend-aws-kms). +- Confirm `DATABASE_URL` is supplied through the Terraform-managed RDS secret. +- Optionally enable storage encryption at rest: run + `./scripts/aws-deploy.sh bootstrap-storage-encryption-key`, then deploy with + `GUARDIAN_STORAGE_ENCRYPTION_SECRET_NAME` set, against an empty store (the Miden + 0.16 reset is the natural window). See "Storage encryption" below. +- Review the RDS durability settings for the stack. The prod stage defaults + to 7-day backup retention (point-in-time recovery), deletion protection + on, and a final snapshot on destroy; Multi-AZ is opt-in via + `rds_multi_az`. See "Durability and recovery" below. +- Set `GUARDIAN_CORS_ALLOWED_ORIGINS` to the exact browser origins that need + access. +- If the operator dashboard is enabled, configure the operator allowlist + secret and use object entries when permissions beyond `dashboard:read` are + needed. +- Before the first managed prod deploy, run + `./scripts/aws-deploy.sh bootstrap-dashboard-cursor-secret`; Terraform then injects the same + `GUARDIAN_DASHBOARD_CURSOR_SECRET` into every ECS task. +- Validate `/`, `/pubkey`, and the relevant SDK or dashboard smoke path after + deploy. +- Size `GUARDIAN_RATE_PER_MIN` for the combined HTTP **and** gRPC volume: + the sustained limit is keyed per IP alone, so gRPC traffic (the Rust + SDK's and benchmark harness's default transport) draws on the same + allowance as HTTP. Budgets sized for HTTP-only traffic under-provision + after the transport-bypass fix. `GUARDIAN_RATE_BURST_PER_SEC` is keyed + per IP and endpoint, so it applies to each HTTP path and gRPC method + separately and can be sized per-endpoint rather than in aggregate. +- On the first deploy that ships gRPC rate limiting, verify keying on the + deployed gRPC path: against staging with a deliberately low + `guardian_rate_burst_per_sec`, exhaust the budget from one machine, + confirm a second machine on a different address is unaffected, then + confirm a forged `X-Forwarded-For` prefix from the exhausted machine + stays throttled. Owned by whoever runs the deploy. +- On the AWS reference deployment, metrics are on by default: the endpoint + binds loopback inside the ECS task and an ADOT sidecar exports selected + metrics to CloudWatch dashboards and alarms — no external exposure, no + bearer token needed. See + [`SERVER_AWS_DEPLOY.md`](./server-aws-deploy.md#metrics-dashboard-and-alarms). +- If you scrape Prometheus yourself in a **self-managed deployment**, set + `GUARDIAN_METRICS_ENABLED=true`, bind an explicitly routable + `GUARDIAN_METRICS_ADDR` only if the scraper lives outside the host or task, + keep the port reachable only from the scraper's network, and set + `GUARDIAN_METRICS_BEARER_TOKEN`. Note the AWS reference Terraform hard-binds + the listener to `127.0.0.1` and exposes no bind-address, security-group, or + token knobs — `cloudwatch_metrics_enabled = false` there leaves a + loopback-only endpoint that only an in-task collector (added by customizing + the module) can reach, not an external scraper. See the + [Observability guide](../guides/observability.md) for scraping and a + Grafana dashboard stack, and + [`CONFIGURATION.md`](../reference/configuration.md#runtime--metrics-prometheus) for + the env vars. + +## Durability and recovery + +On the reference stack, all Guardian state — account state, deltas, +proposals, account metadata, and audit rows — lives in RDS Postgres. The +Guardian server itself is stateless, so crash resistance reduces to the +database's guarantees: + +- **Write durability.** Postgres WAL: a write acknowledged by the database + survives a crash of the instance. Guardian acknowledges a delta to the + client only after the database write succeeds. +- **Point-in-time recovery.** RDS automated backups (daily snapshot plus + continuous WAL archiving) allow restore to any point within + `rds_backup_retention_days` (default 7). WAL is shipped roughly every + five minutes, which bounds worst-case data loss for a total instance + loss to about that window. +- **Operator-error protection.** In the prod stage, deletion protection is + on and destroying the stack takes a final snapshot + (`-postgres-final`) by default. +- **Standby failover.** Multi-AZ is **off by default** in every stage. Set + `rds_multi_az = true` if the deployment needs automatic failover to a + standby replica; this is an availability trade-off (roughly double the + instance cost), not a backup mechanism. + +What is deliberately **not** provided: cross-region replicas, automated +disaster-recovery drills, or backup-failure alarms (see +[`architecture/infra.md`](../architecture/infra.md#things-that-are-deliberately-not-here)). + +Two Guardian-specific caveats when restoring from a backup: + +- Restoring to an earlier point rewinds stored account state. Any delta + that canonicalized on-chain after the restore point cannot be + regenerated by the guardian — guarded accounts are private on Miden, so + only client devices hold the full state. Affected accounts fail + state-commitment verification until a device holding the newer state + re-syncs it (see the failure table in + [`CONCEPTS.md`](../getting-started/concepts.md#failure-and-recovery)). +- If storage encryption is enabled, database backups contain ciphertext. + The Secrets Manager encryption key is part of the recovery set: losing + it makes every restored payload unrecoverable. Keep an out-of-band copy. + +The verification and restore procedure is in +[`runbooks/backup-restore.md`](../runbooks/backup-restore.md). + +## Storage encryption + +Guardian can encrypt the sensitive stored payloads (account state, delta and +proposal payloads) at rest, above whatever disk-level encryption the database +already provides. It is opt-in: set a key source and the server encrypts; set +none and behavior is unchanged. + +- Confidentiality boundary: only the payload JSON is encrypted — the account + `state_json`, delta `delta_payload`, and proposal `delta_payload`. Routing and + index columns (`account_id`, `nonce`, `status`, proposal `commitment`) stay + **plaintext** by design: they are needed for lookups and some are bound as AEAD + additional authenticated data (authenticated, not hidden). "Encrypted at rest" + here means payload confidentiality, not metadata confidentiality — anyone with + database read access still sees which accounts exist, their nonce/commitment + lineage, and proposal status. Use disk/database-level encryption if the index + metadata itself is sensitive in your threat model. + +- Production key source: AWS Secrets Manager, holding + `{ "active": "k1", "keys": { "k1": "" } }`. On the standard + stack, `./scripts/aws-deploy.sh bootstrap-storage-encryption-key` creates it and + a deploy with `GUARDIAN_STORAGE_ENCRYPTION_SECRET_NAME` set wires + `GUARDIAN_STORAGE_ENCRYPTION_KEY_SECRET_ID` plus the task-role + `secretsmanager:GetSecretValue` grant (same pattern as the ACK keys). `AWS_REGION` + is reused. +- Enable against an **empty** store. The server writes a one-time marker on the + first encrypted write and then refuses to mix plaintext and ciphertext, so it + fails fast if a key is configured against a store that already holds plaintext + records. For Miden-only deployments the Miden 0.16 reset (which purges Miden + account data) is the natural enablement window; deployments that also retain EVM + account rows — which the reset preserves — must clear or migrate those + plaintext records before enabling encryption, or the first encrypted write will + fail fast against the mixed store. +- Startup is fail-fast: a missing/malformed/wrong-length key, or more than one + key source, prevents startup rather than degrading to plaintext. +- Key rotation: add a new entry to `keys` and move `active`; keep the old key so + existing records still decrypt. Bulk re-encryption tooling is not yet provided. + +Full configuration and a dev walkthrough are in +[`CONFIGURATION.md`](../reference/configuration.md#storage-encryption-at-rest). + +## Upgrading to Miden 0.16 + +> **One-time, irreversible: the first 0.16 deploy wipes all pre-0.16 Miden +> account data. EVM accounts are unaffected.** Stored 0.15 Miden states, deltas, +> proposals, and metadata can no longer be deserialized or recomputed, and cannot +> be migrated. For what changed on the Miden side and why none of it survives, see +> [`MIDEN_COMPATIBILITY.md`](../miden-compatibility.md#guardian-017x-on-miden-016). +> Guardian 0.17.x is the release that adopts Miden 0.16; Guardian 0.16.x runs on +> Miden 0.15. + +What happens on the first 0.16 startup (Postgres backend): + +- The embedded reset migration + `2026-08-24-000001_miden_016_irreversible_reset` runs automatically via + `run_pending_migrations` and deletes Miden-network rows from + `delta_proposals`, `deltas`, `states`, and `account_metadata`. Rows whose + `account_metadata.network_config->>'kind'` is `evm` are preserved across all + four tables. Every other row is purged, including Miden rows and any row + orphaned from its metadata. +- `account_auth_state` is cleared by cascade from `account_metadata`, so + per-signer replay floors for deleted accounts go with them. +- The migration takes a brief `ACCESS EXCLUSIVE` lock on `account_metadata` so a + replica still running the old binary cannot write Miden rows mid-reset. It + fails fast (5s `lock_timeout`) rather than stalling startup; if that happens, + confirm the old server is stopped and restart. +- **Preserved:** `admin_actions` (append-only audit), `auth_sessions`, + `auth_challenges`, `storage_encryption_marker`, `worker_leases`, and the + Guardian ACK/operator keystore. Guardian identity and operational audit data + survive the reset. +- The migration is irreversible: its `down.sql` is a no-op. There is no + partial-salvage path and no legacy-account filtering: every incompatible + surface is removed at once rather than left for per-instance repair. +- A deployment upgrading from before 0.15 also runs the older + `2026-06-14-000001_v015_account_id_cutover` in the same startup. Its effect is + subsumed by this reset (both purge Miden rows and preserve EVM rows), so the + steps below are the only ones that apply. + +Operator actions, in order: + +1. **Back up the database.** This is the only way to retain pre-0.16 records, + and the reset runs automatically on startup. Do this even if you believe the + data is disposable: for private accounts, Guardian may hold state the client + no longer has, and step 4 clears the client copy too. +2. **Stop the old server** before deploying. The lock above is a backstop, not a + substitute. +3. **Deploy against a matching Miden 0.16 node.** A 0.16 server against a 0.15 + node fails on protocol mismatch, not on anything this reset controls. +4. **Clear client-side state**: SDK SQLite stores, browser IndexedDB, and local + metadata under `~/.guardian`. Stale local metadata outlives the server reset + and will not deserialize. +5. **Recreate and re-register accounts.** There is no in-place account + migration; users re-establish custody accounts and register them on the + Guardian. EVM accounts continue operating unchanged. +6. **Discard all pending and offline proposals.** Exported proposal files and + unexecuted pending proposals from 0.15 are bound to the old summary layout + and procedure roots, so they can never execute. + +Filesystem-backed deployments have no migration step: start from empty storage +and metadata directories, and preserve the keystore directory. + +## Where details live + +| Need | Read | +|---|---| +| Match a Guardian release to a Miden version | [`MIDEN_COMPATIBILITY.md`](../miden-compatibility.md) | +| Step-by-step setup for a specific run mode | [`guides/`](../guides/overview.md) | +| Deploy or update the AWS stack | [`SERVER_AWS_DEPLOY.md`](./server-aws-deploy.md) | +| Understand the AWS topology and Terraform ownership | [`architecture/infra.md`](../architecture/infra.md) | +| Understand server storage modes and why prod uses Postgres | [`architecture/services.md`](../architecture/services.md#storage-modes) | +| Check runtime and deploy-time env vars | [`CONFIGURATION.md`](../reference/configuration.md) | +| Bootstrap, replace, or respond to ACK/operator/EVM secret issues | [`runbooks/secrets.md`](../runbooks/secrets.md) | +| Verify database backups or restore from one | [`runbooks/backup-restore.md`](../runbooks/backup-restore.md) | +| Migrate a deployed stack to verified database TLS | [`runbooks/enable-db-tls.md`](../runbooks/enable-db-tls.md) | +| Configure dashboard operators and permissions | [`DASHBOARD.md`](./dashboard.md) | +| Scrape Prometheus metrics and visualize them | [`guides/observability/`](../guides/observability.md) | +| Diagnose deploy/runtime failures | [`TROUBLESHOOTING.md`](./troubleshooting.md) | + +## Non-goals + +This page does not replace the AWS deploy guide or the runbooks. Keep +procedural steps in those docs so deployment behavior has one source of truth. diff --git a/versioned_docs/version-0.16/builder/miden-guardian/operations/server-aws-deploy.md b/versioned_docs/version-0.16/builder/miden-guardian/operations/server-aws-deploy.md new file mode 100644 index 00000000..8a95902a --- /dev/null +++ b/versioned_docs/version-0.16/builder/miden-guardian/operations/server-aws-deploy.md @@ -0,0 +1,841 @@ +--- +title: "Deploying GUARDIAN Server to AWS ECS" +sidebar_position: 3 +--- +This guide covers the current AWS deployment for Guardian. The AWS stack now uses Amazon RDS for PostgreSQL and no longer supports the legacy ECS-hosted Postgres runtime. + +The deployment surface supports two stage profiles: +- `DEPLOY_STAGE=dev` keeps the current low-cost, fixed-capacity behavior +- `DEPLOY_STAGE=prod` enables ECS autoscaling, RDS storage autoscaling, RDS Proxy, larger default RDS sizing, RDS deletion protection with a final snapshot on destroy, and benchmark-oriented runtime defaults + +## Published Docker images + +Prebuilt, versioned server images are published to the GitHub Container Registry +(GHCR) at `ghcr.io/openzeppelin/guardian`, so you can pull a known-good image +instead of building from source: + +```bash +docker pull ghcr.io/openzeppelin/guardian: # e.g. v1.2.3, or latest +``` + +Images are multi-architecture (`linux/amd64` + `linux/arm64`) and fully +runtime-configurable — every setting and secret is supplied at run time, never +baked in (see [`docs/CONFIGURATION.md`](../reference/configuration.md)): + +```bash +docker run --rm -p 3000:3000 -p 50051:50051 \ + --env-file ./guardian.env \ + ghcr.io/openzeppelin/guardian: +``` + +To run the published image with a Postgres backend locally, use the registry +compose file (no local build): + +```bash +cp .env.registry.example .env.registry # then set POSTGRES_PASSWORD in .env.registry +GUARDIAN_VERSION= docker compose --env-file .env.registry -f docker-compose.registry.yml up +``` + +The stack is driven entirely by the gitignored `.env.registry` (see +`.env.registry.example`): Compose reads `POSTGRES_PASSWORD` / `GUARDIAN_VERSION` from +it for interpolation (via `--env-file`), and the server container loads it for +runtime config. The shared repo `.env` (AWS/deploy config) is intentionally not +used here, so this example never mutates it. The repo's default +`docker-compose.yml` (and the `docker-compose.postgres.yml` override) instead +build the server from source for contributors; `docker-compose.registry.yml` pulls +the published image. + +Maintainers publish a version by running the **Docker Publish** GitHub Actions +workflow one of two ways: + +- **On a GitHub Release.** Publishing a release auto-triggers the workflow: the + version and build ref come from the release tag, the build uses the `postgres` + feature, and an existing tag is never overwritten. The build first **waits for + required-reviewer approval** on the `release` environment before it pushes — + approve releases that should ship a server image, and decline ones that should + not (e.g. SDK-only releases that share the same `vX.Y.Z` tag line). A release + tag that does not match `vMAJOR.MINOR.PATCH[-prerelease]` fails the workflow. + Cutting the release as a **draft** (`gh release create … --draft`) does not + trigger the workflow — review it first, then publish the draft to start the build. +- **Manual dispatch.** Pick the branch/tag/commit to build from, the version to + tag, the build features, and whether to overwrite an existing tag — for + off-release or one-off builds. + +In both cases a version containing `-` (e.g. `v1.2.3-rc.1`) is treated as a +pre-release and does not move the `latest` tag. + +Published images are what the **AWS Deploy** workflow rolls out (next section). +`scripts/aws-deploy.sh` still builds and pushes its own image for infrastructure +changes and first-time stack setup. + +## Deploying a published image from GitHub Actions + +The **AWS Deploy** workflow (`.github/workflows/aws-deploy.yml`) deploys a +published GHCR version to a Guardian stack without local AWS credentials or +Terraform state. GitHub environments are named after the Miden network they +serve and map onto AWS stacks through environment variables, so stack names and +infrastructure profiles stay explicit settings rather than being inferred from +the environment name: + +| Environment | Network | Stack (`STACK_NAME`) | Profile | Hostname | +|---|---|---|---|---| +| `devnet` | MidenDevnet | `guardian` | dev | `guardian-stg.openzeppelin.com` | +| `testnet` | MidenTestnet | `guardian-prod` | prod | `guardian.openzeppelin.com` | + +Run it from the Actions tab with: + +- `environment`: `devnet` or `testnet` +- `version`: a published tag such as `v1.2.3`. Only `devnet` accepts + pre-releases like `v1.2.3-rc.1`; every other environment is release-only. + +What a run does: + +1. Resolves the version tag to an immutable digest and verifies that digest + against the SLSA provenance attestation signed by the Docker Publish + workflow. For release-only environments the attestation must also show the + build ran from the `refs/tags/` release tag, so a manually + dispatched build of another branch cannot ship under a release version. This + happens before the environment's protection rules, so reviewers are only + asked to approve an already-verified digest. +2. Authenticates to AWS with GitHub OIDC (bootstrap role, then role chaining to + the deploy role) and checks the stack's ECR repository and ECS service exist. +3. Mirrors the verified digest into `-server` in ECR, tagged both + `` and `latest`. Moving `latest` keeps + `scripts/aws-deploy.sh plan` / `deploy --skip-build` resolving the deployed + image, so a later Terraform apply does not roll the service back. +4. Registers a new revision of the task definition the service is currently + running, with only the image changed, and waits up to 20 minutes for the + rollout to stabilize. + +The run summary records the deployed image URI and the previous task-definition +ARN. There is no automatic rollback: if a rollout fails, the summary prints the +`aws ecs update-service` command to restore the previous revision, or re-run the +workflow with the previously deployed version. + +The workflow only rolls out an image. When a release also changes the task +definition (new environment variables, secrets, or IAM grants in `infra/`), +apply that release's Terraform first with `scripts/aws-deploy.sh`, then deploy +the image. Stacks that override the default `-server` / `-cluster` +resource names in Terraform (or `ECR_REPO_NAME` in the script) are not +supported by the workflow. The `guardian-evm` stack is also out of scope: GHCR +release images are built with the `postgres` feature only. + +One-time setup per target (infra): + +- A GitHub environment named after the network (`devnet` / `testnet`) with + variables `AWS_REGION`, `ROLE_FOR_OIDC` (role trusted for GitHub OIDC), + `ROLE_TO_ASSUME` (deploy role reached via role chaining), and `STACK_NAME` + (`guardian` / `guardian-prod`). Add required reviewers on `testnet`. +- Each environment must restrict **deployment branches** to `main`. Without + that, anyone able to dispatch the workflow could run an edited copy of it + from a feature branch and obtain the environment's AWS OIDC identity. +- The OIDC role's trust policy must accept this repository's environment + subject claims (`repo:OpenZeppelin/guardian:environment:devnet` / `:testnet`). +- The deploy role needs ECR push/pull on `-server` + (`ecr:GetAuthorizationToken`, `ecr:DescribeRepositories`, + `ecr:BatchCheckLayerAvailability`, `ecr:BatchGetImage`, + `ecr:InitiateLayerUpload`, `ecr:UploadLayerPart`, `ecr:CompleteLayerUpload`, + `ecr:PutImage`), `ecs:DescribeServices`, `ecs:DescribeTaskDefinition`, + `ecs:RegisterTaskDefinition`, `ecs:UpdateService`, and `iam:PassRole` on the + stack's task and task-execution roles. +- The ECR repository must already exist; `scripts/aws-deploy.sh build` creates + it on a new stack. + +## Prerequisites + +- [Terraform](https://developer.hashicorp.com/terraform/downloads) >= 1.0 +- AWS CLI configured with permissions for ECS, ECR, ELB, EC2, IAM, CloudWatch, RDS, and Secrets Manager +- Docker installed locally +- `jq` installed locally when deploying with `GUARDIAN_SERVER_FEATURES=postgres,evm` + +```bash +aws sts get-caller-identity +docker info +terraform version +``` + +## Quick Start + +```bash +aws sso login --profile + +set -a && source .env && set +a + +# Optional: build/deploy ARM64 instead of X86_64 +# export CPU_ARCHITECTURE=ARM64 + +# Miden network the server runs against. The server requires this at startup; +# the deploy script passes MidenTestnet unless you override it here. +export GUARDIAN_NETWORK_TYPE=MidenTestnet + +# Optional: allow dashboard operators and let Terraform create the secret +# export GUARDIAN_OPERATOR_PUBLIC_KEYS_JSON='["0x","0x"]' + +# Optional: use an existing dashboard operator public keys secret instead +# export GUARDIAN_OPERATOR_PUBLIC_KEYS_SECRET_ARN='arn:aws:secretsmanager:us-east-1:123456789012:secret:guardian/operators' + +# Optional: enable EVM support from config/evm/chains.json +# export GUARDIAN_SERVER_FEATURES=postgres,evm +# export GUARDIAN_EVM_CHAIN_CONFIG_FILE=config/evm/chains.json +# export GUARDIAN_CORS_ALLOWED_ORIGINS=https://accounts.openzeppelin.com + +# Optional: choose the deployment profile +export DEPLOY_STAGE=dev +# export DEPLOY_STAGE=prod + +# Optional: override the stack base name or public hostname +export STACK_NAME=guardian +# export SUBDOMAIN=guardian-stg + +aws sts get-caller-identity +./scripts/aws-deploy.sh deploy +./scripts/aws-deploy.sh status +``` + +For a reviewable deployment, split image publishing, planning, and applying: + +```bash +./scripts/aws-deploy.sh build +./scripts/aws-deploy.sh plan +./scripts/aws-deploy.sh deploy --skip-build +./scripts/aws-deploy.sh status +``` + +This builds and pushes `${ECR_REPO_NAME}:latest`, plans Terraform against the immutable digest currently behind that tag, then applies using the existing ECR image without rebuilding. If you push a new image after `plan`, rerun `plan` before `deploy --skip-build`. + +## Terraform Variables + +If you need to override defaults, use `infra/terraform.tfvars`: + +```hcl +aws_region = "us-east-1" + +# Optional: ECS/image architecture +# cpu_architecture = "X86_64" +# cpu_architecture = "ARM64" + +# Optional: derive resource names from a base stack name +# stack_name = "guardian" + +# Only set this when bypassing scripts/aws-deploy.sh. The deploy script resolves +# ECR latest to an immutable digest and passes server_image_uri via -var. +# server_image_uri = "123456789012.dkr.ecr.us-east-1.amazonaws.com/guardian-server@sha256:" + +# Optional: Postgres credentials (defaults derive from stack_name) +# postgres_db = "guardian" +# postgres_user = "guardian" +# postgres_password = "guardian_dev_password" + +# Optional: managed database sizing overrides +# Stage defaults: +# - dev -> db.t3.micro, 20 GiB allocated, no storage autoscaling ceiling +# - prod -> db.t3.medium, 50 GiB allocated, 200 GiB max allocated +# rds_instance_class = "db.t3.medium" +# rds_allocated_storage = 50 +# rds_max_allocated_storage = 200 + +# Optional: Miden network for the server runtime +# server_network_type = "MidenDevnet" + +# Optional: dashboard operator Falcon public keys managed by Terraform +# guardian_operator_public_keys = [ +# "0x", +# "0x", +# ] + +# Optional: existing dashboard operator Falcon public keys secret +# guardian_operator_public_keys_secret_arn = "arn:aws:secretsmanager:us-east-1:123456789012:secret:guardian/operators" + +# Optional: hosted ECDSA ACK signer backed by AWS KMS. +# Setting this is all that is required: Terraform grants the ECS task role +# kms:Sign + kms:GetPublicKey on the key and injects GUARDIAN_ACK_ECDSA_BACKEND +# and GUARDIAN_ACK_ECDSA_KMS_KEY_ID into the server runtime. The key must be +# ECC_SECG_P256K1 / SIGN_VERIFY. On this path the ECDSA Secrets Manager secret is +# not needed; the Falcon ACK secret bootstrap is unchanged and still required in +# prod. +# guardian_ack_ecdsa_kms_key_arn = "arn:aws:kms:us-east-1:123456789012:key/" + +# Optional: EVM runtime configuration +# guardian_evm_allowed_chain_ids = "1,11155111" +# guardian_evm_rpc_urls = "1=https://ethereum-rpc.publicnode.com,11155111=https://ethereum-sepolia-rpc.publicnode.com" +# guardian_evm_entrypoint_address = "0x433709009b8330fda32311df1c2afa402ed8d009" +# guardian_cors_allowed_origins = "https://accounts.openzeppelin.com" + +# Optional: stage/runtime capacity overrides +# deployment_stage = "prod" +# server_desired_count = 2 +# server_autoscaling_enabled = true +# server_autoscaling_min_capacity = 2 +# server_autoscaling_max_capacity = 6 +# server_autoscaling_cpu_target = 65 +# server_autoscaling_memory_target = 75 +# rds_proxy_enabled = true +# rds_proxy_subnet_ids = ["subnet-xxxxxxxx", "subnet-yyyyyyyy"] +# In us-east-1, avoid subnets in us-east-1e/use1-az3 for RDS Proxy. +# guardian_rate_limit_enabled = false +# guardian_rate_burst_per_sec = 200 +# guardian_rate_per_min = 5000 +# guardian_db_pool_max_size = 32 +# guardian_metadata_db_pool_max_size = 32 + +# Optional: application metrics (ADOT sidecar + CloudWatch dashboard/alarms). +# Enabled by default; see "Metrics, Dashboard, And Alarms". +# guardian_metrics_enabled = true # server Prometheus endpoint (loopback-only) +# cloudwatch_metrics_enabled = true # ADOT sidecar + dashboard + alarms +# metrics_namespace = "Guardian/Server" +# alarm_actions = ["arn:aws:sns:us-east-1:123456789012:guardian-alerts"] +# alarm_error_rate_threshold_percent = 5 +# alarm_latency_threshold_seconds = 1 +# alarm_cpu_threshold_percent = 85 +# alarm_memory_threshold_percent = 90 + +# Optional: Route 53 hosted zone ID +# route53_zone_id = "Z1234567890ABC" + +# Optional: Cloudflare DNS management +# cloudflare_zone_id = "..." +# cloudflare_api_token = "..." +``` + +## Database TLS verification + +By default the server `DATABASE_URL` uses `sslmode=require` — the connection is +encrypted but the RDS certificate is **not verified**. To authenticate the +database, provide a CA bundle via Secrets Manager and set the +`rds_ca_bundle_secret_arn` Terraform variable. + +> **Migrating an already-deployed stack?** Follow the staged, fail-safe procedure +> in [`runbooks/enable-db-tls.md`](../runbooks/enable-db-tls.md) (image-first, +> staging-before-prod, RDS-Proxy caveat, rollback). The rest of this section is +> the mechanism reference. The deployment then switches +`DATABASE_URL` to `sslmode=verify-full&sslrootcert=`, and both the +migration (libpq) and runtime (rustls) connections verify the certificate chain +and hostname. + +**How delivery works (image stays CA-free).** The published image ships no CA +bundle. When `rds_ca_bundle_secret_arn` is set, Terraform adds a small +`rds-ca-initializer` **init container** to the task: it reads the secret, writes +it to a shared in-task volume, sets permissions, and exits; Fargate won't start +the Guardian container until it succeeds (`dependsOn: SUCCESS`). The Guardian +container mounts the same volume read-only and reads the bundle as a plain file. +The app never calls Secrets Manager and nothing is baked into the image. + +**Combined CA bundle (required for RDS).** Production routes `DATABASE_URL` +through the **RDS Proxy** endpoint, which presents an AWS Certificate Manager +certificate that chains to **Amazon Trust Services** roots (specifically **Amazon +Root CA 1**) — *not* the Amazon RDS CA roots used by a direct instance. The secret +MUST therefore contain **both** root sets so `verify-full` succeeds against either +endpoint. + +> **Size limit — do NOT use the global RDS bundle.** Secrets Manager caps a secret +> value at **64 KiB**. The `global-bundle.pem` (~165 KB) exceeds that and +> `create-secret` will reject it. Use your **region-specific** RDS bundle (a few +> KB) plus just **Amazon Root CA 1**, which keeps the combined bundle well under +> the cap. The Rust loader already supports multiple roots in one PEM. + +Build it (mind the newline between files so the PEM blocks don't merge) and store +it verbatim — plain PEM text, no encoding: + +```bash +# Region-specific RDS bundle (replace us-east-1), ~4-5 KB +curl -sS https://truststore.pki.rds.amazonaws.com/us-east-1/us-east-1-bundle.pem -o rds.pem +# All Amazon Trust Services roots (~6 KB total) — more rotation-tolerant than +# pinning only the one the RDS Proxy chains to today; still well under 64 KiB. +: > ats.pem +for ca in AmazonRootCA1 AmazonRootCA2 AmazonRootCA3 AmazonRootCA4 SFSRootCAG2; do + curl -sS "https://www.amazontrust.com/repository/${ca}.pem" >> ats.pem; echo >> ats.pem +done +{ cat rds.pem; echo; cat ats.pem; } > rds-combined-ca.pem +grep -c "BEGIN CERTIFICATE" rds-combined-ca.pem # sanity: total root count +test "$(wc -c < rds-combined-ca.pem)" -lt 65536 || echo "WARNING: bundle exceeds the 64 KiB Secrets Manager limit" + +aws secretsmanager create-secret \ + --name guardian-prod/server/rds-ca-bundle \ + --secret-string file://rds-combined-ca.pem +``` + +> **Why Secrets Manager for a public cert?** CA roots aren't confidential, so this +> is a *consistency* choice — it reuses the same secret-injection plumbing and IAM +> pattern as `DATABASE_URL` and the ACK keys, with no new mechanism. The trade-offs +> are the 64 KiB cap above and ~$0.40/secret/month. If a future bundle must exceed +> 64 KiB, the next step is an S3 object (no size cap) or an EFS access point +> fetched by the same init container — the Rust/app side would not change. + +Then set the ARN and apply: + +```hcl +rds_ca_bundle_secret_arn = "arn:aws:secretsmanager:REGION:ACCOUNT:secret:guardian-prod/server/rds-ca-bundle-XXXXXX" +``` + +The execution role is granted `secretsmanager:GetSecretValue` on that ARN +automatically. If the bundle is missing or malformed, the init container or the +server preflight **fails closed** at startup rather than connecting insecurely. + +**Rotation.** Update the secret's value (it may hold old and new roots together +for overlap), then force a new deployment so the init container re-reads the +secret and rewrites the file. Because the task definition still points at the +same secret ARN, changing only the secret value does **not** roll tasks on its +own — force it explicitly: + +```bash +aws ecs update-service --cluster --service --force-new-deployment +``` + +No image or code change is required; ensure the new roots are present before they +become the only trusted ones. + +## Deploy + +### Script Commands + +| Command | Purpose | +| --- | --- | +| `./scripts/aws-deploy.sh build` | Build the Guardian server image and push it to ECR as `latest`. Does not run Terraform. | +| `./scripts/aws-deploy.sh plan` | Run `terraform plan` using the immutable digest currently behind ECR `latest`. Does not build, push, or apply. | +| `./scripts/aws-deploy.sh deploy` | Build and push the image, resolve ECR `latest` to an immutable digest, and run `terraform apply`. | +| `./scripts/aws-deploy.sh deploy --skip-build` | Resolve the existing ECR `latest` image to an immutable digest and run `terraform apply` without rebuilding. | +| `./scripts/aws-deploy.sh bootstrap-ack-keys` | Create the prod ACK key secrets in Secrets Manager. Refuses to overwrite existing secrets. With `TF_VAR_guardian_ack_ecdsa_kms_key_arn` set, creates only the Falcon secret (ECDSA is KMS-backed). | +| `./scripts/aws-deploy.sh bootstrap-kms-ecdsa-key` | Create the KMS ECDSA ACK signing key (`ECC_SECG_P256K1` / `SIGN_VERIFY`) and an `alias/${STACK_NAME}-ack-ecdsa` alias, then print the ARN to set. Refuses to overwrite an existing alias. | +| `./scripts/aws-deploy.sh bootstrap-dashboard-cursor-secret` | Create the shared 32-byte dashboard cursor secret in Secrets Manager. Refuses to overwrite an existing secret. | +| `./scripts/aws-deploy.sh status` | Print Terraform outputs for the active `STACK_NAME` and `DEPLOY_STAGE`. | +| `./scripts/aws-deploy.sh logs` | Tail the deployed server's CloudWatch log group. | +| `./scripts/aws-deploy.sh cleanup` | Run Terraform destroy for the active `STACK_NAME` and `DEPLOY_STAGE`. | + +`--skip-build` is meaningful for `deploy`; `plan` never builds or pushes an image. Use `build` before `plan` for a new stack or whenever ECR does not yet contain `${ECR_REPO_NAME}:latest`. + +### One-Step Deploy + +```bash +./scripts/aws-deploy.sh deploy +``` + +The deploy script resolves the ECR `latest` tag to an immutable digest before calling Terraform, so image pushes always produce a real ECS task-definition revision instead of relying on tag reuse. +It also keeps separate local Terraform state files per `STACK_NAME` and `DEPLOY_STAGE`, using `infra/terraform...tfstate` by default. + +AWS deployments must include the `postgres` server feature. The script defaults `GUARDIAN_SERVER_FEATURES` to `postgres`; set `GUARDIAN_SERVER_FEATURES=postgres,evm` only when deploying the optional EVM API surface. + +### Reviewable Build, Plan, Apply + +Use this flow when you want to inspect Terraform changes before applying them: + +```bash +./scripts/aws-deploy.sh build +./scripts/aws-deploy.sh plan +./scripts/aws-deploy.sh deploy --skip-build +``` + +`build` creates the ECR repository if needed and pushes `${ECR_REPO_NAME}:latest`. Both `plan` and `deploy --skip-build` resolve that tag to an immutable digest before invoking Terraform. Do not rebuild or push a new `latest` between `plan` and `deploy --skip-build` unless you intend to apply a different image; rerun `plan` after any rebuild. + +For `DEPLOY_STAGE=prod`, bootstrap the ACK and dashboard cursor secrets once +before the first deploy: + +```bash +DEPLOY_STAGE=prod ./scripts/aws-deploy.sh bootstrap-ack-keys +DEPLOY_STAGE=prod ./scripts/aws-deploy.sh bootstrap-dashboard-cursor-secret +``` + +The normal deploy path does not create or rotate these secrets. It expects the +prod Secrets Manager entries to already exist. Terraform injects the cursor +secret into every ECS task as `GUARDIAN_DASHBOARD_CURSOR_SECRET`. + +Secret names default to `${STACK_NAME}/server/ack-{falcon,ecdsa}-secret-key`, so distinct stacks (e.g. `guardian-prod`, `guardian-prod-eu`) automatically resolve to distinct secrets and multiple Guardian deployments can coexist in the same AWS account. Override per stack by setting `GUARDIAN_ACK_FALCON_SECRET_NAME` / `GUARDIAN_ACK_ECDSA_SECRET_NAME` before `bootstrap-ack-keys` and `deploy`; they flow into Terraform variables and the ECS task definition's `GUARDIAN_ACK_FALCON_SECRET_ID` / `GUARDIAN_ACK_ECDSA_SECRET_ID` env vars. + +The cursor secret defaults to +`${STACK_NAME}/server/dashboard-cursor-secret`. To use an existing secret, set +`GUARDIAN_DASHBOARD_CURSOR_SECRET_NAME` before both bootstrap and deploy. +The deploy helper always passes this resolved name explicitly, so a stale +`infra/terraform.tfvars` value cannot make validation and deployment select +different secrets. For a customer-managed KMS key, its key policy must also +allow the ECS task execution role to decrypt the secret. + +#### Prod with a KMS-backed ECDSA signer + +To keep the ECDSA private key in AWS KMS (never resident in the server process) while Falcon stays in Secrets Manager, create the key first and export its ARN before the rest of the flow. The script keys off `TF_VAR_guardian_ack_ecdsa_kms_key_arn` (the env var, not `terraform.tfvars`) to skip the ECDSA Secrets Manager secret, so it must be exported **before** `bootstrap-ack-keys` and `deploy`: + +```bash +export DEPLOY_STAGE=prod STACK_NAME= + +./scripts/aws-deploy.sh bootstrap-kms-ecdsa-key # creates the key, prints the ARN +export TF_VAR_guardian_ack_ecdsa_kms_key_arn="arn:aws:kms:...:key/" +./scripts/aws-deploy.sh bootstrap-ack-keys # Falcon only; skips ECDSA +./scripts/aws-deploy.sh deploy +``` + +Terraform then grants the ECS task role `kms:Sign` + `kms:GetPublicKey` and injects `GUARDIAN_ACK_ECDSA_BACKEND=aws-kms` / `GUARDIAN_ACK_ECDSA_KMS_KEY_ID`. See [`runbooks/secrets.md`](../runbooks/secrets.md#hosted-ecdsa-backend-aws-kms) for key lifecycle, the immutable-spec caveat, and migrating an existing deployment (a new keypair, so a `SwitchGuardian` identity change). + +Dashboard operator public keys use a separate optional secret. The easiest +deployment path is to pass the public keys to Terraform and let it create the +stack-scoped secret: + +```bash +export GUARDIAN_OPERATOR_PUBLIC_KEYS_JSON='["0x","0x"]' +``` + +or in `terraform.tfvars`: + +```hcl +guardian_operator_public_keys = [ + "0x", + "0x" +] +``` + +If you already manage the secret outside this stack, pass its ARN through +`GUARDIAN_OPERATOR_PUBLIC_KEYS_SECRET_ARN` or +`guardian_operator_public_keys_secret_arn`. An explicit secret ARN takes +precedence over the Terraform-managed public key list. + +The ECS task role is granted read access only to the configured secret ARN. The +server rereads that secret during operator auth checks, so adding or removing a +key in the existing secret takes effect without an application restart. When +Terraform manages the secret, update the public key list and rerun deploy. + +EVM deployments need the `evm` server feature plus server-owned chain config. +By default, `scripts/aws-deploy.sh` derives allowed chain IDs, RPC URLs, and the +shared EntryPoint address from `config/evm/chains.json`. It passes RPC URLs to +Terraform as a stack-scoped Secrets Manager secret and the EntryPoint address as +a normal ECS environment variable. To use an alternate JSON file, set +`GUARDIAN_EVM_CHAIN_CONFIG_FILE`. + +You can still override the derived values by setting +`GUARDIAN_EVM_ALLOWED_CHAIN_IDS`, `GUARDIAN_EVM_RPC_URLS`, or +`GUARDIAN_EVM_ENTRYPOINT_ADDRESS` directly, or by passing existing secret ARNs +through `GUARDIAN_EVM_ALLOWED_CHAIN_IDS_SECRET_ARN` and +`GUARDIAN_EVM_RPC_URLS_SECRET_ARN`. + +When an EVM UI runs on a different origin, set +`GUARDIAN_CORS_ALLOWED_ORIGINS` to a comma-separated list of exact origins. +Wildcard origins are rejected. When this value is configured, the server enables +credentialed CORS so browsers can include the host-only, `HttpOnly` +`guardian_evm_session` cookie. + +If you still have an older local state file at `infra/terraform.tfstate`, move it manually before using the split-state workflow: + +```bash +cp infra/terraform.tfstate infra/terraform.guardian.dev.tfstate +cp infra/terraform.tfstate.backup infra/terraform.guardian.dev.tfstate.backup 2>/dev/null || true +``` + +Use `--skip-build` when the image already exists in ECR and you only need infra/runtime changes, or when you are applying immediately after a reviewed `plan`: + +```bash +./scripts/aws-deploy.sh deploy --skip-build +``` + +For benchmark-oriented production deploys, prefer explicit overrides rather than changing the base prod profile in code. A typical starting point is: + +```bash +set -a && source .env && set +a + +export DEPLOY_STAGE=prod +export STACK_NAME=guardian-prod +export TF_VAR_server_cpu=2048 +export TF_VAR_server_memory=4096 +export TF_VAR_server_desired_count=3 +export TF_VAR_server_autoscaling_min_capacity=3 +export TF_VAR_server_autoscaling_max_capacity=10 +export TF_VAR_rds_instance_class=db.r6g.large +export TF_VAR_rds_allocated_storage=100 +export TF_VAR_rds_max_allocated_storage=400 +export TF_VAR_rds_proxy_subnet_ids='["subnet-25c1722b","subnet-4d0eca6c"]' +export TF_VAR_guardian_db_pool_max_size=64 +export TF_VAR_guardian_metadata_db_pool_max_size=64 +export TF_VAR_guardian_rate_limit_enabled=false + +./scripts/aws-deploy.sh deploy --skip-build +``` + +## Validate + +```bash +./scripts/aws-deploy.sh status +curl https://guardian.openzeppelin.com/pubkey +grpcurl -import-path crates/server/proto -proto guardian.proto -d '{}' guardian.openzeppelin.com:443 guardian.Guardian/GetPubkey +``` + +## Metrics, Dashboard, And Alarms + +Application metrics ship to CloudWatch by default. Two switches control +this: `guardian_metrics_enabled` turns on the server's Prometheus endpoint, +and `cloudwatch_metrics_enabled` deploys the ADOT sidecar, EMF log group, +IAM policy, dashboard, and alarms on top of it. The export pipeline +cascades off with the endpoint, so `guardian_metrics_enabled = false` alone +turns everything off. Disabling only `cloudwatch_metrics_enabled` keeps the +endpoint without publishing CloudWatch custom metrics — but note the +endpoint stays **loopback-only**, so that mode is useful only for an +alternative in-task collector you add by customizing the module; the stack +exposes no knobs for a routable bind address. + +- The server runs with `GUARDIAN_METRICS_ENABLED=true`, serving the Prometheus + exposition on `127.0.0.1:9464/metrics`. Fargate `awsvpc` containers share one + network namespace, so the endpoint is **loopback-only**: the sidecar reaches + it on `127.0.0.1` while nothing outside the task can — it is never exposed via + the ALB, target groups, or security groups. An externally scraped setup would + additionally require an explicit bind address, restricted security-group + ingress, and `GUARDIAN_METRICS_BEARER_TOKEN`; this module deliberately + configures none of that. +- Latency and other duration metrics are **windowed**: the collector + delta-converts the Prometheus histograms (the awsemf exporter does not do + this for histograms on its own), so CloudWatch `Average` over a 5-minute + period reflects that period, not the process lifetime. +- An **AWS Distro for OpenTelemetry (ADOT) Collector sidecar** in the server + task scrapes the endpoint every 60s and exports a curated selection of + metrics to CloudWatch as EMF log events (log group `/ecs//emf`). + CloudWatch materializes them as custom metrics under the + `metrics_namespace` namespace, which is **per stack**: `Guardian/Server` for + the default stack name, `Guardian-Prod/Server` for `guardian-prod`, and so + on, so stacks in one account never mix metrics. + The collector config is injected via `AOT_CONFIG_CONTENT` + (`infra/observability.tf`); no custom image or SSM parameter is involved. + Dimension sets come from Guardian's closed label sets (status, code, outcome, + kind, event, pool, transport); high-cardinality labels (route, method, + operation) are rolled up to keep the custom-metric count and cost bounded. + No scrape bearer token is configured on this path: network isolation (the + loopback-only listener inside the task) is the first defense layer described + in [the observability guide](../guides/observability.md#protecting-the-endpoint-production), + and no other network path to the endpoint exists. +- The sidecar is **non-essential** (its exit does not stop the task) and its + memory is capped at 256 MiB, which bounds its worst-case share of the shared + task envelope — it is a bound, not isolation: both containers still draw + from the task's `server_memory` total, so size that with ~256 MiB of + headroom in mind. If the collector leaks it is OOM-killed at its cap, the + server keeps serving, and the `metrics-missing` alarm fires on the fleet + going dark. With more than one task, a single dead sidecar is not detected + by that alarm — the remaining tasks keep the metrics alive and the fleet's + Sums/Averages skew until the next deployment; per-task detection is a + possible follow-up. +- Terraform creates a CloudWatch **dashboard** named `-server` (request + volume, error rate, latency, proposal/delta lifecycle, canonicalization + health, storage and DB-pool health, Miden RPC, ECS CPU/memory/tasks) and + these **alarms**: + +| Alarm | Fires when | +|-------|------------| +| `-http-5xx-rate` | HTTP 5xx responses (500/501/502/503/504) exceed `alarm_error_rate_threshold_percent` (default 5%) of requests for 15 min. ALB health checks count as successful requests and dilute the rate on low-traffic multi-task fleets — treat as a sustained-fault signal | +| `-grpc-error-rate` | gRPC server-fault responses (`internal`, `unavailable`, `unknown`, `data_loss`, `deadline_exceeded`) exceed the same threshold for 15 min; the same health-check dilution applies | +| `-http-latency` | Average HTTP latency exceeds `alarm_latency_threshold_seconds` (default 1s) for 15 min. Fleet average across all routes — continuous ALB health-check probes dilute it on low-traffic stacks, so treat it as a sustained-degradation signal | +| `-canonicalization-failures` | Canonicalization passes (full, fast, or reconcile) report `error` or `partial` (some accounts failed) outcomes for 10 min | +| `-metrics-missing` | Application metrics stop arriving — the constant `guardian_build_info` heartbeat disappears (metrics endpoint down, sidecar dead, or scrape failing) | +| `-metrics-refresh-failures` | Slow-aggregate refresher attempts are failing; delta/proposal/account gauges are stale | +| `-metrics-refresh-stale` | The refresh timestamp stopped advancing for ≥ 10 min (hung or dead refresher — catches what the failures counter cannot) | +| `-ecs-cpu-high` / `-ecs-memory-high` | ECS service average CPU/memory exceeds `alarm_cpu_threshold_percent` (85%) / `alarm_memory_threshold_percent` (90%); must sit above the autoscaling targets (enforced at plan time) | + +To receive notifications, point the alarms at one or more SNS topics: + +```hcl +alarm_actions = ["arn:aws:sns:us-east-1:123456789012:guardian-alerts"] +``` + +This stack does **not** provision the SNS topic or a chat integration. The +supported notification path is: CloudWatch alarm → an existing **same-region +SNS topic** (listed in `alarm_actions`) → [Amazon Q Developer in chat +applications](https://docs.aws.amazon.com/chatbot/latest/adminguide/what-is.html) +(formerly AWS Chatbot) subscribed to that topic → Slack channel. Create the +topic and the chat subscription out of band, then pass the topic ARN here; +alarms fire both `alarm_actions` and `ok_actions`, so the channel sees +recovery too. + +Set `guardian_metrics_enabled = false` to turn everything off (no metrics env +vars, no sidecar, no dashboard, no alarms — the CloudWatch flag cascades off +with it), or only `cloudwatch_metrics_enabled = false` to keep the +loopback-only endpoint without any CloudWatch export (see the caveat above +about what that mode is useful for). + +### Verify metrics after a deploy + +```bash +# Every name below is per stack; read them all from Terraform outputs. +NS=$(terraform -chdir=infra output -raw metrics_namespace) +DASH=$(terraform -chdir=infra output -raw metrics_dashboard_name) +LOG_GROUP=$(terraform -chdir=infra output -raw server_log_group) +ALARM=$(terraform -chdir=infra output -raw metrics_missing_alarm_name) + +# 1. Metrics arriving in the namespace (allow ~2 minutes after task start) +aws cloudwatch list-metrics --namespace "$NS" --output table | head -40 + +# 2. Dashboard exists and populates +aws cloudwatch get-dashboard --dashboard-name "$DASH" --query DashboardName +# then open CloudWatch > Dashboards > -server in the console + +# 3. Sidecar logs — scrape failures surface here as +# "Failed to scrape Prometheus endpoint" +aws logs tail "$LOG_GROUP" --log-stream-name-prefix adot --since 15m + +# 4. Exercise one alarm notification path end to end +aws cloudwatch set-alarm-state --alarm-name "$ALARM" \ + --state-value ALARM --state-reason "notification path test" +# the next evaluation returns it to OK automatically +``` + +## Operations + +### Logs + +```bash +./scripts/aws-deploy.sh logs +``` + +### Status + +```bash +./scripts/aws-deploy.sh status +``` + +The script reads the state file for the active `STACK_NAME` and `DEPLOY_STAGE`. The current default path is: + +```text +infra/terraform...tfstate +``` + +You can override that with `TF_STATE_PATH` if needed. + +### Destroy + +```bash +./scripts/aws-deploy.sh cleanup +``` + +In the prod stage the RDS instance has deletion protection on and takes a +final snapshot (`-postgres-final`) on destroy, so a prod cleanup +fails until you set `TF_VAR_rds_deletion_protection=false` and re-apply. +Restoring from the final snapshot is covered in +[`runbooks/backup-restore.md`](../runbooks/backup-restore.md). + +ECR repositories are not managed by Terraform: + +```bash +aws ecr delete-repository --repository-name guardian-server --force --region us-east-1 +``` + +## Resources Created + +| Resource | Description | +|----------|-------------| +| ECS Cluster | Fargate cluster derived from `stack_name` | +| ECS Service | Guardian server service | +| Application Load Balancer | Internet-facing ALB derived from `stack_name` | +| Target Groups | HTTP target group for port `3000` and gRPC target group for port `50051` | +| RDS | Managed PostgreSQL instance and subnet group | +| RDS Proxy | Managed PostgreSQL proxy in the production profile | +| Secrets Manager | Secret containing `DATABASE_URL` for the server task | +| Secrets Manager | Optional operator public keys secret for dashboard auth | +| Secrets Manager | Optional EVM allowed chain IDs and RPC URLs secrets | +| Secrets Manager | Secrets containing the Falcon and ECDSA ack private keys used to seed the server keystore in prod | +| Security Groups | ALB, server, and database security groups | +| CloudWatch Log Groups | Cluster execute-command logs, server logs, and the EMF metrics log group | +| IAM Role | ECS task execution and runtime roles | +| ADOT Sidecar | OpenTelemetry Collector container in the server task exporting Prometheus metrics to CloudWatch | +| CloudWatch Dashboard | `-server` application and ECS overview | +| CloudWatch Alarms | Error rate, latency, canonicalization, metrics pipeline, and ECS saturation alarms | + +## Outputs + +| Output | Description | +|--------|-------------| +| `alb_dns_name` | ALB DNS name | +| `alb_url` | Full ALB URL | +| `custom_domain_url` | Canonical service URL: https with a certificate, http when Terraform manages only the DNS record | +| `grpc_endpoint` | Public gRPC endpoint when HTTPS is enabled | +| `database_endpoint` | RDS endpoint used by the server | +| `rds_proxy_endpoint` | RDS Proxy endpoint when enabled | +| `rds_instance_class` | Effective RDS instance class | +| `rds_allocated_storage` | Effective allocated RDS storage in GiB | +| `database_url_secret_arn` | Secrets Manager ARN for the server `DATABASE_URL` | +| `operator_public_keys_secret_arn` | Secrets Manager ARN used for dashboard operator public keys | +| `operator_public_keys_secret_name` | Terraform-managed operator public keys secret name, when created | +| `guardian_evm_allowed_chain_ids_secret_arn` | Secrets Manager ARN used for EVM allowed chain IDs | +| `guardian_evm_rpc_urls_secret_arn` | Secrets Manager ARN used for EVM RPC URLs | +| `guardian_evm_entrypoint_address` | Shared EVM EntryPoint address configured for the server | +| `guardian_cors_allowed_origins` | Explicit CORS origins configured for the server | +| `ack_falcon_secret_name` | Secrets Manager name for the Falcon ack key | +| `ack_ecdsa_secret_name` | Secrets Manager name for the ECDSA ack key | +| `dashboard_cursor_secret_name` | Secrets Manager name for the shared dashboard cursor key | +| `ecs_cluster_arn` | ECS cluster ARN | +| `server_service_arn` | Server ECS service ARN | +| `metrics_namespace` | CloudWatch namespace receiving Guardian application metrics | +| `metrics_dashboard_name` | CloudWatch dashboard name | +| `metrics_emf_log_group` | Log group the ADOT sidecar writes EMF metric events into | +| `metrics_missing_alarm_name` | Name of the metrics-pipeline heartbeat alarm for this stack | + +## Stage Profiles + +### Dev + +- single ECS task +- no ECS autoscaling +- direct ECS to RDS connection +- no RDS Proxy +- conservative Guardian runtime limits + +### Prod + +- higher ECS desired count +- ECS service autoscaling +- larger default RDS instance class and base storage +- RDS storage autoscaling +- RDS Proxy between ECS and RDS +- higher Guardian runtime rate-limit and DB-pool defaults for benchmark traffic + +#### Horizontal scaling (multiple replicas) + +The prod profile runs 2–6 tasks behind the ALB. Because it sets `GUARDIAN_ENV=prod` +and the Postgres backend, the server runs **shared coordination** (sessions, +login challenges, and the canonicalization lease live in Postgres) — so any +request lands on any replica and canonicalization runs on exactly one replica at +a time. Terraform also sets `GUARDIAN_MAX_REPLICAS` from +`effective_guardian_max_replicas` (the greater of desired count and autoscaling +max, 6 by default) so global HTTP and dashboard commitment rate limits are +partitioned across the steady-state fleet. A rolling deployment may allow up to +`server_deployment_maximum_percent / 100` times the configured aggregate limit +(2× by default). The default dashboard share is 5 requests per minute on a +keep-alive-pinned replica. In prod, Terraform requires the pre-created dashboard +cursor secret and injects the same value into every task, so dashboard +pagination works across replicas. The server itself still warns and uses an +ephemeral key when run without the variable outside this managed prod profile. +Watch the per-replica `GUARDIAN_DB_POOL_MAX_SIZE` against Postgres +`max_connections` (RDS Proxy absorbs most of this). Full operator guidance: +[`runbooks/horizontal-scaling.md`](../runbooks/horizontal-scaling.md). + +## HTTPS And gRPC + +HTTPS is enabled when `acm_certificate_arn` is set. DNS can be managed through Cloudflare, Route 53, or both depending on which variables are provided. + +When HTTPS is enabled, the ALB routes standard HTTPS requests to the server HTTP port `3000` and gRPC requests for `/guardian.Guardian/*` to the server gRPC port `50051`. The public gRPC endpoint uses the same hostname on port `443`. + +On Apple Silicon hosts, `CPU_ARCHITECTURE=X86_64` builds are slower because Docker builds `linux/amd64` images under emulation. Switching to `ARM64` avoids that local emulation cost, but it also changes the ECS task runtime architecture. + +## Migrating An Existing ECS-Postgres Stack + +The current Terraform configuration is RDS-only. There is no supported dual-mode deployment that keeps the old ECS Postgres service alive after apply. + +Use this cutover flow for an existing stack: + +1. Capture the current stack state: + ```bash + ./scripts/aws-deploy.sh status + ``` +2. Create a logical PostgreSQL backup from the existing ECS-hosted database before applying the updated stack. +3. Apply the updated RDS-backed Terraform stack: + ```bash + ./scripts/aws-deploy.sh deploy --skip-build + ``` +4. Restore the backup into the new RDS database. +5. Validate the public service: + ```bash + ./scripts/aws-deploy.sh status + curl https:///pubkey + grpcurl -import-path crates/server/proto -proto guardian.proto -d '{}' :443 guardian.Guardian/GetPubkey + ``` +6. Confirm the old Postgres ECS service and Cloud Map database-discovery resources are gone from AWS before treating the cutover as complete. + +## Troubleshooting + +- If the server task fails during startup, check `./scripts/aws-deploy.sh logs` first and confirm the reported `database_endpoint` matches the expected RDS host. +- If a prod deploy fails before Terraform starts, confirm the fixed prod ACK secrets exist by running `./scripts/aws-deploy.sh bootstrap-ack-keys` once and then retrying the deploy. +- If RDS subnet-group creation fails, verify the selected subnets cover at least two subnets for the database deployment. +- If gRPC works against the ALB directly but fails on the public hostname, check Cloudflare gRPC settings on the zone. +- If the `metrics-missing` alarm fires or the dashboard is empty, tail the sidecar stream (`aws logs tail /ecs/ --log-stream-name-prefix adot`) — scrape failures appear as `Failed to scrape Prometheus endpoint`, and export failures reference `awsemf`. + +## Legacy Script + +The legacy deployment logic has been replaced by the Terraform-backed `scripts/aws-deploy.sh`. diff --git a/versioned_docs/version-0.16/builder/miden-guardian/operations/troubleshooting.md b/versioned_docs/version-0.16/builder/miden-guardian/operations/troubleshooting.md new file mode 100644 index 00000000..7e6a1640 --- /dev/null +++ b/versioned_docs/version-0.16/builder/miden-guardian/operations/troubleshooting.md @@ -0,0 +1,657 @@ +--- +title: "Troubleshooting" +sidebar_position: 4 +--- +Common Guardian failures and how to resolve them. Organised by symptom +first, then by error code. + +For concepts (lifecycle, trust model, recovery flows) see +[`docs/CONCEPTS.md`](../getting-started/concepts.md). +For local-dev setup see [`docs/LOCAL_DEV.md`](../getting-started/local-dev.md). + +## By symptom + +### Client and node disagree about the network version + +A Guardian server or SDK built on the Miden 0.16 line rejects a 0.15 node +(and vice versa) at the RPC boundary: the Miden client sends the genesis +commitment with every request, so a version mismatch surfaces as a gRPC +rejection when connecting or syncing, not as silent corruption. Point the +client at a node running the matching Miden line (devnet runs the 0.16 +node; for local work run a matching `miden-node`). + +### State created on Miden 0.15 fails to load after the 0.16 upgrade + +Deserialization errors such as `Unsupported version. Got '[0, 0, 3]'`, a +local client store that errors on open, or a demo that panics on stale +`~/.guardian` metadata all mean the same thing: state serialized under +Miden 0.15 is not readable under 0.16, and the 0.16 devnet is a fresh +chain, so 0.15-era accounts and notes no longer exist on-chain. There is +no migration: delete local miden-client stores (`store.sqlite3`), +`~/.guardian` metadata, and browser IndexedDB state, then recreate +accounts. The Guardian server does not keep 0.15 account records either: the +first 0.16 startup runs an irreversible reset that deletes Miden account +metadata, states, deltas, and proposals (EVM rows are preserved). So after the +upgrade any remaining failure of this kind is a *client-side* leftover, not +server state. Operator steps are in +[`PRODUCTION.md`](./production.md#upgrading-to-miden-016); what changed between +Miden lines is in +[`MIDEN_COMPATIBILITY.md`](../miden-compatibility.md). + +### Server fails to start + +Most startup failures are environment misconfiguration. Check in order: + +1. **`GUARDIAN_NETWORK_TYPE` unset, non-Unicode, or unrecognized.** The server exits + before binding any port with + `Failed to resolve network type: GUARDIAN_NETWORK_TYPE is not set; accepted values: ...` + (or `contains non-Unicode data` / `has unrecognized value "..."` for + malformed values and typos like `tesnet`). There is no fallback network. + Replace the variable with `MidenLocal`, `MidenTestnet`, or + `MidenDevnet` (short forms `local`/`testnet`/`devnet` work, + case-insensitive). +2. **Miden node unreachable or Miden RPC settings invalid.** The initial + node connection retries transient failures for ≈35 seconds (5 attempts + with backoff) before failing startup with + `Failed to create network client: failed to connect to the Miden RPC endpoint` — + a brief node blip at boot no longer kills the server, but a genuinely + down or wrong endpoint still does, as do TLS/certificate + misconfigurations, which fail immediately without retrying. An invalid + `GUARDIAN_MIDEN_RPC_ENDPOINT` (not an origin-only `http(s)` URL in the + form `scheme://host[:port]`) also fails immediately. Embedded credentials, + paths, queries, and fragments are rejected because tonic does not send them + as authentication. Note that + `GUARDIAN_MIDEN_RPC_MAX_ATTEMPTS` never affects canonicalization: those + reads make one attempt per pass and transient failures are recovered by + the next scheduled pass. See [CONFIGURATION.md](../reference/configuration.md). +3. **`DATABASE_URL` missing under `--features postgres`.** The builder + panics with `"DATABASE_URL environment variable is required"`. Either + set it or rebuild without the `postgres` feature. +4. **Filesystem paths not writable.** Filesystem builds use + `GUARDIAN_STORAGE_PATH`, `GUARDIAN_METADATA_PATH`, and + `GUARDIAN_KEYSTORE_PATH` when set, defaulting to + `/var/guardian/storage`, `/var/guardian/metadata`, and + `/var/guardian/keystore` respectively. Startup fails if the process + cannot create or write to those paths — common on dev machines where + `/var/guardian` doesn't exist or isn't owned by the running user. + Either set the env vars to a writable location or `mkdir -p` + `/var/guardian/{storage,metadata,keystore}` with the right + permissions. +5. **Postgres migrations fail.** The Postgres path runs migrations at + startup. If the DB user lacks `CREATE` permissions, startup fails. + Grant `CREATE` on the schema or run migrations as a privileged user. + For a migration that times out, crash-loops the task, or breaks + authentication mid-deploy, see [Postgres migrations block startup or fail + mid-deploy](#postgres-migrations-block-startup-or-fail-mid-deploy). +6. **ACK secrets missing in prod.** + `scripts/aws-deploy.sh deploy` refuses to apply if either ACK secret + is missing. Run `DEPLOY_STAGE=prod ./scripts/aws-deploy.sh bootstrap-ack-keys` + first (see [Secrets runbook](../runbooks/secrets.md#bootstrap-first-prod-deploy)). +7. **Operator allowlist source not set.** If you intend to use the + dashboard, set `GUARDIAN_OPERATOR_PUBLIC_KEYS_SECRET_ID` (prod) or + `GUARDIAN_OPERATOR_PUBLIC_KEYS_FILE` (local). Without either, the + dashboard is unreachable. +8. **Database TLS misconfigured.** With a verifying `sslmode`, startup fails + closed before migrations run. Map the error: + - error naming `sslmode` (`allow`/`prefer` or an unknown value) → choose an + explicit mode: `disable`, `require`, `verify-ca`, or `verify-full`. + - error naming `sslrootcert` (missing/unreadable/empty file, or `system`) → + mount a readable PEM CA bundle and point `sslrootcert=` at it + (`sslrootcert=system` is unsupported). + - connection refused with a certificate-verification error → wrong CA for the + server, an expired certificate, or (under `verify-full`) a hostname that + doesn't match the certificate's SAN. For AWS RDS Proxy, ensure the bundle + includes the Amazon Trust Services roots, not only the RDS CA roots. + - works under `verify-ca` but fails under `verify-full` → hostname/SAN + mismatch with the endpoint. See [Database TLS](../reference/configuration.md#database-tls). +9. **Replay-protection state file missing or inconsistent (filesystem builds).** Startup + fails with `Replay-protection state file ... is missing` when + `.metadata/auth_state.json` has been deleted from a store whose + `accounts.json` was already migrated off legacy timestamps. Starting + anyway would reset replay protection and re-accept previously seen + request timestamps, so the server refuses instead. Restore + `auth_state.json` from backup together with the rest of the metadata + directory, including `.metadata/auth_state_legacy_floor_v1` when it + exists. If a restore predates that marker, startup re-runs floor + repair and synthesizes an account-level floor from the highest stored + signer timestamp for every account, including ones created after the + per-signer migration. A newly added signer whose first request + timestamp is at or below that floor is rejected once; the next + strictly later timestamp succeeds. If no backup exists, recreating + `auth_state.json` with the literal content `{}` lets the server start. + That is an explicit operator decision to accept a replay window as + wide as the timestamp skew allowance. If startup instead reports + replay state with no matching account metadata, restore + `accounts.json` and `auth_state.json` from the same backup; Guardian + will not rewrite or discard the unmatched floors. + +### Postgres migrations block startup or fail mid-deploy + +Migrations run at server startup, before the connection pools are built. +Each migration runs in a single transaction, so a failure never leaves a +half-applied schema: the task exits, the orchestrator restarts it, and it +retries from the same point. + +**Check which migrations are applied.** Compare the database against the +migration directory (`crates/server/migrations/`, whose directory-name +timestamps are the versions): + +```sql +SELECT version FROM __diesel_schema_migrations ORDER BY version DESC LIMIT 5; +``` + +**`Timed out after 60s waiting for the migration advisory lock`.** Only one +replica migrates at a time, serialised by a Postgres advisory lock. This +message means another replica held it for the whole wait, or a session died +mid-migration without releasing it. Find the holder: + +```sql +SELECT a.pid, a.state, now() - a.xact_start AS age, left(a.query, 120) AS query + FROM pg_locks l + JOIN pg_stat_activity a USING (pid) + WHERE l.locktype = 'advisory'; +``` + +The lock is released when its session ends, so a restart of the stuck replica +clears it. Do not unlock it manually while another replica is still migrating. + +**`Failed to run migrations: ... canceling statement due to lock timeout`.** +Migrations that rewrite a hot table take an explicit table lock with a 5s +`lock_timeout`, so they fail fast rather than queueing every later writer +behind them. A concurrent long-running transaction on the locked table is the +usual cause: + +```sql +SELECT pid, now() - xact_start AS age, state, left(query, 120) AS query + FROM pg_stat_activity + WHERE xact_start IS NOT NULL + AND now() - xact_start > interval '5 seconds' + ORDER BY age DESC; +``` + +A restart normally succeeds. Repeated failures mean a persistent long +transaction, not a broken migration. + +**Authenticated requests fail on some replicas during a deploy.** A migration +that changes a table the *previous* binary writes is not backward compatible, +and the deployment keeps old tasks serving until the new one is healthy +(`deployment_minimum_healthy_percent = 100`). Between the migration committing +and the old tasks draining, requests routed to an old task fail. This is +deliberate: failing closed beats writing state the new schema cannot represent. +It is expected for `2026-07-31-000001_account_auth_state` and +`2026-08-13-000001_auth_state_per_signer`. To eliminate the window rather than +ride it out, stop the old tasks before the new one migrates, which trades the +partial errors for brief full downtime. + +### Guardian public key changes unexpectedly + +**Treat this as a security event** until you can confirm intentional +rotation: + +1. Stop trusting deltas signed under the new key. +2. Check the deployment audit trail: who ran + `aws secretsmanager update-secret` on the ACK secret ARNs? CloudTrail + `GetSecretValue` events identify the principals. +3. Check task restart timing: ACK keys are read once at startup, so a + pubkey change implies either a rotation event or a task that came up + in a different environment (e.g. `GUARDIAN_ENV` unset → ephemeral + filesystem keys). +4. If unrotated, follow the + [compromise response runbook](../runbooks/secrets.md#compromise-response). + +Common benign causes: +- Local dev with `GUARDIAN_KEYSTORE_PATH` pointing at a tmpfs that was + wiped between restarts. +- Running with `GUARDIAN_ENV` unset and no `AWS_REGION` — the server + falls back to filesystem keystore and auto-generates a fresh key. + +### Signed requests are rejected at the auth layer + +This section covers the auth-middleware verdicts, `401` with +`code: authentication_failed` or `code: authentication_replay`. If your +request was *authenticated* but still rejected (e.g. +`403 authorization_failed`, `403 signer_not_authorized`, +`400 commitment_mismatch`, `409 conflict_pending_*`), jump straight to +the [error code reference](#error-code-reference): the signature was +fine, the service layer rejected the operation. + +The two 401 codes mean different things (issue #367 split them): + +- **`authentication_replay`** (`meta.retryable: true`). The request was + correctly signed, but its `x-timestamp` was not strictly greater than + the last timestamp Guardian accepted *from the same signer* on that + account. This is the replay-protection CAS, not a credential problem: + it happens when two in-flight requests from one client land out of + order, or when the same key is used from two processes at once. Both + SDK clients retry it automatically (bounded, fresh timestamp and + signature per attempt); a user-visible `authentication_replay` means + the condition persisted through the retry budget; look for a second + process (background poller, second tab, another host) signing with + the same key. Replay state is scoped per `(account, signer)`, so + other cosigners of a multisig never cause this for you. +- **`authentication_failed`** (`meta.retryable: false`, terminal) for + three reasons: + 1. **Clock skew.** Timestamps must be within ±5 minutes of server time + ([`metadata/auth/credentials.rs:6`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/server/src/metadata/auth/credentials.rs#L6)). + Sync the client clock (NTP) or check for container time drift. + 2. **Invalid or unauthorized signature.** The signer's public-key + commitment is not in the account's authorized set, or the signature + does not verify. + 3. **Modified payload after signing.** The signature covers the + request body hash. Mutating the body (proxy reformatting, JSON + re-serialise) invalidates the signature. Sign-then-send; do not + transform between. + +Headers required on every authenticated request: `x-pubkey`, +`x-signature`, `x-timestamp`. If any are missing the response is also +`authentication_failed`. Never retry `authentication_failed`; it will +not succeed until the underlying cause (clock, key, payload) changes. + +**Mixed server/client rollout:** SDK clients retry only +`authentication_replay`; they never retry `authentication_failed`. A replay CAS +reported under the older authentication code, or received by a client without +replay-specific retry handling, can therefore surface as a terminal 401 until +both sides use the same error contract. + +The same server upgrade changes HTTP `/configure` failures from a +`ConfigureResponse` carrying `success: false` to the standard +`{ code, message, meta }` error envelope. Update direct HTTP integrations that +inspect the old body shape before rolling out the server; gRPC continues to use +the shared protobuf response. + +### Pending proposals never resolve + +A proposal stays `pending` until enough cosigners sign and someone +promotes it via `PushDelta`. If it sits too long: + +- **Threshold not met.** Count signatures: the proposal needs `n` of `m` + per the account configuration. Use `GetDeltaProposal` to see who has + signed. +- **Pending limit reached.** `POST /delta/proposal` returns `409` with + `code: pending_proposals_limit` once an account has + `GUARDIAN_MAX_PENDING_PROPOSALS_PER_ACCOUNT` (default `20`) pending. + Resolve or cancel some. +- **Canonicalization backlog.** Promoting a proposal to canonical depends + on the candidate's matching Miden update being observed. Check the + canonicalization worker logs and Miden RPC health. +- **Storage backend write failures.** A failing metadata backend will + return `storage_error` on signing attempts. Check disk space (filesystem) + or DB connectivity (Postgres). + +### Candidates are being retained or discarded + +Delta moves `candidate` → `retained` (default) or `candidate` → +`discarded` (retention disabled, or a client abandon). The cause is one +of: + +1. The corresponding Miden proof was never submitted. +2. The proof was submitted but the on-chain commitment differs from the + one Guardian acknowledged — usually because another device advanced + the account state in parallel. +3. RPC endpoint targets the wrong network — Guardian polled the wrong + ledger and never saw the update. +4. The canonicalization grace period (default 10 minutes) elapsed before + the proof landed. + +A `retained` delta is not final: the dedicated reconcile pass keeps +probing the chain (backing off as the row ages) and promotes it +automatically if the transaction ever shows up, for up to +`retained_ttl_seconds` (default 24 h). The `status_reason` on the +dashboard feed says which verdict parked it (`retry_exhausted` / +`diverged`); a `diverged` row that later reconciles means the +divergence verdict was spurious (e.g. a lagging RPC node). + +Recovery for the client: check the delta's status first — if it flipped +to `canonical`, the transaction landed and there is nothing to redo. +Otherwise `GET /delta/since` → replay canonical chain → rebuild the +transaction → resubmit (this supersedes the retained row). + +Operator checks: +- Canonicalization worker is running (look for `jobs::canonicalization` + log lines). +- Miden RPC endpoint reachable (`rpc_unavailable` in logs indicates it + isn't). +- No `network_error` storms. +- `guardian_canonicalization_candidates_total{outcome=...}` breaks down + what the worker decided per candidate (`retained` is the default + give-up path; `diverged` and `discarded` are the delete paths when + retention is disabled; `stale_base` means a promotion was rolled back + because the stored state moved mid-pass and will retry next tick; + `reconciled` / `reconcile_deferred` / `reconcile_expired` are the + reconcile pass resolving retained rows). +- `guardian_canonicalization_candidate_age_seconds` growing without + bound means candidates are not converging — check Miden RPC health + and the discard outcomes above. +- `guardian_canonicalization_fast_runs_total{outcome=...}` and + `guardian_canonicalization_fast_run_duration_seconds` expose failures and + latency of the promotion-only pass without changing the full-pass gauges, + age histogram, or fetched-row counter. +- `guardian_canonicalization_reconcile_runs_total{outcome=...}` and + `guardian_canonicalization_reconcile_run_duration_seconds` do the same + for the recoverable-delta reconcile pass. +- `RUST_LOG=server::jobs::canonicalization=debug` emits one + `Fast-promotion pass completed` summary per fast tick, including empty passes, + with page, candidate, account-batch, deadline, and cursor-progress fields. +- Retention and reconciliation emit stable `event` / `reason` fields for + log-based triage (with `account_id`, `nonce`, and `age_seconds` / + `retention_reason` / `expires_at` where applicable): + - `event=candidate_retained reason=retry_exhausted|diverged` + - `event=reconcile_deferred reason=chain_at_stored_base|chain_probe_unavailable|end_state_not_on_chain|base_no_longer_applies|recomputed_commitment_mismatch|no_matching_recoverable_delta` + - `event=reconcile_skipped reason=obsolete_base` + - `event=reconcile_promoted` + - `event=reconcile_expired` + - `event=reconcile_superseded` + The `chain_at_stored_base` / `chain_probe_unavailable` deferrals are + logged (debug/info) but deliberately not counted in + `guardian_canonicalization_candidates_total` — a healthy steady state + probes every due account and finds the chain unmoved, and counting + that would dwarf every other outcome. +- `guardian_canonicalization_commitment_mismatches_total` counting up + means a client omitted `new_commitment` or claimed one that differs + from the recomputed value. The full pass can promote using the value it + independently verifies; the fast pass defers the candidate to that full + path. In either case, investigate the client. + +### `commitment_mismatch` on `PushDelta` + +The client tried to apply a delta on top of a state Guardian doesn't +believe is current. Always recoverable: + +```bash +GET /delta/since?account_id=...&nonce= +``` + +Replay the returned canonical deltas locally, then resubmit your new +delta. This is the same pattern as a Git fast-forward. + +### Stale state served by Guardian + +Symptoms: client reads look "behind reality" relative to Miden. Causes: + +- Canonicalization worker stalled (RPC down, DB write failures). +- Operator is intentionally censoring (run the + [provider rotation flow](../getting-started/concepts.md#provider-rotation)). +- Backend lag — Postgres replication or filesystem fsync latency. + +Always compare against Miden before signing high-value transactions; see +the [client verification checklist](../getting-started/concepts.md#client-verification-checklist). + +### Account is paused + +State-transition, proposal, and EVM mutation calls against the account +(`PushDelta`, `PushDeltaProposal`, `SignDeltaProposal`, and the matching +EVM proposal/session operations) return `409 GUARDIAN_ACCOUNT_PAUSED` +(gRPC `FailedPrecondition`). Reads and `ConfigureAccount` still work. + +- **Confirm:** check `GET /dashboard/accounts/{id}` — paused accounts + report `paused_at` and `paused_reason`. +- **Resume:** an operator holding `accounts:pause` calls + `POST /dashboard/accounts/{id}/unpause` with an optional `{"reason": "..."}` + body. The pause/unpause cycle is idempotent and audit-logged. See + [`DASHBOARD.md`](./dashboard.md#account-pausing). +- **Don't bypass:** the pause is enforced server-side at the metadata + layer, not in the client. There is no env var or feature flag to + disable it. + +### Rate limits triggered + +Over HTTP: `429` with `code: rate_limit_exceeded` and a `Retry-After` +header. Over gRPC: `RESOURCE_EXHAUSTED` with a `retry-after` metadata key +(seconds) and the same `rate_limit_exceeded` envelope in the status +details. The sustained limit is keyed per IP alone, so heavy gRPC +traffic (the Rust SDK and benchmark harness default) can exhaust the +sustained allowance for HTTP calls from the same client, and vice versa. +The burst limit is keyed per IP and endpoint, and HTTP paths never +collide with gRPC method names, so burst buckets are not shared. +The rejection counter `guardian_rate_limit_rejections_total` carries a +`transport` label to tell the two surfaces apart. Rate-limit rejections +happen before any handler runs, so retrying after the hint is always safe. + +ALB gRPC health checks (`/guardian.Guardian/GetPubkey`) are metered like +any other traffic, keyed per ALB-node address. Their volume is far below +any sane budget, but a global limit below `GUARDIAN_MAX_REPLICAS` +partitions each replica's budget to zero and would fail health checks and +cycle tasks; the prod builder refuses to start in that configuration, dev +builds only warn. + +If clients report throttling at request rates well below the configured +budget, check the `Request rate limited` lines' `client_ip` field. They +are logged at `debug` (rejections are expected traffic, and their volume +tracks the flood being shed), so enable them with +`RUST_LOG=info,server::middleware::rate_limit=debug`. +The proxy's address (or `unknown`) on every line means your ingress is +not forwarding the client address, so all clients share one budget: +common with unconfigured reverse proxies (nginx `grpc_pass` needs +explicit `grpc_set_header` for forwarding headers), Kubernetes +`externalTrafficPolicy: Cluster`, or L4 balancers without client-IP +preservation. See +[PRODUCTION.md](./production.md#running-behind-your-own-ingress-non-aws). + +Server knobs (set on the task, not per-account): + +| Variable | Default | Notes | +|---|---|---| +| `GUARDIAN_RATE_LIMIT_ENABLED` | `true` | Set `false` only in test environments. | +| `GUARDIAN_RATE_BURST_PER_SEC` | `10` (dev), `200` (prod) | Requests per one-second window. | +| `GUARDIAN_RATE_PER_MIN` | `60` (dev), `5000` (prod) | Sustained rate. | +| `GUARDIAN_MAX_REQUEST_BYTES` | `1048576` (1 MB) | Reject larger bodies. | + +If you legitimately need higher throughput, raise these via the deploy +script or Terraform variables rather than disabling rate limiting. + +### Dashboard not reachable + +- **Allowlist empty.** Without at least one operator entry, every + challenge fails. Add an operator (see + [`docs/DASHBOARD.md`](./dashboard.md#enrolling-an-operator)). +- **Stale browser session.** Operator sessions are per-task. After a + multi-task deploy, you may be routed to a task that did not issue your + cookie. Re-authenticate. +- **`GUARDIAN_OPERATOR_PUBLIC_KEYS_*` env not set.** No source means no + allowlist means the dashboard refuses every login. Check task env. + +### Browser dashboard returns CORS errors + +By default +([`middleware/cors.rs`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/server/src/middleware/cors.rs)) the +server is permissive: when `GUARDIAN_CORS_ALLOWED_ORIGINS` is unset or +empty, every origin is allowed and credentials are **not** advertised +(useful for local dev). Setting the variable switches to a strict +credentialed allowlist: + +| Variable | Default | Notes | +|---|---|---| +| `GUARDIAN_CORS_ALLOWED_ORIGINS` | unset (allow-any, no credentials) | Comma-separated explicit origins (e.g. `https://accounts.openzeppelin.com,https://admin.openzeppelin.com`). Wildcard `*` is rejected because credentialed CORS requires explicit origins. | + +If the browser console shows a CORS preflight failure after deploy, +either (a) the origin isn't in the allowlist, or (b) the value still +contains `*` and the server failed startup — check task logs for the +`{ALLOWED_ORIGINS_ENV} must use explicit origins` error. + +## Error code reference + +All Guardian error responses carry a stable `code` string. Wire strings +come from +[`crates/server/src/error.rs:206-247`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/server/src/error.rs#L206). + +### Authentication and authorization + +| Code | HTTP | First check | +|---|---|---| +| `authentication_failed` | 401 | Clock skew, invalid/unauthorized signature, modified payload, missing headers. Terminal; never retried. | +| `authentication_replay` | 401 | Correctly signed but the timestamp lost the per-signer replay CAS. `retryable: true`; SDK clients retry it automatically with a fresh timestamp and signature. See [the auth-layer section](#signed-requests-are-rejected-at-the-auth-layer). | +| `authorization_failed` | 403 | Account credentials don't authorize the operation. | +| `signer_not_authorized` | 403 | Signer isn't on the proposal's allowed signer set. | +| `GUARDIAN_INSUFFICIENT_OPERATOR_PERMISSION` | 403 | Operator dashboard call requires a permission the operator doesn't have. Response body carries `missing_permissions: string[]` (lex-sorted, deduplicated) and `retryable: false`. See [`DASHBOARD.md`](./dashboard.md#permission-vocabulary). | + +### Resource lookup + +| Code | HTTP | First check | +|---|---|---| +| `account_not_found` | 404 | Account ID typo or `/configure` never called. | +| `state_not_found` | 404 | Account configured but no state pushed. | +| `delta_not_found` | 404 | Wrong account/nonce; check `GetDeltaSince`. | +| `proposal_not_found` | 404 | Proposal expired or already executed. | +| `account_data_unavailable` | 503 | Backend transient failure; retry. | + +### Conflict and concurrency + +| Code | HTTP | First check | +|---|---|---| +| `account_already_exists` | 409 | `/configure` called twice for the same account. | +| `conflict_pending_delta` | 409 | A non-canonical delta is in-flight; wait for it to finalise. | +| `conflict_pending_proposal` | 409 | Pending proposals exist; resolve before pushing a direct delta. | +| `pending_proposals_limit` | 409 | Account hit `GUARDIAN_MAX_PENDING_PROPOSALS_PER_ACCOUNT` (default 20). | +| `proposal_already_signed` | 409 | This signer already signed this proposal. | +| `GUARDIAN_ACCOUNT_PAUSED` | 409 (gRPC `FailedPrecondition`) | Account is paused by an operator. Response body includes the operator-supplied `paused_reason`. Unpause via `POST /dashboard/accounts/{id}/unpause` (requires `accounts:pause`). See [`DASHBOARD.md`](./dashboard.md#account-pausing). | +| `GUARDIAN_ACCOUNT_RELEASED` | 409 (gRPC `FailedPrecondition`) | The account switched to a different guardian (a canonicalized `switch_guardian` delta moved the guardian key away from this server) and this server released it. Response body includes `released_at`. Reads keep working; mutations stay refused until the wallet re-onboards via `/configure`. | + +### Validation + +| Code | HTTP | First check | +|---|---|---| +| `invalid_input` | 400 | Generic validation failure; the message explains. | +| `invalid_account_id` | 400 | Malformed account ID. | +| `invalid_delta` | 400 | Delta payload failed schema or commitment validation. | +| `invalid_commitment` | 400 | Commitment string isn't a valid hex hash. | +| `commitment_mismatch` | 400 | `prev_commitment` doesn't match server's view; use `GetDeltaSince` to catch up. | +| `invalid_proposal_signature` | 400 | Signature doesn't verify against the proposal payload. | +| `invalid_network_config` | 400 | `Configure` payload's network config is malformed. | +| `invalid_cursor` | 400 | Pagination cursor doesn't decode. | +| `invalid_limit` | 400 | Pagination limit out of range. | +| `invalid_status_filter` | 400 | Status filter string isn't in `{candidate, canonical, retained, discarded}`. | +| `unsupported_for_network` | 400 | Endpoint not available for the account's network. | +| `unsupported_evm_chain` | 400 | EVM chain ID not in the configured allowlist. | +| `invalid_evm_proposal` | 400 | EVM proposal payload validation failed. | +| `insufficient_signatures` | 400 | Threshold not met for a multi-sig execute. | + +### Network and infrastructure + +| Code | HTTP | First check | +|---|---|---| +| `rpc_unavailable` | 502 | Miden RPC endpoint unreachable. Check the configured endpoint and Miden node health. | +| `rpc_validation_failed` | 502 | Miden RPC returned an error during validation. | +| `network_error` | 502 | Miden network call failed mid-flight. | +| `rate_limit_exceeded` | 429 | Backoff using the `Retry-After` header; tune `GUARDIAN_RATE_*` if legitimately needed. | +| `data_unavailable` | 503 | Cross-account aggregate degraded (filesystem backend above `DEFAULT_FILESYSTEM_AGGREGATE_THRESHOLD`). Distinct from `account_data_unavailable`, which is account-scoped. | + +### Server-side + +| Code | HTTP | First check | +|---|---|---| +| `storage_error` | 500 | Persistence backend rejected the write. Check disk (filesystem) or DB (Postgres) health. | +| `signing_error` | 500 | ACK signer failed. Check the keystore mount and Secrets Manager IAM. | +| `configuration_error` | 500 | Server misconfiguration. Almost always means a startup-time env var was wrong. | + +## Logging and observability + +The server emits `tracing` logs — `text` by default, `json` when `GUARDIAN_LOG_FORMAT=json` (see [`CONFIGURATION.md`](../reference/configuration.md#logging)). `text` uses ANSI colors only when stdout is a TTY; `json` emits flattened JSON with span context for CloudWatch Logs Insights. + +Hot-path service handlers emit request events at `debug` — enable them with `RUST_LOG=server=debug` or `RUST_LOG=server::services=debug`. At the default `info` filter each request emits one span-close line instead, carrying the span's fields (account ID, nonce, commitment, signer/match counts) and `time.busy` / `time.idle`: + +``` +2026-08-19T11:53:31.172132Z INFO push_delta_proposal{account_id="0x1234…" nonce=7 commitment="0xabcd…" signer_count=2}: server::services::push_delta_proposal: close time.busy=4.1ms time.idle=112µs +``` + +That line is emitted whether the request succeeded or failed. This matters because the centralized error lines (`guardian error (HTTP 5xx)`, `guardian error (gRPC internal)`) are emitted from the `GuardianError` → response conversion, which runs after the service span has closed: they carry `code` and `detail` only, and the immediately preceding close line is what identifies the account. Low-volume domain milestones (`Account configured`, `Delta proposal created`, `Delta proposal signed`) keep their own `info` line. + +`resolve_account` is a nested helper rather than a request boundary, so its span is `debug` and it contributes no close line at `info`. + +The per-read `Commitment mismatch during state verification` (`network::miden`) is also `debug`; persistent divergence is surfaced by the canonicalization processor's streak-gated WARN (confirmed divergence), not the per-read log. + +Useful filters: + +```bash +# Watch canonicalization worker decisions (including confirmed divergence WARN) +RUST_LOG=server::jobs::canonicalization=debug + +# Watch per-request debug events +RUST_LOG=server=debug +RUST_LOG=server::services=debug + +# Watch auth verifier rejections +RUST_LOG=server::middleware::auth=debug,server::metadata::auth=debug + +# Watch dashboard authz +RUST_LOG=server::dashboard=debug + +# JSON output for CloudWatch +GUARDIAN_LOG_FORMAT=json RUST_LOG=info cargo run -p guardian-server +``` + +### Startup configuration banner + +At boot, before any listener binds, the server emits a one-shot summary +of its resolved, non-secret configuration (target +`server::builder::startup`). Use it to confirm which version, backends, +network, and signers a process is actually running: + +``` +===== Guardian server configuration ===== +Guardian server starting version="0.1.0" git_sha="" profile="release" +network network=MidenTestnet rpc_endpoint="https://rpc.testnet.miden.io" +storage backend storage=Postgres +ack signers falcon="enabled" falcon_commitment=0x… ecdsa_backend="aws-kms" ecdsa_commitment=0x… +dashboard operators=0 cursor_secret="ephemeral" +canonicalization check_interval_seconds=10 fast_promotion_enabled=true fast_promotion_interval_seconds=3 fast_promotion_window_seconds=30 max_retries=48 submission_grace_period_seconds=600 +listeners http=3000 grpc=50051 +compiled features features=["postgres"] +========================================= +``` + +Notes: + +- Only backend **kinds** and ports appear — never connection strings, + KMS key ids, keystore paths, or credentials. +- `git_sha="unknown"` means the build received no `GUARDIAN_GIT_SHA` + build arg and had no git working tree — expected for some Docker + builds; not an error. +- `cursor_secret="ephemeral"` corroborates the separate + `GUARDIAN_DASHBOARD_CURSOR_SECRET` warning: multi-replica deployments + must set a stable shared secret (see + [`CONFIGURATION.md`](../reference/configuration.md)). +- `canonicalization` absent (replaced by an "optimistic mode" line) + means deltas are accepted without on-chain verification. + +In ECS, container logs flow to the CloudWatch log group named +`/ecs/-server` ([`infra/data.tf:88`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/infra/data.tf#L88)). Use ECS Exec +to attach to a live task when needed: + +```bash +aws ecs execute-command --cluster -cluster \ + --task --container -server \ + --interactive --command "/bin/sh" +``` + +ECS Exec requires the task role's `ssmmessages:*` actions +([`infra/iam.tf:115`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/infra/iam.tf#L115)) — already granted by default. + +### What an operator should watch + +- **`candidate` deltas exceeding the canonicalization grace period** — + indicates Miden submission isn't happening. +- **`discarded` delta rate** — small numbers are normal (race conditions); + spikes mean RPC trouble or wrong network targeting. +- **`retained` delta count** — a persistently non-zero gauge means + give-ups are outpacing reconciliation; check Miden RPC health and the + `reconcile_*` outcomes above. +- **`rpc_unavailable` / `rpc_validation_failed` rates** — Miden node + health. +- **`storage_error` rate** — DB or filesystem trouble. +- **`authentication_failed` rate** — sudden spike usually means a client + clock drift event or an attacker probing. +- **ACK pubkey on `GET /pubkey`** — should not change unless you rotated. + +There are no Terraform-managed dashboards or alarms yet — building these +out remains an open production-hardening item. + +## When all else fails + +1. Capture the server logs around the failing request (timestamps, + request IDs, error codes). +2. Capture the client SDK version and the request envelope it built. +3. Compare against Miden directly — if Miden agrees with the client and + Guardian disagrees, the operator probably has a stale or corrupted + backend. +4. Open an issue at [https://github.com/OpenZeppelin/guardian/issues](https://github.com/OpenZeppelin/guardian/issues) + with the request ID, error code, and log excerpt. diff --git a/versioned_docs/version-0.16/builder/miden-guardian/reference/_category_.json b/versioned_docs/version-0.16/builder/miden-guardian/reference/_category_.json new file mode 100644 index 00000000..1b337d5a --- /dev/null +++ b/versioned_docs/version-0.16/builder/miden-guardian/reference/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Reference", + "position": 5 +} diff --git a/versioned_docs/version-0.16/builder/miden-guardian/reference/configuration.md b/versioned_docs/version-0.16/builder/miden-guardian/reference/configuration.md new file mode 100644 index 00000000..babd4176 --- /dev/null +++ b/versioned_docs/version-0.16/builder/miden-guardian/reference/configuration.md @@ -0,0 +1,394 @@ +--- +title: "Configuration Reference" +sidebar_position: 1 +--- +Every environment variable Guardian honours, in one place. + +Use this as a lookup table — pair it with [`LOCAL_DEV.md`](../getting-started/local-dev.md) +for which combinations make sense locally and +[`SERVER_AWS_DEPLOY.md`](../operations/server-aws-deploy.md) for how the deploy script +sets them in production. + +For Terraform variables (everything under `infra/`), see +[`infra/README.md`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/infra/README.md#variables-reference) — those are a +separate surface; the deploy script translates between Terraform vars and +the runtime env vars in this document. + +## Conventions + +- **Required** means the server will refuse to start if the variable is + missing in the relevant build/feature combo. +- **Default** is what the server picks when the variable is unset. +- **Build mode** indicates which Cargo feature gate consumes the value. + Defaults builds are the no-feature filesystem build unless stated. + +## Runtime — server identity and storage + +| Variable | Default | Build mode | Notes | +|---|---|---|---| +| `DATABASE_URL` | _required_ | `postgres` | Postgres connection string. Server panics at startup if unset under `--features postgres`. TLS verification is controlled by the standard `sslmode`/`sslrootcert` parameters — see [Database TLS](#database-tls). | +| `GUARDIAN_STORAGE_PATH` | `/var/guardian/storage` | filesystem | Path for state + delta blobs. | +| `GUARDIAN_METADATA_PATH` | `/var/guardian/metadata` | filesystem | Path for accounts, auth credentials, network config. Holds `.metadata/accounts.json` (account records) and `.metadata/auth_state.json` (replay-protection timestamps). Back up and restore the two files together: the server refuses to start if `auth_state.json` is missing while migrated accounts exist, because empty replay state would re-accept previously seen requests (see [Troubleshooting](../operations/troubleshooting.md#server-fails-to-start)). | +| `GUARDIAN_KEYSTORE_PATH` | `/var/guardian/keystore` | any | Local Falcon/ECDSA key files (ACK signers and per-account creds). | +| `GUARDIAN_DB_POOL_MAX_SIZE` | `16` (code default); `32` set by the prod Terraform profile | `postgres` | Storage backend pool size. | +| `GUARDIAN_METADATA_DB_POOL_MAX_SIZE` | matches storage | `postgres` | Metadata backend pool size; usually leave equal. | +| `GUARDIAN_CANONICALIZATION_FAST_PROMOTION_ENABLED` | `true` | any | Enables the additional promotion-only pass for recent candidates. Set to `false` to use only the full canonicalization interval; AWS deployments can set Terraform variable `guardian_canonicalization_fast_promotion_enabled = false`. | +| `GUARDIAN_CANONICALIZATION_MAX_CONCURRENT_ACCOUNTS` | `10` (code default); `50` set by the prod Terraform profile | any | Accounts one canonicalization pass processes in parallel; `1` = fully sequential. Each account holds a DB connection only during its short fenced transactions — the dominant cost is a connectionless chain RPC — so this may exceed `GUARDIAN_DB_POOL_MAX_SIZE`; simultaneous write bursts just queue briefly at the pool. | +| `GUARDIAN_SERVER_FEATURES` | _build-time_ | deploy script | Comma list (`postgres`, `evm`) the deploy script compiles in. Not read at runtime — controls how the image is built. | + +Canonicalization settings apply as follows: + +| Setting | Full pass | Fast promotion | +|---|---|---| +| `check_interval_seconds` | Sets the full-pass cadence. | Stops new fast work when the next full pass is due. | +| `fast_promotion_enabled` | No effect. | Enables or disables the pass. | +| `fast_promotion_interval_seconds` | No effect. | Sets its cadence and admission window. Work already in flight may finish after the deadline. | +| `fast_promotion_window_seconds` | No effect. | Limits eligibility to recently created candidates. | +| `max_retries` | Controls when repeated verification failures discard a candidate. | Not read or modified. | +| `submission_grace_period_seconds` | Defers retry consumption for young candidates. | Not read or modified. | +| `divergence_confirmations` | Controls confirmed-divergence discard decisions. | Not read or modified. | +| `max_concurrent_accounts` | Bounds concurrent accounts. | Bounds concurrent accounts. Candidates remain sequential within each account. | + +Fast promotion is promotion-only: a miss, absent account, RPC failure, invalid +claim, or reconstruction mismatch does not change candidate status, retry count, +or divergence count. The full pass remains responsible for every retry, defer, +divergence, and discard decision. + +### Database TLS + +TLS behavior is driven entirely by the standard libpq parameters in +`DATABASE_URL`; there is no Guardian-specific TLS env var. To migrate a deployed +AWS stack to verified TLS, see +[`runbooks/enable-db-tls.md`](../runbooks/enable-db-tls.md). The same parameters +govern both the synchronous startup-migration connection and the asynchronous +runtime pools, so they always behave identically. + +| `sslmode` | `sslrootcert` | Behavior | +|---|---|---| +| _omitted_ / `disable` | _(any)_ | Plaintext, no TLS. | +| `require` | _(none)_ | Encrypted, certificate **not** verified. | +| `require` | `` | Encrypted + certificate chain verified (promoted to `verify-ca`, matching libpq). | +| `verify-ca` | `` | Encrypted + chain verified (hostname not checked). | +| `verify-full` | `` | Encrypted + chain **and** hostname verified. Recommended for managed providers. | + +- `sslrootcert=` points at a PEM CA bundle file the server can read; the + whole bundle must parse (a malformed entry fails startup). It may contain + multiple roots. +- Verifying modes **fail closed**: a missing/unreadable/empty CA bundle, an + unknown `sslmode`, `allow`/`prefer` (which permit plaintext fallback), or + `sslrootcert=system` (unsupported — needs libpq ≥16) all abort startup with an + actionable error rather than connecting insecurely. +- Hostname matching under `verify-full` is strict SAN-based; certificates without + a matching Subject Alternative Name (including Common-Name-only certs) are + rejected. + +Per-provider examples: + +```text +# AWS RDS (managed; recommended). Mount a combined bundle of the Amazon RDS CA +# roots AND the Amazon Trust Services roots — the RDS Proxy presents an ACM +# certificate chaining to Amazon Trust Services, a direct instance chains to the +# RDS CA roots. +DATABASE_URL=postgres://USER:PW@HOST:5432/guardian?sslmode=verify-full&sslrootcert=/etc/guardian/tls/rds-combined-ca.pem + +# Another managed provider (GCP Cloud SQL / Azure / Supabase / Neon …) +DATABASE_URL=postgres://USER:PW@HOST:5432/guardian?sslmode=verify-full&sslrootcert=/etc/guardian/tls/provider-ca.pem + +# Local docker compose (no TLS) +DATABASE_URL=postgres://guardian:guardian@localhost:5432/guardian +``` + +## Runtime — ACK signing and network + +| Variable | Default | Notes | +|---|---|---| +| `GUARDIAN_ENV` | _unset_ | Selects the **default** ACK secret source: `prod` → AWS Secrets Manager; anything else (or unset) → ephemeral filesystem keys, regenerated each restart. Override explicitly with `GUARDIAN_ACK_SECRET_PROVIDER` (below) — e.g. `file` for a stable identity without AWS. | +| `AWS_REGION` | _unset_ | **Required** when `GUARDIAN_ENV=prod`. Region for Secrets Manager calls. | +| `GUARDIAN_NETWORK_TYPE` | _none — **required**_ | Miden network identifier: `MidenLocal` (`local`), `MidenTestnet` (`testnet`), `MidenDevnet` (`devnet`); case-insensitive. Pins the network *identity* (bech32 address prefixes, dashboard rendering) and the default Miden RPC endpoint. The server refuses to start when it is unset or unrecognized — there is no fallback network. | +| `GUARDIAN_MIDEN_RPC_ENDPOINT` | per-network default | Overrides the Miden node RPC endpoint (self-hosted node, private RPC, sidecar container) without changing network identity. Must be an origin-only `http(s)` URL (`scheme://host[:port]`); userinfo, non-root paths, queries, and fragments are rejected because tonic does not send them as authentication. An invalid value fails startup. Startup logs the validated origin. Combining an override with `MidenTestnet`/`MidenDevnet` logs a warning (legitimate for a mirror). A configured endpoint never falls back to the network default. | +| `GUARDIAN_MIDEN_RPC_TIMEOUT_MS` | `10000` | Per-request deadline on the node channel (the same 10s default every Miden RPC surface uses). Positive integer; `0` or a malformed value fails startup. | +| `GUARDIAN_MIDEN_RPC_MAX_ATTEMPTS` | `1` (retries off) | Attempt budget for eligible idempotent node reads **outside canonicalization**. Canonicalization performs exactly one node read per observation regardless of this value — transient failures there are recovered by the next scheduled pass, so raising this budget never makes a canonicalization pass hold its lease longer. Transaction submission is never retried regardless of this value. Endpoint failover is not currently implemented. Retry activity is visible as `guardian_miden_rpc_retries_total` (never incremented by canonicalization reads). | + +> **Upgrade note:** this release reduces the default Miden RPC deadline from 30 +> seconds to 10 seconds for existing deployments. The deadline is applied at +> the channel level, so it caps submissions as well as reads even though +> submissions remain single-attempt. Set `GUARDIAN_MIDEN_RPC_TIMEOUT_MS=30000` +> to retain the previous deadline. + +ACK secret IDs are configurable. The server reads two env vars at startup +and falls back to fixed defaults when they're unset +([`crates/server/src/ack/secrets_manager.rs:10-13`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/server/src/ack/secrets_manager.rs#L10)): + +| Variable | Default | Notes | +|---|---|---| +| `GUARDIAN_ACK_FALCON_SECRET_ID` | `guardian-prod/server/ack-falcon-secret-key` | Secrets Manager name/ARN for the Falcon ACK secret key. | +| `GUARDIAN_ACK_ECDSA_SECRET_ID` | `guardian-prod/server/ack-ecdsa-secret-key` | Secrets Manager name/ARN for the ECDSA ACK secret key. Used only when the ECDSA backend is `in-memory`. | + +### ACK secret provider (stable identity without AWS) + +Outside prod the default (`GUARDIAN_ACK_SECRET_PROVIDER=none`) generates a fresh +ACK keypair on every restart, which changes the Guardian's on-chain ack-key +commitment and freezes accounts that pinned the old one. Set the provider to +`file` to load fixed keys from local files instead — a stable identity without +AWS Secrets Manager. Each file holds the hex string emitted by `ack-keygen` +(identical to what Secrets Manager stores). See the +[Secrets runbook](../runbooks/secrets.md#self-hosted-stable-identity-without-aws). + +| Variable | Default | Notes | +|---|---|---| +| `GUARDIAN_ACK_SECRET_PROVIDER` | `aws` when `GUARDIAN_ENV=prod`, else `none` | Source of the ACK signing keys: `aws` (Secrets Manager), `file` (local files), or `none` (ephemeral, dev only). An unrecognized value fails startup; `none` is rejected when `GUARDIAN_ENV=prod`. | +| `GUARDIAN_ACK_FALCON_SECRET_PATH` | _unset_ | **Required** when `GUARDIAN_ACK_SECRET_PROVIDER=file`. Path to a file holding the hex-encoded Falcon ACK secret key. On Unix the file must be owner-only (mode `0600`) or startup fails. | +| `GUARDIAN_ACK_ECDSA_SECRET_PATH` | _unset_ | **Required** when `GUARDIAN_ACK_SECRET_PROVIDER=file`, **unless** `GUARDIAN_ACK_ECDSA_BACKEND=aws-kms` (then the ECDSA key comes from KMS and this file is never read). Path to the hex-encoded ECDSA ACK secret key; same `0600` requirement. | + +### Storage encryption at rest + +Application-layer encryption of the sensitive stored payloads (account state, +delta and proposal payloads). It is **opt-in by key-source presence** — configure +a key and the server encrypts; configure none and it stores plaintext exactly as +before. Routing/index fields (account id, nonce, commitments, status, timestamps) +always stay plaintext. + +**Which variable do I set?** Choose **one key source** — the dev key for local +work, or the Secrets Manager secret for production. You never set both (doing so +is a startup error). `GUARDIAN_STORAGE_ENCRYPTION_KEY_ID` is **not** a key — it is +an optional label, and most users leave it unset. + +**Key sources** (set exactly one; presence is what turns encryption on): + +| Variable | Default | Notes | +|---|---|---| +| `GUARDIAN_STORAGE_ENCRYPTION_KEY` | _unset_ | **Dev key source.** The actual 32-byte key, base64-encoded (`openssl rand -base64 32`). | +| `GUARDIAN_STORAGE_ENCRYPTION_KEY_SECRET_ID` | _unset_ | **Prod key source.** AWS Secrets Manager name/ARN of a secret holding a structured `{ "active": kid, "keys": { kid: base64-32-bytes } }` document (one or more keys). Reuses `AWS_REGION`. | + +**Optional label:** + +| Variable | Default | Notes | +|---|---|---| +| `GUARDIAN_STORAGE_ENCRYPTION_KEY_ID` | `k1` | Only used with the dev key source. Sets the key id (`kid`) recorded on each encrypted record so the key can be identified later. The default is fine for almost everyone — leave it unset unless you specifically need the dev key's id to match an id used elsewhere. (With Secrets Manager the ids come from the secret's `keys`/`active` fields instead, so this variable is ignored.) | + +Every encrypted record stores the `kid` of the key that wrote it, and on read the +key is resolved by that id — this is what lets Secrets Manager hold several keys +and rotate them (add a new key, repoint `active`, keep the old key so existing +records still decrypt). The dev key source holds a single key, so it cannot do +multi-key rotation reads; that is a production capability. + +Rules: configure **exactly one** key source (both set → startup error). When a key +source is set the server validates it at startup and fails fast on a +missing/malformed/wrong-length key — it never silently falls back to plaintext. +Encryption is fixed for a populated store: the server records a marker on the +first encrypted write and refuses to mix plaintext and ciphertext, so enable it +against an empty store (e.g. after the Miden 0.16 reset, see +[`MIDEN_COMPATIBILITY.md`](../miden-compatibility.md#data-resets)). Switching an existing +store requires an explicit re-encryption migration (not yet provided). + +### Hosted ECDSA signer backend + +The ECDSA ACK signer can keep its private key outside the process in a hosted +backend. Falcon is unaffected and always uses the in-memory path. + +| Variable | Default | Notes | +|---|---|---| +| `GUARDIAN_ACK_ECDSA_BACKEND` | `in-memory` | `in-memory` (filesystem keystore, or Secrets Manager when `GUARDIAN_ENV=prod`) or `aws-kms`. An unrecognized value fails startup listing the supported values. | +| `GUARDIAN_ACK_ECDSA_KMS_KEY_ID` | _unset_ | **Required** when `GUARDIAN_ACK_ECDSA_BACKEND=aws-kms`. KMS key id, ARN, or alias. The key must be `ECC_SECG_P256K1` with usage `SIGN_VERIFY`. | + +With `aws-kms`, the server holds only the key handle; the private key never enters +the process. At startup it fetches the public key, validates the key spec, and +performs a sign probe to confirm `kms:Sign` permission — failing fast otherwise. +The ECDSA secret in Secrets Manager (`GUARDIAN_ACK_ECDSA_SECRET_ID`) is **not** +read on this path. Credentials resolve through the standard AWS chain (the ECS +task role in production); required IAM is `kms:GetPublicKey` and `kms:Sign` on the +key. + +> Switching an existing deployment from `in-memory` to `aws-kms` means a new +> keypair, hence a new ECDSA `pubkey`/`commitment`. Re-establish downstream trust +> accordingly. + +> **`_SECRET_ID` (runtime) vs `_SECRET_NAME` (deploy-side):** the server +> reads `GUARDIAN_ACK_*_SECRET_ID` at startup, but you typically don't +> set these by hand. The deploy script accepts +> `GUARDIAN_ACK_*_SECRET_NAME` (see the [Deploy script +> section](#deploy-script-scriptsaws-deploysh) below), passes it into +> Terraform as `guardian_ack_*_secret_name`, and Terraform sets the +> matching `_SECRET_ID` env var on the ECS task. Same value, three +> places — see the [Secrets runbook](../runbooks/secrets.md#ack-signing-keys) +> for the full override chain. + +In the reference AWS deploy, Terraform sets both `_SECRET_ID` env vars on +the ECS task to `${stack_name}/server/ack-{falcon,ecdsa}-secret-key` so +multi-stack deployments get scoped IDs. + +## Runtime — request safety + +| Variable | Default | Notes | +|---|---|---| +| `GUARDIAN_RATE_LIMIT_ENABLED` | `true` | Master kill-switch for rate limiting on **both** transports (HTTP and gRPC; there is no per-transport toggle). Set `false` only in test environments. Client identity for keying comes from the ingress (rightmost `X-Forwarded-For` entry, then `X-Real-IP`, then the socket peer); deployments not behind the reference ALB must forward the client address on both listeners, strip any client-supplied `X-Forwarded-For` if they identify callers with `X-Real-IP`, and restrict direct access to the server ports; see [PRODUCTION.md](../operations/production.md#running-behind-your-own-ingress-non-aws). | +| `GUARDIAN_RATE_BURST_PER_SEC` | `10` (code default); `200` set by the prod Terraform profile | Requests allowed in any one-second window, keyed per IP **and** endpoint, where the endpoint is the HTTP path or the gRPC method. Both transports are metered from one store, but because their endpoint names differ, a burst bucket is never shared across transports. | +| `GUARDIAN_RATE_PER_MIN` | `60` (code default); `5000` set by the prod Terraform profile | Sustained rate, keyed per IP only, so HTTP and gRPC calls from one client draw on the same allowance. This is the cross-transport limit: deployments sized for HTTP-only traffic should re-check it, since gRPC traffic (the Rust SDK's default transport) counts against it since the transport-bypass fix. | +| `GUARDIAN_MAX_REPLICAS` | `1` (code default); greater of desired count and autoscaling max when enabled, or desired count otherwise, set by Terraform | Per-replica rate-limit divisor used by global HTTP+gRPC and dashboard per-commitment limits. Each replica enforces `global / GUARDIAN_MAX_REPLICAS`, keeping the aggregate at or below the configured limit through steady-state autoscaling. During a rolling deployment the temporary aggregate may rise by up to `deployment_maximum_percent / 100` (2× by default). Drives rate-limiting only — coordination mode is backend-derived. Running below the steady-state maximum over-throttles; HTTP keep-alive can pin a client to one replica. An override is clamped up to steady-state capacity by Terraform. Must be a positive integer when set: an invalid value **fails startup in prod** and is treated as `1` with a warning elsewhere. See [`runbooks/horizontal-scaling.md`](../runbooks/horizontal-scaling.md). | +| `GUARDIAN_DASHBOARD_COMMITMENT_RATE_BURST_PER_SEC` | `6` | Fleet-wide dashboard challenge and verification burst budget for one operator commitment. Divided by `GUARDIAN_MAX_REPLICAS` and clamped to at least 1 per replica. A custom value below the divisor can therefore exceed its nominal fleet-wide budget. | +| `GUARDIAN_DASHBOARD_COMMITMENT_RATE_PER_MIN` | `30` | Fleet-wide dashboard challenge and verification sustained budget for one operator commitment. Divided by `GUARDIAN_MAX_REPLICAS` and clamped to at least 1 per replica. A custom value below the divisor can therefore exceed its nominal fleet-wide budget. | +| `GUARDIAN_MAX_REQUEST_BYTES` | `1048576` (1 MB) | Reject request bodies larger than this. | +| `GUARDIAN_MAX_PENDING_PROPOSALS_PER_ACCOUNT` | `20` | Account-level cap; hitting it returns `pending_proposals_limit`. | +| `GUARDIAN_CORS_ALLOWED_ORIGINS` | _unset_ | Comma-separated explicit origins. **Unset → permissive `Any` origin / `Any` methods / `Any` headers, credentials disabled** (suitable for local dev). **Set → strict allowlist with `allow_credentials(true)`** (required for production browser clients). | + +## Runtime — metrics (Prometheus) + +| Variable | Default | Notes | +|---|---|---| +| `GUARDIAN_METRICS_ENABLED` | `false` | Master switch for the Prometheus integration. When `false` (default) nothing runs: no metrics listener, no recorder, no storage instrumentation, no background refresher. | +| `GUARDIAN_METRICS_ADDR` | `127.0.0.1:9464` | Bind address of the **dedicated** metrics listener (separate from the API port). Loopback by default; set `0.0.0.0:9464` in containers so a Prometheus sidecar/agent can reach it. Invalid values fall back to the default with a warning. | +| `GUARDIAN_METRICS_PATH` | `/metrics` | Path serving the Prometheus text exposition on that listener. Must start with `/`. | +| `GUARDIAN_METRICS_REFRESH_INTERVAL_SECS` | `30` | Cadence of the background task that refreshes slow aggregate gauges (delta status counts, in-flight proposals, account count). Scrapes never query storage directly. | +| `GUARDIAN_METRICS_BEARER_TOKEN` | _unset_ | Optional shared-secret scrape token. When set, scrapes must send `Authorization: Bearer ` (Prometheus `authorization.credentials` in the scrape config); anything else gets `401`. Compared in constant time, held as a non-loggable secret wrapper in process. | + +The metrics endpoint is intentionally **not** part of the main API +router: it bypasses rate limiting and CORS and is protected by network +isolation first (loopback default / private network / security group), +the bearer token second, and proxy-terminated TLS where transport +encryption is required. Never expose it to a public network. The +exposed metric taxonomy and cardinality rules are documented in +[`spec/api.md`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/spec/api.md); see the +[Observability guide](../guides/observability.md) for scraping and a +one-command Grafana dashboard stack. + +## Runtime — dashboard + +| Variable | Default | Notes | +|---|---|---| +| `GUARDIAN_OPERATOR_PUBLIC_KEYS_SECRET_ID` | _unset_ | AWS Secrets Manager secret name/ARN holding the operator allowlist JSON. Hot-reloaded on every challenge and authenticated `/dashboard/*` request. | +| `GUARDIAN_OPERATOR_PUBLIC_KEYS_FILE` | _unset_ | Local JSON path for the same payload. Local dev only. | +| `GUARDIAN_DASHBOARD_CURSOR_SECRET` | random per process if unset | 32-byte hex HMAC key for dashboard pagination cursors. Pin a shared value across replicas so cursors validate everywhere. The prod Terraform profile injects a pre-created Secrets Manager value into every task. If unset outside that profile, the server **warns** and generates an ephemeral per-process key and still boots (in every stage); an ephemeral key breaks pagination cursors across replicas and restarts — the dashboard feeds and the client `GET /delta/history` endpoint — nothing else, so it is not a startup guard. | + +`GET /dashboard/info.environment` is derived from `GUARDIAN_NETWORK_TYPE` +(`testnet`, `devnet`, or `local`) rather than configured separately. + +### Prod-stage startup guards & HA behavior + +When `GUARDIAN_ENV=prod`, the server fails fast on misconfigurations that are +silently broken across replicas: + +- the **filesystem** storage backend is refused (single-instance only — use the + Postgres image with `DATABASE_URL`); +- a rate limit that partitions to **0 requests per replica** is refused — i.e. + the global `GUARDIAN_RATE_BURST_PER_SEC`/`GUARDIAN_RATE_PER_MIN` is below + `GUARDIAN_MAX_REPLICAS`, which would make every replica throttle all traffic. + Raise the global limit or lower `GUARDIAN_MAX_REPLICAS`. (Non-prod only warns.) + +On the Postgres backend, operator/EVM sessions, login challenges, and the +canonicalization lease are shared across replicas (backend-derived — no tunable +disables this). If the database is briefly unavailable, authentication **fails +closed** (rejected, never bypassed) and recovers automatically. See the +[horizontal-scaling runbook](../runbooks/horizontal-scaling.md). + +Allowlist payload shapes and enrollment flow: +[`docs/DASHBOARD.md`](../operations/dashboard.md). + +## Runtime — EVM (feature-gated) + +These take effect only when the server is built with `--features evm`. +The server reads only the two variables in this table; the allowed chain +set is **derived from the keys of `GUARDIAN_EVM_RPC_URLS`** rather than a +separate variable. + +| Variable | Default | Notes | +|---|---|---| +| `GUARDIAN_EVM_RPC_URLS` | _unset_ (treated as an empty registry) | Comma list `chain_id=rpc_url`. E.g. `1=https://…,11155111=https://…`. Allowed chain IDs are the keys of this map. **Required for usable EVM chains** — when unset, the server starts but the EVM registry is empty and every chain ID will be rejected. | +| `GUARDIAN_EVM_ENTRYPOINT_ADDRESS` | `0x433709009b8330fda32311df1c2afa402ed8d009` (EntryPoint v0.9) | Shared EntryPoint address used for finality checks across chains. | + +## Logging + +| Variable | Default | Notes | +|---|---|---| +| `RUST_LOG` | `info` | Standard `tracing-subscriber` filter. Module-scoped filters work: `RUST_LOG=server::jobs::canonicalization=debug`. Hot-path request *events* (`get_state`, `get_delta`, `push_delta`, proposal create/sign, `lookup_account`) are `debug`; use `RUST_LOG=server=debug` or `RUST_LOG=server::services=debug` to see them. At `info` each request instead emits a single span-close line carrying the span's fields (account ID, nonce, commitment, signer/match counts) plus `time.busy` / `time.idle`. That line is emitted on the success *and* the error path, which is what correlates the centralized 5xx log (see below) back to an account. | +| `GUARDIAN_LOG_FORMAT` | `text` | Log output format. `text` — human-readable (ANSI when TTY, plain otherwise). `json` — flattened JSON with span context for CloudWatch Logs Insights. `compact` — single-line text. Value is trimmed and case-insensitive; unknown values fall back to `text` with a stderr warn (emitted before the tracing subscriber is installed). Defaults to `text` when unset. ECS default is `json` (see `infra/variables.tf` `guardian_log_format`). | + +The 5xx and 4xx lines emitted from `GuardianError`'s HTTP `IntoResponse` and +`tonic::Status` conversions run *after* the service span has closed, so they +carry only `code` and `detail`, never account fields. The preceding span-close +line is what supplies the account context for them. + +Useful filters during debugging — see +[`TROUBLESHOOTING.md`](../operations/troubleshooting.md#logging-and-observability). + +## Deploy script (`scripts/aws-deploy.sh`) + +These are read by the deploy script, not by the server itself. The script +turns them into Terraform variables or build-time choices. + +| Variable | Default | Notes | +|---|---|---| +| `STACK_NAME` | `guardian` | Base name for all AWS resources and Terraform state file. | +| `DEPLOY_STAGE` | `dev` | `dev` or `prod`; selects stage profile (autoscaling, RDS Proxy, etc.). | +| `CPU_ARCHITECTURE` | `X86_64` | `X86_64` or `ARM64`. Picks the Docker buildx platform and the ECS task arch. | +| `AWS_REGION` | _required_ | All AWS API calls. | +| `DOMAIN_NAME` | `openzeppelin.com` | Root domain for the canonical public hostname. | +| `SUBDOMAIN` | `guardian` | Host portion of the public hostname. | +| `ACM_CERTIFICATE_ARN` | _unset_ | ACM certificate for HTTPS on the canonical hostname. | +| `ROUTE53_ZONE_ID` | _unset_ | Optional Route 53 hosted zone for an alias record. | +| `CLOUDFLARE_ZONE_ID` | _unset_ | Optional Cloudflare zone for CNAME management. | +| `CLOUDFLARE_API_TOKEN` | _unset_ | Required when either primary or secondary Cloudflare DNS management is enabled. | +| `CLOUDFLARE_PROXIED` | `true` | Whether the Cloudflare CNAME should be proxied. | +| `ALIAS_SUBDOMAIN` | _unset_ | Migration-only legacy subdomain under `DOMAIN_NAME`; leave unset for normal deployments. DNS may be Terraform-managed or external. | +| `ALIAS_ACM_CERTIFICATE_ARN` | `ACM_CERTIFICATE_ARN` | Migration-only distinct certificate for the legacy hostname, attached through SNI when needed. | +| `GUARDIAN_ACK_FALCON_SECRET_NAME` | _unset_ → `${STACK_NAME}/server/ack-falcon-secret-key` | Deploy-side override for the Falcon ACK secret. Passed into Terraform as `guardian_ack_falcon_secret_name` and set on the ECS task as the runtime `GUARDIAN_ACK_FALCON_SECRET_ID`. | +| `GUARDIAN_ACK_ECDSA_SECRET_NAME` | _unset_ → `${STACK_NAME}/server/ack-ecdsa-secret-key` | Deploy-side override for the ECDSA ACK secret. Same flow as the Falcon entry above. | +| `GUARDIAN_OPERATOR_PUBLIC_KEYS_JSON` | _unset_ | Inline JSON array of operator pubkeys; Terraform creates the secret from this. Mutually exclusive with `GUARDIAN_OPERATOR_PUBLIC_KEYS_SECRET_ARN`. | +| `GUARDIAN_OPERATOR_PUBLIC_KEYS_SECRET_ARN` | _unset_ | ARN of an externally-managed operator pubkeys secret. When set, Terraform does not create one and the task reads from this ARN instead. | +| `GUARDIAN_EVM_CHAIN_CONFIG_FILE` | _unset_ | Path to a JSON file the deploy script reads to derive `GUARDIAN_EVM_RPC_URLS` (and the bookkeeping `GUARDIAN_EVM_ALLOWED_CHAIN_IDS` Terraform variable). Not read by the server. | +| `GUARDIAN_EVM_ALLOWED_CHAIN_IDS` | _unset_ | Comma list of chain IDs used **only by Terraform** for bookkeeping / secret naming. The server itself derives allowed chains from `GUARDIAN_EVM_RPC_URLS` keys. | +| `GUARDIAN_EVM_RPC_URLS_SECRET_ARN` | _unset_ | ECS-injection only: when set, the ECS task reads `GUARDIAN_EVM_RPC_URLS` from this Secrets Manager ARN at task start (the server still sees a plain env var). | +| `GUARDIAN_EVM_ALLOWED_CHAIN_IDS_SECRET_ARN` | _unset_ | Same, for the bookkeeping chain-ID list. | +| `TF_VAR_*` | _unset_ | Any standard Terraform var override; the script passes through. | + +## Secrets in env vars — scope of in-process protection + +Secret-bearing env vars (`DATABASE_URL`, `GUARDIAN_DASHBOARD_CURSOR_SECRET`, +`GUARDIAN_EVM_RPC_URLS`) are wrapped at the point of read into +zero-on-drop, no-`Display`/no-`Serialize` types inside the server +process, so they cannot accidentally appear in logs, panic messages, or +serialized responses. **This does not protect the OS process +environment block** — `/proc//environ`, coredumps, fork-inherited +env, and ECS task-definition `environment` fields all remain visible at +the OS layer regardless. For the highest-sensitivity material (ACK +signing keys) prefer the AWS Secrets Manager runtime-fetch path +(`GUARDIAN_ACK_FALCON_SECRET_ID` / `GUARDIAN_ACK_ECDSA_SECRET_ID`), +which is already how production loads those keys. + +## What's _not_ env-configurable + +A few things are deliberately compile-time or builder-API only — knowing +this saves you from grepping: + +- **HTTP / gRPC ports.** `3000` and `50051` are builder defaults + ([`builder/mod.rs:68`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/server/src/builder/mod.rs#L68)); + configurable through the Rust builder but not via env. ECS pins these + in the task definition. +- **Storage backend choice.** Cargo feature `postgres` (or its absence), + not an env var. See + [Storage modes](../architecture/services.md#storage-modes). +- **EVM support.** Cargo feature `evm`. If the binary wasn't built with + it, no env var will turn it on. +- **Canonicalization knobs** (`check_interval_seconds`, + `fast_promotion_interval_seconds`, `fast_promotion_window_seconds`, + `max_retries`, `submission_grace_period_seconds`, + `divergence_confirmations`). Currently hard-coded in the canonicalization + worker; require a code change to alter. The exceptions are + `fast_promotion_enabled` and `max_concurrent_accounts`, configurable via + `GUARDIAN_CANONICALIZATION_FAST_PROMOTION_ENABLED` and + `GUARDIAN_CANONICALIZATION_MAX_CONCURRENT_ACCOUNTS` (see the + environment table above). +- **Auth timestamp window.** `MAX_TIMESTAMP_SKEW_MS = 300_000` (5 min) is + hard-coded in + [`metadata/auth/credentials.rs:6`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/server/src/metadata/auth/credentials.rs#L6). + +## Quick combos + +| I want… | Set | +|---|---| +| Minimum local dev | _nothing_ — `docker compose up` works | +| Postgres backend locally | `DATABASE_URL=…` + build with `--features postgres` | +| EVM support locally | `GUARDIAN_EVM_RPC_URLS` (allowed chain set derives from its keys) + build with `--features evm` | +| Use Secrets Manager for ACK keys | `GUARDIAN_ENV=prod` + `AWS_REGION=` + secrets pre-created | +| Run the dashboard locally | `GUARDIAN_OPERATOR_PUBLIC_KEYS_FILE=/path/to/allowlist.json` | +| Multi-replica (HA) | Postgres backend + `GUARDIAN_DASHBOARD_CURSOR_SECRET=<64 hex>` pinned across tasks + `GUARDIAN_MAX_REPLICAS=`. The prod Terraform profile sets these after `bootstrap-dashboard-cursor-secret` creates the required Secrets Manager entry. | +| Higher throughput in prod | `GUARDIAN_RATE_BURST_PER_SEC`, `GUARDIAN_RATE_PER_MIN`, `GUARDIAN_DB_POOL_MAX_SIZE`, `GUARDIAN_CANONICALIZATION_MAX_CONCURRENT_ACCOUNTS` | diff --git a/versioned_docs/version-0.16/builder/miden-guardian/reference/multisig-sdk.md b/versioned_docs/version-0.16/builder/miden-guardian/reference/multisig-sdk.md new file mode 100644 index 00000000..9b44801e --- /dev/null +++ b/versioned_docs/version-0.16/builder/miden-guardian/reference/multisig-sdk.md @@ -0,0 +1,1996 @@ +--- +title: "Miden Multisig SDK" +sidebar_position: 3 +--- +An SDK for creating and managing multisignature accounts on the Miden network. Available for both **TypeScript** (web/browser) and **Rust** (native/server) environments. + +> New to Guardian? Read [`docs/CONCEPTS.md`](../getting-started/concepts.md) for the trust +> model and state/delta lifecycle, and +> [`docs/architecture/services.md`](../architecture/services.md) for the +> server-side surface this SDK targets. + +## Table of Contents + +- [Quick Start](#quick-start) +- [Core Concepts](#core-concepts) +- [TypeScript SDK Guide](#typescript-sdk-guide) +- [Rust SDK Guide](#rust-sdk-guide) +- [Use Cases](#use-cases) +- [Offline Workflow](#offline-workflow) +- [Releasing](#releasing) + +--- + +## Quick Start + +### Installation + +The multisig sdk has as peer dependency on the miden-sdk, you will need to install both. + +**TypeScript (npm)** +```bash +npm install @openzeppelin/miden-multisig-client @miden-sdk/miden-sdk@0.16.0 +``` + +**Rust (Cargo.toml)** +```toml +[dependencies] +miden-multisig-client = "0.17.0" +miden-client = "=0.16.0" +``` + +### 5-Minute Example + +Create a 1-of-3 multisig account, propose a transfer, collect signatures, and execute. + +#### TypeScript + +```typescript +import { MidenClient, AuthSecretKey } from '@miden-sdk/miden-sdk'; +import { MultisigClient, FalconSigner } from '@openzeppelin/miden-multisig-client'; + +// 1. Setup clients +const midenClient = await MidenClient.createDevnet(); +const secretKey = AuthSecretKey.rpoFalconWithRNG(undefined); +const signer = new FalconSigner(secretKey); +const client = new MultisigClient(midenClient, { + guardianEndpoint: 'http://localhost:3000', + midenRpcEndpoint: 'https://rpc.devnet.miden.io', +}); +// Both endpoints are required; construction throws when either is omitted. +// midenRpcEndpoint must point at the same network as the injected MidenClient. + +// 2. Get GUARDIAN server public key +const guardianCommitment = await client.guardianClient.getPubkey(); + +// 3. Create 1-of-3 multisig account +const config = { + threshold: 1, + signerCommitments: [signer.commitment, cosigner1Commitment, cosigner2Commitment], + guardianCommitment, +}; +const multisig = await client.create(config, signer); +await multisig.registerOnGuardian(); + +console.log('Account created:', multisig.accountId); + +// 4. Create a transfer proposal +const proposal = await multisig.createP2idProposal( + recipientAccountId, + faucetAccountId, + 1000n // amount +); + +console.log('Proposal created:', proposal.id); + +// 5. Cosigners sign (only one cosigner is needed) +await multisig.signProposal(proposal.id); + +// 6. Execute when threshold is met +await multisig.executeProposal(proposal.id); + +console.log('Transfer executed!'); +``` + +### Prover endpoint and retry policy + +Both multisig SDKs retry only remote transaction proving. The default is two +total proof attempts. In TypeScript, endpoint-less injected provers, including +local and callback provers, run once. A custom remote URL overrides the Miden +client's injected prover. + +```typescript +const client = new MultisigClient(midenClient, { + guardianEndpoint: 'http://localhost:3000', + midenRpcEndpoint: 'https://rpc.devnet.miden.io', + prover: { + url: 'https://prover.example', + retry: { maxAttempts: 4 }, + }, +}); +``` + +```rust +use miden_multisig_client::{ProverConfig, ProverRetryPolicy}; + +let prover = ProverConfig::new() + .with_url("https://prover.example")? + .with_retry_policy(ProverRetryPolicy::new(4)); + +let client = MultisigClient::builder() + .miden_endpoint(Endpoint::devnet()) + .guardian_endpoint("http://localhost:50051") + .account_dir("/tmp/multisig-client") + .prover_config(prover) + .generate_key() + .build() + .await?; +``` + +URLs are validated during construction and must be absolute HTTP(S) URLs. A +custom prover never falls back to a default endpoint. Retries cover transient +proving conditions such as cancellation, deadlines, temporary unavailability, +capacity exhaustion, HTTP 408/429/502/503/504, I/O timeout, connection reset, +and broken pipe. Permanent or unrecognized failures return immediately. + +Only proving is retried by this policy: transaction execution, GUARDIAN +coordination, Miden submission, and local application each run once. A larger +attempt budget can recover from brief failures but does not add prover +capacity. Idempotent Miden node reads have their own opt-in retry policy — see +the next section. + +### Miden RPC retry policy + +Both multisig SDKs retry idempotent Miden node reads under an `rpc` policy +that mirrors the prover configuration. The default is two total attempts — +one classified, jittered retry — matching the prover policy's default; an +explicit `maxAttempts` of 1 opts out. Transaction submission is **never +retried**, under any configuration: a submission whose outcome is unknown +could execute twice if re-sent. + +Coverage differs by language. In Rust, the policy wraps the node client +itself, so every node read the SDK or the underlying Miden client issues — +syncs, account, note, and block lookups — retries under it. The same policy +also covers the private-note transport: note fetches and stream +establishment retry, while **note sends are never retried in-call** (the +client's relay outbox re-sends undelivered notes on later syncs, so an +in-call resend could deliver twice). Configuring the transport endpoint +itself is covered in [Note transport endpoint](#note-transport-endpoint). +Connection failures retry only when the cause is transport-shaped; TLS, +certificate, and invalid-endpoint problems fail immediately. In TypeScript, +the policy wraps the reads the SDK issues directly: on-chain account lookups, +state-commitment verification, and the guardian-switch node sync; syncs you +perform on the injected Miden client are owned by your application. As part +of this policy the Rust SDK also disables the Miden client library's internal +transport-retry loop, which silently retransmitted rate-limited requests — +**including submissions** — up to four extra times. This explicit policy is +the only retry layer: reads get one retry by default, submissions never. + +```typescript +const client = new MultisigClient(midenClient, { + guardianEndpoint: 'http://localhost:3000', + midenRpcEndpoint: 'https://rpc.devnet.miden.io', + prover: { retry: { maxAttempts: 4 } }, + rpc: { retry: { maxAttempts: 3 } }, +}); +``` + +```rust +use miden_multisig_client::{RpcConfig, RpcRetryPolicy}; + +let rpc = RpcConfig::new() + .with_timeout_ms(15_000)? + .with_retry_policy(RpcRetryPolicy::new(3)); + +let client = MultisigClient::builder() + .miden_endpoint(Endpoint::devnet()) + .guardian_endpoint("http://localhost:50051") + .account_dir("/tmp/multisig-client") + .rpc_config(rpc) + .generate_key() + .build() + .await?; +``` + +The classifier is shared with the prover policy (same transient/permanent +partition and backoff), extended with the node's transport renderings: a +rate-limit rejection (`Too Many Requests!`), and connection failures the node +reports under an `Unknown` status (`i/o timeout`, `connection error`, +`transport error`). Permanent failures — invalid arguments, not-found, +authentication — return immediately without consuming the budget, and once +the budget is exhausted the final upstream error is returned unchanged. A +larger budget adds patience against a rate-limited or briefly unreachable +node; it does not add node capacity. + +In Rust, the per-request gRPC deadline defaults to 10 seconds on every SDK +node connection (presets, custom endpoints, and the direct commitment +reads); `with_timeout_ms` replaces it uniformly. +The TypeScript configuration intentionally has **no timeout**: the browser +WASM RPC client cannot cancel an in-flight request, and a JavaScript-side +timeout would abandon the call — whose side effects may still land — while +reporting failure. Retry behavior itself is fixture-verified to be identical +across both SDKs. + +The Guardian server exposes the same policy for its own node reads via +`GUARDIAN_MIDEN_RPC_ENDPOINT`, `GUARDIAN_MIDEN_RPC_TIMEOUT_MS`, and +`GUARDIAN_MIDEN_RPC_MAX_ATTEMPTS` — see +[docs/CONFIGURATION.md](./configuration.md). + +### Note transport endpoint + +Private notes are relayed through a note transport service that is separate +from the node RPC. On the testnet and devnet presets both SDKs derive the +transport endpoint automatically; a custom node endpoint has no derivable +transport service, so it must be set explicitly — otherwise private-note +relay is disabled. + +In Rust the endpoint lives on the builder, next to the node endpoint: + +```rust +let client = MultisigClient::builder() + .miden_endpoint(Endpoint::try_from("https://my-node.internal:57291")?) + .note_transport_endpoint("https://my-transport.internal") + .guardian_endpoint("http://localhost:50051") + .account_dir("/tmp/multisig-client") + .generate_key() + .build() + .await?; +``` + +In TypeScript the injected Miden client owns note relay, so the endpoint is +set when constructing it, not on the `MultisigClient` configuration: + +```typescript +import { MidenClient } from '@miden-sdk/miden-sdk'; +import { MultisigClient } from '@openzeppelin/miden-multisig-client'; + +const midenClient = await MidenClient.create({ + rpcUrl: 'https://my-node.internal:57291', + noteTransportUrl: 'https://my-transport.internal', +}); + +const client = new MultisigClient(midenClient, { + guardianEndpoint: 'http://localhost:3000', + midenRpcEndpoint: 'https://my-node.internal:57291', +}); +``` + +`noteTransportUrl` also accepts the `'testnet'` and `'devnet'` shorthands, so +a custom node on a public network can reuse that network's public transport +service. In both SDKs the transport shares the node RPC resilience policy +described above. + +#### Rust + +```rust +use miden_multisig_client::{MultisigClient, TransactionType, Endpoint}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // 1. Setup client + let mut client = MultisigClient::builder() + // the Miden node RPC endpoint + .miden_endpoint(Endpoint::new("http://localhost:57291")) + // the GUARDIAN server endpoint + .guardian_endpoint("http://localhost:50051") + // the directory where the miden-client will store the account data + .account_dir("/tmp/multisig-client") + // generate a new Falcon keypair for GUARDIAN authentication + .generate_key() + .build() + .await?; + + // 2. Create 1-of-3 multisig account + let signer_commitments = vec![ + client.user_commitment(), + cosigner1_commitment, + cosigner2_commitment, + ]; + let account = client.create_account(1, signer_commitments).await?; + client.push_account().await?; + + println!("Account created: {}", account.id()); + + // 3. Create a transfer proposal + let tx = TransactionType::transfer(recipient_id, faucet_id, 1000); + let proposal = client.propose_transaction(tx).await?; + + println!("Proposal created: {}", proposal.id); + + // 4. Cosigners sign (only one cosigner is needed) + client.sign_proposal(&proposal.id).await?; + + // 5. Execute when threshold is met + client.execute_proposal(&proposal.id).await?; + + println!("Transfer executed!"); + Ok(()) +} +``` + +--- + +## Core Concepts + +### Multisig Accounts + +A multisig account requires **M-of-N** signatures to authorize transactions: +- **Threshold (M)**: Minimum signatures required +- **Signers (N)**: Total number of authorized cosigners +- **Commitment**: Each signer's Falcon public key commitment (32 bytes, 64 hex chars) + +### Guardian + +GUARDIAN is a coordination server that: +- Stores the account state off-chain +- Coordinates proposal signing between cosigners +- Provides acknowledgment signatures for on-chain execution (ensures the new state is available for the rest of the cosigners) +- Keeps multisig metadata private + +> **Note**: GUARDIAN server setup is covered in separate documentation. This SDK assumes a running GUARDIAN instance. + +### Proposal Lifecycle + +``` +┌──────────┐ ┌──────────┐ ┌───────────┐ +│ PENDING │ ──► │ READY │ ──► │ FINALIZED │ +└──────────┘ └──────────┘ └───────────┘ + │ │ │ + Collecting Threshold Executed + signatures met on-chain +``` + +**States:** +- **Pending**: Proposal created, collecting signatures (shows X/Y signed) +- **Ready**: Threshold met, can be executed +- **Finalized**: Executed on-chain or discarded + +#### Chain-anchored execution + +Since Miden protocol 0.16 a signed transaction summary binds the reference +block commitment, so a summary produced at one block cannot be reproduced by +re-executing at a later one. Proposals therefore carry a **chain anchor** +(`chain_anchor` in the proposal metadata): a serialized Miden `ChainAnchor` +capturing the reference block the proposer executed at. Cosigners verify and +the executor executes against that anchor, so everyone reproduces the exact +summary the signatures authorize regardless of their own sync height. The +anchor is validated on receipt — its internal consistency at deserialization, +and its block commitment against the one signed into the summary — before +anything executes against it. A proposal without an anchor cannot be verified +or executed. + +### Custom Proposal Types + +Guardian accepts any non-empty `proposal_type`, not just the first-party +operations (issue #266). A proposal whose type the SDK does not model is +exposed as the **custom** bucket — `TransactionType::Custom` in Rust, +`proposalType: 'custom'` in TypeScript — while the label is preserved +(Rust `ProposalMetadata.proposal_type`, TypeScript `CustomProposalMetadata.rawProposalType`) +so it can be displayed. The SDK normalizes the label to lowercase `snake_case` +(trim + lowercase, then require `[a-z0-9_]+` — the same shape as built-in +labels), so `b2agg` is accepted, `B2Agg` is lowercased to `b2agg`, and +`add signer` / `add-signer` are rejected. (Normalization is SDK-side; the server +itself still accepts any non-empty string.) + +Custom proposals can be listed, displayed, signed, and exported/imported. + +**Producer API (issue #266).** The integration that owns a custom type builds +its own transaction and drives the create + execute ends; the SDK never +executes a transaction it does not understand. The model is **symmetric across +Rust and TypeScript**: + +- **Create** — `propose_custom_transaction(transaction_request_bytes, proposal_type)` (Rust) / + `createCustomProposal(transactionRequestBytes, proposalType)` (TS). The bytes are a + serialized transaction request; the SDK derives the summary and pushes the + proposal with the custom label. They are **not** stored on the server. + Cosigners then review and sign through the normal flow. + + On a chain whose `verification_base_fee` is non-zero, the request must declare + its fee conversion salt. Call `fee_conversion_salt(salt)` in Rust or + `withFeeConversionSalt(salt)` in TypeScript. The Miden client derives the native + 1:1 conversion info from the execution reference header and commits it to the + auth argument. The typed proposal builders do this for you. A custom producer + must retain the original salt and use it again when rebuilding the request. + Do not pass `summaryAuthArg(summary)` to `withFeeConversionSalt`: the summary + contains the derived commitment, not the original salt. +- **Execute** — `prepare_custom_execution(proposal_id, transaction_request_bytes)` (Rust) / + `prepareCustomExecution(proposalId, transactionRequestBytes)` (TS). The SDK verifies the + proposal is ready, binding-checks the request against the signed commitment + (before any acknowledgment request), fetches the GUARDIAN ack, and returns the + **advice** (cosigner signatures + ack). The integration injects that advice + into its own transaction and submits via its own client: + + ```ts + // TypeScript: rebuild via the integration's builder (the wasm request is immutable) + const advice = await multisig.prepareCustomExecution(proposalId, transactionRequestBytes); + const finalReq = myBuilder.extendAdviceMap(advice).build(); + await multisig.submitTransaction(proposalId, finalReq); + ``` + ```rust + // Rust: inject into the request's advice map, submit via the SDK helper + let advice = client.prepare_custom_execution(&proposal_id, &transaction_request_bytes).await?; + let mut req = deserialize_transaction_request(&transaction_request_bytes)?; + req.advice_map_mut().extend(advice); + client.submit_transaction(&proposal_id, req).await?; + ``` + +The SDK owns the security-critical pieces (binding check, signature + ack +assembly, ack-after-binding ordering); the integration owns only the +transaction recipe + submit. `execute_proposal` on a custom type returns a +clear error pointing to `prepare_custom_execution`. Because the integration must +rebuild its transaction to execute, **custom execution is performed by a party +that holds the recipe** (typically the producer), not by an arbitrary cosigner. + +The returned advice is keyed by the signer and GUARDIAN commitments +(domain-separated digests over the signed `tx_summary`), the same keys the +SDK's own built-in execution uses. Extending a transaction's advice map with it +therefore does not collide with the transaction's ordinary inputs; the +integration extends rather than replaces its advice map. + +> **Security:** for first-party types the SDK reconstructs the transaction from +> metadata and checks it against the signed `tx_summary` commitment. For custom +> types there is no such reconstruction, so the SDK cannot verify that display +> metadata (e.g. `description`) matches what the transaction actually does. +> Cosigners must verify the raw `tx_summary` they are signing — not trust the +> label or description. + +### Offline Workflow + +For air-gapped or offline signing scenarios: + +``` +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ +│ Proposer │ │ Cosigner │ │ Executor │ +│ (Online) │ │ (Air-gapped)│ │ (Online) │ +└──────┬──────┘ └──────┬──────┘ └──────┬──────┘ + │ │ │ + │ Export proposal.json │ │ + │──────────────────────►│ │ + │ │ │ + │ │ Sign offline │ + │ │ │ + │ │ Export signed.json │ + │ │──────────────────────►│ + │ │ │ + │ │ Import & Execute + ▼ ▼ ▼ +``` + +--- + +## TypeScript SDK Guide + +### Installation & Setup + +```typescript +import { MidenClient, AuthSecretKey } from '@miden-sdk/miden-sdk'; +import { + MultisigClient, + Multisig, + FalconSigner, + AccountInspector, + type MultisigConfig, +} from '@openzeppelin/miden-multisig-client'; + +// Initialize Miden client (connects to Miden node) +const midenClient = await MidenClient.createDevnet(); + +// Create signer from secret key +const secretKey = AuthSecretKey.rpoFalconWithRNG(undefined); +const signer = new FalconSigner(secretKey); + +// Initialize multisig client +const client = new MultisigClient(midenClient, { + guardianEndpoint: 'http://localhost:3000', + midenRpcEndpoint: 'https://rpc.devnet.miden.io', +}); +``` + +### Creating Accounts + +```typescript +// Get GUARDIAN server's public key commitment +const guardianCommitment = await client.guardianClient.getPubkey(); + +// Define multisig configuration +const config: MultisigConfig = { + threshold: 2, // 2 signatures required + signerCommitments: [ // 3 authorized signers + signer.commitment, // Your commitment + '0x1234...abcd', // Cosigner 1 + '0x5678...efgh', // Cosigner 2 + ], + guardianCommitment, // GUARDIAN server commitment +}; + +// Create the account +const multisig = await client.create(config, signer); + +// Register with GUARDIAN (stores initial state) +await multisig.registerOnGuardian(); + +console.log('Account ID:', multisig.accountId); +console.log('Threshold:', multisig.threshold); +console.log('Signers:', multisig.signerCommitments); +``` + +### Loading Existing Accounts + +```typescript +// Load as a cosigner joining an existing multisig +const multisig = await client.load(accountId, signer); + +// Fetch latest state from GUARDIAN +const state = await multisig.fetchState(); + +// Inspect account configuration +const detected = AccountInspector.fromBase64(state.stateDataBase64); +console.log('Threshold:', detected.threshold); +console.log('Signers:', detected.signerCommitments); +console.log('Vault balances:', detected.vaultBalances); +``` + +### Delta History + +Guardian retains the account's canonical delta history, allowing a wallet to +render its history after recovery. `deltaHistory()` returns one +page at a time, newest-first by nonce, with server-decoded input and output +note summaries: note ID, P2ID/P2IDE/swap/mint/burn classification, note +visibility (`noteType`), assets, and sender or recipient when exposed by the +note script. Every entry carries `status: 'canonical'` today; the set widens +if the feed gains a status filter. + +```typescript +let cursor: string | undefined; +do { + const page = await multisig.deltaHistory({ limit: 50, cursor }); + for (const entry of page.entries) { + console.log(`nonce ${entry.nonce} at ${entry.timestamp}`); + for (const note of entry.outputNotes) { + console.log(` sent ${note.tag} note ${note.noteId}`); + } + if (entry.decodeWarnings.length > 0) { + // Payload predates the current summary format; sections are empty. + } + } + cursor = page.nextCursor; +} while (cursor !== undefined); +``` + +`limit` accepts 1–500 (default 50). Only canonical (confirmed) +transactions appear — pending proposals live on `syncProposals()`. The +feed is served even while the account is paused. Guardian only ever +sees transactions pushed through it, so history of transactions the +account executed elsewhere is not included. Output notes whose full +details are not in the stored summary (e.g. private notes carried as +partial notes) appear with `tag: 'custom'` and no recipient. + +### Recovering Notes After Device Loss + +Normal forward sync cannot see notes that landed behind the store's +cursors. After `load()` on a recovered account, `multisig.recoverNotes()` +runs the three recovery strategies as one flow — the private-note transport +drain, the proposal-embedded note import, and the historical public-note +backfill — and finishes with a normal sync (transport fetch, chain sync, +and GUARDIAN state sync) so imported notes are verified and ready to +consume. The flow is the one public entry point: the strategies are +internal, so a caller cannot accidentally skip the context they need (a +tracked account, a synced store) or the final verifying sync. + +```typescript +const multisig = await client.load(accountId, signer); + +const report = await multisig.recoverNotes(); +console.log(`recovered ${report.imported} notes`); +for (const problem of report.problems) { + // A strategy that could not run at all lands here; the flow continues + // with the remaining strategies either way. + console.warn(`step ${problem.step} did not run: ${problem.reason}`); +} +if (report.retryable) { + // The flow is idempotent — rerun it to plausibly recover more. +} +``` + +Strategies are individually selectable and the backfill's block range can be +bounded: + +```typescript +// Rescan only the transport backlog and recent public history, skipping the +// proposal import and the final verifying sync. +const report = await multisig.recoverNotes({ + proposalImport: false, + fromBlock: 1_690_000, + syncAfter: false, +}); +``` + +The combined `NoteRecoveryReport` carries each strategy's own report +(`transport`, `proposalImport`, `backfill`) untouched, plus the `problems` +list, `synced`, the total `imported` count, and an aggregate `retryable` +flag. + +#### The transport-drain strategy + +A fresh store has no note-transport cursor — and in a store shared with +other accounts, sync may have advanced the cursor past private notes +addressed to the recovered account. The drain rescans the full transport +backlog for every tracked note tag (running as many transport syncs as the +upstream per-sync tag backfill cap requires), regardless of the stored +cursor, and never regresses it. It is idempotent, and a failed drain +restores the covered-tags bookkeeping so normal sync keeps working exactly +as before the attempt. + +Transport problems land in `report.transport`, never as exceptions: +`unavailable` means no transport endpoint is configured or the transport +could not be reached before anything was imported; `failed` with +`retryable: true` keeps any partial progress and rerunning continues it. + +**Not a backup:** transport recovery is bounded by the transport service's +retention. Senders may deliver private notes out-of-band without using the +transport, and relayed blobs are pruned after the retention window. Notes +outside both are recoverable only from their sender (see the note +export/import helpers). + +#### The proposal-import strategy + +v2 `consume_notes` proposals embed the serialized notes they consume, so +pending proposals are opportunistic recovery material: the strategy syncs +them from GUARDIAN (isolating per-proposal parse or binding failures as +`invalid` outcomes instead of failing the whole step), validates each +embedded note against the proposal's declared note ids, fetches on-chain +inclusion proofs, and imports per note — so it works for private notes too; +the node never needs to hold the note body. + +Each unique embedded note gets a `NoteImportOutcome` in +`report.proposalImport` (`imported`, `already-present`, `already-consumed`, +`not-committed`, `invalid`, or `failed`); duplicates across proposals fold +into one outcome and no per-note problem blocks the others. A note not yet +on chain is recorded as expected with its sync-hint tag so a later sync +picks it up, and an import whose inclusion proof fails verification against +the authenticated chain is demoted to `failed` rather than counted as +recovered. + +Proposals are opportunistic recovery material, not a backup: v1 proposals +carry no note bytes, proposals disappear once canonicalized, and embedded +note bytes are visible to the GUARDIAN operator (existing v2 behavior, not +a new exposure). + +#### The public-backfill strategy + +Normal forward sync starts from the store's **global** cursor, so in a +shared store the cursor may already be past blocks containing the recovered +account's notes. The backfill scans a historical block range — genesis to +the current chain tip by default — for public notes addressed at the +account's standard note tag and imports them with their on-chain inclusion +proofs, without ever touching the global sync height. The scan's cost grows +with the number of matching notes, not the range length, so a full +genesis-to-tip scan is fast on an ordinary account. The flow syncs the +chain state before this strategy runs, so it works on a store that has +never synced. + +`report.backfill` carries the requested range (`scannedFrom` / +`scannedTo`), the number of unique tag matches (`discovered`), the private +matches skipped (`skippedPrivate` — the chain holds no body for them; the +other two strategies cover those), the matches the relevance screen +rejected (`skippedIrrelevant`), the matches this SDK's static screen could +not judge (`skippedUnscreenable` — custom note scripts; the Rust SDK's +execution-based screener judges every note and always reports `0` there), +and one `NoteImportOutcome` per screened-in public note. A proof-less +expected record left by an earlier proposal import is upgraded in place +with the freshly fetched proof rather than skipped. Sub-ranges the scan +could not cover land in `uncovered` with `retryable`/`reason` set, so a +partial scan never aborts the rest of the flow. + +### Preserving Notes Across a Guardian Switch + +Pending proposals do not survive a guardian switch: the new GUARDIAN is +registered with bare account state, so once the client repoints, the notes +embedded in the old GUARDIAN's pending consume-notes proposals are the one +recovery source `recoverNotes` can no longer reach. `executeProposal` runs +the proposal-import slice of the flow automatically on the switch-guardian +path — against the old GUARDIAN, before the switch transaction executes — +so those notes land in the local store while they are still reachable +(issue #417). The step is best-effort and bounded by a 30s timeout with +cooperative cancellation: an unreachable or hung old GUARDIAN is warned +about, never allowed to block the switch. + +The transport drain and public backfill deliberately do not run on the +switch path: a switch executes against an intact local store, and both the +note transport and the node are configured independently of the GUARDIAN, +so a switch loses nothing they rescan. They remain device-loss recovery +tools and work unchanged against the new GUARDIAN. + +A wallet repointing a follower client by hand should request the same +preservation before switching clients: + +```typescript +// Before setGuardianClient(newGuardian): +const report = await multisig.preservePreSwitchProposalNotes(); +``` + +`preservePreSwitchProposalNotes` is the one public entry point for switch +preservation — it wraps the import slice in the switch-specific safety +contract, so there is no equivalent way to request it through +`recoverNotes` options. It returns the slice's `NoteRecoveryReport` (or +`undefined` when the flow could not run or timed out) and never throws. +On timeout a cancellation token stops every step and inner loop at its +next checkpoint (including between RPC retry attempts), and the switch +waits a short bounded grace for the one uninterruptible in-flight +operation to settle so it cannot overlap the switch transaction. Imported +notes are verified by the next normal sync. Like every listing-based flow +this covers proposals pending at the next nonce; one superseded at an +earlier nonce (it lost a same-nonce race) is filtered out and its embedded +notes are not imported here. + +### Proposal Operations + +Every `create*Proposal` method takes a single trailing options object (issue +#387). All of them accept an optional `nonce` that identifies the proposal +(defaults to `Date.now()`); method-specific options are listed with each +method below. Passing a legacy positional `nonce` number where the options +object is expected throws instead of silently applying defaults. + +#### P2ID Transfer (Send Funds) + +```typescript +const proposal = await multisig.createP2idProposal( + recipientAccountId, // Recipient's account ID + faucetAccountId, // Faucet (token) ID + 1000n // Amount to send +); + +// Private note: only the note's hash is published on chain. Pass `noteType` +// in the options object (`NoteType` comes from `@miden-sdk/miden-sdk`). +import { NoteType } from '@miden-sdk/miden-sdk'; + +const privateProposal = await multisig.createP2idProposal( + recipientAccountId, + faucetAccountId, + 1000n, + { noteType: NoteType.Private }, // note visibility; defaults to NoteType.Public +); + +// P2IDE note (issue #366): pass `reclaimHeight` and/or `timelockHeight` +// (absolute block heights) to create a reclaimable/timelocked note instead +// of a plain P2ID note. `reclaimHeight` lets the sender reclaim the note if +// it is still unconsumed at that block; `timelockHeight` prevents +// consumption before that block. +const reclaimableProposal = await multisig.createP2idProposal( + recipientAccountId, + faucetAccountId, + 1000n, + { reclaimHeight: 500_000 }, +); +``` + +Note that the two heights are independent and the SDK does not cross-check +them: a `timelockHeight` at or above `reclaimHeight` means the recipient +never has a window in which they can claim before the sender can reclaim — +usually not what you want. + +> **Warning:** a `private` P2ID note publishes only its hash on chain. The +> recipient cannot discover the note by syncing; the full note details must +> be shared with them out-of-band before they can consume it. Use +> `exportNoteToBytes` / `importNoteFromBytes` (or the browser file variants +> `exportNoteToFile` / `importNoteFromFile`) for that transfer (issue #356): + +```typescript +// Sender: resolve the note ID BEFORE executing (it derives from the +// pre-execution vault state), then export after execution. +const noteId = await multisig.getP2idNoteId(privateProposal); +// ...sign + execute the proposal... +const noteFileBytes = await multisig.exportNoteToBytes(noteId); +// Deliver `noteFileBytes` to the recipient out-of-band (file, message, ...). +// Or, in a browser, trigger a download of the note file directly: +await multisig.exportNoteToFile(noteId); + +// Recipient: import the bytes (or a File from an ), then +// sync so the note's on-chain commitment is tracked; it then appears in +// getConsumableNotes() and can be consumed with createConsumeNotesProposal +// as usual. +const importedNoteId = await multisig.importNoteFromBytes(noteFileBytes); +``` + +> **Note:** every cosigner device that verifies or signs the consume-notes +> proposal needs the note in its local store with the on-chain inclusion +> proof — deliver the note file to each of them (import + sync), not just to +> the proposer. A cosigner whose store lacks the authenticated note rebuilds +> the transaction differently (the input-notes commitment distinguishes +> authenticated from unauthenticated consumption) and rejects the proposal +> with `metadata does not match tx_summary`. The sender's own device heals +> itself: it already knows the full note, so a post-commit sync is enough. + +#### Consume Notes (Claim Received Funds) + +```typescript +// Get consumable notes +const notes = await multisig.getConsumableNotes(); + +// Create proposal to consume them +const noteIds = notes.map(n => n.id); +const proposal = await multisig.createConsumeNotesProposal(noteIds); +``` + +A private note received out-of-band must first be loaded with +`importNoteFromBytes(noteFileBytes)` or `importNoteFromFile(file)` (see the +P2ID section above); after a sync it shows up in `getConsumableNotes()` like +any public note. + +#### Add Signer + +```typescript +const proposal = await multisig.createAddSignerProposal( + newSignerCommitment, // New signer's public key commitment + { newThreshold }, // Options: nonce, newThreshold (defaults to current) +); +``` + +> **Override dilution on signer growth**: per-procedure threshold overrides +> are absolute signature counts, and the on-chain update never re-scales +> them — growing the signer set silently lowers every override's effective +> signing ratio (a 2-of-2 override becomes 2-of-n). Both SDKs surface this: +> the TypeScript SDK logs a `console.warn` per affected override and exposes +> `multisig.overridesDilutedBySignerGrowth(newNumSigners)`; the Rust SDK +> emits a `tracing::warn!` in `propose_transaction` and exposes +> `MultisigAccount::overrides_diluted_by_signer_growth(new_num_signers)`. +> To keep the intended security level, raise the affected overrides via an +> update-procedure-threshold proposal alongside the growth. + +> **Overrides apply to guardian rotation too**: `update_guardian` is a valid +> override target in both SDKs. Guardian rotation is a note-less operation, so +> the upstream contract skips the guardian signature check when +> `update_guardian_public_key` is the only non-auth procedure called — the +> multisig quorum alone authorizes it. That quorum is the override on +> `update_guardian`'s root when one is set, so an override of 1 lets a single +> signer replace the guardian with no guardian consent. Nothing in the builder +> or the contract restricts overrides on this root; treat an override on +> `update_guardian` as a deliberate reduction of the account's recovery +> threshold. Installing such an override is gated — at creation the account's +> author chooses it, and at runtime `set_procedure_threshold` requires the +> default quorum plus a guardian signature — but the gate applies only to +> installing it. Once stored, the reduced quorum governs every future rotation +> on its own, with no further guardian involvement, until another +> update-procedure-threshold proposal raises it back. + +#### Remove Signer + +```typescript +const proposal = await multisig.createRemoveSignerProposal( + signerToRemove, // Signer's commitment to remove + { newThreshold }, // Options: nonce, newThreshold (defaults to + // min(current threshold, remaining signer count)) +); +``` + +#### Change Threshold + +```typescript +const proposal = await multisig.createChangeThresholdProposal( + newThreshold // New threshold value +); +``` + +#### Switch GUARDIAN Provider + +```typescript +const proposal = await multisig.createSwitchGuardianProposal( + newGuardianEndpoint, // New GUARDIAN server URL + newGuardianCommitment // New GUARDIAN server's public key +); +``` + +When the current GUARDIAN is unreachable, create the switch proposal fully +offline instead — nothing is pushed to the current operator (issue #433; +mirrors the Rust `create_proposal_offline`). The returned `ExportedProposal` +already carries the proposer's signature; share its JSON with cosigners for +`importProposal` / `signProposalOffline`, then execute: + +```typescript +const exported = await multisig.createSwitchGuardianProposalOffline( + newGuardianEndpoint, + newGuardianCommitment +); +``` + +### Signing & Executing Proposals + +```typescript +// List all pending proposals +const proposals = await multisig.syncProposals(); + +for (const proposal of proposals) { + console.log(`${proposal.id}: ${proposal.status.type}`); + + if (proposal.status.type === 'pending') { + console.log(` Signatures: ${proposal.status.signaturesCollected}/${proposal.status.signaturesRequired}`); + } +} + +// Sign a proposal +const signed = await multisig.signProposal(proposalId); + +// Execute when ready +if (signed.status.type === 'ready') { + await multisig.executeProposal(proposalId); +} +``` + +### Offline Export/Import + +```typescript +// Export proposal for offline signing +const json = multisig.exportProposalToJson(proposalId); +// Share via file, QR code, etc. + +// On air-gapped machine: import and sign +const imported = multisig.importProposal(json); +const signedJson = multisig.signProposalOffline(proposalId); + +// Back on online machine: import signed proposal +const signedProposal = multisig.importProposal(signedJson); +await multisig.executeProposal(signedProposal.id); +``` + +### Recovering Accounts By Key + +Use `recoverByKey` when a wallet has a signing key from an account's +authorization set but does not know the account ID yet. The helper queries +Guardian's `/state/lookup` endpoint with proof-of-possession of the key, +fetches state for each matching account, and returns `(accountId, state)` +pairs. + +```typescript +const recovered = await client.recoverByKey(signer); + +if (recovered.length === 0) { + console.log('No account on this Guardian authorizes this key'); +} + +for (const { accountId, state } of recovered) { + console.log('Recovered account:', accountId); + console.log('State commitment:', state.commitment); + + const multisig = await client.load(accountId, signer); + // Continue with normal proposal or sync flows. +} +``` + +The signer passed to `recoverByKey` must implement `signLookupMessage`. The +bundled `FalconSigner`, `EcdsaSigner`, Miden Wallet signer, and Para signer +support it. Multiple matches are valid: the same key commitment may authorize +more than one account, and the method returns all matches instead of choosing +one implicitly. + +### API Reference + +#### MultisigClient + +| Method | Description | +|--------|-------------| +| `create(config, signer)` | Create new multisig account | +| `load(accountId, signer)` | Load existing account from GUARDIAN | +| `recoverByKey(signer)` | Discover accounts that authorize the signer's key and fetch each current state | +| `guardianClient` | Access to underlying GUARDIAN HTTP client | + +#### Multisig + +| Method | Description | +|--------|-------------| +| `accountId` | Get account ID (hex string) | +| `threshold` | Get current threshold | +| `signerCommitments` | Get list of signer commitments | +| `fetchState()` | Fetch latest state from GUARDIAN | +| `registerOnGuardian()` | Register new account with GUARDIAN | +| `syncProposals()` | Sync proposals from GUARDIAN | +| `abandonCandidate(nonce)` | Record an abandon intent for a stuck candidate (worker resolves after a short quarantine) | +| `abandonStatus(nonce)` | Poll the abandon resolution: `waiting` / `landed` / `abandoned` / `unexpected` | +| `deltaHistory({ limit?, cursor? }?)` | One page of canonical delta history, newest-first, with decoded note summaries | +| `listProposals()` | Get cached proposals | +| `createP2idProposal(recipient, faucet, amount, { nonce, noteType, reclaimHeight, timelockHeight }?)` | Create transfer proposal (`noteType`: `NoteType.Public` (default) or `NoteType.Private`; presence of `reclaimHeight`/`timelockHeight` creates a P2IDE note, issue #366) | +| `createConsumeNotesProposal(noteIds, { nonce }?)` | Create note consumption proposal | +| `getP2idNoteId(proposal)` | Compute the note ID a P2ID proposal creates (call before executing) | +| `exportNoteToBytes(noteId)` | Export a created note as note-file bytes for out-of-band delivery | +| `exportNoteToFile(noteId, filename?)` | Browser-only: download the note file | +| `importNoteFromBytes(noteBytes)` | Import a note file received out-of-band | +| `importNoteFromFile(file)` | Import a note file from a browser `File`/`Blob` | +| `recoverNotes(options?)` | Run the note-recovery strategies (transport drain, proposal import, public backfill) as one flow with a final verifying sync; returns a combined `NoteRecoveryReport` | +| `preservePreSwitchProposalNotes()` | Pre-switch slice of the flow (issue #417): import notes embedded in the old GUARDIAN's pending proposals before repointing; run automatically by `executeProposal` on the switch path; returns the report or `undefined` | +| `createAddSignerProposal(commitment, { nonce, newThreshold }?)` | Create add signer proposal (`newThreshold` defaults to the current threshold) | +| `createRemoveSignerProposal(commitment, { nonce, newThreshold }?)` | Create remove signer proposal (`newThreshold` defaults to min of current threshold and remaining signer count) | +| `createChangeThresholdProposal(threshold, { nonce }?)` | Create threshold change proposal | +| `createUpdateProcedureThresholdProposal(procedure, threshold, { nonce }?)` | Create per-procedure threshold override proposal (`threshold: 0` clears the override) | +| `createSwitchGuardianProposal(endpoint, pubkey, { nonce }?)` | Create GUARDIAN switch proposal | +| `createSwitchGuardianProposalOffline(endpoint, pubkey, { nonce }?)` | Create GUARDIAN switch proposal without contacting the current GUARDIAN; returns a signed `ExportedProposal` for side-channel cosigning (issue #433) | +| `createCustomProposal(requestBytes, label, { nonce }?)` | Create a producer-built custom proposal (issue #266) | +| `signProposal(id)` | Sign a proposal | +| `executeProposal(id)` | Execute ready proposal | +| `exportProposalToJson(id)` | Export for offline signing | +| `importProposal(json)` | Import offline proposal | +| `signProposalOffline(id)` | Sign imported proposal offline | +| `getConsumableNotes()` | Get notes that can be consumed | +| `getSignerPublicKeyCommitments()` | Read the current signer public-key commitments from account storage, ordered by signer index (strict; throws on partial reads) | +| `getGuardianPublicKeyCommitment()` | Read the current guardian commitment from account storage (strict; throws when the entry is missing — the guarded-multisig always includes a guardian) | + +#### FalconSigner + +| Property/Method | Description | +|-----------------|-------------| +| `commitment` | Public key commitment (hex) | +| `publicKey` | Serialized public key (hex) | +| `signRequest(id, timestamp, requestPayload)` | Sign account ID + timestamp + request payload digest for auth | +| `signCommitment(hex)` | Sign commitment/word | +| `signLookupMessage(timestamp, keyCommitment)` | Sign account-less lookup digest for `recoverByKey` | + +#### AccountInspector + +| Method | Description | +|--------|-------------| +| `fromBase64(data)` | Inspect base64-encoded account | +| `fromAccount(account)` | Inspect Account object | +| `getSignerPublicKeyCommitments(account)` | Read the signer public-key commitments ordered by signer index (strict; throws on a foreign contract version or any absent entry) | +| `getGuardianPublicKeyCommitment(account)` | Read the guardian commitment (strict; throws on a foreign contract version or a missing entry) | + +`fromBase64` / `fromAccount` return `DetectedMultisigConfig`: +- `threshold`: number +- `numSigners`: number +- `signerCommitments`: string[] +- `guardianCommitment`: string | null +- `vaultBalances`: \{ faucetId, amount \}[] + +> **Reading an account's keys:** since the account uses the upstream +> `AuthGuardedMultisig` component, the Miden SDK's +> `Account.getPublicKeyCommitments()` returns the approver commitments +> natively. The accessors above are the strict, layout-insulated +> alternative (issue #306): they validate the complete set against the +> configured signer count and throw instead of silently omitting +> unreadable entries, and they shield consumers from storage-layout +> changes across contract versions (both are gated on the pinned contract +> version — see [Contract version pinning](#contract-version-pinning)). +> Commitments are ordered by signer index as currently stored (indices +> re-pack when signers are removed); hot/cold roles are a consumer-side +> convention. The `Account` must come from the same copy of +> `@miden-sdk/miden-sdk` that this package links — a separately bundled +> SDK copy is rejected by the SDK's instance checks. + +--- + +## Rust SDK Guide + +### Installation & Setup + +```rust +use miden_multisig_client::{ + MultisigClient, MultisigClientBuilder, + MultisigAccount, TransactionType, + Proposal, ProposalStatus, + KeyManager, GuardianKeyStore, + Endpoint, Word, AccountId, SecretKey, +}; + +// Build client with fluent API +let mut client = MultisigClient::builder() + .miden_endpoint(Endpoint::new("http://localhost:57291")) + .guardian_endpoint("http://localhost:50051") + .account_dir("/tmp/multisig-data") + .generate_key() // Or: .with_secret_key(key) + .build() + .await?; +``` + +### Creating Accounts + +```rust +// Collect signer commitments (your key + cosigners) +let signer_commitments = vec![ + client.user_commitment(), // Your commitment + commitment_from_hex("0x1234...")?, // Cosigner 1 + commitment_from_hex("0x5678...")?, // Cosigner 2 +]; + +// Create 2-of-3 multisig +let account = client.create_account(2, signer_commitments).await?; + +// Register with GUARDIAN +client.push_account().await?; + +println!("Created account: {}", account.id()); +println!("Threshold: {}", account.threshold()?); +println!("Signers: {:?}", account.cosigner_commitments_hex()); +``` + +### Loading Existing Accounts + +```rust +// Pull account from GUARDIAN (as a cosigner) +let account = client.pull_account(account_id).await?; + +// Sync with Miden network +client.sync().await?; + +// Inspect account +println!("Threshold: {}", account.threshold()?); +println!("Nonce: {}", account.nonce()); +println!("GUARDIAN commitment: {:?}", account.guardian_commitment()?); +``` + +### Delta History + +Guardian retains the account's canonical delta history, allowing a wallet to +render its history after recovery. `delta_history()` returns one +`HistoryPage` at a time, newest-first by nonce, with server-decoded input and +output note summaries (typed tags, visibility, assets, counterparties). Every +entry carries `HistoryEntryStatus::Canonical` today; the set widens if the +feed gains a status filter. + +```rust +let mut cursor: Option = None; +loop { + let page = client.delta_history(Some(50), cursor.take()).await?; + for entry in &page.entries { + println!("nonce {} at {}", entry.nonce, entry.timestamp); + for note in &entry.output_notes { + println!(" sent {} note {}", note.tag.as_str(), note.note_id); + } + } + match page.next_cursor { + Some(next) => cursor = Some(next), + None => break, + } +} +``` + +`limit` accepts 1–500 (server default 50 when `None`). Only canonical +(confirmed) transactions appear — pending proposals live on +`list_proposals()`. The feed is served even while the account is +paused. Guardian only ever sees transactions pushed through it, so +history of transactions the account executed elsewhere is not included. +Output notes whose full details are not in the stored summary (e.g. +private notes carried as partial notes) appear with tag `custom` and no +recipient. + +### Recovering Notes After Device Loss + +Normal forward sync cannot see notes that landed behind the store's +cursors. After `pull_account` on a recovered account, `recover_notes` runs +the three recovery strategies as one flow — the private-note transport +drain, the proposal-embedded note import, and the historical public-note +backfill — and finishes with a normal sync so imported notes are verified +and ready to consume. The flow is the one public entry point: the +strategies are internal, so a caller cannot accidentally skip the context +they need (a tracked account, a synced store) or the final verifying sync. + +```rust +client.pull_account(account_id).await?; + +// `None` runs every strategy over the full chain and syncs afterwards. +let report = client.recover_notes(None).await?; +println!("recovered {} notes", report.imported); +for problem in &report.problems { + // A strategy that could not run at all lands here; the flow continues + // with the remaining strategies either way. + eprintln!("step {} did not run: {}", problem.step, problem.reason); +} +if report.retryable { + // The flow is idempotent — rerun it to plausibly recover more. +} +``` + +Strategies are individually selectable and the backfill's block range can be +bounded: + +```rust +use miden_multisig_client::{NoteRecoveryOptions, PublicBackfillOptions}; + +// Rescan only the transport backlog and recent public history, skipping the +// proposal import and the final verifying sync. +let report = client + .recover_notes(Some(NoteRecoveryOptions { + proposal_import: false, + backfill: PublicBackfillOptions { + from_block: Some(1_690_000u32.into()), + ..Default::default() + }, + sync_after: false, + ..Default::default() + })) + .await?; +``` + +The combined `NoteRecoveryReport` carries each strategy's own report +(`transport`, `proposal_import`, `backfill`) untouched, plus the `problems` +list, `synced`, the total `imported` count, and an aggregate `retryable` +flag. + +#### The transport-drain strategy + +A fresh store has no note-transport cursor — and in a store shared with +other accounts, sync may have advanced the cursor past private notes +addressed to the recovered account. The drain rescans the full transport +backlog for every tracked note tag (running as many transport syncs as the +upstream per-sync tag backfill cap requires), regardless of the stored +cursor, and never regresses it. It is idempotent, and a failed drain +restores the covered-tags bookkeeping so normal sync keeps working exactly +as before the attempt. + +Transport problems land in `report.transport`, never as errors: +`Unavailable` means no transport endpoint is configured or the transport +could not be reached before anything was imported; `Failed` with +`retryable: true` keeps any partial progress and rerunning continues it. + +**Not a backup:** transport recovery is bounded by the transport service's +retention. Senders may deliver private notes out-of-band without using the +transport, and relayed blobs are pruned after the retention window. Notes +outside both are recoverable only from their sender (see +`import_note_from_file`). + +#### The proposal-import strategy + +v2 `consume_notes` proposals embed the serialized notes they consume, so +pending proposals are opportunistic recovery material: the strategy lists +them from GUARDIAN (isolating per-proposal parse or binding failures as +`Invalid` outcomes instead of failing the whole step), validates each +embedded note against the proposal's declared note ids, fetches on-chain +inclusion proofs, and imports per note — so it works for private notes too; +the node never needs to hold the note body. + +Each unique embedded note gets a `NoteImportOutcome` in +`report.proposal_import` (`Imported`, `AlreadyPresent`, `AlreadyConsumed`, +`NotCommitted`, `Invalid`, or `Failed`); duplicates across proposals fold +into one outcome and no per-note problem blocks the others. A note not yet +on chain is recorded in `Expected` state with its sync-hint tag so a later +sync picks it up, and an import whose inclusion proof fails verification +against the authenticated chain is demoted to `Failed` rather than counted +as recovered. + +Proposals are opportunistic recovery material, not a backup: v1 proposals +carry no note bytes, proposals disappear once canonicalized, and the +embedded bytes are visible to the GUARDIAN operator (existing v2 behavior, +not a new exposure). + +#### The public-backfill strategy + +Normal forward sync starts from the store's **global** cursor, so in a +shared store the cursor may already be past blocks containing the recovered +account's notes. The backfill scans a historical block range — genesis to +the current chain tip by default — for public notes addressed at the +account's standard note tag and imports them with their on-chain inclusion +proofs, without ever touching the global sync height. The scan's cost grows +with the number of matching notes, not the range length, so a full +genesis-to-tip scan is fast on an ordinary account. The flow syncs the +chain state before this strategy runs, so it works on a store that has +never synced. + +`report.backfill` carries the requested range (`scanned_from` / +`scanned_to`), the number of unique tag matches (`discovered`), the private +matches skipped (`skipped_private` — the chain holds no body for them; the +other two strategies cover those), the matches the execution-based +`NoteScreener` rejected (`skipped_irrelevant` — tags are best-effort, +truncated filters, and exactly like normal sync only notes the account can +actually consume are imported), and one `NoteImportOutcome` per screened-in +public note (`skipped_unscreenable` exists for cross-SDK parity and is +always `0` here). A proof-less expected record left by an earlier proposal +import is upgraded in place with the freshly fetched proof rather than +skipped. Sub-ranges the scan could not cover land in `uncovered` with +`retryable`/`reason` set, so a partial scan never aborts the rest of the +flow. + +### Preserving Notes Across a Guardian Switch + +Pending proposals do not survive a guardian switch: the new GUARDIAN is +registered with bare account state, so once the client repoints, the notes +embedded in the old GUARDIAN's pending consume-notes proposals are the one +recovery source `recover_notes` can no longer reach. `execute_proposal` +runs the proposal-import slice of the flow automatically on the +switch-guardian path — against the old GUARDIAN, before the switch +transaction executes — so those notes land in the local store while they +are still reachable (issue #417). The step is best-effort and bounded by a +30s timeout: an unreachable or hung old GUARDIAN is warned about, never +allowed to block the switch. + +The transport drain and public backfill deliberately do not run on the +switch path: a switch executes against an intact local store, and both the +note transport and the node are configured independently of the GUARDIAN, +so a switch loses nothing they rescan. They remain device-loss recovery +tools and work unchanged against the new GUARDIAN. The offline switch flow +(`execute_imported_proposal`) also skips the import — it exists to avoid +contacting the GUARDIAN at all — so when the old GUARDIAN is in fact still +reachable, run the preservation explicitly before executing offline. + +A wallet repointing a follower client by hand should request the same +preservation before `set_guardian_endpoint`: + +```rust +// Before set_guardian_endpoint(new_endpoint, true): +let report = client.preserve_pre_switch_proposal_notes().await; +``` + +`preserve_pre_switch_proposal_notes` is the one public entry point for +switch preservation — it wraps the import slice in the switch-specific +safety contract (timeout, cancellation, warnings), so there is no +equivalent way to request it through `recover_notes` options. It returns +the slice's `NoteRecoveryReport` (`None` when the flow could not run or +timed out) and never errors; on timeout the in-flight flow is cancelled +and the switch proceeds. Imported notes are verified by the next normal +sync. Like every listing-based flow this covers proposals pending at the +next nonce; one superseded at an earlier nonce (it lost a same-nonce +race) is filtered out and its embedded notes are not imported here. + +### Transaction Types + +```rust +// P2ID Transfer (public note) +let tx = TransactionType::transfer(recipient_id, faucet_id, 1000); + +// P2ID Transfer with a private note (only the note hash is published on +// chain; the recipient needs the note shared out-of-band — see +// "Out-of-Band Note Transfer" below). `NoteType` is re-exported from +// `miden_protocol::note`. +let tx = TransactionType::transfer_with_note_type( + recipient_id, faucet_id, 1000, NoteType::Private, +); + +// P2IDE Transfer (issue #366): optional reclaim and/or timelock block +// heights. Presence of either height creates a P2IDE note instead of a +// plain P2ID note. `P2ideHeights` uses `NonZeroU32`, so the invalid zero +// height is unrepresentable. +use std::num::NonZeroU32; +use miden_multisig_client::P2ideHeights; + +let tx = TransactionType::transfer_p2ide( + recipient_id, faucet_id, 1000, NoteType::Public, + P2ideHeights { reclaim: NonZeroU32::new(500_000), timelock: None }, +); + +// Consume Notes +let tx = TransactionType::consume_notes(vec![note_id1, note_id2]); + +// Add Cosigner +let tx = TransactionType::add_cosigner(new_commitment); + +// Remove Cosigner +let tx = TransactionType::remove_cosigner(commitment_to_remove); + +// Update Signers (change threshold and/or signer set) +let tx = TransactionType::update_signers(new_threshold, new_signer_list); + +// Switch GUARDIAN Provider +let tx = TransactionType::switch_guardian(new_endpoint, new_commitment); +``` + +### Proposal Operations + +```rust +// Create and submit proposal +let proposal = client.propose_transaction(tx).await?; +println!("Proposal ID: {}", proposal.id); + +// Or with offline fallback +match client.propose_with_fallback(tx).await? { + ProposalResult::Online(proposal) => { + println!("Submitted to GUARDIAN: {}", proposal.id); + } + ProposalResult::Offline(exported) => { + // Save for file-based sharing + std::fs::write("proposal.json", exported.to_json()?)?; + } +} +``` + +### Listing & Signing Proposals + +```rust +// List pending proposals +let proposals = client.list_proposals().await?; + +for proposal in &proposals { + match &proposal.status { + ProposalStatus::Pending => { + let (signatures_collected, signatures_required) = proposal.signature_counts(); + println!("{}: {}/{} signatures", + proposal.id, signatures_collected, signatures_required); + println!(" Signed by: {:?}", proposal.metadata.signers); + } + ProposalStatus::Ready => { + println!("{}: Ready to execute", proposal.id); + } + ProposalStatus::Finalized => { + println!("{}: Already executed", proposal.id); + } + } +} + +// Sign a proposal +client.sign_proposal(&proposal_id).await?; + +// Execute when ready +client.execute_proposal(&proposal_id).await?; +``` + +### Offline Export/Import + +```rust +// Create offline proposal (when GUARDIAN unavailable) +let exported = client.create_proposal_offline(tx).await?; +std::fs::write("proposal.json", exported.to_json()?)?; + +// On air-gapped machine: load and sign +let json = std::fs::read_to_string("proposal.json")?; +let mut exported: ExportedProposal = serde_json::from_str(&json)?; +client.sign_imported_proposal(&mut exported)?; +std::fs::write("signed.json", exported.to_json()?)?; + +// Back online: execute +let json = std::fs::read_to_string("signed.json")?; +let exported: ExportedProposal = serde_json::from_str(&json)?; +client.execute_imported_proposal(&exported).await?; +``` + +### Note Filtering + +```rust +use miden_multisig_client::NoteFilter; + +// List all consumable notes +let notes = client.list_consumable_notes().await?; + +// Filter by faucet +let filter = NoteFilter::by_faucet(faucet_id); +let notes = client.list_consumable_notes_filtered(filter).await?; + +// Filter by faucet with minimum amount +let filter = NoteFilter::by_faucet_min_amount(faucet_id, 5000); +let notes = client.list_consumable_notes_filtered(filter).await?; + +for note in notes { + println!("Note {}: {} tokens", note.id, note.amount_for_faucet(faucet_id)); +} +``` + +### Out-of-Band Note Transfer (Private Notes) + +A private P2ID note publishes only its commitment on chain, so the recipient's +client can never learn the note contents via sync. The sender must export the +note and deliver the file out-of-band (issue #356): + +```rust +// Sender: resolve the note ID BEFORE executing (it derives from the +// pre-execution vault state), then export after execution. +let note_id = client.p2id_note_id(&proposal)?; +client.execute_proposal(&proposal.id).await?; +client.export_note_to_file(¬e_id.to_hex(), Path::new("note.mno")).await?; +// Deliver note.mno to the recipient out-of-band (file, message, ...). + +// Recipient: import the file, then sync so the note's on-chain commitment +// is tracked; it then appears in list_consumable_notes() and can be +// consumed with a regular consume-notes proposal. +let imported_note_id = client.import_note_from_file(Path::new("note.mno")).await?; +client.sync().await?; +``` + +`export_note_to_bytes` / `import_note_from_bytes` are the in-memory variants +for programmatic delivery. + +Every cosigner device that verifies or signs the consume-notes proposal needs +the note in its local store with the on-chain inclusion proof — deliver the +note file to each of them (import + sync), not just to the proposer. A +cosigner whose store lacks the authenticated note rebuilds the transaction +differently (the input-notes commitment distinguishes authenticated from +unauthenticated consumption) and rejects the proposal with `metadata does not +match tx_summary`. The sender's own device heals itself: it already knows the +full note, so a post-commit sync is enough. + +### Recovering Accounts By Key + +Use `recover_by_key` when the configured signer is known but the account ID is +not. The client signs a lookup-bound authentication message, asks Guardian for +accounts that authorize the signer's commitment, fetches state for each match, +and returns `RecoveredAccount` values. + +```rust +let recovered = client.recover_by_key().await?; + +if recovered.is_empty() { + println!("No account on this Guardian authorizes this key"); +} + +for entry in recovered { + println!("Recovered account: {}", entry.account_id); + println!("State commitment: {}", entry.state.commitment); + + let account_id = AccountId::from_hex(&entry.account_id)?; + client.pull_account(account_id).await?; + // Continue with normal proposal or sync flows. +} +``` + +An empty list means the key is valid but this Guardian has no account metadata +that authorizes its commitment. Authentication failures, malformed lookup +responses, and per-account `get_state` failures are returned as errors. + +### API Reference + +#### MultisigClient + +| Method | Description | +|--------|-------------| +| `builder()` | Create builder for configuration | +| `create_account(threshold, commitments)` | Create new multisig | +| `pull_account(id)` | Join existing multisig | +| `push_account()` | Register account with GUARDIAN | +| `sync()` | Sync with Miden network | +| `account()` | Get loaded account (Option) | +| `account_id()` | Get account ID (Option) | +| `user_commitment()` | Get user's key commitment | +| `user_commitment_hex()` | Get commitment as hex | +| `recover_by_key()` | Discover accounts that authorize the configured signer and fetch each current state | +| `propose_transaction(tx)` | Create and submit proposal | +| `propose_with_fallback(tx)` | Online or offline proposal | +| `list_proposals()` | List pending proposals | +| `sign_proposal(id)` | Sign a proposal | +| `execute_proposal(id)` | Execute ready proposal | +| `abandon_candidate(nonce)` | Record an abandon intent for a stuck candidate (worker resolves after a short quarantine) | +| `abandon_status(nonce)` | Poll the abandon resolution: `Waiting` / `Landed` / `Abandoned` / `Unexpected` | +| `delta_history(limit, cursor)` | One page of canonical delta history, newest-first, with decoded note summaries | +| `create_proposal_offline(tx)` | Create offline proposal | +| `sign_imported_proposal(exported)` | Sign offline proposal | +| `execute_imported_proposal(exported)` | Execute offline proposal | +| `export_proposal(id, path)` | Export to file | +| `import_proposal(path)` | Import from file | +| `list_consumable_notes()` | List available notes | +| `list_consumable_notes_filtered(filter)` | Filter notes | +| `p2id_note_id(proposal)` | Compute the note ID a P2ID proposal creates (call before executing) | +| `export_note_to_file(note_id, path)` | Export a created note to a file for out-of-band delivery | +| `export_note_to_bytes(note_id)` | Export a created note as note-file bytes | +| `import_note_from_file(path)` | Import a note file received out-of-band | +| `import_note_from_bytes(bytes)` | Import a note from note-file bytes | +| `recover_notes(options)` | Run the note-recovery strategies (transport drain, proposal import, public backfill) as one flow with a final verifying sync; returns a combined `NoteRecoveryReport` | +| `preserve_pre_switch_proposal_notes()` | Pre-switch slice of the flow (issue #417): import notes embedded in the old GUARDIAN's pending proposals before repointing; run automatically by `execute_proposal` on the switch path; returns `Option` | + +#### MultisigAccount + +| Method | Description | +|--------|-------------| +| `id()` | Account ID | +| `nonce()` | Current nonce | +| `commitment()` | Account state commitment | +| `threshold()` | Signing threshold | +| `num_signers()` | Number of signers | +| `cosigner_commitments()` | List of commitments (Word) | +| `cosigner_commitments_hex()` | List as hex strings | +| `is_cosigner(commitment)` | Check if commitment is signer | +| `guardian_commitment()` | GUARDIAN server commitment | + +#### TransactionType + +| Variant | Description | +|---------|-------------| +| `P2ID { recipient, faucet_id, amount, note_type, heights }` | Transfer funds (`note_type` selects note visibility; `heights: P2ideHeights` with either constraint set creates a P2IDE note — use `transfer_p2ide()`; `transfer()` defaults to a public plain-P2ID note) | +| `ConsumeNotes { note_ids }` | Consume notes | +| `AddCosigner { new_commitment }` | Add signer | +| `RemoveCosigner { commitment }` | Remove signer | +| `UpdateSigners { new_threshold, signer_commitments }` | Update config | +| `SwitchGuardian { new_endpoint, new_commitment }` | Switch GUARDIAN | + +#### ProposalStatus + +| Variant | Description | +|---------|-------------| +| `Pending` | Collecting sigs (`proposal.signature_counts()`, `proposal.metadata.signers`) | +| `Ready` | Threshold met | +| `Finalized` | Executed | + +--- + +## Use Cases + +### Use Case 1: Treasury Management (2-of-3) + +A company treasury requiring 2 of 3 executives to approve transfers. + +```typescript +// Setup: CEO, CFO, and COO each have their own signer +const config = { + threshold: 2, + signerCommitments: [ceoCommitment, cfoCommitment, cooCommitment], + guardianCommitment, +}; + +const treasury = await client.create(config, ceoSigner); +await treasury.registerOnGuardian(); + +// CEO proposes payment to vendor +const payment = await treasury.createP2idProposal( + vendorAccountId, + usdcFaucetId, + 50000n +); + +// CFO reviews and signs +const cfoMultisig = await cfoClient.load(treasury.accountId, cfoSigner); +await cfoMultisig.syncProposals(); +await cfoMultisig.signProposal(payment.id); + +// Payment executes (threshold met: CEO + CFO = 2) +await treasury.executeProposal(payment.id); +``` + +### Use Case 2: Secure Operations (3-of-5) + +High-security operations requiring 3 of 5 board members. + +```rust +// Create 3-of-5 multisig +let board_commitments = vec![member1, member2, member3, member4, member5]; +let account = client.create_account(3, board_commitments).await?; + +// Propose removing a compromised member +let tx = TransactionType::remove_cosigner(compromised_member); +let proposal = client.propose_transaction(tx).await?; + +// Three members must sign +// member1.sign_proposal(...) +// member2.sign_proposal(...) +// member3.sign_proposal(...) + +// Execute with 3 signatures +client.execute_proposal(&proposal.id).await?; +``` + +### Use Case 3: Note Consumption + +Claiming tokens sent to the multisig. + +```typescript +// Check for incoming notes +const notes = await multisig.getConsumableNotes(); + +console.log('Pending notes:'); +for (const note of notes) { + for (const asset of note.assets) { + if (asset.isFungible()) { + console.log(` ${note.id}: ${asset.amount()} from faucet ${asset.faucetId()}`); + } + } +} + +// Create proposal to consume all notes +const noteIds = notes.map(n => n.id); +const proposal = await multisig.createConsumeNotesProposal(noteIds); + +// After threshold signatures... +await multisig.executeProposal(proposal.id); + +console.log('Notes consumed, funds now in vault'); +``` + +--- + +## Offline Workflow + +### Complete Flow Diagram + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ OFFLINE SIGNING FLOW │ +└─────────────────────────────────────────────────────────────────────┘ + + PROPOSER (Online) COSIGNER (Air-gapped) EXECUTOR (Online) + ───────────────── ──────────────────── ──────────────── + │ │ │ + │ create_proposal_offline() │ │ + │ or propose_with_fallback() │ │ + ▼ │ │ + ┌──────────┐ │ │ + │ Export │ │ │ + │ proposal │ │ │ + │ .json │─────── USB ─────────►│ │ + └──────────┘ │ │ + │ ▼ │ + │ ┌──────────────┐ │ + │ │ Import JSON │ │ + │ │ Sign offline │ │ + │ │ Export JSON │ │ + │ └──────────────┘ │ + │ │ │ + │ │─────── USB ───────────────►│ + │ │ ▼ + │ │ ┌─────────────┐ + │ │ │ Import JSON │ + │ │ │ Verify sigs │ + │ │ │ Execute tx │ + │ │ └─────────────┘ + ▼ ▼ ▼ +``` + +### Export Format (JSON) + +```json +{ + "version": 1, + "account_id": "0x7925bdcc9c4df01068e79d4c94beeb", + "id": "0xabcd1234...", + "nonce": 5, + "tx_summary": { + "...": "transaction summary JSON" + }, + "signatures": [ + { + "signer_commitment": "0x1234...", + "signature": "0x5678..." + } + ], + "signatures_required": 2, + "metadata": { + "proposal_type": "add_signer", + "salt_hex": "0x...", + "new_threshold": 2, + "signer_commitments_hex": ["0x...", "0x..."] + } +} +``` + +## Version Compatibility + +Which Miden protocol line each Guardian release targets, the exact `miden-client` +and `@miden-sdk/miden-sdk` pins, what broke between lines, and which upgrades +reset stored data: see +[`MIDEN_COMPATIBILITY.md`](../miden-compatibility.md). + +Guardian's version and Miden's are not aligned (Guardian 0.16.x runs on Miden +0.15; Miden 0.16 lands in Guardian 0.17.x), so read that matrix rather than +matching the numbers. Per-release breaking changes are also in the +[GitHub release notes](https://github.com/OpenZeppelin/guardian/releases). + +### Contract version pinning + +Accounts are built from the audited upstream `AuthGuardedMultisig` component, pinned +exactly in both SDKs so a TypeScript-built account is byte-identical to a Rust-built +one: + +- **Rust**: the `miden-standards` pin in the workspace `Cargo.toml` +- **TypeScript**: the `@miden-sdk/miden-sdk` pin, whose bundled WASM embeds the + matching upstream `miden-standards` guarded-multisig component + +Matching versions is what makes the roots match, because neither SDK compiles the auth +component any more: both ask `miden-standards` for its `AuthGuardedMultisig` and use what +it returns. That settles the linkage question that used to matter here. `auth_tx` calls +`miden::standards::fee`, so its root depends on whether the standards package is linked +statically (the callee's MAST is inlined) or dynamically (an external reference is left) — +compiling an equivalent source locally links dynamically and roots differently, which is +why a locally built component could not be classified by `AccountComponentInterface::from_procedures` +and never had fee conversion info attached. Taking the component from upstream removes the +choice, so the pinned roots describe upstream's build and nothing else. + +The exact versions for each Guardian release are in +[`MIDEN_COMPATIBILITY.md`](../miden-compatibility.md#support-matrix); they are not +repeated here so there is one place to update. + +The pins are deliberate and must move together: nothing at build time verifies the +npm SDK's embedded miden-standards matches the Rust pin — the CI parity gates +(`procedure_roots_match_upstream_component`, the vitest `procedure-roots` test, and +the Playwright determinism spec) are what catch drift. + +**Deployed accounts are immutable.** An account's code — and therefore its procedure +roots — is fixed at creation. The SDK's hardcoded `PROCEDURE_ROOTS` / +`ProcedureName::root()` values, and the transaction scripts it compiles against the +bundled library, all assume the account was built from the *currently pinned* +contract version. Consequences of bumping the pin to a miden-standards release whose +MASM changed: + +- Management transactions built by the new SDK **fail against old accounts** (the + script calls a procedure root the old account's code does not export). +- Per-procedure threshold reads and `set_procedure_threshold` writes key the + account's `procedure_thresholds` storage map by the *new* roots — against an old + account they silently miss the stored overrides or store overrides the account + never consults. + +**Release policy until a contract-version registry lands**: treat any +miden-standards / @miden-sdk pin bump that changes procedure roots as a breaking +release. Bump the minor version, regenerate the root constants (both SDKs), and +state explicitly in the release notes that the new SDK operates only accounts +created with the new contract version. The planned fix is a version registry keyed +by the account's auth-procedure root, letting one SDK operate accounts from every +supported contract version. + +#### SDK ↔ contract version support + +An SDK release operates only accounts created with its pinned contract version; +the mapping is in +[`MIDEN_COMPATIBILITY.md`](../miden-compatibility.md#support-matrix). + +Compatibility there is about the on-chain account, not Guardian's stored state. +Adopting a new Miden line has twice required an irreversible server-side reset, so +even an account whose contract version still matches must be re-registered +afterwards. See +[`MIDEN_COMPATIBILITY.md`](../miden-compatibility.md#data-resets). + +Both SDKs **enforce** this at runtime rather than trusting the table: before any +procedure-root-keyed storage read, the account's code is checked for the pinned +contract version's auth procedure (`auth_tx_guarded_multisig`). A mismatch fails +loudly — Rust `MultisigError::UnsupportedContractVersion` (from +`MultisigAccount::procedure_threshold` and everything built on it), TS an +`unsupported contract version` error from `AccountInspector.fromAccount` — instead +of silently reporting wrong thresholds. `MultisigAccount::is_pinned_contract_version()` +exposes the check directly. When a new contract version is adopted, add a row here +and regenerate the root constants in the same change. + +--- + +## Releasing + +Steps for publishing a new version of the SDK (Rust crates + TypeScript packages). + +### Pre-Release Checklist + +1. All tests pass: + +```bash +# Rust +cargo test --locked \ + -p guardian-shared \ + -p guardian-client \ + -p miden-confidential-contracts \ + -p miden-multisig-client + +# TypeScript +cd packages +npm ci +npm test -w @openzeppelin/guardian-client +npm test -w @openzeppelin/guardian-evm-client +npm test -w @openzeppelin/guardian-operator-client +npm run build -w @openzeppelin/guardian-client +npm test -w @openzeppelin/miden-multisig-client +``` + +2. TypeScript packages build cleanly: + +```bash +cd packages +npm run build -w @openzeppelin/guardian-client +npm run build -w @openzeppelin/guardian-evm-client +npm run build -w @openzeppelin/guardian-operator-client +npm run build -w @openzeppelin/miden-multisig-client +``` + +3. Version numbers are updated in all files (see below). + +4. The coordinated Rust publication archive passes: + +```bash +cargo publish --dry-run --locked \ + -p guardian-shared \ + -p guardian-client \ + -p miden-confidential-contracts \ + -p miden-multisig-client +``` + +### Version Bump + +Update the version in these files: + +| File | Field | Inherits | +|------|-------|----------| +| `Cargo.toml` (workspace root) | `[workspace.package] version` | `shared`, `client`, `contracts`, `miden-multisig-client` | +| `crates/contracts/Cargo.toml` | `guardian-shared` dep version | - | +| `crates/client/Cargo.toml` | `guardian-shared` dep version | - | +| `crates/miden-multisig-client/Cargo.toml` | `guardian-client`, `guardian-shared`, `miden-confidential-contracts` dep versions | - | +| `packages/guardian-client/package.json` | `version` | - | +| `packages/guardian-evm-client/package.json` | `version` | - | +| `packages/guardian-operator-client/package.json` | `version` | - | +| `packages/miden-multisig-client/package.json` | `version` + `@openzeppelin/guardian-client` dep version | - | +| `packages/package-lock.json` | workspace lockfile | refresh with `npm install` in `packages/` | + +The `server`, `miden-rpc-client`, `miden-keystore`, and example crates have their own independent versions and are not published. + +After bumping TypeScript versions, refresh the workspace lockfile: + +```bash +cd packages +npm install +``` + +The lockfile records a workspace link for `@openzeppelin/guardian-client`, not an npm tarball, so it does not need the new client version to exist on the registry. + +### Publishing Rust Crates + +Rust crates are published by the +[`Publish Rust Crates`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/.github/workflows/publish-crates.yml) workflow. +Publishing a GitHub Release automatically selects all four crates, validates +that the tag is `v` followed by their coordinated version, requests one +`release` environment approval, and uses crates.io trusted publishing. Cargo +receives all selected packages in one command and handles dependency-safe +publication. + +Configure a crates.io trusted publisher for each crate before the first +automated publication: + +```text +GitHub owner: OpenZeppelin +Repository: guardian +Workflow: publish-crates.yml +Environment: release +``` + +Use a manual dry run to validate one or more selected crates without requesting +approval or credentials: + +```bash +gh workflow run publish-crates.yml \ + --ref main \ + -f dry-run=true \ + -f guardian-shared=true \ + -f guardian-client=true \ + -f miden-confidential-contracts=true \ + -f miden-multisig-client=true +``` + +A manual run with `dry-run=false` publishes only the selected crates after one +approval. If an unselected internal prerequisite is not already visible at the +exact coordinated version, validation fails before publication. + +Publication is rerunnable. Exact versions already on crates.io are reported as +already published and omitted from the Cargo command. If a publication is +interrupted, rerun it; the preflight skips versions that reached crates.io and +Cargo processes the remainder. Trusted-publishing failure fails closed; the +workflow has no registry-token fallback. + +### Publishing TypeScript Packages + +Packages live in the `packages/` npm workspace. Install once from that directory, then publish in dependency order. `miden-multisig-client` links the in-repo `@openzeppelin/guardian-client`; the published `package.json` still carries the `^` version range for consumers. + +```bash +cd packages +npm ci + +# 1. Build TypeScript packages +npm run build -w @openzeppelin/guardian-client +npm run build -w @openzeppelin/guardian-evm-client +npm run build -w @openzeppelin/guardian-operator-client +npm run build -w @openzeppelin/miden-multisig-client + +# 2. Publish base clients first (no internal deps) +npm publish -w @openzeppelin/guardian-client --access public +npm publish -w @openzeppelin/guardian-evm-client --access public +npm publish -w @openzeppelin/guardian-operator-client --access public + +# 3. Publish miden-multisig-client (depends on guardian-client) +npm publish -w @openzeppelin/miden-multisig-client --access public +``` + +Publishing a GitHub release runs the `Publish NPM Packages` workflow for all +four packages. A manual workflow run can select any subset of the four package +inputs; at least one package must be selected. The `dry-run` input applies only +to that selected subset. All selected packages run serially in one protected +`release` environment job, so the workflow requires one approval. + +### Post-Release + +1. Create a draft GitHub Release for review: + +```bash +gh release create v --generate-notes --draft +``` + +2. Publish the draft when the coordinated Rust, TypeScript, and server-image + release workflows should start: + +```bash +gh release edit v --draft=false +``` + +3. Approve the `release` environment jobs that should publish. The Rust + workflow summary records the source SHA, coordinated version, + authentication mode, and ordered per-crate outcomes without credentials. + +--- + +## Additional Resources + +- [Miden Documentation](https://docs.miden.io/) +- [GUARDIAN Documentation](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/server/README.md) + - [GUARDIAN Specification](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/spec/index.md) +- [Example Applications](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/examples/) + - [Web Example](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/examples/web/) + - [CLI Demo](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/examples/demo/) + - [Rust Example](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/examples/rust/) diff --git a/versioned_docs/version-0.16/builder/miden-guardian/reference/openapi.md b/versioned_docs/version-0.16/builder/miden-guardian/reference/openapi.md new file mode 100644 index 00000000..94a9627e --- /dev/null +++ b/versioned_docs/version-0.16/builder/miden-guardian/reference/openapi.md @@ -0,0 +1,68 @@ +--- +title: "OpenAPI specification" +sidebar_position: 2 +--- +Guardian's HTTP API is described by an [OpenAPI 3.1](https://spec.openapis.org/oas/v3.1.0) +specification generated directly from the server source with +[`utoipa`](https://docs.rs/utoipa). Because the spec is derived from the +same `#[utoipa::path]` annotations and `#[derive(ToSchema)]` models the +handlers use, it cannot drift from the implementation — and CI fails if +the committed files fall out of sync (see below). + +## Surfaces and files + +Guardian exposes two HTTP surfaces — the **client** API (tag `client`) +consumed by SDKs/packages and the operator **dashboard** API (tag +`dashboard`) — plus the feature-gated EVM API (tag `evm`). Four specs are +committed under [`docs/`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/docs), generated with the `evm` feature so they +document every route: + +| File | Contents | Maps to | +| --- | --- | --- | +| [`openapi.json`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/docs/openapi.json) | combined client + dashboard + evm | served at runtime | +| [`openapi-client.json`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/docs/openapi-client.json) | client API only | `packages/guardian-client` | +| [`openapi-dashboard.json`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/docs/openapi-dashboard.json) | dashboard API only | `packages/guardian-operator-client` | +| [`openapi-evm.json`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/docs/openapi-evm.json) | EVM API only | `packages/guardian-evm-client` | + +Splitting per surface keeps SDK generation scoped and avoids exposing +unrelated schemas to a given client. + +**Served at runtime:** `GET /api-docs/openapi.json` returns the combined +spec for the routes the running binary actually mounts (EVM routes appear +only when the server is built with `--features evm`). + +Point any OpenAPI tooling — Swagger UI, ReDoc, or a client-SDK +generator — at any of these. + +## Authentication + +The specs declare security schemes so tools render auth correctly: + +- **Client API** — three required `apiKey` headers, `x-pubkey`, + `x-signature`, `x-timestamp` (see [`spec/api.md`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/spec/api.md) + "Miden Request Signing"). Public endpoints (`/pubkey`, `/status`, and + its root alias `/`) carry no requirement. +- **Dashboard API** — the `guardian_operator_session` cookie + (`operator_session`). The login challenge/verify endpoints are public. +- **EVM API** — the `guardian_evm_session` cookie (`evm_session`). The + challenge/verify endpoints are public. + +## Regenerating the checked-in files + +Run the `gen-openapi` binary with the `evm` feature, writing into +`docs/`: + +```sh +cargo run --features evm --bin gen-openapi -- docs +``` + +To verify the committed files are current without writing (what CI runs): + +```sh +cargo run --features evm --bin gen-openapi -- --check docs +``` + +Regenerate and commit the specs whenever you add or change an HTTP +handler, its request/response/query/path types, auth behavior, or a +model that appears on the wire — the same way the proto contract is kept +in sync. The `OpenAPI Spec Drift` CI job enforces this. diff --git a/versioned_docs/version-0.16/builder/miden-guardian/runbooks/_category_.json b/versioned_docs/version-0.16/builder/miden-guardian/runbooks/_category_.json new file mode 100644 index 00000000..b3118ef3 --- /dev/null +++ b/versioned_docs/version-0.16/builder/miden-guardian/runbooks/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Runbooks", + "position": 6 +} diff --git a/versioned_docs/version-0.16/builder/miden-guardian/runbooks/backup-restore.md b/versioned_docs/version-0.16/builder/miden-guardian/runbooks/backup-restore.md new file mode 100644 index 00000000..4756f101 --- /dev/null +++ b/versioned_docs/version-0.16/builder/miden-guardian/runbooks/backup-restore.md @@ -0,0 +1,370 @@ +--- +title: "Database Backup and Restore Runbook" +--- +Operational guide for verifying that the Guardian RDS database is backed up +and for restoring it after data loss. Companion to the durability overview in +[`PRODUCTION.md`](../operations/production.md#durability-and-recovery) (what is +guaranteed) and [`architecture/infra.md`](../architecture/infra.md) (how the +stack fits together). + +> **Audience:** operators with AWS (RDS, ECS, Secrets Manager) and Terraform +> access for the target Guardian stack. + +All Guardian state lives in the stack's RDS instance +(`-postgres`) — account state, deltas, proposals, account metadata, +and audit rows. The server tasks are stateless; nothing else needs to be +restored. + +## What the stack backs up for you + +- **Automated backups.** A daily snapshot plus continuous WAL archiving, + retained for `rds_backup_retention_days` (default 7). This enables restore + to any second within the window, up to `LatestRestorableTime` (typically + within ~5 minutes of now). +- **Final snapshot on destroy.** In the prod stage, + `./scripts/aws-deploy.sh cleanup` takes a final snapshot named + `-postgres-final` before deleting the instance. +- **Deletion protection.** In the prod stage the instance refuses deletion + until `rds_deletion_protection` is explicitly turned off. + +What it does **not** back up: + +- **Terraform state.** State files are local to the deploy host (see + [`architecture/infra.md`](../architecture/infra.md#things-that-are-deliberately-not-here)). +- **Secrets Manager values.** The stack-managed `DATABASE_URL` secret is + recreated by Terraform, but the ACK signing keys and — critically — the + **storage encryption key** are not derivable from anything. If storage + encryption is enabled, a database backup without the key is ciphertext: + keep an out-of-band copy of the key secret per + [`secrets.md`](./secrets.md). + +## Verify backups (do this now, not during an incident) + +```bash +aws rds describe-db-instances \ + --db-instance-identifier -postgres \ + --query 'DBInstances[0].{Retention:BackupRetentionPeriod,DeletionProtection:DeletionProtection,MultiAZ:MultiAZ,LatestRestorableTime:LatestRestorableTime}' + +aws rds describe-db-snapshots \ + --db-instance-identifier -postgres \ + --query 'DBSnapshots[*].{Id:DBSnapshotIdentifier,Created:SnapshotCreateTime,Status:Status}' +``` + +For a prod stack, expect `Retention >= 7`, `DeletionProtection: true`, a +recent `LatestRestorableTime`, and at least one `available` snapshot. If +retention is `0`, backups are **off** — fix the stack before anything else. + +## Restore procedure (stack intact) + +This is the path for data loss while the stack still exists — bad data was +written, a migration went wrong, the instance failed. If the whole stack was +destroyed, start at +[Full-stack recovery](#full-stack-recovery-after-a-destroy) instead; it +recreates the prerequisites these steps reference and then re-enters here. + +RDS restores never overwrite an instance in place: both point-in-time and +snapshot restores create a **new** instance. The stack's `DATABASE_URL` +secret is derived by Terraform from the database endpoint (in prod, the RDS +Proxy endpoint, whose target is the instance), and endpoints follow the +instance **identifier**. So the procedure is: restore to a temporary +identifier, then swap names so the restored instance answers at the original +endpoint, then point Terraform state at the restored instance and re-apply. +The state step is not optional: the AWS provider (v5) tracks +`aws_db_instance` by its **DbiResourceId**, which survives renames — after +the swap, Terraform still tracks the renamed-away old instance, and an apply +without the state fix would try to rename it back onto the restored one. +Step 4 covers it. + +### 1. Stop writes + +Scale the server to zero so no deltas land while the database is swapped. +On a stack with ECS autoscaling (the prod default), pin the autoscaling +range first or it will immediately scale back up: + +```bash +aws application-autoscaling register-scalable-target \ + --service-namespace ecs \ + --resource-id service/-cluster/-server \ + --scalable-dimension ecs:service:DesiredCount \ + --min-capacity 0 --max-capacity 0 + +aws ecs update-service --cluster -cluster \ + --service -server --desired-count 0 +``` + +On a dev stack autoscaling is off by default — skip the +`register-scalable-target` call (it would create a scaling target Terraform +does not manage) and run only `update-service`. + +The final `terraform apply` in step 4 restores both settings. + +### 2. Restore to a temporary identifier + +Point-in-time (preferred — pick the moment just before the loss): + +```bash +aws rds restore-db-instance-to-point-in-time \ + --source-db-instance-identifier -postgres \ + --target-db-instance-identifier -postgres-restored \ + --restore-time 2026-08-04T12:00:00Z \ + --db-subnet-group-name -postgres-subnets \ + --vpc-security-group-ids +``` + +Or from a specific snapshot (a daily automated one, or the final snapshot +during [full-stack recovery](#full-stack-recovery-after-a-destroy)): + +```bash +aws rds restore-db-instance-from-db-snapshot \ + --db-instance-identifier -postgres-restored \ + --db-snapshot-identifier \ + --db-subnet-group-name -postgres-subnets \ + --vpc-security-group-ids +``` + +Pass the subnet group and security group explicitly — restores do **not** +inherit them from the source. Find the security group ID with: + +```bash +aws ec2 describe-security-groups \ + --filters Name=group-name,Values=-postgres-sg \ + --query 'SecurityGroups[0].GroupId' --output text +``` +Wait for the instance to become `available` +(`aws rds wait db-instance-available --db-instance-identifier -postgres-restored`). + +### 3. Swap identifiers + +```bash +aws rds modify-db-instance --db-instance-identifier -postgres \ + --new-db-instance-identifier -postgres-old --apply-immediately +aws rds wait db-instance-available --db-instance-identifier -postgres-old + +aws rds modify-db-instance --db-instance-identifier -postgres-restored \ + --new-db-instance-identifier -postgres --apply-immediately +aws rds wait db-instance-available --db-instance-identifier -postgres +``` + +Skip the first rename if the original instance is already gone. Deletion +protection blocks deletion, not renames, so this works on a prod instance. + +A rename changes the instance's DNS endpoint, and propagation can take up to +~10 minutes after the instance reports `available`. Before moving on, +confirm the canonical endpoint resolves: + +```bash +dig +short $(aws rds describe-db-instances \ + --db-instance-identifier -postgres \ + --query 'DBInstances[0].Endpoint.Address' --output text) +``` + +### 4. Reconcile Terraform and restart the service + +Point Terraform state at the restored instance first. The provider tracks +the database by DbiResourceId, which followed the old instance through its +rename — without this step, an apply tries to rename `-postgres-old` +back over the restored instance and fails. The state file lives at +`infra/terraform...tfstate` (unless `TF_STATE_PATH` was +overridden): + +```bash +terraform -chdir=infra state rm \ + -state=terraform...tfstate aws_db_instance.postgres + +terraform -chdir=infra import \ + -state=terraform...tfstate \ + -var stack_name= -var deployment_stage= \ + -var aws_region= -var server_image_uri=unused \ + aws_db_instance.postgres -postgres +``` + +(`server_image_uri` is the only variable without a default; import never +reads it, but Terraform requires a value to evaluate the configuration.) + +Then verify the plan shows only in-place updates to the database (no +destroy, no rename) before applying: + +```bash +./scripts/aws-deploy.sh plan +./scripts/aws-deploy.sh deploy --skip-build +``` + +The apply re-registers the RDS Proxy target, restores deletion protection +and other instance settings Terraform manages, refreshes the `DATABASE_URL` +secret, and returns the ECS service and autoscaling to their configured +capacity. On a prod stack, confirm the proxy target is healthy: + +```bash +aws rds describe-db-proxy-targets --db-proxy-name -postgres-proxy +``` + +### 5. Validate + +```bash +./scripts/aws-deploy.sh status +curl https:/// +curl https:///pubkey +``` + +Then run the relevant SDK or dashboard smoke path from +[`PRODUCTION.md`](../operations/production.md#production-checklist). + +### 6. Clean up + +Keep `-postgres-old` until the restored stack has been validated in +real use. Then snapshot and delete it: + +```bash +aws rds modify-db-instance --db-instance-identifier -postgres-old \ + --no-deletion-protection --apply-immediately +aws rds wait db-instance-available --db-instance-identifier -postgres-old +aws rds delete-db-instance --db-instance-identifier -postgres-old \ + --final-db-snapshot-identifier -postgres-old-final +``` + +## Full-stack recovery (after a destroy) + +`./scripts/aws-deploy.sh cleanup` destroys everything Terraform manages — +not just the database but also the subnet group, security group, RDS Proxy, +ECS service, and the Terraform state entries for all of them. The final +snapshot (`-postgres-final`) survives, but automated backups are +deleted along with the instance, so point-in-time recovery is no longer +available. The stack-intact procedure cannot run against a +destroyed stack: its commands reference resources that no longer exist, and +a plain redeploy would try to *create* a database, not adopt a restored one. + +Recover in two moves: + +### 1. Recreate the stack with no running tasks + +```bash +TF_VAR_server_desired_count=0 \ +TF_VAR_server_autoscaling_max_capacity=0 \ +./scripts/aws-deploy.sh deploy --skip-build +``` + +This recreates every prerequisite — subnet group, security group, proxy, +secrets, ECS service — plus a **fresh, empty** database at the canonical +identifier, all tracked in Terraform state again. `--skip-build` reuses the +ECR image, which survives cleanup (ECR is not Terraform-managed); drop the +flag if the image is gone too. The two overrides pin both the desired count +and the autoscaling ceiling to zero tasks (desired count alone is not +enough — in prod the ceiling would default to 6 and target tracking could +start tasks against the empty database before the swap). + +### 2. Restore from the final snapshot + +Run [stack-intact step 2](#2-restore-to-a-temporary-identifier) using the +snapshot path with `--db-snapshot-identifier -postgres-final`. + +### 3. Reconcile the master password + +Unless the stack pins `postgres_password`, the master password is a +Terraform-generated random value **per state file** — the redeploy in step 1 +minted a new one, while the snapshot carries the old one. Every credential +Terraform writes (the `DATABASE_URL` secret, the proxy auth secret) uses the +new value, so the restored instance must be reset to match before the server +can log in: + +```bash +NEW_PASSWORD=$(aws secretsmanager get-secret-value \ + --secret-id /server/database-url \ + --query SecretString --output text | sed -E 's|^postgres://[^:]+:([^@]+)@.*|\1|') + +aws rds modify-db-instance \ + --db-instance-identifier -postgres-restored \ + --master-user-password "$NEW_PASSWORD" --apply-immediately +aws rds wait db-instance-available \ + --db-instance-identifier -postgres-restored +``` + +The generated password is alphanumeric-only, so extracting it from the URL +needs no decoding. If the stack pins `postgres_password` (same value across +deploys), the passwords already match and this step is a no-op. + +### 4. Swap and finish + +Continue the [stack-intact procedure](#restore-procedure-stack-intact) from +step 3. Step 1 (stop writes) is already satisfied, and the fresh empty +instance plays the role of the original: it gets renamed to +`-postgres-old` and eventually deleted in cleanup. Run the step 4 +re-apply **without** the two `TF_VAR_server_*` overrides so the service and +autoscaling return to their configured capacity. + +## After the restore: Guardian-level reconciliation + +A point-in-time restore rewinds Guardian's stored state. Infrastructure-level +loss is bounded by WAL shipping (~5 minutes), but any delta that +canonicalized **on-chain** after the restore point cannot be regenerated by +the guardian: guarded accounts are private on Miden, so the chain holds only +a commitment hash and the full state lives on client devices. + +Accounts whose on-chain state advanced past the restored database state fail +state-commitment verification until a client device holding the newer state +re-syncs it — the "Guardian database corruption" row in the +[`CONCEPTS.md` failure table](../getting-started/concepts.md#failure-and-recovery). Accounts +onboarded after the restore point disappear from the guardian entirely: +requests fail with `account_not_found` (HTTP 404, gRPC `NOT_FOUND`) rather than +failing verification. A device holding the account re-onboards via +`/configure`, which re-registers it with the state that device currently holds; +other cosigner devices then sync from the guardian. Accounts with no +post-restore-point activity are unaffected. + +## Logical backups (optional) + +For copies that live outside the AWS account (offline archives, cross-region +without a replica), take periodic `pg_dump` backups. Two constraints shape +the procedure: + +- The instance is not publicly accessible, so `pg_dump` must run from inside + the VPC. +- The Guardian runtime image ships `libpq5` only — **no `pg_dump`** + ([`Dockerfile`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/Dockerfile) `server-runner` stage) — so an ECS Exec + session into the server container needs the PostgreSQL client installed + first. The container runs as root and the install is ephemeral (gone when + ECS replaces the task), which is fine for a one-off dump. + +```bash +TASK_ARN=$(aws ecs list-tasks --cluster -cluster \ + --service-name -server --query 'taskArns[0]' --output text) +aws ecs execute-command --cluster -cluster --task "$TASK_ARN" \ + --container -server --interactive --command /bin/bash +``` + +The client's major version must be **at least** the server's — `pg_dump` +refuses servers newer than itself, and Debian bookworm's default +`postgresql-client` is 15 while the stack's engine is whatever AWS defaulted +to at creation (`rds_engine_version` is unpinned). Check the engine first: + +```bash +aws rds describe-db-instances --db-instance-identifier -postgres \ + --query 'DBInstances[0].EngineVersion' --output text +``` + +Inside the session, install the matching client major from the PGDG +repository (substitute the major version from the check above), then dump +using the `DATABASE_URL` already present in the container environment: + +```bash +apt-get update && apt-get install -y postgresql-common awscli +/usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y +apt-get install -y postgresql-client- + +pg_dump "$DATABASE_URL" --format=custom --file=/tmp/guardian-$(date +%F).dump +``` + +To get the dump out, paste short-lived operator credentials into the session +(the task role deliberately has no S3 grant) and upload: + +```bash +export AWS_ACCESS_KEY_ID=... AWS_SECRET_ACCESS_KEY=... AWS_SESSION_TOKEN=... +aws s3 cp /tmp/guardian-*.dump s3:/// +``` + +Alternatively, run a one-off Fargate task from the public `postgres:` +image, attached to the server task's security group, with the same +`DATABASE_URL` secret — avoids touching a serving container at the cost of +registering a task definition. + +If storage encryption is enabled, remember the dump is ciphertext without +the key secret. diff --git a/versioned_docs/version-0.16/builder/miden-guardian/runbooks/enable-db-tls.md b/versioned_docs/version-0.16/builder/miden-guardian/runbooks/enable-db-tls.md new file mode 100644 index 00000000..27939192 --- /dev/null +++ b/versioned_docs/version-0.16/builder/miden-guardian/runbooks/enable-db-tls.md @@ -0,0 +1,189 @@ +--- +title: "Enabling verified database TLS Runbook" +sidebar_position: 2 +--- +Operational guide for migrating an already-deployed Guardian stack from +encrypted-only (`sslmode=require`) to **verified** database TLS +(`sslmode=verify-full`). Companion to +[`SERVER_AWS_DEPLOY.md`](../operations/server-aws-deploy.md#database-tls-verification) (which +explains the mechanism) and [Database TLS](../reference/configuration.md#database-tls) (the +authoritative meaning of `sslmode`/`sslrootcert`). + +> **Audience:** operators with AWS Secrets Manager and ECS/Terraform write access +> for the target Guardian stack. + +## Why this is safe + +- **Opt-in.** Until `rds_ca_bundle_secret_arn` is set, nothing changes — + `DATABASE_URL` stays `sslmode=require`. Setting it is the only trigger. +- **Fail-closed, not service-down.** The service runs + `deployment_minimum_healthy_percent = 100` behind an ALB health check. If a + verifying task can't validate the certificate it exits and fails its health + check, so ECS **keeps the existing encrypt-only tasks serving** and the rollout + stalls. You fix the bundle and re-apply — no outage. +- **One-variable rollback.** Blank `rds_ca_bundle_secret_arn` and `terraform + apply` to revert to `sslmode=require`. + +## The proxy-vs-direct gotcha (read before prod) + +`prod`/`testnet` route `DATABASE_URL` through the **RDS Proxy**, whose certificate +is issued by **AWS Certificate Manager → Amazon Trust Services roots**. A +**direct** RDS instance (the default in non-prod, where the proxy is +`is_prod`-gated) instead presents the **Amazon RDS CA roots**. + +- The **combined bundle (region RDS roots + ATS roots)** covers both — which is + why it's mandatory. +- But validating on a *direct* staging DB does **not** exercise the proxy's + cert/SAN path. Either temporarily enable the proxy on staging to mirror prod, + or treat the prod cutover as the first real proxy validation (with rollback + ready). + +## Pre-flight checklist + +- [ ] The deployed server image **includes this feature** (older images don't + parse `verify-full`/`sslrootcert` and will fail to connect). Roll the new + image out first with verification still **off** (Step 0). +- [ ] Combined CA bundle built for the stack's region and **< 64 KiB** (Secrets + Manager rejects larger; do **not** use `global-bundle.pem`). +- [ ] You know the stack's topology (proxy vs direct) and have matched the test + plan to it. +- [ ] Rollback understood: blank the variable and re-apply. + +## How these commands run + +This stack is driven by [`scripts/aws-deploy.sh`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/scripts/aws-deploy.sh) +(the same tool used in [`SERVER_AWS_DEPLOY.md`](../operations/server-aws-deploy.md)), not raw +`terraform`. Target a stack with `STACK_NAME`/`DEPLOY_STAGE`, and pass the new +variable through Terraform's standard `TF_VAR_` mechanism — the script forwards +the environment to Terraform, which honors `TF_VAR_rds_ca_bundle_secret_arn` +because that variable is declared in [`infra/variables.tf`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/infra/variables.tf). + +```bash +export STACK_NAME=guardian-staging # your stack +export DEPLOY_STAGE=staging # dev | staging | testnet | prod +``` + +`deploy` builds + pushes a new image then applies; `deploy --skip-build` applies +Terraform against the **already-deployed** image (use this for the verification +flip, which needs no new image); `plan` previews; `status` / `logs` inspect. + +## Step 0 — Roll out the feature image with verification OFF + +Deploy the image containing verified DB TLS **without** setting +`rds_ca_bundle_secret_arn`. The stack stays `sslmode=require` but becomes +*capable* of verifying. This separates "new image" from "new verification" so a +regression is attributable to one change, not both. + +```bash +# from a checkout that includes this feature +./scripts/aws-deploy.sh deploy +./scripts/aws-deploy.sh status # confirm the new tasks are healthy +``` + +## Step 1 — Build the combined CA secret (per region) + +There is no `aws-deploy.sh` command for this (it's not an ACK secret), so create +it directly. The name is stack-scoped, matching the ACK convention: + +```bash +REGION=us-east-1 # the stack's region + +curl -sS "https://truststore.pki.rds.amazonaws.com/${REGION}/${REGION}-bundle.pem" -o rds.pem +: > ats.pem +for ca in AmazonRootCA1 AmazonRootCA2 AmazonRootCA3 AmazonRootCA4 SFSRootCAG2; do + curl -sS "https://www.amazontrust.com/repository/${ca}.pem" >> ats.pem; echo >> ats.pem +done +{ cat rds.pem; echo; cat ats.pem; } > rds-combined-ca.pem +test "$(wc -c < rds-combined-ca.pem)" -lt 65536 || { echo "bundle exceeds 64 KiB"; exit 1; } +grep -c "BEGIN CERTIFICATE" rds-combined-ca.pem # sanity: total root count + +aws secretsmanager create-secret \ + --name "${STACK_NAME}/server/rds-ca-bundle" \ + --secret-string file://rds-combined-ca.pem \ + --query ARN --output text # note the ARN for Step 2 +``` + +Create the secret **before** the apply that references its ARN. + +## Step 2 — Flip the stack to verify-full + +Point the variable at the secret ARN from Step 1 and preview, then apply against +the existing image (no rebuild needed — this is a config-only change): + +```bash +export TF_VAR_rds_ca_bundle_secret_arn="arn:aws:secretsmanager:::secret:/server/rds-ca-bundle-XXXXXX" + +./scripts/aws-deploy.sh plan # review +./scripts/aws-deploy.sh deploy --skip-build # apply against the deployed image +``` + +The plan should show: an `rds-ca-initializer` init container added; a shared +volume + read-only mount + `dependsOn { condition = SUCCESS }` on the server +container; an execution-role `GetSecretValue` grant for the secret ARN; and +`DATABASE_URL` changing to `…&sslmode=verify-full&sslrootcert=`. + +> Keep `TF_VAR_rds_ca_bundle_secret_arn` exported for every subsequent +> `aws-deploy.sh` invocation on this stack (or set it in the stack's tfvars), so +> later deploys don't silently revert the stack to `sslmode=require`. + +## Step 3 — Validate + +```bash +./scripts/aws-deploy.sh status # the new deployment should reach a healthy/steady state +./scripts/aws-deploy.sh logs # init container exited 0; no cert error; migrations ran; server listening +``` + +Expected: `rds-ca-initializer` exits 0 → Postgres reachable → migrations applied → HTTP +listening, with **no** `certificate verify failed`. A crash-looping new task with +a cert error means the bundle or endpoint is wrong — the old encrypt-only tasks +keep serving, so fix the secret and re-apply. + +## Step 4 — Promote to prod/testnet + +Repeat Steps 1–3 with `STACK_NAME`/`DEPLOY_STAGE` pointed at prod/testnet and a +prod-scoped secret, in a low-traffic window. Because traffic flows through the +**RDS Proxy**, this is where the ACM/ATS chain and the **proxy-endpoint SAN** are +verified for the first time — watch the rollout (`aws-deploy.sh status`/`logs`) +and keep the rollback ready. + +## Rollback + +```bash +unset TF_VAR_rds_ca_bundle_secret_arn # or remove it from the stack tfvars +./scripts/aws-deploy.sh deploy --skip-build +``` + +`DATABASE_URL` reverts to `sslmode=require`, the init container and mount are +removed, and tasks roll back to encrypt-only. (A *failed* verifying deploy never +displaces the healthy old tasks in the first place.) + +## Rotation (after cutover) + +CA roots rotate rarely, but when they do: update the secret value (it may hold +old and new roots together for overlap), then force a redeploy so the init +container rewrites the file. A value-only secret change doesn't alter the task +definition, so `aws-deploy.sh deploy` won't roll tasks on its own — force it: + +```bash +aws secretsmanager put-secret-value \ + --secret-id "${STACK_NAME}/server/rds-ca-bundle" \ + --secret-string file://rds-combined-ca.pem + +# cluster/service names follow the stack name; confirm via `aws-deploy.sh status` +aws ecs update-service --cluster "${STACK_NAME}-cluster" \ + --service "${STACK_NAME}-server" --force-new-deployment +``` + +Ensure the new roots are present before they become the only trusted ones. + +## Troubleshooting + +| Symptom | Likely cause | +|---|---| +| New task crash-loops with `certificate verify failed` | bundle doesn't cover the presented chain — proxy stack needs the **ATS roots**, direct needs the **RDS CA roots** (use the combined bundle) | +| `create-secret` fails on size | used `global-bundle.pem`; switch to the **region** bundle + ATS roots (< 64 KiB) | +| Startup error naming `sslrootcert` | init container didn't write the file — check its logs and the execution-role secret grant | +| Server won't parse `verify-full` | image predates the feature — complete Step 0 first | + +See [`TROUBLESHOOTING.md`](../operations/troubleshooting.md#server-fails-to-start) for the +full database-TLS failure-to-cause mapping. diff --git a/versioned_docs/version-0.16/builder/miden-guardian/runbooks/guardian-domain-migration.md b/versioned_docs/version-0.16/builder/miden-guardian/runbooks/guardian-domain-migration.md new file mode 100644 index 00000000..cf245eb2 --- /dev/null +++ b/versioned_docs/version-0.16/builder/miden-guardian/runbooks/guardian-domain-migration.md @@ -0,0 +1,266 @@ +--- +title: "Guardian Domain Migration" +--- +This runbook is only for the one-time OpenZeppelin hostname migration in +[issue #341](https://github.com/OpenZeppelin/guardian/issues/341). Normal +Guardian deployments should leave `ALIAS_SUBDOMAIN` unset. + +The migration keeps each legacy hostname available on the same ALB while the new +canonical hostname is verified. Both names route directly; there is no hostname +redirect. + +| Deployment | Network | Canonical `SUBDOMAIN` | Temporary `ALIAS_SUBDOMAIN` | +|---|---|---|---| +| Devnet | `MidenDevnet` | `guardian-devnet` | `guardian-stg` | +| Testnet | `MidenTestnet` | `guardian-testnet` | `guardian` | + +The ACM certificate configured through `ACM_CERTIFICATE_ARN` must cover both +names, or the legacy hostname's certificate must be supplied separately through +`ALIAS_ACM_CERTIFICATE_ARN` for SNI. + +## Select the stack explicitly + +Load shared credentials and deployment settings from `.env`, then override every +value that identifies the stack. Do not rely on the current values in `.env` +when selecting a state file manually. The `GUARDIAN_NETWORK_TYPE` exports pin +the network each stack already runs (the hosted devnet stack runs `MidenDevnet`, +the testnet stack `MidenTestnet`) so a stale `.env` value cannot switch the +server's network inside this domain-only apply. Both hosted records are already +Cloudflare-proxied, so the export blocks also pin `CLOUDFLARE_PROXIED=true` to +prevent this migration from changing their proxy mode. + +For testnet: + +```bash +set -a && source .env && set +a + +export AWS_REGION=us-east-1 +export STACK_NAME=guardian-prod +export DEPLOY_STAGE=prod +export GUARDIAN_NETWORK_TYPE=MidenTestnet +export DOMAIN_NAME=openzeppelin.com +export SUBDOMAIN=guardian-testnet +export ALIAS_SUBDOMAIN=guardian +export CLOUDFLARE_PROXIED=true +export ACM_CERTIFICATE_ARN="arn:aws:acm:us-east-1::certificate/" +export TF_STATE_PATH="$(pwd)/infra/terraform.guardian-prod.prod.tfstate" +``` + +For devnet: + +```bash +set -a && source .env && set +a + +export AWS_REGION=us-east-1 +export STACK_NAME=guardian +export DEPLOY_STAGE=dev +export GUARDIAN_NETWORK_TYPE=MidenDevnet +export DOMAIN_NAME=openzeppelin.com +export SUBDOMAIN=guardian-devnet +export ALIAS_SUBDOMAIN=guardian-stg +export CLOUDFLARE_PROXIED=true +export ACM_CERTIFICATE_ARN="arn:aws:acm:us-east-1::certificate/" +export TF_STATE_PATH="$(pwd)/infra/terraform.guardian.dev.tfstate" +``` + +`ACM_CERTIFICATE_ARN` must point at an issued certificate covering the canonical +hostname. If that certificate does not also cover the legacy hostname, also +export `ALIAS_ACM_CERTIFICATE_ARN` with the legacy hostname's existing +certificate. Both certificates must be in the ALB's AWS region. + +Inspect the certificate status and subject alternative names before changing +state or planning the deployment: + +```bash +CANONICAL_FQDN="${SUBDOMAIN}.${DOMAIN_NAME}" +LEGACY_FQDN="${ALIAS_SUBDOMAIN}.${DOMAIN_NAME}" + +aws acm describe-certificate \ + --region "$AWS_REGION" \ + --certificate-arn "$ACM_CERTIFICATE_ARN" \ + --query 'Certificate.{Status:Status,SANs:SubjectAlternativeNames}' \ + --output json + +if [ -n "${ALIAS_ACM_CERTIFICATE_ARN:-}" ]; then + aws acm describe-certificate \ + --region "$AWS_REGION" \ + --certificate-arn "$ALIAS_ACM_CERTIFICATE_ARN" \ + --query 'Certificate.{Status:Status,SANs:SubjectAlternativeNames}' \ + --output json +fi +``` + +Stop unless every displayed certificate is `ISSUED`. With one certificate, its +SAN list must cover both `$CANONICAL_FQDN` and `$LEGACY_FQDN`, either explicitly +or through a matching wildcard. With split certificates, the canonical +certificate must cover `$CANONICAL_FQDN` and the alias certificate must cover +`$LEGACY_FQDN`. + +With a split-certificate setup, the apply swaps the listener's default +certificate to the canonical one before it creates the SNI attachment for the +legacy certificate, so TLS on the live legacy hostname can fail for a brief +window during the apply. Prefer a single certificate covering both names; when +`ALIAS_ACM_CERTIFICATE_ARN` is unavoidable, schedule the apply for a +low-traffic window. + +Before continuing, authenticate and confirm that the selected state belongs to +the intended deployment: + +```bash +aws sts get-caller-identity +terraform -chdir=infra output -state="$TF_STATE_PATH" deployment_stage +terraform -chdir=infra output -state="$TF_STATE_PATH" ecs_cluster_name +terraform -chdir=infra output -state="$TF_STATE_PATH" custom_domain_url +``` + +Stop if these outputs do not match the selected stack, stage, and legacy +hostname. + +## Determine whether a state move is needed + +A state move is not part of a normal hostname addition and is unnecessary in +most deployments. It is needed only when all of the following are true: + +- the legacy hostname is already managed by this Terraform state; +- it is tracked at the primary DNS resource address; and +- the same apply must make the new hostname primary while retaining the legacy + hostname as the secondary record. + +This is the expected shape of the existing OpenZeppelin devnet and testnet +stacks. The move changes only Terraform's local ownership address; it does not +modify live DNS. + +Inspect the selected state: + +```bash +terraform -chdir=infra state list -state="$TF_STATE_PATH" | + rg '^(cloudflare_dns_record\.service|aws_route53_record\.service_alias)' +``` + +Skip the state move when the legacy record is not present at either primary +address below. Examples include a fresh stack, DNS managed outside Terraform, +or a state that has already been migrated. Do not import or move an unfamiliar +record without first reconciling who owns it. + +If a primary address is present, back up the state: + +```bash +chmod 600 "$TF_STATE_PATH" +install -m 600 "$TF_STATE_PATH" "${TF_STATE_PATH}.before-domain-migration" +``` + +Move only the record type shown by `state list`; move both only if the stack +actually manages both providers. Run each matching block separately. The +subshell keeps a failed guard from terminating the operator's shell, and each +block fails before the move unless the tracked record is the expected legacy +hostname. + +For Cloudflare-managed DNS, provider state may store `name` as either the +relative record name or the full hostname: + +```bash +( + EXPECTED_LEGACY_FQDN="${ALIAS_SUBDOMAIN}.${DOMAIN_NAME}" + CURRENT_LEGACY_NAME=$(terraform -chdir=infra state show -state="$TF_STATE_PATH" \ + 'cloudflare_dns_record.service[0]' | + sed -nE 's/^[[:space:]]*name[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/p') + case "$CURRENT_LEGACY_NAME" in + "$ALIAS_SUBDOMAIN"|"$EXPECTED_LEGACY_FQDN") ;; + *) + echo "Refusing state move: expected ${ALIAS_SUBDOMAIN} or ${EXPECTED_LEGACY_FQDN}, found ${CURRENT_LEGACY_NAME:-}" >&2 + exit 1 + ;; + esac + terraform -chdir=infra state mv -state="$TF_STATE_PATH" \ + 'cloudflare_dns_record.service[0]' \ + 'cloudflare_dns_record.service_secondary[0]' +) +``` + +For Route 53-managed DNS, the record's alias block holds a second `name` +attribute for the ALB, so the guard reads the top-level `fqdn` instead: + +```bash +( + EXPECTED_LEGACY_FQDN="${ALIAS_SUBDOMAIN}.${DOMAIN_NAME}" + CURRENT_LEGACY_FQDN=$(terraform -chdir=infra state show -state="$TF_STATE_PATH" \ + 'aws_route53_record.service_alias[0]' | + sed -nE 's/^[[:space:]]*fqdn[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/p') + test "$CURRENT_LEGACY_FQDN" = "$EXPECTED_LEGACY_FQDN" || { + echo "Refusing state move: expected ${EXPECTED_LEGACY_FQDN}, found ${CURRENT_LEGACY_FQDN:-}" >&2 + exit 1 + } + terraform -chdir=infra state mv -state="$TF_STATE_PATH" \ + 'aws_route53_record.service_alias[0]' \ + 'aws_route53_record.service_secondary[0]' +) +``` + +The hosted devnet and testnet states currently use Cloudflare rather than Route +53, but always trust `terraform state list` for the selected state. After a +move, run it again and confirm the record appears only at the corresponding +`service_secondary[0]` address. + +When DNS is managed outside Terraform, skip both state-move blocks and create or +retain the canonical and legacy records with that provider. Terraform still +attaches the required ACM certificates to the ALB. + +## Plan, deploy, and verify + +Run the plan with the same `STACK_NAME`, `DEPLOY_STAGE`, `SUBDOMAIN`, and +`ALIAS_SUBDOMAIN` used for the state move: + +```bash +./scripts/aws-deploy.sh plan +``` + +For Terraform-managed DNS, the plan must retain the legacy secondary record and +create the new canonical record. On the legacy Cloudflare record, only the +in-place `comment` update is expected; stop if `proxied`, `name`, `content`, or +any other behavior-affecting field changes. With external DNS, the plan should +not add DNS resources; confirm both records separately with that provider. +Certificate and output changes are expected in either case. Do not apply if the +plan destroys or replaces the legacy DNS record, ALB, ECS service, or database. + +A wrong `.env` value surfaces as an in-place `aws_ecs_task_definition` update +rather than a destroy or replace, so inspect that diff too. This domain-only +apply must not change the task definition at all: an environment diff (network +type, CORS origins, operator keys) means per-stack configuration leaked from +`.env`, and an image diff means the ECR `latest` tag has moved since the +stack's last deploy. Stop and pin the correct values before applying. After +reviewing the plan: + +```bash +./scripts/aws-deploy.sh deploy --skip-build +``` + +Verify HTTP and gRPC on both names; the `curl` and `grpcurl` calls each verify +the certificate chain and hostname as part of the TLS handshake. This example +shows testnet; repeat it for `guardian-devnet` and `guardian-stg`: + +```bash +curl --fail-with-body https://guardian-testnet.openzeppelin.com/pubkey +curl --fail-with-body https://guardian.openzeppelin.com/pubkey +grpcurl -import-path crates/server/proto -proto guardian.proto -d '{}' guardian-testnet.openzeppelin.com:443 guardian.Guardian/GetPubkey +grpcurl -import-path crates/server/proto -proto guardian.proto -d '{}' guardian.openzeppelin.com:443 guardian.Guardian/GetPubkey +``` + +Keep SDK, smoke-test, benchmark, and operational defaults on the legacy names +during the observation period. Switch consumers in a follow-up change only after +both canonical hostnames have been confirmed stable. + +## Remove the legacy hostname + +After consumers have moved and monitoring shows no required traffic on the +legacy hostname, unset `ALIAS_SUBDOMAIN` and `ALIAS_ACM_CERTIFICATE_ARN` while +keeping all canonical stack values pinned as above: + +```bash +unset ALIAS_SUBDOMAIN ALIAS_ACM_CERTIFICATE_ARN +./scripts/aws-deploy.sh plan +``` + +The cleanup plan must delete only the legacy DNS record, the optional secondary +listener certificate attachment, and migration-only outputs. Apply it with +`./scripts/aws-deploy.sh deploy --skip-build`, then verify the canonical HTTP and +gRPC endpoints again. diff --git a/versioned_docs/version-0.16/builder/miden-guardian/runbooks/horizontal-scaling.md b/versioned_docs/version-0.16/builder/miden-guardian/runbooks/horizontal-scaling.md new file mode 100644 index 00000000..6884c58a --- /dev/null +++ b/versioned_docs/version-0.16/builder/miden-guardian/runbooks/horizontal-scaling.md @@ -0,0 +1,142 @@ +--- +title: "Runbook: Horizontal Scaling (multiple Guardian replicas)" +--- +Guardian runs as 2–6 ECS tasks behind a round-robin load balancer in the prod +profile. This runbook covers what an operator must configure for a correct +high-availability (HA) deployment and how the server behaves across replicas. +Tracking: issue #242. + +## TL;DR — required for a correct multi-replica deployment + +| Setting | Why it matters across replicas | +|---|---| +| **Postgres backend** (`DATABASE_URL`) | Sessions, login challenges, and the canonicalization lease live in Postgres so they are shared. The filesystem backend is **dev-only** and is refused at startup in the prod stage. | +| **`GUARDIAN_DASHBOARD_CURSOR_SECRET`** (64 hex chars) | Pagination cursors are signed with this key. The prod Terraform profile injects one pre-created Secrets Manager value into every task. Outside that profile, an unset value makes the server **warn** and generate an ephemeral per-process secret; this degrades pagination across replicas, not custody. | +| **`GUARDIAN_ENV=prod`** | Activates the prod-stage startup guards (filesystem-backend refusal, 0-req/replica rate-limit refusal). Set by Terraform from `var.deployment_stage`. | +| **`GUARDIAN_MAX_REPLICAS`** | Rate-limit partitioning divisor (see below). Defaults from the autoscaling max capacity via Terraform. | + +With the published Postgres image + the prod Terraform profile, all of these are +set for you after the one-time cursor-secret bootstrap: + +```bash +DEPLOY_STAGE=prod STACK_NAME= ./scripts/aws-deploy.sh bootstrap-dashboard-cursor-secret +``` + +Normal deploys require that secret to exist and never create or rotate it. The +rest of this doc is for understanding and for non-default deployments. + +## Coordination is backend-derived (not a tunable) + +The coordination mode is determined by the **storage backend alone**: + +- **Postgres backend → shared coordination** (replica-safe). Always. No tunable + can turn this off — a missing or wrong env var can never silently revert a + Postgres deployment to per-process state. +- **Filesystem backend → in-memory, single-process** coordination (dev only). + +The startup log emits one line reflecting the resolved state, e.g.: + +```text +coordination mode=shared backend=postgres stage=prod max_replicas=6 cursor_secret=configured +``` + +If you ever see `mode=single-process backend=filesystem` on a deployment you +believe is multi-replica, that deployment is **not** safe to run with more than +one task. + +If the server is auto-built with the Postgres backend but coordination handles +were not wired (only possible via a manual/embedded builder), it **fails to +start** rather than falling back to per-process state. + +## Behavior across replicas + +- **Operator & EVM login**: a challenge issued on one replica verifies on any + other; an established session is honored everywhere; logout and expiry are + effective fleet-wide. +- **Canonicalization** runs on exactly one replica at a time via a Postgres + lease (`worker_leases`). Leadership transfers automatically to another replica + within one lease TTL (≈ 3× the canonicalization check interval) if the holder + crashes. Every canonicalization write (promotion, discard, retry bookkeeping) + validates the holder's fencing token against the lease row at its database + write boundary and only touches deltas still in candidate status. A write + that validated immediately before a leadership transfer may finish afterward; + account-level serialization and conditional state/candidate updates make that + overlap safe without globally locking the lease row. Promotion itself is + atomic — account state, cosigner auth sync, the candidate→canonical flip, and + the pending-candidate flag release commit together or not at all. A *planned* + stop (deploy, + scale-in) does not release the lease early today — there is no graceful + shutdown hook yet — so after replacing the lease holder, canonicalization + pauses for up to one TTL (~30s at the default 10s check interval) before the + new holder takes over. This is a stall, never a correctness issue. +- **Rate limiting** is per-process but partitioned (see below). + +## Failure modes (by design) + +- **Shared store (Postgres) briefly unavailable → auth fails closed.** Login and + authenticated requests are rejected (never bypassed) until Postgres returns. + The canonicalization leader steps down rather than risk double-processing, and + resumes automatically. This is a deliberate change from the old always- + available in-memory behavior. +- **DB connection budget**: each replica opens up to `GUARDIAN_DB_POOL_MAX_SIZE` + (default 32 in prod) connections, plus the metadata pool, plus per-request + session lookups. Coordination (sessions, challenges, the lease) does not add a + pool of its own — it shares the **metadata pool**, so per-request auth lookups + compete with metadata/canonicalization traffic for the same connections; size + `GUARDIAN_METADATA_DB_POOL_MAX_SIZE` with that in mind. With N replicas the + total can approach `N × (pools × size)`; keep it under Postgres + `max_connections`. Prod routes through RDS Proxy by default, which pools + server-side and absorbs much of this. + +## `GUARDIAN_MAX_REPLICAS` and rate limiting + +The configured global limits (`GUARDIAN_RATE_BURST_PER_SEC`, +`GUARDIAN_RATE_PER_MIN`) and the dashboard per-commitment auth budgets are +divided by `GUARDIAN_MAX_REPLICAS` so each replica enforces +`global / GUARDIAN_MAX_REPLICAS`. With round-robin distribution the fleet +aggregate stays at or below the configured global HTTP limit during steady-state +operation. + +- Default = the deployment's **steady-state capacity** (Terraform): the + greater of desired count and autoscaling max when autoscaling is enabled, or + the desired count otherwise. +- **Drives rate-limiting only.** It has no effect on coordination mode. +- **Tolerance band**: when fewer than the steady-state maximum are running, the + fleet over-throttles. HTTP keep-alive can also pin a client to one replica, + throttling it at `global / capacity`. Both are accepted fail-closed outcomes. +- **Rolling deployments**: ECS may briefly run up to + `server_deployment_maximum_percent` of the steady-state capacity. The + aggregate allowance can rise by the same factor during that window (2× with + the default 200%). This temporary relaxation is accepted to avoid permanent + steady-state over-throttling. +- **Override** (`var.guardian_max_replicas`): an explicit value is clamped **up** + to steady-state capacity. Setting it higher only over-throttles. +- **Invalid values fail fast in prod**: a set-but-unparsable or zero + `GUARDIAN_MAX_REPLICAS` would otherwise fall back to a divisor of 1 and + silently disable partitioning (fail-open). In the prod stage the server + refuses to start; non-prod warns and treats it as `1`. +- **Operator login uses the same divisor**: the dashboard's per-commitment + challenge and verification budgets use `GUARDIAN_MAX_REPLICAS`. The default + prod divisor is 6, yielding 1 request/second and 5 requests/minute per + endpoint on a keep-alive-pinned replica. The fleet-wide budgets can be + overridden with + `GUARDIAN_DASHBOARD_COMMITMENT_RATE_BURST_PER_SEC` and + `GUARDIAN_DASHBOARD_COMMITMENT_RATE_PER_MIN`. Shares are clamped to **≥1 per + replica** so operator login can never be fully denied. The defaults (6/second + and 30/minute) divide exactly across the default six replicas; a custom budget + below the divisor can exceed its nominal fleet-wide value because of the + liveness clamp. + +## Validate the coordination behavior locally + +To see this contract in action before deploying — shared sessions, single-owner +lease with failover, fail-closed auth, rate-limit partitioning — run the +[horizontal-scaling guide](../guides/horizontal-scaling.md): two replicas +behind a round-robin proxy sharing one Postgres, all on Docker Compose. + +## Filesystem backend is dev-only + +The filesystem backend keeps state local to one task (and does not persist audit +events). In the prod stage the server **refuses to start** on the filesystem +backend with an actionable error. Use it only for local development / single +process. diff --git a/versioned_docs/version-0.16/builder/miden-guardian/runbooks/secrets.md b/versioned_docs/version-0.16/builder/miden-guardian/runbooks/secrets.md new file mode 100644 index 00000000..337f686a --- /dev/null +++ b/versioned_docs/version-0.16/builder/miden-guardian/runbooks/secrets.md @@ -0,0 +1,521 @@ +--- +title: "Secrets and Key Management Runbook" +sidebar_position: 1 +--- +Operational guide for the secrets Guardian relies on in production. +Companion to [`docs/architecture/infra.md`](../architecture/infra.md), which +explains *which* AWS resources hold each secret; +this doc covers *how* to bootstrap, replace, and respond to compromise. + +> **Audience:** operators with AWS Secrets Manager and ECS write access for +> the target Guardian stack. + +## Categories at a glance + +| Category | Stored in | Lifecycle | Who reads it | +|---|---|---|---| +| `DATABASE_URL` | Secrets Manager (`/server/database-url`) | Managed by Terraform | ECS task **execution** role, at task start | +| RDS Proxy credentials (prod) | Secrets Manager (`/server/database-credentials`) | Managed by Terraform | RDS Proxy IAM role | +| ACK signing keys (prod) | Secrets Manager — IDs selected by `GUARDIAN_ACK_{FALCON,ECDSA}_SECRET_ID` env vars; default `guardian-prod/server/ack-{falcon,ecdsa}-secret-key`; Terraform sets per-stack `${stack_name}/server/ack-{falcon,ecdsa}-secret-key` | Bootstrapped once via `aws-deploy.sh bootstrap-ack-keys`; never rotated by deploys; replacement is incident/migration work | ECS task **runtime** role, at server startup | +| Storage encryption key (optional) | Secrets Manager — ID from `GUARDIAN_STORAGE_ENCRYPTION_KEY_SECRET_ID` | Created once against an empty store; rotate by adding keys to the structured secret | ECS task **runtime** role, at server startup (loaded once, cached) | +| Operator public keys | Secrets Manager (Terraform-managed or pre-existing ARN) | Updated by editing Terraform var or rotating the secret value | ECS task runtime role, on each dashboard challenge **and each authenticated `/dashboard/*` request** (hot-reloaded — no restart needed) | +| EVM allowed chains + RPC URLs | Secrets Manager (Terraform-managed) | Updated by editing `config/evm/chains.json` and redeploying | ECS task execution role; surfaced as env to the task | + +The ACK secret name is one value that travels through three places. They +have **different variable names by design** — each layer has a distinct +job — but they always carry the same string: + +| Layer | Variable | Lives where | Job | +|---|---|---|---| +| 1. Deploy-time | `GUARDIAN_ACK_FALCON_SECRET_NAME` / `_ECDSA_SECRET_NAME` | Your shell when running `scripts/aws-deploy.sh` | Operator-facing override. The script passes it into Terraform. | +| 2. Terraform | `guardian_ack_falcon_secret_name` / `_ecdsa_secret_name` | [`infra/variables.tf`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/infra/variables.tf), [`infra/data.tf:104-105`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/infra/data.tf#L104) | Creates / looks up the Secrets Manager entry and renders the ECS task definition. | +| 3. Runtime | `GUARDIAN_ACK_FALCON_SECRET_ID` / `_ECDSA_SECRET_ID` | The ECS task env, read by [`secrets_manager.rs:10-13`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/server/src/ack/secrets_manager.rs#L10) | What the server actually consults at startup. | + +Resolution order in each layer: + +1. **Deploy env (`_SECRET_NAME`).** If unset, the deploy script falls + through to `TF_VAR_guardian_ack_*_secret_name`, then to + `${STACK_NAME}/server/ack-{falcon,ecdsa}-secret-key` + ([`aws-deploy.sh:324-329`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/scripts/aws-deploy.sh#L324)). +2. **Terraform variable.** If unset, defaults to + `${stack_name}/server/ack-{falcon,ecdsa}-secret-key`. Renders the ECS + task definition with `GUARDIAN_ACK_*_SECRET_ID` set + ([`infra/ecs.tf:105-110`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/infra/ecs.tf#L105)). +3. **Server runtime env (`_SECRET_ID`).** If unset (unusual — only + happens in non-Terraform prod-mode launches), falls back to the + code-level defaults `guardian-prod/server/ack-{falcon,ecdsa}-secret-key`. + +There is a deliberate drift between the code default +(`guardian-prod/...`) and the Terraform default (`${stack_name}/...`, +no `-prod`). In the reference AWS deploy the server always reads the +Terraform-derived name because the ECS task definition always sets the +`_SECRET_ID` env var — the code default only matters for hand-rolled +prod-mode launches. + +## ACK signing keys + +ACK keys (one Falcon, one ECDSA) are Guardian's own response signers. +Clients pin Guardian's pubkey via `GetPubkey` on first contact and verify +every response thereafter — **stable identity matters**. Treat ACK key +replacement as a Guardian identity change, not a routine secret rotation. + +For Miden multisig accounts, the Guardian commitment is also stored in +account state. Proposal execution checks the live server commitment +against that stored account commitment before using a new ACK signature. +If the Secrets Manager ACK values are replaced without moving accounts to +the new Guardian commitment, normal proposal execution for existing +accounts will fail with a Guardian commitment mismatch. A `SwitchGuardian` +proposal is the account-level migration path for changing that stored +commitment. + +### Where the ACK secrets come from + +`GUARDIAN_ACK_SECRET_PROVIDER` selects the source of the ACK signing keys: + +| Value | Source | Default for | Identity | +|---|---|---|---| +| `aws` | AWS Secrets Manager (IDs from `GUARDIAN_ACK_{FALCON,ECDSA}_SECRET_ID`) | `GUARDIAN_ENV=prod` | Stable | +| `file` | Local files (paths from `GUARDIAN_ACK_{FALCON,ECDSA}_SECRET_PATH`) | — | Stable | +| `none` | Ephemeral keys generated in the keystore on every boot | everything else | **Changes every restart** | + +When unset, it falls back to the historical behavior: `aws` in prod, +`none` otherwise. The `none` path is dev-only — a fresh identity on each +restart freezes every account that pinned the previous commitment, with +per-account `SwitchGuardian` as the only recovery. + +### Self-hosted: stable identity without AWS + +A self-hosted, non-AWS Guardian keeps a fixed identity with the `file` +provider. Set `GUARDIAN_ACK_SECRET_PROVIDER=file` and point +`GUARDIAN_ACK_FALCON_SECRET_PATH` / `GUARDIAN_ACK_ECDSA_SECRET_PATH` at files +holding the hex-encoded secret keys +([`file_provider.rs`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/server/src/ack/file_provider.rs)). The file +format is the hex string emitted by `ack-keygen` — identical to what Secrets +Manager stores — so the same key material is portable between the two providers. + +Generate the pair once and write each value to its own file: + +```bash +cargo run --quiet -p guardian-server --bin ack-keygen \ + | tee /dev/stderr \ + | { read -r json; \ + jq -rj '.falcon_secret_key' <<<"$json" > ack-falcon-secret-key; \ + jq -rj '.ecdsa_secret_key' <<<"$json" > ack-ecdsa-secret-key; } +chmod 600 ack-falcon-secret-key ack-ecdsa-secret-key +``` + +Treat these files like any private key: keep them out of version control and +image layers, and back them up — losing them is a Guardian identity change, with +the same `SwitchGuardian` migration path described above. On Unix the server +**enforces** owner-only permissions and refuses to start if the file is readable +by group or others, so a bare file on disk must be `0600` and a Kubernetes secret +mount needs `defaultMode: 0400` (the `0644` default is rejected). + +With `GUARDIAN_ACK_ECDSA_BACKEND=aws-kms` the ECDSA key comes from KMS and its +file is never read, so you can omit `GUARDIAN_ACK_ECDSA_SECRET_PATH` entirely on +that path; only the Falcon file is required. + +### Hosted ECDSA backend (AWS KMS) + +The ECDSA ACK signer can be backed by AWS KMS instead of a Secrets Manager +secret, so the private key never enters the Guardian process. Set +`GUARDIAN_ACK_ECDSA_BACKEND=aws-kms` and `GUARDIAN_ACK_ECDSA_KMS_KEY_ID` to a KMS +key with spec `ECC_SECG_P256K1` and usage `SIGN_VERIFY`. Grant the ECS task role +`kms:GetPublicKey` and `kms:Sign` on the key (Terraform variable +`guardian_ack_ecdsa_kms_key_arn`). On this path the ECDSA secret in Secrets +Manager is not used and need not exist; Falcon is unaffected. + +Provisioning, rotation, and deletion of the KMS key are performed with the +provider's own tooling — Guardian only signs with an existing key. Because a KMS +key is a distinct keypair, moving an existing deployment to KMS (or rotating the +KMS key) changes Guardian's ECDSA identity and is a Guardian identity change, not +a routine rotation: the same `SwitchGuardian` migration path described above +applies to existing accounts. + +#### Create the key + +The key spec and usage are immutable after creation, and they must match +exactly — the server fails its startup sign probe otherwise. Create the key +once, out of band, and keep its lifecycle separate from the Terraform stack so a +stack teardown never schedules the signing identity for deletion. + +The deploy script creates the key with the correct spec and an +`alias/-ack-ecdsa` alias, refusing to overwrite an existing one, and +prints the ARN to set: + +```bash +STACK_NAME= ./scripts/aws-deploy.sh bootstrap-kms-ecdsa-key +``` + +Or do it by hand: + +```bash +KEY_ID=$(aws kms create-key \ + --key-spec ECC_SECG_P256K1 \ + --key-usage SIGN_VERIFY \ + --description "Guardian ACK ECDSA signer" \ + --query KeyMetadata.KeyId --output text) + +aws kms create-alias \ + --alias-name alias/-ack-ecdsa \ + --target-key-id "$KEY_ID" +``` + +Then pass the key ARN (or the alias ARN) to Terraform as +`guardian_ack_ecdsa_kms_key_arn`; the deploy grants the ECS task role +`kms:GetPublicKey` + `kms:Sign` on it and sets `GUARDIAN_ACK_ECDSA_BACKEND` / +`GUARDIAN_ACK_ECDSA_KMS_KEY_ID` on the server. KMS does not support automatic +rotation for asymmetric keys, which is correct here — rotating a signing +identity is the deliberate `SwitchGuardian` migration above, never automatic. To +retire a key, schedule deletion only after every account has migrated off the +old commitment. + +#### Prod deploy: KMS ECDSA + Secrets Manager Falcon + +The full sequence for a prod deploy where ECDSA is KMS-backed and Falcon stays +in Secrets Manager. Order matters: the deploy script keys off +`TF_VAR_guardian_ack_ecdsa_kms_key_arn` (the env var, not `terraform.tfvars`) to +decide whether to create and require the ECDSA Secrets Manager secret, so it +must be exported **before** `bootstrap-ack-keys` and `deploy`. + +```bash +export DEPLOY_STAGE=prod STACK_NAME= + +# 1. Create the KMS key; copy the printed ARN. +./scripts/aws-deploy.sh bootstrap-kms-ecdsa-key + +# 2. Export it so the script and Terraform both see KMS mode. +export TF_VAR_guardian_ack_ecdsa_kms_key_arn="arn:aws:kms:...:key/" + +# 3. Bootstrap ACK secrets. With the ARN exported this creates only the +# Falcon secret and skips ECDSA. +./scripts/aws-deploy.sh bootstrap-ack-keys + +# 4. Deploy. Terraform grants the task role kms:Sign + kms:GetPublicKey and +# injects GUARDIAN_ACK_ECDSA_BACKEND / GUARDIAN_ACK_ECDSA_KMS_KEY_ID. +./scripts/aws-deploy.sh deploy +``` + +If the ARN is set in `terraform.tfvars` but not exported, Terraform still uses +KMS, but `bootstrap-ack-keys` would create an unused ECDSA secret and `deploy` +would fail validation demanding it — so export it. + +### Bootstrap (first prod deploy) + +```bash +DEPLOY_STAGE=prod STACK_NAME= ./scripts/aws-deploy.sh bootstrap-ack-keys +``` + +What that command does ([`scripts/aws-deploy.sh`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/scripts/aws-deploy.sh)): + +1. Refuses to run if a secret it would create already exists. +2. Generates key material locally via + `cargo run --bin ack-keygen` (no key ever leaves the operator's host + except via the `aws secretsmanager create-secret` call). +3. Creates the Falcon and ECDSA secrets in Secrets Manager with the generated + values. When the ECDSA signer is KMS-backed + (`TF_VAR_guardian_ack_ecdsa_kms_key_arn` set), it creates only the Falcon + secret and skips ECDSA — use `bootstrap-kms-ecdsa-key` for the KMS key. + +Verify. The deploy script resolves the active IDs as +`${GUARDIAN_ACK_*_SECRET_NAME:-${TF_VAR_guardian_ack_*_secret_name:-${STACK_NAME}/server/ack-*-secret-key}}` +([`aws-deploy.sh:324-329`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/scripts/aws-deploy.sh#L324)). Mirror that +locally: + +```bash +FALCON="${GUARDIAN_ACK_FALCON_SECRET_NAME:-${TF_VAR_guardian_ack_falcon_secret_name:-${STACK_NAME:-guardian}/server/ack-falcon-secret-key}}" +ECDSA="${GUARDIAN_ACK_ECDSA_SECRET_NAME:-${TF_VAR_guardian_ack_ecdsa_secret_name:-${STACK_NAME:-guardian}/server/ack-ecdsa-secret-key}}" + +aws secretsmanager describe-secret --secret-id "$FALCON" +aws secretsmanager describe-secret --secret-id "$ECDSA" +``` + +Subsequent `aws-deploy.sh deploy` runs assert these secrets exist +([`aws-deploy.sh:331`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/scripts/aws-deploy.sh#L331)) and fail fast +otherwise. + +### Replacement + +ACK replacement is **not** part of the regular deploy cycle and should not +be scheduled as a routine annual rotation. Use it when no accounts are +bound to the old Guardian commitment, when standing up a replacement +Guardian, or as part of incident response after suspected key exposure. + +Before replacing ACK values for a live stack, decide how existing accounts +will be migrated: + +- Prefer a `SwitchGuardian` flow that moves each account to a Guardian + endpoint whose `GetPubkey` already returns the new commitment. +- For emergency compromise response, replacing the secret immediately + stops the old identity from signing new ACKs, but existing accounts must + still be moved to the new commitment before normal non-switch proposal + execution resumes. +- Downstream clients that cache or pin Guardian identity must refetch + `GetPubkey` after the change. + +Procedure: + +1. Generate new key material on a trusted host: + ```bash + cargo run --quiet --package guardian-server --bin ack-keygen > /tmp/ack-keys.json + ``` +2. Put new values into Secrets Manager — `update-secret` creates a new + version without disturbing the secret ID. Reuse the same + `$FALCON` / `$ECDSA` IDs you resolved in the Verify block above so + multi-stack deploys hit the right secret: + ```bash + FALCON_VALUE=$(jq -r .falcon_secret_key /tmp/ack-keys.json) + ECDSA_VALUE=$(jq -r .ecdsa_secret_key /tmp/ack-keys.json) + + aws secretsmanager update-secret \ + --secret-id "$FALCON" \ + --secret-string "$FALCON_VALUE" + aws secretsmanager update-secret \ + --secret-id "$ECDSA" \ + --secret-string "$ECDSA_VALUE" + ``` +3. Force a new ECS deployment so tasks restart and import the new keys: + ```bash + aws ecs update-service --cluster -cluster \ + --service -server --force-new-deployment + ``` +4. Confirm the replacement: + ```bash + curl https://guardian.openzeppelin.com/pubkey + ``` + Should return the new key material. +5. Securely shred `/tmp/ack-keys.json`. + +### Compromise response + +If you believe an ACK secret leaked: + +1. **Immediately** replace the ACK values using the procedure above — + bypass any change window. +2. Revoke any operator AWS credentials that could have read the secret + (CloudTrail `GetSecretValue` events scoped to those secret ARNs are the + audit trail). +3. Force-cycle all tasks (`update-service --force-new-deployment`) so the + old keys are no longer resident in any task's filesystem keystore. +4. Move affected accounts to the new Guardian commitment with the + account-level `SwitchGuardian` flow, or keep them paused/unavailable + until that migration is complete. +5. Inform downstream clients to refetch the pubkey and invalidate cached + verifiers. +6. File an incident referencing the secret ARN, the replacement timestamp, + and the CloudTrail evidence. + +## Storage encryption key + +Optional. Encrypts account state and delta/proposal payloads at rest (see +[`PRODUCTION.md`](../operations/production.md#storage-encryption)). The key never leaves the +process boundary beyond Secrets Manager; it is loaded once at startup and cached. + +### Bootstrap (against an empty store) + +On the standard AWS stack, use the deploy script. It generates the key locally, +creates the structured secret (`{active, keys}`), and refuses to overwrite an +existing one: + +```bash +DEPLOY_STAGE=prod STACK_NAME=guardian-prod \ + ./scripts/aws-deploy.sh bootstrap-storage-encryption-key +``` + +Then enable it on the next deploy by exporting the secret name (the bootstrap +command prints it). Setting `GUARDIAN_STORAGE_ENCRYPTION_SECRET_NAME` is what turns +encryption on: Terraform injects `GUARDIAN_STORAGE_ENCRYPTION_KEY_SECRET_ID` and +grants the ECS **runtime** role `secretsmanager:GetSecretValue` on the secret: + +```bash +GUARDIAN_STORAGE_ENCRYPTION_SECRET_NAME=guardian-prod/server/storage-encryption-key \ + DEPLOY_STAGE=prod STACK_NAME=guardian-prod ./scripts/aws-deploy.sh deploy +``` + +The bootstrap command defaults the name to `/server/storage-encryption-key`; +leaving `GUARDIAN_STORAGE_ENCRYPTION_SECRET_NAME` unset on deploy keeps storage in +plaintext. The equivalent manual creation writes the document to a restricted +temp file and passes `file://` so the key never lands in the process arg list +(`ps` / `/proc`): + +```bash +f=$(mktemp) +jq -nc --arg k "$(openssl rand -base64 32)" '{active:"k1", keys:{k1:$k}}' >"$f" +aws secretsmanager create-secret \ + --name guardian-prod/server/storage-encryption-key \ + --secret-string "file://$f" +rm -f "$f" +``` + +The server writes a one-time encryption marker on the first write; it will refuse +to start if the key is configured against a store that already holds plaintext +records. + +### Rotation + +Add a new key and repoint `active`, keeping the previous key so existing records +still decrypt: + +```bash +f=$(mktemp) +jq -nc --arg k1 "$OLD_B64" --arg k2 "$(openssl rand -base64 32)" \ + '{active:"k2", keys:{k1:$k1, k2:$k2}}' >"$f" +aws secretsmanager put-secret-value \ + --secret-id guardian-prod/server/storage-encryption-key \ + --secret-string "file://$f" +rm -f "$f" +``` + +New records use `k2`; old `k1` records keep decrypting. Do **not** remove a key +that any stored record still references. Bulk re-encryption tooling is not yet +provided. + +### Nonce budget + +Records are sealed with AES-256-GCM under a random 96-bit nonce. Per NIST +SP 800-38D, a single key must encrypt fewer than ~2³² records (about 4 billion) +before nonce-collision probability becomes non-negligible — and a collision +under one key is catastrophic, not graceful. + +Each state, delta, and proposal write is one encryption under the **active** +key. Treat 2³² as a per-key budget: [rotate](#rotation) the active key well +before any single `active` kid reaches ~1 billion writes (2³⁰, a ~4× margin), +or on a calendar cadence sized to the deployment's write rate. Rotation only +repoints `active` — old records keep decrypting under their original kid — so +the budget resets with each rotation. If 2³² is unreachable for the +deployment's lifetime, no cadence is required. + +### Compromise response + +Treat as a confidentiality breach of account state/history (not key material — +Guardian is non-custodial). Rotate the key, and because old records remain +readable with the old key, plan a re-encryption migration before retiring it. + +Note that the key the store was first initialized with (`init_kid`, recorded in +the store's encryption marker) must stay resolvable in the structured secret for +the server to start, **even after every record has been re-encrypted to a newer +kid**. Fully retiring the initial key therefore requires rewriting the store +marker, not just dropping the key from the secret — and there is no operator +command for that yet. If the initial key is itself the compromised one, track +the marker rewrite as migration work rather than assuming a key drop suffices. + +## Operator public keys + +Operators authenticate to the dashboard via Falcon-signed challenges +against an **allowlist** of public keys. Two ways to manage the list: + +- **Terraform-managed** — set `guardian_operator_public_keys` in + Terraform (or `GUARDIAN_OPERATOR_PUBLIC_KEYS_JSON` in the deploy env); + Terraform creates and maintains the secret. The variable is typed + `list(string)`, so this path only supports the legacy bare-key + array form — every entry implicitly gets `dashboard:read` only. +- **Pre-existing ARN** — set + `guardian_operator_public_keys_secret_arn` to an ARN you manage + externally. Terraform won't touch the contents. Use this path (or + the local `GUARDIAN_OPERATOR_PUBLIC_KEYS_FILE`) when you need the + object form to grant `accounts:pause` or any future permission. + +The secret payload is the JSON shape consumed by +[`dashboard/allowlist.rs`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/server/src/dashboard/allowlist.rs). +See [`docs/DASHBOARD.md`](../operations/dashboard.md#enrolling-an-operator) for the +payload schema and an enrollment walkthrough. + +### Adding or removing an operator + +The server **rereads the operator secret on every dashboard challenge and +every authenticated `/dashboard/*` request** — no ECS restart needed for +allowlist changes. + +Terraform-managed path: +1. Edit `guardian_operator_public_keys` (or the env var the deploy script + reads from). +2. `./scripts/aws-deploy.sh deploy` — Terraform updates the secret in + place. Effect is immediate. + +Externally-managed path: +1. Update the secret with `aws secretsmanager update-secret`. That's it. + +### Revoking a compromised operator + +The hot-reload path makes this fast — no deploy window required: + +```bash +aws secretsmanager update-secret \ + --secret-id \ + --secret-string "$(cat new-operator-list.json)" +``` + +The next challenge issuance or authenticated request from any task picks +up the new list and rejects the revoked key. Active sessions belonging +to the revoked operator are rejected at their next call: the per-request +reload re-validates the operator against the current allowlist on every +authenticated `/dashboard/*` hit +([`dashboard/state.rs:284-324`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/crates/server/src/dashboard/state.rs#L284)). + +Operator sessions are in-memory per task. There is no ALB session +stickiness, so on multi-task deployments operators may be routed to a +task that did not mint their session and prompted to re-authenticate +— this is the normal failure mode, not a revocation signal. Use the +audit / CloudTrail trail (below) to confirm a revocation took effect. + +## `DATABASE_URL` and RDS Proxy credentials + +Both are **created and owned by Terraform** ([`infra/rds.tf:45`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/infra/rds.tf#L45), +[`infra/rds.tf:50`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/infra/rds.tf#L50)). Do not edit them by hand — +the next `terraform apply` will overwrite your change. + +To rotate the database password: +1. Set `postgres_password` to a new value in `terraform.tfvars` (or unset + it to let Terraform regenerate via `random_password`). +2. `terraform apply` — Terraform updates the RDS master password, + `DATABASE_URL`, and the proxy credentials secret atomically. +3. ECS rolls the service automatically on the next deploy; force it + sooner with `update-service --force-new-deployment`. + +There is no separate read-only credential; the server connects with the +master user. This is a known production-hardening gap. + +## EVM allowed chains and RPC URLs + +Populated by the deploy script from +[`config/evm/chains.json`](https://github.com/OpenZeppelin/guardian/blob/v0.17.0/config/evm/chains.json) when +`GUARDIAN_SERVER_FEATURES=postgres,evm`. + +To add a chain: +1. Edit `config/evm/chains.json` — append a new entry to `chains` with + `chainId`, `name`, `network`, and `rpcUrl`. The `entrypointAddress` + is a single top-level field shared by every chain (exposed to the + server as `GUARDIAN_EVM_ENTRYPOINT_ADDRESS`) — do not add it + per-chain. +2. `./scripts/aws-deploy.sh deploy` — the script rebuilds the Secrets + Manager values from the JSON and Terraform updates the secret + versions. +3. ECS rolls and the new task reads the updated lists. + +To rotate an RPC URL (e.g. switch provider): +1. Edit `chains.json`, redeploy. No special handling — the server treats + chain config as a startup-time read. + +## Audit trail + +CloudTrail `GetSecretValue` events are scoped per-secret ARN. The +relevant principals you should see hitting each secret: + +| Secret | Expected principals | +|---|---| +| `DATABASE_URL` | ECS task execution role only | +| `database-credentials` (proxy) | RDS Proxy IAM role only | +| ACK Falcon / ECDSA | ECS task runtime role (on cold start) + operators running `bootstrap-ack-keys` or emergency replacement | +| Operator pubkeys | ECS task runtime role + operators updating the list | +| EVM chains / RPCs | ECS task execution role only | + +Any other principal touching these secrets is suspicious. + +## What is deliberately not here + +- **No KMS CMK** — Secrets Manager uses the default AWS-owned key. Move + to a CMK before enabling cross-account access. +- **No automated rotation lambdas** — secret changes are operator-driven. +- **No envelope encryption of ACK secret values** — Secrets Manager + protects the secret at rest; the value itself is the raw key material + that the server imports into its filesystem keystore on startup. diff --git a/versioned_docs/version-0.16/builder/migration/01-imports-dependencies.md b/versioned_docs/version-0.16/builder/migration/01-imports-dependencies.md new file mode 100644 index 00000000..3ed263a3 --- /dev/null +++ b/versioned_docs/version-0.16/builder/migration/01-imports-dependencies.md @@ -0,0 +1,198 @@ +--- +sidebar_position: 1 +title: "Imports & Dependencies" +description: "Crate version bumps, the VM 0.23 to 0.29 jump, MSRV 1.96, and the artifacts that must be regenerated for v0.16" +--- + +# Imports & Dependencies + +:::warning Breaking Change +The protocol crates move from 0.15.3 to **0.16.0**, `miden-client` from 0.15 to **0.16.0**, and the VM crates jump **0.23 → 0.29.1** — six minor versions, not one. `miden-crypto` was absorbed into the Miden VM workspace and now shares its version number (0.25 → **0.29.1**). The MSRV is Rust **1.96**. Because the MAST wire format, the package format, and several commitment preimages changed, **0.15 artifacts do not round-trip**: re-assemble every package from source, recreate your local store, and upgrade your node in lockstep with your client. +::: + +## Quick Fix + +```toml title="Cargo.toml" +# Replace these +miden-client = "0.15" +miden-client-sqlite-store = "0.15" +miden-protocol = "0.15.3" +miden-standards = "0.15.3" +miden-tx = "0.15.3" +miden-tx-batch-prover = "0.15.3" +miden-assembly = "0.23" +miden-core = "0.23" +miden-core-lib = "0.23" +miden-processor = "0.23" +miden-prover = "0.23" +miden-crypto = "0.25" + +# With these +miden-client = "0.16.0-rc.1" +miden-client-sqlite-store = "0.16.0-rc.1" +miden-protocol = "0.16.0-rc.6" +miden-standards = "0.16.0-rc.6" +miden-tx = "0.16.0-rc.6" +miden-tx-batch = "0.16.0-rc.6" # renamed from miden-tx-batch-prover +miden-assembly = "0.29.1" +miden-core = "0.29.1" +miden-core-lib = "0.29.1" +miden-processor = "0.29.1" +miden-prover = "0.29.1" +miden-crypto = "0.29.1" +``` + +```json title="package.json (Web SDK)" +{ + "@miden-sdk/miden-sdk": "0.16.0-rc.2", + "@miden-sdk/react": "0.16.0-rc.2" +} +``` + +Then run: + +```bash +cargo update && cargo build +``` + +If you encounter errors, continue reading for detailed migration steps. + +:::note Pin the exact version +The 0.16 protocol and client crates currently publish as `0.16.0-rc.N` pre-releases. Cargo does **not** match a pre-release against a plain requirement, so `miden-protocol = "0.16"` will fail to resolve. Pin the exact string as shown above until the final release is published. +::: + +:::warning 0.15 artifacts do not round-trip +The MAST wire format moved `0.0.3` → `0.0.4` and the package format `4.0.0` → `6.0.0`, so **serialized packages and `MastForest` blobs from 0.15 will not load**. The `.masl` library format no longer exists at all. Several commitment preimages also changed (ECDSA public keys, MMR peaks, empty domain-separated hashes), so derived values must be recomputed. Re-assemble from source and re-sync into a fresh store. +::: + +--- + +## Summary + +Every layer of the stack moves: + +- The **protocol crates** (`miden-protocol`, `miden-standards`, `miden-tx`, `miden-testing`) go `0.15.3` → `0.16.0`. +- The **VM crates** (`miden-assembly`, `miden-core`, `miden-core-lib`, `miden-processor`, `miden-prover`, `miden-mast-package`) go `0.23` → `0.29.1`. This is a much larger jump than previous releases and carries breaking MASM language changes — see [VM & Assembler Changes](./vm-assembler). +- **`miden-crypto`** goes `0.25` → `0.29.1`. It is no longer an independent crate line: it was imported into the Miden VM workspace and now shares the VM version number. +- **`miden-client`** and `miden-client-sqlite-store` go `0.15` → `0.16.0`. +- The **Web SDK** packages go `0.15` → `0.16.0`. + +Two crates changed identity: `miden-tx-batch-prover` is now **`miden-tx-batch`**, and a new **`miden-protocol-build-utils`** crate provides MASM assembly helpers. On the VM side the core package was split, adding a **`miden-precompiles`** package alongside `miden-core`. + +--- + +## Version Bumps + +| Crate | v0.15 | v0.16 | +|-------|-------|-------| +| `miden-client` | 0.15 | 0.16.0 | +| `miden-client-sqlite-store` | 0.15 | 0.16.0 | +| `miden-protocol` | 0.15.3 | 0.16.0 | +| `miden-standards` | 0.15.3 | 0.16.0 | +| `miden-tx` | 0.15.3 | 0.16.0 | +| `miden-testing` | 0.15.3 | 0.16.0 | +| `miden-tx-batch-prover` | 0.15.3 | **renamed** to `miden-tx-batch` 0.16.0 | +| `miden-protocol-build-utils` | — | 0.16.0 *(new)* | +| `miden-assembly` | 0.23 | 0.29.1 | +| `miden-core` | 0.23 | 0.29.1 | +| `miden-core-lib` | 0.23 | 0.29.1 | +| `miden-processor` | 0.23 | 0.29.1 | +| `miden-prover` | 0.23 | 0.29.1 | +| `miden-verifier` | 0.23 | 0.29.1 | +| `miden-mast-package` | 0.23 | 0.29.1 | +| `miden-precompiles` | — | 0.29.1 *(new)* | +| `miden-crypto` | 0.25 | 0.29.1 | + +| npm package | v0.15 | v0.16 | +|-------------|-------|-------| +| `@miden-sdk/miden-sdk` | 0.15.x | 0.16.0 | +| `@miden-sdk/react` | 0.15.x | 0.16.0 | +| `@miden-sdk/vite-plugin` | — | 0.16.0 | + +:::note `miden-idxdb-store` is not a package +Earlier guidance listed a `miden-idxdb-store` npm dependency. No such package exists on the public registry — the IndexedDB store ships inside `@miden-sdk/miden-sdk`. Remove it from your `package.json` if you carried it over. +::: + +--- + +## Affected Code + +**Cargo.toml:** +```diff +- miden-client = "0.15" +- miden-client-sqlite-store = "0.15" +- miden-protocol = "0.15.3" +- miden-standards = "0.15.3" +- miden-tx = "0.15.3" +- miden-tx-batch-prover = "0.15.3" +- miden-assembly = "0.23" +- miden-core = "0.23" +- miden-core-lib = "0.23" +- miden-processor = "0.23" +- miden-prover = "0.23" +- miden-crypto = "0.25" ++ miden-client = "0.16.0-rc.1" ++ miden-client-sqlite-store = "0.16.0-rc.1" ++ miden-protocol = "0.16.0-rc.6" ++ miden-standards = "0.16.0-rc.6" ++ miden-tx = "0.16.0-rc.6" ++ miden-tx-batch = "0.16.0-rc.6" ++ miden-assembly = "0.29.1" ++ miden-core = "0.29.1" ++ miden-core-lib = "0.29.1" ++ miden-processor = "0.29.1" ++ miden-prover = "0.29.1" ++ miden-crypto = "0.29.1" +``` + +**package.json (Web SDK):** +```diff +- "@miden-sdk/miden-sdk": "^0.15.0", +- "@miden-sdk/react": "^0.15.0", +- "miden-idxdb-store": "^0.15.0" ++ "@miden-sdk/miden-sdk": "0.16.0-rc.2", ++ "@miden-sdk/react": "0.16.0-rc.2" +``` + +--- + +## MSRV (Minimum Supported Rust Version) + +The MSRV rose across the board. Update your `rust-toolchain.toml` to Rust **1.96**: + +```toml title="rust-toolchain.toml" +[toolchain] +channel = "1.96" +``` + +| Component | v0.15 | v0.16 | +|-----------|-------|-------| +| protocol crates | 1.90 | 1.96.1 | +| `miden-client` | 1.93 | 1.96 | +| Miden VM | 1.90 | 1.96 | + +--- + +## Migration Steps + +1. Bump every Miden crate per the table above, pinning the exact `0.16.0-rc.N` strings for the protocol and client crates, and run `cargo update`. +2. Rename the `miden-tx-batch-prover` dependency to **`miden-tx-batch`** if you used it. +3. Set your toolchain to at least Rust `1.96`. +4. Bump `@miden-sdk/miden-sdk` and `@miden-sdk/react` together — mixing 0.15 and 0.16 packages will not link against the shared WASM ABI. Drop any `miden-idxdb-store` dependency. +5. Re-assemble every `.masp` package from source under the new toolchain, and delete cached `MastForest` blobs. The `.masl` format is gone entirely. +6. **Recreate your local store.** The SQLite store's schema fingerprint changed and existing databases are rejected; browser users have their IndexedDB store cleared automatically on the version bump. See [Client Changes](./client-changes). +7. **Upgrade your node together with your client.** 0.16 clients seal transaction inputs before submission; a 0.16 node rejects plaintext submissions and an older node rejects sealed ones, so the two cannot be mixed. + +--- + +## Common Errors + +| Error Message | Cause | Solution | +| --- | --- | --- | +| `failed to select a version for miden-protocol` | A plain `"0.16"` requirement will not match a `0.16.0-rc.N` pre-release | Pin the exact version string, e.g. `"0.16.0-rc.6"`. | +| `failed to select a version for miden-tx-batch-prover` | Crate renamed in 0.16 | Depend on `miden-tx-batch` instead. | +| `MastForest deserialization failed: unexpected version` | MAST wire format moved to `0.0.4` | Re-assemble every package from source under VM 0.29.1. | +| package fails to load with a version mismatch | Package format moved to `6.0.0` | Rebuild the `.masp`; `.masl` is no longer supported at all. | +| `Migration error: Attempt to migrate a database with a migration number that is too high` | Existing SQLite store predates the 0.16 schema | Delete and recreate the store, then re-sync. | +| Node rejects a submitted transaction | Client and node versions are mixed | Upgrade both to 0.16; sealed and plaintext submissions are mutually incompatible. | +| `rustc` version error during build | MSRV raised to 1.96 | Update `rust-toolchain.toml`. | diff --git a/versioned_docs/version-0.16/builder/migration/02-hashing-crypto.md b/versioned_docs/version-0.16/builder/migration/02-hashing-crypto.md new file mode 100644 index 00000000..7b3c9330 --- /dev/null +++ b/versioned_docs/version-0.16/builder/migration/02-hashing-crypto.md @@ -0,0 +1,135 @@ +--- +sidebar_position: 2 +title: "Hashing & Crypto Changes" +description: "Commitment preimages that changed in v0.16 — ECDSA public keys, MMR peaks, and domain-separated empty inputs" +--- + +# Hashing & Crypto Changes + +:::warning Breaking Change +Four commitment preimages changed. Any value you have **persisted** — in account storage, note storage, an advice map key, or your own database — that was derived from an ECDSA public key, an MMR peak set, a domain-separated empty input, or `hash_bytes(&[])` is now wrong and must be recomputed. These changes are silent: nothing fails to compile, and the old values simply no longer match. +::: + +## Quick Fix + +```rust +// Recompute every stored ECDSA public-key commitment +use miden_crypto::dsa::ecdsa_k256_keccak::PublicKey; +let commitment: Word = public_key.to_commitment(); +``` + +Then re-derive anything downstream: account storage slots, note storage, and advice-map keys built from those commitments. + +If you encounter errors, continue reading for detailed migration steps. + +--- + +## Summary + +Unlike most of this release, nothing here breaks your build. These are value changes, so the symptom is a proof that fails to verify, an account whose storage no longer matches, or an advice-map lookup that misses — all at runtime, all without a compiler error pointing at the cause. + +The rule of thumb: if you stored a hash, recompute it. If you only ever compute hashes on the fly from current inputs, you are unaffected. + +--- + +## ECDSA k256 public-key commitment format changed + +### Summary + +The ECDSA-k256/Keccak public-key commitment now hashes the **native affine coordinate limbs** (`qx || qy` as little-endian `u32` limbs) instead of the compressed SEC1 public-key bytes. Compressed SEC1 *serialization* of the key itself is unchanged — only the commitment value changed ([#3342](https://github.com/0xMiden/miden-vm/pull/3342), [crypto#1075](https://github.com/0xMiden/crypto/issues/1075)). + +### Affected Code + +```text +v0.15: PK_COMM = Poseidon2::hash_elements( 33 compressed SEC1 bytes packed as 9 felts ) +v0.16: PK_COMM = Poseidon2::hash_elements( QX[8] || QY[8] ) # native LE u32 limbs +``` + +```rust +// After (0.16) — regenerate every stored commitment +use miden_crypto::dsa::ecdsa_k256_keccak::PublicKey; +let commitment: Word = public_key.to_commitment(); +``` + +### Migration Steps + +1. Recompute every stored ECDSA public-key commitment with `PublicKey::to_commitment()`. +2. Re-derive anything downstream of that commitment — account storage slots, note storage, advice-map keys. +3. No MASM call-site changes are needed. The operand-stack contract of `ecdsa_k256_keccak::verify` is still `[PK_COMM, MSG_WORD, ...]`; only the value of `PK_COMM` moved. The **advice** layout did change, though — see [MASM Changes](./masm-changes#ecdsa-advice-and-signature-abi-changed). + +--- + +## MMR peak commitments now bind the leaf count + +### Summary + +MMR peak commitments are computed over `[num_leaves, 0, 0, 0] || padded_peaks` instead of `padded_peaks` alone, on both the Rust and MASM sides. **All MMR peak commitments change** ([#3388](https://github.com/0xMiden/miden-vm/pull/3388)). + +### Affected Code + +```text +v0.15: hash_peaks() = Poseidon2::hash_elements( padded_peaks ) +v0.16: hash_peaks() = Poseidon2::hash_elements( [num_leaves, 0, 0, 0] || padded_peaks ) +``` + +The `miden::core::collections::mmr` `pack` and `unpack` procedures were updated to the same preimage. In 0.15, `pack` hashed the range starting at `mmr_ptr + 4`, skipping the leaf-count word; in 0.16 it hashes from `mmr_ptr`, so the leaf count is absorbed first. The MASM stack contracts (`[mmr_ptr, ...] -> [HASH, ...]`) are unchanged. + +### Migration Steps + +1. Recompute and re-persist every stored MMR peak commitment. +2. Invalidate any cached chain-MMR commitment, advice-map entry keyed by an MMR commitment, or proof whose witness depends on one. +3. No MASM call-site changes — `mmr::pack` and `mmr::unpack` keep their signatures. + +--- + +## Domain-separated empty-input hashing changed + +### Summary + +`hash_elements_in_domain(&[], d)` for a nonzero domain `d` used to collide with other inputs. The fix marks the empty-input case in the third capacity element and applies a permutation, so the result is now a distinct, nonzero digest ([#3447](https://github.com/0xMiden/miden-vm/pull/3447), refining [#3366](https://github.com/0xMiden/miden-vm/pull/3366)). + +Related and also digest-changing: `hash_bytes(&[])` no longer returns `Word::default()`. The empty-bytes input now absorbs a padding marker and permutes, producing a nonzero digest consistent with the `10*` sponge padding rule ([#3366](https://github.com/0xMiden/miden-vm/pull/3366)). + +### Affected Code + +```rust +// After (0.16) — the empty-input branch, from the algebraic sponge implementation +} else if total_len == 0 && state[CAPACITY_RANGE.start + 1] != ZERO { + // Mark an empty domain-separated input in an otherwise unused capacity element. + state[CAPACITY_RANGE.start + 2] = Felt::ONE; + S::apply_permutation(&mut state); +} +``` + +### Migration Steps + +1. Re-derive any commitment computed as `hash_elements_in_domain` over an empty element list with a nonzero domain — typically "empty collection" sentinel values. +2. Re-derive any value computed as `hash_bytes(&[])`. A stored zero word is no longer the right answer. +3. `merge_in_domain` and non-empty `hash_elements_in_domain` inputs are unaffected. + +--- + +## `AeadPoseidon2` key derivation restored to canonical decoding + +### Summary + +`AeadPoseidon2::key_from_bytes` was restored to canonical-`Felt` decoding ([#3366](https://github.com/0xMiden/miden-vm/pull/3366)). Keys persisted under the brief SHA-256 KDF contract must be re-derived. + +### Migration Steps + +1. If you persisted AEAD keys derived with `key_from_bytes` during the 0.16 pre-release window, re-derive them. +2. Data encrypted under a key derived by the interim contract cannot be decrypted with a canonically-derived key — re-encrypt it. + +--- + +## Common Errors + +These changes do not produce compile errors. Expect runtime symptoms instead: + +| Symptom | Cause | Solution | +| --- | --- | --- | +| Signature verification traps for a key that worked in 0.15 | Stored `PK_COMM` uses the old preimage | Recompute with `PublicKey::to_commitment()`. | +| Advice-map lookup misses for a key you know you inserted | The key is a changed commitment | Re-derive the key. | +| Chain-MMR commitment mismatch after upgrading | Peak commitment now binds the leaf count | Recompute and re-persist. | +| An "empty" sentinel commitment no longer matches | Empty-input hashing changed | Re-derive the sentinel. | +| Previously encrypted data fails to decrypt | AEAD key derivation changed | Re-derive the key and re-encrypt. | diff --git a/versioned_docs/version-0.16/builder/migration/03-account-changes.md b/versioned_docs/version-0.16/builder/migration/03-account-changes.md new file mode 100644 index 00000000..1c46191e --- /dev/null +++ b/versioned_docs/version-0.16/builder/migration/03-account-changes.md @@ -0,0 +1,325 @@ +--- +sidebar_position: 3 +title: "Account Changes" +description: "Auth components become ordinary components, Approver replaces raw key arguments, component names change, and account updates move to AccountPatch" +--- + +# Account Changes + +:::warning Breaking Change +`AccountBuilder::with_auth_component` was removed — auth components are now passed through `with_component` like any other, and identified by their MASM `@auth_script` attribute. Auth components take an `Approver` instead of a raw key and scheme. Every standard component's `NAME` constant changed, and because component metadata feeds the storage schema commitment, **accounts rebuilt from the same seed will have different commitments**. +::: + +## Quick Fix + +```rust +// Before (0.15) +let account = AccountBuilder::new(init_seed) + .account_type(AccountType::Public) + .with_auth_component(AuthSingleSig::new(pub_key, auth_scheme)) + .with_component(BasicWallet) + .build()?; + +// After (0.16) +let account = AccountBuilder::new(init_seed) + .account_type(AccountType::Public) + .with_component(AuthSingleSig::new(Approver::new(pub_key, auth_scheme))) + .with_component(BasicWallet) + .build()?; +``` + +If you encounter errors, continue reading for detailed migration steps. + +--- + +## Summary + +Three independent shifts land on accounts in this release. + +**Auth stops being special.** In 0.15 the builder had a dedicated auth slot; in 0.16 the auth component is just a component, and the builder finds it by looking for the `@auth_script` attribute in its MASM. This is what makes the fee change possible — the auth procedure is now also where fees get paid, so it needed to compose with everything else. + +**Keys are wrapped in an `Approver`.** `AuthSingleSig::new` took a public-key commitment and a scheme; it now takes a single `Approver` carrying both. Multi-signature components take an `ApproverSet` with a threshold. This is a mechanical rewrite, but it touches every account you construct. + +**Component names were normalised**, which changes commitments. Every standard component's `NAME` dropped its `components::` segment. Since the name feeds component metadata, and metadata feeds the storage schema commitment, this silently changes account commitments even when nothing else about your account changed. + +--- + +## `AccountBuilder::with_auth_component` removed + +### Summary + +`AccountBuilder` now takes all components uniformly through `with_component`, and identifies the auth component by its `@auth_script` MASM attribute. + +### Affected Code + +```diff + let account = AccountBuilder::new(init_seed) + .account_type(AccountType::Public) +- .with_auth_component(auth_component) ++ .with_component(auth_component) + .with_component(BasicWallet) + .build()?; +``` + +Reach for `with_component` whenever you are naming a component yourself — it takes `impl Into`, so no explicit `.into()` is needed. `with_components` is for the case where the count is not known at the call site: a configuration value that expands into one or several components depending on its variant. `AuthNetworkAccount` is the example in this release — see [Network accounts require a fee policy](#network-accounts-require-a-fee-policy). + +`AccountBuilder` also gained `with_asset_callbacks(AssetCallbackFlag)`. Whether a faucet's assets trigger callbacks is now encoded in the account ID rather than in separate storage, so this is set at construction time. + +### Migration Steps + +1. Drop `with_auth_component` and pass the auth component through `with_component`, like any other. +2. If you author a custom auth component, make sure its entry procedure carries the `@auth_script` attribute — that is how the builder recognises it. +3. If you build a faucet whose assets should trigger callbacks, set `with_asset_callbacks`. + +### Common Errors + +| Error Message | Cause | Solution | +| --- | --- | --- | +| `no method named with_auth_component` | Method removed | Use `with_component`. | +| Build fails reporting no auth component | Custom component lacks the attribute | Annotate the entry procedure with `@auth_script`. | + +--- + +## `Approver` and `ApproverSet` replace raw key arguments + +### Summary + +`Approver` bundles a public-key commitment with its signature scheme; `ApproverSet` bundles a list of approvers with a threshold. Both are new in 0.16. The `AuthMethod` enum was removed, and `AuthSingleSigAcl` / `AuthSingleSigAclConfig` were removed outright. + +### Affected Code + +```rust +// Before (0.15) +AuthSingleSig::new(pub_key: PublicKeyCommitment, auth_scheme: AuthScheme) -> Self +``` + +```rust +// After (0.16) +Approver::new(pub_key: PublicKeyCommitment, auth_scheme: AuthScheme) -> Approver +AuthSingleSig::new(approver: Approver) -> Self +ApproverSet::new(approvers: Vec, threshold: u32) -> Result +AuthMultisig::new(approver_set: ApproverSet) -> Self +``` + +The convenience constructors are unchanged and remain the shortest path when you have a concrete key: + +```rust +// Identical in 0.15 and 0.16 +AuthSingleSig::falcon512_poseidon2(pub_key) +AuthSingleSig::ecdsa_k256_keccak(pub_key) +AuthSingleSig::from_public_key(pub_key) +``` + +New accessors: `AuthSingleSig::approver()`, `ApproverSet::approvers()`, and `ApproverSet::threshold()`. + +### Migration Steps + +1. Wrap existing `AuthSingleSig::new(pub_key, scheme)` arguments in `Approver::new(pub_key, scheme)`. +2. Replace multi-signature construction with `ApproverSet::new(approvers, threshold)?` — note it is fallible. +3. Remove any use of `AuthMethod`; the scheme now travels inside the `Approver`. +4. If you used `AuthSingleSigAcl`, there is no drop-in replacement. Rebuild the access-control policy using the components under `miden::standards::access` (for example `RoleBasedAccessControl` or `Authority`). + +### Common Errors + +| Error Message | Cause | Solution | +| --- | --- | --- | +| `this function takes 1 argument but 2 were supplied` on `AuthSingleSig::new` | Signature changed | Wrap the arguments in `Approver::new`. | +| `cannot find type AuthMethod` | Removed | Use `Approver` / `AuthScheme`. | +| `cannot find type AuthSingleSigAcl` | Removed | Rebuild with an access-control component. | + +--- + +## Component names changed, and account commitments with them + +### Summary + +Every standard component's `NAME` constant was normalised by dropping the `components::` segment. Component metadata feeds the storage schema commitment, so **the commitment of an account built from the same seed and the same components differs between 0.15 and 0.16**. + +### Affected Code + +```diff +- "miden::standards::components::auth::singlesig" ++ "miden::standards::auth::singlesig" + +- "miden::standards::components::wallets::basic_wallet" ++ "miden::standards::wallets::basic_wallet" + +- "miden::standards::components::access::rbac" ++ "miden::standards::access::rbac" + +- "miden::standards::components::faucets::fungible_faucet" ++ "miden::standards::faucets::fungible" +``` + +A few names already lacked the segment in 0.15 — `miden::standards::auth::network_account` and `miden::standards::access::ownable2step` are unchanged. Note that the fungible faucet also lost its `_faucet` suffix, so it is not a pure prefix change. + +The `miden::standards::account::metadata` module was also renamed to `miden::standards::account::inspection`, both in MASM and in Rust. + +### Migration Steps + +1. Update any hard-coded component name strings. +2. Expect new account IDs and commitments for accounts rebuilt from the same seed. If you have persisted an account ID derived under 0.15, it will not be reproduced by 0.16 construction. +3. Update references to `account::metadata` to `account::inspection`. + +--- + +## Component MASM must annotate exported procedures + +### Summary + +Account component MASM must annotate every exported procedure with `@account_procedure`. Un-annotated procedures are not exported. This attribute is new in 0.16 — the protocol's own MASM went from zero uses to 99. + +`@auth_script` and `@note_script` already existed in 0.15 and are unchanged. + +### Affected Code + +```masm +# After (0.16) +@account_procedure +pub proc receive_asset(asset: word) + # … +end + +@auth_script +pub proc auth_tx(auth_args: word) + # … +end +``` + +### Migration Steps + +1. Add `@account_procedure` to every procedure your component intends to export. +2. Re-check the resulting `AccountCode` procedure list — a missing annotation shows up as a procedure that silently is not callable, not as a compile error. + +--- + +## `AccountCode::from_parts` is now fallible + +### Summary + +`AccountCode::from_parts` validated its procedure count with `assert!` in 0.15 and now returns a `Result` instead of panicking. + +### Affected Code + +```rust +// Before (0.15) +pub fn from_parts(mast: Arc, procedures: Vec) -> Self + +// After (0.16) +pub fn from_parts( + mast: Arc, + procedures: Vec, +) -> Result +``` + +### Migration Steps + +Add `?` or explicit error handling at every call site. + +--- + +## `AccountId` no longer converts into `[Felt; 2]` + +### Summary + +The `impl From for [Felt; 2]` was removed. Use the `prefix()` and `suffix()` accessors instead. Conversions to `[u8; 15]` and `u128` are unchanged. + +### Affected Code + +```rust +// Before (0.15) +let felts: [Felt; 2] = account_id.into(); +``` + +```rust +// After (0.16) +let prefix: AccountIdPrefix = account_id.prefix(); +let suffix: Felt = account_id.suffix(); +``` + +### Migration Steps + +Replace the `into()` conversion with the two accessors. Note `prefix()` returns an `AccountIdPrefix`, not a bare `Felt`. + +--- + +## Account updates move from `AccountDelta` to `AccountPatch` + +### Summary + +Account **updates** moved from the relative `AccountDelta` to the absolute `AccountPatch`. `ExecutedTransaction` and `AccountUpdateDetails` now carry a patch, and `Account::apply_delta` was replaced by applying a patch. + +:::info `AccountDelta` still exists +This is not a wholesale removal. `AccountDelta` remains, and `TransactionSummary::account_delta()` deliberately still returns one — the signed transaction summary binds a *relative* delta. Only account update representation moved to the absolute patch model. The same split exists on the TypeScript side, where `TransactionSummary.accountDelta()` is unchanged while the result's `accountDelta()` became `accountPatch()`. +::: + +### Affected Code + +```rust +// Before (0.15) +let delta = executed_tx.account_delta(); +account.apply_delta(&delta)?; +``` + +```rust +// After (0.16) +let patch = executed_tx.account_patch(); +account.apply_patch(&patch)?; + +// Or build a fresh account from the patch instead of mutating one: +let account = Account::try_from(&patch)?; +``` + +`apply_patch` is the direct replacement for `apply_delta` and keeps the same in-place shape, so it is the smaller edit for existing code. The same rename applies further down: `AssetVault::apply_delta` became `apply_patch`, taking an `AccountVaultPatch`. + +### Migration Steps + +1. Replace `account_delta()` with `account_patch()` on `ExecutedTransaction` and on client transaction results. +2. Replace `Account::apply_delta(&delta)` with `Account::apply_patch(&patch)`, or construct a new account with `Account::try_from(&patch)`. +3. Leave `TransactionSummary::account_delta()` call sites alone — that one is intentionally still a delta. + +--- + +## Network accounts require a fee policy + +### Summary + +`AuthNetworkAccount::new` now takes a `FeePolicyManager` alongside the allowed-note set. The manager carries the fee faucet and the active fee policy, and expands into the policy's components when the auth component is installed — so you do not install policy components separately. + +:::note This applies to network accounts and faucets, not ordinary accounts +Fee *policies* describe how an account that sponsors or charges fees estimates them. An ordinary user account paying a fee does not install one; it supplies fee conversion info per transaction instead. See [Transaction Changes](./transaction-changes#transaction-fees-are-paid-by-the-auth-procedure). +::: + +### Affected Code + +```rust +// After (0.16) +let manager = FeePolicyManager::builder() + .fee_faucet_id(fee_faucet_id) + .active_fee_policy(FeePolicy::from(BasicConstantFeePolicy::new())) + .build(); + +let auth = AuthNetworkAccount::new(allowed_notes, manager)?; +``` + +`AuthNetworkAccount` no longer converts into a single `AccountComponent` — it expands into several, so install it through `with_components`. + +### Migration Steps + +1. Build a `FeePolicyManager` with the fee faucet and an active fee policy, registering any alternatives with `allowed_fee_policy` for runtime switching. +2. Pass it to `AuthNetworkAccount::new`, which is fallible. +3. Install the auth component with `with_components`, not `with_component`, since it expands to several components. + +--- + +## Common Errors + +| Error Message | Cause | Solution | +| --- | --- | --- | +| `no method named with_auth_component` | Removed | Use `with_component`, or `with_components` for `AuthNetworkAccount`. | +| `this function takes 1 argument but 2 were supplied` | `AuthSingleSig::new` takes an `Approver` | Wrap in `Approver::new`. | +| `cannot find type AuthMethod` / `AuthSingleSigAcl` | Removed | See the auth section above. | +| `expected Result, found AccountCode` | `from_parts` is fallible | Add `?`. | +| `the trait From is not implemented for [Felt; 2]` | Conversion removed | Use `prefix()` / `suffix()`. | +| Account commitment differs from 0.15 for the same seed | Component names changed | Expected; re-record the new ID. | +| Component procedure is not callable but compiles | Missing `@account_procedure` | Annotate the procedure. | diff --git a/versioned_docs/version-0.16/builder/migration/04-note-changes.md b/versioned_docs/version-0.16/builder/migration/04-note-changes.md new file mode 100644 index 00000000..eaf5a2ff --- /dev/null +++ b/versioned_docs/version-0.16/builder/migration/04-note-changes.md @@ -0,0 +1,171 @@ +--- +sidebar_position: 4 +title: "Note Changes" +description: "Standard notes gain typed builders, the per-note asset limit drops from 64 to 16, and mint/burn scripts are unified" +--- + +# Note Changes + +:::warning Breaking Change +Every standard note changed shape. `P2idNote`, `P2ideNote`, `SwapNote`, `MintNote`, and `BurnNote` were marker types with a `create(..)` associated function returning a `Note`; they are now real structs built with a typed builder and converted with `.into()`. Separately, **`MAX_ASSETS_PER_NOTE` dropped from 64 to 16**, so any note packing more than 16 assets now fails to build. +::: + +## Quick Fix + +```rust +// Before (0.15) +let note = P2idNote::create( + sender, target, vec![asset], NoteType::Public, attachments, &mut rng, +)?; + +// After (0.16) +use miden_standards::note::P2idNote; + +let note: Note = P2idNote::builder() + .sender(sender) + .target(target) + .assets(vec![asset]) + .note_type(NoteType::Public) + .generate_serial_number(&mut rng) + .build()? + .into(); +``` + +If you encounter errors, continue reading for detailed migration steps. + +--- + +## Summary + +In 0.15 each standard note was a unit struct — `pub struct P2idNote;` — with a `create` associated function that took every parameter positionally and returned a finished `Note`. In 0.16 each is a real struct holding its fields, built through a `bon` builder and converted to a `Note` with `Into`. + +The cost is that every call site changes. The benefit is worth more than a mechanical rewrite, so it is worth pausing on before you reach for search-and-replace: each standard note is now a distinct type, which means your own functions can take a `P2idNote` or a `SwapNote` instead of a bare `Note`. What used to be a runtime check — is this really a P2ID note? — becomes a signature the compiler enforces, and the conversion to `Note` happens once, at the boundary where you actually need one. Optional parameters also stop being positional, and the typed value is inspectable before you convert it. + +Watch the asset limit separately. Nothing about it is visible at compile time, so your build stays green and only notes carrying more than 16 assets fail, at the point they are built. + +--- + +## Standard notes are built with typed builders + +### Summary + +Each standard note struct now exposes `builder()` and converts into `Note` via `From` / `Into`. + +### Affected Code + +```rust +// Before (0.15) +pub fn create( + sender: AccountId, + target: AccountId, + assets: Vec, + note_type: NoteType, + attachments: NoteAttachments, + rng: &mut R, +) -> Result +``` + +```rust +// After (0.16) +let p2id = P2idNote::builder() + .sender(sender) + .target(target) + .assets(vec![asset]) // or .asset(x), repeatable + .note_type(NoteType::Public) + .generate_serial_number(&mut rng) // or .serial_number(word) + .build()?; +let note: Note = p2id.into(); +``` + +Note two naming details that are easy to get wrong: the setter is `serial_number`, not `serial_num`, and `generate_serial_number(&mut rng)` is the direct replacement for the old `rng` parameter. + +`P2ideNote` takes its optional parameters as optional setters rather than positionally: + +```rust +let p2ide = P2ideNote::builder() + .sender(sender) + .target(target) + .assets(vec![asset]) + .note_type(NoteType::Private) + .serial_number(serial_number) + .reclaimer(reclaimer_account_id) // optional; defaults to the sender + .reclaim_height(BlockNumber::from(n)) // optional + .timelock_height(BlockNumber::from(m)) // optional + .build()?; +``` + +`SwapNote` follows the same pattern. In 0.15 `SwapNote::create` returned a `(Note, NoteDetails)` tuple carrying the payback details; in 0.16 you build the `SwapNote` and read its parts from the typed value. + +The same builder treatment applies to `MintNote`, `BurnNote`, `PswapNote`, and `TxFeeNote`, along with the configuration notes (`AllowlistConfigNote`, `OwnerConfigNote`, `FaucetMetadataConfigNote`, `NetworkAccountConfigNote`, `FaucetPolicyConfigNote`, `MinBurnAmountConfigNote`). + +### Migration Steps + +1. Replace every `XNote::create(..)` call with the corresponding `XNote::builder()` chain ending in `.build()?`, then `.into()` wherever a `Note` is required. +2. Replace the trailing `rng` argument with `.generate_serial_number(&mut rng)`. +3. Use `.assets(..)` for a collection or `.asset(..)` repeatedly for individual assets. +4. For `P2ideNote`, set only the optional parameters you actually need. `reclaimer` still defaults to the sender, so existing "sender can reclaim" behaviour is preserved without changes. +5. Push the `.into()` outward while you are here. Any function of yours that only ever handles one kind of note can take the typed note instead of a `Note`, which turns a runtime check into a compile-time guarantee; convert once, where a `Note` is genuinely needed. + +### Common Errors + +| Error Message | Cause | Solution | +| --- | --- | --- | +| `no function or associated item named create` | Replaced by the builder | Use `XNote::builder()`. | +| `no method named serial_num` | Setter renamed | Use `serial_number` or `generate_serial_number`. | +| `expected Note, found P2idNote` | The builder yields the typed note | Add `.into()`. | +| `a P2ID note must contain at least one asset` | Built with no assets | Add at least one asset. | + +--- + +## `MAX_ASSETS_PER_NOTE` dropped from 64 to 16 + +### Summary + +The protocol limit on assets carried by a single note fell from 64 to 16. + +### Affected Code + +```diff +- pub const MAX_ASSETS_PER_NOTE: usize = 64; ++ pub const MAX_ASSETS_PER_NOTE: usize = 16; +``` + +This is enforced by `NoteAssets::new`, so it surfaces as a `NoteError` at build time rather than a compile error. + +### Migration Steps + +1. Audit any code path that batches assets into a single note and cap it at 16. +2. If you previously relied on packing up to 64 assets, split the payload across multiple notes. +3. If you compute a batch size from the constant rather than hard-coding it, no change is needed beyond a rebuild. + +--- + +## `MINT` and `BURN` are unified across faucet kinds + +### Summary + +One `mint.masm` and one `burn.masm` script now serve both fungible and non-fungible faucets, with the variant carried in the note storage. **The script roots changed**, so any hard-coded or cached root is now wrong. + +### Migration Steps + +1. Recompute and re-store any cached standard note script roots. +2. Remove per-faucet-kind branching that selected between separate mint or burn scripts. + +--- + +## Other note changes + +- **`PswapNote` (partial swap)** gained a minimum-fill parameter, and its fields were renamed. +- **`NoteFile` was reworked and moved to `miden-standards`**, with variants keyed on `NoteId`, `ExpectedNote`, and `Committed`. This mostly affects client code — see [Client Changes](./client-changes). +- **`NoteTag`** moved under `miden::standards::note::note_tag` in MASM. In the released `0.16.0-rc` line it is still reachable at `miden::standards::note_tag`. + +--- + +## Common Errors + +| Error Message | Cause | Solution | +| --- | --- | --- | +| `no function or associated item named create` | Notes use builders now | Rewrite with `XNote::builder()`. | +| `NoteError` about exceeding asset limits | Limit is now 16 | Split across multiple notes. | +| Note script root mismatch for mint or burn | Scripts were unified | Recompute the roots. | +| `expected Note, found MintNote` | Builder returns the typed note | Add `.into()`. | diff --git a/versioned_docs/version-0.16/builder/migration/05-asset-vault-faucet.md b/versioned_docs/version-0.16/builder/migration/05-asset-vault-faucet.md new file mode 100644 index 00000000..9bf7a24b --- /dev/null +++ b/versioned_docs/version-0.16/builder/migration/05-asset-vault-faucet.md @@ -0,0 +1,158 @@ +--- +sidebar_position: 5 +title: "Assets, Vault & Faucet Changes" +description: "AssetVaultKey becomes AssetId, the old AssetId becomes AssetClass, and faucet factories split by authentication scheme" +--- + +# Assets, Vault & Faucet Changes + +:::warning Breaking Change +The asset model was renamed one level down. What was `AssetVaultKey` is now **`AssetId`**, and what was `AssetId` is now **`AssetClass`**. Because the name `AssetId` survives with a different meaning, a careless search-and-replace will compile and be wrong — do the `AssetId` → `AssetClass` rename first. Separately, the single `create_fungible_faucet` factory split into six auth-specific factories. +::: + +## Quick Fix + +```rust +// Before (0.15) +let key: AssetVaultKey = asset.vault_key(); +let key = AssetVaultKey::new_fungible(faucet_id, callback_flag); + +// After (0.16) +let id: AssetId = asset.id(); +let id = AssetId::new_fungible(faucet_id); // callback flag now lives on the AccountId +``` + +If you encounter errors, continue reading for detailed migration steps. + +--- + +## Summary + +The rename reflects a conceptual correction. The per-asset vault key is the thing that actually identifies an asset, so it took the name `AssetId`; the faucet-level identifier it used to share a name with describes a *class* of assets, so it became `AssetClass`. + +This is the most dangerous rename in the release precisely because it is not a removal. `AssetId` still exists after the upgrade, so code referring to it keeps compiling while silently meaning something different. Rename in the right order and let the compiler find the rest. + +:::note Several related types did not change +`AssetAmount`, `AssetComposition`, and `AssetCallbackFlag` all existed in 0.15 and are unchanged. `AssetVault::get_balance` already returned `AssetAmount` in 0.15 — only its parameter type was renamed. If you saw `AssetAmount` described as new, that applies to the [client surface](./client-changes), not the protocol. +::: + +--- + +## `AssetVaultKey` → `AssetId`, and `AssetId` → `AssetClass` + +### Summary + +The vault key type was renamed to `AssetId`, the previous `AssetId` became `AssetClass`, and `Asset::vault_key()` became `Asset::id()`. `AssetIdHash` is the corresponding hash type. + +### Affected Code + +```rust +// Before (0.15) +let key: AssetVaultKey = asset.vault_key(); +let balance: AssetAmount = vault.get_balance(vault_key)?; +let key = AssetVaultKey::new_fungible(faucet_id, callback_flag); +``` + +```rust +// After (0.16) +let id: AssetId = asset.id(); +let balance: AssetAmount = vault.get_balance(asset_id)?; +let id = AssetId::new_fungible(faucet_id); +// or, fully explicit: +let id = AssetId::new(asset_class, faucet_id, composition); +``` + +Note that `AssetId::new_fungible` **no longer takes a callback flag**. Whether a faucet's assets trigger callbacks is encoded in the account ID itself, set at construction time via `AccountBuilder::with_asset_callbacks`. + +`Asset` itself is unchanged in shape — still an enum with `Fungible` and `NonFungible` variants — and `FungibleAsset::new(faucet_id, amount)` keeps its signature. + +### Migration Steps + +1. Rename `AssetId` → `AssetClass` **first**, throughout your codebase. +2. Then rename `AssetVaultKey` → `AssetId`. +3. Replace `asset.vault_key()` with `asset.id()`. +4. Drop the callback-flag argument from `new_fungible` calls; set it on the account instead with `with_asset_callbacks`. +5. Re-index any persisted vault or asset data. Serialized asset identifiers are not compatible across the rename. + +### Common Errors + +| Error Message | Cause | Solution | +| --- | --- | --- | +| `cannot find type AssetVaultKey` | Renamed | Use `AssetId`. | +| `no method named vault_key` | Renamed | Use `id()`. | +| `this function takes 1 argument but 2 were supplied` on `new_fungible` | Callback flag removed | Drop it; set `with_asset_callbacks` on the account. | +| Type mismatch where `AssetId` used to work | `AssetId` now means the vault key | The old meaning is `AssetClass`. | + +--- + +## Faucet factories split by authentication scheme + +### Summary + +`create_fungible_faucet` took an `AuthMethod` and an `AccessControl` argument and dispatched internally. Since `AuthMethod` was removed (see [Account Changes](./account-changes#approver-and-approverset-replace-raw-key-arguments)), the factory split into one function per authentication scheme, each taking a concrete auth component. + +### Affected Code + +```rust +// Before (0.15) +pub fn create_fungible_faucet( + init_seed: [u8; 32], + faucet: FungibleFaucet, + account_type: AccountType, + auth_method: AuthMethod, + access_control: AccessControl, + token_policy_manager: TokenPolicyManager, +) -> Result +``` + +```rust +// After (0.16) +pub fn create_singlesig_user_fungible_faucet( + init_seed: [u8; 32], + faucet: FungibleFaucet, + auth_component: AuthSingleSig, + token_policy_manager: TokenPolicyManager, + account_type: AccountType, +) -> Result +``` + +Note that the parameter **order** changed as well as the parameter list — `account_type` moved to the end. + +The full set of factories: + +| Faucet kind | v0.16 factory | +| --- | --- | +| Fungible, single signature | `create_singlesig_user_fungible_faucet` | +| Fungible, multisig | `create_multisig_user_fungible_faucet` | +| Fungible, guarded multisig | `create_guarded_user_fungible_faucet` | +| Fungible, network account | `create_network_fungible_faucet` | +| Non-fungible, user account | `create_user_non_fungible_faucet` | +| Non-fungible, network account | `create_network_non_fungible_faucet` | + +Non-fungible faucet factories are new in this release; 0.15 shipped only the fungible factory. + +### Migration Steps + +1. Choose the factory matching your authentication scheme and pass a concrete auth component instead of an `AuthMethod`. +2. Drop the `access_control` argument. The factories install `Authority::AuthControlled` and the pausable components for you. +3. Check the argument order — `account_type` is now last. +4. Expect a different account ID for a faucet rebuilt from the same seed, since the component set and names changed. + +### Common Errors + +| Error Message | Cause | Solution | +| --- | --- | --- | +| `cannot find function create_fungible_faucet` | Split into per-scheme factories | Use the matching factory from the table. | +| `cannot find type AuthMethod` | Removed | Pass a concrete auth component. | +| Arguments of the wrong type | Parameter order changed | `account_type` moved to the end. | + +--- + +## Common Errors + +| Error Message | Cause | Solution | +| --- | --- | --- | +| `cannot find type AssetVaultKey` | Renamed to `AssetId` | Rename, after renaming old `AssetId` to `AssetClass`. | +| Silent behaviour change around asset identity | `AssetId` kept its name with a new meaning | Audit every `AssetId` reference. | +| Persisted vault lookups miss after upgrading | Asset identifier serialization changed | Re-index persisted vault data. | +| `cannot find function create_fungible_faucet` | Factories split | Use the auth-specific factory. | diff --git a/versioned_docs/version-0.16/builder/migration/06-transaction-changes.md b/versioned_docs/version-0.16/builder/migration/06-transaction-changes.md new file mode 100644 index 00000000..40c32a1b --- /dev/null +++ b/versioned_docs/version-0.16/builder/migration/06-transaction-changes.md @@ -0,0 +1,314 @@ +--- +sidebar_position: 6 +title: "Transaction Changes" +description: "Fees move into the auth procedure, transaction inputs are sealed before submission, and TransactionSummary binds the reference block — so multi-party signing needs a ChainAnchor" +--- + +# Transaction Changes + +:::warning Breaking Change +Transaction fees moved out of the kernel epilogue and into the **authentication procedure**. On a chain with a non-zero `verification_base_fee`, transactions signed by `AuthSingleSig` or `AuthMultisig` must commit fee conversion info through the transaction's auth args, and the paying account must hold the fee asset. Separately, transaction inputs are now **sealed (encrypted)** before submission, so a 0.16 client cannot submit to a 0.15 node or vice versa. +::: + +## Quick Fix + +```rust +// After (0.16) — declare how the fee is paid +use miden_client::account::component::FeeConversionInfo; + +let info = FeeConversionInfo::one_to_one(fee_faucet_id); +let request = TransactionRequestBuilder::new() + .fee_conversion_info(info, salt) // salt: Word + .build()?; +``` + +If you encounter errors, continue reading for detailed migration steps. + +--- + +## Summary + +The fee change is the largest behavioural change in the release, and it is easy to under-estimate because it does not necessarily break your build. If you target a chain that charges no fee, nothing changes. If you target a chain that does, transactions that used to succeed now fail unless the request declares fee conversion info. + +The reason for the move is visible in the standard auth component's own documentation: paying the fee *before* the transaction summary is created means the fee note and the vault withdrawal funding it are covered by the signature. Under the old model the kernel deducted the fee outside anything the user signed. + +The sealing change is a hard compatibility boundary rather than an API change — it mostly costs you a coordinated upgrade rather than a code edit. + +The summary change is the quiet one. Its constructor break is mechanical, but the binding it introduces silently invalidates any flow that collects signatures on one client and executes on another; that code still compiles and only fails once it runs. + +--- + +## Transaction fees are paid by the auth procedure + +### Summary + +In 0.15 the kernel computed and burned the fee from the native account's vault automatically. In 0.16 the auth procedure reads a `FeeConversionInfo` blob out of the auth args, computes the fee, and emits a `TX_FEE` note to the fee faucet. + +The standard auth components already do this for you — `AuthSingleSig`'s MASM calls `fee::load_conversion_info` and `fee::pay_fee` before authenticating. What you must supply is the auth args. + +### Affected Code + +```rust +// Before (0.15) +// Nothing to declare: the kernel handled the fee. +let request = TransactionRequestBuilder::new().build()?; +let fee = executed_tx.fee(); +``` + +```rust +// After (0.16) +use miden_client::account::component::FeeConversionInfo; + +// Pay in the chain's native fee asset at rate 1/1: +let info = FeeConversionInfo::one_to_one(fee_faucet_id); +// or specify an explicit conversion rate: +let info = FeeConversionInfo::new(fee_faucet_id, rate_num, rate_den)?; + +let request = TransactionRequestBuilder::new() + .fee_conversion_info(info, salt) + .build()?; +``` + +The exact signatures: + +```rust +FeeConversionInfo::new(faucet_id: AccountId, rate_num: u64, rate_den: u64) -> Result +FeeConversionInfo::one_to_one(faucet_id: AccountId) -> Self +commit_fee_conversion_info(conversion_info: FeeConversionInfo, salt: Word) -> (Word, Vec) + +// on the client builder: +pub fn fee_conversion_info(self, conversion_info: FeeConversionInfo, salt: Word) -> Self +``` + +:::caution The `salt` argument is mandatory and undocumented upstream +`fee_conversion_info` takes a second `salt: Word` parameter that the changelog does not mention. Code written from the changelog alone will not compile. +::: + +`FeeConversionInfo` is reachable at `miden_client::account::component::FeeConversionInfo` — it is *not* exported from `miden_client::auth`, where you would naturally look first. Adding a direct `miden-standards` dependency also works. + +### When it is required, and when it is rejected + +Only auth components that actually read the auth args honour the conversion info. The client validates this **before execution** rather than silently paying in the native asset: + +```rust +// miden_client::transaction::TransactionRequestError +FeeConversionInfoUnsupported(String) +// "the request declares fee conversion info but the account's auth component {0} does not read it" +``` + +The check passes only for `AuthSingleSig` and `AuthMultisig`. Declaring fee conversion info for any other auth component — `NoAuth`, for example — is rejected. If the request does not declare fee conversion info at all, the check is skipped, so existing code that never calls the builder method is unaffected by this validation. + +Because `fee_conversion_info` consumes the auth arg, it **conflicts with a manually set `auth_arg`** — whichever is applied last wins. + +### Migration Steps + +1. On a chain with a non-zero `verification_base_fee`, call `fee_conversion_info(info, salt)` on every request signed by an `AuthSingleSig` or `AuthMultisig` account. Use `FeeConversionInfo::one_to_one(fee_faucet_id)` for the native fee asset. +2. Ensure the paying account holds a balance of the fee asset — the auth procedure debits it. +3. Do **not** call `fee_conversion_info` for accounts using any other auth component. +4. If you set `auth_arg` manually, pick one or the other. +5. Replace `executed_tx.fee()` with inspection of the `TX_FEE` output note. `ExecutedTransaction::compute_fee()` still exists but only under the `testing` feature — do not use it in production. +6. If you wrote a **custom auth component** in MASM, it must now call `miden::standards::fee::load_conversion_info` followed by `miden::standards::fee::pay_fee`, or the transaction will fail fee validation. +7. On a zero-fee chain, no change is required. + +### Common Errors + +| Error Message | Cause | Solution | +| --- | --- | --- | +| `this function takes 2 arguments but 1 was supplied` | The `salt` parameter | Pass a `Word` salt. | +| `FeeConversionInfoUnsupported` | Auth component does not read auth args | Only declare it for `AuthSingleSig` / `AuthMultisig`. | +| `cannot find FeeConversionInfo in miden_client::auth` | Exported elsewhere | Use `miden_client::account::component::FeeConversionInfo`. | +| Transaction aborts on a fee-charging chain | No fee conversion info declared, or no fee asset balance | Declare the info and fund the account. | +| Custom auth component transaction fails fee validation | MASM does not pay the fee | Call `fee::load_conversion_info` then `fee::pay_fee`. | + +--- + +## Transaction inputs are sealed before submission + +### Summary + +Transaction inputs are encrypted ("sealed") before being submitted. The RPC layer gained a `get_transaction_encryption_key` method plus a `miden_client::rpc::encryption` module. + +**This is a hard compatibility boundary.** A 0.16 node rejects plaintext submissions and an older node rejects sealed ones, so the client and node must be upgraded together. + +:::note Most applications do not change any code here +`Client::submit_proven_transaction` keeps its 0.15 signature exactly — it still takes `impl Into`, and sealing happens beneath it. Only the `NodeRpcClient` **trait** methods changed to take `SealedTransactionInputs`, so this is a source-breaking change solely for code that implements that trait. +::: + +The requirement this does impose on every application is a **sync before submitting**: sealing resolves an encryption key against the chain state, so a client that has not synced the genesis and chain-tip headers fails with `ClientError::ChainValidationError`. + +### Migration Steps + +1. Upgrade your node and client together. There is no configuration that makes a 0.16 client talk to a 0.15 node. +2. Ensure the client has synced before submitting, or key resolution fails with `ChainValidationError`. +3. If you implement `NodeRpcClient` yourself, update `submit_proven_transaction` and `submit_proven_batch` to take `SealedTransactionInputs`, and add `get_transaction_encryption_key`. + +:::note Key types are not re-exported from the encryption module +The changelog states that `miden_client::rpc::encryption` re-exports the validator DSA key types. It does not — that module contains no `pub use` statements. Reach them via `miden_client::crypto::{ecdsa_k256_keccak, eddsa_25519_sha512}`. +::: + +--- + +## `TransactionSummary` binds the reference block, expiration, and user params + +### Summary + +`TransactionSummary::new` replaced its single `salt` parameter with a block commitment, an expiration delta, and a structured user-parameters value. + +### Affected Code + +```rust +// Before (0.15) +pub fn new( + account_delta: AccountDelta, + input_notes: InputNotes, + output_notes: RawOutputNotes, + salt: Word, +) -> Self +``` + +```rust +// After (0.16) +pub fn new( + account_delta: AccountDelta, + input_notes: InputNotes, + output_notes: RawOutputNotes, + block_commitment: Word, + expiration_delta: u16, + user_params: TransactionSummaryUserParams, +) -> Self +``` + +`TransactionSummaryUserParams` carries seven field elements. On the TypeScript side the corresponding accessor renamed from `TransactionSummary.salt()` to `TransactionSummary.userParams()`. + +The reference block commitment is included because it determines the fee parameters, and therefore the fee amount deducted; the expiration delta is included so the signature covers it. + +:::info `TransactionSummary` still uses `AccountDelta` +Note the first parameter. While account *updates* moved to the absolute `AccountPatch` model (see [Account Changes](./account-changes#account-updates-move-from-accountdelta-to-accountpatch)), the signed transaction summary deliberately still binds a relative `AccountDelta`. Do not rewrite these call sites. +::: + +### Migration Steps + +1. Replace the `salt` argument with the block commitment, expiration delta, and user params. +2. In TypeScript, replace `summary.salt()` with `summary.userParams()`. + +--- + +## Collecting signatures across clients requires a `ChainAnchor` + +### Summary + +The binding above has a consequence that never surfaces as a compile error. Because the summary commits to the reference block, a summary derived at one block only authorizes an execution at *that* block. Clients execute at their own sync height by default, so in any flow that derives a summary, collects signatures, and executes later — a multisig proposal, offline co-signing — the proposer, each co-signer, and the executor sit at different heights and derive three different summaries. The collected signatures do not apply. + +Code written for 0.15 compiles unchanged and fails at run time, which makes this the easiest change in the release to miss. + +`ChainAnchor` is the remedy. It pins execution to a chosen reference block, so the same summary reproduces on a client at any height: the proposer captures one, ships it alongside the summary, and every party executes against it. + +:::note Newer than the versions pinned in Quick Upgrade +`ChainAnchor` landed after the versions in [Quick Upgrade](./#quick-upgrade). It needs `miden-client` `0.16.0-rc.2` and `@miden-sdk/miden-sdk` / `@miden-sdk/react` `0.16.0-rc.3` or later. +::: + +### Affected Code + +```rust +// Before (0.15) +// The proposer and the executor each ran this at their own sync height, +// and the summary the co-signers signed still reproduced. +let result = client.execute_transaction(account_id, request).await?; +``` + +```rust +// After (0.16) +use miden_client::transaction::ChainAnchor; +use miden_client::{Deserializable, Serializable}; + +// Proposer: pin the reference block, and ship the anchor with the summary. +let anchor = client.chain_anchor_for_request(&request).await?; +let bytes = anchor.to_bytes(); + +// Co-signer and executor: rebuild it and execute at the same block. +let anchor = ChainAnchor::read_from_bytes(&bytes)?; +let result = client.execute_transaction_at(account_id, request, anchor).await?; +``` + +```typescript +// After (0.16) — Web SDK +const anchor = await client.transactions.captureAnchor(request); + +const summary = await client.transactions.preview({ + operation: "custom", account, request, anchor, +}); +// ... collect signatures over `summary`, shipping `anchor.serialize()` ... + +await client.transactions.submit(account, request, { anchor }); +``` + +```tsx +// After (0.16) — React +const { captureAnchor, anchoredRequest } = useChainAnchor(); +const { preview } = usePreview(); +const { execute } = useTransaction(); + +const anchor = await captureAnchor({ request: buildRequest }); +const summary = await preview({ accountId, request: anchoredRequest, anchor }); +// ... collect signatures ... +await execute({ accountId, request: anchoredRequest, anchor }); +``` + +Single-client flows need no anchor. `execute_transaction` and an `anchor`-less `submit` keep their 0.15 behaviour of executing at the current sync height, which is correct whenever the client deriving the summary is also the one executing. + +### Verify an anchor you did not capture + +An anchor arrives from whoever proposed the transaction. Deserialization rejects malformed bytes, so the remaining risk is a well-formed anchor pinned to the wrong block. Compare its commitment against the one signed into the summary before executing: + +```typescript +if (anchor.commitment().toHex() !== summary.blockCommitment().toHex()) { + throw new Error("anchor does not match the signed summary"); +} +``` + +The Rust accessor is `ChainAnchor::block_commitment`. Note what this check does and does not prove: it establishes that the anchor and the summary agree, not that either is what you meant to sign, since the proposer supplies both. Inspect the summary's effects separately. + +### Migration Steps + +1. Find every flow that derives a transaction summary on one client and executes on another — multisig proposals and offline co-signing are the common cases. Single-client flows are unaffected. +2. Capture an anchor at proposal time with `chain_anchor_for_request` (Rust), `transactions.captureAnchor` (Web), or `useChainAnchor` (React), and ship the serialized bytes alongside the summary. +3. Re-derive the summary at the anchor when verifying a proposal, rather than at the local sync height: `executeForSummaryAt` on the WASM client, or `preview({ …, anchor })` at the ergonomic layers. Deriving it locally produces a different summary and the comparison always fails. +4. Execute at the anchor: `execute_transaction_at`, or the `anchor` option on `executeRequest` / `submit` / `useTransaction().execute`. +5. Check a received anchor against `summary.blockCommitment()` before using it. +6. In React, preview and execute against the `anchoredRequest` the hook returns, not the value you passed in. Re-resolving a request factory builds a different transaction — anything creating an output note draws a fresh serial number — so the anchor would pin a request nobody executes. +7. Budget for expiry. The expiration delta counts from the anchored block, not from the executing client's height, so an anchor that sat too long during signature collection fails with `AnchoredTransactionExpired`. + +:::caution Rust consumes the anchor; JavaScript does not +`execute_transaction_at` takes the `ChainAnchor` by value, so capture it again — or clone it — for a preview-then-execute sequence. The wasm bindings take it by reference, so one JS handle survives both calls. Because a JS anchor carries a partial blockchain, call `anchor.free()` when done rather than leaving it to the finalizer. +::: + +### Common Errors + +| Error Message | Cause | Solution | +| --- | --- | --- | +| Collected signatures are rejected, with no compile error | Each party derived the summary at its own sync height | Capture an anchor and derive and execute at it. | +| `TRANSACTION_ALREADY_AUTHORIZED` | The transaction needs no further signatures | Submit it with `execute` / `submit` instead of previewing. | +| `AnchoredTransactionExpired` | The anchor aged past the transaction's expiration during signature collection | Re-capture the anchor and re-collect. | +| `ChainAnchorError` on execution | An authenticated input note's creation block is not tracked by the anchor | Capture the anchor with `chain_anchor_for_request`, which tracks those blocks. | +| `INVALID_CHAIN_ANCHOR` | A sync landed mid-capture | Retry the capture. | +| `captureAnchor is not a function` | Web SDK older than `0.16.0-rc.3` | Bump `@miden-sdk/miden-sdk` and `@miden-sdk/react` together. | + +--- + +## Other transaction changes + +- **Proving is synchronous; execution stays asynchronous.** Adjust any code that awaited the proving step. +- **`ExecutedTransaction::account_delta()` became `account_patch()`**, matching the account update model. See [Account Changes](./account-changes#account-updates-move-from-accountdelta-to-accountpatch). +- **`ExecutedTransaction::compute_fee()` is gated behind the `testing` feature.** Production fee figures come from the `TX_FEE` note. + +--- + +## Common Errors + +| Error Message | Cause | Solution | +| --- | --- | --- | +| Node rejects a submitted transaction | Client and node versions mixed | Upgrade both to 0.16. | +| `this function takes 6 arguments but 4 were supplied` | `TransactionSummary::new` changed | Pass block commitment, expiration delta, and user params. | +| `no method named salt` on a summary | Renamed | Use `userParams()`. | +| `no method named fee` on an executed transaction | Fees now flow through the `TX_FEE` note | Inspect the output note. | +| `no method named account_delta` on an executed transaction | Renamed | Use `account_patch()`. | diff --git a/versioned_docs/version-0.16/builder/migration/07-client-changes.md b/versioned_docs/version-0.16/builder/migration/07-client-changes.md new file mode 100644 index 00000000..07e96f98 --- /dev/null +++ b/versioned_docs/version-0.16/builder/migration/07-client-changes.md @@ -0,0 +1,346 @@ +--- +sidebar_position: 7 +title: "Client Changes" +description: "Rust, Web, React and CLI client changes, plus the mandatory local store recreation" +--- + +# Client Changes + +:::danger Your local store must be recreated +Every pre-0.16 SQLite store is rejected. There is no migration path: delete the database and re-sync. Browser applications are handled automatically — the IndexedDB store detects the version bump and wipes itself on first open. In both cases **any state that existed only locally is lost**, including records for accounts not yet committed on-chain. +::: + +:::warning Client and node must be upgraded together +0.16 clients seal (encrypt) transaction inputs before submission. A 0.16 node rejects plaintext submissions and an older node rejects sealed ones, so the two cannot be mixed. Upgrade both. +::: + +## Quick Fix + +```bash +# CLI: the send subcommand was renamed +miden-client transfer -t -a 100:: -n private +``` + +```rust +// Rust: account updates are absolute patches now +let patch = tx_result.account_patch(); +``` + +```typescript +// Web: same split on the TypeScript side +const patch = txResult.accountPatch(); +``` + +If you encounter errors, continue reading for detailed migration steps. + +--- + +## Summary + +The client changes fall into four groups. The **store break** is the one that costs users data, and it is unavoidable. The **fee and sealing changes** are covered in [Transaction Changes](./transaction-changes) — they surface here as a new builder method and a node-version requirement. The **rename churn** (`account_delta` → `account_patch`, `send` → `transfer`, and friends) is mechanical. And a handful of **silent behavioural changes** — the `call` argument counting, the transaction summary display, `notes.sendPrivate` requiring a scan height — will not fail your build but will change what your application does. + +--- + +## (Store) Every pre-0.16 SQLite store must be recreated + +### Summary + +The store schema changed substantially: account SMT forest tables were added, account IDs and all digest columns were retyped from hex `TEXT` to `BLOB`, a `script_root` index was added, and the `migrations` table was dropped in favour of a schema fingerprint. + +### Affected Code + +A store written by miden-client 0.15.5 fails to open with: + +```text +Migration error: Attempt to migrate a database with a migration number that is too high +``` + +:::note This is not the error the changelog names +The changelog says opening a pre-0.16 database fails with `SchemaHashMismatch`. In practice a 0.15.5 store sits at `user_version = 2`, and because 0.16 defines only one migration the fingerprint check is skipped entirely — the failure surfaces from the migration layer instead. `SchemaHashMismatch` is only reached by a store at `user_version = 1`. Both paths fail; only the message differs. +::: + +Beyond the account ID retyping, the schema diff also shows the `latest_account_assets` and `historical_account_assets` column `vault_key` renamed to `asset_id` (following the [protocol rename](./asset-vault-faucet)), a new unique index on `tags(tag, source)`, and *all* digest columns retyped to `BLOB` — `account_commitment`, `note_id`, `nullifier`, `script_root`, `recipient_digest`, and storage keys and values. + +### Migration Steps + +1. Delete the store database and let the client recreate it, then re-sync. +2. Export anything you need to keep **before** upgrading — private note files in particular. +3. Browser applications need no action; the IndexedDB store resets itself when the client's minor version increases. +4. If you implement a custom `Store`, note that `insert_block_header` now takes a `nodes` argument, `insert_partial_blockchain_nodes` was removed, and the new `NoteFilter::ScriptRoots` variant makes existing exhaustive matches fail to compile. + +--- + +## (Rust) Account updates use `AccountPatch` + +`TransactionResult::account_delta()` became `account_patch()`, and `Account::apply_delta` was replaced by construction from a patch. `TransactionSummary::account_delta()` is deliberately unchanged. This is covered in full under [Account Changes](./account-changes#account-updates-move-from-accountdelta-to-accountpatch). + +One import detail specific to the client: in 0.15 `AccountStorageDelta` lived in `miden_client::asset`; the 0.16 replacement `AccountStoragePatch` lives in `miden_client::account`. The module moved as well as the name. `StorageMapDelta` and `StorageSlotDelta` were dropped from `miden_client::asset` alongside it, while `AccountVaultDelta` remains there. + +--- + +## (Rust) Fee conversion info on the transaction request + +`TransactionRequestBuilder::fee_conversion_info(info, salt)` is new and required on fee-charging chains for `AuthSingleSig` and `AuthMultisig` accounts. See [Transaction Changes](./transaction-changes#transaction-fees-are-paid-by-the-auth-procedure) for the full flow, including the mandatory `salt` argument that the changelog omits. + +--- + +## (Rust) Chain-anchored execution for multi-party signing + +`Client::chain_anchor_for_request` and `Client::execute_transaction_at` are new, and they are not optional for any flow that derives a transaction summary on one client and executes it on another. Since the summary now binds the reference block, the parties must agree on that block or the collected signatures do not apply — see [Transaction Changes](./transaction-changes#collecting-signatures-across-clients-requires-a-chainanchor) for the full flow. + +Two details specific to the client: `ClientError` gained a `ChainAnchorError` variant, so an exhaustive match over it no longer compiles; and both methods arrived in `0.16.0-rc.2`, one release after the version pinned in [Quick Upgrade](./#quick-upgrade). + +--- + +## (Rust) Fungible amounts use `AssetAmount` + +### Summary + +The client surface switched from raw `u64` to `AssetAmount` for fungible amounts. `AccountReader::get_balance` returns `AssetAmount`, and the token conversion helpers (`tokens_to_base_units`, `base_units_to_tokens`) and `build_pswap_consume` follow. + +### Migration Steps + +1. Wrap raw amounts with `AssetAmount`, or unwrap with the provided accessor where you need a `u64`. +2. Handle `TokenParseError::InvalidAmount` where you parse user-supplied amounts. + +--- + +## (Rust) Auth and faucet re-exports changed + +### Summary + +`AuthMethod` and `AuthSingleSigAcl` were removed, and the single fungible faucet factory split into auth-specific factories — see [Assets, Vault & Faucet Changes](./asset-vault-faucet#faucet-factories-split-by-authentication-scheme). Note that the client re-exports only two of the six upstream factory functions; for the rest, depend on `miden-standards` directly. + +The account policy components were also renamed, a change absent from the changelog and found by diffing the re-export lists: + +```diff +- AllowlistOwnerControlled ++ AllowlistManager +- BlocklistOwnerControlled ++ BlocklistManager +``` + +--- + +## (Rust) Note screening methods renamed + +### Summary + +`NoteScreener::can_consume` became `get_consumability`, and `can_consume_batch` became `get_batch_consumability`. A new `get_batch_consumability_for_account` was added. `Client::get_consumable_notes` keeps its signature — passing a single account is now screened more efficiently, but nothing about the call changes. + +:::note The rename is cosmetic +The changelog justifies it by saying the methods now return a consumption status per account rather than a boolean. They never returned a boolean — the return type is identical in 0.15 and 0.16. Rename the call sites; do not change how you handle the result. +::: + +--- + +## (Rust) Debug mode removed + +`DebugMode`, `ClientBuilder::in_debug_mode`, `Client::in_debug_mode`, and the `MIDEN_DEBUG` environment variable were all removed. The VM replaced the flag-gated `debug.*` decorators with `miden::core::debug` procedures that print unconditionally, so there is nothing left to gate. See [MASM Changes](./masm-changes#debug-and-trace-decorators-removed). + +--- + +## (Rust) Other library changes + +- **`StateSyncUpdate` is immutable** — construct with `from_parts`, read through accessors, and destructure with `into_parts`. `PartialBlockchainUpdates::insert` lost its nodes argument, and `extend_authentication_nodes` was added. +- **`miden_client::assembly::Library` was removed.** Use `miden_client::vm::Package`. Note that `Package` is not new — it was already re-exported in 0.15; only the `Library` removal is a 0.16 change. +- **`Client::fetch_all_private_notes` was removed**, replaced by note transport syncing. +- **`TransactionRecord` gained a private field**, so struct literal construction no longer compiles. +- **`send_notes` reads its payload from the advice provider** and requires a payload-commitment script argument. A `script_arg` passed alongside a `SendNotes` template is ignored. +- **`AccountSmtForest` is generic over its backend**, and the root-staging API was removed. +- **Response verification moved into `VerifyingRpcClient`.** The built-in gRPC constructors now wrap the transport in it automatically, but `ClientBuilder::rpc` does **not** — passing your own `NodeRpcClient` compiles and runs while silently losing response verification. Wrap it yourself with `VerifyingRpcClient::new(..)`. + +--- + +## (Web) Package and API changes + +Bump `@miden-sdk/miden-sdk` and `@miden-sdk/react` together — mixing 0.15 and 0.16 packages will not link against the shared WASM ABI. + +| Change | Migration | +| --- | --- | +| `ClientOptions.debugMode` removed; `createClient*` drops the trailing `debugMode` argument | Delete the option and the argument. | +| `accountDelta()` → `accountPatch()`; `AccountStorageDelta` removed | Rename. `TransactionSummary.accountDelta()` is unchanged. | +| `TransactionSummary.salt()` → `userParams()` | Rename; the value is now seven field elements. | +| `transactions.preview(..)` returns only a summary while authorization is pending | Do not expect full transaction details from a preview. | +| A summary derived on one client no longer reproduces on another | Capture a `ChainAnchor` and pass it as the `anchor` option. See [Transaction Changes](./transaction-changes#collecting-signatures-across-clients-requires-a-chainanchor). | +| `notes.sendPrivate` requires `scanAfterBlockNum`; new `notes.sendPrivateOutput` | Pass a scan height. | +| `notes.fetchPrivate({ mode: "all" })` removed | Use note transport syncing. | +| `AccountComponent.createNetworkAuth` → `createNetworkAuthComponents` | Rename; it now returns several components. | +| `FungibleAsset.withCallbacks(flag)` removed | Set callbacks on the account at construction. | +| P2ID and P2IDE notes must carry at least one asset | Building an empty note now throws. | +| Production WASM strips MASM debug metadata | Expect less detail in production stack traces. | +| Notes carrying a `NetworkAccountTarget` are priced via a foreign procedure invocation into the target | Behavioural; see the note below. | + +Additive: `notes.list({ scriptRoots })`, `NoteScript.networkAccountConfig()`, `NoteScript.feeSponsorship()`, and `compile.component({ namespace })`. + +Additive in `0.16.0-rc.3`, one release later: `transactions.captureAnchor(request)`, an `anchor` option on `preview` / `executeRequest` / `submit`, the wasm-level `chainAnchorForRequest` / `executeTransactionAt` / `executeForSummaryAt`, and `TransactionSummary.blockCommitment()` / `expirationDelta()`. + +If you author MASM through the Web SDK, the language changes apply to you as well — `@account_procedure` annotations, `mod` declarations, and the new import syntax. See [MASM Changes](./masm-changes). + +:::caution Unverified +The `NetworkAccountTarget` foreign-procedure-invocation requirement is reported from the changelog. We were not able to locate the enforcing call site in source, so treat it as a lead rather than a confirmed behaviour. +::: + +--- + +## (React) Send hooks relay through `sendPrivateOutputNote` + +`useSend`, `useTransaction`, and `useMultiSend` now relay private note output via `sendPrivateOutputNote`, following the `notes.sendPrivate` change above. If you wrapped these hooks, re-check the relay path. + +--- + +## (React) `useChainAnchor` and `usePreview` + +Both are new in `0.16.0-rc.3`, and `useTransaction().execute` accepts an `anchor` alongside them. `usePreview` is the first summary surface in the React SDK — before it, verifying and co-signing a multisig proposal meant dropping to the WASM client. + +If you build a multi-party signing flow, preview and execute against the `anchoredRequest` that `useChainAnchor` returns rather than the request you passed in. Re-resolving a request factory produces a different transaction, and any builder that creates an output note draws a fresh serial number, so the anchor would pin a request nobody executes. See [Transaction Changes](./transaction-changes#collecting-signatures-across-clients-requires-a-chainanchor). + +--- + +## (CLI) `send` renamed to `transfer` + +### Summary + +The `send` subcommand is now `transfer`. Nothing else changed — every flag, short form, and default is identical. `send` is **not** kept as an alias, so existing scripts fail with an unknown-subcommand error. + +### Affected Code + +```bash +# Before (0.15) +miden-client send -s -t -a 100:: -n private + +# After (0.16) +miden-client transfer -s -t -a 100:: -n private +``` + +### Migration Steps + +Replace `miden-client send` with `miden-client transfer` in scripts, aliases, and CI jobs. Change nothing else. + +--- + +## (CLI) `account --with-code` replaced by `account --inspect` + +### Summary + +`--with-code`, which dumped the account code as one pretty-printed blob, is gone. `account --inspect [:]` lists the procedures an account exposes, split into resolved procedures (name, signature, originating package) and unresolved ones listed by MAST root. + +### Affected Code + +```bash +# Before (0.15) +miden-client account --show --with-code + +# After (0.16) +miden-client account --inspect # list procedures +miden-client account --inspect --verbose # with MASM disassembly +miden-client account --inspect :receive_asset # a single procedure +miden-client account --inspect -p ./component.masp # resolve names from extra packages +``` + +### Migration Steps + +1. Replace `account --show --with-code` with `account --inspect --verbose`. +2. `--inspect` is mutually exclusive with `--list`, `--show`, and `--default`. +3. `--package` and `--verbose` both require `--inspect`. +4. Expect `` entries for procedures whose package the CLI cannot find; pass `--package` to resolve them. + +--- + +## (CLI) `call` counts arguments in field elements + +### Summary + +`call` validates argument count against the procedure's signature. In 0.15 it compared against the number of *parameters*; in 0.16 it compares against the total stack width in *field elements*. A procedure taking one `Word` now needs four `--args` values. + +This change is not in the changelog. + +### Affected Code + +```bash +# A procedure with signature `set_item(Word) -> ()` + +# Before (0.15): one parameter, one argument +miden-client call :set_item -p component.masp --args 0x1234 + +# After (0.16): a Word is four felts wide +miden-client call :set_item -p component.masp --args +``` + +### Migration Steps + +1. Re-check every scripted `call` whose procedure takes or returns anything wider than one field element. +2. Expand each wide argument into one value per field element, in signature order. +3. Read the `Raw Signature:` line the command prints — it is now the authoritative stack layout. + +A mismatched count fails with a clear error rather than executing with a mis-shaped stack, so this one fails loudly. + +--- + +## (CLI) `token_symbol_map.toml`: `id` renamed to `address` + +### Summary + +The per-symbol entry key changed from `id` to `address`. The value format is unchanged — it was already a bech32 address — so this is a pure key rename. A file still using `id` fails to parse rather than falling back. + +### Affected Code + +```toml +# Before (0.15) +BTC = { id = "mlcl1qru2e5yvx40ndgqqqzusrryr0ucyd0uj", decimals = 8 } + +# After (0.16) +BTC = { address = "mlcl1qru2e5yvx40ndgqqqzusrryr0ucyd0uj", decimals = 8 } +``` + +### Migration Steps + +1. Rename `id =` to `address =` on every entry. Leave the values alone. +2. The file lives in the `.miden` directory alongside `miden-client.toml`. If you have both a local and a global `.miden` directory, update both. + +--- + +## (CLI) `init` writes a different package set + +### Summary + +`init` now writes nine bundled `.masp` component packages instead of seven. + +```text +# Added in 0.16 +basic-non-fungible-faucet.masp +auth/guarded-multisig-auth.masp +auth/network-account-auth.masp + +# Removed in 0.16 +auth/acl-auth.masp +``` + +The removal is the CLI-side consequence of dropping `AuthSingleSigAcl`, and it is the one most likely to break an existing setup. The changelog mentions only the additions. + +Two error-reporting changes also landed: running `init` where a config already exists now names the configured network and points at `clear-config`, and an unparseable `--remote-prover-endpoint` is a hard error instead of being silently discarded. + +--- + +## (CLI) Other changes + +- **`--debug` and `MIDEN_DEBUG` removed.** Passing `--debug` is now a usage error; setting `MIDEN_DEBUG` is silently ignored. +- **The pre-confirmation transaction summary shows absolute values, not deltas** — including a column rename and `Nonce incremented by: N` becoming `New account nonce: N`. This follows from the `AccountPatch` move but changes what users read before approving a transaction. +- **`swap` gained `--payback-note-type `**, defaulting to `private` (0.15 hardcoded private). Note that `pswap` already had this flag in 0.15 with the same default. The tag the command tells you to track also changed, from a swap-specific tag to an account-target tag derived from the sender's account ID. +- **`consume-notes` gained `--start-debug-adapter ` and `--record `; `exec` gained `--record`.** `exec --start-debug-adapter` already existed in 0.15. Both require a build with the `dap` feature, which is not enabled by default. + +--- + +## Common Errors + +| Error Message | Cause | Solution | +| --- | --- | --- | +| `Migration error: Attempt to migrate a database with a migration number that is too high` | Pre-0.16 store | Delete and recreate the store. | +| `error: unrecognized subcommand 'send'` | Renamed | Use `transfer`. | +| `error: unexpected argument '--with-code'` | Removed | Use `--inspect`. | +| `Procedure '' expects 4 value(s), got 1` | Arguments counted in field elements | Expand wide arguments. | +| `missing field 'address'` parsing the token map | Key renamed | Rename `id` to `address`. | +| `error: unexpected argument '--debug'` | Removed | Delete the flag. | +| `no method named account_delta` on a transaction result | Renamed | Use `account_patch()`. | +| Node rejects a submission | Mixed client and node versions | Upgrade both to 0.16. | +| A component package is missing after `init` | `auth/acl-auth.masp` was removed | Migrate off `AuthSingleSigAcl`. | diff --git a/versioned_docs/version-0.16/builder/migration/08-masm-changes.md b/versioned_docs/version-0.16/builder/migration/08-masm-changes.md new file mode 100644 index 00000000..e525ed04 --- /dev/null +++ b/versioned_docs/version-0.16/builder/migration/08-masm-changes.md @@ -0,0 +1,395 @@ +--- +sidebar_position: 8 +title: "MASM Changes" +description: "The new mod declarations, rewritten import syntax, removal of the debug decorators, and the reorganised protocol procedure surface" +--- + +# MASM Changes + +:::warning Breaking Change +Miden Assembly gained an explicit module tree. A `.masm` file is no longer picked up because it sits in the right directory — its parent must declare it with `mod` or `pub mod`, and **an undeclared file is silently dropped from the artifact** rather than silently included. The `use` form was split into module imports and braced item imports, alias syntax changed from `->` to `as`, and the `debug.*` and `trace` decorators were removed. On the protocol side, asset helpers, note creation, and several account procedures moved to new paths. +::: + +## Quick Fix + +```masm +# Before (0.15) +use miden::standards::wallets::basic->basic_wallet +pub use miden::core::stark::verifier + +# After (0.16) +use miden::standards::wallets::basic as basic_wallet +pub mod verifier +pub use {verify} from self::verifier +``` + +```masm +# Every directory of .masm files now needs a mod.masm declaring its children +pub mod account +pub mod asset +mod callbacks # private to the parent +``` + +If you encounter errors, continue reading for detailed migration steps. + +--- + +## Summary + +The largest change is structural rather than syntactic. In 0.15 the assembler discovered modules by walking directories; in 0.16 it follows an explicit tree of `mod` declarations rooted at your project's root module. This is why the assembler's directory-based entry points disappeared (see [VM & Assembler Changes](./vm-assembler)) and why `miden-project.toml` now requires an explicit `path` to that root. + +The failure mode is worth internalising: forgetting a `mod` declaration is **not** an error at the declaration site. The module simply is not part of the artifact, and you discover it later as an undefined-symbol error at the call site — or, worse, not at all if nothing calls it. + +Everything else on this page is mechanical: import rewrites, decorator replacements, and renamed protocol procedures. + +--- + +## Every module must be declared with `mod` / `pub mod` + +### Summary + +A submodule's source is resolved as either `/.masm` or `//mod.masm`, relative to the declaring module's directory. The assembler includes only modules reachable through these declarations ([#3220](https://github.com/0xMiden/miden-vm/pull/3220)). + +### Affected Code + +In 0.15 the protocol's kernel root module was a comment; the directory tree was walked implicitly. In 0.16 it enumerates its children, and every intermediate directory gained its own `mod.masm` — the protocol repo went from 9 `mod.masm` files to 35: + +```masm +# After (0.16) — kernels/transaction-core/src/mod.masm +pub mod account +pub mod account_update +pub mod asset +pub mod asset_vault +mod callbacks # private: not reachable from outside this module +pub mod constants +pub mod epilogue +# … one line per child module +``` + +Declarations may be interleaved with `use` statements and appear anywhere among the top-level forms. Two other top-level forms landed alongside `mod`: an optional `namespace ` declaration that names the module explicitly, and `extern package "@"`. + +```masm +# After (0.16) — the full top-level form vocabulary +namespace app::accounts +extern package "miden/base@0.1.0" +mod internal +pub mod api +``` + +### Migration Steps + +1. For every directory of `.masm` files, add a `mod.masm` (or a sibling `.masm`) that declares each child with `pub mod `. Use plain `mod ` for modules that should not be reachable from outside the parent. +2. Walk your project root downward and confirm every `.masm` file is reachable from the root through a chain of declarations. +3. Do not declare a submodule with the same name as its parent, and do not declare the same source file from two different parents — both are hard errors. + +### Common Errors + +| Error Message | Cause | Solution | +| --- | --- | --- | +| `invalid submodule declaration '': could not find module sources at '/.masm' or '//mod.masm'` | `mod ` with no matching file | Create the file or remove the declaration. | +| `invalid submodule declaration '': submodules must not have the same name as their parent` | e.g. `mod foo` inside `foo/mod.masm` | Rename the child. | +| `conflicting submodule paths detected: '' can be parsed from either '' and '', but not both` | Both `.masm` and `/mod.masm` exist | Delete one. | +| `invalid submodule declaration '': module source '' is already reachable through another submodule declaration` | Two parents declare the same file | Declare it once. | +| `undefined item ''` on a call that used to work | The callee's module is not declared | Add the missing `mod` declaration. | + +--- + +## Import syntax: item imports, `as` aliases, and global resolution + +### Summary + +The `use` form was split into two explicitly distinguished shapes, and import resolution became strictly global ([#3220](https://github.com/0xMiden/miden-vm/pull/3220)): + +- **Module import** — `use some::module` or `use some::module as alias`. Brings a module into scope under a local name. **May not be `pub`.** +- **Item import** — `use {item} from some::module` or `use {a, b as c} from some::module`. Brings individual procedures, constants, or types into scope. **May be `pub`**, which is how you re-export. + +Four consequences follow: + +1. `pub use ` for re-exporting a *module* is gone. `pub use` is valid only in the braced item form, so **you can no longer re-export a module**, only named items. +2. The alias separator changed from `->` to `as`. +3. An import path may no longer begin with another import's alias — imports resolve in the global namespace, as if every path were absolute. +4. Submodule-relative imports need an explicit `self::` prefix. + +Source-level digest imports (`use 0x->name`) were removed. Direct digest *invocation* targets (`exec.0x…`) still work. + +### Affected Code + +The alias change, from the protocol's own P2IDE note script: + +```diff +- use miden::standards::wallets::basic->basic_wallet ++ use miden::standards::wallets::basic as basic_wallet +``` + +Re-exporting a procedure from another package: + +```diff +- pub use ::miden::utils::panic ++ pub use {panic} from ::miden::utils +``` + +Re-exporting from your own submodule, from the core library's `stark/mod.masm`: + +```masm +# Before (0.15) +use miden::core::stark::verifier +pub use verifier::verify +``` + +```masm +# After (0.16) +pub mod verifier +pub use {verify} from self::verifier +``` + +Note both halves of that change: `verifier` is now a declared submodule, and the re-export path is `self::verifier` rather than the bare alias. In 0.15 the second `use` resolved `verifier` through the first — that chaining is exactly what was removed. + +Plain module imports are unchanged and remain the common case: + +```masm +# Identical in 0.15 and 0.16 +use miden::core::crypto::hashes::poseidon2 +use miden::protocol::active_note +``` + +### Migration Steps + +1. Rewrite every `pub use a::b::c` re-export as `pub use {c} from a::b`. +2. Replace every `use path->alias` with `use path as alias`. +3. Rewrite any `use` whose path begins with an alias introduced by an earlier `use` in the same file to use the full global path. +4. To import from a submodule of the current module, prefix with `self::`. You cannot `use` a submodule you declared yourself — it is already in scope via `mod`, so reference it by name. +5. Delete any `use 0x->name` source-level digest imports. + +### Common Errors + +| Error Message | Cause | Solution | +| --- | --- | --- | +| ``` `pub use` is only supported for braced item imports ``` | `pub use some::module` | Use `pub use {item} from some::module`. | +| ``import aliases use `as`; `->` is no longer supported`` | `use foo->bar` | `use foo as bar`. | +| `import target '' cannot be resolved through import ''` | Path starts with another import's alias | Use the full global path. | +| `cannot import submodule '' declared in the same module` | `use` of your own `mod`-declared child | Drop the `use`. | +| `item import target '' resolved to a module` | `use {x} from …` where `x` is a module | Use the module-import form. | +| `digest imports are not supported` | `use 0x1234->entry` | Remove it; use `exec.0x…` directly. | + +--- + +## `debug.*` and `trace` decorators removed + +### Summary + +The `debug.*` decorator family and the `trace` decorator were removed from the language, along with the CLI `--trace` flag and the decorator wire slots in the MAST format. Print-style debugging now goes through the new `miden::core::debug` module, whose procedures are ordinary `emit` events handled host-side ([#3169](https://github.com/0xMiden/miden-vm/issues/3169), [#3201](https://github.com/0xMiden/miden-vm/pull/3201), [#3208](https://github.com/0xMiden/miden-vm/pull/3208)). + +:::danger These print in production +Because they are events rather than decorators, they carry no MAST cost — but unlike `debug.*`, which only fired when the VM ran in debug mode, **they print whenever invoked**. Leaving one in production code will print, and will disclose private values if your program has moved witness data onto the stack or into memory. +::: + +### Affected Code + +| v0.15 decorator | v0.16 replacement | +| --- | --- | +| `debug.stack` | `exec.debug::print_stack` | +| `debug.stack.` | `exec.debug::print_stack` (prints the whole stack; there is no top-`n` form) | +| `debug.mem` | `exec.debug::print_mem_all` | +| `debug.mem.` | `push. exec.debug::print_mem_addr` | +| `debug.mem..` | `push. push. exec.debug::print_mem` — takes `[start, end]`, end-exclusive | +| `debug.local`, `debug.local.`, `debug.local..` | `locaddr. exec.debug::print_mem_addr` | +| `debug.adv_stack.` | `push. push.0 exec.debug::print_adv_stack`, or `exec.debug::print_adv_stack_all` | +| `trace.` | Removed with no replacement. | + +```masm +# After (0.16) +use miden::core::debug + +begin + exec.debug::print_stack # [] -> [] + exec.debug::print_mem_all # [] -> [] + push.16 push.0 exec.debug::print_mem # [start=0, end=16] -> [] + locaddr.0 exec.debug::print_mem_addr # [addr] -> [] + exec.debug::print_adv_stack_all # [] -> [] + exec.debug::print_adv_map_all # [] -> [] + exec.debug::print_adv_map_item # [KEY] -> [] (consumes the key) +end +``` + +The full export list of `miden::core::debug` is `print_stack`, `print_mem`, `print_mem_addr`, `print_mem_all`, `print_adv_stack`, `print_adv_stack_all`, `print_adv_map_all`, and `print_adv_map_item`. + +On the Rust side, `DebugOptions`, `Instruction::Debug(..)`, and `Instruction::Trace(..)` no longer exist. + +### Migration Steps + +1. Search your MASM for `debug.` and `trace.` and replace per the table. Remember `print_mem` takes `[start, end]` with `end` exclusive, and both operands are consumed. +2. Add `use miden::core::debug` to any module that now calls these. +3. Remove `--trace` from any `miden-vm` invocation. +4. Register the handlers. `CoreLibrary::handlers()` includes the stack and memory debug handlers by default; the **advice** handlers are opt-in, so extend the handler set with `miden_core_lib::handlers::debug::advice_debug_handlers` to enable `print_adv_stack*` and `print_adv_map*`. +5. Strip these calls from production code. + +--- + +## Core library: the `miden::precompiles` namespace, and removals + +### Summary + +The core MASM package was split into `miden::core` and a new `miden::precompiles` namespace ([#3459](https://github.com/0xMiden/miden-vm/pull/3459), [#3222](https://github.com/0xMiden/miden-vm/pull/3222)). Some procedures that used to live under `miden::core::crypto` are now internal precompile support under `miden::precompiles`. + +`miden::core::crypto::hashes::keccak256` still exists and still exports `hash_bytes`, `hash`, and `merge` — it now delegates to `miden::precompiles::hashes::keccak256`. **Application code should keep calling the `miden::core::…` facade**; reach for `miden::precompiles::*` only if you are writing your own precompile wrapper. + +Several modules and procedures were removed outright. EdDSA and SHA-512 are documented as *temporarily* removed pending precompiles-prover support. + +| Removed in v0.16 | Replacement | +| --- | --- | +| `miden::core::crypto::dsa::eddsa_ed25519` (whole module) | None in this line. | +| `miden::core::crypto::hashes::sha512` (whole module) | None in this line. | +| `miden::core::crypto::dsa::ecdsa_k256_keccak::verify_prehash` | `verify`, or the new `verify_bytes`. | +| `miden::core::sys::log_precompile_request` | `miden::core::sys::build_proof_request_key` — a *different* operation, not a rename. | +| `miden::core::pcs::fri::frie2f4::preprocess` | Test-only helper; no replacement. | + +Additions in the same area: `ecdsa_k256_keccak::verify_bytes`, for verifying a signature over a variable-length Keccak256 message held in VM memory ([#3563](https://github.com/0xMiden/miden-vm/pull/3563)), and `miden::core::math::u256` reaching parity with the `u64` and `u128` modules ([#3167](https://github.com/0xMiden/miden-vm/pull/3167)). + +### Migration Steps + +1. If you verify Ed25519 signatures or hash with SHA-512 in MASM, there is no in-VM path in 0.16. Move that work off-chain or defer the upgrade. +2. Replace `ecdsa_k256_keccak::verify_prehash` with `verify` (word-sized message) or `verify_bytes` (variable-length message in memory) — and see the ABI change below, which you need either way. +3. If you wrote a custom precompile wrapper against `sys::log_precompile_request`, rewrite it against the deferred-DAG helpers in `miden::precompiles`. + +--- + +## ECDSA advice and signature ABI changed + +### Summary + +The advice-stack layout consumed by `miden::core::crypto::dsa::ecdsa_k256_keccak::verify` changed from `PK[9] | SIG[17]` — a 33-byte compressed public key and a 65-byte recoverable signature, byte-packed — to `QX[8] | QY[8] | SIG_R[8] | SIG_S[8]`, native little-endian `u32` limbs with **no recovery byte** ([#3222](https://github.com/0xMiden/miden-vm/pull/3222)). The public-key *commitment* preimage changed too; see [Hashing & Crypto Changes](./hashing-crypto). + +### Affected Code + +```masm +# Before (0.15) +#! Operand stack: [PK_COMM, MSG, ...] +#! Advice stack: [PK[9] | SIG[17] | ...] +exec.ecdsa_k256_keccak::verify +``` + +```masm +# After (0.16) +#! Operand stack: [PK_COMM, MSG_WORD, ...] +#! Advice stack: [QX[8] | QY[8] | SIG_R[8] | SIG_S[8] | ...] +exec.ecdsa_k256_keccak::verify + +# New: variable-length message held in memory +#! Operand stack: [PK_COMM, MSG_PTR, MSG_LEN_BYTES, ...] +exec.ecdsa_k256_keccak::verify_bytes +``` + +Two behavioural notes carried in the 0.16 source docs: `verify` **accepts high-`s` signatures**, because it proves that some witness satisfies the ECDSA equation and `(r, s)` and `(r, n-s)` are equivalent witnesses; and it pushes **no result word** — it traps on failure. + +### Migration Steps + +1. Rewrite the host code that populates the advice stack to emit `QX[8]`, `QY[8]`, `SIG_R[8]`, `SIG_S[8]` as little-endian `u32` limbs. +2. Drop the recovery byte — it is not part of the new ABI. +3. If you rely on canonical Ethereum-style signatures, add your own low-`s` check; `verify` will not reject high-`s`. +4. For messages longer than one word, switch from manual chunking to `verify_bytes`. + +--- + +## `do .. while .. end` loops added + +A tail-controlled loop form was added ([#3232](https://github.com/0xMiden/miden-vm/pull/3232)). This is additive — `while.true` is unchanged and still performs an entry check. + +```masm +# New in 0.16 +do + # always runs at least once +while + # must leave one boolean on top of the stack +end +``` + +Use it wherever you previously wrote `push.1 while.true … end` to force a first iteration. + +--- + +## Protocol procedure moves and renames + +### Summary + +The protocol MASM surface was reorganised: asset helpers moved from the protocol library into `miden::standards::assets`, note creation moved behind `miden::standards::note::note_creator`, and several `active_account` procedures moved to `native_account`. + +### Affected Code + +| v0.15 | v0.16 | +| --- | --- | +| `miden::protocol::asset::*` (build/validate helpers) | `miden::standards::assets::*` | +| `miden::protocol::faucet::create_fungible_asset` / `create_non_fungible_asset` | Removed — use the `miden::standards::assets` builders | +| `miden::protocol::output_note::create` (callable from note scripts) | Account context only; note scripts must call `miden::standards::note::note_creator::create_note` | +| `miden::protocol::active_account::get_initial_*` | `miden::protocol::native_account::get_initial_*` | +| `miden::protocol::active_account::has_non_fungible_asset` | `has_asset` | +| `miden::protocol::active_note::get_assets` | `get_initial_assets`, plus explicit removal procedures | +| `basic_wallet::add_assets_to_account` | `basic_wallet::move_note_assets_to_account` | +| `miden::standards::account::metadata` | `miden::standards::account::inspection` | + +```masm +# Before (0.15) — note script moving assets into the account +use miden::standards::wallets::basic->basic_wallet + +@note_script +pub proc main + call.basic_wallet::add_assets_to_account +end +``` + +```masm +# After (0.16) +use miden::standards::wallets::basic as basic_wallet + +@note_script +pub proc main + call.basic_wallet::move_note_assets_to_account +end +``` + +### Migration Steps + +1. Update every `use` path in your MASM per the table above. +2. Replace `output_note::create` in note scripts with `note_creator::create_note`. +3. Rename `add_assets_to_account` to `move_note_assets_to_account`. + +--- + +## Input-note assets are now stateful + +### Summary + +In 0.15 a note script read the note's asset list and the kernel reconciled it at the end of execution. In 0.16 the note's *initial* assets are read with `active_note::get_initial_assets`, and assets must be **explicitly removed** as they are consumed. Partially-consumed notes are representable, so the kernel no longer drains the note for you. + +### Affected Code + +The `active_note` asset surface in 0.16, verified from `asm/protocol/src/active_note.masm`: + +```masm +pub proc get_initial_assets # [dest_ptr] -> [num_assets] +pub proc get_initial_assets_info +pub proc get_initial_num_assets +pub proc get_asset +pub proc remove_asset +pub proc remove_all_assets +``` + +`active_note::get_storage` and the new `active_note::write_storage_to_memory` are the storage-side counterparts. + +### Migration Steps + +1. Replace `active_note::get_assets` with `active_note::get_initial_assets`. +2. Add an explicit removal call for each asset you move out of the note — `remove_asset` for individual assets, or `remove_all_assets` if you consume the note fully. +3. Do not assume the kernel drains the note. If you leave assets in place, the note is treated as partially consumed. + +--- + +## Common Errors + +| Error Message | Cause | Solution | +| --- | --- | --- | +| `undefined item ''` for a procedure that exists on disk | Its module is not declared with `mod` | Add the declaration to the parent module. | +| ``import aliases use `as`; `->` is no longer supported`` | Old alias syntax | Rewrite with `as`. | +| ``` `pub use` is only supported for braced item imports ``` | Re-exporting a module | Re-export named items instead. | +| `undefined instruction debug.stack` | Decorator removed | Use `exec.debug::print_stack`. | +| `undefined item 'add_assets_to_account'` | Procedure renamed | Use `move_note_assets_to_account`. | +| Note consumption fails with assets remaining | Assets are no longer drained implicitly | Call `remove_asset` / `remove_all_assets`. | diff --git a/versioned_docs/version-0.16/builder/migration/09-vm-assembler.md b/versioned_docs/version-0.16/builder/migration/09-vm-assembler.md new file mode 100644 index 00000000..e1f7c6f5 --- /dev/null +++ b/versioned_docs/version-0.16/builder/migration/09-vm-assembler.md @@ -0,0 +1,459 @@ +--- +sidebar_position: 9 +title: "VM & Assembler Changes" +description: "Library becomes Package, the MAST and package wire formats change, ExecutionProof is reworked, and miden-project.toml requires an explicit path" +--- + +# VM & Assembler Changes + +:::warning Breaking Change +The VM jumps **0.23 → 0.29.1**. `Library` and `KernelLibrary` no longer exist — `Package` is the only artifact type, and the `.masl` format is gone. The MAST wire format moved `0.0.3` → `0.0.4` and the package format `4.0.0` → `6.0.0`, so **no 0.15 artifact or serialized proof loads under 0.16**. Verification now takes a single `ExecutionClaim`, and `miden-project.toml` requires an explicit `path` on every target. +::: + +For the MASM language changes that ship with this VM version — the new `mod` declarations, the rewritten `use` syntax, and the removal of the `debug.*` decorators — see [MASM Changes](./masm-changes). For the changed commitment preimages, see [Hashing & Crypto Changes](./hashing-crypto). + +## Quick Fix + +```rust +// Before (0.15) +let mut assembler = Assembler::default(); +assembler.link_dynamic_library(CoreLibrary::default())?; +let program: Program = assembler.assemble_program(source)?; + +// After (0.16) +let mut assembler = Assembler::new(source_manager); +assembler.link_package(CoreLibrary::default().package(), Linkage::Dynamic)?; +let package: Box = assembler.assemble_program("program", source)?; +let program: Program = package.unwrap_program(); +``` + +```diff title="miden-project.toml" + [lib] + namespace = "my::app" ++ path = "mod.masm" +``` + +If you encounter errors, continue reading for detailed migration steps. + +--- + +## Summary + +The assembler was rebuilt around a single artifact type. In 0.15 there were three — `Program`, `Library`, and `KernelLibrary` — serialized as `.masl` for libraries. In 0.16 everything is a `Package` serialized as `.masp`, linking goes through one `link_package(package, linkage)` method, and the directory-walking `*_from_dir` entry points became `*_from_root` entry points that take the root module file. This follows directly from the new explicit module tree: the assembler no longer discovers modules by walking directories, so a directory is no longer a meaningful input. + +Three wire formats changed at the same time and none are backward compatible, which means every artifact must be rebuilt from source rather than migrated. + +Verification was also reshaped: the free `verify(program_info, stack_inputs, stack_outputs, proof)` function became `verify(proof, claim)` over a single `ExecutionClaim`, and the caller-managed precompile registry disappeared entirely — deferred proofs are now rehydrated and bound automatically. + +--- + +## `Library` → `Package` throughout the assembler + +### Summary + +`Library` and `KernelLibrary` were deleted. Every entry point that produced or consumed a `Library` now produces or consumes a `Package`, the `link_*_library` family collapsed into `link_package(package, linkage)`, and `assemble_program` returns a `Box` rather than a `Program`. Every assemble entry point now takes a package name ([#3216](https://github.com/0xMiden/miden-vm/pull/3216), [#3220](https://github.com/0xMiden/miden-vm/pull/3220)). + +### Affected Code + +```rust +// Before (0.15) +use miden_assembly::Assembler; +use miden_core_lib::CoreLibrary; + +let mut assembler = Assembler::default(); +assembler.link_dynamic_library(CoreLibrary::default())?; +let program: Program = assembler.assemble_program(source)?; +``` + +```rust +// After (0.16) +use miden_assembly::{Assembler, Linkage}; +use miden_core_lib::CoreLibrary; + +let mut assembler = Assembler::new(source_manager); +assembler.link_package(CoreLibrary::default().package(), Linkage::Dynamic)?; +for library in libraries { + assembler.link_package(library, Linkage::Dynamic)?; // Arc +} +let package: Box = assembler.assemble_program("program", source)?; +let program: Program = package.unwrap_program(); // or try_into_program() +``` + +The complete mapping: + +| v0.15 | v0.16 | +| --- | --- | +| `Assembler::with_kernel(sm, kernel_lib: KernelLibrary) -> Self` | `Assembler::with_kernel(sm, kernel: Arc) -> Result` | +| `link_library(lib, linkage)` / `link_dynamic_library(lib)` / `link_static_library(lib)` | `link_package(package: Arc, linkage: Linkage)` | +| `with_dynamic_library(lib)` / `with_static_library(lib)` | `with_package(package: Arc, linkage: Linkage)` | +| `compile_and_statically_link_from_dir(dir, namespace)` | `compile_and_statically_link_from_root(root, namespace: Option<&Path>)` | +| `assemble_library(modules) -> Arc` | `assemble_library(name, root, support) -> Box` | +| `assemble_library_from_dir(dir, namespace) -> Arc` | `assemble_library_from_root(root, namespace: Option<&Path>) -> Box` | +| `assemble_kernel(module) -> KernelLibrary` | `assemble_kernel(name, root, support) -> Box` | +| `assemble_kernel_from_dir(sys_path, lib_dir) -> KernelLibrary` | `assemble_kernel_from_root(name, sys_module_path) -> Box` | +| `assemble_program(source) -> Program` | `assemble_program(name, source) -> Box` | +| `kernel() -> &Kernel` | `kernel() -> &KernelDescriptor` | +| — | `with_profile(&miden_project::Profile)` *(new)* | + +Also removed from the `miden_assembly` re-export surface: `Library`, `KernelLibrary`, `Parse`, `ParseOptions`, `LinkLibraryKind`, and the `library` module. Added: `Linkage`, the `module` module, and the project-assembly types (`ProjectSourceProvider`, `MasmSourceProvider`, `ResolvedPackage`, `AssemblyInterrupted`). + +### Migration Steps + +1. Replace every `Library` / `KernelLibrary` binding with `Package` — `Arc` for linking, `Box` from the assemble methods. +2. Collapse `link_dynamic_library(x)` / `link_static_library(x)` / `link_library(x, l)` into `link_package(x, Linkage::Dynamic)` or `Linkage::Static`. +3. Rename `*_from_dir` calls to `*_from_root` and pass the root module file instead of the directory. Their `namespace` parameter is now `Option<&Path>` rather than a required `impl AsRef`. +4. Thread a package name through `assemble_program`, `assemble_library`, and `assemble_kernel`. Any string works; the CLI uses the literal `"program"`. +5. After `assemble_program`, call `.unwrap_program()` (panics on a non-executable package) or `.try_into_program()` to get the `Program` the processor expects. +6. Add `?` to `Assembler::with_kernel` — it is now fallible. + +### Common Errors + +| Error Message | Cause | Solution | +| --- | --- | --- | +| `cannot find type Library in miden_assembly` | Type removed | Use `Package`. | +| `no method named link_dynamic_library` | Collapsed into one method | `link_package(pkg, Linkage::Dynamic)`. | +| `expected Program, found Box` | `assemble_program` return type changed | Call `.unwrap_program()` or `.try_into_program()`. | +| `this function takes 2 arguments but 1 was supplied` | Assemble entry points take a package name | Pass a name as the first argument. | + +--- + +## Core package split into `miden::core` + `miden::precompiles` + +### Summary + +The single core MASM package was split into `miden-core` (namespace `miden::core`) and `miden-precompiles` (namespace `miden::precompiles`), freeing the bare `miden` namespace for sibling packages such as `miden-protocol` ([#3459](https://github.com/0xMiden/miden-vm/pull/3459), [#3222](https://github.com/0xMiden/miden-vm/pull/3222)). Core records a dynamic dependency on precompiles. Link both with `CoreLibrary::packages()` during assembly, and load `&CoreLibrary` into the host for execution so its merged MAST forest is available. + +### Affected Code + +```rust +// Before (0.15) +let mut assembler = Assembler::default(); +assembler.link_dynamic_library(CoreLibrary::default())?; +let lib = CoreLibrary::default().library(); // &Library +``` + +```rust +// After (0.16) +let core_lib = CoreLibrary::default(); +let mut assembler = Assembler::new(source_manager); +for package in core_lib.packages() { // [Arc; 2] + assembler.link_package(package, Linkage::Dynamic)?; +} + +// Or individually: +let core: Arc = core_lib.package(); +let precompiles: Arc = core_lib.precompiles_package(); +let mast: &Arc = core_lib.mast_forest(); // merged, for execution +``` + +`CoreLibrary::SERIALIZED` now holds the `miden-core.masp` bytes and a new `CoreLibrary::PRECOMPILES_SERIALIZED` holds `miden-precompiles.masp`; in 0.15 `SERIALIZED` was `core.masl`. `CoreLibrary::library()` and `CoreLibrary::verifier_registry()` are gone, and `CoreLibrary::recursive_verifier_root()` is new. + +### Migration Steps + +1. Replace the single link call with a loop over `CoreLibrary::default().packages()`, or link `package()` and `precompiles_package()` explicitly. +2. Replace `CoreLibrary::default().library()` with `.package()`. +3. Drop `CoreLibrary::verifier_registry()`. The deferred-precompile registry now lives in the `miden-precompiles` crate as `miden_precompiles::registry()` and is applied by the verifier automatically. + +--- + +## MAST wire format `0.0.4`, package format `6.0.0`, and `.masl` removed + +### Summary + +Three artifact-format changes land together, none backward compatible: + +- The **MAST wire format** bumped `[0,0,3]` → `[0,0,4]`, removing inline metadata slots. Assembly-op and debug-variable metadata now live in a separate indexed `DebugInfo` section ([#3201](https://github.com/0xMiden/miden-vm/pull/3201), [#3208](https://github.com/0xMiden/miden-vm/pull/3208), [#3221](https://github.com/0xMiden/miden-vm/pull/3221)). The stripped serialization mode was removed ([#3268](https://github.com/0xMiden/miden-vm/pull/3268)). +- The **package (`.masp`) format** bumped `[4,0,0]` → `[6,0,0]`, from consolidating debug sections into `PackageDebugInfo` ([#3398](https://github.com/0xMiden/miden-vm/pull/3398)) and binding dense forest and package digests to stored roots and dependencies ([#3334](https://github.com/0xMiden/miden-vm/pull/3334)). +- The **`.masl` library format no longer exists.** `Library::LIBRARY_EXTENSION` is gone along with the type; `.masp` is the only artifact format. + +### Affected Code + +```rust +// Any 0.15 blob fails to read under 0.16: +let forest = MastForest::read_from_bytes(&old_bytes)?; // Err: unexpected version [0,0,3] +let package = Package::read_from_bytes(&old_masp)?; // Err: unexpected version [4,0,0] +``` + +Package deserialization is now tiered by trust level. In 0.15 `Package` implemented only the plain `Deserializable::read_from`: + +```rust +// After (0.16) — three trust levels +Package::read_from(&mut r)? // untrusted: validates MAST, drops debug sections +Package::read_from_bytes(bytes)? +Package::read_from_trusted(&mut r)? // local cache: validates MAST, keeps debug sections +Package::read_from_bytes_trusted(bytes)? +Package::read_from_unchecked(&mut r)? // skips MAST validation; only for self-produced bytes +Package::read_from_bytes_unchecked(bytes)? +``` + +### Migration Steps + +1. Re-assemble every `.masp` package from source under 0.16, and re-serialize every cached `MastForest` blob. Invalidate on-disk and database-persisted copies. +2. Delete `.masl` artifacts and any code that reads them. +3. Discard serialized proofs from 0.15 — the proof envelope changed too. +4. Choose the reader that matches your trust boundary: `read_from_bytes` for anything from a registry, the network, or a user; `read_from_bytes_trusted` for your own build cache when you want debug info retained. + +--- + +## `ExecutionProof` reworked; `Verifier` replaces the free `verify_*` functions + +### Summary + +`ExecutionProof` was restructured from `{ proof, hash_fn, pc_requests }` into two envelopes, `StarkProof` and `DeferredProof`, and **proof serialization changed** ([#3222](https://github.com/0xMiden/miden-vm/pull/3222)). The legacy proof-bound precompile request model was replaced by the deferred-DAG framework in `miden_core::deferred`. + +On the native side, `verify(program_info, stack_inputs, stack_outputs, proof)` and `verify_with_precompiles(..)` were replaced by a `Verifier` type and a free `verify(proof, claim)` taking a single `ExecutionClaim` ([#3422](https://github.com/0xMiden/miden-vm/pull/3422), [#3447](https://github.com/0xMiden/miden-vm/pull/3447)). Recursive MASM verification moved from `exec.vm::verify_proof` to `exec.vm::verify_vm_proof`; the new procedure consumes a claim commitment and returns the deferred root plus proof security parameters. Rust callers construct its request-addressed advice with `RecursiveVerifierInputs::for_request`. + +### Affected Code + +```rust +// Before (0.15) +let security_level = miden_verifier::verify( + program_info, stack_inputs, stack_outputs, proof, +)?; +let (level, commitment) = miden_verifier::verify_with_precompiles( + program_info, stack_inputs, stack_outputs, proof, ®istry, +)?; +``` + +```rust +// After (0.16) +use miden_core::program::ExecutionClaim; +use miden_verifier::{Verifier, verify}; + +let claim = ExecutionClaim::from_program_info(program_info, stack_inputs, stack_outputs); + +let security_level: u32 = verify(proof, claim)?; // free fn, default config +let security_level: u32 = Verifier::new().verify(proof, claim)?; // equivalent + +// Partial (delegable) verification returns a #[must_use] obligation: +let (level, unsettled) = Verifier::new() + .with_max_deferred_elements(n) + .verify_partial(proof, claim)?; +let root: Word = unsettled.root(); +``` + +In VM 0.29.1, `ExecutionProof` exposes `miden_proof() -> &StarkProof` and `deferred_proof() -> &DeferredProof`, with constructors `ExecutionProof::new(miden, deferred)` and `from_parts(bytes, hash_fn, deferred)`. The 0.15 public fields, three-argument `new`, `stark_proof()`, `hash_fn()`, `precompile_requests()`, and `into_parts()` are gone. For a non-default partial-verification budget, configure `Verifier::with_max_deferred_elements(n)` before calling `verify_partial`. + +`verify_with_precompiles` and `verify_with_max_deferred_elements` are both removed. Precompile verification is no longer wired up by the caller: the deferred wire is rehydrated under the built-in `miden_precompiles::registry()` and bound to the STARK public inputs automatically. + +`prove` and `prove_sync` keep their 0.15 signatures. New in this line: `prove_partial`, `prove_partial_sync`, and `prove_partial_from_trace_sync`. + +### Migration Steps + +1. Build an `ExecutionClaim` — usually `ExecutionClaim::from_program_info(info, inputs, outputs)` — and pass `(proof, claim)` to `verify`. +2. Delete `PrecompileVerifierRegistry` plumbing and calls to `verify_with_precompiles` / `verify_with_max_deferred_elements`. Use `Verifier::with_max_deferred_elements(n)` if you need a non-default budget. +3. Replace field access on `ExecutionProof` with `miden_proof()` / `deferred_proof()`. +4. Discard serialized proofs from 0.15 — they will not deserialize. +5. If you use `verify_partial`, do not drop the returned `Unsettled`. It is `#[must_use]` and represents a deferred obligation you must settle or re-expose. + +--- + +## `AdviceInputs.stack` replaced by the `AdviceStack` type + +### Summary + +`AdviceInputs`'s public `stack: Vec` field was replaced by a private `AdviceStack`, and the `with_stack` / `with_stack_values` / `extend_stack` helpers were removed in favour of `with_advice_stack(AdviceStack)` and the `advice_stack()` accessor ([#3423](https://github.com/0xMiden/miden-vm/pull/3423)). + +### Affected Code + +```rust +// Before (0.15) +let advice = AdviceInputs::default() + .with_stack(vec![a, b, c]) + .with_stack_values([1u64, 2, 3])? + .with_map(entries); +let raw: &Vec = &advice.stack; +``` + +```rust +// After (0.16) +use miden_core::advice::{AdviceInputs, AdviceStack}; + +let mut stack = AdviceStack::new(); +stack.append_word(word).append_elements([a, b, c]); +// or, from raw u64s, validating each against the field modulus: +let stack = AdviceStack::try_from_values([1u64, 2, 3])?; + +let advice = AdviceInputs::default() + .with_advice_stack(stack) + .with_map(entries); + +let stack: AdviceStack = advice.advice_stack(); // clone of the stack +let (stack, map, store) = advice.into_parts(); // new in 0.16 +``` + +`AdviceStack` distinguishes append (bottom) from prepend/push (top) and names the MASM instruction each targets: `append_element`, `append_elements`, `append_word`, `append_dword`, `append_for_adv_push`, `append_for_adv_pipe`, `prepend_elements`, `prepend_word`, `prepend_stack`, `push_element`, plus `consume_element` / `consume_word` / `consume_dword` and `into_elements`. `AdviceInputs::map` and `AdviceInputs::store` remain public fields. + +### Migration Steps + +1. Replace `with_stack(iter)` with `with_advice_stack(AdviceStack::…)`, building the stack with the append/prepend methods. +2. Replace `with_stack_values(u64s)?` with `AdviceStack::try_from_values(u64s)?`. +3. Replace direct reads of `advice_inputs.stack` with `advice_inputs.advice_stack()`, or destructure with `into_parts()`. +4. Mind the ordering vocabulary: `append_*` adds below (consumed later), `prepend_*` and `push_element` add on top (consumed first). + +:::info New resource bounds +The live advice map is now bounded by total field-element count and the advice Merkle store by internal node count, both during setup and execution ([#3264](https://github.com/0xMiden/miden-vm/pull/3264)). `FastProcessor` memory growth is bounded by a configurable `ExecutionOptions::max_memory_elements` ([#3226](https://github.com/0xMiden/miden-vm/pull/3226)). If you seed very large advice inputs, expect a setup-time error rather than silent success. +::: + +--- + +## `ModuleInfo` → `ModuleDescriptor`, `Kernel` → `KernelDescriptor` + +### Summary + +The module and kernel metadata types were renamed and relocated ([#3356](https://github.com/0xMiden/miden-vm/pull/3356)). + +### Affected Code + +```diff +- use miden_core::program::Kernel; +- use miden_assembly::library::ModuleInfo; +- let k: &Kernel = assembler.kernel(); ++ use miden_core::program::KernelDescriptor; ++ use miden_assembly::module::ModuleDescriptor; ++ let k: &KernelDescriptor = assembler.kernel(); +``` + +The module path moved as well: the `library` module is gone from `miden_assembly`'s re-exports, and `ModuleDescriptor` lives under `module`. + +### Migration Steps + +1. Rename `Kernel` → `KernelDescriptor` and `ModuleInfo` → `ModuleDescriptor` at every import and binding. +2. Update the import path from `miden_assembly::library` to `miden_assembly::module`. +3. Re-check descriptor method names against the new type — several were renamed alongside it. + +--- + +## `miden-project.toml`: `path` is mandatory on every target + +### Summary + +The `path` key on `[lib]` and `[[bin]]` targets changed from optional to required. It may point at files with extensions other than `.masm` — a Rust project's source root, for example — which is why the implicit default was dropped ([#3216](https://github.com/0xMiden/miden-vm/pull/3216)). In the Rust AST, `LibTarget::path` and `BinTarget::path` moved from `Option>` to `Span`. + +A project with neither a `[lib]` nor any `[[bin]]` still gets an implicit library target defaulting to `mod.masm`; that inference is unchanged. + +### Affected Code + +```diff title="miden-project.toml" + [lib] + namespace = "miden::protocol" ++ path = "mod.masm" + + [[bin]] + name = "entry" ++ path = "bin/main.masm" +``` + +### Migration Steps + +1. Add an explicit `path` to every `[lib]` and `[[bin]]` in every `miden-project.toml`. +2. If you construct `LibTarget` / `BinTarget` in Rust, drop the `Some(..)` wrapper around `path`. + +--- + +## `miden-vm bundle` reworked + +### Summary + +`miden-vm bundle` now takes the path to a **root `.masm` module** instead of a directory, `--kernel` is a boolean flag instead of taking a path, and the output is a `.masp` package instead of a `.masl` library. With `--kernel` set, the kernel's support modules are derived from the explicit `mod` declarations in the root module ([#3216](https://github.com/0xMiden/miden-vm/pull/3216), [#3220](https://github.com/0xMiden/miden-vm/pull/3220)). + +### Affected Code + +```bash +# Before (0.15) +miden-vm bundle --namespace mylib ./src # directory -> out.masl +miden-vm bundle --kernel ./kernel.masm ./src # --kernel takes a path + +# After (0.16) +miden-vm bundle --namespace mylib ./src/mod.masm # root module -> out.masp +miden-vm bundle --kernel ./kernel/mod.masm # --kernel is a flag +``` + +`--namespace` is now optional in the non-kernel case: if omitted, the assembler expects a `namespace` declaration in the root module, where 0.15 fell back to the directory name. For `--kernel` the namespace defaults to `$kernel`. A new `-r` / `--release` flag disables debug symbols. + +### Migration Steps + +1. Change the positional argument from a directory to the root module file. +2. Change `--kernel ` to a bare `--kernel` with the kernel's root module as the positional argument. +3. Update the expected output filename from `out.masl` to `out.masp`. +4. Ensure the root module declares its submodules with `mod` / `pub mod` — that is now how support modules are discovered. +5. Either pass `--namespace` or add a `namespace` declaration to the root module. + +--- + +## `ProjectAssembler::assemble_with_sources` removed + +### Summary + +`ProjectAssembler::assemble_with_sources(target, profile, sources)` was removed — projects must be assembled from the filesystem ([#3216](https://github.com/0xMiden/miden-vm/pull/3216)). In its place, project assembly is extensible through the `ProjectSourceProvider` trait, which lets non-MASM source languages participate ([#3375](https://github.com/0xMiden/miden-vm/pull/3375), [#3383](https://github.com/0xMiden/miden-vm/pull/3383)). + +### Affected Code + +```rust +// Before (0.15) +let pkg = project_assembler.assemble_with_sources(target, profile, sources)?; +``` + +```rust +// After (0.16) +let pkg: Arc = project_assembler.assemble(target_selector, profile_name)?; + +// Register a provider for a non-MASM source language: +let mut pa = Assembler::new(sm) + .for_project_at_path_with_providers(manifest_path, &mut store, [my_provider])?; + +// A provider can interrupt assembly: +match pa.assemble_interruptible(target_selector, profile_name)? { + ControlFlow::Continue(pkg) => { /* … */ }, + ControlFlow::Break(interrupted) => { /* … */ }, +} +``` + +`ProjectAssembler::assemble(target_selector, profile_name)` keeps its 0.15 signature. + +### Migration Steps + +1. Drop `assemble_with_sources`; write your sources to disk and use `assemble`, or implement a `ProjectSourceProvider`. +2. If you need to react to a provider interrupting assembly, use `assemble_interruptible` and match on the `ControlFlow`. + +:::note Changelog correction +The 0.25.4 changelog names the new method `ProjectAssembler::assemble_source_project`. The method that actually exists in the released code is **`assemble_source_package`**. +::: + +--- + +## Smaller Rust API removals + +These are lower-impact, but each will break a build if you touch it. + +| Removed / changed | PR | +| --- | --- | +| `MastForest::compact` removed — deduplicate through builders or explicit `MastForest::merge` | [#3318](https://github.com/0xMiden/miden-vm/pull/3318) | +| Stripped `MastForest` serialization mode removed | [#3268](https://github.com/0xMiden/miden-vm/pull/3268) | +| Dense forest construction moved to `DenseMastForestBuilder`; non-canonical dense payloads rejected | [#3334](https://github.com/0xMiden/miden-vm/pull/3334) | +| `MastForestBuilder` simplified around builder-local refs and immutable finalized forests | [#3139](https://github.com/0xMiden/miden-vm/pull/3139) | +| `prettier::pretty_print_csv`, `MastNodeId::from_usize_safe`, `DecoratorId::from_u32_bounded`, `OpBatch::end_indices` removed | [#3197](https://github.com/0xMiden/miden-vm/pull/3197) | +| `Processor` trait methods moved into their sub-interfaces | [#3202](https://github.com/0xMiden/miden-vm/pull/3202) | +| `ExecutionOptions::with_overlapped_trace_build` added, on by default | [#3407](https://github.com/0xMiden/miden-vm/pull/3407) | +| `miden-vm run` / `miden-vm prove` now fail when the inferred `.inputs` file is missing instead of proceeding | [#3236](https://github.com/0xMiden/miden-vm/pull/3236) | +| `ResumeContext` exposes its debug info outside `miden-processor` and can be built from a `Package` | [#3355](https://github.com/0xMiden/miden-vm/pull/3355) | +| Proof serialization switched from `bincode` to `wincode`; verifier-side STARK proof deserialization bounded to 64 MiB | [#3148](https://github.com/0xMiden/miden-vm/pull/3148) | +| `AeadPoseidon2::key_from_bytes` restored to canonical-`Felt` decoding; keys persisted under the brief SHA-256 KDF contract must be re-derived | [#3366](https://github.com/0xMiden/miden-vm/pull/3366) | +| `Felt::from_{u8,u16,u32}` are now `const`; `Felt::MAX` added | [crypto#1081](https://github.com/0xMiden/crypto/pull/1081) | +| Assembling a procedure with more locals than the frame pointer can represent is a diagnostic error rather than a panic | [#3332](https://github.com/0xMiden/miden-vm/pull/3332) | + +The `miden-crypto` 0.26 and 0.27 breaking changes are almost entirely in `LargeSmt` / `LargeSmtForest` storage backends and prover internals, which application code does not call. The one exception worth knowing: the RustCrypto and dalek stack (`k256`, `sha2`, `sha3`, `curve25519-dalek`, `ed25519-dalek`, `x25519-dalek`, `hkdf`, `der`) was upgraded ([crypto#1045](https://github.com/0xMiden/crypto/pull/1045)) and `rand` moved to 0.10 ([crypto#995](https://github.com/0xMiden/crypto/pull/995)). Expect version-unification pressure if you depend on those crates directly. + +--- + +## Common Errors + +| Error Message | Cause | Solution | +| --- | --- | --- | +| `unexpected version [0,0,3]` reading a `MastForest` | MAST wire format is now `0.0.4` | Re-assemble from source. | +| `unexpected version [4,0,0]` reading a package | Package format is now `6.0.0` | Rebuild the `.masp`. | +| Cannot open a `.masl` file | Format removed entirely | Rebuild as `.masp`. | +| `cannot find function verify_with_precompiles` | Replaced by automatic deferred verification | Use `verify(proof, claim)`. | +| `no field stack on type AdviceInputs` | Field is now private | Use `advice_stack()` or `into_parts()`. | +| `missing field path` parsing `miden-project.toml` | `path` is mandatory | Add it to every `[lib]` and `[[bin]]`. | +| Serialized proof fails to deserialize | Proof envelope and serialization changed | Regenerate the proof. | diff --git a/versioned_docs/version-0.16/builder/migration/10-rust-sdk-compiler.md b/versioned_docs/version-0.16/builder/migration/10-rust-sdk-compiler.md new file mode 100644 index 00000000..75f9a02a --- /dev/null +++ b/versioned_docs/version-0.16/builder/migration/10-rust-sdk-compiler.md @@ -0,0 +1,169 @@ +--- +sidebar_position: 10 +title: "Rust Contract SDK & Compiler" +description: "Changes to the miden crate and midenc for developers writing smart contracts in Rust" +--- + +# Rust Contract SDK & Compiler + +:::info Which "Rust SDK"? +Two different things get called the Rust SDK. This page is about the **`miden` crate and `midenc`**, used to write account components, notes, and transaction scripts *in Rust* and compile them to MASM. The `miden-client` library — used to build applications that talk to a Miden node — is covered in [Client Changes](./client-changes). +::: + +:::warning Breaking Change +Component trait methods must now be marked `#[account_procedure]` to be part of the account interface, and `#[account(..)]` generates one trait per interface instead of inherent methods. Note also that the contract toolchain **lags the rest of the 0.16 line**: it builds against protocol `0.16.0-alpha.4` and VM `0.25`, not the protocol `0.16.0-rc` and VM `0.29.1` that the client and node use. +::: + +## Quick Fix + +```rust +// Before +#[component] +trait BasicWallet { + fn receive_asset(&mut self, asset: Asset); +} + +// After +#[component] +trait BasicWallet { + #[account_procedure] + fn receive_asset(&mut self, asset: Asset); +} +``` + +If you encounter errors, continue reading for detailed migration steps. + +--- + +## Versions + +The contract toolchain versions independently of the rest of the stack, and in this release it is genuinely behind. + +| Component | Version | +| --- | --- | +| `midenc` / compiler workspace | 0.10.0 | +| `miden` contract SDK crate (and `miden-base-sys`, `miden-stdlib-sys`, `miden-sdk-alloc`) | 0.14.0 | +| Protocol it builds against | `0.16.0-alpha.4` | +| VM crates it builds against | 0.25 | +| MSRV | 1.97 (plus a nightly toolchain) | + +Two consequences worth planning around: + +- The MSRV is **1.97**, higher than the 1.96 the rest of the stack requires. Your toolchain must satisfy the highest of the two. +- Because the toolchain pins protocol `0.16.0-alpha.4` and VM `0.25`, contract code compiled with it sees an earlier snapshot of the 0.16 protocol surface than your client does. The MAST and package wire formats are compatible across VM 0.25 and 0.29.1, so artifacts still load; the skew is in the protocol API surface, not serialization. + +--- + +## Component methods must be marked `#[account_procedure]` + +### Summary + +A `#[component]` trait's methods are no longer implicitly part of the account interface. Every method that must be callable from a note script, a transaction script, a foreign procedure invocation, or a sibling component now needs `#[account_procedure]` **on the trait declaration**, not on the `impl`. + +`#[auth_script]` and `#[account_procedure]` cannot be combined in one component. An authentication component keeps using `#[auth_script]` alone; mixing them is a compile error. Like `#[auth_script]`, `#[account_procedure]` is recognised by the enclosing `#[component]` macro and needs no import. + +### Affected Code + +```rust +// Before +use miden::{Asset, NoteIdx, component, component_storage, output_note}; + +#[component] +trait BasicWallet { + fn receive_asset(&mut self, asset: Asset); + fn move_asset_to_note(&mut self, asset: Asset, note_idx: NoteIdx); +} +``` + +```rust +// After +use miden::{Asset, NoteIdx, NoteType, Recipient, Tag, component, component_storage, output_note}; + +#[component] +trait BasicWallet { + #[account_procedure] + fn receive_asset(&mut self, asset: Asset); + + #[account_procedure] + fn move_asset_to_note(&mut self, asset: Asset, note_idx: NoteIdx); + + #[account_procedure] + fn create_note(&mut self, tag: Tag, note_type: NoteType, recipient: Recipient) -> NoteIdx; +} +``` + +The `impl` block is unchanged — the attribute is not repeated there. + +### Migration Steps + +1. For each `#[component] trait`, add `#[account_procedure]` above every method called from a note, a transaction script, FPI, or a sibling component. +2. Leave authentication components alone. They keep `#[auth_script]` and must not gain `#[account_procedure]`. +3. Purely internal helper methods can stay unmarked. + +:::caution The shipped templates disagree with this rule +The `cargo miden new` account template declares its method without `#[account_procedure]` while the sibling note and tx-script templates call it, and the full-project scaffold has the same gap. The repository's own `examples/counter-contract` does mark them. The template tests only build, never execute, so the gap is not caught by CI. If you scaffold a new project, add the attribute yourself rather than trusting the generated code. +::: + +--- + +## `#[account(..)]` generates one trait per interface + +### Summary + +`#[account(..)]` used to generate the referenced component's methods as **inherent** methods on the wrapper struct. It now generates **one trait per referenced interface**, named after the interface, and implements it for the wrapper. This lets two components exporting the same method name coexist on one account. + +Most single-component call sites are unchanged, but two situations break. + +### Affected Code + +The wrapper struct may no longer share its name with a generated trait: + +```rust +// Before — compiled +#[account(counter_contract::CounterContract)] +struct CounterContract; + +// After — rename the wrapper +#[account(counter_contract::CounterContract)] +struct Counter; + +let counter = Counter::new(counter_account_id); +let count = counter.get_count(); +``` + +Cross-module call sites need the generated trait in scope. A `#[note]` or `#[tx_script]` entrypoint in the same module sees it automatically; a call site in a different module needs to import the trait, which is named after the interface: + +```rust +use crate::BasicWallet; // the generated trait, not the wrapper struct +``` + +### Migration Steps + +1. Rename any wrapper struct that collides with its interface name. +2. Import the generated trait at cross-module call sites. + +--- + +## Other changes + +- **Transaction-kernel bindings were renamed and moved** to track the protocol 0.16 surface, and several were removed. +- **Kernel scalars are typed** rather than raw `Felt`, so values that used to be interchangeable now need explicit conversion. +- **`AssetAmount`** is a validated fungible-amount type, matching the protocol and client surfaces. +- **`miden-project.toml` requires an explicit `path`** on `[lib]` and every `[[bin]]`. See [VM & Assembler Changes](./vm-assembler#miden-projecttoml-path-is-mandatory-on-every-target). +- **`#[note]` reserves `get_entrypoint_root`**, so a note struct cannot define a method with that name, and note structs now implement `ToFeltRepr`. +- **`cargo miden new` fetches templates from a release bundle** rather than embedding them. + +Additive in this line: typed transaction-script arguments, note constructors, and `println!`-style formatting. + +--- + +## Common Errors + +| Error Message | Cause | Solution | +| --- | --- | --- | +| A component method is not callable from a note or script | Missing `#[account_procedure]` | Add it to the trait method declaration. | +| Compile error combining auth and account attributes | They are mutually exclusive | Auth components keep `#[auth_script]` only. | +| Name collision between a wrapper struct and a trait | `#[account(..)]` now generates traits | Rename the wrapper. | +| `no method named ..` at a cross-module call site | The generated trait is not in scope | Import the trait named after the interface. | +| `missing field path` in `miden-project.toml` | Now mandatory | Add `path` to every target. | +| Toolchain version error | MSRV is 1.97 here | Use the higher of the stack's requirements. | diff --git a/versioned_docs/version-0.16/builder/migration/index.md b/versioned_docs/version-0.16/builder/migration/index.md new file mode 100644 index 00000000..db60528f --- /dev/null +++ b/versioned_docs/version-0.16/builder/migration/index.md @@ -0,0 +1,199 @@ +--- +title: "v0.16 Migration Guide" +description: "Complete guide for upgrading from Miden v0.15 to v0.16" +pagination_prev: null +--- + +# Miden Testnet 0.16.0 + +This guide covers all breaking changes you need to migrate an application to Miden 0.16.0. Like the 0.15 guide, it is intentionally user-facing: you do not need to know or care which internal crate (VM, protocol, client) a change came from. If you are: + +- building accounts, notes, or transactions +- running a client, web client or React SDK +- writing or compiling MASM +- writing Rust smart contracts with the `miden` SDK +- interacting with storage, auth, or RPCs + +this document is for you. It folds together the breaking changes from the protocol crates (`0.15.3` → `0.16.0`), the VM crates (`miden-vm`, `0.23` → `0.29.1`), `miden-client` (`0.15` → `0.16.0`), the Web SDK (`@miden-sdk/*` `0.15` → `0.16.0`), and the `miden` Rust contract SDK / compiler (`0.13` → `0.14`). + +--- + +## Quick Upgrade + +Try upgrading first — most projects can start with a dependency update: + +```toml title="Cargo.toml" +# Replace these +miden-client = "0.15" +miden-client-sqlite-store = "0.15" +miden-protocol = "0.15.3" +miden-standards = "0.15.3" +miden-tx = "0.15.3" +miden-tx-batch-prover = "0.15.3" +miden-assembly = "0.23" +miden-core = "0.23" +miden-core-lib = "0.23" +miden-processor = "0.23" +miden-prover = "0.23" +miden-crypto = "0.25" + +# With these +miden-client = "0.16.0-rc.1" +miden-client-sqlite-store = "0.16.0-rc.1" +miden-protocol = "0.16.0-rc.6" +miden-standards = "0.16.0-rc.6" +miden-tx = "0.16.0-rc.6" +miden-tx-batch = "0.16.0-rc.6" # renamed from miden-tx-batch-prover +miden-assembly = "0.29.1" +miden-core = "0.29.1" +miden-core-lib = "0.29.1" +miden-processor = "0.29.1" +miden-prover = "0.29.1" +miden-crypto = "0.29.1" +``` + +```json title="package.json (Web SDK)" +{ + "@miden-sdk/miden-sdk": "0.16.0-rc.2", + "@miden-sdk/react": "0.16.0-rc.2" +} +``` + +Then run: + +```bash +cargo update && cargo build +``` + +If you encounter errors, continue reading for detailed migration steps. + +:::warning 0.15 artifacts do not round-trip +The MAST wire format moved `0.0.3` → `0.0.4`, the package format `4.0.0` → `6.0.0`, and the `.masl` library format was removed entirely. Several commitment preimages changed as well. **Re-assemble every package from source and re-sync into a fresh store.** +::: + +:::danger Your local store must be recreated, and your node must be upgraded with your client +Every pre-0.16 SQLite store is rejected — there is no migration path. Browser applications reset their IndexedDB store automatically. Separately, 0.16 clients seal (encrypt) transaction inputs before submission, so a 0.16 client cannot talk to an older node and vice versa. +::: + +--- + +:::info Who should read this? +This guide is for: +- **Rust client developers** migrating from v0.15 → v0.16 +- **Web SDK developers** using the JavaScript/TypeScript SDK +- **Smart contract authors** writing MASM or using protocol APIs +- **App developers** using the protocol, standards, or client crates + +If you're starting fresh on v0.16, you can skip this guide and go directly to the [Get Started guide](../get-started). +::: + +--- + +## At a Glance + +Big themes in 0.16: + +| Change | Summary | +|--------|---------| +| **Fees moved into the auth procedure** | The kernel no longer burns the fee automatically. The auth procedure reads `FeeConversionInfo` from the transaction's auth args and emits a `TX_FEE` note. On a fee-charging chain, requests signed by `AuthSingleSig`/`AuthMultisig` must call `TransactionRequestBuilder::fee_conversion_info(info, salt)`. | +| **MASM gained an explicit module tree** | A `.masm` file is only included if its parent declares it with `mod`/`pub mod` — an undeclared file is *silently dropped*. `use` split into module imports and braced item imports, aliases moved from `->` to `as`, and imports resolve globally. | +| **Account updates became absolute** | `AccountDelta` → **`AccountPatch`** for account updates (`ExecutedTransaction`, `AccountUpdateDetails`, client results). `TransactionSummary::account_delta()` deliberately stays relative. | +| **Signed summaries bind their reference block** | A summary now only authorizes an execution at the block it was derived at, so multisig and offline co-signing flows break silently — every party derives a different summary at its own sync height. Capture a **`ChainAnchor`** and have all of them execute against it. Nothing fails to compile. | +| **Auth is no longer a special builder slot** | `AccountBuilder::with_auth_component` is gone; auth components pass through `with_component(s)` and are found by their `@auth_script` attribute. Keys are wrapped in a new **`Approver`** / `ApproverSet`. `AuthMethod` and `AuthSingleSigAcl` are removed. | +| **Asset identity renamed one level down** | `AssetVaultKey` → **`AssetId`**, and the old `AssetId` → **`AssetClass`**. Because `AssetId` survives with a new meaning, careless renaming compiles and is wrong. | +| **`Library` is gone; `Package` is the only artifact** | `Library`/`KernelLibrary` were deleted, `link_*_library` collapsed into `link_package`, `*_from_dir` became `*_from_root`, and `.masl` no longer exists. MAST `0.0.4` / package `6.0.0` are not backward compatible. | +| **Notes use typed builders, and carry fewer assets** | `XNote::create(..)` → `XNote::builder()…build()?` + `.into()`. **`MAX_ASSETS_PER_NOTE` dropped 64 → 16.** Mint and burn scripts were unified across faucet kinds, changing their roots. | +| **Debug decorators removed** | `debug.*` and `trace` are gone from the language, replaced by `miden::core::debug` procedures — which, unlike the decorators, **print unconditionally**. The client and CLI debug-mode toggles were removed with them. | +| **Commitment preimages changed** | ECDSA public-key commitments, MMR peak commitments, and domain-separated empty-input hashes all changed value. Nothing fails to compile; stored values simply stop matching. | +| **Store and node compatibility both break** | Every pre-0.16 SQLite store must be recreated, and transaction inputs are now sealed, so client and node must be upgraded together. | + +If you only skim a few sections, skim **Transaction Changes**, **Account Changes**, **MASM Changes**, and **Client Changes**. + +--- + +## Compatibility + +| Component | Required | Tested With | +|-----------|----------|-------------| +| Miden VM crates | 0.29+ | 0.29.1 | +| miden-crypto | 0.29+ | 0.29.1 | +| miden-protocol | 0.16+ | 0.16.0-rc.6 | +| miden-standards | 0.16+ | 0.16.0-rc.6 | +| miden-client | 0.16+ | 0.16.0-rc.1 | +| Web SDK (`@miden-sdk/*`) | 0.16+ | 0.16.0-rc.2 | +| `miden` contract SDK | 0.14+ | 0.14.0-rc.1 | +| `midenc` compiler | 0.10+ | 0.10.0-rc.1 | +| Rust (client) | 1.96+ | 1.96 | +| Rust (protocol / VM) | 1.96.1+ | 1.96.1 | +| Rust (contract SDK / compiler) | 1.97+ | 1.97 | + +:::note Pin the exact pre-release version +The 0.16 protocol and client crates currently publish as `0.16.0-rc.N`. Cargo does not match a pre-release against a plain `"0.16"` requirement, so pin the exact string until the final release is published. +::: + +:::note The contract toolchain lags the rest of the line +`midenc` and the `miden` contract SDK build against protocol `0.16.0-alpha.4` and VM `0.25`, not the protocol `0.16.0-rc` and VM `0.29.1` used by the client and node. Artifacts still load — the MAST and package formats are compatible across those VM versions — but the protocol API surface the compiler sees is an earlier snapshot. Its MSRV is also higher, at 1.97. +::: + +--- + +## Migration Sections + +Work through these sections in order for a complete migration: + +| Section | Topics | +|---------|--------| +| [1. Imports & Dependencies](./imports-dependencies) | Crate bumps, VM 0.23 → 0.29.1, MSRV 1.96, artifacts that must be rebuilt | +| [2. Hashing & Crypto Changes](./hashing-crypto) | ECDSA public-key commitments, MMR peaks binding the leaf count, empty domain-separated hashing | +| [3. Account Changes](./account-changes) | `with_auth_component` removed, `Approver`/`ApproverSet`, component name changes, `AccountPatch` | +| [4. Note Changes](./note-changes) | Typed note builders, `MAX_ASSETS_PER_NOTE` 64 → 16, unified mint/burn scripts | +| [5. Assets, Vault & Faucet](./asset-vault-faucet) | `AssetVaultKey` → `AssetId`, old `AssetId` → `AssetClass`, split faucet factories | +| [6. Transaction Changes](./transaction-changes) | Fees paid by the auth procedure, sealed transaction inputs, `TransactionSummary`, `ChainAnchor` | +| [7. Client Changes](./client-changes) | Store recreation, node compatibility, chain-anchored execution, Rust/Web/React/CLI changes | +| [8. MASM Changes](./masm-changes) | `mod` declarations, new import syntax, debug decorators removed, protocol procedure moves | +| [9. VM & Assembler Changes](./vm-assembler) | `Library` → `Package`, MAST `0.0.4`, `ExecutionClaim`, `miden-project.toml` | +| [10. Rust Contract SDK & Compiler](./rust-sdk-compiler) | `#[account_procedure]`, `#[account(..)]` generating traits, toolchain version skew | + +--- + +## Final Checklist + +Complete these steps to verify your migration: + +- [ ] Bump all Miden crate versions per section 1, pinning the exact `0.16.0-rc.N` strings, and rename `miden-tx-batch-prover` to `miden-tx-batch` +- [ ] Bump `@miden-sdk/miden-sdk` and `@miden-sdk/react` together; drop any `miden-idxdb-store` dependency +- [ ] Update the toolchain to Rust 1.96 (1.97 if you also build Rust contracts) +- [ ] Re-assemble every `.masp` from source and delete cached `MastForest` blobs; `.masl` no longer exists +- [ ] **Delete and recreate your local store**, then re-sync — export private note files first +- [ ] **Upgrade your node together with your client** — sealed and plaintext submissions are mutually incompatible +- [ ] Add `mod` / `pub mod` declarations so every `.masm` file is reachable from your project root +- [ ] Rewrite `pub use a::b::c` as `pub use {c} from a::b`, and `use x->y` as `use x as y` +- [ ] Replace `debug.*` / `trace` decorators with `miden::core::debug` procedures, and strip them from production code +- [ ] Declare fee conversion info on transactions if your chain charges a fee, and fund the paying account with the fee asset +- [ ] Move auth components out of `with_auth_component` and wrap keys in `Approver` / `ApproverSet` +- [ ] Rename `AssetId` → `AssetClass` **first**, then `AssetVaultKey` → `AssetId` +- [ ] Replace `account_delta()` with `account_patch()` — but leave `TransactionSummary::account_delta()` alone +- [ ] If you collect signatures over a summary across clients, capture a `ChainAnchor` and derive, verify, and execute the transaction against it +- [ ] Rewrite `XNote::create(..)` calls as builders, and cap notes at 16 assets +- [ ] Recompute stored ECDSA public-key commitments, MMR peak commitments, and empty domain-separated hashes +- [ ] Replace `Library`/`KernelLibrary` with `Package`, and `link_*_library` with `link_package` +- [ ] Add an explicit `path` to every `[lib]` and `[[bin]]` in `miden-project.toml` +- [ ] Build an `ExecutionClaim` and call `verify(proof, claim)`; discard proofs serialized under 0.15 +- [ ] CLI: rename `send` to `transfer`, `--with-code` to `--inspect`, and `id` to `address` in `token_symbol_map.toml` +- [ ] CLI: re-check every `call` invocation — arguments are now counted in field elements +- [ ] *(If you write Rust contracts)* mark component trait methods with `#[account_procedure]` and import the traits generated by `#[account(..)]` +- [ ] Run `cargo build` — **no errors** +- [ ] Run `cargo test` — **all tests pass** + +:::tip You're done! +If your project builds and all tests pass, you've successfully migrated to v0.16. +::: + +--- + +## Need Help? + +- **Telegram:** [Build on Miden](https://t.me/BuildOnMiden) — technical discussion and support. +- **Forum:** [Miden discussions](https://github.com/0xMiden/miden-node/discussions) — longer-form questions and design discussion. +- **GitHub issues:** file against the relevant repo — [`rust-sdk`](https://github.com/0xMiden/rust-sdk/issues), [`web-sdk`](https://github.com/0xMiden/web-sdk/issues), [`protocol`](https://github.com/0xMiden/protocol/issues), [`miden-vm`](https://github.com/0xMiden/miden-vm/issues), or [`compiler`](https://github.com/0xMiden/compiler/issues). +- **Changelogs:** the per-repo `CHANGELOG.md` files carry the full list of changes, including non-breaking features and fixes omitted from this guide. diff --git a/versioned_docs/version-0.16/builder/smart-contracts/_category_.json b/versioned_docs/version-0.16/builder/smart-contracts/_category_.json new file mode 100644 index 00000000..e5cd9472 --- /dev/null +++ b/versioned_docs/version-0.16/builder/smart-contracts/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Smart Contracts", + "position": 3 +} diff --git a/versioned_docs/version-0.16/builder/smart-contracts/accounts/_category_.json b/versioned_docs/version-0.16/builder/smart-contracts/accounts/_category_.json new file mode 100644 index 00000000..65048510 --- /dev/null +++ b/versioned_docs/version-0.16/builder/smart-contracts/accounts/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "Accounts", + "position": 4, + "link": { + "type": "generated-index", + "slug": "/builder/smart-contracts/accounts" + } +} diff --git a/versioned_docs/version-0.16/builder/smart-contracts/accounts/account-operations.md b/versioned_docs/version-0.16/builder/smart-contracts/accounts/account-operations.md new file mode 100644 index 00000000..4a2b65ba --- /dev/null +++ b/versioned_docs/version-0.16/builder/smart-contracts/accounts/account-operations.md @@ -0,0 +1,130 @@ +--- +title: "Account Operations" +sidebar_position: 4 +description: "Query account state and mutate the vault using self methods in Miden components." +--- + +# Account Operations + +The `#[component]` macro automatically provides methods on `self` for interacting with the current account during a transaction. Read-only queries are available on `&self`, and mutations (add/remove assets, increment nonce) require `&mut self`. + +## Read-only queries (`&self`) + +```rust +#[component] +impl MyAccount for MyAccountStorage { + fn check_state(&self, asset_id: Word) { + // Account identity + let id: AccountId = self.get_id(); + let nonce: Nonce = self.get_nonce(); + + // Vault queries + let value: Word = self.get_asset(asset_id); + let initial_value: Word = native_account::get_initial_asset(asset_id); + let has_asset: bool = self.has_asset(asset_id); + let root: Word = self.get_vault_root(); + let initial_root: Word = native_account::get_initial_vault_root(); + + // Commitment queries + let commitment: Word = self.compute_commitment(); + let initial_commit: Word = native_account::get_initial_commitment(); + let storage: Word = self.compute_storage_commitment(); + let initial_storage: Word = native_account::get_initial_storage_commitment(); + let code: Word = self.get_code_commitment(); + + // Procedure queries + let count: u32 = self.get_num_procedures(); + let proc_root: Word = self.get_procedure_root(0); + let exists: bool = self.has_procedure(proc_root); + } +} +``` + +## Mutations (`&mut self`) + +```rust +#[component] +impl MyAccount for MyAccountStorage { + fn receive_asset(&mut self, asset: Asset) { + // Add an asset to the vault — returns the resulting value word + let stored_value: Word = self.add_asset(asset); + } + + fn send_asset(&mut self, asset: Asset, note_idx: NoteIdx) { + // Remove an asset from the vault — returns the resulting value word + // Proof generation fails if the asset doesn't exist or insufficient balance + self.remove_asset(asset); + output_note::add_asset(asset, note_idx); + } +} +``` + +:::warning +The nonce must be incremented for any transaction that modifies account state. Without it, the same transaction could be replayed. +::: + +## When proof generation fails + +Several operations cause proof generation to fail if preconditions aren't met: + +| Operation | Fails when | +|-----------|-----------| +| `remove_asset(asset)` | Asset not in vault or insufficient balance | +| `get_procedure_root(index)` | Index out of bounds | +| Any `assert!()` | Condition is false | +| Transaction body (overall) | No state change occurred **and** no notes were consumed | + +When proof generation fails: +1. The ZK circuit cannot produce a valid proof +2. The transaction is rejected **before reaching the network** +3. No state changes occur +4. The client receives an error describing the failure + +The last row is enforced at end-of-execution by the VM kernel rather than mid-execution: a transaction that mutates no account state (storage, vault, or nonce) **and** consumes no notes is rejected. See [Empty Transaction](../../tutorials/helpers/pitfalls#empty-transaction-no-state-change-no-notes) for the recommended pattern. + +## Example: ManagedWallet + +```rust +#![no_std] +#![feature(alloc_error_handler)] + +use miden::{component, component_storage, output_note, Asset, NoteIdx, Word}; + +#[component_storage] +struct ManagedWalletStorage; + +#[component] +trait ManagedWallet { + #[account_procedure] + fn receive_asset(&mut self, asset: Asset); + #[account_procedure] + fn send_asset(&mut self, asset: Asset, note_idx: NoteIdx); + #[account_procedure] + fn asset_value(&self, asset_id: Word) -> Word; +} + +#[component] +impl ManagedWallet for ManagedWalletStorage { + /// Receive an asset into the vault. + fn receive_asset(&mut self, asset: Asset) { + self.add_asset(asset); + } + + /// Send an asset to an output note, with balance check. + fn send_asset(&mut self, asset: Asset, note_idx: NoteIdx) { + self.remove_asset(asset); + output_note::add_asset(asset, note_idx); + } + + /// Read the value word stored under an asset ID. + fn asset_value(&self, asset_id: Word) -> Word { + self.get_asset(asset_id) + } +} +``` + +To move assets out of an account, create [output notes](../notes/output-notes) with `output_note::add_asset`. For signature verification and nonce management, see [Authentication](./authentication). + +:::info API Reference +Full API docs on docs.rs: [`miden`](https://docs.rs/miden/latest/miden/) +::: diff --git a/versioned_docs/version-0.16/builder/smart-contracts/accounts/authentication.md b/versioned_docs/version-0.16/builder/smart-contracts/accounts/authentication.md new file mode 100644 index 00000000..6766d950 --- /dev/null +++ b/versioned_docs/version-0.16/builder/smart-contracts/accounts/authentication.md @@ -0,0 +1,108 @@ +--- +title: "Authentication" +sidebar_position: 6 +description: "Authentication component pattern and nonce management for Miden accounts." +--- + +# Authentication + +Miden uses digital signatures for transaction authentication. Because transactions execute on the client rather than onchain validators, the system needs a way to prove that a transaction was authorized by the account owner. Without authentication, anyone could construct a valid proof that transfers assets out of an account. The nonce prevents replay attacks — without it, a valid proof could be resubmitted to execute the same state change twice. For details on the cryptographic primitives, see [Cryptography](./cryptography). + +The scheme-agnostic [`AuthSingleSig`](https://docs.rs/miden-standards/latest/miden_standards/account/auth/struct.AuthSingleSig.html) component handles single-signature accounts. It takes an `Approver`, which pairs a public-key commitment with an authentication scheme such as `Falcon512Poseidon2` or `EcdsaK256Keccak`. The native hash function is Poseidon2, and the Falcon-512 verifier MASM module is `miden::core::crypto::dsa::falcon512_poseidon2`. + +## How authentication works + +The standards `AuthSingleSig` component stores two items under well-known names: + +| Storage slot | Name | Description | +|---|---|---| +| Public key | `miden::standards::auth::singlesig::pub_key` | Commitment to the account owner's public key | +| Scheme ID | `miden::standards::auth::singlesig::scheme` | Which signature scheme to use (1 = ECDSA K256 Keccak, 2 = Falcon-512 Poseidon2) | + +During transaction execution the kernel invokes the `@auth_script`-annotated procedure on the account. For `AuthSingleSig`, that procedure loads both slots and: + +1. Increments the account nonce (even if the account state did not change — this is required for replay protection). +2. Pays the transaction fee by creating a public `TX_FEE` note when the chain charges a fee. +3. Computes a transaction summary that binds the account change, input and output notes, reference block commitment, expiration, and user parameters. Because the fee is paid first, the fee note and its vault withdrawal are also covered by the signature. +4. Requests the signature from the advice provider and verifies it with the scheme indicated by the stored scheme ID. + +If verification fails, proof generation fails and the transaction is rejected before reaching the network. The signature itself isn't passed as a function argument — it's provided through the **advice provider**, a mechanism that supplies auxiliary data to the VM during proof generation. See [Advice Provider](../transactions/advice-provider) for the full API. + +## Attaching `AuthSingleSig` to an account + +Authentication is an ordinary account component. Attach `AuthSingleSig` with `AccountBuilder::with_component`; the builder recognizes it through its `@auth_script` procedure. `miden-client` re-exports `AuthScheme` as `AuthSchemeId`: + +```rust +use miden_client::{ + account::{AccountBuilder, AccountType, component::BasicWallet}, + auth::{Approver, AuthSchemeId, AuthSecretKey, AuthSingleSig}, +}; + +let secret_key = AuthSecretKey::new_falcon512_poseidon2(); +let public_key = secret_key.public_key().to_commitment(); + +let account = AccountBuilder::new(seed) + .account_type(AccountType::Public) + .with_component(AuthSingleSig::new(Approver::new( + public_key, + AuthSchemeId::Falcon512Poseidon2, + ))) + .with_component(BasicWallet) + .build()?; +``` + +Here `seed` is a random 32-byte account seed. Keep `secret_key` in your client's +keystore and register it for this account before submitting transactions. A +placeholder public-key commitment cannot authorize transactions. + +If you import directly from `miden-protocol`, the same enum is called `AuthScheme` (`miden_protocol::account::auth::AuthScheme`) — `miden-client` just re-exports it under a friendlier name. + +## Writing a custom auth component + +If you need authentication logic beyond `AuthSingleSig` / `AuthMultisig`, you can write a custom auth component in Rust. Mark exactly one procedure per auth component with `#[auth_script]`. Do not combine `#[auth_script]` with `#[account_procedure]`. If the procedure returns without panicking, the transaction kernel treats authentication as successful. If it panics (for example via `assert!`), authentication fails. On a fee-charging chain, custom authentication must also pay the transaction fee; the standard authentication components handle this automatically. + +```rust +#![no_std] +#![feature(alloc_error_handler)] + +use miden::{component, component_storage, Word}; + +#[component_storage] +struct AuthComponentStorage; + +#[component] +trait AuthComponent { + #[auth_script] + fn verify(&mut self, _arg: Word); +} + +#[component] +impl AuthComponent for AuthComponentStorage { + fn verify(&mut self, _arg: Word) { + // Custom authentication checks go here. + // + // Returning normally = authentication succeeded. + // Panicking (e.g. `assert!(false)`) = authentication failed and + // the transaction will be rejected before proof generation finishes. + todo!() + } +} +``` + +## Nonce management + +The nonce prevents replay attacks — each transaction must use a unique nonce. For accounts using the standards `AuthSingleSig` (or `AuthMultisig`) component, nonce increment is handled inside `authenticate_transaction`. If you implement a fully custom auth script, you are responsible for incrementing the nonce yourself and for binding it into the signed message. + +The nonce is committed into the transaction proof. If someone tries to replay a transaction, the nonce won't match the account's current nonce and verification will fail. + +Auth components are invoked automatically by the kernel — you do not call them directly from note scripts or [transaction scripts](../transactions/transaction-scripts). For access control and security patterns, see [Patterns](../patterns). + +:::info API Reference +Full API docs on docs.rs: [`miden`](https://docs.rs/miden/latest/miden/), [`AuthSingleSig`](https://docs.rs/miden-standards/latest/miden_standards/account/auth/struct.AuthSingleSig.html), [`AuthScheme`](https://docs.rs/miden-protocol/latest/miden_protocol/account/auth/enum.AuthScheme.html) +::: + +## Related + +- [Cryptography](./cryptography) — Falcon-512 / Poseidon2 verification and hashing primitives +- [Advice Provider](../transactions/advice-provider) — supplying auxiliary data during proof generation +- [Patterns](../patterns) — access control, rate limiting, and anti-patterns diff --git a/versioned_docs/version-0.16/builder/smart-contracts/accounts/components.md b/versioned_docs/version-0.16/builder/smart-contracts/accounts/components.md new file mode 100644 index 00000000..93e60c85 --- /dev/null +++ b/versioned_docs/version-0.16/builder/smart-contracts/accounts/components.md @@ -0,0 +1,216 @@ +--- +title: "Components" +sidebar_position: 1 +description: "Define Miden account components using the #[component] macro — storage, methods, and auto-generated bindings." +--- + +# Components + +Components are the building blocks of Miden accounts. Each component defines a [storage](./storage) layout, exposes public methods, and can be composed with other components on the same account — for example, a wallet component + an auth component + custom logic. This modularity lets you reuse a wallet component across many accounts and test or upgrade components independently. + +## The `#[component]` macro + +A component has three Rust pieces: a `#[component_storage]` struct for storage fields, a `#[component]` trait for the public interface, and a `#[component] impl Trait for Storage` block for behavior: + +```rust +use miden::{component, component_storage, felt, Felt, StorageMap, Word}; + +#[component_storage] +struct CounterContractStorage { + #[storage(description = "counter contract storage map")] + count_map: StorageMap, +} + +#[component] +trait CounterContract { + #[account_procedure] + fn get_count(&self) -> Felt; + #[account_procedure] + fn increment_count(&mut self) -> Felt; +} + +#[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 + } +} +``` + +The macros generate: + +1. **Public API exports** describing the component's callable methods +2. **Storage metadata** mapping slot names to slot IDs (derived from the component package + field name) +3. **Runtime bindings** for the Miden execution environment + +## Project manifest + +Every account component crate also needs a `miden-project.toml` next to `Cargo.toml`: + +```toml title="miden-project.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" + +[dependencies] +miden-core = "*" +miden-protocol = "*" +``` + +The namespace interface segment must match the kebab-cased `#[component]` trait name. For generated WIT dependencies, see [Cross-Component Calls](../cross-component-calls#2-generated-wit-dependency). + +## Storage struct + +The storage struct defines the component's storage layout: + +```rust +use miden::{component_storage, AccountId, Felt, StorageMap, StorageValue, Word}; + +#[component_storage] +struct MyContractStorage { + #[storage(description = "owner account identifier")] + owner: StorageValue, + + #[storage(description = "initialization flag")] + initialized: StorageValue, + + #[storage(description = "user balances")] + balances: StorageMap, +} +``` + +### Storage fields + +Each field must be either `StorageValue` (single-slot) or `StorageMap` (map-slot), annotated with `#[storage]`: + +```rust +#[storage(description = "human-readable description")] +field_name: StorageValue, + +#[storage(description = "human-readable description")] +field_name: StorageMap, +``` + +The `description` is optional and becomes part of the generated metadata. Slot IDs are derived from the component package name and the field name, so **renaming a field changes the slot ID**. Ordering does not matter, and `slot(N)` is not supported. + +## Trait and impl block — methods + +Declare callable methods on the `#[component]` trait, mark each one with `#[account_procedure]`, then implement that trait for the storage struct. Unmarked trait methods are not exported from the account interface. Authentication entrypoints use `#[auth_script]` instead; the two attributes cannot be combined. + +### Read methods (`&self`) + +Methods that take `&self` can read component storage but cannot mutate it through `self`: + +```rust +fn get_balance(&self, depositor: AccountId) -> Felt { + self.balances.get(depositor) +} +``` + +### Write methods (`&mut self`) + +Methods that take `&mut self` can update component storage and call mutating methods on `self`: + +```rust +fn deposit(&mut self, asset: Asset) { + self.add_asset(asset); +} +``` + +### Private methods + +Helpers that are not declared on the `#[component]` trait are private. Define them on the storage struct with a normal inherent impl, then call them from the component trait implementation: + +```rust +impl MyContractStorage { + fn require_initialized(&self) { + let state: Word = self.initialized.get(); + assert!(state[0] == felt!(1)); + } +} + +#[component] +impl MyContract for MyContractStorage { + fn do_something(&mut self) { + self.require_initialized(); + // ... + } +} +``` + +### Supported parameter and return types + +Public methods can use SDK types (`Felt`, `Word`, `Asset`, `AccountId`, `NoteIdx`) and custom types annotated with [`#[export_type]`](./custom-types). + +## Auto-generated methods + +The `#[component]` macro automatically provides methods on `self` for account operations. + +### Mutation methods (`&mut self`) + +```rust +// Add an asset to the account vault +self.add_asset(asset: Asset) -> Word + +// Remove an asset from the account vault +self.remove_asset(asset: Asset) -> Word + +// Increment the account nonce (only inside #[auth_script]) +self.incr_nonce() -> Nonce +``` + +The kernel allows `incr_nonce` only from the account's authentication procedure, +and only once per transaction. Calling it from an ordinary account procedure +fails. Standard authentication components handle this increment automatically. + +### Read-only methods (`&self`) + +```rust +// Get the account ID +self.get_id() -> AccountId + +// Get the account nonce +self.get_nonce() -> Nonce + +// Get the value word stored under an asset key +self.get_asset(asset_key: Word) -> Word + +// Check fungible or non-fungible asset ownership +self.has_asset(asset_id: Word) -> bool + +// Compute commitment of account state changes +self.compute_delta_commitment() -> Word + +// Check if a procedure was called during this transaction +self.was_procedure_called(proc_root: Word) -> bool + +// Get storage and vault commitments +self.get_vault_root() -> Word +self.compute_commitment() -> Word +self.compute_storage_commitment() -> Word +// ... and more (see API Reference) +``` + +If storage or the vault has changed, `compute_delta_commitment` requires the +nonce to have been incremented. Compute such a delta inside the authentication +procedure after the increment; calling it earlier fails. + +For the full list of auto-generated methods, see [Account Operations](./account-operations). To export your own types for use in public method signatures, see [Custom Types](./custom-types). + +:::info API Reference +Full API docs on docs.rs: [`miden`](https://docs.rs/miden/latest/miden/) (top-level — `#[component]` macro) +::: diff --git a/versioned_docs/version-0.16/builder/smart-contracts/accounts/cryptography.md b/versioned_docs/version-0.16/builder/smart-contracts/accounts/cryptography.md new file mode 100644 index 00000000..e6e45caf --- /dev/null +++ b/versioned_docs/version-0.16/builder/smart-contracts/accounts/cryptography.md @@ -0,0 +1,68 @@ +--- +title: "Cryptography" +sidebar_position: 5 +description: "Falcon-512 Poseidon2 signature verification and hashing primitives in Miden contracts." +--- + +# Cryptography + +The Miden SDK exposes cryptographic primitives for signature verification and hashing. These are low-level functions used by authentication components and anywhere message digests or hash-based commitments are needed. + +## Falcon-512 Poseidon2 verification + +The core function for signature verification: + +```rust +use miden::{emit_falcon_sig_to_stack, rpo_falcon512_verify}; + +// Verify a Falcon512 signature +// pk: Poseidon2 hash of the public key +// msg: Poseidon2 hash of the message +emit_falcon_sig_to_stack(msg, pk); +rpo_falcon512_verify(pk, msg); +``` + +| Parameter | Type | Description | +|-----------|------|-------------| +| `pk` | `Word` | Poseidon2 hash of the signer's public key | +| `msg` | `Word` | Poseidon2 hash of the message being verified | + +The function panics (proof generation fails) if the signature is invalid. + +:::info Where's the signature? +`emit_falcon_sig_to_stack` requests the signature from the host, which loads it onto the advice stack. The Rust verifier is still named `rpo_falcon512_verify` for compatibility, but it uses Falcon-512 over Poseidon2. You don't pass the signature directly to the verifier. + +With the standard transaction host, generating a new signature is allowed only +inside the authentication procedure, using a valid transaction summary. Outside +authentication, supply the signature in the transaction's advice inputs before +execution; the event can then load it for verification. +::: + +## Hashing + +`hash_words` creates a message digest from a slice of Words: + +```rust +use miden::hash_words; + +// Hash multiple Words into a Digest +let words = [commitment, nonce_word, extra_data]; +let digest: Word = hash_words(&words).into(); +``` + +Other available hash functions: + +```rust +use miden::{blake3_hash, sha256_hash}; + +// BLAKE3 (32-byte input -> 32-byte output) +let hash: [u8; 32] = blake3_hash(input_bytes); + +// SHA256 (32-byte input -> 32-byte output) +let hash: [u8; 32] = sha256_hash(input_bytes); +``` + +## Related + +- [Authentication](./authentication) — auth component pattern and nonce management +- [Advice Provider](../transactions/advice-provider) — supplying auxiliary data during proof generation diff --git a/versioned_docs/version-0.16/builder/smart-contracts/accounts/custom-types.md b/versioned_docs/version-0.16/builder/smart-contracts/accounts/custom-types.md new file mode 100644 index 00000000..99c5e41c --- /dev/null +++ b/versioned_docs/version-0.16/builder/smart-contracts/accounts/custom-types.md @@ -0,0 +1,117 @@ +--- +title: "Custom Types" +sidebar_position: 3 +description: "Export custom structs and enums for use in public component methods with #[export_type]." +--- + +# Custom Types + +When public [component](./components) methods use custom structs or enums, those types must be annotated with `#[export_type]` so the compiler can include them in the component's public API. Types used only internally (in private methods or local variables) don't need this annotation. + +:::tip +If you forget `#[export_type]` on a public API type, the compiler will emit an error telling you to add it. +::: + +## Exporting structs + +Struct fields must be named and use types supported by component interfaces, such as Rust primitives, SDK types (`Felt`, `Word`, `Asset`, etc.), or other types marked with `#[export_type]`: + +```rust +use miden::{component, component_storage, export_type, Asset, Felt, Word}; + +#[export_type] +pub struct StructA { + pub foo: Word, + pub asset: Asset, +} + +#[export_type] +pub struct StructB { + pub bar: Felt, + pub baz: Felt, +} + +#[component_storage] +struct MyAccountStorage; + +#[component] +trait MyAccount { + #[account_procedure] + fn process(&self, a: StructA) -> StructB; +} + +#[component] +impl MyAccount for MyAccountStorage { + fn process(&self, a: StructA) -> StructB { + StructB { + bar: a.foo[0], + baz: a.foo[1], + } + } +} +``` + +## Exporting enums + +Enums use the same annotation. Enum variants can be unit variants: + +```rust +use miden::export_type; + +#[export_type] +pub enum Status { + Active, + Inactive, +} +``` + +## Nested types + +Exported types can reference other exported types: + +```rust +#[derive(Clone, Copy, Debug)] +#[export_type] +pub struct Inner { + pub value: Felt, +} + +#[derive(Clone, Copy, Debug)] +#[export_type] +pub struct Outer { + pub nested: Inner, +} +``` + +The compiler resolves references regardless of declaration order. + +## Types in submodules + +Custom types can be defined in submodules. Each type still needs `#[export_type]`: + +```rust +pub mod my_types { + use miden::{Felt, export_type}; + + #[export_type] + pub struct StructC { + pub inner1: Felt, + pub inner2: Felt, + } +} +``` + +## Rules summary + +| Rule | Details | +|------|---------| +| When needed | Any custom type in a public method signature on a `#[component]` trait | +| Structs | Named-field and unit structs are supported; tuple structs are not | +| Allowed field types | Supported primitives, SDK types, `Option`, `Result`, or other `#[export_type]` types | +| Enums | Unit variants and single-field tuple variants are supported | +| Modules | Types in submodules work — just apply `#[export_type]` to each | +| Order | Declaration order doesn't matter — forward references are resolved | + +:::info API Reference +Full API docs on docs.rs: [`miden`](https://docs.rs/miden/latest/miden/) (`#[export_type]` macro) +::: diff --git a/versioned_docs/version-0.16/builder/smart-contracts/accounts/introduction.md b/versioned_docs/version-0.16/builder/smart-contracts/accounts/introduction.md new file mode 100644 index 00000000..aa617fd9 --- /dev/null +++ b/versioned_docs/version-0.16/builder/smart-contracts/accounts/introduction.md @@ -0,0 +1,72 @@ +--- +title: "What are Accounts?" +sidebar_position: 0 +description: "Accounts are the primary actors in Miden — they store code, state, and assets, and execute provable state transitions." +--- + +# What are Accounts? + +Accounts are the primary actors in Miden. Every entity on the network — wallets, smart contracts, token faucets — is an account. Unlike traditional blockchains where user wallets and smart contracts are fundamentally different, Miden treats them all as programmable accounts with the same structure. + +Each account is an independent state machine. Most transactions execute locally on a client, while network accounts can instead be executed and proven by a network transaction builder. In both cases, correct execution is verified through a zero-knowledge proof. Accounts never share a global execution environment — they run in isolation, which enables parallel execution and privacy by default. + +## Anatomy of an account + +Every account has an immutable identifier and four state elements: + +| Part | Description | +|------|-------------| +| **ID** | An immutable identifier that uniquely identifies the account | +| **Code** | One or more [components](./components.md) that define the account's behavior — its public API and internal logic | +| **Storage** | Persistent state — up to 255 typed [slots](./storage.md) of `StorageValue` or `StorageMap` | +| **Vault** | The fungible and non-fungible assets the account holds | +| **Nonce** | A counter that increments by one in every state-changing transaction, providing replay protection | + +For public accounts, the network stores the full account state. For private accounts, it stores only a commitment to the account state — computed from the ID, nonce, vault root, storage commitment, and code commitment — while the full state remains offchain and must be maintained by the user (see [account design](../../../reference/protocol/account/index.md)). + +## Components, not contracts + +On Ethereum, a smart contract is a single monolithic unit of code deployed to an address. On Miden, accounts are composed of **components** — reusable modules that each contribute their own storage layout and exported procedures. + +```rust +use miden::{component, component_storage, Asset}; + +#[component_storage] +struct MyWalletStorage; + +#[component] +trait MyWallet { + #[account_procedure] + fn receive_asset(&mut self, asset: Asset); +} + +#[component] +impl MyWallet for MyWalletStorage { + fn receive_asset(&mut self, asset: Asset) { + self.add_asset(asset); + } +} +``` + +An account can have multiple components. For example, a DeFi account might combine a wallet component (for holding assets), an auth component (for signature verification), and custom application logic — all in a single account. Components communicate with each other through [cross-component calls](../cross-component-calls.md) using WIT (WebAssembly Interface Types) bindings. + +## Account types + +Accounts are configured with `AccountType`, which controls state visibility: + +| Type | Description | +|------|-------------| +| `AccountType::Public` | Full state is stored onchain and visible to everyone — suitable for shared protocols like DEXs and faucets | +| `AccountType::Private` | Only a state commitment is stored onchain — the actual data stays with the account owner | + +Wallet, contract, and faucet roles are determined by the account's components and options, not by separate account-type enum variants. For example, a fungible faucet is a public or private account that includes the `FungibleFaucet` component and token policy configuration. + +## How accounts differ from EVM contracts + +| | EVM | Miden | +|---|---|---| +| **Execution** | Every validator re-executes every transaction | A client or network transaction builder executes; the network verifies a ZK proof | +| **State visibility** | All state variables are public onchain | Private accounts expose commitments; public accounts store their full state onchain | +| **Code structure** | Monolithic contract deployed to an address | Multiple reusable components composed into one account | +| **Identity** | Wallets are EOAs, contracts are separate | Everything is an account — wallets are smart contracts | +| **Failure** | `revert` consumes gas, leaves an onchain trace | Invalid execution cannot produce a proof, so no failed transaction is submitted onchain | diff --git a/versioned_docs/version-0.16/builder/smart-contracts/accounts/network-accounts.md b/versioned_docs/version-0.16/builder/smart-contracts/accounts/network-accounts.md new file mode 100644 index 00000000..9bacb796 --- /dev/null +++ b/versioned_docs/version-0.16/builder/smart-contracts/accounts/network-accounts.md @@ -0,0 +1,209 @@ +--- +title: "Network Accounts" +sidebar_position: 7 +description: "What a network account is in Miden, including fee policies, deployment, and network notes from Rust and TypeScript." +--- + +# Network Accounts + +A **network account** is a public account that the network can transact against on the owner's behalf — no client needs to be online. When a note is addressed to a network account, the node's network transaction (NTX) builder executes the consuming transaction and commits the resulting state change. This is how you build always-available onchain contracts: counters, faucets, order books, and other components that must react to incoming notes without a user driving them. + +Two sides have to line up for network execution to happen: + +- **The account** opts in by carrying the standardized note-allowlist storage slot, added through the [`AuthNetworkAccount`](https://docs.rs/miden-standards/latest/miden_standards/account/auth/struct.AuthNetworkAccount.html) auth component. +- **The note** targets the account by carrying a `NetworkAccountTarget` attachment. + +If a note's script root is allowlisted and its fee can be estimated by the account's active fee policy, the network can consume it automatically. + +## What makes an account a network account + +`AuthNetworkAccount` writes a standardized [`StorageMap`](./storage) slot named `miden::standards::auth::network_account::allowed_note_scripts`. Off-chain services and the node's NTX builder treat the presence of that slot as the signal that an account is a network account. The slot holds a **note allowlist**: the set of note script roots the account is willing to consume. A note whose script root is not in the allowlist is rejected during authentication. + +The component also holds a second allowlist of permitted **transaction script roots** (`miden::standards::auth::network_account::allowed_tx_scripts`). `AuthNetworkAccount::new` includes the canonical expiration script required by the network transaction builder. Any additional custom transaction script — for example a scripted deploy or interaction — must be allowlisted explicitly. + +Both allowlists can be updated after deployment through the network-account +configuration note. Those mutations must be protected by an owner- or +RBAC-controlled `Authority`; auth-controlled authority is unsafe because +network authentication is intentionally permissionless for allowlisted inputs. +The account example below installs owner-controlled access for this purpose. + +## Prerequisites + +- The account must be `AccountType::Public`. A private account cannot be a network account. +- You need the **script root of every application note type** the account should accept, computed from the compiled note script. Rust's `AuthNetworkAccount::new` accepts an empty application set because it adds the standard configuration and fee-sponsorship scripts. The Web SDK helper requires at least one application `NoteScriptFee`. +- You need a `FeePolicyManager`, the ID of the fungible faucet used for fees, and an active policy that can price every allowed note script. A zero fee is valid but must still be scheduled explicitly by `BasicConstantFeePolicy`. + +## Building a network account + +Compile each application note script and read its MAST root **before** building the account. The standardized allowlist storage installed by `AuthNetworkAccount` is what marks the account as a network account. Compile with the client's code builder: + +```rust +use std::collections::BTreeSet; + +use miden_client::account::{ + AccountBuilder, AccountType, + component::{ + AccessControl, AuthNetworkAccount, BasicConstantFeePolicy, FeePolicyManager, + }, +}; +use miden_client::asset::AssetAmount; +use miden_client::note::{FeeSponsorshipNote, NetworkAccountConfigNote}; + +let note_script = client.code_builder().compile_note_script(note_code)?; +let note_script_root = note_script.root(); +``` + +If the note script calls into the account's own procedures (as the counter example does), link the contract module first so the script compiles — for example `client.code_builder().with_linked_module("external_contract::counter_contract", counter_code)?.compile_note_script(note_code)?`. + +Build an active fee policy, pass it to `AuthNetworkAccount`, then install every component the auth bundle yields: + +```rust +let fee_policy = BasicConstantFeePolicy::new() + .with_fee(note_script_root, AssetAmount::ZERO) + .with_fee( + NetworkAccountConfigNote::script_root(), + AssetAmount::ZERO, + ) + .with_fee(FeeSponsorshipNote::script_root(), AssetAmount::ZERO); +let fee_policy_manager = FeePolicyManager::builder() + .fee_faucet_id(fee_faucet_id) + .active_fee_policy(fee_policy.into()) + .build(); +let auth = AuthNetworkAccount::new( + BTreeSet::from([note_script_root]), + fee_policy_manager, +)?; + +let account = AccountBuilder::new(init_seed) + .account_type(AccountType::Public) + .with_component(counter_component) + .with_components(auth) + .with_components(AccessControl::Ownable2Step { owner: owner_id }) + .build()?; +``` + +If the initial deployment uses a **custom transaction script**, you must also allowlist that script's root, or the network auth procedure rejects the transaction. After deployment, users interact by sending notes; the public RPC rejects user-submitted transactions that directly update an existing network account. + +In that case, replace the earlier `let auth = ...` construction with this one: + +```rust +let tx_script = client.code_builder().compile_tx_script(deploy_script_code)?; + +let auth = AuthNetworkAccount::new( + BTreeSet::from([note_script_root]), + fee_policy_manager, +)? + .with_allowed_tx_scripts(BTreeSet::from([tx_script.root()])); +``` + +## Deploying a network account + +Building the account and adding it to the client store is **not** enough to register it onchain — an account only exists to the network once a committed transaction has advanced its state (nonce `0` → `1`). Submit a transaction against it to deploy it. + +Because `AuthNetworkAccount` bumps the nonce itself, an **empty, scriptless transaction** can register the account on a **zero-fee development chain**. It needs no additional tx-script allowlist entry. A custom deployment script must be allowlisted as above. + +On testnet, the first transaction must also pay a fee. One way to bootstrap the account is to consume an allowed funding note in that transaction. The account needs a component that can receive its assets, and the funding script must be both allowlisted and priced by the fee policy. Consuming that note already deploys the account; do not submit another empty deployment transaction afterward. The example below is only the zero-fee path. + +```rust +use miden_client::transaction::TransactionRequestBuilder; + +client.add_account(&account, false).await?; + +let tx_id = client + .submit_new_transaction(account.id(), TransactionRequestBuilder::new().build()?) + .await?; + +client.sync_state().await?; // repeat until `tx_id` is committed +``` + +Once the deploy transaction is committed, the network watches the account and will consume any allowlisted note addressed to it. + +:::note Deployment on testnet +The [network transactions tutorial](../../tutorials/recipes/rust/network_transactions_tutorial.md) adds `BasicWallet` and permits a P2ID funding note. Its initial funding consumption publishes the network account with count zero. Subsequent increments come from notes consumed by the network transaction builder. +::: + +## Inspecting a network account + +`NetworkAccount` is a validation wrapper that confirms an `Account` is public, carries a valid non-empty note allowlist, and allows the canonical expiration transaction script. Use it to check an account you fetched or built: + +```rust +use miden_client::account::component::NetworkAccount; + +let network_account = NetworkAccount::try_from(account)?; +let allowed = network_account.allowed_notes(); +``` + +## Sending a note to a network account + +A note is executed by the network when it carries a `NetworkAccountTarget` attachment and its script root is in the target account's allowlist. Both Rust and TypeScript can create these notes — this is the part of the flow available to web integrators. + +### TypeScript + +The Web SDK builds and submits a network note in one call. It creates a public, custom-script note carrying the required `NetworkAccountTarget` attachment, so the target network account auto-consumes it: + +```typescript +const { txId, note } = await client.transactions.createNetworkNote({ + account: senderAccountId, // account that creates, funds, and submits the note + target: networkAccountId, // the network account the note targets + script: counterNoteScript, // custom consumption script (or pass a `recipient`) + inputs: [/* note inputs the script reads */], + assets: [/* optional assets locked into the note */], +}); + +// `note.isNetworkNote()` is true; the network account will consume it. +``` + +Use `buildNetworkNote(...)` if you want the built note without submitting it. + +The Web SDK can also build and deploy the account. Pair every application script with its fee, pass the fee-faucet ID, and install **all** returned components. This example uses an empty deployment transaction on a **zero-fee development chain**. On testnet, replace that empty transaction with an initial funding consumption as described above. + +```typescript +import { + AccountBuilder, + AccountComponent, + AccountStorageMode, + NoteScriptFee, + TransactionRequestBuilder, +} from "@miden-sdk/miden-sdk"; + +const networkAuth = AccountComponent.createNetworkAuthComponents( + [new NoteScriptFee(counterNoteScript.root(), 0n)], + feeFaucet.id(), +); + +const builder = new AccountBuilder(seed) + .storageMode(AccountStorageMode.public()) + .withComponent(counterComponent); + +for (const component of networkAuth) { + builder.withComponent(component); +} + +const { account } = builder.build(); +await client.accounts.insert({ account }); +await client.transactions.submit( + account.id(), + new TransactionRequestBuilder().build(), +); +``` + +`createNetworkAuthComponents` returns the auth component plus the components backing its fee policy. Omitting any of them creates an incomplete account. It also includes the standard configuration and fee-sponsorship note scripts in the allowlist. To use configuration notes to update the account, additionally install owner- or RBAC-controlled access components, as in the Rust example. + +## Surface support + +| Flow | Rust | TypeScript | +|---|---|---| +| Create + deploy a network account | ✅ `AuthNetworkAccount` + fee policy | ✅ `createNetworkAuthComponents` + deployment transaction | +| Send a network note to one | ✅ | ✅ `createNetworkNote` / `buildNetworkNote` | +| Inspect (`NetworkAccount`) | ✅ | ✅ `isNetworkAccount()` / `networkNoteAllowlist()` | + +:::info API Reference +Rust: [`AuthNetworkAccount`](https://docs.rs/miden-standards/latest/miden_standards/account/auth/struct.AuthNetworkAccount.html), [`NetworkAccount`](https://docs.rs/miden-standards/latest/miden_standards/account/auth/struct.NetworkAccount.html), [`NetworkAccountNoteAllowlist`](https://docs.rs/miden-standards/latest/miden_standards/account/auth/struct.NetworkAccountNoteAllowlist.html) +::: + +## Related + +- [Network transactions tutorial](../../tutorials/recipes/rust/network_transactions_tutorial.md) — end-to-end Rust walkthrough: build, deploy, and drive a network counter contract +- [Authentication](./authentication) — the auth component pattern `AuthNetworkAccount` builds on +- [Storage](./storage) — how the allowlist `StorageMap` slot is laid out +- [Account components](../standards/account-components) — composing wallet, faucet, and access-control components diff --git a/versioned_docs/version-0.16/builder/smart-contracts/accounts/storage.md b/versioned_docs/version-0.16/builder/smart-contracts/accounts/storage.md new file mode 100644 index 00000000..629b8f21 --- /dev/null +++ b/versioned_docs/version-0.16/builder/smart-contracts/accounts/storage.md @@ -0,0 +1,185 @@ +--- +title: "Storage" +sidebar_position: 2 +description: "Persistent state management with StorageValue slots and StorageMaps in Miden smart contracts." +--- + +# Storage + +Miden accounts have persistent storage organized into up to 255 name-addressable slots. Each slot holds either a single typed value (via `StorageValue`) or a key-value map (via `StorageMap`). Slots are identified by `StorageSlotId` values derived from the component package name, component interface, and field name. Renaming any of these changes the slot ID and is a breaking change for stored data. + +## Storage slots + +An account can have up to **255 storage slots**. Each slot is declared as a field on your component struct: + +```rust +use miden::{component_storage, StorageMap, StorageValue, Word}; + +#[component_storage] +struct MyContractStorage { + #[storage(description = "configuration")] + config: StorageValue, + + #[storage(description = "user balances")] + balances: StorageMap, +} +``` +Field ordering does not matter, and `slot(N)` is not supported. + +## StorageValue — Single-slot storage + +A `StorageValue` stores a single typed value at a fixed slot. It provides `get()` and `set()` methods. Note that `set()` returns the **previous** slot value, not the value written. + +### Reading + +```rust +pub fn get_config(&self) -> Word { + self.config.get() +} + +pub fn is_initialized(&self) -> bool { + let state: Word = self.initialized.get(); + state[0] == felt!(1) +} +``` + +### Writing + +`set()` returns the **previous value**: + +```rust +pub fn initialize(&mut self) { + let old: Word = self.initialized.set( + Word::from([felt!(1), felt!(0), felt!(0), felt!(0)]) + ); + // old contains the previous value of the slot +} +``` + +### Packing multiple values + +Since each slot holds a `Word` (4 Felts), pack related values together: + +```rust +// Store limits as [max_per_tx, daily_max, 0, 0] +pub fn set_limits(&mut self, max_per_tx: u64, daily_max: u64) { + self.limits.set(Word::from([ + Felt::new(max_per_tx).unwrap(), + Felt::new(daily_max).unwrap(), + felt!(0), + felt!(0), + ])); +} + +// Read individual fields +pub fn get_max_per_tx(&self) -> u64 { + let limits: Word = self.limits.get(); + limits[0].as_canonical_u64() +} +``` + +## StorageMap — Key-value storage + +A `StorageMap` stores typed key-value pairs where keys and values can be converted to and from `Word`. It provides `get()` and `set()` methods. + +### Reading from a map + +```rust +// With StorageMap, get returns a Felt. +pub fn get_balance(&self, account_id: AccountId) -> Felt { + self.balances.get(account_id) +} +``` + +Scalar `Felt` map values are encoded in the low word limb (`[value, 0, 0, 0]`). This is handled by the typed `StorageMap` conversion. For a full `Word` value, declare the map value type as `Word`: + +```rust +// Get the full Word value +let full_value: Word = self.my_map.get(key); +``` + +### Writing to a map + +`set()` returns the **previous value**: + +```rust +pub fn set_balance(&mut self, account_id: AccountId, amount: Felt) { + let old: Felt = self.balances.set(account_id, amount); + // old contains the previous balance +} +``` + +### Map with Word values + +```rust +pub fn store_data(&mut self, key: Word, value: Word) { + let old: Word = self.data_map.set(key, value); +} + +pub fn read_data(&self, key: Word) -> Word { + self.data_map.get(key) +} +``` + +## Storage layout conventions + +Each slot and each Felt within a Word can be documented with inline comments on the struct: + +```rust +use miden::{component_storage, StorageMap, StorageValue, Word}; + +#[component_storage] +struct TokenVaultStorage { + /// Config slot: + /// [0] = max_supply (u64) + /// [1] = is_paused (0 or 1) + /// [2] = decimals + /// [3] = unused + #[storage(description = "vault configuration")] + config: StorageValue, + + /// State slot: + /// [0] = total_supply (u64) + /// [1] = total_holders (u64) + /// [2] = last_mint_block + /// [3] = unused + #[storage(description = "vault state")] + state: StorageValue, + + /// Balance map slot: + /// Key: [account_prefix, account_suffix, 0, 0] + /// Value: [balance, last_activity_block, 0, 0] + #[storage(description = "user balances")] + balances: StorageMap, +} +``` + +## Low-level storage access + +Direct storage access outside the component traits uses the bindings. Use the +ID of a value slot for `value_slot_id` and the ID of a map slot for +`map_slot_id`; map operations reject value slots: + +```rust +use miden::storage; + +// Direct slot access +let value: Word = storage::get_item(value_slot_id); +let old: Word = storage::set_item(value_slot_id, new_value); + +// Direct map access +let value: Word = storage::get_map_item(map_slot_id, &key); +let old: Word = storage::set_map_item(map_slot_id, key, value); + +// Initial values (at transaction start) +let initial: Word = storage::get_initial_item(value_slot_id); +let initial: Word = storage::get_initial_map_item(map_slot_id, &key); +``` + +`get_item` and `get_map_item` read the current values. `get_initial_item` and `get_initial_map_item` read the values from the start of the transaction, while `set_item` and `set_map_item` return the values immediately before the write. + +For Felt and Word conversion details, see [Types](../types). To export your own types for public APIs, see [Custom Types](./custom-types). For common storage patterns like access control and rate limiting, see [Patterns](../patterns). + +:::info API Reference +Full API docs on docs.rs: [`miden::storage`](https://docs.rs/miden/latest/miden/storage/) +::: diff --git a/versioned_docs/version-0.16/builder/smart-contracts/cross-component-calls.md b/versioned_docs/version-0.16/builder/smart-contracts/cross-component-calls.md new file mode 100644 index 00000000..531b6ece --- /dev/null +++ b/versioned_docs/version-0.16/builder/smart-contracts/cross-component-calls.md @@ -0,0 +1,156 @@ +--- +title: "Cross-Component Calls" +sidebar_position: 5.5 +description: "Call methods across account components and from note scripts." +--- + +# Cross-Component Calls + +Miden [components](./accounts/components) can call each other's methods. Since accounts can have multiple components (e.g., wallet + auth + custom logic), those components need to communicate. [Note scripts](./notes/note-scripts) can also call methods on the account's components. + +## How it works + +When you build a component with `miden build`, the compiler writes its compiled +package and generates WIT describing the methods marked with +`#[account_procedure]`. Other projects use the package plus that WIT to call +those methods. + +``` +counter-account (package) + → exports counter-contract (component interface) + → counter-note imports the interface + → calls account.get_count() +``` + +## Using `#[note]` macros (recommended) + +The simplest way to make cross-component calls from note scripts is through the `#[note]` macro with an `Account` parameter: + +```rust +use miden::{account, note, Word}; + +#[account(counter_account::CounterContract)] +pub struct CounterAccount; + +#[note] +struct CounterNote; + +#[note] +impl CounterNote { + #[note_script] + pub fn run(self, _arg: Word, account: &mut CounterAccount) { + account.increment_count(); + } +} +``` + +The `_arg: Word` parameter contains the note argument (`NOTE_ARGS`) supplied by the transaction when the note is consumed. It's unused in this example (prefixed with `_`), but note scripts can use it for transaction-specific data like expected account IDs or amounts. + +The `#[account(...)]` wrapper declares which package interface the script will call. Its methods correspond to the referenced component's account procedures. + +## Calling foreign accounts + +The same `#[account(...)]` wrapper can call a different account through foreign procedure invocation (FPI). Construct it with the target `AccountId`, then call the imported methods: + +```rust +use miden::{account, AccountId, Felt}; + +#[account(counter_account::CounterContract)] +struct CounterAccount; + +fn read_foreign_count(counter_account_id: AccountId) -> Felt { + let counter = CounterAccount::new(counter_account_id); + counter.get_count() +} +``` + +Key points: +- The `#[account(package::Interface)]` path names the interface exported by the dependency package and described by its generated WIT, not just the package. +- An account parameter in a note or transaction script refers to the transaction's native account. +- `AccountWrapper::new(account_id)` creates a foreign account caller routed through FPI. + +## Project manifest configuration + +Cross-component calls require dependency declarations in `miden-project.toml`. + +### 1. Package dependency + +```toml +[dependencies] +miden-core = "*" +miden-protocol = "*" +basic-wallet = { path = "../basic-wallet" } +``` + +This tells the compiler where to find the component package. + +### 2. Generated WIT dependency + +The compiled dependency package embeds its generated WIT interface. The SDK reads that interface to create Rust bindings for `#[account(...)]`. + +Do not also set `package.metadata.miden.dependencies..wit` for a package that embeds WIT: the SDK rejects the duplicate interface source. + +### Complete example + +```toml title="counter-note/miden-project.toml" +[package] +name = "counter-note" +version = "0.1.0" + +[lib] +kind = "note" +namespace = "miden:counter-note/miden-counter-note@0.1.0" +path = "src/lib.rs" + +[dependencies] +miden-core = "*" +miden-protocol = "*" +counter-account = { path = "../counter-account" } + +``` + +The consumer's build resolves the `counter-account` path dependency and uses the interface embedded in its compiled package. No separate `target/generated-wit/` path is required. + +## Example: Counter note calling counter contract + +```rust title="counter-note/src/lib.rs" +#![no_std] +#![feature(alloc_error_handler)] + +use miden::{account, note, Felt, Word}; + +#[account(counter_account::CounterContract)] +pub struct CounterAccount; + +#[note] +struct CounterNote; + +#[note] +impl CounterNote { + #[note_script] + pub fn run(self, _arg: Word, account: &mut CounterAccount) { + // Call get_count() on the counter contract component + let initial_value = account.get_count(); + + // Call increment_count() — modifies the account's storage + account.increment_count(); + + // Verify the count increased + let expected_value = initial_value + Felt::from_u32(1); + let final_value = account.get_count(); + assert_eq!(final_value, expected_value); + } +} +``` + +## When to use which pattern + +| Pattern | Use when | +|---------|----------| +| `#[note]` with `&mut AccountWrapper` | Note needs to call the native account's component methods | +| `AccountWrapper::new(account_id)` | Component, note, or transaction script needs to call a foreign account through FPI | +| Multiple `#[account(...)]` wrappers | A script needs to call multiple known component interfaces | + +:::info API Reference +Full API docs on docs.rs: [`miden`](https://docs.rs/miden/latest/miden/) (`#[account]`, `#[note]`, and `#[component]` macros) +::: diff --git a/versioned_docs/version-0.16/builder/smart-contracts/index.md b/versioned_docs/version-0.16/builder/smart-contracts/index.md new file mode 100644 index 00000000..3ae40897 --- /dev/null +++ b/versioned_docs/version-0.16/builder/smart-contracts/index.md @@ -0,0 +1,63 @@ +--- +title: "Miden Smart Contracts" +description: "Build Miden smart contracts in Miden Assembly (MASM) for mainnet production, with the Rust SDK in active development as the long-term direction." +pagination_prev: null +--- + +# Miden Smart Contracts + +This section covers the developer-facing paths for building smart contracts on Miden: an authoring guide for **Miden Assembly (MASM)** (the supported path for mainnet production today) and **Rust** (in active development as the long-term direction), plus the [Miden Standards](./standards/) library of reusable components callable from either. + +:::tip Building for mainnet? +Miden mainnet supports smart contracts authored in **Miden Assembly (MASM)** today. The Rust SDK is in active development and will become the default authoring path once it ships v1. For production deployments now, see [MASM Smart Contracts](./masm/). +::: + +If you're new to Miden, the hands-on [Miden Bank Tutorial](../tutorials/miden-bank/) walks through the full lifecycle using the Rust SDK; the concepts (accounts, notes, transactions, components) translate directly to MASM. + +## Sections + + + + How accounts, notes, transactions, and components fit together. Concepts apply regardless of authoring language. + + + Author production-ready smart contracts directly in Miden Assembly. The path Miden mainnet supports today. + + + Build accounts, notes, transactions, and reusable logic with the Rust-first workflow. Currently in active development and not yet production-ready for mainnet. + + + Standard components, note scripts, faucet policies, and MASM modules. Callable from MASM or Rust. + + + +## Inside the Rust SDK + + + + Components, storage, custom types, operations, cryptography, and authentication. + + + Programmable UTXOs for asset transfers. + + + Transaction context, scripts, and the advice provider. + + + Calling methods across account components and from note scripts. + + + Core types: Felt, Word, AccountId, NoteId, and more. + + + Access control, rate limiting, spending limits, and anti-patterns. + + + +## Reference + + + + Complete API documentation for the miden crate. + + diff --git a/versioned_docs/version-0.16/builder/smart-contracts/masm/index.md b/versioned_docs/version-0.16/builder/smart-contracts/masm/index.md new file mode 100644 index 00000000..26118174 --- /dev/null +++ b/versioned_docs/version-0.16/builder/smart-contracts/masm/index.md @@ -0,0 +1,51 @@ +--- +title: "MASM Smart Contracts" +description: "Author Miden smart contracts directly in Miden Assembly (MASM) — the supported path for mainnet production." +sidebar_position: 0 +--- + +# MASM Smart Contracts + +This section is the practical guide to authoring Miden smart contracts directly in **Miden Assembly (MASM)** — the path Miden mainnet supports for production deployments today. The Rust SDK is in active development and will become the default authoring path once it ships v1; until then, MASM is what you ship with. + +:::info Audience +You're here because you want to deploy a contract to Miden mainnet. MASM is a small, stack-based assembly language — closer to assembly than Rust or Solidity, but it gives you direct, predictable control over the VM and is what the mainnet kernel verifies. The [Reference → Miden VM → Assembly](../../../reference/miden-vm/user_docs/assembly/index.md) section is the full language reference; this section is the Builder-side cookbook for using it. +::: + +## When to use MASM vs. the Rust SDK + +| Concern | MASM | Rust SDK | +|---|---|---| +| **Mainnet production** | Supported today | In active development | +| **Performance / cycle control** | Direct control over emitted instructions | Compiles via Wasm → MASM; less predictable | +| **Learning curve** | Small instruction set, explicit stack semantics | Familiar Rust ergonomics | +| **Miden Standards** | [Standards modules](../standards/) callable directly | Same standards via Rust bindings | +| **Long-term direction** | Long-lived for system-level / performance-critical code | Will become the default authoring path once mature | + +For mainnet shipping today, the choice is straightforward: write MASM. Most production patterns — account components, note scripts, transaction scripts, P2ID and P2IDE flows, faucet policies — are already covered by [Miden Standards](../standards/), so most contracts compose existing standards rather than rolling everything from scratch. + +## Where the language reference lives + +The full Miden Assembly reference — every instruction, the stack semantics, control flow, cryptographic operations, debugging instructions — lives in [Reference → Miden VM → Assembly](../../../reference/miden-vm/user_docs/assembly/index.md). Treat that as the dictionary. This Builder section is the cookbook: how to structure a project, conventions for accounts, notes, and transactions, deployment, testing, and debugging in the context of building a Miden app. + +## Tutorials + +Practical, end-to-end tutorials are in progress in the [Miden tutorials repository](https://github.com/0xMiden/tutorials) and will land in this section as they ship: + +| Tutorial | Status | +|---|---| +| Project setup and first MASM contract | In progress | +| MASM account components | In progress | +| MASM note scripts | In progress | +| MASM transaction scripts | In progress | +| Testing with MockChain | In progress | +| Debugging MASM contracts | In progress | + +In the meantime, the existing [Rust-based Miden Bank tutorial](../../tutorials/miden-bank/) is useful for understanding the overall transaction lifecycle and the patterns that translate to MASM. The [Reference assembly section](../../../reference/miden-vm/user_docs/assembly/index.md) covers the language itself. + +## See also + +- [Miden Standards](../standards/) — reusable account components, standard notes, faucet policies, callable from MASM. +- [Reference → Miden VM → Assembly](../../../reference/miden-vm/user_docs/assembly/index.md) — full language reference (instructions, stack semantics, control flow). +- [Smart Contracts → Overview](../overview) — execution model and lifecycle (the concepts apply regardless of authoring language). +- [Tools → Playground](../../tools/playground) — interactive browser-based MASM environment for quick experiments. diff --git a/versioned_docs/version-0.16/builder/smart-contracts/notes/_category_.json b/versioned_docs/version-0.16/builder/smart-contracts/notes/_category_.json new file mode 100644 index 00000000..aa233038 --- /dev/null +++ b/versioned_docs/version-0.16/builder/smart-contracts/notes/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "Notes", + "position": 4.5, + "link": { + "type": "generated-index", + "slug": "/builder/smart-contracts/notes" + } +} diff --git a/versioned_docs/version-0.16/builder/smart-contracts/notes/introduction.md b/versioned_docs/version-0.16/builder/smart-contracts/notes/introduction.md new file mode 100644 index 00000000..8bd339ce --- /dev/null +++ b/versioned_docs/version-0.16/builder/smart-contracts/notes/introduction.md @@ -0,0 +1,74 @@ +--- +title: "What are Notes?" +sidebar_position: 0 +description: "Miden's cross-account communication mechanism — programmable UTXOs that carry assets, execute scripts, and trigger logic on consuming accounts." +--- + +# What are Notes? + +Notes are Miden's primary mechanism for cross-account communication — they carry assets, execute programmable logic, and trigger state changes on the consuming account. Like UTXOs, notes are created and consumed atomically. Unlike Bitcoin's UTXOs, each Miden note carries an arbitrary executable script that runs when the note is consumed, enabling programmable conditions far beyond simple locking scripts. + +While asset transfers are the most common use of notes, notes are how accounts communicate with one another in general: a note can trigger a counter increment, initiate a swap, delegate an operation, or carry arbitrary data to be acted on by the recipient's logic. + +Assets never transfer directly between accounts. Instead, they always move through notes. With private notes, creation and consumption can be unlinkable to observers because the full note details are not published. Public notes expose those details. + +## Anatomy of a note + +Every note has four main parts: + +| Part | Description | +|------|-------------| +| **Assets** | The fungible or non-fungible tokens the note carries. | +| **Recipient** | The serial number, script, and storage that define the conditions under which the note can be consumed. | +| **Metadata** | Sender ID, note type, note tag, and attachment headers and commitment. Metadata is always public. | +| **Attachments** | Optional public auxiliary data associated with the note. | + +The **recipient** is not necessarily an account address. It is a Poseidon2 commitment to the note's serial number, script, and storage, which together define the conditions under which the note can be consumed: + +``` +recipient = hash(hash(hash(serial_num, [0;4]), script_root), storage_commitment) +``` + +To consume the note, a transaction must provide the values that open this commitment and execute the script successfully. See [Computing a Recipient](./output-notes#computing-a-recipient) for the protocol helpers. + +## The two-transaction model + +Unlike Ethereum where a transfer is a single atomic call, Miden transfers happen across two separate transactions: + +``` +Transaction 1 (Sender) Transaction 2 (Consumer) +┌─────────────────────────┐ ┌─────────────────────────┐ +│ 1. Create note │ │ 1. Discover note │ +│ 2. Attach assets │ │ 2. Consume note │ +│ 3. Note published │──────────▶│ 3. Script runs │ +│ (details/commitment) │ │ 4. Assets move to vault │ +│ │ │ 5. Note nullified │ +└─────────────────────────┘ └─────────────────────────┘ +``` + +**Transaction 1**: The sender's account creates an output note, attaches assets to it, and publishes the note's public representation. + +**Transaction 2**: A consuming account discovers the note and consumes it in its own transaction. The note script runs, its conditions must succeed, and any assets handled by the script can be added to the consumer's vault. A **nullifier** is recorded to prevent the same note from being consumed again (see [note design](../../../reference/protocol/note.md)). + +This separation lets notes be processed independently and allows private-note creation and consumption to remain unlinkable to observers who do not know the note details. + +## Public vs. private notes + +Notes come in two visibility modes: + +| Mode | Description | +|------|-------------| +| **Public** | Metadata, attachments, and full note details (assets, serial number, script, and storage) are published. Anyone can discover and attempt to consume the note. | +| **Private** | Metadata and attachments remain public, but only a commitment to the note details is published. The consumer must obtain the full details separately, for example through a private channel or an encrypted public attachment. | + +Miden provides built-in note patterns (P2ID, P2IDE, SWAP) for common transfer scenarios — see [Standard Note Types](./note-types). You can also write fully custom note scripts for arbitrary consumption logic. + +## How notes differ from EVM transfers + +| | EVM | Miden | +|---|---|---| +| **Transfer model** | Single `transfer()` call on a token contract | Two transactions: create note, then consume note | +| **Privacy** | Sender, recipient, and amount are public | Private notes can hide their details and unlink creation from consumption; metadata and attachments remain public | +| **Programmability** | Token contracts control transfer logic | Each note carries its own script with custom conditions | +| **Failure** | Revert onchain, gas consumed | Proof can't be generated — no onchain trace | +| **Parallelism** | Transfers contend for contract state | Notes can be created and consumed independently | diff --git a/versioned_docs/version-0.16/builder/smart-contracts/notes/note-scripts.md b/versioned_docs/version-0.16/builder/smart-contracts/notes/note-scripts.md new file mode 100644 index 00000000..4fa0eb3d --- /dev/null +++ b/versioned_docs/version-0.16/builder/smart-contracts/notes/note-scripts.md @@ -0,0 +1,164 @@ +--- +title: "Note Scripts" +sidebar_position: 1 +description: "Write note scripts using #[note] and #[note_script] macros to define logic that executes when notes are consumed." +--- + +# Note Scripts + +Note scripts define the logic that executes when a note is consumed. They determine **who** can consume a note and **what happens** to its assets. + +## The `#[note]` pattern + +A note script consists of a struct (holding note storage fields) and an impl block with a `#[note_script]` method: + +```rust +use miden::{account, note, AccountId, Word}; + +#[account(basic_wallet::BasicWallet)] +pub struct Wallet; + +#[note] +struct MyNote { + target_account_id: AccountId, +} + +#[note] +impl MyNote { + #[note_script] + pub fn run(self, _arg: Word, account: &mut Wallet) { + // Script logic here + } +} +``` + +The `#[note]` macro: + +1. Deserializes note storage into the struct fields. +2. Exports the method marked with `#[note_script]` as the note entry point. + +## Struct fields as note storage + +Fields on the `#[note]` struct are populated from the note's storage data when the note is consumed: + +```rust +#[note] +struct MyNote { + target_account_id: AccountId, // Deserialized from note storage +} +``` + +The compiler maps struct fields to note storage values based on their order and type. Supported field types include `AccountId`, `Felt`, `Word`, and other SDK types. + +If the note has no storage fields, use a unit struct: + +```rust +#[note] +struct CounterNote; +``` + +## `#[note_script]` method requirements + +The `#[note_script]` method has specific signature constraints: + +| Constraint | Details | +|------------|---------| +| Receiver | `self` (by value only — not `&self` or `&mut self`) | +| Return type | `()` | +| Required arg | One `Word` argument (the note script argument) | +| Account arg | `&AccountWrapper` or `&mut AccountWrapper`, where `AccountWrapper` is declared with `#[account(package::Interface)]` | + +### Parameter ordering + +The `Word` and `&mut AccountWrapper` parameters can appear in either order: + +```rust +// Both are valid: +pub fn run(self, _arg: Word, account: &mut Wallet) { ... } +pub fn run(self, account: &mut Wallet, _arg: Word) { ... } +``` + +### With account access + +When you include `&mut Wallet` (or `&Wallet`), the note script can call methods on the account's components: + +```rust +#[account(counter_account::CounterContract)] +pub struct CounterAccount; + +#[note_script] +pub fn run(self, _arg: Word, account: &mut CounterAccount) { + account.increment_count(); +} +``` + +Declare the account wrapper with `#[account(package::Interface)]` and configure its package and generated-WIT dependencies in `miden-project.toml` — see [Cross-Component Calls](../cross-component-calls). + +### Without account access + +Use this pattern when the script does not call account-component methods. Add `&AccountWrapper` for read-only calls or `&mut AccountWrapper` for state-changing calls. For asset-moving scripts, see [Reading Notes](./reading-notes#assets). + +```rust +#[note_script] +pub fn run(self, _arg: Word) { + // Logic that does not require account-component methods. +} +``` + +## Example: Counter note (cross-component calls) + +A note that calls methods on the account's component: + +:::note +All note script crates require `#![no_std]` and `#![feature(alloc_error_handler)]` at the crate root. These are omitted from examples for brevity. +::: + +```rust title="counter-note/src/lib.rs" +use miden::{account, note, Felt, Word}; + +#[account(counter_account::CounterContract)] +pub struct CounterAccount; + +#[note] +struct CounterNote; + +#[note] +impl CounterNote { + #[note_script] + pub fn run(self, _arg: Word, account: &mut CounterAccount) { + 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); + } +} +``` + +This note takes the active account as `&mut CounterAccount` and calls the counter component through its generated interface. See [Cross-Component Calls](../cross-component-calls). + +## miden-project.toml for note scripts + +Note script crates require a `miden-project.toml` and must declare dependencies on any account components they interact with: + +```toml title="miden-project.toml" +[package] +name = "counter-note" +version = "0.1.0" + +[lib] +kind = "note" +namespace = "miden:counter-note/miden-counter-note@0.1.0" +path = "src/lib.rs" + +[dependencies] +miden-core = "*" +miden-protocol = "*" +counter-account = { path = "../counter-account" } + +``` + +## Related + +- [Cross-Component Calls](../cross-component-calls) — how `#[account(...)]` wrappers and generated interfaces work +- [Transaction Context](../transactions/transaction-context) — transaction scripts with `#[tx_script]` diff --git a/versioned_docs/version-0.16/builder/smart-contracts/notes/note-types.md b/versioned_docs/version-0.16/builder/smart-contracts/notes/note-types.md new file mode 100644 index 00000000..f19a98ff --- /dev/null +++ b/versioned_docs/version-0.16/builder/smart-contracts/notes/note-types.md @@ -0,0 +1,174 @@ +--- +title: "Standard Note Types" +sidebar_position: 3 +description: "Built-in note types from miden-standards: P2ID, P2IDE (with expiration), and SWAP (atomic exchange)." +--- + +# Standard Note Types + +The `miden-standards` crate provides built-in note patterns for common asset transfer scenarios. These are pre-compiled note scripts you can use directly via the builder API in client code. + +## P2ID (Pay to ID) + +The most common pattern — a note that can only be consumed by a specific account. The note script checks that the consuming account's ID matches the target, then transfers all assets. + +### When to use + +Use P2ID for standard asset transfers where only the intended recipient should be able to consume the note. This is the most common note type. + +:::info +P2ID notes use the typed `P2idNote` builder from `miden_standards::note`. The script is pre-compiled MASM; build the typed note and convert it into a protocol `Note` with `.into()`. +::: + +### How it works + +1. Creator creates a P2ID note containing the assets and the target account ID as a note storage item +2. Consumer's transaction processes the note — the script verifies the consuming account's ID matches the target +3. If the IDs match, all assets transfer to the consuming account; otherwise proof generation fails + +### Note storage + +| Item | Type | Description | +|------|------|-------------| +| `target_account_id` | `AccountId` | The account allowed to consume this note | + +### Builder API + +```rust +use miden_protocol::note::Note; +use miden_standards::note::P2idNote; + +let note: Note = P2idNote::builder() + .sender(sender) + .target(target) + .assets(assets) + .note_type(note_type) + .generate_serial_number(rng) + .build()? + .into(); +``` + +| Parameter | Type | Description | +|-----------|------|-------------| +| `sender` | `AccountId` | Account sending the note | +| `target` | `AccountId` | The only account that can consume this note | +| `assets` | `Vec` | Assets to attach to the note | +| `note_type` | `NoteType` | `Public` or `Private` | +| `attachment` / `attachments` | `NoteAttachment` / iterator | Optional auxiliary data | +| `generate_serial_number` | `&mut impl FeltRng` | Generates the required serial number | + +## P2IDE (Pay to ID with Expiration) + +P2IDE extends P2ID with optional timelock and reclaim conditions. A configured timelock prevents any account from consuming the note before the specified height. If reclaim is enabled, the configured reclaimer can also consume the note once `reclaim_height` has been reached and any configured timelock has expired; the target remains authorized. + +### When to use + +Use P2IDE when the sender wants the option to reclaim assets if the recipient doesn't consume the note within a time window. + +:::info +P2IDE notes use the typed `P2ideNote` builder from `miden_standards::note`. Its reclaimer and block-height constraints are optional builder fields. +::: + +### How it works + +1. The sender creates a P2IDE note with the target account ID and optional timelock, reclaim height, and reclaimer +2. If a timelock is configured, no account can consume the note before it expires +3. The target can consume the note once the timelock condition is satisfied +4. If reclaim is enabled, the reclaimer can also consume the note once `reclaim_height` has been reached and any configured timelock has expired; the sender is the default reclaimer +5. All other consumption attempts fail (proof generation fails) + +### Note storage + +| Item | Type | Description | +|------|------|-------------| +| `reclaimer` | `AccountId` | Account allowed to reclaim; defaults to the sender | +| `target` | `AccountId` | Account allowed to receive the note | +| `reclaim_height` | `Option` | Block height after which the reclaimer can consume the note, subject to the timelock | +| `timelock_height` | `Option` | Block height before which no account can consume the note | + +### Builder API + +```rust +use miden_protocol::note::Note; +use miden_standards::note::P2ideNote; + +let note: Note = P2ideNote::builder() + .sender(sender) + .target(target) + .reclaimer(reclaimer) + .reclaim_height(reclaim_height) + .timelock_height(timelock_height) + .assets(assets) + .note_type(note_type) + .generate_serial_number(rng) + .build()? + .into(); +``` + +| Parameter | Type | Description | +|-----------|------|-------------| +| `sender` | `AccountId` | Account sending the note | +| `target` | `AccountId` | The account that can receive the note | +| `reclaimer` | `AccountId` | Optional reclaiming account; defaults to `sender` | +| `reclaim_height` | `BlockNumber` | Optional block height after which the reclaimer can consume the note, subject to the timelock | +| `timelock_height` | `BlockNumber` | Optional block height before which no account can consume the note | +| `assets` | `Vec` | Assets to attach to the note | +| `note_type` | `NoteType` | `Public` or `Private` | +| `attachment` / `attachments` | `NoteAttachment` / iterator | Optional auxiliary data | +| `generate_serial_number` | `&mut impl FeltRng` | Generates the required serial number | + +## SWAP (Atomic Exchange) + +SWAP enables atomic asset exchange. The creator offers one asset; any consumer who provides the requested asset in return can consume the note. The swap is atomic — both sides happen in a single transaction or neither does. + +### When to use + +Use SWAP for trustless atomic exchanges where two parties trade assets without intermediaries. + +:::info +SWAP notes use the typed `SwapNote` builder from `miden_standards::note`. Read the expected payback details from the typed value before converting it into a protocol `Note`. +::: + +### How it works + +1. The creator creates a SWAP note containing the offered asset and storage describing the requested asset and payback configuration +2. The consumer's transaction moves the requested asset from their vault into a P2ID payback note targeted at the creator +3. The transaction moves the offered asset from the SWAP note into the consumer's vault +4. The payback note creation and offered asset transfer happen atomically in the same transaction + +### Builder API + +```rust +use miden_protocol::note::Note; +use miden_standards::note::SwapNote; + +let swap = SwapNote::builder() + .sender(sender) + .offered_asset(offered_asset) + .requested_asset(requested_asset) + .note_type(swap_note_type) + .payback_note_type(payback_note_type) + .generate_serial_number(rng) + .build()?; + +let payback_note_details = swap.payback_note_details(); +let note: Note = swap.into(); +``` + +| Parameter | Type | Description | +|-----------|------|-------------| +| `sender` | `AccountId` | Account that receives the payback P2ID note | +| `offered_asset` | `Asset` | Asset the note carries (what the consumer receives) | +| `requested_asset` | `Asset` | Asset the consumer must provide in return | +| `swap_note_type` | `NoteType` | `Public` or `Private` for the SWAP note | +| `attachment` / `attachments` | `NoteAttachment` / iterator | Optional auxiliary data for the SWAP note | +| `payback_note_type` | `NoteType` | `Public` or `Private` for the P2ID payback note | +| `generate_serial_number` | `&mut impl FeltRng` | Generates the required serial number | + +The builder returns a typed `SwapNote`. Call `payback_note_details()` before converting it into the `Note` to submit. + +Attachments are optional. Use `.attachment(value)` or `.attachments(values)` only when needed; see [note attachments](./output-notes#note-attachments) for the underlying SDK API. + +## More note types + +For PSWAP, MINT, BURN, and other standard notes, see [Standard Notes](../standards/standard-notes). For writing custom note scripts, see [Note Scripts](./note-scripts). For the transaction context and `#[tx_script]`, see [Transaction Context](../transactions/transaction-context). diff --git a/versioned_docs/version-0.16/builder/smart-contracts/notes/output-notes.md b/versioned_docs/version-0.16/builder/smart-contracts/notes/output-notes.md new file mode 100644 index 00000000..b81771b1 --- /dev/null +++ b/versioned_docs/version-0.16/builder/smart-contracts/notes/output-notes.md @@ -0,0 +1,122 @@ +--- +title: "Output Notes" +sidebar_position: 4 +description: "Create output notes, attach assets, add attachments, and compute recipients." +--- + +# Output Notes + +The `output_note` module creates and updates notes during a transaction. + +```rust +extern crate alloc; + +use alloc::vec::Vec; +use miden::{output_note::{self, OutputNoteAssetsInfo}, Asset, NoteIdx, Tag, NoteType, Recipient}; +``` + +## Create a note + +```rust +let note_idx: NoteIdx = output_note::create(tag, note_type, recipient); +``` + +`create` returns a `NoteIdx` used by subsequent operations in the same transaction. It may only be called from an account component procedure. Transaction and note scripts must call an account procedure such as `BasicWallet::create_note` and use the returned index. + +## Add assets to a note + +```rust +output_note::add_asset(asset, note_idx); +``` + +`add_asset` only adds the asset to the output note; it does not remove it from the native account's vault. When funding a note from that vault, remove the asset first or use `BasicWallet::move_asset_to_note`. + +Call `add_asset` multiple times with the same `note_idx` to attach several assets to one note. A note can carry both fungible and non-fungible assets. + +## Query output note state + +```rust +// Asset commitment and count +let info: OutputNoteAssetsInfo = output_note::get_assets_info(note_idx); + +// All assets on the note +let assets: Vec = output_note::get_assets(note_idx); + +// The recipient hash +let recipient: Recipient = output_note::get_recipient(note_idx); +``` + +`OutputNoteAssetsInfo` contains `commitment: Word` and `num_assets: u32`. + +### Note metadata + +`get_metadata()` returns the encoded metadata header: + +```rust +let metadata: NoteMetadata = output_note::get_metadata(note_idx); +``` + +The SDK's `NoteMetadata` contains a single `header: Word`. Attachment content and its commitment are queried separately. On the protocol side, the full `NoteMetadata` combines `PartialNoteMetadata` (sender, note type, tag) with attachment headers and the attachments commitment. See [Reading Notes — Note metadata](./reading-notes#note-metadata) for details. + +## Note attachments + +Notes can carry auxiliary data as attachments. The attachment API uses a `Felt`-typed scheme identifier; the payload shape is selected by the function: + +```rust +// Single-word attachment; the helper hashes and inserts the word. +output_note::add_word_attachment(note_idx, attachment_scheme, word_data); +``` + +Use `add_attachment` when you already have an attachment commitment and the raw data is present in the advice map. Use `add_attachment_from_memory` for multi-word data that should be hashed and inserted from memory. Attachments are committed into note metadata, and the consumer must have access to the corresponding advice map entries to read the full data. + +## Computing a Recipient + +When creating notes programmatically, an account component needs a `Recipient` to pass to `output_note::create`. The `Recipient` is a commitment to the note's serial number, script, and storage, which together define the conditions under which the note can be consumed. + +The protocol computation is: + +``` +recipient = hash(hash(hash(serial_num, [0;4]), script_root), storage_commitment) +``` + +`script_root` is the hash of the note script program, and `storage_commitment` is the commitment to the note's storage values. MASM authors can use `note::compute_recipient` for an existing storage commitment or `note::compute_and_store_recipient` when the raw storage values should also be inserted into the advice map. + +## Example: creating and funding a note + +A complete flow for creating a note inside an account component: + +```rust +use miden::{ + component, component_storage, felt, native_account, output_note, Asset, NoteIdx, NoteType, + Recipient, Tag, +}; + +#[component_storage] +struct NoteSenderStorage; + +#[component] +trait NoteSender { + #[account_procedure] + fn send_asset(&mut self, recipient: Recipient, asset: Asset, tag: Tag) -> NoteIdx; +} + +#[component] +impl NoteSender for NoteSenderStorage { + fn send_asset(&mut self, recipient: Recipient, asset: Asset, tag: Tag) -> NoteIdx { + // 1. Create the note + let note_idx = output_note::create(tag, NoteType::from(felt!(1)), recipient); + + // 2. Remove the asset from the native account's vault + let _ = native_account::remove_asset(asset); + + // 3. Add the asset to the note + output_note::add_asset(asset, note_idx); + + note_idx + } +} +``` + +:::info API Reference +Full API docs on docs.rs: [`miden::output_note`](https://docs.rs/miden/latest/miden/output_note/) +::: diff --git a/versioned_docs/version-0.16/builder/smart-contracts/notes/reading-notes.md b/versioned_docs/version-0.16/builder/smart-contracts/notes/reading-notes.md new file mode 100644 index 00000000..dbeb1532 --- /dev/null +++ b/versioned_docs/version-0.16/builder/smart-contracts/notes/reading-notes.md @@ -0,0 +1,172 @@ +--- +title: "Reading Notes" +sidebar_position: 3 +description: "Read the active note's data and access input notes by index during transactions." +--- + +# Reading Notes + +Miden provides two modules for reading note data, each for a different execution context: + +- **`active_note`** — used inside **note scripts**. Reads data from the note currently being executed (the note whose `#[note_script]` is running). +- **`input_note`** — used inside **transaction scripts** and **account code**. Reads data from any input note by index, useful when a transaction consumes multiple notes and needs to inspect them. + +## `active_note` — the executing note + +When a note script runs, `active_note` provides access to the current note's storage, creation-time assets, and metadata: + +```rust +use miden::active_note; +``` + +### Storage + +Note storage is a sequence of `Felt` values set by the note creator (e.g., a target account ID, an expiration block height). The recommended way to access it is through the `#[note]` struct — fields are automatically deserialized from the note's storage: + +```rust +#[note] +struct MyNote { + target_account_id: AccountId, // Deserialized from note storage automatically +} +``` + +See [Note Scripts](./note-scripts) for the full `#[note]` pattern. The low-level `active_note::get_storage()` function is also available for advanced use cases: + +```rust +let storage: Vec = active_note::get_storage(); +``` + +### Assets + +```rust +let assets: Vec = active_note::get_initial_assets(); +``` + +The name makes the semantics explicit: these are the assets the note carried when it was created, before any in-transaction movement. This is an inspection API; iterating over this vector does not remove assets from the note's current state. + +:::warning Rust SDK limitation +The protocol exposes stateful `active_note::remove_asset` and `active_note::remove_all_assets` procedures in MASM, but the Rust SDK does not yet bind them. Do not implement asset consumption by passing values from `get_initial_assets()` directly to an account. Until the bindings land, use a standard note such as P2ID/P2IDE or write the removal flow in MASM. +::: + +### Identity and metadata + +```rust +let sender: AccountId = active_note::get_sender(); +let recipient: Recipient = active_note::get_recipient(); +let script_root: Word = active_note::get_script_root(); +let serial_num: Word = active_note::get_serial_number(); +``` + +### Note metadata + +`get_metadata()` returns the encoded metadata header: + +```rust +let metadata: NoteMetadata = active_note::get_metadata(); +``` + +In the onchain Rust SDK, `NoteMetadata` contains a single `header: Word`; attachments and their commitment are queried separately. + +In `miden-protocol`, the user-facing metadata used to construct a note is `PartialNoteMetadata`: + +```rust +pub struct PartialNoteMetadata { + sender: AccountId, + note_type: NoteType, + tag: NoteTag, +} +``` + +The protocol's full `NoteMetadata` wraps that partial metadata together with attachment headers and an attachments commitment. Its `to_metadata_word()` method produces the same four-felt header returned by the onchain SDK: sender suffix plus type/version, sender prefix, tag, and attachment schemes. + +## `input_note` — querying notes by index + +Inside transaction scripts or account code, use `input_note` to read data from any input note being consumed in the current transaction. Each function takes a `NoteIdx` identifying which note to query: + +```rust +use miden::input_note; +``` + +### Assets + +```rust +let info: input_note::InputNoteAssetsInfo = input_note::get_initial_assets_info(note_idx); +let assets: Vec = input_note::get_initial_assets(note_idx); +``` + +`InputNoteAssetsInfo` contains `commitment: Word` and `num_assets: u32`. + +### Identity and metadata + +```rust +let sender: AccountId = input_note::get_sender(note_idx); +let recipient: Recipient = input_note::get_recipient(note_idx); +let script_root: Word = input_note::get_script_root(note_idx); +let serial_num: Word = input_note::get_serial_number(note_idx); +``` + +### Storage + +```rust +let storage_info: input_note::InputNoteStorageInfo = input_note::get_storage_info(note_idx); +``` + +:::note +Unlike `active_note::get_storage()`, the `input_note` API only exposes the storage commitment and item count. To read the storage values, use `active_note::get_storage()` while that note is executing. +::: + +`InputNoteStorageInfo` contains `commitment: Word` and `num_storage_items: u32`. + +### Note metadata + +Returns the same metadata shape as `active_note`: + +```rust +let metadata: NoteMetadata = input_note::get_metadata(note_idx); +``` + +## Examples + +### Reading storage and inspecting initial assets + +A note script that reads the target account ID from storage, verifies the consumer, and inspects the creation-time asset list: + +```rust +use miden::{AccountId, Word, active_note, native_account, note}; + +#[note] +struct InspectionNote { + target_account_id: AccountId, +} + +#[note] +impl InspectionNote { + #[note_script] + pub fn run(self, _arg: Word) { + assert_eq!(native_account::get_id(), self.target_account_id); + + // Inspection only: this does not remove assets from the active note. + let _initial_assets = active_note::get_initial_assets(); + } +} +``` + +### Reading input notes in a transaction script + +A transaction script that reads data from a consumed input note: + +```rust +use miden::*; + +#[tx_script] +pub fn run(_arg: Word) { + // Query the first input note (index 0) + let idx = NoteIdx { inner: felt!(0) }; + let _assets = input_note::get_initial_assets(idx); + let _sender = input_note::get_sender(idx); +} +``` + +:::info API Reference +Full API docs on docs.rs: [`miden::active_note`](https://docs.rs/miden/0.14.0/miden/active_note/), [`miden::input_note`](https://docs.rs/miden/0.14.0/miden/input_note/) +::: diff --git a/versioned_docs/version-0.16/builder/smart-contracts/overview.md b/versioned_docs/version-0.16/builder/smart-contracts/overview.md new file mode 100644 index 00000000..50a3feaa --- /dev/null +++ b/versioned_docs/version-0.16/builder/smart-contracts/overview.md @@ -0,0 +1,184 @@ +--- +title: "What is a Miden Smart Contract" +sidebar_position: 1 +description: "Miden's execution model, account structure, note system, and transaction lifecycle." +--- + +# What is a Miden Smart Contract + +:::info Concepts apply to both authoring paths +This page describes Miden's execution model — accounts, notes, transactions, and lifecycle. The concepts apply regardless of whether you author contracts in MASM (the mainnet path) or the Rust SDK (in active development). Code examples on this page use Rust; for the MASM path, see [MASM Smart Contracts](./masm/). +::: + +Miden is a zero-knowledge layer 2 where transactions execute on the client and only a cryptographic proof is submitted to the network. Every entity — wallets, contracts, faucets — is an account with code, storage, a vault, and a nonce. Assets move between accounts through notes, which act as programmable UTXOs. This page describes the execution model, account structure, note system, and transaction lifecycle. For a hands-on walkthrough, see the [Miden Bank Tutorial](../tutorials/miden-bank/). + +## What makes Miden different + +On Ethereum, smart contracts execute on every node. On Miden, **transactions execute locally on the client** — and only a cryptographic proof is submitted to the network. + +This means: + +- **Privacy by default** — The network sees the proof, not the inputs +- **Parallel execution** — Transactions don't compete for block space +- **Lower fees** — No gas wars; proofs are cheap to verify +- **Client-side proving** — Your machine generates the ZK proof + +## The compilation pipeline + +Miden runs **MASM** (Miden Assembly) — the VM's native instruction set. There are two authoring paths that produce it: + +**MASM directly** (the supported mainnet path): + +``` +MASM → ZK Circuit → Proof +``` + +You write MASM directly and build it into a `.masp` package (Miden Assembly Package). This is what mainnet supports for production today. See [MASM Smart Contracts](./masm/). + +**Rust** (in active development): + +``` +Rust → Wasm → MASM → ZK Circuit → Proof +``` + +The Miden compiler (`cargo-miden`) compiles your `#![no_std]` Rust to WebAssembly, then translates it to MASM. The output is the same `.masp` package, so both paths share the same execution model and the same [Miden Standards](./standards/) library. + +When a transaction executes, the Miden VM runs the MASM and produces a zero-knowledge proof of correct execution. The output of `cargo miden build` (Rust) or the MASM build tooling is a `.masp` file containing the compiled MASM and metadata. + +## The account model + +Every entity on Miden is an **account**. Accounts are smart contracts — even user wallets. + +An account has four parts: + +| Part | Description | +|------|-------------| +| **Code** | One or more components that define the account's behavior | +| **Storage** | Persistent state — up to 255 slots of `Value` or `StorageMap` | +| **Vault** | The assets (fungible and non-fungible) the account holds | +| **Nonce** | A counter that increments with every state change (replay protection) | + +### Components + +Components are reusable code modules attached to accounts. Think of them as **traits or mixins**, not monolithic contracts. An account can have multiple components. + +```rust +use miden::{component, component_storage, Asset}; + +#[component_storage] +struct MyWalletStorage; + +#[component] +trait MyWallet { + #[account_procedure] + fn receive_asset(&mut self, asset: Asset); +} + +#[component] +impl MyWallet for MyWalletStorage { + fn receive_asset(&mut self, asset: Asset) { + self.add_asset(asset); + } +} +``` + +Each component defines its own storage layout and public methods. In the Rust SDK, storage lives on a `#[component_storage]` struct, the public API is a `#[component]` trait, and the `#[component] impl` block provides the behavior. The macro generates the necessary WIT (WebAssembly Interface Type) definitions for cross-component interoperability. + +See [Components](./accounts/components) for full details. + +## Notes as UTXOs + +Assets don't transfer directly between accounts. Instead, they move through **notes** — programmable messages that carry assets and logic. + +``` +Sender Account → creates Note (with assets + script) → Recipient Account consumes Note +``` + +Notes are similar to Bitcoin's UTXOs, but with arbitrary programmable logic. A note contains: + +- **Assets**: The tokens or NFTs being transferred +- **Script**: Code that executes when the note is consumed (e.g., "only account X can claim this") +- **Storage**: Custom data the script can read + +The most common pattern is **P2ID** (Pay to ID) — a note that can only be consumed by a specific account. + +See [Notes](./notes/) for implementation details. + +## Transaction flow + +A transaction in Miden follows this flow: + +```mermaid +flowchart LR + A[Client builds tx locally] --> B[VM executes MASM] + B --> C[Proof generated] + C --> D[Proof submitted to network] + D --> E[Network verifies proof] + E --> F[State updated] +``` + +1. **Build**: The client assembles the transaction — which notes to consume, which account methods to call +2. **Execute**: The Miden VM executes the transaction locally +3. **Prove**: The VM produces a zero-knowledge proof of correct execution +4. **Submit**: Only the proof (and state commitments) go to the network +5. **Verify**: The network verifies the proof and updates state + +:::info Privacy +Because execution happens locally, the network never sees your transaction inputs, the account's private state, or the logic that ran. It only sees that the proof is valid and the resulting state commitments. +::: + +## What "proof generation fails" means + +When you write assertions in Miden contracts: + +```rust +assert!(amount > felt!(0)); +``` + +If the assertion fails, the ZK circuit **cannot produce a valid proof**. This means: + +- The transaction is rejected **before it ever reaches the network** +- No state changes occur — it's as if the transaction never happened +- The client gets an error explaining which assertion failed + +This is fundamentally different from Ethereum's `revert` — there's no onchain transaction that fails. The proof simply doesn't exist if the execution is invalid. + +A separate failure mode is the [empty transaction](../tutorials/helpers/pitfalls#empty-transaction-no-state-change-no-notes) — a transaction that completes without mutating account state or consuming a note is also rejected, since Miden refuses to admit transactions with no observable effect. + +## Account types + +`AccountType` now controls account-state visibility: + +| Type | Description | +|------|-------------| +| `AccountType::Private` | Only a state commitment is stored onchain; the account owner keeps the full state offchain | +| `AccountType::Public` | Full account state is stored onchain and visible to observers | + +Wallet, contract, and faucet behavior comes from the components and configuration attached to the account. For example, a wallet account typically combines `BasicWallet` with an authentication component, while a fungible token issuer uses the standard `FungibleFaucet` component and token policies. + +## Where to go next + +**Authoring paths and libraries:** + +| Section | When to use | +|---|---| +| [MASM Smart Contracts](./masm/) | Production contracts for mainnet today | +| [Rust SDK](./rust/) | Prototyping today; long-term default once it ships v1 | +| [Miden Standards](./standards/) | Reusable building blocks callable from either path | + +**Topic guides** (concepts apply regardless of authoring language): + +| Topic | Description | +|---|---| +| [Components](./accounts/components) | Reusable code modules with storage and exported interfaces | +| [Storage](./accounts/storage) | Up to 255 slots of `Value` or `StorageMap` | +| [Custom Types](./accounts/custom-types) | Exported structs/enums for public APIs | +| [Account Operations](./accounts/account-operations) | Read/write account state and vault | +| [Notes](./notes/) | Programmable UTXOs for asset transfers | +| [Transactions](./transactions/) | Transaction context, scripts, and the advice provider | +| [Authentication](./accounts/authentication) | Falcon-512 Poseidon2 signatures and replay protection | +| [Cross-Component Calls](./cross-component-calls) | Inter-component communication | +| [Types](./types) | Felt, Word, Asset — the VM's native types | +| [Patterns](./patterns) | Access control, rate limiting, spending limits, anti-patterns | + +Ready to start building? The [Miden Bank Tutorial](../tutorials/miden-bank/) is a hands-on walkthrough (currently written against the Rust SDK; the concepts translate to MASM). diff --git a/versioned_docs/version-0.16/builder/smart-contracts/patterns.md b/versioned_docs/version-0.16/builder/smart-contracts/patterns.md new file mode 100644 index 00000000..59d0a666 --- /dev/null +++ b/versioned_docs/version-0.16/builder/smart-contracts/patterns.md @@ -0,0 +1,86 @@ +--- +title: "Patterns" +sidebar_position: 6 +description: "Common patterns and security considerations for Miden smart contracts." +--- + +# Patterns + +Security considerations and common patterns for Miden smart contracts. For runnable examples, see the [compiler examples directory](https://github.com/0xMiden/compiler/tree/next/examples) and the [Miden Bank Tutorial](../tutorials/miden-bank/). + +## Access control + +:::warning[No msg.sender in account components] +Unlike Solidity, account component procedures cannot check "who is calling me." In Miden: +- **Note scripts** can check who created the note via `active_note::get_sender()` +- **Account components** rely on authentication components (Falcon512, ECDSA) which the transaction kernel invokes automatically in the epilogue +::: + +For account-level access control, Miden uses **authentication components** rather than manual sender checks. The transaction kernel calls the account's `auth` procedure automatically during the transaction epilogue — if the signature is invalid, the entire transaction fails. See [Authentication](./accounts/authentication) for the full pattern. + +For note-level access control, note scripts can check who created the note using `active_note::get_sender()`. The protocol-level `ownable2step` standard (`miden-standards/asm/standards/access/ownable2step.masm`) provides `get_owner`, `get_nominated_owner`, `is_sender_owner`, `assert_sender_is_owner`, `transfer_ownership`, `accept_ownership`, and `renounce_ownership` procedures. + +## Rate limiting {#rate-limiting} + +Use `tx::get_block_number()` to enforce cooldown periods between actions. It returns a typed `BlockNumber`; convert it with `.as_u32()` before using integer arithmetic. Store the last action block number in a `Value` storage slot, then compare it with the current block number before allowing the next action. + +See [Transaction Context](./transactions/transaction-context) for the available block and transaction info functions. + +## Security + +### Assertions and error handling + +Use assertions for conditions that must abort contract execution: + +```rust +assert!(amount > 0); +assert_eq!(a, b); +``` + +When an assertion fails, proof generation fails and the transaction is rejected before reaching the network. + +Internal Rust functions can also return and handle `Result` for recoverable +conditions. Returning an error value does not itself abort a transaction: the +caller must handle it or explicitly fail an assertion. Exported procedures and +entry points must follow the SDK's supported signatures. + +### Replay protection + +Every state-changing transaction must increment the nonce. The auth component handles this automatically — see [Authentication](./accounts/authentication). + +### Safe arithmetic + +Use `saturating_sub` to prevent underflow. If the stored block is ahead of the +current block, it returns zero. Ordinary unsigned subtraction can wrap to a large +value in a release build: + +```rust +let current_block = tx::get_block_number().as_u32(); + +// Good — won't underflow +let elapsed = current_block.saturating_sub(last_block); + +// Dangerous — could underflow +let elapsed = current_block - last_block; +``` + +For Felt arithmetic, values wrap modulo the prime field (no overflow panic), but the result may not be what you expect if you're treating Felts as integers. See [Types — Felt](./types#felt--field-elements) for details. + +### Anti-patterns + +- **Don't store secrets in contract code** — public account code is published onchain +- **Don't skip nonce management** — prevents replay attacks +- **Be careful with Felt division** — Felt division computes the multiplicative inverse, not integer division. Convert to `u64` first for integer-style operations + +## `#![no_std]` environment + +All Miden contracts run without the standard library: + +| Not available | Alternative | +|---------------|-------------| +| `std::collections::HashMap` | Use `BTreeMap` from `alloc`, or `StorageMap` for persistent account storage | +| `std::string::String` | Use `alloc::string::String` | +| `std::vec::Vec` | Use `alloc::vec::Vec` | +| `println!()` | Use `miden::println!()` | +| `eprintln!()` | No direct equivalent — run the transaction under the Mockchain and inspect outputs, or use the external debugger | +| Error strings in `assert!()` | Use `assert!(condition)` without messages | diff --git a/versioned_docs/version-0.16/builder/smart-contracts/rust/index.md b/versioned_docs/version-0.16/builder/smart-contracts/rust/index.md new file mode 100644 index 00000000..42c5da51 --- /dev/null +++ b/versioned_docs/version-0.16/builder/smart-contracts/rust/index.md @@ -0,0 +1,55 @@ +--- +title: "Rust Smart Contracts" +description: "Author Miden smart contracts in Rust — the long-term direction for Miden development, currently in active development." +--- + +# Rust Smart Contracts + +The Rust SDK is the long-term direction for Miden smart-contract development: define account components, note scripts, and transaction scripts in idiomatic `#![no_std]` Rust with typed storage, attribute macros, and client-side proving. The SDK compiles to Miden Assembly (MASM) under the hood, so the same execution model and standards library apply. + +:::caution Currently in active development +The Rust SDK is being actively developed and is **not yet production-ready for mainnet**. For production deployments today, write contracts in [Miden Assembly (MASM)](../masm/) — the supported path Miden mainnet verifies. Use the Rust SDK for prototyping, experimentation, and exploration of the long-term direction. +::: + +The pages below describe the Rust SDK's current shape: how accounts and components compose, how notes are scripted, how transactions execute, and the patterns you can already build with. + +## Inside the Rust SDK + + + + Components, storage, custom types, operations, cryptography, and authentication. + + + Programmable UTXOs for asset transfers and cross-account interaction. + + + Transaction context, scripts, and the advice provider. + + + Calling methods across account components and from note scripts. + + + Core types: Felt, Word, AccountId, NoteId, and more. + + + Access control, rate limiting, spending limits, and anti-patterns. + + + +## When to choose Rust vs MASM today + +| Concern | Rust SDK | MASM | +|---|---|---| +| **Mainnet production** | In active development | [Supported today](../masm/) | +| **Ergonomics** | Familiar Rust idioms, type-checked storage, attribute macros | Stack-based assembly, explicit control | +| **Compilation target** | Compiles via Wasm → MASM | Directly authored | +| **Use case** | Long-term default once mature; prototyping and exploration today | Production contracts for mainnet | + +Both authoring paths share the same [Miden Standards](../standards/) library — standard components, notes, and faucet policies are callable from either. + +## Related pages + +- [MASM Smart Contracts](../masm/) — the path mainnet supports today. +- [Miden Standards](../standards/) — reusable building blocks callable from Rust or MASM. +- [Smart Contracts → Overview](../overview) — execution model and lifecycle (concepts apply to both authoring paths). +- [API reference on docs.rs](https://docs.rs/miden/latest/miden/) — full Rust SDK API documentation. diff --git a/versioned_docs/version-0.16/builder/smart-contracts/standards/account-components.md b/versioned_docs/version-0.16/builder/smart-contracts/standards/account-components.md new file mode 100644 index 00000000..5e4fa250 --- /dev/null +++ b/versioned_docs/version-0.16/builder/smart-contracts/standards/account-components.md @@ -0,0 +1,98 @@ +--- +title: "Account Components" +description: "Use standard account components for wallets, authentication, access control, faucets, and account inspection." +--- + +# Account Components + +Standard account components are prebuilt modules you can compose into accounts. They give an account a recognizable interface, such as "can receive assets", "can authenticate with this signature scheme", "can mint fungible tokens", or "uses this access-control model". + +Use these components from Rust when you build accounts with the SDK, or import the underlying MASM modules when you are writing lower-level account code. The same standards surface is what note scripts and transaction scripts rely on when they call account procedures. + +## Common standards surfaces + +| Surface | Use it for | Rust module | +|---------|------------|-------------| +| `BasicWallet` | Holding assets, receiving assets from standard notes, and moving assets into output notes. | `miden_standards::account::wallets` | +| `FungibleFaucet` | Minting, sending, receiving, and burning fungible assets from faucet accounts. | `miden_standards::account::faucets` | +| `AuthSingleSig` | Single-signature authentication of transactions. | `miden_standards::account::auth` | +| `AuthMultisig` / `AuthMultisigSmart` | Threshold or policy-aware multisig authentication. | `miden_standards::account::auth` | +| `AuthGuardedMultisig` | Multisig guarded by a guardian configuration. | `miden_standards::account::auth` | +| `AuthNetworkAccount` | Authentication through note- and transaction-script allowlists for network accounts. | `miden_standards::account::auth` | +| `Ownable2Step` | Access control for account owners. | `miden_standards::account::access` | +| `RoleBasedAccessControl` | Role-based authorization for protected account procedures. | `miden_standards::account::access` | +| `Authority` | Shared authority component for protecting administrative changes. | `miden_standards::account::access` | +| `TokenPolicyManager` | Registering and updating mint, burn, send, and receive token policies. | `miden_standards::account::policies` | +| `BasicBlocklist` | Blocking specific native accounts in send and receive transfer-policy checks. | `miden_standards::account::policies` | +| `BasicAllowlist` | Allowing only specific native accounts in send and receive transfer-policy checks. | `miden_standards::account::policies` | + +These are building blocks. They do not prevent you from adding custom components to the same account. + +## Start with wallet and auth + +Most regular accounts need: + +- an authentication component, such as `AuthSingleSig` or `AuthMultisig` +- the `BasicWallet` component + +`AuthSingleSig` controls transaction authorization. `BasicWallet` exposes the standard wallet procedures used by common notes, including the ability to receive assets and move assets into output notes. + +```rust title="Compose a regular account with standard auth and wallet components" +use miden_client::{ + account::{AccountBuilder, AccountType, component::BasicWallet}, + auth::{Approver, AuthSchemeId, AuthSingleSig}, +}; +use miden_protocol::{account::auth::PublicKeyCommitment, Word}; + +fn build_wallet_account() -> Result<(), Box> { + let public_key = PublicKeyCommitment::from(Word::from([1, 2, 3, 4u32])); + + let account = AccountBuilder::new([1; 32]) + .account_type(AccountType::Public) + .with_component(AuthSingleSig::new(Approver::new( + public_key, + AuthSchemeId::Falcon512Poseidon2, + ))) + .with_component(BasicWallet) + .build()?; + + assert!(account.is_public()); + Ok(()) +} +``` + +For authentication details, see [Authentication](../accounts/authentication). For how component methods are authored in Rust, see [Components](../accounts/components). + +## Check note compatibility + +Standard notes assume the consuming account exposes the procedures they need. For example, P2ID and P2IDE notes need a wallet-compatible account that can receive assets. SWAP notes additionally need the wallet procedure that moves the requested asset into the payback note. + +At the builder level, the practical rule is: + +- Add `BasicWallet` to accounts that should receive standard asset-transfer notes. +- Add `FungibleFaucet` together with `TokenPolicyManager` to faucet accounts that should mint or burn fungible assets. +- For local or user accounts, add an auth component to reject unauthorized transactions. +- For network accounts, use `AuthNetworkAccount` to restrict the allowed note and transaction scripts. + +Prefer building on top of `BasicWallet`: compose it with a custom extension component for application-specific methods. If you replace the wallet interface entirely, test consumption of the relevant standard notes deliberately. + +## Rust and MASM entry points + +Rust APIs are the usual entry point for account composition. MASM modules are available when you need exact low-level behavior. + +| Area | Rust module | MASM module family | +|------|-------------|--------------------| +| Wallets | `miden_standards::account::wallets` | `miden::standards::wallets::*` | +| Authentication | `miden_standards::account::auth` | `miden::standards::auth::*` | +| Access control | `miden_standards::account::access` | `miden::standards::access::*` | +| Faucets | `miden_standards::account::faucets` | `miden::standards::faucets::*` | +| Policies | `miden_standards::account::policies` | `miden::standards::faucets::policies::*` | +| Inspection | `miden_standards::account::inspection` | `miden::standards::inspection::*` | + +Reach for MASM directly when you are implementing low-level behavior, integrating a custom component with a standard procedure, or verifying exact stack effects. + +## Related pages + +- [Standard notes](./standard-notes) - which account interfaces each standard note expects +- [Faucets and policies](./faucets-and-policies) - using faucet and mint policy components +- [`miden-standards` account source](https://github.com/0xMiden/protocol/tree/next/crates/miden-standards/src/account) - current implementation diff --git a/versioned_docs/version-0.16/builder/smart-contracts/standards/faucets-and-policies.md b/versioned_docs/version-0.16/builder/smart-contracts/standards/faucets-and-policies.md new file mode 100644 index 00000000..5d4527c6 --- /dev/null +++ b/versioned_docs/version-0.16/builder/smart-contracts/standards/faucets-and-policies.md @@ -0,0 +1,149 @@ +--- +title: "Faucets and Policies" +description: "Build fungible token faucets and choose standard mint, burn, send, and receive policies." +--- + +# Faucets and Policies + +In Miden, a token issuer is an account. The current `FungibleFaucet` component bundles token metadata with the standard mint and burn procedures, while the asset's composition is determined at the asset level by `AssetComposition`. The standard faucet components give builders a reusable way to create token issuers without hand-writing the entire faucet interface. + +Use this page when you need to create a faucet account, decide who can mint or burn, or understand how faucet behavior relates to standard notes. + +## Faucet component + +The current standard fungible faucet component is `FungibleFaucet`. + +| Surface | Entry point | +|---------|-------------| +| Rust component | `miden_standards::account::faucets::FungibleFaucet` | +| Rust builder/helper | `FungibleFaucetBuilder`, `create_singlesig_user_fungible_faucet` | +| MASM component | `miden::standards::faucets::fungible` | +| Account role | Faucet account whose account ID identifies the issuer. | + +Public state is typical for shared token faucets because clients can discover faucet state, metadata, code, and vault changes. Private state is possible, but it changes who can observe the faucet. + +```rust title="Create a fungible faucet with allow-all policies" +use miden_client::{ + account::{ + AccountType, + component::{ + AuthSingleSig, + BurnPolicy, + FungibleFaucet, + MintPolicy, + TokenName, + TokenPolicyManager, + TransferPolicy, + create_singlesig_user_fungible_faucet, + }, + }, + asset::TokenSymbol, + auth::Approver, +}; +use miden_protocol::{ + account::auth::{AuthScheme, PublicKeyCommitment}, + asset::AssetAmount, + Word, +}; +fn create_faucet_account() -> Result<(), Box> { + let public_key = PublicKeyCommitment::from(Word::from([1, 2, 3, 4u32])); + + let faucet = FungibleFaucet::builder() + .name(TokenName::new("Example Token")?) + .symbol(TokenSymbol::new("EXT")?) + .decimals(6) + .max_supply(AssetAmount::from(1_000_000u32)) + .build()?; + + let policies = TokenPolicyManager::builder() + .active_mint_policy(MintPolicy::allow_all()) + .active_burn_policy(BurnPolicy::allow_all()) + .active_send_policy(TransferPolicy::allow_all()) + .active_receive_policy(TransferPolicy::allow_all()) + .build(); + + let auth = AuthSingleSig::new(Approver::new( + public_key, + AuthScheme::Falcon512Poseidon2, + )); + + let account = create_singlesig_user_fungible_faucet( + [9; 32], + faucet, + auth, + policies, + AccountType::Public, + )?; + + assert!(account.is_public()); + Ok(()) +} +``` + +## Token identity + +A fungible asset is tied to its faucet account ID. The faucet's metadata describes the token, while the account ID identifies the asset issuer. + +| Field | Meaning | +|-------|---------| +| Symbol | Short token symbol. | +| Decimals | Display precision for client UX. | +| Max supply | Upper bound enforced by the faucet component. | +| Token name | Mandatory display name. | +| Optional metadata | Optional display fields such as description, logo URI, and external link. | +| Faucet account ID | The issuer ID used when constructing fungible assets and checking balances. | + +When an account checks its balance for a fungible token at the protocol/client layer, it queries by the asset's `AssetId`, which is derived from the faucet account ID. Whether the asset invokes callbacks is encoded in the faucet account ID at construction time. + +## Choose policy modules + +Policy modules decide which operations are allowed for a token faucet. + +| Policy area | Current standard examples | Use it for | +|-------------|---------------------------|------------| +| Mint | `MintPolicy::allow_all()`, `MintPolicy::owner_only()` | Gate mint operations. | +| Burn | `BurnPolicy::allow_all()`, `BurnPolicy::owner_only()` | Gate burn operations. | +| Send | `TransferPolicy::allow_all()`, `TransferPolicy::empty_basic_blocklist()`, `TransferPolicy::with_basic_blocklist(...)` | Gate assets leaving accounts through notes. | +| Receive | `TransferPolicy::allow_all()`, `TransferPolicy::empty_basic_blocklist()`, `TransferPolicy::with_basic_blocklist(...)` | Gate assets entering account vaults. | + +Use `BlocklistManager` alongside a basic blocklist when its entries must be updated at runtime. + +`TokenPolicyManager` owns the active policy roots and validates policy changes. Authority for changing policies comes from the account's access-control setup, such as owner-controlled or role-based authority. + +## Mint with notes + +Minting does not directly credit a recipient's account vault. A faucet creates a note carrying the minted asset, and the recipient consumes that note to receive the asset. + +For standard flows: + +- A user can send a public, asset-free `MintNote` to a network faucet to request minting. The faucet consumes that request and creates a delivery note containing the minted asset. +- A user-controlled faucet can mint directly into a delivery note, such as P2ID, in its own transaction. +- The recipient discovers and consumes the delivery note. +- The recipient's account must be able to receive the asset, usually by including `BasicWallet`. + +The delivery note is created in one transaction and consumed in another, as described in [What are Notes?](../notes/introduction). A network-faucet flow also includes the earlier transaction that publishes the mint request. + +## Burn returned assets + +Burning is also note-based. A burn note returns assets to the faucet and executes the standard burn behavior. Use `BurnNote` from `miden-standards` rather than hand-writing a burn script unless your protocol needs custom conditions. + +## When to write a custom faucet + +Use `FungibleFaucet` when supply, metadata, minting, burning, and transfer policies match the standard pattern. + +Before writing a custom faucet, first check whether `TokenPolicyManager` plus a standard or custom policy component can express the rule. Custom policies can gate minting, burning, sending, and receiving without replacing the faucet component. For some minting flows, a custom mint note accepted by the faucet can move application-specific supply logic out of the faucet itself. + +Write a custom faucet component when: + +- You need customized versions of the standard faucet procedures. +- The asset issuance model cannot be expressed with `TokenPolicyManager`, custom policies, or custom mint notes. +- The faucet's state model must differ from the standard metadata, supply, and policy layout. + +If you only need additional public methods, compose the faucet account with an extension component instead of replacing `FungibleFaucet`. Even then, consider reusing standard auth, ownership, and wallet components where they fit. Custom faucet logic does not require custom authentication or custom note formats by default. + +## Related pages + +- [Account components](./account-components) - composing faucets with standard auth and ownership components +- [Standard notes](./standard-notes) - mint and burn notes +- [Assets, Vault, and Faucet migration notes](../../migration/asset-vault-faucet) - asset and faucet changes +- [`miden-standards` faucet source](https://github.com/0xMiden/protocol/tree/next/crates/miden-standards/src/account/faucets) - current implementation diff --git a/versioned_docs/version-0.16/builder/smart-contracts/standards/index.md b/versioned_docs/version-0.16/builder/smart-contracts/standards/index.md new file mode 100644 index 00000000..54b1d8d0 --- /dev/null +++ b/versioned_docs/version-0.16/builder/smart-contracts/standards/index.md @@ -0,0 +1,63 @@ +--- +title: "Miden Standards" +description: "Use standard Miden account components, notes, faucets, and policies from Rust or Miden Assembly." +--- + +# Miden Standards + +Miden Standards are reusable building blocks for common smart-contract behavior: wallet interfaces, authentication components, token faucets, note scripts, and policy modules. + +Use them when you want your account, note, or transaction flow to interoperate with the rest of the Miden ecosystem instead of defining every interface from scratch. + +:::caution Versioned APIs +These pages track the v0.16 standards surface. Use the version selector if you are building against an older protocol release. +::: + +This section is a builder guide, not the canonical standards specification. It explains which standard to reach for, how it fits into the smart-contract model, and where to switch to reference docs when you need exact procedure names, storage schemas, or script roots. + +## How standards fit into smart contracts + +Smart Contracts is the domain. Rust and Miden Assembly are authoring paths inside that domain. + +| Layer | Role | +|------|------| +| Rust | Compose standard account components and construct standard notes through Rust APIs. | +| Miden Assembly | Import and call standard MASM modules directly when writing low-level account, note, transaction, or library code. | +| Miden Standards | Shared account components, note scripts, faucet policies, and helper modules used by both authoring paths. | + +## What this section covers + + + + Use standard wallet, authentication, access-control, faucet, and metadata components. + + + Choose P2ID, P2IDE, SWAP, PSWAP, mint, and burn note scripts. + + + Build token faucets and choose mint, burn, send, and receive policy modules. + + + +## When to use standards + +Use a standard component or note when: + +- You want other accounts, note scripts, clients, or tooling to recognize your account interface. +- Your behavior matches an existing pattern, such as holding assets, receiving a P2ID transfer, minting a fungible token, or verifying a single signature. +- You need a stable building block before adding custom application logic. + +Write custom components or note scripts when: + +- The authorization rules are application-specific. +- The note consumption condition is not covered by P2ID, P2IDE, SWAP, mint, or burn notes. +- The account's state model needs custom storage and custom exported methods. + +You can mix both approaches. A typical application account starts with standard authentication and wallet components, then adds one or more custom components for protocol-specific logic. + +## Related pages + +- [Accounts](../accounts/) - components, storage, authentication, and account operations +- [Notes](../notes/) - note model, note scripts, standard note types, and output notes +- [Cross-component calls](../cross-component-calls) - calling component interfaces from scripts and components +- [`miden-standards` source](https://github.com/0xMiden/protocol/tree/next/crates/miden-standards) - current standards implementation diff --git a/versioned_docs/version-0.16/builder/smart-contracts/standards/standard-notes.md b/versioned_docs/version-0.16/builder/smart-contracts/standards/standard-notes.md new file mode 100644 index 00000000..e540c60c --- /dev/null +++ b/versioned_docs/version-0.16/builder/smart-contracts/standards/standard-notes.md @@ -0,0 +1,109 @@ +--- +title: "Standard Notes" +description: "Use standard Miden note scripts for transfers, expiring transfers, swaps, minting, and burning." +--- + +# Standard Notes + +Standard notes are prebuilt note scripts from `miden-standards`. They cover the common asset flows builders need before writing custom note scripts. + +Use the Rust APIs to construct standard notes in client or transaction-building code. The scripts themselves are MASM modules, so direct MASM authors can inspect or import the same standard scripts when they need exact procedure behavior. + +## Which note should I use? + +| Note | Use it when | Rust type | MASM module | +|------|-------------|-----------|-------------| +| P2ID | You are sending assets to a specific account ID. | `P2idNote` | `miden::standards::notes::p2id` | +| P2IDE | You are sending to a specific account ID with a timelock and/or reclaim path. | `P2ideNote` | `miden::standards::notes::p2ide` | +| SWAP | You are offering one asset and requiring a specific asset in return. | `SwapNote` | `miden::standards::notes::swap` | +| PSWAP | You need a partially fillable swap note. | `PswapNote` | `miden::standards::notes::pswap` | +| MINT | You are requesting that a network faucet mint an asset and create a delivery note. | `MintNote` | `miden::standards::notes::mint` | +| BURN | A faucet is burning an asset returned through a note. | `BurnNote` | `miden::standards::notes::burn` | + +For the note model itself, start with [What are Notes?](../notes/introduction). This page focuses on how the standards fit into builder workflows. + +```rust title="Create a public P2ID note" +use miden_protocol::Word; +use miden_protocol::account::{ + AccountId, AccountIdVersion, AccountType, AssetCallbackFlag, +}; +use miden_protocol::asset::{Asset, FungibleAsset}; +use miden_protocol::crypto::rand::RandomCoin; +use miden_protocol::note::{Note, NoteType}; +use miden_standards::note::P2idNote; + +fn dummy_account(byte: u8, account_type: AccountType) -> AccountId { + let mut bytes = [0; 15]; + bytes[0] = byte; + AccountId::dummy( + bytes, + AccountIdVersion::Version1, + account_type, + AssetCallbackFlag::Disabled, + ) +} + +fn create_p2id_note() -> Result<(), Box> { + let sender = dummy_account(1, AccountType::Public); + let target = dummy_account(2, AccountType::Public); + let faucet_id = dummy_account(3, AccountType::Public); + let asset: Asset = FungibleAsset::new(faucet_id, 100)?.into(); + let mut rng = RandomCoin::new(Word::from([1, 2, 3, 4u32])); + + let note: Note = P2idNote::builder() + .sender(sender) + .target(target) + .asset(asset) + .note_type(NoteType::Public) + .generate_serial_number(&mut rng) + .build()? + .into(); + + assert_eq!(note.metadata().sender(), sender); + Ok(()) +} +``` + +`AccountId::dummy` is available with the protocol crate's `testing` feature +and keeps this example self-contained. Production code should use account IDs +created or retrieved through the client. + +## Account requirements + +Standard notes assume the consuming account exposes the procedures the note script calls. + +| Note | Consuming account needs | +|------|-------------------------| +| P2ID / P2IDE | `BasicWallet`, exposing `receive_asset`. | +| SWAP / PSWAP | `BasicWallet` and `NoteCreator`, exposing `receive_asset`, `move_asset_to_note`, and `create_note`. | +| MINT | A network faucet exposing `CodeInspection::has_procedure` and a fungible or non-fungible `mint_and_send` procedure. | +| BURN | The issuing faucet exposing `CodeInspection::has_procedure` and a fungible or non-fungible `receive_and_burn` procedure. | + +If you write a custom wallet or faucet component, test it against the standard notes you expect it to consume. + +## Attachments and execution hints + +Standard notes can use attachments and execution hints to help clients and indexers route notes and decide when a note might be consumable. + +| Helper | Use it for | +|--------|------------| +| `StandardNoteAttachment` | Identifiers for standard attachment schemes. | +| `NetworkAccountTarget` | Attaching network-account targeting data to notes. | +| `AccountTargetNetworkNote` | Wrapping notes known to target network accounts. | +| `NetworkNoteExt` | Convenience helpers for network-targeted notes. | +| `NoteExecutionHint` | Encoding when clients should attempt note execution. | + +Execution hints do not replace note-script checks. The note script still enforces consumption rules during transaction execution. + +## Rust and MASM entry points + +Rust constructors are the usual way to create standard notes in client-side code. Direct MASM authors should use the standard note modules as the source of truth for stack effects and script behavior. + +The Rust types live under `miden_standards::note`. The MASM scripts live under `miden::standards::notes::*`. + +## Related pages + +- [Standard Note Types](../notes/note-types) - more detail on P2ID, P2IDE, and SWAP +- [Output Notes](../notes/output-notes) - creating output notes from transactions +- [Note Scripts](../notes/note-scripts) - writing custom note scripts +- [`miden-standards` note source](https://github.com/0xMiden/protocol/tree/next/crates/miden-standards/src/note) - current implementation diff --git a/versioned_docs/version-0.16/builder/smart-contracts/transactions/_category_.json b/versioned_docs/version-0.16/builder/smart-contracts/transactions/_category_.json new file mode 100644 index 00000000..5f2ae57d --- /dev/null +++ b/versioned_docs/version-0.16/builder/smart-contracts/transactions/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "Transactions", + "position": 5, + "link": { + "type": "generated-index", + "slug": "/builder/smart-contracts/transactions" + } +} diff --git a/versioned_docs/version-0.16/builder/smart-contracts/transactions/advice-provider.md b/versioned_docs/version-0.16/builder/smart-contracts/transactions/advice-provider.md new file mode 100644 index 00000000..bbbeb122 --- /dev/null +++ b/versioned_docs/version-0.16/builder/smart-contracts/transactions/advice-provider.md @@ -0,0 +1,152 @@ +--- +title: "Advice Provider" +sidebar_position: 5 +description: "Reading and writing the advice map, loading preimages, and requesting Falcon signatures in Miden transactions." +--- + +# Advice Provider + +The advice provider is a mechanism for supplying non-deterministic auxiliary data to the VM during proof generation. It backs an **advice map** (a key-value store of `Word → Vec`) and an **advice stack** that host-provided data can be pushed onto. Common uses include passing structured data into transaction scripts, providing Falcon signatures for authentication, and seeding note scripts with external inputs. + +## Trust model and data integrity + +The advice provider is supplied by the **host** (the Miden client or node) — not by onchain consensus. This means the VM cannot blindly trust that the host provided correct data. Two patterns address this: + +### Unverified stack push (caller must verify) + +`adv_push_mapvaln` pushes data onto the advice stack **without verification**. The caller is responsible for checking integrity if the data is security-sensitive: + +```rust +let num_felts = adv_push_mapvaln(key); +// Data is now on the stack — but not verified +// If integrity matters, hash the result and compare to a known commitment +``` + +For signatures, `emit_falcon_sig_to_stack` pushes a Falcon512 signature that is subsequently verified by `rpo_falcon512_verify` — the verification step is what makes it safe. + +### Commitment-verified loading (safe) + +`adv_load_preimage` is integrity-safe by construction. The VM verifies that the loaded data hashes to the provided `commitment` before returning it. If the host tampers with the data, the hash won't match and proof generation fails: + +```rust +// The VM will abort if hash(loaded_data) != commitment +let data = adv_load_preimage(num_words, commitment); +// `data` is guaranteed to match `commitment` +``` + +Use this pattern when the commitment is known ahead of time (e.g., stored in a note input or passed as a script argument). + +:::warning +Never use `adv_push_mapvaln` for security-sensitive data without a subsequent integrity check. The host can supply any value it wants. Use `adv_load_preimage` (commitment-verified) or verify the loaded data yourself using `hash_words`. +::: + +## Reading from the advice map + +### `adv_push_mapvaln` + +Pushes the value associated with a key onto the advice stack and returns its length. + +```rust +use miden::intrinsics::advice::adv_push_mapvaln; + +// Push the value for `key` onto the advice stack; returns the number of Felts pushed. +let num_felts: Felt = adv_push_mapvaln(key); +``` + + +### `adv_load_preimage` + +Pops a preimage from the advice stack and checks it against a commitment and expected word count. Load the data onto the stack first, for example with `adv_push_mapvaln`; `adv_load_preimage` does not look up the advice map itself. This is useful when a note or transaction script needs to retrieve data that was hashed and stored by the sender. + +```rust +extern crate alloc; + +use alloc::vec::Vec; +use miden::{adv_load_preimage, Felt}; + +// Load `num_words` Words whose hash matches `commitment`. +let felts: Vec = adv_load_preimage(num_words, commitment); +``` + + +### Pattern: passing structured data to a transaction script + +The canonical pattern (used in `basic-wallet-tx-script`) combines `adv_push_mapvaln` with `adv_load_preimage` to retrieve structured data encoded as a preimage: + +```rust +extern crate alloc; + +use alloc::vec::Vec; +use miden::{intrinsics::advice::adv_push_mapvaln, *}; + +// 1. Look up the key — returns the number of Felts stored there +let num_felts = adv_push_mapvaln(key); + +// 2. Convert to a Felt count of words and load the preimage (length must be word-aligned) +let num_felts_u64 = num_felts.as_canonical_u64(); +assert_eq(Felt::from_u32((num_felts_u64 % 4) as u32), felt!(0)); +let num_words = Felt::new(num_felts_u64 / 4).unwrap(); +let data: Vec = adv_load_preimage(num_words, key); + +// 3. Index into the data by field position +let tag = data[0]; +let note_type = data[1]; +// ... +``` + +See [Transaction Scripts](./transaction-scripts) for the full `basic-wallet-tx-script` example. + +## Writing to the advice map + +### `adv_insert` + +Inserts a slice of `Word`s into the advice map under the given key. + +```rust +use miden::intrinsics::advice::adv_insert; + +let values: &[Word] = &[word_a, word_b]; +adv_insert(key, values); +``` + +An existing key can be inserted again with the same values. Inserting different +values under an existing key fails; advice-map entries cannot be overwritten. + +### `adv_insert_mem` + +Inserts a range of memory into the advice map. Both `u32` addresses use Miden's field-element address space: `[start_addr, end_addr)` contains `end_addr - start_addr` Felts. A Word occupies four addresses, so the interval below covers two Words. These are VM addresses, not Rust byte pointers; use `adv_insert` when the data is already a Rust slice of Words. + +```rust +use miden::intrinsics::advice::adv_insert_mem; + +let start_addr: u32 = 0; +let end_addr: u32 = 8; +adv_insert_mem(key, start_addr, end_addr); +``` + + +## Requesting a Falcon signature + +`emit_falcon_sig_to_stack` emits an `AUTH_REQUEST_EVENT` that instructs the host to push a Falcon512 signature onto the advice stack. This is typically used in [authentication components](../accounts/authentication) before calling `rpo_falcon512_verify`. + +```rust +use miden::intrinsics::advice::emit_falcon_sig_to_stack; + +// Request the host to push a Falcon signature onto the advice stack +emit_falcon_sig_to_stack(msg, pub_key); +``` + +The standard transaction host generates new signatures only during the +authentication procedure, with a valid transaction summary. A script outside +authentication must receive a signature in its advice inputs before execution; +the event loads that supplied signature for the verifier. + +:::info API Reference +Full API docs on docs.rs: [`miden::intrinsics::advice`](https://docs.rs/miden/latest/miden/intrinsics/advice/) +::: + +## Related + +- [Authentication](../accounts/authentication) — Falcon512 signature verification (over Poseidon2) and nonce management +- [Transaction Scripts](./transaction-scripts) — executing logic in the transaction context +- [Transaction Context](./transaction-context) — overview of transaction execution diff --git a/versioned_docs/version-0.16/builder/smart-contracts/transactions/fees.md b/versioned_docs/version-0.16/builder/smart-contracts/transactions/fees.md new file mode 100644 index 00000000..6062d714 --- /dev/null +++ b/versioned_docs/version-0.16/builder/smart-contracts/transactions/fees.md @@ -0,0 +1,216 @@ +--- +title: "Transaction Fees" +sidebar_position: 1 +description: "How to fund and pay for Miden transactions, understand fee calculation, inspect the payment, and handle network-account fees." +--- + +# Transaction Fees + +Miden transaction fees pay for verification and batch inclusion. The executing account pays from its vault by creating a public `TX_FEE` output note during authentication. The batch builder can collect that note when it includes the transaction. + +- The current clients pay in the network's native fee asset, identified by `fee_faucet_id` in the reference block. +- The amount depends on the reference block's base fee and the logarithm of the transaction's estimated VM cycle count. +- A new account can fund its first transaction by consuming a note containing the fee asset. +- Payment takes effect when the transaction is included and its fee note becomes available onchain. + +Local execution and proving use the client's resources. Charges from a delegated proving service, if any, are separate from the protocol transaction fee. + +## Paying your first fee + +For a standard single-signature account, the client prepares native fee payment automatically. The account must retain enough of the native fee asset to cover the fee after any transfers of that asset. + +Creating a faucet for your own token does not supply native fee funds. The faucet pays when it mints, the sender pays when it sends, and the recipient pays when it consumes the resulting note. Each of those transactions needs its own funding. + +To fund an empty account on a development network: + +1. Request a note containing the native fee asset from that network's faucet. Check that the issuing faucet matches the reference block's `fee_faucet_id`. +2. Synchronize the client until the funding note and its inclusion proof are available. +3. Consume the funding note. Its script deposits the asset before authentication withdraws the fee, leaving the remainder in the account vault. +4. Wait for confirmation and synchronize before using the remaining balance. + +A standard P2ID funding note calls `BasicWallet::receive_asset`. A custom account must expose that interface, or use a funding note compatible with its own receiving procedure. The incoming amount must cover the first consume's fee as well as the balance you want to keep. + +### Native payment with the Rust client + +This fragment uses `miden-client` `0.16.0`. It assumes an initialized `client`, a locally tracked single-signature `account_id` with its signer available, and a committed `funding_note` of type `Note` containing sufficient native fee funds. The account must be able to receive the note's assets. + +```rust +use miden_client::transaction::TransactionRequestBuilder; + +let request = TransactionRequestBuilder::new() + .build_consume_notes(vec![funding_note])?; + +let tx_id = client.submit_new_transaction(account_id, request).await?; +``` + +On a network with a nonzero verification base fee, the client adds the native `1/1` fee-conversion commitment during request preparation. Submission returns a transaction ID; use it to track confirmation before continuing. See [Transactions](../../tools/clients/web-client/transactions.md) for the Web SDK's send, mint, consume, and confirmation APIs. + +## How the fee is calculated + +The transaction's reference block supplies a `verification_base_fee`, expressed in base units of the network's native fee asset. During authentication, `compute_fee` combines the cycles already executed with an estimate of the remaining work, including signature verification, fee payment, and the kernel epilogue. + +For a positive estimated total of $C$ cycles and a verification base fee of $B$, the fee in native base units is: + +$$ +F_{\mathrm{native}} = B \times \left(\left\lfloor \log_2 C \right\rfloor + 1\right) +$$ + +The current output-note component of the protocol fee is zero, so only the verification term contributes. Within one cycle range, additional computation leaves the fee unchanged. Crossing a power-of-two boundary increases the multiplier by one, including when the estimate is exactly that power of two. + +For an illustrative base fee of `500`: + +| Estimated total cycles | Multiplier | Fee in native base units | +| --- | --- | --- | +| 32,768 to 65,535 | 16 | 8,000 | +| 65,536 to 131,071 | 17 | 8,500 | +| 131,072 to 262,143 | 18 | 9,000 | + +A `90,000`-cycle estimate therefore costs `500 × 17 = 8,500` native base units. These are arithmetic examples, not live network prices or measured costs for a particular operation. Read the base fee from the transaction's reference block. + +:::info Why the remaining work is estimated +Authentication runs before the transaction has finished. Standard authentication components add cycle margins for their signature scheme and the remaining kernel work. The resulting estimate can differ from the final measured execution trace; the fee changes only if the estimate crosses a tier boundary. +::: + +## Inspecting a transaction's fee + +With Web SDK `0.16.0`, the manual transaction lifecycle exposes the fee note after local execution and before proving or submission. This fragment assumes an initialized `client`, a tracked `account` with sufficient native funding and an available signer, and a prepared `request`: + +```typescript +const execution = await client.transactions.executeRequest(account, request); +const feeNote = execution.result.executedTransaction().feeNote(); + +if (feeNote) { + const asset = feeNote.assets()?.fungibleAssets()[0]; + if (!asset) throw new Error("Expected a native fee asset"); + console.log("Fee asset:", asset.faucetId().toString()); + console.log("Fee in base units:", asset.amount().toString()); +} else { + console.log("This transaction has no protocol fee note"); +} +``` + +This reads the payment produced by that execution. It does not prove, submit, or apply the transaction. Continue with the same execution artifact if you want to submit it; executing a new request against a different reference block or state can produce a different fee. + +:::note Execution can request a signature +`executeRequest` runs authentication. It requires the account state and the authorization needed to complete execution. The SDK's `preview` supports transactions awaiting authorization, such as multisig proposals; it is not a general signature-free fee estimator for single-signature accounts. A numerical quote from the formula above also needs a cycle estimate and does not execute the request. +::: + +## How payment works + +```mermaid +flowchart TD + A[Run input notes and transaction script] --> B[Authentication computes the fee] + B --> C[Withdraw assets and create a public TX_FEE note] + C --> D[Authorize the transaction summary] + D --> E[Prove and submit the transaction] + E --> F[Include the transaction and collect the fee note] +``` + +The standard `AuthSingleSig` and `AuthMultisig` components perform these steps during authentication: + +1. Load the payment asset and conversion rate committed through the transaction's authentication arguments. The current client prepares the native asset at `1/1`. +2. Calculate the protocol fee from the reference block's base fee and the estimated cycle count. +3. Round the converted payment amount up and withdraw it from the account vault. +4. Create a public `TX_FEE` note containing the payment. +5. Authorize the transaction summary, which commits to both the vault withdrawal and the fee note. + +The fee note is always public, uses the `0xFEE` tag, and has no target account. Its assets and amounts are public even when the application's notes are private. Any account with the basic wallet interface can consume it, allowing the batch builder to claim the payment. When processing output notes, use the SDK's fee-note accessor or identify the standard script; a tag alone does not establish that a note is a protocol fee payment. + +## Choosing a payment asset + +The current Rust and Web clients prepare payment in the network's native fee asset at `1/1`. Use that path for ordinary transactions. + +At the protocol level, fee conversion supports a payment in another fungible asset: + +$$ +P_{\mathrm{asset}} = \left\lceil F_{\mathrm{native}} \times +\frac{r_{\mathrm{num}}}{r_{\mathrm{den}}} \right\rceil +$$ + +Both rate components must be nonzero field elements. For signature-based authentication, the signer commits to the selected asset's faucet ID, the conversion rate, and a salt through the authentication arguments. These cannot change after authorization. The client uses a fixed default salt for standard single-signature accounts so their summaries remain reproducible; multisig accounts require a caller-chosen salt for replay protection when paying a nonzero protocol fee. + +The protocol validates the commitment and conversion arithmetic; it does not determine a market price. Alternative-asset payment needs a compatible integration and an asset and rate accepted by the intended batch builder. Protocol support does not imply that a deployed builder accepts your token. The current client's native-payment builder does not select alternative assets. + +### Multisig and custom authentication + +Standard `AuthMultisig` only accepts the native fee asset and caps the payment at twice the computed native fee. The protocol's alternative-asset conversion support does not override that restriction. + +For a manually built multisig request, declare a fresh fee-conversion salt with Rust's `fee_conversion_salt(salt)`, or obtain a Web SDK builder with `await client.feeAwareTransactionRequestBuilder(account)`. Keep the request, salt, and reference-block anchor unchanged while collecting signatures for that proposal. Choose a new salt for a new proposal. The Web SDK's operations that build their own requests prepare the salt when needed. + +An explicit authentication argument makes the caller responsible for its contents. Custom authentication that reads conversion information must supply the matching commitment and advice data and invoke fee payment. Standard no-auth and network-account authentication pay natively without a caller-supplied conversion commitment. See [Authentication](../accounts/authentication.md#writing-a-custom-auth-component). + +## Zero-fee networks and failed transactions + +If the reference block's `verification_base_fee` is zero, standard authentication creates no `TX_FEE` note. Check the network's parameters rather than assuming that a development network has fees disabled. + +If a transaction fails locally during execution, fee payment, authentication, or proving, its output notes never become live and it transfers no onchain protocol fee. Local execution and proving can still consume resources. + +
+Fee withdrawal fails + +The executing account needs enough of the native fee asset in its vault or incoming notes. A balance of the tutorial's token is insufficient. + +
+ +
+The first P2ID funding note fails + +The account needs a compatible receiving interface and enough incoming funds to pay the consume's fee. + +
+ +
+A manual multisig request reports FeeConversionInfoRequired + +Declare a fresh fee-conversion salt for the proposal. + +
+ +
+Custom authentication cannot read fee conversion data + +Supply its expected commitment and advice data, and call the fee-payment procedure. + +
+ +
+The builder rejects the payment asset or rate + +Use an asset and rate supported by that builder. A transaction that is never included transfers no fee. + +
+ +
+Submission or confirmation times out + +Check the transaction ID's status. A timeout does not establish whether the transaction was included or its fee paid. + +
+ +## Network-account fees + +Network transactions introduce a second, separate fee. The network account pays the protocol transaction fee from its own vault, while its application fee policy prices each note that the network consumes on its behalf. + +| Fee | What it pays for | Who funds it | Payment mechanism | +|---|---|---|---| +| **Protocol transaction fee** | Verification and batch inclusion | The executing network account | Public `TX_FEE` note | +| **Network-account application fee** | The account's service and its protocol-fee cost | The sender of the targeted note, or an upstream network account | `FEE_SPONSORSHIP` note | + +A sponsorship note names the application note it funds. Its script requires that application note to be consumed in the same transaction; a separately configured reclaim path can return unused sponsorship funds. The network account's authentication procedure checks that each application note's fee is covered and collects the sponsorship assets into its vault before paying the protocol fee. + +During the sending transaction's standard authentication, each network output note is priced through the target account's fee policy. A nonzero application fee automatically creates a matching sponsorship note funded from the sender's vault; a zero application fee creates none. This automatic sponsorship uses the native fee asset and requires the target's nonzero fee to use that asset too. + +Setting an application fee to zero does not fund the network account's protocol fee. Conversely, a zero `verification_base_fee` does not necessarily make the application fee zero. That price comes from the network account's own policy. See [Network Accounts](../accounts/network-accounts.md) for configuration and sponsorship behavior. + +## Related + +- [Protocol fee implementation](https://github.com/0xMiden/protocol/blob/v0.16.0/crates/miden-protocol/asm/kernels/transaction-core/src/tx.masm): kernel-level fee calculation +- [Authentication](../accounts/authentication.md): fee payment in standard and custom auth procedures +- [Network Accounts](../accounts/network-accounts.md): application fee policies and sponsorship +- [What are Transactions?](./introduction.md): execution, proving, submission, and failure behavior + +:::info Source Reference +Protocol: [`compute_fee`](https://github.com/0xMiden/protocol/blob/v0.16.0/crates/miden-protocol/asm/kernels/transaction-core/src/tx.masm), [`FeeConversionInfo`](https://github.com/0xMiden/protocol/blob/v0.16.0/crates/miden-standards/src/account/auth/fee.rs), [`TxFeeNote`](https://github.com/0xMiden/protocol/blob/v0.16.0/crates/miden-standards/src/note/tx_fee.rs). + +Clients: [Rust request preparation](https://github.com/0xMiden/rust-sdk/blob/v0.16.0/crates/rust-client/src/transaction/mod.rs), [Web SDK transaction lifecycle](https://github.com/0xMiden/web-sdk/blob/v0.16.0/crates/web-client/js/types/api-types.d.ts). +::: diff --git a/versioned_docs/version-0.16/builder/smart-contracts/transactions/introduction.md b/versioned_docs/version-0.16/builder/smart-contracts/transactions/introduction.md new file mode 100644 index 00000000..792ca5b5 --- /dev/null +++ b/versioned_docs/version-0.16/builder/smart-contracts/transactions/introduction.md @@ -0,0 +1,84 @@ +--- +title: "What are Transactions?" +sidebar_position: 0 +description: "Transactions are Miden's execution unit — they consume input notes and produce output notes, executed locally and proven before submission." +--- + +# What are Transactions? + +Transactions are the execution unit in Miden. Every state change — transferring assets, updating storage, minting tokens — happens inside a transaction. Each transaction runs against a single account, consumes zero or more input notes, and produces zero or more output notes. + +The critical difference from other blockchains: transactions execute locally on the user's machine, not on a shared VM. After execution, the Miden VM generates a zero-knowledge proof that the transaction was valid and the client seals the transaction inputs (see [transaction design](../../../reference/protocol/transaction.md)). The proof, resulting state commitments, and sealed inputs are submitted to the network. The network does not receive private inputs in plaintext or see the account's private state and execution trace. + +## Anatomy of a transaction + +Every transaction has these elements: + +| Element | Description | +|---------|-------------| +| **Account** | The single account this transaction mutates — its storage, vault, and nonce | +| **Input notes** | Zero or more notes being consumed — their scripts run and explicitly remove any assets they move | +| **Output notes** | Zero or more notes being created — carrying assets and scripts for future consumption | +| **Transaction script** | Optional entry-point logic that runs in addition to note scripts and component code | +| **Block reference** | The chain state the transaction executes against — provides block number, timestamp, and commitments | + +A transaction can only modify one account. Cross-account interactions happen through notes: one account's transaction creates a note, another account's transaction consumes it. + +## Transaction lifecycle + +``` +┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ +│ Build │────▶│ Execute │────▶│ Prove │────▶│ Submit │────▶│ Verify │ +│ │ │ │ │ │ │ │ │ │ +│ Assemble │ │ VM runs │ │ ZK proof │ │ Proof + │ │ Network │ +│ tx inputs│ │ locally │ │ generated│ │ sealed │ │ updates │ +│ │ │ │ │ │ │ inputs │ │ state │ +└──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘ +``` + +1. **Build**: The client assembles the transaction — which account, which notes to consume, what methods to call. +2. **Execute**: The Miden VM runs the transaction locally. Note scripts execute, component code runs, storage is mutated, output notes are created. +3. **Prove**: The VM produces a zero-knowledge proof of correct execution. If any assertion fails (e.g., insufficient balance, unauthorized caller), the proof cannot be generated — the transaction is rejected before it ever reaches the network. +4. **Submit**: The proof, sealed transaction inputs, and public state updates (new note commitments, updated account commitment, and nullifiers for consumed notes) are submitted to the network. +5. **Verify**: The network verifies the proof, records the state changes, and includes the transaction in a batch and eventually a block. + +## The transaction context + +During execution, your code runs inside a **transaction context** that provides access to: + +- **Block data** — current block number, timestamp, and commitments via the `tx` module +- **Input notes** — the notes being consumed, their assets, recipient storage, and metadata +- **Account state** — the executing account's storage, vault, and nonce +- **Output notes** — the ability to create new notes and attach assets + +The transaction context is what connects your component code to the chain state. For example, you can implement time-based logic by comparing `tx::get_block_number()` against a stored value, or read note storage to determine what action to take. + +## What happens when execution fails + +When an assertion fails in your code: + +```rust +assert!(amount > felt!(0)); +``` + +The ZK circuit **cannot produce a valid proof**. This means: + +- The transaction is rejected **before it ever reaches the network** +- No state changes occur — it's as if the transaction never happened +- No gas or fees are consumed +- The client gets an error explaining which assertion failed + +This is fundamentally different from Ethereum's `revert`, where the failed transaction still lands onchain, consumes gas, and is visible to everyone. + +A separate failure mode is the **empty transaction**: a transaction that runs to completion but mutates no account state (storage, vault, or nonce) and consumes no input notes. The VM kernel rejects it during execution. This typically catches transaction scripts whose conditional logic takes a no-op branch — see [Empty Transaction](../../tutorials/helpers/pitfalls#empty-transaction-no-state-change-no-notes) in the pitfalls guide for the recommended pattern. + +## How transactions differ from EVM transactions + +| | EVM | Miden | +|---|---|---| +| **Execution** | Every validator re-executes the transaction | Client executes locally, then submits the proof and sealed inputs | +| **Scope** | Can call multiple contracts in one tx | One transaction mutates one account; cross-account via notes | +| **Privacy** | All inputs, state reads, and call traces are public | Private inputs are sealed before submission; the network verifies the proof and state commitments | +| **Failure** | Onchain revert, gas consumed, visible trace | Proof can't be generated — no onchain trace, no cost | +| **Parallelism** | Transactions touching same state must serialize | Single-account scope enables parallel execution | +| **Authentication** | `msg.sender` set by protocol | The account authentication procedure verifies the configured scheme, such as Falcon-512 Poseidon2 or ECDSA K256 Keccak | diff --git a/versioned_docs/version-0.16/builder/smart-contracts/transactions/transaction-context.md b/versioned_docs/version-0.16/builder/smart-contracts/transactions/transaction-context.md new file mode 100644 index 00000000..3079e393 --- /dev/null +++ b/versioned_docs/version-0.16/builder/smart-contracts/transactions/transaction-context.md @@ -0,0 +1,66 @@ +--- +title: "The tx Module" +sidebar_position: 2 +description: "Block queries, note commitments, and expiration management with the tx module." +--- + +# The tx Module + +A Miden transaction consumes zero or more input notes and produces state changes plus output notes for a single account. A client or network transaction builder executes the code and proves the result. Submission includes the proof, sealed inputs, and public state updates. The `tx` module provides access to block information, note commitments, and expiration controls. Transaction scripts (`#[tx_script]`) serve as standalone entry points that orchestrate the transaction. + +## The `tx` module + +```rust +use miden::{BlockNumber, Word, tx}; +``` + +### Block information + +```rust +// Current block number +let block_num: BlockNumber = tx::get_block_number(); + +// Block commitment (hash of block header) +let commitment: Word = tx::get_block_commitment(); + +// Block timestamp (seconds since epoch) +let timestamp: u32 = tx::get_block_timestamp(); +``` + +### Note commitments + +```rust +// Commitment over all input notes in this transaction +let input_commit: Word = tx::get_input_notes_commitment(); + +// Commitment over all output notes in this transaction +let output_commit: Word = tx::get_output_notes_commitment(); + +// Number of input/output notes +let num_inputs: u32 = tx::get_num_input_notes(); +let num_outputs: u32 = tx::get_num_output_notes(); +``` + +### Transaction expiration + +Control how long a transaction remains valid: + +```rust +// Get current expiration delta (in blocks) +let delta: u16 = tx::get_expiration_block_delta(); + +// Set a new expiration delta +tx::update_expiration_block_delta(100); +``` + +The expiration delta is measured from the transaction's reference block. A value of `0` means no expiration has been set; updates must be between `1` and `u16::MAX` and can only tighten an existing expiration limit. + +## Transaction scripts + +Transaction scripts use the `#[tx_script]` macro to define a top-level entry point for the transaction. See [Transaction Scripts](./transaction-scripts) for the full `#[tx_script]` API and examples. + +For signature verification using the transaction context, see [Authentication](../accounts/authentication). For time-based patterns using `tx::get_block_number()`, see [Patterns — Rate limiting](../patterns#rate-limiting). + +:::info API Reference +Full API docs on docs.rs: [`miden::tx`](https://docs.rs/miden/latest/miden/tx/) +::: diff --git a/versioned_docs/version-0.16/builder/smart-contracts/transactions/transaction-scripts.md b/versioned_docs/version-0.16/builder/smart-contracts/transactions/transaction-scripts.md new file mode 100644 index 00000000..40c0d8e7 --- /dev/null +++ b/versioned_docs/version-0.16/builder/smart-contracts/transactions/transaction-scripts.md @@ -0,0 +1,132 @@ +--- +title: "Transaction Scripts" +sidebar_position: 3 +description: "Write transaction scripts with #[tx_script] to orchestrate multi-note transactions and build output notes." +--- + +# Transaction Scripts + +A transaction script is a top-level function that runs once per transaction, after all note scripts have executed. Use it to orchestrate logic that spans multiple consumed notes — moving assets from the account vault into output notes, calling account methods via [cross-component calls](../cross-component-calls), or running anything that must happen after all note scripts finish. + +## `#[tx_script]` signature + +```rust +// With account access +#[tx_script] +fn run(arg: Word, account: &mut Wallet) { ... } + +// Without account access +#[tx_script] +fn run(arg: Word) { ... } +``` + +| Constraint | Details | +|------------|---------| +| Function name | Must be `run` (enforced by the macro) | +| Return type | `()` | +| Required arg | One `Word` (the script argument, passed by the transaction executor) | +| Optional arg | `&AccountWrapper` or `&mut AccountWrapper`, where `AccountWrapper` is declared with `#[account(package::Interface)]` | +| Generics | Not allowed | +| Async | Not allowed | + +## miden-project.toml + +```toml +[package] +name = "basic-wallet-tx-script" +version = "0.1.0" + +[lib] +kind = "tx-script" +namespace = "miden:base/transaction-script@1.0.0" +path = "src/lib.rs" + +[dependencies] +miden-core = "*" +miden-protocol = "*" +basic-wallet = { path = "../basic-wallet" } + +``` + +## Example: basic-wallet-tx-script + +This example decodes structured input from the advice map and asks the account's +wallet component to create the output note. `output_note::create` is restricted +to account-component context, so a transaction script cannot call it directly. + +```rust +// Do not link against libstd (i.e. anything defined in `std::`) +#![no_std] +#![feature(alloc_error_handler)] + +// However, we could still use some standard library types while +// remaining no-std compatible, if we uncommented the following lines: +// +// +// extern crate alloc; +// use alloc::vec::Vec; + +use miden::{account, adv_load_preimage, intrinsics::advice::adv_push_mapvaln, *}; + +#[account(basic_wallet::BasicWallet)] +pub struct Wallet; + +// Input layout constants +const TAG_INDEX: usize = 0; +const NOTE_TYPE_INDEX: usize = 1; +const RECIPIENT_START: usize = 2; +const RECIPIENT_END: usize = 6; +const ASSET_START: usize = 6; +const ASSET_END: usize = 14; + +#[tx_script] +fn run(arg: Word, account: &mut Wallet) { + let num_felts = adv_push_mapvaln(arg.clone()); + let num_felts_u64 = num_felts.as_canonical_u64(); + assert_eq!(Felt::from_u32((num_felts_u64 % 4) as u32), felt!(0)); + + let num_words = Felt::new(num_felts_u64 / 4).unwrap(); + let commitment = arg; + let input = adv_load_preimage(num_words, commitment); + + let tag = input[TAG_INDEX]; + let note_type = input[NOTE_TYPE_INDEX]; + let recipient: [Felt; 4] = + input[RECIPIENT_START..RECIPIENT_END].try_into().unwrap(); + + let note_idx = + account.create_note(tag.into(), note_type.into(), recipient.into()); + + // Contract-side assets contain an ID word followed by a value word. + let asset: [Felt; 8] = input[ASSET_START..ASSET_END].try_into().unwrap(); + let asset_key: [Felt; 4] = asset[..4].try_into().unwrap(); + let asset_value: [Felt; 4] = asset[4..].try_into().unwrap(); + let asset = Asset::new(asset_key, asset_value); + + account.move_asset_to_note(asset, note_idx); +} +``` + +### Walkthrough + +1. **`arg: Word`** is the commitment used to look up the structured input in the advice map. +2. **`adv_push_mapvaln(arg)`** loads the preimage length, and `adv_load_preimage(...)` retrieves the tag, note type, recipient, and two-word asset. +3. **`account.create_note(...)`** crosses into the installed wallet component, where note creation is permitted. +4. **`account.move_asset_to_note(...)`** removes the asset from the account vault and attaches it to the new note. + +:::note +Host code must insert a 16-felt, word-aligned preimage into the advice map: +tag (1), note type (1), recipient (4), asset (8), and two zero padding felts. +Hash all 16 felts and pass that commitment as the transaction-script argument. +Keep the host and guest field order in sync. +::: + +:::tip +`adv_push_mapvaln` and `adv_load_preimage` are part of the advice provider — the mechanism for supplying auxiliary data to a transaction. See [Advice Provider](./advice-provider) for the full function reference. +::: + +## Related + +- [Transaction Context](./transaction-context) — `tx` module (block info, note commitments, expiration) +- [Cross-Component Calls](../cross-component-calls) — how `&mut Account` works in tx scripts +- [Reading Notes](../notes/reading-notes) — reading input notes by index inside tx scripts diff --git a/versioned_docs/version-0.16/builder/smart-contracts/types.md b/versioned_docs/version-0.16/builder/smart-contracts/types.md new file mode 100644 index 00000000..e19c7735 --- /dev/null +++ b/versioned_docs/version-0.16/builder/smart-contracts/types.md @@ -0,0 +1,299 @@ +--- +title: "Types" +sidebar_position: 5.6 +description: "Felt field arithmetic, Word layout, Asset encoding, and type conversions in the Miden Rust SDK." +--- + +# Types + +Miden's type system is built around field elements rather than standard integers. All computation inside the Miden VM is modular arithmetic over the Goldilocks prime field ($p = 2^{64} - 2^{32} + 1$), so overflow and division behave differently from standard integers. `Felt` is the native numeric type, `Word` is a tuple of four Felts used for [storage](./accounts/storage) and hashing, and `Asset` encodes fungible and non-fungible assets as Words. + +## Felt — Field elements + +`Felt` is the fundamental numeric type in Miden. It represents an element of the **Goldilocks prime field**: + +$$ +p = 2^{64} - 2^{32} + 1 = 18446744069414584321 +$$ + +:::warning This is not integer arithmetic +`Felt` uses **modular arithmetic**. Values wrap around the prime modulus, not at `u64::MAX`. Addition, subtraction, and multiplication all happen modulo $p$. Division computes the **multiplicative inverse**, not integer division. +::: + +### Creating Felt values + +```rust +use miden::{felt, Felt}; + +// Literal construction (validated when evaluated) +let zero = felt!(0); +let one = felt!(1); +let answer = felt!(42); + +// From u32 (always safe) +let f = Felt::from_u32(255); + +// From u64 (fallible if the value is outside the field) +let f = Felt::new(1_000_000_000).unwrap(); + +// Built-in zero / one constants +let z = Felt::ZERO; +let o = Felt::ONE; +``` + +:::info `felt!()` range limitation +The `felt!()` macro accepts integer literals representable as `u64` and +validates them through `Felt::new(...).unwrap()`. An out-of-field literal +panics when evaluated; it is not currently rejected by `cargo check`. For +runtime values, use the fallible `Felt::new(...)` and handle the error. +::: + +### Arithmetic + +```rust +let a = felt!(10); +let b = felt!(3); + +// Standard arithmetic (modular) +let sum = a + b; // felt!(13) +let diff = a - b; // felt!(7) +let prod = a * b; // felt!(30) +let neg = -a; // p - 10 + +// Division computes multiplicative inverse +// a / b = a * b^(-1) mod p +let quot = a / b; // NOT integer 3 — it's 10 * inverse(3) mod p + +// In-place operators +let mut x = felt!(5); +x += felt!(1); // x is now felt!(6) +x *= felt!(2); // x is now felt!(12) +``` + +:::note For business logic, prefer u64 +For computing amounts, balances, counters, or any value where overflow/underflow behavior matters, convert to `u64` first, perform the arithmetic, then convert back with `Felt::new(...).unwrap()` or explicit error handling. +::: + +### Comparison and conversion + +```rust +let f = felt!(42); + +// Convert to u64 (canonical representation) +let n: u64 = f.as_canonical_u64(); + +// Equality comparison +if f == felt!(42) { /* ... */ } + +// For numeric comparisons, convert to u64 first +if f.as_canonical_u64() > 100 { /* ... */ } + +// Check parity +if f.is_odd() { /* ... */ } +``` + +:::tip Integer arithmetic on Felt values +Converting `Felt` to `u64` with `.as_canonical_u64()` gives you standard Rust integer arithmetic — with overflow and underflow protection from Rust's debug-mode checks and `saturating_*` / `checked_*` methods. For business logic involving amounts, limits, or counters, prefer `u64` arithmetic: + +```rust +// Convert, compute in u64, convert back +let a: u64 = felt_a.as_canonical_u64(); +let b: u64 = felt_b.as_canonical_u64(); +let sum = a.saturating_add(b); // safe addition +let diff = a.saturating_sub(b); // no underflow +let result = Felt::new(sum).unwrap(); +``` + +Saturating arithmetic prevents `u64` overflow, but the result can still be outside the field. The final `unwrap()` panics if `sum >= p`; handle that conversion explicitly when the inputs are not bounded. +::: + +### Advanced operations + +```rust +let f = felt!(7); + +// Multiplicative inverse: f * f.inv() == felt!(1) +let inv = f.inv(); // Panics if f == felt!(0) + +// Exponentiation: base^exponent mod p +let result = f.exp(felt!(3)); // 7^3 mod p = 343 + +// Squaring: f^2 +let square = f * f; // 7^2 mod p = 49 +``` + +:::caution Squaring with SDK 0.14.0 +With `miden` 0.14.0 and `midenc` 0.10.0, `Felt::square()` is lowered to the VM's `pow2` operation, which computes `2^f`: `felt!(7).square()` returns 128. Use `f * f` to compute the square in this version. +::: + +## Word — Four field elements + +A `Word` holds four `Felt` values. It's the standard unit for storage, hashing, and data passing in Miden. + +```rust +#[repr(C)] +pub struct Word { + pub a: Felt, + pub b: Felt, + pub c: Felt, + pub d: Felt, +} +``` + +### Creating Words + +```rust +use miden::{felt, Felt, Word}; + +// From an array of 4 Felts +let w = Word::new([felt!(1), felt!(2), felt!(3), felt!(4)]); + +// Shorthand via `From<[Felt; 4]>` +let w: Word = [felt!(1), felt!(2), felt!(3), felt!(4)].into(); + +// Shorthand via `From<[u32; 4]>` (or [u8; 4] / [u16; 4] / [bool; 4]) +let w = Word::from([1u32, 2, 3, 4]); + +// All-zero word +let z = Word::empty(); // same as Word::default() +``` + +### Indexing + +```rust +let w = Word::new([felt!(10), felt!(20), felt!(30), felt!(40)]); + +// Named fields +let a: Felt = w.a; // felt!(10) +let d: Felt = w.d; // felt!(40) + +// Convert to array +let arr: [Felt; 4] = w.into_elements(); +// or via the `From for [Felt; 4]` impl +let arr2: [Felt; 4] = w.into(); +``` + +### Packing data into Words + +Since each storage slot holds one `Word`, you'll often pack multiple values: + +```rust +// Pack two u64 values into a Word +let config = Word::new([ + Felt::new(max_amount).unwrap(), // a: max amount + Felt::new(cooldown).unwrap(), // b: cooldown blocks + felt!(0), // c: unused + felt!(0), // d: unused +]); + +// Unpack via named fields +let max_amount = config.a.as_canonical_u64(); +let cooldown = config.b.as_canonical_u64(); +``` + +## Asset + +`Asset` represents either a fungible or non-fungible asset. In contract code it is **two words** — a `key` (the asset ID used by the vault) and a `value` (encoding the fungible amount or non-fungible data). + +```rust +pub struct Asset { + pub key: Word, + pub value: Word, +} +``` + +### Encoding + +**Fungible assets** (tokens): + +| Word | Field | Content | +|----------|-------|---------| +| `key` | `a` | `0` | +| `key` | `b` | `0` | +| `key` | `c` | Faucet ID suffix plus metadata byte | +| `key` | `d` | Faucet ID prefix | +| `value` | `a` | Amount | +| `value` | `b` | `0` | +| `value` | `c` | `0` | +| `value` | `d` | `0` | + +**Non-fungible assets** (NFTs): + +| Word | Field | Content | +|----------|-------|---------| +| `key` | `a` | Data hash element 0 | +| `key` | `b` | Data hash element 1 | +| `key` | `c` | Faucet ID suffix plus metadata byte | +| `key` | `d` | Faucet ID prefix | +| `value` | `a..d`| Data hash elements 0–3 | + +The low metadata byte in `key.c` encodes `AssetComposition` in bits 0-1. Whether assets invoke callbacks is encoded in the faucet account ID when that account is built. Use protocol helpers instead of hand-decoding the metadata. + +### Working with assets + +```rust +use miden::{Asset, Word, felt}; + +// Build a fungible asset from key + value words. +// Fungible key = [0, 0, faucet_suffix_with_metadata, faucet_prefix], +// fungible value = [amount, 0, 0, 0]. +let asset = Asset::new( + Word::from([felt!(0), felt!(0), faucet_suffix_with_metadata, faucet_prefix]), + Word::from([felt!(100), felt!(0), felt!(0), felt!(0)]), +); + +// Read the amount (fungible): first limb of `value`. +let amount: u64 = asset.value.a.as_canonical_u64(); + +// Assets passed into contract procedures are already constructed by the host. +// Read the ID word when querying the active account vault. +let asset_id: Word = asset.key; +let is_present = miden::active_account::has_asset(asset_id); +``` + +:::note Asset on the host side +On the client / host side, `Asset` is an enum (`Asset::Fungible(_) | Asset::NonFungible(_)`) exposed from `miden-protocol`, with `id()` / `to_id_word()` / `to_value_word()` / `from_id_and_value_words()` helpers. Inside a Rust contract the SDK exposes the two-word `Asset` struct shown above. +::: + +## AccountId + +Identifies an account with two Felt values: + +```rust +pub struct AccountId { + pub prefix: Felt, + pub suffix: Felt, +} +``` + +```rust +use miden::AccountId; + +let id = AccountId::new(prefix_felt, suffix_felt); + +// Use in comparisons +let current: AccountId = self.get_id(); +assert_eq!(current.prefix, expected.prefix); +assert_eq!(current.suffix, expected.suffix); +``` + +## Other types + +The SDK also provides `NoteIdx`, `Tag`, `NoteType`, `Recipient`, `Digest`, and `StorageSlotId`. See the [full API docs on docs.rs](https://docs.rs/miden/latest/miden/) for their definitions. +## Type conversion table + +| From | To | Method | +|------|----|--------| +| `u32` | `Felt` | `Felt::from_u32(n)` | +| `u64` | `Felt` | `Felt::new(n).unwrap()` | +| literal | `Felt` | `felt!(n)` | +| `Felt` | `u64` | `f.as_canonical_u64()` | +| `[Felt; 4]` | `Word` | `Word::new(arr)` or `Word::from(arr)` | +| `[u32; 4]` / `[u16; 4]` / `[u8; 4]` / `[bool; 4]` | `Word` | `Word::from(arr)` | +| `Word` | `[Felt; 4]` | `w.into_elements()` or `let arr: [Felt; 4] = w.into()` | + +Use these types in [component definitions](./accounts/components), store and retrieve Words from [persistent storage](./accounts/storage), or define your own types for public APIs with [`#[export_type]`](./accounts/custom-types). + +:::info API Reference +Full API docs on docs.rs: [`Felt`](https://docs.rs/miden/latest/miden/struct.Felt.html), [`Word`](https://docs.rs/miden/latest/miden/struct.Word.html), [`Asset`](https://docs.rs/miden/latest/miden/struct.Asset.html) +::: diff --git a/versioned_docs/version-0.16/builder/tools/_category_.json b/versioned_docs/version-0.16/builder/tools/_category_.json new file mode 100644 index 00000000..6c5100b8 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tools/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Tools", + "position": 6 +} diff --git a/versioned_docs/version-0.16/builder/tools/bridging/_category_.yml b/versioned_docs/version-0.16/builder/tools/bridging/_category_.yml new file mode 100644 index 00000000..330c8da6 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tools/bridging/_category_.yml @@ -0,0 +1,2 @@ +label: Bridging +position: 4 diff --git a/versioned_docs/version-0.16/builder/tools/bridging/agglayer.md b/versioned_docs/version-0.16/builder/tools/bridging/agglayer.md new file mode 100644 index 00000000..4565c6f7 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tools/bridging/agglayer.md @@ -0,0 +1,321 @@ +--- +title: Agglayer on Miden +description: Integrate Agglayer asset transfers between Sepolia and Miden. +sidebar_position: 2 +--- + +# Agglayer on Miden + +:::warning v0.16 snapshot compatibility +This guide is frozen from a bridge portal revision that still uses Miden SDK v0.15. Confirm compatible provider deployments and validate an end-to-end transfer on your target network; this snapshot does not establish v0.16 interoperability. +::: + +Agglayer is the bridge provider. A Miden application builds the source-chain +action, persists the bridge identifiers, and follows the transfer until the +destination transaction settles. + + +Agglayer's documentation defines the Unified Bridge contract, proof model, and +generic bridge services. This guide owns the Miden-specific integration: +destination encoding, `B2AGG` notes, the current Miden-side service, and wallet +note consumption. + + +The current reference integration supports: + +| Direction | Source asset | Destination asset | Typical testnet time | +| ---------------- | ------------------ | ----------------- | -------------------- | +| Sepolia → Miden | Native Sepolia ETH | Miden ETH | 10–20 minutes | +| Miden → Sepolia | Miden ETH | Sepolia ETH | 10–20 minutes | + +These timings are not guarantees. They include source finality, Gateway +observation, exit-root and proof propagation, destination delivery, and—when +receiving on Miden—wallet note synchronization. + +## Dependencies + +```bash +npm install @miden-sdk/miden-sdk \ + @miden-sdk/miden-wallet-adapter-base \ + @miden-sdk/miden-wallet-adapter-react \ + viem +``` + +Use mutually compatible Miden SDK and wallet-adapter versions. The reference +implementation is currently verified with `@miden-sdk/miden-sdk@0.15.7`, +`@miden-sdk/miden-wallet-adapter-base@0.15.1`, and +`@miden-sdk/miden-wallet-adapter-react@0.15.1`. + +## Current testnet parameters + + +Keep deployment parameters in one application configuration object and recheck +them before a funded testnet run. Rollup IDs, bridge accounts, faucets, and +service endpoints can change. + + +```typescript +export const AGGLAYER_BALI = { + sepoliaChainId: 11155111, + sepoliaBridgeAddress: "0x1348947e282138d8f377b467f7d9c2eb0f335d1f", + midenNetworkId: 78, + evmNetworkId: 0, + nativeTokenAddress: "0x0000000000000000000000000000000000000000", + midenBridgeId: "0xa22ec154f9a36d911953fd5c9260a7", + midenEthFaucetId: "0x387149ae66116cf114eebd60bb7381", + bridgeServiceApi: + "https://miden-testnet-bridge.dev.eu-north-3.gateway.fm/api", +} as const; +``` + +## Sepolia → Miden + +```mermaid +sequenceDiagram + participant App + participant EVM as Sepolia wallet + participant Bridge as Agglayer bridge + participant Gateway + participant Miden as Miden bridge + participant Wallet as Miden wallet + + App->>EVM: Submit bridgeAsset + EVM->>Bridge: Deposit ETH + Bridge-->>Gateway: Deposit and exit-root data + Gateway->>Miden: Submit verified claim + Miden-->>Wallet: Deliver P2ID note + Wallet->>Wallet: User consumes note +``` + +### 1. Encode the Miden destination + +Agglayer represents the Miden account inside a 20-byte EVM address. Start from +the 30-hex-character Miden account ID: + +```typescript +export function midenAccountToBridgeDestination(accountId: string) { + const normalized = accountId.trim().replace(/^0x/, ""); + if (!/^[0-9a-fA-F]{30}$/.test(normalized)) { + throw new Error("Expected a 15-byte Miden account ID."); + } + + return `0x00000000${normalized.toLowerCase()}00` as `0x${string}`; +} +``` + +If the wallet returns a bech32 address, convert it to an `AccountId` with the +Miden SDK first. Do not accept an arbitrary EVM address as a Miden account: the +mapping is constrained and only the embedded format decodes successfully. + +### 2. Build `bridgeAsset` + +```typescript +import { encodeFunctionData, parseEther, toHex } from "viem"; + +const bridgeAssetAbi = [ + { + type: "function", + name: "bridgeAsset", + stateMutability: "payable", + inputs: [ + { name: "destinationNetwork", type: "uint32" }, + { name: "destinationAddress", type: "address" }, + { name: "amount", type: "uint256" }, + { name: "token", type: "address" }, + { name: "forceUpdateGlobalExitRoot", type: "bool" }, + { name: "permitData", type: "bytes" }, + ], + outputs: [], + }, +] as const; + +export function buildBridgeInTransaction( + amountEth: string, + midenAccountId: string, +) { + const amount = parseEther(amountEth); + const destinationAddress = + midenAccountToBridgeDestination(midenAccountId); + + return { + to: AGGLAYER_BALI.sepoliaBridgeAddress, + data: encodeFunctionData({ + abi: bridgeAssetAbi, + functionName: "bridgeAsset", + args: [ + AGGLAYER_BALI.midenNetworkId, + destinationAddress, + amount, + AGGLAYER_BALI.nativeTokenAddress, + true, + "0x", + ], + }), + value: toHex(amount), + gas: toHex(BigInt(300000)), + destinationAddress, + }; +} +``` + +Submit the returned transaction through the connected Sepolia wallet. Persist +both the source transaction hash and `destinationAddress`; the embedded +destination is the key used to query deposits. + +### 3. Track destination delivery + +```typescript +export async function fetchAgglayerDeposits(destinationAddress: string) { + const response = await fetch( + `${AGGLAYER_BALI.bridgeServiceApi}/bridges/${destinationAddress}?limit=10&offset=0`, + { cache: "no-store" }, + ); + + if (!response.ok) { + throw new Error(`Agglayer bridge status ${response.status}`); + } + + return (await response.json()).deposits; +} +``` + +Match the row by the source transaction hash instead of assuming the newest +deposit belongs to the current transfer. When `claim_tx_hash` appears, the +Gateway service has created the destination note on Miden. + +That bridge claim does **not** make the asset immediately spendable. Prompt the +Miden wallet to sync, show the delivered note, and let the recipient consume it. + +## Miden → Sepolia + +```mermaid +sequenceDiagram + participant App + participant Wallet as Miden wallet + participant Miden as Miden bridge + participant Gateway + participant Bridge as Sepolia bridge + participant EVM as Sepolia wallet + + App->>Wallet: Request B2AGG transaction + Wallet->>Miden: Submit B2AGG note + Miden-->>Gateway: Append exit leaf + Gateway->>Bridge: Auto-claim ready exit + Bridge-->>EVM: Release ETH +``` + +### 1. Create the `B2AGG` note + +```typescript +import { + AccountId, + AssetCallbackFlag, + EthAddress, + FungibleAsset, + Note, + NoteArray, + NoteAssets, + TransactionRequestBuilder, +} from "@miden-sdk/miden-sdk"; +import { Transaction } from "@miden-sdk/miden-wallet-adapter-base"; + +export async function createAgglayerBridgeOut({ + amount, + destinationAddress, + senderAddress, + requestTransaction, + waitForTransaction, +}: { + amount: bigint; + destinationAddress: string; + senderAddress: string; + requestTransaction: (transaction: Transaction) => Promise; + waitForTransaction: (requestId: string) => Promise<{ txHash: string }>; +}) { + const sender = AccountId.fromBech32(senderAddress); + const bridge = AccountId.fromHex(AGGLAYER_BALI.midenBridgeId); + const faucet = AccountId.fromHex(AGGLAYER_BALI.midenEthFaucetId); + + const asset = new FungibleAsset(faucet, amount).withCallbacks( + AssetCallbackFlag.Enabled, + ); + const note = Note.createB2AggNote( + sender, + bridge, + new NoteAssets([asset]), + AGGLAYER_BALI.evmNetworkId, + EthAddress.fromHex(destinationAddress), + ); + const request = new TransactionRequestBuilder() + .withOwnOutputNotes(new NoteArray([note])) + .build(); + const transaction = Transaction.createCustomTransaction( + senderAddress, + senderAddress, + request, + ); + + const requestId = await requestTransaction(transaction); + const output = await waitForTransaction(requestId); + return output.txHash; +} +``` + +The callback flag is required. Constructing the asset without +`AssetCallbackFlag.Enabled` changes its commitment and the bridge transaction +cannot remove the callback-enabled asset held by the wallet. + +The wallet adapter initially returns a request UUID. Wait for settlement and +persist `output.txHash`; a request UUID is not a Midenscan transaction hash. + +### 2. Track the auto-claim + +Query the bridge indexer with the Sepolia destination address and match a row +whose origin is Miden network `78` and destination is EVM network `0`. A +populated `claim_tx_hash` means Gateway auto-claimed the exit on Sepolia. + +Do not build a manual `claimAsset(...)` button for the current reference flow. + +## State and recovery + +Store at least: + +- direction; +- source transaction hash; +- source and destination network IDs; +- embedded Miden destination or Sepolia recipient; +- bridge deposit count when available; +- destination claim transaction hash. + +Render source settlement, bridge observation, destination settlement, and +Miden note consumption as separate states. A single `complete` boolean cannot +represent the recovery steps a user may still need. + +## Trust boundary + +The onchain Miden bridge verifies Global Exit Roots and Merkle proofs and +prevents duplicate claims. An offchain integration service still observes the +Agglayer state and creates the Miden update and claim notes. Your application +should expose that service dependency and testnet status. + +## Continue with Agglayer + + + + Understand `bridgeAsset`, `claimAsset`, token handling, and proof + verification. + + + Query bridge transactions, claim status, network data, and claim proofs. + + + Index transactions across networks and model `BRIDGED`, + `READY_TO_CLAIM`, and `CLAIMED`. + + + +### Miden implementation references + +- [Agglayer integration in the bridge portal](https://github.com/0xMiden/bridge-portal/tree/main/src/app/lib) +- [Miden Agglayer protocol specification](https://github.com/0xMiden/protocol/blob/next/crates/miden-agglayer/SPEC.md) +- [Gateway bridge monitor](https://gateway-fm.github.io/miden-agglayer/bridge-monitor/bali/) diff --git a/versioned_docs/version-0.16/builder/tools/bridging/epoch.md b/versioned_docs/version-0.16/builder/tools/bridging/epoch.md new file mode 100644 index 00000000..ac0da562 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tools/bridging/epoch.md @@ -0,0 +1,378 @@ +--- +title: Epoch on Miden +description: Integrate Epoch intents between Miden and Ethereum Sepolia. +sidebar_position: 3 +--- + +# Epoch on Miden + +:::warning v0.16 snapshot compatibility +This guide is frozen from a bridge portal revision that still uses Miden SDK v0.15. Confirm compatible allocator, faucet and solver deployments and validate a complete bridge round trip on your target network; this snapshot does not establish v0.16 interoperability. +::: + +Epoch gives a Miden application an intent-based bridge surface: request a +quote, let the user authorize the source collateral, submit the intent, and +track the solver's destination transaction. + +The current reference integration supports Epoch test USDC between Miden +testnet and Ethereum Sepolia in both directions, with a typical testnet time of +1–3 minutes. + + +Epoch's SDK documentation defines the provider workflow. This guide owns the +Miden-specific integration: virtual chain configuration, P2IDE collateral, +wallet callbacks, destination-aware completion, and note recovery. + + + +The Sepolia token in this integration is Epoch's 18-decimal test token, not +Circle's canonical Sepolia USDC. It maps to a 6-decimal USDC faucet on Miden. +Do not interchange the addresses or decimal assumptions. + + +## Why Epoch is faster + +Epoch's allocator and solver coordinate the destination leg after the source +collateral is authorized. The application does not wait for the canonical +Agglayer exit and claim lifecycle, which produces a faster testnet experience. + +The tradeoff is an additional provider boundary: quoting, observation, solver +availability, and destination fulfillment depend on Epoch services. Treat the +1–3 minute range as an estimate, not an SLA. + +## Dependencies + +```bash +npm install @epoch-protocol/epoch-intents-sdk \ + @miden-sdk/miden-sdk \ + @miden-sdk/miden-wallet-adapter-base \ + @miden-sdk/miden-wallet-adapter-react \ + viem +``` + +The snippets are type-checked against +`@epoch-protocol/epoch-intents-sdk@1.0.30`, +`@miden-sdk/miden-sdk@0.15.7`, Miden wallet adapter `0.15.1`, and +`viem@2.51.0`. + +The current reference uses: + +```typescript +export const EPOCH_ALLOCATOR_URL = + "https://testnet-dev.epochprotocol.xyz"; +export const MIDEN_CHAIN_ID = 999999999; +export const SEPOLIA_CHAIN_ID = 11155111; +export const RECLAIM_WINDOW_BLOCKS = 1000; +``` + +## Initialize the SDK per direction + +The wallet client chain ID tells Epoch which side supplies the collateral: + +- Use virtual chain ID `999999999` for Miden → EVM. +- Keep the real Sepolia chain ID for EVM → Miden. + +```typescript +import { EpochIntentSDK } from "@epoch-protocol/epoch-intents-sdk"; +import { + createWalletClient, + custom, + type Chain, + type EIP1193Provider, +} from "viem"; +import { sepolia } from "viem/chains"; + +export function createEpochSdk( + account: `0x${string}`, + provider: EIP1193Provider, + source: "miden" | "sepolia", +) { + const chain: Chain = + source === "miden" + ? { ...sepolia, id: MIDEN_CHAIN_ID } + : sepolia; + const walletClient = createWalletClient({ + account, + chain, + transport: custom(provider), + }); + + return new EpochIntentSDK({ + apiBaseUrl: EPOCH_ALLOCATOR_URL, + walletClient, + }); +} +``` + +For Miden-source flows the EVM account acts as the intent sponsor; it does not +supply the collateral. Do not reuse the virtual-chain override for EVM-source +intents. The reverse direction signs against the connected Sepolia wallet and +therefore needs chain ID `11155111`. + +## Miden → EVM + +```mermaid +sequenceDiagram + participant App + participant Epoch as Epoch allocator + participant Wallet as Miden wallet + participant Solver + participant EVM as Sepolia + + App->>Epoch: getTaskData + getIntentQuote + App->>Epoch: solveIntent + Epoch->>App: Request Miden collateral callback + App->>Wallet: Create public P2IDE note + Wallet-->>App: Transaction hash + note ID + Solver->>EVM: Fulfill destination transfer + App->>Epoch: Poll destination status +``` + +### 1. Compute a reclaim height + +The intent declares an **absolute** Miden block height, while the wallet adapter +uses a **relative** recall window. Derive both from the same chain tip: + +```typescript +const currentBlock = await getCurrentMidenBlock(); +const reclaimHeight = currentBlock + RECLAIM_WINDOW_BLOCKS; +``` + +Never hard-code `1000` as the absolute reclaim height. On a chain above that +height, the note would be reclaimable as soon as it was created. + +### 2. Build and quote the intent + +```typescript +import { TaskType } from "@epoch-protocol/epoch-intents-sdk"; + +const task = await sdk.getTaskData({ + taskType: TaskType.GetTokenOut, + intentData: { + isNative: false, + depositTokenAddress: ZERO_ADDRESS, + tokenInAmount: midenAmountInBaseUnits, + outputTokenAddress: epochSepoliaUsdcAddress, + minTokenOut: minimumEvmOutput, + destinationChainId: String(SEPOLIA_CHAIN_ID), + protocolHashIdentifier: ZERO_HASH, + recipient: evmRecipient, + }, + extraDataTypestring: + "string midenSourceAccount,string midenFaucetId,string midenNoteType,string midenNoteId,uint256 midenReclaimHeight", + extraData: { + midenSourceAccount, + midenFaucetId, + midenNoteType: "P2IDE", + midenNoteId: "", + midenReclaimHeight: String(reclaimHeight), + }, +}); + +const quote = await sdk.getIntentQuote({ + sponsorAddress: evmRecipient, + taskTypeString: task.taskTypeString, + intentData: task.intentData, + isNative: false, +}); +``` + +Amounts in the intent envelope are base-unit strings. Do not apply display +decimals twice. + +### 3. Create the Miden collateral note + +Epoch calls `createMidenP2IDNote` during `solveIntent`. The callback must create +a **public, reclaimable P2IDE** note so the solver can observe it and the user +can recover funds if the intent expires. + +```typescript +import { + AccountId, + AccountInterface, + NetworkId, +} from "@miden-sdk/miden-sdk"; + +function toTestnetAccountAddress(value: string) { + return value.startsWith("0x") + ? AccountId.fromHex(value).toBech32( + NetworkId.testnet(), + AccountInterface.BasicWallet, + ) + : value; +} + +const createMidenP2IDNote = async ( + faucetId: string, + amount: string, + allocatorId: string, +) => { + const amountBaseUnits = BigInt(amount); + if (amountBaseUnits > BigInt(Number.MAX_SAFE_INTEGER)) { + return { success: false }; + } + + const requestId = await requestSend({ + senderAddress: midenSender, + recipientAddress: toTestnetAccountAddress(allocatorId), + faucetId: toTestnetAccountAddress(faucetId), + noteType: "public", + amount: Number(amountBaseUnits), + recallBlocks: RECLAIM_WINDOW_BLOCKS, + }); + const output = await waitForTransaction(requestId); + const noteId = output.outputNotes?.[0]?.id().toString(); + + return noteId + ? { success: true, noteId } + : { success: false }; +}; +``` + +The `Number.MAX_SAFE_INTEGER` guard must run after parsing the SDK's base-unit +string and before converting it for the wallet adapter. Otherwise JavaScript +can silently round the collateral amount and make it differ from the intent. + +### 4. Solve and track the destination + +```typescript +import { CollateralType } from "@epoch-protocol/epoch-intents-sdk"; + +const result = await sdk.solveIntent({ + isNative: false, + sponsorAddress: evmRecipient, + taskTypeString: task.taskTypeString, + intentData: task.intentData, + quoteResult: quote, + collateralType: CollateralType.Miden, + midenFaucetId, + midenSourceAccount, + createMidenP2IDNote, +}); +``` + +Persist the sponsor address and intent nonce returned by the solve result. +Poll `getIntentStatus(sponsorAddress, nonce)` and mark the transfer complete +only after a successful Sepolia row has a destination transaction hash and no +Sepolia row remains pending. + +## EVM → Miden + +```mermaid +sequenceDiagram + participant App + participant Epoch as Epoch allocator + participant EVM as Sepolia wallet + participant Compact + participant Solver + participant Miden + + App->>Epoch: getTaskData + getIntentQuote + App->>Epoch: solveIntent with EVM collateral + Epoch->>EVM: Request approval and deposit + EVM->>Compact: depositERC20AndRegister + Solver->>Miden: Deliver P2ID note + App->>Epoch: Poll destination status +``` + +### 1. Build the reverse task + +```typescript +const task = await sdk.getTaskData({ + taskType: TaskType.GetTokenOut, + intentData: { + isNative: false, + depositTokenAddress: epochSepoliaUsdcAddress, + tokenInAmount: evmAmountInBaseUnits, + outputTokenAddress: ZERO_ADDRESS, + minTokenOut: minimumMidenOutput, + destinationChainId: String(MIDEN_CHAIN_ID), + protocolHashIdentifier: ZERO_HASH, + recipient: evmSourceAddress, + }, + extraDataTypestring: + "string midenRecipientAccount,string midenFaucetId,string midenNoteType", + extraData: { + midenRecipientAccount, + midenFaucetId, + midenNoteType: "P2ID", + }, +}); +``` + +Use `P2ID`, not `P2IDE`, for the destination note. This direction delivers to +the Miden recipient rather than creating reclaimable Miden-side collateral. + +### 2. Quote and solve with EVM collateral + +```typescript +const quote = await sdk.getIntentQuote({ + sponsorAddress: evmSourceAddress, + taskTypeString: task.taskTypeString, + intentData: task.intentData, + isNative: false, +}); + +const result = await sdk.solveIntent({ + isNative: false, + sponsorAddress: evmSourceAddress, + taskTypeString: task.taskTypeString, + intentData: task.intentData, + quoteResult: quote, + collateralType: CollateralType.EVM, +}); +``` + +The SDK may first request an ERC-20 approval and then +`depositERC20AndRegister` against Compact. Surface those as separate wallet +phases so the interface does not appear frozen. + +Poll the intent until the Miden destination row settles. An intermediate +Compact or allocator success is not destination completion. + +## Recovery + +Persist the source transaction hash, sponsor address, intent nonce, direction, +and destination chain ID. The SDK also exposes recovery operations for failed +or cancelled flows: + +- `retryIntentSolve` retries a transient solver failure. +- `disableForcedWithdrawal` clears a pending forced-withdrawal state before + reusing a Compact deposit. +- `withdrawToken` reclaims an unfulfilled EVM-side deposit. +- `initateDepositWithdrawal` starts forced withdrawal. The exported method name + currently contains that spelling. + +Do not hide these states behind a generic "try again" button. Show which +resource is locked and which recovery transaction will be signed. + +## Frontend integration constraints + +- Await `waitForTransaction` before reading a Miden output note ID. +- Keep P2IDE collateral public so the allocator can observe it. +- Dynamically import Miden/Epoch execution code in SSR applications because the + Miden SDK initializes WASM eagerly. +- Do not enable COOP/COEP with the current wallet-popup and Miden gRPC-Web + transport stack. +- Use a destination-aware polling reducer; source settlement is not completion. + +## Continue with Epoch + + + + Review Epoch's current Miden task data, collateral model, and supported + provider workflow. + + + Check current SDK initialization, quoting, submission, status, and recovery + APIs. + + + +### Miden implementation references + +- [Epoch integration in the bridge portal](https://github.com/0xMiden/bridge-portal/tree/main/src/app/lib/epoch) +- [Full Epoch bridging tutorial](../../tutorials/recipes/web/bridging_with_epoch_tutorial.md) +- [Runnable bridging application](https://github.com/0xMiden/tutorials/tree/main/examples/bridging-app) +- [Epoch SDK reference](https://docs.epochprotocol.xyz/integration-guides/sdk-reference) +- [Supported chains and tokens](https://docs.epochprotocol.xyz/supported-chains-and-tokens) diff --git a/versioned_docs/version-0.16/builder/tools/bridging/index.md b/versioned_docs/version-0.16/builder/tools/bridging/index.md new file mode 100644 index 00000000..8c6baac8 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tools/bridging/index.md @@ -0,0 +1,84 @@ +--- +title: Bridging +description: Choose a third-party bridge integration for a Miden application. +sidebar_position: 1 +--- + +# Bridging + +:::warning v0.16 snapshot compatibility +These provider guides are frozen from the bridge portal documentation pinned on September 14, 2026. That portal still uses Miden SDK v0.15; inclusion here does not establish v0.16 compatibility. Confirm compatible provider deployments and validate an end-to-end transfer on your target network before relying on these flows. +::: + +Miden applications can use third-party interoperability providers to move +assets between Miden testnet and Ethereum Sepolia. + +These are provider integrations available to Miden developers, not +Miden-owned bridge products or APIs. + + +The assets, deployments, timings, and endpoints described here are for testnet +development. Confirm current support in the provider's documentation before +building or funding an integration. + + +## Choose an integration + + + + Integrate directly with Agglayer's bridge lifecycle and the Miden-specific + account and note flow. + + + Request a quote, authorize the source asset, and let Epoch coordinate + destination fulfillment. + + + +| | Agglayer | Epoch | +| --- | --- | --- | +| Typical testnet time | 10–20 minutes | 1–3 minutes | +| Current reference asset | Sepolia ETH ↔ Miden ETH | Epoch test USDC on Sepolia ↔ Miden USDC | +| Integration model | Agglayer bridge transaction and lifecycle | Quote-and-solve intent SDK | +| Provider boundary | Agglayer and its Miden-side integration service | Epoch allocator and solver | +| Best fit | Direct Agglayer interoperability | Faster intent-based USDC movement | + +The timing ranges are observations, not service-level agreements. Source +finality, provider observation, proof or solver availability, destination +settlement, and Miden note synchronization can all affect the final duration. + +Choose by asset support, provider trust boundary, recovery model, and the +states your application must expose—not timing alone. + +## Model completion correctly + +A cross-chain transfer is not one status. Your application should distinguish: + + + +**Source authorization and submission** — the user approves and submits the +source-chain action. + +**Provider acceptance** — Agglayer observes the bridge action or Epoch accepts +the intent. + +**Destination settlement** — the provider completes its destination-chain +transaction. + +**Funds become spendable** — the destination wallet discovers and, on Miden, +consumes the delivered note. + + + +On Miden, delivery can create a note that the recipient's wallet still needs +to discover and consume. Do not report spendable funds only because the +provider reports a successful destination transaction. + +## Documentation boundary + + +These pages explain how each provider fits into a Miden application and +identify the Miden-specific integration details. Continue in the provider's +documentation for current SDK methods, deployment addresses, supported assets, +fees, recovery operations, and production guidance. + diff --git a/versioned_docs/version-0.16/builder/tools/clients/_category_.yml b/versioned_docs/version-0.16/builder/tools/clients/_category_.yml new file mode 100644 index 00000000..9c37f780 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tools/clients/_category_.yml @@ -0,0 +1,4 @@ +label: Client +# Determines where this documentation section appears relative to other sections on the main documentation page (which is the parent of this folder in the miden-docs repository) +position: 5 +collapsed: true diff --git a/versioned_docs/version-0.16/builder/tools/clients/common-errors.md b/versioned_docs/version-0.16/builder/tools/clients/common-errors.md new file mode 100644 index 00000000..f041814f --- /dev/null +++ b/versioned_docs/version-0.16/builder/tools/clients/common-errors.md @@ -0,0 +1,37 @@ +## Troubleshooting and transaction lifecycle + +This guide helps you troubleshoot common issues and understand the end-to-end lifecycle of transactions and notes in the Miden client. + +### Actionable error hints + +#### `ClientError::MissingOutputRecipients` +- Cause: The MASM program emitted an output note whose recipient was not listed in `TransactionRequestBuilder::expected_output_recipients(...)`. +- Fix: Reconcile the MASM recipient data with the Rust note structs and update the expected recipients so that the expected recipients are part of the transaction outputs. + +#### `TransactionRequestError::MissingAuthenticatedInputNote` +- Cause: A note ID included in `TransactionRequestBuilder::authenticated_input_notes(...)` did not have a corresponding `InputNoteRecord` in the store, or it was not found to contain authentication data. +- Fix: Import or sync the note, so its record and inclusion proof are present before building and executing the request. + +#### `TransactionRequestError::NoInputNotesNorAccountChange` +- Cause: The transaction neither consumes input notes nor mutates tracked account state. +- Fix: Add at least one authenticated/unauthenticated input note or include an explicit account state update in the request. + +#### `TransactionRequestError::StorageSlotNotFound` +- Cause: The request referenced an account storage slot that does not exist, often because the ABI layout is incorrectly addressed (auth component is always the first component in the account component list). +- Fix: Verify the account ABI and component ordering, then adjust the slot index used in the transaction. + +#### `TransactionExecutorError::ForeignAccountNotAnchoredInReference` +- Cause: The foreign account proof was generated against a different block than the request’s reference block. +- Fix: Re-fetch the foreign account proof anchored at the correct reference block and retry. + +#### `TransactionExecutorError::TransactionProgramExecutionFailed` +- Cause: The MASM kernel failed during execution (e.g., failed assertion or constraint violation). +- Fix: Re-run with the debug mode, capture VM diagnostics and inspect the source manager output to understand why execution failed. + +#### `ClientError::StoreError(AccountCommitmentAlreadyExists(...))` +- Cause: The final account commitment already exists locally, usually because the transaction was applied previously. +- Fix: Sync to confirm the transaction status and avoid resubmitting it; if you need a clean slate for development, reset the store. + +#### `ClientError::NoteNotFoundOnChain(Note ID)`/`RpcError::NoteNotFound(Note ID)` +- Cause: The note has not been found on chain, or the input ID is incorrect. +- Fix: Verify the note ID, ensure it has been committed, and run sync the client before retrying. diff --git a/versioned_docs/version-0.16/builder/tools/clients/img/get-started/account-a.png b/versioned_docs/version-0.16/builder/tools/clients/img/get-started/account-a.png new file mode 100644 index 00000000..4d01f73a Binary files /dev/null and b/versioned_docs/version-0.16/builder/tools/clients/img/get-started/account-a.png differ diff --git a/versioned_docs/version-0.16/builder/tools/clients/img/get-started/account-b.png b/versioned_docs/version-0.16/builder/tools/clients/img/get-started/account-b.png new file mode 100644 index 00000000..b85107c3 Binary files /dev/null and b/versioned_docs/version-0.16/builder/tools/clients/img/get-started/account-b.png differ diff --git a/versioned_docs/version-0.16/builder/tools/clients/img/get-started/commit-height.png b/versioned_docs/version-0.16/builder/tools/clients/img/get-started/commit-height.png new file mode 100644 index 00000000..02693818 Binary files /dev/null and b/versioned_docs/version-0.16/builder/tools/clients/img/get-started/commit-height.png differ diff --git a/versioned_docs/version-0.16/builder/tools/clients/img/get-started/consumed-note.png b/versioned_docs/version-0.16/builder/tools/clients/img/get-started/consumed-note.png new file mode 100644 index 00000000..391ff114 Binary files /dev/null and b/versioned_docs/version-0.16/builder/tools/clients/img/get-started/consumed-note.png differ diff --git a/versioned_docs/version-0.16/builder/tools/clients/img/get-started/miden-account-list.png b/versioned_docs/version-0.16/builder/tools/clients/img/get-started/miden-account-list.png new file mode 100644 index 00000000..30915744 Binary files /dev/null and b/versioned_docs/version-0.16/builder/tools/clients/img/get-started/miden-account-list.png differ diff --git a/versioned_docs/version-0.16/builder/tools/clients/img/get-started/note-view.png b/versioned_docs/version-0.16/builder/tools/clients/img/get-started/note-view.png new file mode 100644 index 00000000..1a3d0964 Binary files /dev/null and b/versioned_docs/version-0.16/builder/tools/clients/img/get-started/note-view.png differ diff --git a/versioned_docs/version-0.16/builder/tools/clients/img/get-started/processing-note.png b/versioned_docs/version-0.16/builder/tools/clients/img/get-started/processing-note.png new file mode 100644 index 00000000..356215ae Binary files /dev/null and b/versioned_docs/version-0.16/builder/tools/clients/img/get-started/processing-note.png differ diff --git a/versioned_docs/version-0.16/builder/tools/clients/img/get-started/transaction-confirmation.png b/versioned_docs/version-0.16/builder/tools/clients/img/get-started/transaction-confirmation.png new file mode 100644 index 00000000..e535a88f Binary files /dev/null and b/versioned_docs/version-0.16/builder/tools/clients/img/get-started/transaction-confirmation.png differ diff --git a/versioned_docs/version-0.16/builder/tools/clients/img/get-started/two-accounts.png b/versioned_docs/version-0.16/builder/tools/clients/img/get-started/two-accounts.png new file mode 100644 index 00000000..9c989073 Binary files /dev/null and b/versioned_docs/version-0.16/builder/tools/clients/img/get-started/two-accounts.png differ diff --git a/versioned_docs/version-0.16/builder/tools/clients/img/get-started/view-account-vault.png b/versioned_docs/version-0.16/builder/tools/clients/img/get-started/view-account-vault.png new file mode 100644 index 00000000..e2eeaa9f Binary files /dev/null and b/versioned_docs/version-0.16/builder/tools/clients/img/get-started/view-account-vault.png differ diff --git a/versioned_docs/version-0.16/builder/tools/clients/index.md b/versioned_docs/version-0.16/builder/tools/clients/index.md new file mode 100644 index 00000000..91c7fc72 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tools/clients/index.md @@ -0,0 +1,55 @@ +--- +title: Clients +description: "Miden client SDKs — Rust, TypeScript, and React surfaces for accounts, transactions, notes, and client-side proving." +sidebar_position: 1 +pagination_prev: null +--- + +# Clients + +The Miden client manages accounts, builds and executes transactions, produces zero-knowledge proofs, and synchronises local state with the node. The same core ships across three consumer surfaces — pick the runtime that matches your application. All three share the same onchain semantics. + +## SDKs + + + + Native Rust library and CLI. Best for services, proving infrastructure, tests, scripting, and local exploration. + + + `@miden-sdk/miden-sdk` — Rust compiled to WebAssembly with a typed TypeScript API. Browser, Node, Electron, service workers. + + + `@miden-sdk/react` — `MidenProvider` + hooks (`useMiden`, `useAccount`, `useSend`, …) wrapping the Web SDK. + + + +## Pick a surface + + + + Core state machine, transaction executor, prover, keystore abstraction, and note transport. Use it in native services, backend proving infrastructure, and integration tests. + + + Wraps the library as commands. Shipped in the same `miden-client` crate — good for local exploration and ops workflows. + + + Rust library compiled to WebAssembly with a typed `MidenClient` JavaScript class. Canonical TS/JS entry point for browser and Node apps. + + + `MidenProvider` + hooks wrapping the Web SDK. Drop it into a React / Next.js browser app for instant Miden integration. + + + +## Shared topics + + + + Run a local node, point Rust and web clients at localhost, import genesis accounts, and debug local transaction state. + + + Errors, diagnostic output, and recovery patterns shared across all surfaces. + + + End-to-end walkthroughs using each client surface — Miden Bank, recipes, helpers. + + diff --git a/versioned_docs/version-0.16/builder/tools/clients/local-node-testing.md b/versioned_docs/version-0.16/builder/tools/clients/local-node-testing.md new file mode 100644 index 00000000..f908ec61 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tools/clients/local-node-testing.md @@ -0,0 +1,202 @@ +--- +title: Local node testing +description: "Run a local Miden node and point Rust, Web SDK, and React SDK clients at it for application testing." +sidebar_position: 2 +--- + +# Local node testing + +Use a local node when a test needs real node state: public accounts, block commits, transaction submission, network notes, or RPC error details. For unit tests and most CI, use the Web SDK mock client instead; it is faster and does not need a node. + +## Supported paths + +| Need | Use | +| --- | --- | +| Browser or app testing against a local network | The node repo Docker Compose stack | +| Rust client integration tests | `TEST_MIDEN_NETWORK=localhost` against a running local node | +| Private note delivery | The node Compose stack with the `note-transport` profile enabled | +| Future one-command local dev | Track [node#1874](https://github.com/0xMiden/node/issues/1874) and [midenup#180](https://github.com/0xMiden/midenup/issues/180) | + +Docker Compose is the supported default path for running the current local node stack. The rust-sdk repo also has a `make start-node` helper for its own integration tests, but that helper runs the test node directly with Cargo and is not the operator-facing Docker workflow. + +## Prerequisites + +- Docker Desktop on macOS, or Docker Engine with the Compose v2 plugin on Linux. +- Rust only if you run the Rust client integration tests or install the `miden-client` CLI locally. +- A browser that supports WebAssembly, Web Workers, and gRPC-web requests. + +On Linux, make sure your user can run Docker commands without `sudo`, or prefix the Docker commands below with `sudo`. + +## Start a local node + +Clone the compatible node release into a directory named `miden-node`. The account export command below assumes this Compose project name, which gives the genesis volume the name `miden-node_node-data`. + +```bash +git clone --branch v0.16.0 --depth 1 https://github.com/0xMiden/node.git miden-node +cd miden-node + +make local-network-build +make local-network-up +``` + +The stack starts the sequencer, three validators, transaction prover, network transaction builder, telemetry services, and network monitor. The RPC endpoint is: + +```text +http://localhost:57291 +``` + +Check the containers: + +```bash +docker compose --profile telemetry --profile monitor ps +``` + +Follow node logs: + +```bash +make local-network-logs +``` + +Stop the node without deleting chain data: + +```bash +make local-network-down +``` + +Reset the chain to a fresh genesis: + +```bash +make local-network-delete +make local-network-up +``` + +For the full node operator workflow, see the [local network development guide](../../../reference/node/local-network-development). + +## Export the genesis account + +The local genesis process writes account files into the Compose volume. Copy the faucet operator account into the repo root when you need an existing local account in a client: + +```bash +docker run --rm \ + -v miden-node_node-data:/data:ro \ + -v "$PWD":/out \ + alpine:3.20 \ + cp /data/accounts/faucet_operator.mac /out/faucet_operator.mac +``` + +Then configure the CLI for localhost and import the account: + +```bash +miden-client init --local --network localhost +miden-client import faucet_operator.mac +miden-client sync +miden-client account --list +``` + +If you created a custom genesis config with more wallets or faucets, import each generated `.mac` file that your test needs. + +## Web SDK configuration + +`rpcUrl: "localhost"` resolves to `http://localhost:57291`. Pass `proverUrl: "local"` to prove inside the browser or Node process, and set a dedicated `storeName` so localhost state does not mix with testnet state in IndexedDB. + +```typescript +import { MidenClient } from "@miden-sdk/miden-sdk"; + +const client = await MidenClient.create({ + rpcUrl: "localhost", + proverUrl: "local", + autoSync: false, + storeName: "miden-local-dev", +}); + +await client.sync(); +``` + +For private note delivery, enable the optional Note Transport service included in the node Compose stack: + +```bash +docker compose --profile note-transport up -d +``` + +Then pass its browser-facing gRPC-Web URL. The Web SDK has `testnet` and `devnet` shorthands for note transport, but no `localhost` shorthand. + +```typescript +const client = await MidenClient.create({ + rpcUrl: "localhost", + proverUrl: "local", + noteTransportUrl: "http://ntl.localhost", + autoSync: false, + storeName: "miden-local-dev", +}); +``` + +## React SDK configuration + +Use the same endpoints through `MidenProvider`: + +```tsx +import { MidenProvider } from "@miden-sdk/react"; +import type { ReactNode } from "react"; + +export function LocalMidenApp({ children }: { children: ReactNode }) { + return ( + + {children} + + ); +} +``` + +If the frontend itself runs inside Docker, `localhost` is the frontend container. Use `http://host.docker.internal:57291` on Docker Desktop, or put the frontend and node RPC service on the same Compose network and use the service name. + +## Rust client smoke test + +The miden-client integration test binary uses the same local network preset: + +```bash +git clone --branch v0.16.0 --depth 1 https://github.com/0xMiden/rust-sdk.git miden-rust-sdk +cd miden-rust-sdk + +TEST_MIDEN_NETWORK=localhost \ + cargo run --package miden-client-integration-tests --release --locked -- \ + --contains client_builder \ + --jobs 1 +``` + +For broader local runs, use the same `TEST_MIDEN_NETWORK=localhost` environment variable with the repo test targets. Set `TEST_MIDEN_RPC_URL`, `TEST_MIDEN_PROVER_URL`, or `TEST_MIDEN_NOTE_TRANSPORT_URL` only when you need to override one component. + +## Browser and proxy notes + +- The node RPC server enables gRPC-web and CORS, so browser clients can call `http://localhost:57291` directly. +- Do not proxy RPC as JSON. If your dev server or reverse proxy sits between the app and node, preserve gRPC-web requests and response headers. +- When switching between testnet, devnet, and localhost, use a different `storeName` or clear the browser IndexedDB database used by the SDK. + +## Debug local failures + +Start with the local logs: + +```bash +make local-network-logs +``` + +Then sync the client and inspect local transaction state: + +```bash +miden-client sync +miden-client tx --list +``` + +For network notes, query the node for the note processing status: + +```bash +miden-client network-note-status +``` + +The status output includes the processing state, attempt count, latest error, and last attempt block when the node has those details. diff --git a/versioned_docs/version-0.16/builder/tools/clients/react-sdk/_category_.yml b/versioned_docs/version-0.16/builder/tools/clients/react-sdk/_category_.yml new file mode 100644 index 00000000..8859b318 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tools/clients/react-sdk/_category_.yml @@ -0,0 +1,4 @@ +label: React +# Sidebar position within tools/clients/ (Rust is 1, TypeScript is 2, React is 3) +position: 3 +collapsed: true diff --git a/versioned_docs/version-0.16/builder/tools/clients/react-sdk/account-state-and-balances.md b/versioned_docs/version-0.16/builder/tools/clients/react-sdk/account-state-and-balances.md new file mode 100644 index 00000000..d743d549 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tools/clients/react-sdk/account-state-and-balances.md @@ -0,0 +1,335 @@ +--- +title: Account state and balances +sidebar_position: 4 +--- + +# Account state and balances + +This page shows the application flow most wallet and dApp frontends need: + +1. mount `MidenProvider`, +2. resolve the active account, +3. sync before reading fresh state, +4. render fungible asset balances, +5. refresh after submitted transactions, and +6. run local view-call style reads with `useExecuteProgram()`. + +The important boundary is: + +- **Sync** pulls network state into the local client store. +- **Query hooks** such as `useAccounts()` and `useAccount()` read from that local store. +- **Mutation hooks** such as `useSend()`, `useMint()`, and `useConsume()` build, prove, and submit transactions. +- **`useExecuteProgram()`** runs locally and does not prove, submit, or change state. + +For the full package README and source-level examples, see +[`web-sdk/packages/react-sdk/README.md`](https://github.com/0xMiden/web-sdk/blob/v0.16.0/packages/react-sdk/README.md). + +## Provider setup + +Mount `MidenProvider` once above every component that calls React SDK hooks. Enable auto-sync for normal app usage, and keep a manual refresh button for user-driven state updates. + +```tsx +import { MidenProvider } from "@miden-sdk/react"; + +export function App() { + return ( + Loading Miden...

} + errorComponent={(error) =>

{error.message}

} + > + +
+ ); +} + +function WalletHome() { + return

Miden wallet ready.

; +} +``` + +## Resolve the active account + +When an external signer is connected, `useMiden()` exposes `signerAccountId`. In local-keystore flows, pick the account from `useAccounts()` instead, usually from a user selection or the first account in the local store. + +```tsx +import { useMemo } from "react"; +import { useAccounts, useMiden } from "@miden-sdk/react"; + +export function useActiveAccountId(selectedAccountId?: string): string | undefined { + const { signerAccountId } = useMiden(); + const { accounts } = useAccounts(); + + return useMemo( + () => selectedAccountId ?? signerAccountId ?? accounts[0]?.id().toString(), + [selectedAccountId, signerAccountId, accounts] + ); +} +``` + +If this returns `undefined`, the app has no connected signer and no local account yet. Render a connect/create-account state before calling transaction hooks. + +## Render all fungible balances + +`useAccount(accountId)` returns the full account plus `assets`, which are extracted from the account vault and decorated with token metadata when it is available. + +```tsx +import { + formatAssetAmount, + useAccount, + useSyncState, +} from "@miden-sdk/react"; + +type BalancePanelProps = { + accountId?: string; + fallbackDecimals?: number; +}; + +export function BalancePanel({ + accountId, + fallbackDecimals = 8, +}: BalancePanelProps) { + const { account, assets, isLoading, error, refetch } = useAccount(accountId); + const { sync, isSyncing } = useSyncState(); + + const refresh = async () => { + await sync(); + await refetch(); + }; + + if (!accountId) return

Connect or create an account to see balances.

; + if (isLoading) return

Loading balances...

; + if (error) return

{error.message}

; + if (!account) return

Account not found in the local store.

; + + return ( +
+
+

{account.bech32id()}

+ +
+ + {assets.length === 0 ? ( +

No fungible assets found for this account.

+ ) : ( +
    + {assets.map((asset) => { + const decimals = asset.decimals ?? fallbackDecimals; + const label = asset.symbol ?? asset.assetId; + + return ( +
  • + {formatAssetAmount(asset.amount, decimals)} {label} +
  • + ); + })} +
+ )} +
+ ); +} +``` + +`sync()` updates the local store from the node. `refetch()` then re-reads the account details for the current component. `useAccount()` also refreshes after successful provider syncs, but explicit `refetch()` keeps button-driven UI immediate. + +## Render one token balance + +For a single known faucet, use the `getBalance(assetId)` helper. It returns `0n` when the account does not hold that asset. + +```tsx +import { formatAssetAmount, useAccount } from "@miden-sdk/react"; + +type TokenBalanceProps = { + accountId?: string; + faucetId: string; + symbol?: string; + decimals?: number; +}; + +export function TokenBalance({ + accountId, + faucetId, + symbol = "TOKEN", + decimals = 8, +}: TokenBalanceProps) { + const { getBalance, isLoading, error } = useAccount(accountId); + const balance = getBalance(faucetId); + + if (!accountId) return -; + if (isLoading) return Loading...; + if (error) return {error.message}; + + return ( + + {formatAssetAmount(balance, decimals)} {symbol} + + ); +} +``` + +## Refresh after a submitted transaction + +`useSend()` returns `{ txId }`. `useMint()` and `useConsume()` return `{ transactionId }`. In all cases, wait for the transaction to commit, then sync the client so query hooks can read the updated local state. + +```tsx +import { + useSend, + useSyncState, + useWaitForCommit, +} from "@miden-sdk/react"; + +type SendFormProps = { + from: string; + to: string; + faucetId: string; +}; + +export function SendForm({ from, to, faucetId }: SendFormProps) { + const { send, stage, isLoading, error } = useSend(); + const { waitForCommit } = useWaitForCommit(); + const { sync } = useSyncState(); + + const submit = async () => { + const result = await send({ + from, + to, + assetId: faucetId, + amount: 100n, + noteType: "private", + }); + + await waitForCommit(result.txId); + await sync(); + }; + + return ( + <> + + {error &&

{error.message}

} + + ); +} +``` + +For `mint` and `consume`, the same pattern is: + +```tsx +import { + useConsume, + useMint, + useSyncState, + useWaitForCommit, +} from "@miden-sdk/react"; + +type MintAndConsumeActionsProps = { + accountId: string; + faucetId: string; + noteIds: string[]; +}; + +export function MintAndConsumeActions({ + accountId, + faucetId, + noteIds, +}: MintAndConsumeActionsProps) { + const { mint, stage: mintStage, isLoading: isMinting } = useMint(); + const { consume, stage: consumeStage, isLoading: isConsuming } = useConsume(); + const { waitForCommit } = useWaitForCommit(); + const { sync } = useSyncState(); + + const mintNewTokens = async () => { + const result = await mint({ + faucetId, + targetAccountId: accountId, + amount: 100n, + }); + + await waitForCommit(result.transactionId); + await sync(); + }; + + const consumeNotes = async () => { + const result = await consume({ + accountId, + notes: noteIds, + }); + + await waitForCommit(result.transactionId); + await sync(); + }; + + return ( + <> + + + + ); +} +``` + +If a transaction creates a private note for another account, the recipient still needs to receive/import the note and consume it before the recipient's balance changes. + +## Local view-call reads + +Use `useExecuteProgram()` for local, static-call style reads against account code. It runs a compiled transaction script locally, returns the stack output, and does not submit a transaction. + +```tsx +import { useExecuteProgram } from "@miden-sdk/react"; +import type { TransactionScript } from "@miden-sdk/miden-sdk"; + +type CounterReadProps = { + accountId: string; + script: TransactionScript; + foreignAccountId?: string; +}; + +export function CounterRead({ + accountId, + script, + foreignAccountId, +}: CounterReadProps) { + const { execute, result, isLoading, error } = useExecuteProgram(); + + const read = async () => { + await execute({ + accountId, + script, + foreignAccounts: foreignAccountId ? [foreignAccountId] : [], + }); + }; + + return ( +
+ + {error &&

{error.message}

} + {result &&

Counter: {result.stack[0]?.toString() ?? "0"}

} +
+ ); +} +``` + +`foreignAccounts` is required only when the script reads another public account through foreign procedure invocation. View calls use the local client context, so set `skipSync: true` only when you intentionally want to read the current local snapshot without pulling fresh network state first. + +## Checklist + +- Wrap app code in `MidenProvider` before calling hooks. +- Use `signerAccountId` for external signer apps and `useAccounts()` for local account selection. +- Call `sync()` before user-visible reads that need fresh network state. +- Read balances from `useAccount(accountId).assets` or `getBalance(faucetId)`. +- After submitted transactions, wait for commit and sync again. +- Use `useExecuteProgram()` only for local reads; it does not prove, submit, or mutate account state. diff --git a/versioned_docs/version-0.16/builder/tools/clients/react-sdk/advanced.md b/versioned_docs/version-0.16/builder/tools/clients/react-sdk/advanced.md new file mode 100644 index 00000000..3f4fdc63 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tools/clients/react-sdk/advanced.md @@ -0,0 +1,341 @@ +--- +title: Advanced +sidebar_position: 5 +--- + +# Advanced hooks + +Hooks beyond the core send / mint / consume trio: custom scripts, anchored transaction previews, MASM compilation, session wallets, store backup, note serialization, and sync control. + +## `useTransaction` + +General-purpose transaction runner that accepts either a prebuilt `TransactionRequest` or a builder callback. This is the escape hatch when the higher-level hooks don't cover your flow. + +```tsx +import { useTransaction } from "@miden-sdk/react"; +import { TransactionRequestBuilder } from "@miden-sdk/miden-sdk"; + +const { execute, isLoading, stage } = useTransaction(); + +// Direct request +await execute({ + accountId: contractAccount, + request: prebuiltRequest, +}); + +// Builder callback — receives the raw WebClient +await execute({ + accountId: contractAccount, + request: (_client) => + new TransactionRequestBuilder() + .withCustomScript(txScript) + .build(), +}); +``` + +`UseTransactionResult` exposes `execute` (not `executeTransaction`), plus `result`, `isLoading`, `stage`, `error`, and `reset`. + +`ExecuteTransactionOptions`: + +| Field | Description | +| --- | --- | +| `accountId` | Account the transaction applies to | +| `request` | `TransactionRequest` or `(client: WebClient) => TransactionRequest \| Promise` | +| `skipSync` | Skip pre-send auto-sync (default `false`) | +| `privateNoteTarget` | Deliver private output notes to this account after commit (any `AccountRef` form) | +| `anchor` | Execute against a reference block captured with `useChainAnchor` | + +The `privateNoteTarget` field is the 4-step pipeline shortcut: execute the tx, commit onchain, then auto-deliver the private note through the note transport to the target. Useful for "send private note" UIs where the recipient already has the React SDK running. + +## `useChainAnchor` and `usePreview` + +Use these hooks when a transaction summary is proposed on one client and authorized or executed on another, such as multisig and offline co-signing flows. `useChainAnchor` pins the request to one reference block; `usePreview` derives the summary awaiting authorization at that block. + +Capture and preview in separate UI steps. `anchoredRequest` is React state, so it becomes available on the render after `captureAnchor()` completes: + +```tsx +import { useState } from "react"; +import { useChainAnchor, usePreview, useTransaction } from "@miden-sdk/react"; +import type { TransactionRequest, TransactionSummary } from "@miden-sdk/miden-sdk"; + +type MultisigProposalProps = { + accountId: string; + buildRequest: () => TransactionRequest | Promise; + sendProposal: (anchor: Uint8Array, summary: Uint8Array) => Promise; + collectAuthorization: ( + summary: TransactionSummary, + request: TransactionRequest, + ) => Promise; +}; + +function MultisigProposal({ + accountId, + buildRequest, + sendProposal, + collectAuthorization, +}: MultisigProposalProps) { + const { captureAnchor, anchor, anchoredRequest, isCapturing } = useChainAnchor(); + const { preview, isPreviewing } = usePreview(); + const { execute, isLoading } = useTransaction(); + const [authorizedRequest, setAuthorizedRequest] = + useState(null); + const [isAuthorizing, setIsAuthorizing] = useState(false); + const busy = isCapturing || isPreviewing || isAuthorizing || isLoading; + + const capture = async () => { + setAuthorizedRequest(null); + await captureAnchor({ request: buildRequest }); + }; + + const previewAndShare = async () => { + if (!anchor || !anchoredRequest) return; + setAuthorizedRequest(null); + setIsAuthorizing(true); + try { + const summary = await preview({ + accountId, + request: anchoredRequest, + anchor, + }); + await sendProposal(anchor.serialize(), summary.serialize()); + setAuthorizedRequest(await collectAuthorization(summary, anchoredRequest)); + } finally { + setIsAuthorizing(false); + } + }; + + const executeAnchored = async () => { + if (!anchor || !authorizedRequest) return; + await execute({ accountId, request: authorizedRequest, anchor }); + setAuthorizedRequest(null); + }; + + return ( + <> + + + + + ); +} +``` + +`collectAuthorization` is supplied by your application. It collects the required +signatures and returns the request with the authorization advice attached. Keep +the proposed actions, fee conversion salt, and reference block unchanged. For a +multisig account, `buildRequest` must declare a fresh salt with +`withFeeConversionSalt`. The execute button becomes available after authorization +has been collected. + +`preview()` rejects with `TRANSACTION_ALREADY_AUTHORIZED` when the request needs no additional authorization; execute it directly in that case. A `ChainAnchor` owns a WASM allocation, so call `anchor.free()` when the proposal workflow no longer needs it. + +## `useExecuteProgram` + +View call — executes a transaction script locally and returns the stack output. No prove, no submit, no state change. Think of it as Miden's `eth_call`. + +```tsx +import { useExecuteProgram } from "@miden-sdk/react"; + +const { execute, isLoading, error } = useExecuteProgram(); + +const result = await execute({ + accountId: contractAccount, + script: compiledTxScript, + foreignAccounts: [counterAccount], // optional +}); + +// result.stack is a bigint[] — read indices directly +const count: bigint = result.stack[0]; +console.log("Count:", count); +``` + +`UseExecuteProgramResult` exposes `execute` (not `executeProgram`), plus `result`, `isLoading`, `error`, and `reset`. No `stage` — view calls don't prove or submit. + +The React hook flattens the 16-element stack into a plain `bigint[]`. `useMidenClient()` exposes the underlying WASM `WebClient` directly — its method is `client.executeProgram(...)` (not namespaced under `client.transactions`). See the [Web SDK transactions guide](../web-client/transactions.md#view-calls-executeprogram) for the imperative `MidenClient.transactions.executeProgram` equivalent and the `FeltArray` shape. + +## `useCompile` + +Compiles Miden Assembly into `AccountComponent`, `TransactionScript`, or `NoteScript`. Each result method is independently callable — call only what you need for the current operation. + +```tsx +import { useCompile } from "@miden-sdk/react"; +import { StorageSlot } from "@miden-sdk/miden-sdk"; + +const { component, txScript, noteScript, isReady } = useCompile(); + +// Account component +const counterComponent = await component({ + code: counterContractCode, + namespace: "external_contract::counter_contract", + slots: [StorageSlot.emptyValue("miden::tutorials::counter")], +}); + +// Transaction script (with optional libraries) +const script = await txScript({ + code: ` + use external_contract::counter_contract + + @transaction_script + pub proc main + call.counter_contract::increment_count + end + `, + libraries: [{ component: counterComponent }], +}); + +// Note script — use the @note_script attribute on a library proc +const attachScript = await noteScript({ + code: ` + use miden::protocol::active_note + use miden::core::sys + + @note_script + pub proc on_consume + # body runs when the consuming account redeems this note + exec.sys::truncate_stack + end + `, +}); +``` + +`UseCompileResult` exposes the three compile methods plus `isReady`. Loading and error state are tracked internally per call — catch errors at the individual `await` site. See the [Web SDK compile guide](../web-client/compile.md) for the full `CompileComponentOptions` / `CompileTxScriptOptions` / `CompileNoteScriptOptions` shapes. + +## `useSessionAccount` + +Drives the "session wallet" pattern — create a throw-away wallet, wait for a funding note, consume it, then hand control back to your app. Useful for one-off interactions that shouldn't touch a long-lived account. + +```tsx +import { useMiden, useSessionAccount } from "@miden-sdk/react"; +import { getWasmOrThrow } from "@miden-sdk/miden-sdk/lazy"; + +// Resolve the numeric authentication enum once, before rendering this module. +const { AuthScheme } = await getWasmOrThrow(); + +export function SessionWallet({ + fund, +}: { + fund: (sessionAccountId: string) => Promise; +}) { + const { isReady: clientReady } = useMiden(); + const { initialize, sessionAccountId, isReady, step, error, reset } = + useSessionAccount({ + // The callback receives the new wallet's hex ID. Send a funding note + // containing native fee tokens, plus any assets the session needs. + fund, + walletOptions: { + storageMode: "private", + authScheme: AuthScheme.AuthRpoFalcon512, + }, + pollIntervalMs: 3_000, + maxWaitMs: 60_000, + }); + + return ( + <> + + {isReady &&

Session ready: {sessionAccountId}

} + {error &&

{error.message}

} + + + ); +} +``` + +In 0.16.0, pass the raw numeric authentication enum explicitly, as above; the +hook's default resolves to an undefined enum member. The `fund` callback belongs +to your application. Its note must include the native fee asset so the new wallet +can pay for its first consume transaction. + +The flow progresses through `idle` → `creating` → `funding` → `consuming` → `ready`. + +`UseSessionAccountReturn`: + +| Field | Description | +| --- | --- | +| `initialize()` | Kicks off the create → fund → consume flow | +| `sessionAccountId` | Hex ID of the session wallet once created | +| `isReady` | `true` after the funding note has been consumed | +| `step` | `SessionAccountStep` — one of the five states above | +| `error` | Non-null if any step failed | +| `reset()` | Clears session data (and any persisted state under `storagePrefix`) | + +Session state persists under the configurable `storagePrefix` (default `"miden-session"`) so page reloads can resume mid-flow. + +## `useExportStore` / `useImportStore` + +Back up and restore the entire local store as a JSON dump. Handy for wallet backup/restore UIs. + +```tsx +import { useExportStore, useImportStore, useMidenClient } from "@miden-sdk/react"; + +// Export — returns a JSON string +const { exportStore } = useExportStore(); +const dump: string = await exportStore(); +download(new Blob([dump]), "wallet-backup.json"); + +// Import (destructive — overwrites the target store) +// Positional: (storeDump, storeName, options?) +const { importStore } = useImportStore(); +const client = useMidenClient(); +const storeName = await client.storeIdentifier(); +await importStore(uploadedDump, storeName, { skipSync: false }); +``` + +`ImportStoreOptions` exposes `skipSync` (default `false`) so you can defer the post-import sync. There's no second "raw bytes" form — `importStore` takes the JSON dump string as its first argument and the target store name as its second. + +## `useImportNote` / `useExportNote` + +Serialize notes to bytes for QR delivery or import notes handed over out-of-band. These complement the private-note transport layer — use the transport when the recipient is online, and QR/bytes when they aren't. + +In 0.16.0, `exportNote` requires an output-note ID tracked by this client. An +imported input note alone is insufficient and returns `No output note found`. + +```tsx +import { useExportNote, useImportNote } from "@miden-sdk/react"; + +const { exportNote } = useExportNote(); +const noteBytes = await exportNote(noteId); +// encode noteBytes into a QR, link, email, etc. + +const { importNote } = useImportNote(); +await importNote(uploadedBytes); +``` + +## `useSyncControl` + +Pause and resume the auto-sync loop without dismounting `MidenProvider`. Useful when a long operation needs consistent local state, or during battery-sensitive background work. + +```tsx +import { useSyncControl } from "@miden-sdk/react"; + +const { pauseSync, resumeSync, isPaused } = useSyncControl(); + +// Before a long sequence +pauseSync(); +// ... operations that need a stable snapshot ... +resumeSync(); +``` + +`pauseSync()` stops the timer but doesn't cancel an in-flight sync — wait for `isSyncing` from `useSyncState()` to settle if you need a truly quiescent state. + +## Next + +- [Signers](./signers.md) — wire external wallets (Para, Turnkey, MidenFi) or build a custom signer. +- [Recipes](./recipes.md) — end-to-end patterns. diff --git a/versioned_docs/version-0.16/builder/tools/clients/react-sdk/index.md b/versioned_docs/version-0.16/builder/tools/clients/react-sdk/index.md new file mode 100644 index 00000000..bacd5345 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tools/clients/react-sdk/index.md @@ -0,0 +1,46 @@ +--- +title: Overview +sidebar_position: 1 +--- + +# React SDK (@miden-sdk/react) + +The React SDK is a thin layer on top of the [Web SDK](../web-client/index.md). It wraps the underlying WASM `WebClient` with a React context (`MidenProvider`), a family of hooks (`useMiden`, `useAccount`, `useSend`, …), automatic sync polling, and a concurrency lock so multiple components never trip over the same WASM instance. + +## When to use it + +Reach for the React SDK when your application is already a React app (Next.js, Vite + React, or a compatible Electron renderer). The hooks: + +- own lifecycle management (the WASM worker, signer wiring, auto-sync loop), +- expose per-hook result interfaces with a domain-named action (`send`, `mint`, `createWallet`, …) plus `isLoading` / `isCreating` / `isImporting`, `error`, and `reset`, +- serialize mutations so concurrent component interactions don't corrupt the WASM state. + +If you are building a non-React app — a service worker, a Node backend, a vanilla-TS dApp — use the imperative [Web SDK](../web-client/index.md) directly. + +You can always reach the underlying WASM client from any hook via `useMidenClient()` when a hook doesn't cover what you need — it returns the low-level `WebClient`, not the imperative `MidenClient` wrapper. + +## What's in the package + +| Surface | Purpose | +| --- | --- | +| [`MidenProvider`](./setup.md) | Root React context; loads WASM, wires the client, runs auto-sync | +| [`useMiden()`](./setup.md#client-lifecycle) | Raw lifecycle hook (`isReady`, `sync`, `runExclusive`) | +| [`useMidenClient()`](./setup.md#client-lifecycle) | Shortcut for the ready WASM `WebClient` | +| [Query hooks](./query-hooks.md) | `useAccount(s)`, `useNotes`, `useNoteStream`, `useTransactionHistory`, `useSyncState`, `useAssetMetadata` | +| [Account state and balances](./account-state-and-balances.md) | Active account selection, sync boundaries, balance rendering, and refresh-after-transaction patterns | +| [Mutation hooks](./mutation-hooks.md) | `useCreateWallet`, `useCreateFaucet`, `useImportAccount`, `useSend`, `useMultiSend`, `useMint`, `useConsume`, `useSwap` | +| [Advanced hooks](./advanced.md) | `useTransaction`, `useChainAnchor`, `usePreview`, `useExecuteProgram`, `useCompile`, `useSessionAccount`, store and note import/export, sync control | +| [External signers](./signers.md) | `MultiSignerProvider`, `SignerContext`, `useSigner`, `useMultiSigner` — pluggable wallet integrations (Para, Turnkey, MidenFi, custom) | +| Utilities | `formatAssetAmount`, `parseAssetAmount`, `getNoteSummary`, `toBech32AccountId`, `createNoteAttachment` / `readNoteAttachment`, … | + +Each hook exports its own result interface — not a generic `{ data, isLoading, error }` wrapper. Data lives in named fields (e.g. `accounts`, `records`, `wallet`, `faucet`). Transaction-producing mutations additionally expose a `stage` field that advances through `idle → executing → proving → submitting → complete`. See [setup](./setup.md#hook-result-conventions) for per-family details. + +## Where to go next + +- [Setup](./setup.md) — install the package, wrap your app in `MidenProvider`, configure the network. +- [Query hooks](./query-hooks.md) — read accounts, notes, sync state, and asset metadata. +- [Account state and balances](./account-state-and-balances.md) — practical account selection, sync, balances, and refresh examples. +- [Mutation hooks](./mutation-hooks.md) — create wallets and faucets, send, mint, consume, swap. +- [Advanced](./advanced.md) — custom scripts, MASM compilation, session accounts, note import/export. +- [Signers](./signers.md) — external wallets (Para, Turnkey, MidenFi) and custom signer providers. +- [Recipes](./recipes.md) — end-to-end patterns and a pointer to Philipp's full wallet tutorial. diff --git a/versioned_docs/version-0.16/builder/tools/clients/react-sdk/mutation-hooks.md b/versioned_docs/version-0.16/builder/tools/clients/react-sdk/mutation-hooks.md new file mode 100644 index 00000000..0f480e92 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tools/clients/react-sdk/mutation-hooks.md @@ -0,0 +1,349 @@ +--- +title: Mutation hooks +sidebar_position: 4 +--- + +# Mutation hooks + +Mutation hooks own the full transaction lifecycle — execute, prove, submit — and serialize under the Web SDK's concurrency lock so two components can't corrupt the WASM state. + +These examples assume the provider configuration from [Setup](./setup.md). +For automatic delivery of private notes, configure its `noteTransportUrl` setting. + +Two result-shape families show up across this page: + +**Transaction-producing hooks** (`useSend`, `useMultiSend`, `useMint`, `useConsume`, `useSwap`, `useTransaction`): + +```ts +{ + [action]: (options) => Promise; // send, sendMany, mint, ... + result: Result | null; + isLoading: boolean; + stage: TransactionStage; + error: Error | null; + reset: () => void; +} +``` + +**Account-creation hooks** (`useCreateWallet`, `useCreateFaucet`, `useImportAccount`) don't go through the prove/submit pipeline, so they expose `isCreating` / `isImporting` instead of `isLoading` + `stage`: + +```ts +{ + createWallet: (opts?) => Promise; + wallet: Account | null; + isCreating: boolean; // or `isImporting` for useImportAccount + error: Error | null; + reset: () => void; +} +``` + +Polling helpers (`useWaitForCommit`, `useWaitForNotes`) are simpler still — they expose just the action. Per-hook exact shapes are called out below. + +See [setup](./setup.md#hook-result-conventions) for the `TransactionStage` progression. + +## `useCreateWallet` + +Creates a new wallet account. Returns the `Account` object. + +With the 0.16.0 packages, pass the low-level authentication enum explicitly. +The React hooks' default refers to an enum member missing from the high-level +SDK export, causing `invalid enum value passed`. Obtain the numeric enum from +`getWasmOrThrow()` as shown below for wallets, faucets, and seed imports. + +```tsx +import { useCreateWallet } from "@miden-sdk/react"; +import { getWasmOrThrow } from "@miden-sdk/miden-sdk/lazy"; + +function NewWalletButton() { + const { createWallet, wallet, isCreating, error } = useCreateWallet(); + + const handleCreate = async () => { + const { AuthScheme } = await getWasmOrThrow(); + const account = await createWallet({ + storageMode: "private", // "private" | "public" (default private) + authScheme: AuthScheme.AuthRpoFalcon512, + }); + console.log("Created:", account.bech32id()); + }; + + return ( + + ); +} +``` + +`CreateWalletOptions` (all optional): + +| Field | Default | Description | +| --- | --- | --- | +| `storageMode` | `"private"` | `"private"` / `"public"` | +| `authScheme` | Pass explicitly in 0.16.0 | Numeric WASM signing scheme | +| `initSeed` | random | 32-byte seed for deterministic account-ID derivation | + +## `useCreateFaucet` + +Creates a fungible-token faucet. + +```tsx +import { useCreateFaucet } from "@miden-sdk/react"; +import { getWasmOrThrow } from "@miden-sdk/miden-sdk/lazy"; + +function NewFaucetButton() { + const { createFaucet, faucet, isCreating } = useCreateFaucet(); + + const handleCreate = async () => { + const { AuthScheme } = await getWasmOrThrow(); + const created = await createFaucet({ + tokenSymbol: "TEST", + decimals: 8, // default 8 + maxSupply: 10_000_000n, // number | bigint + storageMode: "public", // default "private"; public allows FPI reads + authScheme: AuthScheme.AuthRpoFalcon512, + }); + console.log("Faucet:", created.bech32id()); + }; + + return ( + + ); +} +``` + +`CreateFaucetOptions`: + +| Field | Default | Description | +| --- | --- | --- | +| `tokenSymbol` | required | Display symbol (e.g. `"USDC"`) | +| `maxSupply` | required | `bigint \| number` | +| `decimals` | `8` | Token decimals | +| `storageMode` | `"private"` | Public faucets are discoverable/readable onchain | +| `authScheme` | Pass explicitly in 0.16.0 | Numeric WASM signing scheme | + +## `useSend` + +Sends tokens from one account to another. + +```tsx +import { useSend } from "@miden-sdk/react"; + +function SendForm({ from, to, usdcFaucetId }: Props) { + const { send, isLoading, stage, error } = useSend(); + + const handleSend = async () => { + const { txId, note } = await send({ + from, + to, + assetId: usdcFaucetId, + amount: 100n, + noteType: "private", // default "private" + }); + console.log("Transaction:", txId); + }; + + return ( + + ); +} +``` + +`SendOptions`: + +| Field | Required | Description | +| --- | --- | --- | +| `from` | yes | Sender `AccountRef` | +| `to` | yes | Recipient `AccountRef` | +| `assetId` | yes | Token faucet `AccountRef` | +| `amount` | unless `sendAll` | `bigint \| number` | +| `noteType` | — | `"private"` / `"public"` (default `"private"`) | +| `recallHeight` | — | Block height after which sender can reclaim the note | +| `timelockHeight` | — | Block height after which recipient can consume the note | +| `attachment` | — | `bigint[] \| Uint8Array \| number[]` — payload attached to the note | +| `skipSync` | — | Skip the pre-send auto-sync (default `false`) | +| `sendAll` | — | Drain full balance of `assetId` — when `true`, `amount` is ignored | +| `returnNote` | — | Return the `Note` object in the result (for out-of-band delivery, QR codes, etc.) | + +`SendResult`: `{ txId: string; note: Note | null }`. `note` is non-null only when `returnNote: true`. + +## `useMultiSend` + +Batches multiple recipients into one transaction. All outputs must share the same sender and asset. The action function is named `sendMany`. + +```tsx +import { useMultiSend } from "@miden-sdk/react"; + +const { sendMany, isLoading } = useMultiSend(); + +await sendMany({ + from: senderAccountId, + assetId: faucetId, + recipients: [ + { to: recipient1, amount: 500n, noteType: "private" }, + { to: recipient2, amount: 300n }, + ], + noteType: "private", // default for recipients that don't override +}); +``` + +`MultiSendOptions`: + +| Field | Description | +| --- | --- | +| `from`, `assetId` | Single sender + single faucet for the whole batch | +| `recipients` | `MultiSendRecipient[]` — each `{ to, amount, noteType?, attachment? }` | +| `noteType` | Default for recipients that don't specify one (default `"private"`) | +| `skipSync` | Skip pre-send auto-sync | + +## `useMint` + +Mints tokens from a faucet you control into a recipient account. + +```tsx +import { useMint } from "@miden-sdk/react"; + +const { mint, isLoading, stage } = useMint(); + +const result = await mint({ + targetAccountId: recipient, + faucetId: myFaucet, + amount: 10_000n, + noteType: "private", // default "private" +}); +console.log("Mint tx:", result.transactionId); +``` + +`MintOptions`: + +| Field | Description | +| --- | --- | +| `targetAccountId` | Recipient `AccountRef` | +| `faucetId` | Faucet `AccountRef` (must be owned by the caller) | +| `amount` | `bigint \| number` | +| `noteType` | `"private"` / `"public"` (default `"private"`) | + +Returns `TransactionResult`: `{ transactionId: string }`. + +## `useConsume` + +Claims one or more notes into an account. + +```tsx +import { useConsume } from "@miden-sdk/react"; + +const { consume, isLoading } = useConsume(); + +await consume({ + accountId: myAccountId, + notes: [noteIdHex1, noteIdHex2], // hex IDs, NoteId objects, InputNoteRecord, or Note +}); +``` + +`ConsumeOptions`: + +| Field | Description | +| --- | --- | +| `accountId` | Account consuming the notes | +| `notes` | `(string \| NoteId \| InputNoteRecord \| Note)[]` — mix-and-match accepted | + +## `useSwap` + +Atomic swap between two assets. + +```tsx +import { useSwap } from "@miden-sdk/react"; + +const { swap, isLoading } = useSwap(); + +await swap({ + accountId: myWallet, + offeredFaucetId: usdcFaucet, + offeredAmount: 100n, + requestedFaucetId: dagFaucet, + requestedAmount: 200n, + noteType: "public", // default "private" + paybackNoteType: "private", // default "private" +}); +``` + +## `useImportAccount` + +Imports an account by ID (fetches from network), by previously-exported file, or by seed. + +For seed recovery with the 0.16.0 browser packages, set `useWorker: false` in +`MidenProvider`'s `config`. With the worker enabled, a subsequent transaction can +reach the network but fail during local `applyTransaction` with `account data +wasn't found`. The direct client path restores the account and confirms a signed +transaction. If you encounter the worker error, check the account's onchain state +before retrying the transaction. + +```tsx +import { useImportAccount } from "@miden-sdk/react"; +import { getWasmOrThrow } from "@miden-sdk/miden-sdk/lazy"; + +const { importAccount, account, isImporting, error } = useImportAccount(); + +// By ID — public accounts only +const imported = await importAccount({ + type: "id", + accountId: "mtst1qy35...", +}); + +// By seed — public accounts only +const { AuthScheme } = await getWasmOrThrow(); +await importAccount({ + type: "seed", + seed: initSeed, // Uint8Array + authScheme: AuthScheme.AuthRpoFalcon512, +}); + +// By file — works for both public and private accounts +await importAccount({ + type: "file", + file: accountFileBytes, // AccountFile | Uint8Array | ArrayBuffer +}); +``` + +The `type` discriminant is required. For private accounts, use the `"file"` variant — private account state isn't reconstructible from a seed alone. + +## `useWaitForCommit` / `useWaitForNotes` + +Polling helpers for transaction confirmation and note inbox arrivals. Both are minimal hooks — they only expose the action function. + +### `useWaitForCommit` + +Signature: `waitForCommit(txId, options?)` — `txId` is positional (hex string or `TransactionId`), `options` are merged with defaults. + +```tsx +import { useWaitForCommit } from "@miden-sdk/react"; + +const { waitForCommit } = useWaitForCommit(); +await waitForCommit(result.txId, { + timeoutMs: 30_000, // default 10_000 + intervalMs: 1_000, // default 1_000 +}); +``` + +### `useWaitForNotes` + +Exposes `waitForConsumableNotes(options)` and returns the matching `ConsumableNoteRecord[]` when the threshold is reached. + +```tsx +import { useWaitForNotes } from "@miden-sdk/react"; + +const { waitForConsumableNotes } = useWaitForNotes(); + +const notes = await waitForConsumableNotes({ + accountId: recipient, + minCount: 1, // default 1 + timeoutMs: 30_000, +}); +``` + +Both reject on timeout — wrap them in `try/catch` when you want a graceful fallback. + +## Next + +- [Advanced](./advanced.md) — custom scripts, MASM compilation, session accounts, store import/export. +- [Signers](./signers.md) — external wallets (Para, Turnkey, MidenFi) and custom signers. +- [Recipes](./recipes.md) — realistic patterns + link-outs to Philipp's full wallet tutorial. diff --git a/versioned_docs/version-0.16/builder/tools/clients/react-sdk/query-hooks.md b/versioned_docs/version-0.16/builder/tools/clients/react-sdk/query-hooks.md new file mode 100644 index 00000000..a72dccb9 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tools/clients/react-sdk/query-hooks.md @@ -0,0 +1,281 @@ +--- +title: Query hooks +sidebar_position: 3 +--- + +# Query hooks + +Query hooks read from the local store (and trigger a fetch when the cache is cold). Every query hook shares `{ isLoading, error, refetch }` alongside hook-specific data fields. Refetching is automatic after successful syncs, so most components don't need to call `refetch()` manually. + +## `useAccounts` + +Lists every account header tracked by the client. Since protocol 0.15, an account ID/header no longer identifies whether the account is a wallet or faucet; inspect the full account's components when you need that distinction. + +```tsx +import { useAccounts } from "@miden-sdk/react"; + +function AccountList() { + const { accounts, isLoading, error } = useAccounts(); + + if (isLoading) return

Loading…

; + if (error) return

{error.message}

; + + return ( + <> +

Accounts ({accounts.length})

+ {accounts.map((account) => ( +
{account.id().toString()}
+ ))} + + ); +} +``` + +Return type (`AccountsResult`): + +```ts +{ + accounts: AccountHeader[]; // every tracked account + wallets: AccountHeader[]; // deprecated alias that mirrors accounts + faucets: AccountHeader[]; // deprecated; always empty + isLoading: boolean; + error: Error | null; + refetch: () => Promise; +} +``` + +## `useAccount(id)` + +Full details for a single account, including per-asset balances decorated with symbol + decimals when metadata is available. + +```tsx +import { useAccount } from "@miden-sdk/react"; + +function AccountDetails({ id }: { id: string }) { + const { account, assets, getBalance, isLoading, error } = useAccount(id); + + if (isLoading) return

Loading…

; + if (error) return

{error.message}

; + if (!account) return

Not found

; + + return ( + <> +

Account: {account.bech32id()}

+

Nonce: {account.nonce().toString()}

+

USDC balance: {getBalance(usdcFaucetId).toString()}

+ +
    + {assets.map((a) => ( +
  • + {a.amount.toString()} {a.symbol ?? a.assetId} +
  • + ))} +
+ + ); +} +``` + +Return type (`AccountResult`): + +```ts +{ + account: Account | null; + assets: AssetBalance[]; // { assetId, amount, symbol?, decimals? } + isLoading: boolean; + error: Error | null; + refetch: () => Promise; + getBalance: (assetId: string) => bigint; +} +``` + +`getBalance(assetId)` is a convenience for single-asset reads — returns `0n` when the account doesn't hold that asset. + +## `useNotes(filter?)` + +Lists input notes (received) and consumable notes (ready to claim) with optional filtering. + +```tsx +import { useNotes } from "@miden-sdk/react"; + +function NotesInbox({ account }: { account: string }) { + const { notes, consumableNotes, noteSummaries, refetch } = useNotes({ + status: "committed", + accountId: account, + }); + + return ( + <> + +

Received ({notes.length})

+ {noteSummaries.map((s) => ( +
+ {s.assets.map((a) => `${a.amount} ${a.symbol ?? a.assetId}`).join(", ")} +
+ ))} + +

Consumable ({consumableNotes.length})

+ + ); +} +``` + +Filter options (`NotesFilter`): + +| Field | Values | Description | +| --- | --- | --- | +| `status` | `"all" \| "consumed" \| "committed" \| "expected" \| "processing"` | Filter by note lifecycle state | +| `accountId` | `AccountRef` | Only notes relevant to this account | +| `sender` | `string` | Account ID in any accepted format (hex or bech32) — normalised internally | +| `excludeIds` | `string[]` | Skip these note IDs (useful for hiding notes your UI already rendered elsewhere) | + +Return type (`NotesResult`): + +```ts +{ + notes: InputNoteRecord[]; // raw SDK records + consumableNotes: ConsumableNoteRecord[]; + noteSummaries: NoteSummary[]; // pre-computed { id, assets[], sender? } + consumableNoteSummaries: NoteSummary[]; + isLoading: boolean; + error: Error | null; + refetch: () => Promise; +} +``` + +`noteSummaries` is the pragmatic choice for UIs — it pre-extracts asset info and runs metadata resolution. + +## `useNoteStream(options?)` + +Temporal note tracking with first-seen timestamps and per-stream filtering. Useful for notification UIs that want to highlight new arrivals. + +```tsx +import { useNoteStream } from "@miden-sdk/react"; + +function NewNotesToast() { + const { notes, latest, markHandled, markAllHandled } = useNoteStream({ + status: "committed", // default "committed" + since: Date.now(), // numeric timestamp; drop notes seen earlier + amountFilter: (amount) => amount > 0n, + }); + + return ( + <> + {latest &&

New: {latest.id}

} + {notes.map((n) => ( +
+ {n.id} at {new Date(n.firstSeenAt).toISOString()} + +
+ ))} + + + ); +} +``` + +`UseNoteStreamOptions` fields: `status`, `sender`, `since` (numeric timestamp), `excludeIds` (`Set` or `string[]`), and `amountFilter` for predicate-based filtering. The stream also exposes `snapshot()` for passing state across unmount / remount boundaries. + +## `useTransactionHistory(options?)` + +Transaction records, with optional filters for specific IDs or a custom `TransactionFilter`. + +```tsx +import { useTransactionHistory } from "@miden-sdk/react"; + +function HistoryTable() { + const { records, isLoading } = useTransactionHistory(); + if (isLoading) return

Loading…

; + + return ( + + + {records.map((tx) => ( + + + + + ))} + +
{tx.id().toHex()}{tx.blockNum().toString()}
+ ); +} +``` + +Options: + +| Field | Description | +| --- | --- | +| `id` | Single transaction ID lookup | +| `ids` | List of transaction IDs | +| `filter` | Custom `TransactionFilter` (overrides `id` / `ids`) | +| `refreshOnSync` | Re-fetch after every auto-sync (default `true`) | + +Result (`TransactionHistoryResult`): + +```ts +{ + records: TransactionRecord[]; + record: TransactionRecord | null; // convenience when a single id was provided + status: TransactionStatus | null; // convenience when a single id was provided + isLoading: boolean; + error: Error | null; + refetch: () => Promise; +} +``` + +For account-scoped history use a `TransactionFilter` that targets the account — see the `@miden-sdk/miden-sdk` `TransactionFilter` API. + +## `useSyncState` + +Sync heights and manual-trigger controls. + +```tsx +import { useSyncState } from "@miden-sdk/react"; + +function SyncBadge() { + const { syncHeight, isSyncing, lastSyncTime, sync } = useSyncState(); + + return ( + + ); +} +``` + +Manual `sync()` composes with the auto-sync loop (configured via `autoSyncInterval` on `MidenProvider`) — call it when you want to force an immediate refresh. + +## `useAssetMetadata(assetIds?)` + +Symbol + decimals lookup for a batch of asset IDs. The argument is an optional `string[]`; pass an empty array (or nothing) to read the global cache without triggering new fetches. + +```tsx +import { useAssetMetadata } from "@miden-sdk/react"; + +function TokenChip({ assetId }: { assetId: string }) { + const { assetMetadata } = useAssetMetadata([assetId]); + const meta = assetMetadata.get(assetId); + return {meta?.symbol ?? assetId}; +} + +function TokenLegend({ ids }: { ids: string[] }) { + const { assetMetadata } = useAssetMetadata(ids); + return ( + <> + {ids.map((id) => { + const m = assetMetadata.get(id); + return {m?.symbol ?? id}; + })} + + ); +} +``` + +`assetMetadata` is a `Map` keyed by asset ID. The hook dedupes and caches across components, so siblings that ask for overlapping IDs share cost. + +## Next + +- [Mutation hooks](./mutation-hooks.md) — create wallets, faucets, send tokens, consume notes. +- [Advanced](./advanced.md) — custom scripts, note import/export, session accounts. +- [Recipes](./recipes.md) — end-to-end patterns. diff --git a/versioned_docs/version-0.16/builder/tools/clients/react-sdk/recipes.md b/versioned_docs/version-0.16/builder/tools/clients/react-sdk/recipes.md new file mode 100644 index 00000000..b679ed13 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tools/clients/react-sdk/recipes.md @@ -0,0 +1,152 @@ +--- +title: Recipes +sidebar_position: 7 +--- + +# Recipes + +Short patterns covering the common cases. For longer walkthroughs — building a full wallet app from scratch, including UI — see the [React wallet tutorial](https://github.com/0xMiden/tutorials/blob/main/docs/src/web-client/react_wallet_tutorial.md) in the tutorials repo, which uses these hooks end-to-end. + +## Show transaction progress + +`useSend()` exposes `isLoading` and `stage`; use them for optimistic UI: + +```tsx +import { useSend } from "@miden-sdk/react"; + +function SendButton({ from, to, assetId }: Props) { + const { send, stage, isLoading, error } = useSend(); + + const handleSend = async () => { + try { + await send({ from, to, assetId, amount: 100n }); + } catch (err) { + console.error("Send failed:", err); + } + }; + + return ( + <> + + {error &&

{error.message}

} + + ); +} +``` + +## Format token amounts + +```tsx +import { formatAssetAmount, parseAssetAmount } from "@miden-sdk/react"; + +// Display: 1_000_000n with 8 decimals → "0.01" +const display = formatAssetAmount(balance, 8); + +// User input: "0.01" with 8 decimals → 1_000_000n +const amount = parseAssetAmount("0.01", 8); +``` + +## Display a note summary + +```tsx +import { getNoteSummary, formatNoteSummary } from "@miden-sdk/react"; + +const summary = getNoteSummary(note); +const text = summary ? formatNoteSummary(summary) : "Unknown note"; +``` + +`noteSummaries` from `useNotes()` already runs `getNoteSummary` for you — these helpers are for ad-hoc formatting elsewhere. + +## Wait for confirmation after a send + +```tsx +import { useSend, useWaitForCommit } from "@miden-sdk/react"; + +const { send } = useSend(); +const { waitForCommit } = useWaitForCommit(); + +const result = await send({ from, to, assetId, amount: 100n }); +await waitForCommit(result.txId); +``` + +## Drop to the raw client + +```tsx +import { useMidenClient } from "@miden-sdk/react"; + +function SyncHeightPeek() { + const client = useMidenClient(); + + const showSyncHeight = async () => { + const height = await client.getSyncHeight(); + console.log("Sync height:", height); + }; + + return ; +} +``` + +`useMidenClient()` throws if the provider isn't ready. Render the component only after `useMiden().isReady`, or provide `MidenProvider`'s `loadingComponent`. + +## Serialize a custom raw-client flow + +When several raw-client calls must run as one provider-serialized flow, use `runExclusive`. Prevent repeated calls to a mutation hook with that hook's loading state instead. + +```tsx +import { useMiden, useMidenClient } from "@miden-sdk/react"; + +function CompoundFlow() { + const { runExclusive } = useMiden(); + const client = useMidenClient(); + + const run = () => + runExclusive(async () => { + await client.syncState(); + // ...other raw-client calls in the same flow + }); + + return ; +} +``` + +`runExclusive(fn)` takes a zero-argument async function. Built-in transaction hooks coordinate their own client calls, so don't wrap a hook such as `send()` in `runExclusive`; use it only for your own raw-client flow. + +## Separate stores for multiple signers + +`MidenProvider`'s config does not accept a `storeName` directly. Per-user isolation flows through the active signer: each `SignerContext.Provider` supplies its own `storeName` field, and `MidenProvider` reads that when initialising the underlying client. See the [Signers](./signers.md#custom-signer-providers) guide for a custom signer that picks a unique store name per connected user (typically the wallet address or a hash of it). + +For apps that switch between several signers, use [`MultiSignerProvider` and `SignerSlot`](./signers.md#multisignerprovider). `MidenProvider` switches to the store associated with the active signer. Don't mount multiple `MidenProvider`s expecting independent clients: the React SDK state store is shared. + +## Account IDs — hex and bech32 interchangeably + +Account ID parameters accept either format: + +```tsx +import { toBech32AccountId, useAccount } from "@miden-sdk/react"; + +// Both formats are accepted. +const byHex = useAccount(hexAccountId); +const byBech32 = useAccount(bech32Address); + +// Convert for display +byHex.account?.bech32id(); +toBech32AccountId(hexAccountId); +``` + +## Troubleshooting + +| Symptom | Likely cause | +| --- | --- | +| `"Client not ready"` thrown by a hook | Component rendered before `MidenProvider` finished initializing. Guard with `useMiden().isReady` or render via `MidenProvider`'s `loadingComponent`. | +| Transactions stuck in `"proving"` | Remote prover unreachable. Check `prover` config and network; consider `prover: { primary: "testnet", fallback: "local" }`. | +| Notes not appearing after mint | Call `sync()` from `useSyncState()` or verify `autoSyncInterval` isn't `0`. | +| Bech32 address has wrong prefix | `rpcUrl` doesn't match the network you intended. `"testnet"` → `mtst1...`, `"devnet"` → `mdev1...`. | +| WASM init fails in dev | Ensure your bundler serves `.wasm` with the `application/wasm` MIME type. Vite does this automatically; some custom setups don't. | +| `"A send is already in progress"` | The same `useSend` instance received another call before the previous one completed. Disable the trigger with `isLoading` and `await` the previous call. | + +## Next + +- Longer walkthrough: [React wallet tutorial](https://github.com/0xMiden/tutorials/blob/main/docs/src/web-client/react_wallet_tutorial.md) — builds a complete wallet app on top of these hooks. +- Reference: [Setup](./setup.md), [Query hooks](./query-hooks.md), [Mutation hooks](./mutation-hooks.md), [Advanced](./advanced.md), [Signers](./signers.md). diff --git a/versioned_docs/version-0.16/builder/tools/clients/react-sdk/setup.md b/versioned_docs/version-0.16/builder/tools/clients/react-sdk/setup.md new file mode 100644 index 00000000..2e49b680 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tools/clients/react-sdk/setup.md @@ -0,0 +1,227 @@ +--- +title: Setup +sidebar_position: 2 +--- + +# Setting up the React SDK + +## Install + +The React SDK has a hard peer dependency on `@miden-sdk/miden-sdk` — install both: + +```bash +npm install @miden-sdk/react @miden-sdk/miden-sdk +# or +yarn add @miden-sdk/react @miden-sdk/miden-sdk +# or +pnpm add @miden-sdk/react @miden-sdk/miden-sdk +``` + +React 18 or newer is required. + +## Wrap your app in `MidenProvider` + +`MidenProvider` loads the Web SDK's WebAssembly module, spins up the dedicated worker, wires the keystore, and kicks off the auto-sync loop. Put it at the root of your React tree — typically in `App.tsx` or your Next.js root layout. + +```tsx +import { MidenProvider } from "@miden-sdk/react"; + +function App() { + return ( + + + + ); +} +``` + +Every hook in the rest of this section assumes a `MidenProvider` is mounted somewhere above it. + +## Configuration + +```tsx +} // rendered while WASM boots + errorComponent={} // rendered if init fails +> + + +``` + +### `MidenConfig` fields + +| Field | Type | Description | +| --- | --- | --- | +| `rpcUrl` | `"devnet" \| "testnet" \| "localhost" \| string` | Node RPC endpoint. Shorthands expand to hosted Miden endpoints; any other string is treated as a raw URL. | +| `prover` | `"local" \| "devnet" \| "testnet" \| string \| ProverConfig` | Default prover. `"local"` runs in-browser. `ProverConfig` supports a `primary` + `fallback` pair if you want automatic fallback. | +| `autoSyncInterval` | `number` | Milliseconds between automatic sync pulls. `0` disables the loop (you can still call `sync()` manually). Default: 15000. | +| `noteTransportUrl` | `string` | Full note transport service URL. Required for `sendPrivate` / `fetchPrivate`. | +| `proverTimeoutMs` | `number` | Per-transaction prover timeout. | +| `seed` | `Uint8Array` | 32-byte RNG seed for deterministic account-ID derivation in tests. | + +### Network shorthands + +| Shorthand | Meaning | +| --- | --- | +| `devnet` | Development / pre-production testing, fake tokens | +| `testnet` | Pre-production testing against the hosted Miden testnet | +| `localhost` | Local node at `http://localhost:57291` | + +`MidenProvider` expands the `rpcUrl` network shorthands but not `noteTransportUrl`. Pass the full transport URL (`https://transport.miden.io` for testnet or `https://transport.devnet.miden.io` for devnet). + +### `loadingComponent` and `errorComponent` + +- `loadingComponent` is rendered while the provider initializes the client. +- `errorComponent` is rendered if initialization fails. It accepts either a `ReactNode` or `(error: Error) => ReactNode`. + +Both are optional. Without them, the provider renders its children; use `isReady`, `isInitializing`, and `error` to control their loading and error states. + +## Client lifecycle + +`useMiden()` is the raw context hook. Most apps never need it — the specialized hooks are easier — but it's there when you want to reach into lifecycle state directly. + +```tsx +import { useMiden } from "@miden-sdk/react"; + +function Status() { + const { isReady, isInitializing, error, sync } = useMiden(); + + if (error) return

Init error: {error.message}

; + if (isInitializing || !isReady) return

Loading Miden…

; + + return ; +} +``` + +- `isReady` — `true` once the client has initialized. For an external signer, also check `signerConnected`; client readiness does not imply that the wallet is connected. +- `isInitializing` — `true` while client initialization is in progress. +- `error` — non-null if init failed. +- `sync()` — trigger a manual sync pass outside the auto-sync loop. +- `runExclusive(fn: () => Promise): Promise` — serialize a block of async work under the internal lock. `fn` takes no arguments; reach for the client via `useMidenClient()` if you need one inside. See [serialized raw-client flows](./recipes.md#serialize-a-custom-raw-client-flow). + +`useMidenClient()` is a shortcut that returns the ready `WebClient` directly, throwing if the provider isn't ready yet: + +```tsx +import { useMiden, useMidenClient } from "@miden-sdk/react"; + +function LoadSyncHeightButton() { + const { isReady } = useMiden(); + if (!isReady) return ; + return ; +} + +function ReadyLoadSyncHeightButton() { + const client = useMidenClient(); + + const loadHeight = async () => { + const height = await client.getSyncHeight(); + console.log("Block:", height); + }; + + return ; +} +``` + +Use it for APIs the React SDK hooks don't expose. Keep the readiness check in a parent component so `useMidenClient()` is only called after initialization, without changing the order of hooks between renders. + +## Hook result conventions + +Each hook exports its own result interface — `UseSendResult`, `AccountsResult`, `NotesResult`, and so on — rather than a generic `QueryResult` wrapper. Data lives in named fields (e.g. `accounts` and `records`) not inside a common `data` key. The shared machinery is narrower than that: + +### Query hooks + +Every query hook exposes at least: + +```ts +{ + isLoading: boolean; + error: Error | null; + refetch: () => Promise; +} +``` + +Plus the hook-specific data fields. For example: + +```tsx +const { accounts, isLoading, error } = useAccounts(); + +if (isLoading) return ; +if (error) return

{error.message}

; +return ; +``` + +### Mutation hooks + +Every mutation hook exposes: + +```ts +{ + // Domain-specific action function — `send` for useSend, `mint` for useMint, etc. + [action]: (options) => Promise; + result: Result | null; + isLoading: boolean; + stage: TransactionStage; + error: Error | null; + reset: () => void; +} +``` + +The action function name mirrors the hook: `useSend` returns `send`, `useMint` returns `mint`, `useConsume` returns `consume`. That keeps call sites readable without destructured renames. + +Transaction-producing mutations progress through the `TransactionStage` states: + +```ts +type TransactionStage = + | "idle" + | "executing" + | "proving" + | "submitting" + | "complete"; +``` + +Pattern: + +```tsx +const { send, stage, isLoading, error } = useSend(); + +return ( + <> + + {error &&

{error.message}

} + +); +``` + +See [Mutation hooks](./mutation-hooks.md) for the full surface. + +## Account ID formats + +Hooks that take an account ID accept either format. Given a full `Account` (for example, from `useAccount`) passed to your component as a prop: + +```tsx +const accountIdHex = account.id().toString(); +const accountIdBech32 = account.bech32id(); + +useAccount(accountIdHex); +useAccount(accountIdBech32); +``` + +The SDK normalises internally — you don't need to convert yourself. Bech32 prefixes encode the network: `mtst1…` on testnet, `mdev1…` on devnet. The prefix is derived from the `rpcUrl` you configured on `MidenProvider`. + +## Next + +- [Query hooks](./query-hooks.md) — read account, note, sync, and metadata state. +- [Mutation hooks](./mutation-hooks.md) — create wallets, send, mint, consume, swap. +- [Advanced](./advanced.md) — custom scripts, session wallets, import/export. +- [Signers](./signers.md) — integrate Para, Turnkey, MidenFi, or a custom wallet. diff --git a/versioned_docs/version-0.16/builder/tools/clients/react-sdk/signers.md b/versioned_docs/version-0.16/builder/tools/clients/react-sdk/signers.md new file mode 100644 index 00000000..9e71d76e --- /dev/null +++ b/versioned_docs/version-0.16/builder/tools/clients/react-sdk/signers.md @@ -0,0 +1,223 @@ +--- +title: External signers +sidebar_position: 6 +--- + +# External signers + +The React SDK treats signing as a pluggable contract: `MidenProvider` accepts any `SignerContext` that exposes a `signCb` function, and hooks call into it whenever a transaction needs a signature. Prebuilt providers exist for the major wallet integrations; you can also build your own. + +## Built-in signer providers + +:::warning React SDK 0.16 limitation +`@miden-sdk/react` 0.16.0 cannot create a new account through an external signer. Para and Turnkey integrations must pass `importAccountId` for a pre-existing account whose auth component matches the connected signer. Importing an ID does not create an account or reconstruct private account state; the account must already be accessible to the client. The MidenFi adapter's normal path already imports its existing wallet account. +::: + +### Para (EVM wallets) + +```tsx +import { ParaSignerProvider } from "@miden-sdk/para-react"; +import { MidenProvider } from "@miden-sdk/react"; + +function App({ existingAccountId }: { existingAccountId: string }) { + return ( + + + + + + ); +} +``` + +Expose Para-specific data inside your app: + +```tsx +import { useParaSigner } from "@miden-sdk/para-react"; + +const { para, wallet, isConnected } = useParaSigner(); +``` + +### Turnkey + +```tsx +import { TurnkeySignerProvider } from "@miden-sdk/turnkey-react"; +import { MidenProvider } from "@miden-sdk/react"; + +function App({ existingAccountId }: { existingAccountId: string }) { + return ( + + + + + + ); +} +``` + +Connect via passkey authentication: + +```tsx +import { useSigner } from "@miden-sdk/react"; +import { useTurnkeySigner } from "@miden-sdk/turnkey-react"; + +function ConnectButton() { + const signer = useSigner(); + const turnkey = useTurnkeySigner(); // call unconditionally — rules of hooks + + if (!signer) return null; // no signer provider mounted + + if (!signer.isConnected) { + return ; + } + return ( + + ); +} +``` + +### MidenFi wallet adapter + +```tsx +import { MidenFiSignerProvider } from "@miden-sdk/miden-wallet-adapter-react"; +import { MidenProvider } from "@miden-sdk/react"; + + + + + + +``` + +## Unified signer interface + +Use `useSigner()` with any provider. It returns `null` when no signer provider is mounted: + +```tsx +import { useSigner } from "@miden-sdk/react"; + +function Header() { + const signer = useSigner(); + if (!signer) return null; // no signer provider above + + if (!signer.isConnected) { + return ; + } + return ; +} +``` + +## Custom signer providers + +Connect your signing service through `SignerContext`. With React SDK 0.16.0, the service must provide a pre-existing account ID and ECDSA K256/Keccak signatures for that account, plus its public-key commitment serialized as an SDK word. + +```tsx +import { MidenProvider, SignerContext, type SignerContextValue } from "@miden-sdk/react"; +import { AccountStorageMode } from "@miden-sdk/miden-sdk"; + +const signer: SignerContextValue = { + name: "MyWallet", + storeName: `mywallet_${signingService.identity}`, + isConnected: signingService.isConnected, + accountConfig: { + publicKeyCommitment: signingService.publicKeyCommitment, + storageMode: AccountStorageMode.public(), + importAccountId: signingService.accountId, + }, + signCb: async (pubKey, signingInputs) => { + if (!signingService.isConnected) throw new Error("MyWallet is not connected"); + return signingService.signMessage(pubKey, signingInputs); + }, + connect: async () => { await signingService.connect(); }, + disconnect: async () => { await signingService.disconnect(); }, +}; + + + + + + +``` + +Build this value inside your provider's render and update it when the connection changes. Use a unique `storeName` per signing identity to isolate each user's database. + +`importAccountId` must identify an account that was created for this signer and is already available from the network. It bypasses account construction; omitting it uses the unsupported 0.16.0 creation path. A public account ID alone cannot recover a private account's state. + +## Custom `AccountComponent`s + +Do not use `SignerAccountConfig.customComponents` to create an external-signer account with React SDK 0.16.0. The creation path is unsupported, while the `importAccountId` path bypasses the account builder and does not attach supplied components. Create the account with its application-specific components first, then import that existing account. + +## `MultiSignerProvider` + +Use `MultiSignerProvider` to switch signers at runtime. Register each provider with `` and place `MidenProvider` alongside them. On React SDK 0.16.0, each Para or Turnkey slot must import its own pre-existing account: + +```tsx +import { MultiSignerProvider, SignerSlot, MidenProvider } from "@miden-sdk/react"; +import { ParaSignerProvider } from "@miden-sdk/para-react"; +import { TurnkeySignerProvider } from "@miden-sdk/turnkey-react"; + +function App({ + paraAccountId, + turnkeyAccountId, +}: { + paraAccountId: string; + turnkeyAccountId: string; +}) { + return ( + + + + + + + + + + + + + ); +} +``` + +Connect and disconnect by name via `useMultiSigner()`: + +```tsx +import { useMultiSigner } from "@miden-sdk/react"; + +function SignerPicker() { + const multi = useMultiSigner(); + if (!multi) return null; // no MultiSignerProvider above + + return ( + <> + + + + + ); +} +``` + +`connectSigner(name)` uses the provider's `name`; `disconnectSigner()` clears the active signer. + +## Next + +- [Recipes](./recipes.md) — end-to-end patterns with signer integration examples. +- [Setup](./setup.md) — client config and lifecycle. diff --git a/versioned_docs/version-0.16/builder/tools/clients/rust-client/_category_.yml b/versioned_docs/version-0.16/builder/tools/clients/rust-client/_category_.yml new file mode 100644 index 00000000..ff26d637 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tools/clients/rust-client/_category_.yml @@ -0,0 +1,4 @@ +label: Rust +# Determines where this documentation section appears relative to other sections in the parent folder +position: 1 +collapsed: true diff --git a/versioned_docs/version-0.16/builder/tools/clients/rust-client/api-docs.md b/versioned_docs/version-0.16/builder/tools/clients/rust-client/api-docs.md new file mode 100644 index 00000000..f95595f4 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tools/clients/rust-client/api-docs.md @@ -0,0 +1,8 @@ +--- +title: API +sidebar_position: 9 +--- + +:::note +The latest and complete reference for the Miden client API can be found at [`Miden client docs.rs`](https://docs.rs/miden-client/latest/miden_client/). +::: diff --git a/versioned_docs/version-0.16/builder/tools/clients/rust-client/cli/_category_.yml b/versioned_docs/version-0.16/builder/tools/clients/rust-client/cli/_category_.yml new file mode 100644 index 00000000..791dc0ca --- /dev/null +++ b/versioned_docs/version-0.16/builder/tools/clients/rust-client/cli/_category_.yml @@ -0,0 +1,4 @@ +label: CLI +# Determines where this documentation section appears relative to other sections in the parent folder +position: 3 +collapsed: true diff --git a/versioned_docs/version-0.16/builder/tools/clients/rust-client/cli/cli-config.md b/versioned_docs/version-0.16/builder/tools/clients/rust-client/cli/cli-config.md new file mode 100644 index 00000000..f70d91b7 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tools/clients/rust-client/cli/cli-config.md @@ -0,0 +1,193 @@ +--- +title: Config +sidebar_position: 2 +--- + +After [installation](../install-and-run.md#install-the-client), use the client by running the following and adding the [relevant commands](index.md#commands): + +```sh +miden-client +``` + +:::tip +Run `miden-client --help` for information on `miden-client` commands. +::: + +## Client Configuration + +We configure the client using a [TOML](https://en.wikipedia.org/wiki/TOML) file (`miden-client.toml`). The file gets created when running `miden-client init`, which creates a `.miden` directory structure to organize all client-related files. By default, this directory is located in the HOME path, i.e. at `~/.miden`. Running this command is optional, but can be done if you want to have more fine-grained control over the configuration of the `miden-client`. The TOML file can also be edited to use a different configuration for the client. + +Paths in the configuration file are relative to the `.miden` directory containing it. + +```sh +store_filepath = "store.sqlite3" +secret_keys_directory = "keystore" +token_symbol_map_filepath = "token_symbol_map.toml" +remote_prover_endpoint = "http://localhost:8080" +package_directory = "packages" +max_block_number_delta = 256 + +[rpc] +endpoint = "http://localhost:57291" +timeout_ms = 10000 + +[note_transport] # optional +endpoint = "http://localhost:57292" +timeout_ms = 10000 + +[remote_prover_timeout] +secs = 20 +nanos = 0 +``` + +### Configuration Location and Priority + +The client supports both **global** and **local** configuration with intelligent priority handling: + +1. **Global Configuration** (default): Located at `~/.miden/miden-client.toml` in your home directory. The global directory location can be overridden with the `MIDEN_CLIENT_HOME` environment variable (see [Environment variables](#environment-variables)). +2. **Local Configuration** (project-specific): Located at `./.miden/miden-client.toml` in your current working directory + +**Priority Order**: Local configuration takes precedence over global configuration. If both exist, the client will use the local configuration and ignore the global one. + +### Initialization Options + +```bash +# Create global configuration (default behavior) +miden-client init + +# Create local configuration in current directory +miden-client init --local +``` + +The global configuration approach reduces per-project setup overhead while still allowing project-specific customization when needed. + +### Configuration Management + +#### Clear Command + +The `clear-config` command helps manage configuration by removing existing setups: + +```bash +# Remove local config if present, otherwise remove global config +miden-client clear-config + +# Force removal of global configuration only +miden-client clear-config --global +``` + +**Priority Behavior**: The clear command follows the same priority logic as config loading - it will remove the local configuration first if it exists, and only remove the global configuration if no local configuration is found. This ensures you don't accidentally lose both configurations at once. + +**Use Cases**: +- Resetting configuration between releases when changes require clean state +- Switching from local to global configuration (or vice versa) +- Troubleshooting configuration-related issues + +### RPC + +An `rpc` section is used to configure the connection to the Miden node. It contains the following fields: + +- `endpoint`: The Miden node endpoint as a URL, such as `"https://rpc.devnet.miden.io"`. + +This field can be set with the `--network` flag when running the `miden-client init` command. For example, to set the testnet endpoint, you can run: `miden-client init --network testnet`. + +:::note + +- Running the node locally for development is encouraged. +- However, the endpoint can point to any remote node. + ::: + +### Store and keystore + +The `store_filepath` field is used to configure the path to the SQLite database file used by the client. The `secret_keys_directory` field is used to configure the path to the directory where the keystore files are stored. The default values are `store.sqlite3` and `keystore`, respectively. These paths are resolved relative to the `.miden` directory containing the configuration file. + +The store filepath can be set when running the `miden-client init` command with the `--store-path` flag. + +### Default account ID + +The default account is stored in the client's database, not in `miden-client.toml`. When no default exists, a newly created wallet or imported basic wallet becomes the default. + +You can set and unset it with: + +```sh +miden-client account --default #Sets default account +miden-client account --default none #Unsets default account +``` + +:::note +The account must be tracked by the client in order to be set as the default account. +::: + +You can also see the current default account ID with: + +```sh +miden-client account --default +``` + +### Token symbol map + +The `token_symbol_map_filepath` field is used to configure the path to the TOML file that contains the token symbol map. The token symbol map stores the faucet details for different token symbols. The default value is `token_symbol_map.toml`, resolved relative to the `.miden` directory containing the configuration file. + +This file must be updated manually with known token symbol mappings. A sample token symbol map file looks like this: + +```toml +# This addresses in this file are not real and are only for demonstration purposes. +ETH = { address = "mlcl1qru2e5yvx40ndgqqqzusrryr0ucyd0uj", decimals = 18 } +BTC = { address = "mlcl1qple0ejnutx8zyp0cm0pme9wjfgqz0u9djq", decimals = 8 } +``` + +The `address` field must be the faucet account's full Bech32 address; hexadecimal account IDs are not accepted in this file. The `decimals` field is the number of decimals used by the token. + +When the client is configured with a token symbol map, any transaction command that specifies an asset can use the token symbol instead of the asset ID. For example, when specifying an asset normally you would use something like: +`1::mlcl1qple0ejnutx8zyp0cm0pme9wjfgqz0u9djq` + +But if the faucet is included in the token symbol map (using the sample above as the mapping), you would use: +`0.00000001::BTC` + +Notice how the amount specified when using the token symbol takes into account the decimals of the token (`1` base unit of the token is `0.00000001` for BTC as it uses 8 decimals). + +### Remote prover endpoint + +The `remote_prover_endpoint` field is used to configure a remote prover. Set it by calling `miden-client init --remote-prover-endpoint `. To use the configured remote prover, pass `--delegate-proving` to a transaction-creation command, for example `miden-client transfer --delegate-proving`. Without this flag, transactions are proved locally. + +### Package directory +`Packages` are Miden's native packaging format. +This structure contains the outputs of a compiled project, with all of its corresponding metadata. Specifically, a `Package` may contain the compiled MAST for an `Account Component` in the form of a `Library`. + +The `package_directory` field is used to configure the path to the directory where the account components are stored in package (`.masp`) form. The default value is `packages`, resolved relative to the `.miden` directory containing the configuration file. + +In this directory you can place the packages used to create the account components. These define the interface of the account that will be created. + +For more information on miden packages, see: +- [The mast-package crate](https://github.com/0xMiden/miden-vm/blob/next/crates/mast-package/README.md) +- [The Miden package's status article on the Miden compiler](https://docs.miden.xyz/core-concepts/compiler/) + +### Block Delta + +The `max_block_number_delta` is an optional field that is used to configure the maximum number of blocks the client can be behind the network. + +If not set, the default behavior is to ignore the block difference between the client and the network. If set, the client will check this difference is within the specified maximum when validating a transaction. + +```sh +miden-client init --block-delta 256 +``` + +### Environment variables + +- `MIDEN_CLIENT_HOME`: Overrides the default global `.miden` directory (`~/.miden`). When set, all commands that reference the global directory will use the specified path instead. This is useful for keeping separate environments or storing the client data in a non-default location. For example: + + ```sh + export MIDEN_CLIENT_HOME=/path/to/custom/miden + miden-client init + ``` + + Note that this only affects the **global** directory. If a local `./.miden` directory exists, it still takes precedence over the global one (whether default or overridden). + +### Note Transport + +A `note_transport` section is used to configure the connection to the Miden Note Transport node used in the exchange of private notes. It contains the following fields: +- `endpoint`: The endpoint of the Miden Note Transport node; +- `timeout_ms`: The timeout employed in client requests to the node. + +> [!Note] +> - Running the node locally for development is encouraged. +> - However, the endpoint can point to any remote node. diff --git a/versioned_docs/version-0.16/builder/tools/clients/rust-client/cli/cli-troubleshooting.md b/versioned_docs/version-0.16/builder/tools/clients/rust-client/cli/cli-troubleshooting.md new file mode 100644 index 00000000..de916afd --- /dev/null +++ b/versioned_docs/version-0.16/builder/tools/clients/rust-client/cli/cli-troubleshooting.md @@ -0,0 +1,134 @@ +--- +title: Troubleshooting +sidebar_position: 3 +--- + +## Troubleshooting and transaction lifecycle (CLI) + +This guide helps you troubleshoot common issues and understand the end-to-end lifecycle of transactions and notes in the Miden client. + +### TL;DR checklist + +> Note: This section applies to the Miden CLI client. Guidance for the Rust and Web clients may differ. + +- Ensure you have a proper configuration setup: either a global config at `~/.miden/miden-client.toml` or a local config at `./.miden/miden-client.toml`. Local config takes priority if both exist. +- If you need a clean local state, delete the SQLite store file referenced by `store_filepath` (default: `.miden/store.sqlite3`). It will be recreated automatically on the next command. +- Verify your node RPC endpoint is reachable and correct in your configuration file (local `.miden/miden-client.toml` or global `~/.miden/miden-client.toml`). +- Run `miden-client sync` to refresh local state after errors involving missing data or outdated heights. + +### Typical CLI outputs + +```sh +# Force non-interactive submission (e.g., CI) +miden-client transfer --force ... + +# Refresh local state +miden-client sync +``` + +If you see a gRPC error, it may include a status-derived kind (e.g. `Unavailable`, `InvalidArgument`) which narrows possible causes. + +### Common errors and how to resolve + +Below are representative errors you may encounter, their likely causes, and suggested fixes. + +#### `RpcError.GrpcError: Unavailable` / `DeadlineExceeded` +- Cause: Node is down, unreachable, or behind a load balancer that blocked the request. +- Fix: Check `rpc.endpoint` in your configuration file (local `.miden/miden-client.toml` or global `~/.miden/miden-client.toml`), verify the node is running/accessible, and retry. + +#### `RpcError.InvalidArgument` / `ExpectedDataMissing` / `InvalidResponse` +- Cause: Malformed request parameters or unexpected server response. +- Fix: Re-check command flags/inputs. If using partial IDs, ensure they map to a single entity. Update to the latest client if the server API has changed. + +#### Client/network compatibility mismatch +- Cause: Client and network versions or the genesis header commitment are incompatible. +- Symptoms: CLI may report messages like: + + ``` + accept header validation failed: server rejected request - please check your version and network settings + ``` + + or requests being rejected due to a mismatched genesis header commitment. +- Details: These are validated by the node by verifying client headers on gRPC requests. +- Fix: Ensure your client version matches the target network. Switch to the correct network or upgrade/downgrade the client accordingly. Verify the configured genesis header commitment matches the network, then retry. + +#### `ClientError.AccountDataNotFound()` +- Cause: The account is not known to the local store yet. +- Fix: Create/import the account first, or run `miden-client sync` to fetch it if it exists on-chain. + +#### `ClientError.AccountLocked()` +- Cause: Attempting to modify a locked account. +- Fix: Unlock or use another account as appropriate. + +#### `ClientError.StoreError(AccountCommitmentAlreadyExists(...))` +- Cause: Trying to apply a transaction whose final account commitment is already present locally. +- Fix: Ensure you are not re-applying the same transaction. Sync and check transaction status. + +#### `ClientError.NoteNotFoundOnChain()` / `RpcError.NoteNotFound()` +- Cause: The note has not been published/committed yet or the ID is incorrect. +- Fix: Verify the note ID. If it should exist, run `miden-client sync` and retry. + +#### `ClientError.TransactionInputError` / `TransactionScriptError` +- Cause: Invalid transaction inputs, script logic errors, or failing constraints. +- Fix: Validate input notes, foreign accounts, and script assumptions. + +#### `ClientError.TransactionProvingError` +- Cause: Local proving failed or remote prover returned an error. +- Fix: If using remote proving, verify `remote_prover_endpoint` is reachable and add `--delegate-proving`. Check prover logs. + +#### Recency/block delta errors +- Cause: Client is too far behind the network and validation enforces a max delta. +- Fix: Run `miden-client sync` or increase `max_block_number_delta` via `miden-client init --block-delta ` and re-run. + +### Transaction lifecycle (CLI-oriented overview) + +For the full protocol-level lifecycle, see the Miden book: [Transaction lifecycle](https://docs.miden.xyz/builder/smart-contracts/transactions/introduction#transaction-lifecycle). + +```mermaid +flowchart LR + A[Build Request] --> B[Validate Request] + A -.->|optional| C[Collect/Insert Input Notes] + A -.->|optional| D[Load Foreign Accounts] + B -.->|optional| K[Insert Public Note Recipients] + B --> E[Execute Transaction] + E --> F[Prove Transaction] + F --> G[Submit to Node] + G --> H[Track Locally] + + subgraph Tracking + H --> I[Update Account State] + H --> J[Update Notes/Tags] + end +``` + +Key states the CLI surfaces: + +- Transaction status: `Pending` (after execution), `Committed` (after node inclusion), `Discarded` (not included). +- Input notes: `Expected` → `Processing` → `Consumed` (after sync) or `Committed` if fetched with inclusion. + +### Configuration troubleshooting + +#### Config priority confusion +- **Issue**: Unclear which configuration is being used (local vs global) +- **Check**: Run commands from different directories to see if behavior changes +- **Local priority**: If `./.miden/miden-client.toml` exists, it overrides `~/.miden/miden-client.toml` +- **Fix**: Use `miden-client clear-config` to remove unwanted configurations, or `miden-client clear-config --global` to remove only global config. Note: Running `miden-client clear-config` without flags follows priority: if a local .miden folder exists, it removes only that one; if no local folder exists, it removes the global one. Use `--global` to specifically target the global configuration regardless of local config presence. + +#### Clean configuration reset +- **Complete reset**: Use `miden-client clear-config` to remove the active configuration (follows priority: local first, then global) +- **Selective reset**: Use `miden-client clear-config --global` to remove only global configuration while preserving local +- **Fresh start**: After clearing, run `miden-client init` (global) or `miden-client init --local` (local) to recreate + +### Recovery flow + +1. Verify `rpc.endpoint` connectivity and timeouts. +2. Run `miden-client sync` to refresh local headers/notes. +3. If local DB is inconsistent for development purposes, delete the store file (`.miden/store.sqlite3` in local config or `~/.miden/store.sqlite3` in global config) and retry. +5. For configuration issues, use `miden-client clear-config` to reset config and `miden-client init` to recreate. +6. Adjust `max_block_number_delta` if strict recency checks block validation. +7. If proving errors persist with a remote prover, confirm `remote_prover_endpoint` and consider running locally to isolate the issue. + +### References + +- Common error enums originate from the client and RPC layers. +- Protocol lifecycle: [Miden book — Transaction lifecycle](https://docs.miden.xyz/builder/smart-contracts/transactions/introduction#transaction-lifecycle) diff --git a/versioned_docs/version-0.16/builder/tools/clients/rust-client/cli/index.md b/versioned_docs/version-0.16/builder/tools/clients/rust-client/cli/index.md new file mode 100644 index 00000000..4a6e092b --- /dev/null +++ b/versioned_docs/version-0.16/builder/tools/clients/rust-client/cli/index.md @@ -0,0 +1,536 @@ +--- +title: CLI +--- + +The following document lists the commands that the CLI currently supports. + +:::tip +Use `--help` as a flag on any command for more information. +::: + +## Usage + +Call a command on the `miden-client` like this: + +```sh +miden-client +``` + +## Commands + +### `init` + +Creates a global configuration file for the client. Pass `--local` to create one in the current directory. Running this command is optional, as the client will self-initialize by default. By default, the command uses the Testnet network. + +```sh +# This will create a config file named `miden-client.toml` using default values +# This file contains information useful for the CLI like the RPC provider and database path +miden-client init + +# You can set up the CLI for any of the default networks +miden-client init --network testnet +miden-client init --network devnet +miden-client init --network localhost + +# You can also specify a custom network +miden-client init --network http://18.203.155.106 +# You can specify the port +miden-client init --network http://18.203.155.106:8080 +# You can use HTTPS +miden-client init --network https://18.203.155.106 +# You can specify both +miden-client init --network https://18.203.155.106:1234 + +# You can use the --store-path flag to override the default store config +miden-client init --store-path db/store.sqlite3 + +# You can use the --block-delta flag to set maximum number of blocks the client can be behind +miden-client init --block-delta 250 + +# You can provide both flags +miden-client init --network http://18.203.155.106 --store-path db/store.sqlite3 + +# You can set a remote prover to offload the proving process (along with the `--delegate-proving` flag in transaction commands) +miden-client init --remote-prover-endpoint + +# To enable the transport layer, specify the endpoint +miden-client init --note-transport-endpoint +``` + +More information on the configuration file can be found in the [configuration section](cli-config.md). + +### `account` + +Inspect account details. + +#### Action Flags + +| Flags | Description | Short Flag | +| ---------------------------- | ------------------------------------------------------------ | ---------- | +| `--list` | List all accounts monitored by this client | `-l` | +| `--show ` | Show details of the account for the specified ID | `-s` | +| `--inspect ` | List the procedures an account exposes, or resolve a single one | | +| `--default ` | Manage the setting for the default account | `-d` | + +The `--show` flag also accepts a partial ID instead of the full ID. For example, instead of: + +```sh +miden-client account --show 0x8fd4b86a6387f8d8 +``` + +You can call: + +```sh +miden-client account --show 0x8fd4b86 +``` + +For the `--default` flag, if `` is "none" then the previous default account is cleared. If no `` is specified then the default account is shown. + +The `--inspect` flag lists the procedures an account exposes, grouped into resolved procedures (shown in a table with their name, signature, and MAST root) and unresolved ones (listed by their MAST root under a hint to pass `--package`). Pass `:` to resolve a single procedure by name; if no procedure with that name can be resolved (the account does not expose it, or its defining package was not provided) the command fails with an error. Like `--show`, it accepts a partial ID. It supports two additional flags: + +- `-p, --package `: Supplies an additional `.masp` package used to resolve procedure MAST roots to their names and signatures, on top of the packages in the configured packages directory. It is repeatable (pass it once per package); when the same MAST root is exported by more than one package the first-loaded one wins (passed packages are consulted first) and a warning lists the packages involved. Procedures whose name cannot be resolved are still listed by their MAST root. +- `-v, --verbose`: Prints the MASM disassembly of each procedure. + +### `new-wallet` + +Creates a new wallet account. + +A basic wallet is comprised of a basic authentication component (for RPO Falcon signature verification), alongside a basic wallet component (for sending and receiving assets). + +This command has three optional flags: + +- `-t, --account-type `: Used to select the account visibility (private if not specified). It may receive "private" or "public". This is the only thing the protocol's `AccountType` encodes. +- `--extra-packages `: Specifies a list of file paths for packages holding account components to include in the account. If the packages contain placeholders, the CLI will prompt the user to enter the required data for instantiating storage appropriately. +- `--init-storage-data-path `: Specifies an optional file path to a TOML file containing key/value pairs used for initializing storage. Each key should map to a placeholder within the packages' component metadata. The CLI will prompt for any keys that are not present in the file. + +After creating an account with the `new-wallet` command, it is automatically stored and tracked by the client. This means the client can execute transactions that modify the state of accounts and track related changes by synchronizing with the Miden network. + +### `new-account` + +Creates a new account and saves it locally. + +An account may be composed of one or more components, each with its own storage and distinct functionality. This command lets you build a custom account by selecting an account type and optionally adding extra component packages. + +This command has four flags: + +- `-t, --account-type `: Specifies the account visibility. It accepts either "private" or "public", with "private" as the default. This is the only thing the protocol's `AccountType` encodes. + +There is no `--faucet` flag: faucet-vs-regular is derived from the packages. If any package contributes the `FungibleFaucet` component, the resulting account is treated as a fungible faucet and an implicit `TokenPolicyManager` is installed when one is not already provided. `--account-type` only selects visibility. +- `--packages `: Specifies a list of file paths for packages holding account components to include in the account. If the packages contain placeholders, the CLI will prompt the user to enter the required data for instantiating storage appropriately. +- `--init-storage-data-path `: Specifies an optional file path to a TOML file containing key/value pairs used for initializing storage. Each key should map to a placeholder within the packages' component metadata. The CLI will prompt for any keys that are not present in the file. + +After creating an account with the `new-account` command, the account is stored locally and tracked by the client, enabling it to execute transactions and synchronize state changes with the Miden network. + +#### Examples + +```bash +# Create a new wallet with default settings (private visibility, no extra components) +miden-client new-wallet + +# Create a new wallet with public visibility +miden-client new-wallet -t public + +# Create a new wallet that includes custom packages +miden-client new-wallet --extra-packages packages/custom-package.masp + +# Create a fungible faucet with interactive input +# (the resulting account is a faucet because basic-fungible-faucet.masp contributes the +# `FungibleFaucet` component — no extra flag is needed) +miden-client new-account --packages packages/basic-fungible-faucet.masp + +# Create a fungible faucet with preset fields +miden-client new-account --packages packages/basic-fungible-faucet.masp --init-storage-data-path init_data.toml +``` + +where `init_data.toml` is a TOML file with the following example content: +```toml +token_metadata.max_supply = 1000000000 +token_metadata.decimals = 6 +token_metadata.ticker = "TEST" +``` + +### `info` + +View a summary of the current client state. + +#### Action Flags + +| Flag | Description | Short Flag | +| -------------- | -------------------------------------------- | ---------- | +| `--rpc-status` | Display detailed RPC node status information | `-r` | + +When using the `--rpc-status` flag, the command displays additional information about the RPC node including: + +- Node version +- Genesis commitment +- Store connection status and chain tip +- Block producer status and chain tip + +### `notes` + +View and manage notes. Also, exchange private notes using the note transport network. + +#### Action Flags + +| Flags | Description | Short Flag | +| ----------------------- | -------------------------------------------------------- | ---------- | +| `--list []` | List input notes | `-l` | +| `--show ` | Show details of the input note for the specified note ID | `-s` | +| `--send
` | Send a note using the note transport network | | +| `--fetch` | Fetch notes from the note transport network | | + +The `--list` flag receives an optional filter: - expected: Only lists expected notes. - committed: Only lists committed notes. - consumed: Only lists consumed notes. - processing: Only lists processing notes. - consumable: Only lists consumable notes. An additional `--account-id ` flag may be added to only show notes consumable by the specified account. +If no filter is specified then all notes are listed. + +The `--show` flag also accepts a partial ID instead of the full ID. For example, instead of: + +```sh +miden-client notes --show 0x70b7ecba1db44c3aa75e87a3394de95463cc094d7794b706e02a9228342faeb0 +``` + +You can call: + +```sh +miden-client notes --show 0x70b7ec +``` + +To send a private note, the `--send` flag sends a note using the note transport network. +The note ID (hex, in full or a prefix) and recipient's address (bech32) must be provided. +The note is assumed to be stored in the store (e.g., imported using [`import`](#import)). + +You can call: + +```sh +miden-client notes --send 0xc1234567 mm1qpkdyek2c0ywwvzupakc7zlzty8qn2qnfc +``` + +To fetch private notes, the `--fetch` allows to download notes from the note transport network. +Only notes for tracked tags will be fetched (e.g. `miden-client tags --list`). +The downloaded notes will be added to the store. + +```sh +miden-client notes --fetch +``` + +### `network-note-status` + +Query the network for the processing status of a note. This is useful for diagnosing issues with network transactions (NTX), such as notes that are stuck or have been discarded. + +```sh +miden-client network-note-status +``` + +The command displays a table with the following information: + +- **Status**: The current processing state of the note (`Pending`, `Processed`, `Discarded`, or `Committed`). +- **Attempt Count**: The number of times the node has attempted to process the note. +- **Last Error**: The last error encountered during processing, if any. +- **Last Attempt Block**: The block number of the most recent processing attempt. + +The note ID must be provided as a full hex string: + +```sh +miden-client network-note-status 0x70b7ecba1db44c3aa75e87a3394de95463cc094d7794b706e02a9228342faeb0 +``` + +:::note +This command queries the Miden node directly and does not require the note to be tracked locally. +::: + +### `sync` + +Sync the client with the latest state of the Miden network. Shows a brief summary at the end. + +### `tags` + +View and add tags. + +#### Action Flags + +| Flag | Description | Aliases | +| ---------------- | ----------------------------------------------------------- | ------- | +| `--list` | List all tags monitored by this client | `-l` | +| `--add ` | Add a new tag to the list of tags monitored by this client | `-a` | +| `--remove ` | Remove a tag from the list of tags monitored by this client | `-r` | + +### `tx` + +View transactions. + +#### Action Flags + +| Command | Description | Aliases | +| -------- | ------------------------- | ------- | +| `--list` | List tracked transactions | -l | + +After a transaction gets executed, two entities start being tracked: + +- The transaction itself: It follows a lifecycle from `Pending` (initial state) and `Committed` (after the node receives it). It may also be `Discarded` if the transaction was not included in a block. +- Output notes that might have been created as part of the transaction (for example, when executing a pay-to-id transaction). + +### Transaction creation commands + +#### `mint` + +Creates a note that contains a specific amount tokens minted by a faucet, that the target Account ID can consume. + +Usage: `miden-client mint --target --asset :: --note-type ` + +#### `consume-notes` + +Account ID consumes a list of notes, specified by their Note ID. + +Usage: `miden-client consume-notes --account [NOTES]` + +For this command, you can also provide a partial ID instead of the full ID for each note. So instead of + +```sh +miden-client consume-notes --account 0x70b7ecba1db44c3aa75e87a3394de95463cc094d7794b706e02a9228342faeb0 0x80b7ecba1db44c3aa75e87a3394de95463cc094d7794b706e02a9228342faeb0 +``` + +You can do: + +```sh +miden-client consume-notes --account 0x70b7ecb 0x80b7ecb +``` + +Additionally, you can optionally not specify note IDs, in which case any note that is known to be consumable by the executor account ID will be consumed. + +Either `Expected` or `Committed` notes may be consumed by this command, changing their state to `Processing`. It's state will be updated to `Consumed` after the next sync. + +#### `transfer` + +Transfers assets to another account. Sender Account creates a note that a target Account ID can consume. The asset is identified by the tuple `(FAUCET ID, AMOUNT)`. The note can be configured to be recallable making the sender able to consume it after a height is reached. + +Usage: `miden-client transfer --sender --target --asset :: --note-type [--recall-height ]` + +#### `swap` + +The source account creates a `SWAP` note that offers some asset in exchange for some other asset. When another account consumes that note, it will receive the offered asset amount and the requested asset will removed from its vault (and put into a new note which the first account can then consume). Consuming the note will fail if the account doesn't have enough of the requested asset. + +Usage: `miden-client swap --source --offered-asset :: --requested-asset :: --note-type [--payback-note-type ]` + +The `--payback-note-type` option controls the visibility of the payback note created when the swap is consumed. It defaults to `private`. + +### `address` + +View and manage addresses. + +#### Action Subcommands + +| Subcommand | Description | +| ----------------------------------- | -------------------------------------------------------------------------------------------------| +| `list ` | List all addresses or only for the specified account ID (default command) | +| `add
` | Track a bech32-encoded address on the specified account ID | +| `remove
` | Remove a bech32-encoded address from the specified account ID | +| `encode ` | Produce a bech32 address from the account ID, interface, and optional tag length | + +The `list` subcommand optionally takes an account ID to only show the addresses of that account, if it is not provided, it will show all addresses of all accounts. + +```sh +miden-client address list 0x17f13f4f83a8e8100c19d2961dfda2 +``` + +`add` and `remove` take the account ID and a bech32-encoded address as arguments. `add` validates that the bech32 address encodes the same account ID and that its network matches the CLI's configured network. + +Use `encode` to produce a bech32 address from its fields — this output is what `add` expects. The interface can be: +- `basic-wallet`: The basic wallet interface. + +Note: `Unspecified` (shown by `address list`) denotes an address not bound to any interface, it's the default address for every account created. + +```sh +# Produce a bech32 address for the given account, interface, and tag length +miden-client address encode 0x17f13f4f83a8e8100c19d2961dfda2 basic-wallet 10 + +# Track that address on the account +miden-client address add 0x17f13f4f83a8e8100c19d2961dfda2 mlcl1qple0ejnutx8zyp0cm0pme9wjfgqz0u9djq_qruqqypuyph +``` + +```sh +miden-client address remove 0x17f13f4f83a8e8100c19d2961dfda2 mlcl1qple0ejnutx8zyp0cm0pme9wjfgqz0u9djq +``` + +#### Tips + +For `transfer` and `consume-notes`, you can omit the `--sender` and `--account` flags to use the client's [default account](cli-config.md#default-account-id). If you omit the flag but have no default account set, you'll get an error instead. + +For every command which needs an account ID (either wallet or faucet), you can also provide a partial ID instead of the full ID for each account. So instead of + +```sh +miden-client transfer --sender 0x80519a1c5e3680fc --target 0x8fd4b86a6387f8d8 --asset 100::0xa99c5c8764d4e011 --note-type private +``` + +You can do: + +```sh +miden-client transfer --sender 0x80519 --target 0x8fd4b --asset 100::0xa99c5c8764d4e011 --note-type private +``` + +!!! note +The only exception is for using IDs as part of the asset, those should have the full faucet's account ID. + +#### Transaction confirmation + +When creating a new transaction, a summary of the transaction updates will be shown and confirmation for those updates will be prompted: + +```sh +miden-client ... + +TX Summary: + +... + +Continue with proving and submission? Changes will be irreversible once the proof is finalized on the network (y/N) +``` + +This confirmation can be skipped in non-interactive environments by providing the `--force` flag (`miden-client transfer --force ...`). + +#### Delegated proving + +If a remote prover is configured, the CLI can offload the proving process to it. This is done by providing the `--delegate-proving` flag when creating a transaction. The CLI will then send the transaction to the remote prover for processing. + +### Importing and exporting + +#### `export` + +Export input note data to a binary file . + +| Flag | Description | Aliases | +| ----------------------------- | ------------------------------------- | ------- | +| `--filename ` | Desired filename for the binary file. | `-f` | +| `--export-type ` | Exported note type. | `-e` | + +##### Export type + +The user needs to specify how the note should be exported via the `--export-type` flag. The following options are available: + +- `id`: Only the note ID is exported. When importing, if the note ID is already tracked by the client, the note will be updated with missing information fetched from the node. This works for both public and private notes. If the note isn't tracked and the note is public, the whole note is fetched from the node and is stored for later use. +- `full`: The note is exported with all of its information (metadata and inclusion proof). When importing, the note is considered unverified. The note may not be consumed directly after importing as its block header will not be stored in the client. The block header will be fetched and be used to verify the note during the next sync. At this point the note will be committed and may be consumed. +- `partial`: The note is exported with minimal information and may be imported even if the note is not yet committed on chain. At the moment of importing the note, the client will check the state of the note by doing a note sync, using the note's tag. Depending on the response, the note will be either stored as "Expected" or "Committed". + +#### `import` + +Import entities managed by the client, such as accounts and notes. The type of entities is inferred. + +The `--overwrite` flag can be used when importing accounts. It allows the user to overwrite existing accounts with the same ID. This is useful when you want to update the account's information or replace it with a new version. + +### Executing scripts + +#### `exec` + +Execute the specified program against the specified account. + +| Flag | Description | Aliases | +| ----------------------------- | -------------------------------------------- | ------- | +| `--account ` | Account ID to use for the program execution. | `-a` | +| `--script-path ` | Path to script's source code to be executed. | `-s` | +| `--inputs-path ` | Path to the inputs file. | `-i` | +| `--hex-words` | Print the output stack grouped into words. | | + +The file referenced by `--inputs-path` should contain a TOML array of inline tables, where each table has two fields: - `key`: a 256-bit hexadecimal string representing a word to be used as a key for the input entry. The hexadecimal value must be prefixed with 0x. - `values`: an array of 64-bit unsigned integers representing field elements to be used as values for the input entry. Each integer must be written as a separate string, within double quotes. + +The input file should contain a TOML table called `inputs`, as in the following example: + +```toml +inputs = [ { key = "0x0000000000000000000000000000000000000000000000000000001000000000", values = ["13", "9"]}, { key = "0x0000000000000000000000000000000000000000000000000000000000000000" , values = ["1", "2"]}, ] +``` + +#### `call` + +Call a procedure on an account and show what it returns, along with the state changes the call would produce. + +Usage: `miden-client call : [ARGS]... [--package ]` + +| Flag | Description | Aliases | +| ----------------------------- | ------------------------------------------------------------- | ------- | +| `--package ` | The `.masp` package that exports the procedure, as a path or a name resolved in the packages directory. Optional. | `-p` | +| `--inputs-path ` | Path to a TOML file with advice map entries. | `-i` | + +The target is a single argument of the form `:`. For an account tracked by the client, the ID may be given as a partial ID; an account that isn't tracked has to be named by its full hex ID or its bech32 address, since a prefix is resolved against the local store. The procedure name is matched against the package's exports with `_` and `-` treated as equivalent, so it can be written in either snake_case or kebab-case (`get_count` matches the export `get-count`). + +`--package` takes either a path to a `.masp` file or a bare name, which is looked up in the configured packages directory. It is optional: without it, `` must be the procedure's hex digest instead of its name, and the output stack is printed as raw field elements since there is no manifest to read the signature from. + +Arguments are passed positionally after the target, one token per value in the procedure's signature. The signature comes from the package manifest, and it also decides how each token is read and how the result is printed: + +| Type | Token form | Example | +| ---- | ---------- | ------- | +| `felt` | decimal field element | `42` | +| integers (`u8`…`u128`, `i8`…`i128`) | decimal, range-checked against the type | `-1` | +| `bool` | `true`, `false`, `1` or `0` | `true` | +| `word` | hex | `0x00..` | +| `account-id` | hex account ID | `0x4614b8bf575eab71455e97bd394e90` | +| `asset` | `::`, fungible only | `100::0xabcdef0123456789` | +| records and fixed arrays | one token per field, in order | `3 4` for `point { x, y }` | + +Only procedures exported from a WIT interface carry a signature. A procedure without one is still called, with one raw field element per argument written in decimal (a `0x` hex literal is not accepted); the argument count is not checked and the result is printed as a stack dump. + +The arguments are pushed onto the stack so that the first one ends up on top, and together they may occupy at most 16 stack values — that is all a called procedure can see. + +`--inputs-path` takes the same TOML format as [`exec`](#exec). The entries are loaded into the VM's advice map and are visible to the called procedure. + +##### Example + +Calling `increment-count` on a counter contract: + +```sh +miden-client call 0x4614b8bf575eab71455e97bd394e90:increment-count --package target/miden/dev/counter-contract.masp +``` + +The command first prints the procedure's signature and its return values, then the effects the call has on the account: + +```sh +Signature: increment-count() -> felt + +Result: 1 +The transaction will have the following effects: + +No notes will be consumed. + +No notes will be created as a result of this transaction. + +Account Storage will not be changed. +Storage map changes: +┌──────────────────────────────────┬──────────────────────────────────┬─────────────────────────────────┐ +│ Storage Slot ┆ Map Key ┆ New Value │ +╞══════════════════════════════════╪══════════════════════════════════╪═════════════════════════════════╡ +│ counter_contract::counter_contra ┆ 0x000000000000000000000000000000 ┆ 0x01000000000000000000000000000 │ +│ ct::count_map ┆ 00000000000000000001000000000000 ┆ 0000000000000000000000000000000 │ +│ ┆ 00 ┆ 0000 │ +└──────────────────────────────────┴──────────────────────────────────┴─────────────────────────────────┘ +Account Vault will not be changed. +Nonce incremented by: 1. +``` + +A procedure that only reads leaves the transaction with no effects at all, which the transaction kernel does not allow. The result is still printed, followed by a note that the transaction was rejected for having no effects. + +:::note +The call is executed locally. No proof is generated, nothing is submitted to the network, and the account's stored state is left unchanged. +::: + +##### Calling an account that isn't tracked locally + +If the target account is not in the local store, the client reads its state from the network and runs the call from one of your own accounts — the default account if one is set, otherwise the first usable one. That account only runs the call; nothing about it changes. + +This requires the target account's state to be public, so the node can serve it, and it requires at least one of your own accounts to run the call from (accounts whose local state is out of sync with the node are skipped). Such calls can only read the account: the transaction kernel rejects any procedure that would mutate an account other than the one running the transaction, so only the return values are printed. No state delta is shown either — the only account a delta could describe is the one running the call, whose changes come from its own authentication and nonce rather than from the procedure. The account has to be named by its full hex ID or its bech32 address — a partial ID is resolved against the local store, which by definition does not have this account. + +```sh +miden-client call 0x4614b8bf575eab71455e97bd394e90:get-count --package target/miden/dev/counter-contract.masp +``` + +```sh +Account 0x4614b8bf575eab71455e97bd394e90 isn't tracked locally; reading its state from the network and running the call from your account 0x8fa1c2.... + +Signature: get-count() -> felt + +Result: 1 + +A call on an account read from the network can only read it; no state delta. +``` + +:::note +The account state read this way comes from the transaction's reference block, which the wallet running the call picks. It is not revalidated against the account's current on-chain state, so run `miden-client sync` first if you need a recent value. +::: + +### `note-transport` + +Send and fetch private notes using the transport layer. diff --git a/versioned_docs/version-0.16/builder/tools/clients/rust-client/debug-output.md b/versioned_docs/version-0.16/builder/tools/clients/rust-client/debug-output.md new file mode 100644 index 00000000..43adfce8 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tools/clients/rust-client/debug-output.md @@ -0,0 +1,64 @@ +--- +title: MASM Debug Output +sidebar_position: 8 +--- + +# MASM Debug Output + +To print VM state to standard output while a script runs, call the `miden::core::debug` procedures +from your Miden Assembly. The client's transaction executor prints their output by default, so there +is nothing to enable. Debugging is opt-in per script: output is produced only where a script calls a +print procedure. + +## Procedures + +| Procedure | Prints | +| --------- | ------ | +| `print_stack` | the entire operand stack | +| `print_mem` | memory over a `[start, end)` range (consumes the two range arguments, at most 1024 addresses) | +| `print_mem_addr` | the memory cell at a single address | +| `print_mem_all` | every initialized memory cell of the current context | + +The operand-stack and memory printers are enabled by default. The advice-stack and advice-map +printers are not, because they can expose witness data. + +## Example + +Compile and execute a script that prints the operand stack: + +```rust +let tx_script = client.code_builder().compile_tx_script( + " + use miden::core::debug + use miden::core::sys + + @transaction_script + pub proc main + push.1.2.3 + exec.debug::print_stack + drop drop drop + exec.sys::truncate_stack + end + ", +)?; + +client + .execute_program(account_id, tx_script, AdviceInputs::default(), BTreeMap::new()) + .await?; +``` + +Executing it prints the operand stack to standard output (the step count includes the transaction +prologue that runs before the script): + +```text +Stack state before step 2506: +├── 0: 3 +├── 1: 2 +├── 2: 1 +└── (rest of the stack) +``` + +:::note +Under tests, pass `--no-capture` (`cargo nextest`, used by `make test`) or `--nocapture` +(`cargo test`) to see the output. +::: diff --git a/versioned_docs/version-0.16/builder/tools/clients/rust-client/debugging.md b/versioned_docs/version-0.16/builder/tools/clients/rust-client/debugging.md new file mode 100644 index 00000000..b1d0e7bc --- /dev/null +++ b/versioned_docs/version-0.16/builder/tools/clients/rust-client/debugging.md @@ -0,0 +1,160 @@ +--- +title: DAP Debugging +sidebar_position: 7 +--- + +# DAP Debugging + +The Miden client supports interactive debugging via the [Debug Adapter Protocol (DAP)](https://microsoft.github.io/debug-adapter-protocol/). You can debug both raw Miden Assembly scripts and Rust programs compiled to Miden via `midenc`. This lets you step through execution, set breakpoints, and inspect stack/memory state using any DAP-compatible client (e.g. VS Code, the `miden-debug` TUI). + +## Feature flags + +Two feature flags control debugging support: + +| Feature | Crate | What it enables | +| ------- | ----- | --------------- | +| `dap` | `miden-client`, `miden-client-cli` | Compiles in DAP support (`execute_program_with_dap`, `--start-debug-adapter` CLI flag). | +| `testing` | `miden-client-cli` | Enables test-only CLI helpers such as offline account creation. Not available in production builds. | + +### Building with features + +```bash +# Build the CLI with DAP support +cargo build -p miden-client-cli --features dap + +# Build with DAP and test-only offline helpers +cargo build -p miden-client-cli --features dap,testing + +# Build without DAP +cargo build -p miden-client-cli +``` + +Include the `dap` feature to use `--start-debug-adapter`. + +## Quick Start + +### 1. Create an account + +With a running node: + +```bash +miden-client init +miden-client new-wallet +miden-client sync +``` + +Or without a node (requires `testing` feature): + +```bash +miden-client init +miden-client new-wallet --offline +``` + +### 2. Write a test script + +Create a file `test_debug.masm`: + +``` +@transaction_script +pub proc main + push.1.2 + add + push.3 + mul + push.9 + assert_eq +end +``` + +### 3. Start the DAP server + +```bash +miden-client exec \ + --script-path test_debug.masm \ + --start-debug-adapter 127.0.0.1:4711 +``` + +The client will compile the script, start a debug adapter server, and wait for a DAP client to +connect before executing. + +### 4. Connect a debugger + +In a separate terminal, connect the `miden-debug` TUI: + +```bash +miden-debug --dap-connect 127.0.0.1:4711 +``` + +You can now step through execution, inspect the stack, and set breakpoints. + + +## How it works + +When `--start-debug-adapter` is passed: + +1. The client compiles the transaction script from its filesystem path so source locations point at + the real file. +2. The transaction executor runs with the DAP program executor, which binds a TCP listener on the + specified address and waits for a DAP client connection. +3. Once connected, the DAP client controls execution: continue, step, breakpoints, and state + inspection. +4. If the DAP client requests a restart, the client refreshes the cached source file, recompiles the + script from disk, and starts a new debug session. + +## Extracting recorded advice mutations + +During a DAP session the advice mutations produced by the transaction host's event handlers are +recorded, one entry per `on_event` invocation. This log is what an event-replay debug session +needs to re-execute the same transaction without the live transaction host. + +The DAP program executor is created and consumed inside the transaction executor, so the log is +read through a shared handle obtained from the `DapConfig` before execution: + +```rust,ignore +let mut config = miden_debug::DapConfig::new("127.0.0.1:4711"); +let recorder = config.record_event_mutations(); +miden_debug::DapConfig::set_global(config); + +client + .execute_program_with_dap(account_id, tx_script, advice_inputs, foreign_accounts) + .await?; + +// One `Vec` per event handler invocation, in execution order, +// describing the final run of the session (restarts reset the log). +let recorded = recorder.take(); +``` + +The CLI does this automatically and reports the number of recorded mutation sets when the +session ends. + +## Recording a session for offline replay + +Pass `--record ` alongside `--start-debug-adapter` to write a self-contained *replay +snapshot* of the session once it ends: + +```bash +miden-client exec \ + --script-path test_debug.masm \ + --start-debug-adapter 127.0.0.1:4711 \ + --record session.mdsnap +``` + +The snapshot captures the program, its stack and advice inputs, the MAST forests the transaction +host resolved (account code, note scripts, ...), and the recorded event log — everything needed to +re-run the execution without the live transaction host. It always describes the final run of the +session; restarting from the debugger resets it. + +:::warning Sensitive data +Replay snapshots may contain private account state, note data, advice inputs, and other transaction +witness material. Treat them as sensitive files and do not share or commit them publicly. +::: + +Replay it later, offline, in the `miden-debug` TUI — no node, client, or account state required: + +```bash +miden-debug --replay session.mdsnap +``` + +The recorded events are fed back through the debugger's event-replay host, so you can step through +the same execution, set breakpoints, and inspect the stack and memory exactly as during the live +session. The snapshot carries no source files, so the debugger shows disassembly. diff --git a/versioned_docs/version-0.16/builder/tools/clients/rust-client/design.md b/versioned_docs/version-0.16/builder/tools/clients/rust-client/design.md new file mode 100644 index 00000000..5ffebaa7 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tools/clients/rust-client/design.md @@ -0,0 +1,91 @@ +--- +title: Design +sidebar_position: 4 +--- + +The Miden client has the following architectural components: + +- [Store](#store) +- [RPC client](#rpc-client) +- [Transaction executor](#transaction-executor) +- [Keystore](#keystore) +- [Note screener](#note-screener) +- [Note reader](#note-reader) +- [Note transport](#note-transport) + +:::tip + +- The RPC client and the store are Rust traits. +- This allow developers and users to easily customize their implementations. + +::: + +## Store + +The store is central to the client's design. + +It manages the persistence of the following entities: + +- Accounts; including their state history and related information such as vault assets and account code. +- Transactions and their scripts. +- Notes. +- Note tags. +- Block headers and chain information that the client needs to execute transactions and consume notes. + +Because Miden allows off-chain executing and proving, the client needs to know about the state of the blockchain at the moment of execution. To avoid state bloat, however, the client does not need to see the whole blockchain history, just the chain history intervals that are relevant to the user. + +The store can track any number of accounts, and any number of notes that those accounts might have created or may want to consume. + +## RPC client + +The RPC client communicates with the node through a defined set of gRPC methods. The provided client works both in `std` and `wasm` environments. + +The available gRPC methods are documented in the [Node gRPC Reference](https://docs.miden.xyz/miden-node/rpc). + +## Transaction executor + +The transaction executor uses the [Miden VM](https://docs.miden.xyz/core-concepts/miden-vm/) to execute transactions. All transactions run within the [transaction kernel](https://docs.miden.xyz/builder/smart-contracts/transactions/introduction). + +When executing, the executor needs access to relevant blockchain history. The executor uses a `DataStore` interface for accessing this data. This means that there may be some coupling between the executor and the store. + +## Keystore + +The keystore is responsible for storing and managing the private keys of the accounts tracked by the client. + +These private keys are used by the executor to sign and authenticate transactions. Implementations for both rust and web keystores are provided. + +## Note Screener + +The note screener is used to check the consumability of notes by tracked accounts. It can find the tracked accounts that can consume a note, and whether the note can be consumed at the moment or in the future. + +For each candidate note the screener first runs a static input check. It resolves P2ID and P2IDE notes this way, without running the VM. Every other note is screened by trial-executing a consumption transaction against the account at a single reference block, the client's current sync height. Screening a set of notes therefore runs one trial execution per (account, note) pair that the static check does not resolve. + +Within a single screening call, the transaction inputs that stay constant across the pass are read from the store once instead of once per trial execution: each account's state, the reference block header and its partial blockchain, and the vault asset witnesses (including the fee asset). This is sound because screening commits no state, so those inputs do not change between executions, and it is not applied to real transaction execution, where account state evolves. Screening a batch therefore amortizes each account's input fetch across every note checked against it, and the larger an account's vault, the more a batch saves over screening notes one at a time. + +Usage examples for note screening can be found in the [Note screening section](./library.md#note-screening). + +## Note Reader + +The note reader is used to iterate over the input notes a specific account has already consumed. Notes are read lazily from the store and returned in on-chain consumption order. + +Usage examples for the note reader can be found in the [Reading consumed notes section](./library.md#reading-consumed-notes). + +## State Sync component + +The state sync component encapsulates the logic for dealing with synchronization of the client state with the network. It repeatedly queries the node with sync state requests until the chain tip is reached. On every requests it updates the provided tracked elements (accounts, notes, transactions, etc.) and returns an updated state at the end which can be used to update the store (this component does not modify the store directly). + +The component also exposes a specific customizable callback which can be used to react to new note arrivals. + +## Note transport + +Access to the note transport network to exchange private notes is also provided. +The provided client uses gRPC methods to communicate with the note transport network, working both in `std` and `wasm` environments. + +Targeting privacy, notes are primarily exchanged using their tags as identifiers. By default, when notes are created the tag is derived from the recipient account ID, however the tag can also be random. + +The system is also prepared for end-to-end encryption (to be implemented). + +gRPC methods include: + +- `SendNote`: Sends a note to the note transport network. The recipient address is employed to encrypt the outgoing note (to be implemented). +- `FetchNotes`: Fetch notes from the network by note tag. A pagination mechanism using a monotonic-increasing cursor is also employed. The cursor is created by the network and used by the client to reduce the number of fetched notes (to avoid downloading already fetched notes). diff --git a/versioned_docs/version-0.16/builder/tools/clients/rust-client/examples.md b/versioned_docs/version-0.16/builder/tools/clients/rust-client/examples.md new file mode 100644 index 00000000..121f7ec3 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tools/clients/rust-client/examples.md @@ -0,0 +1,54 @@ +--- +title: Examples +sidebar_position: 7 +--- + +:::note +For a complete example on how to run the client and submit transactions to the Miden node, refer to the [`Getting started documentation`](https://docs.miden.xyz/builder/tools/clients/rust-client/get-started/#prerequisites). +::: + +## Prover Fallback Pattern + +When using a remote prover, network issues or server errors may cause proving to fail. A common pattern is to configure the client with a remote prover by default and fall back to local proving when remote proving fails. + +`RemoteTransactionProver` requires the `tonic` feature (see [Crate features](./features.md#crate-features)). + +```rust +use std::sync::Arc; +use miden_client::{ + ClientError, + RemoteTransactionProver, + builder::ClientBuilder, + transaction::{LocalTransactionProver, ProvingOptions}, +}; + +// Create provers +let remote_prover = Arc::new(RemoteTransactionProver::new("https://prover.example.com")); +let local_prover = Arc::new(LocalTransactionProver::new(ProvingOptions::default())); + +// Build client with remote prover as default +let mut client = ClientBuilder::new() + .prover(remote_prover.clone()) + .store(store) + .rpc(rpc) + .authenticator(authenticator) + .build() + .await?; + +// Build transaction request +let tx_request = /* ... build your transaction request ... */; + +// Submit with fallback: try remote prover first, fall back to local on proving error +let tx_id = match client.submit_new_transaction(account_id, tx_request.clone()).await { + Ok(id) => id, + Err(ClientError::TransactionProvingError(_)) => { + println!("Remote proving failed, falling back to local prover..."); + client + .submit_new_transaction_with_prover(account_id, tx_request, local_prover.clone()) + .await? + } + Err(e) => return Err(e.into()), +}; + +println!("Transaction submitted: {}", tx_id); +``` diff --git a/versioned_docs/version-0.16/builder/tools/clients/rust-client/features.md b/versioned_docs/version-0.16/builder/tools/clients/rust-client/features.md new file mode 100644 index 00000000..26d83651 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tools/clients/rust-client/features.md @@ -0,0 +1,42 @@ +--- +title: Features +sidebar_position: 3 +--- + +The Miden client offers a range of functionality for interacting with the Miden rollup. + +### Transaction execution + +The Miden client facilitates the execution of transactions on the Miden rollup; allowing users to transfer assets, mint new tokens, and perform various other operations. + +### Proof generation + +The Miden rollup supports user-generated proofs which are key to ensuring the validity of transactions on the Miden rollup. + +To enable such proofs, the client contains the functionality for executing, proving, and submitting transactions. + +### Miden network interactivity + +The Miden client enables users to interact with the Miden network. This includes syncing with the latest blockchain data and managing account information. + +__Note transport__ The client also supports connectivity with the Miden Note Transport network for the exchange of private notes (end-to-end encryption coming soon). + +### Note screening + +The Miden client supports screening notes against tracked accounts to determine whether they are relevant and when they can be consumed. Applications can use this to filter input notes and prepare consume transactions before execution. More information can be found in the [Note screening section](./library.md#note-screening). + +### Account generation and tracking + +The Miden client provides features for generating and tracking accounts within the Miden rollup ecosystem. Users can create accounts and track their transaction status. + +### Crate features + +The `miden-client` crate gates some of the functionality above behind Cargo features: + +| Feature | Description | +| ------- | ----------- | +| `tonic` | Includes the gRPC pieces that talk to a node: `GrpcClient`, `RemoteTransactionProver`, the gRPC note transport client, and the `ClientBuilder` methods that wire them up (`for_testnet`, `for_devnet`, `for_localhost`, `grpc_client`). Uses `tonic` transport with TLS on native targets and `tonic-web-wasm-client` on `wasm32`. **Disabled by default.** | +| `std` | Enables `std` support and concurrent execution in `miden-tx`. Enabled by default for native targets. This turns on the `tonic` dependency's transport and TLS features, which is not the same as the `tonic` feature above: gRPC support still has to be requested explicitly. | +| `concurrent` | Enables Rayon-parallel proving without pulling in the rest of `std`, for `wasm32` consumers that cannot use it. Native builds get it through `std`. | +| `testing` | Enables mocks and helpers meant for test environments. **Disabled by default.** | +| `dap` | Enables running a transaction under a Debug Adapter Protocol client instead of proving it. Implies `std`. **Disabled by default.** | diff --git a/versioned_docs/version-0.16/builder/tools/clients/rust-client/get-started/_category_.yml b/versioned_docs/version-0.16/builder/tools/clients/rust-client/get-started/_category_.yml new file mode 100644 index 00000000..b280e681 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tools/clients/rust-client/get-started/_category_.yml @@ -0,0 +1,4 @@ +label: Getting Started +# Determines where this documentation section appears relative to other sections in the parent folder +position: 3 +collapsed: true diff --git a/versioned_docs/version-0.16/builder/tools/clients/rust-client/get-started/create-account-use-faucet.md b/versioned_docs/version-0.16/builder/tools/clients/rust-client/get-started/create-account-use-faucet.md new file mode 100644 index 00000000..24438ca6 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tools/clients/rust-client/get-started/create-account-use-faucet.md @@ -0,0 +1,202 @@ +--- +title: Create account +sidebar_position: 1 +--- + +In this section, we show you how to create a new local Miden account and how to receive funds from the public Miden faucet website. + +## Configure the Miden client + +The Miden client facilitates interaction with the Miden rollup and provides a way to execute and prove transactions. + +:::tip +Check the [Miden client documentation](https://docs.miden.xyz/builder/tools/clients/rust-client/cli/) for more information. +::: + +1. If you haven't already done so as part of another tutorial, open your terminal and create a new directory to store the Miden client. + + ```sh + mkdir miden-client + cd miden-client + ``` + +2. Install the Miden client. + + ```sh + cargo install miden-client-cli --locked + ``` + + You can now use the `miden-client --version` command to verify the installed version. + +## Create a new Miden account + +1. Create a new wallet account using the following command: + + ```sh + miden-client new-wallet + ``` + +2. List all created accounts by running the following command: + + ```sh + miden-client account -l + ``` + + You should see something like this: + + + +Save the account ID for a future step. + +## Request tokens from the public faucet + +1. To request funds from the faucet navigate to the following website: [Miden faucet website](https://faucet.testnet.miden.io/). + +2. Copy the **Account ID** printed by the `miden-client account -l` command in the previous step. Feel free to change the amount of tokens to issue. + +3. Paste this ID into the **Request test tokens** input field on the faucet website and click **Send Private Note**. + +:::tip +You can also click **Send Public Note**. If you do this, the note's details will be public and you will not need to download and import it, so you can skip to [Sync the client](#sync-the-client). +::: + +4. After a few seconds your browser should download - or prompt you to download - a file called `note.mno` (mno = Miden note). It contains the funds the faucet sent to your address. + +5. Save this file on your computer, you will need it for the next step. + +## Import the note into the Miden client + +1. Import the private note that you have received using the following commands: + + ```sh + miden-client import /note.mno + ``` + +2. You should see something like this: + + ```sh + Successfully imported note 0x0ff340133840d35e95e0dc2e62c88ed75ab2e383dc6673ce0341bd486fed8cb6 + ``` + +3. Now that the note has been successfully imported, you can view the note's information using the following command: + + ```sh + miden-client notes + ``` + +4. You should see something like this: + + + +:::tip The importance of syncing + +- As you can see, the note is listed as `Expected`. +- This is because you have received a private note but have not yet synced your view of the rollup to check that the note is the result of a valid transaction. +- Hence, before consuming the note we will need to update our view of the rollup by syncing. +- Many users could have received the same private note, but only one user can consume the note in a transaction that gets verified by the Miden operator. + ::: + +### Sync the client + +Do this periodically to keep informed about any updates on the node by running the `sync` command: + +```sh +miden-client sync +``` + +You will see something like this as output: + +```sh +State synced to block 179672 +New public notes: 0 +Committed notes: 1 +Tracked notes consumed: 0 +Tracked accounts updated: 0 +Locked accounts: 0 +Committed transactions: 0 +``` + +## Consume the note & receive the funds + +1. Now that we have synced the client, the input-note imported from the faucet should have a `Committed` status, confirming it exists at the rollup level: + + ```sh + miden-client notes + ``` + +2. You should see something like this: + + + +3. Find your account and note id by listing both `accounts` and `notes`: + + ```sh + miden-client account + miden-client notes + ``` + +4. Consume the note and add the funds from its vault to our account using the following command: + + ```sh + miden-client consume-notes --account + ``` + +5. You should see a confirmation message like this: + + + +6. After confirming you can view the new note status by running the following command: + + ```sh + miden-client notes + ``` + +7. You should see something like this: + + + +8. The note is `Processing`. This means that the proof of the transaction was sent, but there is no network confirmation yet. You can update your view of the rollup by syncing again: + + ```sh + miden-client sync + ``` + +9. After syncing, you should have received confirmation of the consumed note. You should see the note as `Consumed` after listing the notes: + + ```sh + miden-client notes + ``` + + + +Amazing! You just have created a client-side zero-knowledge proof locally on your machine and submitted it to the Miden rollup. + +:::tip +You only need to copy the top line of characters of the Note ID. +::: + +## View confirmations + +5. View your updated account's vault containing the tokens sent by the faucet by running the following command: + + ```sh + miden-client account --show + ``` + +6. You should now see your accounts vault containing the funds sent by the faucet. + + + +## Congratulations! + +You have successfully configured and used the Miden client to interact with a Miden rollup and faucet. + +You have performed basic Miden rollup operations like submitting proofs of transactions, generating and consuming notes. + +For more information on the Miden client, refer to the [Miden client documentation](https://docs.miden.xyz/builder/tools/clients/). + +## Debugging tips (clear state and folder) + +- Need a fresh start? All state is maintained in `store.sqlite3`, located in the directory defined in the `miden-client.toml` file. If you want to clear all state, delete this file. It recreates on any command execution. + +- Getting an error? If you initialized the client with `--local`, run `miden-client` from the directory containing `.miden`. diff --git a/versioned_docs/version-0.16/builder/tools/clients/rust-client/get-started/index.md b/versioned_docs/version-0.16/builder/tools/clients/rust-client/get-started/index.md new file mode 100644 index 00000000..0574c186 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tools/clients/rust-client/get-started/index.md @@ -0,0 +1,19 @@ +--- +title: Getting started +sidebar_position: 2 +--- + +This section shows you how to get started with Miden by generating a new Miden account, requesting funds from a public faucet, consuming private notes, and creating public pay-to-id-notes. + +By the end of this tutorial, you will have: + +- Configured the Miden client. +- Connected to a Miden node. +- Created an account and requested funds from the faucet. +- Transferred assets between accounts by creating and consuming notes. + +## Prerequisites + +### Rust + +Download from [the Rust website](https://www.rust-lang.org/learn/get-started). diff --git a/versioned_docs/version-0.16/builder/tools/clients/rust-client/get-started/p2p-private.md b/versioned_docs/version-0.16/builder/tools/clients/rust-client/get-started/p2p-private.md new file mode 100644 index 00000000..a310429f --- /dev/null +++ b/versioned_docs/version-0.16/builder/tools/clients/rust-client/get-started/p2p-private.md @@ -0,0 +1,128 @@ +--- +title: Private peer-to-peer transfer +sidebar_position: 3 +--- + +In this section, we show you how to make private transactions and send funds to another account using the Miden client. + +:::info Important: Prerequisite steps + +- You should have already followed the [prerequisite steps](index.md#prerequisites) and [create account](create-account-use-faucet) documents. +- You should _not_ have reset the state of your local client. + ::: + +## Create a second account + +:::tip +Remember to use the [Miden client documentation](https://docs.miden.xyz/builder/tools/clients/rust-client/cli/) for clarifications. +::: + +1. Create a second account to send funds with. Previously, we created a wallet account (`Account A`). Now, create another wallet (`Account B`) using the following command: + + ```sh + miden-client new-wallet + ``` + +2. List and view the newly created accounts with the following command: + + ```sh + miden-client account -l + ``` + +3. You should see two accounts: + + + +## Transfer assets between accounts + +1. Now we can transfer some of the tokens we received from the faucet to our second `Account B`. + + To do this, run: + + ```sh + miden-client transfer --sender --target --asset 50:: --note-type private + ``` + + :::note + The faucet account ID can be found on the [Miden faucet website](https://faucet.testnet.miden.io/) under the title **Miden faucet**. + ::: + + This generates a private Pay-to-ID (`P2ID`) note containing `50` assets, transferred from one account to the other. + +2. First, sync the accounts. + + ```sh + miden-client sync + ``` + +3. Get the second note id. + + ```sh + miden-client notes + ``` + +4. Have the second account consume the note. + + ```sh + miden-client consume-notes --account + ``` + + :::tip + It's possible to use a short version of the note id: 7 characters after the `0x` is sufficient, e.g. `0x6ae613a`. + ::: + + You should now see both accounts containing faucet assets with amounts transferred from `Account A` to `Account B`. + +5. Check the second account: + + ```sh + miden-client account --show + ``` + + + +6. Check the original account: + + ```sh + miden-client account --show + ``` + + + +Wanna do more? [Sending public notes](p2p-public) + +## Using the note transport network + +The steps above assume that the client owns both accounts. To exchange notes with other users, the note transport network can be used. +For this the sender (`Account A`) will need the address (bech32 string) of the recipient (`Account B`). +After creating the note (step 1 above), get the created note ID with `miden-client notes --list`. Then send that note through the note transport network, +```sh +miden-client notes --send +``` +Then the recipient can fetch that note using `miden-client sync`, or more specifically, +```sh +miden-client notes --fetch +``` +The note will then be available to be consumed. + +:::note + +The client will fetch notes for tracked note tags. +By default, note tags are derived from the recipient's account ID. However these can also be random to increase privacy. +In this case, to track a specific tag, run `miden-client tags --add `. + +::: + +## Congratulations! + +You have successfully configured and used the Miden client to interact with a Miden rollup and faucet. + +You have performed basic Miden rollup operations like submitting proofs of transactions, generating and consuming notes. + +For more information on the Miden client, refer to the [Miden client documentation](https://docs.miden.xyz/builder/tools/clients/). + +## Clear data + +All state is maintained in `store.sqlite3`, located in the directory defined in the `miden-client.toml` file. + +To clear all state, delete this file. It recreates on any command execution. diff --git a/versioned_docs/version-0.16/builder/tools/clients/rust-client/get-started/p2p-public.md b/versioned_docs/version-0.16/builder/tools/clients/rust-client/get-started/p2p-public.md new file mode 100644 index 00000000..6927ac07 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tools/clients/rust-client/get-started/p2p-public.md @@ -0,0 +1,104 @@ +--- +title: Peer-to-peer transfer +sidebar_position: 2 +--- + +In this section, we show you how to execute transactions and send funds to another account using the Miden client and [public notes](https://docs.miden.xyz/builder/smart-contracts/notes/note-types). + +:::info Important: Prerequisite steps +- You should have already followed the [prerequisite steps](index.md#prerequisites) and [create account](create-account-use-faucet) documents. +- You should have *not* reset the state of your local client. +::: + +## Create a second client + +:::tip +Remember to use the [Miden client documentation](https://docs.miden.xyz/builder/tools/clients/rust-client/cli/) for clarifications. +::: + +This is an alternative to the [private P2P transactions](p2p-private) process. + +In this tutorial, we use two different clients to simulate two different remote users who don't share local state. + +To do this, we use two terminals with their own state (using their own `miden-client.toml`). + +1. Create a new directory to store the new client. + + ```sh + mkdir miden-client-2 + cd miden-client-2 + ``` + +2. Initialize a local configuration for the second client. + + ```sh + miden-client init --local + ``` + +3. On the new client, create a new [basic account](https://docs.miden.xyz/builder/smart-contracts/accounts): + + ```sh + miden-client new-wallet --account-type public + ``` + + We refer to this account as _Account C_. Note that we set the account visibility to `public`, which means that the account details are public and its latest state can be retrieved from the node. + +4. List and view the account with the following command: + + ```sh + miden-client account -l + ``` + +## Transfer assets between accounts + +1. Now we can transfer some of the tokens we received from the faucet to our new account C. Remember to switch back to `miden-client` directory, since you'll be making the txn from Account ID A. + + To do this, from the first client run: + + ```sh + miden-client transfer --sender --target --asset 50:: --note-type public + ``` + + :::note + The faucet account ID can be found on the [Miden faucet website](https://faucet.testnet.miden.io/) under the title **Miden faucet**. + ::: + + This generates a Pay-to-ID (`P2ID`) note containing `50` tokens, transferred from one account to the other. As the note is public, the second account can receive the necessary details by syncing with the node. + +2. First, sync the account on the new client. + + ```sh + miden-client sync + ``` + +3. At this point, we should have received the public note details. + + ```sh + miden-client notes --list + ``` + + Because the note was retrieved from the node, the commit height will be included and displayed. + +4. Have account C consume the note. + + ```sh + miden-client consume-notes --account + ``` + + :::tip + It's possible to use a short version of the note id: 7 characters after the `0x` is sufficient, e.g. `0x6ae613a`. + ::: + +That's it! + +Account C has now consumed the note and there should be new assets in the account: + +```sh +miden-client account --show +``` + +## Clear state + +All state is maintained in `store.sqlite3`, located in the directory defined in the `miden-client.toml` file. + +To clear all state, delete this file. It recreates on any command execution. diff --git a/versioned_docs/version-0.16/builder/tools/clients/rust-client/index.md b/versioned_docs/version-0.16/builder/tools/clients/rust-client/index.md new file mode 100644 index 00000000..10eb06f7 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tools/clients/rust-client/index.md @@ -0,0 +1,22 @@ +--- +title: Rust +sidebar_position: 1 +--- + +# Overview + +The Miden client in Rust contains, the Miden client library and the Miden client cli. + +### Miden client library + +The Miden client library is a Rust library that can be integrated into projects, allowing developers to interact with the Miden rollup. + +The library provides a set of APIs and functions for executing transactions, generating proofs, and managing activity on the Miden network. + +### Miden client CLI + +The Miden client also includes a command-line interface (CLI) that serves as a wrapper around the library, exposing its basic functionality in a user-friendly manner. + +The CLI provides commands for interacting with the Miden rollup, such as submitting transactions, syncing with the network, and managing account data. + +More information about the CLI can be found in the [CLI section](./cli/index.md). diff --git a/versioned_docs/version-0.16/builder/tools/clients/rust-client/install-and-run.md b/versioned_docs/version-0.16/builder/tools/clients/rust-client/install-and-run.md new file mode 100644 index 00000000..21c68da9 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tools/clients/rust-client/install-and-run.md @@ -0,0 +1,35 @@ +--- +title: Installation +sidebar_position: 1 +--- + +## Software prerequisites + +- [Rust installation](https://www.rust-lang.org/tools/install) minimum version 1.98.1. + +## Install the client + +Run the following command to install the miden-client: + +```sh +cargo install miden-client-cli --locked +``` + +This installs the `miden-client` binary (at `~/.cargo/bin/miden-client`). + +## Run the client + +If the install worked correctly, you should be able to check the version by running: + +```sh +miden-client --version +``` + +Once installed, you may run: +```sh +miden-client --help +``` + +This will show you the available commands and options for the client. + +An more in depth tutorial can be fund in the [Getting started section](./get-started). diff --git a/versioned_docs/version-0.16/builder/tools/clients/rust-client/library.md b/versioned_docs/version-0.16/builder/tools/clients/rust-client/library.md new file mode 100644 index 00000000..21428694 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tools/clients/rust-client/library.md @@ -0,0 +1,370 @@ +--- +title: Library +sidebar_position: 5 +--- + +To use the Miden client library in a Rust project, include it as a dependency. + +In your project's `Cargo.toml`, add: + +```toml +miden-client = { version = "0.16.0-alpha.1", features = ["tonic"] } +``` + +The `tonic` feature is not enabled by default and gates everything that speaks gRPC to a node: `GrpcClient`, `RemoteTransactionProver`, the gRPC note transport client, and the `ClientBuilder` methods that wire them up. Leave it out only when supplying your own `NodeRpcClient` and `TransactionProver` implementations. See [Features](./features.md#crate-features) for the full list. + +## Client instantiation + +The recommended way to create a client is using the `ClientBuilder`. For standard networks, use the pre-configured constructors: + +```rust +use std::sync::Arc; +use miden_client::builder::ClientBuilder; +use miden_client_sqlite_store::SqliteStore; + +// Create store +let sqlite_store = SqliteStore::new("path/to/store".try_into()?).await?; +let store = Arc::new(sqlite_store); + +// Build client for testnet (pre-configured RPC, prover, and note transport) +let client = ClientBuilder::for_testnet() + .store(store) + .filesystem_keystore("path/to/keys")? + .build() + .await?; +``` + +Other network constructors are available: +- `ClientBuilder::for_testnet()` - Pre-configured for Miden testnet +- `ClientBuilder::for_devnet()` - Pre-configured for Miden devnet +- `ClientBuilder::for_localhost()` - Pre-configured for local development + +For custom configurations, use `ClientBuilder::new()` and configure each component: + +```rust +use std::sync::Arc; +use miden_client::builder::ClientBuilder; +use miden_client::rpc::{Endpoint, GrpcClient}; +use miden_client_sqlite_store::SqliteStore; + +// Create store +let sqlite_store = SqliteStore::new("path/to/store".try_into()?).await?; +let store = Arc::new(sqlite_store); + +// Setup the gRPC endpoint +let endpoint = Endpoint::new("https".into(), "localhost".into(), Some(57291)); + +let client = ClientBuilder::new() + .grpc_client(&endpoint, None) + .store(store) + .filesystem_keystore("path/to/keys")? + // Optional: custom prover via .prover(Arc::new(prover)) + // Optional: note transport via .note_transport(Arc::new(nt_client)) + // Optional: custom source manager via .source_manager(Arc::new(sm)) — only + // needed when compiling scripts outside the client with an external + // `Assembler`; pass the same `Arc` to both so source spans align. + .build() + .await?; +``` + +## Create local account + +With the Miden client, you can create and track any number of public and local accounts. For local accounts, the state is tracked locally, and the rollup only keeps commitments to the data, which in turn guarantees privacy. + +The `AccountBuilder` can be used to create a new account with the specified parameters and components. The following code creates a new local account: + +```rust +let key_pair = SecretKey::with_rng(client.rng()); + +let new_account = AccountBuilder::new(init_seed) // Seed should be random for each account + .account_type(AccountType::Private) + .with_auth_component(AuthRpoFalcon512::new(key_pair.public_key())) + .with_component(BasicWallet) + .build()?; +keystore.add_key(&AuthSecretKey::RpoFalcon512(key_pair), new_account.id()).await?; +client.add_account(&new_account, false).await?; +``` +Once an account is created, it is kept locally and its state is automatically tracked by the client. + +To create a public account, specify `AccountType::Public`: + +```Rust +let key_pair = SecretKey::with_rng(client.rng()); +let anchor_block = client.get_latest_epoch_block().await.unwrap(); + +let new_account = AccountBuilder::new(init_seed) // Seed should be random for each account + .anchor((&anchor_block).try_into().unwrap()) + .account_type(AccountType::Public) + .with_auth_component(AuthRpoFalcon512::new(key_pair.public_key())) + .with_component(BasicWallet) + .build()?; +keystore.add_key(&AuthSecretKey::RpoFalcon512(key_pair), new_account.id()).await?; +client.add_account(&new_account, false).await?; +``` + +The account's state is also tracked locally, but during sync the client updates the account state by querying the node for the most recent account data. + +### Network accounts + +A network account is a public account that the node drives automatically: it consumes matching notes on the network's behalf via network transactions (NTX). A network account is built by using the `NetworkAccount::builder` method, which takes a standardized allowlist of note script roots. The node uses that allowlist to identify the account and route only allowlisted notes to it; the auth procedure additionally enforces that consumed notes are allowlisted and that no transaction script runs. + +```rust +let network_account = NetworkAccount::builder(init_seed, allowed_note_script_roots)? + .with_component(/* your contract component */) + .build_with_schema_commitment()?; +client.add_account(&network_account, false).await?; + +// Deploy with an empty (scriptless) transaction: `AuthNetworkAccount` forbids transaction +// scripts, and its auth procedure bumps the nonce from 0 to 1, which registers the account. +let deploy = TransactionRequestBuilder::new().build()?; +let tx_id = client.submit_new_transaction(network_account.id(), deploy).await?; +``` + +After deployment the account is a network account, so the node rejects user-submitted transactions against it; all further state changes happen through network transactions. + +## Execute transaction + +In order to execute a transaction, you first need to define which type of transaction is to be executed. This may be done with the `TransactionRequest` which represents a general definition of a transaction. Some standardized constructors are available for common transaction types. + +Here is an example for a `pay-to-id` transaction type: + +```rust +// Define asset +let faucet_id = AccountId::from_hex(faucet_id)?; +let fungible_asset = FungibleAsset::new(faucet_id, *amount)?.into(); + +let sender_account_id = AccountId::from_hex(bob_account_id)?; +let target_account_id = AccountId::from_hex(alice_account_id)?; +let payment_description = PaymentNoteDescription::new( + vec![fungible_asset.into()], + sender_account_id, + target_account_id, +); + +let transaction_request = TransactionRequestBuilder::new().build_pay_to_id( + payment_description, + None, + NoteType::Private, + client.rng(), +)?; + +// Execute transaction. No information is tracked after this. +let transaction_execution_result = client.new_transaction(sender_account_id, transaction_request.clone()).await?; + +// Prove and submit the transaction, which is stored alongside created notes (if any) +client.submit_transaction(transaction_execution_result).await? +``` + +You can decide whether you want the note details to be public or private through the `note_type` parameter. +You may also customize the transaction request with the other `TransactionRequestBuilder` methods. This allows you to run custom code, with custom note arguments and additional output/input notes as well. + +## Note screening + +### When to use note screening + +You can use note screening when you need to decide whether a note is relevant to the accounts tracked by the client. Screening checks whether each tracked account can consume the note now or at a future block. + +Screening cost depends on each screened note's script. A few well-known scripts, such as P2ID, can be checked statically. Every other note is trial-executed against every tracked account, so cost grows with the number of tracked accounts multiplied by the number of notes screened. Use screening when you need consumability information for planning, filtering, or building a consume transaction. + +### Use the Client helpers first + +For notes already tracked by the client, you should usually start with the helper methods on `Client`. These cover the common case without creating a `NoteScreener` directly. + +```rust +use miden_client::note::NoteConsumptionStatus; + +// Return all committed input notes that at least one tracked account may consume. +let consumable_notes = client.get_consumable_notes(None).await?; + +for (note, accounts) in consumable_notes { + for (account_id, status) in accounts { + if matches!(status, NoteConsumptionStatus::Consumable) { + // This account can consume the note at the current sync height. + println!("{} can consume {}", account_id, note.id().to_hex()); + } + } +} +``` + +Passing an account ID to `get_consumable_notes` screens only that account, so its cost scales with the number of committed notes alone. If you only need the committed notes without consumability verdicts, `get_input_notes` with `NoteFilter::Committed` is much cheaper. + +### Obtain a screener + +When you need to check notes that are not already covered by the client helpers, you can obtain a screener from the client. + +```rust +// Build a screener configured with the client's store and RPC client. +let screener = client.note_screener(); +``` + +You can also pass custom transaction arguments to the screener via `with_transaction_args`. The screener uses them during its trial executions, which lets it evaluate consumability under the same conditions you will use when actually consuming. For example: + +```rust +use std::collections::BTreeMap; + +use miden_client::Word; +use miden_client::note::NoteId; +use miden_client::transaction::{AdviceMap, TransactionArgs}; + +// Per-note arguments passed to the note script. +let note_args: BTreeMap = BTreeMap::from([(note_id, custom_args)]); +let tx_args = TransactionArgs::new(AdviceMap::default()).with_note_args(note_args); + +let screener_with_args = client.note_screener().with_transaction_args(tx_args); +``` + +### Check one note + +To check one note, call `get_consumability`. + +```rust +use miden_client::note::{Note, NoteConsumptionStatus}; + +// Fetch the input note from the store. +let input_note_record = client.get_input_note(note_id).await?.unwrap(); +let note: Note = input_note_record.try_into()?; + +let account_statuses = screener.get_consumability(¬e).await?; + +for (account_id, status) in account_statuses { + match status { + NoteConsumptionStatus::Consumable => { + // The note can be consumed now by this account. + println!("{account_id} can consume {}", note.id().to_hex()); + }, + NoteConsumptionStatus::ConsumableAfter(block_number) => { + // The note becomes consumable at a later block. + println!("{account_id} can consume this note after block {block_number}"); + }, + _ => { + // Other statuses explain why the note is not immediately consumable. + println!("{account_id}: {status:?}"); + }, + } +} +``` + +### Check many notes + +When you have several notes, use `get_batch_consumability` to check them all in one pass. + +```rust +use std::collections::BTreeMap; + +use miden_client::note::{Note, NoteConsumability, NoteId}; +use miden_client::store::NoteFilter; + +// Fetch committed input notes from the store. +let input_note_records = client.get_input_notes(NoteFilter::Committed).await?; + +let notes: Vec = input_note_records + .iter() + .cloned() + .map(TryInto::try_into) + .collect::>()?; + +// Check all notes with one executor setup. +let notes_by_id: BTreeMap> = + screener.get_batch_consumability(¬es).await?; + +for (note_id, account_statuses) in notes_by_id { + println!("{} has {} possible consumers", note_id.to_hex(), account_statuses.len()); +} +``` + +Prefer a single `get_batch_consumability` call over calling `get_consumability` in a loop. A batch reuses each account's execution inputs (account state, reference block, and vault witnesses) across every note in the same pass, while separate calls re-read them each time. This reuse lasts only for the duration of the call, so it does not carry across separate screening calls. + +### Check consumability for one account + +If you already know which account will consume the notes, use `check_notes_consumability`. This is useful when planning a multi-note consume transaction for a known account. + +```rust +use miden_client::note::{Note, NoteConsumptionInfo}; +use miden_client::store::NoteFilter; + +// Fetch committed input notes from the store. +let input_note_records = client.get_input_notes(NoteFilter::Committed).await?; + +let notes: Vec = input_note_records + .iter() + .cloned() + .map(TryInto::try_into) + .collect::>()?; + +// Find the largest subset that can execute together for this account. +let consumption_info: NoteConsumptionInfo = screener + .check_notes_consumability(account_id, notes) + .await?; + +for successful_note in &consumption_info.successful { + // These notes can be included together in the consume transaction. + println!("can consume {}", successful_note.id().to_hex()); +} + +for failed_note in &consumption_info.failed { + // Failed notes include the note and the execution error. + println!("cannot consume {}: {}", failed_note.note.id().to_hex(), failed_note.error); +} +``` + +## Reading consumed notes + +### When to use the note reader + +Use the note reader when you need to iterate over the input notes a specific account has already consumed, for example to build a consumption history or reconcile past activity. + +`InputNoteReader` reads lazily from the store, so creating a reader does not run a query. Since the reader queries the local store, sync the client first to see the latest consumptions. Notes are returned in on-chain consumption order, first by block number, then by the account's transaction order within each block. + +### Iterate over an account's consumed notes + +Obtain a reader from the client and call `next` until it returns `None`. Each call to `next` runs one store query. + +```rust +let mut reader = client.input_note_reader(account_id); + +while let Some(note) = reader.next().await? { + // Use the consumed input note. +} +``` + +### Restrict to a block range + +Configure the reader with `in_block_range` to return only notes consumed within an inclusive block range. `reset` returns the reader to the beginning without changing its consumer account or block range. + +```rust +use miden_client::block::BlockNumber; + +let mut reader = client + .input_note_reader(account_id) + .in_block_range(BlockNumber::from(0u32), BlockNumber::from(100u32)); + +while let Some(note) = reader.next().await? { + // Use the consumed input note. +} + +// Start another pass over the same notes. +reader.reset(); +``` + +## Supply foreign account inputs yourself + +A request normally declares foreign accounts as `ForeignAccount::public` or `ForeignAccount::private`, and the client fetches their state and inclusion witnesses from the node at the transaction's reference block. When you already hold that data, declare it as `ForeignAccount::Prefetched` and nothing is fetched for that account. A witness opens against the account tree of exactly one block, so inputs fetched at block `N` are only valid for a transaction whose reference block is `N`; the client rejects a mismatch before execution. Under `Client::execute_transaction_at` the reference block is the anchor's block. Otherwise it is the sync height at execution time, so do not sync between fetching and executing. + +`Client::get_foreign_account_inputs` fetches inputs for a set of declarations at a given block, and `AccountInputs` serializes, so one party can fetch and another can execute: + +```rust +use miden_client::transaction::{ForeignAccount, TransactionRequestBuilder}; + +let declarations = [ForeignAccount::public(foreign_account_id, storage_requirements)?]; +let block_num = client.get_sync_height().await?; +let inputs = client.get_foreign_account_inputs(declarations, block_num).await?; + +let request = TransactionRequestBuilder::new() + .custom_script(tx_script) + .foreign_accounts(inputs) + .build()?; +``` + +Declaring an account ID more than once keeps the last declaration, so prefetched inputs added after a `ForeignAccount::public` declaration for the same account replace it. Only the accounts you pass are fetched: include every account reached through foreign procedure calls and every faucet with asset callbacks enabled whose asset the transaction moves, which on a fee-charging chain can include the fee faucet. Storage map keys and vault assets absent from the inputs are still resolved lazily during execution. + +Prefetching is what lets a request executed with `Client::execute_transaction_at` run after the node stopped serving account state at the anchor's block: fetch the inputs at `ChainAnchor::block_num` while the node still has them, and ship them with the anchor. diff --git a/versioned_docs/version-0.16/builder/tools/clients/theme/Admonition/Icon/Danger.tsx b/versioned_docs/version-0.16/builder/tools/clients/theme/Admonition/Icon/Danger.tsx new file mode 100644 index 00000000..ab14d7de --- /dev/null +++ b/versioned_docs/version-0.16/builder/tools/clients/theme/Admonition/Icon/Danger.tsx @@ -0,0 +1,19 @@ +import React, { type ReactNode } from "react"; +import type { Props } from "@theme/Admonition/Icon/Danger"; + +export default function AdmonitionIconDanger(props: Props): ReactNode { + return ( + + + + ); +} diff --git a/versioned_docs/version-0.16/builder/tools/clients/theme/Admonition/Icon/Info.tsx b/versioned_docs/version-0.16/builder/tools/clients/theme/Admonition/Icon/Info.tsx new file mode 100644 index 00000000..59e48a52 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tools/clients/theme/Admonition/Icon/Info.tsx @@ -0,0 +1,20 @@ +import React, { type ReactNode } from "react"; +import type { Props } from "@theme/Admonition/Icon/Info"; + +export default function AdmonitionIconInfo(props: Props): ReactNode { + return ( + + + + ); +} diff --git a/versioned_docs/version-0.16/builder/tools/clients/theme/Admonition/Icon/Note.tsx b/versioned_docs/version-0.16/builder/tools/clients/theme/Admonition/Icon/Note.tsx new file mode 100644 index 00000000..d7c524b3 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tools/clients/theme/Admonition/Icon/Note.tsx @@ -0,0 +1,20 @@ +import React, { type ReactNode } from "react"; +import type { Props } from "@theme/Admonition/Icon/Note"; + +export default function AdmonitionIconNote(props: Props): ReactNode { + return ( + + + + ); +} diff --git a/versioned_docs/version-0.16/builder/tools/clients/theme/Admonition/Icon/Tip.tsx b/versioned_docs/version-0.16/builder/tools/clients/theme/Admonition/Icon/Tip.tsx new file mode 100644 index 00000000..219bb8d0 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tools/clients/theme/Admonition/Icon/Tip.tsx @@ -0,0 +1,20 @@ +import React, { type ReactNode } from "react"; +import type { Props } from "@theme/Admonition/Icon/Tip"; + +export default function AdmonitionIconTip(props: Props): ReactNode { + return ( + + + + ); +} diff --git a/versioned_docs/version-0.16/builder/tools/clients/theme/Admonition/Icon/Warning.tsx b/versioned_docs/version-0.16/builder/tools/clients/theme/Admonition/Icon/Warning.tsx new file mode 100644 index 00000000..f96398d1 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tools/clients/theme/Admonition/Icon/Warning.tsx @@ -0,0 +1,20 @@ +import React, { type ReactNode } from "react"; +import type { Props } from "@theme/Admonition/Icon/Warning"; + +export default function AdmonitionIconCaution(props: Props): ReactNode { + return ( + + + + ); +} diff --git a/versioned_docs/version-0.16/builder/tools/clients/theme/Admonition/Layout/index.tsx b/versioned_docs/version-0.16/builder/tools/clients/theme/Admonition/Layout/index.tsx new file mode 100644 index 00000000..7b2c170d --- /dev/null +++ b/versioned_docs/version-0.16/builder/tools/clients/theme/Admonition/Layout/index.tsx @@ -0,0 +1,51 @@ +import React, { type ReactNode } from "react"; +import clsx from "clsx"; +import { ThemeClassNames } from "@docusaurus/theme-common"; + +import type { Props } from "@theme/Admonition/Layout"; + +import styles from "./styles.module.css"; + +function AdmonitionContainer({ + type, + className, + children, +}: Pick & { children: ReactNode }) { + return ( +
+ {children} +
+ ); +} + +function AdmonitionHeading({ icon, title }: Pick) { + return ( +
+ {icon} + {/* {title} */} +
+ ); +} + +function AdmonitionContent({ children }: Pick) { + return children ? ( +
{children}
+ ) : null; +} + +export default function AdmonitionLayout(props: Props): ReactNode { + const { type, icon, title, children, className } = props; + return ( + + {title || icon ? : null} + {children} + + ); +} diff --git a/versioned_docs/version-0.16/builder/tools/clients/theme/Admonition/Layout/styles.module.css b/versioned_docs/version-0.16/builder/tools/clients/theme/Admonition/Layout/styles.module.css new file mode 100644 index 00000000..88df7e63 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tools/clients/theme/Admonition/Layout/styles.module.css @@ -0,0 +1,35 @@ +.admonition { + margin-bottom: 1em; +} + +.admonitionHeading { + font: var(--ifm-heading-font-weight) var(--ifm-h5-font-size) / + var(--ifm-heading-line-height) var(--ifm-heading-font-family); + text-transform: uppercase; +} + +/* Heading alone without content (does not handle fragment content) */ +.admonitionHeading:not(:last-child) { + margin-bottom: 0.3rem; +} + +.admonitionHeading code { + text-transform: none; +} + +.admonitionIcon { + display: inline-block; + vertical-align: middle; + margin-right: 0.4em; +} + +.admonitionIcon svg { + display: inline-block; + height: 1.6em; + width: 1.6em; + fill: var(--ifm-alert-foreground-color); +} + +.admonitionContent > :last-child { + margin-bottom: 0; +} diff --git a/versioned_docs/version-0.16/builder/tools/clients/theme/Admonition/Type/Caution.tsx b/versioned_docs/version-0.16/builder/tools/clients/theme/Admonition/Type/Caution.tsx new file mode 100644 index 00000000..b570a37a --- /dev/null +++ b/versioned_docs/version-0.16/builder/tools/clients/theme/Admonition/Type/Caution.tsx @@ -0,0 +1,32 @@ +import React, {type ReactNode} from 'react'; +import clsx from 'clsx'; +import Translate from '@docusaurus/Translate'; +import type {Props} from '@theme/Admonition/Type/Caution'; +import AdmonitionLayout from '@theme/Admonition/Layout'; +import IconWarning from '@theme/Admonition/Icon/Warning'; + +const infimaClassName = 'alert alert--warning'; + +const defaultProps = { + icon: , + title: ( + + caution + + ), +}; + +// TODO remove before v4: Caution replaced by Warning +// see https://github.com/facebook/docusaurus/issues/7558 +export default function AdmonitionTypeCaution(props: Props): ReactNode { + return ( + + {props.children} + + ); +} diff --git a/versioned_docs/version-0.16/builder/tools/clients/theme/Admonition/Type/Danger.tsx b/versioned_docs/version-0.16/builder/tools/clients/theme/Admonition/Type/Danger.tsx new file mode 100644 index 00000000..49901fa9 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tools/clients/theme/Admonition/Type/Danger.tsx @@ -0,0 +1,30 @@ +import React, {type ReactNode} from 'react'; +import clsx from 'clsx'; +import Translate from '@docusaurus/Translate'; +import type {Props} from '@theme/Admonition/Type/Danger'; +import AdmonitionLayout from '@theme/Admonition/Layout'; +import IconDanger from '@theme/Admonition/Icon/Danger'; + +const infimaClassName = 'alert alert--danger'; + +const defaultProps = { + icon: , + title: ( + + danger + + ), +}; + +export default function AdmonitionTypeDanger(props: Props): ReactNode { + return ( + + {props.children} + + ); +} diff --git a/versioned_docs/version-0.16/builder/tools/clients/theme/Admonition/Type/Info.tsx b/versioned_docs/version-0.16/builder/tools/clients/theme/Admonition/Type/Info.tsx new file mode 100644 index 00000000..018e0a16 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tools/clients/theme/Admonition/Type/Info.tsx @@ -0,0 +1,30 @@ +import React, {type ReactNode} from 'react'; +import clsx from 'clsx'; +import Translate from '@docusaurus/Translate'; +import type {Props} from '@theme/Admonition/Type/Info'; +import AdmonitionLayout from '@theme/Admonition/Layout'; +import IconInfo from '@theme/Admonition/Icon/Info'; + +const infimaClassName = 'alert alert--info'; + +const defaultProps = { + icon: , + title: ( + + info + + ), +}; + +export default function AdmonitionTypeInfo(props: Props): ReactNode { + return ( + + {props.children} + + ); +} diff --git a/versioned_docs/version-0.16/builder/tools/clients/theme/Admonition/Type/Note.tsx b/versioned_docs/version-0.16/builder/tools/clients/theme/Admonition/Type/Note.tsx new file mode 100644 index 00000000..c99e0385 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tools/clients/theme/Admonition/Type/Note.tsx @@ -0,0 +1,30 @@ +import React, {type ReactNode} from 'react'; +import clsx from 'clsx'; +import Translate from '@docusaurus/Translate'; +import type {Props} from '@theme/Admonition/Type/Note'; +import AdmonitionLayout from '@theme/Admonition/Layout'; +import IconNote from '@theme/Admonition/Icon/Note'; + +const infimaClassName = 'alert alert--secondary'; + +const defaultProps = { + icon: , + title: ( + + note + + ), +}; + +export default function AdmonitionTypeNote(props: Props): ReactNode { + return ( + + {props.children} + + ); +} diff --git a/versioned_docs/version-0.16/builder/tools/clients/theme/Admonition/Type/Tip.tsx b/versioned_docs/version-0.16/builder/tools/clients/theme/Admonition/Type/Tip.tsx new file mode 100644 index 00000000..18604a5e --- /dev/null +++ b/versioned_docs/version-0.16/builder/tools/clients/theme/Admonition/Type/Tip.tsx @@ -0,0 +1,30 @@ +import React, {type ReactNode} from 'react'; +import clsx from 'clsx'; +import Translate from '@docusaurus/Translate'; +import type {Props} from '@theme/Admonition/Type/Tip'; +import AdmonitionLayout from '@theme/Admonition/Layout'; +import IconTip from '@theme/Admonition/Icon/Tip'; + +const infimaClassName = 'alert alert--success'; + +const defaultProps = { + icon: , + title: ( + + tip + + ), +}; + +export default function AdmonitionTypeTip(props: Props): ReactNode { + return ( + + {props.children} + + ); +} diff --git a/versioned_docs/version-0.16/builder/tools/clients/theme/Admonition/Type/Warning.tsx b/versioned_docs/version-0.16/builder/tools/clients/theme/Admonition/Type/Warning.tsx new file mode 100644 index 00000000..61d9597b --- /dev/null +++ b/versioned_docs/version-0.16/builder/tools/clients/theme/Admonition/Type/Warning.tsx @@ -0,0 +1,30 @@ +import React, {type ReactNode} from 'react'; +import clsx from 'clsx'; +import Translate from '@docusaurus/Translate'; +import type {Props} from '@theme/Admonition/Type/Warning'; +import AdmonitionLayout from '@theme/Admonition/Layout'; +import IconWarning from '@theme/Admonition/Icon/Warning'; + +const infimaClassName = 'alert alert--warning'; + +const defaultProps = { + icon: , + title: ( + + warning + + ), +}; + +export default function AdmonitionTypeWarning(props: Props): ReactNode { + return ( + + {props.children} + + ); +} diff --git a/versioned_docs/version-0.16/builder/tools/clients/theme/Admonition/Types.tsx b/versioned_docs/version-0.16/builder/tools/clients/theme/Admonition/Types.tsx new file mode 100644 index 00000000..2a100190 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tools/clients/theme/Admonition/Types.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import AdmonitionTypeNote from '@theme/Admonition/Type/Note'; +import AdmonitionTypeTip from '@theme/Admonition/Type/Tip'; +import AdmonitionTypeInfo from '@theme/Admonition/Type/Info'; +import AdmonitionTypeWarning from '@theme/Admonition/Type/Warning'; +import AdmonitionTypeDanger from '@theme/Admonition/Type/Danger'; +import AdmonitionTypeCaution from '@theme/Admonition/Type/Caution'; +import type AdmonitionTypes from '@theme/Admonition/Types'; + +const admonitionTypes: typeof AdmonitionTypes = { + note: AdmonitionTypeNote, + tip: AdmonitionTypeTip, + info: AdmonitionTypeInfo, + warning: AdmonitionTypeWarning, + danger: AdmonitionTypeDanger, +}; + +// Undocumented legacy admonition type aliases +// Provide hardcoded/untranslated retrocompatible label +// See also https://github.com/facebook/docusaurus/issues/7767 +const admonitionAliases: typeof AdmonitionTypes = { + secondary: (props) => , + important: (props) => , + success: (props) => , + caution: AdmonitionTypeCaution, +}; + +export default { + ...admonitionTypes, + ...admonitionAliases, +}; diff --git a/versioned_docs/version-0.16/builder/tools/clients/theme/Admonition/index.tsx b/versioned_docs/version-0.16/builder/tools/clients/theme/Admonition/index.tsx new file mode 100644 index 00000000..8f4225da --- /dev/null +++ b/versioned_docs/version-0.16/builder/tools/clients/theme/Admonition/index.tsx @@ -0,0 +1,21 @@ +import React, {type ComponentType, type ReactNode} from 'react'; +import {processAdmonitionProps} from '@docusaurus/theme-common'; +import type {Props} from '@theme/Admonition'; +import AdmonitionTypes from '@theme/Admonition/Types'; + +function getAdmonitionTypeComponent(type: string): ComponentType { + const component = AdmonitionTypes[type]; + if (component) { + return component; + } + console.warn( + `No admonition component found for admonition type "${type}". Using Info as fallback.`, + ); + return AdmonitionTypes.info!; +} + +export default function Admonition(unprocessedProps: Props): ReactNode { + const props = processAdmonitionProps(unprocessedProps); + const AdmonitionTypeComponent = getAdmonitionTypeComponent(props.type); + return ; +} diff --git a/versioned_docs/version-0.16/builder/tools/clients/web-client/_category_.yml b/versioned_docs/version-0.16/builder/tools/clients/web-client/_category_.yml new file mode 100644 index 00000000..9d57d1b6 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tools/clients/web-client/_category_.yml @@ -0,0 +1,4 @@ +label: TypeScript +# Sidebar position within tools/clients/ (Rust is 1, TypeScript is 2, React is 3) +position: 2 +collapsed: true diff --git a/versioned_docs/version-0.16/builder/tools/clients/web-client/accounts.md b/versioned_docs/version-0.16/builder/tools/clients/web-client/accounts.md new file mode 100644 index 00000000..8650c7bc --- /dev/null +++ b/versioned_docs/version-0.16/builder/tools/clients/web-client/accounts.md @@ -0,0 +1,291 @@ +--- +title: Accounts +sidebar_position: 3 +--- + +# Accounts + +`client.accounts` is the resource namespace for everything account-related: creation, lookup, listing, import / export, and address management. All ID fields accept an `AccountRef` — a hex string, bech32 string, `AccountId`, `Account`, or `AccountHeader` — so you rarely need to convert between them. + +## Account type and auth scheme values + +Account creation uses a few small option values. Wallets are the default shape; faucet creation is selected with a faucet type literal; contract creation is selected by passing `components`. + +| Kind | Accepted values | Meaning | +| --- | --- | --- | +| faucet `type` field | `0` \| `1` | Fungible or non-fungible faucet selector | +| `auth` field | `"falcon"` \| `"ecdsa"` | Signing scheme — Falcon is the default | +| `storage` field | `"public"` \| `"private"` | Account visibility mode | +| low-level `AccountStorageMode` | `AccountStorageMode.public()` \| `AccountStorageMode.private()` | WASM builder visibility flag | + +The protocol does not encode wallet/faucet/contract role or mutability in the account ID; role comes from the create options and attached components. + +## Create + +### Wallet + +```typescript +import { MidenClient } from "@miden-sdk/miden-sdk"; + +const client = await MidenClient.createTestnet(); + +// Default: wallet, private storage, Falcon auth +const wallet = await client.accounts.create(); + +// With explicit options +const wallet2 = await client.accounts.create({ + storage: "public", + auth: "ecdsa", + seed: "my-seed", // hashed to 32 bytes via SHA-256 +}); + +console.log(wallet.id().toString()); // hex +console.log(wallet.nonce().toString()); +console.log(wallet.isPublic()); +console.log(wallet.isPrivate()); +console.log(wallet.isFaucet()); +console.log(wallet.isRegularAccount()); +``` + +### Faucet + +```typescript +import { + MidenClient, + type AccountTypeValue, +} from "@miden-sdk/miden-sdk"; + +const FUNGIBLE_FAUCET: AccountTypeValue = 0; + +const client = await MidenClient.createTestnet(); + +const faucet = await client.accounts.create({ + type: FUNGIBLE_FAUCET, + symbol: "TEST", + decimals: 8, + maxSupply: 10_000_000n, // number | bigint +}); + +const faucet2 = await client.accounts.create({ + type: FUNGIBLE_FAUCET, + symbol: "DAG", + decimals: 8, + maxSupply: 10_000_000n, + storage: "public", + auth: "falcon", +}); +``` + +### Contract + +Contract accounts hold custom MASM code. Compile the code into an `AccountComponent` with [`client.compile.component()`](./compile.md), then create the account: + +```typescript +import { + MidenClient, + AuthSecretKey, + StorageSlot, +} from "@miden-sdk/miden-sdk"; + +const counterCode = ` + use miden::protocol::active_account + use miden::protocol::native_account + use miden::core::word + use miden::core::sys + + const COUNTER_SLOT = word("miden::tutorials::counter") + + @account_procedure + pub proc get_count + push.COUNTER_SLOT[0..2] exec.active_account::get_item + exec.sys::truncate_stack + end + + @account_procedure + pub proc increment_count + push.COUNTER_SLOT[0..2] exec.active_account::get_item + add.1 + push.COUNTER_SLOT[0..2] exec.native_account::set_item + exec.sys::truncate_stack + end +`; + +const client = await MidenClient.createTestnet(); + +const component = await client.compile.component({ + code: counterCode, + slots: [StorageSlot.emptyValue("miden::tutorials::counter")], +}); + +const seed = crypto.getRandomValues(new Uint8Array(32)); +const auth = AuthSecretKey.rpoFalconWithRNG(seed); + +const contract = await client.accounts.create({ + seed, + auth, + components: [component], +}); + +console.log("Contract:", contract.id().toString()); +console.log("Is public:", contract.isPublic()); // contracts default to public +``` + +Contract defaults: + +- **Storage** — defaults to `"public"` (so other accounts can read state for FPI). Pass `storage: "private"` to override. +- **Auth** — must be a concrete `AuthSecretKey` instance, not a scheme string. The caller retains the key; the client uses it to sign transactions that touch the contract. +- **Seed** — required, 32 bytes. Determines the account ID. + +### Pre-built via `AccountBuilder` + +When you need full control — e.g. an external signer that provides an auth commitment instead of a secret key — build the account manually and hand it to `insert()`: + +```typescript +import { + MidenClient, + AccountBuilder, + AccountComponent, + AccountStorageMode, +} from "@miden-sdk/miden-sdk"; + +const client = await MidenClient.createTestnet(); + +const commitment = /* externalSigner.getPublicKeyCommitment() */ (undefined as any); +const seed = new Uint8Array(32); // deterministic seed from your derivation path + +const account = new AccountBuilder(seed) + .withAuthComponent( + AccountComponent.createAuthComponentFromCommitment(commitment, 1), + ) + .storageMode(AccountStorageMode.public()) + .withBasicWalletComponent() + .build().account; + +await client.accounts.insert({ account }); +``` + +`accounts.insert()` is local-only: it persists the account to the store without any network call. Use it when you already have a valid `Account` object and just need to track it. + +## Retrieve + +### `get` + +```typescript +const account = await client.accounts.get("0x1234..."); +if (!account) { + console.log("Not found locally"); + return; +} +console.log(account.id().toString()); +console.log(account.nonce().toString()); +console.log(account.isPublic()); +console.log(account.isFaucet()); +``` + +`get` only consults the local store. It returns `null` if the account isn't tracked. + +### `getOrImport` + +```typescript +// Returns the local copy if present; otherwise fetches from the network and stores it. +// publicAccountId is the hex or bech32 ID of an account deployed on this network. +const account = await client.accounts.getOrImport( + publicAccountId, +); +console.log("Nonce:", account.nonce().toString()); +``` + +Use `getOrImport` for accounts you didn't create — a faucet or contract deployed by another party, for example. Once imported, subsequent calls return the local copy without hitting the network. + +`get` vs `getOrImport`: + +- `get` for local-only reads. Cheap, no network. +- `getOrImport` when the account may need to be pulled from chain. + +### `list` + +```typescript +const accounts = await client.accounts.list(); +for (const header of accounts) { + console.log(header.id().toString(), header.nonce().toString()); +} +``` + +Returns `AccountHeader[]` — a lightweight summary suitable for listing UIs. Call `get()` if you need the full `Account`. + +### `getDetails` + +```typescript +const details = await client.accounts.getDetails("0x1234..."); +console.log(details.account.id().toString()); +console.log(details.vault); // AssetVault +console.log(details.storage); // AccountStorage +console.log(details.code); // AccountCode | null +console.log(details.keys); // Word[] (public-key commitments) +``` + +One round-trip returns the full account plus its vault, storage, code, and key commitments. + +### `getBalance` + +```typescript +const balance: bigint = await client.accounts.getBalance( + "0xACCOUNT...", + "0xFAUCET...", +); +console.log(`Balance: ${balance}`); +``` + +## Address management + +```typescript +await client.accounts.addAddress("0xACCOUNT...", "mtst1address..."); +await client.accounts.removeAddress("0xACCOUNT...", "mtst1address..."); +``` + +Associates valid Miden bech32 addresses with an account. The address is a protocol value, not an arbitrary UI alias. + +## Import + +`client.accounts.import()` accepts a discriminated union: + +```typescript +// 1. By reference — fetches a public account from the network. +await client.accounts.import("0x1234..."); // hex +await client.accounts.import(publicAccountBech32Id); // deployed account's bech32 ID + +// 2. From a previously exported file. +await client.accounts.import({ file: accountFile }); + +// 3. From a seed — PUBLIC ACCOUNTS ONLY. +await client.accounts.import({ + seed: initSeed, // Uint8Array + auth: "falcon", +}); +``` + +:::warning[Seed imports are public-only] +Import-by-seed works only for accounts originally created with `storage: "public"`. Private account state is never published onchain, so it can't be reconstructed from a seed alone. For private accounts, use the file export / import workflow below. +::: + +If you aren't sure whether the account is already in your local store, prefer [`getOrImport()`](#getorimport) — it skips the network call when the account is already known. + +## Export + +```typescript +const accountFile = await client.accounts.export("0x1234..."); +// Persist accountFile to disk, send it to another device, etc. +``` + +`AccountFile` includes the full account state, code, seed (if new), and tracked secret keys. Treat it as sensitive. + +## Error behaviour + +- `get()` returns `null` when the account is not in the local store. +- `getDetails()`, `getBalance()`, and `export()` throw when the account is missing. + +## Next + +- [Transactions](./transactions.md) — spend, mint, consume, swap. +- [Compile](./compile.md) — turn MASM into `AccountComponent`s for contract accounts. +- [Sync and store](./sync.md) — refresh account state from the network. diff --git a/versioned_docs/version-0.16/builder/tools/clients/web-client/compile.md b/versioned_docs/version-0.16/builder/tools/clients/web-client/compile.md new file mode 100644 index 00000000..29eaac07 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tools/clients/web-client/compile.md @@ -0,0 +1,240 @@ +--- +title: Compile +sidebar_position: 6 +--- + +# Compile + +`client.compile` turns Miden Assembly (MASM) source into the three runtime artifacts the rest of the SDK consumes: + +| Method | Produces | Used by | +| --- | --- | --- | +| `client.compile.component({ code, namespace?, slots?, supportAllTypes? })` | `AccountComponent` | [`accounts.create({ components: [...] })`](./accounts.md#contract) | +| `client.compile.txScript({ code, libraries? })` | `TransactionScript` | [`transactions.execute({ script })`](./transactions.md#custom-transaction-scripts-execute) | +| `client.compile.noteScript({ code, libraries? })` | `NoteScript` | `Note` construction utilities | + +Each call spins up a fresh `CodeBuilder`, so libraries linked in one call never leak into another. + +## Account components + +```typescript +import { MidenClient, StorageSlot } from "@miden-sdk/miden-sdk"; + +const client = await MidenClient.createTestnet(); + +const contractCode = ` + use miden::protocol::active_account + use miden::protocol::native_account + use miden::core::word + use miden::core::sys + + const COUNTER_SLOT = word("miden::tutorials::counter") + + @account_procedure + pub proc get_count + push.COUNTER_SLOT[0..2] exec.active_account::get_item + exec.sys::truncate_stack + end + + @account_procedure + pub proc increment_count + push.COUNTER_SLOT[0..2] exec.active_account::get_item + add.1 + push.COUNTER_SLOT[0..2] exec.native_account::set_item + exec.sys::truncate_stack + end +`; + +const component = await client.compile.component({ + code: contractCode, + namespace: "external_contract::counter_contract", + slots: [StorageSlot.emptyValue("miden::tutorials::counter")], +}); + +// Use the procedure hash when calling this contract via FPI +const getCountHash = component.getProcedureHash("get_count"); +console.log("get_count hash:", getCountHash); +``` + +Options: + +- `code` — the MASM source for the component. +- `namespace` — module path used to derive procedure identities. Reuse it when rebuilding the source as an inline library; linking `{ component }` preserves the exact compiled identity. +- `slots` — initial storage slots. Use the `StorageSlot` helpers (`emptyValue`, etc.). +- `supportAllTypes` — defaults to `true` and calls `withSupportsAllTypes()` for compatibility. In 0.16, components already apply to every account type; this option does not inject an auth-kernel invocation. + +## Transaction scripts + +### Without libraries + +A script with no `libraries` entry can only reference procedures that exist in the transaction kernel and the standard library — no custom external contracts: + +```typescript +const script = await client.compile.txScript({ + code: ` + use miden::core::sys + + @transaction_script + pub proc main + push.0 + exec.sys::truncate_stack + end + `, +}); +``` + +If your script needs to call into an external contract (as in the FPI section below), pass either the exact compiled component or its source through `libraries` — the compiler only links what you explicitly provide. + +### With inline libraries + +```typescript +import { Linking } from "@miden-sdk/miden-sdk"; + +const script = await client.compile.txScript({ + code: ` + use external_contract::my_contract + use miden::core::sys + + @transaction_script + pub proc main + call.my_contract::do_something + exec.sys::truncate_stack + end + `, + libraries: [ + { + namespace: "external_contract::my_contract", + code: myContractCode, + linking: Linking.Dynamic, // default + }, + ], +}); +``` + +Each inline library takes: + +| Field | Required | Description | +| --- | --- | --- | +| `namespace` | yes | MASM namespace, e.g. `"counter::module"`. | +| `code` | yes | MASM source. | +| `linking` | no | `Linking.Dynamic` (default) or `Linking.Static`. `"dynamic"` / `"static"` string literals are also accepted. | + +`libraries` also accepts `{ component, linking? }`, which links the exact code installed by an `AccountComponent`, or a pre-built `Library`. Prefer the component form when a script calls a component installed on an account. + +### Linking modes + +| Value | Behaviour | When to use | +| --- | --- | --- | +| `Linking.Dynamic` (default) | Retains external procedure MAST roots. The execution host must supply the referenced code. | Linking account procedures without embedding their implementation. | +| `Linking.Static` | Includes the linked library code in the compiled artifact. | Offchain libraries that must be self-contained. | + +## Note scripts + +Note scripts run when an account consumes the note. The shape mirrors `txScript`; use it when you need custom logic on consumption. + +```typescript +const noteScript = await client.compile.noteScript({ + code: ` + use miden::protocol::active_note + use miden::core::sys + + @note_script + pub proc main + # Runs when the consuming account redeems this note. + # Real note scripts inspect note storage, assets, and account state + # using procedures from miden::protocol::active_note. + exec.sys::truncate_stack + end + `, +}); +``` + +Libraries accept the same inline `{ namespace, code, linking? }`, compiled `{ component, linking? }`, and pre-built `Library` forms as transaction scripts. + +## Procedure hashes (for FPI) + +Foreign procedure invocation requires the **hash** of the target procedure. Extract it from a compiled component: + +```typescript +const component = await client.compile.component({ + code: counterContractCode, + namespace: "external_contract::counter_contract", + slots: [StorageSlot.emptyValue("miden::tutorials::counter")], +}); + +const getCountHash = component.getProcedureHash("get_count"); + +const script = await client.compile.txScript({ + code: ` + use external_contract::count_reader_contract + use miden::core::sys + + @transaction_script + pub proc main + padw padw padw padw + push.${getCountHash} + push.${counterAccountId.prefix()} + push.${counterAccountId.suffix()} + call.count_reader_contract::copy_count + exec.sys::truncate_stack + end + `, + libraries: [ + { namespace: "external_contract::count_reader_contract", code: countReaderCode }, + ], +}); +``` + +## End-to-end: compile → create contract → execute script + +```typescript +import { + MidenClient, + AuthSecretKey, + StorageSlot, +} from "@miden-sdk/miden-sdk"; + +const client = await MidenClient.createTestnet(); +await client.sync(); + +// 1. Compile the contract component +const component = await client.compile.component({ + code: counterCode, + namespace: "external_contract::counter_contract", + slots: [StorageSlot.emptyValue("miden::tutorials::counter")], +}); + +// 2. Create the contract account +const seed = crypto.getRandomValues(new Uint8Array(32)); +const auth = AuthSecretKey.rpoFalconWithRNG(seed); + +const contract = await client.accounts.create({ + seed, + auth, + components: [component], +}); + +await client.sync(); + +// 3. Compile the transaction script +const script = await client.compile.txScript({ + code: ` + use external_contract::counter_contract + + @transaction_script + pub proc main + call.counter_contract::increment_count + end + `, + // Link the exact component installed on the account so procedure identities match. + libraries: [{ component }], +}); + +// 4. Execute +const { txId } = await client.transactions.execute({ + account: contract.id(), + script, +}); + +console.log("Tx:", txId.toHex()); +``` diff --git a/versioned_docs/version-0.16/builder/tools/clients/web-client/index.md b/versioned_docs/version-0.16/builder/tools/clients/web-client/index.md new file mode 100644 index 00000000..5646de8a --- /dev/null +++ b/versioned_docs/version-0.16/builder/tools/clients/web-client/index.md @@ -0,0 +1,78 @@ +--- +title: Overview +sidebar_position: 1 +--- + +# Web SDK (@miden-sdk/miden-sdk) + +The Web SDK is the JavaScript toolkit for the Miden network. In browsers, it wraps the Rust client as WebAssembly and exposes a typed API through the `MidenClient` class for web apps, wallets, dApps, and worker contexts. The same package provides a native Node.js entry backed by N-API and SQLite. + +## Capabilities + +- Read and write onchain state: accounts, notes, transactions, tags. +- Build and execute Miden transactions, including custom MASM scripts. +- Compile Miden Assembly into account components, transaction scripts, and note scripts directly in the browser. +- Generate zero-knowledge proofs locally via the in-browser prover, or offload proving to a remote or delegated prover. +- Manage keys through built-in Falcon/ECDSA keystores or external signer integrations. +- Exchange private notes through the Miden note transport network. +- Import / export account files, note files, and full store snapshots for backup and migration. + +## Architecture + +```text +┌────────────────────────────────────────────────┐ +│ @miden-sdk/miden-sdk (npm) │ +│ │ +│ MidenClient (typed TS API) │ +│ │ │ +│ ├─ accounts / transactions / notes / │ +│ │ tags / compile / keystore namespaces │ +│ │ │ +│ └─ wraps WasmWebClient (Rust → WASM) │ +│ │ +│ Browser default: prove / execute on a │ +│ dedicated Web Worker │ +└────────────────────────────────────────────────┘ +``` + +The browser build comes from the `web-client` Rust crate in [0xMiden/web-sdk](https://github.com/0xMiden/web-sdk), compiled with `wasm-bindgen`, and bundled with the WASM module, JavaScript bindings, and a dedicated Web Worker script. Under Node.js, the package selects its native N-API binding and SQLite storage instead. + +## Resource management + +In browsers, each `MidenClient` created with the default `useWorker: true` setting holds a dedicated Web Worker thread. When you no longer need a client — for example in a multi-wallet app that creates one client per active network — call `client.terminate()` to release its underlying resources. Node.js clients and browser clients created with `useWorker: false` do not allocate this worker, but should still be terminated when finished. + +```typescript +import { MidenClient } from "@miden-sdk/miden-sdk"; + +const client = await MidenClient.createTestnet(); + +// ... use the client ... + +// Release client resources when you are done +client.terminate(); +``` + +In environments that support the TC39 [explicit resource management](https://github.com/tc39/proposal-explicit-resource-management) proposal, you can use `using` to let the runtime handle cleanup automatically: + +```typescript +{ + using client = await MidenClient.createTestnet(); + // ... client.terminate() is called automatically when the block exits +} +``` + +After `terminate()`, subsequent client operations throw `Error("Client terminated")`. + +## Where to go next + +- [Setup](./setup.md) — install the SDK and create your first client. +- [Accounts](./accounts.md) — create wallets, faucets, and contract accounts; look up existing ones. +- [Transactions](./transactions.md) — mint, send, consume, swap, and run custom scripts. +- [Notes](./notes.md) — list, import, export, and transport private notes. +- [Compile](./compile.md) — turn Miden Assembly into account components and scripts. +- [Sync and store](./sync.md) — pull network state and manage the local database. +- [Testing](./testing.md) — drive a fully in-memory mock chain for fast, deterministic tests. + +## Migrating from `WebClient` + +The v0.13 flat `WebClient` class is deprecated. The current Web SDK uses `MidenClient` with resource-based namespaces (`client.accounts`, `client.transactions`, …). See the [Web SDK namespace migration guide](../../../migration/07-client-changes.md) for the original namespace migration. diff --git a/versioned_docs/version-0.16/builder/tools/clients/web-client/notes.md b/versioned_docs/version-0.16/builder/tools/clients/web-client/notes.md new file mode 100644 index 00000000..bda18610 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tools/clients/web-client/notes.md @@ -0,0 +1,160 @@ +--- +title: Notes +sidebar_position: 5 +--- + +# Notes + +Notes are the primary mechanism for transferring assets and data between accounts on Miden. This guide covers the `client.notes.*` surface: listing, lookup, import / export, private-note transport, and tags. + +## List received notes + +```typescript +import { MidenClient } from "@miden-sdk/miden-sdk"; + +const client = await MidenClient.createTestnet(); + +// All input notes +const all = await client.notes.list(); + +// Filter by status +const committed = await client.notes.list({ status: "committed" }); +const consumed = await client.notes.list({ status: "consumed" }); +const expected = await client.notes.list({ status: "expected" }); +const processing = await client.notes.list({ status: "processing" }); +const unverified = await client.notes.list({ status: "unverified" }); + +// Filter by specific IDs +const specific = await client.notes.list({ ids: [noteId1, noteId2] }); + +for (const note of all) { + console.log(note.id()?.toString()); +} +``` + +Statuses: + +- `"committed"` — the client has verified the inclusion proof. The note script can still prevent a particular account from consuming it. +- `"consumed"` — already spent. +- `"expected"` — the client expects this note to arrive. +- `"processing"` — mid-consume. +- `"unverified"` — an inclusion proof is stored but has not yet been verified. + +## Retrieve a single note + +```typescript +const note = await client.notes.get("0xnote..."); +if (note) { + console.log(note.id()?.toString()); +} +``` + +Returns `null` when the note isn't tracked locally. + +## List sent notes (output notes) + +```typescript +const sent = await client.notes.listSent(); + +// With status filter +const committedSent = await client.notes.listSent({ status: "committed" }); +``` + +## List consumable notes for an account + +```typescript +const records = await client.notes.listAvailable({ account: wallet }); + +for (const record of records) { + console.log("Note:", record.id()?.toString()); +} +``` + +Returns the input notes available for the specified account. Use this to drive "inbox" UIs. + +## Import and export + +```typescript +import { MidenClient, NoteExportFormat } from "@miden-sdk/miden-sdk"; + +const client = await MidenClient.createTestnet(); + +// Import from a previously exported NoteFile +const importedRef = await client.notes.import(noteFile); +console.log("Imported:", importedRef); + +// Export — formats differ in completeness +const idOnly = await client.notes.export("0xnote...", { format: NoteExportFormat.Id }); +const full = await client.notes.export("0xnote...", { format: NoteExportFormat.Full }); +const details = await client.notes.export("0xnote...", { format: NoteExportFormat.Details }); +``` + +`import()` returns a note ID as a hex string when the file includes one, or the details commitment for a `Details` file. + +`export()` exports output notes created by this client. An imported input note is not an exportable output note; retain the original note file if you need to forward it. + +`NoteExportFormat`: + +- **`Id`** — just the note ID. A recipient can import it only for a public note. +- **`Full`** — complete note data plus inclusion proof. Requires the note to have an onchain inclusion proof. +- **`Details`** — assets and recipient plus a sync hint containing the tag and after-block number. Metadata and attachments are recovered from the chain. + +## Note transport (private notes) + +Private note details can be delivered through the Miden note transport service. The sender must relay the note after creating it; setting `type: "private"` alone does not deliver the details. + +The standard v0.16 client transport path sends note details in plaintext; end-to-end encryption is not implemented in that path. Private onchain visibility does not hide these details from the transport service. + +```typescript +// Relay an arbitrary private note. You can also pass an input note ID or +// record tracked by this client. +await client.notes.sendPrivate({ + note: privateNote, + to: "mtst1recipient...", + scanAfterBlockNum, // chain tip recorded when the transaction was submitted +}); + +// For an applied output note created by this client, let the SDK derive the +// scan-start block from its stored expected height. +await client.notes.sendPrivateOutput({ + noteId: "0xnote...", + to: "mtst1recipient...", +}); + +// On the client that tracks the recipient, fetch incrementally from the +// stored transport cursor. +await recipientClient.notes.fetchPrivate(); + +// Now inspect the inbox +const notes = await recipientClient.notes.list(); +console.log(`Tracked ${notes.length} notes`); +``` + +`scanAfterBlockNum` must be at or below the note's commitment block. A value above it is never scanned backward and can silently prevent delivery. `sendPrivateOutput()` avoids that footgun for applied output notes created by the same client. Newly tracked tags are backfilled by `client.sync()`; `fetchPrivate({ mode: "all" })` is no longer available. + +You need a note transport endpoint configured on the client — set `noteTransportUrl` in `ClientOptions`, or use a network factory (`createTestnet`, `createDevnet`) that preconfigures it. + +## Tags + +Tags are `u32` values that the sync process uses as a fuzzy filter to decide which notes to pull for your client. They come from three sources: + +1. **Account tags** — auto-registered for every account the client tracks. +2. **Note tags** — auto-registered for notes the client expects. +3. **User tags** — manually added via `client.tags.add()`. + +```typescript +await client.tags.add(12345); + +const tags = await client.tags.list(); +console.log("Tracked tags:", tags); + +await client.tags.remove(12345); +``` + +Auto-generated tags (accounts, expected notes) cannot be removed — `remove()` only unregisters user-added tags. Use `NoteTag` helpers (exposed from the WASM module) to compute tag values from faucet IDs and account IDs. + +## Next + +- [Transactions](./transactions.md) — consume notes, send tokens, create output notes. +- [Compile](./compile.md) — author note scripts in MASM. +- [Sync and store](./sync.md) — the pipeline that feeds note state into your client. diff --git a/versioned_docs/version-0.16/builder/tools/clients/web-client/setup.md b/versioned_docs/version-0.16/builder/tools/clients/web-client/setup.md new file mode 100644 index 00000000..bdc1f41b --- /dev/null +++ b/versioned_docs/version-0.16/builder/tools/clients/web-client/setup.md @@ -0,0 +1,157 @@ +--- +title: Setup +sidebar_position: 2 +--- + +# Setting up the Web SDK + +## Install + +Add `@miden-sdk/miden-sdk` to your project. + +```bash +npm install @miden-sdk/miden-sdk +# or +yarn add @miden-sdk/miden-sdk +# or +pnpm add @miden-sdk/miden-sdk +``` + +The SDK targets modern browsers (Chrome, Firefox, Safari, Edge). The browser build uses WebAssembly and a Web Worker when available. Under Node 20+, the package automatically selects its native N-API binding with SQLite-backed storage. + +## Create a client + +Every operation goes through a `MidenClient` instance. Four factories cover the common cases: + +| Factory | Use for | +| --- | --- | +| `MidenClient.createTestnet()` | Miden testnet — RPC, prover, and note transport preconfigured | +| `MidenClient.createDevnet()` | Miden devnet — same shape, devnet endpoints | +| `MidenClient.createMock()` | Deterministic local mock chain for tests — no network | +| `MidenClient.create({ ... })` | Custom endpoints (localhost, self-hosted node, or any shorthand) | + +```typescript +import { MidenClient } from "@miden-sdk/miden-sdk"; + +// Miden testnet (most common for dApps under development) +const client = await MidenClient.createTestnet(); + +// Local node — "localhost" / "local" shorthand resolves to http://localhost:57291 +const local = await MidenClient.create({ rpcUrl: "localhost" }); + +// Any custom URL +const custom = await MidenClient.create({ + rpcUrl: "https://my-node.example.com", +}); + +// Mock chain (see the Testing guide) +const mock = await MidenClient.createMock(); +``` + +All factories are async because they initialize the platform runtime and client storage before the client is usable. + +## `ClientOptions` reference + +`createTestnet()`, `createDevnet()`, and `create()` accept the same `ClientOptions` shape. The differences are in what each factory pre-fills before the options are applied. `createMock()` accepts a separate `MockOptions` shape for configuring its local mock chain. + +### Field reference + +| Option | Type | Description | +| --- | --- | --- | +| `rpcUrl` | `"testnet" \| "devnet" \| "localhost" \| "local" \| string` | Node RPC endpoint. Shorthands expand to the hosted Miden endpoints; any other string is treated as a raw URL. | +| `noteTransportUrl` | `"testnet" \| "devnet" \| string` | Note transport service endpoint. Required for private-note `sendPrivate` / `fetchPrivate`. | +| `proverUrl` | `"local" \| "devnet" \| "testnet" \| string` | Default prover for transactions. `"local"` runs in the current environment; remote shorthands and URLs route to a remote / delegated prover. | +| `autoSync` | `boolean` | When `true`, the client runs one sync pass before the promise resolves. | +| `seed` | `string \| Uint8Array` | Seed for deterministic RNG. Strings are hashed to 32 bytes via SHA-256. | +| `storeName` | `string` | Store isolation key (IndexedDB database name in browsers). Set this to keep multiple clients' data separate in the same origin. | +| `keystore` | `{ getKey, insertKey, sign }` | Browser-only external keystore callbacks. Leave unset to use the built-in keystore; Node uses its filesystem keystore. | +| `useWorker` | `boolean` | Browser-only. Defaults to `true`; set it to `false` for callback provers or single-WebView native shells. | + +### Factory defaults + +Any option not passed falls back to the factory default, then to an SDK default. + +| Factory | `rpcUrl` | `proverUrl` | `noteTransportUrl` | `autoSync` | +| --- | --- | --- | --- | --- | +| `createTestnet(opts?)` | `"testnet"` | `"testnet"` | `"testnet"` | `true` | +| `createDevnet(opts?)` | `"devnet"` | `"devnet"` | `"devnet"` | `true` | +| `create(opts?)` **with** `rpcUrl` | your value | `"local"` | _none_ | `false` | +| `create(opts?)` **without** `rpcUrl` | _delegates to `createTestnet(opts)`_ | ← | ← | ← | + +`create()` without an `rpcUrl` is not a separate "custom" client — it forwards its options to `createTestnet()`. If you want a localhost client with local proving and no autosync, pass `rpcUrl: "localhost"` explicitly. + +### Testnet with an in-browser prover + +```typescript +// Testnet — but prove locally in the browser instead of offloading +const client = await MidenClient.createTestnet({ proverUrl: "local" }); +``` + +### Keeping two isolated clients in the same origin + +```typescript +const a = await MidenClient.createTestnet({ storeName: "wallet-a" }); +const b = await MidenClient.createTestnet({ storeName: "wallet-b" }); +``` + +Each call creates its own IndexedDB database. The two wallets' state never crosses over. + +## Keystores and authentication + +The built-in keystore handles signing for the common flows: `client.accounts.create()` generates a Falcon key, persists it, and the client uses it automatically during `client.transactions.*` calls. + +When you need explicit control — for example when creating a contract account with a pre-derived seed, or wiring an external signer — build an `AuthSecretKey` directly: + +```typescript +import { AuthSecretKey } from "@miden-sdk/miden-sdk"; + +// Falcon key (the default auth scheme for regular wallets) +const seed = crypto.getRandomValues(new Uint8Array(32)); +const auth = AuthSecretKey.rpoFalconWithRNG(seed); +``` + +Passing `auth` to `client.accounts.create()` stores it in the configured keystore for later signing. + +See [Accounts](./accounts.md) for full examples covering wallets, contracts, and faucets. For advanced browser setups — external signers, hardware wallets — the `keystore` option on `ClientOptions` wires the SDK to your own `sign`/`getKey`/`insertKey` callbacks. + +## Remote provers and per-transaction overrides + +Local proving in the browser is CPU-intensive for larger transactions. Override globally via `ClientOptions.proverUrl`, or per transaction via the `prover` field: + +```typescript +// Globally: every transaction uses the remote prover by default +const client = await MidenClient.create({ + rpcUrl: "testnet", + proverUrl: "https://prover.example.com", +}); + +// Per-transaction: pass a TransactionProver instance +await client.transactions.send({ + account: wallet, + to: recipient, + token: faucet, + amount: 100n, + prover: customProver, +}); +``` + +See [Transactions](./transactions.md) for the full lifecycle. + +## Minimal example + +```typescript +import { MidenClient } from "@miden-sdk/miden-sdk"; + +async function demo() { + const client = await MidenClient.createTestnet(); + + const wallet = await client.accounts.create(); + console.log("Wallet:", wallet.id().toString()); + + client.terminate(); +} + +demo().catch(console.error); +``` + +For a testable, offline-friendly version of this pattern, see [Testing](./testing.md). diff --git a/versioned_docs/version-0.16/builder/tools/clients/web-client/sync.md b/versioned_docs/version-0.16/builder/tools/clients/web-client/sync.md new file mode 100644 index 00000000..85611e40 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tools/clients/web-client/sync.md @@ -0,0 +1,89 @@ +--- +title: Sync and store +sidebar_position: 7 +--- + +# Sync and store + +Every operation the Web SDK performs reads from — or writes to — a local store. This page covers how to keep that store in sync with the Miden network, and how to back up / restore the store itself. + +## `client.sync()` + +Fetches private notes from the Note Transport Layer, then pulls onchain updates from the Miden node and applies them to the local store. Returns a `SyncSummary` describing what changed. + +```typescript +import { MidenClient } from "@miden-sdk/miden-sdk"; + +const client = await MidenClient.createTestnet(); + +const summary = await client.sync(); + +console.log("Block:", summary.blockNum()); +console.log("Committed notes:", summary.committedNotes().length); +console.log("Consumed notes:", summary.consumedNotes().length); +console.log("Committed txs:", summary.committedTransactions().length); +console.log("Updated accounts:", summary.updatedAccounts().length); +``` + +`SyncSummary` accessors: + +- `blockNum(): number` — tip block the summary is based on. +- `committedNotes(): NoteId[]` — notes committed in this sync window. +- `consumedNotes(): NoteId[]` — notes consumed in this sync window. +- `committedTransactions(): TransactionId[]` — transactions that were committed. +- `updatedAccounts(): AccountId[]` — accounts whose onchain state advanced. + +## Timeout handling + +```typescript +const summary = await client.sync(); +``` + +`client.sync()` does not take a timeout option. If a UI needs a wall-clock timeout, wrap the promise at the application layer. + +## Auto-sync on creation + +The network factories (`createTestnet`, `createDevnet`) default `autoSync: true`, so one sync runs before the client is returned. `create()` defaults to `false`; pass `autoSync: true` explicitly if you want the same behaviour for a custom endpoint: + +```typescript +const client = await MidenClient.create({ + rpcUrl: "localhost", + autoSync: true, +}); +``` + +## Current sync height + +Cheap check of the locally-known tip, no network call: + +```typescript +const height: number = await client.getSyncHeight(); +console.log("Local tip:", height); +``` + +## Store backup and restore + +The SDK exposes two **standalone** functions (not methods on `MidenClient`) for snapshotting the entire store: + +```typescript +import { exportStore, importStore } from "@miden-sdk/miden-sdk"; + +// Dump the store identified by storeName into a JSON string. +const storeName = await client.storeIdentifier(); // or pass your own storeName +const dump = await exportStore(storeName); + +// Later — on another device, or after a page refresh — restore it. +await importStore(storeName, dump); +``` + +- `exportStore(storeName)` → `Promise` — returns a JSON dump of the IndexedDB store. +- `importStore(storeName, dump)` → `Promise` — replaces all existing data in the target store with the dump. + +`importStore` is destructive. It overwrites the target store entirely — keep a backup if you need the previous state. + +These helpers are package-level exports, not methods on `MidenClient`, so they can run without an active client — for example in a worker that performs scheduled backups. + +## Next + +- [Testing](./testing.md) — running a deterministic, in-memory mock chain for tests. +- [Transactions](./transactions.md) — how network state feeds back into transaction flows. diff --git a/versioned_docs/version-0.16/builder/tools/clients/web-client/testing.md b/versioned_docs/version-0.16/builder/tools/clients/web-client/testing.md new file mode 100644 index 00000000..b9017b15 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tools/clients/web-client/testing.md @@ -0,0 +1,159 @@ +--- +title: Testing +sidebar_position: 8 +--- + +# Testing with the mock client + +`MidenClient.createMock()` returns a client backed by a fully in-memory mock chain. It exposes the same resource API as the real client — `accounts`, `transactions`, `notes`, `tags`, `compile`, `keystore` — so tests for your application code don't need parallel mocking logic. It also adds a handful of mock-specific helpers for driving the chain manually. + +Use it for unit tests, CI, and offline development. It is orders of magnitude faster than hitting testnet, and it works on flaky networks. + +## Create + +```typescript +import { MidenClient } from "@miden-sdk/miden-sdk"; + +const client = await MidenClient.createMock(); +``` + +`createMock()` accepts `MockOptions`: + +| Option | Type | Description | +| --- | --- | --- | +| `seed` | `string \| Uint8Array` | RNG seed. Strings are hashed to 32 bytes via SHA-256. | +| `serializedMockChain` | `Uint8Array` | Restore a previously serialized chain state. | +| `serializedNoteTransport` | `Uint8Array` | Restore a previously serialized note-transport state. | + +## Basic flow + +The mock chain does not create blocks automatically — advance it with `proveBlock()` after each transaction batch. Between `proveBlock()` and subsequent reads, call `client.sync()` to hydrate the store. + +```typescript +import { MidenClient, type AccountTypeValue } from "@miden-sdk/miden-sdk"; + +const FUNGIBLE_FAUCET: AccountTypeValue = 0; + +const client = await MidenClient.createMock(); + +const wallet = await client.accounts.create(); +const faucet = await client.accounts.create({ + type: FUNGIBLE_FAUCET, + symbol: "TEST", + decimals: 8, + maxSupply: 10_000_000n, +}); + +await client.proveBlock(); +await client.sync(); + +await client.transactions.mint({ + account: faucet, + to: wallet, + amount: 1000n, +}); + +await client.proveBlock(); +await client.sync(); + +const result = await client.transactions.consumeAll({ account: wallet }); +console.log(`Consumed ${result.consumed} notes`); + +await client.proveBlock(); +await client.sync(); + +const updatedWallet = await client.accounts.get(wallet); +if (!updatedWallet) throw new Error("Wallet not found after sync"); +const balance = updatedWallet.vault().getBalance(faucet.id()); +console.log(`Balance: ${balance}`); // Balance: 1000 +``` + +Read the refreshed account after syncing so its vault reflects the committed transaction. + +## Dummy proving + +Transaction proving is automatically replaced with `LocalTransactionProver.prove_dummy()` on mock clients. You don't configure anything — any transaction method that would normally prove is instead given a dummy proof that the mock chain accepts. Key consequences: + +- Near-instant transactions, regardless of script complexity. +- No prover configuration required — `proverUrl` is ignored on mock clients. +- Dummy proofs are **not** valid on real chains. + +## Mock-only helpers + +The `MidenClient` class exposes a few methods that only make sense on mock clients. Guard them behind `usesMockChain()` if your code needs to work against both mock and real clients. + +```typescript +if (client.usesMockChain()) { + await client.proveBlock(); +} + +const chainBytes = await client.serializeMockChain(); +const transportBytes = await client.serializeMockNoteTransportNode(); +``` + +| Method | Purpose | +| --- | --- | +| `client.proveBlock()` | Advance the mock chain by one block. | +| `client.usesMockChain()` | `true` on mock clients; `false` on real clients. | +| `client.serializeMockChain()` | Snapshot mock chain state as bytes. | +| `client.serializeMockNoteTransportNode()` | Snapshot mock note transport state as bytes. | + +## Snapshot and restore + +Serialize the state of a running mock client, then restore it in a new client. Useful for long-running test scaffolds that want a shared starting state: + +```typescript +// Setup: create client, advance chain, fund accounts, etc. +const setup = await MidenClient.createMock(); +// ... +const chainState = await setup.serializeMockChain(); +const transportState = await setup.serializeMockNoteTransportNode(); +setup.terminate(); + +// In a test, restore from the snapshot +const client = await MidenClient.createMock({ + serializedMockChain: chainState, + serializedNoteTransport: transportState, +}); +``` + +## Private-note transport + +The mock client ships its own in-memory note transport. The same `sendPrivate` / `fetchPrivate` flow works: + +```typescript +import { + createP2IDNote, + MidenClient, + type AccountTypeValue, +} from "@miden-sdk/miden-sdk"; + +const FUNGIBLE_FAUCET: AccountTypeValue = 0; + +const client = await MidenClient.createMock(); +const recipient = await client.accounts.create(); +const faucet = await client.accounts.create({ + type: FUNGIBLE_FAUCET, + symbol: "TEST", + decimals: 8, + maxSupply: 10_000_000n, +}); + +const note = createP2IDNote({ + from: faucet, + to: recipient, + assets: { token: faucet, amount: 1n }, + type: "private", +}); + +await client.notes.sendPrivate({ + note, + to: recipient, + scanAfterBlockNum: 0, +}); + +await client.notes.fetchPrivate(); + +const notes = await client.notes.list(); +console.log(`Received ${notes.length} notes`); // Received 1 notes +``` diff --git a/versioned_docs/version-0.16/builder/tools/clients/web-client/transactions.md b/versioned_docs/version-0.16/builder/tools/clients/web-client/transactions.md new file mode 100644 index 00000000..0efbead0 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tools/clients/web-client/transactions.md @@ -0,0 +1,427 @@ +--- +title: Transactions +sidebar_position: 4 +--- + +# Transactions + +`client.transactions` is the resource namespace for sending, minting, consuming, swapping, running custom scripts, and inspecting transaction history. The simplified operations below handle the full lifecycle — execute, prove, submit — in one call. + +## Simplified operations + +### `send` + +Creates a pay-to-ID note that transfers tokens from one account to another. + +```typescript +import { MidenClient } from "@miden-sdk/miden-sdk"; + +const client = await MidenClient.createTestnet(); + +const { txId } = await client.transactions.send({ + account: senderWallet, + to: recipientWallet, + token: faucet, + amount: 100n, + type: "private", // "public" or "private" — default "public" + reclaimAfter: 100, // optional: block number after which sender can reclaim + timelockUntil: 90, // optional: block number until which note is timelocked +}); +console.log("Send tx:", txId.toHex()); +``` + +Set `returnNote: true` to receive the created `Note` in the result (useful when you need the note body — for QR delivery, for example): + +```typescript +const { txId, note } = await client.transactions.send({ + account: senderWallet, + to: recipientWallet, + token: faucet, + amount: 100n, + returnNote: true, +}); +// `note` is guaranteed non-null when returnNote is true +``` + +`reclaimAfter` and `timelockUntil` are **block numbers**, not wall-clock times. + +:::note +The `returnNote: true` branch is a separate overload: it does not accept `reclaimAfter` or `timelockUntil`. If you need either of those, use the default branch (without `returnNote`) and retrieve the note through `client.notes.listSent()` instead. +::: + +### `mint` + +```typescript +const { txId } = await client.transactions.mint({ + account: faucet, // the faucet account + to: wallet, // recipient + amount: 1000n, + type: "private", // optional, default "public" +}); +console.log("Mint tx:", txId.toHex()); +``` + +### `consume` + +```typescript +// Consume one or more input notes +const { txId } = await client.transactions.consume({ + account: wallet, + notes: [noteId1, noteId2], // note references — hex strings, NoteIds, records, or Notes +}); + +// A different, unconsumed note can also be passed directly, without an array. +await client.transactions.consume({ + account: wallet, + notes: noteId3, +}); +``` + +### `consumeAll` + +Consumes every available note for an account, up to an optional limit. Useful for quickly draining an inbox. + +```typescript +const result = await client.transactions.consumeAll({ account: wallet }); +console.log(`Consumed ${result.consumed}, ${result.remaining} remaining`); +if (result.txId) { + console.log("Tx:", result.txId.toHex()); +} + +// Cap the number of notes consumed in one transaction +await client.transactions.consumeAll({ account: wallet, maxNotes: 5 }); +``` + +`result.remaining > 0` signals pagination — call `consumeAll` again to drain the rest. + +### `swap` + +Atomic swap between two assets. + +```typescript +const { txId } = await client.transactions.swap({ + account: wallet, + offer: { token: faucetA, amount: 100n }, + request: { token: faucetB, amount: 200n }, + type: "public", // optional — visibility of the offer note + paybackType: "private", // optional — visibility of the payback note +}); +``` + +## Waiting for confirmation + +All simplified operations return as soon as the transaction is submitted to the mempool. To block until the network commits the transaction, use `waitFor`: + +```typescript +const { txId } = await client.transactions.mint({ + account: faucet, + to: wallet, + amount: 1000n, +}); + +// Default: 60s timeout, 5s polling interval +await client.transactions.waitFor(txId.toHex()); + +// Custom polling +await client.transactions.waitFor(txId.toHex(), { + timeout: 120_000, // 2 minutes — set to 0 to poll indefinitely + interval: 3_000, + onProgress: (status) => { + console.log(`Status: ${status}`); // "pending" | "submitted" | "committed" + }, +}); +``` + +`waitFor` throws on rejection or timeout. + +## Preview transactions awaiting authorization + +`preview()` derives the `TransactionSummary` that an account is being asked to authorize without proving or submitting the transaction. It only returns a summary when authorization is still pending, such as a multisig request that has not reached its threshold: + +```typescript +const summary = await client.transactions.preview({ + operation: "send", + account: multisigAccount, + to: recipient, + token: faucet, + amount: 100n, +}); +``` + +If the account already authorizes the request, execution succeeds without producing a pending summary and `preview()` rejects with `TRANSACTION_ALREADY_AUTHORIZED`. Submit that transaction normally instead. The built-in preview operations follow the same rule. Use `operation: "custom"` when previewing a pre-built `TransactionRequest`, as in the cross-client flow below. + +### Keep a cross-client summary reproducible + +A transaction summary commits to its reference block. When one client proposes a transaction and another verifies or executes it later, capture a `ChainAnchor` and send it alongside the summary so every participant derives the transaction at the same block: + +```typescript +import { ChainAnchor, TransactionSummary } from "@miden-sdk/miden-sdk"; + +// Proposer: capture the request's reference block and derive the summary there. +const anchor = await client.transactions.captureAnchor(request); +const summary = await client.transactions.preview({ + operation: "custom", + account: multisigAccount, + request, + anchor, +}); + +const anchorBytes = anchor.serialize(); +const summaryBytes = summary.serialize(); +await sendProposal(anchorBytes, summaryBytes); + +// Co-signer or executor: restore the proposal and re-derive it at the same block. +const receivedAnchor = ChainAnchor.deserialize(anchorBytes); +const proposedSummary = TransactionSummary.deserialize(summaryBytes); +const derivedSummary = await client.transactions.preview({ + operation: "custom", + account: multisigAccount, + request, + anchor: receivedAnchor, +}); + +if (derivedSummary.toCommitment().toHex() !== proposedSummary.toCommitment().toHex()) { + throw new Error("The request does not match the proposed summary"); +} + +if (receivedAnchor.commitment().toHex() !== proposedSummary.blockCommitment().toHex()) { + throw new Error("The anchor does not match the proposed summary"); +} + +// After collecting the required authorization, replay at the anchored block. +await client.transactions.submit(multisigAccount, request, { anchor: receivedAnchor }); +// Release WASM wrappers; Node's native objects do not expose free(). +receivedAnchor.free?.(); +anchor.free?.(); +``` + +An anchor makes the request reproducible; it does not prove that the transaction matches the signer's intent. Inspect the summary's account delta and input/output notes before signing. Also verify a received anchor's block against a trusted node when the proposer is not trusted. If `summary.expirationDelta()` is non-zero, the transaction expires at `anchor.blockNum() + summary.expirationDelta()`; if that deadline passes, capture a new anchor and collect authorization again. + +## Custom transaction scripts (`execute`) + +When the simplified operations aren't enough — for example to call a procedure on a contract account — compile a transaction script with [`client.compile.txScript()`](./compile.md) and run it through `execute`: + +```typescript +import { MidenClient } from "@miden-sdk/miden-sdk"; + +const client = await MidenClient.createTestnet(); + +const script = await client.compile.txScript({ + code: ` + use external_contract::counter_contract + + @transaction_script + pub proc main + call.counter_contract::increment_count + end + `, + // Reuse the component installed on contractAccount. + libraries: [{ component: counterComponent }], +}); + +const { txId } = await client.transactions.execute({ + account: contractAccount.id(), + script, +}); + +console.log("Tx:", txId.toHex()); +``` + +### Foreign procedure invocation (FPI) + +Pass `foreignAccounts` to read state from other contracts during execution: + +```typescript +import { MidenClient, StorageSlot } from "@miden-sdk/miden-sdk"; + +const client = await MidenClient.createTestnet(); + +// Compile the foreign contract to get a procedure hash +const counterComponent = await client.compile.component({ + code: counterContractCode, + namespace: "external_contract::counter_contract", + slots: [StorageSlot.emptyValue("miden::tutorials::counter")], +}); +const getCountHash = counterComponent.getProcedureHash("get_count"); + +const script = await client.compile.txScript({ + code: ` + use external_contract::count_reader_contract + use miden::core::sys + + @transaction_script + pub proc main + padw padw padw padw + push.${getCountHash} + push.${counterAccount.id().prefix()} + push.${counterAccount.id().suffix()} + call.count_reader_contract::copy_count + exec.sys::truncate_stack + end + `, + // Reuse the component installed on countReaderAccount. + libraries: [{ component: countReaderComponent }], +}); + +const { txId } = await client.transactions.execute({ + account: countReaderAccount.id(), + script, + foreignAccounts: [ + // A bare reference is sufficient here because get_count reads a value slot. + counterAccount.id(), + // Storage-map entries require explicit storage requirements: + // { id: counterAccount.id(), storage: requirements }, + ], +}); +``` + +## View calls (`executeProgram`) + +`executeProgram` runs a transaction script locally, returning the resulting stack without submitting or proving anything. Use it to read onchain state — similar to `eth_call` in Ethereum. + +```typescript +const script = await client.compile.txScript({ + code: ` + use external_contract::counter_contract + + @transaction_script + pub proc main + call.counter_contract::get_count + end + `, + libraries: [{ component: counterComponent }], +}); + +const stack = await client.transactions.executeProgram({ + account: contractAccount.id(), + script, +}); + +// Node.js returns Felt[]; browser WASM returns the FeltArray wrapper. +const first = Array.isArray(stack) ? stack[0] : stack.get(0); +const count = first.asInt(); +console.log("Count:", count); +``` + +Options: + +| Option | Type | Description | +| --- | --- | --- | +| `account` | `AccountRef` | Account to execute the script against. | +| `script` | `TransactionScript` | Compiled script. | +| `adviceInputs` | `AdviceInputs?` | Advice inputs for the VM. Defaults to empty. | +| `foreignAccounts` | `(AccountRef \| { id, storage? })[]?` | Foreign accounts for FPI reads. | + +## Manual `TransactionRequest` + +For full control over note inputs and outputs — e.g. emitting multiple output notes from one transaction — build a `TransactionRequest` yourself and pass it to `submit`. + +The builder accepts WASM array classes (`NoteArray`, `NoteDetailsAndTagArray`, `NoteRecipientArray`) rather than plain TypeScript arrays. + +```typescript +import { + MidenClient, + TransactionRequestBuilder, + NoteArray, +} from "@miden-sdk/miden-sdk"; + +const client = await MidenClient.createTestnet(); + +// Populate a NoteArray with your output notes +const ownOutputs = new NoteArray(); +for (const note of outputNotes) { + ownOutputs.push(note); +} + +const request = new TransactionRequestBuilder() + .withOwnOutputNotes(ownOutputs) + .build(); + +const { txId } = await client.transactions.submit(wallet, request); +console.log("Tx:", txId.toHex()); +``` + +Expected-note hints are also available: + +- `withExpectedFutureNotes(NoteDetailsAndTagArray)` — notes the transaction should produce but won't emit directly (e.g. for later pickup). +- `withExpectedOutputRecipients(NoteRecipientArray)` — recipients for expected output notes. + +### Setting an expiration + +```typescript +const request = new TransactionRequestBuilder() + .withOwnOutputNotes(ownOutputs) + .withExpirationDelta(10) // expires 10 blocks after the reference block + .build(); + +await client.transactions.submit(wallet, request); +``` + +`withExpirationDelta()` cannot be combined with `withCustomScript()`; `build()` rejects that combination. A custom script must set its expiration through the transaction context instead. + +## Remote proving + +Local proving is CPU-intensive. Offload globally via `ClientOptions.proverUrl`, or per-transaction via the `prover` field: + +```typescript +// Global: every transaction uses the remote prover +const client = await MidenClient.create({ + rpcUrl: "testnet", + proverUrl: "https://prover.example.com", +}); + +// Per-transaction: pass a TransactionProver instance +await client.transactions.send({ + account: wallet, + to: recipient, + token: faucet, + amount: 100n, + prover: customProver, +}); +``` + +## History + +Query past transactions through `client.transactions.list()`. + +The `expiredBefore` filter was removed in v0.16. Expiry is resolved during state sync: sync first and inspect each transaction record's status. `{ status: "uncommitted" }` selects pending transactions, not expired transactions. + +```typescript +await client.sync(); + +// All transactions +const all = await client.transactions.list(); + +// Only uncommitted +const uncommitted = await client.transactions.list({ status: "uncommitted" }); + +// By ID +const specific = await client.transactions.list({ ids: [txId1, txId2] }); +``` + +Each record exposes: + +```typescript +for (const tx of all) { + tx.id().toHex(); + tx.accountId().toString(); // AccountId.toString() returns canonical hex + tx.blockNum().toString(); + + const status = tx.transactionStatus(); + if (status.isPending()) console.log("Pending"); + if (status.isCommitted()) console.log("Committed at", status.getBlockNum(), status.getCommitTimestamp()); + if (status.isDiscarded()) console.log("Discarded"); + + tx.initAccountState().toHex(); + tx.finalAccountState().toHex(); + + tx.inputNoteNullifiers().map((n) => n.toHex()); + tx.outputNotes().notes().map((note) => note.id().toString()); +} +``` + +## Next + +- [Notes](./notes.md) — list, import, export, private-note transport. +- [Compile](./compile.md) — MASM for account components, transaction scripts, note scripts. +- [Testing](./testing.md) — drive a fully in-memory mock chain for fast, deterministic tests. diff --git a/versioned_docs/version-0.16/builder/tools/index.md b/versioned_docs/version-0.16/builder/tools/index.md new file mode 100644 index 00000000..15c85232 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tools/index.md @@ -0,0 +1,43 @@ +--- +title: Tools +description: "Developer tools for building on and interacting with the Miden network — clients, note transport, bridging, playground, and the live network surface." +pagination_prev: null +--- + +# Tools + +Developer tools for building on and interacting with the Miden network. Use the client SDKs inside your app, Note Transport to relay private notes, Bridging guides to test interoperability flows, the Playground to prototype contracts in-browser, and the Network page to find the live testnet endpoints (status, explorer, RPC, faucet, remote prover). + +## Clients + + + + Full-featured Rust library for Miden layer 2 integration — accounts, transactions, notes, proving. + + + Browser-based client for managing accounts and transactions from a web app. + + + Hooks and components for Miden dApps. + + + +## Toolchain + environments + + + + Install and switch between Miden toolchain channels — VM, compiler, client, stdlib, kernel — from a unified `miden` command. + + + Interactive environment for writing and testing Miden Assembly programs. + + + Live Miden testnet endpoints — status, block explorer (MidenScan), RPC, faucet, remote prover. + + + Offchain relay service for delivering private note payloads between senders and recipients. + + + Compare bridge timing and tradeoffs, then integrate Agglayer or Epoch into a Miden application. + + diff --git a/versioned_docs/version-0.16/builder/tools/midenup.md b/versioned_docs/version-0.16/builder/tools/midenup.md new file mode 100644 index 00000000..61d4cda1 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tools/midenup.md @@ -0,0 +1,119 @@ +--- +title: midenup +sidebar_label: Midenup +sidebar_position: 2 +description: "The Miden toolchain installer — bootstrap, pin, and switch between Miden VM, compiler, client, and package toolchains from a single `miden` entry point." +--- + +# midenup + +`midenup` is the Miden toolchain installer. One install gives you a unified `miden` command that delegates to the Miden VM, compiler (`midenc` + `cargo-miden`), client, formatter, local package registry, and protocol packages — all versioned together as a single release channel. + + + + The installer repo. Tracks the canonical channel manifest and publishes prebuilt toolchain releases. + + + Machine-readable inventory of available toolchain channels and the component versions each one pins. + + + +## Install + +```bash title=">_ Install midenup" +cargo install midenup && midenup init +``` + +`midenup init` sets up `$MIDENUP_HOME`, writes a `miden` symlink into `$CARGO_HOME/bin`, and prepares the toolchain cache. Since most Rust users already have `$CARGO_HOME/bin` on their PATH, the `miden` command is ready immediately. + + +Run `miden --version`. If you see "command not found," add `$CARGO_HOME/bin` (default `~/.cargo/bin`) to your PATH and re-open the shell. + + +## Components it manages + + + + The MASM interpreter, prover, and verifier. Used by every Miden program. + + + `midenc` and `cargo-miden` — the Rust frontend that compiles `#[miden]` code to MASM. + + + `miden-client` — accounts, transactions, notes, proving. + + + `miden-format` — format Miden Assembly source files. + + + Core, protocol, standards, and transaction-kernel MASP packages used by the toolchain. + + + `miden-registry` — publish and inspect packages in a filesystem-backed local registry. + + + +## Toolchain management + +### Install a channel + +```bash +midenup install testnet # follows the release named by the testnet manifest +midenup install 0.16.0 # pin to a specific release line +``` + +### Switch the active toolchain + +```bash +midenup set 0.16.0 # pin for the current project (writes miden-toolchain.toml) +midenup override 0.16.0 # set the system-wide default +midenup show active-toolchain # which one is active right now? +``` + +A `miden-toolchain.toml` in the current directory always wins — otherwise the system default applies, falling back to `stable` if none is set. + +### Uninstall + +```bash +midenup uninstall 0.16.0 +``` + +Delete `$MIDENUP_HOME` to uninstall `midenup` itself. Find its location with `midenup show home`. + + +Removing toolchain directories manually corrupts the `midenup` environment. Use `midenup uninstall` so the installer updates its bookkeeping. + + +## The `miden` entry point + +`miden` delegates to the right component based on the subcommand. Common aliases: + +| `miden` command | Delegates to | What it does | +| --- | --- | --- | +| `miden new` | `cargo miden new` | Create a new Miden Rust project | +| `miden build` | `midenc miden-project.toml` | Build the current Miden project | +| `miden new-wallet` | `miden-client new-wallet` | Create a local wallet account | +| `miden account` | `miden-client new-account` | Create a local account | +| `miden faucet` | `miden-client mint` | Mint your own asset from a faucet account you control | +| `miden mint` | `miden-faucet-client mint` | Request native test tokens from the public faucet | +| `miden call` | `miden-client call` | Call a local account procedure | +| `miden simulate` | `miden-client exec` | Dry-run a transaction without committing | +| `miden transfer` | `miden-client transfer` | Transfer assets to another account | +| `miden format` | `miden-format` | Format MASM source (install with `--component format`) | +| `miden registry` | `miden-registry` | Manage the local registry (install with `--component local-registry`) | + +Use the component name to access commands that do not have an alias. The v0.16 channel has no `miden deploy` alias. `new-wallet` and `new-account` create accounts locally; fund and publish an account through its first successful transaction. Older channel aliases that add `--deploy` cannot be used with the stable v0.16 client. + +## Related + + + + Full environment setup — prerequisites, node install, first account. + + + Walk through `miden client account`, `miden client note`, `miden client sync`, and the rest. + + + Endpoints the `miden` CLI points at — RPC, faucet, remote prover, block explorer. + + diff --git a/versioned_docs/version-0.16/builder/tools/network.md b/versioned_docs/version-0.16/builder/tools/network.md new file mode 100644 index 00000000..39de62f8 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tools/network.md @@ -0,0 +1,70 @@ +--- +title: Network +sidebar_position: 4 +description: "Miden testnet endpoints — status, explorer, RPC, faucet, remote prover, and other public services." +--- + +# Network + +Public Miden testnet services — explorer, RPC, faucet, remote prover, and status. The canonical live inventory lives at [status.testnet.miden.io](https://status.testnet.miden.io/); the page below is an editorial overview that won't go stale as new endpoints ship. + + +The same services exist on devnet under the `devnet` subdomain — e.g., `status.devnet.miden.io`, `faucet.devnet.miden.io`, `devnet.midenscan.com`. Swap `testnet` → `devnet` in any URL on this page to point at devnet instead. + + + + + Dashboard showing every public Miden testnet endpoint with live health checks. Bookmark this when something looks off — it's the source of truth for "is the network up." + + + Search by account ID, transaction ID, note ID, or block height. The verification surface referenced throughout the tutorials. + + + +## Developer endpoints + + + + gRPC endpoint the node exposes for submitting proven transactions and querying account / note / block state. Clients connect here by default; see [status.testnet.miden.io](https://status.testnet.miden.io/) for the current URL. + + + Dispenses test assets (testnet MID and fungible test tokens) to the account ID you specify. Open [faucet.testnet.miden.io](https://faucet.testnet.miden.io/) to mint, or check the status page for the current endpoint. + + + Off-loads proof generation to a hosted prover when the client doesn't have the compute for client-side proving. Opt in via the client's `proverUrl` option. + + + Same as the top-right card — included here so the developer-endpoint view is complete. + + + + +Hard-coding testnet URLs in client configs is fine for demos, but the Miden ops team moves endpoints as new nodes come online. For anything production-ish, keep your configuration loading the current URLs from [status.testnet.miden.io](https://status.testnet.miden.io/) or the client's default (which tracks the canonical testnet host). + + +## Typical flow + + + +**Install the toolchain** — `midenup` pulls the client, CLI, and compiler in one step. See [Installation](../get-started/setup/installation). + +**Point the client at testnet** — default config targets the testnet RPC, so `miden new` projects work out of the box. + +**Top up with the faucet** — mint testnet assets to your account ID before attempting transactions. + +**Submit + verify** — send a proven transaction via the RPC; watch the account and transaction IDs land on [MidenScan](https://testnet.midenscan.com). + +**Offload proving if needed** — the remote prover URL is configurable if client-side proving isn't feasible (mobile, low-power clients). + + + +## Related + + + + End-to-end walkthrough — client → RPC → MidenScan verification. + + + How the node receives transactions, aggregates batches, and produces blocks. + + diff --git a/versioned_docs/version-0.16/builder/tools/note-transport/_category_.yml b/versioned_docs/version-0.16/builder/tools/note-transport/_category_.yml new file mode 100644 index 00000000..54817e06 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tools/note-transport/_category_.yml @@ -0,0 +1,3 @@ +label: "Note Transport" +position: 5 +collapsed: true diff --git a/versioned_docs/version-0.16/builder/tools/note-transport/design.md b/versioned_docs/version-0.16/builder/tools/note-transport/design.md new file mode 100644 index 00000000..22bc0a94 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tools/note-transport/design.md @@ -0,0 +1,107 @@ +--- +sidebar_position: 2 +title: Design +--- + +# Design + +The note transport node is intentionally small: it accepts note bytes, indexes them by note tag, and returns matching notes to clients. + +## Note flow + +1. A sender creates a private note in a Miden transaction. +2. After the note data is available locally, the sender calls `SendNote` with a serialized note header and note details. +3. The transport node parses the header, extracts the note ID and tag, and stores the header and details in SQLite. +4. A recipient calls `FetchNotes` for one or more tags and receives matching notes with a cursor. +5. The recipient stores the returned cursor and uses it on the next fetch. + +The transport node does not connect to a Miden node and does not know whether a note has been committed on-chain. Clients still need to import fetched notes and sync against the Miden network. + +## Stored data + +The transport node stores: + +- note ID, derived from the serialized header; +- note tag, derived from the serialized header; +- serialized header bytes; +- serialized details bytes; +- creation timestamp; +- `seq`, a monotonic SQLite `AUTOINCREMENT` value. + +The note ID column is unique. Re-sending the same note ID is rejected by the database instead of creating a duplicate row. + +## Cursor pagination + +`FetchNotes` uses a `seq` cursor: + +```protobuf +message FetchNotesRequest { + repeated fixed32 tags = 1; + fixed64 cursor = 2; +} + +message FetchNotesResponse { + repeated TransportNote notes = 1; + fixed64 cursor = 2; +} +``` + +The server returns notes matching any requested tag with `seq > cursor`, ordered by ascending `seq`, up to the server batch size. The response cursor is the highest `seq` returned. A client should persist that value and send it on the next request. + +Current limits: + +- A request may include up to 128 tags. +- A response returns up to 500 notes. +- There is no client-specified `limit` field in the protobuf API. + +The multi-tag query runs in one database snapshot. This avoids a race where separate per-tag queries could advance the cursor past a note inserted between queries. + +## Legacy cursor handling + +Earlier designs used timestamp cursors. Existing clients may have stored timestamp-sized cursor values. The node treats cursor values above `1_000_000_000_000` as legacy timestamp cursors and resets the effective query cursor to `0`. + +This lets upgraded clients recover instead of waiting for `seq` to reach an old timestamp-sized value. + +## Streaming + +`StreamNotes` opens a server-side stream for one tag: + +```protobuf +message StreamNotesRequest { + fixed32 tag = 1; + fixed64 cursor = 2; +} + +message StreamNotesUpdate { + repeated TransportNote notes = 1; + fixed64 cursor = 2; +} +``` + +Internally, a background task polls SQLite every 500 ms for new notes matching active subscriptions and forwards updates through bounded channels. + +The current server implementation does not use the request cursor to initialize subscription state. Use `FetchNotes` for durable catch-up and cursor persistence, then use streaming only as a live update channel. + +## Storage and retention + +The node uses SQLite and embedded migrations. File-backed databases use a larger connection pool. In-memory databases use a single connection because SQLite `:memory:` databases are isolated per connection. + +Notes older than the configured retention period are removed by a maintenance task. + +## Block context + +The current protobuf API does not include commitment block number, note metadata, or inclusion proof fields. The transport node stores only `header` and `details`. + +This means the node cannot tell a client which block committed a fetched note. Clients must reconcile fetched notes with chain state themselves. The client-side lookback workaround and the proposed transport-level block context are tracked separately in [0xMiden/note-transport-service#68](https://github.com/0xMiden/note-transport-service/issues/68). + +## What the node does not do + +The node does not: + +- validate note contents against chain state; +- connect to a Miden node; +- attach commitment block context; +- attach note inclusion proofs; +- inspect or decrypt note details; +- authenticate senders or recipients; +- guarantee delivery after the retention period. diff --git a/versioned_docs/version-0.16/builder/tools/note-transport/index.md b/versioned_docs/version-0.16/builder/tools/note-transport/index.md new file mode 100644 index 00000000..3462f1d6 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tools/note-transport/index.md @@ -0,0 +1,53 @@ +--- +sidebar_position: 1 +title: Note Transport +description: "Off-chain relay service for private note delivery on Miden." +pagination_prev: null +--- + +# Note Transport + +The Miden note transport service is an off-chain relay for private note delivery. It gives senders a place to publish serialized private notes and gives recipients a way to fetch notes that match the tags they monitor. + +Private note contents are not published on-chain. The chain stores note commitments, while the full note data must reach the recipient through another channel. Note transport is the standard network service for that off-chain delivery path. + +## Start here + + + + How the node stores notes, assigns cursors, routes by tag, and handles current protocol boundaries. + + + CLI flags, Docker Compose, telemetry, storage, ports, retention, and production cautions. + + + Request and response shapes for send, fetch, stream, stats, plus the recommended client sync pattern. + + + +## API surface + +| RPC | Use it for | Current behavior | +| --- | --- | --- | +| `SendNote` | Publish one transported note. | The `header` must decode as a Miden `NoteHeader`; `details` are stored as opaque bytes. | +| `FetchNotes` | Durable catch-up by tag. | Returns notes for one or more tags using a server-assigned `seq` cursor. | +| `StreamNotes` | Live updates for one tag. | Use it after a fetch cycle; current subscriptions do not initialize from the request cursor. | +| `Stats` | Basic operational counts. | Returns aggregate note and tag counts. Per-tag statistics are defined in protobuf but not populated yet. | + +## Transport model + +- **Private payload delivery.** The Miden chain stores note commitments. Note transport carries the full private note data that recipients need to import locally. +- **Tag-based routing.** Notes are indexed by the 32-bit `NoteTag` embedded in note metadata. The node has no account registry or recipient identity model. +- **Client-owned privacy policy.** The node parses note headers only. Clients decide which tags to monitor and whether note details should be encrypted before sending. +- **Temporary mailbox.** Notes are retained for the configured retention window. Delivery is best-effort and clients must persist fetch cursors. + +## Current boundaries + +- **No chain-state validation.** The node does not connect to a Miden node and does not prove that a stored note was committed on-chain. +- **No block context yet.** The current API does not attach commitment block numbers, note metadata, or inclusion proofs to fetched notes. This is tracked in [0xMiden/note-transport-service#68](https://github.com/0xMiden/note-transport-service/issues/68). +- **Duplicate notes are rejected.** SQLite stores note IDs with a uniqueness constraint. Sending the same note twice fails instead of creating duplicate rows. +- **Cursor values are server-owned.** Fetch pagination uses the monotonic SQLite `seq` value returned by the server. Clients should persist returned cursors, not fabricate them. + +## Current implementation + +The current node implementation is a Rust gRPC service backed by SQLite. It stores each note with a monotonic `seq` value assigned at insert time, uses that value for `FetchNotes` pagination, and can export traces and metrics through OpenTelemetry. diff --git a/versioned_docs/version-0.16/builder/tools/note-transport/operators.md b/versioned_docs/version-0.16/builder/tools/note-transport/operators.md new file mode 100644 index 00000000..380a424d --- /dev/null +++ b/versioned_docs/version-0.16/builder/tools/note-transport/operators.md @@ -0,0 +1,123 @@ +--- +sidebar_position: 3 +title: Operators +--- + +# Operators + +This page covers running a note transport node. + +## Build from source + +From the repository root: + +```bash +cargo install --path bin/node --locked +``` + +This installs the `miden-note-transport-node` binary. + +## Run the node + +The default configuration binds to localhost and stores notes in an in-memory SQLite database: + +```bash +miden-note-transport-node +``` + +For a reachable node with persistent storage: + +```bash +miden-note-transport-node \ + --host 0.0.0.0 \ + --port 57292 \ + --database-url /var/lib/miden-note-transport/node.db \ + --retention-days 30 +``` + +## CLI flags + +| Flag | Default | Description | +| --- | --- | --- | +| `--host` | `127.0.0.1` | Address to bind to. | +| `--port` | `57292` | gRPC port. | +| `--database-url` | `:memory:` | SQLite database URL or file path. Use a file path for persistence. | +| `--retention-days` | `30` | How long to retain notes before cleanup. | +| `--max-note-size` | `512000` | Maximum note details size in bytes. | +| `--max-connections` | `4096` | Maximum concurrent gRPC connections. | +| `--request-timeout` | `4` | Per-request timeout in seconds. | + +The CLI flags above are parsed as command-line arguments. They are not currently read from `DATABASE_URL` or similarly named environment variables. + +## Telemetry and logging + +Telemetry is configured through environment variables: + +| Variable | Default | Description | +| --- | --- | --- | +| `OTEL_ENABLED` | `false` | Enables OpenTelemetry export when set to `true`. | +| `OTEL_TRACES_ENDPOINT` | `http://localhost:4317` | OTLP endpoint for trace and metric export. | +| `JSON_LOGGING` | `false` | Emits JSON logs when set to `true`. | +| `RUST_LOG` | `INFO` | Standard Rust tracing filter. | + +Example: + +```bash +OTEL_ENABLED=true \ +OTEL_TRACES_ENDPOINT=http://otel-collector:4317 \ +JSON_LOGGING=true \ +RUST_LOG=INFO \ +miden-note-transport-node --host 0.0.0.0 --database-url /var/lib/miden-note-transport/node.db +``` + +## Docker Compose + +The repository includes a Docker Compose setup for the node plus telemetry services: + +```bash +make docker-node-up +``` + +This starts: + +- note transport node; +- OpenTelemetry Collector; +- Tempo; +- Prometheus; +- Grafana. + +Use: + +```bash +make docker-node-down +``` + +to stop the stack. + +The Compose node service passes `--database-url /app/data/node.db` and mounts `/app/data` on the `node_data` volume, so note storage survives container restarts. + +## Ports + +| Port | Service | +| --- | --- | +| `57292` | Note transport gRPC API. | +| `4317` | OTLP gRPC receiver in the collector. | +| `4318` | OTLP HTTP receiver in the collector. | +| `3000` | Grafana. | +| `9090` | Prometheus. | +| `3200` | Tempo. | + +The note transport node exposes gRPC health through the same gRPC server, not a separate HTTP health port. + +## Database behavior + +Use a file-backed SQLite path for production-like deployments. The default `:memory:` database is useful for local testing but loses all notes on restart. + +The node runs embedded migrations at startup. The current schema stores note IDs with a uniqueness constraint and uses a monotonic `seq` column for pagination. + +## Operational cautions + +- Treat debug logs as sensitive. Note IDs and tags can be correlated with user activity. +- Configure a retention period that matches the expected offline window for your users. +- Monitor request errors. Duplicate note IDs or invalid note headers are rejected. +- Use `FetchNotes` for durable catch-up. Streaming is best used as a live update channel after a fetch cycle. diff --git a/versioned_docs/version-0.16/builder/tools/note-transport/users.md b/versioned_docs/version-0.16/builder/tools/note-transport/users.md new file mode 100644 index 00000000..70c09a4d --- /dev/null +++ b/versioned_docs/version-0.16/builder/tools/note-transport/users.md @@ -0,0 +1,156 @@ +--- +sidebar_position: 4 +title: Users +--- + +# Users + +This page covers integrating with the note transport gRPC API. + +## API surface + +The service is defined in `proto/proto/miden_note_transport.proto`. + +```protobuf +service MidenNoteTransport { + rpc SendNote(SendNoteRequest) returns (SendNoteResponse); + rpc FetchNotes(FetchNotesRequest) returns (FetchNotesResponse); + rpc StreamNotes(StreamNotesRequest) returns (stream StreamNotesUpdate); + rpc Stats(google.protobuf.Empty) returns (StatsResponse); +} +``` + +## Send a note + +`SendNote` stores one note: + +```protobuf +message SendNoteRequest { + TransportNote note = 1; +} + +message SendNoteResponse {} + +message TransportNote { + bytes header = 1; + bytes details = 2; +} +``` + +`header` must be a serialized Miden `NoteHeader`. The node parses it to extract the note ID and tag. `details` is opaque to the transport node and may contain encrypted note details. + +The server rejects: + +- requests without a note; +- headers that cannot be parsed as `NoteHeader`; +- details larger than the configured `--max-note-size`; +- duplicate note IDs. + +## Fetch notes + +`FetchNotes` returns notes for one or more tags: + +```protobuf +message FetchNotesRequest { + repeated fixed32 tags = 1; + fixed64 cursor = 2; +} + +message FetchNotesResponse { + repeated TransportNote notes = 1; + fixed64 cursor = 2; +} +``` + +Use this flow: + +1. Start with `cursor = 0`. +2. Send all tags the client wants to check, up to 128 tags. +3. Import or process the returned notes. +4. Persist the response `cursor`. +5. Repeat with the stored cursor. + +The response cursor is the highest server-side `seq` value returned in that response. Never fabricate cursor values; use values returned by the server. + +The server batch size is 500 notes. If a response contains many notes, call `FetchNotes` again with the returned cursor until the response is empty or smaller than the batch size. + +## Stream notes + +`StreamNotes` provides a server-side stream for one tag: + +```protobuf +message StreamNotesRequest { + fixed32 tag = 1; + fixed64 cursor = 2; +} + +message StreamNotesUpdate { + repeated TransportNote notes = 1; + fixed64 cursor = 2; +} +``` + +Use streaming as a live update channel. For durable sync, first call `FetchNotes` and persist its cursor. + +Current behavior to account for: + +- The protobuf request includes a `cursor`, but the current server implementation does not seed subscription state from that field. +- Subscriptions are per tag. +- The streamer polls for updates every 500 ms. +- If a subscriber cannot keep up with the bounded channel, the subscription is dropped. + +On reconnect, run `FetchNotes` with your persisted cursor before opening a new stream. + +## Stats + +`Stats` returns aggregate counts: + +```protobuf +message StatsResponse { + uint64 total_notes = 1; + uint64 total_tags = 2; + repeated TagStats notes_per_tag = 3; +} + +message TagStats { + fixed32 tag = 1; + uint64 note_count = 2; + google.protobuf.Timestamp last_activity = 3; +} +``` + +The current server returns `total_notes` and `total_tags`. Per-tag statistics are not populated yet. + +## Client sync pattern + +A typical client should: + +1. Configure a note transport endpoint. +2. Track the note tags it needs to monitor. +3. Fetch notes during sync using the stored transport cursor. +4. Import fetched notes into the client. +5. Sync with the Miden node to reconcile note commitments. +6. Persist the returned transport cursor only after the fetched notes have been handled successfully. + +The transport node does not provide commitment block numbers or inclusion proofs. Clients must still handle chain-state reconciliation. The block-context improvement is tracked in [0xMiden/note-transport-service#68](https://github.com/0xMiden/note-transport-service/issues/68). + +## Troubleshooting + +### Notes do not appear + +- Check that the sender actually called `SendNote`. +- Check that the recipient is fetching the same tag stored in the note header. +- Check whether the note expired under the node retention policy. +- Reset the local transport cursor to `0` if client state is suspected to be ahead of the server. + +### Duplicate send fails + +The database stores note IDs uniquely. Sending the same note twice is rejected instead of producing two stored rows. + +### Streaming misses notes + +Use `FetchNotes` for catch-up. Streaming is not a replacement for durable cursor sync in the current implementation. + +### Large notes are rejected + +The `--max-note-size` setting applies to the note details size. Increase it on the operator side only if the deployment is prepared to accept larger payloads. diff --git a/versioned_docs/version-0.16/builder/tools/playground.md b/versioned_docs/version-0.16/builder/tools/playground.md new file mode 100644 index 00000000..fe2555c1 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tools/playground.md @@ -0,0 +1,50 @@ +--- +title: Playground +sidebar_position: 3 +description: "Browser-based sandbox for building Miden smart contracts and interacting with accounts, notes, and transactions — no local tooling required." +--- + +# Miden Playground + +An interactive browser environment for learning Miden and testing smart contracts. No installation required — create a testnet sandbox, write and compile Rust account components and scripts, inspect the generated Miden Assembly (MASM), and execute transactions. + + + + Launch a tutorial, open an example, or create a testnet sandbox. + + + Instruction set, stack semantics, chiplets, and assembler behaviour. + + + +## What you can do + + + + Write and compile account components, authentication components, note scripts, and transaction scripts in Rust. + + + Review the read-only MASM output together with package exports, dependencies, and compilation errors. + + + Create or import accounts and notes, invoke account procedures, consume notes, and inspect transactions. + + + + +The Playground is useful for guided tutorials and end-to-end experiments without local setup. Move to a `miden new` Rust project when you need source control, automated tests, or a custom build and deployment workflow. See [your first smart contract](../get-started/your-first-smart-contract) for the handoff. + + +## Related + + + + Install the toolchain and build + deploy a counter contract in Rust. + + + Explicit module declarations, new import syntax, debug procedures, and other MASM-level deltas. + + + Accounts, notes, transactions, and the Rust SDK surface. + + diff --git a/versioned_docs/version-0.16/builder/tutorials/_category_.json b/versioned_docs/version-0.16/builder/tutorials/_category_.json new file mode 100644 index 00000000..e3af22eb --- /dev/null +++ b/versioned_docs/version-0.16/builder/tutorials/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Tutorials", + "position": 1 +} diff --git a/versioned_docs/version-0.16/builder/tutorials/components/CodeSdkTabs.module.css b/versioned_docs/version-0.16/builder/tutorials/components/CodeSdkTabs.module.css new file mode 100644 index 00000000..391d6d32 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tutorials/components/CodeSdkTabs.module.css @@ -0,0 +1,82 @@ +/* CodeSdkTabs Component Styles */ + +.codeContainer { + margin: 1rem 0; + border: 1px solid var(--ifm-color-emphasis-300); + border-radius: var(--ifm-global-radius); + overflow: hidden; + background: var(--ifm-background-color); +} + +.tabContainer { + background: var(--ifm-color-emphasis-100); + border-bottom: 1px solid var(--ifm-color-emphasis-300); +} + +.tabButtons { + display: flex; + gap: 0; +} + +.tabButton { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.75rem 1rem; + border: none; + background: transparent; + color: var(--ifm-color-content-secondary); + cursor: pointer; + font-size: 0.875rem; + font-weight: 500; + transition: all 0.2s ease; + border-bottom: 2px solid transparent; +} + +.tabButton:hover { + color: var(--ifm-color-primary); + background: var(--ifm-color-emphasis-200); +} + +.tabButton.active { + color: var(--ifm-color-primary); + background: var(--ifm-color-emphasis-200); + border-bottom: 2px solid var(--ifm-color-primary); +} + +.codeSection { + position: relative; + margin: 0; +} + +.codeSection pre { + margin: 0; + border-radius: 0; + border: none; +} + +/* Remove any extra bottom spacing from the theme code block */ +.codeSection :global(.theme-code-block) { + margin-bottom: 0 !important; +} + +.outputSection { + border-top: 1px solid var(--ifm-color-emphasis-300); + background: var(--ifm-color-emphasis-50); +} + +.outputHeader { + padding: 0.5rem 1rem; + font-size: 0.875rem; + font-weight: 600; + color: var(--ifm-color-content-secondary); + background: var(--ifm-color-emphasis-100); + border-bottom: 1px solid var(--ifm-color-emphasis-200); +} + +.outputSection pre { + margin: 0; + border-radius: 0; + border: none; + background: var(--ifm-color-emphasis-50) !important; +} diff --git a/versioned_docs/version-0.16/builder/tutorials/components/CodeSdkTabs.tsx b/versioned_docs/version-0.16/builder/tutorials/components/CodeSdkTabs.tsx new file mode 100644 index 00000000..48b36ba2 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tutorials/components/CodeSdkTabs.tsx @@ -0,0 +1,138 @@ +import React, { useState } from "react"; +import CodeBlock from "@theme/CodeBlock"; +import styles from "./CodeSdkTabs.module.css"; + +interface CodeExample { + react?: { + code: string; + output?: string; + }; + typescript?: { + code: string; + output?: string; + }; +} + +interface CodeSdkTabsProps { + example: CodeExample; + reactFilename?: string; + tsFilename?: string; +} + +// Dot-indentation convention for CodeSdkTabs +// ───────────────────────────────────────────── +// MDX/webpack strips leading whitespace from template literals inside JSX props. +// To preserve indentation in code snippets, use leading dots in the markdown +// source. Each dot represents one indent level (2 spaces). +// +// Example in a .md file: +// typescript: { code: `export function foo() { +// .const x = 1; +// .if (x) { +// ..console.log(x); +// .} +// }` } +// +// Renders as: +// export function foo() { +// const x = 1; +// if (x) { +// console.log(x); +// } +// } +// +// Rules: +// 0 dots → top-level declarations (export, import, closing braces) +// 1 dot → first level inside a function/block body +// 2 dots → second level (nested blocks, function call arguments) +// 3+ dots → deeper nesting +function preserveIndent(code: string): string { + return code.replace(/^(\.+)/gm, (match) => ' '.repeat(match.length)); +} + +export default function CodeSdkTabs({ + example, + reactFilename = "index.tsx", + tsFilename = "index.ts", +}: CodeSdkTabsProps): JSX.Element { + const [activeTab, setActiveTab] = useState<"react" | "typescript">( + example.react ? "react" : "typescript" + ); + + const hasReact = !!example.react; + const hasTypeScript = !!example.typescript; + + // Infer syntax language from filename extension (.tsx → tsx, .ts → ts) + const langFor = (filename: string, fallback: string) => + filename.endsWith(".tsx") ? "tsx" : filename.endsWith(".ts") ? "ts" : fallback; + + // Don't show tabs if there's only one language + if (!hasReact || !hasTypeScript) { + const singleLang = hasReact ? "react" : "typescript"; + const singleExample = example[singleLang]; + const filename = singleLang === "react" ? reactFilename : tsFilename; + + return ( +
+
+ + {preserveIndent(singleExample!.code)} + +
+ {singleExample!.output && ( +
+
Output
+ {singleExample.output} +
+ )} +
+ ); + } + + const currentExample = example[activeTab]; + const activeFilename = activeTab === "react" ? reactFilename : tsFilename; + + return ( +
+
+
+ + +
+
+ +
+ + {preserveIndent(currentExample!.code)} + +
+ + {currentExample!.output && ( +
+
Output
+ {currentExample.output} +
+ )} +
+ ); +} diff --git a/versioned_docs/version-0.16/builder/tutorials/components/index.ts b/versioned_docs/version-0.16/builder/tutorials/components/index.ts new file mode 100644 index 00000000..33f91fd9 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tutorials/components/index.ts @@ -0,0 +1 @@ +export { default as CodeSdkTabs } from './CodeSdkTabs'; diff --git a/versioned_docs/version-0.16/builder/tutorials/helpers/_category_.json b/versioned_docs/version-0.16/builder/tutorials/helpers/_category_.json new file mode 100644 index 00000000..b6d47724 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tutorials/helpers/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Guides", + "position": 4 +} diff --git a/versioned_docs/version-0.16/builder/tutorials/helpers/debugging.md b/versioned_docs/version-0.16/builder/tutorials/helpers/debugging.md new file mode 100644 index 00000000..fc74d664 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tutorials/helpers/debugging.md @@ -0,0 +1,90 @@ +--- +sidebar_position: 2 +title: "Debugging Guide" +description: "Learn how to debug Miden Rust contracts using debug output and assertions." +--- + +# Debugging Guide + +Miden supports [interactive DAP debugging](../../tools/clients/rust-client/debugging). +Use assertions to check values during execution and `miden::println!` markers to trace paths +when running with a debugger host that renders them. + +## Printing Debug Markers + +Use a literal or string expression to mark the path taken through a contract: + +```rust +miden::println!("entered withdraw"); + +if balance == felt!(0) { + miden::println!("balance is empty"); +} +``` + +`miden::println!` accepts string literals and expressions. It also supports Rust-style formatting +arguments, such as `miden::println!("balance: {}", balance)`. Formatted output requires +`extern crate alloc` and a configured global allocator; literal markers don't allocate. + +:::note Where Rust markers appear +With SDK 0.14.0, `miden::println!` emits a `readonly::miden_debug::println` event. +The normal Rust client and MockChain transaction executors ignore that event. Attaching a DAP +client preserves the transaction host's handlers, so the live DAP connection alone does not +make these messages appear. + +The `miden-debug` local execution and replay host handles these events. For a transaction, +[record a DAP session and replay it](../../tools/clients/rust-client/debugging#recording-a-session-for-offline-replay) +to inspect the markers in the debugger's output. MASM's +[`miden::core::debug` printers](../../tools/clients/rust-client/debug-output) use separate events +that the normal transaction executor prints by default. +::: + +## Using assert_eq + +The `assert_eq` function compares two `Felt` values and fails if they differ: + +```rust +use miden::*; + +// Check if a value equals an expected value +assert_eq(actual_value, expected_value); +``` + +:::note +`assert_eq` is a **function**, not a macro. Use `assert_eq(a, b)` without the exclamation mark. +::: + +## Narrowing Down Failures + +Execution errors include source diagnostics when debug information is available. Combine those +diagnostics with markers and assertions to isolate the failing operation: + +1. Place `miden::println!` markers before and after the code you suspect. +2. Add an `assert_eq` for the value the code expects. +3. Run with a debugger host that renders Rust markers and inspect the last marker and any assertion failure. + +### Example + +```rust +pub fn withdraw(&mut self, depositor: AccountId, amount: Felt) { + let balance = self.get_balance(depositor); + miden::println!("loaded balance"); + + // Check the assumption used by the code below. + assert_eq(balance, felt!(1000)); + + let new_balance = balance - amount; + self.balances.set(depositor, new_balance); + miden::println!("updated balance"); +} +``` + +Move the markers and assertion through the function to narrow down which assumption or operation +fails. + +## Limitations + +- `assert_eq` only works with `Felt` values +- `miden::println!` emits an event and adds execution work even when the host ignores it; remove debug-only calls + from release code +- Remove only diagnostic assertions; keep assertions that enforce contract invariants diff --git a/versioned_docs/version-0.16/builder/tutorials/helpers/pitfalls.md b/versioned_docs/version-0.16/builder/tutorials/helpers/pitfalls.md new file mode 100644 index 00000000..47b585b0 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tutorials/helpers/pitfalls.md @@ -0,0 +1,524 @@ +--- +sidebar_position: 3 +title: "Common Pitfalls" +description: "Reference guide for known issues, limitations, and workarounds when developing with the Miden Rust compiler." +--- + +# Common Pitfalls + +This reference documents known issues and limitations when developing with the Miden Rust compiler, along with recommended workarounds. + +## Comparing Asset Amounts + +### Problem + +`Felt` comparison operators work, but a field element is not a validated integer amount type. +Reading an amount directly from an asset bypasses its fungibility and range checks, and subsequent +arithmetic remains vulnerable to modular wraparound. + +```rust +// Avoid decoding a token amount as a raw field element. +let amount = asset.value[0]; +if amount <= felt!(1_000_000) { + // ... +} +``` + +### Solution + +Use `AssetAmount` for fungible token amounts. It has integer ordering and checked arithmetic: + +```rust +use miden::AssetAmount; + +let a = AssetAmount::from(100_u32); +let b = AssetAmount::from(200_u32); +if a < b { + // Integer comparison +} +``` + +### Example from Bank Contract + +```rust title="contracts/bank-account/src/lib.rs" +// Validating deposit amount +const MAX_DEPOSIT_AMOUNT: u32 = 1_000_000; + +// Asset::amount() validates that the asset is fungible and returns AssetAmount. +let amount = asset.amount(); + +// Use integer comparison +assert!( + amount <= AssetAmount::from(MAX_DEPOSIT_AMOUNT), + "Deposit exceeds maximum" +); +``` + +:::warning Raw Felt values +When a protocol API genuinely gives you a raw `Felt`, use `as_canonical_u64()` only after +confirming that the value is intended to have integer semantics. For fungible assets, prefer +`Asset::amount()` and keep the value as `AssetAmount`. +::: + +--- + +## Stack Limit (16 Elements) + +### Problem + +The Miden VM stack only allows direct access to the first 16 elements. If the compiler emits an instruction that accesses beyond that window, compilation fails with this error: + +``` +invalid stack index: only the first 16 elements on the stack are directly accessible +``` + +### Solution + +This is not a limit of 16 Rust local variables. The compiler can reorder or store values in memory. If you encounter this diagnostic, try reducing how many values a function needs at once. The following sketches illustrate possible refactorings; they omit the application-specific getters and processing logic. + +**1. Reduce local variables:** + +```rust +// Before: keep several values available at once +fn complex_operation(&mut self) { + let a = self.get_a(); + let b = self.get_b(); + let c = self.get_c(); + let d = self.get_d(); + let e = self.get_e(); + let f = self.get_f(); + // ... use these values later +} + +// After: combine values in smaller batches +fn complex_operation(&mut self) { + // Process in smaller batches + let result_ab = self.process(self.get_a(), self.get_b()); + let result_cd = self.process(self.get_c(), self.get_d()); + self.finalize(result_ab, result_cd); +} +``` + +**2. Break into smaller functions:** + +```rust +// Before: one large function +fn do_everything(&mut self, a: Word, b: Word, c: Word) { + // Many operations touching all parameters... +} + +// After: split into stages (processing bodies omitted) +fn stage_one(&mut self, a: Word) -> Felt { + // Process a +} + +fn stage_two(&mut self, b: Word, result: Felt) -> Felt { + // Process b with result from stage one +} + +fn stage_three(&mut self, c: Word, result: Felt) { + // Final processing +} +``` + +**3. Process iteratively:** + +```rust +// CORRECT: Process one at a time +for asset in assets { + self.process_single_asset(asset); +} +``` + +--- + +## Exported Procedure Argument Limit (4 Words) + +### Problem + +Exported component procedures and direct cross-context calls can currently receive at most 4 +Words (16 Felts) as arguments. + +```rust +#[component] +trait Processor { + #[account_procedure] + fn process( + &mut self, + depositor: AccountId, // 2 Felts + asset: Asset, // 2 Words (key + value) + serial_num: Word, // 1 Word + tag: Felt, // 1 Felt + note_type: Felt, // 1 Felt + extra_data: Word, // 1 Word - EXCEEDS LIMIT! + ); +} +``` + +### Solution + +**1. Keep exported procedure inputs within 4 Words:** + +```rust +#[component] +trait Processor { + #[account_procedure] + fn process( + &mut self, + asset: Asset, // 2 Words (key + value) + serial_num: Word, // 1 Word + params: Word, // [tag, note_type, 0, 0] - 1 Word + ); +} +``` + +**2. Use note storage for passing data:** + +For note scripts, pass complex data via `active_note::get_storage()`: + +The example below requires at least two storage elements; indexing a missing element aborts execution. + +```rust +#[note] +struct MyNote; + +#[note] +impl MyNote { + #[note_script] + fn run(self, _arg: Word) { + let storage = active_note::get_storage(); + // Storage can hold many Felts without function argument limits + let param1 = storage[0]; + let param2 = storage[1]; + // ... access up to the full storage capacity + } +} +``` + +**3. Store data first, reference by key:** + +```rust +// Store complex data in storage +fn store_config(&mut self, key: Word, config_data: Word) { + self.configs.set(key, config_data); +} + +// Reference by key in other operations +fn process_with_config(&mut self, key: Word) { + let config = self.configs.get(key); + // Use config... +} +``` + +--- + +## Array Ordering (Rust/MASM Reversal) + +### Problem + +At a Rust/MASM stack boundary, arrays appear on the operand stack in **reversed order**. + +```rust +// In Rust, you define: +let word = Word::from([a, b, c, d]); + +// In MASM, this becomes: [d, c, b, a] +``` + +### Solution + +Be aware of this when: +- Constructing storage keys +- Parsing note storage +- Working with asset data + +**Example: Storage Key Construction** + +```rust +// Balance-key input in Rust contract code +let key = Word::from([ + depositor.prefix, // Position 0 in Rust + depositor.suffix, // Position 1 + faucet.prefix, // Position 2 + faucet.suffix, // Position 3 +]); + +// When the VM processes this, it sees: +// [faucet.suffix, faucet.prefix, depositor.suffix, depositor.prefix] +``` + +:::tip Consistency is Key +The reversal doesn't matter as long as you're **consistent**. Always construct and parse arrays the same way throughout your codebase. +::: + +--- + +## Felt Arithmetic Underflow/Overflow + +### Problem + +Miden uses field element (Felt) arithmetic, which operates in a prime field with modulus `p = 2^64 - 2^32 + 1`. This means arithmetic is **modular** and will silently wrap around instead of causing an error. + +```rust +// DANGEROUS: This does NOT error on underflow! +let balance = felt!(100); +let withdrawal = felt!(500); +let new_balance = balance - withdrawal; // Silently wraps to a huge positive number! +``` + +When you subtract a larger value from a smaller one, the result wraps around to a large positive number (approximately `2^64`). This is NOT an error in the Miden VM - the transaction will succeed with an incorrect balance. + +### Why This Happens + +The Miden VM performs all Felt arithmetic as modular operations within the prime field. There is no automatic overflow or underflow detection at the VM level. + +### Solution + +**Use `AssetAmount` for asset balances:** + +```rust +// CORRECT: Keep balances in a StorageMap. +let current_balance: AssetAmount = self.balances.get(key); +let withdraw_amount = withdraw_asset.amount(); + +// AssetAmount subtraction checks for underflow. +let new_balance = current_balance - withdraw_amount; +self.balances.set(key, new_balance); +``` + +### Example from Bank Contract + +```rust title="contracts/bank-account/src/lib.rs" +pub fn withdraw(&mut self, key: Word, withdraw_asset: Asset) { + let current_balance: AssetAmount = self.balances.get(key); + let new_balance = current_balance - withdraw_asset.amount(); + self.balances.set(key, new_balance); +} +``` + +:::danger Critical Security Issue +Using unchecked raw `Felt` subtraction for balances can lead to: +- Users withdrawing more than their balance +- Balance values becoming astronomically large +- Complete loss of funds in the contract + +Use `AssetAmount` or explicitly validate bounds before subtracting raw `Felt` values. +::: + +--- + +## Wallet Component Requirement + +### Problem + +Standard P2ID notes receive assets through the consuming account's wallet `receive_asset` procedure. The `wallet::move_note_assets_to_account` helper calls that account procedure; the account must provide it. + +### Solution + +Include `BasicWallet` when an account needs the standard wallet receiving capability: + +```rust +use miden_client::account::component::BasicWallet; + +// When creating an account that needs to receive assets +let account = AccountBuilder::new(seed) + .with_component(auth_component) // Your configured authentication component + .with_component(BasicWallet) // Add wallet capability + .with_component(YourCustomComponent) + .build()?; +``` + +Supply the authentication component configured for your application (for example, `AuthSingleSig`) as `auth_component`. `BasicWallet` and a business component do not provide authentication by themselves. + +--- + +## Storage Map Key Consistency + +### Problem + +Storage map lookups return unexpected results or zeros when keys are constructed inconsistently. + +### Solution + +Define a single key construction pattern and use it everywhere: + +```rust title="contracts/bank-account/src/lib.rs" +use miden::{component_storage, AccountId, Asset, AssetAmount, StorageMap, Word}; + +#[component_storage] +struct BankStorage { + #[storage(description = "fungible balances by depositor and asset")] + balances: StorageMap, +} + +impl BankStorage { + /// Combine the depositor and fungible asset ID into one map key. + fn balance_key(depositor: AccountId, asset: &Asset) -> Word { + // Reject non-fungible assets before deriving the compact key. + let _ = asset.amount(); + + Word::from([ + depositor.prefix, + depositor.suffix, + asset.key[3], + asset.key[2], + ]) + } + + fn get_depositor_balance(&self, depositor: AccountId, asset: &Asset) -> AssetAmount { + let key = BankStorage::balance_key(depositor, asset); + self.balances.get(key) + } + + fn update_balance(&mut self, depositor: AccountId, asset: &Asset, amount: AssetAmount) { + let key = BankStorage::balance_key(depositor, asset); + let current = self.balances.get(key); + self.balances.set(key, current + amount); + } +} +``` + +--- + +## Note Type Values + +### Problem + +When creating output notes, the `note_type` parameter uses specific integer values that aren't obvious. + +### Solution + +Use the correct values for note types: + +| Value | Type | Description | +|-------|------|-------------| +| 1 | Public | Note data is visible onchain | +| 0 | Private | Only a commitment to the note details is published | + +```rust +// In note storage or when creating output notes +let note_type = felt!(1); // Public note +// or +let note_type = felt!(0); // Private note +``` + +--- + +## P2ID Script Root + +### Problem + +When creating P2ID (Pay-to-ID) output notes, you need the script's MAST root. The old v0.13 pattern of hardcoding the digest is fragile — it hashed under RPO, which v0.14 replaced with Poseidon2, and any future change to the P2ID script invalidates the constant silently. + +### Solution + +Carry the P2ID script root on the initiating note's storage and read it at runtime instead of hardcoding a value: + +```rust title="contracts/bank-account/src/lib.rs" +// The withdraw-request note encodes the P2ID script root in storage elements +// 10 through 13 (4 felts = 1 Word). The Poseidon2-hashed digest of the P2ID note +// script is injected by the caller when the note is created. +let storage = active_note::get_storage(); +let script_root = Word::from([ + storage[10], storage[11], storage[12], storage[13], +]); + +// Pass the script root through to the P2ID-note constructor +self.create_p2id_note(serial_num, &asset, depositor, tag, note_type, script_root); +``` + +On the client side, compute the script root dynamically from the standard P2ID note script instead of hardcoding it: + +```rust +use miden_client::note::P2idNote; +use miden_client::Word; + +// Script roots are typed NoteScriptRoot values; convert when a Word is needed. +let p2id_script_root: Word = P2idNote::script_root().into(); +``` + +:::info Why Not Hardcode +The native hash function changed from RPO to Poseidon2 in v0.14, so every MAST root — including the P2ID script's — is different from v0.13. Any hardcoded digest from v0.13 will fail a script-root check on current releases. Reading the root from `P2idNote::script_root()` (or the active note's storage for onchain code) keeps the contract resilient to future script changes. +::: + +--- + +## Empty Transaction (No State Change, No Notes) + +### Problem + +Every Miden transaction must either change tracked account state (storage, vault, or nonce) **or** consume at least one input note. A transaction that does neither is rejected. + +The VM kernel enforces this invariant during execution, surfacing the message: + +``` +executed transaction neither changed the account state, nor consumed any notes +``` + +This can catch a transaction script that takes a no-op branch when it consumes no notes and neither authentication nor fee payment changes the account state: + +```rust +#[tx_script] +fn run(arg: Word, account: &mut Account) { + let should_settle = arg[0]; + if should_settle == felt!(1) { + account.settle(); // mutates state + } + // When should_settle != felt!(1), this body does not change state. + // The transaction is rejected if authentication and fee payment + // also leave state unchanged and there are no input notes. +} +``` + +The invariant applies to the complete transaction. A nonce increment during authentication or a fee paid from the account's vault can provide a state change even when the transaction script does nothing. On a chain with zero fees, the standard `NoAuth` component increments the nonce only for a new account or when its state has changed, so a no-op on an existing account can trigger this error. + +### Solution + +If recording an attempted operation is part of your application's behavior, persist that record in the otherwise empty branch: + +```rust +#[tx_script] +fn run(arg: Word, account: &mut Account) { + let should_settle = arg[0]; + if should_settle == felt!(1) { + account.settle(); + } else { + // Record the attempt so the transaction still has a state delta. + account.record_attempt(); + } +} +``` + +Storage or vault changes also require the account nonce to increase. Configure an authentication component that handles this, such as the standard `NoAuth` component in a test fixture. Avoid adding a storage write solely to satisfy this invariant when authentication or fee payment already changes state. + +Alternatively, if the flow naturally consumes a note, make sure the transaction request includes +it. Pass the `Note` with optional `NoteArgs`; the client uses the presence of an inclusion proof in +its store to decide whether to consume it as an authenticated or unauthenticated note: + +```rust +let request = TransactionRequestBuilder::new() + .input_notes(vec![(input_note, None)]) + .build()?; +``` + +--- + +## Quick Reference Table + +| Pitfall | Symptom | Solution | +|---------|---------|----------| +| Asset amount stored as `Felt` | Modular wraparound or an invalid amount | Use `AssetAmount` | +| Stack overflow | "16 elements" error | Reduce locals, split functions | +| Too many exported procedure inputs | Export-lifting error | Group into Words, use note storage | +| Array reversal | Wrong data order | Be consistent with construction | +| Felt underflow | Balance wraps to huge number | Use `AssetAmount` or validate raw values | +| Missing wallet | Asset operation fails | Add `BasicWallet` component | +| Key mismatch | Zero balances | Use helper function for keys | +| Note type | Wrong note visibility | Use 1 (Public) or 0 (Private) | +| Empty transaction | "Neither changed account state nor consumed notes" | Check the complete transaction's state changes and input notes | + +## Next Steps + +- **[Debugging Guide](./debugging)** - Troubleshoot errors +- **[Testing Guide](./testing)** - MockChain patterns +- **[Miden Bank Tutorial](../miden-bank/)** - See these patterns in context diff --git a/versioned_docs/version-0.16/builder/tutorials/helpers/testing.md b/versioned_docs/version-0.16/builder/tutorials/helpers/testing.md new file mode 100644 index 00000000..c0781d78 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tutorials/helpers/testing.md @@ -0,0 +1,592 @@ +--- +sidebar_position: 1 +title: "Testing with MockChain" +description: "Learn how to test Miden Rust compiler contracts using MockChain for simulating blockchain behavior locally." +--- + +# Testing with MockChain + +MockChain provides a local simulation of the Miden blockchain for testing your contracts without connecting to a network. This guide covers testing patterns for account components, note scripts, and transaction scripts. + +## Overview + +MockChain simulates: +- Block production and proving +- Account state management +- Note creation and consumption +- Transaction execution + +This enables fast, deterministic testing of your Miden contracts. + +## Test Project Setup + +Create an integration test crate alongside your contracts: + +```text +your-project/ +├── contracts/ +│ ├── my-account/ +│ └── my-note/ +└── integration/ + ├── Cargo.toml + ├── src/ + │ ├── lib.rs # Exports test helpers + │ └── helpers.rs # Test utilities + └── tests/ + └── my_test.rs # Test files +``` + +### Cargo.toml for Tests + +```toml title="integration/Cargo.toml" +[package] +name = "integration" +version = "0.1.0" +edition = "2024" +rust-version = "1.98.1" + +[[test]] +name = "my_test" +path = "tests/my_test.rs" + +[dependencies] +anyhow = "1.0" +tokio = { version = "1", features = ["rt-multi-thread", "macros"] } +miden-protocol = "0.16" +miden-standards = { version = "0.16", features = ["testing"] } +miden-testing = "0.16" +rand = "0.10" +``` + +Export the helpers from the integration crate: + +```rust title="integration/src/lib.rs" +pub mod helpers; +``` + +## Building Contracts for Tests + +Use the `miden` toolchain to build contracts and then load the generated `.masp` artifact: + +```rust title="integration/src/helpers.rs" +use std::{path::{Path, PathBuf}, process::Command}; +use anyhow::{bail, Context, Result}; +use miden_protocol::{assembly::Package, utils::serde::Deserializable}; + +pub fn build_project_in_dir(dir: &Path, release: bool) -> Result { + let profile_dir = if release { "release" } else { "dev" }; + + let mut command = Command::new("miden"); + command.arg("build"); + if release { + command.arg("--release"); + } + + let status = command + .current_dir(dir) + .status() + .context("failed to run miden build")?; + if !status.success() { + bail!("miden build failed with {status}"); + } + + let artifact_dir = dir.join("target/miden").join(profile_dir); + let mut artifacts = std::fs::read_dir(&artifact_dir)? + .filter_map(|entry| entry.ok().map(|entry| entry.path())) + .filter(|path| path.extension().is_some_and(|ext| ext == "masp")); + let artifact_path: PathBuf = artifacts + .next() + .context("miden build produced no MASP artifact")?; + if artifacts.next().is_some() { + bail!("expected one MASP artifact in {}", artifact_dir.display()); + } + + let package_bytes = std::fs::read(&artifact_path)?; + Package::read_from_bytes(&package_bytes) + .context("Failed to deserialize package") +} +``` + +## MockChain Basics + +### Creating a MockChain + +Use the builder pattern to set up your test environment: + +```rust +use miden_testing::{Auth, MockChain}; + +#[tokio::test] +async fn my_test() -> anyhow::Result<()> { + // Create builder + let mut builder = MockChain::builder(); + + // Add accounts, faucets, notes... + + // Build the chain + let mut mock_chain = builder.build()?; + + Ok(()) +} +``` + +### Adding a Faucet + +Faucets mint assets for testing: + +```rust +use miden_protocol::account::auth::AuthScheme; + +// Create a faucet with 1,000,000 max supply and 100 initial tokens +let faucet = builder.add_existing_basic_faucet( + Auth::BasicAuth { + auth_scheme: AuthScheme::Falcon512Poseidon2, + }, + "TEST", // Token symbol + 1_000_000, // Max supply + Some(100), // Initial token supply +)?; +``` + +### Adding Wallet Accounts + +Create accounts with initial assets: + +```rust +use miden_protocol::{account::auth::AuthScheme, asset::FungibleAsset}; + +// Create a wallet with 100 tokens from the faucet +let sender = builder.add_existing_wallet_with_assets( + Auth::BasicAuth { + auth_scheme: AuthScheme::Falcon512Poseidon2, + }, + [FungibleAsset::new(faucet.id(), 100)?.into()], +)?; +``` + +## Creating Custom Accounts + +For accounts with custom components, create configuration helpers: + +```rust title="integration/src/helpers.rs" +use miden_protocol::account::{component::InitStorageData, AccountType}; + +#[derive(Clone)] +pub struct AccountCreationConfig { + pub account_type: AccountType, + pub init_storage_data: InitStorageData, +} + +impl Default for AccountCreationConfig { + fn default() -> Self { + Self { + account_type: AccountType::Public, + init_storage_data: InitStorageData::default(), + } + } +} +``` + +### Creating Account from Package + +```rust +use std::{path::Path, sync::Arc}; + +use miden_protocol::{ + account::{ + component::InitStorageData, AccountBuilder, AccountComponent, StorageSlotName, + }, + Word, +}; +use miden_testing::{AccountState, Auth}; + +// Build the contract +let bank_package = Arc::new(build_project_in_dir( + Path::new("../contracts/bank-account"), + true, // release mode +)?); + +// Initialize values declared by the package's storage schema. +let initialized_slot = + StorageSlotName::new("bank_account::bank::initialized") + .expect("Valid slot name"); +let mut init_storage_data = InitStorageData::default(); +init_storage_data.insert_value(&initialized_slot, Word::default())?; + +let config = AccountCreationConfig { + init_storage_data, + ..Default::default() +}; + +// Instantiate the component from the package and add an existing account. +let component = AccountComponent::from_package( + &bank_package, + &config.init_storage_data, +)?; +let account = builder.add_account_from_builder( + Auth::IncrNonce, + AccountBuilder::new([7_u8; 32]) + .account_type(config.account_type) + .with_component(component), + AccountState::Exists, +)?; +``` + +## Creating Notes + +### Creating Notes with Assets + +```rust +use std::{path::Path, sync::Arc}; + +use miden_protocol::{asset::FungibleAsset, transaction::RawOutputNote}; +use miden_standards::testing::note::NoteBuilder; + +// Build note script +let deposit_note_package = Arc::new(build_project_in_dir( + Path::new("../contracts/deposit-note"), + true, +)?); + +// Create assets to attach +let deposit_amount: u64 = 1000; +let fungible_asset = FungibleAsset::new(faucet.id(), deposit_amount)?; + +// Create the note from the compiled package. +let mut rng = rand::rng(); +let deposit_note = NoteBuilder::new(sender.id(), &mut rng) + .package((*deposit_note_package).clone()) + .add_assets([fungible_asset.into()]) + .build()?; + +// Add to MockChain +builder.add_output_note(RawOutputNote::Full(deposit_note.clone())); +``` + +### Creating Notes with Inputs + +For notes that read parameters via `active_note::get_storage()`: + +```rust +use miden_protocol::{Felt, Word}; + +// Note storage is a vector of Felts. Define and document the schema for each note. +let serial_num = Word::from([ + Felt::new(0x1234567890abcdef).expect("serial limb is below the field modulus"), + Felt::new(0xfedcba0987654321).expect("serial limb is below the field modulus"), + Felt::new(0xdeadbeefcafebabe).expect("serial limb is below the field modulus"), + Felt::new(0x0123456789abcdef).expect("serial limb is below the field modulus"), +]); +let storage = vec![ + // Serial number [0-3] + serial_num[0], serial_num[1], serial_num[2], serial_num[3], + // Additional parameters [4-5] + Felt::from(tag), + Felt::ONE, // note_type (1 = Public) +]; + +let mut rng = rand::rng(); +let note = NoteBuilder::new(sender.id(), &mut rng) + .package((*note_package).clone()) + .note_storage(storage)? + .build()?; +``` + +## Executing Transactions + +### Basic Transaction Execution + +```rust +// Build MockChain after adding all accounts and notes +let mut mock_chain = builder.build()?; + +// Build and execute a transaction that consumes a committed note. +let executed_tx = mock_chain + .build_transaction(account.id()) + .authenticated_input_note(note.id()) + .build()?; +let executed_tx = executed_tx.execute().await?; + +// Add to pending transactions and prove block +mock_chain.add_pending_executed_transaction(&executed_tx)?; +mock_chain.prove_next_block()?; + +// Read the updated account from committed chain state. +let account = mock_chain.committed_account(account.id())?; +``` + +### Transaction with Script + +For transaction scripts (like initialization): + +```rust +use miden_protocol::transaction::TransactionScript; + +// Build the transaction script +let init_package = Arc::new(build_project_in_dir( + Path::new("../contracts/init-tx-script"), + true, +)?); + +let init_tx_script = TransactionScript::from_package(&init_package)?; + +// Execute with script +let executed_tx = mock_chain + .build_transaction(account.id()) + .tx_script(init_tx_script) + .build()? + .execute() + .await?; +``` + +### Transactions with Expected Output Notes + +When your contract creates output notes, specify them: + +```rust +use miden_protocol::{note::Note, transaction::RawOutputNote}; + +// Build the expected output note +let expected_note = Note::new( + output_assets, + output_metadata, + recipient, +); + +let executed_tx = mock_chain + .build_transaction(account.id()) + .authenticated_input_note(input_note.id()) + .expected_output_note(RawOutputNote::Full(expected_note)) + .build()? + .execute() + .await?; +``` + +## Verifying State Changes + +### Reading Storage After Transaction + +```rust +use miden_protocol::{account::StorageMapKey, asset::FungibleAsset, Felt, Word}; + +// After adding the transaction and proving its block... +let account = mock_chain.committed_account(account.id())?; + +// Read Value storage (by slot name) +let value: Word = account.storage().get_item(&initialized_slot)?; + +// Read Map storage (by slot name) +// Use the deposited asset's key: limb 2 includes its metadata byte. +let asset_key = FungibleAsset::new(faucet.id(), 1000)?.to_id_word(); +let key = Word::from([ + depositor.prefix().as_felt(), + depositor.suffix(), + asset_key[3], + asset_key[2], +]); +let balance = account + .storage() + .get_map_item(&balances_slot, StorageMapKey::new(key))?; + +// Assert expected values +assert_eq!( + balance, + Word::from([Felt::from(1000_u32), Felt::ZERO, Felt::ZERO, Felt::ZERO]), + "Balance should match deposited amount" +); +``` + +## Testing Error Conditions + +### Expecting Transaction Failure + +```rust +#[tokio::test] +async fn should_fail_without_initialization() -> anyhow::Result<()> { + // Setup WITHOUT initialization step... + + // Execute and expect failure + let result = mock_chain + .build_transaction(account.id()) + .authenticated_input_note(note.id()) + .build()? + .execute() + .await; + + assert!( + result.is_err(), + "Expected transaction to fail, but it succeeded" + ); + + // Optionally check error message + if let Err(e) = result { + println!("Expected error: {}", e); + } + + Ok(()) +} +``` + +### Testing Constraint Violations + +```rust +#[tokio::test] +async fn deposit_exceeds_max_should_fail() -> anyhow::Result<()> { + // Create deposit with amount > MAX_DEPOSIT_AMOUNT + let large_amount: u64 = 2_000_000; // Max is 1,000,000 + + // ... setup code ... + + let transaction = mock_chain + .build_transaction(account.id()) + .authenticated_input_note(note.id()) + .build()?; + let result = transaction.execute().await; + + assert!( + result.is_err(), + "Expected deposit to fail due to max limit" + ); + + Ok(()) +} +``` + +## Complete Test Example + +```rust title="integration/tests/counter_test.rs" +use std::{path::Path, sync::Arc}; + +use anyhow::Context; +use integration::helpers::build_project_in_dir; +use miden_protocol::{ + account::{ + auth::AuthScheme, + component::InitStorageData, + AccountBuilder, + AccountComponent, + AccountType, + StorageMapKey, + StorageSlotName, + }, + crypto::rand::RandomCoin, + note::NoteScript, + transaction::RawOutputNote, + Felt, + Word, +}; +use miden_standards::testing::note::NoteBuilder; +use miden_testing::{AccountState, Auth, MockChain}; + +const COUNTER_STORAGE_KEY: Word = + Word::new([Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::ONE]); + +fn counter_storage_slot() -> anyhow::Result { + Ok(StorageSlotName::new( + "counter_account::counter_contract::count_map", + )?) +} + +#[tokio::test] +async fn counter_test() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + + let sender = builder.add_existing_wallet(Auth::BasicAuth { + auth_scheme: AuthScheme::Falcon512Poseidon2, + })?; + + let contract_package = Arc::new(build_project_in_dir( + Path::new("../contracts/counter-account"), + true, + )?); + let note_package = Arc::new(build_project_in_dir( + Path::new("../contracts/increment-note"), + true, + )?); + + 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, + )?; + + let counter_component = AccountComponent::from_package(&contract_package, &init_storage_data) + .context("failed to build account component from counter package")?; + let counter_account = builder.add_account_from_builder( + Auth::BasicAuth { + auth_scheme: AuthScheme::Falcon512Poseidon2, + }, + AccountBuilder::new([3_u8; 32]) + .account_type(AccountType::Public) + .with_component(counter_component), + AccountState::Exists, + )?; + + let mut note_rng = RandomCoin::new(Word::from( + NoteScript::from_package(note_package.as_ref()) + .context("failed to build note script from package")? + .root(), + )); + let counter_note = NoteBuilder::new(sender.id(), &mut note_rng) + .package((*note_package).clone()) + .build() + .context("failed to build counter note from package")?; + + builder.add_output_note(RawOutputNote::Full(counter_note.clone())); + let mut mock_chain = builder.build()?; + + let transaction = mock_chain + .build_transaction(counter_account.id()) + .authenticated_input_note(counter_note.id()) + .build()?; + let executed_transaction = transaction.execute().await?; + + mock_chain.add_pending_executed_transaction(&executed_transaction)?; + mock_chain.prove_next_block()?; + + let count = mock_chain + .committed_account(counter_account.id())? + .storage() + .get_map_item( + &counter_storage_slot, + StorageMapKey::new(COUNTER_STORAGE_KEY), + )?; + + assert_eq!(count[0].as_canonical_u64(), 1); + + Ok(()) +} +``` + +For a step-by-step walkthrough, see +[Test Your Contract](../../get-started/your-first-smart-contract/test). + +## Running Tests + +```bash title=">_ Terminal" +# Run all tests +cargo test -p integration -- --nocapture + +# Run the configured integration test target +cargo test -p integration --test my_test -- --nocapture + +# Run with verbose output +RUST_LOG=debug cargo test -p integration -- --nocapture +``` + +## Key Takeaways + +1. **MockChain Builder Pattern** - Use `MockChain::builder()` to set up test environments +2. **Build Contracts First** - Use `build_project_in_dir()` to compile contracts before tests +3. **Configure Storage Slots** - Match your contract's storage layout when creating accounts +4. **Read Committed State** - After proving a block, use `committed_account()` for updated state +5. **Prove Blocks** - Call `prove_next_block()` after adding executed transactions +6. **Test Failures** - Use `result.is_err()` to verify constraint violations + +## Next Steps + +- **[Debugging Guide](./debugging)** - Troubleshoot common issues +- **[Common Pitfalls](./pitfalls)** - Avoid known gotchas +- **[Miden Bank Tutorial](../miden-bank/)** - See testing in action diff --git a/versioned_docs/version-0.16/builder/tutorials/img/count_copy_fpi_diagram.png b/versioned_docs/version-0.16/builder/tutorials/img/count_copy_fpi_diagram.png new file mode 100644 index 00000000..f0fd3025 Binary files /dev/null and b/versioned_docs/version-0.16/builder/tutorials/img/count_copy_fpi_diagram.png differ diff --git a/versioned_docs/version-0.16/builder/tutorials/img/note_creation_masm.png b/versioned_docs/version-0.16/builder/tutorials/img/note_creation_masm.png new file mode 100644 index 00000000..9458433b Binary files /dev/null and b/versioned_docs/version-0.16/builder/tutorials/img/note_creation_masm.png differ diff --git a/versioned_docs/version-0.16/builder/tutorials/index.md b/versioned_docs/version-0.16/builder/tutorials/index.md new file mode 100644 index 00000000..ce50a49c --- /dev/null +++ b/versioned_docs/version-0.16/builder/tutorials/index.md @@ -0,0 +1,46 @@ +--- +title: Tutorials +sidebar_position: 0 +pagination_prev: null +--- + +# Tutorials + +Hands-on walkthroughs for building on Miden. The [miden-tutorials](https://github.com/0xMiden/miden-tutorials) repo contains Rust and TypeScript recipes and Rust contract tutorials with MockChain tests. Each guide states the environment and prerequisites needed to run its examples. + +## Pick a path + + + + A 9-part curriculum — build a complete banking application covering components, storage, note scripts, cross-component calls, and output notes. + + + Standalone how-to's for specific tasks: counter contract, create/deploy, foreign procedure invocation, React wallet, and more. + + + Run a Miden node locally or on testnet with `midenup` and the node binary. + + + Bridge assets between Miden and EVM chains (Sepolia testnet) with the Epoch protocol intent SDK. + + + +## Development helpers + + + + Test your contracts against MockChain for local simulation. + + + Interpret errors and debug common issues. + + + Avoid known issues and limitations. + + + +## Prerequisites + +- [Install the Miden toolchain](../get-started/setup/installation) with `midenup`. +- Basic familiarity with Rust (or TypeScript for the client examples). +- Understanding of the [core concepts](../smart-contracts/) — accounts, notes, transactions. diff --git a/versioned_docs/version-0.16/builder/tutorials/miden-bank/00-project-setup.md b/versioned_docs/version-0.16/builder/tutorials/miden-bank/00-project-setup.md new file mode 100644 index 00000000..58ffe4b6 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tutorials/miden-bank/00-project-setup.md @@ -0,0 +1,377 @@ +--- +sidebar_position: 0 +title: "Part 0: Project Setup" +description: "Set up a new Miden project and prepare the workspace for building the banking application." +--- + +# Part 0: Project Setup + +In this section, you'll create a new Miden project and set up the workspace structure for our banking application. By the end, you'll have a working project that compiles successfully. + +## What You'll Build in This Part + +By the end of this section, you will have: + +- Created a new Miden project using `miden new` +- Understood the workspace structure +- Renamed and configured the project for our bank +- Successfully compiled a minimal account component + +## Prerequisites + +Before starting, ensure you have completed the [Get Started installation guide](https://docs.miden.xyz/builder/get-started/setup/installation) and have: + +- **Rust toolchain** installed and configured +- **midenup toolchain** installed with Miden CLI tools (`miden` command available) + +Verify your installation: + +```bash title=">_ Terminal" +miden --version +``` + +
+Expected output + +```text +The Miden toolchain porcelain: + +Environment: +- cargo version: cargo 1.98.1 (797e8a9bc 2026-08-05). + +Midenup: +- midenup + miden version: 1.0.0. +- active toolchain version: 0.16.0. +- ... +``` + +
+ +## Step 1: Create the Project + +Create a new Miden project using the CLI: + +```bash title=">_ Terminal" +miden new miden-bank +cd miden-bank +``` + +This creates a workspace with the following structure: + +```text +miden-bank/ +├── contracts/ # Smart contract code +│ ├── counter-account/ # Example account contract (we'll replace this) +│ └── increment-note/ # Example note script (we'll replace this) +├── integration/ # Tests and deployment scripts +│ ├── src/ +│ │ ├── bin/ # Executable scripts for on-chain interactions +│ │ ├── lib.rs +│ │ └── helpers.rs # Helper functions for tests +│ └── tests/ # Test files +├── Cargo.toml # Workspace root +├── miden-toolchain.toml # Miden toolchain specification +└── rust-toolchain.toml # Rust toolchain specification +``` + +The project follows Miden's design philosophy: + +- **`contracts/`**: Your smart contract code (account components, note scripts, transaction scripts) +- **`integration/`**: All on-chain interactions, deployment scripts, and tests + +## Step 2: Set Up the Bank Account Contract + +We'll replace the example `counter-account` with our `bank-account`. First, rename the directory: + +```bash title=">_ Terminal" +mv contracts/counter-account contracts/bank-account +``` + +A contract is configured by three files: a minimal Cargo manifest, a Miden project manifest, and a Cargo build config. + +First, update the `Cargo.toml` inside `contracts/bank-account/`. Keep the generated `build.rs` and its build dependency; they prepare the package cache for Cargo and IDE builds: + +```toml title="contracts/bank-account/Cargo.toml" +[package] +name = "bank-account" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +miden = "0.14" + +[build-dependencies] +miden-sdk-build-script-support = "0.14" +``` + +Next, create `contracts/bank-account/miden-project.toml`. This is the Miden-specific project manifest that tells the compiler what kind of artifact to build and which package namespace to export: + +```toml title="contracts/bank-account/miden-project.toml" +[package] +name = "bank-account" +version = "0.1.0" + +[lib] +kind = "account-component" +path = "src/lib.rs" +namespace = "miden:bank-account/bank@0.1.0" + +[dependencies] +miden-core = "*" +miden-protocol = "*" +``` + +Finally, create `contracts/bank-account/.cargo/config.toml` so the contract always builds for the WebAssembly target with the `miden` cfg enabled (this also makes editor/LSP workflows resolve the right code): + +```toml title="contracts/bank-account/.cargo/config.toml" +[build] +target = "wasm32-wasip2" + +[target.wasm32-wasip2] +# Force-enable `cfg(miden)` for Miden-VM-targeted builds (including editor/LSP workflows). +rustflags = ["--cfg", "miden"] +``` + +### Key Configuration Options + +| Field | File | Description | +| ------------------------------------------- | -------------------- | ---------------------------------------------------- | +| `crate-type = ["cdylib"]` | `Cargo.toml` | Required for WebAssembly compilation | +| `kind = "account-component"` | `miden-project.toml` | Tells the compiler this is an account component | +| `namespace = "miden:bank-account/bank@..."` | `miden-project.toml` | The package namespace used for cross-component calls | +| `target = "wasm32-wasip2"` | `.cargo/config.toml` | Compile target for the Miden VM | + +## Step 3: Create a Minimal Bank Component + +Replace the contents of `contracts/bank-account/src/lib.rs` with a minimal bank structure: + +```rust title="contracts/bank-account/src/lib.rs" +// Do not link against libstd (i.e. anything defined in `std::`) +#![no_std] +#![feature(alloc_error_handler)] + +#[macro_use] +extern crate alloc; + +use miden::*; + +/// Storage layout for the bank account component. +/// +/// We'll build this up throughout the tutorial. The `#[component_storage]` +/// attribute marks the struct that defines the component's named storage slots. +#[component_storage] +struct BankStorage { + /// Tracks whether the bank has been initialized (deposits enabled). + /// Word layout: [is_initialized (0 or 1), 0, 0, 0] + #[storage(description = "initialized")] + initialized: StorageValue, + + /// Maps (depositor AccountId, faucet ID) -> balance (as Felt). + /// We'll use this to track user balances in Part 1. + #[storage(description = "balances")] + balances: StorageMap, +} + +/// API of the bank account component. +/// +/// The `#[component]` trait declares the methods the compiler exports as the +/// component's public WIT interface. +#[component] +trait Bank { + /// Initialize the bank account, enabling deposits. + #[account_procedure] + fn initialize(&mut self); + + /// Get the bank-tracked balance for a depositor and specific asset type. + #[account_procedure] + fn get_depositor_balance(&self, depositor: AccountId, asset: Asset) -> Felt; +} + +#[component] +impl Bank for BankStorage { + fn initialize(&mut self) { + // Check not already initialized + let current: Word = self.initialized.get(); + assert!( + current[0].as_canonical_u64() == 0, + "Bank already initialized" + ); + + // Set initialized flag to 1 + let initialized_word = Word::from([felt!(1), felt!(0), felt!(0), felt!(0)]); + self.initialized.set(initialized_word); + } + + fn get_depositor_balance(&self, depositor: AccountId, asset: Asset) -> Felt { + let key = Word::from([ + depositor.prefix, + depositor.suffix, + asset.key[3], // faucet id prefix + asset.key[2], // faucet id suffix (folds in the asset metadata byte) + ]); + self.balances.get(key) + } +} +``` + +This is our starting point with two storage slots: + +- `initialized`: A `StorageValue` slot to track whether the bank is ready +- `balances`: A `StorageMap` to track user balances (we'll use this starting in Part 1) + +:::note Component Structure +The `#[component_storage]` struct declares the storage layout, the `#[component] trait` declares the exported API, and `#[component] impl Bank for BankStorage` implements it. Any private helper methods you add later live in a separate plain `impl BankStorage` block — the `#[component]` macro only exports trait methods. + +Mark each method that notes, transaction scripts, or other account components can call with `#[account_procedure]` on the trait declaration. Unmarked methods are exported but are not part of the account procedure table. +::: + +:::note get_depositor_balance, not get_balance +The balance accessor is named `get_depositor_balance` rather than `get_balance` so it does not collide with the built-in `ActiveAccount::get_balance` vault method that the account wrapper generates. +::: + +:::info Contracts Are Excluded +Contracts are excluded from the Cargo workspace and built independently by the Miden toolchain. Each contract carries its own `miden` guest dependency plus a `miden-project.toml`. Only the `integration` crate remains a workspace member. + +Because contracts are excluded, your IDE (rust-analyzer) may not provide completions or diagnostics for contract code. This is expected — contracts are built independently using `miden build`. +::: + +## Step 4: Build and Verify + +Let's verify everything compiles correctly: + +```bash title=">_ Terminal" +cd contracts/bank-account +miden build --release +``` + +
+Expected output + +```text + Compiling bank-account v0.1.0 (/path/to/miden-bank/contracts/bank-account) + Finished `release` profile [optimized] target(s) +``` + +
+ +The compiled output is stored in `target/miden/release/bank-account.masp`. + +:::tip What's a .masp File? +A `.masp` file is a Miden Assembly Package. It contains the compiled MASM (Miden Assembly) code and metadata needed to deploy and interact with your contract. +::: + +:::info Contract dependencies +The notes and transaction script call the bank account through generated bindings. Their `miden-project.toml` files declare the bank as an automatic path dependency, so `miden build` compiles it before the dependent contract and provides its interface and procedure roots to `#[account(...)]`. +::: + +## Optional: Verify Your Setup + +:::note +This is an optional self-check. It loads the package compiled in Step 4 and creates a test account locally. +::: + +Create a new test file: + +```rust title="integration/tests/part0_setup_test.rs" +use miden_client::account::{ + component::{BasicWallet, InitStorageData, StorageValueName}, + AccountBuilder, AccountComponent, AccountType, StorageSlotName, +}; +use miden_client::{utils::Deserializable, Word}; +use miden_mast_package::Package; +use miden_standards::account::auth::NoAuth; +use std::path::Path; + +#[test] +fn test_bank_account_loads() -> anyhow::Result<()> { + // Load the bank account package compiled in Step 4. + let package_path = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../contracts/bank-account/target/miden/release/bank-account.masp"); + let bank_package = Package::read_from_bytes(&std::fs::read(package_path)?)?; + + // The `initialized` value slot has no schema default, so it must be seeded + // (with a zero Word = uninitialized) or `AccountComponent::from_package` + // errors with `InitValueNotProvided`. The `balances` map slot defaults to empty. + let initialized_slot = + StorageSlotName::new("bank_account::bank::initialized") + .expect("Valid slot name"); + + let mut init_storage_data = InitStorageData::default(); + init_storage_data.insert_value( + StorageValueName::from_slot_name(&initialized_slot), + Word::default(), + )?; + let bank_component = AccountComponent::from_package(&bank_package, &init_storage_data)?; + assert_eq!(bank_component.procedures().count(), 2); + + let bank_account = AccountBuilder::new([3u8; 32]) + .account_type(AccountType::Public) + .with_component(bank_component) + .with_component(BasicWallet) + .with_component(NoAuth) + .build_existing()?; + + // Verify the account was created + println!("Bank account created with ID: {}", bank_account.id().to_hex()); + println!("Part 0 setup verified!"); + + Ok(()) +} +``` + +Run the test from the project root: + +```bash title=">_ Terminal" +cargo test --package integration test_bank_account_loads -- --nocapture +``` + +
+Expected output + +```text + Compiling integration v0.1.0 (/path/to/miden-bank/integration) + Finished `test` profile [unoptimized + debuginfo] target(s) + Running tests/part0_setup_test.rs + +running 1 test +Bank account created with ID: 0x... +Part 0 setup verified! +test test_bank_account_loads ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored +``` + +
+ +## What We've Built So Far + +At this point, you have: + +| Component | Status | Description | +| ----------------------- | ----------- | ------------------------------------- | +| `bank-account` | Minimal | Initialization flag + balance storage | +| `deposit-note` | Not started | Coming in Part 4 | +| `withdraw-request-note` | Not started | Coming in Part 7 | +| `init-tx-script` | Not started | Coming in Part 6 | + +Your bank can be created, but doesn't do anything useful yet. In the next parts, we'll add: + +1. **Part 1**: Deeper dive into storage (Value vs StorageMap) +2. **Part 2**: Business rules and constraints +3. **Part 3**: Asset handling for deposits +4. And more... + +## Key Takeaways + +1. **`miden new`** creates a complete project workspace with contracts and integration folders +2. **Account components** are defined with a `#[component_storage]` struct plus a `#[component]` trait and impl +3. **Storage slots** are declared with `#[storage(description = "...")]` attributes +4. **`miden build`** compiles Rust to Miden Assembly (.masp package) +5. **Tests verify** that your code works before moving on + +## Next Steps + +Now that your project is set up, let's dive deeper into account components and storage in [Part 1: Account Components and Storage](./account-components). diff --git a/versioned_docs/version-0.16/builder/tutorials/miden-bank/01-account-components.md b/versioned_docs/version-0.16/builder/tutorials/miden-bank/01-account-components.md new file mode 100644 index 00000000..9198ed6d --- /dev/null +++ b/versioned_docs/version-0.16/builder/tutorials/miden-bank/01-account-components.md @@ -0,0 +1,510 @@ +--- +sidebar_position: 1 +title: "Part 1: Account Components and Storage" +description: "Learn how to define account components with the #[component] attribute and manage persistent state using Value and StorageMap storage types." +--- + +# Part 1: Account Components and Storage + +In this section, you'll learn the fundamentals of building Miden account components. We'll explore the storage types introduced in Part 0 — `StorageValue` and `StorageMap` — and add component methods. + +## What You'll Build in This Part + +By the end of this section, you will have: + +- Understood the `#[component]` attribute and what it generates +- Explored how `StorageMap` works for tracking depositor balances +- Implemented a `get_depositor_balance()` query method +- **Verified it works** with an integration test + +## Building on Part 0 + +In Part 0, we created the Bank struct with `initialized` and `balances` storage. Now we'll explore the storage types in detail and add methods: + +```text +Part 0: Part 1: +┌──────────────────────────────────┐ ┌──────────────────────────────────┐ +│ Bank │ │ Bank │ +│ ──────────────────────────────── │ ──► │ ──────────────────────────────── │ +│ initialized (StorageValue) │ │ initialized (StorageValue) │ +│ balances (StorageMap)│ │ balances (StorageMap)│ +└──────────────────────────────────┘ │ + initialize() │ + │ + get_depositor_balance() │ ◄── NEW + │ + require_initialized() │ + └──────────────────────────────────┘ +``` + +## The #[component] Attributes + +A bank component is described with three attributes that work together: + +- **`#[component_storage]`** marks the struct that declares the persistent storage fields +- **`#[component]`** on a `trait` declares the component's exported API +- **`#[component]`** on the `impl Trait for Storage` block implements that API + +When you compile with `miden build`, the macros generate: + +- **WIT (WebAssembly Interface Types)** bindings for cross-component calls +- **MASM (Miden Assembly)** code for the account logic +- **Storage slot management** code + +Only the methods declared in the `#[component]` trait are exported. Private helpers live in a separate plain `impl BankStorage` block (covered later in this tutorial). + +Let's expand our Bank component: + +## Step 1: Understand the Storage Layout + +In Part 0, we created the Bank struct with two storage fields. Let's examine what they do. The storage struct is marked with `#[component_storage]`. Here is `contracts/bank-account/src/lib.rs`: + +```rust title="contracts/bank-account/src/lib.rs" +#![no_std] +#![feature(alloc_error_handler)] + +#[macro_use] +extern crate alloc; + +use miden::*; + +use miden::Felt; + +/// Storage layout for the bank account component. +#[component_storage] +struct BankStorage { + /// Tracks whether the bank has been initialized (deposits enabled). + /// Word layout: [is_initialized (0 or 1), 0, 0, 0] + #[storage(description = "initialized")] + initialized: StorageValue, + + /// Maps (depositor AccountId, faucet ID) -> balance (as Felt). + /// Key: [depositor.prefix, depositor.suffix, faucet_prefix, faucet_suffix(+metadata)] + #[storage(description = "balances")] + balances: StorageMap, +} +``` + +The `balances` field is a `StorageMap` that tracks each depositor's balance. The compiler derives slot IDs by hashing slot names (not by field declaration order). Slot names follow the pattern `{package_name}::{component_interface}::{field_name}` — here `bank_account::bank::initialized` and `bank_account::bank::balances`. + +## Storage Types Explained + +Miden accounts have persistent storage slots. Public account storage is published on-chain; private accounts publish its commitment. A value slot holds one `Word` (4 Felts = 32 bytes), while a map slot holds the root of its key-value map. The Miden Rust compiler provides two abstractions: + +### StorageValue Storage + +The `StorageValue` type provides access to a single storage slot: + +```rust +#[storage(description = "initialized")] +initialized: StorageValue, +``` + +Use `StorageValue` when you need to store a single `Word` of data. + +**Reading and writing:** + +```rust +// Get returns a Word +let current: Word = self.initialized.get(); + +// Check the first element (our flag) +if current[0].as_canonical_u64() == 0 { + // Not initialized +} + +// Set a new value +let new_value = Word::from([felt!(1), felt!(0), felt!(0), felt!(0)]); +self.initialized.set(new_value); +``` + +:::tip Type Annotations +`StorageValue::get()` returns a `Word`. The annotation in `let current: Word = self.initialized.get();` makes that type explicit but is not required. +::: + +### StorageMap + +The `StorageMap` type provides key-value storage within a slot: + +```rust +#[storage(description = "balances")] +balances: StorageMap, +``` + +Use `StorageMap` when you need to store multiple values indexed by keys. + +**Reading and writing:** + +```rust +// Create a key (must be a Word). +// The bank keys balances per (depositor, faucet): asset.key[3] is the faucet +// id prefix and asset.key[2] is the faucet id suffix (plus a metadata byte). +let key = Word::from([ + depositor.prefix, + depositor.suffix, + asset.key[3], + asset.key[2], +]); + +// This field is StorageMap, so get returns Felt. +let balance: Felt = self.balances.get(key); + +// Set stores a Felt at this Word key. +let new_balance = Felt::new(balance.as_canonical_u64() + deposit_amount.as_canonical_u64()).unwrap(); +self.balances.set(key, new_balance); +``` + +:::info StorageMap Has a Generic API +`StorageMap::get()` returns the map's declared value type `V`, which must implement `WordValue`. Our `StorageMap` therefore returns `Felt`; the variable annotation does not change the map's value type. A map declared with `Word` values would return `Word`. +::: + +### Storage Layout + +Plan your storage layout carefully: + +| Name | Type | Purpose | +| ------------- | ------------------------ | ------------------- | +| `initialized` | `StorageValue` | Initialization flag | +| `balances` | `StorageMap` | Depositor balances | + +The `description` attribute adds human-readable metadata. The package namespace, component interface, and field name determine slot names such as `bank_account::bank::initialized`, which tests use to identify slots. The naming convention is `{package_name}::{component_interface}::{field_name}`. The compiler derives slot IDs by hashing these names, so field declaration order does not affect slot assignment. + +## Step 2: Implement Component Methods + +Now let's add methods to our Bank. The exported API is declared as a `#[component]` trait, and the `#[component]` attribute is used again on the `impl Bank for BankStorage` block that implements it: + +```rust title="contracts/bank-account/src/lib.rs" +/// API of the bank account component. +#[component] +trait Bank { + /// Initialize the bank account, enabling deposits. + #[account_procedure] + fn initialize(&mut self); + + /// Get the bank-tracked balance for a depositor and specific asset type. + #[account_procedure] + fn get_depositor_balance(&self, depositor: AccountId, asset: Asset) -> Felt; +} + +#[component] +impl Bank for BankStorage { + fn initialize(&mut self) { + // Check not already initialized + let current: Word = self.initialized.get(); + assert!( + current[0].as_canonical_u64() == 0, + "Bank already initialized" + ); + + // Set initialized flag to 1 + let initialized_word = Word::from([felt!(1), felt!(0), felt!(0), felt!(0)]); + self.initialized.set(initialized_word); + } + + fn get_depositor_balance(&self, depositor: AccountId, asset: Asset) -> Felt { + self.balances.get(BankStorage::balance_key(depositor, &asset)) + } +} +``` + +Note the method is named `get_depositor_balance`, not `get_balance`: the account wrapper generates a built-in `ActiveAccount::get_balance` vault method, so reusing that name would collide with it. + +The private helpers (`balance_key`, `require_initialized`, …) are not part of the exported API, so they live in a separate plain `impl BankStorage` block — the `#[component]` macro only exports the trait methods: + +```rust title="contracts/bank-account/src/lib.rs" +/// Internal helpers that are not part of the component's exported WIT API. +impl BankStorage { + /// Derive the `balances` map key identifying a (depositor, faucet) pair: + /// `[depositor.prefix, depositor.suffix, faucet_prefix, faucet_suffix(+metadata)]`. + fn balance_key(depositor: AccountId, asset: &Asset) -> Word { + Word::from([ + depositor.prefix, + depositor.suffix, + asset.key[3], + asset.key[2], + ]) + } + + /// Check that the bank is initialized. + fn require_initialized(&self) { + let current: Word = self.initialized.get(); + assert!( + current[0].as_canonical_u64() == 1, + "Bank not initialized - deposits not enabled" + ); + } +} +``` + +:::info v0.16 fungible-asset ID layout +A fungible asset's ID Word is `[asset_class_suffix, asset_class_prefix, faucet_suffix | metadata_byte, faucet_prefix]`. For fungible assets, the asset class is empty. `asset.key[3]` is the faucet ID prefix and `asset.key[2]` is the faucet ID suffix with the composition bits in its low byte, so `key[2]` is **not** the raw faucet suffix. The callback flag is now encoded in the faucet account ID, not in the asset metadata byte. The host-side mirror is `FungibleAsset::to_id_word()` indices `[3]`/`[2]`. +::: + +The bank requires initialization before accepting deposits: `require_initialized()` is called at the top of `deposit()` and `withdraw()` (covered in later parts). + +### Exported vs Internal Methods + +- **Trait methods** (declared in the `#[component] trait`) are exposed in the generated WIT interface and can be called by other contracts +- **Inherent helpers** (in the plain `impl BankStorage`) are internal and cannot be called from the outside + +```rust +// Exported: Can be called by note scripts and other contracts +fn get_depositor_balance(&self, depositor: AccountId, asset: Asset) -> Felt { ... } + +// Internal helper, not exposed +fn require_initialized(&self) { ... } +``` + +## Step 3: Build the Component + +Build your updated account component: + +```bash title=">_ Terminal" +cd contracts/bank-account +miden build +``` + +This compiles the Rust code to Miden Assembly and generates: + +- `target/miden/dev/bank-account.masp` - The compiled package +- The package embeds the WIT interface used by dependent contracts + +## Optional: Verify Your Code + +:::note +This is an optional self-check. If you create this test file, you can run it to verify your component. The main runnable tests begin in Part 4. +::: + +This test will: + +1. Create a bank account +2. Initialize it +3. Verify the storage was updated + +Create a new test file: + +```rust title="integration/tests/part1_account_test.rs" +use integration::helpers::{ + build_project_in_dir, create_testing_account_from_package, AccountCreationConfig, +}; +use miden_client::account::{component::{InitStorageData, StorageValueName}, StorageSlotName}; +use miden_client::{Felt, Word}; +use std::{path::Path, sync::Arc}; + +#[tokio::test] +async fn test_bank_account_storage() -> anyhow::Result<()> { + // ========================================================================= + // SETUP: Build contracts and create the bank account + // ========================================================================= + + // Build the bank account contract + let bank_package = Arc::new(build_project_in_dir( + Path::new("../contracts/bank-account"), + true, + )?); + + // Create named storage slots matching the contract's storage layout + // The naming convention is: {package_name}::{component_interface}::{field_name} + let initialized_slot = + StorageSlotName::new("bank_account::bank::initialized") + .expect("Valid slot name"); + let balances_slot = + StorageSlotName::new("bank_account::bank::balances") + .expect("Valid slot name"); + + // The `initialized` value slot has no schema default, so it MUST be seeded + // here — otherwise AccountComponent::from_package fails with InitValueNotProvided. + // Only the `balances` map defaults to empty. + let mut init_storage_data = InitStorageData::default(); + init_storage_data.insert_value( + StorageValueName::from_slot_name(&initialized_slot), + Word::default(), + )?; + let bank_cfg = AccountCreationConfig { + init_storage_data, + ..Default::default() + }; + + let bank_account = + create_testing_account_from_package(bank_package.clone(), bank_cfg)?; + + // ========================================================================= + // VERIFY: Check initial storage state + // ========================================================================= + + // Verify initialized flag starts as 0 + let initialized_value = bank_account.storage().get_item(&initialized_slot)?; + assert_eq!( + initialized_value, + Word::default(), + "Initialized flag should start as 0" + ); + + println!("Bank account created successfully!"); + println!(" Account ID: {:?}", bank_account.id()); + println!(" Initialized flag: {:?}", initialized_value[0].as_canonical_u64()); + + // ========================================================================= + // VERIFY: Storage slots are correctly configured + // ========================================================================= + + // Check that we can query the balances map (should return 0 for any key) + let test_key = Word::from([Felt::from(1u32), Felt::from(2u32), Felt::from(0u32), Felt::from(0u32)]); + let balance = bank_account.storage().get_map_item( + &balances_slot, + miden_client::account::StorageMapKey::new(test_key), + )?; + + // Balance for non-existent depositor should be all zeros + assert_eq!( + balance, + Word::default(), + "Balance for unknown depositor should be zero" + ); + + println!(" Balances map accessible: Yes"); + println!("\nPart 1 test passed!"); + + Ok(()) +} +``` + +Run the test from the project root: + +```bash title=">_ Terminal" +cargo test --package integration test_bank_account_storage -- --nocapture +``` + +
+Expected output + +```text + Compiling integration v0.1.0 (/path/to/miden-bank/integration) + Finished `test` profile [unoptimized + debuginfo] target(s) + Running tests/part1_account_test.rs + +running 1 test +Bank account created successfully! + Account ID: 0x... + Initialized flag: 0 + Balances map accessible: Yes + +Part 1 test passed! +test test_bank_account_storage ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored +``` + +
+ +:::tip Troubleshooting +**"cannot find function `build_project_in_dir`"**: Make sure your `integration/src/helpers.rs` exports this function and `integration/src/lib.rs` has `pub mod helpers;`. + +**"StorageSlot not found"**: Ensure you're using the correct imports: `use miden_client::account::{StorageSlot, StorageSlotName};` +::: + +## Complete Code for This Part + +Here's the full `lib.rs` after Part 1: + +
+Click to expand full code + +```rust title="contracts/bank-account/src/lib.rs" +#![no_std] +#![feature(alloc_error_handler)] + +#[macro_use] +extern crate alloc; + +use miden::*; + +use miden::Felt; + +/// Storage layout for the bank account component. +#[component_storage] +struct BankStorage { + /// Tracks whether the bank has been initialized (deposits enabled). + /// Word layout: [is_initialized (0 or 1), 0, 0, 0] + #[storage(description = "initialized")] + initialized: StorageValue, + + /// Maps (depositor AccountId, faucet ID) -> balance (as Felt). + /// Key: [depositor.prefix, depositor.suffix, faucet_prefix, faucet_suffix(+metadata)] + #[storage(description = "balances")] + balances: StorageMap, +} + +/// API of the bank account component. +#[component] +trait Bank { + /// Initialize the bank account, enabling deposits. + #[account_procedure] + fn initialize(&mut self); + + /// Get the bank-tracked balance for a depositor and specific asset type. + #[account_procedure] + fn get_depositor_balance(&self, depositor: AccountId, asset: Asset) -> Felt; +} + +#[component] +impl Bank for BankStorage { + fn initialize(&mut self) { + // Check not already initialized + let current: Word = self.initialized.get(); + assert!( + current[0].as_canonical_u64() == 0, + "Bank already initialized" + ); + + // Set initialized flag to 1 + let initialized_word = Word::from([felt!(1), felt!(0), felt!(0), felt!(0)]); + self.initialized.set(initialized_word); + } + + fn get_depositor_balance(&self, depositor: AccountId, asset: Asset) -> Felt { + self.balances.get(BankStorage::balance_key(depositor, &asset)) + } +} + +/// Internal helpers that are not part of the component's exported WIT API. +/// +/// The `#[component]` macro exports only the methods of the `Bank` trait, so these +/// inherent methods stay private to the contract. +impl BankStorage { + /// Derive the `balances` map key identifying a (depositor, faucet) pair: + /// `[depositor.prefix, depositor.suffix, faucet_prefix, faucet_suffix(+metadata)]`. + fn balance_key(depositor: AccountId, asset: &Asset) -> Word { + Word::from([ + depositor.prefix, + depositor.suffix, + asset.key[3], + asset.key[2], + ]) + } + + /// Check that the bank is initialized. + fn require_initialized(&self) { + let current: Word = self.initialized.get(); + assert!( + current[0].as_canonical_u64() == 1, + "Bank not initialized - deposits not enabled" + ); + } +} +``` + +
+ +## Key Takeaways + +1. **`#[component]`** marks the exported component trait and its implementation; `#[component_storage]` marks the storage struct +2. **`StorageValue`** stores a single Word, read with `.get()`, write with `.set()` +3. **`StorageMap`** stores key-value pairs, access with `.get()` and `.set()` +4. **Storage slots** are identified by name (IDs derived from hashed slot names), each holds 4 Felts (32 bytes) +5. **Trait methods** (declared in the `#[component] trait`) are callable by other contracts via generated bindings; private helpers live in a plain `impl` block + +:::tip View Complete Source +See the complete bank account implementation in [contracts/bank-account/src/lib.rs](https://github.com/0xMiden/miden-tutorials/blob/main/examples/miden-bank/contracts/bank-account/src/lib.rs). +::: + +## Next Steps + +Now that you understand account components and storage, let's learn how to define business rules with [Part 2: Constants and Constraints](./constants-constraints). diff --git a/versioned_docs/version-0.16/builder/tutorials/miden-bank/02-constants-constraints.md b/versioned_docs/version-0.16/builder/tutorials/miden-bank/02-constants-constraints.md new file mode 100644 index 00000000..61db85c5 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tutorials/miden-bank/02-constants-constraints.md @@ -0,0 +1,530 @@ +--- +sidebar_position: 2 +title: "Part 2: Constants and Constraints" +description: "Learn how to define constants for business rules and use assertions to validate transactions in Miden Rust contracts." +--- + +# Part 2: Constants and Constraints + +In this section, you'll learn how to define business rules using constants and enforce them with assertions. We'll implement deposit limits and see how failed constraints cause transactions to be rejected. + +## What You'll Build in This Part + +By the end of this section, you will have: + +- Defined constants for business rules (`MAX_DEPOSIT_AMOUNT`, `MAX_BALANCE`) +- Used `assert!()` for transaction validation +- Compared token amounts with `u64` business limits using `.as_canonical_u64()` +- Added a deposit method skeleton with validation +- **Verified the component builds and loads** with its initial storage; later parts exercise the transaction guards + +## Building on Part 1 + +In Part 1, we set up the Bank's storage structure. Now we'll add business rules: + +```text +Part 1: Part 2: +┌─────────────────────────┐ ┌─────────────────────────┐ +│ Bank │ │ Bank │ +│ ────────────────────────│ ──► │ ────────────────────────│ +│ + initialize() │ │ + initialize() │ +│ + get_depositor_balance()│ │ + get_depositor_balance()│ +│ │ │ + deposit() │ ◄── NEW (skeleton) +│ │ │ + MAX_DEPOSIT_AMOUNT │ ◄── NEW constant +│ │ │ + MAX_BALANCE │ ◄── NEW constant +└─────────────────────────┘ └─────────────────────────┘ +``` + +## Defining Constants + +Constants in Miden Rust contracts work just like regular Rust constants: + +```rust title="contracts/bank-account/src/lib.rs" +/// Maximum allowed deposit amount per transaction. +/// +/// This limit provides a safety constraint for the banking system. +/// +/// Value: 1,000,000 tokens (arbitrary limit for demonstration) +const MAX_DEPOSIT_AMOUNT: u64 = 1_000_000; + +/// Maximum allowed balance per depositor per asset. +/// +/// This matches `FungibleAsset::MAX_AMOUNT` (2^63 - 2^31) from the Miden protocol. +/// Felt arithmetic is modular (wraps at the Goldilocks prime), so without this guard +/// a cumulative balance could silently wrap around to zero. Validating the u64 result +/// of the addition against this bound prevents that overflow. +const MAX_BALANCE: u64 = 9_223_372_034_707_292_160; // 2^63 - 2^31 +``` + +Use constants for: + +- Business rule limits (max amounts, timeouts) +- Magic numbers that need documentation +- Values used in multiple places + +:::info Constants vs Storage +Constants are compiled into the contract code and cannot change. Use storage slots for values that need to be modified at runtime. +::: + +## The assert!() Macro + +The `assert!()` macro validates conditions during transaction execution: + +```rust title="contracts/bank-account/src/lib.rs" +fn initialize(&mut self) { + // Check not already initialized + let current: Word = self.initialized.get(); + assert!( + current[0].as_canonical_u64() == 0, + "Bank already initialized" + ); + + // Set initialized flag to 1 + let initialized_word = Word::from([felt!(1), felt!(0), felt!(0), felt!(0)]); + self.initialized.set(initialized_word); +} +``` + +When an assertion fails: + +1. The Miden VM execution halts +2. No valid proof can be generated +3. The transaction is rejected + +This is the primary mechanism for enforcing business rules in Miden contracts. + +## Comparing Felt Amounts with Business Limits + +:::note Comparison and Arithmetic +The current SDK's `<`, `>`, `<=`, and `>=` operators compare the canonical integer values of `Felt`s. Direct comparisons are supported. Arithmetic on `Felt` is modular, however, so validate amounts before addition or subtraction can wrap around the field modulus. +::: + +**Comparing two field elements:** + +```rust +// Direct Felt comparison uses canonical integer ordering. +if deposit_amount > felt!(1_000_000) { + // The amount exceeds the limit. +} +``` + +**Comparing with our `u64` constant:** + +```rust +// Convert the amount to compare it with the u64 business limit. +if deposit_amount.as_canonical_u64() > MAX_DEPOSIT_AMOUNT { + // The amount exceeds the limit. +} +``` + +Both comparisons have the same result. This tutorial uses `.as_canonical_u64()` to express quantity checks against `u64` limits explicitly. Converting a value after field arithmetic has wrapped does not recover its original quantity. + +## Step 1: Add the Constant and Deposit Method + +Update your `contracts/bank-account/src/lib.rs` to add the constants and a deposit method skeleton. Keep the storage struct from Part 1 and replace the `Bank` trait and implementation blocks with the code below. Declare `deposit` in the trait with `#[account_procedure]` before implementing it in `#[component] impl Bank for BankStorage`. Private helpers like `require_initialized` and `balance_key` remain in a separate plain `impl BankStorage` block: + +```rust title="contracts/bank-account/src/lib.rs" +const MAX_DEPOSIT_AMOUNT: u64 = 1_000_000; +const MAX_BALANCE: u64 = 9_223_372_034_707_292_160; // 2^63 - 2^31 + +#[component] +trait Bank { + /// Initialize the bank account, enabling deposits. + #[account_procedure] + fn initialize(&mut self); + + /// Get the bank-tracked balance for a depositor and specific asset type. + #[account_procedure] + fn get_depositor_balance(&self, depositor: AccountId, asset: Asset) -> Felt; + + /// Deposit an asset into the bank for a specific depositor. + #[account_procedure] + fn deposit(&mut self, depositor: AccountId, deposit_asset: Asset); +} + +#[component] +impl Bank for BankStorage { + fn initialize(&mut self) { + let current: Word = self.initialized.get(); + assert!( + current[0].as_canonical_u64() == 0, + "Bank already initialized" + ); + + let initialized_word = Word::from([felt!(1), felt!(0), felt!(0), felt!(0)]); + self.initialized.set(initialized_word); + } + + /// Get the bank-tracked balance for a depositor and specific asset type. + fn get_depositor_balance(&self, depositor: AccountId, asset: Asset) -> Felt { + self.balances.get(BankStorage::balance_key(depositor, &asset)) + } + + /// Deposit assets into the bank. + /// For now, this just validates constraints - we'll add asset handling in Part 3. + fn deposit(&mut self, depositor: AccountId, deposit_asset: Asset) { + // NOTE: Initialization guard — enabled in Part 6 (Transaction Scripts) + // self.require_initialized(); + + // Extract the fungible amount from the asset + let deposit_amount = deposit_asset.value[0]; + + // ======================================================================== + // CONSTRAINT: Maximum deposit amount check + // ======================================================================== + assert!( + deposit_amount.as_canonical_u64() <= MAX_DEPOSIT_AMOUNT, + "Deposit amount exceeds maximum allowed" + ); + + // We'll add balance tracking and asset handling in Part 3 + // For now, just validate the constraints + } +} + +/// Internal helpers that are not part of the component's exported WIT API. +impl BankStorage { + /// Derive the `balances` map key identifying a (depositor, faucet) pair: + /// `[depositor.prefix, depositor.suffix, faucet_prefix, faucet_suffix(+metadata)]`. + fn balance_key(depositor: AccountId, asset: &Asset) -> Word { + Word::from([ + depositor.prefix, + depositor.suffix, + asset.key[3], // faucet id prefix + asset.key[2], // faucet id suffix folded with the asset metadata byte + ]) + } + + /// Check that the bank is initialized. + fn require_initialized(&self) { + let current: Word = self.initialized.get(); + assert!( + current[0].as_canonical_u64() == 1, + "Bank not initialized - deposits not enabled" + ); + } +} +``` + +:::warning v0.16 asset-ID layout +In v0.16 the fungible-asset ID Word is `[asset_class_suffix, asset_class_prefix, faucet_suffix | metadata_byte, faucet_prefix]`. The fungible asset class is empty, and the metadata byte contains the composition bits; the callback flag is part of the faucet account ID. Thus `asset.key[2]` is **not** the raw faucet suffix. The host/test side derives the same ID from `FungibleAsset::new(faucet.id(), amt)?.to_id_word()` indices `[3]`/`[2]` (not `faucet.id().prefix()/suffix()`). +::: + +### The require_initialized() Guard + +This helper is defined in the private `impl BankStorage` block and intentionally **commented out** in the deposit method until Part 6. When enabled, it will check initialization state: + +```rust +fn require_initialized(&self) { + let current: Word = self.initialized.get(); + assert!( + current[0].as_canonical_u64() == 1, + "Bank not initialized - deposits not enabled" + ); +} +``` + +This pattern: + +- Centralizes the initialization check +- Provides a clear error message +- Can be reused across multiple methods + +## How Assertions Affect Proving + +When an assertion fails in the Miden VM: + +```text +Transaction Execution Flow: +┌─────────────────────┐ +│ User submits TX │ +└──────────┬──────────┘ + ▼ +┌─────────────────────┐ +│ VM executes code │ +└──────────┬──────────┘ + ▼ + ┌──────┴──────┐ + │ Assertion? │ + └──────┬──────┘ + Pass │ Fail + ┌──────┴──────┐ + ▼ ▼ +┌────────┐ ┌────────────┐ +│ Prove │ │ TX Rejected│ +│ Success│ │ No Proof │ +└────────┘ └────────────┘ +``` + +Key points: + +- Failed assertions prevent proof generation +- No state changes occur if the transaction fails +- Error messages help with debugging + +## Step 2: Build and Verify + +Build the updated contract: + +```bash title=">_ Terminal" +cd contracts/bank-account +miden build +``` + +## Optional: Verify Constraints Work + +:::note +This is an optional self-check. If you create this test file, you can run it to verify the contract compiles with the constraint logic. The main runnable tests begin in Part 4. +::: + +```rust title="integration/tests/part2_constraints_test.rs" +use integration::helpers::{ + build_project_in_dir, create_testing_account_from_package, AccountCreationConfig, +}; +use miden_client::account::{ + component::{InitStorageData, StorageValueName}, + StorageSlotName, +}; +use miden_client::Word; +use std::{path::Path, sync::Arc}; + +/// Test that our constraint logic is set up correctly +#[tokio::test] +async fn test_constraints_are_defined() -> anyhow::Result<()> { + // Build the bank account contract to verify it compiles with constraints + let bank_package = Arc::new(build_project_in_dir( + Path::new("../contracts/bank-account"), + true, + )?); + + // The `initialized` value slot has no schema default, so + // `AccountComponent::from_package` requires it to be seeded (with a zero Word = + // uninitialized) or it errors with `InitValueNotProvided`. Only the `balances` + // map slot defaults to empty. + let initialized_slot = + StorageSlotName::new("bank_account::bank::initialized") + .expect("Valid slot name"); + + // Create an uninitialized bank account + let mut init_storage_data = InitStorageData::default(); + init_storage_data.insert_value( + StorageValueName::from_slot_name(&initialized_slot), + Word::default(), + )?; + let bank_cfg = AccountCreationConfig { + init_storage_data, + ..Default::default() + }; + + let bank_account = + create_testing_account_from_package(bank_package.clone(), bank_cfg)?; + + // Verify the bank starts uninitialized + let initialized = bank_account.storage().get_item(&initialized_slot)?; + assert_eq!( + initialized[0].as_canonical_u64(), + 0, + "Bank should start uninitialized" + ); + + println!("Bank account created with constraints!"); + println!(" - MAX_DEPOSIT_AMOUNT: 1,000,000"); + println!(" - require_initialized() defined (enabled in Part 6)"); + println!(" - Initialization status: {}", initialized[0].as_canonical_u64()); + println!("\nPart 2 constraints test passed!"); + + Ok(()) +} +``` + +Run the test from the project root: + +```bash title=">_ Terminal" +cargo test --package integration test_constraints_are_defined -- --nocapture +``` + +
+Expected output + +```text + Compiling integration v0.1.0 (/path/to/miden-bank/integration) + Finished `test` profile [unoptimized + debuginfo] target(s) + Running tests/part2_constraints_test.rs + +running 1 test +Bank account created with constraints! + - MAX_DEPOSIT_AMOUNT: 1,000,000 + - require_initialized() defined (enabled in Part 6) + - Initialization status: 0 + +Part 2 constraints test passed! +test test_constraints_are_defined ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored +``` + +
+ +:::tip What's Next +In Part 4, we'll write a real deposit-flow test. At that stage, the deposit works without initialization because the guard is still commented out. In Part 6 (Transaction Scripts), we'll enable the initialization guard and verify it works with a dedicated test. +::: + +## Common Constraint Patterns + +### Balance Checks (Preview for Part 3) + +```rust +fn require_sufficient_balance(&self, depositor: AccountId, asset: Asset, amount: Felt) { + let balance = self.get_depositor_balance(depositor, asset); + assert!( + balance.as_canonical_u64() >= amount.as_canonical_u64(), + "Insufficient balance" + ); +} +``` + +:::danger Critical: Always Validate Before Subtraction +This pattern is **mandatory** for any operation that subtracts from a balance. Miden uses field element (Felt) arithmetic, which is modular. Without this check, subtracting more than the balance would NOT cause an error - instead, the value would silently wrap around to a large positive number, effectively allowing unlimited withdrawals. See [Common Pitfalls](https://docs.miden.xyz/builder/tutorials/rust-compiler/pitfalls#felt-arithmetic-underflowoverflow) for more details. +::: + +### State Checks + +This optional extension assumes a `paused: StorageValue` slot has been added to the component and initialized to a zero Word. The Bank component in this tutorial does not include that slot. + +```rust +fn require_not_paused(&self) { + let paused: Word = self.paused.get(); + assert!( + paused[0].as_canonical_u64() == 0, + "Contract is paused" + ); +} +``` + +## Complete Code for This Part + +Here's the full `lib.rs` after Part 2: + +
+Click to expand full code + +```rust title="contracts/bank-account/src/lib.rs" +#![no_std] +#![feature(alloc_error_handler)] + +#[macro_use] +extern crate alloc; + +use miden::*; + +use miden::Felt; + +/// Maximum allowed deposit amount per transaction. +const MAX_DEPOSIT_AMOUNT: u64 = 1_000_000; + +/// Maximum allowed balance per depositor per asset. +const MAX_BALANCE: u64 = 9_223_372_034_707_292_160; // 2^63 - 2^31 + +/// Storage layout for the bank account component. +#[component_storage] +struct BankStorage { + #[storage(description = "initialized")] + initialized: StorageValue, + + #[storage(description = "balances")] + balances: StorageMap, +} + +/// API of the bank account component. +#[component] +trait Bank { + /// Initialize the bank account, enabling deposits. + #[account_procedure] + fn initialize(&mut self); + + /// Get the bank-tracked balance for a depositor and specific asset type. + #[account_procedure] + fn get_depositor_balance(&self, depositor: AccountId, asset: Asset) -> Felt; + + /// Deposit an asset into the bank for a specific depositor. + #[account_procedure] + fn deposit(&mut self, depositor: AccountId, deposit_asset: Asset); +} + +#[component] +impl Bank for BankStorage { + fn initialize(&mut self) { + let current: Word = self.initialized.get(); + assert!( + current[0].as_canonical_u64() == 0, + "Bank already initialized" + ); + + let initialized_word = Word::from([felt!(1), felt!(0), felt!(0), felt!(0)]); + self.initialized.set(initialized_word); + } + + fn get_depositor_balance(&self, depositor: AccountId, asset: Asset) -> Felt { + self.balances.get(BankStorage::balance_key(depositor, &asset)) + } + + fn deposit(&mut self, depositor: AccountId, deposit_asset: Asset) { + // NOTE: Initialization guard — enabled in Part 6 (Transaction Scripts) + // self.require_initialized(); + + let deposit_amount = deposit_asset.value[0]; + + // CONSTRAINT: Maximum deposit amount check + assert!( + deposit_amount.as_canonical_u64() <= MAX_DEPOSIT_AMOUNT, + "Deposit amount exceeds maximum allowed" + ); + + // Balance tracking and asset handling added in Part 3 + } +} + +/// Internal helpers that are not part of the component's exported WIT API. +/// +/// The `#[component]` macro exports only the methods of the `Bank` trait, so these +/// inherent methods stay private to the contract. +impl BankStorage { + /// Derive the `balances` map key identifying a (depositor, faucet) pair: + /// `[depositor.prefix, depositor.suffix, faucet_prefix, faucet_suffix(+metadata)]`. + fn balance_key(depositor: AccountId, asset: &Asset) -> Word { + Word::from([ + depositor.prefix, + depositor.suffix, + asset.key[3], + asset.key[2], + ]) + } + + /// Check that the bank is initialized. + fn require_initialized(&self) { + let current: Word = self.initialized.get(); + assert!( + current[0].as_canonical_u64() == 1, + "Bank not initialized - deposits not enabled" + ); + } +} +``` + +
+ +## Key Takeaways + +1. **Constants** define immutable business rules at compile time +2. **`assert!()`** enforces constraints - failures reject the transaction +3. **Use `.as_canonical_u64()`** to compare amounts with `u64` business limits; validate quantities before field arithmetic can wrap +4. **Helper methods** like `require_initialized()` centralize validation logic +5. **Failed assertions** mean no valid proof can be generated + +:::tip View Complete Source +See the complete constraint implementation in [contracts/bank-account/src/lib.rs](https://github.com/0xMiden/miden-tutorials/blob/main/examples/miden-bank/contracts/bank-account/src/lib.rs). +::: + +## Next Steps + +Now that you can define and enforce business rules, let's learn how to handle assets in [Part 3: Asset Management](./asset-management). diff --git a/versioned_docs/version-0.16/builder/tutorials/miden-bank/03-asset-management.md b/versioned_docs/version-0.16/builder/tutorials/miden-bank/03-asset-management.md new file mode 100644 index 00000000..f45c156c --- /dev/null +++ b/versioned_docs/version-0.16/builder/tutorials/miden-bank/03-asset-management.md @@ -0,0 +1,757 @@ +--- +sidebar_position: 3 +title: "Part 3: Asset Management" +description: "Learn how to handle fungible assets in Miden Rust contracts using vault operations and balance tracking." +--- + +# Part 3: Asset Management + +In this section, you'll learn how to receive and send assets in Miden accounts. We'll complete the deposit logic that receives tokens into the bank's vault and tracks balances per depositor. + +## What You'll Build in This Part + +By the end of this section, you will have: + +- Understood the `Asset` type structure for fungible assets +- Implemented full deposit logic with `native_account::add_asset()` +- Learned about balance key design for per-user, per-asset tracking +- Added a withdraw method skeleton (to be completed in Part 7) +- **Verified deposits work** with a MockChain test + +## Building on Part 2 + +In Part 2, we added constraints. Now we'll complete the deposit function with actual asset handling: + +```text +Part 2: Part 3: +┌──────────────────┐ ┌──────────────────┐ +│ Bank │ │ Bank │ +│ ─────────────────│ ──► │ ─────────────────│ +│ + deposit() │ │ + deposit() │ ◄── COMPLETE +│ (skeleton) │ │ + balance tracking +│ │ │ + vault operations +│ │ │ + withdraw() │ ◄── NEW (skeleton) +└──────────────────┘ └──────────────────┘ +``` + +## The Asset Type + +Miden splits a fungible `Asset` into a `value` word and a `key` word. The `value` +holds the amount; the `key` is the vault key word. In protocol v0.16 the fungible +vault key word has this layout: + +```text +Asset value: [amount, 0, 0, 0] +Asset key: [asset_class_suffix, asset_class_prefix, faucet_suffix | metadata, faucet_prefix] + ━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━ + key index 2 key index 3 +``` + +| Word | Index | Field | Description | +| ------- | ----- | --------------------------- | ---------------------------------------------------------------- | +| `value` | 0 | `amount` | The quantity of tokens | +| `value` | 1 | (reserved) | Always 0 for fungible assets | +| `key` | 2 | `faucet_suffix \| metadata` | Faucet ID suffix with a metadata byte folded into the low 8 bits | +| `key` | 3 | `faucet_prefix` | First part of the faucet account ID | + +Access the amount through `asset.value` and the faucet ID through `asset.key`: + +```rust +let amount = deposit_asset.value[0]; // The token amount +let faucet_suffix = deposit_asset.key[2]; // Faucet ID suffix (+ metadata byte) +let faucet_prefix = deposit_asset.key[3]; // Faucet ID prefix +``` + +:::note v0.16 asset-ID layout +`asset.key[2]` is **not** the raw faucet suffix — the composition metadata is +folded into its low byte. The callback flag is encoded in the faucet account ID +in v0.16. Thus `(asset.key[3], asset.key[2])` is a stable per-faucet identifier. +The host-side mirror is `FungibleAsset::to_id_word()` indices `[3]` / `[2]`. +::: + +## Receiving Assets with add_asset() + +The `native_account::add_asset()` function adds an asset to the account's vault: + +```rust +// Add asset to the bank's vault +native_account::add_asset(deposit_asset); +``` + +When called: + +- The asset is added to the account's internal vault +- The vault tracks all assets the account holds +- Multiple assets of the same type are combined automatically + +:::info Vault vs Balance Tracking +The vault is managed by the Miden protocol automatically. Our `StorageMap` for balances is an **application-level** tracking of who deposited what, separate from the protocol-level vault. +::: + +## Step 1: Complete the Deposit Function + +Update `contracts/bank-account/src/lib.rs` to complete the deposit function with balance tracking and vault operations: + +```rust title="contracts/bank-account/src/lib.rs" +fn deposit(&mut self, depositor: AccountId, deposit_asset: Asset) { + // NOTE: Initialization guard — enabled in Part 6 (Transaction Scripts) + // self.require_initialized(); + + // Verify the asset composition identifies a fungible asset. + // Zero padding in the value word alone cannot distinguish NFTs. + assert!( + deposit_asset.is_fungible(), + "Only fungible assets are supported" + ); + + // Extract the fungible amount from the asset value word + let deposit_amount = deposit_asset.value[0]; + + // Validate deposit amount does not exceed maximum + assert!( + deposit_amount.as_canonical_u64() <= MAX_DEPOSIT_AMOUNT, + "Deposit amount exceeds maximum allowed" + ); + + // Derive the balance-map key from the depositor and the asset's faucet. + let key = Word::from([ + depositor.prefix, + depositor.suffix, + deposit_asset.key[3], // faucet_prefix + deposit_asset.key[2], // faucet_suffix (+ metadata byte; see `balances` field docs) + ]); + + // Update balance in integer space to avoid modular Felt wraparound. + // Felt arithmetic is modular (wraps at the Goldilocks prime), so we + // validate entirely in u64 before storing the result as a Felt. + let current_balance: Felt = self.balances.get(key); + let current_u64 = current_balance.as_canonical_u64(); + let deposit_u64 = deposit_amount.as_canonical_u64(); + + let new_balance_u64 = current_u64 + .checked_add(deposit_u64) + .expect("Balance overflow: addition exceeds u64 range"); + assert!( + new_balance_u64 <= MAX_BALANCE, + "Balance would exceed maximum allowed" + ); + + // Guest-side `Felt::new` is fallible, so unwrap the validated value. + self.balances.set(key, Felt::new(new_balance_u64).unwrap()); + + // Add asset to the bank's vault + native_account::add_asset(deposit_asset); +} +``` + +### Balance Key Design + +The key is derived inline at each call site (in `deposit`, `withdraw`, and +`get_depositor_balance`) by packing the depositor and the asset's faucet into a +composite `Word`: + +```rust +let key = Word::from([ + depositor.prefix, // Who deposited + depositor.suffix, + deposit_asset.key[3], // Which asset type (faucet ID prefix) + deposit_asset.key[2], // Which asset type (faucet ID suffix + metadata byte) +]); +``` + +This design allows: + +- **Per-depositor tracking**: Each user has their own balance +- **Per-asset tracking**: Different token types are tracked separately +- **Unique keys**: The combination ensures no collisions + +Because `asset.key[2]` carries the v0.16 composition metadata in its low byte (not the +raw faucet suffix), the host side must derive the _same_ ID from +`FungibleAsset::to_id_word()` rather than from `faucet.id().suffix()` directly — the +test below shows this. + +The remaining internal helpers live in a separate, plain `impl BankStorage` block (not +the `#[component]` trait impl), because the `#[component]` macro exports only the trait +methods as the contract's WIT API; inherent methods like `require_initialized` and +`create_p2id_note` stay private to the contract. + +## Step 2: Add the Withdraw Method Skeleton + +Now add a withdraw method skeleton. We'll complete it in Part 7 when we cover output notes. + +:::danger Critical Security Warning: Felt Arithmetic Underflow + +Miden uses **modular field arithmetic**. Subtracting a larger value from a smaller one does **NOT** cause an error - it **silently wraps** to a massive positive number! + +For example: `50 - 100` does NOT equal `-50`. Instead, it equals a number close to `2^64`. + +**You MUST validate before ANY subtraction:** + +```rust +// WRONG - DANGEROUS! Silent underflow if balance < amount +let new_balance = current_balance - withdraw_amount; + +// CORRECT - Always validate first +assert!( + current_balance.as_canonical_u64() >= withdraw_amount.as_canonical_u64(), + "Withdrawal amount exceeds available balance" +); +let new_balance = current_balance - withdraw_amount; +``` + +This is not optional - it's a **security requirement** for any financial operation. +::: + +Add this declaration inside your existing `Bank` trait: + +```rust title="contracts/bank-account/src/lib.rs" +/// Withdraw assets back to the depositor. +#[account_procedure] +fn withdraw(&mut self, withdraw_asset: Asset, serial_num: Word, tag: Felt, note_type: Felt); +``` + +Then add this method inside your existing `impl Bank for BankStorage` block: + +```rust title="contracts/bank-account/src/lib.rs" +fn withdraw( + &mut self, + withdraw_asset: Asset, + serial_num: Word, + tag: Felt, + note_type: Felt, +) { + // NOTE: Initialization guard — enabled in Part 6 (Transaction Scripts) + // self.require_initialized(); + + // Identify the depositor from the note's sender — this is cryptographically + // bound to the note metadata, so it cannot be spoofed by a malicious caller. + let depositor = active_note::get_sender(); + + // Verify this is a fungible asset — see `deposit()` for the rationale. + assert!( + withdraw_asset.is_fungible(), + "Only fungible assets are supported" + ); + + // Extract the fungible amount from the asset value word + let withdraw_amount = withdraw_asset.value[0]; + + // Derive the balance-map key from the depositor and the asset's faucet. + let key = Word::from([ + depositor.prefix, + depositor.suffix, + withdraw_asset.key[3], // faucet_prefix + withdraw_asset.key[2], // faucet_suffix (+ metadata byte; see `balances` field docs) + ]); + + // ======================================================================== + // CRITICAL: Validate balance BEFORE subtraction + // ======================================================================== + // Get current balance and validate sufficient funds exist. + // This check is critical: Felt arithmetic is modular, so subtracting + // more than the balance would silently wrap to a large positive number. + let current_balance: Felt = self.balances.get(key); + assert!( + current_balance.as_canonical_u64() >= withdraw_amount.as_canonical_u64(), + "Withdrawal amount exceeds available balance" + ); + + // Now safe to subtract + let new_balance = current_balance - withdraw_amount; + self.balances.set(key, new_balance); + + // Create a P2ID note to send the requested asset back to the depositor. + // The full implementation (reading the P2ID script root from the note's + // storage and emitting the output note) lands in Part 7. + self.create_p2id_note(serial_num, &withdraw_asset, depositor, tag, note_type); +} +``` + +For now, add a placeholder for `create_p2id_note()` in the private +`impl BankStorage` block: + +```rust title="contracts/bank-account/src/lib.rs" +/// Create a P2ID note to send assets to a recipient. +/// Full implementation in Part 7. +fn create_p2id_note( + &mut self, + _serial_num: Word, + _asset: &Asset, + _recipient_id: AccountId, + _tag: Felt, + _note_type: Felt, +) { + // Placeholder - implemented in Part 7: Output Notes + // Calling this placeholder aborts execution + todo!("P2ID note creation - see Part 7") +} +``` + +## Step 3: Build and Verify + +Build the contract: + +```bash title=">_ Terminal" +cd contracts/bank-account +miden build +``` + +## Try It: Verify Deposits Work + +First, verify your bank-account contract compiles: + +```bash title=">_ Terminal" +cd contracts/bank-account +miden build +``` + +:::note Test Dependencies +The full deposit test below also drives the `deposit-note` contract (Part 4) and the +`init-tx-script` (Part 6). You can return to run it after completing those parts. +::: + +
+Preview: Full deposit test (runnable after Parts 4 and 6) + +This test verifies the complete deposit flow — it initializes the bank via the tx +script, then consumes the deposit note and checks the recorded balance: + +```rust title="integration/tests/deposit_test.rs" +use integration::helpers::{ + build_project_in_dir, build_tx_script_from_package, create_testing_account_from_package, + create_testing_note_from_package, AccountCreationConfig, NoteCreationConfig, +}; + +use miden_client::{ + account::{component::{InitStorageData, StorageValueName}, StorageSlotName}, + auth::AuthScheme, + note::NoteAssets, + transaction::RawOutputNote, + Felt, Word, +}; +use miden_client::asset::{Asset, FungibleAsset}; +use miden_testing::{Auth, MockChain}; +use std::{path::Path, sync::Arc}; + +/// Storage slot names for the bank account component. +/// +/// The `initialized` value slot has no schema default, so `AccountComponent::from_package` +/// requires it to be seeded via `InitStorageData` (otherwise it errors with +/// `InitValueNotProvided`). The `balances` map slot defaults to empty and needs no entry. +fn bank_storage_slots() -> (StorageSlotName, StorageSlotName) { + let initialized_slot = + StorageSlotName::new("bank_account::bank::initialized") + .expect("Valid slot name"); + let balances_slot = + StorageSlotName::new("bank_account::bank::balances") + .expect("Valid slot name"); + (initialized_slot, balances_slot) +} + +#[tokio::test] +async fn deposit_test() -> anyhow::Result<()> { + // Test that after executing the deposit note, the depositor's balance is updated + let mut builder = MockChain::builder(); + + // Create a faucet to mint test assets + let faucet = builder.add_existing_basic_faucet( + Auth::BasicAuth { + auth_scheme: AuthScheme::Falcon512Poseidon2, + }, + "TEST", + 1000, + Some(10), + )?; + + // Create note sender account (the depositor) + let sender = builder.add_existing_wallet_with_assets( + Auth::BasicAuth { + auth_scheme: AuthScheme::Falcon512Poseidon2, + }, + [FungibleAsset::new(faucet.id(), 100)?.into()], + )?; + + // Build contracts + let bank_package = Arc::new(build_project_in_dir( + Path::new("../contracts/bank-account"), + true, + )?); + let deposit_note_package = Arc::new(build_project_in_dir( + Path::new("../contracts/deposit-note"), + true, + )?); + let init_tx_script_package = Arc::new(build_project_in_dir( + Path::new("../contracts/init-tx-script"), + true, + )?); + + // Create the bank account. The `initialized` value slot has no schema default, so it must + // be seeded (here with a zero Word = uninitialized) or `from_package` errors with + // `InitValueNotProvided`; the `balances` map defaults to empty. + let (initialized_slot, balances_slot) = bank_storage_slots(); + let bank_cfg = AccountCreationConfig { + init_storage_data: { + let mut data = InitStorageData::default(); + data.insert_value( + StorageValueName::from_slot_name(&initialized_slot), + Word::default(), + )?; + data + }, + ..Default::default() + }; + + let mut bank_account = + create_testing_account_from_package(bank_package.clone(), bank_cfg)?; + + // Create a fungible asset to deposit + let deposit_amount: u64 = 1000; + let fungible_asset = FungibleAsset::new(faucet.id(), deposit_amount)?; + let note_assets = NoteAssets::new(vec![Asset::Fungible(fungible_asset)])?; + + // Create the deposit note with assets attached + // The sender becomes the depositor + let deposit_note = create_testing_note_from_package( + deposit_note_package.clone(), + sender.id(), + NoteCreationConfig { + assets: note_assets, + ..Default::default() + }, + )?; + + // Add bank account and deposit note to mockchain + builder.add_account(bank_account.clone())?; + builder.add_output_note(RawOutputNote::Full(deposit_note.clone())); + + // Build the mock chain + let mut mock_chain = builder.build()?; + + // ********************************************************************************* + // STEP 1: INITIALIZE THE BANK VIA TX SCRIPT + // ********************************************************************************* + // The bank must be initialized before deposits are accepted. + // This is done via a transaction script that calls bank.initialize() + + let init_tx_script = build_tx_script_from_package(init_tx_script_package.as_ref())?; + + let init_tx_context = mock_chain + .build_transaction(bank_account.id()) + .tx_script(init_tx_script) + .build()?; + + let executed_init = init_tx_context.execute().await?; + mock_chain.add_pending_executed_transaction(&executed_init)?; + mock_chain.prove_next_block()?; + bank_account = mock_chain.committed_account(bank_account.id())?.clone(); + + println!("Bank initialized successfully"); + + // ********************************************************************************* + // STEP 2: DEPOSIT + // ********************************************************************************* + + // Build the transaction context where bank consumes the deposit note + let tx_context = mock_chain + .build_transaction(bank_account.id()) + .authenticated_input_note(deposit_note.id()) + .build()?; + + // Execute the transaction + let executed_transaction = tx_context.execute().await?; + + // Add the executed transaction to the mockchain and prove + mock_chain.add_pending_executed_transaction(&executed_transaction)?; + mock_chain.prove_next_block()?; + bank_account = mock_chain.committed_account(bank_account.id())?.clone(); + + // Create the key for the depositor (sender) in the storage map. + // Key format: [depositor_prefix, depositor_suffix, asset.key[3], asset.key[2]]. + // In v0.16 the fungible-asset vault key is + // [asset_class_suffix, asset_class_prefix, faucet_suffix | metadata_byte, faucet_prefix], + // so `key[2]` is the faucet suffix combined with composition metadata, + // not the raw faucet suffix. Derive the read key from the asset's + // actual key word so it matches the key the contract writes. + let asset_key_word = FungibleAsset::new(faucet.id(), deposit_amount)?.to_id_word(); + let depositor_key = Word::from([ + sender.id().prefix().as_felt(), + sender.id().suffix(), + asset_key_word[3], + asset_key_word[2], + ]); + + // Get the depositor's balance from the bank's storage using named slot + let balance = bank_account.storage().get_map_item(&balances_slot, miden_client::account::StorageMapKey::new(depositor_key))?; + + // The contract stores `balance` as a `Felt`; reading the map returns the + // single-Felt value widened into a Word at position [0] ([amount, 0, 0, 0]). + let expected_balance = Word::from([ + Felt::new_unchecked(deposit_amount), + Felt::new_unchecked(0), + Felt::new_unchecked(0), + Felt::new_unchecked(0), + ]); + + assert_eq!( + balance, expected_balance, + "Depositor balance should equal the deposited amount" + ); + + println!("Deposit test passed! Deposited {} tokens", deposit_amount); + Ok(()) +} +``` + +Run the test from the project root: + +```bash title=">_ Terminal" +cargo test --package integration --test deposit_test -- --nocapture +``` + +
+Expected output + +```text + Compiling integration v0.1.0 (/path/to/miden-bank/integration) + Finished `test` profile [unoptimized + debuginfo] target(s) + Running tests/deposit_test.rs + +running 1 test +Deposit test passed! Deposited 1000 tokens +test deposit_test ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored +``` + +
+ +
+ +## Asset Flow Summary + +```text +DEPOSIT FLOW: +┌───────────┐ deposit_note ┌────────────┐ +│ Depositor │ ──────────────────▶ Bank Vault │ +│ Wallet │ (with asset) │ + Balance │ +└───────────┘ └────────────┘ + +WITHDRAW FLOW: +┌────────────┐ P2ID note ┌───────────┐ +│ Bank Vault │ ──────────────────▶ Depositor│ +│ - Balance │ (with asset) │ Wallet │ +└────────────┘ └───────────┘ +``` + +## Complete Code for This Part + +Here's the full `lib.rs` after Part 3: + +
+Click to expand full code + +```rust title="contracts/bank-account/src/lib.rs" +#![no_std] +#![feature(alloc_error_handler)] + +#[macro_use] +extern crate alloc; + +use miden::*; + +use miden::Felt; + +/// Maximum allowed deposit amount per transaction. +const MAX_DEPOSIT_AMOUNT: u64 = 1_000_000; + +/// Maximum allowed balance per depositor per asset. +/// Matches FungibleAsset::MAX_AMOUNT (2^63 - 2^31). +const MAX_BALANCE: u64 = 9_223_372_034_707_292_160; + +/// Storage layout for the bank account component. +#[component_storage] +struct BankStorage { + /// Word layout: [is_initialized (0 or 1), 0, 0, 0] + #[storage(description = "initialized")] + initialized: StorageValue, + + /// Maps (depositor AccountId, faucet ID) -> balance (as Felt). + #[storage(description = "balances")] + balances: StorageMap, +} + +/// API of the bank account component. +#[component] +trait Bank { + /// Initialize the bank account, enabling deposits. + #[account_procedure] + fn initialize(&mut self); + + /// Get the bank-tracked balance for a depositor and specific asset type. + /// + /// Named `get_depositor_balance` (not `get_balance`) to avoid colliding with + /// the built-in `ActiveAccount::get_balance` vault method that the account + /// wrapper generates. + #[account_procedure] + fn get_depositor_balance(&self, depositor: AccountId, asset: Asset) -> Felt; + + /// Deposit an asset into the bank for a specific depositor. + #[account_procedure] + fn deposit(&mut self, depositor: AccountId, deposit_asset: Asset); + + /// Withdraw assets back to the depositor. + #[account_procedure] + fn withdraw(&mut self, withdraw_asset: Asset, serial_num: Word, tag: Felt, note_type: Felt); +} + +#[component] +impl Bank for BankStorage { + fn initialize(&mut self) { + let current: Word = self.initialized.get(); + assert!( + current[0].as_canonical_u64() == 0, + "Bank already initialized" + ); + + let initialized_word = Word::from([felt!(1), felt!(0), felt!(0), felt!(0)]); + self.initialized.set(initialized_word); + } + + fn get_depositor_balance(&self, depositor: AccountId, asset: Asset) -> Felt { + // Create key from depositor's AccountId and asset faucet ID + let key = Word::from([ + depositor.prefix, + depositor.suffix, + asset.key[3], // faucet_prefix + asset.key[2], // faucet_suffix (+ metadata byte; see `balances` field docs) + ]); + self.balances.get(key) + } + + fn deposit(&mut self, depositor: AccountId, deposit_asset: Asset) { + // NOTE: Initialization guard — enabled in Part 6 (Transaction Scripts) + // self.require_initialized(); + + assert!( + deposit_asset.is_fungible(), + "Only fungible assets are supported" + ); + + let deposit_amount = deposit_asset.value[0]; + + assert!( + deposit_amount.as_canonical_u64() <= MAX_DEPOSIT_AMOUNT, + "Deposit amount exceeds maximum allowed" + ); + + let key = Word::from([ + depositor.prefix, + depositor.suffix, + deposit_asset.key[3], // faucet_prefix + deposit_asset.key[2], // faucet_suffix (+ metadata byte; see `balances` field docs) + ]); + + // Validate in integer space — Felt addition is modular + let current_balance: Felt = self.balances.get(key); + let current_u64 = current_balance.as_canonical_u64(); + let deposit_u64 = deposit_amount.as_canonical_u64(); + let new_balance_u64 = current_u64 + .checked_add(deposit_u64) + .expect("Balance overflow"); + assert!(new_balance_u64 <= MAX_BALANCE, "Balance would exceed maximum"); + + // Guest-side `Felt::new` is fallible, so unwrap the validated value. + self.balances.set(key, Felt::new(new_balance_u64).unwrap()); + + native_account::add_asset(deposit_asset); + } + + /// Withdraw assets from the bank. + /// The depositor is identified via `active_note::get_sender()` internally. + fn withdraw( + &mut self, + withdraw_asset: Asset, + serial_num: Word, + tag: Felt, + note_type: Felt, + ) { + // NOTE: Initialization guard — enabled in Part 6 (Transaction Scripts) + // self.require_initialized(); + + let depositor = active_note::get_sender(); + + assert!( + withdraw_asset.is_fungible(), + "Only fungible assets are supported" + ); + + let withdraw_amount = withdraw_asset.value[0]; + + let key = Word::from([ + depositor.prefix, + depositor.suffix, + withdraw_asset.key[3], // faucet_prefix + withdraw_asset.key[2], // faucet_suffix (+ metadata byte; see `balances` field docs) + ]); + + // CRITICAL: Validate balance BEFORE subtraction + let current_balance: Felt = self.balances.get(key); + assert!( + current_balance.as_canonical_u64() >= withdraw_amount.as_canonical_u64(), + "Withdrawal amount exceeds available balance" + ); + + let new_balance = current_balance - withdraw_amount; + self.balances.set(key, new_balance); + + // Full P2ID note creation lands in Part 7. + self.create_p2id_note(serial_num, &withdraw_asset, depositor, tag, note_type); + } +} + +/// Internal helpers that are not part of the component's exported WIT API. +/// +/// The `#[component]` macro exports only the methods of the `Bank` trait, so these +/// inherent methods stay private to the contract. +impl BankStorage { + /// Check that the bank is initialized. + fn require_initialized(&self) { + let current: Word = self.initialized.get(); + assert!( + current[0].as_canonical_u64() == 1, + "Bank not initialized - deposits not enabled" + ); + } + + /// Create a P2ID note - placeholder for Part 7. + fn create_p2id_note( + &mut self, + _serial_num: Word, + _asset: &Asset, + _recipient_id: AccountId, + _tag: Felt, + _note_type: Felt, + ) { + todo!("P2ID note creation - see Part 7") + } +} +``` + +
+ +## Key Takeaways + +1. **Asset layout**: `value[0]` = amount; `key[2]` = faucet suffix plus composition metadata; `key[3]` = faucet prefix. Mirror it host-side with `FungibleAsset::to_id_word()` indices `[3]`/`[2]` +2. **`native_account::add_asset()`** adds assets to the vault +3. **`native_account::remove_asset()`** removes assets from the vault (Part 7) +4. **Balance tracking** is application-level logic using `StorageMap` +5. **Composite keys** allow per-user, per-asset balance tracking +6. **CRITICAL: Always validate before subtraction** - Felt arithmetic wraps silently! + +:::tip View Complete Source +See the complete deposit and withdraw implementations in [contracts/bank-account/src/lib.rs](https://github.com/0xMiden/miden-tutorials/blob/main/examples/miden-bank/contracts/bank-account/src/lib.rs). +::: + +## Next Steps + +Now that you understand asset management, let's learn how to trigger these operations with [Part 4: Note Scripts](./note-scripts). diff --git a/versioned_docs/version-0.16/builder/tutorials/miden-bank/04-note-scripts.md b/versioned_docs/version-0.16/builder/tutorials/miden-bank/04-note-scripts.md new file mode 100644 index 00000000..c37bdda9 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tutorials/miden-bank/04-note-scripts.md @@ -0,0 +1,611 @@ +--- +sidebar_position: 4 +title: "Part 4: Note Scripts" +description: "Learn how to write note scripts that execute when notes are consumed, using active_note APIs to access sender, assets, and inputs." +--- + +# Part 4: Note Scripts + +In this section, you'll learn how to write note scripts - code that executes when a note is consumed by an account. We'll create the deposit note that lets users deposit tokens into the bank. + +## What You'll Build in This Part + +By the end of this section, you will have: + +- Created the `deposit-note` contract +- Understood the `#[note]` struct+impl pattern and the `#[note_script]` method attribute +- Used the `#[account(...)]` wallet wrapper to call the bank's methods from a note +- Used `active_note` APIs to access sender and assets +- Built the note script and its dependencies +- **Verified it works** with a complete deposit flow test + +## Building on Part 3 + +In Part 3, we completed the bank's deposit method. Now we need a way to trigger it: + +```text +Part 3: Part 4: +┌──────────────────┐ ┌──────────────────┐ +│ Bank (complete) │ │ Bank (complete) │ +│ ─────────────────│ │ ─────────────────│ +│ + deposit() │ │ + deposit() │ +│ + withdraw() │ │ + withdraw() │ +└──────────────────┘ └──────────────────┘ + ▲ + │ calls + ┌────────────────────┐ + │ deposit-note │ ◄── NEW + │ (note script) │ + └────────────────────┘ +``` + +## Note Scripts vs Account Components + +| Feature | Account Component | Note Script | +| ----------- | ------------------------- | ------------------------------------------------ | +| Purpose | Persistent account logic | One-time execution when consumed | +| Storage | Has persistent storage | No storage (reads from note data) | +| Attribute | `#[component]` | `#[note]` struct + `#[note_script]` method | +| Entry point | Methods on struct | `fn run(self, _arg: Word, account: &mut Wallet)` | +| Invocation | Called by other contracts | Executes when note is consumed | + +Note scripts are like "messages" that carry code along with data and assets. + +## Step 1: Create the Deposit Note Project + +First, create the deposit-note contract. If you used `miden new`, you may have an `increment-note` folder - rename or replace it: + +```bash title=">_ Terminal" +# Remove or rename the example +rm -rf contracts/increment-note +# Or: mv contracts/increment-note contracts/increment-note-backup + +# Create the deposit-note directory +mkdir -p contracts/deposit-note/src +``` + +## Step 2: Configure the Project Files + +Like every contract in this tutorial, the deposit note has three small config files: a `Cargo.toml`, a `miden-project.toml`, and a `.cargo/config.toml`. + +Create the `Cargo.toml`: + +```toml title="contracts/deposit-note/Cargo.toml" +[package] +name = "deposit-note" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +miden = "=0.14.0" +``` + +Create the `miden-project.toml`. This is where the note declares its kind and its dependency on the bank account it calls into: + +```toml title="contracts/deposit-note/miden-project.toml" +[package] +name = "deposit-note" +version = "0.1.0" + +[lib] +kind = "note" +path = "src/lib.rs" +namespace = "miden:deposit-note/miden-deposit-note@0.1.0" + +[dependencies] +miden-core = "*" +miden-protocol = "*" +bank-account = { path = "../bank-account" } + +``` + +Finally, the `.cargo/config.toml` pins the WebAssembly target and the `miden` cfg: + +```toml title="contracts/deposit-note/.cargo/config.toml" +[build] +target = "wasm32-wasip2" + +[target.wasm32-wasip2] +rustflags = ["--cfg", "miden"] +``` + +Key configuration: + +- `kind = "note"` - Marks this as a note script +- `bank-account = { path = "../bank-account" }` declares the component this note calls. Compiler 0.10 builds the dependency and reads its interface from the compiled package + +## Step 3: Implement the Deposit Note + +Create the note script implementation: + +```rust title="contracts/deposit-note/src/lib.rs" +// Do not link against libstd (i.e. anything defined in `std::`) +#![no_std] +#![feature(alloc_error_handler)] + +use miden::*; + +/// Native (active) account of this note: exposes the `bank-account` component's +/// `Bank` methods, gathered from the `bank-account` package's generated WIT. +#[account(bank_account::Bank)] +pub struct Wallet; + +/// Deposit Note Script +/// +/// When consumed by the Bank account, this note transfers all its assets +/// to the bank and credits the depositor (note sender) with the deposited amount. +#[note] +struct DepositNote; + +#[note] +impl DepositNote { + #[note_script] + fn run(self, _arg: Word, account: &mut Wallet) { + // The depositor is whoever created/sent this note + let depositor = active_note::get_sender(); + + // Get all assets attached to this note + let assets = active_note::get_initial_assets(); + + // Deposit each asset into the bank + for asset in assets { + account.deposit(depositor, asset); + } + } +} +``` + +:::info Cross-Component Calls +The `#[account(bank_account::Bank)] pub struct Wallet;` declaration and the `account.deposit(...)` call use Miden's cross-component binding system. The `#[account(...)]` macro wraps the consuming account so the note can call the bank's `Bank` methods directly. We'll explain exactly how this works in [Part 5: Cross-Component Calls](./cross-component-calls). When you build `deposit-note`, Compiler 0.10 builds the `bank-account` dependency declared in `miden-project.toml` and reads its interface from the compiled package. +::: + +### The #[note] and #[note_script] Attributes + +The `#[note]` attribute is applied to both a unit struct and its `impl` block to define a note script. Within the `impl` block, the `#[note_script]` attribute marks the entry point method. The function signature is always: + +```rust +fn run(self, _arg: Word, account: &mut Wallet) +``` + +The method takes `self` as its first parameter. The `_arg` parameter can pass additional data (we don't use it in the deposit note), and `account: &mut Wallet` is the consuming account, through which we call the bank's methods. + +## Note Context APIs + +The `active_note` module provides APIs to access note data during execution: + +### get_sender() - Who Created the Note + +```rust +let depositor = active_note::get_sender(); +``` + +Returns the `AccountId` of the account that created/sent the note. In our bank: + +- The sender is the depositor +- Their ID is used to credit their balance + +### get_initial_assets() - Attached Assets + +```rust +let assets = active_note::get_initial_assets(); +for asset in assets { + // Process each asset +} +``` + +Returns a `Vec` containing all assets initially attached to the note. The `for` loop consumes that vector. + +### get_storage() - Note Parameters + +```rust +let storage = active_note::get_storage(); +let first_item = storage[0]; +``` + +Returns a `Vec` containing the storage items passed when the note was created. The indexing example requires at least one item. We'll use storage items in the withdraw request note (Part 7). + +## Step 4: Build the Note Script + +:::info Dependencies Build Automatically +Compiler 0.10 resolves and builds the `bank-account` dependency before compiling the note. The `#[account(...)]` macro uses the bank's compiled interface and procedure roots to bind calls to its methods. You can build the note directly using the dependency declaration in `miden-project.toml`. +::: + +From the project root: + +```bash title=">_ Terminal" +cd contracts/deposit-note +miden build --release +cd ../.. +``` + +
+Expected output + +```text + Compiling deposit-note v0.1.0 + Finished `release` profile [optimized] target(s) +``` + +
+ +## Execution Flow Diagram + +```text +1. User creates deposit note with 100 tokens attached + ┌───────────────────────────────────────┐ + │ Note: deposit-note │ + │ Sender: User's AccountId │ + │ Assets: [100 tokens] │ + └───────────────────────────────────────┘ + +2. Bank account consumes the note + ┌───────────────────────────────────────┐ + │ Bank receives assets into vault │ + │ Note script executes... │ + └───────────────────────────────────────┘ + +3. Note script runs + depositor = get_sender() → User's AccountId + assets = get_initial_assets() → [100 tokens] + account.deposit(depositor, 100 tokens) + +4. Bank's deposit() method executes + - Validates asset type and amount + - Checks initialization once the guard is enabled in Part 6 + - Updates balance: balances[User] += 100 + - Adds asset to vault +``` + +## Try It: Verify Deposits Work + +This test verifies the deposit flow end-to-end — building the contracts, initializing the bank, creating a deposit, and checking the balance. + +:::note Preview of the Part 6 Initialization Flow +The bank inherited from Part 3 still has `require_initialized()` commented out, so deposits currently work without initialization. In [Part 6](./06-transaction-scripts.md), we'll create the initialization transaction script and enable the guard, making initialization mandatory. The test below previews that flow: it initializes the bank, consumes a deposit note, and checks the depositor's balance. Run it after completing Part 6, or use the complete example projects from the repository. +::: + +Create the test file: + +:::note Illustrative snippet +The snippet below illustrates the deposit happy-path. The shipped repository's `examples/miden-bank/integration/tests/deposit_test.rs` is the source of truth and additionally exercises failure paths (`deposit_exceeds_max_should_fail`, `deposit_without_init_should_fail`). +::: + +```rust title="integration/tests/deposit_test.rs" +use integration::helpers::{ + build_project_in_dir, build_tx_script_from_package, create_testing_account_from_package, + create_testing_note_from_package, AccountCreationConfig, NoteCreationConfig, +}; + +use miden_client::{ + account::{component::{InitStorageData, StorageValueName}, StorageSlotName}, + auth::AuthScheme, + note::NoteAssets, + transaction::RawOutputNote, + Felt, Word, +}; +use miden_client::asset::{Asset, FungibleAsset}; +use miden_testing::{Auth, MockChain}; +use std::{path::Path, sync::Arc}; + +/// Storage slot names for the bank account component. +/// +/// The `initialized` value slot has no schema default, so `AccountComponent::from_package` +/// requires it to be seeded via `InitStorageData` (otherwise it errors with +/// `InitValueNotProvided`). The `balances` map slot defaults to empty and needs no entry. +fn bank_storage_slots() -> (StorageSlotName, StorageSlotName) { + let initialized_slot = + StorageSlotName::new("bank_account::bank::initialized") + .expect("Valid slot name"); + let balances_slot = + StorageSlotName::new("bank_account::bank::balances") + .expect("Valid slot name"); + (initialized_slot, balances_slot) +} + +#[tokio::test] +async fn deposit_test() -> anyhow::Result<()> { + // Test that after executing the deposit note, the depositor's balance is updated + let mut builder = MockChain::builder(); + + // Create a faucet to mint test assets + let faucet = builder.add_existing_basic_faucet( + Auth::BasicAuth { + auth_scheme: AuthScheme::Falcon512Poseidon2, + }, + "TEST", + 1000, + Some(10), + )?; + + // Create note sender account (the depositor) + let sender = builder.add_existing_wallet_with_assets( + Auth::BasicAuth { + auth_scheme: AuthScheme::Falcon512Poseidon2, + }, + [FungibleAsset::new(faucet.id(), 100)?.into()], + )?; + + // Build contracts + let bank_package = Arc::new(build_project_in_dir( + Path::new("../contracts/bank-account"), + true, + )?); + let deposit_note_package = Arc::new(build_project_in_dir( + Path::new("../contracts/deposit-note"), + true, + )?); + let init_tx_script_package = Arc::new(build_project_in_dir( + Path::new("../contracts/init-tx-script"), + true, + )?); + + // Create the bank account. The `initialized` value slot has no schema default, so it must + // be seeded (here with a zero Word = uninitialized) or `from_package` errors with + // `InitValueNotProvided`; the `balances` map defaults to empty. + let (initialized_slot, balances_slot) = bank_storage_slots(); + let bank_cfg = AccountCreationConfig { + init_storage_data: { + let mut data = InitStorageData::default(); + data.insert_value( + StorageValueName::from_slot_name(&initialized_slot), + Word::default(), + )?; + data + }, + ..Default::default() + }; + + let mut bank_account = + create_testing_account_from_package(bank_package.clone(), bank_cfg)?; + + // Create a fungible asset to deposit + let deposit_amount: u64 = 1000; + let fungible_asset = FungibleAsset::new(faucet.id(), deposit_amount)?; + let note_assets = NoteAssets::new(vec![Asset::Fungible(fungible_asset)])?; + + // Create the deposit note with assets attached + // The sender becomes the depositor + let deposit_note = create_testing_note_from_package( + deposit_note_package.clone(), + sender.id(), + NoteCreationConfig { + assets: note_assets, + ..Default::default() + }, + )?; + + // Add bank account and deposit note to mockchain + builder.add_account(bank_account.clone())?; + builder.add_output_note(RawOutputNote::Full(deposit_note.clone())); + + // Build the mock chain + let mut mock_chain = builder.build()?; + + // ********************************************************************************* + // STEP 1: INITIALIZE THE BANK VIA TX SCRIPT + // ********************************************************************************* + // Preview the Part 6 flow, where require_initialized() is enabled. + // Initialize via a transaction script that calls bank.initialize(). + + let init_tx_script = build_tx_script_from_package(init_tx_script_package.as_ref())?; + + let init_tx_context = mock_chain + .build_transaction(bank_account.id()) + .tx_script(init_tx_script) + .build()?; + + let executed_init = init_tx_context.execute().await?; + mock_chain.add_pending_executed_transaction(&executed_init)?; + mock_chain.prove_next_block()?; + bank_account = mock_chain.committed_account(bank_account.id())?.clone(); + + println!("Bank initialized successfully"); + + // ********************************************************************************* + // STEP 2: DEPOSIT + // ********************************************************************************* + + // Build the transaction context where bank consumes the deposit note + let tx_context = mock_chain + .build_transaction(bank_account.id()) + .authenticated_input_note(deposit_note.id()) + .build()?; + + // Execute the transaction + let executed_transaction = tx_context.execute().await?; + + // Add the executed transaction to the mockchain and prove + mock_chain.add_pending_executed_transaction(&executed_transaction)?; + mock_chain.prove_next_block()?; + bank_account = mock_chain.committed_account(bank_account.id())?.clone(); + + // Create the key for the depositor (sender) in the storage map. + // Key format: [depositor_prefix, depositor_suffix, asset.key[3], asset.key[2]]. + // In v0.16 the fungible-asset vault key is + // [asset_class_suffix, asset_class_prefix, faucet_suffix | metadata_byte, faucet_prefix], + // so `key[2]` is the faucet suffix combined with composition metadata, + // not the raw faucet suffix. Derive the read key from the asset's + // actual key word so it matches the key the contract writes. + let asset_key_word = FungibleAsset::new(faucet.id(), deposit_amount)?.to_id_word(); + let depositor_key = Word::from([ + sender.id().prefix().as_felt(), + sender.id().suffix(), + asset_key_word[3], + asset_key_word[2], + ]); + + // Get the depositor's balance from the bank's storage using named slot + let balance = bank_account.storage().get_map_item(&balances_slot, miden_client::account::StorageMapKey::new(depositor_key))?; + + // The contract stores `balance` as a `Felt`; reading the map returns the + // single-Felt value widened into a Word at position [0] ([amount, 0, 0, 0]). + let expected_balance = Word::from([ + Felt::new_unchecked(deposit_amount), + Felt::new_unchecked(0), + Felt::new_unchecked(0), + Felt::new_unchecked(0), + ]); + + assert_eq!( + balance, expected_balance, + "Depositor balance should equal the deposited amount" + ); + + println!("Deposit test passed! Deposited {} tokens", deposit_amount); + Ok(()) +} +``` + +Run the test from the project root: + +```bash title=">_ Terminal" +cargo test --package integration --test deposit_test -- --nocapture +``` + +
+Expected output + +```text + Compiling integration v0.1.0 (/path/to/miden-bank/integration) + Finished `test` profile [unoptimized + debuginfo] target(s) + Running tests/deposit_test.rs + +running 1 test +Bank initialized successfully +Deposit test passed! Deposited 1000 tokens +test deposit_test ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored +``` + +
+ +## Preview: Withdraw Request Note + +For withdrawals, we'll use note inputs to pass parameters. Here's a preview of the withdraw request note (implemented in Part 7): + +```rust title="contracts/withdraw-request-note/src/lib.rs (preview)" +/// Native (active) account of this note: exposes the `bank-account` component's +/// `Bank` methods, gathered from the `bank-account` package's generated WIT. +#[account(bank_account::Bank)] +pub struct Wallet; + +/// Withdraw Request Note Script +/// +/// # Note Storage (14 Felts) +/// [0-3]: withdraw asset, encoded as [amount, 0, faucet_suffix(+metadata), faucet_prefix]. +/// `storage[2]` carries the faucet suffix with the asset's metadata byte in its +/// low 8 bits (host side: `FungibleAsset::to_id_word()[2]`), not the raw suffix. +/// [4-7]: serial_num (random/unique per note) +/// [8]: tag (P2ID note tag for routing) +/// [9]: note_type (1 = Public, 0 = Private) +/// [10-13]: P2ID script_root (MAST root of the P2ID note script, Poseidon2-hashed) +#[note] +struct WithdrawRequestNote; + +#[note] +impl WithdrawRequestNote { + #[note_script] + fn run(self, _arg: Word, account: &mut Wallet) { + // Get the storage items and validate the expected count. + let storage = active_note::get_storage(); + assert!( + storage.len() == 14, + "Withdraw request requires exactly 14 storage items" + ); + + // Asset: reconstruct the v0.16 fungible-asset ID/value from the note storage. + // key = [0, 0, storage[2], storage[3]] where storage[2] = faucet suffix + metadata + // byte (low 8 bits) and storage[3] = faucet prefix. + // value = [amount, 0, 0, 0] + let withdraw_asset = Asset::new( + Word::from([felt!(0), felt!(0), storage[2], storage[3]]), + Word::from([storage[0], felt!(0), felt!(0), felt!(0)]), + ); + + let serial_num = Word::from([storage[4], storage[5], storage[6], storage[7]]); + + let tag = storage[8]; + let note_type = storage[9]; + + // Note: P2ID script root (storage[10..13]) is read by the bank account directly + // from the active note's storage inside `Bank::withdraw`. + + // The bank identifies the depositor internally via `active_note::get_sender()`, + // which is cryptographically bound to this note's metadata and cannot be spoofed. + account.withdraw(withdraw_asset, serial_num, tag, note_type); + } +} +``` + +:::warning Stack Limits +Note inputs are limited. Keep your input layout compact. See [Common Pitfalls](https://docs.miden.xyz/builder/tutorials/rust-compiler/pitfalls) for stack-related constraints. +::: + +## Complete Code for This Part + +
+Click to expand deposit-note/src/lib.rs + +```rust title="contracts/deposit-note/src/lib.rs" +// Do not link against libstd (i.e. anything defined in `std::`) +#![no_std] +#![feature(alloc_error_handler)] + +use miden::*; + +/// Native (active) account of this note: exposes the `bank-account` component's +/// `Bank` methods, gathered from the `bank-account` package's generated WIT. +#[account(bank_account::Bank)] +pub struct Wallet; + +/// Deposit Note Script +/// +/// When consumed by the Bank account, this note transfers all its assets +/// to the bank and credits the depositor (note sender) with the deposited amount. +#[note] +struct DepositNote; + +#[note] +impl DepositNote { + #[note_script] + fn run(self, _arg: Word, account: &mut Wallet) { + // The depositor is whoever created/sent this note + let depositor = active_note::get_sender(); + + // Get all assets attached to this note + let assets = active_note::get_initial_assets(); + + // Deposit each asset into the bank + for asset in assets { + account.deposit(depositor, asset); + } + } +} +``` + +
+ +## Key Takeaways + +1. **`#[note]`** marks the struct and impl block, with **`#[note_script]`** on the entry point method `fn run(self, _arg: Word, account: &mut Wallet)` +2. **`#[account(bank_account::Bank)] pub struct Wallet;`** wraps the consuming account so the note can call the bank's methods via `account.deposit(...)` +3. **`active_note::get_sender()`** returns who created the note +4. **`active_note::get_initial_assets()`** returns the assets attached to the note at creation time +5. **`active_note::get_storage()`** returns parameterized data +6. **Note scripts execute once** when consumed - no persistent state +7. **Dependencies build automatically** - declare the account component in `miden-project.toml`, then build the note with `miden build` + +:::tip View Complete Source +See the complete note script implementations: + +- [Deposit Note](https://github.com/0xMiden/miden-tutorials/blob/main/examples/miden-bank/contracts/deposit-note/src/lib.rs) +- [Withdraw Request Note](https://github.com/0xMiden/miden-tutorials/blob/main/examples/miden-bank/contracts/withdraw-request-note/src/lib.rs) + ::: + +## Next Steps + +Now that you understand note scripts, let's learn how they call account methods in [Part 5: Cross-Component Calls](./cross-component-calls). diff --git a/versioned_docs/version-0.16/builder/tutorials/miden-bank/05-cross-component-calls.md b/versioned_docs/version-0.16/builder/tutorials/miden-bank/05-cross-component-calls.md new file mode 100644 index 00000000..d09850cf --- /dev/null +++ b/versioned_docs/version-0.16/builder/tutorials/miden-bank/05-cross-component-calls.md @@ -0,0 +1,336 @@ +--- +sidebar_position: 5 +title: "Part 5: Cross-Component Calls" +description: "Learn how note scripts and transaction scripts call account component methods using the #[account(...)] wrapper and proper dependency configuration." +--- + +# Part 5: Cross-Component Calls + +In this section, you'll learn how note scripts call methods on account components. We'll explore the generated bindings system and the dependency configuration that makes the deposit note work. + +## What You'll Learn in This Part + +By the end of this section, you will have: + +- Understood how bindings are generated and imported +- Learned the dependency configuration in `miden-project.toml` +- Explored the embedded WIT interface +- **Verified cross-component calls work** via the deposit flow + +## Building on Part 4 + +In Part 4, you wrote `account.deposit(depositor, asset)` in the deposit note. But how does that call actually work? This part explains the binding system: + +```text +┌────────────────────────────────────────────────────────────────────────┐ +│ How Bindings Work │ +├────────────────────────────────────────────────────────────────────────┤ +│ │ +│ bank-account/ │ +│ └── src/lib.rs │ +│ #[component] trait Bank │ +│ ├── deposit(...) │ +│ └── withdraw(...) │ +│ │ │ +│ │ miden build │ +│ ▼ │ +│ ┌────────────────────────────────────────────────┐ │ +│ │ bank-account.masp │ │ +│ │ Compiled code + embedded WIT + procedure roots │ │ +│ └────────────────────────────────────────────────┘ │ +│ │ │ +│ │ Read the embedded interface during the note build │ +│ ▼ │ +│ deposit-note/ │ +│ └── src/lib.rs │ +│ #[account(bank_account::Bank)] │ +│ pub struct Wallet; │ +│ account.deposit(...) ──▶ generated binding ──▶ Bank::deposit │ +│ │ +└────────────────────────────────────────────────────────────────────────┘ +``` + +## The Bindings System + +When you build an account component with `miden build`, it generates: + +1. **MASM code** - The compiled contract logic +2. **Embedded WIT** - WebAssembly Interface Type definitions stored in the package + +Other contracts (note scripts, transaction scripts) read the package's embedded interface to call the account's methods. + +```text +Build Flow: + +┌────────────────────┐ ┌───────────────────────────────────┐ +│ bank-account/ │ miden build │ bank-account.masp │ +│ src/lib.rs │ ──────────────▶ │ Code + embedded WIT │ +│ Bank component │ │ Account procedure roots │ +└────────────────────┘ └───────────────────────────────────┘ + │ + ▼ + ┌───────────────────────────────────┐ + │ deposit-note/ │ + │ #[account(bank_account::Bank)] │ + │ Generated Wallet bindings │ + │ account.deposit(...) │ + └───────────────────────────────────┘ +``` + +## Declaring the Account Wrapper + +In your note script, declare a wrapper struct over the bank account's `Bank` component using the `#[account(...)]` attribute: + +```rust title="contracts/deposit-note/src/lib.rs" +use miden::*; + +/// Native (active) account of this note: exposes the `bank-account` component's +/// `Bank` methods, gathered from the `bank-account` package's generated WIT. +#[account(bank_account::Bank)] +pub struct Wallet; +``` + +The `#[account(...)]` path follows this pattern: + +``` +#[account({package-name}::{trait-name})] +``` + +For our bank: + +- `bank_account` - The package name (derived from `bank-account` with underscores) +- `Bank` - The component trait whose methods are exposed on the wrapper + +The macro reads the bank account's generated WIT and generates a `Wallet` type whose methods (`deposit`, `withdraw`, `initialize`, `get_depositor_balance`) call into the bank component across the component boundary. + +## Calling Account Methods + +The wrapper is passed into the note script as a mutable `account` parameter. Call the account methods directly on it: + +```rust title="contracts/deposit-note/src/lib.rs" +#[note] +struct DepositNote; + +#[note] +impl DepositNote { + #[note_script] + fn run(self, _arg: Word, account: &mut Wallet) { + // The depositor is whoever created/sent this note + let depositor = active_note::get_sender(); + + // Get all assets attached to this note + let assets = active_note::get_initial_assets(); + + // Deposit each asset into the bank + for asset in assets { + account.deposit(depositor, asset); + } + } +} +``` + +The binding automatically handles: + +- Marshalling arguments across the component boundary +- Invoking the correct MASM procedures +- Returning results back to the caller + +## Configuring Dependencies + +Cross-component calls are configured in the note's `miden-project.toml`, which declares the bank as a path dependency: + +```toml title="contracts/deposit-note/miden-project.toml" +[dependencies] +miden-core = "*" +miden-protocol = "*" +bank-account = { path = "../bank-account" } + +``` + +### `[dependencies]` path + +```toml +[dependencies] +bank-account = { path = "../bank-account" } +``` + +This tells `cargo-miden` where to find the source package. Used during the build process to: + +- Verify interface compatibility +- Link the compiled MASM code + +### Embedded Interface + +Compiler 0.10 embeds the WIT interface in the compiled bank package. The path dependency provides both the interface and the procedure roots. Remove the legacy `[package.metadata.miden.dependencies]` `wit` override: supplying it alongside an embedded interface causes the compiler to reject the build. + +## Build Order + +From the project root, build the account first to inspect its output, then build the note: + +```bash title=">_ Terminal" +# 1. Build the account component first +cd contracts/bank-account +miden build + +# 2. Then build note scripts that depend on it +cd ../deposit-note +miden build + +# 3. Return to the project root +cd ../.. +``` + +If you build a dependent contract directly, compiler 0.10 also builds its path dependencies. + +## What Methods Are Available? + +Only the methods declared on the `#[component] trait Bank` are exported through bindings. The macro exports exactly the trait's methods: + +```rust title="contracts/bank-account/src/lib.rs" +/// API of the bank account component. +#[component] +trait Bank { + // EXPORTED: Available through bindings + #[account_procedure] + fn initialize(&mut self); + #[account_procedure] + fn get_depositor_balance(&self, depositor: AccountId, asset: Asset) -> Felt; + #[account_procedure] + fn deposit(&mut self, depositor: AccountId, deposit_asset: Asset); + #[account_procedure] + fn withdraw(&mut self, withdraw_asset: Asset, serial_num: Word, tag: Felt, note_type: Felt); +} +``` + +Private helpers stay off the trait. They live in a separate plain `impl BankStorage` block, so they are **not** exposed through bindings: + +```rust title="contracts/bank-account/src/lib.rs" +/// Internal helpers that are not part of the component's exported WIT API. +impl BankStorage { + fn balance_key(depositor: AccountId, asset: &Asset) -> Word { ... } + fn require_initialized(&self) { ... } + fn create_p2id_note(&mut self, /* ... */) { ... } +} +``` + +:::note `get_depositor_balance`, not `get_balance` +The balance getter is named `get_depositor_balance` to avoid colliding with the built-in `ActiveAccount::get_balance` vault method that the account wrapper generates. +::: + +## Understanding the Generated WIT + +The compiler embeds this WIT in the bank package. Its imported core types come from the SDK: + +```wit title="Embedded bank interface" +package miden:bank-account@0.1.0; + +use miden:base/core-types@1.0.0; + +interface bank { + use core-types.{account-id, asset, felt, word}; + + initialize: func(); + get-depositor-balance: func(depositor: account-id, asset: asset) -> felt; + deposit: func(depositor: account-id, deposit-asset: asset); + withdraw: func(withdraw-asset: asset, serial-num: word, tag: felt, note-type: felt); +} + +world bank-world { + export bank; +} +``` + +This WIT is what the `#[account(bank_account::Bank)]` macro reads to generate the `Wallet` wrapper's methods. + +## Transaction Script Bindings (Preview) + +Transaction scripts use the same `#[account(...)]` wrapper as note scripts. The wrapper is passed in as the `account` parameter: + +```rust title="contracts/init-tx-script/src/lib.rs" +use miden::*; + +/// Native (active) account this tx-script runs against: the bank-account `Bank` component. +#[account(bank_account::Bank)] +pub struct Wallet; + +#[tx_script] +fn run(_arg: Word, account: &mut Wallet) { + account.initialize(); +} +``` + +The `Wallet` wrapper gives direct method access through the `account` parameter, exactly like the note scripts above. We'll implement this in Part 6. + +## Try It: Verify Bindings Work + +After running the builds above, check the bank package from the project root: + +```bash title=">_ Terminal" +# Check the compiled package (its interface is embedded) +ls contracts/bank-account/target/miden/dev/bank-account.masp +``` + +
+Expected output + +```text +contracts/bank-account/target/miden/dev/bank-account.masp +``` + +
+ +The embedded interface enables the deposit note's `#[account(bank_account::Bank)]` wrapper to call `account.deposit()`. + +## Common Issues + +### "Cannot find module" Error + +``` +error: cannot find module `bindings` +``` + +**Cause**: The account path dependency is missing or points to the wrong project. + +**Solution**: + +1. Build the account: `cd contracts/bank-account && miden build` +2. Verify the `bank-account` path dependency in `miden-project.toml` points to the account project; remove legacy `wit` overrides + +### "Method not found" Error + +``` +error: no method named `deposit` found +``` + +**Cause**: The method isn't declared on the `#[component] trait Bank`. Only trait methods are exported through bindings. + +**Solution**: Ensure the method is declared on the `trait Bank`, not just on the private `impl BankStorage` helpers block. + +### "Dependency not found" Error + +``` +error: dependency 'bank-account' not found +``` + +**Cause**: One of the dependency entries in `miden-project.toml` is missing or has the wrong path. + +**Solution**: Add `bank-account = { path = "../bank-account" }` under `[dependencies]` and remove the legacy `wit` override. + +## Key Takeaways + +1. **Declare the path dependency** - The compiler builds the account package and reads its embedded WIT +2. **Embedded interface** - Declare the path under `[dependencies]`; compiler 0.10 reads the interface from the compiled dependency. Do not add the legacy `wit` override. +3. **Account wrapper pattern** - `#[account(bank_account::Bank)] pub struct Wallet;` exposes the component's methods on the `account` parameter +4. **Only trait methods** - Methods on the private `impl BankStorage` helpers aren't exposed in bindings +5. **Note and tx scripts share the pattern** - Both receive the account wrapper as a parameter (Part 6) + +:::tip View Complete Source +See the complete `miden-project.toml` configurations: + +- [Deposit Note miden-project.toml](https://github.com/0xMiden/miden-tutorials/blob/main/examples/miden-bank/contracts/deposit-note/miden-project.toml) +- [Withdraw Request Note miden-project.toml](https://github.com/0xMiden/miden-tutorials/blob/main/examples/miden-bank/contracts/withdraw-request-note/miden-project.toml) + ::: + +## Next Steps + +Now that you understand cross-component calls, let's create the transaction script that initializes the bank in [Part 6: Transaction Scripts](./transaction-scripts). diff --git a/versioned_docs/version-0.16/builder/tutorials/miden-bank/06-transaction-scripts.md b/versioned_docs/version-0.16/builder/tutorials/miden-bank/06-transaction-scripts.md new file mode 100644 index 00000000..fea8bdea --- /dev/null +++ b/versioned_docs/version-0.16/builder/tutorials/miden-bank/06-transaction-scripts.md @@ -0,0 +1,576 @@ +--- +sidebar_position: 6 +title: "Part 6: Transaction Scripts" +description: "Learn how to write transaction scripts for account initialization and owner-controlled operations using the #[tx_script] attribute." +--- + +# Part 6: Transaction Scripts + +In this section, you'll learn how to write transaction scripts - code that the account owner explicitly executes. We'll implement an initialization script that enables the bank to accept deposits. + +The companion testnet example uses `AuthSingleSig` with Falcon512Poseidon2 and saves the bank owner's key in its keystore. This authentication component enforces ownership. The `NoAuth` component in our MockChain test helper is only for isolated tests and must not be used to deploy the bank to a live network. + +## What You'll Build in This Part + +By the end of this section, you will have: + +- Created the `init-tx-script` transaction script project +- Understood the `#[tx_script]` attribute and function signature +- Learned the difference between transaction scripts and note scripts +- **Verified initialization works** via a MockChain test + +## Building on Part 5 + +In Parts 4-5, you created note scripts that execute when notes are consumed. Now you'll create a transaction script - code the account owner explicitly runs: + +```text +┌────────────────────────────────────────────────────────────────────────┐ +│ Script Types Comparison │ +├────────────────────────────────────────────────────────────────────────┤ +│ │ +│ Note Scripts (Parts 4-5) Transaction Scripts (Part 6) │ +│ ────────────────────────────── ────────────────────────────── │ +│ Triggered by note consumption Attached to a transaction │ +│ Receive account: &mut Wallet Receive account: &mut Wallet │ +│ Read active_note:: context Run setup / owner operations │ +│ Process incoming assets Authorized by account auth │ +│ │ +│ deposit-note/ init-tx-script/ │ +│ └── account.deposit(...) └── account.initialize() │ +│ │ +└────────────────────────────────────────────────────────────────────────┘ +``` + +## Transaction Scripts vs Note Scripts + +| Aspect | Transaction Script | Note Script | +| ---------- | ---------------------------- | ---------------------------------- | +| Initiation | Selected for the transaction | Triggered when note is consumed | +| Access | Account wrapper bindings | Account wrapper bindings | +| Use case | Setup, owner operations | Receiving messages/assets | +| Parameter | `account: &mut Wallet` | `account: &mut Wallet` | +| Context | Active account | Active account and `active_note::` | + +**Use transaction scripts for:** + +- One-time initialization +- Admin/owner operations +- Operations that don't involve receiving notes + +**Use note scripts for:** + +- Receiving assets from other accounts +- Processing requests from other accounts +- Multi-party interactions + +## Step 1: Create the Transaction Script Project + +Create a new directory for the transaction script: + +```bash title=">_ Terminal" +mkdir -p contracts/init-tx-script/src +``` + +## Step 2: Configure the Project Files + +Like the account component and the note scripts, a transaction script needs three project files: `Cargo.toml`, `miden-project.toml`, and `.cargo/config.toml`. + +Create the `Cargo.toml`: + +```toml title="contracts/init-tx-script/Cargo.toml" +[package] +name = "init-tx-script" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +miden = "=0.14.0" +``` + +Create the `miden-project.toml`: + +```toml title="contracts/init-tx-script/miden-project.toml" +[package] +name = "init-tx-script" +version = "0.1.0" + +[lib] +kind = "tx-script" +path = "src/lib.rs" +namespace = "miden:base/transaction-script@1.0.0" + +[dependencies] +miden-core = "*" +miden-protocol = "*" +bank-account = { path = "../bank-account" } + +``` + +Create the `.cargo/config.toml`: + +```toml title="contracts/init-tx-script/.cargo/config.toml" +[build] +target = "wasm32-wasip2" + +[target.wasm32-wasip2] +rustflags = ["--cfg", "miden"] +``` + +Key configuration: + +- `kind = "tx-script"` - Marks this as a transaction script (not `account-component` or `note`) +- `namespace = "miden:base/transaction-script@1.0.0"` - The standard transaction-script namespace +- The `bank-account` path dependency lets the script call the account component through the interface embedded in its compiled package + +## Step 3: Implement the Transaction Script + +Create the initialization script: + +```rust title="contracts/init-tx-script/src/lib.rs" +// Do not link against libstd (i.e. anything defined in `std::`) +#![no_std] +#![feature(alloc_error_handler)] + +use miden::*; + +/// Native (active) account this tx-script runs against: the bank-account `Bank` component. +#[account(bank_account::Bank)] +pub struct Wallet; + +/// Initialize Transaction Script +/// +/// This transaction script initializes the bank account, enabling deposits. +/// It must be executed by the bank account owner before any deposits can be made. +/// +/// # Flow +/// 1. Transaction is created with this script attached +/// 2. Script executes in the context of the bank account +/// 3. Calls `account.initialize()` to enable deposits +/// 4. Bank is ready to process deposits after the transaction commits +/// +/// # Arguments +/// * `_arg` - Transaction script argument (unused in this script) +/// * `account` - Mutable reference to the bank account (`Bank` component) +#[tx_script] +fn run(_arg: Word, account: &mut Wallet) { + account.initialize(); +} +``` + +## The #[account] Attribute and the Native Account + +A transaction script runs against the transaction's _native_ (active) account. The `#[account(...)]` attribute binds a wrapper struct to a component so the script can call that component's methods directly: + +```rust +/// Native (active) account this tx-script runs against: the bank-account `Bank` component. +#[account(bank_account::Bank)] +pub struct Wallet; +``` + +This generates a `Wallet` type that wraps the bank-account `Bank` component. The `#[tx_script]` function then receives a `&mut Wallet`, giving it direct access to the component's public methods (such as `initialize()`). + +## The #[tx_script] Attribute + +The `#[tx_script]` attribute marks the entry point for a transaction script: + +```rust +#[tx_script] +fn run(_arg: Word, account: &mut Wallet) { + account.initialize(); +} +``` + +### Function Signature + +| Parameter | Type | Description | +| --------- | ------------- | --------------------------------------- | +| `_arg` | `Word` | Optional argument passed when executing | +| `account` | `&mut Wallet` | Mutable reference to the native account | + +The `Wallet` type is generated by the `#[account(...)]` attribute and provides access to the bound component's public methods. + +## The Native Account Binding + +Both note scripts and transaction scripts bind the native account with `#[account(bank_account::Bank)]` and call its methods directly on the `&mut Wallet` parameter. The entrypoints below belong in separate note-script and transaction-script contracts; the note method goes inside an `impl` marked `#[note]`. The difference is the trigger and the available context: + +```rust +// Note script: triggered by note consumption, has access to note context. +#[note_script] +fn run(self, _arg: Word, account: &mut Wallet) { + let depositor = active_note::get_sender(); // note context + for asset in active_note::get_initial_assets() { + account.deposit(depositor, asset); // native-account method + } +} + +// Transaction script: explicitly run by the owner, no note context. +#[tx_script] +fn run(_arg: Word, account: &mut Wallet) { + account.initialize(); // native-account method +} +``` + +The `Wallet` wrapper provides: + +- Direct method access without module prefixes +- Proper mutable/immutable borrowing +- Automatic native-account context binding + +## Step 4: Build the Transaction Script + +Compiler 0.10 resolves and builds the `bank-account` path dependency automatically. The `#[account(...)]` macro reads the interface and procedure roots from the compiled dependency to generate the script's account bindings. + +From the project root, build the transaction script directly: + +```bash title=">_ Terminal" +cd contracts/init-tx-script +miden build --release +cd ../.. +``` + +
+Expected output + +```text + Compiling init-tx-script v0.1.0 + Finished `release` profile [optimized] target(s) +``` + +
+ +## Account Deployment Pattern + +A public account becomes visible on-chain when its first transaction commits. The bank's `initialized` flag is separate from deployment: it controls whether the bank's deposit and withdrawal methods can run. + +The live example first consumes a funding note to obtain native tokens for fees. That transaction deploys the account while the bank is still uninitialized. The owner then submits the initialization script: + +```text +┌────────────────────────────────────────────────────────────────────────┐ +│ Initialization Flow │ +├────────────────────────────────────────────────────────────────────────┤ +│ │ +│ 1. OWNER SIGNS THE INITIALIZATION TRANSACTION │ +│ ┌────────────────────────────────────────────┐ │ +│ │ Account: Bank's AccountId │ │ +│ │ Script: init-tx-script │ │ +│ │ AuthSingleSig verifies the owner signature │ │ +│ └────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ 2. TRANSACTION SCRIPT EXECUTES │ +│ ┌────────────────────────────────────────┐ │ +│ │ run(_arg, account) │ │ +│ │ └── account.initialize() │ │ +│ │ └── Sets the initialized flag to 1 │ │ +│ └────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ 3. TRANSACTION COMMITS │ +│ ┌──────────────────────────────────────────────────────────┐ │ +│ │ bank_account::bank::initialized = [1, 0, 0, 0] │ │ +│ │ Bank can now process deposits and withdrawals │ │ +│ │ Funding already deployed the account in the live example │ │ +│ └──────────────────────────────────────────────────────────┘ │ +│ │ +└────────────────────────────────────────────────────────────────────────┘ +``` + +Before initialization, other accounts can already create notes addressed to the bank, but its deposit and withdrawal methods reject them. After initialization, those methods can process the notes. A funding P2ID is handled by `BasicWallet` and does not credit any depositor's ledger balance. + +## Using Script Arguments + +The `_arg` parameter can pass data to the script: + +```rust title="Example: Parameterized script" +#[tx_script] +fn run(arg: Word, account: &mut Wallet) { + assert!(arg[0].as_canonical_u64() == 42, "Expected initialization argument"); + account.initialize(); +} +``` + +When creating the transaction, pass the argument with `tx_script_args`: + +```rust title="Integration code (not contract code)" +let tx_script_args = Word::from([42u32, 0, 0, 0]); +let tx_context = mock_chain + .build_transaction(bank_account.id()) + .tx_script(init_tx_script) + .tx_script_args(tx_script_args) // Pass the argument + .build()?; +``` + +## Try It: Verify Initialization Works + +Let's test that the initialization transaction script correctly flips the initialized flag. The companion test file `integration/tests/init_test.rs` verifies exactly this: + +```rust title="integration/tests/init_test.rs" +use integration::helpers::{ + build_project_in_dir, build_tx_script_from_package, create_testing_account_from_package, + AccountCreationConfig, +}; + +use miden_client::{ + account::{component::{InitStorageData, StorageValueName}, StorageSlotName}, + auth::AuthScheme, + Word, +}; +use miden_testing::{Auth, MockChain}; +use std::{path::Path, sync::Arc}; + +/// Companion test for Part 6 of the miden-bank tutorial. Verifies that running +/// the init transaction script flips the bank's `initialized` flag from 0 to 1. +/// +/// The earlier tutorial parts rely on the bank deferring `require_initialized()` +/// enforcement, so this test exists to prove that once the guard is re-enabled +/// the init flow still works end-to-end before any deposits are accepted. +#[tokio::test] +async fn init_test() -> anyhow::Result<()> { + // Build the bank-account and init-tx-script contracts + let bank_package = Arc::new(build_project_in_dir( + Path::new("../contracts/bank-account"), + true, + )?); + + let init_tx_script_package = Arc::new(build_project_in_dir( + Path::new("../contracts/init-tx-script"), + true, + )?); + + // The `initialized` value slot has no schema default, so `AccountComponent::from_package` + // requires it to be seeded (with a zero Word = uninitialized) or it errors with + // `InitValueNotProvided`. The `balances` map slot defaults to empty. + let initialized_slot = StorageSlotName::new("bank_account::bank::initialized") + .expect("Valid slot name"); + + let bank_cfg = AccountCreationConfig { + init_storage_data: { + let mut data = InitStorageData::default(); + data.insert_value( + StorageValueName::from_slot_name(&initialized_slot), + Word::default(), + )?; + data + }, + ..Default::default() + }; + + let mut bank_account = + create_testing_account_from_package(bank_package.clone(), bank_cfg)?; + + // Verify bank starts uninitialized + let before = bank_account.storage().get_item(&initialized_slot)?; + assert_eq!(before[0].as_canonical_u64(), 0, "Bank should start uninitialized"); + println!("Before init: initialized = {}", before[0].as_canonical_u64()); + + // Build mock chain + let mut builder = MockChain::builder(); + builder.add_existing_basic_faucet( + Auth::BasicAuth { + auth_scheme: AuthScheme::Falcon512Poseidon2, + }, + "TEST", + 10_000_000, + Some(10), + )?; + builder.add_account(bank_account.clone())?; + let mut mock_chain = builder.build()?; + + // Execute init transaction script + let init_tx_script = build_tx_script_from_package(init_tx_script_package.as_ref())?; + + let init_tx_context = mock_chain + .build_transaction(bank_account.id()) + .tx_script(init_tx_script) + .build()?; + + let executed_init = init_tx_context.execute().await?; + mock_chain.add_pending_executed_transaction(&executed_init)?; + mock_chain.prove_next_block()?; + bank_account = mock_chain.committed_account(bank_account.id())?.clone(); + + // Verify initialized flag flipped to 1 + let after = bank_account.storage().get_item(&initialized_slot)?; + assert_eq!( + after[0].as_canonical_u64(), + 1, + "Bank should be initialized after running init tx script" + ); + println!("After init: initialized = {}", after[0].as_canonical_u64()); + + println!("\nInit test passed!"); + Ok(()) +} +``` + +A few things to note in this test: + +- The slot name is `bank_account::bank::initialized` (the namespace is `bank_account`, not `miden_bank_account`). +- The `initialized` value slot has **no schema default**, so it must be seeded via `InitStorageData` or `AccountComponent::from_package` errors with `InitValueNotProvided`. Only the `balances` map slot defaults to empty. +- A `kind = "tx-script"` contract exposes its entry procedure with `#[transaction_script]`. The `build_tx_script_from_package` helper loads it with `TransactionScript::from_package`. + +## Enable the Initialization Guard + +Now that we have the init transaction script, it's time to enable the `require_initialized()` guard that we've had commented out since Part 2. Open `contracts/bank-account/src/lib.rs` and uncomment the guard in both the `deposit()` and `withdraw()` methods: + +**Before (Parts 2-5):** + +```rust + // NOTE: Initialization guard — enabled in Part 6 (Transaction Scripts) + // self.require_initialized(); +``` + +**After (Part 6 onward):** + +```rust + self.require_initialized(); +``` + +With this change, deposits and withdrawals will fail unless the bank has been initialized via the transaction script. This is a critical security measure — it prevents assets from being deposited into an uninitialized bank. + +## Try It: Verify Initialization + +From the project root, run the companion init test to verify the transaction script correctly flips the initialized flag: + +```bash title=">_ Terminal" +cargo test --package integration --test init_test -- --nocapture +``` + +
+Expected output + +```text + Compiling integration v0.1.0 (/path/to/miden-bank/integration) + Finished `test` profile [unoptimized + debuginfo] target(s) + Running tests/init_test.rs + +running 1 test +Before init: initialized = 0 +After init: initialized = 1 + +Init test passed! +test init_test ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored +``` + +
+ +:::tip Expected Output +Your actual output may include additional trace lines from the Miden VM or MockChain. As long as you see the test passing, these can be safely ignored. +::: + +:::tip Troubleshooting +**Account binding errors**: Check that `#[account(bank_account::Bank)]` matches the dependency name and exported component trait. Declare the `bank-account` path under `[dependencies]` and remove legacy `wit` overrides. Rebuild the transaction script with `miden build --release`; the compiler builds its account dependency automatically. + +**"Dependency not found"**: Check that the `bank-account` path dependency in `miden-project.toml` points to the account project. +::: + +:::note Live network bin +The MockChain test above is the source of truth for verifying this flow. The live-network bin (`cargo run --bin initialize`) also runs against a testnet node. It prints the new bank ID and waits while you request native tokens from the testnet faucet, then consumes the funding note and submits the signed initialization transaction. +::: + +## What We've Built So Far + +| Component | Status | Description | +| ----------------------- | ----------- | ----------------------------------------------- | +| `bank-account` | ✅ Complete | Full deposit logic with storage and constraints | +| `deposit-note` | ✅ Complete | Note script that calls deposit method | +| `init-tx-script` | ✅ Complete | Transaction script for initialization | +| `withdraw-request-note` | Not started | Coming in Part 7 | + +## Complete Code for This Part + +
+Click to see the complete init-tx-script code + +```rust title="contracts/init-tx-script/src/lib.rs" +// Do not link against libstd (i.e. anything defined in `std::`) +#![no_std] +#![feature(alloc_error_handler)] + +use miden::*; + +/// Native (active) account this tx-script runs against: the bank-account `Bank` component. +#[account(bank_account::Bank)] +pub struct Wallet; + +/// Initialize Transaction Script +/// +/// This transaction script initializes the bank account, enabling deposits. +/// It must be executed by the bank account owner before any deposits can be made. +/// +/// # Flow +/// 1. Transaction is created with this script attached +/// 2. Script executes in the context of the bank account +/// 3. Calls `account.initialize()` to enable deposits +/// 4. Bank is ready to process deposits after the transaction commits +/// +/// # Arguments +/// * `_arg` - Transaction script argument (unused in this script) +/// * `account` - Mutable reference to the bank account (`Bank` component) +#[tx_script] +fn run(_arg: Word, account: &mut Wallet) { + account.initialize(); +} +``` + +```toml title="contracts/init-tx-script/Cargo.toml" +[package] +name = "init-tx-script" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +miden = "=0.14.0" +``` + +```toml title="contracts/init-tx-script/miden-project.toml" +[package] +name = "init-tx-script" +version = "0.1.0" + +[lib] +kind = "tx-script" +path = "src/lib.rs" +namespace = "miden:base/transaction-script@1.0.0" + +[dependencies] +miden-core = "*" +miden-protocol = "*" +bank-account = { path = "../bank-account" } + +``` + +```toml title="contracts/init-tx-script/.cargo/config.toml" +[build] +target = "wasm32-wasip2" + +[target.wasm32-wasip2] +rustflags = ["--cfg", "miden"] +``` + +
+ +## Key Takeaways + +1. **`#[tx_script]`** marks the entry point with signature `fn run(_arg: Word, account: &mut Wallet)` +2. **`#[account(...)]`** binds a `Wallet` wrapper to the native account's component, enabling direct method calls +3. **Direct account access** - Methods called on the `account` parameter, not via module imports +4. **Authorization** - The account's authentication component controls which transactions can update it; a transaction script alone does not enforce ownership +5. **Deployment pattern** - First state change makes account visible on-chain +6. **Loading a transaction script** - `build_tx_script_from_package` uses `TransactionScript::from_package` to load the compiled entry procedure + +:::tip View Complete Source +See the complete transaction script implementation in [contracts/init-tx-script/src/lib.rs](https://github.com/0xMiden/miden-tutorials/blob/main/examples/miden-bank/contracts/init-tx-script/src/lib.rs). +::: + +## Next Steps + +Now that you understand transaction scripts, let's learn the advanced topic of creating output notes in [Part 7: Creating Output Notes](./output-notes). diff --git a/versioned_docs/version-0.16/builder/tutorials/miden-bank/07-output-notes.md b/versioned_docs/version-0.16/builder/tutorials/miden-bank/07-output-notes.md new file mode 100644 index 00000000..ff47ccb6 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tutorials/miden-bank/07-output-notes.md @@ -0,0 +1,890 @@ +--- +sidebar_position: 7 +title: "Part 7: Creating Output Notes" +description: "Learn how to create output notes programmatically within account methods, including the P2ID (Pay-to-ID) note pattern for sending assets." +--- + +# Part 7: Creating Output Notes + +In this section, you'll learn how to create output notes from within account methods. We'll implement the full withdrawal logic that creates P2ID (Pay-to-ID) notes to send assets back to depositors. + +## What You'll Build in This Part + +By the end of this section, you will have: + +- Created the `withdraw-request-note` note script project +- Implemented the `withdraw()` method with balance validation +- Implemented `create_p2id_note()` for sending assets +- **Verified withdrawals work** via a MockChain test + +## Building on Part 6 + +In Part 6, you created a transaction script for initialization. Now you'll complete the bank by implementing withdrawals that create output notes: + +```text +┌────────────────────────────────────────────────────────────────┐ +│ Complete Bank Flow │ +├────────────────────────────────────────────────────────────────┤ +│ │ +│ Part 6: Initialize │ +│ ┌─────────────────┐ init-tx-script ┌───────────────┐ │ +│ │ Bank (uninit) │ ──────────────────────▶│ Bank (ready) │ │ +│ └─────────────────┘ └───────────────┘ │ +│ │ +│ Part 4: Deposit │ +│ ┌─────────────────┐ deposit-note ┌───────────────┐ │ +│ │ User sends │ ──────────────────────▶│ Balance += X │ │ +│ │ deposit note │ │ Vault += X │ │ +│ └─────────────────┘ └───────────────┘ │ +│ │ +│ Part 7: Withdraw (NEW) │ +│ ┌─────────────────┐ withdraw-request ┌───────────────┐ │ +│ │ User sends │ ──────────────────────▶│ Balance -= X │ │ +│ │ withdraw note │ │ Creates P2ID │ │ +│ └─────────────────┘ │ output note │ │ +│ └───────────────┘ │ +│ │ +└────────────────────────────────────────────────────────────────┘ +``` + +## Output Notes Overview + +When an account needs to send assets to another account, it creates an **output note**. The note travels through the network until the recipient consumes it. + +```text +WITHDRAW FLOW: +┌────────────────┐ ┌────────────────┐ ┌────────────────┐ +│ Bank Account │ creates │ P2ID Note │ consumed │ Depositor │ +│ │ ────────▶│ (with assets) │ ────────▶│ Wallet │ +│ remove_asset() │ │ │ │ receives asset │ +└────────────────┘ └────────────────┘ └────────────────┘ +``` + +## The P2ID Note Pattern + +P2ID (Pay-to-ID) is a standard note pattern in Miden that sends assets to a specific account: + +- **Target account**: Only one account can consume the note +- **Asset transfer**: Assets are transferred on consumption +- **Standard script**: Uses a well-known script from miden-standards + +## Step 1: Complete the Withdraw Method + +In Part 3, we introduced `withdraw()` and `create_p2id_note()` as skeletons. Now we'll complete them with full implementations. + +Replace the existing `withdraw()` method in `impl Bank for BankStorage` in `contracts/bank-account/src/lib.rs`. Keep the other methods: + +```rust title="contracts/bank-account/src/lib.rs" +#[component] +impl Bank for BankStorage { + // ... existing methods (initialize, deposit, get_depositor_balance) ... + + fn withdraw( + &mut self, + withdraw_asset: Asset, + serial_num: Word, + tag: Felt, + note_type: Felt, + ) { + // Ensure the bank is initialized before processing withdrawals + self.require_initialized(); + + // Identify the depositor from the note's sender — this is cryptographically + // bound to the note metadata, so it cannot be spoofed by a malicious caller. + let depositor = active_note::get_sender(); + + // Verify this is a fungible asset — see `deposit()` for the rationale. + assert!( + withdraw_asset.is_fungible(), + "Only fungible assets are supported" + ); + + // Extract the fungible amount from the asset value word + let withdraw_amount = withdraw_asset.value[0]; + + // Derive the balance-map key from the depositor and the asset's faucet. + let key = Word::from([ + depositor.prefix, + depositor.suffix, + withdraw_asset.key[3], // faucet_prefix + withdraw_asset.key[2], // faucet_suffix (+ metadata byte; see `balances` field docs) + ]); + + // Get current balance and validate sufficient funds exist. + let current_balance: Felt = self.balances.get(key); + assert!( + current_balance.as_canonical_u64() >= withdraw_amount.as_canonical_u64(), + "Withdrawal amount exceeds available balance" + ); + + // Update balance: current - withdraw_amount + let new_balance = current_balance - withdraw_amount; + self.balances.set(key, new_balance); + + // Read the P2ID script root from the withdraw-request note's storage (items 10-13). + let storage = active_note::get_storage(); + let script_root = Word::from([storage[10], storage[11], storage[12], storage[13]]); + + // Create a P2ID note to send the requested asset back to the depositor + self.create_p2id_note(serial_num, &withdraw_asset, depositor, tag, note_type, script_root); + } +} +``` + +The withdraw method derives the balance-map key inline by packing `depositor.prefix`, `depositor.suffix`, `withdraw_asset.key[3]`, and `withdraw_asset.key[2]` into a `Word`. In the v0.16 fungible-asset ID layout, `asset.key[3]` is the faucet ID prefix and `asset.key[2]` is the faucet ID suffix with the composition metadata folded into its low byte — so `key[2]` is NOT the raw faucet suffix. `withdraw()` and `deposit()` derive the key the same way so a withdrawal reconstructs the exact key the deposit was recorded under. + +:::danger Critical Security: Balance Validation +Always validate `current_balance >= withdraw_amount` BEFORE subtraction. Miden uses modular field arithmetic - subtracting a larger value silently wraps to a massive positive number! +::: + +## Step 2: How the P2ID Script Root is Supplied + +Instead of hardcoding a version-specific MAST root constant in the bank contract, the P2ID script root is passed through the withdraw-request note's storage (items 10-13). The `withdraw()` method reads it directly from the active note: + +```rust +let storage = active_note::get_storage(); +let script_root = Word::from([storage[10], storage[11], storage[12], storage[13]]); +``` + +This design keeps the bank contract version-agnostic: callers embed the P2ID script root they want to use into the note storage when they create the withdraw-request note. The test obtains the correct value at test time with `P2idNote::script_root()` from the `miden_client` crate. In v0.16 `script_root()` returns a `NoteScriptRoot`, so wrap it in `Word::from(...)` before indexing its felts (see the test below). + +## Step 3: Implement create_p2id_note + +This replaces the `todo!()` placeholder from Part 3. The `#[component]` macro exports only the `Bank` trait methods, so `create_p2id_note` (along with the other private helper `require_initialized`) lives in a plain `impl BankStorage` block, NOT inside `impl Bank for BankStorage`. Replace the existing helper with the implementation below, keeping `require_initialized()` in the same block: + +```rust title="contracts/bank-account/src/lib.rs" +/// Internal helpers that are not part of the component's exported WIT API. +/// +/// The `#[component]` macro exports only the methods of the `Bank` trait, so these +/// inherent methods stay private to the contract. +impl BankStorage { + // ... other helpers (require_initialized) ... + + /// Create a P2ID (Pay-to-ID) note to send assets to a recipient. + /// + /// The P2ID script root is read from the active note's storage by the caller. + fn create_p2id_note( + &mut self, + serial_num: Word, + asset: &Asset, + recipient_id: AccountId, + tag: Felt, + note_type: Felt, + script_root: Word, + ) { + // Convert the passed tag Felt to a Tag and note_type Felt to a NoteType. + // note_type: 1 = Public (stored on-chain), 0 = Private (off-chain) + let tag = Tag::from(tag); + let note_type = NoteType::from(note_type); + + // Compute the recipient hash from serial_num, the P2ID script root, and the + // target account ID [suffix, prefix]. This matches the standard P2ID recipient + // format used by miden-standards. + let recipient = note::build_recipient( + serial_num, + script_root, + vec![ + recipient_id.suffix, + recipient_id.prefix, + ], + ); + + // Create the output note + let note_idx = output_note::create(tag, note_type, recipient); + + // Remove the asset from the bank's vault + native_account::remove_asset(*asset); + + // Add the asset to the output note + output_note::add_asset(*asset, note_idx); + } +} +``` + +### Understanding note::build_recipient() + +| Parameter | Description | +| ------------- | ------------------------------------------ | +| `serial_num` | Unique 4-Felt value preventing note reuse | +| `script_root` | The P2ID script's MAST root digest | +| `storage` | Script storage items (account ID for P2ID) | + +:::warning Array Ordering +Note the order: `suffix` comes before `prefix`. This is the opposite of how `AccountId` fields are typically accessed. See [Common Pitfalls](https://docs.miden.xyz/builder/tutorials/rust-compiler/pitfalls#array-ordering-rustmasm-reversal) for details. +::: + +### Understanding output_note::create() + +| Parameter | Type | Description | +| ----------- | ----------- | -------------------------------- | +| `tag` | `Tag` | Routing information for the note | +| `note_type` | `NoteType` | Public (1) or Private (0) | +| `recipient` | `Recipient` | Who can consume the note | + +## Step 4: Create the Withdraw Request Note Project + +Create the directory structure: + +```bash title=">_ Terminal" +mkdir -p contracts/withdraw-request-note/src +``` + +### Configure the project files + +Like the other contracts in this tutorial, the withdraw-request-note has three project files: `Cargo.toml`, `miden-project.toml`, and `.cargo/config.toml`. + +```toml title="contracts/withdraw-request-note/Cargo.toml" +[package] +name = "withdraw-request-note" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +miden = "=0.14.0" +``` + +```toml title="contracts/withdraw-request-note/miden-project.toml" +[package] +name = "withdraw-request-note" +version = "0.1.0" + +[lib] +kind = "note" +path = "src/lib.rs" +namespace = "miden:withdraw-request-note/miden-withdraw-request-note@0.1.0" + +[dependencies] +miden-core = "*" +miden-protocol = "*" +bank-account = { path = "../bank-account" } + +``` + +```toml title="contracts/withdraw-request-note/.cargo/config.toml" +[build] +target = "wasm32-wasip2" + +[target.wasm32-wasip2] +rustflags = ["--cfg", "miden"] +``` + +The note declares `bank-account` as a path dependency. Compiler 0.10 builds the account package and reads its embedded interface; do not add a separate `wit` override. + +## Step 5: Implement the Withdraw Request Note Script + +```rust title="contracts/withdraw-request-note/src/lib.rs" +// Do not link against libstd (i.e. anything defined in `std::`) +#![no_std] +#![feature(alloc_error_handler)] + +use miden::*; + +/// Native (active) account of this note: exposes the `bank-account` component's +/// `Bank` methods, gathered from the `bank-account` package's generated WIT. +#[account(bank_account::Bank)] +pub struct Wallet; + +/// Withdraw Request Note Script +/// +/// When consumed by the Bank account, this note requests a withdrawal and +/// the bank creates a P2ID note to send assets back to the depositor. +/// +/// # Flow +/// 1. Note is created by a depositor specifying the withdrawal details +/// 2. Bank account consumes this note +/// 3. Note script reads the storage items (asset, serial_num, tag, note_type; script_root is read by the bank itself) +/// 4. Calls `account.withdraw(asset, serial_num, tag, note_type)` +/// 5. Bank identifies the depositor internally via `active_note::get_sender()` — cryptographically bound to this note's metadata, so it cannot be spoofed +/// 6. Bank updates the depositor's balance and creates a P2ID note to send assets back +/// +/// # Note Storage (14 Felts) +/// [0-3]: withdraw asset, encoded as [amount, 0, faucet_suffix(+metadata), faucet_prefix]. +/// Reconstructed into the v0.16 asset ID [0, 0, storage[2], storage[3]] and value +/// [amount, 0, 0, 0]. `storage[2]` carries the faucet suffix with the asset's metadata +/// composition bits in its low byte (host side: `FungibleAsset::to_id_word()[2]`), not the raw +/// suffix — so the bank reconstructs exactly the key the depositor's asset had. +/// [4-7]: serial_num (random/unique per note) +/// [8]: tag (P2ID note tag for routing) +/// [9]: note_type (1 = Public, 0 = Private) +/// [10-13]: P2ID script_root (MAST root of the P2ID note script, Poseidon2-hashed). +/// Consumed by the bank account directly from the active note's storage inside +/// `Bank::withdraw`, so it never appears on the call — this keeps that +/// function within the flat-params limit (≤ 16). +#[note] +struct WithdrawRequestNote; + +#[note] +impl WithdrawRequestNote { + #[note_script] + fn run(self, _arg: Word, account: &mut Wallet) { + // Get the storage items and validate the expected count. + let storage = active_note::get_storage(); + assert!( + storage.len() == 14, + "Withdraw request requires exactly 14 storage items" + ); + + // Asset: reconstruct the v0.16 fungible-asset ID/value from the note storage. + // key = [0, 0, storage[2], storage[3]] where storage[2] = faucet suffix + metadata + // byte (low 8 bits) and storage[3] = faucet prefix. + // value = [amount, 0, 0, 0] + let withdraw_asset = Asset::new( + Word::from([felt!(0), felt!(0), storage[2], storage[3]]), + Word::from([storage[0], felt!(0), felt!(0), felt!(0)]), + ); + + // Serial number: full 4 Felts (random/unique per note) + let serial_num = Word::from([storage[4], storage[5], storage[6], storage[7]]); + + // Tag: single Felt for P2ID note routing + let tag = storage[8]; + + // Note type: 1 = Public, 0 = Private + let note_type = storage[9]; + + // Note: P2ID script root (storage[10..13]) is read by the bank account directly + // from the active note's storage inside `Bank::withdraw`. + + // Call the bank account to withdraw the assets. + // The bank identifies the depositor internally via `active_note::get_sender()`, + // which is cryptographically bound to this note's metadata and cannot be spoofed. + account.withdraw(withdraw_asset, serial_num, tag, note_type); + } +} +``` + +The `#[account(bank_account::Bank)]` macro generates the `Wallet` wrapper from bank-account's WIT, giving the note script a typed `account.withdraw(...)` call. The automatic path dependency in `miden-project.toml` builds bank-account and supplies its compiled interface and procedure roots to the macro. + +### Note Storage Layout + +The withdraw-request-note uses 14 Felt storage items: + +```text +Note Storage (14 Felts): +┌───────┬────────────────┬───────────────────────────────────────────────────┐ +│ Index │ Value │ Description │ +├───────┼────────────────┼───────────────────────────────────────────────────┤ +│ 0 │ amount │ Token amount to withdraw │ +├───────┼────────────────┼───────────────────────────────────────────────────┤ +│ 1 │ 0 │ Reserved (always 0 for fungible) │ +├───────┼────────────────┼───────────────────────────────────────────────────┤ +│ 2 │ faucet_suffix* │ Faucet ID suffix + metadata byte (v0.16 key[2]) │ +├───────┼────────────────┼───────────────────────────────────────────────────┤ +│ 3 │ faucet_prefix │ Faucet ID prefix (identifies asset type) │ +├───────┼────────────────┼───────────────────────────────────────────────────┤ +│ 4-7 │ serial_num │ Unique ID for the output P2ID note (4 Felts) │ +├───────┼────────────────┼───────────────────────────────────────────────────┤ +│ 8 │ tag │ Note routing tag for P2ID note │ +├───────┼────────────────┼───────────────────────────────────────────────────┤ +│ 9 │ note_type │ 1 (Public) or 0 (Private) │ +├───────┼────────────────┼───────────────────────────────────────────────────┤ +│ 10-13 │ script_root │ P2ID script MAST root (Poseidon2-hashed, 4 Felts) │ +└───────┴────────────────┴───────────────────────────────────────────────────┘ +``` + +\*Index 2 is the v0.16 fungible-asset ID's `key[2]`: the faucet ID suffix with the composition metadata folded into its low byte, not the raw suffix. The host side encodes it from `FungibleAsset::new(faucet.id(), amount)?.to_id_word()[2]`. + +:::note Why the Asset is in Inputs +Unlike the deposit note which gets its creation-time assets from `active_note::get_initial_assets()`, the withdraw request note doesn't carry assets. Instead, the asset to withdraw is specified in the note inputs. The bank then withdraws from its own vault based on these inputs. +::: + +## Step 6: Build All Components + +Build the withdrawal note from the workspace root. Its automatic path dependency also rebuilds the bank account: + +```bash title=">_ Terminal" +cd contracts/withdraw-request-note +miden build --release +cd ../.. +``` + +## Try It: Verify Withdrawals Work + +Let's test the complete withdraw flow. This test: + +1. Creates a bank account and initializes it +2. Creates a deposit note and processes it +3. Creates a withdraw-request note with the 14-Felt storage layout +4. Processes the withdrawal and verifies the expected P2ID output note +5. Rejects consumption by another account, then credits the depositor when they consume it + +The test exercises both public and private output notes. MockChain is supplied with the full expected note in either case; on a live network, private note details must reach the recipient separately. + +A few host-side details to note: + +- The bank account's slot names are `bank_account::bank::initialized` and `bank_account::bank::balances`. +- The `initialized` value slot has no schema default, so it MUST be seeded via `InitStorageData` (with a zero `Word` = uninitialized) or `from_package` fails with `InitValueNotProvided`. Only the `balances` map defaults to empty. +- The `build_tx_script_from_package` helper calls `TransactionScript::from_package` to load the procedure marked `#[transaction_script]` from the compiled package. +- Host-side felts use `Felt::new_unchecked`, and the expected output note uses `PartialNoteMetadata` (not `NoteMetadata`). +- The withdraw asset is encoded from `FungibleAsset::new(faucet.id(), withdraw_amount)?.to_id_word()` indices `[2]`/`[3]` so the bank reconstructs the exact asset ID the deposit recorded — NOT from `faucet.id().suffix()/prefix()`. + +```rust title="integration/tests/withdraw_test.rs" +use integration::helpers::{ + build_project_in_dir, build_tx_script_from_package, create_testing_account_from_package, + create_testing_note_from_package, AccountCreationConfig, NoteCreationConfig, +}; + +use miden_client::asset::{Asset, FungibleAsset}; +use miden_client::{ + account::{ + component::{InitStorageData, StorageValueName}, + StorageSlotName, + }, + auth::AuthScheme, + note::{Note, NoteAssets, NoteTag, NoteType, P2idNote, P2idNoteStorage, PartialNoteMetadata}, + transaction::RawOutputNote, + Felt, Word, +}; +use miden_testing::{Auth, MockChain}; +use std::{path::Path, sync::Arc}; + +/// Storage slot names for the bank account component. The `initialized` value slot must be +/// seeded via `InitStorageData` (no schema default); the `balances` map defaults to empty. +fn bank_storage_slots() -> (StorageSlotName, StorageSlotName) { + let initialized_slot = + StorageSlotName::new("bank_account::bank::initialized").expect("Valid slot name"); + let balances_slot = + StorageSlotName::new("bank_account::bank::balances").expect("Valid slot name"); + (initialized_slot, balances_slot) +} + +#[tokio::test] +async fn withdraw_test() -> anyhow::Result<()> { + for note_type in [NoteType::Public, NoteType::Private] { + withdraw_flow(note_type).await?; + } + Ok(()) +} + +async fn withdraw_flow(note_type: NoteType) -> anyhow::Result<()> { + // ********************************************************************************* + // SETUP + // ********************************************************************************* + + // Verify withdrawal through consumption of the resulting P2ID note. + let mut builder = MockChain::builder(); + + // Define the deposit amount + let deposit_amount: u64 = 1000; + + // Create a faucet to mint test assets + let faucet = builder.add_existing_basic_faucet( + Auth::BasicAuth { + auth_scheme: AuthScheme::Falcon512Poseidon2, + }, + "TEST", + deposit_amount, + Some(10), + )?; + + // Create note sender account (the depositor) + let sender = builder.add_existing_wallet_with_assets( + Auth::BasicAuth { + auth_scheme: AuthScheme::Falcon512Poseidon2, + }, + [FungibleAsset::new(faucet.id(), deposit_amount)?.into()], + )?; + + // Build contracts + let bank_package = Arc::new(build_project_in_dir( + Path::new("../contracts/bank-account"), + true, + )?); + let deposit_note_package = Arc::new(build_project_in_dir( + Path::new("../contracts/deposit-note"), + true, + )?); + let init_tx_script_package = Arc::new(build_project_in_dir( + Path::new("../contracts/init-tx-script"), + true, + )?); + + // Create the bank account. The `initialized` value slot has no schema default, so it must + // be seeded (here with a zero Word = uninitialized) or `from_package` errors with + // `InitValueNotProvided`; the `balances` map defaults to empty. + let (initialized_slot, balances_slot) = bank_storage_slots(); + let bank_cfg = AccountCreationConfig { + init_storage_data: { + let mut data = InitStorageData::default(); + data.insert_value( + StorageValueName::from_slot_name(&initialized_slot), + Word::default(), + )?; + data + }, + ..Default::default() + }; + + let mut bank_account = create_testing_account_from_package(bank_package.clone(), bank_cfg)?; + + // ********************************************************************************* + // STEP 1: CRAFT DEPOSIT NOTE + // ********************************************************************************* + + // Create a fungible asset to deposit + let fungible_asset = FungibleAsset::new(faucet.id(), deposit_amount)?; + let note_assets = NoteAssets::new(vec![Asset::Fungible(fungible_asset)])?; + + // Create the deposit note with assets attached + // The sender becomes the depositor + let deposit_note = create_testing_note_from_package( + deposit_note_package.clone(), + sender.id(), + NoteCreationConfig { + assets: note_assets, + ..Default::default() + }, + )?; + + // Add bank account and deposit note to mockchain + builder.add_account(bank_account.clone())?; + builder.add_output_note(RawOutputNote::Full(deposit_note.clone())); + + // ********************************************************************************* + // STEP 2: CRAFT WITHDRAW REQUEST NOTE + // ********************************************************************************* + + let withdraw_amount = deposit_amount / 2; + + // Compute proper P2ID tag for the sender (depositor) who will consume the output note + let p2id_tag = NoteTag::with_account_target(sender.id()); + let p2id_tag_felt = Felt::new_unchecked(p2id_tag.as_u32() as u64); + + println!("Computed P2ID tag for sender: 0x{:08X}", p2id_tag.as_u32()); + + // Random serial number - MUST be unique per note + // In production, this would be generated randomly. For testing, we use fixed values. + let p2id_output_note_serial_num = Word::from([ + Felt::new_unchecked(0x1234567890abcdef), + Felt::new_unchecked(0xfedcba0987654321), + Felt::new_unchecked(0xdeadbeefcafebabe), + Felt::new_unchecked(0x0123456789abcdef), + ]); + + println!("Serial num (random): {:?}", p2id_output_note_serial_num); + + // Note type for the P2ID output note + let note_type_felt = Felt::from(note_type); // Public = 1, Private = 0 + + // Get the P2ID script root (Poseidon2-hashed MAST root). `script_root()` returns + // a `NoteScriptRoot` in v0.16; convert to a `Word` so its felts can be indexed. + let p2id_script_root = Word::from(P2idNote::script_root()); + + // Note storage layout (14 Felts): + // [0-3]: withdraw asset encoded as [amount, 0, asset.key[2] (faucet suffix + metadata byte), asset.key[3] (faucet prefix)] + // [4-7]: serial_num (random/unique per note) + // [8]: tag (P2ID note tag for routing) + // [9]: note_type (1 = Public, 0 = Private) + // [10-13]: P2ID script_root (MAST root for recipient computation) + // In v0.16 the fungible-asset vault key encodes the faucet suffix together with a + // metadata byte at index [2] (and the faucet prefix at [3]). Encode the asset from the + // asset's real key word so the bank reconstructs the same key it deposited under. + let withdraw_asset_key_word = FungibleAsset::new(faucet.id(), withdraw_amount)?.to_id_word(); + let withdraw_request_note_storage = vec![ + // WITHDRAW ASSET ENCODING + Felt::new_unchecked(withdraw_amount), + Felt::new_unchecked(0), + withdraw_asset_key_word[2], + withdraw_asset_key_word[3], + // P2ID OUTPUT NOTE SERIAL NUMBER (random, unique per note) + p2id_output_note_serial_num[0], + p2id_output_note_serial_num[1], + p2id_output_note_serial_num[2], + p2id_output_note_serial_num[3], + // TAG (directly passed, no advice provider needed) + p2id_tag_felt, + // NOTE TYPE (1 = Public, 0 = Private) + note_type_felt, + // P2ID SCRIPT ROOT (4 Felts) + p2id_script_root[0], + p2id_script_root[1], + p2id_script_root[2], + p2id_script_root[3], + ]; + + let withdraw_request_note_package = Arc::new(build_project_in_dir( + Path::new("../contracts/withdraw-request-note"), + true, + )?); + + let withdraw_request_note = create_testing_note_from_package( + withdraw_request_note_package.clone(), + sender.id(), + NoteCreationConfig { + storage: withdraw_request_note_storage, + ..Default::default() + }, + )?; + + builder.add_output_note(RawOutputNote::Full(withdraw_request_note.clone())); + + // ********************************************************************************* + // STEP 3: INITIALIZE THE BANK VIA TX SCRIPT + // ********************************************************************************* + // The bank must be initialized before deposits are accepted. + + // Build the mock chain + let mut mock_chain = builder.build()?; + + let init_tx_script = build_tx_script_from_package(init_tx_script_package.as_ref())?; + + let init_tx_context = mock_chain + .build_transaction(bank_account.id()) + .tx_script(init_tx_script) + .build()?; + + let executed_init = init_tx_context.execute().await?; + mock_chain.add_pending_executed_transaction(&executed_init)?; + mock_chain.prove_next_block()?; + bank_account = mock_chain.committed_account(bank_account.id())?.clone(); + + println!("Bank initialized successfully"); + + // ********************************************************************************* + // STEP 4: MAKE DEPOSIT + // ********************************************************************************* + + // Build the transaction context where bank consumes the deposit note + let deposit_tx_context = mock_chain + .build_transaction(bank_account.id()) + .authenticated_input_note(deposit_note.id()) + .build()?; + + // Execute the transaction + let executed_deposit_transaction = deposit_tx_context.execute().await?; + + // Add the executed transaction to the mockchain and prove + mock_chain.add_pending_executed_transaction(&executed_deposit_transaction)?; + mock_chain.prove_next_block()?; + bank_account = mock_chain.committed_account(bank_account.id())?.clone(); + + println!("Bank deposit successful"); + + // ********************************************************************************* + // STEP 5: MAKE WITHDRAW + // ********************************************************************************* + + // Create expected P2ID output note with the computed tag + let recipient = P2idNoteStorage::new(sender.id()).into_recipient(p2id_output_note_serial_num); + let p2id_output_note_asset = FungibleAsset::new(faucet.id(), withdraw_amount)?; + let p2id_output_note_assets = NoteAssets::new(vec![p2id_output_note_asset.into()])?; + let p2id_output_note_metadata = + PartialNoteMetadata::new(bank_account.id(), note_type).with_tag(p2id_tag); + + println!("Recipient digest: {:?}", recipient.digest().to_hex()); + + let p2id_output_note = Note::new( + p2id_output_note_assets, + p2id_output_note_metadata, + recipient, + ); + + let withdraw_request_tx_context = mock_chain + .build_transaction(bank_account.id()) + .authenticated_input_note(withdraw_request_note.id()) + .expected_output_notes(vec![RawOutputNote::Full(p2id_output_note.clone())]) + .build()?; + + let executed_withdraw_request_transaction = withdraw_request_tx_context.execute().await?; + + mock_chain.add_pending_executed_transaction(&executed_withdraw_request_transaction)?; + mock_chain.prove_next_block()?; + bank_account = mock_chain.committed_account(bank_account.id())?.clone(); + + let remaining = deposit_amount - withdraw_amount; + let asset = FungibleAsset::new(faucet.id(), withdraw_amount)?; + let asset_key = asset.to_id_word(); + let depositor_key = miden_client::account::StorageMapKey::new(Word::from([ + sender.id().prefix().as_felt(), + sender.id().suffix(), + asset_key[3], + asset_key[2], + ])); + let balance = bank_account + .storage() + .get_map_item(&balances_slot, depositor_key)?; + assert_eq!(balance[0].as_canonical_u64(), remaining); + assert_eq!( + u64::from(bank_account.vault().get_balance(asset.id())?), + remaining + ); + assert_eq!( + executed_withdraw_request_transaction + .output_notes() + .num_notes(), + 1 + ); + // MockChain retains only headers for newly committed private notes. Supply their + // full details explicitly, as the recipient would receive them out of band. + let consume_p2id = |account_id| { + let tx = mock_chain.build_transaction(account_id); + if note_type == NoteType::Public { + tx.authenticated_input_note(p2id_output_note.id()) + } else { + tx.unauthenticated_input_note(p2id_output_note.clone()) + } + }; + + // P2ID must reject a consumer other than the depositor. + let error = consume_p2id(bank_account.id()) + .build()? + .execute() + .await + .expect_err("only the depositor may consume the withdrawal note"); + assert!( + format!("{error:?}").contains("FailedAssertion"), + "unexpected failure: {error:?}" + ); + + let sender_balance_before = u64::from(sender.vault().get_balance(asset.id())?); + let executed_receive = consume_p2id(sender.id()).build()?.execute().await?; + mock_chain.add_pending_executed_transaction(&executed_receive)?; + mock_chain.prove_next_block()?; + let sender_after = mock_chain.committed_account(sender.id())?; + assert_eq!( + u64::from(sender_after.vault().get_balance(asset.id())?), + sender_balance_before + withdraw_amount + ); + println!("{note_type:?} withdrawal consumed by depositor! Remaining bank balance: {remaining}"); + + Ok(()) +} +``` + +Run the test from the project root: + +```bash title=">_ Terminal" +cargo test --package integration --test withdraw_test -- --nocapture +``` + +
+Expected output + +```text + Compiling integration v0.1.0 (/path/to/miden-bank/integration) + Finished `test` profile [unoptimized + debuginfo] target(s) + Running tests/withdraw_test.rs + +running 1 test +test withdraw_test ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored +``` + +
+ +:::tip Troubleshooting +**"Insufficient balance for withdrawal"**: Make sure the deposit was processed before attempting withdrawal. + +**"Missing expected output note"**: Verify the P2ID note parameters (tag, serial_num, etc.) match exactly. +::: + +## What We've Built So Far + +| Component | Status | Description | +| ----------------------- | ----------- | ------------------------------------- | +| `bank-account` | ✅ Complete | Full deposit AND withdraw logic | +| `deposit-note` | ✅ Complete | Note script for deposits | +| `withdraw-request-note` | ✅ Complete | Note script for withdrawals | +| `init-tx-script` | ✅ Complete | Transaction script for initialization | + +## Complete Code for This Part + +
+Click to see the complete withdraw-request-note code + +```rust title="contracts/withdraw-request-note/src/lib.rs" +// Do not link against libstd (i.e. anything defined in `std::`) +#![no_std] +#![feature(alloc_error_handler)] + +use miden::*; + +/// Native (active) account of this note: exposes the `bank-account` component's +/// `Bank` methods, gathered from the `bank-account` package's generated WIT. +#[account(bank_account::Bank)] +pub struct Wallet; + +/// Withdraw Request Note Script +/// +/// When consumed by the Bank account, this note requests a withdrawal and +/// the bank creates a P2ID note to send assets back to the depositor. +/// +/// # Note Storage (14 Felts) +/// [0-3]: withdraw asset, encoded as [amount, 0, faucet_suffix(+metadata), faucet_prefix]. +/// Reconstructed into the v0.16 asset ID [0, 0, storage[2], storage[3]] and value +/// [amount, 0, 0, 0]. `storage[2]` carries the faucet suffix with the asset's metadata +/// composition bits in its low byte (host side: `FungibleAsset::to_id_word()[2]`), not the raw +/// suffix — so the bank reconstructs exactly the key the depositor's asset had. +/// [4-7]: serial_num (random/unique per note) +/// [8]: tag (P2ID note tag for routing) +/// [9]: note_type (1 = Public, 0 = Private) +/// [10-13]: P2ID script_root (MAST root of the P2ID note script, Poseidon2-hashed). +/// Consumed by the bank account directly from the active note's storage inside +/// `Bank::withdraw`, so it never appears on the call — this keeps that +/// function within the flat-params limit (≤ 16). +#[note] +struct WithdrawRequestNote; + +#[note] +impl WithdrawRequestNote { + #[note_script] + fn run(self, _arg: Word, account: &mut Wallet) { + // Get the storage items and validate the expected count. + let storage = active_note::get_storage(); + assert!( + storage.len() == 14, + "Withdraw request requires exactly 14 storage items" + ); + + // Asset: reconstruct the v0.16 fungible-asset ID/value from the note storage. + // key = [0, 0, storage[2], storage[3]] where storage[2] = faucet suffix + metadata + // byte (low 8 bits) and storage[3] = faucet prefix. + // value = [amount, 0, 0, 0] + let withdraw_asset = Asset::new( + Word::from([felt!(0), felt!(0), storage[2], storage[3]]), + Word::from([storage[0], felt!(0), felt!(0), felt!(0)]), + ); + + // Serial number: full 4 Felts (random/unique per note) + let serial_num = Word::from([storage[4], storage[5], storage[6], storage[7]]); + + // Tag: single Felt for P2ID note routing + let tag = storage[8]; + + // Note type: 1 = Public, 0 = Private + let note_type = storage[9]; + + // Note: P2ID script root (storage[10..13]) is read by the bank account directly + // from the active note's storage inside `Bank::withdraw`. + + // Call the bank account to withdraw the assets. + // The bank identifies the depositor internally via `active_note::get_sender()`, + // which is cryptographically bound to this note's metadata and cannot be spoofed. + account.withdraw(withdraw_asset, serial_num, tag, note_type); + } +} +``` + +
+ +## Key Takeaways + +1. **`note::build_recipient()`** creates a cryptographic commitment from serial number, script root, and storage items +2. **`output_note::create()`** creates the note with tag, note type, and recipient +3. **`output_note::add_asset()`** attaches assets to the created note +4. **P2ID pattern** uses a standard script with account ID as input +5. **Serial numbers** must be unique to prevent note replay +6. **Array ordering** - P2ID expects `[suffix, prefix, ...]` not `[prefix, suffix, ...]` +7. **Always validate before subtraction** to prevent underflow exploits + +:::tip View Complete Source +See the complete implementation in the [examples/miden-bank](https://github.com/0xMiden/miden-tutorials/tree/main/examples/miden-bank) directory. +::: + +## Next Steps + +Now that you've built all the components, let's see how they work together in [Part 8: Complete Flows](./complete-flows). diff --git a/versioned_docs/version-0.16/builder/tutorials/miden-bank/08-complete-flows.md b/versioned_docs/version-0.16/builder/tutorials/miden-bank/08-complete-flows.md new file mode 100644 index 00000000..a28f4978 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tutorials/miden-bank/08-complete-flows.md @@ -0,0 +1,322 @@ +--- +sidebar_position: 8 +title: "Part 8: Complete Flows" +description: "Walk through end-to-end deposit and withdrawal flows, understanding how all the pieces work together in the banking application." +--- + +# Part 8: Complete Flows + +In this final section, we'll bring everything together and walk through the complete deposit and withdrawal flows, verifying that all the components work as a unified banking system. + +## What You'll Build in This Part + +By the end of this section, you will have: + +- Understood the complete deposit flow from note creation to balance update +- Understood the complete withdraw flow including P2ID note creation +- **Verified the entire system works** with an end-to-end MockChain test +- Completed the Miden Bank tutorial! 🎉 + +## Building on Parts 0-7 + +You've built all the pieces. Now let's see them work together: + +```text +┌────────────────────────────────────────────────────────────────────────┐ +│ COMPLETE BANK SYSTEM │ +├────────────────────────────────────────────────────────────────────────┤ +│ │ +│ Components Built: │ +│ ┌───────────────────────┬───────────────────────────────────┐ │ +│ │ bank-account │ Storage + deposit() + withdraw() │ │ +│ ├───────────────────────┼───────────────────────────────────┤ │ +│ │ deposit-note │ Note script → account.deposit() │ │ +│ ├───────────────────────┼───────────────────────────────────┤ │ +│ │ withdraw-request-note │ Note script → account.withdraw() │ │ +│ ├───────────────────────┼───────────────────────────────────┤ │ +│ │ init-tx-script │ Transaction script → initialize() │ │ +│ └───────────────────────┴───────────────────────────────────┘ │ +│ │ +│ Storage Layout: │ +│ ┌───────────────────────┬──────────────────────────────────┐ │ +│ │ initialized (Value) │ Word: [1, 0, 0, 0] when ready │ │ +│ ├───────────────────────┼──────────────────────────────────┤ │ +│ │ balances (StorageMap) │ Balance per user and asset class │ │ +│ └───────────────────────┴──────────────────────────────────┘ │ +│ │ +└────────────────────────────────────────────────────────────────────────┘ +``` + +## The Complete Deposit Flow + +Let's trace through exactly what happens when a user deposits tokens: + +```text +┌────────────────────────────────────────────────────────────────────────┐ +│ DEPOSIT FLOW │ +├────────────────────────────────────────────────────────────────────────┤ +│ │ +│ 1. USER CREATES DEPOSIT NOTE │ +│ ┌─────────────────────────┐ │ +│ │ Deposit Note │ │ +│ │ sender: User │ │ +│ │ assets: [1000 tokens] │ │ +│ │ script: deposit-note │ │ +│ │ consumed by: Bank │ │ +│ └─────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ 2. BANK CONSUMES THE NOTE │ +│ ┌────────────────────────────────────────────────────┐ │ +│ │ Transaction begins; note script executes │ │ +│ │ Vault is credited when deposit() calls add_asset() │ │ +│ └────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ 3. NOTE SCRIPT CALLS THE BANK COMPONENT │ +│ ┌────────────────────────────────────────────────────────────┐ │ +│ │ depositor = active_note::get_sender() → User's AccountId │ │ +│ │ assets = active_note::get_initial_assets() → [1000 tokens] │ │ +│ │ for asset in assets: │ │ +│ │ account.deposit(depositor, asset) │ │ +│ └────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ 4. DEPOSIT METHOD VALIDATES AND RECEIVES THE ASSET │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ require_initialized() ✓ │ │ +│ │ assert asset.is_fungible() ✓ │ │ +│ │ assert 0 < amount <= MAX_DEPOSIT (1,000,000) ✓ │ │ +│ │ native_account::add_asset(asset) → Credit vault │ │ +│ │ balances[User, asset class] += 1000 → Update ledger │ │ +│ └─────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ 5. TRANSACTION COMMITS │ +│ ┌──────────────────────────────────┐ │ +│ │ Depositor's ledger balance: 1000 │ │ +│ │ Bank vault: +1000 tokens │ │ +│ └──────────────────────────────────┘ │ +│ │ +└────────────────────────────────────────────────────────────────────────┘ +``` + +## The Complete Withdraw Flow + +Now let's trace the withdrawal process: + +```text +┌────────────────────────────────────────────────────────────────────────┐ +│ WITHDRAW FLOW │ +├────────────────────────────────────────────────────────────────────────┤ +│ │ +│ 1. USER CREATES WITHDRAWAL REQUEST NOTE │ +│ ┌───────────────────────────────────────────────────────┐ │ +│ │ Withdrawal Request Note │ │ +│ │ sender: User │ │ +│ │ assets: [] │ │ +│ │ storage (14 Felts): │ │ +│ │ asset (4), serial (4), tag, type, script root (4) │ │ +│ │ requested amount: 500 tokens │ │ +│ │ consumed by: Bank │ │ +│ └───────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ 2. BANK CONSUMES THE REQUEST │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ Note script reads active_note::get_storage() │ │ +│ │ Reconstructs the requested asset from storage[0..4] │ │ +│ │ Calls account.withdraw(asset, serial, tag, type) │ │ +│ └─────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ 3. WITHDRAW METHOD VALIDATES AND UPDATES THE LEDGER │ +│ ┌────────────────────────────────────────────────┐ │ +│ │ require_initialized() ✓ │ │ +│ │ assert asset.is_fungible() ✓ │ │ +│ │ User = active_note::get_sender() │ │ +│ │ current_balance = 1000 │ │ +│ │ assert current_balance >= 500 ✓ │ │ +│ │ balances[User, asset class] = 1000 - 500 = 500 │ │ +│ │ create_p2id_note(...) → Output note │ │ +│ └────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ 4. P2ID NOTE IS CREATED │ +│ ┌──────────────────────────────────────────────────────┐ │ +│ │ script_root = storage[10..14] │ │ +│ │ recipient = note::build_recipient( │ │ +│ │ serial, script_root, [user.suffix, user.prefix]) │ │ +│ │ note_idx = output_note::create(tag, type, recipient) │ │ +│ │ native_account::remove_asset(500 tokens) │ │ +│ │ output_note::add_asset(500 tokens, note_idx) │ │ +│ └──────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ 5. TRANSACTION COMMITS │ +│ ┌───────────────────────────────────────────────────────────┐ │ +│ │ Depositor's ledger balance: 500 │ │ +│ │ Bank vault: -500 tokens │ │ +│ │ Output: public or private P2ID carrying 500 tokens → User │ │ +│ └───────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ 6. USER CONSUMES P2ID IN A SEPARATE TRANSACTION │ +│ ┌──────────────────────────────────────┐ │ +│ │ P2ID recipient check passes for User │ │ +│ │ User's wallet receives 500 tokens │ │ +│ └──────────────────────────────────────┘ │ +│ │ +└────────────────────────────────────────────────────────────────────────┘ +``` + +## Try It: Complete End-to-End Test + +The complete flow is exercised by the three integration test files built up over the previous chapters, which together cover the same `init → deposit → withdraw` story shown in the diagram above: + +- `examples/miden-bank/integration/tests/deposit_test.rs` — introduced in Part 4. Covers the deposit happy path (`deposit_test`) plus rejection tests for excessive deposits, deposits before initialization, and NFTs with zero padding in their value word. +- `examples/miden-bank/integration/tests/init_test.rs` — introduced in Part 6. Exercises the init transaction script (`init_test`) and verifies the `initialized` flag flips from `0` to `1`. +- `examples/miden-bank/integration/tests/withdraw_test.rs` — introduced in Part 7. Runs init + deposit + withdraw end-to-end (`withdraw_test`) for both public and private outputs, rejects consumption by another account, and verifies the depositor receives the withdrawn tokens. + +Run the complete suite from the workspace root: + +```bash title=">_ Terminal" +cargo test --package integration --release -- --nocapture --test-threads=1 +``` + +
+Expected output + +```text + Compiling integration v0.1.0 (/path/to/miden-bank/integration) + Finished `release` profile [optimized] target(s) + Running tests/deposit_test.rs + +running 4 tests +test deposit_test ... ok +test deposit_exceeds_max_should_fail ... ok +test deposit_without_init_should_fail ... ok +test deposit_nft_with_zero_padding_should_fail ... ok + +test result: ok. 4 passed; 0 failed; 0 ignored + + Running tests/init_test.rs + +running 1 test +test init_test ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored + + Running tests/withdraw_test.rs + +running 1 test +test withdraw_test ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored +``` + +
+ +:::note Live network bins +The repository also ships `cargo run --bin initialize` and `cargo run --bin deposit` (under `examples/miden-bank/integration/src/bin/`) for exercising the same flow against a live testnet node. The deposit bin attaches 1,000 native base units, and both binaries wait for actual transaction commitment before reporting success. Testnet charges fees: each binary prints the new account ID and waits for a public native-token P2ID. Request funding for that ID from the testnet faucet while it waits; the helper consumes the funding note before proceeding. The MockChain tests above verify initialization, deposit, and withdrawal without external funding. +::: + +## Summary: All Components + +Here's the complete picture of what you've built: + +| Component | Type | Purpose | +| ----------------------- | ------------------ | --------------------------- | +| `bank-account` | Account Component | Manages balances and vault | +| `deposit-note` | Note Script | Processes incoming deposits | +| `withdraw-request-note` | Note Script | Requests withdrawals | +| `init-tx-script` | Transaction Script | Initializes the bank | + +| Storage Slot | Type | Content | +| ------------- | ------------------------ | ------------------- | +| `initialized` | `StorageValue` | Initialization flag | +| `balances` | `StorageMap` | Depositor balances | + +| API | Purpose | +| ----------------------------------- | --------------------------------- | +| `active_note::get_sender()` | Identify note creator | +| `active_note::get_initial_assets()` | Get creation-time attached assets | +| `active_note::get_storage()` | Get note parameters | +| `native_account::add_asset()` | Receive into vault | +| `native_account::remove_asset()` | Send from vault | +| `output_note::create()` | Create output note | +| `output_note::add_asset()` | Attach assets to note | + +## Key Security Patterns + +Remember these critical patterns from this tutorial: + +:::danger Always Validate Before Subtraction + +```rust +// ❌ DANGEROUS: Silent underflow! +let new_balance = current_balance - withdraw_amount; + +// ✅ SAFE: Validate first +assert!( + current_balance.as_canonical_u64() >= withdraw_amount.as_canonical_u64(), + "Insufficient balance" +); +let new_balance = current_balance - withdraw_amount; +``` + +::: + +:::note Felt Comparison Operators +Direct `Felt` comparisons use canonical integer ordering in the current SDK. Both forms below are valid; this tutorial uses the explicit `u64` form for quantity checks: + +```rust +// Direct comparison of canonical Felt values. +if current_balance < withdraw_amount { ... } + +// Equivalent comparison with explicit integer conversion. +if current_balance.as_canonical_u64() < withdraw_amount.as_canonical_u64() { ... } +``` + +Always perform this check before subtracting. Conversion after modular underflow cannot recover the intended balance. + +::: + +## Congratulations! 🎉 + +You've completed the Miden Bank tutorial! You now understand: + +- ✅ **Account components** with storage (`StorageValue` and `StorageMap`) +- ✅ **Constants and constraints** for business rules +- ✅ **Asset management** with vault operations +- ✅ **Note scripts** for processing incoming notes +- ✅ **Cross-component calls** via generated bindings +- ✅ **Transaction scripts** for owner operations +- ✅ **Output notes** for sending assets (P2ID pattern) +- ✅ **Security patterns** for safe arithmetic + +### Continue Learning + +- **[Testing with MockChain](https://docs.miden.xyz/builder/tutorials/rust-compiler/testing)** - Deep dive into testing patterns +- **[Debugging Guide](https://docs.miden.xyz/builder/tutorials/rust-compiler/debugging)** - Troubleshoot common issues +- **[Common Pitfalls](https://docs.miden.xyz/builder/tutorials/rust-compiler/pitfalls)** - Avoid known gotchas + +### Build More + +Use these patterns to build: + +- Token faucets +- DEX contracts +- NFT marketplaces +- Multi-signature wallets +- And more! + +:::tip View Complete Source +Explore the complete banking application: + +- [All Contracts](https://github.com/0xMiden/miden-tutorials/tree/main/examples/miden-bank/contracts) +- [Integration Tests](https://github.com/0xMiden/miden-tutorials/tree/main/examples/miden-bank/integration/tests) +- [Test Helpers](https://github.com/0xMiden/miden-tutorials/blob/main/examples/miden-bank/integration/src/helpers.rs) + ::: + +Happy building on Miden! 🚀 diff --git a/versioned_docs/version-0.16/builder/tutorials/miden-bank/_category_.json b/versioned_docs/version-0.16/builder/tutorials/miden-bank/_category_.json new file mode 100644 index 00000000..a93e1a4c --- /dev/null +++ b/versioned_docs/version-0.16/builder/tutorials/miden-bank/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Miden Bank", + "position": 1 +} diff --git a/versioned_docs/version-0.16/builder/tutorials/miden-bank/index.md b/versioned_docs/version-0.16/builder/tutorials/miden-bank/index.md new file mode 100644 index 00000000..bf80d823 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tutorials/miden-bank/index.md @@ -0,0 +1,218 @@ +--- +sidebar_position: 4 +title: "Building a Bank with Miden Rust" +description: "Learn Miden Rust compiler fundamentals by building a complete banking application with deposits, withdrawals, and asset management." +--- + +# Building a Bank with Miden Rust + +Welcome to the **Miden Rust Compiler Tutorial**! This hands-on guide teaches you how to build smart contracts on Miden using Rust by walking through a complete banking application. + +## What You'll Build + +You'll create a **banking system** consisting of: + +- **Bank Account Component**: A smart contract that manages depositor balances and vault operations +- **Deposit Note**: A note script that processes deposits into the bank +- **Withdraw Request Note**: A note script that requests withdrawals from the bank +- **Initialization Script**: A transaction script to initialize the bank + +The tutorial includes runnable tests where appropriate — some parts are setup-only or conceptual, with setup tests in Parts 0–2 and transaction tests once the required contracts are in place. + +:::note Version and fee setup +The contracts use stable `miden = "=0.14.0"` and compiler 0.10.0; the native integration harness uses protocol/client 0.16. The companion MockChain tests cover initialization, deposit, deposit rejection, and withdrawal. Live testnet transactions also need native tokens for fees. The live binaries print each new account ID and wait for an externally requested public P2ID funding note, then consume it before proceeding. +::: + +## Tutorial Structure + +This tutorial is designed for hands-on learning. Each part builds on the previous one, and every part includes: + +- **What You'll Build** - Clear objectives for the section +- **Step-by-step code** - Progressively building functionality +- **Verification steps** - Runnable tests, build checks, or code review +- **Complete code** - Full code listing for reference + +### Parts Overview + +| Part | Topic | What You'll Build | +| ---------- | -------------------------------------------------------- | ----------------------------------- | +| **Part 0** | [Project Setup](./00-project-setup.md) | Create project with `miden new` | +| **Part 1** | [Account Components](./01-account-components.md) | Bank struct with storage | +| **Part 2** | [Constants & Constraints](./02-constants-constraints.md) | Business rules and validation | +| **Part 3** | [Asset Management](./03-asset-management.md) | Deposit logic with balance tracking | +| **Part 4** | [Note Scripts](./04-note-scripts.md) | Deposit note for receiving assets | +| **Part 5** | [Cross-Component Calls](./05-cross-component-calls.md) | How bindings enable calls | +| **Part 6** | [Transaction Scripts](./06-transaction-scripts.md) | Initialization script | +| **Part 7** | [Output Notes](./07-output-notes.md) | Withdraw with P2ID output | +| **Part 8** | [Complete Flows](./08-complete-flows.md) | End-to-end verification | + +## Tutorial Cards + +import DocCard from '@theme/DocCard'; +import {useDoc} from '@docusaurus/plugin-content-docs/client'; + +export const BankDocCard = ({item}) => { +const {metadata} = useDoc(); +const sectionPath = metadata.permalink.replace(/\/$/, ''); + return ; +}; + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +## Prerequisites + +Before starting this tutorial, ensure you have: + +- Completed the [Get Started guide](https://docs.miden.xyz/builder/get-started/) (familiarity with `midenup`, `miden new`, basic tooling) +- Basic understanding of Miden concepts (accounts, notes, transactions) +- Rust programming experience + +:::tip No Miden Rust Experience Required +This tutorial assumes no prior experience with the Miden Rust compiler. We'll explain each concept as we encounter it. +::: + +## Concepts Covered + +This tutorial covers the following Miden Rust compiler features: + +| Concept | Description | Part | +| ---------------------------- | ----------------------------------------------------------------- | ---- | +| `#[component]` | Define account components with storage | 1 | +| Storage Types | `StorageValue` for single values, `StorageMap` for key-value data | 1 | +| Constants | Define compile-time business rules | 2 | +| Assertions | Validate conditions and handle errors | 2 | +| Asset Handling | Add and remove assets from account vaults | 3 | +| `#[note]` + `#[note_script]` | Note struct/impl pattern for scripts consumed by accounts | 4 | +| Cross-Component Calls | Call account methods from note scripts | 5 | +| `#[tx_script]` | Transaction scripts for account operations | 6 | +| Output Notes | Create notes programmatically | 7 | + +## Source Code + +The complete source code for this tutorial is available in the [examples/miden-bank](https://github.com/0xMiden/miden-tutorials/tree/main/examples/miden-bank) directory of this repository: + +```bash title=">_ Terminal" +git clone https://github.com/0xMiden/miden-tutorials.git +cd miden-tutorials/examples/miden-bank +``` + +## Supplementary Guides + +These standalone guides complement the tutorial: + +- **[Testing with MockChain](https://docs.miden.xyz/builder/tutorials/rust-compiler/testing)** - Learn to test your contracts +- **[Debugging](https://docs.miden.xyz/builder/tutorials/rust-compiler/debugging)** - Troubleshoot common issues +- **[Common Pitfalls](https://docs.miden.xyz/builder/tutorials/rust-compiler/pitfalls)** - Avoid known gotchas + +## Getting Help + +If you get stuck during this tutorial: + +- Check the [Miden Docs](https://docs.miden.xyz) for detailed technical references +- Join the [Build On Miden](https://t.me/BuildOnMiden) Telegram community for support +- Review the complete code in the [examples/miden-bank](https://github.com/0xMiden/miden-tutorials/tree/main/examples/miden-bank) directory + +Ready to build your first Miden banking application? Let's get started with [Part 0: Project Setup](./00-project-setup.md)! diff --git a/versioned_docs/version-0.16/builder/tutorials/miden_node_setup.md b/versioned_docs/version-0.16/builder/tutorials/miden_node_setup.md new file mode 100644 index 00000000..65bc8c89 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tutorials/miden_node_setup.md @@ -0,0 +1,40 @@ +--- +title: Miden Node Setup +sidebar_position: 2 +--- + +# Miden Node Setup Tutorial + +The v0.16 client tutorials connect to public Miden testnet by default, so no local +node is required. You can also configure them to use your own network. + +## Connecting to the public networks + +The testnet RPC endpoint is: + +```text +https://rpc.testnet.miden.io +``` + +Use `Endpoint::testnet()` in Rust or `MidenClient.createTestnet()` in the web SDK. +The tutorial runner selects testnet by default: + +```bash +yarn tutorials +``` + +Testnet transactions pay fees in the native asset. The examples fund new accounts +from the public faucet before their first transaction. Use fresh local stores and +reassemble MASM sources when migrating from an earlier release. + +## Running a local network + +Running against a local network is optional and only needed for a fully self-hosted setup. The Miden node's own documentation covers standing up a local network end to end — installing the node, bootstrapping genesis, and starting the services: + +- [Local network development](https://docs.miden.xyz/reference/node/local-network-development) + +Network transactions additionally require the **network transaction builder** (`miden-ntx-builder`), the component that executes network notes on an account's behalf. The local-network setup linked above provisions it; a node without the builder will commit network notes but never execute them. + +To use a local network, update the client's RPC endpoint and its address, native +asset, and faucet configuration to match that deployment. The runner's +`TUTORIAL_NETWORK` option accepts only `testnet` and `devnet`, not a local RPC URL. diff --git a/versioned_docs/version-0.16/builder/tutorials/recipes/_category_.json b/versioned_docs/version-0.16/builder/tutorials/recipes/_category_.json new file mode 100644 index 00000000..c5e2a889 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tutorials/recipes/_category_.json @@ -0,0 +1,5 @@ +{ + "label": "Recipes", + "position": 3, + "collapsed": true +} diff --git a/versioned_docs/version-0.16/builder/tutorials/recipes/img/count_copy_fpi_diagram.png b/versioned_docs/version-0.16/builder/tutorials/recipes/img/count_copy_fpi_diagram.png new file mode 100644 index 00000000..f0fd3025 Binary files /dev/null and b/versioned_docs/version-0.16/builder/tutorials/recipes/img/count_copy_fpi_diagram.png differ diff --git a/versioned_docs/version-0.16/builder/tutorials/recipes/img/note_creation_masm.png b/versioned_docs/version-0.16/builder/tutorials/recipes/img/note_creation_masm.png new file mode 100644 index 00000000..9458433b Binary files /dev/null and b/versioned_docs/version-0.16/builder/tutorials/recipes/img/note_creation_masm.png differ diff --git a/versioned_docs/version-0.16/builder/tutorials/recipes/rust/_category_.json b/versioned_docs/version-0.16/builder/tutorials/recipes/rust/_category_.json new file mode 100644 index 00000000..ce988c87 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tutorials/recipes/rust/_category_.json @@ -0,0 +1,5 @@ +{ + "label": "Rust", + "position": 1, + "collapsed": true +} diff --git a/versioned_docs/version-0.16/builder/tutorials/recipes/rust/counter_contract_tutorial.md b/versioned_docs/version-0.16/builder/tutorials/recipes/rust/counter_contract_tutorial.md new file mode 100644 index 00000000..788f6669 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tutorials/recipes/rust/counter_contract_tutorial.md @@ -0,0 +1,579 @@ +--- +title: "Deploying a Counter Contract" +sidebar_position: 4 +--- + +# Deploying a Counter Contract + +_Using the Miden client in Rust to deploy and interact with a custom smart contract on Miden_ + +For toolchain requirements and shared fee helpers, see the [Rust client setup](./index.md#running-the-v016-examples). + +## Overview + +In this tutorial, we will build a simple counter smart contract that maintains a count, deploy it to Miden testnet, and interact with it by incrementing the count. + +Using a script, we will invoke the increment function within the counter contract to update the count. This tutorial provides a foundational understanding of developing and deploying custom smart contracts on Miden. + +## What we'll cover + +- Deploying a custom smart contract on Miden +- Getting up to speed with the basics of Miden assembly +- Calling procedures in an account +- Read-only vs state-changing procedures + +## Prerequisites + +This tutorial assumes you have a basic understanding of Miden assembly. To quickly get up to speed with Miden assembly (MASM), please play around with running basic Miden assembly programs in the [Miden playground](https://0xMiden.github.io/examples/). + +## Step 1: Initialize your repository + +From the parent directory of your `tutorials` clone, create a sibling Cargo project: + +```bash +cargo new miden-counter-contract +cd miden-counter-contract +rustup override set 1.98.1 +cp ../tutorials/rust-client/Cargo.lock Cargo.lock +``` + +Add the following dependencies to your `Cargo.toml` file: + +```toml +[dependencies] +# Clone tutorials next to this Cargo project (see Rust client setup). +rust-client = { path = "../tutorials/rust-client" } +miden-client = { version = "=0.16.0", features = ["testing", "tonic"] } +miden-client-sqlite-store = { version = "=0.16.0", package = "miden-client-sqlite-store" } +miden-protocol = { version = "=0.16.0" } +rand = { version = "0.10" } +tokio = { version = "1.48", features = ["rt-multi-thread", "net", "macros", "fs"] } + +[profile.dev] +opt-level = 2 +``` + +### Set up your `src/main.rs` file + +In the previous section, we explained how to instantiate the Miden client. We reuse that client setup and the shared fee helpers for our counter contract. + +Copy and paste the following code into your `src/main.rs` file: + +```rust no_run +use rand::Rng; +use rust_client::TutorialClientExt; +use std::{path::PathBuf, sync::Arc}; + +use miden_client::{ + ClientError, Word, + account::{ + AccountBuilder, AccountComponent, AccountType, StorageSlot, StorageSlotName, + component::{AccountComponentMetadata, BasicWallet}, + }, + auth::NoAuth, + builder::ClientBuilder, + keystore::FilesystemKeyStore, + rpc::{GrpcClient, VerifyingRpcClient}, + transaction::TransactionRequestBuilder, +}; +use miden_client_sqlite_store::ClientBuilderSqliteExt; +use rust_client::{FeeConfig, TutorialNetwork, fund_account_for_fees}; + +#[tokio::main] +async fn main() -> Result<(), ClientError> { + // Initialize client + let network = TutorialNetwork::from_env()?; + let endpoint = network.endpoint(); + let timeout_ms = 10_000; + let rpc_client = Arc::new(VerifyingRpcClient::new(GrpcClient::new( + &endpoint, timeout_ms, + ))); + + // Initialize keystore + let keystore_path = PathBuf::from("./keystore"); + let keystore = Arc::new(FilesystemKeyStore::new(keystore_path).unwrap()); + + let store_path = PathBuf::from("./store.sqlite3"); + + let mut client = ClientBuilder::new() + .rpc(rpc_client) + .sqlite_store(store_path) + .authenticator(keystore.clone()) + .build() + .await?; + + let sync_summary = client.sync_state().await.unwrap(); + println!("Latest block: {}", sync_summary.block_num); + let fee_config = FeeConfig::from_client(&client, network).await?; + + Ok(()) +} +``` + +_When running the code above, there will be some unused imports, however, we will use these imports later on in the tutorial._ + +**Note**: Running the code above, will generate a `store.sqlite3` file and a `keystore` directory. The Miden client uses the `store.sqlite3` file to keep track of the state of accounts and notes. The `keystore` directory keeps track of private keys used by accounts. Be sure to add both to your `.gitignore`! + +## Step 2: Build the counter contract + +The account and transaction-script sources are already in the repository. We examine them below before compiling them from Rust. + +### Custom Miden smart contract + +Below is our counter contract. It has two exported procedures: `get_count` and `increment_count`. + +At the beginning of the MASM file, we define our imports. In this case, we import +`miden::protocol::active_account`, `miden::protocol::native_account`, and +`miden::core::sys`. + +The `miden::protocol::active_account` and `miden::protocol::native_account` modules contain +procedures for reading and writing contract state. The compile-time expression +`word("miden::tutorials::counter")` derives the named storage slot's ID; `[0..2]` +selects the two elements used by the account storage APIs. + +The import `miden::core::sys` contains a useful procedure for truncating the operand stack at the +end of a procedure. + +#### Here's a breakdown of what the `get_count` procedure does: + +1. Pushes the slot ID prefix and suffix for `miden::tutorials::counter` onto the stack. +2. Calls `active_account::get_item` with the slot ID. +3. Calls `sys::truncate_stack` to truncate the stack to size 16. +4. The value returned from `active_account::get_item` is still on the stack and will be returned + when this procedure is called. + +#### Here's a breakdown of what the `increment_count` procedure does: + +1. Pushes the slot ID prefix and suffix for `miden::tutorials::counter` onto the stack. +2. Calls `active_account::get_item` with the slot ID. +3. Pushes `1` onto the stack. +4. Adds `1` to the count value returned from `active_account::get_item`. +5. Pushes the slot ID prefix and suffix again so we can write the updated count. +6. Calls `native_account::set_item` which saves the incremented count to storage. +7. Drops the old storage word returned by `set_item`, then calls `sys::truncate_stack` to clean up the stack. + +The counter is defined in `masm/accounts/counter.masm`: + +```masm +use miden::protocol::active_account +use miden::protocol::native_account +use miden::core::sys + +# CONSTANTS +# ================================================================================================= + +const COUNTER_SLOT = word("miden::tutorials::counter") + +# PUBLIC INTERFACE +# ================================================================================================= + +#! Returns the current count. +#! +#! Inputs: [pad(16)] +#! Outputs: [count, pad(15)] +#! +#! Invocation: call +@account_procedure +pub proc get_count() -> felt + push.COUNTER_SLOT[0..2] exec.active_account::get_item + # => [[count, 0, 0, 0], pad(16)] + + exec.sys::truncate_stack + # => [count, pad(15)] +end + +#! Increments the current count by one. +#! +#! Inputs: [pad(16)] +#! Outputs: [pad(16)] +#! +#! Invocation: call +@account_procedure +pub proc increment_count() + push.COUNTER_SLOT[0..2] exec.active_account::get_item + # => [[count, 0, 0, 0], pad(16)] + + add.1 + # => [[count + 1, 0, 0, 0], pad(16)] + + push.COUNTER_SLOT[0..2] exec.native_account::set_item + # => [OLD_VALUE, pad(16)] + + dropw + # => [pad(16)] + + exec.sys::truncate_stack + # => [pad(16)] +end +``` + +**Note**: _It's a good habit to add comments below each line of MASM code with the expected stack state. This improves readability and helps with debugging._ + +### Authentication Component + +Accounts require an authentication component. This public counter deliberately uses `NoAuth`, which pays transaction fees from the account's native-asset balance and updates its nonce without verifying a signature. + +This `NoAuth` component allows any user to interact with the smart contract without requiring signature verification. + +### Custom script + +This is a Miden assembly script that will call the `increment_count` procedure during the transaction. + +The Rust code links the counter module as `external_contract::counter_contract`, so the script can call `counter_contract::increment_count` by name. + +The transaction script is defined in `masm/scripts/counter_script.masm`: + +```masm +use external_contract::counter_contract + +#! Increments the counter. +#! +#! Inputs: [ARGS, pad(12)] +#! Outputs: [pad(16)] +#! +#! Where: +#! - ARGS contains unused transaction script arguments. +#! +#! Invocation: dyncall +@transaction_script +pub proc main(args: word) + dropw + # => [pad(16)] + + call.counter_contract::increment_count + # => [pad(16)] +end +``` + +## Step 3: Build the counter smart contract + +To build the counter contract, insert the following code inside `main`, immediately before its final `Ok(())`: + +```rust ignore +// ------------------------------------------------------------------------- +// STEP 1: Create a basic counter contract +// ------------------------------------------------------------------------- +println!("\n[STEP 1] Creating counter contract."); + +// Read the MASM source from the tutorials repository. +let counter_code = std::fs::read_to_string("../tutorials/masm/accounts/counter.masm").unwrap(); + +// Compile the account code into `AccountComponent` with one storage slot. +let counter_slot_name = + StorageSlotName::new("miden::tutorials::counter").expect("valid slot name"); +let component_code = client + .code_builder() + .compile_component_code("external_contract::counter_contract", &counter_code) + .unwrap(); +let counter_component = AccountComponent::new( + component_code, + vec![StorageSlot::with_value( + counter_slot_name.clone(), + Word::default(), + )], + AccountComponentMetadata::new("external_contract::counter_contract"), +) +.unwrap(); + +// Init seed for the counter contract +let mut seed = [0_u8; 32]; +client.rng().fill_bytes(&mut seed); + +// Build the new `Account` with the component +let counter_contract = AccountBuilder::new(seed) + .account_type(AccountType::Public) + .with_component(counter_component.clone()) + .with_component(BasicWallet) + .with_component(NoAuth) + .build() + .unwrap(); + +println!( + "counter_contract commitment: {:?}", + counter_contract.to_commitment() +); +println!("counter_contract id: {:?}", counter_contract.id()); +println!("counter_contract storage: {:?}", counter_contract.storage()); + +client.add_account(&counter_contract, false).await.unwrap(); +fund_account_for_fees(&mut client, counter_contract.id(), &fee_config).await?; +``` + +Run the following command to execute `src/main.rs`: + +```bash +TUTORIAL_NETWORK=testnet cargo run --release +``` + +After the program executes, it prints the initial account commitment, ID, and storage. Abridged output looks like this; generated values vary: + +```text +[STEP 1] Creating counter contract. +counter_contract commitment: Word([...]) +counter_contract id: V1(AccountIdV1 { suffix: ..., prefix: ... }) +counter_contract storage: AccountStorage { slots: [StorageSlot { ... content: Value(Word([0, 0, 0, 0])) }] } +``` + +The funding helper then consumes a native-asset note and waits for confirmation. That first transaction publishes the account without incrementing its counter. + +## Step 4: Incrementing the count + +Now that we have built and funded the counter contract, let's create a transaction request to increment the count: + +Insert the following code after the previous step, still inside `main` and before `Ok(())`: + +```rust ignore +// ------------------------------------------------------------------------- +// STEP 2: Call the Counter Contract with a script +// ------------------------------------------------------------------------- +println!("\n[STEP 2] Call Counter Contract With Script"); + +// Load the MASM script referencing the increment procedure +let script_code = + std::fs::read_to_string("../tutorials/masm/scripts/counter_script.masm").unwrap(); + +// Compile the script with the counter contract code linked as a module +// on the same `CodeBuilder` chain. +let tx_script = client + .code_builder() + .with_linked_module("external_contract::counter_contract", &counter_code) + .unwrap() + .compile_tx_script(&script_code) + .unwrap(); + +// Build a transaction request with the custom script +let tx_increment_request = TransactionRequestBuilder::new() + .custom_script(tx_script) + .build() + .unwrap(); + +// Execute and submit the transaction +let tx_id = client + .submit_tutorial_transaction(counter_contract.id(), tx_increment_request) + .await + .unwrap(); + +println!( + "View transaction on MidenScan: {}/tx/{:?}", + network.explorer_url(), + tx_id +); + +println!( + "Counter contract id: {:?}", + counter_contract.id().to_bech32(network.network_id()) +); + +client.sync_state().await.unwrap(); + +// Retrieve updated contract data to see the incremented counter +let account = client + .get_account(counter_contract.id()) + .await + .unwrap() + .expect("counter contract not found"); +println!( + "counter contract storage: {:?}", + account.storage().get_item(&counter_slot_name) +); +assert_eq!( + account.storage().get_item(&counter_slot_name).unwrap()[0].as_canonical_u64(), + 1, + "the deployed counter must increment from zero to one", +); +``` + +Because this counter uses `NoAuth`, another client can import its public state and execute the same increment script. Public visibility alone does not grant that permission; the account's authentication component does. + +## Summary + +The final `src/main.rs` file should look like this: + +```rust no_run +use rand::Rng; +use rust_client::TutorialClientExt; +use std::{path::PathBuf, sync::Arc}; + +use miden_client::{ + ClientError, Word, + account::{ + AccountBuilder, AccountComponent, AccountType, StorageSlot, StorageSlotName, + component::{AccountComponentMetadata, BasicWallet}, + }, + auth::NoAuth, + builder::ClientBuilder, + keystore::FilesystemKeyStore, + rpc::{GrpcClient, VerifyingRpcClient}, + transaction::TransactionRequestBuilder, +}; +use miden_client_sqlite_store::ClientBuilderSqliteExt; +use rust_client::{FeeConfig, TutorialNetwork, fund_account_for_fees}; + +#[tokio::main] +async fn main() -> Result<(), ClientError> { + // Initialize client + let network = TutorialNetwork::from_env()?; + let endpoint = network.endpoint(); + let timeout_ms = 10_000; + let rpc_client = Arc::new(VerifyingRpcClient::new(GrpcClient::new( + &endpoint, timeout_ms, + ))); + + // Initialize keystore + let keystore_path = PathBuf::from("./keystore"); + let keystore = Arc::new(FilesystemKeyStore::new(keystore_path).unwrap()); + + let store_path = PathBuf::from("./store.sqlite3"); + + let mut client = ClientBuilder::new() + .rpc(rpc_client) + .sqlite_store(store_path) + .authenticator(keystore.clone()) + .build() + .await?; + + let sync_summary = client.sync_state().await.unwrap(); + println!("Latest block: {}", sync_summary.block_num); + let fee_config = FeeConfig::from_client(&client, network).await?; + + // ------------------------------------------------------------------------- + // STEP 1: Create a basic counter contract + // ------------------------------------------------------------------------- + println!("\n[STEP 1] Creating counter contract."); + + // Read the MASM source from the tutorials repository. + let counter_code = std::fs::read_to_string("../tutorials/masm/accounts/counter.masm").unwrap(); + + // Compile the account code into `AccountComponent` with one storage slot. + let counter_slot_name = + StorageSlotName::new("miden::tutorials::counter").expect("valid slot name"); + let component_code = client + .code_builder() + .compile_component_code("external_contract::counter_contract", &counter_code) + .unwrap(); + let counter_component = AccountComponent::new( + component_code, + vec![StorageSlot::with_value( + counter_slot_name.clone(), + Word::default(), + )], + AccountComponentMetadata::new("external_contract::counter_contract"), + ) + .unwrap(); + + // Init seed for the counter contract + let mut seed = [0_u8; 32]; + client.rng().fill_bytes(&mut seed); + + // Build the new `Account` with the component + let counter_contract = AccountBuilder::new(seed) + .account_type(AccountType::Public) + .with_component(counter_component.clone()) + .with_component(BasicWallet) + .with_component(NoAuth) + .build() + .unwrap(); + + println!( + "counter_contract commitment: {:?}", + counter_contract.to_commitment() + ); + println!("counter_contract id: {:?}", counter_contract.id()); + println!("counter_contract storage: {:?}", counter_contract.storage()); + + client.add_account(&counter_contract, false).await.unwrap(); + fund_account_for_fees(&mut client, counter_contract.id(), &fee_config).await?; + + // ------------------------------------------------------------------------- + // STEP 2: Call the Counter Contract with a script + // ------------------------------------------------------------------------- + println!("\n[STEP 2] Call Counter Contract With Script"); + + // Load the MASM script referencing the increment procedure + let script_code = + std::fs::read_to_string("../tutorials/masm/scripts/counter_script.masm").unwrap(); + + // Compile the script with the counter contract code linked as a module + // on the same `CodeBuilder` chain. + let tx_script = client + .code_builder() + .with_linked_module("external_contract::counter_contract", &counter_code) + .unwrap() + .compile_tx_script(&script_code) + .unwrap(); + + // Build a transaction request with the custom script + let tx_increment_request = TransactionRequestBuilder::new() + .custom_script(tx_script) + .build() + .unwrap(); + + // Execute and submit the transaction + let tx_id = client + .submit_tutorial_transaction(counter_contract.id(), tx_increment_request) + .await + .unwrap(); + + println!( + "View transaction on MidenScan: {}/tx/{:?}", + network.explorer_url(), + tx_id + ); + + println!( + "Counter contract id: {:?}", + counter_contract.id().to_bech32(network.network_id()) + ); + + client.sync_state().await.unwrap(); + + // Retrieve updated contract data to see the incremented counter + let account = client + .get_account(counter_contract.id()) + .await + .unwrap() + .expect("counter contract not found"); + println!( + "counter contract storage: {:?}", + account.storage().get_item(&counter_slot_name) + ); + assert_eq!( + account.storage().get_item(&counter_slot_name).unwrap()[0].as_canonical_u64(), + 1, + "the deployed counter must increment from zero to one", + ); + + Ok(()) +} +``` + +Successful output includes the following lines (abridged; generated values vary): + +```text +Latest block: + +[STEP 1] Creating counter contract. +counter_contract commitment: Word([...]) +counter_contract id: V1(AccountIdV1 { suffix: ..., prefix: ... }) +counter_contract storage: AccountStorage { slots: [StorageSlot { ... content: Value(Word([0, 0, 0, 0])) }] } + +[STEP 2] Call Counter Contract With Script +View transaction on MidenScan: https://testnet.midenscan.com/tx/ +Counter contract id: "" +counter contract storage: Ok(Word([1, 0, 0, 0])) +``` + +To increment the contract again without redeploying it, pass the printed testnet account ID to the [public-account interaction tutorial](./public_account_interaction_tutorial.md). Keeping the ID as an input avoids hard-coding an address that becomes invalid after a testnet reset. + +### Running the example + +To run the checked-in example, return to the root of the [tutorials repository](https://github.com/0xMiden/tutorials/) and run: + +```bash +cd rust-client +TUTORIAL_NETWORK=testnet cargo run --release --bin counter_contract_deploy +``` + +### Continue learning + +Next tutorial: [Interacting with Public Smart Contracts](public_account_interaction_tutorial.md) diff --git a/versioned_docs/version-0.16/builder/tutorials/recipes/rust/create_deploy_tutorial.md b/versioned_docs/version-0.16/builder/tutorials/recipes/rust/create_deploy_tutorial.md new file mode 100644 index 00000000..ff446b2c --- /dev/null +++ b/versioned_docs/version-0.16/builder/tutorials/recipes/rust/create_deploy_tutorial.md @@ -0,0 +1,459 @@ +--- +title: "Creating Accounts and Faucets" +sidebar_position: 2 +--- + +# Creating Accounts and Faucets + +_Using the Miden client in Rust to create accounts and deploy faucets_ + +For toolchain requirements and shared fee helpers, see the [Rust client setup](./index.md#running-the-v016-examples). + +## Overview + +In this tutorial, we will create a Miden account for _Alice_ and deploy a fungible faucet. In the next section, we will mint tokens from the faucet to fund her account and transfer tokens from Alice's account to other Miden accounts. + +## What we'll cover + +- Understanding the differences between public and private accounts & notes +- Instantiating the Miden client +- Creating new accounts (public or private) +- Deploying a faucet to fund an account + +## Prerequisites + +The commands in this guide select the public testnet explicitly. If you change the endpoint to a local node, start that node first by following [Miden Node Setup](../../miden_node_setup.md). + +## Public vs. private accounts & notes + +Before diving into coding, let's clarify the concepts of public and private accounts & notes on Miden: + +- Public accounts: The account's data and code are stored on-chain and are openly visible, including its assets. +- Private accounts: Only a commitment to the account state is stored on-chain. The owner keeps the full state locally and may share it with others. +- Public notes: The note's state is visible to anyone - perfect for scenarios where transparency is desired. +- Private notes: The note's state is stored off-chain, you will need to share the note data with the relevant parties (via email or Telegram) for them to be able to consume the note. + +Note: _The term "account" can be used interchangeably with the term "smart contract" since account abstraction on Miden is handled natively._ + +_It is useful to think of notes on Miden as "cryptographic cashier's checks" that allow users to send tokens. Private note details must be shared with the recipient; the chain still records the note commitment and its eventual nullifier._ + +## Step 1: Initialize your repository + +Start in the directory containing your `tutorials` clone and create a sibling Cargo project. The dependency path below assumes the clone is named `tutorials`. + +```bash +cargo new miden-rust-client +cd miden-rust-client +rustup override set 1.98.1 +cp ../tutorials/rust-client/Cargo.lock Cargo.lock +``` + +Keep the generated `[package]` section in `Cargo.toml`, replace its empty `[dependencies]` section with the following, and add the development profile: + +```toml +[dependencies] +# Clone tutorials next to this Cargo project (see Rust client setup). +rust-client = { path = "../tutorials/rust-client" } +miden-client = { version = "=0.16.0", features = ["testing", "tonic"] } +miden-client-sqlite-store = { version = "=0.16.0", package = "miden-client-sqlite-store" } +miden-protocol = { version = "=0.16.0" } +rand = { version = "0.10" } +tokio = { version = "1.48", features = ["rt-multi-thread", "net", "macros", "fs"] } + +[profile.dev] +opt-level = 2 +``` + +## Step 2: Initialize the client + +Before interacting with the Miden network, we must instantiate the client. In this step, we specify several parameters: + +- **RPC endpoint** - The URL of the Miden node you will connect to. +- **Client RNG** - The random number generator used by the client, ensuring that the serial number of newly created notes are unique. +- **SQLite Store** – An SQL database used by the client to store account and note data. +- **Authenticator** - The component responsible for generating transaction signatures. + +Copy and paste the following code into your `src/main.rs` file. + +```rust no_run +use rand::Rng; +use rust_client::TutorialClientExt; +use std::{path::PathBuf, sync::Arc}; +use tokio::time::Duration; + +use miden_client::{ + ClientError, + account::{ + AccountBuilder, AccountId, AccountType, + component::{ + create_singlesig_user_fungible_faucet, BasicWallet, BurnPolicy, FungibleFaucet, + MintPolicy, TokenName, TokenPolicyManager, + }, + }, + asset::{AssetAmount, AssetCallbackFlag, AssetId, FungibleAsset, TokenSymbol}, + auth::{AuthSecretKey, AuthSingleSig}, + builder::ClientBuilder, + keystore::{FilesystemKeyStore, Keystore}, + note::{Note, NoteType, P2idNote}, + rpc::{GrpcClient, VerifyingRpcClient}, + transaction::{PaymentNoteDescription, TransactionRequestBuilder}, +}; +use miden_client_sqlite_store::ClientBuilderSqliteExt; +use miden_protocol::account::AccountIdVersion; +use rust_client::{FeeConfig, TutorialNetwork, fund_account_for_fees}; + +#[tokio::main] +async fn main() -> Result<(), ClientError> { + // Initialize client + let network = TutorialNetwork::from_env()?; + let endpoint = network.endpoint(); + let timeout_ms = 10_000; + let rpc_client = Arc::new(VerifyingRpcClient::new(GrpcClient::new( + &endpoint, timeout_ms, + ))); + + // Initialize keystore + let keystore_path = PathBuf::from("./keystore"); + let keystore = Arc::new(FilesystemKeyStore::new(keystore_path).unwrap()); + + let store_path = PathBuf::from("./store.sqlite3"); + + let mut client = ClientBuilder::new() + .rpc(rpc_client) + .sqlite_store(store_path) + .authenticator(keystore.clone()) + .build() + .await?; + + let sync_summary = client.sync_state().await.unwrap(); + println!("Latest block: {}", sync_summary.block_num); + let fee_config = FeeConfig::from_client(&client, network).await?; + + Ok(()) +} +``` + +_When running the code above, there will be some unused imports, however, we will use these imports later on in the tutorial._ + +**Note**: Running the code above, will generate a `store.sqlite3` file and a `keystore` directory. The Miden client uses the `store.sqlite3` file to keep track of the state of accounts and notes. The `keystore` directory keeps track of private keys used by accounts. Be sure to add both to your `.gitignore`! + +Run the following command to execute `src/main.rs`: + +```bash +TUTORIAL_NETWORK=testnet cargo run --release +``` + +After the program executes, you should see the latest block number printed to the terminal, for example: + +```text +Latest block: +``` + +## Step 3: Creating a wallet + +Now that we've initialized the client, we can create a wallet for Alice. + +To create a wallet for Alice using the Miden client, we select `AccountType::Public` or `AccountType::Private`. A wallet on Miden is simply an account with standardized code. + +In the example below we create a public account for Alice. + +Insert this snippet inside `main()`, immediately before its final `Ok(())`: + +```rust ignore +//------------------------------------------------------------ +// STEP 1: Create a basic wallet for Alice +//------------------------------------------------------------ +println!("\n[STEP 1] Creating a new account for Alice"); + +// Account seed +let mut init_seed = [0_u8; 32]; +client.rng().fill_bytes(&mut init_seed); + +let key_pair = AuthSecretKey::new_falcon512_poseidon2_with_rng(client.rng()); + +// Build the account +let alice_account = AccountBuilder::new(init_seed) + .account_type(AccountType::Public) + .with_component(AuthSingleSig::from_public_key(key_pair.public_key())) + .with_component(BasicWallet) + .build() + .unwrap(); + +// Add the account to the client +client.add_account(&alice_account, false).await?; + +// Add the key pair to the keystore +keystore + .add_key(&key_pair, alice_account.id()) + .await + .unwrap(); + +let alice_account_id_bech32 = alice_account.id().to_bech32(network.network_id()); +println!("Alice's account ID: {:?}", alice_account_id_bech32); + +fund_account_for_fees(&mut client, alice_account.id(), &fee_config).await?; +``` + +## Step 4: Deploying a fungible faucet + +To provide Alice with the tutorial's `MID` asset, we first deploy a faucet. This is separate from the public testnet faucet that supplies the native asset used to pay transaction fees. A faucet account on Miden mints its own fungible token. + +We'll create a public faucet with a token symbol, decimals, and a max supply. Amounts in these examples are raw units: with eight decimals, `100` units means `0.000001 MID`. The faucet's maximum supply is `1_000_000` raw units. We will use it to mint tokens to Alice's account in the next section. + +Insert this snippet inside `main()`, immediately before its final `Ok(())`: + +```rust ignore +//------------------------------------------------------------ +// STEP 2: Deploy a fungible faucet +//------------------------------------------------------------ +println!("\n[STEP 2] Deploying a new fungible faucet."); + +// Faucet seed +let mut init_seed = [0u8; 32]; +client.rng().fill_bytes(&mut init_seed); + +// Faucet parameters +let symbol = TokenSymbol::new("MID").unwrap(); +let decimals = 8; +let max_supply = AssetAmount::new(1_000_000).unwrap(); + +// Generate key pair +let key_pair = AuthSecretKey::new_falcon512_poseidon2_with_rng(client.rng()); + +// Build the faucet account. +// The faucet is a `FungibleFaucet` component plus a `TokenPolicyManager` +// that registers an "allow all" mint (and burn) policy; minting is rejected +// unless an active mint policy is present. +let faucet = FungibleFaucet::builder() + .name(TokenName::new("MID").unwrap()) + .symbol(symbol) + .decimals(decimals) + .max_supply(max_supply) + .build() + .unwrap(); +let policies = TokenPolicyManager::builder() + .active_mint_policy(MintPolicy::allow_all()) + .active_burn_policy(BurnPolicy::allow_all()) + .build(); +// The SDK factory includes BasicWallet so the faucet can receive the native fee asset. +let faucet_account = create_singlesig_user_fungible_faucet( + init_seed, + faucet, + AuthSingleSig::from_public_key(key_pair.public_key()), + policies, + AccountType::Public, +) +.unwrap(); + +// Add the faucet to the client +client.add_account(&faucet_account, false).await?; + +// Add the key pair to the keystore +keystore + .add_key(&key_pair, faucet_account.id()) + .await + .unwrap(); + +let faucet_account_id_bech32 = faucet_account.id().to_bech32(network.network_id()); +println!("Faucet account ID: {:?}", faucet_account_id_bech32); + +fund_account_for_fees(&mut client, faucet_account.id(), &fee_config).await?; + +// Resync to show newly deployed faucet +client.sync_state().await?; +tokio::time::sleep(Duration::from_secs(2)).await; +``` + +`client.add_account` registers each new account locally. `fund_account_for_fees` then consumes a native-asset funding note; this first confirmed transaction deploys the account and gives it a fee balance. + +_When tokens are minted from this faucet, each token batch is represented as a "note" (UTXO). You can think of a Miden Note as a cryptographic cashier's check that has certain spend conditions attached to it._ + +## Summary + +Your updated `main()` function in `src/main.rs` should look like this: + +```rust no_run +use rand::Rng; +use rust_client::TutorialClientExt; +use std::{path::PathBuf, sync::Arc}; +use tokio::time::Duration; + +use miden_client::{ + ClientError, + account::{ + AccountBuilder, AccountId, AccountType, + component::{ + create_singlesig_user_fungible_faucet, BasicWallet, BurnPolicy, FungibleFaucet, + MintPolicy, TokenName, TokenPolicyManager, + }, + }, + asset::{AssetAmount, AssetCallbackFlag, AssetId, FungibleAsset, TokenSymbol}, + auth::{AuthSecretKey, AuthSingleSig}, + builder::ClientBuilder, + keystore::{FilesystemKeyStore, Keystore}, + note::{Note, NoteType, P2idNote}, + rpc::{GrpcClient, VerifyingRpcClient}, + transaction::{PaymentNoteDescription, TransactionRequestBuilder}, +}; +use miden_client_sqlite_store::ClientBuilderSqliteExt; +use miden_protocol::account::AccountIdVersion; +use rust_client::{FeeConfig, TutorialNetwork, fund_account_for_fees}; + +#[tokio::main] +async fn main() -> Result<(), ClientError> { + // Initialize client + let network = TutorialNetwork::from_env()?; + let endpoint = network.endpoint(); + let timeout_ms = 10_000; + let rpc_client = Arc::new(VerifyingRpcClient::new(GrpcClient::new( + &endpoint, timeout_ms, + ))); + + // Initialize keystore + let keystore_path = PathBuf::from("./keystore"); + let keystore = Arc::new(FilesystemKeyStore::new(keystore_path).unwrap()); + + let store_path = PathBuf::from("./store.sqlite3"); + + let mut client = ClientBuilder::new() + .rpc(rpc_client) + .sqlite_store(store_path) + .authenticator(keystore.clone()) + .build() + .await?; + + let sync_summary = client.sync_state().await.unwrap(); + println!("Latest block: {}", sync_summary.block_num); + let fee_config = FeeConfig::from_client(&client, network).await?; + + //------------------------------------------------------------ + // STEP 1: Create a basic wallet for Alice + //------------------------------------------------------------ + println!("\n[STEP 1] Creating a new account for Alice"); + + // Account seed + let mut init_seed = [0_u8; 32]; + client.rng().fill_bytes(&mut init_seed); + + let key_pair = AuthSecretKey::new_falcon512_poseidon2_with_rng(client.rng()); + + // Build the account + let alice_account = AccountBuilder::new(init_seed) + .account_type(AccountType::Public) + .with_component(AuthSingleSig::from_public_key(key_pair.public_key())) + .with_component(BasicWallet) + .build() + .unwrap(); + + // Add the account to the client + client.add_account(&alice_account, false).await?; + + // Add the key pair to the keystore + keystore + .add_key(&key_pair, alice_account.id()) + .await + .unwrap(); + + let alice_account_id_bech32 = alice_account.id().to_bech32(network.network_id()); + println!("Alice's account ID: {:?}", alice_account_id_bech32); + + fund_account_for_fees(&mut client, alice_account.id(), &fee_config).await?; + + //------------------------------------------------------------ + // STEP 2: Deploy a fungible faucet + //------------------------------------------------------------ + println!("\n[STEP 2] Deploying a new fungible faucet."); + + // Faucet seed + let mut init_seed = [0u8; 32]; + client.rng().fill_bytes(&mut init_seed); + + // Faucet parameters + let symbol = TokenSymbol::new("MID").unwrap(); + let decimals = 8; + let max_supply = AssetAmount::new(1_000_000).unwrap(); + + // Generate key pair + let key_pair = AuthSecretKey::new_falcon512_poseidon2_with_rng(client.rng()); + + // Build the faucet account. + // The faucet is a `FungibleFaucet` component plus a `TokenPolicyManager` + // that registers an "allow all" mint (and burn) policy; minting is rejected + // unless an active mint policy is present. + let faucet = FungibleFaucet::builder() + .name(TokenName::new("MID").unwrap()) + .symbol(symbol) + .decimals(decimals) + .max_supply(max_supply) + .build() + .unwrap(); + let policies = TokenPolicyManager::builder() + .active_mint_policy(MintPolicy::allow_all()) + .active_burn_policy(BurnPolicy::allow_all()) + .build(); + // The SDK factory includes BasicWallet so the faucet can receive the native fee asset. + let faucet_account = create_singlesig_user_fungible_faucet( + init_seed, + faucet, + AuthSingleSig::from_public_key(key_pair.public_key()), + policies, + AccountType::Public, + ) + .unwrap(); + + // Add the faucet to the client + client.add_account(&faucet_account, false).await?; + + // Add the key pair to the keystore + keystore + .add_key(&key_pair, faucet_account.id()) + .await + .unwrap(); + + let faucet_account_id_bech32 = faucet_account.id().to_bech32(network.network_id()); + println!("Faucet account ID: {:?}", faucet_account_id_bech32); + + fund_account_for_fees(&mut client, faucet_account.id(), &fee_config).await?; + + // Resync to show newly deployed faucet + client.sync_state().await?; + tokio::time::sleep(Duration::from_secs(2)).await; + + Ok(()) +} +``` + +Let's run the `src/main.rs` program again: + +```bash +TUTORIAL_NETWORK=testnet cargo run --release +``` + +The following is an abbreviated output; account IDs and block numbers vary, and the funding helper also prints transaction confirmations: + +```text +Latest block: + +[STEP 1] Creating a new account for Alice +Alice's account ID: "" + +[STEP 2] Deploying a new fungible faucet. +Faucet account ID: "" +``` + +In this section we explained how to instantiate the Miden client, create a wallet account, and deploy a faucet. + +In the next section we will cover how to mint tokens from the faucet, consume notes, and send tokens to other accounts. + +### Running the example + +From the root of your `tutorials` clone, run the checked-in example: + +```bash +cd rust-client +TUTORIAL_NETWORK=testnet cargo run --release --bin create_mint_consume_send +``` + +### Continue learning + +Next tutorial: [Mint, Consume, and Create Notes](mint_consume_create_tutorial.md) diff --git a/versioned_docs/version-0.16/builder/tutorials/recipes/rust/creating_notes_in_masm_tutorial.md b/versioned_docs/version-0.16/builder/tutorials/recipes/rust/creating_notes_in_masm_tutorial.md new file mode 100644 index 00000000..29fad3b7 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tutorials/recipes/rust/creating_notes_in_masm_tutorial.md @@ -0,0 +1,616 @@ +--- +title: "How to Create Notes in Miden Assembly" +sidebar_position: 11 +--- + +# Creating Notes in Miden Assembly + +_Creating notes inside the MidenVM using Miden assembly_ + +For toolchain requirements and shared fee helpers, see the [Rust client setup](./index.md#running-the-v016-examples). + +## Overview + +In this tutorial, we will create a custom note that generates a copy of itself when it is consumed by an account. The purpose of this tutorial is to demonstrate how to create notes inside the MidenVM using Miden assembly (MASM). By the end of this tutorial, you will understand how to write MASM code that creates notes. + +## What We'll Cover + +- Computing the note storage commitment and recipient in MASM +- Creating notes in MASM + +## Prerequisites + +This tutorial assumes you have a basic understanding of Miden assembly and that you have completed the tutorial on [creating a custom note](./custom_note_how_to.md). + +## Why Creating Notes in MASM Is Useful + +Being able to create a note in MASM enables you to build various types of applications. Creating a note during the consumption of another note or from an account allows you to develop complex DeFi applications. + +Here are some tangible examples of when creating a note in MASM is useful in a DeFi context: + +- Creating notes that record selected values from an account's state +- Representing partially fillable buy/sell orders as notes (SWAPP) +- Handling withdrawals from a smart contract + +## What We Will Be Building + +![Iterative Note Creation](../img/note_creation_masm.png) + +In the diagram above, note A is consumed by an account, and during the transaction, note A' is created. + +In this tutorial, Alice creates a note containing 100 raw units of a fungible asset. Bob consumes it, keeps 50 units, and creates a successor note with the other 50, the same script and storage, and an incremented serial number. The script does not restrict consumption to a particular account. + +This example requires exactly one fungible asset with a positive even amount and performs one split, from 100 to 50. MASM `div` is field division, so this script does not implement rounding for odd integer amounts and should not be used as an arbitrary repeated-halving contract. + +## Step 1: Initialize Your Repository + +Start in the directory containing your `tutorials` clone and create a sibling Cargo project. The dependency path below assumes the clone is named `tutorials`. + +```bash +cargo new miden-project +cd miden-project +rustup override set 1.98.1 +cp ../tutorials/rust-client/Cargo.lock Cargo.lock +``` + +Keep the generated `[package]` section in `Cargo.toml`, replace its empty `[dependencies]` section with the following, and add the development profile: + +```toml +[dependencies] +# Clone tutorials next to this Cargo project (see Rust client setup). +rust-client = { path = "../tutorials/rust-client" } +miden-client = { version = "=0.16.0", features = ["testing", "tonic"] } +miden-client-sqlite-store = { version = "=0.16.0", package = "miden-client-sqlite-store" } +miden-protocol = { version = "=0.16.0" } +rand = { version = "0.10" } +tokio = { version = "1.48", features = ["rt-multi-thread", "net", "macros", "fs"] } + +[profile.dev] +opt-level = 2 +``` + +## Step 2: Write the Note Script + +The note script is in `masm/notes/iterative_output_note.masm`. Note scripts are compiled as libraries; the `@note_script` attribute marks the entrypoint procedure. + +```masm +use miden::protocol::active_note +use miden::protocol::note +use miden::core::sys +use miden::standards::wallets::basic as wallet +use miden::standards::note::note_creator + +# CONSTANTS +# ================================================================================================= + +# get_initial_assets writes the eight-felt asset as ASSET_ID followed by ASSET_VALUE +const ASSET_ID_PTR = 0 +const ASSET_VALUE_PTR = 4 +const ASSET_HALF_VALUE_PTR = 8 +const ACCOUNT_ID_PREFIX = 12 # storage: [prefix, suffix, tag, 0] +const TAG = 14 # ACCOUNT_ID_PREFIX + 2 + +# PUBLIC INTERFACE +# ================================================================================================= + +#! Receives this note's assets and creates a successor with half its fungible amount. +#! +#! This example expects exactly one fungible asset with a positive, even amount. Field division +#! by two is not integer rounding, so an odd amount does not produce a valid half-amount transfer. +#! Any account exposing the wallet and note-creator procedures may consume the note; the account +#! ID in storage is copied into the successor's storage and does not restrict consumption. +#! +#! Inputs: [ARGS, pad(12)] +#! Outputs: [pad(16)] +#! +#! Where: +#! - ARGS contains unused note arguments. +#! - note storage contains a copied account ID and the successor's note tag. +#! +#! Panics if: +#! - the account cannot receive the note's assets or move the computed half amount to the successor. +#! +#! Invocation: dyncall +@note_script +pub proc main(args: word) + # discard the unused note arguments + dropw + # => [pad(16)] + + # get asset contained in note into memory (ASSET_ID at 0, ASSET_VALUE at 4) + # get_initial_assets leaves [num_assets] on the stack; drop it. + push.ASSET_ID_PTR exec.active_note::get_initial_assets drop + # => [pad(16)] + + # load ASSET_VALUE and compute half amount + padw push.ASSET_VALUE_PTR mem_loadw_le + # => [[amount, 0, 0, 0], pad(16)] + + # halve the even fungible amount + push.2 div + # => [[amount / 2, 0, 0, 0], pad(16)] + + # store as ASSET_HALF_VALUE + mem_storew_le.ASSET_HALF_VALUE_PTR dropw + # => [pad(16)] + + # receive all assets from note into the account wallet + exec.wallet::move_note_assets_to_account + # => [pad(16)] + + # push script hash + exec.active_note::get_script_root + # => [SCRIPT_ROOT, pad(16)] + + # get the current note serial number + exec.active_note::get_serial_number + # => [SERIAL_NUM, SCRIPT_ROOT, pad(16)] + + # increment the last element of the serial number by 1 + # (serial_num[3] is at depth 3; matches Rust: serial_num[3] + 1) + swap.3 push.1 add swap.3 + # => [NEXT_SERIAL_NUM, SCRIPT_ROOT, pad(16)] + + # load note storage into memory for recipient construction + push.ACCOUNT_ID_PREFIX + exec.active_note::get_storage + # => [num_storage_items, NEXT_SERIAL_NUM, SCRIPT_ROOT, pad(16)] + + push.ACCOUNT_ID_PREFIX + # => [storage_ptr, num_storage_items, NEXT_SERIAL_NUM, SCRIPT_ROOT, pad(16)] + + # argument shape: [storage_ptr, num_storage_items, SERIAL_NUM, SCRIPT_ROOT]. + exec.note::compute_and_store_recipient + # => [RECIPIENT, pad(16)] + + # push note type to stack (public note = 1) + push.1 + # => [note_type, RECIPIENT, pad(16)] + + # load tag from memory + mem_load.TAG + # => [tag, note_type, RECIPIENT, pad(16)] + + # note creation from a note script must call the account's note-creator procedure. + # pad the stack for the account procedure call convention. + push.0 movdn.6 push.0 movdn.6 padw padw swapdw + # => [tag, note_type, RECIPIENT, pad(26)] + + call.note_creator::create_note + # => [note_idx, pad(31)] + + movdn.15 dropw dropw dropw drop drop drop + # => [note_idx, pad(16)] + + # build [ASSET_ID, ASSET_HALF_VALUE, note_idx] for move_asset_to_note + # inputs: [ASSET_ID, ASSET_VALUE, note_idx, pad(7)] + + # push ASSET_HALF_VALUE (note_idx moves to depth 4) + padw push.ASSET_HALF_VALUE_PTR mem_loadw_le + # => [ASSET_HALF_VALUE, note_idx, pad(16)] + + # push ASSET_ID (ASSET_HALF_VALUE moves to depth 4, note_idx to depth 8) + padw push.ASSET_ID_PTR mem_loadw_le + # => [ASSET_ID, ASSET_HALF_VALUE, note_idx, pad(16)] + + call.wallet::move_asset_to_note + # => [pad(25)] + + dropw dropw dropw dropw + # => [pad(16)] + + exec.sys::truncate_stack + # => [pad(16)] +end +``` + +### How the Assembly Code Works: + +1. **Retrieving the asset:** + The note calls `active_note::get_initial_assets` to copy the initial asset into memory, with `ASSET_ID` at address 0 and `ASSET_VALUE` at address 4. It halves the amount in `ASSET_VALUE` and stores it at `ASSET_HALF_VALUE_PTR`. Finally, it calls `wallet::move_note_assets_to_account`, which explicitly removes the assets from the note and receives them into the consuming account. +2. **Getting the script hash and serial number:** + The note script calls `active_note::get_script_root` to fetch the script hash and `active_note::get_serial_number` to fetch the current serial number, then increments element 3 (the last element) by 1 to avoid duplicate recipients. +3. **Building the `RECIPIENT`:** + The script loads the note storage into memory with `active_note::get_storage`, then calls `note::compute_and_store_recipient`. This computes the storage commitment and stores the preimage in the advice map, which is required for public notes. +4. **Creating the note:** + To create the note from a note script, the script pads the stack for the account-call ABI and calls the account's exported `note_creator::create_note` procedure, which enters the account context and returns the note index. The consuming account must expose `NoteCreator`; `BasicWallet` includes it. +5. **Moving assets to the note:** + After the note is created, the script loads `ASSET_ID` and `ASSET_HALF_VALUE` from memory onto the stack and calls `wallet::move_asset_to_note` with the note index. +6. **Stack cleanup:** + Finally, the script cleans up the stack by calling `sys::truncate_stack`. + +## Step 3: Rust Program + +With the Miden assembly note script written, we can move on to writing the Rust script to create and consume the note. + +Copy and paste the following code into your `src/main.rs` file. + +```rust no_run +use rand::Rng; +use rust_client::TutorialClientExt; +use std::{path::PathBuf, sync::Arc}; +use tokio::time::{Duration, sleep}; + +use miden_client::{ + Client, ClientError, Felt, + account::{ + Account, AccountBuilder, AccountType, + component::{ + create_singlesig_user_fungible_faucet, BasicWallet, BurnPolicy, FungibleFaucet, + MintPolicy, TokenName, TokenPolicyManager, + }, + }, + address::NetworkId, + asset::{AssetAmount, AssetId, FungibleAsset, TokenSymbol}, + auth::{AuthSecretKey, AuthSingleSig}, + builder::ClientBuilder, + crypto::FeltRng, + keystore::{FilesystemKeyStore, Keystore}, + note::{ + Note, NoteAssets, NoteDetails, NoteRecipient, NoteStorage, NoteTag, NoteType, + PartialNoteMetadata, + }, + rpc::{GrpcClient, VerifyingRpcClient}, + transaction::{TransactionId, TransactionRequestBuilder}, +}; +use miden_client_sqlite_store::ClientBuilderSqliteExt; +use rust_client::{FeeConfig, TutorialNetwork, fund_account_for_fees}; + +// Helper to create a basic account +async fn create_basic_account( + client: &mut Client, + keystore: &Arc, +) -> Result { + let mut init_seed = [0u8; 32]; + client.rng().fill_bytes(&mut init_seed); + + let key_pair = AuthSecretKey::new_falcon512_poseidon2_with_rng(client.rng()); + + let account = AccountBuilder::new(init_seed) + .account_type(AccountType::Public) + .with_component(AuthSingleSig::from_public_key(key_pair.public_key())) + .with_component(BasicWallet) + .build() + .unwrap(); + + client.add_account(&account, false).await?; + keystore.add_key(&key_pair, account.id()).await.unwrap(); + + Ok(account) +} + +async fn create_basic_faucet( + client: &mut Client, + keystore: &Arc, +) -> Result { + let mut init_seed = [0u8; 32]; + client.rng().fill_bytes(&mut init_seed); + + let key_pair = AuthSecretKey::new_falcon512_poseidon2_with_rng(client.rng()); + let symbol = TokenSymbol::new("MID").unwrap(); + let decimals = 8; + let max_supply = AssetAmount::new(1_000_000).unwrap(); + + let faucet = FungibleFaucet::builder() + .name(TokenName::new("MID").unwrap()) + .symbol(symbol) + .decimals(decimals) + .max_supply(max_supply) + .build() + .unwrap(); + let policies = TokenPolicyManager::builder() + .active_mint_policy(MintPolicy::allow_all()) + .active_burn_policy(BurnPolicy::allow_all()) + .build(); + let account = create_singlesig_user_fungible_faucet( + init_seed, + faucet, + AuthSingleSig::from_public_key(key_pair.public_key()), + policies, + AccountType::Public, + ) + .unwrap(); + + client.add_account(&account, false).await?; + keystore.add_key(&key_pair, account.id()).await.unwrap(); + + Ok(account) +} + +// Helper to wait until an account has the expected number of consumable notes +async fn wait_for_notes( + client: &mut Client, + account_id: &Account, + expected: usize, + network_id: NetworkId, +) -> Result<(), ClientError> { + for _ in 0..24 { + client.sync_state().await?; + let notes = client + .get_consumable_tutorial_notes(Some(account_id.id())) + .await?; + if notes.len() >= expected { + return Ok(()); + } + println!( + "{} consumable notes found for account {}. Waiting...", + notes.len(), + account_id.id().to_bech32(network_id.clone()) + ); + sleep(Duration::from_secs(3)).await; + } + Err(ClientError::Observer(Box::new(std::io::Error::other( + format!( + "timed out waiting for {expected} tutorial notes for {}", + account_id.id() + ), + )))) +} + +/// Waits for a specific transaction to be committed. +async fn wait_for_tx( + client: &mut Client, + tx_id: TransactionId, +) -> Result<(), ClientError> { + rust_client::wait_for_transaction(client, tx_id).await +} + +#[tokio::main] +async fn main() -> Result<(), ClientError> { + // Initialize client + let network = TutorialNetwork::from_env()?; + let endpoint = network.endpoint(); + let timeout_ms = 10_000; + let rpc_client = Arc::new(VerifyingRpcClient::new(GrpcClient::new( + &endpoint, timeout_ms, + ))); + + // Initialize keystore + let keystore_path = PathBuf::from("./keystore"); + let keystore = Arc::new(FilesystemKeyStore::new(keystore_path).unwrap()); + + let store_path = PathBuf::from("./store.sqlite3"); + + let mut client = ClientBuilder::new() + .rpc(rpc_client) + .sqlite_store(store_path) + .authenticator(keystore.clone()) + .build() + .await?; + + let sync_summary = client.sync_state().await.unwrap(); + println!("Latest block: {}", sync_summary.block_num); + let fee_config = FeeConfig::from_client(&client, network).await?; + + // ------------------------------------------------------------------------- + // STEP 1: Create accounts and deploy faucet + // ------------------------------------------------------------------------- + println!("\n[STEP 1] Creating new accounts"); + let alice_account = create_basic_account(&mut client, &keystore).await?; + println!( + "Alice's account ID: {:?}", + alice_account.id().to_bech32(network.network_id()) + ); + let bob_account = create_basic_account(&mut client, &keystore).await?; + println!( + "Bob's account ID: {:?}", + bob_account.id().to_bech32(network.network_id()) + ); + + println!("\nDeploying a new fungible faucet."); + let faucet = create_basic_faucet(&mut client, &keystore).await?; + println!( + "Faucet account ID: {:?}", + faucet.id().to_bech32(network.network_id()) + ); + for account_id in [alice_account.id(), bob_account.id(), faucet.id()] { + fund_account_for_fees(&mut client, account_id, &fee_config).await?; + } + client.sync_state().await?; + + // ------------------------------------------------------------------------- + // STEP 2: Mint tokens with P2ID + // ------------------------------------------------------------------------- + println!("\n[STEP 2] Mint tokens with P2ID"); + let faucet_id = faucet.id(); + let amount: u64 = 100; + let mint_amount = FungibleAsset::new(faucet_id, amount).unwrap(); + + let tx_req = TransactionRequestBuilder::new() + .build_mint_fungible_asset( + mint_amount, + alice_account.id(), + NoteType::Public, + client.rng(), + ) + .unwrap(); + + let tx_id = client + .submit_tutorial_transaction(faucet.id(), tx_req) + .await?; + println!("Minted tokens. TX: {:?}", tx_id); + + wait_for_notes(&mut client, &alice_account, 1, network.network_id()).await?; + + // Consume the minted note + let consumable_notes = client + .get_consumable_tutorial_notes(Some(alice_account.id())) + .await?; + + if let Some((note_record, _)) = consumable_notes.first() { + let note: Note = note_record.clone().try_into()?; + let consume_req = TransactionRequestBuilder::new().build_consume_notes(vec![note])?; + + let tx_id = client + .submit_tutorial_transaction(alice_account.id(), consume_req) + .await?; + println!("Consumed minted note. TX: {:?}", tx_id); + } + + client.sync_state().await?; + + // ------------------------------------------------------------------------- + // STEP 3: Create iterative output note + // ------------------------------------------------------------------------- + println!("\n[STEP 3] Create iterative output note"); + + // Read the MASM source from the tutorials repository. + let code = + std::fs::read_to_string("../tutorials/masm/notes/iterative_output_note.masm").unwrap(); + let serial_num = client.rng().draw_word(); + + // Create note metadata and tag + let tag = NoteTag::new(0); + let metadata = PartialNoteMetadata::new(alice_account.id(), NoteType::Public).with_tag(tag); + let note_script = client.code_builder().compile_note_script(&code).unwrap(); + let note_storage = NoteStorage::new(vec![ + alice_account.id().prefix().as_felt(), + alice_account.id().suffix(), + tag.into(), + Felt::new_unchecked(0), + ]) + .unwrap(); + + let recipient = NoteRecipient::new(serial_num, note_script.clone(), note_storage.clone()); + let vault = NoteAssets::new(vec![mint_amount.into()])?; + let custom_note = Note::new(vault, metadata, recipient); + + let note_req = TransactionRequestBuilder::new() + .own_output_notes(vec![custom_note.clone()]) + .build() + .unwrap(); + + let tx_id = client + .submit_tutorial_transaction(alice_account.id(), note_req) + .await?; + println!( + "View transaction on MidenScan: {}/tx/{:?}", + network.explorer_url(), + tx_id + ); + + client.sync_state().await?; + + // ------------------------------------------------------------------------- + // STEP 4: Consume the iterative output note + // ------------------------------------------------------------------------- + println!("\n[STEP 4] Bob consumes the note and creates a copy"); + + // Increment the serial number for the new note + let serial_num_1 = [ + serial_num[0], + serial_num[1], + serial_num[2], + serial_num[3] + Felt::new_unchecked(1), + ] + .into(); + + // Reuse the note_script and note_storage + let recipient = NoteRecipient::new(serial_num_1, note_script, note_storage); + + // Note: Change metadata to include Bob's account as the creator + let metadata = PartialNoteMetadata::new(bob_account.id(), NoteType::Public).with_tag(tag); + + let asset_amount_1 = FungibleAsset::new(faucet_id, 50).unwrap(); + let vault = NoteAssets::new(vec![asset_amount_1.into()])?; + let output_note = Note::new(vault, metadata, recipient); + + let consume_custom_req = TransactionRequestBuilder::new() + .input_notes([(custom_note, None)]) + .expected_future_notes(vec![ + ( + NoteDetails::from(output_note.clone()), + output_note.metadata().tag(), + ) + .clone(), + ]) + .expected_output_recipients(vec![output_note.recipient().clone()]) + .build() + .unwrap(); + + let tx_id = client + .submit_tutorial_transaction(bob_account.id(), consume_custom_req) + .await?; + println!( + "Consumed Note Tx on MidenScan: {}/tx/{:?}", + network.explorer_url(), + tx_id + ); + + wait_for_tx(&mut client, tx_id).await?; + + // The SDK verifies expected recipients; also check the actual successor's assets and metadata. + let successor = client + .get_output_note(output_note.id()) + .await? + .expect("the transaction must create the expected successor note"); + assert!(successor.is_committed(), "the successor must be committed"); + assert_eq!(successor.assets(), output_note.assets()); + assert_eq!(successor.metadata(), output_note.metadata()); + println!( + "Successor note committed with 50 tokens: {}", + successor.id() + ); + + let bob = client + .get_account(bob_account.id()) + .await? + .expect("Bob's account must exist after consuming the note"); + let balance = bob.vault().get_balance(AssetId::new_fungible(faucet_id))?; + assert_eq!( + balance.as_u64(), + 50, + "Bob must retain the other half of the note's tokens", + ); + println!("Bob's retained token balance: {balance}"); + + Ok(()) +} +``` + +Run the following command to execute `src/main.rs`: + +```bash +TUTORIAL_NETWORK=testnet cargo run --release +``` + +The following is an abbreviated output; IDs vary, and funding and repeated confirmation messages are omitted: + +```text +Latest block: + +[STEP 1] Creating new accounts +Alice's account ID: "" +Bob's account ID: "" + +Deploying a new fungible faucet. +Faucet account ID: "" + +[STEP 2] Mint tokens with P2ID +Minted tokens. TX: +Consumed minted note. TX: + +[STEP 3] Create iterative output note +View transaction on MidenScan: https://testnet.midenscan.com/tx/ + +[STEP 4] Bob consumes the note and creates a copy +Consumed Note Tx on MidenScan: https://testnet.midenscan.com/tx/ +Transaction committed: +Successor note committed with 50 tokens: +Bob's retained token balance: 50 +``` + +--- + +### Running the example + +From the root of your `tutorials` clone, run the checked-in example: + +```bash +cd rust-client +TUTORIAL_NETWORK=testnet cargo run --release --bin note_creation_in_masm +``` + +### Continue learning + +Next tutorial: [Delegated Proving](./delegated_proving_tutorial.md) diff --git a/versioned_docs/version-0.16/builder/tutorials/recipes/rust/custom_note_how_to.md b/versioned_docs/version-0.16/builder/tutorials/recipes/rust/custom_note_how_to.md new file mode 100644 index 00000000..78f19c9d --- /dev/null +++ b/versioned_docs/version-0.16/builder/tutorials/recipes/rust/custom_note_how_to.md @@ -0,0 +1,495 @@ +--- +title: "How To Create Notes with Custom Logic" +sidebar_position: 7 +--- + +# How to Create a Custom Note + +_Creating notes with custom logic_ + +For toolchain requirements and shared fee helpers, see the [Rust client setup](./index.md#running-the-v016-examples). + +## Overview + +In this guide, we will create a custom note on Miden that can only be consumed by someone who knows the preimage of the hash stored in the note. This approach securely embeds assets into the note and restricts spending to those who possess the correct secret number. + +By following the steps below and using the Miden Assembly code and Rust example, you will learn how to: + +- Create a note with custom logic. +- Store the hash publicly while providing its preimage only to the transaction that consumes the note. + +This example uses public accounts and a public note. Its script checks knowledge of the secret, rather than a particular account ID, so any account with the secret and the required wallet procedure can consume it. + +## What we'll cover + +- Writing Miden assembly for a note +- Consuming notes + +## Step-by-step process + +### 1. Creating two accounts: Alice & Bob + +First, we create two basic accounts for the two users: + +- **Alice:** The account that creates and funds the custom note. +- **Bob:** The account that will consume the note if they know the correct secret. + +### 2. Hashing the secret number + +The security of the custom note hinges on a secret number. Here, we will: + +- Choose a secret number (for example, an array of four integers). +- Hash the four field elements directly with `miden_protocol::Hasher::hash_elements`, which uses Poseidon2 in v0.16. The MASM `hash` instruction computes the matching digest; do not prepend an extra zero word to the Rust input. +- Compute the hash of the secret. The resulting hash will be stored in the note’s storage, meaning that the note can only be consumed if the secret number’s hash preimage is provided during consumption. + +### 3. Creating the custom note + +Now, combine the minted asset and the secret hash to build the custom note. The note is created using the following key steps: + +1. **Assets and storage:** + - The note carries 100 raw units of the tutorial asset and stores the secret's digest in `NoteStorage`. The secret itself is supplied later as the consuming transaction's note arguments. +2. **Miden Assembly Code:** + - The Miden assembly note script ensures that the note can only be consumed if the provided secret, when hashed, matches the hash stored in the note storage. + +Below is the Miden Assembly code for the note. Note scripts are compiled as libraries; the `@note_script` attribute marks the entrypoint procedure. + +```masm +use miden::protocol::active_note +use miden::standards::wallets::basic as wallet + +# CONSTANTS +# ================================================================================================= + +const EXPECTED_DIGEST_PTR = 0 + +# ERRORS +# ================================================================================================= + +const ERROR_DIGEST_MISMATCH = "Expected digest does not match computed digest" + +# PUBLIC INTERFACE +# ================================================================================================= + +#! Consumes the note's assets when the secret hashes to its stored digest. +#! +#! Inputs: [HASH_PREIMAGE_SECRET, pad(12)] +#! Outputs: [pad(16)] +#! +#! Where: +#! - HASH_PREIMAGE_SECRET is the four-felt secret supplied as note arguments. +#! +#! Panics if: +#! - the supplied secret does not match the digest stored in the note. +#! +#! Invocation: dyncall +@note_script +pub proc main(hash_preimage_secret: word) + # => [HASH_PREIMAGE_SECRET, pad(12)] + # hashing the secret number + hash + # => [DIGEST, pad(12)] + + # writing the note storage to memory. + # get_storage leaves only [num_storage_items], so drop a single element + # here, not two, to keep the computed DIGEST intact. + push.EXPECTED_DIGEST_PTR exec.active_note::get_storage drop + + # pad stack and load expected digest from memory (LE: mem[addr] ends up on top) + padw push.EXPECTED_DIGEST_PTR mem_loadw_le + # => [EXPECTED_DIGEST, DIGEST, pad(12)] + + # assert that the note input matches the digest + # will fail if the two hashes do not match + assert_eqw.err=ERROR_DIGEST_MISMATCH + # => [pad(16)] + + # --------------------------------------------------------------------------------------------- + # if the check is successful, we allow for the asset to be consumed + # --------------------------------------------------------------------------------------------- + + # add all assets from the note to the account + exec.wallet::move_note_assets_to_account + # => [pad(16)] +end +``` + +### How the assembly code works: + +1. **Constants and Error Handling:** + The code defines a memory pointer (`EXPECTED_DIGEST_PTR`) for storing the expected hash and an error message for digest mismatches. +2. **Passing the Secret:** + The secret number is passed as `Note Arguments` into the note. +3. **Hashing the Secret:** + The `hash` instruction applies a Poseidon2 hash permutation to the secret number, resulting in a digest that takes up four stack elements. +4. **Digest Comparison:** + The assembly code loads the expected digest from note storage into memory, then reads it back with `mem_loadw_le` (which places `mem[addr]` on top, matching the hash output order) and compares with the computed hash. If they don't match, the transaction fails with a clear error message. +5. **Asset Transfer:** + If the hash matches, `wallet::move_note_assets_to_account` explicitly removes all assets from the note and transfers them into the consuming account's vault. + +### 4. Consuming the note + +With the note created, Bob can now consume it—but only if he provides the correct secret. When Bob initiates the transaction to consume the note, he must supply the same secret number used when Alice created the note. The custom note’s logic will hash the secret and compare it with its stored hash. If they match, Bob’s wallet receives the asset. + +--- + +## Set up the Rust project + +Start in the directory containing your `tutorials` clone and create a sibling Cargo project: + +```bash +cargo new miden-custom-note +cd miden-custom-note +rustup override set 1.98.1 +cp ../tutorials/rust-client/Cargo.lock Cargo.lock +``` + +Keep the generated `[package]` section in `Cargo.toml`, replace its empty `[dependencies]` section with the following, and add the development profile. The path assumes the repository clone is named `tutorials`. + +```toml +[dependencies] +# Clone tutorials next to this Cargo project (see Rust client setup). +rust-client = { path = "../tutorials/rust-client" } +miden-client = { version = "=0.16.0", features = ["testing", "tonic"] } +miden-client-sqlite-store = { version = "=0.16.0", package = "miden-client-sqlite-store" } +miden-protocol = { version = "=0.16.0" } +rand = { version = "0.10" } +tokio = { version = "1.48", features = ["rt-multi-thread", "net", "macros", "fs"] } + +[profile.dev] +opt-level = 2 +``` + +Copy the complete Rust example below into `src/main.rs`. Run it from this new project's directory with `TUTORIAL_NETWORK=testnet cargo run --release`. The client creates `store.sqlite3` and `keystore/` here; keep both out of version control. + +## Full Rust code example + +The following Rust code demonstrates how to implement the steps outlined above using the Miden client library: + +```rust no_run +use rand::Rng; +use rust_client::TutorialClientExt; +use std::{path::PathBuf, sync::Arc}; + +use miden_client::{ + Client, ClientError, Felt, + account::{ + Account, AccountBuilder, AccountType, + component::{ + create_singlesig_user_fungible_faucet, BasicWallet, BurnPolicy, FungibleFaucet, + MintPolicy, TokenName, TokenPolicyManager, + }, + }, + asset::{AssetAmount, AssetId, FungibleAsset, TokenSymbol}, + auth::{AuthSecretKey, AuthSingleSig}, + builder::ClientBuilder, + crypto::FeltRng, + keystore::{FilesystemKeyStore, Keystore}, + note::{Note, NoteAssets, NoteRecipient, NoteStorage, NoteTag, NoteType, PartialNoteMetadata}, + rpc::{GrpcClient, VerifyingRpcClient}, + transaction::{TransactionId, TransactionRequestBuilder}, +}; +use miden_client_sqlite_store::ClientBuilderSqliteExt; +use miden_protocol::Hasher; +use rust_client::{FeeConfig, TutorialNetwork, fund_account_for_fees}; + +// Helper to create a basic account +async fn create_basic_account( + client: &mut Client, + keystore: &Arc, +) -> Result { + let mut init_seed = [0_u8; 32]; + client.rng().fill_bytes(&mut init_seed); + + let key_pair = AuthSecretKey::new_falcon512_poseidon2_with_rng(client.rng()); + + let account = AccountBuilder::new(init_seed) + .account_type(AccountType::Public) + .with_component(AuthSingleSig::from_public_key(key_pair.public_key())) + .with_component(BasicWallet) + .build() + .unwrap(); + + client.add_account(&account, false).await?; + keystore.add_key(&key_pair, account.id()).await.unwrap(); + + Ok(account) +} + +async fn create_basic_faucet( + client: &mut Client, + keystore: &Arc, +) -> Result { + let mut init_seed = [0u8; 32]; + client.rng().fill_bytes(&mut init_seed); + + let key_pair = AuthSecretKey::new_falcon512_poseidon2_with_rng(client.rng()); + let symbol = TokenSymbol::new("MID").unwrap(); + let decimals = 8; + let max_supply = AssetAmount::new(1_000_000).unwrap(); + + let faucet = FungibleFaucet::builder() + .name(TokenName::new("MID").unwrap()) + .symbol(symbol) + .decimals(decimals) + .max_supply(max_supply) + .build() + .unwrap(); + let policies = TokenPolicyManager::builder() + .active_mint_policy(MintPolicy::allow_all()) + .active_burn_policy(BurnPolicy::allow_all()) + .build(); + let account = create_singlesig_user_fungible_faucet( + init_seed, + faucet, + AuthSingleSig::from_public_key(key_pair.public_key()), + policies, + AccountType::Public, + ) + .unwrap(); + + client.add_account(&account, false).await?; + keystore.add_key(&key_pair, account.id()).await.unwrap(); + + Ok(account) +} + +/// Waits for a specific transaction to be committed. +async fn wait_for_tx( + client: &mut Client, + tx_id: TransactionId, +) -> Result<(), ClientError> { + rust_client::wait_for_transaction(client, tx_id).await +} + +#[tokio::main] +async fn main() -> Result<(), ClientError> { + // Initialize client + let network = TutorialNetwork::from_env()?; + let endpoint = network.endpoint(); + let timeout_ms = 10_000; + let rpc_client = Arc::new(VerifyingRpcClient::new(GrpcClient::new( + &endpoint, timeout_ms, + ))); + + // Initialize keystore + let keystore_path = PathBuf::from("./keystore"); + let keystore = Arc::new(FilesystemKeyStore::new(keystore_path).unwrap()); + + let store_path = PathBuf::from("./store.sqlite3"); + + let mut client = ClientBuilder::new() + .rpc(rpc_client) + .sqlite_store(store_path) + .authenticator(keystore.clone()) + .build() + .await?; + + let sync_summary = client.sync_state().await.unwrap(); + println!("Latest block: {}", sync_summary.block_num); + let fee_config = FeeConfig::from_client(&client, network).await?; + + // ------------------------------------------------------------------------- + // STEP 1: Create accounts and deploy faucet + // ------------------------------------------------------------------------- + println!("\n[STEP 1] Creating new accounts"); + let alice_account = create_basic_account(&mut client, &keystore).await?; + println!( + "Alice's account ID: {:?}", + alice_account.id().to_bech32(network.network_id()) + ); + let bob_account = create_basic_account(&mut client, &keystore).await?; + println!( + "Bob's account ID: {:?}", + bob_account.id().to_bech32(network.network_id()) + ); + + println!("\nDeploying a new fungible faucet."); + let faucet = create_basic_faucet(&mut client, &keystore).await?; + println!( + "Faucet account ID: {:?}", + faucet.id().to_bech32(network.network_id()) + ); + for account_id in [alice_account.id(), bob_account.id(), faucet.id()] { + fund_account_for_fees(&mut client, account_id, &fee_config).await?; + } + client.sync_state().await?; + + // ------------------------------------------------------------------------- + // STEP 2: Mint tokens with P2ID + // ------------------------------------------------------------------------- + println!("\n[STEP 2] Mint tokens with P2ID"); + let faucet_id = faucet.id(); + let amount: u64 = 100; + let mint_amount = FungibleAsset::new(faucet_id, amount).unwrap(); + let tx_request = TransactionRequestBuilder::new() + .build_mint_fungible_asset( + mint_amount, + alice_account.id(), + NoteType::Public, + client.rng(), + ) + .unwrap(); + + let tx_id = client + .submit_tutorial_transaction(faucet.id(), tx_request) + .await?; + println!("Minted tokens. TX: {:?}", tx_id); + + // Wait for the note to be available + client.sync_state().await?; + wait_for_tx(&mut client, tx_id).await?; + + // Consume the minted note + let consumable_notes = client + .get_consumable_tutorial_notes(Some(alice_account.id())) + .await?; + + if let Some((note_record, _)) = consumable_notes.first() { + let note: Note = note_record.clone().try_into()?; + let consume_request = TransactionRequestBuilder::new().build_consume_notes(vec![note])?; + + let tx_id = client + .submit_tutorial_transaction(alice_account.id(), consume_request) + .await?; + println!("Consumed minted note. TX: {:?}", tx_id); + } + + client.sync_state().await?; + + // ------------------------------------------------------------------------- + // STEP 3: Create custom note + // ------------------------------------------------------------------------- + println!("\n[STEP 3] Create custom note"); + let secret_vals = vec![ + Felt::new_unchecked(1), + Felt::new_unchecked(2), + Felt::new_unchecked(3), + Felt::new_unchecked(4), + ]; + let digest = Hasher::hash_elements(&secret_vals); + println!("digest: {:?}", digest); + + // Read the MASM source from the tutorials repository. + let code = std::fs::read_to_string("../tutorials/masm/notes/hash_preimage_note.masm").unwrap(); + let serial_num = client.rng().draw_word(); + + let note_script = client.code_builder().compile_note_script(&code).unwrap(); + let note_storage = NoteStorage::new(digest.to_vec()).unwrap(); + let recipient = NoteRecipient::new(serial_num, note_script, note_storage); + let tag = NoteTag::new(0); + let metadata = PartialNoteMetadata::new(alice_account.id(), NoteType::Public).with_tag(tag); + let vault = NoteAssets::new(vec![mint_amount.into()])?; + let custom_note = Note::new(vault, metadata, recipient); + println!("note hash: {:?}", custom_note.id().to_hex()); + + let note_request = TransactionRequestBuilder::new() + .own_output_notes(vec![custom_note.clone()]) + .build() + .unwrap(); + + let tx_id = client + .submit_tutorial_transaction(alice_account.id(), note_request) + .await?; + println!( + "View transaction on MidenScan: {}/tx/{:?}", + network.explorer_url(), + tx_id + ); + + client.sync_state().await?; + + // ------------------------------------------------------------------------- + // STEP 4: Consume the Custom Note + // ------------------------------------------------------------------------- + println!("\n[STEP 4] Bob consumes the Custom Note with Correct Secret"); + + let secret = [ + Felt::new_unchecked(1), + Felt::new_unchecked(2), + Felt::new_unchecked(3), + Felt::new_unchecked(4), + ]; + let consume_custom_request = TransactionRequestBuilder::new() + .input_notes([(custom_note, Some(secret.into()))]) + .build() + .unwrap(); + + let tx_id = client + .submit_tutorial_transaction(bob_account.id(), consume_custom_request) + .await?; + println!( + "Consumed Note Tx on MidenScan: {}/tx/{:?} \n", + network.explorer_url(), + tx_id + ); + + wait_for_tx(&mut client, tx_id).await?; + + let bob = client + .get_account(bob_account.id()) + .await? + .expect("Bob's account must exist after consuming the note"); + let balance = bob.vault().get_balance(AssetId::new_fungible(faucet_id))?; + assert_eq!( + balance.as_u64(), + amount, + "Bob must receive all assets from the hash-preimage note", + ); + println!("Bob's custom-note token balance: {balance}"); + + Ok(()) +} +``` + +The following is an abbreviated output; IDs vary, and funding and repeated confirmation messages are omitted: + +```text +Latest block: + +[STEP 1] Creating new accounts +Alice's account ID: "" +Bob's account ID: "" + +Deploying a new fungible faucet. +Faucet account ID: "" + +[STEP 2] Mint tokens with P2ID +Minted tokens. TX: +Transaction committed: +Consumed minted note. TX: + +[STEP 3] Create custom note +digest: Word([14206540680072267069, 9571949196318390099, 5950603493574130513, 3457190364553631046]) +note hash: "0xf48f362f1817bbc5575e0bb8b77c496dd67e4b85d8ff45d21dff5743de2b174d" +View transaction on MidenScan: https://testnet.midenscan.com/tx/ + +[STEP 4] Bob consumes the Custom Note with Correct Secret +Consumed Note Tx on MidenScan: https://testnet.midenscan.com/tx/ + +Transaction committed: +Bob's custom-note token balance: 100 +``` + +## Conclusion + +You have now seen how to create a custom note on Miden that requires a secret preimage to be consumed. We covered: + +1. Creating and funding accounts (Alice and Bob) +2. Hashing a secret number +3. Building a note with custom logic in Miden Assembly +4. Consuming the note by providing the correct secret + +The fixed secret `[1, 2, 3, 4]` is for demonstration. A real secret must be unpredictable and shared only with the intended consumer. Anyone who learns it can satisfy this note's spending condition. + +### Running the example + +From the root of your `tutorials` clone, run the checked-in example: + +```bash +cd rust-client +TUTORIAL_NETWORK=testnet cargo run --release --bin hash_preimage_note +``` + +### Continue learning + +Next tutorial: [How to Use Unauthenticated Notes](unauthenticated_note_how_to.md) diff --git a/versioned_docs/version-0.16/builder/tutorials/recipes/rust/delegated_proving_tutorial.md b/versioned_docs/version-0.16/builder/tutorials/recipes/rust/delegated_proving_tutorial.md new file mode 100644 index 00000000..f7d8c62e --- /dev/null +++ b/versioned_docs/version-0.16/builder/tutorials/recipes/rust/delegated_proving_tutorial.md @@ -0,0 +1,235 @@ +--- +title: "Delegated Proving" +sidebar_position: 12 +--- + +# Delegated Proving + +_Using delegated proving to minimize transaction proving times on computationally constrained devices_ + +For toolchain requirements and shared fee helpers, see the [Rust client setup](./index.md#running-the-v016-examples). + +## Overview + +In this tutorial we will cover how to use delegated proving with the Miden Rust client to minimize the time it takes to generate a valid transaction proof. We create and fund an account, execute a minimal transaction locally, prove it with the network's remote prover, and verify that its confirmed nonce increases by one. Even this minimal transaction pays a verification fee on testnet. + +## Prerequisites + +This tutorial assumes you have basic familiarity with the Miden Rust client. + +## What we'll cover + +- Explaining what "delegated proving" is and its pros and cons +- How to use delegated proving with the Rust client + +## What is Delegated Proving? + +Before diving into our code example, let's clarify what "delegated proving" means. + +Delegated proving is the process of outsourcing the ZK proof generation of your transaction to a third party. For certain computationally constrained devices such as mobile phones and web browser environments, generating ZK proofs might take too long to ensure an acceptable user experience. Devices that do not have the computational resources to generate Miden proofs in under 1-2 seconds can use delegated proving to provide a more responsive user experience. + +_How does it work?_ When a user chooses to use delegated proving, they send off their locally executed transaction to a dedicated server. This dedicated server generates the ZK proof for the executed transaction and sends the proof back to the user. The transaction proof is verified under the same rules as a locally generated proof: the delegated prover cannot make an invalid state transition valid. This protects transaction integrity, but does not keep the witness private from the prover. + +Delegated proving reveals the transaction witness to the prover and depends on that service being available. The witness can include private account state and note arguments. For example, it would not be advisable to use delegated proving in the case of our "How to Create a Custom Note" tutorial, since the note we create requires knowledge of a hash preimage to redeem the assets in the note. Using delegated proving would reveal the hash preimage to the server running the delegated proving service. + +Anyone can run their own delegated prover server. If you are building a product on Miden, it may make sense to run your own delegated prover server for your users. To run your own delegated proving server, follow the instructions here: https://crates.io/crates/miden-remote-prover. + +This tutorial performs real delegated proving against the public Miden testnet prover at +`https://tx-prover.testnet.miden.io` using `RemoteTransactionProver`. To use your own delegated +prover instead, point `RemoteTransactionProver` at its URL. + +## Step 1: Initialize your repository + +Start in the directory containing your `tutorials` clone and create a sibling Cargo project. The dependency path below assumes the clone is named `tutorials`. + +```bash +cargo new miden-delegated-proving-app +cd miden-delegated-proving-app +rustup override set 1.98.1 +cp ../tutorials/rust-client/Cargo.lock Cargo.lock +``` + +Keep the generated `[package]` section in `Cargo.toml`, replace its empty `[dependencies]` section with the following, and add the development profile: + +```toml +[dependencies] +# Clone tutorials next to this Cargo project (see Rust client setup). +rust-client = { path = "../tutorials/rust-client" } +miden-client = { version = "=0.16.0", features = ["testing", "tonic"] } +miden-client-sqlite-store = { version = "=0.16.0", package = "miden-client-sqlite-store" } +miden-protocol = { version = "=0.16.0" } +rand = { version = "0.10" } +tokio = { version = "1.48", features = ["rt-multi-thread", "net", "macros", "fs"] } + +[profile.dev] +opt-level = 2 +``` + +## Step 2: Initialize the client and prover and construct transactions + +Similarly to previous tutorials, we must instantiate the client. +We construct a `RemoteTransactionProver` pointed at the public Miden testnet delegated prover for this walkthrough. Copy this complete example into `src/main.rs`. The client creates `store.sqlite3` and `keystore/` in the project directory; keep both out of version control. + +```rust no_run +use rand::Rng; +use std::{path::PathBuf, sync::Arc}; + +use miden_client::{ + ClientError, RemoteTransactionProver, + account::{AccountBuilder, AccountType, component::BasicWallet}, + auth::{AuthSecretKey, AuthSingleSig}, + builder::ClientBuilder, + keystore::{FilesystemKeyStore, Keystore}, + rpc::{GrpcClient, VerifyingRpcClient}, + transaction::{TransactionProver, TransactionRequestBuilder}, +}; +use miden_client_sqlite_store::ClientBuilderSqliteExt; +use rust_client::{FeeConfig, TutorialNetwork, fund_account_for_fees}; + +#[tokio::main] +async fn main() -> Result<(), ClientError> { + // Initialize client + let network = TutorialNetwork::from_env()?; + let endpoint = network.endpoint(); + let timeout_ms = 10_000; + let rpc_client = Arc::new(VerifyingRpcClient::new(GrpcClient::new( + &endpoint, timeout_ms, + ))); + + // Initialize keystore + let keystore_path = PathBuf::from("./keystore"); + let keystore = Arc::new(FilesystemKeyStore::new(keystore_path).unwrap()); + + let store_path = PathBuf::from("./store.sqlite3"); + + let mut client = ClientBuilder::new() + .rpc(rpc_client) + .sqlite_store(store_path) + .authenticator(keystore.clone()) + .build() + .await?; + + let sync_summary = client.sync_state().await.unwrap(); + println!("Latest block: {}", sync_summary.block_num); + let fee_config = FeeConfig::from_client(&client, network).await?; + + // Create Alice's account + let mut init_seed = [0_u8; 32]; + client.rng().fill_bytes(&mut init_seed); + + let key_pair = AuthSecretKey::new_falcon512_poseidon2_with_rng(client.rng()); + + let alice_account = AccountBuilder::new(init_seed) + .account_type(AccountType::Private) + .with_component(AuthSingleSig::from_public_key(key_pair.public_key())) + .with_component(BasicWallet) + .build() + .unwrap(); + + client.add_account(&alice_account, false).await?; + keystore + .add_key(&key_pair, alice_account.id()) + .await + .unwrap(); + fund_account_for_fees(&mut client, alice_account.id(), &fee_config).await?; + + // ------------------------------------------------------------------------- + // Set up the delegated (remote) tx prover + // ------------------------------------------------------------------------- + // Delegated proving outsources ZK proof generation to a remote service. This is + // the public prover for the selected network; run your own + // (https://crates.io/crates/miden-remote-prover) and swap the URL to use it. + // The upstream constant keeps this URL synchronized with the selected network. + let remote_tx_prover = RemoteTransactionProver::new(network.remote_prover_url()); + let tx_prover: Arc = Arc::new(remote_tx_prover); + + // We use a dummy transaction request to showcase delegated proving. + // In addition to paying the network fee, this transaction increments Alice's nonce. + let initial_nonce = client + .get_account(alice_account.id()) + .await? + .expect("Alice exists") + .nonce(); + println!("Alice nonce initial: {:?}", initial_nonce); + let script_code = "@transaction_script pub proc main push.1 drop end"; + let tx_script = client + .code_builder() + .compile_tx_script(script_code) + .unwrap(); + + let transaction_request = TransactionRequestBuilder::new() + .custom_script(tx_script) + .build() + .unwrap(); + + // Step 1: Execute the transaction locally + println!("Executing transaction..."); + client.sync_state().await?; + let tx_result = client + .execute_transaction(alice_account.id(), transaction_request) + .await?; + + // Step 2: Prove the transaction using the delegated (remote) prover + println!("Proving transaction with the delegated prover..."); + let proven_transaction = client.prove_transaction_with(&tx_result, tx_prover).await?; + + // Step 3: Submit the proven transaction + println!("Submitting proven transaction..."); + let submission_height = client + .submit_proven_transaction(proven_transaction, &tx_result) + .await?; + + // Step 4: Apply the transaction to local store + client + .apply_transaction(&tx_result, submission_height) + .await?; + rust_client::wait_for_transaction(&mut client, tx_result.id()).await?; + + println!("Transaction submitted successfully using the delegated prover!"); + + client.sync_state().await.unwrap(); + + let account = client + .get_account(alice_account.id()) + .await + .unwrap() + .expect("alice account not found"); + + println!("Alice nonce has increased: {:?}", account.nonce()); + assert_eq!(account.nonce(), initial_nonce + miden_client::Felt::ONE); + + Ok(()) +} +``` + +Now let's run the `src/main.rs` program: + +```bash +TUTORIAL_NETWORK=testnet cargo run --release +``` + +The following is an abbreviated output. The funding transaction has already increased Alice's nonce from 0 to 1; the delegated transaction then increases it to 2: + +```text +Latest block: +Alice nonce initial: 1 +Executing transaction... +Proving transaction with the delegated prover... +Submitting proven transaction... +Transaction committed: +Transaction submitted successfully using the delegated prover! +Alice nonce has increased: 2 +``` + +### Running the example + +From the root of your `tutorials` clone, run the checked-in example: + +```bash +cd rust-client +TUTORIAL_NETWORK=testnet cargo run --release --bin delegated_prover +``` + +### Continue learning + +Next tutorial: [Consuming On-Chain Price Data from the Pragma Oracle](oracle_tutorial.md) diff --git a/versioned_docs/version-0.16/builder/tutorials/recipes/rust/foreign_procedure_invocation_tutorial.md b/versioned_docs/version-0.16/builder/tutorials/recipes/rust/foreign_procedure_invocation_tutorial.md new file mode 100644 index 00000000..b18bb126 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tutorials/recipes/rust/foreign_procedure_invocation_tutorial.md @@ -0,0 +1,744 @@ +--- +title: "Foreign Procedure Invocation" +sidebar_position: 7 +--- + +# Foreign Procedure Invocation Tutorial + +_Using foreign procedure invocation to craft read-only cross-contract calls in the Miden VM_ + +For toolchain requirements and shared fee helpers, see the [Rust client setup](./index.md#running-the-v016-examples). + +## Overview + +In previous tutorials we deployed a public counter contract and incremented the count from a different client instance. + +In this tutorial we will cover the basics of "foreign procedure invocation" (FPI) in the Miden VM. To demonstrate FPI, we will build a "count copy" smart contract that reads the count from our previously deployed counter contract and copies the count to its own local storage. + +Foreign procedure invocation (FPI) is a powerful tool for building smart contracts in the Miden VM. FPI allows one smart contract to call "read-only" procedures in other smart contracts. + +The term "foreign procedure invocation" might sound a bit verbose, but it is as simple as one smart contract calling a non-state modifying procedure in another smart contract. The "EVM equivalent" of foreign procedure invocation would be a smart contract calling a read-only function in another contract. + +FPI is useful for developing smart contracts that extend the functionality of existing contracts on Miden. FPI is the core primitive used by price oracles on Miden. + +## What We Will Build + +![count copy FPI diagram](../img/count_copy_fpi_diagram.png) + +The diagram above depicts the "count copy" smart contract using foreign procedure invocation to read the count state of the counter contract. After reading the state via FPI, the "count copy" smart contract writes the value returned from the counter contract to storage. + +## What we'll cover + +- Foreign Procedure Invocation (FPI) +- Building a "count copy" Smart Contract + +## Prerequisites + +This tutorial assumes you have a basic understanding of Miden assembly and a counter deployed using the [counter contract tutorial](./counter_contract_tutorial.md). Keep its printed `mtst1...` account ID. The reader runs in a separate Cargo project and reads that counter's public state. + +## Step 1: Set up your repository + +From the parent directory of your `tutorials` clone, create a sibling Cargo project: + +```bash +cargo new miden-fpi +cd miden-fpi +rustup override set 1.98.1 +cp ../tutorials/rust-client/Cargo.lock Cargo.lock +``` + +Add these dependencies and the development profile to your `Cargo.toml`: + +```toml +[dependencies] +rust-client = { path = "../tutorials/rust-client" } +miden-client = { version = "=0.16.0", features = ["testing", "tonic"] } +miden-client-sqlite-store = { version = "=0.16.0", package = "miden-client-sqlite-store" } +miden-protocol = { version = "=0.16.0" } +rand = { version = "0.10" } +tokio = { version = "1.48", features = ["rt-multi-thread", "net", "macros", "fs"] } + +[profile.dev] +opt-level = 2 +``` + +## Step 2: Set up the "count reader" contract + +The reader contract in `masm/accounts/count_reader.masm` reads the counter’s value through FPI. + +`masm/accounts/count_reader.masm`: + +```masm +use miden::protocol::native_account +use miden::protocol::tx +use miden::core::sys +use {AccountId, AccountProcedureRoot} from miden::protocol::types + +# CONSTANTS +# ================================================================================================= + +const COUNT_READER_SLOT = word("miden::tutorials::count_reader") + +# PUBLIC INTERFACE +# ================================================================================================= + +#! Copies the count returned by the foreign counter into this account's storage. +#! +#! Inputs: [foreign_account_id_{suffix,prefix}, FOREIGN_PROC_ROOT, pad(10)] +#! Outputs: [pad(16)] +#! +#! Where: +#! - foreign_account_id_{suffix,prefix} identifies the public counter account. +#! - FOREIGN_PROC_ROOT is the root of its get_count procedure. +#! +#! Invocation: call +@account_procedure +@locals(6) +pub proc copy_count(foreign_account_id: AccountId, foreign_proc_root: AccountProcedureRoot) + # save the foreign target while preparing its sixteen zero inputs + loc_store.4 loc_store.5 loc_storew_le.0 dropw + # => [pad(16)] + + padw padw padw padw + # => [foreign_procedure_inputs(16), pad(16)] + + padw loc_loadw_le.0 loc_load.5 loc_load.4 + # => [foreign_account_id_suffix, foreign_account_id_prefix, FOREIGN_PROC_ROOT, foreign_procedure_inputs(16), pad(16)] + + exec.tx::execute_foreign_procedure + # => [[count, 0, 0, 0], pad(28)] + + push.COUNT_READER_SLOT[0..2] + # => [slot_id_suffix, slot_id_prefix, [count, 0, 0, 0], pad(28)] + + exec.native_account::set_item + # => [OLD_VALUE, pad(28)] + + dropw + # => [pad(28)] + + exec.sys::truncate_stack + # => [pad(16)] +end +``` + +In the count reader smart contract we have a `copy_count` procedure that uses `tx::execute_foreign_procedure` to call the `get_count` procedure in the counter contract. + +To call the `get_count` procedure, we push its hash along with the counter contract's ID suffix and prefix. + +This is what the stack state should look like before we call `tx::execute_foreign_procedure`: + +```text +# => [account_id_suffix, account_id_prefix, GET_COUNT_HASH, foreign_procedure_inputs(16)] +``` + +`execute_foreign_procedure` always requires exactly 16 `foreign_procedure_inputs` on the stack +below the procedure hash and account ID. Since `get_count` takes no arguments, we pass 16 zero +felts (`padw padw padw padw`, four words) as the inputs. The reader prepares these +inputs internally after saving the account ID and procedure root in local memory. The +caller therefore passes only those six identifying felts. After the foreign call, +the count is the first of 16 output elements; the reader stores its word, discards +the previous storage value, and truncates the remaining padding. + +After calling the `get_count` procedure in the counter contract, we save the count into the +`miden::tutorials::count_reader` storage slot. + +The transaction script is defined in `masm/scripts/reader_script.masm`: + +```masm +use external_contract::count_reader_contract +use miden::core::sys + +#! Copies a public counter through the reader account. +#! +#! Inputs: [ARGS, pad(12)] +#! Outputs: [pad(16)] +#! +#! Where: +#! - ARGS contains unused transaction script arguments. +#! +#! Invocation: dyncall +@transaction_script +pub proc main(args: word) + dropw + # => [pad(16)] + + push.{get_count_proc_hash} + # => [GET_COUNT_HASH, pad(16)] + + push.{account_id_prefix} + # => [account_id_prefix, GET_COUNT_HASH, pad(16)] + + push.{account_id_suffix} + # => [account_id_suffix, account_id_prefix, GET_COUNT_HASH, pad(16)] + + call.count_reader_contract::copy_count + # => [pad(22)] + + exec.sys::truncate_stack + # => [pad(16)] +end +``` + +The braces mark template values, not valid MASM operands. The Rust code replaces the procedure root and both account ID elements before assembling the script. + +## Step 3: Set up your `src/main.rs` file + +```rust no_run +use rand::Rng; +use rust_client::TutorialClientExt; +use std::{path::PathBuf, sync::Arc, time::Duration}; +use tokio::time::sleep; + +use miden_client::{ + ClientError, Word, + account::{ + AccountBuilder, AccountComponent, AccountId, AccountType, StorageSlot, StorageSlotName, + component::{AccountComponentMetadata, BasicWallet}, + }, + auth::NoAuth, + builder::ClientBuilder, + keystore::FilesystemKeyStore, + rpc::{GrpcClient, VerifyingRpcClient, domain::account::AccountStorageRequirements}, + transaction::{ForeignAccount, TransactionRequestBuilder}, +}; +use miden_client_sqlite_store::ClientBuilderSqliteExt; +use rust_client::{FeeConfig, TutorialNetwork, fund_account_for_fees}; + +#[tokio::main] +async fn main() -> Result<(), ClientError> { + // Initialize client + let network = TutorialNetwork::from_env()?; + let endpoint = network.endpoint(); + let timeout_ms = 10_000; + let rpc_client = Arc::new(VerifyingRpcClient::new(GrpcClient::new( + &endpoint, timeout_ms, + ))); + + // Initialize keystore + let keystore_path = PathBuf::from("./keystore"); + let keystore = Arc::new(FilesystemKeyStore::new(keystore_path).unwrap()); + + let store_path = PathBuf::from("./store.sqlite3"); + + let mut client = ClientBuilder::new() + .rpc(rpc_client) + .sqlite_store(store_path) + .authenticator(keystore.clone()) + .build() + .await?; + + let sync_summary = client.sync_state().await.unwrap(); + println!("Latest block: {}", sync_summary.block_num); + let fee_config = FeeConfig::from_client(&client, network).await?; + + // ------------------------------------------------------------------------- + // STEP 1: Create the Count Reader Contract + // ------------------------------------------------------------------------- + println!("\n[STEP 1] Creating count reader contract."); + + // Read the MASM source from the tutorials repository. + let count_reader_code = + std::fs::read_to_string("../tutorials/masm/accounts/count_reader.masm").unwrap(); + + let count_reader_slot_name = + StorageSlotName::new("miden::tutorials::count_reader").expect("valid slot name"); + let count_reader_component_code = client + .code_builder() + .compile_component_code( + "external_contract::count_reader_contract", + &count_reader_code, + ) + .unwrap(); + let count_reader_component = AccountComponent::new( + count_reader_component_code, + vec![StorageSlot::with_value( + count_reader_slot_name.clone(), + Word::default(), + )], + AccountComponentMetadata::new("external_contract::count_reader_contract"), + ) + .unwrap(); + + let mut init_seed = [0_u8; 32]; + client.rng().fill_bytes(&mut init_seed); + + let count_reader_contract = AccountBuilder::new(init_seed) + .account_type(AccountType::Public) + .with_component(count_reader_component.clone()) + .with_component(BasicWallet) + .with_component(NoAuth) + .build() + .unwrap(); + + println!( + "count_reader hash: {:?}", + count_reader_contract.to_commitment() + ); + println!("count_reader id: {:?}", count_reader_contract.id()); + + client + .add_account(&count_reader_contract, false) + .await + .unwrap(); + fund_account_for_fees(&mut client, count_reader_contract.id(), &fee_config).await?; + + Ok(()) +} +``` + +Run the following command to execute src/main.rs: + +```bash +TUTORIAL_NETWORK=testnet cargo run --release +``` + +The output includes the reader's initial commitment and ID (abridged; values vary): + +```text +Latest block: + +[STEP 1] Creating count reader contract. +count_reader hash: Word([...]) +count_reader id: V1(AccountIdV1 { suffix: ..., prefix: ... }) +``` + +## Step 4: Import the pre-deployed counter contract + +The FPI call needs a counter contract already deployed on-chain. Copy its `mtst1...` testnet account ID into the `MIDEN_COUNTER_ACCOUNT_ID` environment variable. Using an input avoids baking in an address that becomes invalid after a testnet reset. + +Insert this fragment inside `main`, immediately before its final `Ok(())`: + +```rust ignore +// ------------------------------------------------------------------------- +// STEP 2: Build & Get State of the Counter Contract +// ------------------------------------------------------------------------- +println!("\n[STEP 2] Building counter contract from public state"); + +// Pass the account ID printed by `counter_contract_deploy` as the first argument, or via +// `MIDEN_COUNTER_ACCOUNT_ID`. +let counter_contract_bech32 = std::env::args() + .nth(1) + .or_else(|| std::env::var("MIDEN_COUNTER_ACCOUNT_ID").ok()) + .expect("pass the counter account ID from counter_contract_deploy"); +let (account_network, counter_contract_id) = + AccountId::from_bech32(&counter_contract_bech32).expect("invalid counter account ID"); +assert_eq!( + account_network, + network.network_id(), + "counter account must match the selected tutorial network" +); + +println!("counter contract id: {:?}", counter_contract_id); + +client + .import_account_by_id(counter_contract_id) + .await + .unwrap(); + +let counter_contract = client + .get_account(counter_contract_id) + .await + .unwrap() + .expect("counter contract not found"); +println!( + "Account details: {:?}", + counter_contract.storage().slots().first().unwrap() +); +``` + +## Step 5: Call the counter contract via foreign procedure invocation + +Insert this fragment after the import step, inside `main` and before `Ok(())`: + +```rust ignore +// ------------------------------------------------------------------------- +// STEP 3: Call the Counter Contract via Foreign Procedure Invocation (FPI) +// ------------------------------------------------------------------------- +println!("\n[STEP 3] Call counter contract with FPI from count reader contract"); + +let counter_contract_code = + std::fs::read_to_string("../tutorials/masm/accounts/counter.masm").unwrap(); + +// Compile the counter as a component (same path as the deploy binary) to get +// the correct procedure root that matches the on-chain MAST. +let counter_component_code = client + .code_builder() + .compile_component_code( + "external_contract::counter_contract", + &counter_contract_code, + ) + .unwrap(); +let counter_component = AccountComponent::new( + counter_component_code, + vec![], + AccountComponentMetadata::new("external_contract::counter_contract"), +) +.unwrap(); + +let get_count_root = counter_component + .component_code() + .get_procedure_root_by_path("external_contract::counter_contract::get_count") + .expect("get_count export not found"); +let get_count_hash = format!("{}", get_count_root); + +println!("get_count hash: {:?}", get_count_hash); +println!("counter id prefix: {:?}", counter_contract_id.prefix()); +println!("counter id suffix: {:?}", counter_contract_id.suffix()); + +let script_code = std::fs::read_to_string("../tutorials/masm/scripts/reader_script.masm") + .unwrap() + .replace("{get_count_proc_hash}", &get_count_hash) + .replace( + "{account_id_suffix}", + &counter_contract_id.suffix().as_canonical_u64().to_string(), + ) + .replace( + "{account_id_prefix}", + &u64::from(counter_contract_id.prefix()).to_string(), + ); + +// Link the count reader contract code into the same `CodeBuilder` chain +// that compiles the script. +let tx_script = client + .code_builder() + .with_linked_module( + "external_contract::count_reader_contract", + &count_reader_code, + ) + .unwrap() + .compile_tx_script(script_code.as_str()) + .unwrap(); + +let foreign_account = + ForeignAccount::public(counter_contract_id, AccountStorageRequirements::default()).unwrap(); + +let tx_request = TransactionRequestBuilder::new() + .foreign_accounts([foreign_account]) + .custom_script(tx_script) + .build() + .unwrap(); + +let tx_id = client + .submit_tutorial_transaction(count_reader_contract.id(), tx_request) + .await + .unwrap(); + +println!( + "View transaction on MidenScan: {}/tx/{:?}", + network.explorer_url(), + tx_id +); + +client.sync_state().await.unwrap(); +sleep(Duration::from_secs(5)).await; +client.sync_state().await.unwrap(); + +// Retrieve final state to confirm the count was copied. +let counter_slot_name = + StorageSlotName::new("miden::tutorials::counter").expect("valid slot name"); +let account_1 = client + .get_account(counter_contract_id) + .await + .unwrap() + .expect("counter contract not found"); +println!( + "counter contract storage: {:?}", + account_1.storage().get_item(&counter_slot_name) +); + +let account_2 = client + .get_account(count_reader_contract.id()) + .await + .unwrap() + .expect("count reader contract not found"); +println!( + "count reader contract storage: {:?}", + account_2.storage().get_item(&count_reader_slot_name) +); +assert_eq!( + account_2 + .storage() + .get_item(&count_reader_slot_name) + .unwrap(), + account_1.storage().get_item(&counter_slot_name).unwrap(), + "FPI must copy the current counter value", +); +``` + +The `.foreign_accounts()` method declares the foreign state that the client must fetch and prove. `AccountStorageRequirements::default()` suffices here because `get_count` reads a value slot. A procedure that reads map entries must also request proofs for the specific map keys it uses. The MASM script performs the actual foreign call. + +## Summary + +In this tutorial, we created a smart contract that calls the counter's `get_count` procedure through FPI and saves the returned value in its own storage. The reader account pays for this transaction; the foreign counter is read-only and is not charged or modified. + +The final `src/main.rs` file should look like this: + +```rust no_run +use rand::Rng; +use rust_client::TutorialClientExt; +use std::{path::PathBuf, sync::Arc, time::Duration}; +use tokio::time::sleep; + +use miden_client::{ + ClientError, Word, + account::{ + AccountBuilder, AccountComponent, AccountId, AccountType, StorageSlot, StorageSlotName, + component::{AccountComponentMetadata, BasicWallet}, + }, + auth::NoAuth, + builder::ClientBuilder, + keystore::FilesystemKeyStore, + rpc::{GrpcClient, VerifyingRpcClient, domain::account::AccountStorageRequirements}, + transaction::{ForeignAccount, TransactionRequestBuilder}, +}; +use miden_client_sqlite_store::ClientBuilderSqliteExt; +use rust_client::{FeeConfig, TutorialNetwork, fund_account_for_fees}; + +#[tokio::main] +async fn main() -> Result<(), ClientError> { + // Initialize client + let network = TutorialNetwork::from_env()?; + let endpoint = network.endpoint(); + let timeout_ms = 10_000; + let rpc_client = Arc::new(VerifyingRpcClient::new(GrpcClient::new( + &endpoint, timeout_ms, + ))); + + // Initialize keystore + let keystore_path = PathBuf::from("./keystore"); + let keystore = Arc::new(FilesystemKeyStore::new(keystore_path).unwrap()); + + let store_path = PathBuf::from("./store.sqlite3"); + + let mut client = ClientBuilder::new() + .rpc(rpc_client) + .sqlite_store(store_path) + .authenticator(keystore.clone()) + .build() + .await?; + + let sync_summary = client.sync_state().await.unwrap(); + println!("Latest block: {}", sync_summary.block_num); + let fee_config = FeeConfig::from_client(&client, network).await?; + + // ------------------------------------------------------------------------- + // STEP 1: Create the Count Reader Contract + // ------------------------------------------------------------------------- + println!("\n[STEP 1] Creating count reader contract."); + + // Read the MASM source from the tutorials repository. + let count_reader_code = + std::fs::read_to_string("../tutorials/masm/accounts/count_reader.masm").unwrap(); + + let count_reader_slot_name = + StorageSlotName::new("miden::tutorials::count_reader").expect("valid slot name"); + let count_reader_component_code = client + .code_builder() + .compile_component_code( + "external_contract::count_reader_contract", + &count_reader_code, + ) + .unwrap(); + let count_reader_component = AccountComponent::new( + count_reader_component_code, + vec![StorageSlot::with_value( + count_reader_slot_name.clone(), + Word::default(), + )], + AccountComponentMetadata::new("external_contract::count_reader_contract"), + ) + .unwrap(); + + let mut init_seed = [0_u8; 32]; + client.rng().fill_bytes(&mut init_seed); + + let count_reader_contract = AccountBuilder::new(init_seed) + .account_type(AccountType::Public) + .with_component(count_reader_component.clone()) + .with_component(BasicWallet) + .with_component(NoAuth) + .build() + .unwrap(); + + println!( + "count_reader hash: {:?}", + count_reader_contract.to_commitment() + ); + println!("count_reader id: {:?}", count_reader_contract.id()); + + client + .add_account(&count_reader_contract, false) + .await + .unwrap(); + fund_account_for_fees(&mut client, count_reader_contract.id(), &fee_config).await?; + + // ------------------------------------------------------------------------- + // STEP 2: Build & Get State of the Counter Contract + // ------------------------------------------------------------------------- + println!("\n[STEP 2] Building counter contract from public state"); + + // Pass the account ID printed by `counter_contract_deploy` as the first argument, or via + // `MIDEN_COUNTER_ACCOUNT_ID`. + let counter_contract_bech32 = std::env::args() + .nth(1) + .or_else(|| std::env::var("MIDEN_COUNTER_ACCOUNT_ID").ok()) + .expect("pass the counter account ID from counter_contract_deploy"); + let (account_network, counter_contract_id) = + AccountId::from_bech32(&counter_contract_bech32).expect("invalid counter account ID"); + assert_eq!( + account_network, + network.network_id(), + "counter account must match the selected tutorial network" + ); + + println!("counter contract id: {:?}", counter_contract_id); + + client + .import_account_by_id(counter_contract_id) + .await + .unwrap(); + + let counter_contract = client + .get_account(counter_contract_id) + .await + .unwrap() + .expect("counter contract not found"); + println!( + "Account details: {:?}", + counter_contract.storage().slots().first().unwrap() + ); + + // ------------------------------------------------------------------------- + // STEP 3: Call the Counter Contract via Foreign Procedure Invocation (FPI) + // ------------------------------------------------------------------------- + println!("\n[STEP 3] Call counter contract with FPI from count reader contract"); + + let counter_contract_code = + std::fs::read_to_string("../tutorials/masm/accounts/counter.masm").unwrap(); + + // Compile the counter as a component (same path as the deploy binary) to get + // the correct procedure root that matches the on-chain MAST. + let counter_component_code = client + .code_builder() + .compile_component_code( + "external_contract::counter_contract", + &counter_contract_code, + ) + .unwrap(); + let counter_component = AccountComponent::new( + counter_component_code, + vec![], + AccountComponentMetadata::new("external_contract::counter_contract"), + ) + .unwrap(); + + let get_count_root = counter_component + .component_code() + .get_procedure_root_by_path("external_contract::counter_contract::get_count") + .expect("get_count export not found"); + let get_count_hash = format!("{}", get_count_root); + + println!("get_count hash: {:?}", get_count_hash); + println!("counter id prefix: {:?}", counter_contract_id.prefix()); + println!("counter id suffix: {:?}", counter_contract_id.suffix()); + + let script_code = std::fs::read_to_string("../tutorials/masm/scripts/reader_script.masm") + .unwrap() + .replace("{get_count_proc_hash}", &get_count_hash) + .replace( + "{account_id_suffix}", + &counter_contract_id.suffix().as_canonical_u64().to_string(), + ) + .replace( + "{account_id_prefix}", + &u64::from(counter_contract_id.prefix()).to_string(), + ); + + // Link the count reader contract code into the same `CodeBuilder` chain + // that compiles the script. + let tx_script = client + .code_builder() + .with_linked_module( + "external_contract::count_reader_contract", + &count_reader_code, + ) + .unwrap() + .compile_tx_script(script_code.as_str()) + .unwrap(); + + let foreign_account = + ForeignAccount::public(counter_contract_id, AccountStorageRequirements::default()).unwrap(); + + let tx_request = TransactionRequestBuilder::new() + .foreign_accounts([foreign_account]) + .custom_script(tx_script) + .build() + .unwrap(); + + let tx_id = client + .submit_tutorial_transaction(count_reader_contract.id(), tx_request) + .await + .unwrap(); + + println!( + "View transaction on MidenScan: {}/tx/{:?}", + network.explorer_url(), + tx_id + ); + + client.sync_state().await.unwrap(); + sleep(Duration::from_secs(5)).await; + client.sync_state().await.unwrap(); + + // Retrieve final state to confirm the count was copied. + let counter_slot_name = + StorageSlotName::new("miden::tutorials::counter").expect("valid slot name"); + let account_1 = client + .get_account(counter_contract_id) + .await + .unwrap() + .expect("counter contract not found"); + println!( + "counter contract storage: {:?}", + account_1.storage().get_item(&counter_slot_name) + ); + + let account_2 = client + .get_account(count_reader_contract.id()) + .await + .unwrap() + .expect("count reader contract not found"); + println!( + "count reader contract storage: {:?}", + account_2.storage().get_item(&count_reader_slot_name) + ); + assert_eq!( + account_2 + .storage() + .get_item(&count_reader_slot_name) + .unwrap(), + account_1.storage().get_item(&counter_slot_name).unwrap(), + "FPI must copy the current counter value", + ); + + Ok(()) +} +``` + +Run the standalone project with `TUTORIAL_NETWORK=testnet cargo run --release`. With `MIDEN_COUNTER_ACCOUNT_ID` set, the output shows the reader being created, the counter imported from testnet, and both storage slots containing the same count after the FPI transaction is confirmed. The final assertion assumes the counter is not concurrently incremented while the example runs; use a fresh counter for this check. + +### Running the example + +To run the checked-in example, return to the root of the [tutorials repository](https://github.com/0xMiden/tutorials/) and run: + +```bash +cd rust-client +TUTORIAL_NETWORK=testnet cargo run --release --bin counter_contract_fpi -- "$MIDEN_COUNTER_ACCOUNT_ID" +``` + +If `MIDEN_COUNTER_ACCOUNT_ID` is exported in your shell, you can omit `--` and the final argument. + +### Continue learning + +Next tutorial: [How to Use Unauthenticated Notes](unauthenticated_note_how_to.md) diff --git a/versioned_docs/version-0.16/builder/tutorials/recipes/rust/index.md b/versioned_docs/version-0.16/builder/tutorials/recipes/rust/index.md new file mode 100644 index 00000000..5ef55e66 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tutorials/recipes/rust/index.md @@ -0,0 +1,52 @@ +--- +title: "Rust Client" +sidebar_position: 1 +--- + +# Rust Client + +Rust library, which can be used to programmatically interact with the Miden rollup. + +The Miden Rust client can be used for a variety of things, including: + +- Deploying, testing, and creating transactions to interact with accounts and notes on Miden. +- Storing the state of accounts and notes locally. +- Generating and submitting proofs of transactions. + +This section of the docs is an overview of the different things one can achieve using the Rust client, and how to implement them. + +## Running the v0.16 examples + +The examples use Rust 1.98.1 and Miden v0.16. The repository includes the +required toolchain configuration and dependency lockfiles. + +From the repository root, run the Rust tutorials on testnet with Node.js and Yarn: + +```bash +yarn tutorials --rust +``` + +For devnet validation, run `TUTORIAL_NETWORK=devnet yarn tutorials --rust` instead. + +The runner uses a fresh store for each example and deploys a counter before the +FPI and public-account interaction examples. The [oracle tutorial](./oracle_tutorial.md) +requires an external deployment and is excluded from the default run. + +Testnet transactions pay fees in the native asset. The +shared helpers in `rust-client/src/lib.rs` in your v0.16 checkout +fund each executing account, synchronize before submission, and wait for confirmation. +They also filter `TX_FEE` notes when selecting tutorial notes. Set +`MIDEN_FAUCET_URL` if you need to override the network's public faucet API. + +To follow a tutorial in a standalone project, clone this repository as `tutorials` +and create the project alongside it. The tutorial's `Cargo.toml` includes the +local `rust-client` dependency and development optimization profile. Follow its +commands to select the Rust toolchain and copy the repository's `Cargo.lock`. +Run the first build without `--locked` so Cargo can add the new project's package +entry to the lockfile. + +Run standalone programs from their Cargo project directory. They load the shared +MASM files from `../tutorials/masm/`; the corresponding tutorial shows and explains +those sources. No additional MASM files need to be copied into the new project. + +Keep in mind that both the Rust client and the documentation are works-in-progress! diff --git a/versioned_docs/version-0.16/builder/tutorials/recipes/rust/mappings_in_masm_how_to.md b/versioned_docs/version-0.16/builder/tutorials/recipes/rust/mappings_in_masm_how_to.md new file mode 100644 index 00000000..be7f6127 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tutorials/recipes/rust/mappings_in_masm_how_to.md @@ -0,0 +1,397 @@ +--- +title: "How to Use Mappings in Miden Assembly" +sidebar_position: 10 +--- + +# How to Use Mappings in Miden Assembly + +_Using mappings in Miden assembly for storing key value pairs_ + +For toolchain requirements and shared fee helpers, see the [Rust client setup](./index.md#running-the-v016-examples). + +## Overview + +In this example, we will explore how to use mappings in Miden Assembly. Mappings are essential data structures that store key-value pairs. We will demonstrate how to create an account that contains a mapping and then call a procedure in that account to update the mapping. + +At a high level, this example involves: + +- Setting up an account with a mapping stored in one of its storage slots. +- Writing a smart contract in Miden Assembly that includes procedures to read from and write to the mapping. +- Creating a transaction script that calls these procedures. +- Using Rust code to deploy the account and submit a transaction that updates the mapping. + +## What we'll cover + +- **How to Use Mappings in Miden Assembly:** See how to create a smart contract that uses a mapping. +- **How to Link Libraries in Miden Assembly:** Demonstrate how to link procedures across Accounts, Notes, and Scripts. + +## Step-by-step process + +1. **Setting up an account with a mapping** + In this step, you create an account that has a storage slot configured as a mapping. The account smart contract code (shown below) defines procedures to write to and read from this mapping. + +2. **Creating a script that calls a procedure in the account:** + Next, you create a transaction script that calls the procedures defined in the account. This script sends the key-value data and then invokes the account procedure, which updates the mapping. + +3. **How to read and write to a mapping in MASM:** + Finally, we demonstrate how to use MASM instructions to interact with the mapping. The smart contract uses standard procedures to set a mapping item, retrieve a value from the mapping, and get the current mapping root. + +--- + +### Example of smart contract that uses a mapping + +```masm +use miden::protocol::active_account +use miden::protocol::native_account +use miden::core::sys +use {StorageMapKey} from miden::protocol::types + +# CONSTANTS +# ================================================================================================= + +const MAP_SLOT = word("miden::tutorials::mapping::map") + +# PUBLIC INTERFACE +# ================================================================================================= + +#! Stores VALUE under KEY in the mapping. +#! +#! Inputs: [KEY, VALUE, pad(8)] +#! Outputs: [pad(16)] +#! +#! Invocation: call +@account_procedure +pub proc write_to_map(key: StorageMapKey, value: word) + # the storage map is in the mapping slot + push.MAP_SLOT[0..2] + # => [slot_id_suffix, slot_id_prefix, KEY, VALUE, pad(8)] + + # set the key-value pair in the map + exec.native_account::set_map_item + # => [OLD_VALUE, pad(12)] + + dropw + # => [pad(16)] +end + +#! Returns the VALUE stored under KEY in the mapping. +#! +#! Inputs: [KEY, pad(12)] +#! Outputs: [VALUE, pad(12)] +#! +#! Invocation: call +@account_procedure +pub proc get_value_in_map(key: StorageMapKey) -> word + # the storage map is in the mapping slot + push.MAP_SLOT[0..2] + # => [slot_id_suffix, slot_id_prefix, KEY, pad(12)] + + exec.active_account::get_map_item + # => [VALUE, pad(12)] +end + +#! Returns the CURRENT_ROOT of the mapping. +#! +#! Inputs: [pad(16)] +#! Outputs: [CURRENT_ROOT, pad(12)] +#! +#! Invocation: call +@account_procedure +pub proc get_current_map_root() -> word + # get the current root from the mapping slot + push.MAP_SLOT[0..2] exec.active_account::get_item + # => [CURRENT_ROOT, pad(16)] + + exec.sys::truncate_stack + # => [CURRENT_ROOT, pad(12)] +end +``` + +### Explanation of the assembly code + +- **write_to_map:** + The procedure takes a key and a value as inputs. It pushes the slot ID prefix and suffix for the mapping slot onto the stack, then calls the `set_map_item` procedure from the account library to update the mapping. After updating the map, it drops the old value. +- **get_value_in_map:** + This procedure takes a key as input and retrieves the corresponding value from the mapping by calling `get_map_item` after pushing the mapping slot ID. + +- **get_current_map_root:** + This procedure retrieves the current root of the mapping by calling `get_item` with the mapping slot ID and then truncating the stack to leave only the mapping root. + +The Rust account below uses `NoAuth`, so anyone can import its public state and submit a mapping update without a signature. `NoAuth` handles fee payment and nonce changes; incrementing a nonce does not itself grant authorization. Use an appropriate authentication component when writes should be restricted. + +### Transaction script that calls the smart contract + +```masm +use miden_by_example::mapping_example_contract +use miden::core::sys + +#! Writes a mapping entry, reads it, and returns the current map root. +#! +#! Inputs: [ARGS, pad(12)] +#! Outputs: [CURRENT_ROOT, pad(12)] +#! +#! Where: +#! - ARGS contains unused transaction script arguments. +#! - CURRENT_ROOT is the mapping's Merkle root after the write. +#! +#! Invocation: dyncall +@transaction_script +pub proc main(args: word) -> word + dropw + # => [pad(16)] + + push.1.2.3.4 + push.0.0.0.0 + # => [KEY, VALUE, pad(16)] + + call.mapping_example_contract::write_to_map + # => [pad(24)] + + push.0.0.0.0 + # => [KEY, pad(24)] + + call.mapping_example_contract::get_value_in_map + # => [VALUE, pad(24)] + + dropw + # => [pad(24)] + + call.mapping_example_contract::get_current_map_root + # => [CURRENT_ROOT, pad(20)] + + exec.sys::truncate_stack + # => [CURRENT_ROOT, pad(12)] +end +``` + +### Explanation of the transaction script + +The transaction script does the following: + +- It pushes the value with `push.1.2.3.4`, then the key with `push.0.0.0.0`. The last pushed element is on top, so the stored value is `[4, 3, 2, 1]` and the key is `[0, 0, 0, 0]`. +- It calls the `write_to_map` procedure, which is defined in the account’s smart contract. This updates the mapping in the account. +- It then pushes the key again and calls `get_value_in_map` to retrieve the value associated with the key. +- Finally, it calls `get_current_map_root` to get the current state (root) of the mapping. + +The script calls the `write_to_map` procedure in the account which writes the key value pair to the mapping. + +--- + +### Rust code that sets everything up + +From the parent directory of your `tutorials` clone, create a sibling Cargo project: + +```bash +cargo new miden-mappings +cd miden-mappings +rustup override set 1.98.1 +cp ../tutorials/rust-client/Cargo.lock Cargo.lock +``` + +Add these dependencies and the development profile to your `Cargo.toml`: + +```toml +[dependencies] +rust-client = { path = "../tutorials/rust-client" } +miden-client = { version = "=0.16.0", features = ["testing", "tonic"] } +miden-client-sqlite-store = { version = "=0.16.0", package = "miden-client-sqlite-store" } +miden-protocol = { version = "=0.16.0" } +rand = { version = "0.10" } +tokio = { version = "1.48", features = ["rt-multi-thread", "net", "macros", "fs"] } + +[profile.dev] +opt-level = 2 +``` + +Below is the Rust code that deploys the smart contract, creates the transaction script, and submits a transaction to update the mapping in the account: + +```rust no_run +use rand::Rng; +use rust_client::TutorialClientExt; +use std::{path::PathBuf, sync::Arc}; + +use miden_client::{ + ClientError, + account::{ + AccountBuilder, AccountComponent, AccountType, StorageMap, StorageMapKey, StorageSlot, + StorageSlotName, + component::{AccountComponentMetadata, BasicWallet}, + }, + auth::NoAuth, + builder::ClientBuilder, + keystore::FilesystemKeyStore, + rpc::{GrpcClient, VerifyingRpcClient}, + transaction::TransactionRequestBuilder, +}; +use miden_client_sqlite_store::ClientBuilderSqliteExt; +use rust_client::{FeeConfig, TutorialNetwork, fund_account_for_fees}; + +#[tokio::main] +async fn main() -> Result<(), ClientError> { + // Initialize client + let network = TutorialNetwork::from_env()?; + let endpoint = network.endpoint(); + let timeout_ms = 10_000; + let rpc_client = Arc::new(VerifyingRpcClient::new(GrpcClient::new( + &endpoint, timeout_ms, + ))); + + // Initialize keystore + let keystore_path = PathBuf::from("./keystore"); + let keystore = Arc::new(FilesystemKeyStore::new(keystore_path).unwrap()); + + let store_path = PathBuf::from("./store.sqlite3"); + + let mut client = ClientBuilder::new() + .rpc(rpc_client) + .sqlite_store(store_path) + .authenticator(keystore.clone()) + .build() + .await?; + + let sync_summary = client.sync_state().await.unwrap(); + println!("Latest block: {}", sync_summary.block_num); + let fee_config = FeeConfig::from_client(&client, network).await?; + + // ------------------------------------------------------------------------- + // STEP 1: Deploy a smart contract with a mapping + // ------------------------------------------------------------------------- + println!("\n[STEP 1] Deploy a smart contract with a mapping"); + + // Read the MASM source from the tutorials repository. + let account_code = + std::fs::read_to_string("../tutorials/masm/accounts/mapping_example_contract.masm") + .unwrap(); + + // Storage slots are named in v0.16; the component only needs its mapping slot. + let storage_map = StorageMap::new(); + let map_slot_name = + StorageSlotName::new("miden::tutorials::mapping::map").expect("valid slot name"); + let storage_slot_map = StorageSlot::with_map(map_slot_name.clone(), storage_map.clone()); + + // Compile the account code into `AccountComponent` with one storage slot + let component_code = client + .code_builder() + .compile_component_code("miden_by_example::mapping_example_contract", &account_code) + .unwrap(); + let mapping_contract_component = AccountComponent::new( + component_code, + vec![storage_slot_map], + AccountComponentMetadata::new("miden_by_example::mapping_example_contract"), + ) + .unwrap(); + + // Init seed for the mapping contract + let mut init_seed = [0_u8; 32]; + client.rng().fill_bytes(&mut init_seed); + + // Build the new `Account` with the component + let mapping_example_contract = AccountBuilder::new(init_seed) + .account_type(AccountType::Public) + .with_component(mapping_contract_component.clone()) + .with_component(BasicWallet) + .with_component(NoAuth) + .build() + .unwrap(); + + client + .add_account(&mapping_example_contract, false) + .await + .unwrap(); + fund_account_for_fees(&mut client, mapping_example_contract.id(), &fee_config).await?; + + // ------------------------------------------------------------------------- + // STEP 2: Call the Mapping Contract with a Script + // ------------------------------------------------------------------------- + println!("\n[STEP 2] Call Mapping Contract With Script"); + + let script_code = + std::fs::read_to_string("../tutorials/masm/scripts/mapping_example_script.masm").unwrap(); + + // Compile the transaction script with the account code linked as a + // module on the same `CodeBuilder` chain. + let tx_script = client + .code_builder() + .with_linked_module("miden_by_example::mapping_example_contract", &account_code) + .unwrap() + .compile_tx_script(&script_code) + .unwrap(); + + // Build a transaction request with the custom script + let tx_increment_request = TransactionRequestBuilder::new() + .custom_script(tx_script) + .build() + .unwrap(); + + // Execute and submit the transaction + let tx_id = client + .submit_tutorial_transaction(mapping_example_contract.id(), tx_increment_request) + .await + .unwrap(); + + println!( + "View transaction on MidenScan: {}/tx/{:?}", + network.explorer_url(), + tx_id + ); + + client.sync_state().await.unwrap(); + + let account = client + .get_account(mapping_example_contract.id()) + .await + .unwrap() + .expect("mapping contract not found"); + let key = StorageMapKey::empty(); + println!( + "Mapping state\n Index: {:?}\n Key: {:?}\n Value: {:?}", + map_slot_name, + key, + account.storage().get_map_item(&map_slot_name, key) + ); + let value = account.storage().get_map_item(&map_slot_name, key).unwrap(); + assert_eq!( + value + .iter() + .map(|felt| felt.as_canonical_u64()) + .collect::>(), + vec![4, 3, 2, 1], + "the mapping must store the value written by the transaction script", + ); + + Ok(()) +} +``` + +### What the Rust code does + +- **Client Initialization:** + The client connects to Miden testnet and uses a SQLite store to track accounts, notes, and transactions. + +- **Deploying the Smart Contract:** + The account MASM is compiled into an `AccountComponent` with a named map slot. `AccountBuilder` creates the account locally; consuming its native-asset funding note publishes it on-chain. + +- **Creating and Executing a Transaction Script:** + A separate MASM script is compiled into a `TransactionScript`. This script calls the smart contract's procedures to write to and then read from the mapping. + +- **Displaying the Result:** + Finally, after the transaction is processed, the code reads the updated state of the mapping in the account. + +--- + +### Running the example + +For the standalone Cargo project, save the Rust code as `src/main.rs` and run `TUTORIAL_NETWORK=testnet cargo run --release`. + +To run the checked-in example, return to the root of the [tutorials repository](https://github.com/0xMiden/tutorials/) and run: + +```bash +cd rust-client +TUTORIAL_NETWORK=testnet cargo run --release --bin mapping_example +``` + +This example shows how the script calls the procedure in the account, which then updates the mapping stored within the account. The mapping update is verified by reading the mapping’s key-value pair after the transaction completes. + +### Continue learning + +Next tutorial: [How to Create Notes in Miden Assembly](creating_notes_in_masm_tutorial.md) diff --git a/versioned_docs/version-0.16/builder/tutorials/recipes/rust/mint_consume_create_tutorial.md b/versioned_docs/version-0.16/builder/tutorials/recipes/rust/mint_consume_create_tutorial.md new file mode 100644 index 00000000..07e2cc2b --- /dev/null +++ b/versioned_docs/version-0.16/builder/tutorials/recipes/rust/mint_consume_create_tutorial.md @@ -0,0 +1,603 @@ +--- +title: "Mint, Consume, and Create Notes" +sidebar_position: 3 +--- + +# Mint, Consume, and Create Notes + +_Using the Miden client in Rust to mint, consume, and create notes_ + +For toolchain requirements and shared fee helpers, see the [Rust client setup](./index.md#running-the-v016-examples). + +## Overview + +In the previous section, we initialized our repository and covered how to create an account and deploy a faucet. In this section, we will mint tokens from the faucet for _Alice_, consume the newly created notes, and demonstrate how to send assets to other accounts. + +## What we'll cover + +- Minting tokens from a faucet +- Consuming notes to fund an account +- Sending tokens to other users + +## Step 1: Minting tokens from the faucet + +To mint notes with tokens from the faucet we created, the client submits a mint transaction signed by the faucet's key. The faucet executes the transaction and creates a note for Alice; Alice later signs a separate transaction to consume it. + +_In essence, a transaction request is a structured template that outlines the data required to generate a zero-knowledge proof of a state change of an account. It specifies which input notes (if any) will be consumed, includes an optional transaction script to execute, and enumerates the set of notes expected to be created (if any)._ + +Below is an example of a transaction request minting tokens from the faucet for Alice. This code snippet creates and confirms five mint transactions, each producing one note containing 100 raw units of the tutorial asset. Transaction fees are paid in the separate native fee asset. + +Continue the same project from [Creating Accounts and Faucets](./create_deploy_tutorial.md), keeping its `Cargo.toml` and seeded `Cargo.lock`. Insert this snippet inside `main()`, after the previous steps and immediately before its final `Ok(())`: + +```rust ignore +//------------------------------------------------------------ +// STEP 3: Mint 5 notes of 100 tokens for Alice +//------------------------------------------------------------ +println!("\n[STEP 3] Minting 5 notes of 100 tokens each for Alice."); + +let amount: u64 = 100; +let fungible_asset = FungibleAsset::new(faucet_account.id(), amount).unwrap(); + +let mut minted_note_ids = Vec::new(); +for i in 1..=5 { + let transaction_request = TransactionRequestBuilder::new() + .build_mint_fungible_asset( + fungible_asset, + alice_account.id(), + NoteType::Public, + client.rng(), + ) + .unwrap(); + + minted_note_ids.extend( + transaction_request + .expected_output_own_notes() + .iter() + .map(Note::id), + ); + println!("tx request built"); + + let tx_id = client + .submit_tutorial_transaction(faucet_account.id(), transaction_request) + .await?; + println!( + "Minted note #{} of {} tokens for Alice. TX: {:?}", + i, amount, tx_id + ); +} +println!("All 5 notes minted for Alice successfully!"); + +// Re-sync so minted notes become visible +client.sync_state().await?; +``` + +## Step 2: Identifying consumable notes + +Once Alice has minted a note from the faucet, she will eventually want to spend the tokens that she received in the note created by the mint transaction. + +Minting a note from a faucet on Miden means a faucet account creates a new note targeted to the requesting account. The requesting account needs to consume this new note to have the assets appear in their account. + +To identify consumable notes, the Miden client provides `get_consumable_notes`. The `TutorialClientExt::get_consumable_tutorial_notes` wrapper used below filters out `TX_FEE` notes. Call it after syncing the client state. + +Track the output note IDs from each mint request and wait for those notes to be committed. Do not wait for the total consumable-note count to equal five: `TX_FEE` notes can also be consumable and make that condition impossible. The `wait_for_notes_by_id` helper in Step 3 polls for the specific minted notes with a timeout. + +#### Identifying which notes are available: + +```rust ignore +let consumable_notes = client + .get_consumable_tutorial_notes(Some(alice_account.id())) + .await?; +``` + +## Step 3: Consuming multiple notes in a single transaction: + +Now that we know how to identify notes ready to consume, let's consume the notes created by the faucet in a single transaction. After consuming the notes, Alice's wallet balance will be updated. + +The following code snippet identifies consumable notes and consumes them in a single transaction. + +Insert this snippet after the preceding steps inside `main()`, immediately before its final `Ok(())`: + +```rust ignore +//------------------------------------------------------------ +// STEP 4: Alice consumes all her notes +//------------------------------------------------------------ +println!("\n[STEP 4] Alice will now consume all of her notes to consolidate them."); + +// TX_FEE notes are also consumable. Select only the five P2ID notes we minted. +let notes = rust_client::wait_for_notes_by_id(&mut client, &minted_note_ids).await?; +assert_eq!(notes.len(), 5); +let transaction_request = TransactionRequestBuilder::new().build_consume_notes(notes)?; +let tx_id = client + .submit_tutorial_transaction(alice_account.id(), transaction_request) + .await?; +println!( + "All of Alice's notes consumed successfully. TX: {:?}", + tx_id +); +``` + +## Step 4: Sending tokens to other accounts + +After consuming the notes, Alice has tokens in her wallet. Now, she wants to send tokens to her friends. She has two options: create a separate transaction for each transfer or batch multiple transfers into a single transaction. + +_The standard asset transfer note on Miden is the P2ID note (Pay-to-Id). There is also the P2IDE (Pay-to-Id Extended) variant which allows for both timelocking the note (target can only spend the note after a certain block height) and for the note to be reclaimable (the creator of the note can reclaim the note after a certain block height)._ + +In our example, Alice will now send 50 tokens to 5 different accounts. + +For the sake of the example, the first four P2ID transfers are handled in a single transaction, and the fifth transfer is a standard P2ID transfer. + +### Output multiple P2ID notes in a single transaction + +To output multiple notes in a single transaction we need to create a list of our expected output notes. The expected output notes are the notes that we expect to create in our transaction request. + +In the snippet below, we create an empty vector, loop over four iterations using `1..=4`, build a P2ID note for each generated dummy account ID, and push each note onto the vector. We pass all four notes to `.own_output_notes()` and submit one transaction. The following step sends the fifth note. + +Insert this snippet after the preceding steps inside `main()`, immediately before its final `Ok(())`: + +```rust ignore +//------------------------------------------------------------ +// STEP 5: Alice sends 5 notes of 50 tokens to 5 users +//------------------------------------------------------------ +println!("\n[STEP 5] Alice sends 5 notes of 50 tokens each to 5 different users."); + +// Send 50 tokens to 4 accounts in one transaction +println!("Creating multiple P2ID notes for 4 target accounts in one transaction..."); +let mut p2id_notes = vec![]; + +// Creating 4 P2ID notes to 4 'dummy' AccountIds +for _ in 1..=4 { + let init_seed: [u8; 15] = { + let mut init_seed = [0_u8; 15]; + client.rng().fill_bytes(&mut init_seed); + init_seed + }; + let target_account_id = AccountId::dummy( + init_seed, + AccountIdVersion::Version1, + AccountType::Public, + AssetCallbackFlag::Disabled, + ); + + let send_amount = 50; + let fungible_asset = FungibleAsset::new(faucet_account.id(), send_amount).unwrap(); + + let p2id_note: Note = P2idNote::builder() + .sender(alice_account.id()) + .target(target_account_id) + .asset(fungible_asset) + .note_type(NoteType::Public) + .generate_serial_number(client.rng()) + .build()? + .into(); + p2id_notes.push(p2id_note); +} + +// Specifying output notes and creating a tx request to create them +let output_notes = p2id_notes; +let transaction_request = TransactionRequestBuilder::new() + .own_output_notes(output_notes) + .build() + .unwrap(); + +let tx_id = client + .submit_tutorial_transaction(alice_account.id(), transaction_request) + .await?; + +println!("Submitted a transaction with 4 P2ID notes. TX: {:?}", tx_id); +``` + +### Basic P2ID transfer + +`build_pay_to_id` creates the P2ID note and transaction request for a single transfer. Alice will use it to send tokens to one more account. + +Insert this snippet after the preceding steps inside `main()`, immediately before its final `Ok(())`: + +```rust ignore +println!("Submitting one more single P2ID transaction..."); +let init_seed: [u8; 15] = { + let mut init_seed = [0_u8; 15]; + client.rng().fill_bytes(&mut init_seed); + init_seed +}; +let target_account_id = AccountId::dummy( + init_seed, + AccountIdVersion::Version1, + AccountType::Public, + AssetCallbackFlag::Disabled, +); + +let send_amount = 50; +let fungible_asset = FungibleAsset::new(faucet_account.id(), send_amount).unwrap(); + +let payment = PaymentNoteDescription::new( + vec![fungible_asset.into()], + alice_account.id(), + target_account_id, +); +let transaction_request = TransactionRequestBuilder::new().build_pay_to_id( + payment, + NoteType::Public, + client.rng(), +)?; + +let tx_id = client + .submit_tutorial_transaction(alice_account.id(), transaction_request) + .await?; + +println!("Submitted final P2ID transaction. TX: {:?}", tx_id); +let alice = client + .get_account(alice_account.id()) + .await? + .expect("Alice exists"); +let balance = alice + .vault() + .get_balance(AssetId::new_fungible(faucet_account.id()))?; +assert_eq!(balance.as_u64(), 250, "Alice should retain 500 - 250 MID"); + +println!("\nAll steps completed successfully!"); +println!("Alice created a wallet, a faucet was deployed,"); +println!("5 notes of 100 tokens were minted to Alice, those notes were consumed,"); +println!("and then Alice sent 5 separate 50-token notes to 5 different users."); +``` + +Note: _`AccountId::dummy()` generates example IDs without deployable accounts or keys. These notes demonstrate creation and cannot be consumed by real recipients. Use actual recipient IDs when transferring useful assets._ + +## Summary + +Your `src/main.rs` function should now look like this: + +```rust no_run +use rand::Rng; +use rust_client::TutorialClientExt; +use std::{path::PathBuf, sync::Arc}; +use tokio::time::Duration; + +use miden_client::{ + ClientError, + account::{ + AccountBuilder, AccountId, AccountType, + component::{ + create_singlesig_user_fungible_faucet, BasicWallet, BurnPolicy, FungibleFaucet, + MintPolicy, TokenName, TokenPolicyManager, + }, + }, + asset::{AssetAmount, AssetCallbackFlag, AssetId, FungibleAsset, TokenSymbol}, + auth::{AuthSecretKey, AuthSingleSig}, + builder::ClientBuilder, + keystore::{FilesystemKeyStore, Keystore}, + note::{Note, NoteType, P2idNote}, + rpc::{GrpcClient, VerifyingRpcClient}, + transaction::{PaymentNoteDescription, TransactionRequestBuilder}, +}; +use miden_client_sqlite_store::ClientBuilderSqliteExt; +use miden_protocol::account::AccountIdVersion; +use rust_client::{FeeConfig, TutorialNetwork, fund_account_for_fees}; + +#[tokio::main] +async fn main() -> Result<(), ClientError> { + // Initialize client + let network = TutorialNetwork::from_env()?; + let endpoint = network.endpoint(); + let timeout_ms = 10_000; + let rpc_client = Arc::new(VerifyingRpcClient::new(GrpcClient::new( + &endpoint, timeout_ms, + ))); + + // Initialize keystore + let keystore_path = PathBuf::from("./keystore"); + let keystore = Arc::new(FilesystemKeyStore::new(keystore_path).unwrap()); + + let store_path = PathBuf::from("./store.sqlite3"); + + let mut client = ClientBuilder::new() + .rpc(rpc_client) + .sqlite_store(store_path) + .authenticator(keystore.clone()) + .build() + .await?; + + let sync_summary = client.sync_state().await.unwrap(); + println!("Latest block: {}", sync_summary.block_num); + let fee_config = FeeConfig::from_client(&client, network).await?; + + //------------------------------------------------------------ + // STEP 1: Create a basic wallet for Alice + //------------------------------------------------------------ + println!("\n[STEP 1] Creating a new account for Alice"); + + // Account seed + let mut init_seed = [0_u8; 32]; + client.rng().fill_bytes(&mut init_seed); + + let key_pair = AuthSecretKey::new_falcon512_poseidon2_with_rng(client.rng()); + + // Build the account + let alice_account = AccountBuilder::new(init_seed) + .account_type(AccountType::Public) + .with_component(AuthSingleSig::from_public_key(key_pair.public_key())) + .with_component(BasicWallet) + .build() + .unwrap(); + + // Add the account to the client + client.add_account(&alice_account, false).await?; + + // Add the key pair to the keystore + keystore + .add_key(&key_pair, alice_account.id()) + .await + .unwrap(); + + let alice_account_id_bech32 = alice_account.id().to_bech32(network.network_id()); + println!("Alice's account ID: {:?}", alice_account_id_bech32); + + fund_account_for_fees(&mut client, alice_account.id(), &fee_config).await?; + + //------------------------------------------------------------ + // STEP 2: Deploy a fungible faucet + //------------------------------------------------------------ + println!("\n[STEP 2] Deploying a new fungible faucet."); + + // Faucet seed + let mut init_seed = [0u8; 32]; + client.rng().fill_bytes(&mut init_seed); + + // Faucet parameters + let symbol = TokenSymbol::new("MID").unwrap(); + let decimals = 8; + let max_supply = AssetAmount::new(1_000_000).unwrap(); + + // Generate key pair + let key_pair = AuthSecretKey::new_falcon512_poseidon2_with_rng(client.rng()); + + // Build the faucet account. + // The faucet is a `FungibleFaucet` component plus a `TokenPolicyManager` + // that registers an "allow all" mint (and burn) policy; minting is rejected + // unless an active mint policy is present. + let faucet = FungibleFaucet::builder() + .name(TokenName::new("MID").unwrap()) + .symbol(symbol) + .decimals(decimals) + .max_supply(max_supply) + .build() + .unwrap(); + let policies = TokenPolicyManager::builder() + .active_mint_policy(MintPolicy::allow_all()) + .active_burn_policy(BurnPolicy::allow_all()) + .build(); + // The SDK factory includes BasicWallet so the faucet can receive the native fee asset. + let faucet_account = create_singlesig_user_fungible_faucet( + init_seed, + faucet, + AuthSingleSig::from_public_key(key_pair.public_key()), + policies, + AccountType::Public, + ) + .unwrap(); + + // Add the faucet to the client + client.add_account(&faucet_account, false).await?; + + // Add the key pair to the keystore + keystore + .add_key(&key_pair, faucet_account.id()) + .await + .unwrap(); + + let faucet_account_id_bech32 = faucet_account.id().to_bech32(network.network_id()); + println!("Faucet account ID: {:?}", faucet_account_id_bech32); + + fund_account_for_fees(&mut client, faucet_account.id(), &fee_config).await?; + + // Resync to show newly deployed faucet + client.sync_state().await?; + tokio::time::sleep(Duration::from_secs(2)).await; + + //------------------------------------------------------------ + // STEP 3: Mint 5 notes of 100 tokens for Alice + //------------------------------------------------------------ + println!("\n[STEP 3] Minting 5 notes of 100 tokens each for Alice."); + + let amount: u64 = 100; + let fungible_asset = FungibleAsset::new(faucet_account.id(), amount).unwrap(); + + let mut minted_note_ids = Vec::new(); + for i in 1..=5 { + let transaction_request = TransactionRequestBuilder::new() + .build_mint_fungible_asset( + fungible_asset, + alice_account.id(), + NoteType::Public, + client.rng(), + ) + .unwrap(); + + minted_note_ids.extend( + transaction_request + .expected_output_own_notes() + .iter() + .map(Note::id), + ); + println!("tx request built"); + + let tx_id = client + .submit_tutorial_transaction(faucet_account.id(), transaction_request) + .await?; + println!( + "Minted note #{} of {} tokens for Alice. TX: {:?}", + i, amount, tx_id + ); + } + println!("All 5 notes minted for Alice successfully!"); + + // Re-sync so minted notes become visible + client.sync_state().await?; + + //------------------------------------------------------------ + // STEP 4: Alice consumes all her notes + //------------------------------------------------------------ + println!("\n[STEP 4] Alice will now consume all of her notes to consolidate them."); + + // TX_FEE notes are also consumable. Select only the five P2ID notes we minted. + let notes = rust_client::wait_for_notes_by_id(&mut client, &minted_note_ids).await?; + assert_eq!(notes.len(), 5); + let transaction_request = TransactionRequestBuilder::new().build_consume_notes(notes)?; + let tx_id = client + .submit_tutorial_transaction(alice_account.id(), transaction_request) + .await?; + println!( + "All of Alice's notes consumed successfully. TX: {:?}", + tx_id + ); + + //------------------------------------------------------------ + // STEP 5: Alice sends 5 notes of 50 tokens to 5 users + //------------------------------------------------------------ + println!("\n[STEP 5] Alice sends 5 notes of 50 tokens each to 5 different users."); + + // Send 50 tokens to 4 accounts in one transaction + println!("Creating multiple P2ID notes for 4 target accounts in one transaction..."); + let mut p2id_notes = vec![]; + + // Creating 4 P2ID notes to 4 'dummy' AccountIds + for _ in 1..=4 { + let init_seed: [u8; 15] = { + let mut init_seed = [0_u8; 15]; + client.rng().fill_bytes(&mut init_seed); + init_seed + }; + let target_account_id = AccountId::dummy( + init_seed, + AccountIdVersion::Version1, + AccountType::Public, + AssetCallbackFlag::Disabled, + ); + + let send_amount = 50; + let fungible_asset = FungibleAsset::new(faucet_account.id(), send_amount).unwrap(); + + let p2id_note: Note = P2idNote::builder() + .sender(alice_account.id()) + .target(target_account_id) + .asset(fungible_asset) + .note_type(NoteType::Public) + .generate_serial_number(client.rng()) + .build()? + .into(); + p2id_notes.push(p2id_note); + } + + // Specifying output notes and creating a tx request to create them + let output_notes = p2id_notes; + let transaction_request = TransactionRequestBuilder::new() + .own_output_notes(output_notes) + .build() + .unwrap(); + + let tx_id = client + .submit_tutorial_transaction(alice_account.id(), transaction_request) + .await?; + + println!("Submitted a transaction with 4 P2ID notes. TX: {:?}", tx_id); + + println!("Submitting one more single P2ID transaction..."); + let init_seed: [u8; 15] = { + let mut init_seed = [0_u8; 15]; + client.rng().fill_bytes(&mut init_seed); + init_seed + }; + let target_account_id = AccountId::dummy( + init_seed, + AccountIdVersion::Version1, + AccountType::Public, + AssetCallbackFlag::Disabled, + ); + + let send_amount = 50; + let fungible_asset = FungibleAsset::new(faucet_account.id(), send_amount).unwrap(); + + let payment = PaymentNoteDescription::new( + vec![fungible_asset.into()], + alice_account.id(), + target_account_id, + ); + let transaction_request = TransactionRequestBuilder::new().build_pay_to_id( + payment, + NoteType::Public, + client.rng(), + )?; + + let tx_id = client + .submit_tutorial_transaction(alice_account.id(), transaction_request) + .await?; + + println!("Submitted final P2ID transaction. TX: {:?}", tx_id); + let alice = client + .get_account(alice_account.id()) + .await? + .expect("Alice exists"); + let balance = alice + .vault() + .get_balance(AssetId::new_fungible(faucet_account.id()))?; + assert_eq!(balance.as_u64(), 250, "Alice should retain 500 - 250 MID"); + + println!("\nAll steps completed successfully!"); + println!("Alice created a wallet, a faucet was deployed,"); + println!("5 notes of 100 tokens were minted to Alice, those notes were consumed,"); + println!("and then Alice sent 5 separate 50-token notes to 5 different users."); + + Ok(()) +} +``` + +Let's run the `src/main.rs` program again: + +```bash +TUTORIAL_NETWORK=testnet cargo run --release +``` + +The following is an abbreviated output; IDs vary and the helper also prints funding and transaction confirmations: + +```text +Latest block: + +[STEP 1] Creating a new account for Alice +Alice's account ID: "" + +[STEP 2] Deploying a new fungible faucet. +Faucet account ID: "" + +[STEP 3] Minting 5 notes of 100 tokens each for Alice. +tx request built +Minted note #1 of 100 tokens for Alice. TX: +... +Minted note #5 of 100 tokens for Alice. TX: +All 5 notes minted for Alice successfully! + +[STEP 4] Alice will now consume all of her notes to consolidate them. +All of Alice's notes consumed successfully. TX: + +[STEP 5] Alice sends 5 notes of 50 tokens each to 5 different users. +Creating multiple P2ID notes for 4 target accounts in one transaction... +Submitted a transaction with 4 P2ID notes. TX: +Submitting one more single P2ID transaction... +Submitted final P2ID transaction. TX: + +All steps completed successfully! +Alice created a wallet, a faucet was deployed, +5 notes of 100 tokens were minted to Alice, those notes were consumed, +and then Alice sent 5 separate 50-token notes to 5 different users. +``` + +### Running the example + +From the root of your `tutorials` clone, run the checked-in example: + +```bash +cd rust-client +TUTORIAL_NETWORK=testnet cargo run --release --bin create_mint_consume_send +``` + +### Continue learning + +Next tutorial: [Deploying a Counter Contract](counter_contract_tutorial.md) diff --git a/versioned_docs/version-0.16/builder/tutorials/recipes/rust/network_transactions_tutorial.md b/versioned_docs/version-0.16/builder/tutorials/recipes/rust/network_transactions_tutorial.md new file mode 100644 index 00000000..ef41d2d7 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tutorials/recipes/rust/network_transactions_tutorial.md @@ -0,0 +1,845 @@ +--- +title: "Network Transactions on Miden" +sidebar_position: 6 +--- + +# Network Transactions on Miden + +_Using the Miden client in Rust to deploy and interact with smart contracts using network transactions_ + +For toolchain requirements and shared fee helpers, see the [Rust client setup](./index.md#running-the-v016-examples). + +## Overview + +In this tutorial, we will explore Network Transactions (NTXs) on Miden - a powerful feature that enables autonomous smart contract execution and public shared state management. Unlike local transactions that require users to execute and prove, network transactions are executed and proven by a network transaction builder. + +We'll build a public network counter using the same MASM code as the regular counter. `AuthNetworkAccount` configures the note scripts that the network transaction builder may execute, and a fee policy prices those notes. The increment note also carries a `NetworkAccountTarget` attachment identifying its target. See the [account changes migration guide](https://docs.miden.xyz/builder/migration/account-changes). + +Deployment and subsequent updates are different operations. On a fee-enabled network, consuming the initial native-asset funding note publishes the new network account with count **0**. After publication, the public RPC rejects user-submitted transactions that directly update an existing network account. Alice must publish an increment note from her own account; the network transaction builder consumes it and changes the counter to **1**. This restriction is enforced by the [node's submission handler](https://github.com/0xMiden/node/blob/v0.16.0/crates/rpc/src/server/api/submit_proven_tx.rs#L94). + +## What we'll cover + +- Understanding Network Transactions and when to use them +- Deploying public smart contracts that the network operator can execute +- Publishing a new network account through its initial funding transaction +- Creating network notes for user interactions +- Validating network transaction results + +## Prerequisites + +This tutorial assumes you have completed the [counter contract tutorial](counter_contract_tutorial.md) and understand basic Miden assembly. + +## What are Network Transactions? + +Network transactions are executed and proven by the Miden operator rather than the client. They are useful for: + +- **Public shared state**: Multiple users can publish notes targeting the same contract; the network transaction builder orders their execution +- **Autonomous execution**: Smart contracts can execute when conditions are met without user intervention +- **Resource-constrained devices**: Clients that can't generate ZK proofs efficiently +- **AMM applications**: Using network notes, you can build sophisticated AMMs where trades execute automatically + +The account state and increment notes in this example are public, so the operator can see the transaction inputs. + +## Step 1: Initialize your repository + +From the parent directory of your `tutorials` clone, create a sibling Cargo project: + +```bash +cargo new miden-network-transactions +cd miden-network-transactions +rustup override set 1.98.1 +cp ../tutorials/rust-client/Cargo.lock Cargo.lock +``` + +Add the following dependencies to your `Cargo.toml` file: + +```toml +[dependencies] +# Clone tutorials next to this Cargo project (see Rust client setup). +rust-client = { path = "../tutorials/rust-client" } +miden-client = { version = "=0.16.0", features = ["testing", "tonic"] } +miden-client-sqlite-store = { version = "=0.16.0", package = "miden-client-sqlite-store" } +miden-protocol = { version = "=0.16.0" } +rand = { version = "0.10" } +tokio = { version = "1.48", features = ["rt-multi-thread", "net", "macros", "fs"] } + +[profile.dev] +opt-level = 2 +``` + +## Step 2: Set up MASM files + +The example reads the counter and note sources from the repository’s `masm/` directory. + +### Counter Contract + +We'll use the same counter contract MASM code as the regular counter tutorial. The key difference is in the Rust configuration, not the MASM code. + +The counter is defined in `masm/accounts/counter.masm`: + +```masm +use miden::protocol::active_account +use miden::protocol::native_account +use miden::core::sys + +# CONSTANTS +# ================================================================================================= + +const COUNTER_SLOT = word("miden::tutorials::counter") + +# PUBLIC INTERFACE +# ================================================================================================= + +#! Returns the current count. +#! +#! Inputs: [pad(16)] +#! Outputs: [count, pad(15)] +#! +#! Invocation: call +@account_procedure +pub proc get_count() -> felt + push.COUNTER_SLOT[0..2] exec.active_account::get_item + # => [[count, 0, 0, 0], pad(16)] + + exec.sys::truncate_stack + # => [count, pad(15)] +end + +#! Increments the current count by one. +#! +#! Inputs: [pad(16)] +#! Outputs: [pad(16)] +#! +#! Invocation: call +@account_procedure +pub proc increment_count() + push.COUNTER_SLOT[0..2] exec.active_account::get_item + # => [[count, 0, 0, 0], pad(16)] + + add.1 + # => [[count + 1, 0, 0, 0], pad(16)] + + push.COUNTER_SLOT[0..2] exec.native_account::set_item + # => [OLD_VALUE, pad(16)] + + dropw + # => [pad(16)] + + exec.sys::truncate_stack + # => [pad(16)] +end +``` + +### Initial deployment + +Creating an account locally does not publish it. The funding helper below creates +the new account's first committed transaction by consuming its native-asset note. +No separate increment transaction script is needed on a fee-enabled network. The +empty deployment request shown later is only for networks with zero fees, where +funding does not perform that initial transaction. + +### Network Note for User Interaction + +The increment note is defined in `masm/notes/network_increment_note.masm`. Note scripts are compiled as libraries; the `@note_script` attribute marks the entrypoint procedure. + +```masm +use external_contract::counter_contract + +#! Increments the network counter when this note is consumed. +#! +#! Inputs: [ARGS, pad(12)] +#! Outputs: [pad(16)] +#! +#! Where: +#! - ARGS contains unused note script arguments. +#! +#! Invocation: dyncall +@note_script +pub proc main(args: word) + dropw + # => [pad(16)] + + call.counter_contract::increment_count + # => [pad(16)] +end +``` + +After deployment, users will interact with the contract through these network notes. + +## Step 3: Initialize the client and create a user account + +Before deploying the network account and creating network notes, we need to set up the client and create a user account that will interact with our network contract. + +Copy and paste the following code into your `src/main.rs` file: + +```rust no_run +use rust_client::TutorialClientExt; +use std::{collections::BTreeSet, path::PathBuf, sync::Arc}; + +use miden_client::{ + Client, ClientError, Felt, Word, + account::{ + AccountBuilder, AccountComponent, AccountType, StorageSlot, StorageSlotName, + component::{ + AccountComponentMetadata, AuthNetworkAccount, BasicConstantFeePolicy, BasicWallet, + FeePolicy, FeePolicyManager, + }, + }, + asset::AssetAmount, + auth::{AuthSecretKey, AuthSingleSig}, + builder::ClientBuilder, + crypto::FeltRng, + keystore::{FilesystemKeyStore, Keystore}, + note::{ + NetworkAccountTarget, Note, NoteAssets, NoteAttachments, NoteError, NoteExecutionHint, + NoteRecipient, NoteStorage, NoteTag, NoteType, P2idNote, PartialNoteMetadata, + }, + rpc::{GrpcClient, VerifyingRpcClient}, + transaction::{ExpirationTransactionScript, TransactionId, TransactionRequestBuilder}, +}; +use miden_client_sqlite_store::ClientBuilderSqliteExt; +use rand::Rng; +use rust_client::{FeeConfig, TutorialNetwork, fund_account_for_fees}; +use tokio::time::{Duration, sleep}; + +/// Waits for a specific transaction to be committed. +async fn wait_for_tx( + client: &mut Client, + tx_id: TransactionId, +) -> Result<(), ClientError> { + rust_client::wait_for_transaction(client, tx_id).await +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Initialize client + let network = TutorialNetwork::from_env()?; + let endpoint = network.endpoint(); + let timeout_ms = 10_000; + let rpc_client = Arc::new(VerifyingRpcClient::new(GrpcClient::new( + &endpoint, timeout_ms, + ))); + + // Initialize keystore + let keystore_path = PathBuf::from("./keystore"); + let keystore = Arc::new(FilesystemKeyStore::new(keystore_path).unwrap()); + + let store_path = PathBuf::from("./store.sqlite3"); + + let mut client = ClientBuilder::new() + .rpc(rpc_client) + .sqlite_store(store_path) + .authenticator(keystore.clone()) + .build() + .await?; + + let sync_summary = client.sync_state().await.unwrap(); + println!("Latest block: {}", sync_summary.block_num); + let fee_config = FeeConfig::from_client(&client, network).await?; + let fee_faucet_id = fee_config.native_fee_faucet_id(); + + // ------------------------------------------------------------------------- + // STEP 1: Create Basic User Account + // ------------------------------------------------------------------------- + println!("\n[STEP 1] Creating a new account for Alice"); + + // Account seed + let mut init_seed = [0_u8; 32]; + client.rng().fill_bytes(&mut init_seed); + + let key_pair = AuthSecretKey::new_falcon512_poseidon2_with_rng(client.rng()); + + // Build the account + let alice_account = AccountBuilder::new(init_seed) + .account_type(AccountType::Public) + .with_component(AuthSingleSig::from_public_key(key_pair.public_key())) + .with_component(BasicWallet) + .build() + .unwrap(); + + // Add the account to the client + client.add_account(&alice_account, false).await?; + + // Add the key pair to the keystore + keystore + .add_key(&key_pair, alice_account.id()) + .await + .unwrap(); + fund_account_for_fees(&mut client, alice_account.id(), &fee_config).await?; + + println!( + "Alice's account ID: {:?}", + alice_account.id().to_bech32(network.network_id()) + ); + + Ok(()) +} +``` + +This step initializes the Miden client and creates a basic user account (Alice) that will interact with our network contract. + +## Step 4: Create the network counter smart contract + +Build a public account with `AuthNetworkAccount`, the counter component, and +`BasicWallet`. Compile the increment note first so its root can be allowlisted. +Also allow the P2ID funding note. Use `AuthNetworkAccount::custom` for this minimal +account and explicitly allow `ExpirationTransactionScript::script_root()`, which +the network builder uses. No configuration or sponsorship note scripts are +enabled: this account does not implement their required authority components. +There is no custom increment transaction script to allowlist. The zero per-note +policy charge does not remove the network's verification fee: the account pays +that fee from its funded native-asset balance. + +Insert this code inside `main`, immediately before its final `Ok(())`: + +```rust ignore +// ------------------------------------------------------------------------- +// STEP 2: Create Network Counter Smart Contract +// ------------------------------------------------------------------------- +println!("\n[STEP 2] Creating a network counter smart contract"); + +// Read the MASM source from the tutorials repository. +let counter_code = std::fs::read_to_string("../tutorials/masm/accounts/counter.masm").unwrap(); +let network_note_code = + std::fs::read_to_string("../tutorials/masm/notes/network_increment_note.masm").unwrap(); + +// An account is a *network account* (one the network +// transaction builder executes on a user's behalf) if and only if it is +// public AND carries the `AuthNetworkAccount` auth component. That component +// holds an allowlist of note scripts the network builder may execute. +// Compile the increment note first so its root can be included at creation. +let note_script = client + .code_builder() + .with_linked_module("external_contract::counter_contract", &counter_code)? + .compile_note_script(&network_note_code)?; +let note_script_root = note_script.root(); + +// Compile the counter MASM into an account component +let counter_slot_name = + StorageSlotName::new("miden::tutorials::counter").expect("valid slot name"); +let component_code = client + .code_builder() + .compile_component_code("external_contract::counter_contract", &counter_code)?; +let counter_component = AccountComponent::new( + component_code, + vec![StorageSlot::with_value( + counter_slot_name.clone(), + [Felt::new_unchecked(0); 4].into(), + )], + AccountComponentMetadata::new("external_contract::counter_contract"), +)?; + +// Generate a random seed for the account +let mut init_seed = [0_u8; 32]; +client.rng().fill_bytes(&mut init_seed); + +// Build the public network account with the increment and funding notes allowed. +let fee_policy: FeePolicy = BasicConstantFeePolicy::new() + .with_fees( + [note_script_root, P2idNote::script_root()].map(|root| (root, AssetAmount::ZERO)), + ) + .into(); +let fee_policy_manager = FeePolicyManager::builder() + .fee_faucet_id(fee_faucet_id) + .active_fee_policy(fee_policy) + .build(); +// Match the protocol/node counter example: only permit the two note scripts +// this account implements. Config notes need Authority, which it does not have. +// The canonical expiration script is required by the network builder. +let network_auth = AuthNetworkAccount::custom( + BTreeSet::from([note_script_root, P2idNote::script_root()]), + fee_policy_manager, +)? +.with_allowed_tx_scripts([ExpirationTransactionScript::script_root()]); +let counter_contract = AccountBuilder::new(init_seed) + .account_type(AccountType::Public) + .with_components(network_auth) + .with_component(counter_component) + .with_component(BasicWallet) + .build() + .unwrap(); + +client.add_account(&counter_contract, false).await.unwrap(); +fund_account_for_fees(&mut client, counter_contract.id(), &fee_config).await?; + +println!( + "contract id: {:?}", + counter_contract.id().to_bech32(network.network_id()) +); +``` + +This step creates and funds a public network account. On a fee-enabled network, +the funding transaction also publishes it. Its counter remains zero. + +## Step 5: Confirm publication of the network account + +When fees are active, initial funding has already published the network account. Do not send +another direct increment transaction from the user client: the node rejects +user-submitted updates to existing network accounts. Only the zero-fee path needs +an explicit first deployment transaction here. + +Insert this code after account creation, inside `main` and before `Ok(())`: + +```rust ignore +// ------------------------------------------------------------------------- +// STEP 3: Publish the network account +// ------------------------------------------------------------------------- +println!("\n[STEP 3] Deploy network counter smart contract"); + +// On a fee-enabled network, consuming the funding note already published this +// account. RPC permits users to deploy new network accounts, but rejects +// user-submitted transactions for existing ones. Subsequent increments must +// be requested by notes and executed by the network transaction builder. +if !fee_config.fees_are_active() { + let deployment = TransactionRequestBuilder::new().build()?; + client + .submit_tutorial_transaction(counter_contract.id(), deployment) + .await?; +} +println!("Network counter deployed; initial count is 0"); +``` + +The initial committed count is zero. All later counter updates go through network +notes executed by the network transaction builder. + +## Step 6: Create a network note for user interaction + +Alice publishes a public increment note from her own funded account. The network +transaction builder then consumes that note on the counter's behalf, changing +the counter from zero to one. Confirmation of Alice's transaction alone is not +enough; the example also waits for the counter's updated state. + +Replace the final `Ok(())` in `main` with the following fragment. Its polling loop and final `Err(...)` expressions provide the function's result: + +```rust ignore +// ------------------------------------------------------------------------- +// STEP 4: Prepare & Create the Network Note +// ------------------------------------------------------------------------- +println!("\n[STEP 4] Creating a network note for network counter contract"); + +// Create and submit the network note that will increment the counter +// Generate a random serial number for the note +let serial_num = client.rng().draw_word(); + +// Reuse the `note_script` compiled in STEP 2 (its root is allowlisted on the +// account, so the network transaction builder will execute this note). +let note_storage = NoteStorage::new([].to_vec())?; +let recipient = NoteRecipient::new(serial_num, note_script, note_storage); + +// Set up note metadata - tag it with the counter contract ID so it gets consumed +let tag = NoteTag::with_account_target(counter_contract.id()); + +let attachment = NetworkAccountTarget::new(counter_contract.id(), NoteExecutionHint::Always) + .map_err(|e| NoteError::other(e.to_string()))? + .into(); +let metadata = PartialNoteMetadata::new(alice_account.id(), NoteType::Public).with_tag(tag); +let attachments = NoteAttachments::new(vec![attachment]).unwrap(); + +// Create the complete note +let increment_note = + Note::with_attachments(NoteAssets::default(), metadata, recipient, attachments); + +// Build and submit the transaction containing the note +let note_req = TransactionRequestBuilder::new() + .own_output_notes(vec![increment_note]) + .build()?; + +let note_tx_id = client + .submit_tutorial_transaction(alice_account.id(), note_req) + .await?; + +println!( + "View transaction on MidenScan: {}/tx/{:?}", + network.explorer_url(), + note_tx_id +); + +client.sync_state().await?; + +println!("network increment note creation tx submitted, waiting for onchain commitment"); + +// Wait for the note transaction to be committed +wait_for_tx(&mut client, note_tx_id).await.unwrap(); + +// Waiting for network note to be picked up by the network transaction builder +sleep(Duration::from_secs(6)).await; + +let mut last_val = None; +for _ in 0..24 { + client.sync_state().await?; + + // Checking updated state + let new_account_state = client.get_account(counter_contract.id()).await.unwrap(); + + if let Some(account) = new_account_state.as_ref() { + let count: Word = account + .storage() + .get_item(&counter_slot_name) + .unwrap() + .into(); + let val = count[0].as_canonical_u64(); + if val == 1 { + println!("🔢 Final counter value: {}", val); + return Ok(()); + } + last_val = Some(val); + } + + // Give the network note builder time to process the note. + sleep(Duration::from_secs(6)).await; +} + +// The network note was submitted, but it is executed asynchronously by the +// network transaction builder. If the counter has not reached 1 within the +// polling window, the tutorial's final state is unconfirmed, so fail rather +// than claim success. +if let Some(val) = last_val { + Err(format!( + "Counter did not reach the expected value 1 within the timeout (last observed {}). \ + The network note was submitted but its execution is still pending on the network \ + transaction builder; re-run or check Midenscan.", + val + ) + .into()) +} else { + Err("Counter state was not available within the timeout; the network note execution is still pending." + .into()) +} +``` + +## Complete example + +Your complete `src/main.rs` file should look like this: + +```rust no_run +use rust_client::TutorialClientExt; +use std::{collections::BTreeSet, path::PathBuf, sync::Arc}; + +use miden_client::{ + Client, ClientError, Felt, Word, + account::{ + AccountBuilder, AccountComponent, AccountType, StorageSlot, StorageSlotName, + component::{ + AccountComponentMetadata, AuthNetworkAccount, BasicConstantFeePolicy, BasicWallet, + FeePolicy, FeePolicyManager, + }, + }, + asset::AssetAmount, + auth::{AuthSecretKey, AuthSingleSig}, + builder::ClientBuilder, + crypto::FeltRng, + keystore::{FilesystemKeyStore, Keystore}, + note::{ + NetworkAccountTarget, Note, NoteAssets, NoteAttachments, NoteError, NoteExecutionHint, + NoteRecipient, NoteStorage, NoteTag, NoteType, P2idNote, PartialNoteMetadata, + }, + rpc::{GrpcClient, VerifyingRpcClient}, + transaction::{ExpirationTransactionScript, TransactionId, TransactionRequestBuilder}, +}; +use miden_client_sqlite_store::ClientBuilderSqliteExt; +use rand::Rng; +use rust_client::{FeeConfig, TutorialNetwork, fund_account_for_fees}; +use tokio::time::{Duration, sleep}; + +/// Waits for a specific transaction to be committed. +async fn wait_for_tx( + client: &mut Client, + tx_id: TransactionId, +) -> Result<(), ClientError> { + rust_client::wait_for_transaction(client, tx_id).await +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Initialize client + let network = TutorialNetwork::from_env()?; + let endpoint = network.endpoint(); + let timeout_ms = 10_000; + let rpc_client = Arc::new(VerifyingRpcClient::new(GrpcClient::new( + &endpoint, timeout_ms, + ))); + + // Initialize keystore + let keystore_path = PathBuf::from("./keystore"); + let keystore = Arc::new(FilesystemKeyStore::new(keystore_path).unwrap()); + + let store_path = PathBuf::from("./store.sqlite3"); + + let mut client = ClientBuilder::new() + .rpc(rpc_client) + .sqlite_store(store_path) + .authenticator(keystore.clone()) + .build() + .await?; + + let sync_summary = client.sync_state().await.unwrap(); + println!("Latest block: {}", sync_summary.block_num); + let fee_config = FeeConfig::from_client(&client, network).await?; + let fee_faucet_id = fee_config.native_fee_faucet_id(); + + // ------------------------------------------------------------------------- + // STEP 1: Create Basic User Account + // ------------------------------------------------------------------------- + println!("\n[STEP 1] Creating a new account for Alice"); + + // Account seed + let mut init_seed = [0_u8; 32]; + client.rng().fill_bytes(&mut init_seed); + + let key_pair = AuthSecretKey::new_falcon512_poseidon2_with_rng(client.rng()); + + // Build the account + let alice_account = AccountBuilder::new(init_seed) + .account_type(AccountType::Public) + .with_component(AuthSingleSig::from_public_key(key_pair.public_key())) + .with_component(BasicWallet) + .build() + .unwrap(); + + // Add the account to the client + client.add_account(&alice_account, false).await?; + + // Add the key pair to the keystore + keystore + .add_key(&key_pair, alice_account.id()) + .await + .unwrap(); + fund_account_for_fees(&mut client, alice_account.id(), &fee_config).await?; + + println!( + "Alice's account ID: {:?}", + alice_account.id().to_bech32(network.network_id()) + ); + + // ------------------------------------------------------------------------- + // STEP 2: Create Network Counter Smart Contract + // ------------------------------------------------------------------------- + println!("\n[STEP 2] Creating a network counter smart contract"); + + // Read the MASM source from the tutorials repository. + let counter_code = std::fs::read_to_string("../tutorials/masm/accounts/counter.masm").unwrap(); + let network_note_code = + std::fs::read_to_string("../tutorials/masm/notes/network_increment_note.masm").unwrap(); + + // An account is a *network account* (one the network + // transaction builder executes on a user's behalf) if and only if it is + // public AND carries the `AuthNetworkAccount` auth component. That component + // holds an allowlist of note scripts the network builder may execute. + // Compile the increment note first so its root can be included at creation. + let note_script = client + .code_builder() + .with_linked_module("external_contract::counter_contract", &counter_code)? + .compile_note_script(&network_note_code)?; + let note_script_root = note_script.root(); + + // Compile the counter MASM into an account component + let counter_slot_name = + StorageSlotName::new("miden::tutorials::counter").expect("valid slot name"); + let component_code = client + .code_builder() + .compile_component_code("external_contract::counter_contract", &counter_code)?; + let counter_component = AccountComponent::new( + component_code, + vec![StorageSlot::with_value( + counter_slot_name.clone(), + [Felt::new_unchecked(0); 4].into(), + )], + AccountComponentMetadata::new("external_contract::counter_contract"), + )?; + + // Generate a random seed for the account + let mut init_seed = [0_u8; 32]; + client.rng().fill_bytes(&mut init_seed); + + // Build the public network account with the increment and funding notes allowed. + let fee_policy: FeePolicy = BasicConstantFeePolicy::new() + .with_fees( + [note_script_root, P2idNote::script_root()].map(|root| (root, AssetAmount::ZERO)), + ) + .into(); + let fee_policy_manager = FeePolicyManager::builder() + .fee_faucet_id(fee_faucet_id) + .active_fee_policy(fee_policy) + .build(); + // Match the protocol/node counter example: only permit the two note scripts + // this account implements. Config notes need Authority, which it does not have. + // The canonical expiration script is required by the network builder. + let network_auth = AuthNetworkAccount::custom( + BTreeSet::from([note_script_root, P2idNote::script_root()]), + fee_policy_manager, + )? + .with_allowed_tx_scripts([ExpirationTransactionScript::script_root()]); + let counter_contract = AccountBuilder::new(init_seed) + .account_type(AccountType::Public) + .with_components(network_auth) + .with_component(counter_component) + .with_component(BasicWallet) + .build() + .unwrap(); + + client.add_account(&counter_contract, false).await.unwrap(); + fund_account_for_fees(&mut client, counter_contract.id(), &fee_config).await?; + + println!( + "contract id: {:?}", + counter_contract.id().to_bech32(network.network_id()) + ); + + // ------------------------------------------------------------------------- + // STEP 3: Publish the network account + // ------------------------------------------------------------------------- + println!("\n[STEP 3] Deploy network counter smart contract"); + + // On a fee-enabled network, consuming the funding note already published this + // account. RPC permits users to deploy new network accounts, but rejects + // user-submitted transactions for existing ones. Subsequent increments must + // be requested by notes and executed by the network transaction builder. + if !fee_config.fees_are_active() { + let deployment = TransactionRequestBuilder::new().build()?; + client + .submit_tutorial_transaction(counter_contract.id(), deployment) + .await?; + } + println!("Network counter deployed; initial count is 0"); + + // ------------------------------------------------------------------------- + // STEP 4: Prepare & Create the Network Note + // ------------------------------------------------------------------------- + println!("\n[STEP 4] Creating a network note for network counter contract"); + + // Create and submit the network note that will increment the counter + // Generate a random serial number for the note + let serial_num = client.rng().draw_word(); + + // Reuse the `note_script` compiled in STEP 2 (its root is allowlisted on the + // account, so the network transaction builder will execute this note). + let note_storage = NoteStorage::new([].to_vec())?; + let recipient = NoteRecipient::new(serial_num, note_script, note_storage); + + // Set up note metadata - tag it with the counter contract ID so it gets consumed + let tag = NoteTag::with_account_target(counter_contract.id()); + + let attachment = NetworkAccountTarget::new(counter_contract.id(), NoteExecutionHint::Always) + .map_err(|e| NoteError::other(e.to_string()))? + .into(); + let metadata = PartialNoteMetadata::new(alice_account.id(), NoteType::Public).with_tag(tag); + let attachments = NoteAttachments::new(vec![attachment]).unwrap(); + + // Create the complete note + let increment_note = + Note::with_attachments(NoteAssets::default(), metadata, recipient, attachments); + + // Build and submit the transaction containing the note + let note_req = TransactionRequestBuilder::new() + .own_output_notes(vec![increment_note]) + .build()?; + + let note_tx_id = client + .submit_tutorial_transaction(alice_account.id(), note_req) + .await?; + + println!( + "View transaction on MidenScan: {}/tx/{:?}", + network.explorer_url(), + note_tx_id + ); + + client.sync_state().await?; + + println!("network increment note creation tx submitted, waiting for onchain commitment"); + + // Wait for the note transaction to be committed + wait_for_tx(&mut client, note_tx_id).await.unwrap(); + + // Waiting for network note to be picked up by the network transaction builder + sleep(Duration::from_secs(6)).await; + + let mut last_val = None; + for _ in 0..24 { + client.sync_state().await?; + + // Checking updated state + let new_account_state = client.get_account(counter_contract.id()).await.unwrap(); + + if let Some(account) = new_account_state.as_ref() { + let count: Word = account + .storage() + .get_item(&counter_slot_name) + .unwrap() + .into(); + let val = count[0].as_canonical_u64(); + if val == 1 { + println!("🔢 Final counter value: {}", val); + return Ok(()); + } + last_val = Some(val); + } + + // Give the network note builder time to process the note. + sleep(Duration::from_secs(6)).await; + } + + // The network note was submitted, but it is executed asynchronously by the + // network transaction builder. If the counter has not reached 1 within the + // polling window, the tutorial's final state is unconfirmed, so fail rather + // than claim success. + if let Some(val) = last_val { + Err(format!( + "Counter did not reach the expected value 1 within the timeout (last observed {}). \ + The network note was submitted but its execution is still pending on the network \ + transaction builder; re-run or check Midenscan.", + val + ) + .into()) + } else { + Err("Counter state was not available within the timeout; the network note execution is still pending." + .into()) + } +} +``` + +## Step 7: Running the Example + +For the standalone Cargo project, run `TUTORIAL_NETWORK=testnet cargo run --release` from `miden-network-transactions`. + +To run the checked-in example from the repository root: + +```bash +cd rust-client +TUTORIAL_NETWORK=testnet cargo run --release --bin network_notes_counter_contract +``` + +Successful output has this shape (abridged; IDs and block numbers vary): + +```text +Latest block: + +[STEP 1] Creating a new account for Alice +Alice's account ID: "" + +[STEP 2] Creating a network counter smart contract +contract id: "" + +[STEP 3] Deploy network counter smart contract +Network counter deployed; initial count is 0 + +[STEP 4] Creating a network note for network counter contract +View transaction on MidenScan: https://testnet.midenscan.com/tx/ +network increment note creation tx submitted, waiting for onchain commitment +🔢 Final counter value: 1 +``` + +## Summary + +Network transactions on Miden enable powerful use cases by allowing the operator to execute transactions on behalf of users. The key steps are: + +1. **Create user account**: Standard account creation for interaction +2. **Create network account**: Build a public account with `AuthNetworkAccount`, allowlisting the increment and funding note scripts +3. **Publish and fund it**: The initial native-asset consumption transaction registers the new account with count zero +4. **Interact with network notes**: Users create public notes that the operator executes + +The same MASM code works for both regular and network contracts — the difference is purely in the Rust configuration (the `AuthNetworkAccount` auth component and its allowlists). This makes network transactions a powerful tool for building applications like AMMs where multiple users need to interact with shared state efficiently. + +### Continue learning + +Next tutorial: [How To Create Notes with Custom Logic](custom_note_how_to.md) diff --git a/versioned_docs/version-0.16/builder/tutorials/recipes/rust/nft_mint_transfer.md b/versioned_docs/version-0.16/builder/tutorials/recipes/rust/nft_mint_transfer.md new file mode 100644 index 00000000..a560afc3 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tutorials/recipes/rust/nft_mint_transfer.md @@ -0,0 +1,524 @@ +--- +title: "Mint and Transfer an NFT" +sidebar_position: 3.5 +--- + +# Mint and Transfer an NFT + +_Using the Miden client in Rust to mint a non-fungible asset and transfer it between wallets_ + +For toolchain requirements and shared fee helpers, see the [Rust client setup](./index.md#running-the-v016-examples). + +## Overview + +In this tutorial, we will create an NFT collection, mint one NFT to Alice, and transfer it to Bob. Both wallets and the faucet use signature authentication. The example uses the standard `NonFungibleFaucet`, `MintNote`, and P2ID note implementations. + +As in [Mint, Consume, and Create Notes](./mint_consume_create_tutorial.md), receiving a note and holding its asset in a wallet are separate steps. Alice and Bob each consume their NFT note before the NFT appears in their vault. + +## What we'll cover + +- Creating authenticated wallets and an NFT faucet with native fee funding +- Computing an NFT's value from metadata and a salt +- Publishing a MINT request and consuming the minted NFT +- Transferring the NFT to Bob and verifying ownership + +## Prerequisites + +Follow the [v0.16 Rust setup](./index.md#running-the-v016-examples) in a clone of this repository. This tutorial uses its pinned dependencies and shared client helpers. Testnet is the verified network for this example. + +The following snippets walk through `rust-client/src/bin/nft_mint_transfer.rs` in execution order. They belong inside the same `main()` function and share its variables; the [complete example](#summary) includes the imports and function wrapper. + +## Step 1: Initialize the client and create wallets + +Initialize the client with the selected network, a local SQLite store, and a filesystem keystore. Synchronize it before reading the chain's fee configuration: + +```rust ignore +let network = TutorialNetwork::from_env()?; +let rpc = Arc::new(VerifyingRpcClient::new(GrpcClient::new( + &network.endpoint(), + 10_000, +))); +let keystore = Arc::new(FilesystemKeyStore::new(PathBuf::from("./keystore"))?); +let mut client = ClientBuilder::new() + .rpc(rpc) + .sqlite_store(PathBuf::from("./store.sqlite3")) + .authenticator(keystore.clone()) + .build() + .await?; +client.sync_state().await?; +let fees = FeeConfig::from_client(&client, network).await?; +``` + +Create Alice and Bob with `AuthSingleSig` and `BasicWallet`. Register each account and its signing key, then consume a funding note containing the native fee asset. Funding completes before the account submits any NFT transactions. + +```rust ignore +let mut wallets = Vec::new(); +for name in ["Alice", "Bob"] { + let mut seed = [0_u8; 32]; + client.rng().fill_bytes(&mut seed); + let key = AuthSecretKey::new_falcon512_poseidon2_with_rng(client.rng()); + let wallet = AccountBuilder::new(seed) + .account_type(AccountType::Public) + .with_component(AuthSingleSig::from_public_key(key.public_key())) + .with_component(BasicWallet) + .build()?; + client.add_account(&wallet, false).await?; + keystore.add_key(&key, wallet.id()).await?; + fund_account_for_fees(&mut client, wallet.id(), &fees).await?; + println!("{name}: {}", wallet.id().to_bech32(network.network_id())); + wallets.push(wallet.id()); +} +let (alice, bob) = (wallets[0], wallets[1]); +``` + +## Step 2: Create the NFT faucet + +The collection's name and symbol describe its faucet. Configure the standard NFT component and token policies, then compose the account with authentication, authority, and pause-management components. + +The faucet also needs `BasicWallet` to receive native fee funding and `CodeInspection` so the production MINT note can identify its faucet kind. Its `allow_all` mint policy accepts the example's value, while `AuthSingleSig` still requires the faucet's signature. No custom MASM is needed. + +```rust ignore +let mut seed = [0_u8; 32]; +client.rng().fill_bytes(&mut seed); +let key = AuthSecretKey::new_falcon512_poseidon2_with_rng(client.rng()); +let collection = NonFungibleFaucet::builder() + .name(TokenName::new("Recipe Collection")?) + .symbol(TokenSymbol::new("ART")?) + .build(); +let policies = TokenPolicyManager::builder() + .active_mint_policy(MintPolicy::allow_all()) + .active_burn_policy(BurnPolicy::allow_all()) + .build(); +// These are the user-faucet factory's standard components, plus BasicWallet +// for fee funding and CodeInspection for the production MINT note. +let faucet = AccountBuilder::new(seed) + .account_type(AccountType::Public) + .with_component(AuthSingleSig::from_public_key(key.public_key())) + .with_component(collection) + .with_component(Authority::AuthControlled) + .with_components(policies) + .with_component(Pausable::unpaused()) + .with_component(PausableManager) + .with_component(BasicWallet) + .with_component(CodeInspection) + .build()?; +client.add_account(&faucet, false).await?; +keystore.add_key(&key, faucet.id()).await?; +fund_account_for_fees(&mut client, faucet.id(), &fees).await?; +println!( + "NFT faucet: {}", + faucet.id().to_bech32(network.network_id()) +); +``` + +## Step 3: Compute the NFT value + +Use `NonFungibleFaucet::compute_asset_commitment(metadata, salt)` to hash the exact metadata bytes and merge that digest with a random salt. Combine the result with the faucet's ID to describe the NFT that will be minted: + +```rust ignore +let metadata = br#"{"name":"Recipe NFT #1","description":"A mint-and-transfer example"}"#; +let salt = client.rng().draw_word(); +let commitment = NonFungibleFaucet::compute_asset_commitment(metadata, salt); +let nft = NonFungibleAsset::from_parts(faucet.id(), commitment); +let asset = Asset::from(nft); +println!("NFT commitment: {commitment}"); +``` + +This creates a local asset description; minting happens in the next step. + +**This commitment is an off-chain convention.** The faucet does not validate metadata, a salt, the hash construction, or knowledge of a preimage. A recipient who wants to verify metadata must recompute the commitment and compare it with the asset value. The faucet enforces uniqueness of the token ID derived from the value's first two field elements, not the truth of the metadata. + +This example keeps the bytes and salt in memory. Applications that need later verification must retain and share them off-chain. + +## Step 4: Publish the MINT request and mint the NFT + +First describe the public P2ID note that the faucet will create for Alice. Its recipient, NFT, and tag become the storage of an **assetless MINT request**. These are two different notes: the request asks the faucet to mint; the P2ID note carries the minted asset. + +```rust ignore +let alice_note: Note = P2idNote::builder() + .sender(faucet.id()) + .target(alice) + .asset(nft) + .note_type(NoteType::Public) + .generate_serial_number(client.rng()) + .build()? + .into(); +let minted_note_id = alice_note.id(); +let mint_storage = MintNoteStorage::new_non_fungible_public( + alice_note.recipient().clone(), + nft, + alice_note.metadata().tag(), +)?; +let mint_request: Note = MintNote::builder() + .sender(alice) + .mint_storage(mint_storage) + .generate_serial_number(client.rng()) + .build()? + .into(); +let request_note_id = mint_request.id(); +``` + +Alice signs a transaction publishing the MINT request. Track the future P2ID note and wait for the request to commit before the faucet consumes it. The faucet signs that second transaction and creates Alice's NFT-bearing note: + +```rust ignore +assert_eq!(mint_request.assets().num_assets(), 0); +let request = TransactionRequestBuilder::new() + .own_output_notes([mint_request]) + .expected_future_notes(vec![((&alice_note).into(), alice_note.metadata().tag())]) + .build()?; +client.submit_tutorial_transaction(alice, request).await?; +let mint_requests = wait_for_notes_by_id(&mut client, &[request_note_id]).await?; +let request = TransactionRequestBuilder::new() + .expected_output_recipients([alice_note.recipient().clone()]) + .build_consume_notes(mint_requests)?; +// The faucet signs this transaction even though its mint policy is allow_all. +client + .submit_tutorial_transaction(faucet.id(), request) + .await?; +``` + +Wait for that specific P2ID note, then check that it contains exactly the expected NFT. Neither wallet holds the asset yet: + +```rust ignore +let minted_notes = wait_for_notes_by_id(&mut client, &[minted_note_id]).await?; +assert_eq!(minted_notes.len(), 1); +assert_eq!( + minted_notes[0].assets().iter().copied().collect::>(), + vec![asset] +); +assert_eq!(client.get_account_vault(alice).await?.get(nft.id()), None); +assert_eq!(client.get_account_vault(bob).await?.get(nft.id()), None); +println!("Minted: one NFT in Alice's P2ID note; neither wallet owns it yet."); +``` + +## Step 5: Consume the minted NFT into Alice's wallet + +Alice consumes the minted P2ID note in a separate transaction. After commitment, check the complete asset in her vault and confirm that the note is consumed: + +```rust ignore +let request = TransactionRequestBuilder::new().build_consume_notes(minted_notes)?; +client.submit_tutorial_transaction(alice, request).await?; +assert_eq!( + client.get_account_vault(alice).await?.get(nft.id()), + Some(asset) +); +assert_eq!(client.get_account_vault(bob).await?.get(nft.id()), None); +assert!(client + .get_input_note(minted_note_id) + .await? + .unwrap() + .is_consumed()); +println!("Consumed: Alice owns the NFT; Bob does not."); +``` + +## Step 6: Transfer the NFT to Bob + +Use `PaymentNoteDescription` and `build_pay_to_id` to send the same NFT to Bob in a public P2ID note. Once Alice's transaction commits, the NFT is in transit: it has left her vault, but Bob has not consumed it yet. + +```rust ignore +let payment = PaymentNoteDescription::new(vec![asset], alice, bob); +let request = TransactionRequestBuilder::new().build_pay_to_id( + payment, + NoteType::Public, + client.rng(), +)?; +let transfer_note_id = request.expected_output_own_notes()[0].id(); +client.submit_tutorial_transaction(alice, request).await?; +let transfer_notes = wait_for_notes_by_id(&mut client, &[transfer_note_id]).await?; +assert_eq!(transfer_notes.len(), 1); +assert_eq!( + transfer_notes[0] + .assets() + .iter() + .copied() + .collect::>(), + vec![asset] +); +assert_eq!(client.get_account_vault(alice).await?.get(nft.id()), None); +assert_eq!(client.get_account_vault(bob).await?.get(nft.id()), None); +println!("In transit: one NFT in Bob's P2ID note; neither wallet owns it."); +``` + +## Step 7: Consume the transfer and verify ownership + +Bob consumes the transfer note. Wait for his transaction to commit, then confirm that his vault holds the original asset, Alice and the faucet do not hold it, and the transfer note is consumed: + +```rust ignore +let request = TransactionRequestBuilder::new().build_consume_notes(transfer_notes)?; +client.submit_tutorial_transaction(bob, request).await?; +assert_eq!( + client.get_account_vault(bob).await?.get(nft.id()), + Some(asset) +); +assert_eq!(client.get_account_vault(alice).await?.get(nft.id()), None); +assert_eq!( + client.get_account_vault(faucet.id()).await?.get(nft.id()), + None +); +assert!(client + .get_input_note(transfer_note_id) + .await? + .unwrap() + .is_consumed()); +println!("Complete: Bob owns the NFT; Alice does not. Both NFT notes are consumed."); +``` + +The ownership checks cover each stage of the flow. The vault columns below refer only to this NFT; the accounts also hold native fee assets. + +| Stage | Alice's vault | Unconsumed NFT note | Bob's vault | +| -------------- | ------------- | ------------------- | ----------- | +| Minted | No NFT | Addressed to Alice | No NFT | +| Alice consumed | Holds NFT | None | No NFT | +| Alice sent | No NFT | Addressed to Bob | No NFT | +| Bob consumed | No NFT | None | Holds NFT | + +Selecting the exact request, mint, and transfer note IDs keeps fee notes out of these checks. The executing account pays each transaction's fee in the native asset: Alice pays to publish, consume, and transfer; the NFT faucet pays to mint; Bob pays to consume his note. + +## Summary + +You have created an authenticated NFT faucet, minted one NFT into a note for Alice, consumed it, transferred it to Bob, and verified the final owner. Here is the complete runnable example, including the client setup and imports: + +```rust no_run +use std::{error::Error, path::PathBuf, sync::Arc}; + +use miden_client::{ + account::{ + component::{ + Authority, BasicWallet, BurnPolicy, MintPolicy, NonFungibleFaucet, Pausable, + PausableManager, TokenName, TokenPolicyManager, + }, + standards::inspection::CodeInspection, + AccountBuilder, AccountType, + }, + asset::{Asset, NonFungibleAsset, TokenSymbol}, + auth::{AuthSecretKey, AuthSingleSig}, + builder::ClientBuilder, + crypto::FeltRng, + keystore::{FilesystemKeyStore, Keystore}, + note::{MintNote, MintNoteStorage, Note, NoteType, P2idNote}, + rpc::{GrpcClient, VerifyingRpcClient}, + transaction::{PaymentNoteDescription, TransactionRequestBuilder}, +}; +use miden_client_sqlite_store::ClientBuilderSqliteExt; +use rand::Rng; +use rust_client::{ + fund_account_for_fees, wait_for_notes_by_id, FeeConfig, TutorialClientExt, TutorialNetwork, +}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // The runner gives each invocation a fresh store and keystore. + let network = TutorialNetwork::from_env()?; + let rpc = Arc::new(VerifyingRpcClient::new(GrpcClient::new( + &network.endpoint(), + 10_000, + ))); + let keystore = Arc::new(FilesystemKeyStore::new(PathBuf::from("./keystore"))?); + let mut client = ClientBuilder::new() + .rpc(rpc) + .sqlite_store(PathBuf::from("./store.sqlite3")) + .authenticator(keystore.clone()) + .build() + .await?; + client.sync_state().await?; + let fees = FeeConfig::from_client(&client, network).await?; + + // 1. Create authenticated wallets for Alice and Bob. + let mut wallets = Vec::new(); + for name in ["Alice", "Bob"] { + let mut seed = [0_u8; 32]; + client.rng().fill_bytes(&mut seed); + let key = AuthSecretKey::new_falcon512_poseidon2_with_rng(client.rng()); + let wallet = AccountBuilder::new(seed) + .account_type(AccountType::Public) + .with_component(AuthSingleSig::from_public_key(key.public_key())) + .with_component(BasicWallet) + .build()?; + client.add_account(&wallet, false).await?; + keystore.add_key(&key, wallet.id()).await?; + fund_account_for_fees(&mut client, wallet.id(), &fees).await?; + println!("{name}: {}", wallet.id().to_bech32(network.network_id())); + wallets.push(wallet.id()); + } + let (alice, bob) = (wallets[0], wallets[1]); + + // 2. Compose a standard user NFT faucet. + let mut seed = [0_u8; 32]; + client.rng().fill_bytes(&mut seed); + let key = AuthSecretKey::new_falcon512_poseidon2_with_rng(client.rng()); + let collection = NonFungibleFaucet::builder() + .name(TokenName::new("Recipe Collection")?) + .symbol(TokenSymbol::new("ART")?) + .build(); + let policies = TokenPolicyManager::builder() + .active_mint_policy(MintPolicy::allow_all()) + .active_burn_policy(BurnPolicy::allow_all()) + .build(); + // These are the user-faucet factory's standard components, plus BasicWallet + // for fee funding and CodeInspection for the production MINT note. + let faucet = AccountBuilder::new(seed) + .account_type(AccountType::Public) + .with_component(AuthSingleSig::from_public_key(key.public_key())) + .with_component(collection) + .with_component(Authority::AuthControlled) + .with_components(policies) + .with_component(Pausable::unpaused()) + .with_component(PausableManager) + .with_component(BasicWallet) + .with_component(CodeInspection) + .build()?; + client.add_account(&faucet, false).await?; + keystore.add_key(&key, faucet.id()).await?; + fund_account_for_fees(&mut client, faucet.id(), &fees).await?; + println!( + "NFT faucet: {}", + faucet.id().to_bech32(network.network_id()) + ); + + // 3. Compute the NFT value off-chain. Keep the exact bytes and salt if a + // recipient will later verify this convention; the faucet checks neither. + let metadata = br#"{"name":"Recipe NFT #1","description":"A mint-and-transfer example"}"#; + let salt = client.rng().draw_word(); + let commitment = NonFungibleFaucet::compute_asset_commitment(metadata, salt); + let nft = NonFungibleAsset::from_parts(faucet.id(), commitment); + let asset = Asset::from(nft); + println!("NFT commitment: {commitment}"); + + // Describe the P2ID note that MINT will create for Alice. + let alice_note: Note = P2idNote::builder() + .sender(faucet.id()) + .target(alice) + .asset(nft) + .note_type(NoteType::Public) + .generate_serial_number(client.rng()) + .build()? + .into(); + let minted_note_id = alice_note.id(); + let mint_storage = MintNoteStorage::new_non_fungible_public( + alice_note.recipient().clone(), + nft, + alice_note.metadata().tag(), + )?; + let mint_request: Note = MintNote::builder() + .sender(alice) + .mint_storage(mint_storage) + .generate_serial_number(client.rng()) + .build()? + .into(); + let request_note_id = mint_request.id(); + + // 4. Alice publishes an assetless MINT request. The faucet must consume + // this committed request before Alice's NFT-bearing P2ID note exists. + assert_eq!(mint_request.assets().num_assets(), 0); + let request = TransactionRequestBuilder::new() + .own_output_notes([mint_request]) + .expected_future_notes(vec![((&alice_note).into(), alice_note.metadata().tag())]) + .build()?; + client.submit_tutorial_transaction(alice, request).await?; + let mint_requests = wait_for_notes_by_id(&mut client, &[request_note_id]).await?; + let request = TransactionRequestBuilder::new() + .expected_output_recipients([alice_note.recipient().clone()]) + .build_consume_notes(mint_requests)?; + // The faucet signs this transaction even though its mint policy is allow_all. + client + .submit_tutorial_transaction(faucet.id(), request) + .await?; + + let minted_notes = wait_for_notes_by_id(&mut client, &[minted_note_id]).await?; + assert_eq!(minted_notes.len(), 1); + assert_eq!( + minted_notes[0].assets().iter().copied().collect::>(), + vec![asset] + ); + assert_eq!(client.get_account_vault(alice).await?.get(nft.id()), None); + assert_eq!(client.get_account_vault(bob).await?.get(nft.id()), None); + println!("Minted: one NFT in Alice's P2ID note; neither wallet owns it yet."); + + // 5. Alice consumes the minted note into her vault. + let request = TransactionRequestBuilder::new().build_consume_notes(minted_notes)?; + client.submit_tutorial_transaction(alice, request).await?; + assert_eq!( + client.get_account_vault(alice).await?.get(nft.id()), + Some(asset) + ); + assert_eq!(client.get_account_vault(bob).await?.get(nft.id()), None); + assert!(client + .get_input_note(minted_note_id) + .await? + .unwrap() + .is_consumed()); + println!("Consumed: Alice owns the NFT; Bob does not."); + + // 6. Alice transfers the same NFT to Bob in a public P2ID note. + let payment = PaymentNoteDescription::new(vec![asset], alice, bob); + let request = TransactionRequestBuilder::new().build_pay_to_id( + payment, + NoteType::Public, + client.rng(), + )?; + let transfer_note_id = request.expected_output_own_notes()[0].id(); + client.submit_tutorial_transaction(alice, request).await?; + let transfer_notes = wait_for_notes_by_id(&mut client, &[transfer_note_id]).await?; + assert_eq!(transfer_notes.len(), 1); + assert_eq!( + transfer_notes[0] + .assets() + .iter() + .copied() + .collect::>(), + vec![asset] + ); + assert_eq!(client.get_account_vault(alice).await?.get(nft.id()), None); + assert_eq!(client.get_account_vault(bob).await?.get(nft.id()), None); + println!("In transit: one NFT in Bob's P2ID note; neither wallet owns it."); + + // 7. Bob consumes the transfer note; fresh vault reads verify ownership. + let request = TransactionRequestBuilder::new().build_consume_notes(transfer_notes)?; + client.submit_tutorial_transaction(bob, request).await?; + assert_eq!( + client.get_account_vault(bob).await?.get(nft.id()), + Some(asset) + ); + assert_eq!(client.get_account_vault(alice).await?.get(nft.id()), None); + assert_eq!( + client.get_account_vault(faucet.id()).await?.get(nft.id()), + None + ); + assert!(client + .get_input_note(transfer_note_id) + .await? + .unwrap() + .is_consumed()); + println!("Complete: Bob owns the NFT; Alice does not. Both NFT notes are consumed."); + Ok(()) +} +``` + +### Running the example + +From the root of your `tutorials` clone, run the checked-in example: + +```bash +TUTORIAL_NETWORK=testnet yarn tutorials --rust=nft_mint_transfer +``` + +The runner creates a fresh store and keystore for each attempt. The shared helpers fund native transaction fees, synchronize state, and wait for commitment. The NFT is a separate asset from the native tokens used for fees. + +### Expected output + +Alongside funding messages, account IDs, and committed transaction IDs, a successful run prints these ownership checkpoints: + +```text +Minted: one NFT in Alice's P2ID note; neither wallet owns it yet. +Consumed: Alice owns the NFT; Bob does not. +In transit: one NFT in Bob's P2ID note; neither wallet owns it. +Complete: Bob owns the NFT; Alice does not. Both NFT notes are consumed. +``` + +The account IDs, NFT commitment, and transaction IDs change on each run. The final line appears only after the ownership and consumed-note assertions pass. + +### Continue learning + +Next tutorial: [Deploying a Counter Contract](./counter_contract_tutorial.md). + +For the fungible-asset flow, see [Mint, Consume, and Create Notes](./mint_consume_create_tutorial.md). diff --git a/versioned_docs/version-0.16/builder/tutorials/recipes/rust/oracle_tutorial.md b/versioned_docs/version-0.16/builder/tutorials/recipes/rust/oracle_tutorial.md new file mode 100644 index 00000000..c51119f8 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tutorials/recipes/rust/oracle_tutorial.md @@ -0,0 +1,467 @@ +--- +title: "Consuming On-Chain Price Data from the Pragma Oracle" +sidebar_position: 13 +--- + +# Consuming On-Chain Price Data from the Pragma Oracle + +_Using the Pragma oracle to get on chain price data_ + +For toolchain requirements and shared fee helpers, see the [Rust client setup](./index.md#running-the-v016-examples). + +## Overview + +In this tutorial, we will build a simple “price reader” smart contract that will read Bitcoin price data from the on-chain Pragma oracle. + +We will use a script to call the `get_price` procedure in our reader account, which invokes Pragma through foreign procedure invocation (FPI). This example demonstrates the call plumbing and discards the returned price. An application would need to validate and use the result. + +## What we'll cover + +- Deploying a smart contract that can read oracle price data +- Using foreign procedure invocation to query published on-chain price data + +## Prerequisites + +:::warning Deployment required + +Pragma's [published deployment table](https://github.com/astraly-labs/pragma-miden#deployments) +lists Miden v0.15 testnet only. Running this example requires a compatible v0.16 +testnet deployment, its account ID, `get_median` procedure root, and pair identifiers. + +The reader assumes the named slots `pragma::oracle::next_publisher_index`, +`pragma::oracle::publishers`, and `pragma::publisher::entries`. Check these slots, +the publisher ID layout and index range, and the return values against that deployment. + +::: + +This tutorial assumes you have a basic understanding of Miden assembly, have completed the previous tutorials on using the Rust client, and have completed the tutorial on foreign procedure invocation. + +To quickly get up to speed with Miden assembly (MASM), please play around with running Miden programs in the [Miden playground](https://0xMiden.github.io/examples/). + +## Step 1: Initialize your repository + +From the parent directory of your `tutorials` clone, create a sibling Cargo project: + +```bash +cargo new miden-defi-app +cd miden-defi-app +rustup override set 1.98.1 +cp ../tutorials/rust-client/Cargo.lock Cargo.lock +``` + +Add the following dependencies to your `Cargo.toml` file: + +```toml +[dependencies] +# Clone tutorials next to this Cargo project (see Rust client setup). +rust-client = { path = "../tutorials/rust-client" } +miden-client = { version = "=0.16.0", features = ["testing", "tonic"] } +miden-client-sqlite-store = { version = "=0.16.0", package = "miden-client-sqlite-store" } +miden-protocol = { version = "=0.16.0" } +rand = { version = "0.10" } +serde = { version = "1", features = ["derive"] } +serde_json = { version = "1.0", features = ["raw_value"] } +tokio = { version = "1.48", features = ["rt-multi-thread", "net", "macros", "fs"] } + +[profile.dev] +opt-level = 2 +``` + +### Set up your `src/main.rs` file + +Copy and paste the following code into your `src/main.rs` file: + +```rust no_run +use miden_client::{ + Client, ClientError, Felt, Word, ZERO, + account::{ + AccountBuilder, AccountComponent, AccountId, AccountType, StorageMapKey, StorageSlot, + StorageSlotName, + component::{AccountComponentMetadata, BasicWallet}, + }, + assembly::CodeBuilder, + auth::NoAuth, + builder::ClientBuilder, + keystore::FilesystemKeyStore, + rpc::{GrpcClient, VerifyingRpcClient, domain::account::AccountStorageRequirements}, + transaction::{ForeignAccount, TransactionRequestBuilder}, +}; +use miden_client_sqlite_store::ClientBuilderSqliteExt; +use rand::Rng; +use rust_client::TutorialClientExt; +use rust_client::{FeeConfig, TutorialNetwork, fund_account_for_fees}; +use std::sync::Arc; + +/// Import the oracle + its publishers and return the ForeignAccount list +/// Due to Pragma's decentralized oracle architecture, we need to get the +/// list of all data publisher accounts to read price from via a nested FPI call +pub async fn get_oracle_foreign_accounts( + client: &mut Client, + oracle_account_id: AccountId, + faucet_pair: Word, +) -> Result, ClientError> { + client.import_account_by_id(oracle_account_id).await?; + client.sync_state().await?; + + let oracle_record = client + .get_account(oracle_account_id) + .await + .expect("RPC failed") + .expect("oracle account not found"); + + let storage = oracle_record.storage(); + + // The oracle tracks the next free publisher index in a value slot. + // Publisher slots start at index 2, so the publisher count is `next_index - 2`. + let next_index_slot = + StorageSlotName::new("pragma::oracle::next_publisher_index").expect("valid slot name"); + let next_publisher_index = storage + .get_item(&next_index_slot) + .expect("oracle is missing the next_publisher_index slot")[0] + .as_canonical_u64(); + + // Publisher account IDs are stored in the `publishers` map, keyed by index. + let publishers_slot = + StorageSlotName::new("pragma::oracle::publishers").expect("valid slot name"); + let publisher_ids: Vec = (2..next_publisher_index) + .map(|index| { + let key = StorageMapKey::new([Felt::new_unchecked(index), ZERO, ZERO, ZERO].into()); + let publisher_word = storage + .get_map_item(&publishers_slot, key) + .expect("publisher entry missing from oracle storage"); + // The publisher id word is laid out as [prefix, suffix, 0, 0]. + AccountId::new_unchecked([publisher_word[0], publisher_word[1]]) + }) + .collect(); + + // Each publisher exposes its price entries in the `entries` map, keyed by + // the faucet ID word of the trading pair. + let entries_slot = StorageSlotName::new("pragma::publisher::entries").expect("valid slot name"); + let mut foreign_accounts = Vec::with_capacity(publisher_ids.len() + 1); + + for publisher_id in publisher_ids { + client.import_account_by_id(publisher_id).await?; + + let storage_requirements = AccountStorageRequirements::new([( + entries_slot.clone(), + &[StorageMapKey::new(faucet_pair)], + )]); + + foreign_accounts.push(ForeignAccount::public(publisher_id, storage_requirements)?); + } + + // The oracle account itself is also a foreign account. `get_median` reads + // the publisher registry from the oracle's `publishers` map, so the proofs + // for those map keys must be requested as well. + let publisher_index_keys: Vec = (2..next_publisher_index) + .map(|index| StorageMapKey::new([Felt::new_unchecked(index), ZERO, ZERO, ZERO].into())) + .collect(); + foreign_accounts.push(ForeignAccount::public( + oracle_account_id, + AccountStorageRequirements::new([(publishers_slot.clone(), publisher_index_keys.iter())]), + )?); + + client.sync_state().await?; + + Ok(foreign_accounts) +} + +#[tokio::main] +async fn main() -> Result<(), ClientError> { + // ------------------------------------------------------------------------- + // Initialize Client + // ------------------------------------------------------------------------- + let network = TutorialNetwork::from_env()?; + let endpoint = network.endpoint(); + let timeout_ms = 10_000; + let rpc_client = Arc::new(VerifyingRpcClient::new(GrpcClient::new( + &endpoint, timeout_ms, + ))); + + let keystore_path = std::path::PathBuf::from("./keystore"); + let keystore = Arc::new(FilesystemKeyStore::new(keystore_path).unwrap()); + + let store_path = std::path::PathBuf::from("./store.sqlite3"); + + let mut client = ClientBuilder::new() + .rpc(rpc_client) + .sqlite_store(store_path) + .authenticator(keystore.clone()) + .build() + .await?; + + println!("Latest block: {}", client.sync_state().await?.block_num); + let fee_config = FeeConfig::from_client(&client, network).await?; + + // ------------------------------------------------------------------------- + // Get all foreign accounts for oracle data + // ------------------------------------------------------------------------- + // Pass a compatible oracle account ID and its `get_median` procedure root as CLI + // arguments (or through the matching environment variables). This tutorial remains skipped + // by the runner until Pragma publishes a deployment for the current protocol release. + let oracle_bech32 = std::env::args() + .nth(1) + .or_else(|| std::env::var("MIDEN_ORACLE_ACCOUNT_ID").ok()) + .ok_or_else(|| ClientError::Observer(Box::new(std::io::Error::other( + "Oracle deployment is required: set MIDEN_ORACLE_ACCOUNT_ID and MIDEN_ORACLE_GET_MEDIAN_ROOT for the selected network. Use a compatible v0.16 deployment on the selected network.", + ))))?; + let get_median_proc_root = std::env::args() + .nth(2) + .or_else(|| std::env::var("MIDEN_ORACLE_GET_MEDIAN_ROOT").ok()) + .ok_or_else(|| { + ClientError::Observer(Box::new(std::io::Error::other( + "Set MIDEN_ORACLE_GET_MEDIAN_ROOT to the deployed oracle's get_median procedure root", + ))) + })?; + let (account_network, oracle_account_id) = AccountId::from_bech32(&oracle_bech32).unwrap(); + assert_eq!( + account_network, + network.network_id(), + "oracle account must match the selected tutorial network" + ); + + // BTC/USD was identified by the faucet ID pair `1:0` in the previous deployment. Override + // either value with the optional third and fourth CLI arguments for the selected deployment. + // The faucet ID word is laid out as [0, 0, suffix, prefix]. + let pair_prefix: u64 = std::env::args() + .nth(3) + .map_or(1, |value| value.parse().expect("pair prefix must be a u64")); + let pair_suffix: u64 = std::env::args() + .nth(4) + .map_or(0, |value| value.parse().expect("pair suffix must be a u64")); + let btc_usd_pair: Word = [ + ZERO, + ZERO, + Felt::new_unchecked(pair_suffix), + Felt::new_unchecked(pair_prefix), + ] + .into(); + let foreign_accounts: Vec = + get_oracle_foreign_accounts(&mut client, oracle_account_id, btc_usd_pair).await?; + + println!( + "Oracle accountId prefix: {:?} suffix: {:?}", + oracle_account_id.prefix(), + oracle_account_id.suffix() + ); + + // ------------------------------------------------------------------------- + // Create Oracle Reader contract + // ------------------------------------------------------------------------- + let contract_code = std::fs::read_to_string("../tutorials/masm/accounts/oracle_reader.masm") + .unwrap() + .replace("{get_median_proc_root}", &get_median_proc_root) + .replace( + "{oracle_id_prefix}", + &oracle_account_id.prefix().to_string(), + ) + .replace( + "{oracle_id_suffix}", + &oracle_account_id.suffix().to_string(), + ) + .replace("{pair_prefix}", &pair_prefix.to_string()) + .replace("{pair_suffix}", &pair_suffix.to_string()); + + let contract_slot_name = + StorageSlotName::new("miden::tutorials::oracle_reader").expect("valid slot name"); + let contract_component_code = CodeBuilder::new() + .compile_component_code("external_contract::oracle_reader", &contract_code) + .unwrap(); + let contract_component = AccountComponent::new( + contract_component_code, + vec![StorageSlot::with_value( + contract_slot_name.clone(), + Word::default(), + )], + AccountComponentMetadata::new("external_contract::oracle_reader"), + ) + .unwrap(); + + let mut seed = [0_u8; 32]; + client.rng().fill_bytes(&mut seed); + + let oracle_reader_contract = AccountBuilder::new(seed) + .account_type(AccountType::Public) + .with_component(contract_component.clone()) + .with_component(BasicWallet) + .with_component(NoAuth) + .build() + .unwrap(); + + client + .add_account(&oracle_reader_contract, false) + .await + .unwrap(); + fund_account_for_fees(&mut client, oracle_reader_contract.id(), &fee_config).await?; + + // ------------------------------------------------------------------------- + // Build the script that calls our `get_price` procedure + // ------------------------------------------------------------------------- + let script_code = + std::fs::read_to_string("../tutorials/masm/scripts/oracle_reader_script.masm").unwrap(); + + let tx_script = client + .code_builder() + .with_linked_module("external_contract::oracle_reader", &contract_code) + .unwrap() + .compile_tx_script(&script_code) + .unwrap(); + + let tx_increment_request = TransactionRequestBuilder::new() + .foreign_accounts(foreign_accounts) + .custom_script(tx_script) + .build() + .unwrap(); + + let tx_id = client + .submit_tutorial_transaction(oracle_reader_contract.id(), tx_increment_request) + .await + .unwrap(); + + println!( + "View transaction on MidenScan: {}/tx/{:?}", + network.explorer_url(), + tx_id + ); + + client.sync_state().await.unwrap(); + + Ok(()) +} +``` + +The following section explains the two MASM templates loaded by the Rust example. + +In the code above, a compatible testnet oracle account ID and `get_median` procedure root are required inputs. The BTC/USD price feed used prefix `1` and suffix `0` in Pragma's earlier deployment; the optional third and fourth arguments let you supply the pair identifiers published for a new deployment. The `get_oracle_foreign_accounts` function returns every `ForeignAccount` needed to execute the transaction. Since Pragma's oracle aggregates data from multiple publishers, the function reads the on-chain publisher registry and requests the storage proofs needed by the nested FPI calls. + +## Step 2: Build the price reader smart contract and script + +The reader and transaction script are in the repository’s `masm/` directory. + +### Oracle price reader smart contract + +Below is our oracle price reader contract. It has a single exported procedure: `get_price` + +The `miden::protocol::tx` module exports `execute_foreign_procedure`, which the reader uses to invoke the oracle. + +#### Here's a breakdown of what the `get_price` procedure does: + +1. Pushes the 16 foreign procedure inputs that `tx::execute_foreign_procedure` requires. The first four contain the requested pair prefix and suffix, an `amount` of `0`, and a trailing `0`; the remaining twelve are zero padding. +2. Pushes the supplied `get_median` procedure root. +3. Pushes the supplied testnet oracle account ID prefix and suffix. +4. Calls `tx::execute_foreign_procedure`, which invokes `get_median`. The template expects `[is_tracked, median_price, amount]` at the top of the returned stack; confirm this interface against the compatible deployment. +5. Drops the sixteen foreign output elements, including the price, to restore the caller's stack. + +The reader is defined in `masm/accounts/oracle_reader.masm`: + +```masm +# the Rust runner replaces these placeholders with values from a compatible +# pragma deployment before compiling this component. + +use miden::protocol::tx + +# PUBLIC INTERFACE +# ================================================================================================= + +#! Queries the configured Pragma oracle's median price through a foreign procedure. +#! +#! Inputs: [pad(16)] +#! Outputs: [pad(16)] +#! +#! Panics if: +#! - the configured oracle procedure or its required foreign state is unavailable. +#! +#! Invocation: call +@account_procedure +pub proc get_price() + # `execute_foreign_procedure` requires exactly 16 foreign procedure inputs. + # `get_median` only reads the first four, so the rest are zero padding. + padw padw padw + # => [pad(28)] + + # requested pair: faucet ID prefix/suffix, amount `0`. + push.0.0.{pair_suffix}.{pair_prefix} + # => [pair_prefix, pair_suffix, amount, 0, pad(28)] + + # this is the procedure root of the `get_median` procedure. + push.{get_median_proc_root} + # => [GET_MEDIAN_HASH, foreign_procedure_inputs(16), pad(16)] + + # the Pragma oracle account id: prefix then suffix, leaving suffix on top. + push.{oracle_id_prefix}.{oracle_id_suffix} + # => [oracle_id_suffix, oracle_id_prefix, GET_MEDIAN_HASH, foreign_procedure_inputs(16), pad(16)] + + exec.tx::execute_foreign_procedure + # => [is_tracked, median_price, amount, pad(29)] + + dropw dropw dropw dropw + # => [pad(16)] +end +``` + +Stack comments below instruction groups show the expected stack state after execution. The braces in this template are replaced by Rust before assembly. + +### Create the script which calls the `get_price` procedure + +This is a Miden assembly script that will call the `get_price` procedure during the transaction. + +The transaction script is defined in `masm/scripts/oracle_reader_script.masm`: + +```masm +use external_contract::oracle_reader + +#! Queries the configured oracle through the reader account. +#! +#! Inputs: [ARGS, pad(12)] +#! Outputs: [pad(16)] +#! +#! Where: +#! - ARGS contains unused transaction script arguments. +#! +#! Invocation: dyncall +@transaction_script +pub proc main(args: word) + dropw + # => [pad(16)] + + call.oracle_reader::get_price + # => [pad(16)] +end +``` + +## Step 3: Run the program + +Compile-check the standalone program with `cargo check`. To execute it once a compatible deployment is available, set `MIDEN_ORACLE_ACCOUNT_ID` and `MIDEN_ORACLE_GET_MEDIAN_ROOT` in your shell, then run: + +```bash +TUTORIAL_NETWORK=testnet cargo run --release -- \ + "$MIDEN_ORACLE_ACCOUNT_ID" "$MIDEN_ORACLE_GET_MEDIAN_ROOT" +``` + +The command defaults to pair prefix `1` and suffix `0`. Append the deployment's pair prefix and suffix as the third and fourth arguments when those values differ. Do not assume the old pair identifies BTC/USD on a new deployment. + +With a compatible deployment, the output includes: + +```text +Latest block: +Oracle accountId prefix: suffix: +View transaction on MidenScan: https://testnet.midenscan.com/tx/ +``` + +The template expects `[is_tracked, median_price, amount]` on the stack, then drops those values. Before using the price in an application, check the feed's tracking status, freshness rules, and fixed-point precision, then store the value or use it within the same procedure. + +### Running the tutorial + +Once the deployment prerequisites are met, return to the root of the [tutorials repository](https://github.com/0xMiden/tutorials/) and run: + +```bash +cd rust-client +TUTORIAL_NETWORK=testnet cargo run --release --bin oracle_data_query -- \ + "$MIDEN_ORACLE_ACCOUNT_ID" "$MIDEN_ORACLE_GET_MEDIAN_ROOT" +``` + +If both variables are exported in your shell, you can omit `--` and the explicit arguments. The account must use the `mtst1...` testnet prefix. + +### Continue learning + +Next tutorial: [How to Use Unauthenticated Notes](./unauthenticated_note_how_to.md) diff --git a/versioned_docs/version-0.16/builder/tutorials/recipes/rust/public_account_interaction_tutorial.md b/versioned_docs/version-0.16/builder/tutorials/recipes/rust/public_account_interaction_tutorial.md new file mode 100644 index 00000000..fe7695d1 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tutorials/recipes/rust/public_account_interaction_tutorial.md @@ -0,0 +1,489 @@ +--- +title: "Interacting with Public Smart Contracts" +sidebar_position: 5 +--- + +# Interacting with Public Smart Contracts + +_Using the Miden client in Rust to interact with public smart contracts on Miden_ + +For toolchain requirements and shared fee helpers, see the [Rust client setup](./index.md#running-the-v016-examples). + +## Overview + +In the previous tutorial, we built a simple counter contract and deployed it to the Miden testnet. However, we only covered how the contract’s deployer could interact with it. Now, let’s explore how anyone can interact with a public smart contract on Miden. + +We'll import the counter contract's public state from the chain and execute a local transaction against it. Its `NoAuth` authentication component permits this without a signature; a public account with signature authentication would still require the appropriate authorization. For contracts that should execute autonomously on behalf of users, continue with the network transactions tutorial after this one. + +Just like in the previous tutorial, we will use a script to invoke the increment function within the counter contract to update the count. However, this tutorial demonstrates how to call a procedure in a smart contract that was deployed by a different user on Miden. + +## What we'll cover + +- Reading state from a public smart contract +- Interacting with public smart contracts on Miden + +## Prerequisites + +This tutorial assumes you have a basic understanding of Miden assembly and a counter deployed with the code from the previous tutorial. Keep the `mtst1...` account ID printed by that deployment; the standalone program requires it. + +The counter deployment example also funds the contract's native fee balance. +This example spends from that existing balance when incrementing the counter; +ensure the imported contract has enough funds. The runner supplies a freshly +deployed, funded counter automatically. + +## Step 1: Initialize your repository + +From the parent directory of your `tutorials` clone, create a sibling Cargo project: + +```bash +cargo new miden-public-account-interaction +cd miden-public-account-interaction +rustup override set 1.98.1 +cp ../tutorials/rust-client/Cargo.lock Cargo.lock +``` + +Add the following dependencies to your `Cargo.toml` file: + +```toml +[dependencies] +# Clone tutorials next to this Cargo project (see Rust client setup). +rust-client = { path = "../tutorials/rust-client" } +miden-client = { version = "=0.16.0", features = ["testing", "tonic"] } +miden-client-sqlite-store = { version = "=0.16.0", package = "miden-client-sqlite-store" } +miden-protocol = { version = "=0.16.0" } +rand = { version = "0.10" } +tokio = { version = "1.48", features = ["rt-multi-thread", "net", "macros", "fs"] } + +[profile.dev] +opt-level = 2 +``` + +## Step 2: Prepare the counter module and script + +The account already exists on-chain. We use the repository’s `masm/accounts/counter.masm` module to link the increment transaction script: + +```masm +use miden::protocol::active_account +use miden::protocol::native_account +use miden::core::sys + +# CONSTANTS +# ================================================================================================= + +const COUNTER_SLOT = word("miden::tutorials::counter") + +# PUBLIC INTERFACE +# ================================================================================================= + +#! Returns the current count. +#! +#! Inputs: [pad(16)] +#! Outputs: [count, pad(15)] +#! +#! Invocation: call +@account_procedure +pub proc get_count() -> felt + push.COUNTER_SLOT[0..2] exec.active_account::get_item + # => [[count, 0, 0, 0], pad(16)] + + exec.sys::truncate_stack + # => [count, pad(15)] +end + +#! Increments the current count by one. +#! +#! Inputs: [pad(16)] +#! Outputs: [pad(16)] +#! +#! Invocation: call +@account_procedure +pub proc increment_count() + push.COUNTER_SLOT[0..2] exec.active_account::get_item + # => [[count, 0, 0, 0], pad(16)] + + add.1 + # => [[count + 1, 0, 0, 0], pad(16)] + + push.COUNTER_SLOT[0..2] exec.native_account::set_item + # => [OLD_VALUE, pad(16)] + + dropw + # => [pad(16)] + + exec.sys::truncate_stack + # => [pad(16)] +end +``` + +The transaction script is defined in `masm/scripts/counter_script.masm`: + +```masm +use external_contract::counter_contract + +#! Increments the counter. +#! +#! Inputs: [ARGS, pad(12)] +#! Outputs: [pad(16)] +#! +#! Where: +#! - ARGS contains unused transaction script arguments. +#! +#! Invocation: dyncall +@transaction_script +pub proc main(args: word) + dropw + # => [pad(16)] + + call.counter_contract::increment_count + # => [pad(16)] +end +``` + +**Note**: _We explained in the previous counter contract tutorial what exactly happens at each step in the `increment_count` procedure._ + +## Step 3: Set up your `src/main.rs` file + +Copy and paste the following code into your `src/main.rs` file: + +```rust no_run +use rust_client::TutorialClientExt; +use std::{path::PathBuf, sync::Arc}; + +use miden_client::{ + ClientError, + account::{AccountId, StorageSlotName}, + builder::ClientBuilder, + keystore::FilesystemKeyStore, + rpc::{GrpcClient, VerifyingRpcClient}, + transaction::TransactionRequestBuilder, +}; +use miden_client_sqlite_store::ClientBuilderSqliteExt; +use rust_client::TutorialNetwork; + +#[tokio::main] +async fn main() -> Result<(), ClientError> { + // Initialize client + let network = TutorialNetwork::from_env()?; + let endpoint = network.endpoint(); + let timeout_ms = 10_000; + let rpc_client = Arc::new(VerifyingRpcClient::new(GrpcClient::new( + &endpoint, timeout_ms, + ))); + + // Initialize keystore + let keystore_path = PathBuf::from("./keystore"); + let keystore = Arc::new(FilesystemKeyStore::new(keystore_path).unwrap()); + + let store_path = PathBuf::from("./store.sqlite3"); + + let mut client = ClientBuilder::new() + .rpc(rpc_client) + .sqlite_store(store_path) + .authenticator(keystore.clone()) + .build() + .await?; + + let sync_summary = client.sync_state().await.unwrap(); + println!("Latest block: {}", sync_summary.block_num); + + Ok(()) +} +``` + +## Step 4: Reading public state from a smart contract + +This tutorial uses `Client::import_account_by_id` to import a public account from testnet and read its storage. First run the [counter contract tutorial](./counter_contract_tutorial.md), then copy the deployed counter's `mtst1...` account ID. Pass that ID to this program instead of hard-coding an address, because testnet is reset periodically. + +Insert the following code inside `main`, immediately before its final `Ok(())`: + +```rust ignore +// ------------------------------------------------------------------------- +// STEP 1: Read the Public State of the Counter Contract +// ------------------------------------------------------------------------- +println!("\n[STEP 1] Reading data from public state"); + +// Pass the account ID printed by `counter_contract_deploy` as the first argument, or via +// `MIDEN_COUNTER_ACCOUNT_ID`. +let counter_contract_bech32 = std::env::args() + .nth(1) + .or_else(|| std::env::var("MIDEN_COUNTER_ACCOUNT_ID").ok()) + .expect("pass the counter account ID from counter_contract_deploy"); +let (account_network, counter_contract_id) = + AccountId::from_bech32(&counter_contract_bech32).expect("invalid counter account ID"); +assert_eq!( + account_network, + network.network_id(), + "counter account must match the selected tutorial network" +); + +client + .import_account_by_id(counter_contract_id) + .await + .unwrap(); + +let counter_contract = client + .get_account(counter_contract_id) + .await + .unwrap() + .expect("counter contract not found"); +println!( + "Account details: {:?}", + counter_contract.storage().slots().first().unwrap() +); +let counter_slot_name = + StorageSlotName::new("miden::tutorials::counter").expect("valid slot name"); +let count_before = counter_contract + .storage() + .get_item(&counter_slot_name) + .unwrap()[0]; +``` + +Set `MIDEN_COUNTER_ACCOUNT_ID` to the deployed `mtst1...` address in your shell, or replace the quoted variable below with that address. Run the following command to execute `src/main.rs`: + +```bash +TUTORIAL_NETWORK=testnet cargo run --release -- "$MIDEN_COUNTER_ACCOUNT_ID" +``` + +The program prints the imported storage slot. For a freshly deployed counter, the abridged output is: + +```text +Account details: StorageSlot { ... content: Value(Word([1, 0, 0, 0])) } +``` + +## Step 5: Increment the imported counter + +Insert the following code after the import step, inside `main` and before `Ok(())`: + +```rust ignore +// ------------------------------------------------------------------------- +// STEP 2: Call the Counter Contract with a script +// ------------------------------------------------------------------------- +println!("\n[STEP 2] Call the increment_count procedure in the counter contract"); + +// Read the MASM source from the tutorials repository. +let script_code = + std::fs::read_to_string("../tutorials/masm/scripts/counter_script.masm").unwrap(); +let counter_code = std::fs::read_to_string("../tutorials/masm/accounts/counter.masm").unwrap(); + +// Compile the script with the counter contract code linked as a module +// on the same `CodeBuilder` chain. +let tx_script = client + .code_builder() + .with_linked_module("external_contract::counter_contract", &counter_code) + .unwrap() + .compile_tx_script(&script_code) + .unwrap(); + +// Build a transaction request with the custom script +let tx_increment_request = TransactionRequestBuilder::new() + .custom_script(tx_script) + .build() + .unwrap(); + +// Execute and submit the transaction +let tx_id = client + .submit_tutorial_transaction(counter_contract_id, tx_increment_request) + .await + .unwrap(); + +println!( + "View transaction on MidenScan: {}/tx/{:?}", + network.explorer_url(), + tx_id +); + +client.sync_state().await.unwrap(); + +// Retrieve updated contract data to see the incremented counter +let account = client + .get_account(counter_contract_id) + .await + .unwrap() + .expect("counter contract not found"); +println!( + "counter contract storage: {:?}", + account.storage().get_item(&counter_slot_name) +); +assert_eq!( + account.storage().get_item(&counter_slot_name).unwrap()[0], + count_before + miden_client::ONE, + "the imported counter must increment exactly once", +); +``` + +## Summary + +The final `src/main.rs` file should look like this: + +```rust no_run +use rust_client::TutorialClientExt; +use std::{path::PathBuf, sync::Arc}; + +use miden_client::{ + ClientError, + account::{AccountId, StorageSlotName}, + builder::ClientBuilder, + keystore::FilesystemKeyStore, + rpc::{GrpcClient, VerifyingRpcClient}, + transaction::TransactionRequestBuilder, +}; +use miden_client_sqlite_store::ClientBuilderSqliteExt; +use rust_client::TutorialNetwork; + +#[tokio::main] +async fn main() -> Result<(), ClientError> { + // Initialize client + let network = TutorialNetwork::from_env()?; + let endpoint = network.endpoint(); + let timeout_ms = 10_000; + let rpc_client = Arc::new(VerifyingRpcClient::new(GrpcClient::new( + &endpoint, timeout_ms, + ))); + + // Initialize keystore + let keystore_path = PathBuf::from("./keystore"); + let keystore = Arc::new(FilesystemKeyStore::new(keystore_path).unwrap()); + + let store_path = PathBuf::from("./store.sqlite3"); + + let mut client = ClientBuilder::new() + .rpc(rpc_client) + .sqlite_store(store_path) + .authenticator(keystore.clone()) + .build() + .await?; + + let sync_summary = client.sync_state().await.unwrap(); + println!("Latest block: {}", sync_summary.block_num); + + // ------------------------------------------------------------------------- + // STEP 1: Read the Public State of the Counter Contract + // ------------------------------------------------------------------------- + println!("\n[STEP 1] Reading data from public state"); + + // Pass the account ID printed by `counter_contract_deploy` as the first argument, or via + // `MIDEN_COUNTER_ACCOUNT_ID`. + let counter_contract_bech32 = std::env::args() + .nth(1) + .or_else(|| std::env::var("MIDEN_COUNTER_ACCOUNT_ID").ok()) + .expect("pass the counter account ID from counter_contract_deploy"); + let (account_network, counter_contract_id) = + AccountId::from_bech32(&counter_contract_bech32).expect("invalid counter account ID"); + assert_eq!( + account_network, + network.network_id(), + "counter account must match the selected tutorial network" + ); + + client + .import_account_by_id(counter_contract_id) + .await + .unwrap(); + + let counter_contract = client + .get_account(counter_contract_id) + .await + .unwrap() + .expect("counter contract not found"); + println!( + "Account details: {:?}", + counter_contract.storage().slots().first().unwrap() + ); + let counter_slot_name = + StorageSlotName::new("miden::tutorials::counter").expect("valid slot name"); + let count_before = counter_contract + .storage() + .get_item(&counter_slot_name) + .unwrap()[0]; + + // ------------------------------------------------------------------------- + // STEP 2: Call the Counter Contract with a script + // ------------------------------------------------------------------------- + println!("\n[STEP 2] Call the increment_count procedure in the counter contract"); + + // Read the MASM source from the tutorials repository. + let script_code = + std::fs::read_to_string("../tutorials/masm/scripts/counter_script.masm").unwrap(); + let counter_code = std::fs::read_to_string("../tutorials/masm/accounts/counter.masm").unwrap(); + + // Compile the script with the counter contract code linked as a module + // on the same `CodeBuilder` chain. + let tx_script = client + .code_builder() + .with_linked_module("external_contract::counter_contract", &counter_code) + .unwrap() + .compile_tx_script(&script_code) + .unwrap(); + + // Build a transaction request with the custom script + let tx_increment_request = TransactionRequestBuilder::new() + .custom_script(tx_script) + .build() + .unwrap(); + + // Execute and submit the transaction + let tx_id = client + .submit_tutorial_transaction(counter_contract_id, tx_increment_request) + .await + .unwrap(); + + println!( + "View transaction on MidenScan: {}/tx/{:?}", + network.explorer_url(), + tx_id + ); + + client.sync_state().await.unwrap(); + + // Retrieve updated contract data to see the incremented counter + let account = client + .get_account(counter_contract_id) + .await + .unwrap() + .expect("counter contract not found"); + println!( + "counter contract storage: {:?}", + account.storage().get_item(&counter_slot_name) + ); + assert_eq!( + account.storage().get_item(&counter_slot_name).unwrap()[0], + count_before + miden_client::ONE, + "the imported counter must increment exactly once", + ); + Ok(()) +} +``` + +Run the following command to execute src/main.rs: + +```bash +TUTORIAL_NETWORK=testnet cargo run --release -- "$MIDEN_COUNTER_ACCOUNT_ID" +``` + +The output of our program will look something like this depending on the current count value in the smart contract: + +```text +Latest block: + +[STEP 1] Reading data from public state +Account details: StorageSlot { ... content: Value(Word([1, 0, 0, 0])) } + +[STEP 2] Call the increment_count procedure in the counter contract +View transaction on MidenScan: https://testnet.midenscan.com/tx/ +counter contract storage: Ok(Word([2, 0, 0, 0])) +``` + +### Running the example + +To run the checked-in example, return to the root of the [tutorials repository](https://github.com/0xMiden/tutorials/) and run: + +```bash +cd rust-client +TUTORIAL_NETWORK=testnet cargo run --release --bin counter_contract_increment -- "$MIDEN_COUNTER_ACCOUNT_ID" +``` + +If `MIDEN_COUNTER_ACCOUNT_ID` is exported in your shell, you can omit `--` and the final argument. + +### Continue learning + +Next tutorial: [Network Transactions on Miden](network_transactions_tutorial.md) diff --git a/versioned_docs/version-0.16/builder/tutorials/recipes/rust/unauthenticated_note_how_to.md b/versioned_docs/version-0.16/builder/tutorials/recipes/rust/unauthenticated_note_how_to.md new file mode 100644 index 00000000..53e43f7d --- /dev/null +++ b/versioned_docs/version-0.16/builder/tutorials/recipes/rust/unauthenticated_note_how_to.md @@ -0,0 +1,489 @@ +--- +title: "How to Use Unauthenticated Notes" +sidebar_position: 9 +--- + +# How to Use Unauthenticated Notes + +_Using unauthenticated notes for optimistic note consumption_ + +For toolchain requirements and shared fee helpers, see the [Rust client setup](./index.md#running-the-v016-examples). + +## Overview + +In this guide, we supply a complete note to a consuming transaction before waiting for the note's inclusion proof. Such an input is unauthenticated: the node checks the dependency on its creation transaction. This lets a note be created and consumed within the same block, although confirmation still depends on block production. + +We construct the transfer chain with `TransactionRequestBuilder::explicit_input_notes`, wrapping each complete `Note` in `InputNote::unauthenticated`. This pins the input mode even if a sync has already fetched its inclusion proof. `build_consume_notes` selects the mode from the store and can authenticate an input when a proof is available. We also serialize and deserialize each note to demonstrate how its details could be sent between clients. The example uses one client for all accounts and waits for each transfer and consumption to confirm before beginning the next hop. + +For example, our demo creates a chain of unauthenticated note transactions: + +```markdown +Alice ➡ Bob ➡ Charlie ➡ Dave ➡ Eve +``` + +## What we'll cover + +- **Introduction to Unauthenticated Notes:** Understand what unauthenticated notes are and how they differ from standard notes. +- **Serialization Example:** See how to serialize and deserialize a note to demonstrate how notes can be propagated to client instances faster than the blocktime. +- **Confirmation and balances:** Check each transaction and verify the final balances after four transfers between five accounts. + +## Step-by-step process + +1. **Client Initialization:** + - Set up an RPC client to connect with the Miden testnet. + - Initialize a random coin generator and a store for persisting account data. + +2. **Deploying a Fungible Faucet:** + - Use a random seed to deploy a fungible faucet. + - Configure the faucet parameters (symbol, decimals, and max supply) and add it to the client. + +3. **Creating Wallet Accounts:** + - Build multiple wallet accounts using a secure key generation process. + - Add these accounts to the client, making them ready for transactions. + +4. **Minting and Transacting with Unauthenticated Notes:** + - Mint tokens for one of the accounts (Alice) from the deployed faucet. + - Create a note representing the minted tokens. + - Submit the note-creation transaction without waiting for confirmation, then pass the complete note to `.explicit_input_notes([(InputNote::unauthenticated(note), None)])`. The explicit mode stays unauthenticated even if the note commits before the consuming transaction executes. + - Serialize the note to demonstrate how it could be transferred to another client instance. + - Consume the note in a subsequent transaction, effectively creating a chain of unauthenticated transactions. + +5. **Performance Timing and Syncing:** + - Measure the time taken for each transaction iteration. + - Sync the client state and print account balances to verify the transactions. + +## Set up the Rust project + +Start in the directory containing your `tutorials` clone and create a sibling Cargo project: + +```bash +cargo new miden-unauthenticated-notes +cd miden-unauthenticated-notes +rustup override set 1.98.1 +cp ../tutorials/rust-client/Cargo.lock Cargo.lock +``` + +Keep the generated `[package]` section in `Cargo.toml`, replace its empty `[dependencies]` section with the following, and add the development profile. The path assumes the repository clone is named `tutorials`. + +```toml +[dependencies] +# Clone tutorials next to this Cargo project (see Rust client setup). +rust-client = { path = "../tutorials/rust-client" } +miden-client = { version = "=0.16.0", features = ["testing", "tonic"] } +miden-client-sqlite-store = { version = "=0.16.0", package = "miden-client-sqlite-store" } +miden-protocol = { version = "=0.16.0" } +rand = { version = "0.10" } +tokio = { version = "1.48", features = ["rt-multi-thread", "net", "macros", "fs"] } + +[profile.dev] +opt-level = 2 +``` + +Copy the complete Rust example below into `src/main.rs`. Run it from this new project's directory with `TUTORIAL_NETWORK=testnet cargo run --release`. The client creates `store.sqlite3` and `keystore/` here; keep both out of version control. + +## Full Rust code example + +```rust no_run +use rand::Rng; +use rust_client::TutorialClientExt; +use std::{path::PathBuf, sync::Arc}; +use tokio::time::{Duration, Instant}; + +use miden_client::{ + Client, ClientError, + account::{ + AccountBuilder, AccountType, + component::{ + create_singlesig_user_fungible_faucet, BasicWallet, BurnPolicy, FungibleFaucet, + MintPolicy, TokenName, TokenPolicyManager, + }, + }, + asset::{AssetAmount, AssetId, FungibleAsset, TokenSymbol}, + auth::{AuthSecretKey, AuthSingleSig}, + builder::ClientBuilder, + keystore::{FilesystemKeyStore, Keystore}, + note::{Note, NoteType, P2idNote}, + rpc::{GrpcClient, VerifyingRpcClient}, + transaction::{TransactionId, TransactionRequestBuilder}, + utils::{Deserializable, Serializable}, +}; +use miden_client_sqlite_store::ClientBuilderSqliteExt; +use miden_protocol::transaction::InputNote; +use rust_client::{FeeConfig, TutorialNetwork, fund_account_for_fees}; + +/// Waits for a specific transaction to be committed. +async fn wait_for_tx( + client: &mut Client, + tx_id: TransactionId, +) -> Result<(), ClientError> { + rust_client::wait_for_transaction(client, tx_id).await +} + +#[tokio::main] +async fn main() -> Result<(), ClientError> { + // Initialize client + let network = TutorialNetwork::from_env()?; + let endpoint = network.endpoint(); + let timeout_ms = 10_000; + let rpc_client = Arc::new(VerifyingRpcClient::new(GrpcClient::new( + &endpoint, timeout_ms, + ))); + + // Initialize keystore + let keystore_path = PathBuf::from("./keystore"); + let keystore = Arc::new(FilesystemKeyStore::new(keystore_path).unwrap()); + + let store_path = PathBuf::from("./store.sqlite3"); + + let mut client = ClientBuilder::new() + .rpc(rpc_client) + .sqlite_store(store_path) + .authenticator(keystore.clone()) + .build() + .await?; + + let sync_summary = client.sync_state().await.unwrap(); + println!("Latest block: {}", sync_summary.block_num); + let fee_config = FeeConfig::from_client(&client, network).await?; + + //------------------------------------------------------------ + // STEP 1: Deploy a fungible faucet + //------------------------------------------------------------ + println!("\n[STEP 1] Deploying a new fungible faucet."); + + // Faucet seed + let mut init_seed = [0_u8; 32]; + client.rng().fill_bytes(&mut init_seed); + + // Generate key pair + let key_pair = AuthSecretKey::new_falcon512_poseidon2_with_rng(client.rng()); + + // Faucet parameters + let symbol = TokenSymbol::new("MID").unwrap(); + let decimals = 8; + let max_supply = AssetAmount::new(1_000_000).unwrap(); + + // Build the account + let faucet = FungibleFaucet::builder() + .name(TokenName::new("MID").unwrap()) + .symbol(symbol) + .decimals(decimals) + .max_supply(max_supply) + .build() + .unwrap(); + let policies = TokenPolicyManager::builder() + .active_mint_policy(MintPolicy::allow_all()) + .active_burn_policy(BurnPolicy::allow_all()) + .build(); + let faucet_account = create_singlesig_user_fungible_faucet( + init_seed, + faucet, + AuthSingleSig::from_public_key(key_pair.public_key()), + policies, + AccountType::Public, + ) + .unwrap(); + + // Add the faucet to the client + client.add_account(&faucet_account, false).await?; + + println!( + "Faucet account ID: {}", + faucet_account.id().to_bech32(network.network_id()) + ); + + // Add the key pair to the keystore + keystore + .add_key(&key_pair, faucet_account.id()) + .await + .unwrap(); + fund_account_for_fees(&mut client, faucet_account.id(), &fee_config).await?; + + // Resync to show newly deployed faucet + tokio::time::sleep(Duration::from_secs(2)).await; + client.sync_state().await?; + + //------------------------------------------------------------ + // STEP 2: Create basic wallet accounts + //------------------------------------------------------------ + println!("\n[STEP 2] Creating new accounts"); + + let mut accounts = vec![]; + let number_of_accounts = 5; + + for i in 0..number_of_accounts { + let mut init_seed = [0_u8; 32]; + client.rng().fill_bytes(&mut init_seed); + + let key_pair = AuthSecretKey::new_falcon512_poseidon2_with_rng(client.rng()); + + let account = AccountBuilder::new(init_seed) + .account_type(AccountType::Public) + .with_component(AuthSingleSig::from_public_key(key_pair.public_key())) + .with_component(BasicWallet) + .build() + .unwrap(); + + accounts.push(account.clone()); + println!( + "account id {:?}: {}", + i, + account.id().to_bech32(network.network_id()) + ); + client.add_account(&account, true).await?; + + // Add the key pair to the keystore + keystore.add_key(&key_pair, account.id()).await.unwrap(); + fund_account_for_fees(&mut client, account.id(), &fee_config).await?; + } + + // For demo purposes, Alice is the first account. + let alice = &accounts[0]; + + //------------------------------------------------------------ + // STEP 3: Mint and consume tokens for Alice + //------------------------------------------------------------ + println!("\n[STEP 3] Mint tokens"); + println!("Minting tokens for Alice..."); + let amount: u64 = 100; + let fungible_asset_mint_amount = FungibleAsset::new(faucet_account.id(), amount).unwrap(); + let transaction_request = TransactionRequestBuilder::new() + .build_mint_fungible_asset( + fungible_asset_mint_amount, + alice.id(), + NoteType::Public, + client.rng(), + ) + .unwrap(); + + let tx_id = client + .submit_tutorial_transaction(faucet_account.id(), transaction_request) + .await?; + println!("Minted tokens. TX: {:?}", tx_id); + + // Wait for mint transaction to be committed + wait_for_tx(&mut client, tx_id).await?; + + // Get the minted note and consume it + let consumable_notes = client + .get_consumable_tutorial_notes(Some(alice.id())) + .await?; + + if let Some((note_record, _)) = consumable_notes.first() { + let note: Note = note_record.clone().try_into()?; + let transaction_request = + TransactionRequestBuilder::new().build_consume_notes(vec![note])?; + + let consume_tx_id = client + .submit_tutorial_transaction(alice.id(), transaction_request) + .await?; + println!("Consumed minted note. TX: {:?}", consume_tx_id); + + // Wait for consumption to complete + wait_for_tx(&mut client, consume_tx_id).await?; + } + + //------------------------------------------------------------ + // STEP 4: Create unauthenticated note tx chain + //------------------------------------------------------------ + println!("\n[STEP 4] Create unauthenticated note tx chain"); + let start = Instant::now(); + + for i in 0..number_of_accounts - 1 { + let loop_start = Instant::now(); + println!("\nunauthenticated tx {:?}", i + 1); + println!( + "sender: {}", + accounts[i].id().to_bech32(network.network_id()) + ); + println!( + "target: {}", + accounts[i + 1].id().to_bech32(network.network_id()) + ); + + // Time the creation of the p2id note + let send_amount = 20; + let fungible_asset_send_amount = + FungibleAsset::new(faucet_account.id(), send_amount).unwrap(); + + // for demo purposes, unauthenticated notes can be public or private + let note_type = if i % 2 == 0 { + NoteType::Private + } else { + NoteType::Public + }; + + let p2id_note: Note = P2idNote::builder() + .sender(accounts[i].id()) + .target(accounts[i + 1].id()) + .asset(fungible_asset_send_amount) + .note_type(note_type) + .generate_serial_number(client.rng()) + .build() + .unwrap() + .into(); + + let output_note = p2id_note.clone(); + + // Time transaction request building + let transaction_request = TransactionRequestBuilder::new() + .own_output_notes(vec![output_note]) + .build() + .unwrap(); + + // Do not wait for inclusion: the receiver is given the complete note below. + client.sync_state().await?; + let send_tx_id = client + .submit_new_transaction(accounts[i].id(), transaction_request) + .await?; + println!("Created note. TX: {:?}", send_tx_id); + + // Note serialization/deserialization + // This demonstrates how you could send the serialized note to another client instance + let serialized = p2id_note.to_bytes(); + let deserialized_p2id_note = Note::read_from_bytes(&serialized).unwrap(); + + // Time consume note request building + // Keep this input unauthenticated even if syncing has already fetched its proof. + let consume_note_request = TransactionRequestBuilder::new() + .explicit_input_notes([(InputNote::unauthenticated(deserialized_p2id_note), None)]) + .build()?; + + let tx_id = client + .submit_tutorial_transaction(accounts[i + 1].id(), consume_note_request) + .await?; + rust_client::wait_for_transaction(&mut client, send_tx_id).await?; + + println!( + "Consumed Note Tx on MidenScan: {}/tx/{:?}", + network.explorer_url(), + tx_id + ); + println!( + "Total time for loop iteration {}: {:?}", + i, + loop_start.elapsed() + ); + } + + println!( + "\nTotal execution time for unauthenticated note txs: {:?}", + start.elapsed() + ); + + // Final resync and display account balances + tokio::time::sleep(Duration::from_secs(3)).await; + client.sync_state().await?; + for (index, account) in accounts.iter().enumerate() { + let new_account = client.get_account(account.id()).await.unwrap().unwrap(); + let balance = new_account + .vault() + .get_balance(AssetId::new_fungible(faucet_account.id())) + .unwrap(); + println!( + "Account: {} balance: {}", + account.id().to_bech32(network.network_id()), + balance + ); + let expected = if index == 0 { + 80 + } else if index == accounts.len() - 1 { + 20 + } else { + 0 + }; + assert_eq!( + balance.as_u64(), + expected, + "unexpected transfer-chain balance" + ); + } + + Ok(()) +} +``` + +The following is an abbreviated output. IDs and timings vary; each measured iteration includes confirmation polling, and funding logs are omitted: + +```text +Latest block: + +[STEP 1] Deploying a new fungible faucet. +Faucet account ID: + +[STEP 2] Creating new accounts +account id 0: +account id 1: +account id 2: +account id 3: +account id 4: + +[STEP 3] Mint tokens +Minting tokens for Alice... +Minted tokens. TX: +Consumed minted note. TX: + +[STEP 4] Create unauthenticated note tx chain + +unauthenticated tx 1 +sender: +target: +Created note. TX: +Transaction committed: +Transaction committed: +Consumed Note Tx on MidenScan: https://testnet.midenscan.com/tx/ +Total time for loop iteration 0: + +unauthenticated tx 2 +sender: +target: +Created note. TX: +Transaction committed: +Transaction committed: +Consumed Note Tx on MidenScan: https://testnet.midenscan.com/tx/ +Total time for loop iteration 1: + +unauthenticated tx 3 +sender: +target: +Created note. TX: +Transaction committed: +Transaction committed: +Consumed Note Tx on MidenScan: https://testnet.midenscan.com/tx/ +Total time for loop iteration 2: + +unauthenticated tx 4 +sender: +target: +Created note. TX: +Transaction committed: +Transaction committed: +Consumed Note Tx on MidenScan: https://testnet.midenscan.com/tx/ +Total time for loop iteration 3: + +Total execution time for unauthenticated note txs: +Account: balance: 80 +Account: balance: 0 +Account: balance: 0 +Account: balance: 0 +Account: balance: 20 +``` + +## Conclusion + +This example builds and serializes complete notes, then consumes the four transfer notes through `explicit_input_notes` with an explicitly unauthenticated mode. The earlier mint consumption uses `build_consume_notes` and may be authenticated. It confirms four transfers across five accounts and checks the tutorial-asset balances `[80, 0, 0, 0, 20]`; each account's native fee balance is separate. + +Applications can use this pattern to submit dependent transactions before the notes are committed. The node must still accept the creation transaction for its dependent consumption to settle. + +### Running the example + +From the root of your `tutorials` clone, run the checked-in example: + +```bash +cd rust-client +TUTORIAL_NETWORK=testnet cargo run --release --bin unauthenticated_note_transfer +``` + +### Continue learning + +Next tutorial: [How to Use Mappings in Miden Assembly](mappings_in_masm_how_to.md) diff --git a/versioned_docs/version-0.16/builder/tutorials/recipes/web/.prettierrc b/versioned_docs/version-0.16/builder/tutorials/recipes/web/.prettierrc new file mode 100644 index 00000000..0d22bf7b --- /dev/null +++ b/versioned_docs/version-0.16/builder/tutorials/recipes/web/.prettierrc @@ -0,0 +1,9 @@ +{ + "printWidth": 80, + "tabWidth": 2, + "singleQuote": true, + "trailingComma": "all", + "proseWrap": "preserve", + "embeddedLanguageFormatting": "off", + "endOfLine": "lf" +} diff --git a/versioned_docs/version-0.16/builder/tutorials/recipes/web/_category_.json b/versioned_docs/version-0.16/builder/tutorials/recipes/web/_category_.json new file mode 100644 index 00000000..3f10f63b --- /dev/null +++ b/versioned_docs/version-0.16/builder/tutorials/recipes/web/_category_.json @@ -0,0 +1,5 @@ +{ + "label": "Web / TypeScript", + "position": 2, + "collapsed": true +} diff --git a/versioned_docs/version-0.16/builder/tutorials/recipes/web/bridging_with_epoch_tutorial.md b/versioned_docs/version-0.16/builder/tutorials/recipes/web/bridging_with_epoch_tutorial.md new file mode 100644 index 00000000..7c43886b --- /dev/null +++ b/versioned_docs/version-0.16/builder/tutorials/recipes/web/bridging_with_epoch_tutorial.md @@ -0,0 +1,292 @@ +--- +title: 'Bridging Miden to and from EVM with Epoch' +sidebar_position: 9 +--- + +# Bridging Miden to and from EVM with Epoch + +_Move assets between Miden testnet and Sepolia testnet through the Epoch protocol intent SDK, without writing a custom bridge_ + +## Overview + +This is a guided tour of the reference app under [`examples/bridging-app/`](https://github.com/0xMiden/tutorials/tree/main/examples/bridging-app), which bridges fungible tokens between Miden testnet and Sepolia through the [Epoch protocol](https://epochprotocol.xyz/) intent SDK. Clone and run the app, then read the steps below as annotations on the integration points you'd port into your own Miden frontend. Every fenced code block is a verbatim slice of the app; the file and line range above each block points to the source. + +Live settlement requires an Epoch allocator and asset faucet deployed on the selected Miden network and compatible with v0.16. Devnet is available for explicit checks by setting `VITE_MIDEN_NETWORK`, `VITE_MIDEN_RPC_URL`, and `VITE_MIDEN_PROVER` to `devnet` with a matching allocator and faucet. + +Stack: Vite + React 19 + TypeScript, `@miden-sdk/react`, `@epoch-protocol/epoch-intents-sdk`, [RainbowKit](https://www.rainbowkit.com/) + [wagmi](https://wagmi.sh/) + [viem](https://viem.sh/). + +## What we'll cover + +- Wire the Epoch SDK against a wagmi `walletClient`, including the chain-id override for Miden-source intents. +- Build a Miden → EVM bridge: reverse-quote, sign a P2IDE note via the MidenFi wallet adapter, submit the intent, and poll for settlement. +- Build the reverse EVM → Miden bridge: deposit an ERC-20 into Epoch's Compact contract and receive a P2ID note on Miden. +- An `EpochIntentSDK` API reference card for quoting, settlement, and recovery. +- Inline pitfalls — the eleven traps every Epoch integration hits before the first successful round-trip. + +## Prerequisites + +You need three things to follow along. + +1. The reference app, cloned from this repo. The bridging-specific layer (Epoch SDK wiring, wagmi/RainbowKit + viem, intent forms, status panels) lives in `examples/bridging-app/`; `yarn create miden-app` (≥ 1.0.7) is the Miden + Vite + WASM scaffold it started from. Clone the repo, `cd examples/bridging-app`, then `cp .env.example .env` inside that directory and fill in: `VITE_RAINBOWKIT_PROJECT_ID` (a [WalletConnect Cloud](https://cloud.walletconnect.com/) project id — required), `VITE_ALLOCATOR_URL` (an Epoch allocator compatible with the pinned SDK), `VITE_MIDEN_NETWORK` (default `testnet`), optional `VITE_MIDEN_RPC_URL` and `VITE_MIDEN_PROVER` overrides, `VITE_MIDEN_USDC_FAUCET_ID` from the selected allocator deployment, and the optional `VITE_MIDENSCAN_URL`. The wallet network, RPC, faucet, and allocator must agree. Keep native MIDEN tokens in the wallet to pay both collateral and note-consumption fees. See the [setup guide](./setup_guide.md) if this is your first Miden frontend. + +2. Two wallets: an EVM wallet supported by [RainbowKit](https://www.rainbowkit.com/) (MetaMask, Rabby, Coinbase Wallet, …) and the [MidenFi browser extension](https://chromewebstore.google.com/detail/miden-wallet/ablmompanofnodfdkgchkpmphailefpb) for signing P2IDE notes on Miden. + +3. A small Sepolia ETH balance for gas. The community [pk910 PoW faucet](https://sepolia-faucet.pk910.de/) and [Google Cloud Sepolia faucet](https://cloud.google.com/application/web3/faucet/ethereum/sepolia) are possible sources; check their current requirements and limits. Keep enough test ETH for token approvals and the Compact deposit. + +:::caution Do not set COOP/COEP headers +`@miden-sdk/vite-plugin` defaults to `crossOriginIsolation: true`, which sets `Cross-Origin-Opener-Policy` and `Cross-Origin-Embedder-Policy` headers on the dev server and breaks gRPC-Web to `transport.miden.io`. The reference app passes `{ crossOriginIsolation: false }` to opt out — see the [Vite + WASM setup guide](./setup_guide.md) for the deployment-side counterpart. +::: + +## The Reference App + +Clone, install, and boot Vite: + +**From `examples/bridging-app/README.md` (lines 12–15):** + + + +```bash +git clone https://github.com/0xMiden/tutorials.git +cd tutorials/examples/bridging-app +yarn install +yarn dev +``` + +The dev server listens on `http://localhost:5173`. You'll see two tabs — `Bridge to EVM` (Miden → Sepolia) and `Withdraw to Miden` (Sepolia → Miden) — and a wallet-connect strip that gates both forms on the EVM and Miden wallets being connected. The example app is forked from [`epochprotocol/miden-integration-example@efc3a690`](https://github.com/epochprotocol/miden-integration-example) with the bridging-specific adaptations described under "Forked from" in the app's README. + +## Step 1: Wire the Epoch SDK + +`EpochIntentSDK` is the single entry point for the protocol — quoting, intent submission, status, and recovery all live behind it. The reference app lazy-imports the SDK inside a `useEffect` so the React 19 StrictMode double-mount does not initialise it twice, and it overrides `walletClient.chain.id` to `999999999`, the synthetic Miden chain id Epoch's allocator keys Miden lookups by. The EVM-side `walletClient` is otherwise spread through verbatim — wagmi already wired the chain, transport, and signer when the user connected via RainbowKit. + +The snippet below sits inside the `useEpochIntent` hook — `walletClient` is `useWalletClient().data` from wagmi, and `setSdk` is the hook's own `useState` setter. + +**From `examples/bridging-app/src/hooks/useEpochIntent.ts` (lines 22–43):** + + + +```typescript + useEffect(() => { + if (!walletClient) { + setSdk(null); + return; + } + let cancelled = false; + import('@epoch-protocol/epoch-intents-sdk').then(({ EpochIntentSDK }) => { + if (cancelled) return; + const apiBaseUrl = import.meta.env.VITE_ALLOCATOR_URL || 'http://localhost:3000'; + console.log('apiBaseUrl: ', apiBaseUrl); + const midenWalletClient = { + ...(walletClient as any), + chain: { ...((walletClient as any)?.chain ?? {}), id: 999999999 }, + }; + setSdk(new EpochIntentSDK({ apiBaseUrl, walletClient: midenWalletClient })); + }).catch((err) => { + if (cancelled) return; + console.error('[CrossChain] Failed to load Epoch SDK:', err); + setSdk(null); + }); + return () => { cancelled = true; }; + }, [walletClient]); +``` + +:::caution Do not follow the package README +The npm package ships a `# Compact SDK` README that documents a different SDK and a different surface. Treat `EpochIntentSDK`'s exported method names from `dist/sdk/epoch-intent-sdk.d.ts` as the source of truth — the [API Reference Card](#api-reference-card) below lists them. +::: + +The `useWithdrawIntent` hook keeps `walletClient.chain.id` untouched — the EVM → Miden direction uses the real Sepolia chain id (`11155111`). + +## Step 2: Miden → EVM bridge + +A Miden → EVM bridge runs four stages: `getTaskData` (the allocator computes a quote envelope), `getIntentQuote` (price discovery), `solveIntent` (the user signs a P2IDE note on Miden via the wallet adapter callback), and a 5-second polling loop against `getIntentStatus` until the solver lands the EVM transfer. The reference app's `buildEpochTaskDataParams` produces the envelope and computes `midenReclaimHeight` at the call site so it stays relative to the current Miden chain tip (Pitfalls row 4 has the technical reason). + +**From `examples/bridging-app/src/services/epoch-bridge.ts` (lines 133–165):** + + + +```typescript + // Reclaim height must come from the call site as `currentMidenBlock + N`. + // A literal default (e.g. '1000') would be evaluated against an unspecified + // chain tip and become unsafe if the user's note ages before the intent is + // solved — see pitfall §1.7 row 4. + if (params.midenReclaimHeight == null) { + throw new Error( + 'midenReclaimHeight is required; pass String(currentMidenBlock + N) computed at the call site.', + ); + } + + const taskDataParams = { + taskType: 'gettokenout' as TaskType, + intentData: { + // isNative must be false — tokenIn is zero-address (Miden-sourced) but tokenOut is a real EVM token + isNative: false, + depositTokenAddress: ZERO_ADDRESS, + tokenInAmount: amountInSmallestUnit, + outputTokenAddress: outputToken, + minTokenOut: scaledMinTokenOut, + destinationChainId: String(params.destinationChainId), + protocolHashIdentifier: ZERO_HASH, + recipient: params.evmRecipient, + }, + // Mirror EpochSwapWidget Miden extraData pattern exactly + extraDataTypestring: 'string midenSourceAccount,string midenFaucetId,string midenNoteType,string midenNoteId,uint256 midenReclaimHeight', + extraData: { + midenSourceAccount: midenSourceAccountHex, + midenFaucetId: midenFaucetIdHex, + midenNoteType: 'P2IDE', + midenNoteId: '', + midenReclaimHeight: String(params.midenReclaimHeight), + }, + }; +``` + +Once the quote returns and the user clicks **Confirm & sign**, `createMidenP2IDENote` receives the allocator, relative recall window, and mandate-binding attachment from Epoch. The app builds a public P2IDE note containing those exact values, prepares a fee-aware custom request, and asks the wallet to sign it with `requestTransaction`. Amounts remain `bigint`. After confirmation it locates the exact collateral note ID, excluding any `TX_FEE` output. + +**From `examples/bridging-app/src/components/crosschain/IntentForm.tsx` (lines 202–248):** + + + +```typescript + const createMidenP2IDENote: SolveIntentParams['createMidenP2IDENote'] = async ( + faucetIdParam, + amountParam, + allocatorId, + recallBlocks, + bindingAttachmentFelts, + ) => { + setConfirmStatus('Resource lock required — creating P2IDE note on Miden…'); + try { + if (!midenAccountId) { + throw new Error('Missing Miden account id'); + } + if (!requestTransaction || !waitForTransaction || !client) { + throw new Error('Connect a Miden wallet that supports custom transactions and confirmation'); + } + + const { request, expectedNoteId } = await runExclusive(async () => { + const head = await client.syncState(); + const note = createEpochCollateralNote({ + sender: midenAccountId, allocator: allocatorId, faucet: faucetIdParam, + amount: BigInt(amountParam), currentBlock: head.blockNum(), + recallBlocks, bindingAttachmentFelts, + }); + const builder = await client.feeAwareTransactionRequestBuilder(AccountId.fromHex(midenAccountId)); + return { + expectedNoteId: note.id().toString(), + request: builder.withOwnOutputNotes(new NoteArray([note])).build(), + }; + }); + const txId = await requestTransaction( + Transaction.createCustomTransaction(midenAccountId, allocatorId, request), + ); + + // Wait for the wallet to confirm the collateral before submitting it to Epoch. + const finalized = await waitForTransaction(txId, 120_000); + // A fee-paying transaction also creates a TX_FEE note. Match our exact + // collateral note instead of assuming outputNotes[0] is the payment. + const noteId = finalized.outputNotes?.find(note => note.id().toString() === expectedNoteId)?.id().toString(); + if (!noteId) { + throw new Error(`Could not read output note id for tx ${txId}`); + } + setLocalMidenNoteId(noteId); + return { success: true, noteId }; + } catch (err) { + return { success: false, error: err instanceof Error ? err.message : String(err) }; + } + }; +``` + +Success is signalled by the 5-second polling loop: `getIntentStatus` returns an `IntentTransactionStatus[]`, which the app reduces into the composite `IntentFlowStatus`. The forward bridge is settled once the destination chain reports a terminal-OK row (`evmCompleted`) and the synthetic Miden row carries a terminal `midenStatus` — the EVM transfer landed and the allocator consumed the P2IDE note. That reducer is destination-chain aware: it filters status rows to the chain the user selected, never reports completion while any destination-chain row is still `pending`, and takes the last destination-chain success — so an intermediate allocator/Compact row is never mistaken for the final settlement. The Pitfalls section below catalogues the gotchas this step inherits (public note type, awaiting `waitForTransaction`, advisory `midenFaucetDecimals`, the exact collateral-note ID check). + +## Step 3: EVM → Miden bridge + +The reverse direction lives in `buildEVMToMidenTaskDataParams` + `useWithdrawIntent`. The task envelope sets `destinationChainId` to the Miden virtual chain id (`999999999`) so the allocator's `getTokenDataFromMidenFaucetId` resolves the output side as Miden-native, and the note type flips to `P2ID` (not `P2IDE`) because the Miden recipient consumes the note directly rather than recalling it. The reverse-quote convention is the same as Step 2: pass `tokenInAmount: '0'` and a Miden-side `minTokenOut` in base units; the backend computes the required EVM input. + +:::caution Bridge with headroom before the reverse direction +The reverse quote includes route fees, so bridging out one token may not leave enough to request one token back. Compare the quoted input amount with your balance before approval and deposit. Use the selected token's decimals when converting amounts to base units; do not assume an 18-decimal asset. +::: + +**From `examples/bridging-app/src/services/epoch-bridge.ts` (lines 216–237):** + + + +```typescript + const taskDataParams = { + taskType: 'gettokenout' as TaskType, + intentData: { + isNative: false, + depositTokenAddress: params.evmTokenAddress, + tokenInAmount: amountInWei, + outputTokenAddress: ZERO_ADDRESS, + minTokenOut: scaledMinMidenOut, // Miden-side minimum out (base units) + destinationChainId: String(destinationChainId), + protocolHashIdentifier: ZERO_HASH, + recipient: params.evmSourceAddress, + }, + extraDataTypestring: 'string midenRecipientAccount,string midenFaucetId,string midenNoteType', + extraData: { + midenRecipientAccount: midenRecipientHex, + midenFaucetId: midenFaucetHex, + midenNoteType: 'P2ID', + }, + }; + + console.log('[EpochBridge] EVM→Miden task data params built:', taskDataParams); + return taskDataParams; +``` + +`solveIntent({ ..., collateralType: CollateralType.EVM })` then walks the user's wallet through an ERC-20 `approve` (only on the first deposit of a given token) and `depositERC20AndRegister` / `depositNativeAndRegister` against Epoch's [Compact](https://docs.epochprotocol.xyz/integration-guides/sdk-integration-guide) contract on Sepolia. The intent nonce extracted from the solve result drives the same 5-second status poll as the forward direction. + +:::caution Forced-withdrawal preflight +If the user cancelled a prior EVM → Miden intent on the same Compact deposit id, the next intent will revert. Call `sdk.disableForcedWithdrawal(depositId)` first; the SDK error message names the deposit id when this preflight is required. +::: + +:::caution The Withdraw token is Epoch's test ERC-20, not Circle's USDC +The "USDC" the Withdraw form lists is Epoch's test token (`0x2BB4FfD7…`), not Circle's canonical Sepolia USDC, and it has no public faucet. Run **Step 2 (Miden → EVM) first** — it delivers Epoch test USDC to your EVM wallet — then bridge it back. Bridge out before you bridge back. +::: + +## Step 4: The bridged P2ID note is consumed by your wallet + +Step 3's allocator delivers its output as a **P2ID note** addressed to your Miden account, not as a vault credit. The note must be consumed in a transaction before it becomes spendable. Wallet auto-consumption depends on the installed wallet's configuration and sufficient native MIDEN for fees: a USDC-only note cannot pay that native fee. The reference app links the delivered note to Midenscan; confirm its consumption and the final wallet balance before treating the bridge as complete. + +## API Reference Card + +Most apps only touch four methods. The selected signatures below come from `dist/sdk/epoch-intent-sdk.d.ts` in the pinned `@epoch-protocol/epoch-intents-sdk@1.0.38`. + +| Method | Signature (abridged) | Purpose | +| --------------------------- | ------------------------------------------------------------- | ---------------------------- | +| `getTaskData` | `(params: GetTaskDataParams)` | Build the quote envelope | +| `getIntentQuote` | `({ sponsorAddress, taskTypeString, intentData, isNative? })` | Obtain a quote | +| `solveIntent` | `(params: SolveIntentParams)` | Sign and submit the intent | +| `getIntentStatus` | `(userAddress: string, nonce: string)` | Poll settlement | +| `retryIntentSolve` | `(request: CompactRequest)` | Retry a recorded allocation | +| `initateDepositWithdrawal` | `(id: string)` | Initiate forced withdrawal | +| `disableForcedWithdrawal` | `(id: string)` | Cancel forced withdrawal | +| `withdrawToken` | `(id: string, recipient: string, amount: string)` | Withdraw a deposit | +| `getForcedWithdrawalStatus` | `(account: string, id: string)` | Inspect recovery status | +| `getDepositedBalances` | `(account: string, tokens: CompactTokenBalanceInput[])` | Inspect locked balances | +| `getHealthCheck` | `()` | Probe allocator availability | + +Recovery primitives (`retryIntentSolve`, `disableForcedWithdrawal`, `withdrawToken`, `initateDepositWithdrawal`) are the difference between an intent flow that "mostly works" and one that lets users recover from solver outages or network failures. + +## Pitfalls + +Check these integration details before a live round trip: + +- **Don't follow the npm package README.** It documents an unrelated SDK; the [integration guide](https://docs.epochprotocol.xyz/integration-guides/sdk-integration-guide) and `dist/sdk/epoch-intent-sdk.d.ts` are the source of truth. +- **Public notes only.** P2IDE notes for the allocator must be `'public'`; a `'private'` note is invisible to the solver. +- **Await confirmation and match the note ID.** A fee-paying transaction also creates a `TX_FEE` output. Do not pass `outputNotes[0]` to Epoch; locate the exact collateral note ID after `waitForTransaction`. +- **Honor Epoch’s callback window and binding.** `createMidenP2IDENote` supplies `recallBlocks` and `bindingAttachmentFelts`. Build a public P2IDE with `currentBlock + recallBlocks` after a fresh sync and include the attachment verbatim. A plain `SendTransaction` cannot represent this attachment. The quote’s preliminary reclaim-height field is not a substitute for the callback values. +- **`minTokenOut` is base units.** The reverse-quote path passes it straight through — no `parseUnits`. For an 18-decimal token, `"1000000000000000000"` is one whole unit. +- **Override `walletClient.chain.id` for Miden-source intents.** Set `chain.id = 999999999` for Miden → EVM only; leave it as the real EVM chain id for the reverse direction. +- **`midenFaucetDecimals` is advisory.** Fall back to UI-selected decimals when the allocator value would change the displayed amount by an order of magnitude. +- **Keep collateral amounts as `bigint`.** The custom request uses `FungibleAsset` directly, avoiding a lossy `number` conversion. +- **Check browser cross-origin compatibility.** This single-threaded app uses `midenVitePlugin({ crossOriginIsolation: false })`; verify the chosen RPC and wallet support before enabling cross-origin isolation. +- **Forced-withdrawal preflight.** Call `sdk.disableForcedWithdrawal` before re-running an EVM → Miden intent on a deposit id the user cancelled previously. +- **`initateDepositWithdrawal` (misspelling).** The SDK exports the method with the typo — use it verbatim, do not silently rename. + +## Where to go next + +- The runnable [`examples/bridging-app/`](https://github.com/0xMiden/tutorials/tree/main/examples/bridging-app) is the canonical reference; every code block above is a paste-verified slice of it. +- The [Epoch protocol integration guide](https://docs.epochprotocol.xyz/integration-guides/sdk-integration-guide) covers the SDK surface in depth, including the parts this tutorial does not exercise (multi-hop intents, custom resource locks). +- Upstream Epoch example: [`epochprotocol/miden-integration-example`](https://github.com/epochprotocol/miden-integration-example). The reference app forks this with the adaptations documented in its README. +- The companion [React wallet tutorial](./react_wallet_tutorial.md) walks the `@miden-sdk/react` hook surface end-to-end if you want a deeper foundation before extending the bridging app. diff --git a/versioned_docs/version-0.16/builder/tutorials/recipes/web/counter_contract_tutorial.md b/versioned_docs/version-0.16/builder/tutorials/recipes/web/counter_contract_tutorial.md new file mode 100644 index 00000000..53349cf6 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tutorials/recipes/web/counter_contract_tutorial.md @@ -0,0 +1,523 @@ +--- +title: 'Incrementing the Count of the Counter Contract' +sidebar_position: 5 +--- + +_Using the Miden client to interact with a custom smart contract_ + +:::note v0.16 setup + +Follow the [network and fee setup](./setup_guide.md#network-and-fee-setup) +and copy the shared support files imported by the complete example. + +::: + +## Overview + +In this tutorial, we will deploy a custom counter smart contract and increment its count using the Miden client. Each run creates a fresh counter account, deploys it to the network, and immediately calls its `increment_count` procedure via a transaction script — so the final count is always `1`. + +This tutorial provides a foundational understanding of building and interacting with custom smart contracts on Miden. + +## What we'll cover + +- Deploying a custom smart contract on Miden from a web client +- Calling procedures in an account from a transaction script + +## Prerequisites + +- Node `v20` or greater +- Familiarity with TypeScript +- `yarn` + +This tutorial assumes you have a basic understanding of Miden assembly. To quickly get up to speed with Miden assembly (MASM), please play around with running basic Miden assembly programs in the [Miden playground](https://0xmiden.github.io/examples/). + +## Step 1: Initialize your Next.js project + +1. Create a new Next.js app with TypeScript: + + ```bash + npx create-next-app@latest miden-web-app --typescript + ``` + + Hit enter for all terminal prompts. + +2. Change into the project directory: + + ```bash + cd miden-web-app + ``` + +3. Install the Miden SDK: + ```bash + yarn add @miden-sdk/miden-sdk@0.16.0 + ``` + +The current Next.js template uses Turbopack by default. These examples use the webpack configuration from the setup guide, so update both scripts in `package.json`: + +`package.json` + +```json +{ + "scripts": { + "dev": "next dev --webpack", + "build": "next build --webpack" + } +} +``` + +## Step 2: Edit the `app/page.tsx` file: + +Add the following code to the `app/page.tsx` file. This code defines the main page of our web application: + +```tsx +'use client'; +import { useState } from 'react'; +import { incrementCounterContract } from '../lib/incrementCounterContract'; + +export default function Home() { + const [isIncrementCounter, setIsIncrementCounter] = useState(false); + + const handleIncrementCounterContract = async () => { + setIsIncrementCounter(true); + await incrementCounterContract(); + setIsIncrementCounter(false); + }; + + return ( +
+
+

Miden Web App

+

+ Open your browser console to see Miden client logs. +

+ +
+ +
+
+
+ ); +} +``` + +## Step 3: Write the MASM Counter Contract + +The counter contract code lives in a separate `.masm` file. Create a `lib/masm/` directory and add the contract file: + +```bash +mkdir -p lib/masm +``` + +Create the file `lib/masm/counter_contract.masm` with the following Miden Assembly code: + +```masm +use miden::protocol::active_account +use miden::protocol::native_account +use miden::core::sys + +# CONSTANTS +# ================================================================================================= + +const COUNTER_SLOT = word("miden::tutorials::counter") + +# PUBLIC INTERFACE +# ================================================================================================= + +#! Returns the current count. +#! +#! Inputs: [pad(16)] +#! Outputs: [count, pad(15)] +#! +#! Invocation: call +@account_procedure +pub proc get_count() -> felt + push.COUNTER_SLOT[0..2] exec.active_account::get_item + # => [[count, 0, 0, 0], pad(16)] + + exec.sys::truncate_stack + # => [count, pad(15)] +end + +#! Increments the current count by one. +#! +#! Inputs: [pad(16)] +#! Outputs: [pad(16)] +#! +#! Invocation: call +@account_procedure +pub proc increment_count() + push.COUNTER_SLOT[0..2] exec.active_account::get_item + # => [[count, 0, 0, 0], pad(16)] + + add.1 + # => [[count + 1, 0, 0, 0], pad(16)] + + push.COUNTER_SLOT[0..2] exec.native_account::set_item + # => [OLD_VALUE, pad(16)] + + dropw + # => [pad(16)] + + exec.sys::truncate_stack + # => [pad(16)] +end +``` + +Also create `lib/masm/masm.d.ts` so TypeScript recognizes `.masm` imports: + +```ts +declare module '*.masm' { + const content: string; + export default content; +} +``` + +## Step 4: Configure Your Bundler to Import `.masm` Files + +Add an `asset/source` webpack rule so `.masm` files are imported as plain text strings. + +Open `next.config.ts` and add the following rule inside the `webpack` callback: + +```ts +// Import .masm files as strings. Keep the existing WASM configuration. +config.module.rules.push({ + test: /\.masm$/, + type: "asset/source", +}); +``` + +:::tip Other bundlers + +- **Vite:** use the `?raw` suffix — `import code from './masm/counter_contract.masm?raw'` +- **Other bundlers / no bundler:** use `fetch()` at runtime — `const code = await fetch('/masm/counter_contract.masm').then(r => r.text())` + ::: + +## Step 5: Incrementing the Count of the Counter Contract + +Create the file `lib/incrementCounterContract.ts`: + +```bash +touch lib/incrementCounterContract.ts +``` + +Copy and paste the following code into the `lib/incrementCounterContract.ts` file: + +```ts +// lib/incrementCounterContract.ts +import counterContractCode from './masm/counter_contract.masm'; +import { + AuthSecretKey, + StorageSlot, + StorageResult, +} from '@miden-sdk/miden-sdk/lazy'; +import { + createFundableContractAccount, + createTutorialClient, + fundAccountForFees, +} from './feeSupport'; + +export async function incrementCounterContract(): Promise { + if (typeof window === 'undefined') { + console.warn('webClient() can only run in the browser'); + return; + } + + const client = await createTutorialClient({ proverUrl: 'local' }); + console.log('Current block number: ', (await client.sync()).blockNum()); + + const counterSlotName = 'miden::tutorials::counter'; + + const counterAccountComponent = await client.compile.component({ + code: counterContractCode, + slots: [StorageSlot.emptyValue(counterSlotName)], + }); + + const walletSeed = new Uint8Array(32); + crypto.getRandomValues(walletSeed); + const auth = AuthSecretKey.rpoFalconWithRNG(walletSeed); + + const account = await createFundableContractAccount( + client, + walletSeed, + auth, + [counterAccountComponent], + ); + + await fundAccountForFees(client, account); + + const txScriptCode = ` +use external_contract::counter_contract + +#! Increments the counter. +#! +#! Inputs: [ARGS, pad(12)] +#! Outputs: [pad(16)] +#! +#! Where: +#! - ARGS contains unused transaction script arguments. +#! +#! Invocation: dyncall +@transaction_script +pub proc main(args: word) + dropw + # => [pad(16)] + + call.counter_contract::increment_count + # => [pad(16)] +end +`; + + const script = await client.compile.txScript({ + code: txScriptCode, + libraries: [ + { + namespace: 'external_contract::counter_contract', + code: counterContractCode, + }, + ], + }); + + await client.sync(); + const { txId } = await client.transactions.execute({ + account, + script, + waitForConfirmation: true, + timeout: 120_000, + }); + console.log(`Transaction committed: ${txId.toHex()}`); + + console.log('Counter contract ID:', account.id().toString()); + + const counter = await client.accounts.get(account); + // `getItem()` is typed to return a low-level `Word`, but at runtime the SDK + // wraps the slot in a `StorageResult` whose `toBigInt()` reads the first + // felt — the count. The cast reflects that runtime type. + const count = counter?.storage().getItem(counterSlotName) as unknown as + StorageResult | undefined; + const counterValue = Number(count!.toBigInt()); + if (counterValue !== 1) + throw new Error(`Expected counter 1, got ${counterValue}`); + console.log('Count: ', counterValue); +} +``` + +To run the code above in our frontend, run the following command: + +```bash +yarn dev +``` + +Open the browser console and click the button "Increment Counter Contract". + +This is what you should see in the browser console (block number and account +ID will vary with live testnet state; the tutorial deploys a fresh counter and +increments it exactly once before reading, so the final count is always `1`): + +``` +Current block number: +Counter contract ID: +Count: 1 +``` + +## Miden Assembly Counter Contract Explainer + +#### Here's a breakdown of what the `get_count` procedure does: + +1. Pushes the slot ID prefix and suffix for `miden::tutorials::counter` onto the stack. +2. Calls `active_account::get_item` with the slot ID. +3. Calls `sys::truncate_stack` to truncate the stack to size 16. +4. The value returned from `active_account::get_item` is still on the stack and will be returned when this procedure is called. + +#### Here's a breakdown of what the `increment_count` procedure does: + +1. Pushes the slot ID prefix and suffix for `miden::tutorials::counter` onto the stack. +2. Calls `active_account::get_item` with the slot ID. +3. Pushes `1` onto the stack. +4. Adds `1` to the count value returned from `active_account::get_item`. +5. Pushes the slot ID prefix and suffix again so we can write the updated count. +6. Calls `native_account::set_item` which saves the incremented count to storage. +7. Drops the previous storage word returned by `set_item`. +8. Calls `sys::truncate_stack` to leave only the 16 padding elements. + +```masm +use miden::protocol::active_account +use miden::protocol::native_account +use miden::core::sys + +# CONSTANTS +# ================================================================================================= + +const COUNTER_SLOT = word("miden::tutorials::counter") + +# PUBLIC INTERFACE +# ================================================================================================= + +#! Returns the current count. +#! +#! Inputs: [pad(16)] +#! Outputs: [count, pad(15)] +#! +#! Invocation: call +@account_procedure +pub proc get_count() -> felt + push.COUNTER_SLOT[0..2] exec.active_account::get_item + # => [[count, 0, 0, 0], pad(16)] + + exec.sys::truncate_stack + # => [count, pad(15)] +end + +#! Increments the current count by one. +#! +#! Inputs: [pad(16)] +#! Outputs: [pad(16)] +#! +#! Invocation: call +@account_procedure +pub proc increment_count() + push.COUNTER_SLOT[0..2] exec.active_account::get_item + # => [[count, 0, 0, 0], pad(16)] + + add.1 + # => [[count + 1, 0, 0, 0], pad(16)] + + push.COUNTER_SLOT[0..2] exec.native_account::set_item + # => [OLD_VALUE, pad(16)] + + dropw + # => [pad(16)] + + exec.sys::truncate_stack + # => [pad(16)] +end +``` + +The examples follow the [protocol MASM conventions](https://github.com/0xMiden/protocol/tree/next/.claude/skills): public procedures declare typed signatures and invocation style, and stack comments list the top element first. Calls return 16 stack elements, including `pad(N)` padding; storage values are four-element words. + +### Authentication Component + +The counter uses a Falcon single-signature authentication component. The client +stores the secret key and signs transactions that increment the counter. Public +storage lets other accounts read its state through FPI; it does not grant them +permission to update it. + +The account also includes `BasicWallet` so it can consume a native-asset funding +note and pay transaction fees. The `createFundableContractAccount` helper adds +both components and registers the account and key in the client. + +### Compiling the account component + +Use `client.compile.component()` to compile MASM code and its storage slots into an `AccountComponent`. Each call creates a fresh compiler instance so compilations are fully independent: + +```ts +const counterAccountComponent = await client.compile.component({ + code: counterContractCode, + slots: [StorageSlot.emptyValue(counterSlotName)], +}); +``` + +### Creating the contract account + +Use the repository helper to build the account with authentication, the custom +counter component, and `BasicWallet`, then fund it before executing a script: + +```ts +const auth = AuthSecretKey.rpoFalconWithRNG(walletSeed); + +const account = await createFundableContractAccount( + client, + walletSeed, + auth, + [counterAccountComponent], +); +await fundAccountForFees(client, account); +``` + +### Compiling and executing the custom script + +Use `client.compile.txScript()` to compile a transaction script. Pass any needed libraries inline — the client links them dynamically: + +```ts +const script = await client.compile.txScript({ + code: txScriptCode, + libraries: [ + { + namespace: 'external_contract::counter_contract', + code: counterContractCode, + }, + ], +}); +``` + +Synchronize, execute the script, and wait for commitment: + +```ts +await client.sync(); +await client.transactions.execute({ + account, + script, + waitForConfirmation: true, + timeout: 120_000, +}); +``` + +### Custom script + +This is the Miden assembly script that calls the `increment_count` procedure during the transaction. + +```masm +use external_contract::counter_contract + +#! Increments the counter. +#! +#! Inputs: [ARGS, pad(12)] +#! Outputs: [pad(16)] +#! +#! Where: +#! - ARGS contains unused transaction script arguments. +#! +#! Invocation: dyncall +@transaction_script +pub proc main(args: word) + dropw + # => [pad(16)] + + call.counter_contract::increment_count + # => [pad(16)] +end +``` + +### Running the example + +To run a full working example navigate to the `web-client` directory in the [miden-tutorials](https://github.com/0xMiden/miden-tutorials/) repository and run the web application example: + +```bash +cd web-client +yarn install +yarn dev +``` + +### Resetting the `MidenClientDB` + +The Miden webclient stores account and note data in IndexedDB. Stop or terminate the tutorial client and close other tabs using its store before resetting it. This deletes local account data and keys, so use it only for disposable tutorial accounts. The following browser-console snippet deletes the default testnet `MidenClientDB_mtst` store after the deletion request completes; change `name` if you configured a different store. + +```javascript +(async () => { + const name = 'MidenClientDB_mtst'; + await new Promise((resolve, reject) => { + const request = indexedDB.deleteDatabase(name); + request.onsuccess = () => resolve(); + request.onerror = () => reject(request.error); + request.onblocked = () => + reject(new Error('Close clients and tabs using this store, then retry.')); + }); + console.log(`Deleted database: ${name}`); +})(); +``` diff --git a/versioned_docs/version-0.16/builder/tutorials/recipes/web/create_deploy_tutorial.md b/versioned_docs/version-0.16/builder/tutorials/recipes/web/create_deploy_tutorial.md new file mode 100644 index 00000000..15aa44b4 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tutorials/recipes/web/create_deploy_tutorial.md @@ -0,0 +1,469 @@ +--- +title: 'Creating Accounts and Deploying Faucets' +sidebar_position: 2 +--- + +import { CodeSdkTabs } from '@site/src/components'; + +_Using the Miden client in TypeScript to create accounts and deploy faucets_ + +:::note v0.16 setup + +Follow the [network and fee setup](./setup_guide.md#network-and-fee-setup) +and copy the shared support files imported by the complete example. +For React snippets, initialize `authScheme` with `await tutorialAuthScheme()` +as shown in the complete example. + +::: + +## Overview + +In this tutorial, we'll build a simple Next.js application that demonstrates the fundamentals of interacting with the Miden blockchain using the Miden SDK. We'll walk through creating a Miden account for Alice and deploying a fungible faucet contract that can mint tokens. This sets the foundation for more complex operations like issuing assets and transferring them between accounts. + +## What we'll cover + +- Understanding the difference between public and private accounts & notes +- Instantiating the Miden client +- Creating new accounts (public or private) +- Deploying a faucet to fund an account + +## Prerequisites + +- Node `v20` or greater +- Familiarity with TypeScript +- `yarn` + +## Public vs. private accounts & notes + +Before we dive into code, a quick refresher: + +- **Public accounts**: The account's data and code are stored on-chain and are openly visible, including its assets. +- **Private accounts**: The account's state and logic are kept off-chain. The owner retains them locally and can share them; the chain records the account commitment. +- **Public notes**: The note's state is visible to anyone - perfect for scenarios where transparency is desired. +- **Private notes**: The note's state is stored off-chain, you will need to share the note data with the relevant parties (via email or Telegram) for them to be able to consume the note. + +> **Important**: In Miden, "accounts" and "smart contracts" can be used interchangeably due to native account abstraction. Every account is programmable and can contain custom logic. + +It is useful to think of notes on Miden as "cryptographic cashier's checks" that allow users to send tokens. Private note details must be shared with the recipient. The chain still records public note metadata, the note commitment, and its eventual nullifier; privacy also depends on how the details and transaction witness are shared. + +## Step 1: Initialize your Next.js project + +1. Create a new Next.js app with TypeScript: + + ```bash + npx create-next-app@latest miden-web-app --typescript + ``` + + Hit enter for all terminal prompts. + +2. Change into the project directory: + + ```bash + cd miden-web-app + ``` + +3. Install the Miden SDK: + + + +The current Next.js template uses Turbopack by default. These SDK examples use the webpack configuration from the setup guide, so update both scripts in `package.json`: + +`package.json` + +```json +{ + "scripts": { + "dev": "next dev --webpack", + "build": "next build --webpack" + } +} +``` + +## Step 2: Set up the Miden client + +The Miden client is your gateway to interact with the Miden blockchain. It handles state synchronization, transaction creation, and proof generation. Let's set it up. + +### Create the library file + +First, we'll create a separate file for our blockchain logic. In the project root, create a folder `lib/` and inside it `lib/react/createMintConsume.tsx` (React) or `lib/createMintConsume.ts` (TypeScript): + +```bash +mkdir -p lib/react +``` + + { +..// We'll add our logic here +..console.log('Ready to go!'); +.}; + +.return ( +..
+... +..
+.); +} + +export default function CreateMintConsume() { +.return ( +.. +... +.. +.); +}`}, + typescript: { code:`// lib/createMintConsume.ts +import { MidenClient, StorageMode } from '@miden-sdk/miden-sdk/lazy'; + +export async function createMintConsume(): Promise { +.if (typeof window === 'undefined') { +..console.warn('webClient() can only run in the browser'); +..return; +.} + +.// Wait for the WASM module to finish initializing before touching any +.// wasm-bindgen type (see setup_guide.md "Entry points: eager vs lazy"). +.await MidenClient.ready(); + +.// Connect to Miden testnet with local proving +.const client = await MidenClient.createTestnet({ +..proverUrl: 'local', +.}); + +.// 1. Sync with the latest blockchain state +.// This fetches the latest block header and state commitments +.const state = await client.sync(); +.console.log('Latest block number:', state.blockNum()); + +.// At this point, your client is connected and synchronized +.// Ready to create accounts and deploy contracts! +}` }, +}} reactFilename="lib/react/createMintConsume.tsx" tsFilename="lib/createMintConsume.ts" /> + +> Since we will be handling proof generation in the browser, it will be slower than proof generation handled by the Rust client. Check out the [tutorial on delegated proving](./creating_multiple_notes_tutorial.md#what-is-delegated-proving) to speed up proof generation in the browser. + +## Step 3: Create the User Interface + +Now let's create a simple UI that will trigger our blockchain interactions. We'll replace the default Next.js page with a button that calls our function. + +Edit `app/page.tsx`: + +If you're using the **React SDK**, the page simply renders your self-contained component: + +```tsx +// app/page.tsx +'use client'; +import CreateMintConsume from '../lib/react/createMintConsume'; + +export default function Home() { + return ; +} +``` + +If you're using the **TypeScript SDK**, the page manages state and calls the library function directly: + +```tsx +// app/page.tsx +'use client'; +import { useState } from 'react'; +import { createMintConsume } from '../lib/createMintConsume'; + +export default function Home() { + const [isCreatingNotes, setIsCreatingNotes] = useState(false); + + const handleCreateMintConsume = async () => { + setIsCreatingNotes(true); + await createMintConsume(); + setIsCreatingNotes(false); + }; + + return ( +
+
+

Miden Web App

+

+ Open your browser console to see Miden client logs. +

+ +
+ +
+
+
+ ); +} +``` + +## Step 4: Create Alice's Wallet Account + +Now we'll create Alice's account. Let's create a **public** account so we can easily track her transactions. + +Back in your library file, extend the function: + + { +.// 1. Create Alice's wallet (public, mutable) +.console.log('Creating account for Alice…'); +.const alice = await createWallet({ storageMode: StorageMode.Public, authScheme }); +.console.log('Alice ID:', alice.id().toString()); +};` }, +typescript: { code: `// lib/createMintConsume.ts +import { MidenClient, StorageMode } from '@miden-sdk/miden-sdk/lazy'; + +export async function createMintConsume(): Promise { +.if (typeof window === 'undefined') { +..console.warn('webClient() can only run in the browser'); +..return; +.} + +.// Wait for the WASM module to finish initializing before touching any +.// wasm-bindgen type (see setup_guide.md "Entry points: eager vs lazy"). +.await MidenClient.ready(); + +.const client = await MidenClient.createTestnet({ +..proverUrl: 'local', +.}); + +.// 1. Sync with the latest blockchain state +.const state = await client.sync(); +.console.log('Latest block number:', state.blockNum()); + +.// 2. Create Alice's account +.console.log('Creating account for Alice…'); +.const alice = await client.accounts.create({ +..storage: StorageMode.Public, // Public: account state is visible on-chain +.}); +.console.log('Alice ID:', alice.id().toString()); +}` }, +}} reactFilename="lib/react/createMintConsume.tsx" tsFilename="lib/createMintConsume.ts" /> + +## Step 5: Deploy a Fungible Faucet + +A faucet in Miden is a special type of account that can mint new tokens. Think of it as your own token factory. Let's deploy one that will create our custom "MID" tokens. + +Add this code after creating Alice’s account. Use `fundAccount` from `useTutorialSupport` in React, or import `fundAccountForFees` from `./feeSupport` in TypeScript, as shown in the complete example. Consuming native fee funding is the first transaction that deploys each account. + + + +### Understanding Faucet Parameters: + +- **Storage**: We use `StorageMode.Public` so anyone can verify the faucet's minting operations +- **Faucet selection**: In the TypeScript facade a fungible faucet is selected with `type: 0`; the React SDK exposes this directly via `createFaucet` +- **Token Symbol**: A short identifier for your token (e.g., "MID", "USDC", "DAI") +- **Decimals**: Determines the smallest unit of your token. With 8 decimals, 1 MID = 10^8 base units +- **Max Supply**: The maximum number of tokens that can ever exist + +> **Note**: When tokens are minted from a faucet, they're created as "notes" - Miden's version of UTXOs. Each note contains tokens and can have specific spending conditions. + +## Summary + +In this tutorial, we've successfully: + +1. Set up a Next.js application with the Miden SDK +2. Connected to Miden testnet +3. Created a wallet account for Alice +4. Deployed a fungible faucet that can mint custom tokens + +Your final `lib/react/createMintConsume.tsx` (React) or `lib/createMintConsume.ts` (TypeScript) should look like: + + { +..console.log('Synchronizing before creating accounts…'); +..await sync(); +..console.log('Creating Alice with useCreateWallet…'); +..const authScheme = await tutorialAuthScheme(); +..// Native fee tokens and the tutorial's MID token are separate assets. +..const alice = await createWallet({ +...storageMode: StorageMode.Public, +...authScheme, +..}); +..console.log('Alice ID:', alice.id().toString()); +..await fundAccount(alice); + +..// v0.16 faucets include BasicWallet, so they can receive fee funding. +..const faucet = await createFaucet({ +...tokenSymbol: 'MID', +...decimals: 8, +...maxSupply: BigInt(1_000_000), +...storageMode: StorageMode.Public, +...authScheme, +..}); +..console.log('Faucet ID:', faucet.id().toString()); +..await fundAccount(faucet); + +..console.log('Setup complete.'); +.}; + +.return ( +.. +.); +} + +export default function CreateMintConsume() { +.return ( +.. +... +.. +.); +}`}, + typescript: { code: `// lib/createMintConsume.ts +import { StorageMode } from '@miden-sdk/miden-sdk/lazy'; +import { +.createTutorialClient, +.fundAccountForFees, +} from './feeSupport'; + +export async function createMintConsume(): Promise { +.if (typeof window === 'undefined') { +..console.warn('webClient() can only run in the browser'); +..return; +.} + +.const client = await createTutorialClient({ +..proverUrl: 'local', +.}); + +.// 1. Sync with the latest blockchain state +.const state = await client.sync(); +.console.log('Latest block number:', state.blockNum()); + +.// 2. Create Alice's account +.console.log('Creating account for Alice…'); +.const alice = await client.accounts.create({ +..storage: StorageMode.Public, +.}); +.console.log('Alice ID:', alice.id().toString()); + +.// 3. Create our own fungible faucet. SDK v0.16 includes BasicWallet, +.// allowing both accounts to consume native fee funding before minting MID. +.console.log('Creating faucet…'); +.const faucet = await client.accounts.create({ +..type: 0, // 0 = FungibleFaucet +..symbol: 'MID', +..decimals: 8, +..maxSupply: BigInt(1_000_000), +..storage: StorageMode.Public, +.}); +.console.log('Faucet ID:', faucet.id().toString()); +.await fundAccountForFees(client, alice); +.await fundAccountForFees(client, faucet); + +.console.log('Setup complete.'); +}` }, +}} reactFilename="lib/react/createMintConsume.tsx" tsFilename="lib/createMintConsume.ts" /> + +### Running the example + +From the parent directory of `miden-web-app`: + +```bash +cd miden-web-app +yarn install +yarn dev +``` + +Open [http://localhost:3000](http://localhost:3000) in your browser, click **Tutorial #1: Create a wallet and deploy a faucet**, and check the browser console (F12 or right-click → Inspect → Console): + +``` +Latest block number: +Creating account for Alice… +Alice ID: +Creating faucet… +Faucet ID: +Setup complete. +``` + +## What's Next? + +Now that you have: + +- A wallet account for Alice that can hold tokens +- A faucet that can mint new MID tokens + +In the next tutorial, we'll: + +1. Mint tokens from the faucet to Alice's account +2. Consume notes +3. Transfer tokens between accounts diff --git a/versioned_docs/version-0.16/builder/tutorials/recipes/web/creating_multiple_notes_tutorial.md b/versioned_docs/version-0.16/builder/tutorials/recipes/web/creating_multiple_notes_tutorial.md new file mode 100644 index 00000000..a4751a36 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tutorials/recipes/web/creating_multiple_notes_tutorial.md @@ -0,0 +1,577 @@ +--- +title: 'Creating Multiple Notes in a Single Transaction' +sidebar_position: 4 +--- + +import { CodeSdkTabs } from '@site/src/components'; + +_Using the Miden client in TypeScript to create several P2ID notes in a single transaction_ + +:::note v0.16 setup + +Follow the [network and fee setup](./setup_guide.md#network-and-fee-setup) +and copy the shared support files imported by the complete example. +For React snippets, initialize `authScheme` with `await tutorialAuthScheme()` +as shown in the complete example. + +::: + +## Overview + +In the previous sections we learned how to create accounts, deploy faucets, and mint tokens. In this tutorial we will: + +- **Mint** test tokens from a faucet to Alice +- **Consume** the minted notes so the assets appear in Alice's wallet +- **Create three P2ID notes in a _single_ transaction** using standard P2ID notes and delegated proving + +The entire flow is wrapped in a helper called `multiSendWithDelegatedProver()` that you can call from any browser page. + +## What we'll cover + +1. Setting-up the Miden client +2. Building three P2ID notes worth 100 `MID` each +3. Submitting the transaction _using delegated proving_ + +## Prerequisites + +- Node `v20` or greater +- Familiarity with TypeScript +- `yarn` + +## What is Delegated Proving? + +Before diving into our code example, let's clarify what in the world "delegated proving" actually is. + +Delegated proving moves transaction proof generation to a remote service. This can reduce the work required on a mobile device or in a browser. The time to submit a transaction still depends on execution, network latency, prover capacity, and node settlement. + +_How does it work?_ The client sends a transaction witness to the delegated prover, receives the generated proof, and submits the proven transaction to the node. The node verifies the proof. Delegation shares the witness with the prover, including private data needed for execution; use local proving when those inputs must remain on your device. + +Anyone can run their own delegated prover server. If you are building a product on Miden, it may make sense to run your own delegated prover server for your users. To run your own delegated proving server, follow the instructions here: https://crates.io/crates/miden-proving-service + +The code below uses `client.transactions.submit()`, which handles proving via the network's delegated +proving service. This means your browser never has to generate the full ZK proof locally. + +## Step 1: Initialize your Next.js project + +1. Create a new Next.js app with TypeScript: + + ```bash + npx create-next-app@latest miden-web-app --typescript + ``` + + Hit enter for all terminal prompts. + +2. Change into the project directory: + + ```bash + cd miden-web-app + ``` + +3. Install the Miden SDK: + + + +The current Next.js template uses Turbopack by default. These SDK examples use the webpack configuration from the setup guide, so update both scripts in `package.json`: + +`package.json` + +```json +{ + "scripts": { + "dev": "next dev --webpack", + "build": "next build --webpack" + } +} +``` + +## Step 2: Edit the `app/page.tsx` file: + +Add the following code to the `app/page.tsx` file: + +If you're using the **React SDK**, the page simply renders your self-contained component: + +```tsx +// app/page.tsx +'use client'; +import MultiSendWithDelegatedProver from '../lib/react/multiSendWithDelegatedProver'; + +export default function Home() { + return ; +} +``` + +If you're using the **TypeScript SDK**, the page manages state and calls the library function directly: + +```tsx +// app/page.tsx +'use client'; +import { useState } from 'react'; +import { multiSendWithDelegatedProver } from '../lib/multiSendWithDelegatedProver'; + +export default function Home() { + const [isMultiSendNotes, setIsMultiSendNotes] = useState(false); + + const handleMultiSendNotes = async () => { + setIsMultiSendNotes(true); + await multiSendWithDelegatedProver(); + setIsMultiSendNotes(false); + }; + + return ( +
+
+

Miden Web App

+

+ Open your browser console to see Miden client logs. +

+ +
+ +
+
+
+ ); +} +``` + +## Step 3 — Initialize the Miden client + +Create `lib/react/multiSendWithDelegatedProver.tsx` (React) or `lib/multiSendWithDelegatedProver.ts` (TypeScript) and add the following code. This snippet initializes the Miden client. + +```bash +mkdir -p lib/react +``` + + { +..await sync(); +..const authScheme = await tutorialAuthScheme(); +..// We'll add our logic here +.}; + +.return ( +..
+... +..
+.); +} + +export default function MultiSendWithDelegatedProver() { +.return ( +.. +... +.. +.); +}`}, + typescript: { code:`import { +.MidenClient, +.NoteVisibility, +.StorageMode, +.createP2IDNote, +.NoteArray, +} from '@miden-sdk/miden-sdk/lazy'; +import { fundAccountForFees, consumeAllFeeAware } from './feeSupport'; + +export async function multiSendWithDelegatedProver(): Promise { +.// Ensure this runs only in a browser context +.if (typeof window === 'undefined') return console.warn('Run in browser'); + +.// Wait for WASM to be ready before touching any wasm-bindgen type. +.await MidenClient.ready(); + +.const client = await MidenClient.createTestnet(); + +.console.log('Latest block:', (await client.sync()).blockNum()); +}` }, +}} reactFilename="lib/react/multiSendWithDelegatedProver.tsx" tsFilename="lib/multiSendWithDelegatedProver.ts" /> + +## Step 4 — Create an account, deploy a faucet, mint and consume tokens + +Add the code below to the function. The shared helpers imported in Step 3 fund Alice and the faucet with native fee tokens, wait for confirmation and select the tutorial notes before consuming them. The React helpers come from the same `tutorialSupport` file used by the complete example. + + + +## Step 5 — Build and Create P2ID notes + +Add the following code to the function. This code creates three testnet recipients, builds a P2ID note with 100 `MID` for each, and then creates all three notes in the same transaction. + + +..createWallet({ storageMode: StorageMode.Public, authScheme }), +.), +); + +const sent = await sendMany({ +.from: alice, +.assetId: faucet, +.recipients: recipients.map((account) => ({ +..to: account.id().toString(), +..amount: BigInt(100), +.})), +.noteType: NoteVisibility.Public, +}); + +await committed(sent.transactionId); +await assertBalance(alice, faucet, BigInt(9700)); +console.log('All notes created ✅');`}, + typescript: { code:`// ── build 3 P2ID notes (100 MID each) ───────────────────────────────────────────── +const recipients = await Promise.all( +.Array.from({ length: 3 }, () => +..client.accounts.create({ storage: StorageMode.Public }), +.), +); +const recipientAddresses = recipients.map((account) => +.account.id().toString(), +); + +const p2idNotes = recipientAddresses.map((addr) => +.createP2IDNote({ +..from: alice, +..to: addr, +..assets: { token: faucet, amount: BigInt(100) }, +..type: NoteVisibility.Public, +.}), +); + +// ── create all P2ID notes ─────────────────────────────────────────────────────────────── +await client.sync(); +const builder = await client.feeAwareTransactionRequestBuilder(alice); +const outputs = new NoteArray(); +for (const note of p2idNotes) outputs.push(note); +const txRequest = builder.withOwnOutputNotes(outputs).build(); +const { txId } = await client.transactions.submit(alice, txRequest); +await client.transactions.waitFor(txId, { timeout: 120_000 }); + +console.log('All notes created ✅');` }, +}} reactFilename="lib/react/multiSendWithDelegatedProver.tsx" tsFilename="lib/multiSendWithDelegatedProver.ts" /> + +## Summary + +Your library file should now look like this: + + { +..await sync(); +..const authScheme = await tutorialAuthScheme(); +..const alice = await createWallet({ +...storageMode: StorageMode.Public, +...authScheme, +..}); +..console.log('Alice ID:', alice.id().toString()); +..await fundAccount(alice); +..const faucet = await createFaucet({ +...tokenSymbol: 'MID', +...decimals: 8, +...maxSupply: BigInt(1_000_000), +...storageMode: StorageMode.Public, +...authScheme, +..}); +..console.log('Faucet ID:', faucet.id().toString()); +..await fundAccount(faucet); + +..await sync(); +..const minted = await mint({ +...faucetId: faucet, +...targetAccountId: alice, +...amount: BigInt(10_000), +...noteType: NoteVisibility.Public, +..}); +..await committed(minted.transactionId); +..const notes = await waitForTokenNotes(alice, faucet); +..const consumed = await consume({ accountId: alice.id().toString(), notes }); +..await committed(consumed.transactionId); + +..const recipients = []; +..for (let index = 0; index < 3; index += 1) { +...recipients.push( +....await createWallet({ storageMode: StorageMode.Public, authScheme }), +...); +..} +..const sent = await sendMany({ +...from: alice, +...assetId: faucet, +...recipients: recipients.map((account) => ({ +....to: account, +....amount: BigInt(100), +...})), +...noteType: NoteVisibility.Public, +..}); +..await committed(sent.transactionId); +..for (const recipient of recipients) { +...const outputs = await waitForTokenNotes(recipient, faucet); +...if ( +....outputs.length !== 1 || +....outputs[0].details().assets().fungibleAssets()[0]?.amount() !== +.....BigInt(100) +...) { +....throw new Error(\`Expected one 100 MID note for \${recipient.id()}\`); +...} +..} +..await assertBalance(alice, faucet, BigInt(9700)); +..console.log('All notes created ✅'); +.}; + +.return ( +.. +.); +} + +export default function MultiSendWithDelegatedProver() { +.return ( +.. +... +.. +.); +}`}, + typescript: { code: `import { +.NoteArray, +.NoteVisibility, +.StorageMode, +.createP2IDNote, +} from '@miden-sdk/miden-sdk/lazy'; +import { +.consumeAllFeeAware, +.createTutorialClient, +.fundAccountForFees, +} from './feeSupport'; + +export async function multiSendWithDelegatedProver(): Promise { +.// Ensure this runs only in a browser context +.if (typeof window === 'undefined') return console.warn('Run in browser'); + +.const client = await createTutorialClient(); + +.console.log('Latest block:', (await client.sync()).blockNum()); + +.// ── Creating new account ────────────────────────────────────────────────────── +.console.log('Creating account for Alice…'); +.const alice = await client.accounts.create({ +..storage: StorageMode.Public, +.}); +.console.log('Alice account ID:', alice.id().toString()); + +.// ── Creating new faucet ──────────────────────────────────────────────────── +.const faucet = await client.accounts.create({ +..type: 0, // 0 = FungibleFaucet +..symbol: 'MID', +..decimals: 8, +..maxSupply: BigInt(1_000_000), +..storage: StorageMode.Public, +.}); +.console.log('Faucet ID:', faucet.id().toString()); +.await fundAccountForFees(client, alice); +.await fundAccountForFees(client, faucet); + +.// ── mint 10 000 MID to Alice ─────────────────────────────────────────────── +.await client.sync(); +.const { txId: mintTxId } = await client.transactions.mint({ +..account: faucet, +..to: alice, +..amount: BigInt(10_000), +..type: NoteVisibility.Public, +.}); +.console.log('waiting for settlement'); +.await client.transactions.waitFor(mintTxId, { timeout: 120_000 }); +.await consumeAllFeeAware(client, alice); + +.// ── build 3 P2ID notes (100 MID each) ───────────────────────────────────────────── +.const recipients = await Promise.all( +..Array.from({ length: 3 }, () => +...client.accounts.create({ storage: StorageMode.Public }), +..), +.); +.const recipientAddresses = recipients.map((account) => +..account.id().toString(), +.); + +.const p2idNotes = recipientAddresses.map((addr) => +..createP2IDNote({ +...from: alice, +...to: addr, +...assets: { token: faucet, amount: BigInt(100) }, +...type: NoteVisibility.Public, +..}), +.); + +.// ── create all P2ID notes ─────────────────────────────────────────────────────────────── +.await client.sync(); +.const builder = await client.feeAwareTransactionRequestBuilder(alice); +.const outputs = new NoteArray(); +.for (const note of p2idNotes) outputs.push(note); +.const request = builder.withOwnOutputNotes(outputs).build(); +.const { txId } = await client.transactions.submit(alice, request); +.await client.transactions.waitFor(txId, { timeout: 120_000 }); +.console.log(\`Transaction committed: \${txId.toHex()}\`); +.const updatedAlice = await client.accounts.get(alice); +.const balance = updatedAlice?.vault().getBalance(faucet.id()); +.if (balance !== BigInt(9_700)) +..throw new Error(\`Expected Alice to retain 9700 MID, got \${balance}\`); + +.console.log('All notes created ✅'); +}` }, +}} reactFilename="lib/react/multiSendWithDelegatedProver.tsx" tsFilename="lib/multiSendWithDelegatedProver.ts" /> + +### Running the example + +To run a full working example navigate to the `web-client` directory in the [miden-tutorials](https://github.com/0xMiden/miden-tutorials/) repository and run the web application example: + +```bash +cd web-client +yarn install +yarn dev +``` + +### Resetting the `MidenClientDB` + +The Miden webclient stores account and note data in IndexedDB. Stop or terminate the tutorial client and close other tabs using its store before resetting it. This deletes local account data and keys, so use it only for disposable tutorial accounts. The following browser-console snippet deletes the default testnet `MidenClientDB_mtst` store after the deletion request completes; change `name` if you configured a different store. + +```javascript +(async () => { + const name = 'MidenClientDB_mtst'; + await new Promise((resolve, reject) => { + const request = indexedDB.deleteDatabase(name); + request.onsuccess = () => resolve(); + request.onerror = () => reject(request.error); + request.onblocked = () => + reject(new Error('Close clients and tabs using this store, then retry.')); + }); + console.log(`Deleted database: ${name}`); +})(); +``` diff --git a/versioned_docs/version-0.16/builder/tutorials/recipes/web/foreign_procedure_invocation_tutorial.md b/versioned_docs/version-0.16/builder/tutorials/recipes/web/foreign_procedure_invocation_tutorial.md new file mode 100644 index 00000000..e1bf1014 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tutorials/recipes/web/foreign_procedure_invocation_tutorial.md @@ -0,0 +1,723 @@ +--- +title: 'Foreign Procedure Invocation' +sidebar_position: 7 +--- + +# Foreign Procedure Invocation Tutorial + +_Using foreign procedure invocation to craft read-only cross-contract calls with the Miden client_ + +:::note v0.16 setup + +Follow the [network and fee setup](./setup_guide.md#network-and-fee-setup) +and copy the shared support files imported by the complete example. + +::: + +## Overview + +In the previous tutorial we deployed a fresh counter smart contract and incremented its count with a transaction script. + +In this tutorial we will cover the basics of "foreign procedure invocation" (FPI) using the Miden client. This tutorial is self-contained: it deploys its own counter contract from scratch, then builds a "count copy" smart contract, and uses FPI to read the count from the counter contract and copy it to the count reader's local storage. + +Foreign procedure invocation (FPI) is a powerful tool for building composable smart contracts in Miden. FPI allows one smart contract or note to read the state of another contract. + +The term "foreign procedure invocation" might sound a bit verbose, but it is as simple as one smart contract calling a non-state modifying procedure in another smart contract. The "EVM equivalent" of foreign procedure invocation would be a smart contract calling a read-only function in another contract. + +FPI is useful for developing smart contracts that extend the functionality of existing contracts on Miden. FPI is the core primitive used by price oracles on Miden. + +## What We Will Build + +![Count Copy FPI diagram](../img/count_copy_fpi_diagram.png) + +The diagram above depicts the "count copy" smart contract using foreign procedure invocation to read the count state of the counter contract. After reading the state via FPI, the "count copy" smart contract writes the value returned from the counter contract to storage. + +## What we'll cover + +- Foreign Procedure Invocation (FPI) with the Miden client +- Building a "count copy" smart contract +- Executing cross-contract calls in the browser + +## Prerequisites + +- Node `v20` or greater +- Familiarity with TypeScript +- `yarn` + +This tutorial assumes you have a basic understanding of Miden assembly and completed the previous tutorial on incrementing the counter contract. To quickly get up to speed with Miden assembly (MASM), please play around with running basic Miden assembly programs in the [Miden playground](https://0xmiden.github.io/examples/). + +## Step 1: Initialize your Next.js project + +1. Create a new Next.js app with TypeScript: + + ```bash + npx create-next-app@latest miden-fpi-app --typescript + ``` + + Hit enter for all terminal prompts. + +2. Change into the project directory: + + ```bash + cd miden-fpi-app + ``` + +3. Install the Miden SDK: + ```bash + yarn add @miden-sdk/miden-sdk@0.16.0 + ``` + +The current Next.js template uses Turbopack by default. These examples use the webpack configuration from the setup guide, so update both scripts in `package.json`: + +`package.json` + +```json +{ + "scripts": { + "dev": "next dev --webpack", + "build": "next build --webpack" + } +} +``` + +## Step 2: Edit the `app/page.tsx` file + +Add the following code to the `app/page.tsx` file. This code defines the main page of our web application: + +```tsx +'use client'; +import { useState } from 'react'; +import { foreignProcedureInvocation } from '../lib/foreignProcedureInvocation'; + +export default function Home() { + const [isFPIRunning, setIsFPIRunning] = useState(false); + + const handleForeignProcedureInvocation = async () => { + setIsFPIRunning(true); + await foreignProcedureInvocation(); + setIsFPIRunning(false); + }; + + return ( +
+
+

Miden FPI Web App

+

+ Open your browser console to see Miden client logs. +

+ +
+ +
+
+
+ ); +} +``` + +## Step 3: Write the MASM Contract Files + +The MASM (Miden Assembly) code for our smart contracts lives in separate `.masm` files. Create a `lib/masm/` directory and add the two contract files: + +```bash +mkdir -p lib/masm +``` + +### Counter contract + +Create the file `lib/masm/counter_contract.masm`. This is the same counter contract introduced in the previous tutorial; we deploy a fresh instance of it in Step 5 below and also need its source code here so we can compile it locally and obtain the procedure hash for `get_count`: + +```masm +use miden::protocol::active_account +use miden::protocol::native_account +use miden::core::sys + +# CONSTANTS +# ================================================================================================= + +const COUNTER_SLOT = word("miden::tutorials::counter") + +# PUBLIC INTERFACE +# ================================================================================================= + +#! Returns the current count. +#! +#! Inputs: [pad(16)] +#! Outputs: [count, pad(15)] +#! +#! Invocation: call +@account_procedure +pub proc get_count() -> felt + push.COUNTER_SLOT[0..2] exec.active_account::get_item + # => [[count, 0, 0, 0], pad(16)] + + exec.sys::truncate_stack + # => [count, pad(15)] +end + +#! Increments the current count by one. +#! +#! Inputs: [pad(16)] +#! Outputs: [pad(16)] +#! +#! Invocation: call +@account_procedure +pub proc increment_count() + push.COUNTER_SLOT[0..2] exec.active_account::get_item + # => [[count, 0, 0, 0], pad(16)] + + add.1 + # => [[count + 1, 0, 0, 0], pad(16)] + + push.COUNTER_SLOT[0..2] exec.native_account::set_item + # => [OLD_VALUE, pad(16)] + + dropw + # => [pad(16)] + + exec.sys::truncate_stack + # => [pad(16)] +end +``` + +### Count reader contract + +Create the file `lib/masm/count_reader.masm`. This is the new "count copy" contract that reads the counter value via FPI and stores it locally: + +```masm +use miden::protocol::native_account +use miden::protocol::tx +use miden::core::sys +use {AccountId, AccountProcedureRoot} from miden::protocol::types + +# CONSTANTS +# ================================================================================================= + +const COUNT_READER_SLOT = word("miden::tutorials::count_reader") + +# PUBLIC INTERFACE +# ================================================================================================= + +#! Copies the count returned by the foreign counter into this account's storage. +#! +#! Inputs: [foreign_account_id_{suffix,prefix}, FOREIGN_PROC_ROOT, pad(10)] +#! Outputs: [pad(16)] +#! +#! Where: +#! - foreign_account_id_{suffix,prefix} identifies the public counter account. +#! - FOREIGN_PROC_ROOT is the root of its get_count procedure. +#! +#! Invocation: call +@account_procedure +@locals(6) +pub proc copy_count(foreign_account_id: AccountId, foreign_proc_root: AccountProcedureRoot) + # save the foreign target while preparing its sixteen zero inputs + loc_store.4 loc_store.5 loc_storew_le.0 dropw + # => [pad(16)] + + padw padw padw padw + # => [foreign_procedure_inputs(16), pad(16)] + + padw loc_loadw_le.0 loc_load.5 loc_load.4 + # => [foreign_account_id_suffix, foreign_account_id_prefix, FOREIGN_PROC_ROOT, foreign_procedure_inputs(16), pad(16)] + + exec.tx::execute_foreign_procedure + # => [[count, 0, 0, 0], pad(28)] + + push.COUNT_READER_SLOT[0..2] + # => [slot_id_suffix, slot_id_prefix, [count, 0, 0, 0], pad(28)] + + exec.native_account::set_item + # => [OLD_VALUE, pad(28)] + + dropw + # => [pad(28)] + + exec.sys::truncate_stack + # => [pad(16)] +end +``` + +### Type declaration + +Create `lib/masm/masm.d.ts` so TypeScript recognizes `.masm` imports: + +```ts +declare module '*.masm' { + const content: string; + export default content; +} +``` + +## Step 4: Configure Your Bundler to Import `.masm` Files + +We need to tell our bundler to treat `.masm` files as plain text strings. In Next.js, add an `asset/source` webpack rule. + +Open `next.config.ts` and add the highlighted rule inside the `webpack` callback: + +```ts +// Import .masm files as strings. Keep the existing WASM configuration. +config.module.rules.push({ + test: /\.masm$/, + type: "asset/source", +}); +``` + +:::tip Other bundlers + +- **Vite:** use the `?raw` suffix — `import code from './masm/counter_contract.masm?raw'` +- **Other bundlers / no bundler:** use `fetch()` at runtime — `const code = await fetch('/masm/counter_contract.masm').then(r => r.text())` + ::: + +## Step 5: Create the Foreign Procedure Invocation Implementation + +Create the file `lib/foreignProcedureInvocation.ts` and add the following code. + +```bash +touch lib/foreignProcedureInvocation.ts +``` + +Copy and paste the following code into the `lib/foreignProcedureInvocation.ts` file: + +```ts +// lib/foreignProcedureInvocation.ts +import counterContractCode from './masm/counter_contract.masm'; +import countReaderCode from './masm/count_reader.masm'; +import { + AuthSecretKey, + StorageSlot, + StorageResult, +} from '@miden-sdk/miden-sdk/lazy'; +import { + createFundableContractAccount, + createTutorialClient, + fundAccountForFees, +} from './feeSupport'; + +export async function foreignProcedureInvocation(): Promise { + if (typeof window === 'undefined') { + console.warn('foreignProcedureInvocation() can only run in the browser'); + return; + } + + const client = await createTutorialClient({ proverUrl: 'local' }); + console.log('Current block number: ', (await client.sync()).blockNum()); + + const counterSlotName = 'miden::tutorials::counter'; + const countReaderSlotName = 'miden::tutorials::count_reader'; + + // ------------------------------------------------------------------------- + // STEP 1: Deploy the Counter Contract + // ------------------------------------------------------------------------- + console.log('\n[STEP 1] Deploying counter contract.'); + + const counterComponent = await client.compile.component({ + code: counterContractCode, + slots: [StorageSlot.emptyValue(counterSlotName)], + }); + + const counterSeed = new Uint8Array(32); + crypto.getRandomValues(counterSeed); + const counterAuth = AuthSecretKey.rpoFalconWithRNG(counterSeed); + + const counterAccount = await createFundableContractAccount( + client, + counterSeed, + counterAuth, + [counterComponent], + ); + + await fundAccountForFees(client, counterAccount); + + // Deploy the counter to the node by executing a transaction on it + const deployScript = await client.compile.txScript({ + code: ` +use external_contract::counter_contract + +#! Increments the counter. +#! +#! Inputs: [ARGS, pad(12)] +#! Outputs: [pad(16)] +#! +#! Where: +#! - ARGS contains unused transaction script arguments. +#! +#! Invocation: dyncall +@transaction_script +pub proc main(args: word) + dropw + # => [pad(16)] + + call.counter_contract::increment_count + # => [pad(16)] +end +`, + libraries: [ + { + namespace: 'external_contract::counter_contract', + code: counterContractCode, + }, + ], + }); + + // Wait for the deploy transaction to be committed to a block + // before using it as a foreign account in FPI + await client.sync(); + await client.transactions.execute({ + account: counterAccount, + script: deployScript, + waitForConfirmation: true, + timeout: 120_000, + }); + console.log('Counter contract ID:', counterAccount.id().toString()); + + // ------------------------------------------------------------------------- + // STEP 2: Create the Count Reader Contract + // ------------------------------------------------------------------------- + console.log('\n[STEP 2] Creating count reader contract.'); + + const countReaderComponent = await client.compile.component({ + code: countReaderCode, + slots: [StorageSlot.emptyValue(countReaderSlotName)], + }); + + const readerSeed = new Uint8Array(32); + crypto.getRandomValues(readerSeed); + const readerAuth = AuthSecretKey.rpoFalconWithRNG(readerSeed); + + const countReaderAccount = await createFundableContractAccount( + client, + readerSeed, + readerAuth, + [countReaderComponent], + ); + + await fundAccountForFees(client, countReaderAccount); + + console.log('Count reader contract ID:', countReaderAccount.id().toString()); + + // ------------------------------------------------------------------------- + // STEP 3: Call the Counter Contract via Foreign Procedure Invocation (FPI) + // ------------------------------------------------------------------------- + console.log( + '\n[STEP 3] Call counter contract with FPI from count reader contract', + ); + + const getCountProcHash = counterComponent.getProcedureHash('get_count'); + + const fpiScriptCode = ` +use external_contract::count_reader_contract +use miden::core::sys + +#! Copies a public counter through the reader account. +#! +#! Inputs: [ARGS, pad(12)] +#! Outputs: [pad(16)] +#! +#! Where: +#! - ARGS contains unused transaction script arguments. +#! +#! Invocation: dyncall +@transaction_script +pub proc main(args: word) + dropw + # => [pad(16)] + + push.${getCountProcHash} + # => [GET_COUNT_HASH, pad(16)] + + push.${counterAccount.id().prefix()} + # => [account_id_prefix, GET_COUNT_HASH, pad(16)] + + push.${counterAccount.id().suffix()} + # => [account_id_suffix, account_id_prefix, GET_COUNT_HASH, pad(16)] + + call.count_reader_contract::copy_count + # => [pad(16)] + + exec.sys::truncate_stack + # => [pad(16)] +end +`; + + const script = await client.compile.txScript({ + code: fpiScriptCode, + libraries: [ + { + namespace: 'external_contract::count_reader_contract', + code: countReaderCode, + }, + ], + }); + + await client.sync(); + const { txId } = await client.transactions.execute({ + account: countReaderAccount, + script, + foreignAccounts: [counterAccount], + waitForConfirmation: true, + timeout: 120_000, + }); + console.log(`Transaction committed: ${txId.toHex()}`); + + const updatedCountReader = await client.accounts.get(countReaderAccount); + // `getItem()` is typed to return a low-level `Word`, but at runtime the SDK + // wraps the slot in a `StorageResult` whose `toBigInt()` reads the first + // felt — the count. The cast reflects that runtime type. + const countReaderStorage = updatedCountReader + ?.storage() + .getItem(countReaderSlotName) as unknown as StorageResult | undefined; + + if (countReaderStorage) { + const countValue = Number(countReaderStorage.toBigInt()); + if (countValue !== 1) + throw new Error(`Expected copied counter 1, got ${countValue}`); + console.log('Count copied via Foreign Procedure Invocation:', countValue); + } else { + throw new Error('Count reader storage was not available after commitment'); + } + + console.log('\nForeign Procedure Invocation Transaction completed!'); +} +``` + +To run the code above in our frontend, run the following command: + +```bash +yarn dev +``` + +Open the browser console and click the button "Foreign Procedure Invocation Tutorial". + +This is what you should see in the browser console: + +``` +Current block number: 121098 + +[STEP 1] Deploying counter contract. +Counter contract ID: 0xab9cb9598cd6501012de6f8659e2ea + +[STEP 2] Creating count reader contract. +Count reader contract ID: 0x90128b4e27f34500000720bedaa49b + +[STEP 3] Call counter contract with FPI from count reader contract +Count copied via Foreign Procedure Invocation: 1 + +Foreign Procedure Invocation Transaction completed! +``` + +## Understanding the Count Reader Contract + +The count reader smart contract contains a `copy_count` procedure that uses `tx::execute_foreign_procedure` to call the `get_count` procedure in the counter contract. + +```masm +use miden::protocol::native_account +use miden::protocol::tx +use miden::core::sys +use {AccountId, AccountProcedureRoot} from miden::protocol::types + +# CONSTANTS +# ================================================================================================= + +const COUNT_READER_SLOT = word("miden::tutorials::count_reader") + +# PUBLIC INTERFACE +# ================================================================================================= + +#! Copies the count returned by the foreign counter into this account's storage. +#! +#! Inputs: [foreign_account_id_{suffix,prefix}, FOREIGN_PROC_ROOT, pad(10)] +#! Outputs: [pad(16)] +#! +#! Where: +#! - foreign_account_id_{suffix,prefix} identifies the public counter account. +#! - FOREIGN_PROC_ROOT is the root of its get_count procedure. +#! +#! Invocation: call +@account_procedure +@locals(6) +pub proc copy_count(foreign_account_id: AccountId, foreign_proc_root: AccountProcedureRoot) + # save the foreign target while preparing its sixteen zero inputs + loc_store.4 loc_store.5 loc_storew_le.0 dropw + # => [pad(16)] + + padw padw padw padw + # => [foreign_procedure_inputs(16), pad(16)] + + padw loc_loadw_le.0 loc_load.5 loc_load.4 + # => [foreign_account_id_suffix, foreign_account_id_prefix, FOREIGN_PROC_ROOT, foreign_procedure_inputs(16), pad(16)] + + exec.tx::execute_foreign_procedure + # => [[count, 0, 0, 0], pad(28)] + + push.COUNT_READER_SLOT[0..2] + # => [slot_id_suffix, slot_id_prefix, [count, 0, 0, 0], pad(28)] + + exec.native_account::set_item + # => [OLD_VALUE, pad(28)] + + dropw + # => [pad(28)] + + exec.sys::truncate_stack + # => [pad(16)] +end +``` + +To call the `get_count` procedure, we push its hash along with the counter contract's ID suffix and prefix onto the stack before calling `tx::execute_foreign_procedure`. + +The stack state before calling `tx::execute_foreign_procedure` should look like this: + +``` +# => [foreign_account_id_suffix, foreign_account_id_prefix, FOREIGN_PROC_ROOT, foreign_procedure_inputs(16), pad(16)] +``` + +`execute_foreign_procedure` always requires exactly 16 `foreign_procedure_inputs` on the stack below the procedure hash and account ID. Since `get_count` takes no arguments, `copy_count` prepares 16 zero field elements (four words: `padw padw padw padw`) as the inputs. The transaction script only passes the account ID and procedure root; the reader saves them in local memory while preparing those inputs. + +After calling the `get_count` procedure in the counter contract, we save the count into the +`miden::tutorials::count_reader` storage slot. + +## Understanding the Transaction Script + +The transaction script that executes the foreign procedure invocation looks like this: + +```masm +use external_contract::count_reader_contract +use miden::core::sys + +#! Copies a public counter through the reader account. +#! +#! Inputs: [ARGS, pad(12)] +#! Outputs: [pad(16)] +#! +#! Where: +#! - ARGS contains unused transaction script arguments. +#! +#! Invocation: dyncall +@transaction_script +pub proc main(args: word) + dropw + # => [pad(16)] + + push.${getCountProcHash} + # => [GET_COUNT_HASH, pad(16)] + + push.${counterAccount.id().prefix()} + # => [account_id_prefix, GET_COUNT_HASH, pad(16)] + + push.${counterAccount.id().suffix()} + # => [account_id_suffix, account_id_prefix, GET_COUNT_HASH, pad(16)] + + call.count_reader_contract::copy_count + # => [pad(16)] + + exec.sys::truncate_stack + # => [pad(16)] +end +``` + +This script: + +1. Discards the unused transaction script arguments. +2. Pushes the procedure root of `get_count`. +3. Pushes the counter account ID prefix, then suffix, leaving the suffix on top. +4. Calls `copy_count`, which prepares the foreign call and stores the result. +5. Truncates the stack. + +## Key Miden Client Concepts for FPI + +### Getting Procedure Hashes + +Compile the counter contract component using `client.compile.component()` and call `getProcedureHash()` to obtain the hash needed by the FPI script: + +```ts +const counterComponent = await client.compile.component({ + code: counterContractCode, + slots: [StorageSlot.emptyValue(counterSlotName)], +}); + +const getCountProcHash = counterComponent.getProcedureHash('get_count'); +``` + +### Compiling the Transaction Script with a Library + +Use `client.compile.txScript()` and pass the count reader library inline. The library is linked dynamically so the script can call its procedures: + +```ts +const script = await client.compile.txScript({ + code: fpiScriptCode, + libraries: [ + { + namespace: 'external_contract::count_reader_contract', + code: countReaderCode, + }, + ], +}); +``` + +### Foreign Accounts + +Pass the foreign account directly in the `execute()` call using the `foreignAccounts` option. The client creates the `ForeignAccount` and `AccountStorageRequirements` internally — no manual construction needed: + +```ts +await client.transactions.execute({ + account: countReaderAccount, + script, + foreignAccounts: [counterAccount], + waitForConfirmation: true, + timeout: 120_000, +}); +``` + +## Summary + +In this tutorial we created a smart contract that calls the `get_count` procedure in the counter contract using foreign procedure invocation, and then saves the returned value to its local storage using the Miden client. + +The key steps were: + +1. Writing the MASM contract files (`counter_contract.masm` and `count_reader.masm`) +2. Configuring the bundler to import `.masm` files as strings +3. Creating a count reader contract with a `copy_count` procedure +4. Deploying the counter contract on-chain +5. Getting the procedure hash for the `get_count` function +6. Building a transaction script that calls our count reader contract +7. Executing the transaction with a foreign account reference + +### Running the example + +To run a full working example navigate to the `web-client` directory in the [miden-tutorials](https://github.com/0xMiden/miden-tutorials/) repository and run the web application example: + +```bash +cd web-client +yarn install +yarn dev +``` + +### Resetting the `MidenClientDB` + +The Miden webclient stores account and note data in IndexedDB. Stop or terminate the tutorial client and close other tabs using its store before resetting it. This deletes local account data and keys, so use it only for disposable tutorial accounts. The following browser-console snippet deletes the default testnet `MidenClientDB_mtst` store after the deletion request completes; change `name` if you configured a different store. + +```javascript +(async () => { + const name = 'MidenClientDB_mtst'; + await new Promise((resolve, reject) => { + const request = indexedDB.deleteDatabase(name); + request.onsuccess = () => resolve(); + request.onerror = () => reject(request.error); + request.onblocked = () => + reject(new Error('Close clients and tabs using this store, then retry.')); + }); + console.log(`Deleted database: ${name}`); +})(); +``` + +### Continue learning + +Next tutorial: [Creating Multiple Notes](creating_multiple_notes_tutorial.md) diff --git a/versioned_docs/version-0.16/builder/tutorials/recipes/web/index.md b/versioned_docs/version-0.16/builder/tutorials/recipes/web/index.md new file mode 100644 index 00000000..9d16d5ff --- /dev/null +++ b/versioned_docs/version-0.16/builder/tutorials/recipes/web/index.md @@ -0,0 +1,19 @@ +--- +title: 'Web Client' +sidebar_position: 1 +--- + +TypeScript library, which can be used to programmatically interact with the Miden rollup. + +The Miden client can be used for a variety of things, including: + +- Deploying and creating transactions to interact with accounts and notes on Miden. +- Storing the state of accounts and notes in the browser. +- Generating and submitting proofs of transactions. +- Submitting transactions to delegated proving services. + +This section of the docs is an overview of the different things one can achieve using the Miden client, and how to implement them. + +Before starting any web tutorial, see the [Web Client Setup Guide](./setup_guide.md) for required Next.js configuration, Node.js polyfills, and SDK API patterns. + +Keep in mind that both the Miden client and the documentation are works-in-progress! diff --git a/versioned_docs/version-0.16/builder/tutorials/recipes/web/mint_consume_create_tutorial.md b/versioned_docs/version-0.16/builder/tutorials/recipes/web/mint_consume_create_tutorial.md new file mode 100644 index 00000000..0d048e3b --- /dev/null +++ b/versioned_docs/version-0.16/builder/tutorials/recipes/web/mint_consume_create_tutorial.md @@ -0,0 +1,406 @@ +--- +title: 'Mint, Consume, and Create Notes' +sidebar_position: 3 +--- + +import { CodeSdkTabs } from '@site/src/components'; + +_Using the Miden client in TypeScript to mint, consume, and transfer assets_ + +:::note v0.16 setup + +Follow the [network and fee setup](./setup_guide.md#network-and-fee-setup) +and copy the shared support files imported by the complete example. +For React snippets, initialize `authScheme` with `await tutorialAuthScheme()` +as shown in the complete example. + +::: + +## Overview + +In the previous tutorial, we set up the foundation - creating Alice's wallet and deploying a faucet. Now we'll put these to use by minting and transferring assets. + +## What we'll cover + +- Minting assets from a faucet +- Consuming notes to fund an account +- Sending tokens to other users + +## Prerequisites + +This tutorial builds directly on the previous one. Make sure you have: + +- Completed the "Creating Accounts and Deploying Faucets" tutorial +- Your Next.js app with the Miden client set up + +## Understanding Notes in Miden + +Before we start coding, it's important to understand **notes**: + +- Minting a note from a faucet does not automatically add the tokens to your account balance. It creates a note addressed to you. +- You must **consume** a note to add its tokens to your account balance. +- Until consumed, tokens exist in the note but aren't in your account yet. + +## Step 1: Mint tokens from the faucet + +Let's mint some tokens for Alice. When we mint from a faucet, it creates a note containing the specified amount of tokens targeted to Alice's account. + +Add the operations below after funding Alice and the faucet in `createMintConsume`. Import `NoteVisibility` and the shared support functions shown in the complete example. For React, also initialize `useMint`, `useConsume` and `useSend`, and obtain `committed`, `waitForTokenNotes` and `assertBalance` from `useTutorialSupport`. + + + +### What's happening here? + +1. **client.transactions.mint()**: Creates, proves, and submits a mint transaction to Alice. Note that this is only possible to submit transactions on the faucets' behalf if the user controls the faucet (i.e. its keys are stored in the client). +2. **client.transactions.waitFor()**: Polls until the transaction is committed on-chain. + +## Step 2: Consume minted notes + +After minting, Alice has a note waiting for her but the tokens aren't in her account yet. We need to consume the note to add its assets to her account balance. + +Select the tutorial notes before consuming them: v0.16 also exposes globally consumable `TX_FEE` notes. The shared `consumeAllFeeAware` TypeScript helper excludes those fee notes. In React, `waitForTokenNotes` selects committed notes containing our faucet’s token and returns the `InputNoteRecord` values accepted by `useConsume`. + + + +## Step 3: Sending tokens to other accounts + +After consuming the notes, Alice has tokens in her wallet. Now, she wants to send tokens to her friends. She has two options: create a separate transaction for each transfer or batch multiple notes in a single transaction. + +_The standard asset transfer note on Miden is the P2ID note (Pay-to-Id). There is also the P2IDE (Pay-to-Id Extended) variant which allows for both timelocking the note (target can only spend the note after a certain block height) and for the note to be reclaimable (the creator of the note can reclaim the note after a certain block height)._ + +Now that Alice has tokens in her account, she can send some to Bob: + + + +### Understanding P2ID notes + +The transaction creates a **P2ID (Pay-to-ID)** note: + +- It's the standard way to transfer assets in Miden +- The note is "locked" to Bob's account ID, i.e. only Bob can consume this note to receive the tokens +- Public notes are visible onchain; private notes would need to be shared offchain (e.g. via a private channel) + +## Summary + +Here's the complete `lib/react/createMintConsume.tsx` (React) or `lib/createMintConsume.ts` (TypeScript): + + { +..console.log('Synchronizing before creating accounts…'); +..await sync(); +..console.log('Creating Alice with useCreateWallet…'); +..const authScheme = await tutorialAuthScheme(); +..// Native fee tokens and the tutorial's MID token are separate assets. +..const alice = await createWallet({ +...storageMode: StorageMode.Public, +...authScheme, +..}); +..console.log('Alice ID:', alice.id().toString()); +..await fundAccount(alice); + +..// v0.16 faucets include BasicWallet, so they can receive fee funding. +..const faucet = await createFaucet({ +...tokenSymbol: 'MID', +...decimals: 8, +...maxSupply: BigInt(1_000_000), +...storageMode: StorageMode.Public, +...authScheme, +..}); +..console.log('Faucet ID:', faucet.id().toString()); +..await fundAccount(faucet); + +..await sync(); +..const minted = await mint({ +...faucetId: faucet, +...targetAccountId: alice, +...amount: BigInt(1000), +...noteType: NoteVisibility.Public, +..}); +..await committed(minted.transactionId); +..const notes = await waitForTokenNotes(alice, faucet); +..const consumed = await consume({ accountId: alice.id().toString(), notes }); +..await committed(consumed.transactionId); +..await assertBalance(alice, faucet, BigInt(1000)); + +..const bob = await createWallet({ +...storageMode: StorageMode.Public, +...authScheme, +..}); +..const sent = await send({ +...from: alice, +...to: bob, +...assetId: faucet, +...amount: BigInt(100), +...noteType: NoteVisibility.Public, +...returnNote: true, +..}); +..await committed(sent.txId); +..if (!sent.note) throw new Error('Send did not return its output note'); +..await waitForNote(sent.note.id().toString()); +..await assertBalance(alice, faucet, BigInt(900)); +..console.log('Tokens sent successfully!'); +.}; + +.return ( +.. +.); +} + +export default function CreateMintConsume() { +.return ( +.. +... +.. +.); +}`}, + typescript: { code: `// lib/createMintConsume.ts +import { NoteVisibility, StorageMode } from '@miden-sdk/miden-sdk/lazy'; +import { +.consumeAllFeeAware, +.createTutorialClient, +.fundAccountForFees, +} from './feeSupport'; + +export async function createMintConsume(): Promise { +.if (typeof window === 'undefined') { +..console.warn('webClient() can only run in the browser'); +..return; +.} + +.const client = await createTutorialClient({ +..proverUrl: 'local', +.}); + +.// 1. Sync with the latest blockchain state +.const state = await client.sync(); +.console.log('Latest block number:', state.blockNum()); + +.// 2. Create Alice's account +.console.log('Creating account for Alice…'); +.const alice = await client.accounts.create({ +..storage: StorageMode.Public, +.}); +.console.log('Alice ID:', alice.id().toString()); + +.// 3. Create our own fungible faucet. SDK v0.16 includes BasicWallet, +.// allowing both accounts to consume native fee funding before minting MID. +.console.log('Creating faucet…'); +.const faucet = await client.accounts.create({ +..type: 0, // 0 = FungibleFaucet +..symbol: 'MID', +..decimals: 8, +..maxSupply: BigInt(1_000_000), +..storage: StorageMode.Public, +.}); +.console.log('Faucet ID:', faucet.id().toString()); +.await fundAccountForFees(client, alice); +.await fundAccountForFees(client, faucet); + +.// 4. Mint tokens to Alice. +.console.log('Minting tokens to Alice...'); +.await client.sync(); +.const { txId: mintTxId } = await client.transactions.mint({ +..account: faucet, +..to: alice, +..amount: BigInt(1000), +..type: NoteVisibility.Public, +.}); +.console.log('Waiting for transaction confirmation...'); +.await client.transactions.waitFor(mintTxId, { timeout: 120_000 }); + +.// 5-6. Consume all available notes for Alice. +.console.log('Consuming minted notes...'); +.await consumeAllFeeAware(client, alice); + +.console.log('Notes consumed.'); + +.// 7. Send tokens to Bob +.const bob = await client.accounts.create({ +..storage: StorageMode.Public, +.}); +.console.log("Sending tokens to Bob's account..."); +.await client.sync(); +.const { txId: sendTxId } = await client.transactions.send({ +..account: alice, +..to: bob, +..token: faucet, +..amount: BigInt(100), +..type: NoteVisibility.Public, +..waitForConfirmation: true, +..timeout: 120_000, +.}); +.console.log(\`Transaction committed: \${sendTxId.toHex()}\`); +.const updatedAlice = await client.accounts.get(alice); +.const balance = updatedAlice?.vault().getBalance(faucet.id()); +.if (balance !== BigInt(900)) +..throw new Error(\`Expected Alice to retain 900 MID, got \${balance}\`); +.console.log('Tokens sent successfully!'); +}` }, +}} reactFilename="lib/react/createMintConsume.tsx" tsFilename="lib/createMintConsume.ts" /> + +Let's run the function again. Reload the page and click "Start". + +The output will look like this (account IDs and block number vary with live +testnet state): + +``` +Latest block number: +Creating account for Alice… +Alice ID: +Creating faucet… +Faucet ID: +Minting tokens to Alice... +Waiting for transaction confirmation... +Consuming minted notes... +Notes consumed. +Sending tokens to Bob's account... +Tokens sent successfully! +``` + +### Resetting the `MidenClientDB` + +The Miden webclient stores account and note data in IndexedDB. Stop or terminate the tutorial client and close other tabs using its store before resetting it. This deletes local account data and keys, so use it only for disposable tutorial accounts. The following browser-console snippet deletes the default testnet `MidenClientDB_mtst` store after the deletion request completes; change `name` if you configured a different store. + +```javascript +(async () => { + const name = 'MidenClientDB_mtst'; + await new Promise((resolve, reject) => { + const request = indexedDB.deleteDatabase(name); + request.onsuccess = () => resolve(); + request.onerror = () => reject(request.error); + request.onblocked = () => + reject(new Error('Close clients and tabs using this store, then retry.')); + }); + console.log(`Deleted database: ${name}`); +})(); +``` + +## What's next? + +You've now learned the complete note lifecycle in Miden: + +1. **Minting** - Creating new tokens from a faucet (issued in notes) +2. **Consuming** - Adding tokens from notes to an account +3. **Transferring** - Sending tokens to other accounts + +In the next tutorials, we'll explore: + +- Creating multiple notes in a single transaction +- Delegated proving diff --git a/versioned_docs/version-0.16/builder/tutorials/recipes/web/react_wallet_tutorial.md b/versioned_docs/version-0.16/builder/tutorials/recipes/web/react_wallet_tutorial.md new file mode 100644 index 00000000..f288edfa --- /dev/null +++ b/versioned_docs/version-0.16/builder/tutorials/recipes/web/react_wallet_tutorial.md @@ -0,0 +1,1087 @@ +--- +title: 'Building a React Wallet' +sidebar_position: 8 +--- + +# Building a React Wallet + +_Using the Miden React SDK to build a complete wallet UI with account management, token transfers, and note claiming_ + +## Overview + +In this tutorial we will build a complete wallet application using the `@miden-sdk/react` package. The Miden React SDK provides a set of hooks and utilities that make it easy to integrate Miden functionality into React applications. + +By the end of this tutorial, you will have a working wallet that can: + +- Create new accounts +- Display account balances +- List and claim unclaimed notes +- Send tokens to other accounts + +## What we'll cover + +- Setting up a React project with the Miden React SDK +- Using the `MidenProvider` to configure the client +- Managing accounts with `useAccounts`, `useAccount`, and `useCreateWallet` +- Displaying and claiming notes with `useNotes` and `useConsume` +- Sending tokens with `useSend` +- Formatting utilities for assets and notes +- External signer integration patterns + +## Prerequisites + +- Node `v20` or greater +- Familiarity with React and TypeScript +- `yarn` + +:::note v0.16 testnet and fees + +A new wallet needs native fee tokens before sending assets. Request and claim a +funding note from the testnet faucet, as described in the +[fee setup](./setup_guide.md#network-and-fee-setup). Claim the returned +note ID and wait for confirmation before sending. + +Select testnet in both the client and any external wallet adapter. + +::: + +--- + +## Step 1: Project Setup and MidenProvider + +First, create a new Vite + React project and install the Miden React SDK. + +1. Create a new Vite project with React and TypeScript: + + ```bash + npx create-vite@latest miden-wallet --template react-ts + cd miden-wallet + ``` + +2. Install the Miden React SDK: + + ```bash + yarn add @miden-sdk/miden-sdk@0.16.0 @miden-sdk/react@0.16.0 + ``` + +3. Install the Vite integration and update `vite.config.ts`. The Miden plugin v0.16 supports Vite 5 and 6, so pin Vite 6 and its compatible React plugin even if the generator installed newer versions: + + ```bash + yarn add -D vite@^6 @vitejs/plugin-react@^4 @miden-sdk/vite-plugin@0.16.0 vite-plugin-wasm vite-plugin-top-level-await + ``` + + ```ts + import { defineConfig } from 'vite'; + import react from '@vitejs/plugin-react'; + import { midenVitePlugin } from '@miden-sdk/vite-plugin'; + import wasm from 'vite-plugin-wasm'; + import topLevelAwait from 'vite-plugin-top-level-await'; + + export default defineConfig({ + plugins: [react(), midenVitePlugin({ crossOriginIsolation: false }), wasm(), topLevelAwait()], + worker: { format: 'es', plugins: () => [wasm(), topLevelAwait()] }, + }); + ``` + + This Vite tutorial uses the main SDK entries throughout. Para and Turnkey import + `SignerContext` from `@miden-sdk/react`; mixing it with `/lazy` creates separate + contexts in v0.16. Keep providers and hooks on the same entry. + +4. Configure the `MidenProvider` in your `main.tsx` file. The provider initializes the Miden client and makes it available to all child components: + +```tsx +// main.tsx +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { MidenProvider } from '@miden-sdk/react'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + + + , +); +``` + +The `MidenProvider` accepts a `config` object with the following options: + +- `rpcUrl`: The RPC endpoint to connect to (`"testnet"`, `"devnet"`, or a custom URL) +- `prover`: The prover to use (`"testnet"` for testnet delegated proving, or `"local"` for local proving) + +--- + +## Step 2: App Shell with useMiden + +The `useMiden()` hook provides access to the client's initialization state. Use it to show loading and error states while the client initializes. + +```tsx +// App.tsx +import { useMiden } from '@miden-sdk/react'; + +export default function App() { + const { isReady, error } = useMiden(); + + if (error) return
Error: {error.message}
; + if (!isReady) return
Initializing...
; + + return
Wallet ready!
; +} +``` + +The `useMiden()` hook returns: + +- `isReady`: `true` when the client has finished initializing +- `error`: An error object if initialization failed + +--- + +## Step 3: Listing Accounts with useAccounts + +The `useAccounts()` hook provides access to all accounts stored in the client. Use it to check if the user has any existing wallets. + +```tsx +import { useMiden, useAccounts } from '@miden-sdk/react'; + +export default function App() { + const { isReady, error } = useMiden(); + const { wallets, isLoading } = useAccounts(); + + if (error) return
Error: {error.message}
; + if (!isReady || isLoading) return
Loading...
; + + const accountId = wallets[0]?.id().toString(); + + if (!accountId) { + return
No wallet found. Create one!
; + } + + return
Account: {accountId}
; +} +``` + +The `useAccounts()` hook returns: + +- `wallets`: Array of wallet accounts +- `faucets`: Array of faucet accounts +- `isLoading`: `true` while accounts are being fetched + +--- + +## Step 4: Creating a Wallet with useCreateWallet + +The `useCreateWallet()` hook provides a function to create new wallet accounts. + +```tsx +import { useMiden, useAccounts, useCreateWallet } from '@miden-sdk/react'; +import { getWasmOrThrow } from '@miden-sdk/miden-sdk'; + +export default function App() { + const { isReady, error } = useMiden(); + const { wallets, isLoading } = useAccounts(); + const { createWallet, isCreating } = useCreateWallet(); + + if (error) return
Error: {error.message}
; + if (!isReady || isLoading) return
Loading...
; + + const accountId = wallets[0]?.id().toString(); + + if (!accountId) { + return ( +
+

Wallet

+ +
+ ); + } + + return ; +} + +function Wallet({ accountId }: { accountId: string }) { + return
Wallet: {accountId}
; +} +``` + +Pass the low-level Falcon enum explicitly with the pinned lazy React SDK. The +high-level client's authentication enum is not interchangeable with the numeric +enum expected by this hook. + +The `useCreateWallet()` hook returns: + +- `createWallet(options?)`: Function to create a new wallet +- `isCreating`: `true` while a wallet is being created + +--- + +## Step 5: Displaying Account Details with useAccount + +The `useAccount(accountId)` hook provides detailed information about a specific account, including its assets and balances. + +```tsx +import { useAccount, formatAssetAmount } from '@miden-sdk/react'; + +function Wallet({ accountId }: { accountId: string }) { + const { account, assets } = useAccount(accountId); + + return ( +
+

Wallet

+ +
+

Address

+
{account?.bech32id?.() ?? 'Loading...'}
+
+ +
+

Balances

+ {assets.length === 0 ? ( +
No assets
+ ) : ( +
    + {assets.map((asset) => ( +
  • + {asset.symbol ?? asset.assetId} + {formatAssetAmount(asset.amount, asset.decimals)} +
  • + ))} +
+ )} +
+
+ ); +} +``` + +The `useAccount(accountId)` hook returns: + +- `account`: The account object with methods like `bech32id()` +- `assets`: Array of asset objects with `assetId`, `symbol`, `amount`, and `decimals` +- `isLoading`: `true` while account data is being fetched + +The `formatAssetAmount(amount, decimals)` utility formats a raw amount with the correct decimal places. + +--- + +## Step 6: Listing Unclaimed Notes with useNotes + +The `useNotes({ accountId })` hook provides access to notes that can be consumed by the account. + +```tsx +import { useNotes, formatNoteSummary } from '@miden-sdk/react'; + +function UnclaimedNotes({ accountId }: { accountId: string }) { + const { consumableNoteSummaries } = useNotes({ accountId }); + + return ( +
+

Unclaimed Notes

+ {consumableNoteSummaries.length === 0 ? ( +
No unclaimed notes
+ ) : ( +
    + {consumableNoteSummaries.map((summary) => ( +
  • {formatNoteSummary(summary)}
  • + ))} +
+ )} +
+ ); +} +``` + +The `useNotes({ accountId })` hook returns: + +- `consumableNoteSummaries`: Array of note summaries that can be consumed +- `isLoading`: `true` while notes are being fetched + +The `formatNoteSummary(summary)` utility formats a note summary for display. + +--- + +## Step 7: Claiming Notes with useConsume + +The `useConsume()` hook provides a function to consume (claim) notes and add their assets to the account. + +```tsx +import { useConsume, formatNoteSummary, type NoteSummary } from '@miden-sdk/react'; + +function UnclaimedNotes({ + accountId, + consumableNoteSummaries, +}: { + accountId: string; + consumableNoteSummaries: NoteSummary[]; +}) { + const { consume, isLoading: isConsuming } = useConsume(); + + const claimNote = (id: string) => () => { + consume({ accountId, notes: [id] }); + }; + + return ( +
+

Unclaimed Notes

+ {consumableNoteSummaries.length === 0 ? ( +
No unclaimed notes
+ ) : ( +
    + {consumableNoteSummaries.map((summary) => ( +
  • + {formatNoteSummary(summary)} + +
  • + ))} +
+ )} +
+ ); +} +``` + +The `useConsume()` hook returns: + +- `consume({ accountId, notes })`: Function to consume one or more notes +- `isLoading`: `true` while notes are being consumed + +--- + +## Step 8: Sending Tokens with useSend + +The `useSend()` hook provides a function to send tokens to other accounts. Select the first asset when balances arrive asynchronously, as shown below, so a newly funded wallet can use the form. + +```tsx +import { useEffect, useState, type ChangeEvent } from 'react'; +import { useSend, parseAssetAmount } from '@miden-sdk/react'; +import { NoteVisibility } from '@miden-sdk/miden-sdk'; + +function SendForm({ + accountId, + assets, +}: { + accountId: string; + assets: Array<{ assetId: string; symbol?: string; decimals?: number }>; +}) { + const { send, isLoading: isSending } = useSend(); + const [to, setTo] = useState(''); + const [assetId, setAssetId] = useState(''); + const defaultAssetId = assets[0]?.assetId; + + useEffect(() => { + if (!assetId && defaultAssetId) setAssetId(defaultAssetId); + }, [assetId, defaultAssetId]); + const [amount, setAmount] = useState(''); + const [noteType, setNoteType] = useState( + NoteVisibility.Private, + ); + + const selectedAsset = assets.find((asset) => asset.assetId === assetId); + const selectedDecimals = selectedAsset?.decimals; + const hasAssets = assets.length > 0; + const canSend = Boolean(hasAssets && to && assetId && amount); + + const handleSend = async () => { + try { + if (!assetId) return; + const amt = parseAssetAmount(amount, selectedDecimals); + await send({ from: accountId, to, assetId, amount: amt, noteType }); + setAmount(''); + } catch (error) { + console.error(error); + } + }; + + const onAssetChange = (e: ChangeEvent) => + setAssetId(e.target.value); + const onNoteTypeChange = (e: ChangeEvent) => + setNoteType(e.target.value as 'private' | 'public'); + const onToChange = (e: ChangeEvent) => + setTo(e.target.value); + const onAmountChange = (e: ChangeEvent) => + setAmount(e.target.value); + + return ( +
+

Send

+ + + + + +
+ ); +} +``` + +The `useSend()` hook returns: + +- `send({ from, to, assetId, amount, noteType })`: Function to send tokens +- `isLoading`: `true` while the transaction is being processed + +Parameters: + +- `from`: The sender's account ID +- `to`: The recipient's address (bech32 format) +- `assetId`: The asset/faucet ID to send +- `amount`: The amount to send (as a BigInt) +- `noteType`: Either `"private"` or `"public"` + +The `parseAssetAmount(amount, decimals)` utility converts a string amount to a BigInt with the correct decimal places. + +--- + +## Summary: Complete Code + +Here is the complete wallet application combining all the features we've covered. + +**main.tsx** + +```tsx +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { MidenProvider } from '@miden-sdk/react'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + + + , +); +``` + +**App.tsx** + +```tsx +import { useEffect, useState, type ChangeEvent, type ReactNode } from 'react'; +import { + formatAssetAmount, + formatNoteSummary, + parseAssetAmount, +} from '@miden-sdk/react'; +import { + useMiden, + useAccounts, + useAccount, + useNotes, + useCreateWallet, + useConsume, + useSend, +} from '@miden-sdk/react'; +import { NoteVisibility, getWasmOrThrow } from '@miden-sdk/miden-sdk'; + +const Panel = ({ title, children }: { title: string; children: ReactNode }) => ( +
+
{title}
+ {children} +
+); + +export default function App() { + const { isReady, error } = useMiden(); + const { wallets, isLoading } = useAccounts(); + const { createWallet, isCreating } = useCreateWallet(); + const handleCreate = async () => createWallet({ + authScheme: (await getWasmOrThrow()).AuthScheme.AuthRpoFalcon512, + }); + const createLabel = isCreating ? 'Creating...' : 'Create wallet'; + + if (error) return
Error: {error.message}
; + if (!isReady || isLoading) + return ( +
+ {!isReady ? 'Initializing...' : 'Loading...'} +
+ ); + + const accountId = wallets[0]?.id().toString(); + if (!accountId) + return ( +
+

Wallet

+ +
+ ); + + return ; +} + +function Wallet({ accountId }: { accountId: string }) { + const { account, assets } = useAccount(accountId); + const { consumableNoteSummaries } = useNotes({ accountId }); + const { consume, isLoading: isConsuming } = useConsume(); + const { send, isLoading: isSending } = useSend(); + const [to, setTo] = useState(''); + const [assetId, setAssetId] = useState(''); + const [amount, setAmount] = useState(''); + const [noteType, setNoteType] = useState( + NoteVisibility.Private, + ); + const defaultAssetId = assets[0]?.assetId; + const selectedAsset = assets.find((asset) => asset.assetId === assetId); + const selectedDecimals = selectedAsset?.decimals; + const hasAssets = assets.length > 0; + + useEffect(() => { + if (!assetId && defaultAssetId) setAssetId(defaultAssetId); + }, [assetId, defaultAssetId]); + + const handleSend = async () => { + try { + if (!assetId) return; + const amt = parseAssetAmount(amount, selectedDecimals); + await send({ from: accountId, to, assetId, amount: amt, noteType }); + setAmount(''); + } catch (error) { + console.error(error); + } + }; + + const claimNote = (id: string) => () => consume({ accountId, notes: [id] }); + const onAssetChange = (event: ChangeEvent) => + setAssetId(event.target.value); + const onNoteTypeChange = (event: ChangeEvent) => + setNoteType(event.target.value as 'private' | 'public'); + const onToChange = (event: ChangeEvent) => + setTo(event.target.value); + const onAmountChange = (event: ChangeEvent) => + setAmount(event.target.value); + const canSend = Boolean(hasAssets && to && assetId && amount); + const sendLabel = isSending ? 'Sending...' : 'Send'; + + return ( +
+

Wallet

+ +
{account?.bech32id?.() ?? 'Loading...'}
+
+ + {assets.length === 0 ? ( +
None
+ ) : ( +
+ {assets.map((asset) => ( +
+ {asset.symbol ?? asset.assetId} + {formatAssetAmount(asset.amount, asset.decimals)} +
+ ))} +
+ )} +
+ + {consumableNoteSummaries.length === 0 ? ( +
None
+ ) : ( +
+ {consumableNoteSummaries.map((summary) => { + const id = summary.id; + const label = formatNoteSummary(summary); + return ( +
+ {label} + +
+ ); + })} +
+ )} +
+ +
+ + + + + +
+
+
+ ); +} +``` + +--- + +## Running the Example + +The complete upstream wallet example lives in +[`packages/react-sdk/examples/wallet` in the web-sdk v0.16.0 release](https://github.com/0xMiden/web-sdk/tree/v0.16.0/packages/react-sdk/examples/wallet). +Follow that example's README for its workspace setup and select testnet in its +provider configuration. + +To exercise this repository's three React transaction examples on testnet: + +```bash +cd web-client +yarn install +yarn dev +``` + +Open `/react-tutorials` and select an example. These examples create and fund their +own accounts; the wallet UI above starts with an empty wallet that needs funding. + +### Resetting the MidenClientDB + +The Miden client stores account and note data in the browser's IndexedDB. +Upgrading from v0.15 automatically recreates the Miden store; export private +notes and any other local data you need before upgrading. To manually reset only +Miden databases on the current origin, stop the current client, close other tabs using it, and run: + +```javascript +(async () => { + const dbs = await indexedDB.databases(); + for (const db of dbs) { + if (!db.name?.startsWith('MidenClientDB')) continue; + await new Promise((resolve, reject) => { + const request = indexedDB.deleteDatabase(db.name); + request.onsuccess = () => resolve(); + request.onerror = () => reject(request.error); + request.onblocked = () => reject(new Error('Close other Miden tabs and retry')); + }); + console.log(`Deleted Miden database: ${db.name}`); + } +})(); +``` + +--- + +## External Signer Integration + +By default, the Miden React SDK manages keys internally using the browser's IndexedDB. However, for production applications you may want to integrate with external signers that provide enhanced security, key management, or authentication features. + +### The useSigner Hook + +The `useSigner()` hook from `@miden-sdk/react` provides a unified interface for interacting with any signer provider. When you wrap your app with a signer provider (Para, Turnkey, MidenFi, etc.), the hook returns the signer context with connection state and methods. + +```tsx +import { useSigner } from '@miden-sdk/react'; + +function ConnectButton() { + const signer = useSigner(); + + // Returns null if no signer provider is present (local keystore mode) + if (!signer) return null; + + const { isConnected, connect, disconnect, name } = signer; + + return isConnected ? ( + + ) : ( + + ); +} +``` + +The `useSigner()` hook returns: + +- `isConnected`: Whether the signer is connected and ready +- `connect()`: Triggers the authentication flow +- `disconnect()`: Disconnects from the signer +- `name`: Display name of the signer (e.g., "Para", "Turnkey", "MidenFi") + +This unified interface means your wallet UI code works the same regardless of which signer provider is used. + +--- + +### External signer sessions in v0.16 + +The Para and Turnkey examples below import an **existing public testnet account** +controlled by the selected signer. Pass its ID as `existingAccountId`. This uses +`importAccountId` to avoid the new-account authentication-enum mismatch in React +SDK v0.16.0. An API key or organization ID alone does not create that account. + +Use this shared wrapper in `SignerSession.tsx`. It mounts `MidenProvider` after +connection and unmounts it on disconnect, so reconnecting creates a fresh client +instead of calling v0.16.0's unavailable `setSignCb` method. Replace the original +provider wrapper in `main.tsx` with the selected signer example; do not nest two +`MidenProvider` instances. + +```tsx +import type { ReactNode } from 'react'; +import { MidenProvider, useSigner } from '@miden-sdk/react'; + +export function SignerSession({ children }: { children: ReactNode }) { + const signer = useSigner(); + if (!signer?.isConnected) { + return ; + } + return ( + + {children} + + ); +} +``` + +### Para: EVM Wallet Integration + +[Para](https://docs.getpara.com/v2/introduction/welcome) provides a modal-based authentication flow that allows users to sign in with their EVM wallets (MetaMask, WalletConnect, etc.). + +Use `@miden-sdk/para-react@0.16.0` with the 0.16 SDK packages. +Provide a Para API key for the selected environment. + +Install the adapter and its peer dependencies: + +```bash +yarn add @miden-sdk/para-react@0.16.0 @miden-sdk/para@0.16.0 @getpara/react-sdk-lite@^2.11.0 @getpara/web-sdk@^2.11.0 @tanstack/react-query@^5 +yarn add -D vite-plugin-node-polyfills@^0.22.0 +``` + +In `vite.config.ts`, import `paraVitePlugin` from `@miden-sdk/para-react/vite` +and add `paraVitePlugin()` to the existing `plugins` array. It supplies the +browser polyfills required by Para. Import `@getpara/react-sdk-lite/styles.css` +once in `src/main.tsx` for the connection modal. + +**Integration:** + +```tsx +import { ParaSignerProvider } from '@miden-sdk/para-react'; +import { useSigner } from '@miden-sdk/react'; +import { SignerSession } from './SignerSession'; + +function App({ existingAccountId }: { existingAccountId: string }) { + return ( + + + + + + ); +} + +function Wallet() { + const signer = useSigner(); + + return ( +
+ {signer?.isConnected ? ( + + ) : ( + + )} +
+ ); +} +``` + +**ParaSignerProvider Props:** + +| Prop | Type | Description | +| ----------------------- | -------------------------------------------- | ------------------------------------------ | +| `apiKey` | `string` | Your Para API key | +| `environment` | `"PRODUCTION" \| "DEVELOPMENT" \| "SANDBOX"` | Para environment | +| `showSigningModal` | `boolean` | Whether to show signing confirmation modal | +| `customSignConfirmStep` | `CustomSignConfirmStep` | Custom signing confirmation UI | + +--- + +### Turnkey: App-Controlled Authentication + +[Turnkey](https://turnkey.com/) provides programmatic key management, giving your application full control over the authentication flow. + +Use `@miden-sdk/turnkey-react@0.16.0` with the 0.16 SDK packages. +Create a Turnkey organization and pass its ID in `config`. + +Install the adapter and its peer dependencies: + +```bash +yarn add @miden-sdk/turnkey-react@0.16.0 @miden-sdk/turnkey@0.16.0 @turnkey/core@^1.8.2 @turnkey/react-wallet-kit@^1.6.2 @turnkey/sdk-browser@^5.13.4 +``` + +**Integration:** + +```tsx +import { TurnkeySignerProvider } from '@miden-sdk/turnkey-react'; +import { useSigner } from '@miden-sdk/react'; +import { SignerSession } from './SignerSession'; + +function App({ existingAccountId }: { existingAccountId: string }) { + return ( + + + + + + ); +} + +function Wallet() { + const signer = useSigner(); + + return ( +
+ {signer?.isConnected ? ( + + ) : ( + + )} +
+ ); +} +``` + +After configuring the organization, `connect()` starts the Turnkey authentication flow: passkey login, wallet discovery, and account selection. + +**TurnkeySignerProvider Props:** + +| Prop | Type | Description | +| -------- | -------------------------------------- | ------------------------------------------------------------------------------------------ | +| `config` | `TurnkeySignerProviderProps["config"]` | Required. Set `defaultOrganizationId`; `apiBaseUrl` defaults to `https://api.turnkey.com`. | + +The `useTurnkeySigner()` hook is available for advanced use cases where you need direct access to the Turnkey `client`, the selected `account`, or the `setAccount()` method to manually control account selection. + +--- + +### MidenFi: Wallet Adapter + +[MidenFi](https://github.com/0xMiden/wallet) provides a wallet adapter pattern similar to Solana's wallet-adapter, enabling integration with the MidenFi ecosystem. + +**Installation:** + +```bash +yarn add @miden-sdk/miden-wallet-adapter-react@0.16.0 @miden-sdk/miden-wallet-adapter-base@0.16.0 +``` + +**Usage:** + +```tsx +import { MidenFiSignerProvider } from '@miden-sdk/miden-wallet-adapter-react'; +import { WalletAdapterNetwork } from '@miden-sdk/miden-wallet-adapter-base'; +import { useSigner } from '@miden-sdk/react'; +import { SignerSession } from './SignerSession'; + +function App() { + return ( + + + + + + ); +} + +function Wallet() { + const signer = useSigner(); + + return ( +
+ {signer?.isConnected ? ( + + ) : ( + + )} +
+ ); +} +``` + +**MidenFiSignerProvider Props:** + +| Prop | Type | Description | +| ----------------------- | ----------------------- | ---------------------------------------- | +| `network` | `WalletAdapterNetwork` | `Testnet`, `Devnet`, or `Localnet` | +| `privateDataPermission` | `PrivateDataPermission` | Permission level for private data access | +| `allowedPrivateData` | `AllowedPrivateData` | Private-data categories the app requests | + +Import the enums from `@miden-sdk/miden-wallet-adapter-base`. Do not pass raw +network strings, booleans, or string arrays in place of these enum values. + +--- + +### Building a Custom Signer Provider + +If you need to integrate with a different signing service, you can build your own signer provider by implementing the `SignerContextValue` interface and providing it via `SignerContext.Provider`. + +Pass your service initializer as `initializeSigningService`. It must return the ID +of an existing public account on testnet, its serialized public key commitment, +and a callback that signs the SDK's serialized signing inputs. The callback must +verify the requested public key before signing. Account creation and deployment +belong to the signing service in this example: `importAccountId` imports that +account instead of rebuilding it with a new seed. This also avoids the new-account +auth-enum mismatch in the published React SDK v0.16.0. + +Keep a non-null context while disconnected so `useSigner()` can expose `connect` +and `MidenProvider` waits for the signer. In v0.16, `accountConfig` is only read +when connected, although its TypeScript type is non-nullable; the assertion below +reflects that runtime guard. Import WASM classes from the core SDK and await +`MidenClient.ready()` before constructing them. + +The keyed fragment remounts the child `MidenProvider` after each connection. +This avoids v0.16.0's reconnect path, which calls an unavailable `setSignCb` +method. The account-specific store name stays the same, preserving local data. + +```tsx +import { Fragment, useState, useRef, useCallback, useMemo, type ReactNode } from 'react'; +import { + SignerContext, + type SignerContextValue, + type SignerAccountConfig, + type SignCallback, +} from '@miden-sdk/react'; +import { AccountStorageMode, MidenClient } from '@miden-sdk/miden-sdk'; + +interface SigningService { + // An existing public account on the network configured in MidenProvider. + accountId: string; + publicKeyCommitment: Uint8Array; + signMessage: SignCallback; + disconnect?: () => Promise; +} + +interface CustomSignerProviderProps { + children: ReactNode; + initializeSigningService: () => Promise; +} + +export function CustomSignerProvider({ + children, + initializeSigningService, +}: CustomSignerProviderProps) { + const service = useRef(null); + const [accountConfig, setAccountConfig] = useState(null); + const [connection, setConnection] = useState(0); + + const connect = useCallback(async () => { + await MidenClient.ready(); + const connected = await initializeSigningService(); + service.current = connected; + setAccountConfig({ + publicKeyCommitment: connected.publicKeyCommitment, + storageMode: AccountStorageMode.public(), + importAccountId: connected.accountId, + }); + setConnection((previous) => previous + 1); + }, [initializeSigningService]); + + const disconnect = useCallback(async () => { + const connected = service.current; + service.current = null; + setAccountConfig(null); + await connected?.disconnect?.(); + }, []); + + const signCb = useCallback(async (pubKey, signingInputs) => { + if (!service.current) throw new Error('CustomSigner is not connected'); + return service.current.signMessage(pubKey, signingInputs); + }, []); + + const signerContext = useMemo(() => ({ + signCb, + // v0.16 only reads this field when isConnected is true. + accountConfig: accountConfig!, + storeName: accountConfig ? `custom_${accountConfig.importAccountId}` : 'custom', + name: 'CustomSigner', + isConnected: accountConfig !== null, + connect, + disconnect, + }), [accountConfig, signCb, connect, disconnect]); + + return ( + + {children} + + ); +} +``` + +The `SignerContextValue` interface requires: + +| Field | Type | Description | +| --------------- | ------------------------------------------------ | --------------------------------------------------------------- | +| `signCb` | `(pubKey, signingInputs) => Promise` | Signs transaction inputs and returns the signature | +| `accountConfig` | `SignerAccountConfig` | Public key commitment and storage mode | +| `storeName` | `string` | Unique suffix for IndexedDB isolation (e.g., "custom_walletId") | +| `name` | `string` | Display name for UI (e.g., "CustomSigner") | +| `isConnected` | `boolean` | Whether the signer is connected and ready | +| `connect` | `() => Promise` | Triggers the authentication flow | +| `disconnect` | `() => Promise` | Disconnects from the signer | + +--- + +## Continue Learning + +Now that you've built a React wallet, explore these related topics: + +- [Creating Multiple Notes in a Single Transaction](./creating_multiple_notes_tutorial.md) - Learn about batch operations +- [Miden React SDK Reference](https://github.com/0xMiden/web-sdk/tree/v0.16.0/packages/react-sdk) - Full API documentation +- [Miden Documentation](https://docs.miden.xyz/) - Core Miden concepts diff --git a/versioned_docs/version-0.16/builder/tutorials/recipes/web/setup_guide.md b/versioned_docs/version-0.16/builder/tutorials/recipes/web/setup_guide.md new file mode 100644 index 00000000..8823683a --- /dev/null +++ b/versioned_docs/version-0.16/builder/tutorials/recipes/web/setup_guide.md @@ -0,0 +1,281 @@ +--- +title: 'Web Client Setup Guide' +sidebar_position: 0 +--- + +# Web Client Setup Guide + +This guide covers the configuration required to use the Miden web SDK (`@miden-sdk/miden-sdk`) in a Next.js application. These settings apply to all web tutorials in this section. + +## Prerequisites + +- Node.js 20.9+ for the current Next.js template; see the `localStorage` compatibility workaround below if needed +- Next.js 14+ with App Router +- yarn or npm + +## Install the SDK + +```bash +yarn add @miden-sdk/miden-sdk@0.16.0 +``` + +For React hook support: + +```bash +yarn add @miden-sdk/miden-sdk@0.16.0 @miden-sdk/react@0.16.0 +``` + +These tutorials use Next.js, so all code examples import from the SDK's `/lazy` subpath — see [Entry points: eager vs lazy](#entry-points-eager-vs-lazy) below for why that's required. + +## Next.js Configuration + +Create or update `next.config.ts` with these required settings. With Next.js 16 or newer, use `next dev --webpack` and `next build --webpack` so this webpack callback runs. The repository’s Next.js 15 app uses webpack by default. + +```ts +import type { NextConfig } from 'next'; + +const nextConfig: NextConfig = { + // Static export avoids runtime SSR entirely. + // The SDK is browser-only, so static export is recommended. + output: 'export', + trailingSlash: true, + skipTrailingSlashRedirect: true, + experimental: { + // Required for the SDK's ESM bundle to resolve correctly in webpack. + esmExternals: 'loose', + }, + webpack: (config) => { + config.experiments = { + ...config.experiments, + // Required: the SDK loads a WASM binary for Miden VM operations. + asyncWebAssembly: true, + topLevelAwait: true, + }; + + // Serve .wasm files as static assets. + config.module.rules.push({ + test: /\.wasm$/, + type: 'asset/resource', + }); + + return config; + }, +}; + +export default nextConfig; +``` + +### Importing `.masm` files (for smart contract tutorials) + +If your tutorials use Miden assembly (`.masm`) files, add this webpack rule inside the `webpack` callback: + +```ts +// Import .masm files as plain text strings. +config.module.rules.push({ + test: /\.masm$/, + type: 'asset/source', +}); +``` + +Then create `lib/masm/masm.d.ts` so TypeScript recognizes the imports: + +```ts +declare module '*.masm' { + const content: string; + export default content; +} +``` + +:::tip Other bundlers + +- **Vite:** use the `?raw` suffix — `import code from './masm/counter_contract.masm?raw'` +- **No bundler:** use `fetch()` at runtime — `const code = await fetch('/masm/counter_contract.masm').then(r => r.text())` + +::: + +## Entry points: eager vs lazy + +The SDK ships two entry points: + +- **Default entry** (`@miden-sdk/miden-sdk`, `@miden-sdk/react`) — awaits WASM initialization at module top level. Ergonomic for Vite and plain-browser projects: import the SDK and construct wasm-bindgen types on the next line, no ceremony. **Not usable from Next.js App Router** — top-level `await` blocks the server render phase. +- **`/lazy` subpath** (`@miden-sdk/miden-sdk/lazy`, `@miden-sdk/react/lazy`) — synchronous import with no top-level `await`. The caller is responsible for awaiting WASM readiness before constructing any wasm-bindgen type. **This is the correct entry for Next.js.** + +In raw TypeScript, gate every function body on `MidenClient.ready()`: + +```ts +import { MidenClient } from '@miden-sdk/miden-sdk/lazy'; + +export async function doSomething() { + if (typeof window === 'undefined') return; + await MidenClient.ready(); + // Safe to construct wasm-bindgen types from here. + const client = await MidenClient.createTestnet(); + // … +} +``` + +In React, the `@miden-sdk/react/lazy` provider manages WASM readiness for you via the `isReady` flag returned by `useMiden()`. Gate any wasm-bindgen-touching code on `isReady`: + +```tsx +import { useMiden, useCreateWallet } from '@miden-sdk/react/lazy'; +import { getWasmOrThrow } from '@miden-sdk/miden-sdk/lazy'; + +function Component() { + const { isReady } = useMiden(); + const { createWallet } = useCreateWallet(); + return ( + + ); +} +``` + +:::warning Types imported from `/lazy` are stubs until `ready()` resolves + +Never construct wasm-bindgen types (`AccountId`, `Note`, `createP2IDNote`, `TransactionRequestBuilder`, etc.) at module top level or in a render-body `useMemo` — always inside an effect, event handler, or async hook callback where WASM is already initialized. For display-only cases like shortening an address, slice the bech32 string directly (`addr.slice(0, 8) + '…' + addr.slice(-4)`); don't parse it with `AccountId.fromBech32()` just to get a prefix. + +::: + +## Network and fee setup + +The v0.16 examples use testnet by default. Run them with `yarn tutorials --web` +from the repository root; add `--web=react:createMintConsume` to select a React example. +For explicit devnet testing, run `TUTORIAL_NETWORK=devnet yarn tutorials --web`. + +New accounts need the native fee asset before executing transactions. The examples +request a public P2ID note from the faucet and consume it as their first transaction, +paying that transaction's fee from the input note. User-created faucets include +`BasicWallet`, so they can receive fee funding too. The SDK supplies native +fee-conversion data automatically. + +Copy `web-client/lib/feeSupport.ts` with the complete TypeScript examples and +`web-client/lib/react/tutorialSupport.tsx` with React examples. These repository +helpers fund accounts, synchronize state, and await confirmation. They select +application notes by ID or token and exclude `TX_FEE` notes (tag `0xFEE`). + +The React helper's `tutorialAuthScheme()` returns the low-level Falcon enum +required by wallet and faucet hooks; it differs from the high-level client enum. + +The faucet URL and funding amount default to the selected network's faucet and its advertised +`base_amount`. Override them with `NEXT_PUBLIC_MIDEN_FAUCET_URL` and +`NEXT_PUBLIC_MIDEN_FEE_AMOUNT`, or `MIDEN_FAUCET_URL` and `MIDEN_FEE_AMOUNT` in the +repository runner. `NEXT_PUBLIC_MIDEN_NETWORK` selects the network when running the app directly. + +## Node.js 22+ `localStorage` polyfill + +Some Node.js and Next.js combinations fail in the development overlay with: + +``` +TypeError: localStorage.getItem is not a function +``` + +The workaround below handles a server-side `localStorage` object that lacks the methods the development overlay expects. Apply it if you encounter this error; it is not a requirement for every Node.js 22+ installation. + +Add this polyfill at the top of `next.config.ts`, before the config object: + +```ts +{ + const store = new Map(); + const poly = { + getItem: (key: string) => store.get(key) ?? null, + setItem: (key: string, value: string) => { + store.set(key, value); + }, + removeItem: (key: string) => { + store.delete(key); + }, + clear: () => { + store.clear(); + }, + get length() { + return store.size; + }, + key: (index: number) => [...store.keys()][index] ?? null, + }; + (globalThis as Record).localStorage = poly; +} +``` + +This fallback supplies in-memory storage to server-side tooling. It does not persist application data and does not replace the browser's storage. + +## SDK API Patterns + +### Transaction return types + +Transaction calls return an object containing the ID and result. In these fragments, `mintOptions` and `sendOptions` are the parameter objects built with your funded accounts and token, as shown in the mint-and-transfer tutorial: + +```ts +// mint and consume return { txId, result } +const { txId: mintTxId, result: mintResult } = + await client.transactions.mint(mintOptions); + +// send returns { txId, note, result } +// note is non-null when returnNote: true +const { txId: sendTxId, note } = await client.transactions.send({ + ...sendOptions, + returnNote: true, +}); +``` + +### Waiting for confirmation + +You can wait for a transaction to be committed in two ways: + +```ts +// Option 1: Pass waitForConfirmation in the transaction call +await client.transactions.mint({ + ...mintOptions, + waitForConfirmation: true, +}); + +// Option 2: Wait separately using waitFor +const { txId } = await client.transactions.mint(mintOptions); +await client.transactions.waitFor(txId); // accepts TransactionId object or hex string +``` + +### Using transaction IDs in URLs + +When displaying transaction IDs in explorer links, call `.toHex()`: + +```ts +const { txId } = await client.transactions.mint(mintOptions); +console.log(`https://testnet.midenscan.com/tx/${txId.toHex()}`); +``` + +### Authentication + +Create an authentication key using `AuthSecretKey` (inside an async function, after awaiting `MidenClient.ready()` so the wasm-bindgen constructor is live): + +```ts +import { MidenClient, AuthSecretKey } from '@miden-sdk/miden-sdk/lazy'; + +export async function createAuth() { + await MidenClient.ready(); + + const seed = new Uint8Array(32); + crypto.getRandomValues(seed); + const auth = AuthSecretKey.rpoFalconWithRNG(seed); + return { seed, auth }; +} +``` + +Pass `auth` and `seed` when creating contract accounts that require authentication. + +### Concurrency safety and `waitForIdle()` + +All mutating `MidenClient` operations (`transactions.execute`, `transactions.submit`, `sync`, account creation) and async reads are internally serialized through a single promise chain. Consumers no longer need to maintain their own JS-level mutex, and the `"recursive use of an object detected"` wasm-bindgen panic caused by the auto-sync timer racing with user operations is gone. + +For the rare case where you need to coordinate a non-WASM side effect (for example, clearing an in-memory auth key on wallet lock) with whatever SDK work is currently in flight, drain the queue first: + +```ts +await client.waitForIdle(); // resolves when every serialized call has settled +clearMyAuthKeys(); +``` diff --git a/versioned_docs/version-0.16/builder/tutorials/recipes/web/unauthenticated_note_how_to.md b/versioned_docs/version-0.16/builder/tutorials/recipes/web/unauthenticated_note_how_to.md new file mode 100644 index 00000000..82009467 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tutorials/recipes/web/unauthenticated_note_how_to.md @@ -0,0 +1,515 @@ +--- +title: 'How to Use Unauthenticated Notes' +sidebar_position: 6 +--- + +import { CodeSdkTabs } from '@site/src/components'; + +_Using unauthenticated notes for optimistic note consumption with the Miden client_ + +:::note v0.16 setup + +Follow the [network and fee setup](./setup_guide.md#network-and-fee-setup) +and copy the shared support files imported by the complete example. +For React snippets, initialize `authScheme` with `await tutorialAuthScheme()` +as shown in the complete example. + +::: + +## Overview + +This tutorial passes newly created P2ID notes directly to the next consumer. An unauthenticated input contains the full note without an inclusion proof. The transaction kernel delegates verification of the note's existence to the protocol kernels, allowing the consumer transaction to execute before the producer transaction is confirmed. Final settlement still requires verification by the network. + +The Web and React SDKs choose the input mode from the executing client's store: they use an authenticated input when an inclusion proof is available and an unauthenticated input otherwise. Passing a full `Note` supports unauthenticated consumption but does not force that mode; synchronization can make an inclusion proof available. + +The example uses one client to manage Alice and five recipient wallets. At each hop, it submits the consumer transaction before waiting for the sender's confirmation, then waits for both transactions before advancing to the next wallet. It verifies the resulting balances; it does not measure transaction latency or guarantee that both transactions settle in the same batch. + +The asset follows this chain: + +```markdown +Alice ➡ Wallet 1 ➡ Wallet 2 ➡ Wallet 3 ➡ Wallet 4 ➡ Wallet 5 +``` + +## What we'll cover + +- **Introduction to Unauthenticated Notes:** Understand what unauthenticated notes are and how they differ from standard notes. +- **Miden Client Setup:** Configure the Miden client for browser-based transactions. +- **P2ID Note Creation:** Learn how to create Pay-to-ID notes for targeted transfers. +- **Confirmation Boundaries:** Distinguish optimistic execution from confirmed settlement. + +## Prerequisites + +- Node `v20.9.0` or greater (required by the current Next.js template) +- Familiarity with TypeScript +- `yarn` + +This tutorial assumes you have a basic understanding of Miden assembly. To quickly get up to speed with Miden assembly (MASM), please play around with running basic Miden assembly programs in the [Miden playground](https://0xmiden.github.io/examples/). + +## Step-by-step process + +1. **Next.js Project Setup:** + - Create a new Next.js application with TypeScript. + - Install the Miden SDK. + +2. **Client Initialization:** + - Set up the Miden client to connect with Miden testnet. + +3. **Account Creation:** + - Create wallet accounts for Alice and multiple transfer recipients. + - Deploy a fungible faucet for token minting. + +4. **Initial Token Setup:** + - Mint tokens from the faucet to Alice's account. + - Consume the minted tokens to prepare for transfers. + +5. **Unauthenticated Note Transfer Chain:** + - Create P2ID (Pay-to-ID) notes for each transfer in the chain. + - Pass each full output note directly to the next consumer. + - Wait for both transactions to be confirmed and verify the balances. + +## Step 1: Initialize your Next.js project + +1. Create a new Next.js app with TypeScript: + + ```bash + npx create-next-app@latest miden-web-app --typescript + ``` + + Hit enter for all terminal prompts. + +2. Change into the project directory: + + ```bash + cd miden-web-app + ``` + +3. Install the Miden SDK: + + + +The current Next.js template uses Turbopack by default. Use the webpack configuration from the setup guide and update both scripts in `package.json`: + +`package.json` + +```json +{ + "scripts": { + "dev": "next dev --webpack", + "build": "next build --webpack" + } +} +``` + +## Step 2: Edit the `app/page.tsx` file + +Add the following code to the `app/page.tsx` file. This code defines the main page of our web application: + +If you're using the **React SDK**, the page simply renders your self-contained component: + +```tsx +// app/page.tsx +'use client'; +import UnauthenticatedNoteTransfer from '../lib/react/unauthenticatedNoteTransfer'; + +export default function Home() { + return ; +} +``` + +If you're using the **TypeScript SDK**, the page manages state and calls the library function directly: + +```tsx +// app/page.tsx +'use client'; +import { useState } from 'react'; +import { unauthenticatedNoteTransfer } from '../lib/unauthenticatedNoteTransfer'; + +export default function Home() { + const [isTransferring, setIsTransferring] = useState(false); + + const handleUnauthenticatedNoteTransfer = async () => { + setIsTransferring(true); + await unauthenticatedNoteTransfer(); + setIsTransferring(false); + }; + + return ( +
+
+

Miden Web App

+

+ Open your browser console to see Miden client logs. +

+ +
+ +
+
+
+ ); +} +``` + +## Step 3: Create the Unauthenticated Note Transfer Implementation + +Create the library file and add the following code: + +```bash +mkdir -p lib/react +``` + +Copy and paste the following code into `lib/react/unauthenticatedNoteTransfer.tsx` (React) or `lib/unauthenticatedNoteTransfer.ts` (TypeScript): + + { +..await sync(); +..const authScheme = await tutorialAuthScheme(); +..const alice = await createWallet({ +...storageMode: StorageMode.Public, +...authScheme, +..}); +..console.log('Alice ID:', alice.id().toString()); +..await fundAccount(alice); +..const wallets: Account[] = []; +..for (let index = 0; index < 5; index += 1) { +...const wallet = await createWallet({ +....storageMode: StorageMode.Public, +....authScheme, +...}); +...console.log(\`Wallet \${index}:\`, wallet.id().toString()); +...// Every recipient pays fees when consuming and forwarding the note. +...await fundAccount(wallet); +...wallets.push(wallet); +..} +..const faucet = await createFaucet({ +...tokenSymbol: 'MID', +...decimals: 8, +...maxSupply: BigInt(1_000_000), +...storageMode: StorageMode.Public, +...authScheme, +..}); +..console.log('Faucet ID:', faucet.id().toString()); +..await fundAccount(faucet); +..await sync(); +..const minted = await mint({ +...faucetId: faucet, +...targetAccountId: alice, +...amount: BigInt(10_000), +...noteType: NoteVisibility.Public, +..}); +..await committed(minted.transactionId); +..const notes = await waitForTokenNotes(alice, faucet); +..const consumed = await consume({ accountId: alice.id().toString(), notes }); +..await committed(consumed.transactionId); + +..// Pass full Note objects directly, without fetching an inclusion proof. +..let currentSender = alice; +..for (let index = 0; index < wallets.length; index += 1) { +...const wallet = wallets[index]; +...const sent = await send({ +....from: currentSender, +....to: wallet, +....assetId: faucet, +....amount: BigInt(50), +....noteType: NoteVisibility.Public, +....returnNote: true, +...}); +...if (!sent.note) throw new Error('Send did not return its output note'); +...const received = await consume({ +....accountId: wallet.id().toString(), +....notes: [sent.note], +...}); +...await committed(sent.txId); +...await committed(received.transactionId); +...await assertBalance(wallet, faucet, BigInt(50)); +...console.log( +....\`Transfer \${index + 1}: \${tutorialExplorerUrl()}/tx/\${received.transactionId}\`, +...); +...currentSender = wallet; +..} +..await assertBalance(alice, faucet, BigInt(9950)); +..for (const wallet of wallets.slice(0, -1)) +...await assertBalance(wallet, faucet, BigInt(0)); +..console.log('Asset transfer chain completed ✅'); +.}; + +.return ( +.. +.); +} + +export default function UnauthenticatedNoteTransfer() { +.return ( +.. +... +.. +.); +}`}, + typescript: { code: `import { NoteVisibility, StorageMode } from '@miden-sdk/miden-sdk/lazy'; +import { +.consumeAllFeeAware, +.createTutorialClient, +.fundAccountForFees, +.tutorialExplorerUrl, +} from './feeSupport'; + +export async function unauthenticatedNoteTransfer(): Promise { +.// Ensure this runs only in a browser context +.if (typeof window === 'undefined') return console.warn('Run in browser'); + +.const client = await createTutorialClient({ +..proverUrl: 'local', +.}); + +.console.log('Latest block:', (await client.sync()).blockNum()); + +.// ── Creating new account ────────────────────────────────────────────────────── +.console.log('Creating accounts'); + +.console.log('Creating account for Alice…'); +.const alice = await client.accounts.create({ +..storage: StorageMode.Public, +.}); +.console.log('Alice account ID:', alice.id().toString()); + +.const wallets = []; +.for (let i = 0; i < 5; i++) { +..const wallet = await client.accounts.create({ +...storage: StorageMode.Public, +..}); +..wallets.push(wallet); +..console.log('wallet ', i.toString(), wallet.id().toString()); +.} + +.const faucet = await client.accounts.create({ +..type: 0, // 0 = FungibleFaucet +..symbol: 'MID', +..decimals: 8, +..maxSupply: BigInt(1_000_000), +..storage: StorageMode.Public, +.}); +.console.log('Faucet ID:', faucet.id().toString()); +.await fundAccountForFees(client, alice); +.await fundAccountForFees(client, faucet); + +.await client.sync(); +.const { txId: mintTxId } = await client.transactions.mint({ +..account: faucet, +..to: alice, +..amount: BigInt(10_000), +..type: NoteVisibility.Public, +.}); +.console.log('Waiting for settlement'); +.await client.transactions.waitFor(mintTxId, { timeout: 120_000 }); +.await consumeAllFeeAware(client, alice); + +.for (const wallet of wallets) { +..await fundAccountForFees(client, wallet); +.} + +.// ── Create unauthenticated note transfer chain ───────────────────────────────────────────── +.// Alice → wallet 1 → wallet 2 → wallet 3 → wallet 4 +.for (let i = 0; i < wallets.length; i++) { +..console.log(\`\\nUnauthenticated tx \${i + 1}\`); + +..const sender = i === 0 ? alice : wallets[i - 1]; +..const receiver = wallets[i]; + +..console.log('Sender:', sender.id().toString()); +..console.log('Receiver:', receiver.id().toString()); + +..await client.sync(); +..const { note, txId: sendTxId } = await client.transactions.send({ +...account: sender, +...to: receiver, +...token: faucet, +...amount: BigInt(50), +...type: NoteVisibility.Public, +...returnNote: true, +...waitForConfirmation: false, +..}); + +..// Pass the full note before waiting for the sender's transaction. +..await client.sync(); +..const { txId: consumeTxId } = await client.transactions.consume({ +...account: receiver, +...notes: [note], +...waitForConfirmation: true, +...timeout: 120_000, +..}); +..await client.transactions.waitFor(sendTxId, { timeout: 120_000 }); +..console.log(\`Transaction committed: \${consumeTxId.toHex()}\`); + +..console.log( +...\`Consumed Note Tx on MidenScan: \${tutorialExplorerUrl()}/tx/\${consumeTxId.toHex()}\`, +..); +.} + +.const lastWallet = await client.accounts.get(wallets[wallets.length - 1]); +.const balance = lastWallet?.vault().getBalance(faucet.id()); +.if (balance !== BigInt(50)) +..throw new Error(\`Expected last wallet to hold 50 MID, got \${balance}\`); +.console.log('Asset transfer chain completed ✅'); +}` }, +}} reactFilename="lib/react/unauthenticatedNoteTransfer.tsx" tsFilename="lib/unauthenticatedNoteTransfer.ts" /> + +## Key Concepts: Unauthenticated Notes + +### What are Unauthenticated Notes? + +Unauthenticated notes are a powerful feature that allows notes to be: + +- **Created and consumed in the same block** +- **Passed to a consuming transaction before block confirmation** +- **Used for optimistic transactions** + +### Performance Benefits + +By using unauthenticated notes, we can: + +- Skip waiting for block confirmation between note creation and consumption +- Submit dependent transaction chains that may be included in a single block +- Begin dependent execution earlier; final settlement still requires network confirmation + +### Use Cases + +Unauthenticated notes are ideal for: + +- **High-frequency trading applications** +- **Payment channels** +- **Micropayment systems** +- **Applications that benefit from optimistic execution before confirmation** + +## Running the Example + +To run the unauthenticated note transfer example: + +```bash +cd miden-web-app +yarn install +yarn dev +``` + +Open [http://localhost:3000](http://localhost:3000) in your browser, click the **"Tutorial #4: Unauthenticated Note Transfer"** button, and check the browser console for detailed logs. + +### Expected Output + +You should see output similar to this in the browser console (account IDs, +block number, and transaction hashes vary with live testnet state): + +``` +Latest block: +Creating accounts +Creating account for Alice… +Alice account ID: +wallet 0 +wallet 1 +wallet 2 +wallet 3 +wallet 4 +Faucet ID: +Waiting for settlement + +Unauthenticated tx 1 +Sender: +Receiver: +Consumed Note Tx on MidenScan: https://testnet.midenscan.com/tx/ + +Unauthenticated tx 2 +... + +Asset transfer chain completed ✅ +``` + +## Conclusion + +Unauthenticated notes let applications submit dependent transactions without first waiting for the producer's confirmation. Creation and consumption may be included in the same block; this does not provide settlement before block production. In this guide, we walked through: + +- **Setting up the Miden client** against testnet +- **Creating P2ID Notes** for targeted asset transfers between specific accounts +- **Building Transaction Chains** that submit consumption before waiting for the producer's confirmation +- **Confirmation and balance checks** for the complete transfer chain + +By following this guide, you should now have a clear understanding of how to build and deploy high-performance transactions using unauthenticated notes on Miden with the Miden client. Unauthenticated notes are the ideal approach for applications like central limit order books (CLOBs) or other DeFi platforms where transaction speed is critical. + +### Resetting the `MidenClientDB` + +Stop or terminate the tutorial client and close other tabs using its store before resetting it. This deletes local account data and keys, so use it only for disposable tutorial accounts. The snippet below waits for deletion of the default testnet store; change `name` if you configured a different store. + +```javascript +(async () => { + const name = 'MidenClientDB_mtst'; + await new Promise((resolve, reject) => { + const request = indexedDB.deleteDatabase(name); + request.onsuccess = () => resolve(); + request.onerror = () => reject(request.error); + request.onblocked = () => + reject(new Error('Close clients and tabs using this store, then retry.')); + }); + console.log(`Deleted database: ${name}`); +})(); +``` + +### Running the Full Example + +To run a full working example navigate to the `web-client` directory in the [miden-tutorials](https://github.com/0xMiden/miden-tutorials/) repository and run the web application example: + +```bash +cd web-client +yarn install +yarn dev +``` + +### Continue learning + +Next tutorial: [Creating Multiple Notes](creating_multiple_notes_tutorial.md) diff --git a/versioned_docs/version-0.16/builder/tutorials/theme/Admonition/Icon/Danger.tsx b/versioned_docs/version-0.16/builder/tutorials/theme/Admonition/Icon/Danger.tsx new file mode 100644 index 00000000..ab14d7de --- /dev/null +++ b/versioned_docs/version-0.16/builder/tutorials/theme/Admonition/Icon/Danger.tsx @@ -0,0 +1,19 @@ +import React, { type ReactNode } from "react"; +import type { Props } from "@theme/Admonition/Icon/Danger"; + +export default function AdmonitionIconDanger(props: Props): ReactNode { + return ( + + + + ); +} diff --git a/versioned_docs/version-0.16/builder/tutorials/theme/Admonition/Icon/Info.tsx b/versioned_docs/version-0.16/builder/tutorials/theme/Admonition/Icon/Info.tsx new file mode 100644 index 00000000..59e48a52 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tutorials/theme/Admonition/Icon/Info.tsx @@ -0,0 +1,20 @@ +import React, { type ReactNode } from "react"; +import type { Props } from "@theme/Admonition/Icon/Info"; + +export default function AdmonitionIconInfo(props: Props): ReactNode { + return ( + + + + ); +} diff --git a/versioned_docs/version-0.16/builder/tutorials/theme/Admonition/Icon/Note.tsx b/versioned_docs/version-0.16/builder/tutorials/theme/Admonition/Icon/Note.tsx new file mode 100644 index 00000000..d7c524b3 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tutorials/theme/Admonition/Icon/Note.tsx @@ -0,0 +1,20 @@ +import React, { type ReactNode } from "react"; +import type { Props } from "@theme/Admonition/Icon/Note"; + +export default function AdmonitionIconNote(props: Props): ReactNode { + return ( + + + + ); +} diff --git a/versioned_docs/version-0.16/builder/tutorials/theme/Admonition/Icon/Tip.tsx b/versioned_docs/version-0.16/builder/tutorials/theme/Admonition/Icon/Tip.tsx new file mode 100644 index 00000000..219bb8d0 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tutorials/theme/Admonition/Icon/Tip.tsx @@ -0,0 +1,20 @@ +import React, { type ReactNode } from "react"; +import type { Props } from "@theme/Admonition/Icon/Tip"; + +export default function AdmonitionIconTip(props: Props): ReactNode { + return ( + + + + ); +} diff --git a/versioned_docs/version-0.16/builder/tutorials/theme/Admonition/Icon/Warning.tsx b/versioned_docs/version-0.16/builder/tutorials/theme/Admonition/Icon/Warning.tsx new file mode 100644 index 00000000..f96398d1 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tutorials/theme/Admonition/Icon/Warning.tsx @@ -0,0 +1,20 @@ +import React, { type ReactNode } from "react"; +import type { Props } from "@theme/Admonition/Icon/Warning"; + +export default function AdmonitionIconCaution(props: Props): ReactNode { + return ( + + + + ); +} diff --git a/versioned_docs/version-0.16/builder/tutorials/theme/Admonition/Layout/index.tsx b/versioned_docs/version-0.16/builder/tutorials/theme/Admonition/Layout/index.tsx new file mode 100644 index 00000000..7b2c170d --- /dev/null +++ b/versioned_docs/version-0.16/builder/tutorials/theme/Admonition/Layout/index.tsx @@ -0,0 +1,51 @@ +import React, { type ReactNode } from "react"; +import clsx from "clsx"; +import { ThemeClassNames } from "@docusaurus/theme-common"; + +import type { Props } from "@theme/Admonition/Layout"; + +import styles from "./styles.module.css"; + +function AdmonitionContainer({ + type, + className, + children, +}: Pick & { children: ReactNode }) { + return ( +
+ {children} +
+ ); +} + +function AdmonitionHeading({ icon, title }: Pick) { + return ( +
+ {icon} + {/* {title} */} +
+ ); +} + +function AdmonitionContent({ children }: Pick) { + return children ? ( +
{children}
+ ) : null; +} + +export default function AdmonitionLayout(props: Props): ReactNode { + const { type, icon, title, children, className } = props; + return ( + + {title || icon ? : null} + {children} + + ); +} diff --git a/versioned_docs/version-0.16/builder/tutorials/theme/Admonition/Layout/styles.module.css b/versioned_docs/version-0.16/builder/tutorials/theme/Admonition/Layout/styles.module.css new file mode 100644 index 00000000..88df7e63 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tutorials/theme/Admonition/Layout/styles.module.css @@ -0,0 +1,35 @@ +.admonition { + margin-bottom: 1em; +} + +.admonitionHeading { + font: var(--ifm-heading-font-weight) var(--ifm-h5-font-size) / + var(--ifm-heading-line-height) var(--ifm-heading-font-family); + text-transform: uppercase; +} + +/* Heading alone without content (does not handle fragment content) */ +.admonitionHeading:not(:last-child) { + margin-bottom: 0.3rem; +} + +.admonitionHeading code { + text-transform: none; +} + +.admonitionIcon { + display: inline-block; + vertical-align: middle; + margin-right: 0.4em; +} + +.admonitionIcon svg { + display: inline-block; + height: 1.6em; + width: 1.6em; + fill: var(--ifm-alert-foreground-color); +} + +.admonitionContent > :last-child { + margin-bottom: 0; +} diff --git a/versioned_docs/version-0.16/builder/tutorials/theme/Admonition/Type/Caution.tsx b/versioned_docs/version-0.16/builder/tutorials/theme/Admonition/Type/Caution.tsx new file mode 100644 index 00000000..b570a37a --- /dev/null +++ b/versioned_docs/version-0.16/builder/tutorials/theme/Admonition/Type/Caution.tsx @@ -0,0 +1,32 @@ +import React, {type ReactNode} from 'react'; +import clsx from 'clsx'; +import Translate from '@docusaurus/Translate'; +import type {Props} from '@theme/Admonition/Type/Caution'; +import AdmonitionLayout from '@theme/Admonition/Layout'; +import IconWarning from '@theme/Admonition/Icon/Warning'; + +const infimaClassName = 'alert alert--warning'; + +const defaultProps = { + icon: , + title: ( + + caution + + ), +}; + +// TODO remove before v4: Caution replaced by Warning +// see https://github.com/facebook/docusaurus/issues/7558 +export default function AdmonitionTypeCaution(props: Props): ReactNode { + return ( + + {props.children} + + ); +} diff --git a/versioned_docs/version-0.16/builder/tutorials/theme/Admonition/Type/Danger.tsx b/versioned_docs/version-0.16/builder/tutorials/theme/Admonition/Type/Danger.tsx new file mode 100644 index 00000000..49901fa9 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tutorials/theme/Admonition/Type/Danger.tsx @@ -0,0 +1,30 @@ +import React, {type ReactNode} from 'react'; +import clsx from 'clsx'; +import Translate from '@docusaurus/Translate'; +import type {Props} from '@theme/Admonition/Type/Danger'; +import AdmonitionLayout from '@theme/Admonition/Layout'; +import IconDanger from '@theme/Admonition/Icon/Danger'; + +const infimaClassName = 'alert alert--danger'; + +const defaultProps = { + icon: , + title: ( + + danger + + ), +}; + +export default function AdmonitionTypeDanger(props: Props): ReactNode { + return ( + + {props.children} + + ); +} diff --git a/versioned_docs/version-0.16/builder/tutorials/theme/Admonition/Type/Info.tsx b/versioned_docs/version-0.16/builder/tutorials/theme/Admonition/Type/Info.tsx new file mode 100644 index 00000000..018e0a16 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tutorials/theme/Admonition/Type/Info.tsx @@ -0,0 +1,30 @@ +import React, {type ReactNode} from 'react'; +import clsx from 'clsx'; +import Translate from '@docusaurus/Translate'; +import type {Props} from '@theme/Admonition/Type/Info'; +import AdmonitionLayout from '@theme/Admonition/Layout'; +import IconInfo from '@theme/Admonition/Icon/Info'; + +const infimaClassName = 'alert alert--info'; + +const defaultProps = { + icon: , + title: ( + + info + + ), +}; + +export default function AdmonitionTypeInfo(props: Props): ReactNode { + return ( + + {props.children} + + ); +} diff --git a/versioned_docs/version-0.16/builder/tutorials/theme/Admonition/Type/Note.tsx b/versioned_docs/version-0.16/builder/tutorials/theme/Admonition/Type/Note.tsx new file mode 100644 index 00000000..c99e0385 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tutorials/theme/Admonition/Type/Note.tsx @@ -0,0 +1,30 @@ +import React, {type ReactNode} from 'react'; +import clsx from 'clsx'; +import Translate from '@docusaurus/Translate'; +import type {Props} from '@theme/Admonition/Type/Note'; +import AdmonitionLayout from '@theme/Admonition/Layout'; +import IconNote from '@theme/Admonition/Icon/Note'; + +const infimaClassName = 'alert alert--secondary'; + +const defaultProps = { + icon: , + title: ( + + note + + ), +}; + +export default function AdmonitionTypeNote(props: Props): ReactNode { + return ( + + {props.children} + + ); +} diff --git a/versioned_docs/version-0.16/builder/tutorials/theme/Admonition/Type/Tip.tsx b/versioned_docs/version-0.16/builder/tutorials/theme/Admonition/Type/Tip.tsx new file mode 100644 index 00000000..18604a5e --- /dev/null +++ b/versioned_docs/version-0.16/builder/tutorials/theme/Admonition/Type/Tip.tsx @@ -0,0 +1,30 @@ +import React, {type ReactNode} from 'react'; +import clsx from 'clsx'; +import Translate from '@docusaurus/Translate'; +import type {Props} from '@theme/Admonition/Type/Tip'; +import AdmonitionLayout from '@theme/Admonition/Layout'; +import IconTip from '@theme/Admonition/Icon/Tip'; + +const infimaClassName = 'alert alert--success'; + +const defaultProps = { + icon: , + title: ( + + tip + + ), +}; + +export default function AdmonitionTypeTip(props: Props): ReactNode { + return ( + + {props.children} + + ); +} diff --git a/versioned_docs/version-0.16/builder/tutorials/theme/Admonition/Type/Warning.tsx b/versioned_docs/version-0.16/builder/tutorials/theme/Admonition/Type/Warning.tsx new file mode 100644 index 00000000..61d9597b --- /dev/null +++ b/versioned_docs/version-0.16/builder/tutorials/theme/Admonition/Type/Warning.tsx @@ -0,0 +1,30 @@ +import React, {type ReactNode} from 'react'; +import clsx from 'clsx'; +import Translate from '@docusaurus/Translate'; +import type {Props} from '@theme/Admonition/Type/Warning'; +import AdmonitionLayout from '@theme/Admonition/Layout'; +import IconWarning from '@theme/Admonition/Icon/Warning'; + +const infimaClassName = 'alert alert--warning'; + +const defaultProps = { + icon: , + title: ( + + warning + + ), +}; + +export default function AdmonitionTypeWarning(props: Props): ReactNode { + return ( + + {props.children} + + ); +} diff --git a/versioned_docs/version-0.16/builder/tutorials/theme/Admonition/Types.tsx b/versioned_docs/version-0.16/builder/tutorials/theme/Admonition/Types.tsx new file mode 100644 index 00000000..2a100190 --- /dev/null +++ b/versioned_docs/version-0.16/builder/tutorials/theme/Admonition/Types.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import AdmonitionTypeNote from '@theme/Admonition/Type/Note'; +import AdmonitionTypeTip from '@theme/Admonition/Type/Tip'; +import AdmonitionTypeInfo from '@theme/Admonition/Type/Info'; +import AdmonitionTypeWarning from '@theme/Admonition/Type/Warning'; +import AdmonitionTypeDanger from '@theme/Admonition/Type/Danger'; +import AdmonitionTypeCaution from '@theme/Admonition/Type/Caution'; +import type AdmonitionTypes from '@theme/Admonition/Types'; + +const admonitionTypes: typeof AdmonitionTypes = { + note: AdmonitionTypeNote, + tip: AdmonitionTypeTip, + info: AdmonitionTypeInfo, + warning: AdmonitionTypeWarning, + danger: AdmonitionTypeDanger, +}; + +// Undocumented legacy admonition type aliases +// Provide hardcoded/untranslated retrocompatible label +// See also https://github.com/facebook/docusaurus/issues/7767 +const admonitionAliases: typeof AdmonitionTypes = { + secondary: (props) => , + important: (props) => , + success: (props) => , + caution: AdmonitionTypeCaution, +}; + +export default { + ...admonitionTypes, + ...admonitionAliases, +}; diff --git a/versioned_docs/version-0.16/builder/tutorials/theme/Admonition/index.tsx b/versioned_docs/version-0.16/builder/tutorials/theme/Admonition/index.tsx new file mode 100644 index 00000000..8f4225da --- /dev/null +++ b/versioned_docs/version-0.16/builder/tutorials/theme/Admonition/index.tsx @@ -0,0 +1,21 @@ +import React, {type ComponentType, type ReactNode} from 'react'; +import {processAdmonitionProps} from '@docusaurus/theme-common'; +import type {Props} from '@theme/Admonition'; +import AdmonitionTypes from '@theme/Admonition/Types'; + +function getAdmonitionTypeComponent(type: string): ComponentType { + const component = AdmonitionTypes[type]; + if (component) { + return component; + } + console.warn( + `No admonition component found for admonition type "${type}". Using Info as fallback.`, + ); + return AdmonitionTypes.info!; +} + +export default function Admonition(unprocessedProps: Props): ReactNode { + const props = processAdmonitionProps(unprocessedProps); + const AdmonitionTypeComponent = getAdmonitionTypeComponent(props.type); + return ; +} diff --git a/versioned_docs/version-0.16/reference/_category_.json b/versioned_docs/version-0.16/reference/_category_.json new file mode 100644 index 00000000..026f453f --- /dev/null +++ b/versioned_docs/version-0.16/reference/_category_.json @@ -0,0 +1,6 @@ +{ + "label": "Reference", + "position": 2, + "collapsible": false, + "collapsed": false +} diff --git a/versioned_docs/version-0.16/reference/compiler/_category_.yml b/versioned_docs/version-0.16/reference/compiler/_category_.yml new file mode 100644 index 00000000..adabe490 --- /dev/null +++ b/versioned_docs/version-0.16/reference/compiler/_category_.yml @@ -0,0 +1,4 @@ +label: Compiler +# Determines where this documentation section appears relative to other sections on the main documentation page (which is the parent of this folder in the miden-docs repository) +position: 8 +collapsed: true diff --git a/versioned_docs/version-0.16/reference/compiler/appendix/_category_.yml b/versioned_docs/version-0.16/reference/compiler/appendix/_category_.yml new file mode 100644 index 00000000..ab3d9c72 --- /dev/null +++ b/versioned_docs/version-0.16/reference/compiler/appendix/_category_.yml @@ -0,0 +1,4 @@ +label: Compiler +# Determines where this documentation section appears relative to other sections in the parent folder +position: 4 +collapsed: true diff --git a/versioned_docs/version-0.16/reference/compiler/appendix/calling-conventions.md b/versioned_docs/version-0.16/reference/compiler/appendix/calling-conventions.md new file mode 100644 index 00000000..eb80acea --- /dev/null +++ b/versioned_docs/version-0.16/reference/compiler/appendix/calling-conventions.md @@ -0,0 +1,305 @@ +--- +title: Calling Conventions +draft: true +--- + +# Calling conventions + +This document describes the various calling conventions recognized/handled by the compiler, +including a specification for the interaction with the IR type system. + +There are four calling conventions represented in the compiler: + +- `C` aka `SystemV`, which corresponds to the C ABI commonly used for C foreign-function interfaces (FFI). + We specifically use the System V ABI because it is well understood, documented, and straightforward. +- `Fast`, this convention allows the compiler to follow either the `C` calling convention, or modify it + as it sees fit on a function-by-function basis. This convention provides no guarantees about how a + callee will expect arguments to be passed, so it should not be used for functions which are expected to + have a stable, predictable interface. This is a good choice for local functions, or functions which are + only used within an executable/library and are not part of the public interface. +- `Kernel`, this is a special calling convention that is used when defining kernel modules in the IR. + Functions which are part of the kernel's public API are required to use this convention, and it is not + possible to call a function via `syscall` if the callee is not defined with this convention. Because of + the semantics of `syscall`, this convention is highly restrictive. In particular, it is not permitted to + pass pointer arguments, or aggregates containing pointers, as `syscall` involves a context switch, and + thus memory in the caller is not accessible to the callee, and vice versa. +- `Contract`, this is a special calling convention that is used when defining smart contract functions, i.e. + functions that can be `call`'d. The compiler will not permit you to `call` a function if the callee is not + defined with this convention, and functions with this convention cannot be called via `exec`. Like `syscall`, + the `call` instruction involves a context switch, however, unlike the `Kernel` convention, the `Contract` + convention is allowed to have types in its signature that are/contain pointers, with certain caveats around + those pointers. + +All four conventions above are based on the System V C ABI, tailored to the Miden VM. The only exception is +`Fast`, which may modify the ABI arbitrarily as it sees fit, and makes no guarantees about what modifications, +if any, it will make. + +## Data representation + +The following is a description of how the IR type system is represented in the `C` calling convention. Later, +a description of how the other conventions extend/restrict/modify this representation will be provided. + +### Scalars + +General type | C Type | IR Type | `sizeof` | Alignment (bytes) | Miden Type +-|-|-|-|-|- + Integer | `_Bool`/`bool` | `I1` | 1 | 1 | u32 + Integer | `char`, `signed char` | `I8` | 1 | 1 | i32[^1] + Integer | `unsigned char` | `U8` | 1 | 1 | u32 + Integer | `short` / `signed short` | `I16` | 2 | 2 | i32[^1] + Integer | `unsigned short` | `U16` | 2 | 2 | u32 + Integer | `int` / `signed int` / `enum` | `I32` | 4 | 4 | i32[^1][^8] + Integer | `unsigned int` | `U32` | 4 | 4 | u32 + Integer | `long` / `signed long` | `I32` | 4 | 4 | i32[^1] + Integer | `unsigned long` / `size_t` | `U32` | 4 | 4 | u32 + Integer | `long long` / `signed long long` | `I64` | 8 | 8 | i64[^2] + Integer | `unsigned long long` | `U64` | 8 | 8 | u64[^3] + Pointer | *`any-type *`* / *`any-type (*)()`* | `Ptr(_)` | 4 | 4 | u32[^6][^7] + Floating point | `float` | `F32` | 4 | 4 | u32[^4] + Floating point | `double` | `F64` | 8 | 8 | u64[^4] + Floating point | `long double` | (none) | 16 | 16 | (none)[^5] + +[^1]: i32 is not a native Miden type, but is implemented using compiler intrinsics on top of the native u32 type + +[^2]: i64 is not a native Miden type, but is implemented using compiler intrinsics on top of the stdlib u64 type + +[^3]: u64 is not a native Miden type, but is implemented in software using two 32-bit limbs (i.e. a pair of field elements) + +[^4]: floating-point types are not currently supported, but will be implemented using compiler intrinsics + +[^5]: `long double` values correspond to 128-bit IEEE-754 quad-precision binary128 values. These are not currently +supported, and we have no plans to support them in the near term. Should we ever provide such support, we will do +so using compiler intrinsics. + +[^6]: A null pointer (for all types) always has the value zero. + +[^7]: Miden's linear memory is word-addressable, not byte-addressable. The `Ptr` type has an `AddressSpace` parameter, +that by default is set to the byte-addressable address space. The compiler translates values of `Ptr` type that are in +this address space, into the Miden-native, word-addressable address space during codegen of load/store operations. See +the section on the memory model below for more details. + +[^8]: An `enum` is `i32` if all members of the enumeration can be represented by an `int`/`unsigned int`, otherwise it +uses i64. + +:::note + +The compiler does not support scalars larger than one word (128 bits) at this time. As a result, anything that is +larger than that must be allocated in linear memory, or in an automatic allocation (function-local memory), and passed +around by reference. + +::: + +The native scalar type for the Miden VM is a "field element", specifically a 64-bit value representing an integer +in the "Goldilocks" field, i.e. `0..(2^64-2^32+1)`. A number of instructions in the VM operate on field elements directly. +However, the native integral/pointer type, i.e. a "machine word", is actually `u32`. This is because a field element +can fully represent 32-bit integers, but not the full 64-bit integer range. Values of `u32` type are valid field element +values, and can be used anywhere that a field element is expected (barring other constraints). + +Miden also has the notion of a "word", not to be confused with a "machine word" (by which we mean the native integral +type used to represent pointers), which corresponds to a set of 4 field elements. Words are commonly used in Miden, +particularly to represent hashes, and a number of VM instructions operate on word-sized operands. As an aside, 128-bit +integer values are represented using a word, or two 64-bit limbs (each limb consisting of two 32-bit limbs). + +All integral types mentioned above, barring field elements, use two's complement encoding. Unsigned integral types +make use of the sign bit to change the value range (i.e. 0..2^32-1, rather than -2^31..2^31-1), but the encoding follows +two's complement rules. + +The Miden VM only has native support for field elements, words, and `u32`; all other types are implemented in software +using intrinsics. + +### Aggregates and unions + +Structures and unions assume the alignment of their most strictly aligned component. Each member is assigned to the +lowest available offset with the appropriate alignment. The size of any object is always a multiple of the object's alignment. +An array uses the same alignment as its elements. Structure and union objects can require padding to meet size and alignment +constraints. The contents of any padding are undefined. + +### Memory model + +Interacting with memory in Miden is quite similar to WebAssembly in some ways: + +* The address space is linear, with addresses starting at zero, and ranging up to 2^32-1 +* There is no memory protection per se, you either have full read/write access, or no access to a specific memory context +* How memory is used is completely up to the program being executed + +This is where it begins to differ though, and takes on qualities unique to Miden (in part, or whole): + +* Certain regions of the address space are "reserved" for special uses, improper use of those regions may result in +undefined behavior. +* Miden has different types of function call instructions: `call` vs `syscall` vs `exec`. The first two +perform a context switch when transferring control to the callee, and the callee has no access to the +caller's memory (and the caller has no access to the callee's memory). As a result, references to memory +cannot be passed from caller to callee in arguments, nor can they be returned from the callee to the caller. +* Most significant of all though, is that Miden does not have byte-addressable memory, it is instead word-addressable, +i.e. every address refers to a full word. +* It is not possible to load a specific field element from a word in memory, unless it happens to be the first element +of the word. Instead, one must load the full word, and drop the elements you don't need. + +This presents some complications, particularly: + +* Most languages assume a byte-oriented memory model, which is not trivially mapped to a word-oriented model +* Simple things, such as taking the address of a field in a struct, and then dereferencing it, cannot be directly +represented in Miden using native pointer arithmetic and `load` instruction. Operations like this must be translated +into instruction sequences that load whole words from memory, extract the data needed, and discard the unused bits. +This makes the choice of where in memory to store something much more important than byte-addressable memory, as +loads of values which are not aligned to element or word boundaries can be quite inefficient in some cases. + +The compiler solves this by providing a byte-addressable IR, and internally translating operations in the IR to the equivalent +sequence of Miden instructions needed to emulate that operation. This translation is done during code generation, and uses +the following semantics to determine how a particular operation gets lowered: + +* A byte-addressable pointer can be emulated in Miden's word-addressable environment using three pieces of information: + - The address of the word containing the first byte of the value, this is a "native" Miden address value + - The index of the field element within that word containing the first byte of the value + - The offset (in bytes) from the start of the 4 byte chunk represented by the selected element, corresponding + to the first byte of the value. Since the chunk is represented as a u32 value, the offset is relative to the + most-significant bit (i.e. the byte with the lowest address is found in bits 55-63, since Miden integers are little-endian) +* This relies on us treating Miden's linear memory as an array of 16-byte chunks of raw memory (each word is 4 field elements, +each element represents a 4-byte chunk). In short, much like translating a virtual memory address to a physical one, we must +translate byte-addressable "virtual" pointers to "real" Miden pointers with enough metadata to be able to extract the data we're +trying to load (or encode the data we're trying to store). + +Because we're essentially emulating byte-addressable memory on word-addressable memory, loads/stores can range from simple and +straightforward, to expensive and complicated, depending on the size and alignment of the value type. The process goes as follows: + +* If the value type is word-aligned, it can be loaded/stored in as few as a single instruction depending on the size of the type +* Likewise if the value type is element-aligned, and the address is word-aligned +* Element-aligned values require some extra instructions to load a full word and drop the unused elements (or in the case of stores, +loading the full word and replacing the element being stored) +* Loads/stores of types with sub-element alignment depend on the alignment of the pointer itself. Element or word-aligned addresses +are still quite efficient to load/store from, but if the first byte of the value occurs in the middle of an element, then the bytes +of that value must be shifted into place (or unused bytes masked out). If the value crosses an element boundary, then the bytes in +both elements must be isolated and shifted into position such that they can be bitwise-OR'd together to obtain the aligned value on +the operand stack. If a value crosses a word boundary, then elements from both words must be loaded, irrelevant ones discarded, the +relevant bytes isolated and shifted into position so that the resulting operand on the stack is aligned and laid out correctly. +* Stores are further complicated by the need to preserve memory that is not being explicitly written to, so values that do not overwrite +a full word or element, require combining bytes from the operand being stored and what currently resides in memory. + +The worst case scenario for an unaligned load or store involves a word-sized type starting somewhere in the last element of the first +word. This will require loading elements from three consecutive words, plus a lot of shuffling bits around to get the final, aligned +word-sized value on the operand stack. Luckily, such operations should be quite rare, as by default all word-sized scalar types are +word-aligned or element-aligned, so an unaligned load or store would require either a packed struct, or a type such as an array of +bytes starting at some arbitrary address. In practice, most loads/stores are likely to be element-aligned, so most overhead from +emulation will come from values which cross an element or word boundary. + +## Function calls + +This section describes the conventions followed when executing a function call via `exec`, including how arguments are passed on the +operand stack, stack frames, etc. Later, we'll cover the differences when executing calls via `call` or `syscall`. + +### Locals and the stack frame + +Miden does not have registers in the style of hardware architectures. Instead it has an operand stack, on which an arbitrary number of +operands may be stored, and local variables. In both cases - an operand on the operand stack, or a single local variable - the value +type is nominally a field element, but it is easier to reason about them as untyped element-sized values. The operand stack is used +for function arguments, return values, temporary variables, and scratch space. Local variables are not always used, but are typically +used to hold multiply-used values which you don't want to keep on the operand stack, function-scoped automatic allocations (i.e. `alloca`), +and other such uses. + +Miden does not have a stack frame per se. When you call a procedure in Miden Assembly, any local variables declared by that procedure +are allocated space in a reserved region of linear memory in a single consecutive chunk. However, there is no stack or frame pointer, +and because Miden is a Harvard architecture machine, there are no return addresses. Instead, languages (such as C) which have the concept +of a stack frame with implications for the semantics of say, taking the address of a local variable, will need to emit code in function +prologues and epilogues to maintain a shadow stack in Miden's linear memory. If all you need is local variables, you can get away with +leaning on Miden's notion of local variables without implementing a shadow stack. + +Because there are no registers, the notion of callee-saved or caller-saved registers does not have a direct equivalent in Miden. However, +in its place, a somewhat equivalent set of rules defines the contract between caller and callee in terms of the state of the operand stack, +those are described below in the section covering the operand stack. + +#### The shadow stack + +Miden is a [Harvard](https://en.wikipedia.org/wiki/Harvard_architecture) architecture; as such, code and data are not in the same memory +space. More precisely, in Miden, code is only addressable via the hash of the MAST root of that code, which must correspond to code that +has been loaded into the VM. The hash of the MAST root of a function can be used to call that function both directly and indirectly, but +that is the only action you can take with it. Code can not be generated and called on the fly, and it is not stored anywhere that is +accessible to code that is currently executing. + +One consequence of this is that there are no return addresses or instruction pointers visible to executing code. The runtime call stack is +managed by the VM itself, and is not exposed to executing code in any way. This means that address-taken local C variables need to be on a +separate stack in linear memory (which we refer to as a "shadow stack"). Not all functions necessarily require a frame in the shadow stack, +as it cannot be used to perform unwinding, so only functions which have locals require a frame. + +The Miden VM actually provides some built-in support for stack frames when using Miden Assembly. Procedures which are declared with some +number of locals, will be automatically allocated sufficient space for those locals in a reserved region of linear memory when called. If +you use the `locaddr` instruction to get the actual address of a local, that address can be passed as an argument to callees (within the +constraints of the callee's calling convention). + +Languages with more elaborate requirements with regard to the stack will need to implement their own shadow stack, and emit code in function +prologues/epilogues to manage it. + +#### The operand stack + +The Miden virtual machine is a stack machine, not a register machine. Rather than having a fixed set of registers that are used to +store and manipulate scalar values, the Miden VM has the operand stack, which can hold an arbitrary number of operands (where each +operand is a single field element), of which the first 16 can be directly manipulated using special stack instructions. The operand +stack is, as the name implies, a last-in/first-out data structure. + +The following are basic rules all conventions are expected to follow with regard to the operand stack: + +1. The state of the operand stack from the point of view of the caller should be preserved, with two exceptions: + - The callee is expected to consume all of its arguments, and the caller will expect those operands to be gone when control is returned to it + - If the callee signature declares a return value, the caller expects to see that on top of the stack when control is returned to it +2. No more than 16 elements of the operand stack may be used for passing arguments. If more than that is required to represent all of the arguments, +then one of the following must happen: + - Spill to stack frame: in this scenario, up to 15 elements of the operand stack are used for arguments, and the remaining element is used to hold + a pointer to a local variable in the caller's stack frame. That local variable is a struct whose fields are the spilled arguments, appearing in + the same order as they would be passed. The callee must use the pointer it is given to compute the effective address for each spilled argument + that it wishes to access. + - Spill to heap: this is basically identical to the approach above, except the memory is allocated from the global heap, rather than using memory + associated with the caller's stack frame. + - Spill to the advice provider: in this scenario, 12 elements of the stack are used for arguments, and the remaining 4 are used to hold a hash + which refers to the remaining arguments on the advice provider stack. The callee must arrange to fetch the spilled arguments from the advice + provider using that hash. + +#### Function signatures + +Miden Abstract Syntax Trees (MASTs) do not have any notion of functions, and as such are not aware of parameters, return values, etc. For +this document, that's not a useful level of abstraction to examine. Even a step higher, Miden Assembly (MASM) has functions (procedures +in MASM parlance), but no function signature, i.e. given a MASM procedure, there is no way to know how many arguments it expects, how +many values it returns, let alone the types of arguments/return values. Instead, we're going to specify calling conventions in terms of +Miden IR, which has a fairly expressive type system more or less equivalent to that of LLVM, and how that translates to Miden primitives. + +Functions in Miden IR always have a signature, which specifies the following: + +* The calling convention required to call the function +* The number and types of the function arguments +* The type of value, if any, returned by the function, and whether it is returned by value or reference + +The following table relates IR types to how they are expected to be passed from the caller to the callee, and vice versa: + +Type | Parameter | Result | +--------------------------|---------------|----------| +scalar | direct | direct | +empty struct or union[^1] | ignored | ignored | +scalar struct or union[^2] | direct | direct | +other struct or union | indirect | indirect | +array | indirect | N/A | + +[^1]: Zero-sized types have no representation in memory, so they are ignored/skipped + +[^2]: Any struct or union that recursively (including through nested structs, +unions, and arrays) contains just a single scalar value and is not specified to +have greater than natural alignment. + +The compiler will automatically generate code that follows these rules, but if emitting MASM from your own backend, it is necessary to do so manually. +For example, a function whose signature specifies that it returns a non-scalar struct by value, must actually be written such that it expects to receive +a pointer to memory allocated by the caller sufficient to hold the return value, as the first parameter of the function (i.e. the parameter is prepended +to the parameter list). When returning, the function must write the return value to that pointer, rather than returning it on the operand stack. In this +example, the return value is returned indirectly (by reference). + +A universal rule is that the arguments are passed in reverse order, i.e. the first argument in the parameter list of a function will be on top of the +operand stack. This is different than many Miden instructions which seemingly use the opposite convention, e.g. `add`, which expects the right-hand +operand on top of the stack, so `a + b` is represented like `push a, push b, add`. If we were to implement `add` as a function, it would instead be +`push b, push a, exec.add`. The rationale behind this is that, in general, the more frequently used arguments appear earlier in the parameter list, +and thus we want those closer to the top of the operand stack to reduce the amount of stack manipulation we need to do. + +Arguments/return values are laid out on the operand stack just like they would be as if you had just loaded it from memory, so all arguments are aligned, +but may span multiple operands on the operand stack as necessary based on the size of the type (i.e. a struct type that contains a `u32` and a `i1` +field would require two operands to represent). If the maximum number of operands allowed for the call is reached, any remaining arguments must be +spilled to the caller's stack frame, or to the advice provider. The former is used in the case of `exec`, while the latter is used for `call` +and `syscall`, as caller memory is not accessible to the callee with those instructions. + +While ostensibly 16 elements is the maximum number of operands on the operand stack that can represent function arguments, indirect calls +(`dynexec`/`dyncall`) reserve one element for the memory address of the word holding the callee's MAST root, limiting their arguments to 15 +elements; the compiler rejects indirect callee signatures that exceed this instead of spilling. diff --git a/versioned_docs/version-0.16/reference/compiler/appendix/canonabi-adhocabi-mismatch.md b/versioned_docs/version-0.16/reference/compiler/appendix/canonabi-adhocabi-mismatch.md new file mode 100644 index 00000000..7d319e98 --- /dev/null +++ b/versioned_docs/version-0.16/reference/compiler/appendix/canonabi-adhocabi-mismatch.md @@ -0,0 +1,315 @@ +--- +title: Canonical ABI vs Miden ABI Incompatibility +draft: true +--- + +# Canonical ABI vs Miden ABI incompatibility + +:::note + +This document describes an issue that arises when trying to map the ad-hoc calling convention/ABI +used by various Miden Assembly procedures, such as those comprising the transaction kernel, and +the "canonical" ABI(s) representable in Rust. It proposes a solution to this problem in the form +of _adapter functions_, where the details of a given adapter are one of a closed set of known +ABI _transformation strategies_. + +::: + +## Summary + +The gist of the problem is that in Miden, the size and number of procedure results are only constrained +by the maximum addressable operand stack depth. In most programming languages, particularly those in +which interop is typically performed using some variant of the C ABI (commonly the one described +in the System V specification), the number of results is almost always limited to a single result, +and the size of the result type is almost always limited to the size of a single machine word, in +some cases two. On these platforms, procedure results of greater arity or size are typically handled +by reserving space in the caller's stack frame, and implicitly prepending the parameter list of the +callee with an extra parameter: a pointer to the memory allocated for the return value. The callee +will directly write the return value via this pointer, instead of returning a value in a register. + +In the case of Rust, this means that attempting to represent a procedure that returns multiple values, +or returns a larger-than-machine-word type, such as `Word`, will trigger the implicit transformation +described above, as this is allowed by the standard Rust calling conventions. Since various Miden +procedures that are part of the standard library and the transaction kernel are affected by this, +the question becomes "how do we define bindings for these procedures in Rust?". + +The solution is to have the compiler emit glue code that closes the gap between the two ABIs. It +does so by generating adapter functions, which wrap functions that have an ABI unrepresentable in +Rust, and orchestrate lifting/lowering arguments and results between the adapter and the "real" +function. + +When type signatures are available for all Miden Assembly procedures, we can completely automate +this process. For now, we will require a manually curated list of known procedures, their signatures, +and the strategy used to "adapt" those procedures for binding in Rust. + +## Background + +After analyzing all of the functions in the transaction kernel API, the most common cause of a mismatch +between Miden and Rust ABIs, is due to implicit "sret" parameters, i.e. the transformation mentioned +above which inserts an implicit pointer to the caller's stack frame for the callee to write the return +value to, rather than doing so in a register (or in our case, on the operand stack). This seems to +happen for any type that is larger than 8 bytes (i64). + +:::tip + +For a complete list of the transaction kernel functions, in WIT format, see +[miden.wit](https://github.com/0xMiden/compiler/blob/main/tests/rust-apps-wasm/wit-sdk/sdk/wit/miden.wit). + +::: + +For most transaction kernel functions, the adapter function can be generated automatically using the +pattern recognition and adapter functions described below. + +### Prerequisites + +- The compiler must know the type signature for any function we wish to apply the adapter strategy to + +### Implementation + +The compiler will analyze every component import to determine if that import requires an adapter, +as determined by matching against a predefined set of patterns. The adapter generation will take +place in the frontend, as it has access to all of the needed information, and ensures that we do +not have any transformations or analyses that make decisions on the un-adapted procedure. + +The following pseudo-code can be used to recognize the various Miden ABI patterns: + +```rust +pub enum MidenAbiPattern { + /// Calling this procedure will require an sret parameter on the Rust side, so + /// we need to emit an adapter that will lift/lower calls according to that + /// strategy. + ReturnViaPointer, + /// The underlying procedure is fully representable in Rust, and requires no adaptation. + NoAdapterNeeded, +} + +pub struct MidenAbiPatternRecognition { + pattern: Option, + component_function: ComponentFunctionType, + wasm_core_func: Signature, + tx_kernel_function: Signature, +} + +pub fn recognize_miden_abi_pattern( + component_function: &ComponentFunctionType, + wasm_core_func: &Signature, + tx_kernel_func: &Signature) -> MidenAbiPatternRecognition { + if wasm_core_func == tx_kernel_func { + return MidenAbiPatternRecognition { + pattern: Some(NoAdapterNeeded), + component_function, + wasm_core_function, + tx_kernel_function, + }; + } else if component_function.returns[0].byte_size > 8 && wasm_core_func.params.last() == I32 { + return MidenAbiPatternRecognition { + pattern: Some(ReturnViaPointer), + component_function, + wasm_core_function, + tx_kernel_function, + }; + } else { + return MidenAbiPatternRecognition { + pattern: None, + component_function, + wasm_core_function, + tx_kernel_function, + }; + } +} +``` + +The following pseudo-code can then be used to generate the adapter function: + +```rust +pub fn generate_adapter(recognition: MidenAbiPatternRecognition) { + match recognition.pattern { + Some(pattern) => generate_adapter( + pattern, + recognition.component_function, + recognition.wasm_core_function, + recognition.tx_kernel_function + ), + None => use_manual_adapter( + recognition.component_function, + recognition.wasm_core_function, + recognition.tx_kernel_function + ), + } +} + +/// Escape hatch for the cases when the compiler can't generate an adapter function automatically +/// and we need to provide the adapter function manually. +pub fn use_manual_adapter(...) { + // Find and use the manual adapter in the adapter library for the tx_kernel_function +} +``` + +The manual adapter library is a collection of adapter functions that are used when the compiler +can't generate an adapter function automatically so its expected to be provided. The manual adapter +library is a part of the Miden compiler. It is not anticipated that we will have many, or any, of +these; however in the near term we are going to manually map procedures to their adapter strategies, +as we have not yet automated the pattern recognition step. + +### Return-via-pointer adapter + +The return value is expected to be returned by storing its flattened representation in a pointer +passed as an argument. + +Recognize this Miden ABI pattern by looking at the Wasm component function type. If the return value +is bigger than 64 bits, expect the last argument in the Wasm core(HIR) signature to be `i32` (a pointer). + +The adapter function calls the tx kernel function and stores the result in the provided pointer (the +last argument of the Wasm core function). + +Here is the pseudo-code for generating the adapter function for the return-via-pointer Miden ABI +pattern: + +```rust +let ptr = wasm_core_function.params.last(); +let adapter_function = FunctionBuilder::new(wasm_core_function.clone()); +let tx_kernel_function_params = wasm_core_function.params.drop_last(); +let tx_kernel_func_val = adapter_function.call(tx_kernel_function, tx_kernel_function_params); +adapter_function.store(tx_kernel_func_val, ptr); +adapter_function.build(); +``` + +Here is how the adapter might look in pseudo-code for the `add_asset` function: + +```wat +/// Takes an Asset as an argument and returns a new Asset +func wasm_core_add_asset(v0: f64, v1: f64, v2: f64, v3: f64, ptr: i32) { + v4 = call tx_kernel_add_asset(v0, v1, v2, v3); + // v4 is a tuple of 4 f64 values + store v4 in ptr; +} +``` + +### No-op adapter + +No adapter is needed. The Wasm core function type is the same as the tx kernel ad-hoc signature. + +This Miden ABI pattern is selected if no other Miden ABI pattern is applicable and the wasm core function signature is the same as the tx kernel ad-hoc signature. + +For example, the `get_id` function falls under this Miden ABI pattern and its calls will be translated to the tx kernel function calls without any modifications. + +## Transaction kernel functions that require manual adapter functions + +### `get_assets` + +`get_assets:func() -> list` in the `note` interface is the only function that requires attention. +In Canonical ABI, any function that returns a dynamic list of items needs to allocate memory in the caller's +module due to the shared-nothing nature of the Wasm component model. For this case, a `realloc` function +is passed as a part of lift/lower Canonical ABI options for the caller to allocate memory in the caller's +module. + +Here are the signatures of the `get_assets` function in the WIT, core Wasm, and the tx kernel ad-hoc ABI: +Comment from the `miden-base` + +```text +#! Writes the assets of the currently executing note into memory starting at the specified address. +#! +#! Inputs: [dest_ptr] +#! Outputs: [num_assets, dest_ptr] +#! +#! - dest_ptr is the memory address to write the assets. +#! - num_assets is the number of assets in the currently executing note. +``` + +Wasm component function type: +`get-assets: func() -> list;` + +Wasm core signature: +`wasm_core_get_assets(i32) -> ()` + +If we add a new `get_assets_count: func() -> u32;` function to the tx kernel and add the assets count +parameter to the `get_assets` function (`get_assets: func(assets_count: u32) -> list;`) +we should have everything we need to manually write the adapter function for the `get_assets` +function. + +The list is expected to be returned by storing the pointer to its first item in a `ptr` pointer +passed as an argument and item count at `ptr + 4 bytes` address (`ptr` points to two pointers). + +We could try to recognize this Miden ABI pattern by looking at the Wasm component function type. If +the return value is a list, expect the last argument in the Wasm core(HIR) signature to be `i32` +(a pointer). The problem is recognizing the list count parameter in the Wasm core(HIR) signature. + +The adapter function calls allocates `asset_count * item_size` memory via the `realloc` call and +passes the pointer to the newly allocated memory to the tx kernel function. + +Here is how the adapter function might look in pseudo-code for the `get_assets` function: + +```rust +func wasm_core_get_assets(asset_count: u32, ptr_ptr: i32) { + mem_size = asset_count * item_size; + ptr = realloc(mem_size); + (actual_asset_count, ptr) = call tx_kernel_get_assets(ptr); + assert(actual_asset_count == asset_count); + store ptr in ptr_ptr; + store account_count in ptr_ptr + 4; +} +``` + +:::note + +Since the `get_assets` tx kernel function in the current form can trash the provided memory if +the actual assets count differs from the returned by `get_assets_count`, we can introduce the +asset count parameter to the `get_assets` tx kernel function and check that it's the same as the +actual assets count written to memory. + +::: + +## The example of some function signatures + +### `add_asset` (return-via-pointer Miden ABI pattern) + +Comment from the `miden-base` + +```text +#! Add the specified asset to the vault. +#! +#! Panics: +#! - If the asset is not valid. +#! - If the total value of two fungible assets is greater than or equal to 2^63. +#! - If the vault already contains the same non-fungible asset. +#! +#! Stack: [ASSET] +#! Output: [ASSET'] +#! +#! - ASSET' final asset in the account vault is defined as follows: +#! - If ASSET is a non-fungible asset, then ASSET' is the same as ASSET. +#! - If ASSET is a fungible asset, then ASSET' is the total fungible asset in the account vault +#! after ASSET was added to it. +``` + +Wasm component function type: +`add-asset(core-asset) -> core-asset` + +Wasm core signature: +`wasm_core_add_asset(f64, f64, f64, f64, i32) -> ()` +The last `i32` is a pointer to a returned value (`word`) + +Tx kernel ad-hoc signature: +`tx_kernel_add_asset(felt, felt, felt, felt) -> (felt, felt, felt, felt)` + +### `get_id` (no-adapter-needed Miden ABI pattern) + +Comment from the `miden-base` + +```text +#! Returns the account id. +#! +#! Stack: [] +#! Output: [acct_id] +#! +#! - acct_id is the account id. +``` + +Wasm component function type: +`get-id() -> account-id` + +Wasm core signature: +`wasm_core_get_id() -> f64` + +Tx kernel ad-hoc signature: +`tx_kernel_get_id() -> felt` diff --git a/versioned_docs/version-0.16/reference/compiler/appendix/index.md b/versioned_docs/version-0.16/reference/compiler/appendix/index.md new file mode 100644 index 00000000..e2808319 --- /dev/null +++ b/versioned_docs/version-0.16/reference/compiler/appendix/index.md @@ -0,0 +1,12 @@ +--- +title: Appendices +draft: true +--- + +# Appendices + +This section contains supplementary material providing deeper technical insights into the Miden compiler and related systems: + +- [Known Limitations](./known-limitations.md) - Details current constraints and unimplemented features. +- [Calling Conventions](./calling-conventions.md) - Explains function call mechanics and argument passing. +- [Canonical ABI vs Miden ABI](./canonabi-adhocabi-mismatch.md) - Analyzes cross-ABI interoperability challenges. diff --git a/versioned_docs/version-0.16/reference/compiler/appendix/known-limitations.md b/versioned_docs/version-0.16/reference/compiler/appendix/known-limitations.md new file mode 100644 index 00000000..d21d86f1 --- /dev/null +++ b/versioned_docs/version-0.16/reference/compiler/appendix/known-limitations.md @@ -0,0 +1,223 @@ +--- +title: Known Limitations +draft: true +--- + +# Known limitations + +:::tip + +See the [issue tracker](https://github.com/0xMiden/compiler/issues) for information +on known bugs. This document focuses on missing/incomplete features, rather than bugs. + +::: + +The compiler is still in its early stages of development, so there are various features that are +unimplemented, or only partially implemented, and the test suite is still limited in scope, so +we are still finding bugs on a regular basis. We are rapidly improving this situation, but it is +important to be aware of this when using the compiler. + +The features discussed below are broken up into sections, to make them easier to navigate and +reference. + +## Rust language support + +### Floating point types + +- Status: **Unsupported** +- Tracking Issue: N/A +- Release Milestone: N/A + +In order to represent `Felt` "natively" in Rust, we were forced to piggy-back on the `f32` type, +which is propagated through to WebAssembly, and allows us to handle those values specially. + +As a result, floating-point types in Rust are not supported at all. Any attempt to use them will +result in a compilation error. We considered this a fair design tradeoff, as floating point math +is unused/rare in the context in which Miden is used, in comparison to fixed-point or field +arithmetic. In addition, implementing floating-point operations in software on the Miden VM would +be extraordinarily expensive, which generally works against the purpose of using floats in the +first place. + +At this point in time, we have no plans to support floats, but this may change if we are able to +find a better/more natural representation for `Felt` in WebAssembly. + +### Function call indirection + +- Status: **Partially implemented** +- Tracking Issue: [#32](https://github.com/0xMiden/compiler/issues/32) + +This feature corresponds to `call_indirect` in WebAssembly, and is associated with Rust features +such as function pointers, trait objects (which use indirection to call trait methods), and +closures. + +The compiler lowers each locally-defined Wasm `funcref` table through which a `call_indirect` +dispatches (tables that are never dispatched through, which Wasm toolchains routinely emit, are +ignored) to a word-aligned region of linear memory holding two words per table slot: the MAST +root digest of the referenced function, paired with a tag that identifies the function's +signature (an all-zero slot — zero digest, tag 0 — denotes a null entry). The region is +populated at program startup by the component initialization procedure using `procref`, and +`call_indirect` dispatches through it with `dynexec` (i.e. in the caller's memory context) after +two deterministic runtime checks that mirror Wasm's `call_indirect` semantics: + +- a bounds check on the table index, which traps with an "indirect call: function table index + out of bounds" assertion failure; +- a signature check comparing the slot's tag with the signature expected at the call site, + which traps with an "indirect call: callee signature mismatch or null function reference" + assertion failure. Null slots hold the reserved tag 0, so this check also produces Wasm's + uninitialized-element trap. + +The following limitations remain: + +- The signature and null checks share one assertion message (see above), rather than trapping + with Wasm's distinct "indirect call type mismatch" and "uninitialized element" traps. +- Unlike a Wasm table, which lives outside the program's addressable memory, the lowered table + is a region of ordinary linear memory: a wild out-of-bounds write (only possible through + code whose behavior is already undefined) can overwrite a slot and its tag, bypassing the + signature check. A corrupted slot can still never escape the program: `dynexec` resolves the + slot's first word as a MAST root digest, so it either matches a procedure already compiled + into the program or fails execution. +- Imported tables, non-`funcref` tables, element segments with `global.get`-relative offsets, the + table mutation ops (`table.set`, `table.get`, `table.grow`, etc.), `ref.func`/`ref.null` as + function body instructions, and `return_call_indirect` are unsupported, and produce a + compile-time error. +- The callee arguments plus the table index must fit in Miden's 16-element operand stack window, + so indirect callee signatures are limited to 15 field elements worth of arguments. +- A `funcref` table entry naming a function the compiler lowers to an inlined operation — the + linker stubs for intrinsics such as `intrinsics::felt::add`, whose Wasm body is a single + `unreachable` — is rejected: the stub has no procedure body whose MAST root a slot could + hold. Calling such a function directly is supported; only taking its address is not. +- For a linker stub naming an intrinsic the compiler lowers to a MASM procedure, the stub's + declared Wasm signature is taken at face value: it is what calls are emitted from and what the + stub's table entries are tagged with, and it is never checked against the signature the + intrinsic actually has. Declaring such a stub with the wrong signature is a program error the + compiler does not diagnose: a wrong parameter or result type produces a mismatched stack + contract at run time, and a wrong number of parameters fails late in compilation rather than + as a diagnostic. + +### Miden SDK + +- Status: **Incomplete** +- Tracking Issue: [#159](https://github.com/0xMiden/compiler/issues/159) and [#158](https://github.com/0xMiden/compiler/issues/158) +- Release Milestone: [Beta 1](https://github.com/0xMiden/compiler/milestone/4) + +The Miden SDK for Rust, is a Rust crate that provides the implementation of native Miden types, as +well as bindings to the Miden standard library and transaction kernel APIs. + +Currently, only a very limited subset of the API surface has had bindings implemented. This means +that there is a fair amount of native Miden functionality that is not yet available from Rust. We +will be expanding the SDK rapidly over the next few weeks and months, but for the time being, if +you encounter a missing API that you need, let us know, so we can ensure it is prioritized above +APIs which are lesser used. + +### Rust/Miden FFI (foreign function interface) and interop + +- Status: **Internal Use Only** +- Tracking Issue: [#304](https://github.com/0xMiden/compiler/issues/304) +- Release Milestone: TBD + +While the compiler has functionality to link against native Miden Assembly libraries, binding +against procedures exported from those libraries from Rust can require glue code to be emitted +by the compiler in some cases, and the set of procedures for which this is done is currently +restricted to a hardcoded whitelist of known Miden procedures. + +This affects any procedure which returns a type larger than `u32` (excluding `Felt`, which for +this purpose has the same size). For example, returning a Miden `Word` from a procedure, a common +return type, is not compatible with Rust's ABI - it will attempt to generate code which allocates +stack space in the caller, which it expects the callee to write to, inserting a new parameter at +the start of the parameter list, and expecting nothing to be returned by value. The compiler handles +situations like these using a set of ABI "transformation strategies", which lift/lower differences +between the Rust and Miden ABIs at call boundaries. + +To expose the FFI machinery for use with any Miden procedure, we need type signatures for those +procedures at a minimum, and in some cases we may require details of the calling convention/ABI. +This metadata does not currently exist, but is on the roadmap for inclusion into Miden Assembly +and Miden packaging. Once present, we can open up the FFI for general use. + +## Core Miden functionality + +### Dynamic procedure invocation + +- Status: **Partially implemented** +- Tracking Issue: [#32](https://github.com/0xMiden/compiler/issues/32) + +This is the mechanism behind [Function Call Indirection](#function-call-indirection) described +above. Same-context indirect calls are lowered to `dynexec`: the VM pops a word-aligned memory +address from the operand stack, reads the word at that address as the MAST root of the callee, +and transfers control to it, so the callee observes its arguments in the normal order with no +extra stack fixup (older VM versions kept the callee hash on the operand stack, which would have +required per-callee stubs; that design is obsolete). + +Cross-context indirect calls via `dyncall` are not lowered yet; they additionally depend on +[Cross-Context Procedure Invocation](#cross-context-procedure-invocation). + +### Cross-context procedure invocation + +- Status: **Unimplemented** +- Tracking Issue: [#303](https://github.com/0xMiden/compiler/issues/303) +- Release Milestone: [Beta 2](https://github.com/0xMiden/compiler/milestone/5) + +This is required in order to support representing Miden accounts and note scripts in Rust, and +compilation to Miden Assembly. + +Currently, you can write code in Rust that is very close to how accounts and note scripts will +look like in the language, but it is not possible to actually implement either of those in Rust +today. The reasons for this are covered in depth in the tracking issue linked above, but to +briefly summarize, the primary issue has to do with the fact that Rust programs are compiled +for a "shared-everything" environment, i.e. you can pass references to memory from caller to +callee, write to caller memory from the callee, etc. In Miden however, contexts are "shared-nothing" +units of isolation, and thus cross-context operations, such as performing a `call` from a note script +to a method on an account, are not compatible with the usual calling conventions used by Rust and +LLVM. + +The solution to this relies on compiling the Rust code for the `wasm32-wasip2` target, which emits +a new kind of WebAssembly module, known as a _component_. These components adhere to the rules of +the [WebAssembly Component Model](https://component-model.bytecodealliance.org/). Of primary +interest to us, is the fact that components in this model are "shared-nothing", and the ABI used to +communicate across component boundaries, is specially designed to enforce shared-nothing semantics +on caller and callee. In addition to compiling for a specific Wasm target, we also rely on some +additional tooling for describing component interfaces, types, and generating Rust bindings for +those descriptions, to ensure that calls across the boundary remain opaque, even to the linker, +which ensures that the assumptions of the caller and callee with regard to what address space they +operate in are preserved (i.e. a callee can never be inlined into the caller, and thus end up +executing in the caller's context rather than the expected callee context). + +This is one of our top priorities, as it is critical to be able to use Rust to compile code for +the Miden rollup, but it is also the most complex feature on our roadmap, hence why it is scheduled +for our Beta 2 milestone, rather than Beta 1 (the next release), as it depends on multiple other +subfeatures being implemented first. + +## Packaging + +### Package format + +- Status: **Experimental** +- Tracking Issue: [#121](https://github.com/0xMiden/compiler/issues/121) +- Release Milestone: [Beta 1](https://github.com/0xMiden/compiler/milestone/4) + +This feature represents the ability to compile and distribute a single artifact that contains +the compiled MAST, and all required and optional metadata to make linking against, and executing +packages as convenient as a dynamic library or executable. + +The compiler currently produces, by default, an experimental implementation of a package format +that meets the minimum requirements to support libraries and programs compiled from Rust: + +- Name and semantic version information +- Content digest +- The compiled MAST and metadata about the procedures exported from it +- Read-only data segments and their hashes (if needed by the program, used to load data into the + advice provider when a program is loaded, and to write those segments into linear memory when the + program starts) +- Dependency information (optional, specifies what libraries were linked against during compilation) +- Debug information (optional) + +Use a compatible `miden-debug` executable to load and execute `.masp` packages, including their +metadata and dependencies. The compiler's `midenc` executable compiles artifacts; execution and +debugging are provided by the separate debugger. + +```shell +miden-debug program.masp --inputs inputs.toml +``` + +See [Debugging programs](../guides/debugger.md) for installation, interactive use, and command-script +execution. Keep the debugger's package and VM versions compatible with the compiler that produced +the artifact; a package produced by another toolchain version may require that toolchain's debugger. diff --git a/versioned_docs/version-0.16/reference/compiler/guides/_category_.yml b/versioned_docs/version-0.16/reference/compiler/guides/_category_.yml new file mode 100644 index 00000000..9375afab --- /dev/null +++ b/versioned_docs/version-0.16/reference/compiler/guides/_category_.yml @@ -0,0 +1,4 @@ +label: Guides +# Determines where this documentation section appears relative to other sections in the parent folder +position: 3 +collapsed: true diff --git a/versioned_docs/version-0.16/reference/compiler/guides/debugger.md b/versioned_docs/version-0.16/reference/compiler/guides/debugger.md new file mode 100644 index 00000000..e9dd386f --- /dev/null +++ b/versioned_docs/version-0.16/reference/compiler/guides/debugger.md @@ -0,0 +1,250 @@ +--- +title: Debugging programs +sidebar_position: 5 +--- + +# Debugging programs + +The separate `miden-debug` executable loads compiled `.masp` packages and provides an interactive +terminal UI, a command-line REPL, and non-interactive command scripts. + +## Getting started + +Install a debugger version compatible with the compiler's package and VM dependencies. This +checkout pins `miden-debug` 0.10.3 in `Cargo.lock`: + +```shell +cargo install miden-debug --version 0.10.3 --locked +``` + +The default features include the TUI and REPL. When using another compiler release, use its +matching toolchain/debugger version. + +The debugger is launched by executing `miden-debug`, and giving it a path to a program compiled by `midenc`. See [Program Inputs](#program-inputs) for information on how to provide inputs to the program you wish to debug. Run `miden-debug --help` for more detailed usage documentation. + +Add `--repl` to use debugger commands in a terminal instead of the TUI. + +## Example + +```shell +# Compile a program to MAST from a rustc-generated Wasm module +midenc foo.wasm -o foo.masp + +# Load that program into the debugger and start executing it +miden-debug foo.masp +``` + +## Non-interactive execution + +Create a command file, for example `run.commands`: + +```text +continue +stack +quit +``` + +Run the program to completion and print its operand stack: + +```shell +miden-debug foo.masp --commands run.commands -- 10 +``` + +Command scripts use the REPL command syntax. Use `miden-debug --repl foo.masp` and enter `help` +to explore the commands interactively. + +## Program inputs + +To pass arguments to the program on the operand stack, or via the advice provider, you have two +options, depending on the needs of the program: + +1. Pass arguments to `miden-debug` in the same order you wish them to appear on the stack. That + is, the first argument you specify will be on top of the stack, and so on. +2. Specify a configuration file from which to load inputs for the program, via the `--inputs` option. + +### Via command line + +To specify the contents of the operand stack, you can do so by following the raw arguments separator `--`. +Each operand must be a valid field element value, in either decimal or hexadecimal format. For example: + +```shell +miden-debug foo.masp -- 1 2 0xdeadbeef +``` + +If you pass arguments via the command line in conjunction with `--inputs`, then the command line arguments +will be used instead of the contents of the `inputs.stack` option (if set). This lets you specify a baseline +set of inputs, and then try out different arguments using the command line. + +### Via inputs config + +While simply passing operands to the `miden-debug` command is useful, it only allows you to specify +inputs to be passed via operand stack. To provide inputs via the advice provider, you will need to use +the `--inputs` option. The configuration file expected by `--inputs` also lets you tweak the execution +options for the VM, such as the maximum and expected cycle counts. + +An example configuration file looks like so: + +```toml +# This section is used for execution options +[options] +max_cycles = 5000 +expected_cycles = 4000 + +# This section is the root table for all inputs +[inputs] +# Specify elements to place on the operand stack, leftmost element will be on top of the stack +stack = [1, 2, 0xdeadbeef] + +# This section contains input options for the advice provider +[inputs.advice] +# Specify elements to place on the advice stack, leftmost element will be on top +stack = [1, 2, 3, 4] + +# The `inputs.advice.map` section is a list of advice map entries that should be +# placed in the advice map before the program is executed. Entries with duplicate +# keys are handled on a last-write-wins basis. +[[inputs.advice.map]] +# The key for this entry in the advice map +digest = '0x3cff5b58a573dc9d25fd3c57130cc57e5b1b381dc58b5ae3594b390c59835e63' +# The values to be stored under this key +values = [1, 2, 3, 4] + +[[inputs.advice.map]] +digest = '0x20234ee941e53a15886e733cc8e041198c6e90d2a16ea18ce1030e8c3596dd38' +values = [5, 6, 7, 8] +``` + +## Usage + +Once started, you will be dropped into the main debugger UI, stopped at the first cycle of +the program. The UI is organized into pages and panes, with the main/home page being the +one you get dropped into when the debugger starts. The home page contains the following panes: + +- Source Code - displays source code for the current instruction, if available, with + the relevant line and span highlighted, with syntax highlighting (when available) +- Disassembly - displays the 5 most recently executed VM instructions, and the current + cycle count +- Stack Trace - displays a stack trace for the current instruction, if the program was + compiled with tracing enabled. If frames are unavailable, this pane may be empty. +- Operand Stack - displays the contents of the operand stack and its current depth +- Breakpoints - displays the set of current breakpoints, along with how many were hit + at the current instruction, when relevant + +### Keyboard shortcuts + +On the home page, the following keyboard shortcuts are available: + +| Shortcut | Mnemonic | Description | +| -------- | -------------- | ------------------------------------------------------------------------------------------------------- | +| `q` | quit | exit the debugger | +| `h` | next pane | cycle focus to the next pane | +| `l` | prev pane | cycle focus to the previous pane | +| `s` | step | advance the VM one cycle | +| `n` | step next | advance the VM to the next instruction | +| `c` | continue | advance the VM to the next breakpoint, else to completion | +| `e` | exit frame | advance the VM until we exit the current call frame, a breakpoint is triggered, or execution terminates | +| `d` | delete | delete an item (where applicable, e.g. the breakpoints pane) | +| `:` | command prompt | bring up the command prompt (see below for details) | + +When various panes have focus, additional keyboard shortcuts are available, in any pane +with a list of items, or multiple lines (e.g. source code), `j` and `k` (or the up and +down arrows) will select the next item up and down, respectively. As more features are +added, I will document their keyboard shortcuts below. + +### Commands + +From the home page, typing `:` will bring up the command prompt in the footer pane. + +You will know the prompt is active because the keyboard shortcuts normally shown there will +no longer appear, and instead you will see the prompt, starting with `:`. It supports any +of the following commands: + +| Command | Aliases | Action | Description | +| ------------ | ------------ | ----------------- | --------------------------------------------------------------------- | +| `quit` | `q` | quit | exit the debugger | +| `debug` | | show debug log | display the internal debug log for the debugger itself | +| `reload` | | reload program | reloads the program from disk, and resets the UI (except breakpoints) | +| `breakpoint` | `break`, `b` | create breakpoint | see [Breakpoints](#breakpoints) | +| `read` | `r` | read memory | inspect linear memory (see [Reading Memory](#reading-memory) | + +## Breakpoints + +One of the most common things you will want to do with the debugger is set and manage breakpoints. +Using the command prompt, you can create breakpoints by typing `b` (or `break` or `breakpoint`), +followed by a space, and then the desired breakpoint expression to do any of the following: + +- Break at an instruction which corresponds to a source file (or file and line) whose name/path + matches a pattern +- Break at the first instruction which causes a call frame to be pushed for a procedure whose name + matches a pattern +- Break any time a specific opcode is executed +- Break at the next instruction +- Break after N cycles +- Break at CYCLE + +The syntax for each of these can be found below, in the same order (shown using `b` as the command): + +| Expression | Description | +| --------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | +| `b FILE[:LINE]` | Break when an instruction with a source location in `FILE` (a glob pattern)
_and_ that occur on `LINE` (literal, if provided) are hit. | +| `b in NAME` | Break when the glob pattern `NAME` matches the fully-qualified procedure name
containing the current instruction | +| `b for OPCODE` | Break when an instruction with opcode `OPCODE` is exactly matched
(including immediate values) | +| `b next` | Break on the next instruction | +| `b after N` | Break after `N` cycles | +| `b at CYCLE` | Break when the cycle count reaches `CYCLE`.
If `CYCLE` has already occurred, this has no effect | + +When a breakpoint is hit, it will be highlighted, and the breakpoint window will display the number +of hit breakpoints in the lower right. + +After a breakpoint is hit, it expires if it is one of the following types: + +- Break after N +- Break at CYCLE +- Break next + +When a breakpoint expires, it is removed from the breakpoint list on the next cycle. + +## Reading memory + +Another useful diagnostic task is examining the contents of linear memory, to verify that expected +data has been written. You can do this via the command prompt, using `r` (or `read`), followed by +a space, and then the desired memory address and options: + +The format for read expressions is `:r ADDR [OPTIONS..]`, where `ADDR` is a memory address in +decimal or hexadecimal format (the latter requires the `0x` prefix). The `read` command supports +the following for `OPTIONS`: + +| Option | Alias | Values | Default | Description | +| ---------------- | ----- | --------------------------------------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------- | +| `-mode MODE` | `-m` | `words` (`word`, `w`), `bytes` (`byte`, `b`) | `words` | Specify a memory addressing mode | +| `-format FORMAT` | `-f` | `decimal` (`d`), `hex` (`x`), `binary` (`bin`, `b`) | `decimal` | Specify the format used to print integral values | +| `-count N` | `-c` | | `1` | Specify the number of units to read | +| `-type TYPE` | `-t` | See [Types](#types) | `word` | Specify the type of value to read
This also has the effect of modifying the default `-format` and unit size for `-count` | + +Any invalid combination of options, or invalid syntax, will display an error in the status bar. + +### Types + +| Type | Description | +| ------------------ | -------------------------------------------------- | +| `iN` | A signed integer of `N` bits | +| `uN` | An unsigned integer of `N` bits | +| `felt` | A field element | +| `word` | A Miden word, i.e. an array of four field elements | +| `ptr` or `pointer` | A 32-bit memory address (implies `-format hex`) | + +## Roadmap + +The following are some features planned for the near future: + +- **Watchpoints**, i.e. cause execution to break when a memory store touches a specific address +- **Conditional breakpoints**, i.e. only trigger a breakpoint when an expression attached to it + evaluates to true +- More DYIM-style breakpoints, i.e. when breaking on first hitting a match for a file or + procedure, we probably shouldn't continue to break for every instruction to which that + breakpoint technically applies. Instead, it would make sense to break and then temporarily + disable that breakpoint until something changes that would make breaking again useful. + This will rely on the ability to disable breakpoints, not delete them, which we don't yet + support. +- More robust type support in the `read` command +- Display procedure locals and their contents in a dedicated pane diff --git a/versioned_docs/version-0.16/reference/compiler/guides/develop_miden_in_rust.md b/versioned_docs/version-0.16/reference/compiler/guides/develop_miden_in_rust.md new file mode 100644 index 00000000..2af02054 --- /dev/null +++ b/versioned_docs/version-0.16/reference/compiler/guides/develop_miden_in_rust.md @@ -0,0 +1,45 @@ +--- +title: Developing Miden Programs in Rust +sidebar_position: 3 +--- + +# Developing Miden programs in Rust + +This chapter will walk through how to develop Miden programs in Rust using the standard library +provided by the `miden-stdlib-sys` crate (see the +[README](https://github.com/0xMiden/compiler/blob/main/sdk/stdlib-sys/README.md). + +## Getting started + +Import the standard library from the `miden-stdlib-sys` crate: + +```rust +use miden_stdlib_sys::*; +``` + +## Using `Felt` (field element) type + +The `Felt` type is a field element type that is used to represent the field element values of the +Miden VM. + +To initialize a `Felt` value from an integer constant checking the range at compile time, use the +`felt!` macro: + +```rust +let a = felt!(42); +``` + +Otherwise, use the `Felt::new` constructor: + +```rust +let a = Felt::new(some_integer_var).unwrap(); +``` + +The constructor returns an error if the value is not a valid field element, e.g. if it is not in the +range `0..=M` where `M` is the modulus of the field (2^64 - 2^32 + 1). + +The `Felt` type implements the standard arithmetic operations, e.g. addition, subtraction, +multiplication, division, etc. which are accessible through the standard Rust operators `+`, `-`, +`*`, `/`, etc. All arithmetic operations are wrapping, i.e. performed modulo `M`. + +TODO: Add examples of using operations on `Felt` type and available functions (`assert*`, etc.). diff --git a/versioned_docs/version-0.16/reference/compiler/guides/develop_miden_rollup_accounts_and_note_scripts_in_rust.md b/versioned_docs/version-0.16/reference/compiler/guides/develop_miden_rollup_accounts_and_note_scripts_in_rust.md new file mode 100644 index 00000000..ca0e6324 --- /dev/null +++ b/versioned_docs/version-0.16/reference/compiler/guides/develop_miden_rollup_accounts_and_note_scripts_in_rust.md @@ -0,0 +1,8 @@ +--- +title: Developing Miden Rollup Accounts and Note Scripts in Rust +sidebar_position: 4 +--- + +# Developing Miden rollup accounts and note scripts in Rust + +This chapter walks you through how to develop Miden rollup accounts and note scripts in Rust using the Miden SDK crate. diff --git a/versioned_docs/version-0.16/reference/compiler/guides/index.md b/versioned_docs/version-0.16/reference/compiler/guides/index.md new file mode 100644 index 00000000..413bb0ac --- /dev/null +++ b/versioned_docs/version-0.16/reference/compiler/guides/index.md @@ -0,0 +1,13 @@ +--- +title: Guides +sidebar_position: 1 +--- + +# Guides + +This section contains practical guides for working with the Miden compiler: + +* [Compiling Rust to WebAssembly](./rust_to_wasm.md) - Walks through compiling Rust code to WebAssembly modules +* [WebAssembly to Miden Assembly](./wasm_to_masm.md) - Explains compiling Wasm modules to Miden Assembly +* [Developing Miden Programs in Rust](./develop_miden_in_rust.md) - Demonstrates using Rust with Miden's standard library +* [Developing Miden Rollup Accounts and Note Scripts in Rust](./develop_miden_rollup_accounts_and_note_scripts_in_rust.md) - Shows Rust development for Miden rollup components diff --git a/versioned_docs/version-0.16/reference/compiler/guides/rust_to_wasm.md b/versioned_docs/version-0.16/reference/compiler/guides/rust_to_wasm.md new file mode 100644 index 00000000..1b420346 --- /dev/null +++ b/versioned_docs/version-0.16/reference/compiler/guides/rust_to_wasm.md @@ -0,0 +1,181 @@ +--- +title: Rust to WebAssembly +sidebar_position: 1 +--- + +# Compiling Rust To WebAssembly + +This chapter will walk you through compiling a Rust crate to a WebAssembly (Wasm) module +in binary (i.e. `.wasm`) form. The Miden compiler has a frontend which can take such +modules and compile them into Miden Assembly, which will be covered in the next chapter. + +## Setup + +First, let's set up a simple Rust project that contains an implementation of the Fibonacci +function (I know, it's overdone, but we're trying to keep things as simple as possible to +make it easier to show the results at each step, so bear with me): + +Start by creating a new library crate: + + cargo new --lib wasm-fib && cd wasm-fib + +To compile to WebAssembly, you must have the appropriate Rust toolchain installed, so let's add +a toolchain file to our project root so that `rustup` and `cargo` will know what we need, and use +them by default: + + cat <<EOF > rust-toolchain.toml + [toolchain] + channel = "stable" + targets = ["wasm32-wasip1"] + EOF + +Next, edit the `Cargo.toml` file as follows: + +```toml +[package] +name = "wasm-fib" +version = "0.1.0" +edition = "2024" + +[lib] +# Build this crate as a self-contained, C-style dynamic library +# This is required to emit the proper Wasm module type +crate-type = ["cdylib"] + +[dependencies] +# Use a tiny allocator in place of the default one, if we want +# to make use of types in the `alloc` crate, e.g. String. We +# don't need that now, but it's good information to have in hand. +#miden-sdk-alloc = "0.0.5" + +# When we build for Wasm, we'll use the release profile +[profile.release] +# Explicitly disable panic infrastructure on Wasm, as +# there is no proper support for them anyway, and it +# ensures that panics do not pull in a bunch of standard +# library code unintentionally +panic = "abort" +# Enable debug information so that we get useful debugging output +debug = true +# Optimize the output for size +opt-level = "z" +``` + +Most of these things are done to keep the generated code size as small as possible. Miden is a target +where the conventional wisdom about performance should be treated very carefully: we're almost always +going to benefit from less code, even if conventionally that code would be less efficient, simply due +to the difference in proving time accumulated due to extra instructions. That said, there are no hard +and fast rules, but these defaults are good ones to start with. + +:::tip + +We reference a simple bump allocator provided by `miden-sdk-alloc` above, but any simple +allocator will do. The trade offs made by these small allocators are not generally suitable for +long-running, or allocation-heavy applications, as they "leak" memory (generally because they +make little to no attempt to recover freed allocations), however they are very useful for +one-shot programs that do minimal allocation, which is going to be the typical case for Miden +programs. + +::: + +Next, edit `src/lib.rs` as shown below: + +```rust +// Do not link against libstd (i.e. anything defined in `std::`) +#![no_std] + +// However, we could still use some standard library types while +// remaining no-std compatible, if we uncommented the following lines: +// +// extern crate alloc; +// use alloc::{string::String, vec::Vec}; + +// If we wanted to use the types mentioned above, it would also be +// a good idea to use the allocator we pulled in as a dependency +// in Cargo.toml, like so: +//#[global_allocator] +//static ALLOC: miden_sdk_alloc::BumpAlloc = miden_sdk_alloc::BumpAlloc::new(); + +// Required for no-std crates +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + // Compiles to a trap instruction in WebAssembly + core::arch::wasm32::unreachable() +} + +// Marking the function no_mangle ensures that it is exported +// from the compiled binary as `fib`, otherwise it would have +// a mangled name that has no stable form. +// +// You can specify a different name from the library than the +// name in the source code using the `#[unsafe(export_name = "foo")]` +// attribute, which will make the function callable as `foo` +// externally (in this example) +#[unsafe(no_mangle)] +pub fn fib(n: u32) -> u32 { + let mut a = 0; + let mut b = 1; + for _ in 0..n { + let c = a + b; + a = b; + b = c; + } + a +} +``` + +This exports our `fib` function from the library, making it callable from within a larger Miden program. + +All that remains is to compile to WebAssembly: + + cargo build --release --target=wasm32-wasip1 + +This places a `wasm_fib.wasm` file under the `target/wasm32-wasip1/release/` directory, which +we can then examine with [wasm2wat](https://github.com/WebAssembly/wabt) to set the code we generated: + + wasm2wat target/wasm32-wasip1/release/wasm_fib.wasm + +Which dumps the following output (may differ slightly on your machine, depending on the specific compiler version): + +```wat +(module $wasm_fib.wasm + (type (;0;) (func (param i32) (result i32))) + (func $fib (type 0) (param i32) (result i32) + (local i32 i32 i32) + i32.const 0 + local.set 1 + i32.const 1 + local.set 2 + loop (result i32) ;; label = @1 + local.get 2 + local.set 3 + block ;; label = @2 + local.get 0 + br_if 0 (;@2;) + local.get 1 + return + end + local.get 0 + i32.const -1 + i32.add + local.set 0 + local.get 1 + local.get 3 + i32.add + local.set 2 + local.get 3 + local.set 1 + br 0 (;@1;) + end) + (memory (;0;) 16) + (global $__stack_pointer (mut i32) (i32.const 1048576)) + (export "memory" (memory 0)) + (export "fib" (func $fib))) +``` + +Success! + +## Next steps + +In [Compiling WebAssembly to Miden Assembly](wasm_to_masm.md), we walk through how to take the +WebAssembly module we just compiled, and lower it to Miden Assembly using `midenc`! diff --git a/versioned_docs/version-0.16/reference/compiler/guides/wasm_to_masm.md b/versioned_docs/version-0.16/reference/compiler/guides/wasm_to_masm.md new file mode 100644 index 00000000..a88e3ae8 --- /dev/null +++ b/versioned_docs/version-0.16/reference/compiler/guides/wasm_to_masm.md @@ -0,0 +1,136 @@ +--- +title: WebAssembly to Miden Assembly +sidebar_position: 2 +--- + +# Compiling WebAssembly to Miden Assembly + +This guide will walk you through compiling a WebAssembly (Wasm) module, in binary form +(i.e. a `.wasm` file), to Miden Assembly (Masm), both in its binary package form (a `.masp` file), +and in textual Miden Assembly syntax form (i.e. a `.masm` file). + +## Setup + +We will be making use of the example crate we created in [Compiling Rust to WebAssembly](rust_to_wasm.md), +which produces a small Wasm module that is easy to examine in Wasm text format, and demonstrates a +good set of default choices for a project compiling to Miden Assembly from Rust. + +In this chapter, we will be compiling Wasm to Masm using the `midenc` executable, so ensure that +you have followed the instructions in the [Getting Started with `midenc`](../usage/midenc.md) guide +and then return here. + +:::note + +While we are using `midenc` for this guide, the more common use case will be to use the +`cargo-miden` Cargo extension to handle the gritty details of compiling from Rust to Wasm +for you. However, the purpose of this guide is to show you what `cargo-miden` is handling +for you, and to give you a foundation for using `midenc` yourself if needed. + +::: + +## Compiling to Miden Assembly + +In the last chapter, we compiled a Rust crate to WebAssembly that contains an implementation +of the Fibonacci function called `fib`, that was emitted to +`target/wasm32-wasip1/release/wasm_fib.wasm`. All that remains is to tell `midenc` to compile this +module to Miden Assembly. + +The compiler emits a `.masp` package containing compiled MAST and its metadata. We will use +`miden-debug` to load and execute it; see [Debugging programs](debugger.md) for installation. + +We also want to examine the Miden Assembly generated by the compiler, so we're going to ask the +compiler to emit both types of artifacts: + +```bash +midenc --emit masm=wasm_fib.masm target/wasm32-wasip1/release/wasm_fib.wasm +``` + +This will compile our Wasm module to a Miden package with the `.masp` extension, and also emit the +textual Masm to `wasm_fib.masm` so we can review it. The `wasm_fib.masp` file will be emitted in the +default output directory, which is the current working directory by default. + +If we dump the contents of `wasm_fib.masm`, we'll see the following generated code: + +```masm +export.fib + push.0 + push.1 + movup.2 + swap.1 + dup.1 + neq.0 + push.1 + while.true + if.true + push.4294967295 + movup.2 + swap.1 + u32wrapping_add + dup.1 + swap.1 + swap.3 + swap.1 + u32wrapping_add + movup.2 + swap.1 + dup.1 + neq.0 + push.1 + else + drop + drop + push.0 + end + end +end +``` + +If you compare this to the WebAssembly text format, you can see that this is a fairly +faithful translation, but there may be areas where we generate sub-optimal Miden Assembly. + +:::note + +At the moment the compiler does only minimal optimization, late in the pipeline during codegen, +and only in an effort to minimize operand stack management code. So if you see an instruction +sequence you think is bad, bring it to our attention, and if it is something that we can solve +as part of our overall optimization efforts, we will be sure to do so. There _are_ limits to +what we can generate compared to what one can write by hand, particularly because Rust's +memory model requires us to emulate byte-addressable memory on top of Miden's word-addressable +memory, however our goal is to keep this overhead within an acceptable bound in the general case, +and easily-recognized patterns that can be simplified using peephole optimization are precisely +the kind of thing we'd like to know about, as those kinds of optimizations are likely to produce +the most significant wins. + +::: + +## Testing with the Miden VM + +Open the compiled package in the debugger with the input `10`: + +```bash +miden-debug wasm_fib.masp -- 10 +``` + +Continue execution to the end and inspect the operand stack; the Fibonacci result should be `55`. +To run without the interactive UI, create `run.commands` containing: + +```text +continue +stack +quit +``` + +Then execute: + +```bash +miden-debug wasm_fib.masp --commands run.commands -- 10 +``` + +See [Debugging programs](debugger.md) for advice inputs, breakpoints, and other execution controls. + +## Next steps + +This guide is not comprehensive, as we have not yet examined in detail the differences between +compiling libraries vs programs, linking together multiple libraries, packages, or discussed some of +the more esoteric compiler options. We will be updating this documentation with those details and +more in the coming weeks and months, so bear with us while we flesh out our guides! diff --git a/versioned_docs/version-0.16/reference/compiler/index.md b/versioned_docs/version-0.16/reference/compiler/index.md new file mode 100644 index 00000000..db89fdd3 --- /dev/null +++ b/versioned_docs/version-0.16/reference/compiler/index.md @@ -0,0 +1,106 @@ +--- +title: Compiler +sidebar_position: 1 +--- + +# Getting Started + +Welcome to the documentation for the Miden compiler toolchain. + +:::caution + +The compiler is currently in an experimental state, and has known bugs and limitations, it is +not yet ready for production usage. However, we'd encourage you to start experimenting with it +yourself, and give us feedback on any issues or sharp edges you encounter. + +::: + +The documentation found here should provide a good starting point for the current capabilities of +the toolchain, however if you find something that is not covered, but is not listed as +unimplemented or a known limitation, please let us know by reporting an issue on the compiler +[issue tracker](https://github.com/0xMiden/compiler/issues). + +## What is provided? + +The compiler toolchain consists of the following primary components: + +- An intermediate representation (IR), which can be lowered by compiler backends wishing to + support Miden as a target. The Miden IR is an SSA IR, much like Cranelift or LLVM, providing a + much simpler path from any given source language (e.g. Rust), to Miden Assembly. It is used + internally by the rest of the Miden compiler suite. +- A WebAssembly (Wasm) frontend for Miden IR. It can handle lowering both core Wasm modules, as + well as basic components using the experimental WebAssembly Component Model. Currently, the Wasm + frontend is known to work with Wasm modules produced by `rustc`, which is largely just what LLVM + produces, but with the shadow stack placed at the start of linear memory rather than after + read-only data. In the future we intend to support more variety in the structure of Wasm modules + we accept, but for the time being we're primarily focused on using this as the path for lowering + Rust to Miden. +- The compiler driver, in the form of the `midenc` executable, and a Rust crate, `midenc-compile` + to allow integrating the compiler into other tools. This plays the same role as `rustc` does in + the Rust ecosystem. +- A Cargo extension, `cargo-miden`, that provides a convenient developer experience for creating + and compiling Rust projects targeting Miden. It contains a project template for a basic Rust crate, + and handles orchestrating `rustc` and `midenc` to compile the crate to WebAssembly, and then to + Miden Assembly. +- A terminal-based interactive debugger, available as the separate `miden-debug` executable, which provides a UI very + similar to `lldb` or `gdb` when using the TUI mode. You can use this to run a program, or step + through it cycle-by-cycle. You can set various types of breakpoints; see the source code, call + stack, and contents of the operand stack at the current program point; as well as interactively + read memory and format it in various ways for display. +- A Miden SDK for Rust, which provides types and bindings to functionality exported from the Miden + standard library, as well as the Miden transaction kernel API. You can use this to access native + Miden features which are not provided by Rust out-of-the-box. The project template generated by + `cargo miden new` automatically adds this as a dependency. + +## What can I do with it? + +That all sounds great, but what can you do with the compiler today? The answer depends a bit on what +aspect of the compiler you are interested in: + +### Rust + +The most practically useful, and interesting capability provided by the compiler currently, is the +ability to compile arbitrary Rust programs to Miden Assembly. See the guides for more information +on setting up and compiling a Rust crate for execution via Miden. + +### WebAssembly + +More generally, the compiler frontend is capable of compiling WebAssembly modules, with some +constraints, to Miden Assembly. As a result, it is possible to compile a wider variety of languages +to Miden Assembly than just Rust, so long as the language can compile to WebAssembly. However, we +do not currently provide any of the language-level support for languages other than Rust, and +have limited ability to provide engineering support for languages other than Rust at this time. + +Our Wasm frontend does not support all of the extensions to the WebAssembly MVP, most notably the +reference types and GC proposals. + +### Miden IR + +If you are interested in compiling to Miden from your own compiler, you can target Miden IR, and +invoke the driver from your compiler to emit Miden artifacts. At this point in time, we don't have +the resources to provide much in the way of engineering support for this use case, but if you find +issues in your efforts to use the IR in your compiler, we would certainly like to know about them! + +We do not currently perform any optimizations on the IR, since we are primarily working with the +output of compiler backends which have already applied optimizations, at this time. This may change +in the future, but for now it is expected that you implement your own optimization passes as needed. + +## Known bugs and limitations + +For the latest information on known bugs and limitations, see the [issue tracker](https://github.com/0xMiden/compiler/issues). + +## Where to start? + +Provided here are a set of guides which are focused on documenting a couple of supported workflows +we expect will meet the needs of most users, within the constraints of the current feature set of +the compiler. If you find that there is something you wish to do that is not covered, and is not +one of our known limitations, please open an issue, and we will try to address the missing docs as +soon as possible. + +## Installation + +To get started, there are a few ways you might use the Miden compiler. Select the one that applies +to you, and the corresponding guide will walk you through getting up and running: + +1. [Using the Cargo extension](usage/cargo-miden.md) +2. [Using the `midenc` executable](usage/midenc.md) diff --git a/versioned_docs/version-0.16/reference/compiler/theme/Admonition/Icon/Danger.tsx b/versioned_docs/version-0.16/reference/compiler/theme/Admonition/Icon/Danger.tsx new file mode 100644 index 00000000..ab14d7de --- /dev/null +++ b/versioned_docs/version-0.16/reference/compiler/theme/Admonition/Icon/Danger.tsx @@ -0,0 +1,19 @@ +import React, { type ReactNode } from "react"; +import type { Props } from "@theme/Admonition/Icon/Danger"; + +export default function AdmonitionIconDanger(props: Props): ReactNode { + return ( + + + + ); +} diff --git a/versioned_docs/version-0.16/reference/compiler/theme/Admonition/Icon/Info.tsx b/versioned_docs/version-0.16/reference/compiler/theme/Admonition/Icon/Info.tsx new file mode 100644 index 00000000..59e48a52 --- /dev/null +++ b/versioned_docs/version-0.16/reference/compiler/theme/Admonition/Icon/Info.tsx @@ -0,0 +1,20 @@ +import React, { type ReactNode } from "react"; +import type { Props } from "@theme/Admonition/Icon/Info"; + +export default function AdmonitionIconInfo(props: Props): ReactNode { + return ( + + + + ); +} diff --git a/versioned_docs/version-0.16/reference/compiler/theme/Admonition/Icon/Note.tsx b/versioned_docs/version-0.16/reference/compiler/theme/Admonition/Icon/Note.tsx new file mode 100644 index 00000000..d7c524b3 --- /dev/null +++ b/versioned_docs/version-0.16/reference/compiler/theme/Admonition/Icon/Note.tsx @@ -0,0 +1,20 @@ +import React, { type ReactNode } from "react"; +import type { Props } from "@theme/Admonition/Icon/Note"; + +export default function AdmonitionIconNote(props: Props): ReactNode { + return ( + + + + ); +} diff --git a/versioned_docs/version-0.16/reference/compiler/theme/Admonition/Icon/Tip.tsx b/versioned_docs/version-0.16/reference/compiler/theme/Admonition/Icon/Tip.tsx new file mode 100644 index 00000000..219bb8d0 --- /dev/null +++ b/versioned_docs/version-0.16/reference/compiler/theme/Admonition/Icon/Tip.tsx @@ -0,0 +1,20 @@ +import React, { type ReactNode } from "react"; +import type { Props } from "@theme/Admonition/Icon/Tip"; + +export default function AdmonitionIconTip(props: Props): ReactNode { + return ( + + + + ); +} diff --git a/versioned_docs/version-0.16/reference/compiler/theme/Admonition/Icon/Warning.tsx b/versioned_docs/version-0.16/reference/compiler/theme/Admonition/Icon/Warning.tsx new file mode 100644 index 00000000..f96398d1 --- /dev/null +++ b/versioned_docs/version-0.16/reference/compiler/theme/Admonition/Icon/Warning.tsx @@ -0,0 +1,20 @@ +import React, { type ReactNode } from "react"; +import type { Props } from "@theme/Admonition/Icon/Warning"; + +export default function AdmonitionIconCaution(props: Props): ReactNode { + return ( + + + + ); +} diff --git a/versioned_docs/version-0.16/reference/compiler/theme/Admonition/Layout/index.tsx b/versioned_docs/version-0.16/reference/compiler/theme/Admonition/Layout/index.tsx new file mode 100644 index 00000000..7b2c170d --- /dev/null +++ b/versioned_docs/version-0.16/reference/compiler/theme/Admonition/Layout/index.tsx @@ -0,0 +1,51 @@ +import React, { type ReactNode } from "react"; +import clsx from "clsx"; +import { ThemeClassNames } from "@docusaurus/theme-common"; + +import type { Props } from "@theme/Admonition/Layout"; + +import styles from "./styles.module.css"; + +function AdmonitionContainer({ + type, + className, + children, +}: Pick & { children: ReactNode }) { + return ( +
+ {children} +
+ ); +} + +function AdmonitionHeading({ icon, title }: Pick) { + return ( +
+ {icon} + {/* {title} */} +
+ ); +} + +function AdmonitionContent({ children }: Pick) { + return children ? ( +
{children}
+ ) : null; +} + +export default function AdmonitionLayout(props: Props): ReactNode { + const { type, icon, title, children, className } = props; + return ( + + {title || icon ? : null} + {children} + + ); +} diff --git a/versioned_docs/version-0.16/reference/compiler/theme/Admonition/Layout/styles.module.css b/versioned_docs/version-0.16/reference/compiler/theme/Admonition/Layout/styles.module.css new file mode 100644 index 00000000..88df7e63 --- /dev/null +++ b/versioned_docs/version-0.16/reference/compiler/theme/Admonition/Layout/styles.module.css @@ -0,0 +1,35 @@ +.admonition { + margin-bottom: 1em; +} + +.admonitionHeading { + font: var(--ifm-heading-font-weight) var(--ifm-h5-font-size) / + var(--ifm-heading-line-height) var(--ifm-heading-font-family); + text-transform: uppercase; +} + +/* Heading alone without content (does not handle fragment content) */ +.admonitionHeading:not(:last-child) { + margin-bottom: 0.3rem; +} + +.admonitionHeading code { + text-transform: none; +} + +.admonitionIcon { + display: inline-block; + vertical-align: middle; + margin-right: 0.4em; +} + +.admonitionIcon svg { + display: inline-block; + height: 1.6em; + width: 1.6em; + fill: var(--ifm-alert-foreground-color); +} + +.admonitionContent > :last-child { + margin-bottom: 0; +} diff --git a/versioned_docs/version-0.16/reference/compiler/theme/Admonition/Type/Caution.tsx b/versioned_docs/version-0.16/reference/compiler/theme/Admonition/Type/Caution.tsx new file mode 100644 index 00000000..b570a37a --- /dev/null +++ b/versioned_docs/version-0.16/reference/compiler/theme/Admonition/Type/Caution.tsx @@ -0,0 +1,32 @@ +import React, {type ReactNode} from 'react'; +import clsx from 'clsx'; +import Translate from '@docusaurus/Translate'; +import type {Props} from '@theme/Admonition/Type/Caution'; +import AdmonitionLayout from '@theme/Admonition/Layout'; +import IconWarning from '@theme/Admonition/Icon/Warning'; + +const infimaClassName = 'alert alert--warning'; + +const defaultProps = { + icon: , + title: ( + + caution + + ), +}; + +// TODO remove before v4: Caution replaced by Warning +// see https://github.com/facebook/docusaurus/issues/7558 +export default function AdmonitionTypeCaution(props: Props): ReactNode { + return ( + + {props.children} + + ); +} diff --git a/versioned_docs/version-0.16/reference/compiler/theme/Admonition/Type/Danger.tsx b/versioned_docs/version-0.16/reference/compiler/theme/Admonition/Type/Danger.tsx new file mode 100644 index 00000000..49901fa9 --- /dev/null +++ b/versioned_docs/version-0.16/reference/compiler/theme/Admonition/Type/Danger.tsx @@ -0,0 +1,30 @@ +import React, {type ReactNode} from 'react'; +import clsx from 'clsx'; +import Translate from '@docusaurus/Translate'; +import type {Props} from '@theme/Admonition/Type/Danger'; +import AdmonitionLayout from '@theme/Admonition/Layout'; +import IconDanger from '@theme/Admonition/Icon/Danger'; + +const infimaClassName = 'alert alert--danger'; + +const defaultProps = { + icon: , + title: ( + + danger + + ), +}; + +export default function AdmonitionTypeDanger(props: Props): ReactNode { + return ( + + {props.children} + + ); +} diff --git a/versioned_docs/version-0.16/reference/compiler/theme/Admonition/Type/Info.tsx b/versioned_docs/version-0.16/reference/compiler/theme/Admonition/Type/Info.tsx new file mode 100644 index 00000000..018e0a16 --- /dev/null +++ b/versioned_docs/version-0.16/reference/compiler/theme/Admonition/Type/Info.tsx @@ -0,0 +1,30 @@ +import React, {type ReactNode} from 'react'; +import clsx from 'clsx'; +import Translate from '@docusaurus/Translate'; +import type {Props} from '@theme/Admonition/Type/Info'; +import AdmonitionLayout from '@theme/Admonition/Layout'; +import IconInfo from '@theme/Admonition/Icon/Info'; + +const infimaClassName = 'alert alert--info'; + +const defaultProps = { + icon: , + title: ( + + info + + ), +}; + +export default function AdmonitionTypeInfo(props: Props): ReactNode { + return ( + + {props.children} + + ); +} diff --git a/versioned_docs/version-0.16/reference/compiler/theme/Admonition/Type/Note.tsx b/versioned_docs/version-0.16/reference/compiler/theme/Admonition/Type/Note.tsx new file mode 100644 index 00000000..c99e0385 --- /dev/null +++ b/versioned_docs/version-0.16/reference/compiler/theme/Admonition/Type/Note.tsx @@ -0,0 +1,30 @@ +import React, {type ReactNode} from 'react'; +import clsx from 'clsx'; +import Translate from '@docusaurus/Translate'; +import type {Props} from '@theme/Admonition/Type/Note'; +import AdmonitionLayout from '@theme/Admonition/Layout'; +import IconNote from '@theme/Admonition/Icon/Note'; + +const infimaClassName = 'alert alert--secondary'; + +const defaultProps = { + icon: , + title: ( + + note + + ), +}; + +export default function AdmonitionTypeNote(props: Props): ReactNode { + return ( + + {props.children} + + ); +} diff --git a/versioned_docs/version-0.16/reference/compiler/theme/Admonition/Type/Tip.tsx b/versioned_docs/version-0.16/reference/compiler/theme/Admonition/Type/Tip.tsx new file mode 100644 index 00000000..18604a5e --- /dev/null +++ b/versioned_docs/version-0.16/reference/compiler/theme/Admonition/Type/Tip.tsx @@ -0,0 +1,30 @@ +import React, {type ReactNode} from 'react'; +import clsx from 'clsx'; +import Translate from '@docusaurus/Translate'; +import type {Props} from '@theme/Admonition/Type/Tip'; +import AdmonitionLayout from '@theme/Admonition/Layout'; +import IconTip from '@theme/Admonition/Icon/Tip'; + +const infimaClassName = 'alert alert--success'; + +const defaultProps = { + icon: , + title: ( + + tip + + ), +}; + +export default function AdmonitionTypeTip(props: Props): ReactNode { + return ( + + {props.children} + + ); +} diff --git a/versioned_docs/version-0.16/reference/compiler/theme/Admonition/Type/Warning.tsx b/versioned_docs/version-0.16/reference/compiler/theme/Admonition/Type/Warning.tsx new file mode 100644 index 00000000..61d9597b --- /dev/null +++ b/versioned_docs/version-0.16/reference/compiler/theme/Admonition/Type/Warning.tsx @@ -0,0 +1,30 @@ +import React, {type ReactNode} from 'react'; +import clsx from 'clsx'; +import Translate from '@docusaurus/Translate'; +import type {Props} from '@theme/Admonition/Type/Warning'; +import AdmonitionLayout from '@theme/Admonition/Layout'; +import IconWarning from '@theme/Admonition/Icon/Warning'; + +const infimaClassName = 'alert alert--warning'; + +const defaultProps = { + icon: , + title: ( + + warning + + ), +}; + +export default function AdmonitionTypeWarning(props: Props): ReactNode { + return ( + + {props.children} + + ); +} diff --git a/versioned_docs/version-0.16/reference/compiler/theme/Admonition/Types.tsx b/versioned_docs/version-0.16/reference/compiler/theme/Admonition/Types.tsx new file mode 100644 index 00000000..2a100190 --- /dev/null +++ b/versioned_docs/version-0.16/reference/compiler/theme/Admonition/Types.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import AdmonitionTypeNote from '@theme/Admonition/Type/Note'; +import AdmonitionTypeTip from '@theme/Admonition/Type/Tip'; +import AdmonitionTypeInfo from '@theme/Admonition/Type/Info'; +import AdmonitionTypeWarning from '@theme/Admonition/Type/Warning'; +import AdmonitionTypeDanger from '@theme/Admonition/Type/Danger'; +import AdmonitionTypeCaution from '@theme/Admonition/Type/Caution'; +import type AdmonitionTypes from '@theme/Admonition/Types'; + +const admonitionTypes: typeof AdmonitionTypes = { + note: AdmonitionTypeNote, + tip: AdmonitionTypeTip, + info: AdmonitionTypeInfo, + warning: AdmonitionTypeWarning, + danger: AdmonitionTypeDanger, +}; + +// Undocumented legacy admonition type aliases +// Provide hardcoded/untranslated retrocompatible label +// See also https://github.com/facebook/docusaurus/issues/7767 +const admonitionAliases: typeof AdmonitionTypes = { + secondary: (props) => , + important: (props) => , + success: (props) => , + caution: AdmonitionTypeCaution, +}; + +export default { + ...admonitionTypes, + ...admonitionAliases, +}; diff --git a/versioned_docs/version-0.16/reference/compiler/theme/Admonition/index.tsx b/versioned_docs/version-0.16/reference/compiler/theme/Admonition/index.tsx new file mode 100644 index 00000000..8f4225da --- /dev/null +++ b/versioned_docs/version-0.16/reference/compiler/theme/Admonition/index.tsx @@ -0,0 +1,21 @@ +import React, {type ComponentType, type ReactNode} from 'react'; +import {processAdmonitionProps} from '@docusaurus/theme-common'; +import type {Props} from '@theme/Admonition'; +import AdmonitionTypes from '@theme/Admonition/Types'; + +function getAdmonitionTypeComponent(type: string): ComponentType { + const component = AdmonitionTypes[type]; + if (component) { + return component; + } + console.warn( + `No admonition component found for admonition type "${type}". Using Info as fallback.`, + ); + return AdmonitionTypes.info!; +} + +export default function Admonition(unprocessedProps: Props): ReactNode { + const props = processAdmonitionProps(unprocessedProps); + const AdmonitionTypeComponent = getAdmonitionTypeComponent(props.type); + return ; +} diff --git a/versioned_docs/version-0.16/reference/compiler/usage/_category_.yml b/versioned_docs/version-0.16/reference/compiler/usage/_category_.yml new file mode 100644 index 00000000..bb98c44f --- /dev/null +++ b/versioned_docs/version-0.16/reference/compiler/usage/_category_.yml @@ -0,0 +1,4 @@ +label: Usage +# Determines where this documentation section appears relative to other sections in the parent folder +position: 2 +collapsed: true diff --git a/versioned_docs/version-0.16/reference/compiler/usage/cargo-miden.md b/versioned_docs/version-0.16/reference/compiler/usage/cargo-miden.md new file mode 100644 index 00000000..b990740d --- /dev/null +++ b/versioned_docs/version-0.16/reference/compiler/usage/cargo-miden.md @@ -0,0 +1,105 @@ +--- +title: As a Cargo extension +sidebar_position: 3 +--- + +# Getting started with Cargo + +As part of the Miden compiler toolchain, we provide a Cargo extension, `cargo-miden`, which provides +a template to spin up a new Miden project in Rust, and takes care of orchestrating `rustc` and +`midenc` to compile the Rust crate to a Miden package. + +## Installation + +:::warning + +Currently, `midenc` (and as a result, `cargo-miden`), requires the nightly Rust toolchain, so +make sure you have it installed first: + +```bash +rustup toolchain install nightly-2026-09-01 +``` + +NOTE: You can also use the latest nightly, but the specific nightly shown here is known to +work. + +::: + +To install the extension: + +```bash +cargo +nightly-2026-09-01 install cargo-miden --locked + +``` + +This will take a minute to compile, but once complete, you can run `cargo help miden` or just +`cargo miden` to see the set of available commands and options. + +To get help for a specific command, use `cargo miden help ` or `cargo miden --help`. + +## Creating a new project + +Your first step will be to create a new Rust project set up for compiling to Miden: + +```bash +cargo miden new foo +``` + +In this above example, this will create a new directory `foo`, containing a Cargo project for a +crate named `foo`, generated from our Miden project template. + +Templates are released independently of the compiler, so `cargo miden new` +fetches the newest compatible template bundle at run time and falls back to a +copy embedded in `cargo-miden` — you get template fixes without reinstalling, +and project creation still works with no network access. + +Pass `--force-download` to require the released bundle and fail rather than +silently falling back, which is useful for confirming what a released template +actually produces: + +```bash +cargo miden new foo --force-download +``` + +Pass `--template-path ` to generate from templates on disk instead. + +The template we use sets things up so that you can pretty much just build and run. Since the +toolchain depends on Rust's native WebAssembly target, it is set up just like a minimal WebAssembly +crate, with some additional tweaks for Miden specifically. + +Out of the box, you will get a Rust crate that depends on the Miden SDK, and sets the global +allocator to a simple bump allocator we provide as part of the SDK, and is well suited for most +Miden use cases, avoiding the overhead of more complex allocators. + +As there is no panic infrastructure, `panic = "abort"` is set, and the panic handler is configured +to use the native WebAssembly `unreachable` intrinsic, so the compiler will strip out all of the +usual panic formatting code. + +## Compiling to Miden package + +Now that you've created your project, compiling it to Miden package is as easy as running the +following command from the root of the project directory: + +```bash +cargo miden build --release +``` + +This will emit the compiled artifacts to `target/miden/release/foo.masp`, and print the path of +the compiled Miden package on success. + +## Running a compiled Miden VM program + +Use `miden-debug` to execute the compiled package. See [Debugging programs](../guides/debugger.md) +for installation and the input-file format. + +```bash +miden-debug target/miden/release/foo.masp --inputs some_inputs.toml +``` + +This opens the interactive debugger. For terminal commands, add `--repl`; for a non-interactive +run, use `--commands` with a debugger command file, as described in the guide. Run +`miden-debug --help` for the available options. + +## Examples + +Check out the [examples](https://github.com/0xMiden/compiler/tree/next/examples) for some `cargo-miden` project examples. diff --git a/versioned_docs/version-0.16/reference/compiler/usage/index.md b/versioned_docs/version-0.16/reference/compiler/usage/index.md new file mode 100644 index 00000000..10bbd762 --- /dev/null +++ b/versioned_docs/version-0.16/reference/compiler/usage/index.md @@ -0,0 +1,13 @@ +--- +title: Usage +sidebar_position: 1 +--- + +# Usage + +The Usage section documents how to work with Miden's compiler tools. Key components include: + +- **Command-line interface** ([`midenc`](./midenc.md)) - A low-level compiler driver offering precise + control over compilation outputs and diagnostic information +- **Cargo extension** ([`cargo-miden`](./cargo-miden.md)) - Higher-level build tool integration for + managing Miden projects within Rust's package ecosystem diff --git a/versioned_docs/version-0.16/reference/compiler/usage/midenc.md b/versioned_docs/version-0.16/reference/compiler/usage/midenc.md new file mode 100644 index 00000000..c29278e8 --- /dev/null +++ b/versioned_docs/version-0.16/reference/compiler/usage/midenc.md @@ -0,0 +1,125 @@ +--- +title: As an Executable +sidebar_position: 2 +--- + +# Getting started with `midenc` + +The `midenc` executable is the command-line interface for the compiler driver. Use the separate +`miden-debug` executable to execute and debug compiled packages. + +While it is a lower-level tool compared to `cargo-miden`, just like the difference between `rustc` and `cargo`, it provides a lot of functionality for emitting diagnostic information, controlling the output of the compiler, and configuring the compilation pipeline. Most users will want to use `cargo-miden`, but understanding `midenc` is helpful for those times where you need to get your hands dirty. + +## Installation + +To install `midenc`, you have two choices: + +1. Install via [`midenup`](https://github.com/0xMiden/midenup), which also handles other toolchain components that you'll likely want. +2. Install from source + +We'll cover installation from source here - see the `midenup` README for details on how to install Miden components that way. + + +First, clone the compiler repo: + +```bash +git clone https://github.com/0xMiden/compiler +``` + +Then, run the following in your shell in the cloned repo folder: + +```bash +cargo install --path midenc --locked +``` + +## Usage + +Once installed, you should be able to invoke the compiler, you should see output similar to this: + +```text +midenc --help +Usage: midenc [OPTIONS] [FILE] + +Arguments: + [INPUTS]... + Path(s) to the source file(s) to compile. + + You may also use `-` as a file name to read a file from stdin. + +Options: + -p, --package + Package(s) to build + + --manifest-path + Path to the package/project manifest + + If unspecified, the compiler will create a virtual manifest for the given input + + -h, --help + Print help (see a summary with '-h') + + -V, --version + Print version + +..snip.. +``` + +The actual help output covers quite a bit more than that - see the actual command output for the full picture. + +## Compilation + +See the help output for `midenc` for detailed information on its options and their behavior. However, the following is an example of how one might use `midenc` in practice: + +```bash +midenc --entrypoint 'foo::main' \ + -lextra \ + -L ./masm \ + --emit=hir=- \ + -o out.masp \ + target/wasm32-wasip1/release/foo.wasm +``` + +In this scenario, we are in the root of a Rust crate, named `foo`, which we have compiled for the `wasm32-wasip1` target, which placed the resulting WebAssembly module in the `target/wasm32-wasip1/release` directory. This crate exports a function named `main`, which we want to use as the entrypoint of the program. + +Additionally, our Rust code links against some hand-written Miden Assembly code, namespaced under `extra`, which can be found in `./masm/extra`. We are telling `midenc` to link the `extra` library, and to add the `./masm` directory to the library search path. + +Lastly, we're configuring the output: + +- We're using `--emit` to request `midenc` to dump Miden IR (`hir`) to stdout (specified via the `-` shorthand), in addition to the Miden package artifact (produced by default). +- We're telling `midenc` to write the compiled output to `out.masp` in the current directory, rather than the default path that would have been used (`target/miden/foo.masp`). + +### Stopping early + +`--stop-after=CHECKPOINT` ends compilation once the named phase has run, rather than building a package. It is most useful together with `--emit`, to look at an intermediate form without paying for the rest of the build: + +```bash +midenc --stop-after=transform --emit=hir=- foo.wasm +``` + +`CHECKPOINT` is either an alias — `parse`, `analyze`, `transform`, `lower`, `assemble` — or a fully-qualified checkpoint id such as `hir.initial`. Which names are valid depends on the input: each frontend declares its own route, and a name that route does not reach is reported along with the names it does accept. A Miden Assembly input, for example, has no `transform` phase. + +## Debugging + +See [Debugging Programs](../guides/debugger.md) for details on how to debug Miden programs using `miden-debug`. + +## Next steps + +We have put together two useful guides to walk through more detail on compiling Rust to WebAssembly: + +1. To learn how to compile Rust to WebAssembly so that you can invoke `midenc` on the + resulting Wasm module, see [this guide](../guides/rust_to_wasm.md). +2. If you already have a WebAssembly module, or know how to produce one, and want to learn how to + compile it to Miden Assembly, see [this guide](../guides/wasm_to_masm.md). + +To start from a working project rather than an empty one, use `cargo miden new`: + +```bash +cargo miden new my-project # a full project scaffold +cargo miden new my-account --account # a single account component +``` + +The available templates are `--account`, `--note`, `--tx-script`, +`--auth-component`, and `--program`. Their sources live in this repository under +[`extra/templates`](https://github.com/0xMiden/compiler/tree/main/extra/templates) +and are released independently of the compiler, so `cargo miden new` picks up +template updates without you reinstalling `cargo-miden`. diff --git a/versioned_docs/version-0.16/reference/index.md b/versioned_docs/version-0.16/reference/index.md new file mode 100644 index 00000000..afa23a0b --- /dev/null +++ b/versioned_docs/version-0.16/reference/index.md @@ -0,0 +1,215 @@ +--- +sidebar_label: Introduction +sidebar_position: 0 +pagination_next: null +pagination_prev: null +--- + + + +# Miden Reference + +A technical reference for Miden's architecture: the protocol, the zkVM, the compiler toolchain, and the node. + +## Explore by topic + + + + Accounts, notes, state model, and transaction semantics. + + + STARK-based zkVM, chiplets, and Miden Assembly. + + + Rust → WebAssembly → Miden IR → MASM compilation pipeline. + + + gRPC API, batch aggregation, and block production. + + + +--- + +## Architecture overview + +Miden is a zero-knowledge layer 2 that rethinks blockchain architecture. Instead of a single global state updated sequentially, Miden uses an **actor model** where each account is an independent state machine that executes transactions locally and generates validity proofs. + +### Core design principles + +| Principle | How Miden achieves it | +|-----------|----------------------| +| **Privacy** | Public accounts and notes publish their full state or details; private variants keep them offchain behind commitments | +| **Parallelism** | Single-account transactions enable concurrent execution without contention | +| **Scalability** | Client-side proving offloads computation; proof aggregation reduces onchain verification | +| **Programmability** | A Turing-complete VM supports arbitrary smart contract logic in accounts and notes | + + + +**Local execution** — Users execute transactions on their devices, consuming input notes and updating account state. + +**Proof generation** — The client generates a STARK proof attesting to valid state transitions. + +**Note creation** — Transactions produce output notes carrying assets and data to recipients. + +**Verification** — The node verifies proofs, updates state commitments, and makes notes available. + + + +--- + +## Protocol + +The protocol layer defines Miden's data structures, state model, and transaction semantics. + +### Accounts + +Accounts are programmable entities that hold assets and execute code: + +- **ID** — immutable identifier committed to a seed and the initial code and storage; it also encodes the account type and asset-callback flag +- **Code** — smart contract logic defining the account's interface +- **Storage** — key-value store with up to 255 slots for persistent data +- **Vault** — container holding fungible and non-fungible assets +- **Nonce** — monotonically increasing counter preventing replay attacks + +Account code is composed from **components** — modular building blocks that add capabilities like wallet functionality, token standards, or custom logic. + +### Notes + +Notes are programmable messages that transfer assets between accounts: + +- **Assets** — up to 16 fungible or non-fungible assets carried by the note +- **Recipient** — the serial number, script, and storage that define the consumption conditions +- **Metadata** — sender, note type, tag, and attachment headers and commitment; always public +- **Attachments** — optional public auxiliary data associated with the note + +Public notes publish their metadata, attachments, and full note details. Private notes still publish metadata and attachments, but publish only a commitment to the note details; the consumer must obtain those details separately. + +### State model + +Miden maintains three core databases: + +| Database | Structure | Purpose | +|----------|-----------|---------| +| **Accounts** | Sparse Merkle Tree | Maps account IDs to state commitments | +| **Notes** | Merkle Mountain Range | Append-only log of created notes | +| **Nullifiers** | Sparse Merkle Tree | Tracks consumed notes to prevent double-spending | + +This separation enables efficient state proofs and supports both public and private modes for accounts and notes. + +### Transactions + +Transactions are single-account operations that: + +1. Consume zero or more input notes +2. Execute account code and optionally a transaction script +3. Update account state (storage, vault, nonce) +4. Produce zero or more output notes + +The single-account model means transactions don't contend for shared state, enabling **parallel execution** across the network. + +--- + +## Virtual Machine (miden-vm) + +The Miden VM is a STARK-based virtual machine optimized for zero-knowledge proof generation. + +### Architecture + +- **Stack-based** — operates on a push-down stack of 64-bit prime field elements +- **Turing-complete** — supports loops, conditionals, and recursive procedures +- **Deterministic** — same inputs always produce same outputs (with controlled nondeterminism for advice) + +### Core components + +| Component | Function | +|-----------|----------| +| **Decoder** | Fetches and decodes instructions, manages control flow | +| **Stack** | 16 directly accessible elements, unlimited depth via memory | +| **Memory** | Random-access read/write storage | +| **Chiplets** | Specialized circuits for cryptographic and bitwise operations | + +### Chiplets + +Chiplets are co-processors that accelerate common operations: + +- **Hash chiplet** — Poseidon2 hashing and Merkle tree operations +- **Bitwise chiplet** — AND, XOR, and other bitwise operations on 32-bit integers +- **Memory chiplet** — efficient random-access memory with read/write tracking +- **Kernel ROM** — secure execution of privileged kernel procedures + +### Miden Assembly (MASM) + +MASM is the native instruction set with: + +- **Field operations** — arithmetic on prime field elements +- **U32 operations** — 32-bit integer arithmetic, bitwise, and comparison +- **Cryptographic operations** — hashing, Merkle proofs, signature verification +- **Control flow** — conditionals, loops, and procedure calls +- **Memory operations** — load/store to local and global memory + +--- + +## Compiler + +The compiler toolchain enables writing Miden programs in high-level languages. + +### Components + +- **cargo-miden** — Cargo extension for building Miden projects +- **midenc** — core compiler from WebAssembly to Miden Assembly +- **[Debugger](https://github.com/0xMiden/miden-debug)** — interactive debugging with breakpoints and state inspection + +### Compilation pipeline + +```text + Rust → WebAssembly → Miden IR → MASM + (source) (Wasm binary) (compiler IR) (assembly) +``` + +The compiler supports: + +- **Account components** — reusable smart contract modules +- **Note scripts** — logic executed when notes are consumed +- **Transaction scripts** — custom transaction execution logic + +--- + +## Node + +The node is the network infrastructure that receives transactions and produces blocks. + +### Responsibilities + +- Receive and validate proven transactions via gRPC +- Aggregate transaction proofs into batches +- Produce blocks with aggregated proofs +- Maintain state databases and serve sync requests + +### gRPC API + +The node exposes endpoints for: + +- **Account queries** — get account details, proofs, and storage +- **Note queries** — retrieve notes by ID or script +- **Block queries** — fetch block headers and contents +- **Sync operations** — synchronize client state with the network +- **Transaction submission** — submit proven transactions + +--- + +import SectionLinks from '@site/src/components/SectionLinks'; + + diff --git a/versioned_docs/version-0.16/reference/miden-vm/_category_.yml b/versioned_docs/version-0.16/reference/miden-vm/_category_.yml new file mode 100644 index 00000000..ea52768c --- /dev/null +++ b/versioned_docs/version-0.16/reference/miden-vm/_category_.yml @@ -0,0 +1,4 @@ +label: "Virtual Machine" +# Determines where this documentation section appears relative to other sections on the main documentation page (which is the parent of this folder in the miden-docs repository) +position: 7 +collapsed: true diff --git a/versioned_docs/version-0.16/reference/miden-vm/advanced_topics/_category_.yml b/versioned_docs/version-0.16/reference/miden-vm/advanced_topics/_category_.yml new file mode 100644 index 00000000..23cf4ba2 --- /dev/null +++ b/versioned_docs/version-0.16/reference/miden-vm/advanced_topics/_category_.yml @@ -0,0 +1,4 @@ +label: "Advanced Topics" +# Determines where this documentation section appears relative to other sections in the parent folder +position: 9 +collapsed: true diff --git a/versioned_docs/version-0.16/reference/miden-vm/advanced_topics/execution_trace_optimization.md b/versioned_docs/version-0.16/reference/miden-vm/advanced_topics/execution_trace_optimization.md new file mode 100644 index 00000000..160182d0 --- /dev/null +++ b/versioned_docs/version-0.16/reference/miden-vm/advanced_topics/execution_trace_optimization.md @@ -0,0 +1,60 @@ +--- +title: "Execution Trace Optimization" +sidebar_position: 2 +draft: true +--- + +# Execution trace optimization + +## Understanding cycle counts in Miden VM + +When we refer to "number of cycles" in most Miden VM documentation, we're specifically referring to the **stack rows** portion of the execution trace. However, the actual proving time is determined by what we call the "true number of cycles," which is the maximum of all trace segment lengths: + +- **Stack rows**: One row per VM operation (what `clk` outputs). This corresponds to the System, Program decoder and Operand Stack set of columns from the [execution trace diagram](../design/index.md#execution-trace) +- **Range checker rows**: Added for all u32 and memory operations (no more, no less) +- **Chiplet rows**: Added when opcodes call specialized chiplets: + - `hperm` calls the hasher chiplet + - `and`, `or` (and other bitwise ops) call the bitwise chiplet + - memory operations call the memory chiplet + - syscalls call the kernel ROM chiplet + +The **true number of rows in the final trace** is precisely `max(stack_rows, range_checker_rows, chiplet_rows)` + +Note: The maximum gets rounded up to the next power of 2, and the other 2 sets of columns get padded up to this maximum. + +In some cases, either the range checker or chiplets could end up requiring more rows than the stack rows, making the true cycle count higher than what the stack-based cycle counter reports. + +The runtime also applies a hard trace limit of `2^29` rows in parallel trace building. If replayed core or chiplet rows would pass that limit, execution stops with `TraceLenExceeded` instead of trying to allocate a larger trace. + +## Analyzing trace segments with miden-vm analyze + +The `miden-vm analyze` command provides detailed information about trace segment utilization, showing: +- Stack rows used +- Range checker rows used +- Chiplet rows used +- The resulting true number of cycles (maximum of the three) + +This tool helps identify which trace segments are driving the ultimate trace length, and hence overall proving time for a given program. + +## Trace segment growth and proving performance + +Even when two programs run the same number of VM cycles, their proving time can differ significantly because of how the execution trace is structured. + +| Trace segment | Purpose | Native growth rule | +| ------------------ | ---------------------------------------------------- | ------------------------------------------------------ | +| Stack rows | Core transition constraints; one row per opcode | +1 row for every operation | +| Range‑checker rows | Ensure selected values lie in \[0 .. 2¹⁶) | Rows added for all u32 and memory operations | +| Chiplet rows | Bitwise, hash, memory and other accelerator circuits | Rows added only when an opcode calls a chiplet | + +1. **Independent growth** + Each segment expands on its own. A pure arithmetic loop inflates only the stack segment, whereas repeated hashing inflates the chiplet segment. + +2. **Power‑of‑two padding** + After execution halts, the prover finds the largest trace segment length `L`, rounds it up to the next power of two `Lʹ = 2^ceil(log₂ L)`, and pads all segments until all trace segments reach `Lʹ`. The prover expects a square trace matrix of this size. + +> Padding doesn't simply mean "filling with zeros." Instead, padding means setting the cells to whatever values make the constraints work. While this can be intuitively thought of as "setting the cells to 0" in many cases, the actual padding values are determined by what satisfies the AIR constraints for each specific trace segment. + +3. **Cost driver** + Proving time grows roughly with `Lʹ`, not with the raw cycle count. Programs that rely heavily on chiplets might have the same cycle count but significantly longer proving times due to trace segment growth. Opcodes that touch only the stack keep every segment short, yielding faster proofs. Opcodes that generate many chiplet or range‑checker rows can push `L` past a power‑of‑two boundary, doubling every segment's length after padding and markedly increasing proving time. Mixing opcode types unevenly can thus produce a cycle‑efficient program that is still proving‑expensive. + +**Take‑away**: track which segment each opcode stresses, batch chiplet‑heavy work, and watch the next power‑of‑two boundary; staying below it can nearly halve proof time. diff --git a/versioned_docs/version-0.16/reference/miden-vm/advanced_topics/index.md b/versioned_docs/version-0.16/reference/miden-vm/advanced_topics/index.md new file mode 100644 index 00000000..0dd60067 --- /dev/null +++ b/versioned_docs/version-0.16/reference/miden-vm/advanced_topics/index.md @@ -0,0 +1,9 @@ +--- +title: "Advanced Topics" +sidebar_position: 1 +draft: true +--- + +# Advanced Topics + +This section covers advanced concepts and implementation details for users who want to understand the deeper aspects of Miden VM. \ No newline at end of file diff --git a/versioned_docs/version-0.16/reference/miden-vm/background.md b/versioned_docs/version-0.16/reference/miden-vm/background.md new file mode 100644 index 00000000..614f49a8 --- /dev/null +++ b/versioned_docs/version-0.16/reference/miden-vm/background.md @@ -0,0 +1,36 @@ +--- +title: "Background Material" +sidebar_position: 8 +--- + +# Background Material + +Proofs of execution generated by Miden VM are based on STARKs. A STARK is a novel proof-of-computation scheme that allows you to create an efficiently verifiable proof that a computation was executed correctly. The scheme was developed by Eli Ben-Sasson, Michael Riabzev et al. at Technion - Israel Institute of Technology. STARKs do not require an initial trusted setup, and rely on very few cryptographic assumptions. + +Here are some resources to learn more about STARKs: + +- STARKs paper: [Scalable, transparent, and post-quantum secure computational integrity](https://eprint.iacr.org/2018/046) +- STARKs vs. SNARKs: [A Cambrian Explosion of Crypto Proofs](https://medium.com/starkware/the-cambrian-explosion-of-crypto-proofs-7ac080ac9aed) + +Vitalik Buterin's blog series on zk-STARKs: + +- [STARKs, part 1: Proofs with Polynomials](https://vitalik.eth.limo/general/2017/11/09/starks_part_1.html) +- [STARKs, part 2: Thank Goodness it's FRI-day](https://vitalik.eth.limo/general/2017/11/22/starks_part_2.html) +- [STARKs, part 3: Into the Weeds](https://vitalik.eth.limo/general/2018/07/21/starks_part_3.html) + +Alan Szepieniec's STARK tutorials: + +- [Anatomy of a STARK](https://aszepieniec.github.io/stark-anatomy/) +- [BrainSTARK](https://aszepieniec.github.io/stark-brainfuck/) + +StarkWare's STARK Math blog series: + +- [STARK Math: The Journey Begins](https://medium.com/starkware/stark-math-the-journey-begins-51bd2b063c71) +- [Arithmetization I](https://medium.com/starkware/arithmetization-i-15c046390862) +- [Arithmetization II](https://medium.com/starkware/arithmetization-ii-403c3b3f4355) +- [Low Degree Testing](https://medium.com/starkware/low-degree-testing-f7614f5172db) +- [A Framework for Efficient STARKs](https://medium.com/starkware/a-framework-for-efficient-starks-19608ba06fbe) + +StarkWare's STARK tutorial: + +- [STARK 101](https://starkware.co/stark-101/) diff --git a/versioned_docs/version-0.16/reference/miden-vm/design/_category_.yml b/versioned_docs/version-0.16/reference/miden-vm/design/_category_.yml new file mode 100644 index 00000000..485baedd --- /dev/null +++ b/versioned_docs/version-0.16/reference/miden-vm/design/_category_.yml @@ -0,0 +1,4 @@ +label: "Design" +# Determines where this documentation section appears relative to other sections in the parent folder +position: 7 +collapsed: true diff --git a/versioned_docs/version-0.16/reference/miden-vm/design/chiplets/_category_.yml b/versioned_docs/version-0.16/reference/miden-vm/design/chiplets/_category_.yml new file mode 100644 index 00000000..32a2c4e4 --- /dev/null +++ b/versioned_docs/version-0.16/reference/miden-vm/design/chiplets/_category_.yml @@ -0,0 +1,4 @@ +label: "Chiplets" +# Determines where this documentation section appears relative to other sections in the parent folder +position: 5 +collapsed: true diff --git a/versioned_docs/version-0.16/reference/miden-vm/design/chiplets/ace.md b/versioned_docs/version-0.16/reference/miden-vm/design/chiplets/ace.md new file mode 100644 index 00000000..55e63c6e --- /dev/null +++ b/versioned_docs/version-0.16/reference/miden-vm/design/chiplets/ace.md @@ -0,0 +1,511 @@ +--- +title: "ACE Chiplet" +sidebar_position: 5 +--- + +# ACE chiplet + +The following note describes the design and functionality of the Arithmetic Circuit Evaluation (ACE) chiplet. + +Given a description of an arithmetic circuit and a set of input values, it ensures this circuit evaluates to zero over these inputs. +Its main purpose is to reduce the number of cycles required when recursively verifying a STARK proof in Miden assembly. +In particular, it performs the DEEP-ALI constraint composition polynomial check over the evaluations of the trace column polynomials at the out-of-domain point. + +The chiplet expects the caller to have prepared a region of memory containing + +- **inputs** to circuit, +- **constants** which make up the circuit, +- **instructions** describing the arithmetic operations evaluated by the circuit. + +The term **variable** refers to a value, which is either an input or a constant. + +Mathematically, we can represent an arithmetic circuit as a DAG, where leaves correspond to inputs and constants, and the nodes are the operations computing the result (which must be zero). +For example, take the following composition polynomial to be evaluated by the chiplet + +$$ +s(s - 1) + \alpha \left[ s \cdot (\text{output} - 42) + (s - 1) \cdot (\text{output} - \text{input}) \right]. +$$ + +Given an input selector $s$, and input variables $\alpha, \text{input}, \text{output}$, the two combined constraints ensure that + +$$ +s \in \{0,1\}, \quad +\text{output} = +\begin{cases} +\text{input}, &s = 0, +42, &s = 1. +\end{cases} +$$ + +The following graph describes the evaluation of the circuit describing the above polynomial. The leaf nodes correspond to the variables of the polynomial: blue for inputs and green for constants. +Note that the circuit is able to reuse evaluation, as is the case for the node $s-1$. + +```mermaid +flowchart BT + %% Sink node (final expression) + add2["result = c₀ + α⋅c₁"] + mul1["s × (s−1)"] + mul4["α × c₁"] + add1["c₁ = s⋅case₌₁ + (s−1)⋅case₌₀"] + mul2["s × case₌₁"] + mul3["(s−1) × case₌₀"] + sub1["s − 1"] + sub2["case₌₁ = output − 42"] + sub4["case₌₀ = output − input"] + %% Leaf nodes + s["s"] + one1["1"] + alpha["α"] + input["input"] + const42["42"] + output_val["output"] + %% Classes: colored borders only + classDef blue stroke: #3498db, stroke-width: 2px; + classDef green stroke: #2ecc71, stroke-width: 2px; + class s,alpha,input,output_val blue; + class one1,const42 green; + %% Edges (pointing upward) + mul1 --> add2 + mul4 --> add2 + s --> mul1 + sub1 --> mul1 + s --> sub1 + one1 --> sub1 + alpha --> mul4 + add1 --> mul4 + mul2 --> add1 + mul3 --> add1 + s --> mul2 + sub2 --> mul2 + output_val --> sub2 + const42 --> sub2 + sub1 --> mul3 + sub4 --> mul3 + output_val --> sub4 + input --> sub4 +``` + +The chiplet constructs and verifies the correctness of such a DAG by using a logUp argument, which we interpret as a _wiring bus_. +In each row, the chiplet can either insert a new node with the next fresh identifier and desired value or request a node’s value by providing the identifier of a previously inserted node. +Whenever we create a new node in the DAG (when loading a variable or evaluating an operation), we “insert” it onto the wiring bus by emitting a tuple $(id,v)$ together with its final fan‐out count $m$. +In other words, at insertion time we record $(id,v)$ with multiplicity $m$, where $m$ is exactly the number of times this node will later be used as an input to downstream operations. +Then, each time some later instruction reads that node $(id,v)$, we “consume” one copy of $(id,v)$ - i.e., we remove it once from the wiring bus — decrementing the stored multiplicity by exactly 1. +By the time we finish inserting and consuming all nodes, two things must hold: + +1. The very last node we produced (the root of the DAG) has value 0. +2. Every inserted tuple has been consumed exactly $m$ times, so the wiring bus is empty. + +## Trace layout + +The ACE chiplet produces a trace with 16 internal columns. +Each _section_ of the trace corresponds to exactly one circuit evaluation. +Within each section, there are two ordered _blocks_: + +1. A READ block, which loads inputs/constants from memory and inserts them into the DAG. +2. An EVAL block, which executes each instruction — fetching two existing DAG nodes, computing $v_{out}$, and inserting a new node. + +In what follows, we'll refer to READ/EVAL blocks depending on which operation is being performed in a given row, though we sometimes refer to a row's _mode_ when it would conflict with the term _operation_. + +### Columns + +The following table describes the 16 columns used by the chiplet, and their interpretation in each block. + +| **BLOCK** | $s_{start}$ | $s_{block}$ | $ctx$ | $ptr$ | $clk$ | | $id_0$ | $v_{0,0}$ | $v_{0,1}$ | $id_1$ | $v_{1,0}$ | $v_{1,1}$ | | | | $m_0$ | +| --------- | ----------- | ----------- | ----- | ----- | ----- | ---- | ------ | --------- | --------- | ------ | --------- | --------- | ---------- | --------- | --------- | ----- | +| **READ** | = | = | = | = | = | | = | = | = | = | = | = | $n_{eval}$ | | $m_1$ | = | +| **EVAL** | = | = | = | = | = | $op$ | = | = | = | = | = | = | $id_2$ | $v_{2,0}$ | $v_{2,1}$ | = | + +- "=" means the interpretation is the same in both READ/EVAL. +- Empty cell means the cell is unused in this block. +- Unlabeled columns appear under a block‐specific heading instead. + +While we will later describe the purpose of each comment, we provide some intuition about how to interpret the labels themselves: + +- $s_{start}$ is a boolean selector indicating the first row of a section, +- $s_{block}$ is a boolean selector indicating the type of block the row is a part of (0 for READ, and 1 for EVAL), +- $(ctx, ptr, clk)$ identifies the memory request when reading variables or circuit instructions. +- $(id_i, v_{i,0}, v_{i,1})$ refers to a node in the evaluation graph at index $id_i$ whose value is the extension field element $v_i = (v_{i,0}, v_{i,1})$, for $i = 0, 1, 2$. +- $m_i$ is used when inserting a node $(id_i, v_i)$ into the evaluation graph, corresponding to its _fan-out degree_. It is also interpreted as the _multiplicity_ of the element being added to the wiring bus. + +### Memory layout + +The chiplet trace contains multiple sections, each performing a distinct circuit evaluation. +Within a section, the chiplet reads through a contiguous, word‐aligned memory region that contains: + +- **$I$ input variables** each stored as an extension‐field element, with two elements per word. +- **$C$ circuit constants** also stored two per word, which are part of the circuit description and hence included in its commitment. +- **$N$ circuit instructions** each encoded as one field‐element representing a packed triple $(op,id_l,id_r)$. + +The caller is responsible for writing the inputs and circuit into memory before invoking the chiplet. +If the same circuit is evaluated multiple times, the caller must overwrite the input region with the new inputs for each evaluation. +To start a circuit evaluation, the caller pushes one chiplet‐bus message: + +$$ +(\mathsf{ACE\_LABEL},ctx,ptr,clk,n_{read},n_{eval}), +$$ + +where: + +- $(ctx,clk)$ identifies the memory‐access context that every row will use. +- $ptr$ is the word-aligned pointer to the first input variable. +- $n_{read} = I + C$ is the total count of input and constant elements that the chiplet will read. +- $n_{eval} = N$ is the total number of arithmetic operations (instructions) the chiplet will evaluate. + The chiplet stores $N - 1$ in the trace column so that the READ→EVAL switch triggers when $id_0' = n_{eval}$. + +For both inputs and constants, it is permissible to pad their respective regions with zeros. +Any padded zero will simply be ignored by the instructions (they are not referenced by any instruction, so their multiplicity will be set to 0). +To ensure the entire region is word-aligned, we add up to three "dummy" instructions which square the last node. +This has no effect when the last evaluation is zero, as required. + +## Circuit evaluation + +The evaluation is initialized in the first row of the section by setting + +- $id_0 = n_{read} + n_{eval} -1$, indicating the expected number of nodes in the DAG. Whenever the chiplet adds a new wire to the bus, this identifier decrements by 1, until it reaches 0. +- $s_{start} = 1$, indicating the start of the section (it is set to 0 in all remaining rows) +- $s_{block} = 0$, ensuring the evaluation starts by reading variables +- $ctx, clk, n_{eval}, ptr$ provided by the bus request. $(ctx, clk)$ remain constant for the entire section, while $n_{eval}$ (stored as $N-1$ in the trace) is constant only across the READ block. + +In every row, the chiplet the following actions in each block: + +**READ** (when $s_{block} = 0$): + +- Reads a word from memory at $(ctx,ptr,clk)$, containing two extension-field elements $(v_{0},v_{1})$. +- Assigns two new node IDs $(id_0,id_1)$ to those elements, where $id_1 = id_0 - 1$ +- Inserts each new node into the evaluation graph (wiring bus) with a specified fan-out count $m_i$. +- Increments $ptr$ by 4 in the next row. +- Decrements $id_0$ by 2 in the next row. +- If $id_0'$ in the next row is equal to $n_{eval}$, it switches to the EVAL operation by setting $s_{block}' = 1$ + +**EVAL** (when $s_{block} = 1$): + +- Reads a single field-element $instr$ (an encoded instruction) from memory at $(ctx,ptr,clk)$. +- Decodes $(op,id_1,id_2)$ from $instr$. +- Fetches the two input nodes $(id_1,v_1), (id_2, v_2)$ from the wiring bus, consuming one fan-out from each (i.e., with multiplicity $m_i = -1$). +- Computes + $$ + v_{0} = + \begin{cases} + v_1 - v_2, & op = -1, + v_1 \times v_2, & op = 0, + v_1 + v_2, & op = 1. + \end{cases} + $$ +- Inserts $(id_0,v_{0})$ onto the wiring bus with its final fan-out count $m_0$. +- Increments $ptr$ by 4 in the next row. +- If $id_0 = 0$, checks that $v_0 =0$ and ends the evaluation. +- Otherwise, decrements $id_0$ by 1 in the next row. + +_**Note**: Wire bus requests also include the memory access pair $(ctx, clk)$ which ensures that the wires produced by different circuit evaluations are distinct._ + +### Example + +The following is a section of the trace representing the evaluation of the expressions + +$$ +s(s - 1) + \alpha \left[ s \cdot (\text{output} - 42) + (s - 1) \cdot (\text{output} - \text{input}) \right]. +$$ + +We start by assigning values to both the inputs and constants nodes, stored in memory in addresses `0x0000 - 0x0005` + +$$ +\begin{aligned} +v_{14} &= \alpha, & v_{10} &= 42, +v_{13} &= \text{output}, & v_{9} &= 1. +v_{12} &= s, +v_{11} &= \text{input}, +\end{aligned} +$$ + +We can then write down the values of all evaluated nodes. +Their instructions are stored in the memory region `0x0006 - 0x00014` + +$$ +\begin{aligned} +v_{8} &= v_{12} - v_{9} &&|\ s - 1 +v_{7} &= v_{12} \times v_8 &&|\ c_0 = s \times (s - 1) +v_{6} &= v_{13} - v_{10} &&|\ \text{case}_{s=1} = \text{output} - 42 +v_{5} &= v_{13} - v_{11} &&|\ \text{case}_{s=0} = \text{output} - \text{input} +v_{4} &= v_{12} \times v_{6} &&|\ s \times \text{case}_{s=1} +v_{3} &= v_{8} \times v_{5} &&|\ (s-1) \times \text{case}_{s=0} +v_{2} &= v_{4} + v_{3} &&|\ c_1 = s × \text{case}_{s=1} + (s - 1) \times \text{case}_{s=0} +v_{1} &= v_{14} \times v_{2} &&|\ \alpha \times c_1 +v_{0} &= v_{7} + v_1 &&|\ \text{result} = c_0 + \alpha × c_1 +\end{aligned} +$$ + +| $s_{start}$ | $s_{block}$ | $ctx$ | $ptr$ | $clk$ | $op$ | $id_0$ | $v_{0}$ | $id_1$ | $v_{1}$ | $n_{eval}$/$id_{2}$ | $m_1$/$v_2$ | $m_0$ | +| ----------- | ----------- | ----- | ------ | ----- | -------- | ------ | ----------------------------- | ------ | ------------------------ | ------------------- | ----------------------- | ------------ | +| 1 | 0 | ctx | 0x0000 | clk | | 14 | $v_{14} = \alpha$ | 13 | $v_{13} = \text{input}$ | 8 | $m_{13} = 2$ | $m_{14} = 1$ | +| 0 | 0 | ctx | 0x0004 | clk | | 12 | $v_{12} = s$ | 11 | $v_{11} = \text{output}$ | 8 | $m_{11} = 1$ | $m_{12} = 3$ | +| 0 | 0 | ctx | 0x0008 | clk | | 10 | $v_{10} = 42$ | 9 | $v_{9} = 1$ | 8 | $m_{9} = 1$ | $m_{10} = 1$ | +| 0 | 1 | ctx | 0x000c | clk | $-$ | 8 | $v*{8} = v*{12} - v\_{9} $ | 12 | $v_{12} = s$ | 9 | $v_{9}$ | 2 | +| 0 | 1 | ctx | 0x000d | clk | $\times$ | 7 | $v_{7} = v_{12} \times v_8$ | 12 | $v_{12} = s$ | 8 | $v_{8}$ | 1 | +| 0 | 1 | ctx | 0x000e | clk | $-$ | 6 | $v_{6} = v_{13} - v_{10}$ | 13 | $v_{13} = \text{output}$ | 10 | $v_{10} = 42$ | 1 | +| 0 | 1 | ctx | 0x000f | clk | $-$ | 5 | $v_{5} = v_{13} - v_{11}$ | 13 | $v_{13} = \text{output}$ | 11 | $v_{11} = \text{input}$ | 1 | +| 0 | 1 | ctx | 0x0010 | clk | $\times$ | 4 | $v_{4} = v_{12} \times v_{6}$ | 12 | $v_{12} = s$ | 6 | $v_{6}$ | 1 | +| 0 | 1 | ctx | 0x0011 | clk | $\times$ | 3 | $v_{3} = v_{8} \times v_{5}$ | 8 | $v_{8}$ | 5 | $v_{5}$ | 1 | +| 0 | 1 | ctx | 0x0012 | clk | $+$ | 2 | $v_{2} = v_{4} + v_{3}$ | 4 | $v_{4}$ | 3 | $v_{3}$ | 1 | +| 0 | 1 | ctx | 0x0013 | clk | $\times$ | 1 | $v_{1} = v_{14} \times v_{2}$ | 14 | $v_{14} = \alpha$ | 2 | $v_{2}$ | 1 | +| 0 | 1 | ctx | 0x0014 | clk | $+$ | 0 | $v_{0} = v_{7} + v_{1}$ | 7 | $v_{7}$ | 1 | $v_{1}$ | 0 | + +## Constraints + +### Flags + +In this section, we derive boolean flags that indicate whether a row is at the boundary (first, last, or transition) of the ACE chiplet trace, a section, or a READ/EVAL block. + +#### Chiplet flags + +The chiplet trace activates different chiplet constraints using a common set of binary selectors $s_1, \ldots$. +While it is likely that the ACE chiplet will appear in third position, we derive the flags and boundary constraints for the general case where the chiplet appears in the d-th position. +Accounting for this degree allows us to evaluate whether we need a separate degree-1 internal selector for activating the chiplet's constraints. +The layout of the chiplet trace will look something like the following. + +| Chiplet | $s_1, \ldots, s_{d-1}$ | $s_{d}$ | ... | +| -------- | ---------------------- | ------------- | ------------- | +| Previous | $[1, ..., 1, 0]$ | $cols_{prev}$ | $cols_{prev}$ | +| ACE | $[1, ..., 1, 1]$ | $0$ | $cols_{ace}$ | +| Next | $[1, ..., 1, 1]$ | $1$ | $0$ | + +From these common selectors, we derive the following binary flags which indicate which portion of the ACE chiplet is active. + +- $f_{prev}$: The previous chiplet is active. +- $f_{ace}$: The ACE chiplet is active. +- $f_{ace, first}'$: Next row is the first row in ACE chiplet. +- $f_{ace, next}$: Current and next rows are in ACE chiplet. +- $f_{ace, last}$: Last row in ACE chiplet. + +> $$ +> \begin{aligned} +> f_{prev} &\gets (1 - s_{d-1}) \cdot \prod_{i=1}^{d-2} s_{i} && | \deg = d-1 +> f_{ace} &\gets (1 - s_{d}) \cdot \prod_{i=1}^{d-1} s_{i} && | \deg = d +> f_{ace, first}' &\gets f_{prev} \cdot s_{d-1}' \cdot (1 - s_d') && | \deg = d + 1 +> f_{ace, next} &\gets f_{ace} \cdot (1 - s_{d}') && | \deg = d + 1 +> f_{ace, last} &\gets f_{ace} \cdot s_{d}' && | \deg = d + 1 +> \end{aligned} +> $$ + +### Section and block flags + +The selector $s_{start}$ indicates the start of a new section, from which we can derive the following flags indicating which part of the section the current row is in: + +- $f_{start}$: the current row initializes the section. +- $f_{next}$: the current and next rows are in the same section. +- $f_{end}$: the current row finalizes the section. + +> $$ +> \begin{aligned} +> f_{start} &\gets f_{ace} \cdot s_{start} && | \deg = d+1 +> f_{next} &\gets f_{ace, next} \cdot (1 - s_{start}') && | \deg = d+2 +> f_{end} &\gets f_{ace, next} \cdot s_{start}' + f_{ace,last} && | \deg = d+2 +> \end{aligned} +> $$ + +These flags require the following constraints on $s_{start}$. + +- it is binary. +- it must equal 1 in the first row. +- it must equal 0 in the last row. +- two consecutive rows cannot initialize a section, so a section contains at least two rows. + +> $$ +> \begin{aligned} +> f_{ace} \cdot s_{start} \cdot (1 - s_{start}) &= 0 && | \deg = d + 2 +> f_{ace, first}' \cdot (1 - s_{start}') &= 0 && | \deg = d + 1 +> f_{ace, last} \cdot s_{start} &= 0 && | \deg = d + 2 +> f_{ace, next} \cdot s_{start} \cdot s_{start}' &= 0 && | \deg = d + 2. +> \end{aligned} +> $$ + +A section is composed of a READ block followed by an EVAL block. +The flag indicating which block is active is derived from the binary selector $s_{block}$. +These constraints ensure they are mutually exclusive + +> $$ +> \begin{aligned} +> f_{read} \gets (1-s_{block}) & &&| \deg = 1 +> f_{eval} \gets s_{block} & &&| \deg = 1 +> +> f_{ace} \cdot (1-s_{block}) \cdot s_{block} = 0 && &| \deg = d + 2 +> \end{aligned} +> $$ + +The following constraints ensure the proper layout of the trace. In particular, it contains one or more sections each with consecutive READ and EVAL blocks. + +- The first row cannot be EVAL, so it must be READ. +- A row after EVAL cannot be READ. +- The last row cannot be READ, so it must be EVAL. + +> $$ +> \begin{aligned} +> f_{start} \cdot f_{eval} &= 0 && | \deg = d + 2 +> f_{next} \cdot f_{eval} \cdot f_{read}' &= 0 && | \deg = d + 4 +> f_{end} \cdot f_{read} &= 0 && | \deg = d + 3 +> \end{aligned} +> $$ + +In particular, we can infer from the above that + +- each section contains at least two rows (a READ and an EVAL row), and, +- a row following a READ is always in the same section. + +A READ row checks whether $id_0'$ in the next row is equal to $n_{eval}$ (the value stored in the trace, i.e. $N-1$), +in which case we ensure the following row is an EVAL. +Otherwise, $n_{eval}$ remains the same. + +> $$ +> f_{ace} \cdot f_{read} \cdot +> \big[f_{read}' \cdot n_{eval}' + f_{eval}' \cdot id_0' - n_{eval}\big] = 0 \quad | \deg = d + 3. +> $$ + +### Section constraints + +These constraints apply to all rows within the same section: + +- Across the section, $ctx$ and $clk$ are constant. +- A READ/EVAL block requests a word/element from memory, so the $ptr$ increases by 4/1, respectively in the next row. +- A READ/EVAL block adds 2/1 new nodes to the evaluation graph, so $id_0$ decreases by that amount in the next row. + +> $$ +> \begin{aligned} +> f_{next} \cdot (ctx' - ctx) &= 0 && | \deg = d + 3 +> f_{next} \cdot (clk' - clk) &= 0 && | \deg = d + 3 +> f_{next} \cdot \big[ptr' - ptr - 4 \cdot f_{read} - f_{eval}\big] &= 0 && | \deg = d + 3 +> f_{next} \cdot \big[id_0 - id_0' - 2 \cdot f_{read} - f_{eval}\big] &= 0 && | \deg = d + 3 +> \end{aligned} +> $$ + +### READ constraints + +In a READ block, each row requests a row from memory a word containing two extension field elements $v_0 = (v_{0,0}, v_{0,1})$ and $v_1 = (v_{1,0}, v_{1,1})$. +The [wire bus section](#wire-bus) describes how both of these nodes are inserted into the wire bus. + +The only constraint we enforce is that $id_0$ and $id_1$ are consecutive + +> $$ +> \begin{aligned} +> f_{ace} \cdot f_{read} \cdot (id_1 - id_0 + 1) &= 0 && | \deg = d + 2 +> \end{aligned} +> $$ + +### EVAL constraints + +An EVAL block checks that the arithmetic operation $op$ was correctly applied to inputs $v_1, v_2$ and results in $v_0$. +The result is given by the degree-4 expression + +> $$ +> v_{out} \gets op^2 \cdot \big[ v_1 + op\cdot v_2 \big] + (1 - op^2) \cdot \big[ v_1 \cdot v_2 \big] +> = \begin{cases} +> v_1 - v_2, & op = -1, +> v_1 \times v_2, & op = 0, +> v_1 + v_2, & op = 1. +> \end{cases} +> $$ + +The output node is correctly evaluated when: + +- $op \in \{-1, 0, 1\}$ is a valid arithmetic operation, and, +- $v_0$ is equal to $v_{out}$. + +> $$ +> \begin{aligned} +> f_{ace} \cdot f_{eval} \cdot op \cdot (op^2 - 1) &= 0 && | \deg = d + 4 +> f_{ace} \cdot f_{eval} \cdot (v_0 - v_{out}) &= 0 && | \deg = d + 5 +> \end{aligned} +> $$ + +The actual instruction is given by the field element $instr$ read from memory. It encodes + +- the operation $op$ using 2 bits +- the ids of $v_1$ and $v_2$ using 30 bits each and are packed as + > $$ + > instr \gets id_0 + id_1 \cdot 2^{30} + (op+1)\cdot 2^{60}. + > $$ + +It is clear from the constraint on $op$ that $op+1$ will always require 2 bits, and that range constraints on $id_1, id_2$ are unnecessary. +These ids are sent as-is to the wire bus with multiplicity $-1$. +For the logUp argument to be valid, the section must include a row where a node is added to the circuit with the same id such that the pole $\frac{-1}{w_i}$ can be annihilated. +The only way to do so is if there exists a corresponding $id_0$ matching the one in the instruction. +This is ensured by the pointers given by the chiplet bus message initializing the section, and the constraint enforcing it to be strictly increasing in each row. +Therefore, as long as the trusted circuit contains fewer than $2^{30}$ ids, the $id_1$ and $id_2$ values can never overflow this bound. + +To ensure the circuit has finished evaluating and that the final output value is 0, we enforce that the node with $id_0 = 0$ has value $v_0 = 0$ in the last row of the section. + +> $$ +> \begin{aligned} +> f_{end} \cdot id_0 &= 0 && | \deg = d + 3 +> f_{end} \cdot v_0 &= 0 && | \deg = d + 3 +> \end{aligned} +> $$ + +### Wire bus + +Each row of the chip makes up to 3 requests to the circuit's wire bus. +For $i = 0, 1, 2$, each request has the form $(ctx, clk, id_i, v_{i,0}, v_{i,1})$, which uniquely identifies a node in the DAG representing the evaluation of the circuit. +Sending this message to the bus can be viewed as updating the total degree of the node in the graph. +When performing a READ operation, a node is added to the graph, and we set its degree update $e_i$ to be equal to its final fan-out degree at the end of the evaluation. +This value is also referred to as the _multiplicity_ $m_i$. +When a node is used as an input of an arithmetic operation, we set $e_i = - 1$. + +The expression $e_i$ is derived from $m_i$ and the operation flag, so that the wire bus update is uniform across all rows of the chiplet's trace. + +- $v_0$ always defines a new node, and each operation defines its identifier $id_0$ and multiplicity $m_0$ using the same columns. + > $$ + > e_0 \gets m_0 \quad \text{| degree} = 1 + > $$ +- $v_1$ defines a new node when the operation is a READ, but is an input during an EVAL. Again, the columns for these values are identical. + > $$ + > e_1 \gets f_{read} \cdot m_1 - f_{eval} \quad \text{| degree} = 2 + > $$ +- $v_2$ is unused during a READ, and an input during EVAL + > $$ + > e_2 \gets - f_{eval} \quad \text{| degree} = 1 + > $$ + +The auxiliary logUp bus column $b_{wire}$ is updated as follows. +Given random challenges $\alpha_j$ for $j = 0, ..., 5$, let $w_i = \alpha_0 + \alpha_1 \cdot ctx + \alpha_2 \cdot clk + \alpha_3 \cdot id_i + \alpha_4 \cdot v_{i,0} + \alpha_5 \cdot v_{i,1}$ be the randomized node value. +The value of the bus in the next column is given by + +> $$ +> b_{wire}' = b_{wire} + \sum_{i=0}^2 \frac{e_i}{w_i}, +> $$ + +The actual constraint is given by normalizing the denominator + +> $$ +> f_{ace}\cdot \left( (b_{wire} - b_{wire}') \cdot \prod_{i=0}^{2}w_i + \left(e_0 \cdot w_1 \cdot w_2 + e_1 \cdot w_0 \cdot w_2 + e_2 \cdot w_0 \cdot w_1\right)\right) = 0 \quad \text{| degree} = d + 4. +> $$ + +### Chiplet and Virtual table bus + +The ACE chiplet initializes a circuit evaluation by responding to a request made by the decoder, through the [chiplet bus](./index.md#chiplets-bus) $b_{chip}$. +As mentioned earlier, the message corresponds to the tuple, which is sent to the bus only when $f_{start} = 1$. +$$(\mathsf{ACE_LABEL}, ctx, ptr, clk, n_\text{read}, n_\text{eval}).$$ +The value $n_{read}$ is computed as $id_0 - n_{eval}$, since in the first row, $id_0$ is expected to be equal to the total number of nodes inserted (subtracting 1 since identifiers are indexed from zero, and recalling the trace stores $n_{eval} = N - 1$). +We refer to the [chiplet bus constraints](./index.md#chiplets-bus-constraints) which describes the constraints for chiplet bus responses. + +The requests sent to the memory chiplet cannot use the same chiplet bus, as the decoder requests saturate the degree of constraint over its auxiliary column. +Instead, we use the [virtual table bus](./index.md#chiplets-virtual-table) $v_{table}$ which extends the chiplet bus. + +In each row, the chiplet makes one of the two following requests to the memory chiplet depending on which block it is in: + +- $(\mathsf{MEMORY\_READ\_WORD\_LABEL}, ctx, ptr, clk, v_{0,0}, v_{0,1}, v_{1,0}, v_{1,1})$, when $f_{read} = 1$. +- $(\mathsf{MEMORY\_READ\_ELEMENT\_LABEL}, ctx, ptr, clk, instr)$, when $f_{eval} = 1$. + +The values are obtained as-is from the current row, except for the instruction which is given by + +$$ +instr \gets id_0 + id_1 \cdot 2^{30} + (op+1)\cdot 2^{60}. +$$ + +As mentioned earlier, it encodes a circuit instruction applying the arithmetic operation $op \in \{- ,\times, +\}$ (mapped to the range $[0,1,2]$) to the nodes with identifiers $id_1, id_2 \in [0, 2^{30}[$. + +As usual, the messages are randomly reduced using by challenges $\alpha_0, \alpha_1, \ldots$, resulting in the degree-1 expressions +$u_{mem, read}$ and $u_{mem, eval}$, respectively. + +Since the virtual table bus is used exclusively by the chiplets trace, it must be constrained in this chiplet: + +> $$ +> f_{ace} \cdot \Big( v_{table}' \cdot \big( f_{read}\cdot w_{mem,read} + f_{eval}\cdot w_{mem,eval} \big) - v_{table}\Big) = 0 \quad | \deg = d+3. +> $$ diff --git a/versioned_docs/version-0.16/reference/miden-vm/design/chiplets/bitwise.md b/versioned_docs/version-0.16/reference/miden-vm/design/chiplets/bitwise.md new file mode 100644 index 00000000..9079b866 --- /dev/null +++ b/versioned_docs/version-0.16/reference/miden-vm/design/chiplets/bitwise.md @@ -0,0 +1,164 @@ +--- +title: "Bitwise Chiplet" +sidebar_position: 3 +--- + +# Bitwise chiplet + +In this note we describe how to compute bitwise AND and XOR operations on 32-bit values and the constraints required for proving correct execution. + +Assume that $a$ and $b$ are field elements in a 64-bit prime field. Assume also that $a$ and $b$ are known to contain values smaller than $2^{32}$. We want to compute $a \oplus b \rightarrow z$, where $\oplus$ is either bitwise AND or XOR, and $z$ is a field element containing the result of the corresponding bitwise operation. + +First, observe that we can compute AND and XOR relations for **single bit values** as follows: + +$$ +and(a, b) = a \cdot b +$$ + +$$ +xor(a, b) = a + b - 2 \cdot a \cdot b +$$ + +To compute bitwise operations for multi-bit values, we will decompose the values into individual bits, apply the operations to single bits, and then aggregate the bitwise results into the final result. + +To perform this operation we will use a table with 12 columns, and computing a single AND or XOR operation will require 8 table rows. We will also rely on two periodic columns as shown below. + +![bitwise_execution_trace](../../img/design/chiplets/bitwise/bitwise_execution_trace.png) + +In the above, the columns have the following meanings: + +- Periodic columns $k_0$ and $k_1$. These columns contain values needed to switch various constraints on or off. $k_0$ contains a single one, followed by a repeating sequence of seven zeros. $k_1$ contains a repeating sequence of seven ones, followed by a single zero. +- Input columns $a$ and $b$. On the first row of each 8-row cycle, the prover will set values in these columns to the upper 4 bits of the values to which a bitwise operation is to be applied. For all subsequent rows, we will append the next-most-significant 4-bit limb to each value. Thus, by the final row columns $a$ and $b$ will contain the full input values for the bitwise operation. +- Columns $a_0$, $a_1$, $a_2$, $a_3$, $b_0$, $b_1$, $b_2$, $b_3$ will contain lower 4 bits of their corresponding values. +- Output column $z_p$. This column represents the value of column $z$ for the prior row. For the first row, it is set to $0$. +- Output column $z$. This column will be used to aggregate the results of bitwise operations performed over columns $a_0$, $a_1$, $a_2$, $a_3$, $b_0$, $b_1$, $b_2$, $b_3$. By the time we get to the last row in each 8-row cycle, this column will contain the final result. + +## Example + +Let's illustrate the above table on a concrete example. For simplicity, we'll use 16-bit values, and thus, we'll only need 4 rows to complete the operation (rather than 8 for 32-bit values). Let's say $a = 41851$ (`b1010_0011_0111_1011`) and $b = 40426$ (`b1001_1101_1110_1010`), then $and(a, b) = 33130$ (`b1000_0001_0110_1010`). The table for this computation looks like so: + +| a | b | a0 | a1 | a2 | a3 | b0 | b1 | b2 | b3 | zp | z | +| :---: | :---: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :----: | :---: | +| 10 | 9 | 0 | 1 | 0 | 1 | 1 | 0 | 0 | 1 | 0 | 8 | +| 163 | 157 | 1 | 1 | 0 | 0 | 1 | 0 | 1 | 1 | 8 | 129 | +| 2615 | 2526 | 1 | 1 | 1 | 0 | 0 | 1 | 1 | 1 | 129 | 2070 | +| 41851 | 40426 | 1 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 2070 | 33130 | + +Here, in the first row, we set each of the $a$ and $b$ columns to the value of their most-significant 4-bit limb. The bit columns ($a_0 .. a_3$ and $b_0 .. b_3$) in the first row contain the lower 4 bits of their corresponding values (`b1010` and `b1001`). Column $z$ contains the result of bitwise AND for the upper 4 bits (`b1000`), while column $z_p$ contains that result for the prior row. + +With every subsequent row, we inject the next-most-significant 4 bits of each value into the bit columns, increase the $a$ and $b$ columns accordingly, and aggregate the result of bitwise AND into the $z$ column, adding it to $2^4$ times the value of $z$ in the previous row. We set column $z_p$ to be the value of $z$ in the prior row. By the time we get to the last row, the $z$ column contains the result of the bitwise AND, while columns $a$ and $b$ contain their original values. + +## Constraints + +AIR constraints needed to ensure the correctness of the above table are described below. We also add one more column $s$ to the execution trace, to allow us to select between two bitwise operations (`U32AND` and `U32XOR`). + +### Selectors + +The Bitwise chiplet supports two operations with the following operation selectors: + +- `U32AND`: $s = 0$ +- `U32XOR`: $s = 1$ + +Let $fb = s_0 \cdot (1 - s_1)$ be the bitwise chiplet selector flag derived from the chiplet +selectors. All constraints below are implicitly gated by $fb$ so they only apply on bitwise rows. +Degrees shown below exclude the $fb$ gate; to get the effective degree, add the degree of $fb$ +(degree $2$). + +The constraints must require that the selectors be binary and stay the same throughout the cycle: + +> $$ +> s \cdot (s - 1) = 0 \text{ | degree} = 2 +> $$ + +> $$ +> k_1 \cdot (s' - s) = 0 \text{ | degree} = 2 +> $$ + +### Input decomposition + +We need to make sure that inputs $a$ and $b$ are decomposed correctly into their individual bits. To do this, first, we need to make sure that columns $a_0$, $a_1$, $a_2$, $a_3$, $b_0$, $b_1$, $b_2$, $b_3$, can contain only binary values ($0$ or $1$). This can be accomplished with the following constraints (for $i$ ranging between $0$ and $3$): + +> $$ +> a_i \cdot (a_i - 1) = 0 \text{ | degree} = 2 +> $$ + +> $$ +> b_i \cdot (b_i - 1) = 0 \text{ | degree} = 2 +> $$ + +Then, we need to make sure that on the first row of every 8-row cycle, the values in the columns $a$ and $b$ are exactly equal to the aggregation of binary values contained in the individual bit columns $a_i$, and $b_i$. This can be enforced with the following constraints: + +> $$ +> k_0 \cdot \left(a - \sum_{i=0}^3(2^i \cdot a_i)\right) = 0 \text{ | degree} = 2 +> $$ + +> $$ +> k_0 \cdot \left(b - \sum_{i=0}^3(2^i \cdot b_i)\right) = 0 \text{ | degree} = 2 +> $$ + +The above constraints enforce that when $k_0 = 1$, $a = \sum_{i=0}^3(2^i \cdot a_i)$ and $b = \sum_{i=0}^3(2^i \cdot b_i)$. + +Lastly, we need to make sure that for all rows in an 8-row cycle except for the last one, the values in $a$ and $b$ columns are increased by the values contained in the individual bit columns $a_i$ and $b_i$. Denoting $a$ as the value of column $a$ in the current row, and $a'$ as the value of column $a$ in the next row, we can enforce these conditions as follows: + +> $$ +> k_1 \cdot \left(a' - \left(a \cdot 16 + \sum_{i=0}^3(2^i \cdot a'_i)\right)\right) = 0 \text{ | degree} = 2 +> $$ + +> $$ +> k_1 \cdot \left(b' - \left(b \cdot 16 + \sum_{i=0}^3(2^i \cdot b'_i)\right)\right) = 0 \text{ | degree} = 2 +> $$ + +The above constraints enforce that when $k_1 = 1$ , $a' = 16 \cdot a + \sum_{i=0}^3(2^i \cdot a'_i)$ and $b' = 16 \cdot b + \sum_{i=0}^3(2^i \cdot b'_i)$. + +### Output aggregation + +To ensure correct aggregation of operations over individual bits, first we need to ensure that in the first row, the aggregated output value of the previous row should be 0. +> $$ +> k_0 \cdot z_p = 0 \text{ | degree} = 2 +> $$ + +Next, we need to ensure that for each row except the last, the aggregated output value must equal the previous aggregated output value in the next row. +> $$ +> k_1 \cdot \left(z - z'_p\right) = 0 \text{ | degree} = 2 +> $$ + +Lastly, we need to ensure that for all rows the value in the $z$ column is computed by multiplying the previous output value (from the $z_p$ column in the current row) by 16 and then adding it to the bitwise operation applied to the row's set of bits of $a$ and $b$. The selector $s$ chooses between AND and XOR in the expression below. + +Let +$$ +a_{\text{and}} = \sum_{i=0}^3 2^i \cdot (a_i \cdot b_i), \qquad +a_{\text{xor}} = \sum_{i=0}^3 2^i \cdot (a_i + b_i - 2 \cdot a_i \cdot b_i). +$$ + +> $$ +> z - \left(z_p \cdot 16 + a_{\text{and}} + s \cdot (a_{\text{xor}} - a_{\text{and}})\right) = 0 \text{ | degree} = 3 +> $$ + +## Chiplets bus constraints + +To simplify the notation for describing bitwise constraints on the chiplets bus, we'll first define variable $u$, which represents how $a$, $b$, and $z$ in the execution trace are reduced to a single value. Denoting the random values received from the verifier as $\alpha_0, \alpha_1$, etc., this can be achieved as follows. + +$$ +u = \alpha_0 + \alpha_1 \cdot op_{bit} + \alpha_2 \cdot a + \alpha_3 \cdot b + \alpha_4 \cdot z +$$ + +Where, $op_{bit}$ is the unique [operation label](./index.md#operation-labels) of the bitwise operation. + +The request side of the constraint for the bitwise operation is described in the [stack bitwise operation section](../stack/u32_ops.md#u32and). + +To provide the results of bitwise operations to the chiplets bus, we want to include values of $a$, $b$ and $z$ at the last row of the cycle. + +Setting $m_i = 1 - k_{1,i}$, we can compute the permutation product from the +bitwise chiplet as follows: + +$$ +\prod_{i=0}^n (u_i \cdot m_i + 1 - m_i) +$$ + +The above ensures that when $1 - k_1 = 0$ (which is true for all rows in the 8-row cycle except for the last one), the product does not change. Otherwise, $u_i$ gets included into the product. + +The response side of the bus communication can be enforced with the following constraint: + +> $$ +> b'_{chip} = b_{chip} \cdot (u_i \cdot m_i + 1 - m_i) \text{ | degree} = 3 +> $$ diff --git a/versioned_docs/version-0.16/reference/miden-vm/design/chiplets/hasher.md b/versioned_docs/version-0.16/reference/miden-vm/design/chiplets/hasher.md new file mode 100644 index 00000000..e887db85 --- /dev/null +++ b/versioned_docs/version-0.16/reference/miden-vm/design/chiplets/hasher.md @@ -0,0 +1,309 @@ +--- +title: "Hash Chiplet" +sidebar_position: 2 +--- + +# Hash chiplet + +The hash chiplet records Poseidon2-based hash requests and connects them to the +rest of the VM through lookup buses. The Poseidon2 permutation itself is enforced +by the separate `Poseidon2PermutationAir`. + +This split gives the VM one controller trace for hash semantics and one +permutation trace for computation. If the same Poseidon2 input state is requested +more than once, the controller records each request, while the permutation AIR +executes one cycle and carries the request count as a multiplicity. + +## Supported operations + +The controller supports: + +- a single Poseidon2 permutation (`HPERM` / full-state return), +- a 2-to-1 hash, +- sequential sponge hashing over one or more rate blocks, +- Merkle path verification, +- Merkle root update. + +## Chiplet selector prefix + +The chiplets trace uses a top-level selector prefix `s0..s4`. + +| Region | Active when | +|--------|-------------| +| Hash controller | `!s0` | +| Bitwise | `s0 * !s1` | +| Memory | `s0 * s1 * !s2` | +| ACE | `s0 * s1 * s2 * !s3` | +| Kernel ROM | `s0 * s1 * s2 * s3 * !s4` | +| Padding | `s0 * s1 * s2 * s3 * s4` | + +Hash-controller rows therefore have top-level `s0 = 0`. The controller payload +starts at `chiplets[1]`, so the controller-internal selectors do not overlap +with the top-level selector prefix. + +## Controller row layout + +The hash-controller overlay occupies 19 columns, viewed as `chiplets[1..20]`. + +```text +| hs0 hs1 hs2 | state[12] | extra cols | +| | rate0[4] (= digest) | rate1[4] | capacity[4] | idx mr bnd dir perm | +``` + +The controller state is a Poseidon2 sponge state in little-endian sponge order: + +```text +[h0..h11] = [RATE0(4), RATE1(4), CAPACITY(4)] +``` + +`RATE0` (`h0..h3`) is the digest word. + +The controller-internal selectors `(hs0, hs1, hs2)` encode row kind: + +| `(hs0, hs1, hs2)` | Row kind | +|-------------------|----------| +| `(1, 0, 0)` | Sponge input (`LINEAR_HASH`, 2-to-1 hash, `HPERM`) | +| `(1, 0, 1)` | Merkle path verify input | +| `(1, 1, 0)` | Merkle update old-path input | +| `(1, 1, 1)` | Merkle update new-path input | +| `(0, 0, 0)` | Return digest | +| `(0, 0, 1)` | Return full state | +| `(0, 1, *)` | Controller padding | + +These selectors are meaningful only on hash-controller rows. Other chiplet +regions interpret the same physical columns according to their own overlays. + +## Design invariants + +The split design relies on the following invariants. + +- **Only controller rows expose hasher semantics to the VM.** The decoder, stack, + and recursive verifier communicate with the hasher through controller rows on + the chiplets bus. The Poseidon2 permutation AIR is internal computation. +- **Controller rows form request pairs.** Each request has an input row followed + by an output row. The input row contains the pre-permutation state; the output + row contains the post-permutation state. +- **A request pair has one permutation id.** The controller constrains + `perm_id` to be equal on the input and output rows of the pair. +- **Permutation cycles have stable ids.** `Poseidon2PermutationAir` starts at + `perm_id = 0`, keeps the id constant inside each 16-row cycle, and increments + by one when a cycle ends. +- **Multiplicity is cycle-wide.** One Poseidon2 cycle represents one unique + input state. Its multiplicity is the number of controller requests that use + that state. +- **Merkle routing is controller-local.** `node_index`, `direction_bit`, and + `mrupdate_id` have controller semantics. The permutation AIR carries only the + Poseidon2 state, row-scheduled witnesses, multiplicity, and `perm_id`. +- **Sibling-table balancing is partitioned by `mrupdate_id`.** The old-path and + new-path legs of one `MRUPDATE` share the same `mrupdate_id`, while different + updates use different ids. + +## Request lifecycle + +Each permutation request is recorded as two consecutive controller rows: + +- an input row containing the pre-permutation state, +- an output row containing the post-permutation state. + +The first trace row must be a controller input row. Input rows cannot terminate +the controller section, and output rows cannot be followed by output rows. Once +controller padding starts, it remains padding until the next chiplet region. + +The controller trace is padded to `CONTROLLER_TRACE_ALIGNMENT = 8` rows before +the next chiplet region starts. Padding rows use the controller padding selector +pattern and do not participate in hash buses. + +The trace builder also materializes the corresponding permutation cycles into +`Poseidon2PermutationAir`. One cycle is emitted per unique input state, with a +multiplicity column recording how many controller requests use that state. +Padding cycles have multiplicity zero. + +## Poseidon2 permutation AIR + +`Poseidon2PermutationAir` contains one 16-row cycle per unique permutation input. +The state stored on each row is the pre-transition state for that packed step; +row 15 stores the final permutation output. + +The 31-step Poseidon2 schedule is packed as follows: + +| Row | Meaning | +|-----|---------| +| 0 | initial linear layer plus first initial external round | +| 1 | second initial external round | +| 2 | third initial external round | +| 3 | fourth initial external round | +| 4 | internal rounds 1, 2, and 3 | +| 5 | internal rounds 4, 5, and 6 | +| 6 | internal rounds 7, 8, and 9 | +| 7 | internal rounds 10, 11, and 12 | +| 8 | internal rounds 13, 14, and 15 | +| 9 | internal rounds 16, 17, and 18 | +| 10 | internal rounds 19, 20, and 21 | +| 11 | final internal round plus first terminal external round | +| 12 | second terminal external round | +| 13 | third terminal external round | +| 14 | fourth terminal external round | +| 15 | output row | + +The permutation AIR has three witness columns. On rows 4 through 10 they hold +the three S-box outputs for the packed internal rounds. On row 11, `witnesses[0]` +holds the final internal-round S-box output. On rows 0 and 15, `witnesses[0]` +holds the perm-link multiplicity for the cycle. Unused witness cells are +constrained to zero by the permutation step constraints. + +The periodic columns describe the fixed 16-row schedule: + +- one selector for row 0, +- one selector for plain external-round rows, +- one selector for packed-internal rows, +- one selector for row 11, +- twelve round-constant columns. + +The final internal-round constant is used directly by the row-11 constraint +rather than occupying a periodic column with fifteen zero rows. + +## Sponge operations + +Sequential hashing is represented as a chain of controller request pairs: + +- the first input row has `is_boundary = 1`, +- continuation rows have `is_boundary = 0`, +- the final output row has `is_boundary = 1`. + +Across continuation boundaries, the next input row overwrites the rate lanes and +preserves the previous permutation's capacity word. This binds a multi-block +sponge computation into one continuous state transition. + +## Merkle operations + +Merkle verification and update rows also use: + +- `node_index`, +- `direction_bit`, +- `mrupdate_id`. + +The controller AIR enforces: + +- index decomposition `idx = 2 * idx_next + direction_bit` on Merkle input rows, +- direction-bit booleanity, +- continuity of the shifted index across non-final controller boundaries, +- zero capacity on Merkle input rows, +- digest routing into the correct rate half for the next path step. + +On non-final Merkle boundaries, the output row carries the next step's +`direction_bit`. This lets the AIR route the current digest into either `RATE0` +or `RATE1` of the next Merkle input row. + +For `MRUPDATE`, the old-path and new-path legs share the same `mrupdate_id`. +Different updates use different IDs, so sibling-table entries from unrelated +updates cannot cancel each other. + +## Lookup buses {#lookup-buses} + +The hash controller participates in three lookup constructions. + +### Chiplets bus + +The controller sends and receives the chiplets-bus messages used by the decoder, +stack, and recursive verifier. Examples include: + +- full-state sponge starts, +- rate-only sponge continuations, +- selected Merkle leaf words, +- digest returns, +- full-state returns. + +The Poseidon2 permutation AIR does not contribute to this bus. + +### Permutation link + +The `v_wiring` bus links controller rows to `Poseidon2PermutationAir`: + +- controller input rows contribute `+1 / msg_in`, +- controller output rows contribute `+1 / msg_out`, +- Poseidon2 cycle row 0 contributes `-m / msg_in`, +- Poseidon2 cycle row 15 contributes `-m / msg_out`, + +where `m` is the permutation-cycle multiplicity. + +The input and output sides use separate bus domains. Each message contains +`perm_id` plus the full Poseidon2 state. The state starts at the same beta-power +offset used by full-state hasher messages; the beta slot between `perm_id` and +the state is intentionally unused for layout alignment. + +This bus makes permutation deduplication sound: every controller request must be +matched by a permutation cycle with the same input and output states. The +`perm_id` is required because the bus balances input and output multisets +separately. Without the controller-side equality constraint on `perm_id`, a +prover could swap `(perm_id, output_state)` tuples across requests while keeping +the LogUp sums balanced. The controller pair constraint rejects that swap, and +the permutation AIR transition constraints tie each cycle's row-15 output state +to its row-0 input state. + +### Hash-kernel table {#sibling-table-constraints} + +During `MRUPDATE`, old-path rows insert sibling entries into the virtual +hash-kernel table and new-path rows remove them. The entries are keyed by +`mrupdate_id`, `node_index`, the sibling word, and the branch side, so the +running product balances only when the old and new legs of the same update use +the same siblings. + +## AIR obligations + +The hash-controller constraints enforce: + +- top-level chiplet selector ordering through the shared chiplet selector system, +- controller row-kind selector booleanity, +- first-row input boundary, +- input-to-output adjacency, +- output non-adjacency, +- controller padding stability, +- equality of `perm_id` across each input/output pair, +- capacity preservation across sponge continuations, +- Merkle index and direction-bit routing, +- `mrupdate_id` progression for Merkle root updates. + +The Poseidon2 permutation AIR enforces: + +- the packed 16-row Poseidon2 transition schedule, +- zeroing of unused witness cells, +- stable `perm_id` inside each cycle, +- consecutive `perm_id` values across cycle boundaries. + +The perm-link lookup argument binds the two AIRs together by matching +controller-row messages against row 0 and row 15 of each Poseidon2 cycle. + +## Implementation map + +The hasher design is implemented across the following files: + +- `air/src/constraints/chiplets/selectors.rs` + Top-level chiplet selector prefix, booleanity, ordering, and precomputed + `ChipletFlags`. + +- `air/src/constraints/chiplets/hasher_control/mod.rs` + Hash-controller constraints: lifecycle, padding, sponge capacity preservation, + Merkle routing, `mrupdate_id` progression, and pair-level `perm_id` equality. + +- `air/src/constraints/chiplets/hasher_control/flags.rs` + Named row-kind flags derived from the controller-internal selectors. + +- `air/src/constraints/poseidon2_permutation/` + Separate Poseidon2 permutation AIR: packed transition schedule, periodic + columns, cycle-id constraints, and witness zeroing. + +- `air/src/constraints/lookup/buses/chiplets.rs` + Hasher messages visible to the rest of the VM through `b_chiplets`. + +- `air/src/constraints/lookup/buses/wiring.rs` + Controller-to-permutation perm-link relation on the shared `v_wiring` column. + +- `air/src/constraints/lookup/poseidon2_permutation_air.rs` + Poseidon2-side perm-link removals from rows 0 and 15 of each cycle. + +- `air/src/constraints/lookup/buses/hash_kernel.rs` + Sibling-table balancing for Merkle root updates. + +- `processor/src/trace/chiplets/hasher/` + Trace generation for controller rows, request deduplication, `perm_id` + assignment, and Poseidon2 permutation cycles. diff --git a/versioned_docs/version-0.16/reference/miden-vm/design/chiplets/index.md b/versioned_docs/version-0.16/reference/miden-vm/design/chiplets/index.md new file mode 100644 index 00000000..56db3f14 --- /dev/null +++ b/versioned_docs/version-0.16/reference/miden-vm/design/chiplets/index.md @@ -0,0 +1,266 @@ +--- +title: "Chiplets" +sidebar_position: 1 +--- + +# Chiplets + +The Chiplets module contains specialized components dedicated to accelerating complex computations. Each chiplet specializes in executing a specific type of computation and is responsible for proving both the correctness of its computations and its own internal consistency. + +Currently, Miden VM relies on 5 chiplets: + +- The [Hash Chiplet](./hasher.md) (also referred to as the Hasher), used to compute Poseidon2 hashes both for sequential hashing and for Merkle tree hashing. +- The [Bitwise Chiplet](./bitwise.md), used to compute bitwise operations (e.g., `AND`, `XOR`) over 32-bit integers. +- The [Memory Chiplet](./memory.md), used to support random-access memory in the VM. +- The [Arithmetic Circuit Evaluation (ACE)](./ace.md), used to ensure that arithmetic circuits evaluate to zero. +- The [Kernel ROM Chiplet](kernel_rom.md), used to enable executing kernel procedures during the [`SYSCALL` operation](../programs.md#syscall-block). + +Each chiplet executes its computations separately from the rest of the VM and proves the internal correctness of its execution trace in a unique way that is specific to the operation(s) it supports. These methods are described by each chiplet’s documentation. + +## Chiplets module trace + +The execution trace of the Chiplets module is generated by stacking the execution traces of each of its chiplet components. Because each chiplet is expected to generate significantly fewer trace rows than the other VM components (i.e., the decoder, stack, and range checker), stacking them enables the same functionality without adding as many columns to the execution trace. + +Each chiplet is identified within the Chiplets module by one or more chiplet selector columns which cause its constraints to be selectively applied. + +The result is an execution trace of 21 trace columns, which allows space for the widest chiplet component (the hash controller, at 19 internal columns) plus the shared chiplet selector prefix and row clock. + +![chiplets](../../img/design/chiplets/chiplets.png) + +During the finalization of the overall execution trace, the chiplets' traces (including internal selectors) are appended to the trace of the Chiplets module one after another, as pictured. Thus, when one chiplet's trace ends, the trace of the next chiplet starts in the subsequent row. + +Additionally, a padding segment is added to the end of the Chiplets module's trace so that the number of rows in the table always matches the overall trace length of the other VM processors, regardless of the length of the chiplet traces. Padding rows use the all-ones selector prefix and zero payload columns. + +### Chiplets order + +The order in which the chiplets are stacked is determined by the requirements of each chiplet, including the width of its execution trace and the degree of its constraints. + +For simplicity, all of the "cyclic" chiplets which operate in multi-row cycles and require starting at particular row increments should come before any non-cyclic chiplets, and these should be ordered from longest-cycle to shortest-cycle. This avoids any additional alignment padding between chiplets. + +After that, chiplets are ordered by degree of constraints so that higher-degree chiplets get lower-degree chiplet selector flags. + +The resulting order is as follows: + +| Chiplet | Row Alignment | Internal Degree | Chiplet Selector Degree | Total Degree | Columns | Chiplet Selector Flag | +| --------------- | :----------: | :-------------: | :---------------------: | :----------: | :-----: | --------------------- | +| Hash controller | 8 | 8 | 1 | 9 | 19 | $\{0\}$ | +| Bitwise chiplet | 8 | 3 | 2 | 5 | 13 | $\{1, 0\}$ | +| Memory | - | 6 | 3 | 9 | 17 | $\{1, 1, 0\}$ | +| ACE | - | 5 | 4 | 9 | 16 | $\{1, 1, 1, 0\}$ | +| Kernel ROM | - | 3 | 5 | 8 | 5 | $\{1, 1, 1, 1, 0\}$ | +| Padding | - | - | - | - | - | $\{1, 1, 1, 1, 1\}$ | + +### Additional requirements for stacking execution traces + +Stacking the chiplets introduces one new complexity. Each chiplet proves its own correctness with its own set of internal transition constraints, many of which are enforced between each row in its trace and the next row. As a result, when the chiplets are stacked, transition constraints applied to the final row of one chiplet will cause a conflict with the first row of the following chiplet. + +This is true for any transition constraints that are applied at every row and selected by a `Chiplet Selector Flag` for the current row. (Therefore, cyclic transition constraints controlled by periodic columns do not cause any issue.) + +This requires the following adjustments for each chiplet. + +**In the hash chiplet:** controller constraints explicitly confine the end of the controller region. The controller region is padded to an 8-row boundary before the bitwise region begins. Poseidon2 permutation constraints live in `Poseidon2PermutationAir`, not in the chiplets trace. + +**In the bitwise chiplet:** there is no conflict, and therefore no change, since all constraints are periodic. + +**In the memory chiplet:** all transition constraints cause a conflict. To adjust for this, the selector flag for the memory chiplet is designed to exclude its last row. Thus, memory constraints will not be applied when transitioning from the last row of the memory chiplet to the following row. This is achieved without any additional increase in the degree of constraints by using $s'_2$ as a selector instead of $s_2$ as seen [below](#chiplet-constraints). + +**In the ACE chiplet:** some transition constraints must be disabled in the last row. The flags are derived both from the chiplet selectors and are described in the [flags and boundary constraints section](./ace.md#flags). + +**In the kernel ROM chiplet:** the transition constraints referring to the $s_{first}'$ column cause a conflict. +It is resolved by enforcing the initial value of this selector in the last row of the previous chiplet, +and disabling the hash equality constraint in the last row. + +## Operation labels + +Each operation supported by the chiplets is given a unique identifier to ensure that the requests and responses sent to the [chiplets bus](#chiplets-bus) ($b_{chip}$) are indeed processed by the intended chiplet for that operation and that chiplets which support more than one operation execute the correct one. + +The labels are composed from the flag values of the chiplet selector(s) and internal operation selectors (if applicable). +The unique label of the operation is computed as the binary aggregation of the combined selectors plus $1$, note that the combined flag is represented in big-endian, so the bit representation below is reversed. + +| Operation | Chiplet & Internal
Selector Flags | Label | Value | +| ---------------------- | ---------------------------------------- | --------------- | ----- | +| `HASHER_LINEAR_HASH` | $\{0 \,\|\, 1, 0, 0\}$ | `1 + 0b001_0` | 3 | +| `HASHER_MP_VERIFY` | $\{0 \,\|\, 1, 0, 1\}$ | `1 + 0b101_0` | 11 | +| `HASHER_MR_UPDATE_OLD` | $\{0 \,\|\, 1, 1, 0\}$ | `1 + 0b011_0` | 7 | +| `HASHER_MR_UPDATE_NEW` | $\{0 \,\|\, 1, 1, 1\}$ | `1 + 0b111_0` | 15 | +| `HASHER_RETURN_HASH` | $\{0 \,\|\, 0, 0, 0\}$ | `1 + 0b000_0` | 1 | +| `HASHER_RETURN_STATE` | $\{0 \,\|\, 0, 0, 1\}$ | `1 + 0b100_0` | 9 | +| `BITWISE_AND` | $\{1, 0 \,\|\, 0\}$ | `1 + 0b0_01` | 2 | +| `BITWISE_XOR` | $\{1, 0 \,\|\, 1\}$ | `1 + 0b1_01` | 6 | +| `MEMORY_WRITE_ELEMENT` | $\{1, 1, 0 \,\|\, 0, 0\}$ | `1 + 0b00_011` | 4 | +| `MEMORY_WRITE_WORD` | $\{1, 1, 0 \,\|\, 0, 1\}$ | `1 + 0b10_011` | 20 | +| `MEMORY_READ_ELEMENT` | $\{1, 1, 0 \,\|\, 1, 0\}$ | `1 + 0b01_011` | 12 | +| `MEMORY_READ_WORD` | $\{1, 1, 0 \,\|\, 1, 1\}$ | `1 + 0b11_011` | 28 | +| `ACE_INIT` | $\{1, 1, 1, 0 \,\|\, - \}$ | `1 + 0b_0111` | 8 | +| `KERNEL_PROC_CALL` | $\{1, 1, 1, 1, 0 \,\|\, 0\}$ | `1 + 0b0_01111` | 16 | +| `KERNEL_PROC_INIT` | $\{1, 1, 1, 1, 0 \,\|\, 1\}$ | `1 + 0b1_01111` | 48 | + +## Chiplets module constraints + +### Chiplet constraints + +Each chiplet's internal constraints are defined in the documentation for the individual chiplets. To ensure that constraints are only ever selected for one chiplet at a time, the module's selector columns $s_0, s_1, s_2, s_3, s_4$ are combined into flags. Each chiplet's internal constraints are multiplied by its chiplet selector flag, and the degree of each constraint is correspondingly increased. + +This gives the following sets of constraints: + +> $$ +> (1 - s_0) \cdot c_{hash} = 0 \text{ | degree} = 1 + \deg(c_{hash}) +> $$ + +> $$ +> s_0 \cdot (1 - s_1) \cdot c_{bitwise} = 0 \text{ | degree} = 2 + \deg(c_{bitwise}) +> $$ + +> $$ +> s_0 \cdot s_1 \cdot (1 - s'_2) \cdot c_{memory} = 0 \text{ | degree} = 3 + \deg(c_{memory}) +> $$ + +> $$ +> s_0 \cdot s_1 \cdot (s_2) \cdot (1 - s'_3) \cdot c_{ace} = 0 \text{ | degree} = 4 + \deg(c_{ace}) +> $$ + +> $$ +> s_0 \cdot s_1 \cdot (s_2) \cdot (s_3) \cdot (1 - s'_4) \cdot c_{krom} = 0 \text{ | degree} = 5 + \deg(c_{krom}) +> $$ + +In the above: + +- $c_{hash}, c_{bitwise}, c_{memory}, c_{ace}, c_{krom}$ each represent an internal constraint from the indicated chiplet. +- $\deg(c)$ indicates the degree of the specified constraint. +- flags are applied in a like manner for all internal constraints in each respective chiplet. +- the selector for the memory chiplet excludes the last row of the chiplet (as discussed [above](#additional-requirements-for-stacking-execution-traces)). + +### Chiplet selector constraints + +We also need to ensure that the chiplet selector columns are set correctly. The stacked trace design means that selector columns become active one prefix level at a time. Thus, selector constraints should only be applied to selector columns when they are acting as selectors. + +- $s_0$ acts as a selector for the entire trace. +- $s_1$ acts as a selector column when $s_0 = 1$. +- $s_2$ acts as a selector column when $s_0 = 1$ and $s_1 = 1$. +- $s_3$ acts as a selector column when $s_0 = 1$, $s_1 = 1$, and $s_2 = 1$. +- $s_4$ acts as a selector column when $s_0 = 1$, $s_1 = 1$, $s_2 = 1$, and $s_3 = 1$. + +Two conditions must be enforced for columns acting as chiplet selectors. + +1. When acting as a selector, the value in the selector column must be binary. +2. When acting as a selector, the value in the selector column may only change from $0 \rightarrow 1$. + +The following constraints ensure that selector values are binary. + +> $$ +> s_0^2 - s_0 = 0 \text{ | degree} = 2 +> s_0 \cdot (s_1^2 - s_1) = 0 \text{ | degree} = 3 +> s_0 \cdot s_1 \cdot (s_2^2 - s_2) = 0 \text{ | degree} = 4 +> s_0 \cdot s_1 \cdot s_2 \cdot (s_3^2 - s_3) = 0 \text{ | degree} = 5 +> s_0 \cdot s_1 \cdot s_2 \cdot s_3 \cdot (s_4^2 - s_4) = 0 \text{ | degree} = 6 +> $$ + +The following constraints ensure that the chiplets are stacked correctly by restricting selector values so they can only change from $0 \rightarrow 1$. + +> $$ +> s_0 \cdot (s'_0 - s_0) = 0 \text{ | degree} = 2 +> s_0 \cdot s_1 \cdot (s'_1 - s_1) = 0 \text{ | degree} = 3 +> s_0 \cdot s_1 \cdot s_2 \cdot (s'_2 - s_2) = 0 \text{ | degree} = 4 +> s_0 \cdot s_1 \cdot s_2 \cdot s_3 \cdot (s'_3 - s_3) = 0 \text{ | degree} = 5 +> s_0 \cdot s_1 \cdot s_2 \cdot s_3 \cdot s_4 \cdot (s'_4 - s_4) = 0 \text{ | degree} = 6 +> $$ + +In other words, the above constraints enforce that if a selector is $0$ in the current row, then it must be either $0$ or $1$ in the next row; if it is $1$ in the current row, it must be $1$ in the next row. + +## Chiplets bus + +The chiplets must be explicitly connected to the rest of the VM in order for it to use their operations. This connection must prove that all specialized operations which a given VM component claimed to offload to one of the chiplets were in fact executed by the correct chiplet with the same set of inputs and outputs as those used by the offloading component. + +This is achieved via a [bus](../lookups/index.md#communication-buses-in-miden-vm) called $b_{chip}$ where a request can be sent to any chiplet and a corresponding response will be sent back by that chiplet. + +The bus is implemented as a single [running product column](../lookups/multiset.md) where: + +- Each request is “sent” by computing an operation-specific lookup value from an [operation-specific label](#operation-labels), the operation inputs, and the operation outputs, and then dividing it out of the $b_{chip}$ running product column. +- Each chiplet response is “sent” by computing the same operation-specific lookup value from the label, inputs, and outputs, and then multiplying it into the $b_{chip}$ running product column. + +Thus, if the requests and responses match, then the bus column $b_{chip}$ starts at $1$ and ends at the product of randomness-reduced kernel procedure digests. The initial value is enforced by a first-row boundary constraint, while the final value is verified via `aux_finals`. + +Note that the order of the requests and responses does not matter, as long as they are all included in $b_{chip}$. In fact, requests and responses for the same operation will generally occur at different cycles. + +### Chiplets bus constraints + +The running product constraint is + +$$ +(b'_{chip} \cdot req - b_{chip} \cdot resp) = 0 +$$ + +Here, $\mathit{req}$ and $\mathit{resp}$ are the request/response multipliers formed by combining +randomness-reduced messages with their selector flags. In addition, the first row is fixed to +$b_{chip} = 1$. + +$$ +f_{first} \cdot (b_{chip} - 1) = 0 +$$ + +Lookup requests are sent to the chiplets bus by the following components: + +- The stack sends requests for [bitwise](../stack/u32_ops.md#u32and), [memory](../stack/io_ops.md#memory-access-operations), and [cryptographic hash operations](../stack/crypto_ops.md). +- The decoder sends requests for [hash operations](../decoder/index.md#program-block-hashing) for program block hashing. +- The decoder sends a procedure access request to the [Kernel ROM chiplet](./kernel_rom.md) for each `SYSCALL` during [program block hashing](../decoder/index.md#program-block-hashing). +- The verifier initializes the bus with requests to the [Kernel ROM chiplet](./kernel_rom.md) for each unique kernel procedure digest. + +Responses are provided by the [hash](./hasher.md#chiplets-bus-constraints), [bitwise](./bitwise.md#chiplets-bus-constraints), [memory](./memory.md#chiplets-bus-constraints), and [kernel ROM](./kernel_rom.md#chiplets-bus-constraints) chiplets. + +The verifier computes the expected final value of $b_{chip}$ from public inputs and checks it +against `aux_finals`. There is no explicit last-row boundary constraint in the chiplets bus. + +## Chiplets virtual table + +_Note: over time, the use of this construction has evolved to the point where its name doesn't match the way it is used. This is documented in [issue #1779](https://github.com/0xMiden/miden-vm/issues/1779)._ + +The [virtual table](../lookups/multiset.md#virtual-tables) bus $vt_{chip}$ is used by several chiplets as a way to maintain and enforce the correctness of their internal states, and enable communication with each other. + +The hasher chiplet uses it as a way to store [sibling nodes](./hasher.md#sibling-table-constraints) when performing a Merkle tree update. +In particular, it expects an empty bus at the start of this operation, and ensures that all entries it inserts are removed once the new tree is finalized. +Consequently, the column representing this table must be equal to 1 at the boundaries of the hasher chiplet's trace, preventing communication with other chiplets. + +Other chiplets use the table as an extension of the chiplet bus, since both multi-sets are merged in the last row of the overall trace. + +This enables chiplets to make bus requests to other chiplets, without affecting the degree of the chiplet bus. +As currently implemented, a single constraint is required to include all requests made by the main trace and corresponding responses from the chiplets. +The degree of this constraint is the maximum of both message types and is currently reached by the requests by the main trace, +preventing chiplets from performing any requests using the same bus. +Instead, a chiplet can make a request through the $vt_{chip}$ bus, with the receiving chiplet responding through the main chiplet bus $b_{chip}$. + +At the moment, this feature is only used by the [ACE](./ace.md) allowing it to read inputs and circuit instructions stored in the memory chiplet. +Note that the [memory](./memory.md#chiplets-bus-constraints) chiplet responds via the chiplet bus $b_{chip}$. + +### Chiplets virtual table constraints + +The hash-kernel virtual table bus uses a running product constraint similar to the chiplets bus: + +$$ +(p' \cdot req - p \cdot resp) = 0 +$$ + +To combine these correctly, the running product column must be constrained not only at the +beginning and the end of the trace, but also where the hash chiplet ends. Using the hasher chiplet's +selector $s_0$, the following constraint ensures the bus equals one whenever $s_0$ transitions: + +> $$ +> (s'_0 - s_0) \cdot (1 - vt_{chip}) = 0 \text{ | degree} = 2 +> $$ + +To connect the chiplet virtual table and the bus, we enforce the following constraint in the last +row, ensuring the product of both their running products is 1: + +> $$ +> vt_{chip} \cdot b_{chip} - 1 = 0 \text{ | degree} = 2. +> $$ + +TODO: today we enforce that this bus is empty (equals 1) at Merkle path verification boundaries. +This is sound but cumbersome and interferes with log_deferred state tracking. We plan to replace +it with a construction that supports multiple Merkle path verifications without forcing a boundary +reset. + +## Chiplet logUp bus + +An auxiliary [logUp bus](../lookups/logup.md) is available to chiplets, though it is currently only used by the [ACE chiplet](./ace.md#wire-bus) to check the wiring of the arithmetic circuit being evaluated. We refer to that chiplet's documentation for constraint applied to the corresponding auxiliary column. + +Since this column could later be used by other chiplets, we require boundary constraints over the entire chiplet trace to constrain the running sum to be zero in the first and final row of the auxiliary column. diff --git a/versioned_docs/version-0.16/reference/miden-vm/design/chiplets/kernel_rom.md b/versioned_docs/version-0.16/reference/miden-vm/design/chiplets/kernel_rom.md new file mode 100644 index 00000000..648d3fbc --- /dev/null +++ b/versioned_docs/version-0.16/reference/miden-vm/design/chiplets/kernel_rom.md @@ -0,0 +1,61 @@ +--- +title: "Kernel ROM Chiplet" +sidebar_position: 6 +--- + +# Kernel ROM chiplet + +The kernel ROM enables executing predefined kernel procedures. +These procedures are always executed in the root context and can only be accessed by a `SYSCALL` operation. +The chiplet tracks and enforces correctness of all kernel procedure calls as well as maintaining a list of all the procedures defined for the kernel, whether they are executed or not. +More background about Miden VM execution contexts can be found [here](../../user_docs/assembly/execution_contexts.md). + +## Kernel ROM trace {#constraints} + +The kernel ROM table consists of five columns, with exactly one row per declared kernel procedure. +The following example table shows the execution trace for three procedures with digests $a, b, c$, called 1, 2, and 0 times respectively. + +| $m$ | $r_0$ | $r_1$ | $r_2$ | $r_3$ | +|-----|-------|-------|-------|-------| +| 1 | $a_0$ | $a_1$ | $a_2$ | $a_3$ | +| 2 | $b_0$ | $b_1$ | $b_2$ | $b_3$ | +| 0 | $c_0$ | $c_1$ | $c_2$ | $c_3$ | + +Column meanings: + +- $m$ is the CALL-label multiplicity — the number of times the procedure was invoked by a `SYSCALL`. It may be zero for procedures declared in the kernel but never called. +- $r_0, \ldots, r_3$ contain the digest of the kernel procedure. + +## Main-trace constraints + +The kernel ROM chiplet has **no main-trace shape constraints** under the all-LogUp layout. +Earlier designs carried a binary "first-row-of-block" selector, a digest-contiguity rule, and an entry-row anchor to shape the trace for a permutation argument. +LogUp replaces those with multiset equality under a random challenge $\alpha$, so any prover assignment to $(m, r_0, \ldots, r_3)$ that balances the chiplets bus is sound; no extra shape constraints are required. + +## Chiplets bus constraints + +The kernel ROM chiplet emits two fractions on the chiplets bus $b_{chip}$ per active row, gated by the selector flag $f_{krom}$. +Let + +$$ +\begin{aligned} +\tilde{r} &= \sum_{i=0}^{3} \alpha_{i+2} \cdot r_i \\ +v_{init} &= \alpha_0 + \alpha_1 \cdot \textsf{KERNEL\_PROC\_INIT} + \tilde{r} \\ +v_{call} &= \alpha_0 + \alpha_1 \cdot \textsf{KERNEL\_PROC\_CALL} + \tilde{r} +\end{aligned} +$$ + +denote the two encoded bus messages for a row's digest. Here $\textsf{KERNEL\_PROC\_INIT}$ and $\textsf{KERNEL\_PROC\_CALL}$ are the unique [operation labels](./index.md#operation-labels), and $\alpha_i$ are challenges received from the verifier. + +The chiplet contributes to $b_{chip}$ via + +> $$ +> f_{krom} \cdot \left( -\frac{1}{v_{init}} + \frac{m}{v_{call}} \right) +> $$ + +- The **INIT term** removes exactly one fraction per declared procedure. It is balanced by the public-input boundary term the verifier injects on $b_{chip}$ (one add per kernel procedure digest read from public inputs). This anchors every chiplet row to a declared procedure: a forged row would leave an unmatched INIT remove. +- The **CALL term** contributes $m$ fractions. Each `SYSCALL` in the decoder emits one matching remove on $b_{chip}$. Bus balance forces $m$ to equal the true syscall count for that procedure. + +The full set of constraints applied to $b_{chip}$ (including the public-input boundary term for INIT) is described in the [chiplets bus constraints](../chiplets/index.md#chiplets-bus-constraints). + +By using the bus this way, the verifier only learns which procedures can be invoked, not how often they were called — the multiplicity $m$ is a private witness that only reaches the verifier through the bus balance. diff --git a/versioned_docs/version-0.16/reference/miden-vm/design/chiplets/memory.md b/versioned_docs/version-0.16/reference/miden-vm/design/chiplets/memory.md new file mode 100644 index 00000000..772d338a --- /dev/null +++ b/versioned_docs/version-0.16/reference/miden-vm/design/chiplets/memory.md @@ -0,0 +1,369 @@ +--- +title: "Memory Chiplet" +sidebar_position: 4 +--- + +# Memory chiplet + +Miden VM supports linear read-write random access memory. This memory is element-addressable, meaning that a single value is located at each address, although reading and writing values to/from memory in batches of four is supported. Each value is a field element in a $64$-bit prime field with modulus $2^{64} - 2^{32} + 1$. A memory address is a field element in the range $[0, 2^{32})$. + +In this note we describe the rationale for selecting the above design and describe AIR constraints needed to support it. + +The design makes extensive use of $16$-bit range checks. An efficient way of implementing such range checks is described [here](../range.md). + +## Alternative designs + +The simplest (and most efficient) alternative to the above design is contiguous write-once memory. To support such memory, we need to allocate just two trace columns as illustrated below. + +![memory_alternative_design](../../img/design/chiplets/memory/memory_alternative_design.png) + +In the above, `addr` column holds memory address, and `value` column holds the field element representing the value stored at this address. Notice that some rows in this table are duplicated. This is because we need one row per memory access (either read or write operation). In the example above, value $b$ was first stored at memory address $1$, and then read from this address. + +The AIR constraints for this design are very simple. First, we need to ensure that values in the `addr` column either remain the same or are incremented by $1$ as we move from one row to the next. This can be achieved with the following constraint: + +$$ +(a' - a) \cdot (a' - a - 1) = 0 +$$ + +where $a$ is the value in `addr` column in the current row, and $a'$ is the value in this column in the next row. + +Second, we need to make sure that if the value in the `addr` column didn't change, the value in the `value` column also remained the same (i.e., a value stored in a given address can only be set once). This can be achieved with the following constraint: + +$$ +(v' - v) \cdot (a' - a - 1) = 0 +$$ + +where $v$ is the value in `value` column at the current row, and $v'$ is the value in this column in the next row. + +As mentioned above, this approach is very efficient: each memory access requires just $2$ trace cells. + +### Read-write memory + +Write-once memory is tricky to work with, and many developers may need to climb a steep learning curve before they become comfortable working in this model. Thus, ideally, we'd want to support read-write memory. To do this, we need to introduce additional columns as illustrated below. + +![memory_read_write](../../img/design/chiplets/memory/memory_read_write.png) + +In the above, we added `clk` column, which keeps track of the clock cycle at which memory access happened. We also need to differentiate between memory reads and writes. To do this, we now use two columns to keep track of the value: `old val` contains the value stored at the address before the operation, and `new val` contains the value after the operation. Thus, if `old val` and `new val` are the same, it was a read operation. If they are different, it was a write operation. + +The AIR constraints needed to support the above structure are as follows. + +We still need to make sure memory addresses are contiguous: + +$$ +(a' - a) \cdot (a' - a - 1) = 0 +$$ + +Whenever memory address changes, we want to make sure that `old val` is set to $0$ (i.e., our memory is always initialized to $0$). This can be done with the following constraint: + +$$ +(a' - a) \cdot v_{old}' = 0 +$$ + +On the other hand, if memory address doesn't change, we want to make sure that `new val` in the current row is the same as `old val` in the next row. This can be done with the following constraint: + +$$ +(1 + a - a') \cdot (v_{new} - v_{old}') = 0 +$$ + +Lastly, we need to make sure that for the same address values in `clk` column are always increasing. One way to do this is to perform a $16$-bit range check on the value of $(i' - i - 1)$, where $i$ is the reference to `clk` column. However, this would mean that memory operations involving the same address must happen within $65536$ VM cycles from each other. This limitation would be difficult to enforce statically. To remove this limitation, we need to add two more columns as shown below: + +![memory_limitation_diagram](../../img/design/chiplets/memory/memory_limitation_diagram.png) + +In the above column `d0` contains the lower $16$ bits of $(i' - i - 1)$ while `d1` contains the upper $16$ bits. The constraint needed to enforces this is as follows: + +$$ +(1 + a - a') \cdot ((i' - i - 1) - (2^{16} \cdot d_1' + d_0')) = 0 +$$ + +Additionally, we need to apply $16$-bit range checks to columns `d0` and `d1`. + +Overall, the cost of reading or writing a single element is now $6$ trace cells and $2$ $16$-bit range-checks. + +### Non-contiguous memory + +Requiring that memory addresses are contiguous may also be a difficult limitation to impose statically. To remove this limitation, we need to introduce one more column as shown below: + +![memory_non_contiguous_memory](../../img/design/chiplets/memory/memory_non_contiguous_memory.png) + +In the above, the prover sets the value in the new column `t` to $0$ when the address doesn't change, and to $1 / (a' - a)$ otherwise. To simplify constraint description, we'll define variable $n$ computed as follows: + +$$ +n = (a' - a) \cdot t' +$$ + +Then, to make sure the prover sets the value of $t$ correctly, we'll impose the following constraints: + +$$ +n^2 - n = 0 +(1 - n) \cdot (a' - a) = 0 +$$ + +The above constraints ensure that $n=1$ whenever the address changes, and $n=0$ otherwise. We can then define the following constraints to make sure values in columns `d0` and `d1` contain either the delta between addresses or between clock cycles. + +| Condition | Constraint | Comments | +| --------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | +| $n=1$ | $(a' - a) - (2^{16} \cdot d_1' + d_0') = 0$ | When the address changes, columns `d0` and `d1` at the next row should contain the delta between the old and the new address. | +| $n=0$ | $(i' - i) - (2^{16} \cdot d_1' + d_0') = 0$ | When the address remains the same, columns `d0` and `d1` at the next row should contain the delta between the old and the new clock cycle. | + +We can combine the above constraints as follows: + +$$ +\left(n \cdot (a' - a) + (1 - n) \cdot (i' - i)\right) - (2^{16} \cdot d_1' + d_0') = 0 +$$ + +The above constraint, in combination with $16$-bit range checks against columns `d0` and `d1` ensure that values in `addr` and `clk` columns always increase monotonically, and also that column `addr` may contain duplicates. Values in `clk` may repeat for a given address, but only for read operations. + +### Context separation + +In many situations it may be desirable to assign memories to different contexts. For example, when making a cross-contract calls, the memories of the caller and the callee should be separate. That is, the caller should not be able to access the memory of the callee and vice-versa. + +To accommodate this feature, we need to add one more column as illustrated below. + +![memory_context_separation](../../img/design/chiplets/memory/memory_context_separation.png) + +This new column `ctx` should behave similarly to the address column: values in it should increase monotonically, and there could be breaks between them. We also need to change how the prover populates column `t`: + +- If the context changes, `t` should be set to the inverse $(c' - c)$, where $c$ is a reference to column `ctx`. +- If the context remains the same but the address changes, column `t` should be set to the inverse of $(a' - a)$. +- Otherwise, column `t` should be set to $0$. + +To simplify the description of constraints, we'll define two variables $n_0$ and $n_1$ as follows: + +$$ +n_0 = (c' - c) \cdot t' +n_1 = (a' - a) \cdot t' +$$ + +Thus, $n_0 = 1$ when the context changes, and $0$ otherwise. Also, $(1 - n_0) \cdot n_1 = 1$ when context remains the same and address changes, and $0$ otherwise. + +To make sure the prover sets the value of column `t` correctly, we'll need to impose the following constraints: + +$$ +n_0^2 - n_0 = 0 +(1 - n_0) \cdot (c' - c) = 0 +(1 - n_0) \cdot (n_1^2 - n_1) = 0 +(1 - n_0) \cdot (1 - n_1) \cdot (a' - a) = 0 +$$ + +We can then define the following constraints to make sure values in columns `d0` and `d1` contain the delta between contexts, between addresses, or between clock cycles. + +| Condition | Constraint | Comments | +| -------------------- | ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| $n_0=1$ | $(c' - c) - (2^{16} \cdot d_1' + d_0') = 0$ | When the context changes, columns `d0` and `d1` at the next row should contain the delta between the old and the new contexts. | +| $n_0=0$
$n_1=1$ | $(a' - a) - (2^{16} \cdot d_1' + d_0') = 0$ | When the context remains the same but the address changes, columns `d0` and `d1` at the next row should contain the delta between the old and the new addresses. | +| $n_0=0$
$n_1=0$ | $(i' - i) - (2^{16} \cdot d_1' + d_0') = 0$ | When both the context and the address remain the same, columns `d0` and `d1` at the next row should contain the delta between the old and the new clock cycle. | + +We can combine the above constraints as follows: + +$$ +\left(n_0 \cdot (c' - c) + (1 - n_0) \cdot \left(n_1 \cdot (a' - a) + (1 - n_1) \cdot (i' - i) \right) \right) - (2^{16} \cdot d_1' + d_0') = 0 +$$ + +The above constraint, in combination with $16$-bit range checks against columns `d0` and `d1` ensure that values in `ctx`, `addr`, and `clk` columns always increase monotonically, and also that columns `ctx` and `addr` may contain duplicates. Values in `clk` may repeat for a given `(ctx, addr)`, but only for read operations. + +Notice that the above constraint has degree $5$. + +## Miden approach + +While the approach described above works, it comes at significant cost. Reading or writing a single value requires $8$ trace cells and $2$ $16$-bit range checks. Assuming a single range check requires roughly $2$ trace cells, the total number of trace cells needed grows to $12$. This is about $6$x worse the simple contiguous write-once memory described earlier. + +Miden VM frequently needs to deal with batches of $4$ field elements, which we call _words_. For example, the output of Poseidon2 hash function is a single word. A single 256-bit integer value can be stored as two words (where each element contains one $32$-bit value). Thus, we can optimize for this common use case by making the chiplet handle *words* as opposed to individual elements. That is, memory is still element-addressable in that each memory address stores a single field element, and memory addresses may be read or written individually. However, the chiplet also handles reading and writing elements in batches of four simultaneously, with the restriction that such batches be *word-aligned* addresses (*i.e.* the address is a multiple of 4). + +The layout of Miden VM memory table is shown below: + +![memory_miden_vm_layout](../../img/design/chiplets/memory/memory_miden_vm_layout.png) + +where: + +- `rw` is a selector column which is set to $1$ for read operations and $0$ for write operations. +- `ew` is a selector column which is set to $1$ when a word is being accessed, and $0$ when an element is being accessed. +- `ctx` contains context ID. Values in this column must increase monotonically but there can be gaps between two consecutive values of up to $2^{32}$. Also, two consecutive values can be the same. +- `word_addr` contains the memory address of the first element in the word. Values in this column must increase monotonically for a given context but there can be gaps between two consecutive values of up to $2^{32}$. Values in this column must be divisible by 4. Also, two consecutive values can be the same. +- `idx0` and `idx1` are selector columns used to identify which element in the word is being accessed. Specifically, the index within the word is computed as `idx1 * 2 + idx0`. + - However, when `ew` is set to $1$ (indicating that a word is accessed), these columns are meaningless and are set to $0$. +- `clk` contains clock cycle at which the memory operation happened. Values in this column must increase monotonically for a given context and memory word but there can be gaps between two consecutive values of up to $2^{32}$. + - When the context and word address are unchanged, `clk` may stay the same, but only read operations are allowed. +- `v0, v1, v2, v3` columns contain field elements stored at a given context/word/clock cycle after the memory operation. +- Columns `d0` and `d1` contain lower and upper $16$ bits of the delta between two consecutive context IDs, addresses, or clock cycles. Specifically: + - When the context changes within a frame, these columns contain $(ctx' - ctx)$ in the "next" row. + - When the context remains the same but the word address changes within a frame, these columns contain $(a' - a)$ in the "next" row. + - When both the context and the word address remain the same within a frame, these columns contain $(clk' - clk)$ in the "next" row. +- Column `t` contains the inverse of the delta between two consecutive context IDs, addresses, or clock cycles. Specifically: + - When the context changes within a frame, this column contains the inverse of $(ctx' - ctx)$ in the "next" row. + - When the context remains the same but the word address changes within a frame, this column contains the inverse of $(a' - a)$ in the "next" row. +- When both the context and the word address remain the same within a frame, this column contains the inverse of $(clk' - clk)$ in the "next" row. +- Column `f_scw` stands for "flag same context and word address", which is set to $1$ when the current and previous rows have the same context and word address, and $0$ otherwise. + +For every memory access operation (i.e., read or write a word or element), a new row is added to the memory table. If neither `ctx` nor `addr` have changed, the `v` columns are set to equal the values from the previous row (except for any element written to). If `ctx` or `addr` have changed, then the `v` columns are initialized to $0$ (except for any element written to). + +### AIR constraints + +We first define the memory chiplet selector flags. $s_0$, $s_1$ and $s_2$ will refer to the chiplet selector flags. + +- $f_{mem}$ is set to 1 when the current row is in the memory chiplet. +$$ +f_{mem} = s_0 \cdot s_1 \cdot (1 - s_2) \text{ | degree} = 3 +$$ + +- $f_{mem\_nl}$ is set to 1 when the current row is in the memory chiplet, except for the last row of the chiplet. + +$$ +f_{mem\_nl} = s_0 \cdot s_1 \cdot (1 - s_2') \text{ | degree} = 3 +$$ + +- $f_{mem\_fr}$ is set to 1 when the next row is the first row of the memory chiplet. + +$$ +f_{mem\_fr} = (1 - s_1) \cdot s_0 \cdot s_1' \cdot (1 - s_2') \text{ | degree} = 4 +$$ + +To simplify description of constraints, we'll define two variables $n_0$ and $n_1$ as follows: + +$$ +n_0 = \Delta ctx \cdot t' +n_1 = \Delta a \cdot t' +$$ + +Where $\Delta ctx = ctx' - ctx$, $\Delta a = a' - a$, and $\Delta clk = clk' - clk$. + +To make sure the prover sets the value of column `t` correctly, we'll need to impose the following constraints: + +$$ +f_{mem\_nl} \cdot (n_0^2 - n_0) = 0 \text{ | degree} = 7 +$$ + +$$ +f_{mem\_nl} \cdot (1 - n_0) \cdot \Delta ctx = 0 \text{ | degree} = 6 +$$ + +$$ +f_{mem\_nl} \cdot (1 - n_0) \cdot (n_1^2 - n_1) = 0 \text{ | degree} = 9 +$$ + +$$ +f_{mem\_nl} \cdot (1 - n_0) \cdot (1 - n_1) \cdot \Delta a = 0 \text{ | degree} = 8 +$$ + +The above constraints guarantee that when context changes, $n_0 = 1$. When context remains the same but word address changes, $(1 - n_0) \cdot n_1 = 1$. And when neither the context nor the word address change, $(1 - n_0) \cdot (1 - n_1) = 1$. + +We enforce that the `rw`, `ew`, `idx0` and `idx1` contain binary values. + +$$ +f_{mem} \cdot (rw^2 - rw) = 0 \text{ | degree} = 5 +$$ + +$$ +f_{mem} \cdot (ew^2 - ew) = 0 \text{ | degree} = 5 +$$ + +$$ +f_{mem} \cdot (idx0^2 - idx0) = 0 \text{ | degree} = 5 +$$ + +$$ +f_{mem} \cdot (idx1^2 - idx1) = 0 \text{ | degree} = 5 +$$ + +For word access, the element index bits are zero: + +$$ +f_{mem} \cdot ew \cdot idx0 = 0 \text{ | degree} = 5 +$$ + +$$ +f_{mem} \cdot ew \cdot idx1 = 0 \text{ | degree} = 5 +$$ + + +To enforce the values of context ID, word address, and clock cycle grow monotonically as described in the previous section, we define the following constraint. + +$$ +f_{mem\_nl} \cdot \left(n_0 \cdot \Delta ctx + (1 - n_0) \cdot (n_1 \cdot \Delta a + (1 - n_1) \cdot \Delta clk) - (2^{16} \cdot d_1' + d_0') \right) = 0 \text{ | degree} = 8 +$$ + +In addition to this constraint, we also need to make sure that the values in registers $d_0$ and $d_1$ are less than $2^{16}$, and this can be done with [range checks](../range.md). + +Next, we need to ensure that when the context and word address are unchanged and the clock does not advance, both rows are reads. + +$$ +f_{mem\_nl} \cdot f_{scw}' \cdot (1 - \Delta clk \cdot t') \cdot \big((1 - rw) + (1 - rw')\big) = 0 \text{ | degree} = 8 +$$ + + +Next, for all frames where the "current" and "next" rows are in the chiplet, we need to ensure that the value of the `f_scw` column in the "next" row is set to $1$ when the context and word address are the same, and $0$ otherwise. + +$$ +f_{mem\_nl} \cdot (f_{scw}' - (1 - n_0) \cdot (1-n_1)) = 0 \text{ | degree} = 7 +$$ + +Note that this does not constrain the value of `f_scw` in the first row of the chiplet. This is intended, as the first row's constraints do not depend on the previous row (since the previous row is not part of the same chiplet), and therefore do not depend on `f_scw` (see "first row" constraints below). + +Finally, we need to constrain the `v0, v1, v2, v3` columns. We will define a few variables to help in defining the constraints. + +$$ +\begin{align*} +f_0 &= (1 - idx1') \cdot (1 - idx0') \text{ | degree} = 2 \\ +f_1 &= (1 - idx1') \cdot idx0' \text{ | degree} = 2 \\ +f_2 &= idx1' \cdot (1 - idx0') \text{ | degree} = 2 \\ +f_3 &= idx1' \cdot idx0' \text{ | degree} = 2 +\end{align*} +$$ + +The flag $f_i$ is set to $1$ when $v_i'$ is being accessed, and $0$ otherwise. Next, for $0 \leq i < 4$, + +$$ +c_i = rw' + (1 - rw') \cdot (1 - ew') \cdot (1 - f_i) \text{ | degree} = 4 +$$ + +which is set to $1$ when $v_i$ is *not* written to, and $0$ otherwise. + +We're now ready to describe the constraints for the `v0, v1, v2, v3` columns. + +- For the first row of the chiplet (in the "next" position of the frame), for $0 \leq i < 4$, + +$$ +f_{mem\_fr} \cdot c_i \cdot v_i' = 0 \text{ | degree} = 9 +$$ + +That is, if the next row is the first row of the memory chiplet, and $v_i'$ is not written to, then $v_i'$ must be $0$. + +- For all rows of the chiplet except the first, for $0 \leq i < 4$, + +$$ +f_{mem\_nl} \cdot c_i \cdot (f_{scw}' \cdot (v_i' - v_i) + (1 - f_{scw}') \cdot v_i') = 0 \text{ | degree} = 9 +$$ + +That is, if $v_i$ is not written to, then either its value needs to be copied over from the previous row (when $f_{scw}' = 1$), or it must be set to 0 (when $f_{scw}' = 0$). + +#### Chiplets bus constraints {#chiplets-bus-constraints} + +Communication between the memory chiplet and the stack is accomplished via the chiplets bus $b_{chip}$. To respond to memory access requests from the stack, we need to multiply the current value in $b_{chip}$ by the value representing a row in the memory table. + +##### Memory row value {#memory-row-value} + +This value can be computed as follows: + +$$ +\begin{align*} +v_{mem} = \alpha_0 + \alpha_1 \cdot op_{mem} + \alpha_2 \cdot ctx + \alpha_3 \cdot a + \alpha_4 \cdot clk + ew \cdot v_{word} + (1 - ew) \cdot v_{element} \text{ | degree} = 4 +\end{align*} +$$ + +where $a = word\_addr + 2 \cdot idx1 + idx0$ and + +$$ +\begin{align*} +v_{word} &= \sum_{j=0}^3(\alpha_{j + 5} \cdot v_j) \text{ | degree} = 1 +v_{element} &= \alpha_5 \cdot \sum_{i=0}^3 f_i \cdot v_i \text{ | degree} = 3 +\end{align*} +$$ + +and where $op_{mem}$ is the appropriate [operation label](./index.md#operation-labels) of the memory access operation. + +To ensure that values of memory table rows are included into the chiplets bus, we impose the following constraint: + +$$ +b_{chip}' = b_{chip} \cdot v_{mem} \text{ | degree} = 5 +$$ + +On the stack side, for every memory access request, a corresponding value is divided out of the $b_{chip}$ column. Specifics of how this is done are described [here](../stack/io_ops.md#memory-access-operations). diff --git a/versioned_docs/version-0.16/reference/miden-vm/design/decoder/_category_.yml b/versioned_docs/version-0.16/reference/miden-vm/design/decoder/_category_.yml new file mode 100644 index 00000000..3f761fa9 --- /dev/null +++ b/versioned_docs/version-0.16/reference/miden-vm/design/decoder/_category_.yml @@ -0,0 +1,4 @@ +label: "Program decoder" +# Determines where this documentation section appears relative to other sections in the parent folder +position: 2 +collapsed: true diff --git a/versioned_docs/version-0.16/reference/miden-vm/design/decoder/constraints.md b/versioned_docs/version-0.16/reference/miden-vm/design/decoder/constraints.md new file mode 100644 index 00000000..0d1e70b7 --- /dev/null +++ b/versioned_docs/version-0.16/reference/miden-vm/design/decoder/constraints.md @@ -0,0 +1,748 @@ +--- +title: "Miden VM Decoder AIR Constraints" +sidebar_position: 2 +--- + +# Miden VM decoder AIR constraints + +In this section we describe AIR constraints for Miden VM program decoder. These constraints enforce that the execution trace generated by the prover when executing a particular program complies with the rules described in the [previous section](./index.md). + +To refer to decoder execution trace columns, we use the names shown on the diagram below (these are the same names as in the previous section). Additionally, we denote the register containing the value at the top of the stack as $s_0$. + +![air_decoder_columns](../../img/design/decoder/constraints/air_decoder_columns.png) + +We assume that the VM exposes a flag per operation which is set to $1$ when the operation is executed, and to $0$ otherwise. The notation for such flags is $f_{opname}$. For example, when the VM executes a `PUSH` operation, flag $f_{push} = 1$. All flags are mutually exclusive - i.e., when one flag is set to $1$ all other flags are set to $0$. The flags are computed based on values in `op_bits` columns. + +AIR constraints for the decoder involve operations listed in the table below. For each operation we also provide the degree of the corresponding flag and the effect that the operation has on the operand stack (however, in this section we do not cover the constraints needed to enforce the correct transition of the operand stack). + +| Operation | Flag | Degree | Effect on stack | +| --------- | :-----------: |:------:| ------------------------------------------------------------------------------------------------ | +| `JOIN` | $f_{join}$ | 5 | Stack remains unchanged. | +| `SPLIT` | $f_{split}$ | 5 | Top stack element is dropped. | +| `LOOP` | $f_{loop}$ | 5 | Stack remains unchanged. | +| `REPEAT` | $f_{repeat}$ | 4 | Top stack element is dropped. | +| `SPAN` | $f_{span}$ | 5 | Stack remains unchanged. | +| `RESPAN` | $f_{respan}$ | 4 | Stack remains unchanged. | +| `DYN` | $f_{dyn}$ | 5 | Top stack element is dropped. | +| `DYNCALL` | $f_{dyncall}$ | 5 | Top stack element is dropped. | +| `CALL` | $f_{call}$ | 4 | Stack remains unchanged. | +| `SYSCALL` | $f_{syscall}$ | 4 | Stack remains unchanged. | +| `END` | $f_{end}$ | 4 | When exiting a loop block, top stack element is dropped; otherwise, the stack remains unchanged. | +| `HALT` | $f_{halt}$ | 4 | Stack remains unchanged. | +| `PUSH` | $f_{push}$ | 5 | An immediate value is pushed onto the stack. | +| `EMIT` | $f_{emit}$ | 7 | Stack remains unchanged. | + +We also use the [control flow flag](../stack/op_constraints.md#control-flow-flag) $f_{ctrl}$ +exposed by the VM, which is set to $1$ for the control-flow operations +`SPAN`, `JOIN`, `SPLIT`, `LOOP`, `END`, `REPEAT`, `RESPAN`, `HALT`, `DYN`, `DYNCALL`, `CALL`, +and `SYSCALL` (and $0$ otherwise). It has degree $5$. + +As described [previously](./index.md#program-decoding), the general idea of the decoder is that the prover provides the program to the VM by populating some of cells in the trace non-deterministically. Values in these are then used to update virtual tables (represented via multiset checks) such as block hash table, block stack table etc. Transition constraints are used to ensure that the tables are updates correctly, and we also apply boundary constraints to enforce the correct initial and final states of these tables. One of these boundary constraints binds the execution trace to the hash of the program being executed. Thus, if the virtual tables were updated correctly and boundary constraints hold, we can be convinced that the prover executed the claimed program on the VM. + +In the sections below, we describe constraints according to their logical grouping. However, we start out with a set of general constraints which are applicable to multiple parts of the decoder. + +## General constraints + +When `SPLIT` operation is executed, the top of the operand stack must contain a binary value: + +> $$ +> f_{split} \cdot (s_0^2 - s_0) = 0 \text{ | degree} = 7 +> $$ + +When a `DYN` operation is executed, the second half of the hasher registers +($h_4,\dots,h_7$) must be set to $0$ (the first half holds the callee digest): + +> $$ +> f_{dyn} \cdot h_i = 0 \text { for } i \in [4, 8) \text{ | degree} = 6 +> $$ + +When `REPEAT` operation is executed, the value at the top of the operand stack must be $1$: + +> $$ +> f_{repeat} \cdot (1 - s_0) = 0 \text{ | degree} = 5 +> $$ + +Also, when `REPEAT` operation is executed, the value in $h_4$ column (the `is_loop_body` flag), must be set to $1$. This ensures that `REPEAT` operation can be executed only inside a loop: + +> $$ +> f_{repeat} \cdot (1 - h_4) = 0 \text{ | degree} = 5 +> $$ + +When `RESPAN` operation is executed, we need to make sure that the block ID is incremented by $2$: + +> $$ +> f_{respan} \cdot (a' - a - 2) = 0 \text{ | degree} = 5 +> $$ + +When `END` operation is executed and we are exiting a *loop* block (i.e., `is_loop`, value which is stored in $h_5$, is $1$), the value at the top of the operand stack must be $0$: + +> $$ +> f_{end} \cdot h_5 \cdot s_0 = 0 \text{ | degree} = 6 +> $$ + +Also, when `END` operation is executed and the next operation is `REPEAT`, values in $h_0, ..., h_4$ (the hash of the current block and the `is_loop_body` flag) must be copied to the next row: + +> $$ +> f_{end} \cdot f_{repeat}' \cdot (h_i' - h_i) = 0 \text { for } i \in [0, 5) \text{ | degree} = 9 +> $$ + +A `HALT` instruction can be followed only by another `HALT` instruction: + +> $$ +> f_{halt} \cdot (1 - f_{halt}') = 0 \text{ | degree} = 8 +> $$ + +When a `HALT` operation is executed, block address column must be $0$: + +> $$ +> f_{halt} \cdot a = 0 \text{ | degree} = 5 +> $$ + +Values in `op_bits` columns must be binary (i.e., either $1$ or $0$): + +> $$ +> b_i^2 - b_i = 0 \text{ for } i \in [0, 7) \text{ | degree} = 2 +> $$ + +We also use two extra columns ($e_0$, $e_1$) for degree reduction in the operation +flag computation: + +> $$ +> e_0 - b_6 \cdot (1 - b_5) \cdot b_4 = 0 \text{ | degree} = 4 +> $$ + +> $$ +> e_1 - b_6 \cdot b_5 = 0 \text{ | degree} = 3 +> $$ + +Finally, we enforce opcode-prefix constraints needed for flag construction. These eliminate +unused opcode prefixes so that only valid op-code prefixes are allowed: + +> $$ +> b_6 \cdot (1 - b_5) \cdot (1 - b_4) \cdot b_0 = 0 \text{ | degree} = 4 +> $$ + +> $$ +> b_6 \cdot b_5 \cdot b_0 = 0 \text{ | degree} = 3 +> $$ + +> $$ +> b_6 \cdot b_5 \cdot b_1 = 0 \text{ | degree} = 3 +> $$ + +When the value in `in_span` column is set to $1$, control flow operations cannot be executed on the VM, but when `in_span` flag is $0$, only control flow operations can be executed on the VM: + +> $$ +> 1 - sp - f_{ctrl} = 0 \text{ | degree} = 5 +> $$ + +## Block hash computation constraints +As described [previously](./index.md#program-block-hashing), when the VM starts executing a new block, it also initiates computation of the block's hash. There are two separate methodologies for computing block hashes. + +For *join* and *split* blocks, the hash is computed directly from the hashes of the block's children. The prover provides these child hashes non-deterministically by populating registers $h_0,..., h_7$. For *loop* blocks, only the loop body hash is provided in $h_0..h_3$ and the remaining registers $h_4..h_7$ are set to $0$ (padding to a full 8-element rate). For *dyn*, only the second half of the hasher registers ($h_4,\dots,h_7$) are forced to $0$, while the first half holds the callee digest read from memory; thus the input is not all zeros. The hasher is initialized using the hash chiplet, and we use the address of the controller input row as the block's ID. The result of the hash is then available in the paired controller output row at block ID plus $1$, and we read that result when the `END` operation is executed for the block. + +For *basic* blocks, the hash is computed by absorbing a linear sequence of instructions (organized into operation groups and batches) into the hasher and then returning the result. The prover provides operation batches non-deterministically by populating registers $h_0, ..., h_7$. Similarly to other blocks, the hasher is initialized using the hash chiplet at the start of the block, and we use the address of the first controller input row as the ID of the first operation batch in the block. As we absorb additional operation batches into the hasher (by executing `RESPAN`), the next batch starts at the next controller input row, so the batch address is incremented by $2$. We read the result from the controller output row corresponding to the final batch when the `END` operation is executed for the block. + +### Chiplets bus constraints + +The decoder communicates with the hash chiplet via the [chiplets bus](../chiplets/index.md#chiplets-bus). This works by dividing values of the multiset check column $b_{chip}$ by the values of operations providing inputs to or reading outputs from the hash chiplet. A constraint to enforce this would look as $b_{chip}' \cdot u = b_{chip}$, where $u$ is the value which defines the operation. + +In constructing values for decoder bus requests, we use the hasher message format +described in the [hasher chiplet](../chiplets/hasher.md#lookup-buses). +For decoder requests, the node index is always $0$, so the message reduces to: + +$$ +H(label, addr, state) = +\alpha_0 + \alpha_1 \cdot label + \alpha_2 \cdot addr ++ \sum_{i=0}^{11} \alpha_{4+i} \cdot state_i +$$ + +We use the following labels: + +* $L_{init} = \text{LINEAR\_HASH\_LABEL} + 16$ +* $L_{respan} = \text{LINEAR\_HASH\_LABEL} + 32$ +* $L_{end} = \text{RETURN\_HASH\_LABEL} + 32$ + +Let $d = \sum_{i=0}^6(b_i \cdot 2^i)$ denote the opcode value, which is placed in the +capacity domain lane (state index $9$). + +To simplify constraint description, we define the following values: + +$$ +h_{ctrl} = +\alpha_0 + \alpha_1 \cdot L_{init} + \alpha_2 \cdot a' ++ \sum_{i=0}^7(\alpha_{4+i} \cdot h_i) + \alpha_{13} \cdot d +$$ + +$$ +h_{span} = +\alpha_0 + \alpha_1 \cdot L_{init} + \alpha_2 \cdot a' ++ \sum_{i=0}^7(\alpha_{4+i} \cdot h_i) +$$ + +$$ +h_{respan} = +\alpha_0 + \alpha_1 \cdot L_{respan} + \alpha_2 \cdot a' ++ \sum_{i=0}^7(\alpha_{4+i} \cdot h_i) +$$ + +$$ +h_{end} = +\alpha_0 + \alpha_1 \cdot L_{end} + \alpha_2 \cdot (a + 1) ++ \sum_{i=0}^3(\alpha_{4+i} \cdot h_i) +$$ + +$$ +h_{dyn} = +\alpha_0 + \alpha_1 \cdot L_{init} + \alpha_2 \cdot a' ++ \alpha_{13} \cdot d +$$ + +$$ +f_{ctrli} = f_{join} + f_{split} + f_{loop} \text{ | degree} = 5 +$$ + +In the above, $f_{ctrli}$ is set to $1$ when a control flow operation that signifies the initialization of a control block is being executed on the VM (only those control blocks that don't do any concurrent requests to the chiplets bus). Otherwise, it is set to $0$. An exception is made for the `DYN`, `DYNCALL`, `CALL` and `SYSCALL` operations, since although they initialize a control block, they also run another concurrent bus request, and so are handled separately. + +Using the above variables, we define operation values as described below. + +When a control block initializer operation (`JOIN`, `SPLIT`, `LOOP`) is executed, +a new hasher is initialized and the contents of $h_0, ..., h_7$ are absorbed into +the hasher, with the opcode value $d$ placed in the capacity domain lane. + +$$ +u_{ctrli} = f_{ctrli} \cdot h_{ctrl} \text{ | degree} = 6 +$$ + +As mentioned previously, the value sent by the `SYSCALL` operation is defined separately, since in addition to communicating with the hash chiplet it must also send a kernel procedure access request to the kernel ROM chiplet. This value of this kernel procedure request is described by $k_{proc}$. + +$$ +k_{proc} = \alpha_0 + \alpha_1 \cdot op_{krom} + \sum_{i=0}^3 (\alpha_{i + 2} \cdot h_i) +$$ + +In the above, $op_{krom}$ is the unique [operation label](../chiplets/index.md#operation-labels) of the kernel procedure call operation. The values $h_0, h_1, h_2, h_3$ contain the root hash of the procedure being called, which is the procedure that must be requested from the kernel ROM chiplet. + +$$ +u_{syscall} = f_{syscall} \cdot h_{ctrl} \cdot k_{proc} \text{ | degree} = 6 +$$ + +The above value sends both the hash initialization request and the kernel procedure access request to the chiplets bus when the `SYSCALL` operation is executed. + +Similar to `SYSCALL`, `CALL` is handled separately, since in addition to communicating with the hash chiplet, it must also initialize the frame memory pointer (stored in memory at constant address `fmpaddr` with constant value `fmpinit`): + +$$ +m_{fmpwrite} = \alpha_0 + \alpha_1 \cdot m_{writeele} + \alpha_2 \cdot ctx' + \alpha_3 \cdot fmpaddr + \alpha_4 \cdot clk + \alpha_5 \cdot fmpinit +$$ + +In the above, $m_{fmpwrite}$ represents a "write element" memory request equivalent to `mem[fmpaddr] = fmpinit` (in pseudo-code) in the new memory context (*i.e.* the memory context of the callee). Currently $fmpaddr = 2^{32} - 1$, and $fmpinit = 2^{31}$. + +$$ +u_{call} = f_{call} \cdot h_{ctrl} \cdot m_{fmpwrite} \text{ | degree} = 6 +$$ + +Similar to `SYSCALL` and `CALL`, `DYN` and `DYNCALL` are handled separately, since in addition to communicating with the hash chiplet they must also issue a memory read operation for the hash of the procedure being called. + +$$ +h_{dynordyncall} = h_{dyn} +$$ + +$$ +m_{dynordyncall} = \alpha_0 + \alpha_1 \cdot m_{readword} + \alpha_2 \cdot ctx + \alpha_3 \cdot s_0 + \alpha_4 \cdot clk + + \sum_{i=0}^3(\alpha_{i + 5} \cdot h_i) +$$ + +$$ +u_{dyn} = f_{dyn} \cdot h_{dynordyncall} \cdot m_{dynordyncall} \text{ | degree} = 7 +$$ +$$ +u_{dyncall} = f_{dyncall} \cdot h_{dynordyncall} \cdot m_{dynordyncall} \cdot m_{fmpwrite} \text{ | degree} = 8 +$$ + +In the above, $h_{dynordyncall}$ is the control-block request with a zeroed hasher +state, and $m_{dynordyncall}$ represents a memory **word** read request from address +$s_0$, where the result is placed in the first half of the decoder hasher trace. +Note that similar to `CALL`, `DYNCALL` also creates a new memory context, and hence +must also initialize the `fmp`. + +When `SPAN` operation is executed, a new hasher is initialized and contents of $h_0, ..., h_7$ are absorbed into the hasher. The chiplets-bus message uses only the rate lanes; capacity lanes (state indices $8..11$) are zeroed for this request: + +$$ +u_{span} = f_{span} \cdot h_{span} \text{ | degree} = 6 +$$ + +When `RESPAN` operation is executed, contents of $h_0, ..., h_7$ (which contain the new operation batch) are absorbed into the hasher: + +$$ +u_{respan} = f_{respan} \cdot h_{respan} \text{ | degree} = 5 +$$ + +When `END` operation is executed, the hash result is copied into registers $h_0, .., h_3$: + +$$ +u_{end} = f_{end} \cdot h_{end} \text{ | degree} = 5 +$$ + +Using the above definitions, we can describe the constraint for computing block hashes as follows: + +> $$ +> b_{chip}' \cdot (u_{ctrli} + u_{call} + u_{syscall} + u_{dyn} + u_{dyncall} + u_{span} + u_{respan} + u_{end} + \\ +> 1 - (f_{ctrli} + f_{call} + f_{syscall} + f_{dyn} + f_{dyncall} + f_{span} + f_{respan} + f_{end})) = b_{chip} +> $$ + +We need to add $1$ and subtract the sum of the relevant operation flags to ensure that when none of the flags is set to $1$, the above constraint reduces to $b_{chip}' = b_{chip}$. + +The degree of this constraint is $9$. + +## Block stack table constraints +As described [previously](./index.md#block-stack-table), block stack table keeps track of program blocks currently executing on the VM. Thus, whenever the VM starts executing a new block, an entry for this block is added to the block stack table. And when execution of a block completes, it is removed from the block stack table. + +Adding and removing entries to/from the block stack table is accomplished as follows: +* To add an entry, we multiply the value in column $p_1$ by a value representing a tuple `(blk, prnt, is_loop, ctx_next, b0_next, b1_next, fn_hash_next)` +. A constraint to enforce this would look as $p_1' = p_1 \cdot v$, where $v$ is the value representing the row to be added. +* To remove an entry, we divide the value in column $p_1$ by a value representing a tuple `(blk, prnt, is_loop, ctx_next, b0_next, b1_next, fn_hash_next)`. A constraint to enforce this would look as $p_1' \cdot u = p_1$, where $u$ is the value representing the row to be removed. + +> Recall that the columns `ctx_next, b0_next, b1_next, fn_hash_next` are only set on `CALL`, `SYSCALL`, and their corresponding `END` block. Therefore, for simplicity, we will ignore them when documenting all other block types (such that their values are set to `0`). + +Before describing the constraints for the block stack table, we first describe how we compute the values to be added and removed from the table for each operation. In the below, for block start operations (`JOIN`, `SPLIT`, `LOOP`, `SPAN`) $a$ refers to the ID of the parent block, and $a'$ refers to the ID of the starting block. For `END` operation, the situation is reversed: $a$ is the ID of the ending block, and $a'$ is the ID of the parent block. For `RESPAN` operation, $a$ refers to the ID of the current operation batch, $a'$ refers to the ID of the next batch, and the parent ID for both batches is set by the prover non-deterministically in register $h_1$. + +When `JOIN` operation is executed, row $(a', a, 0)$ is added to the block stack table: + +$$ +v_{join} = f_{join} \cdot (\alpha_0 + \alpha_1 \cdot a' + \alpha_2 \cdot a + \alpha_3 \cdot 0) \text{ | degree} = 6 +$$ + +When `SPLIT` operation is executed, row $(a', a, 0)$ is added to the block stack table: + +$$ +v_{split} = f_{split} \cdot (\alpha_0 + \alpha_1 \cdot a' + \alpha_2 \cdot a + \alpha_3 \cdot 0) \text{ | degree} = 6 +$$ + +When `LOOP` operation is executed, row $(a', a, 1)$ is added to the block stack table. Because `LOOP` has do-while semantics, the body is always entered, so the `is_loop` slot is unconditionally $1$: + +$$ +v_{loop} = f_{loop} \cdot (\alpha_0 + \alpha_1 \cdot a' + \alpha_2 \cdot a + \alpha_3) \text{ | degree} = 5 +$$ + +When `SPAN` operation is executed, row $(a', a, 0)$ is added to the block stack table: + +$$ +v_{span} = f_{span} \cdot (\alpha_0 + \alpha_1 \cdot a' + \alpha_2 \cdot a + \alpha_3 \cdot 0) \text{ | degree} = 6 +$$ + +When `RESPAN` operation is executed, row $(a, h_1', 0)$ is removed from the block stack table, and row $(a', h_1', 0)$ is added to the table. The prover sets the value of register $h_1$ at the next row to the ID of the parent block: + +$$ +u_{respan} = f_{respan} \cdot (\alpha_0 + \alpha_1 \cdot a + \alpha_2 \cdot h_1' + \alpha_3 \cdot 0) \text{ | degree} = 5 +v_{respan} = f_{respan} \cdot (\alpha_0 + \alpha_1 \cdot a' + \alpha_2 \cdot h_1' + \alpha_3 \cdot 0) \text{ | degree} = 5 +$$ + +When a `DYN` operation is executed, row $(a', a, 0)$ is added to the block stack table: + +$$ +v_{dyn} = f_{dyn} \cdot (\alpha_0 + \alpha_1 \cdot a' + \alpha_2 \cdot a + \alpha_3 \cdot 0) \text{ | degree} = 6 +$$ + +When a `DYNCALL` operation is executed, row $(a', a, 0, ctx, h_4, h_5, \mathrm{fnhash}[0..3])$ is added to the block stack table (here $h_4, h_5$ hold the post-shift stack depth and overflow address): + +$$ +\begin{align*} +v_{dyncall} &= f_{dyncall} \cdot (\alpha_0 + \alpha_1 \cdot a' + \alpha_2 \cdot a + \alpha_3 \cdot 0 + + \alpha_4 \cdot ctx + \alpha_5 \cdot h_4 + \alpha_6 \cdot h_5 \\ +&\quad + \alpha_7 \cdot \mathrm{fnhash}_0 + \alpha_8 \cdot \mathrm{fnhash}_1 + + \alpha_9 \cdot \mathrm{fnhash}_2 + \alpha_{10} \cdot \mathrm{fnhash}_3) \text{ | degree} = 6 +\end{align*} +$$ + +When a `CALL` or `SYSCALL` operation is executed, row $(a', a, 0, ctx, b_0, b_1, \mathrm{fnhash}[0..3])$ is added to the block stack table: + +$$ +\begin{align*} +v_{callorsyscall} &= (f_{call} + f_{syscall}) \cdot (\alpha_0 + \alpha_1 \cdot a' + \alpha_2 \cdot a + + \alpha_3 \cdot 0 + \alpha_4 \cdot ctx + \alpha_5 \cdot b_0 + \alpha_6 \cdot b_1 \\ +&\quad + \alpha_7 \cdot \mathrm{fnhash}_0 + \alpha_8 \cdot \mathrm{fnhash}_1 + + \alpha_9 \cdot \mathrm{fnhash}_2 + \alpha_{10} \cdot \mathrm{fnhash}_3) \text{ | degree} = 5 +\end{align*} +$$ + +When `END` operation is executed, how we construct the row will depend on whether the `IS_CALL` or `IS_SYSCALL` values are set (stored in registers $h_6$ and $h_7$ respectively). If they are not set, then row $(a, a', h_5)$ is removed from the block span table (where $h_5$ contains the `is_loop` flag); otherwise, row $(a ,a', 0, ctx', b_0', b_1', \mathrm{fnhash}'[0..3])$. + +$$ +\begin{align*} +u_{endnocall} &= \alpha_0 + \alpha_1 \cdot a + \alpha_2 \cdot a' + \alpha_3 \cdot h_5 \\ +u_{endcall} &= u_{endnocall} + \alpha_4 \cdot ctx' + \alpha_5 \cdot b_0' + \alpha_6 \cdot b_1' \\ +&\quad + \alpha_7 \cdot \mathrm{fnhash}_0' + \alpha_8 \cdot \mathrm{fnhash}_1' + + \alpha_9 \cdot \mathrm{fnhash}_2' + \alpha_{10} \cdot \mathrm{fnhash}_3' \\ +u_{end} &= f_{end} \cdot ((1 - h_6 - h_7) \cdot u_{endnocall} + (h_6 + h_7) \cdot u_{endcall} ) \text{ | degree} = 6 +\end{align*} +$$ + +Using the above definitions, we can describe the constraint for updating the block stack table as follows: + +> $$ +> p_1' \cdot (u_{end} + u_{respan} + 1 - (f_{end} + f_{respan})) = p_1 \cdot +> (v_{join} + v_{split} + v_{loop} + v_{span} + v_{respan} + v_{dyn} + v_{dyncall} + v_{callorsyscall} + 1 - +> (f_{join} + f_{split} + f_{loop} + f_{span} + f_{respan} + f_{dyn} + f_{dyncall} + f_{call} + f_{syscall})) +> $$ + +We need to add $1$ and subtract the sum of the relevant operation flags from each side to ensure that when none of the flags is set to $1$, the above constraint reduces to $p_1' = p_1$. + +The degree of this constraint is $9$. + +In addition to the above transition constraint, we also need to impose boundary constraints against the $p_1$ column to make sure the first and the last values in the column are set to $1$. This enforces that the block stack table starts and ends in an empty state. + +## Block hash table constraints +As described [previously](./index.md#block-hash-table), when the VM starts executing a new program block, it adds hashes of the block's children to the block hash table. And when the VM finishes executing a block, it removes the block's hash from the block hash table. This means that the block hash table gets updated when we execute the `JOIN`, `SPLIT`, `LOOP`, `REPEAT`, `DYN`, and `END` operations (executing `SPAN` operation does not affect the block hash table because a *basic* block has no children). + +Adding and removing entries to/from the block hash table is accomplished as follows: +* To add an entry, we multiply the value in column $p_2$ by a value representing a tuple `(prnt_id, block_hash, is_first_child, is_loop_body)`. A constraint to enforce this would look as $p_2' = p_2 \cdot v$, where $v$ is the value representing the row to be added. +* To remove an entry, we divide the value in column $p_2$ by a value representing a tuple `(prnt_id, block_hash, is_first_child, is_loop_body)`. A constraint to enforce this would look as $p_2' \cdot u = p_2$, where $u$ is the value representing the row to be removed. + +To simplify constraint descriptions, we define a generic message for a block hash entry: + +$$ +m(parent, h_0..h_3, is\_first, is\_loop\_body) = +\alpha_0 + \alpha_1 \cdot parent + \sum_{i=0}^3(\alpha_{i+2} \cdot h_i) ++ \alpha_6 \cdot is\_first + \alpha_7 \cdot is\_loop\_body +$$ + +Using this, we define the left and right child messages for a `JOIN` as: + +$$ +ch_1 = m(a', h_0..h_3, 1, 0) \qquad +ch_2 = m(a', h_4..h_7, 0, 0) +$$ + +Graphically, this looks like so: + +![air_decoder_left_right_child](../../img/design/decoder/constraints/air_decoder_left_right_child.png) + +Using the above variables, we define row values to be added to and removed from the block hash table as follows. + +When `JOIN` operation is executed, hashes of both child nodes are added to the block hash table: + +$$ +v_{join} = f_{join} \cdot ch_1 \cdot ch_2 \text{ | degree} = 7 +$$ + +When `SPLIT` operation is executed and the top of the stack is $1$, hash of the *true* branch is added to the block hash table; when the top of the stack is $0$, hash of the *false* branch is added: + +$$ +v_{split} = f_{split} \cdot (s_0 \cdot ch_1 + (1 - s_0) \cdot ch_2) \text{ | degree} = 7 +$$ + +When `LOOP` operation is executed, the hash of the loop body is unconditionally added to the +block hash table (with `is_loop_body = 1`), since the body is always entered for the first +iteration: + +$$ +v_{loop} = f_{loop} \cdot m(a', h_0..h_3, 0, 1) \text{ | degree} = 6 +$$ + +When `REPEAT` operation is executed, hash of the loop body is added to the block hash table: + +$$v_{repeat} = f_{repeat} \cdot m(a', h_0..h_3, 0, 1) \text{ | } \text{degree} = 5$$ + +When `DYN`, `DYNCALL`, `CALL` or `SYSCALL` operation is executed, the hash of the child is +added to the block hash table. In all cases, this child is found in the first half +of the decoder hasher state. + +$$ +v_{allcalls} = (f_{dyn} + f_{dyncall} + f_{call} + f_{syscall}) \cdot m(a', h_0..h_3, 0, 0) \text{ | degree} = 6 +$$ + +When `END` operation is executed, the hash of the completed block is removed from the block +hash table. We differentiate between the first and second child of a `JOIN` by looking at +the next opcode: if the next operation is not `END`, `REPEAT`, `RESPAN`, or `HALT`, then this +block is the first child and `is_first_child = 1`. `RESPAN` is included defensively — no single +constraint forbids `END → RESPAN`, so excluding it would let an adversarial trace inject a +false-positive `is_first_child = 1`. The `is_loop_body` flag is read from $h_4$. + +$$ +u_{end} = f_{end} \cdot m(a', h_0..h_3, 1 - (f_{end}' + f_{repeat}' + f_{respan}' + f_{halt}'), h_4) \text{ | } \text{degree} = 8 +$$ + +Using the above definitions, we can describe the constraint for updating the block hash table as follows: + +> $$ +> p_2' \cdot (u_{end} + 1 - f_{end}) = +> p_2 \cdot (v_{join} + v_{split} + v_{loop} + v_{repeat} + v_{allcalls} + 1 - (f_{join} + f_{split} + f_{loop} + f_{repeat} + f_{dyn} + f_{dyncall} + f_{call} + f_{syscall})) +> $$ + +We need to add $1$ and subtract the sum of the relevant operation flags from each side to ensure that when none of the flags is set to $1$, the above constraint reduces to $p_2' = p_2$. + +The degree of this constraint is $9$. + +In addition to the above transition constraint, the last value in the column is $1$ +(i.e., the block hash table is empty). The initial program-hash boundary constraint +is planned but not enforced yet. + +## Basic block +Basic block constraints ensure proper decoding of basic blocks. In addition to the block stack table constraints and block hash table constraints described previously, decoding of basic blocks requires constraints described below. + +### In-span column constraints +The `in_span` column (denoted as $sp$) marks rows which execute non-control flow +operations. This is enforced by the control-flow constraint +$1 - sp - f_{ctrl} = 0$, so $sp = 1$ for non-control flow operations and $sp = 0$ +otherwise. Semantically, this means $sp$ is 1 throughout basic blocks and 0 on +control-flow rows. We do not separately constrain $sp' = sp$; the control-flow +constraint pins $sp$ on every row, and the SPAN/RESPAN constraints below ensure +the next row enters a basic block. +Here $f_{ctrl}$ includes `SPAN`, `JOIN`, `SPLIT`, `LOOP`, `END`, `REPEAT`, `RESPAN`, `HALT`, +`DYN`, `DYNCALL`, `CALL`, and `SYSCALL`. +The op-group table and group_count constraints enforce that non-control rows can only appear +inside spans, so $sp$ cannot switch to $1$ without a preceding `SPAN`/`RESPAN`. + +We require that the VM starts outside a basic block. Since $sp = 1 - f_{ctrl}$ and $f_{ctrl}$ +is binary, $sp$ is also binary. + +> $$ +> sp = 0 \text{ on the first row} +> $$ + +When executing `SPAN` or `RESPAN`, the next value of $sp$ must be set to $1$: + +> $$ +> f_{span} \cdot (1 - sp') = 0 \text{ | degree} = 6 +> $$ + +> $$ +> f_{respan} \cdot (1 - sp') = 0 \text{ | degree} = 5 +> $$ + +Since these flags are mutually exclusive, we can also merge them into one constraint: + +> $$ +> (f_{span} + f_{respan}) \cdot (1 - sp') = 0 \text{ | degree} = 6 +> $$ + +### Block address constraints +When we are inside a *basic* block, values in block address columns (denoted as $a$) must remain the same. This can be enforced with the following constraint: + +> $$ +> sp \cdot (a' - a) = 0 \text{ | degree} = 2 +> $$ + +Notice that this constraint does not apply when we execute any of the control flow operations. For such operations, the prover sets the value of the $a$ column non-deterministically, except for the `RESPAN` operation. For the `RESPAN` operation the value in the $a$ column is incremented by $2$, which is enforced by a constraint described previously. + +Notice also that this constraint implies that when the next operation is the `END` operation, the value in the $a$ column must also be copied over to the next row. This is exactly the behavior we want to enforce so that when the `END` operation is executed, the block address is set to the address of the current span batch. + +### Group count constraints +The `group_count` column (denoted as $gc$) is used to keep track of the number of operation groups which remains to be executed in a basic block. + +In the beginning of a basic block (i.e., when `SPAN` operation is executed), the prover sets the value of $gc$ non-deterministically. This value is subsequently decremented according to the rules described below. By the time we exit the basic block (i.e., when `END` operation is executed), the value in $gc$ must be $0$. + +The rules for decrementing values in the $gc$ column are as follows: +* The count cannot be decremented by more than $1$ in a single row. +* When an operation group is fully executed (which happens when $h_0 = 0$ inside a basic block), the count is decremented by $1$. +* When `SPAN`, `RESPAN`, or `PUSH` operations are executed, the count is decremented by $1$. + +Note that these rules imply that the `PUSH` operation (or any operation with an immediate value) cannot be the last operation in an operation group (otherwise the count would have to be decremented by $2$). + +To simplify the description of the constraints, we will define the following variable: + +$$ +\Delta gc = gc - gc' +$$ + +Using this variable, we can describe the constraints against the $gc$ column as follows: + +Inside a *basic* block, group count can either stay the same or decrease by one: + +> $$ +> sp \cdot \Delta gc \cdot (\Delta gc - 1) = 0 \text{ | degree} = 3 +> $$ + +When group count is decremented inside a *basic* block, either $h_0$ must be $0$ (we consumed all operations in a group) or we must be executing an operation with an immediate value: + +> $$ +> sp \cdot \Delta gc \cdot (1 - f_{imm})\cdot h_0 = 0 \text{ | degree} = 8 +> $$ + +Notice that the above constraint does not preclude $f_{imm} = 1$ and $h_0 = 0$ from being true at the same time. If this happens, op group decoding constraints (described [here](#op-group-decoding-constraints)) will force that the operation following the operation with an immediate value is a `NOOP`. + +When executing a `SPAN`, a `RESPAN`, or an operation with an immediate value, group count must be decremented by $1$: + +> $$ +> (f_{span} + f_{respan} + f_{imm}) \cdot (\Delta gc - 1) = 0 \text{ | degree} = 6 +> $$ + +If the next operation is either an `END` or a `RESPAN`, group count must remain the same: + +> $$ +> \Delta gc \cdot (f_{end}' + f_{respan}') = 0 \text{ | degree} = 5 +> $$ + +When an `END` operation is executed, group count must be $0$: + +> $$ +> f_{end} \cdot gc = 0 \text{ | degree} = 5 +> $$ + +### Op group decoding constraints +Inside a *basic* block, register $h_0$ is used to keep track of operations to be executed in the current operation group. The value of this register is set by the prover non-deterministically at the time when the prover executes a `SPAN` or a `RESPAN` operation, or when processing of a new operation group within a batch starts. The picture below illustrates this. + +![air_decoder_op_group_constraint](../../img/design/decoder/constraints/air_decoder_op_group_constraint.png) + +In the above: +* The prover sets the value of $h_0$ non-deterministically at row $0$. The value is set to an operation group containing operations `op0` through `op8`. +* As we start executing the group, at every row we "remove" the least significant operation from the group. This can be done by subtracting opcode of the operation from the group, and then dividing the result by $2^7$. +* By row $9$ the group is fully executed. This decrements the group count and set `op_index` to $0$ (constraints against `op_index` column are described in the next section). +* At row $10$ we start executing the next group with operations `op9` through `op11`. In this case, the prover populates $h_0$ with the group having its first operation (`op9`) already removed, and sets the `op_bits` registers to the value encoding `op9`. +* By row $12$ this group is also fully executed. + +To simplify the description of the constraints, we define the following variables: + +$$ +op = \sum_{i=0}^6 (b_i \cdot 2^i) +f_{sgc} = sp \cdot sp' \cdot (1 - \Delta gc) +$$ + +$op$ is just an opcode value implied by the values in `op_bits` registers. $f_{sgc}$ is a flag which is set to $1$ when the group count within a *basic* block does not change. We multiply it by $sp'$ to make sure the flag is $0$ when we are about to end decoding of an operation batch. Note that $f_{sgc}$ flag is mutually exclusive with $f_{span}$, $f_{respan}$, and $f_{imm}$ flags as these three operations decrement the group count. + +Using these variables, we can describe operation group decoding constraints as follows: + +When a `SPAN`, a `RESPAN`, or an operation with an immediate value is executed or when the group count does not change, the value in $h_0$ should be decremented by the value of the opcode in the next row. + +> $$ +> (f_{span} + f_{respan} + f_{imm} + f_{sgc}) \cdot (h_0 - h_0' \cdot 2^7 - op') = 0 \text{ | degree} = 6 +> $$ + +Notice that when the group count does change, and we are not executing $f_{span}$, $f_{respan}$, or $f_{imm}$ operations, no constraints are placed against $h_0$, and thus, the prover can populate this register non-deterministically. + +When we are in a *basic* block and the next operation is `END` or `RESPAN`, the current value in $h_0$ column must be $0$. + +> $$ +> sp \cdot (f_{end}' + f_{respan}') \cdot h_0 = 0 \text{ | degree} = 6 +> $$ + +### Op index constraints +The `op_index` column (denoted as $ox$) tracks index of an operation within its operation group. It is used to ensure that the number of operations executed per group never exceeds $9$. The index is zero-based, and thus, the possible set of values for $ox$ is between $0$ and $8$ (both inclusive). + +To simplify the description of the constraints, we will define the following variables: + +$$ +ng = \Delta gc - f_{imm} +\Delta ox = ox' - ox +$$ + +The value of $ng$ is set to $1$ when we are about to start executing a new operation group (i.e., group count is decremented but we did not execute an operation with an immediate value). Using these variables, we can describe the constraints against the $ox$ column as follows. + +When executing `SPAN` or `RESPAN` operations the next value of `op_index` must be set to $0$: + +> $$ +> (f_{span} + f_{respan}) \cdot ox' = 0 \text{ | degree} = 6 +> $$ + +When starting a new operation group inside a *basic* block, the next value of `op_index` must be set to $0$. Note that we multiply by $sp$ to exclude the cases when the group count is decremented because of `SPAN` or `RESPAN` operations: + +> $$ +> sp \cdot ng \cdot ox' = 0 \text{ | degree} = 6 +> $$ + +When inside a *basic* block but not starting a new operation group, `op_index` must be incremented by $1$. Note that we multiply by $sp'$ to exclude the cases when we are about to exit processing of an operation batch (i.e., the next operation is either `END` or `RESPAN`): + +> $$ +> sp \cdot sp' \cdot (1 - ng) \cdot (\Delta ox - 1) = 0 \text{ | degree} = 7 +> $$ + +Values of `op_index` must be in the range $[0, 8]$. + +> $$ +> \prod_{i=0}^{8}(ox - i) = 0 \text{ | degree} = 9 +> $$ + +### Op batch flags constraints +Operation batch flag columns (denoted $bc_0$, $bc_1$, and $bc_2$) are used to specify how many operation groups are present in an operation batch. This is relevant for the last batch in a basic block (or the first batch if there is only one batch in a block) as all other batches should be completely full (i.e., contain 8 operation groups). + +These columns are used to define the following 4 flags: + +* $f_{g8} = bc_0$: there are 8 operation groups in the batch. +* $f_{g4} = (1 - bc_0) \cdot bc_1 \cdot (1 - bc_2)$: there are 4 operation groups in the batch. +* $f_{g2} = (1 - bc_0) \cdot (1 - bc_1) \cdot bc_2$: there are 2 operation groups in the batch. +* $f_{g1} = (1 - bc_0) \cdot bc_1 \cdot bc_2$: there is only 1 operation groups in the batch. + +Notice that the degree of $f_{g8}$ is $1$, while the degree of the remaining flags is $3$. + +These flags can be set to $1$ only when we are executing `SPAN` or `RESPAN` operations as this is when the VM starts processing new operation batches. Also, for a given flag we need to ensure that only the specified number of operations groups are present in a batch. This can be done with the following constraints. + +All batch flags must be binary: + +> $$ +> bc_i^2 - bc_i = 0 \text{ for } i \in [0, 3) \text{ | degree} = 2 +> $$ + +When `SPAN` or `RESPAN` operations is executed, one of the batch flags must be set to $1$. + +> $$ +> (f_{span} + f_{respan}) - (f_{g1} + f_{g2} + f_{g4} + f_{g8}) = 0 \text{ | degree} = 5 +> $$ + +When neither `SPAN` nor `RESPAN` is executed, all batch flags must be set to $0$. + +$$ +(1 - (f_{span} + f_{respan})) \cdot (bc_0 + bc_1 + bc_2) = 0 \text{ | degree} = 6 +$$ + +When we have at most 4 groups in a batch, registers $h_4, ..., h_7$ should be set to $0$'s. + +> $$ +> (f_{g1} + f_{g2} + f_{g4}) \cdot h_i = 0 \text{ for } i \in [4, 8) \text{ | degree} = 4 +> $$ + +When we have at most 2 groups in a batch, registers $h_2$ and $h_3$ should also be set to $0$'s. + +> $$ +> (f_{g1} + f_{g2}) \cdot h_i = 0 \text{ for } i \in 2, 3 \text{ | degree} = 4 +> $$ + +When we have at most 1 groups in a batch, register $h_1$ should also be set to $0$. + +> $$ +> f_{g1} \cdot h_1 = 0 \text{ | degree} = 4 +> $$ + +### Op group table constraints +Op group table is used to ensure that all operation groups in a given batch are consumed before a new batch is started (i.e., via a `RESPAN` operation) or the execution of a *basic* block is complete (i.e., via an `END` operation). The op group table is updated according to the following rules: + +* When a new operation batch is started, we add groups from this batch to the table. To add a group to the table, we multiply the value in column $p_3$ by a value representing a tuple `(batch_id, group_pos, group)`. A constraint to enforce this would look as $p_3' = p_3 \cdot v$, where $v$ is the value representing the row to be added. Depending on the batch, we may need to add multiple groups to the table (i.e., $p_3' = p_3 \cdot v_1 \cdot v_2 \cdot v_3 ...$). Flags $f_{g1}$, $f_{g2}$, $f_{g4}$, and $f_{g8}$ are used to define how many groups to add. +* When a new operation group starts executing or when an immediate value is consumed, we remove the corresponding group from the table. To do this, we divide the value in column $p_3$ by a value representing a tuple `(batch_id, group_pos, group)`. A constraint to enforce this would look as $p_3' \cdot u = p_3$, where $u$ is the value representing the row to be removed. + +To simplify constraint descriptions, we first define variables representing the rows to be added to and removed from the op group table. + +When a `SPAN` or a `RESPAN` operation is executed, we compute the values of the rows to be added to the op group table as follows: + +$$ +v_i = \alpha_0 + \alpha_1 \cdot a' + \alpha_2 \cdot (gc - i) + \alpha_3 \cdot h_{i} \text{ | degree} = 1 +$$ + +Where $i \in [1, 8)$. Thus, $v_1$ defines row value for group in $h_1$, $v_2$ defines row value for group $h_2$ etc. Note that batch address column comes from the next row of the block address column ($a'$). + +We compute the value of the row to be removed from the op group table as follows: + +$$ +u = \alpha_0 + \alpha_1 \cdot a + \alpha_2 \cdot gc + \alpha_3 \cdot ((h_0' \cdot 2^7 + op') \cdot (1 - f_{imm}) + s_0' \cdot f_{push}) \text{ | degree} = 7 +$$ + +In the above, the value of the group is computed as $(h_0' \cdot 2^7 + op') \cdot (1 - f_{imm}) + s_0' \cdot f_{push}$. This basically says that when we execute a `PUSH` operation we need to remove the immediate value from the table. This value is at the top of the stack (column $s_0$) in the next row. However, when we are not executing a `PUSH` operation, the value to be removed is an op group value which is a combination of values in $h_0$ and `op_bits` columns (also in the next row). Note also that value for batch address comes from the current value in the block address column ($a$), and the group position comes from the current value of the group count column ($gc$). + +We also define a flag which is set to $1$ when a group needs to be removed from the op group table. + +$$ +f_{dg} = sp \cdot \Delta gc \text{ | degree} = 2 +$$ + +The above says that we remove groups from the op group table whenever group count is decremented. We multiply by $sp$ to exclude the cases when the group count is decremented due to `SPAN` or `RESPAN` operations. + +Using the above variables together with flags $f_{g1}$, $f_{g2}$, $f_{g4}$, $f_{g8}$ defined in the previous section, we describe the constraint for updating op group table as follows: + +> $$ +> p_3' \cdot (f_{dg} \cdot u + 1 - f_{dg}) = p_3 \cdot (f_{g1} + f_{g2} \cdot v_1 + f_{g4} \cdot \prod_{i=1}^3 v_i + f_{g8} \cdot (\prod_{i=1}^7 v_i) + 1 - (f_{span} + f_{respan})) +> $$ + +The above constraint specifies that: +* When `SPAN` or `RESPAN` operations are executed, we add between $0$ and $7$ groups to the op group table; else, leave $p3$ untouched. +* When group count is decremented inside a *basic* block, we remove a group from the op group table; else, leave $p3'$ untouched. + +The degree of this constraint is $9$. + +In addition to the above transition constraint, we also need to impose boundary constraints against the $p_3$ column to make sure the first and the last value in the column is set to $1$. This enforces that the op group table table starts and ends in an empty state. diff --git a/versioned_docs/version-0.16/reference/miden-vm/design/decoder/index.md b/versioned_docs/version-0.16/reference/miden-vm/design/decoder/index.md new file mode 100644 index 00000000..07567b9b --- /dev/null +++ b/versioned_docs/version-0.16/reference/miden-vm/design/decoder/index.md @@ -0,0 +1,726 @@ +--- +title: "Miden VM Program Decoder" +sidebar_position: 1 +--- + +# Miden VM Program decoder + +Miden VM program decoder is responsible for ensuring that a program with a given [MAST root](../programs.md) is executed by the VM. As the VM executes a program, the decoder does the following: + +1. Decodes a sequence of field elements supplied by the prover into individual operation codes (or *opcodes* for short). +2. Organizes the sequence of field elements into code blocks, and computes the hash of the program according to the methodology described [here](../programs.md#program-hash-computation). + +At the end of program execution, the decoder outputs the computed program hash. This hash binds the sequence of opcodes executed by the VM to a program the prover claims to have executed. The verifier uses this hash during the STARK proof verification process to verify that the proof attests to a correct execution of a specific program (i.e., the prover didn't claim to execute program $A$ while in fact executing a different program $B$). + +The sections below describe how Miden VM decoder works. Throughout these sections we make the following assumptions: + +1. An opcode requires $7$ bits to represent. +2. An immediate value requires one full field element to represent. +3. A `NOOP` operation has a numeric value of $0$, and thus, can be encoded as seven zeros. Executing a `NOOP` operation does not change the state of the VM, but it does advance operation counter, and may affect program hash. + +## Program execution + +Miden VM programs consist of a set of code blocks organized into a binary tree. The leaves of the tree contain linear sequences of instructions, and control flow is defined by the internal nodes of the tree. + +Managing control flow in the VM is accomplished by executing control flow operations listed in the table below. Each of these operations requires exactly one VM cycle to execute. + +| Operation | Description | +| --------- | ---------------------------------------------------------------------------- | +| `JOIN` | Initiates processing of a new [Join block](../programs.md#join-block). | +| `SPLIT` | Initiates processing of a new [Split block](../programs.md#split-block). | +| `LOOP` | Initiates processing of a new [Loop block](../programs.md#loop-block). | +| `REPEAT` | Initiates a new iteration of an executing loop. | +| `SPAN` | Initiates processing of a new [Basic block](../programs.md#basic-block). (historically called "span block") | +| `RESPAN` | Initiates processing of a new operation batch within a basic block. (historically called "span block") | +| `DYN` | Initiates processing of a new [Dyn block](../programs.md#dyn-block). | +| `DYNCALL` | Initiates processing of a new [Dyncall block](../programs.md#dyncall-block). | +| `CALL` | Initiates processing of a new [Call block](../programs.md#call-block). | +| `SYSCALL` | Initiates processing of a new [Syscall block](../programs.md#syscall-block). | +| `END` | Marks the end of a program block. | +| `HALT` | Marks the end of the entire program. | + +Let's consider a simple program below: + +``` +begin + + if.true + + else + + end +end +``` + +Block structure of this program is shown below. + +``` +JOIN + SPAN + + END + SPLIT + SPAN + + END + SPAN + + END + END +END +``` + +Executing this program on the VM can result in one of two possible instruction sequences. First, if after operations in `` are executed the top of the stack is $1$, the VM will execute the following: + +``` +JOIN +SPAN + +END +SPLIT +SPAN + +END +END +END +HALT +``` + +However, if after `` are executed, the top of the stack is $0$, the VM will execute the following: + +``` +JOIN +SPAN + +END +SPLIT +SPAN + +END +END +END +HALT +``` + +The main task of the decoder is to output exactly the same program hash, regardless of which one of the two possible execution paths was taken. However, before we can describe how this is achieved, we need to give an overview of the overall decoder structure. + +## Decoder structure + +The decoder is one of the more complex parts of the VM. It consists of the following components: + +* Main [execution trace](#decoder-trace) consisting of $24$ trace columns which contain the state of the decoder at a given cycle of a computation. +* Connection to the hash chiplet, which is used to offload [hash computations](#program-block-hashing) from the decoder. +* $3$ [virtual tables](#control-flow-tables) (implemented via multi-set checks), which keep track of code blocks and operations executing on the VM. + +### Decoder trace + +Decoder trace columns can be grouped into several logical sets of registers as illustrated below. + +![decoder_trace.png](../../img/design/decoder/decoder_trace.png) + +These registers have the following meanings: + +1. Block address register $a$. This register contains address of the hasher for the current block (row index from the auxiliary hashing table). It also serves the role of unique block identifiers. This is convenient, because hasher addresses are guaranteed to be unique. +2. Registers $b_0, ..., b_6$, which encode opcodes for operation to be executed by the VM. Each of these registers can contain a single binary value (either $1$ or $0$). And together these values describe a single opcode. +3. Hasher registers $h_0, ..., h_7$. When control flow operations are executed, these registers are used to provide inputs for the current block's hash computation (e.g., for `JOIN`, `SPLIT`, `LOOP`, `SPAN`, `CALL`, `SYSCALL` operations) or to record the result of the hash computation (i.e., for `END` operation). However, when regular operations are executed, $2$ of these registers are used to help with op group decoding, and the remaining $6$ can be used to hold operation-specific helper variables. +4. Register $sp$ which contains a binary flag indicating whether the VM is currently executing instructions inside a *basic* block (historically called "span block"). The flag is set to $1$ when the VM executes non-control flow instructions, and is set to $0$ otherwise. +5. Register $gc$ which keeps track of the number of unprocessed operation groups in a given *basic* block (historically called "span block"). +6. Register $ox$ which keeps track of a currently executing operation's index within its operation group. +7. Operation batch flags $c_0, c_1, c_2$ which indicate how many operation groups a given operation batch contains. These flags are set only for `SPAN` and `RESPAN` operations, and are set to $0$'s otherwise. +8. Two additional registers (not shown) are used primarily for constraint degree reduction. + +### Program block hashing + +To compute hashes of program blocks, the decoder relies on the [hash chiplet](../chiplets/hasher.md). Specifically, the decoder needs to perform two types of hashing operations: + +1. A simple 2-to-1 hash, where we provide a sequence of $8$ field elements and get back $4$ field elements representing the result. This is represented by one hash-controller pair plus one packed 16-row cycle in `Poseidon2PermutationAir` for the corresponding input state. +2. A sequential hash of $n$ elements. This requires multiple absorption steps, and at each step $8$ field elements are absorbed into the hasher. At the controller level, each absorbed batch contributes one `(input, output)` controller pair, so the controller addresses for successive batches advance by $2$. + +To make hashing requests to the hash chiplet and to read the results from it, we will need to divide out relevant values from the [chiplets bus](../chiplets/index.md#chiplets-bus) column $b_{chip}$ as described below. + +#### Simple 2-to-1 hash + +To initiate a 2-to-1 hash of $8$ elements ($v_0, ..., v_7$) we need to divide $b_{chip}$ by the following value: + +$$ +\alpha_0 + \alpha_1 \cdot m_{bp} + \alpha_2 \cdot r + \sum_{i=0}^7 (\alpha_{i+4} \cdot v_i) +$$ + +where: +* $m_{bp}$ is a label indicating beginning of a new permutation. Value of this label is computed based on hash-controller selector flags according to the methodology described [here](../chiplets/hasher.md#lookup-buses). +* $r$ is the address of the row at which the hashing begins. +* Some $\alpha$ values are skipped in the above (e.g., $\alpha_3$) because of how hash-controller rows are reduced to field elements (described [here](../chiplets/hasher.md#lookup-buses)). For example, $\alpha_3$ is used as a coefficient for node index values during Merkle path computations in the hasher, and thus, is not relevant in this case. The capacity lanes (state indices $8..11$, coefficients $\alpha_{12..15}$) are zero for these messages, so those terms drop out. + +To read the $4$-element result ($u_0, ..., u_3$), we need to divide $b_{chip}$ by the following value: + +$$ +\alpha_0 + \alpha_1 \cdot m_{hout} + \alpha_2 \cdot (r + 1) + \sum_{i=0}^3 (\alpha_{i+4} \cdot u_i) +$$ + +where: +* $m_{hout}$ is a label indicating return of the hash value. Value of this label is computed based on hash-controller selector flags according to the methodology described [here](../chiplets/hasher.md#lookup-buses). +* $r$ is the address of the row at which the hashing began. + +#### Sequential hash + +To initiate a sequential hash of $n$ elements ($v_0, ..., v_{n-1}$), we need to divide $b_{chip}$ by the following value: + +$$ +\alpha_0 + \alpha_1 \cdot m_{bp} + \alpha_2 \cdot r + \sum_{i=0}^7 (\alpha_{i+4} \cdot v_i) +$$ + +This also absorbs the first $8$ elements of the sequence into the hasher state. Then, to absorb the next sequence of $8$ elements (e.g., $v_8, ..., v_{15}$), we need to divide $b_{chip}$ by the following value: + +$$ +\alpha_0 + \alpha_1 \cdot m_{abp} + \alpha_2 \cdot (r + 2) + \sum_{i=0}^7 (\alpha_{i+4} \cdot v_{i + 8}) +$$ + +Where $m_{abp}$ is a label indicating absorption of more elements into the hasher state. Value of this label is computed based on hash-controller selector flags according to the methodology described [here](../chiplets/hasher.md#lookup-buses). + +We can keep absorbing elements into the hasher in the similar manner until all elements have been absorbed. Then, to read the result (e.g., $u_0, ..., u_3$), we need to divide $b_{chip}$ by the following value: + +$$ +\alpha_0 + \alpha_1 \cdot m_{hout} + \alpha_2 \cdot (r + 2 \cdot \lceil n / 8 \rceil - 1) + \sum_{i=0}^3 (\alpha_{i+4} \cdot u_i) +$$ + +Thus, for example, if $n = 14$, the result of the hash is available at controller output row $r + 3$ (two absorbed batches). + +### Control flow tables + +In addition to the hash chiplet, control flow operations rely on $3$ virtual tables: *block stack* table, *block hash* table, and _op group_ table. These tables are virtual in that they don't require separate trace columns. Their state is described solely by running product columns: $p_1$, $p_2$, and $p_3$. The tables are described in the following sections. + +#### Block stack table + +When the VM starts executing a new program block, it adds its block ID together with the ID of its parent block (and some additional info) to the *block stack* table. When a program block is fully executed, it is removed from the table. In this way, the table represents a stack of blocks which are currently executing on the VM. By the time program execution completes, block stack table must be empty. + +The block stack table is also used to ensure that execution contexts are managed properly across the `CALL` and `SYSCALL` operations. + +The table can be thought of as consisting of $11$ columns as shown below: + +![decoder_block_stack_table](../../img/design/decoder/decoder_block_stack_table.png) + +where: +* The first column ($t_0$) contains the ID of the block. +* The second column ($t_1$) contains the ID of the parent block. If the block has no parent (i.e., it is a root block of the program), parent ID is 0. +* The third column ($t_2$) contains a binary value which is set to $1$ is the block is a *loop* block, and to $0$ otherwise. +* The following 8 columns are only set to non-zero values for `CALL` and `SYSCALL` operations. They save all the necessary information to be able to restore the parent context properly upon the corresponding `END` operation + - the `prnt_b0` and `prnt_b1` columns refer to the stack helper columns B0 and B1 (current stack depth and last overflow address, respectively) + +In the above diagram, the first 2 rows correspond to 2 different `CALL` operations. The first `CALL` operation is called from the root context, and hence its parent fn hash is the zero hash. Additionally, the second `CALL` operation has a parent fn hash of `[h0, h1, h2, h3]`, indicating that the first `CALL` was to a procedure with that hash. + +Running product column $p_1$ is used to keep track of the state of the table. At any step of the computation, the current value of $p_1$ defines which rows are present in the table. + +To reduce a row in the block stack table to a single value, we compute the following. + +$$ +row = \alpha_0 + \sum_{i=0}^{10} (\alpha_{i+1} \cdot t_i), +$$ + +where $\alpha_0, ..., \alpha_{11}$ are the random values provided by the verifier. + +#### Block hash table + +When the VM starts executing a new program block, it adds hashes of the block's children to the *block hash* table. And when the VM finishes executing a block, it removes its hash from the block hash table. Thus, by the time program execution completes, block hash table must be empty. + +The table can be thought of as consisting of $7$ columns as shown below: + +![block_hash_table](../../img/design/decoder/block_hash_table.png) + +where: +* The first column ($t_0$) contains the ID of the block's parent. For program root, parent ID is $0$. +* The next $4$ columns ($t_1, ..., t_4$) contain the hash of the block. +* The next column ($t_5$) contains a binary value which is set to $1$ if the block is the first child of a *join* block, and to $0$ otherwise. +* The last column ($t_6$) contains a binary value which is set to $1$ if the block is a body of a loop, and to $0$ otherwise. + +Running product column $p_2$ is used to keep track of the state of the table. At any step of the computation, the current value of $p_2$ defines which rows are present in the table. + +To reduce a row in the block hash table to a single value, we compute the following. + +$$ +row = \alpha_0 + \sum_{i=0}^6 (\alpha_{i+1} \cdot t_i) +$$ + +Where $\alpha_0, ..., \alpha_7$ are the random values provided by the verifier. + +Unlike other virtual tables, block hash table does not start out in an empty state. Specifically, it is initialized with a single row containing the hash of the program's root block. This needs to be done because the root block does not have a parent and, thus, otherwise it would never be added to the block hash table. + +Initialization of the block hash table is done by setting the initial value of $p_2$ to the value of the row containing the hash of a program's root block. + +#### Op group table +*Op group* table is used in decoding of *basic* blocks, which are leaves in a program's MAST. As described [here](../programs.md#basic-block), a *basic* block can contain one or more operation batches, each batch containing up to $8$ operation groups. + +When the VM starts executing a new batch of operations, it adds all operation groups within a batch, except for the first one, to the *op group* table. Then, as the VM starts executing an operation group, it removes the group from the table. Thus, by the time all operation groups in a batch have been executed, the *op group* table must be empty. + +The table can be thought of as consisting of $3$ columns as shown below: + +![decoder_op_group_table](../../img/design/decoder/decoder_op_group_table.png) + +The meaning of the columns is as follows: + +* The first column ($t_0$) contains operation batch ID. During the execution of the program, each operation batch is assigned a unique ID. +* The second column ($t_1$) contains the position of the group in the *basic* block (not just in the current batch). The position is $1$-based and is counted from the end. Thus, for example, if a *basic* block consists of a single batch with $4$ groups, the position of the first group would be $4$, the position of the second group would be $3$ etc. (the reason for this is explained in [this](#single-batch-span) section). Note that the group with position $4$ is not added to the table, because it is the first group in the batch, so the first row of the table will be for the group with position $3$. +* The third column ($t_2$) contains the actual values of operation groups (this could include up to $9$ opcodes or a single immediate value). + +Permutation column $p_3$ is used to keep track of the state of the table. At any step of the computation, the current value of $p_3$ defines which rows are present in the table. + +To reduce a row in the op group table to a single value, we compute the following. + +$$ +row = \alpha_0 + \sum_{i=0}^2 (\alpha_{i+1} \cdot t_i) +$$ + +Where $\alpha_0, ..., \alpha_3$ are the random values provided by the verifier. + +### Control flow operation semantics + +In this section we describe high-level semantics of executing all control flow operations. The descriptions are not meant to be complete and omit some low-level details. However, they provide good intuition on how these operations work. + +#### JOIN operation + +Before a `JOIN` operation is executed by the VM, the prover populates $h_0, ..., h_7$ registers with hashes of left and right children of the *join* program block as shown in the diagram below. + +![decoder_join_operation](../../img/design/decoder/decoder_join_operation.png) + +In the above diagram, `blk` is the ID of the *join* block which is about to be executed. `blk` is also the address of the hasher row in the auxiliary hasher table. `prnt` is the ID of the block's parent. + +When the VM executes a `JOIN` operation, it does the following: + +1. Adds a tuple `(blk, prnt, 0, 0...)` to the block stack table. +2. Adds tuples `(blk, left_child_hash, 1, 0)` and `(blk, right_child_hash, 0, 0)` to the block hash table. +3. Initiates a 2-to-1 hash computation in the hash chiplet (as described [here](#simple-2-to-1-hash)) using `blk` as row address in the auxiliary hashing table and $h_0, ..., h_7$ as input values. + +#### SPLIT operation + +Before a `SPLIT` operation is executed by the VM, the prover populates $h_0, ..., h_7$ registers with hashes of true and false branches of the *split* program block as shown in the diagram below. + +![decoder_split_operation](../../img/design/decoder/decoder_split_operation.png) + +In the above diagram, `blk` is the ID of the *split* block which is about to be executed. `blk` is also the address of the hasher row in the auxiliary hasher table. `prnt` is the ID of the block's parent. + +When the VM executes a `SPLIT` operation, it does the following: + +1. Adds a tuple `(blk, prnt, 0, 0...)` to the block stack table. +2. Pops the stack and:\ + a. If the popped value is $1$, adds a tuple `(blk, true_branch_hash, 0, 0)` to the block hash table.\ + b. If the popped value is $0$, adds a tuple `(blk, false_branch_hash, 0, 0)` to the block hash table.\ + c. If the popped value is neither $1$ nor $0$, the execution fails. +3. Initiates a 2-to-1 hash computation in the hash chiplet (as described [here](#simple-2-to-1-hash)) using `blk` as row address in the auxiliary hashing table and $h_0, ..., h_7$ as input values. + +#### LOOP operation + +Before a `LOOP` operation is executed by the VM, the prover populates $h_0, ..., h_3$ registers with hash of the loop's body as shown in the diagram below. The remaining registers $h_4, ..., h_7$ are set to $0$ so the hash input is padded to a full 8-element rate. + +![decoder_loop_operation](../../img/design/decoder/decoder_loop_operation.png) + +In the above diagram, `blk` is the ID of the *loop* block which is about to be executed. `blk` is also the address of the hasher row in the auxiliary hasher table. `prnt` is the ID of the block's parent. + +The `LOOP` operation has do-while semantics: the body is entered unconditionally for the first iteration, with the condition checked only at the end of each iteration by `REPEAT`/`END`. When the VM executes a `LOOP` operation, it does the following: + +1. Adds a tuple `(blk, prnt, 1, 0...)` to the block stack table (the `1` indicates that the + loop's body is expected to be executed). +2. Adds a tuple `(blk, loop_body_hash, 0, 1)` to the block hash table. +3. Initiates a 2-to-1 hash computation in the hash chiplet (as described [here](#simple-2-to-1-hash)) using `blk` as row address in the auxiliary hashing table and the padded input $[h_0, ..., h_3, 0, 0, 0, 0]$. + +The `LOOP` operation does not read or pop the stack. + +#### SPAN operation + +Before a `SPAN` operation is executed by the VM, the prover populates $h_0, ..., h_7$ registers with contents of the first operation batch of the basic block as shown in the diagram below. The prover also sets the group count register $gc$ to the total number of operation groups in the basic block. + +![decoder_span_block](../../img/design/decoder/decoder_span_block.png) + +In the above diagram, `blk` is the ID of the *basic* block which is about to be executed. `blk` is also the address of the hasher row in the auxiliary hasher table. `prnt` is the ID of the block's parent. `g0_op0` is the first operation of the batch, and `g_0'` is the first operation group of the batch with the first operation removed. + +When the VM executes a `SPAN` operation, it does the following: + +1. Adds a tuple `(blk, prnt, 0, 0...)` to the block stack table. +2. Adds groups of the operation batch, as specified by op batch flags (see [here](#operation-batch-flags)) to the op group table. +3. Initiates a sequential hash computation in the hash chiplet (as described [here](#sequential-hash)) using `blk` as row address in the auxiliary hashing table and $h_0, ..., h_7$ as input values. +4. Sets the `in_span` register to $1$. +5. Decrements `group_count` register by $1$. +6. Sets the `op_index` register to $0$. + + +#### DYN operation + +![decoder_dyn_operation](../../img/design/decoder/decoder_dyn_operation.png) + +In the above diagram, `blk` is the ID of the *dyn* block which is about to be executed. `blk` is also the address of the hasher row in the auxiliary hasher table. `p_addr` is the ID of the block's parent. + +When the VM executes a `DYN` operation, it does the following: + +1. Adds a tuple `(blk, p_addr, 0, 0...)` to the block stack table. +2. Sends a memory read request to the memory chiplet, using `s0` as the memory address. The result `hash of callee` is placed in the decoder hasher trace at $h_0, h_1, h_2, h_3$. +3. Adds the tuple `(blk, hash of callee, 0, 0)` to the block hash table. +4. Initiates a 2-to-1 hash computation in the hash chiplet (as described [here](#simple-2-to-1-hash)) using `blk` as row address in the auxiliary hashing table and `[ZERO; 8]` as input values. +5. Performs a stack left shift + - Above `s16` was pulled from the stack overflow table if present; otherwise set to `0`. + +Note that unlike `DYNCALL`, the `ctx` and `fn_hash` registers are unchanged. + +#### DYNCALL operation + +![decoder_dyncall_operation](../../img/design/decoder/decoder_dyncall_operation.png) + +In the above diagram, `blk` is the ID of the *dyn* block which is about to be executed. `blk` is also the address of the hasher row in the auxiliary hasher table. `p_addr` is the ID of the block's parent. + +When the VM executes a `DYNCALL` operation, it does the following: + +1. Adds a tuple `(blk, p_addr, 0, ctx, b_0, b_1, fn_hash[0..3])` to the block stack table. +2. Sends a memory read request to the memory chiplet, using `s0` as the memory address. The result `hash of callee` is placed in the decoder hasher trace at $h_0, h_1, h_2, h_3$. +3. Sends a memory write request to the memory chiplet to set address `FMP_ADDR = 2^32 - 1` to `FMP_INIT_VALUE = 2^31` in the new memory context. This initializes the `fmp` in the new context. +4. Adds the tuple `(blk, hash of callee, 0, 0)` to the block hash table. +5. Initiates a 2-to-1 hash computation in the hash chiplet (as described [here](#simple-2-to-1-hash)) using `blk` as row address in the auxiliary hashing table and `[ZERO; 8]` as input values. +6. Performs a stack left shift + - Above `s16` was pulled from the stack overflow table if present; otherwise set to `0`. + +Similar to `CALL`, `DYNCALL` sets up a new `ctx`, and sets the `fn_hash` registers to the callee hash. + +#### END operation + +Before an `END` operation is executed by the VM, the prover populates $h_0, ..., h_3$ registers with the hash of the block which is about to end. The prover also sets values in $h_4$ and $h_5$ registers as follows: +* $h_4$ is set to $1$ if the block is a body of a *loop* block. We denote this value as `f0`. +* $h_5$ is set to $1$ if the block is a *loop* block. We denote this value as `f1`. Under do-while + semantics every *loop* block is entered, so $h_5 = 1$ for every loop's `END` row. +* $h_6$ is set to $1$ if the block is a *call* block. We denote this value as `f2`. +* $h_7$ is set to $1$ if the block is a *syscall* block. We denote this value as `f3`. + +![decoder_end_operation](../../img/design/decoder/decoder_end_operation.png) + +In the above diagram, `blk` is the ID of the block which is about to finish executing. `prnt` is the ID of the block's parent. + +When the VM executes an `END` operation, it does the following: + +1. Removes a tuple from the block stack table. + - if `f2` or `f3` is set, we remove a row `(blk, prnt, 0, ctx_next, b0_next, b1_next, fn_hash_next[0..4])` + - in the above, the `x_next` variables denote the column `x` in the next row + - else, we remove a row `(blk, prnt, f1, 0, 0, 0, 0, 0)` +2. Removes a tuple `(prnt, current_block_hash, nxt, f0)` from the block hash table, where $nxt=0$ if the next operation is either `END` or `REPEAT`, and $1$ otherwise. +3. Reads the hash result from the hash chiplet (as described [here](#program-block-hashing)) using `blk + 1` as the controller output row address. +4. If $h_5 = 1$ (i.e., we are exiting a *loop* block), pops the value off the top of the stack and verifies that the value is $0$. +5. Verifies that `group_count` register is set to $0$. + +#### HALT operation + +Before a `HALT` operation is executed by the VM, the VM copies values in $h_0, ..., h_3$ registers to the next row as illustrated in the diagram below: + +![decoder_halt_operation](../../img/design/decoder/decoder_halt_operation.png) + +In the above diagram, `blk` is the ID of the block which is about to finish executing. + +When the VM executes a `HALT` operation, it does the following: + +1. Verifies that block address register is set to $0$. +2. If we are not at the last row of the trace, verifies that the next operation is `HALT`. +3. Copies values of $h_0, ..., h_3$ registers to the next row. +4. Populates all other decoder registers with $0$'s in the next row. + +#### REPEAT operation + +Before a `REPEAT` operation is executed by the VM, the VM copies values in registers $h_0, ..., h_4$ to the next row as shown in the diagram below. + +![decoder_repeat_operation](../../img/design/decoder/decoder_repeat_operation.png) + +In the above diagram, `blk` is the ID of the loop's body and `prnt` is the ID of the loop. + +When the VM executes a `REPEAT` operation, it does the following: + +1. Checks whether register $h_4$ is set to $1$. If it isn't (i.e., we are not in a loop), the execution fails. +2. Pops the stack and if the popped value is $1$, adds a tuple `(prnt, loop_body_loop 0, 1)` to the block hash table. If the popped value is not $1$, the execution fails. + +The effect of the above is that the VM needs to execute the loop's body again to clear the block hash table. + +#### RESPAN operation + +Before a `RESPAN` operation is executed by the VM, the VM copies the ID of the current block `blk` and the number of remaining operation groups in the basic block to the next row, and sets the value of `in_span` column to $0$. The prover also sets the value of $h_1$ register for the next row to the ID of the current block's parent `prnt` as shown in the diagram below: + +![decoder_respan_operation](../../img/design/decoder/decoder_respan_operation.png) + +In the above diagram, `g0_op0` is the first operation of the new operation batch, and `g0'` is the first operation group of the batch with `g0_op0` operation removed. + +When the VM executes a `RESPAN` operation, it does the following: + +1. Increments block address by $2$. +2. Removes the tuple `(blk, prnt, 0, 0...)` from the block stack table. +3. Adds the tuple `(blk+2, prnt, 0, 0...)` to the block stack table. +4. Absorbs values in registers $h_0, ..., h_7$ into the hasher state of the hash chiplet (as described [here](#sequential-hash)). +5. Sets the `in_span` register to $1$. +6. Adds groups of the operation batch, as specified by op batch flags (see [here](#operation-batch-flags)) to the op group table using `blk+2` as batch ID. + +The net result of the above is that we incremented the ID of the current block by $2$ (the next controller input row) and added the next set of operation groups to the op group table. + +#### CALL operation + +Recall that the purpose of a `CALL` operation is to execute a procedure in a new execution context. Specifically, this means that the entire memory is zero'd in the new execution context, and the stack is truncated to a depth of 16 (i.e. any element in the stack overflow table is not available in the new context). On the corresponding `END` instruction, the prover will restore the previous execution context (verified by the block stack table). + +Before a `CALL` operation, the prover populates $h_0, ..., h_3$ registers with the hash of the procedure being called. In the next row, the prover + +- sets the context ID to the next row's CLK value +- sets the `fn hash` registers to the hash of the callee + - This register is what the `caller` instruction uses to return the hash of the caller +- resets the stack `B0` register to 16 (which tracks the current stack depth) +- resets the overflow address to 0 (which tracks the "address" of the last element added to the overflow table) + - it is set to 0 to indicate that the overflow table is empty + +![decoder_call_operation](../../img/design/decoder/decoder_call_operation.png) + +In the above diagram, `blk` is the ID of the *call* block which is about to be executed. `blk` is also the address of the hasher row in the auxiliary hasher table. `prnt` is the ID of the block's parent. + +When the VM executes a `CALL` operation, it does the following: + +1. Adds a tuple `(blk, prnt, 0, p_ctx, p_b0, p_b1, prnt_fn_hash[0..4])` to the block stack table. +2. Initiates a 2-to-1 hash computation in the hash chiplet (as described [here](#simple-2-to-1-hash)) using `blk` as row address in the auxiliary hashing table and $h_0, ..., h_3$ as input values. +3. Sends a memory write request to the memory chiplet to set address `FMP_ADDR = 2^32 - 1` to `FMP_INIT_VALUE = 2^31` in the new memory context. This initializes the `fmp` in the new context. + +#### SYSCALL operation + +Similarly to the `CALL` operation, a `SYSCALL` changes the execution context. However, it always jumps back to the root context, and executes kernel procedures only. + +Before a `SYSCALL` operation, the prover populates $h_0, ..., h_3$ registers with the hash of the procedure being called. In the next row, the prover + +- sets the context ID to 0, +- does NOT modify the `fn hash` register + - Hence, the `fn hash` register contains the procedure hash of the caller, to be accessed by the `caller` instruction, +- resets the stack `B0` register to 16 (which tracks the current stack depth) +- resets the overflow address to 0 (which tracks the "address" of the last element added to the overflow table) + - it is set to 0 to indicate that the overflow table is empty + +![decoder_syscall_operation](../../img/design/decoder/decoder_syscall_operation.png) + +In the above diagram, `blk` is the ID of the *syscall* block which is about to be executed. `blk` is also the address of the hasher row in the auxiliary hasher table. `prnt` is the ID of the block's parent. + +When the VM executes a `SYSCALL` operation, it does the following: + +1. Adds a tuple `(blk, prnt, 0, p_ctx, p_b0, p_b1, prnt_fn_hash[0..4])` to the block stack table. +2. Sends a request to the kernel ROM chiplet indicating that `hash of callee` is being accessed. + - this results in a fault if `hash of callee` does not correspond to the hash of a kernel procedure +3. Initiates a 2-to-1 hash computation in the hash chiplet (as described [here](#simple-2-to-1-hash)) using `blk` as row address in the auxiliary hashing table and $h_0, ..., h_3$ as input values. + +## Program decoding + +When decoding a program, we start at the root block of the program. We can compute the hash of the root block directly from hashes of its children. The prover provides hashes of the child blocks non-deterministically, and we use them to compute the program's hash (here we rely on the hash chiplet). We then verify the program hash via boundary constraints. Thus, if the prover provided valid hashes for the child blocks, we will get the expected program hash. + +Now, we need to verify that the VM executed the child blocks correctly. We do this recursively similar to what is described above: for each of the blocks, the prover provides hashes of its children non-deterministically and we verify that the hash has been computed correctly. We do this until we get to the leaf nodes (i.e., *basic* blocks). Hashes of *basic* blocks are computed sequentially from the instructions executed by the VM. + +The sections below illustrate how different types of code blocks are decoded by the VM. + +### JOIN block decoding + +When decoding a *join* bock, the VM first executes a `JOIN` operation, then executes the first child block, followed by the second child block. Once the children of the *join* block are executed, the VM executes an `END` operation. This is illustrated in the diagram below. + +![decoder_join_block_decoding](../../img/design/decoder/decoder_join_block_decoding.png) + +As described previously, when the VM executes a `JOIN` operation, hashes of both children are added to the block hash table. These hashes are removed only when the `END` operations for the child blocks are executed. Thus, until both child blocks are executed, the block hash table is not cleared. + +### SPLIT block decoding + +When decoding a *split* block, the decoder pops an element off the top of the stack, and if the popped element is $1$, executes the block corresponding to the `true branch`. If the popped element is $0$, the decoder executes the block corresponding to the `false branch`. This is illustrated on the diagram below. + +![decoder_split_block_decoding](../../img/design/decoder/decoder_split_block_decoding.png) + +As described previously, when the VM executes a `SPLIT` operation, only the hash of the branch to be executed is added to the block hash table. Thus, until the child block corresponding to the required branch is executed, the block hash table is not cleared. + +### LOOP block decoding + +A *loop* block has do-while semantics: the body is entered unconditionally for the first iteration, and the trailing condition the body leaves on top of the stack determines whether the VM executes another iteration (`REPEAT`) or exits (`END`). + +When the VM executes a `LOOP` operation, it adds the hash of the loop's body to the block hash table and adds a row to the block stack table with the `is_loop` value set to $1$. The `LOOP` operation itself does not read or pop the stack. + +To clear the block hash table, the VM needs to execute the loop body (executing the `END` operation for the loop body block will remove the corresponding row from the block hash table). After the loop body is executed, if the top of the stack is $1$, the VM executes a `REPEAT` operation (executing `REPEAT` operation when the top of the stack is $0$ will result in an error). This operation again adds the hash of the loop's body to the block hash table. Thus, the VM needs to execute the loop body again to clear the block hash table. + +This process is illustrated on the diagram below. + +![decoder_loop_execution](../../img/design/decoder/decoder_loop_execution.png) + +The above steps are repeated until the top of the stack becomes $0$, at which point the VM executes the `END` operation. Since `is_loop` is always $1$ for a loop's `END` row under do-while semantics, the `END` operation always pops the trailing condition off the stack. If the popped value is not $0$, the operation fails. Thus, the VM can exit the loop block only when the top of the stack is $0$. + +### DYN block decoding + +When decoding a *dyn* bock, the VM first executes a `DYN` operation, then executes the child block dynamically specified by the top of the stack. Once the child of the *dyn* block has been executed, the VM executes an `END` operation. This is illustrated in the diagram below. + +![decoder_dyn_block_decoding](../../img/design/decoder/decoder_dyn_block_decoding.png) + +As described previously, when the VM executes a `DYN` operation, the hash of the child is added to the block hash table. This hash is removed only when the `END` operation for the child block is executed. Thus, until the child block corresponding to the dynamically specified target is executed, the block hash table is not cleared. + +### Basic block decoding + +As described [here](../programs.md#basic-block), a *basic* block can contain one or more operation batches, each batch containing up to $8$ operation groups. At the high level, decoding of a basic block is done as follows: + +1. At the beginning of the block, we make a request to the hash chiplet which initiates the hasher, absorbs the first operation batch ($8$ field elements) into the hasher, and returns the row address of the hasher, which we use as the unique ID for the *basic* block (see [here](#sequential-hash)). +2. We then add groups of the operation batch, as specified by op batch flags (but always skipping the first one) to the op group table. +3. We then remove operation groups from the op group table in the FIFO order one by one, and decode them in the manner similar to the one described [here](#operation-group-decoding). +4. Once all operation groups in a batch have been decoded, we absorb the next batch into the hasher and repeat the process described above. +5. Once all batches have been decoded, we return the hash of the basic block from the hasher. + +Overall, three control flow operations are used when decoding a *basic* block: + +1. `SPAN` operation is used to initialize a hasher and absorbs the first operation batch into it. +2. `RESPAN` operation is used to absorb any additional batches in the basic block. +3. `END` operation is used to end the decoding of a basic block and retrieve its hash from the hash chiplet. + +#### Operation group decoding + +As described [here](../programs.md#basic-block), an operation group is a sequence of operations which can be encoded into a single field element. For a field element of $64$ bits, we can fit up to $9$ operations into a group. We do this by concatenating binary representations of opcodes together with the first operation located in the least significant position. + +We can read opcodes from the group by simply subtracting them from the op group value and then dividing the result by $2^7$. Once the value of the op group reaches $0$, we know that all opcodes have been read. Graphically, this can be illustrated like so: + +![decoder_operation_group_decoding](../../img/design/decoder/decoder_operation_group_decoding.png) + +Notice that despite their appearance, `op bits` is actually $7$ separate registers, while `op group` is just a single register. + +We also need to make sure that at most $9$ operations are executed as a part of a single group. For this purpose we use the `op_index` column. Values in this column start out at $0$ for each operation group, and are incremented by $1$ for each executed operation. To make sure that at most $9$ operations can be executed in a group, the value of the `op_index` column is not allowed to exceed $8$. + +#### Operation batch flags + +Operation batch flags are used to specify how many operation groups comprise a given operation batch. For most batches, the number of groups will be equal to $8$. However, for the last batch in a block (or for the first batch, if the block consists of only a single batch), the number of groups may be less than $8$. Since processing of new batches starts only on `SPAN` and `RESPAN` operations, only for these operations the flags can be set to non-zero values. + +To simplify the constraint system, the number of groups in a batch can be only one of the following values: $1$, $2$, $4$, and $8$. If the number of groups in a batch does not match one of these values, the batch is simply padded with `NOOP`'s (one `NOOP` per added group). Consider the diagram below. + +![decoder_OPERATION_batch_flags](../../img/design/decoder/decoder_OPERATION_batch_flags.png) + +In the above, the batch contains $3$ operation groups. To bring the count up to $4$, we consider the $4$-th group (i.e., $0$) to be a part of the batch. Since a numeric value for `NOOP` operation is $0$, op group value of $0$ can be interpreted as a single `NOOP`. + +Operation batch flags (denoted as $c_0, c_1, c_2$), encode the number of groups and define how many groups are added to the op group table as follows: + +* `(1, -, -)` - $8$ groups. Groups in $h_1, ... h_7$ are added to the op group table. +* `(0, 1, 0)` - $4$ groups. Groups in $h_1, ... h_3$ are added to the op group table +* `(0, 0, 1)` - $2$ groups. Groups in $h_1$ is added to the op group table. +* `(0, 1, 1)` - $1$ group. Nothing is added to the op group table +* `(0, 0, 0)` - not a `SPAN` or `RESPAN` operation. + +#### Single-batch span + +The simplest example of a *basic* block is a block with a single batch. This batch may contain up to $8$ operation groups (e.g., $g_0, ..., g_7$). Decoding of such a block is illustrated in the diagram below. + +![decoder_single_batch_span](../../img/design/decoder/decoder_single_batch_span.png) + +Before the VM starts processing this *basic* block, the prover populates registers $h_0, ..., h_7$ with operation groups $g_0, ..., g_7$. The prover also puts the total number of groups into the `group_count` register $gc$. In this case, the total number of groups is $8$. + +When the VM executes a `SPAN` operation, it does the following: + +1. Initiates hashing of elements $g_0, ..., g_7$ using hash chiplet. The hasher address is used as the block ID `blk`, and it is inserted into `addr` register in the next row. +2. Adds a tuple `(blk, prnt, 0)` to the block stack table. +3. Sets the `is_span` register to $1$ in the next row. +4. Sets the `op_index` register to $0$ in the next row. +5. Decrements `group_count` register by $1$. +6. Sets `op bits` registers at the next step to the first operation of $g_0$, and also copies $g_0$ with the first operation removed (denoted as $g_0'$) to the next row. +7. Adds groups $g_1, ..., g_7$ to the op group table. Thus, after the `SPAN` operation is executed, op group table looks as shown below. + +![decoder_op_group_table_after_span_op](../../img/design/decoder/decoder_op_group_table_after_span_op.png) + +Then, with every step the next operation is removed from $g_0$, and by step $9$, the value of $g_0$ is $0$. Once this happens, the VM does the following: + +1. Decrements `group_count` register by $1$. +2. Sets `op bits` registers at the next step to the first operation of $g_1$. +3. Sets `hasher` register $h_0$ to the value of $g_1$ with the first operation removed (denoted as $g_1'$). +4. Removes row `(blk, 7, g1)` from the op group table. This row can be obtained by taking values from registers: `addr`, `group_count`, and $h_0' + \displaystyle\sum_{i=0}^6(2^i \cdot b_i')$ for $i \in [0, 7)$, where $h_0'$ and $b_i'$ refer to values in the next row for the first hasher column and `op_bits` columns respectively. + +Note that we rely on the `group_count` column to construct the row to be removed from the op group table. Since group count is decremented from the total number of groups to $0$, to remove groups from the op group table in correct order, we need to assign group position to groups in the op group table in the reverse order. For example, the first group to be removed should have position $7$, the second group to be removed should have position $6$ etc. + +Decoding of $g_1$ is performed in the same manner as decoding of $g_0$: with every subsequent step the next operation is removed from $g_1$ until its value reaches $0$, at which point, decoding of group $g_2$ begins. + +The above steps are executed until value of `group_count` reaches $0$. Once `group_count` reaches $0$ and the last operation group $g_7$ is executed, the VM executes the `END` operation. Semantics of the `END` operation are described [here](#end-operation). + +Notice that by the time we get to the `END` operation, all rows are removed from the op group table. + +#### Multi-batch basic block + +A *basic* block may contain an unlimited number of operation batches. As mentioned previously, to absorb a new batch into the hasher, the VM executes a `RESPAN` operation. The diagram below illustrates decoding of a *basic* block consisting of two operation batches. + +![decoder_multi_batch_span](../../img/design/decoder/decoder_multi_batch_span.png) + +Decoding of such a block will look very similar to decoding of the single-basic block described previously, but there also will be some differences. + +First, after the `SPAN` operation is executed, the op group table will look as follows: + +![decoder_op_group_table_multi_span](../../img/design/decoder/decoder_op_group_table_multi_span.png) + +Notice that while the same groups ($g_1, ..., g_7$) are added to the table, their positions now reflect the total number of groups in the *basic* block. + +Second, executing a `RESPAN` operation increments the hasher controller address by $2$. This is done because each absorbed batch is represented by one controller pair `(input, output)`, so the next batch starts at the next controller input row. + +Incrementing value of `addr` register actually changes the ID of the *basic* block (though, for a *basic* block, it may be more appropriate to view values in this column as IDs of individual operation batches). This means that we also need to update the block stack table. Specifically, we need to remove row `(blk, prnt, 0)` from it, and replace it with row `(blk + 2, prnt, 0)`. To perform this operation, the prover sets the value of $h_1` in the next row to `prnt`. + +Executing a `RESPAN` operation also adds groups $g_9, g_{10}, g_{11}$ to the op group table, which now would look as follows: + +![decoder_op_group_table_post_respan](../../img/design/decoder/decoder_op_group_table_post_respan.png) + +Then, the execution of the second batch proceeds in a manner similar to the first batch: we remove operations from the current op group, execute them, and when the value of the op group reaches $0$, we start executing the next group in the batch. Thus, by the time we get to the `END` operation, the op group table should be empty. + +When executing the `END` operation, the hash of the *basic* block will be read from the paired controller output row at address `addr + 1`, which, in our example, will be equal to `blk + 3` after one `RESPAN`. + +#### Handling immediate values + +Miden VM operations can carry immediate values. Currently, the only such operation is a `PUSH` operation. Since immediate values can be thought of as constants embedded into program code, we need to make sure that changing immediate values affects program hash. + +To achieve this, we treat immediate values in a manner similar to how we treat operation groups. Specifically, when computing hash of a *basic* block, immediate values are absorbed into the hasher state in the same way as operation groups are. As mentioned previously, an immediate value is represented by a single field element, and thus, an immediate value takes place of a single operation group. + +The diagram below illustrates decoding of a *basic* block with $9$ operations one of which is a `PUSH` operation. + +![decoder_decoding_span_block_with_push](../../img/design/decoder/decoder_decoding_span_block_with_push.png) + +In the above, when the `SPAN` operation is executed, immediate value `imm0` will be added to the op group table, which will look as follows: + +![decoder_imm_vale_op_group_table](../../img/design/decoder/decoder_imm_vale_op_group_table.png) + +Then, when the `PUSH` operation is executed, the VM will do the following: + +1. Decrement `group_count` by $1$. +2. Remove a row from the op group table equal to `(addr, group_count, s0')`, where $s_0'$ is the value of the top of the stack at the next row (i.e., it is the value that is pushed onto the stack). + +Thus, after the `PUSH` operation is executed, the op group table is cleared, and group count decreases to $0$ (which means that there are no more op groups to execute). Decoding of the rest of the op group proceeds as described in the previous sections. + +## Program decoding example + +Let's run through an example of decoding a simple program shown previously: + +``` +begin + + if.true + + else + + end +end +``` + +Translating this into code blocks with IDs assigned, we get the following: + +``` +b0: JOIN + b1: SPAN + + b1: END + b2: SPLIT + b3: SPAN + + b3: END + b4: SPAN + + b4: END + b2: END +b0: END +``` + +The root of the program is a *join* block $b_0$. This block contains two children: a *basic* bock $b_1$ and a *split* block $b_2$. In turn, the *split* block $b_2$ contains two children: a *basic* block $b_3$ and a *basic* block $b_4$. + +When this program is executed on the VM, the following happens: + +1. Before the program starts executing, block hash table is initialized with a single row containing the hash of $b_0$. +2. Then, `JOIN` operation for $b_0$ is executed. It adds hashes of $b_1$ and $b_2$ to the block hash table. It also adds an entry for $b_0$ to the block stack table. States of both tables after this step are illustrated below. +3. Then, *basic* $b_1$ is executed and a sequential hash of its operations is computed. Also, when `SPAN` operation for $b_1$ is executed, an entry for $b_1$ is added to the block stack table. At the end of $b_1$ (when `END` is executed), entries for $b_1$ are removed from both the block hash and block stack tables. +4. Then, `SPLIT` operation for $b_2$ is executed. It adds an entry for $b_2$ to the block stack table. Also, depending on whether the top of the stack is $1$ or $0$, either hash of $b_3$ or hash of $b_4$ is added to the block hash table. Let's say the top of the stack is $1$. Then, at this point, the block hash and block stack tables will look like in the second picture below. +5. Then, *basic* $b_3$ is executed and a sequential hash of its instructions is computed. Also, when `SPAN` operation for $b_3$ is executed, an entry for $b_3$ is added to the block stack table. At the end of $b_3$ (when `END` is executed), entries for $b_3$ are removed from both the block hash and block stack tables. +6. Then, `END` operation for $b_2$ is executed. It removes the hash of $b_2$ from the block hash table, and also removes the entry for $b_2$ from the block stack table. The third picture below illustrates the states of block stack and block hash tables after this step. +7. Then, `END` for $b_0$ is executed, which removes entries for $b_0$ from the block stack and block hash tables. At this point both tables are empty. +8. Finally, a sequence of `HALT` operations is executed until the length of the trace reaches a power of two. + +States of block hash and block stack tables after step 2: +![decoder_state_block_hash_2](../../img/design/decoder/decoder_state_block_hash_2.png) + +States of block hash and block stack tables after step 4: +![decoder_state_block_hash_4](../../img/design/decoder/decoder_state_block_hash_4.png) + +States of block hash and block stack tables after step 6: +![decoder_state_block_hash_6](../../img/design/decoder/decoder_state_block_hash_6.png) diff --git a/versioned_docs/version-0.16/reference/miden-vm/design/deferred/_category_.yml b/versioned_docs/version-0.16/reference/miden-vm/design/deferred/_category_.yml new file mode 100644 index 00000000..52511f7a --- /dev/null +++ b/versioned_docs/version-0.16/reference/miden-vm/design/deferred/_category_.yml @@ -0,0 +1,4 @@ +label: "Deferred computation" +# Determines where this documentation section appears relative to other sections in the parent folder +position: 7 +collapsed: true diff --git a/versioned_docs/version-0.16/reference/miden-vm/design/deferred/index.md b/versioned_docs/version-0.16/reference/miden-vm/design/deferred/index.md new file mode 100644 index 00000000..5c8bb89e --- /dev/null +++ b/versioned_docs/version-0.16/reference/miden-vm/design/deferred/index.md @@ -0,0 +1,275 @@ +--- +title: "Deferred computation" +sidebar_position: 1 +--- + +# Deferred computation + +*Deferred computation* is the mechanism by which a Miden program offloads an expensive or +non-native computation — a hash, a signature check, elliptic-curve or big-integer arithmetic — and +emits, in its place, an auditable record of *what was claimed*. That record is a content-addressed +DAG of nodes, committed by a single rolling digest (`DeferredState.root`, the **deferred +root commitment**). The DAG is designed to be verified **externally**: either alongside the Miden +VM's STARK proof, or by a dedicated *Precompile VM* whose proof attests that every committed node +evaluates correctly. + +The DAG can be read as a small **program**: each node is a term, and evaluating the deferred root +to `TRUE` proves every claim it transitively references. The framework (`miden_core::deferred`) +owns the data model, the root commitment, and the wire format; individual *precompiles* plug in the +meaning of the nodes. + +> **Status.** This page describes the current proof-bound precompile model: the VM accumulates +> precompile claims into a deferred root, and `ExecutionProof` carries a `DeferredProof` for that +> root: empty, wire-backed for partial proofs, or STARK-backed for final proofs. See +> [Status and scope](#status-and-scope). +> +> For the precise `DeferredState`, precompile, and public API contract, see +> [Deferred state semantics and API contract](./semantics.md). + +## Motivation + +The deferred subsystem models proof-bound work as a **graph of tagged payloads and structural +edges**. This lets the Precompile VM prove computation at a finer grain — for example, a single +curve or field operation — without re-hashing every intermediate as an opaque standalone assertion. +A calculation like `(a + b) · c` can reference and share sub-results by content address, so +operation-heavy precompiles avoid duplicating the same hashing work. + +Each deferred node evaluates to a canonical node; an operation references its operands by their +content address; shared sub-computations are shared in the graph. When the framework needs ordered +accumulation, it represents it as a chain of semantic AND nodes whose root is a statement that must +evaluate to `TRUE`. + +The DAG is also intended to match the draft Precompile VM model. In that design, a precompile's +operations and constraints are described as a graph of canonical values, payloads, and joins — so +modelling deferred computation the same way lets a precompile's native host implementation mirror +its eventual constraint implementation. This direction is developed in GitHub discussion #3005. + +## Precompile proof shape + +The VM STARK proves execution and the deferred root accumulated for precompile claims; it does not +prove those claims itself. `DeferredProof` supplies the proof material for that root: + +- `Empty`: no precompile claims. +- `Wire`: partial proof; carries canonical `DeferredStateWire` so another prover can prove it later. +- `Stark`: final proof; carries a precompile VM STARK proof for the same deferred root. + +Verification resolves a trusted root from `DeferredProof` and uses it as the VM STARK public input. +The root is not supplied separately. + +## The model + +A **node** is a `(tag, payload)` pair, addressed by its 4-felt Poseidon2 **digest**. Identical +content yields an identical digest, so equal subterms are shared automatically (hash-consing). + +- A **tag** is a node's identity and constructor: externally, precompile tags are built with + `Tag::precompile(id, args)`, while `Tag::from_word` is reserved for raw stack/wire decoding. The `id` + selects the owning precompile; the three immediate felts (`args`) are entirely the precompile's + to interpret (a discriminant, a data length, a small constant, …). The framework reserves ids `0`, + `1`, and `2` for itself: `Tag::TRUE = [0, 0, 0, 0]` tags the canonical `TRUE` node, + `Tag::AND = [1, 0, 0, 0]` tags semantic conjunction nodes, and + `Tag::CHUNKS = [2, 0, 0, 0]` tags framework-owned opaque byte chunks. No precompile may claim + these ids. Deferred statement accumulation uses the same semantic `AND` constructor as a + restricted right-spined chain. +- A **payload** is the node's body, in one of four shapes: + - the framework `TRUE` sentinel, carrying no data; it is the only zero-payload node; + - a data payload: one or more 8-felt rate-sized chunks, linearly hashed under the tag. An empty + data payload is forbidden; precompiles decide whether a data payload represents a scalar, + digest, message, hash preimage, coordinate, or some other local value; + - a join payload: two child digests (`lhs`, `rhs`) for anything referential, such as a binary + operation, predicate, or AND step; + - a pair-list payload: one or more structural digest pairs for precompile-specific multi-pair + structures. Pairs are encoded in payload order as 8-felt chunks `lhs || rhs`, and their child + order is `lhs0`, `rhs0`, `lhs1`, `rhs1`, and so on. Canonical wire encodes the same ordered + pairs as topological child indices. Empty pair lists are rejected; exact pair-count/arity + constraints are semantic and enforced by the owning precompile. Budget accounting treats each + pair as one ordinary 8-felt payload block, in addition to the tag word. + +The digest binds the tag in the Poseidon2 capacity, so a node's address commits to *both* its +identity and its body. Every non-empty payload is absorbed as one or more 8-felt blocks under the +node tag. + +## Precompiles + +A **precompile** is the framework's extension point: an implementation of the `Precompile` trait +that claims one tag id and, within that slice of tag space, defines a *family of node types* +plus the rules that give them meaning. Think of it as a small typed sub-language embedded in the +DAG. Concrete proof-bound precompiles live in the `miden-precompiles` crate; MASM support code +for them is currently treated as internal implementation detail. + +A precompile supplies three things: + +- `decode(args) -> Option` — *type-checks* a tag: which constructor is this, and what + structural shape does it carry? This inspects only `Tag::args()`; payload data is not available yet. + The returned shape drives registration and wire handling, but exact data/pair-list arity is + semantic and is checked during precompile evaluation: + - `NodeType::Data` declares a non-empty opaque data payload. + - `NodeType::Join` declares one payload block containing two child digests. + - `NodeType::PairList` declares a non-empty list of structural `lhs || rhs` digest pairs. + - `NodeType::True` is reserved for the framework TRUE sentinel; a precompile must not return it. +- `evaluate(args, payload, …) -> Result` — computes a node's **canonical form**. The + common roles are: validate a canonical value represented as data (its canonical is itself), + evaluate an operation (evaluate the child canonicals, then combine), or check a predicate + (evaluate operands, return the `TRUE` node on success or fail otherwise). These roles are + conventions, not a fixed taxonomy — a precompile is free to define multi-ary constructors and so + on over data, join, and pair-list payloads. +- `init() -> Vec` — contributes any canonical constant values (e.g. `ZERO`, `ONE`, a curve + generator) at registry-initialization time. + +Precompiles are collected in a **`PrecompileRegistry`**, the framework's dispatcher: it routes each +tag id to its owning precompile and is otherwise indifferent to how the precompile behaves. A +precompile's `id` is derived the same way event IDs are — the name hashed with Blake3 and folded +into a single field element — but in its own domain-separated namespace, so a precompile and an +event of the same name get different ids by construction. The registry rejects misconfigured or +duplicate ids at construction. The default registry is empty and rejects every precompile-owned +tag. A `DeferredState` carries the registry it evaluates under. The default registry is empty; +the public VM/prover/verifier path installs the standard `miden-precompiles` registry for bundled +proof-bound precompiles. + +During evaluation the framework hands the precompile a `DeferredContext`, through which it can +`get_node` for a registered digest, `evaluate_digest` a child digest to its canonical digest, or +`register` a freshly-minted helper node into the DAG. Registered helper nodes are validated under +the same registry and must satisfy the ordinary child-closure rules. The precompile never touches +the commitment directly — it supplies only per-node meaning, and the framework drives the +depth-first recursion. + +The in-memory `DeferredState` may memoize evaluation results internally. That memoization is +transparent to precompile implementations and is not serialized as trusted state. + +## Building the DAG from a program + +A program grows and evaluates the DAG through deferred system events. Each event mutates only the +*host-side* `DeferredState`; no register event hands a digest back through advice. Code that later +uses or logs that digest must derive it inside the VM from the same operand-stack payload or ordered +memory chunk sequence in a precompile-specific assembly procedure. + +| Event (`adv.*`) | Operand stack in | Effect | +| -------------------------- | -------------------------------- | ------ | +| `register_deferred` | `[PAYLOAD_LO, PAYLOAD_HI, TAG, …]` | Decodes `TAG` and registers an operand-stack node, then evaluates it immediately. `TAG` is one 4-felt word. `PAYLOAD_LO || PAYLOAD_HI` is exactly 8 felts: one data chunk, two 4-felt child digests for a join, or one `lhs_digest || rhs_digest` pair for a pair-list node. If the tag arguments define a different required data or pair-list arity, precompile evaluation rejects the node. Structural child digests may reference only already-registered children, except for the implicit `TRUE_DIGEST`. No advice/stack output; code that needs `NODE_DIGEST` computes it inside the VM with one `hperm` over `[PAYLOAD_LO, PAYLOAD_HI, TAG]`. | +| `register_deferred_data` | `[TAG, ptr, n_chunks, …]` | Decodes `TAG` and registers a memory-backed node, then evaluates it immediately. For data and pair-list tags, `n_chunks` determines the non-empty payload length; when tag arguments define an exact arity, precompile evaluation checks it. Pair-list chunks are interpreted as `lhs_digest || rhs_digest` pairs. Join tags require `n_chunks == 1` and interpret the single chunk as `lhs_digest || rhs_digest`; `TRUE` is rejected. No advice/stack output; code that needs `NODE_DIGEST` computes it inside the VM from the same `TAG` and ordered chunk sequence. | +| `evaluate_deferred` | `[NODE_DIGEST, …]` | Looks the node up, evaluates it to canonical form, and pushes the canonical tag plus canonical payload felts onto the **advice stack**. The tag is first in advice-pop order; for a single 8-felt payload, `adv_pushw adv_pushw adv_pushw` leaves `[PAYLOAD_LO, PAYLOAD_HI, TAG, …]` on the operand stack. `TRUE` emits only `Tag::TRUE`. | +| `evaluate_deferred_tag` | `[NODE_DIGEST, …]` | Looks the node up, evaluates it to canonical form, and pushes only the canonical tag onto the **advice stack**. `TRUE` emits `Tag::TRUE`. | +| `evaluate_deferred_payload` | `[NODE_DIGEST, …]` | Payload-only compatibility event. Looks the node up, evaluates it to canonical form, and pushes only the canonical payload felts onto the **advice stack**. For each 8-felt data chunk, advice is arranged as `HIGH` then `LOW` so `adv_pushw adv_pushw` leaves `LOW` on top and `HIGH` beneath it; chunks preserve canonical chunk order. Join payloads use the same two-word LIFO convention, leaving `lhs_digest` above `rhs_digest` after two `adv_pushw`s. `TRUE` emits no advice. | + +`register_*` validate the decoded shape, require non-empty data and pair lists, and check child +closure for structural payloads. Exact data or pair-list arity is enforced only when the tag's +precompile-specific semantics define one. Registration stores the original node under its digest, +evaluates it immediately, and fails immediately if semantic evaluation fails. + +### Why the digest is computed inside the VM + +A system event is a host hook. Its stack arguments are visible in the VM execution trace, but its +host-side state changes are not constrained by the AIR. In particular, a memory-backed register +event reads `n_chunks` chunks at `ptr` without adding AIR memory accesses that bind the registered +contents to those cells. A proof-relevant digest must therefore be derived with VM instructions: +`hperm` for a stack payload, or `mem_stream` plus `hperm` for the same tag and ordered memory chunk +sequence. + +This composes with the verifier: + +- the **VM-computed hash** binds the digest to the exact operand-stack values or memory reads + consumed by those instructions; +- once the deferred root is threaded into proof public inputs, the **deferred-root match** binds + that digest to the wire the verifier rehydrates; +- `DeferredState::from_wire` rehydrates the canonical wire opening and evaluates the expected root + from wire data. + +Once the root is public, these pieces bind the wire — and therefore every evaluation the verifier +re-checks — to data committed by the VM execution trace. + +### Why `evaluate_deferred` is a bare event + +A deferred-evaluation event delegates work the VM does not perform and returns the result through +advice, making it an **unbound host hint**. Using it soundly requires re-hashing the returned payload +with VM instructions (and, for the full event, checking the returned tag) and logging a predicate +that `from_wire` re-checks; a VM `eq`/`assert` over two raw advice results proves nothing about their +correctness. Because that obligation is precompile-specific (which predicate to log is the +precompile's business), deferred evaluation is intentionally *not* exposed as a generic safe `sys` +procedure. A precompile-specific assembly procedure must use VM instructions to relate the raw event +output to stack or memory values established independently of that advice. Registration has the +same binding obligation: the wrapper must compute the node digest from the exact stack payload or +ordered memory chunk sequence supplied to the event. A mismatch cannot support a proof-relevant +claim about the registered node. + +Predicates are **not** special-cased on evaluation: their canonical is the `TRUE` node like any +other successful predicate. `evaluate_deferred_payload` emits no advice for `TRUE` because `TRUE` +has no payload, while `evaluate_deferred_tag` and full `evaluate_deferred` emit `Tag::TRUE`. A +failed predicate has already surfaced as an error before any felts are pushed. + +## The deferred root commitment + +The deferred root commitment is a rolling AND-chain. `DeferredState.root` starts at the zero word +(`TRUE_DIGEST`), which is also the digest of the always-present canonical `Node::TRUE`. To fold a +verified +**statement** — any registered digest that evaluates to `TRUE`, not necessarily a primitive +predicate node — the framework registers an AND node +`{ tag: Tag::AND, payload: prev_root || stmt_digest }` and advances the root to that node's digest. +The append path first evaluates the statement under the installed registry and rejects missing or +non-`TRUE` statements. Wire verification does not replay append history; it opens the wire's +implicit root and evaluates that root directly. The digest is structural: even `AND(TRUE, TRUE)` hashes +under the distinct capacity `[1, 0, 0, 0]` and is not equal to `TRUE_DIGEST`, though it evaluates +semantically to `TRUE`. + +For wire-backed proofs, the deferred check collapses to a single fixed point: **rehydrate the +proof-carried wire, evaluate its implicit root to `TRUE`, and return that root for VM STARK +verification.** There is no separate finalization step. + +## Wire format and verification + +The partial/delegation witness format is `DeferredStateWire`, not the in-memory `DeferredState`. +`to_wire` lowers state to a passive, canonical, topologically ordered entry stream: + +- wire index `0` is the implicit `TRUE_DIGEST`; +- `entries[i]` has wire index `i + 1`; +- data entries carry literal data chunks; +- join entries encode both children by index; +- pair-list entries encode each pair's children by index; +- structural child indices may reference only `0` or earlier entries; +- empty `entries` opens `TRUE_DIGEST`; a non-empty wire opens the digest of the last entry. + +`to_wire` emits a deterministic child-first DFS of the root-reachable closure, so unreferenced +orphans are dropped. + +`DeferredState::from_wire(registry, wire, max_elements)` is the only trusted path from wire bytes +back to a validated state. It runs as a structural decode, a canonicality check, and a root +evaluation. This validates the wire's own implicit root; deferred-proof verification returns the +resulting `state.root()` as the trusted root for VM STARK verification. + +1. **structural** — seed index `0` as the implicit `TRUE_DIGEST`, reconstruct each explicit + entry (translating structural child indices back to digests), decode its tag, check that the entry + variant and payload shape match the declared `NodeType`, reject explicit `TRUE`, reject duplicate + digests, and require structural children to reference only earlier entries; +2. **canonicality** — register decoded entries into a fresh state, set the implicit wire root as + `state.root`, and require `state.to_wire() == wire`; this rejects dangling nodes, + non-root-last encodings, and equivalent-but-reordered topological wire; +3. **semantic** — evaluate the implicit wire root under the installed precompiles and require it to + equal the canonical `TRUE` node. Evaluation may insert canonical/helper nodes in addition to + the wire nodes. + +A wire that yields any integrity error is rejected; a faithful one reconstructs a state whose root is +the wire's implicit root and whose canonical wire output is byte-for-byte identical to the input +wire. + +## Status and scope + +This framework is now the proof-bound precompile substrate. In its current form: + +- the VM accumulates the DAG host-side and exposes the `DeferredState` on the execution output; +- `log_deferred` advances the deferred root by folding registered statements with `Tag::AND`; +- the final deferred root is threaded into the STARK public inputs; +- `ExecutionProof` carries a `DeferredProof` envelope: `Empty` resolves to `TRUE_DIGEST`, `Wire` + carries canonical `DeferredStateWire` for partial/delegable proofs, and `Stark` carries a + precompile VM STARK proof for the exact deferred root; +- verification resolves the trusted deferred root before VM STARK verification: final verification + accepts `Empty` or verified `Stark`, while explicit partial verification rehydrates `Wire` under + the built-in `miden_precompiles::registry()`; +- the `miden-precompiles` crate provides concrete hash, arithmetic, curve, and native signature + precompile implementations used by core-library facades and built-in verification. + +The proof format binds the final deferred root, not a registry name or version. For that reason, +execution, trace generation, proof generation, and verification all use the built-in +`miden_precompiles::registry()` policy. The public VM/prover/verifier APIs do not accept +caller-supplied precompile registries. Use `verify_with_max_deferred_elements(...)` when verifying +proofs produced with a non-default deferred-state budget. + +More generic DAG resource accounting remains a follow-up; the external STARK that verifies a +committed DAG, the **Precompile VM**, is described in GitHub discussion #3005. diff --git a/versioned_docs/version-0.16/reference/miden-vm/design/deferred/semantics.md b/versioned_docs/version-0.16/reference/miden-vm/design/deferred/semantics.md new file mode 100644 index 00000000..bc4d468c --- /dev/null +++ b/versioned_docs/version-0.16/reference/miden-vm/design/deferred/semantics.md @@ -0,0 +1,196 @@ +--- +title: "Deferred state semantics and API contract" +sidebar_position: 2 +--- + +# Deferred state semantics and API contract + +`DeferredState` is the host-side witness for deferred DAG verification and the deferred root +commitment. It is not serialized directly: partial proofs carry `DeferredStateWire`; final non-empty +proofs carry a precompile VM STARK proof for the exact deferred root. + +The simplified state model is: + +```rust +pub struct DeferredState { + registry: Arc, + nodes: BTreeMap, + root: Digest, + remaining_elements: usize, + // evaluation results may be memoized internally, but this is not part of the public contract +} +``` + +## Vocabulary + +- **Registered** means a digest has an entry in `DeferredState.nodes`. Registration can happen + through `DeferredState::register`, evaluation storing canonical/helper nodes, `log_statement` + storing framework `AND` nodes, or wire rehydration rebuilding entries. +- **Evaluated** means a registered input digest has been semantically reduced to a canonical node + under the installed `PrecompileRegistry`. The canonical node is also stored in `nodes` so it can + be referenced by downstream nodes. +- **Logged** or **root-reachable** means a registered digest contributes to `DeferredState.root`. + Only the root-reachable closure is serialized by `to_wire`; registered/evaluated orphans are + dropped. + +## Registered nodes + +`nodes` is the durable node store. + +- `TRUE_DIGEST` is always present and maps to `Node::TRUE`. +- `Node::TRUE` costs no budget. +- Every non-TRUE node is keyed by `node.digest()`. +- Structural nodes may reference only children already present in `nodes`, except for the implicit + `TRUE_DIGEST`: + - `Join` has two child digests. + - `PairList` has one or more pairs of child digests. +- Re-registering identical content is idempotent and free. +- Reusing an existing digest for different content is rejected as a conflicting node. + +Registration stores and shape-checks a node in `nodes`, evaluates it immediately, and stores the +canonical result. False predicates and other semantic evaluation failures are reported by +registration. + +## One remaining budget + +`DeferredState::new(registry, max_elements)` initializes one total budget: + +```text +remaining_elements = max_elements +``` + +Initialization also installs the registry's `init()` constants, charging them against that same +budget. `extend_precompiles(precompiles)` merges additional precompiles into an existing state +without discarding existing nodes, evaluation results, root, or budget accounting. + +Every new unique durable node inserted into `nodes` decrements `remaining_elements` by the node's +field-element footprint using checked subtraction. Duplicate insertion is free, so registering the +same data node at the exact budget limit succeeds. Evaluation results do not have a separate budget +and do not double-count canonical payloads; only canonical/helper nodes newly inserted into `nodes` +are charged. + +The precompile's `decode` result is the framework shape gate: + +- `NodeType::Data` authorizes a non-empty data payload. For memory-backed registration, the host + reads exactly the stack-supplied `n_chunks`; precompile evaluation checks any tag-derived semantic + data length. +- `NodeType::Join` authorizes exactly one 8-felt payload block, interpreted as two child digests. +- `NodeType::PairList` authorizes a non-empty list of `lhs_digest || rhs_digest` chunks. Precompile + evaluation checks any tag-derived semantic pair count. + +Processor handlers perform a cheap deferred-budget pre-check before allocating or reading a +memory-backed payload, but exact data/pair-list arity remains precompile-specific semantics. + +If insertion exhausts the remaining budget, execution aborts with a budget error. The insertion path +owns this accounting; processor deferred handlers do not perform post-mutation deferred budget +checks. + +## Evaluation + +Evaluation first requires the input digest to be present in `nodes`; evaluation state alone never +creates durable DAG membership. A call to `evaluate_digest(digest)` returns the digest of the +canonical node. This is a semantic operation: it may compute the result or use internal +memoization, but callers do not observe that distinction. Callers that need canonical node contents +can compose `evaluate_digest` with `get_node`. + +Framework nodes evaluate as follows: + +```text +Node::TRUE => Node::TRUE +Node::AND(lhs, rhs) => + require evaluate_digest(lhs) == TRUE_DIGEST + require evaluate_digest(rhs) == TRUE_DIGEST + Node::TRUE +``` + +Precompile-owned nodes are evaluated by `PrecompileRegistry::evaluate`, which dispatches to the +owning `Precompile` with a `DeferredContext`. + +`DeferredContext` gives precompile implementations the same semantic split: + +- `get_node(digest)` queries the registered/original node by digest without evaluating it. +- `evaluate_digest(digest)` evaluates a registered child digest to its canonical digest. +- `evaluate_digest_pair(lhs, rhs)` evaluates two registered child digests to canonical digests. +- `ensure_equal(lhs, rhs)` evaluates two children and requires their canonical digests to match. +- `register(node)` inserts a freshly minted helper node and returns its original digest. + +## Root and wire + +`root` starts at `TRUE_DIGEST`. `log_statement(stmt_digest)` evaluates the current root and +statement, requires both to evaluate to `Node::TRUE`, then appends one framework `AND` node: + +```text +next_root = digest(Node::and(previous_root, stmt_digest)) +``` + +`to_wire` serializes only the root-reachable closure in canonical child-first order: + +- data entries carry literal data chunks; +- join entries emit two child indices; +- pair-list entries emit pairs of child indices. + +The wire root is implicit: empty wire opens `TRUE_DIGEST`, otherwise the root is the digest of the +final entry. `from_wire(registry, wire, max_elements)` decodes untrusted wire, rejects non-canonical +or dangling wire by requiring `state.to_wire() == wire`, then evaluates the implicit wire root to +`Node::TRUE`. Evaluation may insert canonical/helper nodes in addition to the wire nodes. + +## Deferred proof root resolution + +`DeferredProof` carries the material needed to bind VM proof verification to a deferred root: + +- `Empty` is final and resolves to `TRUE_DIGEST`. +- `Wire` is partial/delegable material backed by canonical `DeferredStateWire`. +- `Stark` is final: verification checks the precompile STARK proof against its embedded public root, + then returns that root. + +The public final verifier accepts only final deferred proof forms. It rejects `Wire`; wire-backed +partial proofs are handled by the explicit `Verifier::verify_partial` path. Partial verification +rehydrates the wire into a `DeferredState` with the standard precompile registry, verifies the VM +STARK against the hydrated state's root, and returns the hydrated state. It rejects `Empty` and +`Stark` because neither carries wire material to hydrate. + +The VM STARK public inputs are built with the resolved final root or hydrated partial root. +High-level `miden-prover` proving APIs and the `miden-vm` proving facade produce final proofs by +default: `Empty` when no precompile claims were logged, or `Stark` for non-empty deferred roots. The +`miden-vm` facade exposes only final proving; callers that intentionally delegate or batch precompile +proving use the explicit `miden-prover::prove_partial*` APIs to preserve `Wire` proof material, then +`verify_partial` to validate and rehydrate that material before producing a final deferred proof. + +The current STARK-backed variant is exact-root only; batch-backed proofs need a separate proof form. + +## Low-level framework API + +The preferred low-level `miden_core::deferred::DeferredState` surface is small. These APIs are +framework APIs, not the public proof-verifier policy surface: + +- `DeferredState::new(registry, max_elements)` for a state booted with precompile constants +- `extend_precompiles(precompiles)` for additive setup +- `registry()` +- `root()` +- `remaining_elements()` +- `get_node(digest)` and `nodes() -> &BTreeMap` for registered-node inspection +- `decode(tag)` for structural tag decoding +- `register(node)` for inserting concrete node content +- `evaluate_digest(digest)` for the canonical digest +- `log_statement(stmt_digest)` +- `to_wire()` +- `from_wire(registry, wire, max_elements)` + +Callers that have a concrete node should explicitly `register` it; they may call +`evaluate_digest` on the returned digest when they need the canonical result, and then `get_node` if +they need canonical node contents. Raw evaluation memoization and direct root mutation are not part +of the public contract. + +## Scope note + +Deferred state is the proof-bound precompile witness model. VM execution accumulates a deferred +root for logged precompile claims; `DeferredProof` explains why that root should be accepted. +Final public verification accepts only final proof forms, resolving `Empty` or a verified `Stark` +root before VM STARK verification. Wire-backed material is partial/delegable: +`Verifier::verify_partial` rehydrates `Wire` under the built-in `miden_precompiles::registry()`, +verifies the VM STARK against the hydrated root, and returns the hydrated state. + +The lower-level `DeferredState` APIs remain parameterized by `PrecompileRegistry` for framework +construction, tests, and non-verifier wire validation. The default registry is empty, but the public +VM/prover/verifier path installs the standard `miden_precompiles::registry()` policy and does not +accept caller-supplied precompile registries. diff --git a/versioned_docs/version-0.16/reference/miden-vm/design/index.md b/versioned_docs/version-0.16/reference/miden-vm/design/index.md new file mode 100644 index 00000000..f2745bcf --- /dev/null +++ b/versioned_docs/version-0.16/reference/miden-vm/design/index.md @@ -0,0 +1,57 @@ +--- +title: "Design" +sidebar_position: 1 +--- + +# Design +In the following sections, we provide in-depth descriptions of Miden VM internals, including all AIR constraints for the proving system. We also provide rationale for making specific design choices. + +Throughout these sections we adopt the following notations and assumptions: +* All arithmetic operations, unless noted otherwise, are assumed to be in a prime field with modulus $p = 2^{64} - 2^{32} + 1$. +* A _binary_ value means a field element which is either $0$ or $1$. +* We use lowercase letters to refer to individual field elements (e.g., $a$), and uppercase letters to refer to groups of $4$ elements, also referred to as words (e.g., $A$). To refer to individual elements within a word, we use numerical subscripts. For example, $a_0$ is the first element of word $A$, $b_3$ is the last element of word $B$, etc. +* When describing AIR constraints: + - For a column $x$, we denote the value in the current row simply as $x$, and the value in the next row of the column as $x'$. Thus, all transition constraints for Miden VM work with two consecutive rows of the execution trace. + - For multiset equality constraints, we denote random values sent by the verifier after the prover commits to the main execution trace as $\alpha_0, \alpha_1, \alpha_2$ etc. + - To differentiate constraints from other formulas, we frequently use the following format for constraint equations. + +$$ +x' - (x + y) = 0 \text{ | degree} = 1 +$$ + +In the above, the constraint equation is followed by the implied algebraic degree of the constraint. This degree is determined by the number of multiplications between trace columns. If a constraint does not involve any multiplications between columns, its degree is $1$. If a constraint involves multiplication between two columns, its degree is $2$. If we need to multiply three columns together, the degree is $3$ ect. + +The maximum allowed constraint degree in Miden VM is $9$. If a constraint degree grows beyond that, we frequently need to introduce additional columns to reduce the degree. + +## VM components +Miden VM consists of several interconnected components, each providing a specific set of functionality. These components are: + +* **System**, which is responsible for managing system data, including the current VM cycle (`clk`), and the current and parent execution contexts. +* **Program decoder**, which is responsible for computing a commitment to the executing program and converting the program into a sequence of operations executed by the VM. +* **Operand stack**, which is a push-down stack which provides operands for all operations executed by the VM. +* **Range checker**, which is responsible for providing 16-bit range checks needed by other components. +* **Chiplets**, which is a set of specialized circuits used to accelerate commonly-used complex computations. Currently, the VM relies on 5 chiplets: + - Hash chiplet, used to compute Poseidon2 hashes both for sequential hashing and for Merkle tree hashing. + - Bitwise chiplet, used to compute bitwise operations (e.g., `AND`, `XOR`) over 32-bit integers. + - Memory chiplet, used to support random-access memory in the VM. + - ACE chiplet, used to evaluate arithmetic circuits. + - Kernel ROM chiplet, used to enable calling predefined kernel procedures which are provided before execution begins. + +The above components are connected via **buses**, which are implemented using [lookup arguments](./lookups/index.md). We also use [multiset check lookups](./lookups/multiset.md) internally within components to describe **virtual tables**. + +## VM execution trace +The execution trace of Miden VM consists of $72$ main trace columns and $8$ auxiliary columns (running products / LogUp accumulators), as shown in the diagram below. + +![vm_trace.png](../img/design/vm_trace.png) + +As can be seen from the above, the system, decoder, stack, and range checker components use dedicated sets of columns, while all chiplets share the same $18$ columns. To differentiate between chiplets, we use a set of binary selector columns, a combination of which uniquely identifies each chiplet. + +The system component does not yet have a dedicated documentation section, since the design is likely to change. However, the following column is not expected to change: + +* `clk` which is used to keep track of the current VM cycle. Values in this column start out at $0$ and are incremented by $1$ with each cycle. + +For the `clk` column, the constraints are straightforward: + +$$ +clk' - (clk + 1) = 0 \text{ | degree} = 1 +$$ diff --git a/versioned_docs/version-0.16/reference/miden-vm/design/lookups/_category_.yml b/versioned_docs/version-0.16/reference/miden-vm/design/lookups/_category_.yml new file mode 100644 index 00000000..d76cc7b5 --- /dev/null +++ b/versioned_docs/version-0.16/reference/miden-vm/design/lookups/_category_.yml @@ -0,0 +1,4 @@ +label: "Lookup arguments" +# Determines where this documentation section appears relative to other sections in the parent folder +position: 6 +collapsed: true diff --git a/versioned_docs/version-0.16/reference/miden-vm/design/lookups/index.md b/versioned_docs/version-0.16/reference/miden-vm/design/lookups/index.md new file mode 100644 index 00000000..0c81861e --- /dev/null +++ b/versioned_docs/version-0.16/reference/miden-vm/design/lookups/index.md @@ -0,0 +1,62 @@ +--- +title: "Lookup Arguments in Miden VM" +sidebar_position: 1 +--- + +# Lookup arguments in Miden VM + +Zero knowledge virtual machines frequently make use of lookup arguments to enable performance optimizations. Miden VM uses two types of arguments: multiset checks and a multivariate lookup based on logarithmic derivatives known as LogUp. A brief introduction to multiset checks can be found [here](./multiset.md). The description of LogUp can be found [here](https://eprint.iacr.org/2022/1530.pdf). + +In Miden VM, lookup arguments are used for two purposes: + +1. To prove the consistency of intermediate values that must persist between different cycles of the trace without storing the full data in the execution trace (which would require adding more columns to the trace). +2. To prove correct interaction between two independent sections of the execution trace, e.g., between the main trace where the result of some operation is required, but would be expensive to compute, and a specialized component which can perform that operation cheaply. + +The first is achieved using [virtual tables](#virtual-tables-in-miden-vm) of data, where we add a row at some cycle in the trace and remove it at a later cycle when it is needed again. Instead of maintaining the entire table in the execution trace, multiset checks allow us to prove data consistency of this table using one running product column. + +The second is done by reducing each operation to a lookup value and then using a [communication bus](#communication-buses-in-miden-vm) to provably connect the two sections of the trace. These communication buses can be implemented either via [multiset checks](./multiset.md#communication-buses) or via the [LogUp argument](./logup.md). + + +## Virtual tables in Miden VM + +Miden VM makes use of 6 virtual tables across 4 components, all of which are implemented via [multiset checks](./multiset.md#virtual-tables): + +- Stack: + - [Overflow table](../stack/index.md#overflow-table) +- Decoder: + - [Block stack table](../decoder/index.md#block-stack-table) + - [Block hash table](../decoder/index.md#block-hash-table) + - [Op group table](../decoder/index.md#op-group-table) +- Chiplets: + - [Chiplets virtual table](../chiplets/index.md#chiplets-virtual-table), which combines the following two tables into one: + - [Hash chiplet sibling table](../chiplets/hasher.md#sibling-table-constraints) + - [Kernel ROM chiplet procedure table](../chiplets/kernel_rom.md#constraints) + +## Communication buses in Miden VM + +One strategy for improving the efficiency of a zero knowledge virtual machine is to use specialized components for complex operations and have the main circuit “offload” those operations to the corresponding components by specifying inputs and outputs and allowing the proof of execution to be done by the dedicated component instead of by the main circuit. + +These specialized components are designed to prove the internal correctness of the execution of the operations they support. However, in isolation they cannot make any guarantees about the source of the input data or the destination of the output data. + +In order to prove that the inputs and outputs specified by the main circuit match the inputs and outputs provably executed in the specialized component, some kind of provable communication bus is needed. + +This bus is typically implemented as some kind of lookup argument, and in Miden VM in particular we use multiset checks or LogUp. + +Miden VM uses multiple communication buses: + +- The chiplets bus [$b_{chip}$](../chiplets/index.md#chiplets-bus), which communicates with all of the chiplets (Hash, Bitwise, Memory, ACE, and Kernel ROM). It is implemented using multiset checks. +- The hash/kernel bus [$b_{hash\_kernel}$](../chiplets/index.md#chiplets-bus), which combines several multiset-backed channels (hash sibling table, kernel ROM procedure table, ACE memory requests, and deferred-root updates). +- The ACE wiring bus [$v_{wiring}$](../chiplets/ace.md#circuit-evaluation), which ties together ACE node wiring. It is implemented using LogUp. +- The range checker bus [$b_{range}$](../range.md#communication-bus), which facilitates requests between the [stack](../stack/u32_ops.md) and [memory](../chiplets/memory.md) components and the [range checker](../range.md). It is implemented using LogUp. + + +## Length of auxiliary columns for lookup arguments + +The auxiliary columns used for buses and virtual tables are computed by including information from the *current* row of the main execution trace into the *next* row of the auxiliary trace column. Thus, in order to ensure that the trace is long enough to give the auxiliary column space for its final value, a padding row may be required at the end of the trace of the component upon which the auxiliary column depends. + +This is true when the data in the main trace could go all the way to the end of the trace, such as in the case of the range checker. + +## Cost of auxiliary columns for lookup arguments +It is important to note that depending on the field in which we operate, an auxiliary column implementing a lookup argument may actually require more than one trace column. This is specifically true for small fields. + +Since Miden uses a 64-bit field, each auxiliary column needs to be represented by $2$ columns to achieve ~100-bit security and by $3$ columns to achieve ~128-bit security. diff --git a/versioned_docs/version-0.16/reference/miden-vm/design/lookups/logup.md b/versioned_docs/version-0.16/reference/miden-vm/design/lookups/logup.md new file mode 100644 index 00000000..f3b4d73d --- /dev/null +++ b/versioned_docs/version-0.16/reference/miden-vm/design/lookups/logup.md @@ -0,0 +1,70 @@ +--- +title: "LogUp: Multivariate Lookups with Logarithmic Derivatives" +sidebar_position: 3 +--- + +# LogUp: multivariate lookups with logarithmic derivatives + +The description of LogUp can be found [here](https://eprint.iacr.org/2022/1530.pdf). In MidenVM, LogUp is used to implement efficient [communication buses](./index.md#communication-buses-in-miden-vm). + +Using the LogUp construction instead of a simple [multiset check](./multiset.md) with running products reduces the computational effort for the prover and the verifier. Given two columns $a$ and $b$ in the main trace where $a$ contains duplicates and $b$ does not (i.e. $b$ is part of the lookup table), LogUp allows us to compute two logarithmic derivatives and check their equality. + +$$ +\sum_{i=0}^{l} \frac{1}{(\alpha - a_i)} = \sum_{i=0}^{n} \frac{m_i}{(\alpha - b_i)} +$$ + +In the above: +- $l$ is the number of values in $a$, which must be smaller than the size of the field. (The prime field used for Miden VM has modulus $p = 2^{64} - 2^{32} + 1$, so $l < p$ must be true.) +- $n$ is the number of values in $b$, which must be smaller than the size of the field. ($n < p$, for Miden VM) +- $m_i$ is the multiplicity of $b_i$, which is expected to match the number of times the value $b_i$ is duplicated in column $a$. It must be smaller than the size of the set of lookup values. ($m_i < n$) +- $\alpha$ is a random value that is sent to the prover by the verifier after the prover commits to the execution trace of the program. + +Thus, instead of needing to compute running products, we are able to assert correct lookups by computing running sums. + +## Usage in Miden VM + +The generalized trace columns and constraints for this construction are as follows, where component $X$ is some component in the trace and lookup table $T$ contains the values $v$ which need to be looked up from $X$ and how many times they are looked up (the multiplicity $m$). + +![logup_component_x](../../img/design/lookups/logup_component.png) + +![logup_table_t](../../img/design/lookups/logup_table.png) + +### Constraints + +The diagrams above show running sum columns for computing the logarithmic derivatives for both $X$ and $T$. As an optimization, we can combine these values into a single auxiliary column in the extension field that contains the running sum of values from both logarithmic derivatives. We'll refer to this column as a _communication bus_ $b$, since it communicates the lookup request from the component $X$ to the lookup table $T$. + +This can be expressed as follows: + +> $$ +> b' = b + \frac{m}{(\alpha - v)} - \frac{1}{(\alpha - x)} +> $$ + +Since constraints must be expressed without division, the actual constraint which is enforced will be the following: + +> $$ +> b' \cdot (\alpha - v) \cdot (\alpha - x) = b \cdot (\alpha - x) \cdot (\alpha - v) + m \cdot (\alpha - x) - (\alpha - v) \text{ | degree} = 3 +> $$ + +In general, we will write constraints within these docs using the previous form, since it's clearer and more readable. + +Additionally, boundary constraints must be enforced against $b$ to ensure that its initial and final values are equal (in Miden VM we commonly use $0$ or $1$ depending on the bus). This enforces that the logarithmic derivatives for $X$ and $T$ were equal. + +### Extending the construction to multiple components + +The functionality of the bus can easily be extended to receive lookup requests from multiple components. For example, to additionally support requests from column $y$, the bus constraint would be modified to the following: + +> $$ +> b' = b + \frac{m}{(\alpha - v)} - \frac{1}{(\alpha - x)} - \frac{1}{(\alpha - y)} \text{ | degree} = 4 +> $$ + +Since the maximum constraint degree in Miden VM is 9, the lookup table $T$ could accommodate requests from at most 7 trace columns in the same trace row via this construction. + +### Extending the construction with flags + +Boolean flags can also be used to determine when requests from various components are sent to the bus. For example, let $f_x$ be 1 when a request should be sent from $x$ and 0 otherwise, and let $f_y$ be similarly defined for column $y$. We can use the following constraint to turn requests on or off: + +> $$ +> b' = b + \frac{m}{(\alpha - v)} - \frac{f_x}{(\alpha - x)} - \frac{f_y}{(\alpha - y)} \text{ | degree} = 4 +> $$ + +If any of these flags have degree greater than 2 then this will increase the overall degree of the constraint and reduce the number of lookup requests that can be accommodated by the bus per row. diff --git a/versioned_docs/version-0.16/reference/miden-vm/design/lookups/multiset.md b/versioned_docs/version-0.16/reference/miden-vm/design/lookups/multiset.md new file mode 100644 index 00000000..805649a0 --- /dev/null +++ b/versioned_docs/version-0.16/reference/miden-vm/design/lookups/multiset.md @@ -0,0 +1,111 @@ +--- +title: "Multiset Checks" +sidebar_position: 2 +--- + +# Multiset checks + +A brief introduction to multiset checks can be found [here](https://hackmd.io/@relgabizon/ByFgSDA7D). In Miden VM, multiset checks are used to implement [virtual tables](#virtual-tables) and efficient [communication buses](./index.md#communication-buses-in-miden-vm). + +## Running product columns +Although the multiset equality check can be thought of as comparing multiset equality between two vectors $a$ and $b$, in Miden VM it is implemented as a single running product column in the following way: + +- The running product column is initialized to a value $x$ at the beginning of the trace. (We typically use $x = 1$.) +- All values of $a$ are multiplied into the running product column. +- All values of $b$ are divided out of the running product column. +- If $a$ and $b$ were multiset equal, then the running product column will equal $x$ at the end of the trace. + +Running product columns are computed using a set of random values $\alpha_0$, $\alpha_1, ...$ sent to the prover by the verifier after the prover commits to the execution trace of the program. + +## Virtual tables + +Virtual tables can be used to store intermediate data which is computed at one cycle and used at a different cycle. When the data is computed, the row is added to the table, and when it is used later, the row is deleted from the table. Thus, all that needs to be proved is the data consistency between the row that was added and the row that was deleted. + +The consistency of a virtual table can be proved with a single trace column $p$, which keeps a running product of rows that were inserted into and deleted from the table. This is done by reducing each row to a single value, multiplying the value into $p$ when the row is inserted, and dividing the value out of $p$ when the row is removed. Thus, at any step of the computation, $p$​ will contain a product of all rows currently in the table. + +The initial value of $p$​ is set to 1. Thus, if the table is empty by the time Miden VM finishes executing a program (we added and then removed exactly the same set of rows), the final value of $p$​ will also be equal to 1. The initial and final values are enforced via boundary constraints. + +### Computing a virtual table's trace column + +To compute a product of rows, we'll first need to reduce each row to a single value. This can be done as follows. + +Let $t_0, t_1, t_2, ...$ be columns in the virtual table, and assume the verifier sends a set of random values $\alpha_0$, $\alpha_1, ...$ to the prover after the prover commits to the execution trace of the program. + +The prover reduces row $i$ in the table to a single value $r_i$ as: + +$$ +r_i = \alpha_0 + \alpha_1 \cdot t_{0, i} + \alpha_2 \cdot t_{1, i} + \alpha_3 \cdot t_{2, i} + ... +$$ + +Then, when row $i$ is added to the table, we'll update the value in the $p$ column like so: + +$$ +p' = p \cdot r_i +$$ + +Analogously, when row $i$ is removed from the table, we'll update the value in column $p$ like so: + +$$ +p' = \frac{p}{r_i} +$$ + +### Virtual tables in Miden VM + +Miden VM makes use of 6 virtual tables across 4 components: + +- Stack: + - [Overflow table](../stack/index.md#overflow-table) +- Decoder: + - [Block stack table](../decoder/index.md#block-stack-table) + - [Block hash table](../decoder/index.md#block-hash-table) + - [Op group table](../decoder/index.md#op-group-table) +- Chiplets: + - [Chiplets virtual table](../chiplets/index.md#chiplets-virtual-table), which combines the following two tables into one: + - [Hash chiplet sibling table](../chiplets/hasher.md#sibling-table-constraints) + - [Kernel ROM chiplet procedure table](../chiplets/kernel_rom.md#constraints) + +## Communication buses {#communication-buses} + +A `bus` can be implemented as a single trace column $b$ where a request can be sent to a specific component and a corresponding response will be sent back by that component. + +The values in this column contain a running product of the communication with the component as follows: + +- Each request is “sent” by computing a lookup value from some information that's specific to the specialized component, the operation inputs, and the operation outputs, and then dividing it out of the running product column $b$. +- Each chiplet response is “sent” by computing the same lookup value from the component-specific information, inputs, and outputs, and then multiplying it into the running product column $b$. + +Thus, if the requests and responses match, and the bus column $b$ is initialized to $1$, then $b$ will start and end with the value $1$. This condition is enforced by boundary constraints on column $b$. + +Note that the order of the requests and responses does not matter, as long as they are all included in $b$. In fact, requests and responses for the same operation will generally occur at different cycles. Additionally, there could be multiple requests sent in the same cycle, and there could also be a response provided at the same cycle that a request is received. + +### Communication bus constraints + +These constraints can be expressed in a general way with the 2 following requirements: + +- The lookup value must be computed using random values $\alpha_0, \alpha_1$, etc. that are provided by the verifier after the prover has committed to the main execution trace. +- The lookup value must include all uniquely identifying information for the component/operation and its inputs and outputs. + +Given an example operation $op_{ex}$ with inputs $i_0, ..., i_n$ and outputs $o_0, ..., o_m$, the lookup value can be computed as follows: + +$$lookup = \alpha_0 + \alpha_1 \cdot op_{ex} + \alpha_2 \cdot i_0 + ... + \alpha_{n+2} \cdot i_n + \alpha_{n+3} \cdot o_0 + ... + \alpha_{n + 2 + m} \cdot o_m$$ + +The constraint for sending this to the bus as a request would be: + +$$b' \cdot lookup = b$$ + +The constraint for sending this to the bus as a response would be: + +$$b' = b \cdot lookup$$ + +However, these constraints must be combined, since it's possible that requests and responses both occur during the same cycle. + +To combine them, let $u_{lookup}$ be the request value and let $v_{lookup}$ be the response value. These values are both computed the same way as shown above, but the data sources are different, since the input/output values used to compute $u_{lookup}$ come from the trace of the component that's "offloading" the computation, while the input/output values used to compute $v_{lookup}$ come from the trace of the specialized component. + +The final constraint can be expressed as: + +$$b' \cdot u_{lookup} = b \cdot v_{lookup}$$ + +### Communication buses in Miden VM + +In Miden VM, the specialized components are implemented as dedicated segments of the execution trace, which include the 3 chiplets in the Chiplets module (the hash chiplet, bitwise chiplet, and memory chiplet). + +Miden VM currently uses multiset checks to implement the chiplets bus [$b_{chip}$](../chiplets/index.md#chiplets-bus), which communicates with all of the chiplets (Hash, Bitwise, Memory, ACE, and Kernel ROM). diff --git a/versioned_docs/version-0.16/reference/miden-vm/design/programs.md b/versioned_docs/version-0.16/reference/miden-vm/design/programs.md new file mode 100644 index 00000000..f2be9718 --- /dev/null +++ b/versioned_docs/version-0.16/reference/miden-vm/design/programs.md @@ -0,0 +1,149 @@ +--- +title: "Programs in Miden VM" +sidebar_position: 2 +--- + +# Programs in Miden VM +Miden VM consumes programs in a form of a Merkleized Abstract Syntax Tree (MAST). This tree is a binary tree where each node is a *code block*. The VM starts execution at the root of the tree, and attempts to recursively execute each required block according to its semantics. If the execution of a code block fails, the VM halts at that point and no further blocks are executed. A set of currently available blocks and their execution semantics are described below. + +## Code blocks + +### Join block +A **join** block is used to describe sequential execution. When the VM encounters a *join* block, it executes its left child first, and then executes its right child. + +![join_block](../img/design/programs/join_block.png) + +A *join* block must always have two children, and thus, cannot be a leaf node in the tree. + +### Split block +A **split** block is used to describe conditional execution. When the VM encounters a *split* block, it checks the top of the stack. If the top of the stack is $1$, it executes the left child, if the top of the stack is $0$, it executes the right child. If the top of the stack is neither $0$ nor $1$, the execution fails. + +![split_block](../img/design/programs/split_block.png) + +A *split* block must always have two children, and thus, cannot be a leaf node in the tree. + +### Loop block +A **loop** block is used to describe condition-based iterative execution with do-while semantics: +when the VM encounters a *loop* block, the loop body is executed unconditionally for the first +iteration. After the body has finished, the VM checks the top of the stack: if it is $1$, the body +is executed again; if it is $0$, the loop is exited. If the top of the stack is neither $0$ nor +$1$, the execution fails. + +Source-level constructs that need an entry-time check (`while.true` in Miden assembly) are compiled +to a *split* block wrapping the loop, so the body is skipped entirely when the initial condition +is false. + +![loop_block](../img/design/programs/loop_block.png) + +A *loop* block must always have one child, and thus, cannot be a leaf node in the tree. + +### Dyn block +A **dyn** block is used to describe a node whose target is specified dynamically via the stack. When the VM encounters a *dyn* block, it executes a program which hashes to the target specified by the top of the stack. Thus, it has a dynamic target rather than a hardcoded target. In order to execute a *dyn* block, the VM must be aware of a program with the hash value that is specified by the top of the stack. Otherwise, the execution fails. + +![dyn_block](../img/design/programs/dyn_block.png) + +A *dyn* block must always have one (dynamically-specified) child. Thus, it cannot be a leaf node in the tree. + +### Dyncall block + +A **dyncall** block is used to describe a function call whose target is specified dynamically via the stack. It behaves like a dynamic target combined with a call: the target is not hardcoded, and execution enters a new user context before returning to the caller. + +### Call block + +A **call** block is used to describe a function call which is executed in a [user context](../user_docs/assembly/execution_contexts.md). When the VM encounters a *call* block, it creates a new user context, then executes a program which hashes to the target specified by the *call* block in the new context. Thus, in order to execute a *call* block, the VM must be aware of a program with the specified hash. Otherwise, the execution fails. At the end of the *call* block, execution returns to the previous context. + + +When executing a *call* block, the VM does the following: +1. Checks if a *syscall* is already being executed and fails if so. +2. Sets the depth of the stack to 16. +3. Upon return, checks that the depth of the stack is 16. If so, the original stack depth is restored. Otherwise, an error occurs. + +![call_block](../img/design/programs/call_block.png) + +A *call* block does not have any children. Thus, it must be leaf node in the tree. + +### Syscall block + +A **syscall** block is used to describe a function call which is executed in the [root context](../user_docs/assembly/execution_contexts.md). When the VM encounters a *syscall* block, it returns to the root context, then executes a program which hashes to the target specified by the *syscall* block. Thus, in order to execute a *syscall* block, the VM must be aware of a program with the specified hash, and that program must belong to the kernel against which the code is compiled. Otherwise, the execution fails. At the end of the *syscall* block, execution returns to the previous context. + +When executing a *syscall* block, the VM does the following: +1. Checks if a *syscall* is already being executed and fails if so. +2. Sets the depth of the stack to 16. +3. Upon return, checks that the depth of the stack is 16. If so, the original stack depth is restored. Otherwise, an error occurs. + +![syscall_block](../img/design/programs/syscall_block.png) + +A *syscall* block does not have any children. Thus, it must be leaf node in the tree. + +### Basic block +A **basic** block is used to describe a linear sequence of operations. When the VM encounters a *basic* block, it breaks the sequence of operations into batches and groups according to the following rules: +* A group is represented by a single field element. Thus, assuming a single operation can be encoded using 7 bits, and assuming we are using a 64-bit field, a single group may encode up to 9 operations or a single immediate value. +* A batch is a set of groups which can be absorbed by a hash function used by the VM in a single permutation. For example, assuming the hash function can absorb up to 8 field elements in a single permutation, a single batch may contain up to 8 groups. +* There is no limit on the number of batches contained within a single basic block. + +Thus, for example, executing 8 pushes in a row will result in two operation batches as illustrated in the picture below: + +![span_block_creation](../img/design/programs/span_block_creation.png) + +* The first batch will contain 8 groups, with the first group containing 7 `PUSH` opcodes and 1 `NOOP`, and the remaining 7 groups containing immediate values for each of the push operations. The reason for the `NOOP` is explained later in this section. +* The second batch will contain 2 groups, with the first group containing 1 `PUSH` opcode and 1 `NOOP`, and the second group containing the immediate value for the last push operation. + + +If a sequence of operations does not have any operations which carry immediate values, up to 72 operations can fit into a single batch. + +From the user's perspective, all operations are executed in order, however, the VM may insert occasional `NOOP`s to ensure proper alignment of all operations in the sequence. Currently, the alignment requirements are as follows: +* An operation carrying an immediate value cannot be the last operation in a group. Thus, for example, if a `PUSH` operation is the last operation in a group, the VM will insert a `NOOP` after it. + +A *basic* block does not have any children, and thus, must be leaf node in the tree. + +## Program example +Consider the following program, where $a_0, ..., a_i$, $b_0, ..., b_j$ etc. represent individual operations: + +``` +a_0, ..., a_i +if.true + b_0, ..., b_j +else + c_0, ..., c_k + while.true + d_0, ..., d_n + end + e_0, ..., e_m +end +f_0, ..., f_l +``` + +A MAST for this program would look as follows: + +![mast_of_program](../img/design/programs/mast_of_program.png) + +Execution of this program would proceed as follows: + +1. The VM will start execution at the root of the program which is block $B_5$. +2. Since, $B_5$ is a *join block*, the VM will attempt to execute block $B_4$ first, and only after that execute block $f$. +3. Block $B_4$ is also a *join block*, and thus, the VM will execute block $a$ by executing operations $a_0, ..., a_i$ in sequence, and then execute block $B_3$. +4. Block $B_3$ is a *split block*, and thus, the VM will pop the value off the top of the stack. If the popped value is $1$, operations from block $b$ will be executed in sequence. If the popped value is $0$, then the VM will attempt to execute block $B_2$. +5. $B_2$ is a *join block*, thus, the VM will try to execute block $B_1$ first, and then execute operations from block $e$. +6. Block $B_1$ is also a *join_block*, and thus, the VM will first execute all operations in block $c$, and then will attempt to execute block $B_0$. +7. Block $B_0$ corresponds to a `while.true`. The assembler desugars this into a *split* whose + true branch is the do-while *loop* containing block $d$, and whose false branch is a no-op. The + VM pops the value off the top of the stack: if the popped value is $1$, the VM enters the loop + and executes block $d$; if the popped value is $0$, the VM takes the false branch and skips + $d$, then moves up the tree executing first block $e$, then $f$. +8. If the VM does enter the loop, then after operation $d_n$ is executed, the VM will pop the value off the top of the stack again. If the popped value is $1$, the VM will execute block $d$ again, and again until the top of the stack becomes $0$. Once the top of the stack becomes $0$, the VM will exit the loop and will move up the tree executing first block $e$, then $f$. + +## Program hash computation +Every Miden VM program can be reduced to a unique hash value. Specifically, it is infeasible to find two Miden VM programs with distinct semantics which hash to the same value. Padding a program with `NOOP`s does not change a program's execution semantics, and thus, programs which differ only in the number and/or placement of `NOOP`s may hash to the same value, although in most cases padding with `NOOP` should not affect program hash. + +To prevent program hash collisions we implement domain separation across the variants of control blocks. We define the domain value to be the opcode of the operation that initializes the control block. + +Below we denote $hash$ to be an arithmetization-friendly hash function with $4$-element output and capable of absorbing $8$ elements in a single permutation. The hash domain is specified as the subscript of the hash function and its value is used to populate the second capacity register upon initialization of control block hashing - $hash_{domain}(a, b)$. + +* The hash of a **join** block is computed as $hash_{join}(a, b)$, where $a$ and $b$ are hashes of the code block being joined. +* The hash of a **split** block is computed as $hash_{split}(a, b)$, where $a$ is a hash of a code block corresponding to the *true* branch of execution, and $b$ is a hash of a code block corresponding to the *false branch* of execution. +* The hash of a **loop** block is computed as $hash_{loop}(a, 0)$, where $a$ is a hash of a code block corresponding to the loop body. +* The hash of a **dyn** block is set to a constant, so it is the same for all *dyn* blocks. It does not depend on the hash of the dynamic child. This constant is computed as the Poseidon2 hash of two empty words (`[ZERO, ZERO, ZERO, ZERO]`) using a domain value of `DYN_DOMAIN`, where `DYN_DOMAIN` is the op code of the `Dyn` operation. +* The hash of a **call** block is computed as $hash_{call}(a, 0)$, where $a$ is a hash of a program of which the VM is aware. +* The hash of a **syscall** block is computed as $hash_{syscall}(a, 0)$, where $a$ is a hash of a program belonging to the kernel against which the code was compiled. +* The hash of a **basic** block is computed as $hash(a_1, ..., a_k)$, where $a_i$ is the $i$th batch of operations in the *basic* block. Each batch of operations is defined as containing $8$ field elements, and thus, hashing a $k$-batch *basic* block requires $k$ absorption steps. + * In cases when the number of operations is insufficient to fill the last batch entirely, `NOOPs` are appended to the end of the last batch to ensure that the number of operations in the batch is always equal to $8$. diff --git a/versioned_docs/version-0.16/reference/miden-vm/design/range.md b/versioned_docs/version-0.16/reference/miden-vm/design/range.md new file mode 100644 index 00000000..982caa8d --- /dev/null +++ b/versioned_docs/version-0.16/reference/miden-vm/design/range.md @@ -0,0 +1,177 @@ +--- +title: "Range Checker" +sidebar_position: 4 +--- + +# Range Checker + +Miden VM relies very heavily on 16-bit range-checks (checking if a value of a field element is between $0$ and $2^{16}$). For example, most of the [u32 operations](./stack/u32_ops.md) need to perform between two and four 16-bit range-checks per operation. Similarly, operations involving memory (e.g. load and store) require two 16-bit range-checks per operation. + +Thus, it is very important for the VM to be able to perform a large number of 16-bit range checks very efficiently. In this note we describe how this can be achieved using the [LogUp](./lookups/logup.md) lookup argument. + +## 8-bit range checks + +First, let's define a construction for the simplest possible 8-bit range-check. This can be done with a single column as illustrated below. + +![rc_8_bit_range_check](../img/design/range/rc_8_bit_range_check.png) + +For this to work as a range-check we need to enforce a few constraints on this column: + +- The value in the first row must be $0$. +- The value in the last row must be $255$. +- As we move from one row to the next, we can either keep the value the same or increment it by $1$. + +Denoting $v$ as the value of column $v$ in the current row, and $v'$ as the value of column $v$ in the next row, we can enforce the last condition as follows: + +$$ +(v' - v) \cdot (v' - v - 1) = 0 +$$ + +Together, these constraints guarantee that all values in column $v$ are between $0$ and $255$ (inclusive). + +We can then make use of the LogUp lookup argument by adding another column $b$ which will keep a running sum that is the logarithmic derivative of the product of values in the $v$ column. The transition constraint for $b$ would look as follows: + +$$ +b' = b + \frac{1}{(\alpha - v)} +$$ + +Since constraints cannot include divisions, the constraint would actually be expressed as the following degree 2 constraint: + +$$ +b' \cdot (\alpha - v) = b \cdot (\alpha - v) + 1 +$$ + +Using these two columns we can check if some other column in the execution trace is a permutation of values in $v$. Let's call this other column $x$. We can compute the logarithmic derivative for $x$ as a running sum in the same way as we compute it for $v$. Then, we can check that the last value in $b$ is the same as the final value for the running sum of $x$. + +While this approach works, it has a couple of limitations: + +- First, column $v$ must contain all values between $0$ and $255$. Thus, if column $x$ does not contain one of these values, we need to artificially add this value to $x$ somehow (i.e., we need to pad $x$ with extra values). +- Second, assuming $n$ is the length of execution trace, we can range-check at most $n$ values. Thus, if we wanted to range-check more than $n$ values, we'd need to introduce another column similar to $v$. + +We can get rid of both requirements by including the _multiplicity_ of the value $v$ into the calculation of the logarithmic derivative for LogUp, which will allow us to specify exactly how many times each value needs to be range-checked. + +### A better construction + +Let's add one more column $m$ to our table to keep track of how many times each value should be range-checked. + +![rc_8_bit_logup](../img/design/range/rc_8_bit_logup.png) + +The transition constraint for $b$ is now as follows: + +$$ +b' = b + \frac{m}{(\alpha - v)} +$$ + +This addresses the limitations we had as follows: +1. We no longer need to pad the column we want to range-check with extra values because we can skip the values we don't care about by setting the multiplicity to $0$. +2. We can range check as many unique values as there are rows in the trace, and there is essentially no limit to how many times each of these values can be range-checked. (The only restriction on the multiplicity value is that it must be less than the size of the set of lookup values. Therefore, for long traces where $n > 2^{16}$, $m < 2^{16}$ must hold, and for short traces $m < n$ must be true.) + +Additionally, the constraint degree has not increased versus the naive approach, and the only additional cost is a single trace column. + +## 16-bit range checks + +To support 16-bit range checks, let's try to extend the idea of the 8-bit table. Our 16-bit table would look like so (the only difference is that column $v$ now has to end with value $65535$): + +![rc_16_bit_logup](../img/design/range/rc_16_bit_logup.png) + +While this works, it is rather wasteful. In the worst case, we'd need to enumerate over 65K values, most of which we may not actually need. It would be nice if we could "skip over" the values that we don't want. One way to do this could be to add bridge rows between two values to be range checked and add constraints to enforce the consistency of the gap between these bridge rows. + +If we allow gaps between two consecutive rows to only be 0 or powers of 2, we could enforce a constraint: + +$$ +\Delta v \cdot (\Delta v - 1) \cdot (\Delta v - 2) \cdot (\Delta v - 4) \cdot (\Delta v - 8) \cdot (\Delta v - 16) \cdot (\Delta v - 32) \cdot (\Delta v - 64) \cdot (\Delta v - 128) = 0 +$$ + +This constraint has a degree 9. This construction allows the minimum trace length to be 1024. + +We could go even further and allow the gaps between two consecutive rows to only be 0 or powers of 3. In this case we would enforce the constraint: + +$$ +\Delta v \cdot (\Delta v - 1) \cdot (\Delta v - 3) \cdot (\Delta v - 9) \cdot (\Delta v - 27) \cdot (\Delta v - 81) \cdot (\Delta v - 243) \cdot (\Delta v - 729) \cdot (\Delta v - 2187) = 0 +$$ + +This allows us to reduce the minimum trace length to 64. + +To find out the number of bridge rows to be added in between two values to be range checked, we represent the gap between them as a linear combination of powers of 3, ie, + +$$ +(r' - r) = \sum_{i=0}^{7} x_i \cdot 3^i +$$ + +Then for each $x_i$ except the first, we add a bridge row at a gap of $3^i$. + +## Miden approach + +This construction is implemented in Miden with the following requirements, capabilities and constraints. + +### Requirements + +- 2 columns of the main trace: $m, v$, where $v$ contains the value being range-checked and $m$ is the number of times the value is checked (its multiplicity). +- 1 [bus](./lookups/index.md#communication-buses-in-miden-vm) $b_{range}$ to ensure that the range checks performed in the range checker match those requested by other VM components (the [stack](./stack/u32_ops.md#range-checks) and the [memory chiplet](./chiplets/memory.md)). + +### Capabilities + +The construction gives us the following capabilities: +- For long traces (when $n > 2^{16}$), we can do an essentially unlimited number of arbitrary 16-bit range-checks. +- For short traces ($2^5 < n \le 2^{16}$), we can range-check slightly fewer than $n$ unique values, but there is essentially no practical limit to the total number of range checks. + + +### Execution trace + +The range checker's execution trace looks as follows: + +![rc_with_bridge_rows.png](../img/design/range/rc_with_bridge_rows.png) + +The columns have the following meanings: +- $m$ is the multiplicity column that indicates the number of times the value in that row should be range checked (included into the computation of the logarithmic derivative). +- $v$ contains the values to be range checked. + - These values go from $0$ to $65535$. Values must either stay the same or increase by powers of 3 less than or equal to $3^7$. + - The final 2 rows of the 16-bit section of the trace must both equal $65535$. The extra value of $65535$ is required in order to [pad the trace](./lookups/index.md#length-of-auxiliary-columns-for-lookup-arguments) so the [$b_{range}$](#communication-bus) bus column can be computed correctly. + +### Execution trace constraints + +First, we need to constrain that the consecutive values in the range checker are either the same or differ by powers of 3 that are less than or equal to $3^7$. + +> $$ +> \Delta v \cdot (\Delta v - 1) \cdot (\Delta v - 3) \cdot (\Delta v - 9) \cdot (\Delta v - 27) \cdot (\Delta v - 81) +> \cdot (\Delta v - 243) \cdot (\Delta v - 729) \cdot (\Delta v - 2187) = 0 \text{ | degree} = 9 +> $$ + +In addition to the transition constraints described above, we also need to enforce the following boundary constraints: + +- The value of $v$ in the first row is $0$. +- The value of $v$ in the last row is $65535$. + +### Communication bus + +$b_{range}$ is the [bus](./lookups/index.md#communication-buses-in-miden-vm) that connects components which require 16-bit range checks to the values in the range checker. The bus constraints are defined by the components that use it to communicate. + +Requests are sent to the range checker bus by the following components: +- The Stack sends requests for 16-bit range checks during some [`u32` operations](./stack/u32_ops.md#range-checks). +- The [Memory chiplet](./chiplets/memory.md) sends requests for 16-bit range checks against the values in the $d_0$ and $d_1$ trace columns to enforce internal consistency. + +Responses are provided by the range checker using the transition constraint for the LogUp construction described above. + +> $$ +> b'_{range} = b_{range} + \frac{m}{(\alpha - v)} \text{ | degree} = 2 +> $$ + +To describe the complete transition constraint for the bus, we'll define the following variables: + +- $f_{stack}$: the boolean flag that indicates whether or not a stack operation requiring range checks is occurring. This flag has degree 3. +- $f_{mem}$: the boolean flag that indicates whether or not a memory operation requiring range checks is occurring. This flag has degree 3. +- $s_0, s_1, s_2, s_3$: the values for which range checks are requested from the stack when $f_{stack}$ is set. +- $m_0, m_1$: the values for which range checks are requested from the memory chiplet when $f_{mem}$ is set. + +> $$ +> b'_{range} = b_{range} + \frac{m}{(\alpha - v)} - \frac{f_{stack}}{(\alpha - s_0)} - \frac{f_{stack}}{(\alpha - s_1)} - \frac{f_{stack}}{(\alpha - s_2)} - \frac{f_{stack}}{(\alpha - s_3)} - \frac{f_{mem}}{(\alpha - m_0)} - \frac{f_{mem}}{(\alpha - m_1)} \text{ | degree} = 9 +> $$ + +As previously mentioned, constraints cannot include divisions, so the actual constraint which is applied will be the equivalent expression in which all denominators have been multiplied through, which is degree 9. + +If $b_{range}$ is initialized to $0$ and the values sent to the bus by other VM components match those that are range-checked in the trace, then at the end of the trace we should end up with $b_{range} = 0$. + +Therefore, in addition to the transition constraint described above, we also need to enforce the following boundary constraints: + +- The value of $b_{range}$ in the first row $0$. +- The value of $b_{range}$ in the last row $0$. diff --git a/versioned_docs/version-0.16/reference/miden-vm/design/stack/_category_.yml b/versioned_docs/version-0.16/reference/miden-vm/design/stack/_category_.yml new file mode 100644 index 00000000..246c4af7 --- /dev/null +++ b/versioned_docs/version-0.16/reference/miden-vm/design/stack/_category_.yml @@ -0,0 +1,4 @@ +label: "Operand stack" +# Determines where this documentation section appears relative to other sections in the parent folder +position: 3 +collapsed: true diff --git a/versioned_docs/version-0.16/reference/miden-vm/design/stack/crypto_ops.md b/versioned_docs/version-0.16/reference/miden-vm/design/stack/crypto_ops.md new file mode 100644 index 00000000..f567fe4c --- /dev/null +++ b/versioned_docs/version-0.16/reference/miden-vm/design/stack/crypto_ops.md @@ -0,0 +1,530 @@ +--- +title: "Cryptographic Operations" +sidebar_position: 8 +--- + +# Cryptographic operations +In this section we describe the AIR constraints for Miden VM cryptographic operations. + +Cryptographic operations in Miden VM are performed by the [Hash chiplet](../chiplets/hasher.md). Communication between the stack and the hash chiplet is accomplished via the chiplet bus $b_{chip}$. To make requests to and to read results from the chiplet bus we need to divide its current value by the value representing the request. + +Thus, to describe AIR constraints for the cryptographic operations, we need to define how to compute these input and output values within the stack. We do this in the following sections. + +## HPERM +The `HPERM` operation applies a Poseidon2 permutation to the top $12$ elements of the stack. The stack is arranged in LE state order `[RATE0, RATE1, CAPACITY]`, with $s_0$ at the top and mapping to the first rate lane. The diagram below illustrates this graphically. + +![hperm](../../img/design/stack/crypto_ops/HPERM.png) + +In the above, $r$ (located in the helper register $h_0$) is the row address from the hash chiplet set by the prover non-deterministically. + +For the `HPERM` operation, we define input and output values as follows: + +$$ +v_{input} = \alpha_0 + \alpha_1 \cdot op_{linhash} + \alpha_2 \cdot h_0 + \sum_{j=0}^{11} (\alpha_{j+4} \cdot s_j) +$$ + +$$ +v_{output} = \alpha_0 + \alpha_1 \cdot op_{retstate} + \alpha_2 \cdot (h_0 + 1) + \sum_{j=0}^{11} (\alpha_{j+4} \cdot s_j') +$$ + +In the above, $op_{linhash}$ and $op_{retstate}$ are the unique [operation labels](../chiplets/index.md#operation-labels) for initiating a linear hash and reading the full state of the hasher respectively. Also note that the term for $\alpha_3$ is missing from the above expressions because for Poseidon2 permutation computation the index column is expected to be set to $0$. + +Using the above values, we can describe the constraint for the chiplet bus column as follows: + +$$ +b_{chip}' \cdot v_{input} \cdot v_{output} = b_{chip} \text{ | degree} = 3 +$$ + +The above constraint enforces that the specified input and output controller rows must be present in the hash-controller region. These controller rows are consecutive, so their addresses differ by exactly $1$. + +The effect of this operation on the rest of the stack is: +* **No change** starting from position $12$. + +## MPVERIFY +The `MPVERIFY` operation verifies that a Merkle path from the specified node resolves to the specified root. This operation can be used to prove that the prover knows a path in the specified Merkle tree which starts with the specified node. + +Prior to the operation, the stack is expected to be arranged as follows (from the top): +- Value of the node, 4 elements ($V$ in the below image) +- Depth of the path, 1 element ($d$ in the below image) +- Index of the node, 1 element ($i$ in the below image) +- Root of the tree, 4 elements ($R$ in the below image) + +The Merkle path itself is expected to be provided by the prover non-deterministically (via the advice provider). If the prover is not able to provide the required path, the operation fails. Otherwise, the state of the stack does not change. The diagram below illustrates this graphically. + +![mpverify](../../img/design/stack/crypto_ops/MPVERIFY.png) + +In the above, $r$ (located in the helper register $h_0$) is the row address from the hash chiplet set by the prover non-deterministically. + +For the `MPVERIFY` operation, we define input and output values as follows: + +$$ +v_{input} = \alpha_0 + \alpha_1 \cdot op_{mpver} + \alpha_2 \cdot h_0 + \alpha_3 \cdot s_5 + \sum_{j=0}^3 \alpha_{j + 4} \cdot s_{j} +$$ + +$$ +v_{output} = \alpha_0 + \alpha_1 \cdot op_{rethash} + \alpha_2 \cdot (h_0 + 2 \cdot s_4 - 1) + \sum_{j=0}^3\alpha_{j + 4} \cdot s_{6 + j} +$$ + +In the above, $op_{mpver}$ and $op_{rethash}$ are the unique [operation labels](../chiplets/index.md#operation-labels) for initiating a Merkle path verification computation and reading the hash result respectively. The sum expression for inputs computes the value of the leaf node, while the sum expression for the output computes the value of the tree root. + +Using the above values, we can describe the constraint for the chiplet bus column as follows: + +$$ +b_{chip}' \cdot v_{input} \cdot v_{output} = b_{chip} \text{ | degree} = 3 +$$ + +The above constraint enforces that the specified input and output controller rows must be present in the hash-controller region, and that they must be exactly $2 \cdot d - 1$ rows apart, where $d$ is the depth of the node. Each Merkle level contributes one controller pair `(input, output)`. + +The effect of this operation on the rest of the stack is: +* **No change** starting from position $0$. + +## MRUPDATE +The `MRUPDATE` operation computes a new root of a Merkle tree where a node at the specified position is updated to the specified value. + +The stack is expected to be arranged as follows (from the top): +- old value of the node, 4 element ($V$ in the below image) +- depth of the node, 1 element ($d$ in the below image) +- index of the node, 1 element ($i$ in the below image) +- current root of the tree, 4 elements ($R$ in the below image) +- new value of the node, 4 element ($NV$ in the below image) + +The Merkle path for the node is expected to be provided by the prover non-deterministically (via merkle sets). At the end of the operation, the old node value is replaced with the new root value computed based on the provided path. Everything else on the stack remains the same. The diagram below illustrates this graphically. + +![mrupdate](../../img/design/stack/crypto_ops/MRUPDATE.png) + +In the above, $r$ (located in the helper register $h_0$) is the row address from the hash chiplet set by the prover non-deterministically. + +For the `MRUPDATE` operation, we define input and output values as follows: + +$$ +v_{inputold} = \alpha_0 + \alpha_1 \cdot op_{mruold} + \alpha_2 \cdot h_0 + \alpha_3 \cdot s_5 + \sum_{j=0}^3\alpha_{j + 4} \cdot s_{j} +$$ + +$$ +v_{outputold} = \alpha_0 + \alpha_1 \cdot op_{rethash} + \alpha_2 \cdot (h_0 + 2 \cdot s_4 - 1) + \sum_{j=0}^3\alpha_{j + 4} \cdot s_{6 + j} +$$ + +$$ +v_{inputnew} = \alpha_0 + \alpha_1 \cdot op_{mrunew} + \alpha_2 \cdot (h_0 + 2 \cdot s_4) + \alpha_3 \cdot s_5 + \sum_{j=0}^3\alpha_{j + 4} \cdot s_{10 + j} +$$ + +$$ +v_{outputnew} = \alpha_0 + \alpha_1 \cdot op_{rethash} + \alpha_2 \cdot (h_0 + 4 \cdot s_4 - 1) + \sum_{j=0}^3\alpha_{j + 4} \cdot s_{j}' +$$ + +In the above, the first two expressions correspond to inputs and outputs for verifying the Merkle path between the old node value and the old tree root, while the last two expressions correspond to inputs and outputs for verifying the Merkle path between the new node value and the new tree root. The hash chiplet ensures the same set of sibling nodes are used in both of these computations. + +The $op_{mruold}$, $op_{mrunew}$, and $op_{rethash}$ are the unique [operation labels](../chiplets/index.md#operation-labels) used by the above computations. + +> $$ +> b_{chip}' \cdot v_{inputold} \cdot v_{outputold} \cdot v_{inputnew} \cdot v_{outputnew} = b_{chip} \text{ | degree} = 5 +> $$ + +The above constraint enforces that the specified input and output controller rows for both the old and the new node/root combinations must be present in the hash-controller region. The old-path output is $2 \cdot d - 1$ rows after the old-path input, the new-path input starts immediately after that at offset $2 \cdot d$, and the new-path output is $4 \cdot d - 1$ rows after the initial old-path input. It also ensures that the computation for the old node/root combination is immediately followed by the computation for the new node/root combination. + +The effect of this operation on the rest of the stack is: +* **No change** for positions starting from $4$. + +## CRYPTOSTREAM +The `CRYPTOSTREAM` operation reads two words from memory, combines them with the +top 8 stack elements (the rate), writes the resulting ciphertext back to memory, +and replaces the top 8 stack elements with the ciphertext. The source and +destination pointers are stored in stack positions $12$ and $13$, respectively. + +Let $r_i = s_i$ be the rate values and $c_i = s_i'$ be the ciphertext values on +the stack after the operation. We define plaintext values as $p_i = c_i - r_i$. + +The source and destination pointers advance by two words: + +$$ +s_{12}' = s_{12} + 8 +$$ + +$$ +s_{13}' = s_{13} + 8 +$$ + +The capacity and tail elements are unchanged: + +$$ +s_i' - s_i = 0 \text{ for } i \in \{8,9,10,11,14,15\} +$$ + +We define the two read requests and two write requests as follows: + +$$ +u_{read,1} = \alpha_0 + \alpha_1 \cdot op_{mem\_readword} + \alpha_2 \cdot ctx + +\alpha_3 \cdot s_{12} + \alpha_4 \cdot clk + \sum_{j=0}^3 \alpha_{j+5} \cdot p_j +$$ + +$$ +u_{read,2} = \alpha_0 + \alpha_1 \cdot op_{mem\_readword} + \alpha_2 \cdot ctx + +\alpha_3 \cdot (s_{12} + 4) + \alpha_4 \cdot clk + +\sum_{j=0}^3 \alpha_{j+5} \cdot p_{j+4} +$$ + +$$ +u_{write,1} = \alpha_0 + \alpha_1 \cdot op_{mem\_writeword} + \alpha_2 \cdot ctx + +\alpha_3 \cdot s_{13} + \alpha_4 \cdot clk + \sum_{j=0}^3 \alpha_{j+5} \cdot c_j +$$ + +$$ +u_{write,2} = \alpha_0 + \alpha_1 \cdot op_{mem\_writeword} + \alpha_2 \cdot ctx + +\alpha_3 \cdot (s_{13} + 4) + \alpha_4 \cdot clk + +\sum_{j=0}^3 \alpha_{j+5} \cdot c_{j+4} +$$ + +$$ +u_{mem} = u_{read,1} \cdot u_{read,2} \cdot u_{write,1} \cdot u_{write,2} +$$ + +In the above, $op_{mem\_readword}$ and $op_{mem\_writeword}$ are the unique +[operation labels](../chiplets/index.md#operation-labels) for the memory read +and write word operations. + +Using the above value, the chiplet bus constraint is: + +$$ +b_{chip}' \cdot u_{mem} = b_{chip} \text{ | degree} = 5 +$$ + +The effect of this operation on the rest of the stack is: +* **No change** starting from position $8$, except for the pointer updates above. + +## FRIE2F4 +The `FRIE2F4` operation performs one factor-4 FRI layer fold over the quadratic extension field. It also checks consistency with the previous folded layer and writes the loop state consumed by the next FRI layer. + +The stack for the operation is expected to be arranged as follows: +- The first $8$ stack elements contain $4$ opened leaf values to be folded. Each value is represented by two field elements. The leaf values are stored in bit-reversed order: $q_0 = (v_0, v_1)$, $q_2 = (v_2, v_3)$, $q_1 = (v_4, v_5)$, $q_3 = (v_6, v_7)$. +- The next element $f\_pos$ is the query position in the folded domain. It can be computed as $pos \mod n$, where $pos$ is the position in the source domain, and $n$ is size of the folded domain. +- The next element is the natural coset index $\lfloor \frac{pos}{n} \rfloor$. Since the size of the source domain is always $4$ times bigger than the size of the folded domain, possible coset values are $0$, $1$, $2$, and $3$. +- The next element $poe$ is a power of the current source-domain generator used to compute the domain point $x$. +- The next two elements contain the result of the previous layer folding - a single element in the extension field denoted as $pe = (pe_0, pe_1)$. +- The next two elements specify a random verifier challenge $\alpha$ for the current layer defined as $\alpha = (a_0, a_1)$. +- The last element on the top of the stack ($cptr$) is expected to be a memory address of the layer currently being folded. + +The diagram below illustrates stack transition for `FRIE2F4` operation. + +![frie2f4](../../img/design/stack/crypto_ops/FRIE2F4.png) + +At the high-level, the operation does the following: +- Computes the domain value $x$ based on values of $poe$ and the coset index. +- Using $x$ and $\alpha$, folds the query values $q_0, ..., q_3$ into a single value $r$. +- Compares the previously folded value $pe$ to the leaf value selected by the coset index. +- Computes the new value of $poe$ as $poe' = poe^4$ (this is done in two steps to keep the constraint degree low). +- Increments the layer address pointer by $8$. +- Shifts the stack by $1$ to the left. This moves an element from the stack overflow table into the last position on the stack top. + +To keep the constraint degree low, the operation uses all $6$ helper registers and the first $8$ next-state stack elements as degree-reduction intermediates. Callers should treat those $8$ output elements as scratch. + +> TODO: add detailed constraint descriptions. See discussion [here](https://github.com/0xMiden/miden-vm/issues/567#issuecomment-1398088792). + +The effect on the rest of the stack is: +* **Left shift** starting from position $16$. + +## HORNERBASE + +The `HORNERBASE` operation performs $8$ steps of the Horner method for evaluating a polynomial with coefficients over the base field at a point in the quadratic extension field. More precisely, it performs the following updates to the accumulator on the stack: +$$ +\begin{align*} +\mathsf{tmp0} &= ((\mathsf{acc} \cdot \alpha + c_0) \cdot \alpha) + c_1 \\ +\mathsf{tmp1} &= ((((\mathsf{tmp0} \cdot \alpha) + c_2) \cdot \alpha + c_3) \cdot \alpha) + c_4 \\ +\mathsf{acc}^{'} &= ((((\mathsf{tmp1} \cdot \alpha + c_5) \cdot \alpha + c_6) \cdot \alpha) + c_7) +\end{align*} +$$ + +where $c_i$ are the coefficients of the polynomial, $\alpha$ the evaluation point, $\mathsf{acc}$ the current accumulator value, $\mathsf{acc}^{'}$ the updated accumulator value, and $\mathsf{tmp0}$, $\mathsf{tmp1}$ are helper variables used for constraint degree reduction. + +The stack for the operation is expected to be arranged as follows: +- The first $8$ stack elements (positions 0-7) are the $8$ base field elements representing the current 8-element batch of coefficients for the polynomial being evaluated, arranged as $[c_0, c_1, c_2, c_3, c_4, c_5, c_6, c_7]$ where $c_0$ is at position 0 (top of stack). Here $c_0$ is the highest-degree coefficient ($\alpha^7$ term) and $c_7$ is the constant term. +- The next $5$ stack elements are irrelevant for the operation and unaffected by it. +- The next stack element contains the memory address `alpha_ptr` pointing to the evaluation point $\alpha = (\alpha_0, \alpha_1)$. The operation reads $\alpha_0$ from `alpha_ptr` and $\alpha_1$ from `alpha_ptr + 1`. +- The next $2$ stack elements contain the value of the current accumulator $\textsf{acc} = (\textsf{acc}_0, \textsf{acc}_1)$. + +The diagram below illustrates the stack transition for `HORNERBASE` operation. + +![horner_eval_base](../../img/design/stack/crypto_ops/HORNERBASE.png) + +After calling the operation: +- Helper registers $h_i$ will contain the values $[\alpha_0, \alpha_1, \mathsf{tmp1}_0, \mathsf{tmp1}_1, \mathsf{tmp0}_0, \mathsf{tmp0}_1]$. +- Stack elements $14$ and $15$ will contain the value of the updated accumulator i.e., $\mathsf{acc}^{'}$. + +More specifically, the stack transition for this operation must satisfy the following constraints. +Here $\alpha = (\alpha_0, \alpha_1)$ is an element of $\mathbb{F}_{p^2}$ with $u^2 = 7$. +We write $c_0 = (c_{0,0}, c_{0,1})$, $c_1 = (c_{1,0}, c_{1,1})$, $c_2 = (c_{2,0}, c_{2,1})$, and $c_3 = (c_{3,0}, c_{3,1})$ for the extension-field coefficients. + +$$ +\begin{align*} + \alpha^2 &= (\alpha^2_0, \alpha^2_1) = (\alpha_0^2 + 7 \alpha_1^2, 2 \alpha_0 \alpha_1) \\ + \alpha^3 &= (\alpha^3_0, \alpha^3_1) = (\alpha_0^3 + 21 \alpha_0 \alpha_1^2, 3 \alpha_0^2 \alpha_1 + 7 \alpha_1^3) \\ + \mathsf{tmp0}_0 &= \mathsf{acc}_0 \cdot \alpha^2_0 + \mathsf{acc}_1 \cdot (7 \alpha^2_1) + c_0 \alpha_0 + c_1 \\ + \mathsf{tmp0}_1 &= \mathsf{acc}_0 \cdot \alpha^2_1 + \mathsf{acc}_1 \cdot \alpha^2_0 + c_0 \alpha_1 \\ + \\ + \mathsf{tmp1}_0 &= \mathsf{tmp0}_0 \cdot \alpha^3_0 + \mathsf{tmp0}_1 \cdot (7 \alpha^3_1) + + c_2 \alpha^2_0 + c_3 \alpha_0 + c_4 \\ + \mathsf{tmp1}_1 &= \mathsf{tmp0}_0 \cdot \alpha^3_1 + \mathsf{tmp0}_1 \cdot \alpha^3_0 + + c_2 \alpha^2_1 + c_3 \alpha_1 \\ + \\ + \mathsf{acc}_0^{'} &= \mathsf{tmp1}_0 \cdot \alpha^3_0 + \mathsf{tmp1}_1 \cdot (7 \alpha^3_1) + + c_5 \alpha^2_0 + c_6 \alpha_0 + c_7 \\ + \mathsf{acc}_1^{'} &= \mathsf{tmp1}_0 \cdot \alpha^3_1 + \mathsf{tmp1}_1 \cdot \alpha^3_0 + + c_5 \alpha^2_1 + c_6 \alpha_1 +\end{align*} +$$ + +The `HORNERBASE` makes two memory access requests (reading $\alpha_0$ and $\alpha_1$ individually): + +$$ +\begin{aligned} + u_{mem,0} &= \alpha_0 + \alpha_1 \cdot op_{mem\_read} + \alpha_2 \cdot ctx + \alpha_3 \cdot s_{13} \\ + &\quad + \alpha_4 \cdot clk + \alpha_{5} \cdot h_{0}. +\end{aligned} +$$ + +$$ +\begin{aligned} + u_{mem,1} &= \alpha_0 + \alpha_1 \cdot op_{mem\_read} + \alpha_2 \cdot ctx + \alpha_3 \cdot (s_{13} + 1) \\ + &\quad + \alpha_4 \cdot clk + \alpha_{5} \cdot h_{1}. +\end{aligned} +$$ + +Using the above values, we can describe the constraint for the chiplets bus column as follows: + +$$ +b_{chip}' \cdot u_{mem,0} \cdot u_{mem,1} = b_{chip} \text{ | degree} = 3 +$$ + +The effect on the rest of the stack is: +* **No change.** + +## HORNEREXT +The `HORNEREXT` operation performs $4$ steps of the Horner method for evaluating a polynomial with coefficients over the quadratic extension field at a point in the quadratic extension field. More precisely, it performs the following update to the accumulator on the stack + $$\mathsf{tmp} = (\mathsf{acc} \cdot \alpha + c_3) \cdot \alpha + c_2$$ +$$\mathsf{acc}^{'} = (\mathsf{tmp} \cdot \alpha + c_1) \cdot \alpha + c_0$$ + +where $c_i$ are the coefficients of the polynomial, $\alpha$ the evaluation point, $\mathsf{acc}$ the current accumulator value, $\mathsf{acc}^{'}$ the updated accumulator value, and $\mathsf{tmp}$ is a helper variable used for constraint degree reduction. + +The stack for the operation is expected to be arranged as follows: +- The first $8$ stack elements contain $8$ base field elements that make up the current 4-element batch of coefficients, in the quadratic extension field, for the polynomial being evaluated. We interpret these coefficients as $c_0 = (s_0, s_1)$, $c_1 = (s_2, s_3)$, $c_2 = (s_4, s_5)$, and $c_3 = (s_6, s_7)$. +- The next $5$ stack elements are irrelevant for the operation and unaffected by it. +- The next stack element contains the value of the memory pointer `alpha_ptr` to the evaluation point $\alpha$. The word address containing $\alpha = (\alpha_0, \alpha_1)$ is expected to have layout $[\alpha_0, \alpha_1, k_0, k_1]$ where $[k_0, k_1]$ is the second half of the memory word containing $\alpha$. Note that, in the context of the above expressions, we only care about the first half i.e., $[\alpha_0, \alpha_1]$, but providing the second half of the word in order to be able to do a one word memory read is more optimal than doing two element memory reads. +- The next $2$ stack elements contain the value of the current accumulator $\textsf{acc} = (\textsf{acc}_0, \textsf{acc}_1)$. + +The diagram below illustrates the stack transition for `HORNEREXT` operation. + +![horner_eval_ext](../../img/design/stack/crypto_ops/HORNEREXT.png) + +After calling the operation: +- Helper registers $h_i$ will contain the values $[\alpha_0, \alpha_1, k_0, k_1, \mathsf{tmp}_0, \mathsf{tmp}_1]$. +- Stack elements $14$ and $15$ will contain the value of the updated accumulator i.e., $\mathsf{acc}^{'}$. + +More specifically, the stack transition for this operation must satisfy the following constraints. +Here $\alpha = (\alpha_0, \alpha_1)$ is an element of $\mathbb{F}_{p^2}$ with $u^2 = 7$. + +$$ +\begin{align*} +\alpha^2 &= (\alpha^2_0, \alpha^2_1) = (\alpha_0^2 + 7 \alpha_1^2, 2 \alpha_0 \alpha_1) \\ +\mathsf{tmp}_0 &= \mathsf{acc}_0 \cdot \alpha^2_0 + \mathsf{acc}_1 \cdot (7 \alpha^2_1) + + c_{0,0} \alpha_0 + 7 c_{0,1} \alpha_1 + c_{1,0} \\ +\mathsf{tmp}_1 &= \mathsf{acc}_0 \cdot \alpha^2_1 + \mathsf{acc}_1 \cdot \alpha^2_0 + + c_{0,0} \alpha_1 + c_{0,1} \alpha_0 + c_{1,1} \\ +\\ +\mathsf{acc}_0^{'} &= \mathsf{tmp}_0 \cdot \alpha^2_0 + \mathsf{tmp}_1 \cdot (7 \alpha^2_1) + + c_{2,0} \alpha_0 + 7 c_{2,1} \alpha_1 + c_{3,0} \\ +\mathsf{acc}_1^{'} &= \mathsf{tmp}_0 \cdot \alpha^2_1 + \mathsf{tmp}_1 \cdot \alpha^2_0 + + c_{2,0} \alpha_1 + c_{2,1} \alpha_0 + c_{3,1} +\end{align*} +$$ + +The effect on the rest of the stack is: +* **No change.** + +The `HORNEREXT` makes one memory access request: + +$$ +u_{mem} = \alpha_0 + \alpha_1 \cdot op_{mem\_readword} + \alpha_2 \cdot ctx + \alpha_3 \cdot s_{13} + \alpha_4 \cdot clk + \alpha_{5} \cdot h_{0} + \alpha_{6} \cdot h_{1} + \alpha_{7} \cdot h_{2} + \alpha_{8} \cdot h_{3} +$$ + +Using the above value, we can describe the constraint for the chiplets bus column as follows: + +$$ +b_{chip}' \cdot u_{mem} = b_{chip} \text{ | degree} = 2 +$$ + +## EVALCIRCUIT + +The `EVALCIRCUIT` operation evaluates an arithmetic circuit, given its circuit description and a set of input values, using the [ACE](../chiplets/ace.md) chiplet and asserts that the evaluation is equal to zero. + +The stack is expected to be arranged as follows (from the top): +- A pointer to the circuit description with the [expected](../chiplets/ace.md#memory-layout) layout by the ACE chiplet. +- The number of quadratic extension field elements that are read during the `READ` [phase](../chiplets/ace.md#circuit-evaluation) of circuit evaluation. +- The number of base field elements representing the encodings of instructions that make up the circuit being evaluated during the `EVAL` [phase](../chiplets/ace.md#circuit-evaluation) of circuit evaluation. + +The diagram below illustrates this graphically. + +![evalcircuit](../../img/design/stack/crypto_ops/EVALCIRCUIT.png) + +Calling the operation has no effect on the stack or on helper registers. Instead, the operation makes a request to the `ACE` chiplet using the chiplets' bus. More precisely, let + +$$ +v_{ace} = \alpha_0 + \mathsf{ACE\_LABEL}\cdot\alpha_1 + ctx \cdot\alpha_2 + ptr\cdot\alpha_3 + clk\cdot\alpha_4 + n_{read}\cdot\alpha_5 + n_{eval}\cdot\alpha_6. +$$ + +where: +- $\mathsf{ACE\_LABEL}$ is the unique [operation labels](../chiplets/index.md#operation-labels) for initiating a circuit evaluation request to the ACE chiplet, +- $ctx$ is the memory context from which the operation was initiated, +- $clk$ is the clock cycle at which the operation was initiated, +- $ptr$, $n_{read}$ and $n_{eval}$ are as above. + +Then, using the above value, we can describe the constraint for the chiplets' bus column as follows: + +$$ +b_{chip}' \cdot v_{ace} = b_{chip} \text{ | degree} = 2 +$$ + +## LOG_DEFERRED + +The `log_deferred` operation folds a verified statement digest `STMNT` into the rolling deferred +root. The update is the structural digest of `Node::and(ROOT_PREV, STMNT)`, computed as a Poseidon2 +merge with the framework `Tag::AND` capacity word `[1, 0, 0, 0]`: +`ROOT_NEW = rate0(Poseidon2([ROOT_PREV, STMNT, [1,0,0,0]]))`. The final root is a public input; +`DeferredProof` material later resolves the trusted root before VM STARK verification. Final +verification accepts `Empty` or verified `Stark`; explicit partial verification rehydrates `Wire` +under the built-in `miden_precompiles::registry()`. This section concentrates on the stack +interaction and bus messages. + +### Operation Overview + +The stack is expected to be arranged as `[_, STMNT, _, ...]`, where `STMNT` sits at offsets +4..8 (the HPERM rate1 slots). Stack slots 0..4 and 8..12 are unreferenced by any constraint on +opcode entry. `STMNT` must already be present in the processor's deferred state and evaluate to +`TRUE`; otherwise execution fails when the opcode attempts to log it. Core-library and precompile +support code wrap this low-level opcode by registering nodes and logging statement digests. + +Additionally, the processor maintains a persistent rolling deferred root that is updated with each +`LOG_DEFERRED` invocation. The previous root is provided non‑deterministically via helper +registers and is denoted `ROOT_PREV`. The hasher bus links the constrained Poseidon2 permutation to +the stack transition, while the deferred state enforces that the logged statement evaluates to +`TRUE`. + +The operation evaluates +`[ROOT_NEW, OUT_RATE1, OUT_CAP] = Poseidon2([ROOT_PREV, STMNT, [1,0,0,0]])`, with the following +stack transition: + +``` +Before: [_, STMNT, _, ...] +After: [ROOT_NEW, OUT_RATE1, OUT_CAP, ...] +``` + +`STMNT` placement on rate1 lets the chiplet bus's β⁶..β⁹ products coincide with HPERM's rate1 +products, so they share gates after circuit memoization. The output uses the identity HPERM +lane→slot mapping: `rate0_out -> stack[0..4]` (= `ROOT_NEW`), `rate1_out -> stack[4..8]`, +`cap_out -> stack[8..12]`. `OUT_RATE1` and `OUT_CAP` are unused and are typically dropped by +the caller immediately. + +The operation uses the following helper registers: +- $h_0$: Hasher chiplet row address +- $h_1, h_2, h_3, h_4$: Previous deferred root `ROOT_PREV` + +Note: helper registers expose `ROOT_PREV` for bus constraints only; the VM maintains the deferred +root internally between invocations. + +### Bus Communication + +#### Hasher chiplet + +The following two messages are sent to the hasher chiplet, ensuring the validity of the resulting +permutation. Let $s_i$ denote the $i$-th stack column at that row (top of stack is $s_0$). The +elements appearing on the bus are: + +$$ +\begin{aligned} +\mathsf{ROOT}^{\text{prev}}_i &= h_{i+1} &&\text{(helper registers)}\\ +\mathsf{STMNT}_i &= s_{4+i} &&\text{(stack slots 4..7)}\\ +\mathsf{ANDTAG}_i &= \bigl([1,0,0,0]\bigr)_i &&\text{(`Tag::AND` capacity word)} +\end{aligned} +\qquad i \in \{0,1,2,3\}. +$$ + +The input message reduces the Poseidon2 state in the canonical order +`[ROOT_PREV, STMNT, [1,0,0,0]]`: + +$$ +v_{\text{input}} = \alpha_0 + \alpha_1 \cdot op_{linhash} + \alpha_2 \cdot h_0 + \sum_{i=0}^{3} \alpha_{i+4} \cdot \mathsf{ROOT}^{\text{prev}}_i + \sum_{i=0}^{3} \alpha_{i+8} \cdot \mathsf{STMNT}_i + \sum_{i=0}^{3} \alpha_{i+12} \cdot \mathsf{ANDTAG}_i. +$$ + +One controller row later, the `op_retstate` response provides the permuted state +`[ROOT_NEW, OUT_RATE1, OUT_CAP]`. Denote the stack after the instruction by $s'_i$; the top +twelve elements are `[ROOT_NEW, OUT_RATE1, OUT_CAP]`. Thus + +$$ +\begin{aligned} +\mathsf{ROOT}^{\text{new}}_i &= s'_{i}, +\mathsf{OUT\_RATE1}_i &= s'_{4+i},\\ +\mathsf{OUT\_CAP}_i &= s'_{8+i}, +\end{aligned} +\qquad i \in \{0,1,2,3\}, +$$ + +and the response message is + +$$ +v_{\text{output}} = \alpha_0 + \alpha_1 \cdot op_{retstate} + \alpha_2 \cdot (h_0 + 1) + \sum_{i=0}^{3} \alpha_{i+4} \cdot \mathsf{ROOT}^{\text{new}}_i + \sum_{i=0}^{3} \alpha_{i+8} \cdot \mathsf{OUT\_RATE1}_i + \sum_{i=0}^{3} \alpha_{i+12} \cdot \mathsf{OUT\_CAP}_i. +$$ + +Using the above values, we can describe the constraint for the chiplet bus column as follows: + +$$ +b_{chip}' \cdot v_{input} \cdot v_{output} = b_{chip} +$$ + +The above constraint enforces that the specified input and output controller rows must be present +in the hash-controller region. These two controller rows are consecutive, so their addresses differ +by exactly 1. + + + +### Deferred-root Initialization + +Inside the VM, the deferred root is tracked via the virtual-table bus: each `log_deferred` update +removes the previous root before inserting the next one. + +We denote the messages for removing and inserting the root as + +$$ +v_{rem} = \alpha_0 + \alpha_1 \cdot op_{log\_deferred} + \sum_{j=0}^{3} \alpha_{j+2} \cdot \mathsf{ROOT\_PREV}_j +$$ + +$$ +v_{ins} = \alpha_0 + \alpha_1 \cdot op_{log\_deferred} + \sum_{j=0}^{3} \alpha_{j+2} \cdot \mathsf{ROOT\_NEW}_j +$$ + +The bus constraint is applied to the virtual table column as follows. + +$$ +b_{vtable}' \cdot v_{rem} = b_{vtable} \cdot v_{ins} +$$ + +To ensure the column accounts for the initial and final deferred roots, the verifier initializes the +bus with fixed public values: the initial root is `TRUE_DIGEST` (the zero word) and the final +deferred root is the four-felt public value committed by the VM trace. More specifically, it +constrains the first value of the bus to be equal to + +$$ +b_{vtable,0} = \frac{v_{ins, init}}{v_{rem, last}} +$$ + +The messages $v_{ins, init}$ and $v_{rem, last}$ are given by + +$$ +v_{ins,init} = \alpha_0 + \alpha_1 \cdot op_{log\_deferred}, +$$ + +$$ +v_{rem,last} = \alpha_0 + \alpha_1 \cdot op_{log\_deferred} + \sum_{j=0}^{3} \alpha_{j+2} \cdot \mathsf{ROOT\_FINAL}_j. +$$ + +Because the domain-separated Poseidon2 merge outputs a digest word directly, the deferred root is +itself the digest at every step. The final deferred root is a fixed four-field-element public value, +not a variable-length request transcript. Partial proofs may carry the root-reachable DAG as +`DeferredStateWire`; final proofs may instead carry a precompile VM STARK proof for the same root. diff --git a/versioned_docs/version-0.16/reference/miden-vm/design/stack/field_ops.md b/versioned_docs/version-0.16/reference/miden-vm/design/stack/field_ops.md new file mode 100644 index 00000000..6a01e459 --- /dev/null +++ b/versioned_docs/version-0.16/reference/miden-vm/design/stack/field_ops.md @@ -0,0 +1,263 @@ +--- +title: "Field Operations" +sidebar_position: 4 +--- + +# Field Operations +In this section we describe the AIR constraints for Miden VM field operations (i.e., arithmetic operations over field elements). + +## ADD +Assume $a$ and $b$ are the elements at the top of the stack. The `ADD` operation computes $c \leftarrow (a + b)$. The diagram below illustrates this graphically. + +![add](../../img/design/stack/field_operations/ADD.png) + +Stack transition for this operation must satisfy the following constraints: + +$$ +s_0' - (s_0 + s_1) = 0 \text{ | degree} = 1 +$$ + +The effect on the rest of the stack is: +* **Left shift** starting from position $2$. + +## NEG +Assume $a$ is the element at the top of the stack. The `NEG` operation computes $b \leftarrow (-a)$. The diagram below illustrates this graphically. + +![neg](../../img/design/stack/field_operations/NEG.png) + +Stack transition for this operation must satisfy the following constraints: + +$$ +s_0' + s_0 = 0 \text{ | degree} = 1 +$$ + +The effect on the rest of the stack is: +* **No change** starting from position $1$. + +## MUL +Assume $a$ and $b$ are the elements at the top of the stack. The `MUL` operation computes $c \leftarrow (a \cdot b)$. The diagram below illustrates this graphically. + +![mul](../../img/design/stack/field_operations/MUL.png) + +Stack transition for this operation must satisfy the following constraints: + +$$ +s_0' - s_0 \cdot s_1 = 0 \text{ | degree} = 2 +$$ + +The effect on the rest of the stack is: +* **Left shift** starting from position $2$. + +## INV +Assume $a$ is the element at the top of the stack. The `INV` operation computes $b \leftarrow (a^{-1})$. The diagram below illustrates this graphically. + +![inv](../../img/design/stack/field_operations/INV.png) + +Stack transition for this operation must satisfy the following constraints: + +$$ +1 - s_0' \cdot s_0 = 0 \text{ | degree} = 2 +$$ + +Note that the above constraint can be satisfied only if the value in $s_0 \neq 0$. + +The effect on the rest of the stack is: +* **No change** starting from position $1$. + +## INCR +Assume $a$ is the element at the top of the stack. The `INCR` operation computes $b \leftarrow (a+1)$. The diagram below illustrates this graphically. + +![incr](../../img/design/stack/field_operations/INCR.png) + +Stack transition for this operation must satisfy the following constraints: + +$$ +s_0' - (s_0 + 1) = 0 \text{ | degree} = 1 +$$ + +The effect on the rest of the stack is: +* **No change** starting from position $1$. + +## NOT +Assume $a$ is a binary value at the top of the stack. The `NOT` operation computes $b \leftarrow (\lnot a)$. The diagram below illustrates this graphically. + +![not](../../img/design/stack/field_operations/NOT.png) + +Stack transition for this operation must satisfy the following constraints: + +$$ +s_0^2 - s_0 = 0 \text{ | degree} = 2 +$$ + +$$ +s_0' - (1 - s_0) = 0 \text{ | degree} = 1 +$$ + +The first constraint ensures that the value in $s_0$ is binary, and the second constraint ensures the correctness of the boolean `NOT` operation. + +The effect on the rest of the stack is: +* **No change** starting from position $1$. + +## AND +Assume $a$ and $b$ are binary values at the top of the stack. The `AND` operation computes $c \leftarrow (a \land b)$. The diagram below illustrates this graphically. + +![and](../../img/design/stack/field_operations/AND.png) + +Stack transition for this operation must satisfy the following constraints: + +$$ +s_i^2 - s_i = 0 \text{ for } i \in \{0, 1\} \text{ | degree} = 2 +$$ + +$$ +s_0' - s_0 \cdot s_1 = 0 \text{ | degree} = 2 +$$ + +The first two constraints ensure that the value in $s_0$ and $s_1$ are binary, and the third constraint ensures the correctness of the boolean `AND` operation. + +The effect on the rest of the stack is: +* **Left shift** starting from position $2$. + +## OR +Assume $a$ and $b$ are binary values at the top of the stack. The `OR` operation computes $c \leftarrow (a \lor b)$ The diagram below illustrates this graphically. + +![or](../../img/design/stack/field_operations/OR.png) + +Stack transition for this operation must satisfy the following constraints: + +$$ +s_i^2 - s_i = 0 \text{ for } i \in \{0, 1\} \text{ | degree} = 2 +$$ + +$$ +s_{0}' - (s_{1} + s_{0} - s_{1} \cdot s_{0}) = 0 \text{ | degree} = 2 +$$ + +The first two constraints ensure that the value in $s_0$ and $s_1$ are binary, and the third constraint ensures the correctness of the boolean `OR` operation. + +The effect on the rest of the stack is: +* **Left shift** starting from position $2$. + +## EQ +Assume $a$ and $b$ are the elements at the top of the stack. The `EQ` operation computes $c$ such that $c = 1$ if $a = b$, and $0$ otherwise. The diagram below illustrates this graphically. + +![eq](../../img/design/stack/field_operations/EQ.png) + +Stack transition for this operation must satisfy the following constraints: + +$$ +s_0' \cdot (s_0 - s_1) = 0 \text{ | degree} = 2 +$$ + +$$ +s_0' - (1 - (s_0 - s_1) \cdot h_0) = 0 \text{ | degree} = 2 +$$ + +To satisfy the above constraints, the prover must populate the value of helper register $h_0$ as follows: +* If $s_0 \neq s_1$, set $h_0 = \frac{1}{s_0 - s_1}$. +* Otherwise, set $h_0$ to any value (e.g., $0$). + +The effect on the rest of the stack is: +* **Left shift** starting from position $2$. + +## EQZ +Assume $a$ is the element at the top of the stack. The `EQZ` operation computes $b$ such that $b = 1$ if $a = 0$, and $0$ otherwise. The diagram below illustrates this graphically. + +![eqz](../../img/design/stack/field_operations/EQZ.png) + +Stack transition for this operation must satisfy the following constraints: + +$$ +s_0' \cdot s_0 = 0 \text{ | degree} = 2 +$$ + +$$ +s_0' - (1 - s_0 \cdot h_0) = 0 \text{ | degree} = 2 +$$ + +To satisfy the above constraints, the prover must populate the value of helper register $h_0$ as follows: +* If $s_0 \neq 0$, set $h_0 = \frac{1}{s_0}$. +* Otherwise, set $h_0$ to any value (e.g., $0$). + +The effect on the rest of the stack is: +* **No change** starting from position $1$. + +## EXPACC +The `EXPACC` operation computes one round of the expression $base^{exp}$. It is expected that `Expacc` is called at least `num_exp_bits` times, where `num_exp_bits` is the number of bits required to represent `exp`. + +It pops $4$ elements from the top of the stack, performs a single round of exponent aggregation, and pushes the resulting $4$ values onto the stack. The diagram below illustrates this graphically. + +![expacc](../../img/design/stack/field_operations/EXPACC.png) + +Expacc is based on the observation that the exponentiation of a number can be computed by repeatedly squaring the base and multiplying those powers of the base by the accumulator, for the powers of the base which correspond to the exponent's bits which are set to 1. + +For example, take $b^5 = (b^2)^2 \cdot b$. Over the course of 3 iterations ($5$ is $101$ in binary), the algorithm will compute $b$, $b^2$ and $b^4$ (placed in `base_acc`). Hence, we want to multiply `base_acc` in `acc` when $base_acc = b$ and when $base_acc = b^4$, which occurs on the first and third iterations (corresponding to the $1$ bits in the binary representation of 5). + +Stack transition for this operation must satisfy the following constraints: + +`bit'` should be a binary. + +$$ +s_0'^{2} - s_0' = 0 \text{ | degree} = 2 +$$ + +The `base` in the next frame should be the square of the `base` in the current frame. + +$$ +s_1' - s_1^{2} = 0 \text{ | degree} = 2 +$$ + +The value `val` in the helper register is computed correctly using the `bit` and `exp` in next and current frame respectively. + +$$ +h_0 - ((s_1 - 1) * s_0' + 1) = 0 \text{ | degree} = 2 +$$ + +The `acc` in the next frame is the product of `val` and `acc` in the current frame. + +$$ +s_2' - s_2 * h_0 = 0 \text{ | degree} = 2 +$$ + +`exp` in the next frame is half of `exp` in the current frame (accounting for even/odd). + +$$ +s_3 - (s_3' * 2 + s_0') = 0 \text{ | degree} = 1 +$$ + +The effect on the rest of the stack is: +* **No change** starting from position $4$. + +## EXT2MUL +The `EXT2MUL` operation pops top $4$ values from the top of the stack, performs multiplication between the two extension field elements, and pushes the resulting $4$ values onto the stack. The diagram below illustrates this graphically. + +![ext2mul](../../img/design/stack/field_operations/EXT2MUL.png) + +Stack transition for this operation must satisfy the following constraints: + +The first stack element should be unchanged in the next frame. + +$$ +s_0' - s_0 = 0 \text{ | degree} = 1 +$$ + +The second stack element should be unchanged in the next frame. + +$$ +s_1' - s_1 = 0 \text{ | degree} = 1 +$$ + +The third stack element should satisfy the following constraint. + +$$ +s_2' - (s_2 \cdot s_0 + 7 \cdot s_3 \cdot s_1) = 0 \text{ | degree} = 2 +$$ + +The fourth stack element should satisfy the following constraint. + +$$ +s_3' - ((s_2 + s_3) \cdot (s_0 + s_1) - s_2 \cdot s_0 - s_3 \cdot s_1) = 0 \text{ | degree} = 2 +$$ + +The effect on the rest of the stack is: +* **No change** starting from position $4$. diff --git a/versioned_docs/version-0.16/reference/miden-vm/design/stack/index.md b/versioned_docs/version-0.16/reference/miden-vm/design/stack/index.md new file mode 100644 index 00000000..73cbc674 --- /dev/null +++ b/versioned_docs/version-0.16/reference/miden-vm/design/stack/index.md @@ -0,0 +1,238 @@ +--- +title: "Operand Stack" +sidebar_position: 1 +--- + +# Operand stack + +Miden VM is a stack machine. The stack is a push-down stack of practically unlimited depth (in practical terms, the depth will never exceed $2^{32}$), but only the top $16$ items are directly accessible to the VM. Items on the stack are elements in a prime field with modulus $2^{64} - 2^{32} + 1$. + +To keep the constraint system for the stack manageable, we impose the following rules: + +1. All operations executed on the VM can shift the stack by at most one item. That is, the end result of an operation must be that the stack shrinks by one item, grows by one item, or the number of items on the stack stays the same. +2. Stack depth must always be greater than or equal to $16$. At the start of program execution, the stack is initialized with exactly $16$ input values, all of which could be $0$'s. +3. By the end of program execution, exactly $16$ items must remain on the stack (again, all of them could be $0$'s). These items comprise the output of the program. + +To ensure that managing stack depth does not impose significant burden, we adopt the following rule: + +* When the stack depth is $16$, removing additional items from the stack does not change its depth. To keep the depth at $16$, $0$'s are inserted into the deep end of the stack for each removed item. + +## Stack representation + +The VM allocates $19$ trace columns for the stack. The layout of the columns is illustrated below. + +![trace_layout](../../img/design/stack/trace_layout.png) + +The meaning of the above columns is as follows: + +* $s_0 ... s_{15}$ are the columns representing the top $16$ slots of the stack. +* Column $b_0$ contains the number of items on the stack (i.e., the stack depth). In the above picture, there are 16 items on the stacks, so $b_0 = 16$. +* Column $b_1$ contains an address of a row in the "overflow table" in which we'll store the data that doesn't fit into the top $16$ slots. When $b_1 = 0$, it means that all stack data fits into the top $16$ slots of the stack. +* Helper column $h_0$ is used to ensure that stack depth does not drop below $16$. Values in this column are set by the prover non-deterministically to $\frac{1}{b_0 - 16}$ when $b_0 \neq 16$, and to any other value otherwise. + +### Overflow table + +To keep track of the data which doesn't fit into the top $16$ stack slots, we'll use an overflow table. This will be a [virtual table](../lookups/multiset.md#virtual-tables). To represent this table, we'll use a single auxiliary column $p_1$ (named `p1` in the codebase). + +The table itself can be thought of as having 3 columns as illustrated below. + +![overflow_table_layout](../../img/design/stack/overflow_table_layout.png) + +The meaning of the columns is as follows: + +* Column $t_0$ contains row address. Every address in the table must be unique. +* Column $t_1$ contains the value that overflowed the stack. +* Column $t_2$ contains the address of the row containing the value that overflowed the stack right before the value in the current row. For example, in the picture above, first value $a$ overflowed the stack, then $b$ overflowed the stack, and then value $c$ overflowed the stack. Thus, row with value $b$ points back to the row with value $a$, and row with value $c$ points back to the row with value $b$. + +To reduce a table row to a single value, we'll compute a randomized product of column values as follows: + +$$ +r_i = \alpha_0 + \alpha_1 \cdot t_{0, i} + \alpha_2 \cdot t_{1, i} + \alpha_3 \cdot t_{2, i} +$$ + +Then, when row $i$ is added to the table, we'll update the value in the $p_1$ column like so: + +$$ +p_1' = p_1 \cdot r_i +$$ + +Analogously, when row $i$ is removed from the table, we'll update the value in column $p_1$ like so: + +$$ +p_1' = \frac{p_1}{r_i} +$$ + +The initial value of $p_1$ is set to $1$. Thus, if by the time Miden VM finishes executing a program the table is empty (we added and then removed exactly the same set of rows), $p_1$ will also be equal to $1$. + +There are a couple of other rules we'll need to enforce: + +* We can delete a row only after the row has been inserted into the table. +* We can't insert a row with the same address twice into the table (even if the row was inserted and then deleted). + +How these are enforced will be described a bit later. + +## Right shift + +If an operation adds data to the stack, we say that the operation caused a right shift. For example, `PUSH` and `DUP` operations cause a right shift. Graphically, this looks like so: + +![stack_right_shift](../../img/design/stack/stack_right_shift.png) + +Here, we pushed value $v_{17}$ onto the stack. All other values on the stack are shifted by one slot to the right and the stack depth increases by $1$. There is not enough space at the top of the stack for all $17$ values, thus, $v_1$ needs to be moved to the overflow table. + +To do this, we need to rely on another column: $clk$. This is a system column which keeps track of the current VM cycle. The value in this column is simply incremented by $1$ with every step. + +The row we want to add to the overflow table is defined by tuple $(clk, v1, 0)$, and after it is added, the table would look like so: + +![stack_overflow_table_post_1_right_shift](../../img/design/stack/stack_overflow_table_post_1_right_shift.png) + +The reason we use VM clock cycle as row address is that the clock cycle is guaranteed to be unique, and thus, the same row can not be added to the table twice. + +Let's push another item onto the stack: + +![stack_overflow_push_2nd_item](../../img/design/stack/stack_overflow_push_2nd_item.png) + +Again, as we push $v_{18}$ onto the stack, all items on the stack are shifted to the right, and now $v_2$ needs to be moved to the overflow table. The tuple we want to insert into the table now is $(clk+1, v2, clk)$. After the operation, the overflow table will look like so: + +![stack_overflow_table_post_2_right_shift](../../img/design/stack/stack_overflow_table_post_2_right_shift.png) + +Notice that $t_2$ for row which contains value $v_2$ points back to the row with address $clk$. + +Overall, during a right shift we do the following: + +* Increment stack depth by $1$. +* Shift stack columns $s_0, ..., s_{14}$ right by $1$ slot. +* Add a row to the overflow table described by tuple $(clk, s_{15}, b_0)$. +* Set the next value of $b_1$ to the current value of $clk$. + +Also, as mentioned previously, the prover sets values in $h_0$ non-deterministically to $\frac{1}{b_0 - 16}$. + +## Left shift + +If an operation removes an item from the stack, we say that the operation caused a left shift. For example, a `DROP` operation causes a left shift. Assuming the stack is in the state we left it at the end of the previous section, graphically, this looks like so: + +![stack_1st_left_shift](../../img/design/stack/stack_1st_left_shift.png) + +Overall, during the left shift we do the following: + +* When stack depth is greater than $16$: + * Decrement stack depth by $1$. + * Shift stack columns $s_1, ..., s_{15}$ left by $1$ slot. + * Remove a row from the overflow table with $t_0$ equal to the current value of $b_1$. + * Set the next value of $s_{15}$ to the value in $t_1$ of the removed overflow table row. + * Set the next value of $b_1$ to the value in $t_2$ of the removed overflow table row. +* When the stack depth is equal to $16$: + * Keep the stack depth the same. + * Shift stack columns $s_1, ..., s_{15}$ left by $1$ slot. + * Set the value of $s_{15}$ to $0$. + * Set the value to $h_0$ to $0$ (or any other value). + +If the stack depth becomes (or remains) $16$, the prover can set $h_0$ to any value (e.g., $0$). But if the depth is greater than $16$ the prover sets $h_0$ to $\frac{1}{b_0 - 16}$. + +## AIR Constraints + +To simplify constraint descriptions, we'll assume that the VM exposes two binary flag values described below. + +| Flag | Degree | Description | +| --------- | ------ | ------------------------------------------------------------------------------------------------ | +| $f_{shr}$ | 6 | When this flag is set to $1$, the instruction executing on the VM is performing a "right shift". | +| $f_{shl}$ | 5 | When this flag is set to $1$, the instruction executing on the VM is performing a "left shift". | + +These flags are mutually exclusive. That is, if $f_{shl}=1$, then $f_{shr}=0$ and vice versa. However, both flags can be set to $0$ simultaneously. This happens when the executed instruction does not shift the stack. How these flags are computed is described [here](./op_constraints.md). + +We also use a combined call-entry flag $f_{enter}$ to denote entry into a new execution context. +Here, $f_{enter} = f_{call} + f_{dyncall} + f_{syscall}$. The END-of-call transition is +validated by the block stack table constraints and is not handled by the stack depth rule below. + +### Stack overflow flag + +Additionally, we'll define a flag to indicate whether the overflow table contains values. This flag will be set to $0$ when the overflow table is empty, and to $1$ otherwise (i.e., when stack depth $>16$). This flag can be computed as follows: + +$$ +f_{ov} = (b_0 - 16) \cdot h_0 \text{ | degree} = 2 +$$ + +To ensure that this flag is set correctly, we need to impose the following constraint: + +$$ +(1 - f_{ov}) \cdot (b_0 - 16) = 0 \text{ | degree} = 3 +$$ + +The above constraint can be satisfied only when either of the following holds: + +* $b_0 = 16$, in which case $f_{ov}$ evaluates to $0$, regardless of the value of $h_0$. +* $f_{ov} = 1$, in which case $b_0$ cannot be equal to $16$ (and $h_0$ must be set to $\frac{1}{b_0 - 16}$). + +### Stack depth constraints +To make sure stack depth column $b_0$ is updated correctly, we need to impose the following constraints: + +| Condition | Constraint__ | Description | +| --------------------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------- | +| $f_{shr}=1$ | $b'_0 = b_0 + 1$ | When the stack is shifted to the right, stack depth should be incremented by $1$. | +| $f_{shl}=1$
$f_{ov}=1$ | $b'_0 = b_0 - 1$ | When the stack is shifted to the left and the overflow table is not empty, stack depth should be decremented by $1$. | +| $f_{enter}=1$ | $b'_0 = 16$ | On CALL/SYSCALL/DYNCALL entry, the stack depth resets to the accessible top 16 positions. | +| otherwise | $b'_0 = b_0$ | In all other cases, stack depth should not change. | + +For non-call rows (no CALL/SYSCALL/DYNCALL entry and no END-of-call), we can combine the shift +constraints into a single expression as follows: + +$$ +b'_0 - b_0 + f_{shl} \cdot f_{ov} - f_{shr} = 0 \text{ | degree} = 7 +$$ + +On CALL/SYSCALL/DYNCALL entry, we instead enforce $b'_0 = 16$ via a dedicated term. END-of-call +depth updates are handled by the block stack table constraints. + +### Overflow table constraints + +When the stack is shifted to the right, a tuple $(clk, s_{15}, b_1)$ should be added to the overflow table. We will denote value of the row to be added to the table as follows: + +$$ +v = \alpha_0 + \alpha_1 \cdot clk + \alpha_2 \cdot s_{15} + \alpha_3 \cdot b_1 +$$ + +When the stack is shifted to the left, a tuple $(b_1, s'_{15}, b'_1)$ should be removed from the overflow table. We will denote value of the row to be removed from the table as follows. + +$$ +u = \alpha_0 + \alpha_1 \cdot b_1 + \alpha_2 \cdot s'_{15} + \alpha_3 \cdot b'_1 +$$ + +When the operation is DYNCALL and the overflow table is non-empty, we also remove one row, but +the "prev" value comes from decoder hasher state/helper element 5 instead of $b'_1$. + +Using the above variables, we can ensure that right and left shifts update the overflow table correctly by enforcing the following constraint: + +$$ +p_1' \cdot (u \cdot f_{shl} \cdot f_{ov} + 1 - f_{shl} \cdot f_{ov}) = p_1 \cdot (v \cdot f_{shr} + 1 - f_{shr}) \text{ | degree} = 9 +$$ + +For DYNCALL, the same structure applies with $f_{dyncall}$ in place of $f_{shl}$ and with $u$ +defined using the hasher-state "prev" value described above. + +The above constraint reduces to the following under various flag conditions: + +| Condition | Applied constraint | +| -------------------------------------------------- | -------------------- | +| $f_{shl}=1$, $f_{shr}=0$, $f_{ov}=0$ | $p_1' = p_1$ | +| $f_{shl}=1$, $f_{shr}=0$, $f_{ov}=1$ | $p_1' \cdot u = p_1$ | +| $f_{shl}=0$, $f_{shr}=1$, $f_{ov}=1 \text{ or } 0$ | $p_1' = p_1 \cdot v$ | +| $f_{shl}=0$, $f_{shr}=0$, $f_{ov}=1 \text{ or } 0$ | $p_1' = p_1$ | + +Notice that in the case of the left shift, the constraint forces the prover to set the next values of $s_{15}$ and $b_1$ to values $t_1$ and $t_2$ of the row removed from the overflow table. + +In case of a right shift, we also need to make sure that the next value of $b_1$ is set to the current value of $clk$. This can be done with the following constraint: + +$$ +f_{shr} \cdot (b'_1 - clk) = 0 \text{ | degree} = 7 +$$ + +In case of a left shift, when the overflow table is empty, we need to make sure that a $0$ is "shifted in" from the right (i.e., $s_{15}$ is set to $0$). This can be done with the following constraint: + +$$ +f_{shl} \cdot (1 - f_{ov}) \cdot s_{15}' = 0 \text{ | degree} = 8 +$$ + +### Boundary constraints +In addition to the constraints described above, we also need to enforce the following boundary constraints: +* $b_0 = 16$ at the first and at the last row of execution trace. +* $b_1 = 0$ at the first and at the last row of execution trace. +* $p_1 = 1$ at the first and at the last row of execution trace. diff --git a/versioned_docs/version-0.16/reference/miden-vm/design/stack/io_ops.md b/versioned_docs/version-0.16/reference/miden-vm/design/stack/io_ops.md new file mode 100644 index 00000000..2c03d6de --- /dev/null +++ b/versioned_docs/version-0.16/reference/miden-vm/design/stack/io_ops.md @@ -0,0 +1,251 @@ +--- +title: "Input / Output Operations" +sidebar_position: 7 +--- + +# Input / output operations +In this section we describe the AIR constraints for Miden VM input / output operations. These operations move values between the stack and other components of the VM such as program code (i.e., decoder), memory, and advice provider. + +### PUSH +The `PUSH` operation pushes the provided immediate value onto the stack non-deterministically (i.e., sets the value of $s_0$ register); it is the responsibility of the [Op Group Table](../decoder/index.md#op-group-table) to ensure that the correct value was pushed on the stack. The semantics of this operation are explained in the [decoder section](../decoder/index.md#handling-immediate-values). + +The effect of this operation on the rest of the stack is: +* **Right shift** starting from position $0$. + +### SDEPTH +Assume $a$ is the current depth of the stack stored in the stack bookkeeping register $b_0$ (as described [here](./index.md#stack-representation)). The `SDEPTH` pushes $a$ onto the stack. The diagram below illustrates this graphically. + +![sdepth](../../img/design/stack/io_ops/SDEPTH.png) + +Stack transition for this operation must satisfy the following constraints: + +$$ +s_0' - b_0 = 0 \text{ | degree} = 1 +$$ + +The effect of this operation on the rest of the stack is: +* **Right shift** starting from position $0$. + +### ADVPOP +Assume $a$ is an element at the top of the advice stack. The `ADVPOP` operation removes $a$ from the advice stack and pushes it onto the operand stack. The diagram below illustrates this graphically. + +![advpop](../../img/design/stack/io_ops/ADVPOP.png) + +The `ADVPOP` operation does not impose any constraints against the first element of the operand stack. + +The effect of this operation on the rest of the operand stack is: +* **Right shift** starting from position $0$. + +### ADVPOPW +Assume $a$, $b$, $c$, and $d$, are the elements at the top of the advice stack (with $a$ being on top). The `ADVPOPW` operation removes these elements from the advice stack and puts them onto the operand stack by overwriting the top $4$ stack elements. The diagram below illustrates this graphically. + +![advpopw](../../img/design/stack/io_ops/ADVPOPW.png) + +The `ADVPOPW` operation does not impose any constraints against the top $4$ elements of the operand stack. + +The effect of this operation on the rest of the operand stack is: +* **No change** starting from position $4$. + +## Memory access operations +Miden VM exposes several operations for reading from and writing to random access memory. Memory in Miden VM is managed by the [Memory chiplet](../chiplets/memory.md). + +Communication between the stack and the memory chiplet is accomplished via the chiplet bus $b_{chip}$. To make requests to the chiplet bus we need to divide its current value by the value representing memory access request. The structure of memory access request value is described [here](../chiplets/memory.md#memory-row-value). + +To enforce the correctness of memory access, we can use the following constraint: + +$$ +b_{chip}' \cdot u_{mem} = b_{chip} +$$ + +In the above, $u_{mem}$ is the value of the memory access request. The effective degree of this +constraint is $1 + \deg(u_{mem})$. Thus, to describe AIR constraint for memory operations, it is +sufficient to describe how $u_{mem}$ is computed. We do this in the following sections. + +### MLOADW +Assume that the word with elements $v_0, v_1, v_2, v_3$ is located in memory starting at address $a$. The `MLOADW` operation pops an element off the stack, interprets it as a memory address, and replaces the remaining 4 elements at the top of the stack with values located at the specified address. The diagram below illustrates this graphically. + +![mloadw](../../img/design/stack/io_ops/MLOADW.png) + +To simplify description of the memory access request value, we first define a variable for the value that represents the state of memory after the operation: + +$$ +v = \sum_{i=0}^3\alpha_{i+5} \cdot s_i' +$$ + +Using the above variable, we define the value representing the memory access request as follows: + +$$ +u_{mem} = \alpha_0 + \alpha_1 \cdot op_{mem\_readword} + \alpha_2 \cdot ctx + \alpha_3 \cdot s_0 + \alpha_4 \cdot clk + v +$$ + +In the above: +- $op_{mem\_readword}$ is the unique [operation label](../chiplets/index.md#operation-labels) of the memory "read word" operation. +- $ctx$ is the identifier of the current memory context. +- $s_0$ is the memory address from which the values are to be loaded onto the stack. +- $clk$ is the current clock cycle of the VM. + +The effect of this operation on the rest of the stack is: +* **Left shift** starting from position $5$. + +### MLOAD +Assume that the element $v$ is located in memory at address $a$. The `MLOAD` operation pops an element off the stack, interprets it as a memory address, and pushes the element located at the specified address to the stack. The diagram below illustrates this graphically. + +![mload](../../img/design/stack/io_ops/MLOAD.png) + + +We define the value representing the memory access request as follows: + +$$ +u_{mem} = \alpha_0 + \alpha_1 \cdot op_{mem\_readelement} + \alpha_2 \cdot ctx + \alpha_3 \cdot s_0 + \alpha_4 \cdot clk + \alpha_5 \cdot s_0' +$$ + +In the above: +- $op_{mem\_readelement}$ is the unique [operation label](../chiplets/index.md#operation-labels) of the memory "read element" operation. +- $ctx$ is the identifier of the current memory context. +- $s_0$ is the memory address from which the value is to be loaded onto the stack. +- $clk$ is the current clock cycle of the VM. + +The effect of this operation on the rest of the stack is: +* **No change** starting from position $1$. + +### MSTOREW +The `MSTOREW` operation pops an element off the stack, interprets it as a memory address, and writes the remaining $4$ elements at the top of the stack into memory starting at the specified address. The stored elements are not removed from the stack. The diagram below illustrates this graphically. + +![mstorew](../../img/design/stack/io_ops/MSTOREW.png) + +After the operation the contents of memory at addresses $a$, $a+1$, $a+2$, $a+3$ would be set to $v_0, v_1, v_2, v_3$, respectively. + +To simplify description of the memory access request value, we first define a variable for the value that represents the state of memory after the operation: + +$$ +v = \sum_{i=0}^3\alpha_{i+5} \cdot s_i' +$$ + +Using the above variable, we define the value representing the memory access request as follows: + +$$ +u_{mem} = \alpha_0 + \alpha_1 \cdot op_{mem\_writeword} + \alpha_2 \cdot ctx + \alpha_3 \cdot s_0 + \alpha_4 \cdot clk + v +$$ + +In the above: +- $op_{mem\_writeword}$ is the unique [operation label](../chiplets/index.md#operation-labels) of the memory "write word" operation. +- $ctx$ is the identifier of the current memory context. +- $s_0$ is the memory address into which the values from the stack are to be saved. +- $clk$ is the current clock cycle of the VM. + +The effect of this operation on the rest of the stack is: +* **Left shift** starting from position $1$. + +### MSTORE +The `MSTORE` operation pops an element off the stack, interprets it as a memory address, and writes the remaining element at the top of the stack into memory at the specified memory address. The diagram below illustrates this graphically. + +![mstore](../../img/design/stack/io_ops/MSTORE.png) + +After the operation the contents of memory at address $a$ would be set to $b$. + +We define the value representing the memory access request as follows: + +$$ +u_{mem} = \alpha_0 + \alpha_1 \cdot op_{mem\_writeelement} + \alpha_2 \cdot ctx + \alpha_3 \cdot s_0 + \alpha_4 \cdot clk + \alpha_5 \cdot s_0' +$$ + +In the above: +- $op_{mem\_writeelement} $ is the unique [operation label](../chiplets/index.md#operation-labels) of the memory "write element" operation. +- $ctx$ is the identifier of the current memory context. +- $s_0$ is the memory address into which the value from the stack is to be saved. +- $clk$ is the current clock cycle of the VM. + +The effect of this operation on the rest of the stack is: +* **Left shift** starting from position $1$. + +### MSTREAM + +The `MSTREAM` operation loads two words from memory, and replaces the top 8 elements of the stack with them, element-wise, in stack order. The start memory address from which the words are loaded is stored in the 13th stack element (position 12). The diagram below illustrates this graphically. + +![mstream](../../img/design/stack/io_ops/MSTREAM.png) + +After the operation, the memory address is incremented by 8. + +$$ +s_{12}' = s_{12} + 8 +$$ + +To simplify description of the memory access request value, we first define variables for the values that represent the state of memory after the operation: + +$$ +v_1 = \sum_{i=0}^3\alpha_{i+5} \cdot s_i' +$$ + +$$ +v_2 = \sum_{i=0}^3\alpha_{i+5} \cdot s_{i+4}' +$$ + +Using the above variables, we define the values representing the memory access request as follows: + +$$ +u_{mem, 1} = \alpha_0 + \alpha_1 \cdot op_{mem\_readword} + \alpha_2 \cdot ctx + \alpha_3 \cdot s_{12} + \alpha_4 \cdot clk + v_1 +$$ + +$$ +u_{mem, 2} = \alpha_0 + \alpha_1 \cdot op_{mem\_readword} + \alpha_2 \cdot ctx + \alpha_3 \cdot (s_{12} + 4) + \alpha_4 \cdot clk + v_2 +$$ + +$$ +u_{mem} = u_{mem, 1} \cdot u_{mem, 2} +$$ + +In the above: +- $op_{mem\_readword}$ is the unique [operation label](../chiplets/index.md#operation-labels) of the memory "read word" operation. +- $ctx$ is the identifier of the current memory context. +- $s_{12}$ and $s_{12} + 4$ are the memory addresses from which the words are to be loaded onto the stack. +- $clk$ is the current clock cycle of the VM. + +The effect of this operation on the rest of the stack is: +* **No change** starting from position $8$ except position $12$. + +### PIPE +The `PIPE` operation (assembly instruction `adv_pipe`) pops two words from the +advice stack, writes them to memory, and overwrites the top 8 stack elements +with these words. The destination address for the first word is stored in stack +position $12$, and is incremented by 8. + +$$ +s_{12}' = s_{12} + 8 +$$ + +To simplify description of the memory access request value, we first define +variables for the values that represent the state of memory after the operation: + +$$ +v_1 = \sum_{i=0}^3\alpha_{i+5} \cdot s_i' +$$ + +$$ +v_2 = \sum_{i=0}^3\alpha_{i+5} \cdot s_{i+4}' +$$ + +Using the above variables, we define the values representing the memory access +requests as follows: + +$$ +u_{mem,1} = \alpha_0 + \alpha_1 \cdot op_{mem\_writeword} + \alpha_2 \cdot ctx + +\alpha_3 \cdot s_{12} + \alpha_4 \cdot clk + v_1 +$$ + +$$ +u_{mem,2} = \alpha_0 + \alpha_1 \cdot op_{mem\_writeword} + \alpha_2 \cdot ctx + +\alpha_3 \cdot (s_{12} + 4) + \alpha_4 \cdot clk + v_2 +$$ + +$$ +u_{mem} = u_{mem,1} \cdot u_{mem,2} +$$ + +In the above: +- $op_{mem\_writeword}$ is the unique [operation label](../chiplets/index.md#operation-labels) + of the memory "write word" operation. +- $s_{12}$ and $s_{12} + 4$ are the memory addresses for the two words. +- $clk$ is the current clock cycle of the VM. + +The effect of this operation on the rest of the stack is: +* **No change** starting from position $8$ except position $12$. diff --git a/versioned_docs/version-0.16/reference/miden-vm/design/stack/op_constraints.md b/versioned_docs/version-0.16/reference/miden-vm/design/stack/op_constraints.md new file mode 100644 index 00000000..7f30ad4e --- /dev/null +++ b/versioned_docs/version-0.16/reference/miden-vm/design/stack/op_constraints.md @@ -0,0 +1,306 @@ +--- +title: "Stack Operation Constraints" +sidebar_position: 2 +--- + +# Stack operation constraints + +In addition to the constraints described in the previous section, we need to impose constraints to check that each VM operation is executed correctly. + +For this purpose the VM exposes a set of operation-specific flags. These flags are set to $1$ when a given operation is executed, and to $0$ otherwise. The naming convention for these flags is $f_{opname}$. For example, $f_{dup}$ would be set to $1$ when `DUP` operation is executed, and to $0$ otherwise. Operation flags are discussed in detail in the section [below](#operation-flags). + +To describe how operation-specific constraints work, let's use an example with `DUP` operation. This operation pushes a copy of the top stack item onto the stack. The constraints we need to impose for this operation are as follows: + +$$ +f_{dup} \cdot (s'_0 - s_0) = 0 +f_{dup} \cdot (s'_{i+1} - s_i) = 0 \ \text{ for } i \in [0, 15) +$$ + +The first constraint enforces that the top stack item in the next row is the same as the top stack item in the current row. The second constraint enforces that all stack items (starting from item $0$) are shifted to the right by $1$. We also need to impose all the constraints discussed in the previous section, be we omit them here. + +Let's write similar constraints for `DUP1` operation, which pushes a copy of the second stack item onto the stack: + +$$ +f_{dup1} \cdot (s'_0 - s_1) = 0 +f_{dup1} \cdot (s'_{i+1} - s_i) = 0 \ \text{ for } i \in [0, 15) +$$ + +It is easy to notice that while the first constraint changed, the second constraint remained the same - i.e., we are still just shifting the stack to the right. + +In fact, for most operations it makes sense to make a distinction between constraints unique to the operation vs. more general constraints which enforce correct behavior for the stack items not affected by the operation. In the subsequent sections we describe in detail only the former constraints, and provide high-level descriptions of the more general constraints. Specifically, we indicate how the operation affects the rest of the stack (e.g., shifts right starting from position $0$). + +## Operation flags +As mentioned above, operation flags are used as selectors to enforce operation-specific constraints. That is, they turn on relevant constraints for a given operation. The VM defines operation flags over all 7-bit opcodes (with some combinations not valid due to the need for degree reduction of some op flags), though several opcodes are currently unused. + +Operation flags are mutually exclusive. That is, if one flag is set to $1$, all other flags are set to $0$. Also, one of the flags is always guaranteed to be set to $1$. + +To compute values of operation flags we use _op bits_ registers located in the [decoder](../decoder/index.md#decoder-trace). These registers contain binary representations of operation codes (opcodes). Each opcode consists of $7$ bits, and thus, there are $7$ _op bits_ registers. We denote these registers as $b_0, ..., b_6$. The values are computed by multiplying the op bit registers in various combinations. Notice that binary encoding down below is showed in big-endian order, so the flag bits correspond to the reverse order of the _op bits_ registers, from $b_6$ to $b_0$. + +For example, the value of the flag for `NOOP`, which is encoded as `0000000`, is computed as follows: + +$$ +f_{noop} = (1 - b_6) \cdot (1 - b_5) \cdot (1 - b_4) \cdot (1 - b_3) \cdot (1 - b_2) \cdot (1 - b_1) \cdot (1 - b_0) +$$ + +While the value of the `DROP` operation, which is encoded as `0101001` is computed as follows: + +$$ +f_{drop} = (1 - b_6) \cdot b_5 \cdot (1 - b_4) \cdot b_3 \cdot (1 - b_2) \cdot (1 - b_1) \cdot b_0 +$$ + +As can be seen from above, the degree for both of these flags is $7$. Since degree of constraints in Miden VM can go up to $9$, this means that operation-specific constraints cannot exceed degree $2$. However, there are some operations which require constraints of higher degree (e.g., $3$ or even $5$). To support such constraints, we adopt the following scheme. + +We organize the operations into $4$ groups as shown below and also introduce two extra registers $e_0$ and $e_1$ for degree reduction: + +| $b_6$ | $b_5$ | $b_4$ | $b_3$ | $b_2$ | $b_1$ | $b_0$ | $e_0$ | $e_1$ | # of ops | degree | +| :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :------: | :----: | +| 0 | x | x | x | x | x | x | 0 | 0 | 64 | 7 | +| 1 | 0 | 0 | x | x | x | - | 0 | 0 | 8 | 6 | +| 1 | 0 | 1 | x | x | x | x | 1 | 0 | 16 | 5 | +| 1 | 1 | x | x | x | - | - | 0 | 1 | 8 | 4 | + +In the above: +* Operation flags for operations in the first group (with prefix `0`), are computed using all $7$ op bits, and thus their degree is $7$. +* Operation flags for operations in the second group (with prefix `100`), are computed using only the first $6$ op bits, and thus their degree is $6$. +* Operation flags for operations in the third group (with prefix `101`), are computed using all $7$ op bits. We use the extra register $e_0$ (which is set to $b_6 \cdot (1-b_5) \cdot b_4$) to reduce the degree by $2$. Thus, the degree of op flags in this group is $5$. +* Operation flags for operations in the fourth group (with prefix `11`), are computed using only the first $5$ op bits. We use the extra register $e_1$ (which is set to $b_6 \cdot b_5$) to reduce the degree by $1$. Thus, the degree of op flags in this group is $4$. + +How operations are distributed between these $4$ groups is described in the sections below. + +### No stack shift operations +This group contains $32$ operations which do not shift the stack (this is almost all such operations). Since the op flag degree for these operations is $7$, constraints for these operations cannot exceed degree $2$. + +| Operation | Opcode value | Binary encoding | Operation group | Flag degree | +|-----------|:------------:|:---------------:|:-----------------------------:|:-----------:| +| `NOOP` | $0$ | `000_0000` | [System ops](./system_ops.md) | $7$ | +| `EQZ` | $1$ | `000_0001` | [Field ops](./field_ops.md) | $7$ | +| `NEG` | $2$ | `000_0010` | [Field ops](./field_ops.md) | $7$ | +| `INV` | $3$ | `000_0011` | [Field ops](./field_ops.md) | $7$ | +| `INCR` | $4$ | `000_0100` | [Field ops](./field_ops.md) | $7$ | +| `NOT` | $5$ | `000_0101` | [Field ops](./field_ops.md) | $7$ | +| ``| $6$ | `000_0110` | | $7$ | +| `MLOAD` | $7$ | `000_0111` | [I/O ops](./io_ops.md) | $7$ | +| `SWAP` | $8$ | `000_1000` | [Stack ops](./stack_ops.md) | $7$ | +| `CALLER` | $9$ | `000_1001` | [System ops](./system_ops.md) | $7$ | +| `MOVUP2` | $10$ | `000_1010` | [Stack ops](./stack_ops.md) | $7$ | +| `MOVDN2` | $11$ | `000_1011` | [Stack ops](./stack_ops.md) | $7$ | +| `MOVUP3` | $12$ | `000_1100` | [Stack ops](./stack_ops.md) | $7$ | +| `MOVDN3` | $13$ | `000_1101` | [Stack ops](./stack_ops.md) | $7$ | +| `ADVPOPW` | $14$ | `000_1110` | [I/O ops](./io_ops.md) | $7$ | +| `EXPACC` | $15$ | `000_1111` | [Field ops](./field_ops.md) | $7$ | +| `MOVUP4` | $16$ | `001_0000` | [Stack ops](./stack_ops.md) | $7$ | +| `MOVDN4` | $17$ | `001_0001` | [Stack ops](./stack_ops.md) | $7$ | +| `MOVUP5` | $18$ | `001_0010` | [Stack ops](./stack_ops.md) | $7$ | +| `MOVDN5` | $19$ | `001_0011` | [Stack ops](./stack_ops.md) | $7$ | +| `MOVUP6` | $20$ | `001_0100` | [Stack ops](./stack_ops.md) | $7$ | +| `MOVDN6` | $21$ | `001_0101` | [Stack ops](./stack_ops.md) | $7$ | +| `MOVUP7` | $22$ | `001_0110` | [Stack ops](./stack_ops.md) | $7$ | +| `MOVDN7` | $23$ | `001_0111` | [Stack ops](./stack_ops.md) | $7$ | +| `SWAPW` | $24$ | `001_1000` | [Stack ops](./stack_ops.md) | $7$ | +| `EXT2MUL` | $25$ | `001_1001` | [Field ops](./field_ops.md) | $7$ | +| `MOVUP8` | $26$ | `001_1010` | [Stack ops](./stack_ops.md) | $7$ | +| `MOVDN8` | $27$ | `001_1011` | [Stack ops](./stack_ops.md) | $7$ | +| `SWAPW2` | $28$ | `001_1100` | [Stack ops](./stack_ops.md) | $7$ | +| `SWAPW3` | $29$ | `001_1101` | [Stack ops](./stack_ops.md) | $7$ | +| `SWAPDW` | $30$ | `001_1110` | [Stack ops](./stack_ops.md) | $7$ | +| `EMIT` | $31$ | `001_1111` | [System ops](./system_ops.md) | $7$ | + +### Left stack shift operations +This group contains $16$ operations which shift the stack to the left (i.e., remove an item from the stack). Most of left-shift operations are contained in this group. Since the op flag degree for these operations is $7$, constraints for these operations cannot exceed degree $2$. + +| Operation | Opcode value | Binary encoding | Operation group | Flag degree | +| ----------- | :----------: | :-------------: | :---------------------------: | :---------: | +| `ASSERT` | $32$ | `010_0000` | [System ops](./system_ops.md) | $7$ | +| `EQ` | $33$ | `010_0001` | [Field ops](./field_ops.md) | $7$ | +| `ADD` | $34$ | `010_0010` | [Field ops](./field_ops.md) | $7$ | +| `MUL` | $35$ | `010_0011` | [Field ops](./field_ops.md) | $7$ | +| `AND` | $36$ | `010_0100` | [Field ops](./field_ops.md) | $7$ | +| `OR` | $37$ | `010_0101` | [Field ops](./field_ops.md) | $7$ | +| `U32AND` | $38$ | `010_0110` | [u32 ops](./u32_ops.md) | $7$ | +| `U32XOR` | $39$ | `010_0111` | [u32 ops](./u32_ops.md) | $7$ | +| `FRIE2F4` | $40$ | `010_1000` | [Crypto ops](./crypto_ops.md) | $7$ | +| `DROP` | $41$ | `010_1001` | [Stack ops](./stack_ops.md) | $7$ | +| `CSWAP` | $42$ | `010_1010` | [Stack ops](./stack_ops.md) | $7$ | +| `CSWAPW` | $43$ | `010_1011` | [Stack ops](./stack_ops.md) | $7$ | +| `MLOADW` | $44$ | `010_1100` | [I/O ops](./io_ops.md) | $7$ | +| `MSTORE` | $45$ | `010_1101` | [I/O ops](./io_ops.md) | $7$ | +| `MSTOREW` | $46$ | `010_1110` | [I/O ops](./io_ops.md) | $7$ | +| `` | $47$ | `010_1111` | | $7$ | + +### Right stack shift operations +This group contains $16$ operations which shift the stack to the right (i.e., push a new item onto the stack). Most of right-shift operations are contained in this group. Since the op flag degree for these operations is $7$, constraints for these operations cannot exceed degree $2$. + +| Operation | Opcode value | Binary encoding | Operation group | Flag degree | +| --------- | :----------: | :-------------: | :---------------------------: | :---------: | +| `PAD` | $48$ | `011_0000` | [Stack ops](./stack_ops.md) | $7$ | +| `DUP` | $49$ | `011_0001` | [Stack ops](./stack_ops.md) | $7$ | +| `DUP1` | $50$ | `011_0010` | [Stack ops](./stack_ops.md) | $7$ | +| `DUP2` | $51$ | `011_0011` | [Stack ops](./stack_ops.md) | $7$ | +| `DUP3` | $52$ | `011_0100` | [Stack ops](./stack_ops.md) | $7$ | +| `DUP4` | $53$ | `011_0101` | [Stack ops](./stack_ops.md) | $7$ | +| `DUP5` | $54$ | `011_0110` | [Stack ops](./stack_ops.md) | $7$ | +| `DUP6` | $55$ | `011_0111` | [Stack ops](./stack_ops.md) | $7$ | +| `DUP7` | $56$ | `011_1000` | [Stack ops](./stack_ops.md) | $7$ | +| `DUP9` | $57$ | `011_1001` | [Stack ops](./stack_ops.md) | $7$ | +| `DUP11` | $58$ | `011_1010` | [Stack ops](./stack_ops.md) | $7$ | +| `DUP13` | $59$ | `011_1011` | [Stack ops](./stack_ops.md) | $7$ | +| `DUP15` | $60$ | `011_1100` | [Stack ops](./stack_ops.md) | $7$ | +| `ADVPOP` | $61$ | `011_1101` | [I/O ops](./io_ops.md) | $7$ | +| `SDEPTH` | $62$ | `011_1110` | [I/O ops](./io_ops.md) | $7$ | +| `CLK` | $63$ | `011_1111` | [System ops](./system_ops.md) | $7$ | + +### u32 operations +This group contains $8$ u32 operations. These operations are grouped together because all of them require range checks. The constraints for range checks are of degree $5$, however, since all these operations require them, we can define a flag with common prefix `100` to serve as a selector for the range check constraints. The value of this flag is computed as follows: + +$$ +f_{u32rc} = b_6 \cdot (1 - b_5) \cdot (1 - b_4) +$$ + +The degree of this flag is $3$, which is acceptable for a selector for degree $5$ constraints. + +| Operation | Opcode value | Binary encoding | Operation group | Flag degree | +| ------------ | :----------: | :-------------: | :---------------------: | :---------: | +| `U32ADD` | $64$ | `100_0000` | [u32 ops](./u32_ops.md) | $6$ | +| `U32SUB` | $66$ | `100_0010` | [u32 ops](./u32_ops.md) | $6$ | +| `U32MUL` | $68$ | `100_0100` | [u32 ops](./u32_ops.md) | $6$ | +| `U32DIV` | $70$ | `100_0110` | [u32 ops](./u32_ops.md) | $6$ | +| `U32SPLIT` | $72$ | `100_1000` | [u32 ops](./u32_ops.md) | $6$ | +| `U32ASSERT2` | $74$ | `100_1010` | [u32 ops](./u32_ops.md) | $6$ | +| `U32ADD3` | $76$ | `100_1100` | [u32 ops](./u32_ops.md) | $6$ | +| `U32MADD` | $78$ | `100_1110` | [u32 ops](./u32_ops.md) | $6$ | + +As mentioned previously, the last bit of the opcode is not used in computation of the flag for these operations. We force this bit to always be set to $0$ with the following constraint: + +$$ +b_6 \cdot (1 - b_5) \cdot (1 - b_4) \cdot b_0 = 0 \text{ | degree} = 4 +$$ + +Putting these operations into a group with flag degree $6$ is important for two other reasons: +* Constraints for the `U32SPLIT` operation have degree $3$. Thus, the degree of the op flag for this operation cannot exceed $6$. +* Operations `U32ADD3` and `U32MADD` shift the stack to the left. Thus, having these two operations in this group and putting them under the common prefix `10011` allows us to create a common flag for these operations of degree $5$ (recall that the left-shift flag cannot exceed degree $5$). + +### High-degree operations +This group contains operations which require constraints with degree up to $3$. All $7$ operation bits are used for these flags. The extra $e_0$ column is used for degree reduction of the three high-degree bits. + +| Operation | Opcode value | Binary encoding | Operation group | Flag degree | +|---------------|:------------:|:---------------:|:--------------------------------------:|:-----------:| +| `HPERM` | $80$ | `101_0000` | [Crypto ops](./crypto_ops.md) | $5$ | +| `MPVERIFY` | $81$ | `101_0001` | [Crypto ops](./crypto_ops.md) | $5$ | +| `PIPE` | $82$ | `101_0010` | [I/O ops](./io_ops.md) | $5$ | +| `MSTREAM` | $83$ | `101_0011` | [I/O ops](./io_ops.md) | $5$ | +| `SPLIT` | $84$ | `101_0100` | [Flow control ops](../decoder/index.md) | $5$ | +| `LOOP` | $85$ | `101_0101` | [Flow control ops](../decoder/index.md) | $5$ | +| `SPAN` | $86$ | `101_0110` | [Flow control ops](../decoder/index.md) | $5$ | +| `JOIN` | $87$ | `101_0111` | [Flow control ops](../decoder/index.md) | $5$ | +| `DYN` | $88$ | `101_1000` | [Flow control ops](../decoder/index.md) | $5$ | +| `HORNERBASE` | $89$ | `101_1001` | [Crypto ops](./crypto_ops.md) | $5$ | +| `HORNEREXT` | $90$ | `101_1010` | [Crypto ops](./crypto_ops.md) | $5$ | +| `PUSH` | $91$ | `101_1011` | [I/O ops](./io_ops.md) | $5$ | +| `DYNCALL` | $92$ | `101_1100` | [Flow control ops](../decoder/index.md) | $5$ | +| `EVALCIRCUIT` | $93$ | `101_1101` | [Crypto ops](./crypto_ops.md) | $5$ | +| `LOGDEFERRED` | $94$ | `101_1110` | [Crypto ops](./crypto_ops.md#log_deferred) | $5$ | +| `` | $95$ | `101_1111` | | $5$ | + +Note that the `SPLIT` and `LOOP` operations share the common prefix `101010` and can be detected together with a flag of degree $4$ (using $e_0$ for degree reduction). Only `SPLIT` shifts the stack to the left, however: `LOOP` is do-while and reads no stack input — see [LOOP block decoding](../decoder/index.md#loop-block-decoding). + + +Also, we need to make sure that `extra` register $e_0$, which is used to reduce the flag degree by $2$, is set to $1$ when $b_6 = 1$, $b_5 = 0$, and $b_4 = 1$: + +$$ +e_0 - b_6 \cdot (1 - b_5) \cdot b_4 = 0 \text{ | degree} = 3 +$$ + +### Very high-degree operations +This group contains operations which require constraints with degree up to $5$. + +| Operation | Opcode value | Binary encoding | Operation group | Flag degree | +| ------------ | :----------: | :-------------: | :------------------------------------: | :---------: | +| `MRUPDATE` | $96$ | `110_0000` | [Crypto ops](./crypto_ops.md) | $4$ | +| `CRYPTOSTREAM` | $100$ | `110_0100` | [Crypto ops](./crypto_ops.md) | $4$ | +| `SYSCALL` | $104$ | `110_1000` | [Flow control ops](../decoder/index.md) | $4$ | +| `CALL` | $108$ | `110_1100` | [Flow control ops](../decoder/index.md) | $4$ | +| `END` | $112$ | `111_0000` | [Flow control ops](../decoder/index.md) | $4$ | +| `REPEAT` | $116$ | `111_0100` | [Flow control ops](../decoder/index.md) | $4$ | +| `RESPAN` | $120$ | `111_1000` | [Flow control ops](../decoder/index.md) | $4$ | +| `HALT` | $124$ | `111_1100` | [Flow control ops](../decoder/index.md) | $4$ | + +As mentioned previously, the last two bits of the opcode are not used in computation of the flag for these operations. We force these bits to always be set to $0$ with the following constraints: + +$$ +b_6 \cdot b_5 \cdot b_0 = 0 \text{ | degree} = 3 +$$ + +$$ +b_6 \cdot b_5 \cdot b_1 = 0 \text{ | degree} = 3 +$$ + +Also, we need to make sure that `extra` register $e_1$, which is used to reduce the flag degree by $1$, is set to $1$ when both $b_6$ and $b_5$ columns are set to $1$: + +$$ +e_1 - b_6 \cdot b_5 = 0 \text{ | degree} = 2 +$$ + +## Composite flags +Using the operation flags defined above, we can compute several composite flags which are used by various constraints in the VM. + +### Shift right flag +The right-shift flag indicates that an operation shifts the stack to the right. This flag is computed as follows: + +$$ +f_{shr} = (1 - b_6) \cdot b_5 \cdot b_4 + f_{u32split} + f_{push} \text{ | degree} = 6 +$$ + +In the above, $(1 - b_6) \cdot b_5 \cdot b_4$ evaluates to $1$ for all [right stack shift](#right-stack-shift-operations) operations described previously. This works because all these operations have a common prefix `011`. We also need to add in flags for other operations which shift the stack to the right but are not a part of the above group (e.g., `PUSH` operation). + +### Shift left flag +The left-shift flag indicates that a given operation shifts the stack to the left. To simplify the description of this flag, we will first compute the following intermediate variables: + +A flag which is set to $1$ when $f_{u32add3} = 1$ or $f_{u32madd} = 1$: + +$$ +f_{add3\_madd} = b_6 \cdot (1 - b_5) \cdot (1 - b_4) \cdot b_3 \cdot b_2 \text{ | degree} = 5 +$$ + +Using the above variable, we compute the left-shift flag as follows: + +$$ +f_{shl} = (1 - b_6) \cdot b_5 \cdot (1 - b_4) + f_{add3\_madd} + f_{split} + f_{repeat} + f_{end} \cdot h_5 \text{ | degree} = 5 +$$ + +In the above: +* $(1 - b_6) \cdot b_5 \cdot (1 - b_4)$ evaluates to $1$ for all [left stack shift](#left-stack-shift-operations) operations described previously. This works because all these operations have a common prefix `010`. +* $f_{split}$ is the SPLIT op flag. +* $h_5$ is the helper register in the decoder which is set to $1$ when we are exiting a `LOOP` block, and to $0$ otherwise. Because the loop body is always entered, $h_5$ coincides with "the ending node is a *loop*". + +Thus, similarly to the right-shift flag, we compute the value of the left-shift flag based on the prefix of the operation group which contains most left shift operations, and add in flag values for other operations which shift the stack to the left but are not a part of this group. + +### Control flow flag +The control flow flag $f_{ctrl}$ is set to $1$ when a control flow operation is being executed by the VM, and to $0$ otherwise. Naively, this flag can be computed as follows: + +$$ +f_{ctrl} = f_{join} + f_{split} + f_{loop} + f_{repeat} + f_{span} + f_{respan} + f_{call} + f_{syscall} + f_{end} + f_{halt} \text{ | degree} = 6 +$$ + +However, this can be computed more efficiently via the common operation prefixes for the two groups of control flow operations as follows. + +$$ +f_{span,join,split,loop} = e_0 \cdot (1 - b_3) \cdot b_2 \text{ | degree} = 3 +$$ + +$$ +f_{end,repeat,respan,halt} = e_1 \cdot b_4 \text{ | degree} = 2 +$$ + +$$ +f_{ctrl} = f_{span,join,split,loop} + f_{end,repeat,respan,halt} + f_{dyn} + f_{call} + f_{syscall} \text{ | degree} = 5 +$$ + +### Immediate value flag + +The immediate value flag $f_{imm}$ is set to 1 when an operation has an immediate value, and 0 otherwise: + +$$ +f_{imm} = f_{push} \text{ | degree} = 5 +$$ + +Note that the `ASSERT`, `MPVERIFY` and other operations have immediate values too. However, these immediate values are not included in the MAST digest, and hence are not considered for the $f_{imm}$ flag. diff --git a/versioned_docs/version-0.16/reference/miden-vm/design/stack/precompiles.md b/versioned_docs/version-0.16/reference/miden-vm/design/stack/precompiles.md new file mode 100644 index 00000000..96b2a947 --- /dev/null +++ b/versioned_docs/version-0.16/reference/miden-vm/design/stack/precompiles.md @@ -0,0 +1,95 @@ +# Precompiles + +Precompiles let Miden programs make claims about expensive computations without executing them +directly in the VM trace, while still binding those claims into the VM proof. This page covers the +VM-side mechanics: wrappers register deferred nodes, bind their digests to circuit-visible data, and +log statement digests that evaluate to `TRUE`. `ExecutionProof` carries `DeferredProof` material for +the resulting root; for proof shapes, see [Deferred computation](../deferred/index.md). + +Concrete proof-bound implementations live in the `miden-precompiles` crate. Their MASM support +modules are currently internal implementation detail used by core-library facades and tests. + +## Current data model + +| Concept | Description | +| ------- | ----------- | +| `Tag` | A 4-felt node constructor. Framework ids `0`, `1`, and `2` are reserved for `TRUE`, semantic `AND`, and opaque framework `CHUNKS`; precompile ids are derived from precompile names and interpret the remaining three `args` felts locally. | +| `Node` | A content-addressed `(tag, payload)` term in the deferred DAG. Payloads are data chunks, join child digests, pair lists of `lhs_digest || rhs_digest` chunks, or the framework `TRUE` sentinel. | +| `Precompile` | A host implementation that owns one precompile id, decodes the structural shape for its tags, evaluates nodes to canonical form, and optionally contributes canonical constants through `init()`. | +| `PrecompileRegistry` | The host/framework dispatcher for trusted precompile implementations. Public VM/prover/verifier APIs use the standard registry from `miden-precompiles`. | +| `DeferredState` | The host-side DAG witness accumulated during execution. It tracks registered nodes, evaluates them under the registry, and maintains the rolling deferred root. | +| `DeferredStateWire` | The canonical wire format for partial proofs. It is passive data until rehydrated and validated with `DeferredState::from_wire`. | +| Deferred root | A single digest public value. Each logged statement appends `Node::AND(previous_root, statement_digest)` and advances the root to that node digest. | + +## Lifecycle overview + +1. **Wrapper registers nodes** – Internal MASM support code stages node payloads on the operand + stack or in memory and emits `adv.register_deferred` / `adv.register_deferred_data`. + Registration stores the node in host-side `DeferredState`, checks structural child closure, and + evaluates the node immediately under the installed registry. +2. **Wrapper binds digests inside the VM** – Registration arguments are visible in the VM execution + trace, but the event does not constrain the host-side `DeferredState` update. Memory-backed + registration additionally performs direct host reads without adding AIR memory accesses. The + wrapper computes each proof-relevant digest with VM instructions from the exact same tag and + stack payload or ordered memory chunk sequence. +3. **Wrapper evaluates only through explicit predicates** – When a wrapper uses + `adv.evaluate_deferred*` to obtain host-computed canonical data, it must use VM instructions to + relate that advice to values established independently of it, then log a statement digest that + the verifier can re-evaluate. +4. **`log_deferred` folds a statement** – The opcode expects `STMNT` at stack offsets `4..8`. + `STMNT` must already be registered in `DeferredState` and evaluate to `TRUE`. The constrained + Poseidon2 permutation computes `ROOT_NEW = rate0(Poseidon2([ROOT_PREV, STMNT, Tag::AND]))`, and + host-side deferred state records the corresponding `AND` node. +5. **Prover binds the deferred root** – The VM STARK uses the final deferred root as a public input. + `ExecutionProof` carries the chosen `DeferredProof` form: default proving produces final material + (`Empty` or `Stark`), while explicit partial proving preserves `Wire`. +6. **Verifier resolves and checks** – Final verification resolves a trusted root from `DeferredProof` + before VM STARK verification and rejects `Wire`. `Verifier::verify_partial` accepts only `Wire`, + rehydrates it under the built-in `miden_precompiles::registry()` with the configured deferred + budget, then verifies the VM STARK against the hydrated root. + +## Responsibilities + +| Participant | Responsibilities | +| ----------- | ---------------- | +| VM | Executes deferred advice events and `log_deferred`, maintains the rolling deferred root, and exposes the final root as a public value. | +| Host / advice provider | Maintains `DeferredState`, runs trusted precompile implementations, and supplies evaluation advice when wrappers request it. | +| MASM wrapper | Registers concrete deferred nodes, computes node/statement digests with VM instructions from exact stack payloads or memory reads, logs only registered statements that should evaluate to `TRUE`, and hides helper outputs from callers when appropriate. | +| Prover | Uses the final deferred root as a VM STARK public input and includes the chosen `DeferredProof` form in `ExecutionProof`: final `Empty`/`Stark` by default, or `Wire` for explicit partial proving. | +| Verifier | Resolves a trusted deferred root from `DeferredProof` before VM STARK verification. Final verification rejects `Wire`; partial verification rehydrates `Wire` under the built-in `miden_precompiles::registry()`. | + +## Conventions + +- Tag layout: `TAG = [precompile_id, arg0, arg1, arg2]`. + - `precompile_id` selects the framework or owning precompile. + - `arg0..arg2` are interpreted by the selected precompile. + - Framework id `0` is `Tag::TRUE`; framework id `1` is `Tag::AND`; framework id `2` is + `Tag::CHUNKS`. +- Payload shapes are declared by the selected precompile's `decode(args)`, but semantic lengths are + tag-specific and validated by the owning precompile: + - `NodeType::Data` accepts one or more opaque 8-felt chunks. For memory-backed registration, + the stack-supplied `n_chunks` determines how many chunks are read. + - `NodeType::Join` reads `lhs_digest || rhs_digest`. + - `NodeType::PairList` accepts one or more `lhs_digest || rhs_digest` chunks. Precompiles that + encode a pair count in tag arguments must check the actual payload length during evaluation. +- `log_deferred` stack effect: `[_, STMNT, _, ...] -> [ROOT_NEW, OUT_RATE1, OUT_CAP, ...]` where + `STMNT` occupies stack offsets `4..8`. Wrappers usually drop the three output words after the root + transition has been constrained. +- Input and memory layouts are precompile-specific. Core-library wrappers define the native formats + for hash facades and for arithmetic/curve support used by signature verification. + +## Examples + +- Hash support wrappers register the input/result nodes needed for the hash claim and log a + statement digest that verifies the claimed digest. +- Signature support wrappers register the public key, precompile-specific message input, signature, + and verification predicate nodes, then log the predicate statement. + + +## Related reading + +- [Deferred computation](../deferred/index.md) – deferred DAG model and final/partial proof shapes. +- [`log_deferred` instruction](../../user_docs/assembly/instruction_reference.md) – stack behaviour + and opcode semantics. +- `DeferredStateWire` implementation (`core/src/deferred/wire.rs`) – partial-proof deferred witness + details. diff --git a/versioned_docs/version-0.16/reference/miden-vm/design/stack/stack_ops.md b/versioned_docs/version-0.16/reference/miden-vm/design/stack/stack_ops.md new file mode 100644 index 00000000..656393ee --- /dev/null +++ b/versioned_docs/version-0.16/reference/miden-vm/design/stack/stack_ops.md @@ -0,0 +1,234 @@ +--- +title: "Stack Manipulation" +sidebar_position: 6 +--- + +# Stack Manipulation +In this section we describe the AIR constraints for Miden VM stack manipulation operations. + +## PAD +The `PAD` operation pushes a $0$ onto the stack. The diagram below illustrates this graphically. + +![pad](../../img/design/stack/stack_ops/PAD.png) + +Stack transition for this operation must satisfy the following constraints: + +$$ +s_{0}' = 0 \text{ | degree} = 1 +$$ + +The effect of this operation on the rest of the stack is: +* **Right shift** starting from position $0$. + +## DROP +The `DROP` operation removes an element from the top of the stack. The diagram below illustrates this graphically. + +![drop](../../img/design/stack/stack_ops/DROP.png) + +The `DROP` operation shifts the stack by $1$ element to the left, but does not impose any additional constraints. The degree of left shift constraints is $1$. + +The effect of this operation on the rest of the stack is: +* **Left shift** starting from position $1$. + + +## DUP(n) +The `DUP(n)` operations push a copy of the $n$-th stack element onto the stack. Eg. `DUP` (same as `DUP0`) pushes a copy of the top stack element onto the stack. Similarly, `DUP5` pushes a copy of the $6$-th stack element onto the stack. This operation is valid for $n \in \{0, ..., 7, 9, 11, 13, 15\}$. The diagram below illustrates this graphically. + +![dupn](../../img/design/stack/stack_ops/DUP(n).png) + +Stack transition for this operation must satisfy the following constraints: + +$$ +s_{0}' - s_{n} = 0 \text{ for } n \in \{0, ..., 7, 9, 11, 13, 15\} \text{ | degree} = 1 +$$ + +where $n$ is the depth of the stack from where the element has been copied. + +The effect of this operation on the rest of the stack is: +* **Right shift** starting from position $0$. + +## SWAP +The `SWAP` operations swaps the top two elements of the stack. The diagram below illustrates this graphically. + +![swap](../../img/design/stack/stack_ops/SWAP.png) + +Stack transition for this operation must satisfy the following constraints: + +$$ +s_{0}' - s_{1} = 0 \text{ | degree} = 1 +$$ + +$$ +s_{1}' - s_{0} = 0 \text{ | degree} = 1 +$$ + +The effect of this operation on the rest of the stack is: +* **No change** starting from position $2$. + +## SWAPW +The `SWAPW` operation swaps stack elements $0, 1, 2, 3$ with elements $4, 5, 6, 7$. The diagram below illustrates this graphically. + +![swapw](../../img/design/stack/stack_ops/SWAPW.png) + +Stack transition for this operation must satisfy the following constraints: + +$$ +s_{i}' - s_{i+4} = 0 \text{ for } i \in \{0, 1, 2, 3\} \text{ | degree} = 1 +$$ + +$$ +s_{i + 4}' - s_i = 0 \text{ for } i \in \{0, 1, 2, 3\} \text{ | degree} = 1 +$$ + +The effect of this operation on the rest of the stack is: +* **No change** starting from position $8$. + +## SWAPW2 +The `SWAPW2` operation swaps stack elements $0, 1, 2, 3$ with elements $8, 9, 10, 11$. The diagram below illustrates this graphically. + +![swapw2](../../img/design/stack/stack_ops/SWAPW2.png) + +Stack transition for this operation must satisfy the following constraints: + +$$ +s_i' - s_{i+8} = 0 \text{ for } i \in \{0, 1, 2, 3\} \text{ | degree} = 1 +$$ + +$$ +s_{i + 8}' - s_i = 0 \text{ for } i \in \{0, 1, 2, 3\} \text{ | degree} = 1 +$$ + +The effect of this operation on the rest of the stack is: +* **No change** for elements $4, 5, 6, 7$. +* **No change** starting from position $12$. + +## SWAPW3 +The `SWAPW3` operation swaps stack elements $0, 1, 2, 3$ with elements $12, 13, 14, 15$. The diagram below illustrates this graphically. + +![swapw3](../../img/design/stack/stack_ops/SWAPW3.png) + +Stack transition for this operation must satisfy the following constraints: + +$$ +s_i' - s_{i+12} = 0 \text{ for } i \in \{0, 1, 2, 3\} \text{ | degree} = 1 +$$ + +$$ +s_{i+12}' - s_i = 0 \text{ for } i \in \{0, 1, 2, 3\} \text{ | degree} = 1 +$$ + +The effect of this operation on the rest of the stack is: +* **No change** for elements $4, 5, 6, 7, 8, 9, 10, 11$. +* **No change** starting from position $16$. + +## SWAPDW +The `SWAPDW` operation swaps stack elements $[0, 8)$ with elements $[8, 16)$. The diagram below illustrates this graphically. + +![swapdw](../../img/design/stack/stack_ops/SWAPDW.png) + +Stack transition for this operation must satisfy the following constraints: + +$$ +s_i' - s_{i+8} = 0 \text{ for } i \in [0, 8) \text{ | degree} = 1 +$$ + +$$ +s_{i+8}' - s_i = 0 \text{ for } i \in [0, 8) \text{ | degree} = 1 +$$ + +The effect of this operation on the rest of the stack is: +* **No change** starting from position $16$. + +## MOVUP(n) +The `MOVUP(n)` operation moves the $n$-th element of the stack to the top of the stack. For example, `MOVUP2` moves element at depth $2$ to the top of the stack. All elements with depth less than $n$ are shifted to the right by one, while elements with depth greater than $n$ remain in place, and the depth of the stack does not change. This operation is valid for $n \in [2, 9)$. The diagram below illustrates this graphically. + +![movup](../../img/design/stack/stack_ops/MOVUP(n).png) + +Stack transition for this operation must satisfy the following constraints: + +$$ +s_0' - s_n = 0 \text{ for } n \in [2, 9) \text{ | degree} = 1 +$$ + +where $n$ is the depth of the element which is moved to the top of the stack. + +The effect of this operation on the rest of the stack is: +* **Right shift** for elements between $0$ and $n-1$. +* **No change** starting from position $n+1$. + +## MOVDN(n) +The `MOVDN(n)` operation moves the top element of the stack to the $n$-th position. For example, `MOVDN2` moves the top element of the stack to depth $2$. All the elements with depth less than $n$ are shifted to the left by one, while elements with depth greater than $n$ remain in place, and the depth of the stack does not change. This operation is valid for $n \in [2, 9)$. The diagram below illustrates this graphically. + +![movdn](../../img/design/stack/stack_ops/MOVDN(n).png) + +Stack transition for this operation must satisfy the following constraints: + +$$ +s_n' - s_0 = 0 \text{ for } n \in [2, 9) \text{ | degree} = 1 +$$ + +where $n$ is the depth to which the top stack element is moved. + +The effect of this operation on the rest of the stack is: +* **Left shift** for elements between $1$ and $n$. +* **No change** starting from position $n+1$. + +## CSWAP +The `CSWAP` operation pops an element off the stack and if the element is $1$, swaps the top two remaining elements. If the popped element is $0$, the rest of the stack remains unchanged. The diagram below illustrates this graphically. + +![cswap](../../img/design/stack/stack_ops/CSWAP.png) + +In the above: + +$$ +d = \begin{cases} a, & \text{if}\ c = 0 b, & \text{if}\ c = 1\ \end{cases} e = \begin{cases} b, & \text{if}\ c = 0 a, & \text{if}\ c = 1\ \end{cases} +$$ + +Stack transition for this operation must satisfy the following constraints: + +$$ +s_0' - s_{0} \cdot s_{2} - (1-s_0) \cdot s_1 = 0 \text{ | degree} = 2 +$$ + +$$ +s_1' - s_0 \cdot s_{1} - (1-s_0) \cdot s_2 = 0 \text{ | degree} = 2 +$$ + +We also need to enforce that the value in $s_0$ is binary. This can be done with the following constraint: + +$$ +s_0^2 - s_0 = 0 \text{ | degree} = 2 +$$ + +The effect of this operation on the rest of the stack is: +* **Left shift** starting from position $3$. + +## CSWAPW +The `CSWAPW` operation pops an element off the stack and if the element is $1$, swaps elements $1, 2, 3, 4$ with elements $5, 6, 7, 8$. If the popped element is $0$, the rest of the stack remains unchanged. The diagram below illustrates this graphically. + +![cswapw](../../img/design/stack/stack_ops/CSWAPW.png) + +In the above: + +$$ +D = \begin{cases} A, & \text{if}\ c = 0 B, & \text{if}\ c = 1\ \end{cases} E = \begin{cases} B, & \text{if}\ c = 0 A, & \text{if}\ c = 1\ \end{cases} +$$ + +Stack transition for this operation must satisfy the following constraints: + +$$ +s_i' - s_0 \cdot s_{i+5} - (1-s_0) \cdot s_{i+1} = 0 \text{ for } i \in [0, 4) \text{ | degree} = 2 +$$ + +$$ +s_{i+4}' - s_0 \cdot s_{i+1} - (1-s_0) \cdot s_{i+5} = 0 \text{ for } i \in [0, 4) \text{ | degree} = 2 +$$ + +We also need to enforce that the value in $s_0$ is binary. This can be done with the following constraint: + +$$ +s_0^2 - s_0 = 0 \text{ | degree} = 2 +$$ + +The effect of this operation on the rest of the stack is: +* **Left shift** starting from position $9$. diff --git a/versioned_docs/version-0.16/reference/miden-vm/design/stack/system_ops.md b/versioned_docs/version-0.16/reference/miden-vm/design/stack/system_ops.md new file mode 100644 index 00000000..141b1640 --- /dev/null +++ b/versioned_docs/version-0.16/reference/miden-vm/design/stack/system_ops.md @@ -0,0 +1,68 @@ +--- +title: "System Operations" +sidebar_position: 3 +--- + +# System Operations +In this section we describe the AIR constraints for Miden VM system operations. + +## NOOP +The `NOOP` operation advances the cycle counter but does not change the state of the operand stack (i.e., the depth of the stack and the values on the stack remain the same). + +The `NOOP` operation does not impose any constraints besides the ones needed to ensure that the entire state of the stack is copied over. This constraint looks like so: + +$$ +s'_i - s_i = 0 \ \text{ for } i \in [0, 16) \text { | degree} = 1 +$$ + +## EMIT +The `EMIT` operation interrupts execution for a single cycle and hands control to the host. During this interruption, the host can read the current state of the execution and modify the advice provider as it sees fit. From the VM's perspective, this operation has exactly the same semantics as [`NOOP`](#noop) - the operand stack remains completely unchanged. + +By convention, the top element of the stack is used to encode an event ID (see the [events documentation](../../user_docs/assembly/events.md) for details on event structure and usage). The host can use this event ID to determine what actions to take during the execution interruption. + +## ASSERT +The `ASSERT` operation pops an element off the stack and checks if the popped element is equal to $1$. If the element is not equal to $1$, program execution fails. + +![assert](../../img/design/stack/system_ops/ASSERT.png) + +Stack transition for this operation must satisfy the following constraints: + +$$ +s_0 - 1 = 0 \text{ | degree} = 1 +$$ + +The effect on the rest of the stack is: +* **Left shift** starting from position $1$. + +## CALLER +The `CALLER` operation overwrites the top four stack elements with the caller's function hash. + +Stack transition for this operation must satisfy the following constraints: + +$$ +\begin{aligned} +s_0' - h_0 = 0 \text{ | degree} = 1 \\ +s_1' - h_1 = 0 \text{ | degree} = 1 \\ +s_2' - h_2 = 0 \text{ | degree} = 1 \\ +s_3' - h_3 = 0 \text{ | degree} = 1 +\end{aligned} +$$ + +The effect on the rest of the stack is: +* **No change** starting from position $4$. + +## CLK +The `CLK` operation pushes the current value of the clock cycle onto the stack. The diagram below illustrates this graphically. + +![clk](../../img/design/stack/system_ops/CLK.png) + +The stack transition for this operation must follow the following constraint: + +$$ +s_0' - clk = 0 \text{ | degree} = 1 +$$ + +The effect on the rest of the stack is: +* **Right shift** starting from position $0$. + +**WARNING:** This is a best effort instruction, since given the same program, changes in NOOP padding (which do not change the program commitment) will change the number of rows in the trace (and hence the value of `clk` at various points in the program). Hence, the value returned by `CLK` should be treated as non-deterministic and attacker-controlled (up to the amount of padding allowed by the padding-related constraints). diff --git a/versioned_docs/version-0.16/reference/miden-vm/design/stack/u32_ops.md b/versioned_docs/version-0.16/reference/miden-vm/design/stack/u32_ops.md new file mode 100644 index 00000000..90919709 --- /dev/null +++ b/versioned_docs/version-0.16/reference/miden-vm/design/stack/u32_ops.md @@ -0,0 +1,304 @@ +--- +title: "u32 Operations" +sidebar_position: 5 +--- + +# u32 Operations +In this section we describe semantics and AIR constraints of operations over u32 values (i.e., 32-bit unsigned integers) as they are implemented in Miden VM. Unless stated otherwise, $s_0$ denotes the top of the stack, $s_1$ the next element, $s_2$ the third element, and so on. + +### Range checks +Most operations described below require some number of 16-bit range checks (i.e., verifying that the value of a field element is smaller than $2^{16}$). The number of required range checks varies between $2$ and $4$, depending on the operation. However, to simplify the constraint system, we force each relevant operation to consume exactly $4$ range checks. + +To perform these range checks, the prover puts the values to be range-checked into helper registers $h_0, ..., h_3$, and then updates the range checker bus column $b_{range}$ according to the LogUp construction described in the [range checker](../range.md) documentation, using multiplicity $1$ for each value. + +This operation is enforced via the following constraint. Note that since constraints cannot include divisions, the actual constraint which is enforced will be expressed equivalently with all denominators multiplied through, resulting in a constraint of degree 5. + +$$ +b_{range}' = b_{range} - \frac{1}{(\alpha - h_0)} - \frac{1}{(\alpha - h_1)} - \frac{1}{(\alpha - h_2)} - \frac{1}{(\alpha - h_3)} \text{ | degree} = 5 +$$ + +The above is just a partial constraint as it does not show the range checker's part of the constraint, which adds the required values into the bus column. It also omits the [selector flag](./op_constraints.md#operation-flags) which is used to turn this constraint on only when executing relevant operations. + +### Checking element validity +Another primitive which is required by most of the operations described below is checking whether four 16-bit values form a valid field element. Assume $t_0$, $t_1$, $t_2$, and $t_3$ are known to be 16-bit values, and we want to verify that $2^{48} \cdot t_3 + 2^{32} \cdot t_2 + 2^{16} \cdot t_1 + t_0$ is a valid field element. + +For simplicity, let's denote: + +$$ +v_{hi} = 2^{16} \cdot t_3 + t_2 +v_{lo} = 2^{16} \cdot t_1 + t_0 +$$ + +We can then impose the following constraint to verify element validity: + +> $$ +> \left(1 - m \cdot (2^{32} - 1 - v_{hi})\right) \cdot v_{lo} = 0 \text{ | degree} = 3 +> $$ + +Where $m$ is a value set non-deterministically by the prover. + +The above constraint should hold only if either of the following hold: + +* $v_{lo} = 0$ +* $v_{hi} \ne 2^{32} - 1$ + +To satisfy the latter equation, the prover needs to set $m = (2^{32} - 1 - v_{hi})^{-1}$, which is possible only when $v_{hi} \ne 2^{32} - 1$. + +This constraint is sufficient because modulus $2^{64} - 2^{32} + 1$ in binary representation is 32 ones, followed by 31 zeros, followed by a single one: + +$$ +1111111111111111111111111111111100000000000000000000000000000001 +$$ + +This implies that the largest possible 64-bit value encoding a valid field element would be 32 ones, followed by 32 zeros: + +$$ +1111111111111111111111111111111100000000000000000000000000000000 +$$ + +Thus, for a 64-bit value to encode a valid field element, either the lower 32 bits must be all zeros, or the upper 32 bits must not be all ones (which is $2^{32} - 1$). + +## U32SPLIT +Assume $s_0$ is the element at the top of the stack. The `U32SPLIT` operation computes $(b,c) \leftarrow s_0$, where $b$ contains the lower 32 bits of $s_0$, and $c$ contains the upper 32 bits of $s_0$. The output stack is `[b, c, ...]` with $b$ (low limb) on top. The diagram below illustrates this graphically. + +![u32split](../../img/design/stack/u32_operations/U32SPLIT.png) + +To facilitate this operation, the prover sets values in $h_0, ..., h_3$ to 16-bit limbs of $a$ with $h_0$ being the least significant limb. Thus, stack transition for this operation must satisfy the following constraints: + +$$ +s_{0} = 2^{48} \cdot h_3 + 2^{32} \cdot h_2 + 2^{16} \cdot h_1 + h_0 \text{ | degree} = 1 +$$ + +$$ +s_{0}' = 2^{16} \cdot h_1 + h_0 \text{ | degree} = 1 +$$ + +$$ +s_{1}' = 2^{16} \cdot h_3 + h_2 \text{ | degree} = 1 +$$ + +In addition to the above constraints, we also need to verify that values in $h_0, ..., h_3$ are smaller than $2^{16}$, which we can do using 16-bit range checks as described [previously](#range-checks). Also, we need to make sure that values in $h_0, ..., h_3$, when combined, form a valid field element, which we can do by putting a nondeterministic value $m$ into helper register $h_4$ and using the technique described [here](#checking-element-validity). + +The effect of this operation on the rest of the stack is: +* **Right shift** starting from position $1$. + +## U32ASSERT2 +Assume $s_0$ (top) and $s_1$ (next) are the elements at the top of the stack. The `U32ASSERT2` verifies that both $s_0$ and $s_1$ are smaller than $2^{32}$. The diagram below illustrates this graphically. + +![u32assert2](../../img/design/stack/u32_operations/U32ASSERT2.png) + +To facilitate this operation, the prover sets values in $h_2$ and $h_3$ to low and high 16-bit limbs of $s_0$, and values in $h_0$ and $h_1$ to low and high 16-bit limbs of $s_1$. Thus, stack transition for this operation must satisfy the following constraints: + +$$ +s_0' = 2^{16} \cdot h_3 + h_2 \text{ | degree} = 1 +$$ + +$$ +s_1' = 2^{16} \cdot h_1 + h_0 \text{ | degree} = 1 +$$ + +In addition to the above constraints, we also need to verify that values in $h_0, ..., h_3$ are smaller than $2^{16}$, which we can do using 16-bit range checks as described [previously](#range-checks). + +The effect of this operation on the rest of the stack is: +* **No change** starting from position $0$ - i.e., the state of the stack does not change. + +## U32ADD +Assume $s_0$ and $s_1$ are the values at the top of the stack which are known to be smaller than $2^{32}$. The `U32ADD` operation computes $(c,d) \leftarrow s_0 + s_1$, where $c$ contains the low 32-bits of the result, and $d$ is the carry bit. The output stack is `[c, d, ...]` with $c$ on top. The diagram below illustrates this graphically. + +![u32add](../../img/design/stack/u32_operations/U32ADD.png) + +To facilitate this operation, the prover sets values in $h_0$, $h_1$, and $h_2$ to 16-bit limbs of $s_0 + s_1$ with $h_0$ being the least significant limb. Value in $h_3$ is set to $0$. Thus, stack transition for this operation must satisfy the following constraints: + +$$ +s_0 + s_1 = 2^{32} \cdot h_2 + 2^{16} \cdot h_1 + h_0 \text{ | degree} = 1 +$$ + +$$ +s_0' = 2^{16} \cdot h_1 + h_0 \text{ | degree} = 1 +$$ + +$$ +s_1' = h_2 \text{ | degree} = 1 +$$ + +$$ +h_3 = 0 \text{ | degree} = 1 +$$ + +In addition to the above constraints, we also need to verify that values in $h_0, ..., h_3$ are smaller than $2^{16}$, which we can do using 16-bit range checks as described [previously](#range-checks). + +The effect of this operation on the rest of the stack is: +* **No change** starting from position $2$. + +## U32ADD3 +Assume $s_0$, $s_1$, and $s_2$ are the values at the top of the stack which are known to be smaller than +$2^{32}$. The `U32ADD3` operation computes $(\mathsf{sum}, \mathsf{carry}) \leftarrow s_0 + s_1 + s_2$, +where $\mathsf{sum}$ contains the low 32 bits of the result and $\mathsf{carry}$ contains the +high 32 bits. The diagram below illustrates this graphically. + +![u32add3](../../img/design/stack/u32_operations/U32ADD3.png) + +To facilitate this operation, the prover sets values in $h_0$, $h_1$, and $h_2$ to 16-bit limbs of $s_0 + s_1 + s_2$ with $h_0$ being the least significant limb. Value in $h_3$ is set to $0$. Thus, stack transition for this operation must satisfy the following constraints: + +$$ +s_0 + s_1 + s_2 = 2^{32} \cdot h_2 + 2^{16} \cdot h_1 + h_0 \text{ | degree} = 1 +$$ + +$$ +s_0' = 2^{16} \cdot h_1 + h_0 \text{ | degree} = 1 +$$ + +$$ +s_1' = h_2 \text{ | degree} = 1 +$$ + +$$ +h_3 = 0 \text{ | degree} = 1 +$$ + +In addition to the above constraints, we also need to verify that values in $h_0, ..., h_3$ are smaller than $2^{16}$, which we can do using 16-bit range checks as described [previously](#range-checks). + +The effect of this operation on the rest of the stack is: +* **Left shift** starting from position $3$. + +## U32SUB +Assume $s_0$ and $s_1$ are the values at the top of the stack which are known to be smaller than $2^{32}$. The `U32SUB` operation computes $(\text{borrow}, \text{diff}) \leftarrow s_1 - s_0$, where `diff` contains the 32-bit result in two's complement, and `borrow` is the borrow bit. The diagram below illustrates this graphically. + +![u32sub](../../img/design/stack/u32_operations/U32SUB.png) + +To facilitate this operation, the prover sets values in $h_0$ and $h_1$ to the low and the high 16-bit limbs of $s_1 - s_0$ respectively. Values in $h_2$ and $h_3$ are set to $0$. Thus, stack transition for this operation must satisfy the following constraints: + +$$ +s_1 = s_0 + s_1' - 2^{32} \cdot s_0' \text{ | degree} = 1 +$$ + +$$ +s_0'^2 - s_0' = 0 \text{ | degree} = 2 +$$ + +$$ +s_1' = 2^{16} \cdot h_1 + h_0 \text{ | degree} = 1 +$$ + +In addition to the above constraints, we also need to verify that values in $h_0, ..., h_3$ are smaller than $2^{16}$, which we can do using 16-bit range checks as described [previously](#range-checks). + +The effect of this operation on the rest of the stack is: +* **No change** starting from position $2$. + +## U32MUL +Assume $s_0$ and $s_1$ are the values at the top of the stack which are known to be smaller than $2^{32}$. The `U32MUL` operation computes $(c, d) \leftarrow s_0 \cdot s_1$, where $c$ and $d$ contain the low and the high 32-bits of the result respectively. The output stack is `[c, d, ...]` with $c$ on top. The diagram below illustrates this graphically. + +![u32mul](../../img/design/stack/u32_operations/U32MUL.png) + +To facilitate this operation, the prover sets values in $h_0, ..., h_3$ to 16-bit limbs of $s_0 \cdot s_1$ with $h_0$ being the least significant limb. Thus, stack transition for this operation must satisfy the following constraints: + +$$ +s_0 \cdot s_1 = 2^{48} \cdot h_3 + 2^{32} \cdot h_2 + 2^{16} \cdot h_1 + h_0 \text{ | degree} = 2 +$$ + +$$ +s_0' = 2^{16} \cdot h_1 + h_0 \text{ | degree} = 1 +$$ + +$$ +s_1' = 2^{16} \cdot h_3 + h_2 \text{ | degree} = 1 +$$ + +In addition to the above constraints, we also need to verify that values in $h_0, ..., h_3$ are smaller than $2^{16}$, which we can do using 16-bit range checks as described [previously](#range-checks). Also, we need to make sure that values in $h_0, ..., h_3$, when combined, form a valid field element, which we can do by putting a nondeterministic value $m$ into helper register $h_4$ and using the technique described [here](#checking-element-validity). + +The effect of this operation on the rest of the stack is: +* **No change** starting from position $2$. + +## U32MADD +Assume $s_0$, $s_1$, and $s_2$ are the values at the top of the stack which are known to be smaller than +$2^{32}$. The `U32MADD` operation computes $(\mathsf{lo}, \mathsf{hi}) \leftarrow s_0 \cdot s_1 + s_2$, +where $\mathsf{lo}$ and $\mathsf{hi}$ contain the low and the high 32 bits of the result. The +diagram below illustrates this graphically. + +![u32madd](../../img/design/stack/u32_operations/U32MADD.png) + +To facilitate this operation, the prover sets values in $h_0, ..., h_3$ to 16-bit limbs of $s_0 \cdot s_1 + s_2$ with $h_0$ being the least significant limb. Thus, stack transition for this operation must satisfy the following constraints: + +$$ +s_0 \cdot s_1 + s_2 = 2^{48} \cdot h_3 + 2^{32} \cdot h_2 + 2^{16} \cdot h_1 + h_0 \text{ | degree} = 2 +$$ + +$$ +s_0' = 2^{16} \cdot h_1 + h_0 \text{ | degree} = 1 +$$ + +$$ +s_1' = 2^{16} \cdot h_3 + h_2 \text{ | degree} = 1 +$$ + +In addition to the above constraints, we also need to verify that values in $h_0, ..., h_3$ are smaller than $2^{16}$, which we can do using 16-bit range checks as described [previously](#range-checks). Also, we need to make sure that values in $h_0, ..., h_3$, when combined, form a valid field element, which we can do by putting a nondeterministic value $m$ into helper register $h_4$ and using the technique described [here](#checking-element-validity). + +**Note**: that the above constraints guarantee the correctness of the operation iff $s_0 \cdot s_1 + s_2$ cannot overflow field modules (which is the case for the field with modulus $2^{64} - 2^{32} + 1$). + +The effect of this operation on the rest of the stack is: +* **Left shift** starting from position $3$. + +## U32DIV +Assume the divisor $s_0$ is at the top of the stack and the dividend $s_1$ is below it, and both are +smaller than $2^{32}$. The `U32DIV` operation computes $(r, q) \leftarrow s_1 / s_0$, where $r$ is +the remainder and $q$ is the quotient. The diagram below illustrates this graphically. + +![u32div](../../img/design/stack/u32_operations/U32DIV.png) + +To facilitate this operation, the prover sets values in $h_0$ and $h_1$ to the low and high +16-bit limbs of $(s_1 - q)$, and values in $h_2$ and $h_3$ to the low and high 16-bit limbs of +$(s_0 - r - 1)$. Thus, stack transition for this operation must satisfy the following constraints: + +$$ +s_1 = s_0 \cdot s_1' + s_0' \text{ | degree} = 2 +$$ + +$$ +s_1 - s_1' = 2^{16} \cdot h_1 + h_0 \text{ | degree} = 1 +$$ + +$$ +s_0 - s_0' - 1= 2^{16} \cdot h_3 + h_2 \text{ | degree} = 1 +$$ + +The second constraint enforces that $q \leq s_1$ (equivalently $s_1' \leq s_1$), while the third +constraint enforces that $r < s_0$ (equivalently $s_0' < s_0$). + +The effect of this operation on the rest of the stack is: +* **No change** starting from position $2$. + +## U32AND +Assume $s_0$ and $s_1$ are the values at the top of the stack. The `U32AND` operation computes $c \leftarrow (s_0 \land s_1)$, where $c$ is the result of performing a bitwise AND on $s_0$ and $s_1$. The diagram below illustrates this graphically. + +![u32and](../../img/design/stack/u32_operations/U32AND.png) + +To facilitate this operation, we will need to make a request to the chiplet bus $b_{chip}$ by dividing its current value by the value representing bitwise operation request. This can be enforced with the following constraint: + +$$ +b_{chip}' \cdot \left(\alpha_0 + \alpha_1 \cdot op_{u32and} + \alpha_2 \cdot s_0 + \alpha_3 \cdot s_1 + \alpha_4 \cdot s_0' \right) = b_{chip} \text{ | degree} = 2 +$$ + +In the above, $op_{u32and}$ is the unique [operation label](../chiplets/index.md#operation-labels) of the bitwise `AND` operation. + +**Note**: unlike for many other u32 operations, bitwise AND operation does not assume that the values at the top of the stack are smaller than $2^{32}$. This is because the lookup will fail for any inputs which are not 32-bit integers. + +The effect of this operation on the rest of the stack is: +* **Left shift** starting from position $2$. + +## U32XOR +Assume $s_0$ and $s_1$ are the values at the top of the stack. The `U32XOR` operation computes $c \leftarrow (s_0 \oplus s_1)$, where $c$ is the result of performing a bitwise XOR on $s_0$ and $s_1$. The diagram below illustrates this graphically. + +![u32xor](../../img/design/stack/u32_operations/U32XOR.png) + +To facilitate this operation, we will need to make a request to the chiplet bus $b_{chip}$ by dividing its current value by the value representing bitwise operation request. This can be enforced with the following constraint: + +> $$ +> b_{chip}' \cdot \left(\alpha_0 + \alpha_1 \cdot op_{u32xor} + \alpha_2 \cdot s_0 + \alpha_3 \cdot s_1 + \alpha_4 \cdot s_0' \right) = b_{chip} \text{ | degree} = 2 +> $$ + +In the above, $op_{u32xor}$ is the unique [operation label](../chiplets/index.md#operation-labels) of the bitwise `XOR` operation. + +**Note**: unlike for many other u32 operations, bitwise XOR operation does not assume that the values at the top of the stack are smaller than $2^{32}$. This is because the lookup will fail for any inputs which are not 32-bit integers. + +The effect of this operation on the rest of the stack is: +* **Left shift** starting from position $2$. diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/chiplets/bitwise/bitwise_execution_trace.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/chiplets/bitwise/bitwise_execution_trace.png new file mode 100644 index 00000000..a554cff6 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/chiplets/bitwise/bitwise_execution_trace.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/chiplets/chiplets.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/chiplets/chiplets.png new file mode 100644 index 00000000..fa0ebaeb Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/chiplets/chiplets.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/chiplets/hasher/hash_merkle_tree.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/chiplets/hasher/hash_merkle_tree.png new file mode 100644 index 00000000..996c13f5 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/chiplets/hasher/hash_merkle_tree.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/chiplets/memory/memory_alternative_design.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/chiplets/memory/memory_alternative_design.png new file mode 100644 index 00000000..2c24b0db Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/chiplets/memory/memory_alternative_design.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/chiplets/memory/memory_context_separation.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/chiplets/memory/memory_context_separation.png new file mode 100644 index 00000000..f6fe123f Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/chiplets/memory/memory_context_separation.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/chiplets/memory/memory_limitation_diagram.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/chiplets/memory/memory_limitation_diagram.png new file mode 100644 index 00000000..acc1c907 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/chiplets/memory/memory_limitation_diagram.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/chiplets/memory/memory_miden_vm_layout.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/chiplets/memory/memory_miden_vm_layout.png new file mode 100644 index 00000000..5ddfa01b Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/chiplets/memory/memory_miden_vm_layout.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/chiplets/memory/memory_non_contiguous_memory.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/chiplets/memory/memory_non_contiguous_memory.png new file mode 100644 index 00000000..c17bf0dd Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/chiplets/memory/memory_non_contiguous_memory.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/chiplets/memory/memory_read_write.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/chiplets/memory/memory_read_write.png new file mode 100644 index 00000000..828065a5 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/chiplets/memory/memory_read_write.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/chiplets/memory/memory_reading_memory.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/chiplets/memory/memory_reading_memory.png new file mode 100644 index 00000000..6dc10574 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/chiplets/memory/memory_reading_memory.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/chiplets/memory/memory_writing_to_memory.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/chiplets/memory/memory_writing_to_memory.png new file mode 100644 index 00000000..3ef5bf10 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/chiplets/memory/memory_writing_to_memory.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/block_hash_table.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/block_hash_table.png new file mode 100644 index 00000000..91713e75 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/block_hash_table.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/constraints/air_decoder_columns.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/constraints/air_decoder_columns.png new file mode 100644 index 00000000..3db2751e Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/constraints/air_decoder_columns.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/constraints/air_decoder_in_spans_column_constraint.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/constraints/air_decoder_in_spans_column_constraint.png new file mode 100644 index 00000000..e675de26 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/constraints/air_decoder_in_spans_column_constraint.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/constraints/air_decoder_left_right_child.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/constraints/air_decoder_left_right_child.png new file mode 100644 index 00000000..81cbb8d9 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/constraints/air_decoder_left_right_child.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/constraints/air_decoder_op_group_constraint.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/constraints/air_decoder_op_group_constraint.png new file mode 100644 index 00000000..e52d55d2 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/constraints/air_decoder_op_group_constraint.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_OPERATION_batch_flags.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_OPERATION_batch_flags.png new file mode 100644 index 00000000..20cb5f9b Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_OPERATION_batch_flags.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_block_stack_table.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_block_stack_table.png new file mode 100644 index 00000000..b90826ce Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_block_stack_table.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_call_operation.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_call_operation.png new file mode 100644 index 00000000..625a3dda Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_call_operation.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_decoding_span_block_with_push.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_decoding_span_block_with_push.png new file mode 100644 index 00000000..e90eed18 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_decoding_span_block_with_push.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_dyn_block_decoding.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_dyn_block_decoding.png new file mode 100644 index 00000000..955d88bd Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_dyn_block_decoding.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_dyn_operation.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_dyn_operation.png new file mode 100644 index 00000000..fec8d15d Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_dyn_operation.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_dyncall_operation.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_dyncall_operation.png new file mode 100644 index 00000000..b40697d6 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_dyncall_operation.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_end_operation.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_end_operation.png new file mode 100644 index 00000000..2dd02bc7 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_end_operation.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_halt_operation.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_halt_operation.png new file mode 100644 index 00000000..3bb9d563 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_halt_operation.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_imm_vale_op_group_table.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_imm_vale_op_group_table.png new file mode 100644 index 00000000..eb659bf6 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_imm_vale_op_group_table.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_join_block_decoding.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_join_block_decoding.png new file mode 100644 index 00000000..2e3a4370 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_join_block_decoding.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_join_operation.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_join_operation.png new file mode 100644 index 00000000..a2372e41 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_join_operation.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_loop_execution.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_loop_execution.png new file mode 100644 index 00000000..23e010e7 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_loop_execution.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_loop_operation.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_loop_operation.png new file mode 100644 index 00000000..fc9dcb46 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_loop_operation.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_loop_skipping.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_loop_skipping.png new file mode 100644 index 00000000..4ae5bdcf Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_loop_skipping.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_multi_batch_span.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_multi_batch_span.png new file mode 100644 index 00000000..31110232 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_multi_batch_span.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_op_group_table.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_op_group_table.png new file mode 100644 index 00000000..b814ec2b Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_op_group_table.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_op_group_table_after_span_op.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_op_group_table_after_span_op.png new file mode 100644 index 00000000..4efed18d Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_op_group_table_after_span_op.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_op_group_table_multi_span.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_op_group_table_multi_span.png new file mode 100644 index 00000000..1fdf289b Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_op_group_table_multi_span.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_op_group_table_post_respan.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_op_group_table_post_respan.png new file mode 100644 index 00000000..d3d0d862 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_op_group_table_post_respan.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_operation_group_decoding.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_operation_group_decoding.png new file mode 100644 index 00000000..e206f588 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_operation_group_decoding.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_repeat_operation.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_repeat_operation.png new file mode 100644 index 00000000..30194a0e Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_repeat_operation.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_respan_operation.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_respan_operation.png new file mode 100644 index 00000000..899fec54 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_respan_operation.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_single_batch_span.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_single_batch_span.png new file mode 100644 index 00000000..20c3fe0f Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_single_batch_span.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_span_block.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_span_block.png new file mode 100644 index 00000000..c36f719b Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_span_block.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_split_block_decoding.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_split_block_decoding.png new file mode 100644 index 00000000..8503fe9e Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_split_block_decoding.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_split_operation.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_split_operation.png new file mode 100644 index 00000000..265d8226 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_split_operation.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_state_block_hash_2.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_state_block_hash_2.png new file mode 100644 index 00000000..eb5290a9 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_state_block_hash_2.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_state_block_hash_4.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_state_block_hash_4.png new file mode 100644 index 00000000..1a0a8b15 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_state_block_hash_4.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_state_block_hash_6.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_state_block_hash_6.png new file mode 100644 index 00000000..8da273fd Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_state_block_hash_6.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_syscall_operation.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_syscall_operation.png new file mode 100644 index 00000000..63719101 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_syscall_operation.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_trace.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_trace.png new file mode 100644 index 00000000..3db2751e Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/decoder/decoder_trace.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/lookups/logup_component.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/lookups/logup_component.png new file mode 100644 index 00000000..364e7cbd Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/lookups/logup_component.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/lookups/logup_table.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/lookups/logup_table.png new file mode 100644 index 00000000..ef8f1422 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/lookups/logup_table.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/programs/call_block.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/programs/call_block.png new file mode 100644 index 00000000..2f65c502 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/programs/call_block.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/programs/dyn_block.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/programs/dyn_block.png new file mode 100644 index 00000000..ff205816 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/programs/dyn_block.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/programs/join_block.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/programs/join_block.png new file mode 100644 index 00000000..9be9bae1 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/programs/join_block.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/programs/loop_block.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/programs/loop_block.png new file mode 100644 index 00000000..87e09781 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/programs/loop_block.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/programs/mast_of_program.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/programs/mast_of_program.png new file mode 100644 index 00000000..9940676e Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/programs/mast_of_program.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/programs/span_block_creation.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/programs/span_block_creation.png new file mode 100644 index 00000000..acf04c3e Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/programs/span_block_creation.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/programs/split_block.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/programs/split_block.png new file mode 100644 index 00000000..33f1da25 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/programs/split_block.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/programs/syscall_block.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/programs/syscall_block.png new file mode 100644 index 00000000..abd1b331 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/programs/syscall_block.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/range/rc_16_bit_logup.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/range/rc_16_bit_logup.png new file mode 100644 index 00000000..79dadaf6 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/range/rc_16_bit_logup.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/range/rc_16_bit_range_check.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/range/rc_16_bit_range_check.png new file mode 100644 index 00000000..9e705ba5 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/range/rc_16_bit_range_check.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/range/rc_8_bit_logup.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/range/rc_8_bit_logup.png new file mode 100644 index 00000000..b7cc4dbc Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/range/rc_8_bit_logup.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/range/rc_8_bit_range_check.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/range/rc_8_bit_range_check.png new file mode 100644 index 00000000..bcedb9c3 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/range/rc_8_bit_range_check.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/range/rc_better_construction.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/range/rc_better_construction.png new file mode 100644 index 00000000..3f0e66be Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/range/rc_better_construction.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/range/rc_table_post_8_bit_range_check.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/range/rc_table_post_8_bit_range_check.png new file mode 100644 index 00000000..2032e106 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/range/rc_table_post_8_bit_range_check.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/range/rc_with_bridge_rows.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/range/rc_with_bridge_rows.png new file mode 100644 index 00000000..df95f5f7 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/range/rc_with_bridge_rows.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/range/rc_with_bridge_rows.zip b/versioned_docs/version-0.16/reference/miden-vm/img/design/range/rc_with_bridge_rows.zip new file mode 100644 index 00000000..8b4abdeb Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/range/rc_with_bridge_rows.zip differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/crypto_ops/EVALCIRCUIT.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/crypto_ops/EVALCIRCUIT.png new file mode 100644 index 00000000..4fc7b093 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/crypto_ops/EVALCIRCUIT.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/crypto_ops/FRIE2F4.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/crypto_ops/FRIE2F4.png new file mode 100644 index 00000000..01888804 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/crypto_ops/FRIE2F4.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/crypto_ops/HORNERBASE.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/crypto_ops/HORNERBASE.png new file mode 100644 index 00000000..2779d152 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/crypto_ops/HORNERBASE.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/crypto_ops/HORNEREXT.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/crypto_ops/HORNEREXT.png new file mode 100644 index 00000000..80f69d05 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/crypto_ops/HORNEREXT.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/crypto_ops/HPERM.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/crypto_ops/HPERM.png new file mode 100644 index 00000000..f6aabc83 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/crypto_ops/HPERM.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/crypto_ops/MPVERIFY.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/crypto_ops/MPVERIFY.png new file mode 100644 index 00000000..5a642d12 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/crypto_ops/MPVERIFY.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/crypto_ops/MRUPDATE.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/crypto_ops/MRUPDATE.png new file mode 100644 index 00000000..19451d2b Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/crypto_ops/MRUPDATE.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/decorator_operations/DIVRESULTU64.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/decorator_operations/DIVRESULTU64.png new file mode 100644 index 00000000..408e5740 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/decorator_operations/DIVRESULTU64.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/decorator_operations/MERKLENODE.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/decorator_operations/MERKLENODE.png new file mode 100644 index 00000000..68b83399 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/decorator_operations/MERKLENODE.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/field_operations/ADD.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/field_operations/ADD.png new file mode 100644 index 00000000..1cb8fbb9 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/field_operations/ADD.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/field_operations/AND.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/field_operations/AND.png new file mode 100644 index 00000000..c4e4ff69 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/field_operations/AND.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/field_operations/EQ.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/field_operations/EQ.png new file mode 100644 index 00000000..066b8949 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/field_operations/EQ.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/field_operations/EQZ.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/field_operations/EQZ.png new file mode 100644 index 00000000..34055ddc Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/field_operations/EQZ.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/field_operations/EXPACC.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/field_operations/EXPACC.png new file mode 100644 index 00000000..3bf2dd30 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/field_operations/EXPACC.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/field_operations/EXT2MUL.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/field_operations/EXT2MUL.png new file mode 100644 index 00000000..51a369ed Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/field_operations/EXT2MUL.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/field_operations/INCR.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/field_operations/INCR.png new file mode 100644 index 00000000..579e325c Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/field_operations/INCR.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/field_operations/INV.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/field_operations/INV.png new file mode 100644 index 00000000..66085bc1 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/field_operations/INV.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/field_operations/MUL.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/field_operations/MUL.png new file mode 100644 index 00000000..5811b1e9 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/field_operations/MUL.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/field_operations/NEG.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/field_operations/NEG.png new file mode 100644 index 00000000..afcd31b3 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/field_operations/NEG.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/field_operations/NOT.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/field_operations/NOT.png new file mode 100644 index 00000000..9258b81e Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/field_operations/NOT.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/field_operations/OR.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/field_operations/OR.png new file mode 100644 index 00000000..95625324 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/field_operations/OR.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/io_ops/ADVPOP.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/io_ops/ADVPOP.png new file mode 100644 index 00000000..bbbf4de6 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/io_ops/ADVPOP.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/io_ops/ADVPOPW.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/io_ops/ADVPOPW.png new file mode 100644 index 00000000..fe1d6323 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/io_ops/ADVPOPW.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/io_ops/MLOAD.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/io_ops/MLOAD.png new file mode 100644 index 00000000..a770874b Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/io_ops/MLOAD.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/io_ops/MLOADW.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/io_ops/MLOADW.png new file mode 100644 index 00000000..9354925e Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/io_ops/MLOADW.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/io_ops/MSTORE.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/io_ops/MSTORE.png new file mode 100644 index 00000000..d6078703 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/io_ops/MSTORE.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/io_ops/MSTOREW.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/io_ops/MSTOREW.png new file mode 100644 index 00000000..085c6cc7 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/io_ops/MSTOREW.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/io_ops/MSTREAM.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/io_ops/MSTREAM.png new file mode 100644 index 00000000..6fb82bd2 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/io_ops/MSTREAM.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/io_ops/SDEPTH.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/io_ops/SDEPTH.png new file mode 100644 index 00000000..ce5ca608 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/io_ops/SDEPTH.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/overflow_table_layout.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/overflow_table_layout.png new file mode 100644 index 00000000..bcecf65e Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/overflow_table_layout.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/stack_1st_left_shift.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/stack_1st_left_shift.png new file mode 100644 index 00000000..2061cc60 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/stack_1st_left_shift.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/stack_ops/CSWAP.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/stack_ops/CSWAP.png new file mode 100644 index 00000000..55f3104b Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/stack_ops/CSWAP.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/stack_ops/CSWAPW.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/stack_ops/CSWAPW.png new file mode 100644 index 00000000..88dbbe3e Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/stack_ops/CSWAPW.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/stack_ops/DROP.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/stack_ops/DROP.png new file mode 100644 index 00000000..49887e30 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/stack_ops/DROP.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/stack_ops/DUP(n).png b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/stack_ops/DUP(n).png new file mode 100644 index 00000000..fedf5667 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/stack_ops/DUP(n).png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/stack_ops/MOVDN(n).png b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/stack_ops/MOVDN(n).png new file mode 100644 index 00000000..52b032a3 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/stack_ops/MOVDN(n).png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/stack_ops/MOVUP(n).png b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/stack_ops/MOVUP(n).png new file mode 100644 index 00000000..5d0870cb Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/stack_ops/MOVUP(n).png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/stack_ops/PAD.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/stack_ops/PAD.png new file mode 100644 index 00000000..a94ff693 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/stack_ops/PAD.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/stack_ops/SWAP.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/stack_ops/SWAP.png new file mode 100644 index 00000000..5f1dc913 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/stack_ops/SWAP.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/stack_ops/SWAPDW.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/stack_ops/SWAPDW.png new file mode 100644 index 00000000..18fa8680 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/stack_ops/SWAPDW.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/stack_ops/SWAPW.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/stack_ops/SWAPW.png new file mode 100644 index 00000000..8f677269 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/stack_ops/SWAPW.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/stack_ops/SWAPW2.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/stack_ops/SWAPW2.png new file mode 100644 index 00000000..d2d23a20 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/stack_ops/SWAPW2.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/stack_ops/SWAPW3.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/stack_ops/SWAPW3.png new file mode 100644 index 00000000..03f85162 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/stack_ops/SWAPW3.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/stack_overflow_push_2nd_item.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/stack_overflow_push_2nd_item.png new file mode 100644 index 00000000..c0d7ff12 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/stack_overflow_push_2nd_item.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/stack_overflow_table_post_1_right_shift.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/stack_overflow_table_post_1_right_shift.png new file mode 100644 index 00000000..de5b8e16 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/stack_overflow_table_post_1_right_shift.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/stack_overflow_table_post_2_right_shift.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/stack_overflow_table_post_2_right_shift.png new file mode 100644 index 00000000..4fe5824a Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/stack_overflow_table_post_2_right_shift.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/stack_right_shift.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/stack_right_shift.png new file mode 100644 index 00000000..3b0060bc Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/stack_right_shift.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/system_ops/ASSERT.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/system_ops/ASSERT.png new file mode 100644 index 00000000..407a755c Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/system_ops/ASSERT.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/system_ops/CLK.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/system_ops/CLK.png new file mode 100644 index 00000000..ad009037 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/system_ops/CLK.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/trace_layout.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/trace_layout.png new file mode 100644 index 00000000..d12d8fe4 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/trace_layout.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/u32_operations/U32ADD.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/u32_operations/U32ADD.png new file mode 100644 index 00000000..30bbf891 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/u32_operations/U32ADD.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/u32_operations/U32ADD3.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/u32_operations/U32ADD3.png new file mode 100644 index 00000000..9d1e0ac2 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/u32_operations/U32ADD3.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/u32_operations/U32AND.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/u32_operations/U32AND.png new file mode 100644 index 00000000..3d916e2e Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/u32_operations/U32AND.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/u32_operations/U32ASSERT2.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/u32_operations/U32ASSERT2.png new file mode 100644 index 00000000..c32a18b3 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/u32_operations/U32ASSERT2.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/u32_operations/U32DIV.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/u32_operations/U32DIV.png new file mode 100644 index 00000000..e71a6dae Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/u32_operations/U32DIV.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/u32_operations/U32MADD.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/u32_operations/U32MADD.png new file mode 100644 index 00000000..4ee5c262 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/u32_operations/U32MADD.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/u32_operations/U32MUL.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/u32_operations/U32MUL.png new file mode 100644 index 00000000..b1ca4ce0 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/u32_operations/U32MUL.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/u32_operations/U32SPLIT.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/u32_operations/U32SPLIT.png new file mode 100644 index 00000000..9c9c6e88 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/u32_operations/U32SPLIT.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/u32_operations/U32SUB.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/u32_operations/U32SUB.png new file mode 100644 index 00000000..ed7bdb73 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/u32_operations/U32SUB.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/u32_operations/U32XOR.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/u32_operations/U32XOR.png new file mode 100644 index 00000000..1dc6524d Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/stack/u32_operations/U32XOR.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/design/vm_trace.png b/versioned_docs/version-0.16/reference/miden-vm/img/design/vm_trace.png new file mode 100644 index 00000000..62788bca Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/design/vm_trace.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/intro/vm_components.png b/versioned_docs/version-0.16/reference/miden-vm/img/intro/vm_components.png new file mode 100644 index 00000000..109cb8d7 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/intro/vm_components.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/user_docs/assembly/assembly_to_VM.png b/versioned_docs/version-0.16/reference/miden-vm/img/user_docs/assembly/assembly_to_VM.png new file mode 100644 index 00000000..2e395046 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/user_docs/assembly/assembly_to_VM.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/user_docs/assembly/execution_contexts/context_transitions.png b/versioned_docs/version-0.16/reference/miden-vm/img/user_docs/assembly/execution_contexts/context_transitions.png new file mode 100644 index 00000000..2dd3bb49 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/user_docs/assembly/execution_contexts/context_transitions.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/user_docs/assembly/execution_contexts/mem_layout.png b/versioned_docs/version-0.16/reference/miden-vm/img/user_docs/assembly/execution_contexts/mem_layout.png new file mode 100644 index 00000000..70588dd9 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/user_docs/assembly/execution_contexts/mem_layout.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/img/user_docs/assembly/overview/miden_vm_overview.png b/versioned_docs/version-0.16/reference/miden-vm/img/user_docs/assembly/overview/miden_vm_overview.png new file mode 100644 index 00000000..8572b458 Binary files /dev/null and b/versioned_docs/version-0.16/reference/miden-vm/img/user_docs/assembly/overview/miden_vm_overview.png differ diff --git a/versioned_docs/version-0.16/reference/miden-vm/index.md b/versioned_docs/version-0.16/reference/miden-vm/index.md new file mode 100644 index 00000000..8b499359 --- /dev/null +++ b/versioned_docs/version-0.16/reference/miden-vm/index.md @@ -0,0 +1,43 @@ +--- +title: "Introduction" +sidebar_position: 1 +--- + +# Introduction +Miden VM is a zero-knowledge virtual machine written in Rust. For any program executed on Miden VM, a STARK-based proof of execution is automatically generated. This proof can then be used by anyone to verify that the program was executed correctly without the need for re-executing the program or even knowing the contents of the program. + +## Status and features +Miden VM is currently on release v0.19. In this release, most of the core features of the VM have been stabilized, and most of the STARK proof generation has been implemented. While we expect to keep making changes to the VM internals, the external interfaces should remain relatively stable, and we will do our best to minimize the amount of breaking changes going forward. + +At this point, Miden VM is good enough for experimentation, and even for real-world applications, but it is not yet ready for production use. The codebase has not been audited and contains known and unknown bugs and security flaws. + +### Feature highlights +Miden VM is a fully-featured virtual machine. Despite being optimized for zero-knowledge proof generation, it provides all the features one would expect from a regular VM. To highlight a few: + +* **Flow control.** Miden VM is Turing-complete and supports familiar flow control structures such as conditional statements and counter/condition-controlled loops. There are no restrictions on the maximum number of loop iterations or the depth of control flow logic. +* **Procedures.** Miden assembly programs can be broken into subroutines called *procedures*. This improves code modularity and helps reduce the size of Miden VM programs. +* **Execution contexts.** Miden VM program execution can span multiple isolated contexts, each with its own dedicated memory space. The contexts are separated into the *root context* and *user contexts*. The root context can be accessed from user contexts via customizable kernel calls. +* **Memory.** Miden VM supports read-write random-access memory. Procedures can reserve portions of global memory for easier management of local variables. +* **u32 operations.** Miden VM supports native operations with 32-bit unsigned integers. This includes basic arithmetic, comparison, and bitwise operations. +* **Cryptographic operations.** Miden assembly provides built-in instructions for computing hashes and verifying Merkle paths. These instructions use Poseidon2 hash function (which is the native hash function of the VM). +* **External libraries.** Miden VM supports compiling programs against pre-defined libraries. The VM ships with one such library: `miden-core-lib` which adds support for such things as 64-bit unsigned integers. Developers can build other similar libraries to extend the VM's functionality in ways which fit their use cases. +* **Nondeterminism**. Unlike traditional virtual machines, Miden VM supports nondeterministic programming. This means a prover may do additional work outside of the VM and then provide execution *hints* to the VM. These hints can be used to dramatically speed up certain types of computations, as well as to supply secret inputs to the VM. +* **Customizable hosts.** Miden VM can be instantiated with user-defined hosts. These hosts are used to supply external data to the VM during execution/proof generation (via nondeterministic inputs) and can connect the VM to arbitrary data sources (e.g., a database or RPC calls). + +### Planned features +In the coming months we plan to finalize the design of the VM and implement support for the following features: + +* **Recursive proofs.** Miden VM will soon support efficient STARK-verifiers as pre-compiles, enabling infinitely recursive proofs and enabling proof composition in layers. This is an extremely useful tool for real-world applications that need to verify large numbers of proofs or build complex proof systems. +* **Better debugging.** Miden VM will provide a better debugging experience including the ability to place breakpoints, better source mapping, and more complete program analysis info. +* **Faulty execution.** Miden VM will support generating proofs for programs with faulty execution (a notoriously complex task in ZK context). That is, it will be possible to prove that execution of some program resulted in an error. + +## Structure of this document +This document is meant to provide an in-depth description of Miden VM. It is organized as follows: + +* In the introduction, we provide a high-level overview of Miden VM and describe how to run simple programs. +* In the user documentation section, we provide developer-focused documentation useful to those who want to develop on Miden VM or build compilers from higher-level languages to Miden assembly (the native language of Miden VM). +* In the design section, we provide in-depth descriptions of the VM's internals, including all AIR constraints for the proving system. We also provide the rationale for settling on specific design choices. +* Finally, in the background material section, we provide references to materials which could be useful for learning more about STARKs - the proving system behind Miden VM. + +## License +This project is dual-licensed under the [MIT](http://opensource.org/licenses/MIT) and [Apache 2.0](https://opensource.org/license/apache-2-0) licenses. diff --git a/versioned_docs/version-0.16/reference/miden-vm/overview.md b/versioned_docs/version-0.16/reference/miden-vm/overview.md new file mode 100644 index 00000000..d1d00960 --- /dev/null +++ b/versioned_docs/version-0.16/reference/miden-vm/overview.md @@ -0,0 +1,59 @@ +--- +title: "Overview" +sidebar_position: 2 +--- + +# Miden VM overview + +Miden VM is a stack machine. The base data type of the VM is a field element in a 64-bit [prime field](https://en.wikipedia.org/wiki/Finite_field) defined by modulus $p = 2^{64} - 2^{32} + 1$. This means that all values that the VM operates with are field elements in this field (i.e., values between $0$ and $2^{64} - 2^{32}$, both inclusive). + +Miden VM consists of four high-level components as illustrated below. + +![vm_components](./img/intro/vm_components.png) + +These components are: + +- **Stack** which is a push-down stack where each item is a field element. Most assembly instructions operate with values located on the stack. The stack can grow up to $2^{32}$ items deep, however, only the top 16 items are directly accessible. +- **Memory** which is a linear random-access read-write memory. The memory is element-addressable, meaning, a single element is located at each address. However, there are instructions to read and write elements to/from memory both individually or in batches of four, since the latter is quite common. Memory addresses can be in the range $[0, 2^{32})$. +- **Chiplets** which are specialized circuits for accelerating certain types of computations. These include Poseidon2 hash function, 32-bit binary operations, and 16-bit range checks. +- **Host** which is a way for the prover to communicate with the VM during runtime. This includes responding to the VM's requests for non-deterministic inputs and handling messages sent by the VM (e.g., for debugging purposes). The requests for non-deterministic inputs are handled by the host's _advice provider_. + +Miden VM comes with a default implementation of the host interface (with an in-memory advice provider). However, the users are able to provide their own implementations which can connect the VM to arbitrary data sources (e.g., a database or RPC calls) and define custom logic for handling events emitted by the VM. + +## Writing programs + +Our goal is to make Miden VM an easy compilation target for high-level languages such as Rust, Move, Sway, and others. We believe it is important to let people write programs in the languages of their choice. However, compilers to help with this have not been developed yet. Thus, for now, the primary way to write programs for Miden VM is to use [Miden assembly](./user_docs/assembly/index.md). + +While writing programs in assembly is far from ideal, Miden assembly does make this task a little bit easier by supporting high-level flow control structures and named procedures. + +## Inputs and outputs + +External inputs can be provided to Miden VM in two ways: + +1. Public inputs can be supplied to the VM by initializing the stack with desired values before a program starts executing. At most 16 values can be initialized in this way, so providing more than 16 values will cause an error. +2. Secret (or nondeterministic) inputs can be supplied to the VM via the [_advice provider_](#nondeterministic-inputs). There is no limit on how much data the advice provider can hold. + +After a program finishes executing, the elements remaining on the stack become the outputs of the program. Notice that having more than 16 values on the stack at the end of execution will cause an error, so the values beyond the top 16 elements of the stack should be dropped. We've provided the [`truncate_stack`](./user_docs/core_lib/sys.md) utility procedure in the core library for this purpose. + +The number of public inputs and outputs of a program can be reduced by making use of the advice stack and Merkle trees. Just 4 elements are sufficient to represent a root of a Merkle tree, which can be expanded into an arbitrary number of values. + +For example, if we wanted to provide a thousand public input values to the VM, we could put these values into a Merkle tree, initialize the stack with the root of this tree, initialize the advice provider with the tree itself, and then retrieve values from the tree during program execution using `mtree_get` instruction (described [here](./user_docs/assembly/cryptographic_operations.md#hashing-and-merkle-trees)). + +### Stack depth restrictions + +For reasons explained [here](./design/stack/index.md), the VM imposes the restriction that the stack depth cannot be smaller than $16$. This has the following effects: + +- When initializing a program with fewer than $16$ inputs, the VM will pad the stack with zeros to ensure the depth is $16$ at the beginning of execution. +- If an operation would result in the stack depth dropping below $16$, the VM will insert a zero at the deep end of the stack to make sure the depth stays at $16$. + +### Nondeterministic inputs + +The _advice provider_ component is responsible for supplying nondeterministic inputs to the VM. These inputs only need to be known to the prover (i.e., they do not need to be shared with the verifier). + +The advice provider consists of three components: + +- **Advice stack** which is a one-dimensional array of field elements. Being a stack, the VM can either push new elements onto the advice stack, or pop the elements from its top. +- **Advice map** which is a key-value map where keys are words and values are vectors (dynamic arrays) of field elements. The VM can copy values from the advice map onto the advice stack as well as insert new values into the advice map (e.g., from a region of memory). +- **Merkle store** which contain structured data reducible to Merkle paths. Some examples of such structures are: Merkle tree, Sparse Merkle Tree, and a collection of Merkle paths. The VM can request Merkle paths from the Merkle store, as well as mutate it by updating or merging nodes contained in the store. + +The prover initializes the advice provider prior to executing a program, and from that point on the advice provider is manipulated solely by executing operations on the VM. **Security note:** When reading data from the advice provider, we can't assume it is valid - we always have to verify this (e.g., when writing and then reading the data, we need to make sure that the data we read hashes to some expected value). diff --git a/versioned_docs/version-0.16/reference/miden-vm/performance.md b/versioned_docs/version-0.16/reference/miden-vm/performance.md new file mode 100644 index 00000000..66d9ec3c --- /dev/null +++ b/versioned_docs/version-0.16/reference/miden-vm/performance.md @@ -0,0 +1,58 @@ +--- +title: "Performance" +sidebar_position: 4 +--- + +# Performance + +The benchmarks below should be viewed only as a rough guide for expected future performance. The reasons that many optimizations have not been applied yet, and we expect that there will be some speedup once we dedicate some time to performance optimizations. + +A few general notes on performance: + +- Execution time is dominated by proof generation time. In fact, the time needed to run the program is usually under 0.01% of the time needed to generate the proof. +- Proof verification time is really fast. In most cases it is under 1 ms, but sometimes gets as high as 2 ms or 3 ms. +- Proof generation process is dynamically adjustable. In general, there is a trade-off between execution time, proof size, and security level (i.e. for a given security level, we can reduce proof size by increasing execution time, up to a point). +- Both proof generation and proof verification times are greatly influenced by the hash function used in the STARK protocol. In the benchmarks below, we use BLAKE3, which is a really fast hash function. + +## Single-core prover performance + +When executed on a single CPU core, the current version of Miden VM operates at around 20 - 25 KHz. In the benchmarks below, the VM executes a [Blake3 example](https://github.com/0xMiden/miden-vm/tree/next/miden-vm/masm-examples/hashing/blake3_1to1) program on Apple M4 Max CPU in a single thread. The generated proofs have a target security level of 96 bits. + +| VM cycles | Execution time | Proving time | RAM consumed | Proof size | +| :------------: | :------------: | :----------: | :----------: | :--------: | +| 214 | 0.3 ms | 885 ms | 200 MB | 80 KB | +| 216 | 0.7 ms | 3.6 sec | 750 MB | 100 KB | +| 218 | 1.2 ms | 14.7 sec | 2.9 GB | 116 KB | +| 220 | 11.1 ms | 59 sec | 11 GB | 136 KB | + +As can be seen from the above, proving time roughly doubles with every doubling in the number of cycles, but proof size grows much slower. + +## Multi-core prover performance + +STARK proof generation is massively parallelizable. Thus, by taking advantage of multiple CPU cores we can dramatically reduce proof generation time. For example, when executed on an 16-core CPU (Apple M4 Max), the current version of Miden VM operates at around 170 KHz. And when executed on a 64-core CPU (Amazon Graviton 4), the VM operates at around 200 KHz. + +In the benchmarks below, the VM executes the same Blake3 example program for 220 cycles at 96-bit target security level: + +| Machine | Execution time | Proving time | Execution % | Implied Frequency | +| ------------------------------ | :------------: | :----------: | :---------: | :---------------: | +| Apple M1 Pro (16 threads) | 14.5 ms | 14.7 sec | 0.1% | 70 KHz | +| Apple M4 Max (16 threads) | 6 ms | 5.9 sec | 0.2% | 170 KHz | +| Amazon Graviton 4 (64 threads) | 11 ms | 4.9 sec | 0.2% | 205 KHz | +| AMD EPYC 9R45 (64 threads) | 7.5 ms | 3.7 sec | 0.2% | 270 KHz | +| AMD Ryzen 9 9950X (16 threads) | 7.2 ms | 7.2 sec | 0.1% | 145 KHz | +| AMD Ryzen 9 9950X (32 threads) | 6.5 ms | 6.5 sec | 0.1% | 161 KHz | + +## Recursion-friendly proofs + +Proofs in the above benchmarks are generated using BLAKE3 hash function. While this hash function is very fast, it is not very efficient to execute in Miden VM. Thus, proofs generated using BLAKE3 are not well-suited for recursive proof verification. To support efficient recursive proofs, we need to use an arithmetization-friendly hash function. Miden VM natively supports Poseidon2, which is one such hash function. One of the downsides of arithmetization-friendly hash functions is that they are noticeably slower than regular hash functions. + +In the benchmarks below we execute the same Blake3 example program for 220 cycles at 96-bit target security level using Poseidon2 hash function instead of BLAKE3: + +| Machine | Execution time | Proving time | Slowdown vs BLAKE3 | +| ------------------------------ | :------------: | :----------: | :----------------: | +| Apple M1 Pro (16 threads) | 14.5 ms | 31.9 sec | 2.2x | +| Apple M4 Max (16 threads) | 6 ms | 10.1 sec | 1.7x | +| Amazon Graviton 4 (64 threads) | 11 ms | 7.7 sec | 1.6x | +| AMD EPYC 9R45 (64 threads) | 7.5 ms | 6.9 sec | 1.9x | +| AMD Ryzen 9 9950X (16 threads) | 7.2 ms | 16.0 sec | 2.2x | +| AMD Ryzen 9 9950X (32 threads) | 6.5 ms | 12.9 sec | 2.0x | diff --git a/versioned_docs/version-0.16/reference/miden-vm/theme/Admonition/Icon/Danger.tsx b/versioned_docs/version-0.16/reference/miden-vm/theme/Admonition/Icon/Danger.tsx new file mode 100644 index 00000000..ab14d7de --- /dev/null +++ b/versioned_docs/version-0.16/reference/miden-vm/theme/Admonition/Icon/Danger.tsx @@ -0,0 +1,19 @@ +import React, { type ReactNode } from "react"; +import type { Props } from "@theme/Admonition/Icon/Danger"; + +export default function AdmonitionIconDanger(props: Props): ReactNode { + return ( + + + + ); +} diff --git a/versioned_docs/version-0.16/reference/miden-vm/theme/Admonition/Icon/Info.tsx b/versioned_docs/version-0.16/reference/miden-vm/theme/Admonition/Icon/Info.tsx new file mode 100644 index 00000000..59e48a52 --- /dev/null +++ b/versioned_docs/version-0.16/reference/miden-vm/theme/Admonition/Icon/Info.tsx @@ -0,0 +1,20 @@ +import React, { type ReactNode } from "react"; +import type { Props } from "@theme/Admonition/Icon/Info"; + +export default function AdmonitionIconInfo(props: Props): ReactNode { + return ( + + + + ); +} diff --git a/versioned_docs/version-0.16/reference/miden-vm/theme/Admonition/Icon/Note.tsx b/versioned_docs/version-0.16/reference/miden-vm/theme/Admonition/Icon/Note.tsx new file mode 100644 index 00000000..d7c524b3 --- /dev/null +++ b/versioned_docs/version-0.16/reference/miden-vm/theme/Admonition/Icon/Note.tsx @@ -0,0 +1,20 @@ +import React, { type ReactNode } from "react"; +import type { Props } from "@theme/Admonition/Icon/Note"; + +export default function AdmonitionIconNote(props: Props): ReactNode { + return ( + + + + ); +} diff --git a/versioned_docs/version-0.16/reference/miden-vm/theme/Admonition/Icon/Tip.tsx b/versioned_docs/version-0.16/reference/miden-vm/theme/Admonition/Icon/Tip.tsx new file mode 100644 index 00000000..219bb8d0 --- /dev/null +++ b/versioned_docs/version-0.16/reference/miden-vm/theme/Admonition/Icon/Tip.tsx @@ -0,0 +1,20 @@ +import React, { type ReactNode } from "react"; +import type { Props } from "@theme/Admonition/Icon/Tip"; + +export default function AdmonitionIconTip(props: Props): ReactNode { + return ( + + + + ); +} diff --git a/versioned_docs/version-0.16/reference/miden-vm/theme/Admonition/Icon/Warning.tsx b/versioned_docs/version-0.16/reference/miden-vm/theme/Admonition/Icon/Warning.tsx new file mode 100644 index 00000000..f96398d1 --- /dev/null +++ b/versioned_docs/version-0.16/reference/miden-vm/theme/Admonition/Icon/Warning.tsx @@ -0,0 +1,20 @@ +import React, { type ReactNode } from "react"; +import type { Props } from "@theme/Admonition/Icon/Warning"; + +export default function AdmonitionIconCaution(props: Props): ReactNode { + return ( + + + + ); +} diff --git a/versioned_docs/version-0.16/reference/miden-vm/theme/Admonition/Layout/index.tsx b/versioned_docs/version-0.16/reference/miden-vm/theme/Admonition/Layout/index.tsx new file mode 100644 index 00000000..7b2c170d --- /dev/null +++ b/versioned_docs/version-0.16/reference/miden-vm/theme/Admonition/Layout/index.tsx @@ -0,0 +1,51 @@ +import React, { type ReactNode } from "react"; +import clsx from "clsx"; +import { ThemeClassNames } from "@docusaurus/theme-common"; + +import type { Props } from "@theme/Admonition/Layout"; + +import styles from "./styles.module.css"; + +function AdmonitionContainer({ + type, + className, + children, +}: Pick & { children: ReactNode }) { + return ( +
+ {children} +
+ ); +} + +function AdmonitionHeading({ icon, title }: Pick) { + return ( +
+ {icon} + {/* {title} */} +
+ ); +} + +function AdmonitionContent({ children }: Pick) { + return children ? ( +
{children}
+ ) : null; +} + +export default function AdmonitionLayout(props: Props): ReactNode { + const { type, icon, title, children, className } = props; + return ( + + {title || icon ? : null} + {children} + + ); +} diff --git a/versioned_docs/version-0.16/reference/miden-vm/theme/Admonition/Layout/styles.module.css b/versioned_docs/version-0.16/reference/miden-vm/theme/Admonition/Layout/styles.module.css new file mode 100644 index 00000000..88df7e63 --- /dev/null +++ b/versioned_docs/version-0.16/reference/miden-vm/theme/Admonition/Layout/styles.module.css @@ -0,0 +1,35 @@ +.admonition { + margin-bottom: 1em; +} + +.admonitionHeading { + font: var(--ifm-heading-font-weight) var(--ifm-h5-font-size) / + var(--ifm-heading-line-height) var(--ifm-heading-font-family); + text-transform: uppercase; +} + +/* Heading alone without content (does not handle fragment content) */ +.admonitionHeading:not(:last-child) { + margin-bottom: 0.3rem; +} + +.admonitionHeading code { + text-transform: none; +} + +.admonitionIcon { + display: inline-block; + vertical-align: middle; + margin-right: 0.4em; +} + +.admonitionIcon svg { + display: inline-block; + height: 1.6em; + width: 1.6em; + fill: var(--ifm-alert-foreground-color); +} + +.admonitionContent > :last-child { + margin-bottom: 0; +} diff --git a/versioned_docs/version-0.16/reference/miden-vm/theme/Admonition/Type/Caution.tsx b/versioned_docs/version-0.16/reference/miden-vm/theme/Admonition/Type/Caution.tsx new file mode 100644 index 00000000..b570a37a --- /dev/null +++ b/versioned_docs/version-0.16/reference/miden-vm/theme/Admonition/Type/Caution.tsx @@ -0,0 +1,32 @@ +import React, {type ReactNode} from 'react'; +import clsx from 'clsx'; +import Translate from '@docusaurus/Translate'; +import type {Props} from '@theme/Admonition/Type/Caution'; +import AdmonitionLayout from '@theme/Admonition/Layout'; +import IconWarning from '@theme/Admonition/Icon/Warning'; + +const infimaClassName = 'alert alert--warning'; + +const defaultProps = { + icon: , + title: ( + + caution + + ), +}; + +// TODO remove before v4: Caution replaced by Warning +// see https://github.com/facebook/docusaurus/issues/7558 +export default function AdmonitionTypeCaution(props: Props): ReactNode { + return ( + + {props.children} + + ); +} diff --git a/versioned_docs/version-0.16/reference/miden-vm/theme/Admonition/Type/Danger.tsx b/versioned_docs/version-0.16/reference/miden-vm/theme/Admonition/Type/Danger.tsx new file mode 100644 index 00000000..49901fa9 --- /dev/null +++ b/versioned_docs/version-0.16/reference/miden-vm/theme/Admonition/Type/Danger.tsx @@ -0,0 +1,30 @@ +import React, {type ReactNode} from 'react'; +import clsx from 'clsx'; +import Translate from '@docusaurus/Translate'; +import type {Props} from '@theme/Admonition/Type/Danger'; +import AdmonitionLayout from '@theme/Admonition/Layout'; +import IconDanger from '@theme/Admonition/Icon/Danger'; + +const infimaClassName = 'alert alert--danger'; + +const defaultProps = { + icon: , + title: ( + + danger + + ), +}; + +export default function AdmonitionTypeDanger(props: Props): ReactNode { + return ( + + {props.children} + + ); +} diff --git a/versioned_docs/version-0.16/reference/miden-vm/theme/Admonition/Type/Info.tsx b/versioned_docs/version-0.16/reference/miden-vm/theme/Admonition/Type/Info.tsx new file mode 100644 index 00000000..018e0a16 --- /dev/null +++ b/versioned_docs/version-0.16/reference/miden-vm/theme/Admonition/Type/Info.tsx @@ -0,0 +1,30 @@ +import React, {type ReactNode} from 'react'; +import clsx from 'clsx'; +import Translate from '@docusaurus/Translate'; +import type {Props} from '@theme/Admonition/Type/Info'; +import AdmonitionLayout from '@theme/Admonition/Layout'; +import IconInfo from '@theme/Admonition/Icon/Info'; + +const infimaClassName = 'alert alert--info'; + +const defaultProps = { + icon: , + title: ( + + info + + ), +}; + +export default function AdmonitionTypeInfo(props: Props): ReactNode { + return ( + + {props.children} + + ); +} diff --git a/versioned_docs/version-0.16/reference/miden-vm/theme/Admonition/Type/Note.tsx b/versioned_docs/version-0.16/reference/miden-vm/theme/Admonition/Type/Note.tsx new file mode 100644 index 00000000..c99e0385 --- /dev/null +++ b/versioned_docs/version-0.16/reference/miden-vm/theme/Admonition/Type/Note.tsx @@ -0,0 +1,30 @@ +import React, {type ReactNode} from 'react'; +import clsx from 'clsx'; +import Translate from '@docusaurus/Translate'; +import type {Props} from '@theme/Admonition/Type/Note'; +import AdmonitionLayout from '@theme/Admonition/Layout'; +import IconNote from '@theme/Admonition/Icon/Note'; + +const infimaClassName = 'alert alert--secondary'; + +const defaultProps = { + icon: , + title: ( + + note + + ), +}; + +export default function AdmonitionTypeNote(props: Props): ReactNode { + return ( + + {props.children} + + ); +} diff --git a/versioned_docs/version-0.16/reference/miden-vm/theme/Admonition/Type/Tip.tsx b/versioned_docs/version-0.16/reference/miden-vm/theme/Admonition/Type/Tip.tsx new file mode 100644 index 00000000..18604a5e --- /dev/null +++ b/versioned_docs/version-0.16/reference/miden-vm/theme/Admonition/Type/Tip.tsx @@ -0,0 +1,30 @@ +import React, {type ReactNode} from 'react'; +import clsx from 'clsx'; +import Translate from '@docusaurus/Translate'; +import type {Props} from '@theme/Admonition/Type/Tip'; +import AdmonitionLayout from '@theme/Admonition/Layout'; +import IconTip from '@theme/Admonition/Icon/Tip'; + +const infimaClassName = 'alert alert--success'; + +const defaultProps = { + icon: , + title: ( + + tip + + ), +}; + +export default function AdmonitionTypeTip(props: Props): ReactNode { + return ( + + {props.children} + + ); +} diff --git a/versioned_docs/version-0.16/reference/miden-vm/theme/Admonition/Type/Warning.tsx b/versioned_docs/version-0.16/reference/miden-vm/theme/Admonition/Type/Warning.tsx new file mode 100644 index 00000000..61d9597b --- /dev/null +++ b/versioned_docs/version-0.16/reference/miden-vm/theme/Admonition/Type/Warning.tsx @@ -0,0 +1,30 @@ +import React, {type ReactNode} from 'react'; +import clsx from 'clsx'; +import Translate from '@docusaurus/Translate'; +import type {Props} from '@theme/Admonition/Type/Warning'; +import AdmonitionLayout from '@theme/Admonition/Layout'; +import IconWarning from '@theme/Admonition/Icon/Warning'; + +const infimaClassName = 'alert alert--warning'; + +const defaultProps = { + icon: , + title: ( + + warning + + ), +}; + +export default function AdmonitionTypeWarning(props: Props): ReactNode { + return ( + + {props.children} + + ); +} diff --git a/versioned_docs/version-0.16/reference/miden-vm/theme/Admonition/Types.tsx b/versioned_docs/version-0.16/reference/miden-vm/theme/Admonition/Types.tsx new file mode 100644 index 00000000..2a100190 --- /dev/null +++ b/versioned_docs/version-0.16/reference/miden-vm/theme/Admonition/Types.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import AdmonitionTypeNote from '@theme/Admonition/Type/Note'; +import AdmonitionTypeTip from '@theme/Admonition/Type/Tip'; +import AdmonitionTypeInfo from '@theme/Admonition/Type/Info'; +import AdmonitionTypeWarning from '@theme/Admonition/Type/Warning'; +import AdmonitionTypeDanger from '@theme/Admonition/Type/Danger'; +import AdmonitionTypeCaution from '@theme/Admonition/Type/Caution'; +import type AdmonitionTypes from '@theme/Admonition/Types'; + +const admonitionTypes: typeof AdmonitionTypes = { + note: AdmonitionTypeNote, + tip: AdmonitionTypeTip, + info: AdmonitionTypeInfo, + warning: AdmonitionTypeWarning, + danger: AdmonitionTypeDanger, +}; + +// Undocumented legacy admonition type aliases +// Provide hardcoded/untranslated retrocompatible label +// See also https://github.com/facebook/docusaurus/issues/7767 +const admonitionAliases: typeof AdmonitionTypes = { + secondary: (props) => , + important: (props) => , + success: (props) => , + caution: AdmonitionTypeCaution, +}; + +export default { + ...admonitionTypes, + ...admonitionAliases, +}; diff --git a/versioned_docs/version-0.16/reference/miden-vm/theme/Admonition/index.tsx b/versioned_docs/version-0.16/reference/miden-vm/theme/Admonition/index.tsx new file mode 100644 index 00000000..8f4225da --- /dev/null +++ b/versioned_docs/version-0.16/reference/miden-vm/theme/Admonition/index.tsx @@ -0,0 +1,21 @@ +import React, {type ComponentType, type ReactNode} from 'react'; +import {processAdmonitionProps} from '@docusaurus/theme-common'; +import type {Props} from '@theme/Admonition'; +import AdmonitionTypes from '@theme/Admonition/Types'; + +function getAdmonitionTypeComponent(type: string): ComponentType { + const component = AdmonitionTypes[type]; + if (component) { + return component; + } + console.warn( + `No admonition component found for admonition type "${type}". Using Info as fallback.`, + ); + return AdmonitionTypes.info!; +} + +export default function Admonition(unprocessedProps: Props): ReactNode { + const props = processAdmonitionProps(unprocessedProps); + const AdmonitionTypeComponent = getAdmonitionTypeComponent(props.type); + return ; +} diff --git a/versioned_docs/version-0.16/reference/miden-vm/theme/DocCard/index.tsx b/versioned_docs/version-0.16/reference/miden-vm/theme/DocCard/index.tsx new file mode 100644 index 00000000..a95b4901 --- /dev/null +++ b/versioned_docs/version-0.16/reference/miden-vm/theme/DocCard/index.tsx @@ -0,0 +1,15 @@ +import React, { type ReactNode } from "react"; +import DocCard from "@theme-original/DocCard"; +import type DocCardType from "@theme/DocCard"; +import type { WrapperProps } from "@docusaurus/types"; +import styles from "./styles.module.css"; + +type Props = WrapperProps; + +export default function DocCardWrapper(props: Props): ReactNode { + return ( +
+ +
+ ); +} diff --git a/versioned_docs/version-0.16/reference/miden-vm/theme/DocCard/styles.module.css b/versioned_docs/version-0.16/reference/miden-vm/theme/DocCard/styles.module.css new file mode 100644 index 00000000..06c3b039 --- /dev/null +++ b/versioned_docs/version-0.16/reference/miden-vm/theme/DocCard/styles.module.css @@ -0,0 +1,36 @@ +/* Custom DocCard styling - fix underlines and internal spacing */ + +.customCard { + display: block; + margin-top: 0 !important; + margin-bottom: 1rem; + padding-top: 0 !important; +} + +/* Fix internal card padding */ +.customCard :global(.card__header) { + padding: 1rem 1rem 0.5rem 1rem !important; +} + +.customCard :global(.card__body) { + padding: 0.5rem 1rem 1rem 1rem !important; +} + +/* Remove any top margin/padding from card title */ +.customCard :global(.card__title), +.customCard :global(.card__header h2), +.customCard :global(.card__header h3) { + margin-top: 0 !important; + padding-top: 0 !important; +} + +/* Remove underlines from all card links */ +.customCard :global(a), +.customCard :global(.card__header a), +.customCard :global(.card__body a), +.customCard :global(h2 a), +.customCard :global(h3 a), +.customCard :global(p a) { + text-decoration: none !important; +} + diff --git a/versioned_docs/version-0.16/reference/miden-vm/tools/_category_.yml b/versioned_docs/version-0.16/reference/miden-vm/tools/_category_.yml new file mode 100644 index 00000000..af33c248 --- /dev/null +++ b/versioned_docs/version-0.16/reference/miden-vm/tools/_category_.yml @@ -0,0 +1,4 @@ +label: "Development tooling" +# Determines where this documentation section appears relative to other sections in the parent folder +position: 5 +collapsed: true diff --git a/versioned_docs/version-0.16/reference/miden-vm/tools/index.md b/versioned_docs/version-0.16/reference/miden-vm/tools/index.md new file mode 100644 index 00000000..0a1cb80c --- /dev/null +++ b/versioned_docs/version-0.16/reference/miden-vm/tools/index.md @@ -0,0 +1,18 @@ +--- +title: "Development tooling" +sidebar_position: 1 +--- + +# Development Tools and Resources + +The following tools are available for interacting with Miden VM: + +- Via the [miden-vm](https://crates.io/crates/miden-vm) crate (or within the Miden VM repo): + - [CLI](../usage.md#cli-interface) +- Via your browser: + - The interactive [Miden VM Playground](https://0xMiden.github.io/examples/) for writing, executing, proving, and verifying programs from your browser. + +The following resources are available to help you get started programming with Miden VM more quickly: + +- The [Miden VM examples repo](https://github.com/0xMiden/examples) contains examples of programs written in Miden Assembly. +- The [Miden project template](https://github.com/0xMiden/project-template) provides a scaffold for starting new Miden projects in Rust. diff --git a/versioned_docs/version-0.16/reference/miden-vm/usage.md b/versioned_docs/version-0.16/reference/miden-vm/usage.md new file mode 100644 index 00000000..30e9ac5e --- /dev/null +++ b/versioned_docs/version-0.16/reference/miden-vm/usage.md @@ -0,0 +1,172 @@ +--- +title: "Usage" +sidebar_position: 3 +--- + +# Usage + +Before you can use Miden VM, you'll need to make sure you have Rust [installed](https://www.rust-lang.org/tools/install). Miden VM v0.19 requires Rust version **1.96** or later. + +Miden VM consists of several crates, each of which exposes a small set of functionality. The most notable of these crates are: + +- [miden-processor](https://crates.io/crates/miden-processor), which can be used to execute Miden VM programs. +- [miden-prover](https://crates.io/crates/miden-prover), which can be used to execute Miden VM programs and generate proofs of their execution. +- [miden-verifier](https://crates.io/crates/miden-verifier), which can be used to verify proofs of program execution generated by Miden VM prover. + +The above functionality is also exposed via the single [miden-vm](https://crates.io/crates/miden-vm) crate, which also provides a CLI interface for interacting with Miden VM. + +## CLI interface + +### Compiling Miden VM + +To compile Miden VM into a binary, we have a [Makefile](https://www.gnu.org/software/make/manual/make.html) with the following tasks: + +```shell +make exec +``` + +This will place an optimized, multi-threaded `miden-vm` executable into the `./target/optimized` directory. It is equivalent to executing: + +```shell +cargo build --profile optimized --features concurrent,executable +``` + +If you would like to enable single-threaded mode, you can compile Miden VM using the following command: + +```shell +make exec-single +``` + +### Controlling parallelism + +Internally, Miden VM uses [rayon](https://github.com/rayon-rs/rayon) for parallel computations. To control the number of threads used to generate a STARK proof, you can use `RAYON_NUM_THREADS` environment variable. + +### SIMD acceleration + +Miden VM execution and proof generation can be accelerated via vectorized instructions. Currently, SIMD acceleration can be enabled on platforms supporting [SVE]() and [AVX2](https://en.wikipedia.org/wiki/Advanced_Vector_Extensions#Advanced_Vector_Extensions_2) instructions. + +To compile Miden VM with AVX2 acceleration enabled, you can run the following command: + +```shell +make exec-avx2 +``` + +To compile Miden VM with SVE acceleration enabled, you can run the following command: + +```shell +make exec-sve +``` + +This will place the resulting `miden-vm` executable into the `./target/optimized` directory. + +SVE/AVX2 acceleration is currently applicable only to recursive proofs which can be generated using the `-r` flag. + +### Running Miden VM + +Once the executable has been compiled, you can run Miden VM like so: + +```shell +./target/optimized/miden-vm [subcommand] [parameters] +``` + +Currently, Miden VM can be executed with the following subcommands: + +- `run` - this will execute a Miden assembly program and output the result, but will not generate a proof of execution. +- `prove` - this will execute a Miden assembly program, and will also generate a STARK proof of execution. +- `verify` - this will verify a previously generated proof of execution for a given program. +- `compile` - this will compile a Miden assembly program (i.e., build a program [MAST](./design/programs.md)) and outputs stats about the compilation process. +- `example` - this will execute a Miden assembly example program, generate a STARK proof of execution and verify it. Currently, it is possible to run `blake3` and `fibonacci` examples. + +All of the above subcommands require various parameters to be provided. To get more detailed help on what is needed for a given subcommand, you can run the following: + +```shell +./target/optimized/miden-vm [subcommand] --help +``` + +For example: + +```shell +./target/optimized/miden-vm prove --help +``` + +To execute a program using the Miden VM there needs to be a `.masm` file containing the Miden Assembly code and a `.inputs` file containing the inputs. + +#### Enabling logging + +You can use `MIDEN_LOG` environment variable to control how much logging output the VM produces. For example: + +```shell +MIDEN_LOG=trace ./target/optimized/miden-vm [subcommand] [parameters] +``` + +If the level is not specified, `warn` level is set as default. + +#### Debugging procedures + +You can inspect VM state by importing the [`miden::core::debug`](./user_docs/assembly/debugging.md) module and calling its print procedures from a MASM program. + +```shell +./target/optimized/miden-vm run [path_to.masm] +``` + +If trace building would exceed the VM trace row limit, `run` returns a trace length error instead of trying to build a larger trace. + +### Inputs + +As described [here](https://docs.miden.xyz/miden-vm/overview#inputs-and-outputs) the Miden VM can consume public and secret inputs. + +- Public inputs: + - `operand_stack` - can be supplied to the VM to initialize the stack with the desired values before a program starts executing. If the number of provided input values is less than 16, the input stack will be padded with zeros to the length of 16. The maximum number of the stack inputs is limited by 16 values, providing more than 16 values will cause an error. +- Secret (or nondeterministic) inputs: + - `advice_stack` - can be supplied to the VM. There is no limit on how much data the advice provider can hold. This is provided as a string array where each string entry represents a field element. + - `advice_map` - is supplied as a map of 64-character hex keys, each mapped to an array of numbers. The hex keys are interpreted as 4 field elements and the arrays of numbers are interpreted as arrays of field elements. + - `merkle_store` - the Merkle store is container that allows the user to define `merkle_tree`, `sparse_merkle_tree` and `partial_merkle_tree` data structures. + - `merkle_tree` - is supplied as an array of 64-character hex values where each value represents a leaf (4 elements) in the tree. + - `sparse_merkle_tree` - is supplied as an array of tuples of the form (number, 64-character hex string). The number represents the leaf index and the hex string represents the leaf value (4 elements). + - `partial_merkle_tree` - is supplied as an array of tuples of the form ((number, number), 64-character hex string). The internal tuple represents the leaf depth and index at this depth, and the hex string represents the leaf value (4 elements). + +_Check out the [comparison example](https://github.com/0xMiden/examples/blob/main/examples/comparison.masm) to see how secret inputs work._ + +After a program finishes executing, the elements that remain on the stack become the outputs of the program. Notice that the number of values on the operand stack at the end of the program execution can not be greater than 16, otherwise the program will return an error. The [`truncate_stack`](./user_docs/core_lib/sys.md) utility procedure from the core library could be used to conveniently truncate the stack at the end of the program. + +## Fibonacci example + +In the `miden-vm/masm-examples/fib` directory, we provide a very simple Fibonacci calculator example. This example computes the 1001st term of the Fibonacci sequence. You can execute this example on Miden VM like so: + +```shell +./target/optimized/miden-vm run miden-vm/masm-examples/fib/fib.masm +``` + +### Capturing Output + +This will run the example code to completion and will output the top element remaining on the stack. + +If you want the output of the program in a file, you can use the `--output` or `-o` flag and specify the path to the output file. For example: + +```shell +./target/optimized/miden-vm run miden-vm/masm-examples/fib/fib.masm -o fib.out +``` + +This will dump the output of the program into the `fib.out` file. The output file will contain the state of the stack at the end of the program execution. + +### Running with `miden::core::debug` procedures + +Inside `miden-vm/masm-examples/fib/fib.masm`, import the `miden::core::debug` module before `begin`, then call one of its procedures, such as `exec.debug::print_stack`, anywhere between `begin` and `end`: + +```masm +use miden::core::debug + +begin + # ... + exec.debug::print_stack + # ... +end +``` + +Then run: + +```shell +./target/optimized/miden-vm run miden-vm/masm-examples/fib/fib.masm +``` + +You should see output similar to "Stack state before step ..." diff --git a/versioned_docs/version-0.16/reference/miden-vm/user_docs/_category_.yml b/versioned_docs/version-0.16/reference/miden-vm/user_docs/_category_.yml new file mode 100644 index 00000000..62ab4f48 --- /dev/null +++ b/versioned_docs/version-0.16/reference/miden-vm/user_docs/_category_.yml @@ -0,0 +1,4 @@ +label: "User Documentation" +# Determines where this documentation section appears relative to other sections in the parent folder +position: 6 +collapsed: true diff --git a/versioned_docs/version-0.16/reference/miden-vm/user_docs/assembly/_category_.yml b/versioned_docs/version-0.16/reference/miden-vm/user_docs/assembly/_category_.yml new file mode 100644 index 00000000..6b8f36ca --- /dev/null +++ b/versioned_docs/version-0.16/reference/miden-vm/user_docs/assembly/_category_.yml @@ -0,0 +1,4 @@ +label: "Miden Assembly" +# Determines where this documentation section appears relative to other sections in the parent folder +position: 1 +collapsed: true diff --git a/versioned_docs/version-0.16/reference/miden-vm/user_docs/assembly/code_organization.md b/versioned_docs/version-0.16/reference/miden-vm/user_docs/assembly/code_organization.md new file mode 100644 index 00000000..d9b3e35f --- /dev/null +++ b/versioned_docs/version-0.16/reference/miden-vm/user_docs/assembly/code_organization.md @@ -0,0 +1,475 @@ +--- +title: "Code Organization" +sidebar_position: 2 +--- + +## Code organization +A Miden assembly program is just a sequence of instructions each describing a specific directive or an operation. You can use any combination of whitespace characters to separate one instruction from another. + +In turn, Miden assembly instructions are just keywords which can be parameterized by zero or more parameters. The notation for specifying parameters is *keyword.param1.param2* - i.e., the parameters are separated by periods. For example, `push.123` instruction denotes a `push` operation which is parameterized by value `123`. + +Miden assembly programs are organized into procedures. Procedures, in turn, can be grouped into modules. + +### Procedures +A *procedure* can be used to encapsulate a frequently-used sequence of instructions which can later be invoked via a label. A procedure is introduced using the `proc` keyword. Procedure definitions consist of the following parts, in the order they appear: + +* Zero or more attributes which modify the procedure definition, or annotate it in some way. These can be user-defined, but there are also built-in attributes that modify the procedure itself. Currently, the only built-in attribute is `@locals(N)`, which specifies the number of procedure locals allocated for the procedure. See the [Procedure Attributes](#procedure-attributes) section for more on their syntax and semantics. +* An optional visibility modifier, i.e. `pub` if the procedure is to be exported from the containing module. +* The `proc` keyword +* The procedure name/label +* An optional type signature. See the [Types](#types) section for more details. +* The body of the procedure, consisting of one or more instructions, followed by `end` + +The following is a complete demonstration of the syntax: + +``` +@locals(2) +pub proc foo(a: felt, b: felt) -> felt + add +end +``` + +A procedure label must start with a letter and can contain any combination of numbers, ASCII letters, and underscores (`_`). Should you need to represent a label with other characters, an extended set is permitted via quoted identifiers, i.e. an identifier surrounded by `".."`. Quoted identifiers additionally allow any alphanumeric letter (ASCII or UTF-8), as well as various common punctuation characters: `!`, `?`, `:`, `.`, `<`, `>`, and `-`. Quoted identifiers are primarily intended for representing symbols/identifiers when compiling higher-level languages to Miden Assembly, but can be used anywhere that normal identifiers are expected. + +The number of locals, given by `@locals(N)`, specifies that the procedure requires `N` elements of scratch memory be allocated for use by the VM when it is executed. The default number of locals is 0, so this attribute is not required unless a non-zero number of locals is required. The individual elements of the procedure local memory may be accessed by zero-based index using `loc_load`/`loc_store`, or treated like regular memory by obtaining the address of a specific slot via `locaddr`, and then using any other memory instruction. See [here](./io_operations.md#random-access-memory) for more information on those instructions. A procedure can have at most $2^{16}$ locals, and the total number of locals available to all procedures at runtime is limited to $2^{31} - 1$. Note that the assembler internally always rounds up the number of declared locals to the nearest multiple of 4. + +To execute a procedure, the `exec.