#!/usr/bin/env python2 from __future__ import print_function """ Tool to create or receive a single input, output pair signed single|acp to allow the receiver to add further inputs and outputs and broadcast a full transaction. Thus the original input can be tainted and the taint can spread to other outputs included. This is a tool for Joinmarket wallets specifically. """ import binascii from optparse import OptionParser from pprint import pformat import jmbitcoin as btc from jmclient import ( load_program_config, validate_address, jm_single, WalletError, sync_wallet, RegtestBitcoinCoreInterface, estimate_tx_fee, get_p2pk_vbyte, get_p2sh_vbyte, open_test_wallet_maybe, get_wallet_path) def get_parser(): parser = OptionParser( usage= 'usage: %prog [options] walletfile make utxo\n' + \ 'or: %prog [options] walletfile take amount-in-satoshis destination-addr txhex', description='Makes a single|acp signed in-out pair for giving to others ' + 'or receives such a pair and creates a full transaction with your utxos. ' + 'Primarily useful for spreading utxo taint.') parser.add_option( '-b', '--bump', type='int', dest='bump', default=10000, help= 'How much bigger the output is than the input, when making a single|acp in-out pair.') parser.add_option( '-m', '--mixdepth', type='int', dest='mixdepth', help= 'Mixing depth to source utxo from. default=0.', default=0) parser.add_option('-a', '--amtmixdepths', action='store', type='int', dest='amtmixdepths', help='number of mixdepths in wallet, default 5', default=5) parser.add_option('-g', '--gap-limit', type="int", action='store', dest='gaplimit', help='gap limit for wallet, default=6', default=6) parser.add_option('--fast', action='store_true', dest='fastsync', default=False, help=('choose to do fast wallet sync, only for Core and ' 'only for previously synced wallet')) return parser def is_utxo(utxo): try: txid, N = utxo.split(":") assert len(txid) == 64 N = int(N) except: return False return True def cli_get_wallet(wallet_name, sync=True): wallet_path = get_wallet_path(wallet_name, None) wallet = open_test_wallet_maybe( wallet_path, wallet_name, options.amtmixdepths, gap_limit=options.gaplimit) if jm_single().config.get("BLOCKCHAIN", "blockchain_source") == "electrum-server": jm_single().bc_interface.synctype = "with-script" if sync: while not jm_single().bc_interface.wallet_synced: sync_wallet(wallet, fast=options.fastsync) return wallet #======Electrum specific utils========================= def rev_hex(s): return s.decode('hex')[::-1].encode('hex') def int_to_hex(i, length=1): s = hex(i)[2:].rstrip('L') s = "0"*(2*length - len(s)) + s return rev_hex(s) def serialize_derivation(roc, i): x = ''.join(map(lambda x: int_to_hex(x, 2), (roc, i))) print("returning: ", x) raw_input() return x #======================================================= def get_script_amount_from_utxo(wallet, utxo): """Given a JM wallet and a utxo string, find the corresponding private key and amount controlled in satoshis. """ for md, utxos in wallet.get_utxos_by_mixdepth_().items(): for (txid, index), utxo in utxos.items(): txhex = binascii.hexlify(txid) + ':' + str(index) if txhex != utxo: continue script = wallet.get_script_path(utxo['path']) print("Found utxo, its value is: {}".format(utxo['value'])) return script, utxo['value'] return None, None def create_single_acp_pair(wallet, utxo_in, script, addr_out, amount, bump, segwit=False): """Given a utxo and a signing key for it, and its amout in satoshis, sign a "transaction" consisting of only 1 input and one output, signed with single|acp sighash flags so it can be grafted into a bigger transaction. Also provide a destination address and a 'bump' value (so the creator can claim more output in the final transaction. Note that, for safety, bump *should* be positive if the recipient is untrusted, since otherwise they can waste your money by simply broadcasting this transaction without adding any inputs of their own. Returns the serialized 1 in, 1 out, and signed transaction. """ assert bump >= 0, "Output of single|acp pair must be bigger than input for safety." out = {"address": addr_out, "value": amount + bump} tx = btc.mktx([utxo_in], [out]) return wallet.sign_tx(tx, {0: (script, amount)}, hashcode=btc.SIGHASH_SINGLE|btc.SIGHASH_ANYONECANPAY) def graft_onto_single_acp(wallet, txhex, amount, destaddr): """Given a serialized txhex which is checked to be of form single|acp (one in, one out), a destination address and an amount to spend, grafts in this in-out pair (at index zero) to our own transaction spending amount amount to destination destaddr, and uses a user-specified transaction fee (normal joinmarket configuration), and sanity checks that the bump value is not greater than user specified bump option. Returned: serialized txhex of fully signed transaction. """ d = btc.deserialize(txhex) if len(d['ins']) != 1 or len(d['outs']) != 1: return (False, "Proposed tx should have 1 in 1 out, has: " + ','.join( [str(len(d[x])) for x in ['ins', 'outs']])) #most important part: check provider hasn't bumped more than options.bump: other_utxo_in = d['ins'][0]['outpoint']['hash'] + ":" + str(d['ins'][0]['outpoint']['index']) res = jm_single().bc_interface.query_utxo_set(other_utxo_in) assert len(res) == 1 if not res[0]: return (False, "Utxo provided by counterparty not found.") excess = d['outs'][0]['value'] - res[0]["value"] if not excess <= options.bump: return (False, "Counterparty claims too much excess value: " + str(excess)) #Last sanity check - ensure that it's single|acp, else we're wasting our time try: if 'txinwitness' in d['ins'][0]: sig, pub = d['ins'][0]['txinwitness'] else: sig, pub = btc.deserialize_script(d['ins'][0]['script']) assert sig[-2:] == "83" except Exception as e: return (False, "The transaction's signature does not parse as signed with " "SIGHASH_SINGLE|SIGHASH_ANYONECANPAY, for p2pkh or p2sh-p2wpkh, or " "is otherwise invalid, and so is not valid for this function.\n" + repr(e)) #source inputs for our own chosen spending amount: try: input_utxos = wallet.select_utxos(options.mixdepth, amount) except Exception as e: return (False, "Unable to select sufficient coins from mixdepth: " + str(options.mixdepth)) total_selected = sum([x['value'] for x in input_utxos.values()]) fee = estimate_tx_fee(len(input_utxos)+1, 3, txtype='p2sh-p2wpkh') change_amount = total_selected - amount - excess - fee changeaddr = wallet.get_new_addr(options.mixdepth, 1) #Build new transaction and, graft in signature ins = [other_utxo_in] + input_utxos.keys() outs = [d['outs'][0], {'address': destaddr, 'value': amount}, {'address': changeaddr, 'value': change_amount}] fulltx = btc.mktx(ins, outs) df = btc.deserialize(fulltx) #put back in original signature df['ins'][0]['script'] = d['ins'][0]['script'] if 'txinwitness' in d['ins'][0]: df['ins'][0]['txinwitness'] = d['ins'][0]['txinwitness'] for i, iu in enumerate(input_utxos): script, inamt = get_script_amount_from_utxo(wallet, iu) print("Signing index: ", i+1, " with script: ", script, " and amount: ", inamt, " for utxo: ", iu) fulltx = wallet.sign_tx(df, {i: (script, inamt)}) return True, btc.serialize(fulltx) if __name__ == "__main__": parser = get_parser() (options, args) = parser.parse_args() load_program_config() if get_wallet_cls() != SegwitWallet: print("Only segwit wallets are supported; remove any setting of `segwit`" " in `POLICY` in joinmarket.cfg. Quitting.") exit(0) #default args causes wallet sync here: wallet = cli_get_wallet(args[0]) if args[1] not in ['make', 'take']: print("Second argument must be 'make' or 'take', see '--help'") exit(0) if args[1] == "make": if len(args) < 3 or not is_utxo(args[2]): print("You must provide a utxo as third argument; 64 character hex " "txid, followed by ':', followed by the output index. " "Use wallet-tool.py method 'showutxos' to select one") exit(0) utxo_in = args[2] script, amount = get_script_amount_from_utxo(wallet, utxo_in) if not script: print("Failed to find the utxo's private key from the wallet; check " "if this utxo is actually contained in the wallet using " "wallet-tool.py showutxos") exit(0) #destination sourced from wallet addr_out = wallet.get_new_addr((options.mixdepth+1)%options.amtmixdepths, 1) single_acp = create_single_acp_pair(wallet, utxo_in, script, addr_out, amount, options.bump, segwit=True) print("Created the following one-in, one-out transaction, which will not " "be valid to broadcast itself (negative fee). Pass it to your " "counterparty:") print(pformat(single_acp)) print("Pass the following raw hex to your counterparty:") print(btc.serialize(single_acp)) exit(0) elif args[1] == "take": try: amount, destaddr, txhex = args[2:5] #sanity check input amount = int(amount) assert amount > 0 assert validate_address(destaddr) binascii.unhexlify(txhex) except Exception as e: print("Syntax error, should be 5 arguments, see --help. ", repr(e)) exit(0) success, complete_tx = graft_onto_single_acp(wallet, txhex, amount, destaddr) if not success: print("Quitting, reason: " + complete_tx) exit(0) #allow user to decide whether to broadcast: print("The following transaction has been prepared:") print(pformat(btc.deserialize(complete_tx))) broadcast = raw_input("Do you want to broadcast now? (y/n): ") if broadcast == "y": success = jm_single().bc_interface.pushtx(complete_tx) if not success: print("Failed to broadcast.") exit(0) else: print("You chose not to broadcast.") exit(0) print('done')