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.
 
 
 
 

116 lines
4.4 KiB

#!/usr/bin/env python3
description="""Find SNICKER candidate transactions on
the blockchain.
Using a connection to Bitcoin Core, which allows retrieving
full blocks, this script will list the transaction IDs of
transactions that fit the pattern of SNICKER, as codified in
https://gist.github.com/AdamISZ/2c13fb5819bd469ca318156e2cf25d79
and as checked in the `jmbitcoin.snicker` module function
`is_snicker_tx`, and also optionally, transactions that fit the
pattern of Joinmarket coinjoins (see -j).
Pass a starting and finishing block value as argument. If the
finishing block is not provided, it is assumed to be the latest
block.
**Note that this is slow.**
This script does *NOT* require a wallet, but it does require
a connection to Core, so does not work with `no-blockchain`.
Note that this script obviates the need to have txindex enabled
in Bitcoin Core in order to get full transactions, since it
parses the raw blocks.
"""
import sys
from optparse import OptionParser
from jmbase import bintohex, EXIT_ARGERROR, jmprint
import jmbitcoin as btc
from jmclient import (jm_single, add_base_options, load_program_config,
check_regtest)
from jmclient.configure import get_log
log = get_log()
def found_str(ttype, tx, b):
return "Found {} transaction: {} in block: {}".format(
ttype, bintohex(tx.GetTxid()[::-1]), b)
def write_candidate_to_file(ttype, candidate, blocknum, unspents, filename):
""" Appends the details for the candidate
transaction to the chosen textfile.
"""
with open(filename, "a") as f:
f.write(found_str(ttype, candidate, blocknum) + "\n")
f.write(btc.human_readable_transaction(candidate)+"\n")
f.write("Full transaction hex for creating a proposal is "
"found in the above.\n")
f.write("The unspent indices are: " + " ".join(
(str(u) for u in unspents)) + "\n")
def main():
parser = OptionParser(
usage=
'usage: %prog [options] startingblock [endingblock]',
description=description
)
add_base_options(parser)
parser.add_option('-f',
'--filename',
action='store',
type='str',
dest='candidate_file_name',
help='filename to write details of candidate '
'transactions, default ./candidates.txt',
default='candidates.txt')
parser.add_option(
'-j',
'--include-jm',
action='store_true',
dest='include_joinmarket',
default=True,
help="scan for Joinmarket coinjoin outputs, as well as SNICKER.")
(options, args) = parser.parse_args()
load_program_config(config_path=options.datadir)
if len(args) not in [1,2]:
log.error("Invalid arguments, see --help")
sys.exit(EXIT_ARGERROR)
startblock = int(args[0])
if len(args) == 1:
endblock = jm_single().bc_interface.get_current_block_height()
else:
endblock = int(args[1])
check_regtest()
for b in range(startblock, endblock + 1):
block = jm_single().bc_interface.get_block(b)
for t in btc.get_transactions_in_block(block):
if btc.is_snicker_tx(t):
log.info(found_str("SNICKER", t, b))
# get list of unspent outputs; if empty, skip,
# otherwise, persist to candidate file with unspents
# marked.
unspents = jm_single().bc_interface.get_unspent_indices(t)
if len(unspents) == 0:
continue
write_candidate_to_file("SNICKER", t, b, unspents,
options.candidate_file_name)
# note elif avoids wasting computation if we already found SNICKER:
elif options.include_joinmarket:
cj_amount, n = btc.is_jm_tx(t)
# here we don't care about the stats; the tx is printed anyway.
if cj_amount:
log.info(found_str("Joinmarket coinjoin", t, b))
unspents = jm_single().bc_interface.get_unspent_indices(t)
if len(unspents) == 0:
continue
write_candidate_to_file("Joinmarket coinjoin", t, b,
unspents, options.candidate_file_name)
log.info("Finished processing block: {}".format(b))
if __name__ == "__main__":
main()
jmprint('done', "success")