You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
657 lines
31 KiB
657 lines
31 KiB
#! /usr/bin/env python |
|
from future.utils import iteritems |
|
import base64 |
|
import pprint |
|
import random |
|
import sys |
|
import abc |
|
from binascii import unhexlify |
|
|
|
|
|
from jmbitcoin import SerializationError, SerializationTruncationError |
|
import jmbitcoin as btc |
|
from jmclient.wallet import estimate_tx_fee, compute_tx_locktime |
|
from jmclient.wallet_service import WalletService |
|
from jmclient.configure import jm_single |
|
from jmbase.support import get_log, EXIT_SUCCESS, EXIT_FAILURE |
|
from jmclient.support import calc_cj_fee, select_one_utxo |
|
from jmclient.podle import verify_podle, PoDLE, PoDLEError |
|
from twisted.internet import task, reactor |
|
from .cryptoengine import EngineError |
|
|
|
jlog = get_log() |
|
|
|
class Maker(object): |
|
def __init__(self, wallet_service): |
|
self.active_orders = {} |
|
assert isinstance(wallet_service, WalletService) |
|
self.wallet_service = wallet_service |
|
self.nextoid = -1 |
|
self.offerlist = None |
|
self.sync_wait_loop = task.LoopingCall(self.try_to_create_my_orders) |
|
self.sync_wait_loop.start(2.0) |
|
self.aborted = False |
|
|
|
def try_to_create_my_orders(self): |
|
"""Because wallet syncing is not synchronous(!), |
|
we cannot calculate our offers until we know the wallet |
|
contents, so poll until BlockchainInterface.wallet_synced |
|
is flagged as True. TODO: Use a deferred, probably. |
|
Note that create_my_orders() is defined by subclasses. |
|
""" |
|
if not self.wallet_service.synced: |
|
return |
|
self.offerlist = self.create_my_orders() |
|
self.sync_wait_loop.stop() |
|
if not self.offerlist: |
|
jlog.info("Failed to create offers, giving up.") |
|
sys.exit(EXIT_FAILURE) |
|
jlog.info('offerlist={}'.format(self.offerlist)) |
|
|
|
def on_auth_received(self, nick, offer, commitment, cr, amount, kphex): |
|
"""Receives data on proposed transaction offer from daemon, verifies |
|
commitment, returns necessary data to send ioauth message (utxos etc) |
|
""" |
|
#check the validity of the proof of discrete log equivalence |
|
tries = jm_single().config.getint("POLICY", "taker_utxo_retries") |
|
def reject(msg): |
|
jlog.info("Counterparty commitment not accepted, reason: " + msg) |
|
return (False,) |
|
|
|
# deserialize the commitment revelation |
|
try: |
|
cr_dict = PoDLE.deserialize_revelation(cr) |
|
except PoDLEError as e: |
|
reason = repr(e) |
|
return reject(reason) |
|
|
|
if not verify_podle(str(cr_dict['P']), str(cr_dict['P2']), str(cr_dict['sig']), |
|
str(cr_dict['e']), str(commitment), |
|
index_range=range(tries)): |
|
reason = "verify_podle failed" |
|
return reject(reason) |
|
#finally, check that the proffered utxo is real, old enough, large enough, |
|
#and corresponds to the pubkey |
|
res = jm_single().bc_interface.query_utxo_set([cr_dict['utxo']], |
|
includeconf=True) |
|
if len(res) != 1 or not res[0]: |
|
reason = "authorizing utxo is not valid" |
|
return reject(reason) |
|
age = jm_single().config.getint("POLICY", "taker_utxo_age") |
|
if res[0]['confirms'] < age: |
|
reason = "commitment utxo not old enough: " + str(res[0]['confirms']) |
|
return reject(reason) |
|
reqd_amt = int(amount * jm_single().config.getint( |
|
"POLICY", "taker_utxo_amtpercent") / 100.0) |
|
if res[0]['value'] < reqd_amt: |
|
reason = "commitment utxo too small: " + str(res[0]['value']) |
|
return reject(reason) |
|
|
|
try: |
|
if not self.wallet_service.pubkey_has_script( |
|
unhexlify(cr_dict['P']), unhexlify(res[0]['script'])): |
|
raise EngineError() |
|
except EngineError: |
|
reason = "Invalid podle pubkey: " + str(cr_dict['P']) |
|
return reject(reason) |
|
|
|
# authorisation of taker passed |
|
#Find utxos for the transaction now: |
|
utxos, cj_addr, change_addr = self.oid_to_order(offer, amount) |
|
if not utxos: |
|
#could not find funds |
|
return (False,) |
|
# for index update persistence: |
|
self.wallet_service.save_wallet() |
|
# Construct data for auth request back to taker. |
|
# Need to choose an input utxo pubkey to sign with |
|
# (no longer using the coinjoin pubkey from 0.2.0) |
|
# Just choose the first utxo in self.utxos and retrieve key from wallet. |
|
auth_address = utxos[list(utxos.keys())[0]]['address'] |
|
auth_key = self.wallet_service.get_key_from_addr(auth_address) |
|
auth_pub = btc.privtopub(auth_key) |
|
btc_sig = btc.ecdsa_sign(kphex, auth_key) |
|
return (True, utxos, auth_pub, cj_addr, change_addr, btc_sig) |
|
|
|
def on_tx_received(self, nick, txhex, 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()). |
|
""" |
|
try: |
|
tx = btc.deserialize(txhex) |
|
except (IndexError, SerializationError, SerializationTruncationError) as e: |
|
return (False, 'malformed txhex. ' + repr(e)) |
|
jlog.info('obtained tx\n' + pprint.pformat(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['ins']): |
|
utxo = ins['outpoint']['hash'] + ':' + str(ins['outpoint']['index']) |
|
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) |
|
|
|
txs = self.wallet_service.sign_tx(btc.deserialize(unhexlify(txhex)), our_inputs) |
|
for index in our_inputs: |
|
sigmsg = unhexlify(txs['ins'][index]['script']) |
|
if 'txinwitness' in txs['ins'][index]: |
|
# Note that this flag only implies that the transaction |
|
# *as a whole* is using segwit serialization; it doesn't |
|
# imply that this specific input is segwit type (to be |
|
# fully general, we allow that even our own wallet's |
|
# inputs might be of mixed type). So, we catch the EngineError |
|
# which is thrown by non-segwit types. This way the sigmsg |
|
# will only contain the scriptSig field if the wallet object |
|
# decides it's necessary/appropriate for this specific input |
|
# If it is segwit, we prepend the witness data since we want |
|
# (sig, pub, witnessprogram=scriptSig - note we could, better, |
|
# pass scriptCode here, but that is not backwards compatible, |
|
# as the taker uses this third field and inserts it into the |
|
# transaction scriptSig), else (non-sw) the !sig message remains |
|
# unchanged as (sig, pub). |
|
try: |
|
scriptSig = btc.pubkey_to_p2wpkh_script(txs['ins'][index]['txinwitness'][1]) |
|
sigmsg = b''.join(btc.serialize_script_unit( |
|
x) for x in txs['ins'][index]['txinwitness'] + [scriptSig]) |
|
except IndexError: |
|
#the sigmsg was already set before the segwit check |
|
pass |
|
sigs.append(base64.b64encode(sigmsg).decode('ascii')) |
|
return (True, sigs) |
|
|
|
def verify_unsigned_tx(self, txd, offerinfo): |
|
"""This code is security-critical. |
|
Before signing the transaction the Maker must ensure |
|
that all details are as expected, and most importantly |
|
that it receives the exact number of coins to expected |
|
in total. The data is taken from the offerinfo dict and |
|
compared with the serialized txhex. |
|
""" |
|
tx_utxo_set = set(ins['outpoint']['hash'] + ':' + str( |
|
ins['outpoint']['index']) for ins in txd['ins']) |
|
|
|
utxos = offerinfo["utxos"] |
|
cjaddr = offerinfo["cjaddr"] |
|
cjaddr_script = btc.address_to_script(cjaddr) |
|
changeaddr = offerinfo["changeaddr"] |
|
changeaddr_script = btc.address_to_script(changeaddr) |
|
#Note: this value is under the control of the Taker, |
|
#see comment below. |
|
amount = offerinfo["amount"] |
|
cjfee = offerinfo["offer"]["cjfee"] |
|
txfee = offerinfo["offer"]["txfee"] |
|
ordertype = offerinfo["offer"]["ordertype"] |
|
my_utxo_set = set(utxos.keys()) |
|
if not tx_utxo_set.issuperset(my_utxo_set): |
|
return (False, 'my utxos are not contained') |
|
|
|
#The three lines below ensure that the Maker receives |
|
#back what he puts in, minus his bitcointxfee contribution, |
|
#plus his expected fee. These values are fully under |
|
#Maker control so no combination of messages from the Taker |
|
#can change them. |
|
#(mathematically: amount + expected_change_value is independent |
|
#of amount); there is not a (known) way for an attacker to |
|
#alter the amount (note: !fill resubmissions *overwrite* |
|
#the active_orders[dict] entry in daemon), but this is an |
|
#extra layer of safety. |
|
my_total_in = sum([va['value'] for va in utxos.values()]) |
|
real_cjfee = calc_cj_fee(ordertype, cjfee, amount) |
|
expected_change_value = (my_total_in - amount - txfee + real_cjfee) |
|
jlog.info('potentially earned = {}'.format(real_cjfee - txfee)) |
|
jlog.info('mycjaddr, mychange = {}, {}'.format(cjaddr, changeaddr)) |
|
|
|
#The remaining checks are needed to ensure |
|
#that the coinjoin and change addresses occur |
|
#exactly once with the required amts, in the output. |
|
times_seen_cj_addr = 0 |
|
times_seen_change_addr = 0 |
|
for outs in txd['outs']: |
|
if outs['script'] == cjaddr_script: |
|
times_seen_cj_addr += 1 |
|
if outs['value'] != amount: |
|
return (False, 'Wrong cj_amount. I expect ' + str(amount)) |
|
if outs['script'] == changeaddr_script: |
|
times_seen_change_addr += 1 |
|
if outs['value'] != expected_change_value: |
|
return (False, 'wrong change, i expect ' + str( |
|
expected_change_value)) |
|
if times_seen_cj_addr != 1 or times_seen_change_addr != 1: |
|
fmt = ('cj or change addr not in tx ' |
|
'outputs once, #cjaddr={}, #chaddr={}').format |
|
return (False, (fmt(times_seen_cj_addr, times_seen_change_addr))) |
|
return (True, None) |
|
|
|
def modify_orders(self, to_cancel, to_announce): |
|
"""This code is called on unconfirm and confirm callbacks, |
|
and replaces existing orders with new ones, or just cancels |
|
old ones. |
|
""" |
|
jlog.info('modifying orders. to_cancel={}\nto_announce={}'.format( |
|
to_cancel, to_announce)) |
|
for oid in to_cancel: |
|
order = [o for o in self.offerlist if o['oid'] == oid] |
|
if len(order) == 0: |
|
fmt = 'didnt cancel order which doesnt exist, oid={}'.format |
|
jlog.info(fmt(oid)) |
|
self.offerlist.remove(order[0]) |
|
if len(to_announce) > 0: |
|
for ann in to_announce: |
|
oldorder_s = [o for o in self.offerlist |
|
if o['oid'] == ann['oid']] |
|
if len(oldorder_s) > 0: |
|
self.offerlist.remove(oldorder_s[0]) |
|
self.offerlist += to_announce |
|
|
|
@abc.abstractmethod |
|
def create_my_orders(self): |
|
"""Must generate a set of orders to be displayed |
|
according to the contents of the wallet + some algo. |
|
(Note: should be called "create_my_offers") |
|
""" |
|
|
|
@abc.abstractmethod |
|
def oid_to_order(self, cjorder, oid, amount): |
|
"""Must convert an order with an offer/order id |
|
into a set of utxos to fill the order. |
|
Also provides the output addresses for the Taker. |
|
""" |
|
|
|
@abc.abstractmethod |
|
def on_tx_unconfirmed(self, cjorder, txid): |
|
"""Performs action on receipt of transaction into the |
|
mempool in the blockchain instance (e.g. announcing orders) |
|
""" |
|
|
|
@abc.abstractmethod |
|
def on_tx_confirmed(self, cjorder, txid, confirmations): |
|
"""Performs actions on receipt of 1st confirmation of |
|
a transaction into a block (e.g. announce orders) |
|
""" |
|
|
|
class P2EPMaker(Maker): |
|
""" The P2EP Maker object is instantiated for a specific payment, |
|
with a specific address and expected payment amount. It inherits |
|
normal Maker behaviour on startup and makes fake offers, which |
|
it does not follow up in direct peer interaction (to be specific: |
|
`!fill` requests in privmsg are simply ignored). Under the hood, |
|
the daemon protocol will allow pubkey exchange with any counterparty, |
|
but only after the Taker makes a !tx proposal matching our intended |
|
address and payment amount, which were agreed out of band with the |
|
sender(Taker) counterparty, do we pass over our intended inputs |
|
and partially signed transaction, thus information leak to snoopers |
|
is not possible. |
|
""" |
|
def __init__(self, wallet_service, mixdepth, amount): |
|
super(P2EPMaker, self).__init__(wallet_service) |
|
self.receiving_amount = amount |
|
self.mixdepth = mixdepth |
|
# destination mixdepth must be different from that |
|
# which we source coins from; use the standard "next" |
|
dest_mixdepth = (self.mixdepth + 1) % self.wallet_service.max_mixdepth |
|
# Select an unused destination in the external branch |
|
self.destination_addr = self.wallet_service.get_external_addr( |
|
dest_mixdepth) |
|
# Callback to request user permission (for e.g. GUI) |
|
# args: (1) message, as string |
|
# returns: True or False |
|
self.user_check = self.default_user_check |
|
self.user_info = self.default_user_info_callback |
|
|
|
def default_user_check(self, message): |
|
if input(message) == 'y': |
|
return True |
|
return False |
|
|
|
def default_user_info_callback(self, message): |
|
""" TODO this is basically the same function |
|
as taker_info_callback (currently used for GUI); |
|
fold this and some other convenience functions together |
|
and use a root CJPeer class in jmbase to avoid code |
|
duplication. |
|
""" |
|
jlog.info(message) |
|
|
|
def inform_user_details(self): |
|
self.user_info("Your receiving address is: " + self.destination_addr) |
|
self.user_info("You will receive amount: " + str( |
|
self.receiving_amount) + " satoshis.") |
|
self.user_info("The sender also needs to know your ephemeral " |
|
"nickname: " + jm_single().nickname) |
|
receive_uri = btc.encode_bip21_uri(self.destination_addr, { |
|
'amount': btc.sat_to_btc(self.receiving_amount), |
|
'jmnick': jm_single().nickname |
|
}) |
|
self.user_info("Receive URI: " + receive_uri) |
|
self.user_info("This information has also been stored in a file payjoin.txt;" |
|
" send it to your counterparty when you are ready.") |
|
with open("payjoin.txt", "w") as f: |
|
f.write("Payjoin transfer details:\n\n") |
|
f.write("Receive URI: " + receive_uri + "\n") |
|
f.write("Address: " + self.destination_addr + "\n") |
|
f.write("Amount (in sats): " + str(self.receiving_amount) + "\n") |
|
f.write("Receiver nick: " + jm_single().nickname + "\n") |
|
if not self.user_check("Enter 'y' to wait for the payment:"): |
|
sys.exit(EXIT_SUCCESS) |
|
|
|
def create_my_orders(self): |
|
""" Fake offer for public consumption. |
|
Requests to fill will be ignored. |
|
""" |
|
ordertype = random.choice(("swreloffer", "swabsoffer")) |
|
minsize = random.randint(100000, 10000000) |
|
maxsize = random.randint(100000, 1000000000) + minsize |
|
txfee = random.randint(0, 1000) |
|
if ordertype == "swreloffer": |
|
cjfee = str(random.randint(0, 100000)/100000000.0) |
|
else: |
|
cjfee = random.randint(0, 10000) |
|
order = {'oid': 0, |
|
'ordertype': ordertype, |
|
'minsize': minsize, |
|
'maxsize': maxsize, |
|
'txfee': txfee, |
|
'cjfee': cjfee} |
|
|
|
# sanity check |
|
assert order['minsize'] >= 0 |
|
assert order['maxsize'] > 0 |
|
if order['minsize'] > order['maxsize']: |
|
jlog.info('minsize (' + str(order['minsize']) + ') > maxsize (' + str( |
|
order['maxsize']) + ')') |
|
return [] |
|
|
|
return [order] |
|
|
|
def oid_to_order(self, offer, amount): |
|
# unreachable; only here to satisy abc. |
|
pass |
|
|
|
def on_tx_unconfirmed(self, txd, txid): |
|
""" For P2EP Maker there is no "offer", so |
|
the second argument is repurposed as the deserialized |
|
transaction. |
|
""" |
|
self.user_info("The transaction has been broadcast.") |
|
self.user_info("Txid is: " + txid) |
|
self.user_info("Transaction in detail: " + pprint.pformat(txd)) |
|
self.user_info("shutting down.") |
|
reactor.stop() |
|
|
|
def on_tx_confirmed(self, txd, txid, confirmations): |
|
# will not be reached except in testing |
|
self.on_tx_unconfirmed(txd, txid) |
|
|
|
def on_tx_received(self, nick, txhex): |
|
""" Called when the sender-counterparty has sent a transaction proposal. |
|
1. First we check for the expected destination and amount (this is |
|
sufficient to identify our cp, as this info was presumably passed |
|
out of band, as for any normal payment). |
|
2. Then we verify the validity of the proposed non-coinjoin |
|
transaction; if not, reject, otherwise store this as a |
|
fallback transaction in case the protocol doesn't complete. |
|
3. Next, we select utxos from our wallet, to add into the |
|
payment transaction as input. Try to select so as to not |
|
trigger the UIH2 condition, but continue (and inform user) |
|
even if we can't (if we can't select any coins, broadcast the |
|
non-coinjoin payment, if the user agrees). |
|
Proceeding with payjoin: |
|
4. We update the output amount at the destination address. |
|
5. We modify the change amount in the original proposal (which |
|
will be the only other output other than the destination), |
|
reducing it to account for the increased transaction fee |
|
caused by our additional proposed input(s). |
|
6. Finally we sign our own input utxo(s) and re-serialize the |
|
tx, allowing it to be sent back to the counterparty. |
|
7. If the transaction is not fully signed and broadcast within |
|
the time unconfirm_timeout_sec as specified in the joinmarket.cfg, |
|
we broadcast the non-coinjoin fallback tx instead. |
|
""" |
|
try: |
|
tx = btc.deserialize(txhex) |
|
except (IndexError, SerializationError, SerializationTruncationError) as e: |
|
return (False, 'malformed txhex. ' + repr(e)) |
|
self.user_info('obtained proposed fallback (non-coinjoin) ' +\ |
|
'transaction from sender:\n' + pprint.pformat(tx)) |
|
|
|
if len(tx["outs"]) != 2: |
|
return (False, "Transaction has more than 2 outputs; not supported.") |
|
dest_found = False |
|
destination_index = -1 |
|
change_index = -1 |
|
proposed_change_value = 0 |
|
for index, out in enumerate(tx["outs"]): |
|
if out["script"] == btc.address_to_script(self.destination_addr): |
|
# we found the expected destination; is the amount correct? |
|
if not out["value"] == self.receiving_amount: |
|
return (False, "Wrong payout value in proposal from sender.") |
|
dest_found = True |
|
destination_index = index |
|
else: |
|
change_found = True |
|
proposed_change_out = out["script"] |
|
proposed_change_value = out["value"] |
|
change_index = index |
|
|
|
if not dest_found: |
|
return (False, "Our expected destination address was not found.") |
|
|
|
# Verify valid input utxos provided and check their value. |
|
# batch retrieval of utxo data |
|
utxo = {} |
|
ctr = 0 |
|
for index, ins in enumerate(tx['ins']): |
|
utxo_for_checking = ins['outpoint']['hash'] + ':' + str(ins[ |
|
'outpoint']['index']) |
|
utxo[ctr] = [index, utxo_for_checking] |
|
ctr += 1 |
|
|
|
utxo_data = jm_single().bc_interface.query_utxo_set( |
|
[x[1] for x in utxo.values()]) |
|
|
|
total_sender_input = 0 |
|
for i, u in iteritems(utxo): |
|
if utxo_data[i] is None: |
|
return (False, "Proposed transaction contains invalid utxos") |
|
total_sender_input += utxo_data[i]["value"] |
|
|
|
# Check that the transaction *as proposed* balances; check that the |
|
# included fee is within 0.3-3x our own current estimates, if not user |
|
# must decide. |
|
btc_fee = total_sender_input - self.receiving_amount - proposed_change_value |
|
self.user_info("Network transaction fee of fallback tx is: " + str( |
|
btc_fee) + " satoshis.") |
|
fee_est = estimate_tx_fee(len(tx['ins']), len(tx['outs']), |
|
txtype=self.wallet_service.get_txtype()) |
|
fee_ok = False |
|
if btc_fee > 0.3 * fee_est and btc_fee < 3 * fee_est: |
|
fee_ok = True |
|
else: |
|
if self.user_check("Is this transaction fee acceptable? (y/n):"): |
|
fee_ok = True |
|
if not fee_ok: |
|
return (False, |
|
"Proposed transaction fee not accepted due to tx fee: " + str( |
|
btc_fee)) |
|
|
|
# This direct rpc call currently assumes Core 0.17, so not using now. |
|
# It has the advantage of (a) being simpler and (b) allowing for any |
|
# non standard coins. |
|
# |
|
#res = jm_single().bc_interface.rpc('testmempoolaccept', [txhex]) |
|
#print("Got this result from rpc call: ", res) |
|
#if not res["accepted"]: |
|
# return (False, "Proposed transaction was rejected from mempool.") |
|
|
|
# Manual verification of the transaction signatures. Passing this |
|
# test does imply that the transaction is valid (unless there is |
|
# a double spend during the process), but is restricted to standard |
|
# types: p2pkh, p2wpkh, p2sh-p2wpkh only. Double spend is not counted |
|
# as a risk as this is a payment. |
|
for i, u in iteritems(utxo): |
|
if "txinwitness" in tx["ins"][u[0]]: |
|
ver_amt = utxo_data[i]["value"] |
|
try: |
|
ver_sig, ver_pub = tx["ins"][u[0]]["txinwitness"] |
|
except Exception as e: |
|
self.user_info("Segwit error: " + repr(e)) |
|
return (False, "Segwit input not of expected type, " |
|
"either p2sh-p2wpkh or p2wpkh") |
|
# note that the scriptCode is the same whether nested or not |
|
# also note that the scriptCode has to be inferred if we are |
|
# only given a transaction serialization. |
|
scriptCode = "76a914" + btc.hash160(unhexlify(ver_pub)) + "88ac" |
|
else: |
|
scriptCode = None |
|
ver_amt = None |
|
scriptSig = btc.deserialize_script(tx["ins"][u[0]]["script"]) |
|
if len(scriptSig) != 2: |
|
return (False, |
|
"Proposed transaction contains unsupported input type") |
|
ver_sig, ver_pub = scriptSig |
|
if not btc.verify_tx_input(txhex, u[0], |
|
utxo_data[i]['script'], |
|
ver_sig, ver_pub, |
|
scriptCode=scriptCode, |
|
amount=ver_amt): |
|
return (False, "Proposed transaction is not correctly signed.") |
|
|
|
# At this point we are satisfied with the proposal. Record the fallback |
|
# in case the sender disappears and the payjoin tx doesn't happen: |
|
self.user_info("We'll use this serialized transaction to broadcast if your" |
|
" counterparty fails to broadcast the payjoin version:") |
|
self.user_info(txhex) |
|
# Keep a local copy for broadcast fallback: |
|
self.fallback_tx = txhex |
|
|
|
# Now we add our own inputs: |
|
# See the gist comment here: |
|
# https://gist.github.com/AdamISZ/4551b947789d3216bacfcb7af25e029e#gistcomment-2799709 |
|
# which sets out the decision Bob must make. |
|
# In cases where Bob can add any amount, he selects one utxo |
|
# to keep it simple. |
|
# In cases where he must choose at least X, he selects one utxo |
|
# which provides X if possible, otherwise defaults to a normal |
|
# selection algorithm. |
|
# In those cases where he must choose X but X is unavailable, |
|
# he selects all coins, and proceeds anyway with payjoin, since |
|
# it has other advantages (CIOH and utxo defrag). |
|
my_utxos = {} |
|
largest_out = max(self.receiving_amount, proposed_change_value) |
|
max_sender_amt = max([u['value'] for u in utxo_data]) |
|
not_uih2 = False |
|
if max_sender_amt < largest_out: |
|
# just select one coin. |
|
# have some reasonable lower limit but otherwise choose |
|
# randomly; note that this is actually a great way of |
|
# sweeping dust ... |
|
self.user_info("Choosing one coin at random") |
|
try: |
|
my_utxos = self.wallet_service.select_utxos( |
|
self.mixdepth, jm_single().DUST_THRESHOLD, |
|
select_fn=select_one_utxo) |
|
except: |
|
return self.no_coins_fallback() |
|
not_uih2 = True |
|
else: |
|
# get an approximate required amount assuming 4 inputs, which is |
|
# fairly conservative (but guess by necessity). |
|
fee_for_select = estimate_tx_fee(len(tx['ins']) + 4, 2, |
|
txtype=self.wallet_service.get_txtype()) |
|
approx_sum = max_sender_amt - self.receiving_amount + fee_for_select |
|
try: |
|
my_utxos = self.wallet_service.select_utxos(self.mixdepth, approx_sum) |
|
not_uih2 = True |
|
except Exception: |
|
# TODO probably not logical to always sweep here. |
|
self.user_info("Sweeping all coins in this mixdepth.") |
|
my_utxos = self.wallet_service.get_utxos_by_mixdepth()[self.mixdepth] |
|
if my_utxos == {}: |
|
return self.no_coins_fallback() |
|
if not_uih2: |
|
self.user_info("The proposed tx does not trigger UIH2, which " |
|
"means it is indistinguishable from a normal " |
|
"payment. This is the ideal case. Continuing..") |
|
else: |
|
self.user_info("The proposed tx does trigger UIH2, which it makes " |
|
"it somewhat distinguishable from a normal payment," |
|
" but proceeding with payjoin..") |
|
|
|
my_total_in = sum([va['value'] for va in my_utxos.values()]) |
|
self.user_info("We selected inputs worth: " + str(my_total_in)) |
|
# adjust the output amount at the destination based on our contribution |
|
new_destination_amount = self.receiving_amount + my_total_in |
|
# estimate the required fee for the new version of the transaction |
|
total_ins = len(tx["ins"]) + len(my_utxos.keys()) |
|
est_fee = estimate_tx_fee(total_ins, 2, txtype=self.wallet_service.get_txtype()) |
|
self.user_info("We estimated a fee of: " + str(est_fee)) |
|
new_change_amount = total_sender_input + my_total_in - \ |
|
new_destination_amount - est_fee |
|
self.user_info("We calculated a new change amount of: " + str(new_change_amount)) |
|
self.user_info("We calculated a new destination amount of: " + str(new_destination_amount)) |
|
# now reconstruct the transaction with the new inputs and the |
|
# amount-changed outputs |
|
new_outs = [{"address": self.destination_addr, |
|
"value": new_destination_amount}] |
|
if new_change_amount >= jm_single().BITCOIN_DUST_THRESHOLD: |
|
new_outs.append({"script": proposed_change_out, |
|
"value": new_change_amount}) |
|
new_ins = [x[1] for x in utxo.values()] |
|
new_ins.extend(my_utxos.keys()) |
|
new_tx = btc.make_shuffled_tx(new_ins, new_outs, False, 2, compute_tx_locktime()) |
|
new_tx_deser = btc.deserialize(new_tx) |
|
|
|
# sign our inputs before transfer |
|
our_inputs = {} |
|
for index, ins in enumerate(new_tx_deser['ins']): |
|
utxo = ins['outpoint']['hash'] + ':' + str(ins['outpoint']['index']) |
|
if utxo not in my_utxos: |
|
continue |
|
script = self.wallet_service.addr_to_script(my_utxos[utxo]['address']) |
|
amount = my_utxos[utxo]['value'] |
|
our_inputs[index] = (script, amount) |
|
|
|
txs = self.wallet_service.sign_tx(btc.deserialize(new_tx), our_inputs) |
|
txinfo = tuple((x["script"], x["value"]) for x in txs["outs"]) |
|
self.wallet_service.register_callbacks([self.on_tx_unconfirmed], txinfo, "unconfirmed") |
|
self.wallet_service.register_callbacks([self.on_tx_confirmed], txinfo, "confirmed") |
|
# The blockchain interface just abandons monitoring if the transaction |
|
# is not broadcast before the configured timeout; we want to take |
|
# action in this case, so we add an additional callback to the reactor: |
|
reactor.callLater(jm_single().config.getint("TIMEOUT", |
|
"unconfirm_timeout_sec"), self.broadcast_fallback) |
|
return (True, nick, btc.serialize(txs)) |
|
|
|
def no_coins_fallback(self): |
|
""" Broadcast, optionally, the fallback non-coinjoin transaction |
|
because we were not able to select coins to contribute. |
|
""" |
|
self.user_info("Unable to select any coins; this mixdepth is empty.") |
|
if self.user_check("Would you like to broadcast the non-coinjoin payment?"): |
|
self.broadcast_fallback() |
|
return (False, "Coinjoin unsuccessful, fallback attempted.") |
|
else: |
|
self.user_info("You chose not to broadcast; the payment has NOT been made.") |
|
return (False, "No transaction made.") |
|
|
|
def broadcast_fallback(self): |
|
self.user_info("Broadcasting non-coinjoin fallback transaction.") |
|
txid = btc.txhash(self.fallback_tx) |
|
success = jm_single().bc_interface.pushtx(self.fallback_tx) |
|
if not success: |
|
self.user_info("ERROR: the fallback transaction did not broadcast. " |
|
"The payment has NOT been made.") |
|
else: |
|
self.user_info("Payment received successfully, but it was NOT a coinjoin.") |
|
self.user_info("Txid: " + txid) |
|
reactor.stop()
|
|
|