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.6. 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 is 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.
Prerequisites
Create an account
P2P private transfer
P2P public transfer
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.
comments: true
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.
comments: true
Software prerequisites
- Rust installation minimum version 1.82.
Install the client
We currently recommend installing and running the client with the testing
and concurrent
features.
Run the following command to install the miden-client:
cargo install miden-cli --features concurrent,testing
This installs the miden
binary (at ~/.cargo/bin/miden
) with the testing
and concurrent
features.
Testing
feature
The testing
feature speeds up account creation.
!!! warning "Install the testing
feature on node and client"
- When using the client CLI alongside a locally-running node, make sure to install/execute the node with the testing
feature.
- Some validations can fail if the flag does not match on both the client and the node.
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 config or the store config via--rpc
and--store-path
-
Run the client CLI using:
miden
comments: true
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.
comments: true
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.
comments: true
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.6" }
Features
The Miden client library supports the testing
and concurrent
features which are both recommended for developing applications with the client. To use them, add the following to your project's Cargo.toml
:
miden-client = { version = "0.6", features = ["testing", "concurrent"] }
Client instantiation
Spin up a client using the following Rust code and supplying a store and RPC endpoint.
The current supported store is the SqliteDataStore
, which is a SQLite implementation of the Store
trait.
#![allow(unused)] fn main() { let client: Client<TonicRpcClient, SqliteDataStore> = { let store = SqliteStore::new((&client_config).into()).await.map_err(ClientError::StoreError)?; let mut rng = rand::thread_rng(); let coin_seed: [u64; 4] = rng.gen(); let rng = RpoRandomCoin::new(coin_seed.map(Felt::new)); let authenticator = StoreAuthenticator::new_with_rng(store.clone(), rng); let tx_prover = LocalTransactionProver::new(ProvingOptions::default()); let client = Client::new( Box::new(TonicRpcClient::new(&client_config.rpc)), rng, Arc::new(store), Arc::new(authenticator), Arc::new(tx_prover), false, // set to true if you want a client with debug mode ) }; }
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 AccountTemplate
enum defines the type of account. The following code creates a new local account:
#![allow(unused)] fn main() { let account_template = AccountTemplate::BasicWallet { mutable_code: false, storage_mode: AccountStorageMode::Private, }; let (new_account, account_seed) = client.new_account(account_template).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 account_template = AccountTemplate::BasicWallet {
mutable_code: false,
storage_mode: AccountStorageMode::Public,
};
let (new_account, account_seed) = client.new_account(client_template).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 = TransactionRequest::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.
comments: true
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 use the --rpc flag to override the default rpc config
miden init --rpc 18.203.155.106
# You can specify the port
miden init --rpc 18.203.155.106:8080
# You can also specify the protocol (http/https)
miden init --rpc https://18.203.155.106
# You can specify both
miden init --rpc 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 --rpc 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 two optional flags:
--storage-type <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).
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 two optional flags:
--storage-type <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).
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.
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.
comments: true
After installation, use the client by running the following and adding the relevant commands:
miden
!!! info "Help"
Run miden --help
for information on miden
commands.
Client Configuration
We configure the client using a TOML file (miden-client.toml
).
[rpc]
endpoint = { protocol = "http", host = "localhost", port = 57291 }
timeout_ms = 10000
[store]
database_filepath = "store.sqlite3"
[cli]
default_account_id = "0x012345678"
The TOML file should reside in same the directory from which you run the CLI.
In the configuration file, you will find a section for defining the node's rpc endpoint
and timeout and the store's filename database_filepath
.
By default, the node is set up to run on localhost:57291
.
!!! note - Running the node locally for development is encouraged. - However, the endpoint can point to any remote node.
There's an additional optional section used for CLI configuration. It currently contains the default account ID, which is used to execute transactions against it when the account flag is not provided.
By default none is set, but you can set and unset it with:
miden account --default <ACCOUNT_ID> #Sets default account
miden account --default none #Unsets default account
!!! note - The account must be tracked by the client in order to be set as the default account.
You can also see the current default account ID with:
miden account --default
Environment variables
MIDEN_DEBUG
: When set 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
.
comments: true
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
comments: true
Accounts are basic building blocks representing a user or an autonomous smart contract.
For smart contracts the go-to solution is account-based state. Miden supports expressive smart contracts via a Turing-complete language and the use of accounts.
In Miden, an account is an entity which holds assets and defines rules about how to transfer these assets.
Account design
In Miden every account is a smart contract. The diagram below illustrates the basic components of an account.
!!! tip "Key to diagram" * Account ID: A unique identifier for an account. This does not change throughout its lifetime. * Storage: User-defined data which can be stored in an account. * Nonce: A counter which increments whenever the account state changes. * Vault: A collection of assets stored in an account. * Code: A collection of functions which define the external interface for an account.
Account ID
A ~63
bits long identifier for the account ID (one field element felt
).
The four most significant bits specify the account type - regular or faucet - and the account-storage-modes - public or private.
Account storage
The storage of an account is composed of a variable number of index-addressable storage slots, up to 255 slots in total.
Each slot has a type which defines its size and structure. Currently, the following types are supported:
StorageSlot::Value
: contains a singleWord
of data (i.e., 32 bytes).StorageSlot::Map
: contains a StorageMap which is a key-value map where both keys and values areWord
s. The value of a storage slot containing a map is the commitment to the underlying map.
As described below, accounts can be stored off-chain (private) and on-chain (public). Accounts that store huge amounts of data, as it is possible using storage maps, are better designed as off-chain accounts.
Nonce
A counter which increments whenever the account state changes.
Nonce values must be strictly monotonically increasing and increment by any value smaller than 2^32
for every account update.
Vault
An asset container for an account.
An account vault can contain an unlimited number of assets. The assets are stored in a sparse Merkle tree as follows:
- For fungible assets, the index of a node is defined by the issuing faucet ID, and the value of the node is the asset itself. Thus, for any fungible asset there will be only one node in the tree.
- For non-fungible assets, the index is defined by the asset itself, and the asset is also the value of the node.
An account vault can be reduced to a single hash which is the root of the sparse Merkle tree.
Code
The interface for accounts. In Miden every account is a smart contract. It has an interface that exposes functions that can be called by note scripts and transaction scripts. Users cannot call those functions directly.
Functions exposed by the account have the following properties:
- Functions are actually roots of Miden program MASTs (i.e., a
32
-byte hash). Thus, the function identifier is a commitment to the code which is executed when a function is invoked. - Only account functions have mutable access to an account's storage and vault. Therefore, the only way to modify an account's internal state is through one of the account's functions.
- Account functions can take parameters and can create new notes.
!!! note
Since code in Miden is expressed as MAST, every function is a commitment to the underlying code. The code cannot change unnoticed to the user because its hash would change. Behind any MAST root there can only be 256
functions.
Example account code
Currently, Miden provides two standard implementations for account code.
Basic user account
There is a standard for a basic user account. It exposes three functions via its interface.
Basic user account code
use.miden::contracts::wallets::basic->basic_wallet
use.miden::contracts::auth::basic
export.basic_wallet::receive_asset
export.basic_wallet::create_note
export.basic_wallet::move_asset_to_note
export.basic::auth_tx_rpo_falcon512
Note scripts or transaction scripts can call receive_asset
, create_note
and move_asset_to_note
procedures.
Transaction scripts can also call auth_tx_rpo_falcon512
and authenticate the transaction.
!!! warning
Without correct authentication, i.e. knowing the correct private key, a note cannot successfully invoke receive_asset
, create_note
or move_asset_to_note
.
Basic fungible faucet (faucet for fungible assets)
There is also a standard for a basic fungible faucet.
Fungible faucet code
#! Distributes freshly minted fungible assets to the provided recipient.
#!
#! ...
export.distribute
# get max supply of this faucet. We assume it is stored at pos 3 of slot 1
push.METADATA_SLOT exec.account::get_item drop drop drop
# => [max_supply, amount, tag, note_type, RECIPIENT, ...]
# get total issuance of this faucet so far and add amount to be minted
exec.faucet::get_total_issuance
# => [total_issuance, max_supply, amount, tag, note_type RECIPIENT, ...]
# compute maximum amount that can be minted, max_mint_amount = max_supply - total_issuance
sub
# => [max_supply - total_issuance, amount, tag, note_type, RECIPIENT, ...]
# check that amount =< max_supply - total_issuance, fails if otherwise
dup.1 gte assert.err=ERR_BASIC_FUNGIBLE_MAX_SUPPLY_OVERFLOW
# => [asset, tag, note_type, RECIPIENT, ...]
# creating the asset
exec.asset::create_fungible_asset
# => [ASSET, tag, note_type, RECIPIENT, ...]
# mint the asset; this is needed to satisfy asset preservation logic.
exec.faucet::mint
# => [ASSET, tag, note_type, RECIPIENT, ...]
# store and drop the ASSET
mem_storew.3 dropw
# => [tag, note_type, RECIPIENT, ...]
# create a note containing the asset
exec.tx::create_note
# => [note_ptr, ZERO, ZERO, ...]
# store and drop the ASSET
padw mem_loadw.3 movup.4 exec.tx::add_asset_to_note
# => [note_ptr, ASSET, ZERO, ...]
end
#! Burns fungible assets.
#!
#! ...
export.burn
# burning the asset
exec.faucet::burn
# => [ASSET]
# increments the nonce (anyone should be able to call that function)
push.1 exec.account::incr_nonce
# clear the stack
padw swapw dropw
# => [...]
end
The contract exposes two functions distribute
and burn
.
The first function distribute
can only be called by the faucet owner, otherwise it fails. As inputs, the function expects everything that is needed to create a note containing the freshly minted asset, i.e., amount, metadata, and recipient.
The second function burn
burns the tokens that are contained in a note and can be called by anyone.
!!! info "Difference between burn
and distribute
"
The burn
procedure exposes exec.account::incr_nonce
, so by calling burn
the nonce of the executing account gets increased by 1
and the transaction will pass the epilogue check. The distribute
procedure does not expose that. That means the executing user needs to call basic::auth_tx_rpo_falcon512
which requires the private key.*
Account creation
For an account to exist it must be present in the account database kept on the Miden node(s).
However, new accounts can be created locally by users using the Miden client. The process is as follows:
- Alice creates a new account ID (according to the account types) using the Miden client.
- Alice's Miden client asks the Miden node to check if the new ID already exists.
- Alice shares the ID with Bob (eg. when Alice wants to receive funds).
- Bob executes a transaction and creates a note that contains an asset for Alice.
- Alice consumes Bob's note to receive the asset in a transaction.
- Depending on the account storage mode (private vs. public) and transaction type (local vs. network) the operator eventually receives the new account ID and - if the transaction is correct - adds the ID to the account database.
A user can create an account in one of the following manners:
- Use the Miden client as a wallet.
- Use the Miden base builtin functions for wallet creation: basic wallet, fungible faucet
Account types
There are two basic account types in Miden: Regular accounts and faucets. Only faucets can mint new assets. Regular accounts can be mutable or immutable, which simply means that it is possible to change the account code after creation.
Type and mutability is encoded in the most significant bits of the account's ID.
Basic mutable | Basic immutable | Fungible faucet | Non-fungible faucet | |
---|---|---|---|---|
Description | For most users, e.g. a wallet. Code changes allowed, including public API. | For most smart contracts. Once deployed code is immutable. | Users can issue fungible assets and customize them. | Users can issue non-fungible assets and customize them. |
Code updatability | yes | no | no | no |
Most significant bits | 00 | 01 | 10 | 11 |
Public and private accounts
Users can decide whether to keep their accounts private or public at account creation. The account ID encodes this preference on the third and fourth most significant bit.
- Accounts with public state: The actual state is stored on-chain. This is similar to how accounts work in public blockchains, like Ethereum. Smart contracts that depend on public shared state should be stored public on Miden, e.g., DEX contract.
- Accounts with private state: Only the hash of the account is stored on-chain. Users who want to stay private, and manage their own data, should choose this option. Users who want to interact with private accounts need to know the account's interface.
comments: true
Two of Miden's key goals are parallel transaction execution and privacy.
Polygon Miden implements a hybrid UTXO and account-based state model which enforces these goals with notes. Notes interact with, and transfer assets between, accounts. They can be consumed and produced asynchronously and privately.
The concept of notes is a key divergence from Ethereum’s account-based model.
Note design
!!! tip "Key to diagram"
* Assets: An asset container for a note. It can contain up to 256
assets stored in an array which can be reduced to a single hash.
* Script: To be executed in the transaction in which the note is consumed. The script defines the conditions for the consumption. If the script fails, the note cannot be consumed.
* Inputs: Used to execute the note script. They can be accessed by the note script via transaction kernel procedures. A note can be associated with up to 128
input values. Each value is represented by a single field element. Thus, note input values can contain up to ~1
KB of data.
* Serial number: A note's unique identifier to break link-ability between note hash and nullifier. Should be a random word
chosen by the user - if revealed, the nullifier might be computed easily.
* In addition, a note has metadata including the sender and the note tag. Those values are always public regardless of the note storage mode.
Note lifecycle
New notes are created by executing transactions.
After verifying the transaction proof the operator adds either only the note hash (private notes) or the full note data (public notes) to the note database.
Notes can be produced and consumed locally by users in local transactions or by the operator in a network transaction.
Note consumption requires the transacting party to know the note data to compute the nullifier. After successful verification, the operator sets the corresponding entry in the nullifier database to "consumed".
Note creation
Notes are created as the outputs (OutputNotes
) of Miden transactions. Operators record the notes to the note database. After successful verification of the underlying transactions, those notes can be consumed.
The note script
Every note has a script which gets executed at note consumption. It is always executed in the context of a single account, and thus, may invoke zero or more of the account's functions. The script allows for more than just asset transfers; actions which could be of arbitrary complexity thanks to the Turing completeness of the Miden VM.
By design, every note script can be defined as a unique hash or the root of a Miden program MAST. That also means every function is a commitment to the underlying code. That code cannot change unnoticed to the user because its hash changes. That way it is easy to recognize standardized notes and those which deviate.
Note scripts are created together with their inputs, i.e., the creator of the note defines which inputs are used at note execution by the executor. However, the executor or prover can pass optional note args. Note args are data put onto the stack right before a note script is executed. These are different from note inputs, as the executing account can specify arbitrary note args.
There are standard note scripts (P2ID, P2IDR, SWAP) that users can create and add to their notes using the Miden client or by calling internal Rust code.
- P2ID and P2IDR scripts are used to send assets to a specific account ID. The scripts check at note consumption if the executing account ID equals the account ID that was set by the note creator as note inputs. The P2IDR script is reclaimable and thus after a certain block height can also be consumed by the sender itself.
- SWAP script is a simple way to swap assets. It adds an asset from the note into the consumer's vault and creates a new note consumable by the first note's issuer containing the requested asset.
??? note "Example note script pay to ID (P2ID)"
#### Goal of the P2ID script
The P2ID script defines a specific target account ID as the only account that can consume the note. Such notes ensure a targeted asset transfer.
#### Imports and context
The P2ID script uses procedures from the account, note and wallet API.
```arduino
use.miden::account
use.miden::note
use.miden::contracts::wallets::basic->wallet
```
As discussed in detail in [transaction kernel procedures](transactions/procedures.md) certain procedures can only be invoked in certain contexts. The note script is being executed in the note context of the [transaction kernel](transactions/kernel.md).
#### Main script
The main part of the P2ID script checks if the executing account is the same as the account defined in the `NoteInputs`. The creator of the note defines the note script and the note inputs separately to ensure usage of the same standardized P2ID script regardless of the target account ID. That way, it is enough to check the script root (see above).
```arduino
# 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.
#
# Requires that the account exposes: miden::contracts::wallets::basic::receive_asset procedure.
#
# Inputs: [SCRIPT_ROOT]
# Outputs: []
#
# Note inputs are assumed to be as follows:
# - target_account_id is the ID of the account for which the note is intended.
#
# FAILS if:
# - Account does not expose miden::contracts::wallets::basic::receive_asset procedure.
# - Account ID of executing account is not equal to the Account ID specified via note inputs.
# - The same non-fungible asset already exists in the account.
# - Adding a fungible asset would result in amount overflow, i.e., the total amount would be
# greater than 2^63.
begin
# drop the transaction script root
dropw
# => []
# load the note inputs to memory starting at address 0
push.0 exec.note::get_inputs
# => [inputs_ptr]
# read the target account id from the note inputs
mem_load
# => [target_account_id]
exec.account::get_id
# => [account_id, target_account_id, ...]
# ensure account_id = target_account_id, fails otherwise
assert_eq
# => [...]
exec.add_note_assets_to_account
# => [...]
end
```
1. Every note script starts with the note script root on top of the stack.
2. After the `dropw`, the stack is cleared.
3. Next, the script stored the note inputs at pos 0 in the [relative note context memory](https://0xpolygonmiden.github.io/miden-base/transactions/transaction-procedures.html#transaction-contexts) by `push.0 exec.note::get_inputs`.
4. Then, `mem_load` loads a `Felt` from the specified memory address and puts it on top of the stack, in that cases the `target_account_id` defined by the creator of the note.
5. Now, the note invokes `get_id` from the account API using `exec.account::get_id` - which is possible even in the note context.
Because, there are two account IDs on top of the stack now, `assert_eq` fails if the two account IDs (target_account_id and executing_account_id) are not the same. That means, the script cannot be successfully executed if executed by any other account than the account specified by the note creator using the note inputs.
If execution hasn't failed, the script invokes a helper procedure `exec.add_note_assets_to_account` to add the note's assets into the executing account's vault.
#### Add assets
This procedure adds the assets held by the note into the account's vault.
```arduino
#! Helper procedure to add all assets of a note to an account.
#!
#! Inputs: []
#! Outputs: []
#!
proc.add_note_assets_to_account
push.0 exec.note::get_assets
# => [num_of_assets, 0 = ptr, ...]
# compute the pointer at which we should stop iterating
dup.1 add
# => [end_ptr, ptr, ...]
# pad the stack and move the pointer to the top
padw movup.5
# => [ptr, 0, 0, 0, 0, end_ptr, ...]
# compute the loop latch
dup dup.6 neq
# => [latch, ptr, 0, 0, 0, 0, end_ptr, ...]
while.true
# => [ptr, 0, 0, 0, 0, end_ptr, ...]
# save the pointer so that we can use it later
dup movdn.5
# => [ptr, 0, 0, 0, 0, ptr, end_ptr, ...]
# load the asset and add it to the account
mem_loadw call.wallet::receive_asset
# => [ASSET, ptr, end_ptr, ...]
# increment the pointer and compare it to the end_ptr
movup.4 add.1 dup dup.6 neq
# => [latch, ptr+1, ASSET, end_ptr, ...]
end
# clear the stack
drop dropw drop
end
```
The procedure starts by calling `exec.note::get_assets`. As with the note's inputs before, this writes the assets of the note into memory starting at the specified address. Assets are stored in consecutive memory slots, so `dup.1 add` provides the last memory slot.
In Miden, [assets](assets.md) are represented by `Words`, so we need to pad the stack with four `0`s to make room for an asset. Now, if there is at least one asset (checked by `dup dup.6 neq`), the loop starts. It first saves the pointer for later use (`dup movdn.5`), then loads the first asset `mem_loadw` on top of the stack.
Now, the procedure calls the a function of the account interface `call.wallet::receive_asset` to put the asset into the account's vault. Due to different [contexts](https://0xpolygonmiden.github.io/miden-base/transactions/transaction-procedures.html#transaction-contexts), a note script cannot directly call an account function to add the asset. The account must expose this function in its [interface](https://0xpolygonmiden.github.io/miden-base/architecture/accounts.html#example-account-code).
Lastly, the pointer gets incremented, and if there is a second asset, the loop continues (`movup.4 add.1 dup dup.6 neq`). Finally, when all assets were put into the account's vault, the stack is cleared (`drop dropw drop`).
Note storage mode
Similar to accounts, there are two storage modes for notes in Miden - private and public. Notes can be stored publicly in the note database with all data publicly visible for everyone. Alternatively, notes can be stored privately by committing only the note hash to the note database.
Every note has a unique note hash. It is defined as follows:
hash(hash(hash(hash(serial_num, [0; 4]), script_hash), input_hash), vault_hash)
!!! info
To compute a note's hash, we do not need to know the note's serial_num
. Knowing the hash of the serial_num
(as well as script_hash
, input_hash
and note_vault
) is also sufficient. We compute the hash of serial_num
as hash(serial_num, [0; 4])
to simplify processing within the VM._
Note discovery (note tags)
Note discovery describes the process by which Miden clients find notes they want to consume. Miden clients can query the Miden node for notes carrying a certain note tag in their metadata. Note tags are best-effort filters for notes registered on the network. They are lightweight values (32-bit) used to speed up queries. Clients can follow tags for specific use cases, such as swap scripts, or user-created custom tags. Tags are also used by the operator to identify notes intended for network execution and include the corresponding information on how to execute them.
The two most signification bits of the note tag have the following interpretation:
Prefix | Execution hint | Target | Allowed note type |
---|---|---|---|
0b00 | Network | Specific | NoteType::Public |
0b01 | Network | Use case | NoteType::Public |
0b10 | Local | Any | NoteType::Public |
0b11 | Local | Any | Any |
- Execution hint: Set to
Network
for network transactions. These notes are validated and, if possible, consumed in a network transaction. - Target: Describes how to interpret the bits in the note tag. For tags with a specific target, the rest of the tag is interpreted as an
account_id
. For use case values, the meaning of the rest of the tag is not specified by the protocol and can be used by applications built on top of the rollup. - Allowed note type: Describes the note's storage mode, either
public
orprivate
.
The following 30 bits can represent anything. In the above example note tag, it represents an account Id of a public account. As designed the first bit of a public account is always 0
which overlaps with the second most significant bit of the note tag.
0b00000100_11111010_01010110_11100010
This example note tag indicates that the network operator (Miden node) executes the note against a specific account - 0x09f4adc47857e2f6
. Only the 30 most significant bits of the account id are represented in the note tag, since account Ids are 64-bit values but note tags only have 32-bits. Knowing a 30-bit prefix already narrows the set of potential target accounts down enough.
Using note tags is a compromise between privacy and latency. If a user queries the operator using the note ID, the operator learns which note a specific user is interested in. Alternatively, if a user always downloads all registered notes and filters locally, it is quite inefficient. By using tags, users can customize privacy parameters by narrowing or broadening their note tag schemes.
??? note "Example note tag for P2ID" P2ID scripts can only be consumed by the specified account ID (target ID). In the standard schema, the target ID is encoded into the note tag.
For network execution of a P2ID note, the note tag is encoded as follows: 0b00000100_11111010_01010110_11100010. This encoding allows the Miden operator to quickly identify the account against which the transaction must be executed.
For local execution of a P2ID note, the recipient needs to be able to discover the note. The recipient can query the Miden node for a specific tag to see if there are new P2ID notes to be consumed. In this case, the two most significant bits are set to 0b11, allowing any note type (private or public) to be used. The next 14 bits represent the 14 most significant bits of the account ID, and the remaining 16 bits are set to 0.
Example for local execution:
```
0b11000100_11111010_00000000_00000000
```
This "fuzzy matching" approach balances privacy and efficiency. A note with this tag could be intended for any account sharing the same 16-bit prefix.
Note consumption
As with creation, notes can only be consumed in Miden transactions. If a valid transaction consuming an InputNote
gets verified by the Miden node, the note's unique nullifier gets added to the nullifier database and is therefore consumed.
Notes can only be consumed if the note data is known to the consumer. The note data must be provided as input to the transaction kernel. That means, for privately stored notes, there must be some off-chain communication to transmit the note's data from the sender to the target.
Note recipient to restrict note consumption
There are several ways to restrict the set of accounts that can consume a specific note. One way is to specifically define the target account ID as done in the P2ID and P2IDR note scripts. Another way is by using the concept of a RECIPIENT
. Miden defines a RECIPIENT
(represented as Word
) as:
hash(hash(hash(serial_num, [0; 4]), script_hash), input_hash)
This concept restricts note consumption to those users who know the pre-image data of RECIPIENT
- which might be a bigger set than a single account.
During the transaction prologue the users needs to provide all the data to compute the note hash. That means, one can create notes that can only be consumed if the serial_num
and other data is known. This information can be passed off-chain from the sender to the consumer. This is only useful with private notes. For public notes, all note data is known, and anyone can compute the RECIPIENT
.
You can see in the standard SWAP note script how RECIPIENT
is used. Here, using a single hash, is sufficient to ensure that the swapped asset and its note can only be consumed by the defined target.
Note nullifier to ensure private consumption
The note's nullifier is 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
, andvault_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.
comments: true
In Miden, users can create and trade arbitrary fungible and non-fungible assets.
We differentiate between native and non-native assets in Miden. Native assets follow the Miden asset model. Non-native assets are all other data structures of value that can be exchanged.
Native assets in Polygon Miden have four goals:
- Asset exchange should be parallelizable.
- Asset ownership should be self-sovereign.
- Asset usage should be censorship resistant.
- Fees can be paid using any asset.
All native assets in Miden are stored directly in accounts, like Ether in Ethereum. Miden does not track asset ownership using global hashmaps, e.g., ERC20 contracts. Local asset storage in accounts provides privacy and the ability for client-side proofs. That is because ownership changes are reflected only on an account and not in an ERC20 account (global hashmap). Thus, these changes can happen in parallel. Additionally, asset exchange is censorship resistant at this level because there is no global contract the transfer must pass through. Finally, users can pay fees in any asset.
Native assets
Native assets are data structures that follow the Miden asset model (encoding, issuance, storing). All native assets are encoded using a single word
(4 field elements). The asset encodes both the ID of the issuing account and the asset details.
Having the issuer's ID encoded in the asset makes determining the type of an asset, inside and outside Miden VM, cost-efficient. And, representing the asset in a word
means the representation is a commitment to the asset data itself. That is particularly interesting for non-fungible assets.
Issuance
Only specialized accounts called faucets can issue assets. As with regular accounts, anyone can create a faucet account. Faucets can issue either fungible or non-fungible assets - but not both.
The faucet_id
identifies the faucet and starts with a different sequence depending on the asset type, see the account id discussion. The faucet's code defines rules for how assets can be minted, who can mint them etc. Conceptually, faucet accounts on Miden are similar to ERC20 contracts on Ethereum. However, there is no ownership tracking in Miden faucets.
Faucets can create assets and immediately distribute them by producing notes. However, assets can also stay in the faucet after creation to be sent later, e.g., in a bundle. That way, one can mint a million NFTs locally in a single transaction and then send them out as needed in separate transactions in the future.
Fungible assets
A fungible asset is encoded using the amount and the faucet_id
of the faucet which issued the asset. The amount is guaranteed to be $2^{63} - 1$
or smaller, the maximum supply for any fungible asset. Examples of fungible assets are ETH and stablecoins, e.g., DAI, USDT, and USDC.
If the faucet_id
of MATIC is 2
, 100 MATIC are encoded as [100, 0, 0, 2]
; the 0
s in the middle distinguish between fungible and non-fungible assets.
Non-fungible assets
A non-fungible asset is encoded by hashing the asset data into a word
and then replacing the second element with the faucet_id
of the issuing account: For example [e0, faucet_id, e2, e3]
. Note that the second element is guaranteed to be non-zero. Together with the fungible asset encoding, this makes it easy to differentiate between both asset types by inspecting the second element.
Examples of non-fungible assets are all NFTs, e.g., a DevCon ticket. The ticket's data might be represented in a JSON string representing which DevCon, the date, the initial price, etc. Now, users can create a faucet for non-fungible DevCon tickets. This DevCon faucet would hash the JSON string into a word
to transform the ticket into an asset.
Storage
Accounts and notes contain asset vaults that are used to store assets. Accounts can keep unlimited assets in a sparse Merkle tree called account vault
. Notes can store up to 255
distinct assets.
The information on which and how many assets are owned can be private depending on the account's or note's storage mode. This is true for any native asset in Miden.
Non-native assets
Miden is flexible enough to create other types of assets as well.
For example, developers can replicate the Ethereum ERC20 model, where ownership of fungible assets is recorded in a single account. To transact, users must send a note to that account to change the global hashmap.
Furthermore, a complete account can be treated as a programmable asset because ownership of accounts is transferrable. An account could be a "crypto kitty" with specific attributes and rules, and people can trade these "crypto kitties" by transferring accounts between each other.
We can also think of an account representing a car. The owner of the car can change so the car account - granting access to the physical car - can be treated as an asset. In this car account, there could be rules defining who is allowed to drive the car and when.
comments: true
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 to 0x456..
) would be included in a separate section.
Verifying valid block state
To verify that a block describes a valid state transition, we do the following:
- 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.
Limits
The following are the current limits enforced in the miden-base
and miden-node
:
Accounts
- Max assets per account: no limit.
- Max top-level storage slots per account: 255. Each storage slot can contain an unlimited amount of data (e.g., if the storage slot contains an array or a map).
- Max code size per account: no limit (but we plan to enforce code size limit in the future, at least for public accounts).
Notes
- Min assets per note: 0.
- Max assets per note: 255.
- Max inputs per note: 128. The value can be represented using as a single byte while being evenly divisible by 8.
- Max code size per note: no limit (but we plan to enforce code size limit in the future, at least for public notes).
Transactions
- Max input notes per transaction: 1024.
- Max output notes per transaction: 1024.
- Max code size of tx script: no limit (but we plan to enforce code size limit in the future).
- Max number of VM cycles: $2^{30}$.
Batches
- Max number of input notes: 1024.
- Max number of output notes: 1024.
- Max number of accounts: 1024.
- Max number of VM cycles: $2^{30}$.
Blocks
- Max batches per block: 64.
- Max number of accounts: 65536 (max accounts per batch × max number of batches).
- Max number of input notes: 65536 (max notes per batch × max number of batches).
- Max number of output notes: 65536 (max notes per batch × max number of batches).
- Max public data size (for both notes and accounts): no limit.
comments: true
Miden rollup state describes the current condition of all accounts and notes; i.e. what is currently the case.
As the state model uses concurrent off-chain state, Polygon Miden aims for private, and parallel transaction execution and state bloat minimization.
Miden's goals include:
- Notes and nullifiers to ensure privacy of note consumption.
- Flexible data storage for users who can store their data off-chain or with the network.
- Parallel transactions executed concurrently by distinct actors.
- Concurrent state model that allows block production without knowing the full state.
Privacy is enforced by a UTXO-like state model consisting of notes and nullifiers combined with off-chain execution using zero-knowledge proofs.
State bloat describes the ever growing state stored in blockchain nodes. Polygon Miden addresses this challenge via its state model that enables concurrent off-chain execution and off-chain storage. Simply put, users can store their own data locally which reduces the burden on the network while integrity is ensured using zero-knowledge.
State components
Miden nodes maintain three databases to describe state:
- A database of accounts.
- A database of notes.
- A database of nullifiers for already consumed notes.
These databases are represented by authenticated data structures that enable easy proof of items added to or removed from a database. This ensures that the commitment to the database remains very small.
Polygon Miden has two databases to capture the note states. The note database is append-only and stores all notes permanently. The nullifier database stores nullifiers that indicate that a note has been previously consumed. Separating note storage into these two databases gives Polygon Miden client-side proving and advanced privacy.
Account database
The latest account states - and data for on-chain accounts - are recorded in a sparse Merkle tree which maps account IDs to account hashes, and account data if needed.
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 hashes of accounts are stored on-chain.
Private accounts significantly reduce the storage overhead for nodes. A private account contributes only $40$ bytes to the global state ($8$ bytes account ID + $32$ bytes account hash). Or, said another way, 1 billion private accounts takes up only $40$ GB of state.
!!! warning Losing the state of a private account means loss of funds in a similar manner as a loss of a private key - as the user won't be able to execute transactions. This problem can be mitigated by storing encrypted account state in the cloud or backing it up somewhere else. Unlike storing private keys in the cloud, this does not compromise privacy or account security.
In the future we hope to enable encrypted accounts where the account data is stored on-chain but in an encrypted format. This is especially interesting for shared accounts like advanced multi-sig wallets.
Note database
Notes are recorded in an append-only accumulator, a Merkle Mountain Range. Each leaf is a block header which contains the commitment to all notes created in that block. The commitment is a Sparse Merkle Tree of all the notes in a block. The size of the Merkle Mountain Range grows logarithmically with the number of items in it.
As described in the notes section, there are two types of notes:
- Public notes where the entire note content is recorded in the state.
- Private notes where only a note's hash is recorded in the state.
As with accounts, there is a strong incentive to use private notes as they result in lower fees. This is also beneficial to the network as a private note adds only $64$ bytes to the state ($32$ bytes when it is produced, and $32$ bytes when it is consumed).
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.
However, the size of the note database does not grow indefinitely. Theoretically, at high tps, it grows very quickly: at $1$K TPS there are about $1$TB/year added to the database. But, only the unconsumed public notes, and enough info to construct membership proofs against them, need to be stored explicitly. Private notes, as well as public notes which have already been consumed, can be safely discarded. Such notes would still remain in the accumulator, but there is no need to store them explicitly as the append-only accumulator can be updated without knowing about all the items stored in it. This reduces actual storage requirements to a fraction of the database's nominal size.
Nullifier database
Nullifiers are stored in a sparse Merkle tree, which maps note nullifiers to block numbers at which the nullifiers are inserted into the chain (or to 0
for nullifiers which haven't been recorded yet). Nullifiers provide information on whether a specific note has been consumed. The database allows proving that a given nullifier is not in the database.
To prove that a note has not been consumed previously, the operator needs to provide a Merkle path to its node, and then show that the value in that node is 0
. In our case nullifiers are $32$ bytes each, and thus, the height of the Sparse Merkle Tree needs to be $256$.
To add new nullifiers to the database, operators need to maintain the entire nullifier set. Otherwise, they would not be able to compute the new root of the tree.
!!! note Nullifiers as constructed in Miden break linkability of privately stored notes and the information about the note's consumption. To know the note's nullifier one must know the note's data.
In the future, when the network experiences a large number of transactions per second (TPS), there will be one tree per epoch (~3 months), and Miden nodes always store trees for at least two epochs. However, the roots of the old trees are still stored. If a user wants to consume a note that is more than $6$ months old, there must be a merkle path provided to the Miden Node for verification.
State bloat minimization
Operators don’t need to know the entire state to verify or produce a new block. No operator is required to store the entire state.
At its core, the idea is simple: Instead of storing the full state data with the operators, the users store their data, and the rollup only keeps track of commitments to the data. At least for private accounts, some smart contracts need to be publicly visible. This minimizes state bloat—as the operator doesn’t need to store an ever-growing database, and provides privacy because all other users and the operator only see a hash of other users’ data.
That way the account and note databases remain manageable, even at high usage for extended periods of time.
comments: true
!!! tip "Recap" Polygon Miden network architecture contains a bi-directional token bridge and state machine.
Miden nodes act as operators that maintain state and compress state transitions recursively into STARK-proofs. The token bridge on Ethereum verifies these proofs.
Users can run Miden clients to send RPC requests to the Miden nodes to update the state.
The major components of the Polygon Miden network are:
- Miden clients which represent Miden users.
- Miden nodes which manage the Miden rollup and compress proofs.
- A verifier contract which maintains and verifies state on Ethereum.
- A bridge contract as an entry and exit point for users.
Overview of the Miden network
Miden clients
Users run Miden clients and they provide an interface for wallets representing accounts on Miden.
Miden clients can execute and prove transactions with the tx prover. They can handle arbitrary signature schemes. The default is Falcon. There is a wallet user interface, a database that stores account data locally, and the required smart contract code that represents the account on Miden.
Miden nodes
Operators run Miden nodes.
Operators ensure integrity of account, note, and nullifier states - all of which represent the state of Polygon Miden. Operators can execute and prove transactions against single accounts and they can also verify proofs of locally executed transactions.
Furthermore, the operator compresses the proofs in several steps up to a single proof that gets published and verified on the verifier contract. Operators also watch events emitted by the bridge contract to detect deposits and withdrawals.
Node modules
To manage all of this, Miden nodes have separate modules.
- Tx prover: Executes and proves transactions, like the Miden client.
- Tx aggregator: Batches multiple proofs together to reduce the final state proof size using recursive proving.
- Block producer: exposes the RPC interface to the user and collects transactions in the tx pool and stores the state of Polygon Miden in its three databases (accounts, notes, and nullifiers).
Verifier contract
This contract on Ethereum verifies proofs sent by the operator running a Miden Node. The proof is verified against the current state root. If accepted the state root changes.
!!! note - Polygon Miden will integrate into the AggLayer. - The specific design is not yet finalized.
Bridge contract
This contract serves as a bridge for Miden users on Ethereum. Users can deposit their tokens and get an equivalent amount minted and sent to the specified address on Polygon Miden.
!!! note - Polygon Miden will integrate into the AggLayer and the Unified Bridge. - The specific design is not yet finalized.
comments: true
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 tx script context, the kernel executes some procedures in the kernel context, which is where all necessary information was 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 and add it to the account
mem_loadw call.wallet::receive_asset
# => [ASSET, 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 that was 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_vault_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.
comments: true
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 "Key to diagram" - 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.
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:
- 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 sender nor the recipient need 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.
comments: true
The transaction kernel program, written in MASM, is responsible for executing a Miden rollup transaction within the Miden VM. It is defined as MASM kernel.
The kernel provides context-sensitive security which prevents 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
.
The operand stack holds the global inputs which serve as a commitment to the data being provided via the advice provider.
The advice provider holds data of the last known block, account and input note data. The details are laid out in the next paragraph.
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 (tx vault) containing assets of all inputs (input notes and initial account state).
- Verifies that all input notes are present in the note DB.
In other words, the prologue stores all provided information from the inputs and the advice provider into the appropriate memory slots. It then reads the data for the account and notes from the advice provider, writes it to memory, hashes it, and verifies that the resulting hash matches the commitments provided via the stack. Finally, it creates a single vault for the assets that are involved.
The diagram below shows the memory layout. The kernel context has access to all memory slots.
Bookkeeping section
The bookkeeping section keeps track of variables which are used internally by the transaction kernel.
Global inputs
These are stored in the pre-defined memory slots.
Global inputs come from the operand_stack
and go to the VM at transaction execution. They include the block hash, the account ID, the initial account hash, and the nullifier commitment. This is a sequential hash of all (nullifier, ZERO)
pairs for the notes consumed in the transaction.
Block data
The block data processing involves reading the block data from the advice provider and storing it at the appropriate memory addresses. Block data comes from the latest known block and consists of note, state and tx hash, the block's previous hash and proof hash, as well as the block number. As the data is read from the advice provider, the block hash is computed. It is asserted that the computed block hash matches the block hash stored in the global inputs.
Chain data
Chain data is processed in a similar way to block data. In this case the chain root is recomputed and compared against the chain root stored in the block data section.
Account data
Account data processing involves reading the data from the advice provider and storing it at the appropriate memory addresses. The account data consists of account vault roots, storage, and code.
As the account data is read from the advice provider, the account hash is computed. If the account is new then the global initial account hash is updated and the new account is validated. If the account already exists then the computed account hash is asserted against the account hash provided via global inputs. It is also asserted that the account id matches the account id provided via the global inputs (operand_stack
).
Input note data
Input note processing involves the kernel reading the data from each note and storing it at the appropriate memory addresses. All the data (note, account, and blockchain data) comes from the advice provider and global inputs.
Next to the total number of input notes, input note data consists of a serial number, the roots of the script, the inputs and asset vault, its metadata, and all its assets.
As each note is consumed, its hash and nullifier are computed.
The transaction nullifier commitment is computed via a sequential hash of (nullifier, ZERO)
pairs for all input notes. This step involves authentication such that the input note data provided via the advice provider is consistent with the chain history.
!!! info - Note data is required for computing the nullifier, e.g. the note script and the serial number. - The system needs to know the note data to execute the prologue of a transaction. This is how the note recipient defines the set of users who can consume a specific note. - The executing account provides the pre-image data to the recipient at the time of execution.
If a transaction script is provided, its root is stored at a pre-defined memory address.
Note processing
Input notes are consumed in a loop.
For every note, the MAST root of the note script is loaded onto the stack. Then, by calling a dyncall
the note script is executed in a new context which prevents unwanted memory access.
# loop while we have notes to consume
while.true
# execute the note setup script
exec.note::prepare_note
# => [note_script_root_ptr, NOTE_ARGS]
# invoke the note script using the dyncall instruction
dyncall
# => [OUTPUT_3, OUTPUT_2, OUTPUT_1, OUTPUT_0]
# clean up note script outputs
dropw dropw dropw dropw
# => []
# check if we have more notes to consume and should loop again
exec.note::increment_current_input_note_ptr
loc_load.0
neq
# => [should_loop]
end
When processing a note, new note creation might be triggered. If so, all necessary information about the new note is stored in the output note data in memory.
!!! info - The Miden transaction kernel program prevents notes from having direct access to account storage. - Notes can only call the account interface to trigger write operations in the account.
Transaction script processing
If a transaction script is provided with the transaction, it is processed after all notes are consumed. By loading the transaction script root onto the stack, the kernel can invoke a dyncall
and in doing so execute the script. The transaction script is then again executed in its own context.
The transaction script can be used to authenticate the transaction by increasing the account's nonce and signing the transaction, as in the following example:
use.miden::contracts::auth::basic->auth_tx
begin
call.auth_tx::auth_tx_rpo_falcon512
end
!!! note
- The executing account must expose the auth_tx_rpo_falcon512
function in order for the transaction script to call it.
Epilogue
The epilogue finalizes the transaction. It does the following:
- Computes the final account hash.
- If the account has changed, it asserts that the final account nonce is greater than the initial account nonce.
- Computes the output notes commitment.
- Asserts that the input and output vault roots are equal.
There is an exception for special accounts, called faucets, which can mint or burn assets. In these cases, input and output vault roots are not equal.
Outputs
The transaction kernel program outputs the transaction script root, a commitment of all newly created outputs notes, and the account hash in its new state.
comments: true
There are user-facing procedures and kernel procedures. Users don't directly invoke kernel procedures, but instead they invoke them indirectly via account code, note, or transaction scripts. In these cases, kernel procedures are invoked by a syscall
instruction which always executes in the kernel context.
User-facing procedures (APIs)
These procedures can be used to create smart contract/account code, note scripts, or account scripts. They basically serve as an API for the underlying kernel procedures. If a procedure can be called in the current context, an exec
is sufficient. Otherwise the context procedures must be invoked by call
. Users never need to invoke syscall
procedures themselves.
!!! tip
If capitalized, a variable representing a word
, e.g., ACCT_HASH
consists of four felts
. If lowercase, the variable is represented by a single felt
.
Account
To import the account procedures, set use.miden::account
at the beginning of the file.
Any procedure that changes the account state must be invoked in the account context and not by note or transaction scripts. All procedures invoke syscall
to the kernel API and some are restricted by the kernel procedure exec.authenticate_account_origin
, which fails if the parent context is not the executing account.
Procedure 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 |
|
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_hash | [] | [COM] | account, note |
|
get_output_notes_hash | [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 |
|
comments: true
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.