[V136] feat: add Bitcoin Cash (BCH)

Summary

This MR adds Bitcoin Cash (BCH) as a new chain from 1.136.0. It is part 5 of the V136 chain stack (tracking issue #325) and targets v136/03-bsc. Review only this branch's commits.

Most of the diff can be skipped:

  • 7abb3d5b3 vendors THOR's bifrost/txscript/bchd-txscript unchanged (THOR 5f25d7289): 39 files, about 24 750 lines.
  • test/regression/mnt/ holds the regenerated exports and blocks: 57 files, about 4 800 lines.
  • What is left to review is about 67 files and 7 100 lines.

BCH stays inert until the network reaches 1.136.0. This relies on the chain-activation gate from v136/00-chain-activation.

Changes

common ([V136])

  • d97931f69: adds BCHChain and BCHAsset. Every BCH case in the common predicates is behind IsActivated(): gas asset and units (satsperbyte, 1e8), dust 10 000, MaxMemoLength 220, coinbase 3.125, block time 600 s, InboundNotes. PubKey.GetAddress returns NoAddress before the cache while BCH is inactive. There is no short code.
  • CashAddr parsing in the V136 address set (NewAddressV136, IsChainV136, getNetworkV136, getChainV136):
    • only canonical P2PKH/P2SH CashAddr is accepted: 20-byte hash, no prefix, all lower or all upper case;
    • bchutil.DecodeAddress maps token-aware and unknown version bytes to P2PKH, so the decoder re-encodes the result and requires the same string;
    • legacy addresses, P2SH32 and token-aware (z…/r…) addresses are rejected.
  • getChainV136 appends BCH last, and only once it is activated.

x/mayachain ([V136])

  • b734ae6b9: BCH is in SUPPORT_CHAINS_VCURRENT for mainnet, stagenet and mocknet.
  • AddLiquidityHandler.validateV136 (= validateV128 plus one check): the asset address of a BCH pool must pass IsChain(BCH), and on every chain the asset address must be of the current network (THOR e64139a91f). validateV128 moved unchanged to the archive.
  • migrateStoreV136 calls migrateStoreV136BCHAliases. It deletes MAYAName BCH aliases that fail IsChainV136, and clears a BCH preferred asset left without an alias. It runs on every network, collects first, then writes.
  • Quotes: BCH is in the MaxConfirmations-<chain> list, with a confirmation floor of 6. This is query only.
  • GetRandomBCHAddress returned a DASH address. Fixed.

Bifrost ([Version-unspecific]; BCH only runs after the activation gate)

  • 35a475f5b: new bifrost/pkg/chainclients/bitcoincash/. It is cloned from MAYA's bitcoin/, so it carries the BTC fixes, plus the 01 hardening. The BCH parts come from THOR:
    • addresses come from the script bytes, never from the RPC addresses field;
    • a vault-relevance filter runs before any getrawtransaction, so unrelated txs cost no lookup;
    • the sender is input 0's prevout, single signature only, with index bounds;
    • signing forces FORKID, every input is engine-verified, and NULLFAIL is an error;
    • there is no createwallet (only importaddress), the mempool scan reports vault spends only (inbounds are block-only), and HALTSIGNING[BCH] gates consolidation;
    • a destination that is not a P2PKH/P2SH CashAddr for this network is an error and is counted. THOR skips it silently.
  • 09e266ced: wiring (loadchains.go, env binds, config/default.yaml bch: block, metrics, exinfo solvency cadence 2). BCH is removed from haltchain_known_thorchain_chains, and Makefile BIFROST_UTXO_CLIENT_PKGS now includes the BCH client.

Parity fixes after the THOR comparison ([Version-unspecific]): e20436833, 527055a0c, e8ad66a1d, 06bba62c1, c221f61e1, d60af21e6, 4edd512e3, 433353def. See "THORNode references and parity".

  • ebeaabfa0 runs TestConfirmationsFailClosedWithoutAsgards off mocknet only: on mocknet MaxConfAdjustment always returns 1, so the test failed under make test-network-specific.

Build, simulation, docs and tests

  • 36a39d15f: mocknet bitcoin-cash service. It runs upstream zquestz/bitcoin-cash-node:29.0.0, pinned by digest, in regtest with -txindex.
  • 90c7900ca: simulation, with a BCH lite client, funding, a consolidate actor and the static chain list.
  • 9bce0f9bc: docs/chains/bitcoincash.md (rules and launch runbook) and the confirmation mimirs in docs/mimir.md.
  • f94d6e2fc, 9905d29ed, 7d846682d, 7d1d015d8: activation-rule tests, BCH test vectors, the regression suites suites/bch/*, and the regenerated exports.

Versioning and consensus impact

The [V136] commits change consensus behaviour from 1.136.0 only:

  • the address set parses CashAddr;
  • BCH joins the support list;
  • the add-liquidity guards (BCH CashAddr, asset address network);
  • the alias migration.

Below 1.136.0, every predicate returns for BCH what it returns for an unknown chain. TestBCHActivationRule checks this with enforcement on at 1.135.0.

MsgObservedTxIn/Out for BCH fail ValidateBasic (code 6) at 1.135.0 and pass at 1.136.0. The regression suite bch-version-gate checks this end to end:

  • a genesis node at 1.135.0 drops the BCH observation and refunds the CashAddr memo;
  • after tx-version 1.136.0, the migration removes the legacy alias and the same observation is accepted.

Older versioned bodies are unchanged and still treat BCH as unknown (TestBCHOlderVersionsUnchanged). lint-versions passes without #check-lint-warning.

The Bifrost, quote and docs commits have no state effect.

CashTokens

  • Token outputs are never credited: getOutput and isRelevant skip them (cashtoken_output_ignored).
  • Token UTXOs are never spent. getUTXOs excludes them from balance, coin selection, vinsUnspent and consolidation (cashtoken_utxo_excluded). Each token UTXO is logged and counted once.
  • BCHN hides token UTXOs from listunspent unless includeTokens is set. The client now sets it, so the exclusion filter and the metric run on real data. This was found in the manual test.

Confirmations and fees

  • Confirmations are value / (subsidy(height) × ConfMultiplierBasisPoints-BCH / 1e4). The result is capped at MaxConfirmations-BCH, with a floor of min_confirmations (default 6). The coinbase is never read.
  • Vault spends need 0 confirmations, as on BTC.
  • If the asgard list cannot be read, every tx value is counted. This can only raise the count (fail closed).
  • The network fee comes from getblockstats avgfeerate, clamped to min/max_sats_per_byte (default 2..50 sat/B, at least the node's relay fee rate).
  • The size estimate is an upper bound. Change below 546 sats goes to the fee. A memo over 220 bytes is an error.
  • The broadcast sends allowhighfees, as THOR does for BCH.

THORNode references and parity

The full comparison with THOR's BCH client is in the audit. It found no critical or high findings. Ported into this branch:

Finding Commit Change THOR source
BC-1 e20436833 A non-printable memo is observed as an empty memo, so it is refunded instead of dropped getMemo in client_internal.go
BC-3, BC-7 527055a0c Keep the last vault list when a refresh fails; with no list, retry the block; confirmations fail closed e91742a061
BC-5 e8ad66a1d Rebuild the unsigned tx when the keysign party never forms (KeysignError.IsJoinPartyFailure) c69669e840
BC-8 06bba62c1 On a reorg, walk back by hash to the common ancestor. A read failure retries instead of losing the reorg 87d4dfa89
BC-9 c221f61e1 sendrawtransaction with allowhighfees BCH case in signer.go BroadcastTx
BC-10 d60af21e6 (+ TestProcessReOrg in 06bba62c1) Port THOR's IgnoreTx, GetGas, GetOutput, OnObservedTxIn and IsSelfTransaction tests bitcoincash_test.go
X-7, X-8 4edd512e3 Quote: round the coinbase (3.125, not 3) and guard ConfMultiplierBasisPoints = 0. This also fixes BTC/ZEC/ETH quotes querier_quotes.go
X-15 433353def docs/mimir.md: what ConfMultiplierBasisPoints scales on each chain —

Handled elsewhere:

  • BC-4 (signer-cache expiry, THOR a759cb4f99) is in !880 (merged). Wiring BCH into it is a follow-up once both have landed.
  • X-14: there is no BCH.BCH pool on mainnet (checked 2026-10-20), so no pool cleanup is needed.

Intentional differences from THOR:

  • the subsidy-only confirmation formula and the floor of 6;
  • the fee clamp;
  • the mempool scan reports vault spends only;
  • the 220-byte memo limit;
  • an error on a bad destination;
  • CashAddr only.

Tests

  • Unit tests:
    • every audit item and validation item has a test, and each was checked to fail before its fix;
    • make test and make test-network-specific pass;
    • go test -race passes on bitcoincash, bchd-txscript, common, config and exinfo;
    • FuzzDecodeBCHCashAddrV136 passes;
    • lint (gofumpt, handlers, managers, analyze, versions) passes.
  • Regression: full EXPORT=1 run, 147/147 suites. The only failure is swaps/streaming-swaps line 81. That suite passed on its own 2 out of 2 times, so the failure is a flake. Exports are unchanged after the parity fixes.
  • Mocknet simulation: the full static simulation passes (248 actors). Run on 2026-10-19. It ran before the THOR-parity fixes; the regression run above includes them. Run 1 failed on Swap BCH.BCH => ZEC Unified: the memo was 233 bytes, over the 220 limit. The simulation now skips the full Unified kind for BCH.
  • Manual CashTokens test on BCHN 29 regtest:
    • a token-only output to the vault is not observed;
    • a mixed tx credits only the plain output, for the exact sats;
    • refunds and a 10-input consolidation never spend the 2 token UTXOs (checked with gettxout);
    • the vault balance excludes them;
    • the RPC shapes match what the client assumes: tokenData in getblock 2 and listunspent, and avgfeerate in BCH per byte.

Deferred and open items

  • BC-6: keysign is not pinned to the TxOut height. This is not done: MAYA TSS has no leader rotation, so pinning risks a stuck leader. A decision is needed.
  • X-13: an upper-case CashAddr alias is valid and is kept upper-case. Lower-casing it in the migration is optional.
  • X-16: THOR's keygen chain-set preflight and churn-out (25989b1f7e, 161ba2adaf) are deferred to V137. Check the keygen report by hand in the runbook meanwhile.
  • Still open from #325:
    • mocknet uses a third-party BCHN image pinned by digest; a MAYA-built image is still to be published;
    • the smoke test bitcoin_cash.py is not restored;
    • getGas still makes one RPC per input; batching is not ported.

Operator notes

  • bch ships with disabled: true. Enable it (BIFROST_CHAINS_BCH_DISABLED=false) only after /mayachain/version reports 1.136.0 or later.
  • BCHN must run with -txindex.
  • min_confirmations and min/max_sats_per_byte are node config, but they feed the vote. They must be identical on every node.
  • Launch order and mimirs are in the runbook in docs/chains/bitcoincash.md:
    1. Before the 1.136 binaries: set HaltBCHChain, HaltBCHTrading and PauseLPBCH, and set ConfMultiplierBasisPoints-BCH=5000 and MaxConfirmations-BCH=72.
    2. Enable BCH and churn.
    3. Unhalt the chain and let it scan to the tip.
    4. Unpause LP, with depth caps.
    5. Unhalt trading.
  • node-launcher needs a BCHN deployment and the bch chain section (separate repo).

CI follow-up

  • bab5d31c1 clears the trunk check findings in the BCH client: renamed shadowed errors, a nolint:gosec on the cashtoken_utxo_excluded metric label (G101 false positive), and ${BCH_DISABLED:-true} in node-status.sh (SC2153). No behaviour change.

Review follow-up (round 4)

  • 242928b05: only P2PKH/P2SH outputs are credited to a vault (receiverAddress); P2PK stays accepted for the sender. This replaces the deferred BC-2 item.
  • e7dd5927c: BCH fee floor raised to 2 sat/B (default and config, THOR parity), and the relay floor is size-based (relayfee is per kB), so MaxGas at the lowest posted rate covers a ten-input outbound.
  • bed28cbea: transient funding-tx lookups make the block retryable and untrack what the call tracked; the re-org is committed only after the current block is read.
  • e909006da: two-pass re-org handling: an Unknown re-confirmation keeps the re-org pending, with nothing saved and no errata until it resolves.
  • b7253f261: each rescanned re-org block is sent as its own deck, so confirmation counting is per height.
  • 2d80285c3: FetchMemPool reports vault spends (posted not final by !884), and OnObservedTxIn no longer hides the block observation.
  • 508d62a55 [V136]: validateV136 rejects an asset address of another network on every chain (THOR e64139a91f).
Edited by Bitol Maya

Merge request reports

Loading
Loading