Polygon Miden

A rollup for high-throughput, private applications.

Using Polygon Miden, builders can create novel, high-throughput, private applications for payments, DeFi, digital assets, and gaming. Applications and users are secured by Ethereum and AggLayer.

If you want to join the technical discussion, please check out the following:

Info

  • These docs are still work-in-progress.
  • Some topics have been discussed in greater depth, while others require additional clarification.

Status and features

Polygon Miden is currently on release v0.7. This is an early version of the protocol and its components.

Important We expect breaking changes on all components.

At the time of writing, Polygon Miden doesn't offer all the features you may expect from a zkRollup. During 2024, we expect to gradually implement more features.

Feature highlights

Private accounts

The Miden operator only tracks a commitment to account data in the public database. Users can only execute smart contracts when they know the interface.

Private notes

Like private accounts, the Miden operator only tracks a commitment to notes in the public database. Users need to communicate note details to each other off-chain (via a side channel) in order to consume private notes in transactions.

Public accounts

Polygon Miden supports public smart contracts like Ethereum. The code and state of those accounts are visible to the network and anyone can execute transactions against them.

Public notes

As with public accounts, public notes are also supported. That means, the Miden operator publicly stores note data. Note consumption is not private.

Local transaction execution

The Miden client allows for local transaction execution and proving. The Miden operator verifies the proof and, if valid, updates the state DBs with the new data.

Simple smart contracts

Currently, there are three different smart contracts available. A basic wallet smart contract that sends and receives assets, and fungible and non-fungible faucets to mint and burn assets.

All accounts are written in MASM.

P2ID, P2IDR, and SWAP note scripts

Currently, there are three different note scripts available. Two different versions of pay-to-id scripts of which P2IDR is reclaimable, and a swap script that allows for simple token swaps.

Simple block building

The Miden operator running the Miden node builds the blocks containing transactions.

Maintaining state

The Miden node stores all necessary information in its state DBs and provides this information via its RPC endpoints.

Planned features

Warning The following features are at a planning stage only.

Customized smart contracts

Accounts can expose any interface in the future. This is the Miden version of a smart contract. Account code can be arbitrarily complex due to the underlying Turing-complete Miden VM.

Customized note scripts

Users will be able to write their own note scripts using the Miden client. Note scripts are executed during note consumption and they can be arbitrarily complex due to the underlying Turing-complete Miden VM.

Network transactions

Transaction execution and proving can be outsourced to the network and to the Miden operator. Those transactions will be necessary when it comes to public shared state, and they can be useful if the user's device is not powerful enough to prove transactions efficiently.

Rust compiler

In order to write account code, note or transaction scripts, in Rust, there will be a Rust -> Miden Assembly compiler.

Block and epoch proofs

The Miden node will recursively verify transactions and in doing so build batches of transactions, blocks, and epochs.

Benefits of Polygon Miden

  • Ethereum security.
  • Developers can build applications that are infeasible on other systems. For example:
    • on-chain order book exchange due to parallel transaction execution and updatable transactions.
    • complex, incomplete information games due to client-side proving and cheap complex computations.
    • safe wallets due to hidden account state.
  • Better privacy properties than on Ethereum - first web2 privacy, later even stronger self-sovereignty.
  • Transactions can be recalled and updated.
  • Lower fees due to client-side proving.
  • dApps on Miden are safe to use due to account abstraction and compile-time safe Rust smart contracts.

License

Licensed under the MIT license.

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.

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 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.

    mkdir miden-client
    cd miden-client
    
  2. Install the Miden client.

    cargo install miden-cli --features concurrent
    

    You can now use the miden --version command, and you should see Miden 0.7.0.

  3. Initialize the client. This creates the miden-client.toml file.

miden init --network testnet # Creates a miden-client.toml configured with the testnet node's IP

Create a new Miden account

  1. Create a new account of type mutable using the following command:

    miden new-wallet --mutable
    
  2. List all created accounts by running the following command:

    miden account -l
    

    You should see something like this:

    Result of listing miden accounts

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.

  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.

  1. 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.

  2. 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:

    miden import <path-to-note>/note.mno
    
  2. You should see something like this:

    Successfully imported note 0x0ff340133840d35e95e0dc2e62c88ed75ab2e383dc6673ce0341bd486fed8cb6
    
  3. Now that the note has been successfully imported, you can view the note's information using the following command:

    miden notes
    
  4. You should see something like this:

    Result of viewing miden notes

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:

miden sync

You will see something like this as output:

State synced to block 179672
New public notes: 0
Tracked notes updated: 1
Tracked notes consumed: 0
Tracked accounts updated: 0
Commited 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:

    miden notes
    
  2. You should see something like this:

    Viewing commit height info

  3. Find your account and note id by listing both accounts and notes:

    miden account
    miden notes
    
  4. Consume the note and add the funds from its vault to our account using the following command:

    miden consume-notes --account <Account-Id> <Note-Id>
    
  5. You should see a confirmation message like this:

    Transaction confirmation message

  6. After confirming you can view the new note status by running the following command:

    miden notes
    
  7. You should see something like this:

    Viewing process info

  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:

    miden sync
    
  9. After syncing, you should have received confirmation of the consumed note. You should see the note as Consumed after listing the notes:

    miden notes
    

    Viewing consumed note

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

  1. View your updated account's vault containing the tokens sent by the faucet by running the following command:

    miden account --show <Account-Id>
    
  2. You should now see your accounts vault containing the funds sent by the faucet.

    Viewing account vault with funds

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.

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.

In this section, we show you how to make private transactions and send funds to another account using the Miden client.

Important: Prerequisite steps

Create a second account

Tip Remember to use the Miden client documentation 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:

    miden new-wallet --mutable
    
  2. List and view the newly created accounts with the following command:

    miden account -l
    
  3. You should see two accounts:

    Result of listing miden 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:

    miden send --sender <regular-account-id-A> --target <regular-account-id-B> --asset 50::<faucet-account-id> --note-type private
    

    Note The faucet account id can be found on the Miden faucet website 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.

    miden sync
    
  3. Get the second note id.

    miden notes
    
  4. Have the second account consume the note.

    miden consume-notes --account <regular-account-ID-B> <input-note-id>
    

    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:

    miden account --show <regular-account-ID-B>
    

    Result of listing miden accounts

  6. Check the original account:

    miden account --show <regular-account-ID-A>
    

    Result of listing miden accounts

Wanna do more? Sending public notes

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.

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.

In this section, we show you how to execute transactions and send funds to another account using the Miden client and public notes.

Important: Prerequisite steps

Create a second client

Tip Remember to use the Miden client documentation for clarifications.

This is an alternative to the private P2P transactions 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.

    mkdir miden-client-2
    cd miden-client-2
    
  2. Initialize the client. This creates the miden-client.toml file line-by-line.

    miden init --network testnet # Creates a miden-client.toml file configured with the testnet node's IP
    
  3. On the new client, create a new basic account:

    miden 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.

  4. List and view the account with the following command:

    miden 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:

    miden send --sender <basic-account-id-A> --target <basic-account-id-C> --asset 50::<faucet-account-id> --note-type public
    

    Note The faucet account id is 0xad904b3138d71d3e and can also be found on the Miden faucet website 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.

    miden sync
    
  3. At this point, we should have received the public note details.

    miden 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.

    miden consume-notes --account <regular-account-ID-C> <input-note-id>
    

    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:

miden account --show <account-ID> 

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.

Welcome to MkDocs

For full documentation visit mkdocs.org.

Commands

  • mkdocs new [dir-name] - Create a new project.
  • mkdocs serve - Start the live-reloading docs server.
  • mkdocs build - Build the documentation site.
  • mkdocs -h - Print help message and exit.

Project layout

mkdocs.yml    # The configuration file.
docs/
    index.md  # The documentation homepage.
    ...       # Other markdown pages, images and other files.

Overview

Components

The Miden client currently has two main components:

  1. Miden client library.
  2. 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.

Software prerequisites

Install the client

We currently recommend installing and running the client with the concurrent feature.

Run the following command to install the miden-client:

cargo install miden-cli --features concurrent

This installs the miden binary (at ~/.cargo/bin/miden) with the concurrent feature.

Concurrent feature

The concurrent flag enables optimizations that result in faster transaction execution and proving times.

Run the client

  1. Make sure you have already installed the client. If you don't have a miden-client.toml file in your directory, create one or run miden init to initialize one at the current working directory. You can do so without any arguments to use its defaults or define either the RPC endpoint or the store config via --network and --store-path

  2. Run the client CLI using:

    miden
    

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.

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.

The Miden client has the following architectural components:

Important "Customizable"

  • 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.

Currently, these include:

  • GetBlockHeaderByNumber: Returns the block header information given a specific block number.
  • SyncState: Asks the node for information relevant to the client. For example, specific account changes, whether relevant notes have been created or consumed, etc.
  • SubmitProvenTransaction: Sends a locally-proved transaction to the node for inclusion in the blockchain.

Transaction executor

The transaction executor executes transactions using the Miden VM.

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.

To use the Miden client library in a Rust project, include it as a dependency.

In your project's Cargo.toml, add:

miden-client = { version = "0.7" }

Features

The Miden client library supports the concurrent feature which is recommended for developing applications with the client. To use it, add the following to your project's Cargo.toml:

miden-client = { version = "0.7", features = ["concurrent"] }

The library also supports several other features. Please refer to the crate's documentation to learn more.

Client instantiation

Spin up a client using the following Rust code and supplying a store and RPC endpoint.

#![allow(unused)]
fn main() {
let sqlite_store = SqliteStore::new("path/to/store".try_into()?).await?;
let store = Arc::new(sqlite_store);

// Generate a random seed for the RpoRandomCoin.
let mut rng = rand::thread_rng();
let coin_seed: [u64; 4] = rng.gen();

// Initialize the random coin using the generated seed.
let rng = RpoRandomCoin::new(coin_seed.map(Felt::new));

// Create a store authenticator with the store and random coin.
let authenticator = StoreAuthenticator::new_with_rng(store.clone(), rng);

// Instantiate the client using a Tonic RPC client
let endpoint = Endpoint::new("https".into(), "localhost".into(), Some(57291));
let client: Client<RpoRandomCoin> = Client::new(
    Box::new(TonicRpcClient::new(endpoint, 10_000)),
    rng,
    store,
    Arc::new(authenticator),
    false, // Set to true for debug mode, if needed.
);
}

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:

#![allow(unused)]
fn main() {
let key_pair = SecretKey::with_rng(client.rng());
let anchor_block = client.get_latest_epoch_block().await.unwrap();

let (new_account, seed) = AccountBuilder::new(init_seed) // Seed should be random for each account
    .anchor((&anchor_block).try_into().unwrap())
    .account_type(AccountType::RegularAccountImmutableCode)
    .storage_mode(AccountStorageMode::Private)
    .with_component(RpoFalcon512::new(key_pair.public_key()))
    .with_component(BasicWallet)
    .build()?;

client.add_account(&new_account, Some(seed), &AuthSecretKey::RpoFalcon512(key_pair), 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:

let key_pair = SecretKey::with_rng(client.rng());
let anchor_block = client.get_latest_epoch_block().await.unwrap();

let (new_account, seed) = 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_component(RpoFalcon512::new(key_pair.public_key()))
    .with_component(BasicWallet)
    .build()?;

client.add_account(&new_account, Some(seed), &AuthSecretKey::RpoFalcon512(key_pair), 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:

#![allow(unused)]
fn main() {
// 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_transaction = PaymentTransactionData::new(
    vec![fungible_asset.into()],
    sender_account_id,
    target_account_id,
);

let transaction_request = TransactionRequestBuilder::pay_to_id(
    payment_transaction,
    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 execute a transaction by manually defining a TransactionRequest instance. This allows you to run custom code, with custom note arguments as well.

The following document lists the commands that the CLI currently supports.

Note Use --help as a flag on any command for more information.

Usage

Call a command on the miden-client like this:

miden <command> <flags> <arguments>

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:

miden --debug <flags> <arguments>

Note that the debug flag overrides the MIDEN_DEBUG environment variable.

Commands

init

Creates a configuration file for the client in the current directory.

# 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 init

# You can set up the CLI for any of the default networks
miden init --network testnet # This is the default value if no network is provided
miden init --network devnet
miden init --network localhost

# You can use the --network flag to override the default RPC config
miden init --network 18.203.155.106
# You can specify the port
miden init --network 18.203.155.106:8080
# You can also specify the protocol (http/https)
miden init --network https://18.203.155.106
# You can specify both
miden init --network https://18.203.155.106:1234

# You can use the --store_path flag to override the default store config
miden init --store_path db/store.sqlite3

# You can provide both flags
miden init --network 18.203.155.106 --store_path db/store.sqlite3

account

Inspect account details.

Action Flags

FlagsDescriptionShort Flag
--listList all accounts monitored by this client-l
--show <ID>Show details of the account for the specified ID-s
--default <ID>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:

miden account --show 0x8fd4b86a6387f8d8

You can call:

miden account --show 0x8fd4b86

For the --default flag, if <ID> is "none" then the previous default account is cleared. If no <ID> is specified then the default account is shown.

new-wallet

Creates a new wallet account.

This command has three optional flags:

  • --storage-mode <TYPE>: 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-components <TEMPLATE_FILES_LIST>: Allows to pass a list of account component template files which can be added to the account. If the templates contain placeholders, the CLI will prompt the user to enter the required data for instantiating storage appropriately.

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 node.

new-faucet

Creates a new faucet account.

This command has three optional flags:

  • --storage-mode <TYPE>: Used to select the storage mode of the account (private if not specified). It may receive "private" or "public".
  • --non-fungible: Makes the faucet asset non-fungible (it's fungible by default).
  • --extra-components <TEMPLATE_FILES_LIST>: Allows to pass a list of account component template files which can be added to the account. If the templates contain placeholders, the CLI will prompt the user to enter the required data for instantiating storage appropriately.

After creating an account with the new-faucet 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 node.

Examples

# Create a new wallet with default settings (private storage, immutable, no extra components)
miden new-wallet

# Create a new wallet with public storage and a mutable code
miden new-wallet --storage-mode public --mutable

# Create a new wallet that includes extra components from local templates
miden new-wallet --extra-components template1,template2

# Create a fungible faucet 
miden new-faucet --token-symbol TST --decimals 10 --max-supply 100000 --storage-mode private

info

View a summary of the current client state.

notes

View and manage notes.

Action Flags

FlagsDescriptionShort Flag
--list [<filter>]List input notes-l
--show <ID>Show details of the input note for the specified note ID-s

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 <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:

miden notes --show 0x70b7ecba1db44c3aa75e87a3394de95463cc094d7794b706e02a9228342faeb0

You can call:

miden notes --show 0x70b7ec

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

FlagDescriptionAliases
--listList all tags monitored by this client-l
--add <tag>Add a new tag to the list of tags monitored by this client-a
--remove <tag>Remove a tag from the list of tags monitored by this client-r

tx

View transactions.

Action Flags

CommandDescriptionAliases
--listList 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 mint --target <TARGET ACCOUNT ID> --asset <AMOUNT>::<FAUCET ID> --note-type <NOTE_TYPE>

consume-notes

Account ID consumes a list of notes, specified by their Note ID.

Usage: miden consume-notes --account <ACCOUNT ID> [NOTES]

For this command, you can also provide a partial ID instead of the full ID for each note. So instead of

miden consume-notes --account <some-account-id> 0x70b7ecba1db44c3aa75e87a3394de95463cc094d7794b706e02a9228342faeb0 0x80b7ecba1db44c3aa75e87a3394de95463cc094d7794b706e02a9228342faeb0

You can do:

miden consume-notes --account <some-account-id> 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 send --sender <SENDER ACCOUNT ID> --target <TARGET ACCOUNT ID> --asset <AMOUNT>::<FAUCET ID> --note-type <NOTE_TYPE> <RECALL_HEIGHT>

swap

The source account creates a Swap note that offers some asset in exchange for some other asset. When another account consumes that note, it'll receive the offered amount and it'll have the requested amount removed from its assets (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 swap --source <SOURCE ACCOUNT ID> --offered-asset <OFFERED AMOUNT>::<OFFERED FAUCET ID> --requested-asset <REQUESTED AMOUNT>::<REQUESTED FAUCET ID> --note-type <NOTE_TYPE>

Tips

For send and consume-notes, you can omit the --sender and --account flags to use the default account defined in the config. 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

miden send --sender 0x80519a1c5e3680fc --target 0x8fd4b86a6387f8d8 --asset 100::0xa99c5c8764d4e011

You can do:

miden 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:

miden <tx command> ...

TX Summary:

...

Continue with proving and submission? Changes will be irreversible once the proof is finalized on the rollup (Y/N)

This confirmation can be skipped in non-interactive environments by providing the --force flag (miden send --force ...):

Importing and exporting

export

Export input note data to a binary file .

FlagDescriptionAliases
--filename <FILENAME>Desired filename for the binary file.-f
--export-type <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.

After installation, use the client by running the following and adding the relevant commands:

miden

Info "Help" Run miden --help for information on miden commands.

Client Configuration

We configure the client using a TOML file (miden-client.toml).

[rpc]
endpoint = { protocol = "http", host = "localhost", port = 57291 }
timeout_ms = 10000

[store]
database_filepath = "store.sqlite3"

[cli]
default_account_id = "0x012345678"

The TOML file should reside in same the directory from which you run the CLI.

In the configuration file, you will find a section for defining the node's rpc endpoint and timeout and the store's filename database_filepath.

By default, the node is set up to run on localhost:57291.

Note

  • Running the node locally for development is encouraged.
  • However, the endpoint can point to any remote node.

There's an additional optional section used for CLI configuration. It currently contains the default account ID, which is used to execute transactions against it when the account flag is not provided.

By default none is set, but you can set and unset it with:

miden account --default <ACCOUNT_ID> #Sets default account
miden 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:

miden account --default

Environment variables

  • 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 can be used to do so). This variable can be overridden by the --debug CLI flag.

Example "Executing, proving, and submitting transactions to the Miden node" For a complete example on how to run the client and submit transactions to the Miden node, refer to the Getting started documentation.

Example "Miden client API docs" The latest and complete reference for the Miden client API can be found at Miden client docs.rs.

Miden architecture overview

Polygon Miden’s architecture departs considerably from typical blockchain designs to support privacy and parallel transaction execution.

In traditional blockchains, state and transactions must be transparent to be verifiable. This is necessary for block production and execution.

However, user generated zero-knowledge proofs allow state transitions, e.g. transactions, to be verifiable without being transparent.

Miden design goals

  • High throughput: The ability to process a high number of transactions (state changes) over a given time interval.
  • Privacy: The ability to keep data known to one’s self and anonymous while processing and/or storing it.
  • Asset safety: Maintaining a low risk of mistakes or malicious behavior leading to asset loss.

Actor model

The actor model inspires Polygon Miden’s execution model. This is a well-known computational design paradigm in concurrent systems. In the actor model, actors are state machines responsible for maintaining their own state. In the context of Polygon Miden, each account is an actor. Actors communicate with each other by exchanging messages asynchronously. One actor can send a message to another, but it is up to the recipient to apply the requested change to their state.

Polygon Miden’s architecture takes the actor model further and combines it with zero-knowledge proofs. Now, actors not only maintain and update their own state, but they can also prove the validity of their own state transitions to the rest of the network. This ability to independently prove state transitions enables local smart contract execution, private smart contracts, and much more. And it is quite unique in the rollup space. Normally only centralized entities - sequencer or prover - create zero-knowledge proofs, not the users.

Core concepts

Miden uses accounts and notes, both of which hold assets. Accounts consume and produce notes during transactions. Transactions describe the account state changes of single accounts.

Accounts

Accounts can hold assets and define rules how assets can be transferred. Accounts can represent users or autonomous smart contracts. The accounts chapter describes the design of an account, its storage types, and creating an account.

Notes

Notes are messages that accounts send to each other. A note stores assets and a script that defines how the note can be consumed. The note chapter describes the design, the storage types, and the creation of a note.

Assets

Assets can be fungible and non-fungible. They are stored in the owner’s account itself or in a note. The assets chapter describes asset issuance, customization, and storage.

Transactions

Transactions describe the production and consumption of notes by a single account.

Executing a transaction always results in a STARK proof.

The transaction chapter describes the transaction design and implementation, including an in-depth discussion of how transaction execution happens in the transaction kernel program.

Limits

Limits topic describes limits currently enforced in miden-base and miden-node.

Accounts produce and consume notes to communicate

Architecture core concepts

State and execution

The actor-based execution model requires a radically different approach to recording the system's state. Actors and the messages they exchange must be treated as first-class citizens. Polygon Miden addresses this by combining the state models of account-based systems like Ethereum and UTXO-based systems like Bitcoin and Zcash.

Miden's state model captures the individual states of all accounts and notes, and the execution model describes state progress in a sequence of blocks.

State model

State describes everything that is the case at a certain point in time. Individual states of accounts or notes can be stored on-chain and off-chain. This chapter describes the three different state databases in Miden.

Execution model

Execution defines how state progresses as aggregated-state-updates in batches, blocks, and epochs. The execution chapter describes the execution model and how blocks are built.

Operators capture and progress state

Architecture state process

Account

The primary entity of the Miden protocol

What is the purpose of an account?

In Miden's hybrid UTXO and account-based model Accounts enable the creation of expressive smart contracts via a Turing-complete language.

What is an account?

In Miden, an Account represents an entity capable of holding assets, storing data, and executing custom code. Each Account is a specialized smart contract providing a programmable interface for interacting with its state and managed assets.

Account core components

An Account is composed of several core components, illustrated below:

Account diagram

These components are:

  1. ID
  2. Code
  3. Storage
  4. Vault
  5. Nonce

ID

An immutable and unique identifier for the Account.

A 63-bit long number represents the Account ID. It's four most significant bits encode:

This encoding allows the ID to convey both the Account’s unique identity and its operational settings.

Code

A collection of functions defining the Account’s programmable interface.

Every Miden Account is essentially a smart contract. The Code component defines the account’s functions, which can be invoked through both Note scripts and transaction scripts. Key characteristics include:

  • Mutable access: Only the Account’s own functions can modify its storage and vault. All state changes—such as updating storage slots, incrementing the nonce, or transferring assets—must occur through these functions.
  • Function commitment: Each function can be called by its MAST root. The root represents the underlying code tree as a 32-byte hash. This ensures integrity, i.e., the caller calls what he expects.
  • Note creation: Account functions can generate new notes.

Storage

A flexible, arbitrary data store within the Account.

The storage is divided into a maximum of 255 indexed storage slots. Each slot can either store a 32-byte value or serve as a pointer to a key-value store with large amounts capacity.

  • StorageSlot::Value: Contains 32 bytes of arbitrary data.
  • StorageSlot::Map: Contains a StorageMap, a key-value store where both keys and values are 32 bytes. The slot's value is a commitment (hash) to the entire map.

Vault

A collection of assets stored by the Account.

Large amounts of fungible and non-fungible assets can be stored in the Accounts vault.

Nonce

A counter incremented with each state update to the Account.

The nonce enforces ordering and prevents replay attacks. It must strictly increase with every Account state update. The increment must be less than 2^32 but always greater than the previous nonce, ensuring a well-defined sequence of state changes.

Account lifecycle

Throughout its lifetime, an Account progresses through various phases:

  • Creation and Deployment: Initialization of the Account on the network.
  • Active Operation: Continuous state updates via Account functions that modify the storage, nonce, and vault.
  • Termination or Deactivation: Optional, depending on the contract’s design and governance model.

Account creation

For an Account to be recognized by the network, it must exist in the account database maintained by Miden node(s).

However, a user can locally create a new Account ID before it’s recognized network-wide. The typical process might be:

  1. Alice generates a new Account ID locally (according to the desired Account type) using the Miden client.
  2. The Miden client checks with a Miden node to ensure the ID does not already exist.
  3. Alice shares the new ID with Bob (for example, to receive assets).
  4. Bob executes a transaction, creating a note containing assets for Alice.
  5. Alice consumes Bob’s note in her own transaction to claim the asset.
  6. Depending on the Account’s storage mode and transaction type, the operator receives the new Account ID and, if all conditions are met, includes it in the Account database.

Additional information

Account type

There are two main categories of Accounts in Miden: basic accounts and faucets.

  • Basic Accounts:
    Basic Accounts may be either mutable or immutable:

    • Mutable: Code can be changed after deployment.
    • Immutable: Code cannot be changed once deployed.
  • Faucets:
    Faucets are always immutable and can be specialized by the type of assets they issue:

    • Fungible Faucet: Can issue fungible assets.
    • Non-fungible Faucet: Can issue non-fungible assets.

Type and mutability are encoded in the two most significant bits of the Account's ID.

Account storage mode

Users can choose whether their Accounts are stored publicly or privately. The preference is encoded in the third and forth most significant bits of the Accounts ID:

  • Public Accounts:
    The Account’s state is stored on-chain, similar to how Accounts are stored in public blockchains like Ethereum. Contracts that rely on a shared, publicly accessible state (e.g., a DEX) should be public.

  • Private Accounts:
    Only a commitment (hash) to the Account’s state is stored on-chain. This mode is suitable for users who prioritize privacy or plan to store a large amount of data in their Account. To interact with a private Account, a user must have knowledge of its interface.

The storage mode is chosen during Account creation, it cannot be changed later.

Conclusion

You are now better equipped to understand how a Miden Account operates, how it manages data and assets, and how its programmable interface enables secure and flexible interactions within the Miden protocol.

Note

The medium through which Accounts communicate in the Miden protocol.

What is the purpose of a note?

In Miden's hybrid UTXO and account-based model Notes represent UTXO's which enable parallel transaction execution and privacy through asynchronous local Note production and consumption.

What is a note?

A Note in Miden holds assets and defines how these assets can be consumed.

Note core components

A Note is composed of several core components, illustrated below:

Note diagram

These components are:

  1. Assets
  2. Script
  3. Inputs
  4. Serial number
  5. Metadata

Assets

An asset container for a Note.

A Note can contain up to 256 different assets. These assets represent fungible or non-fungible tokens, enabling flexible asset transfers.

Script

The code executed when the Note is consumed.

Each Note has a script that defines the conditions under which it can be consumed. When accounts consume Notes in transactions, Note scripts call the account’s interface functions. This enables all sorts of operations beyond simple asset transfers. The Miden VM’s Turing completeness allows for arbitrary logic, making Note scripts highly versatile.

Inputs

Arguments passed to the Note script during execution.

A Note can have up to 128 input values, which adds up to a maximum of 1 KB of data. The Note script can access these inputs. They can convey arbitrary parameters for Note consumption.

Serial number

A unique and immutable identifier for the Note.

The serial number has two main purposes. Firstly by adding some randomness to the Note it ensures it's uniqueness, secondly in private Notes it helps prevent linkability between the Note's hash and its nullifier. The serial number should be a random 32 bytes number chosen by the user. If leaked, the Note’s nullifier can be easily computed, potentially compromising privacy.

Metadata

Additional Note information.

Notes include metadata such as the sender’s account ID and a tag that aids in discovery. Regardless of storage mode, these metadata fields remain public.

Note Lifecycle

Architecture core concepts

The Note lifecycle proceeds through four primary phases: creation, validation, discovery, and consumption. Throughout this process, Notes function as secure, privacy-preserving vehicles for asset transfers and logic execution.

Note creation

Accounts can create Notes in a transaction. The Note exists if it is included in the global Notes DB.

  • Users: Executing local or network transactions.
  • Miden operators: Facilitating on-chain actions, e.g. such as executing user Notes against a DEX or other contracts.

Note storage mode

As with accounts, Notes can be stored either publicly or privately:

  • Public mode: The Note data is stored in the note database, making it fully visible on-chain.
  • Private mode: Only the Note’s hash is stored publicly. The Note’s actual data remains off-chain, enhancing privacy.

Ephemeral note

These specific Notes can be consumed even if not yet registered on-chain. They can be chained together into one final proof. This can allow for example sub-second communication below blocktimes by adding additional trust assumptions.

Note validation

Once created, a Note must be validated by a Miden operator. Validation involves checking the transaction proof that produced the Note to ensure it meets all protocol requirements.

  • Private Notes: Only the Note’s hash is recorded on-chain, keeping the data confidential.
  • Public Notes: The full Note data is stored, providing transparency for applications requiring public state visibility.

After validation, Notes become “live” and eligible for discovery and eventual consumption.

Note discovery

Clients often need to find specific Notes of interest. Miden allows clients to query the Note database using Note tags. These lightweight, 32-bit data fields serve as best-effort filters, enabling quick lookups for Notes related to particular use cases, scripts, or account prefixes.

Using Note tags strikes a balance between privacy and efficiency. Without tags, querying a specific Note ID reveals a user’s interest to the operator. Conversely, downloading and filtering all registered Notes locally is highly inefficient. Tags allow users to adjust their level of privacy by choosing how broadly or narrowly they define their search criteria, letting them find the right balance between revealing too much information and incurring excessive computational overhead.

Note consumption

To consume a Note, the consumer must know its data, including the inputs needed to compute the nullifier. Consumption occurs as part of a transaction. Upon successful consumption a nullifier is generated for the consumed Notes.

Upon successful verification of the transaction:

  1. The Miden operator records the Note’s nullifier as “consumed” in the nullifier database.
  2. The Note’s one-time claim is thus extinguished, preventing reuse.

Note recipient restricting consumption

Consumption of a Note can be restricted to certain accounts or entities. For instance, the P2ID and P2IDR Note scripts target a specific account ID. Alternatively, Miden defines a RECIPIENT (represented as 32 bytes) computed as:

hash(hash(hash(serial_num, [0; 4]), script_hash), input_hash)

Only those who know the RECIPIENT’s pre-image can consume the Note. For private Notes, this ensures an additional layer of control and privacy, as only parties with the correct data can claim the Note.

The transaction prologue requires all necessary data to compute the Note hash. This setup allows scenario-specific restrictions on who may consume a Note.

For a practical example, refer to the SWAP note script, where the RECIPIENT ensures that only a defined target can consume the swapped asset.

Note nullifier ensuring private consumption

The Note nullifier, computed as:

hash(serial_num, script_hash, input_hash, vault_hash)

This achieves the following properties:

  • Every Note can be reduced to a single unique nullifier.
  • One cannot derive a Note's hash from its nullifier.
  • To compute the nullifier, one must know all components of the Note: serial_num, script_hash, input_hash, and vault_hash.

That means if a Note is private and the operator stores only the Note's hash, only those with the Note details know if this Note has been consumed already. Zcash first introduced this approach.

Architecture core concepts

Conclusion

Miden’s Note introduce a powerful mechanism for secure, flexible, and private state management. By enabling asynchronous asset transfers, parallel execution, and privacy at scale, Notes transcend the limitations of strictly account-based models. As a result, developers and users alike enjoy enhanced scalability, confidentiality, and control. With these capabilities, Miden is paving the way for true programmable money where assets, logic, and trust converge seamlessly.

Asset

Fungible and Non-fungible assets in the Miden protocol.

What is the purpose of an asset?

In Miden, Assets serve as the primary means of expressing and transferring value between accounts through notes. They are designed with four key principles in mind:

  1. Parallelizable exchange:
    By managing ownership and transfers directly at the account level instead of relying on global structures like ERC20 contracts, accounts can exchange Assets concurrently, boosting scalability and efficiency.

  2. Self-sovereign ownership:
    Assets are stored in the accounts directly. This ensures that users retain complete control over their Assets.

  3. Censorship resistance:
    Users can transact freely and privately with no single contract or entity controlling Asset transfers. This reduces the risk of censored transactions, resulting in a more open and resilient system.

  4. Flexible fee payment:
    Unlike protocols that require a specific base Asset for fees, Miden allows users to pay fees in any supported Asset. This flexibility simplifies the user experience.

What is an asset?

An Asset in Miden is a unit of value that can be transferred from one account to another using notes.

Native asset

All data structures following the Miden asset model that can be exchanged.

Native Assets adhere to the Miden Asset model (encoding, issuance, storage). Every native Asset is encoded using 32 bytes, including both the ID of the issuing account and the Asset details.

Issuance

Info

  • Only faucet accounts can issue assets.

Faucets can issue either fungible or non-fungible Assets as defined at account creation. The faucet's code specifies the Asset minting conditions: i.e., how, when, and by whom these Assets can be minted. Once minted, they can be transferred to other accounts using notes.

Architecture core concepts

Type

Fungible asset

Fungible Assets are encoded with the amount and the faucet_id of the issuing faucet. The amount is always 2^{63} - 1 or smaller, representing the maximum supply for any fungible Asset. Examples include ETH and various stablecoins (e.g., DAI, USDT, USDC).

Non-fungible asset

Non-fungible Assets are encoded by hashing the Asset data into 32 bytes and placing the faucet_id as the second element. Examples include NFTs like a DevCon ticket.

Storage

Accounts and notes have vaults used to store Assets. Accounts use a sparse Merkle tree as a vault while notes use a simple list. This enables an account to store a practically unlimited number of assets while a note can only store 255 assets.

Architecture core concepts

Burning

Assets in Miden can be burned through various methods, such as rendering them unspendable by storing them in an unconsumable note, or sending them back to their original faucet for burning using it's dedicated function.

Alternative asset models

All data structures not following the Miden asset model that can be exchanged.

Miden is flexible enough to support other Asset models. For example, developers can replicate Ethereum’s ERC20 pattern, where fungible Asset ownership is recorded in a single account. To transact, users send a note to that account, triggering updates in the global hashmap state.

Conclusion

Miden’s Asset model provides a secure, flexible, scalable, and privacy-preserving framework for representing and transferring value. By embedding Asset information directly into accounts and supporting multiple Asset types, Miden fosters a decentralized ecosystem where users maintain their privacy, control, transactions can scale efficiently, and censorship is minimized.

Transactions overview

Architecture overview

The Miden transaction architecture comprises a set of components that interact with each other. This section of the documentation discusses each component.

The diagram shows the components responsible for Miden transactions and how they fit together.

Transactions architecture overview

Tip

Miden transactions

Transactions in Miden facilitate single account state changes. Miden requires two transactions to transfer assets between accounts.

A transaction takes a single account and some notes as input, and outputs the same account with a new state, together with some other notes.

Miden aims for the following:

  • Parallel transaction execution: Because a transaction is always performed against a single account, Miden obtains asynchronicity.
  • Private transaction execution: Because every transaction emits a state-change with a STARK proof, there is privacy when the transaction executes locally.

There are two types of transactions in Miden: local transactions and network transactions.

Transaction design

Transactions describe the state-transition of a single account that takes chain data and 0 to 1024 notes as input and produces a TransactionWitness and 0 to 1024 notes as output.

Transaction diagram{ width="75%" }

At its core, a transaction is an executable program—the transaction kernel program—that processes the provided inputs and creates the requested outputs. Because the program is executed by the Miden VM, a STARK-proof is generated for every transaction.

Asset transfer using two transactions

Transferring assets between accounts requires two transactions as shown in the diagram below.

Transaction flow

The first transaction invokes some functions on account_a (e.g. create_note and move_asset_to_note functions) which creates a new note and also updates the internal state of account_a. The second transaction consumes the note which invokes a function on account_b (e.g. a receive_asset function) which updates the internal state of account_b.

Asynchronous execution

Both transactions can be executed asynchronously: first transaction1 is executed, and then, some time later, transaction2 is executed.

This opens up a few interesting possibilities:

  • The owner of account_b may wait until they receive many notes and process them all in a single transaction.
  • A note script may include a clause which allows the source account to consume the note after some time. Thus, if account_b does not consume the note after the specified time, the funds can be returned. This mechanism can be used to make sure funds sent to non-existent accounts are not lost (see the P2IDR note script).
  • Neither the sender nor the recipient needs to know who the other side is. From the sender's perspective, they just need to create note1 (and for this they need to know the assets to be transferred and the root of the note's script). They don't need any information on who will eventually consume the note. From the recipient's perspective, they just need to consume note1. They don't need to know who created it.
  • Both transactions can be executed "locally". For example, we could generate a zk-proof that transaction1 was executed and submit it to the network. The network can verify the proof without the need for executing the transaction itself. The same can be done for transaction2. Moreover, we can mix and match. For example, transaction1 can be executed locally, but transaction2 can be executed on the network, or vice versa.

Local and network transactions

Local vs network transactions

Local transactions

This is where clients executing the transactions also generate the proofs of their correct execution. So, no additional work needs to be performed by the network.

Local transactions are useful for several reasons:

  1. They are cheaper (i.e., lower fees) as zk-proofs are already generated by the clients.
  2. They allow fairly complex computations because the proof size doesn't grow linearly with the complexity of the computation.
  3. They enable privacy as neither the account state nor account code are needed to verify the zk-proof.

Network transactions

This is where the operator executes the transaction and generates the proofs.

Network transactions are useful for two reasons:

  1. Clients may not have sufficient resources to generate zk-proofs.
  2. Executing many transactions against the same public account by different clients is challenging, as the account state changes after every transaction. Due to this, the Miden node/operator acts as a "synchronizer" to execute transactions sequentially by feeding the output of the previous transaction into the input of the next.

The Miden transaction executor is the component that executes transactions.

Transaction execution consists of the following steps and results in an ExecutedTransaction object:

  1. Fetch the data required to execute a transaction from the data store.
  2. Compile the transaction into an executable MASM program using the transaction compiler.
  3. Execute the transaction program and create an ExecutedTransaction object.
  4. Prove the ExecutedTransaction using the transaction prover.

Transaction execution process

One of the main reasons for separating out the execution and proving steps is to allow stateless provers; i.e., the executed transaction has all the data it needs to re-execute and prove a transaction without database access. This supports easier proof-generation distribution.

Data store and transaction inputs

The data store defines the interface that transaction objects use to fetch the data for transaction executions. Specifically, it provides the following inputs to the transaction:

  • Account data which includes the AccountID and the AccountCode that is executed during the transaction.
  • A BlockHeader which contains metadata about the block, commitments to the current state of the chain, and the hash of the proof that attests to the integrity of the chain.
  • A ChainMmr which authenticates input notes during transaction execution. Authentication is achieved by providing an inclusion proof for the transaction's input notes against the ChainMmr-root associated with the latest block known at the time of transaction execution.
  • InputNotes consumed by the transaction that include the corresponding note data, e.g., the note script and serial number.

Note

  • InputNotes must be already recorded on-chain in order for the transaction to succeed.
  • There is no nullifier-check during a transaction. Nullifiers are checked by the Miden operator during transaction verification. So at the transaction level, there is "double spending."

Transaction compiler

Every transaction is executed within the Miden VM to generate a transaction proof. In Miden, there is a proof for every transaction.

The transaction compiler is responsible for building executable programs. The generated MASM programs can then be executed by the Miden VM which generates a zk-proof. In addition to transaction compilation, the transaction compiler provides methods for compiling Miden account code, note scripts, and transaction scripts.

Compilation results in an executable MASM program. The program includes the provided account interface and notes, an optional transaction script, and the transaction kernel program. The transaction kernel program defines procedures and the memory layout for all parts of the transaction.

After compilation, assuming correctly-populated inputs, including the advice provider, the transaction can be executed.

Executed transactions and the transaction outputs

The ExecutedTransaction object represents the result of a transaction, not its proof. From this object, the account and storage delta can be extracted. Furthermore, the ExecutedTransaction is an input to the transaction prover.

A successfully executed transaction results in a new account state which is a vector of all created notes (OutputNotes) and a vector of all the consumed notes (InputNotes) together with their nullifiers.

Transaction prover

The transaction prover proves the inputted ExecutedTransaction and returns a ProvenTransaction object. The Miden node verifies the ProvenTransaction object using the transaction verifier and, if valid, updates the state databases.

Transaction Kernel Program

The transaction kernel program, written in MASM, is responsible for executing a Miden rollup transaction within the Miden VM. It is defined as a MASM kernel.

The kernel provides context-sensitive security, preventing unwanted read and write access. It defines a set of procedures which can be invoked from other contexts; e.g., notes executed in the root context.

In general, the kernel's procedures must reflect everything users might want to do while executing transactions—from transferring assets to complex smart contract interactions with custom code.

Info

The kernel has a well-defined structure which does the following:

  1. The prologue prepares the transaction for processing by parsing the transaction data and setting up the root context.
  2. Note processing executes the note processing loop which consumes each InputNote and invokes the note script of each note.
  3. Transaction script processing executes the optional transaction script.
  4. The epilogue finalizes the transaction by computing the output notes commitment, the final account hash, asserting asset invariant conditions, and asserting the nonce rules are upheld.

Transaction program

Input

The transaction kernel program receives two types of inputs: public inputs via the operand_stack and private inputs via the advice_provider.

  • Operand stack: Holds the global inputs which serve as a commitment to the data being provided via the advice provider.
  • Advice provider: Holds data of the last known block, account, and input note data.

Prologue

The transaction prologue executes at the beginning of a transaction. It performs the following tasks:

  1. Unhashes the inputs and lays them out in the root context memory.
  2. Builds a single vault (transaction vault) containing assets of all inputs (input notes and initial account state).
  3. Verifies that all input notes are present in the note DB.

The memory layout is illustrated below. The kernel context has access to all memory slots.

Memory layout kernel

Bookkeeping section

Tracks variables used internally by the transaction kernel.

Global inputs

Stored in pre-defined memory slots. Global inputs include the block hash, account ID, initial account hash, and nullifier commitment.

Block data

Block data, read from the advice provider, is stored in memory. The block hash is computed and verified against the global inputs.

Chain data

Chain root is recomputed and verified against the chain root in the block data section.

Account data

Reads data from the advice provider, stores it in memory, and computes the account hash. The hash is validated against global inputs. For new accounts, initial account hash and validation steps are applied.

Input note data

Processes input notes by reading data from advice providers and storing it in memory. It computes the note's hash and nullifier, forming a transaction nullifier commitment.

Info

  • Note data is required for computing the nullifier (e.g., the note script and serial number).
  • Note recipients define the set of users who can consume specific notes.

Note Processing

Notes are consumed in a loop, invoking their scripts in isolated contexts using dyncall.

# loop while we have notes to consume
while.true
    exec.note::prepare_note
    dyncall
    dropw dropw dropw dropw
    exec.note::increment_current_input_note_ptr
    loc_load.0
    neq
end

When processing a note, new note creation may be triggered, and information about the new note is stored in the output note data.

Info

  • Notes can only call account interfaces to trigger write operations, preventing direct access to account storage.

Transaction Script Processing

If provided, the transaction script is executed after all notes are consumed. The script may authenticate the transaction by increasing the account nonce and signing the transaction.

use.miden::contracts::auth::basic->auth_tx

begin
    padw padw padw padw
    call.auth_tx::auth_tx_rpo_falcon512
    dropw dropw dropw dropw
end

Note

  • The account must expose the auth_tx_rpo_falcon512 function for the transaction script to call it.

Epilogue

Finalizes the transaction:

  1. Computes the final account hash.
  2. Asserts that the final account nonce is greater than the initial nonce if the account has changed.
  3. Computes the output notes commitment.
  4. Asserts that input and output vault roots are equal (except for special accounts like faucets).

Outputs

The transaction kernel program outputs:

  1. The transaction script root.
  2. A commitment of all newly created output notes.
  3. The account hash in its new state.

Context overview

Miden assembly program execution, the code the transaction kernel runs, spans multiple isolated contexts. An execution context defines its own memory space which is inaccessible from other execution contexts. Note scripts cannot directly write to account data, which should only be possible if the account exposes relevant functions.

Specific contexts

The kernel program always starts executing from a root context. Thus, the prologue sets the memory for the root context. To move execution into a different context, the kernel invokes a procedure using the call or dyncall instruction. In fact, any time the kernel invokes a procedure using the call instruction, it executes in a new context.

While executing in a note, account, or transaction (tx) script context, the kernel executes some procedures in the kernel context, where all necessary information is stored during the prologue. The kernel switches context via the syscall instruction. The set of procedures invoked via the syscall instruction is limited by the transaction kernel API. When the procedure called via syscall returns, execution moves back to the note, account, or tx script where it was invoked.

Context switches

Transaction contexts

The above diagram shows different context switches in a simple transaction. In this example, an account consumes a P2ID note and receives the asset into its vault. As with any MASM program, the transaction kernel program starts in the root context. It executes the prologue and stores all necessary information into the root memory.

The next step, note processing, starts with a dyncall which invokes the note script. This command moves execution into a different context (1). In this new context, the note has no access to the kernel memory. After a successful ID check, which changes back to the kernel context twice to get the note inputs and the account ID, the script executes the add_note_assets_to_account procedure.

# Pay-to-ID script: adds all assets from the note to the account, assuming ID of the account
# matches target account ID specified by the note inputs.
# ...
begin

    ... <check correct ID>

    exec.add_note_assets_to_account
    # => [...]
end

The procedure cannot simply add assets to the account, because it is executed in a note context. Therefore, it needs to call the account interface. This moves execution into a second context - account context - isolated from the note context (2).

#! Helper procedure to add all assets of a note to an account.
#! ...
proc.add_note_assets_to_account
    ...

    while.true
        ...

        # load the asset
        mem_loadw
        # => [ASSET, ptr, end_ptr, ...]
        
        # pad the stack before call
        padw swapw padw padw swapdw
        # => [ASSET, pad(12), ptr, end_ptr, ...]

        # add asset to the account
        call.wallet::receive_asset
        # => [pad(16), ptr, end_ptr, ...]

        # clean the stack after call
        dropw dropw dropw
        # => [0, 0, 0, 0, ptr, end_ptr, ...]
        ...
    end
    ...
end

The wallet smart contract provides an interface that accounts use to receive and send assets. In this new context, the wallet calls the add_asset procedure of the account API.

export.receive_asset
    exec.account::add_asset
    ...
end

The account API exposes procedures to manage accounts. This particular procedure, called by the wallet, invokes a syscall to return back to the root context (3), where the account vault is stored in memory (see prologue). Procedures defined in the Kernel API should be invoked with syscall using the corresponding procedure offset and the exec_kernel_proc kernel procedure.

#! Add the specified asset to the vault.
#! ...
export.add_asset
    exec.kernel_proc_offsets::account_add_asset_offset
    syscall.exec_kernel_proc
end

Now, the asset can be safely added to the vault within the kernel context, and the note can be successfully processed.

There are user-facing procedures and kernel procedures. Users don't directly invoke kernel procedures, but instead they invoke them indirectly via account code, note, or transaction scripts. In these cases, kernel procedures are invoked by a syscall instruction which always executes in the kernel context.

User-facing procedures (APIs)

These procedures can be used to create smart contract/account code, note scripts, or account scripts. They basically serve as an API for the underlying kernel procedures. If a procedure can be called in the current context, an exec is sufficient. Otherwise the context procedures must be invoked by call. Users never need to invoke syscall procedures themselves.

Tip If capitalized, a variable representing a word, e.g., ACCT_HASH consists of four felts. If lowercase, the variable is represented by a single felt.

Account

To import the account procedures, set use.miden::account at the beginning of the file.

Any procedure that changes the account state must be invoked in the account context and not by note or transaction scripts. All procedures invoke syscall to the kernel API and some are restricted by the kernel procedure exec.authenticate_account_origin, which fails if the parent context is not the executing account.

Procedure nameStackOutputContextDescription
get_id[][acct_id]account, note
  • Returns the account id.
get_nonce[][nonce]account, note
  • Returns the account nonce.
get_initial_hash[][H]account, note
  • Returns the initial account hash.
get_current_hash[][ACCT_HASH]account, note
  • Computes and returns the account hash from account data stored in memory.
incr_nonce[value][]account
  • Increments the account nonce by the provided value which can be at most 2^32 - 1 otherwise the procedure panics.
get_item[index][VALUE]account, note
  • Gets an item VALUE by index from the account storage.
  • Panics if the index is out of bounds.
set_item[index, V'][R', V]account
  • Sets an index/value pair in the account storage.
  • Panics if the index is out of bounds. R is the new storage commitment.
set_code[CODE_COMMITMENT][]account
  • Sets the code (CODE_COMMITMENT) of the account the transaction is being executed against.
  • This procedure can only be executed on regular accounts with updatable code. Otherwise, the procedure fails.
get_balance[faucet_id][balance]account, note
  • Returns the balance of a fungible asset associated with a faucet_id.
  • Panics if the asset is not a fungible asset.
has_non_fungible_asset[ASSET][has_asset]account, note
  • Returns a boolean has_asset indicating whether the non-fungible asset is present in the vault.
  • Panics if the ASSET is a fungible asset.
add_asset[ASSET][ASSET']account
  • Adds the specified asset ASSET to the vault. Panics under various conditions.
  • 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.
remove_asset[ASSET][ASSET]account
  • Removes the specified ASSET from the vault.
  • Panics under various conditions.
get_vault_commitment[][COM]account, note
  • Returns a commitment COM to the account vault.

Note

To import the note procedures, set use.miden::note at the beginning of the file. All procedures are restricted to the note context.

Procedure nameInputsOutputsContextDescription
get_assets[dest_ptr][num_assets, dest_ptr]note
  • Writes the assets of the currently executing note into memory starting at the specified address dest_ptr .
  • num_assets is the number of assets in the currently executing note.
get_inputs[dest_ptr][dest_ptr]note
  • Writes the inputs of the currently executed note into memory starting at the specified address, dest_ptr.
get_sender[][sender]note
  • Returns the sender of the note currently being processed. Panics if a note is not being processed.
compute_inputs_hash[inputs_ptr, num_inputs][HASH]note
  • Computes hash of note inputs starting at the specified memory address.
get_note_serial_number[][SERIAL_NUMBER]note
  • Returns the serial number of the note currently being processed.
get_script_hash[][SCRIPT_HASH]note
  • Returns the script hash of the note currently being processed.

Tx

To import the transaction procedures set use.miden::tx at the beginning of the file. Only the create_note procedure is restricted to the account context.

Procedure nameInputsOutputsContextDescription
get_block_number[][num]account, note
  • Returns the block number num of the last known block at the time of transaction execution.
get_block_hash[][H]account, note
  • Returns the block hash H of the last known block at the time of transaction execution.
get_input_notes_commitment[][COM]account, note
  • Returns the input notes hash COM.
  • This is computed as a sequential hash of (nullifier, empty_word_or_note_hash) tuples over all input notes. The empty_word_or_notes_hash functions as a flag, if the value is set to zero, then the notes are authenticated by the transaction kernel. If the value is non-zero, then note authentication will be delayed to the batch/block kernel. The delayed authentication allows a transaction to consume a public note that is not yet included to a block.
get_output_notes_commitment[0, 0, 0, 0][COM]account, note
  • Returns the output notes hash COM.
  • This is computed as a sequential hash of (note_id, note_metadata) tuples over all output notes.
create_note[ASSET, tag, RECIPIENT][ptr]account
  • Creates a new note and returns a pointer to the memory address at which the note is stored.
  • ASSET is the asset to be included in the note.
  • tag is the tag to be included in the note. RECIPIENT is the recipient of the note.
  • ptr is the pointer to the memory address at which the note is stored.

Asset

To import the asset procedures set use.miden::asset at the beginning of the file. These procedures can only be called by faucet accounts.

Procedure nameStackOutputContextDescription
build_fungible_asset[faucet_id, amount][ASSET]faucet
  • Builds a fungible asset ASSET for the specified fungible faucet faucet_id, and amount of asset to create.
create_fungible_asset[amount][ASSET]faucet
  • Creates a fungible asset ASSET for the faucet the transaction is being executed against and amount of the asset to create.
build_non_fungible_asset[faucet_id, DATA_HASH][ASSET]faucet
  • Builds a non-fungible asset ASSET for the specified non-fungible faucet.
  • faucet_id is the faucet to create the asset for.
  • DATA_HASH is the data hash of the non-fungible asset to build.
create_non_fungible_asset[DATA_HASH][ASSET]faucet
  • Creates a non-fungible asset ASSET for the faucet the transaction is being executed against.
  • DATA_HASH is the data hash of the non-fungible asset to create.

Faucet

To import the faucet procedures, set use.miden::faucet at the beginning of the file.

Procedure nameStackOutputsContextDescription
mint[ASSET][ASSET]faucet
  • Mint an asset ASSET from the faucet the transaction is being executed against.
  • Panics under various conditions.
burn[ASSET][ASSET]faucet
  • Burn an asset ASSET from the faucet the transaction is being executed against.
  • Panics under various conditions.
get_total_issuance[][total_issuance]faucet
  • Returns the total_issuance of the fungible faucet the transaction is being executed against.
  • Panics if the transaction is not being executed against a fungible faucet.

State

The snapshot of all accounts, notes, nullifiers and their statuses in Miden, reflecting the “current reality” of the protocol at any given time.

What is the purpose of the Miden state model?

By employing a concurrent State model with local execution and proving, Miden achieves three primary properties: preserving privacy, supporting parallel transactions, and reducing state-bloat by minimizing on-chain data storage.

Miden’s State model focuses on:

  • Concurrency:
    Multiple transactions can be processed concurrently by distinct actors using local transaction execution which improves throughput and efficiency.

  • Flexible data storage:
    Users can store data privately on their own devices or within the network. This approach reduces reliance on the network for data availability, helps maintain user sovereignty, and minimizes unnecessary on-chain storage.

  • Privacy:
    By using notes and nullifiers, Miden ensures that value transfers remain confidential. Zero-knowledge proofs allow users to prove correctness without revealing sensitive information.

What is state?

The State of the Miden rollup describes the current condition of all accounts and notes in the protocol; i.e., the current reality.

State model components

The Miden node maintains three databases to describe State:

  1. Accounts
  2. Notes
  3. Nullifiers

Architecture core concepts

Account database

The accounts database has two main purposes:

  1. Track state commitments of all accounts
  2. Store account data for public accounts

This is done using an authenticated data structure, a sparse Merkle tree.

Architecture core concepts

As described in the accounts section, there are two types of accounts:

  • Public accounts: where all account data is stored on-chain.
  • Private accounts: where only the commitments to the account is stored on-chain.

Private accounts significantly reduce storage overhead. A private account contributes only 40 bytes to the global State (15 bytes for the account ID + 32 bytes for the account commitment + 4 bytes for the block number). For example, 1 billion private accounts take up only 47.47 GB of State.

The storage contribution of a public account depends on the amount of data it stores.

Warning

  • In Miden, when the user is the custodian of their account State (in the case of a private account), losing this State amounts to losing their funds, similar to losing a private key.

Note database

As described in the notes section, there are two types of notes:

  • Public notes: where the entire note content is stored on-chain.
  • Private notes: where only the note’s commitment is stored on-chain.

Private notes greatly reduce storage requirements and thus result in lower fees. At high throughput (e.g., 1K TPS), the note database could grow by about 1TB/year. However, only unconsumed public notes and enough information to construct membership proofs must be stored explicitly. Private notes, as well as consumed public notes, can be discarded. This solves the issue of infinitely growing note databases.

Notes are recorded in an append-only accumulator, a Merkle Mountain Range.

Using a Merkle Mountain Range (append-only accumulator) is important for two reasons:

  1. Membership witnesses (that a note exists in the database) against such an accumulator needs to be updated very infrequently.
  2. Old membership witnesses can be extended to a new accumulator value, but this extension does not need to be done by the original witness holder.

Both of these properties are needed for supporting local transactions using client-side proofs and privacy. In an append-only data structure, witness data does not become stale when the data structure is updated. That means users can generate valid proofs even if they don’t have the latest State of this database; so there is no need to query the operator on a constantly changing State.

Architecture core concepts

Nullifier database

Each note has an associated nullifier which enables the tracking of whether it's associated note has been consumed or not, preventing double-spending.

To prove that a note has not been consumed, the operator must provide a Merkle path to the corresponding node and show that the node’s value is 0. Since nullifiers are 32 bytes each, the sparse Merkle tree height must be sufficient to represent all possible nullifiers. Operators must maintain the entire nullifier set to compute the new tree root after inserting new nullifiers. For each nullifier we also record the block in which it was created. This way "unconsumed" nullifiers have block 0, but all consumed nullifiers have a non-zero block.

Note

  • Nullifiers in Miden break linkability between privately stored notes and their consumption details. To know the note’s nullifier, one must know the note’s data.

Architecture core concepts

Additional information

Public shared state

In most blockchains, most smart contracts and decentralized applications (e.g., AAVE, Uniswap) need public shared State. Public shared State is also available on Miden and can be represented as in the following example:

Public shared state

In this diagram, multiple participants interact with a common, publicly accessible State. The figure illustrates how notes are created and consumed:

  1. Independent Transactions Creating Notes (tx1 & tx2):
    Two separate users (Acc1 and Acc2) execute transactions independently:

    • tx1 produces note 1
    • tx2 produces note 2

    These transactions occur in parallel and do not rely on each other, allowing concurrent processing without contention.

  2. Sequencing and Consuming Notes (tx3):
    The Miden node executes tx3 against the shared account, consuming notes 1 & 2 and producing notes 3 & 4. tx3 is a network transaction executed by the Miden operator. It merges independent contributions into a unified State update.

  3. Further Independent Transactions (tx4 & tx5):
    After the shared State is updated:

    • tx4 consumes note 4
    • tx5 consumes note 5

    Both users can now interact with notes generated by the public account, continuing the cycle of State evolution.

State bloat minimization

Miden nodes do not need to know the entire State to verify or produce new blocks. Rather than storing the full State data with the nodes, users keep their data locally, and the rollup stores only commitments to that data. While some contracts must remain publicly visible, this approach minimizes State bloat. Furthermore the Miden rollup can discard non-required data after certain conditions have been met.

This ensures that the account and note databases remain manageable, even under sustained high usage.

Conclusion

Miden’s State model lays the foundation for a scalable, privacy-preserving, and user-centric environment. By combining parallelizable execution, flexible data storage, and Zero-knowledge proofs that ensure integrity and confidentiality, Miden addresses many of the challenges of traditional blockchains. As a result, the network can handle high throughput, maintain manageable State sizes, and support a wide range of applications.

Polygon Miden is an Ethereum Rollup. It batches transactions - or more precisely, proofs - that occur in the same time period into a block.

The Miden execution model describes how state progresses on an individual level via transactions and at the global level expressed as aggregated state updates in blocks.

Architecture core concepts

Transaction execution

Every transaction results in a ZK proof that attests to its correctness.

There are two types of transactions: local and network. For every transaction there is a proof which is either created by the user in the Miden client or by the operator using the Miden node.

Transaction batching

To reduce the required space on the Ethereum blockchain, transaction proofs are aggregated into batches. This can happen in parallel on different machines that need to verify several proofs using the Miden VM and thus creating a proof.

Verifying a STARK proof within the VM is relatively efficient but it is still costly; we aim for 216 cycles.

Block production

Several batch proofs are aggregated into one block. This cannot happen in parallel and must be done by the Miden operator running the Miden node. The idea is the same, using recursive verification.

State progress

Miden has a centralized operator running a Miden node. Eventually, this will be a decentralized function.

Users send either transaction proofs (using local execution) or transaction data (for network execution) to the Miden node. Then, the Miden node uses recursive verification to aggregate transaction proofs into batches.

Batch proofs are aggregated into blocks by the Miden node. The blocks are then sent to Ethereum, and once a block is added to the L1 chain, the rollup chain is believed to have progressed to the next state.

A block produced by the Miden node looks something like this:

Architecture core concepts

Tip: Block contents

  • State updates only contain the hashes of changes. For example, for each updated account, we record a tuple ([account id], [new account hash]).
  • ZK Proof attests that, given a state commitment from the previous block, there was a sequence of valid transactions executed that resulted in the new state commitment, and the output also included state updates.
  • The block also contains full account and note data for public accounts and notes. For example, if account 123 is an updated public account which, in the state updates section we'd see a records for it as (123, 0x456..). The full new state of this account (which should hash to 0x456..) would be included in a separate section.

Verifying valid block state

To verify that a block describes a valid state transition, we do the following:

  1. Compute hashes of public account and note states.
  2. Make sure these hashes match records in the state updates section.
  3. Verify the included ZKP against the following public inputs:
    • State commitment from the previous block.
    • State commitment from the current block.
    • State updates from the current block.

The above can be performed by a verifier contract on Ethereum L1.

Syncing to current state from genesis

The block structure has another nice property. It is very easy for a new node to sync up to the current state from genesis.

The new node would need to do the following:

  1. Download only the first part of the blocks (i.e., without full account/note states) starting at the genesis up until the latest block.
  2. Verify all ZK proofs in the downloaded blocks. This is super quick (exponentially faster than re-executing original transactions) and can also be done in parallel.
  3. Download the current states of account, note, and nullifier databases.
  4. Verify that the downloaded current state matches the state commitment in the latest block.

Overall, state sync is dominated by the time needed to download the data.

Introduction

Basic tutorials and examples of how to build applications on Miden.

The goal is to make getting up to speed with building on Miden as quick and simple as possible.

All of the following tutorials are accompanied by code examples in Rust and TypeScript, which can be found in the Miden Tutorials repository.

Miden Node Setup Tutorial

To run the Miden tutorial examples, you will need to set up a test enviorment 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

Prerequisites

To run miden-node locally, you need to:

  1. Install the miden-node crate.
  2. Provide a genesis.toml file.
  3. Provide a miden-node.toml file.

Example genesis.toml and miden-node.toml files can be found in the miden-tutorials repository:

  • The genesis.toml file defines the start timestamp for the miden-node testnet and allows you to pre-deploy accounts and funding faucets.
  • The miden-node.toml file configures the RPC endpoint and other settings for the miden-node.

Running the Miden node locally

Step 1: Clone the miden-tutorials repository

In a terminal window, clone the miden-tutorials repository and navigate to the root of the repository using this command:

git clone git@github.com:0xPolygonMiden/miden-tutorials.git
cd miden-tutorials

Step 2: Install the Miden node

Next, install the miden-node crate using this command:

cargo install miden-node --locked

Step 3: Initializing the node

To start the node, we first need to generate the genesis file. To do so, navigate to the /node directory and create the genesis file using this command:

cd node
miden-node make-genesis \
  --inputs-path  config/genesis.toml \
  --output-path  storage/genesis.dat

Expected output:

Genesis input file: config/genesis.toml has successfully been loaded.
Creating fungible faucet account...
Account "faucet" has successfully been saved to: storage/accounts/faucet.mac
Miden node genesis successful: storage/genesis.dat has been created

Step 4: Starting the node

Now, to start the node, navigate to the storage directory and run this command:

cd storage
miden-node start \
  --config node/config/miden-node.toml \
  node

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

Reseting the node

If you need to reset the local state of the node and the rust-client, navigate to the root of the miden-tutorials repository and run this command:

rm -rf rust-client/store.sqlite3 
rm -rf node/storage/accounts
rm -rf node/storage/blocks

Connecting to the Miden testnet

To run the tutorial examples using the Miden testnet, use this endpoint:

https://rpc.devnet.miden.io:443

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 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:

cargo new miden-rust-client
cd miden-rust-client 

Add the following dependencies to your Cargo.toml file:

[dependencies]
miden-client = { version = "0.7", features = ["testing", "concurrent", "tonic", "sqlite"] }
miden-lib = { version = "0.7", default-features = false }
miden-objects = { version = "0.7.2", default-features = false }
miden-crypto = { version = "0.13.2", features = ["executable"] }
rand = { version = "0.8" }
serde = { version = "1", features = ["derive"] }
serde_json = { version = "1.0", features = ["raw_value"] }
tokio = { version = "1.40", features = ["rt-multi-thread", "net", "macros"] }
rand_chacha = "0.3.1"

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.

use miden_client::{
    account::{
        component::{BasicFungibleFaucet, BasicWallet, RpoFalcon512},
        AccountBuilder, AccountId, AccountStorageMode, AccountType,
    },
    asset::{FungibleAsset, TokenSymbol},
    auth::AuthSecretKey,
    crypto::{RpoRandomCoin, SecretKey},
    note::NoteType,
    rpc::{Endpoint, TonicRpcClient},
    store::{sqlite_store::SqliteStore, StoreAuthenticator},
    transaction::{OutputNote, PaymentTransactionData, TransactionRequestBuilder},
    Client, ClientError, Felt,
};
use miden_lib::note::create_p2id_note;
use miden_objects::account::AccountIdVersion;

use rand::Rng;
use std::sync::Arc;
use tokio::time::Duration;

pub async fn initialize_client() -> Result<Client<RpoRandomCoin>, ClientError> {
    // RPC endpoint and timeout
    let endpoint = Endpoint::new("http".to_string(), "localhost".to_string(), Some(57291));
    let timeout_ms = 10_000;

    // Build RPC client
    let rpc_api = Box::new(TonicRpcClient::new(endpoint, timeout_ms));

    // Seed RNG
    let mut seed_rng = rand::thread_rng();
    let coin_seed: [u64; 4] = seed_rng.gen();

    // Create random coin instance
    let rng = RpoRandomCoin::new(coin_seed.map(Felt::new));

    // SQLite path
    let store_path = "store.sqlite3";

    // Initialize SQLite store
    let store = SqliteStore::new(store_path.into())
        .await
        .map_err(ClientError::StoreError)?;
    let arc_store = Arc::new(store);

    // Create authenticator referencing the same store and RNG
    let authenticator = StoreAuthenticator::new_with_rng(arc_store.clone(), rng.clone());

    // Instantiate the client. Toggle `in_debug_mode` as needed
    let client = Client::new(rpc_api, rng, arc_store, Arc::new(authenticator), true);

    Ok(client)
}

#[tokio::main]
async fn main() -> Result<(), ClientError> {
    let mut client = initialize_client().await?;
    println!("Client initialized successfully.");

    let sync_summary = client.sync_state().await.unwrap();
    let block_number = sync_summary.block_num;
    println!("Latest block number: {}", block_number);

    Ok(())
}

When running the code above, there will be some unused imports, however, we will use these imports later on in the tutorial.

In this step, we will initialize a Miden client capable of syncing with the blockchain (in this case, our local node). Run the following command to execute src/main.rs:

cargo run --release 

After the program executes, you should see the latest block number printed to the terminal, for example:

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:

#![allow(unused)]
fn main() {
//------------------------------------------------------------
// STEP 1: Create a basic wallet for Alice
//------------------------------------------------------------
println!("\n[STEP 1] Creating a new account for Alice");

// Account seed
let mut init_seed = [0u8; 32];
client.rng().fill_bytes(&mut init_seed);

// Generate key pair
let key_pair = SecretKey::with_rng(client.rng());

// Anchor block
let anchor_block = client.get_latest_epoch_block().await.unwrap();

// Build the account
let builder = AccountBuilder::new(init_seed)
    .anchor((&anchor_block).try_into().unwrap())
    .account_type(AccountType::RegularAccountUpdatableCode)
    .storage_mode(AccountStorageMode::Public)
    .with_component(RpoFalcon512::new(key_pair.public_key()))
    .with_component(BasicWallet);

let (alice_account, seed) = builder.build().unwrap();

// Add the account to the client
client
    .add_account(
        &alice_account,
        Some(seed),
        &AuthSecretKey::RpoFalcon512(key_pair),
        false,
    )
    .await?;

println!("Alice's account ID: {:?}", alice_account.id().to_hex());
}

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:

#![allow(unused)]
fn main() {
//------------------------------------------------------------
// 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 = SecretKey::with_rng(client.rng());

// Build the account
let builder = AccountBuilder::new(init_seed)
    .anchor((&anchor_block).try_into().unwrap())
    .account_type(AccountType::FungibleFaucet)
    .storage_mode(AccountStorageMode::Public)
    .with_component(RpoFalcon512::new(key_pair.public_key()))
    .with_component(BasicFungibleFaucet::new(symbol, decimals, max_supply).unwrap());

let (faucet_account, seed) = builder.build().unwrap();

// Add the faucet to the client
client
    .add_account(
        &faucet_account,
        Some(seed),
        &AuthSecretKey::RpoFalcon512(key_pair),
        false,
    )
    .await?;

println!("Faucet account ID: {:?}", faucet_account.id().to_hex());
}

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:

#[tokio::main]
async fn main() -> Result<(), ClientError> {
    let mut client = initialize_client().await?;
    println!("Client initialized successfully.");

    let sync_summary = client.sync_state().await.unwrap();
    let block_number = sync_summary.block_num;
    println!("Latest block number: {}", block_number);

    //------------------------------------------------------------
    // STEP 1: Create a basic wallet for Alice
    //------------------------------------------------------------
    println!("\n[STEP 1] Creating a new account for Alice");

    // Account seed
    let mut init_seed = [0u8; 32];
    client.rng().fill_bytes(&mut init_seed);

    // Generate key pair
    let key_pair = SecretKey::with_rng(client.rng());

    // Anchor block
    let anchor_block = client.get_latest_epoch_block().await.unwrap();

    // Build the account
    let builder = AccountBuilder::new(init_seed)
        .anchor((&anchor_block).try_into().unwrap())
        .account_type(AccountType::RegularAccountUpdatableCode)
        .storage_mode(AccountStorageMode::Public)
        .with_component(RpoFalcon512::new(key_pair.public_key()))
        .with_component(BasicWallet);

    let (alice_account, seed) = builder.build().unwrap();

    // Add the account to the client
    client
        .add_account(
            &alice_account,
            Some(seed),
            &AuthSecretKey::RpoFalcon512(key_pair),
            false,
        )
        .await?;

    println!("Alice's account ID: {:?}", alice_account.id().to_hex());

    //------------------------------------------------------------
    // 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 = SecretKey::with_rng(client.rng());

    // Build the account
    let builder = AccountBuilder::new(init_seed)
        .anchor((&anchor_block).try_into().unwrap())
        .account_type(AccountType::FungibleFaucet)
        .storage_mode(AccountStorageMode::Public)
        .with_component(RpoFalcon512::new(key_pair.public_key()))
        .with_component(BasicFungibleFaucet::new(symbol, decimals, max_supply).unwrap());

    let (faucet_account, seed) = builder.build().unwrap();

    // Add the faucet to the client
    client
        .add_account(
            &faucet_account,
            Some(seed),
            &AuthSecretKey::RpoFalcon512(key_pair),
            false,
        )
        .await?;

    println!("Faucet account ID: {:?}", faucet_account.id().to_hex());

    // Resync to show newly deployed faucet
    client.sync_state().await?;

    Ok(())
}

Let's run the src/main.rs program again:

cargo run --release 

The output will look like this:

[STEP 1] Creating a new account for Alice
Alice's account ID: "0x715abc291819b1100000e7cd88cf3e"

[STEP 2] Deploying a new fungible faucet.
Faucet account ID: "0xab5fb36dd552982000009c440264ce"

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 repository and run this command:

cd rust-client
cargo run --release --bin create_mint_consume_send

Continue learning

Next tutorial: Mint, Consume, and Create Notes

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:

#![allow(unused)]
fn main() {
//------------------------------------------------------------
// 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::mint_fungible_asset(
        fungible_asset.clone(),
        alice_account.id(),
        NoteType::Public,
        client.rng(),
    )
    .unwrap()
    .build();
    let tx_execution_result = client
        .new_transaction(faucet_account.id(), transaction_request)
        .await?;

    client.submit_transaction(tx_execution_result).await?;
    println!("Minted note #{} of {} tokens for Alice.", i, amount);
}
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:

#![allow(unused)]
fn main() {
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:

//------------------------------------------------------------
// 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 list_of_note_ids: Vec<_> = consumable_notes.iter().map(|(note, _)| note.id()).collect();

    if list_of_note_ids.len() == 5 {
        println!("Found 5 consumable notes for Alice. Consuming them now...");
        let transaction_request =
            TransactionRequestBuilder::consume_notes(list_of_note_ids).build();
        let tx_execution_result = client
            .new_transaction(alice_account.id(), transaction_request)
            .await?;

        client.submit_transaction(tx_execution_result).await?;
        println!("All of Alice's notes consumed successfully.");
        break;
    } else {
        println!(
            "Currently, Alice has {} consumable notes. Waiting for 5...",
            list_of_note_ids.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 P2IDR (Pay to Id Reclaimable) variant which allows the creator of the note to 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 .with_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:

//------------------------------------------------------------
// 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![];
for _ in 1..=4 {
    let init_seed = {
        let mut seed = [0u8; 15];
        rand::thread_rng().fill(&mut seed);
        seed[0] = 99u8;
        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 = create_p2id_note(
        alice_account.id(),
        target_account_id,
        vec![fungible_asset.into()],
        NoteType::Public,
        Felt::new(0),
        client.rng(),
    )?;
    p2id_notes.push(p2id_note);
}
let output_notes: Vec<OutputNote> = p2id_notes.into_iter().map(OutputNote::Full).collect();

let transaction_request = TransactionRequestBuilder::new()
    .with_own_output_notes(output_notes)
    .unwrap()
    .build();

let tx_execution_result = client
    .new_transaction(alice_account.id(), transaction_request)
    .await?;

client.submit_transaction(tx_execution_result).await?;
println!("Submitted a transaction with 4 P2ID notes.");

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:

// Send 50 tokens to 1 more account as a single P2ID transaction
println!("Submitting one more single P2ID transaction...");
let init_seed = {
    let mut seed = [0u8; 15];
    rand::thread_rng().fill(&mut seed);
    seed[0] = 99u8;
    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 payment_transaction = PaymentTransactionData::new(
    vec![fungible_asset.into()],
    alice_account.id(),
    target_account_id,
);
let transaction_request = TransactionRequestBuilder::pay_to_id(
    payment_transaction,
    None,             // recall_height
    NoteType::Public, // note type
    client.rng(),     // rng
)
.unwrap()
.build();
let tx_execution_result = client
    .new_transaction(alice_account.id(), transaction_request)
    .await?;

client.submit_transaction(tx_execution_result).await?;

Note: In a production environment do not use AccountId::new_dummy(), this is simply for the sake of the tutorial example.

Summary

Your src/main.rs function should now look like this:

use miden_client::{
    account::{
        component::{BasicFungibleFaucet, BasicWallet, RpoFalcon512},
        AccountBuilder, AccountId, AccountStorageMode, AccountType,
    },
    asset::{FungibleAsset, TokenSymbol},
    auth::AuthSecretKey,
    crypto::{RpoRandomCoin, SecretKey},
    note::NoteType,
    rpc::{Endpoint, TonicRpcClient},
    store::{sqlite_store::SqliteStore, StoreAuthenticator},
    transaction::{OutputNote, PaymentTransactionData, TransactionRequestBuilder},
    Client, ClientError, Felt,
};
use miden_lib::note::create_p2id_note;
use miden_objects::account::AccountIdVersion;

use rand::Rng;
use std::sync::Arc;
use tokio::time::Duration;

pub async fn initialize_client() -> Result<Client<RpoRandomCoin>, ClientError> {
    // RPC endpoint and timeout
    let endpoint = Endpoint::new("http".to_string(), "localhost".to_string(), Some(57291));
    let timeout_ms = 10_000;

    // Build RPC client
    let rpc_api = Box::new(TonicRpcClient::new(endpoint, timeout_ms));

    // Seed RNG
    let mut seed_rng = rand::thread_rng();
    let coin_seed: [u64; 4] = seed_rng.gen();

    // Create random coin instance
    let rng = RpoRandomCoin::new(coin_seed.map(Felt::new));

    // SQLite path
    let store_path = "store.sqlite3";

    // Initialize SQLite store
    let store = SqliteStore::new(store_path.into())
        .await
        .map_err(ClientError::StoreError)?;
    let arc_store = Arc::new(store);

    // Create authenticator referencing the same store and RNG
    let authenticator = StoreAuthenticator::new_with_rng(arc_store.clone(), rng.clone());

    // Instantiate the client. Toggle `in_debug_mode` as needed
    let client = Client::new(rpc_api, rng, arc_store, Arc::new(authenticator), true);

    Ok(client)
}

#[tokio::main]
async fn main() -> Result<(), ClientError> {
    let mut client = initialize_client().await?;
    println!("Client initialized successfully.");

    let sync_summary = client.sync_state().await.unwrap();
    let block_number = sync_summary.block_num;
    println!("Latest block number: {}", block_number);

    //------------------------------------------------------------
    // STEP 1: Create a basic wallet for Alice
    //------------------------------------------------------------
    println!("\n[STEP 1] Creating a new account for Alice");

    // Account seed
    let mut init_seed = [0u8; 32];
    client.rng().fill_bytes(&mut init_seed);

    // Generate key pair
    let key_pair = SecretKey::with_rng(client.rng());

    // Anchor block
    let anchor_block = client.get_latest_epoch_block().await.unwrap();

    // Build the account
    let builder = AccountBuilder::new(init_seed)
        .anchor((&anchor_block).try_into().unwrap())
        .account_type(AccountType::RegularAccountUpdatableCode)
        .storage_mode(AccountStorageMode::Public)
        .with_component(RpoFalcon512::new(key_pair.public_key()))
        .with_component(BasicWallet);

    let (alice_account, seed) = builder.build().unwrap();

    // Add the account to the client
    client
        .add_account(
            &alice_account,
            Some(seed),
            &AuthSecretKey::RpoFalcon512(key_pair),
            false,
        )
        .await?;

    println!("Alice's account ID: {:?}", alice_account.id().to_hex());

    //------------------------------------------------------------
    // 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 = SecretKey::with_rng(client.rng());

    // Build the account
    let builder = AccountBuilder::new(init_seed)
        .anchor((&anchor_block).try_into().unwrap())
        .account_type(AccountType::FungibleFaucet)
        .storage_mode(AccountStorageMode::Public)
        .with_component(RpoFalcon512::new(key_pair.public_key()))
        .with_component(BasicFungibleFaucet::new(symbol, decimals, max_supply).unwrap());

    let (faucet_account, seed) = builder.build().unwrap();

    // Add the faucet to the client
    client
        .add_account(
            &faucet_account,
            Some(seed),
            &AuthSecretKey::RpoFalcon512(key_pair),
            false,
        )
        .await?;

    println!("Faucet account ID: {:?}", faucet_account.id().to_hex());

    // 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::mint_fungible_asset(
            fungible_asset.clone(),
            alice_account.id(),
            NoteType::Public,
            client.rng(),
        )
        .unwrap()
        .build();
        let tx_execution_result = client
            .new_transaction(faucet_account.id(), transaction_request)
            .await?;

        client.submit_transaction(tx_execution_result).await?;
        println!("Minted note #{} of {} tokens for Alice.", i, amount);
    }
    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 list_of_note_ids: Vec<_> = consumable_notes.iter().map(|(note, _)| note.id()).collect();

        if list_of_note_ids.len() == 5 {
            println!("Found 5 consumable notes for Alice. Consuming them now...");
            let transaction_request =
                TransactionRequestBuilder::consume_notes(list_of_note_ids).build();
            let tx_execution_result = client
                .new_transaction(alice_account.id(), transaction_request)
                .await?;

            client.submit_transaction(tx_execution_result).await?;
            println!("All of Alice's notes consumed successfully.");
            break;
        } else {
            println!(
                "Currently, Alice has {} consumable notes. Waiting for 5...",
                list_of_note_ids.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![];
    for _ in 1..=4 {
        let init_seed = {
            let mut seed = [0u8; 15];
            rand::thread_rng().fill(&mut seed);
            seed[0] = 99u8;
            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 = create_p2id_note(
            alice_account.id(),
            target_account_id,
            vec![fungible_asset.into()],
            NoteType::Public,
            Felt::new(0),
            client.rng(),
        )?;
        p2id_notes.push(p2id_note);
    }
    let output_notes: Vec<OutputNote> = p2id_notes.into_iter().map(OutputNote::Full).collect();

    let transaction_request = TransactionRequestBuilder::new()
        .with_own_output_notes(output_notes)
        .unwrap()
        .build();

    let tx_execution_result = client
        .new_transaction(alice_account.id(), transaction_request)
        .await?;

    client.submit_transaction(tx_execution_result).await?;
    println!("Submitted a transaction with 4 P2ID notes.");

    // Send 50 tokens to 1 more account as a single P2ID transaction
    println!("Submitting one more single P2ID transaction...");
    let init_seed = {
        let mut seed = [0u8; 15];
        rand::thread_rng().fill(&mut seed);
        seed[0] = 99u8;
        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 payment_transaction = PaymentTransactionData::new(
        vec![fungible_asset.into()],
        alice_account.id(),
        target_account_id,
    );
    let transaction_request = TransactionRequestBuilder::pay_to_id(
        payment_transaction,
        None,             // recall_height
        NoteType::Public, // note type
        client.rng(),     // rng
    )
    .unwrap()
    .build();
    let tx_execution_result = client
        .new_transaction(alice_account.id(), transaction_request)
        .await?;

    client.submit_transaction(tx_execution_result).await?;

    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:

cargo run --release 

The output will look like this:

Client initialized successfully.
Latest block number: 1519

[STEP 1] Creating a new account for Alice
Alice's account ID: "0xd0e8ba5acf2e83100000887188d2b9"

[STEP 2] Deploying a new fungible faucet.
Faucet account ID: "0xcdf877e221333a2000002e2b7ff0b2"

[STEP 3] Minting 5 notes of 100 tokens each for Alice.
Minted note #1 of 100 tokens for Alice.
Minted note #2 of 100 tokens for Alice.
Minted note #3 of 100 tokens for Alice.
Minted note #4 of 100 tokens for Alice.
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 1 consumable notes. Waiting for 5...
Currently, Alice has 4 consumable notes. Waiting for 5...
Found 5 consumable notes for Alice. Consuming them now...
one or more warnings were emitted
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 repository and run this command:

cd rust-client
cargo run --release --bin create_mint_consume_send

Continue learning

Next tutorial: Deploying a Counter Contract

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 Miden programs in the Miden playground.

Step 1: Initialize your repository

Create a new Rust repository for your Miden project and navigate to it with the following command:

cargo new miden-counter-contract
cd miden-counter-contract

Add the following dependencies to your Cargo.toml file:

[dependencies]
miden-client = { version = "0.7", features = ["testing", "concurrent", "tonic", "sqlite"] }
miden-lib = { version = "0.7", default-features = false }
miden-objects = { version = "0.7.2", default-features = false }
miden-crypto = { version = "0.13.2", features = ["executable"] }
rand = { version = "0.8" }
serde = { version = "1", features = ["derive"] }
serde_json = { version = "1.0", features = ["raw_value"] }
tokio = { version = "1.40", features = ["rt-multi-thread", "net", "macros"] }
rand_chacha = "0.3.1"

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:

use std::{fs, path::Path, sync::Arc};

use rand::Rng;
use rand_chacha::rand_core::SeedableRng;
use rand_chacha::ChaCha20Rng;
use tokio::time::Duration;

use miden_client::{
    account::{AccountStorageMode, AccountType},
    crypto::RpoRandomCoin,
    rpc::{Endpoint, TonicRpcClient},
    store::{sqlite_store::SqliteStore, StoreAuthenticator},
    transaction::{TransactionKernel, TransactionRequestBuilder},
    Client, ClientError, Felt,
};

use miden_objects::{
    account::{AccountBuilder, AccountComponent, AuthSecretKey, StorageSlot},
    assembly::Assembler,
    crypto::{dsa::rpo_falcon512::SecretKey, hash::rpo::RpoDigest},
    Word,
};

pub async fn initialize_client() -> Result<Client<RpoRandomCoin>, ClientError> {
    // RPC endpoint and timeout
    let endpoint = Endpoint::new(
        "https".to_string(),
        "rpc.devnet.miden.io".to_string(),
        Some(443),
    );
    let timeout_ms = 10_000;

    // Build RPC client
    let rpc_api = Box::new(TonicRpcClient::new(endpoint, timeout_ms));

    // Seed RNG
    let mut seed_rng = rand::thread_rng();
    let coin_seed: [u64; 4] = seed_rng.gen();

    // Create random coin instance
    let rng = RpoRandomCoin::new(coin_seed.map(Felt::new));

    // SQLite path
    let store_path = "store.sqlite3";

    // Initialize SQLite store
    let store = SqliteStore::new(store_path.into())
        .await
        .map_err(ClientError::StoreError)?;
    let arc_store = Arc::new(store);

    // Create authenticator referencing the store and RNG
    let authenticator = StoreAuthenticator::new_with_rng(arc_store.clone(), rng.clone());

    // Instantiate client (toggle debug mode as needed)
    let client = Client::new(rpc_api, rng, arc_store, Arc::new(authenticator), true);

    Ok(client)
}

pub fn get_new_pk_and_authenticator() -> (Word, AuthSecretKey) {
    // Create a deterministic RNG with zeroed seed
    let seed = [0_u8; 32];
    let mut rng = ChaCha20Rng::from_seed(seed);

    // Generate Falcon-512 secret key
    let sec_key = SecretKey::with_rng(&mut rng);

    // Convert public key to `Word` (4xFelt)
    let pub_key: Word = sec_key.public_key().into();

    // Wrap secret key in `AuthSecretKey`
    let auth_secret_key = AuthSecretKey::RpoFalcon512(sec_key);

    (pub_key, auth_secret_key)
}

#[tokio::main]
async fn main() -> Result<(), ClientError> {
    let mut client = initialize_client().await?;
    println!("Client initialized successfully.");

    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.

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:

mkdir -p masm/accounts masm/scripts

This will create:

masm/
├── accounts/
└── scripts/

Custom Miden smart contract

Below is our counter contract. It has a single exported procedure increment_count.

At the beginning of the MASM file, we define our imports. In this case, we import miden::account and std::sys.

The import miden::account contains useful procedures for interacting with a smart contract's state.

The import std::sys contains a useful procedure for truncating the operand stack at the end of a procedure.

Here's a breakdown of what the increment_count procedure does:

  1. Pushes 0 onto the stack, representing the index of the storage slot to read.
  2. Calls account::get_item with the index of 0.
  3. Pushes 1 onto the stack.
  4. Adds 1 to the count value returned from account::get_item.
  5. For demonstration purposes, calls debug.stack to see the state of the stack
  6. Pushes 0 onto the stack, which is the index of the storage slot we want to write to.
  7. Calls account::set_item which saves the incremented count to storage at index 0
  8. Calls sys::truncate_stack to truncate the stack to size 16.

Inside of the masm/accounts/ directory, create the counter.masm file:

use.miden::account
use.std::sys

export.increment_count
    # => []
    push.0

    # => [index]
    exec.account::get_item

    # => [count]
    push.1 add

    # debug statement with client
    debug.stack

    # => [count+1]
    push.0

    # [index, count+1]
    exec.account::set_item

    # => []
    push.1 exec.account::incr_nonce

    # => []
    exec.sys::truncate_stack
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.

Concept of function visibility and modifiers in Miden smart contracts

The increment_count function in our Miden smart contract behaves like an "external" Solidity function without a modifier, meaning any user can call it to increment the contract's count. This is because it calls account::incr_nonce during execution.

If the increment_count procedure did not call the account::incr_nonce procedure during its execution, only the deployer of the counter contract would be able to increment the count of the smart contract (if the RpoFalcon512 component was added to the account, in this case we didn't add it).

In essence, if a procedure performs a state change in the Miden smart contract, and does not call account::incr_nonce at some point during its execution, this function can be equated to having an onlyOwner Solidity modifer, meaning only the user with knowledge of the private key of the account can execute transactions that result in a state change.

Note: Adding the account::incr_nonce to a state changing procedure allows any user to call the procedure.

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:

begin
    # => []
    call.{increment_count}
end

Step 3: Build the counter smart contract in Rust

To build the counter contract copy and paste the following code at the end of your src/main.rs file:

#![allow(unused)]
fn main() {
// -------------------------------------------------------------------------
// STEP 1: Create a basic counter contract
// -------------------------------------------------------------------------
println!("\n[STEP 1] Creating counter contract.");

// Load the MASM file for the counter contract
let file_path = Path::new("./masm/accounts/counter.masm");
let account_code = fs::read_to_string(file_path).unwrap();

// Prepare assembler (debug mode = true)
let assembler: Assembler = TransactionKernel::assembler().with_debug_mode(true);

// Compile the account code into `AccountComponent` with one storage slot
let account_component = AccountComponent::compile(
    account_code,
    assembler,
    vec![StorageSlot::Value(Word::default())],
)
.unwrap()
.with_supports_all_types();

// Init seed for the counter contract
let init_seed = ChaCha20Rng::from_entropy().gen();

// Anchor block of the account
let anchor_block = client.get_latest_epoch_block().await.unwrap();

// Build the new `Account` with the component
let (counter_contract, counter_seed) = AccountBuilder::new(init_seed)
    .anchor((&anchor_block).try_into().unwrap())
    .account_type(AccountType::RegularAccountImmutableCode)
    .storage_mode(AccountStorageMode::Public)
    .with_component(account_component)
    .build()
    .unwrap();

println!(
    "counter_contract hash: {:?}",
    counter_contract.hash().to_hex()
);
println!("contract id: {:?}", counter_contract.id().to_hex());

// Since the counter contract is public and does sign any transactions, auth_secrete_key is not required.
// However, to import to the client, we must generate a random value.
let (_counter_pub_key, auth_secret_key) = get_new_pk_and_authenticator();

client
    .add_account(
        &counter_contract.clone(),
        Some(counter_seed),
        &auth_secret_key,
        false,
    )
    .await
    .unwrap();
}

Run the following command to execute src/main.rs:

cargo run --release 

After the program executes, you should see the counter contract hash and contract id printed to the terminal, for example:

counter_contract hash: "0xd693494753f51cb73a436916077c7b71c680a6dddc64dc364c1fe68f16f0c087"
contract id: "0x082ed14c8ad9a866"

Step 4: Computing the prodedure roots

Each Miden assembly procedure has an associated hash. When calling a procedure in a smart contract, we need to know the hash of the procedure. The hashes of the procedures form a Merkelized Abstract Syntax Tree (MAST).

To get the procedures of the counter contract, add this code snippet to the end of your main() function:

#![allow(unused)]
fn main() {
// Print procedure root hashes
let procedures = counter_contract.code().procedure_roots();
let procedures_vec: Vec<RpoDigest> = procedures.collect();
for (index, procedure) in procedures_vec.iter().enumerate() {
    println!("Procedure {}: {:?}", index + 1, procedure.to_hex());
}
println!("number of procedures: {}", procedures_vec.len());
}

Run the following command to execute src/main.rs:

cargo run --release 

After the program executes, you should see the procedure hashes printed to the terminal, for example:

Procedure 1: "0x2259e69ba0e49a85f80d5ffc348e25a0386a0bbe7dbb58bc45b3f1493a03c725"

This is the hash of the increment_count procedure.

Step 4: Incrementing the count

Now that we know the hash of the increment_count procedure, we can call the procedure in the counter contract. In the Rust code below, we replace the {increment_count} string with the hash of the increment_count procedure.

Then we create a new transaction request with our custom script, and then pass the transaction request to the client.

Paste the following code at the end of your src/main.rs file:

#![allow(unused)]
fn main() {
// -------------------------------------------------------------------------
// STEP 2: Call the Counter Contract with a script
// -------------------------------------------------------------------------
println!("\n[STEP 2] Call Counter Contract With Script");

// Grab the first procedure hash
let procedure_2_hash = procedures_vec[0].to_hex();
let procedure_call = format!("{}", procedure_2_hash);

// Load the MASM script referencing the increment procedure
let file_path = Path::new("./masm/scripts/counter_script.masm");
let original_code = fs::read_to_string(file_path).unwrap();

// Replace the placeholder with the actual procedure call
let replaced_code = original_code.replace("{increment_count}", &procedure_call);
println!("Final script:\n{}", replaced_code);

// Compile the script referencing our procedure
let tx_script = client.compile_tx_script(vec![], &replaced_code).unwrap();

// Build a transaction request with the custom script
let tx_increment_request = TransactionRequestBuilder::new()
    .with_custom_script(tx_script)
    .unwrap()
    .build();

// Execute the transaction locally
let tx_result = client
    .new_transaction(counter_contract.id(), tx_increment_request)
    .await
    .unwrap();

let tx_id = tx_result.executed_transaction().id();
println!(
    "View transaction on MidenScan: https://testnet.midenscan.com/tx/{:?}",
    tx_id
);

// Submit transaction to the network
let _ = client.submit_transaction(tx_result).await;

// Wait, then re-sync
tokio::time::sleep(Duration::from_secs(3)).await;
client.sync_state().await.unwrap();

// Retrieve updated contract data to see the incremented counter
let account = client.get_account(counter_contract.id()).await.unwrap();
println!(
    "storage item 0: {:?}",
    account.unwrap().account().storage().get_item(0)
);
}

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:

use std::{fs, path::Path, sync::Arc};

use rand::Rng;
use rand_chacha::rand_core::SeedableRng;
use rand_chacha::ChaCha20Rng;
use tokio::time::Duration;

use miden_client::{
    account::{AccountStorageMode, AccountType},
    crypto::RpoRandomCoin,
    rpc::{Endpoint, TonicRpcClient},
    store::{sqlite_store::SqliteStore, StoreAuthenticator},
    transaction::{TransactionKernel, TransactionRequestBuilder},
    Client, ClientError, Felt,
};

use miden_objects::{
    account::{AccountBuilder, AccountComponent, AuthSecretKey, StorageSlot},
    assembly::Assembler,
    crypto::{dsa::rpo_falcon512::SecretKey, hash::rpo::RpoDigest},
    Word,
};

pub async fn initialize_client() -> Result<Client<RpoRandomCoin>, ClientError> {
    // RPC endpoint and timeout
    let endpoint = Endpoint::new(
        "https".to_string(),
        "rpc.devnet.miden.io".to_string(),
        Some(443),
    );
    let timeout_ms = 10_000;

    // Build RPC client
    let rpc_api = Box::new(TonicRpcClient::new(endpoint, timeout_ms));

    // Seed RNG
    let mut seed_rng = rand::thread_rng();
    let coin_seed: [u64; 4] = seed_rng.gen();

    // Create random coin instance
    let rng = RpoRandomCoin::new(coin_seed.map(Felt::new));

    // SQLite path
    let store_path = "store.sqlite3";

    // Initialize SQLite store
    let store = SqliteStore::new(store_path.into())
        .await
        .map_err(ClientError::StoreError)?;
    let arc_store = Arc::new(store);

    // Create authenticator referencing the store and RNG
    let authenticator = StoreAuthenticator::new_with_rng(arc_store.clone(), rng.clone());

    // Instantiate client (toggle debug mode as needed)
    let client = Client::new(rpc_api, rng, arc_store, Arc::new(authenticator), true);

    Ok(client)
}

pub fn get_new_pk_and_authenticator() -> (Word, AuthSecretKey) {
    // Create a deterministic RNG with zeroed seed
    let seed = [0_u8; 32];
    let mut rng = ChaCha20Rng::from_seed(seed);

    // Generate Falcon-512 secret key
    let sec_key = SecretKey::with_rng(&mut rng);

    // Convert public key to `Word` (4xFelt)
    let pub_key: Word = sec_key.public_key().into();

    // Wrap secret key in `AuthSecretKey`
    let auth_secret_key = AuthSecretKey::RpoFalcon512(sec_key);

    (pub_key, auth_secret_key)
}

#[tokio::main]
async fn main() -> Result<(), ClientError> {
    // Initialize client
    let mut client = initialize_client().await?;
    println!("Client initialized successfully.");

    // Fetch latest block from node
    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 file_path = Path::new("./masm/accounts/counter.masm");
    let account_code = fs::read_to_string(file_path).unwrap();

    // Prepare assembler (debug mode = true)
    let assembler: Assembler = TransactionKernel::assembler().with_debug_mode(true);

    // Compile the account code into `AccountComponent` with one storage slot
    let account_component = AccountComponent::compile(
        account_code,
        assembler,
        vec![StorageSlot::Value(Word::default())],
    )
    .unwrap()
    .with_supports_all_types();

    // Init seed for the counter contract
    let init_seed = ChaCha20Rng::from_entropy().gen();

    // Anchor block of the account
    let anchor_block = client.get_latest_epoch_block().await.unwrap();

    // Build the new `Account` with the component
    let (counter_contract, counter_seed) = AccountBuilder::new(init_seed)
        .anchor((&anchor_block).try_into().unwrap())
        .account_type(AccountType::RegularAccountImmutableCode)
        .storage_mode(AccountStorageMode::Public)
        .with_component(account_component)
        .build()
        .unwrap();

    println!(
        "counter_contract hash: {:?}",
        counter_contract.hash().to_hex()
    );
    println!("contract id: {:?}", counter_contract.id().to_hex());

    // Since the counter contract is public and does sign any transactions, auth_secrete_key is not required.
    // However, to import to the client, we must generate a random value.
    let (_counter_pub_key, auth_secret_key) = get_new_pk_and_authenticator();

    client
        .add_account(
            &counter_contract.clone(),
            Some(counter_seed),
            &auth_secret_key,
            false,
        )
        .await
        .unwrap();

    // Print procedure root hashes
    let procedures = counter_contract.code().procedure_roots();
    let procedures_vec: Vec<RpoDigest> = procedures.collect();
    for (index, procedure) in procedures_vec.iter().enumerate() {
        println!("Procedure {}: {:?}", index + 1, procedure.to_hex());
    }
    println!("number of procedures: {}", procedures_vec.len());

    // -------------------------------------------------------------------------
    // STEP 2: Call the Counter Contract with a script
    // -------------------------------------------------------------------------
    println!("\n[STEP 2] Call Counter Contract With Script");

    // Grab the first procedure hash
    let procedure_2_hash = procedures_vec[0].to_hex();
    let procedure_call = format!("{}", procedure_2_hash);

    // Load the MASM script referencing the increment procedure
    let file_path = Path::new("./masm/scripts/counter_script.masm");
    let original_code = fs::read_to_string(file_path).unwrap();

    // Replace the placeholder with the actual procedure call
    let replaced_code = original_code.replace("{increment_count}", &procedure_call);
    println!("Final script:\n{}", replaced_code);

    // Compile the script referencing our procedure
    let tx_script = client.compile_tx_script(vec![], &replaced_code).unwrap();

    // Build a transaction request with the custom script
    let tx_increment_request = TransactionRequestBuilder::new()
        .with_custom_script(tx_script)
        .unwrap()
        .build();

    // Execute the transaction locally
    let tx_result = client
        .new_transaction(counter_contract.id(), tx_increment_request)
        .await
        .unwrap();

    let tx_id = tx_result.executed_transaction().id();
    println!(
        "View transaction on MidenScan: https://testnet.midenscan.com/tx/{:?}",
        tx_id
    );

    // Submit transaction to the network
    let _ = client.submit_transaction(tx_result).await;

    // Wait, then re-sync
    tokio::time::sleep(Duration::from_secs(3)).await;
    client.sync_state().await.unwrap();

    // Retrieve updated contract data to see the incremented counter
    let account = client.get_account(counter_contract.id()).await.unwrap();
    println!(
        "storage item 0: {:?}",
        account.unwrap().account().storage().get_item(0)
    );

    Ok(())
}

The output of our program will look something like this:

Client initialized successfully.
Latest block: 34911

[STEP 1] Creating counter contract.
counter_contract hash: "0x77358072810bc3db93e5527399ab7383889b0de3430053506ab5fc1dfe22f858"
contract id: "0xa0494a47d2ac49000000afba9465bf"
Procedure 1: "0xecd7eb223a5524af0cc78580d96357b298bb0b3d33fe95aeb175d6dab9de2e54"
number of procedures: 1

[STEP 2] Call Counter Contract With Script
Final script:
begin
    # => []
    call.0xecd7eb223a5524af0cc78580d96357b298bb0b3d33fe95aeb175d6dab9de2e54
end
Stack state before step 2598:
├──  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

View transaction on MidenScan: https://testnet.midenscan.com/tx/0x7065f2a5af6fee6cb585c1c10a48a667f3980d6468dc6d3b3010789b4db056d3
storage item 0: Ok(RpoDigest([0, 0, 0, 1]))

The line in the output Stack state before step 2598 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 repository and run this command:

cd rust-client
cargo run --release --bin counter_contract_increment

Creating Accounts and Deploying Faucets

Using the Miden WebClient in TypeScript to create accounts and deploy faucets

Overview

In this tutorial, we will create a basic web application that interacts with Miden using the Miden WebClient.

Our web application will create a Miden account for Alice and then deploy a fungible faucet. In the next section we will mint tokens from the faucet to fund her account, and then send the tokens from Alice's account to other Miden 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

To begin, make sure you have a miden-node running locally in a separate terminal window. To get the Miden node running locally, you can follow the instructions on the Miden Node Setup page.

Note: In this tutorial we use pnpm which is a drop in replacement for npm.

Public vs. private accounts & notes

Before we dive into the coding, let's clarify the concepts of public and private accounts and 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 React TypeScript repository for your Miden web application, navigate to it, and install the Miden WebClient using this command:

pnpm create vite miden-app --template react-ts

Navigate to the new repository:

cd miden-app

Install dependencies:

pnpm install

Install the Miden WebClient SDK:

pnpm i @demox-labs/miden-sdk@0.6.1-next.4

Save this as your vite.config.ts file:

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  build: {
    target: 'esnext',
  },
  optimizeDeps: {
    exclude: ['@demox-labs/miden-sdk'], // Exclude the SDK from optimization
  },
});

Note: ensure you are using Node version v20.12.0

Step 2: Initialize the client

Before we can interact with the Miden network, we need to instantiate the WebClient. In this step, we specify two parameters:

  • RPC endpoint - The URL of the Miden node to which we connect.
  • Delegated Prover Endpoint (optional) – The URL of the delegated prover which the client can connect to.

Create a webClient.ts file:

To instantiate the WebClient, pass in the endpoint of the Miden node. You can also instantiate the client with a delegated prover to speed up the proof generation time, however, in this example we will be instantiating the WebClient only with the endpoint of the Miden node since we will be handling proof generation locally within the browser.

Since we will be handling proof generation in the computationally constrained environment of the browser, it will be slower than proof generation handled by the Rust client. Currently, the Miden WebClient is thread-blocking when not used within a web worker.

Example of instantiating the WebClient with a delegated prover:

const nodeEndpoint = "http://localhost:57291";
const delegatedProver = 'http://18.118.151.210:8082'

let client = new WebClient();
await client.create_client(nodeEndpoint, delegatedProver);

In the src/ directory create a file named webClient.ts and paste the following into it:

// src/webClient.ts
import { WebClient } from "@demox-labs/miden-sdk";

const nodeEndpoint = "http://localhost:57291";

export async function webClient(): Promise<void> {
  try {
    // 1. Create client
    const client = new WebClient();
    await client.create_client(nodeEndpoint);

    // 2. Sync and log block
    const state = await client.sync_state();
    console.log("Latest block number:", state.block_num());
  } catch (error) {
    console.error("Error", error);
    throw error;
  }
}

Edit your App.tsx file:

Set this as your App.tsx file.

// src/App.tsx
import { useState } from "react";
import "./App.css";
import { webClient } from "./webClient";

function App() {
  const [clientStarted, setClientStarted] = useState(false);

  const handleClick = () => {
    webClient();
    setClientStarted(true);
  };

  return (
    <div className="App">
      <h1>Miden Web App</h1>

      <p>Open the console to view logs</p>

      {!clientStarted && <button onClick={handleClick}>Start WebClient</button>}
    </div>
  );
}

export default App;

Starting the frontend:

pnpm run dev

Open the frontend at:

http://localhost:5173/

Now open the browser console. Click the "Start the WebClient" button. Then in the console, you should see something like:

Latest block number: 123

Step 3: Creating a wallet

Now that we've initialized the WebClient, we can create a wallet for Alice.

To create a wallet for Alice using the Miden WebClient, we specify the account type by specifying if the account code is mutable or immutable and whether the account 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.

Our src/webClient.ts file should now look something like this:

// src/webClient.ts
import {
  WebClient,
  AccountStorageMode,
  AccountId,
  NoteType,
} from "@demox-labs/miden-sdk";

const nodeEndpoint = "http://localhost:57291";

export async function webClient(): Promise<void> {
  try {
    // 1. Create client
    const client = new WebClient();
    await client.create_client(nodeEndpoint);

    // 2. Sync and log block
    const state = await client.sync_state();
    console.log("Latest block number:", state.block_num());

    // 3. Create Alice account (public, updatable)
    console.log("Creating account for Alice");
    const aliceAccount = await client.new_wallet(
      AccountStorageMode.public(), // account type
      true,                        // mutability
    );
    const aliceIdHex = aliceAccount.id().to_string();
    console.log("Alice's account ID:", aliceIdHex);

    await client.sync_state();
  } catch (error) {
    console.error("Error:", error);
    throw error;
  }
}

Step 4: Deploying a fungible faucet

For Alice to receive testnet assets, we first need to 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 the webClient() function:

// 4. Create faucet
console.log("Creating faucet...");
const faucetAccount = await client.new_faucet(
  AccountStorageMode.public(), // account type
  false,                       // is fungible
  "MID",                       // symbol
  8,                           // decimals
  BigInt(1_000_000)            // max supply
);
const faucetIdHex = faucetAccount.id().to_string();
console.log("Faucet account ID:", faucetIdHex);

await client.sync_state();

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

Our new src/webClient.ts file should look something like this:

// src/webClient.ts
import {
  WebClient,
  AccountStorageMode,
  AccountId,
  NoteType,
} from "@demox-labs/miden-sdk";

const nodeEndpoint = "http://localhost:57291";

export async function webClient(): Promise<void> {
  try {
    // 1. Create client
    const client = new WebClient();
    await client.create_client(nodeEndpoint);

    // 2. Sync and log block
    const state = await client.sync_state();
    console.log("Latest block number:", state.block_num());

    // 3. Create Alice account (public, updatable)
    console.log("Creating account for Alice");
    const aliceAccount = await client.new_wallet(
      AccountStorageMode.public(),
      true,
    );
    const aliceIdHex = aliceAccount.id().to_string();
    console.log("Alice's account ID:", aliceIdHex);

    // 4. Create faucet
    console.log("Creating faucet...");
    const faucetAccount = await client.new_faucet(
      AccountStorageMode.public(), // account type
      false,                       // is fungible
      "MID",                       // symbol
      8,                           // decimals
      BigInt(1_000_000)            // max supply
    );
    const faucetIdHex = faucetAccount.id().to_string();
    console.log("Faucet account ID:", faucetIdHex);

    await client.sync_state();
  } catch (error) {
    console.error("Error", error);
    throw error;
  }
}

Let's run the src/main.rs program again:

pnpm run dev

The output will look like this:

Latest block number: 2247
Alice's account ID: 0xd70b2072c6495d100000869a8bacf2
Faucet account ID: 0x2d7e506fb88dde200000a1386efec8

In this section, we explained how to instantiate the Miden client, create a wallet, 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 web-client directory in the miden-tutorials repository and run the web application example:

cd web-client
pnpm i
pnpm run dev

Mint, Consume, and Create Notes

Using the Miden WebClient in TypeScript 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 assets 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 can use the WebClient's new_mint_transaction() function.

Below is an example of a transaction request minting tokens from the faucet for Alice.

Add this snippet to the end of the webClient function in the src/webClient.ts file that we created in the previous chapter:

await client.fetch_and_cache_account_auth_by_pub_key(
  AccountId.from_hex(faucetIdHex),
);
await client.sync_state();

console.log("Minting tokens to Alice...");
await client.new_mint_transaction(
  AccountId.from_hex(aliceIdHex),  // target wallet id
  AccountId.from_hex(faucetIdHex), // faucet id
  NoteType.public(),               // note type
  BigInt(1000),                    // amount
);

console.log("Waiting 15 seconds for transaction confirmation...");
await new Promise((resolve) => setTimeout(resolve, 15000));
await client.sync_state();

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 must consume this note for the assets to appear in their account.

To identify notes that are ready to consume, the Miden WebClient has a useful function get_consumable_notes. It is also important to sync the state of the client before calling the get_consumable_notes function.

Tip: If you know the expected number of notes after a transaction, use await or a loop condition to verify their availability before calling get_consumable_notes. This prevents unnecessary application idling.

Identifying which notes are available:

consumable_notes = await client.get_consumable_notes(accountId);

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 and consumes notes in a single transaction.

Add this snippet to the end of the webClient function in the src/webClient.ts file:

await client.fetch_and_cache_account_auth_by_pub_key(
  AccountId.from_hex(aliceIdHex),
);

const mintedNotes = await client.get_consumable_notes(
  AccountId.from_hex(aliceIdHex),
);
const mintedNoteIds = mintedNotes.map((n) =>
  n.input_note_record().id().to_string(),
);
console.log("Minted note IDs:", mintedNoteIds);

console.log("Consuming minted notes...");
await client.new_consume_transaction(
  AccountId.from_hex(aliceIdHex), // account id
  mintedNoteIds,                  // array of note ids to consume
);
await client.sync_state();
console.log("Notes consumed.");

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 notes in a single transaction.

The standard asset transfer note on Miden is the P2ID note (Pay to Id). There is also the P2IDR (Pay to Id Reclaimable) variant which allows the creator of the note to reclaim the note after a certain block height.

In our example, Alice will now send 50 tokens to a different account.

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:

// send single P2ID note
const dummyIdHex = "0x599a54603f0cf9000000ed7a11e379";
console.log("Sending tokens to dummy account...");
await client.new_send_transaction(
  AccountId.from_hex(aliceIdHex),  // sender account id
  AccountId.from_hex(dummyIdHex),  // receiver account id
  AccountId.from_hex(faucetIdHex), // faucet account id
  NoteType.public(),               // note type
  BigInt(100),                     // amount
);
await client.sync_state();

Summary

Your src/webClient.ts function should now look like this:

import {
  WebClient,
  AccountStorageMode,
  AccountId,
  NoteType,
} from "@demox-labs/miden-sdk";

const nodeEndpoint = "http://localhost:57291";

export async function webClient(): Promise<void> {
  try {
    // 1. Create client
    const client = new WebClient();
    await client.create_client(nodeEndpoint);

    // 2. Sync and log block
    const state = await client.sync_state();
    console.log("Latest block number:", state.block_num());

    // 3. Create Alice account (public, updatable)
    console.log("Creating account for Alice");
    const aliceAccount = await client.new_wallet(
      AccountStorageMode.public(), // account type
      true                         // mutability
    );
    const aliceIdHex = aliceAccount.id().to_string();
    console.log("Alice's account ID:", aliceIdHex);

    // 4. Create faucet
    console.log("Creating faucet...");
    const faucetAccount = await client.new_faucet(
      AccountStorageMode.public(),  // account type
      false,                        // fungible
      "MID",                        // symbol
      8,                            // decimals
      BigInt(1_000_000)             // max supply
    );
    const faucetIdHex = faucetAccount.id().to_string();
    console.log("Faucet account ID:", faucetIdHex);

    // 5. Mint tokens to Alice
    await client.fetch_and_cache_account_auth_by_pub_key(
      AccountId.from_hex(faucetIdHex),
    );
    await client.sync_state();

    console.log("Minting tokens to Alice...");
    await client.new_mint_transaction(
      AccountId.from_hex(aliceIdHex),  // target wallet id
      AccountId.from_hex(faucetIdHex), // faucet id
      NoteType.public(),               // note type
      BigInt(1000),                    // amount
    );

    console.log("Waiting 15 seconds for transaction confirmation...");
    await new Promise((resolve) => setTimeout(resolve, 15000));
    await client.sync_state();

    // 6. Fetch minted notes
    await client.fetch_and_cache_account_auth_by_pub_key(
      AccountId.from_hex(aliceIdHex),
    );

    const mintedNotes = await client.get_consumable_notes(
      AccountId.from_hex(aliceIdHex),
    );
    const mintedNoteIds = mintedNotes.map((n) =>
      n.input_note_record().id().to_string(),
    );
    console.log("Minted note IDs:", mintedNoteIds);

    // 7. Consume minted notes
    console.log("Consuming minted notes...");
    await client.new_consume_transaction(
      AccountId.from_hex(aliceIdHex), // account id
      mintedNoteIds,                  // array of note ids to consume
    );
    await client.sync_state();
    console.log("Notes consumed.");

    // 8. Send tokens to a dummy account
    const dummyIdHex = "0x599a54603f0cf9000000ed7a11e379";
    console.log("Sending tokens to dummy account...");
    await client.new_send_transaction(
      AccountId.from_hex(aliceIdHex),  // sender account id
      AccountId.from_hex(dummyIdHex),  // receiver account id
      AccountId.from_hex(faucetIdHex), // faucet account id
      NoteType.public(),               // note type
      BigInt(100),                     // amount
    );
    await client.sync_state();
    console.log("Tokens sent.");
  } catch (error) {
    console.error("Error:", error);
    throw error;
  }
}

Let's run the src/webClient.ts function again. Reload the page and click "Start WebClient".

Note: Currently there is a minor bug in the WebClient that produces a warning message, "Error inserting code with root" when creating multiple accounts. This is currently being fixed.

The output will look like this:

Latest block number: 4807
Alice's account ID: 0x1a20f4d1321e681000005020e69b1a
Creating faucet...
Faucet account ID: 0xaa86a6f05ae40b2000000f26054d5d
Minting tokens to Alice...
Waiting 15 seconds for transaction confirmation...
Minted note IDs: ['0x4edbb3d5dbdf6944f229a4711533114e0602ad48b70cda400993925c61f5bfaa']
Consuming minted notes...
Notes consumed.
Sending tokens to dummy account...
Tokens sent.

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:

(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.");
})();

Running the example

To run a full working example navigate to the web-client directory in the miden-tutorials repository and run the web application example:

cd web-client
pnpm i
pnpm run dev