mirror of
https://github.com/JoinMarket-Org/joinmarket-clientserver.git
synced 2026-08-19 13:27:30 +02:00
This commit fixes two minor bugs encountered in running create-snicker-proposal.py. First, the port for AMP connections was not correct (by convention it is now the Joinmarket proto main port minus 1000), second in the above mentioned script, the reactor startup client side was not disabling the Joinmarket protocol, as it should.
227 lines
9 KiB
Python
227 lines
9 KiB
Python
#!/usr/bin/env python3
|
|
|
|
description="""A rudimentary implementation of creation of a SNICKER proposal.
|
|
|
|
**THIS TOOL DOES NOT SCAN FOR CANDIDATE TRANSACTIONS**
|
|
|
|
It only creates proposals on candidate transactions (individually)
|
|
that you have already found.
|
|
|
|
Input: the user's wallet, mixdepth to source their (1) coin from,
|
|
and a hex encoded pre-existing bitcoin transaction (fully signed)
|
|
as target.
|
|
User chooses the input to source the pubkey from, and the output
|
|
to use to create the SNICKER coinjoin. Tx fees are sourced from
|
|
the config, and the user specifies interactively the number of sats
|
|
to award the receiver (can be negative).
|
|
|
|
Once the proposal is created, it is uploaded to the servers as per
|
|
the `servers` setting in `joinmarket.cfg`, unless the -n option is
|
|
specified (see help for options), in which case the proposal is
|
|
output to stdout in the same string format: base64proposal,hexpubkey.
|
|
"""
|
|
|
|
import sys
|
|
from optparse import OptionParser
|
|
from jmbase import bintohex, jmprint, hextobin, \
|
|
EXIT_ARGERROR, EXIT_FAILURE, EXIT_SUCCESS, get_pow
|
|
import jmbitcoin as btc
|
|
from jmclient import (process_shutdown,
|
|
jm_single, load_program_config, check_regtest,
|
|
estimate_tx_fee, add_base_options, get_wallet_path,
|
|
open_test_wallet_maybe, WalletService, SNICKERClientProtocolFactory,
|
|
start_reactor, JMPluginService)
|
|
from jmclient.configure import get_log
|
|
|
|
log = get_log()
|
|
|
|
def main():
|
|
parser = OptionParser(
|
|
usage=
|
|
'usage: %prog [options] walletname hex-tx input-index output-index net-transfer',
|
|
description=description
|
|
)
|
|
add_base_options(parser)
|
|
parser.add_option('-m',
|
|
'--mixdepth',
|
|
action='store',
|
|
type='int',
|
|
dest='mixdepth',
|
|
help='mixdepth/account to spend from, default=0',
|
|
default=0)
|
|
parser.add_option(
|
|
'-g',
|
|
'--gap-limit',
|
|
action='store',
|
|
type='int',
|
|
dest='gaplimit',
|
|
default = 6,
|
|
help='gap limit for Joinmarket wallet, default 6.'
|
|
)
|
|
parser.add_option(
|
|
'-n',
|
|
'--no-upload',
|
|
action='store_true',
|
|
dest='no_upload',
|
|
default=False,
|
|
help="if set, we don't upload the new proposal to the servers"
|
|
)
|
|
parser.add_option(
|
|
'-f',
|
|
'--txfee',
|
|
action='store',
|
|
type='int',
|
|
dest='txfee',
|
|
default=-1,
|
|
help='Bitcoin miner tx_fee to use for transaction(s). A number higher '
|
|
'than 1000 is used as "satoshi per KB" tx fee. A number lower than that '
|
|
'uses the dynamic fee estimation of your blockchain provider as '
|
|
'confirmation target. This temporarily overrides the "tx_fees" setting '
|
|
'in your joinmarket.cfg. Works the same way as described in it. Check '
|
|
'it for examples.')
|
|
parser.add_option('-a',
|
|
'--amtmixdepths',
|
|
action='store',
|
|
type='int',
|
|
dest='amtmixdepths',
|
|
help='number of mixdepths in wallet, default 5',
|
|
default=5)
|
|
(options, args) = parser.parse_args()
|
|
snicker_plugin = JMPluginService("SNICKER")
|
|
load_program_config(config_path=options.datadir,
|
|
plugin_services=[snicker_plugin])
|
|
if len(args) != 5:
|
|
jmprint("Invalid arguments, see --help")
|
|
sys.exit(EXIT_ARGERROR)
|
|
wallet_name, hextx, input_index, output_index, net_transfer = args
|
|
input_index, output_index, net_transfer = [int(x) for x in [
|
|
input_index, output_index, net_transfer]]
|
|
check_regtest()
|
|
|
|
# If tx_fees are set manually by CLI argument, override joinmarket.cfg:
|
|
if int(options.txfee) > 0:
|
|
jm_single().config.set("POLICY", "tx_fees", str(options.txfee))
|
|
max_mix_depth = max([options.mixdepth, options.amtmixdepths - 1])
|
|
wallet_path = get_wallet_path(wallet_name, None)
|
|
wallet = open_test_wallet_maybe(
|
|
wallet_path, wallet_name, max_mix_depth,
|
|
wallet_password_stdin=options.wallet_password_stdin,
|
|
gap_limit=options.gaplimit)
|
|
wallet_service = WalletService(wallet)
|
|
if wallet_service.rpc_error:
|
|
sys.exit(EXIT_FAILURE)
|
|
snicker_plugin.start_plugin_logging(wallet_service)
|
|
# in this script, we need the wallet synced before
|
|
# logic processing for some paths, so do it now:
|
|
while not wallet_service.synced:
|
|
wallet_service.sync_wallet(fast=not options.recoversync)
|
|
# the sync call here will now be a no-op:
|
|
wallet_service.startService()
|
|
|
|
# now that the wallet is available, we can construct a proposal
|
|
# before encrypting it:
|
|
originating_tx = btc.CMutableTransaction.deserialize(hextobin(hextx))
|
|
txid1 = originating_tx.GetTxid()[::-1]
|
|
# the proposer wallet needs to choose a single utxo, from his selected
|
|
# mixdepth, that is bigger than the output amount of tx1 at the given
|
|
# index.
|
|
fee_est = estimate_tx_fee(2, 3, txtype=wallet_service.get_txtype())
|
|
amt_required = originating_tx.vout[output_index].nValue + fee_est
|
|
|
|
prop_utxo_dict = wallet_service.select_utxos(options.mixdepth,
|
|
amt_required)
|
|
prop_utxos = list(prop_utxo_dict)
|
|
prop_utxo_vals = [prop_utxo_dict[x] for x in prop_utxos]
|
|
# get the private key for that utxo
|
|
priv = wallet_service.get_key_from_addr(
|
|
wallet_service.script_to_addr(prop_utxo_vals[0]['script']))
|
|
# construct the arguments for the snicker proposal:
|
|
our_input_utxos = [btc.CMutableTxOut(x['value'],
|
|
x['script']) for x in prop_utxo_vals]
|
|
|
|
# destination must be a different mixdepth:
|
|
prop_destn_spk = wallet_service.get_new_script((
|
|
options.mixdepth + 1) % (wallet_service.mixdepth + 1), 1)
|
|
change_spk = wallet_service.get_new_script(options.mixdepth, 1)
|
|
their_input = (txid1, output_index)
|
|
# we also need to extract the pubkey of the chosen input from
|
|
# the witness; we vary this depending on our wallet type:
|
|
pubkey, msg = btc.extract_pubkey_from_witness(originating_tx, input_index)
|
|
if not pubkey:
|
|
log.error("Failed to extract pubkey from transaction: {}".format(msg))
|
|
sys.exit(EXIT_FAILURE)
|
|
encrypted_proposal = wallet_service.create_snicker_proposal(
|
|
prop_utxos, their_input,
|
|
our_input_utxos,
|
|
originating_tx.vout[output_index],
|
|
net_transfer,
|
|
fee_est,
|
|
priv,
|
|
pubkey,
|
|
prop_destn_spk,
|
|
change_spk,
|
|
version_byte=1) + b"," + bintohex(pubkey).encode('utf-8')
|
|
if options.no_upload:
|
|
jmprint(encrypted_proposal.decode("utf-8"))
|
|
sys.exit(EXIT_SUCCESS)
|
|
|
|
nodaemon = jm_single().config.getint("DAEMON", "no_daemon")
|
|
daemon = True if nodaemon == 1 else False
|
|
snicker_client = SNICKERPostingClient([encrypted_proposal])
|
|
servers = jm_single().config.get("SNICKER", "servers").split(",")
|
|
snicker_pf = SNICKERClientProtocolFactory(snicker_client, servers)
|
|
start_reactor(jm_single().config.get("DAEMON", "daemon_host"),
|
|
jm_single().config.getint("DAEMON", "daemon_port"),
|
|
snickerfactory=snicker_pf,
|
|
jm_coinjoin=False,
|
|
daemon=daemon)
|
|
|
|
class SNICKERPostingClient(object):
|
|
""" A client object which stores proposals
|
|
ready to be sent to the server/servers, and appends
|
|
proof of work to them according to the server's rules.
|
|
"""
|
|
def __init__(self, pre_nonce_proposals, info_callback=None,
|
|
end_requests_callback=None):
|
|
# the encrypted proposal without the nonce appended for PoW
|
|
self.pre_nonce_proposals = pre_nonce_proposals
|
|
|
|
self.proposals_with_nonce = []
|
|
|
|
# callback for conveying information messages
|
|
if not info_callback:
|
|
self.info_callback = self.default_info_callback
|
|
else:
|
|
self.info_callback = info_callback
|
|
|
|
# callback for action at the end of a set of
|
|
# submissions to servers; by default, this
|
|
# is "one-shot"; we submit to all servers in the
|
|
# config, then shut down the script.
|
|
if not end_requests_callback:
|
|
self.end_requests_callback = \
|
|
self.default_end_requests_callback
|
|
|
|
def default_end_requests_callback(self, response):
|
|
process_shutdown()
|
|
|
|
def default_info_callback(self, msg):
|
|
jmprint(msg)
|
|
|
|
def get_proposals(self, targetbits):
|
|
# the data sent to the server is base64encryptedtx,key,nonce; the nonce
|
|
# part is generated in get_pow().
|
|
for p in self.pre_nonce_proposals:
|
|
nonceval, preimage, niter = get_pow(p+b",", nbits=targetbits,
|
|
truncate=32)
|
|
log.debug("Got POW preimage: {}".format(preimage.decode("utf-8")))
|
|
if nonceval is None:
|
|
log.error("Failed to generate proof of work, message:{}".format(
|
|
preimage))
|
|
sys.exit(EXIT_FAILURE)
|
|
self.proposals_with_nonce.append(preimage)
|
|
return self.proposals_with_nonce
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
jmprint('done', "success")
|