SimpleBTC - Bitcoin-Based Banking System Demo
Welcome to the SimpleBTC project documentation!
Project Introduction
SimpleBTC is a Bitcoin banking system demo implemented in Rust, providing a complete implementation of Bitcoin’s core principles and advanced features. This project is not only a learning tool but also a fully functional blockchain system demo.
Core Features
🔐 Complete UTXO Model
- Unspent Transaction Output (UTXO) management
- Double-spend prevention mechanism
- Balance calculation
- UTXO selection algorithm
⛓️ Blockchain Core Functionality
- Proof of Work (PoW)
- Blockchain validation
- Merkle tree implementation
- Chain hash structure
💼 Wallet System
- Key pair generation (simplified)
- Address generation
- Digital signatures
- Transaction creation
📊 Advanced Transaction Features
- Replace-By-Fee (RBF): Replace unconfirmed transactions to speed up confirmation
- Time Lock (TimeLock): Term deposits, inheritance planning
- Multi-Signature (MultiSig): 2-of-3 enterprise wallets, escrow services
- Transaction Priority: Priority sorting based on fees
🌳 Merkle Tree and SPV
- Efficient transaction verification
- Lightweight client support
- Merkle proof generation and verification
🔧 Engineering Features
- REST API server (Axum framework)
- Persistent storage (JSON)
- Transaction indexer
- Electron visualization interface
Why Choose SimpleBTC?
-
Educational Value
- Deep understanding of Bitcoin principles
- Learn Rust blockchain development
- Master cryptographic fundamentals
-
Complete Implementation
- Conforms to ACID transaction properties
- Implements Bitcoin core protocol
- Includes advanced BIP features
-
Practical Examples
- Corporate fund management
- Escrow transaction services
- Term deposit systems
-
Easy to Extend
- Modular design
- Clear code structure
- Detailed comments
Quick Start
# Clone the project
git clone https://github.com/GeoffreyWang1117/SimpleBTC.git
cd SimpleBTC
# Build the project
cargo build --release
# Run the demo
cargo run --bin btc-demo
# Run the REST API server
cargo run --bin btc-server
# Run examples
cargo run --example enterprise_multisig
cargo run --example escrow_service
cargo run --example timelock_savings
System Architecture
SimpleBTC/
├── src/
│ ├── transaction.rs # Transaction module (UTXO model)
│ ├── block.rs # Block structure
│ ├── blockchain.rs # Blockchain core logic
│ ├── wallet.rs # Wallet management
│ ├── utxo.rs # UTXO set management
│ ├── merkle.rs # Merkle tree implementation
│ ├── multisig.rs # Multi-signature
│ ├── advanced_tx.rs # RBF, time locks, priority
│ ├── persistence.rs # Persistent storage
│ └── indexer.rs # Transaction indexer
├── examples/ # Practical examples
├── frontend/ # Electron GUI
└── docs/ # This documentation
Tech Stack
- Language: Rust (Edition 2021)
- Core Libraries:
- sha2 - SHA256 hashing
- serde - Serialization
- rand - Random number generation
- Web Framework: Axum (async REST API)
- Frontend: Electron + JavaScript
- Documentation: mdBook
Learning Path
Beginner: Understanding Basic Concepts
- Basic Concepts - UTXO, blocks, hashing
- Wallet Management - Creating wallets, sending transactions
- Transaction Processing - Transaction structure, validation
Intermediate: Mastering Core Mechanisms
- Blockchain Operations - Mining, validation
- UTXO Management - UTXO selection, double-spend protection
- Merkle Tree - SPV verification
Advanced: Implementing Complex Applications
- Multi-Signature - Enterprise wallets
- Time Lock - Term deposits
- RBF Mechanism - Transaction acceleration
Differences from Bitcoin
SimpleBTC is a simplified educational implementation; the main differences from real Bitcoin are:
| Feature | SimpleBTC | Real Bitcoin |
|---|---|---|
| Cryptography | Simplified SHA256 | secp256k1 elliptic curve |
| Signatures | Simplified validation | ECDSA signatures |
| Scripts | Simplified scripts | Full Script language |
| P2P Network | No network layer | Full P2P protocol |
| Storage | JSON files | LevelDB database |
| Difficulty adjustment | Fixed difficulty | Dynamic difficulty adjustment |
Project Status
- ✅ UTXO model
- ✅ Proof of Work
- ✅ Merkle tree
- ✅ Multi-signature
- ✅ RBF mechanism
- ✅ Time lock
- ✅ REST API
- ✅ GUI interface
- ✅ Full documentation
Contributing
Contributions of code, documentation, or bug reports are welcome!
See the Contributing Guide for details.
License
This project is licensed under the MIT License.
Let’s start exploring the world of Bitcoin! 🚀
Installation and Configuration
This section will guide you through the installation and basic configuration of SimpleBTC.
System Requirements
Minimum Requirements
- Operating System: Linux, macOS, or Windows (WSL2)
- Rust Version: 1.70.0 or higher
- Memory: At least 2GB RAM
- Storage: At least 500MB free space
Recommended Configuration
- Operating System: Linux/macOS
- Rust Version: Latest stable release
- Memory: 4GB+ RAM
- Storage: 1GB+ free space
- CPU: Multi-core processor (better mining performance)
Installing Rust
If you have not installed Rust yet, visit rust-lang.org or use the following command:
# Linux/macOS
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Verify installation
rustc --version
cargo --version
Cloning the Project
# Clone via HTTPS
git clone https://github.com/GeoffreyWang1117/SimpleBTC.git
# Or clone via SSH
git clone git@github.com:GeoffreyWang1117/SimpleBTC.git
# Enter the project directory
cd SimpleBTC
Building the Project
Development Build
# Fast build (unoptimized, compiles quickly)
cargo build
# Run tests
cargo test
# Run the Demo
cargo run --bin btc-demo
Production Build
# Optimized build (best performance, slower to compile)
cargo build --release
# Run the optimized binary
./target/release/btc-demo
./target/release/btc-server
Running the Examples
SimpleBTC provides three hands-on examples:
# 1. Enterprise multi-signature wallet (2-of-3)
cargo run --example enterprise_multisig
# 2. Escrow service (buyer/seller/arbitrator)
cargo run --example escrow_service
# 3. Time-deposit savings (time lock)
cargo run --example timelock_savings
Starting the REST API Server
# Development mode
cargo run --bin btc-server
# Production mode
cargo run --release --bin btc-server
The server will start at http://localhost:3000
API Endpoints
GET /api/blockchain/info- Get blockchain informationPOST /api/wallet/create- Create a new walletPOST /api/transaction/create- Create a transactionPOST /api/mine- Mine a blockGET /api/balance/:address- Query balance
Starting the Electron GUI
# Install Node.js dependencies
cd frontend
npm install
# Launch the Electron application
npm start
The GUI provides a visual interface including:
- Blockchain explorer
- Wallet management
- Transaction creation
- Real-time mining
- One-click Demo mode
Project Structure
SimpleBTC/
├── src/ # Source code
│ ├── lib.rs # Library entry point
│ ├── main.rs # CLI Demo
│ ├── transaction.rs # Transaction module
│ ├── block.rs # Block module
│ ├── blockchain.rs # Blockchain logic
│ ├── wallet.rs # Wallet management
│ ├── utxo.rs # UTXO management
│ ├── merkle.rs # Merkle tree
│ ├── multisig.rs # Multi-signature
│ ├── advanced_tx.rs # Advanced transaction features
│ ├── persistence.rs # Persistence
│ └── indexer.rs # Indexer
├── examples/ # Example programs
│ ├── enterprise_multisig.rs
│ ├── escrow_service.rs
│ └── timelock_savings.rs
├── frontend/ # Electron GUI
│ ├── main.js
│ ├── app.js
│ └── index.html
├── docs/ # Documentation
├── Cargo.toml # Rust project configuration
└── README.md # Project description
Configuration Options
Mining Difficulty
Modify in src/blockchain.rs:
#![allow(unused)]
fn main() {
pub fn new() -> Blockchain {
let mut blockchain = Blockchain {
difficulty: 3, // Change here: 3-5 is suitable for demos, 6+ is more secure but slower
// ...
}
}
}
Block Reward
#![allow(unused)]
fn main() {
pub fn new() -> Blockchain {
let mut blockchain = Blockchain {
mining_reward: 50, // Modify the mining reward (satoshi)
// ...
}
}
}
API Server Port
Modify in src/bin/server.rs:
#![allow(unused)]
fn main() {
let listener = TcpListener::bind("0.0.0.0:3000") // Change port here
.await
.unwrap();
}
Frequently Asked Questions
Build Errors
Problem: error: failed to fetch
# Solution: Update the Cargo index
cargo update
Problem: error: linker 'cc' not found
# Ubuntu/Debian
sudo apt-get install build-essential
# macOS (install Xcode command-line tools)
xcode-select --install
Runtime Errors
Problem: Address already in use (os error 98)
# Port 3000 is occupied; kill the process using it or change the port
lsof -ti:3000 | xargs kill
Problem: Mining is too slow
# Lower the difficulty
# Set difficulty: 2 in blockchain.rs
Next Steps
- 📖 Read Quick Start for basic usage
- 🎓 Study Core Concepts to understand the principles
- 🔨 Browse Practical Examples
Getting Help
- GitHub Issues: https://github.com/GeoffreyWang1117/SimpleBTC/issues
- Project Documentation: this site
- Rust Community: https://users.rust-lang.org/
Quick Start
This is a 5-minute tutorial that walks you through the core features of SimpleBTC.
Your First Blockchain Program
Create a new Rust project and add the SimpleBTC dependency:
use bitcoin_simulation::{
blockchain::Blockchain,
wallet::Wallet,
};
fn main() {
println!("🚀 SimpleBTC Quick Start\n");
// 1. Create a blockchain
let mut blockchain = Blockchain::new();
println!("✓ Blockchain initialized");
// 2. Create wallets
let alice = Wallet::new();
let bob = Wallet::new();
println!("✓ Two wallets created");
println!(" Alice: {}", alice.address);
println!(" Bob: {}\n", bob.address);
// 3. Alice receives initial funds (from the genesis block)
let tx1 = blockchain.create_transaction(
&Wallet::from_address("genesis_address".to_string()),
alice.address.clone(),
10000, // 10,000 satoshi
0, // No fee (genesis transaction)
).unwrap();
blockchain.add_transaction(tx1).unwrap();
blockchain.mine_pending_transactions(alice.address.clone()).unwrap();
println!("💰 Alice's balance: {} satoshi", blockchain.get_balance(&alice.address));
// 4. Alice sends to Bob
let tx2 = blockchain.create_transaction(
&alice,
bob.address.clone(),
3000, // Transfer 3000
10, // Fee 10
).unwrap();
blockchain.add_transaction(tx2).unwrap();
blockchain.mine_pending_transactions(bob.address.clone()).unwrap();
// 5. View final balances
println!("\n💼 Final balances:");
println!(" Alice: {} satoshi", blockchain.get_balance(&alice.address));
println!(" Bob: {} satoshi\n", blockchain.get_balance(&bob.address));
// 6. Validate the blockchain
if blockchain.is_valid() {
println!("✅ Blockchain validation passed!");
}
// 7. Print blockchain information
blockchain.print_chain();
}
Expected Output
🚀 SimpleBTC Quick Start
✓ Blockchain initialized
✓ Two wallets created
Alice: a3f2d8c9e4b7...
Bob: b9e4c7d2a3f1...
Block mined: 0003ab4f9c2d...
💰 Alice's balance: 10050 satoshi
Block mined: 0007c3e8d1a9...
💼 Final balances:
Alice: 6990 satoshi
Bob: 3060 satoshi
✅ Blockchain validation passed!
Core Concepts at a Glance
1. Blockchain
A blockchain is a chain-linked data structure of blocks, where each block contains multiple transactions.
#![allow(unused)]
fn main() {
let mut blockchain = Blockchain::new();
}
Key methods:
create_transaction()- Create a transactionadd_transaction()- Add to the pending poolmine_pending_transactions()- Mine and package transactionsget_balance()- Query balanceis_valid()- Validate the blockchain
2. Wallet
A wallet manages public keys, private keys, and addresses.
#![allow(unused)]
fn main() {
let wallet = Wallet::new();
println!("Address: {}", wallet.address);
println!("Public key: {}", wallet.public_key);
// Keep the private key secret!
}
Key methods:
new()- Create a new walletsign()- Sign dataverify_signature()- Verify a signature
3. Transaction
A transaction is the basic unit of value transfer, using the UTXO model.
#![allow(unused)]
fn main() {
let tx = blockchain.create_transaction(
&sender, // Sender's wallet
receiver_addr, // Receiver's address
amount, // Amount (satoshi)
fee, // Fee (satoshi)
)?;
}
A transaction contains:
- Inputs: the UTXOs being spent
- Outputs: the new UTXOs being created
- Fee: total inputs - total outputs
4. Mining
Mining is the process of packaging transactions into a block via Proof of Work (PoW).
#![allow(unused)]
fn main() {
blockchain.mine_pending_transactions(miner_address)?;
}
Mining process:
- Collect pending transactions
- Create a Coinbase transaction (reward + fees)
- Compute the Merkle root
- Find a hash satisfying the difficulty target (adjust nonce)
- Add the block to the chain
- Update the UTXO set
Advanced Examples
Multiple Transactions
#![allow(unused)]
fn main() {
// Create multiple transactions
for i in 1..=5 {
let tx = blockchain.create_transaction(
&alice,
bob.address.clone(),
100 * i,
i, // Different fees
)?;
blockchain.add_transaction(tx)?;
}
// Package all transactions at once
blockchain.mine_pending_transactions(miner.address.clone())?;
}
Fee-Rate Priority
#![allow(unused)]
fn main() {
// Low-fee transaction
let slow_tx = blockchain.create_transaction(&alice, bob.address.clone(), 1000, 1)?;
// High-fee transaction
let fast_tx = blockchain.create_transaction(&alice, charlie.address.clone(), 1000, 50)?;
blockchain.add_transaction(slow_tx)?;
blockchain.add_transaction(fast_tx)?;
// Miners will prioritize fast_tx (higher fee rate)
blockchain.mine_pending_transactions(miner.address)?;
}
Balance Query
#![allow(unused)]
fn main() {
let balance = blockchain.get_balance(&alice.address);
println!("Balance: {} satoshi ({:.8} BTC)", balance, balance as f64 / 100_000_000.0);
}
REST API Usage
Start the API server:
cargo run --bin btc-server
Create a Wallet
curl -X POST http://localhost:3000/api/wallet/create
Response:
{
"address": "a3f2d8c9e4b7...",
"public_key": "04f9a...",
"private_key": "keep your private key safe"
}
Create a Transaction
curl -X POST http://localhost:3000/api/transaction/create \
-H "Content-Type: application/json" \
-d '{
"from": "alice_address",
"to": "bob_address",
"amount": 5000,
"fee": 10
}'
Query Balance
curl http://localhost:3000/api/balance/alice_address
Mine a Block
curl -X POST http://localhost:3000/api/mine \
-H "Content-Type: application/json" \
-d '{
"miner_address": "miner_address"
}'
Get Blockchain Information
curl http://localhost:3000/api/blockchain/info
Response:
{
"chain_length": 3,
"difficulty": 3,
"pending_transactions": 2,
"latest_block": {
"index": 2,
"hash": "0003ab4f...",
"timestamp": 1703001234,
"transaction_count": 5
}
}
Electron GUI
Launch the graphical interface:
cd frontend
npm install
npm start
GUI features:
- 📊 Blockchain Explorer: Visualize all blocks
- 👛 Wallet Management: Create and import wallets
- 💸 Send Transactions: Create transactions graphically
- ⛏️ Mining: Click a button to start mining
- 🎮 Demo Mode: Run the full demo with one click
Practical Examples
SimpleBTC provides three complete practical examples:
1. Enterprise Multi-Signature Wallet (2-of-3)
cargo run --example enterprise_multisig
What you will learn:
- Creating a multi-signature address
- Collecting signatures
- Enterprise fund management
2. Escrow Service
cargo run --example escrow_service
What you will learn:
- Buyer/seller transactions
- Arbitration mechanism
- Dispute resolution
3. Time-Deposit Savings
cargo run --example timelock_savings
What you will learn:
- Setting up time locks
- Expiry checks
- Enforced saving
Next Steps
Now that you have mastered the basics, you can dive deeper:
-
Understand the Principles - Core Concepts
- UTXO model in depth
- Proof of Work mechanics
- Merkle tree structure
-
Core Features - Core Module Guide
- In-depth wallet usage
- Advanced transaction features
- Blockchain operations
-
Advanced Features - Advanced Functionality
- Merkle tree and SPV
- Multi-signature
- Replace-By-Fee
- Time locks
-
API Reference - API Docs
- Complete API documentation
- Function signatures
- Usage examples
Tips
💡 Tips:
- Mining difficulty 3-4 is suitable for demos; 6+ is closer to reality
- Higher fees lead to faster transaction confirmation
- Call
is_valid()regularly to verify blockchain integrity - Use
print_chain()to view detailed information
⚠️ Caution:
- A lost private key cannot be recovered
- This project is for learning only — do not use it in production
- The simplified cryptographic implementation is not as secure as real Bitcoin
Ready to explore further? Continue reading Core Concepts!
Core Concepts
This section explains the fundamental concepts of SimpleBTC and Bitcoin in detail.
The UTXO Model
What is a UTXO?
UTXO (Unspent Transaction Output) is a core concept in Bitcoin. It represents an output from a previous transaction that has not yet been spent.
Account Model vs. UTXO Model:
| Feature | Account Model (Ethereum) | UTXO Model (Bitcoin) |
|---|---|---|
| Balance storage | Each account has a balance field | Computed from all UTXOs |
| State | Account state (balance, nonce) | Stateless (only a UTXO set) |
| Transfer | A account -100, B account +100 | Consume A’s UTXO, create new UTXO for B |
| Privacy | Weaker (same address reused) | Stronger (new address each time) |
| Parallelism | Weaker (transactions on same account must be sequential) | Stronger (different UTXOs can be processed in parallel) |
UTXO Example
Alice has 3 UTXOs:
UTXO1: 5 BTC (received from Bob)
UTXO2: 3 BTC (received from Charlie)
UTXO3: 2 BTC (mining reward)
Alice's total balance: 5 + 3 + 2 = 10 BTC
UTXO Lifecycle
1. Creation
Transaction output → Added to UTXO set
2. Existence
UTXO set → Can be queried and spent
3. Spending
Referenced by a transaction input → Removed from UTXO set
4. New UTXO creation
Transaction output → New UTXO added to set
Change Mechanism
A UTXO must be spent in full; it cannot be partially spent:
#![allow(unused)]
fn main() {
// Alice wants to send 3 BTC to Bob, but only has one UTXO worth 5 BTC
Inputs:
- UTXO: 5 BTC (Alice's)
Outputs:
- Output 1: 3 BTC → Bob
- Output 2: 1.999 BTC → Alice (change)
- Fee: 0.001 BTC → Miner (inputs - outputs)
}
Blocks and the Blockchain
Block Structure
┌─────────────────────────────────┐
│ Block Header │
├─────────────────────────────────┤
│ index: 123 │ Block height
│ timestamp: 1703001234 │ Timestamp
│ previous_hash: 0x00012ab... │ Parent block hash
│ merkle_root: 0xabc123... │ Merkle tree root
│ nonce: 2847563 │ Proof of Work
│ hash: 0x000034cd... │ Current block hash
├─────────────────────────────────┤
│ Block Body │
├─────────────────────────────────┤
│ Transaction 1 (Coinbase) │ Mining reward
│ Transaction 2 │ Regular transaction
│ Transaction 3 │ Regular transaction
│ ... │
└─────────────────────────────────┘
Chain Structure
Genesis Block → Block 1 → Block 2 → ... → Latest Block
↓ ↓ ↓ ↓
hash=A hash=B hash=C hash=Z
prev=0 prev=A prev=B prev=Y
Each block points to its parent block via previous_hash, forming an immutable chain.
Why is it Immutable?
- Hash linking: Changing any transaction changes the block’s hash
- Cascade invalidation: A changed block hash breaks the
previous_hashof all subsequent blocks - Computational cost: Tampering with history requires re-mining all subsequent blocks
- Longest chain rule: An attacker would need to outpace the entire network — nearly impossible (except with a 51% attack)
Proof of Work (PoW)
Mining Principle
Find a nonce value such that the block hash satisfies the difficulty requirement:
#![allow(unused)]
fn main() {
target = "000..." // difficulty leading zeros
while hash(block_header + nonce) >= target {
nonce++;
}
}
Difficulty Example
difficulty = 3 (demo)
target = "000..."
Valid hashes:
✅ 0003ab4f9c2d...
✅ 000f12e8a3b9...
Invalid hashes:
❌ 001a3f2e8d4c... (only 2 leading zeros)
❌ 0123456789ab... (only 1 leading zero)
Difficulty and Security
| Difficulty | Average Attempts | Use Case |
|---|---|---|
| 1 | 16 | Testing |
| 3 | 4,096 | Demo |
| 5 | 1,048,576 | Small network |
| 10 | ~10¹² | Private chain |
| 20 | ~10²⁴ | Bitcoin-level |
Bitcoin’s actual difficulty is approximately 70-80 bits, with the global hash rate in the hundreds of EH/s.
Why is PoW Needed?
- Prevent spam attacks: Creating a block requires computational cost
- Fair competition: Higher hash power means higher probability of winning
- Decentralization: Anyone can participate in mining
- Economic incentive: Miners receive rewards (Coinbase + fees)
Merkle Tree
Structure Example
Merkle tree for 4 transactions:
Root Hash
/ \
H(AB) H(CD)
/ \ / \
H(A) H(B) H(C) H(D)
↑ ↑ ↑ ↑
Tx1 Tx2 Tx3 Tx4
Construction process:
- Compute the hash of each transaction (leaf nodes)
- Pair them up and compute the parent node hash
- Repeat until only one root hash remains
- The root hash is stored in the block header
SPV Verification (Lightweight Verification)
No need to download the entire block — only the block header and Merkle proof are needed:
Verify that Tx2 is in the block:
Required:
- Hash of Tx2
- Merkle proof: [H(A), H(CD)]
- Root Hash from the block header
Verification:
1. Compute H(B) = hash(Tx2)
2. Compute H(AB) = hash(H(A) + H(B))
3. Compute Root = hash(H(AB) + H(CD))
4. Compare computed Root with the Root in the block header
✅ Match → Tx2 is indeed in the block
❌ No match → Tx2 is absent or has been tampered with
Advantages of SPV
- Lightweight: Only needs the block header (~80 bytes), not the full block (1-2 MB)
- Fast: O(log n) verification complexity
- Mobile-friendly: Can run on phone wallets
- Secure: Protected by PoW; no need to trust a third party
Cryptographic Foundations
Hash Function (SHA256)
Properties:
- Deterministic: the same input always produces the same output
- Fast to compute: millisecond-level
- Irreversible: cannot reverse-engineer the input from the hash
- Collision-resistant: finding two inputs with the same hash is practically impossible
- Avalanche effect: a tiny change in input produces a completely different hash
Example:
hash("hello") = 2cf24dba5fb0a30e...
hash("hallo") = d3751d33f9cd5049... (completely different!)
Digital Signatures (Simplified ECDSA)
Real Bitcoin:
1. Private key (256-bit random number)
↓ Elliptic curve operation
2. Public key (elliptic curve point)
↓ SHA256 + RIPEMD160
3. Address (Base58 encoded)
SimpleBTC Simplified:
1. Private key (random string)
↓ SHA256
2. Public key (hash value)
↓ SHA256, first 20 bytes
3. Address (hexadecimal string)
Signature Verification
#![allow(unused)]
fn main() {
// Signing
signature = hash(private_key + data)
// Verification (simplified)
verify(public_key, data, signature) -> bool
}
Real Bitcoin uses the ECDSA algorithm, which is mathematically provably secure.
Transaction Structure
Transaction Anatomy
#![allow(unused)]
fn main() {
Transaction {
id: "abc123...", // Transaction hash
inputs: [ // Inputs (which UTXOs to spend)
TxInput {
txid: "prev_tx", // Referenced transaction ID
vout: 0, // Output index
signature: "...", // Signature
pub_key: "...", // Public key
}
],
outputs: [ // Outputs (which new UTXOs to create)
TxOutput {
value: 3000, // Amount (satoshi)
pub_key_hash: "bob_address",
},
TxOutput {
value: 6990, // Change
pub_key_hash: "alice_address",
}
],
timestamp: 1703001234,
fee: 10, // Transaction fee
}
}
Transaction Validation
When a miner validates a transaction, it checks:
- ✅ Valid signature: Each input’s signature is correct
- ✅ UTXO exists: The referenced UTXO is in the UTXO set
- ✅ No double-spend: The UTXO has not been spent by another transaction
- ✅ Sufficient balance: Total inputs ≥ total outputs
- ✅ Correct format: Conforms to the protocol specification
Coinbase Transaction
The first transaction in every block, used to distribute the mining reward:
#![allow(unused)]
fn main() {
Transaction {
id: "coinbase_tx",
inputs: [
TxInput {
txid: "", // Empty (does not reference a UTXO)
vout: 0,
signature: "coinbase",
pub_key: "coinbase",
}
],
outputs: [
TxOutput {
value: 50 + total_fees, // Reward + fees
pub_key_hash: "miner_address",
}
],
fee: 0,
}
}
Consensus Mechanism
Longest Chain Rule
When a fork occurs, the network selects the chain with the most accumulated work:
Block 3a (PoW difficulty 3)
/
Block 2
\
Block 3b → Block 4b (PoW difficulty 3)
The chain containing Block 4b has greater total difficulty and becomes the main chain. Block 3a is orphaned.
Why the Longest Chain?
- Work: A longer chain represents more computational investment
- Majority consensus: Honest nodes always mine on the longest chain
- Attack difficulty: An attacker would need more than 51% of the total network hash rate
The 6-Confirmation Rule
Your Tx → Block N → N+1 → N+2 → N+3 → N+4 → N+5 → N+6
0 conf 1 conf 2 conf 3 conf 4 conf 5 conf 6 conf
- 0 confirmations: May be double-spent (RBF)
- 1 confirmation: Relatively safe (small payments)
- 3 confirmations: Safe (medium amounts)
- 6 confirmations: Very safe (large transfers)
Fee Market
Fee Rate Calculation
fee_rate = fee / transaction_size (sat/byte)
Priority
Miner transaction selection strategy:
#![allow(unused)]
fn main() {
// Sort by fee rate from high to low
transactions.sort_by(|a, b| {
b.fee_rate().cmp(&a.fee_rate())
});
}
Higher fee-rate transactions are packaged first.
Fee Recommendations
| Urgency | Fee Rate | Confirmation Time |
|---|---|---|
| Low priority | 1-5 sat/byte | Several hours |
| Medium priority | 5-20 sat/byte | 30-60 minutes |
| High priority | 20-50 sat/byte | 10-20 minutes |
| Urgent | 50+ sat/byte | Next block |
Network Parameters
Time-Related
- Block time: ~10 minutes (maintained via difficulty adjustment)
- Difficulty adjustment: Every 2,016 blocks (~2 weeks)
- Halving cycle: Every 210,000 blocks (~4 years)
Economic Parameters
- Initial reward: 50 BTC
- Current reward: 3.125 BTC (after the 2024 halving)
- Total supply: 21 million BTC (never increases)
- Smallest unit: 1 satoshi = 0.00000001 BTC
Size Limits
- Block size: 1 MB (legacy) / 4 MB (SegWit)
- Transaction size: ~250-500 bytes on average
- Transactions per block: ~2,000-3,000
Next Steps
Now that you understand the core concepts, continue learning:
- Wallet Management - Deep dive into keys and addresses
- Transaction Processing - Transaction creation and validation
- Blockchain Operations - Mining and block management
- UTXO Management - UTXO set operations
💡 Self-Quiz: Try answering the following questions to test your understanding:
- What is the main difference between the UTXO model and the account model?
- Why is the blockchain immutable?
- How does the Merkle tree enable SPV verification?
- What is the purpose of Proof of Work?
- What is the change mechanism?
Architecture Overview
SimpleBTC is a fully-featured educational implementation of the Bitcoin blockchain, written in Rust. This chapter introduces the project’s overall architectural design, module organization, core data flows, and key abstraction layers.
Project Structure
SimpleBTC/
├── src/
│ ├── lib.rs # Crate entry point, module exports and re-exports
│ ├── main.rs # Executable entry point (demo / interactive CLI)
│ │
│ │── ── Core Layer ──
│ ├── block.rs # Block structure + Proof of Work
│ ├── blockchain.rs # Core blockchain logic (UTXO management, mining, validation)
│ ├── transaction.rs # Transaction structures (TxInput / TxOutput / Transaction)
│ ├── wallet.rs # Wallet + secp256k1 key management
│ ├── utxo.rs # UTXO set (unspent transaction output management)
│ ├── crypto.rs # Extended cryptography (Bech32, WIF export)
│ │
│ │── ── Advanced Feature Layer ──
│ ├── merkle.rs # Merkle tree (transaction inclusion proofs)
│ ├── multisig.rs # Multi-signature (M-of-N)
│ ├── advanced_tx.rs # Advanced transactions (RBF, TimeLock)
│ ├── mempool.rs # Memory pool (sorted by fee rate)
│ ├── script.rs # Bitcoin Script system
│ ├── spv.rs # SPV lightweight client verification
│ ├── parallel_mining.rs # Multi-threaded parallel PoW mining
│ ├── network.rs # P2P network layer
│ │
│ │── ── Infrastructure Layer ──
│ ├── storage.rs # RocksDB high-performance persistent storage
│ ├── persistence.rs # Serialization / deserialization helpers
│ ├── config.rs # Global configuration (difficulty, rewards, etc.)
│ ├── logging.rs # Structured logging (tracing)
│ ├── security.rs # Security validation
│ ├── indexer.rs # Transaction indexer (accelerates address queries)
│ └── error.rs # Unified error types
│
├── docs/ # mdBook documentation
├── Cargo.toml
└── README.md
Three-Layer Architecture Model
SimpleBTC’s modules are organized into three responsibility layers:
┌─────────────────────────────────────────────────────────┐
│ Core Layer │
│ block blockchain transaction wallet utxo crypto │
│ ─── Implements Bitcoin's fundamental data structures │
│ and protocol rules ─── │
├─────────────────────────────────────────────────────────┤
│ Advanced Feature Layer │
│ merkle multisig advanced_tx mempool script spv │
│ parallel_mining network │
│ ─── Implements Bitcoin's advanced features and │
│ extended protocols ─── │
├─────────────────────────────────────────────────────────┤
│ Infrastructure Layer │
│ storage persistence config logging security │
│ indexer error │
│ ─── Provides general-purpose capabilities: storage, │
│ logging, configuration, etc. ─── │
└─────────────────────────────────────────────────────────┘
Core Layer Module Details
| Module | File | Responsibility |
|---|---|---|
block | block.rs | Defines the Block struct, containing block header fields (index, timestamp, nonce, merkle_root, previous_hash, hash) and the single-threaded mine_block() method |
blockchain | blockchain.rs | The Blockchain main struct, coordinating all blockchain operations: genesis block, transaction creation, mempool management, parallel mining, UTXO updates, and chain validation |
transaction | transaction.rs | Three core structs: TxInput, TxOutput, Transaction; Coinbase transaction construction; ECDSA signature verification |
wallet | wallet.rs | Wallet struct, uses secp256k1 to generate real key pairs, P2PKH address derivation, ECDSA signing and verification |
utxo | utxo.rs | UTXOSet manages all unspent transaction outputs, supports balance queries and spendable UTXO retrieval |
crypto | crypto.rs | CryptoWallet extended implementation: Bech32 addresses, WIF private key format import/export |
Advanced Feature Layer Module Details
| Module | File | Responsibility |
|---|---|---|
merkle | merkle.rs | Merkle tree construction and Merkle proof generation/verification (foundation for SPV) |
multisig | multisig.rs | M-of-N multi-signature scheme |
advanced_tx | advanced_tx.rs | RBF (Replace-By-Fee) fee replacement, TimeLock time-locked transactions |
mempool | mempool.rs | Memory pool, sorts pending transactions by fee rate (satoshi/byte) |
script | script.rs | Bitcoin Script opcode interpreter |
spv | spv.rs | Simple Payment Verification — validates transactions using Merkle proofs without downloading the full chain |
parallel_mining | parallel_mining.rs | ParallelMiner: multi-threaded PoW that fully utilizes multi-core CPUs |
network | network.rs | P2P network message propagation layer |
Infrastructure Layer Module Details
| Module | File | Responsibility |
|---|---|---|
storage | storage.rs | High-performance key-value storage based on RocksDB |
persistence | persistence.rs | Blockchain data serialization and deserialization |
config | config.rs | Global parameters (mining difficulty, block reward, network parameters, etc.) |
logging | logging.rs | Structured logging (based on the tracing crate) |
security | security.rs | Additional security validation logic |
indexer | indexer.rs | TransactionIndexer: builds an address → transaction ID index to accelerate balance queries |
error | error.rs | BitcoinError unified error enum, Result<T> type alias |
Core Data Flow
Complete Value Transfer Flow
User initiates a transfer request
│
▼
┌─────────────────────────────────────┐
│ Blockchain::create_transaction() │
│ 1. Find spendable outputs in UTXOSet│
│ 2. Sign inputs with Wallet::sign() │
│ 3. Construct TxInput + TxOutput │
│ 4. Generate Transaction (with hash ID)│
└──────────────┬──────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Blockchain::add_transaction() │
│ 1. Transaction::verify() — verify signatures│
│ 2. Check UTXO exists + balance sufficient│
│ 3. Record pending_spent to prevent double-spend│
│ 4. Add to Mempool (sorted by fee rate)│
└──────────────┬──────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Blockchain::mine_pending_transactions()│
│ 1. Retrieve high-fee-rate transactions from Mempool│
│ 2. Construct Coinbase transaction (reward + fees)│
│ 3. Block::new() computes Merkle Root│
│ 4. ParallelMiner multi-threaded PoW│
│ 5. Validate all transactions in the block│
└──────────────┬──────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Atomic UTXO set update │
│ 1. Consume UTXOs referenced by inputs│
│ 2. Add outputs as new UTXOs │
│ 3. Clear pending_spent │
└──────────────┬──────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Block appended to chain │
│ 1. indexer.index_block() — build index│
│ 2. chain.push(block) │
│ 3. Remove confirmed transactions from Mempool│
└─────────────────────────────────────┘
Data Structure Relationship Diagram
Blockchain
├── chain: Vec<Block>
│ └── Block
│ ├── index, timestamp, nonce
│ ├── previous_hash → previous block's hash (chain linkage)
│ ├── merkle_root → computed by MerkleTree
│ ├── hash → SHA256(index+timestamp+merkle_root+prev+nonce)
│ └── transactions: Vec<Transaction>
│ └── Transaction
│ ├── id → SHA256(transaction content)
│ ├── inputs: Vec<TxInput>
│ │ └── TxInput {txid, vout, signature, pub_key}
│ └── outputs: Vec<TxOutput>
│ └── TxOutput {value, pub_key_hash}
│
├── utxo_set: UTXOSet ← fast balance query and UTXO retrieval
├── mempool: Mempool ← pending transactions (sorted by fee rate)
├── indexer: TransactionIndexer ← address → transaction index
└── miner: ParallelMiner ← multi-threaded PoW
Quick Start
#![allow(unused)]
fn main() {
use bitcoin_simulation::{blockchain::Blockchain, wallet::Wallet};
// 1. Create a blockchain (includes genesis block; genesis wallet receives 10M satoshi initial funds)
let mut blockchain = Blockchain::new();
// 2. Get the pre-funded genesis wallet + create new user wallets
let genesis = Blockchain::genesis_wallet();
let alice = Wallet::new();
let bob = Wallet::new();
// 3. Create transaction: genesis → alice, transfer 1000 satoshi, fee 10
let tx = blockchain.create_transaction(&genesis, alice.address.clone(), 1000, 10)?;
blockchain.add_transaction(tx)?;
// 4. Mine (alice receives the block reward as miner)
blockchain.mine_pending_transactions(alice.address.clone())?;
// 5. Query balance
println!("Alice's balance: {} satoshi", blockchain.get_balance(&alice.address));
// 6. Validate the entire chain's integrity
assert!(blockchain.is_valid());
Ok::<(), String>(())
}
Cryptography Choices
| Algorithm | Library | Purpose |
|---|---|---|
| secp256k1 ECDSA | secp256k1 crate | Private key generation, transaction signing, signature verification |
| SHA-256 | bitcoin_hashes | Block hash, transaction hash, address derivation |
| RIPEMD-160 | ripemd crate | Public key hash (intermediate step in P2PKH address) |
| Base58Check | bs58 crate | P2PKH address encoding, WIF private key encoding |
| Bech32 | bech32 crate | Native SegWit addresses |
| SHA-256d | bitcoin_hashes | Double hash (checksum computation) |
All cryptographic implementations are compatible with the Bitcoin mainnet — addresses generated by Wallet::genesis() can be used legitimately in the real Bitcoin protocol.
Concurrency Design
Mining (ParallelMiner) is the only module in the project that makes heavy use of multi-threading. The blockchain state itself (the Blockchain struct) follows a single-threaded ownership model; Rust’s borrow checker guarantees data safety at compile time, eliminating the need for runtime lock overhead.
#![allow(unused)]
fn main() {
// Parallel mining: automatically partitions the nonce search space based on CPU core count
self.miner
.mine_block(&mut block, self.difficulty)
.map_err(|e| format!("Mining failed: {}", e))?;
}
Error Handling
All public APIs return Result<T, String> or crate::error::Result<T> (i.e., Result<T, BitcoinError>). BitcoinError is a unified enum type covering:
PrivateKeyError— key format error- Insufficient balance, UTXO not found, signature verification failure, and other domain errors
#![allow(unused)]
fn main() {
use bitcoin_simulation::{BitcoinError, Result};
fn example() -> Result<()> {
let blockchain = Blockchain::new();
// ...
Ok(())
}
}
Wallet Management
At its core, a Bitcoin wallet is a key pair: a private key and a public key. SimpleBTC uses secp256k1 elliptic curve cryptography that is fully compatible with real Bitcoin, implementing two structs: Wallet (the primary wallet) and CryptoWallet (extended wallet, supporting Bech32 and WIF).
Key System Overview
Bitcoin key generation follows a strict one-way derivation chain:
Random number (256 bit)
│
▼ secp256k1 elliptic curve multiplication
Private key (SecretKey, 32 bytes)
│
▼ scalar multiplication by generator point G
Public key (PublicKey, 33 bytes compressed format)
│
├─▶ SHA-256 hash
│ │
│ ▼ RIPEMD-160 hash
│ Public key hash (20 bytes)
│ │
│ ▼ version prefix 0x00 + double SHA-256 checksum + Base58
│ P2PKH address (starts with '1')
│
└─▶ SHA-256 + RIPEMD-160
│
▼ Bech32 encoding (witness v0)
Bech32 address (starts with 'bc1')
Elliptic curve equation (secp256k1):
y² = x³ + 7 (mod p)
p = 2²⁵⁶ − 2³² − 977 (a very large prime)
Deriving a public key from a private key is a one-way operation — computationally irreversible (discrete logarithm problem).
The Wallet Struct
Wallet is the most commonly used wallet type in the project, defined in src/wallet.rs:
#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Wallet {
/// Bitcoin P2PKH address (starts with '1')
pub address: String,
/// Compressed public key in hexadecimal (33 bytes = 66 hex characters)
pub public_key: String,
/// secp256k1 private key (stored as hexadecimal during serialization, access restricted)
#[serde(with = "secret_key_serde")]
private_key: SecretKey,
}
}
Field descriptions:
address: P2PKH format address, publicly used — safe to share with others as a receiving addresspublic_key: Compressed public key (33 bytes), used to verify signatures; included in each transaction inputprivate_key: Private key — must be strictly kept secret; possessing the private key is equivalent to owning all funds at the corresponding address
Creating a Wallet
Generating a Random Wallet
#![allow(unused)]
fn main() {
use bitcoin_simulation::wallet::Wallet;
let wallet = Wallet::new();
println!("Address: {}", wallet.address); // P2PKH address starting with '1'
println!("Public key: {}", wallet.public_key); // 66-character hex
println!("Private key: {}", wallet.private_key_hex()); // 64-character hex (keep secret!)
}
Internal flow of Wallet::new():
#![allow(unused)]
fn main() {
pub fn new() -> Self {
let secp = Secp256k1::new();
// Uses a cryptographically secure random number generator (OsRng)
let (secret_key, public_key) = secp.generate_keypair(&mut rand::thread_rng());
let address = Self::pubkey_to_address(&public_key);
let public_key_hex = hex::encode(public_key.serialize());
Wallet { address, public_key: public_key_hex, private_key: secret_key }
}
}
Genesis Wallet
The genesis wallet uses a fixed private key 0x01, generating the same address on every startup. This makes it convenient for spending the initial funds from the genesis block during demos:
#![allow(unused)]
fn main() {
use bitcoin_simulation::{blockchain::Blockchain, wallet::Wallet};
// Two equivalent ways to obtain it
let genesis = Blockchain::genesis_wallet();
let genesis2 = Wallet::genesis();
assert_eq!(genesis.address, genesis2.address); // Address is deterministically consistent
// Internal implementation (src/wallet.rs)
pub fn genesis() -> Self {
Self::from_private_key_hex(
"0000000000000000000000000000000000000000000000000000000000000001",
)
.expect("genesis private key is valid")
}
}
Warning: The genesis wallet’s private key is public — never use it in a production environment.
Recovering a Wallet from a Private Key
#![allow(unused)]
fn main() {
// Recover from a hexadecimal private key
let wallet = Wallet::new();
let hex = wallet.private_key_hex(); // Export private key
let recovered = Wallet::from_private_key_hex(&hex)?;
assert_eq!(wallet.address, recovered.address); // Address is identical
}
P2PKH Address Derivation
Wallet::pubkey_to_address() implements address generation steps that are identical to real Bitcoin:
#![allow(unused)]
fn main() {
fn pubkey_to_address(public_key: &PublicKey) -> String {
// Step 1: Serialize compressed public key (33 bytes: 1-byte prefix + 32-byte x coordinate)
let pubkey_bytes = public_key.serialize();
// Step 2: SHA-256 hash
let sha256_hash = sha256::Hash::hash(&pubkey_bytes);
// Step 3: RIPEMD-160 hash → public key hash (20 bytes)
let mut ripemd = Ripemd160::new();
ripemd.update(&sha256_hash[..]);
let pubkey_hash = ripemd.finalize();
// Step 4: Add version byte (mainnet = 0x00)
let mut versioned = vec![0x00];
versioned.extend_from_slice(&pubkey_hash); // 21 bytes total
// Step 5: Double SHA-256, take first 4 bytes as checksum
let checksum = sha256d::Hash::hash(&versioned);
versioned.extend_from_slice(&checksum[0..4]); // 25 bytes total
// Step 6: Base58 encode → address starting with '1' (~34 characters)
bs58::encode(versioned).into_string()
}
}
Why RIPEMD-160?
- Compresses the 33-byte public key to 20 bytes, saving blockchain storage space
- Even if a quantum computer breaks ECDSA, the attacker would still need to break the hash function
Why Base58 (not Base64)?
- Removes easily confused characters: 0 (zero), O (uppercase O), I (uppercase i), l (lowercase L)
- Avoids issues such as spaces being included when double-clicking to copy
Transaction Signing
Wallet::sign() uses the private key to generate an ECDSA signature over data:
#![allow(unused)]
fn main() {
pub fn sign(&self, data: &str) -> String {
let secp = Secp256k1::new();
// 1. SHA-256 hash the raw data
let msg_hash = sha256::Hash::hash(data.as_bytes());
let message = Message::from_digest(msg_hash.to_byte_array());
// 2. Generate ECDSA signature using the private key
let signature = secp.sign_ecdsa(&message, &self.private_key);
// 3. DER-encode and return as a hex string
hex::encode(signature.serialize_der())
}
}
In Blockchain::create_transaction(), the signed data is "{txid}{vout}" — the location identifier of the UTXO being referenced:
#![allow(unused)]
fn main() {
// Excerpt from src/blockchain.rs
for (txid, vout) in utxos {
let signature = from_wallet.sign(&format!("{}{}", txid, vout));
let input = TxInput::new(txid, vout, signature, from_wallet.public_key.clone());
inputs.push(input);
}
}
This binds each input’s signature to a specific UTXO, preventing the signature from being replayed against other UTXOs.
Signature Verification
Wallet::verify_signature() is a static method — no private key is needed:
#![allow(unused)]
fn main() {
pub fn verify_signature(public_key_hex: &str, data: &str, signature_hex: &str) -> bool {
// 1. Decode the public key
let Ok(pubkey_bytes) = hex::decode(public_key_hex) else { return false; };
let Ok(public_key) = PublicKey::from_slice(&pubkey_bytes) else { return false; };
// 2. Decode the DER signature
let Ok(sig_bytes) = hex::decode(signature_hex) else { return false; };
let Ok(signature) = Signature::from_der(&sig_bytes) else { return false; };
// 3. Re-hash the raw data (exactly as during signing)
let secp = Secp256k1::new();
let msg_hash = sha256::Hash::hash(data.as_bytes());
let message = Message::from_digest(msg_hash.to_byte_array());
// 4. Mathematical verification: check whether the signature was generated by the corresponding private key
secp.verify_ecdsa(&message, &signature, &public_key).is_ok()
}
}
Complete sign + verify example:
#![allow(unused)]
fn main() {
use bitcoin_simulation::wallet::Wallet;
let wallet = Wallet::new();
let data = "Hello, Bitcoin!";
// Sign
let signature = wallet.sign(data);
println!("Signature: {}", &signature[..32]); // DER-encoded hex
// Verify correct data: should pass
assert!(Wallet::verify_signature(&wallet.public_key, data, &signature));
// Verify tampered data: should fail
assert!(!Wallet::verify_signature(&wallet.public_key, "Tampered!", &signature));
// Verify with wrong public key: should fail
let other = Wallet::new();
assert!(!Wallet::verify_signature(&other.public_key, data, &signature));
}
Extended Wallet: CryptoWallet
CryptoWallet in src/crypto.rs adds more Bitcoin protocol features on top of Wallet:
#![allow(unused)]
fn main() {
use bitcoin_simulation::crypto::CryptoWallet;
let wallet = CryptoWallet::new();
println!("P2PKH address: {}", wallet.address); // starts with '1'
println!("Bech32 address: {}", wallet.bech32_address); // starts with 'bc1' (SegWit)
println!("Private key hex: {}", wallet.private_key_hex()); // 64 characters
println!("Public key hex: {}", wallet.public_key_hex()); // 66 characters
}
WIF Private Key Format
WIF (Wallet Import Format) is the standard format for importing and exporting private keys between Bitcoin wallets:
#![allow(unused)]
fn main() {
let wallet = CryptoWallet::new();
// Export as WIF (starts with '5', 'K', or 'L')
let wif = wallet.export_private_key_wif();
println!("WIF: {}", wif);
// Recover wallet from WIF
let imported = CryptoWallet::import_from_wif(&wif)?;
assert_eq!(wallet.address, imported.address);
}
WIF encoding steps:
- Add version byte
0x80(mainnet private key prefix) - Compute double SHA-256 checksum (take first 4 bytes)
- Concatenate and Base58-encode
CryptoWallet Signing Interface
CryptoWallet’s signing interface accepts a byte slice, which is more flexible:
#![allow(unused)]
fn main() {
let wallet = CryptoWallet::new();
let message = b"Hello, Bitcoin!";
// Sign (returns a secp256k1::ecdsa::Signature type)
let signature = wallet.sign(message);
// Verify (static method)
assert!(CryptoWallet::verify(message, &signature, &wallet.public_key));
}
Wallet Serialization
Both Wallet and CryptoWallet implement Serialize / Deserialize, with the private key securely stored as a hexadecimal string:
#![allow(unused)]
fn main() {
use bitcoin_simulation::wallet::Wallet;
let wallet = Wallet::new();
// Serialize to JSON
let json = serde_json::to_string(&wallet)?;
// {"address":"1...","public_key":"02...","private_key":"a1b2c3..."}
// Restore from JSON with full signing capability preserved
let restored: Wallet = serde_json::from_str(&json)?;
let sig = restored.sign("test");
assert!(Wallet::verify_signature(&restored.public_key, "test", &sig));
}
Security Recommendations
| Item | Description |
|---|---|
| Keep private key secret | Anyone who obtains the private key can spend all funds at the corresponding address |
| Do not reuse addresses | Use a new address for each payment to protect privacy |
| Genesis wallet for demo only | Wallet::genesis() uses a public private key — never use it for real funds |
| Back up your private key | Losing the private key means permanently losing the corresponding funds |
| Use WIF format for backups | WIF format includes a checksum, which can detect transcription errors |
Transaction Processing
One of Bitcoin’s core innovations is the UTXO (Unspent Transaction Output) model. Unlike a bank account balance, there is no concept of an “account balance” in the Bitcoin system — all funds exist in a distributed form across the blockchain as unspent transaction outputs. This chapter provides an in-depth look at SimpleBTC’s transaction structure, UTXO model, and transaction creation and validation mechanisms.
UTXO Model Fundamentals
What is a UTXO
UTXO stands for “Unspent Transaction Output.” Every Bitcoin transaction consumes some existing UTXOs (as inputs) and simultaneously creates new UTXOs (as outputs).
┌──────────────────────────────────────────────────────────────────┐
│ Traditional Account Model (e.g., banking) │
│ Alice balance: 100 Bob balance: 0 │
│ Transfer 30 → Alice: 70, Bob: 30 │
├──────────────────────────────────────────────────────────────────┤
│ UTXO Model (Bitcoin) │
│ On-chain: UTXO_A (owned by Alice, value 100) │
│ Transfer 30: │
│ Consume: UTXO_A (100) ← must be consumed in full │
│ Create: UTXO_B (30, owned by Bob) ← transfer amount │
│ UTXO_C (60, owned by Alice) ← change (100 - 30 - 10 fee)│
└──────────────────────────────────────────────────────────────────┘
Key properties of the UTXO model:
- Each UTXO can only be spent once (removed from the UTXO set after spending)
- A UTXO must be consumed in full when spent; any excess is returned to the sender as a “change” output
- “Balance” = the sum of all UTXO values owned by an address (calculated by
UTXOSet) - Unspent UTXOs form the “UTXO set” (a Bitcoin full node must maintain approximately 5-10 GB of UTXO set data)
Transaction Data Structures
TxInput (Transaction Input)
A transaction input references an existing UTXO and provides proof of authorization to spend it (a digital signature):
#![allow(unused)]
fn main() {
// src/transaction.rs
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TxInput {
pub txid: String, // ID of the referenced transaction (32-byte hash, 64 hex characters)
pub vout: usize, // Index of the output within that transaction (0-based)
pub signature: String, // ECDSA signature (DER-encoded, hex string)
pub pub_key: String, // Sender's compressed public key (33 bytes, 66 hex characters)
}
}
The combination of txid + vout uniquely identifies a UTXO on the blockchain. The signature is generated by the sender’s private key, proving ownership of that UTXO.
TxOutput (Transaction Output)
A transaction output defines the amount the recipient can receive; it is the carrier of a new UTXO:
#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TxOutput {
pub value: u64, // Amount (unit: satoshi; 1 BTC = 10⁸ satoshi)
pub pub_key_hash: String, // Recipient address (P2PKH) = locking script
}
impl TxOutput {
pub fn new(value: u64, address: String) -> Self {
TxOutput { value, pub_key_hash: address }
}
/// Check whether this output can be unlocked by a given address
/// (i.e., whether that address is the recipient of this output)
pub fn can_be_unlocked_with(&self, address: &str) -> bool {
self.pub_key_hash == address
}
}
}
pub_key_hash is the locking script (scriptPubKey) in real Bitcoin; here it is simplified to the recipient’s address.
Transaction
#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Transaction {
pub id: String, // Transaction ID = SHA-256(transaction content)
pub inputs: Vec<TxInput>, // Input list (which UTXOs to consume)
pub outputs: Vec<TxOutput>, // Output list (which new UTXOs to create)
pub timestamp: u64, // Unix timestamp (seconds)
pub fee: u64, // Fee = total inputs - total outputs
}
}
Invariant: total inputs = total outputs + fee
Creating a Regular Transaction
Regular transactions are created via Blockchain::create_transaction(), which automatically handles UTXO selection, change calculation, and signing:
#![allow(unused)]
fn main() {
pub fn create_transaction(
&self,
from_wallet: &Wallet, // Sender's wallet (private key required for signing)
to_address: String, // Recipient's address
amount: u64, // Transfer amount (satoshi)
fee: u64, // Transaction fee (satoshi)
) -> Result<Transaction, String>
}
Internal flow:
#![allow(unused)]
fn main() {
// Excerpt from src/blockchain.rs (simplified)
// 1. Calculate total needed
let total_needed = amount + fee;
// 2. Find sufficient UTXOs in the UTXO set (excluding those already spent by pending transactions)
let spendable = self.utxo_set.find_spendable_outputs_excluding(
&from_wallet.address,
total_needed,
&self.pending_spent,
);
let (accumulated, utxos) = spendable.ok_or_else(|| "Insufficient balance (including fee)".to_string())?;
// 3. Create an input for each selected UTXO and sign it
let mut inputs = Vec::new();
for (txid, vout) in utxos {
let signature = from_wallet.sign(&format!("{}{}", txid, vout));
let input = TxInput::new(txid, vout, signature, from_wallet.public_key.clone());
inputs.push(input);
}
// 4. Create outputs (transfer + change)
let mut outputs = Vec::new();
outputs.push(TxOutput::new(amount, to_address));
if accumulated > total_needed {
// Return change to the sender (minus the fee)
outputs.push(TxOutput::new(accumulated - total_needed, from_wallet.address.clone()));
}
// 5. Construct the transaction (ID is computed automatically)
Ok(Transaction::new(inputs, outputs, timestamp, fee))
}
Complete usage example:
#![allow(unused)]
fn main() {
use bitcoin_simulation::{blockchain::Blockchain, wallet::Wallet};
let mut blockchain = Blockchain::new();
let genesis = Blockchain::genesis_wallet(); // Pre-funded with 10M satoshi
let alice = Wallet::new();
// genesis transfers 5000 satoshi to alice, with a fee of 100
let tx = blockchain.create_transaction(
&genesis,
alice.address.clone(),
5000, // Transfer amount
100, // Fee
)?;
println!("Transaction ID: {}", tx.id);
println!("Number of inputs: {}", tx.inputs.len());
println!("Number of outputs: {}", tx.outputs.len()); // Usually 2 (transfer + change)
println!("Fee: {} satoshi", tx.fee);
println!("Fee rate: {:.2} sat/byte", tx.fee_rate());
}
Coinbase Transaction
A Coinbase transaction is the first transaction in every block, dedicated to paying the block reward to the miner. Key differences from a regular transaction:
| Feature | Regular Transaction | Coinbase Transaction |
|---|---|---|
| Inputs | Reference existing UTXOs | Empty txid (created from nothing) |
| Outputs | Transfer + change | Block reward + all fees |
| Signature | ECDSA signature | No signature required (pub_key = "coinbase") |
| Validation | All input signatures verified | Signature verification skipped (is_coinbase() = true) |
#![allow(unused)]
fn main() {
pub fn new_coinbase(to: String, reward: u64, timestamp: u64, total_fees: u64) -> Self {
// Use an atomic counter to ensure each coinbase transaction ID is unique
// (similar to BIP34 block height encoding)
static COINBASE_COUNTER: AtomicU64 = AtomicU64::new(0);
let nonce = COINBASE_COUNTER.fetch_add(1, Ordering::Relaxed);
// Output = block reward + all transaction fees
let tx_out = TxOutput::new(reward + total_fees, to);
let tx_in = TxInput {
txid: String::new(), // Empty txid identifies coinbase
vout: 0,
signature: format!("coinbase:{}", nonce), // Uniqueness field
pub_key: String::from("coinbase"),
};
// ...
}
}
How to identify a Coinbase transaction:
#![allow(unused)]
fn main() {
pub fn is_coinbase(&self) -> bool {
// Only one input, and that input's txid is empty
self.inputs.len() == 1 && self.inputs[0].txid.is_empty()
}
}
Transaction Validation
Transaction::verify() validates the ECDSA signatures of all inputs:
#![allow(unused)]
fn main() {
pub fn verify(&self) -> bool {
// Coinbase transactions do not need signature verification
if self.is_coinbase() {
return true;
}
// Must have at least one input and one output
if self.inputs.is_empty() || self.outputs.is_empty() {
return false;
}
// Verify the ECDSA signature of each input
for input in &self.inputs {
// The signed data is the location identifier of the UTXO being spent
let signed_data = format!("{}{}", input.txid, input.vout);
if !Wallet::verify_signature(&input.pub_key, &signed_data, &input.signature) {
return false;
}
}
true
}
}
Validation logic explanation:
- The signed data is
"{txid}{vout}", binding the signature to a specific UTXO and preventing signature replay attacks - The public key carried in the input (
pub_key) is used to verify the signature — full nodes do not need additional lookups Wallet::verify_signature()internally calls secp256k1 to perform real elliptic curve mathematical verification
Complete validation flow when adding a transaction to the blockchain (Blockchain::add_transaction()):
#![allow(unused)]
fn main() {
// 1. ECDSA signature verification
if !transaction.verify() {
return Err("Transaction verification failed".to_string());
}
// 2. Verify UTXO exists and balance is sufficient
let mut input_sum = 0u64;
for input in &transaction.inputs {
if let Some(outputs) = self.find_transaction_outputs(&input.txid) {
if let Some((_, output)) = outputs.iter().find(|(idx, _)| *idx == input.vout) {
input_sum += output.value;
} else {
return Err("UTXO does not exist".to_string());
}
} else {
return Err("Referenced transaction does not exist".to_string());
}
}
let output_sum: u64 = transaction.outputs.iter().map(|o| o.value).sum();
if input_sum < output_sum {
return Err("Insufficient balance, transaction is invalid".to_string());
}
// 3. Record pending UTXOs (prevent the same UTXO from being double-spent
// by two pending transactions)
for input in &transaction.inputs {
self.pending_spent.insert(format!("{}:{}", input.txid, input.vout));
}
}
Fees and Fee Rate
Fee Calculation
fee = total inputs - total outputs
Example: spend a UTXO (100 satoshi), transfer 85, change 5, fee = 100 - 85 - 5 = 10 satoshi
Fee Rate
#![allow(unused)]
fn main() {
pub fn fee_rate(&self) -> f64 {
let size = self.size(); // Serialized transaction size in bytes
if size == 0 { return 0.0; }
self.fee as f64 / size as f64 // Unit: satoshi/byte
}
}
Typical fee rate reference (real Bitcoin — varies with network congestion):
| Priority | Fee Rate | Confirmation Time |
|---|---|---|
| Low | 1–5 sat/byte | Several hours or longer |
| Medium | 5–20 sat/byte | 30–60 minutes |
| High | 20–50 sat/byte | 10–20 minutes |
| Urgent | 50+ sat/byte | Next block (~10 minutes) |
SimpleBTC’s mempool sorts transactions by fee rate, and higher fee-rate transactions are packaged first:
#![allow(unused)]
fn main() {
// Retrieve top transactions sorted by fee rate (Mempool internal logic)
let pending_txs = self.mempool.get_top_transactions(usize::MAX);
}
Transaction Lifecycle
User initiates a transfer request
│
▼
blockchain.create_transaction(&wallet, to, amount, fee)
→ Select UTXOs, generate signatures, construct Transaction
│
▼
blockchain.add_transaction(tx)
→ Verify signatures (ECDSA)
→ Verify UTXO exists + balance sufficient
→ Add to Mempool (sorted by fee rate)
→ Mark spent UTXOs (pending_spent)
│
▼ Waiting to be packaged by a miner
│
▼
blockchain.mine_pending_transactions(miner_address)
→ Retrieve high-priority transactions from Mempool
→ Create Coinbase transaction (reward + fees)
→ Parallel PoW mining
→ Update UTXO set (atomic operation)
→ Block appended to chain, pending_spent cleared
│
▼
Transaction receives 1 confirmation
(each additional subsequent block = +1 confirmation)
Transaction Hash Calculation
#![allow(unused)]
fn main() {
pub fn calculate_hash(&self) -> String {
// Serialize the transaction to JSON and compute SHA-256
let tx_data = serde_json::to_string(&self).unwrap_or_default();
let mut hasher = Sha256::new();
hasher.update(tx_data.as_bytes());
format!("{:x}", hasher.finalize())
}
}
Real Bitcoin uses double SHA-256 (SHA256d) with a compact binary serialization format. Here, for educational simplicity, JSON serialization and a single SHA-256 are used instead.
Output Sum Query
#![allow(unused)]
fn main() {
// Get the sum of all output amounts
let output_sum = tx.output_sum();
// Transaction size (in bytes, affects fee calculation)
let size = tx.size();
// Whether this is a Coinbase transaction
let is_cb = tx.is_coinbase();
}
Complete Transaction Example
use bitcoin_simulation::{blockchain::Blockchain, wallet::Wallet};
fn main() -> Result<(), String> {
let mut blockchain = Blockchain::new();
let genesis = Blockchain::genesis_wallet();
let alice = Wallet::new();
let bob = Wallet::new();
// First transaction: genesis → alice, transfer 10000 satoshi
let tx1 = blockchain.create_transaction(&genesis, alice.address.clone(), 10000, 50)?;
println!("tx1 id: {}", tx1.id);
println!("tx1 output count: {}", tx1.outputs.len()); // 2 (transfer + change)
println!("tx1 fee rate: {:.2} sat/byte", tx1.fee_rate());
blockchain.add_transaction(tx1)?;
// Mine to confirm (alice receives the mining reward)
blockchain.mine_pending_transactions(alice.address.clone())?;
println!("Alice's balance: {} satoshi", blockchain.get_balance(&alice.address));
// Second transaction: alice → bob, transfer 3000 satoshi
let tx2 = blockchain.create_transaction(&alice, bob.address.clone(), 3000, 100)?;
blockchain.add_transaction(tx2)?;
blockchain.mine_pending_transactions(bob.address.clone())?;
println!("Alice's balance: {} satoshi", blockchain.get_balance(&alice.address));
println!("Bob's balance: {} satoshi", blockchain.get_balance(&bob.address));
// Verify chain integrity
assert!(blockchain.is_valid());
Ok(())
}
Blockchain Operations
The blockchain is the core data structure of SimpleBTC — a cryptographically linked sequence of blocks, where each block contains a batch of validated transactions. This chapter covers the Block struct, the creation and management of Blockchain, the proof-of-work mining mechanism, and the chain validation and query interfaces.
Block Structure
Block Data Structure
#![allow(unused)]
fn main() {
// src/block.rs
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Block {
pub index: u32, // Block height (genesis block is 0)
pub timestamp: u64, // Unix timestamp (seconds)
pub transactions: Vec<Transaction>, // Transaction list (first must be Coinbase)
pub previous_hash: String, // Parent block hash (64-character SHA-256 hex)
pub hash: String, // This block's hash (valid value found by mining)
pub nonce: u64, // Proof-of-work counter
pub merkle_root: String, // Merkle tree root hash of all transaction IDs
}
}
Block header field details:
| Field | Bitcoin Equivalent | Description |
|---|---|---|
index | Block Height | Block height; genesis block is 0, incremented by 1 for each new block |
timestamp | nTime | Block creation time (Unix timestamp) |
previous_hash | hashPrevBlock | Parent block hash, forming the chain structure |
merkle_root | hashMerkleRoot | Merkle tree root of all transactions; a fingerprint of the block’s contents |
nonce | nNonce | A random number continuously adjusted during mining |
hash | Block Hash | SHA-256 hash of all block header fields |
Chain Structure Diagram
Genesis Block (index=0) Block 1 Block 2
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ prev: "0" │◄────│ prev: abc...hash │◄─│ prev: def...hash │
│ hash: abc... │ │ hash: def... │ │ hash: ghi... │
│ nonce: 38291 │ │ nonce: 72481 │ │ nonce: 19374 │
│ merkle: xyz... │ │ merkle: pqr... │ │ merkle: stu... │
│ [Coinbase TX] │ │ [Coinbase TX] │ │ [Coinbase TX] │
│ │ │ [TX_1] │ │ [TX_3] │
│ │ │ [TX_2] │ │ [TX_4] │
└──────────────────┘ └──────────────────┘ └──────────────────┘
Why does the chain structure guarantee immutability?
- Modify any transaction in block 1 →
merkle_rootchanges merkle_rootchanges → block 1’shashis completely different- Block 2 recorded block 1’s old
hash→ block 2’sprevious_hashno longer matches - Fixing block 2 requires re-mining (recalculating PoW), and the same applies to blocks 3, 4…
- An attacker would need to control more than 51% of the total network hashrate to catch up with the honest chain
Block Hash Calculation
The block hash is computed from the key fields of the block header (note: transactions are not hashed directly; the Merkle root is used instead):
#![allow(unused)]
fn main() {
// src/block.rs
pub fn calculate_hash(&self) -> String {
use sha2::{Digest, Sha256};
// Concatenate block header fields into a string
let data = format!(
"{}{}{}{}{}",
self.index,
self.timestamp,
self.merkle_root, // ← represents all transaction content
self.previous_hash,
self.nonce // ← this value changes continuously during mining
);
let mut hasher = Sha256::new();
hasher.update(data.as_bytes());
format!("{:x}", hasher.finalize())
}
}
The role of the Merkle root:
- Any change to a single transaction will cause the Merkle root to change completely
- Verifying whether a transaction is included in a block requires only O(log n) hashes (a Merkle proof), rather than downloading all transactions
Creating a Blockchain
Blockchain Struct
#![allow(unused)]
fn main() {
// src/blockchain.rs
pub struct Blockchain {
pub chain: Vec<Block>, // Block list (the chain)
pub difficulty: usize, // Mining difficulty (number of leading zeros, default 3)
pub mempool: Mempool, // Memory pool (pending transactions, sorted by fee rate)
pub utxo_set: UTXOSet, // UTXO set (all unspent outputs)
pub mining_reward: u64, // Mining reward (default 50 satoshi)
pub indexer: TransactionIndexer, // Transaction indexer (address → transaction, for fast queries)
miner: ParallelMiner, // Parallel PoW miner (private)
pending_spent: HashSet<String>, // UTXOs spent by pending transactions (double-spend prevention)
}
}
Initializing the Blockchain
#![allow(unused)]
fn main() {
use bitcoin_simulation::blockchain::Blockchain;
// Create a blockchain (automatically includes the genesis block)
let mut blockchain = Blockchain::new();
println!("Chain length: {}", blockchain.chain.len()); // 1 (genesis block only)
println!("Mining difficulty: {}", blockchain.difficulty); // 3
println!("Mining reward: {} satoshi", blockchain.mining_reward); // 50
}
Internal flow of Blockchain::new():
#![allow(unused)]
fn main() {
pub fn new() -> Blockchain {
let mempool = Mempool::new_permissive();
let mut blockchain = Blockchain {
chain: vec![],
difficulty: 3,
mempool,
utxo_set: UTXOSet::new(),
mining_reward: 50,
indexer: TransactionIndexer::new(),
miner: ParallelMiner::default(),
pending_spent: HashSet::new(),
};
// Create and add the genesis block
let genesis_block = blockchain.create_genesis_block();
blockchain.indexer.index_block(&genesis_block);
blockchain.chain.push(genesis_block);
blockchain
}
}
Genesis Block
The genesis block is the first block in the blockchain (index = 0). What makes it special:
previous_hash="0"(does not reference any parent block)- Contains a Coinbase transaction that issues 10,000,000 satoshi of initial funds to the genesis wallet
- Uses a deterministic genesis wallet (fixed private key
0x01), ensuring the address is the same every time the system starts
#![allow(unused)]
fn main() {
// src/blockchain.rs
fn create_genesis_block(&mut self) -> Block {
let timestamp = /* current Unix time */;
// Deterministic genesis wallet (fixed private key, can be spent with a signature)
let genesis_wallet = Wallet::genesis();
let coinbase_tx = Transaction::new_coinbase(
genesis_wallet.address,
10_000_000, // Genesis block reward: 10M satoshi
timestamp,
0, // No fee
);
// Add the genesis UTXO to the UTXO set
self.utxo_set.add_transaction(&coinbase_tx);
// The genesis block's previous_hash is fixed as "0"
Block::new(0, vec![coinbase_tx], "0".to_string())
}
}
Two equivalent ways to obtain the genesis wallet:
#![allow(unused)]
fn main() {
let genesis = Blockchain::genesis_wallet(); // Static method of Blockchain
let genesis2 = Wallet::genesis(); // Obtained directly from the wallet module
assert_eq!(genesis.address, genesis2.address);
}
Proof of Work (PoW) Mining
Principle
Proof of work requires miners to find a nonce value such that the block hash satisfies the condition “the first N digits are 0”:
difficulty = 3, target hash format: 000xxxxxxxxx...
Since the output of SHA-256 is completely unpredictable, miners can only brute-force the nonce:
nonce=0: hash = "a7f3b2..." → not satisfied (does not start with "000")
nonce=1: hash = "2c91d4..." → not satisfied
...
nonce=38291: hash = "000a4b7c9..." → satisfied! Block mined
On average, 16³ = 4096 attempts are needed (difficulty 3). Real Bitcoin’s difficulty is equivalent to about 20 leading zeros, requiring approximately 2⁸⁰ attempts.
Block::mine_block() (single-threaded)
#![allow(unused)]
fn main() {
// src/block.rs
pub fn mine_block(&mut self, difficulty: usize) {
let target = "0".repeat(difficulty);
while self.hash[..difficulty] != target {
self.nonce += 1;
self.hash = self.calculate_hash();
}
println!("✓ Block mined: {}", self.hash);
}
}
ParallelMiner (multi-threaded)
Blockchain::mine_pending_transactions() uses ParallelMiner instead of the single-threaded mine_block(), making full use of multi-core CPUs:
#![allow(unused)]
fn main() {
// src/blockchain.rs excerpt
self.miner
.mine_block(&mut block, self.difficulty)
.map_err(|e| format!("Mining failed: {}", e))?;
}
ParallelMiner splits the nonce space among multiple threads to search in parallel; the first thread to find a valid hash wins.
Difficulty and Adjustment
| Difficulty | Leading Zeros | Average Attempts | Use Case |
|---|---|---|---|
| 1 | 1 zero | 16 | Very fast testing |
| 2 | 2 zeros | 256 | Quick demo |
| 3 | 3 zeros | 4,096 | Default config |
| 4 | 4 zeros | 65,536 | Performance testing |
| 6 | 6 zeros | 16,777,216 | Close to real-world |
Adding Transactions and Mining
Complete Flow
#![allow(unused)]
fn main() {
use bitcoin_simulation::{blockchain::Blockchain, wallet::Wallet};
let mut blockchain = Blockchain::new();
let genesis = Blockchain::genesis_wallet();
let alice = Wallet::new();
// 1. Create a transaction
let tx = blockchain.create_transaction(
&genesis,
alice.address.clone(),
5000, // Transfer 5000 satoshi
50, // Fee 50 satoshi
)?;
// 2. Add to mempool (verify signature + UTXO)
blockchain.add_transaction(tx)?;
println!("Mempool transaction count: {}", blockchain.mempool.len()); // 1
// 3. Mine (alice receives the reward as miner)
blockchain.mine_pending_transactions(alice.address.clone())?;
println!("Chain length: {}", blockchain.chain.len()); // 2 (genesis + new block)
println!("Mempool transaction count: {}", blockchain.mempool.len()); // 0 (cleared)
}
Detailed flow of mine_pending_transactions()
#![allow(unused)]
fn main() {
pub fn mine_pending_transactions(&mut self, miner_address: String) -> Result<(), String> {
if self.mempool.is_empty() {
return Err("No pending transactions".to_string());
}
// 1. Fetch high-fee-rate transactions from the mempool (already sorted)
let pending_txs = self.mempool.get_top_transactions(usize::MAX);
// 2. Calculate total fees
let total_fees: u64 = pending_txs.iter().map(|tx| tx.fee).sum();
// 3. Create Coinbase transaction (miner reward = block reward + total fees)
let coinbase_tx = Transaction::new_coinbase(
miner_address,
self.mining_reward, // 50 satoshi
timestamp,
total_fees,
);
// 4. Assemble the block (Coinbase must be the first transaction)
let mut transactions = vec![coinbase_tx];
transactions.extend(pending_txs.iter().cloned());
let previous_hash = self.chain.last().unwrap().hash.clone();
let mut block = Block::new(self.chain.len() as u32, transactions, previous_hash);
// 5. Parallel PoW mining
self.miner.mine_block(&mut block, self.difficulty)?;
// 6. Verify all transaction signatures in the block
if !block.validate_transactions() {
return Err("Block contains invalid transactions".to_string());
}
// 7. Update UTXO set (consume input UTXOs, create output UTXOs)
for tx in &block.transactions {
if !self.utxo_set.process_transaction(tx) {
return Err("UTXO update failed".to_string());
}
}
// 8. Add block to chain + build index
self.indexer.index_block(&block);
self.chain.push(block);
// 9. Clear mempool and pending_spent
for tx in &pending_txs {
let _ = self.mempool.remove_transaction(&tx.id);
}
self.pending_spent.clear();
Ok(())
}
}
Merkle Tree and Transaction Verification
Block::new() automatically builds the Merkle tree and calculates the Merkle root upon creation:
#![allow(unused)]
fn main() {
pub fn new(index: u32, transactions: Vec<Transaction>, previous_hash: String) -> Block {
// Collect all transaction IDs
let tx_ids: Vec<String> = transactions.iter().map(|tx| tx.id.clone()).collect();
// Build Merkle tree, calculate root hash
let merkle_tree = MerkleTree::new(&tx_ids);
let merkle_root = merkle_tree.get_root_hash();
let mut block = Block {
index, timestamp, transactions, previous_hash,
hash: String::new(), nonce: 0, merkle_root,
};
block.hash = block.calculate_hash();
block
}
}
Verifying whether a transaction is included in a block (SPV use case):
#![allow(unused)]
fn main() {
// src/block.rs
pub fn verify_transaction_inclusion(&self, tx_id: &str, index: usize) -> bool {
let tx_ids: Vec<String> = self.transactions.iter().map(|tx| tx.id.clone()).collect();
let merkle_tree = MerkleTree::new(&tx_ids);
if let Some(proof) = merkle_tree.get_proof(tx_id) {
MerkleTree::verify_proof(tx_id, &proof, &self.merkle_root, index)
} else {
false
}
}
}
Chain Validation
Blockchain::is_valid() validates the integrity of the chain block by block, starting from block 1 (skipping the genesis block):
#![allow(unused)]
fn main() {
pub fn is_valid(&self) -> bool {
for i in 1..self.chain.len() {
let current = &self.chain[i];
let previous = &self.chain[i - 1];
// 1. Verify the block's own hash (prevent silent data tampering)
if current.hash != current.calculate_hash() {
println!("Block {} has invalid hash", i);
return false;
}
// 2. Verify forward reference (chain link integrity)
if current.previous_hash != previous.hash {
println!("Block {} has invalid forward reference", i);
return false;
}
// 3. Verify proof of work (hash leading zeros satisfy difficulty requirement)
let target = "0".repeat(self.difficulty);
if current.hash[..self.difficulty] != target {
println!("Block {} has invalid proof of work", i);
return false;
}
// 4. Verify ECDSA signatures of all transactions in the block
if !current.validate_transactions() {
println!("Block {} contains invalid transactions", i);
return false;
}
}
true
}
}
Validation example:
#![allow(unused)]
fn main() {
let mut blockchain = Blockchain::new();
// ... add transactions, mine ...
// Normal case: should pass
assert!(blockchain.is_valid());
// Simulate tampering (for educational purposes; Rust's borrow rules constrain direct access in practice)
// If someone modifies a transaction in a historical block, is_valid() will return false
}
Balance Query
Balances are calculated via the UTXO set, avoiding a scan of all historical blocks:
#![allow(unused)]
fn main() {
pub fn get_balance(&self, address: &str) -> u64 {
self.utxo_set.get_balance(address)
}
}
Usage example:
#![allow(unused)]
fn main() {
let balance = blockchain.get_balance(&alice.address);
println!("Alice balance: {} satoshi", balance);
println!("Alice balance: {:.8} BTC", balance as f64 / 1e8);
}
Performance advantage of UTXOSet:
Without a UTXO set, querying a balance requires scanning all transactions in all blocks (O(n), where n = total number of transactions). The UTXO set caches all current unspent outputs in memory, making a query an O(1) hash table lookup.
Printing Blockchain Information
Blockchain::print_chain() provides formatted debug output:
#![allow(unused)]
fn main() {
blockchain.print_chain();
}
Sample output:
========== Blockchain Info ==========
--- Block #0 ---
Timestamp: 1711497600
Hash: 000a4b7c9d2e1f3a...
Previous hash: 0
Nonce: 38291
Transaction count: 1
Transaction #0: f3a1b2c4...
Type: Coinbase (mining reward)
Input count: 1
Output count: 1
Output 0: 10000000 -> 1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2
--- Block #1 ---
Timestamp: 1711497615
Hash: 000d2f8a1b9e4c7f...
Previous hash: 000a4b7c9d2e1f3a...
Nonce: 72481
Transaction count: 2
Transaction #0: a1b2c3d4...
Type: Coinbase (mining reward)
...
Transaction #1: e5f6a7b8...
Fee: 50 satoshi
Fee rate: 0.23 sat/byte
Input count: 1
Output count: 2
Output 0: 5000 -> 1AliceAddress...
Output 1: 4994950 -> 1GenesisAddress...
================================
Complete Operation Example
use bitcoin_simulation::{blockchain::Blockchain, wallet::Wallet};
fn main() -> Result<(), String> {
// Initialize
let mut blockchain = Blockchain::new();
let genesis = Blockchain::genesis_wallet();
let alice = Wallet::new();
let bob = Wallet::new();
let miner = Wallet::new();
// Round 1: genesis → alice
let tx1 = blockchain.create_transaction(&genesis, alice.address.clone(), 100_000, 100)?;
blockchain.add_transaction(tx1)?;
blockchain.mine_pending_transactions(miner.address.clone())?;
println!("Block 1 mined");
println!("Alice balance: {} sat", blockchain.get_balance(&alice.address));
println!("Miner balance: {} sat", blockchain.get_balance(&miner.address));
// Round 2: alice → bob (two transactions in the same block)
let tx2 = blockchain.create_transaction(&alice, bob.address.clone(), 30_000, 200)?;
let tx3 = blockchain.create_transaction(&alice, miner.address.clone(), 20_000, 150)?;
blockchain.add_transaction(tx2)?;
blockchain.add_transaction(tx3)?;
blockchain.mine_pending_transactions(miner.address.clone())?;
println!("\nBlock 2 mined (contains 2 transactions)");
println!("Chain length: {}", blockchain.chain.len()); // 3
println!("Alice balance: {} sat", blockchain.get_balance(&alice.address));
println!("Bob balance: {} sat", blockchain.get_balance(&bob.address));
println!("Miner balance: {} sat", blockchain.get_balance(&miner.address));
// Verify chain integrity
assert!(blockchain.is_valid(), "Chain validation should pass");
println!("\nChain validation passed!");
// Print full chain information
blockchain.print_chain();
Ok(())
}
Key Parameter Reference
| Parameter | Default | Description |
|---|---|---|
difficulty | 3 | Mining difficulty (number of leading zeros) |
mining_reward | 50 | Base block reward (satoshi) |
| Genesis block reward | 10,000,000 | Genesis Coinbase amount (satoshi) |
| Real Bitcoin initial reward | 5,000,000,000 | 50 BTC (in satoshi) |
| Real Bitcoin halving interval | 210,000 blocks | Approximately every 4 years |
| Real Bitcoin target block time | 10 minutes | Difficulty adjusted approximately every 2016 blocks |
UTXO Management
UTXO (Unspent Transaction Output) is the core of Bitcoin’s ledger model. Understanding UTXOs is a key step toward mastering how Bitcoin works. This chapter introduces the design and usage of UTXOSet in SimpleBTC.
What is a UTXO?
In the traditional banking system (and in Ethereum’s account model), the system directly records the balance of each account. Bitcoin chose a fundamentally different approach: instead of storing balances, it only stores “unspent outputs”.
Every Bitcoin transaction:
- Consumes some previously existing UTXOs (as inputs)
- Creates some new UTXOs (as outputs)
Think of Bitcoin like physical cash: you have a 100-unit bill and a 50-unit bill. You want to buy something worth 80 units, so you hand over the entire 100-unit bill and receive 20 units in change. You “spent” the 100-unit UTXO and “created” two new UTXOs — one worth 80 units for the merchant and one worth 20 units as change for yourself.
UTXO Lifecycle
Creation Existence Spending Destruction
| | | |
v v v v
Tx output ──────► UTXO set ──────────────► Referenced by new tx ──────► Removed from set
(packed into block) (queryable and spendable) (as input) (cannot be reused)
UTXO Model vs. Account Model
| Feature | UTXO Model (Bitcoin) | Account Model (Ethereum) |
|---|---|---|
| State storage | Records all unspent outputs | Records the balance of each account |
| Balance calculation | Sum all UTXOs belonging to the address | Read the account field directly |
| Privacy | Better (each transaction can use a new address) | Weaker (fixed address) |
| Parallel processing | Naturally supported (different UTXOs are independent) | Requires extra concurrency control |
| Double-spend prevention | A UTXO can only be spent once | Controlled via nonce sequence numbers |
| Complex contracts | Harder to implement | Natively supported |
| Intuitiveness | Requires understanding the UTXO concept | Similar to a bank account; intuitive |
The source code comments (src/utxo.rs, lines 8–19) provide a concise summary:
#![allow(unused)]
fn main() {
// Account model (Ethereum, etc.):
// - Records the balance of each account
// - Transfer: account A -100, account B +100
// - Simple and intuitive, but hard to process in parallel
//
// UTXO model (Bitcoin):
// - No concept of account balance
// - Only records unspent transaction outputs
// - Transfer: spend A's UTXO, create a new UTXO for B
// - Better privacy and parallelism
}
UTXOSet Data Structure
SimpleBTC uses UTXOSet to manage the set of all unspent outputs across the entire blockchain.
#![allow(unused)]
fn main() {
#[derive(Debug, Clone)]
pub struct UTXOSet {
// key: txid (transaction ID)
// value: list of all unspent outputs for that transaction [(output index, output details)]
utxos: HashMap<String, Vec<(usize, TxOutput)>>,
}
}
- Key: Transaction ID (txid), a hexadecimal hash string
- Value: The list of outputs under that transaction that have not yet been spent; each item contains the output’s index within the transaction (
vout) and the output’s detailed information (TxOutput)
This data structure makes it very efficient (O(1) hash lookup) to find all available outputs for a given txid, and also makes it easy to remove a specific individual output.
Core API Details
Creating a UTXO Set
#![allow(unused)]
fn main() {
let mut utxo_set = UTXOSet::new();
}
Initializes an empty UTXO set. When the blockchain starts, it processes all transactions block by block from the genesis block to populate it.
Adding Transaction Outputs: add_transaction
#![allow(unused)]
fn main() {
pub fn add_transaction(&mut self, tx: &Transaction)
}
When a new transaction is packed into a block and confirmed, this method is called to add all outputs of that transaction to the UTXO set.
#![allow(unused)]
fn main() {
// Example: a genesis block is mined, and the coinbase reward enters the UTXO set
let coinbase_tx = Transaction::new_coinbase("miner_address".to_string(), 50, 0, 0);
utxo_set.add_transaction(&coinbase_tx);
// Now coinbase_tx.id -> [(0, TxOutput { value: 50, ... })] is in the set
}
Note:
add_transactiononly adds outputs; it does not process inputs (it does not remove spent UTXOs). For complete transaction processing, useprocess_transaction.
Removing Spent Outputs: remove_utxo
#![allow(unused)]
fn main() {
pub fn remove_utxo(&mut self, txid: &str, vout: usize)
}
When a UTXO is referenced by an input of a transaction (i.e., it is spent), it must be removed from the set. This is the core mechanism for preventing double spending.
#![allow(unused)]
fn main() {
// The user spent output #0 of txid="abc123"
utxo_set.remove_utxo("abc123", 0);
// Attempting to spend the same UTXO again will fail validation because it is no longer in the set
}
In the implementation, remove_utxo uses retain to keep other unaffected outputs; if all outputs of a transaction have been spent, the entire txid entry is also deleted:
#![allow(unused)]
fn main() {
pub fn remove_utxo(&mut self, txid: &str, vout: usize) {
if let Some(outputs) = self.utxos.get_mut(txid) {
outputs.retain(|(index, _)| *index != vout);
if outputs.is_empty() {
self.utxos.remove(txid);
}
}
}
}
Querying All UTXOs for an Address: find_utxos
#![allow(unused)]
fn main() {
pub fn find_utxos(&self, address: &str) -> Vec<(String, usize, u64)>
}
Iterates over the entire UTXO set and returns all unspent outputs belonging to the specified address. The result format is (txid, vout, value).
#![allow(unused)]
fn main() {
let utxos = utxo_set.find_utxos("alice_address");
for (txid, vout, value) in &utxos {
println!("UTXO: {}:{} = {} satoshis", txid, vout, value);
}
}
Finding Spendable Outputs: find_spendable_outputs
#![allow(unused)]
fn main() {
pub fn find_spendable_outputs(
&self,
address: &str,
amount: u64,
) -> Option<(u64, Vec<(String, usize)>)>
}
This is the most important API when creating a new transaction. It uses a greedy coin selection algorithm, accumulating UTXOs from the address one by one until the total meets amount.
#![allow(unused)]
fn main() {
// Need to pay 30 satoshis (including fees)
match utxo_set.find_spendable_outputs("alice", 30) {
Some((accumulated, inputs)) => {
// accumulated: the actual total selected (may be > 30; the difference is the change)
// inputs: list of selected UTXOs, each item is (txid, vout)
let change = accumulated - 30;
println!("Selected {} UTXOs, change: {} satoshis", inputs.len(), change);
}
None => {
println!("Insufficient balance");
}
}
}
Change mechanism: If accumulated > amount, the difference must be returned to the sender as a change output. For example, to pay 3 BTC, you select a 5 BTC UTXO, and you need to create a 2 BTC change output (with the fee deducted from it).
UTXO selection strategy comparison:
| Strategy | Description | This Implementation |
|---|---|---|
| Greedy algorithm | Accumulate sequentially until the amount is met | Used here |
| Best match | Combination closest to the target amount | Reduces change |
| Smallest UTXO first | Prefer small-denomination UTXOs | Reduces fragmentation |
| Largest UTXO first | Prefer large-denomination UTXOs | Reduces number of inputs |
Excluding Already-Pending UTXOs: find_spendable_outputs_excluding
#![allow(unused)]
fn main() {
pub fn find_spendable_outputs_excluding(
&self,
address: &str,
amount: u64,
excluded: &HashSet<String>,
) -> Option<(u64, Vec<(String, usize)>)>
}
This is an extended version of find_spendable_outputs. When the same wallet initiates multiple transactions in quick succession, the UTXOs already selected by prior transactions have not yet been confirmed (they are still in the mempool) and cannot be reused. By passing an excluded set (formatted as "txid:vout" strings), these UTXOs already occupied by pending transactions can be skipped.
#![allow(unused)]
fn main() {
// The first transaction used "abc:0"
let mut pending_spent: HashSet<String> = HashSet::new();
pending_spent.insert("abc:0".to_string());
// The second transaction automatically skips "abc:0"
let result = utxo_set.find_spendable_outputs_excluding("alice", 20, &pending_spent);
}
Querying Balance: get_balance
#![allow(unused)]
fn main() {
pub fn get_balance(&self, address: &str) -> u64
}
A Bitcoin “balance” is a computed value, not a stored one. This method internally calls find_utxos and sums up the amounts of all UTXOs belonging to the address.
#![allow(unused)]
fn main() {
let balance = utxo_set.get_balance("alice");
println!("Alice's balance: {} satoshis", balance);
}
Key insight: Querying a balance requires scanning the entire UTXO set (time complexity O(n)). Actual Bitcoin nodes use address indexes to optimize this operation.
Complete Transaction Processing: process_transaction
#![allow(unused)]
fn main() {
pub fn process_transaction(&mut self, tx: &Transaction) -> bool
}
This is the core function for updating UTXO state, and it executes the following steps in order:
- Calls
tx.verify()to verify the transaction signature - If it is not a coinbase transaction, removes all UTXOs referenced by the inputs
- Adds all of the transaction’s outputs to the UTXO set
#![allow(unused)]
fn main() {
// Process a regular transaction
let success = utxo_set.process_transaction(&transfer_tx);
if !success {
eprintln!("Transaction verification failed; UTXO set was not modified");
}
}
This function has atomic semantics — if verification fails, the UTXO set is not modified, ensuring state consistency.
Double-Spend Prevention Mechanism
Double spending is the core security problem that blockchain must solve. The UTXO model naturally defends against double spending:
Attack flow:
1. Attacker has a UTXO worth 10 BTC (txid="xyz", vout=0)
2. Creates transaction A: spend xyz:0, pay merchant 10 BTC
3. Merchant accepts; transaction A enters the mempool
4. Attacker creates transaction B: also spend xyz:0, pay themselves 10 BTC
5. Attempts to broadcast transaction B
Defense result:
- After transaction A is confirmed, xyz:0 is deleted from the UTXO set
- When transaction B is validated, xyz:0 is not found and is rejected by the node
- Even if transaction A is unconfirmed, the mempool's double-spend detection rejects transaction B at step 3
In SimpleBTC’s Mempool, utxo_index (HashMap<"txid:vout", spending_txid>) records which UTXOs have already been claimed by transactions in the mempool, enabling double-spend detection and rejection of transaction B at step 3.
Complete Usage Example
use bitcoin_simulation::utxo::UTXOSet;
use bitcoin_simulation::transaction::Transaction;
fn main() {
let mut utxo_set = UTXOSet::new();
// Step 1: Mine a block, create a coinbase transaction (bitcoin created from scratch)
let coinbase = Transaction::new_coinbase("alice".to_string(), 50, 0, 0);
utxo_set.process_transaction(&coinbase);
// Step 2: Query Alice's balance
let alice_balance = utxo_set.get_balance("alice");
println!("Alice balance: {} satoshis", alice_balance); // Output: 50
// Step 3: Alice transfers 20 satoshis to Bob (fee: 2 satoshis)
let needed = 22; // 20 for Bob + 2 fee
if let Some((accumulated, inputs)) = utxo_set.find_spendable_outputs("alice", needed) {
let change = accumulated - needed;
println!("Selected {} UTXOs, total: {}, change: {}", inputs.len(), accumulated, change);
// Build and broadcast the transaction (signature details omitted here)
// let tx = build_transaction(inputs, "bob", 20, "alice", change, 2);
// utxo_set.process_transaction(&tx);
}
// Step 4: Query Bob's balance
// println!("Bob balance: {}", utxo_set.get_balance("bob"));
}
Summary
The UTXO model is the cornerstone of Bitcoin’s architecture. UTXOSet ensures ledger security through the following mechanisms:
process_transaction: Atomically updates UTXO state (removes inputs first, then adds outputs)remove_utxo: Ensures each UTXO can only be spent once, preventing double spendingfind_spendable_outputs_excluding: Resolves UTXO conflicts for consecutive transactions by tracking viapending_spentget_balance: Balance is a computed value — the sum of all UTXOs belonging to that address
The next chapter will cover the specific structure of transactions and the signature verification mechanism.
Merkle Tree and SPV Verification
Merkle trees (hash trees) and Simplified Payment Verification (SPV) are the technical foundation for Bitcoin’s lightweight clients. They allow a mobile wallet to securely verify transactions with only a few MB of storage, without needing to download the full blockchain of over 500 GB.
What is a Merkle Tree?
A Merkle tree is a binary hash tree invented by computer scientist Ralph Merkle in 1979. Its core idea is: by recursively hashing data, any quantity of data can be compressed into a fixed-length “fingerprint” (the root hash).
In Bitcoin, all the transactions included in each block are organized into a Merkle tree. The tree’s root hash (Merkle Root) is stored in the block header, protected by proof of work (PoW). Any tampering with the transaction data will cause the root hash to change, invalidating that block and all subsequent blocks.
Tree Structure Diagram (4 transactions)
┌─────────────┐
│ Root Hash │
│ hash(H12+H34)│
└──────┬──────┘
┌───────┴───────┐
┌──────┴──────┐ ┌─────┴──────┐
│ H12 │ │ H34 │
│ hash(H1+H2) │ │ hash(H3+H4)│
└──────┬──────┘ └─────┬──────┘
┌────────┴───┐ ┌─────┴───┐
┌──┴──┐ ┌───┴──┐ ┌───┴──┐ ┌──┴───┐
│ H1 │ │ H2 │ │ H3 │ │ H4 │
│hash │ │ hash │ │ hash │ │ hash │
│(tx1)│ │(tx2) │ │(tx3) │ │(tx4) │
└──┬──┘ └──┬───┘ └──┬───┘ └──┬───┘
│ │ │ │
tx1 tx2 tx3 tx4
(Transaction 1) (Transaction 2) (Transaction 3) (Transaction 4)
Construction Process
Construction follows a bottom-up approach in two phases:
Phase 1: Build the Leaf Layer
The raw data of each transaction is SHA-256 hashed to become a leaf node:
H1 = SHA256(tx1_data)
H2 = SHA256(tx2_data)
H3 = SHA256(tx3_data)
H4 = SHA256(tx4_data)
Odd number handling: If the number of transactions is odd, the last transaction is duplicated to make the layer count even. This is the standard practice specified by the Bitcoin protocol.
Phase 2: Merge Layer by Layer to the Root
Each pair of adjacent nodes’ hashes is concatenated and then hashed to produce the parent node:
H12 = SHA256(H1 + H2)
H34 = SHA256(H3 + H4)
Root = SHA256(H12 + H34)
This process is repeated until only one node remains, which is the Merkle root.
Implementation in SimpleBTC
Node Structure: MerkleNode
#![allow(unused)]
fn main() {
#[derive(Debug, Clone)]
pub struct MerkleNode {
pub hash: String, // The node's hash value
pub left: Option<Box<MerkleNode>>, // Left child node (only for internal nodes)
pub right: Option<Box<MerkleNode>>, // Right child node (only for internal nodes)
}
}
- Leaf nodes: Both
leftandrightareNone;hashis the SHA-256 value of the transaction data - Internal nodes: Have left and right child nodes;
hashisSHA256(left.hash + right.hash) - Root node: The top node of the tree; the final Merkle Root
Two factory methods for creating nodes:
#![allow(unused)]
fn main() {
// Leaf node: hash the raw data directly
let leaf = MerkleNode::new_leaf("tx_data_string");
// Internal node: merge two child nodes
let parent = MerkleNode::new_internal(left_node, right_node);
}
Tree Structure: MerkleTree
#![allow(unused)]
fn main() {
#[derive(Debug, Clone)]
pub struct MerkleTree {
pub root: Option<MerkleNode>, // Tree root node
pub leaves: Vec<String>, // List of original transaction hashes
}
}
Building the Merkle Tree: MerkleTree::new
#![allow(unused)]
fn main() {
pub fn new(transactions: &[String]) -> Self
}
Accepts a list of transaction IDs (strings) and automatically builds the complete Merkle tree:
#![allow(unused)]
fn main() {
use bitcoin_simulation::merkle::MerkleTree;
let txs = vec![
"tx1_hash".to_string(),
"tx2_hash".to_string(),
"tx3_hash".to_string(),
"tx4_hash".to_string(),
];
let tree = MerkleTree::new(&txs);
let root = tree.get_root_hash();
println!("Merkle Root: {}", root);
// Output: a 64-character hexadecimal hash string
}
Key steps in the internal implementation:
#![allow(unused)]
fn main() {
// 1. Pad odd count
if !leaves.len().is_multiple_of(2) {
leaves.push(leaves.last().unwrap().clone());
}
// 2. Build the leaf node layer
let mut nodes: Vec<MerkleNode> = leaves.iter()
.map(|tx| MerkleNode::new_leaf(tx))
.collect();
// 3. Merge layer by layer from bottom to top
while nodes.len() > 1 {
let mut next_level = Vec::new();
for i in (0..nodes.len()).step_by(2) {
let left = nodes[i].clone();
let right = nodes[i + 1].clone(); // Even count already guaranteed
next_level.push(MerkleNode::new_internal(left, right));
}
nodes = next_level;
}
}
Generating a Merkle Proof: get_proof
#![allow(unused)]
fn main() {
pub fn get_proof(&self, tx_hash: &str) -> Option<Vec<String>>
}
Generates a Merkle proof (also called a “Merkle path”) for the specified transaction. This proof contains the hashes of all sibling nodes along the path from that transaction’s leaf node to the root node.
#![allow(unused)]
fn main() {
// Generate a proof for tx1
let proof = tree.get_proof("tx1_hash").unwrap();
// proof = [H2, H34] ← list of sibling hashes needed for verification
}
Diagram: proof needed to verify tx1
┌────────────┐
│ Root │ ← known (stored in block header)
└─────┬──────┘
┌──────────┴──────────┐
┌──────┴──────┐ ┌──────┴──────┐
│ H12 │ │ ★ H34 ★ │ ← proof element[1]
└──────┬──────┘ └─────────────┘
┌───────┴───────┐
┌──┴──┐ ┌────┴──┐
│ H1 │ │★ H2 ★│ ← proof element[0]
└──┬──┘ └───────┘
│
[tx1] ← the transaction to verify (known)
The verifier only needs two hashes [H2, H34] (log₂4 = 2 steps), without needing to know the contents of tx2, tx3, or tx4.
Verifying a Merkle Proof: verify_proof
#![allow(unused)]
fn main() {
pub fn verify_proof(
tx_hash: &str, // Hash of the transaction to verify
proof: &[String], // Merkle proof (list of sibling hashes)
root_hash: &str, // Merkle root from the block header
index: usize, // The transaction's index position in the block
) -> bool
}
This is a static method; verification is possible without holding the complete Merkle tree. SPV clients use exactly this method to verify transactions.
#![allow(unused)]
fn main() {
// Known: tx1 is in the block at index 0; Merkle Root comes from the block header
let is_valid = MerkleTree::verify_proof(
"tx1_hash",
&proof, // [H2, H34]
&root_hash, // from block header, protected by PoW
0, // tx1 is transaction #0
);
println!("Transaction verification result: {}", is_valid); // true
}
Verification algorithm steps (using tx1, index=0 as an example):
Step 1: current_hash = SHA256("tx1_hash") → get H1
index=0 (even), H1 is on the left
combined = H1 + proof[0] (H2)
current_hash = SHA256(H1 + H2) → get H12
index = 0 / 2 = 0
Step 2: index=0 (even), H12 is on the left
combined = H12 + proof[1] (H34)
current_hash = SHA256(H12 + H34) → get the computed Root
Verify: computed Root == merkle_root in the block header?
Source code implementation:
#![allow(unused)]
fn main() {
pub fn verify_proof(tx_hash: &str, proof: &[String], root_hash: &str, index: usize) -> bool {
let mut current_hash = MerkleNode::hash_data(tx_hash);
let mut current_index = index;
for sibling_hash in proof {
let combined = if current_index.is_multiple_of(2) {
// Current node is on the left, sibling is on the right
format!("{}{}", current_hash, sibling_hash)
} else {
// Current node is on the right, sibling is on the left
format!("{}{}", sibling_hash, current_hash)
};
current_hash = MerkleNode::hash_data(&combined);
current_index /= 2;
}
current_hash == root_hash
}
}
SPV Light Clients
SPV Concept
SPV (Simplified Payment Verification) was proposed by Satoshi Nakamoto in Section 8 of the Bitcoin whitepaper. Its core idea is: a light client does not need to verify all transactions; it only needs to trust the longest proof-of-work chain and use Merkle proofs to verify transactions relevant to itself.
| Feature | Full Node | SPV Node |
|---|---|---|
| Storage requirement | 400+ GB (full blockchain) | ~5 MB (block headers only) |
| Bandwidth consumption | Full blocks (1–4 MB/block) | Block headers only (80 bytes/block) |
| Verification scope | All transactions | Only transactions relevant to itself |
| Security level | Highest (fully self-verified) | Relies on PoW; trusts miner honesty |
| Suitable for | Mining pools, exchanges, full nodes | Mobile wallets, embedded devices |
SPV Implementation in SimpleBTC
Block Header Structure: BlockHeader
SPV clients only download and store block headers, not the transaction body:
#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BlockHeader {
pub height: u32, // Block height
pub hash: String, // Block hash
pub previous_hash: String, // Previous block hash (ensures chain structure)
pub merkle_root: String, // Merkle root (32 bytes, used to verify transactions)
pub timestamp: u64, // Timestamp
pub bits: u32, // Difficulty target
pub nonce: u64, // Proof-of-work nonce
}
}
Each block header is a fixed 80 bytes. Bitcoin currently has approximately 830,000 blocks, so the total size of all block headers is about 66 MB — a tremendous saving compared to the full blockchain of 600+ GB.
SPV Client: SPVClient
#![allow(unused)]
fn main() {
pub struct SPVClient {
headers: Vec<BlockHeader>, // Block header chain
header_index: HashMap<String, BlockHeader>, // hash → header for fast lookup
verified_transactions: HashMap<String, (String, bool)>, // txid → (block_hash, verified result)
chain_tip: Option<String>, // Current latest block hash
total_work: u64, // Accumulated work
}
}
SPV Workflow
Step 1: Sync Block Headers
#![allow(unused)]
fn main() {
use bitcoin_simulation::spv::SPVClient;
let mut client = SPVClient::new();
// Fetch blocks from a full node and extract their headers
let blocks = /* fetched from the P2P network */;
client.sync_from_blocks(&blocks).unwrap();
println!("Synced {} block headers", client.get_height());
println!("Storage used: {} bytes", client.estimate_storage_size());
// 1000 block headers require only 80,000 bytes (about 78 KB)
}
Headers can also be added one at a time:
#![allow(unused)]
fn main() {
use bitcoin_simulation::spv::BlockHeader;
let header = BlockHeader {
height: 0,
hash: "genesis_hash".to_string(),
previous_hash: "0000...".to_string(),
merkle_root: "merkle_root_hash".to_string(),
timestamp: 1231006505,
bits: 0x1d00ffff,
nonce: 2083236893,
};
client.add_block_header(header).unwrap();
}
The continuity of the block header chain is automatically verified by add_block_header: the new header’s previous_hash must match the hash of the previous header; otherwise it is rejected:
#![allow(unused)]
fn main() {
// Attempting to add a non-continuous block header returns an error
let bad_header = BlockHeader {
height: 1,
hash: "block_1".to_string(),
previous_hash: "wrong_hash".to_string(), // does not match!
// ...
};
let result = client.add_block_header(bad_header);
assert!(result.is_err()); // rejected
}
Step 2: Verify Transaction Inclusion
When a user receives a payment, they need to verify that this transaction has indeed been packed into a block:
#![allow(unused)]
fn main() {
// Suppose a merchant receives a payment notification: tx_id is at position 0 in block_hash
let tx_id = "payment_tx_hash";
let block_hash = "some_block_hash";
// Request a Merkle proof from a full node (in practice, this is done via P2P protocol)
let proof = vec!["sibling_hash_1".to_string(), "sibling_hash_2".to_string()];
let tx_index = 0; // Position of the transaction in the block
let is_valid = client.verify_transaction(tx_id, &proof, block_hash, tx_index).unwrap();
if is_valid {
println!("Payment confirmed! Transaction {} is in the block", tx_id);
} else {
println!("Verification failed; the transaction may not be in that block");
}
}
Step 3: Check Historical Verification Results
#![allow(unused)]
fn main() {
// Check whether a transaction has already passed SPV verification
if let Some(verified) = client.is_transaction_verified(tx_id) {
if verified {
println!("This transaction has been verified");
}
}
// Get SPV statistics
let stats = client.get_stats();
println!("Block header count: {}", stats.header_count);
println!("Storage size: {} bytes", stats.storage_size);
println!("Verified transaction count: {}", stats.verified_tx_count);
}
Complete Example: Build a Tree and Perform SPV Verification
use bitcoin_simulation::merkle::MerkleTree;
use bitcoin_simulation::spv::{SPVClient, BlockHeader};
fn main() {
// 1. Assume a block contains 4 transactions
let transactions = vec![
"tx1".to_string(),
"tx2".to_string(),
"tx3".to_string(),
"tx4".to_string(),
];
// 2. Build the Merkle tree (what a full node does)
let tree = MerkleTree::new(&transactions);
let merkle_root = tree.get_root_hash();
println!("Merkle Root: {}", merkle_root);
// 3. Generate a proof for tx1 (full node generates it at the SPV client's request)
let proof = tree.get_proof("tx1").unwrap();
println!("Merkle proof for tx1 contains {} hashes", proof.len());
// 4. SPV client verification (knows only the block header and proof, not the other transactions)
let mut spv = SPVClient::new();
let header = BlockHeader {
height: 0,
hash: "block_0".to_string(),
previous_hash: "0".to_string(),
merkle_root: merkle_root.clone(),
timestamp: 1700000000,
bits: 0,
nonce: 42,
};
spv.add_block_header(header).unwrap();
let valid = spv.verify_transaction("tx1", &proof, "block_0", 0).unwrap();
println!("SPV verification result: {}", valid); // true
// 5. Verify directly using the static method (no SPVClient needed)
let valid2 = MerkleTree::verify_proof("tx1", &proof, &merkle_root, 0);
println!("Static verification result: {}", valid2); // true
}
Why is SPV Verification Secure?
An attacker cannot forge a Merkle proof for two reasons:
- SHA-256 collision resistance: Finding two different inputs that produce the same hash is computationally infeasible (requires approximately 2¹²⁸ hash operations).
- PoW protection: The
merkle_rootis stored in the block header, which is protected by proof of work. To forge a block header containing a fakemerkle_root, an attacker would need to redo the mining work for that block and all subsequent blocks, which is computationally extremely difficult (the “longest chain rule”).
The only trust assumption of SPV is: honest miners control more than 51% of the hashrate. As long as this assumption holds, an attacker cannot deceive an SPV client at any practical cost.
Summary
| Component | Role |
|---|---|
MerkleNode | Basic unit of the Merkle tree; stores the hash value and references to child nodes |
MerkleTree::new | Builds the complete Merkle tree bottom-up from a list of transactions |
MerkleTree::get_proof | Generates an O(log n)-sized Merkle proof for a specified transaction |
MerkleTree::verify_proof | Verifies a transaction using a proof + root hash; O(log n) time complexity |
BlockHeader | Block header; 80 bytes; contains the Merkle Root |
SPVClient | Light client; downloads only block headers and verifies transactions using Merkle proofs |
Multi-Signature (MultiSig)
Multi-signature is an advanced Bitcoin feature that requires M signatures to spend funds controlled by N public keys (M-of-N).
Overview
What is Multi-Signature?
A multi-signature address requires multiple private keys to jointly sign before funds can be spent, rather than a single private key as in traditional setups.
Examples:
- 2-of-3: Requires any 2 of 3 keys
- 3-of-5: Requires any 3 of 5 keys
- 2-of-2: Requires both keys to agree
Why Use MultiSig?
1. Improved Security
- No single point of failure
- A stolen private key does not immediately result in fund loss
- Risk is distributed
2. Distributed Trust
- Corporate governance: prevents misuse by a single person
- Escrow services: buyer + seller + arbitrator
- Joint family management: shared management by spouses
3. Flexibility
- Different M-N combinations meet different needs
- Emergency recovery mechanisms can be set up
- Supports complex business logic
Technical Implementation
MultiSig Address Structure
#![allow(unused)]
fn main() {
pub struct MultiSigAddress {
pub address: String, // Multi-sig address (starts with "3")
pub required_sigs: usize, // M (number of required signatures)
pub total_keys: usize, // N (total number of keys)
pub public_keys: Vec<String>, // Public keys of all participants
pub script: String, // Locking script
}
}
Creating a Multi-Sig Address
#![allow(unused)]
fn main() {
use bitcoin_simulation::{multisig::MultiSigAddress, wallet::Wallet};
// Create participants
let ceo = Wallet::new();
let cfo = Wallet::new();
let cto = Wallet::new();
// Collect public keys
let public_keys = vec![
ceo.public_key.clone(),
cfo.public_key.clone(),
cto.public_key.clone(),
];
// Create a 2-of-3 multi-sig address
let multisig = MultiSigAddress::new(2, public_keys)?;
println!("Multi-sig address: {}", multisig.address);
println!("Required signatures: {}/{}", multisig.required_sigs, multisig.total_keys);
}
MultiSig Types
SimpleBTC provides preset common multi-sig types:
#![allow(unused)]
fn main() {
use bitcoin_simulation::multisig::MultiSigType;
// 2-of-2: both parties must agree
let two_of_two = MultiSigAddress::from_type(
MultiSigType::TwoOfTwo,
vec![alice.public_key, bob.public_key]
)?;
// 2-of-3: any two parties suffice (most common)
let two_of_three = MultiSigAddress::from_type(
MultiSigType::TwoOfThree,
vec![party1.public_key, party2.public_key, party3.public_key]
)?;
// 3-of-5: high-security scenarios
let three_of_five = MultiSigAddress::from_type(
MultiSigType::ThreeOfFive,
vec![pk1, pk2, pk3, pk4, pk5]
)?;
}
Use Cases
Use Case 1: Corporate Financial Management
Requirement: Company funds require joint approval from multiple executives
Solution: 2-of-3 MultiSig (CEO + CFO + CTO)
#![allow(unused)]
fn main() {
fn setup_corporate_wallet() -> Result<MultiSigAddress, String> {
// 1. Create executive wallets
let ceo = Wallet::new();
let cfo = Wallet::new();
let cto = Wallet::new();
println!("=== Corporate Multi-Sig Wallet ===");
println!("CEO: {}", &ceo.address[..16]);
println!("CFO: {}", &cfo.address[..16]);
println!("CTO: {}", &cto.address[..16]);
// 2. Create multi-sig address
let company_wallet = MultiSigAddress::new(
2, // Requires 2 signatures
vec![
ceo.public_key.clone(),
cfo.public_key.clone(),
cto.public_key.clone(),
]
)?;
println!("\nCompany multi-sig address: {}", company_wallet.address);
println!("Rule: any 2 executives can authorize a transfer\n");
Ok(company_wallet)
}
// Transfer scenario
fn corporate_payment(
multisig: &MultiSigAddress,
ceo: &Wallet,
cfo: &Wallet,
recipient: &str,
amount: u64
) -> Result<(), String> {
println!("Transferring {} satoshi to {}", amount, &recipient[..16]);
// 1. CEO signs
let ceo_sig = ceo.sign(&format!("{}{}", multisig.address, amount));
println!("✓ CEO has signed");
// 2. CFO signs
let cfo_sig = cfo.sign(&format!("{}{}", multisig.address, amount));
println!("✓ CFO has signed");
// 3. Collect signatures
let signatures = vec![ceo_sig, cfo_sig];
// 4. Verify signature count
if signatures.len() >= multisig.required_sigs {
println!("✅ Signature count meets requirement; transaction can be executed");
// Create and broadcast transaction...
Ok(())
} else {
Err("Insufficient signatures".to_string())
}
}
}
Advantages:
- ✅ Prevents misuse of funds by a single person
- ✅ CFO + CTO can still operate when CEO is traveling
- ✅ Even if one person is compromised, funds remain safe
Use Case 2: Escrow Service
Requirement: Buyer and seller do not trust each other; a third-party arbitrator is needed
Solution: 2-of-3 MultiSig (buyer + seller + arbitrator)
#![allow(unused)]
fn main() {
fn escrow_service() -> Result<(), String> {
// Participants
let buyer = Wallet::new();
let seller = Wallet::new();
let arbitrator = Wallet::new();
println!("=== Escrow Service ===");
println!("Buyer: {}", &buyer.address[..16]);
println!("Seller: {}", &seller.address[..16]);
println!("Arbitrator: {}", &arbitrator.address[..16]);
// Create escrow multi-sig address
let escrow = MultiSigAddress::new(
2,
vec![
buyer.public_key.clone(),
seller.public_key.clone(),
arbitrator.public_key.clone(),
]
)?;
println!("\nEscrow address: {}", escrow.address);
// Scenario 1: Normal transaction (buyer + seller)
println!("\n--- Scenario 1: Transaction completed smoothly ---");
println!("Buyer received goods; satisfied");
println!("Buyer signs: ✓");
println!("Seller signs: ✓");
println!("✅ 2/3 signatures; funds released to seller");
// Scenario 2: Dispute (buyer + arbitrator or seller + arbitrator)
println!("\n--- Scenario 2: Dispute arises ---");
println!("Buyer: goods are defective");
println!("Seller: goods are fine");
println!("Arbitrator investigates...");
println!("Arbitrator: buyer is right");
println!("Buyer signs: ✓");
println!("Arbitrator signs: ✓");
println!("✅ 2/3 signatures; funds refunded to buyer");
Ok(())
}
}
Advantages:
- ✅ Buyer protection: refund if goods don’t match description
- ✅ Seller protection: funds released automatically for normal transactions
- ✅ Fair: arbitrator cannot control funds alone
Use Case 3: Personal Asset Protection
Requirement: Prevent loss due to a single private key being lost or stolen
Solution: 2-of-3 MultiSig (primary key + backup key + custodian key)
#![allow(unused)]
fn main() {
fn personal_security_setup() -> Result<(), String> {
// Key assignment
let main_key = Wallet::new(); // Daily use
let backup_key = Wallet::new(); // Safe deposit box
let custodian_key = Wallet::new(); // Lawyer / trust company
println!("=== Personal Asset Protection ===");
println!("Primary key (daily): {}", &main_key.address[..16]);
println!("Backup key (safe): {}", &backup_key.address[..16]);
println!("Custodian key (lawyer): {}", &custodian_key.address[..16]);
let secure_wallet = MultiSigAddress::new(
2,
vec![
main_key.public_key,
backup_key.public_key,
custodian_key.public_key,
]
)?;
println!("\nSecure wallet: {}", secure_wallet.address);
// Usage scenarios
println!("\n--- Usage Scenarios ---");
println!("Daily transfers: primary key + backup key");
println!("Primary key lost: backup key + custodian key");
println!("Theft risk: requires 2 keys; single key theft poses no risk");
Ok(())
}
}
Use Case 4: Cold-Hot Wallet Combination
Requirement: Security for large storage + convenience for small amounts
Solution: 2-of-3 (hot wallet + cold wallet 1 + cold wallet 2)
#![allow(unused)]
fn main() {
fn cold_hot_wallet_setup() -> Result<(), String> {
let hot_wallet = Wallet::new(); // Online device
let cold_wallet_1 = Wallet::new(); // Hardware wallet 1
let cold_wallet_2 = Wallet::new(); // Paper wallet
println!("=== Cold-Hot Wallet Combination ===");
println!("Hot wallet (phone): {}", &hot_wallet.address[..16]);
println!("Cold wallet 1 (Ledger): {}", &cold_wallet_1.address[..16]);
println!("Cold wallet 2 (paper wallet): {}", &cold_wallet_2.address[..16]);
let vault = MultiSigAddress::new(
2,
vec![
hot_wallet.public_key,
cold_wallet_1.public_key,
cold_wallet_2.public_key,
]
)?;
println!("\nVault address: {}", vault.address);
println!("\n--- Usage Strategy ---");
println!("Daily small amounts: hot wallet + cold wallet 1 (convenient)");
println!("Large transfers: cold wallet 1 + cold wallet 2 (most secure)");
println!("Hot wallet hacked: still requires cold wallet cooperation; funds safe");
Ok(())
}
}
Advanced Usage
TimeLock + MultiSig
Combining time locks for inheritance planning:
#![allow(unused)]
fn main() {
use bitcoin_simulation::advanced_tx::TimeLock;
fn inheritance_setup() -> Result<(), String> {
let owner = Wallet::new();
let heir = Wallet::new();
let lawyer = Wallet::new();
// Normal: 2-of-2 (owner + heir; protects privacy)
let normal_multisig = MultiSigAddress::new(
2,
vec![owner.public_key.clone(), heir.public_key.clone()]
)?;
// Time lock: 1 year later
let one_year = 365 * 24 * 60 * 60;
let unlock_time = current_timestamp() + one_year;
let timelock = TimeLock::new_time_based(unlock_time);
println!("=== Inheritance Plan ===");
println!("Normal period: requires owner + heir (2-of-2)");
println!("After 1 year: heir can operate independently");
// Or use 3-of-3, downgraded to 2-of-3 after 1 year
let emergency_multisig = MultiSigAddress::new(
2, // Only 2 required after 1 year
vec![owner.public_key, heir.public_key, lawyer.public_key]
)?;
Ok(())
}
}
Hierarchical MultiSig
Multi-level multi-sig structure for large organizations:
#![allow(unused)]
fn main() {
// Board of directors: 5-of-9
let board = MultiSigAddress::new(5, board_members)?;
// Executive committee: 3-of-5
let exec_committee = MultiSigAddress::new(3, executives)?;
// Petty cash: 2-of-3
let petty_cash = MultiSigAddress::new(2, managers)?;
println!("Permission levels:");
println!("< 10 BTC: manager level 2-of-3");
println!("10-100 BTC: executive level 3-of-5");
println!("> 100 BTC: board level 5-of-9");
}
Security Considerations
⚠️ Important Notes
-
Key Management
- Store keys in dispersed locations; do not keep them together
- Use hardware wallets for cold keys
- Regularly test backup recovery
-
Choosing M
- M too small: security is reduced
- M too large: usability is reduced
- Recommended: M = (N+1)/2 or N-1
-
Choosing N
- N=2: simple but has a single point of failure
- N=3: balances security and convenience (most common)
- N=5+: high security but complex
-
Choosing Participants
- Geographically dispersed
- Trusted but mutually independent
- Have emergency contact information
Best Practices
#![allow(unused)]
fn main() {
// ✅ Good practice
let multisig = MultiSigAddress::new(
2, // Reasonable M value
vec![key1, key2, key3] // 3 independent keys
)?;
// Dispersed storage
// key1 -> mobile hot wallet
// key2 -> hardware wallet (safe)
// key3 -> paper wallet (bank safe deposit box)
// ❌ Bad practice
// All keys stored on the same computer
// M=N (loses fault tolerance)
// Multiple keys derived from the same mnemonic
}
Complete Example
#![allow(unused)]
fn main() {
use bitcoin_simulation::{
blockchain::Blockchain,
wallet::Wallet,
multisig::MultiSigAddress,
};
fn complete_multisig_demo() -> Result<(), String> {
let mut blockchain = Blockchain::new();
// Create participants
let alice = Wallet::new();
let bob = Wallet::new();
let charlie = Wallet::new();
// Create 2-of-3 multi-sig
let multisig = MultiSigAddress::new(
2,
vec![
alice.public_key.clone(),
bob.public_key.clone(),
charlie.public_key.clone(),
]
)?;
println!("Multi-sig address: {}", multisig.address);
// 1. Deposit funds
let funding_tx = blockchain.create_transaction(
&Wallet::from_address("funder".to_string()),
multisig.address.clone(),
10000,
0,
)?;
blockchain.add_transaction(funding_tx)?;
blockchain.mine_pending_transactions(alice.address.clone())?;
println!("Multi-sig balance: {}", blockchain.get_balance(&multisig.address));
// 2. Multi-sig transfer (requires 2 signatures)
let recipient = Wallet::new();
// Alice signs
let alice_sig = alice.sign(&format!("{}{}",
multisig.address, recipient.address));
// Bob signs
let bob_sig = bob.sign(&format!("{}{}",
multisig.address, recipient.address));
// Verify signatures
println!("\nCollecting signatures:");
println!("Alice: ✓");
println!("Bob: ✓");
if vec![alice_sig, bob_sig].len() >= multisig.required_sigs {
println!("✅ Signatures meet requirement; transfer can proceed");
// Create transfer transaction
// Note: actual implementation requires multi-sig transaction building logic
println!("Transaction created and broadcast");
}
Ok(())
}
}
References
Next: TimeLock Tutorial | RBF Mechanism
Replace-By-Fee (RBF) Mechanism
RBF (Replace-By-Fee) is a mechanism proposed in BIP125 that allows users to replace unconfirmed transactions.
Overview
What is RBF?
RBF allows a sender to replace an unconfirmed transaction with a new transaction carrying a higher fee.
Scenario:
1. Alice sends transaction A: 1 BTC → Bob, fee 1 sat/byte
2. Network congestion; transaction A remains unconfirmed for a long time
3. Alice sends transaction B: 1 BTC → Bob, fee 50 sat/byte
4. Miners prioritize packing transaction B (higher fee)
5. Transaction A is discarded
Why is RBF Needed?
-
Speed up confirmation
- Initial fee estimate was inaccurate
- Network suddenly became congested
- Urgent transaction needs fast confirmation
-
Cancel a transaction
- Sent to the wrong address
- Changed your mind
- “Cancel” by sending to yourself
-
Batch optimization
- Initial transaction includes some recipients
- Add more recipients later
- Save on total fees
Technical Implementation
RBF Signaling
nSequence field:
#![allow(unused)]
fn main() {
// Enable RBF
input.sequence = 0xFFFFFFFD; // < 0xFFFFFFFE
// Disable RBF (final transaction)
input.sequence = 0xFFFFFFFF;
}
BIP125 Rules
A replacement transaction must satisfy:
-
Higher fee
#![allow(unused)] fn main() { new_tx.fee > original_tx.fee } -
Spends the same UTXOs
#![allow(unused)] fn main() { new_tx.inputs == original_tx.inputs } -
Fee increment
#![allow(unused)] fn main() { new_tx.fee >= original_tx.fee + min_relay_fee } -
Does not introduce new unconfirmed UTXOs
RBFManager Implementation
Data Structure
#![allow(unused)]
fn main() {
pub struct RBFManager {
replaceable_txs: Vec<String>, // List of replaceable transaction IDs
}
}
Methods
new
#![allow(unused)]
fn main() {
pub fn new() -> Self
}
Creates a new RBF manager.
mark_replaceable
#![allow(unused)]
fn main() {
pub fn mark_replaceable(&mut self, txid: String)
}
Marks a transaction as replaceable.
Example:
#![allow(unused)]
fn main() {
let mut rbf = RBFManager::new();
rbf.mark_replaceable(tx.id.clone());
}
is_replaceable
#![allow(unused)]
fn main() {
pub fn is_replaceable(&self, txid: &str) -> bool
}
Checks whether a transaction is replaceable.
replace_transaction
#![allow(unused)]
fn main() {
pub fn replace_transaction(
&mut self,
original_txid: &str,
new_tx: Transaction
) -> Result<(), String>
}
Replaces the original transaction with a new one.
Validation:
- The original transaction must be replaceable
- The new transaction has a higher fee
- The new transaction is valid
Use Cases
Use Case 1: Speeding Up Confirmation
#![allow(unused)]
fn main() {
use bitcoin_simulation::{
blockchain::Blockchain,
wallet::Wallet,
advanced_tx::RBFManager,
};
fn speed_up_transaction() -> Result<(), String> {
let mut blockchain = Blockchain::new();
let mut rbf = RBFManager::new();
let alice = Wallet::new();
let bob = Wallet::new();
// Initialize balance
setup_balance(&mut blockchain, &alice, 10000)?;
println!("=== RBF Transaction Acceleration Demo ===\n");
// 1. Create a low-fee transaction
println!("--- Step 1: Send a low-fee transaction ---");
let slow_tx = blockchain.create_transaction(
&alice,
bob.address.clone(),
1000,
1, // Low fee: 1 sat
)?;
println!("Original transaction:");
println!(" ID: {}", &slow_tx.id[..16]);
println!(" Amount: 1000 sat");
println!(" Fee: 1 sat");
println!(" Fee rate: {:.2} sat/byte\n", slow_tx.fee_rate());
blockchain.add_transaction(slow_tx.clone())?;
rbf.mark_replaceable(slow_tx.id.clone());
// 2. Network congestion; transaction remains unconfirmed for a long time
println!("--- Step 2: Network congestion ---");
println!("⏰ Waiting for confirmation...");
println!("⏰ Still unconfirmed after 10 minutes");
println!("⚠️ Fee too low; need to accelerate\n");
// 3. Create a high-fee replacement transaction
println!("--- Step 3: Create replacement transaction (higher fee) ---");
let fast_tx = blockchain.create_transaction(
&alice,
bob.address.clone(),
1000,
50, // High fee: 50 sat
)?;
println!("Replacement transaction:");
println!(" ID: {}", &fast_tx.id[..16]);
println!(" Amount: 1000 sat");
println!(" Fee: 50 sat (50x)");
println!(" Fee rate: {:.2} sat/byte\n", fast_tx.fee_rate());
// 4. Validate and replace
if rbf.is_replaceable(&slow_tx.id) {
if fast_tx.fee > slow_tx.fee {
println!("✓ RBF conditions met:");
println!(" New fee ({}) > Original fee ({})", fast_tx.fee, slow_tx.fee);
// Remove the original transaction from the pending pool
blockchain.pending_transactions.retain(|tx| tx.id != slow_tx.id);
// Add the new transaction
blockchain.add_transaction(fast_tx)?;
println!("✓ Transaction replaced\n");
}
}
// 5. Mine and confirm
println!("--- Step 4: Miner packs (prioritizes high fee rate) ---");
blockchain.mine_pending_transactions(alice.address.clone())?;
println!("✓ Transaction confirmed");
println!(" Bob balance: {} sat", blockchain.get_balance(&bob.address));
Ok(())
}
}
Output:
=== RBF Transaction Acceleration Demo ===
--- Step 1: Send a low-fee transaction ---
Original transaction:
ID: abc123...
Amount: 1000 sat
Fee: 1 sat
Fee rate: 0.01 sat/byte
--- Step 2: Network congestion ---
⏰ Waiting for confirmation...
⏰ Still unconfirmed after 10 minutes
⚠️ Fee too low; need to accelerate
--- Step 3: Create replacement transaction (higher fee) ---
Replacement transaction:
ID: def456...
Amount: 1000 sat
Fee: 50 sat (50x)
Fee rate: 0.50 sat/byte
✓ RBF conditions met:
New fee (50) > Original fee (1)
✓ Transaction replaced
--- Step 4: Miner packs (prioritizes high fee rate) ---
✓ Transaction confirmed
Bob balance: 1000 sat
Use Case 2: Canceling a Transaction
#![allow(unused)]
fn main() {
fn cancel_transaction() -> Result<(), String> {
let mut blockchain = Blockchain::new();
let mut rbf = RBFManager::new();
let alice = Wallet::new();
let wrong_addr = Wallet::new().address; // Wrong address
setup_balance(&mut blockchain, &alice, 10000)?;
println!("=== RBF Transaction Cancellation Demo ===\n");
// 1. Sent to the wrong address
println!("--- Error: sent to wrong address ---");
let wrong_tx = blockchain.create_transaction(
&alice,
wrong_addr.clone(),
5000,
10,
)?;
println!("Erroneous transaction:");
println!(" Recipient: {} (wrong!)", &wrong_addr[..16]);
println!(" Amount: 5000 sat\n");
blockchain.add_transaction(wrong_tx.clone())?;
rbf.mark_replaceable(wrong_tx.id.clone());
// 2. Error discovered; attempt to cancel
println!("--- Error discovered; attempting to cancel ---");
println!("Strategy: send to yourself with a higher fee\n");
// 3. Create a "cancel" transaction (send to yourself)
let cancel_tx = blockchain.create_transaction(
&alice,
alice.address.clone(), // Send to yourself
4950, // Slightly less (fee deducted)
50, // Higher fee
)?;
println!("Cancellation transaction:");
println!(" Recipient: {} (yourself)", &alice.address[..16]);
println!(" Amount: 4950 sat");
println!(" Fee: 50 sat (5x)\n");
// 4. Replace
if cancel_tx.fee > wrong_tx.fee {
blockchain.pending_transactions.retain(|tx| tx.id != wrong_tx.id);
blockchain.add_transaction(cancel_tx)?;
println!("✓ Transaction cancelled (actually replaced)\n");
}
// 5. Confirm
blockchain.mine_pending_transactions(alice.address.clone())?;
println!("✓ Funds returned");
println!(" Alice balance: {} sat", blockchain.get_balance(&alice.address));
println!(" Wrong address balance: {} sat", blockchain.get_balance(&wrong_addr));
Ok(())
}
}
Use Case 3: Batch Payment Optimization
#![allow(unused)]
fn main() {
fn batch_payment_optimization() -> Result<(), String> {
let mut blockchain = Blockchain::new();
let mut rbf = RBFManager::new();
let alice = Wallet::new();
let recipients: Vec<_> = (0..5).map(|_| Wallet::new()).collect();
setup_balance(&mut blockchain, &alice, 100000)?;
println!("=== RBF Batch Payment Optimization ===\n");
// 1. Initial payment (2 recipients)
println!("--- Initial batch payment (2 recipients) ---");
let mut outputs = vec![
TxOutput::new(1000, recipients[0].address.clone()),
TxOutput::new(2000, recipients[1].address.clone()),
];
// Create transaction... (simplified)
println!("Payment:");
println!(" Recipient 1: 1000 sat");
println!(" Recipient 2: 2000 sat");
println!(" Fee: 10 sat\n");
// 2. Add more recipients
println!("--- Add more recipients (RBF extension) ---");
outputs.push(TxOutput::new(3000, recipients[2].address.clone()));
outputs.push(TxOutput::new(4000, recipients[3].address.clone()));
println!("Newly added:");
println!(" Recipient 3: 3000 sat");
println!(" Recipient 4: 4000 sat");
println!(" Fee: 15 sat (only 5 sat more!)\n");
println!("Advantages:");
println!(" ✓ 4 transactions merged into 1");
println!(" ✓ Fee savings (4×10 - 15 = 25 sat)");
println!(" ✓ Block space saved");
Ok(())
}
}
Security Considerations
⚠️ Zero-Confirmation Transaction Risk
Problem: RBF makes zero-confirmation transactions insecure
#![allow(unused)]
fn main() {
// Attack scenario
// 1. Attacker: Alice → merchant Bob (1 BTC, low fee)
// Merchant sees the transaction and ships the goods
// 2. Attacker replaces: Alice → Alice (1 BTC, high fee)
// Funds return to attacker; merchant suffers a loss
// Defense: wait for confirmation
if confirmations < 1 {
println!("⚠️ Warning: zero-confirmation transactions are unsafe (RBF risk)");
println!("Recommendation: wait for at least 1 confirmation");
}
}
Merchant Recommendations
#![allow(unused)]
fn main() {
fn accept_payment(tx: &Transaction) -> bool {
// 1. Check whether RBF is enabled
if is_rbf_enabled(tx) {
println!("⚠️ Transaction has RBF enabled");
// Option A: Reject zero-confirmation
println!("Waiting for confirmation...");
return false;
// Option B: Require a higher fee
if tx.fee_rate() < 50.0 {
println!("Fee too low; requires >= 50 sat/byte");
return false;
}
}
// 2. Wait for sufficient confirmations
let confirmations = get_confirmations(tx);
if confirmations < 1 {
return false;
}
true
}
}
RBF vs CPFP
Child-Pays-For-Parent (CPFP)
CPFP: A child transaction pays for the parent transaction’s fee
Parent transaction: Alice → Bob (low fee)
↓
Child transaction: Bob → Charlie (high fee)
Miners will pack them together to collect the higher fee
Comparison
| Feature | RBF | CPFP |
|---|---|---|
| Operator | Sender | Receiver |
| Mechanism | Replace transaction | Child transaction pulls parent |
| Fee paid by | Sender | Receiver |
| Complexity | Simple | Slightly more complex |
| Use case | Sender accelerates | Receiver accelerates |
Best Practices
1. When to Use RBF
#![allow(unused)]
fn main() {
// ✅ Suitable scenarios for RBF
if network_congested && !urgent {
// Send with a low fee first; accelerate when needed
create_rbf_transaction(fee_low);
}
// ❌ Unsuitable scenarios for RBF
if urgent || large_amount {
// Send with a high fee directly
create_transaction(fee_high);
}
}
2. Fee Strategy
#![allow(unused)]
fn main() {
fn calculate_replacement_fee(original_fee: u64) -> u64 {
// Increase by at least 50% of the original fee
let min_increase = original_fee / 2;
// Or reach the current recommended fee rate
let recommended = get_recommended_fee_rate() * tx_size;
max(original_fee + min_increase, recommended)
}
}
3. User Notification
#![allow(unused)]
fn main() {
fn notify_replacement(original_tx: &Transaction, new_tx: &Transaction) {
println!("📢 Transaction has been replaced:");
println!(" Original transaction: {}", &original_tx.id[..16]);
println!(" New transaction: {}", &new_tx.id[..16]);
println!(" Original fee: {} sat", original_tx.fee);
println!(" New fee: {} sat", new_tx.fee);
println!(" Increase: +{} sat", new_tx.fee - original_tx.fee);
}
}
Implementation Example
Complete RBF Transaction Flow
#![allow(unused)]
fn main() {
use bitcoin_simulation::advanced_tx::RBFManager;
fn rbf_complete_example() -> Result<(), String> {
let mut blockchain = Blockchain::new();
let mut rbf = RBFManager::new();
let alice = Wallet::new();
let bob = Wallet::new();
// 1. Initialize
setup_balance(&mut blockchain, &alice, 10000)?;
// 2. Create a replaceable transaction
let tx1 = blockchain.create_transaction(&alice, bob.address.clone(), 1000, 5)?;
blockchain.add_transaction(tx1.clone())?;
rbf.mark_replaceable(tx1.id.clone());
println!("✓ Original transaction created (fee: 5 sat)");
// 3. Monitor transaction status
std::thread::sleep(std::time::Duration::from_secs(30));
if !is_confirmed(&blockchain, &tx1.id) {
println!("⚠️ Still unconfirmed after 30 seconds; preparing to accelerate...");
// 4. Create replacement transaction
let tx2 = blockchain.create_transaction(&alice, bob.address, 1000, 50)?;
// 5. Validate RBF rules
if rbf.can_replace(&tx1, &tx2) {
// 6. Execute replacement
blockchain.pending_transactions.retain(|tx| tx.id != tx1.id);
blockchain.add_transaction(tx2.clone())?;
println!("✓ Transaction replaced (fee: 50 sat)");
// 7. Confirm
blockchain.mine_pending_transactions(alice.address)?;
println!("✓ New transaction confirmed");
}
}
Ok(())
}
}
References
Summary: RBF is a powerful tool, but be mindful of zero-confirmation transaction risks. Merchants should wait for confirmation; users should use it judiciously.
Time Lock (TimeLock / nLockTime)
Time locks are an important Bitcoin feature that prevent a transaction from being confirmed before a specific time or block height.
Overview
What is a Time Lock?
A time lock allows a transaction to be used only at some future point in time, implementing a “deferred payment” feature.
Example:
Alice creates a transaction: 1 BTC → Bob
Time lock: January 1, 2025
Before January 1, 2025:
❌ Transaction cannot be confirmed
❌ Miners refuse to pack it
After January 1, 2025:
✓ Transaction can be confirmed
✓ Miners can pack it
Two Types
1. Unix Timestamp-Based
#![allow(unused)]
fn main() {
// locktime >= 500,000,000
let unlock_time = 1735689600; // 2025-01-01 00:00:00
let timelock = TimeLock::new_time_based(unlock_time);
}
Characteristics:
- Unit: seconds
- Suitable for precise time control
- Affected by system time
Use cases:
- Salary payment (1st of each month)
- Bond maturity (fixed date)
- Term deposit (3/6/12 months)
2. Block Height-Based
#![allow(unused)]
fn main() {
// locktime < 500,000,000
let unlock_height = 800000; // Block #800,000
let timelock = TimeLock::new_block_based(unlock_height);
}
Characteristics:
- Unit: blocks
- More precise (approximately 10 minutes per block)
- Not affected by system time
Use cases:
- More precise time control
- Avoid timestamp manipulation
- Smart contract triggers
Time estimation:
1 block ≈ 10 minutes
6 blocks ≈ 1 hour
144 blocks ≈ 1 day
1008 blocks ≈ 1 week
4032 blocks ≈ 1 month
TimeLock Implementation
Data Structure
#![allow(unused)]
fn main() {
pub struct TimeLock {
pub locktime: u64, // Lock time / height
pub is_block_height: bool, // true: block height, false: timestamp
}
}
Methods
new_time_based
#![allow(unused)]
fn main() {
pub fn new_time_based(timestamp: u64) -> Self
}
Creates a time-based time lock.
Parameters:
timestamp- Unix timestamp (seconds)
Example:
#![allow(unused)]
fn main() {
use bitcoin_simulation::advanced_tx::TimeLock;
use std::time::{SystemTime, UNIX_EPOCH};
let current_time = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
// Unlock after 3 months
let three_months = 90 * 24 * 3600;
let unlock_time = current_time + three_months;
let timelock = TimeLock::new_time_based(unlock_time);
println!("Locked until: {}", format_timestamp(unlock_time));
}
new_block_based
#![allow(unused)]
fn main() {
pub fn new_block_based(block_height: u64) -> Self
}
Creates a block height-based time lock.
Parameters:
block_height- Target block height
Example:
#![allow(unused)]
fn main() {
let current_height = blockchain.chain.len() as u64;
// Unlock after 1000 blocks (approximately 1 week)
let unlock_height = current_height + 1000;
let timelock = TimeLock::new_block_based(unlock_height);
println!("Locked until block #{}", unlock_height);
}
is_mature
#![allow(unused)]
fn main() {
pub fn is_mature(&self, current_time: u64, current_height: u64) -> bool
}
Checks whether the time lock has expired.
Parameters:
current_time- Current Unix timestampcurrent_height- Current block height
Return value:
true- Expired; can be usedfalse- Not yet expired; still locked
Example:
#![allow(unused)]
fn main() {
let timelock = TimeLock::new_time_based(unlock_time);
if timelock.is_mature(current_time, 0) {
println!("✓ Expired; can be spent");
} else {
let remaining = unlock_time - current_time;
println!("🔒 Still locked; {} seconds remaining", remaining);
}
}
Use Cases
Use Case 1: Term Deposit
#![allow(unused)]
fn main() {
fn savings_account_demo() -> Result<(), String> {
println!("=== Term Deposit Demo ===\n");
let alice = Wallet::new();
let mut blockchain = Blockchain::new();
// Initialize balance
setup_balance(&mut blockchain, &alice, 100000)?;
let current_time = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
// Product 1: 3-month term deposit
println!("--- Product A: 3-Month Term Deposit ---");
let three_months = 90 * 24 * 3600;
let maturity_3m = current_time + three_months;
let deposit_3m = TimeLock::new_time_based(maturity_3m);
println!("Deposit amount: 30,000 sat");
println!("Term: 3 months");
println!("Maturity date: {}", format_date(maturity_3m));
println!("Annual interest rate: 3%\n");
// Product 2: 1-year term deposit
println!("--- Product B: 1-Year Term Deposit ---");
let one_year = 365 * 24 * 3600;
let maturity_1y = current_time + one_year;
let deposit_1y = TimeLock::new_time_based(maturity_1y);
println!("Deposit amount: 50,000 sat");
println!("Term: 1 year");
println!("Maturity date: {}", format_date(maturity_1y));
println!("Annual interest rate: 5%\n");
// Check maturity status
println!("--- Current Status Check ---");
println!("Current time: {}", format_date(current_time));
if deposit_3m.is_mature(current_time, 0) {
println!("✓ 3-month deposit has matured; can be withdrawn");
let interest = 30000 * 3 / 100 / 4; // Quarterly interest
println!(" Principal + interest: {} sat", 30000 + interest);
} else {
let days_left = (maturity_3m - current_time) / 86400;
println!("🔒 3-month deposit is locked");
println!(" Remaining: {} days", days_left);
}
if deposit_1y.is_mature(current_time, 0) {
println!("✓ 1-year deposit has matured; can be withdrawn");
let interest = 50000 * 5 / 100; // Annual interest
println!(" Principal + interest: {} sat", 50000 + interest);
} else {
let days_left = (maturity_1y - current_time) / 86400;
println!("🔒 1-year deposit is locked");
println!(" Remaining: {} days", days_left);
}
Ok(())
}
}
Output:
=== Term Deposit Demo ===
--- Product A: 3-Month Term Deposit ---
Deposit amount: 30,000 sat
Term: 3 months
Maturity date: 2025-03-15 00:00:00
Annual interest rate: 3%
--- Product B: 1-Year Term Deposit ---
Deposit amount: 50,000 sat
Term: 1 year
Maturity date: 2025-12-15 00:00:00
Annual interest rate: 5%
--- Current Status Check ---
Current time: 2024-12-15 00:00:00
🔒 3-month deposit is locked
Remaining: 90 days
🔒 1-year deposit is locked
Remaining: 365 days
Use Case 2: Inheritance Planning
#![allow(unused)]
fn main() {
fn inheritance_planning() -> Result<(), String> {
println!("=== Inheritance Plan ===\n");
let owner = Wallet::new();
let heir = Wallet::new();
let lawyer = Wallet::new();
println!("Participants:");
println!(" Owner: {}", &owner.address[..16]);
println!(" Heir: {}", &heir.address[..16]);
println!(" Lawyer: {}\n", &lawyer.address[..16]);
let current_time = current_timestamp();
// Plan: after 1 year of inactivity, assets automatically transfer to the heir
println!("--- Plan Design ---");
println!("Normal situation:");
println!(" Requires: owner + heir (2-of-2)");
println!(" Protects privacy; prevents unilateral transfer\n");
println!("Emergency situation (after 1 year):");
println!(" Owner is unreachable or deceased");
println!(" Time lock has expired");
println!(" Heir can operate independently\n");
// Create time lock transaction
let one_year = 365 * 24 * 3600;
let inheritance_time = current_time + one_year;
let timelock = TimeLock::new_time_based(inheritance_time);
println!("--- Time Lock Configuration ---");
println!("Trigger time: {}", format_date(inheritance_time));
println!("Trigger condition: no transaction signed by owner within 1 year\n");
// Periodic check (performed by lawyer)
println!("--- Periodic Check ---");
let last_activity = current_time;
let inactive_period = current_time - last_activity;
if inactive_period > one_year {
if timelock.is_mature(current_time, 0) {
println!("✓ Time lock triggered");
println!("✓ Inheritance process started");
println!("✓ Assets can be transferred to heir");
}
} else {
let days_remaining = (one_year - inactive_period) / 86400;
println!("🔒 Normal status");
println!("{} days until inheritance trigger", days_remaining);
}
Ok(())
}
}
Use Case 3: Salary Payment
#![allow(unused)]
fn main() {
fn salary_payment_system() -> Result<(), String> {
println!("=== Salary Payment System ===\n");
let company = Wallet::new();
let employees: Vec<_> = (0..5)
.map(|i| (format!("Employee {}", i+1), Wallet::new()))
.collect();
let current_time = current_timestamp();
println!("Company address: {}", &company.address[..16]);
println!("Number of employees: {}\n", employees.len());
// Pay salary on the 1st of each month
println!("--- Salary Payment Schedule ---");
for month in 1..=3 {
// Calculate the timestamp for the 1st of the next month
let payment_date = calculate_first_day_of_month(current_time, month);
let timelock = TimeLock::new_time_based(payment_date);
println!("Month {} salary:", month);
println!(" Payment date: {}", format_date(payment_date));
if timelock.is_mature(current_time, 0) {
println!(" Status: ✓ Ready to pay");
for (name, wallet) in &employees {
println!(" {} → {} sat", name, 10000);
}
} else {
let days_until = (payment_date - current_time) / 86400;
println!(" Status: 🔒 Locked");
println!(" Countdown: {} days", days_until);
}
println!();
}
println!("--- Advantages ---");
println!("✓ Automated payment");
println!("✓ Cannot be diverted early");
println!("✓ Employees have predictable income");
println!("✓ Reduced administrative costs");
Ok(())
}
}
Use Case 4: Crowdfunding Refund
#![allow(unused)]
fn main() {
fn crowdfunding_refund() -> Result<(), String> {
println!("=== Crowdfunding Refund Mechanism ===\n");
let project_owner = Wallet::new();
let backers: Vec<_> = (0..10).map(|_| Wallet::new()).collect();
let current_time = current_timestamp();
println!("--- Crowdfunding Project ---");
println!("Target amount: 1,000,000 sat");
println!("Currently raised: 500,000 sat");
println!("Deadline: 30 days from now\n");
// If the target is not met after 30 days, automatically refund
let deadline = current_time + 30 * 86400;
let refund_timelock = TimeLock::new_time_based(deadline);
println!("--- Refund Time Lock ---");
println!("Trigger condition: target not met after 30 days");
println!("Trigger time: {}", format_date(deadline));
println!("Refund method: automatically returned to backers\n");
// Check status
if refund_timelock.is_mature(current_time, 0) {
println!("--- Project failed; executing refund ---");
for (i, backer) in backers.iter().enumerate() {
println!("✓ Refund to backer #{}: {} sat", i+1, 50000);
}
} else {
let days_left = (deadline - current_time) / 86400;
println!("--- Crowdfunding in progress ---");
println!("Time remaining: {} days", days_left);
println!("Still needed: 500,000 sat");
}
Ok(())
}
}
Advanced Usage
TimeLock + MultiSig
Combining multi-sig for more complex logic:
#![allow(unused)]
fn main() {
fn timelock_multisig_combination() -> Result<(), String> {
let owner = Wallet::new();
let heir = Wallet::new();
let lawyer = Wallet::new();
// Normal: 2-of-2 (owner + heir)
let normal_multisig = MultiSigAddress::new(
2,
vec![owner.public_key.clone(), heir.public_key.clone()]
)?;
// After 1 year: 2-of-3 (any two parties)
let emergency_multisig = MultiSigAddress::new(
2,
vec![owner.public_key, heir.public_key, lawyer.public_key]
)?;
let one_year = 365 * 24 * 3600;
let timelock = TimeLock::new_time_based(current_timestamp() + one_year);
println!("=== TimeLock + MultiSig Combination ===");
println!("\nNormal period (first year):");
println!(" Multi-sig address: {}", &normal_multisig.address[..16]);
println!(" Requirement: owner + heir (2-of-2)");
println!("\nEmergency period (after one year):");
println!(" Multi-sig address: {}", &emergency_multisig.address[..16]);
println!(" Requirement: any two parties (2-of-3)");
println!(" Possible combinations:");
println!(" - owner + heir");
println!(" - owner + lawyer");
println!(" - heir + lawyer");
Ok(())
}
}
Technical Details
nLockTime Field
In actual Bitcoin transactions:
#![allow(unused)]
fn main() {
struct Transaction {
version: u32,
inputs: Vec<TxInput>,
outputs: Vec<TxOutput>,
locktime: u32, // Time lock field
}
}
Rules:
if locktime < 500,000,000:
# Block height mode
if current_block_height >= locktime:
can confirm
else:
reject
else:
# Timestamp mode
if current_timestamp >= locktime:
can confirm
else:
reject
nSequence and Time Locks
For a time lock to be enabled, nSequence must be < 0xFFFFFFFF:
#![allow(unused)]
fn main() {
// Enable time lock
input.sequence = 0xFFFFFFFD;
// Disable time lock (final transaction)
input.sequence = 0xFFFFFFFF;
}
Security Considerations
1. Timestamp Manipulation
Problem: Miners may manipulate block timestamps
Constraints:
- Timestamp cannot be earlier than the median of the previous 11 blocks
- Cannot be more than 2 hours later than the current time
Recommendation: Using block height is more reliable
2. Emergencies
Problem: A time lock cannot be cancelled
Solutions:
#![allow(unused)]
fn main() {
// Solution 1: Use RBF to replace before expiry
if !timelock.is_mature(...) && need_cancel {
replace_with_non_locked_tx();
}
// Solution 2: Double spend (before expiry)
create_alternative_tx_without_timelock();
}
3. Key Loss
Problem: Key lost before expiry
Recommendation:
- Use multi-sig to reduce risk
- Back up keys
- Set up recovery mechanisms
Relationship to CLTV/CSV
CheckLockTimeVerify (CLTV)
Introduced in BIP65:
OP_CLTV opcode
Locks a single UTXO
More flexible
nLockTime vs CLTV:
nLockTime: locks the entire transaction
CLTV: locks a single output (more flexible)
CheckSequenceVerify (CSV)
Introduced in BIP112:
OP_CSV opcode
Relative time lock
Calculated from the time the UTXO was created
Best Practices
1. Choose the Right Type
#![allow(unused)]
fn main() {
// Exact date: use timestamp
let birthday = to_timestamp("2025-01-01");
let timelock = TimeLock::new_time_based(birthday);
// Relative delay: use block height
let blocks_1week = 1008; // About 1 week
let timelock = TimeLock::new_block_based(current_height + blocks_1week);
}
2. User-Friendly Time Display
#![allow(unused)]
fn main() {
fn display_timelock_status(timelock: &TimeLock, current_time: u64, current_height: u64) {
if timelock.is_mature(current_time, current_height) {
println!("✓ Unlocked");
} else {
if timelock.is_block_height {
let blocks_left = timelock.locktime - current_height;
let hours = blocks_left * 10 / 60; // Approximately 10 minutes per block
println!("🔒 Locked; {} blocks remaining (approximately {} hours)", blocks_left, hours);
} else {
let seconds_left = timelock.locktime - current_time;
let days = seconds_left / 86400;
println!("🔒 Locked; {} days remaining", days);
}
}
}
}
3. Testing Time Locks
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
#[test]
fn test_timelock() {
let current = 1000000;
let future = 2000000;
let timelock = TimeLock::new_time_based(future);
// Not yet expired
assert!(!timelock.is_mature(current, 0));
// Expired
assert!(timelock.is_mature(future + 1, 0));
}
}
}
References
Summary: Time locks are a key technology for implementing deferred payments and smart contracts. Used appropriately, they enable a wide range of applications such as term deposits, inheritance planning, and salary payments.
Transaction Priority
When the network is congested, thousands of pending transactions may accumulate in the mempool. Miners can only pack approximately 1 MB of data into each block, so a priority mechanism is needed to decide which transactions are confirmed first. This chapter covers how transaction priority is calculated in SimpleBTC, the mempool sorting logic, and the fee recommendation strategy.
Core Concept: Fee Rate
Fee rate is the most important metric for measuring transaction priority:
Fee Rate (sat/byte) = Fee (satoshi) / Transaction Size (bytes)
Miners preferentially select high-fee-rate transactions to pack into blocks, because doing so maximizes fee income for the same block space.
Why Use Fee Rate Instead of Absolute Fee?
A complex transaction with 10 inputs may pay a fee of 1000 sat but occupies 900 bytes, giving a fee rate of about 1.1 sat/byte. A simple transaction with only 1 input pays 200 sat but occupies only 192 bytes, giving a fee rate of about 1.04 sat/byte. From the miner’s perspective, both are roughly equivalent. If only the absolute fee is considered, the former would be incorrectly prioritized, wasting block space.
Mempool Structure
SimpleBTC’s Mempool uses a dual-index structure to support efficient priority sorting:
#![allow(unused)]
fn main() {
pub struct Mempool {
// Primary storage: txid → mempool entry
transactions: HashMap<String, MempoolEntry>,
// Fee rate index: fee_rate → set of txids (BTreeMap is automatically sorted; efficient iteration)
fee_index: BTreeMap<ordered_float::NotNan<f64>, HashSet<String>>,
// UTXO index: used for double-spend detection
utxo_index: HashMap<String, String>,
// Capacity control
max_size: usize, // Maximum bytes (default 300 MB)
current_size: usize, // Currently used bytes
min_fee_rate: f64, // Minimum accepted fee rate (default 1.0 sat/byte)
max_age: u64, // Maximum retention time (default 72 hours)
}
}
BTreeMap (a balanced binary search tree) is the key: it automatically sorts by fee rate, making the operation “get the top N transactions by fee rate” a simple reverse iteration from the tail, with time complexity O(N).
Mempool Entry: MempoolEntry
#![allow(unused)]
fn main() {
pub struct MempoolEntry {
pub transaction: Transaction, // Full transaction data
pub added_time: u64, // Time added (Unix timestamp)
pub size: usize, // Estimated byte size
pub fee_rate: f64, // Calculated fee rate (sat/byte)
pub replaceable: bool, // Whether RBF replacement is supported
}
}
The fee rate is calculated immediately when a MempoolEntry is created and cached, avoiding repeated calculations:
#![allow(unused)]
fn main() {
impl MempoolEntry {
pub fn new(transaction: Transaction, size: usize) -> Self {
let fee_rate = if size > 0 {
transaction.fee as f64 / size as f64
} else {
0.0
};
// ...
}
}
}
Transaction Size Estimation
SimpleBTC uses a simplified formula to estimate transaction byte size:
#![allow(unused)]
fn main() {
fn estimate_tx_size(&self, tx: &Transaction) -> usize {
let base = 10; // Fixed overhead (version number, lock time, etc.)
let inputs_size = tx.inputs.len() * 148; // About 148 bytes per input
let outputs_size = tx.outputs.len() * 34; // About 34 bytes per output
base + inputs_size + outputs_size
}
}
Real Bitcoin transaction size reference (native SegWit, P2WPKH format):
| Transaction Type | Inputs | Outputs | Estimated Size |
|---|---|---|---|
| Simple transfer | 1 | 2 | ~192 bytes |
| Consolidate multiple UTXOs | 5 | 2 | ~898 bytes |
| Batch payment | 1 | 10 | ~388 bytes |
Transaction Addition and Validation Flow
When mempool.add_transaction(tx) is called, the following checks are executed in order internally:
Transaction arrives at mempool
│
▼
① Already exists? ──Yes──► Reject (duplicate transaction)
│No
▼
② Basic security validation (format, signature, etc.)
│
▼
③ Double-spend detection: has any input already been spent by another mempool transaction?
│
├─ Yes, and old transaction supports RBF and new fee is higher ──► Trigger RBF replacement; continue
│
└─ Yes, but RBF conditions not met ──► Reject (double-spend attack)
│No
▼
④ Estimate size; calculate fee rate
│
▼
⑤ Fee rate ≥ min_fee_rate? ──No──► Reject (fee rate too low)
│Yes
▼
⑥ Mempool full? ──Yes──► Trigger eviction of low-fee-rate transactions
│
▼
⑦ Add to transactions, fee_index, utxo_index
│
▼
Success
#![allow(unused)]
fn main() {
// Example: add a transaction to the mempool
let mut mempool = Mempool::default(); // 300 MB limit, 1 sat/byte minimum fee rate
let tx = Transaction::new(inputs, outputs, 0, 200); // 200 sat fee
match mempool.add_transaction(tx) {
Ok(()) => println!("Transaction entered mempool"),
Err(e) => println!("Rejected reason: {}", e),
}
}
Priority Sorting and Block Packing
Get Top N by Fee Rate: get_top_transactions
#![allow(unused)]
fn main() {
pub fn get_top_transactions(&self, max_count: usize) -> Vec<Transaction>
}
By reverse-iterating fee_index (BTreeMap from high to low), the highest-fee-rate transactions are quickly retrieved:
#![allow(unused)]
fn main() {
// Iterate from high fee rate to low
for (_fee_rate, txids) in self.fee_index.iter().rev() {
for txid in txids {
if let Some(entry) = self.transactions.get(txid) {
result.push(entry.transaction.clone());
if result.len() >= max_count {
return result;
}
}
}
}
}
#![allow(unused)]
fn main() {
// Usage: miner wants to preview the top 10 best transactions
let top_txs = mempool.get_top_transactions(10);
for tx in &top_txs {
println!("txid: {}, fee: {} sat", tx.id, tx.fee);
}
}
Pack by Block Size Limit: get_transactions_for_block
#![allow(unused)]
fn main() {
pub fn get_transactions_for_block(&self, max_size: usize) -> Vec<Transaction>
}
A more practical block-packing function. It also selects transactions from high fee rate to low, but additionally checks that the accumulated size does not exceed max_size bytes:
#![allow(unused)]
fn main() {
pub fn get_transactions_for_block(&self, max_size: usize) -> Vec<Transaction> {
let mut result = Vec::new();
let mut total_size = 0;
for (_fee_rate, txids) in self.fee_index.iter().rev() {
for txid in txids {
if let Some(entry) = self.transactions.get(txid) {
if total_size + entry.size <= max_size {
result.push(entry.transaction.clone());
total_size += entry.size;
}
}
}
}
result
}
}
#![allow(unused)]
fn main() {
// Usage: pack transactions for a new block (Bitcoin block limit is about 1 MB = 1,000,000 bytes)
let block_txs = mempool.get_transactions_for_block(1_000_000);
println!("Selected {} transactions for packing", block_txs.len());
}
Composite Priority Score
SimpleBTC provides TxPriorityCalculator in src/advanced_tx.rs, implementing more refined priority calculation.
Basic Fee Rate Calculation
#![allow(unused)]
fn main() {
pub fn calculate_fee_rate(fee: u64, size: usize) -> f64 {
if size == 0 { return 0.0; }
fee as f64 / size as f64
}
}
#![allow(unused)]
fn main() {
// 200 sat fee, transaction size 192 bytes
let fee_rate = TxPriorityCalculator::calculate_fee_rate(200, 192);
println!("Fee rate: {:.2} sat/byte", fee_rate); // ~1.04 sat/byte
}
Coin Age Priority
In early Bitcoin (before SegWit), “coin age” was also considered: the UTXO’s value multiplied by the number of blocks it has been waiting, divided by the transaction size:
#![allow(unused)]
fn main() {
/// Priority = (input value × input confirmations) / transaction size
pub fn calculate_priority(
input_value: u64, // Total value of inputs (satoshi)
input_age: u32, // Number of blocks the input UTXO has been confirmed
tx_size: usize,
) -> f64 {
(input_value as f64 * input_age as f64) / tx_size as f64
}
}
#![allow(unused)]
fn main() {
// Example: input value 1 BTC = 100,000,000 sat, confirmed 100 blocks, transaction size 200 bytes
let priority = TxPriorityCalculator::calculate_priority(100_000_000, 100, 200);
println!("Coin age priority: {:.0}", priority); // 50,000,000
}
Historical context: Bitcoin Core removed coin-age-based free transaction priority in version 0.12 (2016), because low-fee transactions significantly slowed down block packing. In the modern network, fee rate is the only priority metric that actually matters.
Composite Score Formula: 70% Fee Rate + 30% Coin Age
#![allow(unused)]
fn main() {
/// Composite score = fee_rate × 0.7 + priority × 0.001 × 0.3
pub fn calculate_score(fee_rate: f64, priority: f64) -> f64 {
fee_rate * 0.7 + priority * 0.001 * 0.3
}
}
Design rationale of this weighted formula:
- 70% weight for fee rate: ensures miner revenue maximization; high-fee-rate transactions still take priority
- 30% weight for coin age (scaled by 0.001): gives a “bonus” to long-waiting transactions, preventing low-fee-rate old UTXOs from never being confirmed
#![allow(unused)]
fn main() {
// Full scoring example
let fee_rate = TxPriorityCalculator::calculate_fee_rate(500, 200); // 2.5 sat/byte
let priority = TxPriorityCalculator::calculate_priority(50_000_000, 10, 200); // 2,500,000
let score = TxPriorityCalculator::calculate_score(fee_rate, priority);
println!("Composite score: {:.4}", score);
// score = 2.5 * 0.7 + 2_500_000 * 0.001 * 0.3 = 1.75 + 750 = 751.75
}
Fee Recommendations
TxPriorityCalculator::recommend_fee returns a suggested fee based on urgency:
#![allow(unused)]
fn main() {
pub enum FeeUrgency {
Low, // Low priority: confirmed within a few hours
Medium, // Medium priority: confirmed within 30–60 minutes
High, // High priority: 10–20 minutes (approximately 1–2 blocks)
Urgent, // Urgent: next block (highest priority)
}
pub fn recommend_fee(tx_size: usize, urgency: FeeUrgency) -> u64 {
let sat_per_byte = match urgency {
FeeUrgency::Low => 1.0, // 1 sat/byte
FeeUrgency::Medium => 5.0, // 5 sat/byte
FeeUrgency::High => 20.0, // 20 sat/byte
FeeUrgency::Urgent => 50.0, // 50 sat/byte
};
(tx_size as f64 * sat_per_byte) as u64
}
}
#![allow(unused)]
fn main() {
use bitcoin_simulation::advanced_tx::{TxPriorityCalculator, FeeUrgency};
// Estimate the suggested fee for a standard transaction (1 input, 2 outputs)
let tx_size = 10 + 1 * 148 + 2 * 34; // = 226 bytes
let low_fee = TxPriorityCalculator::recommend_fee(tx_size, FeeUrgency::Low);
let medium_fee = TxPriorityCalculator::recommend_fee(tx_size, FeeUrgency::Medium);
let high_fee = TxPriorityCalculator::recommend_fee(tx_size, FeeUrgency::High);
let urgent_fee = TxPriorityCalculator::recommend_fee(tx_size, FeeUrgency::Urgent);
println!("Low priority: {} sat ({} sat/byte)", low_fee, 1); // 226 sat
println!("Medium priority: {} sat ({} sat/byte)", medium_fee, 5); // 1130 sat
println!("High priority: {} sat ({} sat/byte)", high_fee, 20); // 4520 sat
println!("Urgent: {} sat ({} sat/byte)", urgent_fee, 50); // 11300 sat
}
Real Bitcoin network fee rate reference (2024 data, BTC/USD = $60,000):
| Urgency | Typical Fee Rate | Approximate USD (226-byte transaction) |
|---|---|---|
| Low | 1–3 sat/byte | $0.14 – $0.41 |
| Medium | 5–15 sat/byte | $0.68 – $2.03 |
| High | 20–50 sat/byte | $2.71 – $6.78 |
| Urgent | 50–200 sat/byte | $6.78 – $27.1 |
Note: Actual fee rates are heavily influenced by network congestion. At the peak of the 2017 bull market, some users paid fees exceeding $50 for fast confirmation.
Low-Fee-Rate Transaction Eviction
When the mempool reaches its capacity limit, it automatically evicts the lowest-fee-rate transactions to free up space:
#![allow(unused)]
fn main() {
fn evict_low_fee_transactions(&mut self, needed_size: usize) -> Result<()> {
let mut freed_size = 0;
let mut to_remove = Vec::new();
// Iterate from [low fee rate to high fee rate] (BTreeMap forward iteration)
for (_fee_rate, txids) in self.fee_index.iter() {
for txid in txids {
if let Some(entry) = self.transactions.get(txid) {
to_remove.push(txid.clone());
freed_size += entry.size;
if freed_size >= needed_size {
break;
}
}
}
if freed_size >= needed_size { break; }
}
// Execute eviction
for txid in &to_remove {
self.remove_transaction(txid)?;
}
Ok(())
}
}
This design ensures the mempool always maintains the subset of “highest-fee-rate” transactions; low-fee-rate transactions are naturally eliminated through competition.
#![allow(unused)]
fn main() {
// Create a very small mempool to demonstrate eviction behavior
let mut mempool = Mempool::new(1000, 1.0); // Only 1 KB capacity
// Add multiple transactions; when over 1 KB, low-fee-rate ones are evicted
for i in 1..=10 {
let tx = create_tx_with_fee(i * 100); // fee: 100, 200, ..., 1000
let _ = mempool.add_transaction(tx); // Low-fee-rate ones may be evicted
}
}
Expired Transaction Cleanup
By default, transactions that have been waiting in the mempool for more than 72 hours are cleared:
#![allow(unused)]
fn main() {
// Call periodically (e.g., once per hour)
let expired_count = mempool.clear_expired();
if expired_count > 0 {
println!("Cleared {} expired transactions", expired_count);
}
}
Mempool Statistics
#![allow(unused)]
fn main() {
let stats = mempool.get_stats();
println!("Pending transaction count: {}", stats.tx_count);
println!("Mempool size: {} / {} bytes", stats.total_size, stats.max_size);
println!("Total pending fees: {} sat", stats.total_fees);
println!("Average fee rate: {:.2} sat/byte", stats.avg_fee_rate);
println!("Minimum accepted fee rate: {:.2} sat/byte", stats.min_fee_rate);
}
Replace-By-Fee (RBF)
RBF (BIP125) allows users to replace an old transaction in the mempool with a new one carrying a higher fee. In SimpleBTC, RBFManager in advanced_tx.rs manages replaceable transactions:
#![allow(unused)]
fn main() {
use bitcoin_simulation::advanced_tx::RBFManager;
let mut rbf = RBFManager::new();
// Mark a transaction as replaceable (set sequence < 0xFFFFFFFE when sending)
rbf.mark_replaceable("original_tx_id");
// Later, replace the old transaction with a new one bearing a higher fee
let can_replace = rbf.can_replace(&old_tx, &new_tx);
match can_replace {
Ok(()) => println!("RBF replacement successful"),
Err(reason) => println!("Replacement rejected: {}", reason),
}
}
RBF replacement conditions (verified by can_replace):
- The old transaction must have been marked as replaceable (
replaceable = true) - The number of inputs in the new and old transactions must be the same, referencing the same UTXOs
- The new transaction’s fee must be strictly higher than the old transaction’s
- The fee increment must be at least the size of the old transaction (approximately 1 sat/byte)
Summary
SimpleBTC’s transaction priority system consists of three layers:
| Layer | Component | Role |
|---|---|---|
| Mempool sorting | Mempool + BTreeMap<fee_rate> | Automatically maintains a sorted queue by fee rate |
| Priority calculation | TxPriorityCalculator | Fee rate, coin age, composite score |
| Fee recommendation | FeeUrgency + recommend_fee | Recommends a reasonable fee by urgency level |
Core formula recap:
Fee Rate (sat/byte) = fee / transaction size
Composite Score = fee_rate × 0.7 + coin_age_priority × 0.001 × 0.3
Recommended Fee = transaction size × sat_per_byte (select 1/5/20/50 by urgency)
Enterprise Multisig Wallet in Practice
This example demonstrates how to use 2-of-3 multisig to manage corporate funds.
Scenario Description
A technology company needs to manage its bitcoin assets with the following requirements:
- Three executives (CEO, CFO, CTO) each hold one key
- Any two executives can authorize a transfer
- Prevent misuse or loss of control by a single person
- Normal operations continue even if one executive is unavailable
Run the Example
cargo run --example enterprise_multisig
Code Walkthrough
1. Initialization
use bitcoin_simulation::{
blockchain::Blockchain,
wallet::Wallet,
multisig::MultiSigAddress,
};
fn main() -> Result<(), String> {
println!("=== Enterprise Multisig Wallet Demo ===\n");
// Create the blockchain
let mut blockchain = Blockchain::new();
// Create wallets for the three executives
let ceo = Wallet::new();
let cfo = Wallet::new();
let cto = Wallet::new();
println!("Created executive wallets:");
println!(" CEO: {}", &ceo.address[..20]);
println!(" CFO: {}", &cfo.address[..20]);
println!(" CTO: {}\n", &cto.address[..20]);
2. Create the Multisig Address
#![allow(unused)]
fn main() {
// Create a 2-of-3 multisig address
let company_multisig = MultiSigAddress::new(
2, // requires 2 signatures
vec![
ceo.public_key.clone(),
cfo.public_key.clone(),
cto.public_key.clone(),
]
).expect("Failed to create multisig address");
println!("Company multisig address created:");
println!(" Address: {}", &company_multisig.address[..20]);
println!(" Type: {}-of-{} multisig",
company_multisig.required_sigs,
company_multisig.total_keys);
println!(" Rule: any 2 executives can authorize a transfer\n");
}
Key points:
required_sigs = 2: requires 2 signaturestotal_keys = 3: 3 total keys- Any combination of two executives works: CEO+CFO, CEO+CTO, or CFO+CTO
3. Fund the Address
#![allow(unused)]
fn main() {
// Fund the company multisig address
println!("--- Scenario 1: Company receives investment ---");
let investor = Wallet::new();
println!("Investor address: {}\n", &investor.address[..20]);
// Create a funding transaction (from the genesis address)
let funding_tx = blockchain.create_transaction(
&Wallet::from_address("genesis_address".to_string()),
company_multisig.address.clone(),
100000, // 100,000 satoshi
0,
)?;
blockchain.add_transaction(funding_tx)?;
blockchain.mine_pending_transactions(investor.address.clone())?;
let company_balance = blockchain.get_balance(&company_multisig.address);
println!("Funding complete");
println!(" Company account balance: {} satoshi\n", company_balance);
}
4. Scenario Demo: Normal Expenditure
#![allow(unused)]
fn main() {
println!("--- Scenario 2: Normal payment (approved by CEO + CFO) ---");
let supplier = Wallet::new();
println!("Supplier address: {}\n", &supplier.address[..20]);
// Simulate the multisig flow
let payment_amount = 30000;
let payment_data = format!("{}{}", company_multisig.address, supplier.address);
// Step 1: CEO signs
let ceo_signature = ceo.sign(&payment_data);
println!("CEO has approved and signed");
// Step 2: CFO signs
let cfo_signature = cfo.sign(&payment_data);
println!("CFO has approved and signed");
// Step 3: Verify signature count
let signatures = vec![ceo_signature, cfo_signature];
if signatures.len() >= company_multisig.required_sigs {
println!("Signature count satisfies requirement (2/3)");
println!("Transaction can be executed\n");
// Execute the transfer
let payment_tx = blockchain.create_transaction(
&Wallet::from_address(company_multisig.address.clone()),
supplier.address.clone(),
payment_amount,
100,
)?;
blockchain.add_transaction(payment_tx)?;
blockchain.mine_pending_transactions(ceo.address.clone())?;
println!("Payment complete");
println!(" Amount paid: {} satoshi", payment_amount);
println!(" Company balance: {} satoshi\n",
blockchain.get_balance(&company_multisig.address));
}
}
Workflow:
- CEO initiates a payment request
- CEO signs with their private key
- CFO reviews and signs
- The system verifies the signature count (2 ≥ required 2)
- The transfer is executed
5. Scenario Demo: CEO Unavailable
#![allow(unused)]
fn main() {
println!("--- Scenario 3: Emergency payment while CEO is traveling (CFO + CTO) ---");
let emergency_vendor = Wallet::new();
println!("Emergency vendor: {}\n", &emergency_vendor.address[..20]);
let emergency_amount = 20000;
let emergency_data = format!("{}{}",
company_multisig.address, emergency_vendor.address);
println!("CEO is traveling and cannot be reached");
println!("CFO and CTO decide to approve the emergency payment\n");
// CFO signs
let cfo_sig = cfo.sign(&emergency_data);
println!("CFO has signed");
// CTO signs
let cto_sig = cto.sign(&emergency_data);
println!("CTO has signed");
let emergency_sigs = vec![cfo_sig, cto_sig];
if emergency_sigs.len() >= company_multisig.required_sigs {
println!("Signatures satisfy requirement (2/3)");
println!("Business continues normally even without the CEO\n");
// Execute the transfer
let emergency_tx = blockchain.create_transaction(
&Wallet::from_address(company_multisig.address.clone()),
emergency_vendor.address,
emergency_amount,
100,
)?;
blockchain.add_transaction(emergency_tx)?;
blockchain.mine_pending_transactions(cfo.address.clone())?;
println!("Emergency payment complete");
println!(" Final balance: {} satoshi\n",
blockchain.get_balance(&company_multisig.address));
}
Ok(())
}
}
Sample Output
=== Enterprise Multisig Wallet Demo ===
Created executive wallets:
CEO: a3f2d8c9e4b7f1a8...
CFO: b9e4c7d2a3f1e8b6...
CTO: c8f1e9d3b4a7c2e5...
Company multisig address created:
Address: 3Mf2d8c9e4b7f1a8...
Type: 2-of-3 multisig
Rule: any 2 executives can authorize a transfer
--- Scenario 1: Company receives investment ---
Investor address: d7c2e8f3a9b1d4c6...
Block mined: 0003ab4f9c2d...
Funding complete
Company account balance: 100000 satoshi
--- Scenario 2: Normal payment (approved by CEO + CFO) ---
Supplier address: e6d1f8c2b9a3e7d4...
CEO has approved and signed
CFO has approved and signed
Signature count satisfies requirement (2/3)
Transaction can be executed
Block mined: 0007c3e8d1a9...
Payment complete
Amount paid: 30000 satoshi
Company balance: 69900 satoshi
--- Scenario 3: Emergency payment while CEO is traveling (CFO + CTO) ---
Emergency vendor: f5e2d9c3a8b7f1e6...
CEO is traveling and cannot be reached
CFO and CTO decide to approve the emergency payment
CFO has signed
CTO has signed
Signatures satisfy requirement (2/3)
Business continues normally even without the CEO
Block mined: 000ab7e4f2c8...
Emergency payment complete
Final balance: 49800 satoshi
Business Value
1. Security
| Traditional Single-Sig | Enterprise Multisig |
|---|---|
| CEO’s private key stolen → all funds lost | Requires 2 keys; theft of one is harmless |
| Single point of failure | Distributed risk |
| High risk of insider fraud | Requires two people to collude |
2. Business Continuity
| Scenario | Traditional Approach | Multisig Approach |
|---|---|---|
| CEO on vacation | Business halts | CFO+CTO continue operations |
| Executive resigns | Must transfer all funds | Replace one key |
| Emergency payment | Key holder unavailable | Any 2 people can approve |
3. Compliance
Audit trail:
- Every transaction requires 2 signatures
- Clear record of who approved what
- Satisfies internal control requirements
- Meets financial audit standards
Extended Solutions
Tiered Authorization
#![allow(unused)]
fn main() {
// Small amounts: manager level 2-of-3
if amount < 10000 {
let managers_multisig = MultiSigAddress::new(2, manager_keys)?;
}
// Medium amounts: executive level 2-of-3
else if amount < 100000 {
let exec_multisig = MultiSigAddress::new(2, exec_keys)?;
}
// Large amounts: board 5-of-9
else {
let board_multisig = MultiSigAddress::new(5, board_keys)?;
}
}
Timelock Protection
#![allow(unused)]
fn main() {
use bitcoin_simulation::advanced_tx::TimeLock;
// Large transfers require a 24-hour delay
let timelock = TimeLock::new_time_based(
current_time() + 24 * 3600
);
// Can be cancelled during the delay period
// Protects against coerced transfers
}
Emergency Recovery
#![allow(unused)]
fn main() {
// Normal: 2-of-3
let normal_multisig = MultiSigAddress::new(
2,
vec![ceo_key, cfo_key, cto_key]
)?;
// Emergency (2 keys lost): attorney-custodied recovery key
let recovery_multisig = MultiSigAddress::new(
1,
vec![lawyer_key] // requires legal documentation
)?;
}
Implementation Recommendations
1. Key Management
CEO key:
- Primary: mobile hot wallet (daily signing)
- Backup: hardware wallet (safe)
CFO key:
- Primary: desktop hot wallet (office)
- Backup: paper wallet (bank safe deposit box)
CTO key:
- Primary: hardware wallet (carried personally)
- Backup: encrypted USB drive (offsite storage)
2. Operating Procedure
1. Initiator creates transfer request
2. Initiator signs
3. Notify the second approver
4. Second approver reviews and signs
5. System automatically verifies signature count
6. Execute transaction and notify everyone
7. Record audit log
3. Security Checklist
- Keys stored in separate locations
- Regularly test the recovery process
- Back up all keys
- Set amount thresholds
- Enable transaction notifications
- Regularly audit transaction records
- Prepare a contingency plan for key loss
- Train all key holders
Related Resources
Summary
The enterprise multisig wallet achieves the following through a 2-of-3 mechanism:
Security — no single point of failure Flexibility — any two people can approve Continuity — operations continue even if one person is absent Compliance — satisfies internal controls Transparency — all operations are traceable
This is a best practice for enterprises managing digital assets.
Escrow Service in Practice
This example demonstrates how to implement a Bitcoin escrow service using 2-of-3 multisig.
Scenario Description
In e-commerce transactions, buyers and sellers don’t trust each other and need a third-party escrow:
- Buyer’s concern: pays but the seller doesn’t ship
- Seller’s concern: ships but the buyer doesn’t pay
- Solution: funds held in a 2-of-3 multisig address
Participants:
- Buyer
- Seller
- Arbitrator
Rules:
- Normal transaction: Buyer + Seller sign → funds go to Seller
- Dispute resolution: Buyer/Seller + Arbitrator → outcome per arbitration ruling
Run the Example
cargo run --example escrow_service
Code Walkthrough
1. Initialize Participants
use bitcoin_simulation::{
blockchain::Blockchain,
wallet::Wallet,
multisig::MultiSigAddress,
};
fn main() -> Result<(), String> {
println!("=== Bitcoin Escrow Service Demo ===\n");
// Create the blockchain
let mut blockchain = Blockchain::new();
// Create participant wallets
let buyer = Wallet::new();
let seller = Wallet::new();
let arbitrator = Wallet::new();
println!("Participants created:");
println!(" Buyer: {}", &buyer.address[..20]);
println!(" Seller: {}", &seller.address[..20]);
println!(" Arbitrator: {}\n", &arbitrator.address[..20]);
2. Create the Escrow Multisig Address
#![allow(unused)]
fn main() {
// Create a 2-of-3 escrow multisig address
let escrow_multisig = MultiSigAddress::new(
2, // requires 2 signatures
vec![
buyer.public_key.clone(),
seller.public_key.clone(),
arbitrator.public_key.clone(),
]
).expect("Failed to create multisig address");
println!("Escrow address created:");
println!(" Address: {}", &escrow_multisig.address[..20]);
println!(" Type: {}-of-{} multisig",
escrow_multisig.required_sigs,
escrow_multisig.total_keys);
println!(" Rule: any 2 parties can sign");
println!(" Possible combinations:");
println!(" - Buyer + Seller (normal transaction)");
println!(" - Buyer + Arbitrator (refund to buyer)");
println!(" - Seller + Arbitrator (payment to seller)\n");
}
Key points:
- 2-of-3 ensures no single party has unilateral control
- In normal cases, buyer and seller resolve directly
- The arbitrator intervenes only in disputes
3. Buyer Deposits Funds
#![allow(unused)]
fn main() {
println!("--- Scenario 1: Buyer deposits escrow funds ---");
// Buyer receives initial funds
let funding_tx = blockchain.create_transaction(
&Wallet::from_address("genesis_address".to_string()),
buyer.address.clone(),
100000, // 100,000 satoshi
0,
)?;
blockchain.add_transaction(funding_tx)?;
blockchain.mine_pending_transactions(buyer.address.clone())?;
let buyer_initial = blockchain.get_balance(&buyer.address);
println!("Buyer balance: {} sat\n", buyer_initial);
// Buyer transfers payment to the escrow address
let escrow_amount = 50000; // 50,000 sat
println!("Item price: {} sat", escrow_amount);
println!("Buyer is transferring payment to the escrow address...\n");
let deposit_tx = blockchain.create_transaction(
&buyer,
escrow_multisig.address.clone(),
escrow_amount,
100, // fee
)?;
blockchain.add_transaction(deposit_tx)?;
blockchain.mine_pending_transactions(buyer.address.clone())?;
let escrow_balance = blockchain.get_balance(&escrow_multisig.address);
println!("Funds escrowed");
println!(" Escrow amount: {} sat", escrow_balance);
println!(" Buyer balance: {} sat\n", blockchain.get_balance(&buyer.address));
}
Flow:
- Buyer obtains initial funds
- Buyer transfers payment to the escrow address
- Funds are locked in the multisig address
- Seller sees the successful escrow and ships
4. Scenario A: Normal Transaction Completed
#![allow(unused)]
fn main() {
println!("--- Scenario 2A: Normal transaction (buyer satisfied) ---");
println!("Seller has shipped");
println!("Buyer received the item and is satisfied\n");
// Both buyer and seller sign to release funds to the seller
let payment_amount = escrow_balance - 50; // deduct fee
let payment_data = format!("{}{}{}",
escrow_multisig.address,
seller.address,
payment_amount);
println!("Signing process:");
// Buyer signs
let buyer_signature = buyer.sign(&payment_data);
println!(" Buyer has signed (confirming receipt)");
// Seller signs
let seller_signature = seller.sign(&payment_data);
println!(" Seller has signed (accepting payment)");
// Verify signature count
let signatures = vec![buyer_signature, seller_signature];
if signatures.len() >= escrow_multisig.required_sigs {
println!("\nSignatures satisfy requirement (2/3)");
println!("Releasing funds to seller\n");
// Create the payment transaction
let payment_tx = blockchain.create_transaction(
&Wallet::from_address(escrow_multisig.address.clone()),
seller.address.clone(),
payment_amount,
50,
)?;
blockchain.add_transaction(payment_tx)?;
blockchain.mine_pending_transactions(seller.address.clone())?;
println!("=== Transaction Complete ===");
println!("Seller balance: {} sat", blockchain.get_balance(&seller.address));
println!("Escrow balance: {} sat", blockchain.get_balance(&escrow_multisig.address));
}
}
Normal flow:
- Seller ships
- Buyer confirms receipt
- Buyer signs (confirming satisfaction)
- Seller signs (accepting payment)
- 2 signatures satisfy the requirement
- Funds released to seller
5. Scenario B: Dispute Resolution
#![allow(unused)]
fn main() {
println!("\n--- Scenario 2B: Dispute resolution (item has a problem) ---");
// Re-create the scenario (hypothetical)
let escrow_multisig_dispute = MultiSigAddress::new(
2,
vec![
buyer.public_key.clone(),
seller.public_key.clone(),
arbitrator.public_key,
]
)?;
println!("Buyer: item doesn't match the description, requesting a refund");
println!("Seller: item is fine, refusing refund");
println!("Arbitrator intervenes to investigate...\n");
println!("Arbitration ruling:");
println!(" Investigation confirms the item has a problem");
println!(" Decision: refund the buyer\n");
// Buyer + Arbitrator sign
let refund_data = format!("{}{}{}",
escrow_multisig_dispute.address,
buyer.address,
payment_amount);
println!("Signing process:");
let buyer_sig_dispute = buyer.sign(&refund_data);
println!(" Buyer has signed (agreeing to refund)");
let arbitrator_sig = arbitrator.sign(&refund_data);
println!(" Arbitrator has signed (executing ruling)");
let dispute_sigs = vec![buyer_sig_dispute, arbitrator_sig];
if dispute_sigs.len() >= escrow_multisig_dispute.required_sigs {
println!("\nSignatures satisfy requirement (2/3)");
println!("Executing refund\n");
println!("=== Dispute Resolved ===");
println!("Refund to buyer: {} sat", payment_amount);
println!("Arbitration fee: 50 sat (deducted from escrow)");
}
Ok(())
}
}
Dispute flow:
- Buyer reports a problem with the item
- Seller refuses a refund
- Arbitrator investigates
- Arbitrator issues a ruling
- Buyer + Arbitrator sign
- Funds returned to buyer
Sample Output
=== Bitcoin Escrow Service Demo ===
Participants created:
Buyer: a3f2d8c9e4b7f1a8...
Seller: b9e4c7d2a3f1e8b6...
Arbitrator: c8f1e9d3b4a7c2e5...
Escrow address created:
Address: 3Mf2d8c9e4b7f1a8...
Type: 2-of-3 multisig
Rule: any 2 parties can sign
Possible combinations:
- Buyer + Seller (normal transaction)
- Buyer + Arbitrator (refund to buyer)
- Seller + Arbitrator (payment to seller)
--- Scenario 1: Buyer deposits escrow funds ---
Buyer balance: 100000 sat
Item price: 50000 sat
Buyer is transferring payment to the escrow address...
Funds escrowed
Escrow amount: 50000 sat
Buyer balance: 49900 sat
--- Scenario 2A: Normal transaction (buyer satisfied) ---
Seller has shipped
Buyer received the item and is satisfied
Signing process:
Buyer has signed (confirming receipt)
Seller has signed (accepting payment)
Signatures satisfy requirement (2/3)
Releasing funds to seller
=== Transaction Complete ===
Seller balance: 49950 sat
Escrow balance: 0 sat
--- Scenario 2B: Dispute resolution (item has a problem) ---
Buyer: item doesn't match the description, requesting a refund
Seller: item is fine, refusing refund
Arbitrator intervenes to investigate...
Arbitration ruling:
Investigation confirms the item has a problem
Decision: refund the buyer
Signing process:
Buyer has signed (agreeing to refund)
Arbitrator has signed (executing ruling)
Signatures satisfy requirement (2/3)
Executing refund
=== Dispute Resolved ===
Refund to buyer: 49950 sat
Arbitration fee: 50 sat (deducted from escrow)
Business Value
1. Buyer Protection
| Traditional Transaction | Escrow Service |
|---|---|
| Pay and seller doesn’t ship | Funds escrowed; released only after shipping |
| Item doesn’t match; no refund | Arbitrator can rule for a refund |
| No recourse for disputes | Arbitration mechanism protects rights |
2. Seller Protection
| Traditional Transaction | Escrow Service |
|---|---|
| Ships but buyer refuses to pay | Payment escrowed; ship normally |
| Malicious refund requests | Arbitrator makes a fair judgment |
| No guaranteed payment | Payment certainty |
3. Fairness
Buyer alone cannot withdraw funds (needs seller or arbitrator)
Seller alone cannot withdraw funds (needs buyer or arbitrator)
Arbitrator alone cannot withdraw funds (needs buyer or seller)
→ Three-party checks and balances — fair and impartial
Extended Solutions
1. Automated Arbitration
#![allow(unused)]
fn main() {
struct AutoArbitration {
logistics_tracking: bool,
photo_evidence: Vec<String>,
chat_records: Vec<Message>,
}
fn auto_judge(evidence: &AutoArbitration) -> Decision {
if evidence.logistics_tracking && evidence.photo_evidence.len() > 3 {
Decision::RefundBuyer // automatic refund
} else {
Decision::ManualReview // human review
}
}
}
2. Staged Release
#![allow(unused)]
fn main() {
// Stage 1: shipping confirmed — release 50%
// Stage 2: receipt confirmed — release remaining 50%
let stage1 = escrow_amount / 2;
let stage2 = escrow_amount - stage1;
// Seller provides tracking number → release stage1
// Buyer confirms receipt → release stage2
}
3. Timelock Protection
#![allow(unused)]
fn main() {
use bitcoin_simulation::advanced_tx::TimeLock;
// If no dispute within 7 days, automatically release to seller
let seven_days = 7 * 24 * 3600;
let auto_release = TimeLock::new_time_based(current_time + seven_days);
if auto_release.is_mature(...) && no_dispute {
release_to_seller();
}
}
4. Multi-Tier Arbitration
#![allow(unused)]
fn main() {
// Level 1 arbitration: standard arbitrator
// Level 2 arbitration: senior arbitrator
// Level 3 arbitration: arbitration committee (3-of-5)
let appeals_committee = MultiSigAddress::new(
3,
vec![arbitrator1, arbitrator2, arbitrator3, arbitrator4, arbitrator5]
)?;
}
Arbitrator Mechanism
Selection Criteria
Good reputation (track record)
Subject matter expertise (product category)
Neutral and impartial (no conflicts of interest)
Responsive (within 24 hours)
Arbitration Fees
#![allow(unused)]
fn main() {
let arbitration_fee = match dispute_complexity {
Simple => 50, // 0.1%
Medium => 100, // 0.2%
Complex => 500, // 1%
};
// Deducted from the escrow amount
let net_amount = escrow_amount - arbitration_fee;
}
Arbitration Process
1. Buyer or seller initiates dispute
2. Submit evidence (photos, chat records)
3. Arbitrator reviews (within 3 business days)
4. Issue a ruling
5. Execute ruling (sign)
6. Collect arbitration fee
Security Considerations
1. Arbitrator Collusion
Risk: arbitrator colludes with buyer or seller
Protection:
#![allow(unused)]
fn main() {
// Arbitrator must post collateral
let arbitrator_deposit = 100000;
// Collusion detected → slash collateral
if collusion_detected {
slash_deposit(&arbitrator);
ban_arbitrator(&arbitrator);
}
// Multiple arbitrators vote
let arbitrators = vec![arb1, arb2, arb3];
let decision = majority_vote(&arbitrators);
}
2. Evidence Fabrication
Protection:
#![allow(unused)]
fn main() {
// Logistics information on-chain
blockchain.add_tracking_info(tracking_number);
// Photo hash on-chain (tamper-proof)
let photo_hash = hash_photo(photo);
blockchain.add_evidence_hash(photo_hash);
// Timestamp proof
let timestamp = blockchain.get_block_time();
}
3. Malicious Delays
Protection:
#![allow(unused)]
fn main() {
// Set arbitration deadline
let deadline = current_time + 7 * 86400; // 7 days
if current_time > deadline && no_decision {
// Timeout → automatic refund
refund_to_buyer();
}
}
Implementation Recommendations
1. Tech Stack
Frontend: web interface to display escrow flow
Backend: SimpleBTC + database
Storage: evidence storage (IPFS)
Notifications: email/SMS alerts
2. User Flow
Buyer:
1. Browse items
2. Place order and transfer payment to escrow
3. Wait for seller to ship
4. Receive item and confirm
5. Sign to release funds
Seller:
1. Wait for buyer to deposit escrow funds
2. Ship upon seeing successful escrow
3. Provide tracking number
4. Wait for buyer to confirm
5. Sign to receive payment
3. Fee Structure
Platform fee: 1%
Arbitration fee: 0.1–1% (in disputes)
Blockchain transaction fee: dynamic (50–200 sat)
Comparison with Traditional Solutions
vs. Alipay Escrow
| Feature | SimpleBTC Escrow | Alipay Escrow |
|---|---|---|
| Decentralized | Yes | No (centralized) |
| Censorship resistance | Yes | No (can be censored) |
| Cross-border payments | Yes | Restricted |
| Fee | Low (0.1–1%) | Higher (1–3%) |
| Privacy | Better | Worse |
vs. PayPal Disputes
| Feature | SimpleBTC Escrow | PayPal |
|---|---|---|
| Dispute resolution | Arbitrator | Platform support |
| Transparency | On-chain, publicly verifiable | Black-box process |
| Irreversibility | Yes | No (can freeze accounts) |
Related Resources
Summary
The escrow service achieves the following through 2-of-3 multisig:
Buyer protection — refunds available if item doesn’t match Seller protection — guaranteed receipt of payment Fair arbitration — impartial third-party ruling Decentralized — no need to trust a central platform Transparent — on-chain records are publicly accessible
This is an ideal solution for e-commerce, freelancing, and cross-border trade.
Time Deposit System
This example demonstrates how to use the TimeLock feature to implement a time deposit system. Users can choose from different terms (3 months or 1 year); funds cannot be withdrawn before maturity, and principal plus interest can be collected upon maturity.
Business Scenario
Pain points of traditional time deposits:
- Early withdrawal requires bank approval
- Interest calculation is opaque
- Requires trusting a bank
- Manual action needed at maturity
Blockchain solution:
- Smart contract executes automatically — no approval needed
- Interest rules written into code — fully transparent
- Technically impossible to withdraw before maturity (trustless)
- Automatically unlocks at maturity
System Architecture
Product Design
| Product | Term | Annual Rate | Minimum Amount | Maturity |
|---|---|---|---|---|
| Short-Term Saver | 3 months | 3% | 1,000 satoshi | Automatic unlock |
| Steady Earner | 1 year | 5% | 5,000 satoshi | Automatic unlock |
Time Calculation
#![allow(unused)]
fn main() {
// 3-month term (approximately 13 weeks, 91 days)
const BLOCKS_PER_3_MONTHS: u64 = 13 * 7 * 144; // 13,104 blocks
// 1-year term (approximately 52 weeks, 365 days)
const BLOCKS_PER_YEAR: u64 = 52 * 7 * 144; // 52,416 blocks
// Note: Bitcoin averages one block every 10 minutes, approximately 144 blocks per day
}
Complete Implementation
The following is the complete time deposit system code:
use bitcoin_simulation::{
blockchain::Blockchain,
wallet::Wallet,
advanced_tx::{TimeLock, TimeLockType},
};
fn main() -> Result<(), String> {
println!("=== Time Deposit System Demo ===\n");
// Initialize the blockchain
let mut blockchain = Blockchain::new();
// Create a user wallet
let user = Wallet::new();
println!("User address: {}...{}", &user.address[..16], &user.address[36..]);
// Give the user initial funds
setup_balance(&mut blockchain, &user, 20000)?;
println!("Initial balance: {} satoshi\n", blockchain.get_balance(&user.address));
// Scenario 1: 3-month time deposit
println!("--- Scenario 1: 3-month time deposit (3% annual rate) ---");
let amount_3m = 5000;
let rate_3m = 0.03;
let blocks_3m = 13 * 7 * 144; // 13 weeks
// Calculate 3-month interest
let interest_3m = (amount_3m as f64 * rate_3m * 3.0 / 12.0) as u64;
let total_3m = amount_3m + interest_3m;
println!("Deposit amount: {} satoshi", amount_3m);
println!("Expected interest: {} satoshi (3 months @ 3%)", interest_3m);
println!("Total at maturity: {} satoshi", total_3m);
println!("Lock period (blocks): {}", blocks_3m);
// Create the 3-month term
let timelock_3m = TimeLock::new(
TimeLockType::BlockHeight(blockchain.chain.len() as u64 + blocks_3m)
);
let deposit_tx_3m = timelock_3m.create_timelocked_transaction(
&mut blockchain,
&user,
user.address.clone(), // returned to the depositor at maturity
total_3m, // principal + interest
10,
)?;
blockchain.add_transaction(deposit_tx_3m.clone())?;
blockchain.mine_pending_transactions(user.address.clone())?;
println!("3-month deposit created successfully");
println!("Transaction ID: {}...{}\n", &deposit_tx_3m.id[..16], &deposit_tx_3m.id[56..]);
// Scenario 2: 1-year time deposit
println!("--- Scenario 2: 1-year time deposit (5% annual rate) ---");
let amount_1y = 10000;
let rate_1y = 0.05;
let blocks_1y = 52 * 7 * 144; // 52 weeks
// Calculate 1-year interest
let interest_1y = (amount_1y as f64 * rate_1y) as u64;
let total_1y = amount_1y + interest_1y;
println!("Deposit amount: {} satoshi", amount_1y);
println!("Expected interest: {} satoshi (1 year @ 5%)", interest_1y);
println!("Total at maturity: {} satoshi", total_1y);
println!("Lock period (blocks): {}", blocks_1y);
// Create the 1-year term
let timelock_1y = TimeLock::new(
TimeLockType::BlockHeight(blockchain.chain.len() as u64 + blocks_1y)
);
let deposit_tx_1y = timelock_1y.create_timelocked_transaction(
&mut blockchain,
&user,
user.address.clone(),
total_1y,
10,
)?;
blockchain.add_transaction(deposit_tx_1y.clone())?;
blockchain.mine_pending_transactions(user.address.clone())?;
println!("1-year deposit created successfully");
println!("Transaction ID: {}...{}\n", &deposit_tx_1y.id[..16], &deposit_tx_1y.id[56..]);
// Show current balance
let current_balance = blockchain.get_balance(&user.address);
println!("Available balance: {} satoshi", current_balance);
println!("Total in time deposits: {} satoshi (locked)\n", amount_3m + amount_1y);
// Scenario 3: Attempt early withdrawal (should fail)
println!("--- Scenario 3: Attempting early withdrawal ---");
println!("Current block height: {}", blockchain.chain.len());
println!("3-month deposit unlock height: {}", blockchain.chain.len() as u64 + blocks_3m);
match timelock_3m.is_spendable(&blockchain) {
true => println!("ERROR: deposit not yet matured but withdrawal is possible!"),
false => println!("Correct: deposit not yet matured, funds are locked"),
}
// Scenario 4: Simulate passage of time (mine to 3 months later)
println!("\n--- Scenario 4: 3-month deposit matures ---");
println!("Simulating mining {} blocks...", blocks_3m);
// Fast-simulate mining
for _ in 0..blocks_3m {
blockchain.mine_pending_transactions(user.address.clone())?;
}
println!("Current block height: {}", blockchain.chain.len());
// Check whether withdrawal is possible
if timelock_3m.is_spendable(&blockchain) {
println!("3-month deposit has matured, withdrawal is available");
// Collect principal + interest
println!("Withdrawal amount: {} satoshi (principal {} + interest {})",
total_3m, amount_3m, interest_3m);
let final_balance = blockchain.get_balance(&user.address);
println!("Balance after receipt: {} satoshi", final_balance);
} else {
println!("ERROR: deposit has matured but withdrawal is unavailable");
}
// Scenario 5: 1-year deposit not yet matured
println!("\n--- Scenario 5: 1-year deposit status ---");
println!("Current block height: {}", blockchain.chain.len());
println!("1-year deposit unlock height: {}", blockchain.chain.len() as u64 + blocks_1y - blocks_3m);
match timelock_1y.is_spendable(&blockchain) {
true => println!("1-year deposit has matured, withdrawal is available"),
false => {
let remaining = blocks_1y - blocks_3m;
println!("1-year deposit has not yet matured; {} blocks remaining (approximately {} days)",
remaining, remaining / 144);
}
}
println!("\n=== Demo Complete ===");
Ok(())
}
// Helper function: initialize balance
fn setup_balance(
blockchain: &mut Blockchain,
wallet: &Wallet,
amount: u64
) -> Result<(), String> {
let genesis = Wallet::from_address("genesis".to_string());
let tx = blockchain.create_transaction(
&genesis,
wallet.address.clone(),
amount,
0,
)?;
blockchain.add_transaction(tx)?;
blockchain.mine_pending_transactions(wallet.address.clone())?;
Ok(())
}
Code Walkthrough
1. Product Parameter Definitions
#![allow(unused)]
fn main() {
// Short-Term Saver: 3-month term
let amount_3m = 5000; // deposit amount
let rate_3m = 0.03; // 3% annual rate
let blocks_3m = 13 * 7 * 144; // 3 months = 13 weeks = 13,104 blocks
// Calculate interest: principal × annual rate × time (months/12)
let interest_3m = (amount_3m as f64 * rate_3m * 3.0 / 12.0) as u64;
// interest_3m = 5000 × 0.03 × 0.25 = 37.5 ≈ 37 satoshi
}
Why block height instead of timestamp?
- More precise: block height is a discrete integer with no ambiguity
- More reliable: timestamps can be manipulated by miners (±2 hours)
- More consistent: the entire network has uniform consensus on block height
2. Creating the TimeLock Term
#![allow(unused)]
fn main() {
// Create a timelock: current height + lock period
let timelock_3m = TimeLock::new(
TimeLockType::BlockHeight(
blockchain.chain.len() as u64 + blocks_3m
)
);
}
Key points:
blockchain.chain.len()= current block height+ blocks_3m= unlock block height- Before the unlock height, the transaction cannot be spent
3. Creating the Time Deposit Transaction
#![allow(unused)]
fn main() {
let deposit_tx_3m = timelock_3m.create_timelocked_transaction(
&mut blockchain,
&user, // depositor
user.address.clone(), // returned to the depositor at maturity
total_3m, // principal + interest
10, // fee
)?;
}
Transaction flow:
User balance → [timelocked transaction] → UTXO pool (locked state)
↓
(spendable only at maturity)
↓
User balance (principal + interest)
4. Maturity Check
#![allow(unused)]
fn main() {
if timelock_3m.is_spendable(&blockchain) {
// withdrawal available
} else {
// not yet matured
}
}
Check logic:
#![allow(unused)]
fn main() {
pub fn is_spendable(&self, blockchain: &Blockchain) -> bool {
match &self.lock_type {
TimeLockType::BlockHeight(height) => {
blockchain.chain.len() as u64 >= *height
},
TimeLockType::Timestamp(time) => {
// Compare with current timestamp
current_timestamp() >= *time
}
}
}
}
Sample Output
$ cargo run --example timelock_savings
=== Time Deposit System Demo ===
User address: a3f2d8c9e4b7f1a8...c4e7d9b2a5c
Initial balance: 20000 satoshi
--- Scenario 1: 3-month time deposit (3% annual rate) ---
Deposit amount: 5000 satoshi
Expected interest: 37 satoshi (3 months @ 3%)
Total at maturity: 5037 satoshi
Lock period (blocks): 13104
3-month deposit created successfully
Transaction ID: d4f7a9e2b5c8f1a3...b5c8f1a3d4f7
--- Scenario 2: 1-year time deposit (5% annual rate) ---
Deposit amount: 10000 satoshi
Expected interest: 500 satoshi (1 year @ 5%)
Total at maturity: 10500 satoshi
Lock period (blocks): 52416
1-year deposit created successfully
Transaction ID: e5g8b0f3c6d9g2b4...c6d9g2b4e5g8
Available balance: 4960 satoshi
Total in time deposits: 15000 satoshi (locked)
--- Scenario 3: Attempting early withdrawal ---
Current block height: 4
3-month deposit unlock height: 13108
Correct: deposit not yet matured, funds are locked
--- Scenario 4: 3-month deposit matures ---
Simulating mining 13104 blocks...
Current block height: 13108
3-month deposit has matured, withdrawal is available
Withdrawal amount: 5037 satoshi (principal 5000 + interest 37)
Balance after receipt: 10497 satoshi
--- Scenario 5: 1-year deposit status ---
Current block height: 13108
1-year deposit unlock height: 52420
1-year deposit has not yet matured; 39312 blocks remaining (approximately 273 days)
=== Demo Complete ===
Business Value
Value to Users
| Feature | Traditional Bank Term Deposit | Blockchain Term Deposit | Advantage |
|---|---|---|---|
| Rate transparency | Bank decides | Code is open | Fully transparent |
| Forced savings | Can withdraw early | Technically locked | Truly forced |
| Interest guarantee | Bank’s promise | Smart contract | Auto-executes |
| Maturity action | Must go to bank | Automatic unlock | No action needed |
| Trust cost | High (trust the bank) | Low (trust the code) | Decentralized |
Return Comparison (assuming 10,000 satoshi deposited)
| Product | Term | Rate | Total at Maturity | Earnings |
|---|---|---|---|---|
| Demand deposit | — | 0.3% | 10,030 | 30 |
| Short-Term Saver | 3 months | 3% | 10,075 | 75 |
| Steady Earner | 1 year | 5% | 10,500 | 500 |
Calculation formula:
Total at maturity = principal × (1 + annual rate × term in years)
3 months: 10000 × (1 + 0.03 × 0.25) = 10075
1 year: 10000 × (1 + 0.05 × 1.0) = 10500
Extended Solutions
1. Laddered Deposits
#![allow(unused)]
fn main() {
struct LadderDeposit {
amount: u64,
start_height: u64,
periods: Vec<(u64, f64)>, // (term blocks, rate)
}
impl LadderDeposit {
// Create a laddered deposit: stagger maturity dates
pub fn new(total: u64, blockchain: &Blockchain) -> Self {
let per_amount = total / 4;
let current = blockchain.chain.len() as u64;
LadderDeposit {
amount: per_amount,
start_height: current,
periods: vec![
(13 * 7 * 144, 0.03), // 3 months, 3%
(26 * 7 * 144, 0.04), // 6 months, 4%
(39 * 7 * 144, 0.045), // 9 months, 4.5%
(52 * 7 * 144, 0.05), // 12 months, 5%
],
}
}
}
// Benefits:
// - One tranche matures every 3 months, maintaining liquidity
// - Average rate higher than a single short-term deposit
// - Reduces interest rate fluctuation risk
}
2. Auto-Renewal
#![allow(unused)]
fn main() {
struct AutoRenewDeposit {
principal: u64,
term_blocks: u64,
rate: f64,
max_renewals: u32,
}
impl AutoRenewDeposit {
pub fn create_auto_renew(
&self,
blockchain: &mut Blockchain,
wallet: &Wallet,
) -> Result<Vec<Transaction>, String> {
let mut transactions = Vec::new();
let mut total = self.principal;
for i in 0..self.max_renewals {
let lock_height = blockchain.chain.len() as u64
+ (i as u64 + 1) * self.term_blocks;
// Calculate this period's principal + interest
let interest = (total as f64 * self.rate
* (self.term_blocks as f64 / 52416.0)) as u64;
total += interest;
// Create the renewal transaction
let timelock = TimeLock::new(
TimeLockType::BlockHeight(lock_height)
);
let tx = timelock.create_timelocked_transaction(
blockchain,
wallet,
wallet.address.clone(),
total,
10,
)?;
transactions.push(tx);
}
Ok(transactions)
}
}
// Usage example:
let auto_deposit = AutoRenewDeposit {
principal: 10000,
term_blocks: 13 * 7 * 144, // 3 months
rate: 0.03,
max_renewals: 4, // auto-renew 4 times = 1 year
};
// Automatically creates 4 deposits, renewing every 3 months
let txs = auto_deposit.create_auto_renew(&mut blockchain, &user)?;
}
3. Capital-Protected Floating Yield
#![allow(unused)]
fn main() {
struct FloatingDeposit {
principal: u64,
min_rate: f64, // capital protection rate
bonus_rate: f64, // bonus rate
target_blocks: u64, // target block count
}
impl FloatingDeposit {
pub fn calculate_interest(&self, blockchain: &Blockchain) -> u64 {
let actual_blocks = blockchain.chain.len() as u64;
// Base interest (capital protection)
let base = (self.principal as f64 * self.min_rate) as u64;
// Bonus interest (based on actual holding time)
if actual_blocks >= self.target_blocks {
let bonus = (self.principal as f64 * self.bonus_rate) as u64;
base + bonus
} else {
base
}
}
}
// Usage example:
let floating = FloatingDeposit {
principal: 10000,
min_rate: 0.03, // 3% guaranteed
bonus_rate: 0.02, // additional 2% bonus
target_blocks: 52 * 7 * 144, // bonus requires holding for 1 year
};
// Under 1 year: 3% interest = 300 satoshi
// 1 full year: 5% interest = 500 satoshi
}
4. Early Redemption (with Penalty)
#![allow(unused)]
fn main() {
struct EarlyWithdraw {
deposit_tx: Transaction,
lock_height: u64,
penalty_rate: f64, // penalty rate
}
impl EarlyWithdraw {
pub fn withdraw_early(
&self,
blockchain: &mut Blockchain,
wallet: &Wallet,
) -> Result<Transaction, String> {
let current = blockchain.chain.len() as u64;
// Check whether this is an early redemption
if current >= self.lock_height {
return Err("Deposit has matured; please use normal withdrawal".to_string());
}
// Calculate penalty
let principal = self.deposit_tx.outputs[0].value;
let penalty = (principal as f64 * self.penalty_rate) as u64;
let actual_amount = principal.saturating_sub(penalty);
// Create early redemption transaction (requires admin signature)
let tx = blockchain.create_transaction(
wallet,
wallet.address.clone(),
actual_amount,
10,
)?;
println!("Early redemption: principal {}, penalty {}, received {}",
principal, penalty, actual_amount);
Ok(tx)
}
}
// Usage example:
// User deposits 10000 for 1 year, withdraws early after 6 months
// Penalty 5% = 500 satoshi
// Receives 9500 satoshi (loses 500)
}
Security Considerations
1. Interest Funding Source
#![allow(unused)]
fn main() {
// Incorrect: creating interest out of thin air
let interest = 100;
let total = principal + interest; // where does the interest come from?
// Correct: interest paid from a reserve pool
struct DepositPool {
reserves: u64, // reserve funds
}
impl DepositPool {
pub fn pay_interest(&mut self, principal: u64, rate: f64) -> Result<u64, String> {
let interest = (principal as f64 * rate) as u64;
if self.reserves < interest {
return Err("Insufficient pool balance".to_string());
}
self.reserves -= interest;
Ok(interest)
}
}
}
2. Timestamp Manipulation Attack
Attack scenario: miner manipulates the timestamp to cause early maturity
Defense:
#![allow(unused)]
fn main() {
// Use block height, not timestamp
TimeLockType::BlockHeight(height) // recommended
// Avoid using timestamps (easily manipulated)
TimeLockType::Timestamp(time) // not recommended
}
3. Reentrancy Attack
#![allow(unused)]
fn main() {
// Incorrect: transfer first, then update state
fn withdraw(&mut self) {
self.transfer(user, amount); // transfer first
self.balance = 0; // update later (may be reentered)
}
// Correct: update state first, then transfer (checks-effects-interactions pattern)
fn withdraw(&mut self) {
let amount = self.balance; // check
self.balance = 0; // effect
self.transfer(user, amount); // interaction
}
}
4. Integer Overflow
#![allow(unused)]
fn main() {
// Incorrect: may overflow
let total = principal + interest; // u64 overflow risk
// Correct: use checked_add
let total = principal.checked_add(interest)
.ok_or("Arithmetic overflow")?;
}
Implementation Recommendations
Technical Considerations
-
Adequate testing
#![allow(unused)] fn main() { #[cfg(test)] mod tests { #[test] fn test_interest_calculation() { /* ... */ } #[test] fn test_early_withdraw_penalty() { /* ... */ } #[test] fn test_timelock_enforcement() { /* ... */ } } } -
Code audit
- Verify interest calculation formulas are correct
- Verify that the timelock is reliable
- Verify that the funding source is clearly defined
- Handle edge cases
-
Monitoring and alerts
#![allow(unused)] fn main() { // Monitor key metrics - Reserve pool balance warning (< 10%) - Matured deposits not yet collected (> 1 month) - Abnormal early redemption frequency }
Business Considerations
-
Risk disclosures
Time Deposit Risk Notice: 1. Funds will be locked and cannot be withdrawn before maturity 2. Interest is paid from the reserve pool and carries payment risk 3. Smart contracts may contain unknown vulnerabilities 4. Blockchain transactions are irreversible; proceed with care -
User education
- Provide a sandbox demo environment for users to practice
- Offer detailed operation guides
- Explain the differences from traditional banking
- Emphasize the importance of private key custody
-
Product iteration
- Collect user feedback
- Analyze maturity data
- Optimize interest rate strategy
- Add more product types
Real-World Applications
DeFi Time Deposit Protocols
Compound: lending protocol, deposits accrue interest automatically
User deposits ETH → receives cETH (interest-bearing token)
Interest rate floats with the market → withdrawable at any time
Anchor Protocol: fixed-rate deposits (Terra ecosystem)
Deposit UST → fixed ~20% APY
Interest comes from lending markets and staking rewards
Alchemix: self-repaying loans
Deposit DAI → borrow alUSD (50% LTV)
Interest automatically repays the loan → no repayments needed
Comparison with SimpleBTC
| Feature | SimpleBTC Term Deposit | DeFi Term Deposit |
|---|---|---|
| Timelock | Hard lock (nLockTime) | Soft lock (contract) |
| Rate | Fixed | Usually floating |
| Liquidity | Withdraw only at maturity | Early withdrawal available (with penalty) |
| Interest source | Reserve pool | Lending/staking |
| Risk | Timelock risk | Smart contract risk |
FAQ
Q1: Where does the interest for time deposits come from?
A: SimpleBTC interest is for demo purposes. In real applications, interest may come from:
- Reserve pool funds
- Interest spreads in lending markets
- Distribution of mining rewards
- Return of transaction fees
- Protocol token issuance
Q2: Can I withdraw early?
A: SimpleBTC uses nLockTime hard locking, so early withdrawal is technically impossible. Real applications can be designed with:
- Early redemption with a penalty (5–10% fee)
- NFT collateral borrowing (keeps the term deposit running)
- Secondary market transfer (sell at a discount)
Q3: What if I forget to collect after maturity?
A: UTXOs are permanently valid and can be collected at any time. However, note that:
- No additional interest accrues after maturity
- Set maturity reminders
- Auto-renewal can be implemented
Q4: How is the timelock period calculated?
A:
Block height method (recommended):
- 3 months ≈ 13,104 blocks (91 days × 144 blocks/day)
- 1 year ≈ 52,416 blocks (365 days × 144 blocks/day)
Timestamp method (not recommended):
- 3 months = current timestamp + 7,862,400 seconds
- 1 year = current timestamp + 31,536,000 seconds
References
- TimeLock Tutorial — timelock principles and usage
- Transaction API — transaction creation
- Blockchain API — blockchain operations
- Compound Finance — DeFi lending protocol
- BIP65 - CHECKLOCKTIMEVERIFY
Back to Examples | Next Example: Enterprise Multisig
Core Module API
The SimpleBTC core module provides fundamental Bitcoin blockchain functionality.
Module List
Transaction Module
- Transaction API - Transaction structure, UTXO model, fee calculation
Block Module
- Block API - Block structure, proof of work, Merkle root
Blockchain Module
- Blockchain API - Blockchain management, mining, validation
Wallet Module
- Wallet API - Key generation, signing, addresses
UTXO Module
- UTXO API - UTXO set, balance queries, double-spend protection
Quick Index
Common Functions
Create a Wallet
#![allow(unused)]
fn main() {
use bitcoin_simulation::wallet::Wallet;
let wallet = Wallet::new();
}
Create a Transaction
#![allow(unused)]
fn main() {
let tx = blockchain.create_transaction(
&from_wallet,
to_address,
amount,
fee
)?;
}
Mine
#![allow(unused)]
fn main() {
blockchain.mine_pending_transactions(miner_address)?;
}
Query Balance
#![allow(unused)]
fn main() {
let balance = blockchain.get_balance(&address);
}
Data Flow
1. Create Wallet
Wallet::new() → generate key pair → obtain address
2. Create Transaction
Select UTXOs → build inputs/outputs → sign → verify
3. Add Transaction
Validate transaction → add to pending pool → wait to be mined
4. Mine
Collect transactions → create Coinbase → compute Merkle root → PoW → update UTXOs
5. Query
Traverse UTXO set → accumulate balance
Type Definitions
Core Types
#![allow(unused)]
fn main() {
// Amount unit: satoshi
type Amount = u64; // 1 BTC = 100,000,000 satoshi
// Address: 40-character hexadecimal
type Address = String;
// Hash: 64-character hexadecimal
type Hash = String;
// Unix timestamp (seconds)
type Timestamp = u64;
}
Error Types
#![allow(unused)]
fn main() {
// All APIs return Result<T, String>
type ApiResult<T> = Result<T, String>;
// Common error messages
"Insufficient balance (including fee)"
"UTXO does not exist"
"Transaction validation failed"
"Referenced transaction does not exist"
"No pending transactions"
}
Usage Patterns
Basic Pattern
use bitcoin_simulation::{
blockchain::Blockchain,
wallet::Wallet,
};
fn main() -> Result<(), String> {
// 1. Initialize
let mut blockchain = Blockchain::new();
let wallet = Wallet::new();
// 2. Operate
let tx = blockchain.create_transaction(...)?;
blockchain.add_transaction(tx)?;
blockchain.mine_pending_transactions(...)?;
// 3. Query
let balance = blockchain.get_balance(&wallet.address);
Ok(())
}
Error Handling Pattern
#![allow(unused)]
fn main() {
match blockchain.create_transaction(&alice, bob_addr, 1000, 10) {
Ok(tx) => {
blockchain.add_transaction(tx)?;
println!("✓ Transaction successful");
}
Err(e) => {
eprintln!("✗ Error: {}", e);
// Handle error...
}
}
}
Performance Considerations
UTXO Queries
- Time complexity: O(n), where n is the total number of UTXOs
- Recommendation: Use index optimization (see
indexer.rs)
Mining
- Time complexity: O(2^difficulty)
- Recommendation: Difficulty 3–4 is suitable for demos; real applications require higher values
Blockchain Validation
- Time complexity: O(n*m), where n is the block count and m is the average transaction count
- Recommendation: Validate periodically rather than after every operation
Thread Safety
⚠️ Note: The current implementation is not thread-safe.
For concurrent access:
#![allow(unused)]
fn main() {
use std::sync::{Arc, Mutex};
let blockchain = Arc::new(Mutex::new(Blockchain::new()));
// In different threads
let blockchain = blockchain.clone();
let mut bc = blockchain.lock().unwrap();
bc.create_transaction(...)?;
}
Next Steps
- View detailed API documentation for specific modules
- Read the Advanced Module API
- Refer to Real-World Examples
Transaction API
The transaction module provides a complete implementation of the Bitcoin UTXO model.
Data Structures
TxInput
A transaction input that references a previously unspent output (UTXO).
#![allow(unused)]
fn main() {
pub struct TxInput {
pub txid: String, // ID of the referenced transaction
pub vout: usize, // Output index
pub signature: String, // Digital signature
pub pub_key: String, // Public key
}
}
Methods:
new
#![allow(unused)]
fn main() {
pub fn new(
txid: String,
vout: usize,
signature: String,
pub_key: String
) -> Self
}
Creates a new transaction input.
Parameters:
txid- ID of the referenced transactionvout- Output index numbersignature- Signature generated with the private keypub_key- Corresponding public key
Example:
#![allow(unused)]
fn main() {
let input = TxInput::new(
"abc123...".to_string(),
0,
wallet.sign("data"),
wallet.public_key.clone()
);
}
TxOutput
A transaction output representing an unspent amount (UTXO).
#![allow(unused)]
fn main() {
pub struct TxOutput {
pub value: u64, // Amount (satoshi)
pub pub_key_hash: String, // Recipient address
}
}
Methods:
new
#![allow(unused)]
fn main() {
pub fn new(value: u64, address: String) -> Self
}
Creates a new transaction output.
Parameters:
value- Output amount (satoshi)address- Recipient address
Example:
#![allow(unused)]
fn main() {
let output = TxOutput::new(5000, bob_address);
}
can_be_unlocked_with
#![allow(unused)]
fn main() {
pub fn can_be_unlocked_with(&self, address: &str) -> bool
}
Checks whether this output can be unlocked by the specified address.
Parameters:
address- The address to check
Return value:
true- Address matchesfalse- Address does not match
Example:
#![allow(unused)]
fn main() {
if output.can_be_unlocked_with(&alice.address) {
println!("Alice can spend this output");
}
}
Transaction
The complete transaction structure.
#![allow(unused)]
fn main() {
pub struct Transaction {
pub id: String, // Transaction ID
pub inputs: Vec<TxInput>, // List of inputs
pub outputs: Vec<TxOutput>, // List of outputs
pub timestamp: u64, // Unix timestamp
pub fee: u64, // Transaction fee
}
}
Methods:
new
#![allow(unused)]
fn main() {
pub fn new(
inputs: Vec<TxInput>,
outputs: Vec<TxOutput>,
timestamp: u64,
fee: u64
) -> Self
}
Creates a new transaction.
Parameters:
inputs- List of transaction inputsoutputs- List of transaction outputstimestamp- Unix timestampfee- Transaction fee (satoshi)
Return value:
- A newly created transaction instance with an automatically computed ID
Example:
#![allow(unused)]
fn main() {
let tx = Transaction::new(
vec![input1, input2],
vec![output1, output2],
SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(),
10
);
}
new_coinbase
#![allow(unused)]
fn main() {
pub fn new_coinbase(
to: String,
reward: u64,
timestamp: u64,
total_fees: u64
) -> Self
}
Creates a Coinbase transaction (mining reward).
Parameters:
to- Miner addressreward- Block reward (excluding fees)timestamp- Unix timestamptotal_fees- Sum of all transaction fees in the block
Return value:
- Coinbase transaction instance
Example:
#![allow(unused)]
fn main() {
let coinbase = Transaction::new_coinbase(
miner.address,
50,
timestamp,
total_fees
);
}
calculate_hash
#![allow(unused)]
fn main() {
pub fn calculate_hash(&self) -> String
}
Computes the transaction hash (transaction ID).
Return value:
- 64-character hexadecimal hash string
Notes:
- Uses the SHA256 algorithm
- Includes all transaction data (inputs, outputs, timestamp, fee)
- Any change in data will produce a completely different hash
Example:
#![allow(unused)]
fn main() {
let tx_id = tx.calculate_hash();
println!("Transaction ID: {}", tx_id);
}
is_coinbase
#![allow(unused)]
fn main() {
pub fn is_coinbase(&self) -> bool
}
Checks whether this is a Coinbase transaction.
Return value:
true- Coinbase transactionfalse- Regular transaction
Criteria:
- Exactly one input
- That input’s txid is empty
Example:
#![allow(unused)]
fn main() {
if tx.is_coinbase() {
println!("This is a mining reward transaction");
} else {
println!("This is a regular transaction");
}
}
verify
#![allow(unused)]
fn main() {
pub fn verify(&self) -> bool
}
Validates the transaction (simplified version).
Validation items:
- Coinbase transactions are always valid
- Checks for at least one input and one output
- Checks that the signature and public key are non-empty
Return value:
true- Transaction is validfalse- Transaction is invalid
Note: Real Bitcoin also requires:
- ECDSA signature correctness
- UTXO existence
- Amount balance
- Script execution
Example:
#![allow(unused)]
fn main() {
if tx.verify() {
blockchain.add_transaction(tx)?;
} else {
return Err("Invalid transaction".to_string());
}
}
size
#![allow(unused)]
fn main() {
pub fn size(&self) -> usize
}
Calculates the transaction size in bytes.
Return value:
- Byte size of the transaction
Use cases:
- Computing fee rate (sat/byte)
- Estimating block space usage
- Fee estimation
Example:
#![allow(unused)]
fn main() {
let size = tx.size();
println!("Transaction size: {} bytes", size);
}
fee_rate
#![allow(unused)]
fn main() {
pub fn fee_rate(&self) -> f64
}
Calculates the transaction fee rate (satoshi/byte).
Return value:
- Fee rate (sat/byte)
Formula:
fee_rate = fee / size
Fee rate reference:
- 1–5 sat/byte: Low priority
- 5–20 sat/byte: Medium priority
- 20–50 sat/byte: High priority
- 50+ sat/byte: Urgent
Example:
#![allow(unused)]
fn main() {
let rate = tx.fee_rate();
println!("Fee rate: {:.2} sat/byte", rate);
if rate < 5.0 {
println!("Warning: Low fee rate, confirmation may be slow");
}
}
output_sum
#![allow(unused)]
fn main() {
pub fn output_sum(&self) -> u64
}
Gets the total amount of all outputs.
Return value:
- Total output amount (satoshi)
Use cases:
- Verifying transaction balance
- Computing the actual fee
Formula:
fee = input_sum - output_sum
Example:
#![allow(unused)]
fn main() {
let output_total = tx.output_sum();
let fee = input_total - output_total;
println!("Fee: {}", fee);
}
Usage Examples
Creating a Simple Transaction
use bitcoin_simulation::{
blockchain::Blockchain,
wallet::Wallet,
};
fn main() -> Result<(), String> {
let mut blockchain = Blockchain::new();
let alice = Wallet::new();
let bob = Wallet::new();
// Alice receives initial funds
let init_tx = blockchain.create_transaction(
&Wallet::from_address("genesis".to_string()),
alice.address.clone(),
10000,
0,
)?;
blockchain.add_transaction(init_tx)?;
blockchain.mine_pending_transactions(alice.address.clone())?;
// Alice sends to Bob
let tx = blockchain.create_transaction(
&alice,
bob.address.clone(),
3000, // amount
10, // fee
)?;
// View transaction details
println!("Transaction ID: {}", tx.id);
println!("Number of inputs: {}", tx.inputs.len());
println!("Number of outputs: {}", tx.outputs.len());
println!("Fee: {}", tx.fee);
println!("Fee rate: {:.2} sat/byte", tx.fee_rate());
// Add to the blockchain
blockchain.add_transaction(tx)?;
blockchain.mine_pending_transactions(bob.address)?;
Ok(())
}
Batch Transactions
#![allow(unused)]
fn main() {
// Create multiple transactions to test fee priority
let transactions = vec![
(bob.address.clone(), 1000, 1), // Low fee
(charlie.address.clone(), 2000, 50), // High fee
(david.address.clone(), 3000, 5), // Medium fee
];
for (to, amount, fee) in transactions {
let tx = blockchain.create_transaction(&alice, to, amount, fee)?;
blockchain.add_transaction(tx)?;
}
// Mining sorts transactions by fee rate, highest first
blockchain.mine_pending_transactions(miner.address)?;
}
Manually Building a Transaction
#![allow(unused)]
fn main() {
use std::time::{SystemTime, UNIX_EPOCH};
use bitcoin_simulation::transaction::{Transaction, TxInput, TxOutput};
// 1. Create input (requires knowledge of the prior UTXO)
let input = TxInput::new(
"previous_tx_id".to_string(),
0, // vout
alice.sign("tx_data"),
alice.public_key.clone(),
);
// 2. Create outputs
let output1 = TxOutput::new(3000, bob.address); // To Bob
let output2 = TxOutput::new(6990, alice.address); // Change
// 3. Assemble transaction
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
let tx = Transaction::new(
vec![input],
vec![output1, output2],
timestamp,
10, // fee
);
// 4. Verify and add
if tx.verify() {
blockchain.add_transaction(tx)?;
}
}
Error Handling
#![allow(unused)]
fn main() {
match blockchain.create_transaction(&alice, bob.address, 1000, 10) {
Ok(tx) => {
println!("✓ Transaction created successfully");
blockchain.add_transaction(tx)?;
}
Err(e) => {
eprintln!("❌ Transaction creation failed: {}", e);
// Common errors:
// - "Insufficient balance (including fee)"
// - "UTXO does not exist"
// - "Referenced transaction does not exist"
}
}
}
Best Practices
1. Setting Fees
#![allow(unused)]
fn main() {
// Set fee based on urgency
let size = estimate_tx_size(inputs_count, outputs_count);
let fee = match urgency {
Urgency::Low => size * 1, // 1 sat/byte
Urgency::Medium => size * 10, // 10 sat/byte
Urgency::High => size * 50, // 50 sat/byte
};
}
2. UTXO Selection
#![allow(unused)]
fn main() {
// Prefer small UTXOs to avoid fragmentation
let utxos = blockchain.utxo_set.find_spendable_outputs(&address, amount)?;
println!("Used {} UTXOs", utxos.1.len());
}
3. Transaction Validation
#![allow(unused)]
fn main() {
// Validate immediately after creating a transaction
let tx = Transaction::new(...);
assert!(tx.verify(), "Transaction validation failed");
assert!(tx.fee_rate() >= 1.0, "Fee rate too low");
}
References
- Blockchain API - Blockchain transaction management
- UTXO API - UTXO set operations
- Wallet API - Wallet and signing
Block API
Block is the fundamental unit of the blockchain, defined in src/block.rs. Each block contains a batch of confirmed transactions and is linked to the previous block via a hash chain, together forming an immutable ledger.
Block Struct
#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Block {
pub index: u32, // Block height (index); genesis block is 0
pub timestamp: u64, // Unix timestamp (seconds)
pub transactions: Vec<Transaction>, // Transaction list (first must be a Coinbase transaction)
pub previous_hash: String, // Parent block hash (SHA256, 64-character hex)
pub hash: String, // Current block hash (found through mining)
pub nonce: u64, // Proof-of-work nonce (adjusted during mining)
pub merkle_root: String, // Merkle tree root hash of all transactions
}
}
Field Descriptions
| Field | Type | Description |
|---|---|---|
index | u32 | Block height. Genesis block is 0; each subsequent block increments by 1. |
timestamp | u64 | Unix timestamp (seconds) when the block was created. Automatically set by SystemTime::now(). |
transactions | Vec<Transaction> | List of transactions in the block. The first must be a Coinbase transaction (miner reward). |
previous_hash | String | SHA256 hash (64-character hex) of the parent block. For the genesis block, this field is "0". |
hash | String | SHA256 hash of the current block. Computed by calculate_hash(), continuously updated during mining until the difficulty requirement is met. |
nonce | u64 | Proof-of-work nonce. Miners increment nonce to find a hash satisfying the difficulty target. |
merkle_root | String | Merkle tree root hash of all transactions in the block. Any tampered transaction will change this value. |
Chain Structure
Genesis Block (index=0) -> Block 1 -> Block 2
prev: "0" prev: abc123... prev: def456...
hash: abc123... hash: def456... hash: ghi789...
Because each block’s hash depends on previous_hash and all transactions (via merkle_root), modifying any historical block requires recomputing the hash of all subsequent blocks — computationally infeasible.
Methods
Block::new
Creates a new block. Automatically sets the timestamp and computes the Merkle root, but nonce starts at 0 and hash is the initial computed value (does not yet satisfy mining difficulty).
#![allow(unused)]
fn main() {
pub fn new(
index: u32,
transactions: Vec<Transaction>,
previous_hash: String,
) -> Block
}
Parameters:
index— Height of the new block.transactions— List of transactions to include in the block (first should be a Coinbase transaction).previous_hash— Hash string of the parent block.
Return value: An initialized Block instance (mining not yet complete).
Internal flow:
- Obtain the current Unix timestamp.
- Build a
MerkleTreefrom theidlist oftransactionsand computemerkle_root. - Construct the block with
nonce = 0and callcalculate_hash()to get the initial hash.
#![allow(unused)]
fn main() {
use simplebtc::block::Block;
use simplebtc::transaction::Transaction;
let coinbase = Transaction::new_coinbase("miner_address", 3125000); // 3.125 BTC (satoshi)
let block = Block::new(1, vec![coinbase], "abc123...".to_string());
println!("Block #{}: {}", block.index, block.hash);
}
Block::calculate_hash
Computes the SHA256 hash of the block. The hash input includes index, timestamp, merkle_root, previous_hash, and nonce.
#![allow(unused)]
fn main() {
pub fn calculate_hash(&self) -> String
}
Return value: 64-character lowercase hexadecimal SHA256 hash string.
Hash input format:
"{index}{timestamp}{merkle_root}{previous_hash}{nonce}"
Using merkle_root rather than the full transaction data keeps the block header lightweight (approximately 80 bytes) while ensuring the integrity of all transaction content.
#![allow(unused)]
fn main() {
let mut block = Block::new(1, transactions, prev_hash);
// Recompute hash after modifying nonce (core mining logic)
block.nonce += 1;
block.hash = block.calculate_hash();
println!("New hash: {}", block.hash);
}
Block::validate_transactions
Validates the signature validity of all transactions in the block. Calls each transaction’s verify() method in sequence.
#![allow(unused)]
fn main() {
pub fn validate_transactions(&self) -> bool
}
Return value:
true— All transaction signatures are valid.false— At least one invalid transaction exists.
#![allow(unused)]
fn main() {
let block = Block::new(1, transactions, prev_hash);
if block.validate_transactions() {
println!("All transactions valid, can be added to chain");
} else {
println!("Block contains invalid transactions, rejected");
}
}
Note: This method only validates signatures, not UTXO balances. Balance validation is handled at the
Blockchainlayer.
Block::verify_transaction_inclusion
Verifies whether a specific transaction is included in this block using a Merkle proof. This is the core functionality of SPV (Simplified Payment Verification), requiring no traversal of all transactions — time complexity is O(log n).
#![allow(unused)]
fn main() {
pub fn verify_transaction_inclusion(
&self,
tx_id: &str,
index: usize,
) -> bool
}
Parameters:
tx_id— The transaction ID (hash string) to verify.index— The position index of the transaction in the block’s transaction list (starting from 0).
Return value:
true— The transaction is indeed included in this block and the Merkle proof is valid.false— The transaction is not in this block, or the proof is invalid.
Internal flow:
- Reconstruct the block’s
MerkleTree. - Call
get_proof(tx_id)to generate a Merkle proof. - Call
MerkleTree::verify_proof()to verify the proof againstmerkle_root.
#![allow(unused)]
fn main() {
let tx_id = "abc123def456...";
let tx_index = 2; // Position of the transaction in the block
if block.verify_transaction_inclusion(tx_id, tx_index) {
println!("Transaction confirmed in block #{}", block.index);
} else {
println!("Transaction is not in this block");
}
}
Block::mine_block
Proof-of-Work mining. Continuously increments nonce and recomputes the hash until the hash prefix satisfies the difficulty requirement (i.e., starts with difficulty number of '0' characters).
#![allow(unused)]
fn main() {
pub fn mine_block(&mut self, difficulty: usize)
}
Parameters:
difficulty— Mining difficulty: the number of leading'0'characters required in the hash.
Side effects: Modifies self.nonce and self.hash until a valid hash is found.
#![allow(unused)]
fn main() {
let mut block = Block::new(1, transactions, prev_hash);
println!("Starting mining, difficulty: 4");
block.mine_block(4); // Hash must start with "0000"
println!("Mining complete: {}", block.hash);
println!("Nonce used: {}", block.nonce);
// Example output: 0000a3f7c2...
}
About difficulty: Bitcoin mainnet’s current difficulty is roughly equivalent to about 20 leading
'0'characters in the hash (requiring approximately 2^80 hash calculations). This project uses smaller difficulty values (such as 2–4) for demonstration purposes.
Complete Usage Example
use simplebtc::block::Block;
use simplebtc::transaction::Transaction;
use simplebtc::wallet::Wallet;
fn main() {
// 1. Create miner wallet
let miner = Wallet::new();
// 2. Create Coinbase transaction (miner reward)
let coinbase = Transaction::new_coinbase(&miner.address, 3_125_000);
// 3. Create a regular transfer transaction
let alice = Wallet::new();
let bob = Wallet::new();
let transfer = Transaction::new(&alice, &bob.address, 50_000, 500);
// 4. Pack into a block (assuming parent hash is known)
let prev_hash = "0000abc123...".to_string();
let mut block = Block::new(1, vec![coinbase, transfer], prev_hash);
// 5. Mine (proof of work)
block.mine_block(3); // Difficulty 3: hash starts with "000"
// 6. Validate the block
assert!(block.validate_transactions(), "Block transactions invalid");
println!("Block hash: {}", block.hash);
println!("Merkle root: {}", block.merkle_root);
println!("Nonce: {}", block.nonce);
// 7. SPV verification: is a transaction included in this block?
let included = block.verify_transaction_inclusion(&block.transactions[0].id.clone(), 0);
println!("Coinbase transaction included: {}", included);
}
Immutability Principle
Attacker attempts to modify a transaction in Block 1:
Modify transaction
↓
Transaction hash changes
↓
Merkle Root changes
↓
Block 1's Hash changes
↓
Block 2's previous_hash no longer matches
↓
Hashes of Blocks 2, 3, 4... all become invalid
↓
Attacker must re-mine all subsequent blocks (computationally infeasible)
This is the mathematical guarantee of blockchain “immutability.”
Related Modules
MerkleTree— Merkle tree implementation, used to computemerkle_rootand generate SPV proofs.Transaction— Transaction struct, the core data of a Block.Blockchain— Manages the blockchain, callsmine_block(), and maintains chain state.
Blockchain API
The blockchain module is the core of SimpleBTC, managing the entire state and operations of the blockchain.
Data Structures
Blockchain
#![allow(unused)]
fn main() {
pub struct Blockchain {
pub chain: Vec<Block>, // The blockchain (list of blocks)
pub difficulty: usize, // Mining difficulty
pub pending_transactions: Vec<Transaction>, // Pending transaction pool
pub utxo_set: UTXOSet, // UTXO set
pub mining_reward: u64, // Mining reward
pub indexer: TransactionIndexer, // Transaction indexer
}
}
Methods
Initialization
new
#![allow(unused)]
fn main() {
pub fn new() -> Blockchain
}
Creates a new blockchain, automatically creating the genesis block.
Initial parameters:
difficulty: 3- Mining difficulty (3 leading zeros)mining_reward: 50- Block reward (50 satoshi)- Genesis block contains 100 satoshi sent to
genesis_address
Return value: New blockchain instance
Example:
#![allow(unused)]
fn main() {
let mut blockchain = Blockchain::new();
println!("Blockchain initialized, current height: {}", blockchain.chain.len());
}
Transaction Management
create_transaction
#![allow(unused)]
fn main() {
pub fn create_transaction(
&self,
from_wallet: &Wallet,
to_address: String,
amount: u64,
fee: u64,
) -> Result<Transaction, String>
}
Creates a new transaction. Automatically selects UTXOs, builds inputs/outputs, and adds signatures.
Parameters:
from_wallet- Sender’s wallet (requires private key for signing)to_address- Recipient addressamount- Transfer amount (satoshi)fee- Transaction fee (satoshi)
Return value:
Ok(Transaction)- Transaction created successfullyErr(String)- Error message
Error cases:
"Insufficient balance (including fee)"- Not enough UTXOs"UTXO does not exist"- Referenced UTXO has already been spent"Referenced transaction does not exist"- Data inconsistency
Workflow:
- Find available UTXOs for the sender
- Select sufficient UTXOs (greedy algorithm)
- Create transaction inputs (including signatures)
- Create transaction outputs (recipient + change)
- Compute transaction ID
Example:
#![allow(unused)]
fn main() {
// Basic usage
let tx = blockchain.create_transaction(
&alice,
bob.address.clone(),
5000, // transfer 5000 satoshi
10, // fee 10 satoshi
)?;
blockchain.add_transaction(tx)?;
// Check balance
let balance = blockchain.get_balance(&alice.address);
if balance < amount + fee {
return Err("Insufficient balance".to_string());
}
// Batch creation
for i in 1..=10 {
let tx = blockchain.create_transaction(
&alice,
recipients[i].clone(),
1000,
i as u64, // different fees
)?;
blockchain.add_transaction(tx)?;
}
}
add_transaction
#![allow(unused)]
fn main() {
pub fn add_transaction(&mut self, transaction: Transaction) -> Result<(), String>
}
Adds a transaction to the pending pool, waiting to be mined.
Parameters:
transaction- The transaction to add
Validation items:
- ✅ Transaction format is correct (
verify()) - ✅ UTXOs referenced by inputs exist
- ✅ Signatures are valid
- ✅ Total inputs ≥ total outputs
Return value:
Ok(())- Added successfullyErr(String)- Reason for validation failure
Example:
#![allow(unused)]
fn main() {
let tx = blockchain.create_transaction(&alice, bob.address, 1000, 5)?;
match blockchain.add_transaction(tx) {
Ok(_) => println!("✓ Transaction added to pending pool"),
Err(e) => eprintln!("✗ Invalid transaction: {}", e),
}
// View number of pending transactions
println!("Pending: {} transactions", blockchain.pending_transactions.len());
}
Mining
mine_pending_transactions
#![allow(unused)]
fn main() {
pub fn mine_pending_transactions(
&mut self,
miner_address: String
) -> Result<(), String>
}
Mining: packs pending transactions into a new block.
Parameters:
miner_address- Miner address (receives reward)
Mining flow:
- Check if there are pending transactions
- Sort by fee rate, highest to lowest
- Compute total fees
- Create Coinbase transaction (reward + fees)
- Build Merkle tree
- Proof of work (adjust nonce to find a valid hash)
- Validate all transactions in the block
- Update UTXO set (atomic operation)
- Add block to the chain
- Clear the pending pool
Return value:
Ok(())- Mining successfulErr(String)- Error message
Error cases:
"No pending transactions"- Pending pool is empty"Block contains invalid transactions"- Transaction validation failed"UTXO update failed"- Data inconsistency
Performance:
- Difficulty 3: ~0.001–0.1 seconds
- Difficulty 4: ~0.01–1 second
- Difficulty 5: ~0.1–10 seconds
- Difficulty 6+: Several seconds to several minutes
Example:
#![allow(unused)]
fn main() {
// Basic mining
blockchain.mine_pending_transactions(miner.address.clone())?;
// Mining loop (similar to a real miner)
loop {
if blockchain.pending_transactions.is_empty() {
println!("Waiting for new transactions...");
std::thread::sleep(Duration::from_secs(1));
continue;
}
println!("Starting mining...");
let start = Instant::now();
blockchain.mine_pending_transactions(miner.address.clone())?;
let duration = start.elapsed();
println!("✓ Mining successful! Time elapsed: {:?}", duration);
// Check reward
let reward = blockchain.get_balance(&miner.address);
println!("Miner balance: {} satoshi", reward);
}
}
Query Operations
get_balance
#![allow(unused)]
fn main() {
pub fn get_balance(&self, address: &str) -> u64
}
Queries the balance of an address.
Parameters:
address- The address to query
Return value: Balance (satoshi)
Computation method: Traverse the UTXO set and sum all UTXOs for the address
Example:
#![allow(unused)]
fn main() {
let balance = blockchain.get_balance(&alice.address);
println!("Balance: {} satoshi", balance);
println!("Balance: {:.8} BTC", balance as f64 / 100_000_000.0);
// Batch query
let addresses = vec![alice.address, bob.address, charlie.address];
for addr in addresses {
let bal = blockchain.get_balance(&addr);
println!("{}: {}", &addr[..10], bal);
}
}
is_valid
#![allow(unused)]
fn main() {
pub fn is_valid(&self) -> bool
}
Validates the integrity of the entire blockchain.
Validation items:
- ✅ Each block’s hash is correct
- ✅ Forward references are correct (
previous_hashlinks) - ✅ Proof of work is valid (hash satisfies difficulty)
- ✅ All transactions are valid
Return value:
true- Blockchain is complete and validfalse- Tampering or error detected
Use cases:
- Periodic integrity checks
- Validation after syncing nodes
- Detection of tampering attacks
Example:
#![allow(unused)]
fn main() {
// Periodic validation
if !blockchain.is_valid() {
panic!("❌ Blockchain has been tampered with!");
}
// Detailed validation log
for (i, block) in blockchain.chain.iter().enumerate() {
if block.hash != block.calculate_hash() {
eprintln!("Block {} has invalid hash", i);
}
if !block.validate_transactions() {
eprintln!("Block {} contains invalid transactions", i);
}
}
if blockchain.is_valid() {
println!("✅ Blockchain validation passed");
}
}
print_chain
#![allow(unused)]
fn main() {
pub fn print_chain(&self)
}
Prints detailed blockchain information (for debugging).
Output includes:
- Block index, timestamp, hash
- Previous block hash
- Nonce value
- Transaction list (ID, type, fee, inputs/outputs)
Example:
#![allow(unused)]
fn main() {
blockchain.print_chain();
// Example output:
// ========== Blockchain Info ==========
//
// --- Block #0 ---
// Timestamp: 1703001234
// Hash: 0003ab4f9c2d...
// Previous hash: 0
// Nonce: 1247
// Transaction count: 1
// Transaction #0: abc123...
// Type: Coinbase (mining reward)
// Inputs: 1
// Outputs: 1
// Output 0: 100 -> genesis_address
// ...
}
Usage Examples
Complete Blockchain Demo
use bitcoin_simulation::{blockchain::Blockchain, wallet::Wallet};
fn main() -> Result<(), String> {
println!("=== SimpleBTC Blockchain Demo ===\n");
// 1. Initialize
let mut blockchain = Blockchain::new();
println!("✓ Blockchain created (genesis block)\n");
// 2. Create participants
let alice = Wallet::new();
let bob = Wallet::new();
let miner = Wallet::new();
println!("✓ Created 3 wallets");
println!(" Alice: {}", &alice.address[..16]);
println!(" Bob: {}", &bob.address[..16]);
println!(" Miner: {}\n", &miner.address[..16]);
// 3. Alice receives initial funds
let init_tx = blockchain.create_transaction(
&Wallet::from_address("genesis_address".to_string()),
alice.address.clone(),
10000,
0,
)?;
blockchain.add_transaction(init_tx)?;
blockchain.mine_pending_transactions(miner.address.clone())?;
println!("✓ Block #1 mined");
println!(" Alice balance: {}\n", blockchain.get_balance(&alice.address));
// 4. Multiple transactions
println!("Creating 5 transactions (different fees)...");
for i in 1..=5 {
let tx = blockchain.create_transaction(
&alice,
bob.address.clone(),
100 * i,
i as u64, // incrementing fees
)?;
blockchain.add_transaction(tx)?;
println!(" Transaction #{}: {} sat, fee rate: {} sat/byte",
i, 100 * i, i);
}
// 5. Mine (transactions sorted by fee rate)
println!("\nStarting mining...");
blockchain.mine_pending_transactions(miner.address.clone())?;
println!("✓ Block #2 mined\n");
// 6. Final balances
println!("=== Final Balances ===");
println!("Alice: {} satoshi", blockchain.get_balance(&alice.address));
println!("Bob: {} satoshi", blockchain.get_balance(&bob.address));
println!("Miner: {} satoshi", blockchain.get_balance(&miner.address));
// 7. Validate blockchain
println!("\n=== Validate Blockchain ===");
if blockchain.is_valid() {
println!("✅ Blockchain integrity validation passed");
} else {
println!("❌ Blockchain validation failed");
}
// 8. Print details
println!("\n=== Blockchain Details ===");
blockchain.print_chain();
Ok(())
}
Fee Priority Demo
#![allow(unused)]
fn main() {
fn fee_priority_demo() -> Result<(), String> {
let mut blockchain = Blockchain::new();
let alice = Wallet::new();
let recipients: Vec<_> = (0..3).map(|_| Wallet::new()).collect();
// Initialize Alice's balance
setup_balance(&mut blockchain, &alice, 10000)?;
// Create transactions with different fee rates
let txs = vec![
("Slow", 1000, 1), // 1 sat/byte
("Fast", 1000, 50), // 50 sat/byte
("Medium", 1000, 10), // 10 sat/byte
];
println!("Adding transactions:");
for (i, (name, amount, fee)) in txs.iter().enumerate() {
let tx = blockchain.create_transaction(
&alice,
recipients[i].address.clone(),
*amount,
*fee,
)?;
println!(" {}: {} sat, fee rate {} sat/byte", name, amount, fee);
blockchain.add_transaction(tx)?;
}
println!("\nMining (auto-sorted by fee rate)...");
blockchain.mine_pending_transactions(recipients[0].address.clone())?;
// View transaction order in the latest block
let latest_block = blockchain.chain.last().unwrap();
println!("\nTransaction order in block:");
for (i, tx) in latest_block.transactions.iter().skip(1).enumerate() {
println!(" #{}: fee rate {:.2} sat/byte",
i + 1, tx.fee_rate());
}
Ok(())
}
}
Monitoring Blockchain State
#![allow(unused)]
fn main() {
fn blockchain_monitor(blockchain: &Blockchain) {
println!("=== Blockchain State ===");
println!("Block height: {}", blockchain.chain.len());
println!("Difficulty: {} ({} leading zeros)",
blockchain.difficulty, blockchain.difficulty);
println!("Mining reward: {} satoshi", blockchain.mining_reward);
println!("Pending transactions: {}", blockchain.pending_transactions.len());
// UTXO statistics
let total_utxos = blockchain.utxo_set.utxos
.values()
.map(|v| v.len())
.sum::<usize>();
println!("Total UTXOs: {}", total_utxos);
// Latest block info
if let Some(latest) = blockchain.chain.last() {
println!("\nLatest block:");
println!(" Hash: {}", &latest.hash[..16]);
println!(" Transactions: {}", latest.transactions.len());
println!(" Merkle root: {}", &latest.merkle_root[..16]);
}
}
}
Configuration Recommendations
Mining Difficulty
#![allow(unused)]
fn main() {
// Demo environment
blockchain.difficulty = 3; // Fast (milliseconds)
// Test environment
blockchain.difficulty = 4; // Moderate (seconds)
// Production environment
blockchain.difficulty = 6; // Secure (minutes)
}
Block Reward
#![allow(unused)]
fn main() {
// Bitcoin-style (gradual halving)
let halving_interval = 210000;
let halvings = blockchain.chain.len() / halving_interval;
blockchain.mining_reward = 50 >> halvings; // 50, 25, 12.5, ...
}
Performance Optimization
1. UTXO Indexing
Use indexer to speed up queries:
#![allow(unused)]
fn main() {
// Find all transactions for an address
let txs = blockchain.indexer.get_transactions_by_address(&address);
// Find a specific transaction
let tx = blockchain.indexer.get_transaction(&txid);
}
2. Batch Operations
#![allow(unused)]
fn main() {
// Batch add transactions
for tx in transactions {
blockchain.add_transaction(tx)?;
}
// Mine all at once
blockchain.mine_pending_transactions(miner.address)?;
}
3. Parallel Validation
#![allow(unused)]
fn main() {
use rayon::prelude::*;
// Parallel validation of all transactions (requires rayon dependency)
let all_valid = blockchain.pending_transactions
.par_iter()
.all(|tx| tx.verify());
}
References
Wallet API
The wallet module is responsible for managing key pairs, addresses, and signatures.
Data Structures
Wallet
#![allow(unused)]
fn main() {
pub struct Wallet {
pub address: String, // Wallet address (public key hash)
pub private_key: String, // Private key
pub public_key: String, // Public key
}
}
Methods
Creating a Wallet
new
#![allow(unused)]
fn main() {
pub fn new() -> Self
}
Creates a new wallet, automatically generating a key pair and address.
Key generation flow:
- Generate a random private key (64-character hexadecimal)
- Derive the public key from the private key (SHA256)
- Derive the address from the public key hash (first 40 characters)
Return value: New wallet instance
Security notes:
- ⚠️ The private key must be kept secret
- ⚠️ A lost private key cannot be recovered
- ⚠️ Back up to a secure location
Example:
#![allow(unused)]
fn main() {
use bitcoin_simulation::wallet::Wallet;
// Create a new wallet
let wallet = Wallet::new();
println!("Address: {}", wallet.address);
println!("Public key: {}", wallet.public_key);
// Never print or share the private key!
// Multiple wallets
let alice = Wallet::new();
let bob = Wallet::new();
let charlie = Wallet::new();
}
from_address
#![allow(unused)]
fn main() {
pub fn from_address(address: String) -> Self
}
Creates a wallet from a known address (for demonstration only).
Note:
- This generates a new random key pair
- The keys do not correspond to the address
- For testing and demo purposes only
Parameters:
address- The specified address string
Return value: Wallet instance (keys are newly generated)
Example:
#![allow(unused)]
fn main() {
// Used for demonstrating the genesis address
let genesis = Wallet::from_address("genesis_address".to_string());
// In real applications, restore from private key
// let wallet = Wallet::from_private_key(private_key);
}
Signing Operations
sign
#![allow(unused)]
fn main() {
pub fn sign(&self, data: &str) -> String
}
Signs data with the private key.
Signing process (simplified):
signature = SHA256(private_key + data)
Real Bitcoin uses ECDSA:
1. Double SHA256 the data
2. Generate signature using private key and secp256k1 curve
3. Signature contains two parts: r and s
Parameters:
data- Data to sign (usually transaction data)
Return value: Signature string (64-character hexadecimal)
Use cases:
- Proving ownership of the private key
- Authorizing transactions
- Preventing transaction tampering
Example:
#![allow(unused)]
fn main() {
let wallet = Wallet::new();
// Sign transaction data
let tx_data = "send 100 BTC to Bob";
let signature = wallet.sign(tx_data);
println!("Signature: {}", signature);
// Use in a transaction
let input = TxInput::new(
prev_txid,
vout,
wallet.sign(&tx_data), // signature
wallet.public_key.clone(),
);
}
verify_signature (static method)
#![allow(unused)]
fn main() {
pub fn verify_signature(
public_key: &str,
data: &str,
signature: &str
) -> bool
}
Verifies whether a signature is valid (simplified version).
Verification process (simplified):
- Checks that the public key and signature are non-empty
Real Bitcoin uses ECDSA verification:
- Recover the public key from the signature
- Verify the public key matches
- Verify the mathematical correctness of the signature
Parameters:
public_key- Signer’s public keydata- Original datasignature- Signature
Return value:
true- Signature is validfalse- Signature is invalid
Example:
#![allow(unused)]
fn main() {
let wallet = Wallet::new();
let data = "transaction data";
let signature = wallet.sign(data);
// Verify signature
if Wallet::verify_signature(&wallet.public_key, data, &signature) {
println!("✓ Signature valid");
} else {
println!("✗ Signature invalid");
}
// Used in transaction validation
for input in transaction.inputs {
if !Wallet::verify_signature(&input.pub_key, &tx_data, &input.signature) {
return Err("Signature verification failed");
}
}
}
Address Formats
SimpleBTC Address
Format: 40-character hexadecimal string
Example: a3f2d8c9e4b7f1a89c2d5e8f3b6a1c4e7d9b2a5c
Real Bitcoin Addresses
P2PKH (starts with 1)
1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa
Generation process:
Public key → SHA256 → RIPEMD160 → Add version → Checksum → Base58 encoding
P2SH (starts with 3)
3J98t1WpEZ73CNmYviecrnyiWrnqRhWNLy
Use case: Multisig, script addresses
Bech32 (starts with bc1)
bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4
Advantage: SegWit address, lower fees
Key Management
Private Key Security
Best practices:
#![allow(unused)]
fn main() {
// ✅ Good practice
let wallet = Wallet::new();
// Encrypt and store the private key
let encrypted = encrypt_private_key(&wallet.private_key, password);
save_to_secure_storage(&encrypted);
// Clear memory immediately after use
drop(wallet);
// Back up to multiple locations
backup_to_hardware_wallet(&wallet.private_key);
backup_to_paper(&wallet.private_key);
backup_to_encrypted_usb(&wallet.private_key);
}
#![allow(unused)]
fn main() {
// ❌ Bad practice
println!("Private key: {}", wallet.private_key); // Never print
save_to_file(&wallet.private_key); // Plaintext storage
send_via_email(&wallet.private_key); // Network transmission
}
Key Recovery
#![allow(unused)]
fn main() {
// Recover wallet from private key (needs implementation)
fn recover_wallet(private_key: &str) -> Wallet {
// 1. Validate private key format
// 2. Derive public key from private key
// 3. Generate address from public key
// 4. Return wallet instance
}
// Using mnemonic phrase (BIP39 standard, needs implementation)
fn from_mnemonic(words: &str) -> Wallet {
// Mnemonic → seed → master private key → derived keys
}
}
Use Cases
Case 1: Basic Transfer
#![allow(unused)]
fn main() {
use bitcoin_simulation::{blockchain::Blockchain, wallet::Wallet};
fn basic_transfer() -> Result<(), String> {
let mut blockchain = Blockchain::new();
// Create participants
let alice = Wallet::new();
let bob = Wallet::new();
// Alice receives initial funds
setup_balance(&mut blockchain, &alice, 10000)?;
// Alice transfers to Bob
let tx = blockchain.create_transaction(
&alice, // from_wallet
bob.address.clone(),
5000, // amount
10, // fee
)?;
blockchain.add_transaction(tx)?;
blockchain.mine_pending_transactions(alice.address.clone())?;
// Check balances
println!("Alice: {}", blockchain.get_balance(&alice.address));
println!("Bob: {}", blockchain.get_balance(&bob.address));
Ok(())
}
}
Case 2: Batch Wallet Creation
#![allow(unused)]
fn main() {
fn create_wallet_pool(count: usize) -> Vec<Wallet> {
let mut wallets = Vec::new();
for i in 0..count {
let wallet = Wallet::new();
println!("Wallet #{}: {}", i, &wallet.address[..16]);
wallets.push(wallet);
}
wallets
}
// Usage
let users = create_wallet_pool(100); // Create 100 wallets
}
Case 3: Wallet Import/Export
#![allow(unused)]
fn main() {
use serde_json;
// Export wallet (encrypted)
fn export_wallet(wallet: &Wallet, password: &str) -> Result<String, String> {
let wallet_json = serde_json::to_string(wallet)?;
let encrypted = encrypt(&wallet_json, password);
Ok(encrypted)
}
// Import wallet
fn import_wallet(encrypted_data: &str, password: &str) -> Result<Wallet, String> {
let decrypted = decrypt(encrypted_data, password)?;
let wallet: Wallet = serde_json::from_str(&decrypted)?;
Ok(wallet)
}
// Usage
let wallet = Wallet::new();
let backup = export_wallet(&wallet, "strong_password")?;
save_to_file("wallet_backup.enc", &backup)?;
// Restore
let backup_data = read_from_file("wallet_backup.enc")?;
let recovered = import_wallet(&backup_data, "strong_password")?;
}
Case 4: Multisig Wallet Integration
#![allow(unused)]
fn main() {
use bitcoin_simulation::multisig::MultiSigAddress;
fn create_multisig_wallet() -> Result<MultiSigAddress, String> {
// Create participant wallets
let alice = Wallet::new();
let bob = Wallet::new();
let charlie = Wallet::new();
// Collect public keys
let public_keys = vec![
alice.public_key,
bob.public_key,
charlie.public_key,
];
// Create 2-of-3 multisig address
let multisig = MultiSigAddress::new(2, public_keys)?;
println!("Multisig address: {}", multisig.address);
Ok(multisig)
}
}
Differences from Real Bitcoin
| Feature | SimpleBTC | Real Bitcoin |
|---|---|---|
| Private key generation | Random string | 256-bit random number |
| Public key derivation | SHA256 | secp256k1 elliptic curve |
| Address format | 40-character hex | Base58/Bech32 encoding |
| Signing algorithm | SHA256 | ECDSA |
| Signature verification | Simplified check | Full mathematical verification |
Real Bitcoin flow:
Private key (256 bits)
↓ secp256k1
Public key (33/65 bytes)
↓ SHA256 + RIPEMD160
Public key hash (20 bytes)
↓ Version + checksum + Base58
Address (25-34 chars)
Security Recommendations
1. Private Key Protection
#![allow(unused)]
fn main() {
// Use OS keyring
use keyring::Entry;
fn store_private_key(address: &str, private_key: &str) -> Result<(), String> {
let entry = Entry::new("SimpleBTC", address)?;
entry.set_password(private_key)?;
Ok(())
}
fn retrieve_private_key(address: &str) -> Result<String, String> {
let entry = Entry::new("SimpleBTC", address)?;
let private_key = entry.get_password()?;
Ok(private_key)
}
}
2. Multiple Backups
- ✅ Paper wallet (fireproof and waterproof)
- ✅ Hardware wallet (Ledger, Trezor)
- ✅ Encrypted USB drive (offsite storage)
- ✅ Secret sharing (Shamir’s Secret Sharing)
3. Regular Audits
#![allow(unused)]
fn main() {
fn audit_wallets(wallets: &[Wallet]) {
for (i, wallet) in wallets.iter().enumerate() {
println!("Wallet #{}", i);
println!(" Address: {}", wallet.address);
println!(" Public key present: {}", !wallet.public_key.is_empty());
println!(" Private key present: {}", !wallet.private_key.is_empty());
// Test signing
let test_sig = wallet.sign("test");
assert!(Wallet::verify_signature(
&wallet.public_key,
"test",
&test_sig
));
}
}
}
Frequently Asked Questions
Q: How do I recover a lost wallet?
A: You can only recover from a backed-up private key. If the private key is lost, the bitcoin is permanently lost.
Q: Can I derive a private key from an address?
A: No. Addresses are one-way hashes, computationally irreversible.
Q: Can one private key generate multiple addresses?
A: Hierarchical Deterministic Wallets (HD Wallet, BIP32) can derive multiple key pairs from a single seed.
Q: How do I know if a wallet has been compromised?
A: Monitor transactions on the blockchain. If unauthorized transactions appear, the private key has been leaked.
References
- Transaction API - Using wallets to create transactions
- MultiSig API - Multisig wallets
- Quick Start - Wallet usage tutorial
- BIP32 - HD Wallets
- BIP39 - Mnemonic Phrases
UTXO API
The UTXO module manages all unspent transaction outputs and is the core of Bitcoin’s accounting model.
Data Structures
UTXOSet
#![allow(unused)]
fn main() {
pub struct UTXOSet {
// key: txid (transaction ID)
// value: Vec<(vout_index, TxOutput)>
utxos: HashMap<String, Vec<(usize, TxOutput)>>,
}
}
Internal structure:
HashMap {
"tx1": [(0, Output{value: 100, addr: "alice"}),
(1, Output{value: 50, addr: "bob"})],
"tx2": [(0, Output{value: 200, addr: "charlie"})],
}
Core Concepts
UTXO Model vs. Account Model
Account Model (Ethereum)
Account balances:
Alice: 100 BTC
Bob: 50 BTC
Transfer: Alice → Bob (30 BTC)
Alice: 70 BTC (-30)
Bob: 80 BTC (+30)
UTXO Model (Bitcoin)
UTXO set:
tx1:0 → 100 BTC (Alice)
tx1:1 → 50 BTC (Bob)
Transfer: Alice → Bob (30 BTC)
Spend: tx1:0 (100 BTC)
Create:
tx2:0 → 30 BTC (Bob)
tx2:1 → 70 BTC (Alice, change)
New UTXO set:
tx1:1 → 50 BTC (Bob)
tx2:0 → 30 BTC (Bob)
tx2:1 → 70 BTC (Alice)
Advantages of the UTXO Model
-
Better privacy
- Can use a new address each time
- Harder to trace fund flows
-
Parallel processing
- Different UTXOs can be validated concurrently
- No account locking issues
-
Simplified validation
- Only need to check UTXO existence
- No need for account history
Methods
Initialization
new
#![allow(unused)]
fn main() {
pub fn new() -> Self
}
Creates an empty UTXO set.
Example:
#![allow(unused)]
fn main() {
use bitcoin_simulation::utxo::UTXOSet;
let mut utxo_set = UTXOSet::new();
}
UTXO Management
add_transaction
#![allow(unused)]
fn main() {
pub fn add_transaction(&mut self, tx: &Transaction)
}
Adds all outputs of a transaction to the UTXO set.
Process:
- Iterate over all outputs of the transaction
- Mark each output as unspent
- Add to the UTXO set
Parameters:
tx- The transaction to add
Note: Only adds outputs, does not process inputs
Example:
#![allow(unused)]
fn main() {
let tx = Transaction::new(...);
utxo_set.add_transaction(&tx);
// Now all of tx's outputs can be spent
}
remove_utxo
#![allow(unused)]
fn main() {
pub fn remove_utxo(&mut self, txid: &str, vout: usize)
}
Removes a spent UTXO.
Double-spend protection:
- A UTXO can only be spent once
- Removed from the set immediately after spending
- A second reference will fail
Parameters:
txid- Transaction IDvout- Output index
Example:
#![allow(unused)]
fn main() {
// Spend a UTXO
utxo_set.remove_utxo("tx1", 0);
// Attempt to spend again (fails)
// UTXO does not exist
}
process_transaction
#![allow(unused)]
fn main() {
pub fn process_transaction(&mut self, tx: &Transaction) -> bool
}
Fully processes a transaction (removes inputs, adds outputs).
ACID properties:
Atomicity:
#![allow(unused)]
fn main() {
// Either fully succeeds or fully fails
if !tx.verify() {
return false; // No modifications made
}
// All processed
}
Consistency:
#![allow(unused)]
fn main() {
// Before and after processing: input_sum = output_sum + fee
assert_eq!(input_sum, output_sum + fee);
}
Steps:
- Validate the transaction
- Remove UTXOs referenced by inputs
- Add newly created outputs
Parameters:
tx- The transaction to process
Return value:
true- Processing successfulfalse- Transaction is invalid
Example:
#![allow(unused)]
fn main() {
let tx = blockchain.create_transaction(&alice, bob.address, 1000, 10)?;
if utxo_set.process_transaction(&tx) {
println!("✓ UTXO updated successfully");
} else {
println!("✗ Invalid transaction");
}
}
Query Operations
find_utxos
#![allow(unused)]
fn main() {
pub fn find_utxos(&self, address: &str) -> Vec<(String, usize, u64)>
}
Finds all UTXOs for an address.
Return format: Vec<(txid, vout, value)>
Parameters:
address- The address to query
Return value: List of UTXOs
Example:
#![allow(unused)]
fn main() {
let utxos = utxo_set.find_utxos(&alice.address);
println!("Alice's UTXOs:");
for (txid, vout, value) in utxos {
println!(" {}:{} → {} sat", &txid[..8], vout, value);
}
// Output:
// Alice's UTXOs:
// tx1:0 → 5000 sat
// tx2:1 → 3000 sat
// tx5:0 → 2000 sat
}
find_spendable_outputs
#![allow(unused)]
fn main() {
pub fn find_spendable_outputs(
&self,
address: &str,
amount: u64
) -> Option<(u64, Vec<(String, usize)>)>
}
Finds a combination of UTXOs usable for payment.
UTXO selection strategies:
-
Greedy algorithm (current implementation):
#![allow(unused)] fn main() { accumulated = 0 for utxo in utxos: accumulated += utxo.value if accumulated >= amount: return utxos } -
Optimal match (possible optimization):
- Select the combination closest in total to the target
- Reduces change, saving on fees
-
Smallest UTXO first:
- Prefer small UTXOs
- Avoids UTXO fragmentation
Parameters:
address- Sender addressamount- Required amount (including fee)
Return value:
Some((accumulated, utxo_list))- Sufficient UTXOs foundaccumulated: Accumulated amountutxo_list: Selected UTXO list
None- Insufficient balance
Example:
#![allow(unused)]
fn main() {
// Need 1000 sat (including fee)
let result = utxo_set.find_spendable_outputs(&alice.address, 1000);
match result {
Some((accumulated, utxos)) => {
println!("✓ Found sufficient UTXOs");
println!(" Accumulated amount: {} sat", accumulated);
println!(" UTXOs used: {}", utxos.len());
println!(" Change: {} sat", accumulated - 1000);
}
None => {
println!("✗ Insufficient balance");
}
}
}
get_balance
#![allow(unused)]
fn main() {
pub fn get_balance(&self, address: &str) -> u64
}
Calculates the total balance for an address.
Computation method:
#![allow(unused)]
fn main() {
balance = sum(all_utxos.value)
}
Parameters:
address- The address to query
Return value: Balance (satoshi)
Example:
#![allow(unused)]
fn main() {
let balance = utxo_set.get_balance(&alice.address);
println!("Balance: {} satoshi", balance);
println!("Balance: {:.8} BTC", balance as f64 / 100_000_000.0);
// Batch query
let addresses = vec![alice.address, bob.address, charlie.address];
for addr in addresses {
let bal = utxo_set.get_balance(&addr);
println!("{}: {} sat", &addr[..10], bal);
}
}
Use Cases
Case 1: Selecting UTXOs When Creating a Transaction
#![allow(unused)]
fn main() {
fn create_payment(
utxo_set: &UTXOSet,
from: &Wallet,
to: &str,
amount: u64,
fee: u64
) -> Result<Transaction, String> {
let total_needed = amount + fee;
// 1. Find available UTXOs
let result = utxo_set.find_spendable_outputs(&from.address, total_needed);
let (accumulated, utxo_refs) = result.ok_or("Insufficient balance")?;
// 2. Build inputs
let mut inputs = Vec::new();
for (txid, vout) in utxo_refs {
let signature = from.sign(&format!("{}{}", txid, vout));
inputs.push(TxInput::new(txid, vout, signature, from.public_key.clone()));
}
// 3. Build outputs
let mut outputs = vec![
TxOutput::new(amount, to.to_string()), // To recipient
];
// 4. Change
if accumulated > total_needed {
outputs.push(TxOutput::new(
accumulated - total_needed,
from.address.clone()
));
}
// 5. Create transaction
Ok(Transaction::new(inputs, outputs, current_timestamp(), fee))
}
}
Case 2: Detailed Balance Query
#![allow(unused)]
fn main() {
fn balance_breakdown(utxo_set: &UTXOSet, address: &str) {
let utxos = utxo_set.find_utxos(address);
let total = utxo_set.get_balance(address);
println!("=== Balance Details ===");
println!("Address: {}", &address[..20]);
println!("Total balance: {} sat ({:.8} BTC)", total, total as f64 / 1e8);
println!("UTXO count: {}", utxos.len());
println!("\nUTXO list:");
for (i, (txid, vout, value)) in utxos.iter().enumerate() {
println!(" #{}: {}:{} → {} sat",
i + 1, &txid[..8], vout, value);
}
// Statistics
if !utxos.is_empty() {
let avg = total / utxos.len() as u64;
let max = utxos.iter().map(|(_, _, v)| v).max().unwrap();
let min = utxos.iter().map(|(_, _, v)| v).min().unwrap();
println!("\nStatistics:");
println!(" Average: {} sat", avg);
println!(" Maximum: {} sat", max);
println!(" Minimum: {} sat", min);
}
}
}
Case 3: UTXO Consolidation
#![allow(unused)]
fn main() {
fn consolidate_utxos(
blockchain: &mut Blockchain,
wallet: &Wallet
) -> Result<(), String> {
let utxos = blockchain.utxo_set.find_utxos(&wallet.address);
// If too many UTXOs (>50), consolidate into 1
if utxos.len() > 50 {
println!("Starting UTXO consolidation...");
println!(" Current UTXO count: {}", utxos.len());
// Create a self-to-self transaction consolidating all UTXOs
let total = blockchain.get_balance(&wallet.address);
let fee = 100; // Fixed fee
let tx = blockchain.create_transaction(
wallet,
wallet.address.clone(),
total - fee,
fee,
)?;
blockchain.add_transaction(tx)?;
blockchain.mine_pending_transactions(wallet.address.clone())?;
let new_utxos = blockchain.utxo_set.find_utxos(&wallet.address);
println!("✓ Consolidation complete");
println!(" New UTXO count: {}", new_utxos.len());
}
Ok(())
}
}
Case 4: UTXO Audit
#![allow(unused)]
fn main() {
fn audit_utxo_set(utxo_set: &UTXOSet, blockchain: &Blockchain) -> bool {
println!("=== UTXO Audit ===");
// 1. Count total UTXOs
let total_utxos: usize = utxo_set.utxos.values()
.map(|v| v.len())
.sum();
println!("Total UTXO count: {}", total_utxos);
// 2. Sum total value
let mut total_value = 0u64;
for outputs in utxo_set.utxos.values() {
for (_, output) in outputs {
total_value += output.value;
}
}
println!("Total value: {} sat", total_value);
// 3. Validate each UTXO
let mut valid = true;
for (txid, outputs) in &utxo_set.utxos {
// Verify the transaction exists on the blockchain
let tx_exists = blockchain.chain.iter()
.any(|block| block.transactions.iter()
.any(|tx| &tx.id == txid));
if !tx_exists {
println!("✗ Warning: UTXO references non-existent transaction {}", txid);
valid = false;
}
}
if valid {
println!("✓ UTXO set is complete");
}
valid
}
}
Performance Optimization
1. Index Optimization
#![allow(unused)]
fn main() {
// Create an index for addresses
pub struct IndexedUTXOSet {
utxos: HashMap<String, Vec<(usize, TxOutput)>>,
// New: address index
address_index: HashMap<String, Vec<(String, usize)>>,
}
impl IndexedUTXOSet {
pub fn find_utxos(&self, address: &str) -> Vec<(String, usize, u64)> {
// O(1) lookup instead of O(n)
if let Some(refs) = self.address_index.get(address) {
refs.iter()
.filter_map(|(txid, vout)| {
self.utxos.get(txid)
.and_then(|outputs| outputs.iter()
.find(|(idx, _)| idx == vout)
.map(|(_, output)| (txid.clone(), *vout, output.value))
)
})
.collect()
} else {
vec![]
}
}
}
}
2. Batch Operations
#![allow(unused)]
fn main() {
// Process transactions in batch
pub fn process_transactions(&mut self, txs: &[Transaction]) -> bool {
// 1. Validate all transactions
for tx in txs {
if !tx.verify() {
return false;
}
}
// 2. Batch update UTXOs
for tx in txs {
// Remove inputs
if !tx.is_coinbase() {
for input in &tx.inputs {
self.remove_utxo(&input.txid, input.vout);
}
}
// Add outputs
self.add_transaction(tx);
}
true
}
}
3. Balance Caching
#![allow(unused)]
fn main() {
pub struct CachedUTXOSet {
utxos: HashMap<String, Vec<(usize, TxOutput)>>,
balance_cache: HashMap<String, u64>, // Balance cache
}
impl CachedUTXOSet {
pub fn get_balance(&mut self, address: &str) -> u64 {
// Check cache
if let Some(balance) = self.balance_cache.get(address) {
return *balance;
}
// Compute and cache
let balance = self.calculate_balance(address);
self.balance_cache.insert(address.to_string(), balance);
balance
}
fn invalidate_cache(&mut self, address: &str) {
self.balance_cache.remove(address);
}
}
}
Comparison with Ethereum Account Model
| Feature | UTXO Model (Bitcoin) | Account Model (Ethereum) |
|---|---|---|
| State | Stateless (only UTXO set) | Stateful (account balances, nonce) |
| Balance | Computed value (UTXO sum) | Stored value (directly stored) |
| Transfer | Spend UTXOs, create new UTXOs | Account balance increases/decreases |
| Privacy | Better (can use new address) | Worse (reuse of addresses) |
| Parallelism | Easy to validate in parallel | Requires sequential processing (nonce) |
| Complexity | Complex transaction construction | Simple transactions |
| Smart contracts | Limited (Script) | Flexible (EVM) |
Frequently Asked Questions
Q: Why use the UTXO model?
A:
- ✅ Better privacy (new address each time)
- ✅ Parallel validation (different UTXOs are independent)
- ✅ Simplified validation logic
- ✅ Natural double-spend prevention
Q: Will UTXOs keep accumulating?
A: Yes. Solutions:
- UTXO consolidation (merge multiple small UTXOs)
- Higher transaction fees (limit junk UTXOs)
- UTXO commitments (reduce storage)
Q: How do I prevent UTXO fragmentation?
A:
#![allow(unused)]
fn main() {
// Consolidate periodically
if utxos.len() > threshold {
consolidate_utxos();
}
// Prioritize small UTXOs
utxos.sort_by_key(|u| u.value); // Smallest first
}
Q: What if UTXOs are lost?
A: As long as you have the private key, you can rebuild the UTXO set from the blockchain:
#![allow(unused)]
fn main() {
fn rebuild_utxo_set(blockchain: &Blockchain, address: &str) -> UTXOSet {
let mut utxo_set = UTXOSet::new();
for block in &blockchain.chain {
for tx in &block.transactions {
utxo_set.process_transaction(tx);
}
}
utxo_set
}
}
References
- Transaction API - Creating and spending UTXOs
- Blockchain API - UTXO set management
- Basic Concepts - UTXO Model
- Bitcoin Whitepaper
Advanced Modules
SimpleBTC’s advanced modules build a complete set of Bitcoin protocol features on top of the core blockchain functionality. These modules work together to cover the full stack — from data integrity verification to complex multi-party signatures, and from lightweight payment verification to script language execution.
Module Overview
| Module | Source File | Core Functionality |
|---|---|---|
| Merkle Tree | src/merkle.rs | Data integrity verification, SPV proof generation and verification |
| Multisig | src/multisig.rs | M-of-N multi-party signature addresses and transaction construction |
| Advanced Transactions | src/advanced_tx.rs | RBF replacement mechanism, timelocks, fee estimation |
| Mempool | src/mempool.rs | Unconfirmed transaction management and priority ordering |
| Script Engine | src/script.rs | Bitcoin Script subset interpretation and execution |
| SPV | src/spv.rs | Lightweight payment verification client |
Merkle Tree
Source file: src/merkle.rs | Documentation: Merkle API
The Merkle tree is the foundation of blockchain data integrity. SimpleBTC uses SHA256 to build a binary hash tree, aggregating all transaction hashes in a block into a single 32-byte merkle_root stored in the block header.
The core value lies in supporting SPV (Simplified Payment Verification): a light wallet does not need to download the full block (1–2 MB); it only needs to obtain the block header (80 bytes) and an O(log n) hash path to cryptographically prove that a transaction has been confirmed.
#![allow(unused)]
fn main() {
use simplebtc::merkle::MerkleTree;
let tx_ids = vec!["tx1_hash".to_string(), "tx2_hash".to_string()];
let tree = MerkleTree::new(&tx_ids);
let root = tree.get_root_hash();
// Generate and verify an SPV proof
let proof = tree.get_proof("tx1_hash").unwrap();
let valid = MerkleTree::verify_proof("tx1_hash", &proof, &root, 0);
}
The Merkle tree is called internally by Block::new() to compute merkle_root, and is also used by Block::verify_transaction_inclusion() for SPV verification.
Multisig
Source file: src/multisig.rs | Documentation: MultiSig API
Multisig implements Bitcoin’s M-of-N signature scheme: N participants each hold an ECDSA key pair, and at least M of them must sign to authorize fund movement. This is the core mechanism in the Bitcoin protocol for implementing distributed control and risk distribution.
Typical use cases include: 2-of-3 corporate fund management (preventing single-person embezzlement), 2-of-3 third-party escrow (buyer-seller-arbitrator), and personal multi-device backup (recovery is still possible even if the primary key is lost).
#![allow(unused)]
fn main() {
use simplebtc::multisig::{MultiSigAddress, MultiSigTxBuilder};
use simplebtc::wallet::Wallet;
// Create a 2-of-3 multisig address
let (w1, w2, w3) = (Wallet::new(), Wallet::new(), Wallet::new());
let pub_keys = vec![w1.public_key.clone(), w2.public_key.clone(), w3.public_key.clone()];
let ms_addr = MultiSigAddress::new(2, pub_keys).unwrap();
// Collect signatures (any two participants can sign)
let mut builder = MultiSigTxBuilder::new(ms_addr);
builder.add_signature(&w1, "transaction data").unwrap();
builder.add_signature(&w2, "transaction data").unwrap();
assert!(builder.is_complete());
}
Multisig addresses start with "3" (corresponding to Bitcoin’s P2SH address format), generated via script hash, and support up to 15 participating keys.
Advanced Transactions
Source file: src/advanced_tx.rs | Documentation: Advanced TX API
The advanced transaction module provides three key features that address real engineering problems in the Bitcoin network:
RBF (Replace-By-Fee): Allows users to replace an unconfirmed transaction with a new one bearing a higher fee, thereby accelerating confirmation or canceling an erroneous transaction. RBFManager maintains a list of replaceable transactions and enforces replacement validation rules (same inputs, higher fee, increment meets minimum requirement).
TimeLock: Restricts a transaction from being mined before a specified time or block height. Supports two types: Unix timestamp-based (new_time_based) and block height-based (new_height_based). Commonly used for time deposits, inheritance, smart contracts, and similar scenarios.
TxPriorityCalculator: Recommends a reasonable fee based on transaction size and urgency (Low/Medium/High/Urgent), and calculates a composite priority score (70% fee rate weight + 30% priority weight).
#![allow(unused)]
fn main() {
use simplebtc::advanced_tx::{AdvancedTxBuilder, TimeLock, RBFManager, TxPriorityCalculator, FeeUrgency};
// Combined RBF + timelock usage
let timelock = TimeLock::new_height_based(850_000);
let builder = AdvancedTxBuilder::new()
.with_rbf()
.with_timelock(timelock);
// Recommended fee
let fee = TxPriorityCalculator::recommend_fee(250, FeeUrgency::High); // 250-byte transaction
println!("Recommended fee: {} satoshi", fee); // ~5000 satoshi
}
Mempool
Source file: src/mempool.rs
The mempool (Memory Pool) stores all transactions that have been broadcast but not yet packed into a block. Miners select transactions from the mempool by priority to build new blocks.
Main responsibilities:
- Receive and temporarily store broadcast transactions
- Sort by fee rate, offering high-value transactions first to miners
- Detect and reject double-spend attempts
- Handle transaction replacement in conjunction with the RBF mechanism
- Remove packed transactions from the pool after block confirmation
#![allow(unused)]
fn main() {
use simplebtc::mempool::Mempool;
let mut pool = Mempool::new();
pool.add_transaction(tx);
let pending = pool.get_pending_transactions(10); // Get the 10 highest-fee transactions
}
Script Engine
Source file: src/script.rs
Bitcoin Script is a simple stack-based scripting language used to define the spending conditions for transactions. SimpleBTC implements the core subset of Script, supporting the most common transaction types.
Supported script types:
- P2PKH (Pay-to-Public-Key-Hash): The most common ordinary address transaction; lock script format is
OP_DUP OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG. - P2SH (Pay-to-Script-Hash): The basis for multisig and complex contracts; addresses starting with
"3". - OP_RETURN: Writes arbitrary non-spendable data on-chain (up to 80 bytes).
The script engine provides the underlying support for the multisig module: the script field of MultiSigAddress stores a simplified Script locking script.
SPV (Simplified Payment Verification)
Source file: src/spv.rs
SPV simulates the operation of Bitcoin light wallets: validating the legitimacy of transactions without downloading the full blockchain. This is critical for resource-constrained devices (mobile phones, embedded systems).
SPV verification flow:
- Download only block headers (each approximately 80 bytes, all headers approximately 60 MB)
- Verify the proof of work (PoW) of the block headers
- Request the Merkle proof for the target transaction (a few hundred bytes)
- Execute
MerkleTree::verify_proof()locally
Full node mode: Download full blockchain (~500 GB) → Full local verification
SPV mode: Download block headers (~60 MB) + Merkle proof (few KB) → O(log n) verification
The SPV module is deeply integrated with the Merkle module, relying on MerkleTree::get_proof() and MerkleTree::verify_proof() for lightweight verification.
Module Dependencies
Core Modules
├── Transaction (src/transaction.rs)
├── Wallet (src/wallet.rs)
└── Block (src/block.rs)
│
▼
Advanced Modules (built on top of core modules)
├── Merkle ← Used internally by Block (computing merkle_root and SPV proofs)
├── MultiSig ← Depends on Wallet (ECDSA signing) + Script (locking scripts)
├── AdvancedTx ← Depends on Transaction (RBF replacement validation)
├── Mempool ← Depends on Transaction + AdvancedTx (RBF support)
├── Script ← Foundation for MultiSig and SPV
└── SPV ← Depends on Merkle (proof verification) + Block (block headers)
Quick Navigation
- Merkle API — Merkle tree data structure and SPV proofs
- MultiSig API — M-of-N multisig
- Advanced TX API — RBF, timelocks, fee calculation
- Block API — Block structure and mining
- Transaction API — Transaction construction and validation
- Wallet API — Wallet and key management
Merkle API
The Merkle tree (hash tree) is implemented in src/merkle.rs and is the core data structure for blockchain data integrity verification. SimpleBTC uses SHA256 to build a binary Merkle tree, aggregating all transaction hashes in a block into a single root hash (merkle_root) stored in the block header.
Data Structures
MerkleNode Struct
A single node in the Merkle tree, which can be a leaf node (corresponding to one transaction) or an internal node (corresponding to the hash of its child node hashes).
#![allow(unused)]
fn main() {
#[derive(Debug, Clone)]
pub struct MerkleNode {
pub hash: String, // SHA256 hash of the node (64-character hex)
pub left: Option<Box<MerkleNode>>, // Left child node (None for leaf nodes)
pub right: Option<Box<MerkleNode>>, // Right child node (None for leaf nodes)
}
}
| Field | Type | Description |
|---|---|---|
hash | String | Node hash. For leaf nodes: SHA256(transaction ID); for internal nodes: SHA256(left_hash + right_hash). |
left | Option<Box<MerkleNode>> | Left child node. None for leaf nodes. |
right | Option<Box<MerkleNode>> | Right child node. None for leaf nodes. |
MerkleNode Methods
#![allow(unused)]
fn main() {
// Create a leaf node from raw data (computes SHA256 hash)
pub fn new_leaf(data: &str) -> Self
// Create an internal node from two child nodes (hash = SHA256(left_hash + right_hash))
pub fn new_internal(left: MerkleNode, right: MerkleNode) -> Self
}
MerkleTree Struct
The complete Merkle tree, holding the root node and the original leaf data list.
#![allow(unused)]
fn main() {
#[derive(Debug, Clone)]
pub struct MerkleTree {
pub root: Option<MerkleNode>, // Root node (None when transaction list is empty)
pub leaves: Vec<String>, // Original leaf data list (transaction ID list)
}
}
| Field | Type | Description |
|---|---|---|
root | Option<MerkleNode> | The root node of the tree. None when input is empty. |
leaves | Vec<String> | The original transaction ID list passed in during construction (unhashed). |
Tree Structure Illustration
Example with 4 transactions:
Root
/ \
H12 H34
/ \ / \
H1 H2 H3 H4
│ │ │ │
tx1 tx2 tx3 tx4
Where:
H1 = SHA256(tx1)
H2 = SHA256(tx2)
H12 = SHA256(H1 + H2)
H34 = SHA256(H3 + H4)
Root = SHA256(H12 + H34)
Handling odd number of transactions: If a layer has an odd number of nodes, the last node is duplicated to form a pair (e.g., with 3 transactions, tx3 is duplicated as tx3’).
Methods
MerkleTree::new
Builds a complete Merkle tree from a list of transaction IDs. Uses a bottom-up approach to build layer by layer; time complexity O(n).
#![allow(unused)]
fn main() {
pub fn new(transactions: &[String]) -> Self
}
Parameters:
transactions— A slice of transaction IDs (or arbitrary strings). Can be empty, in which caserootisNone.
Return value: A fully constructed MerkleTree instance.
#![allow(unused)]
fn main() {
use simplebtc::merkle::MerkleTree;
// Build tree from transaction ID list
let tx_ids = vec![
"tx_hash_1".to_string(),
"tx_hash_2".to_string(),
"tx_hash_3".to_string(),
"tx_hash_4".to_string(),
];
let tree = MerkleTree::new(&tx_ids);
// Handle empty transaction list
let empty_tree = MerkleTree::new(&[]);
assert!(empty_tree.root.is_none());
}
MerkleTree::get_root_hash
Gets the root hash string of the Merkle tree. This value is stored in the merkle_root field of the block header.
#![allow(unused)]
fn main() {
pub fn get_root_hash(&self) -> String
}
Return value:
- 64-character lowercase hexadecimal SHA256 hash string (when tree is non-empty).
- Empty string
""(when tree is empty, i.e.,rootisNone).
#![allow(unused)]
fn main() {
let tree = MerkleTree::new(&tx_ids);
let root_hash = tree.get_root_hash();
println!("Merkle root: {}", root_hash);
// Output: a3f7c2e1b4d9...(64-character hex)
// Compare with the value stored in the block
assert_eq!(root_hash, block.merkle_root);
}
MerkleTree::get_proof
Generates a Merkle proof (SPV proof) for a specified transaction. The proof is a set of sibling node hashes; an SPV client uses these hashes to reconstruct the root hash layer by layer from the leaf, without accessing the full block.
#![allow(unused)]
fn main() {
pub fn get_proof(&self, tx_hash: &str) -> Option<Vec<String>>
}
Parameters:
tx_hash— The transaction ID to generate a proof for (must exist inself.leaves).
Return value:
Some(Vec<String>)— List of sibling node hashes needed for the proof, ordered from the leaf layer to the root layer.None— Transaction ID does not exist in this Merkle tree.
Proof size: For a block with n transactions, the proof contains ceil(log2(n)) hashes, each 32 bytes. For example, a block with 2000 transactions requires only approximately 352 bytes of proof (11 hashes).
#![allow(unused)]
fn main() {
let tree = MerkleTree::new(&tx_ids);
match tree.get_proof("tx_hash_1") {
Some(proof) => {
println!("Proof contains {} sibling hashes", proof.len());
for (i, hash) in proof.iter().enumerate() {
println!(" Layer {}: {}", i, &hash[..16]);
}
}
None => println!("Transaction does not exist in this Merkle tree"),
}
}
MerkleTree::verify_proof
Verifies a Merkle proof (static method). This is the core function of SPV lightweight verification: using the sibling hashes in the proof, it computes upward layer by layer from the leaf node, verifying whether the final result matches the merkle_root in the block header.
#![allow(unused)]
fn main() {
pub fn verify_proof(
tx_hash: &str,
proof: &[String],
root_hash: &str,
index: usize,
) -> bool
}
Parameters:
tx_hash— The transaction ID string to verify (raw value, not hashed).proof— List of sibling node hashes generated byget_proof().root_hash— Themerkle_rootvalue stored in the block header.index— The position index of the transaction in the block’s transaction list (starting from 0), used to determine left/right merge order.
Return value:
true— Proof is valid; the transaction is indeed included in the corresponding block.false— Proof is invalid; the transaction is not in this block, or data has been tampered with.
Verification algorithm:
Input: tx_hash, proof = [sibling_0, sibling_1, ...], root_hash, index
Steps:
current = SHA256(tx_hash)
For each sibling_hash in proof:
If index is even (current node is on the left):
current = SHA256(current + sibling_hash)
If index is odd (current node is on the right):
current = SHA256(sibling_hash + current)
index = index / 2
Final: current == root_hash → verification passes
#![allow(unused)]
fn main() {
use simplebtc::merkle::MerkleTree;
let tx_ids = vec![
"tx1".to_string(),
"tx2".to_string(),
"tx3".to_string(),
"tx4".to_string(),
];
let tree = MerkleTree::new(&tx_ids);
let root = tree.get_root_hash();
// Generate proof
let proof = tree.get_proof("tx1").expect("Transaction exists");
// Verify proof (index=0, tx1 is the first transaction)
let is_valid = MerkleTree::verify_proof("tx1", &proof, &root, 0);
assert!(is_valid, "SPV proof verification failed");
println!("Transaction tx1 confirmed included in block");
// Tamper test: proof becomes invalid after modifying transaction content
let tampered = MerkleTree::verify_proof("tx1_TAMPERED", &proof, &root, 0);
assert!(!tampered, "Proof should be invalid after tampering");
}
Complete Usage Examples
Example 1: Integration with a Block
use simplebtc::block::Block;
use simplebtc::merkle::MerkleTree;
use simplebtc::transaction::Transaction;
use simplebtc::wallet::Wallet;
fn main() {
// Simulate packing 4 transactions
let miner = Wallet::new();
let alice = Wallet::new();
let bob = Wallet::new();
let transactions = vec![
Transaction::new_coinbase(&miner.address, 3_125_000),
Transaction::new(&alice, &bob.address, 100_000, 1_000),
Transaction::new(&alice, &miner.address, 50_000, 500),
Transaction::new(&bob, &alice.address, 20_000, 200),
];
// Block::new internally builds a MerkleTree and computes merkle_root
let block = Block::new(1, transactions, "000000abc...".to_string());
println!("Merkle root: {}", block.merkle_root);
// SPV verification: is tx[2] in this block?
let tx_id = block.transactions[2].id.clone();
let included = block.verify_transaction_inclusion(&tx_id, 2);
println!("Transaction included in block: {}", included);
}
Example 2: Using MerkleTree Standalone
#![allow(unused)]
fn main() {
use simplebtc::merkle::MerkleTree;
fn spv_demo() {
// Full node builds complete Merkle tree
let tx_ids: Vec<String> = (1..=8)
.map(|i| format!("transaction_{:04}", i))
.collect();
let tree = MerkleTree::new(&tx_ids);
let root = tree.get_root_hash();
println!("Merkle root for 8 transactions: {}", root);
// Generate SPV proof for tx #5 (index=4)
let target_tx = "transaction_0005";
let proof = tree.get_proof(target_tx).expect("Transaction exists");
println!("Proof size: {} hashes (log2(8)=3 layers)", proof.len());
// SPV client verification (only needs root + proof, not full transaction list)
let verified = MerkleTree::verify_proof(target_tx, &proof, &root, 4);
println!("SPV verification result: {}", verified);
}
}
Example 3: Detecting Data Tampering
#![allow(unused)]
fn main() {
use simplebtc::merkle::MerkleTree;
fn tamper_detection() {
let original = vec!["tx_a".to_string(), "tx_b".to_string(), "tx_c".to_string()];
let tree = MerkleTree::new(&original);
let original_root = tree.get_root_hash();
// Simulate attacker modifying tx_b
let mut tampered = original.clone();
tampered[1] = "tx_b_MALICIOUS".to_string();
let tampered_tree = MerkleTree::new(&tampered);
let tampered_root = tampered_tree.get_root_hash();
// Merkle roots are completely different; tampering is immediately detected
assert_ne!(original_root, tampered_root);
println!("Original root: {}", &original_root[..16]);
println!("Tampered root: {}", &tampered_root[..16]);
println!("Tamper detected: root hash has changed");
}
}
Security Notes
Why can Merkle proofs be trusted?
For an attacker to forge a valid Merkle proof, they would need to:
- Find a SHA256 hash collision (computational complexity approximately 2^128 — currently infeasible); or
- Re-mine the block (changing
merkle_rootchanges the block hash, requiring redo of the proof of work).
Therefore, as long as an SPV client can obtain block headers protected by honest proof of work, the security of a Merkle proof is equivalent to that of a full node.
Block timestamp variance: Miners’ block timestamps are allowed to differ by approximately 2 hours, but this does not affect the security of Merkle verification (the Merkle tree does not depend on timestamps).
Related Modules
Block—Block::new()internally callsMerkleTree::new()to computemerkle_root;Block::verify_transaction_inclusion()usesget_proof()andverify_proof().- Advanced Modules — The SPV module (
src/spv.rs) is built on the Merkle API to implement a lightweight client.
MultiSig API
Multi-Signature (MultiSig) is implemented in src/multisig.rs and supports Bitcoin’s M-of-N signature scheme: out of N participants, at least M valid ECDSA signatures are required to authorize fund movement.
Core Concepts
M-of-N signatures: N participants each hold a key pair; any M of them can authorize a transaction. Common combinations:
| Type | Meaning | Typical Use Case |
|---|---|---|
| 2-of-2 | Both must agree | Joint accounts, partnerships |
| 2-of-3 | Any two of three agree | Corporate fund management, escrow services |
| 3-of-5 | Any three of five agree | Large institutions, board decisions |
Address format: Multisig addresses start with "3", corresponding to Bitcoin’s P2SH (Pay-to-Script-Hash) format.
MultiSigAddress Struct
Represents an M-of-N multisig address and its configuration.
#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MultiSigAddress {
pub address: String, // Multisig address ("3"-prefixed, P2SH format)
pub required_sigs: usize, // Required number of signatures M
pub total_keys: usize, // Total number of keys N
pub public_keys: Vec<String>, // Public key list for all participants
pub script: String, // Locking script (simplified Script code)
}
}
Field Descriptions
| Field | Type | Description |
|---|---|---|
address | String | Multisig address starting with "3", generated by taking 42 characters of the SHA256 hash of the locking script. |
required_sigs | usize | M value: minimum number of valid signatures required to spend funds. |
total_keys | usize | N value: total number of participants (equals public_keys.len()). |
public_keys | Vec<String> | Public key list of all participants (hex-encoded). |
script | String | Locking script in the format OP_{M}{pubkeys...}OP_CHECKMULTISIG. |
MultiSigAddress Methods
MultiSigAddress::new
Creates an M-of-N multisig address. Validates parameter legality, then generates the locking script and the corresponding P2SH address.
#![allow(unused)]
fn main() {
pub fn new(
required_sigs: usize,
public_keys: Vec<String>,
) -> Result<Self, String>
}
Parameters:
required_sigs— Required number of signatures M; must satisfy1 <= M <= N.public_keys— List of all participants’ public keys; length equals N, maximum 15.
Return value:
Ok(MultiSigAddress)— Created successfully.Err(String)— Invalid parameters; error reasons include:"Invalid signature requirement"—required_sigs == 0orrequired_sigs > total_keys."Maximum 15 keys supported"—public_keys.len() > 15(Bitcoin protocol limit).
#![allow(unused)]
fn main() {
use simplebtc::multisig::MultiSigAddress;
use simplebtc::wallet::Wallet;
// Create three participant wallets
let wallet1 = Wallet::new();
let wallet2 = Wallet::new();
let wallet3 = Wallet::new();
let public_keys = vec![
wallet1.public_key.clone(),
wallet2.public_key.clone(),
wallet3.public_key.clone(),
];
// Create 2-of-3 multisig address
let multisig = MultiSigAddress::new(2, public_keys).unwrap();
assert!(multisig.address.starts_with('3'));
assert_eq!(multisig.required_sigs, 2);
assert_eq!(multisig.total_keys, 3);
println!("Multisig address: {}", multisig.address);
println!("Locking script: {}", multisig.script);
// Parameter validation error examples
let result = MultiSigAddress::new(0, vec!["key1".to_string()]);
assert!(result.is_err()); // required_sigs cannot be 0
let result = MultiSigAddress::new(3, vec!["key1".to_string(), "key2".to_string()]);
assert!(result.is_err()); // required_sigs(3) > total_keys(2)
}
MultiSigAddress::verify_signatures
Quickly checks whether the number of signatures meets the requirement (does not verify signature content, only validates count).
#![allow(unused)]
fn main() {
pub fn verify_signatures(&self, signatures: &[String]) -> bool
}
Parameters:
signatures— List of signatures.
Return value: signatures.len() >= self.required_sigs.
#![allow(unused)]
fn main() {
let sigs = vec!["sig1".to_string(), "sig2".to_string()];
let count_ok = multisig.verify_signatures(&sigs);
println!("Signature count meets requirement: {}", count_ok); // true (2 >= 2)
}
MultiSigAddress::verify_signatures_with_data
Full signature verification: not only checks count, but also uses ECDSA to verify whether each signature was produced by one of the keys in public_keys.
#![allow(unused)]
fn main() {
pub fn verify_signatures_with_data(
&self,
signatures: &[String],
data: &str,
) -> bool
}
Parameters:
signatures— List of ECDSA signatures in hex DER encoding.data— The original data string that was signed (typically a transaction ID or transaction digest).
Return value:
true— Number of valid signatures >=required_sigs. Each signature matches at most one public key (preventing the same signature from being counted twice).false— Insufficient valid signatures, or signatures do not correspond to any key inpublic_keys.
#![allow(unused)]
fn main() {
// Sign transaction data with wallet1 and wallet2
let data = "Transaction digest: Alice transfers 0.01 BTC to Bob";
let sig1 = wallet1.sign(data);
let sig2 = wallet2.sign(data);
let valid = multisig.verify_signatures_with_data(
&[sig1, sig2],
data,
);
println!("ECDSA verification passed: {}", valid);
}
MultiSigTxBuilder Struct
The multisig transaction builder is responsible for collecting signatures and confirming whether the M-of-N requirement is met. Built-in duplicate signature detection.
#![allow(unused)]
fn main() {
pub struct MultiSigTxBuilder {
pub multisig_address: MultiSigAddress,
pub signatures: Vec<String>,
// signed_keys: HashMap<String, bool> // Private field, prevents same public key from signing twice
}
}
| Field | Type | Description |
|---|---|---|
multisig_address | MultiSigAddress | Associated multisig address configuration (includes M, N, public key list). |
signatures | Vec<String> | List of collected valid ECDSA signatures. |
MultiSigTxBuilder Methods
MultiSigTxBuilder::new
Creates a multisig transaction builder associated with the specified MultiSigAddress.
#![allow(unused)]
fn main() {
pub fn new(multisig_address: MultiSigAddress) -> Self
}
Parameters:
multisig_address— An already-createdMultiSigAddressinstance.
#![allow(unused)]
fn main() {
use simplebtc::multisig::MultiSigTxBuilder;
let builder = MultiSigTxBuilder::new(multisig);
assert_eq!(builder.signatures.len(), 0);
assert!(!builder.is_complete());
}
MultiSigTxBuilder::add_signature
Adds an ECDSA signature from a participant. Internally validates automatically:
- The wallet’s public key must be in
multisig_address.public_keys. - The wallet cannot sign twice (prevents the same person from signing twice to fake M signatures).
#![allow(unused)]
fn main() {
pub fn add_signature(
&mut self,
wallet: &Wallet,
data: &str,
) -> Result<(), String>
}
Parameters:
wallet— The participant’sWalletinstance (used to callwallet.sign(data)to generate a signature).data— Data to sign (typically a transaction digest or transaction ID).
Return value:
Ok(())— Signature added successfully.Err(String)— Error reasons:"This wallet is not in the multisig address"— The wallet’s public key is not in thepublic_keyslist."This wallet has already signed"— The wallet has previously signed.
#![allow(unused)]
fn main() {
let mut builder = MultiSigTxBuilder::new(multisig.clone());
let data = "transfer_tx_hash_abc123";
// wallet1 signs successfully
builder.add_signature(&wallet1, data).unwrap();
assert_eq!(builder.signatures.len(), 1);
// wallet1 cannot sign again
let err = builder.add_signature(&wallet1, data);
assert!(err.is_err());
println!("Duplicate signature error: {}", err.unwrap_err()); // "This wallet has already signed"
// A wallet not in the multisig address cannot sign
let outsider = Wallet::new();
let err = builder.add_signature(&outsider, data);
assert!(err.is_err());
println!("External wallet error: {}", err.unwrap_err()); // "This wallet is not in the multisig address"
}
MultiSigTxBuilder::is_complete
Checks whether enough signatures have been collected (signatures.len() >= required_sigs).
#![allow(unused)]
fn main() {
pub fn is_complete(&self) -> bool
}
Return value: true means the M-of-N requirement is satisfied and the transaction can be broadcast.
#![allow(unused)]
fn main() {
let mut builder = MultiSigTxBuilder::new(multisig);
assert!(!builder.is_complete()); // 0 signatures
builder.add_signature(&wallet1, data).unwrap();
assert!(!builder.is_complete()); // 1 signature, not enough yet (needs 2)
builder.add_signature(&wallet2, data).unwrap();
assert!(builder.is_complete()); // 2 signatures, satisfies 2-of-3
println!("Multisig complete, transaction can be broadcast");
}
MultiSigTxBuilder::get_signatures
Gets a copy of all collected signatures.
#![allow(unused)]
fn main() {
pub fn get_signatures(&self) -> Vec<String>
}
Return value: Vec<String> — Clone of the signature list (does not affect the builder’s internal state).
#![allow(unused)]
fn main() {
let sigs = builder.get_signatures();
println!("Collected {} signatures", sigs.len());
for (i, sig) in sigs.iter().enumerate() {
println!(" Signature {}: {}...", i + 1, &sig[..16]);
}
}
MultiSigType Enum (Convenience API)
Provides quick creation for common multisig types.
#![allow(unused)]
fn main() {
pub enum MultiSigType {
TwoOfTwo, // 2-of-2
TwoOfThree, // 2-of-3 (most common)
ThreeOfFive, // 3-of-5
}
impl MultiSigType {
pub fn create_address(&self, wallets: &[Wallet]) -> Result<MultiSigAddress, String>
}
}
#![allow(unused)]
fn main() {
use simplebtc::multisig::MultiSigType;
use simplebtc::wallet::Wallet;
let wallets: Vec<Wallet> = (0..3).map(|_| Wallet::new()).collect();
let ms_addr = MultiSigType::TwoOfThree.create_address(&wallets).unwrap();
println!("2-of-3 address: {}", ms_addr.address);
// Returns an error if the number of wallets does not match
let err = MultiSigType::ThreeOfFive.create_address(&wallets); // Requires 5 wallets
assert!(err.is_err());
}
Complete Usage Examples
Scenario 1: Corporate Fund Management (2-of-3)
#![allow(unused)]
fn main() {
use simplebtc::multisig::{MultiSigAddress, MultiSigTxBuilder};
use simplebtc::wallet::Wallet;
fn corporate_treasury() {
// Three company executives each hold one key
let ceo = Wallet::new();
let cfo = Wallet::new();
let cto = Wallet::new();
// Create 2-of-3 corporate multisig address
let pub_keys = vec![
ceo.public_key.clone(),
cfo.public_key.clone(),
cto.public_key.clone(),
];
let treasury = MultiSigAddress::new(2, pub_keys).unwrap();
println!("Corporate treasury address: {}", treasury.address);
// Initiate a payment (requires CEO + CFO signatures)
let tx_data = "Pay 10 BTC to supplier ABC";
let mut builder = MultiSigTxBuilder::new(treasury);
builder.add_signature(&ceo, tx_data).unwrap();
println!("CEO has signed, waiting for second authorization...");
builder.add_signature(&cfo, tx_data).unwrap();
println!("CFO has signed");
if builder.is_complete() {
let signatures = builder.get_signatures();
println!("Transaction authorized, signature count: {}", signatures.len());
// Here, attach signatures to the transaction and broadcast to the network
}
}
}
Scenario 2: Third-Party Escrow (Buyer-Seller-Arbitrator)
#![allow(unused)]
fn main() {
fn escrow_service() {
let buyer = Wallet::new();
let seller = Wallet::new();
let arbiter = Wallet::new();
let pub_keys = vec![
buyer.public_key.clone(),
seller.public_key.clone(),
arbiter.public_key.clone(),
];
// 2-of-3: normally buyer+seller; in a dispute, either party + arbitrator
let escrow = MultiSigAddress::new(2, pub_keys).unwrap();
println!("Escrow address: {}", escrow.address);
let release_tx = "Release escrow funds to seller address";
// Normal flow: buyer confirms receipt; buyer+seller sign to release funds
let mut builder = MultiSigTxBuilder::new(escrow.clone());
builder.add_signature(&buyer, release_tx).unwrap();
builder.add_signature(&seller, release_tx).unwrap();
assert!(builder.is_complete());
println!("Funds released normally to seller");
// Dispute flow: arbitrator intervenes; seller+arbitrator sign to release funds
let dispute_tx = "Dispute ruling: refund to buyer";
let mut dispute_builder = MultiSigTxBuilder::new(escrow);
dispute_builder.add_signature(&buyer, dispute_tx).unwrap();
dispute_builder.add_signature(&arbiter, dispute_tx).unwrap();
assert!(dispute_builder.is_complete());
println!("Arbitration complete, funds returned to buyer");
}
}
Scenario 3: Full Signature Verification Flow
#![allow(unused)]
fn main() {
fn full_verification() {
let w1 = Wallet::new();
let w2 = Wallet::new();
let pub_keys = vec![w1.public_key.clone(), w2.public_key.clone()];
let ms = MultiSigAddress::new(2, pub_keys).unwrap(); // 2-of-2
let tx_data = "Transfer 1 BTC";
// Collect signatures
let sig1 = w1.sign(tx_data);
let sig2 = w2.sign(tx_data);
let sigs = vec![sig1, sig2];
// Full ECDSA verification (verifies signatures were produced by keys in public_keys)
let valid = ms.verify_signatures_with_data(&sigs, tx_data);
println!("ECDSA multisig verification: {}", valid);
// Quick count check (does not verify content)
let count_ok = ms.verify_signatures(&sigs);
println!("Signature count satisfied: {}", count_ok);
}
}
Limitations and Notes
- Maximum keys: Due to Bitcoin script limits, N is at most 15 (returns an error if exceeded).
- Duplicate signature prevention:
MultiSigTxBuilderinternally maintains a set of already-signed public keys; callingadd_signaturetwice with the same wallet returns an error. - Signature order: Verification does not require signatures to be in the same order as the public keys; any M valid signatures will do.
- Zero-confirmation risk: A multisig transaction is still unconfirmed before being mined. For important transactions, wait for at least 1 block confirmation.
Related Modules
Wallet— Providessign()andverify_signature()methods, the foundation of multisig ECDSA operations.AdvancedTxBuilder— Can be combined with timelocks to implement advanced scenarios such as “reduced multisig requirement after time expiry.”- Advanced Modules Overview — Understand multisig’s place in SimpleBTC’s overall architecture.
Advanced TX API
The advanced transaction module is implemented in src/advanced_tx.rs and provides three key mechanisms addressing real engineering problems in the Bitcoin network: RBF (Replace-By-Fee) (allowing acceleration or cancellation of unconfirmed transactions), TimeLock (restricting transactions from being confirmed before a specified time or block), and TxPriorityCalculator (recommending reasonable fees and calculating transaction priority).
RBFManager — Replace-By-Fee Manager
RBF (Replace-By-Fee), defined in BIP125, allows users to replace an unconfirmed transaction in the mempool with a new one bearing a higher fee, thereby accelerating confirmation or canceling an erroneous transaction.
Struct Definition
#![allow(unused)]
fn main() {
pub struct RBFManager {
// replaceable_txs: Vec<String> // Private field, stores list of replaceable transaction IDs
}
}
Methods
RBFManager::new
Creates a new RBF manager instance (replaceable transaction list is empty).
#![allow(unused)]
fn main() {
pub fn new() -> Self
}
RBFManager::mark_replaceable
Marks the specified transaction as supporting RBF replacement. Idempotent operation; marking the same transaction multiple times has no side effects.
#![allow(unused)]
fn main() {
pub fn mark_replaceable(&mut self, tx_id: &str)
}
Parameters:
tx_id— Transaction ID to mark as replaceable.
In the Bitcoin protocol, a transaction indicates RBF support by setting its nSequence field to a value less than 0xFFFFFFFE. AdvancedTxBuilder::with_rbf() automatically sets sequence to 0xFFFFFFFD.
RBFManager::is_replaceable
Checks whether a transaction has been marked as replaceable.
#![allow(unused)]
fn main() {
pub fn is_replaceable(&self, tx_id: &str) -> bool
}
RBFManager::can_replace
Validates whether a new transaction can legitimately replace the old one. Performs full RBF rule checks:
#![allow(unused)]
fn main() {
pub fn can_replace(
&self,
old_tx: &Transaction,
new_tx: &Transaction,
) -> Result<(), String>
}
Validation rules (in order):
- Replaceability check:
old_tx.idmust be in the replaceable list; otherwise returns"Original transaction does not support RBF". - Same inputs: Both transactions’ input lists have the same length, and corresponding inputs have identical
txidandvout(must spend the same UTXOs); otherwise returns"Must spend the same UTXOs". - Higher fee:
new_tx.fee > old_tx.fee; otherwise returns"New transaction fee({}) must be higher than old transaction({})". - Sufficient increment:
fee_increase >= old_tx.size()(simplified rule: fee increment must be at least as many satoshi as the old transaction’s byte count), preventing low-cost spam replacement attacks.
Return value:
Ok(())— Replacement is legitimate; the new transaction can be broadcast.Err(String)— Specific validation failure reason.
RBFManager::remove_confirmed
Removes a confirmed transaction from the replaceable list.
#![allow(unused)]
fn main() {
pub fn remove_confirmed(&mut self, tx_id: &str)
}
RBFManager Usage Example
#![allow(unused)]
fn main() {
use simplebtc::advanced_tx::RBFManager;
let mut rbf = RBFManager::new();
// Mark original transaction as supporting RBF
rbf.mark_replaceable("original_tx_001");
assert!(rbf.is_replaceable("original_tx_001"));
assert!(!rbf.is_replaceable("other_tx_002"));
// Validate whether replacement is legitimate
match rbf.can_replace(&old_tx, &new_tx) {
Ok(()) => println!("Replacement legitimate, broadcast new transaction"),
Err(e) => println!("Replacement rejected: {}", e),
}
// Remove record after transaction is confirmed
rbf.remove_confirmed("original_tx_001");
assert!(!rbf.is_replaceable("original_tx_001"));
}
TimeLock — Timelock
Timelocks restrict a transaction from being mined by miners before a specific time or block height. They are a foundational primitive for implementing advanced scenarios such as time deposits, inheritance, smart contracts, and more.
Struct Definition
#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TimeLock {
pub locktime: u64, // Lock value: Unix timestamp (seconds) or block height
pub is_block_height: bool, // true: block-height-based; false: timestamp-based
}
}
| Field | Type | Description |
|---|---|---|
locktime | u64 | Lock value. When is_block_height = true, this is the block height; otherwise it is a Unix timestamp (seconds). |
is_block_height | bool | Lock type flag. Corresponds to Bitcoin protocol: locktime < 500_000_000 = block height; >= 500_000_000 = timestamp. |
Methods
TimeLock::new_time_based
Creates a Unix timestamp-based timelock. The transaction cannot be confirmed before timestamp seconds.
#![allow(unused)]
fn main() {
pub fn new_time_based(timestamp: u64) -> Self
}
Parameters:
timestamp— Unix timestamp (seconds) for unlock time. For example,1767225600represents 2026-01-01 00:00:00 UTC.
TimeLock::new_height_based
Creates a block-height-based timelock. The transaction cannot be confirmed until the blockchain reaches the specified height.
#![allow(unused)]
fn main() {
pub fn new_height_based(height: u64) -> Self
}
Parameters:
height— Block height required for unlock. For example,900_000represents approximately mid-2027 (based on approximately 10 minutes per block).
TimeLock::is_mature
Checks whether the timelock has expired (is ready to be spent).
#![allow(unused)]
fn main() {
pub fn is_mature(&self, current_time: u64, current_height: u32) -> bool
}
Parameters:
current_time— Current Unix timestamp (seconds).current_height— Current blockchain height.
Return value:
- Time-based:
current_time >= self.locktime - Block-height-based:
current_height as u64 >= self.locktime
TimeLock::remaining
Gets how much time (seconds) or how many blocks remain until unlock.
#![allow(unused)]
fn main() {
pub fn remaining(&self, current_time: u64, current_height: u32) -> i64
}
Return value: Remaining seconds or block count. A negative value indicates the lock time has already passed (expired).
TimeLock Usage Example
#![allow(unused)]
fn main() {
use simplebtc::advanced_tx::TimeLock;
// Timestamp-based: lock until 2026-01-01 00:00:00 UTC
let time_lock = TimeLock::new_time_based(1_767_225_600);
let now = 1_740_000_000u64; // Current time (2025)
println!("Timelock expired: {}", time_lock.is_mature(now, 0)); // false
println!("Remaining until unlock: {} seconds", time_lock.remaining(now, 0));
// Block-height-based: lock until block 900,000
let block_lock = TimeLock::new_height_based(900_000);
let current_height = 850_000u32; // Current block height
println!("Block lock expired: {}", block_lock.is_mature(0, current_height)); // false
println!("Remaining until unlock: {} blocks", block_lock.remaining(0, current_height)); // 50000
// An already-expired timelock
let expired_lock = TimeLock::new_height_based(800_000);
println!("Expired: {}", expired_lock.is_mature(0, 850_000)); // true
println!("Remaining (negative = already past): {}", expired_lock.remaining(0, 850_000)); // -50000
}
AdvancedTxBuilder — Advanced Transaction Builder
AdvancedTxBuilder is a builder (Builder Pattern) for configuring advanced transaction options (RBF support and timelocks) and generating the corresponding sequence field value.
Struct Definition
#![allow(unused)]
fn main() {
pub struct AdvancedTxBuilder {
pub enable_rbf: bool,
pub timelock: Option<TimeLock>,
pub sequence: u32,
}
}
| Field | Type | Description |
|---|---|---|
enable_rbf | bool | Whether RBF support is enabled. true after with_rbf(). |
timelock | Option<TimeLock> | Associated timelock configuration. Some(TimeLock) after with_timelock(). |
sequence | u32 | Transaction input sequence number, encoding RBF and timelock state: 0xFFFFFFFF (default/no feature), 0xFFFFFFFD (RBF), 0x00000000 (timelock). |
Methods
AdvancedTxBuilder::new
Creates a default builder. RBF and timelock are disabled by default; sequence = 0xFFFFFFFF.
#![allow(unused)]
fn main() {
pub fn new() -> Self
}
AdvancedTxBuilder::with_rbf
Enables RBF support. Sets enable_rbf to true and sequence to 0xFFFFFFFD (less than 0xFFFFFFFE, compliant with BIP125).
#![allow(unused)]
fn main() {
pub fn with_rbf(mut self) -> Self
}
Return value: Self (supports method chaining).
AdvancedTxBuilder::with_timelock
Sets a timelock. Sets timelock to Some(timelock) and sequence to 0 (enables nLockTime mechanism).
#![allow(unused)]
fn main() {
pub fn with_timelock(mut self, timelock: TimeLock) -> Self
}
Parameters:
timelock— TheTimeLockinstance to associate.
Return value: Self (supports method chaining).
AdvancedTxBuilder::get_sequence
Gets the final sequence field value, which should be written to the transaction input’s nSequence field.
#![allow(unused)]
fn main() {
pub fn get_sequence(&self) -> u32
}
AdvancedTxBuilder::supports_rbf
Checks whether the current configuration supports RBF (sequence < 0xFFFFFFFE).
#![allow(unused)]
fn main() {
pub fn supports_rbf(&self) -> bool
}
AdvancedTxBuilder Usage Example
#![allow(unused)]
fn main() {
use simplebtc::advanced_tx::{AdvancedTxBuilder, TimeLock};
// Enable RBF only
let rbf_builder = AdvancedTxBuilder::new()
.with_rbf();
println!("sequence: 0x{:08X}", rbf_builder.get_sequence()); // 0xFFFFFFFD
println!("Supports RBF: {}", rbf_builder.supports_rbf()); // true
// Enable timelock only (lock until block 900,000)
let timelock = TimeLock::new_height_based(900_000);
let timelock_builder = AdvancedTxBuilder::new()
.with_timelock(timelock);
println!("sequence: 0x{:08X}", timelock_builder.get_sequence()); // 0x00000000
// RBF + timelock combined (with_timelock overrides sequence to 0)
let combined = AdvancedTxBuilder::new()
.with_rbf()
.with_timelock(TimeLock::new_time_based(1_800_000_000));
println!("Timelock: {:?}", combined.timelock);
println!("sequence: 0x{:08X}", combined.get_sequence()); // 0x00000000
// Default builder (no advanced features)
let default_builder = AdvancedTxBuilder::new();
println!("sequence: 0x{:08X}", default_builder.get_sequence()); // 0xFFFFFFFF
println!("Supports RBF: {}", default_builder.supports_rbf()); // false
}
TxPriorityCalculator — Transaction Priority Calculator
TxPriorityCalculator is a stateless utility class (all methods are associated functions) for calculating transaction priority scores and recommending reasonable fees. Miners use priority scores to decide which mempool transactions to pack first.
Struct Definition
#![allow(unused)]
fn main() {
pub struct TxPriorityCalculator;
}
FeeUrgency — Fee Urgency Level
#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Copy)]
pub enum FeeUrgency {
Low, // Low priority: 1 sat/byte, confirmation within hours
Medium, // Medium priority: 5 sat/byte, 30–60 minute confirmation
High, // High priority: 20 sat/byte, 10–20 minutes (approximately 1–2 blocks)
Urgent, // Urgent: 50 sat/byte, next block (approximately 10 minutes)
}
}
| Enum Value | Fee Rate | Expected Confirmation Time | Typical Use Case |
|---|---|---|---|
Low | 1 sat/byte | Hours to days | Non-urgent transfers, low network fee periods |
Medium | 5 sat/byte | 30–60 minutes | Everyday transactions, normal confirmation speed |
High | 20 sat/byte | 10–20 minutes | Time-sensitive transactions (Lightning Network channel opening) |
Urgent | 50 sat/byte | ~10 minutes (next block) | Urgent payments, exchange withdrawals |
Methods
TxPriorityCalculator::calculate_priority
Calculates the traditional priority score based on UTXO value and age.
Formula: priority = (input_value × input_age) / tx_size
#![allow(unused)]
fn main() {
pub fn calculate_priority(
input_value: u64, // Total value of input UTXOs (satoshi)
input_age: u32, // Age of input UTXOs (number of confirmations)
tx_size: usize, // Transaction size (bytes)
) -> f64
}
Older (larger age) and higher-value UTXOs have higher priority. Returns 0.0 when tx_size = 0 (prevents division by zero).
TxPriorityCalculator::calculate_fee_rate
Calculates the transaction fee rate (sat/byte).
Formula: fee_rate = fee / size
#![allow(unused)]
fn main() {
pub fn calculate_fee_rate(
fee: u64, // Fee (satoshi)
size: usize, // Transaction size (bytes)
) -> f64
}
Returns 0.0 when size = 0.
TxPriorityCalculator::calculate_score
Calculates a composite score (miner sorting basis).
Formula: score = fee_rate × 0.7 + priority × 0.001 × 0.3
#![allow(unused)]
fn main() {
pub fn calculate_score(fee_rate: f64, priority: f64) -> f64
}
Weight distribution: 70% fee-rate-based, 30% UTXO-priority-based. Transactions with higher fee rates get higher composite scores and are more likely to be selected by miners.
TxPriorityCalculator::recommend_fee
Recommends a fee (satoshi) based on transaction size and urgency.
#![allow(unused)]
fn main() {
pub fn recommend_fee(
tx_size: usize, // Transaction size (bytes)
urgency: FeeUrgency, // Fee urgency level
) -> u64
}
Return value: (tx_size × sat_per_byte) as u64 truncated.
TxPriorityCalculator Usage Example
#![allow(unused)]
fn main() {
use simplebtc::advanced_tx::{TxPriorityCalculator, FeeUrgency};
// A standard Bitcoin transaction is approximately 250 bytes (1 input + 2 outputs)
let tx_size = 250usize;
// Recommended fee for each urgency level
println!("Low priority: {} sat", TxPriorityCalculator::recommend_fee(tx_size, FeeUrgency::Low));
// 250 sat
println!("Medium priority: {} sat", TxPriorityCalculator::recommend_fee(tx_size, FeeUrgency::Medium));
// 1250 sat
println!("High priority: {} sat", TxPriorityCalculator::recommend_fee(tx_size, FeeUrgency::High));
// 5000 sat
println!("Urgent: {} sat", TxPriorityCalculator::recommend_fee(tx_size, FeeUrgency::Urgent));
// 12500 sat
// Calculate fee rate for an existing transaction
let actual_fee = 2000u64; // Actual fee
let fee_rate = TxPriorityCalculator::calculate_fee_rate(actual_fee, tx_size);
println!("Actual fee rate: {:.1} sat/byte", fee_rate); // 8.0 sat/byte
// Calculate UTXO priority (hold 1 BTC, confirmed for 100 blocks, 250-byte transaction)
let priority = TxPriorityCalculator::calculate_priority(
100_000_000, // 1 BTC = 100,000,000 satoshi
100, // 100 blocks of age
tx_size,
);
println!("Priority score: {:.0}", priority); // 40,000,000
// Composite score (miner sorting basis)
let score = TxPriorityCalculator::calculate_score(fee_rate, priority);
println!("Composite score: {:.2}", score);
}
Complete Usage Examples
Scenario 1: Accelerating an Unconfirmed Transaction with RBF
#![allow(unused)]
fn main() {
use simplebtc::advanced_tx::{RBFManager, AdvancedTxBuilder, TxPriorityCalculator, FeeUrgency};
fn accelerate_tx_example() {
let mut rbf = RBFManager::new();
// 1. Send the original transaction (low fee, supports RBF)
let builder = AdvancedTxBuilder::new().with_rbf();
println!("RBF sequence: 0x{:08X}", builder.get_sequence()); // 0xFFFFFFFD
// Simulate transaction sent but still unconfirmed after 30 minutes
// ... create and broadcast original transaction original_tx ...
rbf.mark_replaceable("original_tx_id_001");
// 2. Network congested, need to increase fee
let tx_size = 250usize;
let old_fee = TxPriorityCalculator::recommend_fee(tx_size, FeeUrgency::Low);
let new_fee = TxPriorityCalculator::recommend_fee(tx_size, FeeUrgency::High);
println!("Original fee: {} sat -> New fee: {} sat", old_fee, new_fee);
// 3. Validate replacement rules
// can_replace checks: same inputs, higher fee, sufficient increment
// match rbf.can_replace(&old_tx, &new_tx) {
// Ok(()) => { /* broadcast new transaction */ }
// Err(e) => println!("Replacement rejected: {}", e),
// }
// 4. Clean up after old transaction is confirmed (or replaced)
rbf.remove_confirmed("original_tx_id_001");
}
}
Scenario 2: Time Deposit with Timelock
#![allow(unused)]
fn main() {
use simplebtc::advanced_tx::{AdvancedTxBuilder, TimeLock};
fn savings_timelock() {
// Lock until block height 950,000 (approximately 2028)
let unlock_height = 950_000u64;
let timelock = TimeLock::new_height_based(unlock_height);
let builder = AdvancedTxBuilder::new()
.with_timelock(timelock.clone());
println!("Transaction sequence: 0x{:08X}", builder.get_sequence()); // 0x00000000
println!("Timelock enabled: {}", builder.timelock.is_some());
// Check if funds can currently be used
let current_height = 870_000u32;
if timelock.is_mature(0, current_height) {
println!("Funds unlocked, can be used");
} else {
let remaining = timelock.remaining(0, current_height);
println!("Need to wait {} more blocks (approximately {} days)",
remaining,
remaining * 10 / 60 / 24); // Approximate days
}
}
}
Scenario 3: Fee Strategy Analysis
#![allow(unused)]
fn main() {
use simplebtc::advanced_tx::{TxPriorityCalculator, FeeUrgency};
fn fee_strategy_analysis() {
let tx_sizes = vec![
(125, "Simple payment (1 input, 1 output)"),
(250, "Standard transaction (1 input, 2 outputs)"),
(500, "Batch payment (multiple inputs and outputs)"),
(1000, "Large transaction (common before SegWit)"),
];
println!("{:<40} {:>10} {:>10} {:>10} {:>10}",
"Transaction Type", "Low", "Medium", "High", "Urgent");
println!("{}", "-".repeat(80));
for (size, desc) in &tx_sizes {
println!("{:<40} {:>10} {:>10} {:>10} {:>10}",
desc,
TxPriorityCalculator::recommend_fee(*size, FeeUrgency::Low),
TxPriorityCalculator::recommend_fee(*size, FeeUrgency::Medium),
TxPriorityCalculator::recommend_fee(*size, FeeUrgency::High),
TxPriorityCalculator::recommend_fee(*size, FeeUrgency::Urgent),
);
}
// Composite score comparison
let fee_rate_a = TxPriorityCalculator::calculate_fee_rate(500, 250); // 2 sat/byte
let fee_rate_b = TxPriorityCalculator::calculate_fee_rate(5000, 250); // 20 sat/byte
let priority_a = TxPriorityCalculator::calculate_priority(10_000_000, 50, 250);
let priority_b = TxPriorityCalculator::calculate_priority(100_000, 1, 250);
println!("\nTransaction A (old UTXO, low fee rate) composite score: {:.2}", TxPriorityCalculator::calculate_score(fee_rate_a, priority_a));
println!("Transaction B (new UTXO, high fee rate) composite score: {:.2}", TxPriorityCalculator::calculate_score(fee_rate_b, priority_b));
}
}
sequence Field Value Reference
| sequence Value | Meaning |
|---|---|
0xFFFFFFFF | Default value; RBF and timelock not enabled |
0xFFFFFFFE | Does not support RBF, but allows nLockTime |
0xFFFFFFFD | Supports RBF (BIP125 standard value) |
0x00000000 | Enables timelock (nLockTime takes effect) |
Related Modules
Mempool— The mempool usesRBFManagerfor transaction replacement andTxPriorityCalculatorto sort pending transactions.MultiSig— Multisig and timelocks can be combined to implement scenarios such as “M-of-N required before expiry, reduced to 1-of-N after.”- Advanced Modules Overview — View the full advanced module dependency graph.
REST API
SimpleBTC provides a complete RESTful API, allowing interaction with the blockchain system over HTTP. This page describes all endpoints, request and response formats, and cURL call examples in detail.
Basic Information
| Item | Description |
|---|---|
| Base URL | http://localhost:3000 |
| Protocol | HTTP/1.1 |
| Data Format | JSON |
| CORS | All origins allowed (*) |
| Authentication | None (demo version) |
Starting the Server
# Development mode
cargo run --bin server
# Release mode (better performance)
cargo run --release --bin server
Server output on startup:
SimpleBTC Server v1.0
=====================
Web UI: http://localhost:3000
API: http://localhost:3000/api/blockchain/info
Genesis: <genesis_address> (pre-funded with 100 BTC)
Crypto: secp256k1 ECDSA (real Bitcoin signatures)
Common Response Format
All endpoints use a unified JSON response structure:
Success Response
{
"success": true,
"data": { },
"error": null
}
Error Response
{
"success": false,
"data": null,
"error": "Error description"
}
HTTP Status Codes
| Status Code | Description |
|---|---|
| 200 | Request successful |
| 400 | Parameter error or business logic failure |
Endpoint Summary
| Method | Path | Description |
|---|---|---|
| GET | / | Returns embedded Web UI |
| GET | /api/blockchain/info | Get blockchain status information |
| GET | /api/blockchain/chain | Get full blockchain data |
| GET | /api/blockchain/validate | Validate blockchain integrity |
| POST | /api/wallet/create | Create a new wallet |
| GET | /api/wallet/balance/:address | Query address balance |
| POST | /api/transaction/create | Create a transfer transaction |
| POST | /api/mine | Mine (pack pending transactions) |
Endpoint Details
GET /
Returns the embedded Web UI page (HTML). Suitable for opening directly in a browser.
Example
curl http://localhost:3000/
GET /api/blockchain/info
Gets the current blockchain state, including height, difficulty, number of pending transactions, mining reward, and genesis address.
Response Fields
| Field | Type | Description |
|---|---|---|
height | number | Blockchain height (total blocks included) |
difficulty | number | Current mining difficulty (number of leading zeros in hash) |
pending_transactions | number | Number of unconfirmed transactions in the mempool |
mining_reward | number | Block mining reward (satoshi) |
genesis_address | string | Genesis wallet address (pre-funded with 100 BTC) |
Response Example
{
"success": true,
"data": {
"height": 3,
"difficulty": 4,
"pending_transactions": 1,
"mining_reward": 5000,
"genesis_address": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"
},
"error": null
}
cURL Example
curl http://localhost:3000/api/blockchain/info
GET /api/blockchain/chain
Gets the full blockchain data, including all fields and transaction lists for each block.
Block Fields
| Field | Type | Description |
|---|---|---|
index | number | Block index (starting from 0) |
timestamp | number | Block timestamp (milliseconds) |
transactions | array | Transaction list |
previous_hash | string | Previous block hash |
hash | string | This block’s hash |
nonce | number | Proof-of-work nonce |
merkle_root | string | Merkle root of transactions |
difficulty | number | Block difficulty |
Transaction Fields
| Field | Type | Description |
|---|---|---|
id | string | Transaction ID (hash) |
inputs | array | Transaction input list (UTXO references) |
outputs | array | Transaction output list (recipients and amounts) |
timestamp | number | Transaction timestamp |
Response Example
{
"success": true,
"data": [
{
"index": 0,
"timestamp": 1700000000000,
"transactions": [],
"previous_hash": "0",
"hash": "0000abcdef...",
"nonce": 12345,
"merkle_root": "abc123...",
"difficulty": 4
},
{
"index": 1,
"timestamp": 1700000060000,
"transactions": [
{
"id": "txabc123...",
"inputs": [],
"outputs": [
{ "address": "a1b2c3...", "amount": 5000 }
],
"timestamp": 1700000059000
}
],
"previous_hash": "0000abcdef...",
"hash": "0000fedcba...",
"nonce": 67890,
"merkle_root": "def456...",
"difficulty": 4
}
],
"error": null
}
cURL Example
# Get full blockchain (with jq for formatted output)
curl http://localhost:3000/api/blockchain/chain | jq
GET /api/blockchain/validate
Validates the integrity of the blockchain, checking whether the hash chain and proof of work for all blocks are valid.
Response Example (Validation Passed)
{
"success": true,
"data": "Blockchain is valid",
"error": null
}
Response Example (Validation Failed)
{
"success": false,
"data": "Blockchain is invalid",
"error": null
}
cURL Example
curl http://localhost:3000/api/blockchain/validate
POST /api/wallet/create
Generates a new secp256k1 key pair on the server side, returning the wallet address and public key. The private key is saved in server memory for subsequent ECDSA signing of transactions.
Note: The private key is not returned via the API. The wallet address created this way can be directly used to receive transfers and initiate transactions.
Request Body
None required.
Response Fields
| Field | Type | Description |
|---|---|---|
address | string | Wallet address (40-character hex) |
public_key | string | Compressed public key (secp256k1) |
Response Example
{
"success": true,
"data": {
"address": "3f8a2d1c9e4b7f0a5c8d2e6f1b4a9c3d7e5f2b8a",
"public_key": "04d8c9e4b7f1a89c2d5e8f3b6a1c4e7d9b2a5c8f1a3d6e9b4c7f0a3d6e9b4c7f0"
},
"error": null
}
cURL Example
curl -X POST http://localhost:3000/api/wallet/create
GET /api/wallet/balance/:address
Queries the current balance of a specified wallet address; balance is computed from the UTXO set.
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
address | string | Yes | Wallet address (40-character hex) |
Response Fields
| Field | Type | Description |
|---|---|---|
address | string | The queried wallet address |
balance | number | Balance (satoshi; 1 BTC = 100,000,000 satoshi) |
Response Example
{
"success": true,
"data": {
"address": "3f8a2d1c9e4b7f0a5c8d2e6f1b4a9c3d7e5f2b8a",
"balance": 10000000000
},
"error": null
}
cURL Example
ADDRESS="3f8a2d1c9e4b7f0a5c8d2e6f1b4a9c3d7e5f2b8a"
curl http://localhost:3000/api/wallet/balance/$ADDRESS
POST /api/transaction/create
Creates a transfer transaction, signs it with the sender’s private key using secp256k1 ECDSA, and adds it to the mempool to await mining confirmation.
Prerequisite: The sender address must be a wallet created via
/api/wallet/create(the server must hold its private key to sign).
Request Body
{
"from_address": "3f8a2d1c9e4b7f0a5c8d2e6f1b4a9c3d7e5f2b8a",
"to_address": "7c1e5b9f4a2d8e3c6f0b5a9d2e7f4c1b8a3d6e9f",
"amount": 5000,
"fee": 10
}
Request Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
from_address | string | Yes | Sender wallet address |
to_address | string | Yes | Recipient wallet address |
amount | number | Yes | Transfer amount (satoshi) |
fee | number | Yes | Transaction fee (satoshi, goes to the miner) |
Success Response
{
"success": true,
"data": "Transaction created: txabc123def456...",
"error": null
}
Error Response Example
{
"success": false,
"data": null,
"error": "Wallet not found: 3f8a.... Please first create a wallet via /api/wallet/create."
}
Common Errors
| Error | Cause | Resolution |
|---|---|---|
| Wallet not found | Sender address not created on this server | Call /api/wallet/create first |
| Insufficient balance | Balance < amount + fee | Reduce amount, or mine to receive a reward first |
cURL Example
curl -X POST http://localhost:3000/api/transaction/create \
-H "Content-Type: application/json" \
-d '{
"from_address": "3f8a2d1c9e4b7f0a5c8d2e6f1b4a9c3d7e5f2b8a",
"to_address": "7c1e5b9f4a2d8e3c6f0b5a9d2e7f4c1b8a3d6e9f",
"amount": 5000,
"fee": 10
}'
POST /api/mine
Performs proof-of-work mining, packing all pending transactions in the mempool into a new block, and distributing the block reward to the miner address.
Note: Mining is a CPU-intensive operation that may take several seconds depending on the current difficulty. The mining reward (
mining_reward) and all transaction fees go to the miner address; the balance change is visible after the next query.
Request Body
{
"miner_address": "3f8a2d1c9e4b7f0a5c8d2e6f1b4a9c3d7e5f2b8a"
}
Request Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
miner_address | string | Yes | Miner wallet address (receives reward) |
Success Response
{
"success": true,
"data": "Block mined! Height: 4",
"error": null
}
Error Response Example
{
"success": false,
"data": null,
"error": "No pending transactions"
}
cURL Example
curl -X POST http://localhost:3000/api/mine \
-H "Content-Type: application/json" \
-d '{"miner_address": "3f8a2d1c9e4b7f0a5c8d2e6f1b4a9c3d7e5f2b8a"}'
Quick Start: Complete Workflow
The following example shows the complete flow from creating a wallet to completing a transfer.
Step 1: Get the Genesis Address
The genesis wallet is pre-funded with 100 BTC (10,000,000,000 satoshi); its address can be obtained from blockchain/info.
# Get genesis address
curl -s http://localhost:3000/api/blockchain/info | jq '.data.genesis_address'
# Example output: "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"
GENESIS="a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"
Step 2: Create a New Wallet
# Create wallet, save address
WALLET=$(curl -s -X POST http://localhost:3000/api/wallet/create)
echo $WALLET | jq
MY_ADDR=$(echo $WALLET | jq -r '.data.address')
echo "My address: $MY_ADDR"
Step 3: Transfer from Genesis Address to New Wallet
# Genesis address transfers 10,000 satoshi to new wallet
curl -X POST http://localhost:3000/api/transaction/create \
-H "Content-Type: application/json" \
-d "{
\"from_address\": \"$GENESIS\",
\"to_address\": \"$MY_ADDR\",
\"amount\": 10000,
\"fee\": 100
}"
Step 4: Mine to Confirm Transaction
# Use new wallet as miner address (also receives mining reward)
curl -X POST http://localhost:3000/api/mine \
-H "Content-Type: application/json" \
-d "{\"miner_address\": \"$MY_ADDR\"}"
Step 5: Query Balance
# Query new wallet balance (should include transfer amount + mining reward)
curl http://localhost:3000/api/wallet/balance/$MY_ADDR | jq
Step 6: Validate the Blockchain
# Confirm blockchain data is complete
curl http://localhost:3000/api/blockchain/validate | jq
Complete Script
#!/bin/bash
BASE="http://localhost:3000"
echo "=== SimpleBTC Quick Demo ==="
# 1. Get genesis address
GENESIS=$(curl -s $BASE/api/blockchain/info | jq -r '.data.genesis_address')
echo "Genesis address: $GENESIS"
# 2. Create new wallet
MY_ADDR=$(curl -s -X POST $BASE/api/wallet/create | jq -r '.data.address')
echo "New wallet address: $MY_ADDR"
# 3. Create transaction (genesis address → new wallet, transfer 10000 satoshi)
TX=$(curl -s -X POST $BASE/api/transaction/create \
-H "Content-Type: application/json" \
-d "{\"from_address\":\"$GENESIS\",\"to_address\":\"$MY_ADDR\",\"amount\":10000,\"fee\":100}")
echo "Transaction: $(echo $TX | jq -r '.data')"
# 4. Mine to confirm
MINE=$(curl -s -X POST $BASE/api/mine \
-H "Content-Type: application/json" \
-d "{\"miner_address\":\"$MY_ADDR\"}")
echo "Mining: $(echo $MINE | jq -r '.data')"
# 5. Query balance
BAL=$(curl -s $BASE/api/wallet/balance/$MY_ADDR | jq '.data.balance')
echo "Balance: $BAL satoshi"
# 6. Validate blockchain
VALID=$(curl -s $BASE/api/blockchain/validate | jq -r '.data')
echo "Validation: $VALID"
Unit Notes
All amount fields in SimpleBTC use satoshi as the unit (consistent with Bitcoin):
| Unit | Conversion |
|---|---|
| 1 BTC | 100,000,000 satoshi |
| 1 mBTC | 100,000 satoshi |
| 1 satoshi | Minimum unit, indivisible |
The genesis wallet’s pre-funded balance is 10,000,000,000 satoshi (100 BTC). The default mining reward is 5000 satoshi.
Glossary
Core terminology related to Bitcoin and blockchain.
A
Address
A unique identifier used to receive bitcoin. Generated from a public key through hashing and encoding.
Example: 1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa
Types:
- P2PKH (starts with 1): legacy address
- P2SH (starts with 3): script/multisig address
- Bech32 (starts with bc1): SegWit address
ACID
Four properties of database transactions:
- Atomicity: all-or-nothing execution
- Consistency: maintains a consistent data state
- Isolation: concurrent transactions do not interfere with each other
- Durability: committed changes are persisted permanently
SimpleBTC transaction processing conforms to ACID properties.
B
Block
A data structure containing a list of transactions, linked by hashes to form a blockchain.
Contains:
- Block header (index, timestamp, hash, previous_hash, merkle_root, nonce)
- Transaction list (the first is the Coinbase transaction)
Size: approximately 1–4 MB in Bitcoin
Blockchain
A chronologically linked sequence of blocks, made tamper-resistant through cryptography.
Properties:
- Decentralized
- Tamper-resistant
- Transparent and verifiable
- Trustless (no third party required)
Block Height
The position of a block in the chain. The genesis block has height 0.
Example: If the blockchain contains 100 blocks, the latest block has height 99.
BIP (Bitcoin Improvement Proposal)
A proposal for improving the Bitcoin protocol.
Important BIPs:
- BIP11: M-of-N multisig
- BIP16: P2SH (Pay-to-Script-Hash)
- BIP32: Hierarchical Deterministic Wallets
- BIP39: Mnemonic phrases
- BIP125: RBF (Replace-By-Fee)
C
Coinbase Transaction
The first transaction in a block, used to pay the mining reward to the miner.
Characteristics:
- No valid inputs (does not spend UTXOs)
- Creates new bitcoin
- Includes block reward + transaction fees
Example:
#![allow(unused)]
fn main() {
Transaction::new_coinbase(
miner_address,
50, // block reward
timestamp,
total_fees, // sum of fees
)
}
Cold Wallet
A wallet that stores private keys offline, not connected to the internet.
Types:
- Hardware wallets (Ledger, Trezor)
- Paper wallets
- Air-gapped computers
Advantage: Extremely high security Disadvantage: Inconvenient to use
Confirmation
The number of times a transaction has been included in a block and subsequently extended by additional blocks.
Confirmation counts:
- 0 confirmations: in the mempool, not yet mined
- 1 confirmation: included in a block
- 6 confirmations: very secure (Bitcoin standard)
Time: approximately 10 minutes per confirmation in Bitcoin
D
Difficulty
The computational difficulty of mining, which determines how hard it is to find a valid block hash.
SimpleBTC:
#![allow(unused)]
fn main() {
blockchain.difficulty = 3; // 3 leading zeros
}
Bitcoin: dynamically adjusted every 2016 blocks (approximately 2 weeks), targeting a 10-minute block time.
Double Spending
An attack that attempts to spend the same bitcoin twice.
Defense mechanisms:
- UTXO model (each UTXO can only be spent once)
- Block confirmations (almost impossible after 6 confirmations)
- Proof of Work (requires 51% hash power to rewrite history)
E
ECDSA (Elliptic Curve Digital Signature Algorithm)
The digital signature scheme used by Bitcoin.
Curve: secp256k1
Flow:
Private key → ECDSA → Public key → Hash → Address
SimpleBTC uses a simplified SHA256-based signature.
F
Fee
The amount paid to miners as an incentive to include transactions in a block.
Calculation:
Fee = total inputs − total outputs
Fee rate:
Fee rate = fee / transaction size (sat/byte)
Recommendations:
- Low: 1–5 sat/byte
- Medium: 10–20 sat/byte
- High: 50+ sat/byte
Fork
A situation where the blockchain has multiple valid branches.
Types:
- Temporary fork: two miners produce a block simultaneously; resolved by the longest-chain rule
- Hard fork: protocol-incompatible upgrade (e.g., BCH)
- Soft fork: backward-compatible upgrade (e.g., SegWit)
G
Genesis Block
The first block in the blockchain, with index 0.
Bitcoin genesis block:
- Date: January 3, 2009
- Reward: 50 BTC (unspendable)
- Message: “The Times 03/Jan/2009 Chancellor on brink of second bailout for banks”
SimpleBTC:
#![allow(unused)]
fn main() {
fn create_genesis_block() {
// Create the block at index 0
// previous_hash = "0"
}
}
H
Hash
A function that converts arbitrary data into a fixed-length string.
Bitcoin uses:
- SHA256 (transaction IDs, block hashes)
- RIPEMD160 (address generation)
Properties:
- Deterministic
- One-way (preimage resistant)
- Collision resistant
- Avalanche effect
Example:
SHA256("hello") = 2cf24dba5fb0a30e...
Hash Rate
The number of hash computations performed per second.
Units:
- H/s (hashes per second)
- KH/s = 1,000 H/s
- MH/s = 1,000,000 H/s
- GH/s = 1,000,000,000 H/s
- TH/s = 1,000,000,000,000 H/s
- EH/s = 1,000,000,000,000,000,000 H/s
Bitcoin network: approximately 300+ EH/s
Hot Wallet
A wallet connected to the internet, convenient for everyday use.
Types:
- Mobile wallets
- Desktop wallets
- Web wallets
Advantage: Convenient to use Disadvantage: Lower security
M
Merkle Tree
A binary hash tree of transactions; the root hash is stored in the block header.
Structure:
Root
/ \
H12 H34
/ \ / \
H1 H2 H3 H4
Uses:
- SPV lightweight verification
- Proving a transaction exists in a block
- O(log n) verification complexity
Mining
The process of creating new blocks through proof of work.
Steps:
- Collect pending transactions
- Create the Coinbase transaction
- Compute the Merkle root
- Adjust the nonce to find a valid hash
- Broadcast the block
Reward: block reward + transaction fees
Multisig (M-of-N)
An address that requires M signatures (out of N total keys) to spend funds.
Examples:
- 2-of-3: CEO + CFO + CTO, any two suffice
- 3-of-5: a board of 5, requiring 3 to agree
Address: starts with “3” (P2SH)
N
Node
A computer running Bitcoin client software.
Types:
- Full node: stores the complete blockchain, validates all transactions
- Light node: stores only block headers, uses SPV verification
- Miner node: a full node that participates in mining
Nonce
A number adjusted during mining to change the block hash.
Purpose: proof of work
#![allow(unused)]
fn main() {
while hash(block_data + nonce) >= target {
nonce++; // keep trying
}
}
P
P2P (Peer-to-Peer)
A network where nodes communicate directly with each other, without a central server.
Bitcoin network:
- Decentralized
- Censorship-resistant
- No single point of failure
P2PKH (Pay-to-Public-Key-Hash)
The traditional Bitcoin address type, starting with “1”.
Flow:
Public key → SHA256 → RIPEMD160 → Base58 → Address
P2SH (Pay-to-Script-Hash)
Pay-to-script-hash, used for advanced features like multisig; addresses start with “3”.
Advantages:
- Supports complex scripts
- Hides script details
- Fee is borne by the recipient
Private Key
A secret number used to sign transactions.
Properties:
- A 256-bit random number
- Owning the private key = owning the bitcoin
- Cannot be recovered if lost
Protection:
- Never share it
- Store it encrypted
- Keep multiple backups
Proof of Work (PoW)
Bitcoin’s consensus mechanism.
Principle: Finding a hash that satisfies the difficulty target requires a large amount of computation.
Purpose:
- Prevent spam attacks
- Decentralized consensus
- Extremely high cost for a 51% attack
Public Key
A publicly shareable number derived from a private key, used to generate addresses and verify signatures.
Derivation:
Private key → elliptic curve operation → Public key → Hash → Address
R
RBF (Replace-By-Fee)
A mechanism that allows replacing an unconfirmed transaction (BIP125).
Uses:
- Speed up a transaction (by increasing the fee)
- Cancel a transaction
- Batch optimization
Marker: nSequence < 0xFFFFFFFE
S
Satoshi (sat)
The smallest unit of bitcoin.
1 BTC = 100,000,000 satoshi
1 sat = 0.00000001 BTC
Named after: Bitcoin’s creator, Satoshi Nakamoto
Script
Bitcoin’s scripting language, which defines spending conditions.
Opcodes:
- OP_DUP
- OP_HASH160
- OP_EQUALVERIFY
- OP_CHECKSIG
- OP_CHECKMULTISIG
SimpleBTC uses a simplified version.
SPV (Simplified Payment Verification)
Lightweight verification that does not require downloading the full blockchain.
Principle: Uses Merkle proofs to verify transactions.
Advantages:
- Only needs block headers (~80 bytes each)
- Suitable for mobile wallets
- O(log n) verification
T
Timelock (nLockTime)
Restricts a transaction from being confirmed before a specific time.
Types:
- Timestamp-based (≥ 500,000,000)
- Block height-based (< 500,000,000)
Applications:
- Time deposits
- Inheritance
- Payroll disbursement
Transaction (TX)
The basic unit of value transfer.
Contains:
- Inputs (which UTXOs to spend)
- Outputs (which new UTXOs to create)
- Timestamp
- Fee
U
UTXO (Unspent Transaction Output)
An unspent transaction output, representing bitcoin that can be spent.
Lifecycle:
- Created (as a transaction output)
- Exists (in the UTXO set)
- Spent (referenced by a transaction input)
- Removed (deleted from the UTXO set)
Balance: the sum of all UTXOs
W
Wallet
Software that manages private keys, public keys, and addresses.
Types:
- Hot wallet (online)
- Cold wallet (offline)
- Hardware wallet
- Paper wallet
Functions:
- Generate key pairs
- Create addresses
- Sign transactions
- Query balances
Numbers
51% Attack
An attack in which an attacker controls more than 50% of the network’s hash power, enabling rewriting of blockchain history.
Consequences:
- Double-spend attacks
- Blocking transaction confirmations
Defense: Bitcoin’s hash rate is so large that the cost of such an attack is prohibitively high.
6 Confirmations
The standard number of confirmations for a Bitcoin transaction to be considered secure.
Time: approximately 60 minutes (6 blocks × 10 minutes)
Reason: After 6 blocks, rewriting history is virtually impossible.
Reference Resources
Back to Documentation Home | Basic Concepts
Bitcoin Principles
This appendix is a standalone educational article for readers who want to understand the underlying principles of Bitcoin. No programming background is required, though some familiarity with cryptography and distributed systems will be helpful.
Introduction
On October 31, 2008, a person using the pseudonym “Satoshi Nakamoto” published a 9-page paper on a cryptography mailing list: Bitcoin: A Peer-to-Peer Electronic Cash System. The paper proposed a revolutionary answer to the question: How can two strangers transfer value without a trusted third party (such as a bank)?
Bitcoin’s answer rests on four core technical pillars: hash functions, public-key cryptography, proof of work, and the blockchain data structure. This chapter introduces each of these principles in turn.
I. Hash Functions and SHA-256
What Is a Hash Function?
A hash function is a mathematical function that maps data of arbitrary length to a fixed-length “digest.” Bitcoin uses SHA-256 (Secure Hash Algorithm 256-bit), which always produces a 256-bit (32-byte) output, typically represented as 64 hexadecimal characters.
SHA-256("Hello, Bitcoin!") =
a3b5c7d2e1f0... (64 hexadecimal characters)
SHA-256("Hello, Bitcoin.") =
f9e8d7c6b5a4... (completely different hash)
Four Key Properties of Hash Functions
1. Deterministic The same input always produces the same output. There is no randomness.
2. Avalanche Effect A tiny change in the input (even a single bit) causes a large, unpredictable change in the output. This ensures the hash is highly sensitive to even minor modifications.
3. One-Way (Preimage Resistance) It is computationally infeasible to reverse-engineer the original input from a hash output. Even knowing the SHA-256 output, recovering the input by brute force within the lifetime of the universe is impossible (the search space is 2²⁵⁶).
4. Collision Resistance It is computationally infeasible to find two different inputs that produce the same output (a hash collision). This is the foundation of the Bitcoin blockchain’s tamper resistance.
SHA-256 Applications in Bitcoin
Bitcoin uses SHA-256 (or double SHA-256, i.e., SHA-256(SHA-256(data))) in several places:
| Application | Hash Method | Purpose |
|---|---|---|
| Block hash | SHA-256(SHA-256(block header)) | Unique block identifier, links the blockchain |
| Transaction ID | SHA-256(SHA-256(transaction data)) | Unique transaction identifier |
| Address generation | RIPEMD-160(SHA-256(public key)) | Derives a Bitcoin address from a public key |
| Merkle tree | SHA-256(SHA-256(node concatenation)) | Efficient verification of a transaction set |
| Mining | SHA-256(SHA-256(block header)) | Finds a nonce satisfying the difficulty target |
Why Double SHA-256?
Bitcoin uses SHA-256 twice rather than once, primarily to defend against length extension attacks. SHA-256’s mathematical structure has a weakness: knowing SHA-256(M), one can compute SHA-256(M || X) (for arbitrary appended data X) without knowing M. Double SHA-256 eliminates this security concern.
II. Public-Key Cryptography
Symmetric vs. Asymmetric Encryption
Traditional symmetric encryption (such as AES) uses the same key for both encryption and decryption. The problem is that if Alice wants to send an encrypted message to Bob, she first needs to securely transmit the key to Bob — but if a secure channel already exists for that, why not use it directly for the message?
Asymmetric encryption (public-key cryptography) solves this “key distribution problem.” Each user has two keys:
- Public Key: can be shared openly with anyone
- Private Key: must be kept strictly secret and never shared
Their relationship is: a public key can be derived from a private key, but a private key cannot be reverse-engineered from a public key.
Private key (a random 256-bit number)
│
▼ (one-way, irreversible)
Public key (a point on an elliptic curve)
│
▼ (one-way, irreversible)
Bitcoin address (hash of the public key)
Elliptic Curve Cryptography (ECC) and secp256k1
Bitcoin uses the Elliptic Curve Digital Signature Algorithm (ECDSA) with a specific curve called secp256k1. This curve is defined by the equation:
y² = x³ + 7 (over the finite field Fp, where p = 2²⁵⁶ − 2³² − 977)
The security of elliptic curve cryptography is based on the Elliptic Curve Discrete Logarithm Problem (ECDLP): given a generator point G on the curve and a point P = k·G, it is computationally infeasible to recover the integer k from P.
Why secp256k1 rather than the more common secp256r1 (NIST P-256)?
Satoshi Nakamoto chose secp256k1 parameters that were derived from a deterministic formula rather than generated randomly, making it less likely to contain a backdoor inserted by the NSA — a concern that is debated but worth considering in the cryptography community. The coefficients of secp256k1 are very simple (a=0, b=7), with no complex “seemingly random” parameters, providing greater transparency.
Key generation process:
1. Generate a private key: randomly select a 256-bit integer k (1 ≤ k ≤ n−1, where n is the curve order)
Private key = random number k (typically from a cryptographically secure random number generator)
2. Generate a public key: compute elliptic curve point multiplication
Public key = k × G (G is the standard base point of secp256k1)
Note: point multiplication is a special operation defined on the elliptic curve, not ordinary multiplication
3. Generate an address (simplified):
Address = RIPEMD-160(SHA-256(public key)) + checksum
The randomness of the private key is critical. Documented theft cases show that private keys generated using weak random number generators (such as timestamps) have been brute-forced. Truly secure private keys come from the operating system’s cryptographic random number interface (e.g., /dev/urandom on Linux).
III. Digital Signatures
The Role of Signatures
Digital signatures solve the most fundamental problem in Bitcoin: How do you prove you have the right to spend some funds without revealing your private key?
By analogy with the real world: you sign a cheque and the bank verifies the signature is yours. A digital signature is the cryptographic equivalent of this process, but more secure — it does not rely on the visual appearance of the signature (which can be forged), but on a cryptographic proof that is mathematically impossible to forge.
The ECDSA Signing Process
Signing:
Input: message M (hash of transaction data), private key k
Output: signature (r, s)
Steps:
1. Generate a random number r_rand (must be different for each signature!)
2. Compute the curve point R = r_rand × G
3. r = R.x mod n (take the x-coordinate of R)
4. s = r_rand⁻¹ × (hash(M) + k × r) mod n
Verification:
Input: message M, signature (r, s), public key P = k × G
Output: valid / invalid
Steps:
1. u1 = hash(M) × s⁻¹ mod n
2. u2 = r × s⁻¹ mod n
3. Compute point Q = u1 × G + u2 × P
4. Verify Q.x mod n == r
The verification process uses only the public key and does not require the private key. This means anyone can verify a signature, but only the holder of the private key can create a valid signature.
Critical Security Requirement: The Random Number Must Not Be Reused
The random number r_rand in the signing algorithm must be unique and unpredictable for each signature. In 2013, the ECDSA implementation in the PlayStation 3 was broken because it used a fixed random number, leading to private key exposure. Similar cases have occurred in Bitcoin history.
Modern implementations (including Bitcoin Core) use RFC 6979, which deterministically generates the random number from the private key and message, completely eliminating the risk of random number reuse.
IV. Proof-of-Work Consensus
The Byzantine Generals Problem
In a distributed system, how can consensus be reached if nodes may send incorrect information (whether due to malice or failure)? This is known as the Byzantine Generals Problem, formally introduced by Lamport, Shostak, and Pease in 1982.
The classical conclusion: in a traditional message-passing model, if there are f Byzantine nodes, at least 3f+1 total nodes are required to tolerate the faults. However, this result has a prerequisite: communication costs are negligible.
Satoshi Nakamoto’s insight was that in Bitcoin, speaking on the network requires a real physical cost (electricity), which fundamentally changes the game-theoretic equilibrium.
Proof of Work
Proof of Work requires miners to find a special number (nonce) such that the hash of the block header satisfies a specific condition (beginning with a certain number of zeros):
Target: SHA-256(SHA-256(block header)) < target value
Equivalently: the block header hash begins with `difficulty` leading zeros
Example (difficulty=4):
0000a3f7d2e1b5c8... ← valid (starts with 4 zeros)
0001a3f7d2e1b5c8... ← invalid (4th character is not 0)
Miners repeatedly modify the nonce and recompute the hash until a valid value is found:
while SHA-256(SHA-256(block header || nonce)) >= target:
nonce += 1 // try the next number
Once found, broadcast this block to the entire network
Difficulty adjustment: Bitcoin automatically adjusts the difficulty every 2016 blocks (approximately two weeks), targeting an average block time of 10 minutes. If hash power increases, the difficulty rises; if hash power decreases, the difficulty falls.
Why Does PoW Prevent Double Spending?
Suppose an attacker attempts to double-spend:
- Sends transaction A to a merchant (paying 10 BTC)
- The merchant waits for N block confirmations before shipping
- The attacker secretly mines on a separate chain, creating blocks containing transaction B (sending the 10 BTC back to themselves)
The attacker needs to secretly outpace the honest miners who have already mined N blocks, producing a longer chain. If the attacker controls a fraction α of the hash power (α < 0.5), their probability of success decreases exponentially as N increases. Satoshi Nakamoto proved in the whitepaper that:
P(success) ≈ (α / (1−α))^N
When α = 0.3 (30% hash power) and N = 6 (6 confirmations, approximately 1 hour):
P(success) ≈ (0.3/0.7)^6 = (0.4286)^6 ≈ 0.0006 = 0.06%
This is the mathematical basis for Bitcoin’s “6 confirmations” rule.
PoW vs. Other Consensus Mechanisms
| Consensus Mechanism | Representative Projects | Advantages | Disadvantages |
|---|---|---|---|
| Proof of Work (PoW) | Bitcoin, Litecoin | No need to trust participants, Sybil attack resistant | High energy consumption, slow block time |
| Proof of Stake (PoS) | Ethereum 2.0, Cardano | Low energy consumption, good scalability | Initial distribution fairness issues, “nothing-at-stake” problem |
| Delegated Proof of Stake (DPoS) | EOS, Tron | High throughput | Centralization risk, 21 nodes |
| Practical Byzantine Fault Tolerance (PBFT) | Hyperledger | Efficient, has finality | Suitable only for consortium chains with known participants |
V. Blockchain Data Structure
Block Structure
Each block consists of two parts:
Block header (80 bytes):
Version (4 bytes) - protocol version
Previous hash(32 bytes) - links this block to the previous one
Merkle root (32 bytes) - root hash of the Merkle tree of all transactions
Timestamp (4 bytes) - Unix timestamp
Difficulty (4 bytes) - current mining difficulty (compact format)
Nonce (4 bytes) - value adjusted by the miner
Block body (variable size):
Transaction count (varint)
Transaction list [Transaction...]
├── Transaction 1 (coinbase, miner reward)
├── Transaction 2
└── ...
Chain Structure and Tamper Resistance
The “chain” in blockchain comes from each block header containing the hash of the previous block:
Block 0 (genesis) Block 1 Block 2
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ prev_hash: 0000 │ │ prev_hash: H(B0) │ │ prev_hash: H(B1) │
│ merkle_root: ... │◄───│ merkle_root: ... │◄───│ merkle_root: ... │
│ nonce: 2083236893│ │ nonce: 12345678 │ │ nonce: 87654321 │
│ hash: H(B0) │ │ hash: H(B1) │ │ hash: H(B2) │
└──────────────────┘ └──────────────────┘ └──────────────────┘
If an attacker modifies a transaction in Block 1:
- Block 1’s Merkle root changes
- Block 1’s block header hash changes
- Block 2’s
prev_hashfield no longer matches Block 1’s new hash - The attacker must redo the proof of work to compute Block 2’s nonce
- This invalidates Block 3, requiring it to be mined again…
- The attacker must recompute the PoW for every block from the tampered one to the chain tip
Because honest miners are continuously extending the chain, the attacker would need to complete this work faster than the entire network’s hash power. Under the assumption of 51% honest hash power, this is an impossible task.
The UTXO Set: The “State” of the Blockchain
The full blockchain records all historical transactions, but validating new transactions only requires knowing “which outputs are currently unspent” — the UTXO set (Unspent Transaction Output Set).
The UTXO set is the “ledger state” obtained by sequentially processing all transactions starting from the genesis block. As of 2024, the Bitcoin UTXO set contains approximately 110 million entries, occupying about 5–6 GB of memory, far smaller than the full blockchain at 600+ GB.
Blockchain (historical record, ~600 GB)
↓ Full node processes sequentially
UTXO set (current state, ~5 GB)
↓ Query
Validate whether new transactions are valid
VI. Decentralization and Network Security
P2P Network
Bitcoin nodes connect to each other through a peer-to-peer (P2P) network, with no central server. Each node establishes connections to dozens of peers, forming a small-world network.
New transactions propagate through the network via a Gossip protocol:
- Node A creates a transaction and broadcasts it to connected nodes
- Each node that receives the transaction validates it, then forwards it to its own connected nodes
- The transaction spreads to most nodes worldwide within seconds
Economic Analysis of a 51% Attack
Attacking the Bitcoin network requires controlling more than 50% of the global hash rate. As of 2024, Bitcoin’s global hash rate is approximately 600 EH/s. Purchasing or renting 50% of that hash power would require billions of dollars in hardware investment, plus ongoing electricity costs.
More critically, there is the economic incentive problem of the attack:
- Potential gain from a successful attack: double-spending in a large transaction, possibly defrauding an exchange
- Cost of the attack: Bitcoin’s price collapses, and the attacker’s bitcoin holdings and mining equipment drop sharply in value
- Conclusion: a rational economic actor would prefer to use their hash power to mine honestly (approximately $20 million per day in revenue) rather than launch an attack with limited upside and extreme risk
This economically self-reinforcing security is the elegance of Bitcoin’s design — security automatically strengthens as the network’s value grows.
Node Types and Network Roles
| Node Type | Description | Typical Use Case |
|---|---|---|
| Full node | Stores the full blockchain, independently validates all rules | Exchanges, node operators |
| Pruned node | Retains only recent blocks, saves disk space | Home users |
| SPV node | Downloads only block headers, lightweight verification | Mobile wallets |
| Mining pool node | Coordinates large numbers of miners, allocates hash power | Mining farm operators |
| Lightning Network node | Manages payment channels, enables instant micropayments | Merchants, everyday payments |
Further Reading and References
The following are important references for a deeper understanding of Bitcoin’s technical principles:
Foundational Papers
-
Nakamoto, S. (2008). Bitcoin: A Peer-to-Peer Electronic Cash System. https://bitcoin.org/bitcoin.pdf The Bitcoin whitepaper, 9 pages, covering all core concepts. Essential reading.
-
Merkle, R. C. (1979). Secrecy, Authentication, and Public Key Systems. Stanford University doctoral dissertation, the original paper on Merkle trees.
-
Lamport, L., Shostak, R., & Pease, M. (1982). The Byzantine Generals Problem. ACM Transactions on Programming Languages and Systems. The classical formal description of the distributed consensus problem.
-
Back, A. (2002). Hashcash – A Denial of Service Counter-Measure. http://www.hashcash.org/papers/hashcash.pdf The predecessor to Bitcoin’s PoW mechanism, originally designed to prevent email spam.
-
Dai, W. (1998). b-money. http://www.weidai.com/bmoney.txt An early decentralized digital currency proposal cited by Satoshi Nakamoto.
Cryptography Fundamentals
-
Johnson, D., Menezes, A., & Vanstone, S. (2001). The Elliptic Curve Digital Signature Algorithm (ECDSA). International Journal of Information Security. The authoritative technical specification for ECDSA.
-
Pornin, T. (2013). RFC 6979: Deterministic Usage of the Digital Signature Algorithm (DSA) and Elliptic Curve Digital Signature Algorithm (ECDSA). IETF Request for Comments. The standard for deterministic signature nonce generation, eliminating the nonce reuse vulnerability.
-
National Institute of Standards and Technology. (2015). FIPS PUB 180-4: Secure Hash Standard. The official specification document for SHA-256.
Books
-
Antonopoulos, A. M. (2017). Mastering Bitcoin: Programming the Open Blockchain (2nd ed.). O’Reilly Media. The most comprehensive introductory book on Bitcoin technology; the open-source version is freely available.
-
Song, J. (2019). Programming Bitcoin. O’Reilly Media. Implements the Bitcoin protocol from scratch in Python; ideal for hands-on learning.
-
Narayanan, A., Bonneau, J., Felten, E., Miller, A., & Goldfeder, S. (2016). Bitcoin and Cryptocurrency Technologies. Princeton University Press. Free PDF available, a comprehensive academic analysis.
Advanced Resources
-
Bitcoin Improvement Proposals (BIPs). https://github.com/bitcoin/bips All improvement proposals for the Bitcoin protocol, including SegWit (BIP141), RBF (BIP125), HD Wallets (BIP32), and more.
-
Bitcoin Core source code. https://github.com/bitcoin/bitcoin The reference implementation of Bitcoin, written in C++, approximately 150,000 lines of code.
Summary: Bitcoin’s Five-Layer Architecture
Layer 5: Economic Incentive Layer
PoW rewards (block reward + fees) → drives honest miner behavior
↑
Layer 4: Consensus Layer
Longest-chain rule + PoW difficulty adjustment → network-wide agreement on "which chain is correct"
↑
Layer 3: Data Structure Layer
Blockchain (chained blocks) + Merkle tree → tamper-resistant transaction history
↑
Layer 2: Transaction Layer
UTXO model + Script system → defines the rules for value transfer
↑
Layer 1: Cryptography Layer
SHA-256 (integrity) + ECDSA (authentication) → trustless mathematical guarantees
Bitcoin’s revolution does not lie in any single technical innovation — SHA-256, elliptic curve cryptography, P2P networks, hash chains, and PoW all existed before. Satoshi Nakamoto’s genius was in combining these known technologies in a specific way to create a self-consistent, game-theoretically stable decentralized monetary system.
The SimpleBTC project implements a teaching version of this system, helping you understand how each layer works through runnable code. Reading this article alongside the source code is recommended, combining theory with practice.
Frequently Asked Questions (FAQ)
Installation and Configuration
Q: How do I install SimpleBTC?
A:
# 1. Make sure Rust is installed
rustc --version
# 2. Clone the project
git clone https://github.com/GeoffreyWang1117/SimpleBTC.git
cd SimpleBTC
# 3. Build
cargo build --release
# 4. Run
cargo run --bin btc-demo
See the Installation Guide for details.
Q: I get a “linker ‘cc’ not found” error when compiling.
A: You need to install a C compiler:
# Ubuntu/Debian
sudo apt-get install build-essential
# macOS
xcode-select --install
# Windows
# Install Visual Studio Build Tools
Q: How do I change the mining difficulty?
A: Edit src/blockchain.rs:
#![allow(unused)]
fn main() {
pub fn new() -> Blockchain {
Blockchain {
difficulty: 3, // change this value
// 3-4 is good for demos
// 5-6 is more secure but slower
// ...
}
}
}
Basic Concepts
Q: What is UTXO? Why not use account balances?
A: UTXO (Unspent Transaction Output) is a core Bitcoin concept.
Account model (Ethereum):
Alice account: 100 BTC
After transfer:
Alice: 70 BTC
Bob: 30 BTC
UTXO model (Bitcoin):
Alice has UTXOs: [50 BTC, 30 BTC, 20 BTC]
Transferring 30 BTC to Bob:
- Consume the 50 BTC UTXO
- Create 30 BTC output for Bob
- Create 20 BTC change output for Alice (50 - 30)
Advantages:
- Better privacy (use a new address each time)
- Parallel processing (different UTXOs can be handled concurrently)
- Simpler validation logic
See Basic Concepts - UTXO for details.
Q: Why do transactions need fees?
A: Transaction fees serve several purposes:
- Prevent spam attacks — sending a transaction has a cost
- Incentivize miners — miners prioritize transactions with higher fee rates
- Resource allocation — when the network is congested, those willing to pay more get priority
Fee calculation:
#![allow(unused)]
fn main() {
fee = total inputs - total outputs
// Example
inputs: 100 satoshi
outputs: 90 satoshi
fee: 10 satoshi
}
Fee rate recommendations:
- 1–5 sat/byte: low priority (several hours)
- 10–20 sat/byte: medium priority (30–60 minutes)
- 50+ sat/byte: high priority (next block)
Q: What is Proof of Work (PoW)? Why is mining necessary?
A: PoW is Bitcoin’s consensus mechanism.
Mining process:
#![allow(unused)]
fn main() {
target = "000..." // difficulty requirement
while hash(block_data + nonce) >= target {
nonce++; // keep trying
}
// Found a valid nonce; the block is accepted
}
Why it is needed:
- Prevents spam blocks (creating a block requires computational cost)
- Decentralized consensus (hash power as votes)
- Extremely high cost for a 51% attack (requires more than half the global hash power)
Difficulty and time:
- Difficulty 3: milliseconds (demo)
- Difficulty 10: seconds (private chain)
- Difficulty 20: minutes (Bitcoin scale)
See Basic Concepts - PoW for details.
Usage Questions
Q: How do I create a wallet?
A:
#![allow(unused)]
fn main() {
use bitcoin_simulation::wallet::Wallet;
// Create a new wallet
let wallet = Wallet::new();
println!("Address: {}", wallet.address);
println!("Public key: {}", wallet.public_key);
// Keep the private key secret!
}
Important:
- Losing the private key = permanent loss of bitcoin
- Exposing the private key = bitcoin theft
- Back up the private key to a secure location
Q: How do I send a transfer?
A:
#![allow(unused)]
fn main() {
use bitcoin_simulation::{blockchain::Blockchain, wallet::Wallet};
let mut blockchain = Blockchain::new();
let alice = Wallet::new();
let bob = Wallet::new();
// 1. Create the transaction
let tx = blockchain.create_transaction(
&alice, // sender
bob.address, // recipient
1000, // amount (satoshi)
10, // fee
)?;
// 2. Add to the pending pool
blockchain.add_transaction(tx)?;
// 3. Mine to confirm
blockchain.mine_pending_transactions(miner.address)?;
}
Q: What do I do if my balance is insufficient?
A: Check the following:
- Query balance:
#![allow(unused)]
fn main() {
let balance = blockchain.get_balance(&address);
println!("Balance: {}", balance);
}
- Ensure you have UTXOs:
#![allow(unused)]
fn main() {
let utxos = blockchain.utxo_set.find_utxos(&address);
println!("UTXO count: {}", utxos.len());
}
- Check that the fee is included:
#![allow(unused)]
fn main() {
let total_needed = amount + fee;
if balance < total_needed {
return Err("Insufficient balance (including fee)");
}
}
- Wait for transaction confirmation: A recently sent transaction must be confirmed by mining before its outputs can be used.
Q: What if a transaction remains unconfirmed for a long time?
A: Possible causes and solutions:
Cause 1: Fee too low
#![allow(unused)]
fn main() {
// Increase the fee
let tx = blockchain.create_transaction(
&alice,
bob.address,
1000,
50, // higher fee
)?;
}
Cause 2: No miner is mining
# Mine manually
cargo run --bin btc-demo
# Or in code
blockchain.mine_pending_transactions(miner.address)?;
Cause 3: Transaction is invalid
#![allow(unused)]
fn main() {
// Validate the transaction
if !tx.verify() {
println!("Transaction invalid, check:");
println!("- Whether the input UTXOs exist");
println!("- Whether the signature is correct");
println!("- Whether the balance is sufficient");
}
}
Use RBF to speed up:
#![allow(unused)]
fn main() {
// Create a replacement transaction with a higher fee rate
let faster_tx = blockchain.create_transaction(
&alice,
bob.address,
1000,
100, // higher fee
)?;
}
Advanced Features
Q: How do I use multisig?
A:
#![allow(unused)]
fn main() {
use bitcoin_simulation::multisig::MultiSigAddress;
// 1. Create participant wallets
let alice = Wallet::new();
let bob = Wallet::new();
let charlie = Wallet::new();
// 2. Create a 2-of-3 multisig address
let multisig = MultiSigAddress::new(
2, // requires 2 signatures
vec![
alice.public_key,
bob.public_key,
charlie.public_key,
]
)?;
// 3. Send funds to the multisig address
let tx = blockchain.create_transaction(
&funder,
multisig.address.clone(),
10000,
0,
)?;
// 4. Spend from the multisig address (requires 2 signatures)
let alice_sig = alice.sign(&payment_data);
let bob_sig = bob.sign(&payment_data);
if vec![alice_sig, bob_sig].len() >= multisig.required_sigs {
// Execute the transaction
}
}
See the Multisig Tutorial for details.
Q: What is a Merkle tree? What is it used for?
A: A Merkle tree is a hash tree of transactions, stored in the block header.
Structure:
Root Hash
/ \
H(AB) H(CD)
/ \ / \
H(A) H(B) H(C) H(D)
tx1 tx2 tx3 tx4
Uses:
- SPV verification — light wallets do not need to download the full block
#![allow(unused)]
fn main() {
// Only needs block header + Merkle proof
let proof = merkle_tree.get_proof(&tx_hash)?;
let valid = MerkleTree::verify_proof(
&tx_hash,
&proof,
&block.merkle_root,
tx_index
);
}
-
Data integrity — any change to a transaction changes the root hash
-
Efficient verification — O(log n) complexity
See the Merkle Tree Tutorial for details.
Q: What is a timelock? How do I use one?
A: A timelock restricts a transaction from being confirmed before a specific time.
Two types:
- Timestamp-based:
#![allow(unused)]
fn main() {
use bitcoin_simulation::advanced_tx::TimeLock;
// Unlock after 3 months
let three_months = 90 * 24 * 3600;
let unlock_time = current_time + three_months;
let timelock = TimeLock::new_time_based(unlock_time);
// Check if it has matured
if timelock.is_mature(current_time, 0) {
println!("Matured, funds can be spent");
}
}
- Block height-based:
#![allow(unused)]
fn main() {
// Unlock after block 100,000
let timelock = TimeLock::new_block_based(100000);
if timelock.is_mature(current_time, current_block_height) {
println!("Block height reached");
}
}
Use cases:
- Time deposits
- Inheritance
- Payroll disbursement
- Project vesting periods
See the Timelock Tutorial for details.
Development Questions
Q: How do I integrate SimpleBTC into my project?
A: SimpleBTC can be used as a library:
# Cargo.toml
[dependencies]
bitcoin_simulation = { path = "../SimpleBTC" }
#![allow(unused)]
fn main() {
// In your code
use bitcoin_simulation::{
blockchain::Blockchain,
wallet::Wallet,
};
fn my_app() {
let blockchain = Blockchain::new();
// ... your business logic
}
}
Q: How do I use the REST API?
A:
Start the server:
cargo run --bin btc-server
# Server runs at http://localhost:3000
API call examples:
# Create a wallet
curl -X POST http://localhost:3000/api/wallet/create
# Create a transaction
curl -X POST http://localhost:3000/api/transaction/create \
-H "Content-Type: application/json" \
-d '{
"from": "alice_address",
"to": "bob_address",
"amount": 1000,
"fee": 10
}'
# Query balance
curl http://localhost:3000/api/balance/alice_address
# Mine
curl -X POST http://localhost:3000/api/mine \
-H "Content-Type: application/json" \
-d '{"miner_address": "miner_address"}'
See the REST API documentation for details.
Q: How do I run tests?
A:
# Run all tests
cargo test
# Run a specific test
cargo test test_blockchain
# Show output
cargo test -- --nocapture
# Run examples
cargo run --example enterprise_multisig
cargo run --example escrow_service
cargo run --example timelock_savings
Q: How do I deploy the documentation site?
A:
Local preview:
cd docs
mdbook serve --open
GitHub Pages deployment:
# Build
mdbook build
# Deploy to the gh-pages branch
# See docs/README.md
Docker deployment:
FROM nginx:alpine
COPY docs/book /usr/share/nginx/html
EXPOSE 80
See the Documentation Deployment Guide for details.
Performance Questions
Q: Mining is too slow. What can I do?
A: Adjust the difficulty:
#![allow(unused)]
fn main() {
// In blockchain.rs
blockchain.difficulty = 3; // lower the difficulty
// 3: milliseconds
// 4: seconds
// 5: several seconds
// 6+: may be very slow
}
Or use Release mode:
cargo run --release --bin btc-demo
# Release mode is much faster than Debug
Q: Balance queries are slow. How can I speed them up?
A: Use the built-in indexer:
#![allow(unused)]
fn main() {
// SimpleBTC has a built-in indexer
let txs = blockchain.indexer.get_transactions_by_address(&address);
// Or cache balances
let balance_cache: HashMap<String, u64> = HashMap::new();
}
Security Questions
Q: Is SimpleBTC secure? Can it be used in production?
A: SimpleBTC is an educational project and is not recommended for production use.
Differences from real Bitcoin:
- Simplified cryptography (SHA256 instead of ECDSA)
- No P2P network layer
- Simplified script system
- No full SPV implementation
- JSON storage (should use LevelDB)
Requirements for production use:
- Implement full secp256k1 elliptic curve
- Implement ECDSA signature verification
- Add a P2P network protocol
- Use a professional database
- Full Script engine
- Security audit
Q: How do I protect my private key?
A: Private key security recommendations:
- Never share the private key
- Multiple backups:
- Paper wallet (fire- and water-resistant)
- Hardware wallet
- Encrypted USB drive
- Distributed storage:
- Home safe
- Bank safe deposit box
- Offsite backup
- Use multisig:
- 2-of-3 reduces single-point-of-failure risk
- Regularly test recovery
Other Questions
Q: What are the differences between SimpleBTC and real Bitcoin?
A:
| Feature | SimpleBTC | Real Bitcoin |
|---|---|---|
| Cryptography | SHA256 (simplified) | secp256k1 ECDSA |
| Consensus | PoW (simplified) | PoW (full) |
| Script | Simplified | Full Script language |
| Network | None | P2P network |
| Storage | JSON | LevelDB |
| Difficulty adjustment | Fixed | Every 2016 blocks |
Value of SimpleBTC:
- Learn Bitcoin principles
- Understand the UTXO model
- Practice blockchain development
- Rapid prototype validation
Q: How do I contribute code?
A:
- Fork the project
- Create a feature branch
- Submit a Pull Request
- Wait for review
See the Contributing Guide for details.
Q: What do I do if I find a bug?
A:
-
Open an issue on GitHub: https://github.com/GeoffreyWang1117/SimpleBTC/issues
-
Include the following information:
- Operating system
- Rust version
- Error message
- Steps to reproduce
- Relevant code
Q: Where can I get help?
A:
- Documentation: this site
- GitHub Issues: report problems and suggestions
- Rust community: https://users.rust-lang.org/
- Bitcoin whitepaper: https://bitcoin.org/bitcoin.pdf
More Resources
Didn’t find your question? Open an issue on GitHub
Contributing Guide
Thank you for your interest in the SimpleBTC project! All forms of contribution are welcome.
Ways to Contribute
1. Report Bugs
Submit a bug report on GitHub Issues.
Include the following information:
- Operating system and version
- Rust version (
rustc --version) - Error message
- Steps to reproduce
- Relevant code snippets
2. Suggest Features
Submit a feature request in Issues, explaining:
- Feature description
- Use case
- Implementation ideas (optional)
3. Contribute Code
- Fork the project
# Fork on GitHub
# Clone your fork
git clone https://github.com/YOUR_USERNAME/SimpleBTC.git
cd SimpleBTC
- Create a feature branch
git checkout -b feature/your-feature-name
-
Write code
- Follow the Rust style guide
- Add tests
- Update documentation
-
Submit a Pull Request
git add .
git commit -m "Add: your feature description"
git push origin feature/your-feature-name
Create a Pull Request on GitHub.
4. Improve Documentation
Documentation matters just as much as code!
- Fix errors
- Add examples
- Improve explanations
- Translate documentation
Development Guide
Code Style
# Format code
cargo fmt
# Run lint checks
cargo clippy
Testing
# Run all tests
cargo test
# Add tests
#[cfg(test)]
mod tests {
#[test]
fn test_something() {
// ...
}
}
Documentation Comments
#![allow(unused)]
fn main() {
/// Brief description of the function
///
/// Detailed explanation...
///
/// # Parameters
/// * `param1` - parameter description
///
/// # Return Value
/// Description of the return value
///
/// # Examples
/// \```
/// let result = function(arg);
/// \```
pub fn function(param1: Type) -> ReturnType {
// ...
}
}
Pull Request Checklist
Before submitting a PR, ensure:
- Code is formatted with
cargo fmt - Code passes
cargo clippychecks - All tests pass with
cargo test - Necessary tests have been added
- Relevant documentation has been updated
- Commit messages are clear and descriptive
Community Guidelines
- Be friendly and respectful
- Keep discussions constructive
- Welcome newcomers
- Stay focused on technical topics
License
Code contributed to this project will be released under the project’s MIT license.
Thank you for your contribution!