Transactions
Snowblossom uses a UTXO model like Bitcoin, but with a few deliberate twists: the transaction id is the hash of an opaque byte string so it can never be mutated by re-serialisation, signatures are made per transaction rather than per input, spending requires publishing the address spec (the multisig definition) that hashes to the address, and the complete unspent-output set is committed in every block header.
Structure
Transaction id
tx_hash = Skein-256-256(inner_data), exactly the bytes carried in the message. Protocol Buffers do not guarantee canonical serialisation, so the inner transaction is kept as bytes and never re-encoded on the way through the network; the bytes must parse as a TransactionInner with no trailing data. (An old comment in the .proto describing an HMAC construction is wrong; the code simply hashes.)
Fields
| Field | Rules |
|---|---|
version | Must be 1. |
inputs[] | At least one for a normal transaction, none for the coinbase. spec_hash 20 bytes (the address being spent), src_tx_id 32 bytes, src_tx_out_idx in [0, 32768). value (SIP-4): optional; if non-zero it must equal the referenced output's value, so offline signers can verify what they sign. |
outputs[] | 1 to 32,767 outputs, each value > 0 (no dust rule), recipient_spec_hash 20 bytes. Optional: requirements (time/height lock), for_benefit_of_spec_hash, ids, target_shard (0 … max shard id in v2 blocks). |
claims[] | Exactly one AddressSpec per distinct input address, each hashing to one of them; no extras. This is where public keys are revealed. |
fee | ≥ 0; Σ inputs = Σ outputs + fee exactly. No minimum fee in consensus (policy below). |
extra | Up to 100 bytes of anything - “short letters to grandma”, timestamps, audit-log entries. |
signatures[] | Each (claim_idx, key_idx) unique and in range; each signature must verify; every claim needs at least required_signers valid distinct keys. More signatures than needed are fine, invalid ones are fatal. |
| Size | Whole Transaction ≤ 1,000,000 bytes (MAX_TX_SIZE). |
Signing
What is signed is the 32-byte tx_hash. For each input address the spender includes the address's AddressSpec as a claim, then adds SignatureEntrys pointing at (claim, key). Because signing is per transaction, spending ten outputs of the same address costs one claim and one signature, not ten - “space efficient signing”. Multisig is native: an address spec is an M-of-N over any mix of algorithms, and a transaction is valid once each claim has M valid signatures. Wallets can sign incrementally (sign_transaction reports signatures_added / all_signed), which makes watch-only + offline signing and multi-party signing straightforward. Algorithms, key encodings and the quantum-resistant options are on the Addresses & signatures page.
Validation
Stateless (checkTransactionBasics)
- Size ≤ 1 MB;
tx_hashis 32 bytes and equals the hash ofinner_data; inner parses with no trailing bytes; version 1. - Coinbase:
is_coinbase, no inputs, no signatures, fee 0, remarks ≤ 100 bytes. Non-coinbase: not coinbase, ≥ 1 input, emptycoinbase_extras. - 1 ≤ outputs < 32,768; fee ≥ 0; input index and hash sizes as above.
- Claims ↔ distinct input addresses one-to-one; every claim hashes to a used address.
- Every signature verifies against its key; no duplicate (claim, key); each claim satisfied.
- Every output value > 0, recipient 20 bytes;
extra≤ 100 bytes.
Against the chain state (deepTransactionCheck)
- Each input must exist in the UTXO trie of the shard being built. A second spend of the same outpoint - even within one transaction - fails because the first spend writes a tombstone into the update buffer.
- Locks (SIP-3, since block 35,000): an output with
required_block_heightorrequired_timeis not spendable until the block's height/timestamp reaches it. - SIP-4 (since 151,680): if
input.value ≠ 0it must match the spent output. - SIP-6 (since 358,700): signatures of type SPHINCS+ or Dilithium are rejected before activation.
- Outputs: before the SIP-3 heights no requirements/extras were allowed;
for_benefit_of_spec_hashif present is 20 bytes; in v1 blockstarget_shardmust be 0, in v2 blocks ≤ max shard id. Outputs whose target shard is in the building shard's cover set enter its UTXO; others are collected into per-shard export tries (cross-shard transfers). Σ inputs = Σ outputs + fee.
Coinbase
Transaction 0 of every block. No inputs, no signatures; coinbase_extras carries the block height and shard id (guaranteeing uniqueness), up to 100 bytes of remarks (pool names, greetings) and the SIP votes motions_approved / motions_rejected. Its outputs must total exactly block reward + fees; they may be split arbitrarily, which is how MrPlow pays every miner directly in each block it finds. Coinbase outputs carry target_shard = shard_id.
Special outputs
- Time / height locks
requirements.required_block_heightandrequired_timemake an output unspendable until then (SIP-3). Used to stake identities and for escrow-like constructions. CLI:sendlocked.- For-benefit-of (FBO)
for_benefit_of_spec_hashmarks an output paid to one address as being held for another. No consensus meaning; every node indexes it and servesGetFBOList.- Identifiers
ids.username/ids.channelnameclaim a name. Nodes index names case- and accent-insensitively (Unicode NFC + primary-strength collation, then Skein) andGetIDListreturns claims oldest first - by convention the earliest unspent claim owns the name. Built for Snowblossom Channels.target_shard- Which shard the output should live in. Ordinary wallets leave it 0, which always resolves to the coordinator lineage after splits. See Sharding.
extra- 100 bytes of arbitrary data on the transaction (not per output). Visible in explorers.
Fees and policy
- Fees are whatever is left over:
fee = Σ inputs − Σ outputs. The node's fee estimate (GetFeeEstimate) starts atBASIC_FEE = 2.5flakes/byte and, when the mempool would fill more than two-thirds of a block, moves to 1.01 × the average rate of the lower-priority half of a simulated block. Recomputed every 30 s per shard. - Block assembly sorts transaction clusters (a transaction with its unconfirmed ancestors) by fee per byte of the whole cluster - so a child can pay for its parent. Clusters under
LOW_FEE = 2.2flakes/byte only get the firstLOW_FEE_SIZE_IN_BLOCK = 100,000bytes of a block. - The wallet estimates size as 82 + inner size + 56 per input + 28 per output + the bytes of every claim and an estimated signature for every key, then multiplies by the fee rate. A normal one-key transaction is ~270 bytes (~0.0007 SNOW); a
qhardspend is ~4.4 KB, apqc1spend ~15 KB.
Mempool
- One mempool per shard the node builds on (
MetaMemPooloffers a transaction to each; the first that accepts wins). - Limits: 80,000 transactions (
MEM_POOL_MAX, “mempool is full”); from 25,000 transactions upwards, transactions below 2.2 flakes/byte are refused (“too full for low fee transactions”). Zero-fee transactions are accepted while the pool is small. - Admission runs the full validation: stateless checks, then the transaction is placed in a cluster with any unconfirmed parents (dependency chains of any length, but never across shards), and the ordered cluster is deep-checked against a scratch UTXO view with a dummy next-block header - signatures, balances, SIP-4 and SIP-6 included.
- Double spends: first seen wins. A transaction conflicting with one already in the pool is discarded (“Discarding as double-spend”). This first-seen-first-added behaviour is also the network's defence for keys that are revealed at spend time.
- When the UTXO root changes (new block), the pool re-clusters and drops what no longer validates.
- Gossip: accepted transactions go to every peer through a rate-limited broadcaster (2 tx/s, burst 5 s, queue 2,500); every 5 s one random pool transaction is re-announced (not the same one within 300 s). A peer that cannot find a transaction's parent asks for the whole cluster (
req_cluster). - Nodes can refuse transactions arriving over P2P with
mempool_reject_p2p_tx=true(useful for a private node that should only relay its own).
The UTXO trie
Unspent outputs are stored in a hashed trie (lib/src/trie/HashedTrie.java): a radix trie in which every node is stored under the hash of its content, children are referenced by hash, and a whole tree state is just its root hash. Nothing is ever overwritten - a modification from root R yields a new root R′ sharing every unchanged node (copy-on-write). Consequences:
- The root after applying a block is
utxo_root_hashin the header. Two nodes with the same root have byte-identical UTXO sets. - Side chains and reorgs need no rollback: each block's UTXO state is addressable by its root and old states remain readable.
- Proofs: the node can return the nodes along a path (
GetUTXONodewithinclude_proof), and a client recomputes every hash up to the root it got from a block header. Because the structure is deterministic, absence can be proven too (show the parent where the key would be). - Nothing is pruned; history grows, which for a ledger is acceptable and is why an indexed mainnet database is ~100 GB.
Keys and values
key (54 bytes) = recipient_spec_hash (20) ‖ tx_id (32) ‖ out_idx as big-endian uint16 (2)
value = the raw wire bytes of the TransactionOutput as they appear inside inner_data
Because the key starts with the address, all outputs of an address are adjacent: a balance lookup is a prefix scan with the 20-byte address. The 2-byte index is why a transaction is limited to 32,768 outputs. Node hashes are SHA-256(prefix ‖ leaf_data ‖ child_key ‖ child_hash …) with children sorted by key; the empty trie's root is SHA-256(""). Two tries share this code: the UTXO trie (map u) and the chain-index trie (map cit) holding the address-history, tx-to-block, FBO and name indexes, whose root is stored per block in the summary so the indexes are reorg-safe too.
Address history and transaction lookups
- With
addr_index=truethe node writes ana2tx ‖ address ‖ tx_id → (height, block hash, tx id)entry for every input and output address of every transaction.GetAddressHistoryreturns up to 10,000 entries per shard in trie order (not chronological); the wallet and explorer sort them. Without the index the answer is simply empty. - With
tx_index=truethe node keeps every transaction by id (GetTransaction) and atx2bmap forGetTransactionStatus(unknown / mempool / confirmed with confirmation count). - Indexes are not built retroactively: enable them before the first sync or delete the database and resync.
Building a transaction (wallet side)
client/src/TransactionFactory.java is used by the CLI, the JSON-RPC server and IceLeaf:
- Collect spendable outputs of all wallet addresses - verified against the UTXO root, with the mempool overlaid - grouped by the shard they currently sit in.
- Pick a shard that alone covers the amount (shards are tried in random order); a transaction spends from one shard only. (A send no single shard can cover reports which shards hold what and why - splitting one payment across shards is still not implemented.)
- Choose inputs until they cover amount + fee, preferring confirmed outputs over unconfirmed ones (within each group the order is random), recomputing the fee estimate as inputs are added; add one claim per distinct input address. The same outpoint can never be used twice. With coin control (
--input/input_specific_list+input_use_all) the user's chosen outputs are all consumed instead. - Create change to a fresh, random or specified address, optionally split into chunks (
split_change_over; the fee is re-estimated to cover the extra outputs, and an exact multiple no longer produces a zero-value output). The change targets the shard the inputs came from, so on a split network it stays where it is spendable instead of defaulting to shard 0. - Shuffle inputs, outputs and claims (so the change output is not identifiable by position), serialise, hash, sign, and submit via
SubmitTransaction.
Options are documented under JSON-RPC → send.