Files
stealth/backend/script/create_random_transactions.py
2026-02-27 02:23:47 -03:00

701 lines
27 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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()