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
|
||||
|
||||
Reference in new issue
Block a user