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.
-
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
-
Install the Miden client.
cargo install miden-cli --features concurrent
You can now use the
miden --version
command, and you should seeMiden 0.7.0
. -
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
-
Create a new account of type
mutable
using the following command:miden new-wallet --mutable
-
List all created accounts by running the following command:
miden account -l
You should see something like this:
Save the account ID for a future step.
Request tokens from the public faucet
-
To request funds from the faucet navigate to the following website: Miden faucet website.
-
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. -
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.
-
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. -
Save this file on your computer, you will need it for the next step.
Import the note into the Miden client
-
Import the private note that you have received using the following commands:
miden import <path-to-note>/note.mno
-
You should see something like this:
Successfully imported note 0x0ff340133840d35e95e0dc2e62c88ed75ab2e383dc6673ce0341bd486fed8cb6
-
Now that the note has been successfully imported, you can view the note's information using the following command:
miden notes
-
You should see something like this:
Tip: The importance of syncing
- As you can see, the note is listed as
Expected
.- This is because you have received a private note but have not yet synced your view of the rollup to check that the note is the result of a valid transaction.
- Hence, before consuming the note we will need to update our view of the rollup by syncing.
- Many users could have received the same private note, but only one user can consume the note in a transaction that gets verified by the Miden operator.
Sync the client
Do this periodically to keep informed about any updates on the node by running the sync
command:
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
-
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
-
You should see something like this:
-
Find your account and note id by listing both
accounts
andnotes
:miden account miden notes
-
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>
-
You should see a confirmation message like this:
-
After confirming you can view the new note status by running the following command:
miden notes
-
You should see something like this:
-
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
-
After syncing, you should have received confirmation of the consumed note. You should see the note as
Consumed
after listing the notes:miden notes
Amazing! You just have created a client-side zero-knowledge proof locally on your machine and submitted it to the Miden rollup.
Tip You only need to copy the top line of characters of the Note ID.
View confirmations
-
View your updated account's vault containing the tokens sent by the faucet by running the following command:
miden account --show <Account-Id>
-
You should now see your accounts vault containing the funds sent by the faucet.
Congratulations!
You have successfully configured and used the Miden client to interact with a Miden rollup and faucet.
You have performed basic Miden rollup operations like submitting proofs of transactions, generating and consuming notes.
For more information on the Miden client, refer to the Miden client documentation.
Debugging tips (clear state and folder)
-
Need a fresh start? All state is maintained in
store.sqlite3
, located in the directory defined in themiden-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 yourmiden-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
- You should have already followed the prerequisite steps and get started documents.
- You should not have reset the state of your local client.
Create a second account
Tip Remember to use the Miden client documentation for clarifications.
-
Create a second account to send funds with. Previously, we created a type
mutable
account (account A). Now, create anothermutable
(account B) using the following command:miden new-wallet --mutable
-
List and view the newly created accounts with the following command:
miden account -l
-
You should see two accounts:
Transfer assets between accounts
-
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 containing50
assets, transferred from one account to the other. -
First, sync the accounts.
miden sync
-
Get the second note id.
miden notes
-
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
toAccount B
. -
Check the second account:
miden account --show <regular-account-ID-B>
-
Check the original account:
miden account --show <regular-account-ID-A>
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
- You should have already followed the prerequisite steps and get started documents.
- You should have not reset the state of your local client.
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
).
-
Create a new directory to store the new client.
mkdir miden-client-2 cd miden-client-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
-
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. -
List and view the account with the following command:
miden account -l
Transfer assets between accounts
-
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 containing50
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. -
First, sync the account on the new client.
miden sync
-
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.
-
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:
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
- Rust installation minimum version 1.82.
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
-
Make sure you have already installed the client. If you don't have a
miden-client.toml
file in your directory, create one or runmiden 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
-
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
Flags | Description | Short Flag |
---|---|---|
--list | List 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
Flags | Description | Short 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
Flag | Description | Aliases |
---|---|---|
--list | List 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
Command | Description | Aliases |
---|---|---|
--list | List tracked transactions | -l |
After a transaction gets executed, two entities start being tracked:
- The transaction itself: It follows a lifecycle from
Pending
(initial state) andCommitted
(after the node receives it). It may also beDiscarded
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 .
Flag | Description | Aliases |
---|---|---|
--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 onmiden
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 totrue
, 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
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
Account
The primary entity of the Miden protocol
What is the purpose of an account?
In Miden's hybrid UTXO and account-based model Account
s 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:
These components are:
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:
- Account type: basic or faucet.
- Account storage mode: public or private.
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 Account
s 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:
- Alice generates a new
Account
ID locally (according to the desiredAccount
type) using the Miden client. - The Miden client checks with a Miden node to ensure the ID does not already exist.
- Alice shares the new ID with Bob (for example, to receive assets).
- Bob executes a transaction, creating a note containing assets for Alice.
- Alice consumes Bob’s note in her own transaction to claim the asset.
- Depending on the
Account
’s storage mode and transaction type, the operator receives the newAccount
ID and, if all conditions are met, includes it in theAccount
database.
Additional information
Account type
There are two main categories of Account
s 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:
Type and mutability are encoded in the two most significant bits of the Account
's ID.
Account storage mode
Users can choose whether their Account
s are stored publicly or privately. The preference is encoded in the third and forth most significant bits of the Account
s ID:
-
Public
Account
s:
TheAccount
’s state is stored on-chain, similar to howAccount
s are stored in public blockchains like Ethereum. Contracts that rely on a shared, publicly accessible state (e.g., a DEX) should be public. -
Private
Account
s:
Only a commitment (hash) to theAccount
’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 theirAccount
. To interact with a privateAccount
, 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 Note
s 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:
These components are:
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 Note
s 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 Note
s 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.
Note
s 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
The Note
lifecycle proceeds through four primary phases: creation, validation, discovery, and consumption. Throughout this process, Note
s function as secure, privacy-preserving vehicles for asset transfers and logic execution.
Note creation
Accounts can create Note
s in a transaction. The Note
exists if it is included in the global Note
s DB.
- Users: Executing local or network transactions.
- Miden operators: Facilitating on-chain actions, e.g. such as executing user
Note
s against a DEX or other contracts.
Note storage mode
As with accounts, Note
s 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. TheNote
’s actual data remains off-chain, enhancing privacy.
Ephemeral note
These specific Note
s 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, Note
s become “live” and eligible for discovery and eventual consumption.
Note discovery
Clients often need to find specific Note
s 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 Note
s 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 Note
s 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 Note
s.
Upon successful verification of the transaction:
- The Miden operator records the
Note
’s nullifier as “consumed” in the nullifier database. - 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 Note
s, 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.
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, Note
s 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, Asset
s serve as the primary means of expressing and transferring value between accounts through notes. They are designed with four key principles in mind:
-
Parallelizable exchange:
By managing ownership and transfers directly at the account level instead of relying on global structures like ERC20 contracts, accounts can exchangeAsset
s concurrently, boosting scalability and efficiency. -
Self-sovereign ownership:
Asset
s are stored in the accounts directly. This ensures that users retain complete control over theirAsset
s. -
Censorship resistance:
Users can transact freely and privately with no single contract or entity controllingAsset
transfers. This reduces the risk of censored transactions, resulting in a more open and resilient system. -
Flexible fee payment:
Unlike protocols that require a specific baseAsset
for fees, Miden allows users to pay fees in any supportedAsset
. 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 Asset
s 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 Asset
s as defined at account creation. The faucet's code specifies the Asset
minting conditions: i.e., how, when, and by whom these Asset
s can be minted. Once minted, they can be transferred to other accounts using notes.
Type
Fungible asset
Fungible Asset
s 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 Asset
s 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 Asset
s. 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.
Burning
Asset
s 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.
Tip
- The transaction executor prepares, executes, and proves transactions.
- The executor compiles the transaction kernel plus user-defined notes and transaction scripts into a single executable program for the Miden VM.
- Users write scripts using kernel procedures and contexts.
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.
{ 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.
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 consumenote1
. 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 fortransaction2
. Moreover, we can mix and match. For example,transaction1
can be executed locally, buttransaction2
can be executed on the network, or vice versa.
Local and 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:
- They are cheaper (i.e., lower fees) as zk-proofs are already generated by the clients.
- They allow fairly complex computations because the proof size doesn't grow linearly with the complexity of the computation.
- 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:
- Clients may not have sufficient resources to generate zk-proofs.
- 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:
- Fetch the data required to execute a transaction from the data store.
- Compile the transaction into an executable MASM program using the transaction compiler.
- Execute the transaction program and create an
ExecutedTransaction
object. - Prove the
ExecutedTransaction
using the transaction prover.
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 theChainMmr
-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
- Learn more about Miden transaction procedures and contexts.
The kernel has a well-defined structure which does the following:
- The prologue prepares the transaction for processing by parsing the transaction data and setting up the root context.
- Note processing executes the note processing loop which consumes each
InputNote
and invokes the note script of each note. - Transaction script processing executes the optional transaction script.
- 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.
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:
- Unhashes the inputs and lays them out in the root context memory.
- Builds a single vault (transaction vault) containing assets of all inputs (input notes and initial account state).
- 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.
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:
- Computes the final account hash.
- Asserts that the final account nonce is greater than the initial nonce if the account has changed.
- Computes the output notes commitment.
- Asserts that input and output vault roots are equal (except for special accounts like faucets).
Outputs
The transaction kernel program outputs:
- The transaction script root.
- A commitment of all newly created output notes.
- 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
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 fourfelts
. If lowercase, the variable is represented by a singlefelt
.
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 name | Stack | Output | Context | Description |
---|---|---|---|---|
get_id | [] | [acct_id] | account, note |
|
get_nonce | [] | [nonce] | account, note |
|
get_initial_hash | [] | [H] | account, note |
|
get_current_hash | [] | [ACCT_HASH] | account, note |
|
incr_nonce | [value] | [] | account |
|
get_item | [index] | [VALUE] | account, note |
|
set_item | [index, V'] | [R', V] | account |
|
set_code | [CODE_COMMITMENT] | [] | account |
|
get_balance | [faucet_id] | [balance] | account, note |
|
has_non_fungible_asset | [ASSET] | [has_asset] | account, note |
|
add_asset | [ASSET] | [ASSET'] | account |
|
remove_asset | [ASSET] | [ASSET] | account |
|
get_vault_commitment | [] | [COM] | account, note |
|
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 name | Inputs | Outputs | Context | Description |
---|---|---|---|---|
get_assets | [dest_ptr] | [num_assets, dest_ptr] | note |
|
get_inputs | [dest_ptr] | [dest_ptr] | note |
|
get_sender | [] | [sender] | note |
|
compute_inputs_hash | [inputs_ptr, num_inputs] | [HASH] | note |
|
get_note_serial_number | [] | [SERIAL_NUMBER] | note |
|
get_script_hash | [] | [SCRIPT_HASH] | note |
|
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 name | Inputs | Outputs | Context | Description |
---|---|---|---|---|
get_block_number | [] | [num] | account, note |
|
get_block_hash | [] | [H] | account, note |
|
get_input_notes_commitment | [] | [COM] | account, note |
|
get_output_notes_commitment | [0, 0, 0, 0] | [COM] | account, note |
|
create_note | [ASSET, tag, RECIPIENT] | [ptr] | account |
|
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 name | Stack | Output | Context | Description |
---|---|---|---|---|
build_fungible_asset | [faucet_id, amount] | [ASSET] | faucet |
|
create_fungible_asset | [amount] | [ASSET] | faucet |
|
build_non_fungible_asset | [faucet_id, DATA_HASH] | [ASSET] | faucet |
|
create_non_fungible_asset | [DATA_HASH] | [ASSET] | faucet |
|
Faucet
To import the faucet procedures, set use.miden::faucet
at the beginning of the file.
Procedure name | Stack | Outputs | Context | Description |
---|---|---|---|---|
mint | [ASSET] | [ASSET] | faucet |
|
burn | [ASSET] | [ASSET] | faucet |
|
get_total_issuance | [] | [total_issuance] | 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
:
- Accounts
- Notes
- Nullifiers
Account database
The accounts database has two main purposes:
- Track state commitments of all accounts
- Store account data for public accounts
This is done using an authenticated data structure, a sparse Merkle tree.
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 thisState
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:
- Membership witnesses (that a note exists in the database) against such an accumulator needs to be updated very infrequently.
- 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
.
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.
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:
In this diagram, multiple participants interact with a common, publicly accessible State
. The figure illustrates how notes are created and consumed:
-
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.
-
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 unifiedState
update. -
Further Independent Transactions (tx4 & tx5):
After the sharedState
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.
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:
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 to0x456..
) 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:
- Compute hashes of public account and note states.
- Make sure these hashes match records in the state updates section.
- 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:
- Download only the first part of the blocks (i.e., without full account/note states) starting at the genesis up until the latest block.
- 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.
- Download the current states of account, note, and nullifier databases.
- 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:
- Run the Miden node locally
- Connect to the Miden testnet
Prerequisites
To run miden-node
locally, you need to:
- Install the
miden-node
crate. - Provide a
genesis.toml
file. - 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 themiden-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 themiden-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:
- Pushes
0
onto the stack, representing the index of the storage slot to read. - Calls
account::get_item
with the index of0
. - Pushes
1
onto the stack. - Adds
1
to the count value returned fromaccount::get_item
. - For demonstration purposes, calls
debug.stack
to see the state of the stack - Pushes
0
onto the stack, which is the index of the storage slot we want to write to. - Calls
account::set_item
which saves the incremented count to storage at index0
- 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