第一章:玩具版区块链
概述
区块链的基本概念非常简单:一个分布式数据库用以存储不断增长的有序记录列表。本章我们将实现区块链的玩具版。在本章末尾我们将实现以下区块链的基本功能:
定义好的区块及区块链结构
将携带任意数据的新区块添加到区块链上的方法
与其它区块链节点通信并同步的区块链节点
一个简单的HTTP API用来控制区块链节点
区块结构
我们将从定义区块结构开始,暂时只添加最重要的属性。
index: 区块在区块链中的索引
data:区块中包含的任意数据
timestamp: 时间戳
hash: 一个根据区块内容生成的sha256 hash值
previousHash:前一个区块的hash值。该值显式地定义了上一个区块。
快结构的代码如下所示:
区块哈希值
区块的hash值是该区块最重要的属性之一。hash值根据区块的所有数据计算得来,这意味着如果区块中的任何内容发生更改,原始hash值将失效。区块hash也可认为是该块的唯一标识符。例如,不同的区块的index值可能会相同,但是hash值是唯一的。
我们使用下面代码来计算hash值:
应该注意的是,区块hash和挖矿没有任何关系,因为这里没有“权益证明”问题需要解决。我们使用区块hash来保持区块的完整性并明确地引用上一个区块。
包含hash和previousHash的一个重要的结果是:除非改变每一个连续区块的hash,否则区块无法被修改。
下面例子证明了这一点。如果块44中的数据从 改变为 ,则必须改变连续块的所有hash。这是因为块的hash值取决于previousHash(和其他属性)。
在介绍权益证明概念时,这是一个非常重要的特性。区块在区块链中位置越深,修改它就越困难,因为它需要修改每个连续的区块。
初始区块
初始区块是区块链中的第一个区块。它是唯一没有previousHash的区块。我们将其硬编码在源码里:
生成区块
要生成一个区块,必须知道前一个区块的hash值,并创建其他需要的内容(index、hash、data和timestamp)。块数据由最终用户提供,但其余参数将使用以下代码生成:
存储区块链
目前我们只会使用内存中的JavaScript数组来存储区块链。这意味着当节点终止时数据不会被持久化。
验证区块的完整性
任何时候,我们必须能够验证块或区块链在完整性上是否有效。特别是当我们从其它节点接收新区块时,并且决定是否接受它们。
要使区块有效,必须满足以下条件:
区块的索引必须等于前一个区块的索引加1
区块的previousHash匹配前一个区块的hash
区块本身的hash必须有效
以下代码演示了这一点:
我们还必须验证区块的结构,以便 (译者注:网络中的其他节点,通常是一些区块网络中的机器实体)发送格式错误的内容不会导致我们的节点崩溃。
现在我们有了验证单个区块的方法,可以继续验证完整的区块链了。受限检查区块链中的第一个块是否与初始块匹配。之后我们使用之前描述的方法验证每个连续的块。如下图所示:
选择最长的区块链
在同一时间点,区块链中应该始终只有一组显示的区块。如果发生冲突(例如,两个节点都生成了区块72号),我们选择具有最长块数的链条。下面例子中,块72:a350235b00中引入的数据将不会包含在区块链中,因为它将被较长的链覆盖。
以下代码实现了这个逻辑:
和其他节点通信
节点的一个重要功能是与其它节点共享和同步区块链。以下规则用于保持网络同步。
当一个节点产生一个新块时,它将被广播到网络中
当一个节点连接到一个新的对等节点时,它将查询最新的区块
当一个节点遇到一个索引大于当前已知区块索引的区块时,它会将该块添加到当前区块链中,或者查询完整的区块链。
我们将使用websockets进行点对点通信。每个节点的活动套接字存储在 变量中。这里没有用自动对等节点发现工具。必须手动添加对等节点的位置(Websocket URLs)。
控制节点
用户必须能够以某种方式控制节点。可以通过创建HTTP服务器来实现。
如你所见,用户能够通过以下方式与节点交互:
呈现所有区块
使用用户给定的内容创建一个新区块
呈现和添加对等节点
控制节点的最直接方式是:例如使用Curl
架构
应该注意的是,该节点实际上公开了两个Web服务器:一个用于控制节点(HTTP服务器),一个用于节点之间的对等通信。(Websocket HTTP服务)。
结论
Naivecoin现在还只是一个玩具版的“通用”区块链。此外,本章还介绍了如何用简单的方式实现区块链的一些基本原理。下一章我们将把权益证明算法(挖矿)添加到naivecoin中。
第一章完整的代码参见这里
第二章: 权益证明
概述
本章,我们将为玩具版区块链定制了一个简单的权益证明方案。在第一章版本中,任何人都可以在不增加成本的情况下将区块添加到区块链中。通过权益证明,我们可以在块添加到区块链之前引入需要解决的计算难题。视图解决这个难题通常被称为“挖矿”。
通过权益证明验证,还可以控制区块链新增区块的频率。这是通过改变难题的难度来完成的。如果区块被挖掘出来过于频繁,难题的难度就会增加,反之亦然。
应指出,本章尚未介绍交易。这意味着矿工生产块还没有激励。通常在加密货币中,矿工会因为寻找到区块而获得奖励,但在我们的区块链中情况并非如此。
难度、随机性和权益证明难题
我们将在块结构中新增两个属性: (译者注:难度系数) 和 (译者注:随机数)。为了便于理解其含义,必须先介绍权益证明难题。
权益证明难题是找到一个拥有特定数量的0作为前缀的区块hash。 属性定义了区块hash必须有多少个前缀0,以使块有效。前缀的0通过块hash的二进制格式进行检查。
下面是一些针对各种难度系数的有效和无效hash的例子:
检查hash在难度系数上是否正确的代码如下:
为了找到满足难度系数的hash值,必须能够为相同内容的区块计算不同的hash值。这是通过修改 参数来完成的。由于SHA256是一个hash函数,每当块中的任何内容发生变化时,hash会完全不同。“挖矿”基本上只是尝试不同的随机数,直到块hash值符合难度系数的要求。
增加了 和 的块结构如下:
我们还须记得更新起始区块!
找到一个区块
如上所述,为了找到一个有效的区块hash,必须增加nonce,直到得到一个有效的hash。找到满意的hash完全是一个随机过程。我们必须通过足够的随机循环,直到找到满意的hash为止:
当找到该块时,就像第一章中所述那样将其广播到网络中。
对难度系数达成共识
现在我们已经有办法查找并验证hash值,但是如何确定难度系数呢?节点必须有一种方式来同意目前的难度系数。为此,我们介绍一些用来计算当前网络难度系数的新规则。
我们为网络定义以下新的常量:
,定义应该找到区块的频率。(在比特币中这个值是10分钟)
,定义了难度系数应该随网络hash率(译者注:hash率代表区块网络每秒能够执行的hash计算的次数)增加或减少而调整的频率。(在比特币中,这个值是2016个区块)
我们将区块生成间隔设置为10秒,难度调整为10个区块。这些常量不会随时间变化,且它们是硬编码的。
现在我们有办法就区块的困难系数达成一致。对于每10个生成的区块,我们检查生成这些块的时间是大于还是小于预期时间。预期时间计算如下: 。预期时间表示hash率与当前困难度完全匹配的情况。
如果所花费的时间比预期难度至少大两倍或者更小,可以将难度系数提高或者降低1,。难度调整由以下代码处理:
时间戳验证
第一章中,时间戳没有任何验证作用。事实上,它可能是客户端决定生成的任何东西。现在情况发生了变化,因为引入了难度调整timeTaken变量(在前面的代码段中),它是根据区块的时间戳计算而来的。
为了减轻引入错误时间戳的攻击以便处理难度系数,引入以下规则:
如果时间戳比前一个区块的时间戳晚一分钟内,则区块链中的块是有效的。
如果时间戳在当前时间之后一分钟内,则块是有效的。
累积难度
在上一章的区块链版本中,始终选择“最长”区块链作为有效区块链。有了难度系数后方案必须要调整。现在正确的链条不是“最长”的,而是具有最大累积库困难度的链条。换言之,正确的链是一条需要大部分资源(= hashRate * time)才能生成的链条。
为了获得链条的累积困难度,需要为每个区块执行2^difficulty次计算,并将所有区块的计算次数累加。我们必须使用2^difficulty来表示计算次数,因为我们选择了用难度系数来表示二进制hash前缀的0的个数。例如,如果我们比较5和11这两个难度系数,后者需要额外增加2^(11-5) = 2^6的工作量才能找到后一个区块。
以下例子中,虽然链条A拥有更长的链条,但链条B才是正确的链条。
计算累积难度时,只有区块的难度系数有用,而不是实际的hash(假设hash是有效的)。例如,如果难度系数为4,hash为:000000a34c……(也满足6的难度),则在计算累积难度时仅考虑为4的难度系数。
这个属性也被成为“Nakamoto共识”,它是Satoshi在发明比特币时最重要的发明之一。在分叉的情况下,矿工必须选择他们决定将当前的资源(hashRate)放置哪个链条上。由于矿工的兴趣在于生产将包含在区块链中的此类区块,因此矿工们获得激励,最终选择相同的链条。
结论
权益证明难题必须具备的一个重要特征是它很难解决,但易于验证。寻找特定的SHA256 hash值是这类问题的一个很好和简单的例子。
我们实现了难度方面,但节点现在必须“挖矿”,以便链条可以添加新块。在下一章中,我们将实现交易。
本章完整代码可以在这里找到
第3章:交易
概述
本章我们将介绍交易的概念。通过这种修改,我们实际上把该项目从“通用”的区块链转换为加密货币。因此,如果我们能够首先证明我们拥有这些地址,我们就可以将硬币发送到地址。
为了实现这一切,必须提出许多新概念。包括公钥密码学,签名和交易输入输出。
本章将要实现的完整代码可以在这里找到。
公钥加密和签名
在公钥密码学中,你有一个秘钥对:一个私钥和一个公钥。公钥可以从私钥中导出,但私钥不能从公钥中派生。公钥(顾名思义)可以安全地共享给任何人。
任何消息都可以使用私钥来创建签名。使用此签名和相应的公钥,任何人都可以验证签名是由公钥匹配的私钥生成的。
我们将使用一个名为elliptic的库用于公钥密码系统,该密码系统使用elliptic curve(ECDSA)
总而言之,两种不同的密码功能在加密货币中用于不同的目的:
用于权益证明挖掘的散列函数(SHA256)(也用于保持块的完整性)
用于交易的公钥加密(ECDSA)(将在本章中实现)
私钥和公钥(ECDSA)
一个有效的私钥是任何随机的32字节字符串,例如:
一个有效的公钥是用“04”串联一个64字节字符串,例如:
公钥可以从私钥派生出来。公钥将被用作交易中硬币的“接受者”(地址)。
交易概述
在编写任何代码之前,概述一下交易的结构。交易由两部分组成:输入和输出。输出指定了硬币的发送位置,输入证明实际发送的硬币首先存在并由“发送者”拥有。输入总是指向一个现有(未使用)的输出。
交易输出
交易输出(txOut)由一个地址和一定数量的硬币组成。该地址是一个ECDSA公钥。这意味着具有引用公钥(地址)的私钥用户将能够访问硬币。
交易输入
交易输入(txIn)提供硬币来自何处的信息。每个txIn指向一个较早的输出,硬币在该输出处被“解锁”,并携带签名。这些解锁的硬币现在可用于txOut。签名证明只有具有被引用的公钥(地址)的私钥用户才能创建该交易。
应该注意的是,txIn仅包含签名(由私钥创建),而不包含私钥本身。区块链包含公钥和签名,永远不会有私钥。
作为一个结论,可以这样认为:输入用于解锁硬币;输出重新锁定硬币
交易结构
因为定义了txIn和txOut,因此交易结构非常简单
交易ID
交易ID是通过从交易内容中获取hash来计算得到的。但是txId的签名并未包含在交易的hash中,稍后将会添加到交易中。
交易签名
交易内容在签名后无法更改是非常重要的。由于交易是公开的,任何人都可以访问这些交易,甚至在它们被纳入区块链之前。
对交易输入签名时,只会对txId进行签名。如果交易中的任何内容被修改,则txId必须更改,以使交易和签名失效。
让我们尝试了解如果有人视图修改交易会发生什么情况:
攻击者运行一个节点并接收一个交易,包含以下内容:“send 10 coins from address AAA to BBB”,其中txId为
攻击者将接收者地址改为 ,并在网络中转发。现在交易的内容是““send 10 coins from address AAA to CCC”
但是,随着接收者地址的更改,txId不再有效。一个新的有效txId将是
如果txId设置为新值,则签名无效。该签名仅与原始的txId 匹配。
修改后的交易不会被其他节点接受,因为无论哪种方式,它都是无效的。
未使用的交易输出
交易输入必须始终引用未使用的交易输出(uTxO)。因此,当您在区块链中拥有一些硬币时,您实际拥有的是未使用的交易输出清单,其公钥与您拥有的私钥相匹配。
就交易验证而言,我们只能关注未使用的交易输出清单,以确定交易是否有效。未使用的交易输出列表始终可以从给当前区块链派生。在这个视线中,我们将更新未使用的交易输出清单,并将交易纳入区块链中。
未使用的交易输出的数据结构如下所示:
如果是一个列表:
更新未使用的交易输出
每当一个新块添加到链中,我们都必须更新未使用的交易输出列表。这是因为新交易将花费一些现有交易输出并带来新的未使用输出。
为了处理这个问题,我们将首先从新块中检索所有未使用的交易输出(newUnspentTxOuts):
我们还需要知道块的新交易消耗了哪些交易输出(consumeTxOuts)。这将通过检查新交易的输入来解决:
最后,我们可以通过移除消耗的TxOut并将新的UnspentTxOut添加到现有的交易输出来生成newUnspentTxOut。
所描述的代码和功能包含在updateUnspentTxOuts方法中。应该注意的是,只有在块中的交易(和块本身)已被验证之后,才会调用此方法。
交易验证
现在我们终于可以制定交易有效的规则了:
正确的交易结构
交易必须符合Transaction、TxIn和TxOut的类定义
有效的交易id
交易id必须正确计算
有效的输入
输入中的签名必须有效,并且所引用的输出必须没有被使用。
有效的输出
输出中指定的值的总和必须等于输入中指定的值的总和。如果参考包含50个硬币的输出,新输出中的值的总和也必须是50个硬币。
Coinbase交易
交易输出必须始终引用未使用的交易输出,但最初的硬币从哪里进入区块链?为了解决这个问题,引入了一种特殊类型的交易:coinbase交易
coinbase交易只包含一个输出,没有输入。这意味着coinbase交易会为流通添加新的硬币。我们指定coinbase输出量为50个硬币。
coinbase交易总是块中的第一笔交易,并且由块的矿工包含。硬币奖励是对矿工的一种激励:如果你找到了这个块,你就可以收集50个硬币。
我们将添加块高度到coinbase交易的输入中。这是为了确保每个coinbase事务都有一个唯一的txId。例如,如果没有这个规则,一个表明“give 50 coins to address 0xabc”的coinbase交易总是有相同的txId。
coinbase交易的确认与“正常”交易的确认略有不同
结论
我们将交易的概念包含在区块链中。基本思想很简单:引用交易输入中的未使用输出并使用签名来表示解锁部分是有效的。然后使用输出将它们“重新锁定”到一个接受者地址。
但是,创建交易仍然非常困难。我们必须手动创建交易的输入和输出,并使用我们的私钥对它们进行签名。当我们在下一章介绍钱包时,情况会有所改观。
目前还没有交易中继:要将交易包含在区块链中,你必须自己挖掘。这也是我们尚未引入交易费概念的原因。
第四章: 钱包
概述
钱包的目标是为最终用户创建一个更抽象的交互接口。
最终用户必须能够:
创建一个新钱包(即私钥)
查看钱包余额
将硬币发送到其他地址
所有上述都必须正常工作,以便最终用户不必了解输入输出是如何工作的。例如,就想比特币一样:你可以将硬币发送到某个地址,并在其他人可以发送硬币的地方发布你的地址。
生成并存储私钥
本教程中,将使用最简单的方法来处理钱包生成和存储:我们将生成一个未加密的私钥文件 。
如上所述,公钥(地址)可以从私钥计算得来。
应该注意的是,以非加密格式存储私钥非常不安全。这么做的目的只是为了让事情变的简单。另外,钱包只支持一个私钥,所以你需要生成一个新的钱包来获得一个新的公共地址。
钱包余额
上一章提醒:当您在区块链中拥有一些硬币时,您实际拥有的是未使用的交易输出列表,其公钥与您拥有的私钥匹配。
这意味着计算给定地址的余额非常简单:只需将该地址“拥有”的所有未使用的交易累加即可:
如代码所示,查询地址的余额不需要私钥。这意味着给定一个地址,任何人都可以查询余额。
生成交易
发送硬币时,用户应该能够忽略交易输入和输出的概念。但是,如果用户A具有50个硬币的余额(即在单个交易输出中并且用户想要向用户B发送10个硬币),会发生什么呢?
这种情况下,解决方案是将10个比特币发送到用户B的地址,并将40个硬币发送回用户A。完整的交易输出必须始终用完,因此在将硬币分配给新的输出时必须完成“拆分”部分。这个简单的例子在下面图片中有所展示(输入没有显示):
我们来演示一下更复杂的交易场景:
用户C起初拥有0个硬币
用户C收到3笔价值10,20和30硬币的交易
用户C想要向用户D发送55个硬币。交易将会是什么样子?
此种场景,必须使用所有三个输出,并且输出必须具有给用户D的55个硬币和返回给用户C的5个硬币的值。
让我们将描述的逻辑显示为代码。首先,将创建交易输入,为此,我们将循环处理未使用的交易输出,知道这些输出的总和大于或等于我们要发送的数量。
如上所示,我们还将计算出发回自身地址的 值。
因为我们有未使用的交易输出列表,因此可以创建交易的输入:
接下来创建交易的两个输出:一个用于接收硬币的输出和一个用于leftOverAmount的输出。如果输入恰好具有所需值的精确值(leftOverAmount为0),就不会创建“leftOver”交易。
最后,我们计算交易ID并对输入签名:
使用钱包
再为钱包功能添加一个有意义的控制点:
如上所示,最终用户只能提供该节点的地址和硬币的数量。节点将计算其余部分。
结论
通过简单的交易生成,我们只是实现了一个初级的未加密钱包。尽管此交易生成算法从不会创建超过2个输出的事务,但应该注意的是,区块链本身支持任意数量的输出。你可以创建有效的交易,输入50个硬币并输出5,15,30个硬币,但必须使用/mineRawBlock 接口手动创建。
此外,在区块链中包含所需交易的唯一方法是自己动手挖矿。节点不交换关于尚未包含在区块链中的交易的信息。这将在下一章中讨论。
第五章:交易中继
概述
本章,我们将实现这些交易中继,这些交易尚未包含在区块链中。在比特币中,这些交易也被成为“未经证实的交易”。通常,当有人想将交易包含到区块链(将硬币发送到某个地址)中时,它会将交易广播给网络,并希望某个节点将交易划入区块链。
低功能对于正在运行的加密货币非常重要,因为这意味着你无需自行挖掘区块,以便将区块链中的交易包含在内。
因此,节点在彼此通信时将共享两种类型的数据:
区块链的状态(包含在区块链中的区块和交易)
未确认的交易(尚未包含在区块链中的交易)
交易池
我们将未经确认的交易存储在一个名为“交易池”(也成为比特币中的“mempool”)的新实体中。交易池是一个包含我们节点知道的所有“未经确认交易”的结构。在这个简单的实现中,我们将只使用一个列表。
我们还将为节点引入一个新的接口: 。此方法基于现有钱包功能为我们的本地交易池创建交易。现在,当想要将新交易包含到区块链时,将使用此方法作为首选接口。
创建交易就想在第四章中所作的那样,我们只是将创建的交易添加到池中,而不是不断地尝试挖掘一个块:
广播
未经证实的交易的全部要点是它们将遍布整个网络,最终一些节点将挖掘出交易给区块链。为了解决这个问题,将介绍一下关于未确认交易联网的简单规则:
当一个节点收到未确认的交易时,它将向所有对等节点广播整个交易池。
当一个节点首先连接到另一个节点时,它将查询该节点的交易池。
我们将添加两个新的MessageType来达此目的: 和 。MessageType枚举值如下所示:
交易池消息将按以下方式创建:
为了实现所描述的交易广播逻辑,我们添加了处理 消息类型的代码。每当收到未经确认的交易时,都会尝试将其添加到交易池中。如果设法将交易添加到池中,这意味着交易有效,并且节点之前没有看到交易。这种情况下,我们将自己的交易池广播给所有的对等节点。
验证收到的未确认交易
由于对等节点可以向我们发送任何类型的交易,因此必须对交易进行验证,然后才能将其添加到交易池中。所有现有的交易验证规则都适用。例如,交易必须格式正确,交易输入,输出的签名必须匹配。
除此之外,还添加了一条新规则:如果在现有交易池中已经找到任何交易输入,则不能讲交易添加到池中。如下代码所示:
没有明确的方式从交易池中删除交易。但是,每次找到新块时,交易池都会更新。
从交易池到区块链
接下来让我们为未确认的交易实现一种方式,以从本地交易池到同一节点挖掘的区块中。这很简单:当一个节点开始挖掘一个块时,它将把交易池中的交易包含到新块候选中。
由于交易已经验证,因此在将它们添加到池中之前,在这一点上没有进行任何进一步的验证。
更新交易池
随着交易的新块被开采到区块链中,我们必须在每次找到新块时重新验证交易池。新块可能包含使池中的某些交易无效的交易。这可能会发生,例如:
该池中的交易是由该节点本身或者其他人开采的
在未确认的交易中引用的未使用的交易输出被其他交易使用了
交易池将用以下代码进行更新:
可以看出,我们只需要知道当前未使用的交易输出,以便决定是否应将交易从池中移除。
结论
现在,我们可以将交易包含在区块链中,而无需实际开采自己的区块。然而,由于我们没有实现交易费的概念,因此没有激励节点将接收到的交易包含在区块中。
下一章,我们将为这个钱包和一个简单的区块链的开采者创建一些UI。
第六章:钱包UI和区块链浏览器
概述
本章,我们将为钱包添加UI,并为区块链创建区块链浏览器。我们的节点已经使用HTTP的方式发布了它的功能,所以我们将创建一个网页,向这些接口发送请求并可视化结果。
为了实现所有这些,必须添加一些额外的接口并为节点添加一些逻辑,例如:
查询块和交易的信息
查询特定地址的信息
新的接口
让我们添加一个用户可以查询特定块的接口,如果hash已知的话。
查询特定交易亦是如此:
我们还想显示有关特定地址的信息。现在我们回到该地址的未使用输出列表,因为根据这些信息,可以计算例如:该地址的总余额。
还可以添加关于给定地址的已使用交易输出的信息,以便可视化给定地址的完整历史记录。
前端技术
我们将使用Vue.js来实现钱包和区块链浏览器的UI部分。由于本教程不涉及前端开发,因此我们不会介绍前端代码。用户界面代码的仓库可以在这里找到。
区块链浏览器
区块链浏览器是一个用于可视化区块链状态的网站。区块链浏览器的典型用例是轻松检查给定地址的余额,或者验证给定交易是否包含在区块链中。
在本例中,我们只需向节点发出http请求,并以一种有意义的方式显示响应。我们从不提出任何修改区块链状态的请求,因此构建区块链浏览器全都是关于以有意义的方式可视化节点提供的信息。
区块链浏览器的截图如下所示:
钱包界面
对于钱包界面,我们还将创建一个类似于区块链浏览器的网站。用户应该能够发送硬币并查看地址的余额。我们还将显示交易池。
钱包截图:
领取专属 10元无门槛券
私享最新 技术干货