1 changed files with 0 additions and 269 deletions
@ -1,269 +0,0 @@ |
|||||||
#!/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') |
|
||||||
Loading…
Reference in new issue