Browse Source
add-utxo.py and sendtomany.py; cmttools renamed to cmtdata, only stores commitments.json files.master
7 changed files with 463 additions and 4 deletions
@ -0,0 +1,61 @@
|
||||
from __future__ import print_function |
||||
|
||||
import sys, os |
||||
import jmclient.btc as btc |
||||
from jmclient import jm_single, get_p2pk_vbyte |
||||
|
||||
def quit(parser, errmsg): |
||||
parser.error(errmsg) |
||||
sys.exit(0) |
||||
|
||||
def get_utxo_info(upriv): |
||||
"""Verify that the input string parses correctly as (utxo, priv) |
||||
and return that. |
||||
""" |
||||
try: |
||||
u, priv = upriv.split(',') |
||||
u = u.strip() |
||||
priv = priv.strip() |
||||
txid, n = u.split(':') |
||||
assert len(txid)==64 |
||||
assert len(n) in range(1, 4) |
||||
n = int(n) |
||||
assert n in range(256) |
||||
except: |
||||
#not sending data to stdout in case privkey info |
||||
print("Failed to parse utxo information for utxo") |
||||
try: |
||||
hexpriv = btc.from_wif_privkey(priv, vbyte=get_p2pk_vbyte()) |
||||
except: |
||||
print("failed to parse privkey, make sure it's WIF compressed format.") |
||||
return u, priv |
||||
|
||||
def validate_utxo_data(utxo_datas, retrieve=False): |
||||
"""For each txid: N, privkey, first |
||||
convert the privkey and convert to address, |
||||
then use the blockchain instance to look up |
||||
the utxo and check that its address field matches. |
||||
If retrieve is True, return the set of utxos and their values. |
||||
""" |
||||
results = [] |
||||
for u, priv in utxo_datas: |
||||
print('validating this utxo: ' + str(u)) |
||||
hexpriv = btc.from_wif_privkey(priv, vbyte=get_p2pk_vbyte()) |
||||
addr = btc.privkey_to_address(hexpriv, magicbyte=get_p2pk_vbyte()) |
||||
print('claimed address: ' + addr) |
||||
res = jm_single().bc_interface.query_utxo_set([u]) |
||||
print('blockchain shows this data: ' + str(res)) |
||||
if len(res) != 1: |
||||
print("utxo not found on blockchain: " + str(u)) |
||||
return False |
||||
if res[0]['address'] != addr: |
||||
print("privkey corresponds to the wrong address for utxo: " + str(u)) |
||||
print("blockchain returned address: " + res[0]['address']) |
||||
print("your privkey gave this address: " + addr) |
||||
return False |
||||
if retrieve: |
||||
results.append((u, res[0]['value'])) |
||||
print('all utxos validated OK') |
||||
if retrieve: |
||||
return results |
||||
return True |
||||
@ -0,0 +1,53 @@
|
||||
# Command line scripts for Joinmarket |
||||
|
||||
|
||||
All user level scripts here. |
||||
|
||||
(The phrase "normal Joinmarket" in the below refers to the [existing repo](https://github.com/Joinmarket-Org/joinmarket). |
||||
|
||||
The subdirectories `logs` and `wallets` have the same role as in normal Joinmarket. |
||||
The subdirectory `cmtdata` contains only your `commitments.json` storage of your used |
||||
commitments (ignored by github of course!). The filename is set in joinmarket.cfg. |
||||
|
||||
The `joinmarket.cfg` will be created and maintained in this directory. |
||||
|
||||
Brief explanation of the function of each of the scripts: |
||||
|
||||
###sendpayment.py |
||||
|
||||
Either use the same syntax as for normal Joinmarket: |
||||
|
||||
`python sendpayment.py --fast -N 3 -m 1 -P wallet.json 50000000 <address>` |
||||
|
||||
or use the new schedule approach. For an example, see the [sample schedule file](https://github.com/AdamISZ/joinmarket-clientserver/blob/master/scripts/sample-schedule-for-testnet). |
||||
Do: |
||||
|
||||
`python sendpayment.py --fast -S sample-schedule-for-testnet wallet.json` |
||||
|
||||
Note that the magic string `INTERNAL` in the file creates a payment to a new address |
||||
in the next mixdepth (wrapping around to zero if you reach the maximum mixdepth). |
||||
|
||||
The schedule file can have any name, and is a comma separated value file, the lists |
||||
must follow that format (length 4 items). |
||||
|
||||
###wallet-tool.py |
||||
|
||||
This is the same as in normal Joinmarket. |
||||
|
||||
###joinmarketd.py |
||||
|
||||
This file's role is explained in the main README in the top level directory. It only |
||||
takes one argument, the port it serves on: |
||||
|
||||
`python joinmarketd.py 12345` |
||||
|
||||
###add-utxo.py |
||||
|
||||
This works exactly as in normal Joinmarket, with the exception of the location |
||||
of the `commitments.json` file, explained above. |
||||
|
||||
###sendtomany.py |
||||
|
||||
As above. |
||||
|
||||
More details above, and probably more scripts, will be added later. |
||||
@ -0,0 +1,233 @@
|
||||
#! /usr/bin/env python |
||||
from __future__ import absolute_import |
||||
"""A very simple command line tool to import utxos to be used |
||||
as commitments into joinmarket's commitments.json file, allowing |
||||
users to retry transactions more often without getting banned by |
||||
the anti-snooping feature employed by makers. |
||||
""" |
||||
|
||||
import binascii |
||||
import sys |
||||
import os |
||||
import json |
||||
from pprint import pformat |
||||
|
||||
from optparse import OptionParser |
||||
import jmclient.btc as btc |
||||
from jmclient import (load_program_config, jm_single, get_p2pk_vbyte, |
||||
Wallet, sync_wallet, add_external_commitments, |
||||
generate_podle, update_commitments, PoDLE, |
||||
set_commitment_file, get_podle_commitments, |
||||
get_utxo_info, validate_utxo_data, quit) |
||||
|
||||
def add_ext_commitments(utxo_datas): |
||||
"""Persist the PoDLE commitments for this utxo |
||||
to the commitments.json file. The number of separate |
||||
entries is dependent on the taker_utxo_retries entry, by |
||||
default 3. |
||||
""" |
||||
def generate_single_podle_sig(u, priv, i): |
||||
"""Make a podle entry for key priv at index i, using a dummy utxo value. |
||||
This calls the underlying 'raw' code based on the class PoDLE, not the |
||||
library 'generate_podle' which intelligently searches and updates commitments. |
||||
""" |
||||
#Convert priv to hex |
||||
hexpriv = btc.from_wif_privkey(priv, vbyte=get_p2pk_vbyte()) |
||||
podle = PoDLE(u, hexpriv) |
||||
r = podle.generate_podle(i) |
||||
return (r['P'], r['P2'], r['sig'], |
||||
r['e'], r['commit']) |
||||
ecs = {} |
||||
for u, priv in utxo_datas: |
||||
ecs[u] = {} |
||||
ecs[u]['reveal']={} |
||||
for j in range(jm_single().config.getint("POLICY", "taker_utxo_retries")): |
||||
P, P2, s, e, commit = generate_single_podle_sig(u, priv, j) |
||||
if 'P' not in ecs[u]: |
||||
ecs[u]['P']=P |
||||
ecs[u]['reveal'][j] = {'P2':P2, 's':s, 'e':e} |
||||
add_external_commitments(ecs) |
||||
|
||||
def main(): |
||||
parser = OptionParser( |
||||
usage= |
||||
'usage: %prog [options] [txid:n]', |
||||
description="Adds one or more utxos to the list that can be used to make " |
||||
"commitments for anti-snooping. Note that this utxo, and its " |
||||
"PUBkey, will be revealed to makers, so consider the privacy " |
||||
"implication. " |
||||
|
||||
"It may be useful to those who are having trouble making " |
||||
"coinjoins due to several unsuccessful attempts (especially " |
||||
"if your joinmarket wallet is new). " |
||||
|
||||
"'Utxo' means unspent transaction output, it must not " |
||||
"already be spent. " |
||||
"The options -w, -r and -R offer ways to load these utxos " |
||||
"from a file or wallet. " |
||||
"If you enter a single utxo without these options, you will be " |
||||
"prompted to enter the private key here - it must be in " |
||||
"WIF compressed format. " |
||||
|
||||
"BE CAREFUL about handling private keys! " |
||||
"Don't do this in insecure environments. " |
||||
|
||||
"Also note this ONLY works for standard (p2pkh) utxos." |
||||
) |
||||
parser.add_option( |
||||
'-r', |
||||
'--read-from-file', |
||||
action='store', |
||||
type='str', |
||||
dest='in_file', |
||||
help='name of plain text csv file containing utxos, one per line, format: ' |
||||
'txid:N, WIF-compressed-privkey' |
||||
) |
||||
parser.add_option( |
||||
'-R', |
||||
'--read-from-json', |
||||
action='store', |
||||
type='str', |
||||
dest='in_json', |
||||
help='name of json formatted file containing utxos with private keys, as ' |
||||
'output from "python wallet-tool.py -u -p walletname showutxos"' |
||||
) |
||||
parser.add_option( |
||||
'-w', |
||||
'--load-wallet', |
||||
action='store', |
||||
type='str', |
||||
dest='loadwallet', |
||||
help='name of wallet from which to load utxos and use as commitments.' |
||||
) |
||||
parser.add_option( |
||||
'-g', |
||||
'--gap-limit', |
||||
action='store', |
||||
type='int', |
||||
dest='gaplimit', |
||||
default = 6, |
||||
help='Only to be used with -w; gap limit for Joinmarket wallet, default 6.' |
||||
) |
||||
parser.add_option( |
||||
'-M', |
||||
'--max-mixdepth', |
||||
action='store', |
||||
type='int', |
||||
dest='maxmixdepth', |
||||
default=5, |
||||
help='Only to be used with -w; number of mixdepths for wallet, default 5.' |
||||
) |
||||
parser.add_option( |
||||
'-d', |
||||
'--delete-external', |
||||
action='store_true', |
||||
dest='delete_ext', |
||||
help='deletes the current list of external commitment utxos', |
||||
default=False |
||||
) |
||||
parser.add_option( |
||||
'-v', |
||||
'--validate-utxos', |
||||
action='store_true', |
||||
dest='validate', |
||||
help='validate the utxos and pubkeys provided against the blockchain', |
||||
default=False |
||||
) |
||||
parser.add_option( |
||||
'-o', |
||||
'--validate-only', |
||||
action='store_true', |
||||
dest='vonly', |
||||
help='only validate the provided utxos (file or command line), not add', |
||||
default=False |
||||
) |
||||
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')) |
||||
(options, args) = parser.parse_args() |
||||
load_program_config() |
||||
#TODO; sort out "commit file location" global so this script can |
||||
#run without this hardcoding: |
||||
utxo_data = [] |
||||
if options.delete_ext: |
||||
other = options.in_file or options.in_json or options.loadwallet |
||||
if len(args) > 0 or other: |
||||
if raw_input("You have chosen to delete commitments, other arguments " |
||||
"will be ignored; continue? (y/n)") != 'y': |
||||
print "Quitting" |
||||
sys.exit(0) |
||||
c, e = get_podle_commitments() |
||||
print pformat(e) |
||||
if raw_input( |
||||
"You will remove the above commitments; are you sure? (y/n): ") != 'y': |
||||
print "Quitting" |
||||
sys.exit(0) |
||||
update_commitments(external_to_remove=e) |
||||
print "Commitments deleted." |
||||
sys.exit(0) |
||||
|
||||
#Three options (-w, -r, -R) for loading utxo and privkey pairs from a wallet, |
||||
#csv file or json file. |
||||
if options.loadwallet: |
||||
wallet = Wallet(options.loadwallet, |
||||
options.maxmixdepth, |
||||
options.gaplimit) |
||||
sync_wallet(wallet, fast=options.fastsync) |
||||
unsp = {} |
||||
for u, av in wallet.unspent.iteritems(): |
||||
addr = av['address'] |
||||
key = wallet.get_key_from_addr(addr) |
||||
wifkey = btc.wif_compressed_privkey(key, vbyte=get_p2pk_vbyte()) |
||||
unsp[u] = {'address': av['address'], |
||||
'value': av['value'], 'privkey': wifkey} |
||||
for u, pva in unsp.iteritems(): |
||||
utxo_data.append((u, pva['privkey'])) |
||||
elif options.in_file: |
||||
with open(options.in_file, "rb") as f: |
||||
utxo_info = f.readlines() |
||||
for ul in utxo_info: |
||||
ul = ul.rstrip() |
||||
if ul: |
||||
u, priv = get_utxo_info(ul) |
||||
if not u: |
||||
quit(parser, "Failed to parse utxo info: " + str(ul)) |
||||
utxo_data.append((u, priv)) |
||||
elif options.in_json: |
||||
if not os.path.isfile(options.in_json): |
||||
print "File: " + options.in_json + " not found." |
||||
sys.exit(0) |
||||
with open(options.in_json, "rb") as f: |
||||
try: |
||||
utxo_json = json.loads(f.read()) |
||||
except: |
||||
print "Failed to read json from " + options.in_json |
||||
sys.exit(0) |
||||
for u, pva in utxo_json.iteritems(): |
||||
utxo_data.append((u, pva['privkey'])) |
||||
elif len(args) == 1: |
||||
u = args[0] |
||||
priv = raw_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) |
||||
utxo_data.append((u, priv)) |
||||
else: |
||||
quit(parser, 'Invalid syntax') |
||||
if options.validate or options.vonly: |
||||
if not validate_utxo_data(utxo_data): |
||||
quit(parser, "Utxos did not validate, quitting") |
||||
if options.vonly: |
||||
sys.exit(0) |
||||
|
||||
#We are adding utxos to the external list |
||||
assert len(utxo_data) |
||||
add_ext_commitments(utxo_data) |
||||
|
||||
if __name__ == "__main__": |
||||
main() |
||||
print('done') |
||||
@ -0,0 +1,4 @@
|
||||
# Ignore all |
||||
* |
||||
# Except this file |
||||
!.gitignore |
||||
@ -0,0 +1,105 @@
|
||||
#! /usr/bin/env python |
||||
from __future__ import absolute_import, print_function |
||||
"""A simple command line tool to create a bunch |
||||
of utxos from one (thus giving more potential commitments |
||||
for a Joinmarket user, although of course it may be useful |
||||
for other reasons). |
||||
""" |
||||
|
||||
import binascii |
||||
import sys, os |
||||
from pprint import pformat |
||||
from optparse import OptionParser |
||||
import jmclient.btc as btc |
||||
from jmclient import (load_program_config, estimate_tx_fee, jm_single, |
||||
get_p2pk_vbyte, validate_address, get_log, |
||||
get_utxo_info, validate_utxo_data, quit) |
||||
log = get_log() |
||||
|
||||
def sign(utxo, priv, destaddrs): |
||||
"""Sign a tx sending the amount amt, from utxo utxo, |
||||
equally to each of addresses in list destaddrs, |
||||
after fees; the purpose is to create a large |
||||
number of utxos. |
||||
""" |
||||
results = validate_utxo_data([(utxo, priv)], retrieve=True) |
||||
if not results: |
||||
return False |
||||
assert results[0][0] == utxo |
||||
amt = results[0][1] |
||||
ins = [utxo] |
||||
estfee = estimate_tx_fee(1, len(destaddrs)) |
||||
outs = [] |
||||
share = int((amt - estfee) / len(destaddrs)) |
||||
fee = amt - share*len(destaddrs) |
||||
assert fee >= estfee |
||||
log.info("Using fee: " + str(fee)) |
||||
for i, addr in enumerate(destaddrs): |
||||
outs.append({'address': addr, 'value': share}) |
||||
unsigned_tx = btc.mktx(ins, outs) |
||||
return btc.sign(unsigned_tx, 0, btc.from_wif_privkey( |
||||
priv, vbyte=get_p2pk_vbyte())) |
||||
|
||||
def main(): |
||||
parser = OptionParser( |
||||
usage= |
||||
'usage: %prog [options] utxo destaddr1 destaddr2 ..', |
||||
description="For creating multiple utxos from one (for commitments in JM)." |
||||
"Provide a utxo in form txid:N that has some unspent coins;" |
||||
"Specify a list of destination addresses and the coins will" |
||||
"be split equally between them (after bitcoin fees)." |
||||
|
||||
"You'll be prompted to enter the private key for the utxo" |
||||
"during the run; it must be in WIF compressed format." |
||||
"After the transaction is completed, the utxo strings for" |
||||
|
||||
"the new outputs will be shown." |
||||
"Note that these utxos will not be ready for use as external" |
||||
|
||||
"commitments in Joinmarket until 5 confirmations have passed." |
||||
" BE CAREFUL about handling private keys!" |
||||
" Don't do this in insecure environments." |
||||
" Also note this ONLY works for standard (p2pkh) utxos." |
||||
) |
||||
parser.add_option( |
||||
'-v', |
||||
'--validate-utxos', |
||||
action='store_true', |
||||
dest='validate', |
||||
help='validate the utxos and pubkeys provided against the blockchain', |
||||
default=False |
||||
) |
||||
parser.add_option( |
||||
'-o', |
||||
'--validate-only', |
||||
action='store_true', |
||||
dest='vonly', |
||||
help='only validate the provided utxos (file or command line), not add', |
||||
default=False |
||||
) |
||||
(options, args) = parser.parse_args() |
||||
load_program_config() |
||||
if len(args) < 2: |
||||
quit(parser, 'Invalid syntax') |
||||
u = args[0] |
||||
priv = raw_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) |
||||
txsigned = sign(u, priv, destaddrs) |
||||
log.debug("Got signed transaction:\n" + txsigned) |
||||
log.debug("Deserialized:") |
||||
log.debug(pformat(btc.deserialize(txsigned))) |
||||
if raw_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) |
||||
|
||||
if __name__ == "__main__": |
||||
main() |
||||
print('done') |
||||
Loading…
Reference in new issue