Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

交易处理

比特币的核心创新之一是 UTXO(Unspent Transaction Output)模型。与银行账户余额不同,比特币系统中不存在“账户余额“这一概念——所有资金都以未花费交易输出的形式分散存在于区块链中。本章深入介绍 SimpleBTC 的交易结构、UTXO 模型、交易创建和验证机制。


UTXO 模型基础

什么是 UTXO

UTXO 是“Unspent Transaction Output“(未花费交易输出)的缩写。每笔比特币交易消费若干现有 UTXO(作为输入),同时创造若干新 UTXO(作为输出)。

┌──────────────────────────────────────────────────────────────────┐
│ 传统账户模型(如银行)                                            │
│  Alice 余额: 100        Bob 余额: 0                              │
│  转账 30 → Alice: 70, Bob: 30                                    │
├──────────────────────────────────────────────────────────────────┤
│ UTXO 模型(比特币)                                               │
│  区块链上存在:UTXO_A (属于 Alice, 值 100)                        │
│  转账 30:                                                        │
│    消费:UTXO_A (100)  ← 必须整体消费,不能部分花费              │
│    创造:UTXO_B (30, 属于 Bob)   ← 转账金额                      │
│          UTXO_C (60, 属于 Alice) ← 找零(100 - 30 - 10 手续费)  │
└──────────────────────────────────────────────────────────────────┘

UTXO 模型的关键特性:

  • 每个 UTXO 只能被花费一次(花费后从 UTXO 集合中移除)
  • 花费时必须消费完整的 UTXO,多余的部分以“找零“输出返还给发送者
  • “余额”= 某地址拥有的所有 UTXO 价值之和(由 UTXOSet 计算)
  • 没有被花费的 UTXO 形成“UTXO 集“(比特币全节点需要维护约 5-10 GB 的 UTXO 集)

交易数据结构

TxInput(交易输入)

交易输入引用一个现有的 UTXO,并提供花费它的授权证明(数字签名):

#![allow(unused)]
fn main() {
// src/transaction.rs
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TxInput {
    pub txid: String,       // 被引用交易的 ID(32 字节哈希,64 hex 字符)
    pub vout: usize,        // 该交易中第几个输出(从 0 开始)
    pub signature: String,  // ECDSA 签名(DER 编码,hex 字符串)
    pub pub_key: String,    // 发送者的压缩公钥(33 字节,66 hex 字符)
}
}

txid + vout 的组合唯一定位区块链上的某个 UTXO。signature 由发送者的私钥生成,证明其对该 UTXO 的所有权。

TxOutput(交易输出)

交易输出定义了接收方可获得的金额,是新 UTXO 的载体:

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TxOutput {
    pub value: u64,           // 金额(单位:satoshi,1 BTC = 10⁸ satoshi)
    pub pub_key_hash: String, // 接收者地址(P2PKH)= 锁定脚本
}

impl TxOutput {
    pub fn new(value: u64, address: String) -> Self {
        TxOutput { value, pub_key_hash: address }
    }

    /// 检查是否可以被某地址解锁(即该地址是否为此输出的接收者)
    pub fn can_be_unlocked_with(&self, address: &str) -> bool {
        self.pub_key_hash == address
    }
}
}

pub_key_hash 在真实比特币中是锁定脚本(locking script / scriptPubKey),这里简化为接收者地址。

Transaction(交易)

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Transaction {
    pub id: String,              // 交易 ID = SHA-256(交易内容)
    pub inputs: Vec<TxInput>,    // 输入列表(消费哪些 UTXO)
    pub outputs: Vec<TxOutput>,  // 输出列表(创建哪些新 UTXO)
    pub timestamp: u64,          // Unix 时间戳(秒)
    pub fee: u64,                // 手续费 = 输入总额 - 输出总额
}
}

不变量: 输入总额 = 输出总额 + 手续费


创建普通交易

普通交易通过 Blockchain::create_transaction() 创建,该方法会自动处理 UTXO 选择、找零计算和签名:

#![allow(unused)]
fn main() {
pub fn create_transaction(
    &self,
    from_wallet: &Wallet,   // 发送者钱包(需要私钥签名)
    to_address: String,     // 接收者地址
    amount: u64,            // 转账金额(satoshi)
    fee: u64,               // 手续费(satoshi)
) -> Result<Transaction, String>
}

内部流程:

#![allow(unused)]
fn main() {
// src/blockchain.rs 节选(简化展示)

// 1. 计算总需求
let total_needed = amount + fee;

// 2. 从 UTXO 集合中查找足够的 UTXO(排除已被待确认交易花费的)
let spendable = self.utxo_set.find_spendable_outputs_excluding(
    &from_wallet.address,
    total_needed,
    &self.pending_spent,
);
let (accumulated, utxos) = spendable.ok_or_else(|| "余额不足(包括交易费)".to_string())?;

// 3. 对每个选中的 UTXO 创建输入并签名
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. 创建输出(转账 + 找零)
let mut outputs = Vec::new();
outputs.push(TxOutput::new(amount, to_address));
if accumulated > total_needed {
    // 找零返还给发送者(扣除手续费)
    outputs.push(TxOutput::new(accumulated - total_needed, from_wallet.address.clone()));
}

// 5. 构造交易(自动计算 ID)
Ok(Transaction::new(inputs, outputs, timestamp, fee))
}

完整使用示例:

#![allow(unused)]
fn main() {
use bitcoin_simulation::{blockchain::Blockchain, wallet::Wallet};

let mut blockchain = Blockchain::new();
let genesis = Blockchain::genesis_wallet(); // 预置资金 10M satoshi
let alice = Wallet::new();

// genesis 向 alice 转账 5000 satoshi,手续费 100
let tx = blockchain.create_transaction(
    &genesis,
    alice.address.clone(),
    5000,   // 转账金额
    100,    // 手续费
)?;

println!("交易 ID: {}", tx.id);
println!("输入数量: {}", tx.inputs.len());
println!("输出数量: {}", tx.outputs.len()); // 通常为 2(转账 + 找零)
println!("手续费: {} satoshi", tx.fee);
println!("费率: {:.2} sat/byte", tx.fee_rate());
}

Coinbase 交易

Coinbase 交易是每个区块的第一笔交易,专门用于向矿工发放区块奖励。它与普通交易的关键区别:

特性普通交易Coinbase 交易
输入引用现有 UTXO空 txid(凭空创建)
输出转账 + 找零区块奖励 + 所有手续费
签名ECDSA 签名无需签名(pub_key = "coinbase"
验证验证所有输入签名跳过签名验证(is_coinbase() = true
#![allow(unused)]
fn main() {
pub fn new_coinbase(to: String, reward: u64, timestamp: u64, total_fees: u64) -> Self {
    // 使用原子计数器保证每个 coinbase 交易 ID 唯一(类似 BIP34 区块高度编码)
    static COINBASE_COUNTER: AtomicU64 = AtomicU64::new(0);
    let nonce = COINBASE_COUNTER.fetch_add(1, Ordering::Relaxed);

    // 输出 = 区块奖励 + 所有交易手续费
    let tx_out = TxOutput::new(reward + total_fees, to);
    let tx_in = TxInput {
        txid: String::new(),              // 空 txid 标识 coinbase
        vout: 0,
        signature: format!("coinbase:{}", nonce),  // 唯一性字段
        pub_key: String::from("coinbase"),
    };
    // ...
}
}

Coinbase 交易识别方式:

#![allow(unused)]
fn main() {
pub fn is_coinbase(&self) -> bool {
    // 只有一个输入,且该输入的 txid 为空
    self.inputs.len() == 1 && self.inputs[0].txid.is_empty()
}
}

交易验证

Transaction::verify() 验证所有输入的 ECDSA 签名:

#![allow(unused)]
fn main() {
pub fn verify(&self) -> bool {
    // Coinbase 交易无需验证签名
    if self.is_coinbase() {
        return true;
    }

    // 必须有输入和输出
    if self.inputs.is_empty() || self.outputs.is_empty() {
        return false;
    }

    // 对每个输入验证 ECDSA 签名
    for input in &self.inputs {
        // 签名的原始数据:被花费 UTXO 的位置标识
        let signed_data = format!("{}{}", input.txid, input.vout);
        if !Wallet::verify_signature(&input.pub_key, &signed_data, &input.signature) {
            return false;
        }
    }

    true
}
}

验证逻辑说明:

  • 签名数据为 "{txid}{vout}",将签名绑定到具体的 UTXO,防止签名重放攻击
  • 使用输入中携带的公钥(pub_key)验证签名,全节点无需额外查询
  • Wallet::verify_signature() 内部调用 secp256k1 执行真实的椭圆曲线数学验证

添加交易到区块链时的完整验证流程(Blockchain::add_transaction()):

#![allow(unused)]
fn main() {
// 1. ECDSA 签名验证
if !transaction.verify() {
    return Err("交易验证失败".to_string());
}

// 2. 验证 UTXO 存在且余额充足
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 不存在".to_string());
        }
    } else {
        return Err("引用的交易不存在".to_string());
    }
}

let output_sum: u64 = transaction.outputs.iter().map(|o| o.value).sum();
if input_sum < output_sum {
    return Err("余额不足,交易无效".to_string());
}

// 3. 记录待确认 UTXO(防止同一 UTXO 被两笔待确认交易双花)
for input in &transaction.inputs {
    self.pending_spent.insert(format!("{}:{}", input.txid, input.vout));
}
}

手续费与费率

手续费计算

手续费 = 输入总额 - 输出总额

例如:花费 UTXO (100 satoshi),转账 85,找零 5,手续费 = 100 - 85 - 5 = 10 satoshi

费率

#![allow(unused)]
fn main() {
pub fn fee_rate(&self) -> f64 {
    let size = self.size();  // 交易序列化后的字节大小
    if size == 0 { return 0.0; }
    self.fee as f64 / size as f64  // 单位:satoshi/byte
}
}

典型费率参考(真实比特币,随网络拥堵波动):

优先级费率确认时间
1–5 sat/byte数小时甚至更长
5–20 sat/byte30–60 分钟
20–50 sat/byte10–20 分钟
紧急50+ sat/byte下一个区块(约 10 分钟)

SimpleBTC 的内存池按费率排序,高费率交易优先被打包:

#![allow(unused)]
fn main() {
// 获取按费率排序的顶部交易(Mempool 内部逻辑)
let pending_txs = self.mempool.get_top_transactions(usize::MAX);
}

交易生命周期

用户发起转账请求
       │
       ▼
blockchain.create_transaction(&wallet, to, amount, fee)
  → 选择 UTXO,生成签名,构造 Transaction
       │
       ▼
blockchain.add_transaction(tx)
  → 验证签名(ECDSA)
  → 验证 UTXO 存在 + 余额充足
  → 加入 Mempool(按费率排序)
  → 标记已花费 UTXO(pending_spent)
       │
       ▼  等待矿工打包
       │
       ▼
blockchain.mine_pending_transactions(miner_address)
  → 从 Mempool 取出高优先级交易
  → 创建 Coinbase 交易(奖励 + 手续费)
  → 并行 PoW 挖矿
  → 更新 UTXO 集合(原子操作)
  → 区块上链,清空 pending_spent
       │
       ▼
交易获得 1 次确认
(每增加一个后续区块 = +1 次确认)

交易哈希计算

#![allow(unused)]
fn main() {
pub fn calculate_hash(&self) -> String {
    // 将交易序列化为 JSON,计算 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())
}
}

真实比特币使用双重 SHA-256(SHA256d),并且序列化格式为紧凑二进制格式;这里为了教学简化为 JSON + 单次 SHA-256。


输出总额查询

#![allow(unused)]
fn main() {
// 获取所有输出的金额之和
let output_sum = tx.output_sum();

// 交易大小(字节数,影响手续费计算)
let size = tx.size();

// 是否为 Coinbase 交易
let is_cb = tx.is_coinbase();
}

完整交易示例

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();

    // 第一笔:genesis → alice,转账 10000 satoshi
    let tx1 = blockchain.create_transaction(&genesis, alice.address.clone(), 10000, 50)?;
    println!("tx1 id: {}", tx1.id);
    println!("tx1 输出数: {}", tx1.outputs.len()); // 2(转账 + 找零)
    println!("tx1 费率: {:.2} sat/byte", tx1.fee_rate());
    blockchain.add_transaction(tx1)?;

    // 挖矿确认(alice 收矿工奖励)
    blockchain.mine_pending_transactions(alice.address.clone())?;

    println!("Alice 余额: {} satoshi", blockchain.get_balance(&alice.address));

    // 第二笔:alice → bob,转账 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 余额: {} satoshi", blockchain.get_balance(&alice.address));
    println!("Bob 余额:   {} satoshi", blockchain.get_balance(&bob.address));

    // 验证链完整性
    assert!(blockchain.is_valid());

    Ok(())
}