def test_sign_example(set_master_key, sim_execfile, start_sign, end_sign): # Use the private key given in BIP 174 and do similar signing # as the examples. # PROBLEM: revised BIP174 has p2sh multisig cases which we don't support yet. raise pytest.skip('not ready for multisig') # expect xfp=0x4f6a0cd9 exk = 'tprv8ZgxMBicQKsPd9TeAdPADNnSyH9SSUUbTVeFszDE23Ki6TBB5nCefAdHkK8Fm3qMQR6sHwA56zqRmKmxnHk37JkiFzvncDqoKmPWubu7hDF' set_master_key(exk) psbt = a2b_hex(open('data/worked-unsigned.psbt', 'rb').read()) start_sign(psbt) signed = end_sign(True) open('debug/ex-signed.psbt', 'wb').write(signed) b4 = BasicPSBT().parse(psbt) aft = BasicPSBT().parse(signed) assert b4 != aft, "signing didn't change anything?" open('debug/example-signed.psbt', 'wb').write(signed) expect = BasicPSBT().parse( a2b_hex(open('data/worked-combined.psbt', 'rb').read())) assert aft == expect
def test_change_fraud_addr(start_sign, end_sign, check_against_bitcoind, cap_story): # fraud: BIP-32 path of output doesn't match TXO address from pycoin.tx.Tx import Tx from pycoin.tx.TxOut import TxOut # NOTE: out#1 is change: #chg_addr = 'mvBGHpVtTyjmcfSsy6f715nbTGvwgbgbwo' psbt = open('data/example-change.psbt', 'rb').read() b4 = BasicPSBT().parse(psbt) # tweak output addr to garbage t = Tx.parse(BytesIO(b4.txn)) chg = t.txs_out[1] # pycoin.tx.TxOut.TxOut b = bytearray(chg.script) b[-5] ^= 0x55 chg.script = bytes(b) b4.txn = t.as_bin() with BytesIO() as fd: b4.serialize(fd) mod_psbt = fd.getvalue() open('debug/mod-addr.psbt', 'wb').write(mod_psbt) start_sign(mod_psbt) with pytest.raises(CCProtoError) as ee: signed = end_sign(True) assert 'Change output is fraud' in str(ee)
def doit(accept=True, in_psbt=None, finalize=False): if accept != None: need_keypress('y' if accept else 'x') if accept == False: with pytest.raises(CCUserRefused): done = None while done == None: time.sleep(0.050) done = dev.send_recv(CCProtocolPacker.get_signed_txn(), timeout=None) return else: done = None while done == None: time.sleep(0.050) done = dev.send_recv(CCProtocolPacker.get_signed_txn(), timeout=None) assert len(done) == 2 resp_len, chk = done psbt_out = dev.download_file(resp_len, chk) if not finalize: if in_psbt: assert BasicPSBT().parse(in_psbt) == BasicPSBT().parse(psbt_out) else: from pycoin.tx.Tx import Tx # parse it res = psbt_out assert res[0:4] != b'psbt' t = Tx.from_bin(res) assert t.version in [1, 2] return psbt_out
def test_change_p2sh_p2wpkh(start_sign, end_sign, check_against_bitcoind, cap_story, case): # not fraud: output address encoded in various equiv forms from pycoin.tx.Tx import Tx from pycoin.tx.TxOut import TxOut # NOTE: out#1 is change: #chg_addr = 'mvBGHpVtTyjmcfSsy6f715nbTGvwgbgbwo' psbt = open('data/example-change.psbt', 'rb').read() b4 = BasicPSBT().parse(psbt) t = Tx.parse(BytesIO(b4.txn)) pkh = t.txs_out[1].hash160() if case == 'p2wpkh': t.txs_out[1].script = bytes([0, 20]) + bytes(pkh) from bech32 import encode expect_addr = encode('tb', 0, pkh) elif case == 'p2sh': spk = bytes([0xa9, 0x14]) + pkh + bytes([0x87]) b4.outputs[1].redeem_script = bytes([0, 20]) + bytes(pkh) t.txs_out[1].script = spk expect_addr = t.txs_out[1].address('XTN') b4.txn = t.as_bin() with BytesIO() as fd: b4.serialize(fd) mod_psbt = fd.getvalue() open('debug/mod-%s.psbt' % case, 'wb').write(mod_psbt) start_sign(mod_psbt) time.sleep(.1) _, story = cap_story() check_against_bitcoind(B2A(b4.txn), Decimal('0.00000294'), change_outs=[ 1, ], dests=[(1, expect_addr)]) #print(story) assert expect_addr in story assert parse_change_back(story) == (Decimal('1.09997082'), [expect_addr]) signed = end_sign(True)
def test_psbt_proxy_parsing(fn, sim_execfile, sim_exec): # unit test: parsing by the psbt proxy object sim_exec('import main; main.FILENAME = %r; ' % ('../../testing/'+fn)) rv = sim_execfile('devtest/unit_psbt.py') assert not rv, rv rb = '../unix/work/readback.psbt' oo = BasicPSBT().parse(open(fn, 'rb').read()) rb = BasicPSBT().parse(open(rb, 'rb').read()) assert oo == rb
def test_change_troublesome(start_sign, cap_story, try_path, expect): from struct import pack # NOTE: out#1 is change: # addr = 'mvBGHpVtTyjmcfSsy6f715nbTGvwgbgbwo' # path = (m=4369050F)/44'/1'/0'/1/5 # pubkey = 03c80814536f8e801859fc7c2e5129895b261153f519d4f3418ffb322884a7d7e1 psbt = open('data/example-change.psbt', 'rb').read() b4 = BasicPSBT().parse(psbt) if 0: #from pycoin.tx.Tx import Tx #from pycoin.tx.TxOut import TxOut # tweak output addr to garbage t = Tx.parse(BytesIO(b4.txn)) chg = t.txs_out[1] # pycoin.tx.TxOut.TxOut b = bytearray(chg.script) b[-5] ^= 0x55 chg.script = bytes(b) b4.txn = t.as_bin() pubkey = a2b_hex( '03c80814536f8e801859fc7c2e5129895b261153f519d4f3418ffb322884a7d7e1') path = [ int(p) if ("'" not in p) else 0x80000000 + int(p[:-1]) for p in try_path.split('/') ] bin_path = b4.outputs[1].bip32_paths[pubkey][0:4] \ + b''.join(pack('<I', i) for i in path) b4.outputs[1].bip32_paths[pubkey] = bin_path with BytesIO() as fd: b4.serialize(fd) mod_psbt = fd.getvalue() open('debug/troublesome.psbt', 'wb').write(mod_psbt) start_sign(mod_psbt) time.sleep(0.1) title, story = cap_story() assert 'OK TO SEND' in title assert '(1 warning below)' in story, "no warning shown" assert expect in story, story assert parse_change_back(story) == (Decimal('1.09997082'), ['mvBGHpVtTyjmcfSsy6f715nbTGvwgbgbwo'])
def doit(num_ins, num_outs, fat=0): psbt = BasicPSBT() txn = Tx(2, [], []) for i in range(num_ins): h = TxIn(pack('4Q', 0, 0, 0, i), i) txn.txs_in.append(h) for i in range(num_outs): # random P2PKH scr = bytes([0x76, 0xa9, 0x14]) + pack( 'I', i + 1) + bytes(16) + bytes([0x88, 0xac]) h = TxOut((1E6 * i) if i else 1E8, scr) txn.txs_out.append(h) with BytesIO() as b: txn.stream(b) psbt.txn = b.getvalue() psbt.inputs = [BasicPSBTInput(idx=i) for i in range(num_ins)] psbt.outputs = [BasicPSBTOutput(idx=i) for i in range(num_outs)] if fat: for i in range(num_ins): psbt.inputs[i].utxo = os.urandom(fat) rv = BytesIO() psbt.serialize(rv) assert rv.tell() <= MAX_TXN_LEN, 'too fat' return rv.getvalue()
def doit(num_ins, num_outs, master_xpub, subpath="0/%d", fee=10000): psbt = BasicPSBT() txn = Tx(2, [], []) # we have a key; use it to provide "plausible" value inputs from pycoin.key.BIP32Node import BIP32Node mk = BIP32Node.from_wallet_key(master_xpub) xfp = mk.fingerprint() psbt.inputs = [BasicPSBTInput(idx=i) for i in range(num_ins)] psbt.outputs = [BasicPSBTOutput(idx=i) for i in range(num_outs)] for i in range(num_ins): # make a fake txn to supply each of the inputs # - each input is 1BTC # addr where the fake money will be stored. subkey = mk.subkey_for_path(subpath % i) sec = subkey.sec() assert len(sec) == 33, "expect compressed" assert subpath[0:2] == '0/' psbt.inputs[i].bip32_paths[sec] = xfp + pack('<II', 0, i) # UTXO that provides the funding for to-be-signed txn supply = Tx(2, [TxIn(pack('4Q', 0xdead, 0xbeef, 0, 0), 73)], []) scr = bytes([0x76, 0xa9, 0x14]) + subkey.hash160() + bytes( [0x88, 0xac]) supply.txs_out.append(TxOut(1E8, scr)) with BytesIO() as fd: supply.stream(fd) psbt.inputs[i].utxo = fd.getvalue() if 0: with BytesIO() as fd: supply.stream(fd, include_witness_data=True) psbt.inputs[i].witness_utxo = fd.getvalue() spendable = TxIn(supply.hash(), 0) txn.txs_in.append(spendable) for i in range(num_outs): # random P2PKH scr = bytes([0x76, 0xa9, 0x14]) + pack( 'I', i + 1) + bytes(16) + bytes([0x88, 0xac]) h = TxOut(round(((1E8 * num_ins) - fee) / num_outs, 4), scr) txn.txs_out.append(h) with BytesIO() as b: txn.stream(b) psbt.txn = b.getvalue() rv = BytesIO() psbt.serialize(rv) assert rv.tell() <= MAX_TXN_LEN, 'too fat' return rv.getvalue()
def spend_outputs(funding_psbt, finalized_txn, tweaker=None): # take details from PSBT that created a finalized txn (also provided) # and build a new PSBT that spends those change outputs. from pycoin.tx.Tx import Tx from pycoin.tx.TxOut import TxOut from pycoin.tx.TxIn import TxIn funding = Tx.from_bin(finalized_txn) b4 = BasicPSBT().parse(funding_psbt) # segwit change outputs only spendables = [(n, i) for n, i in enumerate(funding.tx_outs_as_spendable()) if i.script[0:2] == b'\x00\x14' and b4.outputs[n].bip32_paths ] #spendables = list(reversed(spendables)) random.shuffle(spendables) if tweaker: tweaker(spendables) nn = BasicPSBT() nn.inputs = [BasicPSBTInput(idx=i) for i in range(len(spendables))] nn.outputs = [BasicPSBTOutput(idx=0)] # copy input values from funding PSBT's output side for p_in, (f_out, sp) in zip(nn.inputs, [(b4.outputs[x], s) for x, s in spendables]): p_in.bip32_paths = f_out.bip32_paths p_in.witness_script = f_out.redeem_script with BytesIO() as fd: sp.stream(fd) p_in.witness_utxo = fd.getvalue() # build new txn: single output, no change, no miner fee act_scr = fake_dest_addr('p2wpkh') dest_out = TxOut(sum(s.coin_value for n, s in spendables), act_scr) txn = Tx(2, [s.tx_in() for _, s in spendables], [dest_out]) # put unsigned TXN into PSBT with BytesIO() as b: txn.stream(b) nn.txn = b.getvalue() with BytesIO() as rv: nn.serialize(rv) raw = rv.getvalue() open('debug/spend_outs.psbt', 'wb').write(raw) return nn, raw
def test_sign_example(set_master_key, sim_execfile, start_sign, end_sign): # use the private key given in BIP 174 and do similar signing # as the examples. # TODO fix this # - doesn't work anymore, because we won't sign a multisig we don't know the wallet details for raise pytest.skip('needs rework') exk = 'tprv8ZgxMBicQKsPd9TeAdPADNnSyH9SSUUbTVeFszDE23Ki6TBB5nCefAdHkK8Fm3qMQR6sHwA56zqRmKmxnHk37JkiFzvncDqoKmPWubu7hDF' set_master_key(exk) mk = BIP32Node.from_wallet_key(exk) psbt = a2b_hex(open('data/worked-unsigned.psbt', 'rb').read()) start_sign(psbt) signed = end_sign(True) aft = BasicPSBT().parse(signed) expect = BasicPSBT().parse(open('data/worked-combined.psbt', 'rb').read()) assert aft == expect
def doit(accept=True, in_psbt=None, finalize=False, accept_ms_import=False, expect_txn=True): if accept_ms_import: # XXX would be better to do cap_story here, but that would limit test to simulator need_keypress('y') time.sleep(0.050) if accept != None: need_keypress('y' if accept else 'x', timeout=None) if accept == False: with pytest.raises(CCUserRefused): done = None while done == None: time.sleep(0.050) done = dev.send_recv(CCProtocolPacker.get_signed_txn(), timeout=None) return else: done = None while done == None: time.sleep(0.00) done = dev.send_recv(CCProtocolPacker.get_signed_txn(), timeout=None) assert len(done) == 2 resp_len, chk = done psbt_out = dev.download_file(resp_len, chk) if not expect_txn: # skip checks; it's text return psbt_out if not finalize: if in_psbt: from psbt import BasicPSBT assert BasicPSBT().parse(in_psbt) != None else: from pycoin.tx.Tx import Tx # parse it res = psbt_out assert res[0:4] != b'psbt', 'still a PSBT, but asked for finalize' t = Tx.from_bin(res) assert t.version in [1, 2] return psbt_out
def test_ms_sign_myself(M, make_myself_wallet, segwit, num_ins, dev, clear_ms, fake_ms_txn, try_sign, bitcoind_finalizer, incl_xpubs, bitcoind_analyze, bitcoind_decode): # IMPORTANT: wont work if you start simulator with -m flag. Use no args all_out_styles = list(unmap_addr_fmt.keys()) num_outs = len(all_out_styles) clear_ms() # create a wallet, with 3 bip39 pw's keys, select_wallet = make_myself_wallet(M, do_import=(not incl_xpubs)) N = len(keys) psbt = fake_ms_txn(num_ins, num_outs, M, keys, segwit_in=segwit, incl_xpubs=incl_xpubs, outstyles=all_out_styles, change_outputs=list(range(1,num_outs))) open(f'debug/myself-before.psbt', 'wb').write(psbt) for idx in range(M): select_wallet(idx) _, updated = try_sign(psbt, accept_ms_import=(incl_xpubs and (idx==0))) open(f'debug/myself-after.psbt', 'wb').write(updated) assert updated != psbt aft = BasicPSBT().parse(updated) # check all inputs gained a signature assert all(len(i.part_sigs)==(idx+1) for i in aft.inputs) psbt = updated # should be fully signed now. anal = bitcoind_analyze(aft.as_bytes()) try: assert not any(inp.get('missing') for inp in anal['inputs']), "missing sigs: %r" % anal assert all(inp['next'] in {'finalizer','updater'} for inp in anal['inputs']), "other issue: %r" % anal except: # XXX seems to be a bug in analyzepsbt function ... not fully studied pprint(anal, stream=open('debug/analyzed.txt', 'wt')) decode = bitcoind_decode(aft.as_bytes()) pprint(decode, stream=open('debug/decoded.txt', 'wt')) if M==N or segwit: # as observed, bug not trigged, so raise if it *does* happen raise else: print("ignoring bug in bitcoind") if 0: # why doesn't this work? extracted_psbt, txn, is_complete = bitcoind_finalizer(aft.as_bytes(), extract=True) ex = BasicPSBT().parse(extracted_psbt) assert is_complete assert ex != aft
def KEEP_test_random_psbt(try_sign, sim_exec, fname="data/ .psbt"): # allow almost any PSBT to run on simulator, at least up until wrong pubkeys detected # - detects expected XFP and changes to match # - good for debug of random psbt oo = BasicPSBT().parse(open(fname, 'rb').read()) paths = [] for i in oo.inputs: paths.extend(i.bip32_paths.values()) used = set(i[0:4] for i in paths) assert len(used) == 1, "multiple key fingerprints in inputs, can only handle 1" need_xfp, = struct.unpack("<I", used.pop()) sim_exec('from main import settings; settings.set("xfp", 0x%x);' % need_xfp) with pytest.raises(CCProtoError) as ee: orig, result = try_sign(fname, accept=True) msg = ee.value.args[0] assert 'Signing failed late' in msg assert 'led to wrong pubkey for input' in msg
def test_change_case(start_sign, end_sign, check_against_bitcoind, cap_story): # is change shown/hidden at right times. no fraud checks # NOTE: out#1 is change: chg_addr = 'mvBGHpVtTyjmcfSsy6f715nbTGvwgbgbwo' psbt = open('data/example-change.psbt', 'rb').read() start_sign(psbt) time.sleep(.1) _, story = cap_story() assert chg_addr in story b4 = BasicPSBT().parse(psbt) check_against_bitcoind(B2A(b4.txn), Decimal('0.00000294'), change_outs=[ 1, ]) signed = end_sign(True) open('debug/chg-signed.psbt', 'wb').write(signed) # modify it: remove bip32 path b4.outputs[1].bip32_paths = {} with BytesIO() as fd: b4.serialize(fd) mod_psbt = fd.getvalue() start_sign(mod_psbt) time.sleep(.1) _, story = cap_story() # no change expected (they are outputs) assert 'Change back' not in story check_against_bitcoind(B2A(b4.txn), Decimal('0.00000294'), change_outs=[]) signed2 = end_sign(True) open('debug/chg-signed2.psbt', 'wb').write(signed) aft = BasicPSBT().parse(signed) aft2 = BasicPSBT().parse(signed2) assert aft.txn == aft2.txn
def test_vs_bitcoind(match_key, check_against_bitcoind, bitcoind, start_sign, end_sign, we_finalize, num_dests): bal = bitcoind.getbalance() assert bal > 0, "need some play money; drink from a faucet" amt = round((bal / 8) / num_dests, 6) args = {} for no in range(num_dests): dest = bitcoind.getrawchangeaddress() assert dest[0] in '2mn' or dest.startswith('tb1'), dest args[dest] = amt if 0: # old approach: fundraw + convert to psbt # working with hex strings here txn = bitcoind.createrawtransaction([], args) assert txn[0:2] == '02' #print(txn) resp = bitcoind.fundrawtransaction(txn) txn2 = resp['hex'] fee = resp['fee'] chg_pos = resp['changepos'] #print(txn2) print("Sending %.8f XTN to %s (Change back in position: %d)" % (amt, dest, chg_pos)) psbt = b64decode(bitcoind.converttopsbt(txn2, True)) else: # use walletcreatefundedpsbt # - updated/validated against 0.17.1 resp = bitcoind.walletcreatefundedpsbt([], args, 0, {}, True) psbt = b64decode(resp['psbt']) fee = resp['fee'] chg_pos = resp['changepos'] # pull out included txn txn2 = B2A(BasicPSBT().parse(psbt).txn) open('debug/vs.psbt', 'wb').write(psbt) start_sign(psbt, finalize=we_finalize) # verify against how bitcoind reads it check_against_bitcoind(txn2, fee) signed = end_sign(accept=True) #signed = end_sign(None) open('debug/vs-signed.psbt', 'wb').write(signed) if not we_finalize: b4 = BasicPSBT().parse(psbt) aft = BasicPSBT().parse(signed) assert b4 != aft, "signing didn't change anything?" open('debug/signed.psbt', 'wb').write(signed) resp = bitcoind.finalizepsbt(str(b64encode(signed), 'ascii'), True) #combined_psbt = b64decode(resp['psbt']) #open('debug/combined.psbt', 'wb').write(combined_psbt) assert resp['complete'] == True, "bitcoind wasn't able to finalize it" network = a2b_hex(resp['hex']) # assert resp['complete'] #print("Final txn: %r" % network) # try to send it txed = bitcoind.sendrawtransaction(B2A(network)) print("Final txn hash: %r" % txed) else: assert signed[0:4] != b'psbt', "expecting raw bitcoin txn" #print("Final txn: %s" % B2A(signed)) open('debug/finalized.psbt', 'wb').write(signed) txed = bitcoind.sendrawtransaction(B2A(signed)) print("Final txn hash: %r" % txed)
def doit(f_or_data, accept=True, finalize=False, accept_ms_import=False, complete=False, encoding='binary', del_after=0): if f_or_data[0:5] == b'psbt\xff': ip = f_or_data filename = 'memory' else: filename = f_or_data ip = open(f_or_data, 'rb').read() if ip[0:10] == b'70736274ff': ip = a2b_hex(ip.strip()) assert ip[0:5] == b'psbt\xff' psbtname = 'ftrysign' # population control from glob import glob import os pat = microsd_path(psbtname + '*.psbt') for f in glob(pat): assert 'psbt' in f os.unlink(f) if encoding == 'hex': ip = b2a_hex(ip) elif encoding == 'base64': from base64 import b64encode, b64decode ip = b64encode(ip) else: assert encoding == 'binary' with open_microsd(psbtname + '.psbt', 'wb') as sd: sd.write(ip) goto_home() pick_menu_item('Ready To Sign') time.sleep(.1) _, story = cap_story() if 'Choose PSBT file' in story: need_keypress('y') time.sleep(.1) pick_menu_item(psbtname + '.psbt') time.sleep(.1) if accept_ms_import: # XXX would be better to do cap_story here, but that would limit test to simulator need_keypress('y') time.sleep(0.050) title, story = cap_story() assert title == 'OK TO SEND?' if accept != None: need_keypress('y' if accept else 'x') if accept == False: time.sleep(0.050) # look for "Aborting..." ?? return ip, None, None # wait for it to finish for r in range(10): time.sleep(0.1) title, story = cap_story() if title == 'PSBT Signed': break else: assert False, 'timed out' txid = None lines = story.split('\n') if 'Final TXID:' in lines: txid = lines[-1] result_fname = lines[-4] else: result_fname = lines[-1] result = open_microsd(result_fname, 'rb').read() if encoding == 'hex' or finalize: result = a2b_hex(result.strip()) elif encoding == 'base64': result = b64decode(result) else: assert encoding == 'binary' in_file = microsd_path(psbtname + '.psbt') # read back final product if finalize: if del_after: if not txid: txid = re.findall('[0-9a-f]{64}', result_fname)[0] assert result_fname == txid + '.txn' assert not os.path.exists(in_file) else: assert 'final' in result_fname assert os.path.exists(in_file) from pycoin.tx.Tx import Tx # parse it a little assert result[ 0:4] != b'psbt', 'still a PSBT, but asked for finalize' t = Tx.from_bin(result) assert t.version in [1, 2] assert t.id() == txid else: assert result[0:5] == b'psbt\xff' if complete: assert '-signed' in result_fname else: assert '-part' in result_fname if del_after: assert not os.path.exists(in_file) from psbt import BasicPSBT was = BasicPSBT().parse(ip) now = BasicPSBT().parse(result) assert was.txn == now.txn assert was != now return ip, result, txid
def test_bitcoind_cosigning(dev, bitcoind, start_sign, end_sign, import_ms_wallet, clear_ms, explora, try_sign, need_keypress, addr_style): # Make a P2SH wallet with local bitcoind as a co-signer (and simulator) # - send an receive various # - following text of <https://github.com/bitcoin/bitcoin/blob/master/doc/psbt.md> # - the constructed multisig walelt will only work for a single pubkey on core side # - before starting this test, have some funds already deposited to bitcoind testnet wallet from pycoin.encoding import sec_to_public_pair from binascii import a2b_hex import re if addr_style == 'legacy': addr_fmt = AF_P2SH elif addr_style == 'p2sh-segwit': addr_fmt = AF_P2WSH_P2SH elif addr_style == 'bech32': addr_fmt = AF_P2WSH try: addr, = bitcoind.getaddressesbylabel("sim-cosign").keys() except: addr = bitcoind.getnewaddress("sim-cosign") info = bitcoind.getaddressinfo(addr) #pprint(info) assert info['address'] == addr bc_xfp = swab32(int(info['hdmasterfingerprint'], 16)) bc_deriv = info['hdkeypath'] # example: "m/0'/0'/3'" bc_pubkey = info['pubkey'] # 02f75ae81199559c4aa... pp = sec_to_public_pair(a2b_hex(bc_pubkey)) # No means to export XPUB from bitcoind! Still. In 2019. # - this fake will only work for for one pubkey value, the first/topmost node = BIP32Node('XTN', b'\x23'*32, depth=len(bc_deriv.split('/'))-1, parent_fingerprint=a2b_hex('%08x' % bc_xfp), public_pair=pp) keys = [ (bc_xfp, None, node), (1130956047, None, BIP32Node.from_hwif('tpubD8NXmKsmWp3a3DXhbihAYbYLGaRNVdTnr6JoSxxfXYQcmwVtW2hv8QoDwng6JtEonmJoL3cNEwfd2cLXMpGezwZ2vL2dQ7259bueNKj9C8n')), # simulator: m/45' ] M,N=2,2 clear_ms() import_ms_wallet(M, N, keys=keys, accept=1, name="core-cosign") cc_deriv = "m/45'/55" cc_pubkey = B2A(BIP32Node.from_hwif(simulator_fixed_xprv).subkey_for_path(cc_deriv[2:]).sec()) # NOTE: bitcoind doesn't seem to implement pubkey sorting. We have to do it. resp = bitcoind.addmultisigaddress(M, list(sorted([cc_pubkey, bc_pubkey])), 'shared-addr-'+addr_style, addr_style) ms_addr = resp['address'] bc_redeem = a2b_hex(resp['redeemScript']) assert bc_redeem[0] == 0x52 def mapper(cosigner_idx): return list(str2ipath(cc_deriv if cosigner_idx else bc_deriv)) scr, pubkeys, xfp_paths = make_redeem(M, keys, mapper) assert scr == bc_redeem # check Coldcard calcs right address to match got_addr = dev.send_recv(CCProtocolPacker.show_p2sh_address( M, xfp_paths, scr, addr_fmt=addr_fmt), timeout=None) assert got_addr == ms_addr time.sleep(.1) need_keypress('x') # clear screen / start over print(f"Will be signing an input from {ms_addr}") if xfp2str(bc_xfp) in ('5380D0ED', 'EDD08053'): # my own expected values assert ms_addr in ( '2NDT3ymKZc8iMfbWqsNd1kmZckcuhixT5U4', '2N1hZJ5mazTX524GQTPKkCT4UFZn5Fqwdz6', 'tb1qpcv2rkc003p5v8lrglrr6lhz2jg8g4qa9vgtrgkt0p5rteae5xtqn6njw9') # Need some UTXO to sign # # - but bitcoind can't give me that (using listunspent) because it's only a watched addr?? # did_fund = False while 1: rr = explora('address', ms_addr, 'utxo') pprint(rr) avail = [] amt = 0 for i in rr: txn = i['txid'] vout = i['vout'] avail.append( (txn, vout) ) amt += i['value'] # just use first UTXO available; save other for later tests break else: # doesn't need to confirm, but does need to reach public testnet/blockstream assert not amt and not avail if not did_fund: print(f"Sending some XTN to {ms_addr} (wait)") bitcoind.sendtoaddress(ms_addr, 0.0001, 'fund testing') did_fund = True else: print(f"Still waiting ...") time.sleep(2) if amt: break ret_addr = bitcoind.getrawchangeaddress() ''' If you get insufficent funds, even tho we provide the UTXO (!!), do this: bitcoin-cli importaddress "2NDT3ymKZc8iMfbWqsNd1kmZckcuhixT5U4" true true Better method: always fund addresses for testing here from same wallet (ie. got from non-multisig to multisig on same bitcoin-qt instance). -> Now doing that, automated, above. ''' resp = bitcoind.walletcreatefundedpsbt([dict(txid=t, vout=o) for t,o in avail], [{ret_addr: amt/1E8}], 0, {'subtractFeeFromOutputs': [0], 'includeWatching': True}, True) assert resp['changepos'] == -1 psbt = b64decode(resp['psbt']) open('debug/funded.psbt', 'wb').write(psbt) # patch up the PSBT a little ... bitcoind doesn't know the path for the CC's key ex = BasicPSBT().parse(psbt) cxpk = a2b_hex(cc_pubkey) for i in ex.inputs: assert cxpk in i.bip32_paths, 'input not to be signed by CC?' i.bip32_paths[cxpk] = pack('<3I', keys[1][0], *str2ipath(cc_deriv)) psbt = ex.as_bytes() open('debug/patched.psbt', 'wb').write(psbt) _, updated = try_sign(psbt, finalize=False) open('debug/cc-updated.psbt', 'wb').write(updated) # have bitcoind do the rest of the signing rr = bitcoind.walletprocesspsbt(b64encode(updated).decode('ascii')) pprint(rr) open('debug/bc-processed.psbt', 'wt').write(rr['psbt']) assert rr['complete'] # finalize and send rr = bitcoind.finalizepsbt(rr['psbt'], True) open('debug/bc-final-txn.txn', 'wt').write(rr['hex']) assert rr['complete'] txn_id = bitcoind.sendrawtransaction(rr['hex']) print(txn_id)
def test_finalization_vs_bitcoind(match_key, check_against_bitcoind, bitcoind, start_sign, end_sign, num_dests): # Compare how we finalize vs bitcoind ... should be exactly the same txn wallet_xfp = match_key() bal = bitcoind.getbalance() assert bal > 0, "need some play money; drink from a faucet" amt = round((bal/4)/num_dests, 6) args = {} for no in range(num_dests): dest = bitcoind.getrawchangeaddress() assert dest[0] in '2mn' or dest.startswith('tb1'), dest args[dest] = amt # use walletcreatefundedpsbt # - updated/validated against 0.17.1 resp = bitcoind.walletcreatefundedpsbt([], args, 0, { 'subtractFeeFromOutputs': list(range(num_dests)), 'feeRate': 0.00001500}, True) psbt = b64decode(resp['psbt']) fee = resp['fee'] chg_pos = resp['changepos'] open('debug/vs.psbt', 'wb').write(psbt) # check some basics mine = BasicPSBT().parse(psbt) for i in mine.inputs: got_xfp, = struct.unpack_from('I', list(i.bip32_paths.values())[0]) #assert hex(got_xfp) == hex(wallet_xfp), "wrong HD master key fingerprint" # see <https://github.com/bitcoin/bitcoin/issues/15884> if hex(got_xfp) != hex(wallet_xfp): raise pytest.xfail("wrong HD master key fingerprint") # pull out included txn txn2 = B2A(mine.txn) start_sign(psbt, finalize=True) # verify against how bitcoind reads it check_against_bitcoind(txn2, fee) signed_final = end_sign(accept=True) assert signed_final[0:4] != b'psbt', "expecting raw bitcoin txn" open('debug/finalized-by-ckcc.txn', 'wt').write(B2A(signed_final)) # Sign again, but don't finalize it. start_sign(psbt, finalize=False) signed = end_sign(accept=True) open('debug/vs-signed-unfin.psbt', 'wb').write(signed) # Use bitcoind to finalize it this time. resp = bitcoind.finalizepsbt(str(b64encode(signed), 'ascii'), True) assert resp['complete'] == True, "bitcoind wasn't able to finalize it" network = a2b_hex(resp['hex']) # assert resp['complete'] #print("Final txn: %r" % network) open('debug/finalized-by-btcd.txn', 'wt').write(B2A(network)) assert network == signed_final, "Finalized differently" # try to send it txed = bitcoind.sendrawtransaction(B2A(network)) print("Final txn hash: %r" % txed)
def test_sign_p2sh_example(set_master_key, sim_execfile, start_sign, end_sign, decode_psbt_with_bitcoind, offer_ms_import, need_keypress, clear_ms): # Use the private key given in BIP 174 and do similar signing # as the examples. # PROBLEM: we can't handle this, since we don't allow same cosigner key to be used # more than once and that check happens after we decide we can sign an input, and yet # no way to provide the right set of keys needed since 4 in total, etc, etc. # - code below nearly works tho raise pytest.skip('difficult example') # expect xfp=4F6A0CD9 exk = 'tprv8ZgxMBicQKsPd9TeAdPADNnSyH9SSUUbTVeFszDE23Ki6TBB5nCefAdHkK8Fm3qMQR6sHwA56zqRmKmxnHk37JkiFzvncDqoKmPWubu7hDF' set_master_key(exk) # Peeked at PSBT to know the full, deep hardened path we'll need. # in1: 0'/0'/0' and 0'/0'/1' # in2: 0'/0'/3' and 0'/0'/2' config = "name: p2sh-example\npolicy: 2 of 2\n\n" n1 = BIP32Node.from_hwif(exk).subkey_for_path("0'/0'").hwif() n2 = BIP32Node.from_hwif(exk).subkey_for_path("0'/0'").hwif() xfp = '4F6A0CD9' config += f'{xfp}: {n1}\n{xfp}: {n2}\n' clear_ms() offer_ms_import(config) time.sleep(.1) need_keypress('y') psbt = a2b_hex(open('data/worked-unsigned.psbt', 'rb').read()) # PROBLEM: revised BIP-174 has p2sh multisig cases which we don't support yet. # - it has two signatures from same key on same input # - that's a rare case and not worth supporting in the firmware # - but we can do it in two passes # - the MS wallet is also hard, since dup xfp (same actual key) ... altho can # provide different subkeys start_sign(psbt) part_signed = end_sign(True) open('debug/ex-signed-part.psbt', 'wb').write(part_signed) b4 = BasicPSBT().parse(psbt) aft = BasicPSBT().parse(part_signed) assert b4 != aft, "(partial) signing didn't change anything?" # NOTE: cannot handle combining multisig txn yet, so cannot finalize on-device start_sign(part_signed, finalize=False) signed = end_sign(True, finalize=False) open('debug/ex-signed.psbt', 'wb').write(signed) aft2 = BasicPSBT().parse(signed) decode = decode_psbt_with_bitcoind(signed) pprint(decode) mx_expect = BasicPSBT().parse(a2b_hex(open('data/worked-combined.psbt', 'rb').read())) assert aft2 == mx_expect expect = a2b_hex(open('data/worked-combined.psbt', 'rb').read()) decode_ex = decode_psbt_with_bitcoind(expect) # NOTE: because we are using RFC6979, the exact bytes of the signatures should match for i in range(2): assert decode['inputs'][i]['partial_signatures'] == \ decode_ex['inputs'][i]['partial_signatures'] if 0: import json, decimal def EncodeDecimal(o): if isinstance(o, decimal.Decimal): return float(round(o, 8)) raise TypeError json.dump(decode, open('debug/core-decode.json', 'wt'), indent=2, default=EncodeDecimal)
def doit(num_ins, num_outs, M, keys, fee=10000, outvals=None, segwit_in=False, outstyles=['p2pkh'], change_outputs=[], incl_xpubs=False): psbt = BasicPSBT() txn = Tx(2,[],[]) if incl_xpubs: # add global header with XPUB's # - assumes BIP45 for xfp, m, sk in keys: kk = pack('<II', xfp, 45|0x80000000) psbt.xpubs.append( (sk.serialize(as_private=False), kk) ) psbt.inputs = [BasicPSBTInput(idx=i) for i in range(num_ins)] psbt.outputs = [BasicPSBTOutput(idx=i) for i in range(num_outs)] for i in range(num_ins): # make a fake txn to supply each of the inputs # - each input is 1BTC # addr where the fake money will be stored. addr, scriptPubKey, script, details = make_ms_address(M, keys, idx=i) # lots of supporting details needed for p2sh inputs if segwit_in: psbt.inputs[i].witness_script = script else: psbt.inputs[i].redeem_script = script for pubkey, xfp_path in details: psbt.inputs[i].bip32_paths[pubkey] = b''.join(pack('<I', j) for j in xfp_path) # UTXO that provides the funding for to-be-signed txn supply = Tx(2,[TxIn(pack('4Q', 0xdead, 0xbeef, 0, 0), 73)],[]) supply.txs_out.append(TxOut(1E8, scriptPubKey)) with BytesIO() as fd: if not segwit_in: supply.stream(fd) psbt.inputs[i].utxo = fd.getvalue() else: supply.txs_out[-1].stream(fd) psbt.inputs[i].witness_utxo = fd.getvalue() spendable = TxIn(supply.hash(), 0) txn.txs_in.append(spendable) for i in range(num_outs): # random P2PKH if not outstyles: style = ADDR_STYLES[i % len(ADDR_STYLES)] else: style = outstyles[i % len(outstyles)] if i in change_outputs: addr, scriptPubKey, scr, details = \ make_ms_address(M, keys, idx=i, addr_fmt=unmap_addr_fmt[style]) for pubkey, xfp_path in details: psbt.outputs[i].bip32_paths[pubkey] = b''.join(pack('<I', j) for j in xfp_path) if 'w' in style: psbt.outputs[i].witness_script = scr if style.endswith('p2sh'): psbt.outputs[i].redeem_script = b'\0\x20' + sha256(scr).digest() elif style.endswith('sh'): psbt.outputs[i].redeem_script = scr else: scr = fake_dest_addr(style) assert scr if not outvals: h = TxOut(round(((1E8*num_ins)-fee) / num_outs, 4), scriptPubKey) else: h = TxOut(outvals[i], scriptPubKey) txn.txs_out.append(h) with BytesIO() as b: txn.stream(b) psbt.txn = b.getvalue() rv = BytesIO() psbt.serialize(rv) assert rv.tell() <= MAX_TXN_LEN, 'too fat' return rv.getvalue()
def doit(num_ins, num_outs, master_xpub, subpath="0/%d", fee=10000, outvals=None, segwit_in=False, outstyles=['p2pkh'], change_outputs=[]): psbt = BasicPSBT() txn = Tx(2, [], []) # we have a key; use it to provide "plausible" value inputs mk = BIP32Node.from_wallet_key(master_xpub) xfp = mk.fingerprint() psbt.inputs = [BasicPSBTInput(idx=i) for i in range(num_ins)] psbt.outputs = [BasicPSBTOutput(idx=i) for i in range(num_outs)] for i in range(num_ins): # make a fake txn to supply each of the inputs # - each input is 1BTC # addr where the fake money will be stored. subkey = mk.subkey_for_path(subpath % i) sec = subkey.sec() assert len(sec) == 33, "expect compressed" assert subpath[0:2] == '0/' psbt.inputs[i].bip32_paths[sec] = xfp + pack('<II', 0, i) # UTXO that provides the funding for to-be-signed txn supply = Tx(2, [TxIn(pack('4Q', 0xdead, 0xbeef, 0, 0), 73)], []) scr = bytes([0x76, 0xa9, 0x14]) + subkey.hash160() + bytes( [0x88, 0xac]) supply.txs_out.append(TxOut(1E8, scr)) with BytesIO() as fd: if not segwit_in: supply.stream(fd) psbt.inputs[i].utxo = fd.getvalue() else: supply.txs_out[-1].stream(fd) psbt.inputs[i].witness_utxo = fd.getvalue() spendable = TxIn(supply.hash(), 0) txn.txs_in.append(spendable) for i in range(num_outs): # random P2PKH if not outstyles: style = ADDR_STYLES[i % len(ADDR_STYLES)] else: style = outstyles[i % len(outstyles)] if i in change_outputs: scr, act_scr, isw, pubkey, sp = make_change_addr(mk, style) psbt.outputs[i].bip32_paths[pubkey] = sp else: scr = act_scr = fake_dest_addr(style) isw = ('w' in style) #if style.endswith('sh'): assert scr act_scr = act_scr or scr if isw: psbt.outputs[i].witness_script = scr elif style.endswith('sh'): psbt.outputs[i].redeem_script = scr if not outvals: h = TxOut(round(((1E8 * num_ins) - fee) / num_outs, 4), act_scr) else: h = TxOut(outvals[i], act_scr) txn.txs_out.append(h) with BytesIO() as b: txn.stream(b) psbt.txn = b.getvalue() rv = BytesIO() psbt.serialize(rv) assert rv.tell() <= MAX_TXN_LEN, 'too fat' return rv.getvalue()
def test_vs_bitcoind(match_key, check_against_bitcoind, bitcoind, start_sign, end_sign, we_finalize, num_dests): wallet_xfp = match_key() bal = bitcoind.getbalance() assert bal > 0, "need some play money; drink from a faucet" amt = round((bal/4)/num_dests, 6) args = {} for no in range(num_dests): dest = bitcoind.getrawchangeaddress() assert dest[0] in '2mn' or dest.startswith('tb1'), dest args[dest] = amt if 0: # old approach: fundraw + convert to psbt # working with hex strings here txn = bitcoind.createrawtransaction([], args) assert txn[0:2] == '02' #print(txn) resp = bitcoind.fundrawtransaction(txn) txn2 = resp['hex'] fee = resp['fee'] chg_pos = resp['changepos'] #print(txn2) print("Sending %.8f XTN to %s (Change back in position: %d)" % (amt, dest, chg_pos)) psbt = b64decode(bitcoind.converttopsbt(txn2, True)) # use walletcreatefundedpsbt # - updated/validated against 0.17.1 resp = bitcoind.walletcreatefundedpsbt([], args, 0, { 'subtractFeeFromOutputs': list(range(num_dests)), 'feeRate': 0.00001500}, True) if 0: # OMFG all this to reconstruct the rpc command! import json, decimal def EncodeDecimal(o): if isinstance(o, decimal.Decimal): return float(round(o, 8)) raise TypeError print('walletcreatefundedpsbt "[]" "[%s]" 0 {} true' % json.dumps(args, default=EncodeDecimal).replace('"', '\\"')) psbt = b64decode(resp['psbt']) fee = resp['fee'] chg_pos = resp['changepos'] open('debug/vs.psbt', 'wb').write(psbt) # check some basics mine = BasicPSBT().parse(psbt) for i in mine.inputs: got_xfp, = struct.unpack_from('I', list(i.bip32_paths.values())[0]) #assert hex(got_xfp) == hex(wallet_xfp), "wrong HD master key fingerprint" # see <https://github.com/bitcoin/bitcoin/issues/15884> if hex(got_xfp) != hex(wallet_xfp): raise pytest.xfail("wrong HD master key fingerprint") # pull out included txn txn2 = B2A(mine.txn) start_sign(psbt, finalize=we_finalize) # verify against how bitcoind reads it check_against_bitcoind(txn2, fee) signed = end_sign(accept=True) open('debug/vs-signed.psbt', 'wb').write(signed) if not we_finalize: b4 = BasicPSBT().parse(psbt) aft = BasicPSBT().parse(signed) assert b4 != aft, "signing didn't change anything?" open('debug/signed.psbt', 'wb').write(signed) resp = bitcoind.finalizepsbt(str(b64encode(signed), 'ascii'), True) #combined_psbt = b64decode(resp['psbt']) #open('debug/combined.psbt', 'wb').write(combined_psbt) assert resp['complete'] == True, "bitcoind wasn't able to finalize it" network = a2b_hex(resp['hex']) # assert resp['complete'] print("Final txn: %r" % network) open('debug/finalized-by-btcd.txn', 'wb').write(network) # try to send it txed = bitcoind.sendrawtransaction(B2A(network)) print("Final txn hash: %r" % txed) else: assert signed[0:4] != b'psbt', "expecting raw bitcoin txn" #print("Final txn: %s" % B2A(signed)) open('debug/finalized-by-cc.txn', 'wb').write(signed) txed = bitcoind.sendrawtransaction(B2A(signed)) print("Final txn hash: %r" % txed)
def build_psbt(ctx, xfp, addrs, pubkey=None, xpubs=None, redeem=None): locals().update(ctx.obj) payout_address = ctx.obj['payout_address'] out_psbt = ctx.obj['output_psbt'] if pubkey: assert len(addrs) == 1 # can only be single addr in that case assert len(pubkey) == 33 spending = [] total = 0 psbt = BasicPSBT() for path, addr in addrs: print(f"addr: {path} => {addr} ... ", end='') rr = explora('address', addr, 'utxo') if not rr: print('nada') continue here = 0 for u in rr: here += u['value'] tt = TxIn(h2b_rev(u['txid']), u['vout']) spending.append(tt) #print(rr) pin = BasicPSBTInput(idx=len(psbt.inputs)) psbt.inputs.append(pin) pubkey = pubkey or calc_pubkey(xpubs, path) pin.bip32_paths[pubkey] = str2path(xfp, path) # fetch the UTXO for witness signging td = explora('tx', u['txid'], 'hex', is_json=False) #print(f"txis {u['txid']}:\b{td!r}") outpt = Tx.from_hex(td.decode('ascii')).txs_out[u['vout']] with BytesIO() as b: outpt.stream(b) pin.witness_utxo = b.getvalue() if redeem: pin.redeem_script = redeem print('%.8f BTC' % (here / 1E8)) total += here if len(spending) > 15: print("Reached practical limit on # of inputs. " "You'll need to repeat this process again later.") break assert total, "Sorry! Didn't find any UTXO" print("Found total: %.8f BTC" % (total / 1E8)) if payout_address: print("Planning to send to: %s" % payout_address) dest_scr = BTC.contract.for_address(payout_address) txn = Tx(2, spending, [TxOut(total, dest_scr)]) else: print("Output section of PSBT will be empty. Change downstream") txn = Tx(2, spending, []) fee = tx_fee.recommended_fee_for_tx(txn) # placeholder, single output that isn't change pout = BasicPSBTOutput(idx=0) psbt.outputs.append(pout) print("Guestimate fee: %.8f BTC" % (fee / 1E8)) if txn.txs_out: txn.txs_out[0].coin_value -= fee # write txn into PSBT with BytesIO() as b: txn.stream(b) psbt.txn = b.getvalue() out_psbt.write(psbt.as_bytes()) print("PSBT to be signed:\n\n\t" + out_psbt.name, end='\n\n')
signed2 = end_sign(True) open('debug/chg-signed2.psbt', 'wb').write(signed) aft = BasicPSBT().parse(signed) aft2 = BasicPSBT().parse(signed2) assert aft.txn == aft2.txn @pytest.mark.parametrize('case', [ 1, 2]) @pytest.mark.bitcoind def test_change_fraud_path(start_sign, end_sign, case, check_against_bitcoind, cap_story): # fraud: BIP-32 path of output doesn't lead to pubkey indicated # NOTE: out#1 is change: chg_addr = 'mvBGHpVtTyjmcfSsy6f715nbTGvwgbgbwo' psbt = open('data/example-change.psbt', 'rb').read() b4 = BasicPSBT().parse(psbt) (pubkey, path), = b4.outputs[1].bip32_paths.items() skp = bytearray(b4.outputs[1].bip32_paths[pubkey]) if case == 1: # change subkey skp[-2] ^= 0x01 elif case == 2: # change xfp skp[0] ^= 0x01 b4.outputs[1].bip32_paths[pubkey] = bytes(skp) with BytesIO() as fd: b4.serialize(fd) mod_psbt = fd.getvalue()
def doit(f_or_data, accept=True, finalize=False, accept_ms_import=False, complete=False): if f_or_data[0:5] == b'psbt\xff': ip = f_or_data filename = 'memory' else: filename = f_or_data ip = open(f_or_data, 'rb').read() if ip[0:10] == b'70736274ff': ip = a2b_hex(ip.strip()) assert ip[0:5] == b'psbt\xff' psbtname = 'ftrysign' # population control from glob import glob import os pat = microsd_path(psbtname + '*.psbt') for f in glob(pat): assert 'psbt' in f os.unlink(f) with open_microsd(psbtname + '.psbt', 'wb') as sd: sd.write(ip) goto_home() pick_menu_item('Ready To Sign') time.sleep(.1) _, story = cap_story() if 'Choose PSBT file' in story: need_keypress('y') time.sleep(.1) pick_menu_item(psbtname + '.psbt') time.sleep(.1) if accept_ms_import: # XXX would be better to do cap_story here, but that would limit test to simulator need_keypress('y') time.sleep(0.050) title, story = cap_story() assert title == 'OK TO SEND?' if accept != None: need_keypress('y' if accept else 'x') if accept == False: time.sleep(0.050) # look for "Aborting..." ?? return ip, None # wait for it to finish for r in range(10): time.sleep(0.1) title, story = cap_story() if title == 'PSBT Signed': break else: assert False, 'timed out' result_fname = story.split('\n')[-1] result = open_microsd(result_fname, 'rb').read() # read back final product if finalize: assert 'final' in result_fname from pycoin.tx.Tx import Tx # parse it a little assert result[ 0:4] != b'psbt', 'still a PSBT, but asked for finalize' t = Tx.from_bin(result) assert t.version in [1, 2] else: if complete: assert '-signed' in result_fname else: assert '-part' in result_fname from psbt import BasicPSBT was = BasicPSBT().parse(ip) now = BasicPSBT().parse(result) assert was.txn == now.txn assert was != now return ip, result