def doit(raw_txn): # verify our understanding of a TXN (and esp its outputs) matches # the same values as what bitcoind generates try: return bitcoind.decoderawtransaction(B2A(raw_txn)) except ConnectionResetError: # bitcoind sleeps on us sometimes, give it another chance. return bitcoind.decoderawtransaction(B2A(raw_txn))
def test_ms_cli(dev, addr_fmt, clear_ms, import_ms_wallet, addr_vs_path, M=1, N=3): # exercise the p2sh command of ckcc:cli ... hard to do manually. from subprocess import check_output clear_ms() keys = import_ms_wallet(M, N, name='cli-test', accept=1) pmapper = lambda i: [HARD(45), i, 0,0] scr, pubkeys, xfp_paths = make_redeem(M, keys, pmapper) def decode_path(p): return '/'.join(str(i) if i < 0x80000000 else "%d'"%(i& 0x7fffffff) for i in p) for mode in [ 'full', 'prefix']: args = ['ckcc'] if dev.is_simulator: args += ['-x'] args += ['p2sh', '-q'] if addr_fmt == AF_P2WSH: args += ['-s'] elif addr_fmt == AF_P2WSH_P2SH: args += ['-s', '-w'] if mode == 'full': args += ['-p', "", B2A(scr)] args += [xfp2str(x)+'/'+decode_path(path) for x,*path in xfp_paths] else: args += [B2A(scr)] args += [xfp2str(x)+'/'+decode_path(path[1:]) for x,*path in xfp_paths] print('CMD: ' + (' '.join(args))) addr = check_output(args, encoding='ascii').strip() print(addr) addr_vs_path(addr, addr_fmt=addr_fmt, script=scr) # test case for make_ms_address really. expect_addr, scr2, _ = make_ms_address(M, keys, path_mapper=pmapper, addr_fmt=addr_fmt) assert expect_addr == addr assert scr2 == scr # need to re-start our connection once ckcc has talked to simulator dev.start_encryption() dev.check_mitm() clear_ms()
def doit(M, pubkeys, fmt): fmt = { AF_P2SH: 'legacy', AF_P2WSH: 'bech32', AF_P2WSH_P2SH: 'p2sh-segwit' }[fmt] try: rv = bitcoind.createmultisig(M, [B2A(i) for i in pubkeys], fmt) except ConnectionResetError: # bitcoind sleeps on us sometimes, give it another chance. rv = bitcoind.createmultisig(M, [B2A(i) for i in pubkeys], fmt) return rv['address'], rv['redeemScript']
def doit(M, keys, addr_fmt=AF_P2SH, bip45=True, **make_redeem_args): # test we are showing addresses correctly # - verifies against bitcoind as well addr_fmt = unmap_addr_fmt.get(addr_fmt, addr_fmt) # make a redeem script, using provided keys/pubkeys if bip45: make_redeem_args['path_mapper'] = lambda i: [HARD(45), i, 0,0] scr, pubkeys, xfp_paths = make_redeem(M, keys, **make_redeem_args) assert len(scr) <= 520, "script too long for standard!" got_addr = dev.send_recv(CCProtocolPacker.show_p2sh_address( M, xfp_paths, scr, addr_fmt=addr_fmt), timeout=None) title, story = cap_story() #print(story) assert got_addr in story assert all((xfp2str(xfp) in story) for xfp,_,_ in keys) if bip45: for i in range(len(keys)): assert ('/_/%d/0/0' % i) in story need_keypress('y') # check expected addr was generated based on my math addr_vs_path(got_addr, addr_fmt=addr_fmt, script=scr) # also check against bitcoind core_addr, core_scr = bitcoind_p2sh(M, pubkeys, addr_fmt) assert B2A(scr) == core_scr assert core_addr == got_addr
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_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_sign_wutxo(start_sign, set_seed_words, end_sign, cap_story, sim_exec, sim_execfile): # Example from SomberNight: we can sign it, but signature won't be accepted by # network because the PSBT lies about the UTXO amount and tries to give away to miners, # as overly-large fee. set_seed_words( 'fault lava rice chest uncle exclude power tornado catalog stool' ' swear rival sun aspect oyster deer pepper exchange scrap toward' ' mix second world shaft') in_psbt = a2b_hex(open('data/snight-example.psbt', 'rb').read()[:-1]) for fin in (False, True): start_sign(in_psbt, finalize=fin) time.sleep(.1) _, story = cap_story() #print(story) assert 'Network fee:\n0.00000500 XTN' in story # check we understood it right ex = dict(had_witness=False, num_inputs=1, num_outputs=1, sw_inputs=[True], miner_fee=500, warnings_expected=0, lock_time=1442308, total_value_out=99500, total_value_in=100000) rv = sim_exec('import main; main.EXPECT = %r; ' % ex) if rv: pytest.fail(rv) rv = sim_execfile('devtest/check_decode.py') if rv: pytest.fail(rv) signed = end_sign(True, finalize=fin) open('debug/sn-signed.' + ('txn' if fin else 'psbt'), 'wt').write(B2A(signed))
def test_hmac_key(sim_exec, count=50): from hashlib import pbkdf2_hmac, sha256 from constants import simulator_serial_number from ckcc_protocol.constants import PBKDF2_ITER_COUNT salt = sha256(b'pepper' + simulator_serial_number.encode('ascii')).digest() for i in range(count): pw = ('test%09d' % i).encode('ascii') pw = pw[1:i] if i > 2 else pw cmd = "from users import calc_hmac_key; from h import b2a_hex; " + \ f"RV.write(b2a_hex(calc_hmac_key({pw})))" got = sim_exec(cmd) expect = B2A(pbkdf2_hmac('sha256', pw, salt, PBKDF2_ITER_COUNT)) assert got == expect print(got)
def test_hmac_key(dev, sim_exec, count=10): from hashlib import pbkdf2_hmac, sha256 from ckcc_protocol.constants import PBKDF2_ITER_COUNT sn = sim_exec('import version; RV.write(version.serial_number().encode())').encode() salt = sha256(b'pepper'+sn).digest() for i in range(count): pw = ('test%09d' % i).encode('ascii') pw = pw[1:i] if i > 2 else pw cmd = "from users import calc_hmac_key; from h import b2a_hex; " + \ f"RV.write(b2a_hex(calc_hmac_key({pw})))" got = sim_exec(cmd) #print('pw=%r s=%r cnt=%d' % (pw, salt, PBKDF2_ITER_COUNT)) expect = B2A(pbkdf2_hmac('sha512', pw, salt, PBKDF2_ITER_COUNT)[0:32]) assert got == expect print(got)
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_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)
with BytesIO() as fd: b4.serialize(fd) mod_psbt = fd.getvalue() open('debug/mod-%d.psbt' % case, 'wb').write(mod_psbt) if case == 1: start_sign(mod_psbt) with pytest.raises(CCProtoError) as ee: signed = end_sign(True) assert 'BIP-32 path' in str(ee) elif case == 2: # will not consider it a change output, but not an error either start_sign(mod_psbt) check_against_bitcoind(B2A(b4.txn), Decimal('0.00000294'), change_outs=[]) time.sleep(.1) _, story = cap_story() assert chg_addr in story assert 'Change back:' not in story signed = end_sign(True) @pytest.mark.bitcoind 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:
with BytesIO() as fd: b4.serialize(fd) mod_psbt = fd.getvalue() open('debug/mod-%d.psbt' % case, 'wb').write(mod_psbt) if case == 1: start_sign(mod_psbt) with pytest.raises(CCProtoError) as ee: signed = end_sign(True) assert 'BIP32 path' in str(ee) elif case == 2: # will not consider it a change output, but not an error either start_sign(mod_psbt) check_against_bitcoind(B2A(b4.txn), Decimal('0.00000294'), change_outs=[]) time.sleep(.1) _, story = cap_story() assert chg_addr in story assert 'Change back:' not in story signed = end_sign(True) @pytest.mark.bitcoind def test_change_fraud_addr(start_sign, end_sign, check_against_bitcoind, cap_story): # fraud: BIP32 path of output doesn't match TXO address
def test_bip_vectors(mode, index, entropy, expect, set_encoded_secret, dev, cap_menu, pick_menu_item, goto_home, cap_story, need_keypress, microsd_path, settings_set, sim_eval, sim_exec ): set_encoded_secret(a2b_hex(EXAMPLE_XPRV)) settings_set('chain', 'BTC') goto_home() pick_menu_item('Advanced') pick_menu_item('Derive Entropy') time.sleep(0.1) title, story = cap_story() assert 'seed value' in story assert 'other wallet systems' in story need_keypress('y') time.sleep(0.1) pick_menu_item(mode) if index is not None: time.sleep(0.1) for n in str(index): need_keypress(n) need_keypress('y') time.sleep(0.1) title, story = cap_story() assert f'Path Used (index={index}):' in story assert "m/83696968'/" in story assert f"/{index}'" in story if entropy is not None: assert f"Raw Entropy:\n{entropy}" in story do_import = False if 'words' in mode: num_words = int(mode.split()[0]) assert f'Seed words ({num_words}):' in story assert f"m/83696968'/39'/0'/{num_words}'/{index}'" in story assert '\n 1: ' in story assert f'\n{num_words}: ' in story got = [ln[4:] for ln in story.split('\n') if len(ln)>5 and ln[2] == ':'] assert ' '.join(got) == expect do_import = 'words' elif 'XPRV' in mode: assert 'Derived XPRV:' in story assert f"m/83696968'/32'/{index}'" in story assert expect in story do_import = 'xprv' elif 'WIF' in mode: assert 'WIF (privkey)' in story assert f"m/83696968'/2'/{index}'" in story assert expect in story elif 'bytes hex' in mode: width = int(mode.split('-')[0]) assert width in { 32, 64} assert f'Hex ({width} bytes):' in story assert f"m/83696968'/128169'/{width}'/{index}'" in story assert expect in story else: raise ValueError(mode) # write to SD msg = story.split('Press', 1)[0] if 1: assert 'Press 1 to save' in story need_keypress('1') time.sleep(0.1) title, story = cap_story() assert title == 'Saved' fname = story.split('\n')[-1] need_keypress('y') time.sleep(0.1) title, story = cap_story() assert story.startswith(msg) path = microsd_path(fname) assert path.endswith('.txt') txt = open(path, 'rt').read() assert txt.strip() == msg.strip() if do_import: assert '2 to switch to derived secret' in story try: time.sleep(0.1) need_keypress('2') time.sleep(0.1) title, story = cap_story() assert 'New master key in effect' in story encoded = sim_eval('main.pa.fetch()') print(encoded) assert encoded.startswith('bytearray(b') encoded = eval(encoded) assert len(encoded) == 72 marker = encoded[0] if do_import == 'words': assert marker & 0x80 == 0x80 width = ((marker & 0x3) + 2) * 8 assert width in {16, 24, 32} assert encoded[1:1+width] == a2b_hex(entropy) elif do_import == 'xprv': assert marker == 0x01 node = BIP32Node.from_hwif(expect) ch, pk = encoded[1:33], encoded[33:65] assert node.chain_code() == ch assert node.secret_exponent() == int(B2A(pk), 16) finally: # required cleanup sim_exec('import main; from pincodes import PinAttempt; ' 'main.pa = PinAttempt(); main.pa.setup("12-12"); main.pa.login();') need_keypress('x')
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)