Support bech32 and bech32m (taproot) addresses in the query command

Coin.BECH32_HRP (plm, tplm, rplm) and a BIP173/BIP350 segwit decoder let
address_to_hashX read segwit addresses. The witness transaction fixtures are
no longer skipped.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
davideandClaude Sonnet 5.5 committed 2026-10-23 15:32:17 +02:00
1 parent 2ef6a7abcb
commit b7ce0efad1
6 files changed
+170 -10

No files matched your search

+12 -5
View File
@@ -36,7 +36,7 @@ from hashlib import sha256
from typing import Sequence, Optional, Type
import electrumx.lib.util as util
from electrumx.lib.hash import Base58, double_sha256, hash_to_hex_str, HASHX_LEN
from electrumx.lib.hash import Base58, Bech32, double_sha256, hash_to_hex_str, HASHX_LEN
from electrumx.lib.script import ScriptPubKey
import electrumx.lib.tx as lib_tx
from electrumx.lib.tx import Tx
@@ -72,6 +72,7 @@ class Coin:
P2PKH_VERBYTE = bytes.fromhex("00")
P2SH_VERBYTES = (bytes.fromhex("05"),)
DECODE_CHECK = Base58.decode_check
BECH32_HRP: Optional[str] = None # human-readable part of the segwit addresses
GENESIS_HASH = ('000000000019d6689c085ae165831e93'
'4ff763ae46a2a6c172b3f1b60a8ce26f') # 'hum' byte-order
@@ -184,9 +185,11 @@ class Coin:
def pay_to_address_script(cls, address):
'''Return a pubkey script that pays to a pubkey hash.
Pass the address (either P2PKH or P2SH) in base58 form.
Only used for the local RPC 'query' command (bech32 addresses are not supported).
Pass the address in base58 form (P2PKH or P2SH) or in bech32 form (segwit).
Only used for the local RPC 'query' command.
'''
if cls.BECH32_HRP and address.lower().startswith(cls.BECH32_HRP + '1'):
return ScriptPubKey.segwit_script(*Bech32.decode_segwit(cls.BECH32_HRP, address))
raw = cls.DECODE_CHECK(address)
# Require version byte(s) plus hash160.
@@ -261,15 +264,17 @@ class Coin:
class Palladium(Coin):
"""Palladium (PLM), a Bitcoin Core 0.19-based chain. Standard 80-byte headers, sha256d PoW, segwit.
"""Palladium (PLM). Standard 80-byte headers, sha256d PoW, segwit and taproot (scripts are indexed by hash,
so no special handling is needed for either).
Its daemon predates 'txospenderindex', 'getindexinfo' and 'submitpackage', so
Its daemon has no 'txospenderindex', 'getindexinfo' or 'submitpackage', so
'blockchain.outpoint.subscribe' and 'blockchain.transaction.broadcast_package' will
return daemon errors to clients. The daemon needs 'txindex=1'.
"""
NAME = "Palladium"
SHORTNAME = "PLM"
NET = "mainnet"
BECH32_HRP = "plm"
P2PKH_VERBYTE = bytes.fromhex("37")
P2SH_VERBYTES = (bytes.fromhex("05"),)
# same genesis block as Bitcoin mainnet
@@ -303,6 +308,7 @@ class Palladium(Coin):
class PalladiumTestnet(Palladium):
SHORTNAME = "TPLM"
NET = "testnet"
BECH32_HRP = "tplm"
P2PKH_VERBYTE = bytes.fromhex("7f")
P2SH_VERBYTES = (bytes.fromhex("73"),)
# same genesis block as Bitcoin testnet3
@@ -318,6 +324,7 @@ class PalladiumTestnet(Palladium):
class PalladiumRegtest(PalladiumTestnet):
NET = "regtest"
BECH32_HRP = "rplm"
# same genesis block as Bitcoin regtest
GENESIS_HASH = ('0f9188f13cb7b2c71f2a335e3a4fc328'
'bf5beb436012afca590b1a11466e2206')
+75
View File
@@ -137,3 +137,78 @@ class Base58:
into a Base58Check string."""
be_bytes = payload + hash_fn(payload)[:4]
return Base58.encode(be_bytes)
class Bech32Error(Exception):
'''Exception used for Bech32 and Bech32m errors.'''
class Bech32:
'''Decoding of segwit addresses, see BIP 173 (bech32) and BIP 350 (bech32m).'''
CHARSET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l'
BECH32_CONST = 1
BECH32M_CONST = 0x2bc830a3
@staticmethod
def _polymod(values):
gen = (0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3)
chk = 1
for value in values:
top = chk >> 25
chk = (chk & 0x1ffffff) << 5 ^ value
for i in range(5):
chk ^= gen[i] if (top >> i) & 1 else 0
return chk
@staticmethod
def _convert_bits(data, from_bits, to_bits):
'''General power-of-2 base conversion without padding. Returns None if the input is invalid.'''
acc = 0
bits = 0
ret = bytearray()
maxv = (1 << to_bits) - 1
for value in data:
acc = (acc << from_bits) | value
bits += from_bits
while bits >= to_bits:
bits -= to_bits
ret.append((acc >> bits) & maxv)
if bits >= from_bits or (acc << (to_bits - bits)) & maxv:
return None
return bytes(ret)
@staticmethod
def decode_segwit(hrp, address):
'''Decode a segwit address of the given human-readable part. Returns (witness version,
witness program) or raises Bech32Error.'''
if not isinstance(address, str):
raise TypeError('a string is required')
if any(ord(c) < 33 or ord(c) > 126 for c in address):
raise Bech32Error('invalid character in address')
if address.lower() != address and address.upper() != address:
raise Bech32Error('mixed case address')
address = address.lower()
pos = address.rfind('1')
if pos < 1 or pos + 7 > len(address) or len(address) > 90:
raise Bech32Error('invalid address length or separator position')
if address[:pos] != hrp:
raise Bech32Error(f'unexpected human-readable part "{address[:pos]}"')
data = [Bech32.CHARSET.find(c) for c in address[pos + 1:]]
if -1 in data:
raise Bech32Error('invalid bech32 character')
const = Bech32._polymod([ord(c) >> 5 for c in hrp] + [0] + [ord(c) & 31 for c in hrp] + data)
if const not in (Bech32.BECH32_CONST, Bech32.BECH32M_CONST):
raise Bech32Error('invalid bech32 checksum')
witver = data[0]
if witver > 16:
raise Bech32Error('invalid witness version')
# version 0 uses bech32, later versions use bech32m
if const != (Bech32.BECH32_CONST if witver == 0 else Bech32.BECH32M_CONST):
raise Bech32Error('checksum does not match the witness version')
program = Bech32._convert_bits(data[1:-6], 5, 8)
if program is None or not 2 <= len(program) <= 40:
raise Bech32Error('invalid witness program')
if witver == 0 and len(program) not in (20, 32):
raise Bech32Error('invalid witness program length for version 0')
return witver, program
+5
View File
@@ -94,6 +94,11 @@ class ScriptPubKey:
+ Script.push_data(hash160)
+ bytes((OpCodes.OP_EQUALVERIFY, OpCodes.OP_CHECKSIG)))
@classmethod
def segwit_script(cls, witver, program):
'''The script of a segwit output: the version (OP_0, OP_1 ... OP_16) and the witness program.'''
return bytes((OpCodes.OP_0 if witver == 0 else OpCodes.OP_1 + witver - 1,)) + Script.push_data(program)
class Script:
+2 -2
View File
@@ -38,7 +38,7 @@ import electrumx
import electrumx.lib.util as util
from electrumx.lib.lrucache import LRUCache
from electrumx.lib.util import OldTaskGroup, is_hex_str, get_running_loop
from electrumx.lib.hash import (HASHX_LEN, Base58Error, hash_to_hex_str,
from electrumx.lib.hash import (HASHX_LEN, Base58Error, Bech32Error, hash_to_hex_str,
hex_str_to_hash, sha256, double_sha256)
from electrumx.lib.merkle import MerkleCache
from electrumx.lib.text import sessions_lines
@@ -714,7 +714,7 @@ class SessionManager:
hashX = coin.address_to_hashX(arg)
lines.append(f'Address: {arg}')
return hashX
except Base58Error:
except (Base58Error, Bech32Error):
pass
return None
+76 -1
View File
@@ -4,6 +4,8 @@ import pytest
from electrumx.lib import tx as lib_tx
from electrumx.lib.coins import Coin, Palladium, PalladiumTestnet, PalladiumRegtest
from electrumx.lib.hash import Base58Error, Bech32, Bech32Error
from electrumx.lib.script import ScriptPubKey
# (class, net, rpc port, P2PKH verbyte, P2SH verbyte, genesis hash)
PARAMS = [
@@ -35,7 +37,80 @@ def test_lookup_coin_class(coin, net):
def test_daemon_requirements_disabled():
# Palladium Core (Bitcoin Core 0.19 based) has no getindexinfo/txospenderindex
# Palladium Core has no getindexinfo/txospenderindex
for coin, *_ in PARAMS:
assert coin.MIN_REQUIRED_DAEMON_VERSION is None
assert not coin.REQUIRED_DAEMON_INDEXES
def bech32_encode_v0(hrp, program):
"""Minimal bech32 encoder of a witness v0 address, only to build test addresses"""
charset = Bech32.CHARSET
acc = bits = 0
data = [0]
for byte in program:
acc = (acc << 8) | byte
bits += 8
while bits >= 5:
bits -= 5
data.append((acc >> bits) & 31)
if bits:
data.append((acc << (5 - bits)) & 31)
values = [ord(c) >> 5 for c in hrp] + [0] + [ord(c) & 31 for c in hrp] + data
polymod = Bech32._polymod(values + [0] * 6) ^ Bech32.BECH32_CONST
checksum = [(polymod >> 5 * (5 - i)) & 31 for i in range(6)]
return hrp + "1" + "".join(charset[d] for d in data + checksum)
# bech32 / bech32m: the valid and invalid segwit addresses of BIP 173 and BIP 350
BIP_VALID = [
("bc", "BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4", 0, "751e76e8199196d454941c45d1b3a323f1433bd6"),
("tb", "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7", 0,
"1863143c14c5166804bd19203356da136c985678cd4d27a1b8c6329604903262"),
("bc", "bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kt5nd6y", 1,
"751e76e8199196d454941c45d1b3a323f1433bd6751e76e8199196d454941c45d1b3a323f1433bd6"),
("bc", "BC1SW50QGDZ25J", 16, "751e"),
]
BIP_INVALID = [
("bc", "tc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vq5zuyut"), # wrong hrp
("bc", "bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqh2y7hd"), # bech32 for version 1
("bc", "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kemeawh"), # bech32m for version 0
("bc", "BC1S0XLXVLHEMJA6C4DQV22UAPCTQUPFHLXM9H8Z3K2E72Q4K9HCZ7VQ54WELL"), # bad checksum
("bc", "Bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4"), # mixed case
("bc", "bc1gmk9yu"), # empty program
("bc", "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t5"), # bad checksum
]
@pytest.mark.parametrize("hrp,address,witver,program", BIP_VALID)
def test_bech32_decode_segwit(hrp, address, witver, program):
assert Bech32.decode_segwit(hrp, address) == (witver, bytes.fromhex(program))
@pytest.mark.parametrize("hrp,address", BIP_INVALID)
def test_bech32_decode_segwit_invalid(hrp, address):
with pytest.raises(Bech32Error):
Bech32.decode_segwit(hrp, address)
def test_segwit_script():
assert ScriptPubKey.segwit_script(0, bytes(20)) == b"\x00\x14" + bytes(20)
assert ScriptPubKey.segwit_script(1, bytes(32)) == b"\x51\x20" + bytes(32)
assert ScriptPubKey.segwit_script(16, b"\x75\x1e") == b"\x60\x02\x75\x1e"
@pytest.mark.parametrize("coin,hrp", [(Palladium, "plm"), (PalladiumTestnet, "tplm"), (PalladiumRegtest, "rplm")])
def test_bech32_address_to_script(coin, hrp):
assert coin.BECH32_HRP == hrp
# a v0 witness keyhash address built from a known hash160 (the checksum is computed with a constant)
h160 = bytes.fromhex("751e76e8199196d454941c45d1b3a323f1433bd6")
address = bech32_encode_v0(hrp, h160)
assert coin.pay_to_address_script(address) == b"\x00\x14" + h160
assert coin.pay_to_address_script(address.upper()) == b"\x00\x14" + h160
assert coin.address_to_hashX(address) == coin.hashX_from_script(b"\x00\x14" + h160)
def test_bech32_address_of_another_network_is_rejected():
address = bech32_encode_v0("tplm", bytes(20))
with pytest.raises((Base58Error, Bech32Error)): # not a mainnet bech32 address, so it is read as base58
Palladium.pay_to_address_script(address)
-2
View File
@@ -61,7 +61,5 @@ def test_transaction(transaction_details):
spk = vout[i]['scriptPubKey']
tx_pks = tx.outputs[i].pk_script
assert spk['hex'] == tx_pks.hex()
if spk['type'].startswith("witness_"):
continue # bech32 addresses are not supported by Coin.address_to_hashX
address, = spk["addresses"]
assert coin.address_to_hashX(address) == coin.hashX_from_script(tx_pks)