Skip to content

[Bug] is_valid_address rejects all Taproot (P2TR / bech32m) addresses — TAO→BTC swaps to a bc1p… payout address always fail #448

@cleanjunc

Description

@cleanjunc

Summary

BitcoinProvider.is_valid_address rejects every valid Taproot (P2TR) Bitcoin address. As a result, any TAO→BTC swap whose BTC receiving address is a Taproot address (bc1p… on mainnet, tb1p…/bcrt1p… on test networks) is rejected 100% of the time — both in the CLI before sending and in the validator at confirm time — with a misleading "Invalid destination address format" error.

Taproot is the default receive-address type in many modern wallets and exchanges, so this blocks a large and growing class of legitimate swaps.

Root cause

allways/chain_providers/bitcoin.py (is_valid_address):

def is_valid_address(self, address: str) -> bool:
    """Validate BTC address format without RPC (bech32/base58 decode)."""
    if not address or not isinstance(address, str):
        return False
    try:
        if address.lower().startswith(('bc1', 'tb1', 'bcrt1')):
            hrp, data = bech32.bech32_decode(address)
            return data is not None
        decoded = base58.b58decode_check(address)
        return len(decoded) == 21 and decoded[0] in (0x00, 0x05, 0x6F, 0xC4)
    except Exception:
        return False

bech32.bech32_decode validates only the BIP-173 bech32 checksum (polymod constant 1). Taproot addresses (witness v1) are encoded with bech32m (BIP-350, polymod constant 0x2bc830a3), so bech32_decode returns (None, None) for them and the function returns False.

Reproduction

Run against the pinned bech32 dependency:

import bech32, base58

def is_valid_address(address):
    if not address or not isinstance(address, str):
        return False
    try:
        if address.lower().startswith(('bc1', 'tb1', 'bcrt1')):
            hrp, data = bech32.bech32_decode(address)
            return data is not None
        decoded = base58.b58decode_check(address)
        return len(decoded) == 21 and decoded[0] in (0x00, 0x05, 0x6F, 0xC4)
    except Exception:
        return False

cases = {
    'P2WPKH v0 (bc1q)': 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4',
    'P2TR  v1 (bc1p)':  'bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr',
    'P2WSH v0 (bc1q)':  'bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3',
    'P2PKH (1...)':     '1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2',
    'P2SH  (3...)':     '3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy',
}
for name, addr in cases.items():
    print(f'{name:18} -> {is_valid_address(addr)}')

Output:

P2WPKH v0 (bc1q)   -> True
P2TR  v1 (bc1p)    -> False   <-- valid mainnet Taproot, rejected
P2WSH v0 (bc1q)    -> True
P2PKH (1...)       -> True
P2SH  (3...)       -> True

bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr is a valid mainnet Taproot address, yet it is rejected.

Impact

For a TAO→BTC swap the BTC address is a pure payout destination — the user only receives funds there, so no message signing is involved. Every other stage of the pipeline already supports Taproot:

  • Miner payout derives a valid P2TR scriptPubKey via embit's address_to_scriptpubkey.
  • Validator dest-tx verification matches on Esplora's scriptpubkey_address, which is the Taproot address.

The only thing that fails is the format gate:

  • allways/validator/axon_handlers.py (handle_swap_confirm):
if not to_provider.is_valid_address(synapse.to_address):
    reject_synapse(synapse, 'Invalid destination address format', ctx)
    return synapse
  • allways/cli/swap_commands/swap.py:
if not to_provider.is_valid_address(receive_address):
    ...

So a user swapping TAO→BTC into a Taproot wallet is blocked deterministically, every time, with a misleading "invalid format" message for an address that is valid and fully payable/verifiable by the rest of the system.

Note: the "Taproot not yet supported" comments elsewhere in bitcoin.py refer to BIP-137 message signing, which only applies to a BTC source address (BTC→TAO). They do not apply to a BTC payout destination, so this rejection is over-broad.

Expected behavior

is_valid_address should accept valid Taproot addresses (witness v1+, bech32m), consistent with the miner send path and the validator verification path that already handle them.

Suggested fix

Make is_valid_address witness-version-aware: validate witness v0 with bech32 and witness v1+ with bech32m (BIP-350). The currently pinned bech32 package does not implement bech32m, so either add a bech32m checksum check (polymod constant 0x2bc830a3) for v1+ programs or switch to a library that implements BIP-350.

Environment

  • File: allways/chain_providers/bitcoin.py
  • Affected flows: TAO→BTC swaps with a Taproot BTC receiving address (CLI swap and validator handle_swap_confirm).
  • Reproducible: every time.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions