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