Skip to main content
Version: 0.13 (unstable)

Part 7: Creating Output Notes

In this section, you'll learn how to create output notes from within account methods. We'll implement the full withdrawal logic that creates P2ID (Pay-to-ID) notes to send assets back to depositors.

What You'll Build in This Part

By the end of this section, you will have:

  • Created the withdraw-request-note note script project
  • Implemented the withdraw() method with balance validation
  • Implemented create_p2id_note() for sending assets
  • Verified withdrawals work via a MockChain test

Building on Part 6

In Part 6, you created a transaction script for initialization. Now you'll complete the bank by implementing withdrawals that create output notes:

┌────────────────────────────────────────────────────────────────┐
│ Complete Bank Flow │
├────────────────────────────────────────────────────────────────┤
│ │
│ Part 6: Initialize │
│ ┌─────────────────┐ init-tx-script ┌───────────────┐ │
│ │ Bank (uninit) │ ──────────────────────▶│ Bank (ready) │ │
│ └─────────────────┘ └───────────────┘ │
│ │
│ Part 4: Deposit │
│ ┌─────────────────┐ deposit-note ┌───────────────┐ │
│ │ User sends │ ──────────────────────▶│ Balance += X │ │
│ │ deposit note │ │ Vault += X │ │
│ └─────────────────┘ └───────────────┘ │
│ │
│ Part 7: Withdraw (NEW) │
│ ┌─────────────────┐ withdraw-request ┌───────────────┐ │
│ │ User sends │ ──────────────────────▶│ Balance -= X │ │
│ │ withdraw note │ │ Creates P2ID │ │
│ └─────────────────┘ │ output note │ │
│ └───────────────┘ │
│ │
└────────────────────────────────────────────────────────────────┘

Output Notes Overview

When an account needs to send assets to another account, it creates an output note. The note travels through the network until the recipient consumes it.

WITHDRAW FLOW:
┌────────────────┐ ┌────────────────┐ ┌────────────────┐
│ Bank Account │ creates │ P2ID Note │ consumed │ Depositor │
│ │ ────────▶│ (with assets) │ ────────▶│ Wallet │
│ remove_asset() │ │ │ │ receives asset │
└────────────────┘ └────────────────┘ └────────────────┘

The P2ID Note Pattern

P2ID (Pay-to-ID) is a standard note pattern in Miden that sends assets to a specific account:

  • Target account: Only one account can consume the note
  • Asset transfer: Assets are transferred on consumption
  • Standard script: Uses a well-known script from miden-lib

Step 1: Add Withdraw Method to Bank Account

First, let's add the withdraw() method to your bank account. Update contracts/bank-account/src/lib.rs:

contracts/bank-account/src/lib.rs
#[component]
impl Bank {
// ... existing methods (initialize, deposit, get_balance) ...

/// Withdraw assets back to the depositor.
///
/// Creates a P2ID note that sends the requested asset to the depositor's account.
///
/// # Arguments
/// * `depositor` - The AccountId of the user withdrawing
/// * `withdraw_asset` - The fungible asset to withdraw
/// * `serial_num` - Unique serial number for the P2ID output note
/// * `tag` - The note tag for the P2ID output note (allows caller to specify routing)
/// * `aux` - Auxiliary data for the note (application-specific, typically 0)
/// * `note_type` - Note type: 1 = Public (stored on-chain), 2 = Private (off-chain)
///
/// # Panics
/// Panics if the withdrawal amount exceeds the depositor's current balance.
/// Panics if the bank has not been initialized.
pub fn withdraw(
&mut self,
depositor: AccountId,
withdraw_asset: Asset,
serial_num: Word,
tag: Felt,
aux: Felt,
note_type: Felt,
) {
// Ensure the bank is initialized before processing withdrawals
self.require_initialized();

// Extract the fungible amount from the asset
let withdraw_amount = withdraw_asset.inner[0];

// Create key from depositor's AccountId and asset faucet ID
let key = Word::from([
depositor.prefix,
depositor.suffix,
withdraw_asset.inner[3], // asset prefix (faucet)
withdraw_asset.inner[2], // asset suffix (faucet)
]);

// Get current balance and validate sufficient funds exist.
// This check is critical: Felt arithmetic is modular, so subtracting
// more than the balance would silently wrap to a large positive number.
let current_balance: Felt = self.balances.get(&key);
assert!(
current_balance.as_u64() >= withdraw_amount.as_u64(),
"Withdrawal amount exceeds available balance"
);

// Update balance: current - withdraw_amount
let new_balance = current_balance - withdraw_amount;
self.balances.set(key, new_balance);

// Create a P2ID note to send the requested asset back to the depositor
self.create_p2id_note(serial_num, &withdraw_asset, depositor, tag, aux, note_type);
}
}

Always validate current_balance >= withdraw_amount BEFORE subtraction. Miden uses modular field arithmetic - subtracting a larger value silently wraps to a massive positive number!

Step 2: Add the P2ID Note Root

The P2ID note uses a standard script from miden-lib. Add this helper function:

contracts/bank-account/src/lib.rs
#[component]
impl Bank {
// ... other methods ...

/// Returns the P2ID note script root digest.
///
/// This is a constant value derived from the standard P2ID note script in miden-lib.
/// The digest is the MAST root of the compiled P2ID note script.
fn p2id_note_root() -> Digest {
Digest::from_word(Word::new([
Felt::from_u64_unchecked(15783632360113277539),
Felt::from_u64_unchecked(7403765918285273520),
Felt::from_u64_unchecked(15691985194755641846),
Felt::from_u64_unchecked(10399643920503194563),
]))
}
}

This digest is specific to miden-lib version. If the P2ID script changes in a future version, this digest must be updated.

Step 3: Implement create_p2id_note

Add the private method that creates the output note:

contracts/bank-account/src/lib.rs
#[component]
impl Bank {
// ... other methods ...

/// Create a P2ID (Pay-to-ID) note to send assets to a recipient.
///
/// # Arguments
/// * `serial_num` - Unique serial number for the note
/// * `asset` - The asset to include in the note
/// * `recipient_id` - The AccountId that can consume this note
/// * `tag` - The note tag (passed by caller to allow proper P2ID routing)
/// * `aux` - Auxiliary data for application-specific purposes
/// * `note_type` - Note type as Felt: 1 = Public, 2 = Private
fn create_p2id_note(
&mut self,
serial_num: Word,
asset: &Asset,
recipient_id: AccountId,
tag: Felt,
aux: Felt,
note_type: Felt,
) {
// Convert the passed tag Felt to a Tag
// The caller is responsible for computing the proper P2ID tag
// (typically LocalAny with account ID bits embedded)
let tag = Tag::from(tag);

// Convert note_type Felt to NoteType
// 1 = Public (stored on-chain), 2 = Private (off-chain)
let note_type = NoteType::from(note_type);

// Execution hint: always (standard P2ID behavior per miden-base)
// This is hardcoded to match miden-base's standard P2ID note implementation
// which uses NoteExecutionHint::always() - represented as 0 in Felt form
let execution_hint = felt!(0);

// Get the P2ID note script root digest
let script_root = Self::p2id_note_root();

// Compute the recipient hash from:
// - serial_num: unique identifier for this note instance
// - script_root: the P2ID note script's MAST root
// - inputs: the target account ID (padded to 8 elements)
//
// The P2ID script expects inputs as [suffix, prefix, 0, 0, 0, 0, 0, 0]
let recipient = Recipient::compute(
serial_num,
script_root,
vec![
recipient_id.suffix,
recipient_id.prefix,
felt!(0),
felt!(0),
felt!(0),
felt!(0),
felt!(0),
felt!(0),
],
);

// Create the output note
let note_idx = output_note::create(tag, aux, note_type, execution_hint, recipient);

// Remove the asset from the bank's vault
native_account::remove_asset(asset.clone());

// Add the asset to the output note
output_note::add_asset(asset.clone(), note_idx);
}
}

Understanding Recipient::compute()

ParameterDescription
serial_numUnique 4-Felt value preventing note reuse
script_rootThe P2ID script's MAST root digest
inputsScript inputs (account ID for P2ID)

Note the order: suffix comes before prefix. This is the opposite of how AccountId fields are typically accessed. See Common Pitfalls for details.

Understanding output_note::create()

ParameterTypeDescription
tagTagRouting information for the note
auxFeltAuxiliary data (application-specific)
note_typeNoteTypePublic (1) or Private (2)
execution_hintFeltWhen the note should execute
recipientRecipientWho can consume the note

Step 4: Create the Withdraw Request Note Project

Create the directory structure:

>_ Terminal
mkdir -p contracts/withdraw-request-note/src

Configure Cargo.toml

contracts/withdraw-request-note/Cargo.toml
[package]
name = "withdraw-request-note"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
miden = { workspace = true }

[package.metadata.component]
package = "miden:withdraw-request-note"

[package.metadata.miden]
project-kind = "note-script"

[package.metadata.miden.dependencies]
"miden:bank-account" = { path = "../bank-account" }

[package.metadata.component.target.dependencies]
"miden:bank-account" = { path = "../bank-account/target/generated-wit/" }

Update Workspace

Add to your root Cargo.toml:

Cargo.toml
[workspace]
resolver = "2"

members = [
"contracts/bank-account",
"contracts/deposit-note",
"contracts/init-tx-script",
"contracts/withdraw-request-note", # Add this line
"integration",
]

[workspace.dependencies]
miden = { version = "0.8" }

Step 5: Implement the Withdraw Request Note Script

contracts/withdraw-request-note/src/lib.rs
// Do not link against libstd (i.e. anything defined in `std::`)
#![no_std]
#![feature(alloc_error_handler)]

use miden::*;

// Import the bank account's generated bindings
use crate::bindings::miden::bank_account::bank_account;

/// Withdraw Request Note Script
///
/// When consumed by the Bank account, this note requests a withdrawal and
/// the bank creates a P2ID note to send assets back to the depositor.
///
/// # Flow
/// 1. Note is created by a depositor specifying the withdrawal details
/// 2. Bank account consumes this note
/// 3. Note script reads the sender (depositor) and inputs
/// 4. Calls `bank_account::withdraw(depositor, asset, serial_num, tag, aux, note_type)`
/// 5. Bank updates the depositor's balance
/// 6. Bank creates a P2ID note with the specified parameters to send assets back
///
/// # Note Inputs (11 Felts)
/// [0-3]: withdraw asset (amount, 0, faucet_suffix, faucet_prefix)
/// [4-7]: serial_num (random/unique per note)
/// [8]: tag (P2ID note tag for routing)
/// [9]: aux (auxiliary data, application-specific, typically 0)
/// [10]: note_type (1 = Public, 2 = Private)
#[note_script]
fn run(_arg: Word) {
// The depositor is whoever created/sent this note
let depositor = active_note::get_sender();

// Get the inputs
let inputs = active_note::get_inputs();

// Asset: [amount, 0, faucet_suffix, faucet_prefix]
let withdraw_asset = Asset::new(Word::from([inputs[0], inputs[1], inputs[2], inputs[3]]));

// Serial number: full 4 Felts (random/unique per note)
let serial_num = Word::from([inputs[4], inputs[5], inputs[6], inputs[7]]);

// Tag: single Felt for P2ID note routing
let tag = inputs[8];

// Aux: auxiliary data for application-specific purposes
let aux = inputs[9];

// Note type: 1 = Public, 2 = Private
let note_type = inputs[10];

// Call the bank account to withdraw the assets
bank_account::withdraw(depositor, withdraw_asset, serial_num, tag, aux, note_type);
}

Note Input Layout

The withdraw-request-note expects 11 Felt inputs:

Note Inputs (11 Felts):
┌───────────────────────────────────────────────────────────────────────────┐
│ Index │ Value │ Description │
├───────┼─────────────────┼─────────────────────────────────────────────────┤
│ 0 │ amount │ Token amount to withdraw │
│ 1 │ 0 │ Reserved (always 0 for fungible) │
│ 2 │ faucet_suffix │ Faucet ID suffix (identifies asset type) │
│ 3 │ faucet_prefix │ Faucet ID prefix (identifies asset type) │
│ 4-7 │ serial_num │ Unique ID for the output P2ID note (4 Felts) │
│ 8 │ tag │ Note routing tag for P2ID note │
│ 9 │ aux │ Auxiliary data (typically 0) │
│ 10 │ note_type │ 1 (Public) or 2 (Private) │
└───────────────────────────────────────────────────────────────────────────┘

Unlike the deposit note which gets assets from active_note::get_assets(), the withdraw request note doesn't carry assets. Instead, the asset to withdraw is specified in the note inputs. The bank then withdraws from its own vault based on these inputs.

Step 6: Build All Components

Build in dependency order:

>_ Terminal
# 1. Build the account component (generates WIT files)
cd contracts/bank-account
miden build

# 2. Build the withdraw request note
cd ../withdraw-request-note
miden build

Try It: Verify Withdrawals Work

Let's test the complete withdraw flow. This test:

  1. Creates a bank account and initializes it
  2. Creates a deposit note and processes it
  3. Creates a withdraw-request note with the 11-Felt input layout
  4. Processes the withdrawal and verifies a P2ID output note is created
integration/tests/part7_withdraw_test.rs
use integration::helpers::{
build_project_in_dir, create_testing_account_from_package, create_testing_note_from_package,
AccountCreationConfig, NoteCreationConfig,
};
use miden_client::{
account::StorageMap,
note::{Note, NoteAssets, NoteExecutionHint, NoteMetadata, NoteTag, NoteType},
transaction::OutputNote,
Felt, Word,
};
use miden_lib::note::utils::build_p2id_recipient;
use miden_objects::{
account::AccountId,
asset::{Asset, FungibleAsset},
transaction::TransactionScript,
};
use miden_testing::{Auth, MockChain};
use std::{path::Path, sync::Arc};

/// Compute a P2ID note tag for a local account.
fn compute_p2id_tag_for_local_account(account_id: AccountId) -> NoteTag {
const LOCAL_ANY_PREFIX: u32 = 0xC000_0000;
const TAG_BITS: u8 = 14;

let prefix_u64 = account_id.prefix().as_u64();
let shifted = (prefix_u64 >> 34) as u32;
let mask = u32::MAX << (30 - TAG_BITS);
let account_bits = shifted & mask;
let tag_value = LOCAL_ANY_PREFIX | account_bits;

NoteTag::LocalAny(tag_value)
}

#[tokio::test]
async fn test_withdraw_creates_p2id_note() -> anyhow::Result<()> {
// =========================================================================
// SETUP
// =========================================================================
let mut builder = MockChain::builder();

let deposit_amount: u64 = 1000;

// Create faucet and sender (depositor)
let faucet =
builder.add_existing_basic_faucet(Auth::BasicAuth, "TEST", deposit_amount, Some(10))?;
let sender = builder.add_existing_wallet_with_assets(
Auth::BasicAuth,
[FungibleAsset::new(faucet.id(), deposit_amount)?.into()],
)?;

// Build contracts
let bank_package = Arc::new(build_project_in_dir(
Path::new("../contracts/bank-account"),
true,
)?);
let deposit_note_package = Arc::new(build_project_in_dir(
Path::new("../contracts/deposit-note"),
true,
)?);
let init_tx_script_package = Arc::new(build_project_in_dir(
Path::new("../contracts/init-tx-script"),
true,
)?);
let withdraw_request_note_package = Arc::new(build_project_in_dir(
Path::new("../contracts/withdraw-request-note"),
true,
)?);

// Create bank account
let bank_cfg = AccountCreationConfig {
storage_slots: vec![
miden_client::account::StorageSlot::Value(Word::default()),
miden_client::account::StorageSlot::Map(StorageMap::with_entries([])?),
],
..Default::default()
};
let mut bank_account =
create_testing_account_from_package(bank_package.clone(), bank_cfg).await?;

// Create deposit note
let fungible_asset = FungibleAsset::new(faucet.id(), deposit_amount)?;
let note_assets = NoteAssets::new(vec![Asset::Fungible(fungible_asset)])?;
let deposit_note = create_testing_note_from_package(
deposit_note_package.clone(),
sender.id(),
NoteCreationConfig {
assets: note_assets,
..Default::default()
},
)?;

// Add accounts and notes to builder
builder.add_account(bank_account.clone())?;
builder.add_output_note(OutputNote::Full(deposit_note.clone().into()));

// =========================================================================
// CRAFT WITHDRAW REQUEST NOTE (11-Felt input layout)
// =========================================================================
let withdraw_amount = deposit_amount / 2;

// Compute P2ID tag for the sender
let p2id_tag = compute_p2id_tag_for_local_account(sender.id());
let p2id_tag_u32 = match p2id_tag {
NoteTag::LocalAny(v) => v,
_ => panic!("Expected LocalAny tag"),
};
let p2id_tag_felt = Felt::new(p2id_tag_u32 as u64);

// Serial number for output note
let p2id_output_note_serial_num = Word::from([
Felt::new(0x1234567890abcdef),
Felt::new(0xfedcba0987654321),
Felt::new(0xdeadbeefcafebabe),
Felt::new(0x0123456789abcdef),
]);

let aux = Felt::new(0);
let note_type_felt = Felt::new(1); // Public

// Note inputs: 11 Felts
// [0-3]: withdraw asset (amount, 0, faucet_suffix, faucet_prefix)
// [4-7]: serial_num
// [8]: tag
// [9]: aux
// [10]: note_type
let withdraw_request_note_inputs = vec![
Felt::new(withdraw_amount),
Felt::new(0),
faucet.id().suffix(),
faucet.id().prefix().as_felt(),
p2id_output_note_serial_num[0],
p2id_output_note_serial_num[1],
p2id_output_note_serial_num[2],
p2id_output_note_serial_num[3],
p2id_tag_felt,
aux,
note_type_felt,
];

let withdraw_request_note = create_testing_note_from_package(
withdraw_request_note_package.clone(),
sender.id(),
NoteCreationConfig {
inputs: withdraw_request_note_inputs,
..Default::default()
},
)?;

builder.add_output_note(OutputNote::Full(withdraw_request_note.clone().into()));

// =========================================================================
// EXECUTE: Initialize, Deposit, Withdraw
// =========================================================================
let mut mock_chain = builder.build()?;

// Initialize bank
let init_program = init_tx_script_package.unwrap_program();
let init_tx_script = TransactionScript::new((*init_program).clone());
let init_tx_context = mock_chain
.build_tx_context(bank_account.id(), &[], &[])?
.tx_script(init_tx_script)
.build()?;
let executed_init = init_tx_context.execute().await?;
bank_account.apply_delta(&executed_init.account_delta())?;
mock_chain.add_pending_executed_transaction(&executed_init)?;
mock_chain.prove_next_block()?;

println!("Step 1: Bank initialized");

// Process deposit
let deposit_tx_context = mock_chain
.build_tx_context(bank_account.id(), &[deposit_note.id()], &[])?
.build()?;
let executed_deposit = deposit_tx_context.execute().await?;
bank_account.apply_delta(&executed_deposit.account_delta())?;
mock_chain.add_pending_executed_transaction(&executed_deposit)?;
mock_chain.prove_next_block()?;

println!("Step 2: Deposited {} tokens", deposit_amount);

// Process withdraw with expected P2ID output note
let recipient = build_p2id_recipient(sender.id(), p2id_output_note_serial_num)?;
let p2id_output_note_asset = FungibleAsset::new(faucet.id(), withdraw_amount)?;
let p2id_output_note_assets = NoteAssets::new(vec![p2id_output_note_asset.into()])?;
let p2id_output_note_metadata = NoteMetadata::new(
bank_account.id(),
NoteType::Public,
p2id_tag,
NoteExecutionHint::none(),
aux,
)?;
let p2id_output_note = Note::new(
p2id_output_note_assets,
p2id_output_note_metadata,
recipient,
);

let withdraw_tx_context = mock_chain
.build_tx_context(bank_account.id(), &[withdraw_request_note.id()], &[])?
.extend_expected_output_notes(vec![OutputNote::Full(p2id_output_note.into())])
.build()?;
let executed_withdraw = withdraw_tx_context.execute().await?;
bank_account.apply_delta(&executed_withdraw.account_delta())?;
mock_chain.add_pending_executed_transaction(&executed_withdraw)?;
mock_chain.prove_next_block()?;

println!("Step 3: Withdrew {} tokens", withdraw_amount);
println!("\nPart 7 withdraw test passed!");

Ok(())
}

Run the test from the project root:

>_ Terminal
cargo test --package integration part7_withdraw -- --nocapture
Expected output
   Compiling integration v0.1.0 (/path/to/miden-bank/integration)
Finished `test` profile [unoptimized + debuginfo] target(s)
Running tests/part7_withdraw_test.rs

running 1 test
Step 1: Bank initialized
Step 2: Deposited 1000 tokens
Step 3: Withdrew 500 tokens

Part 7 withdraw test passed!
test test_withdraw_creates_p2id_note ... ok

test result: ok. 1 passed; 0 failed; 0 ignored

"Insufficient balance for withdrawal": Make sure the deposit was processed before attempting withdrawal.

"Missing expected output note": Verify the P2ID note parameters (tag, serial_num, etc.) match exactly.

What We've Built So Far

ComponentStatusDescription
bank-account✅ CompleteFull deposit AND withdraw logic
deposit-note✅ CompleteNote script for deposits
withdraw-request-note✅ CompleteNote script for withdrawals
init-tx-script✅ CompleteTransaction script for initialization

Complete Code for This Part

Click to see the complete withdraw-request-note code
contracts/withdraw-request-note/src/lib.rs
// Do not link against libstd (i.e. anything defined in `std::`)
#![no_std]
#![feature(alloc_error_handler)]

use miden::*;

// Import the bank account's generated bindings
use crate::bindings::miden::bank_account::bank_account;

/// Withdraw Request Note Script
///
/// When consumed by the Bank account, this note requests a withdrawal and
/// the bank creates a P2ID note to send assets back to the depositor.
///
/// # Note Inputs (11 Felts)
/// [0-3]: withdraw asset (amount, 0, faucet_suffix, faucet_prefix)
/// [4-7]: serial_num (random/unique per note)
/// [8]: tag (P2ID note tag for routing)
/// [9]: aux (auxiliary data)
/// [10]: note_type (1 = Public, 2 = Private)
#[note_script]
fn run(_arg: Word) {
// The depositor is whoever created/sent this note
let depositor = active_note::get_sender();

// Get the inputs
let inputs = active_note::get_inputs();

// Asset: [amount, 0, faucet_suffix, faucet_prefix]
let withdraw_asset = Asset::new(Word::from([inputs[0], inputs[1], inputs[2], inputs[3]]));

// Serial number: full 4 Felts (random/unique per note)
let serial_num = Word::from([inputs[4], inputs[5], inputs[6], inputs[7]]);

// Tag: single Felt for P2ID note routing
let tag = inputs[8];

// Aux: auxiliary data for application-specific purposes
let aux = inputs[9];

// Note type: 1 = Public, 2 = Private
let note_type = inputs[10];

// Call the bank account to withdraw the assets
bank_account::withdraw(depositor, withdraw_asset, serial_num, tag, aux, note_type);
}

Key Takeaways

  1. Recipient::compute() creates a cryptographic commitment from serial number, script root, and inputs
  2. output_note::create() creates the note with tag, type, and recipient
  3. output_note::add_asset() attaches assets to the created note
  4. P2ID pattern uses a standard script with account ID as input
  5. Serial numbers must be unique to prevent note replay
  6. Array ordering - P2ID expects [suffix, prefix, ...] not [prefix, suffix, ...]
  7. Always validate before subtraction to prevent underflow exploits

See the complete implementation in the miden-bank repository.

Next Steps

Now that you've built all the components, let's see how they work together in Part 8: Complete Flows.