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_RETURNoutput 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
txValueis the sum of all vault-receiving outputsblockRewardincludes 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
MaxUTXOsToSpendUTXOs. 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:
- Set
HALTSIGNINGZEC=1before any node upgrades. - Roll the new build out to the whole fleet.
- 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.