diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 9277fb01..66ea321f 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -41,7 +41,10 @@ jobs: runs-on: ubuntu-latest env: DEFAULT_REF: next - DEFAULT_TUTORIALS_REF: main + # v0.14 pins tutorials to the PR #186 branch (kbg/chore/v14-migration) + # so the live /next/ ingest matches what v0.14 snapshot was cut from. + # TODO: flip back to `main` once 0xMiden/tutorials#186 merges upstream. + DEFAULT_TUTORIALS_REF: kbg/chore/v14-migration steps: - name: Checkout docs site uses: actions/checkout@v4 diff --git a/.release/release-manifest.yml b/.release/release-manifest.yml index 9177a21b..9d4e5a80 100644 --- a/.release/release-manifest.yml +++ b/.release/release-manifest.yml @@ -1,9 +1,9 @@ -version: "0.13" # the label to snapshot (used if no manual override) +version: "0.14" # the label to snapshot (used if no manual override) refs: # exact, immutable refs per source repo (tags or release branches) - protocol: "refs/tags/v0.13.3" - node: "refs/tags/v0.13.4" - miden-client: "refs/tags/v0.13.0" - tutorials: "refs/heads/main" - miden-vm: "refs/tags/v0.20.6" - compiler: "refs/tags/0.7.0" -next_version: "0.14" + protocol: "refs/tags/v0.14.4" + node: "refs/tags/v0.14.9" + miden-client: "refs/tags/v0.14.4" + tutorials: "refs/pull/186/head" + miden-vm: "refs/tags/v0.22.1" + compiler: "refs/tags/v0.8.1" +next_version: "0.15" diff --git a/docs/builder/get-started/accounts.md b/docs/builder/get-started/accounts.md index 518cdd60..b25fad61 100644 --- a/docs/builder/get-started/accounts.md +++ b/docs/builder/get-started/accounts.md @@ -165,7 +165,7 @@ 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/rust-compiler/). +For detailed frontend setup guidance (React, wallets, UI), see the [Tutorials section](../tutorials/). ::: ## Creating Accounts Programmatically diff --git a/docs/builder/get-started/your-first-smart-contract/create.md b/docs/builder/get-started/your-first-smart-contract/create.md index 30663787..c930bde7 100644 --- a/docs/builder/get-started/your-first-smart-contract/create.md +++ b/docs/builder/get-started/your-first-smart-contract/create.md @@ -143,7 +143,7 @@ struct CounterContract { } ``` -The `#[component]` attribute marks this as a Miden [Account component](/core-concepts/miden-base/account). The `count_map` field is a `StorageMap` stored in a named storage slot of the account. In v0.13, storage slots are identified by name rather than explicit index numbers — the slot name is derived automatically from the component's package name and field name (e.g., `miden::component::miden_counter_account::count_map`). +The `#[component]` attribute marks this as a Miden [Account component](/core-concepts/protocol/account). The `count_map` field is a `StorageMap` stored in a named storage slot of the account. In v0.13, storage slots are identified by name rather than explicit index numbers — the slot name is derived automatically from the component's package name and field name (e.g., `miden::component::miden_counter_account::count_map`). **Important**: Storage slots in Miden hold `Word` values, which are composed of four field elements (`Felt`). Each `Felt` is a 64-bit unsigned integer (u64). The `StorageMap` provides a key-value interface within a single storage slot, allowing you to store multiple key-value pairs within the four-element word structure. diff --git a/docs/builder/get-started/your-first-smart-contract/test.md b/docs/builder/get-started/your-first-smart-contract/test.md index a47c5bbc..961c0637 100644 --- a/docs/builder/get-started/your-first-smart-contract/test.md +++ b/docs/builder/get-started/your-first-smart-contract/test.md @@ -304,7 +304,7 @@ Congratulations! You've successfully completed the Miden smart contract quick st To deepen your knowledge, we recommend exploring the following resources: -- Visit the [Tutorials section](../../tutorials/rust-compiler/) for detailed, hands-on guides on topics such as contract interactions, advanced storage, custom note scripting, and integrating with external applications. +- 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 of core concepts, consult the [Core Concepts section](../../../core-concepts/) 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/docs/builder/index.md b/docs/builder/index.md index 3725aefe..a8abbed2 100644 --- a/docs/builder/index.md +++ b/docs/builder/index.md @@ -8,10 +8,6 @@ pagination_next: null Accounts, notes, and transactions — authored in Rust, compiled to MASM, proved client-side. - - v0.13 is the current stable release and the target of these docs. v0.14 ships with the next docs snapshot. See the [migration guide](./migration/) for what changed. - - ## Start here @@ -29,7 +25,7 @@ Accounts, notes, and transactions — authored in Rust, compiled to MASM, proved Accounts, notes, storage, components, transactions — the full Rust SDK surface. - + Real-world examples: the Miden Bank, private multisig, custom note scripts. diff --git a/docs/builder/tools/clients/index.md b/docs/builder/tools/clients/index.md index 3da4c9bb..60b678b3 100644 --- a/docs/builder/tools/clients/index.md +++ b/docs/builder/tools/clients/index.md @@ -1,30 +1,52 @@ --- -title: Client +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 --- -# Miden client - -The Miden client is the user-facing entry point to the Miden network. It manages accounts, builds and executes transactions, produces zero-knowledge proofs, and synchronises local state with the node. The same client logic ships across four consumer surfaces so you can pick the runtime that fits your application: - -| Surface | Package | Best for | -| --- | --- | --- | -| **Rust library** | `miden-client` crate | Native services, proving infrastructure, tests | -| **Rust CLI** | `miden-client` binary | Scripting, local exploration, ops workflows | -| **Web SDK** | `@miden-sdk/miden-sdk` (npm) | Browser and Node apps, Electron, service workers | -| **React SDK** | `@miden-sdk/react` (npm) | React / Next.js / React Native dApps | - -Navigate to each surface via the sidebar on the left. - -## How the surfaces relate - -- The **Rust library** contains the core state machine, transaction executor, prover, keystore abstraction, and note transport. -- The **Rust CLI** wraps the library and exposes its functionality as commands. -- The **Web SDK** compiles the Rust library to WebAssembly and exposes a typed JavaScript API (the `MidenClient` class). It is the canonical TypeScript/JavaScript entry point. -- The **React SDK** wraps the Web SDK with a `MidenProvider` and a family of hooks (`useMiden`, `useAccount`, `useSend`, …). It shares the exact same on-chain semantics. - -Pick whichever surface matches your application — each section documents the full API for that runtime. - -## Common topics - -Errors, diagnostics, and other behaviour that is shared across all surfaces is documented once under **Common errors** in the sidebar. +# 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 on-chain 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 / React Native app for instant Miden integration. + + + +## Shared topics + + + + 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/docs/builder/tutorials/helpers/pitfalls.md b/docs/builder/tutorials/helpers/pitfalls.md index a13eee66..23ae65c7 100644 --- a/docs/builder/tutorials/helpers/pitfalls.md +++ b/docs/builder/tutorials/helpers/pitfalls.md @@ -502,4 +502,4 @@ See these patterns in context in the [miden-bank repository](https://github.com/ - **[Debugging Guide](./debugging)** - Troubleshoot errors - **[Testing Guide](./testing)** - MockChain patterns -- **[Miden Bank Tutorial](../tutorials/miden-bank/)** - See these patterns in context +- **[Miden Bank Tutorial](../miden-bank/)** - See these patterns in context diff --git a/docs/builder/tutorials/helpers/testing.md b/docs/builder/tutorials/helpers/testing.md index 55f736c5..ff2bdc58 100644 --- a/docs/builder/tutorials/helpers/testing.md +++ b/docs/builder/tutorials/helpers/testing.md @@ -607,4 +607,4 @@ See the complete test implementations in the [miden-bank repository](https://git - **[Debugging Guide](./debugging)** - Troubleshoot common issues - **[Common Pitfalls](./pitfalls)** - Avoid known gotchas -- **[Miden Bank Tutorial](../tutorials/miden-bank/)** - See testing in action +- **[Miden Bank Tutorial](../miden-bank/)** - See testing in action diff --git a/docs/builder/tutorials/miden-bank/index.md b/docs/builder/tutorials/miden-bank/index.md index d28e4af4..46f918ab 100644 --- a/docs/builder/tutorials/miden-bank/index.md +++ b/docs/builder/tutorials/miden-bank/index.md @@ -31,31 +31,31 @@ Every part builds on the previous one and includes: ## Walkthrough - + Create your project with `miden new` and understand the workspace structure. - + Learn `#[component]`, `Value` storage, and `StorageMap` for managing state. - + Define constants and validate inputs with assertions. - + Handle fungible assets with vault operations and balance tracking. - + Write scripts that execute when notes are consumed. - + Call account methods from note scripts via bindings. - + Write scripts for account initialization and owner operations. - + Create P2ID notes programmatically for withdrawals. - + Walk through end-to-end deposit and withdraw operations. @@ -96,13 +96,13 @@ cd miden-tutorials/examples/miden-bank ## Supplementary guides - + Learn to test your contracts with MockChain for local simulation. - + Interpret errors and debug common issues. - + Avoid known issues and limitations. @@ -113,4 +113,4 @@ cd miden-tutorials/examples/miden-bank - Join the [Build on Miden](https://t.me/BuildOnMiden) Telegram for support. - Review the complete code in the [examples/miden-bank](https://github.com/0xMiden/miden-tutorials/tree/main/examples/miden-bank) directory. -Ready? Start with [Part 0: Project setup](./00-project-setup). +Ready? Start with [Part 0: Project setup](./project-setup). diff --git a/docusaurus.config.ts b/docusaurus.config.ts index c5481acc..3347014e 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -343,7 +343,7 @@ const config: Config = { items: [ { label: "Get started", to: "/builder/get-started" }, { label: "Smart contracts", to: "/builder/smart-contracts" }, - { label: "Tutorials", to: "/builder/tutorials/rust-compiler" }, + { label: "Tutorials", to: "/builder/tutorials" }, { label: "Tools", to: "/builder/tools" }, { label: "Migration", to: "/builder/migration" }, ], diff --git a/sidebars.ts b/sidebars.ts index 01be4ee2..2112d7a8 100644 --- a/sidebars.ts +++ b/sidebars.ts @@ -90,7 +90,26 @@ const sidebars: SidebarsConfig = { label: "Tutorials", link: { type: "doc", id: "builder/tutorials/index" }, items: [ - "builder/tutorials/miden-bank/index", + { + type: "category", + label: "Miden Bank", + link: { type: "doc", id: "builder/tutorials/miden-bank/index" }, + collapsed: true, + items: [ + // Docusaurus's default numberPrefixParser strips the "00-" style + // prefix from the doc ID, so files like 00-project-setup.md get + // the doc ID "project-setup" (and URL slug to match). + "builder/tutorials/miden-bank/project-setup", + "builder/tutorials/miden-bank/account-components", + "builder/tutorials/miden-bank/constants-constraints", + "builder/tutorials/miden-bank/asset-management", + "builder/tutorials/miden-bank/note-scripts", + "builder/tutorials/miden-bank/cross-component-calls", + "builder/tutorials/miden-bank/transaction-scripts", + "builder/tutorials/miden-bank/output-notes", + "builder/tutorials/miden-bank/complete-flows", + ], + }, { type: "category", label: "Recipes", @@ -122,10 +141,7 @@ const sidebars: SidebarsConfig = { link: { type: "doc", id: "builder/tutorials/recipes/web/index" }, collapsed: true, items: [ - // NOTE: "builder/tutorials/recipes/web/setup_guide" lives - // only on 0xMiden/tutorials#186 (kbg/chore/v14-migration). - // Re-add once that PR merges into tutorials' main — - // it'll be part of the v0.14 release branch meanwhile. + "builder/tutorials/recipes/web/setup_guide", "builder/tutorials/recipes/web/counter_contract_tutorial", "builder/tutorials/recipes/web/create_deploy_tutorial", "builder/tutorials/recipes/web/mint_consume_create_tutorial", @@ -158,6 +174,44 @@ const sidebars: SidebarsConfig = { label: "Clients", link: { type: "doc", id: "builder/tools/clients/index" }, items: [ + { + type: "category", + label: "Rust", + link: { type: "doc", id: "builder/tools/clients/rust-client/index" }, + items: [ + "builder/tools/clients/rust-client/install-and-run", + "builder/tools/clients/rust-client/features", + "builder/tools/clients/rust-client/design", + { + type: "category", + label: "Get started", + link: { type: "doc", id: "builder/tools/clients/rust-client/get-started/index" }, + items: [ + "builder/tools/clients/rust-client/get-started/create-account-use-faucet", + "builder/tools/clients/rust-client/get-started/p2p-public", + "builder/tools/clients/rust-client/get-started/p2p-private", + ], + }, + { + type: "category", + label: "CLI", + link: { type: "doc", id: "builder/tools/clients/rust-client/cli/index" }, + items: [ + "builder/tools/clients/rust-client/cli/cli-config", + "builder/tools/clients/rust-client/cli/cli-troubleshooting", + ], + }, + "builder/tools/clients/rust-client/examples", + "builder/tools/clients/rust-client/library", + "builder/tools/clients/rust-client/api-docs", + // NOTE: debugging.md exists in v0.14.4 (and the v0.14 + // snapshot) but was removed on miden-client's `next` + // branch. The live /next/ build ingests from `next`, + // so listing it here would fail sidebar validation. + // Re-add if a future miden-vNN release brings the page + // back. + ], + }, { type: "category", label: "TypeScript", diff --git a/src/components/VersionNote/index.tsx b/src/components/VersionNote/index.tsx index f7d2d5b6..a1ec7df5 100644 --- a/src/components/VersionNote/index.tsx +++ b/src/components/VersionNote/index.tsx @@ -8,7 +8,7 @@ interface VersionNoteProps { export default function VersionNote({ subject = "Full documentation", - stableVersion = "v0.13", + stableVersion = "v0.14", }: VersionNoteProps): JSX.Element { return (
diff --git a/versioned_docs/version-0.14/builder/_category_.json b/versioned_docs/version-0.14/builder/_category_.json new file mode 100644 index 00000000..34d2480b --- /dev/null +++ b/versioned_docs/version-0.14/builder/_category_.json @@ -0,0 +1,6 @@ +{ + "label": "Builder", + "position": 1, + "collapsible": false, + "collapsed": false +} diff --git a/versioned_docs/version-0.14/builder/faq.md b/versioned_docs/version-0.14/builder/faq.md new file mode 100644 index 00000000..cefcf9cb --- /dev/null +++ b/versioned_docs/version-0.14/builder/faq.md @@ -0,0 +1,101 @@ +# 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? + +### 1. Transaction Creation + +- User creates a transaction specifying the operations to perform (transfers, contract interactions, etc.) +- Client performs preliminary validation of the transaction and its structure +- The user authorizes the specified state transitions by signing the transaction + +### 2. Transaction Submission + +- The signed transaction is submitted to Miden network nodes +- The transaction enters the mempool (transaction pool) where it waits to be selected to be included in the state +- Nodes perform basic validation checks on the transaction structure and signature + +### 3. Transaction Selection + +- A sequencer (or multiple sequencers in a decentralized setting) selects transactions from the mempool +- The sequencer groups transactions into bundles based on state access patterns and other criteria +- The transaction execution order is determined according to protocol mechanism + +### 4. Transaction Execution + +- The current state relevant to the transaction is loaded +- The Miden VM executes the transaction operations +- **State Transition Computation**: The resulting state transitions are computed +- An execution trace of the transaction is generated which captures all the computation + +### 5. Proof Generation + +- A STARK based cryptographic proof is generated attesting to the correctness of the execution +- A proof for the aggregated transaction is created + +### 6. Block Production + +- The aggregated bundle of transactions along with their proofs are assembled into a block +- A recursive proof attesting to all bundle proofs is generated +- The block data structure is finalized with the aggregated proof + +### 7. L1 Submission + +- Transaction data is posted to the data availability layer +- The block proof and state delta commitment are submitted to the Miden contract (that is bridged to Ethereum/Agglayer) +- The L1 contract verifies validity of the proof +- Upon successful verification, the L1 contract updates the state root + +### 8. Finalization + +- Transaction receipts and events are generated +- The global state commitment is updated to reflect the new state +- The transaction is now considered finalized on the L1 +- Users and indexers get notified/updated about the transaction completion + +## 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 rollup. 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 works in Miden? + +Miden does not yet have a fully operational bridge, work in progress. + +## What does the gas fee model of Miden look like? + +Miden does not yet have a fully implemented fee model, work in progress. diff --git a/versioned_docs/version-0.14/builder/get-started/_category_.json b/versioned_docs/version-0.14/builder/get-started/_category_.json new file mode 100644 index 00000000..f230021f --- /dev/null +++ b/versioned_docs/version-0.14/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.14/builder/get-started/accounts.md b/versioned_docs/version-0.14/builder/get-started/accounts.md new file mode 100644 index 00000000..b25fad61 --- /dev/null +++ b/versioned_docs/version-0.14/builder/get-started/accounts.md @@ -0,0 +1,411 @@ +--- +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 between public or private storage modes + +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 + +**Storage Modes:** + +- **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 type, storage mode and version. + pub id: [u8; 15], + + /// Determines mutability of the account (immutable, mutable). + pub account_type: AccountType, + + /// Storage placement preference (public/private). + pub storage_mode: StorageMode, + + /// 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. "RpoFalcon512"). + pub authentication: AuthenticationDescriptor, +} +``` + +**Account Types:** + +```rust +pub enum AccountType { + // Faucet that can issue fungible assets + FungibleFaucet = 2, + // Faucet that can issue non-fungible assets + NonFungibleFaucet = 3, + // Regular account with immutable code + RegularAccountImmutableCode = 0, + /// Regular account with updateable code + RegularAccountUpdatableCode = 1, +} +``` + +**Storage Modes:** + +```rust +pub enum StorageMode { + /// State stored onchain and publicly readable. + Public, + /// Only a commitment is onchain; full state is held privately by the owner. + Private, +} +``` + +
+ +## 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 +``` + +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, AccountStorageMode, AccountType, + }, + auth::AuthSecretKey, + builder::ClientBuilder, + keystore::{FilesystemKeyStore, Keystore}, + rpc::{Endpoint, GrpcClient}, +}; +use miden_client_sqlite_store::ClientBuilderSqliteExt; +use rand::RngCore; +use std::sync::Arc; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + // Initialize RPC connection + let endpoint = Endpoint::testnet(); + let timeout_ms = 10_000; + let rpc_client = Arc::new(GrpcClient::new(&endpoint, timeout_ms)); + + // 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() + .rpc(rpc_client) + .sqlite_store(store_path) + .authenticator(keystore.clone()) + .in_debug_mode(true.into()) + .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::RegularAccountUpdatableCode) + .storage_mode(AccountStorageMode::Public) + .with_auth_component(AuthSingleSig::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, AccountType } 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({ + type: AccountType.MutableWallet, // Standard wallet with upgradeable code + 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, BasicFungibleFaucet}, + AccountBuilder, AccountStorageMode, AccountType, + }, + asset::TokenSymbol, + auth::AuthSecretKey, + builder::ClientBuilder, + keystore::{FilesystemKeyStore, Keystore}, + rpc::{Endpoint, GrpcClient}, + Felt, +}; +use miden_client_sqlite_store::ClientBuilderSqliteExt; +use rand::RngCore; +use std::sync::Arc; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + // Initialize RPC connection + let endpoint = Endpoint::testnet(); + let timeout_ms = 10_000; + let rpc_client = Arc::new(GrpcClient::new(&endpoint, timeout_ms)); + + // 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() + .rpc(rpc_client) + .sqlite_store(store_path) + .authenticator(keystore.clone()) + .in_debug_mode(true.into()) + .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 = Felt::new(1_000_000); + + // Generate key pair + let key_pair = AuthSecretKey::new_falcon512_poseidon2(); + + // Build the account + let builder = AccountBuilder::new(init_seed) + .account_type(AccountType::FungibleFaucet) + .storage_mode(AccountStorageMode::Public) + .with_auth_component(AuthSingleSig::new( + key_pair.public_key().to_commitment(), + AuthScheme::Falcon512Poseidon2, + )) + .with_component(BasicFungibleFaucet::new(symbol, decimals, max_supply)?); + + let faucet_account = builder.build()?; + + 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, AccountType } 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: AccountType.FungibleFaucet, + 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:** + +- **Regular Accounts**: Standard smart contract wallets that can hold assets and execute custom logic +- **Faucet Accounts**: Specialized accounts with minting permissions for tokens + +**Storage Modes:** + +- **Public**: Account state is fully transparent and visible onchain +- **Private**: Only cryptographic commitments are stored onchain, with full state maintained privately + +**Modular Components:** + +- **BasicWallet**: Provides asset management functionality +- **BasicFungibleFaucet**: Enables token minting capabilities +- **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.14/builder/get-started/index.md b/versioned_docs/version-0.14/builder/get-started/index.md new file mode 100644 index 00000000..120fb9a1 --- /dev/null +++ b/versioned_docs/version-0.14/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**: every transaction, note, and account in Miden is private by default — only the involved parties can view asset amounts or transfer details + +## 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.14/builder/get-started/notes.md b/versioned_docs/version-0.14/builder/get-started/notes.md new file mode 100644 index 00000000..8a189c0d --- /dev/null +++ b/versioned_docs/version-0.14/builder/get-started/notes.md @@ -0,0 +1,861 @@ +--- +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. + +## 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` notes are visible onchain and are stored by the Miden network, while `Private` notes are not stored by the network and must be exchanged directly between parties via other channels. +- **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, BasicFungibleFaucet, BasicWallet}, + AccountBuilder, AccountStorageMode, AccountType, + }, + asset::{FungibleAsset, TokenSymbol}, + auth::AuthSecretKey, + builder::ClientBuilder, + keystore::{FilesystemKeyStore, Keystore}, + note::NoteType, + rpc::{Endpoint, GrpcClient}, + transaction::TransactionRequestBuilder, + Felt, +}; +use miden_client_sqlite_store::ClientBuilderSqliteExt; +use rand::RngCore; +use std::sync::Arc; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + // Initialize RPC connection + let endpoint = Endpoint::testnet(); + let timeout_ms = 10_000; + let rpc_client = Arc::new(GrpcClient::new(&endpoint, timeout_ms)); + + // 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() + .rpc(rpc_client) + .sqlite_store(store_path) + .authenticator(keystore.clone()) + .in_debug_mode(true.into()) + .build() + .await?; + + client.sync_state().await?; + + //------------------------------------------------------------ + // CREATING A FAUCET AND MINTING TOKENS + //------------------------------------------------------------ + + // 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 = Felt::new(1_000_000); + + // 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(init_seed) + .account_type(AccountType::RegularAccountUpdatableCode) + .storage_mode(AccountStorageMode::Public) + .with_auth_component(AuthSingleSig::new( + alice_key_pair.public_key().to_commitment(), + AuthScheme::Falcon512Poseidon2, + )) + .with_component(BasicWallet); + + // Build the faucet + let faucet_builder = AccountBuilder::new(init_seed) + .account_type(AccountType::FungibleFaucet) + .storage_mode(AccountStorageMode::Public) + .with_auth_component(AuthSingleSig::new( + faucet_key_pair.public_key().to_commitment(), + AuthScheme::Falcon512Poseidon2, + )) + .with_component(BasicFungibleFaucet::new(symbol, decimals, max_supply)?); + + let alice_account = account_builder.build()?; + let faucet_account = faucet_builder.build()?; + + 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?; + + let amount: u64 = 1000; + 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 + 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, AccountType } from "@miden-sdk/miden-sdk"; + +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({ + type: AccountType.MutableWallet, + storage: "public", // Public: account state is visible on-chain + }); + 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: AccountType.FungibleFaucet, + symbol: "TEST", + decimals, + maxSupply, + storage: "public", + }); + console.log("Faucet account ID:", faucet.id().toString()); + + // 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, // faucet is the executing account + to: alice, + amount: 1000n, + type: "public", // note visibility + }); + console.log("Mint transaction submitted successfully, ID:", txId.toString()); +} +``` + +
+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 is a complete, self-contained example that includes the setup and minting steps from the previous section. **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, BasicFungibleFaucet, BasicWallet}, + Account, AccountBuilder, AccountStorageMode, AccountType, + }, + asset::{FungibleAsset, TokenSymbol}, + auth::AuthSecretKey, + builder::ClientBuilder, + keystore::{FilesystemKeyStore, Keystore}, + note::NoteType, + rpc::{Endpoint, GrpcClient}, + transaction::TransactionRequestBuilder, + Felt, +}; +use miden_client_sqlite_store::ClientBuilderSqliteExt; +use rand::RngCore; +use std::sync::Arc; +use tokio::time::Duration; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + // Initialize RPC connection + let endpoint = Endpoint::testnet(); + let timeout_ms = 10_000; + let rpc_client = Arc::new(GrpcClient::new(&endpoint, timeout_ms)); + + // 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() + .rpc(rpc_client) + .sqlite_store(store_path) + .authenticator(keystore.clone()) + .in_debug_mode(true.into()) + .build() + .await?; + + client.sync_state().await?; + + //------------------------------------------------------------ + // CREATING A FAUCET AND MINTING TOKENS + //------------------------------------------------------------ + + // 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 = Felt::new(1_000_000); + + // 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(init_seed) + .account_type(AccountType::RegularAccountUpdatableCode) + .storage_mode(AccountStorageMode::Public) + .with_auth_component(AuthSingleSig::new( + alice_key_pair.public_key().to_commitment(), + AuthScheme::Falcon512Poseidon2, + )) + .with_component(BasicWallet); + + // Build the faucet + let faucet_builder = AccountBuilder::new(init_seed) + .account_type(AccountType::FungibleFaucet) + .storage_mode(AccountStorageMode::Public) + .with_auth_component(AuthSingleSig::new( + faucet_key_pair.public_key().to_commitment(), + AuthScheme::Falcon512Poseidon2, + )) + .with_component(BasicFungibleFaucet::new(symbol, decimals, max_supply)?); + + let alice_account = account_builder.build()?; + let faucet_account = faucet_builder.build()?; + + 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?; + + let amount: u64 = 1000; + 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 + 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. + 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() { + println!("Waiting for P2ID note to be comitted..."); + 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(); + println!( + "Alice's TEST token balance: {:?}", + vault.get_balance(faucet_account.id()) + ); + + break; // Exit the loop after consuming the note + } + + Ok(()) +} +``` + +```typescript title="src/demo.ts" +import { MidenClient, AccountType } from "@miden-sdk/miden-sdk"; + +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({ + type: AccountType.MutableWallet, + 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: AccountType.FungibleFaucet, + symbol: "TEST", + decimals, + maxSupply, + storage: "public", + }); + console.log("Faucet account ID:", faucet.id().toString()); + + // 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, + to: alice, + amount: 1000n, + type: "public", + waitForConfirmation: true, + }); + console.log( + "Mint transaction submitted successfully, ID:", + mintResult.txId.toString(), + ); + + // List notes available to Alice and consume them — tokens move into her vault. + const notes = await client.notes.listAvailable({ account: alice }); + const consumeResult = await client.transactions.consume({ + account: alice, + notes: [notes[0]], + waitForConfirmation: true, + }); + console.log( + "Consume transaction submitted successfully, ID:", + consumeResult.txId.toString(), + ); + + // Read Alice's TEST token balance directly via the accounts resource. + const balance = await client.accounts.getBalance(alice, faucet); + 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: Ok(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 + +This approach means Alice and Bob's transactions are completely separate and unlinkable, providing strong privacy guarantees. + +Let's implement the complete flow - mint, consume, then send: + +:::tip +This is a complete, self-contained example that includes all previous steps. **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, BasicFungibleFaucet, BasicWallet}, + Account, AccountBuilder, AccountId, AccountStorageMode, AccountType, + }, + asset::{FungibleAsset, TokenSymbol}, + auth::AuthSecretKey, + builder::ClientBuilder, + keystore::{FilesystemKeyStore, Keystore}, + note::{NoteAttachment, NoteType, P2idNote}, + rpc::{Endpoint, GrpcClient}, + transaction::TransactionRequestBuilder, + Felt, +}; +use miden_client_sqlite_store::ClientBuilderSqliteExt; +use rand::RngCore; +use std::sync::Arc; +use tokio::time::Duration; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + // Initialize RPC connection + let endpoint = Endpoint::testnet(); + let timeout_ms = 10_000; + let rpc_client = Arc::new(GrpcClient::new(&endpoint, timeout_ms)); + + // 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() + .rpc(rpc_client) + .sqlite_store(store_path) + .authenticator(keystore.clone()) + .in_debug_mode(true.into()) + .build() + .await?; + + client.sync_state().await?; + + //------------------------------------------------------------ + // CREATING A FAUCET AND MINTING TOKENS + //------------------------------------------------------------ + + // 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 = Felt::new(1_000_000); + + // 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(init_seed) + .account_type(AccountType::RegularAccountUpdatableCode) + .storage_mode(AccountStorageMode::Public) + .with_auth_component(AuthSingleSig::new( + alice_key_pair.public_key().to_commitment(), + AuthScheme::Falcon512Poseidon2, + )) + .with_component(BasicWallet); + + // Build the faucet + let faucet_builder = AccountBuilder::new(init_seed) + .account_type(AccountType::FungibleFaucet) + .storage_mode(AccountStorageMode::Public) + .with_auth_component(AuthSingleSig::new( + faucet_key_pair.public_key().to_commitment(), + AuthScheme::Falcon512Poseidon2, + )) + .with_component(BasicFungibleFaucet::new(symbol, decimals, max_supply)?); + + let alice_account = account_builder.build()?; + let faucet_account = faucet_builder.build()?; + + 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?; + + let amount: u64 = 1000; + 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 + 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. + 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() { + println!("Waiting for P2ID note to be comitted..."); + 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(); + println!( + "Alice's TEST token balance: {:?}", + vault.get_balance(faucet_account.id()) + ); + + break; // Exit the loop after consuming the note + } + + //------------------------------------------------------------ + // SENDING TOKENS TO BOB + //------------------------------------------------------------ + + let bob_account_id = AccountId::from_hex("0x103f8a1ad4b983104aec0412ab0b0d")?; + let send_amount = 100; + let fungible_asset_to_send = FungibleAsset::new(faucet_account.id(), send_amount)?; + + let p2id_note = P2idNote::create( + alice_account.id(), + bob_account_id, + vec![fungible_asset_to_send.into()], + NoteType::Public, + NoteAttachment::default(), + client.rng(), + )?; + + // 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 100 tokens to Bob note transaction ID: {:?}", + send_p2id_note_tx_id.to_hex() + ); + + Ok(()) +} +``` + +```typescript title="src/demo.ts" +import { MidenClient, AccountType } from "@miden-sdk/miden-sdk"; + +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({ + type: AccountType.MutableWallet, + 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: AccountType.FungibleFaucet, + symbol: "TEST", + decimals, + maxSupply, + storage: "public", + }); + console.log("Faucet account ID:", faucet.id().toString()); + + // 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, + to: alice, + amount: 1000n, + type: "public", + waitForConfirmation: true, + }); + console.log( + "Mint transaction submitted successfully, ID:", + mintResult.txId.toString(), + ); + + const notes = await client.notes.listAvailable({ account: alice }); + const consumeResult = await client.transactions.consume({ + account: alice, + notes: [notes[0]], + waitForConfirmation: true, + }); + console.log( + "Consume transaction submitted successfully, ID:", + consumeResult.txId.toString(), + ); + + const balance = await client.accounts.getBalance(alice, faucet); + console.log("Alice's TEST token balance:", Number(balance)); + + // Send 100 tokens from Alice to Bob. + const bobAccountId = "0x103f8a1ad4b983104aec0412ab0b0d"; + console.log("Sending 100 tokens to Bob..."); + const { txId } = await client.transactions.send({ + account: alice, + to: bobAccountId, + token: faucet, + amount: 100n, + type: "public", + waitForConfirmation: true, + }); + console.log("Send transaction submitted successfully, ID:", txId.toString()); +} +``` + +
+Expected output + +```text +Alice's account ID: 0xd6b8bb0ed10b1610282c513501778a +Faucet account ID: 0xe48c43d6ad6496201bcfa585a5a4b6 +Minting 1000 tokens to Alice... +Mint transaction submitted successfully, ID: 0x948a0eef754068b3126dd3261b6b54214fa5608fb13c5e5953faf59bad79c75f +Consume transaction submitted successfully, ID: 0xc69ab84b784120abe858bb536aebda90bd2067695f11d5da93ab0b704f39ad78 +Alice's TEST token balance: 100 +Send 100 tokens to Bob note transaction 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.14/builder/get-started/read-storage.md b/versioned_docs/version-0.14/builder/get-started/read-storage.md new file mode 100644 index 00000000..a0ee4e1e --- /dev/null +++ b/versioned_docs/version-0.14/builder/get-started/read-storage.md @@ -0,0 +1,231 @@ +--- +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 interact with a counter contract deployed on the Miden testnet. This contract maintains a simple counter value in a named storage map slot. + +### Reading the Count of a Counter contract + +```rust title="integration/src/bin/read-count.rs" +use miden_client::{ + account::{Account, AccountId, StorageSlotName}, + builder::ClientBuilder, + keystore::FilesystemKeyStore, + rpc::{Endpoint, GrpcClient}, + 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; + let rpc_client = Arc::new(GrpcClient::new(&endpoint, timeout_ms)); + + // 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() + .rpc(rpc_client) + .sqlite_store(store_path) + .authenticator(keystore.clone()) + .in_debug_mode(true.into()) + .build() + .await?; + + client.sync_state().await?; + + //------------------------------------------------------------ + // READ PUBLIC STATE OF THE COUNTER ACCOUNT + //------------------------------------------------------------ + + let counter_account_id = AccountId::from_hex("0x224a96d294e10d006aef3d4f1b0876")?; + + 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 from the counter account's named storage map slot + let slot_name = StorageSlotName::new( + "miden::component::miden_counter_account::count_map" + )?; + let count_key = Word::from([Felt::new(0), Felt::new(0), Felt::new(0), Felt::new(1)]); + let count = counter_account + .storage() + .get_map_item(&slot_name, count_key)?; + + println!("Count: {:?}", count); + + 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(); + + const counterAccountId = "0x224a96d294e10d006aef3d4f1b0876"; + + // 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 = "miden::component::miden_counter_account::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 4th value is the counter number. + console.log("Count:", Number(count?.toU64s()[3])); +} +``` + +
+Expected output + +```text +Count: 1 +``` + +
+ +## Reading Account Token Balances + +You can also query the assets (tokens) held by an account: + +```rust title="integration/src/bin/token-balance.rs" +use miden_client::{ + account::{Account, AccountId}, + builder::ClientBuilder, + keystore::FilesystemKeyStore, + rpc::{Endpoint, GrpcClient}, +}; +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; + let rpc_client = Arc::new(GrpcClient::new(&endpoint, timeout_ms)); + + // 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() + .rpc(rpc_client) + .sqlite_store(store_path) + .authenticator(keystore.clone()) + .in_debug_mode(true.into()) + .build() + .await?; + + client.sync_state().await?; + + //------------------------------------------------------------ + // READ TOKEN BALANCE OF AN ACCOUNT + //------------------------------------------------------------ + + let alice_account_id = AccountId::from_hex("0x5b2840a923dedc102ea67e0c1eba3c")?; + let faucet_account_id = AccountId::from_hex("0x29dd1dc628d2842032e751ed1b5da7")?; + + client.import_account_by_id(alice_account_id).await?; + + let alice_account: Account = client + .get_account(alice_account_id) + .await? + .ok_or_else(|| anyhow::anyhow!("Account not found"))? + .try_into()?; + + let balance = alice_account + .vault() + .get_balance(faucet_account_id)?; + + println!("Alice's TEST token balance: {:?}", balance); + + 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(); + + const aliceId = "0x5b2840a923dedc102ea67e0c1eba3c"; + const faucetId = "0x29dd1dc628d2842032e751ed1b5da7"; + + // Fetch Alice's account (imports it into the local store if needed) + // and query her balance for the faucet's token. + await client.accounts.getOrImport(aliceId); + const balance = await client.accounts.getBalance(aliceId, faucetId); + + console.log("Alice's TEST token balance:", Number(balance)); +} +``` + +
+Expected output + +```text +Alice's TEST token balance: 900 +``` + +
+ +--- diff --git a/versioned_docs/version-0.14/builder/get-started/setup/_category_.json b/versioned_docs/version-0.14/builder/get-started/setup/_category_.json new file mode 100644 index 00000000..103ee735 --- /dev/null +++ b/versioned_docs/version-0.14/builder/get-started/setup/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Set Up", + "position": 1 +} diff --git a/versioned_docs/version-0.14/builder/get-started/setup/cli-basics.md b/versioned_docs/version-0.14/builder/get-started/setup/cli-basics.md new file mode 100644 index 00000000..e3736152 --- /dev/null +++ b/versioned_docs/version-0.14/builder/get-started/setup/cli-basics.md @@ -0,0 +1,189 @@ +--- +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 new-wallet +``` + +
+Expected output + +```text +Successfully created new wallet. +To view account details execute miden-client account --show 0x05bd1f642cd368800cc95956b2696a +Config updated successfully +Setting account 0x05bd1f642cd368800cc95956b2696a as the default account ID. +You can unset it with `miden-client account --default none`. +``` + +
+ +This command creates a **BasicMutable** account with **private** storage mode, giving you full control while keeping your data confidential. + +### View Your Account + +List all your accounts: + +```bash title=">_ Terminal" +miden client account +``` + +
+Expected output + +```text +| Account ID | Type | Storage Mode | 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 | +| Type | Regular | +| Storage mode | private | +| Code Commitment | 0x6a11161925930dae89cc24cbddf0d161cead39b0fe88c262d4e790cff35be01d | +| Vault Root | 0x3e128c57f6cfa0d44ab1308994171af13cb513422add28d1916b3ff254fef82d | +| Storage Root | 0x5f95d38174f10c8ce91a0202763b0813fdcbb2714704cda411af6483ebc8d012 | +| Nonce | 0 | + +Assets: + +| Asset Type | Faucet | Amount | +|------------|---------|---------| +| | | | + +Storage: + +| Item Slot Index | Item Slot Type | Value/Commitment | +|-----------------|----------------|------------------| +| 0 | Value | 0xa52ef6357625c54a2eaefd11b8cfc2ee3429c37d9f8a827e23886857ea284834 | +``` + +
+ +**Key Account Components:** + +- **Account ID**: Unique 120-bit identifier encoding the account type and storage mode +- **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 +``` + +### Deploy Your Account + +The `miden client new-wallet` command above already deploys your account on-chain automatically. You can verify your account is deployed by syncing and checking its status: + +```bash title=">_ Terminal" +miden client sync +miden client account +``` + +## Mint Your First Tokens + +Request tokens from the public testnet faucet: + +```bash title=">_ Terminal" +miden mint --target-account --amount 1000 +``` + +This sends a mint request to the [public testnet faucet](https://faucet-api.testnet.miden.io) and automatically consumes the resulting note, depositing the tokens into your account. + +Display your balance: + +```bash title=">_ Terminal" +miden client sync +miden client account -s +``` + +## 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 --network devnet +``` + +Available networks: + +- `testnet` - Miden's public test network +- `devnet` - Development network +- `localhost` - Local node for testing + +### Important Files Created + +When you manually initialize the Miden client in your working directory, several local files are created: + +- **`miden-client.toml`**: Configuration file with network settings +- **`store.sqlite3`**: Database storing your account data and transaction history +- **`keystore/`**: Directory containing your private keys (keep secure!) +- **`templates/`**: Pre-built smart contract components + +:::danger +Private keys in the `keystore/` directory are **not encrypted**. Keep these files secure and never share them. +::: + +To return to your global client configuration, remove the local `miden-client.toml` (and any local store/keystore files you no longer need). + +--- diff --git a/versioned_docs/version-0.14/builder/get-started/setup/installation.md b/versioned_docs/version-0.14/builder/get-started/setup/installation.md new file mode 100644 index 00000000..704aec35 --- /dev/null +++ b/versioned_docs/version-0.14/builder/get-started/setup/installation.md @@ -0,0 +1,206 @@ +--- +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.92.0-nightly (fa3155a64 2025-09-30) +``` + +
+ +### Install Node.js & Yarn + +For TypeScript development with the Miden Web Client, you'll need Node.js and Yarn. + +**Install Node.js:** + +```bash title=">_ Terminal" +# Install Node.js using the official installer or package manager +# For macOS with Homebrew: +brew install node + +# For Ubuntu/Debian: +curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash - +sudo apt-get install -y nodejs + +# For Windows, download 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 +``` + +:::info +Until published to crates.io, install using: `cargo install --git https://github.com/0xMiden/midenup.git` +::: + +**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** + +Install the latest stable Miden components: + +```bash title=">_ Terminal" +midenup install stable +``` + +:::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 +``` + +
+Expected output + +```text +stable +``` + +
+ +### 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 you have config files from a previous Miden installation, they may be incompatible with the current version. Delete the old config and database, then re-initialize: + +```bash title=">_ Terminal" +rm -f miden-client.toml store.sqlite3 +miden client init +``` + +## 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. 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 +``` + +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.14/builder/get-started/your-first-smart-contract/_category_.json b/versioned_docs/version-0.14/builder/get-started/your-first-smart-contract/_category_.json new file mode 100644 index 00000000..9041f571 --- /dev/null +++ b/versioned_docs/version-0.14/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.14/builder/get-started/your-first-smart-contract/create.md b/versioned_docs/version-0.14/builder/get-started/your-first-smart-contract/create.md new file mode 100644 index 00000000..c930bde7 --- /dev/null +++ b/versioned_docs/version-0.14/builder/get-started/your-first-smart-contract/create.md @@ -0,0 +1,265 @@ +--- +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 +│ └── increment-note/ # Example: Increment note contract +├── integration/ # Integration crate (scripts + tests) +│ ├── src/ +│ │ ├── bin/ # Rust binaries for on-chain 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 on-chain interactions, deployment scripts, and tests + +Each contract is organized as its own individual crate, providing independent versioning, dependencies, and clear isolation between different contracts. + +## 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, felt, Felt, StorageMap, StorageMapAccess, Word}; + +/// Main contract structure for the counter example. +#[component] +struct CounterContract { + /// Storage map holding the counter value. + #[storage(description = "counter contract storage map")] + count_map: StorageMap, +} + +#[component] +impl CounterContract { + /// Returns the current counter value stored in the contract's storage map. + pub fn get_count(&self) -> Felt { + // Define a fixed key for the counter value within the map + let key = Word::from_u64_unchecked(0, 0, 0, 1); + // Read the value associated with the key from the storage map + self.count_map.get(&key) + } + + /// Increments the counter value stored in the contract's storage map by one. + pub fn increment_count(&mut self) -> Felt { + // Define the same fixed key + let key = Word::from_u64_unchecked(0, 0, 0, 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, felt, Felt, StorageMap, StorageMapAccess, Word}; +``` + +These imports provide: + +- **`component`**: Macro for defining contract components +- **`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 +- **`StorageMapAccess`**: Needed for reading storage values (`get_count` function) + +:::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)` is a compile-time macro that creates `Felt` values from integer literals with compile-time range validation. Currently `felt!` only accepts values up to 2^32 (compiler limitation); for larger values use `Felt::from_u64_unchecked()`. +::: + +#### Contract Structure Definition + +```rust +#[component] +struct CounterContract { + /// Storage map holding the counter value. + #[storage(description = "counter contract storage map")] + count_map: StorageMap, +} +``` + +The `#[component]` attribute marks this as a Miden [Account component](/core-concepts/protocol/account). The `count_map` field is a `StorageMap` stored in a named storage slot of the account. In v0.13, storage slots are identified by name rather than explicit index numbers — the slot name is derived automatically from the component's package name and field name (e.g., `miden::component::miden_counter_account::count_map`). + +**Important**: Storage slots in Miden hold `Word` values, which are composed of four field elements (`Felt`). Each `Felt` is a 64-bit unsigned integer (u64). The `StorageMap` provides a key-value interface within a single storage slot, allowing you to store multiple key-value pairs within the four-element word structure. + +#### Contract Implementation + +```rust +impl CounterContract { + // Function implementations... +} +``` + +The `CounterContract` implementation defines the external interface that other contracts and notes can call. This is the contract's public API. + +#### Storage Key Strategy + +```rust +let key = Word::from_u64_unchecked(0, 0, 0, 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. The `Word::from_u64_unchecked` constructor creates a `Word` from four `u64` values. 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::*; + +use crate::bindings::miden::counter_account::counter_account; + +#[note] +struct IncrementNote; + +#[note] +impl IncrementNote { + #[note_script] + fn run(self, _arg: Word) { + let initial_value = counter_account::get_count(); + counter_account::increment_count(); + let expected_value = initial_value + Felt::from_u32(1); + let final_value = counter_account::get_count(); + assert_eq(final_value, expected_value); + } +} +``` + +### Increment Note Script Walkthrough + +#### No-std Setup + +Similar to the account contract, the note script uses `#![no_std]` with the same allocator and panic handler setup. + +#### Miden Imports + +```rust +use miden::*; + +use crate::bindings::miden::counter_account::counter_account; +``` + +The wildcard import brings in all Miden note script functionality. The `counter_account` binding imports the interface functions from the counter contract, allowing the note script to call them. + +#### 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) { ... } +} +``` + +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. + +Learn more about [note scripts in the Miden documentation](/core-concepts/protocol/note/). + +#### The Note Script Function + +```rust +#[note_script] +fn run(self, _arg: Word) { + let initial_value = counter_account::get_count(); + counter_account::increment_count(); + let expected_value = initial_value + Felt::from_u32(1); + let final_value = counter_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 imported `counter_account::get_count()` function +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.14/builder/get-started/your-first-smart-contract/deploy.md b/versioned_docs/version-0.14/builder/get-started/your-first-smart-contract/deploy.md new file mode 100644 index 00000000..f4f9da9a --- /dev/null +++ b/versioned_docs/version-0.14/builder/get-started/your-first-smart-contract/deploy.md @@ -0,0 +1,253 @@ +--- +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. + +Navigate to your project's integration folder: + +```bash title=">_ Terminal" +cd integration +ls -la +``` + +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://getfoundry.sh/guides/scripting-with-solidity). 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](/core-concepts/protocol/transaction#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 + +Execute the increment script to deploy your contract: + +```bash title=">_ Terminal" +cd integration +cargo run --bin increment_count --release +``` + +
+Expected Output + +```text +Account ID: V0(AccountIdV0 { prefix: 14134910893364381952, suffix: 3644349760121494784 }) +Sender account ID: "0xd85b347218c5a80052dbd47b2f36ad" +Counter note hash: "0xf0e821396a896eb9983e682bc056021d57ddcaa43082f34597bf9e026421e566" +Note publish transaction ID: "0xc6f080855724402cadf26650ffe993fe97a127a8f6c9c82ec621960e936e6d732 +Consume transaction ID: "0x2d1d8510e546ce0fbc22fa7d1a82322259d73cd1d7e0ca86622d0be70fab0548" +Account delta: AccountDelta { account_id: V0(AccountIdV0 { prefix: 7255964780328958976, suffix: 2724050564200846336 }), storage: AccountStorageDelta { values: {}, maps: {0: StorageMapDelta({LexicographicWord(Word([0, 0, 0, 1])): Word([0, 0, 0, 1])})} }, vault: AccountVaultDelta { fungible: FungibleAssetDelta({}), non_fungible: NonFungibleAssetDelta({}) }, nonce_delta: 1 } +``` + +
+ +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**: In Miden, accounts (contracts) become visible onchain only when they undergo a state change. Simply creating an account locally doesn't deploy it - the account must participate in a transaction that modifies its state. In our case, by incrementing the counter, we're effectively "deploying" the contract and making it visible on the Miden testnet explorer. This is why the increment operation serves both as the deployment and the first interaction with the contract. + +## How the Scripts Work + +The integration scripts work by connecting to the Miden client and then building contracts from the Miden package files. These package files are generated when you run `miden build` inside each contract directory, but the scripts handle this compilation step automatically - you don't need to manually build the contracts before running the scripts. + +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 your contract's Rust source code +- Compiles the Rust code into a Miden package (`.masp` file) +- Generates a package containing the compiled contract bytecode and metadata +- This is equivalent to manually running `miden build` in each contract directory + +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 count_storage_key = Word::from([Felt::new(0), Felt::new(0), Felt::new(0), Felt::new(1)]); +let initial_count = Word::from([Felt::new(0), Felt::new(0), Felt::new(0), Felt::new(0)]); + +// The slot name is constructed as: +// `miden::component::[to_underscore(Cargo.toml:package.metadata.component.package)]::[field_name]` +let counter_storage_slot = + StorageSlotName::new("miden::component::miden_counter_account::count_map").unwrap(); +let storage_slots = vec![StorageSlot::with_map( + counter_storage_slot.clone(), + StorageMap::with_entries([(count_storage_key, initial_count)]).unwrap(), +)]; +let counter_cfg = AccountCreationConfig { + storage_slots, + ..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 their storage slots specified when instantiating the account. In v0.13, storage slots are identified by name rather than index. The slot name follows the pattern `miden::component::::`. We define the storage configuration with: + +- A named `StorageMap` slot (`miden::component::miden_counter_account::count_map`) +- The counter key `[0, 0, 0, 1]` with initial value `[0, 0, 0, 0]` (representing count = 0) + +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 +// Convert the increment note package into an executable note +let counter_note = create_note_from_package( + &mut client, + note_package.clone(), + sender_account.id(), + NoteCreationConfig::default() +) +.context("Failed to create counter note from package")?; + +// Publish the note to the network +let note_publish_request = TransactionRequestBuilder::new() + .own_output_notes(vec![OutputNote::Full(counter_note.clone())]) + .build() + .context("Failed to build note publish transaction request")?; +``` + +The `create_note_from_package()` function: + +- Takes the compiled note script package +- Combines it with the sender account ID and configuration +- Creates 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.14/builder/get-started/your-first-smart-contract/index.md b/versioned_docs/version-0.14/builder/get-started/your-first-smart-contract/index.md new file mode 100644 index 00000000..826cec03 --- /dev/null +++ b/versioned_docs/version-0.14/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.14/builder/get-started/your-first-smart-contract/test.md b/versioned_docs/version-0.14/builder/get-started/your-first-smart-contract/test.md new file mode 100644 index 00000000..961c0637 --- /dev/null +++ b/versioned_docs/version-0.14/builder/get-started/your-first-smart-contract/test.md @@ -0,0 +1,310 @@ +--- +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 on-chain 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 passed! +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 integration::helpers::{ + build_project_in_dir, create_testing_account_from_package, create_testing_note_from_package, + AccountCreationConfig, NoteCreationConfig, +}; + +use miden_client::{ + account::{StorageMap, StorageMapKey, StorageSlot, StorageSlotName}, + auth::AuthSchemeId, + transaction::RawOutputNote, + Word, +}; +use miden_testing::{Auth, MockChain}; +use std::{path::Path, sync::Arc}; + +#[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 initial storage and no-auth auth component + let count_storage_key = Word::from([0u32, 0, 0, 1]); + let initial_count = Word::default(); + + // The slot name is constructed as + // `miden::component::[to_underscore(Cargo.toml:package.metadata.component.package)]::[field_name]` + let counter_storage_slot = + StorageSlotName::new("miden::component::miden_counter_account::count_map").unwrap(); + let storage_slots = vec![StorageSlot::with_map( + counter_storage_slot.clone(), + StorageMap::with_entries([(StorageMapKey::new(count_storage_key), initial_count)]).unwrap(), + )]; + let counter_cfg = AccountCreationConfig { + storage_slots, + ..Default::default() + }; + + // create testing counter account + let mut counter_account = + create_testing_account_from_package(contract_package.clone(), counter_cfg).await?; + + // create testing increment note + let counter_note = create_testing_note_from_package( + note_package.clone(), + sender.id(), + NoteCreationConfig::default(), + )?; + + // add counter account and note to mockchain + builder.add_account(counter_account.clone())?; + builder.add_output_note(RawOutputNote::Full(counter_note.clone())); + + // Build the mock chain + let mut mock_chain = builder.build()?; + // Build the transaction context + let tx_context = mock_chain + .build_tx_context(counter_account.id(), &[counter_note.id()], &[])? + .build()?; + + // Execute the transaction + let executed_transaction = tx_context.execute().await?; + + // Apply the account delta to the counter account + counter_account.apply_delta(executed_transaction.account_delta())?; + + // 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 = counter_account + .storage() + .get_map_item(&counter_storage_slot, count_storage_key) + .expect("Failed to get counter value from storage slot"); + + // Assert that the count value is equal to 1 after executing the transaction + assert_eq!( + count, + Word::from([0u32, 0, 0, 1]), + "Count value is not equal to 1" + ); + + println!("Test passed!"); + 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 initial storage and no-auth auth component +let count_storage_key = Word::from([0u32, 0, 0, 1]); +let initial_count = Word::default(); + +let counter_storage_slot = + StorageSlotName::new("miden::component::miden_counter_account::count_map").unwrap(); +let storage_slots = vec![StorageSlot::with_map( + counter_storage_slot.clone(), + StorageMap::with_entries([(StorageMapKey::new(count_storage_key), initial_count)]).unwrap(), +)]; +let counter_cfg = AccountCreationConfig { + storage_slots, + ..Default::default() +}; + +// Create testing entities +let mut counter_account = create_testing_account_from_package(contract_package.clone(), counter_cfg).await?; +let counter_note = create_testing_note_from_package( + note_package.clone(), + sender.id(), + NoteCreationConfig::default(), +)?; +``` + +**What's happening:** + +- We configure the **counter account's initial storage** with count = 0 at storage key `[0, 0, 0, 1]` +- We create the **testing counter account** from the compiled package using `create_testing_account_from_package()` +- We create the **testing increment note** using `create_testing_note_from_package()` +- These helper functions create test-specific versions optimized for the Mockchain environment + +### 4. Adding Components to the Mockchain + +```rust +builder.add_account(counter_account.clone())?; +builder.add_output_note(RawOutputNote::Full(counter_note.clone())); +let mut mock_chain = builder.build()?; +``` + +**What's happening:** + +- We **add the counter account** to the mockchain builder +- 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 + +### 5. Creating and Executing the Transaction + +```rust +let tx_context = mock_chain + .build_tx_context(counter_account.id(), &[counter_note.id()], &[])? + .build()?; + +let executed_transaction = tx_context.execute().await?; +``` + +**What's happening:** + +- We **build the transaction context** using the counter account and counter note +- We **execute the transaction** - this runs the increment logic locally in the mockchain + +### 6. Verifying the Results + +```rust +// Apply the account delta to the counter account +counter_account.apply_delta(executed_transaction.account_delta())?; + +// 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 = counter_account + .storage() + .get_map_item(&counter_storage_slot, count_storage_key) + .expect("Failed to get counter value from storage slot"); + +// Assert that the count value is equal to 1 after executing the transaction +assert_eq!( + count, + Word::from([0u32, 0, 0, 1]), + "Count value is not equal to 1" +); +``` + +**What's happening:** + +- We **apply the account delta** from the executed transaction to the counter account to update its state +- We **add the executed transaction** to the mockchain +- We **read the counter value** from storage using the same key we initialized +- 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 of core concepts, consult the [Core Concepts section](../../../core-concepts/) 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.14/builder/glossary.md b/versioned_docs/version-0.14/builder/glossary.md new file mode 100644 index 00000000..36fdbaf1 --- /dev/null +++ b/versioned_docs/version-0.14/builder/glossary.md @@ -0,0 +1,140 @@ +--- +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 off-chain 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 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.14/builder/index.md b/versioned_docs/version-0.14/builder/index.md new file mode 100644 index 00000000..a8abbed2 --- /dev/null +++ b/versioned_docs/version-0.14/builder/index.md @@ -0,0 +1,83 @@ +--- +sidebar_label: Introduction +sidebar_position: 0 +pagination_next: null +--- + +# Build on Miden + +Accounts, notes, and transactions — authored in Rust, compiled to MASM, proved client-side. + +## 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. + + + +## 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 built on Miden. + + + +## 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.14/builder/miden-guardian/_category_.json b/versioned_docs/version-0.14/builder/miden-guardian/_category_.json new file mode 100644 index 00000000..12835209 --- /dev/null +++ b/versioned_docs/version-0.14/builder/miden-guardian/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Guardian", + "position": 4 +} diff --git a/versioned_docs/version-0.14/builder/miden-guardian/core-concepts/_category_.json b/versioned_docs/version-0.14/builder/miden-guardian/core-concepts/_category_.json new file mode 100644 index 00000000..722a5299 --- /dev/null +++ b/versioned_docs/version-0.14/builder/miden-guardian/core-concepts/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Core Concepts", + "position": 1 +} diff --git a/versioned_docs/version-0.14/builder/miden-guardian/core-concepts/architecture.md b/versioned_docs/version-0.14/builder/miden-guardian/core-concepts/architecture.md new file mode 100644 index 00000000..ad632e1f --- /dev/null +++ b/versioned_docs/version-0.14/builder/miden-guardian/core-concepts/architecture.md @@ -0,0 +1,110 @@ +--- +title: Architecture +sidebar_position: 1 +--- + +# Architecture + +Guardian sits between Miden clients and the Miden network, providing an off-chain coordination layer for private account state. + +## System overview + +```mermaid +graph LR + A["Miden Client A
(Desktop)"] <-->|"gRPC / HTTP"| Guardian["Guardian Server"] + B["Miden Client B
(Mobile)"] <-->|"gRPC / HTTP"| Guardian + Guardian <-->|"Validates state"| Node["Miden Node"] +``` + +- **Miden Client** handles transaction execution, proving, and local state management. +- **Guardian Server** stores state snapshots and deltas, authenticates requests, validates changes against the network, and coordinates multi-party workflows. +- **Miden Node** is the network's RPC endpoint that Guardian validates state against. + +Each account is independently configured on Guardian with its own authentication policy and storage. Clients interact with Guardian through either gRPC or HTTP — both interfaces expose the same semantics. + +## End-to-end transaction flow + +Transactions proceed through a step-by-step process to ensure consistency and verifiability: + +```mermaid +sequenceDiagram + participant Client as Miden Client + participant Guardian as Guardian Server + participant Chain as Miden Network + + Client->>Client: 1. Execute transaction locally
Generate delta + Client->>Guardian: 2. Submit delta for acknowledgment + Guardian->>Guardian: 3. Validate delta against policies + Guardian->>Guardian: Co-sign as "candidate" + Guardian-->>Client: Return ack signature + Client->>Chain: 4. Submit ZK proof + state update + Chain-->>Guardian: 5. Guardian monitors on-chain commitment + alt Commitment matches candidate + Guardian->>Guardian: Promote to "canonical" + Guardian-->>Client: Propagate confirmed delta + else Commitment mismatch + Guardian->>Guardian: Mark as "discarded" + Guardian-->>Client: Signal resync needed + end +``` + +1. **Local execution**: The user computes a transaction locally, generating a delta (state change). +2. **Delta submission**: The user sends the delta to Guardian for acknowledgment. +3. **Guardian acknowledgment**: Guardian validates the delta and co-signs it, designating it as a "candidate" state. +4. **Proof submission**: The user generates the ZK proof and submits it to the chain. +5. **Canonical confirmation**: Guardian monitors the chain. If the on-chain commitment matches the candidate, the state becomes "canonical" and is propagated to other devices or signers. + +## Multi-device sync + +For users with multiple devices, Guardian keeps state synchronized seamlessly: + +```mermaid +sequenceDiagram + participant Desktop as Desktop + participant Guardian as Guardian Server + participant Mobile as Mobile + + Desktop->>Desktop: Execute transaction + Desktop->>Guardian: Push delta + Guardian->>Guardian: Validate & acknowledge + Desktop->>Desktop: Submit proof to chain + Guardian->>Guardian: Confirm canonical + Mobile->>Guardian: Get state + Guardian-->>Mobile: Return latest state + Mobile->>Mobile: Replay delta locally + Note over Mobile: State now matches Desktop +``` + +The desktop executes a transaction and pushes the delta to Guardian. After on-chain confirmation, Guardian propagates the canonical delta to the mobile device, which replays it locally — all without querying the chain directly. + +## Account management + +Accounts are configured with per-account authentication based on public keys (commitments). During setup, Guardian records which keys are authorized to manage the account. + +For each request, the client signs a payload with one of those keys and the server verifies the signature against the account's authorized keys. See [Components](./components.md) for details on the auth model. + +## Canonicalization + +Canonicalization is the process of validating that a state transition (delta) is valid against the on-chain commitment. It is optional and mainly used in multi-user setups. + +```mermaid +stateDiagram-v2 + [*] --> candidate : push_delta + candidate --> canonical : On-chain commitment matches + candidate --> discarded : On-chain commitment mismatch + canonical --> [*] + discarded --> [*] +``` + +- **Candidate mode** (default): A background worker promotes or discards deltas after a configurable delay and network verification. +- **Optimistic mode**: Deltas become canonical immediately, skipping the verification window. + +| Parameter | Default | Description | +|---|---|---| +| `delay_seconds` | 900 (15 min) | How long a candidate waits before the worker checks it. | +| `check_interval_seconds` | 60 (1 min) | How often the worker runs. | + +## Common use cases + +- **Single-user accounts**: Back up and sync state securely. If a device is lost, recover state from Guardian. +- **Multi-user accounts**: Coordinate state and transactions between participants. Guardian helps keep everyone on the latest canonical state. diff --git a/versioned_docs/version-0.14/builder/miden-guardian/core-concepts/components.md b/versioned_docs/version-0.14/builder/miden-guardian/core-concepts/components.md new file mode 100644 index 00000000..c14f9bf2 --- /dev/null +++ b/versioned_docs/version-0.14/builder/miden-guardian/core-concepts/components.md @@ -0,0 +1,120 @@ +--- +title: Components +sidebar_position: 3 +--- + +# Components + +The Guardian server is composed of several pluggable components that handle different responsibilities. + +## API + +The API exposes a consistent interface for operating on states and deltas over HTTP and gRPC. Behavior is identical across transports, so clients can switch between them without semantic changes. + +**HTTP endpoints** (default port 3000): + +| Method | Path | Description | +|---|---|---| +| `POST` | `/configure` | Create account with auth policy and initial state | +| `POST` | `/delta` | Push a delta (server validates, signs, sets status) | +| `GET` | `/delta?account_id&nonce` | Fetch delta by nonce | +| `GET` | `/delta/since?account_id&from_nonce` | Merged canonical snapshot since a nonce | +| `GET` | `/state?account_id` | Latest account state | +| `POST` | `/delta/proposal` | Create pending proposal for multi-party signing | +| `GET` | `/delta/proposal?account_id` | List pending proposals | +| `PUT` | `/delta/proposal` | Append cosigner signature to a proposal | +| `GET` | `/pubkey` | Server acknowledgment public key (unauthenticated) | + +**gRPC** (default port 50051) mirrors all HTTP endpoints with the same semantics. Credentials are provided via metadata headers. + +For the full API specification, see the [spec/api.md](https://github.com/OpenZeppelin/guardian/blob/main/spec/api.md) in the repository. + +## Auth + +Request authentication is configured per account. All endpoints except `/pubkey` require authentication. + +### Falcon RPO + +The current authentication policy uses Miden Falcon RPO signatures with an allowlist of **cosigner commitments** — hashes of authorized public keys. + +Every authenticated request includes three headers: + +| Header | Description | +|---|---| +| `x-pubkey` | Signer's public key (full serialized key or 32-byte commitment hex) | +| `x-signature` | Falcon RPO signature over the request digest | +| `x-timestamp` | Unix timestamp in milliseconds | + +The signature is computed over: + +``` +RPO256_hash([account_id_prefix, account_id_suffix, timestamp_ms, 0]) +``` + +### Verification flow + +```mermaid +sequenceDiagram + participant Client + participant Guardian as Guardian Server + + Client->>Guardian: Request + x-pubkey, x-signature, x-timestamp + + Guardian->>Guardian: 1. Derive commitment from public key + Guardian->>Guardian: 2. Check commitment is in account allowlist + Guardian->>Guardian: 3. Verify timestamp within 300s window + Guardian->>Guardian: 4. Check timestamp > last_auth_timestamp + Guardian->>Guardian: 5. Verify Falcon signature over
(account_id, timestamp) digest + + alt All checks pass + Guardian->>Guardian: Update last_auth_timestamp (CAS) + Guardian-->>Client: Process request + else Any check fails + Guardian-->>Client: 400 AuthenticationFailed + end +``` + +### Replay protection + +Guardian prevents replay attacks through two mechanisms: + +1. **Timestamp window**: The signed timestamp must be within **300 seconds** (5 minutes) of the server's current time. +2. **Monotonic timestamps**: Each request's timestamp must be strictly greater than the account's `last_auth_timestamp`, enforced atomically via compare-and-swap. + +## Acknowledger + +The Acknowledger produces tamper-evident acknowledgments for accepted deltas: + +- Signs the digest of `new_commitment` and returns the signature as `ack_sig`. +- The server's acknowledgment key is exposed via the `/pubkey` endpoint for clients to cache and verify against. + +Clients should verify `ack_sig` after every `push_delta` to confirm the server processed the change correctly. + +## Network + +The Network component handles interactions with the Miden blockchain: + +- Computes commitments and validates deltas against the target network's rules. +- Validates account identifiers and request credentials against network-owned state. +- Merges multiple deltas into a single snapshot payload (for `get_delta_since`). +- Surfaces suggested auth updates (e.g., rotated cosigner commitments) so metadata remains aligned with the network. + +## Storage + +Storage persists account snapshots, deltas, and delta proposals: + +- Provides retrieval by account and nonce, plus range queries for canonicalization. +- Stores pending delta proposals in a per-account namespace keyed by proposal commitment. +- Backends are pluggable without altering API semantics. + +Available backends: +- **Filesystem** (default): Stores data on disk. Suitable for **testing and development**. No external dependencies. +- **PostgreSQL** (optional): Recommended for **production** deployments. Requires the `postgres` feature flag at build time. Migrations run automatically on startup. + +## Metadata + +The Metadata store holds per-account configuration: + +- `account_id`, authentication policy, storage backend type, timestamps, and `last_auth_timestamp` for replay protection. +- Supports CRUD operations and list iteration over accounts. +- Can use filesystem or PostgreSQL as its backing store, independent of the Storage backend choice. diff --git a/versioned_docs/version-0.14/builder/miden-guardian/core-concepts/data-structures.md b/versioned_docs/version-0.14/builder/miden-guardian/core-concepts/data-structures.md new file mode 100644 index 00000000..0ba0e185 --- /dev/null +++ b/versioned_docs/version-0.14/builder/miden-guardian/core-concepts/data-structures.md @@ -0,0 +1,120 @@ +--- +title: Data Structures +sidebar_position: 2 +--- + +# Data Structures + +Guardian models account state as an append-only chain of snapshots and changes. + +## State + +A **state** is a canonical snapshot of an account at a point in time. It includes the account's ID, commitment, nonce, vault (assets), storage, and other account data. + +```json +{ + "account_id": "0xabc123...", + "commitment": "0xdef456...", + "nonce": 10, + "assets": [ + { "balance": 12000, "asset_id": "USDC" }, + { "balance": 2, "asset_id": "ETH" } + ] +} +``` + +When you first register an account with Guardian, you provide an **initial state** — the baseline from which all subsequent changes are tracked. + +## Delta + +A **delta** represents a set of changes applied to a state. Deltas are append-only — each delta references the commitment of the state it was applied to, forming an unbroken chain. + +```json +{ + "account_id": "0xabc123...", + "nonce": 11, + "prev_commitment": "0xdef456...", + "delta_payload": { + "data": "" + } +} +``` + +A useful mental model: a delta is a compact, replayable description of "what changed" in an account's local state. Deltas can sync, back up, and reconstruct state without shipping full snapshots. + +Key properties: + +- **Ordered**: Each delta has a nonce that determines its position in the chain. +- **Linked**: The `prev_commitment` field references the state the delta was applied to. This prevents forks — if two deltas reference different base states, the server rejects the conflicting one. +- **Validated**: The server verifies each delta against the Miden network before accepting it. +- **Acknowledged**: Once accepted, the server signs the delta's `new_commitment`, providing cryptographic proof that it was processed. + +### Delta status lifecycle + +Each delta goes through a state machine: + +```mermaid +stateDiagram-v2 + [*] --> candidate : push_delta + candidate --> canonical : On-chain commitment matches + candidate --> discarded : On-chain commitment mismatch + canonical --> [*] + discarded --> [*] +``` + +| Status | Meaning | +|---|---| +| `candidate` | Accepted by Guardian but not yet verified on-chain. Awaiting canonicalization. | +| `canonical` | Verified against the network and permanently recorded. | +| `discarded` | Failed on-chain verification. Removed from the active delta chain. | + +In **optimistic mode**, deltas skip the `candidate` stage and are immediately marked `canonical`. + +## Commitments + +A **commitment** is a cryptographic hash that uniquely identifies a particular version of an account's state. Commitments are the integrity backbone of Guardian: + +```mermaid +graph LR + S0["State₀
commitment₀"] -->|"Delta₁
prev: commitment₀"| S1["State₁
commitment₁"] + S1 -->|"Delta₂
prev: commitment₁"| S2["State₂
commitment₂"] + S2 -->|"Delta₃
prev: commitment₂"| S3["State₃
commitment₃"] +``` + +- Each state snapshot has a commitment. +- Each delta includes a `prev_commitment` (the base state) and produces a `new_commitment` (the resulting state). +- The chain ensures that any tampering — inserting, reordering, or dropping deltas — is detectable by any client that tracks commitments. + +## Delta proposals + +A **delta proposal** is a coordination mechanism for multi-party accounts. When multiple signers must agree on a transaction: + +1. **Propose**: One signer creates a delta proposal containing a `TransactionSummary`. Guardian validates the proposal against the current account state. +2. **Sign**: Other authorized cosigners fetch the pending proposal, verify it locally, and submit their signatures. +3. **Execute**: Once enough signatures are collected (meeting the threshold), any cosigner can promote the proposal to a canonical delta via `push_delta`. + +```mermaid +sequenceDiagram + participant P as Proposer + participant Guardian as Guardian Server + participant C as Cosigner + + P->>Guardian: push_delta_proposal
(tx_summary + initial signature) + Guardian->>Guardian: Validate against current state + Guardian-->>P: Return proposal with commitment + + C->>Guardian: get_delta_proposals + Guardian-->>C: Return pending proposals + + C->>C: Verify proposal details locally + C->>Guardian: sign_delta_proposal
(commitment + signature) + Guardian-->>C: Updated proposal with signatures + + P->>Guardian: push_delta
(with all collected signatures) + Guardian->>Guardian: Validate & acknowledge + Guardian-->>P: Canonical delta +``` + +Proposals remain in `pending` status until promoted. Once the corresponding delta becomes canonical, the proposal is automatically cleaned up. + +Delta proposals have their own commitment, derived from `(account_id, nonce, tx_summary)`, used as a stable identifier. diff --git a/versioned_docs/version-0.14/builder/miden-guardian/core-concepts/security.md b/versioned_docs/version-0.14/builder/miden-guardian/core-concepts/security.md new file mode 100644 index 00000000..dc55b0d6 --- /dev/null +++ b/versioned_docs/version-0.14/builder/miden-guardian/core-concepts/security.md @@ -0,0 +1,101 @@ +--- +title: Security +sidebar_position: 4 +--- + +# Edge Cases & Security Considerations + +Guardian is designed around a clear trust model with well-defined security boundaries. + +## Trust model + +The right threat model for Guardian is **honest-but-curious**: the server is expected to follow the protocol and availability guarantees, but may try to learn as much as it can from any data it is given. + +Guardian is non-custodial. The provider holds no keys that can unilaterally move funds. + +### What Guardian can do + +- **Store and relay** state snapshots and deltas. +- **Validate** deltas against the Miden network before acknowledging them. +- **Co-sign** transactions as one party in a threshold scheme. +- **Enforce policies** (rate limits, timelocks) at the co-signing layer. + +### What Guardian cannot do + +- **Forge state**: Every delta references the previous commitment. Inserting, reordering, or dropping deltas breaks the commitment chain, detectable by any client. +- **Move funds unilaterally**: Guardian holds at most one key in a multi-key setup. It always needs the user's key to complete a transaction. +- **Tamper silently**: The server signs each accepted delta with its acknowledgment key. Clients can verify these signatures to detect any tampering. + +### What Guardian can do adversarially + +- **Deny service**: The server can refuse to serve data or accept deltas. This is a liveness issue, not a safety issue — users can recover using their own keys. +- **Withhold updates**: The server could delay propagating deltas to other devices. Clients should verify state freshness against on-chain commitments. + +## Integrity guarantees + +### Commitment chain + +Every delta includes a `prev_commitment` referencing the base state. This creates an unbroken chain: + +- If the server drops a delta, subsequent deltas won't validate because their `prev_commitment` won't match. +- If the server inserts a fake delta, the commitment chain diverges from the on-chain state. +- Clients can verify the chain independently by tracking commitments locally. + +### Server acknowledgment + +After accepting a delta, the server signs the `new_commitment` with its acknowledgment key. Clients should: + +1. Retrieve the server's public key via `/pubkey`. +2. Verify `ack_sig` on every `push_delta` response. +3. Alert on verification failure — it indicates the server may have been compromised or is not processing deltas correctly. + +## 2-of-3 key setup + +A common Guardian configuration uses a **2-of-3** threshold embedded in the account's authentication code: + +| Key | Holder | Purpose | +|---|---|---| +| **Key 1** | User hot key | Daily transactions | +| **Key 2** | User cold key | Recovery and emergency override | +| **Key 3** | Guardian service key | Co-signing and policy enforcement | + +```mermaid +graph TD + subgraph "Normal operation (Hot + Guardian)" + Hot["User Hot Key"] --> TX["Transaction"] + GuardianKey["Guardian Service Key"] --> TX + end + + subgraph "Emergency override (Hot + Cold)" + Hot2["User Hot Key"] --> Override["Rotate Guardian / Adjust policies
Switch providers"] + Cold["User Cold Key"] --> Override + end +``` + +- **Normal operations**: Hot key + Guardian's co-signature suffice. Guardian verifies the signer is working from the latest state. +- **Emergency override**: Hot + cold keys alone can rotate out Guardian, adjust policies, or switch providers. +- **Recovery**: If the Guardian provider disappears, the user's hot + cold keys provide full independent control. + +## Device recovery + +**Without Guardian**: A lost device means falling back to a cold backup. Any state changes since the last checkpoint are lost. If an attacker has the device PIN, funds may be at risk. + +**With Guardian**: The remaining device already has the latest state (synced through Guardian). The user initiates a hot key rotation using their cold key. The stolen device's keys become invalid. Recovery takes minutes. + +## Edge cases + +### State divergence + +If two devices submit deltas referencing different base states, Guardian rejects the conflicting one (commitment mismatch). The rejected device must resync from Guardian before retrying. + +### Stale candidates + +If a candidate delta's on-chain commitment doesn't match during canonicalization, it is marked `discarded`. Clients are signaled to resync. This is not a rollback of the chain — it prevents clients from drifting onto an invalid local branch. + +### Clock skew + +Authentication requires timestamps within a 300-second window. Devices with significantly drifting clocks will fail authentication. Ensure NTP synchronization on client devices. + +### Provider rotation + +Users can switch Guardian providers at any time using their hot + cold keys. The new provider is configured with the account's current state and a fresh cosigner allowlist. The old provider's key is rotated out of the account's authentication policy. diff --git a/versioned_docs/version-0.14/builder/miden-guardian/index.md b/versioned_docs/version-0.14/builder/miden-guardian/index.md new file mode 100644 index 00000000..84052546 --- /dev/null +++ b/versioned_docs/version-0.14/builder/miden-guardian/index.md @@ -0,0 +1,56 @@ +--- +title: Miden Guardian +sidebar_position: 0 +--- + +# Miden Guardian + +Miden Guardian is a system built by [OpenZeppelin](https://www.openzeppelin.com/) that allows Miden accounts to back up and sync their private state securely without trust assumptions about other participants or the server operator. + +## 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 on-chain commitment, but it isn't readable in a way that keeps devices and signers automatically up to date. The coordination surface moves off-chain. + +## What Guardian provides + +Guardian addresses these challenges by acting as an off-chain 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** — Every state change is validated against the Miden network and acknowledged with a cryptographic signature. + +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. + +## Learn more + + + + How Guardian fits between clients and the Miden network. + + + State, deltas, commitments, and delta proposals. + + + API, authentication, storage, and other server components. + + + Trust model, integrity guarantees, and edge cases. + + + How to run, deploy, and troubleshoot a Guardian server. + + + 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.14/builder/miden-guardian/operator-guide/_category_.json b/versioned_docs/version-0.14/builder/miden-guardian/operator-guide/_category_.json new file mode 100644 index 00000000..4608c549 --- /dev/null +++ b/versioned_docs/version-0.14/builder/miden-guardian/operator-guide/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Operator Guide", + "position": 2 +} diff --git a/versioned_docs/version-0.14/builder/miden-guardian/operator-guide/deployment.md b/versioned_docs/version-0.14/builder/miden-guardian/operator-guide/deployment.md new file mode 100644 index 00000000..9031e37b --- /dev/null +++ b/versioned_docs/version-0.14/builder/miden-guardian/operator-guide/deployment.md @@ -0,0 +1,97 @@ +--- +title: Deployment +sidebar_position: 2 +--- + +# How to Deploy + +This page covers Guardian server configuration for production deployments. + +## Environment variables + +| Variable | Default | Description | +|---|---|---| +| `DATABASE_URL` | — | PostgreSQL connection URL (required for Postgres backend) | +| `PSM_KEYSTORE_PATH` | `/var/psm/keystore` | Path for cryptographic key storage | +| `PSM_STORAGE_PATH` | — | Storage backend path (states and deltas) | +| `PSM_METADATA_PATH` | — | Metadata store path | +| `PSM_ENV` | `dev` | Environment mode | +| `RUST_LOG` | `info` | Log level (`debug`, `info`, `warn`, `error`) | + +### Rate limiting + +| Variable | Default | Description | +|---|---|---| +| `PSM_RATE_BURST_PER_SEC` | `10` | Max requests per second (burst) | +| `PSM_RATE_PER_MIN` | `60` | Max requests per minute (sustained) | + +Rate limits are applied per client IP, with enhanced keying when `x-pubkey` or `account_id` is present. Exceeded limits return `429 Too Many Requests` with a `Retry-After` header. + +### Request size limits + +| Variable | Default | Description | +|---|---|---| +| `PSM_MAX_REQUEST_BYTES` | `1048576` (1 MB) | Maximum request body size | + +Requests exceeding this limit receive `413 Payload Too Large`. + +## Storage backends + +Guardian uses a single storage backend per instance. + +### Filesystem (default) + +Stores state and deltas on disk. No external dependencies. Used when the binary is built without the `postgres` feature. + +Ensure `PSM_STORAGE_PATH` points to a writable directory with sufficient disk space. + +### PostgreSQL (optional) + +Requires the `postgres` feature flag at build time. Migrations run automatically on startup. + +```bash +DATABASE_URL=postgres://psm:password@localhost:5432/psm \ + cargo run --features postgres --package guardian-server +``` + +## Metadata store + +The metadata store can be configured independently from the storage backend. It supports both filesystem and PostgreSQL backends. + +Ensure `PSM_METADATA_PATH` points to a writable directory (filesystem mode) or configure `DATABASE_URL` (Postgres mode). + +## Logging + +The server uses structured logging via the `tracing` crate. + +```bash +# Debug level for entire server +RUST_LOG=debug cargo run --package guardian-server + +# Trace only canonicalization jobs +RUST_LOG=server::jobs::canonicalization=trace cargo run + +# Multiple modules +RUST_LOG=server::jobs=debug,server::services=info cargo run +``` + +## Canonicalization configuration + +When canonicalization is enabled (default), configure the verification window: + +| Parameter | Default | Description | +|---|---|---| +| `delay_seconds` | 900 (15 min) | How long a candidate waits before verification | +| `check_interval_seconds` | 60 (1 min) | How often the canonicalization worker runs | + +Set to **optimistic mode** to skip the verification window and mark deltas canonical immediately. + +## Reproducible builds + +The server binary supports reproducible builds. Building from the same source code and target architecture produces bit-for-bit identical binaries: + +```bash +./crates/server/tests/verify-build-hash.sh +``` + +This is useful for verifying published binaries match the source code. diff --git a/versioned_docs/version-0.14/builder/miden-guardian/operator-guide/running.md b/versioned_docs/version-0.14/builder/miden-guardian/operator-guide/running.md new file mode 100644 index 00000000..b3e58122 --- /dev/null +++ b/versioned_docs/version-0.14/builder/miden-guardian/operator-guide/running.md @@ -0,0 +1,86 @@ +--- +title: Running +sidebar_position: 1 +--- + +# How to Run + +This guide covers running a Guardian server locally for development or testing. + +## Prerequisites + +- [Docker](https://docs.docker.com/get-docker/) and Docker Compose (recommended), or +- Rust toolchain 1.90+ (to build from source) + +## Docker Compose (recommended) + +The repository includes a Docker Compose configuration with PostgreSQL: + +```bash +git clone https://github.com/OpenZeppelin/guardian.git +cd guardian +docker-compose up -d +``` + +This starts: + +| Service | Port | Description | +|---|---|---| +| Guardian HTTP API | `localhost:3000` | REST endpoints | +| Guardian gRPC API | `localhost:50051` | gRPC service | +| PostgreSQL | `localhost:5432` | Metadata and state storage | + +View logs: + +```bash +docker-compose logs -f +``` + +Stop services: + +```bash +docker-compose down +``` + +## Building from source + +```bash +git clone https://github.com/OpenZeppelin/guardian.git +cd guardian + +# With filesystem storage (default) +cargo build --release --bin server +cargo run --release --bin server + +# With PostgreSQL storage +DATABASE_URL=postgres://psm:password@localhost:5432/psm \ + cargo run --features postgres --package guardian-server +``` + +## Ports + +| Protocol | Default Port | Description | +|---|---|---| +| HTTP | `3000` | REST API | +| gRPC | `50051` | gRPC service | + +Both can be configured programmatically via the `ServerBuilder`: + +```rust +use server::builder::ServerBuilder; + +let builder = ServerBuilder::new() + .http(true, 3000) + .grpc(true, 50051); +``` + +## Verifying the server + +Once running, check the server's acknowledgment public key: + +```bash +curl http://localhost:3000/pubkey +# Returns: { "pubkey": "0x..." } +``` + +This endpoint is unauthenticated and confirms the server is operational. diff --git a/versioned_docs/version-0.14/builder/miden-guardian/operator-guide/troubleshooting.md b/versioned_docs/version-0.14/builder/miden-guardian/operator-guide/troubleshooting.md new file mode 100644 index 00000000..5ee316aa --- /dev/null +++ b/versioned_docs/version-0.14/builder/miden-guardian/operator-guide/troubleshooting.md @@ -0,0 +1,74 @@ +--- +title: Troubleshooting +sidebar_position: 3 +--- + +# Troubleshooting + +Common issues when operating a Guardian server and how to resolve them. + +## Server won't start + +**Symptom**: Server exits immediately or fails to bind ports. + +- Check that ports 3000 (HTTP) and 50051 (gRPC) are not already in use. +- If using Postgres, ensure `DATABASE_URL` is set and the database is reachable. +- Check logs with `RUST_LOG=debug` for detailed error messages. + +## Acknowledgment key mismatch + +**Symptom**: Clients report `ack_sig` verification failures. + +- Verify the server's public key via `GET /pubkey` and compare with what clients expect. +- If the keystore was regenerated (new `PSM_KEYSTORE_PATH`), clients need to re-fetch the server's public key. +- Ensure `PSM_KEYSTORE_PATH` is persistent across restarts — a new key on every restart will break client verification. + +## Authentication failures + +**Symptom**: Requests return `400 AuthenticationFailed`. + +- **Clock skew**: Client timestamp must be within 300 seconds of the server's time. Ensure NTP synchronization. +- **Replay rejection**: Each request's timestamp must be strictly greater than the account's `last_auth_timestamp`. Rapid-fire requests with the same timestamp will fail. +- **Wrong key**: The `x-pubkey` commitment must be in the account's cosigner allowlist. Verify the account's auth configuration. + +## Deltas stuck as candidates + +**Symptom**: Deltas remain in `candidate` status and never become `canonical`. + +- Check that the canonicalization worker is running (default: checks every 60 seconds). +- Deltas must wait at least `delay_seconds` (default: 15 minutes) before the worker processes them. +- If the on-chain commitment doesn't match, deltas are `discarded`. Check that the transaction was actually submitted and confirmed on-chain. +- Inspect canonicalization logs: `RUST_LOG=server::jobs::canonicalization=debug` + +## Storage and metadata path issues + +**Symptom**: Server returns errors on state or delta operations. + +- Ensure `PSM_STORAGE_PATH` and `PSM_METADATA_PATH` point to writable directories. +- For Postgres: verify the connection string and that migrations have run (they run automatically on startup). +- Check disk space — filesystem storage can grow with the number of accounts and deltas. + +## Rate limiting + +**Symptom**: Clients receive `429 Too Many Requests`. + +- Default limits: 10 requests/second (burst), 60 requests/minute (sustained). +- Adjust via `PSM_RATE_BURST_PER_SEC` and `PSM_RATE_PER_MIN` environment variables. +- The `Retry-After` header in the response indicates how long to wait. + +## Common error codes + +| Error | Meaning | +|---|---| +| `AccountNotFound` | No account configured with this ID | +| `AuthenticationFailed` | Invalid signature, unknown key, or expired timestamp | +| `InvalidDelta` | Delta fails validation against current state or network | +| `CommitmentMismatch` | Delta's `prev_commitment` doesn't match current state | +| `ConflictPendingDelta` | Another candidate delta is already pending for this account | +| `TimestampExpired` | Request timestamp is outside the 300-second window | +| `TimestampReplay` | Request timestamp is not greater than last accepted timestamp | + +## Links + +- [Server README](https://github.com/OpenZeppelin/guardian/tree/main/crates/server) — full server documentation +- [Guardian Specification](https://github.com/OpenZeppelin/guardian/tree/main/spec) — protocol specification diff --git a/versioned_docs/version-0.14/builder/migration/01-imports-dependencies.md b/versioned_docs/version-0.14/builder/migration/01-imports-dependencies.md new file mode 100644 index 00000000..debc577e --- /dev/null +++ b/versioned_docs/version-0.14/builder/migration/01-imports-dependencies.md @@ -0,0 +1,114 @@ +--- +sidebar_position: 1 +title: "Imports & Dependencies" +description: "Crate version bumps, import relocations, and MSRV changes in v0.14" +--- + +# Imports & Dependencies + +:::warning Breaking Change +Miden VM dependencies move from 0.20 to 0.22 and `miden-crypto` from 0.19 to 0.23. Several types have been relocated across crates, and `Felt::as_int()` has been renamed. +::: + +## Quick Fix + +```toml title="Cargo.toml" +# Replace these +miden-protocol = "0.13" +miden-standards = "0.13" +miden-assembly = "0.20" +miden-core = "0.20" +miden-processor = "0.20" +miden-prover = "0.20" +miden-crypto = "0.19" + +# With these +miden-protocol = "0.14" +miden-standards = "0.14" +miden-assembly = "0.22" +miden-core = "0.22" +miden-processor = "0.22" +miden-prover = "0.22" +miden-crypto = "0.23" +``` + +--- + +## Version Bumps + +| Crate | v0.13 | v0.14 | +|-------|-------|-------| +| `miden-protocol` | 0.13 | 0.14 | +| `miden-standards` | 0.13 | 0.14 | +| `miden-assembly` | 0.20 | 0.22 | +| `miden-core` | 0.20 | 0.22 | +| `miden-core-lib` | 0.20 | 0.22 | +| `miden-processor` | 0.20 | 0.22 | +| `miden-prover` | 0.20 | 0.22 | +| `miden-crypto` | 0.19 | 0.23 | + +--- + +## `ExecutionOptions`, `ProvingOptions`, `ExecutionProof` Relocated + +These types moved out of `miden-air` into their respective crates: + +```rust +// Before (0.13) +use miden_air::{ExecutionOptions, ProvingOptions, ExecutionProof}; + +// After (0.14) +use miden_processor::ExecutionOptions; +use miden_prover::ProvingOptions; +use miden_core::ExecutionProof; +``` + +--- + +## `Felt::as_int()` → `Felt::as_canonical_u64()` + +The `Felt::as_int()` method has been renamed to `Felt::as_canonical_u64()` for clarity: + +```rust +// Before (0.13) +let value: u64 = felt.as_int(); + +// After (0.14) +let value: u64 = felt.as_canonical_u64(); +``` + +:::tip +Use find-and-replace across your codebase: `as_int()` → `as_canonical_u64()`. +::: + +--- + +## MSRV (Minimum Supported Rust Version) + +If you depend on `miden-client`, update your `rust-toolchain.toml` to Rust **1.91**: + +```toml title="rust-toolchain.toml" +[toolchain] +channel = "1.91" +``` + +--- + +## Migration Steps + +1. Bump every Miden crate version in `Cargo.toml` per the table above. +2. Move imports of `ExecutionOptions`, `ProvingOptions`, `ExecutionProof` to their new homes. +3. Replace all `Felt::as_int()` calls with `Felt::as_canonical_u64()`. +4. If you depend on `miden-client`, update your `rust-toolchain.toml` to Rust 1.91. +5. Run `cargo update` to pull the new versions. +6. Run `cargo build` and fix any remaining import errors. + +--- + +## Common Errors + +| Error Message | Cause | Solution | +| --- | --- | --- | +| `unresolved import miden_air::ExecutionOptions` | Type moved | Import from `miden_processor::ExecutionOptions`. | +| `no method named as_int found for Felt` | Method renamed | Use `Felt::as_canonical_u64()`. | +| `package requires rustc 1.91` | MSRV bumped | Update toolchain. | diff --git a/versioned_docs/version-0.14/builder/migration/02-hashing-stack.md b/versioned_docs/version-0.14/builder/migration/02-hashing-stack.md new file mode 100644 index 00000000..aaef8d0d --- /dev/null +++ b/versioned_docs/version-0.14/builder/migration/02-hashing-stack.md @@ -0,0 +1,117 @@ +--- +sidebar_position: 2 +title: "Hashing & Stack Changes" +description: "Poseidon2 hash function, little-endian stack, and Falcon module rename in v0.14" +--- + +# Hashing & Stack Changes + +:::warning Breaking Change +The native hash function changed from RPO to Poseidon2, and the operand stack is now little-endian. These are the most fundamental changes in v0.14 — every digest and every multi-limb operation is affected. +::: + +--- + +## Native Hash Function: RPO → Poseidon2 + +### Summary + +The VM's native sponge hash flipped from RPO to Poseidon2. Rust type names (`Word`, `Hasher`, digests) are unchanged but **every digest the VM produces is different**: MAST roots, advice-map keys derived from hashing, account/note commitments, transaction IDs — none of them roundtrip with 0.13 artifacts. + +### Migration Steps + +1. Re-assemble every `.masl` and `.masp` from source under 0.22 (the MAST format version was bumped — old serialized forests will not deserialize anyway). +2. Re-derive any persisted commitments (account commitments, note commitments, advice-map keys, MAST roots). +3. Discard any cached transaction IDs, proven transactions, and proofs from 0.13. +4. If you hard-coded MAST root literals in tests, regenerate them. + +### Common Errors + +| Error Message | Cause | Solution | +| --- | --- | --- | +| `MastForest deserialization failed: unexpected version` | MAST format version bumped along with the hash change | Re-assemble from source under 0.22. | +| `transaction id mismatch` / `account commitment mismatch` | Old digest computed under RPO | Recompute from current state under 0.14. | + +--- + +## Operand Stack Is Little-Endian + +### Summary + +Every multi-limb operation in the VM was unified around **"low limb closest to the top of the stack"**. Before 0.14 conventions were mixed: `u64` lived on the stack as `[hi, lo]`, `hperm` consumed `[R1, R0, C]` and produced its digest at indices 4..8, `mem_stream` returned words with the address-highest word on top. After 0.14 the low-significance limb is always on top: `u64` is `[lo, hi]`, `hperm` takes `[R0, R1, C]` with the digest at indices 0..4, and `mem_stream` returns the words in address-ascending order. + +This affects MASM that uses `u32split`, `u32widening_mul`, `u32madd`, `hperm`, `hmerge`, `mem_stream`, `adv_pipe`, `adv.insert_hdword`, `adv.insert_hdword_d`, `adv.insert_hqword`, `adv.insert_hperm`, all of `std::math::u64`, all of `std::math::u256`, and `ext2` extension-field values. + +### Affected Code + +**MASM (u64 add):** +```masm +# Before (0.13): [b_hi, b_lo, a_hi, a_lo, ...] -> [c_hi, c_lo, ...] +push.0.0xFFFFFFFF push.0.1 +exec.::std::math::u64::wrapping_add + +# After (0.14): [b_lo, b_hi, a_lo, a_hi, ...] -> [c_lo, c_hi, ...] +push.0xFFFFFFFF.0 push.1.0 +exec.::std::math::u64::wrapping_add +``` + +**MASM (`hperm` and digest extraction):** the input is now `[R0, R1, C, ...]` (was `[R1, R0, C, ...]`) and the digest comes out at indices `0..4` (was `4..8`). + +**MASM (`hmerge`):** input is now `[A, B, ...] -> [hash(A || B), ...]` (was `[B, A, ...]`). + +**Rust (`StackInputs` / `StackOutputs`):** + +For `StackInputs::try_from_ints([1, 2, 3, 4])` the first element (`1`) is the top of the stack — no reversal. When seeding a `u64`, push `[lo, hi]`, not `[hi, lo]`. Same when reading `StackOutputs`: `stack[0]` is the top. + +### Migration Steps + +1. Audit every MASM call into `std::math::u64` / `u256` and flip the limb order in adjacent `push`/`movdn`/`movup` instructions. +2. Audit every `hperm`/`hmerge` site and update the index where you extract the digest (it moved from `[4..8]` to `[0..4]`). +3. Audit `mem_stream` / `adv_pipe` users — the word at the address now lands on top, not buried. +4. In Rust, drop any `[Felt; 16]` reversal helpers you used to construct stacks "in pretty order". + +### Common Errors + +| Error Message | Cause | Solution | +| --- | --- | --- | +| `assertion failed: ...` (off-by-one in u64 arithmetic) | Limb order flipped | Push `[lo, hi]` instead of `[hi, lo]`. | +| `expected hash 0x... at clock cycle ...` | Digest at wrong stack offset | The digest is now at indices 0..4, not 4..8. | + +--- + +## Falcon Module Rename + +### Summary + +Because the native hash flipped to Poseidon2, the Falcon-512 verifier was rewritten and renamed. The MASM module path moved to `miden::core::crypto::dsa::falcon512_poseidon2` and the Rust auth scheme variant is `Falcon512Poseidon2`. + +### Affected Code + +**MASM:** +```masm +# Before (0.13) +use.miden::core::crypto::dsa::falcon512rpo +exec.falcon512rpo::verify + +# After (0.14) +use.miden::core::crypto::dsa::falcon512_poseidon2 +exec.falcon512_poseidon2::verify +``` + +**Rust:** +```rust +// Before (0.13) +use miden_protocol::account::auth::AuthScheme; +let scheme = AuthScheme::Falcon512Rpo; + +// After (0.14) +use miden_protocol::account::auth::AuthScheme; +let scheme = AuthScheme::Falcon512Poseidon2; +``` + +### Common Errors + +| Error Message | Cause | Solution | +| --- | --- | --- | +| `unknown module miden::core::crypto::dsa::falcon512rpo` | Module renamed | Use `falcon512_poseidon2`. | +| `no variant or associated item named Falcon512Rpo for AuthScheme` | Variant renamed | Use `AuthScheme::Falcon512Poseidon2`. | diff --git a/versioned_docs/version-0.14/builder/migration/03-account-changes.md b/versioned_docs/version-0.14/builder/migration/03-account-changes.md new file mode 100644 index 00000000..91a206db --- /dev/null +++ b/versioned_docs/version-0.14/builder/migration/03-account-changes.md @@ -0,0 +1,342 @@ +--- +sidebar_position: 3 +title: "Account Changes" +--- + +# Account Changes + +:::warning Breaking Change +The v0.14 release introduces significant breaking changes to account construction, authentication components, and related APIs. All of the changes below require code updates when migrating from v0.13. +::: + +## `AccountComponent::new` requires `AccountComponentMetadata` + +### Summary + +`AccountComponent::new` previously accepted two arguments (library, storage slots). It now requires a third argument: an `AccountComponentMetadata` value that describes the component's name and supported account types. The builder methods `with_metadata`, `with_supported_type`, and `with_supports_all_types` have been removed. Additionally, `AccountComponentTemplateError` has been renamed to `ComponentMetadataError`. + +### Affected Code + +Before: + +```rust +let component = AccountComponent::new(library, storage_slots)? + .with_supports_all_types(); +``` + +After: + +```rust +let metadata = AccountComponentMetadata::new( + "my-component", + AccountType::all(), +)?; + +let component = AccountComponent::new(library, storage_slots, metadata)?; +``` + +### Migration Steps + +1. Create an `AccountComponentMetadata` instance with a name and the set of supported `AccountType` values. +2. Pass the metadata as the third argument to `AccountComponent::new`. +3. Remove any chained calls to `with_supports_all_types()`, `with_supported_type()`, or `with_metadata()`. +4. Replace references to `AccountComponentTemplateError` with `ComponentMetadataError`. + +### Common Errors + +| Error Message | Cause | Solution | +|---|---|---| +| `expected 3 arguments, found 2` | Missing metadata argument | Add `AccountComponentMetadata` as the third argument | +| `no method named with_supports_all_types` | Removed builder method | Use `AccountType::all()` in metadata constructor | +| `AccountComponentTemplateError not found` | Type renamed | Replace with `ComponentMetadataError` | + +## Auth components consolidated into `AuthSingleSig` + +### Summary + +The six per-scheme authentication types (`AuthFalcon512Rpo`, `AuthFalcon512Poseidon2`, `AuthEcdsaK256Keccak`, `AuthEcdsaK256Rpx`, `AuthEcdsaB256Rpx`, `AuthEcdsaB256Poseidon2`) have been replaced by a single `AuthSingleSig` component that accepts an `AuthScheme` enum. The corresponding ACL type `AuthSingleSigAcl` and multisig type `AuthMultisig` follow the same pattern. + +### Affected Code + +Before: + +```rust +// Falcon-based auth +let auth_component: AccountComponent = AuthFalcon512Rpo::new(pk_commitment).into(); + +// ECDSA-based auth +let auth_component: AccountComponent = AuthEcdsaK256Keccak::new(pk_commitment).into(); +``` + +After: + +```rust +// Falcon-based auth +let auth_component: AccountComponent = + AuthSingleSig::new(pk_commitment, AuthScheme::Falcon512Poseidon2).into(); + +// ECDSA-based auth +let auth_component: AccountComponent = + AuthSingleSig::new(pk_commitment, AuthScheme::EcdsaK256Keccak).into(); +``` + +### Migration Steps + +1. Replace all per-scheme auth constructors with `AuthSingleSig::new(pk_commitment, AuthScheme::...)`. +2. Choose the correct `AuthScheme` variant that matches the old type name. +3. Update ACL components to use `AuthSingleSigAcl` and multisig components to use `AuthMultisig`. + +## `@auth_script` MASM attribute replaces `auth_` prefix + +### Summary + +Authentication procedures in MASM are no longer identified by a naming convention (`auth_` prefix). Instead, exactly one procedure per auth component must be annotated with the `@auth_script` attribute. + +### Affected Code + +Before: + +```masm +use.miden::standards::auth::falcon512_rpo + +export.auth_tx_falcon512_rpo + exec.falcon512_rpo::authenticate +end +``` + +After: + +```masm +use.miden::standards::auth::signature + +@auth_script +export.auth_tx + exec.signature::authenticate +end +``` + +### Migration Steps + +1. Replace the scheme-specific `use` import with `use.miden::standards::auth::signature`. +2. Add the `@auth_script` attribute on the line before the procedure declaration. +3. Rename the procedure to remove scheme-specific suffixes (e.g., `auth_tx_falcon512_rpo` becomes `auth_tx`). +4. Replace the scheme-specific `exec` call with `exec.signature::authenticate`. + +## `AccountComponent::get_procedures()` replaced by `procedures()` + +### Summary + +The method `get_procedures()` has been renamed to `procedures()` and now returns an iterator instead of a `Vec<(Word, bool)>`. The `is_auth` flag is now derived from the `@auth_script` attribute rather than a naming convention. + +### Affected Code + +Before: + +```rust +let procs: Vec<(Word, bool)> = component.get_procedures(); +``` + +After: + +```rust +for (proc_root, is_auth) in component.procedures() { + let digest: Word = proc_root.into(); + // is_auth is true when @auth_script is present +} +``` + +### Migration Steps + +1. Replace `get_procedures()` with `procedures()`. +2. Update code to consume an iterator instead of a `Vec`. +3. Note that `is_auth` is now determined by the `@auth_script` attribute, not the procedure name. + +## `commitment()` renamed to `to_commitment()` everywhere; `hash_account` removed + +### Summary + +All `commitment()` methods across the codebase have been renamed to `to_commitment()` for consistency. The standalone `hash_account` function has been removed in favor of calling `to_commitment()` on the account directly. + +### Affected Code + +Before: + +```rust +let account_hash = account.commitment(); +let note_hash = note_header.commitment(); +let account_hash = hash_account(&id, &nonce, &code_commitment, &storage_commitment); +``` + +After: + +```rust +let account_hash = account.to_commitment(); +let note_hash = note_header.to_commitment(); +let account_hash = account.to_commitment(); +``` + +### Migration Steps + +1. Find and replace all calls to `.commitment()` with `.to_commitment()`. +2. Replace any calls to `hash_account(...)` with `.to_commitment()` on the account instance. + +## `TransactionAuthenticator::get_public_key` returns `Arc` + +### Summary + +`TransactionAuthenticator::get_public_key` now returns `Option>` instead of `Option<&PublicKey>`. This change enables sharing the public key across threads without lifetime constraints. + +### Affected Code + +Before: + +```rust +fn get_public_key(&self) -> Option<&PublicKey> { + Some(&self.public_key) +} +``` + +After: + +```rust +fn get_public_key(&self) -> Option> { + Some(Arc::new(self.public_key.clone())) +} +``` + +### Migration Steps + +1. Update the return type of any `get_public_key` implementations to `Option>`. +2. Wrap returned values in `Arc::new(...)`. +3. Add `use std::sync::Arc;` if not already imported. + +## `Ownable2Step` and `MintPolicyConfig` for faucets + +### Summary + +`NetworkFungibleFaucet::new` no longer accepts an owner account ID. Ownership is now managed through a separate `Ownable2Step` component, and mint policy is configured through an `AuthControlled` component with `AuthControlledInitConfig`. This enables two-step ownership transfer and more flexible access control. + +### Affected Code + +Before: + +```rust +let faucet = NetworkFungibleFaucet::new( + owner_account_id, + symbol, + 8, + max_supply, +)?; +``` + +After: + +```rust +let faucet = NetworkFungibleFaucet::new(symbol, 8, max_supply)?; +let ownable = Ownable2Step::new(owner_account_id); +let auth_controlled = AuthControlled::new(AuthControlledInitConfig::AllowAll); + +// Add all three components to the account builder +``` + +### Migration Steps + +1. Remove the `owner_account_id` argument from `NetworkFungibleFaucet::new`. +2. Create an `Ownable2Step` component with the owner account ID. +3. Create an `AuthControlled` component with the desired policy (e.g., `AuthControlledInitConfig::AllowAll`). +4. Add all three components to the account builder. + +## `AccountSchemaCommitment` and `build_with_schema_commitment` + +### Summary + +A new `AccountBuilderSchemaCommitmentExt` extension trait provides a `build_with_schema_commitment()` method on the account builder. This enables building accounts that include a schema commitment for type-safe component validation. + +### Affected Code + +After: + +```rust +use miden_account::AccountBuilderSchemaCommitmentExt; + +let (account, seed) = AccountBuilder::new(init_seed) + .with_component(component) + .build_with_schema_commitment()?; +``` + +### Migration Steps + +1. Add `use miden_account::AccountBuilderSchemaCommitmentExt;` to bring the extension trait into scope. +2. Replace `.build()` with `.build_with_schema_commitment()` where schema commitment is desired. + +## `EthAddress` / `EthEmbeddedAccountId` split + +### Summary + +The single `EthAddressFormat` type has been split into two distinct types: `EthAddress` for raw Ethereum addresses and `EthEmbeddedAccountId` for account IDs that embed an Ethereum address. This provides clearer semantics for different use cases. + +### Affected Code + +Before: + +```rust +let eth_addr = EthAddressFormat::new(*params.origin_token_address); +``` + +After: + +```rust +let eth_addr = EthAddress::new(params.origin_token_address); +let embedded_id = EthEmbeddedAccountId::from_account_id(account_id); +``` + +### Migration Steps + +1. Replace `EthAddressFormat::new(...)` with `EthAddress::new(...)` when working with raw Ethereum addresses. +2. Use `EthEmbeddedAccountId::from_account_id(...)` when extracting an Ethereum address from a Miden account ID. +3. Update imports to use the new type names. + +## `SchemaTypeId` renamed to `SchemaType` + +### Summary + +`SchemaTypeId` has been renamed to `SchemaType`. This is a pure rename with no behavioral changes. + +### Affected Code + +Before: + +```rust +use SchemaTypeId; + +let t: SchemaTypeId = SchemaType::native_felt(); +``` + +After: + +```rust +use SchemaType; + +let t: SchemaType = SchemaType::native_felt(); +``` + +### Migration Steps + +1. Find and replace all occurrences of `SchemaTypeId` with `SchemaType`. + +## Common Errors Reference + +| Error Message | Cause | Solution | +|---|---|---| +| `expected 3 arguments, found 2` on `AccountComponent::new` | Missing `AccountComponentMetadata` argument | Create metadata and pass as third argument | +| `no method named with_supports_all_types` | Removed builder method | Use `AccountType::all()` in `AccountComponentMetadata` | +| `AccountComponentTemplateError not found` | Type renamed | Replace with `ComponentMetadataError` | +| `AuthFalcon512Rpo not found` | Per-scheme auth types removed | Use `AuthSingleSig` with `AuthScheme::Falcon512Poseidon2` | +| `AuthEcdsaK256Keccak not found` | Per-scheme auth types removed | Use `AuthSingleSig` with `AuthScheme::EcdsaK256Keccak` | +| `no method named get_procedures` | Method renamed | Use `procedures()` (returns iterator) | +| `no method named commitment` | Method renamed | Use `to_commitment()` | +| `hash_account not found` | Function removed | Call `.to_commitment()` on the account instance | +| `expected Option<&PublicKey>, found Option>` | Return type changed | Update signature to return `Option>` | +| `expected 4 arguments, found 3` on `NetworkFungibleFaucet::new` | Owner removed from constructor | Remove owner arg; use `Ownable2Step` component instead | +| `EthAddressFormat not found` | Type split into two | Use `EthAddress` or `EthEmbeddedAccountId` | +| `SchemaTypeId not found` | Type renamed | Replace with `SchemaType` | diff --git a/versioned_docs/version-0.14/builder/migration/04-note-changes.md b/versioned_docs/version-0.14/builder/migration/04-note-changes.md new file mode 100644 index 00000000..db2e4702 --- /dev/null +++ b/versioned_docs/version-0.14/builder/migration/04-note-changes.md @@ -0,0 +1,441 @@ +--- +sidebar_position: 4 +title: "Note Changes" +description: "Note API renames, script format, asset building, type simplification, and constructor changes in v0.14" +--- + +# Note Changes + +:::warning Breaking Change +`NoteInputs` has been renamed to `NoteStorage` across the entire API (Rust and MASM), and note scripts are now MASM libraries annotated with `@note_script` instead of programs with `begin` blocks. These two changes affect virtually every note-related code path. +::: + +--- + +## NoteInputs → NoteStorage + +### Summary + +The `NoteInputs` type has been renamed end-to-end to `NoteStorage`. This affects the Rust struct name, its methods, associated constants, `NoteRecipient` construction, the MASM procedure path, and the relevant error variant. + +### Affected Code + +**Rust:** +```rust +// Before (0.13) +use miden_protocol::note::NoteInputs; + +let inputs = NoteInputs::new(values)?; +let v = inputs.values(); +let n = inputs.num_values(); +assert!(n <= NoteInputs::MAX_INPUTS_PER_NOTE); + +let recipient = NoteRecipient::new(serial_num, script, inputs); + +// After (0.14) +use miden_protocol::note::NoteStorage; + +let storage = NoteStorage::new(values)?; +let v = storage.storage(); +let n = storage.num_items(); +assert!(n <= NoteStorage::MAX_NOTE_STORAGE_ITEMS); + +let recipient = NoteRecipient::new(serial_num, script, storage); +``` + +**MASM:** +```masm +# Before (0.13) +exec.active_note::get_inputs + +# After (0.14) +exec.active_note::get_storage +``` + +**Error variants:** +```rust +// Before (0.13) +NoteError::TooManyInputs + +// After (0.14) +NoteError::TooManyStorageItems +``` + +### Migration Steps + +1. Find-and-replace `NoteInputs` with `NoteStorage` across all Rust files. +2. Rename method calls: `values()` → `storage()`, `num_values()` → `num_items()`. +3. Replace `MAX_INPUTS_PER_NOTE` with `MAX_NOTE_STORAGE_ITEMS`. +4. Update the third argument of `NoteRecipient::new(...)` from inputs to storage. +5. In MASM, replace `active_note::get_inputs` with `active_note::get_storage`. +6. Update any error matching on `NoteError::TooManyInputs` to `NoteError::TooManyStorageItems`. + +### Common Errors + +| Error Message | Cause | Solution | +| --- | --- | --- | +| `cannot find type NoteInputs in module note` | Type renamed | Use `NoteStorage`. | +| `no method named values found for NoteStorage` | Method renamed | Use `.storage()`. | +| `unknown procedure active_note::get_inputs` | MASM procedure renamed | Use `active_note::get_storage`. | + +--- + +## NoteMetadata::new No Longer Takes a Tag + +### Summary + +`NoteMetadata::new` previously accepted three arguments including the tag. It now takes only `sender` and `type`. The tag is set separately via the builder method `.with_tag(tag)`. + +### Affected Code + +```rust +// Before (0.13) +let metadata = NoteMetadata::new(sender, note_type, tag); + +// After (0.14) +let metadata = NoteMetadata::new(sender, note_type).with_tag(tag); +``` + +### Migration Steps + +1. Find every call to `NoteMetadata::new(sender, type, tag)`. +2. Remove the third argument and chain `.with_tag(tag)` on the result. + +### Common Errors + +| Error Message | Cause | Solution | +| --- | --- | --- | +| `this function takes 2 arguments but 3 arguments were supplied` | Signature changed | Remove the tag argument and use `.with_tag(tag)`. | + +--- + +## Note Scripts Are MASM Libraries with @note_script + +### Summary + +Note scripts are no longer standalone programs with a `begin ... end` block. They are now MASM libraries that use the `@note_script` attribute on a `pub proc`. In Rust, you construct a `NoteScript` from a compiled `Library` instead of a `Program`. + +### Affected Code + +**MASM:** +```masm +# Before (0.13) +use.miden::contracts::wallets::basic->wallet + +begin + exec.wallet::receive_asset +end + +# After (0.14) +use.miden::contracts::wallets::basic->wallet + +@note_script +pub proc main + exec.wallet::receive_asset +end +``` + +**Rust:** +```rust +// Before (0.13) +let program = assembler.assemble_program(source)?; +let script = NoteScript::new(program); + +// After (0.14) +let library = assembler.assemble_library(source)?; +let script = NoteScript::from_library(&library)?; +``` + +### Migration Steps + +1. In every `.masm` note script, replace the `begin ... end` block with `@note_script pub proc main ... end`. +2. In Rust, switch from `assemble_program` to `assemble_library` and use `NoteScript::from_library(&library)?`. +3. Ensure the procedure is marked `pub` — non-public procedures cannot be note entry points. + +### Common Errors + +| Error Message | Cause | Solution | +| --- | --- | --- | +| `no note_script attribute found in library` | Missing `@note_script` annotation | Add `@note_script` above the entry `pub proc`. | +| `no method named new found for NoteScript` | Constructor changed | Use `NoteScript::from_library(&library)?`. | + +--- + +## NoteAssets::add_asset Removed; OutputNoteBuilder Accumulates + +### Summary + +The `NoteAssets::add_asset` method has been removed. You now construct `NoteAssets` in one shot with the full list of assets. The `OutputNoteBuilder` stores a `Vec` internally and computes the commitment when `.build()` is called. + +### Affected Code + +```rust +// Before (0.13) +let mut assets = NoteAssets::default(); +assets.add_asset(asset_a)?; +assets.add_asset(asset_b)?; + +// After (0.14) +let assets = NoteAssets::new(vec![asset_a, asset_b])?; +``` + +### Migration Steps + +1. Collect all assets into a `Vec` before constructing `NoteAssets`. +2. Replace incremental `add_asset` calls with a single `NoteAssets::new(vec![...])`. +3. If using `OutputNoteBuilder`, pass assets to the builder — it accumulates them internally and computes the commitment on `.build()`. + +### Common Errors + +| Error Message | Cause | Solution | +| --- | --- | --- | +| `no method named add_asset found for NoteAssets` | Method removed | Use `NoteAssets::new(vec![...])`. | + +--- + +## NoteType::Encrypted Removed + +### Summary + +The `NoteType` enum has been simplified to two variants: `Private` and `Public`. The former `Encrypted` variant has been removed. + +### Affected Code + +```rust +// Before (0.13) +let note_type = NoteType::Encrypted; + +// After (0.14) +// Use NoteType::Private or NoteType::Public +let note_type = NoteType::Private; +``` + +### Migration Steps + +1. Replace all uses of `NoteType::Encrypted` with `NoteType::Private` (or `NoteType::Public` depending on your intent). +2. Update any match arms that handle `Encrypted` separately. + +### Common Errors + +| Error Message | Cause | Solution | +| --- | --- | --- | +| `no variant named Encrypted found for enum NoteType` | Variant removed | Use `NoteType::Private` or `NoteType::Public`. | + +--- + +## NoteHeader::commitment → to_commitment; NoteLocation Field Rename + +### Summary + +The method `NoteHeader::commitment` has been renamed to `NoteHeader::to_commitment`. Additionally, the `NoteLocation` field `node_index_in_block` has been renamed to `block_note_tree_index`. + +### Affected Code + +```rust +// Before (0.13) +let commitment = header.commitment(); +let index = location.node_index_in_block; + +// After (0.14) +let commitment = header.to_commitment(); +let index = location.block_note_tree_index; +``` + +### Migration Steps + +1. Replace `.commitment()` with `.to_commitment()` on `NoteHeader`. +2. Replace `node_index_in_block` with `block_note_tree_index` on `NoteLocation`. + +--- + +## OutputNote::Header Removed; PrivateNoteHeader Introduced + +### Summary + +The `OutputNote` enum has been restructured. The old `OutputNote::Full` and `OutputNote::Header` variants are gone. There are now two enums: + +- **`RawOutputNote`** with variants `Full` and `Partial`. +- **`OutputNote`** with variants `Public(PublicOutputNote)` and `Private(PrivateNoteHeader)`. + +### Affected Code + +```rust +// Before (0.13) +match output_note { + OutputNote::Full(note) => { /* ... */ } + OutputNote::Header(header) => { /* ... */ } +} + +// After (0.14) +match output_note { + OutputNote::Public(public_note) => { /* ... */ } + OutputNote::Private(private_header) => { /* ... */ } +} +``` + +### Migration Steps + +1. Replace `OutputNote::Full(note)` matches with `OutputNote::Public(public_note)`. +2. Replace `OutputNote::Header(header)` matches with `OutputNote::Private(private_header)`. +3. If you need raw full/partial note data, use `RawOutputNote::Full` or `RawOutputNote::Partial`. +4. Update any type annotations referencing the old variants. + +### Common Errors + +| Error Message | Cause | Solution | +| --- | --- | --- | +| `no variant named Full found for enum OutputNote` | Enum restructured | Use `OutputNote::Public(PublicOutputNote)`. | +| `no variant named Header found for enum OutputNote` | Variant removed | Use `OutputNote::Private(PrivateNoteHeader)`. | + +--- + +## WellKnownNote / WellKnownComponent → Standard... + +### Summary + +All `WellKnown*` types have been renamed to `Standard*`: + +| Before (0.13) | After (0.14) | +| --- | --- | +| `WellKnownComponent` | `StandardAccountComponent` | +| `WellKnownNote` | `StandardNote` | +| `WellKnownNoteAttachment` | `StandardNoteAttachment` | + +The module path is now `miden_standards::note::StandardNote`. + +### Affected Code + +```rust +// Before (0.13) +use miden_standards::note::WellKnownNote; +use miden_standards::component::WellKnownComponent; +use miden_standards::note::WellKnownNoteAttachment; + +// After (0.14) +use miden_standards::note::StandardNote; +use miden_standards::component::StandardAccountComponent; +use miden_standards::note::StandardNoteAttachment; +``` + +### Migration Steps + +1. Find-and-replace `WellKnownComponent` → `StandardAccountComponent`. +2. Find-and-replace `WellKnownNote` → `StandardNote`. +3. Find-and-replace `WellKnownNoteAttachment` → `StandardNoteAttachment`. +4. Update import paths accordingly. + +--- + +## Standard Component Name Prefix + +### Summary + +The `NAME` constants on standard account components have moved under the `miden::standards::components::*` namespace. + +### Affected Code + +```rust +// Before (0.13) +AuthSingleSig::NAME // "miden::auth::single_sig" +BasicWallet::NAME // "miden::wallets::basic" +BasicFungibleFaucet::NAME // "miden::faucets::basic_fungible" + +// After (0.14) +AuthSingleSig::NAME // "miden::standards::components::auth_single_sig" +BasicWallet::NAME // "miden::standards::components::basic_wallet" +BasicFungibleFaucet::NAME // "miden::standards::components::basic_fungible_faucet" +``` + +### Migration Steps + +1. If you match on or compare against `NAME` constants, update the expected string values. +2. Search your codebase for any hard-coded component name strings and update them to the `miden::standards::components::*` namespace. + +--- + +## NoteExecutionHint Moved to miden-standards + +### Summary + +`NoteExecutionHint` has been relocated from `miden-protocol` to `miden-standards`. + +### Affected Code + +```rust +// Before (0.13) +use miden_protocol::note::NoteExecutionHint; + +// After (0.14) +use miden_standards::note::NoteExecutionHint; +``` + +### Migration Steps + +1. Update all imports of `NoteExecutionHint` to use `miden_standards::note::NoteExecutionHint`. + +### Common Errors + +| Error Message | Cause | Solution | +| --- | --- | --- | +| `unresolved import miden_protocol::note::NoteExecutionHint` | Type moved to `miden-standards` | Import from `miden_standards::note::NoteExecutionHint`. | + +--- + +## 256 KiB Note Size Limit + +### Summary + +Public output notes are now subject to a maximum size of 256 KiB (`NOTE_MAX_SIZE = 2^18` bytes). Notes exceeding this limit will be rejected. + +### Migration Steps + +1. Audit any note construction that could produce large public notes (e.g., notes with many assets or large storage payloads). +2. If your notes approach the 256 KiB limit, consider splitting them or reducing their payload. + +### Common Errors + +| Error Message | Cause | Solution | +| --- | --- | --- | +| `note size exceeds maximum allowed` | Public note exceeds 2^18 bytes | Reduce note payload or split into multiple notes. | + +--- + +## Note Constructors Moved to Associated Methods + +### Summary + +Free-standing note constructor functions have been replaced by associated methods on their respective types. + +### Affected Code + +```rust +// Before (0.13) +let note = create_p2id_note(sender, target, assets, recall_height)?; +let note = create_swap_note(sender, offered, requested)?; +let note = create_mint_note(faucet_id, amount, target)?; +let note = create_burn_note(faucet_id, amount)?; +let note = create_p2ide_note(sender, target, assets, recall_height)?; + +// After (0.14) +let note = P2idNote::create(sender, target, assets, recall_height)?; +let note = SwapNote::create(sender, offered, requested)?; +let note = MintNote::create(faucet_id, amount, target)?; +let note = BurnNote::create(faucet_id, amount)?; +let note = P2ideNote::create(sender, target, assets, recall_height)?; +``` + +### Migration Steps + +1. Replace `create_p2id_note(...)` with `P2idNote::create(...)`. +2. Replace `create_swap_note(...)` with `SwapNote::create(...)`. +3. Replace `create_mint_note(...)` with `MintNote::create(...)`. +4. Replace `create_burn_note(...)` with `BurnNote::create(...)`. +5. Replace `create_p2ide_note(...)` with `P2ideNote::create(...)`. +6. Add the appropriate `use` imports for the new types (e.g., `use miden_standards::note::P2idNote`). + +### Common Errors + +| Error Message | Cause | Solution | +| --- | --- | --- | +| `cannot find function create_p2id_note` | Function moved to associated method | Use `P2idNote::create(...)`. | +| `cannot find function create_swap_note` | Function moved to associated method | Use `SwapNote::create(...)`. | diff --git a/versioned_docs/version-0.14/builder/migration/05-asset-vault-faucet.md b/versioned_docs/version-0.14/builder/migration/05-asset-vault-faucet.md new file mode 100644 index 00000000..e79bba8c --- /dev/null +++ b/versioned_docs/version-0.14/builder/migration/05-asset-vault-faucet.md @@ -0,0 +1,296 @@ +--- +sidebar_position: 5 +title: "Assets, Vault & Faucet" +description: "Two-word asset representation, vault API changes, faucet burn updates, and TokenSymbol changes in v0.14" +--- + +# Assets, Vault & Faucet + +:::warning Breaking Change +Assets are now represented as two words (`ASSET_KEY` + `ASSET_VALUE`) instead of a single `ASSET` word. This is the largest MASM-level change in v0.14 and affects every procedure that creates, inspects, adds, removes, or burns assets. +::: + +--- + +## ASSET to ASSET_KEY + ASSET_VALUE + +### Summary + +The single 4-felt `ASSET` word has been split into two words: + +- **`ASSET_KEY`** = `[asset_id_suffix, asset_id_prefix, (faucet_id_suffix << 8) | callbacks_enabled, faucet_id_prefix]` +- **`ASSET_VALUE`** = `[amount, 0, 0, 0]` for fungible assets, or `DATA_HASH` for non-fungible assets. + +Every kernel procedure and standard-library helper that previously accepted or returned `ASSET` now works with the `ASSET_KEY, ASSET_VALUE` pair. + +### Affected Code + +**MASM (`native_account::add_asset`):** +```masm +# Before (0.13): stack = [ASSET, pad(12)] +exec.native_account::add_asset +# -> [ASSET, pad(12)] + +# After (0.14): stack = [ASSET_KEY, ASSET_VALUE, pad(8)] +exec.native_account::add_asset +# -> [ASSET_KEY, ASSET_VALUE, pad(8)] +``` + +**Rust:** +```rust +// Before (0.13) +let word: Word = fungible_asset.into(); + +// After (0.14) +let key: Word = fungible_asset.to_key_word(); +let value: Word = fungible_asset.to_value_word(); + +// Reconstructing from key/value +let asset = FungibleAsset::from_key_value_words(key, value)?; +``` + +### Migration Steps + +1. Find every MASM site that pushes an `ASSET` word onto the stack before a kernel call. Replace the single word with `ASSET_KEY, ASSET_VALUE` and adjust padding from 12 to 8. +2. In Rust, replace `.into()` conversions to `Word` with `.to_key_word()` and `.to_value_word()`. +3. Replace any `Asset::from(word)` with `Asset::from_key_value_words(key, value)`. +4. Update stack comments throughout your MASM to reflect the new two-word layout. + +### Common Errors + +| Error Message | Cause | Solution | +| --- | --- | --- | +| `FailedAssertion` at `add_asset` / `remove_asset` | Single `ASSET` word pushed instead of key+value pair | Split into `ASSET_KEY` and `ASSET_VALUE`. | +| `no method named into found for FungibleAsset` (when targeting `Word`) | Direct `Word` conversion removed | Use `to_key_word()` and `to_value_word()`. | +| Stack underflow in asset procedures | Padding not adjusted from 12 to 8 | Reduce padding to account for the extra word. | + +--- + +## build_*_asset Renamed to create_*_asset + +### Summary + +The `asset::build_fungible_asset` and `asset::build_non_fungible_asset` procedures have been renamed to `create_fungible_asset` and `create_non_fungible_asset`. They also accept a new `enable_callbacks` flag and return the two-word `ASSET_KEY, ASSET_VALUE` pair. + +### Affected Code + +**MASM (fungible asset creation):** +```masm +# Before (0.13): stack = [faucet_id_prefix, faucet_id_suffix, amount, ...] +exec.asset::build_fungible_asset +# -> [ASSET, ...] + +# After (0.14): stack = [enable_callbacks, faucet_id_suffix, faucet_id_prefix, amount, ...] +exec.asset::create_fungible_asset +# -> [ASSET_KEY, ASSET_VALUE, ...] +``` + +**Rust:** +```rust +// Before (0.13) +let asset = FungibleAsset::new(faucet_id, amount)?; + +// After (0.14) — Rust API may vary; consult crate docs for exact constructor +let asset = FungibleAsset::new(faucet_id, amount)?; +// The MASM-level rename is the primary change; Rust constructors +// now produce assets compatible with the two-word representation. +``` + +### Migration Steps + +1. Rename all `exec.asset::build_fungible_asset` calls to `exec.asset::create_fungible_asset`. +2. Rename all `exec.asset::build_non_fungible_asset` calls to `exec.asset::create_non_fungible_asset`. +3. Add the `enable_callbacks` flag as the new top-of-stack element. +4. Note the changed argument order: `[enable_callbacks, faucet_id_suffix, faucet_id_prefix, amount]`. +5. Update consumers to expect `[ASSET_KEY, ASSET_VALUE]` on the stack instead of a single `[ASSET]`. + +### Common Errors + +| Error Message | Cause | Solution | +| --- | --- | --- | +| `unknown procedure asset::build_fungible_asset` | Procedure renamed | Use `asset::create_fungible_asset`. | +| `unknown procedure asset::build_non_fungible_asset` | Procedure renamed | Use `asset::create_non_fungible_asset`. | +| `FailedAssertion` in `create_fungible_asset` | Missing `enable_callbacks` flag or wrong argument order | Push `[enable_callbacks, faucet_id_suffix, faucet_id_prefix, amount]`. | + +--- + +## get_balance / has_non_fungible_asset Removed + +### Summary + +The kernel no longer exposes `get_balance` or `has_non_fungible_asset`. Instead, a single `get_asset` procedure handles both fungible and non-fungible lookups. It takes an `ASSET_KEY` and returns the corresponding `ASSET_VALUE`. + +### Affected Code + +**MASM (fungible balance check):** +```masm +# Before (0.13): stack = [faucet_id, ...] +exec.native_account::get_balance +# -> [balance, ...] + +# After (0.14): stack = [ASSET_KEY, pad(12)] +exec.native_account::get_asset +# -> [ASSET_VALUE, pad(12)] +# balance is ASSET_VALUE[0] (top of ASSET_VALUE word) +``` + +**MASM (non-fungible existence check):** +```masm +# Before (0.13): stack = [ASSET, pad(12)] +exec.native_account::has_non_fungible_asset +# -> [has_asset, pad(15)] + +# After (0.14): stack = [ASSET_KEY, pad(12)] +exec.native_account::get_asset +# -> [ASSET_VALUE, pad(12)] +# If the asset exists, ASSET_VALUE will be the DATA_HASH; +# if not, ASSET_VALUE will be [0, 0, 0, 0]. +``` + +**Rust:** +```rust +// Before (0.13) +let balance = account.vault().get_balance(faucet_id)?; +let has_nft = account.vault().has_non_fungible_asset(&asset)?; + +// After (0.14) +let asset_value = account.vault().get_asset(asset_key)?; +// For fungible: amount = asset_value[0] +// For non-fungible: check if asset_value != [0, 0, 0, 0] +``` + +### Migration Steps + +1. Replace every `exec.native_account::get_balance` with `exec.native_account::get_asset`. Construct the `ASSET_KEY` for the fungible faucet and read the amount from the first element of the returned `ASSET_VALUE`. +2. Replace every `exec.native_account::has_non_fungible_asset` with `exec.native_account::get_asset`. Construct the `ASSET_KEY` and compare the returned `ASSET_VALUE` against `[0, 0, 0, 0]` to determine existence. +3. The same pattern applies to `get_initial_asset` (for checking assets at transaction start). + +### Common Errors + +| Error Message | Cause | Solution | +| --- | --- | --- | +| `unknown procedure native_account::get_balance` | Procedure removed | Use `native_account::get_asset` with the asset key. | +| `unknown procedure native_account::has_non_fungible_asset` | Procedure removed | Use `native_account::get_asset` and check for zero value. | +| Wrong balance value | Reading wrong element from `ASSET_VALUE` | The amount is in `ASSET_VALUE[0]` (top of stack after the call). | + +--- + +## AssetVault::remove_asset Returns Remaining Asset + +### Summary + +In Rust, `AssetVault::remove_asset` previously returned `Result` (the removed asset). It now returns `Result>` where the value is the **remaining** asset in the vault after partial removal. For non-fungible assets, the return is always `None` (the entire asset is removed). + +### Affected Code + +**Rust:** +```rust +// Before (0.13) +let removed: Asset = vault.remove_asset(asset)?; + +// After (0.14) +let remaining: Option = vault.remove_asset(asset)?; +// remaining = Some(fungible_asset) if fungible with leftover balance +// remaining = None if the vault entry was fully consumed (or non-fungible) +``` + +### Migration Steps + +1. Update all call sites of `AssetVault::remove_asset` to handle `Option` instead of `Asset`. +2. If you previously used the return value as "the asset that was removed", note that the semantics have changed to "the asset remaining in the vault". +3. For non-fungible assets, expect `None` — the asset is either fully present or fully removed. + +### Common Errors + +| Error Message | Cause | Solution | +| --- | --- | --- | +| `expected Asset, found Option` | Return type changed | Unwrap or pattern-match the `Option`. | +| Logic error: treating return as "removed" amount | Semantics changed from removed to remaining | Adjust logic to interpret the value as remaining balance. | + +--- + +## faucet::burn and native_account::remove_asset Stack Effects + +### Summary + +Both `faucet::burn` and `native_account::remove_asset` have updated stack effects to match the two-word asset representation. Notably, `faucet::burn` no longer returns the asset on the stack. + +### Affected Code + +**MASM (`faucet::burn`):** +```masm +# Before (0.13): stack = [ASSET, ...] -> [ASSET, ...] +exec.faucet::burn + +# After (0.14): stack = [ASSET_KEY, ASSET_VALUE, ...] -> [...] +exec.faucet::burn +# Stack is consumed — nothing is returned for the burned asset. +``` + +**MASM (`native_account::remove_asset`):** +```masm +# Before (0.13): stack = [ASSET, pad(12)] -> [ASSET, pad(12)] +exec.native_account::remove_asset + +# After (0.14): stack = [ASSET_KEY, ASSET_VALUE, pad(8)] -> [REMAINING_ASSET_VALUE, pad(12)] +exec.native_account::remove_asset +# Returns the REMAINING asset value in the vault (not the removed one). +``` + +### Migration Steps + +1. For `faucet::burn`: remove any code that reads the return value from the stack. The procedure now consumes `[ASSET_KEY, ASSET_VALUE]` and leaves nothing. +2. For `native_account::remove_asset`: update stack expectations. The output is `[REMAINING_ASSET_VALUE, pad(12)]`, not the removed asset. +3. If you need the removed amount, compute it before calling `remove_asset` (e.g., subtract the remaining value from the original). + +### Common Errors + +| Error Message | Cause | Solution | +| --- | --- | --- | +| Stack underflow after `faucet::burn` | Code tries to read a return value that no longer exists | `burn` now returns nothing; remove post-call reads. | +| Wrong amount after `remove_asset` | Return value is remaining, not removed | Compute removed amount separately if needed. | +| `FailedAssertion` in `burn` or `remove_asset` | Single `ASSET` word passed instead of key+value | Pass `[ASSET_KEY, ASSET_VALUE]` with correct padding. | + +--- + +## TokenSymbol Changes + +### Summary + +The `TokenSymbol` type has several breaking changes: + +- **`to_string()`** is now available via the `Display` trait (infallible). Previously it could fail. +- **`default()`** has been removed — there is no zero-value token symbol. +- **`TryFrom`** now rejects values below `MIN_ENCODED_VALUE`. +- **`MAX_SYMBOL_LENGTH`** increased from **6** to **12** characters. + +### Affected Code + +**Rust:** +```rust +// Before (0.13) +let symbol = TokenSymbol::default(); // zero-value placeholder +let s = symbol.to_string()?; // fallible conversion +let sym = TokenSymbol::try_from(felt)?; // accepted any felt + +// After (0.14) +// TokenSymbol::default() is removed — use a real symbol +let s = format!("{}", symbol); // Display trait, infallible +let sym = TokenSymbol::try_from(felt)?; // rejects below MIN_ENCODED_VALUE +// Symbols can now be up to 12 characters (was 6) +``` + +### Migration Steps + +1. Remove any calls to `TokenSymbol::default()`. Replace with an explicit symbol via `TokenSymbol::new("TOKEN")` or equivalent constructor. +2. Replace fallible `to_string()` calls with `format!("{}", symbol)` or `.to_string()` (now infallible via `Display`). +3. If you validate token symbol length, update the upper bound from 6 to 12. +4. If you construct `TokenSymbol` from a `Felt`, ensure the value is at or above `MIN_ENCODED_VALUE`. + +### Common Errors + +| Error Message | Cause | Solution | +| --- | --- | --- | +| `no function or associated item named default found for TokenSymbol` | `default()` removed | Use an explicit symbol value. | +| `encoded value below minimum` / `InvalidTokenSymbol` | `TryFrom` now rejects small values | Ensure the felt is at or above `MIN_ENCODED_VALUE`. | +| Compilation warning about unused `Result` on `to_string()` | Now returns `String` directly via `Display` | Remove error handling around `to_string()`. | diff --git a/versioned_docs/version-0.14/builder/migration/06-transaction-changes.md b/versioned_docs/version-0.14/builder/migration/06-transaction-changes.md new file mode 100644 index 00000000..6ff5513f --- /dev/null +++ b/versioned_docs/version-0.14/builder/migration/06-transaction-changes.md @@ -0,0 +1,230 @@ +--- +sidebar_position: 6 +title: "Transaction Changes" +description: "TransactionId hashing, ProvenTransaction construction, kernel event namespacing, stack order, and other transaction-level changes in v0.14" +--- + +# Transaction Changes + +:::warning Breaking Change +Transaction identity, construction, event namespacing, summary stack layout, output accessors, and block signing have all changed in v0.14. These changes affect anyone building, proving, verifying, or signing transactions and blocks. +::: + +--- + +## TransactionId Now Hashes the Fee Asset + +### Summary + +`TransactionId::new` now requires a `fee_asset: FungibleAsset` parameter so the fee is included in the transaction hash. The function takes 5 arguments (6 words total) instead of the previous 4. + +### Affected Code + +**Rust:** +```rust +// Before (0.13) +let tx_id = TransactionId::new(init, final_state, input_notes, output_notes); + +// After (0.14) +let tx_id = TransactionId::new(init, final_state, input_notes, output_notes, fee_asset); +``` + +### Migration Steps + +1. Add the `fee_asset: FungibleAsset` argument to every `TransactionId::new` call site. +2. Ensure the `fee_asset` value matches the fee used in the transaction. +3. If you compute or verify transaction IDs externally (e.g., in tests), update the hashing logic to include the fee asset. + +### Common Errors + +| Error Message | Cause | Solution | +| --- | --- | --- | +| `this function takes 5 arguments but 4 arguments were supplied` | Missing `fee_asset` parameter | Add the `FungibleAsset` fee as the fifth argument. | +| Transaction ID mismatch in verification | Hash computed without fee asset | Recompute the ID with the fee asset included. | + +--- + +## ProvenTransactionBuilder Removed in Favor of ProvenTransaction::new + +### Summary + +The `ProvenTransactionBuilder` has been removed. Instead, construct a `TxAccountUpdate` and pass it directly to `ProvenTransaction::new`. + +### Affected Code + +**Rust:** +```rust +// Before (0.13) +let proven_tx = ProvenTransactionBuilder::new(account_id, init_hash, final_hash, proof) + .account_update_details(details) + .add_input_notes(input_notes) + .add_output_notes(output_notes) + .build()?; + +// After (0.14) +let account_update = TxAccountUpdate::new(account_id, init_hash, final_hash, details); +let proven_tx = ProvenTransaction::new( + account_update, + input_notes, + output_notes, + ref_block_num, + ref_block_commitment, + fee, + expiration_block_num, + proof, +); +``` + +### Migration Steps + +1. Remove all `ProvenTransactionBuilder` usage. +2. Create a `TxAccountUpdate` from the account ID, initial hash, final hash, and update details. +3. Call `ProvenTransaction::new` with the account update and all remaining fields as positional arguments. +4. If you previously relied on builder defaults for optional fields, you must now provide them explicitly (e.g., `ref_block_num`, `ref_block_commitment`, `expiration_block_num`). + +### Common Errors + +| Error Message | Cause | Solution | +| --- | --- | --- | +| `cannot find struct ProvenTransactionBuilder` | Builder removed | Use `ProvenTransaction::new` with `TxAccountUpdate`. | +| `cannot find struct TxAccountUpdate` | Missing import | Add `use miden_objects::transaction::TxAccountUpdate;` (check crate docs for exact path). | +| Wrong number of arguments to `ProvenTransaction::new` | Missing required positional parameters | Provide all 8 arguments: account_update, input_notes, output_notes, ref_block_num, ref_block_commitment, fee, expiration_block_num, proof. | + +--- + +## Kernel Events Prefixed with miden::protocol + +### Summary + +All kernel events have been renamespaced under `miden::protocol::`. The previous `miden::` prefix is no longer recognized. + +### Affected Code + +**MASM:** +```masm +# Before (0.13) +event("miden::account::vault_before_add_asset") +event("miden::auth::request") + +# After (0.14) +event("miden::protocol::account::vault_before_add_asset") +event("miden::protocol::auth::request") +``` + +### Migration Steps + +1. Search your MASM files for all `event("miden::` occurrences. +2. Replace `miden::` with `miden::protocol::` in every event name. +3. If you have event handlers or listeners matching on event names, update those patterns as well. + +### Common Errors + +| Error Message | Cause | Solution | +| --- | --- | --- | +| Event not firing / handler not triggered | Event name changed but handler still matches old name | Update both the event emission and handler to use `miden::protocol::` prefix. | +| `unknown event` or silent no-op | Old event name no longer exists | Rename to `miden::protocol::...`. | + +--- + +## Transaction Summary Stack Order Reversed + +### Summary + +The 4-word message that authentication procedures sign has its order reversed. Previously `SALT` was on top of the stack; now `ACCOUNT_DELTA_COMMITMENT` is on top. + +### Affected Code + +**MASM (stack layout for auth signing):** +```masm +# Before (0.13) +# [SALT, OUTPUT_NOTES_COMMITMENT, INPUT_NOTES_COMMITMENT, ACCOUNT_DELTA_COMMITMENT, PUB_KEY] + +# After (0.14) +# [ACCOUNT_DELTA_COMMITMENT, INPUT_NOTES_COMMITMENT, OUTPUT_NOTES_COMMITMENT, SALT, PUB_KEY] +``` + +### Migration Steps + +1. If you implement a custom authentication procedure that reads or signs the transaction summary, update the expected stack layout. +2. The new order (top to bottom) is: `ACCOUNT_DELTA_COMMITMENT`, `INPUT_NOTES_COMMITMENT`, `OUTPUT_NOTES_COMMITMENT`, `SALT`, then `PUB_KEY`. +3. Update any manual stack manipulation that assumed the old ordering. + +### Common Errors + +| Error Message | Cause | Solution | +| --- | --- | --- | +| Signature verification failure | Auth procedure signing words in old order | Update to the new stack order before signing. | +| `FailedAssertion` in custom auth | Stack words read in wrong positions | Reorder stack reads to match new layout. | + +--- + +## TransactionOutputs Fields Private + +### Summary + +Fields on `TransactionOutputs` are no longer public. Use accessor methods instead. + +### Affected Code + +**Rust:** +```rust +// Before (0.13) +let account = outputs.account; +let fee = outputs.fee; +let expiration = outputs.expiration_block_num; +let notes = outputs.output_notes; + +// After (0.14) +let account = outputs.account(); +let fee = outputs.fee(); +let expiration = outputs.expiration_block_num(); +let notes = outputs.output_notes(); // now returns &RawOutputNotes +``` + +### Migration Steps + +1. Replace all direct field accesses on `TransactionOutputs` with the corresponding method calls. +2. Note that `output_notes()` now returns `&RawOutputNotes` instead of the previous type. Update downstream code to work with `RawOutputNotes`. +3. Since accessors return references, add borrows or clones where ownership was previously obtained via field access. + +### Common Errors + +| Error Message | Cause | Solution | +| --- | --- | --- | +| `field account of TransactionOutputs is private` | Fields no longer public | Use `outputs.account()`. | +| `field fee of TransactionOutputs is private` | Fields no longer public | Use `outputs.fee()`. | +| Type mismatch on output notes | `output_notes()` returns `&RawOutputNotes` | Update code to handle `RawOutputNotes` instead of the previous type. | + +--- + +## SignedBlock Added; BlockSigner Removed + +### Summary + +The `BlockSigner` trait has been removed. Instead, sign the block header commitment directly and construct a `SignedBlock`. + +### Affected Code + +**Rust:** +```rust +// Before (0.13) +let signed = signer.sign(&header); + +// After (0.14) +let sig = sk.sign(header.to_commitment()); +let signed_block = SignedBlock::new(header, body, sig); +``` + +### Migration Steps + +1. Remove all `BlockSigner` trait implementations and usages. +2. Sign `header.to_commitment()` with your signing key directly. +3. Construct `SignedBlock::new(header, body, signature)` with the header, block body, and resulting signature. + +### Common Errors + +| Error Message | Cause | Solution | +| --- | --- | --- | +| `cannot find trait BlockSigner` | Trait removed | Sign `header.to_commitment()` directly with your key. | +| `cannot find struct SignedBlock` | Missing import | Add the appropriate import for `SignedBlock` from the Miden crate. | +| Signature verification fails on block | Signing the header directly instead of its commitment | Sign `header.to_commitment()`, not the header itself. | diff --git a/versioned_docs/version-0.14/builder/migration/07-client-changes.md b/versioned_docs/version-0.14/builder/migration/07-client-changes.md new file mode 100644 index 00000000..48cd8c3f --- /dev/null +++ b/versioned_docs/version-0.14/builder/migration/07-client-changes.md @@ -0,0 +1,381 @@ +--- +sidebar_position: 7 +title: "Client Changes" +description: "Web SDK resource-based API, Rust SDK keystore trait, lazy readers, state sync, and CLI changes in v0.14" +--- + +# Client Changes + +:::info Affects Both Rust and Web SDKs +v0.14 introduces significant API changes to both the Rust and Web Miden clients. The Web SDK adopts a resource-based API pattern, while the Rust SDK gains a new `Keystore` trait, lazy readers, and streamlined state sync. Review both sections if you maintain cross-platform code. +::: + +--- + +## Web SDK + +### New resource-based MidenClient API + +The monolithic `WebClient` god-object has been replaced by `MidenClient` with dedicated resource sub-objects. All operations are now accessed through typed namespaces: `client.accounts`, `client.transactions`, `client.notes`, `client.tags`, `client.settings`, `client.compile`, and `client.keystore`. + +#### Construction + +```typescript +// Before (0.13) +import { WebClient } from "@miden-sdk/miden-sdk"; + +const client = new WebClient(); +await client.createClient({ + node_url: "https://rpc.testnet.miden.io", + store_name: "my-store", +}); +``` + +```typescript +// After (0.14) +import { MidenClient, AccountType } from "@miden-sdk/miden-sdk"; + +const client = await MidenClient.create({ + rpcUrl: "https://rpc.testnet.miden.io", + noteTransportUrl: "https://ntx.testnet.miden.io", + storeName: "my-store", +}); + +// Or use the testnet convenience constructor +const client = await MidenClient.createTestnet(); +``` + +#### Account creation + +```typescript +// Before (0.13) +const wallet = await client.newWallet( + AccountStorageMode.private(), + true, + undefined, +); +const faucet = await client.newFaucet( + AccountStorageMode.public(), + false, + "DAG", + 8, + BigInt(10_000_000), +); +``` + +```typescript +// After (0.14) +const wallet = await client.accounts.create(); // mutable, private wallet (defaults) +const faucet = await client.accounts.create({ + type: AccountType.FungibleFaucet, + symbol: "DAG", + decimals: 8, + maxSupply: 10_000_000n, + storage: "public", +}); +``` + +#### Sending assets + +```typescript +// Before (0.13) +const request = client.newSendTransactionRequest( + wallet.id(), + AccountId.fromHex(targetHex), + faucet.id(), + NoteType.private(), + BigInt(100), + null, + null, +); +const tx = await client.submitNewTransaction(wallet.id(), request); +``` + +```typescript +// After (0.14) +const { txId } = await client.transactions.send({ + account: wallet, // executing (sender) account + to: targetHex, // string hex ID, AccountId, or Account all accepted + token: faucet, // faucet account that minted the asset + amount: 100n, + waitForConfirmation: true, +}); +``` + +#### Consuming notes + +```typescript +// Before (0.13) +const notes = await client.getConsumableNotes(wallet.id()); +const req = client.newConsumeTransactionRequest( + [notes[0].inputNoteRecord().id().toString()], +); +await client.submitNewTransaction(wallet.id(), req); +``` + +```typescript +// After (0.14) +const notes = await client.notes.listAvailable({ account: wallet }); +await client.transactions.consume({ account: wallet, notes: [notes[0]] }); + +// Or consume every available note in one call: +await client.transactions.consumeAll({ account: wallet }); +``` + +#### Listing accounts + +```typescript +// Before (0.13) +const accounts = await client.getAccounts(); +``` + +```typescript +// After (0.14) +const accounts = await client.accounts.list(); +``` + +#### Custom contracts + +The new API adds first-class support for deploying custom smart contracts: + +```typescript +// After (0.14) — compile and deploy a custom contract +const component = await client.compile.component({ + code: contractMasm, + slots: [], +}); + +const contract = await client.accounts.create({ + type: AccountType.MutableContract, + seed: new Uint8Array(32), + auth: secretKey, + components: [component], +}); + +// Compile a transaction script and execute it against the contract. +const script = await client.compile.txScript({ + code: scriptMasm, +}); +await client.transactions.execute({ account: contract, script }); +``` + +--- + +### AccountId.fromHex and key getters return Result + +`AccountId.fromHex()` and related key getter methods previously called `unwrap()` internally and panicked on invalid input. They now return a `Result` type that throws on error in JavaScript. + +```typescript +// Before (0.13) — panics on invalid hex +const id = AccountId.fromHex(hexString); + +// After (0.14) — throws a descriptive error on invalid hex +try { + const id = AccountId.fromHex(hexString); +} catch (e) { + console.error("Invalid account ID:", e.message); +} +``` + +--- + +### Keystore API moved to client.keystore sub-object + +Top-level keystore functions have been consolidated into the `client.keystore` namespace: + +```typescript +// Before (0.13) +import { addAccountSecretKeyToWebStore, getAccountSecretKeyFromWebStore } from "@miden-sdk/miden-sdk"; + +await addAccountSecretKeyToWebStore(accountId, secretKey); +const key = await getAccountSecretKeyFromWebStore(accountId); +``` + +```typescript +// After (0.14) +await client.keystore.insert(accountId, secretKey); +const key = await client.keystore.get(pubKeyCommitment); // takes a Word commitment +const commitments = await client.keystore.getCommitments(accountId); +const id = await client.keystore.getAccountId(pubKeyCommitment); +``` + +--- + +## Rust SDK + +### Keystore trait replaces TransactionAuthenticator in Client + +The new `Keystore` super-trait extends `TransactionAuthenticator` and consolidates key management. The client no longer requires separate calls to register public key commitments. + +```rust +// Before (0.13) +keystore.insert_key(&secret).await?; +client.register_account_public_key_commitments( + account_id, + &[public_key_commitment], +).await?; +``` + +```rust +// After (0.14) +// Keystore::add_key handles both storage and commitment registration +keystore.add_key(&secret, account_id).await?; +``` + +The `Client::new()` constructor now accepts `impl Keystore` instead of `impl TransactionAuthenticator`: + +```rust +// Before (0.13) +let client = Client::new(rpc, store, authenticator, ...); + +// After (0.14) +let client = Client::new(rpc, store, keystore, ...); +``` + +--- + +### New AccountReader and InputNoteReader + +Lazy readers allow you to access account and note data without loading everything into memory at once. + +```rust +// Before (0.13) — loads the full Account object +let account = client.get_account(account_id).await?.unwrap(); +let vault = account.vault(); +let storage = account.storage(); +``` + +```rust +// After (0.14) — lazy reader exposes commitments, balances, and storage items +let reader = client.account_reader(account_id); +let (header, status) = reader.header().await?; +let balance = reader.get_balance(faucet_id).await?; +let storage_item = reader.get_storage_item(slot_name).await?; +// plus reader.nonce(), vault_root(), storage_commitment(), code_commitment() +``` + +```rust +// After (0.14) — lazy, iterator-style traversal of notes consumable by an account +let mut notes = client.input_note_reader(consumer_account_id); +while let Some(note) = notes.next().await? { + // process each InputNoteRecord +} +``` + +--- + +### Lazy foreign-account loading + +Public foreign accounts are now fetched automatically via RPC when needed during transaction execution. You only need to provide `PartialAccount` data upfront for **private** foreign accounts. + +```rust +// Before (0.13) — all foreign accounts had to be provided +let foreign_accounts = vec![ + ForeignAccount::public(public_account_id).await?, + ForeignAccount::private(private_partial_account), +]; +let tx_request = TransactionRequestBuilder::new() + .with_foreign_accounts(foreign_accounts) + .build()?; + +// After (0.14) — only private foreign accounts need explicit loading +let tx_request = TransactionRequestBuilder::new() + .with_foreign_accounts(vec![ + ForeignAccount::private(private_partial_account), + ]) + .build()?; +// Public foreign accounts are auto-fetched via RPC during execution +``` + +--- + +### StorageMapKey is now a newtype + +`StorageMapKey` was previously a type alias for `Word`. It is now a proper newtype, and the `LexicographicWord` wrapper has been removed. + +```rust +// Before (0.13) +use miden_objects::accounts::StorageMapKey; +let key: StorageMapKey = [felt0, felt1, felt2, felt3]; // Was just a Word alias + +// After (0.14) +use miden_protocol::account::StorageMapKey; +use miden_protocol::Word; +let key = StorageMapKey::new(Word::from([felt0, felt1, felt2, felt3])); +// LexicographicWord is no longer needed — StorageMapKey handles ordering. +// For sequential u32 keys, use the StorageMapKey::from_index(idx) shortcut. +``` + +--- + +### `sync_state()` no longer takes arguments; `StateSyncInput` now internal + +`Client::sync_state()` builds its own `StateSyncInput` from the current store state — you no longer pass `account_ids`, `note_tags`, or `nullifiers` by hand. For custom sync scenarios, construct a `StateSyncInput` explicitly via `Client::build_sync_input()` and use the lower-level `StateSync` type. + +```rust +// Before (0.13) +client.sync_state( + &mut partial_mmr, + account_ids, + note_tags, + nullifiers, +).await?; +``` + +```rust +// After (0.14) +let summary = client.sync_state().await?; + +// For custom sync scenarios, build the input manually: +use miden_client::sync::StateSyncInput; +let input: StateSyncInput = client.build_sync_input().await?; +// …tweak `input.note_tags`, `input.input_notes`, etc. and drive +// a `StateSync` instance directly. +``` + +--- + +### MSRV 1.93 + +The minimum supported Rust version is now **1.93**. Update your toolchain: + +```toml title="rust-toolchain.toml" +[toolchain] +channel = "1.93" +``` + +--- + +### CLI: CliConfig::load and CliClient::new + +The CLI configuration and client constructors have been renamed for consistency. + +```rust +// Before (0.13) +let config = CliConfig::from_system()?; +let client = CliClient::from_system_user_config(config, keystore).await?; +``` + +```rust +// After (0.14) +let config = CliConfig::load()?; +let client = CliClient::new(config, keystore).await?; +``` + +--- + +## New Client Features + +:::info New in v0.14 +These are new capabilities introduced in v0.14 that do not require migration but are worth adopting. +::: + +- **NoteScreener and batch screening** - The `Client::note_screener()` API provides efficient batch screening of notes against account filters, replacing manual note-by-note checking. + +- **Account history pruning** - New methods allow pruning old account state history to reduce local storage usage while preserving the current state. + +- **GrpcClient automatic retry on rate limiting** - The gRPC client now automatically retries requests when rate-limited by the node, with up to 5 retries and honoring the server's `retry-after` header. + +- **Automatic NTX note script registration** - Note scripts sent via the NTX transport are now registered automatically. Manual `register_note_script()` calls are no longer needed. + +- **Typed RPC error parsing** - RPC errors are now parsed into structured Rust types, enabling programmatic error handling instead of string matching on error messages. diff --git a/versioned_docs/version-0.14/builder/migration/08-masm-changes.md b/versioned_docs/version-0.14/builder/migration/08-masm-changes.md new file mode 100644 index 00000000..1859170a --- /dev/null +++ b/versioned_docs/version-0.14/builder/migration/08-masm-changes.md @@ -0,0 +1,295 @@ +--- +sidebar_position: 8 +title: "MASM Changes" +description: "New MASM features and standard library additions in v0.14" +--- + +# MASM Changes + +:::info New MASM Features +Miden v0.14 introduces assembly-time string-literal constants (`word("...")`, `event("...")`) and new standard library modules for MASM, including native 128-bit unsigned integer arithmetic. +::: + +--- + +## String-Literal Constants: `word("...")` and `event("...")` + +### Summary + +Miden v0.14 adds two constant expressions that derive their values from a string literal at assembly time: + +- `word("literal")` — hashes the literal with Blake3 and produces a `Word` (four field elements). +- `event("literal")` — hashes the literal with Blake3 and reduces to a single `Felt` event id (the first element of the word, reduced modulo the Goldilocks prime). + +Both run at assembly time, so the hashing cost is paid by the assembler — the emitted MAST sees only the final word/felt immediate. + +:::note Kernel events also renamespaced in v0.14 +Event names on the kernel moved from `miden::…` to `miden::protocol::…` — see [Kernel Events Prefixed with `miden::protocol`](./transaction-changes#kernel-events-prefixed-with-midenprotocol). +::: + +### `word("...")` + +`word("literal")` is valid anywhere a `Word` constant expression is accepted, most commonly in a `const` declaration. The literal string is hashed with Blake3; the 32-byte digest is split into four little-endian `u64` limbs and each limb becomes a `Felt`. + +**Naming a storage slot:** + +```masm +use.miden::protocol::active_account +use.miden::protocol::native_account +use.miden::core::sys + +const COUNTER_SLOT = word("miden::tutorials::counter") + +#! Inputs: [] +#! Outputs: [count] +pub proc get_count + push.COUNTER_SLOT[0..2] exec.active_account::get_item + # => [count] + + exec.sys::truncate_stack +end + +#! Inputs: [] +#! Outputs: [] +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 +``` + +`push.COUNTER_SLOT` pushes the full four-element word; `push.COUNTER_SLOT[0..2]` pushes a slice of the word. The derived word is deterministic — the same literal always hashes to the same word — so components that agree on a string convention share the same slot without a separately distributed constant. + +`word("...")` is only valid inside a `const` declaration. `push` and other instructions accept named constants but not inline `word("...")` calls, so always bind the expression to a name first. + +### `event("...")` + +`event("literal")` derives a `Felt` event id from the literal using the same hashing procedure as `word(...)`, then taking the first limb. This matches `EventId::from_name` in `miden-core`. + +Use `event("...")` in a `const` to name an event, then `emit.NAME`: + +```masm +const U64_DIV_EVENT = event("miden::core::math::u64::u64_div") + +proc handler + # ... stack shaping ... + emit.U64_DIV_EVENT + # ... continue ... +end +``` + +You can also emit a literal inline, without naming it: + +```masm +begin + emit.event("miden::test::equiv") +end +``` + +The assembler guarantees the three forms below produce the same MAST: + +```masm +const EVT = event("miden::test::equiv") + +# (1) named constant +begin + emit.EVT +end + +# (2) inline literal +begin + emit.event("miden::test::equiv") +end + +# (3) manual push + emit +begin + push.EVT + emit + drop +end +``` + +### Where each is valid + +| Context | `word("...")` | `event("...")` | +| ------------------------------------ | :-----------: | :------------: | +| `const NAME = ...` | yes | yes | +| `push.NAME` (immediate Word context) | yes | — | +| `push.NAME` (immediate Felt context) | — | yes | +| `emit.NAME` | — | yes | +| `emit.event("...")` inline | — | yes | +| Inline `word("...")` in `push.` | no | — | + +`emit.CONST` is accepted when `CONST` resolves (directly or via an alias chain) to an `event("...")` hash. A constant declared with `word("...")` or a plain integer will be rejected by the assembler. + +### Why prefer literal hashing? + +- **Readable MASM.** `event("miden::core::math::u64::u64_div")` documents intent directly in the source; the equivalent `push.` loses all context. +- **Decoupled components.** Two MASM libraries that agree on a string name (e.g. `"miden::tutorials::counter"`) share a storage slot or event id without passing a constant between them. +- **Assembly-time cost.** The Blake3 hash is computed once by the assembler; the running VM sees a plain immediate. + +### Notes and limitations + +- Hashing uses **Blake3**, not the VM's native hash. The derived values are convenient identifiers, not commitments produced inside a proof. +- `word("")` and `event("")` (empty string) are valid and hash like any other input; collisions are vanishingly unlikely but not impossible. +- Event ids are `Felt`s, so they are reduced modulo the Goldilocks prime ($p = 2^{64} - 2^{32} + 1$). Two distinct Blake3 digests whose first 8 little-endian bytes coincide modulo $p$ would collide — again, vanishingly unlikely for meaningful names. +- These constants are evaluated inside the assembler (`miden-assembly` / `miden-assembly-syntax`). Code generated before v0.14 that did its own string-to-felt mapping for event ids must drop that logic — the assembler now owns it. + +--- + +## 128-bit Integer Math: `std::math::u128` + +### Summary + +A new `std::math::u128` module provides full 128-bit unsigned integer arithmetic in MASM. A `u128` value is represented as four `u32` limbs in **little-endian** order `[a0, a1, a2, a3]`, where `a0` (the low limb) sits on top of the stack. + +For example, the value `0x00000001_00000002_00000003_00000004` is pushed as: + +```masm +# Stack (top → bottom): [0x4, 0x3, 0x2, 0x1] +# a0 a1 a2 a3 +push.0x00000001.0x00000002.0x00000003.0x00000004 +``` + +### Usage Examples + +**Wrapping addition:** + +```masm +use.std::math::u128 + +# Push two u128 values (low limb on top) +# a = 0x00000000_00000000_00000000_00000005 +push.0.0.0.5 +# b = 0x00000000_00000000_00000000_00000003 +push.0.0.0.3 + +exec.u128::wrapping_add +# Stack: [8, 0, 0, 0] (a + b = 8) +``` + +**Comparison (`lt`, `gte`):** + +```masm +use.std::math::u128 + +# a = 10 +push.0.0.0.10 +# b = 20 +push.0.0.0.20 + +exec.u128::lt +# Stack: [1] (10 < 20 is true) +``` + +**Bitwise operations (`and`, `xor`):** + +```masm +use.std::math::u128 + +push.0.0.0.0xFF +push.0.0.0.0x0F + +exec.u128::and +# Stack: [0x0F, 0, 0, 0] +``` + +**Shift left:** + +```masm +use.std::math::u128 + +push.0.0.0.1 +push.4 # shift amount + +exec.u128::shl +# Stack: [16, 0, 0, 0] (1 << 4 = 16) +``` + +**Division and divmod:** + +```masm +use.std::math::u128 + +# a = 100 +push.0.0.0.100 +# b = 7 +push.0.0.0.7 + +exec.u128::divmod +# Stack: [quotient (4 limbs), remainder (4 limbs)] +``` + +### Available Procedures + +The full list of procedures in `std::math::u128`: + +| Category | Procedures | +|----------|-----------| +| **Arithmetic** | `overflowing_add`, `widening_add`, `wrapping_add`, `overflowing_sub`, `wrapping_sub`, `overflowing_mul`, `widening_mul`, `wrapping_mul`, `div`, `mod`, `divmod` | +| **Comparison** | `eq`, `neq`, `eqz`, `lt`, `gt`, `lte`, `gte`, `min`, `max` | +| **Bitwise** | `and`, `or`, `xor`, `not` | +| **Bit counting** | `clz`, `ctz`, `clo`, `cto` | +| **Shifts / Rotates** | `shl`, `shr`, `rotl`, `rotr` | + +--- + +## `u32overflowing_mul` → `u32widening_mul` + +### Summary + +Three rename pairs that better reflect that the result is *wider* than the operands (not arithmetic overflow): + +```masm +# Before (0.13) +u32overflowing_mul +u32overflowing_madd +exec.::std::math::u64::overflowing_mul + +# After (0.14) +u32widening_mul +u32widening_madd +exec.::std::math::u64::widening_mul +``` + +`u32overflowing_add` is unchanged (the rename only applies to multiplication and madd). v0.14 also adds matching `u32widening_add` and `std::math::u64::widening_add` helpers. + +--- + +## `breakpoint` Instruction Removed + +The `breakpoint` MASM instruction was a no-op used with the old `miden debug` REPL. Both the REPL and the instruction are gone — any `.masm` source containing `breakpoint` will now fail to assemble. Delete those lines. + +```masm +# Before (0.13) +breakpoint # used for debugging + +# After (0.14) +# Just remove the line — breakpoint no longer exists +``` + +--- + +## Migration Steps + +1. Replace any hand-rolled string-to-felt mapping for event ids with `event("...")` — the assembler now owns this derivation. +2. Where a storage-slot name or event id is shared across components, prefer `word("...")` / `event("...")` over distributing a pre-computed `Word` / `Felt` constant. +3. If you have custom 128-bit arithmetic helpers in MASM, consider replacing them with the new `std::math::u128` module. +4. Remember that `u128` values follow the same little-endian convention as the rest of v0.14 — the low limb (`a0`) is on top of the stack. +5. Rename `u32overflowing_mul` → `u32widening_mul` and `u32overflowing_madd` → `u32widening_madd`. +6. Rename `std::math::u64::overflowing_mul` → `std::math::u64::widening_mul`. +7. Remove any `breakpoint` instructions from your MASM source. + +--- + +## Common Errors + +| Error Message | Cause | Solution | +| --- | --- | --- | +| `unknown module std::math::u128` | Using an older assembler version | Update to `miden-assembly` 0.22. | +| Incorrect `u128` results | Limbs pushed in big-endian order | Push low limb last so it lands on top: `push.a3.a2.a1.a0`. | +| `unknown instruction u32overflowing_mul` | Instruction renamed | Use `u32widening_mul`. | +| `unknown instruction breakpoint` | Instruction removed | Delete the line. | diff --git a/versioned_docs/version-0.14/builder/migration/09-vm-assembler.md b/versioned_docs/version-0.14/builder/migration/09-vm-assembler.md new file mode 100644 index 00000000..c8abd415 --- /dev/null +++ b/versioned_docs/version-0.14/builder/migration/09-vm-assembler.md @@ -0,0 +1,265 @@ +--- +sidebar_position: 9 +title: "VM & Assembler Changes" +description: "Host trait consolidation, FastProcessor builder API, and other VM-level breaking changes in v0.14" +--- + +# VM & Assembler Changes + +:::warning Breaking Change +The VM host interface has been collapsed from three traits to one, the processor construction API has changed to a builder pattern, and several types have been cleaned up or relocated. These changes affect anyone embedding the Miden VM directly. +::: + +--- + +## `SyncHost` / `BaseHost` Removed; `AsyncHost` → `Host` + +### Summary + +The previous three-trait hierarchy (`BaseHost`, `SyncHost`, `AsyncHost`) has been collapsed into a single `Host` trait. All methods now return `impl FutureMaybeSend<...>`, making the trait async-compatible by default. Additionally, `ProcessState` was renamed from `ProcessorState`, and the `on_assert_failed` callback was removed. + +### Affected Code + +```rust +// Before (0.13) +impl BaseHost for MyHost {} + +impl SyncHost for MyHost { + fn get_mast_forest( + &self, + node_digest: &Digest, + ) -> Option> { + // ... + } + + fn on_event( + &mut self, + process: &ProcessState, + event_id: u32, + ) -> Result<(), ExecutionError> { + // ... + } +} +``` + +```rust +// After (0.14) +impl Host for MyHost { + fn get_mast_forest( + &self, + node_digest: &Digest, + ) -> impl FutureMaybeSend>> { + async { + // ... + } + } + + fn on_event( + &mut self, + process: &ProcessorState, + event_id: u32, + ) -> impl FutureMaybeSend> { + async { + // ... + } + } +} +``` + +### Migration Steps + +1. Remove `impl BaseHost for ...` and `impl SyncHost for ...` blocks. +2. Implement the single `Host` trait instead. +3. Wrap each method body in `async { ... }` and return `impl FutureMaybeSend<...>`. +4. Rename any `ProcessState` references to `ProcessorState`. +5. Remove any `on_assert_failed` implementations — the callback no longer exists. + +--- + +## `FastProcessor` Builder API + +### Summary + +`FastProcessor::new_with_advice_inputs()` and `FastProcessor::new_debug()` have been removed. Construction now follows a builder pattern starting from `FastProcessor::new(stack_inputs)`. + +### Affected Code + +```rust +// Before (0.13) +let processor = FastProcessor::new_with_advice_inputs(stack_inputs, advice_inputs); +let processor = FastProcessor::new_debug(stack_inputs); +``` + +```rust +// After (0.14) +let processor = FastProcessor::new(stack_inputs) + .with_advice(advice_inputs) + .with_debugging(true) + .with_tracing(true); + +// Or with execution options: +let processor = FastProcessor::new(stack_inputs) + .with_advice(advice_inputs) + .with_options(execution_options); +``` + +### Migration Steps + +1. Replace `new_with_advice_inputs(stack, advice)` with `new(stack).with_advice(advice)`. +2. Replace `new_debug(stack)` with `new(stack).with_debugging(true)`. +3. Chain `.with_options(...)` or `.with_tracing(true)` as needed. + +--- + +## `StackInputs` / `StackOutputs` API Cleanup + +### Summary + +Both `StackInputs` and `StackOutputs` now derive `Copy`, so you can drop explicit `.clone()` calls. `StackInputs::new()` now takes a `&[Felt]` slice instead of a `Vec`. + +### Affected Code + +```rust +// Before (0.13) +let inputs = StackInputs::new(vec![felt_a, felt_b, felt_c]); +let inputs_copy = inputs.clone(); + +// After (0.14) +let inputs = StackInputs::new(&[felt_a, felt_b, felt_c]); +let inputs_copy = inputs; // Copy, no clone needed +``` + +### Migration Steps + +1. Replace `StackInputs::new(vec![...])` with `StackInputs::new(&[...])`. +2. Remove unnecessary `.clone()` calls on `StackInputs` and `StackOutputs`. + +--- + +## `MastForest::strip_decorators` → `clear_debug_info` + +### Summary + +`MastForest::strip_decorators()` has been renamed to `MastForest::clear_debug_info()` and now wipes the entire `DebugInfo` structure, not just decorators. A new convenience method `MastForest::write_stripped()` writes the forest with debug info removed without mutating the original. + +The MAST serialization format version was also bumped — `.masl` and `.masp` files produced by v0.13 will **not** deserialize under v0.14. + +### Affected Code + +```rust +// Before (0.13) +forest.strip_decorators(); + +// After (0.14) +forest.clear_debug_info(); + +// Or write a stripped copy without mutating: +forest.write_stripped(&mut output)?; +``` + +### Migration Steps + +1. Replace `strip_decorators()` calls with `clear_debug_info()`. +2. Consider using `write_stripped()` if you only need stripped output without modifying the forest in memory. +3. Re-assemble all `.masl` and `.masp` files from source — 0.13 serialized files will fail to deserialize. + +### Common Errors + +| Error Message | Cause | Solution | +| --- | --- | --- | +| `no method named strip_decorators found` | Method renamed | Use `clear_debug_info()`. | +| `MastForest deserialization failed: unexpected version` | MAST format version bumped | Re-assemble from source under 0.22. | + +--- + +## `Process`, `VmStateIterator`, `execute_iter()` Removed + +### Summary + +The "slow" `Process` type, `VmStateIterator`, `VmState`, `AsmOpInfo`, `SlowProcessState`, and the top-level `miden_processor::execute_iter()` function have been removed. Execution goes exclusively through `FastProcessor`. + +### Affected Code + +```rust +// Before (0.13) +for state in execute_iter(&program, stack_inputs, advice_inputs, &mut host) { + let state: VmState = state?; + println!("clk={} op={:?}", state.clk(), state.op()); +} + +// After (0.14) +let mut processor = FastProcessor::new(stack_inputs).with_advice(advice_inputs); +let mut ctx = processor.get_initial_resume_context(&program)?; +loop { + let outcome = processor.step(&program, &mut ctx, &mut host)?; + if outcome.is_done() { break; } +} +``` + +For one-shot non-iterating execution, use `FastProcessor::execute_sync(...)` or the top-level `miden_processor::execute_sync(...)`. + +--- + +## `miden debug` / `analyze` / `repl` Removed + +The three CLI subcommands were removed along with `VmStateIterator`. The CLI now exposes only `compile`, `bundle`, `run`, `prove`, `verify`. There is no drop-in replacement; use the external debugger or write a small Rust program that drives `FastProcessor::step` if you need step-by-step inspection. + +--- + +## `Operation` Enum Trimmed to Basic-Block Ops + +### Summary + +Control-flow opcodes (`Join`, `Split`, `Loop`, `Call`, `Dyn`, `Dyncall`, `SysCall`, `Span`, `End`, `Respan`, `Halt`) were removed from `miden_core::Operation`; they live exclusively at the MAST node level (`MastNode::Join`, etc.). Pattern matches over those variants must move to traversals over `MastNode`. + +--- + +## `ExecutionOptions`, `ProvingOptions`, `ExecutionProof` Relocated + +These types have moved crates. See [Imports & Dependencies](./imports-dependencies) for the updated import paths. + +--- + +## Project File Format + +### Summary + +v0.14 introduces a **first-class Miden project file format** (`miden-project.toml`), implemented in the new `miden-project` crate. Projects describe a single package or a workspace of packages with dependencies (path, git, registry), profiles, lints, and per-package metadata — much like `Cargo.toml`. The assembler compiles a project directly to a `.masp` package via `Assembler::link_package`, with dependency resolution through `pubgrub`. + +This is **additive** for existing per-file assembly users — old `Assembler::compile_and_statically_link` / `assemble_library` APIs still work — but it is the recommended layout for any new MASM project of more than a single file. + +### Standalone Package Example + +```toml title="my-app/miden-project.toml" +[package] +name = "example" +version = "0.1.0" + +[lib] +path = "lib/mod.masm" + +[dependencies] +miden-protocol = { path = "../protocol/userspace" } + +[profile.test] +inherits = "dev" +network = "local" + +[lints.miden] +unused = "error" +``` + +### Building from Rust + +```rust +use miden_assembly::Assembler; + +let package = Assembler::default() + .link_package("./my-app/miden-project.toml")?; +``` + +--- + +:::tip +For the full VM changelog, see the [miden-vm releases](https://github.com/0xMiden/miden-vm/releases). +::: diff --git a/versioned_docs/version-0.14/builder/migration/index.md b/versioned_docs/version-0.14/builder/migration/index.md new file mode 100644 index 00000000..c23fb37c --- /dev/null +++ b/versioned_docs/version-0.14/builder/migration/index.md @@ -0,0 +1,151 @@ +--- +title: "v0.14 Migration Guide" +description: "Complete guide for upgrading from Miden v0.13 to v0.14" +--- + +# Miden Testnet 0.14.0 + +This guide covers all breaking changes you need to migrate an application to Miden 0.14.0. Like the 0.13 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 +- interacting with storage, auth, or RPCs + +this document is for you. + +--- + +## Quick Upgrade + +Try upgrading first — most projects can start with a dependency update: + +```toml title="Cargo.toml" +# Replace these +miden-protocol = "0.13" +miden-standards = "0.13" +miden-assembly = "0.20" +miden-core = "0.20" +miden-processor = "0.20" +miden-prover = "0.20" +miden-crypto = "0.19" + +# With these +miden-protocol = "0.14" +miden-standards = "0.14" +miden-assembly = "0.22" +miden-core = "0.22" +miden-processor = "0.22" +miden-prover = "0.22" +miden-crypto = "0.23" +``` + +Then run: + +```bash +cargo update && cargo build +``` + +If you encounter errors, continue reading for detailed migration steps. + +--- + +:::info Who should read this? +This guide is for: +- **Rust client developers** migrating from v0.13 → v0.14 +- **Web SDK developers** using the JavaScript/TypeScript SDK +- **Smart contract authors** writing MASM or using protocol APIs +- **App developers** using `miden-protocol`, `miden-standards`, or client crates + +If you're starting fresh on v0.14, you can skip this guide and go directly to the [Get Started guide](../get-started). +::: + +--- + +## At a Glance + +Big themes in 0.14: + +| Change | Summary | +|--------|---------| +| **Poseidon2 hashing** | The native hash function changed from RPO to Poseidon2. Every MAST root, commitment, and persisted digest changes. Rebuild all artifacts. | +| **Little-endian stack** | The operand stack is now little-endian. `u64`, `u256`, `hperm`, `hmerge`, `mem_stream`, `adv_pipe` all have the low limb on top. | +| **Two-word assets** | `ASSET` is now `ASSET_KEY` + `ASSET_VALUE`. Every procedure touching assets has a new stack signature. | +| **NoteStorage** | `NoteInputs` renamed to `NoteStorage` end-to-end (Rust, MASM, errors). | +| **MASM library scripts** | Note and auth scripts are now MASM libraries with `@note_script` / `@auth_script` attributes. | +| **AuthSingleSig** | Six per-scheme auth components consolidated into `AuthSingleSig`, `AuthSingleSigAcl`, `AuthMultisig`. | +| **Web MidenClient** | The flat `WebClient` replaced by resource-based `MidenClient` API. | +| **Ownable2Step** | New built-in two-step ownership transfer component for faucets. | +| **128-bit math** | New `std::math::u128` module in the standard library. | + +--- + +## New Features in v0.14 + +These are additive features you may want to adopt (not breaking changes): + +- **NoteScreener and batch screening**: A new `Client::note_screener()` API adds batch note consumability checks for more efficient note screening workflows. +- **Account history pruning**: The client now exposes account history pruning APIs for storage and state management. +- **GrpcClient automatic retry on rate limiting**: `GrpcClient` now automatically retries rate-limited requests up to five times and honors `retry-after` when provided. +- **Automatic NTX note script registration**: NTX note scripts are now registered automatically — no more manual script registration as part of transaction submission. +- **Typed RPC error parsing**: RPC errors are now parsed into typed client errors, improving programmatic error handling. +- **128-bit integer math**: New `std::math::u128` module provides full unsigned integer arithmetic, comparison, bitwise, and shift/rotate operations. + +--- + +## Compatibility + +| Component | Required | Tested With | +|-----------|----------|-------------| +| Miden VM crates | 0.22+ | 0.22.0 | +| miden-crypto | 0.23+ | 0.23.0 | +| miden-protocol | 0.14+ | 0.14.3 | +| miden-standards | 0.14+ | 0.14.3 | +| Rust | 1.91+ | 1.91.0 | + +--- + +## Migration Sections + +Work through these sections in order for a complete migration: + +| Section | Topics | +|---------|--------| +| [1. Imports & Dependencies](./imports-dependencies) | Cargo.toml bumps, import relocations, MSRV, `Felt` rename | +| [2. Hashing & Stack Changes](./hashing-stack) | Poseidon2, little-endian stack, Falcon module rename | +| [3. Account Changes](./account-changes) | Components, AuthSingleSig, `@auth_script`, Ownable2Step | +| [4. Note Changes](./note-changes) | NoteStorage, `@note_script`, OutputNote variants, StandardNote | +| [5. Assets, Vault & Faucet](./asset-vault-faucet) | Two-word assets, create_* rename, get_asset, vault changes | +| [6. Transaction Changes](./transaction-changes) | TransactionId, ProvenTransaction, events, SignedBlock | +| [7. Client Changes](./client-changes) | Web MidenClient, Rust Keystore, AccountReader, StateSync | +| [8. MASM Changes](./masm-changes) | 128-bit math, event namespace | +| [9. VM & Assembler Changes](./vm-assembler) | Host trait, FastProcessor, type relocations | + +--- + +## Final Checklist + +Complete these steps to verify your migration: + +- [ ] Bump all Miden crate versions in `Cargo.toml` per section 1 +- [ ] Update `rust-toolchain.toml` to Rust 1.91+ (if using miden-client) +- [ ] Re-assemble all `.masl` and `.masp` files from source (Poseidon2 hash change) +- [ ] Discard any cached commitments, transaction IDs, or proofs from v0.13 +- [ ] Audit MASM stack arithmetic for little-endian limb order +- [ ] Update asset handling to two-word `ASSET_KEY` + `ASSET_VALUE` format +- [ ] Rename `NoteInputs` → `NoteStorage` everywhere +- [ ] Add `@note_script` / `@auth_script` attributes to MASM entrypoints +- [ ] Replace per-scheme auth components with `AuthSingleSig` +- [ ] Update `AccountComponent::new` to pass `AccountComponentMetadata` +- [ ] Replace `commitment()` calls with `to_commitment()` +- [ ] Update `NoteMetadata::new` to use `with_tag()` builder +- [ ] Replace `WellKnownNote` / `WellKnownComponent` with `Standard…` equivalents +- [ ] Migrate note constructors to associated methods (`P2idNote::create`, etc.) +- [ ] Update Web SDK from `WebClient` to `MidenClient` resource API +- [ ] Replace `TransactionAuthenticator` with `Keystore` trait in Rust client +- [ ] 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.14. +::: diff --git a/versioned_docs/version-0.14/builder/private-multisig/_category_.json b/versioned_docs/version-0.14/builder/private-multisig/_category_.json new file mode 100644 index 00000000..e1a43e30 --- /dev/null +++ b/versioned_docs/version-0.14/builder/private-multisig/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Private Multisig", + "position": 2 +} diff --git a/versioned_docs/version-0.14/builder/private-multisig/core-concepts.md b/versioned_docs/version-0.14/builder/private-multisig/core-concepts.md new file mode 100644 index 00000000..47bcc853 --- /dev/null +++ b/versioned_docs/version-0.14/builder/private-multisig/core-concepts.md @@ -0,0 +1,121 @@ +--- +title: Core Concepts +sidebar_position: 1 +--- + +# Core Concepts + +## Transaction lifecycle + +Every multisig transaction follows the same lifecycle: propose, sign, execute, sync. + +```mermaid +sequenceDiagram + participant P as Proposer + participant Guardian as Guardian Server + participant C1 as Cosigner 1 + participant C2 as Cosigner 2 + + P->>P: Build TransactionSummary locally + P->>Guardian: push_delta_proposal
(tx_summary + proposer signature) + Guardian->>Guardian: Validate against current state + + C1->>Guardian: get_delta_proposals + Guardian-->>C1: Pending proposals + C1->>C1: Verify locally + C1->>Guardian: sign_delta_proposal + + C2->>Guardian: get_delta_proposals + Guardian-->>C2: Pending proposals + C2->>C2: Verify locally + C2->>Guardian: sign_delta_proposal + + Note over Guardian: Threshold met + + P->>Guardian: push_delta (with all signatures) + Guardian-->>P: Canonical delta with ack_sig + P->>P: Execute on-chain with ZK proof +``` + +1. **Propose**: The proposer builds a `TransactionSummary` locally, signs it, and pushes it to Guardian as a delta proposal. +2. **Sign**: Each cosigner fetches pending proposals, verifies the transaction details against their local state, and submits their signature. +3. **Execute**: Once the threshold is met, any participant pushes the final delta (with all signatures). Guardian returns the acknowledged delta. The executor builds and submits the on-chain transaction. +4. **Sync**: All participants fetch the latest state from Guardian to stay synchronized. + +## Key architecture: 2-of-3 setup + +A common configuration uses a 2-of-3 threshold: + +| Key | Holder | Purpose | +|---|---|---| +| **Key 1** | User hot key | Daily transactions | +| **Key 2** | User cold key | Recovery and emergency override | +| **Key 3** | Guardian service key | Co-signing and policy enforcement | + +```mermaid +graph TD + subgraph "Normal operation (Hot + Guardian)" + Hot["User Hot Key"] --> TX["Transaction"] + GuardianKey["Guardian Service Key"] --> TX + end + + subgraph "Emergency override (Hot + Cold)" + Hot2["User Hot Key"] --> Override["Rotate Guardian / Adjust policies
Switch providers"] + Cold["User Cold Key"] --> Override + end +``` + +- **Normal operations**: Hot key + Guardian's co-signature are sufficient. +- **Emergency override**: Hot + cold keys alone can rotate out Guardian or switch providers. +- **Guardian alone cannot move funds**: It holds only one key in the threshold. + +## Transaction types + +| Type | Description | +|---|---| +| **Transfer (P2ID)** | Send assets to another account | +| **Consume notes** | Spend incoming notes | +| **Add signer** | Add a new cosigner to the multisig account | +| **Remove signer** | Remove a cosigner | +| **Change threshold** | Update the required signature count | +| **Switch Guardian** | Change the Guardian provider endpoint | + +## Signer types + +The TypeScript SDK supports multiple signer backends: + +| Signer | Scheme | Use case | +|---|---|---| +| `FalconSigner` | Falcon | Local Falcon key (default) | +| `EcdsaSigner` | ECDSA | Local ECDSA key | +| `ParaSigner` | ECDSA | External EVM wallets via [Para](https://getpara.com) SDK | +| `MidenWalletSigner` | Any | [Miden Wallet](https://github.com/demox-labs/miden-wallet) browser extension | + +## Offline fallback + +If Guardian is unreachable, the SDKs support fully offline workflows: + +```mermaid +sequenceDiagram + participant A as Signer A + participant File as JSON File
(side channel) + participant B as Signer B + + A->>A: Create proposal offline + A->>File: Export proposal.json + + File-->>B: Share via email, chat, etc. + B->>B: Sign proposal locally + B->>File: Export signed proposal + + File-->>A: Return signed file + A->>A: Collect enough signatures + A->>A: Execute transaction on-chain +``` + +1. Create a proposal locally and export it as JSON. +2. Share the file with cosigners through any side channel. +3. Each cosigner signs offline and returns the signed file. +4. Once the threshold is met, execute the transaction on-chain. + +This ensures multisig operations remain functional even without Guardian connectivity. diff --git a/versioned_docs/version-0.14/builder/private-multisig/index.md b/versioned_docs/version-0.14/builder/private-multisig/index.md new file mode 100644 index 00000000..57e76ecb --- /dev/null +++ b/versioned_docs/version-0.14/builder/private-multisig/index.md @@ -0,0 +1,76 @@ +--- +title: Private Multisig +sidebar_position: 0 +--- + +# Private Multisig + +Private multisig on Miden allows multiple parties to collectively control an account, requiring a configurable threshold of signatures (N-of-M) to execute transactions — all while keeping account state private. + +## The problem + +On public chains, multisig coordination is straightforward: every signer can read the same on-chain state and build their next action on top of it. Safe-style multisigs work because the ledger is transparent. + +In Miden's private account model, account state lives client-side. The chain stores only cryptographic commitments. This means: + +- Signers can't independently observe the latest state from the chain. +- Proposals and signatures need an off-chain coordination surface. +- Without a shared state view, participants risk divergent state or stale approvals. + +The [Miden Guardian](../miden-guardian/) solves this by acting as the coordination server for multisig accounts — keeping signers synchronized, managing proposal workflows, and ensuring all parties work from the same canonical state. + +## How it works + +Miden multisigs can be fully private (code, signers, metadata, etc. are not visible). Guardian coordinates the workflow: + +1. **Propose**: A signer pushes a delta proposal (containing a `TransactionSummary`) to Guardian. Guardian validates the proposal against the current account state and the Miden network. +2. **Sign**: Other authorized cosigners fetch the pending proposal from Guardian, verify the transaction details locally, and submit their signatures. +3. **Ready**: Once enough signatures are collected (meeting the threshold), Guardian emits an acknowledgment. +4. **Execute**: Any cosigner builds the final transaction using all signatures plus the Guardian acknowledgment, and submits it on-chain. +5. **Sync**: All participants fetch the latest canonical state from Guardian. + +```mermaid +sequenceDiagram + participant A as Signer A (proposer) + participant Guardian as Guardian Server + participant B as Signer B (cosigner) + participant Chain as Miden Network + + A->>A: Build transaction locally + A->>Guardian: Push delta proposal
(tx_summary + signature) + Guardian->>Guardian: Validate against state & network + + B->>Guardian: Fetch pending proposals + Guardian-->>B: Return proposal details + B->>B: Verify transaction locally + B->>Guardian: Sign proposal + + Note over Guardian: Threshold met (2-of-3) + + A->>Guardian: Push delta
(with all signatures + Guardian ack) + Guardian-->>A: Acknowledged delta + A->>Chain: Submit ZK proof + Chain-->>Guardian: Commitment confirmed + Guardian->>Guardian: Mark canonical +``` + +## Learn more + + + + Transaction lifecycle, key architecture, and offline fallback. + + + Rust SDK for building multisig workflows. + + + TypeScript SDK for building multisig workflows. + + + +## Repositories + +| Repository | Description | +|---|---| +| [Miden Guardian](https://github.com/OpenZeppelin/guardian) | Guardian server, client SDKs, and multisig client libraries | +| [MultiSig](https://github.com/OpenZeppelin/MultiSig) | MultiSig reference application (Next.js frontend + coordinator) | diff --git a/versioned_docs/version-0.14/builder/private-multisig/rust-sdk.md b/versioned_docs/version-0.14/builder/private-multisig/rust-sdk.md new file mode 100644 index 00000000..e4a64e21 --- /dev/null +++ b/versioned_docs/version-0.14/builder/private-multisig/rust-sdk.md @@ -0,0 +1,113 @@ +--- +title: Rust SDK +sidebar_position: 2 +--- + +# Rust Multisig SDK + +The `miden-multisig-client` crate provides a high-level Rust SDK for private multisig workflows on Miden. It wraps the on-chain multisig contracts and Guardian coordination into a single API. + +**Source**: [`crates/miden-multisig-client`](https://github.com/OpenZeppelin/guardian/tree/main/crates/miden-multisig-client) + +## Installation + +```toml +[dependencies] +miden-multisig-client = { git = "https://github.com/OpenZeppelin/guardian", package = "miden-multisig-client" } +``` + +## Setup + +```rust +use miden_client::rpc::Endpoint; +use miden_multisig_client::MultisigClient; +use miden_objects::Word; + +let signer1: Word = /* RPO Falcon commitment */ Word::default(); +let signer2: Word = Word::default(); + +let mut client = MultisigClient::builder() + .miden_endpoint(Endpoint::new("http://localhost:57291")) + .psm_endpoint("http://localhost:50051") + .account_dir("/tmp/multisig") + .generate_key() + .build() + .await?; +``` + +## Creating a multisig account + +```rust +// Create a 2-of-2 multisig account and register it on Guardian +let account = client.create_account(2, vec![signer1, signer2]).await?; +``` + +## Propose, sign, execute + +```rust +use miden_multisig_client::TransactionType; +use miden_objects::account::AccountId; + +let recipient = AccountId::from_hex("0x...")?; +let faucet = AccountId::from_hex("0x...")?; +let tx = TransactionType::transfer(recipient, faucet, 1_000); + +// Proposer creates the transaction proposal on Guardian +let proposal = client.propose_transaction(tx).await?; + +// Cosigner lists and signs the proposal +let proposals = client.list_proposals().await?; +let to_sign = proposals.iter().find(|p| p.id == proposal.id).unwrap(); +client.sign_proposal(&to_sign.id).await?; + +// Once threshold is met, execute +client.execute_proposal(&proposal.id).await?; +``` + +## Offline fallback + +If Guardian is unavailable, the SDK automatically produces an offline proposal: + +```rust +use miden_multisig_client::{ProposalResult, TransactionType}; + +let tx = TransactionType::consume_notes(vec![note_id]); +match client.propose_with_fallback(tx).await? { + ProposalResult::Online(p) => { + println!("Proposal {} is live on Guardian", p.id); + } + ProposalResult::Offline(exported) => { + std::fs::write("proposal.json", exported.to_json()?)?; + println!("Share proposal.json with cosigners for offline signing"); + } +} +``` + +### Offline signing and execution + +```rust +// Cosigner signs an imported proposal +client.sign_imported_proposal(&mut exported)?; +std::fs::write("proposal_signed.json", exported.to_json()?)?; + +// Once enough signatures are collected +client.execute_imported_proposal(&exported).await?; +``` + +## Listing consumable notes + +```rust +use miden_multisig_client::NoteFilter; + +// All consumable notes +let notes = client.list_consumable_notes().await?; + +// Filter by faucet and minimum amount +let faucet = AccountId::from_hex("0x...")?; +let filter = NoteFilter::by_faucet_min_amount(faucet, 5_000); +let spendable = client.list_consumable_notes_filtered(filter).await?; +``` + +## Full reference + +See the [`crates/miden-multisig-client/README.md`](https://github.com/OpenZeppelin/guardian/tree/main/crates/miden-multisig-client) for the complete API reference. diff --git a/versioned_docs/version-0.14/builder/private-multisig/typescript-sdk.md b/versioned_docs/version-0.14/builder/private-multisig/typescript-sdk.md new file mode 100644 index 00000000..b951da70 --- /dev/null +++ b/versioned_docs/version-0.14/builder/private-multisig/typescript-sdk.md @@ -0,0 +1,146 @@ +--- +title: TypeScript SDK +sidebar_position: 3 +--- + +# TypeScript Multisig SDK + +The `@openzeppelin/miden-multisig-client` package provides a high-level TypeScript SDK for private multisig workflows on Miden. It supports multiple signer types including external wallets. + +**Package**: [`@openzeppelin/miden-multisig-client`](https://www.npmjs.com/package/@openzeppelin/miden-multisig-client) +**Source**: [`packages/miden-multisig-client`](https://github.com/OpenZeppelin/guardian/tree/main/packages/miden-multisig-client) + +## Installation + +```bash +npm install @openzeppelin/miden-multisig-client @miden-sdk/miden-sdk +``` + +## Setup + +```typescript +import { MultisigClient, FalconSigner } from '@openzeppelin/miden-multisig-client'; +import { MidenClient, AuthSecretKey } from '@miden-sdk/miden-sdk'; + +// Initialize the Miden SDK client +const midenClient = await MidenClient.createTestnet(); + +// Create a Falcon signer +const secretKey = AuthSecretKey.rpoFalconWithRNG(undefined); +const signer = new FalconSigner(secretKey); + +// Create the multisig client and fetch Guardian info +const client = new MultisigClient(midenClient, { + psmEndpoint: 'http://localhost:3000', +}); +const { psmCommitment } = await client.initialize(); +``` + +:::note +The multisig client accepts any `MidenClient` instance, so `createTestnet()` / `createDevnet()` / `create({ rpcUrl })` all work. See the [Web SDK setup guide](../tools/clients/web-client/setup.md) for the full factory list. +::: + +## Creating a multisig account + +```typescript +const config = { + threshold: 2, + signerCommitments: [signer.commitment, otherSignerCommitment], + psmCommitment, + signatureScheme: 'falcon', +}; + +const multisig = await client.create(config, signer); +console.log('Account ID:', multisig.accountId); + +// Register on Guardian +await multisig.registerOnPsm(); +``` + +## Loading an existing account + +```typescript +// Configuration is auto-detected from on-chain storage +const multisig = await client.load(accountId, signer); +``` + +## Syncing state + +Fetch proposals, state, notes, and config in a single call: + +```typescript +const { proposals, state, notes, config } = await multisig.syncAll(); +``` + +## Creating and executing proposals + +```typescript +// Send payment +const { proposal } = await multisig.createSendProposal(recipientId, faucetId, amount); + +// Sign a proposal +await multisig.signTransactionProposal(proposal.commitment); + +// Execute when ready +if (proposal.status.type === 'ready') { + await multisig.executeTransactionProposal(proposal.commitment); +} +``` + +Other proposal types: + +```typescript +await multisig.createConsumeNotesProposal(noteIds); +await multisig.createAddSignerProposal(newCommitment, { newThreshold: 3 }); +await multisig.createRemoveSignerProposal(signerToRemove); +await multisig.createChangeThresholdProposal(3); +await multisig.createSwitchPsmProposal(newEndpoint, newPubkey); +``` + +## External wallet integration + +For browser wallets where the signing key is external: + +```typescript +// Sign the commitment with an external wallet +const signature = await wallet.sign(proposal.commitment); + +// Submit the external signature +await multisig.signTransactionProposalExternal({ + commitment: proposal.commitment, + signature, + publicKey: wallet.publicKey, + scheme: 'ecdsa', +}); +``` + +### Wallet signers + +```typescript +import { ParaSigner, MidenWalletSigner } from '@openzeppelin/miden-multisig-client'; + +// Para wallet integration +const signer = new ParaSigner(paraContext, walletId, commitment, publicKey); + +// Miden wallet integration +const signer = new MidenWalletSigner(walletContext, commitment, 'ecdsa'); +``` + +## Offline workflows + +Export and import proposals for side-channel signing: + +```typescript +// Export a proposal as JSON +const json = multisig.exportTransactionProposalToJson(proposal.commitment); + +// Sign offline +const signedJson = multisig.signTransactionProposalOffline(proposal.commitment); + +// Import on another device +const { proposal: imported } = multisig.importTransactionProposal(json); +``` + +## Full reference + +See the [`packages/miden-multisig-client/README.md`](https://github.com/OpenZeppelin/guardian/tree/main/packages/miden-multisig-client) for the complete API reference. diff --git a/versioned_docs/version-0.14/builder/smart-contracts/_category_.json b/versioned_docs/version-0.14/builder/smart-contracts/_category_.json new file mode 100644 index 00000000..e5cd9472 --- /dev/null +++ b/versioned_docs/version-0.14/builder/smart-contracts/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Smart Contracts", + "position": 3 +} diff --git a/versioned_docs/version-0.14/builder/smart-contracts/accounts/_category_.json b/versioned_docs/version-0.14/builder/smart-contracts/accounts/_category_.json new file mode 100644 index 00000000..65048510 --- /dev/null +++ b/versioned_docs/version-0.14/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.14/builder/smart-contracts/accounts/account-operations.md b/versioned_docs/version-0.14/builder/smart-contracts/accounts/account-operations.md new file mode 100644 index 00000000..b0e3a38d --- /dev/null +++ b/versioned_docs/version-0.14/builder/smart-contracts/accounts/account-operations.md @@ -0,0 +1,129 @@ +--- +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 { + pub fn check_state(&self) { + // Account identity + let id: AccountId = self.get_id(); + let nonce: Felt = self.get_nonce(); + + // Vault queries + let balance: Felt = self.get_balance(faucet_id); + let initial: Felt = self.get_initial_balance(faucet_id); + let has_nft: bool = self.has_non_fungible_asset(asset); + let root: Word = self.get_vault_root(); + let initial_root: Word = self.get_initial_vault_root(); + + // Commitment queries + let commitment: Word = self.compute_commitment(); + let initial_commit: Word = self.get_initial_commitment(); + let storage: Word = self.compute_storage_commitment(); + let initial_storage: Word = self.get_initial_storage_commitment(); + let code: Word = self.get_code_commitment(); + + // Procedure queries + let count: Felt = 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 { + pub fn receive_asset(&mut self, asset: Asset) { + // Add an asset to the vault — returns the asset as stored + let stored: Asset = self.add_asset(asset); + } + + pub fn send_asset(&mut self, asset: Asset, note_idx: NoteIdx) { + // Remove an asset from the vault — returns the removed asset + // Proof generation fails if the asset doesn't exist or insufficient balance + let removed: Asset = self.remove_asset(asset); + output_note::add_asset(removed, note_idx); + } + + pub fn auth(&mut self) { + // Increment the account nonce (replay protection) + let new_nonce: Felt = self.incr_nonce(); + + // Compute commitment of all state changes in this transaction + let delta: Word = self.compute_delta_commitment(); + + // Check if a specific procedure was called during this transaction + let called: bool = self.was_procedure_called(proc_root); + } +} +``` + +:::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_balance(faucet_id)` | Referenced asset is non-fungible | +| `get_procedure_root(index)` | Index out of bounds | +| Any `assert!()` | Condition is false | + +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 + +## Example: ManagedWallet + +```rust +#![no_std] +#![feature(alloc_error_handler)] + +use miden::{component, output_note, Asset, AccountId, NoteIdx, Felt}; + +#[component] +struct ManagedWallet; + +#[component] +impl ManagedWallet { + /// Receive an asset into the vault. + pub fn receive_asset(&mut self, asset: Asset) { + self.add_asset(asset); + } + + /// Send an asset to an output note, with balance check. + pub fn send_asset(&mut self, asset: Asset, note_idx: NoteIdx) { + let removed = self.remove_asset(asset); + output_note::add_asset(removed, note_idx); + } + + /// Query the balance of a fungible asset. + pub fn balance_of(&self, faucet_id: AccountId) -> Felt { + self.get_balance(faucet_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.14/builder/smart-contracts/accounts/authentication.md b/versioned_docs/version-0.14/builder/smart-contracts/accounts/authentication.md new file mode 100644 index 00000000..07b112c6 --- /dev/null +++ b/versioned_docs/version-0.14/builder/smart-contracts/accounts/authentication.md @@ -0,0 +1,100 @@ +--- +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 on-chain 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). + +v0.14 unifies the previous per-scheme components (`AuthFalcon512Rpo`, `AuthEcdsaK256Keccak`, …) into a single scheme-agnostic [`AuthSingleSig`](https://docs.rs/miden-standards/latest/miden_standards/account/auth/struct.AuthSingleSig.html) component that takes an `AuthScheme` enum (`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 delegates to `miden::standards::auth::signature::authenticate_transaction`, which: + +1. Increments the account nonce (even if the account state did not change — this is required for replay protection). +2. Computes the transaction summary message: `hash([ACCOUNT_DELTA_COMMITMENT, INPUT_NOTES_COMMITMENT, OUTPUT_NOTES_COMMITMENT, [0, 0, ref_block_num, final_nonce]])`. +3. 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 + +On the client side, attach `AuthSingleSig` via `AccountBuilder::with_auth_component`. `miden-client` re-exports `AuthScheme` as `AuthSchemeId`: + +```rust +use miden_client::{ + account::{AccountBuilder, AccountStorageMode, AccountType, component::BasicWallet}, + auth::{AuthSchemeId, AuthSecretKey, AuthSingleSig}, +}; + +let key_pair = AuthSecretKey::new_falcon512_poseidon2_with_rng(client.rng()); + +let account = AccountBuilder::new(seed) + .account_type(AccountType::RegularAccountUpdatableCode) + .storage_mode(AccountStorageMode::Public) + .with_auth_component(AuthSingleSig::new( + key_pair.public_key().to_commitment(), + AuthSchemeId::Falcon512Poseidon2, + )) + .with_component(BasicWallet) + .build()?; +``` + +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]`. If the procedure returns without panicking, the transaction kernel treats authentication as successful. If it panics (for example via `assert!`), authentication fails. + +```rust +#![no_std] +#![feature(alloc_error_handler)] + +use miden::{component, Word}; + +#[component] +struct AuthComponent; + +#[component] +impl AuthComponent { + #[auth_script] + pub fn verify(&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!() + } +} +``` + +The kernel also increments the nonce automatically as part of the `@auth_script` contract — you do not need to call `self.incr_nonce()` from inside the procedure for replay protection. + +## 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.14/builder/smart-contracts/accounts/components.md b/versioned_docs/version-0.14/builder/smart-contracts/accounts/components.md new file mode 100644 index 00000000..7e012989 --- /dev/null +++ b/versioned_docs/version-0.14/builder/smart-contracts/accounts/components.md @@ -0,0 +1,172 @@ +--- +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 + +Apply `#[component]` to both a struct definition and its impl block: + +```rust +use miden::{component, felt, Felt, StorageMap, Word}; + +#[component] +struct CounterContract { + #[storage(description = "counter contract storage map")] + count_map: StorageMap, +} + +#[component] +impl CounterContract { + pub fn get_count(&self) -> Felt { + let key = Word::from_u64_unchecked(0, 0, 0, 1); + self.count_map.get(&key) + } + + pub fn increment_count(&mut self) -> Felt { + let key = Word::from_u64_unchecked(0, 0, 0, 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 macro generates: + +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 + +## Struct definition + +The struct defines the component's storage layout: + +```rust +#[component] +struct MyContract { + #[storage(description = "owner account identifier")] + owner: Value, + + #[storage(description = "user balances")] + balances: StorageMap, +} +``` + +### Storage fields + +Each field must be either `Value` (single-slot) or `StorageMap` (map-slot), annotated with `#[storage]`: + +```rust +#[storage(description = "human-readable description")] +field_name: Value, + +#[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 (from `[package.metadata.component]`) and the field name, so **renaming a field changes the slot ID**. Ordering does not matter, and `slot(N)` is not supported. + +## Impl block — methods + +### Read methods (`&self`) + +Methods that take `&self` are **read-only** — they can query storage and account state but cannot modify anything: + +```rust +pub fn get_balance(&self, depositor: AccountId) -> Felt { + let key = Word::from([depositor.prefix, depositor.suffix, felt!(0), felt!(0)]); + self.balances.get(&key) +} +``` + +### Write methods (`&mut self`) + +Methods that take `&mut self` can **modify state** — write to storage, add/remove assets, create notes: + +```rust +pub fn deposit(&mut self, asset: Asset) { + self.add_asset(asset); +} +``` + +:::info ZK proof implications +Read methods (`&self`) produce proofs that don't include state transitions. Write methods (`&mut self`) produce proofs that do. The distinction is enforced by the compiler and determines which kernel operations are available. +::: + +### Private methods + +Methods without `pub` are private — they can be called from other methods in the same component but are not exported: + +```rust +fn require_initialized(&self) { + let state: Word = self.initialized.read(); + assert!(state[0] == felt!(1)); +} + +pub 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) -> Asset + +// Remove an asset from the account vault +self.remove_asset(asset: Asset) -> Asset + +// Increment the account nonce (replay protection) +self.incr_nonce() -> Felt + +// Compute commitment of account state changes (read-only) +self.compute_delta_commitment() -> Word + +// Check if a procedure was called during this transaction (read-only) +self.was_procedure_called(proc_root: Word) -> bool +``` + +### Read-only methods (`&self`) + +```rust +// Get the account ID +self.get_id() -> AccountId + +// Get the account nonce +self.get_nonce() -> Felt + +// Get fungible asset balance for a faucet +self.get_balance(faucet_id: AccountId) -> Felt + +// Check non-fungible asset ownership +self.has_non_fungible_asset(asset: Asset) -> 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) +``` + +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.14/builder/smart-contracts/accounts/cryptography.md b/versioned_docs/version-0.14/builder/smart-contracts/accounts/cryptography.md new file mode 100644 index 00000000..803c2e52 --- /dev/null +++ b/versioned_docs/version-0.14/builder/smart-contracts/accounts/cryptography.md @@ -0,0 +1,62 @@ +--- +title: "Cryptography" +sidebar_position: 5 +description: "RPO-Falcon512 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. + +## RPO-Falcon512 verification + +The core function for signature verification: + +```rust +use miden::rpo_falcon512_verify; + +// Verify a Falcon512 signature +// pk: RPO256 hash of the public key +// msg: RPO256 hash of the message +rpo_falcon512_verify(pk, msg); +``` + +| Parameter | Type | Description | +|-----------|------|-------------| +| `pk` | `Word` | RPO256 hash of the signer's public key | +| `msg` | `Word` | RPO256 hash of the message being verified | + +The function panics (proof generation fails) if the signature is invalid. + +:::info Where's the signature? +The actual signature data is loaded onto the advice stack by the host. The `rpo_falcon512_verify` function reads it from there. You don't pass the signature as an argument. +::: + +## 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.14/builder/smart-contracts/accounts/custom-types.md b/versioned_docs/version-0.14/builder/smart-contracts/accounts/custom-types.md new file mode 100644 index 00000000..1fc5ead0 --- /dev/null +++ b/versioned_docs/version-0.14/builder/smart-contracts/accounts/custom-types.md @@ -0,0 +1,109 @@ +--- +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 public and use types that are either SDK types (`Felt`, `Word`, `Asset`, etc.) or themselves marked with `#[export_type]`: + +```rust +use miden::{export_type, Felt, Word, Asset, component}; + +#[export_type] +pub struct StructA { + pub foo: Word, + pub asset: Asset, +} + +#[export_type] +pub struct StructB { + pub bar: Felt, + pub baz: Felt, +} + +#[component] +struct MyAccount; + +#[component] +impl MyAccount { + pub fn process(&self, a: StructA, asset: Asset) -> 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, Felt}; + +#[export_type] +pub enum Status { + Active, + Inactive, +} +``` + +## Nested types + +Exported types can reference other exported types: + +```rust +#[export_type] +pub struct Inner { + pub value: Felt, +} + +#[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 `pub fn` signature on a `#[component]` impl | +| Struct fields | Must be `pub` | +| Allowed field types | `Felt`, `Word`, `Asset`, `AccountId`, or other `#[export_type]` types | +| Enums | Unit variants 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.14/builder/smart-contracts/accounts/introduction.md b/versioned_docs/version-0.14/builder/smart-contracts/accounts/introduction.md new file mode 100644 index 00000000..e9a708a2 --- /dev/null +++ b/versioned_docs/version-0.14/builder/smart-contracts/accounts/introduction.md @@ -0,0 +1,73 @@ +--- +title: "What are Accounts?" +sidebar_position: 0 +description: "Accounts are the primary actors in Miden — they store code, state, and assets, and execute transactions locally with private state." +--- + +# 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 that executes transactions locally and generates a zero-knowledge proof of correct execution. This means 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 four parts: + +| Part | Description | +|------|-------------| +| **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 `Value` or `StorageMap` | +| **Vault** | The fungible and non-fungible assets the account holds | +| **Nonce** | A counter that increments exactly once per state change, providing replay protection | + +The network doesn't store the full account state. Instead, it stores cryptographic commitments — hashes of the code, storage, and vault (see [account design](/core-concepts/protocol/account/)). Only the account owner (or a public account's observers) sees the actual data. + +## 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, Asset}; + +#[component] +struct MyWallet; + +#[component] +impl MyWallet { + pub 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 by type and storage mode: + +| Type | Description | +|------|-------------| +| `RegularAccountUpdatableCode` | Standard account — code can be updated after deployment | +| `RegularAccountImmutableCode` | Account with fixed code — cannot be changed after deployment | +| `FungibleFaucet` | Mints and burns fungible tokens | +| `NonFungibleFaucet` | Mints and burns non-fungible tokens (NFTs) | + +Storage mode controls privacy: + +| Mode | Description | +|------|-------------| +| **Public** | Full state is stored on-chain and visible to everyone — suitable for shared protocols like DEXs and faucets | +| **Private** | Only a state commitment is stored on-chain — the actual data stays with the account owner | + +## How accounts differ from EVM contracts + +| | EVM | Miden | +|---|---|---| +| **Execution** | Every validator re-executes every transaction | Account owner executes locally, submits a ZK proof | +| **State visibility** | All state variables are public on-chain | State is private by default (only commitments on-chain) | +| **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 on-chain trace | Proof cannot be generated — no on-chain trace, no cost | + diff --git a/versioned_docs/version-0.14/builder/smart-contracts/accounts/storage.md b/versioned_docs/version-0.14/builder/smart-contracts/accounts/storage.md new file mode 100644 index 00000000..9d284883 --- /dev/null +++ b/versioned_docs/version-0.14/builder/smart-contracts/accounts/storage.md @@ -0,0 +1,181 @@ +--- +title: "Storage" +sidebar_position: 2 +description: "Persistent state management with Value 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 [`Word`](../types) (via `Value`) or a key-value map (via `StorageMap`). Slots are identified by `StorageSlotId` values derived from slot names, which in turn are derived from the component package name and the field name. Renaming a field 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 +#[component] +struct MyContract { + #[storage(description = "configuration")] + config: Value, + + #[storage(description = "user balances")] + balances: StorageMap, +} +``` +Slot IDs are derived from the component package name and the field name. Ordering does not matter, and `slot(N)` is not supported. + +## Value — Single-slot storage + +A `Value` stores a single `Word` (4 Felts) at a fixed slot. It provides `read()` and `write()` methods. Note that `write()` returns the **previous** slot value, not the value written. + +### Reading + +```rust +pub fn get_config(&self) -> Word { + self.config.read() +} + +pub fn is_initialized(&self) -> bool { + let state: Word = self.initialized.read(); + state[0] == felt!(1) +} +``` + +### Writing + +`write()` returns the **previous value**: + +```rust +pub fn initialize(&mut self) { + let old: Word = self.initialized.write( + 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.write(Word::from([ + Felt::from_u64_unchecked(max_per_tx), + Felt::from_u64_unchecked(daily_max), + felt!(0), + felt!(0), + ])); +} + +// Read individual fields +pub fn get_max_per_tx(&self) -> u64 { + let limits: Word = self.limits.read(); + limits[0].as_u64() +} +``` + +## StorageMap — Key-value storage + +A `StorageMap` stores key-value pairs where both keys and values are `Word`-sized. It provides `get()` and `set()` methods. + +### Reading from a map + +```rust +// Get returns a Felt (the last element of the stored Word) +pub fn get_balance(&self, account_id: AccountId) -> Felt { + let key = Word::from([account_id.prefix, account_id.suffix, felt!(0), felt!(0)]); + self.balances.get(&key) +} +``` + +When you call `get()` with a `Felt` return type, it returns index 3 of the stored `Word` (via `From for Felt`). This means single-value maps should store their value at index 3. For the full `Word`, specify the return type: + +```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 key = Word::from([account_id.prefix, account_id.suffix, felt!(0), felt!(0)]); + let old: Felt = self.balances.set(key, 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 +#[component] +struct TokenVault { + /// Config slot: + /// [0] = max_supply (u64) + /// [1] = is_paused (0 or 1) + /// [2] = decimals + /// [3] = unused + #[storage(description = "vault configuration")] + config: Value, + + /// State slot: + /// [0] = total_supply (u64) + /// [1] = total_holders (u64) + /// [2] = last_mint_block + /// [3] = unused + #[storage(description = "vault state")] + state: Value, + + /// 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: + +```rust +use miden::storage; + +// Direct slot access +let value: Word = storage::get_item(slot_id); +let old: Word = storage::set_item(slot_id, new_value); + +// Direct map access +let value: Word = storage::get_map_item(slot_id, &key); +let old: Word = storage::set_map_item(slot_id, key, value); + +// Initial values (at transaction start) +let initial: Word = storage::get_initial_item(slot_id); +let initial: Word = storage::get_initial_map_item(slot_id, &key); +``` + +These functions return values from before any modifications in the current transaction. + +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.14/builder/smart-contracts/cross-component-calls.md b/versioned_docs/version-0.14/builder/smart-contracts/cross-component-calls.md new file mode 100644 index 00000000..ff3479d5 --- /dev/null +++ b/versioned_docs/version-0.14/builder/smart-contracts/cross-component-calls.md @@ -0,0 +1,184 @@ +--- +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) also need to call methods on the account's components to transfer assets. + +## How it works + +When you build a component with `miden build`, the compiler generates an interface describing its public methods. Other projects can import this interface to call those methods. + +``` +counter-contract (component) + → generates interface + → counter-note imports the interface + → calls counter_contract::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 crate::bindings::Account; +use miden::{active_note, Asset}; + +#[note] +struct P2idNote; + +#[note] +impl P2idNote { + #[note_script] + pub fn run(self, _arg: Word, account: &mut Account) { + // Iterate over the note's assets and transfer each to the account + for asset in active_note::get_assets() { + account.receive_asset(asset); + } + } +} +``` + +The `_arg: Word` parameter is the note's first input Word, passed automatically when the note is consumed. It's unused in this example (prefixed with `_`), but note scripts can use it for recipient-specific data like expected account IDs or amounts. + +The `Account` type is auto-generated from the bindings of the dependent component. Its methods correspond to the component's public methods. + +## Using `generate!()` directly + +For more control (e.g., calling multiple components), use `miden::generate!()` to manually generate bindings: + +```rust title="cross-ctx-note/src/lib.rs" +#![no_std] +#![feature(alloc_error_handler)] + +#[global_allocator] +static ALLOC: miden::BumpAlloc = miden::BumpAlloc::new(); + +use miden::*; + +miden::generate!(); +bindings::export!(MyNote); + +use bindings::{ + exports::miden::base::note_script::Guest, + miden::cross_ctx_account::foo::process_felt, +}; + +struct MyNote; + +impl Guest for MyNote { + fn run(_arg: Word) { + let input = Felt::from_u32(11); + let output = process_felt(input); + assert_eq!(output, felt!(53)); + } +} +``` + +Key points: +- `miden::generate!()` generates the `bindings` module from component interfaces +- `bindings::export!(MyNote)` registers `MyNote` as the implementation +- `process_felt` is imported from the `cross-ctx-account` component +- The import path follows the package structure: `bindings::miden::cross_ctx_account::foo::process_felt` + +## Cargo.toml configuration + +Cross-component calls require two dependency declarations: + +### 1. Miden dependencies (for `cargo-miden` linking) + +```toml +[package.metadata.miden.dependencies] +"miden:basic-wallet" = { path = "../basic-wallet" } +``` + +This tells `cargo-miden` where to find the component for linking. + +### 2. Component dependencies (for binding generation) + +```toml +[package.metadata.component.target.dependencies] +"miden:basic-wallet" = { path = "../basic-wallet/target/generated-wit/" } +``` + +This points to the generated interface files used to create Rust bindings. + +### Complete example + +```toml title="counter-note/Cargo.toml" +[package] +name = "counter-note" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +miden = { path = "../../sdk/sdk" } + +[package.metadata.miden] +project-kind = "note-script" + +[package.metadata.component] +package = "miden:counter-note" + +# Miden dependency — the component we want to call +[package.metadata.miden.dependencies] +"miden:counter-contract" = { path = "../counter-contract" } + +# Component dependency — generated interface for binding generation +[package.metadata.component.target.dependencies] +"miden:counter-account" = { path = "../counter-contract/target/generated-wit/" } +``` + +:::info Build order matters +The dependent component must be built first so its interface files exist. Build `counter-contract` before building `counter-note`. +::: + +## Example: Counter note calling counter contract + +```rust title="counter-note/src/lib.rs" +#![no_std] +#![feature(alloc_error_handler)] + +use miden::*; + +// Import the counter contract's generated bindings +use crate::bindings::miden::counter_contract::counter_contract; + +#[note] +struct CounterNote; + +#[note] +impl CounterNote { + #[note_script] + pub fn run(self, _arg: Word) { + // Call get_count() on the counter contract component + let initial_value = counter_contract::get_count(); + + // Call increment_count() — modifies the account's storage + counter_contract::increment_count(); + + // Verify the count increased + let expected_value = initial_value + Felt::from_u32(1); + let final_value = counter_contract::get_count(); + assert_eq!(final_value, expected_value); + } +} +``` + +## When to use which pattern + +| Pattern | Use when | +|---------|----------| +| `#[note]` with `&mut Account` | Note needs to call a single account component's methods | +| `generate!()` + `Guest` trait | Note needs to call multiple components or needs full control | +| Direct module imports | Calling specific functions from a known component interface | + +:::info API Reference +Full API docs on docs.rs: [`miden`](https://docs.rs/miden/latest/miden/) (`generate!()` macro) +::: diff --git a/versioned_docs/version-0.14/builder/smart-contracts/index.md b/versioned_docs/version-0.14/builder/smart-contracts/index.md new file mode 100644 index 00000000..beffdb67 --- /dev/null +++ b/versioned_docs/version-0.14/builder/smart-contracts/index.md @@ -0,0 +1,42 @@ +--- +title: "Miden Smart Contracts" +description: "Reference documentation for building Miden smart contracts in Rust using the Miden SDK." +pagination_prev: null +--- + +# Miden Smart Contracts + +This section is the complete reference for building smart contracts on Miden using Rust and the Miden SDK. If you're new to Miden, follow the hands-on [Miden Bank Tutorial](../tutorials/miden-bank/). + +All Miden Rust contracts compile under these constraints: `#![no_std]`, Rust 2024 edition. + +## Core concepts + + + + 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. + + + +## Reference + + + + Core types: Felt, Word, AccountId, NoteId, and more. + + + Access control, rate limiting, spending limits, and anti-patterns. + + + Complete API documentation for the miden crate. + + diff --git a/versioned_docs/version-0.14/builder/smart-contracts/notes/_category_.json b/versioned_docs/version-0.14/builder/smart-contracts/notes/_category_.json new file mode 100644 index 00000000..aa233038 --- /dev/null +++ b/versioned_docs/version-0.14/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.14/builder/smart-contracts/notes/introduction.md b/versioned_docs/version-0.14/builder/smart-contracts/notes/introduction.md new file mode 100644 index 00000000..f9ac4f41 --- /dev/null +++ b/versioned_docs/version-0.14/builder/smart-contracts/notes/introduction.md @@ -0,0 +1,77 @@ +--- +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 — written in Rust — 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. This indirection is what makes Miden private: the network sees notes being created and consumed, but it can't link sender and recipient accounts because those operations happen in separate transactions. + +## Anatomy of a note + +Every note has four parts: + +| Part | Description | +|------|-------------| +| **Assets** | The fungible or non-fungible tokens the note carries | +| **Script** | Code that executes when the note is consumed — determines who can claim it and what side effects occur | +| **Storage** | Custom data stored with the note that the script can read at consumption time (e.g., a target account ID, an expiration block) | +| **Metadata** | Sender ID, note tag (for discovery routing), and auxiliary data | + +The **recipient** is a cryptographic hash that encodes who can consume the note. When creating notes programmatically (via [`output_note::create`](./output-notes#create-a-note)), you compute a `Recipient` from the note's serial number, script root, and storage commitment: + +``` +recipient = hash(hash(hash(serial_num, [0;4]), script_root), storage_commitment) +``` + +Only someone who knows these values can construct a valid consumption proof. See [Computing a Recipient](./output-notes#computing-a-recipient) for the SDK API. + +## 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 (Recipient) +┌─────────────────────────┐ ┌─────────────────────────┐ +│ 1. Create note │ │ 1. Discover note │ +│ 2. Attach assets │ │ 2. Consume note │ +│ 3. Note published │──────────▶│ 3. Script runs │ +│ (on-chain or private) │ │ 4. Assets move to vault │ +│ │ │ 5. Note nullified │ +└─────────────────────────┘ └─────────────────────────┘ +``` + +**Transaction 1**: The sender's account creates an output note, attaches assets to it, and the note is published (either on-chain or kept private). + +**Transaction 2**: The recipient discovers the note, consumes it in their own transaction, the note script runs and verifies the consumer is authorized, and assets transfer into the recipient's vault. A **nullifier** is recorded to prevent the same note from being consumed again (see [note design](/core-concepts/protocol/note)). + +This separation is what enables privacy and parallelism — the two transactions are independent and unlinkable from the network's perspective. + +## Public vs. private notes + +Notes come in two visibility modes: + +| Mode | Description | +|------|-------------| +| **Public** | The note's full data (assets, script, storage) is stored by the Miden network and visible on-chain. Anyone can discover and attempt to consume it. | +| **Private** | Only a commitment (hash) is stored on-chain. The actual note data must be communicated off-chain between sender and recipient. | + +Private notes provide stronger privacy guarantees — the network can't even see what assets a note carries — but they require the sender and recipient to have a communication channel outside the protocol. + +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 | Transactions are unlinkable; private notes hide all data | +| **Programmability** | Token contracts control transfer logic | Each note carries its own script with custom conditions | +| **Failure** | Revert on-chain, gas consumed | Proof can't be generated — no on-chain trace | +| **Parallelism** | Transfers contend for contract state | Notes are independent — unlimited parallel creation | + diff --git a/versioned_docs/version-0.14/builder/smart-contracts/notes/note-scripts.md b/versioned_docs/version-0.14/builder/smart-contracts/notes/note-scripts.md new file mode 100644 index 00000000..17160e9f --- /dev/null +++ b/versioned_docs/version-0.14/builder/smart-contracts/notes/note-scripts.md @@ -0,0 +1,153 @@ +--- +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 inputs) and an impl block with a `#[note_script]` method: + +```rust +use miden::{AccountId, Word, active_note, note}; + +#[note] +struct MyNote { + target_account_id: AccountId, +} + +#[note] +impl MyNote { + #[note_script] + pub fn run(self, _arg: Word, account: &mut Account) { + // Script logic here + } +} +``` + +The `#[note]` macro: +1. Deserializes note inputs into struct fields +2. Exports the `run` function as the note's entry point + +## Struct fields as note inputs + +Fields on the `#[note]` struct are populated from the note's input data when the note is consumed: + +```rust +#[note] +struct MyNote { + target_account_id: AccountId, // Deserialized from note inputs +} +``` + +The compiler maps struct fields to note inputs based on their order and type. Supported field types include `AccountId`, `Felt`, `Word`, and other SDK types. + +If you don't need inputs, 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 | `&Account` or `&mut Account` for calling account methods | + +### Parameter ordering + +The `Word` and `&mut Account` parameters can appear in either order: + +```rust +// Both are valid: +pub fn run(self, _arg: Word, account: &mut Account) { ... } +pub fn run(self, account: &mut Account, _arg: Word) { ... } +``` + +### With account access + +When you include `&mut Account` (or `&Account`), the note script can call methods on the account's components: + +```rust +#[note_script] +pub fn run(self, _arg: Word, account: &mut Account) { + let assets = active_note::get_assets(); + for asset in assets { + account.receive_asset(asset); // Cross-component call + } +} +``` + +The `Account` type is auto-generated from the bindings of the dependent component — see [Cross-Component Calls](../cross-component-calls). + +### Without account access + +Use this pattern for **trigger or command notes** that carry no assets and only execute logic (e.g., calling a counter contract). If your note transfers assets, include `&mut Account`. + +```rust +#[note_script] +pub fn run(self, _arg: Word) { + // For logic-only notes that carry no assets. + // Cannot call account methods — see the counter note example below. +} +``` + +## 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::*; + +// generated by the build process from the counter component's interface +use crate::bindings::miden::counter_contract::counter_contract; + +#[note] +struct CounterNote; + +#[note] +impl CounterNote { + #[note_script] + pub fn run(self, _arg: Word) { + let initial_value = counter_contract::get_count(); + counter_contract::increment_count(); + let expected_value = initial_value + Felt::from_u32(1); + let final_value = counter_contract::get_count(); + assert_eq!(final_value, expected_value); + } +} +``` + +This note doesn't take `&mut Account` — instead it calls the counter contract's methods directly through generated bindings. See [Cross-Component Calls](../cross-component-calls). + +## Cargo.toml for note scripts + +Note scripts require `project-kind = "note-script"` and must declare dependencies on any account components they interact with: + +```toml +[package.metadata.miden] +project-kind = "note-script" + +# Miden dependencies — account components this note calls +[package.metadata.miden.dependencies] +"miden:basic-wallet" = { path = "../basic-wallet" } +``` + +## Related + +- [Cross-Component Calls](../cross-component-calls) — how `bindings::Account` and `counter_contract::` calls work +- [Transaction Context](../transactions/transaction-context) — transaction scripts with `#[tx_script]` diff --git a/versioned_docs/version-0.14/builder/smart-contracts/notes/note-types.md b/versioned_docs/version-0.14/builder/smart-contracts/notes/note-types.md new file mode 100644 index 00000000..0d9d7d25 --- /dev/null +++ b/versioned_docs/version-0.14/builder/smart-contracts/notes/note-types.md @@ -0,0 +1,166 @@ +--- +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 `P2idNote::create` from the `miden-standards` crate (`miden_standards::note::P2idNote`). The script is pre-compiled MASM — use the builder API to create P2ID notes in client code. +::: + +### 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_standards::note::P2idNote; + +P2idNote::create( + sender, // AccountId: who sends the note + target, // AccountId: the only account that can consume this note + assets, // Vec: assets to attach + note_type, // NoteType: Public or Private + attachment, // NoteAttachment: auxiliary data + rng, // &mut impl FeltRng +) -> Result +``` + +| 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` | `NoteAttachment` | Auxiliary data for the note | +| `rng` | `&mut impl FeltRng` | Random number generator | + +## P2IDE (Pay to ID with Expiration) + +P2IDE extends P2ID with a timelock and a reclaim window. The note can't be consumed before `timelock_height`, and if the target hasn't consumed it by `reclaim_height`, the creator can reclaim the assets. + +### 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 `P2ideNote::create` from the `miden-standards` crate (`miden_standards::note::P2ideNote`). The script is pre-compiled MASM — use the builder API to create P2IDE notes in client code. +::: + +### How it works + +1. Creator creates a P2IDE note with the target account ID, a timelock height, and a reclaim height as note storage items +2. **Target consumes after `timelock_height`** — assets transfer to the target account +3. **Creator reclaims after `reclaim_height`** — assets return to the creator +4. **Before timelock or between timelock and reclaim by a non-target** — any consumption attempt fails (proof generation fails) + +### Note storage + +| Item | Type | Description | +|------|------|-------------| +| `target_account_id_prefix` | `Felt` | Target account ID prefix | +| `target_account_id_suffix` | `Felt` | Target account ID suffix | +| `reclaim_height` | `Felt` | Block height after which the creator can reclaim | +| `timelock_height` | `Felt` | Block height before which the note can't be consumed | + +### Builder API + +```rust +use miden_standards::note::{P2ideNote, P2ideNoteStorage}; + +P2ideNote::create( + sender, // AccountId: who sends the note + P2ideNoteStorage::new(target, reclaim_height, timelock_height), + assets, // Vec: assets to attach + note_type, // NoteType: Public or Private + attachment, // NoteAttachment: auxiliary data + rng, // &mut impl FeltRng +) -> Result +``` + +| Parameter | Type | Description | +|-----------|------|-------------| +| `sender` | `AccountId` | Account sending the note | +| `target` | `AccountId` | The only account that can consume this note (set on `P2ideNoteStorage`) | +| `reclaim_height` | `Option` | Block height after which sender can reclaim; `None` = no reclaim (set on `P2ideNoteStorage`) | +| `timelock_height` | `Option` | Block height before which note can't be consumed; `None` = no timelock (set on `P2ideNoteStorage`) | +| `assets` | `Vec` | Assets to attach to the note | +| `note_type` | `NoteType` | `Public` or `Private` | +| `attachment` | `NoteAttachment` | Auxiliary data for the note | +| `rng` | `&mut impl FeltRng` | Random number generator | + +## 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 `SwapNote::create` from the `miden-standards` crate (`miden_standards::note::SwapNote`). The script is pre-compiled MASM — use the builder API to create SWAP notes in client code. +::: + +### How it works + +1. Creator creates a SWAP note containing the offered asset and metadata describing the requested asset + payback recipient +2. Consumer's transaction processes the note — the script creates a P2ID payback note targeted at the original creator containing the requested asset +3. Consumer receives the offered asset into their vault +4. Both transfers happen atomically in one transaction + +### Builder API + +```rust +use miden_standards::note::SwapNote; + +SwapNote::create( + sender, + offered_asset, + requested_asset, + swap_note_type, + swap_note_attachment, + payback_note_type, + payback_note_attachment, + rng, +) -> Result<(Note, NoteDetails), NoteError> +``` + +| 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 | +| `swap_note_attachment` | `NoteAttachment` | Auxiliary data for the SWAP note | +| `payback_note_type` | `NoteType` | `Public` or `Private` for the P2ID payback note | +| `payback_note_attachment` | `NoteAttachment` | Auxiliary data for the payback note | +| `rng` | `&mut impl FeltRng` | Random number generator | + +Returns a tuple of `(Note, NoteDetails)` — the SWAP note to submit and the expected payback note details (for tracking). + +`NoteAttachment` is defined in the `miden-standards` crate (not the core `miden` SDK). It wraps the attachment data that gets set on output notes — see [note attachments](./output-notes#note-attachments) for the underlying SDK API. + +## More note types + +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.14/builder/smart-contracts/notes/output-notes.md b/versioned_docs/version-0.14/builder/smart-contracts/notes/output-notes.md new file mode 100644 index 00000000..e3a1bc77 --- /dev/null +++ b/versioned_docs/version-0.14/builder/smart-contracts/notes/output-notes.md @@ -0,0 +1,111 @@ +--- +title: "Output Notes" +sidebar_position: 4 +description: "Create output notes, attach assets, set attachments, and compute recipients." +--- + +# Output Notes + +The `output_note` module creates notes from inside account component code and transaction scripts. Use it to send assets to other accounts by creating notes that carry assets and a recipient hash. + +```rust +use miden::{output_note, Asset, NoteIdx, Tag, NoteType, Recipient}; +``` + +## Create a note + +```rust +let note_idx: NoteIdx = output_note::create(tag, note_type, recipient); +``` + + +To construct a tag targeting a specific account, use `NoteTag::with_account_target(account_id)` from `miden_protocol::note`. + +Returns a `NoteIdx` used to reference this note in subsequent operations within the same transaction. + +## Add assets to a note + +```rust +output_note::add_asset(asset, note_idx); +``` + +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: Felt`. + +### Note metadata + +Returns a `NoteMetadata` struct (not a raw `Word`): + +```rust +let metadata: NoteMetadata = output_note::get_metadata(note_idx); +``` + +See [Reading Notes — Note metadata](./reading-notes#note-metadata) for the `NoteMetadata` struct definition. + +## Note attachments + +Notes can carry auxiliary data as attachments. The attachment API uses `Felt`-typed discriminants to select the scheme and kind: + +```rust +// Full form — specify scheme, kind, and payload +output_note::set_attachment(note_idx, attachment_scheme, attachment_kind, attachment_word); + +// Word attachment — a single Word of inline data +output_note::set_word_attachment(note_idx, attachment_scheme, word_data); + +// Array attachment — a commitment to data stored in the advice map +output_note::set_array_attachment(note_idx, attachment_scheme, commitment_word); +``` + + +**Word attachments** store data directly in the note. **Array attachments** store a commitment (hash) to larger data that lives in the advice map — the consumer must have access to the corresponding advice map entries to read the full data. + +## Computing a Recipient + +When creating notes programmatically, you need a `Recipient` to pass to `output_note::create`. The `Recipient` is a hash that encodes the note script and inputs, ensuring only someone who knows these values can consume the note. + +```rust +use miden::{Recipient, Digest}; + +let recipient = Recipient::compute( + serial_num, // Word: unique serial number for this note + script_digest, // Digest: hash of the note script + inputs, // Vec: note inputs (e.g., target account ID) +); +``` + +The computation is: `hash(hash(hash(serial_num, [0;4]), script_root), inputs_commitment)`. `script_digest` and `script_root` refer to the same value — the hash of the note script program. The formula uses `script_root`; the Rust parameter is named `script_digest`. + +## Example: creating and funding a note + +A complete flow for creating a note inside an account component: + +```rust +use miden::{output_note, Asset, Tag, NoteType, Recipient, Digest}; + +pub fn send_assets(recipient: Recipient, asset: Asset, tag: Tag) { + // 1. Create the note + let note_idx = output_note::create(tag, NoteType::Public, recipient); + + // 2. Attach assets + output_note::add_asset(asset, 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.14/builder/smart-contracts/notes/reading-notes.md b/versioned_docs/version-0.14/builder/smart-contracts/notes/reading-notes.md new file mode 100644 index 00000000..42c9f3ab --- /dev/null +++ b/versioned_docs/version-0.14/builder/smart-contracts/notes/reading-notes.md @@ -0,0 +1,163 @@ +--- +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, 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_assets(); +``` + +### 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 a `NoteMetadata` struct containing the note's attachment and header: + +```rust +let metadata: NoteMetadata = active_note::get_metadata(); +``` + +`NoteMetadata` has two fields: + +```rust +pub struct NoteMetadata { + pub attachment: Word, // auxiliary data attached to the note + pub header: Word, // metadata header (sender, tag, etc.) +} +``` + +## `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: InputNoteAssetsInfo = input_note::get_assets_info(note_idx); +let assets: Vec = input_note::get_assets(note_idx); +``` + +`InputNoteAssetsInfo` contains `commitment: Word` and `num_assets: Felt`. + +### 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: InputNoteStorageInfo = input_note::get_storage_info(note_idx); +``` + +:::note +Unlike `active_note::get_storage()` which returns the full `Vec` of storage values, `input_note` only exposes the storage commitment and count — not the actual values. The transaction kernel only has commitments for input notes that are not currently executing. To read actual storage values, use `active_note::get_storage()` inside the note script itself. +::: + +`InputNoteStorageInfo` contains `commitment: Word` and `num_storage_items: Felt`. + +### Note metadata + +Returns the same `NoteMetadata` struct as `active_note`: + +```rust +let metadata: NoteMetadata = input_note::get_metadata(note_idx); +``` + +## Examples + +### Reading storage in a note script + +A note script that reads the target account ID from storage and verifies the consumer: + +```rust +use miden::{AccountId, Word, active_note, note}; + +#[note] +struct P2idNote { + target_account_id: AccountId, +} + +#[note] +impl P2idNote { + #[note_script] + pub fn run(self, _arg: Word, account: &mut Account) { + assert_eq!(account.get_id(), self.target_account_id); + + let assets = active_note::get_assets(); + for asset in assets { + account.receive_asset(asset); + } + } +} +``` + +### 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_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/latest/miden/active_note/), [`miden::input_note`](https://docs.rs/miden/latest/miden/input_note/) +::: diff --git a/versioned_docs/version-0.14/builder/smart-contracts/overview.md b/versioned_docs/version-0.14/builder/smart-contracts/overview.md new file mode 100644 index 00000000..69b7b825 --- /dev/null +++ b/versioned_docs/version-0.14/builder/smart-contracts/overview.md @@ -0,0 +1,151 @@ +--- +title: "What is a Miden Smart Contract" +sidebar_position: 1 +description: "Miden's execution model, account structure, note system, and transaction lifecycle — how Rust code becomes zero-knowledge proofs." +--- + +# What is a Miden Smart Contract + +Miden is a ZK rollup 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 + +Your Rust code goes through several transformations before it runs: + +``` +Rust → Wasm → Miden Assembly (MASM) → ZK Circuit → Proof +``` + +1. **Rust → Wasm**: The Miden compiler (`cargo-miden`) compiles your `#![no_std]` Rust to WebAssembly +2. **Wasm → MASM**: The compiler translates Wasm to Miden Assembly, the VM's native instruction set +3. **MASM → Proof**: When a transaction executes, the Miden VM runs the MASM code and produces a zero-knowledge proof + +The output of `miden build` is a `.masp` file (Miden Assembly Package) 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 +#[component] +struct MyWallet; + +#[component] +impl MyWallet { + pub fn receive_asset(&mut self, asset: Asset) { + self.add_asset(asset); + } +} +``` + +Each component defines its own storage layout and public methods. The `#[component]` 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") +- **Inputs**: 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 on-chain transaction that fails. The proof simply doesn't exist if the execution is invalid. + +## Account types + +Miden supports several account types, configured in `Cargo.toml`: + +| Type | Description | +|------|-------------| +| `RegularAccountUpdatableCode` | Standard account — code can be updated after deployment | +| `RegularAccountImmutableCode` | Account with fixed code — cannot be changed | +| `FungibleFaucet` | Token minting account (fungible assets) | +| `NonFungibleFaucet` | NFT minting account (non-fungible assets) | + +## Building blocks + +| Building Block | Description | Details | +|----------------|-------------|---------| +| [Components](./accounts/components) | Reusable code modules with storage and WIT interfaces | `#[component]` macro | +| [Storage](./accounts/storage) | Up to 255 slots of Value or StorageMap | Persistent state | +| [Custom Types](./accounts/custom-types) | Exported structs/enums for public APIs | `#[export_type]` | +| [Account Operations](./accounts/account-operations) | Read/write account state and vault | `active_account`, `native_account` | +| [Notes](./notes/) | Programmable UTXOs for asset transfers | Note scripts | +| [The tx Module](./transactions/transaction-context) | Block queries and expiration management | `tx` module, `#[tx_script]` | +| [Authentication](./accounts/authentication) | Falcon512 signatures and replay protection | Nonce management | +| [Cross-Component Calls](./cross-component-calls) | Inter-component communication | WIT bindings, `generate!()` | +| [Types](./types) | Felt, Word, Asset — the VM's native types | Field arithmetic | + +Ready to start building? Follow the [Miden Bank Tutorial](../tutorials/miden-bank/) for a hands-on walkthrough. diff --git a/versioned_docs/version-0.14/builder/smart-contracts/patterns.md b/versioned_docs/version-0.14/builder/smart-contracts/patterns.md new file mode 100644 index 00000000..114ec53a --- /dev/null +++ b/versioned_docs/version-0.14/builder/smart-contracts/patterns.md @@ -0,0 +1,76 @@ +--- +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 `ownable` standard (`miden-standards/asm/standards/access/ownable.masm`) provides `verify_owner`, `get_owner`, `transfer_ownership`, and `renounce_ownership` procedures. + +## Rate limiting {#rate-limiting} + +Use `tx::get_block_number()` to enforce cooldown periods between actions. Store the last action block number in a `Value` storage slot, then compare against 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 + +Miden doesn't support error strings or `Result` types in contract execution. Use assertions: + +```rust +assert!(amount > 0); +assert_eq!(a, b); +``` + +When an assertion fails, proof generation fails and the transaction is rejected before reaching the network. + +### 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: + +```rust +// 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** — contract code is visible on-chain +- **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!()` / `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.14/builder/smart-contracts/transactions/_category_.json b/versioned_docs/version-0.14/builder/smart-contracts/transactions/_category_.json new file mode 100644 index 00000000..5f2ae57d --- /dev/null +++ b/versioned_docs/version-0.14/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.14/builder/smart-contracts/transactions/advice-provider.md b/versioned_docs/version-0.14/builder/smart-contracts/transactions/advice-provider.md new file mode 100644 index 00000000..f5912da8 --- /dev/null +++ b/versioned_docs/version-0.14/builder/smart-contracts/transactions/advice-provider.md @@ -0,0 +1,140 @@ +--- +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 on-chain 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` + +Loads a preimage from the advice provider given a commitment and expected word count. This is useful when a note or transaction script needs to retrieve data that was hashed and stored by the sender. + +```rust +use miden::adv_load_preimage; + +// 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 +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); +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); +``` + + +### `adv_insert_mem` + +Inserts a range of memory into the advice map. The VM reads `Word`s from addresses `[start_addr, end_addr)` and stores them under the key. Both address arguments are `u32`. + +```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); +``` + + +:::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.14/builder/smart-contracts/transactions/introduction.md b/versioned_docs/version-0.14/builder/smart-contracts/transactions/introduction.md new file mode 100644 index 00000000..9a5e79e7 --- /dev/null +++ b/versioned_docs/version-0.14/builder/smart-contracts/transactions/introduction.md @@ -0,0 +1,82 @@ +--- +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 (see [transaction design](/core-concepts/protocol/transaction)). Only this proof and the resulting state commitments are submitted to the network. The network never sees the transaction inputs, the account's private state, or the logic that ran. + +## 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 assets transfer to the account | +| **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│ │ state │ │ updates │ +│ │ │ │ │ │ │ sent │ │ 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 and public state updates (new note commitments, updated account commitment, 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, inputs, 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 inputs 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 on-chain, consumes gas, and is visible to everyone. + +## How transactions differ from EVM transactions + +| | EVM | Miden | +|---|---|---| +| **Execution** | Every validator re-executes the transaction | Client executes locally, submits only the proof | +| **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 | Network sees only the proof and state commitments | +| **Failure** | On-chain revert, gas consumed, visible trace | Proof can't be generated — no on-chain trace, no cost | +| **Parallelism** | Transactions touching same state must serialize | Single-account scope enables parallel execution | +| **Authentication** | `msg.sender` set by protocol | Falcon512 signatures verified inside the transaction | diff --git a/versioned_docs/version-0.14/builder/smart-contracts/transactions/transaction-context.md b/versioned_docs/version-0.14/builder/smart-contracts/transactions/transaction-context.md new file mode 100644 index 00000000..9414f6b2 --- /dev/null +++ b/versioned_docs/version-0.14/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 is a local operation that consumes zero or more input notes and produces state changes plus output notes for a single account. The transaction executes entirely on the client — the VM runs the code, generates a ZK proof, and only the proof is submitted to the network. 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::tx; +``` + +### Block information + +```rust +// Current block number +let block_num: Felt = tx::get_block_number(); + +// Block commitment (hash of block header) +let commitment: Word = tx::get_block_commitment(); + +// Block timestamp (seconds since epoch) +let timestamp: Felt = 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: Felt = tx::get_num_input_notes(); +let num_outputs: Felt = tx::get_num_output_notes(); +``` + +### Transaction expiration + +Control how long a transaction remains valid: + +```rust +// Get current expiration delta (in blocks) +let delta: Felt = tx::get_expiration_block_delta(); + +// Set a new expiration delta +tx::update_expiration_block_delta(felt!(100)); +``` + +The expiration delta determines how many blocks after creation the transaction remains valid. If the transaction isn't included within this window, it expires. + +## 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.14/builder/smart-contracts/transactions/transaction-scripts.md b/versioned_docs/version-0.14/builder/smart-contracts/transactions/transaction-scripts.md new file mode 100644 index 00000000..f8bda2e1 --- /dev/null +++ b/versioned_docs/version-0.14/builder/smart-contracts/transactions/transaction-scripts.md @@ -0,0 +1,104 @@ +--- +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 Account) { ... } + +// 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 | `&Account` or `&mut Account` | +| Generics | Not allowed | +| Async | Not allowed | + +## Cargo.toml + +```toml +[package.metadata.miden] +project-kind = "transaction-script" +``` + +## Example: basic-wallet-tx-script + +This example reads note parameters from the advice map and creates an output note: + +```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::{intrinsics::advice::{adv_load_preimage, adv_push_mapvaln}, *}; + +use crate::bindings::Account; + +// 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 = 10; + +#[tx_script] +fn run(arg: Word, account: &mut Account) { + let num_felts = adv_push_mapvaln(arg.clone()); + let num_felts_u64 = num_felts.as_u64(); + assert_eq!(Felt::from_u32((num_felts_u64 % 4) as u32), felt!(0)); + let num_words = Felt::from_u64_unchecked(num_felts_u64 / 4); + 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 = output_note::create(tag.into(), note_type.into(), recipient.into()); + let asset: [Felt; 4] = input[ASSET_START..ASSET_END].try_into().unwrap(); + account.move_asset_to_note(asset.into(), note_idx); +} +``` + +### Walkthrough + +1. **`arg: Word`** is a map key used to look up the transaction data in the advice map +2. **`adv_push_mapvaln(arg)`** reads the number of felts stored at that key +3. **`adv_load_preimage(num_words, commitment)`** retrieves the actual data (tag, note_type, recipient, asset) from the advice map +4. **`output_note::create(tag, note_type, recipient)`** creates the output note +5. **`account.move_asset_to_note(asset, note_idx)`** moves the asset from the account vault into the newly created note + +:::note +This script uses the advice map to pass structured input data. The caller encodes the note parameters (tag, note_type, recipient, asset) as a preimage and passes the commitment hash as the `arg` Word. +::: + +:::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.14/builder/smart-contracts/types.md b/versioned_docs/version-0.14/builder/smart-contracts/types.md new file mode 100644 index 00000000..78ece75b --- /dev/null +++ b/versioned_docs/version-0.14/builder/smart-contracts/types.md @@ -0,0 +1,290 @@ +--- +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}; + +// Compile-time literal (validated at compile time) +let zero = felt!(0); +let one = felt!(1); +let answer = felt!(42); + +// From u32 (always safe) +let f = Felt::from_u32(255); + +// From u64 (infallible — values are reduced into canonical form internally) +let f = Felt::new(1_000_000_000); + +// Built-in zero / one constants +let z = Felt::ZERO; +let o = Felt::ONE; +``` + +:::info `felt!()` range limitation +The `felt!()` macro currently only accepts values up to `u32::MAX` (4,294,967,295). For larger values, use `Felt::new()`. This limitation may be lifted in a future release. +::: + +### 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()`. +::: + +### Comparison and conversion + +```rust +let f = felt!(42); + +// Convert to u64 (canonical representation) +let n: u64 = f.as_u64(); + +// Equality comparison +if f == felt!(42) { /* ... */ } + +// For numeric comparisons, convert to u64 first +if f.as_u64() > 100 { /* ... */ } + +// Check parity +if f.is_odd() { /* ... */ } +``` + +:::tip Integer arithmetic on Felt values +Converting `Felt` to `u64` with `.as_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_u64(); +let b: u64 = felt_b.as_u64(); +let sum = a.saturating_add(b); // safe addition +let diff = a.saturating_sub(b); // no underflow +let result = Felt::new(sum); +``` +::: + +### 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 + +// Power of 2: computes 2^self +let power = felt!(10).pow2(); // 2^10 = 1024 (panics if self > 63) +``` + +## 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), // a: max amount + Felt::new(cooldown), // b: cooldown blocks + felt!(0), // c: unused + felt!(0), // d: unused +]); + +// Unpack via named fields +let max_amount = config.a.as_u64(); +let cooldown = config.b.as_u64(); +``` + +## Asset + +`Asset` represents either a fungible or non-fungible asset. In v0.14 it is **two words** — a `key` (identifies the asset class) and a `value` (encodes 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 | +| `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 | +| `key` | `d` | Faucet ID prefix | +| `value` | `a..d`| Data payload (implementation-defined) | + +### Working with assets + +```rust +use miden::{Asset, Word, felt}; + +// Build a fungible asset from key + value words. +// Fungible key = [0, 0, faucet_suffix, faucet_prefix], +// fungible value = [amount, 0, 0, 0]. +let asset = Asset::new( + Word::from([felt!(0), felt!(0), faucet_suffix, 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_u64(); + +// Build a fungible asset from faucet ID + amount via the SDK helper. +use miden::asset; +let asset = asset::create_fungible_asset(faucet_id, felt!(1000)); + +// Build a non-fungible asset. +let nft = asset::create_non_fungible_asset(faucet_id, data_hash); +``` + +:::note Asset on the host side +On the client / host side, `Asset` is an enum (`Asset::Fungible(_) | Asset::NonFungible(_)`) exposed from `miden-protocol`, with `to_key_word()` / `to_value_word()` / `from_key_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)` | +| literal | `Felt` | `felt!(n)` | +| `Felt` | `u64` | `f.as_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.14/builder/tools/_category_.json b/versioned_docs/version-0.14/builder/tools/_category_.json new file mode 100644 index 00000000..6c5100b8 --- /dev/null +++ b/versioned_docs/version-0.14/builder/tools/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Tools", + "position": 6 +} diff --git a/versioned_docs/version-0.14/builder/tools/clients/_category_.yml b/versioned_docs/version-0.14/builder/tools/clients/_category_.yml new file mode 100644 index 00000000..9c37f780 --- /dev/null +++ b/versioned_docs/version-0.14/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.14/builder/tools/clients/common-errors.md b/versioned_docs/version-0.14/builder/tools/clients/common-errors.md new file mode 100644 index 00000000..f041814f --- /dev/null +++ b/versioned_docs/version-0.14/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.14/builder/tools/clients/img/get-started/account-a.png b/versioned_docs/version-0.14/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.14/builder/tools/clients/img/get-started/account-a.png differ diff --git a/versioned_docs/version-0.14/builder/tools/clients/img/get-started/account-b.png b/versioned_docs/version-0.14/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.14/builder/tools/clients/img/get-started/account-b.png differ diff --git a/versioned_docs/version-0.14/builder/tools/clients/img/get-started/commit-height.png b/versioned_docs/version-0.14/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.14/builder/tools/clients/img/get-started/commit-height.png differ diff --git a/versioned_docs/version-0.14/builder/tools/clients/img/get-started/consumed-note.png b/versioned_docs/version-0.14/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.14/builder/tools/clients/img/get-started/consumed-note.png differ diff --git a/versioned_docs/version-0.14/builder/tools/clients/img/get-started/miden-account-list.png b/versioned_docs/version-0.14/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.14/builder/tools/clients/img/get-started/miden-account-list.png differ diff --git a/versioned_docs/version-0.14/builder/tools/clients/img/get-started/note-view.png b/versioned_docs/version-0.14/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.14/builder/tools/clients/img/get-started/note-view.png differ diff --git a/versioned_docs/version-0.14/builder/tools/clients/img/get-started/processing-note.png b/versioned_docs/version-0.14/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.14/builder/tools/clients/img/get-started/processing-note.png differ diff --git a/versioned_docs/version-0.14/builder/tools/clients/img/get-started/transaction-confirmation.png b/versioned_docs/version-0.14/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.14/builder/tools/clients/img/get-started/transaction-confirmation.png differ diff --git a/versioned_docs/version-0.14/builder/tools/clients/img/get-started/two-accounts.png b/versioned_docs/version-0.14/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.14/builder/tools/clients/img/get-started/two-accounts.png differ diff --git a/versioned_docs/version-0.14/builder/tools/clients/img/get-started/view-account-vault.png b/versioned_docs/version-0.14/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.14/builder/tools/clients/img/get-started/view-account-vault.png differ diff --git a/versioned_docs/version-0.14/builder/tools/clients/index.md b/versioned_docs/version-0.14/builder/tools/clients/index.md new file mode 100644 index 00000000..60b678b3 --- /dev/null +++ b/versioned_docs/version-0.14/builder/tools/clients/index.md @@ -0,0 +1,52 @@ +--- +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 on-chain 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 / React Native app for instant Miden integration. + + + +## Shared topics + + + + 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.14/builder/tools/clients/react-sdk/_category_.yml b/versioned_docs/version-0.14/builder/tools/clients/react-sdk/_category_.yml new file mode 100644 index 00000000..8859b318 --- /dev/null +++ b/versioned_docs/version-0.14/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.14/builder/tools/clients/react-sdk/advanced.md b/versioned_docs/version-0.14/builder/tools/clients/react-sdk/advanced.md new file mode 100644 index 00000000..3562456f --- /dev/null +++ b/versioned_docs/version-0.14/builder/tools/clients/react-sdk/advanced.md @@ -0,0 +1,213 @@ +--- +title: Advanced +sidebar_position: 5 +--- + +# Advanced hooks + +Hooks beyond the core send / mint / consume trio: custom scripts, 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) | + +The `privateNoteTarget` field is the 4-step pipeline shortcut: execute the tx, commit on-chain, 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. + +## `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, + slots: [StorageSlot.emptyValue("miden::tutorials::counter")], +}); + +// Transaction script (with optional libraries) +const script = await txScript({ + code: ` + use external_contract::counter_contract + begin + call.counter_contract::increment_count + end + `, + libraries: [ + { namespace: "external_contract::counter_contract", code: counterContractCode }, + ], +}); + +// Note script — v0.14 uses 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 { useSessionAccount } from "@miden-sdk/react"; + +const { initialize, sessionAccountId, isReady, step, error, reset } = + useSessionAccount({ + // Called with the new session wallet's bech32 ID once it's created. + // Your code is responsible for actually sending funds to it — e.g. by + // triggering a send from a main wallet elsewhere in the app. + fund: async (sessionAccountId) => { + await sendFromMainWallet(sessionAccountId); + }, + assetId: usdcFaucetId, // optional + walletOptions: { storageMode: "private", mutable: true }, + pollIntervalMs: 3_000, // default 3_000 + maxWaitMs: 60_000, // default 60_000 + }); + +await initialize(); +// step progresses: "idle" → "creating" → "funding" → "consuming" → "ready" +``` + +`UseSessionAccountReturn`: + +| Field | Description | +| --- | --- | +| `initialize()` | Kicks off the create → fund → consume flow | +| `sessionAccountId` | bech32 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(); +await importStore(uploadedDump, client.storeIdentifier(), { 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. + +```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.14/builder/tools/clients/react-sdk/index.md b/versioned_docs/version-0.14/builder/tools/clients/react-sdk/index.md new file mode 100644 index 00000000..b47ec301 --- /dev/null +++ b/versioned_docs/version-0.14/builder/tools/clients/react-sdk/index.md @@ -0,0 +1,44 @@ +--- +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, React Native, Electron + React, etc.). 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` | +| [Mutation hooks](./mutation-hooks.md) | `useCreateWallet`, `useCreateFaucet`, `useImportAccount`, `useSend`, `useMultiSend`, `useMint`, `useConsume`, `useSwap` | +| [Advanced hooks](./advanced.md) | `useTransaction`, `useExecuteProgram`, `useCompile`, `useSessionAccount`, `useExportStore`, `useImportStore`, `useImportNote`, `useExportNote`, `useSyncControl`, `useWaitForCommit`, `useWaitForNotes` | +| [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`, `wallets`, `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. +- [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.14/builder/tools/clients/react-sdk/mutation-hooks.md b/versioned_docs/version-0.14/builder/tools/clients/react-sdk/mutation-hooks.md new file mode 100644 index 00000000..7bb02444 --- /dev/null +++ b/versioned_docs/version-0.14/builder/tools/clients/react-sdk/mutation-hooks.md @@ -0,0 +1,330 @@ +--- +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. + +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. + +```tsx +import { useCreateWallet, AuthScheme } from "@miden-sdk/react"; + +function NewWalletButton() { + const { createWallet, wallet, isCreating, error } = useCreateWallet(); + + const handleCreate = async () => { + const account = await createWallet({ + storageMode: "private", // "private" | "public" | "network" (default private) + mutable: true, // default true — updatable code + authScheme: AuthScheme.AuthRpoFalcon512, // default + }); + console.log("Created:", account.bech32id()); + }; + + return ( + + ); +} +``` + +`CreateWalletOptions` (all optional): + +| Field | Default | Description | +| --- | --- | --- | +| `storageMode` | `"private"` | `"private"` / `"public"` / `"network"` | +| `mutable` | `true` | Whether code can be updated after deployment | +| `authScheme` | `AuthScheme.AuthRpoFalcon512` | 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"; + +function NewFaucetButton() { + const { createFaucet, faucet, isCreating } = useCreateFaucet(); + + const handleCreate = async () => { + const created = await createFaucet({ + tokenSymbol: "TEST", + decimals: 8, // default 8 + maxSupply: 10_000_000n, // number | bigint + storageMode: "public", // default "private"; public allows FPI reads + }); + 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 on-chain | +| `authScheme` | `AuthScheme.AuthRpoFalcon512` | 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. + +```tsx +import { useImportAccount, AuthScheme } from "@miden-sdk/react"; + +const { importAccount, account, isImporting, error } = useImportAccount(); + +// By ID — public accounts only +const imported = await importAccount({ + type: "id", + accountId: "mtst1qy35...", +}); + +// By seed — public accounts only +await importAccount({ + type: "seed", + seed: initSeed, // Uint8Array + mutable: true, // default true + 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.14/builder/tools/clients/react-sdk/query-hooks.md b/versioned_docs/version-0.14/builder/tools/clients/react-sdk/query-hooks.md new file mode 100644 index 00000000..2474f6b6 --- /dev/null +++ b/versioned_docs/version-0.14/builder/tools/clients/react-sdk/query-hooks.md @@ -0,0 +1,282 @@ +--- +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 tracked by the client, pre-categorised into wallets and faucets. + +```tsx +import { useAccounts } from "@miden-sdk/react"; + +function AccountList() { + const { accounts, wallets, faucets, isLoading, error } = useAccounts(); + + if (isLoading) return

Loading…

; + if (error) return

{error.message}

; + + return ( + <> +

Wallets ({wallets.length})

+ {wallets.map((w) =>
{w.id().toString()}
)} + +

Faucets ({faucets.length})

+ {faucets.map((f) =>
{f.id().toString()}
)} + + ); +} +``` + +Return type (`AccountsResult`): + +```ts +{ + accounts: AccountHeader[]; // every tracked account + wallets: AccountHeader[]; // regular accounts + faucets: AccountHeader[]; // token faucets + 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.14/builder/tools/clients/react-sdk/recipes.md b/versioned_docs/version-0.14/builder/tools/clients/react-sdk/recipes.md new file mode 100644 index 00000000..c6801a96 --- /dev/null +++ b/versioned_docs/version-0.14/builder/tools/clients/react-sdk/recipes.md @@ -0,0 +1,149 @@ +--- +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 + +Every mutation hook 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 = formatNoteSummary(summary); // "1.5 USDC" +``` + +`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 BlockHeaderPeek() { + const client = useMidenClient(); + const header = await client.getBlockHeaderByNumber(100); + // ... whatever the hooks don't expose +} +``` + +`useMidenClient()` throws if the provider isn't ready — guard with `useMiden().isReady` when you render before init. + +## Prevent race conditions + +Two user actions can fire in quick succession — a double-click on "Send", or a hook plus a manual button both wanting to sign. The React SDK exposes a lock: + +```tsx +import { useMiden, useMidenClient } from "@miden-sdk/react"; + +function CompoundFlow() { + const { runExclusive } = useMiden(); + const client = useMidenClient(); + + const run = () => + runExclusive(async () => { + // Multiple client calls that must not interleave with other hooks' + // WASM work run here — the lock serialises them across the whole app. + await client.sync(); + // ... + }); + + return ; +} +``` + +`runExclusive(fn)` takes a zero-argument async function; reach for the client via `useMidenClient()` inside it. Built-in mutations already use this lock internally; `runExclusive` is the escape hatch for your own compound flows. + +## Isolated clients for multi-wallet apps + +`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). + +If you just need two wallets side-by-side in a dev environment and don't want to wire a signer, mount two separate `MidenProvider`s in isolated subtrees backed by different signer contexts. + +## Account IDs — hex and bech32 interchangeably + +Every hook accepts either: + +```tsx +// Both are valid +useAccount("0x1234567890abcdef"); +useAccount("mtst1qy35..."); + +// Convert for display +account.bech32id(); // "mtst1qy35..." + +import { toBech32AccountId } from "@miden-sdk/react"; +toBech32AccountId(someHexId); // "mtst1qy35..." +``` + +## 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"` | Two `useSend` mutations fired simultaneously. Either `await` the previous call before starting the next, or use `runExclusive` to coordinate. | + +## 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.14/builder/tools/clients/react-sdk/setup.md b/versioned_docs/version-0.14/builder/tools/clients/react-sdk/setup.md new file mode 100644 index 00000000..df95b16e --- /dev/null +++ b/versioned_docs/version-0.14/builder/tools/clients/react-sdk/setup.md @@ -0,0 +1,211 @@ +--- +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` | `"devnet" \| "testnet" \| string` | Note transport service. 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` | + +### `loadingComponent` and `errorComponent` + +- `loadingComponent` is rendered during the brief WASM load phase (first render only). +- `errorComponent` is rendered if initialization fails. It accepts either a `ReactNode` or `(error: Error) => ReactNode`. + +Both are optional. Leaving them unset uses sensible defaults. + +## 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, runExclusive } = useMiden(); + + if (isInitializing) return

Loading Miden…

; + if (error) return

Init error: {error.message}

; + + return ; +} +``` + +- `isReady` — `true` once the WASM module, keystore, and signer are fully initialised. +- `isInitializing` — `true` during the first load. +- `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 [race conditions](./recipes.md#prevent-race-conditions). + +`useMidenClient()` is a shortcut that returns the ready `WebClient` directly, throwing if the provider isn't ready yet: + +```tsx +import { useMidenClient } from "@miden-sdk/react"; + +function AdvancedCall() { + const client = useMidenClient(); + const header = await client.getBlockHeaderByNumber(100); + // ... +} +``` + +Use it for APIs the React SDK hooks don't expose. + +## 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`, `wallets`, `faucets`) 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 { wallets, faucets, isLoading, error, refetch } = 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 ( + +); +``` + +See [Mutation hooks](./mutation-hooks.md) for the full surface. + +## Account ID formats + +Every hook accepts either form: + +```tsx +useAccount("0x1234567890abcdef"); // hex +useAccount("mtst1qy35..."); // bech32 (testnet prefix) + +// Convert for display +account.bech32id(); // "mtst1qy35..." on testnet +``` + +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.14/builder/tools/clients/react-sdk/signers.md b/versioned_docs/version-0.14/builder/tools/clients/react-sdk/signers.md new file mode 100644 index 00000000..64a39269 --- /dev/null +++ b/versioned_docs/version-0.14/builder/tools/clients/react-sdk/signers.md @@ -0,0 +1,224 @@ +--- +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 + +### Para (EVM wallets) + +```tsx +import { ParaSignerProvider } from "@miden-sdk/para"; +import { MidenProvider } from "@miden-sdk/react"; + +function App() { + return ( + + + + + + ); +} +``` + +Expose Para-specific data inside your app: + +```tsx +import { useParaSigner } from "@miden-sdk/para"; + +const { para, wallet, isConnected } = useParaSigner(); +``` + +### Turnkey + +```tsx +import { TurnkeySignerProvider } from "@miden-sdk/miden-turnkey-react"; + +// Config is optional — defaults to https://api.turnkey.com and reads +// VITE_TURNKEY_ORG_ID from the environment. + + + + + + +// Or with explicit config: + + ... + +``` + +Connect via passkey authentication. `useSigner()` returns `SignerContextValue | null` (null when no signer provider is above the component), so always null-guard: + +```tsx +import { useSigner } from "@miden-sdk/react"; +import { useTurnkeySigner } from "@miden-sdk/miden-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/wallet-adapter-react"; + + + + + + +``` + +## Unified signer interface + +Every prebuilt provider exposes the same `useSigner` surface, so UI code that only cares about connect/disconnect stays signer-agnostic. Note the return is `SignerContextValue | null`: + +```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 + +For a signing service that doesn't have a prebuilt provider — internal HSM, hardware wallet, or experimental integration — wire `SignerContext` directly: + +```tsx +import { SignerContext, type SignerContextValue } from "@miden-sdk/react"; + +const signer: SignerContextValue = { + name: "MyWallet", + storeName: `mywallet_${userAddress}`, // unique per user for DB isolation + isConnected: true, + accountConfig: { + publicKeyCommitment: userPublicKeyCommitment, // Uint8Array + storageMode: "private", + accountType: "RegularAccountUpdatableCode", + }, + signCb: async (pubKey, signingInputs) => { + // Route to your signing service + return signature; // Uint8Array + }, + connect: async () => { + /* trigger wallet connection */ + }, + disconnect: async () => { + /* clear session */ + }, +}; + +function App() { + return ( + + + + + + ); +} +``` + +`storeName` is critical: each user's data lives in its own IndexedDB database, so make the `storeName` unique per signing identity (typically the wallet address or a derived hash). + +## Custom `AccountComponent`s + +Attach application-specific components — compiled from `.masp` packages, e.g. a DEX module — alongside the default auth and basic wallet components: + +```tsx +import { type SignerAccountConfig } from "@miden-sdk/react"; +import { AccountComponent } from "@miden-sdk/miden-sdk"; + +const myDexComponent: AccountComponent = await loadCompiledComponent(); + +const accountConfig: SignerAccountConfig = { + publicKeyCommitment: userPublicKeyCommitment, + accountType: "RegularAccountUpdatableCode", + storageMode: "private", + customComponents: [myDexComponent], +}; +``` + +Components are appended to the `AccountBuilder` after the default basic wallet component and before `build()` is called, so the account always includes wallet functionality plus any extras you pass. The field is optional; leaving it unset (or passing an empty array) preserves the default behaviour. + +## `MultiSignerProvider` + +For apps that need to swap between multiple signer providers at runtime (e.g. "connect with Para" or "connect with Turnkey"), use `MultiSignerProvider`. Each registered signer provider renders its own `` (the component takes no children — it registers the provider's current `SignerContext` with the multi-signer context) and `MidenProvider` sits as a sibling inside `MultiSignerProvider`: + +```tsx +import { MultiSignerProvider, SignerSlot, MidenProvider } from "@miden-sdk/react"; + +function App() { + 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 ( + <> + + + + + ); +} +``` + +`useMultiSigner()` returns `MultiSignerContextValue | null`; its `connectSigner(name)` / `disconnectSigner()` actions switch and clear the active signer respectively. The name passed to `connectSigner` matches the `name` field on each signer's `SignerContextValue`. + +## 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.14/builder/tools/clients/rust-client/_category_.yml b/versioned_docs/version-0.14/builder/tools/clients/rust-client/_category_.yml new file mode 100644 index 00000000..ff26d637 --- /dev/null +++ b/versioned_docs/version-0.14/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.14/builder/tools/clients/rust-client/api-docs.md b/versioned_docs/version-0.14/builder/tools/clients/rust-client/api-docs.md new file mode 100644 index 00000000..7fe20be7 --- /dev/null +++ b/versioned_docs/version-0.14/builder/tools/clients/rust-client/api-docs.md @@ -0,0 +1,8 @@ +--- +title: API +sidebar_position: 8 +--- + +:::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.14/builder/tools/clients/rust-client/cli/_category_.yml b/versioned_docs/version-0.14/builder/tools/clients/rust-client/cli/_category_.yml new file mode 100644 index 00000000..791dc0ca --- /dev/null +++ b/versioned_docs/version-0.14/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.14/builder/tools/clients/rust-client/cli/cli-config.md b/versioned_docs/version-0.14/builder/tools/clients/rust-client/cli/cli-config.md new file mode 100644 index 00000000..129ddef0 --- /dev/null +++ b/versioned_docs/version-0.14/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` 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. + +```sh +store_filepath = ".miden/store.sqlite3" +secret_keys_directory = ".miden/keystore" +default_account_id = "0x012345678" +token_symbol_map_filepath = ".miden/token_symbol_map.toml" +remote_prover_endpoint = "http://localhost:8080" +package_directory = ".miden/packages" +max_block_number_delta = 256 + +[rpc] +endpoint = { protocol = "http", host = "localhost", port = 57291 } +timeout_ms = 10000 + +[note-transport] # optional +endpoint = "http://localhost:57292" +timeout_ms = 10000 +``` + +### 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` command helps manage configuration by removing existing setups: + +```bash +# Remove local config if present, otherwise remove global config +miden-client clear + +# Force removal of global configuration only +miden-client clear --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 endpoint of the Miden node. It can be a specific url (like `"https://rpc.devnet.miden.io"`) or a table with the following fields: + - `protocol`: The protocol used to connect to the node. It can be either `http` or `https`. + - `host`: The host of the node. It can be either an IP address or a domain name. + - `port`: The port of the node. It is an integer. + +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 `.miden/store.sqlite3` and `.miden/keystore`, respectively, organizing these files within the `.miden` directory structure. + +The store filepath can be set when running the `miden-client init` command with the `--store-path` flag. + +### Default account ID + +The `default_account_id` field contains the default account ID to be used by the client's command when no `account` is provided. It is a hexadecimal string that represents the account ID. The field is optional, and if not set, the client will set it once the first account is created. + +By default none is set, but 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 `.miden/token_symbol_map.toml`. + +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 = { id = "0xa031cc137adecd54", decimals = 18 } +BTC = { id = "0x2f3c4b5e6a7b8c9d", decimals = 8 } +``` + +The `id` field is the faucet account ID and 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::0x2f3c4b5e6a7b8c9d` + +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 the usage of a remote prover. You can set a remote prover when calling the `miden-client prover` command with the `--remote-prover-endpoint` flag. The prover will be used for all transactions that are executed with the `miden` command. By default, no remote prover is used and all transactions are executed 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 `.miden/packages`. + +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://0xmiden.github.io/compiler/appendix/known-limitations.html#packaging) + +### 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). + +- `MIDEN_DEBUG`: When set to `true`, enables debug mode on the transaction executor and the script compiler. For any script that has been compiled and executed in this mode, debug logs will be output in order to facilitate MASM debugging ([these instructions](https://0xMiden.github.io/miden-vm/user_docs/assembly/debugging.html) can be used to do so). This variable can be overridden by the `--debug` CLI flag. + +### 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.14/builder/tools/clients/rust-client/cli/cli-troubleshooting.md b/versioned_docs/version-0.14/builder/tools/clients/rust-client/cli/cli-troubleshooting.md new file mode 100644 index 00000000..9cfd9b0e --- /dev/null +++ b/versioned_docs/version-0.14/builder/tools/clients/rust-client/cli/cli-troubleshooting.md @@ -0,0 +1,147 @@ +--- +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 with debug output when troubleshooting: add `--debug` or set `MIDEN_DEBUG=true`. +- Run `miden-client sync` to refresh local state after errors involving missing data or outdated heights. + +### Enable debug output + +- CLI flag: `miden-client --debug ...` (overrides `MIDEN_DEBUG`) +- Environment variable: `MIDEN_DEBUG=true` + +When enabled, the transaction executor and script compiler emit debug logs that help diagnose MASM-level issues (you can also consult the Miden VM debugging instructions). + +### Typical CLI outputs when debugging + +```sh +# Enable debug output for a command +miden-client --debug send --sender --target --asset 100:: + +# Force non-interactive submission (e.g., CI) +miden-client send --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: Run with `--debug` to collect execution logs. 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://0xmiden.github.io/miden-docs/imported/miden-base/src/transaction.html#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. Re-run with `--debug` or `MIDEN_DEBUG=true` for richer logs. +2. Verify `rpc.endpoint` connectivity and timeouts. +3. Run `miden-client sync` to refresh local headers/notes. +4. 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 + +- CLI debug flag and environment variable are documented in `CLI` and `Config` docs. +- Common error enums originate from the client and RPC layers. +- Protocol lifecycle: [Miden book — Transaction lifecycle](https://0xmiden.github.io/miden-docs/imported/miden-base/src/transaction.html#transaction-lifecycle) diff --git a/versioned_docs/version-0.14/builder/tools/clients/rust-client/cli/index.md b/versioned_docs/version-0.14/builder/tools/clients/rust-client/cli/index.md new file mode 100644 index 00000000..d9a0bbf0 --- /dev/null +++ b/versioned_docs/version-0.14/builder/tools/clients/rust-client/cli/index.md @@ -0,0 +1,410 @@ +--- +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 +``` + +Optionally, you can include the `--debug` flag to run the command with debug mode, which enables debug output logs from scripts that were compiled in this mode: + +```sh +miden-client --debug +``` + +Note that the debug flag overrides the `MIDEN_DEBUG` environment variable. + +## Commands + +### `init` + +Creates a configuration file for the client 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 18.203.155.106 +# You can specify the port +miden-client init --network 18.203.155.106:8080 +# You can also specify the protocol (http/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 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](https://github.com/0xMiden/miden-client/docs/typedoc/rust-client/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` | +| `--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. + +### `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: + +- `--storage-mode `: Used to select the storage mode of the account (private if not specified). It may receive "private" or "public". +- `--mutable`: Makes the account code mutable (it's immutable by default). +- `--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: + +- `--storage-mode `: Specifies the storage mode of the account. It accepts either "private" or "public", with "private" as the default. +- `--account-type `: Specifies the type of account to create. Accepted values are: + - `fungible-faucet` + - `non-fungible-faucet` + - `regular-account-immutable-code` + - `regular-account-updatable-code` +- `--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 storage, immutable, no extra components) +miden-client new-wallet + +# Create a new wallet with public storage and a mutable code +miden-client new-wallet --storage-mode public --mutable + +# 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 +miden-client new-account --account-type fungible-faucet --packages packages/basic-fungible-faucet.masp + +# Create a fungible faucet with preset fields +miden-client new-account --account-type fungible-faucet --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 +``` + +### `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. + +#### `send` + +Sends 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 send --sender --target --asset :: --note-type ` + +#### `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 ` + +### `address` + +View and manage addresses. + +#### Action Subcommands + +| Subcommand | Description | +| -------------------------------- | ---------------------------------------------------------------------------------------| +| `list ` | List all addresses or only for the specified account ID (default command) | +| `add ` | Bind an address for an interface for the specified account ID with optional tag length | +| `remove
` | Remove an address for the specified account ID | + +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 as a mandatory argument, and also the interface of the address, this values can be: +- `BasicWallet`: The basic wallet interface. + +Note: the `Unspecified` denotes an address not bound to any interface, it's the default address for every account created. + +```sh +miden-client address add 0x17f13f4f83a8e8100c19d2961dfda2 BasicWallet 10 +``` + +```sh +miden-client address remove 0x17f13f4f83a8e8100c19d2961dfda2 mlcl1qple0ejnutx8zyp0cm0pme9wjfgqz0u9djq +``` + +#### Tips + +For `send` and `consume-notes`, you can omit the `--sender` and `--account` flags to use the default account defined in the [config](https://github.com/0xMiden/miden-client/docs/typedoc/rust-client/cli-config.md). If you omit the flag but have no default account defined in the config, 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 send --sender 0x80519a1c5e3680fc --target 0x8fd4b86a6387f8d8 --asset 100::0xa99c5c8764d4e011 +``` + +You can do: + +```sh +miden-client send --sender 0x80519 --target 0x8fd4b --asset 100::0xa99c5c8764d4e011 +``` + +!!! 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 send --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"]}, ] +``` + +### `note-transport` + +Send and fetch private notes using the transport layer. diff --git a/versioned_docs/version-0.14/builder/tools/clients/rust-client/debugging.md b/versioned_docs/version-0.14/builder/tools/clients/rust-client/debugging.md new file mode 100644 index 00000000..d8a1f411 --- /dev/null +++ b/versioned_docs/version-0.14/builder/tools/clients/rust-client/debugging.md @@ -0,0 +1,96 @@ +--- +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). **Enabled by default.** | +| `testing` | `miden-client-cli` | Enables the `--offline` flag on `new-wallet`/`new-account` commands for node-less account creation. Not available in production builds. | + +### Building with features + +```bash +# Default build (DAP enabled) +cargo build -p miden-client-cli + +# With offline mode for testing +cargo build -p miden-client-cli --features testing + +# Without DAP (smaller binary) +cargo build -p miden-client-cli --no-default-features +``` + +If you build from source with default features disabled, 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`: + +``` +begin + push.1.2 + add + push.3 + mul +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 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 normally. +2. Instead of using the default `FastProcessor`, it creates a `DapExecutor` (from the `miden-debug` crate) which implements the `ProgramExecutor` trait. +3. The `DapExecutor` binds a TCP listener on the specified address and waits for a DAP client connection. +4. Once connected, the DAP client controls execution (continue, step, breakpoints, inspect state). +5. If the DAP client requests a restart, the client recompiles the script from disk and re-executes — enabling an edit-and-continue workflow. diff --git a/versioned_docs/version-0.14/builder/tools/clients/rust-client/design.md b/versioned_docs/version-0.14/builder/tools/clients/rust-client/design.md new file mode 100644 index 00000000..a9c783b9 --- /dev/null +++ b/versioned_docs/version-0.14/builder/tools/clients/rust-client/design.md @@ -0,0 +1,80 @@ +--- +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 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://0xmiden.github.io/miden-docs/imported/miden-vm/src/intro/main.html) to execute transactions. All transactions run within the [transaction kernel](https://0xmiden.github.io/miden-docs/imported/miden-base/src/transaction.html). + +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 performs fast static checks (e.g. checking the inputs for well known notes) and also dry runs of consumption transactions. + +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. + +## 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.14/builder/tools/clients/rust-client/examples.md b/versioned_docs/version-0.14/builder/tools/clients/rust-client/examples.md new file mode 100644 index 00000000..7118159a --- /dev/null +++ b/versioned_docs/version-0.14/builder/tools/clients/rust-client/examples.md @@ -0,0 +1,52 @@ +--- +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://0xmiden.github.io/miden-docs/imported/miden-client/src/get-started/prerequisites.html#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. + +```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.14/builder/tools/clients/rust-client/features.md b/versioned_docs/version-0.14/builder/tools/clients/rust-client/features.md new file mode 100644 index 00000000..474e0455 --- /dev/null +++ b/versioned_docs/version-0.14/builder/tools/clients/rust-client/features.md @@ -0,0 +1,26 @@ +--- +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). + +### 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. diff --git a/versioned_docs/version-0.14/builder/tools/clients/rust-client/get-started/_category_.yml b/versioned_docs/version-0.14/builder/tools/clients/rust-client/get-started/_category_.yml new file mode 100644 index 00000000..b280e681 --- /dev/null +++ b/versioned_docs/version-0.14/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.14/builder/tools/clients/rust-client/get-started/create-account-use-faucet.md b/versioned_docs/version-0.14/builder/tools/clients/rust-client/get-started/create-account-use-faucet.md new file mode 100644 index 00000000..95e352e1 --- /dev/null +++ b/versioned_docs/version-0.14/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://0xMiden.github.io/miden-docs/miden-client/cli-reference.html) 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, and you should see `Miden 0.10.0`. + +## Create a new Miden account + +1. Create a new account of type `mutable` using the following command: + + ```sh + miden-client new-wallet --mutable + ``` + +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 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://0xMiden.github.io/miden-docs/miden-client/index.html). + +## 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? Only execute the `miden-client` command in the folder where your `miden-client.toml` is located. diff --git a/versioned_docs/version-0.14/builder/tools/clients/rust-client/get-started/index.md b/versioned_docs/version-0.14/builder/tools/clients/rust-client/get-started/index.md new file mode 100644 index 00000000..0574c186 --- /dev/null +++ b/versioned_docs/version-0.14/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.14/builder/tools/clients/rust-client/get-started/p2p-private.md b/versioned_docs/version-0.14/builder/tools/clients/rust-client/get-started/p2p-private.md new file mode 100644 index 00000000..3eab6ec9 --- /dev/null +++ b/versioned_docs/version-0.14/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://0xMiden.github.io/miden-docs/miden-client/cli-reference.html) for clarifications. +::: + +1. Create a second account to send funds with. Previously, we created a type `mutable` account (`Account A`). Now, create another `mutable` (`Account B`) using the following command: + + ```sh + miden-client new-wallet --mutable + ``` + +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 send --sender --target --asset 50:: --note-type private + ``` + + :::note + The faucet account ID can be found on the [Miden faucet website](https://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://0xMiden.github.io/miden-docs/miden-client/index.html). + +## 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.14/builder/tools/clients/rust-client/get-started/p2p-public.md b/versioned_docs/version-0.14/builder/tools/clients/rust-client/get-started/p2p-public.md new file mode 100644 index 00000000..17f118e8 --- /dev/null +++ b/versioned_docs/version-0.14/builder/tools/clients/rust-client/get-started/p2p-public.md @@ -0,0 +1,97 @@ +--- +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://0xMiden.github.io/miden-docs/miden-base/architecture/notes.html#note-storage-mode). + +:::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://0xMiden.github.io/miden-docs/miden-client/cli-reference.html) 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. On the new client, create a new [basic account](https://0xMiden.github.io/miden-docs/miden-base/architecture/accounts.html): + + ```sh + miden-client new-wallet --mutable -s public + ``` + + We refer to this account as _Account C_. Note that we set the account's storage mode to `public`, which means that the account details are public and its latest state can be retrieved from the node. + +3. 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 send --sender --target --asset 50:: --note-type public + ``` + + :::note + The faucet account ID can be found on the [Miden faucet website](https://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.14/builder/tools/clients/rust-client/index.md b/versioned_docs/version-0.14/builder/tools/clients/rust-client/index.md new file mode 100644 index 00000000..10eb06f7 --- /dev/null +++ b/versioned_docs/version-0.14/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.14/builder/tools/clients/rust-client/install-and-run.md b/versioned_docs/version-0.14/builder/tools/clients/rust-client/install-and-run.md new file mode 100644 index 00000000..1915b161 --- /dev/null +++ b/versioned_docs/version-0.14/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.88. + +## 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.14/builder/tools/clients/rust-client/library.md b/versioned_docs/version-0.14/builder/tools/clients/rust-client/library.md new file mode 100644 index 00000000..0863b23e --- /dev/null +++ b/versioned_docs/version-0.14/builder/tools/clients/rust-client/library.md @@ -0,0 +1,139 @@ +--- +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.11" } +``` + +## 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: debug mode via .in_debug_mode(DebugMode::Enabled) + .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::RegularAccountImmutableCode) + .storage_mode(AccountStorageMode::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 an public account, you can specify `AccountStorageMode::Public` like so: + +```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::RegularAccountImmutableCode) + .storage_mode(AccountStorageMode::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. + +## 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. diff --git a/versioned_docs/version-0.14/builder/tools/clients/theme/Admonition/Icon/Danger.tsx b/versioned_docs/version-0.14/builder/tools/clients/theme/Admonition/Icon/Danger.tsx new file mode 100644 index 00000000..ab14d7de --- /dev/null +++ b/versioned_docs/version-0.14/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.14/builder/tools/clients/theme/Admonition/Icon/Info.tsx b/versioned_docs/version-0.14/builder/tools/clients/theme/Admonition/Icon/Info.tsx new file mode 100644 index 00000000..59e48a52 --- /dev/null +++ b/versioned_docs/version-0.14/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.14/builder/tools/clients/theme/Admonition/Icon/Note.tsx b/versioned_docs/version-0.14/builder/tools/clients/theme/Admonition/Icon/Note.tsx new file mode 100644 index 00000000..d7c524b3 --- /dev/null +++ b/versioned_docs/version-0.14/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.14/builder/tools/clients/theme/Admonition/Icon/Tip.tsx b/versioned_docs/version-0.14/builder/tools/clients/theme/Admonition/Icon/Tip.tsx new file mode 100644 index 00000000..219bb8d0 --- /dev/null +++ b/versioned_docs/version-0.14/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.14/builder/tools/clients/theme/Admonition/Icon/Warning.tsx b/versioned_docs/version-0.14/builder/tools/clients/theme/Admonition/Icon/Warning.tsx new file mode 100644 index 00000000..f96398d1 --- /dev/null +++ b/versioned_docs/version-0.14/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.14/builder/tools/clients/theme/Admonition/Layout/index.tsx b/versioned_docs/version-0.14/builder/tools/clients/theme/Admonition/Layout/index.tsx new file mode 100644 index 00000000..7b2c170d --- /dev/null +++ b/versioned_docs/version-0.14/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.14/builder/tools/clients/theme/Admonition/Layout/styles.module.css b/versioned_docs/version-0.14/builder/tools/clients/theme/Admonition/Layout/styles.module.css new file mode 100644 index 00000000..88df7e63 --- /dev/null +++ b/versioned_docs/version-0.14/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.14/builder/tools/clients/theme/Admonition/Type/Caution.tsx b/versioned_docs/version-0.14/builder/tools/clients/theme/Admonition/Type/Caution.tsx new file mode 100644 index 00000000..b570a37a --- /dev/null +++ b/versioned_docs/version-0.14/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.14/builder/tools/clients/theme/Admonition/Type/Danger.tsx b/versioned_docs/version-0.14/builder/tools/clients/theme/Admonition/Type/Danger.tsx new file mode 100644 index 00000000..49901fa9 --- /dev/null +++ b/versioned_docs/version-0.14/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.14/builder/tools/clients/theme/Admonition/Type/Info.tsx b/versioned_docs/version-0.14/builder/tools/clients/theme/Admonition/Type/Info.tsx new file mode 100644 index 00000000..018e0a16 --- /dev/null +++ b/versioned_docs/version-0.14/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.14/builder/tools/clients/theme/Admonition/Type/Note.tsx b/versioned_docs/version-0.14/builder/tools/clients/theme/Admonition/Type/Note.tsx new file mode 100644 index 00000000..c99e0385 --- /dev/null +++ b/versioned_docs/version-0.14/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.14/builder/tools/clients/theme/Admonition/Type/Tip.tsx b/versioned_docs/version-0.14/builder/tools/clients/theme/Admonition/Type/Tip.tsx new file mode 100644 index 00000000..18604a5e --- /dev/null +++ b/versioned_docs/version-0.14/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.14/builder/tools/clients/theme/Admonition/Type/Warning.tsx b/versioned_docs/version-0.14/builder/tools/clients/theme/Admonition/Type/Warning.tsx new file mode 100644 index 00000000..61d9597b --- /dev/null +++ b/versioned_docs/version-0.14/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.14/builder/tools/clients/theme/Admonition/Types.tsx b/versioned_docs/version-0.14/builder/tools/clients/theme/Admonition/Types.tsx new file mode 100644 index 00000000..2a100190 --- /dev/null +++ b/versioned_docs/version-0.14/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.14/builder/tools/clients/theme/Admonition/index.tsx b/versioned_docs/version-0.14/builder/tools/clients/theme/Admonition/index.tsx new file mode 100644 index 00000000..8f4225da --- /dev/null +++ b/versioned_docs/version-0.14/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.14/builder/tools/clients/web-client/_category_.yml b/versioned_docs/version-0.14/builder/tools/clients/web-client/_category_.yml new file mode 100644 index 00000000..9d57d1b6 --- /dev/null +++ b/versioned_docs/version-0.14/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.14/builder/tools/clients/web-client/accounts.md b/versioned_docs/version-0.14/builder/tools/clients/web-client/accounts.md new file mode 100644 index 00000000..db231a4c --- /dev/null +++ b/versioned_docs/version-0.14/builder/tools/clients/web-client/accounts.md @@ -0,0 +1,292 @@ +--- +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 two small enums: + +| Kind | Accepted values | Meaning | +| --- | --- | --- | +| `AccountType.FungibleFaucet` | `0` | Mintable token source | +| `AccountType.NonFungibleFaucet` | `1` | Non-fungible token source | +| `AccountType.RegularAccountImmutableCode` | `2` | Immutable wallet or contract | +| `AccountType.RegularAccountUpdatableCode` | `3` | Mutable wallet or contract (default) | +| `auth` field | `"falcon"` \| `"ecdsa"` | Signing scheme — Falcon is the default | +| `storage` field | `"public"` \| `"private"` \| `"network"` | Visibility and persistence mode | + +All snippets below use these names. The SDK also ships friendly aliases (`AccountType.MutableWallet`, `AccountType.ImmutableContract`, etc.) but prefer the canonical names above when writing TypeScript — the aliases currently fail strict type-checking in some builds. + +## Create + +### Wallet + +```typescript +import { MidenClient, AccountType } from "@miden-sdk/miden-sdk"; + +const client = await MidenClient.createTestnet(); + +// Default: mutable wallet, private storage, Falcon auth +const wallet = await client.accounts.create(); + +// With explicit options +const wallet2 = await client.accounts.create({ + type: AccountType.RegularAccountImmutableCode, // immutable wallet + 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, AccountType } from "@miden-sdk/miden-sdk"; + +const client = await MidenClient.createTestnet(); + +const faucet = await client.accounts.create({ + type: AccountType.FungibleFaucet, + symbol: "TEST", + decimals: 8, + maxSupply: 10_000_000n, // number | bigint +}); + +const faucet2 = await client.accounts.create({ + type: AccountType.FungibleFaucet, + 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, + AccountType, + 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") + + pub proc get_count + push.COUNTER_SLOT[0..2] exec.active_account::get_item + exec.sys::truncate_stack + end + + 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({ + type: AccountType.RegularAccountImmutableCode, // immutable contract + 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, + AccountType, +} 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), + ) + .accountType(AccountType.RegularAccountImmutableCode) + .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. +const account = await client.accounts.getOrImport( + "mtst1arjemrxne8lj5qz4mg9c8mtyxg954483", +); +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 one or more bech32 addresses with an account. Useful when your UI lets users alias accounts by a human-readable string. + +## 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("mtst1arjemrxne8lj5qz4mg9c8mtyxg954483"); // bech32 + +// 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 + type: AccountType.RegularAccountUpdatableCode, // mutable wallet + 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 on-chain, 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 `"Account not found: 0x..."` when the account is missing. +- `import({ seed, ... })` on a private account throws at resolve time — private state isn't recoverable from a seed. + +## 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.14/builder/tools/clients/web-client/compile.md b/versioned_docs/version-0.14/builder/tools/clients/web-client/compile.md new file mode 100644 index 00000000..64749ebd --- /dev/null +++ b/versioned_docs/version-0.14/builder/tools/clients/web-client/compile.md @@ -0,0 +1,225 @@ +--- +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, 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") + + pub proc get_count + push.COUNTER_SLOT[0..2] exec.active_account::get_item + exec.sys::truncate_stack + end + + 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, + 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. +- `slots` — initial storage slots. Use the `StorageSlot` helpers (`emptyValue`, etc.). +- `supportAllTypes` — defaults to `true`. When `true`, the compiler auto-injects an auth-kernel invocation so the component accepts the standard set of input types for authenticated transactions. Set to `false` if your component already invokes an auth kernel procedure itself, or intentionally omits one. + +## 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 + begin + push.0 + exec.sys::truncate_stack + end + `, +}); +``` + +If your script needs to call into an external contract (as in the FPI section below), you must pass that contract's code 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 + begin + call.my_contract::do_something + exec.sys::truncate_stack + end + `, + libraries: [ + { + namespace: "external_contract::my_contract", + code: myContractCode, + linking: Linking.Dynamic, // default + }, + ], +}); +``` + +Each 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. | + +### Linking modes + +| Value | Behaviour | When to use | +| --- | --- | --- | +| `Linking.Dynamic` (default) | Links via DYNCALL at prove time. The foreign contract's on-chain code is fetched by the prover. | FPI — foreign contract lives on-chain. | +| `Linking.Static` | Inlines library code into the script at compile time. | Off-chain 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 + + begin + # 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 follow the same `{ namespace, code, linking? }` shape 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, + 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 + begin + push.${getCountHash} + push.${counterAccountId.suffix()} + push.${counterAccountId.prefix()} + 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, + AccountType, + 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, + 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({ + type: AccountType.RegularAccountImmutableCode, + seed, + auth, + components: [component], +}); + +await client.sync(); + +// 3. Compile the transaction script +const script = await client.compile.txScript({ + code: ` + use external_contract::counter_contract + begin + call.counter_contract::increment_count + end + `, + libraries: [ + { namespace: "external_contract::counter_contract", code: counterCode }, + ], +}); + +// 4. Execute +const { txId } = await client.transactions.execute({ + account: contract.id(), + script, +}); + +console.log("Tx:", txId.toHex()); +``` diff --git a/versioned_docs/version-0.14/builder/tools/clients/web-client/index.md b/versioned_docs/version-0.14/builder/tools/clients/web-client/index.md new file mode 100644 index 00000000..fe38017d --- /dev/null +++ b/versioned_docs/version-0.14/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 browser-focused toolkit for the Miden network. It wraps the Rust client, compiles to WebAssembly, and exposes a typed JavaScript API through the `MidenClient` class. Use it from web apps, wallets, dApps, service workers, Node servers — any JavaScript runtime that supports Web Workers and WebAssembly. + +## Capabilities + +- Read and write on-chain 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) │ +│ │ +│ Runs prove / execute on a dedicated │ +│ Web Worker to keep the main thread responsive │ +└────────────────────────────────────────────────┘ +``` + +The SDK is built from the `web-client` Rust crate in [0xMiden/miden-client](https://github.com/0xMiden/miden-client), compiled with `wasm-bindgen`, and bundled with the WASM module, JavaScript bindings, and a dedicated Web Worker script. + +## Resource management + +Each `MidenClient` instance 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 the worker. + +```typescript +import { MidenClient } from "@miden-sdk/miden-sdk"; + +const client = await MidenClient.createTestnet(); + +// ... use the client ... + +// Free the Web Worker 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()`, every subsequent method call throws `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. v0.14 introduces `MidenClient` with resource-based namespaces (`client.accounts`, `client.transactions`, …). See the [v0.13 → v0.14 Web SDK migration guide](../../../migration/07-client-changes.md) for the full delta. diff --git a/versioned_docs/version-0.14/builder/tools/clients/web-client/notes.md b/versioned_docs/version-0.14/builder/tools/clients/web-client/notes.md new file mode 100644 index 00000000..fc44c3e4 --- /dev/null +++ b/versioned_docs/version-0.14/builder/tools/clients/web-client/notes.md @@ -0,0 +1,145 @@ +--- +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"` — on-chain, consumable. +- `"consumed"` — already spent. +- `"expected"` — the client expects this note to arrive. +- `"processing"` — mid-consume. +- `"unverified"` — on-chain, awaiting local verification. + +## 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 noteId = await client.notes.import(noteFile); +console.log("Imported:", noteId.toString()); + +// 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 }); +``` + +`NoteExportFormat`: + +- **`Id`** — just the note ID. Only works for public notes. +- **`Full`** — complete note data plus inclusion proof. Requires the note to have an on-chain inclusion proof. +- **`Details`** — note ID, metadata, and creation block. + +## Note transport (private notes) + +Private notes are delivered through the Miden note transport service. The sender emits a note with `type: "private"`; the recipient fetches it from the transport network. + +```typescript +// Send a private note +await client.notes.sendPrivate({ + note: "0xnote...", // NoteInput + to: "mtst1recipient...", // recipient AccountRef +}); + +// Fetch — default is incremental (paginated) +await client.notes.fetchPrivate(); + +// Or fetch everything at once (initial-setup scenarios) +await client.notes.fetchPrivate({ mode: "all" }); + +// Now inspect the inbox +const notes = await client.notes.list(); +console.log(`Fetched ${notes.length} notes`); +``` + +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.14/builder/tools/clients/web-client/setup.md b/versioned_docs/version-0.14/builder/tools/clients/web-client/setup.md new file mode 100644 index 00000000..4f1f3364 --- /dev/null +++ b/versioned_docs/version-0.14/builder/tools/clients/web-client/setup.md @@ -0,0 +1,158 @@ +--- +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) with WebAssembly and Web Worker support. It also runs under Node 20+ when the host provides those primitives. + +## 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 in-memory 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 — the SDK has to load its WebAssembly module and spin up a Web Worker before the client is usable. + +## `ClientOptions` reference + +All four factories accept the same `ClientOptions` shape. The differences are in what each factory pre-fills before the options are applied. + +### 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 browser; 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 }` | External keystore callbacks. Leave unset to use the built-in keystore. | + +### 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)`_ | ← | ← | ← | +| `createMock(opts?)` | _(no network)_ | _(dummy proving)_ | _(in-memory)_ | _(manual)_ | + +`create()` without an `rpcUrl` is not a separate "custom" client — it forwards its options to `createTestnet()`. If you want a no-prover, no-autosync client against localhost, 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); +``` + +The caller is responsible for retaining `auth` as long as the account is in use: the client holds a reference for signing, but the secret material only exists on the caller side until it is handed to the keystore. + +See [Accounts](./accounts.md) for full examples covering wallets, contracts, and faucets. For advanced 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(); + await client.sync(); + + 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.14/builder/tools/clients/web-client/sync.md b/versioned_docs/version-0.14/builder/tools/clients/web-client/sync.md new file mode 100644 index 00000000..2f32a89a --- /dev/null +++ b/versioned_docs/version-0.14/builder/tools/clients/web-client/sync.md @@ -0,0 +1,88 @@ +--- +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()` + +Pulls 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 on-chain state advanced. + +## Sync with timeout + +```typescript +// 30-second timeout +const summary = await client.sync({ timeout: 30_000 }); +``` + +## 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 = 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.14/builder/tools/clients/web-client/testing.md b/versioned_docs/version-0.14/builder/tools/clients/web-client/testing.md new file mode 100644 index 00000000..0401fa15 --- /dev/null +++ b/versioned_docs/version-0.14/builder/tools/clients/web-client/testing.md @@ -0,0 +1,132 @@ +--- +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, AccountType } from "@miden-sdk/miden-sdk"; + +const client = await MidenClient.createMock(); + +const wallet = await client.accounts.create(); +const faucet = await client.accounts.create({ + type: AccountType.FungibleFaucet, + symbol: "TEST", + decimals: 8, + maxSupply: 10_000_000n, +}); + +client.proveBlock(); +await client.sync(); + +await client.transactions.mint({ + account: faucet, + to: wallet, + amount: 1000n, +}); + +client.proveBlock(); +await client.sync(); + +const result = await client.transactions.consumeAll({ account: wallet }); +console.log(`Consumed ${result.consumed} notes`); + +client.proveBlock(); +await client.sync(); + +const balance = await client.accounts.getBalance(wallet, faucet); +console.log(`Balance: ${balance}`); // 1000n +``` + +## 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()) { + client.proveBlock(); +} + +const chainBytes = client.serializeMockChain(); +const transportBytes = 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 = setup.serializeMockChain(); +const transportState = 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 { MidenClient } from "@miden-sdk/miden-sdk"; + +const client = await MidenClient.createMock(); + +await client.notes.sendPrivate({ + note: "0xnote...", + to: "mtst1recipient...", +}); + +await client.notes.fetchPrivate(); + +const notes = await client.notes.list(); +console.log(`Received ${notes.length} notes`); +``` diff --git a/versioned_docs/version-0.14/builder/tools/clients/web-client/transactions.md b/versioned_docs/version-0.14/builder/tools/clients/web-client/transactions.md new file mode 100644 index 00000000..bde5520a --- /dev/null +++ b/versioned_docs/version-0.14/builder/tools/clients/web-client/transactions.md @@ -0,0 +1,379 @@ +--- +title: Transactions +sidebar_position: 4 +--- + +# Transactions + +`client.transactions` is the resource namespace for everything that mutates on-chain state: sending, minting, consuming, swapping, running custom scripts, and inspecting history. Every mutation method handles 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 +}); + +// Single note also accepted directly, no array needed +await client.transactions.consume({ + account: wallet, + notes: noteId1, +}); +``` + +### `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 (dry run) + +Run any of the simplified operations as a dry-run to inspect its effects without submitting to the network. The return type is a `TransactionSummary`. + +```typescript +const summary = await client.transactions.preview({ + operation: "send", + account: wallet, + to: recipient, + token: faucet, + amount: 100n, +}); +``` + +`operation` accepts `"send"`, `"mint"`, `"consume"`, and `"swap"`. + +## 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 + begin + call.counter_contract::increment_count + end + `, + libraries: [ + { + namespace: "external_contract::counter_contract", + code: counterContractCode, + }, + ], +}); + +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, + 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 + begin + push.${getCountHash} + push.${counterAccount.id().suffix()} + push.${counterAccount.id().prefix()} + call.count_reader_contract::copy_count + exec.sys::truncate_stack + end + `, + libraries: [ + { namespace: "external_contract::count_reader_contract", code: countReaderCode }, + ], +}); + +const { txId } = await client.transactions.execute({ + account: countReaderAccount.id(), + script, + foreignAccounts: [ + // Bare reference — client fetches storage requirements automatically + counterAccount.id(), + // Or with 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 on-chain state — similar to `eth_call` in Ethereum. + +```typescript +const script = await client.compile.txScript({ + code: ` + use external_contract::counter_contract + begin + call.counter_contract::get_count + end + `, + libraries: [ + { namespace: "external_contract::counter_contract", code: counterContractCode }, + ], +}); + +const stack = await client.transactions.executeProgram({ + account: contractAccount.id(), + script, + foreignAccounts: [counterAccount.id()], +}); + +// stack is a FeltArray — 16 elements representing the final stack +const count = stack.get(0).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 custom 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 TS arrays. This is unusual but required by the underlying wasm-bindgen interface: the array types take ownership of their elements and are explicitly disposable. + +```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() + .withCustomScript(transactionScript) + .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 submission + .build(); + +await client.transactions.submit(wallet, request); +``` + +`withExpirationDelta()` composes with `withCustomScript()` — the builder applies the expiration at the request level regardless of how the script was provided. You can still set expiration inside the script itself when you need a different rule; the two paths don't interact. + +## 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()`. + +```typescript +// 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] }); + +// By expiration +const expired = await client.transactions.list({ expiredBefore: 1000 }); +``` + +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().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.14/builder/tools/index.md b/versioned_docs/version-0.14/builder/tools/index.md new file mode 100644 index 00000000..5e0d4a94 --- /dev/null +++ b/versioned_docs/version-0.14/builder/tools/index.md @@ -0,0 +1,34 @@ +--- +title: Tools +description: "Developer tools for building on and interacting with the Miden network — clients, 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, 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 rollup integration — accounts, transactions, notes, proving. + + + Browser-based client for managing accounts and transactions from a web app. + + + Hooks and components for Miden dApps. + + + +## Environments + + + + Interactive environment for writing and testing Miden Assembly programs. + + + Live Miden testnet endpoints — status, block explorer (MidenScan), RPC, faucet, remote prover. + + diff --git a/versioned_docs/version-0.14/builder/tools/network.md b/versioned_docs/version-0.14/builder/tools/network.md new file mode 100644 index 00000000..e0acb99f --- /dev/null +++ b/versioned_docs/version-0.14/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`, `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. Check status for the current faucet 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.14/builder/tools/playground.md b/versioned_docs/version-0.14/builder/tools/playground.md new file mode 100644 index 00000000..2d53bfa6 --- /dev/null +++ b/versioned_docs/version-0.14/builder/tools/playground.md @@ -0,0 +1,50 @@ +--- +title: Playground +sidebar_position: 3 +description: "Browser-based environment to write, compile, and execute Miden Assembly programs — no local tooling required." +--- + +# Miden Playground + +An interactive browser environment for writing, compiling, and executing Miden Assembly (MASM) programs. No installation required — prototype account code, test note scripts, and experiment with VM instructions straight from a URL. + + + + Launch the browser IDE and start writing MASM immediately. + + + Instruction set, stack semantics, chiplets, and assembler behaviour. + + + +## What you can do + + + + Syntax-highlighted editor with inline error reporting for the Miden assembler. + + + Run programs against the Miden VM in-browser and inspect the resulting stack and memory. + + + Shareable URLs with embedded code for reproducing bugs or teaching examples. + + + + +The Playground shines for learning MASM and for quick prototyping. For anything bigger than a snippet — components, storage, note dispatch, transaction flows — move to a `miden new` Rust project locally. 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. + + + New `word(...)` / `event(...)` constants, `std::math::u128`, and other MASM-level deltas. + + + Accounts, notes, transactions, and the Rust SDK surface. + + diff --git a/versioned_docs/version-0.14/builder/tutorials/_category_.json b/versioned_docs/version-0.14/builder/tutorials/_category_.json new file mode 100644 index 00000000..e3af22eb --- /dev/null +++ b/versioned_docs/version-0.14/builder/tutorials/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Tutorials", + "position": 1 +} diff --git a/versioned_docs/version-0.14/builder/tutorials/components/CodeSdkTabs.module.css b/versioned_docs/version-0.14/builder/tutorials/components/CodeSdkTabs.module.css new file mode 100644 index 00000000..391d6d32 --- /dev/null +++ b/versioned_docs/version-0.14/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.14/builder/tutorials/components/CodeSdkTabs.tsx b/versioned_docs/version-0.14/builder/tutorials/components/CodeSdkTabs.tsx new file mode 100644 index 00000000..48b36ba2 --- /dev/null +++ b/versioned_docs/version-0.14/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.14/builder/tutorials/components/index.ts b/versioned_docs/version-0.14/builder/tutorials/components/index.ts new file mode 100644 index 00000000..33f91fd9 --- /dev/null +++ b/versioned_docs/version-0.14/builder/tutorials/components/index.ts @@ -0,0 +1 @@ +export { default as CodeSdkTabs } from './CodeSdkTabs'; diff --git a/versioned_docs/version-0.14/builder/tutorials/helpers/_category_.json b/versioned_docs/version-0.14/builder/tutorials/helpers/_category_.json new file mode 100644 index 00000000..b6d47724 --- /dev/null +++ b/versioned_docs/version-0.14/builder/tutorials/helpers/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Guides", + "position": 4 +} diff --git a/versioned_docs/version-0.14/builder/tutorials/helpers/debugging.md b/versioned_docs/version-0.14/builder/tutorials/helpers/debugging.md new file mode 100644 index 00000000..ed50001d --- /dev/null +++ b/versioned_docs/version-0.14/builder/tutorials/helpers/debugging.md @@ -0,0 +1,59 @@ +--- +sidebar_position: 2 +title: "Debugging Guide" +description: "Learn how to debug Miden Rust contracts using assert_eq and cycle counts." +--- + +# Debugging Guide + +Miden contracts don't support traditional debugging tools like console.log or print statements. Instead, you can use `assert_eq` statements to check values during execution. + +## 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. +::: + +## Debugging with Cycle Counts + +When your code fails, the error output includes a **cycle count** indicating where execution stopped. You can use this to narrow down problems: + +1. **Note the cycle count** when your code fails +2. **Place an `assert_eq`** before the code you suspect is failing +3. **Run again** and check the result: + - If the assertion fails at an **earlier cycle count**: the value you're checking is wrong + - If the assertion passes and fails at the **same cycle count**: the value is correct, the problem is elsewhere + +### Example + +```rust +pub fn withdraw(&mut self, depositor: AccountId, amount: Felt) { + let balance = self.get_balance(depositor); + + // Debug: Check if balance is what you expect + assert_eq(balance, felt!(1000)); + + // If the above passes, the problem is below this line + // If it fails, the balance isn't what you expected + + let new_balance = balance - amount; + self.balances.set(key, new_balance); +} +``` + +By moving the `assert_eq` statement around, you can isolate which value is incorrect. + +## Limitations + +- No console.log or print debugging in contract code +- `assert_eq` only works with `Felt` values +- This is currently the primary debugging technique available diff --git a/versioned_docs/version-0.14/builder/tutorials/helpers/pitfalls.md b/versioned_docs/version-0.14/builder/tutorials/helpers/pitfalls.md new file mode 100644 index 00000000..23ae65c7 --- /dev/null +++ b/versioned_docs/version-0.14/builder/tutorials/helpers/pitfalls.md @@ -0,0 +1,505 @@ +--- +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. + +## Felt Comparison Operators + +### Problem + +Direct comparison operators (`<`, `>`, `<=`, `>=`) on `Felt` values produce incorrect results. + +```rust +// WRONG: This does NOT work correctly +let a = Felt::new(100); +let b = Felt::new(200); +if a < b { // May produce unexpected results! + // ... +} +``` + +### Solution + +Always convert Felt values to `u64` before comparing: + +```rust +// CORRECT: Convert to u64 first +let a = Felt::new(100); +let b = Felt::new(200); +if a.as_u64() < b.as_u64() { + // Works correctly +} +``` + +### Example from Bank Contract + +```rust title="contracts/bank-account/src/lib.rs" +// Validating deposit amount +let amount = asset.unwrap_fungible().amount().as_u64(); + +// Use u64 comparison +assert!( + amount <= MAX_DEPOSIT_AMOUNT, // MAX_DEPOSIT_AMOUNT is u64 + "Deposit exceeds maximum" +); +``` + +:::warning Always Use .as_u64() +Any time you compare Felt values, convert them first. This applies to: +- Amount comparisons +- Balance checks +- Index comparisons +- Any numeric ordering +::: + +--- + +## Stack Limit (16 Elements) + +### Problem + +The Miden VM stack only allows direct access to the first 16 elements. Complex functions with many local variables trigger this error: + +``` +invalid stack index: only the first 16 elements on the stack are directly accessible +``` + +This may also appear as: +``` +values not found in advice provider +``` + +### Solution + +**1. Reduce local variables:** + +```rust +// WRONG: Too many local variables +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(); + // ... more variables cause stack overflow +} + +// CORRECT: Use values directly or minimize locals +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 +// WRONG: One large function +fn do_everything(&mut self, a: Word, b: Word, c: Word) { + // Many operations touching all parameters... +} + +// CORRECT: Split into stages +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); +} +``` + +--- + +## Function Argument Limit (4 Words) + +### Problem + +Miden functions can receive at most 4 Words (16 Felts) as arguments: + +``` +error: expected at most 4 words of arguments +``` + +```rust +// WRONG: Too many arguments +fn process( + &mut self, + depositor: AccountId, // ~1 Word + asset: Asset, // 1 Word + serial_num: Word, // 1 Word + tag: Felt, // 1 Felt + note_type: Felt, // 1 Felt + extra_data: Word, // 1 Word - EXCEEDS LIMIT! +) { + // ... +} +``` + +### Solution + +**1. Make sure to only pass 4 Words to functions:** + +```rust +// CORRECT: Only pass 4 Words +fn process( + &mut self, + depositor: AccountId, // ~1 Word + asset: Asset, // 1 Word + serial_num: Word, // 1 Word + params: Word, // [tag, note_type, 0, 0] - 1 Word +) { + let tag = params[0]; + let note_type = params[1]; + // ... +} +``` + +**2. Use note storage for passing data:** + +For note scripts, pass complex data via `active_note::get_storage()`: + +```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 + +Arrays passed from Rust to the Miden VM are received 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 inputs +- Working with asset data + +**Example: Storage Key Construction** + +```rust +// Balance key format in Rust +let key = Word::from([ + depositor.prefix().as_felt(), // Position 0 in Rust + depositor.suffix(), // Position 1 + faucet.prefix().as_felt(), // Position 2 + faucet.suffix(), // Position 3 +]); + +// When the VM processes this, it sees: +// [faucet.suffix, faucet.prefix, depositor.suffix, depositor.prefix] +``` + +**Example: Asset Structure** + +```rust +// Asset Word layout (Rust perspective) +// [amount, 0, faucet_suffix, faucet_prefix] + +let asset_word = Word::from([ + Felt::new(amount), // [0] amount + Felt::new(0), // [1] padding + faucet.id().suffix(), // [2] faucet suffix + faucet.id().prefix().as_felt(), // [3] faucet 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::new(100); +let withdrawal = Felt::new(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. The Rust compiler's default overflow mode is `Unchecked`, meaning it compiles directly to raw VM arithmetic operations. + +### Solution + +**Always validate before subtraction:** + +```rust +// CORRECT: Check balance before subtracting +let current_balance: Felt = self.balances.get(&key); +let withdraw_amount = withdraw_asset.inner[0]; + +// Validate that balance is sufficient +assert!( + current_balance.as_u64() >= withdraw_amount.as_u64(), + "Withdrawal amount exceeds available balance" +); + +// Only subtract after validation +let new_balance = current_balance - withdraw_amount; +``` + +### Example from Bank Contract + +```rust title="contracts/bank-account/src/lib.rs" +pub fn withdraw(&mut self, depositor: AccountId, withdraw_asset: Asset, /* ... */) { + let withdraw_amount = withdraw_asset.inner[0]; + + // 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_u64() >= withdraw_amount.as_u64(), + "Withdrawal amount exceeds available balance" + ); + + let new_balance = current_balance - withdraw_amount; + self.balances.set(key, new_balance); +} +``` + +:::danger Critical Security Issue +Failure to validate before subtraction can lead to: +- Users withdrawing more than their balance +- Balance values becoming astronomically large +- Complete loss of funds in the contract + +**Always check bounds before Felt subtraction operations.** +::: + +--- + +## Wallet Component Requirement + +### Problem + +The `active_note::add_assets_to_account()` function fails if the consuming account doesn't have the basic wallet component. + +``` +Error: Account does not support asset operations +``` + +### Solution + +Ensure accounts that receive assets via this function have wallet capability: + +```rust +use miden_client::account::component::BasicWallet; + +// When creating an account that needs to receive assets +let account = AccountBuilder::new(seed) + .with_component(BasicWallet) // Add wallet capability + .with_component(YourCustomComponent) + .build()?; +``` + +**Alternative: Use `native_account::add_asset()`** + +For account components, use the native account API instead: + +```rust +#[component] +impl Bank { + pub fn deposit(&mut self, depositor: AccountId, asset: Asset) { + // This works for any account - no wallet required + native_account::add_asset(asset); + + // Track balance in storage + self.update_balance(depositor, asset); + } +} +``` + +--- + +## 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" +#[component] +impl Bank { + /// Construct a balance key for a depositor and asset. + /// Key format: [depositor_prefix, depositor_suffix, faucet_prefix, faucet_suffix] + fn balance_key(&self, depositor: AccountId, faucet_id: AccountId) -> Word { + Word::from([ + depositor.prefix().as_felt(), + depositor.suffix(), + faucet_id.prefix().as_felt(), + faucet_id.suffix(), + ]) + } + + pub fn get_balance(&self, depositor: AccountId, faucet_id: AccountId) -> Felt { + let key = self.balance_key(depositor, faucet_id); + self.balances.get(&key) + } + + fn update_balance(&mut self, depositor: AccountId, faucet_id: AccountId, amount: Felt) { + let key = self.balance_key(depositor, faucet_id); + self.balances.set(key, 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 on-chain | +| 2 | Private | Note data is hidden (only hash on-chain) | + +```rust +// In note inputs or when creating output notes +let note_type = Felt::new(1); // Public note +// or +let note_type = Felt::new(2); // Private note +``` + +--- + +## P2ID Script Root + +### Problem + +When creating P2ID (Pay-to-ID) output notes, you need the script's MAST root. The v0.13 pattern of hardcoding the digest is fragile — it hashes under RPO, which v0.14 replaced with Poseidon2, and any future change to the P2ID script invalidates the constant silently. + +### Solution (v0.14) + +Carry the P2ID script root on the initiating note's storage and read it at runtime instead of hardcoding a value. This is the pattern used in the v0.14 `miden-bank` example: + +```rust title="contracts/bank-account/src/lib.rs" +// The withdraw-request note encodes the P2ID script root at storage slots +// 10..14 (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; + +// v0.14: ask miden-standards for the current P2ID script, take its MAST root. +let p2id_script_root: Word = P2idNote::script().root(); +``` + +:::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 against v0.14. Reading the root from `P2idNote::script().root()` (or the active note's storage for on-chain code) keeps the contract resilient to future script changes. +::: + +--- + +## Quick Reference Table + +| Pitfall | Symptom | Solution | +|---------|---------|----------| +| Felt comparison | Wrong comparison results | Use `.as_u64()` | +| Stack overflow | "16 elements" error | Reduce locals, split functions | +| Too many args | "4 words" error | Group into Words, use inputs | +| Array reversal | Wrong data order | Be consistent with construction | +| Felt underflow | Balance wraps to huge number | Validate before subtraction | +| 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 2 (Private) | + +:::tip View Complete Source +See these patterns in context in the [miden-bank repository](https://github.com/keinberger/miden-bank). +::: + +## 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.14/builder/tutorials/helpers/testing.md b/versioned_docs/version-0.14/builder/tutorials/helpers/testing.md new file mode 100644 index 00000000..ff2bdc58 --- /dev/null +++ b/versioned_docs/version-0.14/builder/tutorials/helpers/testing.md @@ -0,0 +1,610 @@ +--- +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/ + │ └── 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 = "2021" + +[lib] +path = "src/helpers.rs" + +[[test]] +name = "my_test" +path = "tests/my_test.rs" + +[dependencies] +anyhow = "1.0" +tokio = { version = "1", features = ["rt-multi-thread", "macros"] } + +# Miden dependencies +cargo-miden = { version = "0.7" } +miden-client = { version = "0.13", features = ["tonic", "testing"] } +miden-client-sqlite-store = { version = "0.13", package = "miden-client-sqlite-store" } +miden-core = { version = "0.20" } +miden-standards = { version = "0.13", default-features = false, features = ["testing"] } +miden-testing = "0.13" +miden-mast-package = { version = "0.20", default-features = false } +rand = { version = "0.9" } +``` + +## Building Contracts for Tests + +Use `cargo-miden` to build your contracts programmatically: + +```rust title="integration/src/helpers.rs" +use std::path::Path; +use anyhow::{bail, Context, Result}; +use cargo_miden::{run, OutputType}; +use miden_mast_package::Package; + +pub fn build_project_in_dir(dir: &Path, release: bool) -> Result { + let profile = if release { "--release" } else { "--debug" }; + let manifest_path = dir.join("Cargo.toml"); + let manifest_arg = manifest_path.to_string_lossy(); + + let args = vec![ + "cargo", "miden", "build", + profile, + "--manifest-path", &manifest_arg, + ]; + + let output = run(args.into_iter().map(String::from), OutputType::Masm) + .context("Failed to compile project")? + .context("Cargo miden build returned None")?; + + let artifact_path = match output { + cargo_miden::CommandOutput::BuildCommandOutput { output } => match output { + cargo_miden::BuildOutput::Masm { artifact_path } => artifact_path, + other => bail!("Expected Masm output, got {:?}", other), + }, + other => bail!("Expected BuildCommandOutput, got {:?}", other), + }; + + 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 +// Create a faucet with 1,000,000 total supply, decimals = 10 +let faucet = builder.add_existing_basic_faucet( + Auth::BasicAuth, + "TEST", // Token symbol + 1_000_000, // Total supply + Some(10), // Decimals +)?; +``` + +### Adding Wallet Accounts + +Create accounts with initial assets: + +```rust +use miden_client::asset::FungibleAsset; + +// Create a wallet with 100 tokens from the faucet +let sender = builder.add_existing_wallet_with_assets( + Auth::BasicAuth, + [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_client::account::{AccountStorageMode, AccountType, StorageSlot}; + +#[derive(Clone)] +pub struct AccountCreationConfig { + pub account_type: AccountType, + pub storage_mode: AccountStorageMode, + pub storage_slots: Vec, +} + +impl Default for AccountCreationConfig { + fn default() -> Self { + Self { + account_type: AccountType::RegularAccountImmutableCode, + storage_mode: AccountStorageMode::Public, + storage_slots: vec![], + } + } +} +``` + +### Creating Account from Package + +```rust +use miden_client::account::{StorageMap, StorageSlot, StorageSlotName}; +use std::sync::Arc; + +// Build the contract +let bank_package = Arc::new(build_project_in_dir( + Path::new("../contracts/bank-account"), + true, // release mode +)?); + +// Configure named storage slots +let initialized_slot = + StorageSlotName::new("miden::component::miden_bank_account::initialized") + .expect("Valid slot name"); +let balances_slot = + StorageSlotName::new("miden::component::miden_bank_account::balances") + .expect("Valid slot name"); + +let config = AccountCreationConfig { + storage_slots: vec![ + StorageSlot::with_value(initialized_slot, Word::default()), + StorageSlot::with_map( + balances_slot.clone(), + StorageMap::with_entries([]).expect("Empty storage map"), + ), + ], + ..Default::default() +}; + +// Create the account +let mut account = create_testing_account_from_package( + bank_package.clone(), + config, +).await?; + +// Add to MockChain +builder.add_account(account.clone())?; +``` + +## Creating Notes + +### Note Configuration + +```rust title="integration/src/helpers.rs" +use miden_client::note::{NoteAssets, NoteTag, NoteType}; +use miden_core::Felt; + +pub struct NoteCreationConfig { + pub note_type: NoteType, + pub tag: NoteTag, + pub assets: NoteAssets, + pub inputs: Vec, +} + +impl Default for NoteCreationConfig { + fn default() -> Self { + Self { + note_type: NoteType::Public, + tag: NoteTag::new(0), + assets: Default::default(), + inputs: Default::default(), + } + } +} +``` + +### Creating Notes with Assets + +```rust +use miden_client::asset::{Asset, FungibleAsset}; +use miden_client::note::NoteAssets; + +// 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)?; +let note_assets = NoteAssets::new(vec![Asset::Fungible(fungible_asset)])?; + +// Create the note +let deposit_note = create_testing_note_from_package( + deposit_note_package.clone(), + sender.id(), // Note sender + NoteCreationConfig { + assets: note_assets, + ..Default::default() + }, +)?; + +// Add to MockChain +builder.add_output_note(OutputNote::Full(deposit_note.clone())); +``` + +### Creating Notes with Inputs + +For notes that read parameters via `active_note::get_inputs()`: + +```rust +use miden_core::Felt; + +// Note inputs are a vector of Felts +let inputs = vec![ + // Asset data [0-3] + Felt::new(withdraw_amount), + Felt::new(0), + faucet.id().suffix(), + faucet.id().prefix().as_felt(), + // Serial number [4-7] + Felt::new(0x1234567890abcdef), + Felt::new(0xfedcba0987654321), + Felt::new(0xdeadbeefcafebabe), + Felt::new(0x0123456789abcdef), + // Additional parameters + Felt::new(tag as u64), + Felt::new(1), // note_type (1 = Public) +]; + +let note = create_testing_note_from_package( + note_package.clone(), + sender.id(), + NoteCreationConfig { + inputs, + ..Default::default() + }, +)?; +``` + +## Executing Transactions + +### Basic Transaction Execution + +```rust +// Build MockChain after adding all accounts and notes +let mut mock_chain = builder.build()?; + +// Build transaction context +// Args: (account_id, input_note_ids, expected_output_note_ids) +let tx_context = mock_chain + .build_tx_context(account.id(), &[note.id()], &[])? + .build()?; + +// Execute +let executed_tx = tx_context.execute().await?; + +// Apply state changes to local account copy +account.apply_delta(&executed_tx.account_delta())?; + +// Add to pending transactions and prove block +mock_chain.add_pending_executed_transaction(&executed_tx)?; +mock_chain.prove_next_block()?; +``` + +### Transaction with Script + +For transaction scripts (like initialization): + +```rust +use miden_client::transaction::TransactionScript; + +// Build the transaction script +let init_package = Arc::new(build_project_in_dir( + Path::new("../contracts/init-tx-script"), + true, +)?); + +let init_program = init_package.unwrap_program(); +let init_tx_script = TransactionScript::new((*init_program).clone()); + +// Execute with script +let tx_context = mock_chain + .build_tx_context(account.id(), &[], &[])? + .tx_script(init_tx_script) + .build()?; + +let executed_tx = tx_context.execute().await?; +``` + +### Transactions with Expected Output Notes + +When your contract creates output notes, specify them: + +```rust +use miden_client::transaction::OutputNote; + +// Build the expected output note +let expected_note = Note::new( + output_assets, + output_metadata, + recipient, +); + +let tx_context = mock_chain + .build_tx_context(account.id(), &[input_note.id()], &[])? + .extend_expected_output_notes(vec![OutputNote::Full(expected_note)]) + .build()?; +``` + +## Verifying State Changes + +### Reading Storage After Transaction + +```rust +// After executing and applying delta... + +// Read Value storage (by slot name) +let value: Word = account.storage().get_item(&initialized_slot)?; + +// Read Map storage (by slot name) +let key = Word::from([ + depositor.prefix().as_felt(), + depositor.suffix(), + faucet.id().prefix().as_felt(), + faucet.id().suffix(), +]); +let balance = account.storage().get_map_item(&balances_slot, key)?; + +// Assert expected values +assert_eq!( + balance, + Word::from([Felt::new(0), Felt::new(0), Felt::new(0), Felt::new(1000)]), + "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... + + let tx_context = mock_chain + .build_tx_context(account.id(), &[note.id()], &[])? + .build()?; + + // Execute and expect failure + let result = tx_context.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 result = tx_context.execute().await; + + assert!( + result.is_err(), + "Expected deposit to fail due to max limit" + ); + + Ok(()) +} +``` + +## Complete Test Example + +```rust title="integration/tests/deposit_test.rs" +use integration::helpers::{ + build_project_in_dir, create_testing_account_from_package, + create_testing_note_from_package, AccountCreationConfig, NoteCreationConfig, +}; +use miden_client::{ + account::{StorageMap, StorageSlot, StorageSlotName}, + asset::{Asset, FungibleAsset}, + note::NoteAssets, + transaction::{OutputNote, TransactionScript}, + Felt, Word, +}; +use miden_testing::{Auth, MockChain}; +use std::{path::Path, sync::Arc}; + +#[tokio::test] +async fn deposit_test() -> anyhow::Result<()> { + // 1. Setup MockChain builder + let mut builder = MockChain::builder(); + + // 2. Create faucet and sender + let faucet = builder.add_existing_basic_faucet(Auth::BasicAuth, "TEST", 1000, Some(10))?; + let sender = builder.add_existing_wallet_with_assets( + Auth::BasicAuth, + [FungibleAsset::new(faucet.id(), 100)?.into()], + )?; + + // 3. 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 + )?); + + // 4. Create bank account with named storage slots + let initialized_slot = + StorageSlotName::new("miden::component::miden_bank_account::initialized") + .expect("Valid slot name"); + let balances_slot = + StorageSlotName::new("miden::component::miden_bank_account::balances") + .expect("Valid slot name"); + + let bank_cfg = AccountCreationConfig { + storage_slots: vec![ + StorageSlot::with_value(initialized_slot, Word::default()), + StorageSlot::with_map( + balances_slot.clone(), + StorageMap::with_entries([]).expect("Empty storage map"), + ), + ], + ..Default::default() + }; + let mut bank_account = create_testing_account_from_package( + bank_package.clone(), bank_cfg + ).await?; + + // 5. Create deposit note + let deposit_amount: u64 = 1000; + let fungible_asset = FungibleAsset::new(faucet.id(), deposit_amount)?; + let note_assets = NoteAssets::new(vec![Asset::Fungible(fungible_asset)])?; + let deposit_note = create_testing_note_from_package( + deposit_note_package.clone(), + sender.id(), + NoteCreationConfig { assets: note_assets, ..Default::default() }, + )?; + + // 6. Add to builder and build chain + builder.add_account(bank_account.clone())?; + builder.add_output_note(OutputNote::Full(deposit_note.clone())); + let mut mock_chain = builder.build()?; + + // 7. Initialize bank + let init_program = init_tx_script_package.unwrap_program(); + let init_tx_script = TransactionScript::new((*init_program).clone()); + let init_tx_context = mock_chain + .build_tx_context(bank_account.id(), &[], &[])? + .tx_script(init_tx_script) + .build()?; + let executed_init = init_tx_context.execute().await?; + bank_account.apply_delta(&executed_init.account_delta())?; + mock_chain.add_pending_executed_transaction(&executed_init)?; + mock_chain.prove_next_block()?; + + // 8. Execute deposit + let tx_context = mock_chain + .build_tx_context(bank_account.id(), &[deposit_note.id()], &[])? + .build()?; + let executed_tx = tx_context.execute().await?; + bank_account.apply_delta(&executed_tx.account_delta())?; + mock_chain.add_pending_executed_transaction(&executed_tx)?; + mock_chain.prove_next_block()?; + + // 9. Verify balance + let depositor_key = Word::from([ + sender.id().prefix().as_felt(), + sender.id().suffix(), + faucet.id().prefix().as_felt(), + faucet.id().suffix(), + ]); + let balance = bank_account.storage().get_map_item(&balances_slot, depositor_key)?; + let expected = Word::from([ + Felt::new(0), Felt::new(0), Felt::new(0), Felt::new(deposit_amount) + ]); + assert_eq!(balance, expected, "Balance should match deposit"); + + println!("Deposit test passed!"); + Ok(()) +} +``` + +## Running Tests + +```bash title=">_ Terminal" +# Run all tests +cargo test -p integration -- --nocapture + +# Run specific test +cargo test -p integration deposit_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. **Apply Deltas** - Always call `apply_delta()` on local account copies after transactions +5. **Prove Blocks** - Call `prove_next_block()` after adding executed transactions +6. **Test Failures** - Use `result.is_err()` to verify constraint violations + +:::tip View Complete Source +See the complete test implementations in the [miden-bank repository](https://github.com/keinberger/miden-bank/tree/main/integration/tests). +::: + +## 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.14/builder/tutorials/img/count_copy_fpi_diagram.png b/versioned_docs/version-0.14/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.14/builder/tutorials/img/count_copy_fpi_diagram.png differ diff --git a/versioned_docs/version-0.14/builder/tutorials/img/note_creation_masm.png b/versioned_docs/version-0.14/builder/tutorials/img/note_creation_masm.png new file mode 100644 index 00000000..9458433b Binary files /dev/null and b/versioned_docs/version-0.14/builder/tutorials/img/note_creation_masm.png differ diff --git a/versioned_docs/version-0.14/builder/tutorials/index.md b/versioned_docs/version-0.14/builder/tutorials/index.md new file mode 100644 index 00000000..dc1a45db --- /dev/null +++ b/versioned_docs/version-0.14/builder/tutorials/index.md @@ -0,0 +1,43 @@ +--- +title: Tutorials +sidebar_position: 0 +pagination_prev: null +--- + +# Tutorials + +Hands-on walkthroughs for building on Miden. Every tutorial pairs with runnable Rust and TypeScript examples from the [miden-tutorials](https://github.com/0xMiden/miden-tutorials) repo and with MockChain tests so you can verify each step locally. + +## 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. + + + +## 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.14/builder/tutorials/miden-bank/00-project-setup.md b/versioned_docs/version-0.14/builder/tutorials/miden-bank/00-project-setup.md new file mode 100644 index 00000000..6fda6470 --- /dev/null +++ b/versioned_docs/version-0.14/builder/tutorials/miden-bank/00-project-setup.md @@ -0,0 +1,348 @@ +--- +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.93.0 (083ac5135 2025-12-15). + +Midenup: +- midenup + miden version: 0.1.0. +- active toolchain version: 0.20.3. +- ... +``` + +
+ +## 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 +└── 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 +``` + +Now update the `Cargo.toml` inside `contracts/bank-account/`: + +```toml title="contracts/bank-account/Cargo.toml" +[package] +name = "bank-account" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +miden = { version = "0.12" } + +[package.metadata.component] +package = "miden:bank-account" + +[package.metadata.miden] +project-kind = "account" +supported-types = ["RegularAccountImmutableCode"] +``` + +### Key Configuration Options + +| Field | Description | +| -------------------------------- | ---------------------------------------------------- | +| `crate-type = ["cdylib"]` | Required for WebAssembly compilation | +| `project-kind = "account"` | Tells the compiler this is an account component | +| `supported-types` | Account types this component supports | +| `package = "miden:bank-account"` | The component package name for cross-component calls | + +:::info Supported Account Types +`RegularAccountImmutableCode` means the account code cannot be changed after deployment. This is appropriate for our bank since we want the logic to be fixed. +::: + +## 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::*; + +/// Bank account component - we'll build this up throughout the tutorial. +#[component] +struct Bank { + /// 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 -> balance (as Felt). + /// We'll use this to track user balances in Part 1. + #[storage(description = "balances")] + balances: StorageMap, +} + +#[component] +impl Bank { + /// Initialize the bank account, enabling deposits. + pub fn initialize(&mut self) { + // Get current value from storage + let current: Word = self.initialized.get(); + + // Check not already initialized + 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); + } + + /// Get the balance for a depositor. + /// + /// This method is required for the component to compile correctly - + /// account components must use WIT binding types (like AccountId) + /// in at least one public method. + pub fn get_balance(&self, depositor: AccountId) -> Felt { + let key = Word::from([depositor.prefix, depositor.suffix, felt!(0), felt!(0)]); + 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 Compiler Requirement +Account components must use WIT binding types (like `AccountId`, `Asset`, etc.) in at least one public method signature for the compiler to generate the required bindings correctly. The `get_balance` method serves this purpose. +::: + +## Step 4: Update the Workspace Configuration + +Update the root `Cargo.toml` to reflect our renamed contract: + +```toml title="Cargo.toml" +[workspace] +members = [ + "integration" +] +exclude = [ + "contracts/", +] +resolver = "2" + +[workspace.package] +edition = "2021" + +[workspace.dependencies] +``` + +:::info Contracts Are Excluded +In v0.14, contracts are excluded from the Cargo workspace and built independently by `cargo miden`. Each contract specifies its own `miden` dependency directly. Only the `integration` crate remains a workspace member. +::: + +## Step 5: 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) +Creating Miden package /path/to/miden-bank/target/miden/release/bank_account.masp +``` + +
+ +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. +::: + +## Try It: Verify Your Setup + +Let's create a simple test to verify the bank account can be created. Create a new test file: + +```rust title="integration/tests/part0_setup_test.rs" +use integration::helpers::{ + build_project_in_dir, create_testing_account_from_package, AccountCreationConfig, +}; +use miden_client::account::{StorageMap, StorageSlot, StorageSlotName}; +use miden_client::Word; +use std::{path::Path, sync::Arc}; + +#[tokio::test] +async fn test_bank_account_builds_and_loads() -> anyhow::Result<()> { + // 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 + let initialized_slot = + StorageSlotName::new("miden_bank_account::bank::initialized") + .expect("Valid slot name"); + let balances_slot = + StorageSlotName::new("miden_bank_account::bank::balances") + .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_cfg = AccountCreationConfig { + init_storage_data, + ..Default::default() + }; + + let bank_account = + create_testing_account_from_package(bank_package.clone(), bank_cfg)?; + + // Verify the account was created + println!("Bank account created with ID: {:?}", bank_account.id()); + println!("Part 0 setup verified!"); + + Ok(()) +} +``` + +Run the test from the project root: + +```bash title=">_ Terminal" +cargo test --package integration test_bank_account_builds_and_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_builds_and_loads ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored +``` + +
+ +:::tip Troubleshooting +**"Failed to build bank account contract"**: Make sure the `contracts/bank-account/Cargo.toml` is properly configured and you've updated the root `Cargo.toml` members list. + +**"cannot find module helpers"**: Ensure the `integration/src/helpers.rs` file exists (it should have been generated by `miden new`). +::: + +## 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-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 `#[component]` on a struct +3. **Storage slots** are declared with `#[storage(description = "...")]` attributes (the compiler auto-assigns slot numbers) +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.14/builder/tutorials/miden-bank/01-account-components.md b/versioned_docs/version-0.14/builder/tutorials/miden-bank/01-account-components.md new file mode 100644 index 00000000..15e1a300 --- /dev/null +++ b/versioned_docs/version-0.14/builder/tutorials/miden-bank/01-account-components.md @@ -0,0 +1,426 @@ +--- +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 expand our Bank to include balance tracking with a `StorageMap`, giving us the foundation for deposits and withdrawals. + +## What You'll Build in This Part + +By the end of this section, you will have: + +- Understood the `#[component]` attribute and what it generates +- Added a `StorageMap` for tracking depositor balances +- Implemented a `get_balance()` query method +- **Verified it works** with a MockChain test + +## Building on Part 0 + +In Part 0, we created a minimal bank with just an `initialized` flag. Now we'll add balance tracking: + +```text +Part 0: Part 1: +┌──────────────────────────────┐ ┌──────────────────────────────────┐ +│ Bank │ │ Bank │ +│ ──────────────────────── │ ──► │ ──────────────────────────── │ +│ initialized (StorageValue) │ │ initialized (StorageValue) │ +│ │ │ balances (StorageMap)│ ◄── NEW +└──────────────────────────────┘ └──────────────────────────────────┘ +``` + +## The #[component] Attribute + +The `#[component]` attribute marks a struct as a Miden account component. When you compile with `miden build`, it generates: + +- **WIT (WebAssembly Interface Types)** bindings for cross-component calls +- **MASM (Miden Assembly)** code for the account logic +- **Storage slot management** code + +Let's expand our Bank component: + +## Step 1: Add the Balances Storage Map + +Update `contracts/bank-account/src/lib.rs`: + +```rust title="contracts/bank-account/src/lib.rs" {17-20} +#![no_std] +#![feature(alloc_error_handler)] + +#[macro_use] +extern crate alloc; + +use miden::*; + +/// Bank account component that tracks depositor balances. +#[component] +struct Bank { + /// 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 -> balance (as Felt) + /// Key: [prefix, suffix, asset_prefix, asset_suffix] + #[storage(description = "balances")] + balances: StorageMap, +} +``` + +We've added a `StorageMap` that will track each depositor's balance. The compiler auto-assigns slot numbers based on field order. + +## Storage Types Explained + +Miden accounts have storage slots that persist state on-chain. Each slot holds one `Word` (4 Felts = 32 bytes). 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 +The `.get()` method requires a type annotation: `let current: Word = self.initialized.get();` +::: + +### 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) +let key = Word::from([ + depositor.prefix, + depositor.suffix, + felt!(0), + felt!(0), +]); + +// Get returns a Felt (single value, not a Word) +let balance: Felt = self.balances.get(&key); + +// Set stores a Felt at the key +let new_balance = balance + deposit_amount; +self.balances.set(key, new_balance); +``` + +:::warning StorageMap Returns Felt +Unlike `Value::read()` which returns a `Word`, `StorageMap::get()` returns a single `Felt`. This is an important distinction. +::: + +### Storage Layout + +Plan your storage layout carefully: + +| Name | Type | Purpose | +| ------------- | ------------------------ | ------------------- | +| `initialized` | `StorageValue` | Initialization flag | +| `balances` | `StorageMap` | Depositor balances | + +The `description` attribute generates named slot identifiers (e.g., `miden_bank_account::bank::initialized`) used in tests to reference specific slots. The naming convention is `{package_name}::{component_struct}::{field_name}`. The compiler auto-assigns slot numbers based on field order. + +## Step 2: Implement Component Methods + +Now let's add methods to our Bank. The `#[component]` attribute is also used on the `impl` block: + +```rust title="contracts/bank-account/src/lib.rs" +#[component] +impl Bank { + /// Initialize the bank account, enabling deposits. + pub fn initialize(&mut self) { + // Get current value from storage + let current: Word = self.initialized.get(); + + // Check not already initialized + 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); + } + + /// Get the balance for a depositor. + pub fn get_balance(&self, depositor: AccountId) -> Felt { + let key = Word::from([depositor.prefix, depositor.suffix, felt!(0), felt!(0)]); + self.balances.get(key) + } + + /// 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" + ); + } +} +``` + +### Public vs Private Methods + +- **Public methods** (`pub fn`) are exposed in the generated WIT interface and can be called by other contracts +- **Private methods** (`fn`) are internal and cannot be called from outside + +```rust +// Public: Can be called by note scripts and other contracts +pub fn get_balance(&self, depositor: AccountId) -> Felt { ... } + +// Private: 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/release/bank_account.masp` - The compiled package +- `target/generated-wit/` - WIT interface files for other contracts to use + +## Try It: Verify Your Code + +Let's write a MockChain test to verify our Bank component works correctly. 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::{StorageMap, StorageSlot, 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_struct}::{field_name} + let initialized_slot = + StorageSlotName::new("miden_bank_account::bank::initialized") + .expect("Valid slot name"); + let balances_slot = + StorageSlotName::new("miden_bank_account::bank::balances") + .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_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::new(1), Felt::new(2), Felt::new(0), Felt::new(0)]); + let balance = bank_account.storage().get_map_item(&balances_slot, 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::*; + +/// Bank account component that tracks depositor balances. +#[component] +struct Bank { + /// 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 -> balance (as Felt) + /// Key: [prefix, suffix, asset_prefix, asset_suffix] + #[storage(description = "balances")] + balances: StorageMap, +} + +#[component] +impl Bank { + /// Initialize the bank account, enabling deposits. + pub fn initialize(&mut self) { + // Get current value from storage + let current: Word = self.initialized.get(); + + // Check not already initialized + 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); + } + + /// Get the balance for a depositor. + pub fn get_balance(&self, depositor: AccountId) -> Felt { + let key = Word::from([depositor.prefix, depositor.suffix, felt!(0), felt!(0)]); + self.balances.get(key) + } + + /// 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 structs and impl blocks as Miden account components +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 (auto-assigned by compiler), each holds 4 Felts (32 bytes) +5. **Public methods** are callable by other contracts via generated bindings + +:::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.14/builder/tutorials/miden-bank/02-constants-constraints.md b/versioned_docs/version-0.14/builder/tutorials/miden-bank/02-constants-constraints.md new file mode 100644 index 00000000..c82b65ed --- /dev/null +++ b/versioned_docs/version-0.14/builder/tutorials/miden-bank/02-constants-constraints.md @@ -0,0 +1,456 @@ +--- +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 +- Used `assert!()` for transaction validation +- Learned safe Felt comparison with `.as_canonical_u64()` +- Added a deposit method skeleton with validation +- **Verified constraints work** by testing that invalid operations fail + +## 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_balance() │ │ + get_balance() │ +│ │ │ + deposit() │ ◄── NEW (skeleton) +│ │ │ + MAX_DEPOSIT │ ◄── 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. +/// +/// Value: 1,000,000 tokens (arbitrary limit for demonstration) +const MAX_DEPOSIT_AMOUNT: u64 = 1_000_000; +``` + +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" +pub 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. + +## Safe Felt Comparisons + +:::warning Pitfall: Felt Comparison Operators +Never use `<`, `>`, `<=`, or `>=` operators directly on `Felt` values. They produce incorrect results due to field element ordering. +::: + +**Wrong approach:** + +```rust +// DON'T DO THIS - produces incorrect results +if deposit_amount > felt!(1_000_000) { + // This comparison is unreliable! +} +``` + +**Correct approach:** + +```rust +// CORRECT - convert to u64 first +if deposit_amount.as_canonical_u64() > MAX_DEPOSIT_AMOUNT { + // This works correctly +} +``` + +The `.as_canonical_u64()` method extracts the underlying 64-bit integer from a Felt, allowing standard Rust comparisons. + +## Step 1: Add the Constant and Deposit Method + +Update your `contracts/bank-account/src/lib.rs` to add the constant and a deposit method skeleton: + +```rust title="contracts/bank-account/src/lib.rs" {1-4,36-55} +/// Maximum allowed deposit amount per transaction. +/// +/// Value: 1,000,000 tokens (arbitrary limit for demonstration) +const MAX_DEPOSIT_AMOUNT: u64 = 1_000_000; + +#[component] +impl Bank { + /// Initialize the bank account, enabling deposits. + pub 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 balance for a depositor. + pub fn get_balance(&self, depositor: AccountId) -> Felt { + let key = Word::from([depositor.prefix, depositor.suffix, felt!(0), felt!(0)]); + self.balances.get(key) + } + + /// 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" + ); + } + + /// Deposit assets into the bank. + /// For now, this just validates constraints - we'll add asset handling in Part 3. + pub fn deposit(&mut self, depositor: AccountId, deposit_asset: Asset) { + // ======================================================================== + // CONSTRAINT: Bank must be initialized + // ======================================================================== + 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 + } +} +``` + +### The require_initialized() Guard + +We use a helper method to 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 +``` + +## Try It: Verify Constraints Work + +Let's write a test to verify our constraints work correctly. This test verifies that depositing without initialization fails: + +```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::{StorageMap, StorageSlot, 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, + )?); + + // Create named storage slots + let initialized_slot = + StorageSlotName::new("miden_bank_account::bank::initialized") + .expect("Valid slot name"); + let balances_slot = + StorageSlotName::new("miden_bank_account::bank::balances") + .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() guard in place"); + 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() guard in place + - Initialization status: 0 + +Part 2 constraints test passed! +test test_constraints_are_defined ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored +``` + +
+ +:::tip Preview: Testing Failed Assertions +In Part 4, when we have the deposit note script, we'll write a full test that verifies: + +1. Depositing without initialization fails +2. Depositing amounts over MAX_DEPOSIT_AMOUNT fails + +For now, the constraint logic is in place and we've verified the contract compiles. +::: + +## Common Constraint Patterns + +### Balance Checks (Preview for Part 3) + +```rust +fn require_sufficient_balance(&self, depositor: AccountId, amount: Felt) { + let balance = self.get_balance(depositor); + 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 + +```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::*; + +/// Maximum allowed deposit amount per transaction. +const MAX_DEPOSIT_AMOUNT: u64 = 1_000_000; + +/// Bank account component that tracks depositor balances. +#[component] +struct Bank { + #[storage(description = "initialized")] + initialized: StorageValue, + + #[storage(description = "balances")] + balances: StorageMap, +} + +#[component] +impl Bank { + /// Initialize the bank account, enabling deposits. + pub 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 balance for a depositor. + pub fn get_balance(&self, depositor: AccountId) -> Felt { + let key = Word::from([depositor.prefix, depositor.suffix, felt!(0), felt!(0)]); + self.balances.get(key) + } + + /// 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" + ); + } + + /// Deposit assets into the bank. + pub fn deposit(&mut self, depositor: AccountId, deposit_asset: Asset) { + // CONSTRAINT: Bank must be initialized + 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 + } +} +``` + +
+ +## Key Takeaways + +1. **Constants** define immutable business rules at compile time +2. **`assert!()`** enforces constraints - failures reject the transaction +3. **Always use `.as_canonical_u64()`** for Felt comparisons, never direct operators +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.14/builder/tutorials/miden-bank/03-asset-management.md b/versioned_docs/version-0.14/builder/tutorials/miden-bank/03-asset-management.md new file mode 100644 index 00000000..f49857b7 --- /dev/null +++ b/versioned_docs/version-0.14/builder/tutorials/miden-bank/03-asset-management.md @@ -0,0 +1,620 @@ +--- +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 represents fungible assets as a `Word` (4 Felts) with this layout: + +```text +Asset Layout: [amount, 0, faucet_suffix, faucet_prefix] + ━━━━━━━ ━ ━━━━━━━━━━━━━ ━━━━━━━━━━━━━ + index 0 1 index 2 index 3 +``` + +| Index | Field | Description | +| ----- | --------------- | ------------------------------------ | +| 0 | `amount` | The quantity of tokens | +| 1 | (reserved) | Always 0 for fungible assets | +| 2 | `faucet_suffix` | Second part of the faucet account ID | +| 3 | `faucet_prefix` | First part of the faucet account ID | + +Access amount through `asset.value` and 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 +let faucet_prefix = deposit_asset.key[3]; // Faucet ID prefix +``` + +## 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" +/// Deposit assets into the bank. +pub fn deposit(&mut self, depositor: AccountId, deposit_asset: Asset) { + // ======================================================================== + // CONSTRAINT: Bank must be initialized + // ======================================================================== + self.require_initialized(); + + // Extract the fungible amount from the asset value word + 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" + ); + + // ======================================================================== + // UPDATE BALANCE + // ======================================================================== + // Create key from depositor's AccountId and asset faucet ID + // This allows tracking balances per depositor per asset type + let key = Word::from([ + depositor.prefix, + depositor.suffix, + deposit_asset.key[3], // faucet_prefix + deposit_asset.key[2], // faucet_suffix + ]); + + // Update balance: current + deposit_amount + let current_balance: Felt = self.balances.get(key); + let new_balance = current_balance + deposit_amount; + self.balances.set(key, new_balance); + + // ======================================================================== + // ADD ASSET TO VAULT + // ======================================================================== + native_account::add_asset(deposit_asset); +} +``` + +### Balance Key Design + +We construct a composite key for balance tracking: + +```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) +]); +``` + +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 + +## 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 method to your Bank impl block: + +```rust title="contracts/bank-account/src/lib.rs" +/// Withdraw assets from the bank. +/// Creates a P2ID note to send assets back to the depositor. +pub fn withdraw( + &mut self, + depositor: AccountId, + withdraw_asset: Asset, + serial_num: Word, + tag: Felt, + note_type: Felt, +) { + // ======================================================================== + // CONSTRAINT: Bank must be initialized + // ======================================================================== + self.require_initialized(); + + // Extract the fungible amount from the asset value word + let withdraw_amount = withdraw_asset.value[0]; + + // Create key from depositor's AccountId and asset faucet ID + let key = Word::from([ + depositor.prefix, + depositor.suffix, + withdraw_asset.key[3], // faucet_prefix + withdraw_asset.key[2], // faucet_suffix + ]); + + // ======================================================================== + // 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 + // We'll implement create_p2id_note() in Part 7 + self.create_p2id_note(serial_num, &withdraw_asset, depositor, tag, note_type); +} +``` + +For now, add a placeholder for `create_p2id_note()`: + +```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 + // For now, this will cause a compile error if actually called + 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 + +Let's write a test to verify our deposit logic works correctly: + +```rust title="integration/tests/part3_deposit_test.rs" +use integration::helpers::{ + build_project_in_dir, create_testing_account_from_package, + create_testing_note_from_package, AccountCreationConfig, NoteCreationConfig, +}; +use miden_client::account::{StorageMap, StorageSlot, StorageSlotName}; +use miden_client::asset::{Asset, FungibleAsset}; +use miden_client::auth::AuthSchemeId; +use miden_client::note::NoteAssets; +use miden_client::transaction::{RawOutputNote, TransactionScript}; +use miden_client::{Felt, Word}; +use miden_testing::{Auth, MockChain}; +use std::{path::Path, sync::Arc}; + +#[tokio::test] +async fn test_deposit_updates_balance() -> anyhow::Result<()> { + // ========================================================================= + // SETUP + // ========================================================================= + let mut builder = MockChain::builder(); + + // Create a faucet for test tokens + let faucet = builder.add_existing_basic_faucet(Auth::BasicAuth { auth_scheme: AuthSchemeId::Falcon512Poseidon2 }, "TEST", 10_000_000, Some(10))?; + + // Create sender wallet with tokens + let sender = builder.add_existing_wallet_with_assets(Auth::BasicAuth { auth_scheme: AuthSchemeId::Falcon512Poseidon2 }, [FungibleAsset::new(faucet.id(), 1000)?.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 with storage slots + let initialized_slot = + StorageSlotName::new("miden_bank_account::bank::initialized") + .expect("Valid slot name"); + let balances_slot = + StorageSlotName::new("miden_bank_account::bank::balances") + .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_cfg = AccountCreationConfig { + init_storage_data, + ..Default::default() + }; + + let mut bank_account = + create_testing_account_from_package(bank_package.clone(), bank_cfg)?; + + // Add to mock chain + builder.add_account(bank_account.clone())?; + + // ========================================================================= + // STEP 2: Create deposit note before building the mock chain + // ========================================================================= + let deposit_amount: u64 = 1000; + let fungible_asset = FungibleAsset::new(faucet.id(), deposit_amount)?; + let note_assets = NoteAssets::new(vec![Asset::Fungible(fungible_asset)])?; + + let deposit_note = create_testing_note_from_package( + deposit_note_package.clone(), + sender.id(), + NoteCreationConfig { + assets: note_assets, + ..Default::default() + }, + )?; + + // Add note to builder before building + builder.add_output_note(RawOutputNote::Full(deposit_note.clone())); + + let mut mock_chain = builder.build()?; + + // ========================================================================= + // STEP 1: Initialize the bank + // ========================================================================= + let init_program = init_tx_script_package.unwrap_program(); + let init_tx_script = TransactionScript::new((*init_program).clone()); + + let init_tx_context = mock_chain + .build_tx_context(bank_account.id(), &[], &[])? + .tx_script(init_tx_script) + .build()?; + + let executed_init = init_tx_context.execute().await?; + bank_account.apply_delta(&executed_init.account_delta())?; + mock_chain.add_pending_executed_transaction(&executed_init)?; + mock_chain.prove_next_block()?; + + // Verify initialization + let initialized = bank_account.storage().get_item(&initialized_slot)?; + assert_eq!( + initialized[0].as_canonical_u64(), + 1, + "Bank should be initialized" + ); + println!("Bank initialized successfully!"); + + // ========================================================================= + // STEP 2: Execute deposit + // ========================================================================= + + // Execute deposit transaction + let tx_context = mock_chain + .build_tx_context(bank_account.id(), &[deposit_note.id()], &[])? + .build()?; + + let executed_transaction = tx_context.execute().await?; + bank_account.apply_delta(&executed_transaction.account_delta())?; + mock_chain.add_pending_executed_transaction(&executed_transaction)?; + mock_chain.prove_next_block()?; + + println!("Deposit transaction executed!"); + + // ========================================================================= + // VERIFY: Check balance was updated + // ========================================================================= + let depositor_key = Word::from([ + sender.id().prefix().as_felt(), + sender.id().suffix(), + faucet.id().prefix().as_felt(), + faucet.id().suffix(), + ]); + + let balance = bank_account.storage().get_map_item(&balances_slot, depositor_key)?; + + // Balance is stored as a single Felt in the last position of the Word + let balance_value = balance[3].as_canonical_u64(); + + println!("Depositor balance: {}", balance_value); + assert_eq!( + balance_value, + deposit_amount, + "Balance should equal deposited amount" + ); + + println!("\nPart 3 deposit test passed!"); + + Ok(()) +} +``` + +:::note Test Dependencies +This test requires: + +- `deposit-note` contract (Part 4) +- `init-tx-script` contract (Part 6) + +If you haven't created these yet, you can run this test after completing Parts 4 and 6, or create placeholder contracts. For now, let's verify the bank-account compiles correctly. +::: + +Build verification: + +```bash title=">_ Terminal" +cd contracts/bank-account +miden build +``` + +If you have the note scripts ready, run the full test from the project root: + +```bash title=">_ Terminal" +cargo test --package integration test_deposit_updates_balance -- --nocapture +``` + +
+Expected output + +```text + Compiling integration v0.1.0 (/path/to/miden-bank/integration) + Finished `test` profile [unoptimized + debuginfo] target(s) + Running tests/part3_deposit_test.rs + +running 1 test +Bank initialized successfully! +Deposit transaction executed! +Depositor balance: 1000 + +Part 3 deposit test passed! +test test_deposit_updates_balance ... 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::*; + +/// Maximum allowed deposit amount per transaction. +const MAX_DEPOSIT_AMOUNT: u64 = 1_000_000; + +/// Bank account component that tracks depositor balances. +#[component] +struct Bank { + #[storage(description = "initialized")] + initialized: StorageValue, + + #[storage(description = "balances")] + balances: StorageMap, +} + +#[component] +impl Bank { + /// Initialize the bank account, enabling deposits. + pub 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 balance for a depositor. + pub fn get_balance(&self, depositor: AccountId) -> Felt { + let key = Word::from([depositor.prefix, depositor.suffix, felt!(0), felt!(0)]); + self.balances.get(key) + } + + /// 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" + ); + } + + /// Deposit assets into the bank. + pub fn deposit(&mut self, depositor: AccountId, deposit_asset: Asset) { + self.require_initialized(); + + 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 + ]); + + let current_balance: Felt = self.balances.get(key); + let new_balance = current_balance + deposit_amount; + self.balances.set(key, new_balance); + + native_account::add_asset(deposit_asset); + } + + /// Withdraw assets from the bank. + pub fn withdraw( + &mut self, + depositor: AccountId, + withdraw_asset: Asset, + serial_num: Word, + tag: Felt, + note_type: Felt, + ) { + self.require_initialized(); + + 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 + ]); + + // 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); + + self.create_p2id_note(serial_num, &withdraw_asset, depositor, tag, note_type); + } + + /// 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; `key[3]` = faucet_prefix +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.14/builder/tutorials/miden-bank/04-note-scripts.md b/versioned_docs/version-0.14/builder/tutorials/miden-bank/04-note-scripts.md new file mode 100644 index 00000000..424d3569 --- /dev/null +++ b/versioned_docs/version-0.14/builder/tutorials/miden-bank/04-note-scripts.md @@ -0,0 +1,544 @@ +--- +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 `#[note_script]` method attribute +- 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)` | +| 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 Cargo.toml + +Create the `Cargo.toml` for the deposit note: + +```toml title="contracts/deposit-note/Cargo.toml" +[package] +name = "deposit-note" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +miden = { version = "0.12" } + +[package.metadata.component] +package = "miden:deposit-note" + +[package.metadata.miden] +project-kind = "note-script" + +# Dependencies on account components +[package.metadata.miden.dependencies] +"miden:bank-account" = { path = "../bank-account" } + +[package.metadata.component.target.dependencies] +"miden:bank-account" = { path = "../bank-account/target/generated-wit/" } +``` + +Key configuration: + +- `project-kind = "note-script"` - Marks this as a note script +- Dependencies sections declare which accounts it can interact with + +## Step 3: Implement the Deposit Note + +Create the note script implementation: + +```rust title="contracts/deposit-note/src/lib.rs" +#![no_std] +#![feature(alloc_error_handler)] + +use miden::*; + +// Import the bank account's generated bindings +use crate::bindings::miden::bank_account::bank_account; + +/// 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) { + // 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_assets(); + + // Deposit each asset into the bank + for asset in assets { + bank_account::deposit(depositor, asset); + } + } +} +``` + +### 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) +``` + +The method takes `self` as its first parameter. The `_arg` parameter can pass additional data, but we don't use it in the deposit note. + +## 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_assets() - Attached Assets + +```rust +let assets = active_note::get_assets(); +for asset in assets { + // Process each asset +} +``` + +Returns an iterator over all assets attached to the note. + +### get_storage() - Note Parameters + +```rust +let storage = active_note::get_storage(); +let first_item = storage[0]; +``` + +Returns a slice of `Felt` values passed when the note was created. We'll use storage items in the withdraw request note (Part 7). + +## Step 4: Update the Workspace + +Update the root `Cargo.toml` to include the new contract: + +```toml title="Cargo.toml" {5} +[workspace] +members = [ + "integration" +] +exclude = [ + "contracts/", +] +resolver = "2" + +[workspace.package] +edition = "2021" + +[workspace.dependencies] +``` + +## Step 5: Build the Note Script + +:::info Build Order Matters +Build account components **first** before building note scripts that depend on them. The note script needs the generated WIT files from the account. +::: + +```bash title=">_ Terminal" +# First, ensure bank-account is built (generates WIT files) +cd contracts/bank-account +miden build + +# Now build the deposit note +cd ../deposit-note +miden build +``` + +
+Expected output + +```text + Compiling deposit-note v0.1.0 + Finished `release` profile [optimized] target(s) +Creating Miden package /path/to/miden-bank/target/miden/release/deposit_note.masp +``` + +
+ +## 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_assets() → [100 tokens] + bank_account::deposit(depositor, 100 tokens) + +4. Bank's deposit() method executes + - Validates initialization and amount + - Updates balance: balances[User] += 100 + - Adds asset to vault +``` + +## Try It: Verify Deposits Work + +Now let's write a test to verify the complete deposit flow. This test: + +1. Initializes the bank +2. Creates a deposit note with tokens +3. Has the bank consume the note +4. Verifies the balance was updated + +```rust title="integration/tests/part4_deposit_note_test.rs" +use integration::helpers::{ + build_project_in_dir, create_testing_account_from_package, + create_testing_note_from_package, AccountCreationConfig, NoteCreationConfig, +}; +use miden_client::account::{StorageMap, StorageSlot, StorageSlotName}; +use miden_client::asset::{Asset, FungibleAsset}; +use miden_client::auth::AuthSchemeId; +use miden_client::note::NoteAssets; +use miden_client::transaction::{RawOutputNote, TransactionScript}; +use miden_client::{Felt, Word}; +use miden_testing::{Auth, MockChain}; +use std::{path::Path, sync::Arc}; + +#[tokio::test] +async fn test_deposit_note_credits_depositor() -> anyhow::Result<()> { + // ========================================================================= + // SETUP: Build contracts and create mock chain + // ========================================================================= + let mut builder = MockChain::builder(); + + // Create a faucet for test tokens + let faucet = builder.add_existing_basic_faucet(Auth::BasicAuth { auth_scheme: AuthSchemeId::Falcon512Poseidon2 }, "TEST", 10_000_000, Some(10))?; + + // Create sender (depositor) wallet + let sender = builder.add_existing_wallet_with_assets(Auth::BasicAuth { auth_scheme: AuthSchemeId::Falcon512Poseidon2 }, [FungibleAsset::new(faucet.id(), 1000)?.into()])?; + + // Build all 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 bank account + let initialized_slot = + StorageSlotName::new("miden_bank_account::bank::initialized") + .expect("Valid slot name"); + let balances_slot = + StorageSlotName::new("miden_bank_account::bank::balances") + .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_cfg = AccountCreationConfig { + init_storage_data, + ..Default::default() + }; + + let mut bank_account = + create_testing_account_from_package(bank_package.clone(), bank_cfg)?; + + builder.add_account(bank_account.clone())?; + + // Create the deposit note and add it before building the chain + let deposit_amount: u64 = 1000; + let fungible_asset = FungibleAsset::new(faucet.id(), deposit_amount)?; + let note_assets = NoteAssets::new(vec![Asset::Fungible(fungible_asset)])?; + + let deposit_note = create_testing_note_from_package( + deposit_note_package.clone(), + sender.id(), // Sender is the depositor + NoteCreationConfig { + assets: note_assets, + ..Default::default() + }, + )?; + + builder.add_output_note(RawOutputNote::Full(deposit_note.clone())); + let mut mock_chain = builder.build()?; + + // ========================================================================= + // STEP 1: Initialize the bank + // ========================================================================= + let init_program = init_tx_script_package.unwrap_program(); + let init_tx_script = TransactionScript::new((*init_program).clone()); + + let init_tx_context = mock_chain + .build_tx_context(bank_account.id(), &[], &[])? + .tx_script(init_tx_script) + .build()?; + + let executed_init = init_tx_context.execute().await?; + bank_account.apply_delta(&executed_init.account_delta())?; + mock_chain.add_pending_executed_transaction(&executed_init)?; + mock_chain.prove_next_block()?; + + println!("Step 1: Bank initialized"); + + // ========================================================================= + // STEP 2: Execute deposit + // ========================================================================= + let tx_context = mock_chain + .build_tx_context(bank_account.id(), &[deposit_note.id()], &[])? + .build()?; + + let executed_transaction = tx_context.execute().await?; + bank_account.apply_delta(&executed_transaction.account_delta())?; + mock_chain.add_pending_executed_transaction(&executed_transaction)?; + mock_chain.prove_next_block()?; + + println!("Step 2: Deposit note consumed"); + + // ========================================================================= + // VERIFY: Balance was updated + // ========================================================================= + let depositor_key = Word::from([ + sender.id().prefix().as_felt(), + sender.id().suffix(), + faucet.id().prefix().as_felt(), + faucet.id().suffix(), + ]); + + let balance = bank_account.storage().get_map_item(&balances_slot, depositor_key)?; + let balance_value = balance[3].as_canonical_u64(); + + println!("Step 3: Verified balance = {}", balance_value); + + assert_eq!( + balance_value, + deposit_amount, + "Balance should equal deposited amount" + ); + + println!("\nPart 4 deposit note test passed!"); + + Ok(()) +} +``` + +:::note Dependencies +This test requires the `init-tx-script` contract which we'll create in Part 6. You can either: + +1. Skip ahead to create a minimal init-tx-script (see Part 6) +2. Run this test after completing Part 6 + +For now, verify that your deposit-note builds successfully. +::: + +Run the test from the project root (after creating init-tx-script in Part 6): + +```bash title=">_ Terminal" +cargo test --package integration test_deposit_note_credits_depositor -- --nocapture +``` + +
+Expected output + +```text + Compiling integration v0.1.0 (/path/to/miden-bank/integration) + Finished `test` profile [unoptimized + debuginfo] target(s) + Running tests/part4_deposit_note_test.rs + +running 1 test +Step 1: Bank initialized +Step 2: Deposit note consumed +Step 3: Verified balance = 1000 + +Part 4 deposit note test passed! +test test_deposit_note_credits_depositor ... 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)" +/// Withdraw Request Note Script +/// +/// # Note Storage (14 Felts) +/// [0-3]: withdraw asset encoded as [amount, 0, faucet_suffix, faucet_prefix] +/// [4-7]: serial_num (random/unique per note) +/// [8]: tag (P2ID note tag for routing) +/// [9]: note_type (1 = Public, 2 = 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) { + let depositor = active_note::get_sender(); + let storage = active_note::get_storage(); + + // Parse parameters from storage + 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_account::withdraw. + + bank_account::withdraw(depositor, 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" +#![no_std] +#![feature(alloc_error_handler)] + +use miden::*; + +use crate::bindings::miden::bank_account::bank_account; + +/// Deposit Note Script +#[note] +struct DepositNote; + +#[note] +impl DepositNote { + #[note_script] + fn run(self, _arg: Word) { + let depositor = active_note::get_sender(); + let assets = active_note::get_assets(); + + for asset in assets { + bank_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)` +2. **`active_note::get_sender()`** returns who created the note +3. **`active_note::get_assets()`** returns assets attached to the note +4. **`active_note::get_storage()`** returns parameterized data +5. **Note scripts execute once** when consumed - no persistent state +6. **Build order matters** - account components first, then note scripts + +:::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.14/builder/tutorials/miden-bank/05-cross-component-calls.md b/versioned_docs/version-0.14/builder/tutorials/miden-bank/05-cross-component-calls.md new file mode 100644 index 00000000..6cb83beb --- /dev/null +++ b/versioned_docs/version-0.14/builder/tutorials/miden-bank/05-cross-component-calls.md @@ -0,0 +1,296 @@ +--- +sidebar_position: 5 +title: "Part 5: Cross-Component Calls" +description: "Learn how note scripts and transaction scripts call account component methods using generated bindings 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 `Cargo.toml` +- Explored the WIT interface files +- **Verified cross-component calls work** via the deposit flow + +## Building on Part 4 + +In Part 4, you wrote `bank_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 miden build │ +│ pub fn deposit() ─────────────▶ generated-wit/ │ +│ pub fn withdraw() miden_bank-account.wit +│ │ +│ ┌───────────────────────────┐ │ +│ ▼ │ │ +│ deposit-note/ │ │ +│ └── src/lib.rs │ │ +│ use crate::bindings::miden::bank_account::bank_account; +│ bank_account::deposit(...) ◄───── calls via binding │ +│ │ +└────────────────────────────────────────────────────────────┘ +``` + +## The Bindings System + +When you build an account component with `miden build`, it generates: + +1. **MASM code** - The compiled contract logic +2. **WIT files** - WebAssembly Interface Type definitions + +Other contracts (note scripts, transaction scripts) import these WIT files to call the account's methods. + +```text +Build Flow: +┌──────────────────┐ miden build ┌─────────────────────────────────┐ +│ bank-account/ │ ─────────────────▶│ target/generated-wit/ │ +│ src/lib.rs │ │ miden_bank-account.wit │ +│ │ │ miden_bank-account_world.wit │ +└──────────────────┘ └─────────────────────────────────┘ + │ + ▼ + ┌─────────────────────────────────┐ + │ deposit-note/ │ + │ imports generated bindings │ + └─────────────────────────────────┘ +``` + +## Importing Bindings + +In your note script, import the generated bindings: + +```rust title="contracts/deposit-note/src/lib.rs" +// Import the bank account's generated bindings +use crate::bindings::miden::bank_account::bank_account; +``` + +The import path follows this pattern: + +``` +crate::bindings::{package-prefix}::{component-name}::{interface-name} +``` + +For our bank: + +- `miden` - The package prefix from `[package.metadata.component]` +- `bank_account` - The component name (derived from package name with underscores) +- `bank_account` - The interface name (same as component) + +## Calling Account Methods + +Once imported, call the account methods directly: + +```rust title="contracts/deposit-note/src/lib.rs" +#[note] +struct DepositNote; + +#[note] +impl DepositNote { + #[note_script] + fn run(self, _arg: Word) { + let depositor = active_note::get_sender(); + let assets = active_note::get_assets(); + + for asset in assets { + // Call the bank account's deposit method + bank_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 + +Your `Cargo.toml` needs **two** dependency sections: + +```toml title="contracts/deposit-note/Cargo.toml" +[package.metadata.miden.dependencies] +"miden:bank-account" = { path = "../bank-account" } + +[package.metadata.component.target.dependencies] +"miden:bank-account" = { path = "../bank-account/target/generated-wit/" } +``` + +### miden.dependencies + +```toml +[package.metadata.miden.dependencies] +"miden: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 + +### component.target.dependencies + +```toml +[package.metadata.component.target.dependencies] +"miden:bank-account" = { path = "../bank-account/target/generated-wit/" } +``` + +This tells the Rust compiler where to find the WIT interface files. The path points to the `generated-wit/` directory created when you built the account component. + +:::warning Both Sections Required +If either section is missing, your build will fail with linking or interface errors. +::: + +## Build Order + +Components must be built in dependency order: + +```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 +``` + +If you build out of order, you'll see errors about missing WIT files. + +## What Methods Are Available? + +Only **public methods** (`pub fn`) on the `#[component] impl` block are available through bindings: + +```rust title="contracts/bank-account/src/lib.rs" +#[component] +impl Bank { + // PUBLIC: Available through bindings + pub fn deposit(&mut self, depositor: AccountId, deposit_asset: Asset) { ... } + pub fn withdraw(&mut self, /* ... */) { ... } + pub fn get_balance(&self, depositor: AccountId) -> Felt { ... } + pub fn initialize(&mut self) { ... } + + // PRIVATE: NOT available through bindings + fn require_initialized(&self) { ... } + fn create_p2id_note(&mut self, /* ... */) { ... } +} +``` + +## Understanding the Generated WIT + +The WIT files describe the interface. Here's a simplified example: + +```wit title="target/generated-wit/miden_bank-account.wit" +interface bank-account { + use miden:types/types.{account-id, asset, felt, word}; + + initialize: func(); + deposit: func(depositor: account-id, deposit-asset: asset); + withdraw: func(depositor: account-id, withdraw-asset: asset, ...); + get-balance: func(depositor: account-id) -> felt; +} +``` + +This WIT is used to generate the Rust bindings that appear in `crate::bindings`. + +## Transaction Script Bindings (Preview) + +Transaction scripts use a slightly different import pattern: + +```rust title="contracts/init-tx-script/src/lib.rs" +use crate::bindings::Account; + +#[tx_script] +fn run(_arg: Word, account: &mut Account) { + // The account parameter IS the bound component + account.initialize(); +} +``` + +The `Account` binding in transaction scripts wraps the entire component, giving direct method access through the `account` parameter. We'll implement this in Part 6. + +## Try It: Verify Bindings Work + +If you completed Part 4 and built both contracts, the bindings are already working! Let's verify: + +```bash title=">_ Terminal" +# Check that the WIT files were generated +ls contracts/bank-account/target/generated-wit/ +``` + +
+Expected output + +```text +miden_bank-account.wit +miden_bank-account_world.wit +``` + +
+ +These files enable the deposit note to call `bank_account::deposit()`. + +## Common Issues + +### "Cannot find module" Error + +``` +error: cannot find module `bindings` +``` + +**Cause**: The account component wasn't built, or the WIT path is wrong. + +**Solution**: + +1. Build the account: `cd contracts/bank-account && miden build` +2. Verify the WIT path in `Cargo.toml` points to `target/generated-wit/` + +### "Method not found" Error + +``` +error: no method named `deposit` found +``` + +**Cause**: The method isn't marked `pub` in the account component. + +**Solution**: Ensure the method has `pub fn` visibility. + +### "Dependency not found" Error + +``` +error: dependency 'miden:bank-account' not found +``` + +**Cause**: One of the dependency sections is missing or has the wrong path. + +**Solution**: Ensure both `[package.metadata.miden.dependencies]` and `[package.metadata.component.target.dependencies]` are present with correct paths. + +## Key Takeaways + +1. **Build accounts first** - They generate WIT files that note scripts need +2. **Two dependency sections** - Both `miden.dependencies` and `component.target.dependencies` are required +3. **Import path pattern** - `crate::bindings::{package}::{component}::{interface}` +4. **Only public methods** - Private methods aren't exposed in bindings +5. **Transaction scripts differ** - They receive the account as a parameter (Part 6) + +:::tip View Complete Source +See the complete Cargo.toml configurations: + +- [Deposit Note Cargo.toml](https://github.com/0xMiden/miden-tutorials/blob/main/examples/miden-bank/contracts/deposit-note/Cargo.toml) +- [Withdraw Request Note Cargo.toml](https://github.com/0xMiden/miden-tutorials/blob/main/examples/miden-bank/contracts/withdraw-request-note/Cargo.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.14/builder/tutorials/miden-bank/06-transaction-scripts.md b/versioned_docs/version-0.14/builder/tutorials/miden-bank/06-transaction-scripts.md new file mode 100644 index 00000000..e8dd1364 --- /dev/null +++ b/versioned_docs/version-0.14/builder/tutorials/miden-bank/06-transaction-scripts.md @@ -0,0 +1,493 @@ +--- +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. + +## 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 • Explicitly called by owner│ +│ • Import bindings via modules • Receive account parameter │ +│ • Process incoming assets • Setup, admin operations │ +│ │ +│ deposit-note/ init-tx-script/ │ +│ └── calls bank_account::deposit() └── calls account.initialize() +│ │ +└────────────────────────────────────────────────────────────────┘ +``` + +## Transaction Scripts vs Note Scripts + +| Aspect | Transaction Script | Note Script | +| ---------- | ---------------------------------- | -------------------------------- | +| Initiation | Explicitly called by account owner | Triggered when note is consumed | +| Access | Direct account method access | Must call through bindings | +| Use case | Setup, owner operations | Receiving messages/assets | +| Parameter | `account: &mut Account` | Note context via `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 Cargo.toml + +Create the Cargo.toml with transaction script configuration: + +```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 = { version = "0.12" } + +[package.metadata.component] +package = "miden:init-tx-script" + +[package.metadata.miden] +project-kind = "transaction-script" + +[package.metadata.miden.dependencies] +"miden:bank-account" = { path = "../bank-account" } + +[package.metadata.component.target.dependencies] +"miden:bank-account" = { path = "../bank-account/target/generated-wit/" } +``` + +Key configuration: + +- `project-kind = "transaction-script"` - Marks this as a transaction script (not "account" or "note") +- Dependencies reference the account component (same pattern as note scripts) + +## Step 3: Add to Workspace + +Update your root `Cargo.toml` to include the new project: + +```toml title="Cargo.toml" +[workspace] +members = [ + "integration" +] +exclude = [ + "contracts/", +] +resolver = "2" + +[workspace.package] +edition = "2021" + +[workspace.dependencies] +``` + +## Step 4: 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::*; + +// Import the Account binding which wraps the bank-account component methods +use crate::bindings::Account; + +/// 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 account is now "deployed" and visible on chain +#[tx_script] +fn run(_arg: Word, account: &mut Account) { + account.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 Account) { + account.initialize(); +} +``` + +### Function Signature + +| Parameter | Type | Description | +| --------- | -------------- | ------------------------------------------ | +| `_arg` | `Word` | Optional argument passed when executing | +| `account` | `&mut Account` | Mutable reference to the account component | + +The `Account` type is generated from your component's bindings and provides access to all public methods. + +## The Account Binding + +Unlike note scripts that import bindings like `bank_account::deposit()`, transaction scripts receive the account as a parameter: + +```rust +// Note script style (indirect): +use crate::bindings::miden::bank_account::bank_account; +bank_account::deposit(depositor, asset); + +// Transaction script style (direct): +use crate::bindings::Account; +fn run(_arg: Word, account: &mut Account) { + account.initialize(); // Direct method call +} +``` + +The `Account` wrapper provides: + +- Direct method access without module prefixes +- Proper mutable/immutable borrowing +- Automatic context binding + +## Step 5: Build the Transaction Script + +Build in dependency order: + +```bash title=">_ Terminal" +# First, ensure the account component is built (generates WIT files) +cd contracts/bank-account +miden build + +# Then build the transaction script +cd ../init-tx-script +miden build +``` + +
+Expected output + +```text + Compiling init-tx-script v0.1.0 + Finished `release` profile [optimized] target(s) +Creating Miden package /path/to/miden-bank/target/miden/release/init_tx_script.masp +``` + +
+ +## Account Deployment Pattern + +In Miden, accounts are only visible on-chain after their first state change. Transaction scripts are commonly used for this "deployment": + +```text +Execution Flow: + +1. Account owner creates transaction with init-tx-script + ┌───────────────────────────────────────┐ + │ Transaction │ + │ Account: Bank's AccountId │ + │ Script: init-tx-script │ + └───────────────────────────────────────┘ + +2. Transaction executes + ┌───────────────────────────────────────┐ + │ run(_arg, account) │ + │ └─ account.initialize() │ + │ └─ Sets initialized flag to 1 │ + └───────────────────────────────────────┘ + +3. Account state updated + ┌───────────────────────────────────────┐ + │ Bank Account │ + │ Storage[0] = [1, 0, 0, 0] ← Initialized + │ Now visible on-chain │ + └───────────────────────────────────────┘ +``` + +Before initialization: + +- Account exists locally but isn't visible on the network +- Cannot receive notes or interact with other accounts + +After initialization: + +- Account is "deployed" and visible +- Can receive deposits and interact normally + +## 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 Account) { + // Use arg as configuration + let config_value = arg[0]; + account.configure(config_value); +} +``` + +When creating the transaction, provide the argument: + +```rust title="Integration code (not contract code)" +let tx_script_args = Word::from([felt!(42), felt!(0), felt!(0), felt!(0)]); +let tx_context = mock_chain + .build_tx_context(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 enables deposits. + +Create a test file: + +```rust title="integration/tests/part6_tx_script_test.rs" +use integration::helpers::{ + build_project_in_dir, create_testing_account_from_package, AccountCreationConfig, +}; +use miden_client::account::{StorageMap, StorageSlot, StorageSlotName}; +use miden_client::Word; +use miden_client::transaction::TransactionScript; +use miden_testing::MockChain; +use std::{path::Path, sync::Arc}; + +/// Test that the init-tx-script properly initializes the bank account +#[tokio::test] +async fn test_init_tx_script_enables_deposits() -> anyhow::Result<()> { + // Build all required packages + let mut builder = MockChain::builder(); + + 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, + )?); + + // Create uninitialized bank account with named storage slots + let initialized_slot = + StorageSlotName::new("miden_bank_account::bank::initialized") + .expect("Valid slot name"); + let balances_slot = + StorageSlotName::new("miden_bank_account::bank::balances") + .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_cfg = AccountCreationConfig { + init_storage_data, + ..Default::default() + }; + + let mut bank_account = + create_testing_account_from_package(bank_package.clone(), bank_cfg)?; + + // Verify bank is NOT initialized + let initial_storage = bank_account.storage().get_item(&initialized_slot)?; + assert_eq!( + initial_storage[0].as_canonical_u64(), + 0, + "Bank should start uninitialized" + ); + + println!("Step 1: Bank starts uninitialized (storage[0] = 0)"); + + // Add bank to mock chain + builder.add_account(bank_account.clone())?; + let mut mock_chain = builder.build()?; + + // Create the TransactionScript from our init-tx-script + let init_program = init_tx_script_package.unwrap_program(); + let init_tx_script = TransactionScript::new((*init_program).clone()); + + // Build and execute the initialization transaction + let init_tx_context = mock_chain + .build_tx_context(bank_account.id(), &[], &[])? + .tx_script(init_tx_script) + .build()?; + + let executed_init = init_tx_context.execute().await?; + bank_account.apply_delta(&executed_init.account_delta())?; + mock_chain.add_pending_executed_transaction(&executed_init)?; + mock_chain.prove_next_block()?; + + // Verify bank IS now initialized + let final_storage = bank_account.storage().get_item(&initialized_slot)?; + assert_eq!( + final_storage[0].as_canonical_u64(), + 1, + "Bank should be initialized after tx script" + ); + + println!("Step 2: Bank initialized via transaction script (storage[0] = 1)"); + println!("\nPart 6 transaction script test passed!"); + + Ok(()) +} +``` + +Run the test from the project root: + +```bash title=">_ Terminal" +cargo test --package integration test_init_tx_script_enables_deposits -- --nocapture +``` + +
+Expected output + +```text + Compiling integration v0.1.0 (/path/to/miden-bank/integration) + Finished `test` profile [unoptimized + debuginfo] target(s) + Running tests/part6_tx_script_test.rs + +running 1 test +✓ Bank successfully initialized via transaction script + Storage[0] changed from [0,0,0,0] to [1,0,0,0] + Bank is now ready to accept deposits! +test test_init_tx_script_enables_deposits ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored +``` + +
+ +:::tip Troubleshooting +**"Cannot find module bindings"**: The bank-account wasn't built. Run `miden build` in `contracts/bank-account` first. + +**"Dependency not found"**: Check that both dependency sections are in Cargo.toml with correct paths. +::: + +## 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::*; + +// Import the Account binding which wraps the bank-account component methods +use crate::bindings::Account; + +/// 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 account is now "deployed" and visible on chain +#[tx_script] +fn run(_arg: Word, account: &mut Account) { + 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 = { version = "0.12" } + +[package.metadata.component] +package = "miden:init-tx-script" + +[package.metadata.miden] +project-kind = "transaction-script" + +[package.metadata.miden.dependencies] +"miden:bank-account" = { path = "../bank-account" } + +[package.metadata.component.target.dependencies] +"miden:bank-account" = { path = "../bank-account/target/generated-wit/" } +``` + +
+ +## Key Takeaways + +1. **`#[tx_script]`** marks the entry point with signature `fn run(_arg: Word, account: &mut Account)` +2. **Direct account access** - Methods called on the `account` parameter, not via module imports +3. **Owner-initiated** - Only the account owner can execute transaction scripts +4. **Deployment pattern** - First state change makes account visible on-chain +5. **Dependencies** - Same Cargo.toml configuration as note scripts + +:::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.14/builder/tutorials/miden-bank/07-output-notes.md b/versioned_docs/version-0.14/builder/tutorials/miden-bank/07-output-notes.md new file mode 100644 index 00000000..3f7ffaab --- /dev/null +++ b/versioned_docs/version-0.14/builder/tutorials/miden-bank/07-output-notes.md @@ -0,0 +1,732 @@ +--- +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: Add Withdraw Method to Bank Account + +First, let's add the `withdraw()` method to your bank account. Update `contracts/bank-account/src/lib.rs`: + +```rust title="contracts/bank-account/src/lib.rs" +#[component] +impl Bank { + // ... existing methods (initialize, deposit, get_balance) ... + + /// Withdraw assets back to the depositor. + /// + /// Creates a P2ID note that sends the requested asset to the depositor's account. + /// + /// # Arguments + /// * `depositor` - The AccountId of the user withdrawing + /// * `withdraw_asset` - The fungible asset to withdraw + /// * `serial_num` - Unique serial number for the P2ID output note + /// * `tag` - The note tag for the P2ID output note (allows caller to specify routing) + /// * `note_type` - Note type: 1 = Public (stored on-chain), 2 = Private (off-chain) + /// + /// # Panics + /// Panics if the withdrawal amount exceeds the depositor's current balance. + /// Panics if the bank has not been initialized. + pub fn withdraw( + &mut self, + depositor: AccountId, + withdraw_asset: Asset, + serial_num: Word, + tag: Felt, + note_type: Felt, + ) { + // Ensure the bank is initialized before processing withdrawals + self.require_initialized(); + + // Extract the fungible amount from the asset value word + let withdraw_amount = withdraw_asset.value[0]; + + // Create key from depositor's AccountId and asset faucet ID + let key = Word::from([ + depositor.prefix, + depositor.suffix, + withdraw_asset.key[3], // faucet_prefix + withdraw_asset.key[2], // faucet_suffix + ]); + + // 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" + ); + + // 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). + // This avoids hardcoding a version-specific MAST root constant. + 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); + } +} +``` + +:::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 uses `P2idNote::script_root()` from the `miden_client` crate to obtain the correct value at test time. + +## Step 3: Implement create_p2id_note + +Add the private method that creates the output note: + +```rust title="contracts/bank-account/src/lib.rs" +#[component] +impl Bank { + // ... other methods ... + + /// Create a P2ID (Pay-to-ID) note to send assets to a recipient. + /// + /// # Arguments + /// * `serial_num` - Unique serial number for the note + /// * `asset` - The asset to include in the note + /// * `recipient_id` - The AccountId that can consume this note + /// * `tag` - The note tag (passed by caller to allow proper P2ID routing) + /// * `note_type` - Note type as Felt: 1 = Public, 2 = Private + /// * `script_root` - The P2ID note script MAST root (Poseidon2-hashed) + 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 + // The caller is responsible for computing the proper P2ID tag + // (typically with_account_target for the recipient) + let tag = Tag::from(tag); + + // Convert note_type Felt to NoteType + // 1 = Public (stored on-chain), 2 = Private (off-chain) + let note_type = NoteType::from(note_type); + + // Compute the recipient hash from: + // - serial_num: unique identifier for this note instance + // - script_root: the P2ID note script's MAST root + // - storage: the target account ID + // + // The P2ID script expects inputs as [suffix, prefix] + 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.clone()); + + // Add the asset to the output note + output_note::add_asset(asset.clone(), 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 (2) | +| `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 Cargo.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 = { version = "0.12" } + +[package.metadata.component] +package = "miden:withdraw-request-note" + +[package.metadata.miden] +project-kind = "note-script" + +[package.metadata.miden.dependencies] +"miden:bank-account" = { path = "../bank-account" } + +[package.metadata.component.target.dependencies] +"miden:bank-account" = { path = "../bank-account/target/generated-wit/" } +``` + +### Update Workspace + +Add to your root `Cargo.toml`: + +```toml title="Cargo.toml" +[workspace] +members = [ + "integration" +] +exclude = [ + "contracts/", +] +resolver = "2" + +[workspace.package] +edition = "2021" + +[workspace.dependencies] +``` + +## 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::*; + +// Import the bank account's generated bindings +use crate::bindings::miden::bank_account::bank_account; + +/// 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 sender (depositor) and storage items +/// 4. Calls `bank_account::withdraw(depositor, asset, serial_num, tag, note_type)` +/// 5. Bank updates the depositor's balance +/// 6. Bank reads P2ID script root from storage[10-13] and creates a P2ID output note +/// +/// # Note Storage (14 Felts) +/// [0-3]: withdraw asset encoded as [amount, 0, faucet_suffix, faucet_prefix] +/// [4-7]: serial_num (random/unique per note) +/// [8]: tag (P2ID note tag for routing) +/// [9]: note_type (1 = Public, 2 = 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) { + // The depositor is whoever created/sent this note + let depositor = active_note::get_sender(); + + // Get the storage items + let storage = active_note::get_storage(); + + // Asset: reconstruct from [amount, 0, faucet_suffix, faucet_prefix] encoding + 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, 2 = 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_account::withdraw. + + // Call the bank account to withdraw the assets + bank_account::withdraw(depositor, withdraw_asset, serial_num, tag, note_type); + } +} +``` + +### 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 (identifies asset type) │ +│ 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 2 (Private) │ +│ 10-13 │ script_root │ P2ID script MAST root (Poseidon2-hashed, 4 Felts)│ +└───────────────────────────────────────────────────────────────────────────┘ +``` + +:::note Why the Asset is in Inputs +Unlike the deposit note which gets assets from `active_note::get_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 in dependency order: + +```bash title=">_ Terminal" +# 1. Build the account component (generates WIT files) +cd contracts/bank-account +miden build + +# 2. Build the withdraw request note +cd ../withdraw-request-note +miden build +``` + +## 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 a P2ID output note is created + +```rust title="integration/tests/part7_withdraw_test.rs" +use integration::helpers::{ + build_project_in_dir, create_testing_account_from_package, create_testing_note_from_package, + AccountCreationConfig, NoteCreationConfig, +}; +use miden_client::{ + account::{StorageMap, StorageSlotName}, + asset::{Asset, FungibleAsset}, + auth::AuthSchemeId, + note::{P2idNote, P2idNoteStorage, Note, NoteAssets, NoteMetadata, NoteTag, NoteType}, + transaction::{RawOutputNote, TransactionScript}, + Felt, Word, +}; +use miden_testing::{Auth, MockChain}; +use std::{path::Path, sync::Arc}; + +#[tokio::test] +async fn test_withdraw_creates_p2id_note() -> anyhow::Result<()> { + // ========================================================================= + // SETUP + // ========================================================================= + let mut builder = MockChain::builder(); + + let deposit_amount: u64 = 1000; + + // Create faucet and sender (depositor) + let faucet = + builder.add_existing_basic_faucet(Auth::BasicAuth { auth_scheme: AuthSchemeId::Falcon512Poseidon2 }, "TEST", deposit_amount, Some(10))?; + let sender = builder.add_existing_wallet_with_assets( + Auth::BasicAuth { auth_scheme: AuthSchemeId::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, + )?); + let withdraw_request_note_package = Arc::new(build_project_in_dir( + Path::new("../contracts/withdraw-request-note"), + true, + )?); + + // Create bank account with named storage slots + let initialized_slot = + StorageSlotName::new("miden_bank_account::bank::initialized") + .expect("Valid slot name"); + let balances_slot = + StorageSlotName::new("miden_bank_account::bank::balances") + .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_cfg = AccountCreationConfig { + init_storage_data, + ..Default::default() + }; + let mut bank_account = + create_testing_account_from_package(bank_package.clone(), bank_cfg)?; + + // Create deposit note + let fungible_asset = FungibleAsset::new(faucet.id(), deposit_amount)?; + let note_assets = NoteAssets::new(vec![Asset::Fungible(fungible_asset)])?; + let deposit_note = create_testing_note_from_package( + deposit_note_package.clone(), + sender.id(), + NoteCreationConfig { + assets: note_assets, + ..Default::default() + }, + )?; + + // Add accounts and notes to builder + builder.add_account(bank_account.clone())?; + builder.add_output_note(RawOutputNote::Full(deposit_note.clone())); + + // ========================================================================= + // CRAFT WITHDRAW REQUEST NOTE (14-Felt storage layout) + // ========================================================================= + let withdraw_amount = deposit_amount / 2; + + // Compute P2ID tag for the sender + let p2id_tag = NoteTag::with_account_target(sender.id()); + let p2id_tag_felt = Felt::new(p2id_tag.as_u32() as u64); + + // Serial number for output note + let p2id_output_note_serial_num = Word::from([ + Felt::new(0x1234567890abcdef), + Felt::new(0xfedcba0987654321), + Felt::new(0xdeadbeefcafebabe), + Felt::new(0x0123456789abcdef), + ]); + + let note_type_felt = Felt::new(1); // Public + + // Get the P2ID script root (Poseidon2-hashed MAST root) + let p2id_script_root = P2idNote::script_root(); + + // Note storage: 14 Felts + // [0-3]: withdraw asset (amount, 0, faucet_suffix, faucet_prefix) + // [4-7]: serial_num + // [8]: tag + // [9]: note_type + // [10-13]: P2ID script_root + let withdraw_request_note_storage = vec![ + Felt::new(withdraw_amount), + Felt::new(0), + faucet.id().suffix(), + faucet.id().prefix().as_felt(), + p2id_output_note_serial_num[0], + p2id_output_note_serial_num[1], + p2id_output_note_serial_num[2], + p2id_output_note_serial_num[3], + p2id_tag_felt, + note_type_felt, + p2id_script_root[0], + p2id_script_root[1], + p2id_script_root[2], + p2id_script_root[3], + ]; + + 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())); + + // ========================================================================= + // EXECUTE: Initialize, Deposit, Withdraw + // ========================================================================= + let mut mock_chain = builder.build()?; + + // Initialize bank + let init_program = init_tx_script_package.unwrap_program(); + let init_tx_script = TransactionScript::new((*init_program).clone()); + let init_tx_context = mock_chain + .build_tx_context(bank_account.id(), &[], &[])? + .tx_script(init_tx_script) + .build()?; + let executed_init = init_tx_context.execute().await?; + bank_account.apply_delta(&executed_init.account_delta())?; + mock_chain.add_pending_executed_transaction(&executed_init)?; + mock_chain.prove_next_block()?; + + println!("Step 1: Bank initialized"); + + // Process deposit + let deposit_tx_context = mock_chain + .build_tx_context(bank_account.id(), &[deposit_note.id()], &[])? + .build()?; + let executed_deposit = deposit_tx_context.execute().await?; + bank_account.apply_delta(&executed_deposit.account_delta())?; + mock_chain.add_pending_executed_transaction(&executed_deposit)?; + mock_chain.prove_next_block()?; + + println!("Step 2: Deposited {} tokens", deposit_amount); + + // Process withdraw with expected P2ID output note + 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 = NoteMetadata::new(bank_account.id(), NoteType::Public) + .with_tag(p2id_tag); + let p2id_output_note = Note::new( + p2id_output_note_assets, + p2id_output_note_metadata, + recipient, + ); + + let withdraw_tx_context = mock_chain + .build_tx_context(bank_account.id(), &[withdraw_request_note.id()], &[])? + .extend_expected_output_notes(vec![RawOutputNote::Full(p2id_output_note)]) + .build()?; + let executed_withdraw = withdraw_tx_context.execute().await?; + bank_account.apply_delta(&executed_withdraw.account_delta())?; + mock_chain.add_pending_executed_transaction(&executed_withdraw)?; + mock_chain.prove_next_block()?; + + println!("Step 3: Withdrew {} tokens", withdraw_amount); + println!("\nPart 7 withdraw test passed!"); + + Ok(()) +} +``` + +Run the test from the project root: + +```bash title=">_ Terminal" +cargo test --package integration test_withdraw_creates_p2id_note -- --nocapture +``` + +
+Expected output + +```text + Compiling integration v0.1.0 (/path/to/miden-bank/integration) + Finished `test` profile [unoptimized + debuginfo] target(s) + Running tests/part7_withdraw_test.rs + +running 1 test +Step 1: Bank initialized +Step 2: Deposited 1000 tokens +Step 3: Withdrew 500 tokens + +Part 7 withdraw test passed! +test test_withdraw_creates_p2id_note ... 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::*; + +// Import the bank account's generated bindings +use crate::bindings::miden::bank_account::bank_account; + +/// 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, faucet_prefix] +/// [4-7]: serial_num (random/unique per note) +/// [8]: tag (P2ID note tag for routing) +/// [9]: note_type (1 = Public, 2 = 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) { + // The depositor is whoever created/sent this note + let depositor = active_note::get_sender(); + + // Get the storage items + let storage = active_note::get_storage(); + + // Asset: reconstruct from [amount, 0, faucet_suffix, faucet_prefix] encoding + 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, 2 = 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_account::withdraw. + + // Call the bank account to withdraw the assets + bank_account::withdraw(depositor, 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.14/builder/tutorials/miden-bank/08-complete-flows.md b/versioned_docs/version-0.14/builder/tutorials/miden-bank/08-complete-flows.md new file mode 100644 index 00000000..2406a703 --- /dev/null +++ b/versioned_docs/version-0.14/builder/tutorials/miden-bank/08-complete-flows.md @@ -0,0 +1,594 @@ +--- +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 → bank_account::deposit() │ │ +│ ├─────────────────┼───────────────────────────────────────┤ │ +│ │ withdraw-note │ Note script → bank_account::withdraw() │ │ +│ ├─────────────────┼───────────────────────────────────────┤ │ +│ │ init-tx-script │ Transaction script → initialize() │ │ +│ └─────────────────┴───────────────────────────────────────┘ │ +│ │ +│ Storage Layout: │ +│ ┌─────────────────────────────────────────────────────────┐ │ +│ │ initialized (Value) │ Word: [1, 0, 0, 0] when ready│ │ +│ │ balances (StorageMap) │ Map: user_key → [balance, 0, 0, 0]│ │ +│ └─────────────────────────────────────────────────────────┘ │ +│ │ +└────────────────────────────────────────────────────────────────┘ +``` + +## 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 tok] │ │ +│ │ script: deposit-note│ │ +│ │ target: Bank │ │ +│ └──────────────────────┘ │ +│ │ │ +│ ▼ │ +│ 2. BANK CONSUMES NOTE (Transaction begins) │ +│ ┌──────────────────────┐ │ +│ │ Bank Account │ │ +│ │ vault += 1000 tokens│ ◀── Protocol adds assets to vault │ +│ └──────────────────────┘ │ +│ │ │ +│ ▼ │ +│ 3. NOTE SCRIPT EXECUTES │ +│ depositor = active_note::get_sender() → User's AccountId │ +│ assets = active_note::get_assets() → [1000 tokens] │ +│ for asset in assets: │ +│ bank_account::deposit(depositor, asset) ◀── Cross-component│ +│ │ │ +│ ▼ │ +│ 4. DEPOSIT METHOD RUNS (in bank-account context) │ +│ ┌──────────────────────────────────────────┐ │ +│ │ require_initialized() ✓ Passes │ │ +│ │ amount <= MAX_DEPOSIT ✓ 1000 <= 100k │ │ +│ │ native_account::add_asset() ← Confirm │ │ +│ │ balances[User] += 1000 ← Update │ │ +│ └──────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ 5. TRANSACTION COMPLETES │ +│ Bank storage: balances[User] = 1000 │ +│ Bank vault: +1000 tokens │ +│ │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +## The Complete Withdraw Flow + +Now let's trace the withdrawal process: + +```text +┌─────────────────────────────────────────────────────────────────────┐ +│ WITHDRAW FLOW │ +├─────────────────────────────────────────────────────────────────────┤ +│ │ +│ 1. USER CREATES WITHDRAW REQUEST NOTE │ +│ ┌──────────────────────────────┐ │ +│ │ Withdraw Request Note │ │ +│ │ sender: User │ │ +│ │ inputs: [serial, tag, │ │ +│ │ note_type] │ │ +│ │ assets: [withdraw amount] │ │ +│ │ target: Bank │ │ +│ └──────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ 2. BANK CONSUMES REQUEST (Transaction begins) │ +│ ┌──────────────────────────────┐ │ +│ │ Note script executes: │ │ +│ │ sender = get_sender() │ │ +│ │ storage = get_storage() │ │ +│ │ asset = Asset from inputs │ │ +│ │ bank_account::withdraw(...) │ │ +│ └──────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ 3. WITHDRAW METHOD RUNS │ +│ ┌──────────────────────────────────────────────────────┐ │ +│ │ require_initialized() ✓ Passes │ │ +│ │ current_balance = get_balance(User) → 1000 │ │ +│ │ VALIDATE: 1000 >= 400 ✓ Passes │ ◀ CRITICAL +│ │ balances[User] = 1000 - 400 → 600 │ │ +│ │ create_p2id_note(...) → Output note │ │ +│ └──────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ 4. P2ID NOTE CREATED (inside create_p2id_note) │ +│ ┌──────────────────────────────────────────────────────┐ │ +│ │ script_root = storage[10..13] → MAST digest │ │ +│ │ recipient = note::build_recipient( │ │ +│ │ serial_num, script_root, │ │ +│ │ [user.suffix, user.prefix] │ │ +│ │ ) │ │ +│ │ note_idx = output_note::create(tag, note_type, │ │ +│ │ recipient) │ │ +│ │ native_account::remove_asset(400 tokens) │ │ +│ │ output_note::add_asset(400 tokens, note_idx) │ │ +│ └──────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ 5. TRANSACTION COMPLETES │ +│ Bank storage: balances[User] = 600 │ +│ Bank vault: -400 tokens │ +│ Output: P2ID note with 400 tokens → User │ +│ │ │ +│ ▼ │ +│ 6. USER CONSUMES P2ID NOTE (separate transaction) │ +│ User's wallet receives 400 tokens │ +│ │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +## Try It: Complete End-to-End Test + +Let's create a comprehensive test that exercises the entire bank system: + +```rust title="integration/tests/part8_complete_flow_test.rs" +use integration::helpers::{ + build_project_in_dir, create_testing_account_from_package, create_testing_note_from_package, + AccountCreationConfig, NoteCreationConfig, +}; +use miden_client::{ + account::{StorageMap, StorageSlot, StorageSlotName}, + asset::{Asset, FungibleAsset}, + note::{P2idNote, P2idNoteStorage, Note, NoteAssets, NoteMetadata, NoteTag, NoteType}, + transaction::{RawOutputNote, TransactionScript}, + Felt, Word, +}; +use miden_client::auth::AuthSchemeId; +use miden_testing::{Auth, MockChain}; +use std::{path::Path, sync::Arc}; + +/// Complete end-to-end test of the Miden Bank +/// +/// This test exercises: +/// 1. Bank initialization via transaction script +/// 2. Deposit via deposit-note +/// 3. Withdrawal via withdraw-request-note +/// 4. Balance verification at each step +#[tokio::test] +async fn test_complete_bank_flow() -> anyhow::Result<()> { + println!("╔══════════════════════════════════════════════════════════════╗"); + println!("║ MIDEN BANK - COMPLETE FLOW TEST ║"); + println!("╚══════════════════════════════════════════════════════════════╝"); + + // ═══════════════════════════════════════════════════════════════════ + // SETUP + // ═══════════════════════════════════════════════════════════════════ + println!("\n📦 Setting up test environment..."); + + let mut builder = MockChain::builder(); + + let deposit_amount: u64 = 1000; + let withdraw_amount: u64 = 400; + + // Create a faucet to mint test assets + let faucet = + builder.add_existing_basic_faucet(Auth::BasicAuth { auth_scheme: AuthSchemeId::Falcon512Poseidon2 }, "TEST", deposit_amount, Some(10))?; + + // Create note sender account (the depositor) + let sender = builder.add_existing_wallet_with_assets( + Auth::BasicAuth { auth_scheme: AuthSchemeId::Falcon512Poseidon2 }, + [FungibleAsset::new(faucet.id(), deposit_amount)?.into()], + )?; + println!(" ✓ Faucet and sender wallet created"); + + // Build all packages + 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, + )?); + let withdraw_request_note_package = Arc::new(build_project_in_dir( + Path::new("../contracts/withdraw-request-note"), + true, + )?); + println!(" ✓ All packages built"); + + // Create named storage slots + let initialized_slot = + StorageSlotName::new("miden_bank_account::bank::initialized") + .expect("Valid slot name"); + let balances_slot = + StorageSlotName::new("miden_bank_account::bank::balances") + .expect("Valid slot name"); + + // Create bank account with storage slots + 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 mut bank_account = + create_testing_account_from_package(bank_package.clone(), bank_cfg)?; + println!(" ✓ Bank account created: {:?}", bank_account.id()); + + // Create deposit note with assets + let fungible_asset = FungibleAsset::new(faucet.id(), deposit_amount)?; + let note_assets = NoteAssets::new(vec![Asset::Fungible(fungible_asset)])?; + let deposit_note = create_testing_note_from_package( + deposit_note_package.clone(), + sender.id(), + NoteCreationConfig { + assets: note_assets, + ..Default::default() + }, + )?; + + // Craft withdraw request note with 10-Felt input layout + let p2id_tag = NoteTag::with_account_target(sender.id()); + let p2id_tag_felt = Felt::new(p2id_tag.as_u32() as u64); + + let p2id_output_note_serial_num = Word::from([ + Felt::new(0x1234567890abcdef), + Felt::new(0xfedcba0987654321), + Felt::new(0xdeadbeefcafebabe), + Felt::new(0x0123456789abcdef), + ]); + + let note_type_felt = Felt::new(1); // Public + + // Get the P2ID script root (Poseidon2-hashed MAST root) + let p2id_script_root = P2idNote::script_root(); + + // Note storage: 14 Felts + // [0-3]: withdraw asset (amount, 0, faucet_suffix, faucet_prefix) + // [4-7]: serial_num + // [8]: tag + // [9]: note_type + // [10-13]: P2ID script_root + let withdraw_request_note_storage = vec![ + Felt::new(withdraw_amount), + Felt::new(0), + faucet.id().suffix(), + faucet.id().prefix().as_felt(), + p2id_output_note_serial_num[0], + p2id_output_note_serial_num[1], + p2id_output_note_serial_num[2], + p2id_output_note_serial_num[3], + p2id_tag_felt, + note_type_felt, + p2id_script_root[0], + p2id_script_root[1], + p2id_script_root[2], + p2id_script_root[3], + ]; + + let withdraw_request_note = create_testing_note_from_package( + withdraw_request_note_package.clone(), + sender.id(), + NoteCreationConfig { + storage: withdraw_request_note_storage, + ..Default::default() + }, + )?; + + // Add to builder + builder.add_account(bank_account.clone())?; + builder.add_output_note(RawOutputNote::Full(deposit_note.clone())); + builder.add_output_note(RawOutputNote::Full(withdraw_request_note.clone())); + + let mut mock_chain = builder.build()?; + println!(" ✓ MockChain built"); + + // ═══════════════════════════════════════════════════════════════════ + // STEP 1: Initialize the bank + // ═══════════════════════════════════════════════════════════════════ + println!("\n1️⃣ INITIALIZING BANK..."); + + let init_program = init_tx_script_package.unwrap_program(); + let init_tx_script = TransactionScript::new((*init_program).clone()); + + let init_tx_context = mock_chain + .build_tx_context(bank_account.id(), &[], &[])? + .tx_script(init_tx_script) + .build()?; + + let executed_init = init_tx_context.execute().await?; + bank_account.apply_delta(&executed_init.account_delta())?; + mock_chain.add_pending_executed_transaction(&executed_init)?; + mock_chain.prove_next_block()?; + + println!(" ✓ Bank initialized (storage[0] = [1, 0, 0, 0])"); + + // ═══════════════════════════════════════════════════════════════════ + // STEP 2: Deposit tokens + // ═══════════════════════════════════════════════════════════════════ + println!("\n2️⃣ DEPOSITING TOKENS..."); + println!(" Deposit amount: {} tokens", deposit_amount); + + let deposit_tx_context = mock_chain + .build_tx_context(bank_account.id(), &[deposit_note.id()], &[])? + .build()?; + + let executed_deposit = deposit_tx_context.execute().await?; + bank_account.apply_delta(&executed_deposit.account_delta())?; + mock_chain.add_pending_executed_transaction(&executed_deposit)?; + mock_chain.prove_next_block()?; + + // Verify balance after deposit + let depositor_key = Word::from([ + sender.id().prefix().as_felt(), + sender.id().suffix(), + faucet.id().prefix().as_felt(), + faucet.id().suffix(), + ]); + let balance_after_deposit = bank_account.storage().get_map_item(&balances_slot, depositor_key)?; + println!( + " ✓ Bank processed deposit, balance: {} tokens", + balance_after_deposit[3].as_canonical_u64() + ); + + // ═══════════════════════════════════════════════════════════════════ + // STEP 3: Withdraw tokens + // ═══════════════════════════════════════════════════════════════════ + println!("\n3️⃣ WITHDRAWING TOKENS..."); + println!(" Withdraw amount: {} tokens", withdraw_amount); + + // Build expected P2ID output note + 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 = NoteMetadata::new(bank_account.id(), NoteType::Public) + .with_tag(p2id_tag); + let p2id_output_note = Note::new( + p2id_output_note_assets, + p2id_output_note_metadata, + recipient, + ); + + let withdraw_tx_context = mock_chain + .build_tx_context(bank_account.id(), &[withdraw_request_note.id()], &[])? + .extend_expected_output_notes(vec![RawOutputNote::Full(p2id_output_note)]) + .build()?; + + let executed_withdraw = withdraw_tx_context.execute().await?; + bank_account.apply_delta(&executed_withdraw.account_delta())?; + mock_chain.add_pending_executed_transaction(&executed_withdraw)?; + mock_chain.prove_next_block()?; + + println!(" ✓ Bank processed withdraw request"); + println!(" ✓ P2ID output note created for sender"); + + // Verify final balance + let final_balance = bank_account.storage().get_map_item(&balances_slot, depositor_key)?; + let final_balance_amount = final_balance[3].as_canonical_u64(); + let expected_final = deposit_amount - withdraw_amount; + + println!(" ✓ Final balance verified: {} tokens", final_balance_amount); + + // ═══════════════════════════════════════════════════════════════════ + // SUMMARY + // ═══════════════════════════════════════════════════════════════════ + println!("\n╔══════════════════════════════════════════════════════════════╗"); + println!("║ TEST SUMMARY ║"); + println!("╠══════════════════════════════════════════════════════════════╣"); + println!( + "║ Initial deposit: {:>6} tokens ║", + deposit_amount + ); + println!( + "║ Withdrawal: -{:>6} tokens ║", + withdraw_amount + ); + println!( + "║ Final balance: {:>6} tokens ║", + final_balance_amount + ); + println!("║ ║"); + println!("║ ✅ All operations completed successfully! ║"); + println!("╚══════════════════════════════════════════════════════════════╝"); + + assert_eq!( + final_balance_amount, expected_final, + "Final balance should be deposit - withdraw" + ); + + Ok(()) +} +``` + +Run the complete test from the project root: + +```bash title=">_ Terminal" +cargo test --package integration test_complete_bank_flow -- --nocapture +``` + +
+Expected output + +```text + Compiling integration v0.1.0 (/path/to/miden-bank/integration) + Finished `test` profile [unoptimized + debuginfo] target(s) + Running tests/part8_complete_flow_test.rs + +running 1 test +╔══════════════════════════════════════════════════════════════╗ +║ MIDEN BANK - COMPLETE FLOW TEST ║ +╚══════════════════════════════════════════════════════════════╝ + +📦 Setting up test environment... + ✓ Faucet and sender wallet created + ✓ All packages built + ✓ Bank account created: 0x... + ✓ MockChain built + +1️⃣ INITIALIZING BANK... + ✓ Bank initialized (storage[0] = [1, 0, 0, 0]) + +2️⃣ DEPOSITING TOKENS... + Deposit amount: 1000 tokens + ✓ Bank processed deposit, balance: 1000 tokens + +3️⃣ WITHDRAWING TOKENS... + Withdraw amount: 400 tokens + ✓ Bank processed withdraw request + ✓ P2ID output note created for sender + ✓ Final balance verified: 600 tokens + +╔══════════════════════════════════════════════════════════════╗ +║ TEST SUMMARY ║ +╠══════════════════════════════════════════════════════════════╣ +║ Initial deposit: 1000 tokens ║ +║ Withdrawal: - 400 tokens ║ +║ Final balance: 600 tokens ║ +║ ║ +║ ✅ All operations completed successfully! ║ +╚══════════════════════════════════════════════════════════════╝ +test test_complete_bank_flow ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored +``` + +
+ +## 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_assets()` | Get 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; +``` + +::: + +:::warning Felt Comparison Operators +Never use `<`, `>` on Felt values directly. Always convert to u64 first: + +```rust +// ❌ BROKEN: Produces incorrect results +if current_balance < withdraw_amount { ... } + +// ✅ CORRECT: Use as_canonical_u64() +if current_balance.as_canonical_u64() < withdraw_amount.as_canonical_u64() { ... } +``` + +::: + +## 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.14/builder/tutorials/miden-bank/_category_.json b/versioned_docs/version-0.14/builder/tutorials/miden-bank/_category_.json new file mode 100644 index 00000000..a93e1a4c --- /dev/null +++ b/versioned_docs/version-0.14/builder/tutorials/miden-bank/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Miden Bank", + "position": 1 +} diff --git a/versioned_docs/version-0.14/builder/tutorials/miden-bank/index.md b/versioned_docs/version-0.14/builder/tutorials/miden-bank/index.md new file mode 100644 index 00000000..46f918ab --- /dev/null +++ b/versioned_docs/version-0.14/builder/tutorials/miden-bank/index.md @@ -0,0 +1,116 @@ +--- +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, part by part. + +## What you'll build + +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. +- **Initialization script** — a transaction script to deploy and initialize the bank. + +Each part ends with a **runnable MockChain test** so you can verify what you built works correctly. + +## Tutorial structure + +Every part builds on the previous one and includes: + +- **What you'll build** — clear objectives for the section. +- **Step-by-step code** — progressively building functionality. +- **Try it** — a MockChain test to verify your code works. +- **Complete code** — full code listing for reference. + +## Walkthrough + + + + Create your project with `miden new` and understand the workspace structure. + + + Learn `#[component]`, `Value` storage, and `StorageMap` for managing state. + + + Define constants and validate inputs with assertions. + + + Handle fungible assets with vault operations and balance tracking. + + + Write scripts that execute when notes are consumed. + + + Call account methods from note scripts via bindings. + + + Write scripts for account initialization and owner operations. + + + Create P2ID notes programmatically for withdrawals. + + + Walk through end-to-end deposit and withdraw operations. + + + +## Prerequisites + +- Completed the [Get started guide](../../get-started) — `midenup`, `miden new`, basic tooling. +- Understanding of Miden concepts: [accounts, notes, transactions](../../smart-contracts). +- Rust programming experience. + + +This tutorial assumes no prior experience with the Miden Rust compiler. We explain every concept as it comes up. + + +## Concepts covered + +| Concept | What it does | Part | +| ---------------------------- | ---------------------------------------------------------- | ---- | +| `#[component]` | Define account components with storage | 1 | +| Storage types | `Value` 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 the miden-tutorials repository: + +```bash title=">_ Terminal" +git clone https://github.com/0xMiden/miden-tutorials.git +cd miden-tutorials/examples/miden-bank +``` + +## Supplementary guides + + + + Learn to test your contracts with MockChain for local simulation. + + + Interpret errors and debug common issues. + + + Avoid known issues and limitations. + + + +## Getting help + +- Detailed technical reference: [docs.miden.xyz](https://docs.miden.xyz). +- Join the [Build on Miden](https://t.me/BuildOnMiden) Telegram for support. +- Review the complete code in the [examples/miden-bank](https://github.com/0xMiden/miden-tutorials/tree/main/examples/miden-bank) directory. + +Ready? Start with [Part 0: Project setup](./project-setup). diff --git a/versioned_docs/version-0.14/builder/tutorials/miden_node_setup.md b/versioned_docs/version-0.14/builder/tutorials/miden_node_setup.md new file mode 100644 index 00000000..731af74a --- /dev/null +++ b/versioned_docs/version-0.14/builder/tutorials/miden_node_setup.md @@ -0,0 +1,100 @@ +--- +title: Miden Node Setup +sidebar_position: 2 +--- + +# Miden Node Setup Tutorial + +To run the Miden tutorial examples, you will need to set up a test environment and connect to a Miden node. + +There are two ways to connect to a Miden node: + +1. Run the Miden node locally +2. Connect to the Miden testnet + +## Running the Miden node locally + +:::tip[Prerequisites] +Building the node from source requires a C/C++ toolchain (for compiling RocksDB). On **macOS**, make sure you have the Xcode Command Line Tools installed: + +```bash +xcode-select --install +``` + +On **Ubuntu**, see the [node installation page](https://docs.miden.xyz/miden-node/operator/installation#install-using-cargo) for the required packages. If you run into `'cstdint' file not found` errors on macOS, see the [troubleshooting section](https://docs.miden.xyz/miden-node/operator/installation#install-using-cargo) on the installation page. +::: + +### Step 1: Install the Miden node + +Install the miden-node crate using this command: + +```bash +# Installs from GitHub (crates.io publication was not verified): +cargo install --locked --git https://github.com/0xMiden/miden-node --tag v0.14.6 miden-node +``` + +Check the [miden-node releases](https://github.com/0xMiden/miden-node/releases) for the latest version compatible with your target network. + +### Step 2: Initializing the node + +To start the node, we first need to generate the genesis file. Create the genesis file using this command: + +```bash +mkdir data +mkdir accounts + +miden-node bundled bootstrap \ + --data-directory data \ + --accounts-directory accounts +``` + +Expected output: + +``` +2025-04-16T18:05:30.049129Z INFO miden_node::commands::store: bin/node/src/commands/store.rs:145: Generating account, index: 0, total: 1 +``` + +### Step 3: Starting the node + +To start the node run this command: + +```bash +miden-node bundled start \ + --data-directory data \ + --rpc.url http://0.0.0.0:57291 +``` + +Expected output: + +``` +2025-01-17T12:14:55.432445Z INFO try_build_batches: miden-block-producer: /Users/username/.cargo/registry/src/index.crates.io-6f17d22bba15001f/miden-node-block-producer-0.6.0/src/txqueue/mod.rs:85: close, time.busy: 8.88µs, time.idle: 103µs +2025-01-17T12:14:57.433162Z INFO try_build_batches: miden-block-producer: /Users/username/.cargo/registry/src/index.crates.io-6f17d22bba15001f/miden-node-block-producer-0.6.0/src/txqueue/mod.rs:85: new +2025-01-17T12:14:57.433256Z INFO try_build_batches: miden-block-producer: /Users/username/.cargo/registry/src/index.crates.io-6f17d22bba15001f/miden-node-block-producer-0.6.0/src/txqueue/mod.rs:85: close, time.busy: 6.46µs, time.idle: 94.0µs +``` + +Congratulations, you now have a Miden node running locally. Now we can start creating a testing environment for building applications on Miden! + +The endpoint of the Miden node running locally is: + +``` +http://localhost:57291 +``` + +### Resetting the node + +_If you need to reset the local state of the node run this command:_ + +```bash +rm -r data +rm -r accounts +``` + +After resetting the state of the node, follow steps 2 and 4 again. + +## Connecting to the Miden testnet + +To run the tutorial examples using the Miden testnet, use this endpoint: + +```bash +https://rpc.testnet.miden.io:443 +``` diff --git a/versioned_docs/version-0.14/builder/tutorials/recipes/_category_.json b/versioned_docs/version-0.14/builder/tutorials/recipes/_category_.json new file mode 100644 index 00000000..c5e2a889 --- /dev/null +++ b/versioned_docs/version-0.14/builder/tutorials/recipes/_category_.json @@ -0,0 +1,5 @@ +{ + "label": "Recipes", + "position": 3, + "collapsed": true +} diff --git a/versioned_docs/version-0.14/builder/tutorials/recipes/img/count_copy_fpi_diagram.png b/versioned_docs/version-0.14/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.14/builder/tutorials/recipes/img/count_copy_fpi_diagram.png differ diff --git a/versioned_docs/version-0.14/builder/tutorials/recipes/img/note_creation_masm.png b/versioned_docs/version-0.14/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.14/builder/tutorials/recipes/img/note_creation_masm.png differ diff --git a/versioned_docs/version-0.14/builder/tutorials/recipes/rust/_category_.json b/versioned_docs/version-0.14/builder/tutorials/recipes/rust/_category_.json new file mode 100644 index 00000000..ce988c87 --- /dev/null +++ b/versioned_docs/version-0.14/builder/tutorials/recipes/rust/_category_.json @@ -0,0 +1,5 @@ +{ + "label": "Rust", + "position": 1, + "collapsed": true +} diff --git a/versioned_docs/version-0.14/builder/tutorials/recipes/rust/counter_contract_tutorial.md b/versioned_docs/version-0.14/builder/tutorials/recipes/rust/counter_contract_tutorial.md new file mode 100644 index 00000000..866deb2b --- /dev/null +++ b/versioned_docs/version-0.14/builder/tutorials/recipes/rust/counter_contract_tutorial.md @@ -0,0 +1,617 @@ +--- +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_ + +## Overview + +In this tutorial, we will build a simple counter smart contract that maintains a count, deploy it to the Miden testnet, and interact with it by incrementing the count. You can also deploy the counter contract on a locally running Miden node, similar to previous tutorials. + +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 +- Pure 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 + +Create a new Rust repository for your Miden project and navigate to it with the following command: + +```bash +cargo new miden-counter-contract +cd miden-counter-contract +``` + +Add the following dependencies to your `Cargo.toml` file: + +```toml +[dependencies] +miden-client = { version = "0.14", features = ["testing", "tonic"] } +miden-client-sqlite-store = { version = "0.14", package = "miden-client-sqlite-store" } +miden-protocol = { version = "0.14" } +rand = { version = "0.9" } +serde = { version = "1", features = ["derive"] } +serde_json = { version = "1.0", features = ["raw_value"] } +tokio = { version = "1.46", features = ["rt-multi-thread", "net", "macros", "fs"] } +rand_chacha = "0.9.0" +``` + +### Set up your `src/main.rs` file + +In the previous section, we explained how to instantiate the Miden client. We can reuse the same `initialize_client` function for our counter contract. + +Copy and paste the following code into your `src/main.rs` file: + +```rust no_run +use miden_client::auth::NoAuth; +use miden_client::transaction::TransactionKernel; +use rand::RngCore; +use std::{fs, path::Path, sync::Arc}; + +use miden_client::{ + address::NetworkId, + assembly::{ + Assembler, + CodeBuilder, + DefaultSourceManager, + Module, + ModuleKind, + Path as AssemblyPath, + }, + builder::ClientBuilder, + keystore::FilesystemKeyStore, + rpc::{Endpoint, GrpcClient}, + transaction::TransactionRequestBuilder, + ClientError, +}; +use miden_client_sqlite_store::ClientBuilderSqliteExt; +use miden_client::{ + account::{ + component::AccountComponentMetadata, AccountBuilder, AccountComponent, AccountStorageMode, + AccountType, StorageSlot, StorageSlotName, + }, + Word, +}; + +fn create_library( + library_path: &str, + source_code: &str, +) -> Result, Box> { + let source_manager = Arc::new(DefaultSourceManager::default()); + let assembler = TransactionKernel::assembler_with_source_manager(source_manager.clone()); + let module = Module::parser(ModuleKind::Library).parse_str( + AssemblyPath::new(library_path), + source_code, + source_manager, + )?; + let library = assembler.assemble_library([module])?; + Ok(library) +} + +#[tokio::main] +async fn main() -> Result<(), ClientError> { + // Initialize client + let endpoint = Endpoint::testnet(); + let timeout_ms = 10_000; + let rpc_client = Arc::new(GrpcClient::new(&endpoint, timeout_ms)); + + // 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"); + + let mut client = ClientBuilder::new() + .rpc(rpc_client) + .sqlite_store(store_path) + .authenticator(keystore.clone()) + .in_debug_mode(true.into()) + .build() + .await?; + + let sync_summary = client.sync_state().await.unwrap(); + println!("Latest block: {}", sync_summary.block_num); + + 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 + +For better code organization, we will separate the Miden assembly code from our Rust code. + +Create a directory named `masm` at the **root** of your `miden-counter-contract` directory. This will contain our contract and script masm code. + +Initialize the `masm` directory: + +```bash +mkdir -p masm/accounts masm/scripts +``` + +This will create: + +```text +masm/ +├── accounts/ +└── scripts/ +``` + +### Custom Miden smart contract + +Below is our counter contract. It has a 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`, `miden::core::word`, and +`miden::core::sys`. + +The `miden::protocol::active_account` and `miden::protocol::native_account` modules contain +procedures for reading and writing contract state. We use `miden::core::word` to convert the slot +name into a slot ID for 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. Calls `sys::truncate_stack` to clean up the stack. + +Inside of the `masm/accounts/` directory, create the `counter.masm` file: + +```masm +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") + +#! Inputs: [] +#! Outputs: [count] +pub proc get_count + push.COUNTER_SLOT[0..2] exec.active_account::get_item + # => [count] + + exec.sys::truncate_stack + # => [count] +end + +#! Inputs: [] +#! Outputs: [] +pub proc increment_count + push.COUNTER_SLOT[0..2] exec.active_account::get_item + # => [count] + + add.1 + # => [count+1] + + push.COUNTER_SLOT[0..2] exec.native_account::set_item + # => [] + + exec.sys::truncate_stack + # => [] +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 + +**Important**: Starting with Miden Client 0.10.0, all accounts must have an authentication component. For smart contracts that don't require authentication (like our counter contract), we use a `NoAuth` component. + +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 string `{increment_count}` will be replaced with the hash of the `increment_count` procedure in our rust program. + +Inside of the `masm/scripts/` directory, create the `counter_script.masm` file: + +```masm +use external_contract::counter_contract + +begin + call.counter_contract::increment_count +end +``` + +## Step 3: Build the counter smart contract + +To build the counter contract copy and paste the following code at the end of your `src/main.rs` file: + +```rust ignore +// ------------------------------------------------------------------------- +// STEP 1: Create a basic counter contract +// ------------------------------------------------------------------------- +println!("\n[STEP 1] Creating counter contract."); + +// Load the MASM file for the counter contract +let counter_path = Path::new("../masm/accounts/counter.masm"); +let counter_code = fs::read_to_string(counter_path).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 = CodeBuilder::new() + .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", AccountType::all()), +) +.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::RegularAccountImmutableCode) + .storage_mode(AccountStorageMode::Public) + .with_component(counter_component.clone()) + .with_auth_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(); +``` + +Run the following command to execute `src/main.rs`: + +```bash +cargo run --release +``` + +After the program executes, you should see the counter contract hash and contract id printed to the terminal, for example: + +```text +[STEP 1] Creating counter contract. +counter_contract commitment: RpoDigest([3700134472268167470, 14878091556015233722, 3335592073702485043, 16978997897830363420]) +counter_contract id: "" +counter_contract storage: AccountStorage { slots: [Value([0, 0, 0, 0]), Value([0, 0, 0, 0])] } +``` + +## Step 4: Incrementing the count + +Now that we built the counter contract, lets create a transaction request to increment the count: + +Paste the following code at the end of your `src/main.rs` file: + +```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_path = Path::new("../masm/scripts/counter_script.masm"); +let script_code = fs::read_to_string(script_path).unwrap(); + +// Create a library from the counter contract code +let account_component_lib = create_library( + "external_contract::counter_contract", + &counter_code, +) +.unwrap(); + +let tx_script = client + .code_builder() + .with_dynamically_linked_library(&account_component_lib) + .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_new_transaction(counter_contract.id(), tx_increment_request) + .await + .unwrap(); + +println!( + "View transaction on MidenScan: https://testnet.midenscan.com/tx/{:?}", + tx_id +); + +println!( + "Counter contract id: {:?}", + counter_contract.id().to_bech32(NetworkId::Testnet) +); + +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) +); +``` + +**Note**: _Once our counter contract is deployed, other users can increment the count of the smart contract simply by knowing the account id of the contract and the procedure hash of the `increment_count` procedure._ + +## Summary + +The final `src/main.rs` file should look like this: + +```rust no_run +use miden_client::auth::NoAuth; +use miden_client::transaction::TransactionKernel; +use rand::RngCore; +use std::{fs, path::Path, sync::Arc}; + +use miden_client::{ + address::NetworkId, + assembly::{ + Assembler, + CodeBuilder, + DefaultSourceManager, + Module, + ModuleKind, + Path as AssemblyPath, + }, + builder::ClientBuilder, + keystore::FilesystemKeyStore, + rpc::{Endpoint, GrpcClient}, + transaction::TransactionRequestBuilder, + ClientError, +}; +use miden_client_sqlite_store::ClientBuilderSqliteExt; +use miden_client::{ + account::{ + component::AccountComponentMetadata, AccountBuilder, AccountComponent, AccountStorageMode, + AccountType, StorageSlot, StorageSlotName, + }, + Word, +}; + +fn create_library( + library_path: &str, + source_code: &str, +) -> Result, Box> { + let source_manager = Arc::new(DefaultSourceManager::default()); + let assembler = TransactionKernel::assembler_with_source_manager(source_manager.clone()); + let module = Module::parser(ModuleKind::Library).parse_str( + AssemblyPath::new(library_path), + source_code, + source_manager, + )?; + let library = assembler.assemble_library([module])?; + Ok(library) +} + +#[tokio::main] +async fn main() -> Result<(), ClientError> { + // Initialize client + let endpoint = Endpoint::testnet(); + let timeout_ms = 10_000; + let rpc_client = Arc::new(GrpcClient::new(&endpoint, timeout_ms)); + + // 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"); + + let mut client = ClientBuilder::new() + .rpc(rpc_client) + .sqlite_store(store_path) + .authenticator(keystore.clone()) + .in_debug_mode(true.into()) + .build() + .await?; + + let sync_summary = client.sync_state().await.unwrap(); + println!("Latest block: {}", sync_summary.block_num); + + // ------------------------------------------------------------------------- + // STEP 1: Create a basic counter contract + // ------------------------------------------------------------------------- + println!("\n[STEP 1] Creating counter contract."); + + // Load the MASM file for the counter contract + let counter_path = Path::new("../masm/accounts/counter.masm"); + let counter_code = fs::read_to_string(counter_path).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 = CodeBuilder::new() + .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", AccountType::all()), + ) + .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::RegularAccountImmutableCode) + .storage_mode(AccountStorageMode::Public) + .with_component(counter_component.clone()) + .with_auth_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(); + + // ------------------------------------------------------------------------- + // 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_path = Path::new("../masm/scripts/counter_script.masm"); + let script_code = fs::read_to_string(script_path).unwrap(); + + // Create a library from the counter contract code + let account_component_lib = create_library( + "external_contract::counter_contract", + &counter_code, + ) + .unwrap(); + + let tx_script = client + .code_builder() + .with_dynamically_linked_library(&account_component_lib) + .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_new_transaction(counter_contract.id(), tx_increment_request) + .await + .unwrap(); + + println!( + "View transaction on MidenScan: https://testnet.midenscan.com/tx/{:?}", + tx_id + ); + + println!( + "Counter contract id: {:?}", + counter_contract.id().to_bech32(NetworkId::Testnet) + ); + + 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) + ); + + Ok(()) +} +``` + +The output of our program will look something like this: + +```text +Latest block: 374255 + +[STEP 1] Creating counter contract. +one or more warnings were emitted +counter_contract commitment: Word([3964727668949550262, 4265714847747507878, 5784293172192015964, 16803438753763367241]) +counter_contract id: "" +counter_contract storage: AccountStorage { slots: [Value(Word([0, 0, 0, 0]))] } + +[STEP 2] Call Counter Contract With Script +Stack state before step 2610: +├── 0: 1 +├── 1: 0 +├── 2: 0 +├── 3: 0 +├── 4: 0 +├── 5: 0 +├── 6: 0 +├── 7: 0 +├── 8: 0 +├── 9: 0 +├── 10: 0 +├── 11: 0 +├── 12: 0 +├── 13: 0 +├── 14: 0 +├── 15: 0 +├── 16: 0 +├── 17: 0 +├── 18: 0 +└── 19: 0 + +└── (0 more items) + +View transaction on MidenScan: https://testnet.midenscan.com/tx/0x9767940bbed7bd3a74c24dc43f1ea8fe90a876dc7925621c217f648c63c4ab7a +counter contract storage: Ok(Word([0, 0, 0, 1])) +``` + +The line in the output `Stack state before step 2505` ouputs the stack state when we call "debug.stack" in the `counter.masm` file. + +To increment the count of the counter contract all you need is to know the account id of the counter and the procedure hash of the `increment_count` procedure. To increment the count without deploying the counter each time, you can modify the program above to hardcode the account id of the counter and the procedure hash of the `increment_count` prodedure in the masm script. + +### Running the example + +To run the full example, navigate to the `rust-client` directory in the [miden-tutorials](https://github.com/0xMiden/miden-tutorials/) repository and run this command: + +```bash +cd rust-client +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.14/builder/tutorials/recipes/rust/create_deploy_tutorial.md b/versioned_docs/version-0.14/builder/tutorials/recipes/rust/create_deploy_tutorial.md new file mode 100644 index 00000000..f355e862 --- /dev/null +++ b/versioned_docs/version-0.14/builder/tutorials/recipes/rust/create_deploy_tutorial.md @@ -0,0 +1,400 @@ +--- +title: "Creating Accounts and Faucets" +sidebar_position: 2 +--- + +# Creating Accounts and Faucets + +_Using the Miden client in Rust to create accounts and deploy faucets_ + +## 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 + +Before you begin, ensure that a Miden node is running locally in a separate terminal window. To get the Miden node running locally, you can follow the instructions on the [Miden Node Setup](../miden_node_setup.md) page. + +## 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: The account's state and logic are off-chain, only known to its owner. +- 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. If the note is private, the note transfer is only known to the sender and receiver._ + +## Step 1: Initialize your repository + +Create a new Rust repository for your Miden project and navigate to it with the following command: + +```bash +cargo new miden-rust-client +cd miden-rust-client +``` + +Add the following dependencies to your `Cargo.toml` file: + +```toml +[dependencies] +miden-client = { version = "0.14", features = ["testing", "tonic"] } +miden-client-sqlite-store = { version = "0.14", package = "miden-client-sqlite-store" } +miden-protocol = { version = "0.14" } +rand = { version = "0.9" } +serde = { version = "1", features = ["derive"] } +serde_json = { version = "1.0", features = ["raw_value"] } +tokio = { version = "1.46", features = ["rt-multi-thread", "net", "macros", "fs"] } +rand_chacha = "0.9.0" +``` + +## 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 miden_client::auth::{AuthSchemeId, AuthSingleSig}; +use rand::RngCore; +use std::sync::Arc; +use tokio::time::Duration; + +use miden_client::{ + account::{ + component::{BasicFungibleFaucet, BasicWallet}, + AccountId, + }, + address::NetworkId, + auth::AuthSecretKey, + builder::ClientBuilder, + keystore::FilesystemKeyStore, + note::{NoteType, P2idNote}, + rpc::{Endpoint, GrpcClient}, + transaction::TransactionRequestBuilder, + ClientError, +}; +use miden_client_sqlite_store::ClientBuilderSqliteExt; +use miden_protocol::account::AccountIdVersion; +use miden_client::{ + account::{AccountBuilder, AccountStorageMode, AccountType}, + asset::{FungibleAsset, TokenSymbol}, + Felt, +}; + +#[tokio::main] +async fn main() -> Result<(), ClientError> { + // Initialize client + let endpoint = Endpoint::testnet(); + let timeout_ms = 10_000; + let rpc_client = Arc::new(GrpcClient::new(&endpoint, timeout_ms)); + + // 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"); + + let mut client = ClientBuilder::new() + .rpc(rpc_client) + .sqlite_store(store_path) + .authenticator(keystore.clone()) + .in_debug_mode(true.into()) + .build() + .await?; + + let sync_summary = client.sync_state().await.unwrap(); + println!("Latest block: {}", sync_summary.block_num); + + 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 +cargo run --release +``` + +After the program executes, you should see the latest block number printed to the terminal, for example: + +```text +Latest block number: 3855 +``` + +## 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 define the account type as mutable or immutable and specify whether it is public or private. A mutable wallet means you can change the account code after deployment. A wallet on Miden is simply an account with standardized code. + +In the example below we create a mutable public account for Alice. + +Add this snippet to the end of your file in the `main()` function: + +```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::RegularAccountUpdatableCode) + .storage_mode(AccountStorageMode::Public) + .with_auth_component(AuthSingleSig::new(key_pair.public_key().to_commitment(), AuthSchemeId::Falcon512Poseidon2)) + .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 +use miden_client::keystore::Keystore; +keystore.add_key(&key_pair, alice_account.id()).await.unwrap(); + +let alice_account_id_bech32 = alice_account.id().to_bech32(NetworkId::Testnet); +println!("Alice's account ID: {:?}", alice_account_id_bech32); +``` + +## Step 4: Deploying a fungible faucet + +To provide Alice with testnet assets, we must first deploy a faucet. A faucet account on Miden mints fungible tokens. + +We'll create a public faucet with a token symbol, decimals, and a max supply. We will use this faucet to mint tokens to Alice's account in the next section. + +Add this snippet to the end of your file in the `main()` function: + +```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 = Felt::new(1_000_000); + +// Generate key pair +let key_pair = AuthSecretKey::new_falcon512_poseidon2_with_rng(client.rng()); + +// Build the faucet account +let faucet_account = AccountBuilder::new(init_seed) + .account_type(AccountType::FungibleFaucet) + .storage_mode(AccountStorageMode::Public) + .with_auth_component(AuthSingleSig::new(key_pair.public_key().to_commitment(), AuthSchemeId::Falcon512Poseidon2)) + .with_component(BasicFungibleFaucet::new(symbol, decimals, max_supply).unwrap()) + .build() + .unwrap(); + +// Add the faucet to the client +client.add_account(&faucet_account, false).await?; + +// Add the key pair to the keystore +use miden_client::keystore::Keystore; +keystore.add_key(&key_pair, faucet_account.id()).await.unwrap(); + +let faucet_account_id_bech32 = faucet_account.id().to_bech32(NetworkId::Testnet); +println!("Faucet account ID: {:?}", faucet_account_id_bech32); + +// Resync to show newly deployed faucet +client.sync_state().await?; +tokio::time::sleep(Duration::from_secs(2)).await; +``` + +_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 miden_client::auth::{AuthSchemeId, AuthSingleSig}; +use rand::RngCore; +use std::sync::Arc; +use tokio::time::Duration; + +use miden_client::{ + account::{ + component::{AuthControlled, BasicFungibleFaucet, BasicWallet}, + AccountId, + }, + address::NetworkId, + auth::AuthSecretKey, + builder::ClientBuilder, + keystore::{FilesystemKeyStore, Keystore}, + note::{NoteType, P2idNote}, + rpc::{Endpoint, GrpcClient}, + transaction::TransactionRequestBuilder, + ClientError, +}; +use miden_client_sqlite_store::ClientBuilderSqliteExt; +use miden_protocol::account::AccountIdVersion; +use miden_client::{ + account::{AccountBuilder, AccountStorageMode, AccountType}, + asset::{FungibleAsset, TokenSymbol}, + Felt, +}; + +#[tokio::main] +async fn main() -> Result<(), ClientError> { + // Initialize client + let endpoint = Endpoint::testnet(); + let timeout_ms = 10_000; + let rpc_client = Arc::new(GrpcClient::new(&endpoint, timeout_ms)); + + // 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"); + + let mut client = ClientBuilder::new() + .rpc(rpc_client) + .sqlite_store(store_path) + .authenticator(keystore.clone()) + .in_debug_mode(true.into()) + .build() + .await?; + + let sync_summary = client.sync_state().await.unwrap(); + println!("Latest block: {}", sync_summary.block_num); + + //------------------------------------------------------------ + // 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::RegularAccountUpdatableCode) + .storage_mode(AccountStorageMode::Public) + .with_auth_component(AuthSingleSig::new(key_pair.public_key().to_commitment(), AuthSchemeId::Falcon512Poseidon2)) + .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(NetworkId::Testnet); + println!("Alice's account ID: {:?}", alice_account_id_bech32); + + //------------------------------------------------------------ + // 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 = Felt::new(1_000_000); + + // Generate key pair + let key_pair = AuthSecretKey::new_falcon512_poseidon2_with_rng(client.rng()); + + // Build the faucet account + let faucet_account = AccountBuilder::new(init_seed) + .account_type(AccountType::FungibleFaucet) + .storage_mode(AccountStorageMode::Public) + .with_auth_component(AuthSingleSig::new(key_pair.public_key().to_commitment(), AuthSchemeId::Falcon512Poseidon2)) + .with_component(BasicFungibleFaucet::new(symbol, decimals, max_supply).unwrap()) + .with_component(AuthControlled::allow_all()) + .build() + .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(NetworkId::Testnet); + println!("Faucet account ID: {:?}", faucet_account_id_bech32); + + // 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 +cargo run --release +``` + +The output will look like this: + +```text +Latest block: 17771 + +[STEP 1] Creating a new account for Alice +Alice's account ID: "0x3cb3e596d14ad410000017901eaa7b" + +[STEP 2] Deploying a new fungible faucet. +Faucet account ID: "0x6ad1894ac233e4200000088311bb6b" +``` + +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 + +To run a full working example navigate to the `rust-client` directory in the [miden-tutorials](https://github.com/0xMiden/miden-tutorials/) repository and run this command: + +```bash +cd rust-client +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.14/builder/tutorials/recipes/rust/creating_notes_in_masm_tutorial.md b/versioned_docs/version-0.14/builder/tutorials/recipes/rust/creating_notes_in_masm_tutorial.md new file mode 100644 index 00000000..4f831b41 --- /dev/null +++ b/versioned_docs/version-0.14/builder/tutorials/recipes/rust/creating_notes_in_masm_tutorial.md @@ -0,0 +1,523 @@ +--- +title: "How to Create Notes in Miden Assembly" +sidebar_position: 11 +--- + +# Creating Notes in Miden Assembly + +_Creating notes inside the MidenVM using Miden assembly_ + +## 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 inputs commitment 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 snapshots of an account's state at a specific point in time (not possible in an EVM context) +- 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, we will create a note that contains an asset. When consumed, it outputs a copy of itself and allows the consuming account to take half of the asset. Although this type of note would not be used in a real-world context, it demonstrates several key concepts for writing MASM code that can create notes. + +## Step 1: Initialize Your Repository + +Create a new Rust repository for your Miden project and navigate to it with the following command: + +```bash +cargo new miden-project +cd miden-project +``` + +Add the following dependencies to your `Cargo.toml` file: + +```toml +[dependencies] +miden-client = { version = "0.14", features = ["testing", "tonic"] } +miden-client-sqlite-store = { version = "0.14", package = "miden-client-sqlite-store" } +miden-protocol = { version = "0.14" } +rand = { version = "0.9" } +serde = { version = "1", features = ["derive"] } +serde_json = { version = "1.0", features = ["raw_value"] } +tokio = { version = "1.46", features = ["rt-multi-thread", "net", "macros", "fs"] } +rand_chacha = "0.9.0" +``` + +## Step 2: Write the Note Script + +For better code organization, we will separate the Miden assembly code from our Rust code. + +Create a directory named `masm` at the **root** of your `miden-project` directory. This directory will contain our contract and MASM script code. + +Initialize the `masm` directory: + +```bash +mkdir masm/notes +``` + +This will create: + +```text +masm/ +└── notes/ +``` + +Inside the `masm/notes/` directory, create the file `iterative_output_note.masm`: + +```masm +use miden::protocol::active_note +use miden::protocol::note +use miden::protocol::output_note +use miden::core::sys +use miden::standards::wallets::basic->wallet + +# Memory Addresses +# get_assets writes: ASSET_KEY at ASSET_KEY_PTR, ASSET_VALUE at ASSET_KEY_PTR+4 (ASSET_SIZE=8) +const ASSET_KEY_PTR=0 +const ASSET_VALUE_PTR=4 +const ASSET_HALF_VALUE_PTR=8 # half-amount ASSET_VALUE stored here +const ACCOUNT_ID_PREFIX=12 # storage: [prefix, suffix, tag, 0] +const TAG=14 # = ACCOUNT_ID_PREFIX + 2 + +# => [] +begin + # Drop word if user accidentally pushes note_args + dropw + # => [] + + # Get asset contained in note into memory (ASSET_KEY at 0, ASSET_VALUE at 4) + push.ASSET_KEY_PTR exec.active_note::get_assets drop drop + # => [] + + # Load ASSET_VALUE and compute half amount + padw push.ASSET_VALUE_PTR mem_loadw_le + # => [av0, av1, av2, av3] (av0 = amount for fungible asset, av1/av2/av3 = 0) + + # Halve the amount (av0 is amount for fungible assets in v0.14) + push.2 div + # => [av0/2, av1, av2, av3] + + # Store as ASSET_HALF_VALUE + mem_storew_le.ASSET_HALF_VALUE_PTR dropw + # => [] + + # Receive all assets from note into the account wallet + exec.wallet::add_assets_to_account + # => [] + + # Push script hash + exec.active_note::get_script_root + # => [SCRIPT_HASH] + + # Get the current note serial number + exec.active_note::get_serial_number + # => [SERIAL_NUM, SCRIPT_HASH] + + # 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 + # => [SERIAL_NUM+1, SCRIPT_HASH] + + # Load note storage into memory for recipient construction + push.ACCOUNT_ID_PREFIX + exec.active_note::get_storage + # => [num_storage_items, dest_ptr, SERIAL_NUM+1, SCRIPT_HASH] + + swap + # => [dest_ptr, num_storage_items, SERIAL_NUM+1, SCRIPT_HASH] + + exec.note::build_recipient + # => [RECIPIENT] + + # Push note type to stack (public note = 1) + push.1 + # => [note_type, RECIPIENT] + + # Load tag from memory + mem_load.TAG + # => [tag, note_type, RECIPIENT] + + exec.output_note::create + # => [note_idx] + + # Build [ASSET_KEY, ASSET_HALF_VALUE, note_idx] for move_asset_to_note + # Inputs: [ASSET_KEY, 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] + + # Push ASSET_KEY (ASSET_HALF_VALUE moves to depth 4, note_idx to depth 8) + padw push.ASSET_KEY_PTR mem_loadw_le + # => [ASSET_KEY, ASSET_HALF_VALUE, note_idx] + + call.wallet::move_asset_to_note + # => [pad(16)] + + dropw dropw dropw dropw + # => [] + + exec.sys::truncate_stack + # => [] +end +``` + +### How the Assembly Code Works: + +1. **Retrieving the asset:** + The note calls `active_note::get_assets` to write the asset into memory, with `ASSET_KEY` 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::add_assets_to_account` to receive all note assets 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::build_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, the script pushes the note type and tag onto the stack, then calls the `output_note::create` procedure, which returns the note index. +5. **Moving assets to the note:** + After the note is created, the script loads `ASSET_KEY` 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 miden_client::auth::{AuthSchemeId, AuthSingleSig}; +use rand::RngCore; +use std::{fs, path::Path, sync::Arc}; +use tokio::time::{sleep, Duration}; + +use miden_client::{ + account::{ + component::{AuthControlled, BasicFungibleFaucet, BasicWallet}, + Account, + }, + address::NetworkId, + asset::{FungibleAsset, TokenSymbol}, + auth::AuthSecretKey, + builder::ClientBuilder, + crypto::FeltRng, + keystore::{FilesystemKeyStore, Keystore}, + note::{ + Note, NoteAssets, NoteMetadata, NoteRecipient, NoteStorage, NoteTag, NoteType, + }, + rpc::{Endpoint, GrpcClient}, + transaction::TransactionRequestBuilder, + Client, ClientError, Felt, +}; +use miden_client_sqlite_store::ClientBuilderSqliteExt; +use miden_client::{ + account::{AccountBuilder, AccountStorageMode, AccountType}, + note::NoteDetails, +}; + +// 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::RegularAccountUpdatableCode) + .storage_mode(AccountStorageMode::Public) + .with_auth_component(AuthSingleSig::new(key_pair.public_key().to_commitment(), AuthSchemeId::Falcon512Poseidon2)) + .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 = Felt::new(1_000_000); + + let account = AccountBuilder::new(init_seed) + .account_type(AccountType::FungibleFaucet) + .storage_mode(AccountStorageMode::Public) + .with_auth_component(AuthSingleSig::new(key_pair.public_key().to_commitment(), AuthSchemeId::Falcon512Poseidon2)) + .with_component(BasicFungibleFaucet::new(symbol, decimals, max_supply).unwrap()) + .with_component(AuthControlled::allow_all()) + .build() + .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, +) -> Result<(), ClientError> { + loop { + client.sync_state().await?; + let notes = client.get_consumable_notes(Some(account_id.id())).await?; + if notes.len() >= expected { + break; + } + println!( + "{} consumable notes found for account {}. Waiting...", + notes.len(), + account_id.id().to_bech32(NetworkId::Testnet) + ); + sleep(Duration::from_secs(3)).await; + } + Ok(()) +} + +#[tokio::main] +async fn main() -> Result<(), ClientError> { + // Initialize client + let endpoint = Endpoint::testnet(); + let timeout_ms = 10_000; + let rpc_client = Arc::new(GrpcClient::new(&endpoint, timeout_ms)); + + // 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"); + + let mut client = ClientBuilder::new() + .rpc(rpc_client) + .sqlite_store(store_path) + .authenticator(keystore.clone()) + .in_debug_mode(true.into()) + .build() + .await?; + + let sync_summary = client.sync_state().await.unwrap(); + println!("Latest block: {}", sync_summary.block_num); + + // ------------------------------------------------------------------------- + // 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(NetworkId::Testnet) + ); + let bob_account = create_basic_account(&mut client, &keystore).await?; + println!( + "Bob's account ID: {:?}", + bob_account.id().to_bech32(NetworkId::Testnet) + ); + + println!("\nDeploying a new fungible faucet."); + let faucet = create_basic_faucet(&mut client, &keystore).await?; + println!( + "Faucet account ID: {:?}", + faucet.id().to_bech32(NetworkId::Testnet) + ); + 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_new_transaction(faucet.id(), tx_req).await?; + println!("Minted tokens. TX: {:?}", tx_id); + + wait_for_notes(&mut client, &alice_account, 1).await?; + + // Consume the minted note + let consumable_notes = client + .get_consumable_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_new_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"); + + let code = fs::read_to_string(Path::new("../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 = NoteMetadata::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(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_new_transaction(alice_account.id(), note_req) + .await?; + println!( + "View transaction on MidenScan: https://testnet.midenscan.com/tx/{:?}", + 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], + Felt::new(serial_num[3].as_canonical_u64() + 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 = NoteMetadata::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_new_transaction(bob_account.id(), consume_custom_req) + .await?; + println!( + "Consumed Note Tx on MidenScan: https://testnet.midenscan.com/tx/{:?}", + tx_id + ); + + Ok(()) +} +``` + +Run the following command to execute `src/main.rs`: + +```bash +cargo run --release +``` + +The output will look something like this: + +```text +Latest block: 4186 + +[STEP 1] Creating new accounts +Alice's account ID: "mtst1aqnztxg76d5exyr7ja05pzhvegx3jc84" +Bob's account ID: "mtst1az0mfyzm3xqe2yrezucvmc24dv5gset0" + +Deploying a new fungible faucet. +Faucet account ID: "mtst1ary7kplxvatxkgpes4llcc53yu8px433" + +[STEP 2] Mint tokens with P2ID +Minted tokens. TX: 0x8577213057226b7d0545d73f6e1bc416354a324089450cb859f5784c133d4cc0 +0 consumable notes found for account mtst1aqnztxg76d5exyr7ja05pzhvegx3jc84. Waiting... +Consumed minted note. TX: 0xd93e791d3283192df121de4cf938a4f9cfaed48f4e593e1b82d147e2d642b7bd + +[STEP 3] Create iterative output note +View transaction on MidenScan: https://testnet.midenscan.com/tx/0xcd71a1d87c60ab7632a7836723fff4014b02992ef7ea5d3fe2030a971e795a8a + +[STEP 4] Bob consumes the note and creates a copy +Consumed Note Tx on MidenScan: https://testnet.midenscan.com/tx/0x4d91519c180874c931480fa58620f864fd7c7aada35ada2d40082291298084bb +Account delta: AccountVaultDelta { fungible: FungibleAssetDelta({V0(Accoun +``` + +--- + +### Running the example + +To run the full example, navigate to the `rust-client` directory in the [miden-tutorials](https://github.com/0xMiden/miden-tutorials/) repository and run this command: + +```bash +cd rust-client +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.14/builder/tutorials/recipes/rust/custom_note_how_to.md b/versioned_docs/version-0.14/builder/tutorials/recipes/rust/custom_note_how_to.md new file mode 100644 index 00000000..7fc58599 --- /dev/null +++ b/versioned_docs/version-0.14/builder/tutorials/recipes/rust/custom_note_how_to.md @@ -0,0 +1,437 @@ +--- +title: "How To Create Notes with Custom Logic" +sidebar_position: 7 +--- + +# How to Create a Custom Note + +_Creating notes with custom logic_ + +## 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. +- Leverage Miden’s privacy features to keep certain transaction details private. + +Unlike Ethereum, where all pending transactions are publicly visible in the mempool, Miden enables you to partially or completely hide transaction details. + +## 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). +- For simplicity, we're only hashing 4 elements. Therefore, we prepend an empty word—consisting of 4 zero integers—as a placeholder. This is required by the RPO hashing algorithm to ensure the input has the correct structure and length for proper processing. +- Compute the hash of the secret. The resulting hash will serve as the note’s input, 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. **Note Inputs:** + - The note is set up with the asset and the hash of the secret number as its input. +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 input. + +Below is the Miden Assembly code for the note: + +```masm +use miden::protocol::active_note +use miden::standards::wallets::basic->wallet + +# CONSTANTS +# ================================================================================================= + +const EXPECTED_DIGEST_PTR=0 + +# ERRORS +# ================================================================================================= + +const ERROR_DIGEST_MISMATCH="Expected digest does not match computed digest" + +#! Inputs (arguments): [HASH_PREIMAGE_SECRET] +#! Outputs: [] +#! +#! Note storage is assumed to be as follows: +#! => EXPECTED_DIGEST +begin + # => HASH_PREIMAGE_SECRET + # Hashing the secret number + hash + # => [DIGEST] + + # Writing the note storage to memory + push.EXPECTED_DIGEST_PTR exec.active_note::get_storage drop 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] + + # Assert that the note input matches the digest + # Will fail if the two hashes do not match + assert_eqw.err=ERROR_DIGEST_MISMATCH + # => [] + + # --------------------------------------------------------------------------------------------- + # If the check is successful, we allow for the asset to be consumed + # --------------------------------------------------------------------------------------------- + + # Add all assets from the note to the account + exec.wallet::add_assets_to_account + # => [] +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::add_assets_to_account` transfers all note assets into the consuming account's vault. + +### 5. 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. + +--- + +## 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 miden_client::auth::{AuthSchemeId, AuthSingleSig}; +use rand::RngCore; +use std::{fs, path::Path, sync::Arc}; +use tokio::time::{sleep, Duration}; + +use miden_client::{ + account::{ + component::{AuthControlled, BasicFungibleFaucet, BasicWallet}, + Account, + }, + address::NetworkId, + auth::AuthSecretKey, + builder::ClientBuilder, + crypto::FeltRng, + keystore::{FilesystemKeyStore, Keystore}, + note::{ + Note, NoteAssets, NoteMetadata, NoteRecipient, NoteStorage, NoteTag, NoteType, + }, + rpc::{Endpoint, GrpcClient}, + store::TransactionFilter, + transaction::{TransactionId, TransactionRequestBuilder, TransactionStatus}, + Client, ClientError, Felt, +}; +use miden_client_sqlite_store::ClientBuilderSqliteExt; +use miden_client::{ + account::{AccountBuilder, AccountStorageMode, AccountType}, + asset::{FungibleAsset, TokenSymbol}, +}; +use miden_protocol::Hasher; + +// 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::RegularAccountUpdatableCode) + .storage_mode(AccountStorageMode::Public) + .with_auth_component(AuthSingleSig::new(key_pair.public_key().to_commitment(), AuthSchemeId::Falcon512Poseidon2)) + .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 = Felt::new(1_000_000); + + let account = AccountBuilder::new(init_seed) + .account_type(AccountType::FungibleFaucet) + .storage_mode(AccountStorageMode::Public) + .with_auth_component(AuthSingleSig::new(key_pair.public_key().to_commitment(), AuthSchemeId::Falcon512Poseidon2)) + .with_component(BasicFungibleFaucet::new(symbol, decimals, max_supply).unwrap()) + .with_component(AuthControlled::allow_all()) + .build() + .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> { + loop { + client.sync_state().await?; + + // Check transaction status + let txs = client + .get_transactions(TransactionFilter::Ids(vec![tx_id])) + .await?; + let tx_committed = if !txs.is_empty() { + matches!(txs[0].status, TransactionStatus::Committed { .. }) + } else { + false + }; + + if tx_committed { + println!("✅ transaction {} committed", tx_id.to_hex()); + break; + } + + println!( + "Transaction {} not yet committed. Waiting...", + tx_id.to_hex() + ); + sleep(Duration::from_secs(2)).await; + } + Ok(()) +} + +#[tokio::main] +async fn main() -> Result<(), ClientError> { + // Initialize client + let endpoint = Endpoint::testnet(); + let timeout_ms = 10_000; + let rpc_client = Arc::new(GrpcClient::new(&endpoint, timeout_ms)); + + // 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"); + + let mut client = ClientBuilder::new() + .rpc(rpc_client) + .sqlite_store(store_path) + .authenticator(keystore.clone()) + .in_debug_mode(true.into()) + .build() + .await?; + + let sync_summary = client.sync_state().await.unwrap(); + println!("Latest block: {}", sync_summary.block_num); + + // ------------------------------------------------------------------------- + // 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(NetworkId::Testnet) + ); + let bob_account = create_basic_account(&mut client, &keystore).await?; + println!( + "Bob's account ID: {:?}", + bob_account.id().to_bech32(NetworkId::Testnet) + ); + + println!("\nDeploying a new fungible faucet."); + let faucet = create_basic_faucet(&mut client, &keystore).await?; + println!( + "Faucet account ID: {:?}", + faucet.id().to_bech32(NetworkId::Testnet) + ); + 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_new_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_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]) + .unwrap(); + + let tx_id = client + .submit_new_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(1), Felt::new(2), Felt::new(3), Felt::new(4)]; + let digest = Hasher::hash_elements(&secret_vals); + println!("digest: {:?}", digest); + + let code = fs::read_to_string(Path::new("../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 = NoteMetadata::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_new_transaction(alice_account.id(), note_request) + .await?; + println!( + "View transaction on MidenScan: https://testnet.midenscan.com/tx/{:?}", + 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(1), Felt::new(2), Felt::new(3), Felt::new(4)]; + let consume_custom_request = TransactionRequestBuilder::new() + .input_notes([(custom_note, Some(secret.into()))]) + .build() + .unwrap(); + + let tx_id = client + .submit_new_transaction(bob_account.id(), consume_custom_request) + .await?; + println!( + "Consumed Note Tx on MidenScan: https://testnet.midenscan.com/tx/{:?} \n", + tx_id + ); + + Ok(()) +} +``` + +The output of our program will look something like this: + +```text +Latest block: 226943 + +[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 +Note 0x88d8c4a50c0e6342e58026b051fb6038867de21d3bd3963aec67fd6c45861faf not found. Waiting... +Note 0x88d8c4a50c0e6342e58026b051fb6038867de21d3bd3963aec67fd6c45861faf not found. Waiting... +✅ note found 0x88d8c4a50c0e6342e58026b051fb6038867de21d3bd3963aec67fd6c45861faf + +[STEP 3] Create custom note +digest: RpoDigest([14371582251229115050, 1386930022051078873, 17689831064175867466, 9632123050519021080]) +note hash: "0x14c66143377223e090e5b4da0d1e5ce6c6521622ad5b92161a704a25c915769b" +View transaction on MidenScan: https://testnet.midenscan.com/tx/0xffbee228a2c6283efe958c6b3cd31af88018c029221b413b0f23fcfacb2cb611 + +[STEP 4] Bob consumes the Custom Note with Correct Secret +Consumed Note Tx on MidenScan: https://testnet.midenscan.com/tx/0xe6c8bb7b469e03dcacd8f1f400011a781e96ad4266ede11af8e711379e85b929 + +account delta: AccountVaultDelta { fungible: FungibleAssetDelta({V0(AccountIdV0 { prefix: 6702563556733766432, suffix: 1016103534633728 }): 100}), non_fungible: NonFungibleAssetDelta({}) } +``` + +## 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 + +By leveraging Miden’s privacy features, you can create customized logic for secure asset transfers that depend on keeping parts of the transaction private. + +### Running the example + +To run the custom note example, navigate to the `rust-client` directory in the [miden-tutorials](https://github.com/0xMiden/miden-tutorials/) repository and run this command: + +```bash +cd rust-client +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.14/builder/tutorials/recipes/rust/delegated_proving_tutorial.md b/versioned_docs/version-0.14/builder/tutorials/recipes/rust/delegated_proving_tutorial.md new file mode 100644 index 00000000..e3413ce8 --- /dev/null +++ b/versioned_docs/version-0.14/builder/tutorials/recipes/rust/delegated_proving_tutorial.md @@ -0,0 +1,211 @@ +--- +title: "Delegated Proving" +sidebar_position: 12 +--- + +# Delegated Proving + +_Using delegated proving to minimize transaction proving times on computationally constrained devices_ + +## 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. In the code below, we will create an account, mint tokens from a faucet, then send the tokens to another account using delegated proving. + +## 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 choses 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. Proving a transaction with delegated proving is trustless, meaning if the delegated prover is malicious, they could not compromise the security of the account that is submitting a transaction to be processed by the delegated prover. + +The only downside of using delegated proving is that it reduces the privacy of the account that uses delegated proving, because the delegated prover would have knowledge of the inputs to the transaction that is being proven. 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. + +To keep this tutorial runnable without external services, the code below uses a local prover. The +flow is the same if you swap in `RemoteTransactionProver` and point it at your delegated prover. + +## Step 1: Initialize your repository + +Create a new Rust repository for your Miden project and navigate to it with the following command: + +```bash +cargo new miden-delegated-proving-app +cd miden-delegated-proving-app +``` + +Add the following dependencies to your `Cargo.toml` file: + +```toml +[dependencies] +miden-client = { version = "0.14", features = ["testing", "tonic"] } +miden-client-sqlite-store = { version = "0.14", package = "miden-client-sqlite-store" } +miden-protocol = { version = "0.14" } +rand = { version = "0.9" } +serde = { version = "1", features = ["derive"] } +serde_json = { version = "1.0", features = ["raw_value"] } +tokio = { version = "1.46", features = ["rt-multi-thread", "net", "macros", "fs"] } +rand_chacha = "0.9.0" +``` + +## Step 2: Initialize the client and prover and construct transactions + +Similarly to previous tutorials, we must instantiate the client. +We construct a `LocalTransactionProver` for this walkthrough. + +```rust no_run +use miden_client::auth::AuthSecretKey; +use miden_client::auth::{AuthSchemeId, AuthSingleSig}; +use rand::RngCore; +use std::sync::Arc; + +use miden_client::{ + account::component::BasicWallet, + builder::ClientBuilder, + keystore::{FilesystemKeyStore, Keystore}, + rpc::{Endpoint, GrpcClient}, + transaction::{ + LocalTransactionProver, + ProvingOptions, + TransactionProver, + TransactionRequestBuilder, + }, + ClientError, +}; +use miden_client_sqlite_store::ClientBuilderSqliteExt; +use miden_client::account::{AccountBuilder, AccountStorageMode, AccountType}; + +#[tokio::main] +async fn main() -> Result<(), ClientError> { + // Initialize client + let endpoint = Endpoint::testnet(); + let timeout_ms = 10_000; + let rpc_client = Arc::new(GrpcClient::new(&endpoint, timeout_ms)); + + // 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"); + + let mut client = ClientBuilder::new() + .rpc(rpc_client) + .sqlite_store(store_path) + .authenticator(keystore.clone()) + .in_debug_mode(true.into()) + .build() + .await?; + + let sync_summary = client.sync_state().await.unwrap(); + println!("Latest block: {}", sync_summary.block_num); + + // 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::RegularAccountImmutableCode) + .storage_mode(AccountStorageMode::Private) + .with_auth_component(AuthSingleSig::new(key_pair.public_key().to_commitment(), AuthSchemeId::Falcon512Poseidon2)) + .with_component(BasicWallet) + .build() + .unwrap(); + + client.add_account(&alice_account, false).await?; + keystore.add_key(&key_pair, alice_account.id()).await.unwrap(); + + // ------------------------------------------------------------------------- + // Setup the local tx prover + // ------------------------------------------------------------------------- + let local_tx_prover = LocalTransactionProver::new(ProvingOptions::default()); + let tx_prover: Arc = Arc::new(local_tx_prover); + + // We use a dummy transaction request to showcase delegated proving. + // The only effect of this tx should be increasing Alice's nonce. + println!("Alice nonce initial: {:?}", alice_account.nonce()); + let script_code = "begin 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..."); + let tx_result = client + .execute_transaction(alice_account.id(), transaction_request) + .await?; + + // Step 2: Prove the transaction using the local prover + println!("Proving transaction with local 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?; + + println!("Transaction submitted successfully using local 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()); + + Ok(()) +} +``` + +Now let's run the `src/main.rs` program: + +```bash +cargo run --release +``` + +The output will look like this: + +```text +Latest block: 226954 +Alice initial account balance: Ok(1000) +Alice final account balance: Ok(900) +``` + +### Running the example + +To run a full working example navigate to the `rust-client` directory in the [miden-tutorials](https://github.com/0xMiden/miden-tutorials/) repository and run this command: + +```bash +cd rust-client +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.14/builder/tutorials/recipes/rust/foreign_procedure_invocation_tutorial.md b/versioned_docs/version-0.14/builder/tutorials/recipes/rust/foreign_procedure_invocation_tutorial.md new file mode 100644 index 00000000..f7df1084 --- /dev/null +++ b/versioned_docs/version-0.14/builder/tutorials/recipes/rust/foreign_procedure_invocation_tutorial.md @@ -0,0 +1,666 @@ +--- +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_ + +## 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 completed the previous tutorial on deploying the counter contract. We will be working within the same `miden-counter-contract` repository that we created in the [Interacting with Public Smart Contracts](./public_account_interaction_tutorial.md) tutorial. + +## Step 1: Set up your repository + +We will be using the same repository used in the "Interacting with Public Smart Contracts" tutorial. To set up your repository for this tutorial, first follow up until step two [here](./public_account_interaction_tutorial.md). + +## Step 2: Set up the "count reader" contract + +Inside of the `masm/accounts/` directory, create the `count_reader.masm` file. This is the smart contract that will read the "count" value from the counter contract. + +`masm/accounts/count_reader.masm`: + +```masm +use miden::protocol::active_account +use miden::protocol::native_account +use miden::protocol::tx +use miden::core::word +use miden::core::sys + +const COUNT_READER_SLOT = word("miden::tutorials::count_reader") + +# => [account_id_suffix, account_id_prefix, PROC_HASH(4), foreign_procedure_inputs(16)] +pub proc copy_count + exec.tx::execute_foreign_procedure + # => [count, pad(12)] + + push.COUNT_READER_SLOT[0..2] + # [slot_id_prefix, slot_id_suffix, count, pad(12)] + + exec.native_account::set_item + # => [OLD_VALUE, pad(12)] + + dropw dropw dropw dropw + # => [] + + exec.sys::truncate_stack + # => [] +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 +words (`padw padw padw padw`) as the inputs. After the call, the procedure returns 16 output +elements; the count word sits at the top and we clean up the rest with `dropw dropw dropw dropw`. + +After calling the `get_count` procedure in the counter contract, we save the count into the +`miden::tutorials::count_reader` storage slot. + +**Note**: _The bracket symbols used in the count copy contract are not valid MASM syntax. These are simply placeholder elements that we will replace with the actual values before compilation._ + +Inside the `masm/scripts/` directory, create the `reader_script.masm` file: + +```masm +use external_contract::count_reader_contract +use miden::core::sys + +begin + padw padw padw padw + # => [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 + # => [] + + exec.sys::truncate_stack + # => [] +end +``` + +**Note**: _`push.{get_count_proc_hash}` is not valid MASM, we will format the string with the value get_count_proc_hash before passing this script code to the assembler._ + +### Step 3: Set up your `src/main.rs` file: + +```rust no_run +use rand::RngCore; +use std::{fs, path::Path, sync::Arc, time::Duration}; +use tokio::time::sleep; + +use miden_client::{ + account::{ + component::AccountComponentMetadata, AccountBuilder, AccountComponent, + AccountStorageMode, AccountType, StorageSlot, StorageSlotName, + }, + account::AccountId, + assembly::{ + CodeBuilder, + DefaultSourceManager, + Library, + Module, + ModuleKind, + Path as AssemblyPath, + }, + auth::NoAuth, + builder::ClientBuilder, + keystore::FilesystemKeyStore, + rpc::{domain::account::AccountStorageRequirements, Endpoint, GrpcClient}, + transaction::{ForeignAccount, TransactionKernel, TransactionRequestBuilder}, + ClientError, Word, +}; +use miden_client_sqlite_store::ClientBuilderSqliteExt; + +fn create_library( + library_path: &str, + source_code: &str, +) -> Result, Box> { + let source_manager = Arc::new(DefaultSourceManager::default()); + let assembler = TransactionKernel::assembler_with_source_manager(source_manager.clone()); + let module = Module::parser(ModuleKind::Library).parse_str( + AssemblyPath::new(library_path), + source_code, + source_manager, + )?; + let library = assembler.assemble_library([module])?; + Ok(library) +} + +#[tokio::main] +async fn main() -> Result<(), ClientError> { + // Initialize client + let endpoint = Endpoint::testnet(); + let timeout_ms = 10_000; + let rpc_client = Arc::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()) + .in_debug_mode(true.into()) + .build() + .await?; + + let sync_summary = client.sync_state().await.unwrap(); + println!("Latest block: {}", sync_summary.block_num); + + // ------------------------------------------------------------------------- + // STEP 1: Create the Count Reader Contract + // ------------------------------------------------------------------------- + println!("\n[STEP 1] Creating count reader contract."); + + let count_reader_path = Path::new("../masm/accounts/count_reader.masm"); + let count_reader_code = fs::read_to_string(count_reader_path).unwrap(); + + let count_reader_slot_name = + StorageSlotName::new("miden::tutorials::count_reader").expect("valid slot name"); + let count_reader_component_code = CodeBuilder::new() + .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", AccountType::all()), + ) + .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::RegularAccountImmutableCode) + .storage_mode(AccountStorageMode::Public) + .with_component(count_reader_component.clone()) + .with_auth_component(NoAuth) + .build() + .unwrap(); + + println!( + "count_reader hash: {:?}", + count_reader_contract.to_commitment() + ); + println!("contract id: {:?}", count_reader_contract.id()); + + client + .add_account(&count_reader_contract, false) + .await + .unwrap(); + + Ok(()) +} +``` + +Run the following command to execute src/main.rs: + +```bash +cargo run --release +``` + +The output of our program will look something like this: + +```text +Latest block: 226976 + +[STEP 1] Creating count reader contract. +count_reader hash: RpoDigest([15888177100833057221, 15548657445961063290, 5580812380698193124, 9604096693288041818]) +contract id: "" +``` + +## Step 4: Import the pre-deployed counter contract + +The FPI call needs a counter contract already deployed on-chain. We import the counter contract that was deployed in the [counter contract tutorial](./counter_contract_tutorial.md) by its testnet address: + +```rust ignore +// ------------------------------------------------------------------------- +// STEP 2: Build & Get State of the Counter Contract +// ------------------------------------------------------------------------- +println!("\n[STEP 2] Building counter contract from public state"); + +// Define the Counter Contract account id from counter contract deploy +let (_, counter_contract_id) = + AccountId::from_bech32("mtst1apsd609q5966cqra992t4a00tgstrkfk").unwrap(); + +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 + +Add this snippet to the end of your file in the `main()` function: + +```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"); + +// Derive the get_count procedure hash from the locally compiled counter library. +let counter_contract_path = Path::new("../masm/accounts/counter.masm"); +let counter_contract_code = fs::read_to_string(counter_contract_path).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 = CodeBuilder::new() + .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", AccountType::all()), +) +.unwrap(); + +let get_count_root = counter_component + .component_code() + .as_library() + .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_path = Path::new("../masm/scripts/reader_script.masm"); +let script_code_original = fs::read_to_string(script_path).unwrap(); +let script_code = script_code_original + .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(), + ); + +let account_component_lib = create_library( + "external_contract::count_reader_contract", + &count_reader_code, +) +.unwrap(); + +let tx_script = client + .code_builder() + .with_dynamically_linked_library(&account_component_lib) + .unwrap() + .compile_tx_script(&script_code) + .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_new_transaction(count_reader_contract.id(), tx_request) + .await + .unwrap(); + +println!( + "View transaction on MidenScan: https://testnet.midenscan.com/tx/{:?}", + 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) +); +``` + +The key here is the use of the `.foreign_accounts()` method on the `TransactionRequestBuilder`. Using this method, it is possible to create transactions with multiple foreign procedure calls. + +## Summary + +In this tutorial 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. + +The final `src/main.rs` file should look like this: + +```rust no_run +use rand::RngCore; +use std::{fs, path::Path, sync::Arc, time::Duration}; +use tokio::time::sleep; + +use miden_client::{ + account::{ + component::AccountComponentMetadata, AccountBuilder, AccountComponent, AccountId, + AccountStorageMode, AccountType, StorageSlot, StorageSlotName, + }, + assembly::{ + CodeBuilder, DefaultSourceManager, Library, Module, ModuleKind, + Path as AssemblyPath, + }, + auth::NoAuth, + builder::ClientBuilder, + keystore::FilesystemKeyStore, + rpc::{domain::account::AccountStorageRequirements, Endpoint, GrpcClient}, + transaction::{ForeignAccount, TransactionKernel, TransactionRequestBuilder}, + ClientError, Word, +}; +use miden_client_sqlite_store::ClientBuilderSqliteExt; + +fn create_library( + library_path: &str, + source_code: &str, +) -> Result, Box> { + let source_manager = Arc::new(DefaultSourceManager::default()); + let assembler = TransactionKernel::assembler_with_source_manager(source_manager.clone()); + let module = Module::parser(ModuleKind::Library).parse_str( + AssemblyPath::new(library_path), + source_code, + source_manager, + )?; + let library = assembler.assemble_library([module])?; + Ok(library) +} + +#[tokio::main] +async fn main() -> Result<(), ClientError> { + // Initialize client + let endpoint = Endpoint::testnet(); + let timeout_ms = 10_000; + let rpc_client = Arc::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()) + .in_debug_mode(true.into()) + .build() + .await?; + + let sync_summary = client.sync_state().await.unwrap(); + println!("Latest block: {}", sync_summary.block_num); + + // ------------------------------------------------------------------------- + // STEP 1: Create the Count Reader Contract + // ------------------------------------------------------------------------- + println!("\n[STEP 1] Creating count reader contract."); + + let count_reader_path = Path::new("../masm/accounts/count_reader.masm"); + let count_reader_code = fs::read_to_string(count_reader_path).unwrap(); + + let count_reader_slot_name = + StorageSlotName::new("miden::tutorials::count_reader").expect("valid slot name"); + let count_reader_component_code = CodeBuilder::new() + .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", + AccountType::all(), + ), + ) + .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::RegularAccountImmutableCode) + .storage_mode(AccountStorageMode::Public) + .with_component(count_reader_component.clone()) + .with_auth_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(); + + // ------------------------------------------------------------------------- + // STEP 2: Build & Get State of the Counter Contract + // ------------------------------------------------------------------------- + println!("\n[STEP 2] Building counter contract from public state"); + + // Define the Counter Contract account id from counter contract deploy + let (_, counter_contract_id) = + AccountId::from_bech32("mtst1apsd609q5966cqra992t4a00tgstrkfk").unwrap(); + + 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_path = Path::new("../masm/accounts/counter.masm"); + let counter_contract_code = fs::read_to_string(counter_contract_path).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 = CodeBuilder::new() + .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", AccountType::all()), + ) + .unwrap(); + + let get_count_root = counter_component + .component_code() + .as_library() + .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_path = Path::new("../masm/scripts/reader_script.masm"); + let script_code_original = fs::read_to_string(script_path).unwrap(); + let script_code = script_code_original + .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(), + ); + + let account_component_lib = create_library( + "external_contract::count_reader_contract", + &count_reader_code, + ) + .unwrap(); + + let tx_script = client + .code_builder() + .with_dynamically_linked_library(&account_component_lib) + .unwrap() + .compile_tx_script(&script_code) + .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_new_transaction(count_reader_contract.id(), tx_request) + .await + .unwrap(); + + println!( + "View transaction on MidenScan: https://testnet.midenscan.com/tx/{:?}", + 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) + ); + + Ok(()) +} +``` + +The output will show the count reader contract being created, the counter contract being imported from testnet, and finally both storage slots reflecting the same count value after the FPI transaction is confirmed. + +### Running the example + +To run the full example, navigate to the `rust-client` directory in the [miden-tutorials](https://github.com/0xMiden/miden-tutorials/) repository and run this command: + +```bash +cd rust-client +cargo run --release --bin counter_contract_fpi +``` + +### Continue learning + +Next tutorial: [How to Use Unauthenticated Notes](unauthenticated_note_how_to.md) diff --git a/versioned_docs/version-0.14/builder/tutorials/recipes/rust/index.md b/versioned_docs/version-0.14/builder/tutorials/recipes/rust/index.md new file mode 100644 index 00000000..4515a95b --- /dev/null +++ b/versioned_docs/version-0.14/builder/tutorials/recipes/rust/index.md @@ -0,0 +1,18 @@ +--- +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. + +Keep in mind that both the Rust client and the documentation are works-in-progress! diff --git a/versioned_docs/version-0.14/builder/tutorials/recipes/rust/mappings_in_masm_how_to.md b/versioned_docs/version-0.14/builder/tutorials/recipes/rust/mappings_in_masm_how_to.md new file mode 100644 index 00000000..46ab6e87 --- /dev/null +++ b/versioned_docs/version-0.14/builder/tutorials/recipes/rust/mappings_in_masm_how_to.md @@ -0,0 +1,358 @@ +--- +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_ + +## 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. + After the Miden Assembly snippets, we explain that the transaction script calls a procedure in the account. This procedure then updates the mapping by modifying the mapping stored in the account's storage slot. + +## 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::word +use miden::core::sys + +const MAP_SLOT = word("miden::tutorials::mapping::map") + +# Inputs: [KEY, VALUE] +# Outputs: [] +pub proc write_to_map + # The storage map is in the mapping slot. + push.MAP_SLOT[0..2] + # => [slot_id_prefix, slot_id_suffix, KEY, VALUE] + + # Setting the key value pair in the map + exec.native_account::set_map_item + # => [OLD_VALUE] + + dropw + # => [] +end + +# Inputs: [KEY] +# Outputs: [VALUE] +pub proc get_value_in_map + # The storage map is in the mapping slot. + push.MAP_SLOT[0..2] + # => [slot_id_prefix, slot_id_suffix, KEY] + + exec.active_account::get_map_item + # => [VALUE] +end + +# Inputs: [] +# Outputs: [CURRENT_ROOT] +pub proc get_current_map_root + # Getting the current root from the mapping slot. + push.MAP_SLOT[0..2] exec.active_account::get_item + # => [CURRENT_ROOT] + + exec.sys::truncate_stack + # => [CURRENT_ROOT] +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. + +**Security Note**: The procedure `write_to_map` calls the account procedure `incr_nonce`. This allows any external account to be able to write to the storage map of the account. Smart contract developers should know that procedures that call the `account::incr_nonce` procedure allow anyone to call the procedure and modify the state of the account. + +### Transaction script that calls the smart contract + +```masm +use miden_by_example::mapping_example_contract +use miden::core::sys + +begin + push.1.2.3.4 + push.0.0.0.0 + # => [KEY, VALUE] + + call.mapping_example_contract::write_to_map + # => [] + + push.0.0.0.0 + # => [KEY] + + call.mapping_example_contract::get_value_in_map + # => [VALUE] + + dropw + # => [] + + call.mapping_example_contract::get_current_map_root + # => [CURRENT_ROOT] + + exec.sys::truncate_stack +end +``` + +### Explanation of the transaction script + +The transaction script does the following: + +- It pushes a key (`[0.0.0.0]`) and a value (`[1.2.3.4]`) onto the stack. +- 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 + +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 miden_client::auth::NoAuth; +use miden_client::transaction::TransactionKernel; +use rand::RngCore; +use std::{fs, path::Path, sync::Arc}; + +use miden_client::{ + assembly::{ + Assembler, + CodeBuilder, + DefaultSourceManager, + Module, + ModuleKind, + Path as AssemblyPath, + }, + builder::ClientBuilder, + keystore::FilesystemKeyStore, + rpc::{Endpoint, GrpcClient}, + transaction::TransactionRequestBuilder, + ClientError, +}; +use miden_client_sqlite_store::ClientBuilderSqliteExt; +use miden_client::{ + account::{ + component::AccountComponentMetadata, AccountBuilder, AccountComponent, AccountStorageMode, + AccountType, StorageMap, StorageSlot, StorageSlotName, + }, + Felt, Word, +}; + +fn create_library( + assembler: Assembler, + library_path: &str, + source_code: &str, +) -> Result, Box> { + let source_manager = Arc::new(DefaultSourceManager::default()); + let module = Module::parser(ModuleKind::Library).parse_str( + AssemblyPath::new(library_path), + source_code, + source_manager.clone(), + )?; + let library = assembler.clone().assemble_library([module])?; + Ok(library) +} + +#[tokio::main] +async fn main() -> Result<(), ClientError> { + // Initialize client + let endpoint = Endpoint::testnet(); + let timeout_ms = 10_000; + let rpc_client = Arc::new(GrpcClient::new(&endpoint, timeout_ms)); + + // 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"); + + let mut client = ClientBuilder::new() + .rpc(rpc_client) + .sqlite_store(store_path) + .authenticator(keystore.clone()) + .in_debug_mode(true.into()) + .build() + .await?; + + let sync_summary = client.sync_state().await.unwrap(); + println!("Latest block: {}", sync_summary.block_num); + + // ------------------------------------------------------------------------- + // STEP 1: Deploy a smart contract with a mapping + // ------------------------------------------------------------------------- + println!("\n[STEP 1] Deploy a smart contract with a mapping"); + + // Load the MASM file for the counter contract + let file_path = Path::new("../masm/accounts/mapping_example_contract.masm"); + let account_code = fs::read_to_string(file_path).unwrap(); + + // Prepare assembler (debug mode = true) + let assembler: Assembler = TransactionKernel::assembler(); + + // Using an empty storage value in slot 0 since this is usually reserved + // for the account pub_key and metadata + let empty_slot_name = + StorageSlotName::new("miden::tutorials::mapping::value").expect("valid slot name"); + let empty_storage_slot = StorageSlot::with_value(empty_slot_name.clone(), Word::default()); + + // initialize storage map + 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 = CodeBuilder::new() + .compile_component_code("miden_by_example::mapping_example_contract", &account_code) + .unwrap(); + let mapping_contract_component = AccountComponent::new( + component_code, + vec![empty_storage_slot, storage_slot_map], + AccountComponentMetadata::new("miden_by_example::mapping_example_contract", AccountType::all()), + ) + .unwrap(); + + // Init seed for the counter 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::RegularAccountImmutableCode) + .storage_mode(AccountStorageMode::Public) + .with_component(mapping_contract_component.clone()) + .with_auth_component(NoAuth) + .build() + .unwrap(); + + client + .add_account(&mapping_example_contract, false) + .await + .unwrap(); + + // ------------------------------------------------------------------------- + // STEP 2: Call the Mapping Contract with a Script + // ------------------------------------------------------------------------- + println!("\n[STEP 2] Call Mapping Contract With Script"); + + let script_code = + fs::read_to_string(Path::new("../masm/scripts/mapping_example_script.masm")).unwrap(); + + // Create the library from the account source code using the helper function. + let account_component_lib = create_library( + assembler.clone(), + "miden_by_example::mapping_example_contract", + &account_code, + ) + .unwrap(); + + // Compile the transaction script with the library. + let tx_script = client + .code_builder() + .with_dynamically_linked_library(&account_component_lib) + .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_new_transaction(mapping_example_contract.id(), tx_increment_request) + .await + .unwrap(); + + println!( + "View transaction on MidenScan: https://testnet.midenscan.com/tx/{:?}", + 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 = [Felt::new(0), Felt::new(0), Felt::new(0), Felt::new(0)].into(); + println!( + "Mapping state\n Slot: {:?}\n Key: {:?}\n Value: {:?}", + map_slot_name, + key, + account.storage().get_map_item(&map_slot_name, key) + ); + + Ok(()) +} +``` + +### What the Rust code does + +- **Client Initialization:** + The client is initialized with a connection to the Miden Testnet and a SQLite store. This sets up the environment to deploy and interact with accounts. + +- **Deploying the Smart Contract:** + The account containing the mapping is created by reading the MASM smart contract from a file, compiling it into an `AccountComponent`, and deploying it using an `AccountBuilder`. + +- **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 + +To run the full example, navigate to the `rust-client` directory in the [miden-tutorials](https://github.com/0xMiden/miden-tutorials/) repository and run this command: + +```bash +cd rust-client +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.14/builder/tutorials/recipes/rust/mint_consume_create_tutorial.md b/versioned_docs/version-0.14/builder/tutorials/recipes/rust/mint_consume_create_tutorial.md new file mode 100644 index 00000000..302aca40 --- /dev/null +++ b/versioned_docs/version-0.14/builder/tutorials/recipes/rust/mint_consume_create_tutorial.md @@ -0,0 +1,598 @@ +--- +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_ + +## 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, Alice needs to call the faucet with a mint transaction request. + +_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 will create 5 transaction mint transaction requests. + +Add this snippet to the end of your file in the `main()` function that we created in the previous chapter: + +```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(); + +for i in 1..=5 { + let transaction_request = TransactionRequestBuilder::new() + .build_mint_fungible_asset( + fungible_asset, + alice_account.id(), + NoteType::Public, + client.rng(), + ) + .unwrap(); + + println!("tx request built"); + + let tx_id = client + .submit_new_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 the `get_consumable_notes` function. Before calling it, ensure that the client state is synced. + +_Tip: If you know how many notes to expect after a transaction, use an await or loop condition to check how many notes of the type you expect are available for consumption instead of using a set timeout before calling `get_consumable_notes`. This ensures your application isn't idle for longer than necessary._ + +#### Identifying which notes are available: + +```rust ignore +let consumable_notes = client.get_consumable_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. + +Add this snippet to the end of your file in the `main()` function: + +```rust ignore +//------------------------------------------------------------ +// STEP 4: Alice consumes all her notes +//------------------------------------------------------------ +println!("\n[STEP 4] Alice will now consume all of her notes to consolidate them."); + +// Consume all minted notes in a single transaction +loop { + // Resync to get the latest data + client.sync_state().await?; + + let consumable_notes = client + .get_consumable_notes(Some(alice_account.id())) + .await?; + let notes = consumable_notes + .iter() + .map(|(note, _)| note.clone().try_into()) + .collect::, _>>()?; + + if notes.len() == 5 { + println!("Found 5 consumable notes for Alice. Consuming them now..."); + let transaction_request = TransactionRequestBuilder::new() + .build_consume_notes(notes) + .unwrap(); + + let tx_id = client + .submit_new_transaction(alice_account.id(), transaction_request) + .await?; + println!( + "All of Alice's notes consumed successfully. TX: {:?}", + tx_id + ); + break; + } else { + println!( + "Currently, Alice has {} consumable notes. Waiting...", + notes.len() + ); + tokio::time::sleep(Duration::from_secs(3)).await; + } +} +``` + +## 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 to store five P2ID output notes, loop over five iterations `(using 0..=4)` to create five unique dummy account IDs, build a P2ID note for each one, and push each note onto the vector. Finally, we build a transaction request using `.own_output_notes()`—passing in all five notes—and submit it to the node. + +Add this snippet to the end of your file in the `main()` function: + +```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::Version0, + AccountType::RegularAccountUpdatableCode, + AccountStorageMode::Public, + ); + + let send_amount = 50; + let fungible_asset = FungibleAsset::new(faucet_account.id(), send_amount).unwrap(); + + let p2id_note = P2idNote::create( + alice_account.id(), + target_account_id, + vec![fungible_asset.into()], + NoteType::Public, + NoteAttachment::default(), + client.rng(), + )?; + p2id_notes.push(p2id_note); +} + +// Specifying output notes and creating a tx request to create them +let transaction_request = TransactionRequestBuilder::new() + .own_output_notes(p2id_notes) + .build() + .unwrap(); + +let tx_id = client + .submit_new_transaction(alice_account.id(), transaction_request) + .await?; + +println!("Submitted a transaction with 4 P2ID notes. TX: {:?}", tx_id); +``` + +### Basic P2ID transfer + +Now as an example, Alice will send some tokens to an account in a single transaction. + +Add this snippet to the end of your file in the `main()` function: + +```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::Version0, + AccountType::RegularAccountUpdatableCode, + AccountStorageMode::Public, +); + +let send_amount = 50; +let fungible_asset = FungibleAsset::new(faucet_account.id(), send_amount).unwrap(); + +let p2id_note = P2idNote::create( + alice_account.id(), + target_account_id, + vec![fungible_asset.into()], + NoteType::Public, + NoteAttachment::default(), + client.rng(), +)?; + +let transaction_request = TransactionRequestBuilder::new() + .own_output_notes(vec![p2id_note]) + .build() + .unwrap(); + +let tx_id = client + .submit_new_transaction(alice_account.id(), transaction_request) + .await?; + +println!("Submitted final P2ID transaction. TX: {:?}", tx_id); +``` + +Note: _In a production environment do not use `AccountId::dummy()`, this is simply for the sake of the tutorial example._ + +## Summary + +Your `src/main.rs` function should now look like this: + +```rust no_run +use miden_client::auth::{AuthSchemeId, AuthSingleSig}; +use rand::RngCore; +use std::sync::Arc; +use tokio::time::Duration; + +use miden_client::{ + account::{ + component::{AuthControlled, BasicFungibleFaucet, BasicWallet}, + AccountId, + }, + address::NetworkId, + auth::AuthSecretKey, + builder::ClientBuilder, + keystore::{FilesystemKeyStore, Keystore}, + note::{NoteAttachment, NoteType, P2idNote}, + rpc::{Endpoint, GrpcClient}, + transaction::TransactionRequestBuilder, + ClientError, +}; +use miden_client_sqlite_store::ClientBuilderSqliteExt; +use miden_protocol::account::AccountIdVersion; +use miden_client::{ + account::{AccountBuilder, AccountStorageMode, AccountType}, + asset::{FungibleAsset, TokenSymbol}, + Felt, +}; + +#[tokio::main] +async fn main() -> Result<(), ClientError> { + // Initialize client + let endpoint = Endpoint::testnet(); + let timeout_ms = 10_000; + let rpc_client = Arc::new(GrpcClient::new(&endpoint, timeout_ms)); + + // 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"); + + let mut client = ClientBuilder::new() + .rpc(rpc_client) + .sqlite_store(store_path) + .authenticator(keystore.clone()) + .in_debug_mode(true.into()) + .build() + .await?; + + let sync_summary = client.sync_state().await.unwrap(); + println!("Latest block: {}", sync_summary.block_num); + + //------------------------------------------------------------ + // 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::RegularAccountUpdatableCode) + .storage_mode(AccountStorageMode::Public) + .with_auth_component(AuthSingleSig::new(key_pair.public_key().to_commitment(), AuthSchemeId::Falcon512Poseidon2)) + .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(NetworkId::Testnet); + println!("Alice's account ID: {:?}", alice_account_id_bech32); + + //------------------------------------------------------------ + // 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 = Felt::new(1_000_000); + + // Generate key pair + let key_pair = AuthSecretKey::new_falcon512_poseidon2_with_rng(client.rng()); + + // Build the faucet account + let faucet_account = AccountBuilder::new(init_seed) + .account_type(AccountType::FungibleFaucet) + .storage_mode(AccountStorageMode::Public) + .with_auth_component(AuthSingleSig::new(key_pair.public_key().to_commitment(), AuthSchemeId::Falcon512Poseidon2)) + .with_component(BasicFungibleFaucet::new(symbol, decimals, max_supply).unwrap()) + .with_component(AuthControlled::allow_all()) + .build() + .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(NetworkId::Testnet); + println!("Faucet account ID: {:?}", faucet_account_id_bech32); + + // 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(); + + for i in 1..=5 { + let transaction_request = TransactionRequestBuilder::new() + .build_mint_fungible_asset( + fungible_asset, + alice_account.id(), + NoteType::Public, + client.rng(), + ) + .unwrap(); + + println!("tx request built"); + + let tx_id = client + .submit_new_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."); + + // Consume all minted notes in a single transaction + loop { + // Resync to get the latest data + client.sync_state().await?; + + let consumable_notes = client + .get_consumable_notes(Some(alice_account.id())) + .await?; + let notes = consumable_notes + .iter() + .map(|(note, _)| note.clone().try_into()) + .collect::, _>>()?; + + if notes.len() == 5 { + println!("Found 5 consumable notes for Alice. Consuming them now..."); + let transaction_request = TransactionRequestBuilder::new() + .build_consume_notes(notes) + .unwrap(); + + let tx_id = client + .submit_new_transaction(alice_account.id(), transaction_request) + .await?; + println!( + "All of Alice's notes consumed successfully. TX: {:?}", + tx_id + ); + break; + } else { + println!( + "Currently, Alice has {} consumable notes. Waiting...", + notes.len() + ); + tokio::time::sleep(Duration::from_secs(3)).await; + } + } + + //------------------------------------------------------------ + // 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::Version0, + AccountType::RegularAccountUpdatableCode, + AccountStorageMode::Public, + ); + + let send_amount = 50; + let fungible_asset = FungibleAsset::new(faucet_account.id(), send_amount).unwrap(); + + let p2id_note = P2idNote::create( + alice_account.id(), + target_account_id, + vec![fungible_asset.into()], + NoteType::Public, + NoteAttachment::default(), + client.rng(), + )?; + p2id_notes.push(p2id_note); + } + + // Specifying output notes and creating a tx request to create them + let transaction_request = TransactionRequestBuilder::new() + .own_output_notes(p2id_notes) + .build() + .unwrap(); + + let tx_id = client + .submit_new_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::Version0, + AccountType::RegularAccountUpdatableCode, + AccountStorageMode::Public, + ); + + let send_amount = 50; + let fungible_asset = FungibleAsset::new(faucet_account.id(), send_amount).unwrap(); + + let p2id_note = P2idNote::create( + alice_account.id(), + target_account_id, + vec![fungible_asset.into()], + NoteType::Public, + NoteAttachment::default(), + client.rng(), + )?; + + let transaction_request = TransactionRequestBuilder::new() + .own_output_notes(vec![p2id_note]) + .build() + .unwrap(); + + let tx_id = client + .submit_new_transaction(alice_account.id(), transaction_request) + .await?; + + println!("Submitted final P2ID transaction. TX: {:?}", tx_id); + + 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 +cargo run --release +``` + +The output will look like this: + +```text +Latest block: 226896 + +[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 request built +Minted note #2 of 100 tokens for Alice. +tx request built +Minted note #3 of 100 tokens for Alice. +tx request built +Minted note #4 of 100 tokens for Alice. +tx request built +Minted note #5 of 100 tokens for Alice. +All 5 notes minted for Alice successfully! + +[STEP 4] Alice will now consume all of her notes to consolidate them. +Currently, Alice has 2 consumable notes. Waiting... +Currently, Alice has 4 consumable notes. Waiting... +Found 5 consumable notes for Alice. Consuming them now... +All of Alice's notes consumed successfully. + +[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. +Submitting one more single P2ID transaction... + +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 + +To run a full working example navigate to the `rust-client` directory in the [miden-tutorials](https://github.com/0xMiden/miden-tutorials/) repository and run this command: + +```bash +cd rust-client +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.14/builder/tutorials/recipes/rust/network_transactions_tutorial.md b/versioned_docs/version-0.14/builder/tutorials/recipes/rust/network_transactions_tutorial.md new file mode 100644 index 00000000..af58f456 --- /dev/null +++ b/versioned_docs/version-0.14/builder/tutorials/recipes/rust/network_transactions_tutorial.md @@ -0,0 +1,859 @@ +--- +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_ + +## 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 network counter smart contract using the same MASM code as the regular counter, but with different storage configuration in Rust to enable network execution. + +## What we'll cover + +- Understanding Network Transactions and when to use them +- Deploying smart contracts with network storage mode +- Using transaction scripts to initialize network contracts on-chain +- 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 interact with the same contract state without race conditions +- **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 main trade-off is reduced privacy since the operator can see transaction inputs. + +## Step 1: Initialize your repository + +Create a new Rust repository for your Miden project and navigate to it: + +```bash +cargo new miden-network-transactions +cd miden-network-transactions +``` + +Add the following dependencies to your `Cargo.toml` file: + +```toml +[dependencies] +miden-client = { version = "0.14", features = ["testing", "tonic"] } +miden-client-sqlite-store = { version = "0.14", package = "miden-client-sqlite-store" } +miden-protocol = { version = "0.14" } +rand = { version = "0.9" } +serde = { version = "1", features = ["derive"] } +serde_json = { version = "1.0", features = ["raw_value"] } +tokio = { version = "1.46", features = ["rt-multi-thread", "net", "macros", "fs"] } +rand_chacha = "0.9.0" +``` + +## Step 2: Set up MASM files + +Create the directory structure: + +```bash +mkdir -p masm/accounts masm/scripts masm/notes +``` + +### 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. + +Create `masm/accounts/counter.masm`: + +```masm +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") + +#! Inputs: [] +#! Outputs: [count] +pub proc get_count + push.COUNTER_SLOT[0..2] exec.active_account::get_item + # => [count] + + exec.sys::truncate_stack + # => [count] +end + +#! Inputs: [] +#! Outputs: [] +pub proc increment_count + push.COUNTER_SLOT[0..2] exec.active_account::get_item + # => [count] + + add.1 + # => [count+1] + + push.COUNTER_SLOT[0..2] exec.native_account::set_item + # => [] + + exec.sys::truncate_stack + # => [] +end +``` + +### Transaction Script for Deployment + +Create `masm/scripts/counter_script.masm`: + +```masm +use external_contract::counter_contract + +begin + call.counter_contract::increment_count +end +``` + +This script executes a function call (increment) that creates a necessary state change for our contract to be deployed and stored on the network on-chain. In Miden, network contracts must have their state modified through a transaction to be properly registered and committed to the blockchain - simply creating the account isn't sufficient for network storage mode. + +### Network Note for User Interaction + +Create `masm/notes/network_increment_note.masm`: + +```masm +use external_contract::counter_contract + +begin + call.counter_contract::increment_count +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 std::{fs, path::Path, sync::Arc}; + +use miden_client::account::component::BasicWallet; +use miden_client::{ + address::NetworkId, + auth::AuthSecretKey, + crypto::FeltRng, + builder::ClientBuilder, + keystore::{FilesystemKeyStore, Keystore}, + note::{ + NetworkAccountTarget, Note, NoteAssets, NoteError, NoteExecutionHint, NoteMetadata, + NoteRecipient, NoteStorage, NoteTag, NoteType, + }, + rpc::{Endpoint, GrpcClient}, + store::TransactionFilter, + transaction::{TransactionId, TransactionRequestBuilder, TransactionStatus}, + Client, ClientError, Felt, Word, +}; +use miden_client_sqlite_store::ClientBuilderSqliteExt; +use miden_client::auth::{self, AuthSchemeId, AuthSingleSig}; +use miden_client::transaction::TransactionKernel; +use miden_client::{ + account::{ + component::AccountComponentMetadata, AccountBuilder, AccountComponent, AccountStorageMode, + AccountType, StorageSlot, StorageSlotName, + }, + assembly::{ + Assembler, + CodeBuilder, + DefaultSourceManager, + Library, + Module, + ModuleKind, + Path as AssemblyPath, + }, +}; +use rand::RngCore; +use tokio::time::{sleep, Duration}; + +/// Waits for a specific transaction to be committed. +async fn wait_for_tx( + client: &mut Client, + tx_id: TransactionId, +) -> Result<(), ClientError> { + loop { + client.sync_state().await?; + + // Check transaction status + let txs = client + .get_transactions(TransactionFilter::Ids(vec![tx_id])) + .await?; + let tx_committed = if !txs.is_empty() { + matches!(txs[0].status, TransactionStatus::Committed { .. }) + } else { + false + }; + + if tx_committed { + println!("✅ transaction {} committed", tx_id.to_hex()); + break; + } + + println!( + "Transaction {} not yet committed. Waiting...", + tx_id.to_hex() + ); + sleep(Duration::from_secs(2)).await; + } + Ok(()) +} + +/// Creates a Miden library from the provided account code and library path. +fn create_library( + account_code: String, + library_path: &str, +) -> Result, Box> { + let assembler: Assembler = TransactionKernel::assembler(); + let source_manager = Arc::new(DefaultSourceManager::default()); + let module = Module::parser(ModuleKind::Library).parse_str( + AssemblyPath::new(library_path), + account_code, + source_manager.clone(), + )?; + let library = assembler.clone().assemble_library([module])?; + Ok(library) +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Initialize client + let endpoint = Endpoint::testnet(); + let timeout_ms = 10_000; + let rpc_client = Arc::new(GrpcClient::new(&endpoint, timeout_ms)); + + // 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"); + + let mut client = ClientBuilder::new() + .rpc(rpc_client) + .sqlite_store(store_path) + .authenticator(keystore.clone()) + .in_debug_mode(true.into()) + .build() + .await?; + + let sync_summary = client.sync_state().await.unwrap(); + println!("Latest block: {}", sync_summary.block_num); + + // ------------------------------------------------------------------------- + // 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::RegularAccountUpdatableCode) + .storage_mode(AccountStorageMode::Public) + .with_auth_component(AuthSingleSig::new(key_pair.public_key().to_commitment(), AuthSchemeId::Falcon512Poseidon2)) + .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(); + + println!( + "Alice's account ID: {:?}", + alice_account.id().to_bech32(NetworkId::Testnet) + ); + + 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 + +Now we'll create a network smart contract. The key difference from regular contracts is using `AccountStorageMode::Network` instead of `AccountStorageMode::Public`. + +Add this code to your `main()` function: + +```rust ignore +// ------------------------------------------------------------------------- +// STEP 2: Create Network Counter Smart Contract +// ------------------------------------------------------------------------- +println!("\n[STEP 2] Creating a network counter smart contract"); + +let counter_code = fs::read_to_string(Path::new("../masm/accounts/counter.masm")).unwrap(); + +// Create the network counter smart contract account +// First, compile the MASM code into an account component +let counter_slot_name = + StorageSlotName::new("miden::tutorials::counter").expect("valid slot name"); +let component_code = CodeBuilder::new() + .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(), [Felt::new(0); 4].into())], // Initialize counter storage to 0 + AccountComponentMetadata::new("external_contract::counter_contract", AccountType::all()), +) +.unwrap(); + +// Generate a random seed for the account +let mut init_seed = [0_u8; 32]; +client.rng().fill_bytes(&mut init_seed); + +// Build the immutable network account with no authentication +let counter_contract = AccountBuilder::new(init_seed) + .account_type(AccountType::RegularAccountImmutableCode) // Immutable code + .storage_mode(AccountStorageMode::Network) // Stored on network + .with_auth_component(auth::NoAuth) // No authentication required + .with_component(counter_component) + .build() + .unwrap(); + +client.add_account(&counter_contract, false).await.unwrap(); + +println!( + "contract id: {:?}", + counter_contract.id().to_bech32(NetworkId::Testnet) +); +``` + +This step creates a network smart contract with `AccountStorageMode::Network`, which enables the contract to be executed by the network operator. + +## Step 5: Deploy the network account with a transaction script + +We use a transaction script to deploy the network account and ensure it's properly registered on-chain. The script calls the `increment` function, which initializes the counter to 1. + +Add this code to your `main()` function: + +```rust ignore +// ------------------------------------------------------------------------- +// STEP 3: Deploy Network Account with Transaction Script +// ------------------------------------------------------------------------- +println!("\n[STEP 3] Deploy network counter smart contract"); + +let script_code = fs::read_to_string(Path::new("../masm/scripts/counter_script.masm")).unwrap(); + +let account_code = fs::read_to_string(Path::new("../masm/accounts/counter.masm")).unwrap(); +let library_path = "external_contract::counter_contract"; + +let library = create_library(account_code, library_path).unwrap(); + +let tx_script = client + .code_builder() + .with_dynamically_linked_library(&library)? + .compile_tx_script(&script_code)?; + +let tx_increment_request = TransactionRequestBuilder::new() + .custom_script(tx_script) + .build() + .unwrap(); + +let tx_id = client + .submit_new_transaction(counter_contract.id(), tx_increment_request) + .await + .unwrap(); + +println!( + "View transaction on MidenScan: https://testnet.midenscan.com/tx/{:?}", + tx_id +); + +// Wait for the transaction to be committed +wait_for_tx(&mut client, tx_id).await.unwrap(); +``` + +This step uses a transaction script to deploy the network account and ensure it's properly registered on-chain. The script calls the `increment` function, which initializes the counter to 1. + +## Step 6: Create a network note for user interaction + +We create a public note that the network operator can consume to execute the increment function. This increments the counter from 1 to 2. + +Add this code to your `main()` function: + +```rust ignore +// ------------------------------------------------------------------------- +// STEP 4: Prepare & Create the Network Note +// ------------------------------------------------------------------------- +println!("\n[STEP 4] Creating a network note for network counter contract"); + +let network_note_code = + fs::read_to_string(Path::new("../masm/notes/network_increment_note.masm")).unwrap(); +let account_code = fs::read_to_string(Path::new("../masm/accounts/counter.masm")).unwrap(); + +let library_path = "external_contract::counter_contract"; +let library = create_library(account_code, library_path).unwrap(); + +// 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(); + +// Compile the note script with the counter contract library +let note_script = client + .code_builder() + .with_dynamically_linked_library(&library)? + .compile_note_script(&network_note_code)?; + +// Create note recipient with empty storage +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 = NoteMetadata::new(alice_account.id(), NoteType::Public) + .with_tag(tag) + .with_attachment(attachment); + +// Create the complete note +let increment_note = Note::new(NoteAssets::default(), metadata, recipient); + +// 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_new_transaction(alice_account.id(), note_req) + .await?; + +println!( + "View transaction on MidenScan: https://testnet.midenscan.com/tx/{:?}", + 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; + +client.sync_state().await?; + +let mut last_val = None; +for _ in 0..10 { + 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 >= 2 { + 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; +} + +if let Some(val) = last_val { + println!( + "Counter value did not reach 2 yet (last observed value: {}).", + val + ); +} else { + println!("Counter value not available yet."); +} +``` + +This step creates a public note that the network operator can consume to execute the increment function. This increments the counter from 1 to 2. + +## Summary + +Your complete `main()` function should look like this: + +```rust no_run +use std::{fs, path::Path, sync::Arc}; + +use miden_client::account::component::BasicWallet; +use miden_client::{ + address::NetworkId, + auth::AuthSecretKey, + crypto::FeltRng, + builder::ClientBuilder, + keystore::{FilesystemKeyStore, Keystore}, + note::{ + NetworkAccountTarget, Note, NoteAssets, NoteError, NoteExecutionHint, NoteMetadata, + NoteRecipient, NoteStorage, NoteTag, NoteType, + }, + rpc::{Endpoint, GrpcClient}, + store::TransactionFilter, + transaction::{TransactionId, TransactionRequestBuilder, TransactionStatus}, + Client, ClientError, Felt, Word, +}; +use miden_client_sqlite_store::ClientBuilderSqliteExt; +use miden_client::auth::{self, AuthSchemeId, AuthSingleSig}; +use miden_client::transaction::TransactionKernel; +use miden_client::{ + account::{ + component::AccountComponentMetadata, AccountBuilder, AccountComponent, AccountStorageMode, + AccountType, StorageSlot, StorageSlotName, + }, + assembly::{ + Assembler, + CodeBuilder, + DefaultSourceManager, + Library, + Module, + ModuleKind, + Path as AssemblyPath, + }, +}; +use rand::RngCore; +use tokio::time::{sleep, Duration}; + +/// Waits for a specific transaction to be committed. +async fn wait_for_tx( + client: &mut Client, + tx_id: TransactionId, +) -> Result<(), ClientError> { + loop { + client.sync_state().await?; + + // Check transaction status + let txs = client + .get_transactions(TransactionFilter::Ids(vec![tx_id])) + .await?; + let tx_committed = if !txs.is_empty() { + matches!(txs[0].status, TransactionStatus::Committed { .. }) + } else { + false + }; + + if tx_committed { + println!("✅ transaction {} committed", tx_id.to_hex()); + break; + } + + println!( + "Transaction {} not yet committed. Waiting...", + tx_id.to_hex() + ); + sleep(Duration::from_secs(2)).await; + } + Ok(()) +} + +/// Creates a Miden library from the provided account code and library path. +fn create_library( + account_code: String, + library_path: &str, +) -> Result, Box> { + let assembler: Assembler = TransactionKernel::assembler(); + let source_manager = Arc::new(DefaultSourceManager::default()); + let module = Module::parser(ModuleKind::Library).parse_str( + AssemblyPath::new(library_path), + account_code, + source_manager.clone(), + )?; + let library = assembler.clone().assemble_library([module])?; + Ok(library) +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Initialize client + let endpoint = Endpoint::testnet(); + let timeout_ms = 10_000; + let rpc_client = Arc::new(GrpcClient::new(&endpoint, timeout_ms)); + + // 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"); + + let mut client = ClientBuilder::new() + .rpc(rpc_client) + .sqlite_store(store_path) + .authenticator(keystore.clone()) + .in_debug_mode(true.into()) + .build() + .await?; + + let sync_summary = client.sync_state().await.unwrap(); + println!("Latest block: {}", sync_summary.block_num); + + // ------------------------------------------------------------------------- + // 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::RegularAccountUpdatableCode) + .storage_mode(AccountStorageMode::Public) + .with_auth_component(AuthSingleSig::new(key_pair.public_key().to_commitment(), AuthSchemeId::Falcon512Poseidon2)) + .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(); + + println!( + "Alice's account ID: {:?}", + alice_account.id().to_bech32(NetworkId::Testnet) + ); + + // ------------------------------------------------------------------------- + // STEP 2: Create Network Counter Smart Contract + // ------------------------------------------------------------------------- + println!("\n[STEP 2] Creating a network counter smart contract"); + + let counter_code = fs::read_to_string(Path::new("../masm/accounts/counter.masm")).unwrap(); + + // Create the network counter smart contract account + // First, compile the MASM code into an account component + let counter_slot_name = + StorageSlotName::new("miden::tutorials::counter").expect("valid slot name"); + let component_code = CodeBuilder::new() + .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(), [Felt::new(0); 4].into())], // Initialize counter storage to 0 + AccountComponentMetadata::new("external_contract::counter_contract", AccountType::all()), + ) + .unwrap(); + + // Generate a random seed for the account + let mut init_seed = [0_u8; 32]; + client.rng().fill_bytes(&mut init_seed); + + // Build the immutable network account with no authentication + let counter_contract = AccountBuilder::new(init_seed) + .account_type(AccountType::RegularAccountImmutableCode) // Immutable code + .storage_mode(AccountStorageMode::Network) // Stored on network + .with_auth_component(auth::NoAuth) // No authentication required + .with_component(counter_component) + .build() + .unwrap(); + + client.add_account(&counter_contract, false).await.unwrap(); + + println!( + "contract id: {:?}", + counter_contract.id().to_bech32(NetworkId::Testnet) + ); + + // ------------------------------------------------------------------------- + // STEP 3: Deploy Network Account with Transaction Script + // ------------------------------------------------------------------------- + println!("\n[STEP 3] Deploy network counter smart contract"); + + let script_code = fs::read_to_string(Path::new("../masm/scripts/counter_script.masm")).unwrap(); + + let account_code = fs::read_to_string(Path::new("../masm/accounts/counter.masm")).unwrap(); + let library_path = "external_contract::counter_contract"; + + let library = create_library(account_code, library_path).unwrap(); + + let tx_script = client + .code_builder() + .with_dynamically_linked_library(&library)? + .compile_tx_script(&script_code)?; + + let tx_increment_request = TransactionRequestBuilder::new() + .custom_script(tx_script) + .build() + .unwrap(); + + let tx_id = client + .submit_new_transaction(counter_contract.id(), tx_increment_request) + .await + .unwrap(); + + println!( + "View transaction on MidenScan: https://testnet.midenscan.com/tx/{:?}", + tx_id + ); + + // Wait for the transaction to be committed + wait_for_tx(&mut client, tx_id).await.unwrap(); + + // ------------------------------------------------------------------------- + // STEP 4: Prepare & Create the Network Note + // ------------------------------------------------------------------------- + println!("\n[STEP 4] Creating a network note for network counter contract"); + + let network_note_code = + fs::read_to_string(Path::new("../masm/notes/network_increment_note.masm")).unwrap(); + let account_code = fs::read_to_string(Path::new("../masm/accounts/counter.masm")).unwrap(); + + let library_path = "external_contract::counter_contract"; + let library = create_library(account_code, library_path).unwrap(); + + // 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(); + + // Compile the note script with the counter contract library + let note_script = client + .code_builder() + .with_dynamically_linked_library(&library)? + .compile_note_script(&network_note_code)?; + + // Create note recipient with empty inputs + 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 = NoteMetadata::new(alice_account.id(), NoteType::Public) + .with_tag(tag) + .with_attachment(attachment); + + // Create the complete note + let increment_note = Note::new(NoteAssets::default(), metadata, recipient); + + // 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_new_transaction(alice_account.id(), note_req) + .await?; + + println!( + "View transaction on MidenScan: https://testnet.midenscan.com/tx/{:?}", + 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; + + client.sync_state().await?; + + let mut last_val = None; + for _ in 0..10 { + 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 >= 2 { + 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; + } + + if let Some(val) = last_val { + println!( + "Counter value did not reach 2 yet (last observed value: {}).", + val + ); + } else { + println!("Counter value not available yet."); + } + + Ok(()) +} +``` + +## Step 7: Running the Example + +To run the complete network transaction example: + +```bash +cd rust-client +cargo run --release --bin network_notes_counter_contract +``` + +Expected output: + +```text +Latest block: 4342 + +[STEP 1] Creating a new account for Alice +Alice's account ID: "mtst1azkn605dchqv7yrd9crnvrkknvw8j4d3" + +[STEP 2] Creating a network counter smart contract +contract id: "mtst1ar5dqpk49zjsvsqenfuqzskcvvmf9spc" + +[STEP 3] Deploy network counter smart contract +View transaction on MidenScan: https://testnet.midenscan.com/tx/0xe28fa8e527335499d972e653dfd944ad591752e537a41b151e7b80d598c5660c +✅ transaction 0xe28fa8e527335499d972e653dfd944ad591752e537a41b151e7b80d598c5660c committed + +[STEP 4] Creating a network note for network counter contract +View transaction on MidenScan: https://testnet.midenscan.com/tx/0x3cd653f2848f2fbc3de76d7b0a92c82d23ad1f9f24c9fb86d58772534e17ee30 +network increment note created, waiting for onchain commitment +✅ transaction 0x3cd653f2848f2fbc3de76d7b0a92c82d23ad1f9f24c9fb86d58772534e17ee30 committed +🔢 Final counter value: 2 +``` + +## 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**: Use `AccountStorageMode::Network` instead of `Public` +3. **Deploy with transaction script**: Ensures the contract is registered on-chain +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. 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.14/builder/tutorials/recipes/rust/oracle_tutorial.md b/versioned_docs/version-0.14/builder/tutorials/recipes/rust/oracle_tutorial.md new file mode 100644 index 00000000..9b649848 --- /dev/null +++ b/versioned_docs/version-0.14/builder/tutorials/recipes/rust/oracle_tutorial.md @@ -0,0 +1,443 @@ +--- +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_ + +## 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 `read_price` function in our "price reader" smart contract, which, in turn, calls the Pragma oracle via foreign procedure invocation (FPI). This tutorial lays the foundation for how you can integrate on-chain price data into your DeFi applications on Miden. + +## What we'll cover + +- Deploying a smart contract that can read oracle price data +- Using foreign procedure invocation to get real time on-chain price data + +## Prerequisites + +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 + +Create a new Rust repository for your Miden project and navigate to it with the following command: + +```bash +cargo new miden-defi-app +cd miden-defi-app +``` + +Add the following dependencies to your `Cargo.toml` file: + +```toml +[dependencies] +miden-client = { version = "0.14", features = ["testing", "tonic"] } +miden-client-sqlite-store = { version = "0.14", package = "miden-client-sqlite-store" } +miden-protocol = { version = "0.14" } +rand = { version = "0.9" } +serde = { version = "1", features = ["derive"] } +serde_json = { version = "1.0", features = ["raw_value"] } +tokio = { version = "1.46", features = ["rt-multi-thread", "net", "macros", "fs"] } +rand_chacha = "0.9.0" +``` + +### Step 1: 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::{ + assembly::{ + Assembler, + CodeBuilder, + DefaultSourceManager, + Module, + ModuleKind, + Path as AssemblyPath, + }, + builder::ClientBuilder, + keystore::FilesystemKeyStore, + rpc::{ + domain::account::{AccountStorageRequirements, StorageMapKey}, + Endpoint, GrpcClient, + }, + transaction::{ForeignAccount, TransactionRequestBuilder}, + Client, ClientError, +}; +use miden_client_sqlite_store::ClientBuilderSqliteExt; +use miden_client::{auth::NoAuth, transaction::TransactionKernel}; +use miden_client::{ + account::{ + component::AccountComponentMetadata, AccountComponent, AccountId, AccountStorageMode, + AccountType, StorageSlot, StorageSlotName, StorageSlotType, + }, + Felt, Word, ZERO, +}; +use rand::RngCore; +use std::{fs, path::Path, 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, + trading_pair: u64, +) -> Result, ClientError> { + client.import_account_by_id(oracle_account_id).await?; + + let oracle_account = client + .get_account(oracle_account_id) + .await + .expect("RPC failed") + .expect("oracle account not found"); + + let storage = oracle_account.storage(); + let publisher_count_slot = storage + .slots() + .iter() + .find(|slot| { + let name = slot.name().as_str(); + name.contains("publisher") && name.contains("count") + }) + .map(|slot| slot.name().clone()) + .or_else(|| storage.slots().first().map(|slot| slot.name().clone())) + .expect("oracle storage is expected to have at least one slot"); + + let publisher_count = storage + .get_item(&publisher_count_slot) + .map(|word| word[0].as_canonical_u64()) + .unwrap_or(0); + + let publisher_id_slots: Vec = storage + .slots() + .iter() + .filter(|slot| slot.slot_type() == StorageSlotType::Value) + .filter(|slot| slot.name() != &publisher_count_slot) + .map(|slot| slot.name().clone()) + .collect(); + + let publisher_ids: Vec = publisher_id_slots + .iter() + .take(publisher_count.saturating_sub(1) as usize) + .filter_map(|slot_name| storage.get_item(slot_name).ok()) + .map(|digest| { + let words: Word = digest.into(); + AccountId::new_unchecked([words[3], words[2]]) + }) + .collect(); + + let mut foreign_accounts = Vec::with_capacity(publisher_ids.len() + 1); + let empty_keys: [StorageMapKey; 0] = []; + + for pid in publisher_ids { + client.import_account_by_id(pid).await?; + + let publisher_account = client + .get_account(pid) + .await + .expect("RPC failed") + .expect("publisher account not found"); + let map_slot_names: Vec = publisher_account + .storage() + .slots() + .iter() + .filter(|slot| slot.slot_type() == StorageSlotType::Map) + .map(|slot| slot.name().clone()) + .collect(); + + let storage_requirements = AccountStorageRequirements::new( + map_slot_names + .iter() + .map(|slot_name| (slot_name.clone(), empty_keys.iter())), + ); + + foreign_accounts.push(ForeignAccount::public(pid, storage_requirements)?); + } + + foreign_accounts.push(ForeignAccount::public( + oracle_account_id, + AccountStorageRequirements::default(), + )?); + + Ok(foreign_accounts) +} + +fn create_library( + assembler: Assembler, + library_path: &str, + source_code: &str, +) -> Result, Box> { + let source_manager = Arc::new(DefaultSourceManager::default()); + let module = Module::parser(ModuleKind::Library).parse_str( + AssemblyPath::new(library_path), + source_code, + source_manager.clone(), + )?; + let library = assembler.clone().assemble_library([module])?; + Ok(library) +} + +#[tokio::main] +async fn main() -> Result<(), ClientError> { + // ------------------------------------------------------------------------- + // Initialize Client + // ------------------------------------------------------------------------- + let endpoint = Endpoint::testnet(); + let timeout_ms = 10_000; + let rpc_client = Arc::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()) + .in_debug_mode(true.into()) + .build() + .await?; + + println!("Latest block: {}", client.sync_state().await?.block_num); + + // ------------------------------------------------------------------------- + // Get all foreign accounts for oracle data + // ------------------------------------------------------------------------- + // The oracle account ID must be supplied as a CLI argument. + let oracle_bech32 = std::env::args() + .nth(1) + .expect("Usage: oracle_data_query "); + let (_, oracle_account_id) = AccountId::from_bech32(&oracle_bech32).unwrap(); + let btc_usd_pair_id = 120195681; + let foreign_accounts: Vec = + get_oracle_foreign_accounts(&mut client, oracle_account_id, btc_usd_pair_id).await?; + + println!( + "Oracle accountId prefix: {:?} suffix: {:?}", + oracle_account_id.prefix(), + oracle_account_id.suffix() + ); + + // ------------------------------------------------------------------------- + // Create Oracle Reader contract + // ------------------------------------------------------------------------- + let contract_code = + fs::read_to_string(Path::new("../masm/accounts/oracle_reader.masm")).unwrap(); + + 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", AccountType::all()), + ) + .unwrap(); + + let mut seed = [0_u8; 32]; + client.rng().fill_bytes(&mut seed); + + let oracle_reader_contract = miden_client::account::AccountBuilder::new(seed) + .account_type(AccountType::RegularAccountImmutableCode) + .storage_mode(AccountStorageMode::Public) + .with_component(contract_component.clone()) + .with_auth_component(NoAuth) + .build() + .unwrap(); + + client + .add_account(&oracle_reader_contract, false) + .await + .unwrap(); + + // ------------------------------------------------------------------------- + // Build the script that calls our `get_price` procedure + // ------------------------------------------------------------------------- + let script_path = Path::new("../masm/scripts/oracle_reader_script.masm"); + let script_code = fs::read_to_string(script_path).unwrap(); + + let assembler = TransactionKernel::assembler(); + let library_path = "external_contract::oracle_reader"; + let account_component_lib = + create_library(assembler.clone(), library_path, &contract_code).unwrap(); + + let tx_script = client + .code_builder() + .with_dynamically_linked_library(&account_component_lib) + .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_new_transaction(oracle_reader_contract.id(), tx_increment_request) + .await + .unwrap(); + + println!( + "View transaction on MidenScan: https://testnet.midenscan.com/tx/{:?}", + tx_id + ); + + client.sync_state().await.unwrap(); + + Ok(()) +} +``` + +_Don't run this code just yet, we still need to create our smart contract that queries the oracle_ + +In the code above, we specified the Pragma oracle account id `0x4f67e78643022e00000220d8997e33` and the BTC/USD pair `120195681`. The `get_oracle_foreign_accounts` function returns all of the `ForeignAccounts` that you will need to execute the transaction to get the price data from the oracle. Since Pragma's oracle depends on multiple publishers, this function queries all of the publisher account ids required to make a successful FPI call. + +:::note +The oracle account ID, procedure hash, and trading pair ID used in this tutorial reference Pragma's testnet deployment. These values are maintained by Pragma and may change if they redeploy their oracle. For the latest values, check the [Pragma Miden repository](https://github.com/astraly-labs/pragma-miden). +::: + +## Step 2: Build the price reader smart contract and script + +Just like in previous tutorials, for better code organization we will separate the Miden assembly code from our Rust code. + +Create a directory named `masm` at the **root** of your `miden-counter-contract` directory. This will contain our contract and script masm code. + +Initialize the `masm` directory: + +```bash +mkdir -p masm/accounts masm/scripts +``` + +This will create: + +``` +masm/ +├── accounts/ +└── scripts/ +``` + +### Oracle price reader smart contract + +Below is our oracle price reader contract. It has a a single exported procedure: `get_price` + +The import `miden::tx` contains the `tx::execute_foreign_procedure` which we will use to read the price from the oracle contract. + +#### Here's a breakdown of what the `get_price` procedure does: + +1. Pushes `0.0.0.120195681` onto the stack, representing the BTC/USD pair in the Pragma oracle. +2. Pushes `0xb86237a8c9cd35acfef457e47282cc4da43df676df410c988eab93095d8fb3b9` onto the stack which is the procedure root of the `get_median` procedure in the oracle. +3. Pushes `939716883672832.2172042075194638080` onto the stack which is the oracle id prefix and suffix. +4. Calls `tx::execute_foreign_procedure` which calls the `get_median` procedure via foreign procedure invocation. + +Inside of the `masm/accounts/` directory, create the `oracle_reader.masm` file: + +```masm +use miden::protocol::tx + +# Fetches the current price from the `get_median` +# procedure from the Pragma oracle +# => [] +pub proc get_price + push.0.0.0.120195681 + # => [PAIR] + + # This is the procedure root of the `get_median` procedure + push.0xb86237a8c9cd35acfef457e47282cc4da43df676df410c988eab93095d8fb3b9 + # => [GET_MEDIAN_HASH, PAIR] + + push.939716883672832.2172042075194638080 + # => [oracle_id_prefix, oracle_id_suffix, GET_MEDIAN_HASH, PAIR] + + exec.tx::execute_foreign_procedure + # => [price] + + debug.stack + # => [price] + + dropw dropw +end +``` + +**Note**: _It's a good habit to add comments above each line of MASM code with the expected stack state. This improves readability and helps with debugging._ + +### 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. + +Inside of the `masm/scripts/` directory, create the `oracle_reader_script.masm` file: + +```masm +use external_contract::oracle_reader + +begin + exec.oracle_reader::get_price +end +``` + +## Step 3: Run the program + +Run the following command to execute src/main.rs: + +``` +cargo run --release +``` + +The output of our program will look something like this: + +``` +cleared sqlite store: ./store.sqlite3 +Latest block: 648397 +Oracle accountId prefix: V0(AccountIdPrefixV0 { prefix: 5721796415433354752 }) suffix: 599064613630720 +Stack state before step 8766: +├── 0: 82655190335 +├── 1: 0 +├── 2: 0 +├── 3: 0 +├── 4: 0 +├── 5: 0 +├── 6: 0 +├── 7: 0 +├── 8: 0 +├── 9: 0 +├── 10: 0 +├── 11: 0 +├── 12: 0 +├── 13: 0 +├── 14: 0 +├── 15: 0 +├── 16: 0 +├── 17: 0 +├── 18: 0 +└── 19: 0 + +View transaction on MidenScan: https://testnet.midenscan.com/tx/0xc8951190564d5c3ac59fe99d8911f8c17f5b59ba542e2eb860413898902f3722 +``` + +As you can see, at the top of the stack is the price returned from the Pragma oracle. The price is returned with 6 decimal places. Currently Pragma only publishes the `BTC/USD` price feed on testnet. + +### Running the tutorial + +To run this tutorial end-to-end, navigate to the `rust-client` directory in the [miden-tutorials](https://github.com/0xMiden/miden-tutorials/) repository and run: + +```bash +cd rust-client +cargo run --release --bin oracle_data_query -- +``` + +where `` is Pragma's deployed oracle account ID on testnet. + +### Continue learning + +Next tutorial: [How to Use Unauthenticated Notes](./unauthenticated_note_how_to.md) diff --git a/versioned_docs/version-0.14/builder/tutorials/recipes/rust/public_account_interaction_tutorial.md b/versioned_docs/version-0.14/builder/tutorials/recipes/rust/public_account_interaction_tutorial.md new file mode 100644 index 00000000..1bdc4b1d --- /dev/null +++ b/versioned_docs/version-0.14/builder/tutorials/recipes/rust/public_account_interaction_tutorial.md @@ -0,0 +1,513 @@ +--- +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_ + +## 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 retrieve the counter contract’s state from the chain and rebuild it locally so a local transaction can be executed against it. In the near future, Miden will support network transactions, making the process of submitting transactions to public smart contracts much more like traditional blockchains. + +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 completed the previous tutorial on deploying the counter contract. Although not a requirement, it is recommended to complete the counter contract deployment tutorial before starting this tutorial. + +## Step 1: Initialize your repository + +Create a new Rust repository for your Miden project and navigate to it with the following command: + +```bash +cargo new miden-counter-contract +cd miden-counter-contract +``` + +Add the following dependencies to your `Cargo.toml` file: + +```toml +[dependencies] +miden-client = { version = "0.14", features = ["testing", "tonic"] } +miden-client-sqlite-store = { version = "0.14", package = "miden-client-sqlite-store" } +miden-protocol = { version = "0.14" } +rand = { version = "0.9" } +serde = { version = "1", features = ["derive"] } +serde_json = { version = "1.0", features = ["raw_value"] } +tokio = { version = "1.46", features = ["rt-multi-thread", "net", "macros", "fs"] } +rand_chacha = "0.9.0" +``` + +## Step 2: Build the counter contract + +For better code organization, we will separate the Miden assembly code from our Rust code. + +Create a directory named `masm` at the **root** of your `miden-counter-contract` directory. This will contain our contract and script masm code. + +Initialize the `masm` directory: + +```bash +mkdir -p masm/accounts masm/scripts +``` + +This will create: + +```text +masm/ +├── accounts/ +└── scripts/ +``` + +Inside of the `masm/accounts/` directory, create the `counter.masm` file: + +```masm +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") + +#! Inputs: [] +#! Outputs: [count] +pub proc get_count + push.COUNTER_SLOT[0..2] exec.active_account::get_item + # => [count] + + exec.sys::truncate_stack + # => [count] +end + +#! Inputs: [] +#! Outputs: [] +pub proc increment_count + push.COUNTER_SLOT[0..2] exec.active_account::get_item + # => [count] + + add.1 + # => [count+1] + + push.COUNTER_SLOT[0..2] exec.native_account::set_item + # => [] + + exec.sys::truncate_stack + # => [] +end +``` + +Inside of the `masm/scripts/` directory, create the `counter_script.masm` file: + +```masm +use external_contract::counter_contract + +begin + call.counter_contract::increment_count +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 miden_client::transaction::TransactionKernel; +use std::{fs, path::Path, sync::Arc}; + +use miden_client::{ + account::AccountId, + assembly::{ + Assembler, + DefaultSourceManager, + Module, + ModuleKind, + Path as AssemblyPath, + }, + builder::ClientBuilder, + keystore::FilesystemKeyStore, + rpc::{Endpoint, GrpcClient}, + transaction::TransactionRequestBuilder, + ClientError, +}; +use miden_client_sqlite_store::ClientBuilderSqliteExt; + +fn create_library( + assembler: Assembler, + library_path: &str, + source_code: &str, +) -> Result, Box> { + let source_manager = Arc::new(DefaultSourceManager::default()); + let module = Module::parser(ModuleKind::Library).parse_str( + AssemblyPath::new(library_path), + source_code, + source_manager.clone(), + )?; + let library = assembler.clone().assemble_library([module])?; + Ok(library) +} + +#[tokio::main] +async fn main() -> Result<(), ClientError> { + // Initialize client + let endpoint = Endpoint::testnet(); + let timeout_ms = 10_000; + let rpc_client = Arc::new(GrpcClient::new(&endpoint, timeout_ms)); + + // 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"); + + let mut client = ClientBuilder::new() + .rpc(rpc_client) + .sqlite_store(store_path) + .authenticator(keystore.clone()) + .in_debug_mode(true.into()) + .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 + +To read the public storage state of a smart contract on Miden we either instantiate the `TonicRpcClient` by itself, or use the `test_rpc_api()` method on the `Client` instance. In this example, we will be using the `test_rpc_api()` method. + +We will be reading the public storage state of the counter contract deployed on the testnet at address `0x303dd027d27adc0000012b07dbf1b4`. + +Add the following code snippet to the end of your `src/main.rs` function: + +```rust ignore +// ------------------------------------------------------------------------- +// STEP 1: Read the Public State of the Counter Contract +// ------------------------------------------------------------------------- +println!("\n[STEP 1] Reading data from public state"); + +// Define the Counter Contract account id from counter contract deploy +let (_, counter_contract_id) = + AccountId::from_bech32("mtst1apsd609q5966cqra992t4a00tgstrkfk").unwrap(); + +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() +); +``` + +Run the following command to execute src/main.rs: + +```bash +cargo run --release +``` + +After the program executes, you should see the counter contract count value and nonce printed to the terminal, for example: + +```text +count val: [0, 0, 0, 5] +counter nonce: 5 +``` + +## Step 5: Importing a public account + +Add the following code snippet to the end of your `src/main.rs` function: + +```rust ignore +// ------------------------------------------------------------------------- +// STEP 2: Call the Counter Contract with a script +// ------------------------------------------------------------------------- +println!("\n[STEP 2] Call the increment_count procedure in the counter contract"); + +// Load the MASM script referencing the increment procedure +let script_path = Path::new("../masm/scripts/counter_script.masm"); +let script_code = fs::read_to_string(script_path).unwrap(); + +let counter_path = Path::new("../masm/accounts/counter.masm"); +let counter_code = fs::read_to_string(counter_path).unwrap(); + +let assembler = TransactionKernel::assembler(); +let account_component_lib = create_library( + assembler.clone(), + "external_contract::counter_contract", + &counter_code, +) +.unwrap(); + +let tx_script = client + .code_builder() + .with_dynamically_linked_library(&account_component_lib) + .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_new_transaction(counter_contract.id(), tx_increment_request) + .await + .unwrap(); + +println!( + "View transaction on MidenScan: https://testnet.midenscan.com/tx/{:?}", + 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"); +let counter_slot_name = + miden_client::account::StorageSlotName::new("miden::tutorials::counter") + .expect("valid slot name"); +println!( + "counter contract storage: {:?}", + account.storage().get_item(&counter_slot_name) +); +``` + +## Summary + +The final `src/main.rs` file should look like this: + +```rust no_run +use miden_client::transaction::TransactionKernel; +use std::{fs, path::Path, sync::Arc}; + +use miden_client::{ + account::AccountId, + assembly::{ + Assembler, + DefaultSourceManager, + Module, + ModuleKind, + Path as AssemblyPath, + }, + builder::ClientBuilder, + keystore::FilesystemKeyStore, + rpc::{Endpoint, GrpcClient}, + transaction::TransactionRequestBuilder, + ClientError, +}; +use miden_client_sqlite_store::ClientBuilderSqliteExt; + +fn create_library( + assembler: Assembler, + library_path: &str, + source_code: &str, +) -> Result, Box> { + let source_manager = Arc::new(DefaultSourceManager::default()); + let module = Module::parser(ModuleKind::Library).parse_str( + AssemblyPath::new(library_path), + source_code, + source_manager.clone(), + )?; + let library = assembler.clone().assemble_library([module])?; + Ok(library) +} + +#[tokio::main] +async fn main() -> Result<(), ClientError> { + // Initialize client + let endpoint = Endpoint::testnet(); + let timeout_ms = 10_000; + let rpc_client = Arc::new(GrpcClient::new(&endpoint, timeout_ms)); + + // 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"); + + let mut client = ClientBuilder::new() + .rpc(rpc_client) + .sqlite_store(store_path) + .authenticator(keystore.clone()) + .in_debug_mode(true.into()) + .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"); + + // Define the Counter Contract account id from counter contract deploy + let (_, counter_contract_id) = + AccountId::from_bech32("mtst1apsd609q5966cqra992t4a00tgstrkfk").unwrap(); + + 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 2: Call the Counter Contract with a script + // ------------------------------------------------------------------------- + println!("\n[STEP 2] Call the increment_count procedure in the counter contract"); + + // Load the MASM script referencing the increment procedure + let script_path = Path::new("../masm/scripts/counter_script.masm"); + let script_code = fs::read_to_string(script_path).unwrap(); + + let counter_path = Path::new("../masm/accounts/counter.masm"); + let counter_code = fs::read_to_string(counter_path).unwrap(); + + let assembler = TransactionKernel::assembler(); + let account_component_lib = create_library( + assembler.clone(), + "external_contract::counter_contract", + &counter_code, + ) + .unwrap(); + + let tx_script = client + .code_builder() + .with_dynamically_linked_library(&account_component_lib) + .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_new_transaction(counter_contract.id(), tx_increment_request) + .await + .unwrap(); + + println!( + "View transaction on MidenScan: https://testnet.midenscan.com/tx/{:?}", + 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"); + let counter_slot_name = + miden_client::account::StorageSlotName::new("miden::tutorials::counter") + .expect("valid slot name"); + println!( + "counter contract storage: {:?}", + account.storage().get_item(&counter_slot_name) + ); + Ok(()) +} +``` + +Run the following command to execute src/main.rs: + +```bash +cargo run --release +``` + +The output of our program will look something like this depending on the current count value in the smart contract: + +```text +Client initialized successfully. +Latest block: 242342 + +[STEP 1] Building counter contract from public state +count val: [0, 0, 0, 1] +counter nonce: 1 + +[STEP 2] Call the increment_count procedure in the counter contract +Procedure 1: "0x92495ca54d519eb5e4ba22350f837904d3895e48d74d8079450f19574bb84cb6" +Procedure 2: "0xecd7eb223a5524af0cc78580d96357b298bb0b3d33fe95aeb175d6dab9de2e54" +number of procedures: 2 +Final script: +begin + # => [] + call.0xecd7eb223a5524af0cc78580d96357b298bb0b3d33fe95aeb175d6dab9de2e54 +end +Stack state before step 1812: +├── 0: 2 +├── 1: 0 +├── 2: 0 +├── 3: 0 +├── 4: 0 +├── 5: 0 +├── 6: 0 +├── 7: 0 +├── 8: 0 +├── 9: 0 +├── 10: 0 +├── 11: 0 +├── 12: 0 +├── 13: 0 +├── 14: 0 +├── 15: 0 +├── 16: 0 +├── 17: 0 +├── 18: 0 +└── 19: 0 + +View transaction on MidenScan: https://testnet.midenscan.com/tx/0x8183aed150f20b9c26d4cb7840bfc92571ea45ece31116170b11cdff2649eb5c +counter contract storage: Ok(RpoDigest([0, 0, 0, 2])) +``` + +### Running the example + +To run the full example, navigate to the `rust-client` directory in the [miden-tutorials](https://github.com/0xMiden/miden-tutorials/) repository and run this command: + +```bash +cd rust-client +cargo run --release --bin counter_contract_increment +``` + +### Continue learning + +Next tutorial: [Network Transactions on Miden](network_transactions_tutorial.md) diff --git a/versioned_docs/version-0.14/builder/tutorials/recipes/rust/unauthenticated_note_how_to.md b/versioned_docs/version-0.14/builder/tutorials/recipes/rust/unauthenticated_note_how_to.md new file mode 100644 index 00000000..d9e3560c --- /dev/null +++ b/versioned_docs/version-0.14/builder/tutorials/recipes/rust/unauthenticated_note_how_to.md @@ -0,0 +1,464 @@ +--- +title: "How to Use Unauthenticated Notes" +sidebar_position: 9 +--- + +# How to Use Unauthenticated Notes + +_Using unauthenticated notes for optimistic note consumption_ + +## Overview + +In this guide, we will explore how to leverage unauthenticated notes on Miden to settle transactions faster than the blocktime. Unauthenticated notes are essentially UTXOs that have not yet been fully committed into a block. This feature allows the notes to be created and consumed within the same block. + +We construct a chain of transactions using the unauthenticated notes method on the transaction builder. Unauthenticated notes are also referred to as "unauthenticated notes" or "erasable notes". We also demonstrate how a note can be serialized and deserialized, highlighting the ability to transfer notes between client instances for asset transfers that can be settled between parties faster than the blocktime. + +For example, our demo creates a chain of unauthenticated note transactions: + +```markdown +Alice ➡ Bob ➡ Charlie ➡ Dave ➡ Eve ➡ Frank ➡ ... +``` + +## 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. +- **Performance Insights:** Observe how unauthenticated notes can reduce transaction times dramatically. + +## 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. + - Build and submit a transaction that uses the unauthenticated note via the "unauthenticated" method. + - 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. + +## Full Rust code example + +```rust no_run +use miden_client::auth::{AuthSchemeId, AuthSingleSig}; +use rand::RngCore; +use std::sync::Arc; +use tokio::time::{sleep, Duration, Instant}; + +use miden_client::{ + account::component::{AuthControlled, BasicFungibleFaucet, BasicWallet}, + address::NetworkId, + asset::{FungibleAsset, TokenSymbol}, + auth::AuthSecretKey, + builder::ClientBuilder, + keystore::{FilesystemKeyStore, Keystore}, + note::{Note, NoteAttachment, NoteType, P2idNote}, + rpc::{Endpoint, GrpcClient}, + store::TransactionFilter, + transaction::{TransactionId, TransactionRequestBuilder, TransactionStatus}, + utils::{Deserializable, Serializable}, + Client, ClientError, Felt, +}; +use miden_client_sqlite_store::ClientBuilderSqliteExt; +use miden_client::account::{AccountBuilder, AccountStorageMode, AccountType}; + +/// Waits for a specific transaction to be committed. +async fn wait_for_tx( + client: &mut Client, + tx_id: TransactionId, +) -> Result<(), ClientError> { + loop { + client.sync_state().await?; + + // Check transaction status + let txs = client + .get_transactions(TransactionFilter::Ids(vec![tx_id])) + .await?; + let tx_committed = if !txs.is_empty() { + matches!(txs[0].status, TransactionStatus::Committed { .. }) + } else { + false + }; + + if tx_committed { + println!("✅ transaction {} committed", tx_id.to_hex()); + break; + } + + println!( + "Transaction {} not yet committed. Waiting...", + tx_id.to_hex() + ); + sleep(Duration::from_secs(2)).await; + } + Ok(()) +} + +#[tokio::main] +async fn main() -> Result<(), ClientError> { + // Initialize client + let endpoint = Endpoint::testnet(); + let timeout_ms = 10_000; + let rpc_client = Arc::new(GrpcClient::new(&endpoint, timeout_ms)); + + // 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"); + + let mut client = ClientBuilder::new() + .rpc(rpc_client) + .sqlite_store(store_path) + .authenticator(keystore.clone()) + .in_debug_mode(true.into()) + .build() + .await?; + + let sync_summary = client.sync_state().await.unwrap(); + println!("Latest block: {}", sync_summary.block_num); + + //------------------------------------------------------------ + // 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 = Felt::new(1_000_000); + + // Build the account + let faucet_account = AccountBuilder::new(init_seed) + .account_type(AccountType::FungibleFaucet) + .storage_mode(AccountStorageMode::Public) + .with_auth_component(AuthSingleSig::new(key_pair.public_key().to_commitment(), AuthSchemeId::Falcon512Poseidon2)) + .with_component(BasicFungibleFaucet::new(symbol, decimals, max_supply).unwrap()) + .with_component(AuthControlled::allow_all()) + .build() + .unwrap(); + + // Add the faucet to the client + client.add_account(&faucet_account, false).await?; + + println!( + "Faucet account ID: {}", + faucet_account.id().to_bech32(NetworkId::Testnet) + ); + + // Add the key pair to the keystore + keystore.add_key(&key_pair, faucet_account.id()).await.unwrap(); + + // 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 = 2; + + 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::RegularAccountUpdatableCode) + .storage_mode(AccountStorageMode::Public) + .with_auth_component(AuthSingleSig::new(key_pair.public_key().to_commitment(), AuthSchemeId::Falcon512Poseidon2)) + .with_component(BasicWallet) + .build() + .unwrap(); + + accounts.push(account.clone()); + println!( + "account id {:?}: {}", + i, + account.id().to_bech32(NetworkId::Testnet) + ); + client.add_account(&account, true).await?; + + // Add the key pair to the keystore + keystore.add_key(&key_pair, account.id()).await.unwrap(); + } + + // 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_new_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_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]) + .unwrap(); + + let consume_tx_id = client + .submit_new_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(NetworkId::Testnet)); + println!( + "target: {}", + accounts[i + 1].id().to_bech32(NetworkId::Testnet) + ); + + // 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 = P2idNote::create( + accounts[i].id(), + accounts[i + 1].id(), + vec![fungible_asset_send_amount.into()], + note_type, + NoteAttachment::default(), + client.rng(), + ) + .unwrap(); + + // Time transaction request building + let transaction_request = TransactionRequestBuilder::new() + .own_output_notes(vec![p2id_note.clone()]) + .build() + .unwrap(); + + let tx_id = client + .submit_new_transaction(accounts[i].id(), transaction_request) + .await?; + println!("Created note. TX: {:?}", 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 + let consume_note_request = TransactionRequestBuilder::new() + .input_notes([(deserialized_p2id_note, None)]) + .build() + .unwrap(); + + let tx_id = client + .submit_new_transaction(accounts[i + 1].id(), consume_note_request) + .await?; + + println!( + "Consumed Note Tx on MidenScan: https://testnet.midenscan.com/tx/{:?}", + 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 account in accounts.clone() { + let new_account = client.get_account(account.id()).await.unwrap().expect("account not found"); + let balance = new_account.vault().get_balance(faucet_account.id()).unwrap(); + println!( + "Account: {} balance: {}", + account.id().to_bech32(NetworkId::Testnet), + balance + ); + } + + Ok(()) +} +``` + +The output of our program will look something like this: + +```text +Latest block: 227040 + +[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: +account id 5: +account id 6: +account id 7: +account id 8: +account id 9: + +[STEP 3] Mint tokens +Minting tokens for Alice... + +[STEP 4] Create unauthenticated note tx chain + +unauthenticated tx 1 +sender: +target: +Consumed Note Tx on MidenScan: https://testnet.midenscan.com/tx/0x31f48117c645c5b4ccff78ef356bad764798d4f207925e492ebbae1b86ef4f55 +Total time for loop iteration 0: 1.952243542s + +unauthenticated tx 2 +sender: +target: +Consumed Note Tx on MidenScan: https://testnet.midenscan.com/tx/0x45b4c62c6e8e79a1c7200d1c84dc6304a88debd37b20b069dd739498827354c1 +Total time for loop iteration 1: 2.091625458s + +unauthenticated tx 3 +sender: +target: +Consumed Note Tx on MidenScan: https://testnet.midenscan.com/tx/0xb2241e10df8f6f891b910975a3b4f4fd47657c47de164138300d683cfca5dd61 +Total time for loop iteration 2: 1.846021291s + +unauthenticated tx 4 +sender: +target: +Consumed Note Tx on MidenScan: https://testnet.midenscan.com/tx/0xd3ea6fa1da6c317f055ac4b069388d93b88d526039e01531879e75598e0f8cff +Total time for loop iteration 3: 1.877627958s + +unauthenticated tx 5 +sender: +target: +Consumed Note Tx on MidenScan: https://testnet.midenscan.com/tx/0x6098638ec0ff7331432c037331ee7372977abe20af5c56315985fd314e21548d +Total time for loop iteration 4: 1.884586875s + +unauthenticated tx 6 +sender: +target: +Consumed Note Tx on MidenScan: https://testnet.midenscan.com/tx/0x8258292e49e0cfdd96603450c2de6738afecb1e7482ede0fb68ea375e884e1d8 +Total time for loop iteration 5: 1.886505875s + +unauthenticated tx 7 +sender: +target: +Consumed Note Tx on MidenScan: https://testnet.midenscan.com/tx/0x9e0f84e00a9393bf6e5f224d55ccdf8bd0ef32ee20c3299e2dfccf1771001dfd +Total time for loop iteration 6: 2.095149458s + +unauthenticated tx 8 +sender: +target: +Consumed Note Tx on MidenScan: https://testnet.midenscan.com/tx/0xa9db6445dfaa44ccf9dd52bf4cd8d9057946571ccb5299a7a56c59faf2ed2093 +Total time for loop iteration 7: 1.935587291s + +unauthenticated tx 9 +sender: +target: +Consumed Note Tx on MidenScan: https://testnet.midenscan.com/tx/0xba4bb4ae3c7aaf949cdd3be8c9ea52169f958e7dca8e9d4541fd5ac939393e41 +Total time for loop iteration 8: 1.964682833s + +Total execution time for unauthenticated note txs: 17.534611542s +blocks: [BlockNumber(227047), BlockNumber(227047), BlockNumber(227047), BlockNumber(227047), BlockNumber(227047), BlockNumber(227047), BlockNumber(227047), BlockNumber(227047), BlockNumber(227047)] +Account: balance: 80 +Account: balance: 0 +Account: balance: 0 +Account: balance: 0 +Account: balance: 0 +Account: balance: 0 +Account: balance: 0 +Account: balance: 0 +Account: balance: 0 +Account: balance: 20 +``` + +## Conclusion + +Unauthenticated notes on Miden offer a powerful mechanism for achieving faster asset settlements by allowing notes to be both created and consumed within the same block. In this guide, we walked through: + +- **Minting and Transacting with Unauthenticated Notes:** Building, serializing, and consuming notes quickly using the Miden client's "unauthenticated note" method. +- **Performance Observations:** Measuring and demonstrating how unauthenticated notes enable assets to be sent faster than the blocktime. + +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. Unauthenticated notes are the ideal approach for applications like central limit order books (CLOBs) or other DeFi platforms where transaction speed is critical. + +### Running the example + +To run the unauthenticated note transfer example, navigate to the `rust-client` directory in the [miden-tutorials](https://github.com/0xMiden/miden-tutorials/) repository and run this command: + +```bash +cd rust-client +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.14/builder/tutorials/recipes/web/.prettierrc b/versioned_docs/version-0.14/builder/tutorials/recipes/web/.prettierrc new file mode 100644 index 00000000..665b6ea7 --- /dev/null +++ b/versioned_docs/version-0.14/builder/tutorials/recipes/web/.prettierrc @@ -0,0 +1,9 @@ +{ + "printWidth": 80, + "tabWidth": 2, + "singleQuote": true, + "trailingComma": "all", + "proseWrap": "preserve", + "embeddedLanguageFormatting": "auto", + "endOfLine": "lf" +} diff --git a/versioned_docs/version-0.14/builder/tutorials/recipes/web/_category_.json b/versioned_docs/version-0.14/builder/tutorials/recipes/web/_category_.json new file mode 100644 index 00000000..3f10f63b --- /dev/null +++ b/versioned_docs/version-0.14/builder/tutorials/recipes/web/_category_.json @@ -0,0 +1,5 @@ +{ + "label": "Web / TypeScript", + "position": 2, + "collapsed": true +} diff --git a/versioned_docs/version-0.14/builder/tutorials/recipes/web/counter_contract_tutorial.md b/versioned_docs/version-0.14/builder/tutorials/recipes/web/counter_contract_tutorial.md new file mode 100644 index 00000000..7b52d0d9 --- /dev/null +++ b/versioned_docs/version-0.14/builder/tutorials/recipes/web/counter_contract_tutorial.md @@ -0,0 +1,448 @@ +--- +title: 'Incrementing the Count of the Counter Contract' +sidebar_position: 5 +--- + +_Using the Miden client to interact with a custom smart contract_ + +## 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 + yarn 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.14.4 + ``` + +**NOTE!**: Be sure to add the `--webpack` command to your `package.json` when running the `dev script`. The dev script should look like this: + +`package.json` + +```json + "scripts": { + "dev": "next dev --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::word +use miden::core::sys + +const COUNTER_SLOT = word("miden::tutorials::counter") + +#! Inputs: [] +#! Outputs: [count] +pub proc get_count + push.COUNTER_SLOT[0..2] exec.active_account::get_item + # => [count] + + exec.sys::truncate_stack + # => [count] +end + +#! Inputs: [] +#! Outputs: [] +pub proc increment_count + push.COUNTER_SLOT[0..2] exec.active_account::get_item + # => [count] + + add.1 + # => [count+1] + + push.COUNTER_SLOT[0..2] exec.native_account::set_item + # => [] + + exec.sys::truncate_stack + # => [] +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 +webpack: (config, { isServer }) => { + // ... existing WASM config ... + + // Import .masm files as strings + config.module.rules.push({ + test: /\.masm$/, + type: "asset/source", + }); + + return config; +}, +``` + +:::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 { + AccountType, + AuthSecretKey, + StorageMode, + StorageSlot, + MidenClient, +} from '@miden-sdk/miden-sdk/lazy'; + +export async function incrementCounterContract(): 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 nodeEndpoint = 'https://rpc.testnet.miden.io'; + const client = await MidenClient.create({ rpcUrl: nodeEndpoint }); + console.log('Current block number: ', (await client.sync()).blockNum()); + + const counterSlotName = 'miden::tutorials::counter'; + + // Compile the counter component + 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); + + // Create the counter contract account + const account = await client.accounts.create({ + type: AccountType.RegularAccountImmutableCode, + storage: StorageMode.Public, + seed: walletSeed, + auth, + components: [counterAccountComponent], + }); + + // Building the transaction script which will call the counter contract + const txScriptCode = ` + use external_contract::counter_contract + begin + call.counter_contract::increment_count + end +`; + + const script = await client.compile.txScript({ + code: txScriptCode, + libraries: [ + { + namespace: 'external_contract::counter_contract', + code: counterContractCode, + }, + ], + }); + + // Executing the transaction script against the counter contract — this + // deploys the counter and runs `increment_count` in a single transaction. + await client.transactions.execute({ + account, + script, + }); + + console.log('Counter contract ID:', account.id().toString()); + + // Logging the count of the counter contract we just incremented + const counter = await client.accounts.get(account); + + // Here we get the Word from storage of the counter contract. + // The counter is stored as a Felt widened to Word [count, 0, 0, 0]; + // toU64s() preserves native order so the value lives at index 0. + const count = counter?.storage().getItem(counterSlotName); + + const counterValue = Number(count!.toU64s()[0]); + + console.log('Count: ', counterValue); +} +``` + +To run the code above in our frontend, run the following command: + +``` +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. Calls `sys::truncate_stack` to truncate the stack to size 16. + +```masm +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") + +#! Inputs: [] +#! Outputs: [count] +pub proc get_count + push.COUNTER_SLOT[0..2] exec.active_account::get_item + # => [count] + + exec.sys::truncate_stack + # => [count] +end + +#! Inputs: [] +#! Outputs: [] +pub proc increment_count + push.COUNTER_SLOT[0..2] exec.active_account::get_item + # => [count] + + add.1 + # => [count+1] + + push.COUNTER_SLOT[0..2] exec.native_account::set_item + # => [] + + exec.sys::truncate_stack + # => [] +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 + +**Important**: All accounts must have an authentication component. For smart contracts that do not require authentication (like our counter contract), we use a `NoAuth` component. + +This `NoAuth` component allows any user to interact with the smart contract without requiring signature verification. + +**Note**: _Adding the `account::incr_nonce` to a state changing procedure allows any user to call the procedure._ + +### 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 `client.accounts.create()` with `type: AccountType.RegularAccountImmutableCode` to build and register the contract. You must supply a `seed` (for deterministic ID derivation) and a raw `AuthSecretKey` — the client stores the key automatically: + +```ts +const auth = AuthSecretKey.rpoFalconWithRNG(walletSeed); + +const account = await client.accounts.create({ + type: AccountType.RegularAccountImmutableCode, + storage: StorageMode.Public, + seed: walletSeed, + auth, + components: [counterAccountComponent], +}); +``` + +### 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, + }, + ], +}); +``` + +Then execute it with `client.transactions.execute()`: + +```ts +await client.transactions.execute({ + account, + script, +}); +``` + +### Custom script + +This is the Miden assembly script that calls the `increment_count` procedure during the transaction. + +```masm +use external_contract::counter_contract + +begin + call.counter_contract::increment_count +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 start +``` + +### Resetting the `MidenClientDB` + +The Miden webclient stores account and note data in the browser. If you get errors such as "Failed to build MMR", then you should reset the Miden webclient store. When switching between Miden networks such as from localhost to testnet be sure to reset the browser store. To clear the account and node data in the browser, paste this code snippet into the browser console: + +```javascript +(async () => { + const dbs = await indexedDB.databases(); + for (const db of dbs) { + await indexedDB.deleteDatabase(db.name); + console.log(`Deleted database: ${db.name}`); + } + console.log('All databases deleted.'); +})(); +``` diff --git a/versioned_docs/version-0.14/builder/tutorials/recipes/web/create_deploy_tutorial.md b/versioned_docs/version-0.14/builder/tutorials/recipes/web/create_deploy_tutorial.md new file mode 100644 index 00000000..a6ae2b0c --- /dev/null +++ b/versioned_docs/version-0.14/builder/tutorials/recipes/web/create_deploy_tutorial.md @@ -0,0 +1,426 @@ +--- +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_ + +## 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 off-chain, only known to its owner. +- **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. If the note is private, the note transfer is only known to the sender and receiver. + +## Step 1: Initialize your Next.js project + +1. Create a new Next.js app with TypeScript: + + ```bash + yarn 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: + + + +**NOTE!**: Be sure to add the `--webpack` command to your `package.json` when running the `dev script`. The dev script should look like this: + +`package.json` + +```json + "scripts": { + "dev": "next dev --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 +``` + + { +..// We'll add our logic here +..console.log('Ready to go!'); +.}; + +.return ( +..
+... +..
+.); +} + +export default function CreateMintConsume() { +.return ( +.. +... +.. +.); +}`}, + typescript: { code:`// lib/createMintConsume.ts +import { MidenClient, AccountType, 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 RPC endpoint +.const client = await MidenClient.create({ +..rpcUrl: 'https://rpc.testnet.miden.io', +.}); + +.// 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 }); +.console.log('Alice ID:', alice.id().toString()); +};` }, +typescript: { code: `// lib/createMintConsume.ts +import { MidenClient } 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.create({ +..rpcUrl: 'https://rpc.testnet.miden.io', +.}); + +.// 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({ +..type: AccountType.RegularAccountUpdatableCode, // Mutable: account code can be upgraded later +..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: + + + +### Understanding Faucet Parameters: + +- **Storage Mode**: We use `public()` so anyone can verify the faucet's minting operations +- **Mutability**: Set to `false` to ensure the faucet rules can't be changed after deployment +- **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 the 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: + + { +..// 1. Create Alice's wallet (public, mutable) +..console.log('Creating account for Alice…'); +..const alice = await createWallet({ storageMode: StorageMode.Public }); +..console.log('Alice ID:', alice.id().toString()); + +..// 2. Deploy a fungible faucet +..console.log('Creating faucet…'); +..const faucet = await createFaucet({ +...tokenSymbol: 'MID', +...decimals: 8, +...maxSupply: BigInt(1_000_000), +...storageMode: StorageMode.Public, +..}); +..console.log('Faucet ID:', faucet.id().toString()); + +..console.log('Setup complete.'); +.}; + +.return ( +..
+... +..
+.); +} + +export default function CreateMintConsume() { +.return ( +.. +... +.. +.); +}`}, + typescript: { code:`// lib/createMintConsume.ts +import { MidenClient, AccountType, 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.create({ +..rpcUrl: 'https://rpc.testnet.miden.io', +.}); + +.// 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({ +..type: AccountType.RegularAccountUpdatableCode, +..storage: StorageMode.Public, +.}); +.console.log('Alice ID:', alice.id().toString()); + +.// 3. Deploy a fungible faucet +.console.log('Creating faucet…'); +.const faucet = await client.accounts.create({ +..type: AccountType.FungibleFaucet, +..symbol: 'MID', +..decimals: 8, +..maxSupply: BigInt(1_000_000), +..storage: StorageMode.Public, +.}); +.console.log('Faucet ID:', faucet.id().toString()); + +.console.log('Setup complete.'); +}` }, +}} reactFilename="lib/react/createMintConsume.tsx" tsFilename="lib/createMintConsume.ts" /> + +### Running the example + +```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: 2247 +Creating account for Alice… +Alice ID: 0xd70b2072c6495d100000869a8bacf2 +Creating faucet… +Faucet ID: 0x2d7e506fb88dde200000a1386efec8 +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.14/builder/tutorials/recipes/web/creating_multiple_notes_tutorial.md b/versioned_docs/version-0.14/builder/tutorials/recipes/web/creating_multiple_notes_tutorial.md new file mode 100644 index 00000000..de7b5f8a --- /dev/null +++ b/versioned_docs/version-0.14/builder/tutorials/recipes/web/creating_multiple_notes_tutorial.md @@ -0,0 +1,511 @@ +--- +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_ + +## 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 a custom note‑script 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 is the process of outsourcing a part of 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 choses to use delegated proving, they send off a portion of the zk proof of their transaction to a dedicated server. This dedicated server generates the remainder of the ZK proof of the transaction and submits it to the network. Submitting a transaction with delegated proving is trustless, meaning if the delegated prover is malicious, the could not compromise the security of the account that is submitting a transaction to be processed by the delegated prover. The downside of using delegated proving is that it reduces the privacy of the account that uses delegated proving, because the delegated prover would have knowledge of the transaction that is being proven. Additionally, transactions that require sensitive data such as the knowledge of a hash preimage or a secret, should not use delegated proving as this data will be shared with the delegated prover for proof generation. + +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 + yarn 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: + + + +**NOTE!**: Be sure to add the `--webpack` command to your `package.json` when running the `dev script`. The dev script should look like this: + +`package.json` + +```json + "scripts": { + "dev": "next dev --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. + +``` +mkdir -p lib +``` + + { +..// We'll add our logic here +.}; + +.return ( +..
+... +..
+.); +} + +export default function MultiSendWithDelegatedProver() { +.return ( +.. +... +.. +.); +}`}, + typescript: { code:`import { +.MidenClient, +.AccountType, +.NoteVisibility, +.StorageMode, +.createP2IDNote, +.NoteArray, +.TransactionRequestBuilder, +} from '@miden-sdk/miden-sdk/lazy'; + +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.create({ +..rpcUrl: 'https://rpc.testnet.miden.io', +.}); + +.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 snippet below to the function. This code creates a wallet and faucet, mints tokens from the faucet for the wallet, and then consumes the minted tokens. + + + +## Step 5 — Build and Create P2ID notes + +Add the following code to the function. This code defines three recipient addresses, builds three P2ID notes with 100 `MID` each, and then creates all three notes in the same transaction. + + +.createP2IDNote({ +..from: alice, +..to: addr, +..assets: { token: faucet, amount: BigInt(100) }, +..type: NoteVisibility.Public, +.}), +); + +// ── create all P2ID notes ─────────────────────────────────────────────────────────────── +const builder = new TransactionRequestBuilder(); +const txRequest = builder.withOwnOutputNotes(new NoteArray(p2idNotes)).build(); +await client.transactions.submit(alice, txRequest); + +console.log('All notes created ✅');` }, +}} reactFilename="lib/react/multiSendWithDelegatedProver.tsx" tsFilename="lib/multiSendWithDelegatedProver.ts" /> + +## Summary + +Your library file should now look like this: + + { +..// 1. Create Alice's wallet +..console.log('Creating account for Alice…'); +..const alice = await createWallet({ storageMode: StorageMode.Public }); +..const aliceId = alice.id().toString(); +..console.log('Alice account ID:', aliceId); + +..// 2. Deploy a fungible faucet +..const faucet = await createFaucet({ +...tokenSymbol: 'MID', +...decimals: 8, +...maxSupply: BigInt(1_000_000), +...storageMode: StorageMode.Public, +..}); +..const faucetId = faucet.id().toString(); +..console.log('Faucet ID:', faucetId); + +..// 3. Mint 10,000 MID to Alice +..const mintResult = await mint({ +...faucetId, +...targetAccountId: aliceId, +...amount: BigInt(10_000), +...noteType: NoteVisibility.Public, +..}); + +..console.log('Waiting for settlement…'); +..await waitForCommit(mintResult.transactionId); + +..// 4. Consume the freshly minted notes +..const notes = await waitForConsumableNotes({ accountId: aliceId }); +..await consume({ accountId: aliceId, notes }); + +..// 5. Send 100 MID to three recipients in a single transaction +..await sendMany({ +...from: aliceId, +...assetId: faucetId, +...recipients: [ +....{ to: 'mtst1aqezqc90x7dkzypr9m5fmlpp85w6cl04', amount: BigInt(100) }, +....{ to: 'mtst1apjg2ul76wrkxyr5qlcnczaskypa4ljn', amount: BigInt(100) }, +....{ to: 'mtst1arpee6y9cm8t7ypn33pc8fzj6gkzz7kd', amount: BigInt(100) }, +...], +...noteType: NoteVisibility.Public, +..}); + +..console.log('All notes created ✅'); +.}; + +.return ( +..
+... +..
+.); +} + +export default function MultiSendWithDelegatedProver() { +.return ( +.. +... +.. +.); +}`}, + typescript: { code:`import { +.MidenClient, +.AccountType, +.NoteVisibility, +.StorageMode, +.createP2IDNote, +.NoteArray, +.TransactionRequestBuilder, +} from '@miden-sdk/miden-sdk/lazy'; + +/\*\* +.\* Demonstrates multi-send functionality with delegated proving on the Miden Network +.\* Creates multiple P2ID (Pay to ID) notes for different recipients +.\* +.\* @throws {Error} If the function cannot be executed in a browser environment +.\*/ +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.create({ +..rpcUrl: 'https://rpc.testnet.miden.io', +.}); + +.console.log('Latest block:', (await client.sync()).blockNum()); + +.// ── Creating new account ────────────────────────────────────────────────────── +.console.log('Creating account for Alice…'); +.const alice = await client.accounts.create({ +..type: AccountType.RegularAccountUpdatableCode, +..storage: StorageMode.Public, +.}); +.console.log('Alice account ID:', alice.id().toString()); + +.// ── Creating new faucet ────────────────────────────────────────────────────── +.const faucet = await client.accounts.create({ +..type: AccountType.FungibleFaucet, +..symbol: 'MID', +..decimals: 8, +..maxSupply: BigInt(1_000_000), +..storage: StorageMode.Public, +.}); +.console.log('Faucet ID:', faucet.id().toString()); + +.// ── mint 10 000 MID to Alice ────────────────────────────────────────────────────── +.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); + +.// ── consume the freshly minted notes ────────────────────────────────────────────── +.await client.transactions.consumeAll({ +..account: alice, +.}); + +.// ── build 3 P2ID notes (100 MID each) ───────────────────────────────────────────── +.const recipientAddresses = [ +..'mtst1aqezqc90x7dkzypr9m5fmlpp85w6cl04', +..'mtst1apjg2ul76wrkxyr5qlcnczaskypa4ljn', +..'mtst1arpee6y9cm8t7ypn33pc8fzj6gkzz7kd', +.]; + +.const p2idNotes = recipientAddresses.map((addr) => +..createP2IDNote({ +...from: alice, +...to: addr, +...assets: { token: faucet, amount: BigInt(100) }, +...type: NoteVisibility.Public, +..}), +.); + +.// ── create all P2ID notes ─────────────────────────────────────────────────────────────── +.const builder = new TransactionRequestBuilder(); +.const txRequest = builder.withOwnOutputNotes(new NoteArray(p2idNotes)).build(); +.await client.transactions.submit(alice, txRequest); + +.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 start +``` + +### Resetting the `MidenClientDB` + +The Miden webclient stores account and note data in the browser. To clear the account and node data in the browser, paste this code snippet into the browser console: + +```javascript +(async () => { + const dbs = await indexedDB.databases(); // Get all database names + for (const db of dbs) { + await indexedDB.deleteDatabase(db.name); + console.log(`Deleted database: ${db.name}`); + } + console.log('All databases deleted.'); +})(); +``` diff --git a/versioned_docs/version-0.14/builder/tutorials/recipes/web/foreign_procedure_invocation_tutorial.md b/versioned_docs/version-0.14/builder/tutorials/recipes/web/foreign_procedure_invocation_tutorial.md new file mode 100644 index 00000000..a7276356 --- /dev/null +++ b/versioned_docs/version-0.14/builder/tutorials/recipes/web/foreign_procedure_invocation_tutorial.md @@ -0,0 +1,600 @@ +--- +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_ + +## 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 + yarn 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.14.4 + ``` + +**NOTE!**: Be sure to add the `--webpack` command to your `package.json` when running the `dev script`. The dev script should look like this: + +`package.json` + +```json + "scripts": { + "dev": "next dev --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::word +use miden::core::sys + +const COUNTER_SLOT = word("miden::tutorials::counter") + +#! Inputs: [] +#! Outputs: [count] +pub proc get_count + push.COUNTER_SLOT[0..2] exec.active_account::get_item + # => [count] + + exec.sys::truncate_stack + # => [count] +end + +#! Inputs: [] +#! Outputs: [] +pub proc increment_count + push.COUNTER_SLOT[0..2] exec.active_account::get_item + # => [count] + + add.1 + # => [count+1] + + push.COUNTER_SLOT[0..2] exec.native_account::set_item + # => [] + + exec.sys::truncate_stack + # => [] +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::active_account +use miden::protocol::native_account +use miden::protocol::tx +use miden::core::word +use miden::core::sys + +const COUNT_READER_SLOT = word("miden::tutorials::count_reader") + +# => [account_id_suffix, account_id_prefix, PROC_HASH(4), foreign_procedure_inputs(16)] +pub proc copy_count + exec.tx::execute_foreign_procedure + # => [count, pad(12)] + + push.COUNT_READER_SLOT[0..2] + # [slot_id_prefix, slot_id_suffix, count, pad(12)] + + exec.native_account::set_item + # => [OLD_VALUE, pad(12)] + + dropw dropw dropw dropw + # => [] + + exec.sys::truncate_stack + # => [] +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 +webpack: (config, { isServer }) => { + // ... existing WASM config ... + + // Import .masm files as strings + config.module.rules.push({ + test: /\.masm$/, + type: "asset/source", + }); + + return config; +}, +``` + +:::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 { + AccountType, + AuthSecretKey, + StorageMode, + StorageSlot, + MidenClient, +} from '@miden-sdk/miden-sdk/lazy'; + +export async function foreignProcedureInvocation(): Promise { + if (typeof window === 'undefined') { + console.warn('foreignProcedureInvocation() 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 nodeEndpoint = 'https://rpc.testnet.miden.io'; + const client = await MidenClient.create({ rpcUrl: nodeEndpoint }); + 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 client.accounts.create({ + type: AccountType.RegularAccountImmutableCode, + storage: StorageMode.Public, + seed: counterSeed, + auth: counterAuth, + components: [counterComponent], + }); + + // Deploy the counter to the node by executing a transaction on it + const deployScript = await client.compile.txScript({ + code: ` + use external_contract::counter_contract + begin + call.counter_contract::increment_count + 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.transactions.execute({ + account: counterAccount, + script: deployScript, + waitForConfirmation: true, + }); + 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 client.accounts.create({ + type: AccountType.RegularAccountImmutableCode, + storage: StorageMode.Public, + seed: readerSeed, + auth: readerAuth, + components: [countReaderComponent], + }); + + 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 + + begin + padw padw padw padw + # => [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 + # => [] + + exec.sys::truncate_stack + # => [] + + end +`; + + const script = await client.compile.txScript({ + code: fpiScriptCode, + libraries: [ + { + namespace: 'external_contract::count_reader_contract', + code: countReaderCode, + }, + ], + }); + + await client.transactions.execute({ + account: countReaderAccount, + script, + foreignAccounts: [counterAccount], + }); + + const updatedCountReader = await client.accounts.get(countReaderAccount); + const countReaderStorage = updatedCountReader + ?.storage() + .getItem(countReaderSlotName); + + if (countReaderStorage) { + // The reader contract stores the copied count as a Felt widened to + // Word [count, 0, 0, 0]; toU64s() preserves native order so the + // value lives at index 0. + const countValue = Number(countReaderStorage.toU64s()[0]); + console.log('Count copied via Foreign Procedure Invocation:', countValue); + } + + 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::active_account +use miden::protocol::native_account +use miden::protocol::tx +use miden::core::word +use miden::core::sys + +const COUNT_READER_SLOT = word("miden::tutorials::count_reader") + +# => [account_id_suffix, account_id_prefix, PROC_HASH(4), foreign_procedure_inputs(16)] +pub proc copy_count + exec.tx::execute_foreign_procedure + # => [count, pad(12)] + + push.COUNT_READER_SLOT[0..2] + # [slot_id_prefix, slot_id_suffix, count, pad(12)] + + exec.native_account::set_item + # => [OLD_VALUE, pad(12)] + + dropw dropw dropw dropw + # => [] + + exec.sys::truncate_stack + # => [] +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: + +``` +# => [account_id_suffix, account_id_prefix, PROC_HASH(4), 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 words (`padw padw padw padw`) as the 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 + +begin + padw padw padw padw + # => [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 + # => [] + + exec.sys::truncate_stack + # => [] +end +``` + +This script: + +1. Pushes the procedure hash of the `get_count` function +2. Pushes the counter contract's account ID suffix and prefix +3. Calls the `copy_count` procedure in our count reader contract +4. 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], +}); +``` + +## 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 start +``` + +### Resetting the `MidenClientDB` + +The Miden webclient stores account and note data in the browser. If you get errors such as "Failed to build MMR", then you should reset the Miden webclient store. When switching between Miden networks such as from localhost to testnet be sure to reset the browser store. To clear the account and node data in the browser, paste this code snippet into the browser console: + +```javascript +(async () => { + const dbs = await indexedDB.databases(); + for (const db of dbs) { + await indexedDB.deleteDatabase(db.name); + console.log(`Deleted database: ${db.name}`); + } + console.log('All databases deleted.'); +})(); +``` + +### Continue learning + +Next tutorial: [Creating Multiple Notes](creating_multiple_notes_tutorial.md) diff --git a/versioned_docs/version-0.14/builder/tutorials/recipes/web/index.md b/versioned_docs/version-0.14/builder/tutorials/recipes/web/index.md new file mode 100644 index 00000000..9d16d5ff --- /dev/null +++ b/versioned_docs/version-0.14/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.14/builder/tutorials/recipes/web/mint_consume_create_tutorial.md b/versioned_docs/version-0.14/builder/tutorials/recipes/web/mint_consume_create_tutorial.md new file mode 100644 index 00000000..3090c158 --- /dev/null +++ b/versioned_docs/version-0.14/builder/tutorials/recipes/web/mint_consume_create_tutorial.md @@ -0,0 +1,346 @@ +--- +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_ + +## 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 this to the end of your `createMintConsume` function: + + + +### 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. + +The TypeScript `MidenClient` exposes `client.transactions.consumeAll({ account })` — a single call that finds every consumable note targeted at `account` and consumes them in one transaction. The React SDK instead splits the flow into two hooks: `useWaitForNotes().waitForConsumableNotes(...)` surfaces the notes and `useConsume().consume(...)` consumes them. + + + +## 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): + + { +..// 1. Create Alice's wallet (public, mutable) +..console.log('Creating account for Alice…'); +..const alice = await createWallet({ storageMode: StorageMode.Public }); +..const aliceId = alice.id().toString(); +..console.log('Alice ID:', aliceId); + +..// 2. Deploy a fungible faucet +..console.log('Creating faucet…'); +..const faucet = await createFaucet({ +...tokenSymbol: 'MID', +...decimals: 8, +...maxSupply: BigInt(1_000_000), +...storageMode: StorageMode.Public, +..}); +..const faucetId = faucet.id().toString(); +..console.log('Faucet ID:', faucetId); + +..// 3. Mint 1000 tokens to Alice +..console.log('Minting tokens to Alice...'); +..const mintResult = await mint({ +...faucetId, +...targetAccountId: aliceId, +...amount: BigInt(1000), +...noteType: NoteVisibility.Public, +..}); +..console.log('Mint tx:', mintResult.transactionId); + +..// 4. Wait for the mint transaction to be committed +..await waitForCommit(mintResult.transactionId); + +..// 5. Wait for consumable notes to appear, then consume them +..const notes = await waitForConsumableNotes({ accountId: alice }); +..console.log('Consumable notes:', notes.length); + +..console.log('Consuming minted notes...'); +..await consume({ accountId: alice.id().toString(), notes }); +..console.log('Notes consumed.'); + +..// 6. Send 100 tokens to Bob +..const bobAddress = 'mtst1apve54rq8ux0jqqqqrkh5y0r0y8cwza6'; +..console.log("Sending tokens to Bob's account..."); +..await send({ +...from: aliceId, +...to: bobAddress, +...assetId: faucetId, +...amount: BigInt(100), +...noteType: NoteVisibility.Public, +..}); +..console.log('Tokens sent successfully!'); +.}; + +.return ( +..
+... +..
+.); +} + +export default function CreateMintConsume() { +.return ( +.. +... +.. +.); +}`}, + typescript: { code:`// lib/createMintConsume.ts +import { MidenClient, AccountType, NoteVisibility, 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 WASM to be ready before touching any wasm-bindgen type. +.await MidenClient.ready(); + +.const client = await MidenClient.create({ +..rpcUrl: 'https://rpc.testnet.miden.io', +.}); + +.// 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({ +..type: AccountType.RegularAccountUpdatableCode, +..storage: StorageMode.Public, +.}); +.console.log('Alice ID:', alice.id().toString()); + +.// 3. Deploy a fungible faucet +.console.log('Creating faucet…'); +.const faucet = await client.accounts.create({ +..type: AccountType.FungibleFaucet, +..symbol: 'MID', +..decimals: 8, +..maxSupply: BigInt(1_000_000), +..storage: StorageMode.Public, +.}); +.console.log('Faucet ID:', faucet.id().toString()); + +.// 4. Mint tokens to Alice + +.console.log('Minting tokens to Alice...'); +.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); + +.// 5-6. Consume all available notes for Alice in a single transaction +.console.log('Consuming minted notes...'); +.await client.transactions.consumeAll({ +..account: alice, +.}); + +.console.log('Notes consumed.'); + +.// 7. Send tokens to Bob +.const bobAddress = 'mtst1apve54rq8ux0jqqqqrkh5y0r0y8cwza6'; +.console.log("Sending tokens to Bob's account..."); +.await client.transactions.send({ +..account: alice, +..to: bobAddress, +..token: faucet, +..amount: BigInt(100), +..type: NoteVisibility.Public, +.}); +.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 the browser. To clear the account and note data in the browser, paste this code snippet into the browser console: + +```javascript +(async () => { + const dbs = await indexedDB.databases(); // Get all database names + for (const db of dbs) { + await indexedDB.deleteDatabase(db.name); + console.log(`Deleted database: ${db.name}`); + } + console.log('All databases deleted.'); +})(); +``` + +## 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.14/builder/tutorials/recipes/web/react_wallet_tutorial.md b/versioned_docs/version-0.14/builder/tutorials/recipes/web/react_wallet_tutorial.md new file mode 100644 index 00000000..e2f03514 --- /dev/null +++ b/versioned_docs/version-0.14/builder/tutorials/recipes/web/react_wallet_tutorial.md @@ -0,0 +1,936 @@ +--- +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` + +--- + +## 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 + yarn create vite miden-wallet --template react-ts + cd miden-wallet + ``` + +2. Install the Miden React SDK: + + ```bash + yarn add @miden-sdk/react + ``` + +3. 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/lazy'; +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"` or a custom URL) +- `prover`: The prover to use (`"testnet"` for 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/lazy'; + +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/lazy'; + +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/lazy'; + +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}
; +} +``` + +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/lazy'; + +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/lazy'; + +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 } from '@miden-sdk/react/lazy'; + +function UnclaimedNotes({ + accountId, + consumableNoteSummaries, +}: { + accountId: string; + consumableNoteSummaries: Array<{ id: string }>; +}) { + 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. + +```tsx +import { useState, type ChangeEvent } from 'react'; +import { useSend, parseAssetAmount } from '@miden-sdk/react/lazy'; +import { NoteVisibility } from '@miden-sdk/miden-sdk/lazy'; + +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(assets[0]?.assetId ?? ''); + 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/lazy'; +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/lazy'; +import { + useMiden, + useAccounts, + useAccount, + useNotes, + useCreateWallet, + useConsume, + useSend, +} from '@miden-sdk/react/lazy'; +import { NoteVisibility } from '@miden-sdk/miden-sdk/lazy'; + +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 = () => createWallet(); + 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 + +To run a full working example, navigate to the `packages/react-sdk/examples/wallet` directory in the [miden-client](https://github.com/0xMiden/miden-client/) repository: + +```bash +git clone https://github.com/0xMiden/miden-client.git +cd miden-client/packages/react-sdk/examples/wallet +yarn install +yarn dev +``` + +### Resetting the MidenClientDB + +The Miden client stores account and note data in the browser's IndexedDB. To clear this data, paste the following into your browser console: + +```javascript +(async () => { + const dbs = await indexedDB.databases(); + for (const db of dbs) { + await indexedDB.deleteDatabase(db.name); + console.log(`Deleted database: ${db.name}`); + } + console.log('All databases deleted.'); +})(); +``` + +--- + +## 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/lazy'; + +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. + +--- + +### Para: EVM Wallet Integration + +[Para](https://para.space/) provides a modal-based authentication flow that allows users to sign in with their EVM wallets (MetaMask, WalletConnect, etc.). + +**Installation:** + +```bash +yarn add @miden-sdk/use-miden-para-react +``` + +**Usage:** + +```tsx +import { ParaSignerProvider } from '@miden-sdk/use-miden-para-react'; +import { MidenProvider, useSigner } from '@miden-sdk/react/lazy'; + +function App() { + 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` | `ReactNode` | 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. + +**Installation:** + +```bash +yarn add @miden-sdk/miden-turnkey-react @turnkey/sdk-browser +``` + +**Usage:** + +```tsx +import { TurnkeySignerProvider } from '@miden-sdk/miden-turnkey-react'; +import { MidenProvider, useSigner } from '@miden-sdk/react/lazy'; + +function App() { + return ( + + + + + + ); +} + +function Wallet() { + const signer = useSigner(); + + return ( +
+ {signer?.isConnected ? ( + + ) : ( + + )} +
+ ); +} +``` + +Calling `connect()` handles the full Turnkey authentication flow: passkey login, wallet discovery, and account selection. No manual setup is needed. + +**TurnkeySignerProvider Props:** + +| Prop | Type | Description | +| -------- | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | +| `config` | `Partial` | Optional. Defaults to `apiBaseUrl: "https://api.turnkey.com"` and `defaultOrganizationId` from `VITE_TURNKEY_ORG_ID` env var. | + +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://miden.fi/) 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 +``` + +**Usage:** + +```tsx +import { MidenFiSignerProvider } from '@miden-sdk/miden-wallet-adapter-react'; +import { MidenProvider, useSigner } from '@miden-sdk/react/lazy'; + +function App() { + return ( + + + + + + ); +} + +function Wallet() { + const signer = useSigner(); + + return ( +
+ {signer?.isConnected ? ( + + ) : ( + + )} +
+ ); +} +``` + +**MidenFiSignerProvider Props:** + +| Prop | Type | Description | +| ----------------------- | ------------------------- | -------------------------------------- | +| `network` | `"testnet" \| "localnet"` | Target network | +| `privateDataPermission` | `boolean` | Whether to request private data access | +| `allowedPrivateData` | `string[]` | List of allowed private data types | + +--- + +### 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`. + +```tsx +import { useState, useCallback, type ReactNode } from 'react'; +import { SignerContext, type SignerContextValue } from '@miden-sdk/react/lazy'; +import { AccountStorageMode } from '@miden-sdk/miden-sdk/lazy'; + +interface CustomSignerProviderProps { + children: ReactNode; + // Your provider-specific config +} + +export function CustomSignerProvider({ children }: CustomSignerProviderProps) { + const [isConnected, setIsConnected] = useState(false); + const [signerContext, setSignerContext] = useState( + null, + ); + + const connect = useCallback(async () => { + // 1. Initialize your signing service and get credentials + const { publicKeyCommitment, signMessage } = + await initializeYourSigningService(); + + // 2. Build the signer context + const context: SignerContextValue = { + signCb: async (pubKey, signingInputs) => { + // Sign the message using your service + return signMessage(signingInputs); + }, + accountConfig: { + publicKeyCommitment, + accountType: 'RegularAccountImmutableCode', + storageMode: AccountStorageMode.public(), + }, + storeName: 'custom_signer', + name: 'CustomSigner', + isConnected: true, + connect, + disconnect, + }; + + setSignerContext(context); + setIsConnected(true); + }, []); + + const disconnect = useCallback(async () => { + setSignerContext(null); + setIsConnected(false); + }, []); + + 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, account type, 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/miden-client/tree/main/packages/react-sdk) - Full API documentation +- [Miden Documentation](https://docs.miden.io/) - Core Miden concepts diff --git a/versioned_docs/version-0.14/builder/tutorials/recipes/web/setup_guide.md b/versioned_docs/version-0.14/builder/tutorials/recipes/web/setup_guide.md new file mode 100644 index 00000000..0e8121f6 --- /dev/null +++ b/versioned_docs/version-0.14/builder/tutorials/recipes/web/setup_guide.md @@ -0,0 +1,256 @@ +--- +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+ (Node 22+ requires an extra `localStorage` polyfill — see below) +- Next.js 14+ with App Router +- yarn or npm + +## Install the SDK + +```bash +yarn add @miden-sdk/miden-sdk +``` + +For React hook support: + +```bash +yarn add @miden-sdk/react +``` + +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: + +```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 + +Starting with `@miden-sdk/miden-sdk@0.14.4`, 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.create({ + rpcUrl: 'https://rpc.testnet.miden.io', + }); + // … +} +``` + +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'; + +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. + +::: + +## Node.js 22+ `localStorage` polyfill + +If you run `next dev` under Node.js 22 or later, every page request will crash with: + +``` +TypeError: localStorage.getItem is not a function +``` + +This is a Node + Next.js interaction, not a Miden SDK issue. Node 22+ defines `globalThis.localStorage` as an object, but its methods (`getItem`, `setItem`, …) are undefined unless Node is launched with `--localstorage-file`. Next.js's dev overlay guards with `typeof localStorage !== 'undefined'`, which passes on Node 22+, and then calls the missing methods. + +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 only affects `next dev` (SSR); static exports via `next build` are unaffected. The polyfill is harmless on Node ≤21 — it installs an in-memory stub that the dev overlay uses just like Node 22+'s (broken) built-in. + +## SDK API Patterns + +### Transaction return types + +All transaction methods return an object, not a plain transaction ID: + +```ts +// mint and consume return { txId, result } +const { txId } = await client.transactions.mint({ ... }); + +// send returns { txId, note, result } +// note is non-null when returnNote: true +const { txId, note } = await client.transactions.send({ + ..., + 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({ + ..., + waitForConfirmation: true, +}); + +// Option 2: Wait separately using waitFor +const { txId } = await client.transactions.mint({ ... }); +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({ ... }); +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()` + +As of `@miden-sdk/miden-sdk@0.14.4`, all mutating `WebClient` methods (`transactions.execute`, `transactions.submit`, `syncState`, account creation) and async proxy-fallback reads (`getAccount`, `importAccountById`, `getAccountStorage`, etc.) 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 15-second 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.14/builder/tutorials/recipes/web/unauthenticated_note_how_to.md b/versioned_docs/version-0.14/builder/tutorials/recipes/web/unauthenticated_note_how_to.md new file mode 100644 index 00000000..c59210c3 --- /dev/null +++ b/versioned_docs/version-0.14/builder/tutorials/recipes/web/unauthenticated_note_how_to.md @@ -0,0 +1,465 @@ +--- +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_ + +## Overview + +In this tutorial, we will explore how to leverage unauthenticated notes on Miden to settle transactions faster than the blocktime using the Miden client. Unauthenticated notes are essentially UTXOs that have not yet been fully committed into a block. This feature allows the notes to be created and consumed within the same batch during [batch production](https://0xmiden.github.io/miden-docs/imported/miden-base/src/blockchain.html#batch-production). + +When using unauthenticated notes, both the creation and consumption of notes can happen within the same batch, enabling faster-than-blocktime settlement. This is particularly powerful for applications requiring high-frequency transactions or optimistic settlement patterns. + +We construct a chain of transactions using the unauthenticated notes method on the transaction builder. Unauthenticated notes are also referred to as "erasable notes". We also demonstrate how a note can be created and consumed, highlighting the ability to transfer notes between client instances for asset transfers that can be settled between parties faster than the blocktime. + +For example, our demo creates a chain of unauthenticated note transactions: + +```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. +- **Performance Insights:** Observe how unauthenticated notes can reduce transaction times dramatically. + +## 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-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 the 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. + - Use unauthenticated input notes to consume notes faster than blocktime. + - Measure and observe the performance benefits. + +## Step 1: Initialize your Next.js project + +1. Create a new Next.js app with TypeScript: + + ```bash + yarn 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: + + + +**NOTE!**: Be sure to add the `--webpack` command to your `package.json` when running the `dev script`. The dev script should look like this: + +`package.json` + +```json + "scripts": { + "dev": "next dev --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 +``` + +Copy and paste the following code into `lib/react/unauthenticatedNoteTransfer.tsx` (React) or `lib/unauthenticatedNoteTransfer.ts` (TypeScript): + + { +..// 1. Create Alice and 5 wallets for the transfer chain +..console.log('Creating accounts…'); +..const alice = await createWallet({ storageMode: StorageMode.Public }); +..console.log('Alice account ID:', alice.id().toString()); + +..const wallets = []; +..for (let i = 0; i < 5; i++) { +...const wallet = await createWallet({ storageMode: StorageMode.Public }); +...wallets.push(wallet); +...console.log(\`Wallet \${i}:\`, wallet.id().toString()); +..} + +..// 2. Deploy a fungible faucet +..const faucet = await createFaucet({ +...tokenSymbol: 'MID', +...decimals: 8, +...maxSupply: BigInt(1_000_000), +...storageMode: StorageMode.Public, +..}); +..console.log('Faucet ID:', faucet.id().toString()); + +..// 3. Mint 10,000 MID to Alice +..const mintResult = await mint({ +...faucetId: faucet, +...targetAccountId: alice, +...amount: BigInt(10_000), +...noteType: NoteVisibility.Public, +..}); + +..console.log('Waiting for settlement…'); +..await waitForCommit(mintResult.transactionId); + +..// 4. Consume the freshly minted notes +..const notes = await waitForConsumableNotes({ accountId: alice }); +..await consume({ accountId: alice, notes }); + +..// 5. Create the unauthenticated note transfer chain: +..// Alice → Wallet 0 → Wallet 1 → Wallet 2 → Wallet 3 → Wallet 4 +..console.log('Starting unauthenticated transfer chain…'); +..let currentSender = alice; +..for (let i = 0; i < wallets.length; i++) { +...const wallet = wallets[i]; +...const { note } = await send({ +....from: currentSender, +....to: wallet, +....assetId: faucet, +....amount: BigInt(50), +....noteType: NoteVisibility.Public, +....returnNote: true, +...}); +...const result = await consume({ accountId: wallet.id().toString(), notes: [note] }); +...console.log( +....\`Transfer \${i + 1}: https://testnet.midenscan.com/tx/\${result.transactionId}\`, +...); +...currentSender = wallet; +..} + +..console.log('Asset transfer chain completed ✅'); +.}; + +.return ( +..
+... +..
+.); +} + +export default function UnauthenticatedNoteTransfer() { +.return ( +.. +... +.. +.); +}`}, + typescript: { code:`import { +.MidenClient, +.AccountType, +.NoteVisibility, +.StorageMode, +} from '@miden-sdk/miden-sdk/lazy'; + +/\*\* +.\* Demonstrates unauthenticated note transfer chain against Miden testnet +.\* Creates a chain of P2ID (Pay to ID) notes: Alice → wallet 1 → wallet 2 → wallet 3 → wallet 4 +.\* +.\* @throws {Error} If the function cannot be executed in a browser environment +.\*/ +export async function unauthenticatedNoteTransfer(): 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.create({ +..rpcUrl: 'https://rpc.testnet.miden.io', +.}); + +.console.log('Latest block:', (await client.sync()).blockNum()); + +.// ── Creating accounts ────────────────────────────────────────────────────── +.console.log('Creating account for Alice…'); +.const alice = await client.accounts.create({ +..type: AccountType.RegularAccountUpdatableCode, +..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({ +...type: AccountType.RegularAccountUpdatableCode, +...storage: StorageMode.Public, +..}); +..wallets.push(wallet); +..console.log('wallet ', i.toString(), wallet.id().toString()); +.} + +.// ── Creating new faucet ────────────────────────────────────────────────────── +.const faucet = await client.accounts.create({ +..type: AccountType.FungibleFaucet, +..symbol: 'MID', +..decimals: 8, +..maxSupply: BigInt(1_000_000), +..storage: StorageMode.Public, +.}); +.console.log('Faucet ID:', faucet.id().toString()); + +.// ── Mint 10,000 MID to Alice ────────────────────────────────────────────────────── +.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); + +.// ── Consume the freshly minted note ────────────────────────────────────────────── +.await client.transactions.consumeAll({ +..account: alice, +.}); + +.// ── 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()); + +..const { note } = await client.transactions.send({ +...account: sender, +...to: receiver, +...token: faucet, +...amount: BigInt(50), +...type: NoteVisibility.Public, +...returnNote: true, +..}); + +..const { txId: consumeTxId } = await client.transactions.consume({ +...account: receiver, +...notes: [note], +..}); + +..console.log( +...\`Consumed Note Tx on MidenScan: https://testnet.midenscan.com/tx/\${consumeTxId.toHex()}\`, +..); +.} + +.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** +- **Transferred faster than blocktime** +- **Used for optimistic transactions** + +### Performance Benefits + +By using unauthenticated notes, we can: + +- Skip waiting for block confirmation between note creation and consumption +- Create transaction chains that execute within a single block +- Achieve sub-blocktime settlement for certain use cases + +### Use Cases + +Unauthenticated notes are ideal for: + +- **High-frequency trading applications** +- **Payment channels** +- **Micropayment systems** +- **Any scenario requiring fast settlement** + +## 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 on Miden offer a powerful mechanism for achieving faster asset settlements by allowing notes to be both created and consumed within the same block. 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** using unauthenticated input notes for sub-blocktime settlement +- **Performance Observations** demonstrating how unauthenticated notes enable faster-than-blocktime transfers + +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` + +The Miden webclient stores account and note data in the browser. If you get errors such as "Failed to build MMR", then you should reset the Miden webclient store. When switching between Miden networks such as from localhost to testnet be sure to reset the browser store. To clear the account and node data in the browser, paste this code snippet into the browser console: + +```javascript +(async () => { + const dbs = await indexedDB.databases(); + for (const db of dbs) { + await indexedDB.deleteDatabase(db.name); + console.log(`Deleted database: ${db.name}`); + } + console.log('All databases deleted.'); +})(); +``` + +### 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 start +``` + +### Continue learning + +Next tutorial: [Creating Multiple Notes](creating_multiple_notes_tutorial.md) diff --git a/versioned_docs/version-0.14/builder/tutorials/theme/Admonition/Icon/Danger.tsx b/versioned_docs/version-0.14/builder/tutorials/theme/Admonition/Icon/Danger.tsx new file mode 100644 index 00000000..ab14d7de --- /dev/null +++ b/versioned_docs/version-0.14/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.14/builder/tutorials/theme/Admonition/Icon/Info.tsx b/versioned_docs/version-0.14/builder/tutorials/theme/Admonition/Icon/Info.tsx new file mode 100644 index 00000000..59e48a52 --- /dev/null +++ b/versioned_docs/version-0.14/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.14/builder/tutorials/theme/Admonition/Icon/Note.tsx b/versioned_docs/version-0.14/builder/tutorials/theme/Admonition/Icon/Note.tsx new file mode 100644 index 00000000..d7c524b3 --- /dev/null +++ b/versioned_docs/version-0.14/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.14/builder/tutorials/theme/Admonition/Icon/Tip.tsx b/versioned_docs/version-0.14/builder/tutorials/theme/Admonition/Icon/Tip.tsx new file mode 100644 index 00000000..219bb8d0 --- /dev/null +++ b/versioned_docs/version-0.14/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.14/builder/tutorials/theme/Admonition/Icon/Warning.tsx b/versioned_docs/version-0.14/builder/tutorials/theme/Admonition/Icon/Warning.tsx new file mode 100644 index 00000000..f96398d1 --- /dev/null +++ b/versioned_docs/version-0.14/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.14/builder/tutorials/theme/Admonition/Layout/index.tsx b/versioned_docs/version-0.14/builder/tutorials/theme/Admonition/Layout/index.tsx new file mode 100644 index 00000000..7b2c170d --- /dev/null +++ b/versioned_docs/version-0.14/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.14/builder/tutorials/theme/Admonition/Layout/styles.module.css b/versioned_docs/version-0.14/builder/tutorials/theme/Admonition/Layout/styles.module.css new file mode 100644 index 00000000..88df7e63 --- /dev/null +++ b/versioned_docs/version-0.14/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.14/builder/tutorials/theme/Admonition/Type/Caution.tsx b/versioned_docs/version-0.14/builder/tutorials/theme/Admonition/Type/Caution.tsx new file mode 100644 index 00000000..b570a37a --- /dev/null +++ b/versioned_docs/version-0.14/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.14/builder/tutorials/theme/Admonition/Type/Danger.tsx b/versioned_docs/version-0.14/builder/tutorials/theme/Admonition/Type/Danger.tsx new file mode 100644 index 00000000..49901fa9 --- /dev/null +++ b/versioned_docs/version-0.14/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.14/builder/tutorials/theme/Admonition/Type/Info.tsx b/versioned_docs/version-0.14/builder/tutorials/theme/Admonition/Type/Info.tsx new file mode 100644 index 00000000..018e0a16 --- /dev/null +++ b/versioned_docs/version-0.14/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.14/builder/tutorials/theme/Admonition/Type/Note.tsx b/versioned_docs/version-0.14/builder/tutorials/theme/Admonition/Type/Note.tsx new file mode 100644 index 00000000..c99e0385 --- /dev/null +++ b/versioned_docs/version-0.14/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.14/builder/tutorials/theme/Admonition/Type/Tip.tsx b/versioned_docs/version-0.14/builder/tutorials/theme/Admonition/Type/Tip.tsx new file mode 100644 index 00000000..18604a5e --- /dev/null +++ b/versioned_docs/version-0.14/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.14/builder/tutorials/theme/Admonition/Type/Warning.tsx b/versioned_docs/version-0.14/builder/tutorials/theme/Admonition/Type/Warning.tsx new file mode 100644 index 00000000..61d9597b --- /dev/null +++ b/versioned_docs/version-0.14/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.14/builder/tutorials/theme/Admonition/Types.tsx b/versioned_docs/version-0.14/builder/tutorials/theme/Admonition/Types.tsx new file mode 100644 index 00000000..2a100190 --- /dev/null +++ b/versioned_docs/version-0.14/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.14/builder/tutorials/theme/Admonition/index.tsx b/versioned_docs/version-0.14/builder/tutorials/theme/Admonition/index.tsx new file mode 100644 index 00000000..8f4225da --- /dev/null +++ b/versioned_docs/version-0.14/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.14/core-concepts/_category_.json b/versioned_docs/version-0.14/core-concepts/_category_.json new file mode 100644 index 00000000..7f413902 --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/_category_.json @@ -0,0 +1,6 @@ +{ + "label": "Core Concepts", + "position": 2, + "collapsible": false, + "collapsed": false +} diff --git a/versioned_docs/version-0.14/core-concepts/compiler/_category_.yml b/versioned_docs/version-0.14/core-concepts/compiler/_category_.yml new file mode 100644 index 00000000..adabe490 --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/compiler/appendix/_category_.yml b/versioned_docs/version-0.14/core-concepts/compiler/appendix/_category_.yml new file mode 100644 index 00000000..ab3d9c72 --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/compiler/appendix/calling-conventions.md b/versioned_docs/version-0.14/core-concepts/compiler/appendix/calling-conventions.md new file mode 100644 index 00000000..f9c6a199 --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/compiler/appendix/calling-conventions.md @@ -0,0 +1,304 @@ +--- +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` | 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`/`dynexec`, 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, due to the way `dynexec`/`dyncall` +work, it is actually limited to 12 elements, because at least 4 must be free to hold the hash of the function being indirectly called. diff --git a/versioned_docs/version-0.14/core-concepts/compiler/appendix/canonabi-adhocabi-mismatch.md b/versioned_docs/version-0.14/core-concepts/compiler/appendix/canonabi-adhocabi-mismatch.md new file mode 100644 index 00000000..7d319e98 --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/compiler/appendix/index.md b/versioned_docs/version-0.14/core-concepts/compiler/appendix/index.md new file mode 100644 index 00000000..e2808319 --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/compiler/appendix/known-limitations.md b/versioned_docs/version-0.14/core-concepts/compiler/appendix/known-limitations.md new file mode 100644 index 00000000..d2098073 --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/compiler/appendix/known-limitations.md @@ -0,0 +1,259 @@ +--- +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: **Unimplemented** +- Tracking Issue: [#32](https://github.com/0xMiden/compiler/issues/32) +- Release Milestone: [Beta 1](https://github.com/0xMiden/compiler/milestone/4) + +This feature corresponds to `call_indirect` in WebAssembly, and is associated with Rust features +such as trait objects (which use indirection to call trait methods), and closures. Note that the +Rust compiler is able to erase the indirection associated with certain abstractions statically +in some cases, as shown below. If Rust is unable to statically resolve all call targets, then +`midenc` will raise an error when it encounters any use of `call_indirect`. + +:::warning + +The following examples rely on `rustc`/LLVM inlining enough code to be able to convert indirect +calls to direct calls. This may require you to enable link-time optimization with `lto = "fat"` +and compile all of the code in the crate together with `codegen-units = 1`, in order to maximize +the amount of inlining that can occur. Even then, it may not be possible to remove some forms of +indirection, in which case you will need to find another workaround. + +::: + +#### Iterator lowered to loop + +```rust +pub fn is_zeroed(bytes: &[u8; 32]) -> bool { + // Rust is able to convert this to a loop, erasing the closure completely + bytes.iter().copied().all(|b| b == 0) +} +``` + +#### Monomorphization + inlining + +```rust +pub fn call(fun: F) -> T +where + F: Fn() -> T, +{ + fun() +} + +#[inline(never)] +pub fn foo() -> bool { true } + +fn main() { + // Rust is able to inline the body of `call` after monomorphization, which results in + // the call to `foo` being resolved statically. + call(foo) +} +``` + +#### Inlined trait impl + +```rust +pub trait Foo { + fn is_foo(&self) -> bool; +} + +impl Foo for u32 { + #[inline(never)] + fn is_foo(&self) -> bool { true } +} + +fn has_foo(items: &[dyn Foo]) -> bool { + items.iter().any(|item| item.is_foo()) +} + +fn main() -> u32 { + // Rust inlines `has_foo`, converts the iterator chain to a loop, and is able to realize + // that the `dyn Foo` items are actually `u32`, and resolves the call to `is_foo` to + // `::is_foo`. + let foo: &dyn Foo = &u32::MAX as &dyn Foo; + has_foo(&[foo]) as u32 +} +``` + +### 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: **Unimplemented** +- Tracking Issue: [#32](https://github.com/0xMiden/compiler/issues/32) +- Release Milestone: [Beta 1](https://github.com/0xMiden/compiler/milestone/4) + +This is a dependency of [Function Call Indirection](#function-call-indirection) described above, +and is the mechanism by which we can perform indirect calls in Miden. In order to implement support +for indirect calls in the Wasm frontend, we need underlying support for `dynexec`, which is not yet +implemented. + +This feature adds support for lowering indirect calls to `dynexec` or `dyncall` instructions, +depending on the ABI of the callee. `dyncall` has an additional dependency on support for +[Cross-Context Procedure Invocation](#cross-context-procedure-invocation). + +A known issue with this feature is that `dyn(exec|call)` consumes a word on the operand stack +for the hash of the callee being invoked, but this word _remains_ on the stack when entering the +callee, which has the effect of requiring procedures to have a different ABI depending on whether +they expect to be dynamically-invoked or not. + +Our solution to that issue is to generate stubs which are used as the target of `dyn(exec|call)`, +the body of which drops the callee hash, fixes up the operand stack as necessary, and then uses a +simple `exec` or `call` to invoke the "real" callee. We will emit a single stub for every function +which has its "address" taken, and use the hash of the stub in place of the actual callee hash. + +### 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) + +However, this package format is not yet understood by the Miden VM itself. This means you cannot, +currently, compile a package and then run it using `miden run` directly. Instead, you can use +`midenc run` to load and run code from a package, as the compiler ships with the VM embedded for +use with the interactive debugger, and provides native support for packaging on top of it. You can +also use `midenc debug` to execute your program interactively in the debugger, depending on your +needs. See [Debugging Programs](../guides/debugger.md) for more information on how to use the +debugger, and `midenc help run` for more information on executing programs with the `midenc run` +command. + +While it is possible to emit raw MAST from `midenc`, rather than the experimental package format, +the resulting artifact cannot be run without some fragile and error-prone manual setup, in order +to ensure that the advice provider is correctly initialized with any read-only data segments. For +now, it is recommended that you use the `midenc` tooling for testing programs, until the format +is stabilized. diff --git a/versioned_docs/version-0.14/core-concepts/compiler/guides/_category_.yml b/versioned_docs/version-0.14/core-concepts/compiler/guides/_category_.yml new file mode 100644 index 00000000..9375afab --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/compiler/guides/debugger.md b/versioned_docs/version-0.14/core-concepts/compiler/guides/debugger.md new file mode 100644 index 00000000..ac226282 --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/compiler/guides/debugger.md @@ -0,0 +1,230 @@ +--- +title: Debugging programs +sidebar_position: 5 +--- + +# Debugging programs + +A very useful tool in the Miden compiler suite, is its TUI-based interactive debugger, accessible +via the `midenc debug` command. + +:::warning + +The debugger is still quite new, and while very useful already, it still has a fair number of +UX annoyances. Please report any bugs you encounter, and we'll try to get them patched ASAP! + +::: + +## Getting started + +The debugger is launched by executing `midenc debug`, and giving it a path to a program compiled +by `midenc compile`. See [Program Inputs](#program-inputs) for information on how to provide inputs +to the program you wish to debug. Run `midenc help debug` for more detailed usage documentation. + +The debugger may also be used as a library, but that is left as an exercise for the reader for now. + +## Example + +```shell +# Compile a program to MAST from a rustc-generated Wasm module +midenc compile foo.wasm -o foo.masp + +# Load that program into the debugger and start executing it +midenc debug foo.masp +``` + +## 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 `midenc 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 +midenc 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 `midenc 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.14/core-concepts/compiler/guides/develop_miden_in_rust.md b/versioned_docs/version-0.14/core-concepts/compiler/guides/develop_miden_in_rust.md new file mode 100644 index 00000000..2af02054 --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/compiler/guides/develop_miden_rollup_accounts_and_note_scripts_in_rust.md b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/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.14/core-concepts/compiler/guides/index.md b/versioned_docs/version-0.14/core-concepts/compiler/guides/index.md new file mode 100644 index 00000000..413bb0ac --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/compiler/guides/rust_to_wasm.md b/versioned_docs/version-0.14/core-concepts/compiler/guides/rust_to_wasm.md new file mode 100644 index 00000000..1b420346 --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/compiler/guides/wasm_to_masm.md b/versioned_docs/version-0.14/core-concepts/compiler/guides/wasm_to_masm.md new file mode 100644 index 00000000..8b6bb44f --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/compiler/guides/wasm_to_masm.md @@ -0,0 +1,143 @@ +--- +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. + +Currently, by default, the compiler will emit an experimental package format that the Miden VM does +not yet support. We will instead use `midenc run` to execute the package using the VM for us, but +once the package format is stabilized, this same approach will work with `miden run` as well. + +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 compile --emit masm=wasm_fib.masm,masp 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 + +:::note + +Because the compiler ships with the VM embedded for `midenc debug`, you can run your program +without having to install the VM separately, though you should do that as well, as `midenc` only +exposes a limited set of commands for executing programs, intended for debugging. + +::: + +We can test our compiled program like so: + +```bash +$ midenc run --num-outputs 1 wasm_fib.masp -- 10 +============================================================ +Run program: wasm_fib.masp +============================================================ +Executed program with hash 0xe5ba88695040ec2477821b26190e9addbb1c9571ae30c564f5bbfd6cabf6c535 in 19 milliseconds +Output: [55] +VM cycles: 295 extended to 512 steps (42% padding). +├── Stack rows: 295 +├── Range checker rows: 67 +└── Chiplets rows: 250 +├── Hash chiplet rows: 248 +├── Bitwise chiplet rows: 0 +├── Memory chiplet rows: 1 +└── Kernel ROM rows: 0 +``` + +Success! We got the expected result of `55`. + +## 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.14/core-concepts/compiler/index.md b/versioned_docs/version-0.14/core-concepts/compiler/index.md new file mode 100644 index 00000000..b4a18c62 --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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 via `midenc debug`, 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.14/core-concepts/compiler/theme/Admonition/Icon/Danger.tsx b/versioned_docs/version-0.14/core-concepts/compiler/theme/Admonition/Icon/Danger.tsx new file mode 100644 index 00000000..ab14d7de --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/compiler/theme/Admonition/Icon/Info.tsx b/versioned_docs/version-0.14/core-concepts/compiler/theme/Admonition/Icon/Info.tsx new file mode 100644 index 00000000..59e48a52 --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/compiler/theme/Admonition/Icon/Note.tsx b/versioned_docs/version-0.14/core-concepts/compiler/theme/Admonition/Icon/Note.tsx new file mode 100644 index 00000000..d7c524b3 --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/compiler/theme/Admonition/Icon/Tip.tsx b/versioned_docs/version-0.14/core-concepts/compiler/theme/Admonition/Icon/Tip.tsx new file mode 100644 index 00000000..219bb8d0 --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/compiler/theme/Admonition/Icon/Warning.tsx b/versioned_docs/version-0.14/core-concepts/compiler/theme/Admonition/Icon/Warning.tsx new file mode 100644 index 00000000..f96398d1 --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/compiler/theme/Admonition/Layout/index.tsx b/versioned_docs/version-0.14/core-concepts/compiler/theme/Admonition/Layout/index.tsx new file mode 100644 index 00000000..7b2c170d --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/compiler/theme/Admonition/Layout/styles.module.css b/versioned_docs/version-0.14/core-concepts/compiler/theme/Admonition/Layout/styles.module.css new file mode 100644 index 00000000..88df7e63 --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/compiler/theme/Admonition/Type/Caution.tsx b/versioned_docs/version-0.14/core-concepts/compiler/theme/Admonition/Type/Caution.tsx new file mode 100644 index 00000000..b570a37a --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/compiler/theme/Admonition/Type/Danger.tsx b/versioned_docs/version-0.14/core-concepts/compiler/theme/Admonition/Type/Danger.tsx new file mode 100644 index 00000000..49901fa9 --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/compiler/theme/Admonition/Type/Info.tsx b/versioned_docs/version-0.14/core-concepts/compiler/theme/Admonition/Type/Info.tsx new file mode 100644 index 00000000..018e0a16 --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/compiler/theme/Admonition/Type/Note.tsx b/versioned_docs/version-0.14/core-concepts/compiler/theme/Admonition/Type/Note.tsx new file mode 100644 index 00000000..c99e0385 --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/compiler/theme/Admonition/Type/Tip.tsx b/versioned_docs/version-0.14/core-concepts/compiler/theme/Admonition/Type/Tip.tsx new file mode 100644 index 00000000..18604a5e --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/compiler/theme/Admonition/Type/Warning.tsx b/versioned_docs/version-0.14/core-concepts/compiler/theme/Admonition/Type/Warning.tsx new file mode 100644 index 00000000..61d9597b --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/compiler/theme/Admonition/Types.tsx b/versioned_docs/version-0.14/core-concepts/compiler/theme/Admonition/Types.tsx new file mode 100644 index 00000000..2a100190 --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/compiler/theme/Admonition/index.tsx b/versioned_docs/version-0.14/core-concepts/compiler/theme/Admonition/index.tsx new file mode 100644 index 00000000..8f4225da --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/compiler/usage/_category_.yml b/versioned_docs/version-0.14/core-concepts/compiler/usage/_category_.yml new file mode 100644 index 00000000..bb98c44f --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/compiler/usage/cargo-miden.md b/versioned_docs/version-0.14/core-concepts/compiler/usage/cargo-miden.md new file mode 100644 index 00000000..547f4f06 --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/compiler/usage/cargo-miden.md @@ -0,0 +1,92 @@ +--- +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-2025-12-10 +``` + +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-2025-12-10 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. + +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`. + +## Running a compiled Miden VM program + +:::warning + +To run the compiled Miden VM program you need to have `midenc` installed. See [`midenc` docs](./midenc.md) for the installation instructions. + +::: + +The compiled Miden VM program can be run from the Miden package with the following: + +```bash +midenc run target/miden/release/foo.masp --inputs some_inputs.toml +``` + +See `midenc run --help` for the inputs file format. + +## 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.14/core-concepts/compiler/usage/index.md b/versioned_docs/version-0.14/core-concepts/compiler/usage/index.md new file mode 100644 index 00000000..10bbd762 --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/compiler/usage/midenc.md b/versioned_docs/version-0.14/core-concepts/compiler/usage/midenc.md new file mode 100644 index 00000000..ca8ec1c2 --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/compiler/usage/midenc.md @@ -0,0 +1,134 @@ +--- +title: As an Executable +sidebar_position: 2 +--- + +# Getting started with `midenc` + +The `midenc` executable is the command-line interface for the compiler driver, as well as other +helpful tools, such as the interactive debugger. + +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 + +:::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-2025-12-10 +``` + +NOTE: You can also use the latest nightly, but the specific nightly shown here is known to +work. + +::: + +To install the `midenc`, clone the compiler repo first: + +```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: + +```bash +midenc help compile +Usage: midenc compile [OPTIONS] [-- ...] + +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: + --output-dir + Write all compiler artifacts to DIR + + -W + Modify how warnings are treated by the compiler + + [default: auto] + + Possible values: + - none: Disable all warnings + - auto: Enable all warnings + - error: Promotes warnings to errors + + -v, --verbose + When set, produces more verbose output during compilation + + -h, --help + Print help (see a summary with '-h') +``` + +The actual help output covers quite a bit more than shown here, this is just for illustrative +purposes. + +The `midenc` executable supports two primary functions at this time: + +- `midenc compile` to compile one of our supported input formats to Miden Assembly +- `midenc debug` to run a Miden program attached to an interactive debugger +- `midenc run` to run a Miden program non-interactively, equivalent to `miden run` + +## Compilation + +See the help output for `midenc compile` for detailed information on its options and their +behavior. However, the following is an example of how one might use `midenc compile` in practice: + +```bash +midenc compile --target rollup \ + --entrypoint 'foo::main' \ + -lextra \ + -L ./masm \ + --emit=hir=-,masp \ + -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 (`masp`). +- 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`). + +## Debugging + +See [Debugging Programs](../guides/debugger.md) for details on using `midenc debug` to debug Miden programs. + +## 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 compile` 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). + +You may also be interested in our [basic account project template](https://github.com/0xMiden/rust-templates/tree/main/account/template), +as a starting point for your own Rust project. diff --git a/versioned_docs/version-0.14/core-concepts/index.md b/versioned_docs/version-0.14/core-concepts/index.md new file mode 100644 index 00000000..1b73afe3 --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/index.md @@ -0,0 +1,215 @@ +--- +sidebar_label: Introduction +sidebar_position: 0 +pagination_next: null +pagination_prev: null +--- + + + +# Miden Core Concepts + +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 rollup 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** | Accounts and notes store only cryptographic commitments on-chain; full data remains with users | +| **Parallelism** | Single-account transactions enable concurrent execution without contention | +| **Scalability** | Client-side proving offloads computation; proof aggregation reduces on-chain 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** — unique identifier derived from initial code and storage +- **Code** — smart contract logic defining the account's interface +- **Storage** — key-value store with up to 256 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: + +- **Script** — code executed when the note is consumed +- **Inputs** — public data available to the consuming transaction +- **Assets** — tokens transferred to the recipient +- **Metadata** — sender, tag (for discovery), and auxiliary data + +Notes can be **public** (all data on-chain) or **private** (only a commitment stored). Private notes require off-chain communication between sender and recipient. + +### 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** — Rescue Prime Optimized hashing, 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** — 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.14/core-concepts/miden-vm/_category_.yml b/versioned_docs/version-0.14/core-concepts/miden-vm/_category_.yml new file mode 100644 index 00000000..ea52768c --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/advanced_topics/_category_.yml b/versioned_docs/version-0.14/core-concepts/miden-vm/advanced_topics/_category_.yml new file mode 100644 index 00000000..23cf4ba2 --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/advanced_topics/execution_trace_optimization.md b/versioned_docs/version-0.14/core-concepts/miden-vm/advanced_topics/execution_trace_optimization.md new file mode 100644 index 00000000..160182d0 --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/advanced_topics/index.md b/versioned_docs/version-0.14/core-concepts/miden-vm/advanced_topics/index.md new file mode 100644 index 00000000..0dd60067 --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/background.md b/versioned_docs/version-0.14/core-concepts/miden-vm/background.md new file mode 100644 index 00000000..3f559a96 --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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://nakamoto.com/cambrian-explosion-of-crypto-proofs/) + +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.14/core-concepts/miden-vm/design/_category_.yml b/versioned_docs/version-0.14/core-concepts/miden-vm/design/_category_.yml new file mode 100644 index 00000000..485baedd --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/design/chiplets/_category_.yml b/versioned_docs/version-0.14/core-concepts/miden-vm/design/chiplets/_category_.yml new file mode 100644 index 00000000..32a2c4e4 --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/design/chiplets/ace.md b/versioned_docs/version-0.14/core-concepts/miden-vm/design/chiplets/ace.md new file mode 100644 index 00000000..55e63c6e --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/design/chiplets/bitwise.md b/versioned_docs/version-0.14/core-concepts/miden-vm/design/chiplets/bitwise.md new file mode 100644 index 00000000..9079b866 --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/design/chiplets/hasher.md b/versioned_docs/version-0.14/core-concepts/miden-vm/design/chiplets/hasher.md new file mode 100644 index 00000000..46975e37 --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/miden-vm/design/chiplets/hasher.md @@ -0,0 +1,518 @@ +--- +title: "Hash Chiplet" +sidebar_position: 2 +--- + +# Hash chiplet + +Miden VM "offloads" all hash-related computations to a separate _hash processor_. This chiplet supports executing the [Poseidon2](https://eprint.iacr.org/2023/323) hash function in the following settings: + +- A single permutation of Poseidon2. +- A simple 2-to-1 hash. +- A linear hash of $n$ field elements. +- Merkle path verification. +- Merkle root update. + +The chiplet can be thought of as having a small instruction set of $11$ instructions. These instructions are listed below, and examples of how these instructions are used by the chiplet are described in the following sections. + +| Instruction | Description | Cycles | Context | Notes | +| ----------- | ----------- | ------ | ------- | ----- | +| `HR` | Executes a single round of the VM's native hash function | $0$-$30$, $32$-$62$, $64$-$94$... (not $31$, $63$, $95$...) | Any | | +| `BP` | Initiates computation of a single permutation, a 2-to-1 hash, or a linear hash of many elements | Multiples of $32$ ($0$, $32$, $64$...) | Start of computation | Concurrent with `HR` | +| `MP` | Initiates Merkle path verification computation | Multiples of $32$ | Start of computation | Concurrent with `HR` | +| `MV` | Initiates Merkle path verification for the "old" node value | Multiples of $32$ | Merkle root update | Concurrent with `HR` | +| `MU` | Initiates Merkle path verification for the "new" node value | Multiples of $32$ | Merkle root update | Concurrent with `HR` | +| `HOUT` | Returns the "output" portion of the hasher state (indices $[0,4)$) | $32n-1$ ($31$, $63$, $95$...) | End of computation | | +| `SOUT` | Returns entire hasher state | $32n-1$ ($31$, $63$, $95$...) | End of computation | Only after `BP` | +| `ABP` | Absorbs a new set of elements into the hasher state | $32n-1$ ($31$, $63$, $95$...) | Linear hash (multi-block) | Only after `BP` | +| `MPA` | Absorbs the next Merkle path node into the hasher state | $32n-1$ ($31$, $63$, $95$...) | Merkle path verification | Only after `MP` | +| `MVA` | Absorbs the next Merkle path node into the hasher state during Merkle path verification for the "old" node value | $32n-1$ ($31$, $63$, $95$...) | Merkle root update | Only after `MV` | +| `MUA` | Absorbs the next Merkle path node into the hasher state during Merkle path verification for the "new" node value | $32n-1$ ($31$, $63$, $95$...) | Merkle root update | Only after `MU` | + +## Chiplet trace + +Execution trace table of the chiplet consists of $16$ trace columns and $3$ periodic columns. The structure of the table is such that a single permutation of the hash function can be computed using $32$ table rows. The layout of a single 32-row cycle is summarized below (rows omitted are identical permutation rows). + +| Row (mod 32) | $k_2$ | $k_1$ | $k_0$ | $s_0,s_1,s_2$ | RATE0 ($h_0..h_3$) | RATE1 ($h_4..h_7$) | CAP ($h_8..h_{11}$) | $i$ | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | +| 0 | 1 | 0 | 0 | op-specific | input rate0 | input rate1 | input capacity | index | +| 1 | 0 | 0 | 0 | op-specific | permutation state | permutation state | permutation state | index | +| ... | ... | ... | ... | ... | ... | ... | ... | ... | +| 30 | 0 | 1 | 0 | op-specific | permutation state | permutation state | permutation state | index | +| 31 | 0 | 0 | 1 | op-specific | state (post-permutation) | state (post-permutation) | state (post-permutation) | index | + +The meaning of the columns is as follows: + +- Three periodic columns $k_0$, $k_1$, and $k_2$ are used to help select the instruction executed at a given row. All of these columns contain patterns which repeat every $32$ rows. For $k_0$ the pattern is $31$ zeros followed by $1$ one, helping us identify the last row in the cycle. For $k_1$ the pattern is $30$ zeros, $1$ one, and $1$ zero, which can be used to identify the second-to-last row in a cycle. For $k_2$ the pattern is $1$ one followed by $31$ zeros, which can identify the first row in the cycle. +- Three selector columns $s_0$, $s_1$, and $s_2$. These columns can contain only binary values (ones or zeros), and they are also used to help select the instruction to execute at a given row. +- Twelve hasher state columns $h_0, ..., h_{11}$. These columns are used to hold the hasher state for each round of the hash function permutation. The state is laid out as follows: + - The first eight columns ($h_0, ..., h_7$) are reserved for the rate elements of the state, arranged as two 4-element words (RATE0, RATE1). Once the permutation is complete, the hash output is located in the first rate word ($h_0, ..., h_3$). + - The last four columns ($h_8, ..., h_{11}$) are reserved for capacity elements of the state. When the state is initialized for hash computations, $h_8$ should be set to $0$ if the number of elements to be hashed is a multiple of the rate width ($8$). Otherwise, $h_8$ should be set to $1$. $h_9$ should be set to the domain value if a domain has been provided (as in the case of [control block hashing](../programs.md#program-hash-computation)). The remaining capacity lanes ($h_{10}$, $h_{11}$) are set to $0$. +- One index column $i$. This column is used to help with Merkle path verification and Merkle root update computations. + +In addition to the columns described above, the chiplet relies on two running product columns which are used to facilitate multiset checks (similar to the ones described [here](https://hackmd.io/@relgabizon/ByFgSDA7D)). These columns are: + +- $b_{chip}$ - which is used to tie the chiplet table with the main VM's stack and decoder. That is, values representing inputs consumed by the chiplet and outputs produced by the chiplet are multiplied into $b_{chip}$, while the main VM stack (or decoder) divides them out of $b_{chip}$. Thus, if the sets of inputs and outputs between the main VM stack and hash chiplet are the same, the value of $b_{chip}$ should be equal to $1$ at the start and the end of the execution trace. +- $p_1$ - which is used to keep track of the _sibling_ table used for Merkle root update computations. Specifically, when a root for the old leaf value is computed, we add an entry for all sibling nodes to the table (i.e., we multiply $p_1$ by the values representing these entries). When the root for the new leaf value is computed, we remove the entries for the nodes from the table (i.e., we divide $p_1$ by the value representing these entries). Thus, if both computations used the same set of sibling nodes (in the same order), the sibling table should be empty by the time Merkle root update procedure completes (i.e., the value of $p_1$ would be $1$). + +## Instruction flags + +As mentioned above, chiplet instructions are encoded using a combination of periodic and selector columns. These columns can be used to compute a binary flag for each instruction. Thus, when a flag for a given instruction is set to $1$, the chiplet executes this instruction. Formulas for computing instruction flags are listed below. + +| Flag | Value | Notes | +| ---------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| $f_{rpr}$ | $1 - k_0$ | Set to $1$ on the first $31$ steps of every $32$-step cycle. | +| $f_{bp}$ | $k_2 \cdot s_0 \cdot (1 - s_1) \cdot (1 - s_2)$ | Set to $1$ when selector flags are $(1, 0, 0)$ on rows which are multiples of $32$. | +| $f_{mp}$ | $k_2 \cdot s_0 \cdot (1 - s_1) \cdot s_2$ | Set to $1$ when selector flags are $(1, 0, 1)$ on rows which are multiples of $32$. | +| $f_{mv}$ | $k_2 \cdot s_0 \cdot s_1 \cdot (1 - s_2)$ | Set to $1$ when selector flags are $(1, 1, 0)$ on rows which are multiples of $32$. | +| $f_{mu}$ | $k_2 \cdot s_0 \cdot s_1 \cdot s_2$ | Set to $1$ when selector flags are $(1, 1, 1)$ on rows which are multiples of $32$. | +| $f_{hout}$ | $k_0 \cdot (1 - s_0) \cdot (1 - s_1) \cdot (1 - s_2)$ | Set to $1$ when selector flags are $(0, 0, 0)$ on rows which are $1$ less than a multiple of $32$. | +| $f_{sout}$ | $k_0 \cdot (1 - s_0) \cdot (1 - s_1) \cdot s_2$ | Set to $1$ when selector flags are $(0, 0, 1)$ on rows which are $1$ less than a multiple of $32$. | +| $f_{abp}$ | $k_0 \cdot s_0 \cdot (1 - s_1) \cdot (1 - s_2)$ | Set to $1$ when selector flags are $(1, 0, 0)$ on rows which are $1$ less than a multiple of $32$. | +| $f_{mpa}$ | $k_0 \cdot s_0 \cdot (1 - s_1) \cdot s_2$ | Set to $1$ when selector flags are $(1, 0, 1)$ on rows which are $1$ less than a multiple of $32$. | +| $f_{mva}$ | $k_0 \cdot s_0 \cdot s_1 \cdot (1 - s_2)$ | Set to $1$ when selector flags are $(1, 1, 0)$ on rows which are $1$ less than a multiple of $32$. | +| $f_{mua}$ | $k_0 \cdot s_0 \cdot s_1 \cdot s_2$ | Set to $1$ when selector flags are $(1, 1, 1)$ on rows which are $1$ less than a multiple of $32$. | + +A few additional notes about flag values: + +- With the exception of $f_{rpr}$, all flags are mutually exclusive. That is, if one flag is set to $1$, all other flats are set to $0$. +- With the exception of $f_{rpr}$, computing flag values involves $3$ multiplications, and thus the degree of these flags is $4$. +- We can also define a flag $f_{out} = k_0 \cdot (1 - s_0) \cdot (1 - s_1)$. This flag will be set to $1$ when either $f_{hout}=1$ or $f_{sout}=1$ in the current row. +- We can define a flag $f_{out}' = k_1 \cdot (1 - s_0') \cdot (1 - s_1')$. This flag will be set to $1$ when either $f_{hout}=1$ or $f_{sout}=1$ in the next row. + +We also impose the following restrictions on how values in selector columns can be updated: + +- Values in columns $s_1$ and $s_2$ must be copied over from one row to the next, unless $f_{out} = 1$ or $f_{out}' = 1$ indicating the `hout` or `sout` flag is set for the current or the next row. +- Value in $s_0$ must be set to $1$ if $f_{out}=1$ for the previous row, and to $0$ if any of the flags $f_{abp}$, $f_{mpa}$, $f_{mva}$, or $f_{mua}$ are set to $1$ for the previous row. + +The above rules ensure that we must finish one computation before starting another, and we can't change the type of the computation before the computation is finished. + +## Computation examples + +### Single permutation + +Computing a single permutation of Poseidon2 hash function involves the following steps: + +1. Initialize hasher state with $12$ field elements. +2. Apply Poseidon2 permutation. +3. Return the entire hasher state as output. + +The chiplet accomplishes the above by executing the following instructions: + +``` +[BP, HR] // init state and execute a hash round (concurrently) +HR HR HR HR HR HR HR HR HR HR // execute 10 more hash rounds +HR HR HR HR HR HR HR HR HR HR // execute 10 more hash rounds +HR HR HR HR HR HR HR HR HR HR // execute 10 more hash rounds +SOUT // return the entire state as output +``` + +Execution trace for this computation would look as illustrated below. + +| Row (mod 32) | $k_2$ | $k_1$ | $k_0$ | $s_0,s_1,s_2$ | RATE0 ($h_0..h_3$) | RATE1 ($h_4..h_7$) | CAP ($h_8..h_{11}$) | $i$ | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | +| 0 | 1 | 0 | 0 | BP | $a_0..a_3$ | $a_4..a_7$ | $a_8..a_{11}$ | $i$ | +| 1 | 0 | 0 | 0 | HR | permute | permute | permute | $i$ | +| ... | ... | ... | ... | ... | ... | ... | ... | ... | +| 31 | 0 | 0 | 1 | SOUT | $b_0..b_3$ | $b_4..b_7$ | $b_8..b_{11}$ | $i$ | + +In the above $\{a_0, ..., a_{11}\}$ is the input state of the hasher, and $\{b_0, ..., b_{11}\}$ is the output state of the hasher. + +### Simple 2-to-1 hash + +Computing a 2-to-1 hash involves the following steps: + +1. Initialize hasher state with $8$ field elements, setting the second capacity element to $domain$ if the domain is provided (as in the case of [control block hashing](../programs.md#program-hash-computation)) or else $0$, and the remaining capacity elements to $0$. +2. Apply Poseidon2 permutation. +3. Return elements $[0, 4)$ of the hasher state as output. + +The chiplet accomplishes the above by executing the following instructions: + +``` +[BP, HR] // init state and execute a hash round (concurrently) +HR HR HR HR HR HR HR HR HR HR // execute 10 more hash rounds +HR HR HR HR HR HR HR HR HR HR // execute 10 more hash rounds +HR HR HR HR HR HR HR HR HR HR // execute 10 more hash rounds +HOUT // return elements 0, 1, 2, 3 of the state as output (the digest) +``` + +Execution trace for this computation would look as illustrated below. + +| Row (mod 32) | $k_2$ | $k_1$ | $k_0$ | $s_0,s_1,s_2$ | RATE0 ($h_0..h_3$) | RATE1 ($h_4..h_7$) | CAP ($h_8..h_{11}$) | $i$ | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | +| 0 | 1 | 0 | 0 | BP | $a_0..a_3$ | $b_0..b_3$ | $0,\,domain,\,0,\,0$ | $i$ | +| 1 | 0 | 0 | 0 | HR | permute | permute | permute | $i$ | +| ... | ... | ... | ... | ... | ... | ... | ... | ... | +| 31 | 0 | 0 | 1 | HOUT | $c_0..c_3$ | unused | unused | $i$ | + +In the above, we compute the following: + +$$ +\{c_0, c_1, c_2, c_3\} \leftarrow hash(\{a_0, a_1, a_2, a_3\}, \{b_0, b_1, b_2, b_3\}) +$$ + +### Linear hash of n elements + +Computing a linear hash of $n$ elements consists of the following steps: + +1. Initialize hasher state with the first $8$ elements, setting the first capacity register to $0$ if $n$ is a multiple of the rate width ($8$) or else $1$, and the remaining capacity elements to $0$. +2. Apply Poseidon2 permutation. +3. Absorb the next set of elements into the state (up to $8$ elements), while keeping capacity elements unchanged. +4. Repeat steps 2 and 3 until all $n$ elements have been absorbed. +5. Return elements $[0, 4)$ of the hasher state as output. + +The chiplet accomplishes the above by executing the following instructions (for hashing $16$ elements): + +``` +[BP, HR] // init state and execute a hash round (concurrently) +HR HR HR HR HR HR HR HR HR HR // execute 10 more hash rounds +HR HR HR HR HR HR HR HR HR HR // execute 10 more hash rounds +HR HR HR HR HR HR HR HR HR HR // execute 10 more hash rounds +ABP // absorb the next set of elements into the state +HR HR HR HR HR HR HR HR HR HR // execute 10 more hash rounds +HR HR HR HR HR HR HR HR HR HR // execute 10 more hash rounds +HR HR HR HR HR HR HR HR HR HR HR // execute 11 more hash rounds +HOUT // return elements 0, 1, 2, 3 of the state as output (the digest) +``` + +Execution trace for this computation would look as illustrated below. + +| Row (mod 32) | $k_2$ | $k_1$ | $k_0$ | $s_0,s_1,s_2$ | RATE0 ($h_0..h_3$) | RATE1 ($h_4..h_7$) | CAP ($h_8..h_{11}$) | $i$ | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | +| 0 | 1 | 0 | 0 | BP | $a_0..a_3$ | $a_4..a_7$ | $p,\,0,\,0,\,0$ | $i$ | +| 1 | 0 | 0 | 0 | HR | permute | permute | permute | $i$ | +| ... | ... | ... | ... | ... | ... | ... | ... | ... | +| 31 | 0 | 0 | 1 | ABP | $b_0..b_3$ | $b_4..b_7$ | (carry) | $i$ | +| 32 | 1 | 0 | 0 | BP | $b_0..b_3$ | $b_4..b_7$ | (carry) | $i$ | +| ... | ... | ... | ... | ... | ... | ... | ... | ... | +| 63 | 0 | 0 | 1 | HOUT | $r_0..r_3$ | unused | unused | $i$ | + +In the above, the value absorbed into hasher state between rows $31$ and $32$ is the next batch of +$8$ input elements, which overwrite the rate lanes. Thus, if we define these elements as $b_i$ for +$i \in [0, 8)$, the above computes the following: + +$$ +\{r_0, r_1, r_2, r_3\} \leftarrow hash(a_0, ..., a_7, b_0, ..., b_7) +$$ + +### Verify Merkle path + +Verifying a Merkle path involves the following steps: + +1. Initialize hasher state with the leaf and the first node of the path, setting all capacity elements to $0$s. + a. Also, initialize the index register to the leaf's index value. +2. Apply Poseidon2 permutation. + a. Make sure the index value doesn't change during this step. +3. Copy the result of the hash to the next row, and absorb the next node of the Merkle path into the hasher state. + a. Remove a single bit from the index, and use it to determine how to place the copied result and absorbed node in the state. +4. Repeat steps 2 and 3 until all nodes of the Merkle path have been absorbed. +5. Return elements $[0, 4)$ of the hasher state as output. + a. Also, make sure the index value has been reduced to $0$. + +The chiplet accomplishes the above by executing the following instructions (for Merkle tree of depth $3$): + +``` +[MP, HR] // init state and execute a hash round (concurrently) +HR HR HR HR HR HR HR HR HR HR // execute 10 more hash rounds +HR HR HR HR HR HR HR HR HR HR // execute 10 more hash rounds +HR HR HR HR HR HR HR HR HR HR // execute 10 more hash rounds +MPA // copy result & absorb the next node into the state +HR HR HR HR HR HR HR HR HR HR // execute 10 more hash rounds +HR HR HR HR HR HR HR HR HR HR // execute 10 more hash rounds +HR HR HR HR HR HR HR HR HR HR HR // execute 11 more hash rounds +HOUT // return elements 0, 1, 2, 3 of the state as output (the digest) +``` + +Suppose we have a Merkle tree as illustrated below. This Merkle tree has $4$ leaves, each of which consists of $4$ field elements. For example, leaf $a$ consists of elements $a_0, a_1, a_2, a_3$, leaf be consists of elements $b_0, b_1, b_2, b_3$ etc. + +![hash_merkle_tree](../../img/design/chiplets/hasher/hash_merkle_tree.png) + +If we wanted to verify that leaf $d$ is in fact in the tree, we'd need to compute the following hashes: + +$$ +r \leftarrow hash(e, hash(c, d)) +$$ + +And if $r = g$, we can be convinced that $d$ is in fact in the tree at position $3$. Execution trace for this computation would look as illustrated below. + +| Row (mod 32) | $k_2$ | $k_1$ | $k_0$ | $s_0,s_1,s_2$ | RATE0 ($h_0..h_3$) | RATE1 ($h_4..h_7$) | CAP ($h_8..h_{11}$) | $i$ | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | +| 0 | 1 | 0 | 0 | MP | $c_0..c_3$ | $d_0..d_3$ | $0,0,0,0$ | $3$ | +| 1 | 0 | 0 | 0 | HR | permute | permute | permute | $3$ | +| ... | ... | ... | ... | ... | ... | ... | ... | ... | +| 31 | 0 | 0 | 1 | MPA | $e_0..e_3$ | $f_0..f_3$ | $0,0,0,0$ | $3$ | +| 32 | 1 | 0 | 0 | MP | $e_0..e_3$ | $f_0..f_3$ | $0,0,0,0$ | $1$ | +| ... | ... | ... | ... | ... | ... | ... | ... | ... | +| 63 | 0 | 0 | 1 | HOUT | $g_0..g_3$ | unused | unused | $0$ | + +In the above, the prover provides values for nodes $c$ and $e$ non-deterministically. +The index $i$ remains constant throughout a permutation and is shifted at the `MPA` row, so it +changes only between cycles. + +### Update Merkle root + +Updating a node in a Merkle tree (which also updates the root of the tree) can be simulated by verifying two Merkle paths: the path that starts with the old leaf and the path that starts with the new leaf. + +Suppose we have the same Merkle tree as in the previous example, and we want to replace node $d$ with node $d'$. The computations we'd need to perform are: + +$$ +r \leftarrow hash(e, hash(c, d)) +r' \leftarrow hash(e, hash(c, d')) +$$ + +Then, as long as $r = g$, and the same values were used for $c$ and $e$ in both computations, we can be convinced that the new root of the tree is $r'$. + +The chiplet accomplishes the above by executing the following instructions: + +``` +// verify the old merkle path +[MV, HR] // init state and execute a hash round (concurrently) +HR HR HR HR HR HR HR HR HR HR // execute 10 more hash rounds +HR HR HR HR HR HR HR HR HR HR // execute 10 more hash rounds +HR HR HR HR HR HR HR HR HR HR // execute 10 more hash rounds +MVA // copy result & absorb the next node into the state +HR HR HR HR HR HR HR HR HR HR // execute 10 more hash rounds +HR HR HR HR HR HR HR HR HR HR // execute 10 more hash rounds +HR HR HR HR HR HR HR HR HR HR HR // execute 11 more hash rounds +HOUT // return elements 0, 1, 2, 3 of the state as output (the digest) + +// verify the new merkle path +[MU, HR] // init state and execute a hash round (concurrently) +HR HR HR HR HR HR HR HR HR HR // execute 10 more hash rounds +HR HR HR HR HR HR HR HR HR HR // execute 10 more hash rounds +HR HR HR HR HR HR HR HR HR HR // execute 10 more hash rounds +MUA // copy result & absorb the next node into the state +HR HR HR HR HR HR HR HR HR HR // execute 10 more hash rounds +HR HR HR HR HR HR HR HR HR HR // execute 10 more hash rounds +HR HR HR HR HR HR HR HR HR HR HR // execute 11 more hash rounds +HOUT // return elements 0, 1, 2, 3 of the state as output (the digest) +``` + +The semantics of `MV` and `MU` instructions are similar to the semantics of `MP` instruction from the previous example (and `MVA` and `MUA` are similar to `MPA`) with one important difference: `MV*` instructions add the absorbed node (together with its index in the tree) to permutation column $p_1$, while `MU*` instructions remove the absorbed node (together with its index in the tree) from $p_1$. Thus, if the same nodes were used during both Merkle path verification, the state of $p_1$ should not change. This mechanism is used to ensure that the same internal nodes were used in both computations. + +## AIR constraints + +When describing AIR constraints, we adopt the following notation: for 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 described in this note work with two consecutive rows of the execution trace. + +### Selector columns constraints + +For selector columns, first we must ensure that only binary values are allowed in these columns. This can be done with the following constraints: + +$$ +s_0^2 - s_0 = 0 \text{ | degree} = 2 +s_1^2 - s_1 = 0 \text{ | degree} = 2 +s_2^2 - s_2 = 0 \text{ | degree} = 2 +$$ + +Next, we need to make sure that unless $f_{out}=1$ or $f_{out}'=1$, the values in columns $s_1$ and $s_2$ are copied over to the next row. This can be done with the following constraints: + +$$ +(s_1' - s_1) \cdot (1 - f_{out}') \cdot (1 - f_{out}) = 0 \text{ | degree} = 7 +(s_2' - s_2) \cdot (1 - f_{out}') \cdot (1 - f_{out}) = 0 \text{ | degree} = 7 +$$ + +Next, we need to enforce that if any of $f_{abp}, f_{mpa}, f_{mva}, f_{mua}$ flags is set to $1$, the next value of $s_0$ is $0$. In all other cases, $s_0$ should be unconstrained. These flags will only be set for rows that are 1 less than a multiple of 32 (the last row of each cycle). This can be done with the following constraint: + +$$ +s_0' \cdot (f_{abp} + f_{mpa} + f_{mva} + f_{mua})= 0 \text{ | degree} = 5 +$$ + +Lastly, we need to make sure that no invalid combinations of flags are allowed. This can be done with the following constraints: + +$$ +k_0 \cdot (1 - s_0) \cdot s_1 = 0 \text{ | degree} = 3 +$$ + +The above constraints enforce that on every step which is one less than a multiple of $32$, if $s_0 = 0$, then $s_1$ must also be set to $0$. Basically, if we set $s_0=0$, then we must make sure that either $f_{hout}=1$ or $f_{sout}=1$. + +### Node index constraints + +Node index column $i$ is relevant only for Merkle path verification and Merkle root update computations, but to simplify the overall constraint system, the same constraints will be imposed on this column for all computations. + +Overall, we want values in the index column to behave as follows: + +- When we start a new computation, we should be able to set $i$ to an arbitrary value. +- When a computation is finished, value in $i$ must be $0$. +- When we absorb a new node into the hasher state we must shift the value in $i$ by one bit to the right. +- In all other cases value in $i$ should not change. + +A shift by one bit to the right can be described with the following equation: $i = 2 \cdot i' + b$, where $b$ is the value of the bit which is discarded. Thus, as long as $b$ is a binary value, the shift to the right is performed correctly, and this can be enforced with the following constraint: + +$$ +b^2 - b = 0 +$$ + +Since we want to enforce this constraint only when a new node is absorbed into the hasher state, we'll define a flag for when this should happen as follows: + +$$ +f_{an} = f_{mp} + f_{mv} + f_{mu} + f_{mpa} + f_{mva} + f_{mua} +$$ + +And then the full constraint would looks as follows: + +$$ +f_{an} \cdot (b^2 - b) = 0 \text{ | degree} = 6 +$$ + +Next, to make sure when a computation is finished $i=0$, we can use the following constraint: + +$$ +f_{out} \cdot i = 0 \text{ | degree} = 4 +$$ + +Finally, to make sure that the value in $i$ is copied over to the next row unless we are absorbing a new row or the computation is finished, we impose the following constraint: + +$$ +(1 - f_{an} - f_{out}) \cdot (i' - i) = 0 \text{ | degree} = 5 +$$ + +To satisfy these constraints for computations not related to Merkle paths (i.e., 2-to-1 hash and liner hash of elements), we can set $i = 0$ at the start of the computation. This guarantees that $i$ will remain $0$ until the end of the computation. + +### Hasher state constraints + +Hasher state columns $h_0, ..., h_{11}$ should behave as follows: + +- For the first $31$ rows of every $32$-row cycle (i.e., when $k_0=0$), we need to apply [Poseidon2](https://eprint.iacr.org/2023/323) round constraints to the hasher state. For brevity, we omit these constraints from this note. +- On the $32$nd row of every $32$-row cycle, we apply the constraints based on which transition flag is set as described in the table below. + +Specifically, when absorbing the next set of elements into the state during linear hash computation (i.e., $f_{abp} = 1$), the last $4$ elements (the capacity portion) are carried over to the next row. For $j \in [0, 4)$ this can be described as follows: + +$$ +f_{abp} \cdot (h'_{j+8} - h_{j+8}) = 0 \text{ | degree} = 5 +$$ + +When absorbing the next node during Merkle path computation (i.e., $f_{mp} + f_{mv} + f_{mu}=1$), the result of the previous hash ($h_0, ..., h_3$) is copied over either to $(h_0', ..., h_3')$ or to $(h_4', ..., h_7')$ depending on the value of $b$, which is defined in the same way as in the previous section. For $j \in [0, 4)$ this can be described as follows: + +$$ +(f_{mp} + f_{mv} + f_{mu}) \cdot ((1 - b) \cdot (h_{j}' - h_{j}) + b \cdot (h_{j + 4}' - h_{j})) = 0 \text{ | degree} = 6 +$$ + +Note, that when a computation is completed (i.e., $f_{out}=1$), the next hasher state is unconstrained. + +### Multiset check constraints + +In this sections we describe constraints which enforce updates for [multiset check columns](../lookups/multiset.md) $b_{chip}$ and $p_1$. These columns can be updated only on rows which are multiples of $32$ or $1$ less than a multiple of $32$. On all other rows the values in the columns remain the same. + +To simplify description of the constraints, we define the following variables. Below, 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. + +$$ +m = op_{label} + 2^4 \cdot k_2 + 2^5 \cdot k_0 +v_h = \alpha_0 + \alpha_1 \cdot m + \alpha_2 \cdot (clk + 1) + \alpha_3 \cdot i +v_a = \sum_{j=0}^{3}(\alpha_{j+4} \cdot h_j) +v_b = \sum_{j=0}^{3}(\alpha_{j+8} \cdot h_{j+4}) +v_c = \sum_{j=0}^{3}(\alpha_{j+12} \cdot h_{j+8}) +v_d = \sum_{j=0}^{3}(\alpha_{j+4} \cdot h_{j+4}) +$$ + +Message slot layout is fixed: slots $0..3$ use $\alpha_{4..7}$, slots $4..7$ use +$\alpha_{8..11}$, and slots $8..11$ use $\alpha_{12..15}$. For partial messages, +we place the payload into slots $0..7$ and set capacity slots $8..11$ to zero. +Leaf/output messages use only slots $0..3$. + +In the above: + +- $m$ is a _transition label_, composed of the [operation label](./index.md#operation-labels) and the periodic columns that uniquely identify each transition function. The values in the $k_0$ and $k_2$ periodic columns are included to identify the row in the hash cycle where the operation occurs. They serve to differentiate between operations that share selectors but occur at different rows in the cycle, such as `BP`, which uses $op_{linhash}$ at the first row in the cycle to initiate a linear hash, and `ABP`, which uses $op_{linhash}$ at the last row in the cycle to absorb new elements. +- $v_h$ is a _common header_ which is a combination of the transition label, a unique row address, and the node index. For the unique row address, the `clk` column from the system component is used, but we add $1$, because the system's `clk` column starts at $0$. +- $v_a$, $v_b$, $v_c$ are the rate0, rate1, and capacity words (4 elements each). +- $v_d$ reuses the rate0 alpha slice on the rate1 word. This is used for Merkle leaf encoding + when the right child is selected. For next-row values, $v_d'$ is defined the same way using $h'$. + +#### Chiplets bus constraints + +As described previously, the [chiplets bus](./index.md#chiplets-bus) $b_{chip}$, implemented as a running product column, is used to tie the hash chiplet with the main VM's stack and decoder. When receiving inputs from or returning results to the stack (or decoder), the hash chiplet multiplies $b_{chip}$ by their respective values. On the other side, when sending inputs to the hash chiplet or receiving results from the chiplet, the stack (or decoder) divides $b_{chip}$ by their values. + +In the section below we describe only the hash chiplet side of the constraints (i.e., multiplying $b_{chip}$ by relevant values). We define the values which are to be multiplied into $b_{chip}$ for each operation as follows: + +When starting a new simple or linear hash computation (i.e., $f_{bp}=1$) or when returning the entire state of the hasher ($f_{sout}=1$), the entire hasher state is included into $b_{chip}$: + +$$ +v_{all} = v_h + v_a + v_b + v_c +$$ + +When starting a Merkle path computation (i.e., $f_{mp} + f_{mv} + f_{mu} = 1$), we include the leaf of the path into $b_{chip}$. The leaf is selected from the state based on value of $b$ (defined as in the previous section): + +$$ +v_{leaf} = v_h + (1-b) \cdot v_a + b \cdot v_d +$$ + +When absorbing a new set of elements into the state while computing a linear hash (i.e., $f_{abp}=1$), we include the next rate (state slots $0..7$) into $b_{chip}$: + +$$ +v_{abp} = v_h + v_a' + v_b' +$$ + +When a computation is complete (i.e., $f_{hout}=1$), we include the first rate word of the hasher state (the result) into $b_{chip}$: + +$$ +v_{res} = v_h + v_a +$$ + +Using the above values, we can describe the constraints for updating column $b_{chip}$ as follows. + +$$ +b_{chip}' = b_{chip} \cdot ((f_{bp} + f_{sout}) \cdot v_{all} + (f_{mp} + f_{mv} + f_{mu}) \cdot v_{leaf} + f_{abp} \cdot v_{abp} + f_{hout} \cdot v_{res} + +1 - (f_{bp} + f_{mp} + f_{mv} + f_{mu} + f_{abp} + f_{out})) +$$ + +The above constraint reduces to the following under various flag conditions: + +| Condition | Applied constraint | +| -------------- | ------------------------------------- | +| $f_{bp} = 1$ | $b_{chip}' = b_{chip} \cdot v_{all}$ | +| $f_{sout} = 1$ | $b_{chip}' = b_{chip} \cdot v_{all}$ | +| $f_{mp} = 1$ | $b_{chip}' = b_{chip} \cdot v_{leaf}$ | +| $f_{mv} = 1$ | $b_{chip}' = b_{chip} \cdot v_{leaf}$ | +| $f_{mu} = 1$ | $b_{chip}' = b_{chip} \cdot v_{leaf}$ | +| $f_{abp} = 1$ | $b_{chip}' = b_{chip} \cdot v_{abp}$ | +| $f_{hout} = 1$ | $b_{chip}' = b_{chip} \cdot v_{res}$ | +| Otherwise | $b_{chip}' = b_{chip}$ | + +Note that the degree of the above constraint is $7$. + +#### Sibling table constraints + +_Note: Although this table is described independently, it is implemented as part of the [chiplets virtual table](../chiplets/index.md#chiplets-virtual-table), which combines all virtual tables required by any of the chiplets into a single master table._ + +As mentioned previously, the sibling table (represented by running column $p_1$) is used to keep track of sibling nodes used during Merkle root update computations. For this computation, we need to enforce the following rules: + +- When computing the old Merkle root, whenever a new sibling node is absorbed into the hasher state (i.e., $f_{mv} + f_{mva} = 1$), an entry for this sibling should be included into $p_1$. +- When computing the new Merkle root, whenever a new sibling node is absorbed into the hasher state (i.e., $f_{mu} + f_{mua} = 1$), the entry for this sibling should be removed from $p_1$. + +To simplify the description of the constraints, we use variables $v_a$ and $v_b$ defined above and define the value representing an entry in the sibling table as follows: + +$$ +v_{sib0} = \alpha_0 + \alpha_3 \cdot i + v_b + +v_{sib1} = \alpha_0 + \alpha_3 \cdot i + v_a + +v_{sibling} = (1-b) \cdot v_{sib0} + b \cdot v_{sib1} +$$ + +Using the above value, we can define the constraint for updating $p_1$ as follows: + +$$ +p_1' \cdot \left( (f_{mv} + f_{mva}) \cdot v_{sibling} + 1 - (f_{mv} + f_{mva}) \right) = +p_1 \cdot \left( (f_{mu} + f_{mua}) \cdot v_{sibling} + 1 - (f_{mu} + f_{mua}) \right) +$$ + +The above constraint reduces to the following under various flag conditions: + +| Condition | Applied constraint | +| ------------- | ------------------------------ | +| $f_{mv} = 1$ | $p_1' \cdot v_{sibling} = p_1$ | +| $f_{mva} = 1$ | $p_1' \cdot v_{sibling} = p_1$ | +| $f_{mu} = 1$ | $p_1' = p_1 \cdot v_{sibling}$ | +| $f_{mua} = 1$ | $p_1' = p_1 \cdot v_{sibling}$ | +| Otherwise | $p_1' = p_1$ | + +Note that the degree of the above constraint is $7$. + +To make sure computation of the old Merkle root is immediately followed by the computation of the new Merkle root, we impose the following constraint: + +$$ +(f_{bp} + f_{mp} + f_{mv}) \cdot (1 - p_1) = 0 \text{ | degree} = 5 +$$ + +The above means that whenever we start a new computation which is not the computation of the new Merkle root, the sibling table must be empty. Thus, after the hash chiplet computes the old Merkle root, the only way to clear the table is to compute the new Merkle root. + +Together with boundary constraints enforcing that $p_1=1$ at the first and last rows of the running product column which implements the sibling table, the above constraints ensure that if a node was included into $p_1$ as a part of computing the old Merkle root, the same node must be removed from $p_1$ as a part of computing the new Merkle root. These two boundary constraints are described as part of the [chiplets virtual table constraints](../chiplets/index.md#chiplets-virtual-table-constraints). diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/design/chiplets/index.md b/versioned_docs/version-0.14/core-concepts/miden-vm/design/chiplets/index.md new file mode 100644 index 00000000..2472a6c8 --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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 18 trace columns, which allows space for the widest chiplet component (the hash chiplet) and a column to select for it. + +![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. The padding will simply contain zeroes. + +### 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 | Cycle Length | Internal Degree | Chiplet Selector Degree | Total Degree | Columns | Chiplet Selector Flag | +| --------------- | :----------: | :-------------: | :---------------------: | :----------: | :-----: | --------------------- | +| Hash chiplet | 32 | 8 | 1 | 9 | 17 | $\{0\}$ | +| Bitwise chiplet | 8 | 3 | 2 | 5 | 13 | $\{1, 0\}$ | +| Memory | - | 6 | 3 | 9 | 12 | $\{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:** there is no conflict, and therefore no change, since all constraints are periodic. + +**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. Although there are three columns for chiplet selectors, the stacked trace design means that they do not all act as selectors for the entire trace. 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) \text{ | degree} = 3 +> s_0 \cdot s_1 \cdot s_2 \cdot (s'_2 - s_2) \text{ | degree} = 4 +> s_0 \cdot s_1 \cdot s_2 \cdot s_3 \cdot (s'_3 - s_3) \text{ | degree} = 5 +> s_0 \cdot s_1 \cdot s_2 \cdot s_3 \cdot s_4 \cdot (s'_4 - s_4) \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_precompile 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.14/core-concepts/miden-vm/design/chiplets/kernel_rom.md b/versioned_docs/version-0.14/core-concepts/miden-vm/design/chiplets/kernel_rom.md new file mode 100644 index 00000000..4ff902ff --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/miden-vm/design/chiplets/kernel_rom.md @@ -0,0 +1,119 @@ +--- +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 + +The kernel ROM table consists of five columns. +The following example table shows the execution trace of the kernel ROM with procedure digests $a, b, c$, which were called 1, 2, and 0 times, respectively. +Each digest is included once to respond to the initialization request by the public inputs, and then repeated for each call made by the decoder. + +| $s_{first}$ | $r_0$ | $r_1$ | $r_2$ | $r_3$ | +|-------------|-------|-------|-------|-------| +| 1 | $a_0$ | $a_1$ | $a_2$ | $a_3$ | +| 0 | $a_0$ | $a_1$ | $a_2$ | $a_3$ | +| 1 | $b_0$ | $b_1$ | $b_2$ | $b_3$ | +| 0 | $b_0$ | $b_1$ | $b_2$ | $b_3$ | +| 0 | $b_0$ | $b_1$ | $b_2$ | $b_3$ | +| 1 | $c_0$ | $c_1$ | $c_2$ | $c_3$ | + +The meaning of columns in the above is as follows: + +- Column $s_{first}$ specifies the start of a block of rows with identical kernel procedure digests. +- $r_0, ..., r_3$ contain the digests of the kernel procedures. The values in these columns can change only when $s_{first}$ is set to 1 in the next row. Otherwise, the values in the $r$ columns remain the same. + +## Constraints + +We first define the selector flag $f_{krom}$ that is active in all rows of the kernel ROM chiplet. + +>$$ +> f_{krom} = s_0 \cdot s_1 \cdot s_2 \cdot s_3 \cdot (1 - s_4) \text{ | degree} = 5 +>$$ + +where $s_i$ are the chiplet selector flags. Refer to the [chiplets order](./index.md#chiplets-order) for more information. + +The following constraints are required to enforce the correctness of the kernel ROM trace. + +The $s_{first}$ column is a selector indicating the start of a new digest included in the kernel ROM chiplet trace. +In this row, the chiplet responds to a bus request made by the verifier to ensure consistency with the set of kernel procedure digests given as public inputs. + +As $s_{first}$ is a selector, it must be binary. + +> $$ +> f_{krom} \cdot (s_{first}^2 - s_{first}) = 0 \text{ | degree} = 7 +> $$ + + +The flag $s_{first}$ must be set to be 1 in the first row of the kernel ROM chiplet. +Otherwise, the digest in this row would not be matched with one of the input procedure roots. +This constraint is enforced in the last row of the previous trace, using selector columns from the [chiplets](index.md) module. +More precisely, we use the virtual $f_{ACE}$ flag, which is active in all rows of the ACE chiplet (which comes right before this chiplet), +along with the selector $s_3$ which transitions from 0 to 1 in the last row, allowing us to target the first row of the kernel ROM trace. + +> $$ +> f_{ACE} \cdot s_{3}' \cdot (1 - s_{4}') \cdot (s_{first}' - 1) = 0 \text{ | degree} = 7 +> $$ + +The contiguity of the digests in a block is ensured by enforcing equality between digests across two consecutive rows, whenever the next row is not the start of a new block. +That is, when $s_{first}' = 0$, it must hold that $r_i = r_i'$. +We disable this constraint in the last row of the kernel ROM chiplet trace by using the kernel ROM chiplet selector $s_4'$, since the latter transitions from 0 to 1 in the first row of the next chiplet. + +For $i \in \{0,1,2,3\}$, + +> $$ +> f_{krom} \cdot (1 - s_4') \cdot (1 - s_{first}') \cdot (r_i' - r_i) = 0 \text{ | degree} = 8 +> $$ + +### Chiplets bus constraints + +The kernel ROM chiplet must ensure that all kernel procedure digests requested by the decoder correspond to one of the digests provided by the verifier through public inputs. +This is achieved by making use of the chiplet bus $b_{bus}$, responding to requests made by the decoder and by the verifier through public inputs. + +In the first row of each new block of hashes in the kernel ROM chiplet trace (i.e., when $s_{first} = 1$), the chiplet responds to a message $v_{init}$ requested by the verifier. +Since these initialization messages must match, the set of digests across all blocks must be equal to the set of procedure digests provided by the verifier (though not necessarily in the same order). + +Whenever a digest is requested by the decoder during program block hashing of the [`SYSCALL` operation](../decoder/constraints.md#block-hash-computation-constraints), a new row is added to the trace after the first row which is used to respond to one of the initialization requests made by the verifier using public inputs. +The chiplet responds to the request with a message $v_{call}$. + +In other words, the selector $s_{first}$ indicates whether the chiplet should respond to the decoder or the verifier initialization requests. +If a digest is requested $n$ times by the decoder, the same digest appears in a single block of length $n+1$. + +The variables $v_{init}$ and $v_{call}$ representing the bus messages contain reduced bus messages containing a kernel procedure digest. +Denoting the random values received from the verifier as $\alpha_0, \alpha_1$, etc., this can be defined as + +$$ +\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} +$$ + +Here, $\textsf{KERNEL\_PROC\_INIT}$ and $\textsf{KERNEL\_PROC\_CALL}$ are the unique [operation labels](./index.md#operation-labels) for the kernel ROM bus message. + +Each row of the kernel ROM chiplet trace responds to either a procedure digest initialization or decoder call request. +Since the $s_{first}$ column defines which type of response is sent to the bus, it is used to combine both requests into a single constraint given by + +> $$ +> b'_{chip} = b_{chip} \cdot (s_{first} \cdot v_{init} + (1 - s_{first}) \cdot v_{call}) \text{ | degree} = 3. +> $$ + +The above simplifies to + +- $s_{first} = 1$: $b'_{chip} = b_{chip} \cdot v_{init}$, when responding to a $\textsf{KERNEL\_PROC\_INIT}$ request. +- $s_{first} = 0$: $b'_{chip} = b_{chip} \cdot v_{call}$, when responding to a $\textsf{KERNEL\_PROC\_CALL}$ request. + +The kernel procedure digests initialization requests are implemented by imposing a boundary constraint in the first row of the $b_{chip}$ column. +This is described in the [chiplets bus constraints](../chiplets/index.md#chiplets-bus-constraints). + +By using the bus to initialize the kernel ROM procedure digest in this way, the verifier only learns which procedures can be invoked but doesn't learn how often they were called, if at all. + +The full set of constraints applied to the $b_{chip}$ are described as part of the [chiplets bus constraints](../chiplets/index.md#chiplets-bus-constraints). diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/design/chiplets/memory.md b/versioned_docs/version-0.14/core-concepts/miden-vm/design/chiplets/memory.md new file mode 100644 index 00000000..772d338a --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/design/decoder/_category_.yml b/versioned_docs/version-0.14/core-concepts/miden-vm/design/decoder/_category_.yml new file mode 100644 index 00000000..3f761fa9 --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/design/decoder/constraints.md b/versioned_docs/version-0.14/core-concepts/miden-vm/design/decoder/constraints.md new file mode 100644 index 00000000..76f58a7f --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/miden-vm/design/decoder/constraints.md @@ -0,0 +1,746 @@ +--- +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 | Top stack element is dropped. | +| `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` or `LOOP` operation is executed, the top of the operand stack must contain a binary value: + +> $$ +> (f_{split} + f_{loop}) \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 $32$: + +> $$ +> f_{respan} \cdot (a' - a - 32) = 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 hasher as the block's ID. The result of the hash is available $31$ rows down in the hasher table (i.e., at row with index equal to block ID plus $31$). We read the result from the hasher table at the time the `END` operation is executed for a given 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 hasher as the ID of the first operation batch in the block. As we absorb additional operation batches into the hasher (by executing `RESPAN` operation), the batch address is incremented by $32$. This moves the "pointer" into the hasher table $32$ rows down with every new batch. We read the result from the hasher table at the time the `END` operation is executed for a given 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#multiset-check-constraints). +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' - 1) ++ \sum_{i=0}^7(\alpha_{4+i} \cdot h_i) +$$ + +$$ +h_{end} = +\alpha_0 + \alpha_1 \cdot L_{end} + \alpha_2 \cdot (a + 31) ++ \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 if the value at the top of the operand stack is $1$, and row $(a', a, 0)$ is added to the block stack table if the value at the top of the operand stack is $0$: + +$$ +v_{loop} = f_{loop} \cdot (\alpha_0 + \alpha_1 \cdot a' + \alpha_2 \cdot a + \alpha_3 \cdot s_0) \text{ | degree} = 6 +$$ + +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 and the top of the stack is $1$, hash of the loop body +is added to the block hash table (with `is_loop_body = 1`). When the top of the stack +is $0$, no insertion is made: + +$$ +v_{loop} = f_{loop} \cdot (s_0 \cdot m(a', h_0..h_3, 0, 1) + (1 - s_0)) \text{ | degree} = 7 +$$ + +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`, or `HALT`, then this block +is the first child and `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_{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 $32$, 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.14/core-concepts/miden-vm/design/decoder/index.md b/versioned_docs/version-0.14/core-concepts/miden-vm/design/decoder/index.md new file mode 100644 index 00000000..5edd45ac --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/miden-vm/design/decoder/index.md @@ -0,0 +1,739 @@ +--- +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. Computing such a hash requires $32$ rows in the hash chiplet. +2. A sequential hash of $n$ elements. Computing such a hash requires multiple absorption steps, and at each step $8$ field elements are absorbed into the hasher. Thus, computing a sequential hash of $n$ elements requires $32 \cdot \lceil {n/8} \rceil$ rows in the hash chiplet. At the end, we also get $4$ field elements representing the result. + +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 chiplet selector flags according to the methodology described [here](../chiplets/hasher.md#multiset-check-constraints). +* $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 the specifics of how auxiliary hasher table rows are reduced to field elements (described [here](../chiplets/hasher.md#multiset-check-constraints)). 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 + 31) + \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 chiplet selector flags according to the methodology described [here](../chiplets/hasher.md#multiset-check-constraints). +* $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 + 31) + \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 chiplet selector flags according to the methodology described [here](../chiplets/hasher.md#multiset-check-constraints). + +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 + \lceil n / 8 \rceil \cdot 32 - 1) + \sum_{i=0}^3 (\alpha_{i+4} \cdot u_i) +$$ + +Thus, for example, if $n = 14$, the result of the hash will be available at hasher row $r + 63$. + +### 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. + +When the VM executes a `LOOP` operation, it does the following: + +1. Pops the stack and:\ + a. If the popped value is $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). Then, adds a tuple `(blk, loop_body_hash, 0, 1)` to the block hash table.\ + b. If the popped value is $0$, adds `(blk, prnt, 0, 0...)` to the block stack table. In this case, nothing is added to the block hash table.\ + c. If the popped value is neither $1$ nor $0$, the execution fails. +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 the padded input $[h_0, ..., h_3, 0, 0, 0, 0]$. + +#### 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`. +* $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 + 31` as row address in the auxiliary hashing table. +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 $32$. +2. Removes the tuple `(blk, prnt, 0, 0...)` from the block stack table. +3. Adds the tuple `(blk+32, 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+32` as batch ID. + +The net result of the above is that we incremented the ID of the current block by $32$ 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 + +When decoding a *loop* bock, we need to consider two possible scenarios: + +* When the top of the stack is $1$, we need to enter the loop and execute loop body at least once. +* When the top of the stack is, $0$ we need to skip the loop. + +In both cases, we need to pop an element off the top of the stack. + +#### Executing the loop + +If the top of the stack is $1$, the VM executes a `LOOP` operation. This removes the top element from the stack and adds the hash of the loop's body to the block hash table. It also adds a row to the block stack table setting the `is_loop` value to $1$. + +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 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 in the beginning we set `is_loop` column in the block stack table to $1$, $h_6$ column will be set to $1$ when the `END` operation is executed. Thus, executing the `END` operation will also remove the top value from the stack. If the removed value is not $0$, the operation will fail. Thus, the VM can exit the loop block only when the top of the stack is $0$. + +#### Skipping the loop + +If the top of the stack is $0$, the VM still executes the `LOOP` operation. But unlike in the case when we need to enter the loop, the VM sets `is_loop` flag to $0$ in the block stack table, and does not add any rows to the block hash table. The last point means that the only possible operation to be executed after the `LOOP` operation is the `END` operation. This is illustrated in the diagram below. + +![decoder_loop_skipping](../../img/design/decoder/decoder_loop_skipping.png) + +Moreover, since we've set the `is_loop` flag to $0$, executing the `END` operation does not remove any items from the stack. + +### 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 hasher address by $32$. This is done because absorbing additional $8$ elements into the hasher state requires $32$ more rows in the auxiliary hasher table. + +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 + 32, 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 hasher row at address `addr + 31`, which, in our example, will be equal to `blk + 63`. + +#### 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.14/core-concepts/miden-vm/design/index.md b/versioned_docs/version-0.14/core-concepts/miden-vm/design/index.md new file mode 100644 index 00000000..81705a13 --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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 $71$ main trace columns, $2$ buses, and $5$ virtual tables, 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.14/core-concepts/miden-vm/design/lookups/_category_.yml b/versioned_docs/version-0.14/core-concepts/miden-vm/design/lookups/_category_.yml new file mode 100644 index 00000000..d76cc7b5 --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/design/lookups/index.md b/versioned_docs/version-0.14/core-concepts/miden-vm/design/lookups/index.md new file mode 100644 index 00000000..4dd158c8 --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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 precompile transcript 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.14/core-concepts/miden-vm/design/lookups/logup.md b/versioned_docs/version-0.14/core-concepts/miden-vm/design/lookups/logup.md new file mode 100644 index 00000000..f3b4d73d --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/design/lookups/multiset.md b/versioned_docs/version-0.14/core-concepts/miden-vm/design/lookups/multiset.md new file mode 100644 index 00000000..805649a0 --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/design/programs.md b/versioned_docs/version-0.14/core-concepts/miden-vm/design/programs.md new file mode 100644 index 00000000..101f3867 --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/miden-vm/design/programs.md @@ -0,0 +1,135 @@ +--- +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. When the VM encounters a *loop* block, it checks the top of the stack. If the top of the stack is $1$, it executes the loop body, if the top of the stack is $0$, the block is not executed. If the top of the stack is neither $0$ nor $1$, the execution fails. + +After the body of the loop is executed, the VM checks the top of the stack again. If the top of the stack is $1$, the body is executed again, if the top of the stack is $0$, the loop is exited. If the top of the stack is neither $0$ nor $1$, the execution fails. + +![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. + +### 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$ is a loop block, thus, the VM will pop the value off the top of the stack. If the popped value is $1$, the VM will execute the body of the loop defined by block $d$. If the popped value is $0$, the VM will not execute block $d$ and instead will move 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.14/core-concepts/miden-vm/design/range.md b/versioned_docs/version-0.14/core-concepts/miden-vm/design/range.md new file mode 100644 index 00000000..982caa8d --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/design/stack/_category_.yml b/versioned_docs/version-0.14/core-concepts/miden-vm/design/stack/_category_.yml new file mode 100644 index 00000000..246c4af7 --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/design/stack/crypto_ops.md b/versioned_docs/version-0.14/core-concepts/miden-vm/design/stack/crypto_ops.md new file mode 100644 index 00000000..e8bdc141 --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/miden-vm/design/stack/crypto_ops.md @@ -0,0 +1,501 @@ +--- +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 + 31) + \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 rows must be present in the trace of the hash chiplet, and that they must be exactly $31$ rows apart. + +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 + 32 \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 rows must be present in the trace of the hash chiplet, and that they must be exactly $32 \cdot d - 1$ rows apart, where $d$ is the depth of the node. + +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 + 32 \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 + 32 \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 + 2 \cdot 32 \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 rows for both, the old and the new node/root combinations, must be present in the trace of the hash chiplet, and that they must be exactly $32 \cdot d - 1$ rows apart, where $d$ is the depth of the node. 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 FRI layer folding by a factor of 4 for FRI protocol executed in a degree 2 extension of the base field. It also performs several computations needed for checking correctness of the folding from the previous layer as well as simplifying folding of the next FRI layer. + +The stack for the operation is expected to be arranged as follows: +- The first $8$ stack elements contain $4$ query points to be folded. Each point is represented by two field elements because points to be folded are in the extension field. We denote these points as $q_0 = (v_0, v_1)$, $q_1 = (v_2, v_3)$, $q_2 = (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 $d\_seg$ is a value indicating domain segment from which the position in the original domain was folded. It can be computed as $\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 domain segment values can be $0$, $1$, $2$, or $3$. +- The next element $poe$ is a power of initial domain generator which aids in a computation of 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 $d\_seg$. +- 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 appropriate value of $q_0, ..., q_3$ to verify that the folding of the previous layer was done correctly. +- 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 degree of the constraints low, a number of intermediate values are used. Specifically, the operation relies on all $6$ helper registers, and also uses the first $10$ elements of the stack at the next state for degree reduction purposes. Thus, once the operation has been executed, the top $10$ elements of the stack can be considered to be "garbage". + +> 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_PRECOMPILE + +The `log_precompile` operation logs a precompile event by recording two user-provided words (`TAG` and `COMM`) into the precompile transcript (implemented via an Poseidon2 sponge). Initialization and boundary enforcement are handled via variable‑length public inputs; see [Precompile flow](./precompiles.md) for a high‑level overview. This section concentrates on the stack interaction and bus messages. + +### Operation Overview + +The stack is expected to be arranged as `[COMM, TAG, PAD, ...]`. See [Precompiles](./precompiles.md#core-data) for a thorough explanation of the precompile commitment model. In brief: +- `TAG` encodes the precompile's event ID (first element) along with metadata or simple outputs (remaining elements), +- `COMM` commits to the precompile inputs (and may include outputs for long results), +- `PAD` is a word that will get overwritten in the next cycle. + +Additionally, the processor maintains a persistent precompile transcript state word `CAP` (the sponge capacity) that is updated with each `LOG_PRECOMPILE` invocation. This word is provided non-deterministically via helper registers and is denoted `CAP_PREV`. The virtual table bus links each removal to a matching insertion, ensuring a single, consistent state sequence. + +The operation evaluates `[R0, R1, CAP_NEXT] = Poseidon2([COMM, TAG, CAP_PREV])`, with the following stack transition + +``` +Before: [COMM, TAG, PAD, ...] +After: [R0, R1, CAP_NEXT, ...] +``` + +The VM updates its internal precompile transcript state (`CAP_NEXT`) using a bus as we describe below. The rate outputs `R0` and `R1` are transient; together with `CAP_NEXT`, they are typically dropped by the caller immediately after logging. + +The operation uses the following helper registers: +- $h_0$: Hasher chiplet row address +- $h_1, h_2, h_3, h_4$: Previous capacity `CAP_PREV` + +Note: helper registers expose `CAP_PREV` for bus constraints only; the VM maintains the +transcript state 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{CAP}^{\text{prev}}_i &= h_{i+1} &&\text{(helper registers)}\\ +\mathsf{TAG}_i &= s_{4+i} &&\text{(stack slots 4..7)}\\ +\mathsf{COMM}_i &= s_{i} &&\text{(stack slots 0..3)} +\end{aligned} +\qquad i \in \{0,1,2,3\}. +$$ + +The input message therefore reduces the Poseidon2 state in the canonical order `[COMM, TAG, CAP_PREV]`: + +$$ +v_{\text{input}} = \alpha_0 + \alpha_1 \cdot op_{linhash} + \alpha_2 \cdot h_0 + \sum_{i=0}^{3} \alpha_{i+4} \cdot \mathsf{COMM}_i + \sum_{i=0}^{3} \alpha_{i+8} \cdot \mathsf{TAG}_i + \sum_{i=0}^{3} \alpha_{i+12} \cdot \mathsf{CAP}_{\text{prev},i}. +$$ + +Thirty-one rows later, the `op_retstate` response provides the permuted state `[R0, R1, CAP_{next}]` (with R0 on top). Denote the stack after the instruction by $s'_i$; the top twelve elements are `[R0, R1, CAP_NEXT]`. Thus + +$$ +\begin{aligned} +\mathsf{R}_0{}_i &= s'_{i},\\ +\mathsf{R}_1{}_i &= s'_{4+i},\\ +\mathsf{CAP}^{\text{next}}_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 + 31) + \sum_{i=0}^{3} \alpha_{i+4} \cdot \mathsf{R}_0{}_i + \sum_{i=0}^{3} \alpha_{i+8} \cdot \mathsf{R}_1{}_i + \sum_{i=0}^{3} \alpha_{i+12} \cdot \mathsf{CAP}^{\text{next}}_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 rows must be present in the trace of the hash chiplet, and that they must be exactly 31 rows apart. The Poseidon2 permutation outputs `[R0, R1, CAP]` (with R0 on top); on the stack, the VM stores these words as `[R0, R1, CAP]`. + +Given the similarity with the `HPERM` opcode which sends the same message, albeit from different variables in the trace, it should be possible to combine the bus constraint in a way that avoids increasing the degree of the overall bus expression. + +### Capacity Initialization + +Inside the VM, the transcript state (sponge capacity) is tracked via the virtual table bus: each update removes the previous entry before inserting the next one. + +We denote the messages for removing and inserting the message as + +$$ +v_{rem} = \alpha_0 + \alpha_1 \cdot op_{log\_precompile} + \sum_{j=0}^{3} \alpha_{j+2} \cdot \mathsf{CAP\_PREV}_j +$$ + +$$ +v_{ins} = \alpha_0 + \alpha_1 \cdot op_{log\_precompile} + \sum_{j=0}^{3} \alpha_{j+2} \cdot \mathsf{CAP\_NEXT}_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 transcript state, the verifier initializes the bus with variable‑length public inputs (see kernel ROM chiplet). More specifically, it constrains the first value of the bus to be equal to + +$$ +b_{vtable,0} = \frac{v_{ins, init}}{v_{rem, last}} +$$ + +Usually, we initialize the transcript state to the empty word `[0,0,0,0]`, though it may also be used to extend an existing running state from a previous execution. The final transcript state is provided to the verifier (as a variable‑length public input) and enforced via the boundary constraint. +The messages $v_{ins, init}$ and $v_{rem, last}$ are given by + +$$ +v_{ins,init} = \alpha_0 + \alpha_1 \cdot op_{log\_precompile}, +$$ + +$$ +v_{rem,last} = \alpha_0 + \alpha_1 \cdot op_{log\_precompile} + \sum_{j=0}^{3} \alpha_{j+2} \cdot \mathsf{CAP\_FINAL}_j. +$$ + +The bus records only the transcript state (sponge capacity); the VM never finalizes the digest internally. Instead, the verifier (or any external consumer) reconstructs the transcript from the recorded requests. By convention, when a digest is required, the transcript is finalized by absorbing two empty words and applying one more permutation—matching the fact that `log_precompile` discards the rate outputs (`R0`, `R1`) after each absorption. diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/design/stack/field_ops.md b/versioned_docs/version-0.14/core-concepts/miden-vm/design/stack/field_ops.md new file mode 100644 index 00000000..6a01e459 --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/design/stack/index.md b/versioned_docs/version-0.14/core-concepts/miden-vm/design/stack/index.md new file mode 100644 index 00000000..73cbc674 --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/design/stack/io_ops.md b/versioned_docs/version-0.14/core-concepts/miden-vm/design/stack/io_ops.md new file mode 100644 index 00000000..2c03d6de --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/design/stack/op_constraints.md b/versioned_docs/version-0.14/core-concepts/miden-vm/design/stack/op_constraints.md new file mode 100644 index 00000000..ecc768fc --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/miden-vm/design/stack/op_constraints.md @@ -0,0 +1,311 @@ +--- +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$ | +| `LOGPRECOMPILE` | $94$ | `101_1110` | [Crypto ops](./crypto_ops.md#log_precompile) | $5$ | +| `` | $95$ | `101_1111` | | $5$ | + +Note that the `SPLIT` and `LOOP` operations are grouped together under the common prefix `101010`, and thus can have a common flag of degree $4$ (using $e_0$ for degree reduction). This is important because both of these operations shift the stack to the left. + + +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 +$$ + +A flag which is set to $1$ when $f_{split} = 1$ or $f_{loop} = 1$: + +$$ +f_{split\_loop} = e_0 \cdot (1 - b_3) \cdot b_2 \cdot (1 - b_1) \text{ | degree} = 4 +$$ + +Using the above variables, we compute left-shift flag as follows: + +$$ +f_{shl} = (1 - b_6) \cdot b_5 \cdot (1 - b_4) + f_{add3\_madd} + f_{split\_loop} + 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`. +* $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. + +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.14/core-concepts/miden-vm/design/stack/precompiles.md b/versioned_docs/version-0.14/core-concepts/miden-vm/design/stack/precompiles.md new file mode 100644 index 00000000..855959f6 --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/miden-vm/design/stack/precompiles.md @@ -0,0 +1,79 @@ +# Precompiles + +Precompiles let Miden programs defer expensive computations to the host while still producing +auditable evidence inside the STARK. This page describes how the VM, host, prover, and verifier +coordinate to maintain a sequential commitment to every precompile invocation. + +## Core data + +| Concept | Description | +| ------- | ----------- | +| `PrecompileRequest` | Minimal calldata for a precompile, recorded by the host when the event handler runs. It contains exactly the information needed to deterministically recompute the result and the commitment. Requests are included in the proof artifact. | +| `PrecompileCommitment` | A word pair `(TAG, COMM)` computed by the MASM wrapper, and deterministically recomputable from the corresponding `PrecompileRequest`. `COMM` typically commits to inputs, and may also include outputs for long results; the three free elements in `TAG` carry metadata and/or simple results. Together `(TAG, COMM)` represent the full request (inputs + outputs). | +| `PrecompileTranscript` | A sequential commitment to all precompile requests. Implemented with an Poseidon2 sponge; the VM stores only the capacity (4 elements). The verifier reconstructs the same transcript by re‑evaluating requests and their commitments. Finalizing yields a transcript digest. | + +## Lifecycle overview + +1. **Wrapper emits event** – The MASM wrapper stages inputs (e.g., on stack/memory) and emits the event for the target precompile. +2. **Host handler runs** – The host executes the event handler, reads required inputs from the current process state, stores a `PrecompileRequest` (raw calldata) for later verification, and pushes the precompile result to the VM via the advice stack. +3. **Wrapper constructs commitment** – The wrapper pops result(s) from advice, computes `(TAG, COMM)` per the precompile’s convention, and prepares to log the operation. +4. **`log_precompile` records the commitment** – The wrapper invokes `log_precompile` with `[COMM, TAG, PAD, ...]`. The instruction: + - Reads the previous transcript capacity `CAP_PREV` (non‑deterministically via helper registers). + - Applies the Poseidon2 permutation to `[COMM, TAG, CAP_PREV]`, producing `[R0, R1, CAP_NEXT]`. + - Writes `[R0, R1, CAP_NEXT]` back onto the stack; programs typically drop these words immediately. +5. **Capacity tracking via vtable** – Capacity is tracked inside the VM via the chiplets’ virtual table; the host never tracks capacity. The table always stores the current capacity (the transcript state). On each `log_precompile`: + - The previous capacity is removed from the table. + - The permutation links `CAP_PREV --[COMM, TAG]--> CAP_NEXT`. + - The next capacity is inserted back into the table. + This enforces that updates can only occur by applying the permutation. +6. **Trace output and proof** – The capacity state is used to construct the vtable auxiliary column, while the prover stores only the ordered `PrecompileRequest`s in the proof. +7. **Verifier reconstruction** – The verifier replays each request via a `PrecompileVerifier` to recompute `(TAG, COMM)`, records them into a fresh transcript, and enforces the initial/final capacity via public inputs. To check correct linking, the verifier initializes the column with an initial insertion of the empty capacity and a removal of the final capacity; the final capacity is provided as a public input to the AIR. +8. **Finalization convention** – When a digest is needed, finalize the transcript by absorbing two empty words (zeros in the rate) and permuting once. The transcript digests the ordered sequence of `[COMM, TAG]` words for all requests; `log_precompile` discards rate outputs (`R0`, `R1`), so only the capacity persists. + +## Responsibilities + +| Participant | Responsibilities | +| ----------- | ---------------- | +| VM | Executes `log_precompile`, maintains the capacity word internally, and participates in capacity initialization via the chiplets’ virtual table. | +| Host | Executes the event handler, reads inputs from process state, stores `PrecompileRequest`, and returns the result via the advice provider (typically the advice stack; map/Merkle store as needed). | +| MASM wrapper | Collects inputs and emits the event; pops results from advice; computes `(TAG, COMM)`; invokes `log_precompile`. | +| Prover | Includes the precompile requests in the proof. | +| Verifier | Replays requests via registered verifiers, rebuilds the transcript, enforces the initial/final capacity via variable‑length public inputs, and finalizes to a digest if needed. | + +## Conventions + +- Tag layout: `TAG = [event_id, meta1, meta2, meta3]`. + - First element is the precompile’s `event_id`. + - The remaining three elements carry metadata or simple results: + - Examples: byte length of inputs; boolean validity of a signature; flag bits. +- Commitment layout: `COMM` + - Typically commits to inputs. + - May also include outputs when results are long, so that `(TAG, COMM)` together represent the full request (inputs + outputs). + - The exact composition is precompile‑specific and defined by its verifier specification. +- `log_precompile` stack effect: `[COMM, TAG, PAD, ...] -> [R0, R1, CAP_NEXT, ...]` where + `Poseidon2([COMM, TAG, CAP_PREV]) = [R0, R1, CAP_NEXT]`. + +- Input encoding: + - By convention, inputs are encoded as packed u32 values in field elements (4 bytes per element, little‑endian). If the input length is not a multiple of 4, the final u32 is zero‑padded. Because of this packing, wrappers commonly include the byte length in `TAG` to distinguish data bytes from padding. + +## Examples + +- Hash function + - Inputs: byte sequence at a given memory location; Output: digest (long). + - Wrapper emits the event; handler reads memory and returns digest via advice; wrapper computes: + - `TAG = [event_id, len_bytes, 0, 0]` + - `COMM = Poseidon2( Poseidon2(input_words) || Poseidon2(digest_words) )` (bind input and digest) + - Wrapper calls `log_precompile` with `[COMM, TAG, PAD, ...]` and drops the outputs. + +- Signature scheme + - Inputs: public key, message (or prehash), signature; may include flag bits indicating special operation options. Output: `is_valid` (boolean). + - Wrapper emits the event; handler verifies and may push auxiliary results; wrapper computes: + - `TAG = [event_id, is_valid, flags, 0]` (encode simple result and flags) + - `COMM = Poseidon2( prepared_inputs[..] )` (inputs‑only is typical when outputs are simple) + - Wrapper calls `log_precompile` to record the request commitment and result tag. + +## Related reading + +- [`log_precompile` instruction](../../user_docs/assembly/instruction_reference.md) – stack behaviour and semantics. +- `PrecompileTranscript` implementation (`core/src/precompile.rs`) – transcript details in the codebase. +- Kernel ROM chiplet initialization pattern (`../chiplets/kernel_rom.md`) – example use of variable‑length public inputs to initialize a chiplet/aux column via the bus. diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/design/stack/stack_ops.md b/versioned_docs/version-0.14/core-concepts/miden-vm/design/stack/stack_ops.md new file mode 100644 index 00000000..656393ee --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/design/stack/system_ops.md b/versioned_docs/version-0.14/core-concepts/miden-vm/design/stack/system_ops.md new file mode 100644 index 00000000..84e9c3de --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/miden-vm/design/stack/system_ops.md @@ -0,0 +1,66 @@ +--- +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$. diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/design/stack/u32_ops.md b/versioned_docs/version-0.14/core-concepts/miden-vm/design/stack/u32_ops.md new file mode 100644 index 00000000..2d17cb41 --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/miden-vm/design/stack/u32_ops.md @@ -0,0 +1,296 @@ +--- +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 +$$ + +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 +$$ + +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.14/core-concepts/miden-vm/img/design/chiplets/bitwise/bitwise_execution_trace.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/chiplets/bitwise/bitwise_execution_trace.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/chiplets/chiplets.png b/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/chiplets/chiplets.png new file mode 100644 index 00000000..fa0ebaeb Binary files /dev/null and b/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/chiplets/chiplets.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/chiplets/hasher/hash_merkle_tree.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/chiplets/hasher/hash_merkle_tree.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/chiplets/memory/memory_alternative_design.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/chiplets/memory/memory_alternative_design.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/chiplets/memory/memory_context_separation.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/chiplets/memory/memory_context_separation.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/chiplets/memory/memory_limitation_diagram.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/chiplets/memory/memory_limitation_diagram.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/chiplets/memory/memory_miden_vm_layout.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/chiplets/memory/memory_miden_vm_layout.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/chiplets/memory/memory_non_contiguous_memory.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/chiplets/memory/memory_non_contiguous_memory.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/chiplets/memory/memory_read_write.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/chiplets/memory/memory_read_write.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/chiplets/memory/memory_reading_memory.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/chiplets/memory/memory_reading_memory.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/chiplets/memory/memory_writing_to_memory.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/chiplets/memory/memory_writing_to_memory.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/decoder/block_hash_table.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/decoder/block_hash_table.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/decoder/constraints/air_decoder_columns.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/decoder/constraints/air_decoder_columns.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/decoder/constraints/air_decoder_in_spans_column_constraint.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/decoder/constraints/air_decoder_in_spans_column_constraint.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/decoder/constraints/air_decoder_left_right_child.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/decoder/constraints/air_decoder_left_right_child.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/decoder/constraints/air_decoder_op_group_constraint.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/decoder/constraints/air_decoder_op_group_constraint.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/decoder/decoder_OPERATION_batch_flags.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/decoder/decoder_OPERATION_batch_flags.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/decoder/decoder_block_stack_table.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/decoder/decoder_block_stack_table.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/decoder/decoder_call_operation.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/decoder/decoder_call_operation.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/decoder/decoder_decoding_span_block_with_push.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/decoder/decoder_decoding_span_block_with_push.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/decoder/decoder_dyn_block_decoding.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/decoder/decoder_dyn_block_decoding.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/decoder/decoder_dyn_operation.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/decoder/decoder_dyn_operation.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/decoder/decoder_dyncall_operation.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/decoder/decoder_dyncall_operation.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/decoder/decoder_end_operation.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/decoder/decoder_end_operation.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/decoder/decoder_halt_operation.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/decoder/decoder_halt_operation.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/decoder/decoder_imm_vale_op_group_table.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/decoder/decoder_imm_vale_op_group_table.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/decoder/decoder_join_block_decoding.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/decoder/decoder_join_block_decoding.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/decoder/decoder_join_operation.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/decoder/decoder_join_operation.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/decoder/decoder_loop_execution.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/decoder/decoder_loop_execution.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/decoder/decoder_loop_operation.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/decoder/decoder_loop_operation.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/decoder/decoder_loop_skipping.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/decoder/decoder_loop_skipping.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/decoder/decoder_multi_batch_span.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/decoder/decoder_multi_batch_span.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/decoder/decoder_op_group_table.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/decoder/decoder_op_group_table.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/decoder/decoder_op_group_table_after_span_op.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/decoder/decoder_op_group_table_after_span_op.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/decoder/decoder_op_group_table_multi_span.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/decoder/decoder_op_group_table_multi_span.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/decoder/decoder_op_group_table_post_respan.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/decoder/decoder_op_group_table_post_respan.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/decoder/decoder_operation_group_decoding.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/decoder/decoder_operation_group_decoding.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/decoder/decoder_repeat_operation.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/decoder/decoder_repeat_operation.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/decoder/decoder_respan_operation.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/decoder/decoder_respan_operation.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/decoder/decoder_single_batch_span.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/decoder/decoder_single_batch_span.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/decoder/decoder_span_block.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/decoder/decoder_span_block.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/decoder/decoder_split_block_decoding.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/decoder/decoder_split_block_decoding.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/decoder/decoder_split_operation.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/decoder/decoder_split_operation.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/decoder/decoder_state_block_hash_2.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/decoder/decoder_state_block_hash_2.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/decoder/decoder_state_block_hash_4.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/decoder/decoder_state_block_hash_4.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/decoder/decoder_state_block_hash_6.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/decoder/decoder_state_block_hash_6.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/decoder/decoder_syscall_operation.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/decoder/decoder_syscall_operation.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/decoder/decoder_trace.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/decoder/decoder_trace.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/lookups/logup_component.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/lookups/logup_component.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/lookups/logup_table.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/lookups/logup_table.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/programs/call_block.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/programs/call_block.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/programs/dyn_block.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/programs/dyn_block.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/programs/join_block.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/programs/join_block.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/programs/loop_block.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/programs/loop_block.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/programs/mast_of_program.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/programs/mast_of_program.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/programs/span_block_creation.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/programs/span_block_creation.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/programs/split_block.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/programs/split_block.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/programs/syscall_block.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/programs/syscall_block.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/range/rc_16_bit_logup.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/range/rc_16_bit_logup.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/range/rc_16_bit_range_check.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/range/rc_16_bit_range_check.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/range/rc_8_bit_logup.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/range/rc_8_bit_logup.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/range/rc_8_bit_range_check.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/range/rc_8_bit_range_check.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/range/rc_better_construction.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/range/rc_better_construction.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/range/rc_table_post_8_bit_range_check.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/range/rc_table_post_8_bit_range_check.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/range/rc_with_bridge_rows.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/range/rc_with_bridge_rows.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/range/rc_with_bridge_rows.zip b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/range/rc_with_bridge_rows.zip differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/stack/crypto_ops/EVALCIRCUIT.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/stack/crypto_ops/EVALCIRCUIT.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/stack/crypto_ops/FRIE2F4.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/stack/crypto_ops/FRIE2F4.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/stack/crypto_ops/HORNERBASE.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/stack/crypto_ops/HORNERBASE.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/stack/crypto_ops/HORNEREXT.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/stack/crypto_ops/HORNEREXT.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/stack/crypto_ops/HPERM.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/stack/crypto_ops/HPERM.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/stack/crypto_ops/MPVERIFY.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/stack/crypto_ops/MPVERIFY.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/stack/crypto_ops/MRUPDATE.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/stack/crypto_ops/MRUPDATE.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/stack/decorator_operations/DIVRESULTU64.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/stack/decorator_operations/DIVRESULTU64.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/stack/decorator_operations/MERKLENODE.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/stack/decorator_operations/MERKLENODE.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/stack/field_operations/ADD.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/stack/field_operations/ADD.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/stack/field_operations/AND.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/stack/field_operations/AND.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/stack/field_operations/EQ.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/stack/field_operations/EQ.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/stack/field_operations/EQZ.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/stack/field_operations/EQZ.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/stack/field_operations/EXPACC.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/stack/field_operations/EXPACC.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/stack/field_operations/EXT2MUL.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/stack/field_operations/EXT2MUL.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/stack/field_operations/INCR.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/stack/field_operations/INCR.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/stack/field_operations/INV.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/stack/field_operations/INV.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/stack/field_operations/MUL.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/stack/field_operations/MUL.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/stack/field_operations/NEG.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/stack/field_operations/NEG.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/stack/field_operations/NOT.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/stack/field_operations/NOT.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/stack/field_operations/OR.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/stack/field_operations/OR.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/stack/io_ops/ADVPOP.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/stack/io_ops/ADVPOP.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/stack/io_ops/ADVPOPW.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/stack/io_ops/ADVPOPW.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/stack/io_ops/MLOAD.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/stack/io_ops/MLOAD.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/stack/io_ops/MLOADW.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/stack/io_ops/MLOADW.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/stack/io_ops/MSTORE.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/stack/io_ops/MSTORE.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/stack/io_ops/MSTOREW.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/stack/io_ops/MSTOREW.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/stack/io_ops/MSTREAM.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/stack/io_ops/MSTREAM.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/stack/io_ops/SDEPTH.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/stack/io_ops/SDEPTH.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/stack/overflow_table_layout.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/stack/overflow_table_layout.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/stack/stack_1st_left_shift.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/stack/stack_1st_left_shift.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/stack/stack_ops/CSWAP.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/stack/stack_ops/CSWAP.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/stack/stack_ops/CSWAPW.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/stack/stack_ops/CSWAPW.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/stack/stack_ops/DROP.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/stack/stack_ops/DROP.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/stack/stack_ops/DUP(n).png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/stack/stack_ops/DUP(n).png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/stack/stack_ops/MOVDN(n).png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/stack/stack_ops/MOVDN(n).png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/stack/stack_ops/MOVUP(n).png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/stack/stack_ops/MOVUP(n).png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/stack/stack_ops/PAD.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/stack/stack_ops/PAD.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/stack/stack_ops/SWAP.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/stack/stack_ops/SWAP.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/stack/stack_ops/SWAPDW.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/stack/stack_ops/SWAPDW.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/stack/stack_ops/SWAPW.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/stack/stack_ops/SWAPW.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/stack/stack_ops/SWAPW2.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/stack/stack_ops/SWAPW2.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/stack/stack_ops/SWAPW3.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/stack/stack_ops/SWAPW3.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/stack/stack_overflow_push_2nd_item.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/stack/stack_overflow_push_2nd_item.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/stack/stack_overflow_table_post_1_right_shift.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/stack/stack_overflow_table_post_1_right_shift.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/stack/stack_overflow_table_post_2_right_shift.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/stack/stack_overflow_table_post_2_right_shift.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/stack/stack_right_shift.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/stack/stack_right_shift.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/stack/system_ops/ASSERT.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/stack/system_ops/ASSERT.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/stack/system_ops/CLK.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/stack/system_ops/CLK.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/stack/trace_layout.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/stack/trace_layout.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/stack/u32_operations/U32ADD.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/stack/u32_operations/U32ADD.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/stack/u32_operations/U32ADD3.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/stack/u32_operations/U32ADD3.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/stack/u32_operations/U32AND.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/stack/u32_operations/U32AND.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/stack/u32_operations/U32ASSERT2.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/stack/u32_operations/U32ASSERT2.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/stack/u32_operations/U32DIV.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/stack/u32_operations/U32DIV.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/stack/u32_operations/U32MADD.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/stack/u32_operations/U32MADD.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/stack/u32_operations/U32MUL.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/stack/u32_operations/U32MUL.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/stack/u32_operations/U32SPLIT.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/stack/u32_operations/U32SPLIT.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/stack/u32_operations/U32SUB.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/stack/u32_operations/U32SUB.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/stack/u32_operations/U32XOR.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/design/stack/u32_operations/U32XOR.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/vm_trace.png b/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/vm_trace.png new file mode 100644 index 00000000..62788bca Binary files /dev/null and b/versioned_docs/version-0.14/core-concepts/miden-vm/img/design/vm_trace.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/intro/vm_components.png b/versioned_docs/version-0.14/core-concepts/miden-vm/img/intro/vm_components.png new file mode 100644 index 00000000..109cb8d7 Binary files /dev/null and b/versioned_docs/version-0.14/core-concepts/miden-vm/img/intro/vm_components.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/user_docs/assembly/assembly_to_VM.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/user_docs/assembly/assembly_to_VM.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/user_docs/assembly/execution_contexts/context_transitions.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/user_docs/assembly/execution_contexts/context_transitions.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/user_docs/assembly/execution_contexts/mem_layout.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/user_docs/assembly/execution_contexts/mem_layout.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/img/user_docs/assembly/overview/miden_vm_overview.png b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/img/user_docs/assembly/overview/miden_vm_overview.png differ diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/index.md b/versioned_docs/version-0.14/core-concepts/miden-vm/index.md new file mode 100644 index 00000000..8b499359 --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/overview.md b/versioned_docs/version-0.14/core-concepts/miden-vm/overview.md new file mode 100644 index 00000000..d1d00960 --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/performance.md b/versioned_docs/version-0.14/core-concepts/miden-vm/performance.md new file mode 100644 index 00000000..45174910 --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/miden-vm/performance.md @@ -0,0 +1,75 @@ +--- +title: "Performance" +sidebar_position: 4 +--- + +# Performance + +> **Note:** The benchmark data in this document is currently outdated. The proving stack has recently +transitioned from Winterfell to Plonky3, and these numbers should be refreshed. + +The benchmarks below should be viewed only as a rough guide for expected future performance. The reasons for this are twofold: +1. Not all constraints have been implemented yet, and we expect that there will be some slowdown once constraint evaluation is completed. +2. Many optimizations have not been applied yet, and we expect that there will be some speedup once we dedicate some time to performance optimizations. + +Overall, we don't expect the benchmarks to change significantly, but there will definitely be some deviation from the below numbers in the future. + +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 1% 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 Fibonacci calculator program on Apple M1 Pro 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 | +| :-------------: | :------------: | :----------: | :-----------: | :--------: | +| 210 | 1 ms | 60 ms | 20 MB | 46 KB | +| 212 | 2 ms | 180 ms | 52 MB | 56 KB | +| 214 | 8 ms | 680 ms | 240 MB | 65 KB | +| 216 | 28 ms | 2.7 sec | 950 MB | 75 KB | +| 218 | 81 ms | 11.4 sec | 3.7 GB | 87 KB | +| 220 | 310 ms | 47.5 sec | 14 GB | 100 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. + +We can also generate proofs at a higher security level. The cost of doing so is roughly doubling of proving time and roughly 40% increase in proof size. In the benchmarks below, the same Fibonacci calculator program was executed on Apple M1 Pro CPU at 128-bit target security level: + +| VM cycles | Execution time | Proving time | RAM consumed | Proof size | +| :-------------: | :------------: | :----------: | :-----------: | :--------: | +| 210 | 1 ms | 120 ms | 30 MB | 61 KB | +| 212 | 2 ms | 460 ms | 106 MB | 77 KB | +| 214 | 8 ms | 1.4 sec | 500 MB | 90 KB | +| 216 | 27 ms | 4.9 sec | 2.0 GB | 103 KB | +| 218 | 81 ms | 20.1 sec | 8.0 GB | 121 KB | +| 220 | 310 ms | 90.3 sec | 20.0 GB | 138 KB | + +## 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 8-core CPU (Apple M1 Pro), the current version of Miden VM operates at around 100 KHz. And when executed on a 64-core CPU (Amazon Graviton 3), the VM operates at around 250 KHz. + +In the benchmarks below, the VM executes the same Fibonacci calculator program for 220 cycles at 96-bit target security level: + +| Machine | Execution time | Proving time | Execution % | Implied Frequency | +| ------------------------------ | :------------: | :----------: | :---------: | :---------------: | +| Apple M1 Pro (16 threads) | 310 ms | 7.0 sec | 4.2% | 140 KHz | +| Apple M2 Max (16 threads) | 280 ms | 5.8 sec | 4.5% | 170 KHz | +| AMD Ryzen 9 5950X (16 threads) | 270 ms | 10.0 sec | 2.6% | 100 KHz | +| Amazon Graviton 3 (64 threads) | 330 ms | 3.6 sec | 8.5% | 265 KHz | + +### Recursive 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 considerably slower than regular hash functions. + +In the benchmarks below we execute the same Fibonacci calculator program for 220 cycles at 96-bit target security level using Poseidon2 hash function instead of BLAKE3: + +| Machine | Execution time | Proving time | Proving time (HW) | +| ------------------------------ | :------------: | :----------: | :---------------: | +| Apple M1 Pro (16 threads) | 310 ms | 94.3 sec | 42.0 sec | +| Apple M2 Max (16 threads) | 280 ms | 75.1 sec | 20.9 sec | +| AMD Ryzen 9 5950X (16 threads) | 270 ms | 59.3 sec | | +| Amazon Graviton 3 (64 threads) | 330 ms | 21.7 sec | 14.9 sec | + +In the above, proof generation on some platforms can be hardware-accelerated. Specifically: + +* On Apple M1/M2 platforms the built-in GPU is used for a part of proof generation process. diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/theme/Admonition/Icon/Danger.tsx b/versioned_docs/version-0.14/core-concepts/miden-vm/theme/Admonition/Icon/Danger.tsx new file mode 100644 index 00000000..ab14d7de --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/theme/Admonition/Icon/Info.tsx b/versioned_docs/version-0.14/core-concepts/miden-vm/theme/Admonition/Icon/Info.tsx new file mode 100644 index 00000000..59e48a52 --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/theme/Admonition/Icon/Note.tsx b/versioned_docs/version-0.14/core-concepts/miden-vm/theme/Admonition/Icon/Note.tsx new file mode 100644 index 00000000..d7c524b3 --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/theme/Admonition/Icon/Tip.tsx b/versioned_docs/version-0.14/core-concepts/miden-vm/theme/Admonition/Icon/Tip.tsx new file mode 100644 index 00000000..219bb8d0 --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/theme/Admonition/Icon/Warning.tsx b/versioned_docs/version-0.14/core-concepts/miden-vm/theme/Admonition/Icon/Warning.tsx new file mode 100644 index 00000000..f96398d1 --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/theme/Admonition/Layout/index.tsx b/versioned_docs/version-0.14/core-concepts/miden-vm/theme/Admonition/Layout/index.tsx new file mode 100644 index 00000000..7b2c170d --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/theme/Admonition/Layout/styles.module.css b/versioned_docs/version-0.14/core-concepts/miden-vm/theme/Admonition/Layout/styles.module.css new file mode 100644 index 00000000..88df7e63 --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/theme/Admonition/Type/Caution.tsx b/versioned_docs/version-0.14/core-concepts/miden-vm/theme/Admonition/Type/Caution.tsx new file mode 100644 index 00000000..b570a37a --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/theme/Admonition/Type/Danger.tsx b/versioned_docs/version-0.14/core-concepts/miden-vm/theme/Admonition/Type/Danger.tsx new file mode 100644 index 00000000..49901fa9 --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/theme/Admonition/Type/Info.tsx b/versioned_docs/version-0.14/core-concepts/miden-vm/theme/Admonition/Type/Info.tsx new file mode 100644 index 00000000..018e0a16 --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/theme/Admonition/Type/Note.tsx b/versioned_docs/version-0.14/core-concepts/miden-vm/theme/Admonition/Type/Note.tsx new file mode 100644 index 00000000..c99e0385 --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/theme/Admonition/Type/Tip.tsx b/versioned_docs/version-0.14/core-concepts/miden-vm/theme/Admonition/Type/Tip.tsx new file mode 100644 index 00000000..18604a5e --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/theme/Admonition/Type/Warning.tsx b/versioned_docs/version-0.14/core-concepts/miden-vm/theme/Admonition/Type/Warning.tsx new file mode 100644 index 00000000..61d9597b --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/theme/Admonition/Types.tsx b/versioned_docs/version-0.14/core-concepts/miden-vm/theme/Admonition/Types.tsx new file mode 100644 index 00000000..2a100190 --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/theme/Admonition/index.tsx b/versioned_docs/version-0.14/core-concepts/miden-vm/theme/Admonition/index.tsx new file mode 100644 index 00000000..8f4225da --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/theme/DocCard/index.tsx b/versioned_docs/version-0.14/core-concepts/miden-vm/theme/DocCard/index.tsx new file mode 100644 index 00000000..a95b4901 --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/theme/DocCard/styles.module.css b/versioned_docs/version-0.14/core-concepts/miden-vm/theme/DocCard/styles.module.css new file mode 100644 index 00000000..06c3b039 --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/tools/_category_.yml b/versioned_docs/version-0.14/core-concepts/miden-vm/tools/_category_.yml new file mode 100644 index 00000000..af33c248 --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/tools/index.md b/versioned_docs/version-0.14/core-concepts/miden-vm/tools/index.md new file mode 100644 index 00000000..0a1cb80c --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/usage.md b/versioned_docs/version-0.14/core-concepts/miden-vm/usage.md new file mode 100644 index 00000000..82d519a6 --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/miden-vm/usage.md @@ -0,0 +1,160 @@ +--- +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.90** 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. + +#### Enable Debugging features + +You can use the run command with `--debug` parameter to enable debugging with the [debug instruction](./user_docs/assembly/debugging.md) such as `debug.stack`: + +```shell +./target/optimized/miden-vm run [path_to.masm] --debug +``` + +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 debug instruction enabled + +Inside `miden-vm/masm-examples/fib/fib.masm`, insert `debug.stack` instruction anywhere between `begin` and `end`. Then run: + +```shell +./target/optimized/miden-vm run miden-vm/masm-examples/fib/fib.masm -n 1 --debug +``` + +You should see output similar to "Stack state before step ..." diff --git a/versioned_docs/version-0.14/core-concepts/miden-vm/user_docs/_category_.yml b/versioned_docs/version-0.14/core-concepts/miden-vm/user_docs/_category_.yml new file mode 100644 index 00000000..62ab4f48 --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/user_docs/assembly/_category_.yml b/versioned_docs/version-0.14/core-concepts/miden-vm/user_docs/assembly/_category_.yml new file mode 100644 index 00000000..6b8f36ca --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/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.14/core-concepts/miden-vm/user_docs/assembly/code_organization.md b/versioned_docs/version-0.14/core-concepts/miden-vm/user_docs/assembly/code_organization.md new file mode 100644 index 00000000..915d48e4 --- /dev/null +++ b/versioned_docs/version-0.14/core-concepts/miden-vm/user_docs/assembly/code_organization.md @@ -0,0 +1,426 @@ +--- +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.