用GO语言构建区块链——第四部分:交易1

 前言
      交易是比特币的核心,区块链的唯一目的是以安全可靠的方式存储交易,因此没有人可以在创建交易后对其进行修改。今天我们开始实施交易。但是因为这是一个相当大的话题,我将它分为两部分:在第一部分,我们将实现交易的一般机制;在第二部分,我们将通过细节进行处理。
此外,由于代码更改很大,因此在这里描述所有代码是没有意义的。您可以在此处查看所有更改。

      如果您曾经开发过Web应用程序,那么为了实现付款,您可能会在数据库中创建这些表:accounts和transactions。帐户将存储关于用户的信息,包括他们的个人信息和余额,并且交易将存储关于从一个帐户转移到另一个帐户的钱的信息。在比特币中,支付以完全不同的方式实现。有:
      1. 没有帐户。
      2. 没有余额。
      3. 没有地址。
      4. 没有硬币。
      5. 没有发件人和收件人。

由于区块链是公共和开放式数据库,因此我们不希望存储有关钱包所有者的敏感信息。帐户中不会收集硬币。交易不会将钱从一个地址转移到另一个地址。没有包含帐户余额的字段或属性。只有交易,但交易中有什么?

比特币交易

交易是输入和输出的组合:

用GO语言构建区块链——第四部分:交易1

输入前一个事务的新事务引用输出(虽然有例外,我们稍后会讨论)。输出是硬币实际存储的地方。下图演示了事务的互连:

用GO语言构建区块链——第四部分:交易1       请注意:
      1. 有些输出与输入无关。
      2. 在一个事务中,输入可以引用多个事务的输出。
      3. 输入必须引用输出。

在整篇文章中,我们将使用“钱”,“硬币”,“花”,“发送”,“帐户”等词语。但比特币中没有这样的概念。事务只是用脚本锁定值,只能由锁定它们的人解锁。

 交易输出

让我们先从输出开始:

用GO语言构建区块链——第四部分:交易1

实际上,它是存储“硬币”的输出(注意Value上面的字段)。并且存储意味着用难题锁定它们,难题存储在ScriptPubKey中。在内部,比特币使用一种名为Script的脚本语言,用于定义输出锁定和解锁逻辑。这种语言非常原始(这是故意制作的,以避免可能的黑客攻击和滥用),但我们不会详细讨论它。你可以在这里找到它的详细解释。
在比特币中,值字段存储satoshis的数量,而不是BTC的数量。一个satoshi 是一百万分之1的一个比特币(0.00000001 BTC),因此,这是货币的比特币的最小单位(如百分比)。
由于我们没有实现地址,因此我们现在将避免使用与脚本相关的整个逻辑。ScriptPubKey将存储任意字符串(用户定义的钱包地址)。
同样拥有这样的脚本语言意味着比特币也可以用作智能合约平台。

关于输出的一个重要点是它们是不可分割的,这意味着你不能引用它的一部分价值。在新事务中引用输出时,它将作为一个整体使用。如果其值大于要求,则会生成更改并将其发送回发件人。这类似于现实世界的情况,当你支付5美元的钞票,购买价格为1美元物品时你会得到4美元的找零。

交易输入

这是输入:

用GO语言构建区块链——第四部分:交易1

如前所述,输入引用先前的输出:Txid存储此类事务的ID,并Vout在事务中存储输出的索引。ScriptSig是一个脚本,提供要在输出中使用的数据ScriptPubKey。如果数据正确,则可以解锁输出,并且可以使用其值来生成新输出; 如果不正确,则无法在输入中引用输出。这是保证用户不能花费属于其他人的硬币的机制。
同样,由于我们尚未实现地址,ScriptSig因此将仅存储任意用户定义的钱包地址。我们将在下一篇文章中实现公钥和签名检查。
让我们总结一下:输出是存储“硬币”的地方。每个输出都带有一个解锁脚本,它确定解锁输出的逻辑。每个新事务必须至少有一个输入和输出。输入引用先前事务的输出,并提供ScriptSig在输出的解锁脚本中使用的数据(字段)以解锁它并使用其值来创建新输出。

但首先是:输入还是输出?

先有输出
     在比特币中,是先有输出,再有输入。该逻辑可以参考经典的“鸡或蛋”问题:输入产生输出和输出使输入成为可能。在比特币中,输出在输入之前。
     当矿工开始挖掘一个区块时,它会向其添加一个coinbase交易。coinbase事务是一种特殊类型的事务,不需要以前存在的输出。它无处不在地创造输出(即“硬币”),这是矿工开采新区块的奖励。
      如你所知,区块链开头就有创世块。正是这个块在区块链中生成了第一个输出。并且不需要先前的输出,因为没有先前的交易就没有这样的输出。
      让我们创建一个coinbase交易:

用GO语言构建区块链——第四部分:交易1

coinbase交易只有一个输入。在我们的实现中,它Txid是空的,Vout等于-1。此外,coinbase事务不会存储脚本ScriptSig。相反,任意数据存储在那里。
     在比特币中,第一个投币基础交易包含以下信息:“The Times 03/Jan/2009 Chancellor on brink of second bailout for banks”。
     subsidy是奖励金额。在比特币中,此数字不存储在任何地方,仅基于块的总数计算:块的数量除以210000就是 subsidy。挖掘生成区块产生50 BTC,每挖出210000区块后奖励减半。在我们的实施中,我们将奖励存储为常数(至少现在为止是)。

      在区块链中存储交易
      从现在开始,每个块必须存储至少一个交易,并且不可能在没有交易的情况下挖掘块。这意味着我们应该删除Block中Data字段,在进行存储交易:

用GO语言构建区块链——第四部分:交易1

NewBlock并且NewGenesisBlock还必须相应地改变:

用GO语言构建区块链——第四部分:交易1

接下来要改变的是创建一个新的区块链:

用GO语言构建区块链——第四部分:交易1

现在,该函数获取一个地址,该地址将获得挖掘生成块的奖励。

     工作量证明算法

工作量证明算法必须考虑存储在块中的事务,以保证区块链作为事务存储的一致性和可靠性。所以现在我们必须修改ProofOfWork.prepareData方法:

用GO语言构建区块链——第四部分:交易1

而不是之前使用的pow.block.Data,现在我们使用的是pow.block.HashTransactions():

用GO语言构建区块链——第四部分:交易1
      我们使用哈希提供数据的唯一表示,这个之前也遇到过。我们想要通过仅仅一个哈希,就可以识别一个块里面的所有交易。为此,我们获得每笔交易的哈希,将它们关联起来,然后获得一个连接后的组合哈希。

      比特币使用更复杂的技术:它将包含在块中的所有事务表示为Merklet  tree,并在工作证明系统中使用树的根哈希(root hash)。此方法允许快速检查块是否包含特定事务,仅具有根哈希并且不下载所有事务。
      我们到目前为止检查一切是否正确:

用GO语言构建区块链——第四部分:交易1

好!我们收到了第一次采矿奖励。但是我们如何检查余额呢?

未花费的交易输出
     我们需要找到所有未使用的事务输出(UTXO)。未使用意味着这些输出未在任何输入中引用。在上图中,这些是:
     1. tx0,输出1;
     2. tx1,输出0;
     3. tx3,输出0;
     4. tx4,输出0。
     当然,当我们检查余额时,我们不需要所有知道这些,只需要关注那些可以通过我们拥有的密钥解锁的UTXO(目前我们没有实现密钥,而是使用用户定义的地址)。首先,让我们在输入和输出上定义锁定解锁方法:

用GO语言构建区块链——第四部分:交易1

这里我们只是比较脚本字段unlockingData。在我们基于私钥实现地址之后,将在以后的文章中对这些部分进行改进。
     下一步——查找包含未使用输出的事务非常困难:

用GO语言构建区块链——第四部分:交易1

由于事务存储在块中,我们必须检查区块链中的每个块。让我们从输出开始:

用GO语言构建区块链——第四部分:交易1

如果输出被同一地址锁定,我们正在搜索未使用的事务输出,那么这就是我们想要的输出。但在采用之前,我们需要检查输入中是否已引用输出:

用GO语言构建区块链——第四部分:交易1

我们跳过在输入中引用的那些(它们的值被移动到其他输出,因此我们无法计算它们)。在检查输出后,我们收集所有可以解锁使用提供的地址锁定的输出的输入(这不适用于coinbase事务,因为它们不解锁输出):

用GO语言构建区块链——第四部分:交易1

该函数返回包含未使用输出的事务列表。为了计算余额,我们需要另外一个函数来获取事务并仅返回输出:

用GO语言构建区块链——第四部分:交易1

就是这么多了!现在我们可以实现getbalance命令:

用GO语言构建区块链——第四部分:交易1
      帐户余额是帐户地址锁定的所有未使用的交易输出的值的总和。
      在挖掘创世块之后让我们检查一下我们的余额:

用GO语言构建区块链——第四部分:交易1

这是我们的第一笔钱!

发送硬币
     现在,我们要向其他人发送一些硬币。为此,我们需要创建一个新交易,将其放在一个块中,并挖掘该块。到目前为止,我们只实现了coinbase交易(这是一种特殊类型的交易),现在我们需要一个通用的交易:

用GO语言构建区块链——第四部分:交易1

在创建新输出之前,我们首先必须找到所有未使用的输出,并确保它们存储足够的值,这是FindSpendableOutputs方法的要求。之后,对于每个找到的输出,创建引用它的输入。接下来,我们创建两个输出:
      1. 一个与接收者地址锁定的,这是将硬币实际转移到其他地址。
      2. 一个与发件人地址锁定的。这是一个变化,它仅在未使用的输出保存的值超过新事务所需的值时创建。请记住:输出是不可再分的。
      FindSpendableOutputs方法基于FindUnspentTransactions方法:

用GO语言构建区块链——第四部分:交易1

该方法迭代所有未花费的事务并累积其值。当累计值大于或等于我们想要转移的金额时,它会停止并返回按交易ID分组的累计值和输出索引,我们不想超支。
     现在我们可以修改Blockchain.MineBlock方法:

用GO语言构建区块链——第四部分:交易1

最后,让我们实现send命令:

用GO语言构建区块链——第四部分:交易1

发送硬币意味着创建交易并通过挖掘块将其添加到区块链。但比特币不会立即这样做(就像我们一样)。相反,它将所有新交易放入内存池(或mempool),当矿工准备挖掘块时,它会从内存池中获取所有事务并创建候选块,仅当包含它们的块被挖掘并添加到区块链时,才会确认交易。
让我们检查发送硬币是否有效:

用GO语言构建区块链——第四部分:交易1

太好了!现在,让我们创建更多交易,并确保从多个输出发送工作正常:

用GO语言构建区块链——第四部分:交易1

现在,Helen的硬币锁定在两个输出中:一个来自Pedro,一个来自Ivan。让我们把它们发给别人:

用GO语言构建区块链——第四部分:交易1

看起来很好!现在让我们测试会出现失败的情况:

用GO语言构建区块链——第四部分:交易1


结论

即便这不容易,但我们现在有交易!不过缺少如同比特币这类加密货币的一些关键特征:
     1. 地址。我们还没有真正的基于私钥的地址。
     2. 奖励。采矿区绝对没有利润!
     3. UTXO设置。获得平衡需要扫描整个区块链,这可能需要很长时间才能有很多块。此外,如果我们想要验证以后的交易,可能需要很长时间。UTXO集旨在解决这些问题,并使交易快速进行。
     4. 内存池。这是在块打包之前存储事务的地方。在我们当前的实现中,块只包含一个交易,这是非常低效的。

 编辑:却原来

本文系转载自其它媒体或授权刊载,目的在于信息传递,并不代表本站赞同其观点和对其真实性负责,如有新闻稿件和图片作品的内容、版权以及其它问题的,请联系我们。