Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions allways/chain_providers/bitcoin.py
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,17 @@ def get_wif(self, address: str) -> Optional[str]:
result = self.rpc_call('dumpprivkey', [address])
return result if isinstance(result, str) else None

def is_proof_supported(self, address: str) -> bool:
"""Whether ownership of this address can be proven via the BIP-137 proof path.

``is_valid_address`` accepts every type embit can decode (incl. Taproot), which
is correct for swap *destinations*. Swap *sources* additionally need a reserve
proof, and ``sign_from_proof`` / ``verify_from_proof`` only handle P2PKH, P2WPKH
and P2SH-P2WPKH. Taproot (P2TR) and unrecognized types return False so callers
can reject a bc1p source up front instead of dead-ending after validation.
"""
return detect_address_type(address) in (ADDR_TYPE_P2PKH, ADDR_TYPE_P2WPKH, ADDR_TYPE_P2SH_P2WPKH)

def sign_from_proof(self, address: str, message: str, key: Optional[Any] = None) -> str:
"""Sign a message proving ownership of a Bitcoin address.

Expand Down
10 changes: 10 additions & 0 deletions allways/cli/swap_commands/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,16 @@ def sign_or_prompt_external(

Returns an empty string when no valid signature is obtained.
"""
# For BTC sources, address types the proof path can't sign/verify (Taproot)
# otherwise pass is_valid_address, then dead-end at the misleading "no signing
# key" prompt below. Reject them up front with the actual reason.
if chain == 'btc' and not getattr(provider, 'is_proof_supported', lambda _addr: True)(address):
console.print(
f'[red]Taproot (bc1p) source addresses are not yet supported for reserve proofs: {address}\n'
'Move the funds to a SegWit (bc1q...), P2SH (3...) or legacy (1...) address and try again.[/red]'
)
return ''

try:
signature = provider.sign_from_proof(address, message, key)
except Exception as e:
Expand Down
22 changes: 22 additions & 0 deletions tests/test_bitcoin_signing.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,28 @@ def make_lightweight_provider() -> BitcoinProvider:
return BitcoinProvider()


class TestIsProofSupported:
"""is_proof_supported gates swap *sources* to address types the BIP-137 proof
path can actually sign/verify, so a Taproot source is rejected up front instead
of passing is_valid_address and dead-ending at reserve (issue #476)."""

def test_p2wpkh_supported(self):
assert make_lightweight_provider().is_proof_supported('bc1q6tvmnmetj8vfz98vuetpvtuplqtj4uvvwjgxxc')

def test_p2pkh_supported(self):
assert make_lightweight_provider().is_proof_supported('1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa')

def test_p2sh_p2wpkh_supported(self):
assert make_lightweight_provider().is_proof_supported('37XAVCtKEvPbx2rpkxx7FmrUsetFXSawx5')

def test_taproot_not_supported(self):
# Same bc1p source is_valid_address accepts but the proof path cannot handle.
assert not make_lightweight_provider().is_proof_supported('bc1pxyz0000000000000000000000000000000000000000000000000000')

def test_unknown_not_supported(self):
assert not make_lightweight_provider().is_proof_supported('xyz-not-a-bitcoin-address')


class TestBitcoinProviderSignFromProof:
"""Direct coverage of BitcoinProvider.sign_from_proof — the wrapper our
validator/CLI actually invoke, not the underlying library."""
Expand Down