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.
 
 
 
 

210 lines
8.9 KiB

#!/usr/bin/env python3
description="""Make fake SNICKER transactions to aid discovery.
Use this script to send money to yourself in a transaction which
fits the format of SNICKER v1 (so it will have two equal sized
outputs and a change output, also obeying the other minor rules
for SNICKER).
Having done this your transaction will be picked up by blockchain
scanners looking for the SNICKER "fingerprint", allowing them
to propose coinjoins with your coins.
The transaction is generated with at least TWO utxos from your chosen
source mixdepth/account (-m), so it must contain at least two.
The reason for using one account, not two, is to prevent violating
the principle of not co-spending from different accounts; even though
this is a simulated coinjoin, it may be deducible that it is only really
a *signalling* fake coinjoin, so it is better not to violate the principle.
"""
import sys
import random
from optparse import OptionParser
from jmbase import bintohex, jmprint, EXIT_ARGERROR, EXIT_FAILURE
import jmbitcoin as btc
from jmclient import (jm_single, load_program_config, check_regtest,
estimate_tx_fee, add_base_options, get_wallet_path,
open_test_wallet_maybe, WalletService, JMPluginService)
from jmclient.support import select_greedy, NotEnoughFundsException
from jmclient.configure import get_log
log = get_log()
def main():
parser = OptionParser(
usage=
'usage: %prog [options] walletname',
description=description
)
add_base_options(parser)
parser.add_option('-m',
'--mixdepth',
action='store',
type='int',
dest='mixdepth',
help='mixdepth/account, 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(
'-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)
parser.add_option('-N',
'--net-transfer',
action='store',
type='int',
dest='net_transfer',
help='how many sats are sent to the "receiver", default randomised.',
default=-1000001)
(options, args) = parser.parse_args()
snicker_plugin = JMPluginService("SNICKER")
load_program_config(config_path=options.datadir,
plugin_services=[snicker_plugin])
if len(args) != 1:
log.error("Invalid arguments, see --help")
sys.exit(EXIT_ARGERROR)
wallet_name = args[0]
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()
fee_est = estimate_tx_fee(2, 3, txtype=wallet_service.get_txtype())
# first, order the utxos in the mixepth by size. Then (this is the
# simplest algorithm; we could be more sophisticated), choose the
# *second* largest utxo as the receiver utxo; this ensures that we
# have enough for the proposer to cover. We consume utxos greedily,
# meaning we'll at least some of the time, be consolidating.
utxo_dict = wallet_service.get_utxos_by_mixdepth()[options.mixdepth]
if not len(utxo_dict) >= 2:
log.error("Cannot create fake SNICKER tx without at least two utxos, quitting")
sys.exit(EXIT_ARGERROR)
# sort utxos by size
sorted_utxos = sorted(list(utxo_dict.keys()),
key=lambda k: utxo_dict[k]['value'],
reverse=True)
# receiver is the second largest:
receiver_utxo = sorted_utxos[1]
receiver_utxo_val = utxo_dict[receiver_utxo]
# gather the other utxos into a list to select from:
nonreceiver_utxos = [sorted_utxos[0]] + sorted_utxos[2:]
# get the net transfer in our fake coinjoin:
if options.net_transfer < -1000001:
log.error("Net transfer must be greater than negative 1M sats")
sys.exit(EXIT_ARGERROR)
if options.net_transfer == -1000001:
# default; low-ish is more realistic and avoids problems
# with dusty utxos
options.net_transfer = random.randint(-1000, 1000)
# select enough to cover: receiver value + fee + transfer + breathing room
# we select relatively greedily to support consolidation, since
# this transaction does not pretend to isolate the coins.
try:
available = [{'utxo': utxo, 'value': utxo_dict[utxo]["value"]}
for utxo in nonreceiver_utxos]
# selection algos return [{"utxo":..,"value":..}]:
prop_utxos = {x["utxo"] for x in select_greedy(available,
receiver_utxo_val["value"] + fee_est + options.net_transfer + 1000)}
prop_utxos = list(prop_utxos)
prop_utxo_vals = [utxo_dict[prop_utxo] for prop_utxo in prop_utxos]
except NotEnoughFundsException as e:
log.error(repr(e))
sys.exit(EXIT_FAILURE)
# Due to the fake nature of this transaction, and its distinguishability
# (not only in trivial output pattern, but also in subset-sum), there
# is little advantage in making it use different output mixdepths, so
# here to prevent fragmentation, everything is kept in the same mixdepth.
receiver_addr, proposer_addr, change_addr = (wallet_service.script_to_addr(
wallet_service.get_new_script(options.mixdepth, 1)) for _ in range(3))
# persist index update:
wallet_service.save_wallet()
outputs = btc.construct_snicker_outputs(
sum([x["value"] for x in prop_utxo_vals]),
receiver_utxo_val["value"],
receiver_addr,
proposer_addr,
change_addr,
fee_est,
options.net_transfer)
tx = btc.make_shuffled_tx(prop_utxos + [receiver_utxo],
outputs,
version=2,
locktime=0)
# before signing, check we satisfied the criteria, otherwise
# this is pointless!
if not btc.is_snicker_tx(tx):
log.error("Code error, created non-SNICKER tx, not signing.")
sys.exit(EXIT_FAILURE)
# sign all inputs
# scripts: {input_index: (output_script, amount)}
our_inputs = {}
for index, ins in enumerate(tx.vin):
utxo = (ins.prevout.hash[::-1], ins.prevout.n)
script = utxo_dict[utxo]['script']
amount = utxo_dict[utxo]['value']
our_inputs[index] = (script, amount)
success, msg = wallet_service.sign_tx(tx, our_inputs)
if not success:
log.error("Failed to sign transaction: " + msg)
sys.exit(EXIT_FAILURE)
# TODO condition on automatic brdcst or not
if not jm_single().bc_interface.pushtx(tx.serialize()):
# this represents an error about state (or conceivably,
# an ultra-short window in which the spent utxo was
# consumed in another transaction), but not really
# an internal logic error, so we do NOT return False
log.error("Failed to broadcast fake SNICKER coinjoin: " +\
bintohex(tx.GetTxid()[::-1]))
log.info(btc.human_readable_transaction(tx))
sys.exit(EXIT_FAILURE)
log.info("Successfully broadcast fake SNICKER coinjoin: " +\
bintohex(tx.GetTxid()[::-1]))
if __name__ == "__main__":
main()
jmprint('done', "success")