# (c) Copyright 2020 by Coinkite Inc. This file is covered by license found in COPYING-CC. # # stuff I need sometimes import random, hashlib from binascii import b2a_hex, a2b_hex from decimal import Decimal from pysecp256k1 import tagged_sha256 from pysecp256k1.extrakeys import xonly_pubkey_serialize, xonly_pubkey_tweak_add, xonly_pubkey_from_pubkey from pysecp256k1.extrakeys import xonly_pubkey_parse from ripemd import ripemd160 def B2A(s): return str(b2a_hex(s), 'ascii') def U2SAT(v): return int(v * Decimal('1E8')) def hash160(data): return ripemd160(hashlib.sha256(data).digest()) def prandom(count): # make some bytes, randomly, but not: deterministic return bytes(random.randint(0, 255) for i in range(count)) def taptweak(internal_key, tweak=None): tweak = internal_key if tweak is None else internal_key + tweak assert len(internal_key) == 32, "not xonly-pubkey (len!=32)" xonly_pubkey = xonly_pubkey_parse(internal_key) tweak = tagged_sha256(b"TapTweak", tweak) tweaked_pubkey = xonly_pubkey_tweak_add(xonly_pubkey, tweak) tweaked_xonnly_pubkey, parity = xonly_pubkey_from_pubkey(tweaked_pubkey) return xonly_pubkey_serialize(tweaked_xonnly_pubkey) def fake_dest_addr(style='p2pkh'): # Make a plausible output address, but it's random garbage. Cant use for change outs # See CTxOut.get_address() in ../shared/serializations if style == 'p2wpkh': return bytes([0, 20]) + prandom(20) if style == 'p2wsh': return bytes([0, 32]) + prandom(32) if style in ['p2sh', 'p2wsh-p2sh', 'p2wpkh-p2sh']: # all equally bogus P2SH outputs return bytes([0xa9, 0x14]) + prandom(20) + bytes([0x87]) if style == 'p2pkh': return bytes([0x76, 0xa9, 0x14]) + prandom(20) + bytes([0x88, 0xac]) if style in ('p2pk', 'p2pk-compressed'): pubkey = bytes([random.choice((2, 3))]) + prandom(32) return bytes([len(pubkey)]) + pubkey + bytes([0xac]) if style == 'p2pk-uncompressed': pubkey = bytes([4]) + prandom(64) return bytes([len(pubkey)]) + pubkey + bytes([0xac]) if style == "p2tr": return bytes([81, 32]) + prandom(32) if style == "unknown": # OP_CHECKLOCKTIMEVERIFY OP_DROP OP_DUP OP_HASH160 OP_EQUALVERIFY OP_CHECKSIG hex_str = "049f7b2a5cb17576a914371c20fb2e9899338ce5e99908e64fd30b78931388ac" return bytes.fromhex(hex_str) raise ValueError('not supported: ' + style) def make_change_addr(wallet, style): # provide script, pubkey and xpath for a legit-looking change output import struct, random redeem_scr, actual_scr = None, None deriv = [12, 34, random.randint(0, 1000)] xfp, = struct.unpack('I', wallet.fingerprint()) dest = wallet.subkey_for_path('/'.join(str(i) for i in deriv)) target = dest.hash160() assert len(target) == 20 is_segwit = True pubkey = dest.sec(compressed=(style != 'p2pk-uncompressed')) if style == 'p2pkh': redeem_scr = bytes([0x76, 0xa9, 0x14]) + target + bytes([0x88, 0xac]) is_segwit = False elif style in ('p2pk', 'p2pk-compressed', 'p2pk-uncompressed'): redeem_scr = bytes([len(pubkey)]) + pubkey + bytes([0xac]) is_segwit = False elif style == 'p2wpkh': redeem_scr = bytes([0, 20]) + target elif style == 'p2wpkh-p2sh': redeem_scr = bytes([0, 20]) + target actual_scr = bytes([0xa9, 0x14]) + hash160(redeem_scr) + bytes([0x87]) elif style == 'p2tr': tweaked_xonly = taptweak(dest.sec()[1:]) redeem_scr = bytes([81, 32]) + tweaked_xonly return redeem_scr, actual_scr, is_segwit, dest.sec()[1:], b'\x00' + struct.pack('4I', xfp, *deriv) else: raise ValueError('cant make fake change output of type: ' + style) return redeem_scr, actual_scr, is_segwit, pubkey, struct.pack('4I', xfp, *deriv) def swab32(n): # endian swap: 32 bits import struct return struct.unpack('>I', struct.pack(' 3 assert 'XTN' in lines[s+1] or 'XRT' in lines[s+1] or 'BTC' in lines[s+1] val = Decimal(lines[s+1].split()[0]) assert 'address' in lines[s+2] addrs = [] for y in range(s+3, len(lines)): line = lines[y].strip() if line: if line[0] == "\x02": addrs.append(addr_from_display_format(line)) if line.startswith(("3","2","1","m","n","tb1","bc1","bcrt")): addrs.append(line) if len(addrs) >= 2: assert 'to addresses' in lines[s+2] return val, addrs def path_to_str(bin_path, prefix='m/', skip=1): return prefix + '/'.join(str(i & 0x7fffffff) + ("h" if i & 0x80000000 else "") for i in bin_path[skip:]) def str_to_path(path): # Take string derivation path, and make a list of numbers, # - no syntax checking here rv = [] for p in path.split('/'): if p == 'm': continue if not p: continue # trailing or duplicated slashes if p[-1] in "'h": here = int(p[:-1]) | 0x80000000 else: here = int(p) rv.append(here) # assert path == path_to_str(rv, skip=0) return rv def slip132undo(orig): # take a SLIP-132 xpub/ypub/z/U/?pub/prv and convert into BIP-32 style # - preserve testnet vs. mainnet # - return addr fmt info from base58 import decode_base58_checksum, encode_base58_checksum from ckcc_protocol.constants import AF_P2WPKH_P2SH, AF_P2SH, AF_P2WSH, AF_P2WSH_P2SH, AF_CLASSIC from ckcc_protocol.constants import AF_P2WPKH assert orig[0] not in 'xt', "already legit bip32" xpub = a2b_hex('0488B21E') tpub = a2b_hex('043587cf') xprv = a2b_hex('0488ADE4') tprv = a2b_hex('04358394') variants = [ (False, AF_P2WPKH_P2SH, '049d7cb2', '049d7878', 'y'), (False, AF_P2WPKH, '04b24746', '04b2430c', 'z'), (False, AF_P2WSH_P2SH, '0295b43f', '0295b005', 'Y'), (False, AF_P2WSH, '02aa7ed3', '02aa7a99', 'Z'), (True, AF_P2WPKH_P2SH, '044a5262', '044a4e28', 'u'), (True, AF_P2WPKH, '045f1cf6', '045f18bc', 'v'), (True, AF_P2WSH_P2SH, '024289ef', '024285b5', 'U'), (True, AF_P2WSH, '02575483', '02575048', 'V'), ] raw = decode_base58_checksum(orig) for testnet, addr_fmt, pub, priv, hint in variants: if raw[0:4] == a2b_hex(pub): return encode_base58_checksum((tpub if testnet else xpub) + raw[4:]), \ testnet, addr_fmt, False if raw[0:4] == a2b_hex(priv): return encode_base58_checksum((tprv if testnet else xprv) + raw[4:]), \ testnet, addr_fmt, True raise RuntimeError("unknown prefix") def detruncate_address(s): try: _idx = s.index("↳") except ValueError: _idx = -1 if _idx != -1: s = s[_idx+1:] start, end = s.strip().split('⋯') return start, end def seconds2human_readable(s): # duplicate from shared/utils.py - needed for tests days = s // (3600 * 24) hours = s % (3600 * 24) // 3600 minutes = (s % 3600) // 60 seconds = (s % 3600) % 60 msg = [] if days: msg.append("%dd" % days) if hours: msg.append("%dh" % hours) if minutes: msg.append("%dm" % minutes) if seconds: msg.append("%ds" % seconds) return " ".join(msg) def bitcoind_addr_fmt(script_type): if script_type == "p2wsh": addr_type = "bech32" elif script_type == "p2sh": addr_type = "legacy" else: assert script_type == "p2sh-p2wsh" addr_type = "p2sh-segwit" return addr_type # EOF