UTXO Chain Clients

THORChain supports several UTXO-based chains: Bitcoin, Litecoin, Bitcoin Cash, Dogecoin, and Zcash. These chains share a common implementation based on utxo/client.go, which handles block scanning, mempool observations, signing, consolidation, and solvency reporting.

Shared Architecture

UTXO chains use a shared client located at:

/bifrost/pkg/chainclients/utxo/client.go

Each chain has a per-chain configuration file within the utxo/ directory:

/bifrost/pkg/chainclients/utxo/bitcoin.go
/bifrost/pkg/chainclients/utxo/litecoin.go
/bifrost/pkg/chainclients/utxo/bitcoincash.go
/bifrost/pkg/chainclients/utxo/dogecoin.go
/bifrost/pkg/chainclients/utxo/zcash.go

The shared logic handles:

  • Block and mempool scanning
  • UTXO input/output analysis
  • Re-org tracking (BlockCache)
  • Child-pays-for-parent (CPFP) support
  • Fee rate reporting (sats/byte)
  • Transaction signing with btcutil
  • Vault balance tracking and solvency reporting

Inbound Observations

Inbound UTXOs are detected by scanning new blocks and mempool data. A transaction is valid for observation if:

  • It has at least one output to an Asgard vault
  • It includes an OP_RETURN output with a valid memo

The observer parses these and pushes a TxIn to THORChain once confirmed.

See Inbound Transactions. For wallet developers, see Sending UTXO Transactions for required transaction structure, dust thresholds, OP_RETURN handling, and examples of short and long memos.

Confirmation Counting

THORChain dynamically delays finality based on transaction value and miner incentives.

See Finality & Confirmation Counting.

UTXO Specifics

  • txValue is the sum of all vault-receiving outputs
  • blockReward includes the subsidy + miner fees
  • If no coinbase tx is found, defaults to 3.125 (for BTC). Rewards for each UTXO chain are defined DefaultCoinbase().

Gas Tracking

Fee is calculated as:

Fee = total inputs - total outputs

Reported to THORChain as sats/byte based on the previous block and max of last 20.

See Gas Tracking.


Vault Address Derivation

UTXO vaults use compressed ECDSA public keys to derive P2WPKH addresses.

btcec.PublicKey.SerializeCompressed()

Reorg Handling

UTXO clients cache the last BlockCacheSize = 144 blocks. When a block reappears at an existing height, previously observed transactions are checked. If missing, they are re-orged and an ErrataTx is submitted to revert state.

See Re-orgs & Errata.

UTXO Consolidation

Bitcoin and similar chains limit unconfirmed transaction ancestry to ~20 ancestors. Additionally, signing large transactions with many inputs leads to long, resource-heavy ceremonies. While TSS signing is parallelized, this can still create bottlenecks.

To manage this:

  • Trigger: Consolidation is initiated when a vault holds more than MaxUTXOsToSpend UTXOs. This threshold is chain-specific and controlled via Mimir.
  • Frequency: Evaluated once per block, but only if no consolidation is already pending.
  • Process: Bifrost generates a consolidation transaction that sends funds from the vault back to itself with memo consolidate.

THORChain validates these transactions using the MsgConsolidate handler, ensuring:

  • Inputs belong to the vault
  • Output address matches the vault
  • Memo is correct

If validation fails, the vault is slashed for the attempted consolidation amount.

Consolidation is initiated by Bifrost but validated by THORChain. This logic ensures inputs are managed proactively to prevent large, unmanageable signing payloads.

RBF Handling

Replace-by-fee (RBF) is supported for user-initiated deposits to Asgard vaults. This allows users to bump the fee on a stuck transaction by rebroadcasting it with a higher fee. Vault-generated transactions do not use RBF. Instead, fee bumping (if required) is handled via other mechanisms such as child-pays-for-parent (CPFP).

Zcash Network Upgrades

Matching Zebra upgrade schedules are an operator requirement. Before the agreed ZEC signing height reaches activation height - 40, operators must ensure their daemons report the same schedule. Different schedules can produce different transaction expiries and prevent a signing party from forming, even before activation. Update comfortably before this boundary; identical Zebra versions are not required if their schedules match.

Transparent transaction version switch (v5 → v6)

The vault signs fresh transparent outbounds as v6 (ZIP-229), the standard format on mainnet since NU6.3. v5 and v6 produce different ZIP-244 signing digests (the version/version-group id is committed in the header digest and the empty-shielded digest uses v6 personalizations), so a fleet running a mix of v5-signing and v6-signing builds cannot form a keysign party for ZEC.

Any release that changes the signed transaction version is therefore a coordinated switch gated operationally by HALTSIGNINGZEC:

  1. Set HALTSIGNINGZEC=1 before any node upgrades.
  2. Roll the new build out to the whole fleet.
  3. Release the halt (HALTSIGNINGZEC=0) only after the last node is on the new build.

In-flight v5 checkpoints signed before the switch are still accepted and finished as v5 on resume (the version is pinned in the checkpoint, not re-chosen), but over a multi-block halt they will reach their ZecTxExpiry (40 blocks) and be retired and rebuilt as v6. The v5 resume support is a short-lived safety net, not something the rollout depends on.