Snowblossom Docs
Docs › Protocol › Transactions

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 tx_hash (32 B) = Skein-256-256(inner_data) inner_data (bytes: a serialised TransactionInner, hashed as-is) version = 1 · is_coinbase · coinbase_extras{height, shard_id, remarks, motions} inputs[] {spec_hash 20 B, src_tx_id 32 B, src_tx_out_idx, value (SIP-4, optional)} outputs[] {value, recipient_spec_hash 20 B, requirements{height, time}, for_benefit_of_spec_hash, ids{username, channelname}, target_shard} claims[] AddressSpec{required_signers, sig_specs[{signature_type, public_key}]} fee (flakes) · extra (≤ 100 B, free-form) one claim per distinct input address; claim must hash to that address signatures[] claim_idxkey_idxsignature (bytes) each signs the 32-byte tx_hash with claims[claim_idx] .sig_specs[key_idx] outside the hash: adding orremoving sigs keeps the id
Inputs reference outputs by (address, tx id, index). Signatures are separate from the hashed inner data, so a partially signed transaction has the same id as the finished one.

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

FieldRules
versionMust 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).
extraUp 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.
SizeWhole 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)

  1. Size ≤ 1 MB; tx_hash is 32 bytes and equals the hash of inner_data; inner parses with no trailing bytes; version 1.
  2. Coinbase: is_coinbase, no inputs, no signatures, fee 0, remarks ≤ 100 bytes. Non-coinbase: not coinbase, ≥ 1 input, empty coinbase_extras.
  3. 1 ≤ outputs < 32,768; fee ≥ 0; input index and hash sizes as above.
  4. Claims ↔ distinct input addresses one-to-one; every claim hashes to a used address.
  5. Every signature verifies against its key; no duplicate (claim, key); each claim satisfied.
  6. Every output value > 0, recipient 20 bytes; extra ≤ 100 bytes.

Against the chain state (deepTransactionCheck)

  1. 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.
  2. Locks (SIP-3, since block 35,000): an output with required_block_height or required_time is not spendable until the block's height/timestamp reaches it.
  3. SIP-4 (since 151,680): if input.value ≠ 0 it must match the spent output.
  4. SIP-6 (since 358,700): signatures of type SPHINCS+ or Dilithium are rejected before activation.
  5. Outputs: before the SIP-3 heights no requirements/extras were allowed; for_benefit_of_spec_hash if present is 20 bytes; in v1 blocks target_shard must 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).
  6. Σ inputs = Σ outputs + fee.
Note for implementersThe value sums are plain 64-bit additions; there is no explicit overflow guard beyond each output being positive. Independent implementations should add one.

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_height and required_time make 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_hash marks an output paid to one address as being held for another. No consensus meaning; every node indexes it and serves GetFBOList.
Identifiers
ids.username / ids.channelname claim a name. Nodes index names case- and accent-insensitively (Unicode NFC + primary-strength collation, then Skein) and GetIDList returns 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 at BASIC_FEE = 2.5 flakes/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.2 flakes/byte only get the first LOW_FEE_SIZE_IN_BLOCK = 100,000 bytes 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 qhard spend is ~4.4 KB, a pqc1 spend ~15 KB.

Mempool

  • One mempool per shard the node builds on (MetaMemPool offers 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_hash in 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 (GetUTXONode with include_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=true the node writes an a2tx ‖ address ‖ tx_id → (height, block hash, tx id) entry for every input and output address of every transaction. GetAddressHistory returns 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=true the node keeps every transaction by id (GetTransaction) and a tx2b map for GetTransactionStatus (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:

  1. Collect spendable outputs of all wallet addresses - verified against the UTXO root, with the mempool overlaid - grouped by the shard they currently sit in.
  2. 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.)
  3. 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.
  4. 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.
  5. 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.