Part 2: Constants and Constraints
In this section, you'll learn how to define business rules using constants and enforce them with assertions. We'll implement deposit limits and see how failed constraints cause transactions to be rejected.
What You'll Build in This Part
By the end of this section, you will have:
- Defined constants for business rules (
MAX_DEPOSIT_AMOUNT,MAX_BALANCE) - Used
assert!()for transaction validation - Learned safe Felt comparison with
.as_canonical_u64() - Added a deposit method skeleton with validation
- Verified constraints work by testing that invalid operations fail
Building on Part 1
In Part 1, we set up the Bank's storage structure. Now we'll add business rules:
Part 1: Part 2:
┌─────────────────────────┐ ┌─────────────────────────┐
│ Bank │ │ Bank │
│ ────────────────────────│ ──► │ ────────────────────────│
│ + initialize() │ │ + initialize() │
│ + get_depositor_balance()│ │ + get_depositor_balance()│
│ │ │ + deposit() │ ◄── NEW (skeleton)
│ │ │ + MAX_DEPOSIT_AMOUNT │ ◄── NEW constant
│ │ │ + MAX_BALANCE │ ◄── NEW constant
└─────────────────────────┘ └─────────────────────────┘
Defining Constants
Constants in Miden Rust contracts work just like regular Rust constants:
/// Maximum allowed deposit amount per transaction.
///
/// This limit provides a safety constraint for the banking system.
///
/// Value: 1,000,000 tokens (arbitrary limit for demonstration)
const MAX_DEPOSIT_AMOUNT: u64 = 1_000_000;
/// Maximum allowed balance per depositor per asset.
///
/// This matches `FungibleAsset::MAX_AMOUNT` (2^63 - 2^31) from the Miden protocol.
/// Felt arithmetic is modular (wraps at the Goldilocks prime), so without this guard
/// a cumulative balance could silently wrap around to zero. Validating the u64 result
/// of the addition against this bound prevents that overflow.
const MAX_BALANCE: u64 = 9_223_372_034_707_292_160; // 2^63 - 2^31
Use constants for:
- Business rule limits (max amounts, timeouts)
- Magic numbers that need documentation
- Values used in multiple places
Constants are compiled into the contract code and cannot change. Use storage slots for values that need to be modified at runtime.
The assert!() Macro
The assert!() macro validates conditions during transaction execution:
fn initialize(&mut self) {
// Check not already initialized
let current: Word = self.initialized.get();
assert!(
current[0].as_canonical_u64() == 0,
"Bank already initialized"
);
// Set initialized flag to 1
let initialized_word = Word::from([felt!(1), felt!(0), felt!(0), felt!(0)]);
self.initialized.set(initialized_word);
}
When an assertion fails:
- The Miden VM execution halts
- No valid proof can be generated
- The transaction is rejected
This is the primary mechanism for enforcing business rules in Miden contracts.
Safe Felt Comparisons
Never use <, >, <=, or >= operators directly on Felt values. They produce incorrect results due to field element ordering.
Wrong approach:
// DON'T DO THIS - produces incorrect results
if deposit_amount > felt!(1_000_000) {
// This comparison is unreliable!
}
Correct approach:
// CORRECT - convert to u64 first
if deposit_amount.as_canonical_u64() > MAX_DEPOSIT_AMOUNT {
// This works correctly
}
The .as_canonical_u64() method extracts the underlying 64-bit integer from a Felt, allowing standard Rust comparisons.
Step 1: Add the Constant and Deposit Method
Update your contracts/bank-account/src/lib.rs to add the constant and a deposit method skeleton. The component's public API lives in #[component] impl Bank for BankStorage, while private helpers like require_initialized and balance_key live in a separate plain impl BankStorage block (the #[component] macro only exports trait methods):
const MAX_DEPOSIT_AMOUNT: u64 = 1_000_000;
const MAX_BALANCE: u64 = 9_223_372_034_707_292_160; // 2^63 - 2^31
#[component]
impl Bank for BankStorage {
fn initialize(&mut self) {
let current: Word = self.initialized.get();
assert!(
current[0].as_canonical_u64() == 0,
"Bank already initialized"
);
let initialized_word = Word::from([felt!(1), felt!(0), felt!(0), felt!(0)]);
self.initialized.set(initialized_word);
}
/// Get the bank-tracked balance for a depositor and specific asset type.
fn get_depositor_balance(&self, depositor: AccountId, asset: Asset) -> Felt {
self.balances.get(BankStorage::balance_key(depositor, &asset))
}
/// Deposit assets into the bank.
/// For now, this just validates constraints - we'll add asset handling in Part 3.
fn deposit(&mut self, depositor: AccountId, deposit_asset: Asset) {
// NOTE: Initialization guard — enabled in Part 6 (Transaction Scripts)
// self.require_initialized();
// Extract the fungible amount from the asset
let deposit_amount = deposit_asset.value[0];
// ========================================================================
// CONSTRAINT: Maximum deposit amount check
// ========================================================================
assert!(
deposit_amount.as_canonical_u64() <= MAX_DEPOSIT_AMOUNT,
"Deposit amount exceeds maximum allowed"
);
// We'll add balance tracking and asset handling in Part 3
// For now, just validate the constraints
}
}
/// Internal helpers that are not part of the component's exported WIT API.
impl BankStorage {
/// Derive the `balances` map key identifying a (depositor, faucet) pair:
/// `[depositor.prefix, depositor.suffix, faucet_prefix, faucet_suffix(+metadata)]`.
fn balance_key(depositor: AccountId, asset: &Asset) -> Word {
Word::from([
depositor.prefix,
depositor.suffix,
asset.key[3], // faucet id prefix
asset.key[2], // faucet id suffix folded with the asset metadata byte
])
}
/// Check that the bank is initialized.
fn require_initialized(&self) {
let current: Word = self.initialized.get();
assert!(
current[0].as_canonical_u64() == 1,
"Bank not initialized - deposits not enabled"
);
}
}
In v0.15 the fungible-asset vault key Word is [asset_id_suffix, asset_id_prefix, faucet_suffix | metadata_byte, faucet_prefix]. So asset.key[2] is the faucet suffix combined with a metadata byte (asset composition + a callback flag in the low 8 bits), not the raw faucet suffix. For the callbacks-disabled fungible assets this bank accepts the metadata byte is constant, so (key[3], key[2]) remains a stable per-faucet identifier. The host/test side derives the same key from FungibleAsset::new(faucet.id(), amt)?.to_key_word() indices [3]/[2] (not faucet.id().prefix()/suffix()).
The require_initialized() Guard
This helper is defined in the private impl BankStorage block and intentionally commented out in the deposit method until Part 6. When enabled, it will check initialization state:
fn require_initialized(&self) {
let current: Word = self.initialized.get();
assert!(
current[0].as_canonical_u64() == 1,
"Bank not initialized - deposits not enabled"
);
}
This pattern:
- Centralizes the initialization check
- Provides a clear error message
- Can be reused across multiple methods
How Assertions Affect Proving
When an assertion fails in the Miden VM:
Transaction Execution Flow:
┌─────────────────────┐
│ User submits TX │
└──────────┬──────────┘
▼
┌─────────────────────┐
│ VM executes code │
└──────────┬──────────┘
▼
┌──────┴──────┐
│ Assertion? │
└──────┬──────┘
Pass │ Fail
┌──────┴──────┐
▼ ▼
┌────────┐ ┌────────────┐
│ Prove │ │ TX Rejected│
│ Success│ │ No Proof │
└────────┘ └────────────┘
Key points:
- Failed assertions prevent proof generation
- No state changes occur if the transaction fails
- Error messages help with debugging
Step 2: Build and Verify
Build the updated contract:
cd contracts/bank-account
miden build
The Miden compiler prints non-fatal MAST-serialization ERROR lines on every build. These are cosmetic — the build still succeeds and emits the .masp package.
Optional: Verify Constraints Work
This is an optional self-check. If you create this test file, you can run it to verify the contract compiles with the constraint logic. The main runnable tests begin in Part 4.
use integration::helpers::{
build_project_in_dir, create_testing_account_from_package, AccountCreationConfig,
};
use miden_client::account::{
component::{InitStorageData, StorageValueName},
StorageSlotName,
};
use miden_client::Word;
use std::{path::Path, sync::Arc};
/// Test that our constraint logic is set up correctly
#[tokio::test]
async fn test_constraints_are_defined() -> anyhow::Result<()> {
// Build the bank account contract to verify it compiles with constraints
let bank_package = Arc::new(build_project_in_dir(
Path::new("../contracts/bank-account"),
true,
)?);
// The `initialized` value slot has no schema default, so
// `AccountComponent::from_package` requires it to be seeded (with a zero Word =
// uninitialized) or it errors with `InitValueNotProvided`. Only the `balances`
// map slot defaults to empty.
let initialized_slot =
StorageSlotName::new("bank_account::bank::initialized")
.expect("Valid slot name");
// Create an uninitialized bank account
let mut init_storage_data = InitStorageData::default();
init_storage_data.insert_value(
StorageValueName::from_slot_name(&initialized_slot),
Word::default(),
)?;
let bank_cfg = AccountCreationConfig {
init_storage_data,
..Default::default()
};
let bank_account =
create_testing_account_from_package(bank_package.clone(), bank_cfg)?;
// Verify the bank starts uninitialized
let initialized = bank_account.storage().get_item(&initialized_slot)?;
assert_eq!(
initialized[0].as_canonical_u64(),
0,
"Bank should start uninitialized"
);
println!("Bank account created with constraints!");
println!(" - MAX_DEPOSIT_AMOUNT: 1,000,000");
println!(" - require_initialized() defined (enabled in Part 6)");
println!(" - Initialization status: {}", initialized[0].as_canonical_u64());
println!("\nPart 2 constraints test passed!");
Ok(())
}
Run the test from the project root:
cargo test --package integration test_constraints_are_defined -- --nocapture
Expected output
Compiling integration v0.1.0 (/path/to/miden-bank/integration)
Finished `test` profile [unoptimized + debuginfo] target(s)
Running tests/part2_constraints_test.rs
running 1 test
Bank account created with constraints!
- MAX_DEPOSIT_AMOUNT: 1,000,000
- require_initialized() defined (enabled in Part 6)
- Initialization status: 0
Part 2 constraints test passed!
test test_constraints_are_defined ... ok
test result: ok. 1 passed; 0 failed; 0 ignored
In Part 4, we'll write a real deposit-flow test. At that stage, the deposit works without initialization because the guard is still commented out. In Part 6 (Transaction Scripts), we'll enable the initialization guard and verify it works with a dedicated test.
Common Constraint Patterns
Balance Checks (Preview for Part 3)
fn require_sufficient_balance(&self, depositor: AccountId, asset: Asset, amount: Felt) {
let balance = self.get_depositor_balance(depositor, asset);
assert!(
balance.as_canonical_u64() >= amount.as_canonical_u64(),
"Insufficient balance"
);
}
This pattern is mandatory for any operation that subtracts from a balance. Miden uses field element (Felt) arithmetic, which is modular. Without this check, subtracting more than the balance would NOT cause an error - instead, the value would silently wrap around to a large positive number, effectively allowing unlimited withdrawals. See Common Pitfalls for more details.
State Checks
fn require_not_paused(&self) {
let paused: Word = self.paused.get();
assert!(
paused[0].as_canonical_u64() == 0,
"Contract is paused"
);
}
Complete Code for This Part
Here's the full lib.rs after Part 2:
Click to expand full code
#![no_std]
#![feature(alloc_error_handler)]
#[macro_use]
extern crate alloc;
use miden::*;
use miden::Felt;
/// Maximum allowed deposit amount per transaction.
const MAX_DEPOSIT_AMOUNT: u64 = 1_000_000;
/// Maximum allowed balance per depositor per asset.
const MAX_BALANCE: u64 = 9_223_372_034_707_292_160; // 2^63 - 2^31
/// Storage layout for the bank account component.
#[component_storage]
struct BankStorage {
#[storage(description = "initialized")]
initialized: StorageValue<Word>,
#[storage(description = "balances")]
balances: StorageMap<Word, Felt>,
}
/// API of the bank account component.
#[component]
trait Bank {
/// Initialize the bank account, enabling deposits.
fn initialize(&mut self);
/// Get the bank-tracked balance for a depositor and specific asset type.
fn get_depositor_balance(&self, depositor: AccountId, asset: Asset) -> Felt;
/// Deposit an asset into the bank for a specific depositor.
fn deposit(&mut self, depositor: AccountId, deposit_asset: Asset);
}
#[component]
impl Bank for BankStorage {
fn initialize(&mut self) {
let current: Word = self.initialized.get();
assert!(
current[0].as_canonical_u64() == 0,
"Bank already initialized"
);
let initialized_word = Word::from([felt!(1), felt!(0), felt!(0), felt!(0)]);
self.initialized.set(initialized_word);
}
fn get_depositor_balance(&self, depositor: AccountId, asset: Asset) -> Felt {
self.balances.get(BankStorage::balance_key(depositor, &asset))
}
fn deposit(&mut self, depositor: AccountId, deposit_asset: Asset) {
// NOTE: Initialization guard — enabled in Part 6 (Transaction Scripts)
// self.require_initialized();
let deposit_amount = deposit_asset.value[0];
// CONSTRAINT: Maximum deposit amount check
assert!(
deposit_amount.as_canonical_u64() <= MAX_DEPOSIT_AMOUNT,
"Deposit amount exceeds maximum allowed"
);
// Balance tracking and asset handling added in Part 3
}
}
/// Internal helpers that are not part of the component's exported WIT API.
///
/// The `#[component]` macro exports only the methods of the `Bank` trait, so these
/// inherent methods stay private to the contract.
impl BankStorage {
/// Derive the `balances` map key identifying a (depositor, faucet) pair:
/// `[depositor.prefix, depositor.suffix, faucet_prefix, faucet_suffix(+metadata)]`.
fn balance_key(depositor: AccountId, asset: &Asset) -> Word {
Word::from([
depositor.prefix,
depositor.suffix,
asset.key[3],
asset.key[2],
])
}
/// Check that the bank is initialized.
fn require_initialized(&self) {
let current: Word = self.initialized.get();
assert!(
current[0].as_canonical_u64() == 1,
"Bank not initialized - deposits not enabled"
);
}
}
Key Takeaways
- Constants define immutable business rules at compile time
assert!()enforces constraints - failures reject the transaction- Always use
.as_canonical_u64()for Felt comparisons, never direct operators - Helper methods like
require_initialized()centralize validation logic - Failed assertions mean no valid proof can be generated
See the complete constraint implementation in contracts/bank-account/src/lib.rs.
Next Steps
Now that you can define and enforce business rules, let's learn how to handle assets in Part 3: Asset Management.