Merge pull request #5 from LORDBABUINO/cleanup/remove-unused-code

Remove unused code, optimize critical path, fix datadir auth
This commit is contained in:
LORDBABUINO
2026-03-05 19:45:45 -03:00
committed by GitHub
27 changed files with 71 additions and 3135 deletions
+1
View File
@@ -1,3 +1,4 @@
backend/script/bitcoin-data/
node_modules/
dist/
.env
+7 -44
View File
@@ -5,7 +5,6 @@ Connection settings are read from config.ini in the same directory.
import json
import subprocess
import time
import os
import configparser
@@ -23,6 +22,13 @@ def _build_base_args(section):
args = [cli_bin]
# Datadir — resolve relative paths from this file's directory
datadir = section.get("datadir", "").strip()
if datadir:
if not os.path.isabs(datadir):
datadir = os.path.join(os.path.dirname(os.path.abspath(__file__)), datadir)
args.append(f"-datadir={datadir}")
network_flags = {
"regtest": "-regtest",
"testnet": "-testnet",
@@ -42,10 +48,6 @@ def _build_base_args(section):
_cfg = _load_config()
_BASE_ARGS = _build_base_args(_cfg)
# Keep these for any scripts that might reference them directly
CLI = _cfg.get("cli", "bitcoin-cli")
SIGNET_ARGS = _BASE_ARGS
def cli(*args, wallet=None):
"""Call bitcoin-cli [network] [wallet] <args> and return parsed JSON or string."""
cmd = list(_BASE_ARGS)
@@ -73,23 +75,6 @@ def mine_blocks(n=1):
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")
@@ -128,11 +113,6 @@ def finalize_psbt(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))
@@ -143,11 +123,6 @@ def sign_raw_tx(wallet_name, hex_tx):
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"))
@@ -163,15 +138,3 @@ def send_to_address(wallet_name, address, amount):
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}")
+5 -1
View File
@@ -5,8 +5,12 @@ network = regtest
# Path to the bitcoin-cli binary (use full path if not on PATH)
cli = bitcoin-cli
# Data directory for bitcoind (matches setup.sh).
# Relative paths are resolved from the directory containing this file.
datadir = bitcoin-data
# Optional: override RPC connection details.
# Leave these blank to use the defaults from ~/.bitcoin/bitcoin.conf.
# Leave these blank to use cookie auth from the datadir.
rpchost =
rpcport =
rpcuser =
@@ -1,700 +0,0 @@
#!/usr/bin/env python3
"""
create_random_transactions.py
==============================
Creates n varied, realistic-looking Bitcoin transactions involving Alice's wallet
on regtest. Each run is seeded with fresh entropy (block height + wall clock) so
the on-chain history grows organically and never looks the same twice.
Address types used:
• bech32 (P2WPKH) — bcrt1q…
• bech32m (P2TR) — bcrt1p…
• p2sh-segwit (P2SH-P2WPKH) — 2… (regtest)
• legacy (P2PKH) — m… (regtest)
Transaction archetypes (weighted random selection):
01. simple_payment Alice pays a peer, natural change
02. multi_output Alice batch-pays multiple recipients in one TX
03. consolidation Alice sweeps many small UTXOs → one
04. self_transfer Alice rotates to her own fresh address
05. utxo_split Alice fans one large UTXO out into several
06. receive_from_peer Peer spontaneously sends Alice funds
07. exchange_withdrawal Exchange batch-withdraws to Alice + others
08. chain_hop Alice→Bob, then Bob→Carol (multi-hop chain)
09. mixed_type_spend Spend P2WPKH + P2TR inputs in one TX
10. round_amount_payment Deliberately round consumer-style payment
11. psbt_coinjoin Alice+Bob cooperate via PSBT (PayJoin-like)
12. cold_to_hot Taproot "cold" → P2WPKH "hot" sweep
13. lightning_channel_like Exact-msat-aligned channel-open sizing
14. high_freq_small Burst of rapid tiny payments (merchant pattern)
15. receive_multiple_senders Several wallets simultaneously send Alice funds
Usage:
python3 create_random_transactions.py 20
python3 create_random_transactions.py 50 --no-mine-final
python3 create_random_transactions.py 10 --seed 42
"""
import sys
import os
import json
import time
import random
import argparse
import hashlib
from collections import Counter
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from bitcoin_rpc import (
cli, mine_blocks, 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,
)
# ─── Colours ──────────────────────────────────────────────────────────────────
G = "\033[92m"
Y = "\033[93m"
R = "\033[91m"
B = "\033[1m"
C = "\033[96m"
DIM = "\033[2m"
RST = "\033[0m"
def ok(msg): print(f" {G}{RST} {msg}")
def info(msg): print(f" {Y}{RST} {msg}")
def warn(msg): print(f" {R}{RST} {msg}")
def hdr(msg): print(f"\n {B}{C}{msg}{RST}")
# ─── Constants ────────────────────────────────────────────────────────────────
ALICE = "alice"
SIDE_WALLETS = ["bob", "carol", "exchange", "miner"]
ALL_WALLETS = [ALICE] + SIDE_WALLETS + ["risky"]
# Every address-type label that Bitcoin Core's getnewaddress accepts
ADDR_TYPES = ["bech32", "bech32m", "p2sh-segwit", "legacy"]
FEE_RESERVE = 0.00025 # BTC per input/output to leave for fees
DUST_LIMIT = 0.00000546
# ─── Entropy / RNG ───────────────────────────────────────────────────────────
def reseed() -> int:
"""Seed random from chain height + nanosecond wall clock. Returns seed."""
h = get_block_count()
raw = f"{h}{time.time_ns()}{os.getpid()}"
seed = int(hashlib.sha256(raw.encode()).hexdigest(), 16) % (2**32)
random.seed(seed)
info(f"RNG seeded from block {h} + wall-clock (seed={seed})")
return seed
# ─── Amount helpers ───────────────────────────────────────────────────────────
def rand_btc(lo: float = 0.0005, hi: float = 0.05) -> float:
"""Random BTC amount; occasionally semi-rounded to mimic human behaviour."""
v = random.uniform(lo, hi)
r = random.random()
if r < 0.15:
v = round(v, 2) # e.g. 0.03
elif r < 0.30:
v = round(v, 4) # e.g. 0.0312
else:
v = round(v, 8)
return max(lo, min(hi, v))
def round_btc() -> float:
"""A consumer-style round amount."""
return random.choice([
0.001, 0.002, 0.005, 0.01, 0.02, 0.025, 0.05, 0.1,
0.0025, 0.0075, 0.015,
])
def rand_addr_type() -> str:
return random.choice(ADDR_TYPES)
def rand_peer(exclude=None) -> str:
pool = [w for w in SIDE_WALLETS if w != exclude]
return random.choice(pool)
# ─── Funding / block helpers ──────────────────────────────────────────────────
def ensure_funded(wallet: str, min_btc: float = 0.5) -> None:
bal = get_balance(wallet)
if bal < min_btc:
addr = get_new_address(wallet, "bech32")
top_up = min_btc + random.uniform(0.5, 2.0)
send_to_address("miner", addr, round(top_up, 8))
info(f"Topped up {wallet} with {top_up:.4f} BTC from miner")
def maybe_mine(force: bool = False) -> None:
"""Mine 1-3 blocks with 40 % probability (or always when forced)."""
if force or random.random() < 0.40:
n = random.randint(1, 3)
maddr = get_new_address("miner", "bech32")
cli("generatetoaddress", n, maddr)
ok(f"Mined {n} block(s) (height={get_block_count()})")
time.sleep(0.15)
def mine_confirm(n: int = 1) -> None:
maddr = get_new_address("miner", "bech32")
cli("generatetoaddress", n, maddr)
time.sleep(0.15)
# ─── Transaction archetypes ───────────────────────────────────────────────────
def tx_simple_payment() -> str:
"""Alice pays a random peer a random amount — wallet produces change."""
hdr("Simple Payment")
ensure_funded(ALICE, 0.5)
peer = rand_peer()
addr_type = rand_addr_type()
dest = get_new_address(peer, addr_type)
amt = rand_btc(0.001, 0.08)
if get_balance(ALICE) < amt + FEE_RESERVE * 2:
ensure_funded(ALICE, 1.0)
txid = send_to_address(ALICE, dest, amt)
maybe_mine()
ok(f"Alice → {peer} ({addr_type}) {amt:.8f} BTC TX={txid[:16]}")
return txid
def tx_multi_output() -> str:
"""Alice batch-pays 2-5 recipients in one sendmany transaction."""
hdr("Multi-Output Batch Payment")
ensure_funded(ALICE, 1.5)
n_recv = random.randint(2, 5)
batch = {}
total = 0.0
for _ in range(n_recv):
addr = get_new_address(rand_peer(), rand_addr_type())
amt = rand_btc(0.001, 0.025)
batch[addr] = amt
total += amt
if get_balance(ALICE) < total + FEE_RESERVE * 4:
ensure_funded(ALICE, total + 1.5)
txid = cli("sendmany", "", json.dumps(batch), wallet=ALICE)
maybe_mine()
ok(f"Alice batch → {n_recv} recipients TX={txid[:16]}")
return txid
def tx_consolidation() -> str | None:
"""Alice sweeps several small UTXOs into one output (wallet hygiene)."""
hdr("UTXO Consolidation")
# First scatter several small UTXOs to Alice via different sender wallets
n_scatter = random.randint(3, 7)
for _ in range(n_scatter):
sender = rand_peer()
ensure_funded(sender, 0.3)
addr = get_new_address(ALICE, random.choice(["bech32", "bech32m"]))
send_to_address(sender, addr, rand_btc(0.003, 0.015))
mine_confirm(1)
utxos = get_utxos(ALICE, 1)
small = [u for u in utxos if 0.001 < u["amount"] < 0.02]
if len(small) < 2:
info("Not enough small UTXOs for consolidation, skipping")
return None
to_merge = small[: random.randint(2, min(len(small), 7))]
dest = get_new_address(ALICE, "bech32")
total = sum(u["amount"] for u in to_merge)
fee = FEE_RESERVE * len(to_merge)
net = round(total - fee, 8)
if net <= DUST_LIMIT:
info("Net after fee too small, skipping")
return None
inputs = [{"txid": u["txid"], "vout": u["vout"]} for u in to_merge]
raw = create_raw_tx(inputs, [{dest: net}])
signed = sign_raw_tx(ALICE, raw)
txid = send_raw(signed["hex"])
maybe_mine()
ok(f"Consolidated {len(to_merge)} UTXOs → 1 TX={txid[:16]}")
return txid
def tx_self_transfer() -> str:
"""Alice rotates coins to her own fresh address (key rotation / cold→warm)."""
hdr("Self-Transfer")
ensure_funded(ALICE, 0.3)
addr_type = rand_addr_type()
dest = get_new_address(ALICE, addr_type)
amt = rand_btc(0.01, 0.2)
if get_balance(ALICE) < amt + FEE_RESERVE * 2:
ensure_funded(ALICE, amt + 0.5)
txid = send_to_address(ALICE, dest, amt)
maybe_mine()
ok(f"Alice self → {addr_type} {amt:.8f} BTC TX={txid[:16]}")
return txid
def tx_utxo_split() -> str | None:
"""Alice fans one large UTXO out into 2-5 smaller outputs (own addresses)."""
hdr("UTXO Split / Fan-out")
ensure_funded(ALICE, 1.0)
utxos = get_utxos(ALICE, 1)
big = [u for u in utxos if u["amount"] > 0.25]
if not big:
send_to_address("miner", get_new_address(ALICE, "bech32"), 1.5)
mine_confirm(1)
utxos = get_utxos(ALICE, 1)
big = [u for u in utxos if u["amount"] > 0.25]
if not big:
info("No large UTXO available for split, skipping")
return None
source = random.choice(big)
n_out = random.randint(2, 5)
budget = source["amount"] - FEE_RESERVE * (n_out + 1)
if budget <= 0:
info("Budget after fee too small, skipping")
return None
# Give each output a random share of the budget
shares = [random.random() for _ in range(n_out)]
total_s = sum(shares)
outputs = []
for share in shares:
amt = round(budget * share / total_s, 8)
amt = max(0.0001, amt)
atype = random.choice(["bech32", "bech32m"])
addr = get_new_address(ALICE, atype)
outputs.append({addr: amt})
raw = create_raw_tx(
[{"txid": source["txid"], "vout": source["vout"]}],
outputs
)
signed = sign_raw_tx(ALICE, raw)
txid = send_raw(signed["hex"])
maybe_mine()
ok(f"Split 1 UTXO → {n_out} outputs TX={txid[:16]}")
return txid
def tx_receive_from_peer() -> str:
"""A peer spontaneously sends Alice funds — she just receives."""
hdr("Receive from Peer")
peer = rand_peer()
ensure_funded(peer, 0.3)
addr_type = rand_addr_type()
alice_addr = get_new_address(ALICE, addr_type)
amt = rand_btc(0.005, 0.12)
txid = send_to_address(peer, alice_addr, amt)
maybe_mine()
ok(f"{peer} → Alice ({addr_type}) {amt:.8f} BTC TX={txid[:16]}")
return txid
def tx_exchange_withdrawal() -> str:
"""Exchange batch-withdraws to Alice and several other wallets at once."""
hdr("Exchange Batch Withdrawal")
ensure_funded("exchange", 3.0)
recipients = [ALICE] + random.sample([w for w in SIDE_WALLETS if w != "exchange"],
random.randint(2, 3))
batch = {}
for w in recipients:
addr = get_new_address(w, "bech32") # exchanges use bech32
batch[addr] = rand_btc(0.005, 0.06)
txid = cli("sendmany", "", json.dumps(batch), wallet="exchange")
maybe_mine()
ok(f"Exchange batch → {len(recipients)} wallets incl. Alice TX={txid[:16]}")
return txid
def tx_chain_hop() -> tuple[str, str]:
"""Alice pays Bob; Bob immediately forwards part to Carol (multi-hop)."""
hdr("Chain Hop Alice → Bob → Carol")
ensure_funded(ALICE, 0.3)
ensure_funded("bob", 0.2)
hop_amt = rand_btc(0.008, 0.06)
bob_addr = get_new_address("bob", rand_addr_type())
txid1 = send_to_address(ALICE, bob_addr, hop_amt)
mine_confirm(1) # Bob needs confirmed UTXO to spend
fwd_amt = round(hop_amt * random.uniform(0.4, 0.85), 8)
carol_addr = get_new_address("carol", rand_addr_type())
txid2 = send_to_address("bob", carol_addr, fwd_amt)
maybe_mine()
ok(f"Alice→Bob TX={txid1[:16]}… Bob→Carol TX={txid2[:16]}")
return txid1, txid2
def tx_mixed_type_spend() -> str | None:
"""Spend a P2WPKH UTXO and a P2TR UTXO together in one transaction."""
hdr("Mixed Script-Type Spend (P2WPKH + P2TR)")
wpkh_addr = get_new_address(ALICE, "bech32")
tr_addr = get_new_address(ALICE, "bech32m")
fund_amt = rand_btc(0.06, 0.2)
send_to_address("miner", wpkh_addr, fund_amt)
send_to_address("miner", tr_addr, fund_amt)
mine_confirm(1)
utxos = get_utxos(ALICE, 1)
wu = next((u for u in utxos if u.get("address") == wpkh_addr), None)
tu = next((u for u in utxos if u.get("address") == tr_addr), None)
if not wu or not tu:
info("Could not locate both script-type UTXOs, skipping")
return None
dest = get_new_address(rand_peer(), rand_addr_type())
total = wu["amount"] + tu["amount"] - FEE_RESERVE * 2
raw = create_raw_tx(
[{"txid": wu["txid"], "vout": wu["vout"]},
{"txid": tu["txid"], "vout": tu["vout"]}],
[{dest: round(total, 8)}]
)
signed = sign_raw_tx(ALICE, raw)
txid = send_raw(signed["hex"])
maybe_mine()
ok(f"Mixed P2WPKH+P2TR spend TX={txid[:16]}")
return txid
def tx_round_amount_payment() -> str:
"""Alice makes a suspiciously round-amount payment — normal consumer habit."""
hdr("Round-Amount Payment")
ensure_funded(ALICE, 0.5)
peer = rand_peer()
amt = round_btc()
if get_balance(ALICE) < amt + FEE_RESERVE * 2:
ensure_funded(ALICE, amt + 0.5)
dest = get_new_address(peer, rand_addr_type())
txid = send_to_address(ALICE, dest, amt)
maybe_mine()
ok(f"Alice round {amt} BTC → {peer} TX={txid[:16]}")
return txid
def tx_psbt_coinjoin() -> str | None:
"""Alice + Bob cooperate via PSBT (PayJoin / collaborative TX)."""
hdr("PSBT Cooperative TX (PayJoin-like)")
ensure_funded(ALICE, 0.5)
ensure_funded("bob", 0.5)
carol_dest = get_new_address("carol", rand_addr_type())
alice_chg = get_new_address(ALICE, rand_addr_type())
bob_chg = get_new_address("bob", rand_addr_type())
alice_pay = rand_btc(0.01, 0.08)
alice_ret = rand_btc(0.005, 0.02)
bob_ret = rand_btc(0.005, 0.02)
outputs = [
{carol_dest: alice_pay},
{alice_chg: alice_ret},
{bob_chg: bob_ret},
]
try:
psbt_res = create_funded_psbt(ALICE, [], outputs, {"fee_rate": 2})
signed_a = process_psbt(ALICE, psbt_res["psbt"])
signed_b = process_psbt("bob", signed_a["psbt"])
final = finalize_psbt(signed_b["psbt"])
if not final.get("complete"):
info("PSBT incomplete, falling back to simple payment")
return tx_simple_payment()
txid = send_raw(final["hex"])
maybe_mine()
ok(f"Cooperative PSBT Alice+Bob TX={txid[:16]}")
return txid
except Exception as e:
info(f"PSBT failed ({e}), falling back to simple payment")
return tx_simple_payment()
def tx_cold_to_hot() -> str | None:
"""Sweep Taproot 'cold' address → P2WPKH 'hot' address (cold storage move)."""
hdr("Cold→Hot Taproot → P2WPKH")
cold_addr = get_new_address(ALICE, "bech32m")
fund_amt = rand_btc(0.15, 0.6)
send_to_address("miner", cold_addr, fund_amt)
mine_confirm(1)
utxos = get_utxos(ALICE, 1)
cold_utxo = next((u for u in utxos if u.get("address") == cold_addr), None)
if not cold_utxo:
info("Cold UTXO not found, skipping")
return None
hot_addr = get_new_address(ALICE, "bech32")
net = round(cold_utxo["amount"] - FEE_RESERVE, 8)
raw = create_raw_tx(
[{"txid": cold_utxo["txid"], "vout": cold_utxo["vout"]}],
[{hot_addr: net}]
)
signed = sign_raw_tx(ALICE, raw)
txid = send_raw(signed["hex"])
maybe_mine()
ok(f"Cold(P2TR)→Hot(P2WPKH) {fund_amt:.8f} BTC TX={txid[:16]}")
return txid
def tx_lightning_channel_like() -> str:
"""Fund a precise msat-aligned amount (simulates LN channel-open output)."""
hdr("Lightning Channel-Open-Like")
ensure_funded(ALICE, 0.5)
# Real LN channel capacities are multiples of 1 000 sats
cap_sats = random.choice([
50_000, 100_000, 200_000, 250_000, 500_000,
1_000_000, 2_000_000, 3_000_000, 5_000_000,
])
cap_btc = round(cap_sats / 1e8, 8)
peer = rand_peer()
dest = get_new_address(peer, "bech32") # LN always opens P2WPKH/P2WSH
if get_balance(ALICE) < cap_btc + FEE_RESERVE * 2:
ensure_funded(ALICE, cap_btc + 0.5)
txid = send_to_address(ALICE, dest, cap_btc)
maybe_mine()
ok(f"Channel-open-like {cap_sats:,} sats → {peer} TX={txid[:16]}")
return txid
def tx_high_freq_small() -> list[str]:
"""Burst of rapid tiny payments — simulates a micro-payment merchant."""
hdr("High-Frequency Small Payments")
ensure_funded(ALICE, 0.5)
n = random.randint(3, 9)
txids = []
for _ in range(n):
if get_balance(ALICE) < 0.001 + FEE_RESERVE:
ensure_funded(ALICE, 0.5)
peer = rand_peer()
dest = get_new_address(peer, "bech32")
amt = rand_btc(0.0001, 0.003)
txid = send_to_address(ALICE, dest, amt)
txids.append(txid)
time.sleep(random.uniform(0.03, 0.12)) # mimic real timing jitter
maybe_mine()
ok(f"Alice fired {n} small payments last={txids[-1][:16]}")
return txids
def tx_receive_multiple_senders() -> None:
"""Multiple wallets independently send Alice funds within the same block."""
hdr("Receive from Multiple Senders")
senders = random.sample(SIDE_WALLETS, random.randint(2, len(SIDE_WALLETS)))
for sender in senders:
ensure_funded(sender, 0.2)
alice_addr = get_new_address(ALICE, rand_addr_type())
amt = rand_btc(0.005, 0.05)
txid = send_to_address(sender, alice_addr, amt)
ok(f" {sender} → Alice {amt:.8f} BTC TX={txid[:16]}")
maybe_mine()
def tx_legacy_address_receive() -> str:
"""A peer sends Alice funds via a legacy P2PKH address (old-school wallet)."""
hdr("Legacy P2PKH Receive")
peer = rand_peer()
ensure_funded(peer, 0.3)
legacy_addr = get_new_address(ALICE, "legacy")
amt = rand_btc(0.002, 0.05)
txid = send_to_address(peer, legacy_addr, amt)
maybe_mine()
ok(f"{peer} → Alice (legacy) {amt:.8f} BTC TX={txid[:16]}")
return txid
def tx_p2sh_wrapped_receive() -> str:
"""Receive into a P2SH-wrapped segwit address (older mobile wallets)."""
hdr("P2SH-Wrapped Segwit Receive")
peer = rand_peer()
ensure_funded(peer, 0.3)
p2sh_addr = get_new_address(ALICE, "p2sh-segwit")
amt = rand_btc(0.002, 0.06)
txid = send_to_address(peer, p2sh_addr, amt)
maybe_mine()
ok(f"{peer} → Alice (p2sh-segwit) {amt:.8f} BTC TX={txid[:16]}")
return txid
def tx_change_avoidance() -> str | None:
"""Alice finds an exact-match UTXO to pay without producing change output."""
hdr("Change-Avoidance Payment (exact UTXO match)")
ensure_funded(ALICE, 0.5)
utxos = get_utxos(ALICE, 1)
if not utxos:
info("No UTXOs, skipping")
return None
utxo = random.choice(utxos)
fee = FEE_RESERVE
net = round(utxo["amount"] - fee, 8)
if net <= DUST_LIMIT:
info("UTXO too small, skipping")
return None
peer = rand_peer()
dest = get_new_address(peer, rand_addr_type())
raw = create_raw_tx(
[{"txid": utxo["txid"], "vout": utxo["vout"]}],
[{dest: net}]
)
signed = sign_raw_tx(ALICE, raw)
txid = send_raw(signed["hex"])
maybe_mine()
ok(f"Change-avoidance {net:.8f} BTC → {peer} TX={txid[:16]}")
return txid
def tx_risky_origin_receive() -> str:
"""Simulate receiving funds from the 'risky' wallet (taint scenario)."""
hdr("Receive from Risky Wallet")
ensure_funded("risky", 0.3)
alice_addr = get_new_address(ALICE, rand_addr_type())
amt = rand_btc(0.003, 0.04)
txid = send_to_address("risky", alice_addr, amt)
maybe_mine()
ok(f"risky → Alice {amt:.8f} BTC TX={txid[:16]}")
return txid
def tx_address_reuse_receive() -> tuple[str, str]:
"""Two different peers send to the same Alice address (natural address-reuse)."""
hdr("Natural Address Reuse (two inbound)")
reused_addr = get_new_address(ALICE, random.choice(["bech32", "bech32m"]))
peer_a, peer_b = random.sample(SIDE_WALLETS, 2)
ensure_funded(peer_a, 0.2)
ensure_funded(peer_b, 0.2)
txid1 = send_to_address(peer_a, reused_addr, rand_btc(0.003, 0.03))
txid2 = send_to_address(peer_b, reused_addr, rand_btc(0.003, 0.03))
maybe_mine()
ok(f"Two peers sent to same Alice addr TX1={txid1[:16]}… TX2={txid2[:16]}")
return txid1, txid2
# ─── Archetype registry (name, function, weight) ─────────────────────────────
ARCHETYPES: list[tuple[str, callable, float]] = [
("simple_payment", tx_simple_payment, 3.5),
("receive_from_peer", tx_receive_from_peer, 3.0),
("round_amount_payment", tx_round_amount_payment, 2.5),
("self_transfer", tx_self_transfer, 2.0),
("multi_output", tx_multi_output, 2.0),
("high_freq_small", tx_high_freq_small, 1.5),
("receive_multiple_senders", tx_receive_multiple_senders, 1.5),
("exchange_withdrawal", tx_exchange_withdrawal, 1.5),
("legacy_address_receive", tx_legacy_address_receive, 1.5),
("p2sh_wrapped_receive", tx_p2sh_wrapped_receive, 1.5),
("change_avoidance", tx_change_avoidance, 1.5),
("consolidation", tx_consolidation, 1.0),
("utxo_split", tx_utxo_split, 1.0),
("chain_hop", tx_chain_hop, 1.0),
("mixed_type_spend", tx_mixed_type_spend, 1.0),
("cold_to_hot", tx_cold_to_hot, 1.0),
("lightning_channel_like", tx_lightning_channel_like, 1.0),
("address_reuse_receive", tx_address_reuse_receive, 1.0),
("risky_origin_receive", tx_risky_origin_receive, 0.5),
("psbt_coinjoin", tx_psbt_coinjoin, 0.5),
]
_NAMES, _FNS, _WEIGHTS = zip(*ARCHETYPES)
_TOTAL_W = sum(_WEIGHTS)
def weighted_choice() -> tuple[str, callable]:
r = random.uniform(0, _TOTAL_W)
cum = 0.0
for name, fn, w in zip(_NAMES, _FNS, _WEIGHTS):
cum += w
if r <= cum:
return name, fn
return _NAMES[-1], _FNS[-1]
# ─── Main ─────────────────────────────────────────────────────────────────────
def main() -> None:
parser = argparse.ArgumentParser(
description="Create n realistic varied Bitcoin transactions for Alice's wallet on regtest",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__,
)
parser.add_argument("n", type=int,
help="Number of transaction events to generate")
parser.add_argument("--seed", type=int, default=None,
help="Fix RNG seed (for reproducible runs)")
parser.add_argument("--mine-final", dest="mine_final",
action="store_true", default=True,
help="Mine a final confirming block after all TXs (default: on)")
parser.add_argument("--no-mine-final", dest="mine_final",
action="store_false",
help="Skip the final confirming block")
args = parser.parse_args()
print(f"\n{B}{C}{''*70}{RST}")
print(f"{B}{C} create_random_transactions.py{RST}")
print(f"{B} Generating {args.n} realistic transaction events for Alice{RST}")
print(f"{B}{C}{''*70}{RST}")
if args.seed is not None:
random.seed(args.seed)
info(f"RNG seeded manually: {args.seed}")
else:
reseed()
# ── Bootstrap: make sure every wallet has funds ──────────────────────────
info("Bootstrapping wallet balances…")
for w in ALL_WALLETS:
ensure_funded(w, 0.3)
mine_confirm(1)
time.sleep(0.3)
# ── Main loop ─────────────────────────────────────────────────────────────
completed = 0
failed = 0
used_types: list[str] = []
next_mine = random.randint(3, 6) # mine after this many events
for i in range(args.n):
name, fn = weighted_choice()
print(f"\n{B}[{i+1}/{args.n}]{RST} {DIM}{name}{RST}")
try:
fn()
completed += 1
used_types.append(name)
except Exception as exc:
warn(f"'{name}' raised: {exc}")
failed += 1
# Fallback: guaranteed-safe simple payment
try:
tx_simple_payment()
completed += 1
used_types.append("simple_payment(fallback)")
except Exception as exc2:
warn(f"Fallback also failed: {exc2}")
# Periodic mining to keep mempool manageable
if (i + 1) >= next_mine:
info("Periodic block mine to clear mempool…")
mine_blocks(random.randint(1, 2))
time.sleep(0.2)
next_mine += random.randint(3, 6)
# ── Final block ───────────────────────────────────────────────────────────
if args.mine_final:
info("Mining final confirming block…")
mine_confirm(1)
# ── Summary ───────────────────────────────────────────────────────────────
type_counts = Counter(used_types)
print(f"\n{B}{C}{''*70}{RST}")
print(f"{B} Summary{RST}")
print(f"{B}{C}{''*70}{RST}")
print(f" Requested : {args.n}")
print(f" Completed : {G}{completed}{RST}")
print(f" Failed : {R if failed else G}{failed}{RST}")
print(f" Chain height: {get_block_count()}")
alice_bal = get_balance(ALICE)
print(f" Alice balance: {G}{alice_bal:.8f}{RST} BTC")
print(f"\n Transaction-type breakdown:")
for t, cnt in type_counts.most_common():
bar = "" * cnt
print(f" {G}{cnt:3d}{RST} {bar[:30]:<30} {t}")
print(f"{B}{C}{''*70}{RST}\n")
if __name__ == "__main__":
main()
+12 -5
View File
@@ -20,11 +20,8 @@ Usage:
import sys
import os
import json
import time
import hashlib
import argparse
from collections import defaultdict
from math import log2
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from bitcoin_rpc import cli, get_tx
@@ -172,6 +169,8 @@ class TxGraph:
self.our_addrs = set(addr_map.keys())
self.utxos = utxos # current UTXOs
self.tx_cache = {} # txid -> decoded tx
self._input_cache = {} # txid -> parsed input addresses
self._output_cache = {} # txid -> parsed output addresses
self.our_txids = set() # txids we participate in
# Index: address -> list of (txid, direction, value)
@@ -205,9 +204,12 @@ class TxGraph:
return self.tx_cache[txid]
def get_input_addresses(self, txid):
"""Get all input addresses for a transaction."""
"""Get all input addresses for a transaction (cached)."""
if txid in self._input_cache:
return self._input_cache[txid]
tx = self.fetch_tx(txid)
if not tx:
self._input_cache[txid] = []
return []
addrs = []
for vin in tx.get("vin", []):
@@ -219,12 +221,16 @@ class TxGraph:
addr = vout_data.get("scriptPubKey", {}).get("address", "")
value = vout_data.get("value", 0)
addrs.append({"address": addr, "value": value, "txid": vin["txid"], "vout": vin["vout"]})
self._input_cache[txid] = addrs
return addrs
def get_output_addresses(self, txid):
"""Get all output addresses for a transaction."""
"""Get all output addresses for a transaction (cached)."""
if txid in self._output_cache:
return self._output_cache[txid]
tx = self.fetch_tx(txid)
if not tx:
self._output_cache[txid] = []
return []
addrs = []
for vout in tx.get("vout", []):
@@ -235,6 +241,7 @@ class TxGraph:
"n": vout["n"],
"type": vout.get("scriptPubKey", {}).get("type", "unknown"),
})
self._output_cache[txid] = addrs
return addrs
def is_ours(self, address):
-28
View File
@@ -1,28 +0,0 @@
#!/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
View File
@@ -1 +0,0 @@
code ~/.bitcoin/bitcoin.conf
-35
View File
@@ -1,35 +0,0 @@
#!/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 "$@"
+40 -68
View File
@@ -3,10 +3,9 @@
# setup.sh — Bootstrap Bitcoin Core regtest for privacy vulnerability testing
# =============================================================================
# Reproduces the full environment:
# • Writes ~/.bitcoin/bitcoin.conf (regtest, txindex, dustrelayfee, etc.)
# • Stops any running bitcoind (both regtest and signet)
# • Optionally wipes the regtest data dir (pass --fresh to start from block 0)
# • Starts: bitcoind -daemon -regtest
# • Starts bitcoind with all config passed via CLI flags (no bitcoin.conf edits)
# • Creates wallets: miner alice bob carol exchange risky
# • Mines 110 blocks so coinbases mature and miner has spendable BTC
#
@@ -16,18 +15,19 @@
# =============================================================================
set -euo pipefail
# ─── Colours ──────────────────────────────────────────────────────────────────
# ─── Config ───────────────────────────────────────────────────────────────────
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DATADIR="${SCRIPT_DIR}/bitcoin-data"
REGTEST_DIR="${DATADIR}/regtest"
WALLETS=(miner alice bob carol exchange risky)
INITIAL_BLOCKS=110 # must be >100 so coinbases mature
# ─── Helpers ──────────────────────────────────────────────────────────────────
G="\033[92m"; Y="\033[93m"; R="\033[91m"; B="\033[1m"; C="\033[96m"; RST="\033[0m"
ok() { echo -e " ${G}${RST} $*"; }
info() { echo -e " ${Y}${RST} $*"; }
err() { echo -e " ${R}${RST} $*"; exit 1; }
# ─── Config ───────────────────────────────────────────────────────────────────
BITCOIN_CONF="${HOME}/.bitcoin/bitcoin.conf"
REGTEST_DIR="${HOME}/.bitcoin/regtest"
WALLETS=(miner alice bob carol exchange risky)
INITIAL_BLOCKS=110 # must be >100 so coinbases mature
MINER_FUND_BTC=500 # approximate, depends on block subsidy
bcli() { bitcoin-cli -datadir="$DATADIR" -regtest "$@"; }
# ─── Parse args ───────────────────────────────────────────────────────────────
FRESH=0
@@ -46,21 +46,13 @@ echo ""
echo -e "${B}Step 1: Stop any running bitcoind${RST}"
# Try to stop regtest instance (port 18443)
if bitcoin-cli -regtest stop 2>/dev/null; then
if bcli stop 2>/dev/null; then
ok "Stopped regtest bitcoind"
sleep 2
else
info "No regtest bitcoind running (or already stopped)"
fi
# Try to stop signet instance (port 38332) if one is running
if bitcoin-cli -signet stop 2>/dev/null; then
ok "Stopped signet bitcoind"
sleep 2
else
info "No signet bitcoind running"
fi
# Hard-kill any remaining bitcoind processes
if pgrep -x bitcoind > /dev/null 2>&1; then
info "Hard-killing remaining bitcoind processes …"
@@ -68,48 +60,29 @@ if pgrep -x bitcoind > /dev/null 2>&1; then
sleep 2
fi
# ─── 2. Write bitcoin.conf ────────────────────────────────────────────────────
echo ""
echo -e "${B}Step 2: Write ${BITCOIN_CONF}${RST}"
mkdir -p "$(dirname "$BITCOIN_CONF")"
cat > "$BITCOIN_CONF" << 'EOF'
# Bitcoin Core configuration
# Network: regtest (local testing only)
regtest=1
txindex=1
[regtest]
# Fee policy — needed so wallets can broadcast without estimatefee data
fallbackfee=0.00010
# Allow outputs as small as 1 sat (needed for dust-attack reproduction)
dustrelayfee=0.00000001
# Accept non-standard transactions (needed for some test scenarios)
acceptnonstdtxn=1
# Enable RPC server
server=1
EOF
ok "Wrote bitcoin.conf"
# ─── 3. Optionally wipe regtest chain ─────────────────────────────────────────
# ─── 2. Optionally wipe regtest chain ────────────────────────────────────────
if [[ $FRESH -eq 1 ]]; then
echo ""
echo -e "${B}Step 3: Wipe regtest data dir${RST}"
echo -e "${B}Step 2: Wipe regtest data dir${RST}"
rm -rf "$REGTEST_DIR"
ok "Wiped ${REGTEST_DIR}"
else
echo ""
info "Step 3: Keeping existing regtest chain (use --fresh to wipe)"
info "Step 2: Keeping existing regtest chain (use --fresh to wipe)"
fi
# ─── 4. Start bitcoind ────────────────────────────────────────────────────────
# ─── 3. Start bitcoind ────────────────────────────────────────────────────────
echo ""
echo -e "${B}Step 4: Start bitcoind -daemon -regtest${RST}"
bitcoind -daemon -regtest
echo -e "${B}Step 3: Start bitcoind${RST}"
mkdir -p "$DATADIR"
bitcoind -daemon \
-datadir="$DATADIR" \
-regtest \
-txindex=1 \
-server=1 \
-fallbackfee=0.00010 \
-dustrelayfee=0.00000001 \
-acceptnonstdtxn=1
ok "bitcoind launched"
# Wait for RPC to become ready
@@ -117,7 +90,7 @@ echo -n " … waiting for RPC"
for i in $(seq 1 30); do
sleep 1
echo -n "."
if bitcoin-cli -regtest getblockchaininfo > /dev/null 2>&1; then
if bcli getblockchaininfo > /dev/null 2>&1; then
echo ""
ok "RPC ready after ${i}s"
break
@@ -128,18 +101,18 @@ for i in $(seq 1 30); do
fi
done
BLOCKS=$(bitcoin-cli -regtest getblockcount)
BLOCKS=$(bcli getblockcount)
info "Chain height: ${BLOCKS} blocks"
# ─── 5. Create / load wallets ─────────────────────────────────────────────────
# ─── 4. Create / load wallets ─────────────────────────────────────────────────
echo ""
echo -e "${B}Step 5: Create wallets${RST}"
echo -e "${B}Step 4: Create wallets${RST}"
for w in "${WALLETS[@]}"; do
if bitcoin-cli -regtest createwallet "$w" 2>/dev/null | grep -q '"name"'; then
if bcli createwallet "$w" 2>/dev/null | grep -q '"name"'; then
ok "Created wallet: ${w}"
else
# Wallet DB already exists on disk — just load it
if bitcoin-cli -regtest loadwallet "$w" 2>/dev/null | grep -q '"name"'; then
if bcli loadwallet "$w" 2>/dev/null | grep -q '"name"'; then
ok "Loaded existing wallet: ${w}"
else
# Already loaded (returned error -35)
@@ -148,32 +121,32 @@ for w in "${WALLETS[@]}"; do
fi
done
# ─── 6. Mine initial blocks (only if fresh or chain has <110 blocks) ──────────
# ─── 5. Mine initial blocks (only if fresh or chain has <110 blocks) ──────────
echo ""
echo -e "${B}Step 6: Mine initial blocks${RST}"
BLOCKS=$(bitcoin-cli -regtest getblockcount)
echo -e "${B}Step 5: Mine initial blocks${RST}"
BLOCKS=$(bcli getblockcount)
if [[ $BLOCKS -lt $INITIAL_BLOCKS ]]; then
NEED=$(( INITIAL_BLOCKS - BLOCKS ))
info "At block ${BLOCKS}, need ${NEED} more to reach ${INITIAL_BLOCKS}"
MINER_ADDR=$(bitcoin-cli -regtest -rpcwallet=miner getnewaddress "" bech32)
bitcoin-cli -regtest generatetoaddress "$NEED" "$MINER_ADDR" > /dev/null
BLOCKS=$(bitcoin-cli -regtest getblockcount)
MINER_ADDR=$(bcli -rpcwallet=miner getnewaddress "" bech32)
bcli generatetoaddress "$NEED" "$MINER_ADDR" > /dev/null
BLOCKS=$(bcli getblockcount)
ok "Mined to block ${BLOCKS}"
else
ok "Already at block ${BLOCKS} — no mining needed"
fi
MINER_BAL=$(bitcoin-cli -regtest -rpcwallet=miner getbalance)
MINER_BAL=$(bcli -rpcwallet=miner getbalance)
ok "Miner balance: ${MINER_BAL} BTC"
# ─── 7. Summary ───────────────────────────────────────────────────────────────
# ─── 6. Summary ───────────────────────────────────────────────────────────────
echo ""
echo -e "${B}${C}══════════════════════════════════════════════════════════${RST}"
echo -e "${B} Setup complete!${RST}"
echo -e "${B}${C}══════════════════════════════════════════════════════════${RST}"
echo -e " Chain: ${G}regtest${RST}"
echo -e " Blocks: ${G}$(bitcoin-cli -regtest getblockcount)${RST}"
echo -e " Blocks: ${G}$(bcli getblockcount)${RST}"
echo -e " Wallets: ${G}${WALLETS[*]}${RST}"
echo ""
echo -e " Next steps:"
@@ -181,5 +154,4 @@ echo -e " python3 reproduce.py # create 12 vulnerability scenarios"
echo -e " python3 detect.py --wallet alice \\"
echo -e " --known-risky-wallets risky \\"
echo -e " --known-exchange-wallets exchange"
echo -e " python3 verify.py --fresh # full automated proof"
echo ""
-294
View File
@@ -1,294 +0,0 @@
#!/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"
File diff suppressed because it is too large Load Diff
-258
View File
@@ -1,258 +0,0 @@
#!/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 (112) 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()
+1 -16
View File
@@ -66,22 +66,7 @@
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.bitcoindevkit</groupId>
<artifactId>bdk-jvm</artifactId>
<version>0.30.0</version>
</dependency>
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna</artifactId>
<version>5.13.0</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId>
<version>2.1.0</version>
</dependency>
<dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
@@ -4,13 +4,9 @@ import jakarta.enterprise.context.ApplicationScoped;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.backend.stealth.mocks.WalletMockData;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
@ApplicationScoped
@Path("/api/wallet")
@@ -21,56 +17,6 @@ public class WalletResource {
@ConfigProperty(name = "stealth.detect.script", defaultValue = "../../script/detect.py")
String detectScript;
private static final Map<String, String> sessions = new ConcurrentHashMap<>();
// DTOs
public record AnalyzeRequest(String descriptor) {}
public record AnalyzeResponse(String analysisId) {}
public record VulnerabilityData(String type, String severity, String description) {}
public record UtxoData(
String txid,
int vout,
String address,
double amountBtc,
int confirmations,
List<VulnerabilityData> vulnerabilities
) {}
public record SummaryData(int total, int clean, int vulnerable) {}
public record ReportResponse(String descriptor, SummaryData summary, List<UtxoData> utxos) {}
// Endpoints
@POST
@Path("/analyze")
public Response analyze(AnalyzeRequest req) {
if (req == null || req.descriptor() == null || req.descriptor().isBlank()) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("error", "descriptor is required"))
.build();
}
String analysisId = UUID.randomUUID().toString();
sessions.put(analysisId, req.descriptor());
return Response.ok(new AnalyzeResponse(analysisId)).build();
}
@GET
@Path("/{analysisId}/utxos")
public Response getUtxos(@PathParam("analysisId") String analysisId) {
String descriptor = sessions.get(analysisId);
if (descriptor == null) {
return Response.status(Response.Status.NOT_FOUND)
.entity(Map.of("error", "analysisId not found"))
.build();
}
return Response.ok(WalletMockData.buildReport(descriptor)).build();
}
@GET
@Path("/scan")
public Response scan(@QueryParam("descriptor") String descriptor) {
@@ -1,22 +0,0 @@
package org.backend.stealth.controller.response;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import org.backend.stealth.service.impl.WalletController;
import org.bitcoindevkit.BdkException;
@Path("/hello")
public class ExampleResponse {
@GET
@Produces(MediaType.TEXT_PLAIN)
public String hello() throws BdkException {
WalletController controller = new WalletController();
controller.ConnectWallet();
return "Hello from Quarkus REST";
}
}
@@ -1,9 +0,0 @@
package org.backend.stealth.domain.entity;
public class UTXO {
private String value;
private String scriptPubKey;
private String txid;
private Integer vout;
}
@@ -1,31 +0,0 @@
package org.backend.stealth.domain.entity;
public class Wallet {
private Integer id;
private String descriptor;
public Wallet() {}
public Wallet(Integer id, String descriptor) {
this.id = id;
this.descriptor = descriptor;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getDescriptor() {
return descriptor;
}
public void setDescriptor(String descriptor) {
this.descriptor = descriptor;
}
}
@@ -1,4 +0,0 @@
package org.backend.stealth.domain.repository;
public class BitcoinRepository {
}
@@ -1,75 +0,0 @@
package org.backend.stealth.mocks;
import org.backend.stealth.controller.WalletResource.ReportResponse;
import org.backend.stealth.controller.WalletResource.SummaryData;
import org.backend.stealth.controller.WalletResource.UtxoData;
import org.backend.stealth.controller.WalletResource.VulnerabilityData;
import java.util.List;
public class WalletMockData {
public static ReportResponse buildReport(String descriptor) {
List<UtxoData> utxos = List.of(
new UtxoData(
"3a7f2b8c1d4e9f0a6b5c2d7e8f3a1b4c9d2e5f0a7b8c1d4e9f2a5b6c3d7e8f1",
0,
"bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
0.05234891,
1842,
List.of()
),
new UtxoData(
"b4c8e2f6a1d5b9c3e7f1a5d9b3c7e1f5a9d3b7c1e5f9a3d7b1c5e9f3a7d1b5",
1,
"bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq",
0.00023000,
312,
List.of(
new VulnerabilityData("DUST_SPEND", "medium",
"This UTXO is near the dust threshold. Spending it may cost more in fees than its value, and dust outputs are often used as tracking vectors by chain surveillance companies."),
new VulnerabilityData("ADDRESS_REUSE", "high",
"This address has received funds in 3 separate transactions. Address reuse breaks the one-time-address privacy model and allows observers to link all deposits to the same wallet.")
)
),
new UtxoData(
"f9e3d7c1b5a9f3d7c1b5a9f3d7c1b5a9f3d7c1b5a9f3d7c1b5a9f3d7c1b5a9",
0,
"bc1q9h7garjcdkl4h5khfz2yxkhsmhep5j7g4cjtch",
0.12000000,
4521,
List.of(
new VulnerabilityData("CONSOLIDATION", "medium",
"This UTXO was created by consolidating 7 inputs in a single transaction. Consolidation reveals that all input addresses belong to the same wallet, reducing privacy significantly.")
)
),
new UtxoData(
"2c6e0a4f8b2d6e0a4f8b2d6e0a4f8b2d6e0a4f8b2d6e0a4f8b2d6e0a4f8b2d",
2,
"bc1qm34mqf4vn8f5vhf0q3djg2zuzfm9aap6e3n4j",
0.87654321,
98,
List.of(
new VulnerabilityData("CIOH", "high",
"Common Input Ownership Heuristic (CIOH): this UTXO was spent alongside UTXOs from different derivation paths in the same transaction, strongly suggesting to analysts that all inputs share a common owner."),
new VulnerabilityData("ADDRESS_REUSE", "high",
"This address appears in 5 transactions as both sender and receiver, a pattern that severely compromises wallet privacy and makes cluster analysis trivial.")
)
),
new UtxoData(
"7d1b5e9f3a7d1b5e9f3a7d1b5e9f3a7d1b5e9f3a7d1b5e9f3a7d1b5e9f3a7d",
0,
"bc1qcr8te4kr609gcawutmrza0j4xv80jy8zeqchgx",
0.00500000,
2103,
List.of(
new VulnerabilityData("DUST_SPEND", "low",
"A small dust amount was received at this address in a prior transaction. While the dust has not been spent, its presence could be used to track this UTXO if included in a future transaction.")
)
)
);
SummaryData summary = new SummaryData(5, 1, 4);
return new ReportResponse(descriptor, summary, utxos);
}
}
@@ -1,31 +0,0 @@
package org.backend.stealth.service.impl;
import org.bitcoindevkit.*;
public class WalletController {
public void ConnectWallet() throws BdkException {
Mnemonic mnemonic = new Mnemonic(WordCount.WORDS12);
DescriptorSecretKey masterKey = new DescriptorSecretKey(
Network.REGTEST,
mnemonic,
""
);
String externalDescStr = "wpkh(" + masterKey.asString() + "/84'/1'/0'/0/*)";
Descriptor externalDescriptor = new Descriptor(externalDescStr, Network.REGTEST);
Wallet wallet = new Wallet(
externalDescriptor,
null, // changeDescriptor (pode continuar null por enquanto)
Network.REGTEST,
DatabaseConfig.Memory.INSTANCE
);
System.out.println("✅ Carteira criada com sucesso! Endereço: " +
wallet.getAddress(AddressIndex.New.INSTANCE).getAddress());
}
}
@@ -1,6 +0,0 @@
package org.backend.stealth.utils;
public class WalletUtils {
}
@@ -1,8 +0,0 @@
package org.backend.stealth;
import io.quarkus.test.junit.QuarkusIntegrationTest;
@QuarkusIntegrationTest
class ExampleResponseIT extends ExampleResponseTest {
// Execute the same tests but in packaged mode.
}
@@ -1,20 +0,0 @@
package org.backend.stealth;
import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.is;
@QuarkusTest
class ExampleResponseTest {
@Test
void testHelloEndpoint() {
given()
.when().get("/hello")
.then()
.statusCode(200)
.body(is("Hello from Quarkus REST"));
}
}
-84
View File
@@ -1,84 +0,0 @@
import { useState } from 'react'
import VulnerabilityBadge from './VulnerabilityBadge'
import styles from './UtxoCard.module.css'
function truncateAddress(addr) {
if (!addr || addr.length <= 20) return addr
return `${addr.slice(0, 12)}${addr.slice(-8)}`
}
function truncateTxid(txid) {
if (!txid || txid.length <= 24) return txid
return `${txid.slice(0, 16)}${txid.slice(-8)}`
}
export default function UtxoCard({ utxo }) {
const [open, setOpen] = useState(false)
const isClean = utxo.vulnerabilities.length === 0
const highestSeverity = utxo.vulnerabilities.reduce((acc, v) => {
const order = { high: 3, medium: 2, low: 1 }
return (order[v.severity] ?? 0) > (order[acc] ?? 0) ? v.severity : acc
}, null)
return (
<div
className={`${styles.card} ${isClean ? styles.clean : styles.hasVulnerabilities}`}
>
<div
className={styles.header}
onClick={() => setOpen((o) => !o)}
role="button"
aria-expanded={open}
>
<div className={styles.headerLeft}>
<div className={styles.addressRow}>
<span className={styles.address} title={utxo.address}>
{truncateAddress(utxo.address)}
</span>
</div>
<div className={styles.badges}>
{isClean ? (
<span className={styles.cleanLabel}> Clean</span>
) : (
utxo.vulnerabilities.map((v, i) => (
<VulnerabilityBadge key={i} type={v.type} severity={v.severity} />
))
)}
</div>
</div>
<div className={styles.headerRight}>
<span className={styles.amount}>
{utxo.amountBtc.toFixed(8)} BTC
</span>
<span className={styles.confirmations}>
{utxo.confirmations.toLocaleString()} confs
</span>
</div>
<span className={`${styles.chevron} ${open ? styles.open : ''}`}></span>
</div>
<div className={`${styles.detail} ${open ? styles.open : ''}`}>
<span className={styles.txidLabel}>txid</span>
<div className={styles.txid}>
{utxo.txid}:{utxo.vout}
</div>
{!isClean && (
<div className={styles.vulnerabilityList}>
{utxo.vulnerabilities.map((v, i) => (
<div key={i} className={`${styles.vulnItem} ${styles[v.severity]}`}>
<div className={styles.vulnHeader}>
<VulnerabilityBadge type={v.type} severity={v.severity} />
</div>
<p className={styles.vulnDesc}>{v.description}</p>
</div>
))}
</div>
)}
</div>
</div>
)
}
-168
View File
@@ -1,168 +0,0 @@
.card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
overflow: hidden;
transition: border-color var(--transition);
}
.card:hover {
border-color: var(--border-hover);
}
.card.hasVulnerabilities {
border-left: 3px solid var(--danger);
}
.card.clean {
border-left: 3px solid var(--accent);
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
cursor: pointer;
gap: 16px;
user-select: none;
}
.headerLeft {
display: flex;
flex-direction: column;
gap: 6px;
min-width: 0;
flex: 1;
}
.addressRow {
display: flex;
align-items: center;
gap: 8px;
}
.address {
font-family: var(--font-data);
font-size: 14px;
color: var(--text);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.badges {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.cleanLabel {
font-size: 11px;
font-weight: 600;
color: var(--accent);
letter-spacing: 0.05em;
text-transform: uppercase;
}
.headerRight {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 4px;
flex-shrink: 0;
}
.amount {
font-family: var(--font-data);
font-size: 16px;
font-weight: 500;
color: var(--text);
}
.confirmations {
font-size: 12px;
color: var(--text-muted);
}
.chevron {
color: var(--text-muted);
font-size: 12px;
transition: transform var(--transition);
flex-shrink: 0;
}
.chevron.open {
transform: rotate(180deg);
}
/* Detail panel */
.detail {
border-top: 1px solid var(--border);
padding: 0;
overflow: hidden;
max-height: 0;
transition: max-height 0.3s ease, padding 0.3s ease;
}
.detail.open {
max-height: 600px;
padding: 16px 20px;
}
.txid {
font-family: var(--font-data);
font-size: 12px;
color: var(--text-muted);
word-break: break-all;
margin-bottom: 16px;
}
.txidLabel {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-dim);
display: block;
margin-bottom: 4px;
}
.vulnerabilityList {
display: flex;
flex-direction: column;
gap: 12px;
}
.vulnItem {
border-radius: var(--radius);
padding: 14px 16px;
}
.vulnItem.high {
background: var(--danger-dim);
border: 1px solid rgba(255, 77, 109, 0.2);
}
.vulnItem.medium {
background: var(--warning-dim);
border: 1px solid rgba(244, 162, 97, 0.2);
}
.vulnItem.low {
background: var(--safe-dim);
border: 1px solid rgba(46, 196, 182, 0.2);
}
.vulnHeader {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
}
.vulnDesc {
font-size: 13px;
color: var(--text-muted);
line-height: 1.6;
}
-90
View File
@@ -1,90 +0,0 @@
export const mockReport = {
descriptor: 'wpkh([a1b2c3d4/84h/0h/0h]xpub6CatWdiZynkCminahu8Gmr7FAVnQXBTSMaBxn6qmBNkdm9tDkFzWmjmDrLBCQSTa7BHgpEjCXzMTCyDsQLSmcGYJHBB7cTwpqLNRKGP47uw/0/*)#qwer1234',
summary: {
total: 5,
clean: 1,
vulnerable: 4,
},
utxos: [
{
txid: '3a7f2b8c1d4e9f0a6b5c2d7e8f3a1b4c9d2e5f0a7b8c1d4e9f2a5b6c3d7e8f1',
vout: 0,
address: 'bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh',
amountBtc: 0.05234891,
confirmations: 1842,
vulnerabilities: [],
},
{
txid: 'b4c8e2f6a1d5b9c3e7f1a5d9b3c7e1f5a9d3b7c1e5f9a3d7b1c5e9f3a7d1b5',
vout: 1,
address: 'bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq',
amountBtc: 0.00023000,
confirmations: 312,
vulnerabilities: [
{
type: 'DUST_SPEND',
severity: 'medium',
description:
'This UTXO is near the dust threshold. Spending it may cost more in fees than its value, and dust outputs are often used as tracking vectors by chain surveillance companies.',
},
{
type: 'ADDRESS_REUSE',
severity: 'high',
description:
'This address has received funds in 3 separate transactions. Address reuse breaks the one-time-address privacy model and allows observers to link all deposits to the same wallet.',
},
],
},
{
txid: 'f9e3d7c1b5a9f3d7c1b5a9f3d7c1b5a9f3d7c1b5a9f3d7c1b5a9f3d7c1b5a9',
vout: 0,
address: 'bc1q9h7garjcdkl4h5khfz2yxkhsmhep5j7g4cjtch',
amountBtc: 0.12000000,
confirmations: 4521,
vulnerabilities: [
{
type: 'CONSOLIDATION',
severity: 'medium',
description:
'This UTXO was created by consolidating 7 inputs in a single transaction. Consolidation reveals that all input addresses belong to the same wallet, reducing privacy significantly.',
},
],
},
{
txid: '2c6e0a4f8b2d6e0a4f8b2d6e0a4f8b2d6e0a4f8b2d6e0a4f8b2d6e0a4f8b2d',
vout: 2,
address: 'bc1qm34mqf4vn8f5vhf0q3djg2zuzfm9aap6e3n4j',
amountBtc: 0.87654321,
confirmations: 98,
vulnerabilities: [
{
type: 'CIOH',
severity: 'high',
description:
'Common Input Ownership Heuristic (CIOH): this UTXO was spent alongside UTXOs from different derivation paths in the same transaction, strongly suggesting to analysts that all inputs share a common owner.',
},
{
type: 'ADDRESS_REUSE',
severity: 'high',
description:
'This address appears in 5 transactions as both sender and receiver, a pattern that severely compromises wallet privacy and makes cluster analysis trivial.',
},
],
},
{
txid: '7d1b5e9f3a7d1b5e9f3a7d1b5e9f3a7d1b5e9f3a7d1b5e9f3a7d1b5e9f3a7d',
vout: 0,
address: 'bc1qcr8te4kr609gcawutmrza0j4xv80jy8zeqchgx',
amountBtc: 0.00500000,
confirmations: 2103,
vulnerabilities: [
{
type: 'DUST_SPEND',
severity: 'low',
description:
'A small dust amount was received at this address in a prior transaction. While the dust has not been spent, its presence could be used to track this UTXO if included in a future transaction.',
},
],
},
],
}
+5 -4
View File
@@ -2,10 +2,11 @@ import { useState, useEffect } from 'react'
import styles from './LoadingScreen.module.css'
const MESSAGES = [
'Parsing descriptor',
'Fetching transactions',
'Scanning UTXO set',
'Running heuristics',
'Resolving descriptors',
'Deriving addresses',
'Importing & scanning blockchain',
'Loading transaction history',
'Running vulnerability detectors',
]
export default function LoadingScreen({ descriptor }) {