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:
1 parent
2ef6a7abcb
commit
b7ce0efad1
6 files changed
+170
-10
No files matched your search
@@ -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')
|
||||
|
||||
@@ -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
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
Reference in new issue
Block a user