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 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 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')