Chain Client Implementation Guide

This guide explains how to implement a new ChainClient in THORChain's Bifrost module. It assumes the chain has passed the evaluation process and received Node Mimir approval.

Chain clients enable THORChain to:

  • Observe inbound L1 transactions
  • Sign and broadcast outbound vault transactions
  • Track vault balances and emit solvency reports
  • Handle chain-specific behaviors like mempool handling, reorgs, and gas estimation

Most chains extend existing clients — see EVM, UTXO, or BFT.

Directory Layout

All chain clients live under:

/bifrost/pkg/chainclients

If your chain fits an existing type (EVM, UTXO, BFT), you should place your client in the corresponding shared folder and extend the base implementation. This allows you to reuse common logic and minimize custom code.

FolderTypePurpose / NotesExample Chains
evm/SharedFor EVM-compatible chainsEthereum, BSC, AVAX, Base
utxo/SharedFor Bitcoin-style UTXO chainsBitcoin, Litecoin, Dogecoin
bft/SharedFor Cosmos SDK or Tendermint-style chainsCosmos Hub, GIA, Noble
solana/CustomCustom logic (non-shared Solana client)Solana
xrp/CustomCustom Ripple clientXRP
tron/CustomCustom TRON clientTRON

How to Choose

  • Use an existing shared type folder (evm/, utxo/, bft/) if your chain's architecture matches.
  • Use a new folder (e.g. solana/, xrp/, tron/) only if the chain requires custom observation, signing, or RPC handling that doesn't align with existing types.

Example:

  • evm/ethereum/client.go – Ethereum-specific config using shared EVM logic
  • utxo/bitcoin/client.go – Bitcoin-specific config using shared UTXO logic
  • xrp/client.go – XRP-specific logic using custom implementation

Required Interfaces

You must implement the following core interfaces:

InterfacePurpose
ChainClientMain interface for observation, signing, solvency
ObserverWatches for inbound txs
SignerBuilds and signs outbound txs
BlockScannerScans blocks and mempool

See the ChainClient interface for method-level detail.

Vault Address Derivation

Every client must derive vault addresses from the TSS public key:

ChainAlgoMethod
BitcoinECDSAP2WPKH from compressed pubkey
EthereumECDSAkeccak256(pubkey)[12:], checksummed
SolanaEDDSABase58-encoded ed25519

Use:

btcec.PublicKey.SerializeCompressed()     // ECDSA
edwards25519.PublicKey.Bytes()            // EDDSA

Implement:

func (c *YourChain) GetAddress(pubkey common.PubKey) string
  • Handle both PubKey and PubKeyEddsa
  • Ensure address format is deterministic

Observation Logic

Each client must observe inbound txs and forward them to THORChain.

Implement:

func (c *YourChain) FetchTxs(height int64) ([]types.TxIn, error)
func (c *YourChain) FetchMemPool() ([]types.TxIn, error) // optional

Inbound txs must:

  • Be directed to active or retiring vaults
  • Include a valid THORChain memo
  • Be pushed to the global tx queue:
globalTxsQueue <- types.TxIn

Dust Threshold

Prevent spam by implementing:

func (c *YourChain) GetThreshold() cosmos.Uint

Return the minimum inbound amount considered valid.

Memo Parsing

Memos must be:

  • Present
  • Decoded and parsed via x/thorchain/memo
  • Rejected if invalid or missing

Confirmation Counting

Chains with delayed finality (EVM, UTXO) must track confirmations:

func (c *YourChain) GetConfirmationCount(txIn types.TxIn) int64

Use:

RequiredConfirmations = min((TxValue / BlockReward) × Multiplier, MAXCONFIRMATIONS)
  • MAXCONFIRMATIONS-<CHAIN> is set via Mimir

Outbound Signing

Outbound txs are signed by the TSS and passed to your ChainClient.

Implement:

SignTx(txOut types.TxOutItem, height int64) ([]byte, []byte, *types.TxInItem, error)
BroadcastTx(txOut types.TxOutItem, rawTx []byte) (string, error)
  • Encode signature correctly (ECDSA r,s,v or EDDSA)
  • Return the tx hash
  • Submit outbound observation via MsgOutboundTx

THORChain does not verify the tx on-chain — incorrect hashes are slashable.

Gas Estimation

Clients must report gas rates and outbound fees:

  • GetGasRate() — gas price estimate (per block)
  • GetMaxGas() — ceiling per tx
  • GetFee() — base fee (used for outbound calculation)

See Gas Tracking for details.

Reorg Handling

If a previously observed tx disappears due to a reorg:

  • Emit an ErrataTx
  • Allow THORChain to revert state

Ensure short-range reorgs are gracefully handled in block polling logic.

Solvency Reporting

Clients must emit vault balance data periodically:

globalSolvencyQueue <- types.Solvency

Missing funds (beyond PermittedSolvencyGap) will halt the chain.

Stuck Transactions

Clients must detect stuck outbound txs due to:

  • Low gas
  • Dust
  • Mempool eviction

EVM chains must implement the unstuck logic:

  • Reuse the same nonce
  • Send a 0-value tx to self
  • Use max(gasPrice × 1.1, 2 × current median gas)

Testing & Simulation

Before submitting your PR:

  • Launch the chain on stagenet or testnet

  • Test:

    • Inbound observation
    • Outbound signing
    • Vault churn
    • Memo parsing
    • Fee behavior
    • Errata + solvency
  • Use:

Also see New Chain Process for required test volumes.