Exemple #1
0
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))
Exemple #2
0
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}
Exemple #5
0
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"])})
Exemple #7
0
 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
Exemple #8
0
    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}
Exemple #11
0
 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'))
Exemple #14
0
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
Exemple #15
0
    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')
Exemple #17
0
 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
Exemple #20
0
 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
Exemple #21
0
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
Exemple #25
0
 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]))
Exemple #28
0
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)
Exemple #29
0
    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)
Exemple #30
0
    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)