区块链基础知识(二)

原文:zh.annas-archive.org/md5/70e290e9f76882896adba256279d5064

译者:飞龙

协议:CC BY-NC-SA 4.0

第五章:加密货币

在本章中,我们将探索区块链技术的原始和最佳实现——加密货币。加密货币不仅仅是一个区块链应用程序;它利用了诸如数字签名之类的加密基元来实现通过称为交易的原子事件来管理资产。在整个本章中,我们将熟悉理解加密货币与传统数字货币有何不同所需的所有概念。

在本章中,我们将涵盖以下主题:

  • 比特币的基础知识

  • 密钥和地址

  • 交易

  • 挖矿和共识

  • 区块链

  • 区块链网络

  • 创建一个简单的加密货币应用程序

比特币是第一个成功部署在去中心化网络中的加密货币。由于其具有韧性的软件和基础设施、在各个领域的广泛采用以及高市值,比特币是迄今为止最知名的加密货币。截至 2017 年底,比特币的市值达到了 3000 亿美元,这是迄今为止所有加密货币中最高的。市场上的大多数加密货币都受到比特币的启发,并采用类似的设计。我们将使用比特币来了解加密货币中大多数相关概念,并在本章的后面部分,我们还将实现一个类似比特币的加密货币。

加密货币是一种使用密码学来确保、花费和验证交易价值的数字资产。加密货币可以从所有者转移到任何接收者,而无需中介来完成交易。尽管早期采用的加密货币提供了许多功能,如伪匿名性、更低的交易费用和消除中介的需求,但它从未实现真正的去中心化。已知存在问题,例如双花。这是因为单一资产转移到多个接收者,因为没有集中的来源来验证这些交易。当一个完全去中心化的加密货币——比特币在 2009 年创建时,所有这些问题都得到了解决。这是通过使用不可变的区块链在节点之间实现共识来首次解决了在去中心化网络中的双花问题。

比特币基础

比特币是加密学和去中心化共识算法的集合,它使得创造一个完全去中心化的数字货币生态系统成为可能。

比特币可以像传统货币一样使用。可以用来购买和出售商品和服务,或者只是向他人转账。比特币相对于传统货币有一些优势,比如较低的交易成本以及能够将货币转账到世界上的任何地方,因为它不受任何国家当局控制。比特币也是完全虚拟的,意味着没有实体形式的货币。比特币的价值是通过比特币的交易产生的。任何人都可以通过交易将比特币转账到特定的比特币地址。合法接收比特币的地址将通过与地址对应的秘钥进行标识。然后用户可以使用秘钥构建一个新的交易将比特币转账给其他人。一般来说,比特币地址是使用公钥创建的,而秘钥是私钥的对应。这些秘钥通常存储在一个称为钱包的软件应用程序中,但如果我们需要更好的安全性,它们也可以被备份和存储在任何地方。

我们知道,比特币是铺就了区块链发明之路的系统。它利用了我们迄今为止讨论过的所有概念,构建了一个在完全去中心化的点对点P2P)系统中运作的加密货币。由于比特币的完全去中心化网络,没有必要有一个像银行这样的中心信任机构充当调解者和验证交易。相反,比特币生态系统中的每个人都参与确保有效交易的进行。

比特币软件是开源的,任何人都可以通过在智能手机或计算机等设备上运行该软件来加入比特币网络。在计算和存储能力有限的设备上可以使用比特币软件的轻量级版本。有一种特殊类型的节点称为矿工,它使用处理能力来验证交易并通过解决一个难解的加密难题为区块的创建做出贡献。这是一个哈希难题,更具体地称为工作证明共识算法,在第三章中讨论过,区块链中的加密技术。每 10 分钟,一个矿工可以发布一个有效的区块,然后比特币网络中的每个人都会验证它。矿工以比特币作为奖励来补偿用于创建该区块的计算能力。由于挖矿竞争的加剧,难题的难度已经调整,以使平均区块创建时间保持在 10 分钟左右。

因此,每当矿工创建一个新区块时,新的比特币就会产生,并在比特币网络中流通。对比特币网络的流通总量设置了一个上限,它被硬性限制在 2100 万枚硬币。

总之,以下创新帮助比特币在完全无需信任的网络中生存下来:

  • 一个去中心化的点对点(P2P)网络

  • 区块链(公共分类账)

  • 分散式共识算法(工作量证明)

  • 交易验证规则

在本章中,我们将试图解释比特币如何利用这些概念,从而实现了它的创造。

开始使用比特币核心

比特币是由一个开源社区维护的实验性数字货币。比特币核心是一种开源软件,可实现该货币的使用。它是比特币系统的原始实现,最初由中本聪创建。

开源软件是指其源代码向公众开放,具有阅读、修改和重新分发的权利。虽然开源代码可以由不同的许可证覆盖,但大部分都可以免费用于任何目的。比特币是在 MIT 许可下发布的。

设置比特币全节点

比特币全节点可以用于开发目的,或者只是为了使用户成为比特币网络的一部分,以验证或探索交易。如果用户想要建立完整的开发环境,他们必须设置可能需要的所有工具、库和依赖应用程序,而安装软件只需不费吹灰之力。

安装比特币全节点

正如前面提到的,安装比特币全节点比设置开发环境简单得多。比特币全节点非常适合想要成为比特币网络一部分但不想担心任何实现细节的用户。

运行比特币全节点有一定的硬件要求。它需要专用存储空间,因为它必须存储公共分类账的所有区块。在撰写本文时,比特币区块链区块占用约 180 GB 的存储空间。比特币全节点还需要相当数量的内存和处理能力,以验证每个区块的交易。比特币可以相当轻松地安装在 Linux、macOS 和 Windows 平台上。

我们不会在这里提供安装细节,因为它因平台而异。您可以在该书的 GitHub 存储库 (github.com/PacktPublishing/Foundations-of-Blockchain) 中找到不同平台的安装详情。此外,您可以在bitcoin.org/en/full-node上找到所有平台的安装说明。

从源代码编译

比特币开发环境是通过编译从比特币存储库获得的源代码来设置的。比特币核心的源代码托管在 GitHub 存储库中,使用 MIT 许可证。您可以克隆并获取所有分支,也可以下载特定版本的 ZIP 文件。

您可以使用 Git 工具从 github.com/bitcoin/bitcoin.git 仓库克隆比特币核心项目。项目克隆后,您可以使用最新的主代码或使用 Git 标签检出任何发布版本。

编译过程可能需要长达一个小时,具体时间取决于系统的硬件配置。编译源代码只涉及几个步骤,但它们耗时:

  1. 作为第一步,比特币核心需要您运行一个名为autogen.sh的脚本,该脚本创建一组自动配置脚本,检查系统并确保系统具有编译代码所需的所有库。脚本的执行如下:
 $ ./autogen.sh
  1. 下一步是使用配置脚本通过启用或禁用某些功能来自定义构建过程。因为大多数比特币核心功能都需要设置节点,所以我们可以使用默认功能构建比特币核心。配置脚本的执行如下:
 $ ./configure

  1. 最后,源代码将被编译以创建可执行文件,并安装创建的可执行文件。使用以下命令实现:
      $ make
      $ make install

安装将创建一个名为bitcoind的二进制文件,它创建比特币守护进程,并创建一个名为bitcoin-cli的命令行工具,用于调用比特币 API 与本地比特币节点进行通信。

运行比特币节点

当执行bitcoind时,通过创建配置文件创建比特币守护进程。基本配置文件包括 JSON-RPC 接口的用户名和密码。在运行比特币节点时,可以指定几个选项来修改其行为。这些选项也可以在配置文件中指定。执行bitcoind --help以列出可用选项。创建配置文件后,可以执行bitcoind

$ bitcoind -daemon

这将作为后台进程启动比特币核心。加载区块索引并验证区块可能需要几分钟时间。一旦创建了bitcoind进程,就可以使用bitcoin-cli工具检查状态。

以下命令显示有关比特币核心的一些基本信息以及一些本地区块链信息。在调用 API 时,mainnet链在那时已经挖掘了 519,993 个区块:

$ bitcoin-cli getblockchaininfo 
{ 
  "chain": "main", 
  "blocks": 519993, 
  "headers": 519993, 
  "bestblockhash": "0000000000000000000d4715ff499c5ce23c4b355634d4b59a2fe3823387dd12", 
  "difficulty": 3839316899029.672, 
  "mediantime": 1524733692, 
  "verificationprogress": 0.999994783377989, 
  "chainwork": "0000000000000000000000000000000000000000019897317fc702c4837762b2", 
  "pruned": false, 
... 
} 

比特币由比特币节点形成了几个区块链网络。每个网络维护不同的区块链。比特币的主网络称为mainnet,并且有一个称为testnet的测试网络。关于这个主题的更多信息在本章的区块链和网络部分有涉及。

与比特币节点通信

比特币核心通过 JSON-RPC 接口提供 API,以便与比特币节点进行通信。可以使用此接口在节点上执行大多数共识、钱包和 P2P 比特币操作。比特币使用 8332 作为主网的默认 JSON-RPC 服务器端口。用户应确保不允许任意机器访问 JSON-RPC 端口。暴露此接口将允许外部机器访问私人信息,这可能导致盗窃。

比特币有一个命令行界面工具,名为 bitcoin-cli,可用于访问所有 JSON-RPC API。

通过指定交易 ID,可以使用 getrawtransaction RPC 命令获取任何交易的详细信息:

$ bitcoin-cli getrawtransaction 
4289bf1e7a4295e75fcff0644c44bd1c114511b7ec5407afea64de2d280bddb802000000010e1bd74a37fa90e5e8de8e4c20ec42a26c70ef40330b5361c560d03f3c8ba7e9000000006a47304402201a62b24dcbeba9ec65478be8a12ccd31c3c984
9813782d1ca0bcab657a88762402204897f9c9e5e99de969fd5d076d80aebbaef19493f5e273663d4727864a67295b012102b21f43b03f57e029ea43f2cec448d4ff43740af4a68607507f34fd93be97bc30feffffff028096980000000000197
6a914523f63d0e9f8cb9519482fc6a8476689e57555e688ac59065701000000001976a91469cac07f09af880832eedbcbc7e0dea94fb68e2688acb1bb1300

输出是十六进制格式的序列化交易。然后可以使用 decoderawtransaction API 对此数据进行解码:

$ bitcoin-cli decoderawtransaction 
02000000010e1bd74a37fa90e5e8de8e4c20ec42a26c70ef40330b5361c560d03f3c8ba7e9000000006a47304402201a62b24dcbeba9ec65478be8a12ccd31c3c9849813782d1ca0bcab657a88762402204897f9c9e5e99de969fd5d076d80aebbaef19493f5e273663d4727864a67295b012102b21f43b03f57e029ea43f2cec448d4ff43740af4a68607507f34fd93be97bc30feffffff0280969800000000001976a914523f63d0e9f8cb9519482fc6a8476689e57555e688ac59065701000000001976a91469cac07f09af880832eedbcbc7e0dea94fb68e2688acb1bb1300

这会生成一个以人类可读的 JSON 格式解码的交易。我们将在本章的 交易 部分讨论解码后的交易。

通过 JSON-RPC 实现使用脚本语言进行通信

任何编程语言的 JSON-RPC 实现都可以用于与比特币节点进行通信。让我们使用 Python 的 JSON-RPC 实现执行一个 API,它将自动生成所有用于 RPC 调用的 Python 方法。Python 有几个支持 JSON-RPC 的库,但我们将使用 python-bitcoinlib,它不仅提供了 JSON-RPC 实现,还可以与比特币数据结构和协议进行接口交互。以下 Python 脚本接受十六进制格式的交易 ID,并使用 lx() 函数将其转换为原始字节。创建了一个 RPC 对象 proxy_connection,它可以用来调用任何 API。gettransaction API 将获取所提供交易 ID 的解码交易:

import bitcoin.rpc 
from bitcoin.core import lx 

bitcoin.SelectParams('testnet') 

proxy_connection = bitcoin.rpc.Proxy() 
tx_id = lx(input()) 
print(proxy_connection.gettransaction(tx_id)) 

密钥和地址

我们已经介绍了在理解加密货币中使用的密钥、地址和钱包所需的所有加密学概念。在本节中,我们将深入了解密钥和地址如何通过密码原语来控制资金所有权。

我们已经介绍了非对称加密是如何用于创建区块链网络中的公钥/私钥,这些密钥标识用户帐户。比特币生成用于标识用户并帮助他们通过数字签名声称资金所有权的公钥/私钥对。私钥在加密货币中也称为密钥,因为它对公众保密。数字签名是密码学中常见的概念,它允许密钥所有者为交易创建签名,并允许任何人验证交易。密钥所有者使用称为钱包的轻量级软件管理其所有密钥。密钥独立于区块链协议,由钱包创建和管理。

比特币使用地址来识别比特币用户。这些地址是从用户的秘密密钥派生的公共密钥的编码版本。让我们考虑一个银行业务的例子:爱丽丝通过签署一张支票并寄给鲍勃来向鲍勃转账。比特币使用类似的方法,通过使用秘密密钥签署交易并提供接收方的账户号码(比特币公共地址)来完成交易。唯一的公共信息是账户号码,这与将比特币地址公开的概念类似。

公共和私有密钥

使用非对称加密生成公共和私有密钥。私有密钥保密存放在用户的钱包中,而公共密钥以比特币地址的形式公开。比特币使用椭圆曲线加密生成公共/私有密钥对。私有密钥是随机选择的,并执行椭圆曲线乘法以生成公共密钥。椭圆曲线乘法是一种单向加密函数,使得从公开的公共密钥推导出私有密钥变得不可能。你可以在第二章,一点点密码学中探索椭圆曲线加密的数学解释和分析。

比特币的私有密钥是随机选择的 256 位字符串或 64 位十六进制字符串。这意味着私有密钥可以是介于 1 和 2²⁵⁶ 之间的任意数字。因此,通过暴力破解 2²⁵⁶ 组合来找到公共密钥的对应私有密钥是不可能的。随机生成的私有密钥与比特币网络隔离,并在比特币钱包中保密存放。

比特币的命令行界面可用于生成秘密密钥。比特币有一套处理密钥和地址的 API。以下命令在本地钱包上执行。dumprivkey 获取已存在的从公共密钥生成的私有密钥:

$ bitcoin-cli getnewaddress
1JK1yCXbP2WkwgzbAUqpWTeo9rQkA9seNg
$ bitcoin-cli dumpprivkey 1JK1yCXbP2WkwgzbAUqpWTeo9rQkA9seNg
L2NAKvQsbkeQZyhfPRWw1juQ19ohxGCFbdr8izQSHEmKWYFtVjXK  

比特币公共地址

比特币中的公共地址由以数字 1 或 3 开头的公共密钥生成。大多数比特币地址有 33 或 34 个字符长,并使用 Base58 编码。比特币公共密钥地址始终代表秘密密钥的所有者,并用于交易的接收者字段。然而,地址也可以有不同的用途,例如表示在本章的交易部分中将要介绍的支付到脚本哈希P2SH)交易中使用的付款脚本。

比特币地址是通过构建一个名为 Base58Check 的编码字符串从公钥派生出来的。Base58Check 是一个 Base58 编码的字符串,附带固定字符作为错误检查码。代表比特币地址的 Base58Check 编码字符串有三部分——一个前缀版本字节,一个从公钥派生出来的有效载荷,和一个校验和。版本字节代表比特币地址的类型。表 5.1展示了在比特币地址中找到的各种前缀:

前缀(Base58)用途
1比特币公钥哈希(P2PKH 地址)
3比特币脚本哈希(P2SH 地址)
5私钥(WIF)
m 或 n比特币测试网公钥哈希
2比特币测试网脚本哈希

表 5.1:用作版本字节的前缀(来源:https://en.bitcoin.it)

比特币公共地址的第二部分是通过使用SHA256RIPEMD160哈希算法对公钥进行哈希计算得到的。正如我们已经知道的,哈希函数是单向函数,这使得无法从计算出的比特币地址派生出公钥。

在下面的函数中,K是从私钥派生出的公钥,H是比特币公钥哈希:

H = RIPEMD160(SHA256(K)) 

使用哈希函数构建哈希值H后,地址的第三部分——校验和,是从结果值计算出来的。最后,三个部分被连接并使用 Base58 编码进行编码,Base58 编码使用 58 个字符(请参阅第二章, 密码学知识点,了解 Base58 编码的更多详情)来创建比特币公钥地址,如前面提到的 Base58Check 格式:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5.1:从公钥生成比特币公共地址

图 5.1显示了如何从 64 字节的公钥以及 1 字节的版本开始进行哈希计算,最初产生 20 字节的摘要。这就是有效载荷,然后对其进行编码以生成比特币地址。但最终地址还包括校验和位,用于防止错误。这 4 字节的校验和是通过使用 SHA256 哈希函数反复对有效载荷进行两次哈希计算并从结果的 32 字节摘要中提取初始 4 字节来计算出来的。

这个校验和与有效载荷和版本字节连接起来。最后,得到的字符串使用 Base58 编码系统进行编码以生成比特币地址,这将标识持有相应密钥的用户。

众所周知,从公开的比特币地址中生成私钥是不可能的。这是由于用于派生公钥的函数。公钥是使用椭圆曲线乘法来创建的,这是一个单向函数。同样,比特币地址是通过应用哈希函数来自公钥派生出来的,这些函数本质上也是单向函数。这也防止了任何人从比特币公共地址中生成公钥:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5.2: 比特币地址生成的单向功能

图 5.2描述了比特币地址生成中使用的单向功能。虽然代表持有私钥实体的身份的比特币地址为公众所知,但由于这些功能的单向性质,无法检索私钥。

交易

我们已经涵盖了比特币中用于在分布式网络中创建加密货币的许多概念。尽管比特币中的每个概念都在其中发挥着重要作用,但交易扮演着中心角色。比特币中的其他一切都旨在确保有效的交易安全地包含在区块链中并传播到整个节点网络中。

与使用基于账户的分类账的传统簿记不同,比特币维护基于交易的分类账。在将每笔交易输入比特币之前,必须对其进行验证。比特币节点引用这些交易。

包含在其他块或交易内存池中,以验证每个交易。在本节中,我们将深入探讨交易创建、验证和交易各部分的概念。

交易内存池是每个比特币节点维护的未确认交易的集合。内存池中的交易最终将被包含在区块链中。可以在www.blockchain.com/btc/unconfirmed-transactions上实时查看比特币内存池。

交易的高级概览

在深入了解交易及其各部分的低级细节之前,让我们看一个例子,从用户角度说明了一个简单的比特币交易。假设 Alice 从她的比特币钱包向 Bob 的钱包发送了 0.1 比特币。由于她的钱包中已经拥有超过 0.1 比特币,将会创建一个有效的交易并传播到网络中。图 5.3显示了在区块浏览器应用程序中的交易详情:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5.3: 从 Alice 到 Bob 的比特币交易

本节中展示的交易详细信息均使用比特币的testnet区块链创建。您可以通过将比特币节点切换到 testnet 区块链来检查交易细节。区块浏览器应用程序也在 testnet 区块链中运行。您可以在testnet.blockexplorer.com上验证这些交易。

交易对所有访问区块链的人都是可见的。需要使用十六进制编辑器读取交易,因为它们不是以人类可读的形式存储的。比特币核心的命令行界面可以读取原始交易并对其进行解码:

{ "txid": "4289bf1e7a4295e75fcff0644c44bd1c114511b7ec5407afea64de2d280bddb8", "hash": "4289bf1e7a4295e75fcff0644c44bd1c114511b7ec5407afea64de2d280bddb8", "version": 2, "size": 225, "vsize": 225, 
  "locktime": 1293233, 
  "vin": [ 
    { 
      "txid": "e9a78b3c3fd060c561530b3340ef706ca242ec204c8edee8e590fa374ad71b0e", 
      "vout": 0, 
      "scriptSig": { 
        "asm": "304402201a62b24dcbeba9ec65478be8a12ccd31c3c9849813782d1ca0bcab657a88762402204897f9c9e5e99de969fd5d076d80aebbaef19493f5e273663d4727864a67295b[ALL] 02b21f43b03f57e029ea43f2cec448d4ff43740af4a68607507f34fd93be97bc30", 
        "hex": "47304402201a62b24dcbeba9ec65478be8a12ccd31c3c
9849813782d1ca0bcab657a88762402204897f9c9e5e99de969fd5d076d80aebbaef19493f5e273663d4727864a67295b012102b21f43b03f57e029ea43f2cec448d4ff43740af4a68607507f34fd93be97bc30" 
  }, 
      "sequence": 4294967294 
    } 
  ], 
  "vout": [ 
    { 
      "value": 0.10000000, 
      "n": 0, 
      "scriptPubKey": { 
        "asm": "OP_DUP OP_HASH160 523f63d0e9f8cb9519482fc6a8476689e57555e6 OP_EQUALVERIFY OP_CHECKSIG", 
        "hex": "76a914523f63d0e9f8cb9519482fc6a8476689e57555e688ac", 
        "reqSigs": 1, 
        "type": "pubkeyhash", 
        "addresses": [ 
          "mo1qeC6G2cLZqeBiSeLgKL2z1QcvsG9Mu3" 
        ] 
      } 
    },  
    { 
      "value": 0.22480473, 
      "n": 1, 
      "scriptPubKey": { 
        "asm": "OP_DUP OP_HASH160 69cac07f09af880832eedbcbc7e0dea94fb68e26 OP_EQUALVERIFY OP_CHECKSIG", 
        "hex": "76a91469cac07f09af880832eedbcbc7e0dea94fb68e2688ac", 
        "reqSigs": 1, 
        "type": "pubkeyhash", 
        "addresses": [ 
          "mqAL8eXgSE7tHGF9fdYhYnwMFW3nnNyH9z" 
        ] 
      } 
    } 
  ] 
} 

getrawtransaction API 用于检索编码的交易。可以使用decoderawtransaction对其进行解码。可以使用比特币的命令行界面或任何 RPC 客户端调用这些命令。

原始交易的可读版本具有许多字段,在第一眼看起来可能会很压倒性。我们将在接下来的部分中介绍交易的一些组成部分,以便理解原始交易。

交易输入和输出

交易主要由输入和输出构成。每个交易可以有多个输入和输出。与基于账户的簿记不同,比特币需要跟踪每笔交易的输出。节点需要拥有所有交易输出信息才能知道账户的可消费余额。当用户想要花费他们的加密货币时,输出可以在交易的输入中被引用。这个输出由不可分割的货币块组成,在交易中消耗后才能分割。在任何交易输入中都没有被引用的输出被称为未消费交易输出UTXO

每当用户想要花费 UTXO 时,它必须完全花费。在花费 UTXO 后,任何多余的金额都将作为另一个 UTXO 返回给用户。这类似于现实世界中的货币,其中硬币无法分解为更小的值,并且会因支付超额金额而收到找零。让我们考虑之前的例子,阿丽斯向鲍勃发送了 0.1 比特币。正如我们在图 5.3中所看到的,阿丽斯没有价值为 0.1 的 UTXO。因此,阿丽斯消耗了价值为 0.325 的交易输出,这大于 0.1。在向鲍勃发送 0.1 比特币后,剩余的金额将退回给阿丽斯,创建一个新的 UTXO。从交易中我们可以看到,阿丽斯的账户得到了略少于 0.225 比特币的退款。

这是由于在将交易插入区块链时收取的交易费。此费用将被支付给执行工作量证明的矿工。此交易创建了两个输出值,0.1 和 ~0.225。这两个输出值必须完全被其他输入消耗。

交易输出

如前一节所述,每笔交易都会创建输出,后续交易输入可以消耗。每个完整节点客户端都会跟踪所有 UTXO,以便通过检查 UTXO 池轻松验证每个交易输入。

让我们调查一下之前阿丽斯和鲍勃之间交易的输出。交易输出使用vout键来引用:

 "vout": [ 
    { 
      "value": 0.1, 
      "n": 0, 
      "scriptPubKey": { 
        "asm": "OP_DUP OP_HASH160 523f63d0e9f8cb9519482fc6a8476689e57555e6 OP_EQUALVERIFY OP_CHECKSIG", 
        "hex": "76a914523f63d0e9f8cb9519482fc6a8476689e57555e688ac", 
        "reqSigs": 1, 
        "type": "pubkeyhash", 
        "addresses": [ 
          "mo1qeC6G2cLZqeBiSeLgKL2z1QcvsG9Mu3" 
        ] 
      } 
    } 
  ] 

先前的vout是交易的输出之一。每个输出都有两个主要组成部分,价值密码条件,它们解释了谁拥有这笔交易。在前述交易中,输出值表示转账给鲍勃的比特币数量,scriptPubKey是密码条件(或锁定脚本),它确保输出金额只能被鲍勃花费。 scriptPubKey具有几个包含序列化(hex)和反序列化(asm)格式的锁定脚本的字段。它还提供了一些额外信息,例如所需签名(reqSigs)、类型以及接收方的公共地址。虽然交易输出有几个字段,但对于一笔交易来说,只有锁定脚本是感兴趣的,其他字段可以由它派生。大部分锁定脚本是用户公共地址的简单表示。我们将在本章的后面更深入地研究锁定脚本。

交易输入

当用户希望进行比特币交易时,交易输入会引用 UTXO。交易输入使用解锁脚本来声明 UTXO。有效的交易输入通过这个解锁脚本证明对 UTXO 的所有权。

一个交易输入可以有多个输入指向多个 UTXO。交易输入确保有足够的 UTXO 使交易得以进行。在前面的示例中,交易输入有一个指向 UTXO 的单个输入:

"vin": [ 
    { 
      "txid": "e9a78b3c3fd060c561530b3340ef706ca242ec204c8edee8e590fa374ad71b0e", 
      "vout": 0, 
      "scriptSig": { 
        "asm": "304402201a62b24dcbeba9ec65478be8a12ccd31c3c9849813782d1ca0bcab657a88762402204897f9c9e5e99de969fd5d076d80aebbaef19493f5e273663d4727864a67295b[ALL] 02b21f43b03f57e029ea43f2cec448d4ff43740af4a68607507f34fd93be97bc30", 
        "hex": "47304402201a62b24dcbeba9ec65478be8a12
ccd31c3c9849813782d1ca0bcab657a88762402204897f9c9e5e99de969fd5d076d80aebbaef19493f5e273663d4727864a67295b012102b21f43b03f57e029ea43f2cec448d4ff43740af4a68607507f34fd93be97bc30" 
      }, 
      "sequence": 4294967294 
    } 
  ], 

单个 UTXO 足以完成交易。图 5.3显示,这个单个 UTXO 的价值为 0.325,足以向鲍勃发送 0.1 的价值。交易输入通过使用交易 ID(txid)和创建此 UTXO 的交易的序列号来指向 UTXO。与交易输出一样,交易输入包含一个解锁脚本,用于证明用户对 UTXO 的索赔并确保交易有效。花费者最初检索 UTXO 并使用交易 ID 引用它。解锁脚本使用解锁资金所需的秘密信息。一个简单的解锁脚本将带有使用私钥签名的数字签名和相应的公钥。但是,表示可能是复杂的,但这是我们将在下一节更详细地介绍的内容。

在这个例子中,爱丽丝使用txid指向交易中可花费的 UTXO。然后爱丽丝创建一个解锁脚本并将其放置在交易的scriptSig字段中。每个得到交易的人都将通过检查 UTXO 中的锁定脚本来验证它。

如前所述,除了 Alice 打算转账给 Bob 的 0.1 比特币之外,她还需要支付交易费用。但是,在原始交易结构中没有交易费用字段。通过检查所有引用的 UTXO 的值然后从交易输入值中减去此值来计算。这不被跟踪在交易输出中的附加值形成交易费用。每个矿工将为每个交易计算费用,并将合并的值作为特殊的 coinbase 交易奖励给自己。我们将在本章的挖矿和共识部分更多地涵盖 coinbase 交易。

交易验证

交易验证是通过解锁脚本和锁定脚本来执行的。比特币使用一种简单的自定义脚本语言称为 Script,它类似于基于堆栈的执行语言 Forth。为了验证交易,执行输入中的解锁脚本与其相应的锁定脚本。基于堆栈的执行应返回一个真值并成功执行解锁脚本。

在示例中,按顺序评估了输入的 scriptSig 和引用的输出的 scriptPubKey,其中 scriptPubKey 使用 scriptSig 留在堆栈上的值。如果 scriptPubKey 返回真,则授权输入。

Script

在尝试理解锁定和解锁脚本之前,我们需要了解 Script 语言的基本执行方式。Script 是一个简单的基于堆栈的语言,从左到右进行处理。它有意地不是图灵完备的。它没有复杂的控制流程,如循环,除了条件。这确保了程序在可预测的时间内执行。这是有意为之,以避免拒绝服务攻击,这些攻击会创建无限执行循环。Script 语言的简单性确保了比特币不会受到这种攻击的任何影响。

Script 也是无状态的,这意味着在执行之前或之后没有存储任何信息。这确保了执行不受系统的任何其他方面的影响,并且脚本可以在任何系统上执行。

Script 是一种基于堆栈的语言,因为它在执行过程中使用堆栈数据结构。堆栈数据结构对数据项执行推送和弹出操作。推送操作将项添加到堆栈顶部,弹出操作删除最后插入的项。Script 通过从左到右处理项来执行程序。每当遇到数据项时,就将其推送到堆栈上。

操作员从堆栈中弹出一个项目并对它们执行操作,然后将结果推回到堆栈中。脚本具有一个庞大的操作员集合,这些操作员由操作码表示,可以用于项目上。诸如OP_ADD之类的算术操作码对堆栈顶部的两个项目执行加法,而条件OP_EQUAL操作码评估条件,产生布尔结果。比特币交易脚本主要由一个条件运算符组成,其最终结果必须在交易被视为有效时评估为真值。

脚本示例

让我们通过一个简单的例子来执行该脚本:

2 4 OP_ADD 6 OP_EQUAL 

脚本的执行从左边开始。当执行开始时,数据常量24被插入到堆栈中。接下来,脚本执行一个加法操作,由OP_ADD操作员表示。加法操作在弹出堆栈顶部的两个项目后执行,因此 2 + 4 = 6。结果被推回到堆栈中。当遇到它时,数据常量6被推到堆栈中。最后,在堆栈项目上执行条件OP_EQUAL操作员。最后两个项目从堆栈中弹出并进行比较,以查看它们是否相等。由于我们堆栈中的最后两个数据项是6,所以相等条件将返回一个TRUE值。

锁定和解锁脚本

比特币使用类似的脚本执行方法,但具有不同的操作码集。比特币交易使用锁定和解锁脚本,这些脚本一起执行以验证交易。如前所述,锁定脚本是在交易输出中指定的支出条件,而解锁脚本在执行两个脚本时满足这个条件。

我们可以通过分解前面的脚本示例来创建一个简单的锁定和解锁脚本。脚本的一部分可以形成一个锁定脚本,如下所示:

4 OP_ADD 6 OP_EQUAL 

这只能通过解锁脚本来满足:

2 

任何节点都将通过按顺序组合和执行锁定和解锁脚本来验证这些脚本,就像前面的示例中所示:

2 4 OP_ADD 6 OP_EQUAL 

但这些只是基本脚本,任何具有基本算术技能的人都可以创建一个解锁脚本以花费交易输出。这就是为什么比特币使用一个具有密码难题的复杂条件作为锁定脚本。只有具有私钥的合法所有者才能通过创建具有解锁脚本的证明来花费资金。

在比特币中,锁定脚本被称为scriptPubKey,就像在交易部分的早期交易示例中看到的那样。这是因为在锁定脚本中使用了公钥哈希(比特币地址)来将资金转移到相应私钥的所有者。类似地,解锁脚本可以在交易输入的scriptSig字段中找到。解锁脚本通常通过创建数字签名来证明与公钥对应的私钥的所有权。这就是为什么解锁脚本通常被称为scriptSig的原因。

类似于示例,比特币交易验证者通过执行合并的锁定和解锁脚本来验证交易。验证者检索交易输入引用的 UTXO,并将锁定和解锁脚本并排放置以进行顺序执行。

交易脚本类型

比特币目前创建两种不同的基本scriptSig/scriptPubKey对。在比特币交易中很少使用复杂的交易脚本。付款至公钥哈希P2PKH)和付款至脚本哈希P2SH)是最受欢迎的脚本。比特币网络中执行的大多数脚本使用 P2PKH 作为它们的交易脚本。

P2PKH

比特币交易创建一个带有名为 P2PKH 的公钥哈希的输出交易。公钥哈希表示用户持有的相应私钥的比特币公共地址。用户使用接收者的公共地址创建一个锁定脚本。除了相应私钥的持有者外,没有人能够索取交易输出。

在我们之前的示例中,Alice 创建了一个包含scriptSig的交易输入,并使用scriptPubKey创建了交易输出。

以下脚本显示了scriptPubKeyscriptSig的语法:

scriptPubKey: OP_DUP OP_HASH160 <pubKeyHash> OP_EQUALVERIFY 
OP_CHECKSIG 
scriptSig: <sig> <pubKey> 

验证者将合并脚本并按顺序执行它们:

<sig> <pubKey> OP_DUP OP_HASH160 <pubKeyHash> OP_EQUALVERIFY 
OP_CHECKSIG 

花费者记录的签名(sig)和公钥(pubKey)被推送到堆栈中。OP_DUP运算符在堆栈中复制pubKey。下一个运算符,OP_HASH160,使用SHA256RIPEMD160算法计算公钥的 160 位哈希值:

RIPEMD160(SHA256(pubKey)) 

锁定脚本中的pubKeyHash值被推入堆栈。OP_EQUALVERIFY运算符验证解锁脚本中创建的公钥哈希与之前推送到堆栈的pubKeyHash值是否相等。如果锁定和解锁脚本的公钥哈希匹配,则返回TRUE值。最后,OP_CHECKSIG从堆栈中弹出sigpubKey,执行数字签名验证,并验证签名是否有效。一旦验证成功,脚本返回TRUE值,表示解锁脚本有效。

P2SH

P2SH 是早期比特币开发者之一加文·安德森引入的。根据加文·安德森的说法,P2SH 是有目的地创建的:

“将供应赎回交易的条件的责任从资金发送方移交给赎回方。”

– 安德森

在 P2SH 中,资金寻址到脚本的哈希而不是公共地址。这个脚本被称为赎回脚本,其中包含了花费资金所需的所有条件。正如 Gavin Andresen 所说,交易的创建者无需担心花费资金的条件,只需提及包含条件的脚本的哈希。当需要花费资金时,赎回者应提供与提及的脚本哈希匹配的脚本,并确保脚本求值为 true。

P2SH 提供了一种进行复杂交易的方式,不像 P2PKH 对scriptPubKeyscriptSig有特定的定义。该规范对脚本没有任何限制,因此任何脚本都可以使用这些地址进行资金支付。脚本的概念类似于智能合约的概念,这将在第七章,深入区块链-所有权的证明中进行讨论。

挖矿和共识

比特币中的挖矿是在去中心化的比特币网络中实现区块链状态共识的一个至关重要的概念。比特币网络中的任何节点都可以执行挖矿操作,并且这些节点会因为它们对挖矿的贡献而获得激励。这导致了对挖矿和奖励之间的混淆。尽管奖励是挖矿的一部分,但挖矿并不只是为了奖励。挖矿是比特币网络去中心化的机制,通过构建一个被所有人接受的区块链,在不可信任的网络中实现节点之间的共识。

与任何其他比特币节点一样,矿工也会验证新的交易并将它们存储在本地内存池中。除了执行验证,矿工还会创建一个交易块并解决哈希难题,以将创建的区块包括在全球比特币总账中。一旦创建的区块被纳入区块链,矿工将获得两种激励:每笔交易的交易费和每个区块中新创建的比特币。交易费是每笔交易处理所收取的费用,并且由交易的创建者附加。每个区块都有一个特殊的交易,创建新的比特币并将其授予负责创建区块的矿工。这种特殊的交易被称为 coinbase 交易,负责创建新的比特币。

由于比特币总量存在硬性上限(2100 万枚),在未来的某个时间点,矿工只会从交易费用中获得激励。每挖掘出 210,000 个区块,矿工获得的新铸比特币最大数量就减半一次。由于每个区块的创建时间保持在约 10 分钟左右,每四年就会创建出 210,000 个区块。激励从 2009 年比特币启动时的每个区块 50 个比特币开始,之后在 2012 年和 2016 年分别减半。

目前,每个区块的矿工奖励为 12.5 个新铸币的比特币:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5.4:截至 2018 年的比特币货币供应量(来源:www.blockchain.info)

图 5.4显示了比特币随时间的供应量。由于每个区块的比特币奖励减半,曲线呈几何减少趋势。比特币网络可能在 2140 年之前供应 2100 万枚硬币。当年份接近 2040 年时,图中的线将几乎平行于 x 轴。

挖掘一个区块

比特币网络中的任何节点都可以创建一个区块并自称为矿工。矿工必须运行维护完整区块链的比特币节点。虽然挖矿操作可以使用最小的硬件要求执行,但由于比特币节点之间的激烈竞争,使用最小硬件配置的独立计算硬件进行挖矿已经不再具有盈利性。这就是为什么比特币矿工经常运行具有更高处理能力的专用计算机硬件,如 GPU。

由于矿工之间的竞争加剧,比特币的挖矿难度增加了。矿工开始构建专门用于挖矿的专用集成电路。这些专用集成电路被称为专用集成电路ASICs),它们不能用于通用计算。有几家比特币 ASIC 制造商生产不同规格的 ASIC。Bitmain 的 Antminer 设备是最广泛使用的 ASIC。

每个矿工节点还将监听比特币网络以获取新的交易和区块。在得出他们已成功挖掘新区块之前,他们会执行几项任务:

  • 交易验证

  • 将交易汇总到区块中

  • 使用工作量证明算法挖掘区块

交易验证

正如我们所看到的,每个有效的交易是通过收集 UTXO 并使用适当的脚本来解锁它们,然后使用锁定脚本创建的,该脚本将资金锁定给下一个所有者。交易被广播到比特币网络,以便每个人都知道区块链的更新状态。广播交易还确保交易到达矿工节点并包含在任何创建的区块中。

尽管交易在广播到网络之前会被验证,但矿工节点在将其包含在区块之前总是验证每一笔交易。一个无效的交易可能会导致整个区块被比特币网络拒绝。为防止不必要的损失,矿工总是确保只有有效的交易被包含在区块中。

将交易聚合到一个区块中

就像比特币网络中的任何完整节点一样,矿工会累积它收到的所有交易,并将它们添加到本地内存池中。矿工将开始构建一个候选区块,该区块可以通过包含区块中的一组交易来插入区块链。节点会确保在区块构建过程中每次有新区块到来时,候选区块中的所有交易都应该被忽略,因为这将创建重复的交易。

一旦为区块和交易创建了所有的元数据,矿工通过执行工作证明(Proof of Work)来解决哈希难题。一旦成功为区块创建了工作证明,该区块就会立即被广播到比特币网络中。

Coinbase 交易

比特币区块链中创建的每个区块都有一种特殊类型的交易,即区块的第一笔交易。这笔交易是由矿工创建的,用于奖励自己通过交易费和新创建的比特币,就像前面提到的那样。

这是比特币主网区块 520,956 的 coinbase 交易。解码该区块的第一笔交易将给出以下细节:

$ bitcoin-cli getrawtransaction fc72760e6339eb43111034d76e67ffce69f9f3a4a5aa53f29dfe7299623bbba8 
{ 
  "txid": 
"fc72760e6339eb43111034d76e67ffce69f9f3a4a5aa53f29dfe7299623bbba8", 
  "hash": 
"d06aecb12c942dfd059b0d6ef7fbb76f8310fb2cfd4159984c8dce32d3f94b8f", 
  "version": 2, 
  "size": 243, 
  "vsize": 216, 
  "locktime": 0, 
  "vin": [ 
    { 
      "coinbase": 
"03fcf20704ff5bea5a622f4254432e434f4d2ffabe6d6dcc95de16874f4618351fc3946c8590509f1c5b0ac2ce802d71786eaef2f0da4301000000000000006e9694143ddc55d500000000",
 "sequence": 4294967295 
    } 
  ], 
  "vout": [ 
    { 
      "value": 12.59334356, 
      "n": 0, 
      "scriptPubKey": { 
        "asm": "OP_DUP OP_HASH160 
 78ce48f88c94df3762da89dc8498205373a8ce6f OP_EQUALVERIFY 
 OP_CHECKSIG", 
        "hex": 
 "76a91478ce48f88c94df3762da89dc8498205373a8ce6f88ac", 
        "reqSigs": 1, 
        "type": "pubkeyhash", 
        "addresses": [ 
          "1C1mCxRukix1KfegAY5zQQJV7samAciZpv" 
        ] 
      } 
    },  
    { 
     "value": 0.00000000, 
      "n": 1, 
      "scriptPubKey": { 
        "asm": "OP_RETURN 
 aa21a9edcde65b6fb3a180d2d81bdaab66592c9c2deb778ca3b3464e31d5209737
 e67f1b", 
        "hex": 
 "6a24aa21a9edcde65b6fb3a180d2d81bdaab66592c9c2deb778ca3b3464e31d52
 09737e67f1b", 
        "type": "nulldata" 
      } 
    } 
  ] 
} 

Coinbase 交易在交易输入中没有任何指向未使用交易输出 (UTXO) 的引用,因为该金额是来自新币和交易费。交易输出中提到的地址是矿工自己的比特币地址,因此 coinbase 交易的所有资金都被完全转移到了矿工名下。

使用工作证明算法挖掘区块

工作证明算法在比特币中用于在比特币网络中达成关于属于区块链的区块的共识。它有助于在网络中的节点之间实现分类帐数据的一致性。工作证明算法可以创建证明一定量的工作已经被完成以创建区块。

比特币的工作证明是一个使用 SHA256 哈希函数寻找所需哈希值并解决难题的哈希难题。比特币中使用的工作证明算法类似于 第三章《区块链中的密码学》 中解释的算法。您可以参考 第三章《区块链中的密码学》来获取有关工作证明算法的实现和分析的更多细节。

挖矿池

我们知道,比特币中的挖矿难度会调整以保持平均区块创建时间为 10 分钟。但由于矿工之间的激烈竞争,解决哈希难题的难度随着时间的推移而增加。

这迫使矿工升级其硬件以实现更高的哈希率。拥有有限资源的矿工无法与拥有大量计算资源的矿工竞争。这就是引入矿池的时候,用于汇集个人有限资源矿工资源的时候。

一个矿池是矿工计算资源的集合,用于共享计算能力并获得更高的哈希率以解决哈希难题。如果矿池的组合哈希能力解决了一个区块的哈希难题,那么每个加入矿池的矿工都将根据其贡献的哈希能力获得奖励。在不同语言中实现了许多矿池。矿工可以加入任何一个矿池服务器并开始贡献哈希能力。Slush Pool 是最古老的矿池,曾被称为Bitcoin.cz Mining

矿池使用不同的协议在矿工和矿池服务器之间进行通信。getblocktemplate、getwork 和 stratum 协议是矿池中使用的一些挖矿协议。stratum 挖矿协议是一种广泛使用的协议,旨在取代 getwork 协议。

连接到矿池服务器的每个矿工都必须遵循一些步骤才能成功贡献到矿池挖矿中:

  • 矿工必须在开始工作之前使用正确的凭据进行授权。

  • 然后他们需要获取一个工作的交易集。

  • 最后,矿工需要将工作提交给服务器,并附上用户名和工作细节。

有几种方式可以分配矿工的份额。大多数矿池根据矿池创建的区块的矿工份额对矿工进行奖励。

哈希率是区块链挖矿中用于确定矿工的计算能力或哈希能力的单位。它只是每秒钟产生的哈希数量。

区块链

比特币中的区块链是一系列有序的区块列表,将上一个区块与哈希指针连接起来。比特币使用 Google 的LevelDB数据库存储区块链元数据。每个区块的身份是通过使用区块头的 SHA256 哈希值创建的,并且该区块哈希值存储在区块链中下一个区块的区块头中,以形成链接。

区块结构

比特币区块的数据结构包含一系列交易和关于该区块的元数据信息。一个区块由头部和体部组成,其中包含所有交易。一个区块平均包含 500 多个交易。

表 5.2 展示了区块中包括的所有字段。区块的主要部分是头和交易,占用的大小会有所不同。每个字段展示了区块中的大小或者交易所占用的大小:

字段描述大小
魔数值始终为 0xD9B4BEF94 字节
区块大小截至区块末尾的字节数4 字节
区块头包括六个项目80 字节
交易计数器整数计数1 - 9 字节
交易交易列表可变

表 5.2:区块结构

区块头

区块头存储了区块的元数据,其大小为 80 字节,如前表所示。它存储了版本信息,可以用来识别区块。hashPrevBlock存储了前一个区块的 256 位哈希值,将区块链接起来,并确保区块链的完整性。hashMerkleRoot是交易的哈希摘要,并确保了交易的完整性。

时间、难度位和随机数字段与工作量证明共识算法有关:

字段用途大小
Version区块版本4 字节
hashPrevBlock上一个区块头的 256 位哈希32 字节
hashMerkleRoot基于区块中所有交易的 256 位哈希32 字节
时间从 1970-01-01T00:00 UTC 以来的当前时间戳(秒)4 字节
Difficulty Bits以紧凑格式表示的当前目标(en.bitcoin.it/wiki/Target4 字节
Nonce32 位数字(从 0 开始)4 字节

表 5.3:区块头

创世区块

创世区块是比特币区块链中的第一个区块,由中本聪创建。它是静态编码的,以便每个运行比特币核心节点的人只会相信一个区块链状态。

可以通过获取第 0 个索引的区块哈希来获取创世区块的哈希:

$ bitcoin-cli getblockhash 0 000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f 

以下是比特币创世区块的详细信息。该区块中有一个 coinbase 交易,没有其他交易:

$ bitcoin-cli getblock 000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f 
{ 
  "hash": "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f", 
  "confirmations": 521239, 
  "strippedsize": 285, 
  "size": 285, 
  "weight": 1140, 
  "height": 0, 
  "version": 1, 
  "versionHex": "00000001", 
  "merkleroot": "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b", 
  "tx": [ 
    "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b" 
  ], 
  "time": 1231006505, 
  "mediantime": 1231006505, 
  "nonce": 2083236893, 
  "bits": "1d00ffff", 
  "difficulty": 1, 
  "chainwork": "0000000000000000000000000000000000000000000000000000000100010001", 
  "nextblockhash": "00000000839a8e6886ab5951d76f411475428afc90947ee320161bbf18eb6048" 
} 

区块的 coinbase 交易除了有其交易输入中的正常数据外,还有以下文本:

The Times 03/Jan/2009 Chancellor on brink of second bailout for banks

这证明第一个区块是在 2009 年 1 月 3 日或之后挖掘出来的。coinbase 交易也有一个输出交易,价值 50 比特币,就像任何其他 coinbase 交易一样。

默克尔树

比特币的区块头包含了总结区块中所有交易的元数据。这是通过使用一种称为默克尔哈希树的特殊类型树来创建摘要而实现的。正如在第二章 密码学概要 中所提到的,默克尔哈希树是一种用于总结大型数据集的二进制哈希树。默克尔树用于总结比特币中的所有交易,并确保交易的完整性。它们提供了一种高效的方法来验证交易是否包含在区块中。

默克尔树从叶子开始递归地对节点进行哈希,叶子是交易的哈希,直到只剩下一个哈希。这个哈希值总结了区块中的所有交易,称为默克尔根。比特币对交易的哈希应用了两次 SHA256 哈希函数。

当一个区块中有N个交易时,默克尔树确保只需2log2(N)*次计算就可以检查交易是否包含在区块中。这使得默克尔树实现成为一种非常高效的验证交易是否包含以及检查完整性的方式。在交易数量大的情况下,默克尔树非常高效。即使区块中的交易数量呈指数增长,由于默克尔树的二叉树性质,验证交易所需的路径始终是对数的。

区块链网络

比特币区块链网络由分散的节点组成,每个节点使用 P2P 网络协议与其他节点通信。每个运行比特币客户端的节点都有助于区块链和网络的增长。令人惊讶的是,比特币网络由处理多个公共区块链的节点组成。用于保存具有实际价值交易的主要区块链被称为主网。这是最长的区块链,参与节点数量最多。除了主网之外,比特币还有几个用于测试的其他区块链。目前,比特币有测试网segnetregtest区块链网络。

测试网

测试网是专为测试目的而创建的比特币区块链版本。测试网区块链与主网相同的网络上运行,具有钱包、交易和挖矿等功能。唯一的区别是,在测试网中流通的硬币没有任何货币价值。测试网被创建为一个测试网络,开发人员可以在将功能和修复部署到主网之前检查它们。在测试网上进行测试是至关重要的,因为由于分散化,不可能回滚主网区块链。测试网应该通过保持挖矿难度最低来使用轻量级配置,以便甚至可以使用简单的硬件进行测试。但人们倾向于在测试网络中使用高配置的硬件,这会增加挖矿难度。不时地,通过传播一个新的创世区块并重置难度来重新创建测试网。当前版本的测试网被称为测试网 3。

可以通过创建单独的守护进程使用比特币核心来创建一个测试网区块链:

$ bitcoind -testnet &

这将创建一个新的进程,它创建一个新的测试网区块链副本。测试网区块链比主网小得多。测试网区块链比主网区块链更快地同步所有区块。比特币的命令行界面通过类似的参数调用:

$ bitcoin-cli -testnet getinfo

2016 年,一个名为 segnet 的专用测试网络被创建,用于测试比特币的隔离见证功能。然而,由于 segnet 功能已经添加到测试网络 3 中,不再需要运行一个单独的网络。

回归测试

回归测试是用于回归目的的测试区块链。与测试网络不同,回归测试不是一个公共区块链。回归测试是一个可以由用户为本地测试目的创建的区块链。这对于测试不需要与网络进行大量交互的功能非常理想。您可以使用本地创世区块创建您自己的区块链版本。与测试网络类似,命令中添加了一个回归测试标志来启动进程:

$ bitcoind -regtest

由于区块链是一个本地副本,用户可以在不担心共识的情况下挖掘区块。以下命令在几秒钟内挖掘了 500 个区块,并且用户将在每个 coinbase 交易中获得硬币作为奖励:

$ bitcoin-cli -regtest generate 500  

比特币硬分叉和另类币

比特币的硬分叉是协议的更新,不会支持旧协议,因此需要所有人升级。硬分叉升级通常包括重大更改,如改变区块链结构、交易或共识规则。软分叉和硬分叉之间的主要区别在于后者不向后兼容,这意味着旧系统在更新的协议中将无法运行。

区块链硬分叉后将会有两个不同版本的区块链。区块链节点之间对遵循单一协议的分歧导致了多个版本的区块链。区块链硬分叉通常会导致协议升级。比特币曾经有过几次硬分叉,这导致了比特币分叉加密货币的诞生。比特币现金是第一个成功的硬分叉加密货币,于 2017 年 8 月 1 日在比特币的第 478,558^(th)个区块上分叉。比特币现金主要是为了增加区块大小到 8 MB 而创建的。比特币黄金和比特币私人是随后跟随比特币现金的另外两个成功的硬分叉。

替代币,或者另类币是在比特币成功之后推出的加密货币。另类币是在单独的区块链上创建的,不像比特币的硬分叉加密货币。大多数另类币使用比特币提供的基本框架,并尝试解决其现有的限制。其中一些币尝试通过使用替代的工作证明算法来增加交易速度,而另一些币尝试通过增加交易的匿名性来增强安全性。

Namecoin 是最初基于比特币的知名替代币之一。莱特币、Zcash、狗狗币和以太坊是跟随 Namecoin 的一些货币。莱特币是对比特币最接近的实现,并且被认为是比特币的银子。莱特币的总供应量为 8400 万枚,是比特币的四倍。它还通过减少块创建时间来提高交易速度。莱特币使用一种内存密集型的工作证明算法,称为 Scrypt。

自比特币发明以来已经创建了数千种替代币,并且这个数量每天都在增长。然而,到目前为止,比特币是最广泛使用的加密货币。

一个简单的加密货币应用程序

创建一个加密货币应用程序将使我们能够实现我们到目前为止所学的所有区块链概念,以及比特币中使用的交易结构,然后我们可以将其部署在完全的 P2P 网络中。我们在第四章,区块链中的网络中创建了一个在去中心化的 P2P 网络中的区块链应用程序。我们将使用相同的应用程序来创建和传播网络中的区块,但还将通过交易和钱包的概念扩展该应用程序,以创建一个完全去中心化的加密货币:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5.5:连接应用程序所有组件的流程图

图 5.5显示了如何通过添加钱包和交易功能来扩展加密货币应用程序。我们将逐步了解每个组件的实现以理解其功能。

交易

我们创建了一个应用程序,在该应用程序中,块是用数据创建的,而没有验证数据的内容。我们将通过限制块仅接受交易数据来扩展此功能。交易数据类似于本章中所涵盖的内容。它由输入和输出组件组成。输出指定了交易的接收方,而输入确保用户拥有足够的资金以便交易成功进行。输入引用了现有的未花费输出。

交易输出

交易输出具有只接受接收方地址和交易金额的结构。该地址是椭圆曲线密码学ECC)密钥对的公钥对应物:

class TxOut: 
    def __init__(self, address, amount): 
        self.address = address 
        self.amount = amount 

交易输入

交易输入提供有关将通过引用可消费交易输出而花费的资金的信息:

class TxIn: 
    def __init__(self, tx_out_id, tx_out_index, signature): 
        self.tx_out_id = tx_out_id 
        self.tx_out_index = tx_out_index 
        self.signature = signature 

tx_out_idtx_out_index用于引用交易输出,而签名提供了支出者是资金的合法所有者的证明。与比特币不同,我们没有使用类似于脚本的语言来锁定和解锁交易。交易验证将通过使用椭圆曲线数字签名算法ECDSA)简单地验证签名来执行。

交易结构

交易是有效交易输入和输出的集合,如下所示:

class Transaction: 
    def __init__(self, tx_ins, tx_outs, tx_id=None): 

        self.tx_ins = tx_ins 
        self.tx_outs = tx_outs 
        self.id = tx_id if tx_id else get_transaction_id(self) 

交易 ID 是从整个交易的摘要派生的。使用SHA256哈希函数来计算连接的交易输入和输出内容的摘要,如下所示:

def get_transaction_id(transaction): 

    tx_in_content = reduce(lambda a, b : a + b, map( 
        (lambda tx_in: str(tx_in.tx_out_id) + str(tx_in.tx_out_index)), transaction.tx_ins), '') 

    tx_out_content = reduce(lambda a, b : a + b, map( 
        (lambda tx_out: str(tx_out.address) + str(tx_out.amount)), transaction.tx_outs), '') 

    return SHA256.new((tx_in_content + tx_out_content).encode()).hexdigest() 

UTXO

交易输入将始终引用 UTXO。本地维护着一个 UTXO 列表。在处理交易时更新此列表,并在交易验证期间引用该列表。尽管可以通过遍历整个区块链随时生成此列表,但为了方便快速交易验证,它被维护在内存中:

class UnspentTxOut: 
    def __init__(self, tx_out_id, tx_out_index, address, amount): 
        self.tx_out_id = tx_out_id 
        self.tx_out_index = tx_out_index 
        self.address = address 
        self.amount = amount 

UTXO 列表是一个简单的列表,最初是通过处理创世交易来创建的。

self.unspent_tx_outs = process_transactions([self.genesis_transaction], [], 0)

每当节点创建或接收交易时,它会在处理过程中更新未花费的交易输出。从新添加的交易中计算一组新的 UTXO 如下:

def update_unspent_tx_outs(a_transactions, a_unspent_tx_outs): 

    def find_utxos(t): 
        utxos = [] 
        for index, tx_out in enumerate(t.tx_outs): 
            utxos.append(UnspentTxOut(t.id, index, tx_out.address, tx_out.amount)) 
        return utxos 

    new_utxos = reduce((lambda a, b: a + b), map(lambda t: find_utxos(t), a_transactions), []) 

所有在交易输入中引用的 UTXO 都被收集为已消耗的 UTXO:

    consumed_utxos = list(map(lambda txin: UnspentTxOut(txin.tx_out_id, txin.tx_out_index, '', 0), 
        reduce((lambda a, b : a + b), map(lambda t: t.tx_ins, a_transactions), []))) 

更新后的 UTXO 列表是通过添加新创建的 UTXO 并删除所有已消耗的 UTXO 来创建的:

    resulting_utxos = list(filter(lambda utxo : not find_unspent_tx_out(utxo.tx_out_id, utxo.tx_out_index, consumed_utxos), a_unspent_tx_outs)) + new_utxos 
    return resulting_utxos 

交易验证

每当节点接收到交易池或新的区块时,在将交易数据存储到本地区块链之前,所有交易都会经过验证。通过检查每个字段的数据结构来测试每个交易的结构。还会验证交易的输入和输出,以确保不包含任何无效的输入或输出:

def validate_transaction(transaction, a_unspent_tx_outs): 

    if not is_valid_transaction_structure(transaction): 
        return False 

如果交易结构无效,则交易被拒绝:

    if get_transaction_id(transaction) != transaction.id: 
        print('invalid tx id: ' + transaction.id) 
        return False 

本应用中的交易 ID 是使用 SHA256 哈希函数计算的,这证明了交易的完整性。如果 ID 被篡改且未通过完整性检查,则交易被视为无效:

    has_valid_tx_ins = reduce((lambda a, b: a and b), map(lambda tx_in: validate_tx_in(tx_in, transaction, a_unspent_tx_outs), transaction.tx_ins), True) 

    if not has_valid_tx_ins: 
        print('some of the tx_ins are invalid in tx: ' + transaction.id) 
        return False 

通过检查交易是否引用了有效的 UTXO 以及是否具有用 UTXO 中提到的公钥的有效签名来验证交易输入:

    total_tx_in_values = reduce((lambda a, b : a + b), 
        map(lambda tx_in : get_tx_in_amount(tx_in, a_unspent_tx_outs), transaction.tx_ins), 0) 

    total_tx_out_values = reduce((lambda a, b : a + b), 
        map(lambda tx_out : tx_out.amount, transaction.tx_outs), 0) 

    if total_tx_out_values != total_tx_in_values: 
        print('total_tx_out_values !== total_tx_in_values in tx: ' + transaction.id) 
        return False 
    return True 

通过对输出和输入金额求和来将总交易输出金额与总交易输入金额进行比较。如果输入金额与输出金额不匹配,则交易被视为无效。在比特币中,由于交易费用,交易输出始终低于交易输入。此金额包含在 coinbase 交易中。由于本应用中没有交易费用,输入和输出金额应始终匹配。

交易签名

交易签名是一个至关重要的过程,用于解锁资金,使其可以转移到新的所有者手中。每个交易输入都包括一个签名字段,其中包含由引用交易输出资金的所有者签名的签名。每个交易输入都对交易 ID 进行签名,这确保了不能篡改任何交易输入的交易 ID,因为交易 ID 是整个交易的摘要。修改任何输入都将使所有签名无效。

我们将使用软件包来执行事务信息的序列化、签名和验证:

import binascii 
from ecdsa import SigningKey, VerifyingKey, SECP256k1 

尽管在整本书中我们一直将pycryptodome作为密码学的核心库,但由于其对数字签名的独家支持,我们将在本应用程序中使用ecdsa软件包进行数字签名。

为了查找公钥以验证签名者是否是资金的所有者,必须获取引用的 UTXO:

def sign_tx_in(transaction, tx_in_index, private_key, a_unspent_tx_outs): 

    tx_in = transaction.tx_ins[tx_in_index] 
    data_to_sign = str(transaction.id) 
    referenced_utxo = find_unspent_tx_out(tx_in.tx_out_id, tx_in.tx_out_index, a_unspent_tx_outs) 
    if referenced_utxo is None: 
        print('could not find referenced txOut') 
        return False 

签名者的公钥将与引用的 UTXO 公钥进行比较,以检查签名者是否被授权对交易输入进行签名:

    referenced_address = referenced_utxo.address 
    if get_public_key(private_key) != referenced_address: 
        print('trying to sign an input with private' + ' key that does not match the address that is referenced in tx_in') 
        return False 

最后,使用用户的私钥对交易 ID 进行签名。签名使用secp256k1曲线的 ECDSA 创建。有关使用 secp256k1 曲线进行 ECDSA 签名和验证的更多细节,请参阅第二章《密码学的一点知识》:

    sk = SigningKey.from_string(private_key, curve=SECP256k1) 
    signature = binascii.b2a_hex(sk.sign(data_to_sign.encode())).decode() 
    return signature 

钱包

就我们所知,钱包通过存储用户的私钥来保管资金的所有权。

钱包的实现只能为我们提供抽象操作,例如查看用户账户余额并向其他用户发送资金。钱包通常被认为是终端用户应用程序,适用于那些不愿或不需要了解交易内部实现的人。

密钥管理

我们将实现密钥对的生成,并将密钥以明文形式存储,即不应用任何加密。虽然钱包可以存储多个密钥,也可以由种子短语生成,但在本应用中,我们将每个钱包使用单个密钥,以使钱包的实现尽可能简单。以下方法将读取私钥并将十六进制转换为字节表示:

PRIV_KEY_LOC = 'private_key' 
from ecdsa import SigningKey 

def generate_private_key(): 
    sk = SigningKey.generate(curve=SECP256k1) 
    with open(PRIV_KEY_LOC, 'wt') as file_obj: 
        file_obj.write(binascii.b2a_hex(sk.to_string()).decode()) 

通过使用 ecdsa 软件包创建私钥来初始化钱包。SigningKey 类具有 generate 方法,用于在 ecdsa 中创建签名密钥。然后将此密钥转换为十六进制格式,然后存储在文件中。

def get_private_from_wallet(): 
    return binascii.a2b_hex(open(PRIV_KEY_LOC).read()) 

公钥可以随时通过私钥生成。以下方法通过读取原始私钥创建一个SigningKey对象。该对象可以生成一个验证密钥,即公钥:

def get_public_from_wallet(): 
    sk = SigningKey.from_string(get_private_from_wallet(), 
curve=SECP256k1) 
    vk = sk.get_verifying_key() 
    return binascii.b2a_hex(vk.to_string()).decode() 

钱包余额

在加密货币中拥有资金归结为声明寄送给用户的交易输出。钱包的余额通过收集所有地址与用户私钥的公钥对应的 UTXO 来计算。

由于我们的应用程序每个钱包维护一个单独的私钥,用户的所有 UTXO 将被引用到一个单独的公共地址上,这在用户拥有的 UTXO 将被寻址到多个地址的实现中并非如此。下面的方法找到与用户公钥匹配的地址中指定的所有资金金额的总和:

def get_balance(address, unspent_tx_outs): 
    return sum(map(lambda utxo : utxo.amount, find_unspent_tx_outs(address, unspent_tx_outs))) 

在我们的应用程序中,公共地址是一个公钥。这在其他加密货币应用程序中并非如此,它们可能使用锁定和解锁脚本,并且公共地址是通过使用哈希函数从公钥生成的。

创建交易

创建交易的过程只是构造一个具有有效的交易输入和输出的交易对象,以满足用户的交易请求。

消耗 UTXO

要创建用于转移资金的交易,您需要组合一个或多个 UTXO,就像收集硬币或现金支付给某人一样。图 5.6 显示了如何将两个值为 40 和 10 的 UTXO 组合以创建一个输出值为 45 的交易输出以支付给其他用户。剩余的输出值为 5,称为找零金额,这将以类似于在商店支付时收到找零的方式返回给交易的创建者:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5.6:从可用 UTXO 创建新的输出交易输出

用户钱包选择一组可以用于花费特定金额的 UTXO。所选 UTXO 的总值将始终等于或大于所需金额。下面的方法串行遍历并选择所有足以满足请求金额的交易输出。计算剩余金额,这是创建新交易输出所需的金额,并将其发送给交易创建者:

def find_tx_outs_for_amount(amount, my_unspent_tx_outs): 
    current_amount = 0 
    incl_unspent_tx_outs = [] 
    for my_unspent_tx_out in my_unspent_tx_outs: 
        incl_unspent_tx_outs.append(my_unspent_tx_out) 
        current_amount = current_amount + my_unspent_tx_out.amount 
        if current_amount >= amount: 
            left_over_amount = current_amount - amount 
            return incl_unspent_tx_outs, left_over_amount 
    e_msg = 'Cannot create transaction from the available unspent transaction outputs.' \
            ' Required amount:' + str(amount) + '. Available unspent_tx_outs:' +          
            json.dumps(my_unspent_tx_outs)
    print(e_msg)
    return None, None 

构造交易

通过构造有效的交易输入和输出来创建交易。可消耗的 UTXO 将通过 find_tx_outs_for_amount 方法获取,如前一节所述。将为这些 UTXO 创建交易输入。剩余金额用于创建一个找零交易:

def create_transaction(receiver_address, amount, private_key,
                                      unspent_tx_outs, tx_pool): 

    my_address = get_public_key(private_key) 

    my_unspent_tx_outs_a = list(filter(lambda utxo: utxo.address == my_address, unspent_tx_outs)) 

    my_unspent_tx_outs = filter_tx_pool_txs(my_unspent_tx_outs_a, tx_pool) 

用户的未花费交易将被过滤,并将在交易输入中引用:

    incl_unspent_tx_outs, left_over_amount = find_tx_outs_for_amount(amount, my_unspent_tx_outs) 
    if not incl_unspent_tx_outs: 
        return None 

交易输入是通过最初保持签名字段为空的 TxIn 类创建的:

        def to_unsigned_tx_in(unspent_tx_out): 
            tx_in = TxIn(unspent_tx_out.tx_out_id, unspent_tx_out.tx_out_index, '') 
            return tx_in 

        unsigned_tx_ins = list(map(to_unsigned_tx_in, incl_unspent_tx_outs)) 

交易是使用 create_tx_outs 方法创建的未签名交易输入和输出值。此方法创建了接收方和找零交易输出:

        tx = Transaction(unsigned_tx_ins,
            create_tx_outs(receiver_address, my_address, amount, left_over_amount))

最后,未签名的交易输入由钱包所有者使用私钥签名:

        def sign_transaction(tx, index): 
            tx.tx_ins[index].signature = sign_tx_in(tx, index, private_key, unspent_tx_outs) 

        for index, txIn in enumerate(tx.tx_ins): 
            sign_transaction(tx, index) 
        return tx 

交易管理

一旦交易创建完成,它们应当被包含在区块链中,以更新全局交易状态。尚未被包含在区块链中的交易被称为未确认交易。未确认交易总是被本地存储在一个名为交易池的池中。这与比特币的内存池相同。

交易池

用户和其他节点创建的所有未确认交易都包含在交易池中。交易池可以是一个本地文件或一个内存池。我们将在内存中维护一个存储所有交易的列表:

transaction_pool = [] 

当节点创建一个交易时,它会在广播之前将交易添加到本地交易池中。以下send_transaction方法在创建交易后将交易添加到池中:

def send_transaction(self, address, amount): 
    tx = create_transaction(address, amount, get_private_from_wallet(),
     self.get_unspent_tx_outs(), get_transaction_pool()) 
    add_to_transaction_pool(tx, self.get_unspent_tx_outs()) 
    return tx 

这些交易在未被包含在区块链中时会保留在交易池中。一旦交易被包含在一个区块中,交易池就需要更新。每当节点接收到一个新区块时,节点都会更新其交易池。当未能找到交易输入引用的 UTXO 时,以下方法会从池中移除该交易:

def update_transaction_pool(unspent_tx_outs): 
    global transaction_pool 
    for tx in transaction_pool[:]: 
        for tx_in in tx.tx_ins: 
            if not has_tx_in(tx_in, unspent_tx_outs): 
                transaction_pool.remove(tx) 
                print('removing the following transactions from txPool: %s' % json.dumps(tx)) 
                break 

缺失的 UTXO 表示该交易已被包含在区块链中,因此该交易可以从池中删除。

广播

用户节点可以为交易挖掘一个区块,也可以将交易传播到区块链网络,以便其他节点可以挖掘这些交易。到目前为止,我们的区块链应用程序只通信了区块信息。由于并非所有交易创建者都想亲自挖掘区块,交易需要与节点进行通信。我们将向应用程序添加另外两种消息类型。更多细节请参考第四章中描述的消息类型,区块链中的网络

这些是节点之间将要交换的查询和响应消息的格式。与区块的广播类似,当节点创建新的交易或从其他节点接收到未确认交易时,交易将被广播。节点首次连接到另一个节点时,将广播一个交易池查询消息:

QUERY_TRANSACTION_POOL = 3 
RESPONSE_TRANSACTION_POOL = 4 
def query_transaction_pool_msg(self): 
    return { 
                'type': QUERY_TRANSACTION_POOL, 
                'data': None 
    } 

def response_transaction_pool_msg(self): 
    return { 
                'data': JSON.dumps(get_transaction_pool()) 
    } 

区块链

虽然应用程序的区块链部分与第四章中创建的应用程序非常相似,但我们由于交易数据结构的引入添加了一些功能。

挖掘没有交易的区块链是直截了当的;它只涉及构建一个带有头部和数据的区块。但是当任意数据被替换为交易数据时,节点需要从本地交易池中获取交易:

def construct_next_block(self): 
    coinbase_tx = get_coinbase_transaction(get_public_from_wallet(), self.blocks[-1].index + 1) 
    block_data = [coinbase_tx] + get_transaction_pool() 
    return self.generate_next_block(block_data) 

此方法构建和挖掘一个区块的数据,其中包含一个 coinbase 交易和来自池的交易。 一旦验证了块头和交易,该块将被添加到区块链中。

应用程序端点

如前所述,应用程序具有用于管理节点的 HTTP 接口以及用于节点之间的 P2P 通信的 WebSocket 接口。 以下是管理节点所需的一些端点:

  • /blocks

  • /block/<hash>

  • /mineBlock

  • /transaction/<id>

  • /sendTransaction

sendTransaction 端点基本上会创建交易并将其添加到池中,如事务管理部分所述。 未确认的交易通过使用 /mineBlock 端点被包括在区块链中。

以下输出显示了在执行任何其他交易之前区块链的状态,因为它包含了一个创世区块:

[ 
  { 
    "data": { 
      "id": "baeece2d8e57aef79ef4e693df0485ca8938ad1f27fa9a0426c8788a3802f02f", 
      "tx_ins": [ 
        { 
          "signature": "", 
          "tx_out_id": "", 
          "tx_out_index": 0 
        } 
      ], 
      "tx_outs": [ 
        { 
          "address": "0ae66e6adc350ec5c7961cc59cb53372dd421447d4d1b6d11ef8637ac21972068
 8f8019485ac751414049162f1a71c1cc86c4e58bffb836a0d2eea3f324708df", 
          "amount": 50 
        } 
      ] 
    }, 
    "difficulty_bits": 0, 
    "hash": "816534932c2b7154836da6afc367695e6337db8a921823784c14378abed4f7d7", 
    "index": 0, 
    "nonce": 0, 
    "previous_hash": "0", 
    "timestamp": 1465154705 
  } 
] 

一旦通过将其插入到区块链中确认了交易,就可以通过向 blockstransaction 端点发送 HTTP GET 请求来查看它。 下面是当使用交易 ID 请求交易端点时抛出的交易结果:

{ 
  "data": { 
    "id": "ac3d108ebbde3b657a5875ff4237682decf530e6dd6c4b7a77711b89e23a8618", 
    "tx_ins": [ 
      { 
        "signature": "901ea472a28294280fb7468fbc61efa0ddc5a98e375d022b4b7724a4184325c4c
 2182c1091b493aec69f7ef81d912648a9e29b7941651c5fd660f72764698383", 
        "tx_out_id": "baeece2d8e57aef79ef4e693df0485ca8938ad1f27fa9a0426c8788a3802f02f", 
        "tx_out_index": 0 
      } 
    ], 
    "tx_outs": [ 
      { 
        "address": "0ae66e6adc350ec5c7961cc59cb53372dd421447d4d1b6d11ef8637ac21972068
 8f8019485ac751414049162f1a71c1cc86c4e58bffb836a0d2eea3f324708d2", 
        "amount": 20 
      }, 
      { 
        "address": "0ae66e6adc350ec5c7961cc59cb53372dd421447d4d1b6d11ef8637ac21972068
 8f8019485ac751414049162f1a71c1cc86c4e58bffb836a0d2eea3f324708df", 
        "amount": 30 
      } 
    ] 
  } 
} 

这笔交易使用了创世区块的交易输出,其价值为 50。由于创建了总价值为 20 个币的交易,剩余的 30 个币被发送回了所有者。

可以在 GitHub 仓库中找到整个应用程序的代码。 由于这里没有描述应用程序的所有组件,请参阅仓库中的代码以了解和执行实现。 您还将在该应用程序中找到一个区块浏览器和钱包 UI。

摘要

由于区块链的概念源自加密货币,理解区块链技术的真正实现方式最好通过加密货币应用程序。 在本章中,我们通过比特币来帮助理解加密货币在分散网络中的功能,覆盖了加密货币的所有概念。

我们从比特币的基础知识开始,了解了它们是如何组成一个分散网络的。 然后,我们深入探讨了比特币中使用的密钥、地址和钱包等概念。 比特币交易也被深入探讨了,因为这些是为比特币网络带来价值的事件。 我们还深入探讨了区块链的本质,包括比特币的挖矿和共识。 最后,我们通过创建一个简单的加密货币应用程序来总结了本章。

本章作为参考,因为它详细阐述了部署基本加密货币所使用的大多数概念。 现在我们已经熟悉了分散应用程序的关键概念,我们将在下一章中通过使用现有平台创建应用程序来深入探讨区块链。

第六章:深入区块链 - 存证

到目前为止,在本书中,我们已经了解了区块链技术的基本概念,探讨了诸如加密和去中心化网络等主题。我们还创建了一个简单的区块链应用程序,并使自己熟悉了在去中心化加密货币应用程序中使用的交易。虽然我们创建的区块链应用程序使我们对区块链技术有了概览,但我们还没有探讨除加密货币之外需要去中心化网络的任何用例。通过深入了解区块链,我们将介绍并熟悉区块链框架,最终将通过构建所展示的用例而结束。

在这一章中,我们将涵盖以下主题:

  • 具体来说,区块链平台:

    • 为什么我们选择使用 MultiChain?

    • MultiChain 基础介绍

    • MultiChain 中包含的功能

  • 如何设置区块链环境

  • 存证的体系结构

  • 如何构建一个存证应用

在我们探索 MultiChain 区块链平台的各个方面之前,了解核心区块链平台是很重要的。任何想要构建去中心化应用程序的用户都不需要从零开始构建所有组件。

相反,你会发现使用现有框架总是更好。为什么呢?因为它会帮助你以较少的努力构建应用程序。现有的区块链平台为应用程序开发提供了一个框架,你不必担心使用的基础区块链概念,而是可以更多地专注于实现区块链用例。用户不必过多担心以这种方式构建的区块链网络的扩展性,因为该平台已经通过成千上万的开发人员和用户进行了测试。因此,这个系统应该是具有弹性的。

每个区块链平台都有其独特的特点和功能,超越了使用区块链技术构建去中心化网络的基础知识。一些区块链平台提供了从比特币项目派生的基本功能,而其他平台则提供了高级的脚本能力,以便在区块链网络内部部署智能应用。你会发现有大量的平台供你选择,从而开发和部署应用程序,但总是最好根据你正在创建的应用程序的用例选择一个框架。一些提供开发应用程序平台的著名区块链项目有以太坊超级账本NeoMultiChainCordaBigchainDB。清单很长,我们将在第八章 区块链项目中查看其中的一些平台。

由于我们有大量选择,很难找到最佳平台,因为大多数平台都可以成功地用于我们要使用的相同用例。然而,每个平台都是为特定目的而设计的。我们将在第十二章,区块链用例中指出区块链平台的选择标准,在那里我们将讨论几个区块链用例。在整个本章中,我们将讨论使用 MultiChain 构建“存在性证明”应用的用例。我们将在接下来的几节中讨论选择这个平台的理由。

MultiChain 区块链平台

MultiChain 是许多平台之一,帮助企业轻松构建和部署区块链应用程序。众所周知,比特币拥有一个坚韧的公开区块链,可以扩展其网络和处理交易,非常适合公开区块链。这是通过从比特币获得灵感并创建一个私人区块链平台实现的 MultiChain 项目。

在比特币的公开区块链中,存在一些限制,比如资产分发有限、交易成本高、交易速率较低和透明交易。虽然很难在公开区块链中摆脱这些限制,但并非所有用例都需要承担这些限制。

在私人网络中可以实现的一个用例不应当为每笔交易付费,达到更高的交易速度,甚至为操作设置访问控制。MultiChain 可以在私人网络内实现所有这些。

下面是 MultiChain 的一些特点,帮助它克服比特币中的问题,使其无法在企业作为私人区块链实现一般用例:

  • 区块链资产创建没有限制。这是因为它将由组织来设定上限。

  • 你不必支付交易成本。这是因为内部节点无需奖励。

  • 交易确认延迟减少是由工作量证明共识算法导致的。

  • 在区块链交易中,隐私性不足得到缓解。这是通过为区块链节点提供访问控制来实现的。

为什么选择 MultiChain?

正如我们已经提到的,当我们实施区块链应用程序时,有几个区块链平台可供选择。框架的选择大部分取决于我们将要实施的应用程序的用例。

选择 MultiChain 平台而非其他平台有几个原因。其中一个最重要的因素是在平台内实现我们的存证用例的简单性。MultiChain 帮助我们构建我们的用例,而无需为部署和执行编写任何复杂逻辑,使其更易访问。MultiChain 还有一个称为数据流的功能,将用于将信息存储在区块链中,而无需改变数据结构。我们将通过查看本章的后续章节,开始使用 MultiChain,来实现这一点。选择 MultiChain 的另一个因素是它与比特币非常相似,这使我们更容易理解所有其扩展功能。

所有这些因素影响我们选择 MultiChain 作为构建我们第一个区块链应用的合适平台。接下来我们将介绍一些 MultiChain 的特点。

MultiChain 的基础知识

MultiChain 是从比特币分叉出来的项目;因此这使其与比特币生态系统兼容。这是基于权限的区块链,意味着在区块链上执行的任何操作都是受权限控制的。网络上的节点并不一定对区块链具有相同的权限。一些节点可能被分配基本权限以读取区块链,其他节点则可被赋予写权限甚至管理员权限。MultiChain 也可以配置为无权限,使网络中的每个节点平等。MultiChain 的灵活性使得实现区块链用例变得容易,无需投入过多开发资源。

MultiChain 为我们提供完整的资产管理周期,类似于比特币交易。资产为我们提供了一种灵活的方式来处理其元数据。由于我们的存证用例不涉及身份,我们不会使用资产管理概念来创建我们的应用程序。MultiChain 还提供了数据存储与检索机制,借助数据流的帮助。在我们的示例中,我们将使用数据流功能进行 Proof of Existence 应用程序的数据存储。

MultiChain 功能

正如我们之前解释过的,MultiChain 继承了大部分功能来自比特币项目,并帮助开发人员创建应用程序而无需学习全新的生态系统。MultiChain 具有一系列额外功能,使开发者轻松构建和部署区块链应用程序。在本节中,我们将谈论其中一些功能。

权限管理

当 MultiChain 区块链在企业中作为私人网络部署时,可以进行配置,使每个节点具有不同级别的访问控制权限。当区块链网络中启用权限模式时,每个节点都必须使用其公共地址明确授权。权限级别包括连接、发送、接收、发行、挖矿、激活和管理员等。权限还可以针对特定资产分配,从而使权限管理更加细粒化。节点可以随时撤销访问权限。权限管理确保没有陌生节点被允许进入私人区块链,或者通过为不同节点设置不同级别的访问控制来建立组织中的等级结构。权限管理是私人区块链中的一个重要功能。

资产管理

资产管理是从比特币交易中衍生出来的概念。比特币有一个由交易验证的单一资产。尽管比特币可以在交易元数据中存储额外资产,但这些资产不会由区块链节点验证。MultiChain 通过提供一种功能来解决这个问题,使您能够创建多种类型的资产,并仍然验证所有资产的交易。MultiChain 具有完整的资产管理生命周期。

数据流管理

数据流是 MultiChain 区块链中用于提供数据存储的一种机制。它作为一种方便的方式来以键值对的形式存储和检索数据。多个项目可以发布到单个数据流中。在对数据流进行操作之前,节点必须订阅该数据流。数据流项目可以按键、签名和区块号等进行索引。

设置区块链环境

区块链网络是一个去中心化的网络,每个节点都应该拥有关于区块链账本的类似信息。去中心化网络可以通过允许每个人连接并在区块链上执行操作来在开放网络中设置,或者同样也可以在私人网络中维护。通过在每个节点上启用连接权限,可以实现公共区块链网络。企业通常更愿意建立私人网络,因为这有助于阻止不良行为者。这个网络配置可以在每个 MultiChain 节点中轻松配置。

运行 MultiChain 节点

MultiChain 可以安装在 Linux、Windows 和 Mac 平台上,这些平台的 64 位处理器至少需要 512 MB 内存和 1 GB 存储空间。安装过程包括从 MultiChain 网站下载压缩的编译文件并进行解压缩。MultiChain 是用 C++ 开发的开源项目。每个节点都可以使用开源代码并进行编译,以便对程序的逻辑有更多控制。

可以在书籍的 GitHub 存储库或 MultiChain 的官方网站找到直接在机器上安装 MultiChain 以及从源代码构建的安装说明:www.multichain.com/download-install。我们将在本书中演示使用 Linux 发行版 Ubuntu 16.04。

每个 MultiChain 节点都带有三个主要的二进制文件,称为multichaindmultichain-climultichain-util

  • multichaind:这是每个节点上作为守护程序运行的进程。该进程是节点的支柱,并启动保持本地区块链最新的所有必需任务。

  • multichain-cli:这提供了一个命令行接口,您可以使用它执行 API 来对区块链执行操作。

  • multichain-util:这是一个工具,您可以使用它执行操作,比如创建一个新的区块链。

开始使用 MultiChain

现在我们已经熟悉了 MultiChain 平台,同样熟悉了在私有网络中设置节点的流程,我们需要创建一个区块链,以便在网络中的节点之间发布和共享数据。第一步是在实施我们的用例之前介绍所有功能。

创建一个链

设置了一个节点后,它可以通过连接到链来加入现有网络,或者创建自己的链。使用multichain-util创建新链,如下所示:

$ multichain-util create chain1  

这将创建一个新的本地区块链。然后,节点必须使用multichaind启动一个进程来连接到创建的链。可以通过启动多个multichaind守护进程在单台机器上初始化多个链。创建multichaind进程如下:

$ multichaind chain1 -daemon 

上面的行实例化一个进程并启动服务器。然后,节点为创建的链挖掘创世区块。此代码将产生一个地址,其他节点可以使用该地址连接到刚刚创建的链。

连接到现有链

如果在私有网络中创建了一个链,其他节点可以连接到创建的节点并在同一区块链上执行操作。任何节点都可以使用以下命令连接到链:

$ multichaind chain1@[ip-address]:[port] 

网络中的任何远程节点都可以使用 IP 地址和 MultiChain 端口连接到链。每个 MultiChain 守护进程为其服务器分配一个不同的端口号。如果链配置的连接权限未设置为公开,则必须对网络中的每个节点进行明确授予权限,如下所示:

$ multichain-cli chain1 grant [node-address] connect 

节点地址是节点的公共地址或钱包地址,可从钱包的公私钥对的公钥中提取。节点可以尝试通过重新启动multichaind连接到链,但只有获得授权后才能这样做:

$ multichaind chain1 -daemon 

检查区块链

在成功连接到区块链后,节点将完全设置在私有区块链网络中。本地区块链将通过接受来自网络中节点的区块来进行更新。可以通过命令行界面发出以下命令来验证区块链的状态:

$ multichain-cli chain1 getinfo 

此命令提供有关节点、MultiChain 和几个区块链参数的常规信息,如下所示:

    {"method":"getinfo","params":[],"id":1,"chain_name":"chain1"}

    {
      "version": "1.0.2",
      "nodeversion": 10002901,
      "protocolversion": 10009,
      "chainname": "chain1",
      "description": "MultiChain chain1",
      "protocol": "multichain",
      "port": 4273,
      "setupblocks": 60,
      "nodeaddress": "chain1@192.168.0.107:4273",
      "burnaddress": "1XXXXXXXQrXXXXXXEeXXXXXXXBXXXXXXaDTujx",
      "incomingpaused": false,
      "miningpaused": false,
      "walletversion": 60000,
      "balance": 0,
      "walletdbversion": 2,
      "reindex": false,
      "blocks": 59,
      "timeoffset": 0,
      "connections": 0,
      "proxy": "",
      "difficulty": 6e-8,
      "testnet": false,
      "keypoololdest": 1523352447,
      "keypoolsize": 2,
      "paytxfee": 0,
      "relayfee": 0,
      "errors": ""
    }

注意:所有的 MultiChain 命令都可以通过最初使用multichain-cli chain1命令启动 shell 以交互模式运行。这将打开一个界面,在此界面上可以使用关键字和所需的参数执行所有命令。可以通过输入help来获取所有命令的完整列表。

使用流进行工作

如前所述,流用于将数据项存储为区块链中的键值对。流是一种方便的数据存储方式。可以通过命令行界面轻松创建和管理它们。这里使用的所有命令都是在执行以下命令进入交互模式后执行的:

multichain-cli chain1

用户可以通过发出liststreams命令来检查链中的所有流,该命令返回所有流的详细信息以及一个名为root的默认流。然后可以通过执行以下命令来创建新流:

    create stream stream1 false 

stream1是新创建的流的名称。如果将create命令最初设置为 false,则只有管理员和具有显式权限的节点才能为stream1创建流项目。如果初始时将create命令设置为 false,则可以使用grant命令将发布流项目的权限授予特定节点。

也可以使用以下publish命令将键值对数据项发布到创建的流中。流项的值应始终指定为十六进制字符串:

    publish stream1 key1 73747265616d2064617461

每当节点想要监听发布的流项目时,它都必须通过订阅来监听该流;可以通过以下命令实现:

    subscribe stream1
    liststreamitems stream1

执行该命令将导致所有发布到流中的项目都被显示出来,同时还会显示发布者地址、区块创建时间、交易 ID 以及一定数量的区块确认信息。发布的十六进制值存储在data键中:

[ 
  { 
    "publishers": [ 
      "1MpkvCWj1Z9ZYfzBQzk4QvR1qih4ZiaHfh9Dd3" 
    ], 
    "key": "key1", 
    "data": "73747265616d2064617461", 
    "confirmations": 11, 
    "blocktime": 1523373741, 
    "txid": "23ad75620539f9995eef990856090e4c016e4da46bee82905483021b68da616e" 
  } 
] 

现在,我们已经介绍了 MultiChain 平台提供的基本功能,我们拥有了构建我们自己应用程序所需的所有关键因素。

存在性证明架构

存在性证明是一种证明数字文档是否在特定时间存在的机制。区块链作为公证的良好替代品,因为它可以证明文档的存在而无需第三方。每个文档通过使用哈希算法(如 SHA-256)创建其摘要来进行标识,然后通过为交易戳记时间来将文档的身份存储在区块链中。

证明文档存在的区块链实现是由开发人员 Manuel Araoz 和 Esteban Ordano 在 2013 年初创建的。它被发布为一个开源项目。此服务使用比特币的公共网络来存储有关文档的信息。有关文档的信息存储在称为 OP_RETURN 的交易元数据中,这允许在交易中存储任意信息。

在其生命周期中,存在性证明架构有两个用例。任何想要证明文件存在的用户都可以进行发布操作,并且任何人都可以通过进行验证来检查此证明。该应用程序的架构将包括用户界面、与区块链节点的后端界面以及区块链本身。

发布文档

想要证明文档在特定时间存在的所有者可以将文档上传到存在性证明应用程序中。所有者可以添加需要与文档一起保留的附加信息,例如文档描述、大小和用户详细信息。该应用程序的用户界面将随后接受此数据以及文档。然后,使用哈希算法创建文档的摘要,以唯一标识文档并以固定大小的标识表示它。文档信息与摘要一起将被发送到区块链应用程序的 Web 界面。

区块链应用程序的 Web 界面在需要时将在 MultiChain 区块链上执行特定操作。当发布操作使用所有必需的数据调用时,应用程序将创建一个项目,并请求 MultiChain 节点将其发布到区块链流中。一旦交易到达网络中的一个节点,它将被交换并包含在一个区块中,最后,它将被嵌入到区块链总账中。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6.1:发布文档存在性证明的架构图

验证文档

想要验证文档存在性的用户将会遵循类似于发布文档的过程,如前所示。但是,他们需要访问该文档以获取其存在信息。当验证功能被触发时,想要验证文档的用户将会与 Web 界面进行类似的交互。MultiChain 区块链中的验证操作将验证操作是否被调用。Web 界面将接受文档的摘要,并使用此摘要查询 MultiChain 区块链以获取文档信息。

存储在 MultiChain 流中的文档信息可以通过提交文档的摘要来检索,如下图所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6.2: 验证文档存在证明的架构图

如果项目包含在其中一个块中,则区块链节点将能够在流中找到该项目。 如果在区块链流中找到该项目并确认用户,提供有关文档的更多信息,则声称存在证明的验证将成功。

构建存在证明应用程序

正如我们在存在证明应用程序的架构中讨论的那样,每个区块链节点都有一个 Web 接口,通过该接口,其用户将发布和验证文档的存在。

我们将创建一个与部署的 MultiChain 节点通信的 Web 接口。 然后,用户将通过使用 REST API 与 Web 接口进行通信。 在我们的示例中,我们将使用第四章 区块链中的网络 中使用的 Python Sanic Web 服务器创建简单的 REST API。 然后,此 Web 接口将与 MultiChain 节点的 JSON-RPC 服务器通信,该服务器将允许节点在 MultiChain 区块链上执行任何操作。 所有由 multichain-cli 提供的功能都将在 JSON-RPC 调用中可用。 我们将使用一个名为 Savoir 的 Python 驱动程序与 MultiChain 节点的 JSON-RPC 服务器通信。

在本节中,我们将将服务器端应用程序分解为三个部分,以适应架构。 这些部分如下:

  • MultiChain JSON-RPC 驱动程序

  • 存在证明库

  • 存在证明 Web 服务器

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6.3: 服务器端应用程序的分层架构

上述 图 6.3 描绘了分层架构,通过该架构,用户通过高级 Web 服务器接口进行通信,应用程序通过低级 JSON-RPC 驱动程序与 MultiChain 节点进行交互。

MultiChain JSON-RPC 驱动程序

我们将使用驱动程序与 MultiChain 节点通信。 MultiChain 提供了一个 JSON-RPC 服务器,可用于执行任何所需的区块链操作。

在我们的用例中,我们将使用一个名为 Savoir 的 Python 驱动程序,该驱动程序将连接到 MultiChain 节点的 JSON-RPC 服务器并调用必要的函数:

from Savoir import Savoir 

class MultichainClient(object): 

    def __init__(self, **kwargs): 

        self.rpcuser = kwargs.get('rpcuser', 'multichainrpc') 
        self.rpcpasswd = kwargs.get('rpcpasswd', 'HFzmag67bJg2f4YuExgVDqQK5VfnvXRS5SKrByuCgiXm') 
        self.rpchost = kwargs.get('rpchost', 'localhost') 
        self.rpcport = kwargs.get('rpcport', '4416') 
        self.chainname = kwargs.get('chainname', 'chain1') 

我们还将使用 Savoir 创建一个 MultiChain JSON-RPC 客户端。 该客户端将需要 RPC 连接信息,例如用户名、密码、主机、端口和链名称,以便与 RPC 服务器建立连接。 在这里,命令行 multichainrpc 是默认的 RPC 用户名,4416 是默认的 RPC 端口。 我们将在先前的部分中使用相同的链,作为 chain1 创建。

注意:RPC 用户名和密码可以在创建的链的配置文件中进行配置。它位于 Linux 机器上的/home/user/.multichain/chainname/multichain.conf,或其他平台的等效安装目录中。其他区块链参数,如端口号,可以在以下位置的参数文件中配置:/home/user/.multichain/chainname/params.dat

    def connect(self):
        """connects to rpc interface"""

        try: 
            api = Savoir(self.rpcuser, self.rpcpasswd, self.rpchost, self.rpcport, self.chainname) 
            return api 

        except Exception as e: 
            return False 

然后,使用提供的连接信息创建 RPC 连接对象。此对象返回到库层以调用所需的 MultiChain API。

存在证明库

存在证明库是在区块链上执行高级任务的方法集合。该库包含用户提交的文档上可以执行的所有操作。根据架构的设计,有两种执行用户文档操作的主要方式:发布和验证。

发布操作发布文档摘要以及用户传递的任何其他信息。由于我们使用 MultiChain 流进行发布,因此数据必须以十六进制字符串格式化,如前所述。

下面的Document类展示了publishverify方法,以及一些用于获取流项目的方法:

class Document(object): 

    def __init__(self): 
        self.client = MultichainClient().connect() 
        self.stream = 'poe' 

上述构造函数使用 RPC 服务器初始化与 MultiChain 节点的连接。此连接对象可用于调用任何 MultiChain API。

以下两种方法分别用于根据其流密钥和交易 ID 获取流项目。第一个使用 MultiChain API liststreamkeyitems,传递流名称和密钥作为参数。在第二种方法中,使用其交易 ID 获取流项目,使用getwallettransaction API,该 API 接受已发布流项目的交易 ID 作为参数:

    def fetch_by_key(self, key): 
        """fetches the existence info of a document in blockchain""" 

        return self.client.liststreamkeyitems(self.stream, key) 

    def fetch_by_txid(self, tx_id): 

        return self.client.getwallettransaction(tx_id) 

文档信息使用流项目发布 API 存储在键值对中。流项目以键值对的形式发布,其中键是文档的唯一摘要,值是编码的十六进制字符串。然后,发布 API 将创建一个交易并将其插入区块链:

    def publish(self, key, value): 
        """publishes the existence of a document in blockchain""" 

        return self.client.publish(self.stream, key, value) 

当用户想要在验证其存在时检索已发布文档的信息时,将调用verify方法。验证是通过之前描述的fetch_by_key方法来执行的,该方法接受流密钥作为参数。如果密钥存在,则此方法将返回流项目列表,否则将返回一个空列表:

    def verify(self, digest): 
        """verifies the existence of a document in blockchain""" 

        return self.fetch_by_key(digest) 

接下来的方法返回最新发布的文档信息。用户还可以通过指定计数来查询所需数量的文档。使用liststreamitems API 来检索流中的所有项目。然后将返回的列表反转,并检索指定数量的项目。只从流项目中筛选出必要的信息并返回:

    def fetch_latest(self, count): 

        latest_docs = [] 
        for doc in self.client.liststreamitems(self.stream)[::-1][:count]: 
             latest_docs.append({"digest": doc.get('key'), 
            "blocktime": doc.get('blocktime'), 
            "confirmations": doc.get('confirmations')}) 
        return latest_docs 

存在证明 Web 服务器

存在证明 Web 服务器是与 MultiChain 区块链通信的接口。我们将创建一个 REST API 来与区块链应用程序通信。每个用户将能够通过此 Web 界面发送请求执行 publishverify 操作。

首先,需要导入编码数据和创建 Web 服务器所需的软件包。Web 服务器的默认端口号设置为8000

import binascii 
import json as JSON 

from base64 import b64encode, b64decode 
from datetime import datetime 
from sanic import Sanic 
from sanic.response import json 
from sanic_cors import CORS, cross_origin 
from poe_libs import Document
port = 8000 

下一步是创建文档对象。这将用于执行存在证明操作。为验证、发布和获取文档信息定义了三个 REST API 端点。我们将为所有定义的 API 端点创建一个实现:

class Server(object): 

    def __init__(self): 

        self.app = Sanic() 
        CORS(self.app) 
        self.document = Document() 

        self.app.add_route(self.publish, '/publish', methods=['POST']) 
        self.app.add_route(self.verify, '/verify', methods=['GET']) 
        self.app.add_route(self.details, '/details', methods=['GET']) 

发布文档

当用户想要证明文档存在时,将调用 publish 端点实现。publish 实现是一个 HTTP POST 端点,因为它将通过 Web 服务器界面在区块链中创建一个新记录。用户通过传递文档摘要(整个文档的哈希值)来调用此端点。用户还将传递诸如名称、电子邮件和消息之类的信息,这些信息将作为元数据存储在流项中。必需信息通过 POST 表单传递。必需的值从 request 对象中提取以创建字典:

async def publish(self, request): 

    try: 
        json_data = {'name': request.form.get('name'), 
            'email': request.form.get('email'), 
            'message': request.form.get('message'), 
            'digest': request.form.get('digest')} 

由于流项将值存储为十六进制字符串,因此字典将被转换为字符串,然后进行 base64 编码,最后编码为十六进制字符串:

        json_string = JSON.dumps(json_data) 
        encoded = b64encode(json_string.encode('utf-8')) 
        hex_encoded = binascii.b2a_hex(encoded).decode() 

调用存在证明库的 publish 方法,以及摘要和计算的编码值,以便将其存储在区块链中:

        tx_id = self.document.publish(json_data['digest'], hex_encoded) 
        tx_info = self.document.fetch_by_txid(tx_id) 

然后构造响应数据以确认用户的请求。向用户提供有关已发布项目的信息,例如交易 ID、区块哈希、时间戳和确认数。时间戳信息在存在证明用例中至关重要,因为它用于证明文档在特定时间点存在:

        response_data = { 
            'digest': json_data['digest'], 
            'transaction_id': tx_id, 
            'confirmations': tx_info.get('confirmations'), 
            'blockhash': tx_info.get('blockhash'), 
            'blocktime': tx_info.get('blocktime'), 
            'name': json_data['name'], 
            'email': json_data['email'], 
            'message': json_data['message'], 
            'timestamp': datetime.now().timestamp(), 
            'status': True} 

    except Exception as e: 

        response_data = {'status': False} 

    return json(response_data) 

核实文档

需要验证文档存在的用户将调用 verify 实现端点。verify 是一个 HTTP GET 方法,它接受文档摘要作为查询字符串,然后响应有关文档的详细信息(如果已经发布)。通过这种方式,用户可以确信文档的存在,前提是它已经在区块链上发布。

digest 查询字符串键的值作为参数传递给存在证明库的 verify 方法。如果可以在区块链分类帐中找到文档摘要,则会返回项目列表:

    async def verify(self, request):
        """returns details about verified document"""    
        digest = request.args.get('digest') 
        verified_docs = self.document.verify(digest) 

存储的十六进制编码值被解码回二进制字符串。结果的 base64 编码字符串被解码回以获取提交文档的元数据:

        response_data = [] 
        for doc in verified_docs: 
            meta_data = JSON.loads(b64decode(binascii.a2b_hex(doc.get('data'))).decode()) 

交易和区块信息以及文档的元数据将返回给文档的验证者:

            doc = {"digest": digest, 
                   "transaction_id": doc.get('txid'), 
                   "confirmations": doc.get('confirmations'), 
                   "blocktime": doc.get('blocktime'), 
                   "name": meta_data.get('name'), 
                   "email": meta_data.get('email'), 
                   "message": meta_data.get('message'), 
                   "recorded_timestamp_UTC": doc.get('blocktime'), 
                   "readable_time_UTC": datetime.fromtimestamp(int(doc.get('blocktime'))).strftime("%c")} 
            response_data.append(doc) 
        return json(response_data) 

此端点实现获取最近发布文档的详细信息。它是一个接受文档计数作为参数的 HTTP GET方法:

    async def details(self, request): 
        """returns details of latest inserted documents""" 

        latest_docs = self.document.fetch_latest(int(request.args.get('count'))) 
        return json(latest_docs) 

执行和部署应用程序

应用程序的服务器端由运行 Python Web 服务器应用程序来执行。服务器应用程序可以在任何区块链节点上执行,也可以在任何具有对区块链 JSON-RPC 服务器访问权限的机器上执行。应用程序的主要函数在指定的端口实例化 Web 服务器应用程序,如下所示:

if __name__ == '__main__':
    """main function to serve the api"""
    server = Server() 
    server.app.run(host='0.0.0.0', port=port, debug=True) 

一旦服务器成功实例化,用户就可以访问 REST 接口。让我们使用 REST 端点发布并验证文档的存在。

使用curl工具调用本地机器上运行的/publish POST 方法。我们可以使用任何哈希函数生成摘要。你可以使用 Linux 中的sha256sum工具生成哈希值:

$ sha256sum index.php 
86abfbd5f1a9e928935cdee9b2fd1bc2d43254b40d996e262026e9d668555613  index.php 

$ curl -X POST -F 'name=user' -F 'email=test@test.com1' -F 
 'message=some message' -F 
 'digest=86abfbd5f1a9e928935cdee9b2fd1bc2d43254b40d996e262026e9d668555613' 
 http://localhost:8000/publish 

POST请求通过 MultiChain 节点发布文档。如果发布操作成功,服务器将以以下数据响应:

{ 
  "transaction_id": "62eca6e6c20a4af350bd70fa3745c16de5d9a8ad70bc79cbf4c5450283424010", 
  "message": "some message", 
  "confirmations": 0, 
  "digest": "86abfbd5f1a9e928935cdee9b2fd1bc2d43254b40d996e262026e9d668555613", 
  "name": "user", 
  "email": "test@test.com1", 
  "blocktime": null, 
  "timestamp": 1523467920.313183, 
  "status": true, 
  "blockhash": null 
} 

如果服务器响应具有交易 ID,则文档的存在已成功发布,如前述输出所示。blockhashblocktime设置为 null,因为交易尚未包含在区块链中。

用户可以调用/verify GET 方法端点,并使用文档的摘要来验证其存在,如下所示:

$ curl http://localhost:8000/verify?digest=86abfbd5f1a9e928935cdee9b2fd1bc2d43254b40d996e262026e9d668555613 

[ 
  { 
    "transaction_id": "62eca6e6c20a4af350bd70fa3745c16de5d9a8ad70bc79cbf4c5450283424010", 
    "email": "test@test.com1", 
    "recorded_timestamp_UTC": 1523467857, 
    "blocktime": 1523467857, 
    "confirmations": 22, 
    "message": "some message", 
    "digest": "86abfbd5f1a9e928935cdee9b2fd1bc2d43254b40d996e262026e9d668555613", 
    "name": "user", 
    "readable_time_UTC": "Wed Apr 11 23:00:57 2018" 
  } 
] 

前述响应证明了文档在指定时间戳存在。它还给出了文档的发布详细信息。

还可以通过调用/details端点获取所有最新发布的文档信息:

$ curl http://localhost:8000/details?count=3 
[ 
  { 
    "digest": "d9d7e36d0059dfab8d7ca2ddaf9e27956e96721209d3b41cd9da46942d48f77b", 
    "blocktime": "2018-04-12 00:42:38 UTC", 
    "confirmations": 1 
  }, 
  { 
    "digest": "e459c629bfdf54c5849f7718dae9db2b0035f6cb21a04cf2f8e17ffe63b60710", 
    "blocktime": "2018-04-12 00:42:10 UTC", 
    "confirmations": 6 
  }, 
  { 
    "digest": "86abfbd5f1a9e928935cdee9b2fd1bc2d43254b40d996e262026e9d668555613", 
    "blocktime": "2018-04-12 00:13:16 UTC", 
    "confirmations": 17 
  } 
] 

文档的详细信息显示了文档的最新发布证据。正如我们所看到的,最新的文档信息比较旧的信息具有较少的确认。这是因为较早的发布交易被深深地插入到区块链中。与公共区块链不同,其中交易插入取决于交易的优先级,MultiChain 节点将所有交易视为具有高优先级,并按照交易到达的顺序插入,这是由于其相对简单的共识算法。

如架构中所述,Web 服务器应用程序与连接到 MultiChain 网络的区块链节点进行通信。Web 服务器应用程序可以部署在一个可被 MultiChain 节点访问的单独机器上,或者可以部署在同一个区块链节点上。虽然将应用程序部署在另一台服务器上会产生相同的结果,但由于只有一个中央 Web 服务器应用程序,这会引入集中化的问题。最佳做法是在有人希望发布或验证文档的存在性证明时,在区块链节点上本地运行应用程序。

每个应用程序都需要用户界面来提供良好的用户体验。我们的区块链应用程序可以与用户界面集成,在这个界面中,发布用例接受一个文档以及与文档相关的必要信息作为参数,而验证用例只需要文档以检查其存在性。前端应用程序在这两种情况下都会计算文档的摘要。

注:整个“证明存在性”项目以及与前端应用程序的集成可以在本书的 GitHub 仓库中找到(github.com/PacktPublishing/Foundations-of-Blockchain)。它可以用于在私有网络中部署区块链应用程序。

摘要

在本书前几章介绍区块链的核心概念后,在本章中,我们通过创建一个区块链应用程序来深入研究区块链。在本章中,我们仔细分析了一个区块链用例,并提出了使用 MultiChain 平台构建简单区块链应用程序的架构。MultiChain 平台的简单性以及我们讨论的其他功能使我们能够以最小的工作量创建和部署应用程序。熟悉 MultiChain 平台为我们提供了足够的洞察力,使我们能够构建和部署任何其他区块链平台上的区块链应用程序的基础,并激励我们这样做。

现在,通过实现一个相当简单的区块链用例,我们对区块链技术有了扎实的背景,这将为区块链应用程序的开发奠定基础。现在,我们将进一步深入区块链开发,通过实现另一个区块链用例来熟悉分布式智能合约。

第七章:深入区块链 - 拥有证明

在本章中,我们将通过创建一个拥有证明应用程序来介绍区块链的更广泛应用。在本章中,我们将讨论区块链内智能合约的概念,以便实施此应用程序。由于我们已经在本书的前几章介绍了区块链的概念,本章主要将关注智能合约的高级细节,即拥有证明和去中心化应用程序的创建。

在本章中,我们将重点关注以下主题:

  • 创建拥有证明应用程序

  • 区块链内智能合约的概念

  • 如何选择智能合约平台

  • 探索 NEO 区块链平台

  • 在 NEO 区块链中创建去中心化应用程序(拥有证明)

  • 探索以太坊区块链平台

  • 在以太坊区块链中创建去中心化应用程序(拥有证明)

在资产的世界中,如果你想要声明和证明拥有它们,就必须跟踪每一个资产。但是资产是由世界各地的不同实体创建的,并且没有一个单一的协议来管理资产,因为每个实体都有自己的资产管理系统。例如,如果爱丽丝在一个城市拥有一所房子和一辆车,并且她想把房子和车都卖给鲍勃,因为她打算搬出这个城市,她必须通过不同的程序将所有权转让给鲍勃 - 她必须处理房地产登记处的房屋,交通部门的汽车。

此外,她还任命了一名律师,因为这个程序相当复杂。只有在处理注册办公室、律师和公证员之后,她才能最终将车和房屋的所有权转让给鲍勃。注册和管理不同资产的协议意味着爱丽丝必须与不同的实体打交道来执行一个简单的任务。

当前的资产管理系统需要来自某些受信任的权威的批准。受信任的权威参与的主要原因是资产存在于一个无信任的社会中。不同的实体创建自己的一套处理资产的程序。其中一些实体可能正在使用过时的技术,使得用户不得不处理一些传统的程序,这样很难使用。

本章提出的拥有证明解决方案将使用区块链来构建一个去中心化的应用程序,以缓解集中式资产管理系统所面临的所有问题。我们将利用数字身份、资产和智能合约来借助区块链技术创建一个完全去中心化的资产管理系统。

数字资产和身份

数字资产是以数字格式存在的可编程资产。这些资产可以拥有自己的价值(数字代币),或者可以虚拟代表现有的实物资产(车辆所有权)。数字资产自数字时代开始就已经被使用,但直到现在它们一直存在于管理集中的环境中。区块链的发明使数字资产得以存在于去中心化网络中,无需信任的中介来注册或交易资产。去除中介意味着用户在交易资产时无需支付任何额外费用。

数字身份对于处理资产所有权时至关重要。它代表了以数字格式存在的任何个人或组织的身份。数字身份基于公钥基础设施PKI),为用户提供准确的身份管理。与容易伪造的传统身份文件不同,数字身份要求用户通过数字签名进行身份验证以证明其身份。这个系统通常使用安全的密钥基础设施,不容易被破坏。

在前几章我们讨论了索取数字资产的问题;你会记得我们讲解了为用户创建身份,用户将能够使用秘密密钥来索取资产。类似的方法将被用来在我们将用于构建本章应用程序的平台中创建和管理用户的数字身份。

所有权证明

世界上的每一项资产都归某个实体所有。由于部分所有权记录缺失或现有记录数据的模糊性,资产的部分所有权可能无法证明。尽管所有权是通过数字方式或其他方式由实体证明的,在大多数情况下,所有权信息在所有系统中并不一致。通过保持数字记录来证明所有权是最佳解决方案,数字资产和身份在其中扮演了重要角色。

数字资产与数字身份一起为主张对任何商品的所有权提供了一种方便的方式,因为资产与用户的身份一起在数字上注册。每当用户需要验证和证明资产的所有权时,他们可以提供他们的身份详细信息以及他们正在试图索赔的资产。用户经常需要通过提供一些秘密信息或使用秘密信息进行身份验证来验证他们的身份。身份验证过程取决于建立资产管理系统的第三方。在我们之前的示例中,Alice 想要将她的房子和车卖给 Bob,她需要向土地登记处和交通部门提供身份信息,然后可以通过与他们系统上的记录进行比较来进行验证。这种所有权证明系统的缺点是没有在不同组织之间保持适当的协议。这就是为什么身份管理在每个系统中都不安全的原因,以及为什么组织仍然使用传统系统,例如用户身份的硬拷贝,来验证身份而没有适当的身份验证机制,这很容易被不良行为者利用。

完全安全的所有权证明系统可以通过使用数字身份来创建,该身份使用强身份验证来证明用户的身份。大多数实现此所有权证明模型的现有系统都是集中式的,这需要对集中式机构的信任。尽管这种出处模型解决了问题,但它需要用户完全信任第三方组织来证明和验证所有权。使用区块链创建分散的所有权证明系统是唯一已知的解决方案,可以解决有关资产管理和所有权证明的所有问题。由于其不可变性和可追溯性,区块链是资产管理的最合适技术,因为一旦某个资产的某些信息被附加到区块链上,就无法撤销。可追溯性使得验证任何交易变得容易,并且如果隐私是一个问题,还允许使用专用区块链限制交易。

尽管在分散网络中可以使用区块链实现所有权证明,但在某些情况下,交易参与者之间可能存在一些复杂的协议。这些协议是通过创建合同而形成的。一个称为智能合约的概念用于在分散网络中执行此操作。我们将使用此功能来创建分散的所有权证明应用程序。

分散式所有权证明应用的最佳示例之一是Everledger,它为钻石市场的供应链构建了一个所有权证明模型(diamonds.everledger.io)。Everledger 提供了一个全球数字区块链分类账,以跟踪资产的所有权历史。它试图防止钻石行业的欺诈行为,据估计,每年达数十亿美元。

智能合约

合同是在各方之间建立以强制执行协议并确保参与者不能以后否认协议的协议。智能合约是一种协议,允许合同以自动执行的方式进行验证和执行。简单来说,它在合同条件满足时执行由各方达成的合同,而无需任何人的干预。智能合约这个术语是由密码学家尼克·萨博(Nick Szabo)在 1994 年提出的。尽管智能合约在 20 世纪 90 年代初概念化,用于自动执行传统合同,但直到比特币底层区块链技术的采用之后才在公共网络中得以实现。

正是拜占庭容错共识算法使得在分散式公共网络中执行智能合约成为可能。许多现有的区块链平台支持图灵完备编程语言,这使得创建构建智能合约所需的逻辑变得更容易。

如果一个编程语言可以用来模拟图灵机,那么它被认为是图灵完备的。比特币的脚本语言被有意地设计成图灵不完备,以尽可能简化比特币交易。

由于智能合约是在区块链中创建和部署的,它们将受益于区块链提供的所有功能。一旦合同被各方接受和部署,由于其不可变性,区块链中存储的任何合同都无法被任何人篡改。除此之外,在区块链中部署智能合约会提供完全的透明度,因为任何人都可以随时验证合同的存在。

智能合约还具有其他几个额外的好处:

  • 更快的部署和执行:以传统方式准备合同将需要用户花费数小时的时间准备文件并加工处理。智能合约只是一组自动化这些任务的指令,消除了许多不必要的步骤。

  • 成本效率的部署和执行:在区块链上创建和执行智能合约比使用传统合同更便宜,传统合同需要中介参与才能处理。

  • 安全管理:区块链中创建的所有合同都得到了安全管理。这是区块链的固有特性。

  • 复制证明:由于网络中的去中心化公共账本,每个合约都驻留在网络的每个节点上,提供多重备份。在区块链网络上不可能丢失合约。

  • 准确执行:智能合约以一组指令创建,在区块链网络中的每个节点上一致执行。这确保智能合约始终准确运行。由于区块链的拜占庭容错特性,网络将忽略合约的任何错误执行。

选择智能合约平台

智能合约是自动执行的合约,可以通过支持在其交易中执行基本脚本的任何区块链应用部署。大多数区块链平台支持领域特定语言。我们已经了解了比特币交易中使用的语言,称为Script,这是一种功能有限的基于堆栈的语言。尽管 Script 是一种图灵不完全的语言,但只有少数选项可以用于创建复杂交易。它可以创建多重签名交易,付款通道和原子跨链交易。此外,比特币可以创建一个带有锁定时间的交易。可以创建交易,但在某段时间内锁定,以防创建者希望在锁定时间到期之前使交易无效。尽管比特币的 Script 语言提供了足够的灵活性来创建复杂交易,但不适合创建复杂合约。

自那时以来,已经创建了许多区块链平台,这些平台提供使用自己的领域特定语言的高级脚本功能,例如以太坊的Solidity和卡尔达诺的Plutus。此外,大多数平台都有自己的运行时环境,编译后的智能合约在其中执行。运行时环境类似于通用编程语言(如 Java)中使用的环境。智能合约将在虚拟机上运行,类似于Java 虚拟机JVM)。区块链虚拟机提供一种在公共网络中执行不受信任代码的方式。这些虚拟机还可以提供对抗攻击(如拒绝服务DoS)攻击)的安全性,这是在执行不受信任代码的系统中必不可少的功能。

提供这些服务的一些区块链平台如下:

  • EOS:一种智能合约平台和去中心化操作系统,旨在通过每秒进行数百万次交易来解决区块链的可扩展性问题。

  • 以太坊:这是最著名的智能合约平台。它在其区块链上实现了一个几乎图灵完备的语言。它使用一个名为 Solidity 的领域特定语言,在以太坊虚拟机EVM)上编译和执行。

  • Hyperledger Fabric:这是由 Linux Foundation 主办的 Hyperledger 项目下的一个许可区块链项目。它允许执行称为链码的智能合约。它还允许共识机制作为组件插入。

  • NEO:这是一个区块链平台,允许智能合约用几种通用编程语言编写,例如 C#、Python 和 JavaScript。我们将在下一节详细介绍 NEO 区块链。

  • NXT:这是一个公共区块链平台,执行了有限的智能合约模板选择。它并没有太多的空间来创建复杂的合约。

选择创建智能合约的平台取决于需要构建的应用类型、所需的性能、智能合约语言以及许多其他因素。在选择平台之前考虑所有的要求是很重要的。就我们的所有权证明应用而言,它需要一个能处理数字资产和数字身份的平台。

NEO 区块链提供了处理数字身份和资产的便利方式。除此之外,智能合约可以用通用编程语言编写,以构建去中心化应用程序。我们将用 Python 编程语言创建我们的智能合约,这样我们就不需要掌握任何额外的编程语言。以太坊是另一个平台,可以便捷地实现各种用例。我们将在这两个平台上实现我们的所有权证明用例,因为它们是最常用的区块链平台,同时对这两个平台的介绍将为去中心化应用程序开发打下坚实的基础。

在接下来的章节中,我们将深入探讨 NEO 和以太坊区块链平台,以及所有权证明的实现。

NEO 区块链

NEO 是一个区块链平台和加密货币,能够促进数字资产和智能合约的创建和管理。NEO 项目最初于 2014 年以 AntShares 的名义发布,直到 2017 年 6 月更名为 NEO。所有的开发资源都是由创始人达·宏飞的区块链解决方案公司 Onchain 提供的。NEO 项目的主要目的是在分布式网络中实现智能经济,借助数字资产、数字身份和智能合约。

该平台使用两种类型的代币,称为 NEO 和 GAS。与比特币不同,NEO 代币是不可分割的,这意味着 NEO 的最小单位为 1. 持有 NEO 代币可以在共识机制中行使投票权,该机制在共识算法部分有解释。持有 NEO 代币会产生一种名为 GAS 的新代币,用于支付交易费。GAS 代币就像燃料一样,如果你想在区块链上部署和执行任何智能合约,就必不可少。创世区块中发行了 1 亿个 NEO 代币。但相应的 1 亿个 GAS 代币将在大约 22 年内逐渐发行。

NEO 区块链的构建块

NEO 区块链使用了去中心化网络的两个重要元素,以创建去中心化应用程序,因此构建了智能经济。这些元素是数字资产和数字身份,它们是任何区块链应用程序的基石。这些概念,连同智能合约,在之前的章节中已经提到,对于任何所有权证明应用程序的创建是至关重要的。

NEO 提供了在去中心化 NEO 区块链网络中创建和管理数字资产的便利方式。NEO 提供了两种不同类型的资产:

  • 全局资产

  • 合约资产

全局资产记录在系统中,并且所有客户端和智能合约都能识别。NEO 和 GAS 代币是全局资产。合约资产与特定合约绑定,不能被其他合约识别。只有某些兼容的客户端才能访问合约资产。基于 NEP-5 的资产就是合约资产的一个例子。

NEP-5 是 NEO 指定的用于创建加密代币的标准。该标准帮助开发人员在构建与代币相关的应用程序时保持模板。NEP-5 代币类似于以太坊中使用的 ERC-20 代币。我们将在第十二章中实施使用 NEP-5 代币的用例,区块链使用案例

NEO 利用数字身份在区块链中处理物理资产和数字资产之间的连接。NEO 实现了X.509公钥证书颁发标准来创建数字身份。借助区块链,NEO 可以取代在线证书状态协议OCSP)来管理和记录 X.509 证书吊销列表CRL)。

NEO 技术

NEO 提供了几种功能来作为可扩展的区块链平台运行。下面讨论了 NEO 中使用的一些技术。

共识算法

NEO 使用委托拜占庭容错dBFT),这是一种改进的拜占庭容错共识算法。它是一种允许区块链参与者通过代理投票达成共识的机制。一组特殊的节点,称为记账人,通过投票达成共识以在网络中生成新区块。这些记账人是由 NEO 代币持有人通过投票选举产生的。dBFT 算法具有f =(n-1) / 3 ⌋ of n节点的容错能力,即约 33%的节点。一旦生成和确认了区块和交易,几乎不可能撤销它们。

在 NEO 中生成一个区块大约需要 15 到 20 秒,而且它的吞吐量为每秒 1000 笔交易,与基于工作量证明的实现相比,这是非常高的。由于其高的交易吞吐量,NEO 的应用可以轻松扩展。

NEO 智能合约

智能合约是 NEO 区块链突出特点之一。在 NEO 区块链系统中编写智能合约相对于其他智能合约平台来说是一个相当简单的过程,主要是因为它支持大量通用目的语言,可用于创建智能合约。与以太坊不同,NEO 不需要特定领域的语言来创建和执行智能合约。

NEO 拥有一个轻量级的虚拟机,类似于 Java 中的 JVM。NEO 的虚拟机按顺序执行智能合约指令。NEO 虚拟机只执行由 NEO 编译器编译的指令。NEO 计划支持 C#,Java,C,C ++,Go,JavaScript,Python 和 Ruby 等语言的编译器;尽管并非所有语言都已实现,但正在进行开发以支持大多数语言。除了这些编译器,NEO 目前还支持 Java 和 C#的 IDE 插件。这有助于开发人员在不改变开发生态系统的情况下创建智能合约。

其他 NEO 项目

NEO 是一个不断发展的社区,其路线图中有许多项目;你会在以下列表中找到最受欢迎的一些样本:

  • NeoX:NEO 的这一特性将允许不同链之间的资产交换。它将提供原子资产交换协议,以确保交易要么完全处理,要么完全拒绝。即使是不兼容的区块链,只要提供一些基本的智能合约功能,也可以通过 NeoX 进行通信。由于 NeoX 将帮助实现跨链协作,一个智能合约可以在两条链上执行操作。

  • NeoFS:这是一种将使用 分布式哈希表 (DHT) 技术的分布式存储机制。 每个文档将以其内容的摘要索引。 大型文档被分割成块并分布在区块链节点之间。 NeoFS 计划为存储需要更高可靠性的文档的节点提供代币奖励。 NeoFS 节点可用于存储 NEO 区块链的旧块数据,以减轻完整节点的负载。

  • NeoQS:NEO 计划解决量子计算对加密算法的挑战。 NeoQS 计划开发量子安全的加密机制。

尽管所有这些项目都在开发中,但 NEO 的路线图看起来非常有前景。 NeoFS 和 NeoQS 将于 2018 年第三季度提供研究更新,而 NeoX 预计将在 2018 年最后一个季度进行初步测试。

NEO 节点

与任何其他区块链平台一样,NEO 也有保存完整区块链历史记录的节点,称为完整节点。 这些完整节点构成了网络的骨干,并且它们使用 P2P 协议进行通信。

入门指南

NEO 支持两种变体的完整节点,一种具有图形用户界面,另一种仅支持命令行。 名为 NEO-GUI 的图形用户界面变体提供了用户所需的所有功能。 NEO-CLI 针对想要使用基本钱包功能和 API 的开发人员。 使用任何一个变体都非常简单。 我们将主要处理 NEO-CLI,因为它更适合开发人员。

配置完整节点

原始的 NEO-CLI 实现是用 C# 编写的,源代码可以在 github.com/neo-project/neo-cli 找到。 这个实现需要一个已安装 .NET Core 的用户节点来运行编译的二进制文件。 该存储库说明了如何在不同环境中设置 .NET Core。 源代码中的一个 动态链接库 (DLL) 文件需要执行才能运行 NEO-CLI。 一旦在您的系统上安装了 .NET Core,以下命令就会启动 NEO-CLI 完整节点进程:

$ dotnet neo-cli.dll 

完整节点使用三个不同的端口进行 JSON-RPC (10332)、通过 TCP 进行 P2P (10333) 和通过 WebSocket 进行 P2P (10334)。 除此之外,JSON-RPC 还有一个 HTTPS 版本 (10331)。 NEO 对测试网和私有网使用不同的端口集。 它使用初始的 “1” 替换为 “2” 用于测试网和 “3” 用于私有网。 在本章中,我们将主要处理私有网节点,因为我们将创建自己的网络来部署应用程序。

可以通过运行以下带有 /rpc 标志的命令来暴露节点的 JSON-RPC 接口:

$ dotnet neo-cli.dll /rpc 

NEO-CLI 打开一个交互式界面,用户可以在其中执行所有区块链节点和钱包操作:

NEO-CLI Version: 2.7.4.0 
neo> 

在执行任何管理钱包或节点的命令之前,用户必须创建一个钱包或打开一个现有的钱包。在 NEO-CLI shell 中,以下命令创建并打开一个钱包:

create wallet neo_wallet.db3 
password: *** 
address: ASxUka4WqmEkD2mJtGy37J9NeuTe8bTtYF 
pubkey: 0374c66e892d7a8cbbbd4c8bd5b7b71ec83819a90c2327d7057b1234072291b5d8 

open wallet neo_wallet.db3 

用户需要提供一个密码来保护钱包,每次解锁钱包都会使用这个密码。用户可以在打开钱包后执行任何钱包、交易或区块操作。

您可以参考 docs.neo.org/en-us/node/cli/cli.html 查看 NEO-CLI 文档中的所有命令。

NEO-CLI 支持在区块链中测试、构建和部署智能合约,但目前不支持用多种编程语言编写智能合约。NEO-CLI 只提供 .NET 和 Java 的编译器。我们需要第三方编译器来在任何其他编程语言中创建智能合约。neo-python 就是这样一个项目。它由 City of Zion 组织支持,该组织由一群积极贡献的开发人员组成。我们将在本章中使用 neo-python 项目来构建我们的应用程序。

设置 neo-python 环境

neo-python 项目提供了一个 NEO 节点和一个 SDK,使开发人员可以使用 Python 在 NEO 区块链上创建、测试、部署和执行智能合约。该项目支持你在钱包和区块链节点中管理资产所需的所有功能。该项目旨在完全移植 NEO-CLI 实现。

要设置 neo-python,您需要安装 Python 3.6 解释器。neo-python 可以安装在任何平台上,尽管需要执行一些特定于平台的步骤。在安装 neo-python 之前,需要安装 leveldbopenssl 库。

从快速入门到构建复杂智能合约,neo-python 的完整文档可以在 neo-python.readthedocs.io 找到。

neo-python 软件包可以像任何其他 Python 软件包一样从 PyPI 安装,也可以从源代码库安装。安装完成后,可以使用 np-prompt 命令启动 neo-python 的交互式 shell。其 shell 界面如下所示,与 NEO-CLI 类似:

$ np-prompt 
NEO cli. Type 'help' to get started 
neo> 

neo-python 也可以通过从 github.com/CityOfZion/neo-python 克隆存储库并以开发模式安装 neo-python 软件包来安装。然后可以使用 np-prompt 命令启动 neo-python shell,或者简单地运行 Python 脚本 neo/bin/prompt.py

在打开钱包后,可以像在 NEO-CLI 上一样执行任何命令。

虽然在 NEO-CLI 上执行的所有操作在 neo-python 中也可以执行,但有些命令语法与 NEO-CLI 命令不同。在 neo-python shell 中键入 help 列出所有命令。

设置节点的 JSON-RPC 接口

如在设置节点时的前一节所指定的,NEO 节点充当 JSON-RPC 服务器,以便可以使用 RPC 接口进行通信。可以通过在 NEO-CLI 中添加 /rpc 标志来实例化 JSON-RPC 服务器,如前所述。您需要启动一个不同的进程来在 neo-python 中创建 RPC 服务器:

$ np-api-server --port-rpc 10332 

就像比特币的 JSON-RPC 接口一样,NEO 为其每个 API 提供了一个 RPC 终端点:

$ curl -X POST http://localhost:10332 -H 'Content-Type: 
 application/json' -d '{ "jsonrpc": "2.0", "id": 5, "method": 
 "getversion", "params": [] }' 

{"jsonrpc": "2.0", "id": 5, "result": {"port": 8080, "nonce": 
 1439440988, "useragent": "/NEO-PYTHON:0.6.6/"}} 

大多数前端应用程序使用 JSON-RPC 与去中心化应用程序和区块链本身通信。

neon-js 由 City of Zion 维护,提供了使用 NEO 节点公开的 RPC 接口与区块链通信的 JavaScript 库。

NEO 网络

NEO 使用网络协议来建立连接并与每个节点双工通信。网络中的节点根据其责任被分类为两种类型:验证节点(记账节点)和普通对等节点。对等节点在它们被验证后帮助广播区块和未确认的交易,而记账节点生成新的区块。NEO 遵循与比特币类似的网络协议,以启动连接并在对等节点之间交换区块。

当我们通过 NEO-CLI 或 neo-python 启动 NEO shell 时,节点将加入默认配置中指定的网络。neo-python 节点可以属于主网、测试网或私有网络。如果启动时未指定网络,则 NEO 节点将加入测试网络。以下命令将节点启动在私有网络中,该私有网络协议配置文件位于 neo/data/ 中:

$ np-prompt -p 

我们将在本章中使用私有网络执行所有 neo-python 操作。可以通过显式指定网络配置文件并使用 -c 标志来启动节点。

测试网络

NEO 测试网络类似于主网。在测试网络中,用户可以开发、部署和执行程序。用户可以使用没有实际价值的测试代币,而不是花费真实的 GAS 和 NEO 代币。在测试网上执行的每个其他操作与在主网上执行的操作相同。因此,这是开发人员在部署到主网之前测试应用程序的理想环境。由于测试网是一个来自世界各地的参与者的活跃网络,NEO 和 GAS 代币的供应是有限的,当您加入网络时,将不会提供任何代币,就像在主网上一样。由于部署智能合约需要至少 500 GAS,用户可以从其他测试网用户那里获取这些 GAS,或者在 neo.org/Testnet/Create 申请。用户在拥有足够 GAS 后可以将智能合约部署到网络上。

私有网络

私有网络是一组 NEO 节点,它们自行实现区块链状态一致性。这些 NEO 节点与主网或测试网的公共节点完全隔离。私有网络非常适合在组织内部创建区块链网络。

私有 NEO 网络需要至少四个节点才能达成共识。私有 NEO 网络可以部署在局域网中,并且还可以通过创建虚拟机在单个设备上部署多个节点。即使私有网络的节点也需要 GAS 来在私有区块链中创建和部署智能合约。私有节点可以通过从所有共识节点创建多方签名地址来提取网络中的所有 NEO 和 GAS 代币。然后可以将 NEO 和 GAS 从联系地址转移到普通地址。

通过部署托管在 hub.docker.com/r/cityofzion/neo-privatenet 的即插即用 Docker 镜像,可以创建具有有限共识节点的小型私有网络。此 Docker 镜像部署了四个 NEO 验证节点,并预领取了所有 1 亿 NEO 和 16,600 GAS 代币。加入网络的任何用户都可以使用包含所有代币的钱包部署智能合约。只需几个命令即可启动私有网络:

$ docker pull cityofzion/neo-privatenet
$ docker run --rm -d --name neo-privatenet -p 20333-20336:20333-
 20336/tcp -p 30333-30336:30333-30336/tcp cityofzion/neo-privatenet 

Docker 镜像将创建一个包含四个节点的容器,同时公开 P2P(20333-20336)端口和 RPC 端口(30333-30336)。

在相同网络中的任何 neo-python 节点都可以将这四个节点添加为其种子节点,并开始同步私有网络区块链。然后用户可以使用包含所有网络代币的钱包创建交易和部署智能合约。

NEO 交易

NEO 网络上的每个节点都可以在 NEO 区块链中创建交易并执行操作。节点必须打开钱包才能创建交易并进行广播:

open wallet neo_wallet.db3
wallet 

neo-python 中的 wallet 命令为您提供有关已打开钱包的完整信息:

{ 
  "path": "neo_wallet.db3", 
  "addresses": [ 
    { 
      "version": 0, 
      "script_hash": "AK2nJJpJr6o664CWJKi1QRXjqeic2zRp8y", 
      "frozen": false, 
      "votes": [], 
      "balances": { 
        "0xc56f33fc6ecfcd0c225c4ab356fee59390af8560be0e930faebe74
 a6daff7c9b": "100000000.0", 
        "0x602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee
 7969282de7": "16024.0" 
      }, 
      "is_watch_only": false 
    } 
  ], 
  "height": 20066, 
  "percent_synced": 100, 
  "synced_balances": [ 
    "[NEO]: 100000000.0 ", 
    "[NEOGas]: 16024.0 " 
  ], 
  "public_keys": [ 
    { 
      "Address": "AK2nJJpJr6o664CWJKi1QRXjqeic2zRp8y", 
      "Public Key": "031a6c6fbbdf02ca351745fa86b9ba5
 a9452d785ac4f7fc2b7548ca2a46c4fcf4a" 
    } 
  ], 
  "tokens": [], 
  "claims": { 
    "available": "143992.0", 
    "unavailable": "480.0" 
  } 
} 

钱包在每笔交易验证后维护更新的详细信息。addresses 字段包含钱包持有的所有密钥的详细信息。已打开的钱包仅有一个密钥,其公共地址为 AK2nJJpJr6o664CWJKi1QRXjqeic2zRp8yaddresses 字段内的 balances 字段显示节点当前拥有的 NEO 和 GAS 代币。在 balances 字段中,0xc56f33fc6ecfcd0c225c4ab356fee59390af8560be0e930faebe74a6daff7c9b 表示 NEO 代币的交易 ID,而 0x602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7 是 GAS 代币的交易 ID。NEO 将这些用作 NEO 和 GAS 代币的标准 ID。

与比特币节点不同,NEO 节点可以创建多种不同类型的交易来支持区块链中执行的所有操作。以下表格描述了可以在 NEO 网络中创建的不同类型的交易:

名称描述
MinerTransaction分配字节费用
IssueTransaction发行资产
ClaimTransaction领取 NEO 币
EnrollmentTransaction注册为验证者
VotingTransaction为验证者投票
RegisterTransaction资产注册
ContractTransaction合约交易
AgencyTransaction订单交易

表格 7.1. NEO 中的交易类型

每种类型的交易都将具有专用字段来存储有关交易的更多信息。例如,MinerTransaction 有一个额外的字段来存储 nonce,它是一个随机数。

资产转移

节点可以使用交易对 NEO 资产执行操作。交易由节点使用其在钱包中的私钥创建。一旦打开钱包,用户就可以对资产执行任何操作。neo-python 中的 send 命令通过接收资产 ID、接收地址和金额来转移资产。send 命令创建一个交易并将其中继到网络,以便将其包含在区块链中:

send NEO AZ81H31DMWzbSnFDLFkzh9vHwaDLayV7fU 100 
[Password]> *** 
Relayed Tx: 53b72dbce63a28a01432c1ddcc82aed8c28fb1fa338cab812c979
 d56cc8e4410 

创建的交易是一种 ContractTransaction,详细信息显示它包含 voutvin 字段,其功能类似于比特币交易中的字段。vin 字段指向被引用其未花费输出的交易,而 vout 由新创建的未花费输出组成。第一个输出是用户交易的金额,第二个是输出的变化。与比特币交易不同,验证脚本在一个单独的 script 字段下找到:

{ 
  "txid": "0x53b72dbce63a28a01432c1ddcc82aed8c28fb1fa338cab812
 c979d56cc8e4410", 
  "type": "ContractTransaction", 
  "version": 0, 
  "attributes": [ 
    { 
      "usage": 32, 
      "data": "23ba2703c53263e8d6e522dc32203339dcd8eee9" 
    } 
  ], 
  "vout": [ 
    { 
      "n": 0, 
      "asset": "0xc56f33fc6ecfcd0c225c4ab356fee59390af8560be0e930
 faebe74a6daff7c9b", 
      "value": "100", 
      "address": "AZ81H31DMWzbSnFDLFkzh9vHwaDLayV7fU" 
    }, 
    { 
      "n": 1, 
      "asset": "0xc56f33fc6ecfcd0c225c4ab356fee59390af8560be0e930
 faebe74a6daff7c9b", 
      "value": "99999900", 
      "address": "AK2nJJpJr6o664CWJKi1QRXjqeic2zRp8y" 
    } 
  ], 
  "vin": [ 
    { 
      "txid": "2b8907db07ebbc3ea2244162ff3d696e7b80874d3ddc3f1fc52
 e427d91cd91c3", 
      "vout": 0 
    } 
  ], 
  "sys_fee": "0", 
  "net_fee": "0", 
  "scripts": [ 
    { 
      "invocation": "40f6b2e5c2ca932a536284136c254119096813ee35
 d494c939d9e26a7b6247f0801284a34c39e0194c35d1db68bf54fa1de2852
 b86182d86a673a206dcf64c6f04", 
      "verification": "21031a6c6fbbdf02ca351745fa86b9ba5a9452d785
 ac4f7fc2b7548ca2a46c4fcf4aac" 
    } 
  ], 
  "height": 20594, 
  "unspents": [ 
    { 
      "n": 0, 
      "asset": "0xc56f33fc6ecfcd0c225c4ab356fee59390af8560be0e930
 faebe74a6daff7c9b", 
      "value": "100", 
      "address": "AZ81H31DMWzbSnFDLFkzh9vHwaDLayV7fU" 
    }, 
    { 
      "n": 1, 
      "asset": "0xc56f33fc6ecfcd0c225c4ab356fee59390af8560be0e930
 faebe74a6daff7c9b", 
      "value": "99999900", 
      "address": "AK2nJJpJr6o664CWJKi1QRXjqeic2zRp8y" 
    } 
  ] 
} 

创建一个去中心化应用程序

现在我们已经了解了 NEO 平台的一些基本功能,我们准备使用 NEO 区块链创建我们的第一个去中心化应用程序。智能合约是使用 NEO 创建去中心化应用程序的基础。在创建去中心化所有权证明应用程序之前,我们将通过创建一个 hello world 应用程序来熟悉智能合约。

基本智能合约

首先,我们将创建一个简单的 Python 脚本,返回一个连接的字符串来向用户问候:

from boa.builtins import concat 

def main(name): 
  return concat("Hello ", name) 

合约脚本使用 boa 提供的 concat 方法来连接两个字符串。每个智能合约都应该有一个名为 main 的函数,这将是入口点。智能合约需要编译成字节码,可以在 NeoVM 中执行。合约可以通过 neo-python shell 使用 neo-boa 编译器编译,如下所示:

build hello.py test 07 07 False False Alice 

build 命令带有一个 test 参数,用于测试样本结果。test 标志之后的代码表示参数和返回类型的数据类型。前面的代码规定了合约函数接受一个字符串参数并返回一个字符串值。下表列出了所有可用的数据类型及其代码:

数据类型代码
Signature0x00
布尔值0x01
Integer0x02
Hash1600x03
Hash2560x04
字节数组0x05
公钥0x06
字符串0x07
数组0x10
互操作接口0xf0
0xff

表 7.2。合约参数使用的数据类型

在构建过程中,跟随数据类型后面的第一个布尔值规定合约是否需要本地存储,第二个布尔值指示合约是否动态调用其他智能合约,其地址仅在执行期间才知道。 在构建过程中,合约的任何输入都遵循这些参数。测试调用将显示结果,以及调用合约所需的 GAS。 结果显示输出是 string 类型,以及它的值。 最重要的是,build 调用通过创建 AVM 文件将合约指令生成为字节码,并存储在同一目录中:

Calling hello.py with arguments ['Alice'] 
Test deploy invoke successful 
Used total of 19 operations 
Result [{'type': 'String', 'value': 'Hello Alice'}] 
Invoke TX gas cost: 0.0001 

生成的 AVM 文件需要导入到 NeoVM,然后中继到区块链网络。以下的 import 调用执行合约导入。 import 命令接受类似于构建过程中指定的参数:

import contract hello.avm 07 07 False False Alice 

用户需要在创建和中继到网络之前输入智能合约的详细信息:

Please fill out the following contract details: 
[Contract Name] > Hello World 
[Contract Version] > 1.0.0 
[Contract Author] > Alice 
[Contract Email] > alice@neotest.com 
[Contract Description] > Basic smart contract 
Creating smart contract.... 
{ 
  "hash": "0x6ed9fabe179b236ca7c22deb72a02bdf65b57b84", 
  "script": 
 "54c56b6a00527ac40648656c6c6f206a00c37e6c75665ec56b6a00527ac46a515
 27ac46a51c36a00c3946a52527ac46a52c3c56a53527ac4006a54527ac46a00c36
 a55527ac461616a00c36a51c39f6433006a54c36a55c3936a56527ac46a56c36a5
 3c36a54c37bc46a54c351936a54527ac46a55c36a54c3936a00527ac462c8ff616
 1616a53c36c7566", 
  "parameters": "07", 
  "returntype": "07" 
} 
Used 100.0 Gas 

一旦创建智能合约,将生成脚本哈希以及合约脚本。脚本哈希代表合约,并且可以由网络中的所有人用来调用智能合约。

交易中使用的 GAS 取决于智能合约操作类型和智能合约中使用的系统调用。 创建智能合约的成本为 100 GAS 加上系统调用的额外费用。 如果智能合约需要存储空间,则需要额外花费 400 GAS。 我们之前的智能合约部署只使用了 100 GAS 来创建智能合约,因为没有需要本地存储的东西。

neo-python 提供了一个 testinvoke 命令,可以用于在区块链上已部署的合约哈希上进行测试。除非用户接受,否则 testinvoke 调用不会被中继到网络。它只接受合约的脚本哈希和其参数:

testinvoke 0x6ed9fabe179b236ca7c22deb72a02bdf65b57b84 Alice 

一旦节点更新其本地区块链以包含之前创建的中继合约,就可以执行 testinvoke 调用。以下是输出:

Test invoke successful 
Total operations: 19 
Results ['48656c6c6f20416c696365'] 
Invoke TX GAS cost: 0.0 
Invoke TX fee: 0.0001 

testinvoke 从区块链中调用合约,并以十六进制字符串数组的形式返回计算结果。十六进制结果 '48656c6c6f20416c696365' 转换为 “Hello Alice”,这是期望的输出。

所有权证明应用

我们之前已经详细讨论了在本章早期在分散应用中创建所有权证明应用的好处;现在我们将继续创建一个使用 NEO 智能合约来执行资产管理的所有权证明应用,以在分散网络中执行。

我们创建了一个“存在证明”应用程序来证明第六章中文档的存在,深入区块链 - 存在证明。在本节中,我们将创建一个资产管理系统来注册和证明本节中文档的所有权。目标是创建以下资产管理功能:

  • 资产注册

  • 资产查询

  • 资产移除

  • 资产转移

我们将在智能合约中实现所有功能以证明文档的所有权。

创建智能合约

使用 Python 创建的智能合约包含main函数作为入口点。此函数接受两个参数。第一个参数接受操作类型,所有附加参数都传递给第二个参数的列表。operation参数接受registerquerydeletetransfer,以便执行资产管理功能。

智能合约的main函数解析operation参数,并调用智能合约中的相应函数以对资产执行操作。main函数解析args参数,并将列表的第一项分配给asset_id,其他项分配给owner

from boa.interop.Neo.Runtime import Log, Notify 
from boa.interop.Neo.Storage import Get, Put, GetContext 
from boa.interop.Neo.Runtime import GetTrigger,CheckWitness 
from boa.builtins import concat 

def main(operation, args): 
  nargs = len(args) 
  if nargs == 0: 
    print("No asset id supplied") 
    return 0 

  if operation == 'query': 
    asset_id = args[0] 
    return query_asset(asset_id) 

  elif operation == 'delete': 
    asset_id = args[0] 
    return delete_asset(asset_id) 

  elif operation == 'register': 
    if nargs < 2: 
      print("required arguments: [asset_id] [owner]") 
      return 0  
    asset_id = args[0] 
    owner = args[1] 
    return register_asset(asset_id, owner) 

  elif operation == 'transfer': 
    if nargs < 2: 
      print("required arguments: [asset_id] [to_address]") 
      return 0 
    asset_id = args[0] 
    to_address = args[1] 
    return transfer_asset(asset_id, to_address) 

register_asset函数接受asset_id和所有者地址,并在区块链中创建所有权条目。CheckWitness是一个 NEO 运行时功能检查,检查所有者地址是否与调用合同的用户地址匹配。如果要注册的资产所有者与调用合同的用户不同,则合同返回False。合同通过调用 NEO 存储库的Get方法来验证是否已经注册了asset_id。最后,通过使用Put存储方法将资产 ID 和所有者详细信息存储在键/值对中,将资产注册给所有者:

def register_asset(asset_id, owner): 
  msg = concat("RegisterAsset: ", asset_id) 
  Notify(msg) 

  if not CheckWitness(owner): 
    Notify("Owner argument is not the same as the sender") 
    return False 

  context = GetContext() 
  exists = Get(context, asset_id) 
  if exists: 
    Notify("Asset is already registered") 
    return False 

  Put(context, asset_id, owner) 
  return True 

NEO 通过在区块链中存储数据的方式提供存储功能,采用键/值对。智能合约必须在部署时指定脚本是否需要合约存储空间。在部署期间使用存储会额外消耗 GAS。

query_asset函数查询本地存储以检查用户是否已经注册了资产。如果找到资产,则返回所有者地址:

def query_asset(asset_id): 
  msg = concat("QueryAsset: ", asset_id) 
  Notify(msg) 

  context = GetContext() 
  owner = Get(context, asset_id) 
  if not owner: 
    Notify("Asset is not yet registered") 
    return False 

  Notify(owner) 
  return owner 

以下功能需要asset_id和资产接收者地址以便转移资产。作为第一步,它通过使用Get方法检查存储来验证资产的存在。然后检查资产所有者是否与调用者相同。合同还验证了接收者地址是否是有效地址。最后,使用Put存储方法更新资产的新所有者:

def transfer_asset(asset_id, to_address): 
  msg = concat("TransferAsset: ", asset_id) 
  Notify(msg) 

  context = GetContext() 
  owner = Get(context, asset_id) 
  if not owner: 
    Notify("Asset is not yet registered") 
    return False 

  if not CheckWitness(owner): 
    Notify("Sender is not the owner, cannot transfer") 
    return False 

  if not len(to_address) != 34: 
    Notify("Invalid new owner address. Must be exactly 34 
 characters") 
    return False 

  Put(context, asset_id, to_address) 
  return True 

delete_asset 方法实现与 transfer_asset 类似的功能,不同之处在于它删除资产而不是更新它。Delete 函数调用用于从存储中删除存储的键值对:

def delete_asset(asset_id): 
  msg = concat("DeleteAsset: ", asset_id) 
  Notify(msg) 

  context = GetContext() 
  owner = Get(context, asset_id) 
  if not owner: 
    Notify("Asset is not yet registered") 
    return False 

  if not CheckWitness(owner): 
    Notify("Sender is not the owner, cannot transfer") 
    return False 

  Delete(context, asset_id) 
  return True 

现在,我们已经实现了资产管理的所有基本功能,我们将在下一部分中使用 neo-python shell 执行合约。

执行智能合约

智能合约的执行步骤与我们部署基本智能合约的之前部分类似。唯一的区别是提供给合约的参数和相应的返回数据不同。

正如前面提到的,我们将创建一个所有权证明应用来跟踪文件。每个文件可以通过其摘要唯一标识。我们将使用文件的 SHA256 哈希值作为资产 ID。让我们考虑一个具有以下内容的文件。这些文件通常存储有一个额外的看不见的换行符:

This document was created by Alice. 

以下摘要表示文件的 SHA256 哈希值:

f572f8ce40bf97b56bad1c6f8d62552b8b066039a9835f294ea4826629278df3 

让我们使用哈希值作为资产 ID 来唯一标识每个文件。合同在 neo-python shell 中使用以下命令构建:

build poo.py test 0710 05 True False query 
 ["f572f8ce40bf97b56bad1c6f8d62552b8b066039a9835f294ea4826629278
 df3"] 

Test deploy invoke successful 
Used total of 113 operations 
Result [{'type': 'ByteArray', 'value': ''}] 
Invoke TX gas cost: 0.0001 

build 过程以 0710 作为参数类型,表示它以一个字符串 (07) 和一个数组 (10) 作为参数。05 表示它具有字节数组返回类型。

成功构建后,可以使用创建的 AVM 文件部署合约:

import poo.avm 0710 05 True False 
{ 
  "hash": "0x60a7ed582c6885addf1f9bec7e413d01abe54f1a", 
  "script": "....", 
  "parameters": "0710", 
  "returntype": "05" 
} 
Used 500.0 Gas 

由于智能合约需要本地存储,交易需要额外的 400 GAS。总共消耗的 GAS 将为 500,如前面的代码块所示。

一旦合约交易包含在区块链中并在本地区块链中同步,合约就可以执行。让我们使用 testinvoke 命令来测试我们创建的智能合约。

让我们使用先前提到的相同的 SHA256 值作为资产 ID 注册文件。注册操作调用一个参数列表,包括哈希值和调用智能合约的地址:

testinvoke 0x60a7ed582c6885addf1f9bec7e413d01abe54f1a register 
 ["f572f8ce40bf97b56bad1c6f8d62552b8b066039a9835f294ea4826629278
 df3", "AK2nJJpJr6o664CWJKi1QRXjqeic2zRp8y"] 

一旦交易被调用并传递到网络上,其他操作就可以在资产上执行。现在让我们通过调用 transfer 操作并在列表中指定接收者地址来将文件所有权转让给新所有者:

testinvoke 0x60a7ed582c6885addf1f9bec7e413d01abe54f1a transfer 
 ["f572f8ce40bf97b56bad1c6f8d62552b8b066039a9835f294ea4826629278
 df3", "AZ81H31DMWzbSnFDLFkzh9vHwaDLayV7fU"] 

可以随时通过调用查询操作并传递资产 ID 来验证文件所有权:

testinvoke 0x60a7ed582c6885addf1f9bec7e413d01abe54f1a query 
 ["f572f8ce40bf97b56bad1c6f8d62552b8b066039a9835f294ea4826629278
 df3"] 

Test invoke successful 
Total operations: 118 
Results ['415a3831483331444d577a62536e46444c466b7a683976487761444
 c617956376655'] 
Invoke TX GAS cost: 0.0 
Invoke TX fee: 0.0001 

查询返回一个具有十六进制字符串的字节数组。十六进制结果表示地址 AZ81H31DMWzbSnFDLFkzh9vHwaDLayV7fU。由于文件的所有权已转移给新所有者,结果显示文件的更新所有者。

我们已经完成了创建智能合约的过程,以演示一个跟踪文档所有权的系统。一旦智能合约在区块链上部署,它将永远留在那里。用户只需处理智能合约的调用。NEO 节点的 RPC 接口提供了一种方便的方式来与区块链进行通信。现在,我们将看看如何通过创建一个接口来方便地与区块链进行通信。

应用程序的界面

NEO 社区创建了一些 JavaScript 库,用于与 NEO 区块链进行交互。我们将使用一个名为 neon-js 的流行库(github.com/CityOfZion/neon-js),该库得到了 City of Zion 社区的支持。

以下脚本创建了一个接口,用于查询我们所有权证明应用程序中资产的所有者:

queryAsset(assetID) {
  const props = { 
    scriptHash: '60a7ed582c6885addf1f9bec7e413d01abe54f1a', 
    operation: 'query', 
    args: [assetID.hexEncode()]
  }; 
  const Script = Neon.create.script(props); 

  rpc.Query.invokeScript(Script).execute('http://localhost:30333')
 .then((res) => { 
    return res.result.stack[0].value.hexDecode() 
  }); 
} 

使用 neon-js 库创建的接口使用Neon.create.script方法构建了智能合约脚本。然后,它使用 RPC 接口调用智能合约脚本。之后,queryAsset返回拥有文档资产的用户地址。

创建与区块链智能合约的接口是构建完整去中心化应用程序的关键部分。该接口还为与区块链节点通信提供了便捷的方式,从而增强了用户体验。

以太坊区块链

以太坊是一个公共区块链,由 Vitalik Buterin 于 2013 年末提出,并于 2015 年向公众发布。以太坊是最初创建的区块链平台之一,旨在帮助程序员使用智能合约开发和部署去中心化应用程序。以太坊拥有丰富的框架和库,用于开发、测试和部署应用程序。本节我们将介绍使用以太坊平台开发和部署所有权证明应用程序的开发和部署。有关以太坊生态系统的更多详细信息,请参阅第八章,区块链项目

以太坊节点

与比特币和 NEO 类似,不同语言中存在多种客户端软件实现,可用作以太坊完整节点。以太坊客户端实现可以在 Java、JavaScript、Python、Go 等多种语言中找到。以太坊的 Golang 实现称为Go EthereumGeth,是其中最受欢迎的。

入门

在深入应用开发之前,设置节点是必不可少的一步。尽管任何以太坊客户端都可以用来设置完整节点,我们将设置 Geth 客户端以与公共区块链同步并进行交互。与 NEO 区块链类似,Geth 客户端可以连接到主网、测试网或私有网络。

设置节点

我们将设置一个 Geth 客户端,用于同步整个区块链交易。它还提供了 JSON-RPC 接口,以调用客户端软件支持的任何方法。JSON-RPC 接口可用于执行多个操作,包括部署和调用智能合约。

可以通过在大多数平台上找到的软件包来构建或安装 Geth 客户端。不同平台的安装说明可以在 github.com/ethereum/go-ethereum/wiki/Building-Ethereum 找到。

Geth 提供了一个命令行界面,可用于启动节点。一旦安装了所有依赖项的 Geth,就可以启动它以将本地区块链数据与公共区块链同步。可以通过提供几个参数来配置 Geth 客户端,用于链、交易池、性能调优、账户、网络、矿工等。以下命令使用了几个参数来实例化以太坊节点:rpc(启用 RPC 服务器)、rpcapi(通过 RPC 接口访问的 API 列表)、cache(用于内部缓存的内存分配)、rpcport(RPC 服务器端口)和 rpcaddr(RPC 服务器地址):

$ geth --rpc --rpcapi db,eth,net,web3,personal --cache=2048 
 --rpcport 8545 --rpcaddr 127.0.0.1 

实例化的以太坊节点将尝试通过连接到主网节点来同步区块链。或者,可以配置 Geth 来连接到以太坊测试网络(RinkebyKovanRopsten)。以下命令实例化了一个带有 Rinkeby 测试网络区块链的节点:

$ geth -rinkeby 

一组 Geth 客户端也可以形成自己的私有以太坊网络,而不是连接到现有的主网或测试网。

使用 Geth 客户端设置私有网络的说明可以在这里找到:github.com/ethereum/go-ethereum/wiki/Setting-up-private-network-or-local-cluster

在本章中,我们将使用Truffle套件框架中提供的工具来创建一个私有区块链,该工具称为 Ganache CLI,这是一个 JavaScript 包,可以使用 node 包管理器进行安装。在执行以下命令之前,请确保系统中安装了 nodenpm

$ npm install -g ganache-cli 

可以通过启动 Ganache CLI 来实例化私有区块链。可以通过指定几个参数来配置 Ganache CLI,也可以在没有参数的情况下启动,如下所示的命令:

$ ganache-cli 

成功启动将创建一个私有区块链,并加载了一些以太币的账户,可用于支付任何创建的交易的交易费用。 Ganache CLI 还将创建一个客户端应用程序,默认情况下将侦听端口 8545。我们将使用私有区块链和运行在端口 8545 上的应用程序,在接下来的章节中部署和查询智能合约。

设置开发环境

现在我们已经实例化了一个带有私有区块链的本地节点,我们将设置开发环境以便轻松创建和部署去中心化应用程序。由于我们需要创建一个完全成熟的去中心化应用程序,我们需要使用 JavaScript 等脚本语言与以太坊区块链进行通信。以太坊提供了一个称为 web3.js 的 JavaScript 库,其中包含与以太坊区块链交互的 API。

web3.js 使用 RPC 调用与暴露 RPC 接口(应用程序端口 8545)的以太坊节点进行通信。因此,web3.js 可以调用以太坊节点提供的任何方法。以下代码片段可在节点终端上执行:

Web3 = require('web3') 
web3 = new Web3(new 
 Web3.providers.HttpProvider("http://localhost:8545")); 
web3.eth.getBlockNumber(console.log); 

这段代码将创建一个 web3 实例,然后将本地节点添加为提供程序。当在 web 浏览器中执行 web3.js 应用程序时,web3 对象可以通过 MetaMask 等桥接注入。在构建所有权证明应用程序时,我们将使用 MetaMask。

尽管 web3.js 提供了与区块链交互所需的所有方法,但它没有设置完整的开发环境。这可以通过称为 Truffle 的以太坊开发框架来实现。Truffle 提供了一个完整的开发环境,以及方便的智能合约测试、部署和迁移。可以使用节点包管理器安装 Truffle 框架:

$ npm install -g truffle 

可以在空目录中初始化一个 Truffle 项目,以构建应用程序开发所需的初始文件:

$ truffle init 

这将创建三个目录 - contracts(智能合约)、migrations(部署脚本)和 test(测试脚本)以及一个配置文件,truffle.js。我们需要向 truffle.js 文件添加以下配置,以将创建的 Truffle 项目指向我们的私有区块链:

module.exports = { 
  networks: { 
    development: { 
      host: '127.0.0.1', 
      port: 8545, 
      network_id: '*' } 
  } 
}; 

可以从 Truffle 项目目录启动交互式 Truffle 控制台:

$ truffle console 

web3 对象将在 Truffle 控制台中已经被实例化,并且可以使用这个对象访问任何 web3 API。我们将在下一节使用 Truffle 控制台查询部署的合约。

创建去中心化应用程序

由于我们已经设置好了开发环境,我们现在可以在以太坊平台上创建我们的第一个去中心化应用程序。我们将通过创建并部署一个 hello world 应用程序来熟悉以太坊智能合约。

基本智能合约

以太坊使用一种称为 Solidity 的特定领域语言来编写智能合约的逻辑。Solidity 是一种高级编程语言,可以编译生成字节码,然后在 EVM 上执行。

Solidity 是一种由 Gavin Wood 最初提出的静态类型编程语言。它被设计为与 ECMAScript 语法类似,以便它可以被 Web 开发人员社区轻松适应。有关 Solidity 编程语言的更多细节可以在solidity.readthedocs.io找到。

我们将使用 Solidity 编程语言创建一个简单的 hello world 智能合约:

pragma solidity ⁰.4.23; 

contract Hello { 

  function greetUser(bytes user) view public returns (bytes) { 
    return abi.encodePacked("Hello ", user); 

  } 
} 

Solidity 脚本的第一行是版本pragma,用于指示 solidity 程序的版本。前面的脚本不应该在版本早于 0.4.23 的 Solidity 编译器上编译。每个合约被定义为一个类,类名与文件名相同。所有函数都被定义在合约内部。一个构造函数也可以被创建,其名称与合同的名称相同。greetUser函数接受一个bytes类型的字符串并返回一个bytes字符串。函数同时具有公共可见性,意味着它可以从任何地方调用。greetUser函数将串联并返回两个字符串。

我们将使用 Truffle 框架来部署和调用智能合约。我们需要通过在migrations文件夹内包括以下代码片段的 JavaScript 文件(2_deploy_contracts.js)或通过更新现有的1_initial_migration.js文件将智能合约文件指向 Truffle 框架:

var Hello = artifacts.require("./Hello.sol"); 
module.exports = function(deployer) { 
  deployer.deploy(Hello); 
}; 

可以使用以下 Truffle 命令编译智能合约:

$ truffle compile 

它将在build/contracts文件夹中生成一个名为应用二进制接口ABI)的接口文件。生成的 ABI 文件将是 JSON 格式的,并且它提供了在以太坊生态系统中与合约交互的接口。

然后通过迁移将合约部署到区块链上:

$ truffe migrate 
Deploying Hello... 
  ... 0x4d85f83c2ffcf1405eb7b610e0f34c99f42b4189f11fbb5ffb782b6eb4d96316 
  Hello: 0x149cd2285f8b8a72a5f8b7286aceb94fb54c1aee 

部署的合约将生成一个合约地址,可用于与之交互。我们将使用 Truffle 控制台与合约交互:

$ truffle console 
truffle(development)> 
Hello.deployed().then((instance) => instance.greetUser("Alice")); 

当在 Truffle 控制台上执行前面的代码片段以使用Alice作为参数调用合约的greetUser函数时,合约将返回0x48656c6c6f20416c696365,这是一个代表"Hello Alice"的十六进制字符串。

所有权证明应用

我们将创建一个具有资产管理功能的应用程序,用于注册、查询、移除和转移资产。

创建智能合约

应用程序的逻辑类似于 NEO 智能合约中使用的逻辑。但由于虚拟机提供的功能,用于存储资产信息的数据结构不同。以太坊合约与 NEO 合约之间的一个主要区别是,以太坊合约中的函数可以直接通过 ABI 的帮助进行调用。

ProofOfOwnership智能合约使用映射数据结构以键值对的形式存储资产所有权信息。类型为bytes32的资产信息映射到资产所有者的以太坊地址:

pragma solidity ⁰.4.23; 

contract ProofOfOwnership { 
  mapping (bytes32 => address) public assetOwners; 

registerAsset函数将调用合约的用户地址与资产 ID 映射到映射数据结构中:

  function registerAsset(bytes32 asset) public { 
    if (address(assetOwners[asset]) == address(0))
      {
        assetOwners[asset] = msg.sender;
      }
  } 

资产所有者可以通过映射数据结构中存储的信息检索:

  function queryAsset(bytes32 asset) view public returns (address) 
  { 
    return assetOwners[asset]; 
  } 

transferAsset函数在验证资产当前所有者与调用合约的用户相同时,将所有权转移到新地址:

  function transferAsset(bytes32 asset, address owner) public { 
    if (assetOwners[asset] == msg.sender) 
    { 
      assetOwners[asset] = owner; 
    } 
  } 

当资产所有者调用合约时,deleteAsset函数将为资产分配一个空地址:

  function deleteAsset(bytes32 asset) public { 
    if (assetOwners[asset] == msg.sender) 
    { 
      assetOwners[asset] = 
 0x0000000000000000000000000000000000000000; 
    } 
  } 
} 

与以前的智能合约部署类似,应在新的 JavaScript 文件(2_deploy_contracts.js)中创建以下配置,放在migrations文件夹内,或者应更新现有的1_initial_migration.js文件:

var ProofOfOwnership= artifacts.require("./ProofOfOwnership.sol"); 
module.exports = function(deployer) { 
  deployer.deploy(ProofOfOwnership); 
}; 

智能合约随后可以使用 Truffle 框架进行编译和迁移,就像前面的例子一样:

$ truffle migrate 
Deploying ProofOfOwnership... 
  ... 0x0902a793d20a3846935fffa9558fb8a2f59f74edd2ef811189c9dcdd0a0aedcc 
  ProofOfOwnership: 0xc58b4e456b840ca924ddc1c971932febec717e95 

执行智能合约

让我们考虑在 NEO 应用程序中以前使用的跟踪文档所有权的相同例子。我们将使用相同的文件,并且具有以下内容作为资产:

This document was created by Alice. 

我们将使用 md5(32 个字符)哈希算法来计算该文件的资产 ID,而不是使用 SHA256(64 个字符)。这是因为我们合约中映射数据结构的键(bytes32)只能接受 32 个字符。以下是 md5 算法生成的文件的 32 个字符或 128 位哈希值:

c9f50a3bdd2efccb7e34fbd8b42e9675 

一旦所有权智能合约部署到区块链上,就可以在 Truffle 控制台中调用。让我们使用registerAsset函数注册资产:

$ truffle console 
truffle(development)> 
ProofOfOwnership.deployed().then((instance)=>
 instance.registerAsset("c9f50a3bdd2efccb7e34fbd8b42e9675", 
 {from: "0xebe41ec4c574fde7a1d13d333d17267ca93df491"})); 

如果调用节点有多个帐户,from地址可以包含在函数调用中,以标识调用合约的用户。由于registerAsset函数执行写操作,它在执行期间需要 GAS。一旦创建交易,将显示使用的总 GAS。

transferAsset将资产 ID 与新所有者地址作为参数:

ProofOfOwnership.deployed().then((instance)=>
 instance.transferAsset("c9f50a3bdd2efccb7e34fbd8b42e9675", 
 "0xfda013eecad647a2593aacbb3c18445f051d0f52", 
 {from: "0xebe41ec4c574fde7a1d13d333d17267ca93df491"})); 

我们可以随时查询资产,检查文档的所有者:

ProofOfOwnership.deployed().then((instance)=>
 instance.queryAsset("c9f50a3bdd2efccb7e34fbd8b42e9675")); 

如果查询返回0xfda013eecad647a2593aacbb3c18445f051d0f52作为当前所有者,则我们成功执行了transferAsset函数。

应用程序的接口

通过将前端应用程序与以太坊区块链集成,可以创建一个完整的分散式应用程序。我们可以利用web3.js库提供的 API 与区块链网络交互。我们已经用 Truffle 开发环境部署和调用了智能合约。在本节中,我们将利用 Truffle 库与合约通信。

以下代码可以在任何 JavaScript 运行环境中执行,例如 Node.js。请参考本书的 GitHub 仓库,查找使用 React 库的实现。

ProofOfOwnership.json 是在合同编译期间创建的 ABI 文件。这个 ABI 对于与智能合同通信是必不可少的:

import Web3 from 'web3'; 
import { default as contract } from 'truffle-contract'; 
import contract_artifacts from 
 './contracts/ProofOfOwnership.json'; 

如果代码在安装了 MetaMask 等桥接应用程序的浏览器中执行,web3 对象将与提供程序一起注入到浏览器中。web3 对象也可以通过将提供程序设置为本地或任何其他远程 RPC 服务器节点来创建。

MetaMask 浏览器插件可以从 metamask.io 安装。安装插件后,必须创建一个帐户。用户必须将 MetaMask 指向用于开发的私有区块链。由私有区块链(通过 Ganache CLI)创建的帐户可以导入到 MetaMask 钱包中,以便用于支付交易 GAS。

const web3 = window.web3; 
if (typeof web3 !== 'undefined') 
{ 
  this.web3 = new Web3(web3.currentProvider); 
  this.user_address = this.web3.eth.accounts[0] 
} 
else 
{ 
  this.web3 = new Web3
 (new Web3.providers.HttpProvider("http://localhost:8545")); 
} 

可以从导入的 ABI 创建合同实例。然后可以使用此合同实例调用任何合同函数:

this.poo = contract(contract_artifacts); 
this.poo.setProvider(this.web3.currentProvider); 

以下函数将通过将 assetID 作为参数传递来调用合同的 registerAsset 函数。当该函数从浏览器执行时,MetaMask 将弹出一个窗口,询问是否确认交易。在确认交易后,合同函数将被执行:

registerAsset(assetID) 
{ 
  try { 
    let user_address = this.user_address; 
    this.poo.deployed().then(function(contractInstance) { 

      contractInstance.registerAsset(assetID, {gas: 1400000, from: user_address}).then(function(c) { 
        console.log(c.toLocaleString()); 
      }); 
    }); 
  } 
  catch (err) { 
    console.log(err); 
  } 
} 

类似地,查询函数将调用智能合同的 queryAsset 函数。由于 queryAsset 不会写入区块链,MetaMask 将不会创建新的交易:

  queryAsset(assetID) 
  { 
    try { 
      let user_address = this.user_address; 
      this.poo.deployed().then(function(contractInstance) { 

        contractInstance.queryAsset
 (assetID, {gas: 1400000, from: user_address}).then(function(c) { 
          console.log(c.toLocaleString()); 
        }); 
      }); 
    } 
    catch (err) { 
      console.log(err); 
    } 
  } 

所有其他的产权证明应用功能都可以用类似的方式实现。参考 GitHub 仓库(github.com/PacktPublishing/Foundations-of-Blockchain)获取应用程序的完整前端实现。

现在我们已经使用 NEO 和以太坊区块链平台实现了产权证明应用程序,我们有足够的信息来在这些平台上构建其他应用程序。由于我们还比较了两个平台的功能,我们可以根据需求决定最适合实现任何用例的平台。

总结

在本章中,我们深入探讨了创建和使用智能合同,以及使用 NEO 和以太坊平台构建去中心化应用程序。在创建 NEO 和以太坊区块链的基础之上,我们创建了一个证明资产所有权的系统。

本章希望通过向您介绍智能合同来激励您开发去中心化应用程序。在下一章中,我们将通过探索来自不同领域的项目,探索区块链技术的实际应用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值