def multiply(s, pub, return_serialized=True): '''Input binary compressed pubkey P(33 bytes) and scalar s(32 bytes), return s*P. The return value is a binary compressed public key, or a PublicKey object if return_serialized is False. Note that the called function does the type checking of the scalar s. ('raw' options passed in) ''' try: CKey(s) except ValueError: raise ValueError("Invalid tweak for libsecp256k1 " "multiply: {}".format(bintohex(s))) pub_obj = CPubKey(pub) if not pub_obj.is_fullyvalid(): raise ValueError("Invalid pubkey for multiply: {}".format( bintohex(pub))) privkey_arg = ctypes.c_char_p(s) pubkey_buf = pub_obj._to_ctypes_char_array() ret = secp_lib.secp256k1_ec_pubkey_tweak_mul(secp256k1_context_verify, pubkey_buf, privkey_arg) if ret != 1: assert ret == 0 raise ValueError('Multiplication failed') if not return_serialized: return CPubKey._from_ctypes_char_array(pubkey_buf) return bytes(CPubKey._from_ctypes_char_array(pubkey_buf))
def human_readable_transaction(tx, jsonified=True): """ Given a CTransaction object, output a human readable json-formatted string (suitable for terminal output or large GUI textbox display) containing all details of that transaction. If `jsonified` is False, the dict is returned, instead of the json string. """ assert isinstance(tx, CTransaction) outdict = {} outdict["hex"] = bintohex(tx.serialize()) outdict["inputs"] = [] outdict["outputs"] = [] outdict["txid"] = bintohex(tx.GetTxid()[::-1]) outdict["nLockTime"] = tx.nLockTime outdict["nVersion"] = tx.nVersion for i, inp in enumerate(tx.vin): if not tx.wit.vtxinwit: # witness section is not initialized/empty witarg = None else: witarg = tx.wit.vtxinwit[i] outdict["inputs"].append(human_readable_input(inp, witarg)) for i, out in enumerate(tx.vout): outdict["outputs"].append(human_readable_output(out)) if not jsonified: return outdict return json.dumps(outdict, indent=4)
def detect_script_type(script_str): """ Given a scriptPubKey, decide which engine to use, one of: p2pkh, p2sh-p2wpkh, p2wpkh. Note that for the p2sh case, we are assuming the nature of the redeem script (p2wpkh wrapped) because that is what we support; but we can't know for sure, from the sPK only. Raises EngineError if the type cannot be detected, so callers MUST handle this exception to avoid crashes. """ script = btc.CScript(script_str) if not script.is_valid(): raise EngineError("Unknown script type for script '{}'" .format(bintohex(script_str))) if script.is_p2pkh(): return TYPE_P2PKH elif script.is_p2sh(): # see note above. # note that is_witness_v0_nested_keyhash does not apply, # since that picks up scriptSigs not scriptPubKeys. return TYPE_P2SH_P2WPKH elif script.is_witness_v0_keyhash(): return TYPE_P2WPKH elif script.is_witness_v0_scripthash(): return TYPE_P2WSH raise EngineError("Unknown script type for script '{}'" .format(bintohex(script_str)))
def create_orderbook_obj(self): with self.taker.dblock: rows = self.taker.db.execute('SELECT * FROM orderbook;').fetchall() fbonds = self.taker.db.execute( "SELECT * FROM fidelitybonds;").fetchall() if not rows or not fbonds: return [] fidelitybonds = [] if jm_single().bc_interface != None: (fidelity_bond_data, fidelity_bond_values, bond_outpoint_conf_times) =\ get_fidelity_bond_data(self.taker) fidelity_bond_values_dict = dict([ (bond_data.maker_nick, bond_value) for (bond_data, _), bond_value in zip(fidelity_bond_data, fidelity_bond_values) ]) for ((parsed_bond, bond_utxo_data), fidelity_bond_value, bond_outpoint_conf_time)\ in zip(fidelity_bond_data, fidelity_bond_values, bond_outpoint_conf_times): fb = { "counterparty": parsed_bond.maker_nick, "utxo": { "txid": bintohex(parsed_bond.utxo[0]), "vout": parsed_bond.utxo[1] }, "bond_value": fidelity_bond_value, "locktime": parsed_bond.locktime, "amount": bond_utxo_data["value"], "script": bintohex(bond_utxo_data["script"]), "utxo_confirmations": bond_utxo_data["confirms"], "utxo_confirmation_timestamp": bond_outpoint_conf_time, "utxo_pub": bintohex(parsed_bond.utxo_pub), "cert_expiry": parsed_bond.cert_expiry } fidelitybonds.append(fb) else: fidelity_bond_values_dict = {} offers = [] for row in rows: o = dict(row) if 'cjfee' in o: if o['ordertype'] == 'swabsoffer'\ or o['ordertype'] == 'sw0absoffer': o['cjfee'] = int(o['cjfee']) else: o['cjfee'] = str(Decimal(o['cjfee'])) o["fidelity_bond_value"] = fidelity_bond_values_dict.get( o["counterparty"], 0) offers.append(o) return {"offers": offers, "fidelitybonds": fidelitybonds}
def test_sign_standard_txs(addrtype): # liberally copied from python-bitcoinlib tests, # in particular see: # https://github.com/petertodd/python-bitcoinlib/pull/227 # Create the (in)famous correct brainwallet secret key. priv = hashlib.sha256(b'correct horse battery staple').digest() + b"\x01" pub = btc.privkey_to_pubkey(priv) # Create an address from that private key. # (note that the input utxo is fake so we are really only creating # a destination here). scriptPubKey = btc.CScript([btc.OP_0, btc.Hash160(pub)]) address = btc.P2WPKHCoinAddress.from_scriptPubKey(scriptPubKey) # Create a dummy outpoint; use same 32 bytes for convenience txid = priv[:32] vout = 2 amount = btc.coins_to_satoshi(float('0.12345')) # Calculate an amount for the upcoming new UTXO. Set a high fee to bypass # bitcoind minfee setting. amount_less_fee = int(amount - btc.coins_to_satoshi(0.01)) # Create a destination to send the coins. destination_address = address target_scriptPubKey = scriptPubKey # Create the unsigned transaction. txin = btc.CTxIn(btc.COutPoint(txid[::-1], vout)) txout = btc.CTxOut(amount_less_fee, target_scriptPubKey) tx = btc.CMutableTransaction([txin], [txout]) # Calculate the signature hash for the transaction. This is then signed by the # private key that controls the UTXO being spent here at this txin_index. if addrtype == "p2wpkh": sig, msg = btc.sign(tx, 0, priv, amount=amount, native="p2wpkh") elif addrtype == "p2sh-p2wpkh": sig, msg = btc.sign(tx, 0, priv, amount=amount, native=False) elif addrtype == "p2pkh": sig, msg = btc.sign(tx, 0, priv) else: assert False if not sig: print(msg) raise print("created signature: ", bintohex(sig)) print("serialized transaction: {}".format(bintohex(tx.serialize()))) print("deserialized transaction: {}\n".format( btc.human_readable_transaction(tx)))
def __repr__(self): """ Specified here to allow logging. """ # note: will throw if not fully initalised r = self.reveal() success, utxo = utxo_to_utxostr(r["utxo"]) assert success, "invalid utxo in PoDLE." return pformat({'used': r["used"], 'utxo': utxo, 'P': bintohex(r["P"]), 'P2': bintohex(r["P2"]), 'commit': bintohex(r["commit"]), 'sig': bintohex(r["sig"]), 'e': bintohex(r["e"])})
def auth_counterparty(self, btc_sig, auth_pub, maker_pk): """Validate the counterpartys claim to own the btc address/pubkey that will be used for coinjoining with an ecdsa verification. """ try: # maker pubkey as message is in hex format: if not btc.ecdsa_verify(bintohex(maker_pk), btc_sig, auth_pub): jlog.debug('signature didnt match pubkey and message') return False except Exception as e: jlog.info("Failed ecdsa verify for maker pubkey: " + bintohex(maker_pk)) jlog.info("Exception was: " + repr(e)) return False return True
def push(self): jlog.debug('\n' + bintohex(self.latest_tx.serialize())) self.txid = bintohex(self.latest_tx.GetTxid()[::-1]) jlog.info('txid = ' + self.txid) #If we are sending to a bech32 address, in case of sweep, will #need to use that bech32 for address import, which requires #converting to script (Core does not allow import of bech32) if self.my_cj_addr.lower()[:2] in ['bc', 'tb']: notify_addr = btc.CCoinAddress(self.my_cj_addr).to_scriptPubKey() else: notify_addr = self.my_cj_addr #add the callbacks *before* pushing to ensure triggering; #this does leave a dangling notify callback if the push fails, but #that doesn't cause problems. self.wallet_service.register_callbacks([self.unconfirm_callback], self.txid, "unconfirmed") self.wallet_service.register_callbacks([self.confirm_callback], self.txid, "confirmed") task.deferLater( reactor, float(jm_single().config.getint("TIMEOUT", "unconfirm_timeout_sec")), self.handle_unbroadcast_transaction, self.txid, self.latest_tx) tx_broadcast = jm_single().config.get('POLICY', 'tx_broadcast') nick_to_use = None if tx_broadcast == 'self': pushed = self.push_ourselves() elif tx_broadcast in ['random-peer', 'not-self']: n = len(self.maker_utxo_data) if tx_broadcast == 'random-peer': i = random.randrange(n + 1) else: i = random.randrange(n) if i == n: pushed = self.push_ourselves() else: nick_to_use = list(self.maker_utxo_data.keys())[i] pushed = True else: jlog.info("Only self, random-peer and not-self broadcast " "methods supported. Reverting to self-broadcast.") pushed = self.push_ourselves() if not pushed: self.on_finished_callback(False, fromtx=True) else: if nick_to_use: return (nick_to_use, bintohex(self.latest_tx.serialize()))
def get_transaction(self, txid): """ Argument txid is passed in binary. Returns a serialized transaction for txid txid, in hex as returned by Bitcoin Core rpc, or None if no transaction can be retrieved. Works also for watch-only wallets. """ htxid = bintohex(txid) #changed syntax in 0.14.0; allow both syntaxes try: res = self.rpc("gettransaction", [htxid, True]) except Exception as e: try: res = self.rpc("gettransaction", [htxid, 1]) except JsonRpcError as e: #This should never happen (gettransaction is a wallet rpc). log.warn("Failed gettransaction call; JsonRpcError: " + repr(e)) return None except Exception as e: log.warn("Failed gettransaction call; unexpected error:") log.warn(str(e)) return None if "confirmations" not in res: log.warning("Malformed gettx result: " + str(res)) return None return res
def on_JM_MAKE_TX(self, nick_list, txhex): show_receipt("JMMAKETX", nick_list, txhex) d = self.callRemote(JMSigReceived, nick="dummynick", sig="xxxsig") self.defaultCallbacks(d) #add dummy calls to check message sign and message verify d2 = self.callRemote(JMRequestMsgSig, nick="dummynickforsign", cmd="command1", msg="msgforsign", msg_to_be_signed="fullmsgforsign", hostid="hostid1") self.defaultCallbacks(d2) #To test, this must include a valid ecdsa sig fullmsg = "fullmsgforverify" priv = b"\xaa"*32 + b"\x01" pub = bintohex(bitcoin.privkey_to_pubkey(priv)) sig = bitcoin.ecdsa_sign(fullmsg, priv) d3 = self.callRemote(JMRequestMsgSigVerify, msg="msgforverify", fullmsg=fullmsg, sig=sig, pubkey=pub, nick="dummynickforverify", hashlen=4, max_encoded=5, hostid="hostid2") self.defaultCallbacks(d3) d4 = self.callRemote(JMSigReceived, nick="dummynick", sig="dummysig") self.defaultCallbacks(d4) return {'accepted': True}
def on_JM_AUTH_RECEIVED(self, nick, offer, commitment, revelation, amount, kphex): offer = json.loads(offer) revelation = json.loads(revelation) retval = self.client.on_auth_received(nick, offer, commitment, revelation, amount, kphex) if not retval[0]: jlog.info("Maker refuses to continue on receiving auth.") else: utxos, auth_pub, cj_addr, change_addr, btc_sig = retval[1:] # json does not allow non-string keys: utxos_strkeyed = {} for k in utxos: success, u = utxo_to_utxostr(k) assert success utxos_strkeyed[u] = { "value": utxos[k]["value"], "address": utxos[k]["address"] } auth_pub_hex = bintohex(auth_pub) d = self.callRemote(commands.JMIOAuth, nick=nick, utxolist=json.dumps(utxos_strkeyed), pubkey=auth_pub_hex, cjaddr=cj_addr, changeaddr=change_addr, pubkeysig=btc_sig) self.defaultCallbacks(d) return {"accepted": True}
def create_wallet_for_sync(wallet_structure, a, **kwargs): #We need a distinct seed for each run so as not to step over each other; #make it through a deterministic hash of all parameters including optionals. preimage = "".join([str(x) for x in a] + [str(y) for y in kwargs.values()]).encode("utf-8") print("using preimage: ", preimage) seedh = bintohex(btc.Hash(preimage))[:32] return make_wallets( 1, [wallet_structure], fixed_seeds=[seedh], **kwargs)[0]['wallet']
def write_to_podle_file(used, external): """ Update persisted commitment data in PODLE_COMMIT_FILE. """ to_write = {} to_write['used'] = [bintohex(x) for x in used] externalfmt = external_dict_to_file(external) to_write['external'] = externalfmt with open(PODLE_COMMIT_FILE, "wb") as f: f.write(json.dumps(to_write, indent=4).encode('utf-8'))
def human_readable_input(txinput, txinput_witness): """ Pass objects of type CTxIn and CTxInWitness (or None) and a dict of human-readable entries for this input is returned. """ assert isinstance(txinput, CTxIn) outdict = {} success, u = utxo_to_utxostr( (txinput.prevout.hash[::-1], txinput.prevout.n)) assert success outdict["outpoint"] = u outdict["scriptSig"] = bintohex(txinput.scriptSig) outdict["nSequence"] = txinput.nSequence if txinput_witness: outdict["witness"] = bintohex( txinput_witness.scriptWitness.serialize()) return outdict
def on_tx_received(self, nick, tx, offerinfo): """Called when the counterparty has sent an unsigned transaction. Sigs are created and returned if and only if the transaction passes verification checks (see verify_unsigned_tx()). """ # special case due to cjfee passed as string: it can accidentally parse # as hex: if not isinstance(offerinfo["offer"]["cjfee"], str): offerinfo["offer"]["cjfee"] = bintohex(offerinfo["offer"]["cjfee"]) try: tx = btc.CMutableTransaction.deserialize(tx) except Exception as e: return (False, 'malformed tx. ' + repr(e)) # if the above deserialization was successful, the human readable # parsing will be also: jlog.info('obtained tx\n' + btc.human_readable_transaction(tx)) goodtx, errmsg = self.verify_unsigned_tx(tx, offerinfo) if not goodtx: jlog.info('not a good tx, reason=' + errmsg) return (False, errmsg) jlog.info('goodtx') sigs = [] utxos = offerinfo["utxos"] our_inputs = {} for index, ins in enumerate(tx.vin): utxo = (ins.prevout.hash[::-1], ins.prevout.n) if utxo not in utxos: continue script = self.wallet_service.addr_to_script(utxos[utxo]['address']) amount = utxos[utxo]['value'] our_inputs[index] = (script, amount) success, msg = self.wallet_service.sign_tx(tx, our_inputs) assert success, msg for index in our_inputs: # The second case here is kept for backwards compatibility. if self.wallet_service.get_txtype() == 'p2pkh': sigmsg = tx.vin[index].scriptSig elif self.wallet_service.get_txtype() == 'p2sh-p2wpkh': sig, pub = [ a for a in iter(tx.wit.vtxinwit[index].scriptWitness) ] scriptCode = btc.pubkey_to_p2wpkh_script(pub) sigmsg = btc.CScript([sig]) + btc.CScript(pub) + scriptCode elif self.wallet_service.get_txtype() == 'p2wpkh': sig, pub = [ a for a in iter(tx.wit.vtxinwit[index].scriptWitness) ] sigmsg = btc.CScript([sig]) + btc.CScript(pub) else: jlog.error("Taker has unknown wallet type") sys.exit(EXIT_FAILURE) sigs.append(base64.b64encode(sigmsg).decode('ascii')) return (True, sigs)
def test_JMRequestMsgSigVerify(self): fullmsg = 'fullmsgforverify' priv = b"\xaa"*32 + b"\x01" pub = bintohex(bitcoin.privkey_to_pubkey(priv)) sig = bitcoin.ecdsa_sign(fullmsg, priv) yield self.init_client() yield self.callClient( JMRequestMsgSigVerify, msg='msgforverify', fullmsg=fullmsg, sig=sig, pubkey=pub, nick='dummynickforverify', hashlen=4, max_encoded=5, hostid='hostid2')
def on_JM_REQUEST_MSGSIG(self, nick, cmd, msg, msg_to_be_signed, hostid): sig = btc.ecdsa_sign(str(msg_to_be_signed), self.nick_priv) msg_to_return = str(msg) + " " + bintohex(self.nick_pubkey) + " " + sig d = self.callRemote(commands.JMMsgSignature, nick=nick, cmd=cmd, msg_to_return=msg_to_return, hostid=hostid) self.defaultCallbacks(d) return {'accepted': True}
def main(): parser = OptionParser( usage='usage: %prog [options] utxo destaddr1 destaddr2 ..', description=description, formatter=IndentedHelpFormatterWithNL()) parser.add_option( '-t', '--utxo-address-type', action='store', dest='utxo_address_type', help= ('type of address of coin being spent - one of "p2pkh", "p2wpkh", "p2sh-p2wpkh". ' 'No other scriptpubkey types (e.g. multisig) are supported. If not set, we default ' 'to what is in joinmarket.cfg.'), default="") add_base_options(parser) (options, args) = parser.parse_args() load_program_config(config_path=options.datadir) if len(args) < 2: quit(parser, 'Invalid syntax') u = args[0] priv = input('input private key for ' + u + ', in WIF compressed format : ') u, priv = get_utxo_info(','.join([u, priv])) if not u: quit(parser, "Failed to parse utxo info: " + u) destaddrs = args[1:] for d in destaddrs: if not validate_address(d): quit(parser, "Address was not valid; wrong network?: " + d) success, utxo = utxostr_to_utxo(u) if not success: quit(parser, "Failed to load utxo from string: " + utxo) if options.utxo_address_type == "": if jm_single().config.get("POLICY", "segwit") == "false": utxo_address_type = "p2pkh" elif jm_single().config.get("POLICY", "native") == "false": utxo_address_type = "p2sh-p2wpkh" else: utxo_address_type = "p2wpkh" else: utxo_address_type = options.utxo_address_type txsigned = sign(utxo, priv, destaddrs, utxo_address_type) if not txsigned: log.info( "Transaction signing operation failed, see debug messages for details." ) return log.info("Got signed transaction:\n" + bintohex(txsigned.serialize())) log.info(btc.human_readable_transaction(txsigned)) if input('Would you like to push to the network? (y/n):')[0] != 'y': log.info("You chose not to broadcast the transaction, quitting.") return jm_single().bc_interface.pushtx(txsigned.serialize())
def serialize_revelation(self, separator='|'): """ Outputs the over-the-wire format as used in Joinmarket communication protocol. """ state_dict = self.reveal() success, utxo = utxo_to_utxostr(state_dict["utxo"]) assert success, "invalid utxo in PoDLE" ser_list = [utxo] ser_list += [bintohex(state_dict[x]) for x in ["P", "P2", "sig", "e"]] ser_string = separator.join(ser_list) return ser_string
def pushtx(self, txbin): """ Given a binary serialized valid bitcoin transaction, broadcasts it to the network. """ txhex = bintohex(txbin) try: txid = self._rpc('sendrawtransaction', [txhex]) except JsonRpcConnectionError as e: log.warning('error pushing = ' + repr(e)) return False except JsonRpcError as e: log.warning('error pushing = ' + str(e.code) + " " + str(e.message)) return False return True
def human_readable_output(txoutput): """ Returns a dict of human-readable entries for this output. """ assert isinstance(txoutput, CTxOut) outdict = {} outdict["value_sats"] = txoutput.nValue outdict["scriptPubKey"] = bintohex(txoutput.scriptPubKey) try: addr = CCoinAddress.from_scriptPubKey(txoutput.scriptPubKey) outdict["address"] = str(addr) except CCoinAddressError: pass # non standard script return outdict
def verify_all_NUMS(write=False): """Check that the algorithm produces the expected NUMS values; more a sanity check than anything since if the file is modified, all of it could be; this function is mostly for testing, but runs fast with pre-computed context so can be run in user code too. """ nums_points = {} for i in range(256): nums_points[i] = bintohex(getNUMS(i)) if write: with open("nums_basepoints.txt", "wb") as f: from pprint import pformat f.write(pformat(nums_points).encode('utf-8')) assert nums_points == precomp_NUMS, "Precomputed NUMS points are not valid!"
def make_wallets(n, wallet_structures=None, mean_amt=1, sdev_amt=0, start_index=0, fixed_seeds=None, wallet_cls=SegwitWallet, mixdepths=5, populate_internal=False): '''n: number of wallets to be created wallet_structure: array of n arrays , each subarray specifying the number of addresses to be populated with coins at each depth (for now, this will only populate coins into 'receive' addresses) mean_amt: the number of coins (in btc units) in each address as above sdev_amt: if randomness in amouts is desired, specify here. Returns: a dict of dicts of form {0:{'seed':seed,'wallet':Wallet object},1:..,} ''' # FIXME: this is basically the same code as test/common.py assert mixdepths > 0 if len(wallet_structures) != n: raise Exception("Number of wallets doesn't match wallet structures") if not fixed_seeds: seeds = chunks(bintohex(os.urandom( BIP32Wallet.ENTROPY_BYTES * n)), BIP32Wallet.ENTROPY_BYTES * 2) else: seeds = fixed_seeds wallets = {} for i in range(n): assert len(seeds[i]) == BIP32Wallet.ENTROPY_BYTES * 2 w = open_test_wallet_maybe(seeds[i], seeds[i], mixdepths - 1, test_wallet_cls=wallet_cls) wallet_service = WalletService(w) wallets[i + start_index] = {'seed': seeds[i], 'wallet': wallet_service} for j in range(mixdepths): for k in range(wallet_structures[i][j]): deviation = sdev_amt * random.random() amt = mean_amt - sdev_amt / 2.0 + deviation if amt < 0: amt = 0.001 amt = float(Decimal(amt).quantize(Decimal(10)**-8)) jm_single().bc_interface.grab_coins(wallet_service.get_new_addr( j, populate_internal), amt) return wallets
def query_utxo_set(self, txout, includeconf=False, includeunconf=False): """If txout is either (a) a single utxo in (txidbin, n) form, or a list of the same, returns, as a list for each txout item, the result of gettxout from the bitcoind rpc for those utxos; if any utxo is invalid, None is returned. includeconf: if this is True, the current number of confirmations of the prescribed utxo is included in the returned result dict. includeunconf: if True, utxos which currently have zero confirmations are included in the result set. If the utxo is of a non-standard type such that there is no address, the address field in the dict is None. """ if not isinstance(txout, list): txout = [txout] result = [] for txo in txout: txo_hex = bintohex(txo[0]) if len(txo_hex) != 64: log.warn("Invalid utxo format, ignoring: {}".format(txo)) result.append(None) continue try: txo_idx = int(txo[1]) except ValueError: log.warn("Invalid utxo format, ignoring: {}".format(txo)) result.append(None) continue ret = self.rpc('gettxout', [txo_hex, txo_idx, includeunconf]) if ret is None: result.append(None) else: if ret['scriptPubKey'].get('addresses'): address = ret['scriptPubKey']['addresses'][0] else: address = None result_dict = { 'value': int(Decimal(str(ret['value'])) * Decimal('1e8')), 'address': address, 'script': hextobin(ret['scriptPubKey']['hex']) } if includeconf: result_dict['confirms'] = int(ret['confirmations']) result.append(result_dict) return result
def query_utxo_set(self, txout, includeconfs=False, include_mempool=True): """If txout is either (a) a single utxo in (txidbin, n) form, or a list of the same, returns, as a list for each txout item, the result of gettxout from the bitcoind rpc for those utxos; if any utxo is invalid, None is returned. includeconfs: if this is True, the current number of confirmations of the prescribed utxo is included in the returned result dict. include_mempool: if True, the contents of the mempool are included; this *both* means that utxos that are spent in in-mempool transactions are *not* returned, *and* means that utxos that are created in the mempool but have zero confirmations *are* returned. If the utxo is of a non-standard type such that there is no address, the address field in the dict is None. """ if not isinstance(txout, list): txout = [txout] result = [] for txo in txout: txo_hex = bintohex(txo[0]) if len(txo_hex) != 64: log.warn("Invalid utxo format, ignoring: {}".format(txo)) result.append(None) continue try: txo_idx = int(txo[1]) except ValueError: log.warn("Invalid utxo format, ignoring: {}".format(txo)) result.append(None) continue ret = self._rpc('gettxout', [txo_hex, txo_idx, include_mempool]) if ret is None: result.append(None) else: result_dict = { 'value': int(Decimal(str(ret['value'])) * Decimal('1e8')), 'script': hextobin(ret['scriptPubKey']['hex']) } if includeconfs: result_dict['confirms'] = int(ret['confirmations']) result.append(result_dict) return result
def test_podle_error_string(setup_podle): example_utxos = [(b"\x00"*32, i) for i in range(6)] priv_utxo_pairs = [('fakepriv1', example_utxos[0]), ('fakepriv2', example_utxos[1])] to = example_utxos[2:4] ts = example_utxos[4:6] wallet_service = make_wallets(1, [[1, 0, 0, 0, 0]])[0]['wallet'] cjamt = 100 tua = "3" tuamtper = "20" errmgsheader, errmsg = generate_podle_error_string(priv_utxo_pairs, to, ts, wallet_service, cjamt, tua, tuamtper) assert errmgsheader == ("Failed to source a commitment; this debugging information" " may help:\n\n") y = [bintohex(x[0]) for x in example_utxos] assert all([errmsg.find(x) != -1 for x in y]) #ensure OK with nothing errmgsheader, errmsg = generate_podle_error_string([], [], [], wallet_service, cjamt, tua, tuamtper)
def main(): parser = OptionParser(usage='usage: %prog [options] walletname', description=description) add_base_options(parser) parser.add_option('-m', '--mixdepth', action='store', type='int', dest='mixdepth', help='mixdepth/account, default 0', default=0) parser.add_option('-g', '--gap-limit', action='store', type='int', dest='gaplimit', default=6, help='gap limit for Joinmarket wallet, default 6.') parser.add_option( '-f', '--txfee', action='store', type='int', dest='txfee', default=-1, help='Bitcoin miner tx_fee to use for transaction(s). A number higher ' 'than 1000 is used as "satoshi per KB" tx fee. A number lower than that ' 'uses the dynamic fee estimation of your blockchain provider as ' 'confirmation target. This temporarily overrides the "tx_fees" setting ' 'in your joinmarket.cfg. Works the same way as described in it. Check ' 'it for examples.') parser.add_option('-a', '--amtmixdepths', action='store', type='int', dest='amtmixdepths', help='number of mixdepths in wallet, default 5', default=5) parser.add_option( '-N', '--net-transfer', action='store', type='int', dest='net_transfer', help='how many sats are sent to the "receiver", default randomised.', default=-1000001) (options, args) = parser.parse_args() snicker_plugin = JMPluginService("SNICKER") load_program_config(config_path=options.datadir, plugin_services=[snicker_plugin]) if len(args) != 1: log.error("Invalid arguments, see --help") sys.exit(EXIT_ARGERROR) wallet_name = args[0] check_regtest() # If tx_fees are set manually by CLI argument, override joinmarket.cfg: if int(options.txfee) > 0: jm_single().config.set("POLICY", "tx_fees", str(options.txfee)) max_mix_depth = max([options.mixdepth, options.amtmixdepths - 1]) wallet_path = get_wallet_path(wallet_name, None) wallet = open_test_wallet_maybe( wallet_path, wallet_name, max_mix_depth, wallet_password_stdin=options.wallet_password_stdin, gap_limit=options.gaplimit) wallet_service = WalletService(wallet) if wallet_service.rpc_error: sys.exit(EXIT_FAILURE) snicker_plugin.start_plugin_logging(wallet_service) # in this script, we need the wallet synced before # logic processing for some paths, so do it now: while not wallet_service.synced: wallet_service.sync_wallet(fast=not options.recoversync) # the sync call here will now be a no-op: wallet_service.startService() fee_est = estimate_tx_fee(2, 3, txtype=wallet_service.get_txtype()) # first, order the utxos in the mixepth by size. Then (this is the # simplest algorithm; we could be more sophisticated), choose the # *second* largest utxo as the receiver utxo; this ensures that we # have enough for the proposer to cover. We consume utxos greedily, # meaning we'll at least some of the time, be consolidating. utxo_dict = wallet_service.get_utxos_by_mixdepth()[options.mixdepth] if not len(utxo_dict) >= 2: log.error( "Cannot create fake SNICKER tx without at least two utxos, quitting" ) sys.exit(EXIT_ARGERROR) # sort utxos by size sorted_utxos = sorted(list(utxo_dict.keys()), key=lambda k: utxo_dict[k]['value'], reverse=True) # receiver is the second largest: receiver_utxo = sorted_utxos[1] receiver_utxo_val = utxo_dict[receiver_utxo] # gather the other utxos into a list to select from: nonreceiver_utxos = [sorted_utxos[0]] + sorted_utxos[2:] # get the net transfer in our fake coinjoin: if options.net_transfer < -1000001: log.error("Net transfer must be greater than negative 1M sats") sys.exit(EXIT_ARGERROR) if options.net_transfer == -1000001: # default; low-ish is more realistic and avoids problems # with dusty utxos options.net_transfer = random.randint(-1000, 1000) # select enough to cover: receiver value + fee + transfer + breathing room # we select relatively greedily to support consolidation, since # this transaction does not pretend to isolate the coins. try: available = [{ 'utxo': utxo, 'value': utxo_dict[utxo]["value"] } for utxo in nonreceiver_utxos] # selection algos return [{"utxo":..,"value":..}]: prop_utxos = { x["utxo"] for x in select_greedy( available, receiver_utxo_val["value"] + fee_est + options.net_transfer + 1000) } prop_utxos = list(prop_utxos) prop_utxo_vals = [utxo_dict[prop_utxo] for prop_utxo in prop_utxos] except NotEnoughFundsException as e: log.error(repr(e)) sys.exit(EXIT_FAILURE) # Due to the fake nature of this transaction, and its distinguishability # (not only in trivial output pattern, but also in subset-sum), there # is little advantage in making it use different output mixdepths, so # here to prevent fragmentation, everything is kept in the same mixdepth. receiver_addr, proposer_addr, change_addr = (wallet_service.script_to_addr( wallet_service.get_new_script(options.mixdepth, 1)) for _ in range(3)) # persist index update: wallet_service.save_wallet() outputs = btc.construct_snicker_outputs( sum([x["value"] for x in prop_utxo_vals]), receiver_utxo_val["value"], receiver_addr, proposer_addr, change_addr, fee_est, options.net_transfer) tx = btc.make_shuffled_tx(prop_utxos + [receiver_utxo], outputs, version=2, locktime=0) # before signing, check we satisfied the criteria, otherwise # this is pointless! if not btc.is_snicker_tx(tx): log.error("Code error, created non-SNICKER tx, not signing.") sys.exit(EXIT_FAILURE) # sign all inputs # scripts: {input_index: (output_script, amount)} our_inputs = {} for index, ins in enumerate(tx.vin): utxo = (ins.prevout.hash[::-1], ins.prevout.n) script = utxo_dict[utxo]['script'] amount = utxo_dict[utxo]['value'] our_inputs[index] = (script, amount) success, msg = wallet_service.sign_tx(tx, our_inputs) if not success: log.error("Failed to sign transaction: " + msg) sys.exit(EXIT_FAILURE) # TODO condition on automatic brdcst or not if not jm_single().bc_interface.pushtx(tx.serialize()): # this represents an error about state (or conceivably, # an ultra-short window in which the spent utxo was # consumed in another transaction), but not really # an internal logic error, so we do NOT return False log.error("Failed to broadcast fake SNICKER coinjoin: " +\ bintohex(tx.GetTxid()[::-1])) log.info(btc.human_readable_transaction(tx)) sys.exit(EXIT_FAILURE) log.info("Successfully broadcast fake SNICKER coinjoin: " +\ bintohex(tx.GetTxid()[::-1]))
def main(): parser = OptionParser( usage= 'usage: %prog [options] walletname hex-tx input-index output-index net-transfer', description=description) add_base_options(parser) parser.add_option('-m', '--mixdepth', action='store', type='int', dest='mixdepth', help='mixdepth/account to spend from, default=0', default=0) parser.add_option('-g', '--gap-limit', action='store', type='int', dest='gaplimit', default=6, help='gap limit for Joinmarket wallet, default 6.') parser.add_option( '-n', '--no-upload', action='store_true', dest='no_upload', default=False, help="if set, we don't upload the new proposal to the servers") parser.add_option( '-f', '--txfee', action='store', type='int', dest='txfee', default=-1, help='Bitcoin miner tx_fee to use for transaction(s). A number higher ' 'than 1000 is used as "satoshi per KB" tx fee. A number lower than that ' 'uses the dynamic fee estimation of your blockchain provider as ' 'confirmation target. This temporarily overrides the "tx_fees" setting ' 'in your joinmarket.cfg. Works the same way as described in it. Check ' 'it for examples.') parser.add_option('-a', '--amtmixdepths', action='store', type='int', dest='amtmixdepths', help='number of mixdepths in wallet, default 5', default=5) (options, args) = parser.parse_args() snicker_plugin = JMPluginService("SNICKER") load_program_config(config_path=options.datadir, plugin_services=[snicker_plugin]) if len(args) != 5: jmprint("Invalid arguments, see --help") sys.exit(EXIT_ARGERROR) wallet_name, hextx, input_index, output_index, net_transfer = args input_index, output_index, net_transfer = [ int(x) for x in [input_index, output_index, net_transfer] ] check_regtest() # If tx_fees are set manually by CLI argument, override joinmarket.cfg: if int(options.txfee) > 0: jm_single().config.set("POLICY", "tx_fees", str(options.txfee)) max_mix_depth = max([options.mixdepth, options.amtmixdepths - 1]) wallet_path = get_wallet_path(wallet_name, None) wallet = open_test_wallet_maybe( wallet_path, wallet_name, max_mix_depth, wallet_password_stdin=options.wallet_password_stdin, gap_limit=options.gaplimit) wallet_service = WalletService(wallet) if wallet_service.rpc_error: sys.exit(EXIT_FAILURE) snicker_plugin.start_plugin_logging(wallet_service) # in this script, we need the wallet synced before # logic processing for some paths, so do it now: while not wallet_service.synced: wallet_service.sync_wallet(fast=not options.recoversync) # the sync call here will now be a no-op: wallet_service.startService() # now that the wallet is available, we can construct a proposal # before encrypting it: originating_tx = btc.CMutableTransaction.deserialize(hextobin(hextx)) txid1 = originating_tx.GetTxid()[::-1] # the proposer wallet needs to choose a single utxo, from his selected # mixdepth, that is bigger than the output amount of tx1 at the given # index. fee_est = estimate_tx_fee(2, 3, txtype=wallet_service.get_txtype()) amt_required = originating_tx.vout[output_index].nValue + fee_est prop_utxo_dict = wallet_service.select_utxos(options.mixdepth, amt_required) prop_utxos = list(prop_utxo_dict) prop_utxo_vals = [prop_utxo_dict[x] for x in prop_utxos] # get the private key for that utxo priv = wallet_service.get_key_from_addr( wallet_service.script_to_addr(prop_utxo_vals[0]['script'])) # construct the arguments for the snicker proposal: our_input_utxos = [ btc.CMutableTxOut(x['value'], x['script']) for x in prop_utxo_vals ] # destination must be a different mixdepth: prop_destn_spk = wallet_service.get_new_script( (options.mixdepth + 1) % (wallet_service.mixdepth + 1), 1) change_spk = wallet_service.get_new_script(options.mixdepth, 1) their_input = (txid1, output_index) # we also need to extract the pubkey of the chosen input from # the witness; we vary this depending on our wallet type: pubkey, msg = btc.extract_pubkey_from_witness(originating_tx, input_index) if not pubkey: log.error("Failed to extract pubkey from transaction: {}".format(msg)) sys.exit(EXIT_FAILURE) encrypted_proposal = wallet_service.create_snicker_proposal( prop_utxos, their_input, our_input_utxos, originating_tx.vout[output_index], net_transfer, fee_est, priv, pubkey, prop_destn_spk, change_spk, version_byte=1) + b"," + bintohex(pubkey).encode('utf-8') if options.no_upload: jmprint(encrypted_proposal.decode("utf-8")) sys.exit(EXIT_SUCCESS) nodaemon = jm_single().config.getint("DAEMON", "no_daemon") daemon = True if nodaemon == 1 else False snicker_client = SNICKERPostingClient([encrypted_proposal]) servers = jm_single().config.get("SNICKER", "servers").split(",") snicker_pf = SNICKERClientProtocolFactory(snicker_client, servers) start_reactor(jm_single().config.get("DAEMON", "daemon_host"), jm_single().config.getint("DAEMON", "daemon_port"), None, snickerfactory=snicker_pf, daemon=daemon)
def request_to_psbt(self, payment_psbt_base64, sender_parameters): """ Takes a payment psbt from a sender and their url parameters, and returns a new payment PSBT proposal, assuming all conditions are met. Returns: (False, errormsg, errortype) in case of failure. or: (True, base64_payjoin_psbt) in case of success. """ # we only support version 1; reject others: if not self.pj_version == int(sender_parameters[b'v'][0]): return (False, "This version of payjoin is not supported. ", "version-unsupported") try: payment_psbt = btc.PartiallySignedTransaction.from_base64( payment_psbt_base64) except: return (False, "invalid psbt format", "original-psbt-rejected") try: self.manager.set_payment_tx_and_psbt(payment_psbt) except Exception: # note that Assert errors, Value errors and CheckTransaction errors # are all possible, so we catch all exceptions to avoid a crash. return (False, "Proposed initial PSBT does not pass sanity checks.", "original-psbt-rejected") # if the sender set the additionalfeeoutputindex and maxadditionalfeecontribution # settings, pass them to the PayJoin manager: try: if b"additionalfeeoutputindex" in sender_parameters: afoi = int(sender_parameters[b"additionalfeeoutputindex"][0]) else: afoi = None if b"maxadditionalfeecontribution" in sender_parameters: mafc = int( sender_parameters[b"maxadditionalfeecontribution"][0]) else: mafc = None if b"minfeerate" in sender_parameters: minfeerate = float(sender_parameters[b"minfeerate"][0]) else: minfeerate = None except Exception as e: return (False, "Invalid request parameters.", "original-psbt-rejected") # if sender chose a fee output it must be the change output, # and the mafc will be applied to that. Any more complex transaction # structure is not supported. # If they did not choose a fee output index, we must rely on the feerate # reduction being not too much, which is checked against minfeerate; if # it is too big a reduction, again we fail payjoin. if (afoi is not None and mafc is None) or (mafc is not None and afoi is None): return (False, "Invalid request parameters.", "original-psbt-rejected") if afoi and not (self.manager.change_out_index == afoi): return (False, "additionalfeeoutputindex is " "not the change output. Joinmarket does " "not currently support this.", "original-psbt-rejected") # while we do not need to defend against probing attacks, # it is still safer to at least verify the validity of the signatures # at this stage, to ensure no misbehaviour with using inputs # that are not signed correctly: res = jm_single().bc_interface.testmempoolaccept( bintohex(self.manager.payment_tx.serialize())) if not res[0]["allowed"]: return (False, "Proposed transaction was " "rejected from mempool.", "original-psbt-rejected") # Now that the PSBT is accepted, we schedule fallback in case anything # fails later on in negotiation (as specified in BIP78): self.manager.timeout_fallback_dc = reactor.callLater( 60, fallback_nonpayjoin_broadcast, b"timeout", self.manager) receiver_utxos = self.manager.select_receiver_utxos() if not receiver_utxos: return (False, "Could not select coins for payjoin", "unavailable") # construct unsigned tx for payjoin-psbt: payjoin_tx_inputs = [(x.prevout.hash[::-1], x.prevout.n) for x in payment_psbt.unsigned_tx.vin] # See https://github.com/bitcoin/bips/blob/master/bip-0078.mediawiki#Protocol random_insert(payjoin_tx_inputs, receiver_utxos.keys()) pay_out = { "value": self.manager.pay_out.nValue, "address": str( btc.CCoinAddress.from_scriptPubKey( self.manager.pay_out.scriptPubKey)) } if self.manager.change_out: change_out = { "value": self.manager.change_out.nValue, "address": str( btc.CCoinAddress.from_scriptPubKey( self.manager.change_out.scriptPubKey)) } # we now know there were one/two outputs and know which is payment. # set the ordering of the outputs correctly. if change_out: # indices of original payment were set in JMPayjoinManager # sanity check: if self.manager.change_out_index == 0 and \ self.manager.pay_out_index == 1: outs = [change_out, pay_out] elif self.manager.change_out_index == 1 and \ self.manager.pay_out_index == 0: outs = [pay_out, change_out] else: assert False, "More than 2 outputs is not supported." else: outs = [pay_out] # bump payment output with our input: our_inputs_val = sum([v["value"] for _, v in receiver_utxos.items()]) pay_out["value"] += our_inputs_val log.debug("We bumped the payment output value by: " + str(our_inputs_val) + " sats.") log.debug("It is now: " + str(pay_out["value"]) + " sats.") # if the sender allowed a fee bump, we can apply it to the change output # now (we already checked it's the right index). # A note about checking `minfeerate`: it is impossible for the receiver # to be 100% certain on the size of the final transaction, since he does # not see in advance the (slightly) variable sizes of the sender's final # signatures; hence we do not attempt more than an estimate of the final # signed transaction's size and hence feerate. Very small inaccuracies # (< 1% typically) are possible, therefore. # # First, let's check that the user's requested minfeerate is not higher # than the feerate they already chose: if minfeerate and minfeerate > self.manager.get_payment_psbt_feerate(): return (False, "Bad request: minfeerate " "bigger than original psbt feerate.", "original-psbt-rejected") # set the intended virtual size of our input: vsize = self.manager.get_vsize_for_input() our_fee_bump = 0 if afoi: # We plan to reduce the change_out by a fee contribution. # Calculate the additional fee we think we need for our input, # to keep the same feerate as the original transaction (this also # accounts for rounding as per the BIP). # If it is more than mafc, then bump by mafc, else bump by the # calculated amount. # This should not meaningfully change the feerate. our_fee_bump = int(self.manager.get_payment_psbt_feerate() * vsize) if our_fee_bump > mafc: our_fee_bump = mafc elif minfeerate: # In this case the change_out will remain unchanged. # the user has not allowed a fee bump; calculate the new fee # rate; if it is lower than the limit, give up. expected_new_tx_size = self.manager.initial_psbt.extract_transaction( ).get_virtual_size() + vsize expected_new_fee_rate = self.manager.initial_psbt.get_fee() / ( expected_new_tx_size + vsize) if expected_new_fee_rate < minfeerate: return (False, "Bad request: we cannot " "achieve minfeerate requested.", "original-psbt-rejected") # Having checked the sender's conditions, we can apply the fee bump # intended: outs[self.manager.change_out_index]["value"] -= our_fee_bump unsigned_payjoin_tx = btc.mktx( payjoin_tx_inputs, outs, version=payment_psbt.unsigned_tx.nVersion, locktime=payment_psbt.unsigned_tx.nLockTime) # to create the PSBT we need the spent_outs for each input, # in the right order: spent_outs = [] for i, inp in enumerate(unsigned_payjoin_tx.vin): input_found = False for j, inp2 in enumerate(payment_psbt.unsigned_tx.vin): if inp.prevout == inp2.prevout: # this belongs to sender. # respect sender's sequence number choice, even # if they were not uniform: inp.nSequence = inp2.nSequence spent_outs.append(payment_psbt.inputs[j].utxo) input_found = True sender_index = i break if input_found: continue # if we got here this input is ours, we must find # it from our original utxo choice list: for ru in receiver_utxos.keys(): if (inp.prevout.hash[::-1], inp.prevout.n) == ru: spent_outs.append( self.wallet_service.witness_utxos_to_psbt_utxos( {ru: receiver_utxos[ru]})[0]) input_found = True break # there should be no other inputs: assert input_found # respect the sender's fixed sequence number, if it was used (we checked # in the initial sanity check) if self.manager.fixed_sequence_number: for inp in unsigned_payjoin_tx.vin: inp.nSequence = self.manager.fixed_sequence_number log.debug("We created this unsigned tx: ") log.debug(btc.human_readable_transaction(unsigned_payjoin_tx)) r_payjoin_psbt = self.wallet_service.create_psbt_from_tx( unsigned_payjoin_tx, spent_outs=spent_outs) log.debug("Receiver created payjoin PSBT:\n{}".format( self.wallet_service.human_readable_psbt(r_payjoin_psbt))) signresultandpsbt, err = self.wallet_service.sign_psbt( r_payjoin_psbt.serialize(), with_sign_result=True) assert not err, err signresult, receiver_signed_psbt = signresultandpsbt assert signresult.num_inputs_final == len(receiver_utxos) assert not signresult.is_final # with signing succcessful, remove the utxo field from the # counterparty's input (this is required by BIP78). Note we don't # do this on PSBT creation as the psbt signing code throws ValueError # unless utxos are present. receiver_signed_psbt.inputs[sender_index] = btc.PSBT_Input( index=sender_index) log.debug( "Receiver signing successful. Payjoin PSBT is now:\n{}".format( self.wallet_service.human_readable_psbt(receiver_signed_psbt))) # construct txoutset for the wallet service callback; we cannot use # txid as we don't have all signatures (TODO: ? but segwit only? even so, # works anyway). txinfo = tuple((x.scriptPubKey, x.nValue) for x in receiver_signed_psbt.unsigned_tx.vout) self.wallet_service.register_callbacks([self.end_receipt], txinfo=txinfo, cb_type="unconfirmed") return (True, receiver_signed_psbt.to_base64(), None)
def make_commitment(self): """The Taker default commitment function, which uses PoDLE. Alternative commitment types should use a different commit type byte. This will allow future upgrades to provide different style commitments by subclassing Taker and changing the commit_type_byte; existing makers will simply not accept this new type of commitment. In case of success, return the commitment and its opening. In case of failure returns (None, None) and constructs a detailed log for the user to read and discern the reason. """ def filter_by_coin_age_amt(utxos, age, amt): results = jm_single().bc_interface.query_utxo_set(utxos, includeconf=True) newresults = [] too_old = [] too_small = [] for i, r in enumerate(results): #results return "None" if txo is spent; drop this if not r: continue valid_age = r['confirms'] >= age valid_amt = r['value'] >= amt if not valid_age: too_old.append(utxos[i]) if not valid_amt: too_small.append(utxos[i]) if valid_age and valid_amt: newresults.append(utxos[i]) return newresults, too_old, too_small def priv_utxo_pairs_from_utxos(utxos, age, amt): #returns pairs list of (priv, utxo) for each valid utxo; #also returns lists "too_old" and "too_small" for any #utxos that did not satisfy the criteria for debugging. priv_utxo_pairs = [] new_utxos, too_old, too_small = filter_by_coin_age_amt( list(utxos.keys()), age, amt) new_utxos_dict = {k: v for k, v in utxos.items() if k in new_utxos} for k, v in new_utxos_dict.items(): addr = self.wallet_service.script_to_addr(v["script"]) priv = self.wallet_service.get_key_from_addr(addr) if priv: #can be null from create-unsigned priv_utxo_pairs.append((priv, k)) return priv_utxo_pairs, too_old, too_small commit_type_byte = "P" tries = jm_single().config.getint("POLICY", "taker_utxo_retries") age = jm_single().config.getint("POLICY", "taker_utxo_age") #Minor rounding errors don't matter here amt = int( self.cjamount * jm_single().config.getint("POLICY", "taker_utxo_amtpercent") / 100.0) priv_utxo_pairs, to, ts = priv_utxo_pairs_from_utxos( self.input_utxos, age, amt) #For podle data format see: podle.PoDLE.reveal() #In first round try, don't use external commitments podle_data = generate_podle(priv_utxo_pairs, tries) if not podle_data: #Pre-filter the set of external commitments that work for this #transaction according to its size and age. dummy, extdict = get_podle_commitments() if len(extdict) > 0: ext_valid, ext_to, ext_ts = filter_by_coin_age_amt( list(extdict.keys()), age, amt) else: ext_valid = None #We defer to a second round to try *all* utxos in spending mixdepth; #this is because it's much cleaner to use the utxos involved #in the transaction, about to be consumed, rather than use #random utxos that will persist after. At this step we also #allow use of external utxos in the json file. mixdepth_utxos = self.wallet_service.get_utxos_by_mixdepth()[ self.mixdepth] if len(self.input_utxos) == len(mixdepth_utxos): # Already tried the whole mixdepth podle_data = generate_podle([], tries, ext_valid) else: priv_utxo_pairs, to, ts = priv_utxo_pairs_from_utxos( mixdepth_utxos, age, amt) podle_data = generate_podle(priv_utxo_pairs, tries, ext_valid) if podle_data: jlog.debug("Generated PoDLE: " + repr(podle_data)) return (commit_type_byte + bintohex(podle_data.commitment), podle_data.serialize_revelation(), "Commitment sourced OK") else: errmsgheader, errmsg = generate_podle_error_string( priv_utxo_pairs, to, ts, self.wallet_service, self.cjamount, jm_single().config.get("POLICY", "taker_utxo_age"), jm_single().config.get("POLICY", "taker_utxo_amtpercent")) with open("commitments_debug.txt", "wb") as f: errmsgfileheader = (b"THIS IS A TEMPORARY FILE FOR DEBUGGING; " b"IT CAN BE SAFELY DELETED ANY TIME.\n") errmsgfileheader += (b"***\n") f.write(errmsgfileheader + errmsg.encode('utf-8')) return (None, (priv_utxo_pairs, to, ts), errmsgheader + errmsg)