Merge pull request #165 from newtonick/ur_output_descriptor

Ur output descriptor
This commit is contained in:
SeedSigner
2022-03-28 21:08:59 -05:00
committed by GitHub
3 changed files with 148 additions and 245 deletions
-181
View File
@@ -1,181 +0,0 @@
### bc-ur encoding stuff
from io import BytesIO
import hashlib
CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
def bech32_polymod(values):
"""Internal function that computes the Bech32 checksum."""
generator = [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 ^= generator[i] if ((top >> i) & 1) else 0
return chk
def bech32_hrp_expand(hrp):
"""Expand the HRP into values for checksum computation."""
return [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp]
def bech32_verify_checksum(hrp, data):
"""Verify a checksum given HRP and converted data characters."""
return bech32_polymod(bech32_hrp_expand(hrp) + data) == 1
def bech32_create_checksum(hrp, data):
"""Compute the checksum values given HRP and data."""
values = bech32_hrp_expand(hrp) + data
polymod = bech32_polymod(values + [0, 0, 0, 0, 0, 0]) ^ 1
return [(polymod >> 5 * (5 - i)) & 31 for i in range(6)]
def bech32_encode(hrp, data):
"""Compute a Bech32 string given HRP and data values."""
combined = data + bech32_create_checksum(hrp, data)
return hrp + "1" + "".join([CHARSET[d] for d in combined])
def bech32_decode(bech):
"""Validate a Bech32 string, and determine HRP and data."""
if (any(ord(x) < 33 or ord(x) > 126 for x in bech)) or (
bech.lower() != bech and bech.upper() != bech
):
return (None, None)
bech = bech.lower()
pos = bech.rfind("1")
if pos < 1 or pos + 7 > len(bech) or len(bech) > 90:
return (None, None)
if not all(x in CHARSET for x in bech[pos + 1 :]):
return (None, None)
hrp = bech[:pos]
data = [CHARSET.find(x) for x in bech[pos + 1 :]]
if not bech32_verify_checksum(hrp, data):
return (None, None)
return (hrp, data[:-6])
def convertbits(data, frombits, tobits, pad=True):
"""General power-of-2 base conversion."""
acc = 0
bits = 0
ret = []
maxv = (1 << tobits) - 1
max_acc = (1 << (frombits + tobits - 1)) - 1
for value in data:
if value < 0 or (value >> frombits):
return None
acc = ((acc << frombits) | value) & max_acc
bits += frombits
while bits >= tobits:
bits -= tobits
ret.append((acc >> bits) & maxv)
if pad:
if bits:
ret.append((acc << (tobits - bits)) & maxv)
elif bits >= frombits or ((acc << (tobits - bits)) & maxv):
return None
return ret
def decode(hrp, addr):
"""Decode a segwit address."""
hrpgot, data = bech32_decode(addr)
if hrpgot != hrp:
return (None, None)
decoded = convertbits(data[1:], 5, 8, False)
if decoded is None or len(decoded) < 2 or len(decoded) > 40:
return (None, None)
if data[0] > 16:
return (None, None)
if data[0] == 0 and len(decoded) != 20 and len(decoded) != 32:
return (None, None)
return (data[0], decoded)
def encode(hrp, witver, witprog):
"""Encode a segwit address."""
ret = bech32_encode(hrp, [witver] + convertbits(witprog, 8, 5))
if decode(hrp, ret) == (None, None):
return None
return ret
def bc32encode(data: bytes) -> str:
"""
bc32 encoding
see https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-004-bc32.md
"""
dd = convertbits(data, 8, 5)
polymod = bech32_polymod([0] + dd + [0, 0, 0, 0, 0, 0]) ^ 0x3FFFFFFF
chk = [(polymod >> 5 * (5 - i)) & 31 for i in range(6)]
return "".join([CHARSET[d] for d in dd + chk])
def bc32decode(bc32: str) -> bytes:
"""
bc32 decoding
see https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-004-bc32.md
"""
if bc32.lower() != bc32 and bc32.upper() != bc32:
return None
bc32 = bc32.lower()
if not all([x in CHARSET for x in bc32]):
return None
res = [CHARSET.find(c) for c in bc32.lower()]
if bech32_polymod([0] + res) != 0x3FFFFFFF:
return None
return bytes(convertbits(res[:-6], 5, 8, False))
def cbor_encode(data):
l = len(data)
if l <= 23:
prefix = bytes([0x40 + l])
elif l <= 255:
prefix = bytes([0x58, l])
elif l <= 65535:
prefix = b"\x59" + l.to_bytes(2, "big")
else:
prefix = b"\x60" + l.to_bytes(4, "big")
return prefix + data
def cbor_decode(data):
s = BytesIO(data)
b = s.read(1)[0]
if b >= 0x40 and b < 0x58:
l = b - 0x40
return s.read(l)
if b == 0x58:
l = s.read(1)[0]
return s.read(l)
if b == 0x59:
l = int.from_bytes(s.read(2), "big")
return s.read(l)
if b == 0x60:
l = int.from_bytes(s.read(4), "big")
return s.read(l)
return None
def bcur_encode(data):
"""Returns bcur encoded string and hash digest"""
cbor = cbor_encode(data)
enc = bc32encode(cbor)
h = hashlib.sha256(cbor).digest()
enc_hash = bc32encode(h)
return enc, enc_hash
def bcur_decode(data, checksum=None):
"""Returns decoded data, verifies hash digest if provided"""
cbor = bc32decode(data)
if checksum is not None:
h = bc32decode(checksum)
assert h == hashlib.sha256(cbor).digest()
return cbor_decode(cbor)
+144 -62
View File
@@ -9,9 +9,10 @@ from embit import psbt, bip39
from pyzbar import pyzbar
from pyzbar.pyzbar import ZBarSymbol
from urtypes.crypto import PSBT as UR_PSBT
from urtypes.crypto import Account, HDKey, Output, Keypath, PathComponent, SCRIPT_EXPRESSION_TAG_MAP
from urtypes.bytes import Bytes
from seedsigner.helpers.ur2.ur_decoder import URDecoder
from seedsigner.helpers.bcur import (cbor_decode, bc32decode)
from seedsigner.models.psbt_parser import PSBTParser
from . import QRType, Seed
@@ -64,15 +65,12 @@ class DecodeQR:
if self.qr_type == None:
self.qr_type = qr_type
if self.qr_type == QRType.PSBT__UR2:
self.decoder = URDecoder() # UR2 decoder
if self.qr_type in [QRType.PSBT__UR2, QRType.OUTPUT__UR, QRType.ACCOUNT__UR, QRType.BYTES__UR]:
self.decoder = URDecoder() # BCUR Decoder
elif self.qr_type == QRType.PSBT__SPECTER:
self.decoder = SpecterPsbtQrDecoder() # Specter Desktop PSBT QR base64 decoder
elif self.qr_type == QRType.PSBT__LEGACY_UR:
self.decoder = LegacyUrPsbtQrDecoder() # UR Legacy decoder
elif self.qr_type == QRType.PSBT__BASE64:
self.decoder = Base64PsbtQrDecoder() # Single Segments Base64
@@ -93,6 +91,9 @@ class DecodeQR:
elif self.qr_type == QRType.WALLET__GENERIC:
self.decoder = GenericWalletQrDecoder()
elif self.qr_type == QRType.WALLET__CONFIGFILE:
self.decoder = MultiSigConfigFileQRDecoder()
elif self.qr_type != qr_type:
raise Exception('QR Fragment Unexpected Type Change')
@@ -114,7 +115,7 @@ class DecodeQR:
# it's already str data
qr_str = data
if self.qr_type == QRType.PSBT__UR2:
if self.qr_type in [QRType.PSBT__UR2, QRType.OUTPUT__UR, QRType.ACCOUNT__UR, QRType.BYTES__UR]:
self.decoder.receive_part(qr_str)
if self.decoder.is_complete():
self.complete = True
@@ -192,17 +193,29 @@ class DecodeQR:
def get_wallet_descriptor(self):
if self.is_wallet_descriptor:
return self.decoder.get_wallet_descriptor()
if self.qr_type in [QRType.OUTPUT__UR, QRType.ACCOUNT__UR, QRType.BYTES__UR]:
cbor = self.decoder.result_message().cbor
if self.qr_type == QRType.OUTPUT__UR:
return Output.from_cbor(cbor).descriptor()
elif self.qr_type == QRType.ACCOUNT__UR:
return Account.from_cbor(cbor).output_descriptors[0].descriptor()
elif self.qr_type == QRType.BYTES__UR:
raw = Bytes.from_cbor(cbor).data
descriptor = DecodeQR.multisig_setup_file_to_descriptor(raw.decode("utf-8"))
return descriptor
else:
# All the other wallet output descriptor decoder types use the same method signature
return self.decoder.get_wallet_descriptor()
def get_percent_complete(self) -> int:
if not self.decoder:
return 0
if self.qr_type == QRType.PSBT__UR2:
if self.qr_type in [QRType.PSBT__UR2, QRType.OUTPUT__UR, QRType.ACCOUNT__UR]:
return int(self.decoder.estimated_percent_complete() * 100)
elif self.qr_type in [QRType.PSBT__SPECTER, QRType.PSBT__LEGACY_UR]:
elif self.qr_type in [QRType.PSBT__SPECTER]:
if self.decoder.total_segments == None:
return 0
return int((self.decoder.collected_segments / self.decoder.total_segments) * 100)
@@ -233,12 +246,10 @@ class DecodeQR:
return self.qr_type in [
QRType.PSBT__UR2,
QRType.PSBT__SPECTER,
QRType.PSBT__LEGACY_UR,
QRType.PSBT__BASE64,
QRType.PSBT__BASE43,
]
@property
def is_seed(self):
return self.qr_type in [
@@ -262,8 +273,14 @@ class DecodeQR:
@property
def is_wallet_descriptor(self):
return self.qr_type in [QRType.WALLET__SPECTER, QRType.WALLET__UR, QRType.WALLET__BLUEWALLET, QRType.WALLET__GENERIC]
check = self.qr_type in [QRType.WALLET__SPECTER, QRType.WALLET__UR, QRType.WALLET__CONFIGFILE, QRType.WALLET__GENERIC, QRType.OUTPUT__UR]
if self.qr_type in [QRType.BYTES__UR]:
cbor = self.decoder.result_message().cbor
raw = Bytes.from_cbor(cbor).data
check = 'multisig setup file' in raw.decode("utf-8").lower()
return check
@property
def is_settings(self):
@@ -303,12 +320,18 @@ class DecodeQR:
# PSBT
if re.search("^UR:CRYPTO-PSBT/", s, re.IGNORECASE):
return QRType.PSBT__UR2
elif re.search("^UR:CRYPTO-OUTPUT/", s, re.IGNORECASE):
return QRType.OUTPUT__UR
elif re.search("^UR:CRYPTO-ACCOUNT/", s, re.IGNORECASE):
return QRType.ACCOUNT__UR
elif re.search(r'^p(\d+)of(\d+) ([A-Za-z0-9+\/=]+$)', s, re.IGNORECASE): #must be base64 characters only in segment
return QRType.PSBT__SPECTER
elif re.search("^UR:BYTES/", s, re.IGNORECASE):
return QRType.PSBT__LEGACY_UR
return QRType.BYTES__UR
elif DecodeQR.is_base64_psbt(s):
return QRType.PSBT__BASE64
@@ -323,6 +346,9 @@ class DecodeQR:
# if json starting with label and contains descriptor, assume specter wallet json
return QRType.WALLET__SPECTER
elif "multisig setup file" in s.lower():
return QRType.WALLET__CONFIGFILE
elif "sortedmulti" in s:
return QRType.WALLET__GENERIC
@@ -453,8 +479,101 @@ class DecodeQR:
return True
else:
return False
@staticmethod
def multisig_setup_file_to_descriptor(text) -> str:
# sample text file, parse the contents and create descriptor
"""
Name: SeedSigner Dev Funds
Policy: 4 of 6
Derivation: m/48'/0'/0'/2'
Format: P2WSH
E0811B6B: xpub6E8v7uy63pCeJvHe5W8ea8zTnCtKMFgMRb5bueWWcUFMw6sWmUwTqxM8cFiKQRWkA2Fxth9HJZufJwjWTTvU1UGZNpTrh9khrswYMgeHiCt
852B308F: xpub6ErhgAWfnEqW7xDBm1iLq5JjNyUS65YUFnjHLrRv9zmdDEtuE75bpWQ8o6bSBnpT6AkrrsA8eA5SmEFArZn11KEPaZJzx9mHTXPWZCsxLyh
7EDF9C59: xpub6DaFfKoe7WpofrbYeNo3Wv2AiLUMeyrPwotXfukFxUHbK4JxaLHTd5394QtH5wnjFzBgr2YnJpHhXv25Zsqv2APmMFvH1DsKHj5LCr3pmXs
B433E095: xpub6EF51itHko2YhGTjVeuYbBgJjVbTzzpYzn2a3JwZHpDrMePRVgXGBHMx2Yv1KwgLsUn9i7ExcAo8uqMx4pDjVRY9J7qnceFAwRRj16dd5AS
184D07EB: xpub6EEoTpcQu7N4R8D84pJjZ69j3minevnYLDDoo2HBzYBXTQ4rGVf4XGTyCYFwJuZdsF9MyFYJNzYEjg5LGMA1ubTGWuDnjHAZz6ficVRDTSy
3E451EFE: xpub6ExQPvQxGBMaPxr8Fv7Vq91ztJFFX3VWvtpvex6UPZ1AptTeuAiJGCtKkgwJkrwpMZMagh9ex6rL4sM8axfFcdQbERoFCRUKTJxrBkJh56g
"""
lines = text.split('\n')
m = 0
n = 0
xpubs = []
x = 0
derivation = ''
descriptor = ''
lines = text.split('\n')
for l in lines:
if l.find('#') == 0:
# skip comments
continue
l = l.strip()
if ':' not in l:
# when label/value divider not found, skip line
continue
label, value = l.split(':', 1)
label = label.strip().lower()
value = value.strip()
if label == 'policy':
try:
match = re.search(r'(\d+)\D*(\d+)', value)
m = int(match.group(1))
n = int(match.group(2))
except:
raise Exception(f"Policy line not supported")
elif label == 'derivation':
derivation = value
elif label == 'format':
if value.lower() in ['p2wsh', 'p2sh-p2wsh', 'p2wsh-p2sh']:
script_type = value.lower()
elif len(label) == 8:
if len(xpubs) == 0:
xpubs = [None] * n
xpubs[x] = {'xfp': label, 'key': value}
x += 1
if None in xpubs or len(xpubs) != n:
raise Exception(f"bad or missing xpub")
if m <= 0 or m > 9 or n <= 0 or n > 9:
raise Exception(f"bad or missing policy")
if len(derivation) == 0:
raise Exception(f"bad or missing derivation path")
if script_type not in ['p2wsh', 'p2sh-p2wsh', 'p2wsh-p2sh']:
raise Exception(f"bad or missing script format")
# create descriptor string
if script_type == "p2wsh":
script_open = "wsh(sortedmulti(" + str(m)
script_close = "))"
elif script_type in ["p2sh-p2wsh", 'p2wsh-p2sh']:
script_open = "sh(wsh(sortedmulti(" + str(m)
script_close = ")))"
descriptor = script_open
for x in xpubs:
if derivation[0] == 'm':
derivation = derivation[1:]
derivation = derivation.replace("'", "h")
descriptor += ',[' + x['xfp'] + derivation + "]" + x['key'] + "/{0,1}/*"
descriptor += script_close
return descriptor
class BaseQrDecoder:
def __init__(self):
@@ -551,51 +670,6 @@ class SpecterPsbtQrDecoder(BaseAnimatedQrDecoder):
def parse_segment(self, segment) -> str:
return segment.split(" ")[-1].strip()
class LegacyUrPsbtQrDecoder(BaseAnimatedQrDecoder):
"""
Decodes Legacy UR animated qr encoding
"""
def get_base64_data(self) -> str:
bc32_cbor = "".join(self.segments)
raw = cbor_decode(bc32decode(bc32_cbor))
base64 = b2a_base64(raw)
if self.complete:
return base64
return None
def get_data(self):
if not self.complete:
return None
bc32_cbor = "".join(self.segments)
raw = cbor_decode(bc32decode(bc32_cbor))
return raw
def parse_current_segment_num(self, segment) -> int:
if re.search(r'^UR:BYTES/(\d+)OF(\d+)', segment, re.IGNORECASE) != None:
return int(re.search(r'^UR:BYTES/(\d+)OF(\d+)', segment, re.IGNORECASE).group(1))
else:
raise Exception('Unexpected Legacy UR Error')
def parse_total_segment_nums(self, segment) -> int:
if re.search(r'^UR:BYTES/(\d+)OF(\d+)', segment, re.IGNORECASE) != None:
return int(re.search(r'^UR:BYTES/(\d+)OF(\d+)', segment, re.IGNORECASE).group(2))
else:
return 1
def parse_segment(self, segment) -> str:
return segment.split("/")[-1].strip()
class Base64PsbtQrDecoder(BaseSingleFrameQrDecoder):
"""
Decodes single frame base64 encoded qr image.
@@ -1005,6 +1079,7 @@ class GenericWalletQrDecoder(BaseSingleFrameQrDecoder):
# Validate via embit
Descriptor.from_string(segment)
self.descriptor = segment
self.complete = True
return DecodeQRStatus.COMPLETE
except Exception as e:
print(repr(e))
@@ -1013,4 +1088,11 @@ class GenericWalletQrDecoder(BaseSingleFrameQrDecoder):
def get_wallet_descriptor(self):
return self.descriptor
class MultiSigConfigFileQRDecoder(GenericWalletQrDecoder):
def add(self, segment, qr_type=QRType.WALLET__CONFIGFILE):
descriptor = DecodeQR.multisig_setup_file_to_descriptor(segment)
return super().add(descriptor,qr_type=QRType.WALLET__CONFIGFILE)
+4 -2
View File
@@ -4,7 +4,6 @@ class QRType:
"""
PSBT__BASE64 = "psbt__base64"
PSBT__SPECTER = "psbt__specter"
PSBT__LEGACY_UR = "psbt__legacy_ur"
PSBT__BASE43 = "psbt__base43"
PSBT__UR2 = "psbt__ur2"
@@ -24,7 +23,10 @@ class QRType:
WALLET__SPECTER = "wallet__specter"
WALLET__UR = "wallet__ur"
WALLET__BLUEWALLET = "wallet__bluewallet"
WALLET__CONFIGFILE = "wallet__configfile"
WALLET__GENERIC = "wallet__generic"
OUTPUT__UR = "output__ur"
ACCOUNT__UR = "account__ur"
BYTES__UR = "bytes__ur"
INVALID = "invalid"