主页 > 苹果手机如何下载imtoken > 比特币源码分析——交易交易(一)

比特币源码分析——交易交易(一)

苹果手机如何下载imtoken 2023-02-02 05:45:35

比特币交易可以说是比特币最核心的部分。比特币是由交易产生的,区块用于存储交易。因此,交易是比特币存在的载体,也是比特币最复杂的部分。交易的运作是环环相扣的,每一个环节都缺一不可,而且非常严格,体现了中本聪高超的设计功底。接下来,我们将用多个章节逐步介绍比特币中的交易。

比特币或者类似的分布式系统在设计的时候会和普通的设计有很大的不同:

分发中的每个节点既是客户端又是服务器。

因此,在分布式系统的设计中,在使用类来描述对象时,有时需要区分该类何时用作客户端,何时用作服务器从客户端接收的类。因为客户端和服务端运行的是同一组代码,而在实际运行过程中,如果你看C/S这个模型(其实你不应该这样看,你应该换个思路到考虑设计p2p节点),你会看到C产生的Tx/Block和S接收的Tx/Block。因此,此时需要区分类中哪些属性属于客户端/服务器平等对待,根据情况使用哪些属性。除此以外,

本章介绍比特币源代码中交易的总览

本文只介绍比特币交易源码中几个类的组合,并做一个大概的介绍。至于交易的原理,我们将在下一篇文章中详细介绍。在这篇文章中,比特币的一些概念会提前出现,但目前没有必要知道它指的是什么。了解有这样的事情就足够了。

如上面的类图所示,这个 UML 图包含了与比特币交易相关的所有关键类。

交易

图中,核心类是CTransaction。这个类就是我们常说的比特币的“交易”(一般叫Tx,后面会用到)

其实对于这个Tx类来说,这只是一个shell,类本身没有任何作用。这个类的作用是

vector vin;
vector vout;

这两个关键成员变量。这两个成员变量分别代表比特币交易的“收入”和“支出”。比特币交易不会以账户的形式记录数据变化(比如我们用银行模型来描述从 A 到 B 的 100 元转账,那么银行在记录这次转账的过程中会记录 3 条记录,而这些3条记录连接为A Transaction(交易)过程:A的账户减100元,记录id为tid1,B的账户加100元,记录id为tid2,一条转账记录记录tid1转账100元tid2,成为 A 账户减少和 B 账户增加之间的“关系”呈对数形:比特币的 Tx 只记录“ 该日志记录仅包括A向B转账100元的信息。而这里的in是从'who'记录下来的(目前看的比较简单,其实不止这些,后面会重新解释),out是转给谁的比特币项目开源代码,还有转出的金额包含在 out 中。在中本聪的命名风格中,使用前缀来表示这个属性的类型。如果它是一个标志,一个 f 将被添加。所以这里的vin/vout表示in和out都是向量类型,所以这里我们可以看到一个Tx可以有多个in/out。在下面的文字中,我们调用了TxIn和out TxOut(注意,将in out与两个人进行比较是完全不合适的,我们稍后会重新讨论。描述)该日志记录仅包括A向B转账100元的信息。而这里的in是从'who'记录下来的(目前看的比较简单,其实不止这些,后面会重新解释),out是转给谁的,还有转出的金额包含在 out 中。在中本聪的命名风格中,使用前缀来表示这个属性的类型。如果它是一个标志,一个 f 将被添加。所以这里的vin/vout表示in和out都是向量类型,所以这里我们可以看到一个Tx可以有多个in/out。在下面的文字中,我们调用了TxIn和out TxOut(注意,将in out与两个人进行比较是完全不合适的,我们稍后会重新讨论。描述)而这里的in是从'who'记录下来的(目前看的比较简单,其实不止这些,后面会重新解释),out是转给谁的,还有转出的金额包含在 out 中。在中本聪的命名风格中,使用前缀来表示这个属性的类型。如果它是一个标志,一个 f 将被添加。所以这里的vin/vout表示in和out都是向量类型,所以这里我们可以看到一个Tx可以有多个in/out。在下面的文字中,我们调用了TxIn和out TxOut(注意,将in out与两个人进行比较是完全不合适的,我们稍后会重新讨论。描述)而这里的in是从'who'记录下来的(目前看的比较简单,其实不止这些,后面会重新解释),out是转给谁的,还有转出的金额包含在 out 中。在中本聪的命名风格中,使用前缀来表示这个属性的类型。如果它是一个标志,一个 f 将被添加。所以这里的vin/vout表示in和out都是向量类型,所以这里我们可以看到一个Tx可以有多个in/out。在下面的文字中,我们调用了TxIn和out TxOut(注意,将in out与两个人进行比较是完全不合适的,我们稍后会重新讨论。描述)out 是转给谁的,转出的金额包含在 out 中。在中本聪的命名风格中,使用前缀来表示这个属性的类型。如果它是一个标志,一个 f 将被添加。所以这里的vin/vout表示in和out都是向量类型,所以这里我们可以看到一个Tx可以有多个in/out。在下面的文字中,我们调用了TxIn和out TxOut(注意,将in out与两个人进行比较是完全不合适的,我们稍后会重新讨论。描述)out 是转给谁的,转出的金额包含在 out 中。在中本聪的命名风格中,使用前缀来表示这个属性的类型。如果它是一个标志,一个 f 将被添加。所以这里的vin/vout表示in和out都是向量类型,所以这里我们可以看到一个Tx可以有多个in/out。在下面的文字中,我们调用了TxIn和out TxOut(注意,将in out与两个人进行比较是完全不合适的,我们稍后会重新讨论。描述)

以及这两个类的另外两个属性

int nVersion;
int nTimeLock;

前者显然是用来控制版本的(这涉及到区块链系统的另一个核心缺陷——分叉,本系列暂时可能不分析这方面)

后者在比特币v0.1的源代码中没有发挥作用。不过这个属性会在以后的比特币版本中提供指定转账过程中时间的能力,因为这个版本不涉及任何分析和描述(从这里我们也可以看出中本聪的前瞻性)

CTxIn / CTxOut

从这一步,我们直接抛弃了“两个人之间的交易”的概念,直接认为比特币交易系统中没有“所有权”的概念(这一定很奇怪,因为没有比特币的所有者是什么意思币,但我稍后会解释),但只是将“交易”视为“比特币流”的中转节点,就像那些被水流叉合并的节点一样:

典型的比特币交易链:(来自开发者指南 - 比特币)

流分岔图:

每笔交易都是一个中转(分叉)节点,每笔交易的进出都是这个中转(分叉)节点的流入和流出。

比特币有个很重要的规定,就是每一个Tx的In全部进入币流,并且必须在本次交易中全部流出(流出并不意味着成为其他Tx的In,而是必须成为一个TxOut。)

例如:如果A给B转了100,但是现在A可以控制2个Out,一个是Out1是60,另一个是Out2是50,那么A检查自己的时候会发现60和50都不够Out 100,则只能使用 Out1 和 Out2 作为当前要生产的 Tx 的 In。但是在这种情况下,所有Ins的总和大于100要花费。那么,如果没有支付交易费用,不包括当前Tx对应的100转给B的Out,就会多出10。在比特币中,必须为额外的10个区块创建一个Out来锁定这些10 个区块,这样每个交易的 In 和 Out 的总数必须相同。那么因为这10个相当于我们通俗意义上的“变变”,所以Out的10个区块的锁当然是A可以控制的锁,

所以我们可以看到一笔交易只包含一个输入和一个输出,那么这个交易就不算是从一个人到另一个人的转账,而是像水一样的货币流动,从某个本地流入到这个的输入交易,并从这个交易的输出到另一个地方。那么接下来的问题就变得显而易见了——如何控制资金流向?答案是 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“from where”中包含的(前一个)Tx就是这个Tx的前一个Tx的Out的指针。这个 Tx 不能容纳前一个 Tx 的 out,所以它使用 Point 指针来记录。

nSequence 在 v0.1 中没有任何作用,也不会用于验证,但该字段以后会用于其他用途,是比特币软分叉的最好例子。

对于 CTxOut:

value 是记录“从这个出口流出多少”的信息。简单来说,可以理解为通俗意义上的转移。但我们这里还是要强调,对比特币的第一个理解是抛开支付交易等概念,而把比特币当做流水,这里的价值是记录有多少比特币会从这里流出。显然,一个Tx的所有TxOut的值之和应该等于所有TxIn流入的总和(不考虑手续费,手续费小于等于弱对价),否则应考虑交易非法(你不能凭空花更多的钱)。

scriptSig/scriptPubkey

然后不介绍的scriptSig和scriptPubkey是控制“为什么从这里流”的机制。这件作品绝对是中本聪创造比特币的又一惊人发明。这两个属性就是未来著名的“智能合约”的雏形。以后会花一篇详细的文章来详细介绍。这里我们简要介绍以下内容:

我们从刚才的讨论中知道,比特币是从一笔交易流向另一笔交易的,而且是这样进行的。但这显然是行不通的,因为没有人声明这个“流”的所有权。也就是说,我们在日常交易中使用的是100元,核心是100元纸币从一个人的手流到另一个人的手上。但是当你持有 100 美元钞票时,你确认了 100 美元钞票流的所有权。

但是在比特币系统中,请直接放弃这个想法,换一种思维方式,这样换个角度看,和交易100美元大钞是一样的。

这个想法是,当我们重新审视交易时,我们看到资金流动是一个从一个(多个)交易到一个(多个)交易的过程。那么如果我们有一个独特的手段来控制它为什么可以流动,例如我们采取一种手段,当它流出时将出口锁在外面,当你想控制这个出口的流出时,你创建一个可以打开这个锁作为下一个交易的入口。也就是说,我们不断的看2笔交易的中间部分:上一笔交易的out和下一笔交易的in。如果我们可以在前一个out上加一个锁,然后规定后一个in成立的条件是in所附的钥匙可以打开out锁。(维护为什么可以解锁锁的过程由矿工保证。

这笔钱虽然不像现实生活中的100块钱,但只能通过进出锁来控制钱的流向。但是换个方向想想,虽然我们只能提供这个“钥匙”,但是这个“钥匙和锁”可以控制out里面包含的币流比特币项目开源代码,那么这个in/out的加锁和解锁机制是不是就相当于你拥有了呢?这笔钱?(因为虽然这笔钱并不是真的在你手里(比如银行账户有你的账户而比特币系统没有),但是你可以控制锁定在你锁里的钱的一些流动权,那么它就像水流一样 只有你可以解锁分叉点的出口,虽然别人可以看到,但是因为别人无法解锁,所以别人也无能为力,因为他们可以'

所以我们可以看到,我们所谓的转账是在比特币系统中。比如A给B转账100,那么B需要向A提供一些信息(比特币地址)。这些信息不会透露B的个人情况,但可以是表面的。B 可以控制由该信息创建的锁。然后A可以创建一个事务,这个事务的out可以使用B提供的信息来锁一个只有B可以控制的锁,然后这个事务的in由A提供。对应其他事务的Out那A可以控制键。如下所示:

好吧,经过上面这么长的陈述,我们终于可以提出CTxIn和CTxOut的scriptSig和scriptPubkey属性就是我们刚才讨论的钥匙和锁了。scriptSig 是用于对应签名的密钥,scriptPubkey 是 B 提供地址生成的锁。

而我们所说的实现钥匙和锁的功能取决于这两个属性的类型 -> CScript

从命名中可以看出,中本聪在设计之初就认为具有这样功能的东西应该像“脚本”一样“执行”。熟悉计算机的人看到脚本的名字就可以想象这种机制可以“编程”。在比特币系统中也是如此。比特币提供了一系列操作指令,允许用户自行编程。验证过程实际上就是脚本的执行。这里就不做太多描述了,后面会有文章详细描述比特币的脚本系统。

输出点

这个类有两个属性

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

根据上面的解释,我们可以得到,这里的hash指的是txin来自的Tx的hash,n指的是之前交易的第n个out的in,如下图所示:

CinPoint

这个类在我们讨论比特币时不是很重要,这个类只出现在一个维护 COutPoint 和 CInPoint 的地图中。

所以我们认为 CInPoint 和 COutPoint 是键值对应。当我们确认一个 COutPoint 时,我们可以假装把这个 COutPoint 看成是上一个 Tx 的 Out,那么这个 map 对应的 CInPoint 就是上一个 Tx 的 Out 所指向的下一个点

它拥有的属性

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

CTransaction* 是 COutPoint 的事务,其中 In 指向 Out。那么在COutPoint图的例子中,就是指向当前Tx的指针,即Tx。而这里的n指的是这个In是当前Tx的第n个In,在上图中也是0(因为只有1个In)

脚本

CScript 实际上是一个向量,这意味着 Script 实际上是一个字节流。只是这个字节流可以被解析成或者这样一个一一的元信息。脚本是由这些元信息组成的字节流。

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

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

CTxIndex / CDiskTxPos

这两个类与比特币协议无关。它们用于 Tx 的本地存储和索引。不过这里需要注意的是,在比特币的源码中,CTxIndex是一个非常重要的类。它的存储、更新和删除控制着是否可以在本地存储中找到对应的Tx数据,以及Tx是否被花费。

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

在存储上,比特币使用 Tx 的哈希作为 key,CTxIndex 作为 value 进行存储。所以当你得到一个CTransaction(或其子类)时,你可以通过获取这个Tx的哈希索引的本地存储来得到这个Tx对应的TxIndex。

TxIndex 的属性 vSpent 是一个非常重要的属性,因为它关系到一个 Tx 的 Out 是否为 UTXO(Unspent Transaction Output)。从前面的讨论可以看出,那么一个UTXO就是一个被锁定但没有解锁的Out。而这个TxIndex的vSpent是一个向量,对应的是当前Tx的vout。

这里我们要强调的是,Tx的生成和确认不是同一个决定,而是前面讨论的Client和Server。生成 Tx 的称为客户端,接受有效的 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是第三个Out,但是A不管,还是用这个用过的Out。那么当其他人收到这个Tx进行验证时,就会检查自己的本地存储对应的Tx_prev的Tx_index_prev,然后检查vSpent[3]是否为null。如果为空,则合法。如果不是Null,那么就说明这个Out已经用完了。可以看出,这里的验证与A的本地存储无关,A不可能修改自己的本地存储来欺骗别人。因为传输的内容只有Tx,每个节点根据收到的 Tx 或区块生成 TxIndex。因此,节点一旦检查发现 vSpent[3] 不为空,就会认为 A 的 Tx 是非法的。

而 CDiskTxPos 表示这个 Tx 在本地存储的位置。在比特币源码中,Tx的存储是紧密排列在文件中的,要找到这个Tx就是先找到存储的文件,然后再找到这个Tx在这个文件中的偏移量。所以nFile和nTxPos分别代表了哪个文件和文件中的偏移位置。

nBlockPos 表示该 Tx 在 Block 中的位置。

CMerkleTx

该类是Tx的子类,该类用于Block中的相关处理。CMerkleTx 是矿工(上面提到的服务器)保存 Tx 时的相关类

在原Tx的基础上增加

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

3个属性,hashBlock表示当前Tx所在Block的hash(作为索引),vMerkleBranch是Merkle树中Tx配对的所有hash值(这个配对的hash值会在后面的文章中解释),这里是用于验证块中 Tx 的附加信息。index 表示 Tx 在块中的位置。

CWalletTx

该类是CMerkleTx的子类,实际上我们生成Tx和与钱包相关的Tx。这里重点介绍Tx,以及钱包信息和生成过程,这里暂不介绍。

结尾

以上是对比特币中Tx相关类的介绍。我们只能先解释一下在比特币中实现Tx的过程中用到了哪些类,以及类中属性的可能作用。在下一篇文章中,我将描述 Tx 的工作原理。那么关于Tx的文章就是看源码是如何按照原理处理的。