Last active
April 3, 2023 20:09
-
-
Save AdamISZ/639333568ad98f50354c933057ee32a4 to your computer and use it in GitHub Desktop.
Simple Python script to find Joinmarket type transactions in blocks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
from __future__ import print_function | |
""" | |
Find/count JM transactions in blocks. | |
Ensure your joinmarket-clientserver virtualenv (jmvenv) is activated, | |
make sure your Bitcoin Core node is available and joinmarket.cfg is appropriately set. | |
Pass start and end block number: | |
`python jmtxfinder.py 400000 400200`. | |
Note it's stupid slow, requiring 10-15 seconds on my machine for a normal-ish block. | |
Outputs results to console and to jmtxfinder_results.txt and to a csv file. | |
Ignores coinjoins with < 3 equal sized outputs; vanishingly few Joinmarket txs | |
have that, and it's not a "proper" Joinmarket join anyway. | |
Also ignores coinjoins with sizes sub-100000 sats, these are often | |
false positives (and not economically relevant anyway). | |
""" | |
import binascii | |
import os | |
import sys | |
import jmbitcoin as btc | |
from jmclient import (load_program_config, jm_single, | |
RegtestBitcoinCoreInterface, | |
BitcoinCoreInterface) | |
outfile = "jmtxfinder_results.txt" | |
csvfile = "jmtxf_summ.csv" | |
def read_length(x): | |
bx = binascii.unhexlify(x) | |
val = bx[0] | |
if val < 253: | |
n = 1 | |
elif val == 253: | |
val = btc.decode(bx[1:3][::-1], 256) | |
n = 3 | |
elif val == 254: | |
val = btc.decode(bx[1:5][::-1], 256) | |
n = 5 | |
elif val == 255: | |
val = btc.decode(bx[1:9][::-1], 256) | |
n = 9 | |
else: | |
assert False | |
return (val, n) | |
def assumed_cj_out_num(nout): | |
"""Return the value ceil(nout/2) | |
""" | |
x = nout//2 | |
if nout %2: return x+1 | |
return x | |
def most_common_value(x): | |
return max(set(x), key=x.count) | |
def is_jm_tx(tx): | |
#rules are: nins >= number of coinjoin outs (equal sized) | |
#non-equal outs = coinjoin outs or coinjoin outs -1 | |
#at least 3 coinjoin outs (2 technically possible but excluded) | |
#also possible to try to get clever about fees, but won't bother | |
#also BlockSci's algo additionally addresses subset sum, so will | |
#give better quality data, but again keeping it simple for now. | |
nouts = len(tx["outs"]) | |
nins = len(tx["ins"]) | |
assumed_coinjoin_outs = assumed_cj_out_num(nouts) | |
if assumed_coinjoin_outs < 3: | |
return False | |
if nins < assumed_coinjoin_outs: | |
return False | |
outvals = [x["value"] for x in tx["outs"]] | |
mcov = most_common_value(outvals) | |
if mcov < 100000: | |
return False | |
cjoutvals = [x for x in outvals if x == mcov] | |
if len(cjoutvals) != assumed_coinjoin_outs: | |
return False | |
return [mcov, nouts] | |
def get_transactions_from_block(blockheight): | |
block = jm_single().bc_interface.get_block(blockheight) | |
txdata = block[160:] | |
ntx, nbytes = read_length(txdata) | |
print("Got nbytes: ", nbytes) | |
txdata = txdata[nbytes*2:] | |
found_txs = [] | |
for i in range(ntx): | |
tx = btc.deserialize(txdata) | |
if i != 0: | |
found_txs.append(tx) | |
len_tx = len(btc.serialize(tx)) | |
txdata = txdata[len_tx:] | |
return found_txs | |
def tee(x, f): | |
print(x) | |
f.write((x + "\n").encode()) | |
if __name__ == "__main__": | |
if len(sys.argv) < 3: | |
print("error, syntax: python jmtxfinder.py startblocknumber endblocknumber") | |
sys.exit(1) | |
start, stop = [int(x) for x in sys.argv[1:3]] | |
load_program_config() | |
if stop < 0: | |
#flag to just dump all serialized transactions in this block | |
txs = get_transactions_from_block(start) | |
with open("testtxs-"+str(start)+".txt", "wb") as f: | |
for tx in txs: | |
f.write(btc.serialize(tx)+"\n") | |
print("done") | |
exit(0) | |
with open(outfile, "ab+") as f: | |
with open(csvfile, "ab+") as f2: | |
for blocknum in range(start, stop + 1): | |
txs = get_transactions_from_block(blocknum) | |
header = "*****************************************\n" + str(blocknum) + \ | |
"\n" + str(len(txs)) + " transactions.\n" + \ | |
"*****************************************" | |
tee(header, f) | |
found = 0 | |
for t in txs: | |
res = is_jm_tx(t) | |
if res: | |
tee(btc.txhash(btc.serialize(t)) + " : " + str(res[0]) + " " + str(res[1]), f) | |
found += 1 | |
f2.write((",".join([str(blocknum), btc.txhash(btc.serialize(t)), str(res[0]), str(res[1])])+"\n").encode()) | |
tee("Found in total: " + str(found) + " joinmarket style transactions.", f) | |
print("done") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment