bitcoin 源码解析 – 交易 Transaction(二) – 原理篇

作者:知识天地

时间:2018年1月14日

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

这篇文章我断断续续写了呃···· 应该快三个星期了? 所以前后的风格可能差别相当大。真是十分的怠惰啊··· 最近实在是不够努力。用python重写bitcoin的项目也卡在网络编程部分(这方面真是我的软肋)

这篇文章通篇都是文字-_-, 没有其他东西,这个样子给读者会造成很大的压力吧····

虽然题目所说的是原理,但是实际上一部分原理已经在前面几篇文章都有过一些零散的说明了,感觉写出来又有点重复。。所以最好先读过前面几篇可能看起来更好点。。等我把所有的东西都写完后应该会重新整理,然后重新写一份更可读的吧(这是个flag)

该篇将会详细阐述 Bitcoin 的交易本质。同样,在本篇中不探讨区块,只讨论 Tx 在整个 bitcoin 系统中是如何运作的。本篇所用的术语承接于上一篇文章,并直接使用上一篇文章讨论的细节。

在整个bitcoin 的源码中,尤为重要的文件只有main.cpp/.h (还有script.cpp/.h)这个文件,其他部分都属于附属功能。在main.cpp/.h文件中,包含了在后续版本中称为“core”的代码。

我们都知道,bitcoin 系统中有2个核心概念,Tx和 Block。但是这两个概念联系的相当紧密,甚至难以分割。但是当我们单纯的说Tx的时候,下文会回避一些和Block相关的说明。

在bitcoin 系统中,所谓的交易应该包含2个过程:

产生交易->交易被(矿工)验证

我们可以看到在这个流程中,关联的人应该有3个:转账人,收款人,矿工(们)

接下来我们分别介绍其中运作的原理,并通过这两个流程作为切入点,展开我对bitcoin交易的理解。

Transaction 的产生

现在我们概要性的描述一个交易产生的流程:

A 想要转账 1 btc 给 B,那么流程如下(这部分代码实现位于 main.cpp CreateTransaction() 这个函数中):

B 首先通过某种途径告诉了A自己的比特币地址(关于地址的介绍在其他文章中会详细描述)

然后A根据B给出的地址,并且附加上一些其他信息(非必要) 产生了scriptPubkey,这个scriptPubkey 就是前一篇文中所提到的“锁”。然后,根据要转账的金额 value, A就开始产生一个交易了:

  • 首先A检查属于自己的所有CWalletTx, 实际上检查的是 mapWallet 这个全局变量:
    • WalletTx是一般指代Out指向自己的Tx,而Out指向自己实际是在说,这个Out的 scriptPubkey 脚本中其中的地址是由自己本地所持有的公钥生成的。也就是说自己持有的所有的公钥对应的地址,只要Out的scriptPubkey的地址在自己的公钥地址库里,那么这个Tx就是 IsMine() 的 (不过位于WalletTx的还包含自己提起的Tx)
    • 通过一定的方式随机选择Out,直到被选择的Out所包含的Value之和大于等于需要转账的Value。在bitcoin中,我们把每个 Out 能够提供的 value 称为 credit
  • 然后根据选择的Out所在的Tx开始创建Tx
    • 首先根据 B 提供的地址生成的 scriptPubkey 和 转账的 value 生成一个 TxOut 填充到 新创建交易的 vOut[0] 中(在上文中已经说到,一个交易是由 n 个 TxIn 和 n 个 TxOut 构成,而这里表示 代表真正转账的Out 填充到 第 0 个 TxOut 中)
    • 被选择的Out构成的credit 之和如果是大于 需要转账的value,那么就意味着这笔交易结束后必须会有找零。由于bitcoin采用了 “UTXO” 机制,所以被选择的所有 Out 应该全部被“花费”掉,但是此时被选择的Out之和是大于需要转账的value的,所以必定存在差值。在这里,bitcoin 就是再创建了一个 Out,但是这个新建的Out的 scriptPubkey 使用的是自己的bitcoin地址(这个地址在0.1源码中是从被选择的Out中选择第一个Out提取出自己的bitcoin地址(因为这些Out都是指向自己的,既然指向自己,那么肯定是自己能控制的bitcoin地址),当然也可以选择创建一个新的密钥生成地址),而这个新建的Out的 value 就是 credit之和 减去 要转账的value, 也就是找零。之后就把这个新创立的Out填充到下一个TxOut的list中。(就是 vOut[1])
    • 然后就根据被选择的所有Out来创立这个 Tx 的TxIn list(原因见上文)。这里就是简单的使用这个TxOut所在的Tx的hash 和这个Out位于这个Tx的index(第几个)作为参数构建 TxIn,并且把这个 TxIn 添加到 TxIn list 当中。
    • 构建完TxIn List 后就需要生对这些 TxIn 的scriptSig (也就是提供能操作这些Out的钥匙,见上文),这里是如何完成签名的在之后关于script的文章会详细描述,总之这里可以先理解为 因为这些TxIn 是由 指向自己的 TxOut 构成的,这些TxOut 的scriptPubkey 是由自己所持有的公钥生成的地址构成,所以自己可以提供这些公钥对应的私钥来生成签名(Solver函数),这些签名就是散文提到过的“钥匙”(但是请注意,这里生成的签名到底能不能用,这个在创建这个Tx的时候别人是管不着的,这就是bitcoin精彩的地方。而维护这些签名是不是合法的,就是依靠广大的矿工,这在下一个流程中会描述。)
  • 此时一个交易实际上就被创立好了,根据bitcoin的规则,这个Tx的交易费用实际上是由这个Tx的大小(大小很大部分是由参与的TxIn 以及脚本的大小组成)决定的,所以这里要重新验证附加上交易费后有没有超过自己能提供的credit之和,如果超过了,那么只能把附加上交易费的总费用重新作为转账的value,并跳到第一步重新开始计算。
  • 在一笔交易真正成立之后,之后是一些扫尾工作,例如填充构成这个交易应该有的前置交易(AddSupportingTransactions),修改本地的一些存储信息(CommitTransactionSpent),在修改本地的存储信息中有一点很关键,就是标记该交易是已被花费过的。注意这里的标记是和CWalletTx相绑定的,并且标记的是当前的这个新产生的交易的TxIn所关联的交易。因为我们一般都认为在一个交易中一个参与者只应该提供一个地址,所以对于这个交易者来说,CWalletTx的fSpend标记可以代表这个交易对于该交易者的Out有没有有被花费(也就是说fSpend是针对该交易者的),之后在检索的时候可以节省很多。

Transaction 的验证

现在描述一个Tx的验证过程。请注意,对于 Tx 的验证重点并不是交易的发起人对Tx进行验证,而是需要广大的矿工对该笔Tx进行验证,也就是我们说的狭义上的关于Tx的共识。本人对Tx的验证并没有什么用,只有整个系统中的大部分人都认同这个Tx的时候,这个Tx才算是成立。(准确的说应该是当Tx打包入Block,这个Block被广播后大多数节点认同他并添加到最长链上那么这个Tx才算是真正的成立,但是本文先不讨论block)。但是请注意,从这部分开始,在讨论的过程中请认清,对于发起交易的交易者A,和对这笔交易认证的广大矿工,分别是依据从哪里获得的信息进行验证的。

好我们继续上一节假设的场景,A转账了1 btc 给B,A 在检查了自己过去的Tx后依据自己保存的信息并认为这些信息合法后,创建了一个新的Tx,并将其通过某种方式进行广播(该部分会在网络相关的文中进行描述)。但矿工收到了这个Tx那么验证就可以开始了(接下来我们省略的主语都是矿工):

验证的部分位于 CTransaction::AcceptTransaction() 函数中。首先先明确一点,在bitcoin系统中,无论查询什么东西,都是基于这个东西的hash,也就是说我们一般认为一个hash是这个对象(实例,实体)的索引id

  • 首先根据收到的信息的hash查询自己的Tx的MemoryPool(是mapTransaction这个全局变量)和本地存储看是否已经存在了这个Tx,如果已经存在了那么就跳过认证过程。
  • 检查待认证交易的所有TxIn持有的Tx的指针(概念上的指针,这里说的是COutPoint,就是这个TxIn是来自哪个Tx的),如果这个指针已经在自己的一个缓存中(mapNextTx,使用COutPoint作为key,value是这个TxIn(之前已经出现过的)被包含的那个Tx(代表已经有Tx或收到同一个Tx)),先检查已在缓存的Tx和当前收到的这个Tx哪一个“更新”,更新的话就保留,否则就退出验证
  • 检查收到的这个交易的TxIn提供的信息是否能和自己本地存储的已有的Tx信息匹配:
    • 如果是coinbase则以下步骤全部跳过
    • TxIn对应的Tx(TxIndex)是否在本地存在
    • 简单检查TxIn持有的prevout 和 Tx 对应的 out的基本信息是否相同
    • 检查 TxIn 提供的 signature script 是否符合 存储在本地的Tx对应的TxOut的 public script
    • 检查 TxIn 对应的 Tx 存储在本地的 TxIndex 的对应的Out的部分(vSpent[prevout.n]) 是否是已被标记过的(标记过就是代表这个out已经被花费了,注意这里的已花费和CWalletTx的spent有点不一样),如果是未标记过则这个 TxIn 是合法的,并在下面进行标记,若是已被标记过的那就是错误的。
    • 如果以上都是正确的,这里不进行存储,存储放在区块打包部分
  • 至此这个交易已被验证通过
  • 将交易放入内存池里等待打包
  • 进行一些扫尾工作。

接下来这个Tx就存在于该矿工的内存池中了,这个矿工 可能会义务的 帮你把这个验证正确的交易广播给更多的人,其他矿工接收到也会进行在本地进行相同的验证流程并可能广播给更多的人。之后就等待这个矿工把这个交易打包进入区块,当这个区块被大部分矿工承认后这个交易就生效了。

简单分析

上文的那两部分中我们详细的介绍了Tx的产生和验证,实际上真正的流程比上述复杂的多,有很多分支的处理。。。但是即便如此,上面所述若不对照相应的代码,估计任何初始者看了都是一头雾水。我只是把我认为比较重要的部分用黑体标识出来。其中我用黑体标识出的部分重点在于突出一个Tx到底是怎么在一个分布式环境中,即便各个节点是互不信任的,仍然可以接受别人产生的交易。在上面的那两个过程中:

  • Tx 的产生 对应的是 交易发起者
  • Tx 的验证 对应的是 承认交易的过程。

若按照传统的流程(网上银行模型),这两个过程应该如下:ps(关于中央铸币节点的概念参考比特币白皮书的文章)

  • 交易发起者向中央铸币节点(如银行)获取自己的账号里的余额(非必须)->交易发起者获得交易接受者的相关信息(如对方的银行卡号)->交易发起者将转账金额和对方的信息告诉中央铸币节点
  • 中央铸币节点获取发起者的信息->检查发起者的账户余额->判断转账金额是否在余额之下->销毁发起者账户需要转账的金额(减少发起者账户的资产)->铸造给接收者账户接受转账的金额(增加接收者账户相同的资产)->销毁和铸造构成了一个“事务处理”

而在bitcoin的转账的整体流程中,这两个过程是不一样的,其中的交易发起者和交易接收者的角度是一致的,但是验证交易的角色由原来的“中央铸币节点”转变成为了“所有的矿工及接受包含这个Tx的区块的所有人”

  • 交易发起者检查自己的本地存储的信息(Wallet)获得自己能够使用的总资产(非必须)->交易发起者获得交易接收者的相关信息(比特币地址)->经过本地的检查后(非必须)产生一个交易Tx并进行广播
  • 矿工(们)收到交易-> 根据自己的本地存储信息(所有的TxIndex)用于校验收到信息的 TxIn 对应的历史交易信息是否吻合,最主要包含两方面
    • TxIn 对应的 Tx 的 Out 是否是 未被花费过的 (UTXO)
    • TxIn 对应的 Tx 的 Out 的 pubkey_script 是否能被 TxIn 提供的 sig_script 所验证成功

    -> 若校验成功则放入内存池等待打包进区块

    ====

    -> 当打包进入区块后,修改自己的本地存储(TxIndex) -> 把这个区块广播给更多的人

    ==== (区块相关部分,以后会详细讲解)

    ->(其他矿工)若收到的区块是最长链->对这个区块进行验证->验证成功就保存下来(别人的历史记录)->剔除在内存池中相关的Tx->中断自己的打包区块过程->重新打包剩下内存池中的Tx

其中最后一点提一下:如果一个矿工内存池中的Tx不多,或者他有选择的接受一些Tx而剔除一些Tx(比如给的交易费不够高),如果他很强大,能够一直获得打包区块的记账权,那么很可能有一些交易就永远都无法被打包,即便其他矿工打包了你的交易,但是只要这个这些其他矿工抢不过这个很厉害的矿工,那么你的交易仍然是无法生效的,因为“没有被记录进历史记录当中”,你想根据这个交易产生剩下的交易也是不行的。

总结

经过上面的分析,从传统的方式和bitcoin的方式的比较可以得出,至少在我的观点中,最大的区别在于:

校验历史记录的这个过程被转移到了哪里

因为在保障交易流程的正确性中,关键点在于一个交易能够成功,是要依托于以前的交易是否是正确的,如:

  • 在传统交易中,要检查用户的账户余额是满足转账金额的
  • 在bitcoin中,要检查Tx使用的TxIn 是否是已被花费过及是否是这个交易的发起者能够控制的(pubkey_script, sig_script的配对)

而传统交易和 bitcoin 是有相当显著的区别的:

  • 传统交易必须依托于 中心节点 ,绝对不可能绕过中心产生交易,否则这笔交易是不会被承认的
  • bitcoin 交易的校验历史记录 完全依托于个人存储的历史记录 ,毫无例外,无论是个人还是矿工

这就回到在分析章节提出的问题:

一个Tx到底是怎么在一个分布式环境中,即便各个节点是互不信任的,仍然可以接受别人产生的交易。

这个问题的答案很简单:

因为节点是互不信任的,所以他们信任的是自己,节点只相信自己的历史记录,只会根据自己的历史记录做出判断。那么问题就来了:自己的历史记录到底是对还是错呢?(这里的对错是相对的含义,应该说是不是和大部分人一致)

这个问题就涉及到了bitcoin的最高原则了,只认同工作量最长链为唯一公认的bitcoin链。

那么剩下的问题就会在block的部分继续进行分析。

相关文章:

发表评论