How a transaction works
In plain words
Section titled “In plain words”Imagine a shared notebook that everyone in town copies. Every page of it is a list of who paid whom. Nobody owns the notebook. Everybody keeps a copy, and they all agree on the same pages.
A transaction is a signed note that says: “I, the owner of this account, pay 5 coins to my friend.” Four things decide whether the note is written in the notebook:
- The lock works. The note carries two signatures, like two different keys turning two different locks. Both must fit. If either is wrong, the note is thrown away.
- The note is the next one. Each account numbers its notes 0, 1, 2, and so on, like cheque numbers. A note that reuses an old number is refused. This stops someone from copying your note and paying twice.
- You have the coins. You cannot spend more than your account holds.
- You paid the postage. Every note costs a small fee, in proportion to its size. The fee is destroyed, so it does not go to anyone.
When enough of the town agrees on a page, that page is final.
The short path
Section titled “The short path”wallet node ledger────── ──── ──────build body ─► sign ─► mempool ─► DAG anchor commits ─► block ─► stage each tx ─► state root (inputs, (ML-DSA + (both sigs (ADR-027: block verify sigs (apply in order, outputs, SLH-DSA) checked derived from fee, nonce, all or nothing) nonce, fee) early) certificates) balance)The steps in the order the code runs them, in
crates/node/src/state/db.rs, stage_transaction:
| # | Check | Code | Refusal |
|---|---|---|---|
| 1 | Both signatures verify over the signing bytes | Transaction::verify (core/transaction.rs) | MissingSignature, SignatureSuite or a verification error |
| 2 | Fee: outputs to the fee collector cover base_fee × size | charge_fee (state/fee_exec.rs) | FeeTooLow { required, offered } |
| 3 | Sender: derived from the keys, never from the wire | tx.sender() | the address is the hash of both keys |
| 4 | Vault limits, if the sender is a vault account (ADR-030) | vault_check | vault error |
| 5 | Nonce equals the sender’s next nonce | tx.nonce != sender.nonce | InvalidNonce |
| 6 | Sum of outputs (checked, 256-bit) is covered by the balance | ledger_math::total_outputs | InsufficientBalance, BalanceOverflow |
| 7 | Debit the sender, advance the nonce, credit outputs | overlay writes | none, once 1 to 6 pass |
A block is applied all-or-nothing. The fee burn is recorded in the overlay and
settled before staking runs (settle_fees).
What a transaction contains
Section titled “What a transaction contains”From crates/node/src/core/transaction.rs:
| Field | Meaning |
|---|---|
inputs | references (prev_tx, index) to earlier outputs |
outputs | (amount: U256, recipient: [u8; 32]) pairs, amounts in base units |
nonce | replay counter; the account’s next expected value |
kind | what it does; TxKind::Transfer adds nothing to the encoding |
public_key | both public keys, ML-DSA-65 and SLH-DSA-SHA2-128s |
signature | both signatures, present or absent, never half |
The wire version selects the authorization. Versions 5 and 6 are the hybrid
pair. Version 7 is a suite-tagged single signature, accepted only at or past
SUITE_ENVELOPE_ACTIVATION_HEIGHT. Version 8 is an m-of-n multisig policy.
Versions 1 to 4 are refused by name, because their 3,309-byte single signature
is the thing the hybrid exists to rule out
(docs/hybrid-signatures.md).
What gets signed
Section titled “What gets signed”TX_DOMAIN = "custom-l1-node.tx.v4"signing_bytes = TX_DOMAIN ‖ chain_tag ‖ body_bytes() (signature excluded)TXID_DOMAIN = "custom-l1-node.txid.v1"txid = BLAKE3(TXID_DOMAIN ‖ kind ‖ body ‖ σ_ML ‖ σ_SLH) (hybrid)The txid hashes the signatures too, so re-signing changes the id. Signing is deterministic (see signatures), so one payload signed by one key pair always gives one id.
The chain tag is the genesis block id (ADR-036). A transaction signed for one Maya2C network cannot verify on another. The txid is chain-independent and only deduplicates; verification is what stops replay.
| Rule | Where |
|---|---|
required = base_fee × size_in_bytes, size = tx.to_bytes().len() | fee_exec.rs |
The offer is the sum of outputs to FEE_COLLECTOR | fee_exec.rs |
| A mining share with no outputs pays no base fee | fee_exec.rs (ADR-044) |
| The burned fee leaves circulating supply | settle_fees |
Measured on 2026-09-29 through a gateway on a fee-charging chain: a hybrid transfer was 13,255 bytes, so the minimum fee at base fee 1 was 13,255. The wallets’ default, “Standard”, paid twice that (API reference).
A demand-responsive fee market is built and tested in crates/fee-market and
switched off: every network runs FeeConfig::DISABLED, and nothing calls the
crate from the node. See docs/fee-market.md.
Build and send one
Section titled “Build and send one”l1-wallet --keystore my.key --rpc-url https://rpc.maya2c.dev/rpc balance
l1-wallet --keystore my.key --rpc-url https://rpc.maya2c.dev/rpc \ send --to <recipient address> --amount 100
# Sign and print the hex without broadcasting:l1-wallet --keystore my.key send --to <recipient address> --amount 100 --no-broadcast
# Pin the nonce yourself when submitting a burst:l1-wallet --keystore my.key send --to <recipient address> --amount 1 --nonce 7Flags are those of bins/l1-wallet/src/main.rs: --keystore (default
wallet.key, env L1_WALLET_KEYSTORE), --rpc-url (default
http://127.0.0.1:8545, env L1_RPC_URL), and on send --to, --amount,
--nonce, --fee, --no-broadcast.
Data-flow references
Section titled “Data-flow references”- Mempool applies the same both-signatures rule earlier:
crates/node/src/network/mempool.rs. - Block contents are derived from committed certificates, not proposed:
consensus::bft::builder, described in consensus. - Wallet guidance on nonces and bursts: wallet integration.
The predicate
Section titled “The predicate”Let be the ledger state and the signature pair over , where is the chain tag. A transaction is applicable to exactly when
where and are the sender’s address and balance, its next nonce, and the amounts and recipients of the outputs, the serialized length in bytes, and the BLAKE3 address derivation with its domain string. The fee output is one of the , so the balance inequality covers it too.
The conjunction is ordered. The signature conjunction is evaluated first, before any balance rule reads state, which is what makes the stage order in the engineer tab a security property, not a style choice.
Amounts: 256-bit, checked
Section titled “Amounts: 256-bit, checked”Amounts are unsigned 256-bit integers in base units. ADR-046 sets
the decimals convention; ADR-068 raised it to 24, so one coin is base units.
ADR-048
implements the arithmetic in the dependency-free crate crates/amount.
| Quantity | Value |
|---|---|
| Largest amount | base units |
| In coins at 24 decimals | |
Largest u64 at 24 decimals | base units, about 0.0000184 coins |
Representation: , little-endian limbs.
Operations are total: checked_add returns rather than wrapping, and
checked_sub returns when . Multiplication widens:
This is what lets a constant-product quote compute exactly even though the intermediate product needs 512 bits. Kani harnesses in the crate prove that no checked operation wraps and that add and subtract are inverses.
The fee rule
Section titled “The fee rule”The base fee is charged per byte. The controller in crates/fee-market
(built, switched off) is an EIP-1559-style update. With the target block
size in bytes, the parent’s size and the denominator:
So a block at raises the fee by exactly , and an empty block lowers it by . The charge is , where . Of the base fee, 80% is burned and 20% goes to the treasury, rounding the treasury share down, so no unit is created or lost.
The bound the code enforces is MAX_SUPPLY in
crates/fee-market/src/supply.rs. Older documentation mentions a cap of
21,000,000 coins at 8 decimals; that predates ADR-046 and is out of date.
Replay and ordering
Section titled “Replay and ordering”Nonce monotonicity gives each account a total order on its own transactions. Two transactions built on the same nonce are mutually exclusive, and only the first one in the final order applies.
Across accounts, the order is the one the DAG commits: a function of the certificate graph alone, with no clock in it (see consensus).
Status
Section titled “Status”- Built and tested. Not audited.
- Fee market: built, tested, not wired into state transitions.
- 256-bit amounts: implemented in the node path for transfers; the DEX arithmetic uses the same crate.
