# BSV Transaction Module
> Transaction construction, signing, serialization, and SPV verification for BSV blockchain
## Overview
This module provides complete Bitcoin transaction functionality:
- Transaction inputs, outputs, construction, and manipulation
- Binary/hex serialization and Extended Format (BRC-30)
- Transaction hash and TXID computation
- Signing with script templates
- Fee calculation with pluggable fee models
- MerklePath (BRC-74 BUMP) for merkle proofs
- BEEF format (BRC-62/95/96) for SPV proofs with recursive ancestry collection
- JSON serialization matching Go SDK format for cross-SDK compatibility
- Async Broadcaster trait with ARC, Teranode, and WhatsOnChain implementations
- Async ChainTracker trait with WhatsOnChain and BlockHeadersService implementations
Compatible with the TypeScript and Go SDKs through shared binary formats.
## Files
| `mod.rs` | Module root; re-exports all public types and sighash constants |
| `input.rs` | `TransactionInput` and `Utxo` for transaction inputs |
| `output.rs` | `TransactionOutput` for transaction outputs |
| `transaction.rs` | `Transaction` with parsing, serialization, signing, SPV verification, BEEF ancestry collection |
| `tx_json.rs` | JSON serialization/deserialization matching Go SDK's `MarshalJSON`/`UnmarshalJSON` |
| `merkle_path.rs` | `MerklePath` (BRC-74 BUMP) for merkle proofs |
| `beef.rs` | `Beef` (BRC-62/95/96) with validation, sorting, merging, and logging |
| `beef_tx.rs` | `BeefTx` wrapper, `TxDataFormat`, format constants |
| `fee_model.rs` | `FeeModel` trait, `FixedFee` |
| `fee_models/` | `SatoshisPerKilobyte`, `LivePolicy`, `LivePolicyConfig`, default constants |
| `broadcaster.rs` | `Broadcaster` trait, response/failure types, helper functions |
| `broadcasters/` | `ArcBroadcaster`, `TeranodeBroadcaster`, `WhatsOnChainBroadcaster` |
| `chain_tracker.rs` | `ChainTracker` trait, `MockChainTracker`, `AlwaysValidChainTracker` |
| `chain_trackers/` | `WhatsOnChainTracker`, `BlockHeadersServiceTracker`, `BlockHeadersServiceConfig` |
## Core Types
### TransactionInput
```rust
pub struct TransactionInput {
pub source_transaction: Option<Box<Transaction>>, // Full source tx (preferred for BEEF)
pub source_txid: Option<String>, // TXID if source tx unavailable
pub source_output_index: u32,
pub unlocking_script: Option<UnlockingScript>, // Populated after signing
pub unlocking_script_template: Option<Box<ScriptTemplateUnlock>>,
pub sequence: u32, // Default: 0xFFFFFFFF
}
impl TransactionInput {
pub fn new(source_txid: String, source_output_index: u32) -> Self
pub fn with_source_transaction(tx: Transaction, output_index: u32) -> Self
pub fn get_source_txid(&self) -> Result<String> // Falls back to source_transaction
pub fn get_source_txid_bytes(&self) -> Result<[u8; 32]> // Internal byte order
pub fn source_satoshis(&self) -> Option<u64>
pub fn source_locking_script(&self) -> Option<&LockingScript>
pub fn set_unlocking_script_template(&mut self, template: ScriptTemplateUnlock)
pub fn set_unlocking_script(&mut self, script: UnlockingScript)
pub fn has_source_transaction(&self) -> bool
}
```
Note: `unlocking_script_template` is not cloned when cloning `TransactionInput` (templates contain closures).
### Utxo
```rust
pub struct Utxo {
pub txid: String, // TXID of the source transaction
pub vout: u32, // Output index in source transaction
pub satoshis: u64, // Amount in the output
pub locking_script: LockingScript, // Spending conditions
}
```
Used with `Transaction::add_inputs_from_utxos()` for convenient batch input creation without needing full source transactions.
### TransactionOutput
```rust
pub struct TransactionOutput {
pub satoshis: Option<u64>, // None for change outputs before fee calc
pub locking_script: LockingScript,
pub change: bool, // If true, amount computed during fee()
}
impl TransactionOutput {
pub fn new(satoshis: u64, locking_script: LockingScript) -> Self
pub fn new_change(locking_script: LockingScript) -> Self
pub fn get_satoshis(&self) -> u64 // Returns 0 if None
pub fn has_satoshis(&self) -> bool
pub fn serialized_size(&self) -> usize // 8 + varint + script_len
}
```
### Transaction
```rust
pub struct Transaction {
pub version: u32, // Default: 1
pub inputs: Vec<TransactionInput>,
pub outputs: Vec<TransactionOutput>,
pub lock_time: u32, // Default: 0
pub metadata: HashMap<String, Value>, // Not serialized
pub merkle_path: Option<MerklePath>, // SPV proof (stops BEEF ancestry walk)
// Internal caches: cached_hash, raw_bytes_cache, hex_cache
}
impl Transaction {
// Constructors
pub fn new() -> Self
pub fn with_params(version, inputs, outputs, lock_time) -> Self
// Parsing
pub fn from_binary(bin: &[u8]) -> Result<Self>
pub fn from_hex(hex: &str) -> Result<Self>
pub fn from_ef(ef: &[u8]) -> Result<Self> // Extended Format (BRC-30)
pub fn from_hex_ef(hex: &str) -> Result<Self>
pub fn from_beef(beef: &[u8], txid: Option<&str>) -> Result<Self>
pub fn from_atomic_beef(beef: &[u8]) -> Result<Self>
pub fn from_json(json: &str) -> Result<Self> // Go SDK-compatible JSON
pub fn parse_script_offsets(bin: &[u8]) -> Result<ScriptOffsets>
// Serialization
pub fn to_binary(&self) -> Vec<u8>
pub fn to_hex(&self) -> String
pub fn to_ef(&self) -> Result<Vec<u8>>
pub fn to_hex_ef(&self) -> Result<String>
pub fn to_beef(&self, allow_partial: bool) -> Result<Vec<u8>> // BEEF V2
pub fn to_beef_v1(&self, allow_partial: bool) -> Result<Vec<u8>> // BEEF V1 for ARC
pub fn to_atomic_beef(&self, allow_partial: bool) -> Result<Vec<u8>>
pub fn to_json(&self) -> Result<String> // Go SDK-compatible JSON
pub fn to_json_pretty(&self) -> Result<String> // Pretty-printed JSON
// Hashing
pub fn hash(&self) -> [u8; 32] // Double SHA-256 (internal byte order)
pub fn id(&self) -> String // TXID (reversed hash as hex)
pub fn hash_hex(&self) -> String
// Building
pub fn add_input(&mut self, input: TransactionInput) -> Result<()>
pub fn add_output(&mut self, output: TransactionOutput) -> Result<()>
pub fn add_p2pkh_output(&mut self, address: &str, satoshis: Option<u64>) -> Result<()>
pub fn add_op_return_output(&mut self, data: &[u8]) -> Result<()> // OP_FALSE OP_RETURN <data>
pub fn add_op_return_parts_output(&mut self, parts: &[&[u8]]) -> Result<()> // Multi-part OP_RETURN
pub fn add_hash_puzzle_output(&mut self, secret: &str, public_key_hash: &str, satoshis: u64) -> Result<()>
pub fn add_input_from_tx(&mut self, source_tx: Transaction, vout: u32, template: ScriptTemplateUnlock) -> Result<()>
pub fn add_input_from(&mut self, prev_txid: &str, vout: u32, prev_locking_script: &LockingScript, satoshis: u64, template: ScriptTemplateUnlock) -> Result<()>
pub fn add_inputs_from_utxos(&mut self, utxos: &[Utxo]) -> Result<()> // No template set
pub fn update_metadata(&mut self, key: &str, value: Value)
pub fn input_count(&self) -> usize
pub fn output_count(&self) -> usize
// Inspection
pub fn is_coinbase(&self) -> bool // True if single input with null TXID
pub fn has_data_outputs(&self) -> bool // True if any output has OP_RETURN
pub fn total_input_satoshis(&self) -> Result<u64> // Sum of all input satoshis
pub fn total_output_satoshis(&self) -> u64 // Sum of all output satoshis
// Signing & Fees
pub async fn sign(&mut self) -> Result<()>
pub async fn fee(&mut self, fee_sats: Option<u64>, change_distribution: ChangeDistribution) -> Result<()>
pub fn get_fee(&self) -> Result<u64>
pub fn estimate_size(&self) -> usize
// SPV Verification
pub async fn verify(&self, chain_tracker: &dyn ChainTracker, fee_model: Option<&dyn FeeModel>) -> Result<bool>
}
pub enum ChangeDistribution { Equal, Random } // Random uses Benford's law
pub struct ScriptOffsets { pub inputs: Vec<ScriptOffset>, pub outputs: Vec<ScriptOffset> }
pub struct ScriptOffset { pub index: usize, pub offset: usize, pub length: usize }
```
## JSON Serialization (tx_json.rs)
Cross-SDK compatible JSON format matching Go SDK's `MarshalJSON`/`UnmarshalJSON`:
```rust
// Serialize to JSON
let json = tx.to_json()?; // Compact JSON
let json = tx.to_json_pretty()?; // Pretty-printed
// Deserialize from JSON (supports hex-only or field-based)
let tx = Transaction::from_json(&json)?;
```
JSON format:
```json
{
"txid": "hex...",
"hex": "full_serialized_tx_hex",
"inputs": [{ "unlockingScript": "hex", "txid": "hex", "vout": 0, "sequence": 4294967295 }],
"outputs": [{ "satoshis": 1000, "lockingScript": "hex" }],
"version": 1,
"lockTime": 0
}
```
Deserialization priority: if `hex` field is present and non-empty, reconstructs from hex (matching Go SDK). Otherwise builds from individual fields (`inputs`, `outputs`, `version`, `lockTime`).
## Fee Models
```rust
pub trait FeeModel: Send + Sync {
fn compute_fee(&self, tx: &Transaction) -> Result<u64>;
}
pub struct FixedFee(u64); // Always returns same fee
pub struct SatoshisPerKilobyte { pub value: u64 } // Fee based on tx size (default: 100 sat/KB)
pub struct LivePolicy { /* ... */ } // Fetches rate from ARC policy endpoint
// Exported constants
pub const DEFAULT_POLICY_URL: &str; // "https://arc.gorillapool.io/v1/policy"
pub const DEFAULT_FALLBACK_RATE: u64; // Fallback sat/KB rate
pub const DEFAULT_CACHE_TTL_SECS: u64; // Cache TTL for live policy
```
`LivePolicy` has `new()`, `with_url()`, `with_config(LivePolicyConfig)`, `refresh()` (async fetch), `cached_rate()`, `effective_rate()`, `set_rate()`. Config: `LivePolicyConfig { policy_url, api_key, cache_ttl, fallback_rate, timeout_ms }`. Default policy URL: `https://arc.gorillapool.io/v1/policy`.
## Broadcasting
```rust
#[async_trait(?Send)]
pub trait Broadcaster: Send + Sync {
async fn broadcast(&self, tx: &Transaction) -> BroadcastResult;
async fn broadcast_many(&self, txs: Vec<Transaction>) -> Vec<BroadcastResult>; // Default: sequential
}
pub type BroadcastResult = Result<BroadcastResponse, BroadcastFailure>;
pub enum BroadcastStatus { Success, Error }
// BroadcastResponse: status, txid, message, competing_txs. Constructors: success(), success_with_competing()
// BroadcastFailure: status, code, txid, description, more. Constructors: new(), with_txid(), with_details()
// Helpers: is_broadcast_success(), is_broadcast_failure()
```
### Broadcaster Implementations (require `http` feature)
| `ArcBroadcaster` | BEEF V1 (JSON) | `default()` = gorillapool.io | `ArcConfig` |
| `TeranodeBroadcaster` | Extended Format (EF binary) | No default URL | `TeranodeConfig` |
| `WhatsOnChainBroadcaster` | Raw tx hex | `mainnet()`, `testnet()`, `stn()` | `WocBroadcastConfig`, `WocBroadcastNetwork` |
ARC/Teranode have `new(url, api_key)` and `with_config(config)` constructors. WoC has `new(network, api_key)`, `mainnet()`, `testnet()`, `stn()`, `with_config(config)`, `with_base_url(url, network, api_key)`. `WocBroadcastNetwork` enum selects `Mainnet`, `Testnet`, or `STN`.
## Chain Tracking
```rust
#[async_trait]
pub trait ChainTracker: Send + Sync {
async fn is_valid_root_for_height(&self, root: &str, height: u32) -> Result<bool, ChainTrackerError>;
async fn current_height(&self) -> Result<u32, ChainTrackerError>;
}
pub enum ChainTrackerError { NetworkError(String), InvalidResponse(String), BlockNotFound(u32), Other(String) }
// Test mocks
pub struct MockChainTracker { pub height: u32, pub roots: HashMap<u32, String> }
pub struct AlwaysValidChainTracker { pub height: u32 }
// MockChainTracker: new(height), add_root(height, root), always_valid(height) -> AlwaysValidChainTracker
// AlwaysValidChainTracker: new(height) - always returns true for any root
```
### Chain Tracker Implementations (require `http` feature)
| `WhatsOnChainTracker` | `mainnet()`, `testnet()` | `WocNetwork` enum |
| `BlockHeadersServiceTracker` | Any (via URL) | `BlockHeadersServiceConfig`, `DEFAULT_HEADERS_URL` |
## MerklePath (BUMP - BRC-74)
```rust
pub struct MerklePath {
pub block_height: u32,
pub path: Vec<Vec<MerklePathLeaf>>, // Tree structure, level 0 = txids
}
pub struct MerklePathLeaf {
pub offset: u64, pub hash: Option<String>, pub txid: bool, pub duplicate: bool,
}
// Leaf constructors: new(offset, hash), new_txid(offset, hash), new_duplicate(offset)
impl MerklePath {
pub fn new(block_height: u32, path: Vec<Vec<MerklePathLeaf>>) -> Result<Self> // Full validation
pub fn new_unchecked(block_height, path) -> Result<Self> // Skips offset validation
pub fn from_hex/from_binary/from_reader(...) // Parsing
pub fn from_coinbase_txid(txid: &str, height: u32) -> Self // Single-tx block
pub fn to_hex/to_binary/to_writer(...) // Serialization
pub fn compute_root(&self, txid: Option<&str>) -> Result<String>
pub fn contains(&self, txid: &str) -> bool
pub fn txids(&self) -> Vec<String>
pub fn combine(&mut self, other: &MerklePath) -> Result<()> // Same height/root required
pub fn trim(&mut self) // Remove unnecessary internal nodes
}
```
Validation in `new()`: level 0 non-empty, no duplicate offsets, legal higher-level offsets, all txids compute same root.
## BEEF Format (BRC-62/95/96)
```rust
pub struct Beef {
pub bumps: Vec<MerklePath>, // Merkle proofs
pub txs: Vec<BeefTx>, // Transactions (sorted by dependency)
pub version: u32, // BEEF_V1 or BEEF_V2
pub atomic_txid: Option<String>, // Target txid for Atomic BEEF
// Internal: txid_index (HashMap for fast lookup), needs_sort flag
}
impl Beef {
// Construction: new() [V2], with_version(), from_hex(), from_binary()
// Serialization: to_hex(), to_binary() [auto-sorts], to_binary_atomic(txid)
// Validation: is_valid(allow_txid_only), verify_valid() -> BeefValidationResult, sort_txs() -> SortResult
// Lookup: find_txid(), find_txid_mut(), find_bump(), find_transaction_for_signing(), find_atomic_transaction(), is_atomic()
// Merging: merge_bump() [combines same height/root], merge_transaction(), merge_raw_tx(), merge_txid_only(), make_txid_only(), merge_beef()
// Utility: clone_shallow(), to_log_string()
}
pub struct BeefValidationResult { pub valid: bool, pub roots: HashMap<u32, String> }
pub struct SortResult { pub missing_inputs, not_valid, valid, with_missing_inputs, txid_only: Vec<String> }
```
**BeefTx**: Wraps a single tx in BEEF. Constructors: `from_tx(tx, bump_index)`, `from_raw_tx(raw_tx, bump_index)`, `from_txid(txid)`. Accessors: `txid()`, `tx()`, `tx_mut()`, `raw_tx()`, `raw_tx_or_compute()`. State: `has_proof()`, `is_txid_only()`, `bump_index()`, `set_bump_index()`. Public fields: `input_txids`, `is_valid`.
**Constants**: `BEEF_V1 = 0xEFBE0001` (BRC-62), `BEEF_V2 = 0xEFBE0002` (BRC-96 with txid-only), `ATOMIC_BEEF = 0x01010101` (BRC-95). `TxDataFormat`: `RawTx=0`, `RawTxAndBumpIndex=1`, `TxidOnly=2`.
## Re-exported Sighash Constants
The module re-exports sighash constants from `primitives::bsv::sighash`:
- `SIGHASH_ALL`, `SIGHASH_NONE`, `SIGHASH_SINGLE`
- `SIGHASH_ANYONECANPAY`, `SIGHASH_FORKID`
## Usage Examples
```rust
// Building and Signing
let mut tx = Transaction::new();
tx.add_input(TransactionInput::with_source_transaction(source_tx, 0))?;
tx.add_output(TransactionOutput::new(100_000, locking_script))?;
tx.add_p2pkh_output("1MyChange...", None)?; // Change output
tx.fee(None, ChangeDistribution::Equal).await?;
tx.sign().await?;
// Convenience input methods (match Go SDK)
tx.add_input_from_tx(source_tx, 0, p2pkh_unlock)?; // Full source tx + template
tx.add_input_from(txid, 0, &locking_script, 50_000, p2pkh_unlock)?; // By TXID + script
tx.add_inputs_from_utxos(&utxos)?; // Batch add from UTXOs (templates set separately)
// Inspecting
assert!(!tx.is_coinbase());
assert!(tx.has_data_outputs()); // If any OP_RETURN outputs exist
// JSON serialization (cross-SDK compatible with Go SDK)
let json = tx.to_json()?;
let tx2 = Transaction::from_json(&json)?;
assert_eq!(tx.to_hex(), tx2.to_hex());
// Broadcasting (requires `http` feature)
let result = ArcBroadcaster::default().broadcast(&tx).await; // BEEF V1
let result = WhatsOnChainBroadcaster::mainnet().broadcast(&tx).await; // Raw hex
let result = TeranodeBroadcaster::new(url, None).broadcast(&tx).await; // EF binary
// Serializing to BEEF (recursively collects ancestors)
let beef_v1 = tx.to_beef_v1(false)?; // For ARC
let beef_v2 = tx.to_beef(false)?; // V2 format
let atomic = tx.to_atomic_beef(false)?;
// SPV Verification (Transaction-level, recursive)
let tracker = WhatsOnChainTracker::mainnet();
let is_valid = tx.verify(&tracker, None).await?; // Recursive SPV verify
let is_valid = tx.verify(&tracker, Some(&fee_model)).await?; // With fee validation
// SPV Verification (BEEF-level, manual root checking)
let validation = beef.verify_valid(false);
for (height, root) in validation.roots {
tracker.is_valid_root_for_height(&root, height).await?;
}
// Fee Models
let fee = SatoshisPerKilobyte::new(100).compute_fee(&tx)?; // Fixed rate
let live = LivePolicy::default(); live.refresh().await?; // Live rate
```
## Implementation Notes
- **Caching**: Transaction hash/serialization cached in RefCell; invalidates on modification via `add_input()`, `add_output()`, `sign()`, `fee()`
- **Inputs**: Must have `source_txid` or `source_transaction`; fee calc/signing need full source tx with satoshis/locking_script
- **Convenience inputs**: `add_input_from_tx` sets template; `add_input_from` builds a minimal source tx; `add_inputs_from_utxos` does NOT set templates (caller must set them separately)
- **Change**: Created via `new_change()` or `add_p2pkh_output(_, None)`; computed in `fee()` using Benford's law for Random distribution
- **TXID**: `hash()` = internal byte order; `id()` = reversed hex (display format)
- **Async traits**: `Broadcaster` uses `?Send` (Transaction has RefCell); `ChainTracker` is standard async
- **HTTP feature**: `ArcBroadcaster`, `TeranodeBroadcaster`, `WhatsOnChainBroadcaster`, `WhatsOnChainTracker`, `BlockHeadersServiceTracker`, and `LivePolicy` require the `http` feature flag
- **BEEF ancestry**: `to_beef()` and `to_beef_v1()` recursively walk `source_transaction` chain via `collect_ancestors()`, stops at txs with `merkle_path`
- **BEEF V1 vs V2**: Use `to_beef_v1()` for ARC compatibility (BRC-62), `to_beef()` for V2 with TXID-only support (BRC-96)
- **BEEF indexing**: Beef maintains an internal `txid_index` HashMap for O(1) transaction lookup by txid
- **MerklePath dedup**: BEEF ancestry collection deduplicates proofs by `"height:root"` key; combines proofs at same height/root
- **Dependency order**: BEEF transactions sorted oldest-first; inputs processed in reverse order during collection (like TS SDK)
- **JSON format**: `to_json()`/`from_json()` match Go SDK's `MarshalJSON`/`UnmarshalJSON`; deserialization prefers `hex` field when present
- **Default impls**: `Transaction`, `TransactionInput`, `TransactionOutput`, `Beef`, `MockChainTracker`, `WhatsOnChainBroadcaster`, `WhatsOnChainTracker` implement `Default`
- **SPV verify**: `verify()` performs queue-based recursive verification: checks merkle paths against chain tracker, validates fees (optional), runs script interpreter on each input, enqueues unverified source transactions. Matches Go SDK's `spv.Verify()` and TS SDK's `Transaction.verify()`
- **Equality**: `Transaction` and `TransactionOutput` implement `PartialEq`/`Eq` based on binary serialization
## BEEF Ancestry Collection Algorithm
The `collect_ancestors()` method implements the same algorithm as TypeScript/Go SDKs:
1. **Cycle detection**: Skip transactions already seen (by txid)
2. **Proven transactions**: If tx has `merkle_path`, add proof (deduplicated by height:root) and stop recursion
3. **Unproven transactions**: Recursively process each input's `source_transaction` in reverse order
4. **Dependency order**: After processing all ancestors, add current transaction
5. **Partial support**: `allow_partial=true` skips inputs with missing source transactions
## Error Types
| `TransactionError` | Missing source, satoshis, uncomputed change, EF marker issues, BEEF parsing, JSON serialization, SPV verification failures (invalid merkle path, fee too low, script validation) |
| `FeeModelError` | Input missing unlocking script or template |
| `BeefError` | Invalid version (not V1/V2), missing atomic txid, txid not in BEEF |
| `MerklePathError` | Empty path, duplicate offset, invalid offset at height, mismatched roots |
| `ChainTrackerError` | `NetworkError`, `InvalidResponse`, `BlockNotFound(height)`, `Other` |
## Related Documentation
- `../script/CLAUDE.md` - LockingScript, UnlockingScript, templates
- `../primitives/CLAUDE.md` - Reader, Writer, sha256d, from_hex, to_hex
- `fee_models/CLAUDE.md` - SatoshisPerKilobyte, LivePolicy
- `broadcasters/CLAUDE.md` - ArcBroadcaster, TeranodeBroadcaster, WhatsOnChainBroadcaster
- `chain_trackers/CLAUDE.md` - WhatsOnChainTracker, BlockHeadersServiceTracker