[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:
7abb3d5b3vendors THOR'sbifrost/txscript/bchd-txscriptunchanged (THOR5f25d7289): 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: addsBCHChainandBCHAsset. Every BCH case in thecommonpredicates is behindIsActivated(): gas asset and units (satsperbyte, 1e8), dust 10 000,MaxMemoLength220, coinbase 3.125, block time 600 s,InboundNotes.PubKey.GetAddressreturnsNoAddressbefore 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.DecodeAddressmaps 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.
getChainV136appends BCH last, and only once it is activated.
x/mayachain ([V136])
b734ae6b9: BCH is inSUPPORT_CHAINS_VCURRENTfor mainnet, stagenet and mocknet.AddLiquidityHandler.validateV136(=validateV128plus one check): the asset address of a BCH pool must passIsChain(BCH), and on every chain the asset address must be of the current network (THORe64139a91f).validateV128moved unchanged to the archive.migrateStoreV136callsmigrateStoreV136BCHAliases. It deletes MAYAName BCH aliases that failIsChainV136, 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. GetRandomBCHAddressreturned a DASH address. Fixed.
Bifrost ([Version-unspecific]; BCH only runs after the activation gate)
35a475f5b: newbifrost/pkg/chainclients/bitcoincash/. It is cloned from MAYA'sbitcoin/, 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
addressesfield; - 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(onlyimportaddress), the mempool scan reports vault spends only (inbounds are block-only), andHALTSIGNING[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.
- addresses come from the script bytes, never from the RPC
09e266ced: wiring (loadchains.go, env binds,config/default.yamlbch:block, metrics, exinfo solvency cadence 2). BCH is removed fromhaltchain_known_thorchain_chains, andMakefileBIFROST_UTXO_CLIENT_PKGSnow 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".
ebeaabfa0runsTestConfirmationsFailClosedWithoutAsgardsoff mocknet only: on mocknetMaxConfAdjustmentalways returns 1, so the test failed undermake test-network-specific.
Build, simulation, docs and tests
36a39d15f: mocknetbitcoin-cashservice. It runs upstreamzquestz/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 indocs/mimir.md.f94d6e2fc,9905d29ed,7d846682d,7d1d015d8: activation-rule tests, BCH test vectors, the regression suitessuites/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:
getOutputandisRelevantskip them (cashtoken_output_ignored). - Token UTXOs are never spent.
getUTXOsexcludes them from balance, coin selection,vinsUnspentand consolidation (cashtoken_utxo_excluded). Each token UTXO is logged and counted once. - BCHN hides token UTXOs from
listunspentunlessincludeTokensis 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 atMaxConfirmations-BCH, with a floor ofmin_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 tomin/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.BCHpool 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 testandmake test-network-specificpass;go test -racepasses onbitcoincash,bchd-txscript,common,configandexinfo;FuzzDecodeBCHCashAddrV136passes;- lint (gofumpt, handlers, managers, analyze, versions) passes.
- Regression: full
EXPORT=1run, 147/147 suites. The only failure isswaps/streaming-swapsline 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:
tokenDataingetblock 2andlistunspent, andavgfeeratein 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.pyis not restored; getGasstill makes one RPC per input; batching is not ported.
Operator notes
bchships withdisabled: true. Enable it (BIFROST_CHAINS_BCH_DISABLED=false) only after/mayachain/versionreports 1.136.0 or later.- BCHN must run with
-txindex. min_confirmationsandmin/max_sats_per_byteare 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:- Before the 1.136 binaries: set
HaltBCHChain,HaltBCHTradingandPauseLPBCH, and setConfMultiplierBasisPoints-BCH=5000andMaxConfirmations-BCH=72. - Enable BCH and churn.
- Unhalt the chain and let it scan to the tip.
- Unpause LP, with depth caps.
- Unhalt trading.
- Before the 1.136 binaries: set
- node-launcher needs a BCHN deployment and the
bchchain section (separate repo).
CI follow-up
bab5d31c1clears the trunk check findings in the BCH client: renamed shadowed errors, anolint:gosecon thecashtoken_utxo_excludedmetric label (G101 false positive), and${BCH_DISABLED:-true}innode-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 (relayfeeis 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:FetchMemPoolreports vault spends (posted not final by !884), andOnObservedTxInno longer hides the block observation.508d62a55[V136]:validateV136rejects an asset address of another network on every chain (THORe64139a91f).