A Rust crate that periodically sends Bitcoin transactions to a node using bdk_wallet.
Both the sender wallet and receiver address are derived from a single BIP39 seed phrase using BIP84 (native SegWit P2WPKH).
This is meant to help development on Stratum V2 Reference Implementation.
- Two modes: unit (1 tx per interval) and batch (N txs per interval)
- Pruned-node compatible: uses
scantxoutsetRPC instead of block-by-block sync - Continuous batch: change outputs from unconfirmed txs are reused across ticks
- Double-spend protection: tracks spent outpoints within batches and across mempool
- Dual interface: runs as a standalone binary (
config.toml) or as a Rust library
- Rust 1.85+
- Bitcoin Core node with RPC enabled (pruned or archival)
- The node must have
-rpcuser/-rpcpassword(or cookie auth)
- Create
config.toml(see config.toml for an example):
interval_secs = 30
mode = "batch"
batch_size = 10
seed_phrase = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
sender_account_index = 0
receiver_account_index = 1
# change_account_index = 0 # optional — defaults to sender_account_index when omitted
amount_sat = 1000
fee_rate_sat_per_vb = 5
network = "regtest"
rpc_address = "127.0.0.1"
rpc_port = 18443
rpc_username = "user"
rpc_password = "password"- Run:
cargo run -- config.tomluse sv2_tx_generator::{Config, Mode, NetworkConfig, TxGenerator};
let config = Config {
interval_secs: 30,
mode: Mode::Batch,
batch_size: Some(10),
seed_phrase: "abandon abandon ...".into(),
sender_account_index: 0,
receiver_account_index: 1,
change_account_index: None,
amount_sat: 1000,
fee_rate_sat_per_vb: 5,
network: NetworkConfig::Regtest,
rpc_address: "127.0.0.1".into(),
rpc_port: 18443,
rpc_username: "user".into(),
rpc_password: "pass".into(),
};
let mut generator = TxGenerator::new(config)?;
generator.run().await?;┌──────────────┐ ┌───────────────┐ ┌───────────────┐
│ config.toml │────▶│ TxGenerator │────▶│ Bitcoin Core │
│ (binary) │ │ ::new() │ │ RPC │
└──────────────┘ └──────┬────────┘ └───────┬───────┘
│ │
┌──────▼───────┐ ┌───────▼───────┐
│ run() loop │ │ scantxoutset │
│ │ │ getrawtx │
│ per tick: │ │ getblockhash │
│ 1. scan │ │ getrawmempool │
│ 2. wallet │ │ sendrawtx │
│ 3. mempool │ └───────────────┘
│ 4. build │
│ 5. broadcast│
└──────────────┘
scantxoutset— scans the UTXO set for outputs matching wallet descriptors (works on pruned nodes)fetch_mempool_data— reads all mempool txs to build spent-outpoint exclusion setcreate_wallet_from_utxos— creates a fresh in-memory BDK wallet, fetches full parent txs, applies them with block anchorsinject_mempool_txs— adds unconfirmed mempool txs to the wallet so change outputs become spendablebuild_tx— BDK coin selection + PSBT signing (P2WPKH)sendrawtransaction— broadcasts all txs in the batch
| Decision | Rationale |
|---|---|
| Fresh wallet per tick | Avoids stale UTXO accumulation; each scan is the source of truth |
scantxoutset for discovery |
Works on pruned nodes — reads chainstate, not blocks |
| Full parent tx fetch | BDK requires full txs for canonicalization and spending |
| Mempool tx injection | Enables continuous batch mode across ticks without block confirmation |
unspendable outpoints |
Prevents double-spends within a batch and against mempool txs |
| In-memory SQLite | No disk files; wallet state is ephemeral |
Explorer note: block explorers infer a "sender" from input prevout addresses. That address may differ from the displayed
Sender (display /0/0)printed on startup, because the generator uses coin selection from all wallet UTXOs — not only/0/0.
| Field | Type | Description |
|---|---|---|
interval_secs |
u64 | Seconds between transaction batches |
mode |
string | "unit" or "batch" |
batch_size |
u32 | Txs per tick (only for batch mode) |
seed_phrase |
string | BIP39 mnemonic (12/24 words) |
sender_account_index |
u32 | BIP84 account for sending wallet |
receiver_account_index |
u32 | BIP84 account for receiving address |
change_account_index |
u32 | (Optional) BIP84 account for fixed change address; defaults to sender |
amount_sat |
u64 | Amount per tx in satoshis |
fee_rate_sat_per_vb |
u64 | Fee rate in sats/vbyte |
network |
string | "bitcoin", "testnet", "regtest", or "signet" |
rpc_address |
string | Bitcoin Core RPC host |
rpc_port |
u16 | Bitcoin Core RPC port |
rpc_username |
string | RPC auth username |
rpc_password |
string | RPC auth password |
Both sender and receiver use BIP84 (native SegWit, wpkh descriptors):
Sender wallet descriptors:
External: m/84'/{coin_type}'/{sender_idx}'/0/*
Receiver address:
m/84'/{coin_type}'/{receiver_idx}'/0/0
Displayed sender address:
m/84'/{coin_type}'/{sender_idx}'/0/0
Change address (drain_to target):
m/84'/{coin_type}'/{change_idx}'/0/0
Wallet internal descriptor:
When change_idx == sender_idx:
Internal: m/84'/{coin_type}'/{sender_idx}'/1/*
When change_idx != sender_idx:
Internal: m/84'/{coin_type}'/{change_idx}'/0/*
Coin types:
- Mainnet:
0' - Testnet / Regtest / Signet:
1'
MIT OR Apache-2.0