bitcoin 源码分析

此处只分析bitcoin-core交易部分源码。

首先先看源码对应的 UML 类图:

这个 UML 图中包含了 bitcoin 交易相关的所有关键类。

CTransaction

最核心的类的是 CTransaction 。这个类就是 bitcoin 的 “交易” (一般称为 Tx, 后文也使用此称)。这个类的作用大体起一个壳的作用。主要的属性是

vector<CTxIn> vin;
vector<CTxOut> vout;

这两个关键成员变量分别代表着比特币交易的 “收入” 与 “支出”。

比特币的交易并不是记录账户形的数据变化,比如这里建立一个银行的转账模型为:

而是日志形:

源码的命名风格是使用前缀代表这个属性的类型,如果是 flag 会加上一个 f。这里的 vin/vout 就是指代 in 和 out 都是 vector 类型,所以这里我们可以看到,一个 Tx 的 in/out 是可以有多个的。在后文中,我们称呼 in 为 TxIn,out 为 TxOut。

另外两个属性

int nVersion;
int nTimeLock;

前者显然是用来控制版本的,后者在新的 bitcoin 版本中提供了一个转账过程中能够约定时间的能力。

CTxIn / CTxOut

之前所说的比特币交易是两个人之间的转账是不太合适的,从这步起,我们直接抛弃 ”两个人之间进行交易“这样的概念,直接认为在比特币的交易系统中是不具备”所有人“这样概念(这样肯定很奇怪因为都没有所有人了比特币还有什么意义,但之后会解释),而只是把 ”交易“ 看作 ”比特币流“ 的中转的中转节点,就像水流分叉合并的那些节点一样:

典型的 bitcoin 交易链:

水流流量分叉图:

每个交易就是一个分叉节点,而每个交易的 in / out 就是 这个中转(分叉)节点的流入和流出。

bitcoin 有一个相当相当重要的规定就是 每个 Tx 的 所有 In 进入了货币流必须在这个交易中全部流出去,流出去不代表成为其他 Tx 的 In,而是必须要成为一个 TxOut。

所以我们可以看到,一个交易只含有一个输入和一个输出,那么这个交易并不是看作一个人转账到了另一个人身上,而是把比特币看作像流水一样的货币流,从某个地方流入到了这个交易的输入,由从这个交易的输出流到另一个地方去。

CTxIn

COutPoint 这个类如其名,就是起到 Point 的作用,但是它的命名是 OutPoint, 最初接触的时肯定会感到迷惑。但这个名字确实起得十分正确:TxIn 虽然按照之前的分析可知它是 Tx 的 流入,而流入 Tx 必定来自于 另一个 Tx。 TxIn 只是 Tx 的一个属性,描述了 本 Tx 的 ”流入“ 的情况,但它本身也是个壳,而从哪个 Tx 流入的信息就是由 COutPoint 所记录。所以对于本 Tx 来说,TxIn 所含有的 ”从哪流入的“ ,上一个 Tx 对于本 Tx 来说 就是上一个 Tx 的 Out 的指向。而本 Tx 是不能持有上一个 Tx 的 out 的,所以就使用了 Point 指针来记录。

nSequence 在 v0.1 中没有起到什么作用,也不会用来作校验,但是这个字段今后被作为了其他用途,而且成为了 bitcoin 的一个软分叉的最佳例子。

CTxOut

value 就是记录着”从这个出口会流出多少“的信息。

scriptSig / scriptPubkey 就是控制 ”凭什么从这里流出“ 的机制。这两个属性就是 导致今后著名的”智能合约“的雏形。

在刚才的讨论中我们知道,比特币流从一个交易流动到了另一个交易,像这样一直传递下去。但是这样显然是不行的,因为没有人宣告这个”流“的归属。换句话说,我们在日常中使用 100 块钱进行交易,核心是因为这 100 块的纸币从一个人的手上流动到了另个一个人的手上。

但是在比特币的体系中,请直接抛弃这种思想,而是使用另一种方式来思考,而这种方式当你换个角度看的时候它就和你交易 100 块的纸币是相同的。

这种思想那就是当我们重新审视交易的时候,我们发现货币流是从一个(多个)交易流向一个(多个)交易的过程。那么如果我们有独特的手段能够控制它为什么能流动,比如说在流出去的时候我们采取一个手段给这个出口 out 上一个锁,而当你想控制这个从这个出口出去的流的时候你就创造一把能打开这个锁的钥匙,作为下一个交易的 in 。也就是说我们连续的看 2 个交易的中间部分:前一个交易的 out 和 后一个交易的 in。如果我们能把 前一个 out 加一个锁,然后规定后一个 in 要能成立的条件是 in 附带的 钥匙 能够打开 out 锁。那么因为上锁和开锁是“个人”行为,但一个交易的 out 被上了一把只有一个特别的人才能打开的锁,那么就像本应该从这个 out 流出去的货币被“锁在了这个 out”里面,这个上锁的过程不需要这个特别的人参与,这个特别的人只需要提供一点信息代表这把锁只能他打开就行,就是指地址。那么这种行为就等价于只有这个特别的人才能“控制”这个 Tx 的 Out,也就是只有这个特别的人才能 “占有” Out 里面被锁住的钱。

虽然这个钱并不像现实生活中能够实实在在的把这 100 块钱拿到手上,而只能是通过 in/out 的锁控制 钱的流动。但是我们换个方向想,虽然我们只能提供这个“钥匙”,但是这个“钥匙和锁”能够控制 out 所含有的货币流动,那么这个 in/out 的上锁开锁机制就相当于你占有了这笔钱。

所以我们可以看到,我们所谓的转账在 bitcoin 的系统当中,例如 A 转账 100 给 B,那么就需要 B 向 A 提供一些信息(bitcoin 地址),这个信息不会暴露 B 的个人情况,但是可以表明 B 能够控制由这个信息产生的锁。随后 A 就可以创建一个交易,这个交易的 out 就可以用 B 提供的这个信息上了一把只有 B 能够控制的锁,然后这个交易的 in 就是 由 A 提供 A 能够控制的其他交易的 Out 的 对应的钥匙。 如下图所示:

CTxIn 和 CTxOut 的属性 scriptSig 和 scriptPubkey 就是刚在我们讨论中的 钥匙 和 锁。 scriptSig 就是用于对应签名的钥匙,而 scriptPubkey 就是 B 提供了地址而生成的锁。

而我们所说的实现钥匙和锁的功能,依靠的就是 这两个属性的类型 -> CScript

COutPoint

这个类含有两个属性

class COutPoint{
public:
    uint256 hash;
    unsigned int n;
};

这里的 hash 指代的就是 txin 所来自的那个 Tx 的 hash, 而 n 指代这个 in 是来自上一个交易的第 n 个 out:

CInPoint

这个类只出现在一个维护 COutPoint 与 CInPoint 的 map 中。 所以我们认为 CInPoint 和 COutPoint 是 键值对应关系。当我们确认了一个 COutPoint 的时候,我们可以假装把这个 COutPoint 看作是上一个 Tx 的 Out, 那么 这个 map 对应的 CInPoint 意思就是指代为 上一个 Tx 的 Out 指向的 下一个。

CTransaction* 是一个 针对 COutPoint 这个 Out 指向的 In 所在的那个交易。那么在 COutPoint 那个图的例子中就是指代当前的 Tx 这个 Tx 的指针。而这里的 n 就是指代 这个 In 是当前的 Tx 的 第 n 个 In, 在上图中也同样是 0 (因为只有 1 个 In)。

CScript

Script 实际上就是一串 Bytes 流。只不过这个字节流 是可以被解析为 <指令> 或者 <指令> <数据> 这样的一个一个元信息。而一个 Script 就是这些元信息组成的字节流。

CTxIndex / CDiskTxPos

他们是用来 Tx 在本地的存储与索引使用的。在 bitcoin 的源码中,CTxIndex 是很重要的一个类,它的存储,更新和删除控制这能否在本地存储中找到这个对应的 Tx 数据,以及标注这个 Tx 是否被花费。

class CTxIndex{
public:
    CDiskTxPos pos;
    vector<CDiskTxPos> vSpent;
};
class CDiskTxPos{
public:
    unsigned int nFile;
    unsigned int nBlockPos;
    unsigned int nTxPos;
};

在存储中,bitcoin 使用 Tx 的 hash 为键,CTxIndex 为值进行存储。所以在拿到一个 CTransaction(或其子类)可以通过得到这个 Tx 的 hash 索引本地的存储 得到这个 Tx 所对应的 TxIndex。

而 TxIndex 的属性 vSpent 就是一个相当重要的属性,因为这关系到一个 Tx 的 Out 是否是 UTXO(Unspent Transaction Output)。由前文的讨论可知,那么一个 UTXO 就是一个被上锁了但是没有被开锁过的 Out。而这个 TxIndex 的 vSpent 是一个 vector ,它和当前 Tx 的 vout 相对应。

这里我们要强调,Tx 的产生和 确认 不是同一个决定的。产生 Tx 的称为 client ,接受确认这个 Tx 合法的是 Server, Client 和 Server 存储的 CTxIndex 是不会进行传输的!所以 CTxIndex 在 C/S 上是分别生成的。那么我们使用 CTxIndex 的 vSpent 来标识这个 Out 是不是一个 UTXO 就相当重要了。因为 C / S 分别的存储都是根据自己的历史生成的,所以如果 Client 要欺骗别人, 是不能在 别人自己的验证中通过的。

举例来说就是 A 产生了 Tx 并告诉别人来确认这个 Tx 是合法的,但是 A 使用的一个 in 来自的一个 Out 是已经被花费过的,比如我们假设这个 Out 所在的 Tx 叫做 Tx_prev,这个 Out 是第 3 个 Out,但是 A 不管,仍然使用了这个被花费过的 Out。那么当别人收到这个 Tx 进行验证的时候,他们就检查自己 Tx_prev 所对应的自己的本地存储的 Tx_index_prev ,然后一检查 vSpent[3] 是否是 null, 如果是 null 那么就是合法的,如果不是 Null,那么就代表这个 Out 已经被花费过了。可见这里的验证是和 A 的本地存储无关的,A 不可能修改自己的本地存储来欺骗别人。因为传输的内容只有 Tx, 而 TxIndex 是各个节点根据收到的 Tx 或 block 自己生成的。所以节点们一检查发现 vSpent[3] 不是个 null, 那么就会认为 A 的那个 Tx 是非法的。

CDiskTxPos 是代表这这个 Tx 在本地存储的位置。 在 bitcoin 源码中,Tx 的存储是紧密的排列在文件当中的,而找到这个 Tx 就是首先找到存储的文件,再找到这个 Tx 在这个文件中的偏移。所以 nFile 和 nTxPos 就分别代表着是*哪一个文件*和*在文件中的偏移位置*。

nBlockPos 代表这个 Tx 在 Block 中的位置。

CMerkleTx

这个类是 Tx 的子类,这个类使用来在 Block 中相关处理的时候用的。CMerkleTx 是矿工(前文指代的 server)所保存 Tx 时相关的类

它在原本的 Tx 的基础上添加了 3 个属性

class CMerkleTx : public CTransaction{
public:
    uint256 hashBlock;
    vector<uint256> vMerkleBranch;
    int nIndex;
};
  • hashBlock 代表着当前的 Tx 所在的 Block 的 hash(作为索引)
  • vMerkleBranch 是用来验证 Tx 在 block 中的附加信息
  • index 代表着该 Tx 在 block 中的位置

CWalletTx

这个类是 CMerkleTx 的子类,实际上就是我们产生 Tx 以及和 wallet 相关的 Tx。