mirror of
https://github.com/LORDBABUINO/stealth.git
synced 2026-05-24 08:34:46 -07:00
feat: add vuln reproduction and detection scripts
This commit is contained in:
committed by
LORDBABUINO
parent
1f7ecf321c
commit
fb5381d7b1
29
backend/script/README.md
Normal file
29
backend/script/README.md
Normal file
@@ -0,0 +1,29 @@
|
||||
pass: "aW2u~fYiuLu3)%a"
|
||||
|
||||
METAMASK:
|
||||
1.twenty
|
||||
2.series
|
||||
3.camera
|
||||
4.invite
|
||||
5.dismiss
|
||||
6.gentle
|
||||
7.dose
|
||||
8.hotel
|
||||
9.circle
|
||||
10.eight
|
||||
11.rotate
|
||||
12.assault
|
||||
|
||||
ENKRYPT:
|
||||
damage
|
||||
scare
|
||||
aerobic
|
||||
eagle
|
||||
club
|
||||
typical
|
||||
cricket
|
||||
kick
|
||||
jaguar
|
||||
paddle
|
||||
void
|
||||
dinner
|
||||
BIN
backend/script/__pycache__/bitcoin_rpc.cpython-310.pyc
Normal file
BIN
backend/script/__pycache__/bitcoin_rpc.cpython-310.pyc
Normal file
Binary file not shown.
142
backend/script/bitcoin_rpc.py
Normal file
142
backend/script/bitcoin_rpc.py
Normal file
@@ -0,0 +1,142 @@
|
||||
"""
|
||||
bitcoin_rpc.py — Thin wrapper around bitcoin-cli for Python tests.
|
||||
Uses subprocess calls to bitcoin-cli -regtest.
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import time
|
||||
import os
|
||||
|
||||
CLI = "bitcoin-cli"
|
||||
SIGNET_ARGS = [CLI, "-regtest"]
|
||||
|
||||
def cli(*args, wallet=None):
|
||||
"""Call bitcoin-cli -regtest [wallet] <args> and return parsed JSON or string."""
|
||||
cmd = list(SIGNET_ARGS)
|
||||
if wallet:
|
||||
cmd.append(f"-rpcwallet={wallet}")
|
||||
cmd.extend(str(a) for a in args)
|
||||
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"bitcoin-cli error: {result.stderr.strip()}\n cmd: {' '.join(cmd)}")
|
||||
|
||||
output = result.stdout.strip()
|
||||
if not output:
|
||||
return None
|
||||
try:
|
||||
return json.loads(output)
|
||||
except json.JSONDecodeError:
|
||||
return output
|
||||
|
||||
|
||||
def mine_blocks(n=1):
|
||||
"""Mine n blocks on regtest using generatetoaddress."""
|
||||
miner_addr = cli("getnewaddress", "", "bech32", wallet="miner")
|
||||
cli("generatetoaddress", n, miner_addr)
|
||||
return int(cli("getblockcount"))
|
||||
|
||||
|
||||
def fund_wallet(wallet_name, amount=1.0, from_wallet="miner"):
|
||||
"""Send `amount` BTC from `from_wallet` to a new address in `wallet_name`."""
|
||||
addr = cli("getnewaddress", "", "bech32", wallet=wallet_name)
|
||||
txid = cli("sendtoaddress", addr, f"{amount:.8f}", wallet=from_wallet)
|
||||
return txid, addr
|
||||
|
||||
|
||||
def wait_for_mempool_empty(timeout=60):
|
||||
"""Wait until mempool is empty (all txs mined)."""
|
||||
for _ in range(timeout * 2):
|
||||
info = cli("getmempoolinfo")
|
||||
if info["size"] == 0:
|
||||
return True
|
||||
time.sleep(0.5)
|
||||
return False
|
||||
|
||||
|
||||
def get_tx(txid):
|
||||
"""Get decoded transaction."""
|
||||
return cli("getrawtransaction", txid, "true")
|
||||
|
||||
|
||||
def get_utxos(wallet_name, min_conf=0):
|
||||
"""List unspent outputs for a wallet."""
|
||||
return cli("listunspent", min_conf, wallet=wallet_name)
|
||||
|
||||
|
||||
def get_balance(wallet_name):
|
||||
"""Get wallet balance."""
|
||||
return float(cli("getbalance", wallet=wallet_name))
|
||||
|
||||
|
||||
def send_raw(hex_tx):
|
||||
"""Broadcast a raw transaction."""
|
||||
return cli("sendrawtransaction", hex_tx)
|
||||
|
||||
|
||||
def create_funded_psbt(wallet_name, inputs, outputs, options=None):
|
||||
"""Create a funded PSBT."""
|
||||
args = ["walletcreatefundedpsbt", json.dumps(inputs), json.dumps(outputs), 0]
|
||||
if options:
|
||||
args.append(json.dumps(options))
|
||||
return cli(*args, wallet=wallet_name)
|
||||
|
||||
|
||||
def process_psbt(wallet_name, psbt):
|
||||
"""Sign a PSBT."""
|
||||
return cli("walletprocesspsbt", psbt, wallet=wallet_name)
|
||||
|
||||
|
||||
def finalize_psbt(psbt):
|
||||
"""Finalize a PSBT."""
|
||||
return cli("finalizepsbt", psbt)
|
||||
|
||||
|
||||
def decode_psbt(psbt):
|
||||
"""Decode a PSBT."""
|
||||
return cli("decodepsbt", psbt)
|
||||
|
||||
|
||||
def create_raw_tx(inputs, outputs):
|
||||
"""Create a raw transaction."""
|
||||
return cli("createrawtransaction", json.dumps(inputs), json.dumps(outputs))
|
||||
|
||||
|
||||
def sign_raw_tx(wallet_name, hex_tx):
|
||||
"""Sign a raw transaction."""
|
||||
return cli("signrawtransactionwithwallet", hex_tx, wallet=wallet_name)
|
||||
|
||||
|
||||
def decode_raw_tx(hex_tx):
|
||||
"""Decode a raw transaction."""
|
||||
return cli("decoderawtransaction", hex_tx)
|
||||
|
||||
|
||||
def get_block_count():
|
||||
"""Get current block height."""
|
||||
return int(cli("getblockcount"))
|
||||
|
||||
|
||||
def get_new_address(wallet_name, addr_type="bech32"):
|
||||
"""Get a new address."""
|
||||
return cli("getnewaddress", "", addr_type, wallet=wallet_name)
|
||||
|
||||
|
||||
def send_to_address(wallet_name, address, amount):
|
||||
"""Send BTC to an address."""
|
||||
return cli("sendtoaddress", address, f"{amount:.8f}", wallet=wallet_name)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Testing RPC connection...")
|
||||
info = cli("getblockchaininfo")
|
||||
print(f" Chain: {info['chain']}")
|
||||
print(f" Blocks: {info['blocks']}")
|
||||
|
||||
for w in ["miner", "alice", "bob", "carol", "exchange", "risky"]:
|
||||
try:
|
||||
bal = get_balance(w)
|
||||
print(f" {w}: {bal} BTC")
|
||||
except Exception as e:
|
||||
print(f" {w}: ERROR - {e}")
|
||||
1141
backend/script/detect.py
Normal file
1141
backend/script/detect.py
Normal file
File diff suppressed because it is too large
Load Diff
28
backend/script/mine_blocks.sh
Executable file
28
backend/script/mine_blocks.sh
Executable file
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env bash
|
||||
# mine_blocks.sh — Mine N blocks on the custom Signet
|
||||
set -euo pipefail
|
||||
|
||||
N="${1:-1}"
|
||||
source "$HOME/.bitcoin/signet_keys.env"
|
||||
|
||||
MINER="/home/renato/Desktop/bitcoin/bitcoin/contrib/signet/miner"
|
||||
GRIND="bitcoin-util grind"
|
||||
CLI="bitcoin-cli -signet"
|
||||
|
||||
CURRENT=$($CLI getblockcount)
|
||||
TARGET=$((CURRENT + N))
|
||||
echo "Mining $N blocks (from $CURRENT to $TARGET)..."
|
||||
|
||||
BLOCK_TIME=$(date +%s)
|
||||
for i in $(seq 1 $N); do
|
||||
BLOCK_TIME=$((BLOCK_TIME + 1))
|
||||
$MINER \
|
||||
--cli="bitcoin-cli -rpcwallet=miner" \
|
||||
generate \
|
||||
--grind-cmd="$GRIND" \
|
||||
--address="$MINER_ADDR" \
|
||||
--min-nbits \
|
||||
--set-block-time="$BLOCK_TIME" \
|
||||
2>&1 >/dev/null
|
||||
done
|
||||
echo "Done. Block height: $($CLI getblockcount)"
|
||||
1
backend/script/openconf.sh
Executable file
1
backend/script/openconf.sh
Executable file
@@ -0,0 +1 @@
|
||||
code ~/.bitcoin/bitcoin.conf
|
||||
418
backend/script/reproduce.py
Normal file
418
backend/script/reproduce.py
Normal file
@@ -0,0 +1,418 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
reproduce.py
|
||||
============
|
||||
Reproduces 12 Bitcoin privacy vulnerabilities on a local custom Signet.
|
||||
Each run creates NEW on-chain transactions that exhibit the vulnerability.
|
||||
No detection logic — that lives in detect.py.
|
||||
|
||||
Usage:
|
||||
python3 reproduce.py # Create all 12 vulnerability scenarios
|
||||
python3 reproduce.py -k 3 # Create only vulnerability 3
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
import time
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from bitcoin_rpc import (
|
||||
cli, mine_blocks, get_tx, get_utxos, get_balance,
|
||||
get_new_address, send_to_address, create_raw_tx, sign_raw_tx,
|
||||
send_raw, get_block_count, create_funded_psbt,
|
||||
process_psbt, finalize_psbt,
|
||||
)
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# Formatting helpers
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
G = "\033[92m"; Y = "\033[93m"; C = "\033[96m"; B = "\033[1m"; R = "\033[0m"
|
||||
|
||||
def header(num, title):
|
||||
print(f"\n{'═'*78}")
|
||||
print(f"{B}{C} REPRODUCE {num}: {title}{R}")
|
||||
print(f"{'═'*78}")
|
||||
|
||||
def ok(msg):
|
||||
print(f" {G}✓{R} {msg}")
|
||||
|
||||
def info(msg):
|
||||
print(f" {Y}ℹ{R} {msg}")
|
||||
|
||||
def ensure_funds(wallet, min_btc=0.5):
|
||||
bal = get_balance(wallet)
|
||||
if bal < min_btc:
|
||||
addr = get_new_address(wallet, "bech32")
|
||||
send_to_address("miner", addr, min_btc + 0.5)
|
||||
mine_blocks(1)
|
||||
|
||||
def mine_and_confirm():
|
||||
mine_blocks(1)
|
||||
time.sleep(0.5)
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# 1. Address Reuse
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
def reproduce_01():
|
||||
header(1, "Address Reuse")
|
||||
ensure_funds("bob", 1.0)
|
||||
reused_addr = get_new_address("alice", "bech32")
|
||||
txid1 = send_to_address("bob", reused_addr, 0.01)
|
||||
txid2 = send_to_address("bob", reused_addr, 0.02)
|
||||
mine_and_confirm()
|
||||
ok(f"Sent to same address {reused_addr} twice: TX {txid1[:16]}… and {txid2[:16]}…")
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# 2. Multi-input / CIOH
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
def reproduce_02():
|
||||
header(2, "Multi-input / CIOH (Common Input Ownership Heuristic)")
|
||||
ensure_funds("bob", 2.0)
|
||||
for _ in range(5):
|
||||
addr = get_new_address("alice", "bech32")
|
||||
send_to_address("bob", addr, 0.005)
|
||||
mine_and_confirm()
|
||||
|
||||
utxos = get_utxos("alice", 1)
|
||||
small = [u for u in utxos if 0.004 < u["amount"] < 0.006][:5]
|
||||
if len(small) < 2:
|
||||
info("Not enough small UTXOs, skipping consolidation step")
|
||||
return
|
||||
inputs = [{"txid": u["txid"], "vout": u["vout"]} for u in small]
|
||||
dest = get_new_address("bob", "bech32")
|
||||
total = sum(u["amount"] for u in small)
|
||||
psbt_result = create_funded_psbt(
|
||||
"alice", inputs, [{dest: round(total - 0.001, 8)}],
|
||||
{"subtractFeeFromOutputs": [0], "add_inputs": False}
|
||||
)
|
||||
signed = process_psbt("alice", psbt_result["psbt"])
|
||||
final = finalize_psbt(signed["psbt"])
|
||||
txid = send_raw(final["hex"])
|
||||
mine_and_confirm()
|
||||
ok(f"Consolidated {len(small)} inputs in TX {txid[:16]}… (CIOH trigger)")
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# 3. Dust UTXO Detection
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
def reproduce_03():
|
||||
header(3, "Dust UTXO Detection")
|
||||
ensure_funds("bob", 1.0)
|
||||
dust1 = get_new_address("alice", "bech32")
|
||||
dust2 = get_new_address("alice", "bech32")
|
||||
bob_utxos = get_utxos("bob", 1)
|
||||
big = max(bob_utxos, key=lambda u: u["amount"])
|
||||
change = get_new_address("bob", "bech32")
|
||||
change_amt = round(big["amount"] - 0.00001000 - 0.00000546 - 0.0001, 8)
|
||||
raw = create_raw_tx(
|
||||
[{"txid": big["txid"], "vout": big["vout"]}],
|
||||
[{dust1: 0.00001000}, {dust2: 0.00000546}, {change: change_amt}]
|
||||
)
|
||||
signed = sign_raw_tx("bob", raw)
|
||||
txid = send_raw(signed["hex"])
|
||||
mine_and_confirm()
|
||||
ok(f"Created 1000-sat and 546-sat dust outputs to Alice in TX {txid[:16]}…")
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# 4. Spending Dust with Normal Inputs
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
def reproduce_04():
|
||||
header(4, "Spending Dust with Normal Inputs")
|
||||
ensure_funds("alice", 0.5)
|
||||
utxos = get_utxos("alice", 1)
|
||||
dust_utxos = [u for u in utxos if u["amount"] <= 0.00001]
|
||||
normal_utxos = [u for u in utxos if u["amount"] > 0.001]
|
||||
|
||||
if not dust_utxos:
|
||||
info("No dust UTXOs, creating one first…")
|
||||
ensure_funds("bob", 1.0)
|
||||
a = get_new_address("alice", "bech32")
|
||||
bu = get_utxos("bob", 1)
|
||||
big = max(bu, key=lambda u: u["amount"])
|
||||
ch = get_new_address("bob", "bech32")
|
||||
raw = create_raw_tx(
|
||||
[{"txid": big["txid"], "vout": big["vout"]}],
|
||||
[{a: 0.00001000}, {ch: round(big["amount"] - 0.00001 - 0.0001, 8)}]
|
||||
)
|
||||
signed = sign_raw_tx("bob", raw)
|
||||
send_raw(signed["hex"])
|
||||
mine_and_confirm()
|
||||
utxos = get_utxos("alice", 1)
|
||||
dust_utxos = [u for u in utxos if u["amount"] <= 0.00001]
|
||||
normal_utxos = [u for u in utxos if u["amount"] > 0.001]
|
||||
|
||||
if not normal_utxos:
|
||||
ensure_funds("alice", 0.5)
|
||||
mine_and_confirm()
|
||||
utxos = get_utxos("alice", 1)
|
||||
normal_utxos = [u for u in utxos if u["amount"] > 0.001]
|
||||
|
||||
dust = dust_utxos[0]
|
||||
normal = normal_utxos[0]
|
||||
dest = get_new_address("bob", "bech32")
|
||||
total = dust["amount"] + normal["amount"]
|
||||
raw = create_raw_tx(
|
||||
[{"txid": dust["txid"], "vout": dust["vout"]},
|
||||
{"txid": normal["txid"], "vout": normal["vout"]}],
|
||||
[{dest: round(total - 0.0001, 8)}]
|
||||
)
|
||||
signed = sign_raw_tx("alice", raw)
|
||||
txid = send_raw(signed["hex"])
|
||||
mine_and_confirm()
|
||||
ok(f"Spent dust ({int(dust['amount']*1e8)} sats) + normal ({normal['amount']:.8f}) together in TX {txid[:16]}…")
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# 5. Change Detection
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
def reproduce_05():
|
||||
header(5, "Change Detection — Round Payment")
|
||||
ensure_funds("alice", 1.0)
|
||||
bob_addr = get_new_address("bob", "bech32")
|
||||
txid = send_to_address("alice", bob_addr, 0.05)
|
||||
mine_and_confirm()
|
||||
ok(f"Alice paid Bob 0.05 BTC (round amount) in TX {txid[:16]}… — change output is obvious")
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# 6. Consolidation Origin
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
def reproduce_06():
|
||||
header(6, "Consolidation Origin")
|
||||
ensure_funds("bob", 2.0)
|
||||
for _ in range(4):
|
||||
addr = get_new_address("alice", "bech32")
|
||||
send_to_address("bob", addr, 0.003)
|
||||
mine_and_confirm()
|
||||
|
||||
utxos = get_utxos("alice", 1)
|
||||
small = [u for u in utxos if 0.002 < u["amount"] < 0.004][:4]
|
||||
if len(small) < 3:
|
||||
info(f"Only {len(small)} small UTXOs, creating more…")
|
||||
for _ in range(4):
|
||||
addr = get_new_address("alice", "bech32")
|
||||
send_to_address("bob", addr, 0.003)
|
||||
mine_and_confirm()
|
||||
utxos = get_utxos("alice", 1)
|
||||
small = [u for u in utxos if 0.002 < u["amount"] < 0.004][:4]
|
||||
|
||||
inputs = [{"txid": u["txid"], "vout": u["vout"]} for u in small]
|
||||
consol_addr = get_new_address("alice", "bech32")
|
||||
total = sum(u["amount"] for u in small)
|
||||
raw = create_raw_tx(inputs, [{consol_addr: round(total - 0.0001, 8)}])
|
||||
signed = sign_raw_tx("alice", raw)
|
||||
consol_txid = send_raw(signed["hex"])
|
||||
mine_and_confirm()
|
||||
ok(f"Consolidated {len(small)} UTXOs → 1 in TX {consol_txid[:16]}…")
|
||||
|
||||
# Now spend the consolidated output
|
||||
utxos = get_utxos("alice", 1)
|
||||
cu = [u for u in utxos if u["txid"] == consol_txid]
|
||||
if cu:
|
||||
dest = get_new_address("carol", "bech32")
|
||||
raw = create_raw_tx(
|
||||
[{"txid": cu[0]["txid"], "vout": cu[0]["vout"]}],
|
||||
[{dest: round(cu[0]["amount"] - 0.0001, 8)}]
|
||||
)
|
||||
signed = sign_raw_tx("alice", raw)
|
||||
txid2 = send_raw(signed["hex"])
|
||||
mine_and_confirm()
|
||||
ok(f"Spent consolidated UTXO in TX {txid2[:16]}… — carries full cluster history")
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# 7. Script Type Mixing
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
def reproduce_07():
|
||||
header(7, "Script Type Mixing")
|
||||
ensure_funds("bob", 2.0)
|
||||
wpkh = get_new_address("alice", "bech32")
|
||||
tr = get_new_address("alice", "bech32m")
|
||||
send_to_address("bob", wpkh, 0.005)
|
||||
send_to_address("bob", tr, 0.005)
|
||||
mine_and_confirm()
|
||||
|
||||
utxos = get_utxos("alice", 1)
|
||||
def is_wpkh(addr):
|
||||
return addr and not addr.startswith(("tb1p","bc1p","bcrt1p")) and addr.startswith(("tb1q","bc1q","bcrt1q"))
|
||||
def is_tr(addr):
|
||||
return addr and addr.startswith(("tb1p","bc1p","bcrt1p"))
|
||||
wu = next((u for u in utxos if is_wpkh(u.get("address","")) and u["amount"] >= 0.004), None)
|
||||
tu = next((u for u in utxos if is_tr(u.get("address","")) and u["amount"] >= 0.004), None)
|
||||
if not wu or not tu:
|
||||
info("Could not find both UTXO types")
|
||||
return
|
||||
dest = get_new_address("bob", "bech32")
|
||||
total = wu["amount"] + tu["amount"]
|
||||
raw = create_raw_tx(
|
||||
[{"txid": wu["txid"], "vout": wu["vout"]},
|
||||
{"txid": tu["txid"], "vout": tu["vout"]}],
|
||||
[{dest: round(total - 0.0002, 8)}]
|
||||
)
|
||||
signed = sign_raw_tx("alice", raw)
|
||||
txid = send_raw(signed["hex"])
|
||||
mine_and_confirm()
|
||||
ok(f"Mixed P2WPKH + P2TR inputs in TX {txid[:16]}… — script type fingerprint")
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# 8. Cluster Merge
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
def reproduce_08():
|
||||
header(8, "Cluster Merge")
|
||||
ensure_funds("bob", 2.0)
|
||||
ensure_funds("carol", 2.0)
|
||||
a_addr = get_new_address("alice", "bech32")
|
||||
b_addr = get_new_address("alice", "bech32")
|
||||
txid_a = send_to_address("bob", a_addr, 0.004)
|
||||
txid_b = send_to_address("carol", b_addr, 0.004)
|
||||
mine_and_confirm()
|
||||
|
||||
utxos = get_utxos("alice", 1)
|
||||
ua = next((u for u in utxos if u["txid"] == txid_a), None)
|
||||
ub = next((u for u in utxos if u["txid"] == txid_b), None)
|
||||
if not ua: ua = next((u for u in utxos if u.get("address") == a_addr), None)
|
||||
if not ub: ub = next((u for u in utxos if u.get("address") == b_addr), None)
|
||||
if not ua or not ub:
|
||||
info("Could not find both cluster UTXOs")
|
||||
return
|
||||
dest = get_new_address("bob", "bech32")
|
||||
total = ua["amount"] + ub["amount"]
|
||||
raw = create_raw_tx(
|
||||
[{"txid": ua["txid"], "vout": ua["vout"]},
|
||||
{"txid": ub["txid"], "vout": ub["vout"]}],
|
||||
[{dest: round(total - 0.0002, 8)}]
|
||||
)
|
||||
signed = sign_raw_tx("alice", raw)
|
||||
txid = send_raw(signed["hex"])
|
||||
mine_and_confirm()
|
||||
ok(f"Merged Bob-cluster and Carol-cluster UTXOs in TX {txid[:16]}…")
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# 9. Lookback Depth
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
def reproduce_09():
|
||||
header(9, "Lookback Depth / UTXO Age")
|
||||
old_addr = get_new_address("alice", "bech32")
|
||||
send_to_address("miner", old_addr, 0.01)
|
||||
mine_blocks(20)
|
||||
new_addr = get_new_address("alice", "bech32")
|
||||
send_to_address("miner", new_addr, 0.01)
|
||||
mine_and_confirm()
|
||||
ok(f"Created old UTXO (20+ blocks ago) and new UTXO (just now) for Alice")
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# 10. Exchange Origin
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
def reproduce_10():
|
||||
header(10, "Exchange Origin — Batch Withdrawal")
|
||||
ensure_funds("exchange", 5.0)
|
||||
batch = {}
|
||||
wallets = ["alice", "bob", "carol", "alice", "bob", "carol", "alice", "bob"]
|
||||
for i in range(8):
|
||||
addr = get_new_address(wallets[i], "bech32")
|
||||
batch[addr] = round(0.01 + i * 0.001, 8)
|
||||
txid = cli("sendmany", "", json.dumps(batch), wallet="exchange")
|
||||
mine_and_confirm()
|
||||
ok(f"Exchange batch withdrawal to 8 recipients in TX {txid[:16]}…")
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# 11. Tainted UTXOs
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
def reproduce_11():
|
||||
header(11, "Tainted UTXOs / Dirty Money")
|
||||
ensure_funds("risky", 2.0)
|
||||
ensure_funds("bob", 1.0)
|
||||
ta = get_new_address("alice", "bech32")
|
||||
taint_txid = send_to_address("risky", ta, 0.01)
|
||||
ca = get_new_address("alice", "bech32")
|
||||
clean_txid = send_to_address("bob", ca, 0.01)
|
||||
mine_and_confirm()
|
||||
|
||||
utxos = get_utxos("alice", 1)
|
||||
tu = next((u for u in utxos if u["txid"] == taint_txid), None)
|
||||
cu = next((u for u in utxos if u["txid"] == clean_txid), None)
|
||||
if not tu: tu = next((u for u in utxos if u.get("address") == ta), None)
|
||||
if not cu: cu = next((u for u in utxos if u.get("address") == ca), None)
|
||||
if not tu or not cu:
|
||||
info("Could not locate tainted + clean UTXOs")
|
||||
return
|
||||
dest = get_new_address("carol", "bech32")
|
||||
total = tu["amount"] + cu["amount"]
|
||||
raw = create_raw_tx(
|
||||
[{"txid": tu["txid"], "vout": tu["vout"]},
|
||||
{"txid": cu["txid"], "vout": cu["vout"]}],
|
||||
[{dest: round(total - 0.0002, 8)}]
|
||||
)
|
||||
signed = sign_raw_tx("alice", raw)
|
||||
txid = send_raw(signed["hex"])
|
||||
mine_and_confirm()
|
||||
ok(f"Merged tainted + clean UTXOs in TX {txid[:16]}… — taint propagation")
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# 12. Behavioral Fingerprinting
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
def reproduce_12():
|
||||
header(12, "Behavioral Fingerprinting")
|
||||
ensure_funds("alice", 3.0)
|
||||
ensure_funds("bob", 3.0)
|
||||
|
||||
info("Alice's pattern: round amounts, always bech32…")
|
||||
for i in range(5):
|
||||
dest = get_new_address("carol", "bech32")
|
||||
send_to_address("alice", dest, 0.01 * (i + 1))
|
||||
|
||||
mine_and_confirm()
|
||||
|
||||
info("Bob's pattern: odd amounts, mixed address types…")
|
||||
for i in range(5):
|
||||
atype = "bech32m" if i % 2 == 0 else "bech32"
|
||||
dest = get_new_address("carol", atype)
|
||||
send_to_address("bob", dest, round(0.00723 * (i + 1) + 0.00011, 8))
|
||||
|
||||
mine_and_confirm()
|
||||
ok("Created distinguishable behavioral patterns for Alice and Bob")
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# Main
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
ALL = [
|
||||
(1, "Address Reuse", reproduce_01),
|
||||
(2, "Multi-input / CIOH", reproduce_02),
|
||||
(3, "Dust UTXO Detection", reproduce_03),
|
||||
(4, "Dust Spending w/ Normal", reproduce_04),
|
||||
(5, "Change Detection", reproduce_05),
|
||||
(6, "Consolidation Origin", reproduce_06),
|
||||
(7, "Script Type Mixing", reproduce_07),
|
||||
(8, "Cluster Merge", reproduce_08),
|
||||
(9, "Lookback Depth", reproduce_09),
|
||||
(10, "Exchange Origin", reproduce_10),
|
||||
(11, "Tainted UTXOs", reproduce_11),
|
||||
(12, "Behavioral Fingerprint", reproduce_12),
|
||||
]
|
||||
|
||||
def main():
|
||||
filt = None
|
||||
if "-k" in sys.argv:
|
||||
idx = sys.argv.index("-k")
|
||||
if idx + 1 < len(sys.argv):
|
||||
filt = sys.argv[idx + 1]
|
||||
|
||||
print(f"\n{B}{'═'*78}{R}")
|
||||
print(f"{B}{C} REPRODUCE — Bitcoin Privacy Vulnerabilities{R}")
|
||||
print(f"{B}{C} Custom Signet — {get_block_count()} blocks{R}")
|
||||
print(f"{B}{'═'*78}{R}")
|
||||
|
||||
for num, name, fn in ALL:
|
||||
if filt and str(num) != filt:
|
||||
continue
|
||||
try:
|
||||
fn()
|
||||
except Exception as e:
|
||||
print(f" \033[91m✗ ERROR in {name}: {e}\033[0m")
|
||||
import traceback; traceback.print_exc()
|
||||
|
||||
print(f"\n{B}{'═'*78}{R}")
|
||||
print(f" {G}Done. All vulnerability scenarios have been created on-chain.{R}")
|
||||
print(f" Now run: python3 detect.py <descriptor>")
|
||||
print(f"{B}{'═'*78}{R}\n")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
35
backend/script/run_all.sh
Executable file
35
backend/script/run_all.sh
Executable file
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env bash
|
||||
# run_all.sh — Setup custom signet and run all 12 vulnerability tests
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
echo "╔══════════════════════════════════════════════════════════════╗"
|
||||
echo "║ Bitcoin Privacy Vulnerability Suite — Full Run ║"
|
||||
echo "╚══════════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
|
||||
# Step 1: Setup signet (if not already running)
|
||||
if bitcoin-cli -signet getblockchaininfo &>/dev/null; then
|
||||
HEIGHT=$(bitcoin-cli -signet getblockcount)
|
||||
echo "✓ Custom Signet already running at block $HEIGHT"
|
||||
|
||||
# Check wallets exist
|
||||
WALLETS=$(bitcoin-cli -signet listwallets 2>/dev/null)
|
||||
if echo "$WALLETS" | grep -q "alice"; then
|
||||
echo "✓ Wallets already created"
|
||||
else
|
||||
echo "⚠ Wallets not found. Running setup..."
|
||||
bash setup_signet.sh
|
||||
fi
|
||||
else
|
||||
echo "Starting custom Signet setup..."
|
||||
bash setup_signet.sh
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Running vulnerability tests..."
|
||||
echo ""
|
||||
|
||||
# Step 2: Run all tests
|
||||
python3 test_vulnerabilities.py "$@"
|
||||
294
backend/script/setup_signet.sh
Executable file
294
backend/script/setup_signet.sh
Executable file
@@ -0,0 +1,294 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# setup_signet.sh — Bootstrap a private custom Signet for vulnerability testing
|
||||
# =============================================================================
|
||||
set -euo pipefail
|
||||
|
||||
DATADIR="$HOME/.bitcoin"
|
||||
SIGNET_DIR="$DATADIR/signet"
|
||||
MINER="/home/renato/Desktop/bitcoin/bitcoin/contrib/signet/miner"
|
||||
GRIND="bitcoin-util grind"
|
||||
CLI="bitcoin-cli"
|
||||
|
||||
echo "============================================"
|
||||
echo " STEP 0: Cleanup previous state"
|
||||
echo "============================================"
|
||||
bitcoin-cli stop 2>/dev/null || true
|
||||
bitcoin-cli -signet stop 2>/dev/null || true
|
||||
sleep 3
|
||||
|
||||
# Remove old signet data but keep blocks/chainstate for mainnet untouched
|
||||
rm -rf "$SIGNET_DIR"
|
||||
rm -f "$DATADIR/bitcoin.conf"
|
||||
|
||||
echo "============================================"
|
||||
echo " STEP 1: Generate Signet challenge key"
|
||||
echo "============================================"
|
||||
rm -f "$DATADIR/bitcoin.conf"
|
||||
|
||||
# Generate key pair using Python + bitcoin-cli (no wallet needed)
|
||||
KEYPAIR=$(python3 -c "
|
||||
import hashlib, os, struct
|
||||
|
||||
# Generate a random 32-byte private key
|
||||
privkey_bytes = os.urandom(32)
|
||||
|
||||
# secp256k1 parameters
|
||||
P = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
|
||||
A = 0
|
||||
B = 7
|
||||
Gx = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798
|
||||
Gy = 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8
|
||||
N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
|
||||
|
||||
def modinv(a, m):
|
||||
g, x, _ = extended_gcd(a % m, m)
|
||||
return x % m
|
||||
|
||||
def extended_gcd(a, b):
|
||||
if a == 0:
|
||||
return b, 0, 1
|
||||
g, x, y = extended_gcd(b % a, a)
|
||||
return g, y - (b // a) * x, x
|
||||
|
||||
def point_add(p1, p2):
|
||||
if p1 is None: return p2
|
||||
if p2 is None: return p1
|
||||
x1, y1 = p1
|
||||
x2, y2 = p2
|
||||
if x1 == x2 and y1 != y2:
|
||||
return None
|
||||
if x1 == x2:
|
||||
lam = (3 * x1 * x1 + A) * modinv(2 * y1, P) % P
|
||||
else:
|
||||
lam = (y2 - y1) * modinv(x2 - x1, P) % P
|
||||
x3 = (lam * lam - x1 - x2) % P
|
||||
y3 = (lam * (x1 - x3) - y1) % P
|
||||
return (x3, y3)
|
||||
|
||||
def scalar_mult(k, point):
|
||||
result = None
|
||||
addend = point
|
||||
while k:
|
||||
if k & 1:
|
||||
result = point_add(result, addend)
|
||||
addend = point_add(addend, addend)
|
||||
k >>= 1
|
||||
return result
|
||||
|
||||
privkey_int = int.from_bytes(privkey_bytes, 'big') % N
|
||||
if privkey_int == 0:
|
||||
privkey_int = 1
|
||||
privkey_bytes = privkey_int.to_bytes(32, 'big')
|
||||
|
||||
pub = scalar_mult(privkey_int, (Gx, Gy))
|
||||
pubkey_bytes = b'\x02' + pub[0].to_bytes(32, 'big') if pub[1] % 2 == 0 else b'\x03' + pub[0].to_bytes(32, 'big')
|
||||
|
||||
# WIF encode (testnet/signet = 0xEF prefix)
|
||||
wif_payload = b'\xef' + privkey_bytes + b'\x01' # compressed
|
||||
checksum = hashlib.sha256(hashlib.sha256(wif_payload).digest()).digest()[:4]
|
||||
import base64
|
||||
# base58 encoding
|
||||
ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
|
||||
num = int.from_bytes(wif_payload + checksum, 'big')
|
||||
b58 = ''
|
||||
while num > 0:
|
||||
num, rem = divmod(num, 58)
|
||||
b58 = ALPHABET[rem] + b58
|
||||
for byte in (wif_payload + checksum):
|
||||
if byte == 0:
|
||||
b58 = '1' + b58
|
||||
else:
|
||||
break
|
||||
|
||||
print(f'{b58} {pubkey_bytes.hex()}')
|
||||
")
|
||||
|
||||
PRIVKEY=$(echo "$KEYPAIR" | awk '{print $1}')
|
||||
PUBKEY=$(echo "$KEYPAIR" | awk '{print $2}')
|
||||
|
||||
# Build 1-of-1 multisig signet challenge: OP_1 <33-byte pubkey> OP_1 OP_CHECKMULTISIG
|
||||
SIGNETCHALLENGE="5121${PUBKEY}51ae"
|
||||
|
||||
echo ""
|
||||
echo ">>> Private key (WIF): $PRIVKEY"
|
||||
echo ">>> Public key: $PUBKEY"
|
||||
echo ">>> Signet challenge: $SIGNETCHALLENGE"
|
||||
echo ""
|
||||
|
||||
# Save keys for later use
|
||||
cat > "$DATADIR/signet_keys.env" <<EOF
|
||||
PRIVKEY=$PRIVKEY
|
||||
PUBKEY=$PUBKEY
|
||||
SIGNETCHALLENGE=$SIGNETCHALLENGE
|
||||
EOF
|
||||
|
||||
echo "============================================"
|
||||
echo " STEP 2: Write bitcoin.conf for custom Signet"
|
||||
echo "============================================"
|
||||
rm -rf "$DATADIR/regtest"
|
||||
rm -f "$DATADIR/bitcoin.conf"
|
||||
|
||||
cat > "$DATADIR/bitcoin.conf" <<EOF
|
||||
signet=1
|
||||
[signet]
|
||||
daemon=1
|
||||
server=1
|
||||
txindex=1
|
||||
signetchallenge=$SIGNETCHALLENGE
|
||||
acceptnonstdtxn=1
|
||||
fallbackfee=0.00010
|
||||
mintxfee=0.00001
|
||||
dustrelayfee=0.00000001
|
||||
minrelaytxfee=0.00001
|
||||
EOF
|
||||
|
||||
echo "Config written to $DATADIR/bitcoin.conf"
|
||||
cat "$DATADIR/bitcoin.conf"
|
||||
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo " STEP 3: Start custom Signet node"
|
||||
echo "============================================"
|
||||
bitcoind
|
||||
sleep 3
|
||||
|
||||
# Wait for RPC
|
||||
for i in $(seq 1 30); do
|
||||
if $CLI -signet getblockchaininfo >/dev/null 2>&1; then
|
||||
echo " Signet node ready"
|
||||
break
|
||||
fi
|
||||
echo " Waiting for node to start... ($i)"
|
||||
sleep 2
|
||||
done
|
||||
|
||||
echo "Node info:"
|
||||
$CLI -signet getblockchaininfo | python3 -c "
|
||||
import sys, json
|
||||
d = json.load(sys.stdin)
|
||||
print(f' Chain: {d[\"chain\"]}')
|
||||
print(f' Blocks: {d[\"blocks\"]}')
|
||||
"
|
||||
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo " STEP 4: Create wallets"
|
||||
echo "============================================"
|
||||
WALLETS=("miner" "alice" "bob" "carol" "exchange" "risky")
|
||||
|
||||
for w in "${WALLETS[@]}"; do
|
||||
echo -n " Creating wallet '$w'... "
|
||||
$CLI -signet -named createwallet wallet_name="$w" descriptors=true 2>&1 | grep -o '"name": "[^"]*"' || echo "(exists or loaded)"
|
||||
done
|
||||
|
||||
echo " Loaded wallets:"
|
||||
$CLI -signet listwallets
|
||||
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo " STEP 5: Import Signet challenge key into miner wallet"
|
||||
echo "============================================"
|
||||
# For descriptor wallets, we need to import with the PRIVATE key in the descriptor
|
||||
# Import both combo (for general key use) and multi(1,...) (for signet challenge signing)
|
||||
COMBO_INFO=$($CLI -signet getdescriptorinfo "combo($PRIVKEY)")
|
||||
COMBO_CHECKSUM=$(echo "$COMBO_INFO" | python3 -c "import sys,json; print(json.load(sys.stdin)['checksum'])")
|
||||
COMBO_DESC="combo($PRIVKEY)#$COMBO_CHECKSUM"
|
||||
|
||||
MULTI_INFO=$($CLI -signet getdescriptorinfo "multi(1,$PRIVKEY)")
|
||||
MULTI_CHECKSUM=$(echo "$MULTI_INFO" | python3 -c "import sys,json; print(json.load(sys.stdin)['checksum'])")
|
||||
MULTI_DESC="multi(1,$PRIVKEY)#$MULTI_CHECKSUM"
|
||||
|
||||
echo " Importing combo and multi(1,...) descriptors..."
|
||||
|
||||
$CLI -signet -rpcwallet=miner importdescriptors "[{\"desc\": \"$COMBO_DESC\", \"timestamp\": \"now\"}, {\"desc\": \"$MULTI_DESC\", \"timestamp\": \"now\"}]" | python3 -c "
|
||||
import sys, json
|
||||
r = json.load(sys.stdin)
|
||||
for i, item in enumerate(r):
|
||||
s = item.get('success', False)
|
||||
print(f' Import {i+1} success: {s}')
|
||||
if not s:
|
||||
print(f' Error: {item.get(\"error\", \"unknown\")}')
|
||||
"
|
||||
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo " STEP 6: Mine initial blocks (110 blocks)"
|
||||
echo "============================================"
|
||||
|
||||
MINER_ADDR=$($CLI -signet -rpcwallet=miner getnewaddress "" bech32)
|
||||
echo " Mining to: $MINER_ADDR"
|
||||
|
||||
echo " Mining 110 blocks (this may take a minute)..."
|
||||
BLOCK_TIME=$(date +%s)
|
||||
for i in $(seq 1 110); do
|
||||
BLOCK_TIME=$((BLOCK_TIME + 1))
|
||||
$MINER \
|
||||
--cli="$CLI -rpcwallet=miner" \
|
||||
generate \
|
||||
--grind-cmd="$GRIND" \
|
||||
--address="$MINER_ADDR" \
|
||||
--min-nbits \
|
||||
--set-block-time="$BLOCK_TIME" \
|
||||
2>&1 >/dev/null
|
||||
if [ $((i % 10)) -eq 0 ]; then
|
||||
echo " Mined block $i / 110"
|
||||
fi
|
||||
done
|
||||
|
||||
HEIGHT=$($CLI -signet getblockcount)
|
||||
echo " Block height: $HEIGHT"
|
||||
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo " STEP 7: Fund wallets"
|
||||
echo "============================================"
|
||||
|
||||
MINER_BAL=$($CLI -signet -rpcwallet=miner getbalance)
|
||||
echo " Miner balance: $MINER_BAL BTC"
|
||||
|
||||
# Fund each wallet
|
||||
for w in alice bob carol exchange risky; do
|
||||
ADDR=$($CLI -signet -rpcwallet=$w getnewaddress "" bech32)
|
||||
TXID=$($CLI -signet -rpcwallet=miner sendtoaddress "$ADDR" 10.0)
|
||||
echo " Funded $w with 10 BTC (txid: ${TXID:0:16}...)"
|
||||
done
|
||||
|
||||
echo " Mining 6 more blocks to confirm funding..."
|
||||
BLOCK_TIME=$(($(date +%s) + 200))
|
||||
for i in $(seq 1 6); do
|
||||
BLOCK_TIME=$((BLOCK_TIME + 1))
|
||||
$MINER \
|
||||
--cli="$CLI -rpcwallet=miner" \
|
||||
generate \
|
||||
--grind-cmd="$GRIND" \
|
||||
--address="$MINER_ADDR" \
|
||||
--min-nbits \
|
||||
--set-block-time="$BLOCK_TIME" \
|
||||
2>&1 >/dev/null
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo " Final balances:"
|
||||
for w in miner alice bob carol exchange risky; do
|
||||
BAL=$($CLI -signet -rpcwallet=$w getbalance)
|
||||
echo " $w: $BAL BTC"
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo " SETUP COMPLETE"
|
||||
echo "============================================"
|
||||
echo ""
|
||||
echo " Custom Signet is running with:"
|
||||
echo " - 6 wallets (miner, alice, bob, carol, exchange, risky)"
|
||||
echo " - Each funded with 10 BTC"
|
||||
echo " - txindex=1 for historical lookups"
|
||||
echo " - acceptnonstdtxn=1 for dust experiments"
|
||||
echo ""
|
||||
echo " Signet keys saved to: $DATADIR/signet_keys.env"
|
||||
echo " To mine more blocks, use: ./mine_blocks.sh <N>"
|
||||
echo ""
|
||||
|
||||
# Save mining address for later mining
|
||||
echo "MINER_ADDR=$MINER_ADDR" >> "$DATADIR/signet_keys.env"
|
||||
1079
backend/script/test_vulnerabilities.py
Normal file
1079
backend/script/test_vulnerabilities.py
Normal file
File diff suppressed because it is too large
Load Diff
258
backend/script/verify.py
Normal file
258
backend/script/verify.py
Normal file
@@ -0,0 +1,258 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
verify.py
|
||||
=========
|
||||
End-to-end proof that detect.py catches every vulnerability that reproduce.py
|
||||
creates — on a REGTEST chain.
|
||||
|
||||
Steps:
|
||||
1. Wipe & restart regtest
|
||||
2. Create wallets, fund miner
|
||||
3. Run reproduce.py (create all 12 vulnerability scenarios)
|
||||
4. Run detect.py --wallet alice (capture output)
|
||||
5. Parse output and assert every detector (1–12) produced ≥1 finding
|
||||
6. Print a 12-row proof table
|
||||
|
||||
Usage:
|
||||
python3 verify.py
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
|
||||
DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
WALLETS = ["miner", "alice", "bob", "carol", "exchange", "risky"]
|
||||
|
||||
G = "\033[92m"
|
||||
R = "\033[91m"
|
||||
B = "\033[1m"
|
||||
C = "\033[96m"
|
||||
Y = "\033[93m"
|
||||
RST = "\033[0m"
|
||||
|
||||
|
||||
def run(cmd, check=True, timeout=300):
|
||||
"""Run a shell command, return stdout."""
|
||||
result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=timeout)
|
||||
if check and result.returncode != 0:
|
||||
print(f" {R}FAIL:{RST} {cmd}")
|
||||
print(f" stderr: {result.stderr.strip()}")
|
||||
sys.exit(1)
|
||||
return result.stdout.strip()
|
||||
|
||||
|
||||
def btc(cmd):
|
||||
return run(f"bitcoin-cli -regtest {cmd}")
|
||||
|
||||
|
||||
def btcw(wallet, cmd):
|
||||
return run(f"bitcoin-cli -regtest -rpcwallet={wallet} {cmd}")
|
||||
|
||||
|
||||
def banner(msg):
|
||||
print(f"\n{B}{C}{'═' * 70}{RST}")
|
||||
print(f"{B}{C} {msg}{RST}")
|
||||
print(f"{B}{C}{'═' * 70}{RST}")
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Step 1: Fresh regtest
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
def setup_regtest():
|
||||
banner("Step 1: Fresh regtest chain")
|
||||
# Stop if running
|
||||
run("bitcoin-cli -regtest stop 2>/dev/null || true", check=False)
|
||||
time.sleep(2)
|
||||
|
||||
# Wipe
|
||||
run("rm -rf ~/.bitcoin/regtest")
|
||||
print(" ✓ Wiped regtest datadir")
|
||||
|
||||
# Ensure bitcoin.conf exists with regtest settings
|
||||
conf = os.path.expanduser("~/.bitcoin/bitcoin.conf")
|
||||
with open(conf, "w") as f:
|
||||
f.write("regtest=1\ntxindex=1\n\n[regtest]\n"
|
||||
"fallbackfee=0.00010\ndustrelayfee=0.00000001\n"
|
||||
"acceptnonstdtxn=1\nserver=1\n")
|
||||
print(" ✓ Wrote bitcoin.conf")
|
||||
|
||||
# Start
|
||||
run("bitcoind -regtest -daemon")
|
||||
# Wait for RPC to become ready
|
||||
print(" … waiting for bitcoind RPC …", end="", flush=True)
|
||||
for i in range(30):
|
||||
time.sleep(1)
|
||||
res = subprocess.run("bitcoin-cli -regtest getblockchaininfo",
|
||||
shell=True, capture_output=True, text=True, timeout=10)
|
||||
if res.returncode == 0:
|
||||
print(f" ready after {i+1}s")
|
||||
break
|
||||
else:
|
||||
print(f"\n {R}ERROR: bitcoind didn't start after 30s{RST}")
|
||||
sys.exit(1)
|
||||
print(" ✓ bitcoind started")
|
||||
|
||||
# Create wallets
|
||||
for w in WALLETS:
|
||||
btc(f'createwallet "{w}"')
|
||||
print(f" ✓ Created wallets: {', '.join(WALLETS)}")
|
||||
|
||||
# Mine 110 blocks to get mature coinbases
|
||||
addr = btcw("miner", 'getnewaddress "" bech32')
|
||||
btc(f"generatetoaddress 110 {addr}")
|
||||
balance = btcw("miner", "getbalance")
|
||||
print(f" ✓ Mined 110 blocks — miner balance: {balance} BTC")
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Step 2: Reproduce
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
def run_reproduce():
|
||||
banner("Step 2: Run reproduce.py (create 12 vulnerability scenarios)")
|
||||
result = subprocess.run(
|
||||
[sys.executable, os.path.join(DIR, "reproduce.py")],
|
||||
capture_output=True, text=True, timeout=300,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
print(f" {R}reproduce.py FAILED:{RST}")
|
||||
print(result.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Count successes
|
||||
successes = result.stdout.count("✓")
|
||||
print(f" ✓ reproduce.py completed — {successes} scenario(s) created")
|
||||
# Print abbreviated output
|
||||
for line in result.stdout.split("\n"):
|
||||
if "✓" in line or "REPRODUCE" in line:
|
||||
print(f" {line.strip()}")
|
||||
return result.stdout
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Step 3: Detect
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
def run_detect():
|
||||
banner("Step 3: Run detect.py --wallet alice")
|
||||
result = subprocess.run(
|
||||
[sys.executable, os.path.join(DIR, "detect.py"),
|
||||
"--wallet", "alice",
|
||||
"--known-risky-wallets", "risky",
|
||||
"--known-exchange-wallets", "exchange"],
|
||||
capture_output=True, text=True, timeout=300,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
print(f" {R}detect.py FAILED:{RST}")
|
||||
print(result.stderr)
|
||||
sys.exit(1)
|
||||
print(f" ✓ detect.py completed")
|
||||
return result.stdout
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Step 4: Parse & verify
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
DETECTORS = {
|
||||
1: ("Address Reuse", r"1 · Address Reuse"),
|
||||
2: ("CIOH", r"2 · Common Input Ownership"),
|
||||
3: ("Dust UTXO Detection", r"3 · Dust UTXO Detection"),
|
||||
4: ("Dust Spent with Normal", r"4 · Dust Spent with Normal"),
|
||||
5: ("Change Output Detection", r"5 · Probable Change Output"),
|
||||
6: ("Consolidation Origin", r"6 · UTXOs from Prior Consolidation"),
|
||||
7: ("Script Type Mixing", r"7 · Script Type Mixing"),
|
||||
8: ("Cluster Merge", r"8 · Cluster Merge"),
|
||||
9: ("UTXO Age / Lookback", r"9 · UTXO Age"),
|
||||
10: ("Exchange Origin", r"10 · Probable Exchange Origin"),
|
||||
11: ("Tainted UTXOs", r"11 · Tainted UTXOs"),
|
||||
12: ("Behavioral Fingerprint", r"12 · Behavioral Fingerprint"),
|
||||
}
|
||||
|
||||
|
||||
def parse_and_verify(detect_output):
|
||||
banner("Step 4: Verification — does detect catch every reproduced vulnerability?")
|
||||
|
||||
# Split output into sections per detector
|
||||
lines = detect_output.split("\n")
|
||||
results = {}
|
||||
current_id = None
|
||||
|
||||
for line in lines:
|
||||
# Check if this line starts a detector section
|
||||
for did, (name, pattern) in DETECTORS.items():
|
||||
if pattern in line:
|
||||
current_id = did
|
||||
results[did] = {"findings": 0, "warnings": 0, "lines": []}
|
||||
break
|
||||
# Count findings/warnings within current section
|
||||
if current_id is not None:
|
||||
if "FINDING" in line:
|
||||
results[current_id]["findings"] += 1
|
||||
if "WARNING" in line:
|
||||
results[current_id]["warnings"] += 1
|
||||
results[current_id]["lines"].append(line)
|
||||
|
||||
# Also parse the summary line
|
||||
total_findings = 0
|
||||
total_warnings = 0
|
||||
m = re.search(r"Findings:\s+(\d+)", detect_output)
|
||||
if m:
|
||||
total_findings = int(m.group(1))
|
||||
m = re.search(r"Warnings:\s+(\d+)", detect_output)
|
||||
if m:
|
||||
total_warnings = int(m.group(1))
|
||||
|
||||
# ── Print proof table ──
|
||||
print()
|
||||
print(f" {'#':>3} {'Detector':<30} {'Findings':>8} {'Warnings':>8} {'Status'}")
|
||||
print(f" {'─'*3} {'─'*30} {'─'*8} {'─'*8} {'─'*8}")
|
||||
|
||||
all_pass = True
|
||||
for did in sorted(DETECTORS.keys()):
|
||||
name = DETECTORS[did][0]
|
||||
r = results.get(did, {"findings": 0, "warnings": 0})
|
||||
f_count = r["findings"]
|
||||
w_count = r["warnings"]
|
||||
detected = f_count > 0 or w_count > 0
|
||||
status = f"{G}PASS ✓{RST}" if detected else f"{R}FAIL ✗{RST}"
|
||||
if not detected:
|
||||
all_pass = False
|
||||
print(f" {did:>3} {name:<30} {f_count:>8} {w_count:>8} {status}")
|
||||
|
||||
print(f" {'─'*3} {'─'*30} {'─'*8} {'─'*8} {'─'*8}")
|
||||
print(f" {'':>3} {'TOTAL':<30} {total_findings:>8} {total_warnings:>8}")
|
||||
print()
|
||||
|
||||
if all_pass:
|
||||
print(f" {G}{B}═══ ALL 12 DETECTORS FIRED — PROOF COMPLETE ═══{RST}")
|
||||
print(f" {G}Every reproduced vulnerability was caught by detect.py on regtest.{RST}")
|
||||
else:
|
||||
failed = [did for did in DETECTORS if results.get(did, {}).get("findings", 0) == 0
|
||||
and results.get(did, {}).get("warnings", 0) == 0]
|
||||
print(f" {R}{B}═══ FAILURE — {len(failed)} detector(s) did not fire ═══{RST}")
|
||||
for did in failed:
|
||||
print(f" {R} Detector {did}: {DETECTORS[did][0]}{RST}")
|
||||
|
||||
return all_pass
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Main
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
def main():
|
||||
print(f"\n{B}{'═' * 70}{RST}")
|
||||
print(f"{B}{C} VERIFY: reproduce → detect end-to-end proof on REGTEST{RST}")
|
||||
print(f"{B}{'═' * 70}{RST}")
|
||||
|
||||
setup_regtest()
|
||||
run_reproduce()
|
||||
detect_output = run_detect()
|
||||
passed = parse_and_verify(detect_output)
|
||||
|
||||
print()
|
||||
sys.exit(0 if passed else 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user