You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

230 lines
9.1 KiB

#!/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, check_and_start_tor)
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_and_start_tor()
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")