Browse Source

Merge #729: Update commitments utility scripts

cc78dae Update commitments utility scripts (Adam Gibson)
master
Adam Gibson 5 years ago
parent
commit
8f8c52afb2
No known key found for this signature in database
GPG Key ID: 141001A1AF77F20B
  1. 2
      jmclient/jmclient/__init__.py
  2. 34
      jmclient/jmclient/commitment_utils.py
  3. 12
      jmclient/test/test_commitment_utils.py
  4. 35
      scripts/add-utxo.py
  5. 70
      scripts/sendtomany.py

2
jmclient/jmclient/__init__.py

@ -18,7 +18,7 @@ from .wallet import (Mnemonic, estimate_tx_fee, WalletError, BaseWallet, ImportW
UTXOManager, WALLET_IMPLEMENTATIONS, compute_tx_locktime) UTXOManager, WALLET_IMPLEMENTATIONS, compute_tx_locktime)
from .storage import (Argon2Hash, Storage, StorageError, RetryableStorageError, from .storage import (Argon2Hash, Storage, StorageError, RetryableStorageError,
StoragePasswordError, VolatileStorage) StoragePasswordError, VolatileStorage)
from .cryptoengine import (BTCEngine, BTC_P2PKH, BTC_P2SH_P2WPKH, EngineError, from .cryptoengine import (BTCEngine, BTC_P2PKH, BTC_P2SH_P2WPKH, BTC_P2WPKH, EngineError,
TYPE_P2PKH, TYPE_P2SH_P2WPKH, TYPE_P2WPKH) TYPE_P2PKH, TYPE_P2SH_P2WPKH, TYPE_P2WPKH)
from .configure import (load_test_config, process_shutdown, from .configure import (load_test_config, process_shutdown,
load_program_config, jm_single, get_network, update_persist_config, load_program_config, jm_single, get_network, update_persist_config,

34
jmclient/jmclient/commitment_utils.py

@ -1,8 +1,8 @@
import sys import sys
from jmbase import jmprint from jmbase import jmprint, utxostr_to_utxo
from jmclient import jm_single, BTCEngine, BTC_P2PKH, BTC_P2SH_P2WPKH from jmclient import jm_single, BTCEngine, BTC_P2PKH, BTC_P2SH_P2WPKH, BTC_P2WPKH
from jmbase.support import EXIT_FAILURE, utxostr_to_utxo from jmbase.support import EXIT_FAILURE, utxostr_to_utxo, utxo_to_utxostr
def quit(parser, errmsg): #pragma: no cover def quit(parser, errmsg): #pragma: no cover
@ -32,8 +32,8 @@ def get_utxo_info(upriv):
raise raise
return u, priv return u, priv
def validate_utxo_data(utxo_datas, retrieve=False, segwit=False): def validate_utxo_data(utxo_datas, retrieve=False, utxo_address_type="p2wpkh"):
"""For each txid: N, privkey, first """For each (utxo, privkey), first
convert the privkey and convert to address, convert the privkey and convert to address,
then use the blockchain instance to look up then use the blockchain instance to look up
the utxo and check that its address field matches. the utxo and check that its address field matches.
@ -44,21 +44,33 @@ def validate_utxo_data(utxo_datas, retrieve=False, segwit=False):
""" """
results = [] results = []
for u, priv in utxo_datas: for u, priv in utxo_datas:
jmprint('validating this utxo: ' + str(u), "info") success, utxostr = utxo_to_utxostr(u)
if not success:
jmprint("Invalid utxo format: " + str(u), "error")
sys.exit(EXIT_FAILURE)
jmprint('validating this utxo: ' + utxostr, "info")
# as noted in `ImportWalletMixin` code comments, there is not # as noted in `ImportWalletMixin` code comments, there is not
# yet a functional auto-detection of key type from WIF, so the # yet a functional auto-detection of key type from WIF, so the
# second argument is ignored; we assume p2sh-p2wpkh if segwit, # second argument is ignored; we assume p2sh-p2wpkh if segwit=True,
# else we assume p2pkh. # p2pkh if segwit=False, and p2wpkh if segwit="native" (slightly
engine = BTC_P2SH_P2WPKH if segwit else BTC_P2PKH # ugly, just done for backwards compat.).
if utxo_address_type == "p2wpkh":
engine = BTC_P2WPKH
elif utxo_address_type == "p2sh-p2wpkh":
engine = BTC_P2SH_P2WPKH
elif utxo_address_type == "p2pkh":
engine = BTC_P2PKH
else:
raise Exception("Invalid argument: " + str(utxo_address_type))
rawpriv, _ = BTCEngine.wif_to_privkey(priv) rawpriv, _ = BTCEngine.wif_to_privkey(priv)
addr = engine.privkey_to_address(rawpriv) addr = engine.privkey_to_address(rawpriv)
jmprint('claimed address: ' + addr, "info") jmprint('claimed address: ' + addr, "info")
res = jm_single().bc_interface.query_utxo_set([u]) res = jm_single().bc_interface.query_utxo_set([u])
if len(res) != 1 or None in res: if len(res) != 1 or None in res:
jmprint("utxo not found on blockchain: " + str(u), "error") jmprint("utxo not found on blockchain: " + utxostr, "error")
return False return False
if res[0]['address'] != addr: if res[0]['address'] != addr:
jmprint("privkey corresponds to the wrong address for utxo: " + str(u), "error") jmprint("privkey corresponds to the wrong address for utxo: " + utxostr, "error")
jmprint("blockchain returned address: {}".format(res[0]['address']), "error") jmprint("blockchain returned address: {}".format(res[0]['address']), "error")
jmprint("your privkey gave this address: " + addr, "error") jmprint("your privkey gave this address: " + addr, "error")
return False return False

12
jmclient/test/test_commitment_utils.py

@ -2,6 +2,7 @@
from commontest import DummyBlockchainInterface from commontest import DummyBlockchainInterface
import pytest import pytest
from jmbase import utxostr_to_utxo
from jmclient import (load_test_config, jm_single) from jmclient import (load_test_config, jm_single)
from jmclient.commitment_utils import get_utxo_info, validate_utxo_data from jmclient.commitment_utils import get_utxo_info, validate_utxo_data
from jmbitcoin import select_chain_params from jmbitcoin import select_chain_params
@ -15,12 +16,13 @@ def test_get_utxo_info():
dbci = DummyBlockchainInterface() dbci = DummyBlockchainInterface()
privkey = "L1RrrnXkcKut5DEMwtDthjwRcTTwED36thyL1DebVrKuwvohjMNi" privkey = "L1RrrnXkcKut5DEMwtDthjwRcTTwED36thyL1DebVrKuwvohjMNi"
#to verify use from_wif_privkey and privkey_to_address #to verify use from_wif_privkey and privkey_to_address
iaddr = "1LDsjB43N2NAQ1Vbc2xyHca4iBBciN8iwC" iaddr = "bc1q6tvmnmetj8vfz98vuetpvtuplqtj4uvvwjgxxc"
fakeutxo = "aa"*32+":08" fakeutxo = "aa"*32+":08"
success, fakeutxo_bin = utxostr_to_utxo(fakeutxo)
assert success
fake_query_results = [{'value': 200000000, fake_query_results = [{'value': 200000000,
'address': iaddr, 'address': iaddr,
'utxo': fakeutxo, 'utxo': fakeutxo_bin,
'confirms': 20}] 'confirms': 20}]
dbci.insert_fake_query_results(fake_query_results) dbci.insert_fake_query_results(fake_query_results)
jm_single().bc_interface = dbci jm_single().bc_interface = dbci
@ -39,12 +41,12 @@ def test_get_utxo_info():
with pytest.raises(Exception) as e_info: with pytest.raises(Exception) as e_info:
u, priv = get_utxo_info(fakeutxo + "," + p2) u, priv = get_utxo_info(fakeutxo + "," + p2)
utxodatas = [(fakeutxo, privkey)] utxodatas = [(fakeutxo_bin, privkey)]
retval = validate_utxo_data(utxodatas, False) retval = validate_utxo_data(utxodatas, False)
assert retval assert retval
#try to retrieve #try to retrieve
retval = validate_utxo_data(utxodatas, True) retval = validate_utxo_data(utxodatas, True)
assert retval[0] == (fakeutxo, 200000000) assert retval[0] == (fakeutxo_bin, 200000000)
fake_query_results[0]['address'] = "fakeaddress" fake_query_results[0]['address'] = "fakeaddress"
dbci.insert_fake_query_results(fake_query_results) dbci.insert_fake_query_results(fake_query_results)
#validate should fail for wrong address #validate should fail for wrong address

35
scripts/add-utxo.py

@ -17,7 +17,8 @@ from jmclient import load_program_config, jm_single,\
open_wallet, WalletService, add_external_commitments, update_commitments,\ open_wallet, WalletService, add_external_commitments, update_commitments,\
PoDLE, get_podle_commitments, get_utxo_info, validate_utxo_data, quit,\ PoDLE, get_podle_commitments, get_utxo_info, validate_utxo_data, quit,\
get_wallet_path, add_base_options, BTCEngine, BTC_P2SH_P2WPKH get_wallet_path, add_base_options, BTCEngine, BTC_P2SH_P2WPKH
from jmbase.support import EXIT_SUCCESS, EXIT_FAILURE, EXIT_ARGERROR, jmprint from jmbase.support import EXIT_SUCCESS, EXIT_FAILURE, EXIT_ARGERROR, \
jmprint, utxostr_to_utxo
def add_ext_commitments(utxo_datas): def add_ext_commitments(utxo_datas):
@ -181,11 +182,10 @@ def main():
# minor note: adding a utxo from an external wallet for commitments, we # minor note: adding a utxo from an external wallet for commitments, we
# default to not allowing disabled utxos to avoid a privacy leak, so the # default to not allowing disabled utxos to avoid a privacy leak, so the
# user would have to explicitly enable. # user would have to explicitly enable.
for md, utxos in wallet_service.get_utxos_by_mixdepth(hexfmt=False).items(): for md, utxos in wallet_service.get_utxos_by_mixdepth().items():
for (txid, index), utxo in utxos.items(): for utxo, utxodata in utxos.items():
txhex = binascii.hexlify(txid).decode('ascii') + ':' + str(index) wif = wallet_service.get_wif_path(utxodata['path'])
wif = wallet_service.get_wif_path(utxo['path']) utxo_data.append((utxo, wif))
utxo_data.append((txhex, wif))
elif options.in_file: elif options.in_file:
with open(options.in_file, "rb") as f: with open(options.in_file, "rb") as f:
@ -196,7 +196,7 @@ def main():
u, priv = get_utxo_info(ul) u, priv = get_utxo_info(ul)
if not u: if not u:
quit(parser, "Failed to parse utxo info: " + str(ul)) quit(parser, "Failed to parse utxo info: " + str(ul))
utxo_data.append((u, priv)) utxo_data.append((utxostr_to_utxo(u), priv))
elif options.in_json: elif options.in_json:
if not os.path.isfile(options.in_json): if not os.path.isfile(options.in_json):
jmprint("File: " + options.in_json + " not found.", "error") jmprint("File: " + options.in_json + " not found.", "error")
@ -208,7 +208,7 @@ def main():
jmprint("Failed to read json from " + options.in_json, "error") jmprint("Failed to read json from " + options.in_json, "error")
sys.exit(EXIT_FAILURE) sys.exit(EXIT_FAILURE)
for u, pva in iteritems(utxo_json): for u, pva in iteritems(utxo_json):
utxo_data.append((u, pva['privkey'])) utxo_data.append((utxostr_to_utxo(u), pva['privkey']))
elif len(args) == 1: elif len(args) == 1:
u = args[0] u = args[0]
priv = input( priv = input(
@ -216,15 +216,26 @@ def main():
u, priv = get_utxo_info(','.join([u, priv])) u, priv = get_utxo_info(','.join([u, priv]))
if not u: if not u:
quit(parser, "Failed to parse utxo info: " + u) quit(parser, "Failed to parse utxo info: " + u)
utxo_data.append((u, priv)) utxo_data.append((utxostr_to_utxo(u), priv))
else: else:
quit(parser, 'Invalid syntax') quit(parser, 'Invalid syntax')
if options.validate or options.vonly: if options.validate or options.vonly:
sw = False if jm_single().config.get("POLICY", "segwit") == "false" else True # if the utxos are loaded from a wallet, we use the wallet's
if not validate_utxo_data(utxo_data, segwit=sw): # txtype to determine the value of `utxo_address_type`; if not,
# we use joinmarket.cfg.
if options.loadwallet:
utxo_address_type = wallet_service.get_txtype()
else:
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"
if not validate_utxo_data(utxo_data, utxo_address_type=utxo_address_type):
quit(parser, "Utxos did not validate, quitting") quit(parser, "Utxos did not validate, quitting")
if options.vonly: if options.vonly:
sys.exit(EXIT_ARGERROR) sys.exit(EXIT_SUCCESS)
#We are adding utxos to the external list #We are adding utxos to the external list
assert len(utxo_data) assert len(utxo_data)

70
scripts/sendtomany.py

@ -11,26 +11,24 @@ import jmbitcoin as btc
from jmbase import get_log, jmprint, bintohex, utxostr_to_utxo from jmbase import get_log, jmprint, bintohex, utxostr_to_utxo
from jmclient import load_program_config, estimate_tx_fee, jm_single,\ from jmclient import load_program_config, estimate_tx_fee, jm_single,\
validate_address, get_utxo_info, add_base_options,\ validate_address, get_utxo_info, add_base_options,\
validate_utxo_data, quit, BTCEngine validate_utxo_data, quit, BTCEngine, compute_tx_locktime
log = get_log() log = get_log()
def sign(utxo, priv, destaddrs, segwit=True): def sign(utxo, priv, destaddrs, utxo_address_type):
"""Sign a tx sending the amount amt, from utxo utxo, """Sign a tx sending the amount amt, from utxo utxo,
equally to each of addresses in list destaddrs, equally to each of addresses in list destaddrs,
after fees; the purpose is to create a large after fees; the purpose is to create multiple utxos.
number of utxos. If segwit=True the (single) utxo is assumed to utxo_address_type must be one of p2sh-p2wpkh/p2wpkh/p2pkh.
be of type segwit p2sh/p2wpkh.
""" """
results = validate_utxo_data([(utxo, priv)], retrieve=True, segwit=segwit) results = validate_utxo_data([(utxo, priv)], retrieve=True,
utxo_address_type=utxo_address_type)
if not results: if not results:
return False return False
assert results[0][0] == utxo assert results[0][0] == utxo
amt = results[0][1] amt = results[0][1]
ins = [utxo] ins = [utxo]
# TODO extend to other utxo types estfee = estimate_tx_fee(1, len(destaddrs), txtype=utxo_address_type)
txtype = 'p2sh-p2wpkh' if segwit else 'p2pkh'
estfee = estimate_tx_fee(1, len(destaddrs), txtype=txtype)
outs = [] outs = []
share = int((amt - estfee) / len(destaddrs)) share = int((amt - estfee) / len(destaddrs))
fee = amt - share*len(destaddrs) fee = amt - share*len(destaddrs)
@ -38,10 +36,14 @@ def sign(utxo, priv, destaddrs, segwit=True):
log.info("Using fee: " + str(fee)) log.info("Using fee: " + str(fee))
for i, addr in enumerate(destaddrs): for i, addr in enumerate(destaddrs):
outs.append({'address': addr, 'value': share}) outs.append({'address': addr, 'value': share})
tx = btc.mktx(ins, outs) tx = btc.make_shuffled_tx(ins, outs, version=2, locktime=compute_tx_locktime())
amtforsign = amt if segwit else None amtforsign = amt if utxo_address_type != "p2pkh" else None
rawpriv, _ = BTCEngine.wif_to_privkey(priv) rawpriv, _ = BTCEngine.wif_to_privkey(priv)
success, msg = btc.sign(tx, 0, rawpriv, amount=amtforsign) if utxo_address_type == "p2wpkh":
native = utxo_address_type
else:
native = False
success, msg = btc.sign(tx, 0, rawpriv, amount=amtforsign, native=native)
assert success, msg assert success, msg
return tx return tx
@ -69,28 +71,14 @@ def main():
" joinmarket.cfg for the former." " joinmarket.cfg for the former."
) )
parser.add_option( parser.add_option(
'-v', '-t',
'--validate-utxos', '--utxo-address-type',
action='store_true', action='store',
dest='validate', dest='utxo_address_type',
help='validate the utxos and pubkeys provided against the blockchain', help=('type of address of coin being spent - one of "p2pkh", "p2wpkh", "p2sh-p2wpkh". '
default=False 'No other scriptpubkey types (e.g. multisig) are supported. If not set, we default '
) 'to what is in joinmarket.cfg.'),
parser.add_option( default=""
'-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(
'-n',
'--non-segwit-input',
action='store_true',
dest='nonsegwit',
help='input is p2pkh ("1" address), not segwit; if not used, input is assumed to be segwit type.',
default=False
) )
add_base_options(parser) add_base_options(parser)
(options, args) = parser.parse_args() (options, args) = parser.parse_args()
@ -110,13 +98,21 @@ def main():
success, utxo = utxostr_to_utxo(u) success, utxo = utxostr_to_utxo(u)
if not success: if not success:
quit(parser, "Failed to load utxo from string: " + utxo) quit(parser, "Failed to load utxo from string: " + utxo)
txsigned = sign(utxo, priv, destaddrs, segwit = not options.nonsegwit) 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: if not txsigned:
log.info("Transaction signing operation failed, see debug messages for details.") log.info("Transaction signing operation failed, see debug messages for details.")
return return
log.info("Got signed transaction:\n" + bintohex(txsigned.serialize())) log.info("Got signed transaction:\n" + bintohex(txsigned.serialize()))
log.debug("Deserialized:") log.info(btc.human_readable_transaction(txsigned))
log.debug(pformat(str(txsigned)))
if input('Would you like to push to the network? (y/n):')[0] != 'y': if input('Would you like to push to the network? (y/n):')[0] != 'y':
log.info("You chose not to broadcast the transaction, quitting.") log.info("You chose not to broadcast the transaction, quitting.")
return return

Loading…
Cancel
Save