主页 > imtoken多签钱包 > 比特币源码分析——交易Transcation(一)

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

imtoken多签钱包 2023-05-17 06:57:10

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

在设计比特币或者类似的分布式系统的时候,会有一个地方和普通的设计很不一样:

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

因此,在分布式系统的设计中,在使用类来描述对象时,有时需要区分该类何时作为客户端使用,何时作为服务端从客户端接收的类使用。 因为客户端和服务端运行的是同一套代码,而在实际运行过程中,如果按照C/S模型来看(其实不应该这样看,应该换个思路到设计p2p节点的思路),那么你会看到C产生的Tx/Block和S接收到的Tx/Block。所以这时候需要区分类中哪些属性属于哪些属性由客户端/服务器平等对待,并根据情况使用哪些属性。 否则很容易在看源码的过程中失去自己的位置。

本章介绍比特币源码中交易的总体概况

本文仅介绍源码中结合了哪些类型的比特币交易,做一个大概的介绍。 至于交易的原理,我们会在下一篇文章中详细介绍。 本文会提前介绍比特币的一些概念,但目前没必要知道它指的是什么,先了解有这么个东西就可以了。

莱特币和比特币代码_比特币源代码编译_比特币股市代码是什么

如上图类图所示比特币源代码编译,这个UML图包含了比特币交易相关的所有关键类。

C事务

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

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

vector vin;
vector vout;

这两个关键成员变量。 这两个成员变量分别代表比特币交易的“收入”和“支出”。 比特币交易不记录账户型的数据变化(比如我们用银行模型来描述一笔100元从A到B的转账,那么银行在记录转账的过程中会有3条记录比特币源代码编译,而这3条A Transaction(交易)流程:A账户减100元,记录id为tid1,B账户增加100元,记录id为tid2,一条转账记录记录tid1向tid2转账100元,成为A账户减少和B账户增加之间的“关系连接”。),但是日志形:比特币的Tx只记录了A向B转账的“关系连接”,而这个日志记录只包括A的转账100元给乙发了这条信息。 而这里的in是记录谁从“谁”传来的(目前简单这么看,其实不止于此,后面会重新说明),out是传给谁, 转了多少钱包含在 In out 中。 中本聪的命名风格是用前缀来表示这个属性的类型,如果是flag就加一个f。 所以这里的vin/vout指的是in和out都是vector类型,所以这里我们可以看到一个Tx可以有多个in/out。 下面我们调用in为TxIn,out为TxOut(注意,把in和out比作两个人是完全不合适的,后面会重新描述)

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

int nVersion;
int nTimeLock;

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

比特币股市代码是什么_比特币源代码编译_莱特币和比特币代码

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

CTx输入/CTx输出

从这一点开始,我们直接抛弃了“两人交易”的概念,直接认为比特币交易系统中不存在“所有人”的概念(这一定很奇怪,因为没有人是什么人)币的含义,后面会解释),只是把“交易”当成“比特币流”的中转节点,就像水流分叉和汇合的那些节点:

典型的比特币交易链:(来自Developer Guide - 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个。在比特币中,为了多出来的10个区块,强制创建一个Out来锁定这10个区块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不能hold住之前Tx的out,所以用Point指针来记录。

nSequence在v0.1中没有任何作用,也不会用于验证,但这个字段在未来被用于其他用途,成为比特币软分叉的最好例子。

对于 CTxOut:

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

scriptSig / scriptPubkey

那么没有引入的scriptSig和scriptPubkey就是控制“为什么从这里流出”的机制。 这绝对是创造比特币的中本聪的又一惊人发明。 这两个属性就是后来大名鼎鼎的“智能合约”的雏形。 以后会有详细的文章介绍。 在此我们简单介绍如下:

我们在刚才的讨论中看到,比特币是从交易流到交易,等等。 但这显然是不能接受的,因为没有人声明过这个“流量”的归属。 也就是说,我们在日常交易中使用100元,核心是因为100张钞票从一个人手里流到另一个人手里。 但是当你持有这100张钞票的时候,你就确认了这100张钞票流的所有权。

但是在比特币系统中,请直接放弃这种想法,换一种方式去想,这样换个角度看,就跟交易100张纸币是一样的。

这种思路是,当我们重新审视交易时,发现货币流转是从一笔(多笔)交易流向一笔(多笔)交易的过程。 那么如果我们有一个独特的手段来控制它为什么可以流出,比如我们采取一个方法,在流出的时候把出口锁在外面,当你想控制流出这个出口的时候,你创建一个可以打开这把锁,作为下一笔交易的入场券。 也就是说,我们不断地看两笔交易的中间部分:上一笔交易的out和下一笔交易的in。 如果我们可以给前面的out加一把锁,然后规定后面的in成立的条件是in附带的钥匙可以打开out的锁。 (维护为什么锁可以解锁的过程是矿工保证的,现在只谈交易不谈区块,所以可以理解为一个自然规律。)那么因为加锁和解锁是“个人”行为,而是一个交易的流出是用一把只有专人才能打开的锁锁住的,所以就好像本该流出这个流出的币被“锁在了这个流出”(注:这个锁定过程不需要这个有一个特殊的人参与,这个特殊的人只需要提供一点信息来代表这个锁只能由他来打开(也就是地址)。 那么这种行为就相当于只有这个特殊的人才能“控制”这个Tx的Out,也就是只有这个特殊的人才能“占有”Out中的锁币。

虽然这笔钱不像现实生活中那样,你其实可以拿到手中的100块钱,但是你只能通过in/out lock来控制钱的流向。 但是我们换个方向想,虽然我们只能提供这个“钥匙”,但是这个“钥匙加锁”可以控制out里面包含的货币流向,那么这个in/out的加锁和解锁机制是不是就等同于你的占有呢? 这点钱? (因为虽然钱不是真的在你手里(比如银行账户有你的账户但是比特币系统没有),但是你可以控制一些被你的锁锁定的资金流向的权利,那么就是就像水的流动只有你能解锁分岔点的出口,虽然别人能看到,但因为别人无法解锁,那么别人也无能为力,因为他们无法控制)。

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

莱特币和比特币代码_比特币股市代码是什么_比特币源代码编译

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

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

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

比特币股市代码是什么_比特币源代码编译_莱特币和比特币代码

输出点

这个类包含两个属性

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

根据上面的解释,我们可以得到,这里的hash是指txin来自的Tx的hash,n是指从上一笔交易中出来的第n个,如下图:

比特币股市代码是什么_比特币源代码编译_莱特币和比特币代码

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

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

它拥有的属性

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

比特币股市代码是什么_莱特币和比特币代码_比特币源代码编译

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

脚本语言

CScript其实是一个vector,也就是说,Script其实是一个Bytes流。 只是这个字节流可以被解析成这样一个元信息。 而一个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的hash作为key,CTxIndex作为value进行存储。 所以在得到一个CTransaction(或者它的子类)之后,可以通过获取这个Tx的hash索引存储到本地,得到这个Tx对应的TxIndex。

比特币股市代码是什么_莱特币和比特币代码_比特币源代码编译

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

这里要强调的是,Tx的产生和确认不是由同一个decision决定的,而是由前面讨论的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就是第三个Out,但是A不在乎,还是用的是spent过的Out。 然后其他人收到这个Tx进行校验时,查看自己本地存储的Tx_prev对应的Tx_index_prev,然后查看vSpent[3]是否为null。 如果它为空,则它是合法的。 如果不为Null,则说明这个Out已经被花费了。 可见,这里的验证与A的本地存储无关,A也不可能通过修改自己的本地存储来欺骗他人。 因为传输的内容只有Tx,而TxIndex是各个节点根据收到的Tx或者block生成的。 因此,一旦节点检查发现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值会在a中解释)后面的文章),这里是用来验证区块中Tx的附加信息的。 index代表Tx在区块中的位置。

CWalletTX

这个类是CMerkleTx的子类,其实就是我们生成Tx和钱包相关的Tx。 这里重点介绍Tx、钱包信息和生成过程,这里暂不介绍。

结尾

以上就是对比特币中Tx相关类的介绍。 我们只能说明在比特币中实现Tx的过程中使用了哪些类,以及类中属性可能的作用。 在下一篇文章中我将介绍 Tx 是如何工作的。 而后续关于Tx的文章都是根据原理来看源码是如何处理的。