Creating Notes in Miden Assembly
Creating notes inside the MidenVM using Miden assembly
Overview
In this tutorial, we will create a custom note that generates a copy of itself when it is consumed by an account. The purpose of this tutorial is to demonstrate how to create notes inside the MidenVM using Miden assembly (MASM). By the end of this tutorial, you will understand how to write MASM code that creates notes.
What We'll Cover
- Computing the note inputs commitment in MASM
- Creating notes in MASM
Prerequisites
This tutorial assumes you have a basic understanding of Miden assembly and that you have completed the tutorial on creating a custom note.
Why Creating Notes in MASM Is Useful
Being able to create a note in MASM enables you to build various types of applications. Creating a note during the consumption of another note or from an account allows you to develop complex DeFi applications.
Here are some tangible examples of when creating a note in MASM is useful in a DeFi context:
- Creating snapshots of an account's state at a specific point in time (not possible in an EVM context)
- Representing partially fillable buy/sell orders as notes (SWAPP)
- Handling withdrawals from a smart contract
What We Will Be Building
In the diagram above, note A is consumed by an account, and during the transaction, note A' is created.
In this tutorial, we will create a note that contains an asset. When consumed, it outputs a copy of itself and allows the consuming account to take half of the asset. Although this type of note would not be used in a real-world context, it demonstrates several key concepts for writing MASM code that can create notes.
Step 1: Initialize Your Repository
Create a new Rust repository for your Miden project and navigate to it with the following command:
cargo new miden-project
cd miden-project
Add the following dependencies to your Cargo.toml
file:
[dependencies]
miden-client = { version = "0.10.0", features = ["testing", "tonic", "sqlite"] }
miden-lib = { version = "0.10.0", default-features = false }
miden-objects = { version = "0.10.0", default-features = false }
miden-crypto = { version = "0.15.0", features = ["executable"] }
miden-assembly = "0.15.0"
rand = { version = "0.9" }
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.9.0"
Step 2: Write the Note Script
For better code organization, we will separate the Miden assembly code from our Rust code.
Create a directory named masm
at the root of your miden-project
directory. This directory will contain our contract and MASM script code.
Initialize the masm
directory:
mkdir masm/notes
This will create:
masm/
└── notes/
Inside the masm/notes/
directory, create the file iterative_output_note.masm
:
use.miden::note
use.miden::tx
use.std::sys
use.std::crypto::hashes::rpo
use.miden::contracts::wallets::basic->wallet
# Memory Addresses
const.ASSET=0
const.ASSET_HALF=4
const.ACCOUNT_ID_PREFIX=8
const.ACCOUNT_ID_SUFFIX=9
const.TAG=10
# => []
begin
# Drop word if user accidentally pushes note_args
dropw
# => []
# Get note inputs
push.ACCOUNT_ID_PREFIX exec.note::get_inputs drop drop
# => []
# Get asset contained in note
push.ASSET exec.note::get_assets drop drop
# => []
mem_loadw.ASSET
# => [ASSET]
# Compute half amount of asset
swap.3 push.2 div swap.3
# => [ASSET_HALF]
mem_storew.ASSET_HALF dropw
# => []
mem_loadw.ASSET
# => [ASSET]
# Receive the entire asset amount to the wallet
call.wallet::receive_asset
# => []
# Get note inputs commitment
push.8.ACCOUNT_ID_PREFIX
# => [memory_address_pointer, number_of_inputs]
# Note: Must pad with 0s to nearest multiple of 8
exec.rpo::hash_memory
# => [INPUTS_COMMITMENT]
# Push script hash
exec.note::get_script_root
# => [SCRIPT_HASH, INPUTS_COMMITMENT]
# Get the current note serial number
exec.note::get_serial_number
# => [SERIAL_NUM, SCRIPT_HASH, INPUTS_COMMITMENT]
# Increment serial number by 1
push.1 add
# => [SERIAL_NUM+1, SCRIPT_HASH, INPUTS_COMMITMENT]
exec.tx::build_recipient_hash
# => [RECIPIENT]
# Push hint, note type, and aux to stack
push.1.1.0
# => [aux, public_note, execution_hint_always, RECIPIENT]
# Load tag from memory
mem_load.TAG
# => [tag, aux, note_type, execution_hint, RECIPIENT]
call.tx::create_note
# => [note_idx, pad(15) ...]
padw mem_loadw.ASSET_HALF
# => [ASSET / 2, note_idx]
call.wallet::move_asset_to_note
# => [ASSET, note_idx, pad(11)]
dropw drop
# => []
exec.sys::truncate_stack
# => []
end
How the Assembly Code Works:
- Reads note inputs:
The note begins by writing the note inputs to memory by calling thenote::get_inputs
procedure. It writes the note inputs starting at memory address 8, which is defined as the constantACCOUNT_ID_PREFIX
. - Retrieving the asset:
The note then callsnote::get_assets
to write the asset contained in the note to memory address 0, defined asASSET
. It computes half of the asset and stores the value at memory address 4, defined asASSET_HALF
. Finally, the note calls thewallet::receive_asset
procedure to move the asset contained in the note to the consuming account. - Computing note inputs hash in MASM:
The script calls thenote::compute_inputs_hash
procedure with the number of inputs and the memory address where the inputs begin. This procedure returns the note inputs commitment. - Getting the script hash:
Next, the note script calls thenote::get_script_hash
procedure, which returns the note's script hash. - Getting the serial number for the future note:
Although not strictly necessary in this scenario, preventing two identical notes from having the same serial number is important. If an account creates two identical notes with the same serial number, recipient, and asset vault, one of the notes may not be consumed. Therefore, the MASM code increments the serial number of the current note by 1. - Computing the
RECIPIENT
hash:
TheRECIPIENT
hash is defined as:
hash(hash(hash(serial_num, [0; 4]), script_root), input_commitment)
To compute it in MASM, the script calls thetx::build_recipient_hash
procedure with the serial number, script hash, and inputs commitment on the stack. - Creating the note:
To create the note, the script pushes the execution hint, note type, aux value, and tag onto the stack, then calls thewallet::create_note
procedure, which returns a pointer to the note. - Moving assets to the note:
After the note is created, the script loads the half asset value computed in step 2 onto the stack and calls thewallet::move_asset_to_note
procedure. - Stack cleanup:
Finally, the script cleans up the stack by callingsys::truncate_stack
after creating the note and adding the assets.
Step 3: Rust Program
With the Miden assembly note script written, we can move on to writing the Rust script to create and consume the note.
Copy and paste the following code into your src/main.rs
file.
use rand::{prelude::StdRng, RngCore};
use std::{fs, path::Path, sync::Arc};
use tokio::time::{sleep, Duration};
use miden_client::{
account::{
component::{BasicFungibleFaucet, BasicWallet, RpoFalcon512},
AccountBuilder, AccountStorageMode, AccountType,
},
asset::{FungibleAsset, TokenSymbol},
auth::AuthSecretKey,
builder::ClientBuilder,
crypto::{FeltRng, SecretKey},
keystore::FilesystemKeyStore,
note::{
Note, NoteAssets, NoteExecutionHint, NoteExecutionMode, NoteInputs, NoteMetadata,
NoteRecipient, NoteScript, NoteTag, NoteType,
},
rpc::{Endpoint, TonicRpcClient},
transaction::{OutputNote, TransactionKernel, TransactionRequestBuilder},
Client, ClientError, Felt,
};
use miden_objects::account::NetworkId;
use miden_objects::note::NoteDetails;
// Helper to create a basic account
async fn create_basic_account(
client: &mut Client,
keystore: FilesystemKeyStore<StdRng>,
) -> Result<miden_client::account::Account, ClientError> {
let mut init_seed = [0u8; 32];
client.rng().fill_bytes(&mut init_seed);
let key_pair = SecretKey::with_rng(client.rng());
let builder = AccountBuilder::new(init_seed)
.account_type(AccountType::RegularAccountUpdatableCode)
.storage_mode(AccountStorageMode::Public)
.with_auth_component(RpoFalcon512::new(key_pair.public_key()))
.with_component(BasicWallet);
let (account, seed) = builder.build().unwrap();
client.add_account(&account, Some(seed), false).await?;
keystore
.add_key(&AuthSecretKey::RpoFalcon512(key_pair))
.unwrap();
Ok(account)
}
async fn create_basic_faucet(
client: &mut Client,
keystore: FilesystemKeyStore<StdRng>,
) -> Result<miden_client::account::Account, ClientError> {
let mut init_seed = [0u8; 32];
client.rng().fill_bytes(&mut init_seed);
let key_pair = SecretKey::with_rng(client.rng());
let symbol = TokenSymbol::new("MID").unwrap();
let decimals = 8;
let max_supply = Felt::new(1_000_000);
let builder = AccountBuilder::new(init_seed)
.account_type(AccountType::FungibleFaucet)
.storage_mode(AccountStorageMode::Public)
.with_auth_component(RpoFalcon512::new(key_pair.public_key()))
.with_component(BasicFungibleFaucet::new(symbol, decimals, max_supply).unwrap());
let (account, seed) = builder.build().unwrap();
client.add_account(&account, Some(seed), false).await?;
keystore
.add_key(&AuthSecretKey::RpoFalcon512(key_pair))
.unwrap();
Ok(account)
}
// Helper to wait until an account has the expected number of consumable notes
async fn wait_for_notes(
client: &mut Client,
account_id: &miden_client::account::Account,
expected: usize,
) -> Result<(), ClientError> {
loop {
client.sync_state().await?;
let notes = client.get_consumable_notes(Some(account_id.id())).await?;
if notes.len() >= expected {
break;
}
println!(
"{} consumable notes found for account {}. Waiting...",
notes.len(),
account_id.id().to_bech32(NetworkId::Testnet)
);
sleep(Duration::from_secs(3)).await;
}
Ok(())
}
#[tokio::main]
async fn main() -> Result<(), ClientError> {
// Initialize client & keystore
let endpoint = Endpoint::testnet();
let timeout_ms = 10_000;
let rpc_api = Arc::new(TonicRpcClient::new(&endpoint, timeout_ms));
let mut client = ClientBuilder::new()
.rpc(rpc_api)
.filesystem_keystore("./keystore")
.in_debug_mode(true)
.build()
.await?;
let sync_summary = client.sync_state().await.unwrap();
println!("Latest block: {}", sync_summary.block_num);
let keystore: FilesystemKeyStore<rand::prelude::StdRng> =
FilesystemKeyStore::new("./keystore".into()).unwrap();
// -------------------------------------------------------------------------
// STEP 1: Create accounts and deploy faucet
// -------------------------------------------------------------------------
println!("\n[STEP 1] Creating new accounts");
let alice_account = create_basic_account(&mut client, keystore.clone()).await?;
println!(
"Alice's account ID: {:?}",
alice_account.id().to_bech32(NetworkId::Testnet)
);
let bob_account = create_basic_account(&mut client, keystore.clone()).await?;
println!(
"Bob's account ID: {:?}",
bob_account.id().to_bech32(NetworkId::Testnet)
);
println!("\nDeploying a new fungible faucet.");
let faucet = create_basic_faucet(&mut client, keystore.clone()).await?;
println!(
"Faucet account ID: {:?}",
faucet.id().to_bech32(NetworkId::Testnet)
);
client.sync_state().await?;
// -------------------------------------------------------------------------
// STEP 2: Mint tokens with P2ID
// -------------------------------------------------------------------------
println!("\n[STEP 2] Mint tokens with P2ID");
let faucet_id = faucet.id();
let amount: u64 = 100;
let mint_amount = FungibleAsset::new(faucet_id, amount).unwrap();
let tx_req = TransactionRequestBuilder::new()
.build_mint_fungible_asset(
mint_amount,
alice_account.id(),
NoteType::Public,
client.rng(),
)
.unwrap();
let tx_exec = client.new_transaction(faucet.id(), tx_req).await?;
client.submit_transaction(tx_exec.clone()).await?;
let p2id_note = if let OutputNote::Full(note) = tx_exec.created_notes().get_note(0) {
note.clone()
} else {
panic!("Expected OutputNote::Full");
};
wait_for_notes(&mut client, &alice_account, 1).await?;
let consume_req = TransactionRequestBuilder::new()
.authenticated_input_notes([(p2id_note.id(), None)])
.build()
.unwrap();
let tx_exec = client
.new_transaction(alice_account.id(), consume_req)
.await?;
client.submit_transaction(tx_exec).await?;
client.sync_state().await?;
// -------------------------------------------------------------------------
// STEP 3: Create iterative output note
// -------------------------------------------------------------------------
println!("\n[STEP 3] Create iterative output note");
let assembler = TransactionKernel::assembler().with_debug_mode(true);
let code = fs::read_to_string(Path::new("../masm/notes/iterative_output_note.masm")).unwrap();
let rng = client.rng();
let serial_num = rng.draw_word();
// Create note metadata and tag
let tag = NoteTag::for_public_use_case(0, 0, NoteExecutionMode::Local).unwrap();
let metadata = NoteMetadata::new(
alice_account.id(),
NoteType::Public,
tag,
NoteExecutionHint::always(),
Felt::new(0),
)?;
let note_script = NoteScript::compile(code, assembler.clone()).unwrap();
let note_inputs = NoteInputs::new(vec![
alice_account.id().prefix().as_felt(),
alice_account.id().suffix(),
tag.into(),
Felt::new(0),
])
.unwrap();
let recipient = NoteRecipient::new(serial_num, note_script.clone(), note_inputs.clone());
let vault = NoteAssets::new(vec![mint_amount.into()])?;
let custom_note = Note::new(vault, metadata, recipient);
let note_req = TransactionRequestBuilder::new()
.own_output_notes(vec![OutputNote::Full(custom_note.clone())])
.build()
.unwrap();
let tx_result = client
.new_transaction(alice_account.id(), note_req)
.await
.unwrap();
println!(
"View transaction on MidenScan: https://testnet.midenscan.com/tx/{:?}",
tx_result.executed_transaction().id()
);
let _ = client.submit_transaction(tx_result).await;
client.sync_state().await?;
// -------------------------------------------------------------------------
// STEP 4: Consume the iterative output note
// -------------------------------------------------------------------------
println!("\n[STEP 4] Bob consumes the note and creates a copy");
// Increment the serial number for the new note
let serial_num_1 = [
serial_num[0],
serial_num[1],
serial_num[2],
Felt::new(serial_num[3].as_int() + 1),
];
// Reuse the note_script and note_inputs
let recipient = NoteRecipient::new(serial_num_1, note_script, note_inputs);
// Note: Change metadata to include Bob's account as the creator
let metadata = NoteMetadata::new(
bob_account.id(),
NoteType::Public,
tag,
NoteExecutionHint::always(),
Felt::new(0),
)?;
let asset_amount_1 = FungibleAsset::new(faucet_id, 50).unwrap();
let vault = NoteAssets::new(vec![asset_amount_1.into()])?;
let output_note = Note::new(vault, metadata, recipient);
let consume_custom_req = TransactionRequestBuilder::new()
.unauthenticated_input_notes([(custom_note, None)])
.own_output_notes(vec![(OutputNote::Full(output_note))])
.build()
.unwrap();
let tx_result = client
.new_transaction(bob_account.id(), consume_custom_req)
.await
.unwrap();
println!(
"Consumed Note Tx on MidenScan: https://testnet.midenscan.com/tx/{:?}",
tx_result.executed_transaction().id()
);
println!("Account delta: {:?}", tx_result.account_delta().vault());
let _ = client.submit_transaction(tx_result).await;
Ok(())
}
Run the following command to execute src/main.rs
:
cargo run --release
The output will look something like this:
Latest block: 226933
[STEP 1] Creating new accounts
Alice's account ID: "mtst1qpljtarjtawzcyqqqdcqu53adytw09yw"
Bob's account ID: "mtst1qzaynsxth84vsyqqq0emse6ygcax3j59"
Deploying a new fungible faucet.
Faucet account ID: "mtst1qpqpq6z8vrqvugqqqwjdnajgvurs9zgl"
[STEP 2] Mint tokens with P2ID
0 consumable notes found for account mtst1qpljtarjtawzcyqqqdcqu53adytw09yw. Waiting...
0 consumable notes found for account mtst1qpljtarjtawzcyqqqdcqu53adytw09yw. Waiting...
[STEP 3] Create iterative output note
View transaction on MidenScan: https://testnet.midenscan.com/tx/0x335061a434ccccbf9619052bdeacbc71b6e755b24f0ead0c3741a3b0954c78af
[STEP 4] Bob consumes the note and creates a copy
Consumed Note Tx on MidenScan: https://testnet.midenscan.com/tx/0xa39acf2bb965b4669b91bf564e8aa2987a9fc86ee350cd159ad5db1054cb67ab
Account delta: AccountVaultDelta { fungible: FungibleAssetDelta({V0(Accoun
Running the example
To run the full example, navigate to the rust-client
directory in the miden-tutorials repository and run this command:
cd rust-client
cargo run --release --bin note_creation_in_masm
Continue learning
Next tutorial: Delegated Proving