深入比特币核心,交易源码解析与关键机制剖析
摘要:比特币作为第一个成功的去中心化数字货币,其核心在于一套精密的密码学和经济模型,而交易的生成、验证与打包,则是整个比特币网络运行的生命线,理解比特币交易的源码,是掌握其工作原理、安全机制和设计哲学的关键...
比特币作为第一个成功的去中心化数字货币,其核心在于一套精密的密码学和经济模型,而交易的生成、验证与打包,则是整个比特币网络运行的生命线,理解比特币交易的源码,是掌握其工作原理、安全机制和设计哲学的关键,本文将以比特币核心参考客户端(Bitcoin Core)的源码为基础,深入剖析比特币交易的结构、签名验证、脚本系统等核心部分。
比特币交易:数字世界的所有权转移凭证
在源码层面,比特币交易的本质是一组数据结构,它规定了比特币的转移,一个典型的交易由以下几个主要部分构成(以C++代码结构为例,主要在src/primitives/transaction.h中定义):
- 版本号 (Version):
int32_t类型,标识交易的版本号,用于未来协议升级。 - 标记 (Optional Marker/Flag): 在隔离见证(SegWit)交易中引入,用于区分SegWit交易和传统交易。
- 输入列表 (Inputs):
CTxIn对象的向量,表示交易花费的来源。- 前一笔交易哈希 (prevout.hash): 指向被花费的UTXO(Unspent Transaction Output)所在交易的哈希。
- 前一笔交易输出索引 (prevout.n): 指向被花费的UTXO在其交易中的索引。
- 签名脚本 (ScriptSig): 也称为 unlocking script,提供满足前一笔交易输出脚本(锁定脚本)的条件,通常包含签名和公钥。
- 序列号 (nSequence): 用于相对锁定时间和替代交易(RBF)等功能。
- 输出列表 (Outputs):
CTxOut对象的向量,表示交易产生的新的UTXO。- 金额 (nValue):
int64_t类型,以聪(satoshi,比特币的最小单位)为单位的转账金额。 - 锁定脚本 (ScriptPubKey): 也称为 script,定义了未来花费该UTXO的条件,通常包含公钥哈希或其他脚本逻辑。
- 金额 (nValue):
- 见证数据 (Witness): 在隔离见证(SegWit)中引入,存储在交易外部但与交易关联,包含签名和公钥等,与ScriptSig分开,提升了可扩展性,存储在
CTxWitness结构中(src/primitives/witness.h)。 - 锁定时间 (nLockTime):
uint32_t类型,表示该交易最早可以被纳入区块的时间或高度,可以是区块高度或UNIX时间戳。
// 简化的CTransaction结构示意 (src/primitives/transaction.h)
class CTransaction
{
public:
// 基本字段
int32_t nVersion;
std::vector<CTxIn> vin;
std::vector<CTxOut> vout;
uint32_t nLockTime;
// SegWit相关
bool HasWitness() const;
CTxWitness witness; // 见证数据
// 构造函数、序列化、验证等方法...
};
交易的生成与签名:所有权的证明
用户创建交易时,需要指定要花费的UTXO(输入)、接收地址(输出)以及支付金额,核心在于如何正确构建签名脚本(ScriptSig)以证明对输入UTXO的所有权。
-
UTXO查找与选择: 源码中,
CWallet类负责管理用户的UTXO集合(mapWallet),当创建交易时,钱包会选择合适的UTXO来满足支付金额和找零需求(SelectCoins函数,大致在src/wallet/coinselection.cpp)。 -
签名过程: 签名是交易安全的核心,比特币使用椭圆曲线数字签名算法(ECDSA)。
- 获取签名哈希 (Signature Hash, SIGHASH): 在签名之前,需要对交易的部分数据进行哈希,生成一个签名哈希,SIGHASH类型决定了哪些数据被哈希以及如何签名,允许签名者对交易的不同部分进行承诺(SIGHASH_ALL签名所有输入和输出,SIGHASH_SINGLE签名对应索引的输出)。
- 源码中,
SignatureHash函数(src/script/interpreter.cpp)负责计算不同类型的SIGHASH。
- 源码中,
- 生成签名: 针对每个输入,钱包使用用户的私钥对相应的SIGHASH进行签名,生成ECDSA签名(通常使用DER编码)。
- 构建ScriptSig: 对于P2PKH(Pay-to-Public-Key-Hash)类型,ScriptSig通常包含:
<签名> <公钥>,这个脚本会被提供给输入,用于验证签名是否有效。
- 获取签名哈希 (Signature Hash, SIGHASH): 在签名之前,需要对交易的部分数据进行哈希,生成一个签名哈希,SIGHASH类型决定了哪些数据被哈希以及如何签名,允许签名者对交易的不同部分进行承诺(SIGHASH_ALL签名所有输入和输出,SIGHASH_SINGLE签名对应索引的输出)。
// 简化的签名过程示意 (src/wallet/transaction.cpp 等相关文件)
bool CWallet::SignTransaction(CMutableTransaction& tx, std::map<COutPoint, CScript>& mapScriptPubKeys, int nHashType)
{
for (size_t i = 0; i < tx.vin.size(); ++i) {
const CTxIn& txin = tx.vin[i];
CScript scriptPubKey = mapScriptPubKeys[txin.prevout];
// 获取对应的私钥(通常从钱包中)
CKey key;
if (!GetKey(txin.prevout.hash, key)) // 假设GetKey能找到对应私钥
return false;
// 计算签名哈希
uint256 hashSig = SignatureHash(scriptPubKey, tx, i, nHashType);
// 使用私钥签名
std::vector<unsigned char> vchSig;
if (!key.Sign(hashSig, vchSig))
return false;
// DER编码签名,并添加SIGHASH类型
vchSig.push_back((unsigned char)nHashType);
tx.vin[i].scriptSig << vchSig; // 将签名添加到ScriptSig
// 如果是P2PKH,还需要添加公钥
if (scriptPubKey.IsPayToPublicKeyHash()) {
CPubKey pubkey = key.GetPubKey();
tx.vin[i].scriptSig << pubkey;
}
}
return true;
}
交易验证:网络共识的基石
当一个交易被广播到比特币网络,或者节点在打包区块时,都需要对交易进行严格的验证,验证逻辑主要在CTxMemPoolEntry(内存池交易条目)和ValidateTransaction函数(src/validation.cpp)中体现。
-
语法验证:
- 检查交易大小是否在合理范围内。
- 检查输入输出数量是否非空且不超过限制。
- 检查每个输入的prevout是否存在且未被花费(通过UTXO集检查)。
- 检查锁定时间是否合法。
-
语义验证:
- 输入值非负且总和不超过输出值: 确保没有凭空创造比特币(交易费 = 输入总和 - 输出总和 >= 0)。
- 脚本验证: 这是最核心的部分。
- 对于每个输入,节点会取出其
ScriptSig和对应输出(UTXO)的ScriptPubKey。 - 脚本解释器(
ScriptInterpreter,src/script/interpreter.cpp)会执行这两个脚本的组合。ScriptSig提供数据(签名、公钥),ScriptPubKey定义执行逻辑(操作码序列)。 - P2PKH脚本的执行过程大致是:
ScriptSig被压入栈:[签名] [公钥]ScriptPubKey被压入栈:OP_DUP OP_HASH160 <公钥哈希> OP_EQUALVERIFY OP_CHECKSIG- 解释器依次执行操作码:
OP_DUP: 复制栈顶元素(公钥)。OP_HASH160: 对复制的公钥进行RIPEMD160(SHA256())哈希,得到公钥哈希。OP_EQUALVERIFY: 比较栈顶的公钥哈希与ScriptPubKey中的<公钥哈希>是否相等,不等则失败。OP_CHECKSIG: 使用栈顶的签名和次栈顶的公钥,验证签名是否对正确的消息(SIGHASH)有效。
- 如果脚本执行最终结果为
true(非零非空),则该输入验证通过,所有输入验证通过,交易才有效。
- 对于每个输入,节点会取出其
// 简化的脚本验证示意 (src/script/interpreter.cpp) bool EvalScript(std::vector<std::vector<unsigned char>>& stack, const CScript& scriptCode, const CScript& scriptSig
