bitcoin原理与源码解析

bitcoin源码分析

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

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

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

CTransaction

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

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

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

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

而是日志形:

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

另外两个属性

1
2
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

这个类含有两个属性

1
2
3
4
5
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 是否被花费。

1
2
3
4
5
6
7
8
9
10
11
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。