mirror of
https://github.com/LORDBABUINO/stealth.git
synced 2026-07-27 17:18:10 -07:00
Merge pull request #5 from LORDBABUINO/cleanup/remove-unused-code
Remove unused code, optimize critical path, fix datadir auth
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
backend/script/bitcoin-data/
|
||||||
node_modules/
|
node_modules/
|
||||||
dist/
|
dist/
|
||||||
.env
|
.env
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ Connection settings are read from config.ini in the same directory.
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import subprocess
|
import subprocess
|
||||||
import time
|
|
||||||
import os
|
import os
|
||||||
import configparser
|
import configparser
|
||||||
|
|
||||||
@@ -23,6 +22,13 @@ def _build_base_args(section):
|
|||||||
|
|
||||||
args = [cli_bin]
|
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 = {
|
network_flags = {
|
||||||
"regtest": "-regtest",
|
"regtest": "-regtest",
|
||||||
"testnet": "-testnet",
|
"testnet": "-testnet",
|
||||||
@@ -42,10 +48,6 @@ def _build_base_args(section):
|
|||||||
_cfg = _load_config()
|
_cfg = _load_config()
|
||||||
_BASE_ARGS = _build_base_args(_cfg)
|
_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):
|
def cli(*args, wallet=None):
|
||||||
"""Call bitcoin-cli [network] [wallet] <args> and return parsed JSON or string."""
|
"""Call bitcoin-cli [network] [wallet] <args> and return parsed JSON or string."""
|
||||||
cmd = list(_BASE_ARGS)
|
cmd = list(_BASE_ARGS)
|
||||||
@@ -73,23 +75,6 @@ def mine_blocks(n=1):
|
|||||||
return int(cli("getblockcount"))
|
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):
|
def get_tx(txid):
|
||||||
"""Get decoded transaction."""
|
"""Get decoded transaction."""
|
||||||
return cli("getrawtransaction", txid, "true")
|
return cli("getrawtransaction", txid, "true")
|
||||||
@@ -128,11 +113,6 @@ def finalize_psbt(psbt):
|
|||||||
return cli("finalizepsbt", psbt)
|
return cli("finalizepsbt", psbt)
|
||||||
|
|
||||||
|
|
||||||
def decode_psbt(psbt):
|
|
||||||
"""Decode a PSBT."""
|
|
||||||
return cli("decodepsbt", psbt)
|
|
||||||
|
|
||||||
|
|
||||||
def create_raw_tx(inputs, outputs):
|
def create_raw_tx(inputs, outputs):
|
||||||
"""Create a raw transaction."""
|
"""Create a raw transaction."""
|
||||||
return cli("createrawtransaction", json.dumps(inputs), json.dumps(outputs))
|
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)
|
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():
|
def get_block_count():
|
||||||
"""Get current block height."""
|
"""Get current block height."""
|
||||||
return int(cli("getblockcount"))
|
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)
|
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,8 +5,12 @@ network = regtest
|
|||||||
# Path to the bitcoin-cli binary (use full path if not on PATH)
|
# Path to the bitcoin-cli binary (use full path if not on PATH)
|
||||||
cli = bitcoin-cli
|
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.
|
# 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 =
|
rpchost =
|
||||||
rpcport =
|
rpcport =
|
||||||
rpcuser =
|
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()
|
|
||||||
@@ -20,11 +20,8 @@ Usage:
|
|||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
import time
|
|
||||||
import hashlib
|
|
||||||
import argparse
|
import argparse
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from math import log2
|
|
||||||
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
from bitcoin_rpc import cli, get_tx
|
from bitcoin_rpc import cli, get_tx
|
||||||
@@ -172,6 +169,8 @@ class TxGraph:
|
|||||||
self.our_addrs = set(addr_map.keys())
|
self.our_addrs = set(addr_map.keys())
|
||||||
self.utxos = utxos # current UTXOs
|
self.utxos = utxos # current UTXOs
|
||||||
self.tx_cache = {} # txid -> decoded tx
|
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
|
self.our_txids = set() # txids we participate in
|
||||||
|
|
||||||
# Index: address -> list of (txid, direction, value)
|
# Index: address -> list of (txid, direction, value)
|
||||||
@@ -205,9 +204,12 @@ class TxGraph:
|
|||||||
return self.tx_cache[txid]
|
return self.tx_cache[txid]
|
||||||
|
|
||||||
def get_input_addresses(self, 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)
|
tx = self.fetch_tx(txid)
|
||||||
if not tx:
|
if not tx:
|
||||||
|
self._input_cache[txid] = []
|
||||||
return []
|
return []
|
||||||
addrs = []
|
addrs = []
|
||||||
for vin in tx.get("vin", []):
|
for vin in tx.get("vin", []):
|
||||||
@@ -219,12 +221,16 @@ class TxGraph:
|
|||||||
addr = vout_data.get("scriptPubKey", {}).get("address", "")
|
addr = vout_data.get("scriptPubKey", {}).get("address", "")
|
||||||
value = vout_data.get("value", 0)
|
value = vout_data.get("value", 0)
|
||||||
addrs.append({"address": addr, "value": value, "txid": vin["txid"], "vout": vin["vout"]})
|
addrs.append({"address": addr, "value": value, "txid": vin["txid"], "vout": vin["vout"]})
|
||||||
|
self._input_cache[txid] = addrs
|
||||||
return addrs
|
return addrs
|
||||||
|
|
||||||
def get_output_addresses(self, txid):
|
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)
|
tx = self.fetch_tx(txid)
|
||||||
if not tx:
|
if not tx:
|
||||||
|
self._output_cache[txid] = []
|
||||||
return []
|
return []
|
||||||
addrs = []
|
addrs = []
|
||||||
for vout in tx.get("vout", []):
|
for vout in tx.get("vout", []):
|
||||||
@@ -235,6 +241,7 @@ class TxGraph:
|
|||||||
"n": vout["n"],
|
"n": vout["n"],
|
||||||
"type": vout.get("scriptPubKey", {}).get("type", "unknown"),
|
"type": vout.get("scriptPubKey", {}).get("type", "unknown"),
|
||||||
})
|
})
|
||||||
|
self._output_cache[txid] = addrs
|
||||||
return addrs
|
return addrs
|
||||||
|
|
||||||
def is_ours(self, address):
|
def is_ours(self, address):
|
||||||
|
|||||||
@@ -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 +0,0 @@
|
|||||||
code ~/.bitcoin/bitcoin.conf
|
|
||||||
@@ -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
@@ -3,10 +3,9 @@
|
|||||||
# setup.sh — Bootstrap Bitcoin Core regtest for privacy vulnerability testing
|
# setup.sh — Bootstrap Bitcoin Core regtest for privacy vulnerability testing
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Reproduces the full environment:
|
# Reproduces the full environment:
|
||||||
# • Writes ~/.bitcoin/bitcoin.conf (regtest, txindex, dustrelayfee, etc.)
|
|
||||||
# • Stops any running bitcoind (both regtest and signet)
|
# • Stops any running bitcoind (both regtest and signet)
|
||||||
# • Optionally wipes the regtest data dir (pass --fresh to start from block 0)
|
# • 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
|
# • Creates wallets: miner alice bob carol exchange risky
|
||||||
# • Mines 110 blocks so coinbases mature and miner has spendable BTC
|
# • Mines 110 blocks so coinbases mature and miner has spendable BTC
|
||||||
#
|
#
|
||||||
@@ -16,18 +15,19 @@
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
set -euo pipefail
|
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"
|
G="\033[92m"; Y="\033[93m"; R="\033[91m"; B="\033[1m"; C="\033[96m"; RST="\033[0m"
|
||||||
ok() { echo -e " ${G}✓${RST} $*"; }
|
ok() { echo -e " ${G}✓${RST} $*"; }
|
||||||
info() { echo -e " ${Y}ℹ${RST} $*"; }
|
info() { echo -e " ${Y}ℹ${RST} $*"; }
|
||||||
err() { echo -e " ${R}✗${RST} $*"; exit 1; }
|
err() { echo -e " ${R}✗${RST} $*"; exit 1; }
|
||||||
|
bcli() { bitcoin-cli -datadir="$DATADIR" -regtest "$@"; }
|
||||||
# ─── 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
|
|
||||||
|
|
||||||
# ─── Parse args ───────────────────────────────────────────────────────────────
|
# ─── Parse args ───────────────────────────────────────────────────────────────
|
||||||
FRESH=0
|
FRESH=0
|
||||||
@@ -46,21 +46,13 @@ echo ""
|
|||||||
echo -e "${B}Step 1: Stop any running bitcoind${RST}"
|
echo -e "${B}Step 1: Stop any running bitcoind${RST}"
|
||||||
|
|
||||||
# Try to stop regtest instance (port 18443)
|
# 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"
|
ok "Stopped regtest bitcoind"
|
||||||
sleep 2
|
sleep 2
|
||||||
else
|
else
|
||||||
info "No regtest bitcoind running (or already stopped)"
|
info "No regtest bitcoind running (or already stopped)"
|
||||||
fi
|
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
|
# Hard-kill any remaining bitcoind processes
|
||||||
if pgrep -x bitcoind > /dev/null 2>&1; then
|
if pgrep -x bitcoind > /dev/null 2>&1; then
|
||||||
info "Hard-killing remaining bitcoind processes …"
|
info "Hard-killing remaining bitcoind processes …"
|
||||||
@@ -68,48 +60,29 @@ if pgrep -x bitcoind > /dev/null 2>&1; then
|
|||||||
sleep 2
|
sleep 2
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ─── 2. Write bitcoin.conf ────────────────────────────────────────────────────
|
# ─── 2. Optionally wipe regtest chain ────────────────────────────────────────
|
||||||
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 ─────────────────────────────────────────
|
|
||||||
if [[ $FRESH -eq 1 ]]; then
|
if [[ $FRESH -eq 1 ]]; then
|
||||||
echo ""
|
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"
|
rm -rf "$REGTEST_DIR"
|
||||||
ok "Wiped ${REGTEST_DIR}"
|
ok "Wiped ${REGTEST_DIR}"
|
||||||
else
|
else
|
||||||
echo ""
|
echo ""
|
||||||
info "Step 3: Keeping existing regtest chain (use --fresh to wipe)"
|
info "Step 2: Keeping existing regtest chain (use --fresh to wipe)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ─── 4. Start bitcoind ────────────────────────────────────────────────────────
|
# ─── 3. Start bitcoind ────────────────────────────────────────────────────────
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${B}Step 4: Start bitcoind -daemon -regtest${RST}"
|
echo -e "${B}Step 3: Start bitcoind${RST}"
|
||||||
bitcoind -daemon -regtest
|
mkdir -p "$DATADIR"
|
||||||
|
bitcoind -daemon \
|
||||||
|
-datadir="$DATADIR" \
|
||||||
|
-regtest \
|
||||||
|
-txindex=1 \
|
||||||
|
-server=1 \
|
||||||
|
-fallbackfee=0.00010 \
|
||||||
|
-dustrelayfee=0.00000001 \
|
||||||
|
-acceptnonstdtxn=1
|
||||||
ok "bitcoind launched"
|
ok "bitcoind launched"
|
||||||
|
|
||||||
# Wait for RPC to become ready
|
# Wait for RPC to become ready
|
||||||
@@ -117,7 +90,7 @@ echo -n " … waiting for RPC"
|
|||||||
for i in $(seq 1 30); do
|
for i in $(seq 1 30); do
|
||||||
sleep 1
|
sleep 1
|
||||||
echo -n "."
|
echo -n "."
|
||||||
if bitcoin-cli -regtest getblockchaininfo > /dev/null 2>&1; then
|
if bcli getblockchaininfo > /dev/null 2>&1; then
|
||||||
echo ""
|
echo ""
|
||||||
ok "RPC ready after ${i}s"
|
ok "RPC ready after ${i}s"
|
||||||
break
|
break
|
||||||
@@ -128,18 +101,18 @@ for i in $(seq 1 30); do
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
BLOCKS=$(bitcoin-cli -regtest getblockcount)
|
BLOCKS=$(bcli getblockcount)
|
||||||
info "Chain height: ${BLOCKS} blocks"
|
info "Chain height: ${BLOCKS} blocks"
|
||||||
|
|
||||||
# ─── 5. Create / load wallets ─────────────────────────────────────────────────
|
# ─── 4. Create / load wallets ─────────────────────────────────────────────────
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${B}Step 5: Create wallets${RST}"
|
echo -e "${B}Step 4: Create wallets${RST}"
|
||||||
for w in "${WALLETS[@]}"; do
|
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}"
|
ok "Created wallet: ${w}"
|
||||||
else
|
else
|
||||||
# Wallet DB already exists on disk — just load it
|
# 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}"
|
ok "Loaded existing wallet: ${w}"
|
||||||
else
|
else
|
||||||
# Already loaded (returned error -35)
|
# Already loaded (returned error -35)
|
||||||
@@ -148,32 +121,32 @@ for w in "${WALLETS[@]}"; do
|
|||||||
fi
|
fi
|
||||||
done
|
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 ""
|
||||||
echo -e "${B}Step 6: Mine initial blocks${RST}"
|
echo -e "${B}Step 5: Mine initial blocks${RST}"
|
||||||
BLOCKS=$(bitcoin-cli -regtest getblockcount)
|
BLOCKS=$(bcli getblockcount)
|
||||||
|
|
||||||
if [[ $BLOCKS -lt $INITIAL_BLOCKS ]]; then
|
if [[ $BLOCKS -lt $INITIAL_BLOCKS ]]; then
|
||||||
NEED=$(( INITIAL_BLOCKS - BLOCKS ))
|
NEED=$(( INITIAL_BLOCKS - BLOCKS ))
|
||||||
info "At block ${BLOCKS}, need ${NEED} more to reach ${INITIAL_BLOCKS}"
|
info "At block ${BLOCKS}, need ${NEED} more to reach ${INITIAL_BLOCKS}"
|
||||||
MINER_ADDR=$(bitcoin-cli -regtest -rpcwallet=miner getnewaddress "" bech32)
|
MINER_ADDR=$(bcli -rpcwallet=miner getnewaddress "" bech32)
|
||||||
bitcoin-cli -regtest generatetoaddress "$NEED" "$MINER_ADDR" > /dev/null
|
bcli generatetoaddress "$NEED" "$MINER_ADDR" > /dev/null
|
||||||
BLOCKS=$(bitcoin-cli -regtest getblockcount)
|
BLOCKS=$(bcli getblockcount)
|
||||||
ok "Mined to block ${BLOCKS}"
|
ok "Mined to block ${BLOCKS}"
|
||||||
else
|
else
|
||||||
ok "Already at block ${BLOCKS} — no mining needed"
|
ok "Already at block ${BLOCKS} — no mining needed"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
MINER_BAL=$(bitcoin-cli -regtest -rpcwallet=miner getbalance)
|
MINER_BAL=$(bcli -rpcwallet=miner getbalance)
|
||||||
ok "Miner balance: ${MINER_BAL} BTC"
|
ok "Miner balance: ${MINER_BAL} BTC"
|
||||||
|
|
||||||
# ─── 7. Summary ───────────────────────────────────────────────────────────────
|
# ─── 6. Summary ───────────────────────────────────────────────────────────────
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${B}${C}══════════════════════════════════════════════════════════${RST}"
|
echo -e "${B}${C}══════════════════════════════════════════════════════════${RST}"
|
||||||
echo -e "${B} Setup complete!${RST}"
|
echo -e "${B} Setup complete!${RST}"
|
||||||
echo -e "${B}${C}══════════════════════════════════════════════════════════${RST}"
|
echo -e "${B}${C}══════════════════════════════════════════════════════════${RST}"
|
||||||
echo -e " Chain: ${G}regtest${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 -e " Wallets: ${G}${WALLETS[*]}${RST}"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " Next steps:"
|
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 " python3 detect.py --wallet alice \\"
|
||||||
echo -e " --known-risky-wallets risky \\"
|
echo -e " --known-risky-wallets risky \\"
|
||||||
echo -e " --known-exchange-wallets exchange"
|
echo -e " --known-exchange-wallets exchange"
|
||||||
echo -e " python3 verify.py --fresh # full automated proof"
|
|
||||||
echo ""
|
echo ""
|
||||||
|
|||||||
@@ -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
@@ -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 (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()
|
|
||||||
@@ -66,22 +66,7 @@
|
|||||||
<artifactId>rest-assured</artifactId>
|
<artifactId>rest-assured</artifactId>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<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>
|
|
||||||
<groupId>org.slf4j</groupId>
|
<groupId>org.slf4j</groupId>
|
||||||
<artifactId>slf4j-api</artifactId>
|
<artifactId>slf4j-api</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|||||||
-54
@@ -4,13 +4,9 @@ import jakarta.enterprise.context.ApplicationScoped;
|
|||||||
import jakarta.ws.rs.*;
|
import jakarta.ws.rs.*;
|
||||||
import jakarta.ws.rs.core.MediaType;
|
import jakarta.ws.rs.core.MediaType;
|
||||||
import jakarta.ws.rs.core.Response;
|
import jakarta.ws.rs.core.Response;
|
||||||
import org.backend.stealth.mocks.WalletMockData;
|
|
||||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
|
|
||||||
@ApplicationScoped
|
@ApplicationScoped
|
||||||
@Path("/api/wallet")
|
@Path("/api/wallet")
|
||||||
@@ -21,56 +17,6 @@ public class WalletResource {
|
|||||||
@ConfigProperty(name = "stealth.detect.script", defaultValue = "../../script/detect.py")
|
@ConfigProperty(name = "stealth.detect.script", defaultValue = "../../script/detect.py")
|
||||||
String detectScript;
|
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
|
@GET
|
||||||
@Path("/scan")
|
@Path("/scan")
|
||||||
public Response scan(@QueryParam("descriptor") String descriptor) {
|
public Response scan(@QueryParam("descriptor") String descriptor) {
|
||||||
|
|||||||
-22
@@ -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;
|
|
||||||
}
|
|
||||||
-31
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
-4
@@ -1,4 +0,0 @@
|
|||||||
package org.backend.stealth.domain.repository;
|
|
||||||
|
|
||||||
public class BitcoinRepository {
|
|
||||||
}
|
|
||||||
-75
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-31
@@ -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"));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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.',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
@@ -2,10 +2,11 @@ import { useState, useEffect } from 'react'
|
|||||||
import styles from './LoadingScreen.module.css'
|
import styles from './LoadingScreen.module.css'
|
||||||
|
|
||||||
const MESSAGES = [
|
const MESSAGES = [
|
||||||
'Parsing descriptor',
|
'Resolving descriptors',
|
||||||
'Fetching transactions',
|
'Deriving addresses',
|
||||||
'Scanning UTXO set',
|
'Importing & scanning blockchain',
|
||||||
'Running heuristics',
|
'Loading transaction history',
|
||||||
|
'Running vulnerability detectors',
|
||||||
]
|
]
|
||||||
|
|
||||||
export default function LoadingScreen({ descriptor }) {
|
export default function LoadingScreen({ descriptor }) {
|
||||||
|
|||||||
Reference in New Issue
Block a user