bitcoin源码解析 – 交易 Transcation (一)

作者:知识天地

时间:2018年1月14日

来源:https://www.cnblogs.com/mfryf/p/8284790.html

比特币中的交易可谓是比特币的最核心部分。比特币由交易产生,而区块就是用来存储交易的。所以,交易是比特币存在的载体,同时也是比特币中最复杂的部分。交易的运作层层相扣,各个部分缺一不可,十分严密,由此体现出了中本聪高超的设计技巧。接下来将会花费多个章节逐步介绍bitcoin中的交易

比特币或者类似的分布式系统在设计的时候会有一个和普通设计中有极大区别的地方:

分布式中的每个节点既是 client 也是 server。

所以在分布式系统的设计中,使用类来描述对象的时候,有时就要分清哪些情况下这个类是作为client使用的,哪些时候是作为server收到client的这个类使用。因为client和server运行的是同一套代码,而在实际运作过程中如果按照C/S的这种模型(其实不应该这样看待,应该转换自己的思维变为设计p2p节点的思维)来看待,就会看到C产出的 Tx/Block 和 S 收到的 Tx/Block。所以这时候就需要分清类中的属性哪些是属于client/server都等价对待的属性,哪些属性是分情况使用的。否则在看源码的过程中就很容易迷失自己的定位。

本章介绍交易在bitcoin源码当中的总体概况

本文只介绍 bitcoin 的 交易 在源码中是由哪几个类组合得到的,做个总体的介绍。而对于交易的原理,我们放在下一篇文章做详细介绍。本文会提前出现一些bitcoin的概念,但是目前并不需要知道指代什么,只需要先明白有这样的东西就足够了。

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

CTransaction

在图中,最为核心的类的是 CTransaction 这个类就是我们常说的bitcoin的 “交易” (一般称为 Tx, 后文也会沿用这种说法)

其实对于这个 Tx 类,这里只是一个壳,本身这个类并没有什么作用。这个类起作用的是

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

这两个关键成员变量。这两个成员变量分别代表着比特币交易的 “收入” 与 “支出”。比特币的交易并不是记录账户形的数据变化(比如我们采用银行的模型来描述 A 向 B 转账100元,那么银行在记录这个转账的过程中会出现3个记录,这三个记录连成了一个 Transaction (事务)过程:A 的账户减少100元,记录的id为 tid1,B的账户加上100元,记录的id为 tid2,一笔转账记录记录了tid1向tid2转账了100元,成为A账户减少与B账户增加的“关系连接”。),而是日志形:比特币的Tx 只记录A 向 B 转账的这个“关系连接”,这条日志记录只包含了 A 向 B 转账了 100 元这条信息。而这里的 in 就是记录着 从 ‘’谁“ 来(目前先简单的这样看,实际完全不止这样,后文会慢慢重新解释), out 就是转给了谁,而转账了多少钱是包含在 out 中的。在中本聪的命名风格是使用一个前缀代表这个属性的类型,如果是flag还会加上一个f。所以这里的 vin/vout 就是指代in 和 out 都是 vector 类型,所以这里我们可以看到,一个 Tx 的 in/out 是可以有多个的。在后文中,我们称呼 in 为 TxIn,out 为 TxOut (注意这里把 in out 比作两个人是完全不恰当的,之后会重新描述)

而这两个类的另外两个属性
int nVersion;
int nTimeLock;

前者显然是用来控制版本的(这个涉及到另一个区块链系统的核心缺陷问题 — 分叉 , 本系列可能暂时不会对这方面做出分析)
而后者在 bitcoin v0.1 源码中并没有起到什么作用。但是这个属性在今后的 bitcoin 版本中提供了一个转账过程中能够约定时间的能力,因为这个版本不涉及就不进行分析描述了(从这里也可以看出中本聪的前瞻性)

CTxIn / CTxOut

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

典型的 bitcoin 交易链:(from Developer Guide – Bitcoin)

水流流量分叉图:

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

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

举例来说:如果A 转账 100 给 B,但是现在A能控制的Out 有2个,一个是Out1是60,一个是Out2是50,那么A一检查自己的Out就会发现,60和50都不够100,那么就只能把 Out1 和 Out2 都作为当前要生产的 Tx 的 In。但是这种情况下,所有In的和就大于要支出的 100了。那么如果不付交易费的话,除去转账给 B 的 100 所对应的当前Tx的 Out,那么还会多出10。在bitcoin中就强行规定,这多出的10也要创建一个 Out 来锁住这10 块,以规定每笔交易的 In 和 Out 的总数都要相同。那么因为这 10 相当于我们通俗意义上的“找零”,所以这个 10 块的 Out 的锁当然就是 A 自己可以控制的锁,相当于这个Out指向了自己。

所以这样我们可以看到,一个交易只含有一个输入和一个输出,那么这个交易并不是看作一个人转账到了另一个人身上,而是把比特币看作像流水一样的货币流,从某个地方流入到了这个交易的输入,由从这个交易的输出流到另一个地方去。那么接下来的问题就显而易见了–如何控制货币流的流动?答案你就是 CTxIn 和 CTxOut 的属性。

我们来看下这两个类有些什么属性:
class CTxIn{
public:
COutPoint prevout;
CScript scriptSig;
unsigned int nSequence;
};
class CTxOut{
public:
int64 nValue;
CScript scriptPubKey;
};

对于 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 就是记录着”从这个出口会流出多少“的信息。简单的来说就是可以理解为通俗意义上的转账了。但是我们这里还是强调,首先理解bitcoin先抛开 支付交易 等概念,而是把 bitcoin 看成流动的水,而这里的 value 就是记录从这里会流出多少 bitcoin 的意思。显然一个 Tx 的所有 TxOut 的 value 的和 应该等于 所有 TxIn 流入的总和 (不考虑手续费,弱考虑手续费就是小于等于),否则这笔交易就应该认为是非法的(不能凭空多出钱来)。

scriptSig / scriptPubkey

那么没有介绍的 scriptSig 和 scriptPubkey 就是控制 ”凭什么从这里流出“ 的机制。这块绝对是 中本聪 创立 比特币的 又一大惊为天人的发明,这两个属性就是 导致今后著名的”智能合约“的雏形。今后将会花费详细的一篇文章仔细介绍。在这里我们先简单的介绍如下:

在刚才的讨论中我们知道,比特币流从一个交易流动到了另一个交易,像这样一直传递下去。但是这样显然是不行的,因为没有人宣告这个”流“的归属。换句话说,我们在日常中使用100块钱进行交易,核心是因为这100块的纸币从一个人的手上流动到了另个一个人的手上。但是当你持有了这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 系统没有),但是你可以控制一些 针对你的锁所锁定的钱的流动权利,那么就像水流的分叉点的出口只有你能开锁,虽然别人都看得到,但是因为别人不能开锁,那么别人也无可奈何,因为他们控制不了)。

所以我们可以看到,我们所谓的转账在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

从命名上可以看出,中本聪在设计之初就认为具有这样功能的东西应该是像”脚本“一样可以被”执行“,熟悉计算机的人当看到script 的命名就可以想象到 这个机制 是可以 ”被编程的”。而在bitcoin 的系统中也确实如此,bitcoin 提供了一系列的操作指令,可以让使用者自行编程。而验证的过程实际上就是执行了脚本。在此先不做过多描述,之后会有文章详细描述bitcoin的script系统。

COutPoint

这个类含有两个属性

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

按照上文的解释,我们可以得到这里的 hash 指代的就是 txin 所来自的那个Tx的 hash, 而n指代这个 in 是来自上一个交易的第 n 个 out, 如下图所示:

CInPoint

这个类在我们对 bitcoin 的讨论中不是很重要,这个类只出现在一个维护 COutPoint与CInPoint的 map 中。

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

它持有的属性

class CInPoint{
public:
CTransaction* ptx;
unsigned int n;
};

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

CScript

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

所以 CScript 本身这个类 不重要,重要的是 Script 说代表的指令和数据,以及这些指令的组合关系以达到相应的效果。

它的验证需要一个 VM 来执行(脚本),而执行(解析)指令的方式和指令的含义与规则就是VM的规则与实现

CTxIndex / CDiskTxPos

这两个类和bitcoin的协议也就没什么关系了,他们是用来Tx 在本地的存储与索引使用的。不过这里要注意,在bitcoin的源码中,CTxIndex 是很重要的一个类,它的存储,更新和删除控制这能否在本地存储中找到这个对应的 Tx 数据,以及标注这个Tx 是否被花费。

class CTxIndex{
public:
CDiskTxPos pos;
vector 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 的产生和 确认 不是同一个决定的,是之前所讨论的 Client 和 Server。产生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的基础上添加了

class CMerkleTx : public CTransaction{
public:
uint256 hashBlock;
vector vMerkleBranch;
int nIndex;
};

3个属性,hashBlock代表着当前的 Tx 所在的Block 的hash(作为索引),vMerkleBranch是该Tx 在 merkle tree 中 所配对的所有hash值(这个配对的hash值在以后的文章会解释),这里是用来验证Tx 在block中的附加信息。index代表着该Tx在block中的位置。

CWalletTx

这个类是 CMerkleTx 的子类,实际上就是我们产生Tx以及和wallet相关的Tx。这里我们着重介绍Tx,和wallet的信息以及产生的过程就暂时不先在这里介绍。

结尾

以上就是对bitcoin中 Tx 相关的类的介绍。只能先说明 bitcoin 实现 Tx 的过程中使用了哪些类及类中的属性可能起到的作用。在下一篇文章中我将会介绍 Tx 的运作原理。而之后关于 Tx 的文章就是根据原理看源码是如何处理的。

相关文章:

发表评论

您的电子邮箱地址不会被公开。