前言
所思即所见,所见即所得,所得皆所想,技术从未停歇,也未曾缺乏。
目录
Web3.0+
区块链技术
一个不断增长的记录列表,被称为区块链中的块,这些记录使用密码学安全地链接在一起;整个区块链采用分布式且去中心的网络。
假想
- 未来互联网采用的必要技术之一。
- 产业区块链将会创造额外的价值。
- 自治下的社会体系,将会是一个公共记录的网络。
目录
区块链简史
公链
公共区块链绝对没有访问限制。任何有网络访问能力的人都可以向它发送事务,也可以成为验证者(即,参与共识协议的执行)。通常,这样的网络为那些使用某种类型的权益证明或工作量证明算法的人提供经济激励。一些最大、最知名的公共区块链,如,比特币区块链和以太坊区块链。
私链
私有的区块链是有准入权限的,除非网络管理员邀请,否则不能加入。为了区分开放区块链和其他非开放专用计算集群的点对点去中心化数据库应用程序,分布式账本(DLT)通常用于私有区块链。任何人想要加入网络必须获得区块链集中管理员的授权,公众不能直接访问。
联盟链
联盟链也是一种需要授权的区块链,但与私链不同的是,它由多个组织或机构管理,每个组织或机构管理一个或多个节点,其数据只允许系统中不同的机构读写和发送事务。联盟链的每个节点通常都有对应的实体组织,只有授权后才能加入或退出网络。各组织形成利益相关者联盟,共同维护区块链的健康运行。
混合链
混合链是一种独特的区块链技术,它结合了公共链和私有链的特点,利用公共链和私人链各自的特点,结合成为最佳解决方案,换句话说,混合链有两种状态:公共链和私有链。
侧链
侧链是与主区块链并行运行的区块链账本的名称。来自主区块链的条目(其中所说的条目通常代表数字资产)可以链接到侧链,也可以从侧链连接;这允许侧链以其他方式独立于主区块链运行(如,通过使用另一种保持记录的方法,另一种共识算法等)。
比特币
比特币是一种去中心化的数字货币,可以在点对点比特币网络上进行传输,比特币交易由网络节点通过加密技术进行验证,并记录在一个名为区块链的公共分布式账本中。
以太坊
以太坊是一个去中心化、开源的区块链,具有智能合约功能。以太币(ETH)是该平台的原生加密货币。在加密货币中,以太币的市值仅次于比特币。
莱特币
莱特币是一个分布式点对点的加密货币和开源软件项目,根据 MIT/X11 开源协议发布。在技术细节方面,莱特币主链共享一个稍微修改的比特币代码库,这些代码差异的实际效果是为了更低的交易费用,更快的交易确认以及更快的挖矿难度目标重定。由于其与比特币的潜在相似性,莱特币历来被称为“比特币的黄金之银”。2022年,莱特币通过MWEB(MimbleWimble 扩展块)升级,通过软分叉的方式添加了可选的隐私功能。
Coin 与 Token
Coin 和 Token 在基本层面上非常相似,它们都代表价值,可以处理支付,也可以用 Coin 交换 Token,反之亦然。大多数加密货币用户通常同时拥有 Coin 和 Token。如果加密货币交易由区块链处理,Coin 必须作用于区块链,而 Token 则依赖于智能合约。
Decentralized application
一个去中心化的应用程序(DApp,dApp,Dapp,dapp)是一个可以自主操作的应用程序,通常通过使用智能合约,运行在去中心化计算,区块链或其他分布式账本系统上。与传统应用程序一样,DApps 为用户提供一些功能或实用程序。然而与传统应用程序不同的是,DApps 不需要人工干预就可以运行,也不属于任何一个实体,而是 DApps 分布式的 tokens 代表其所有权。
Decentralized finance
通过在区块链上使用智能合约,由去中心化金融(DeFi)提供金融工具,而不依赖券商、交易所或银行等中介机构。DeFi 平台允许人们从他人那里借入资金,利用衍生品对资产的价格走势进行投机,交易加密货币,投保风险,并从类似储蓄的账户中赚取利息。DeFi 使用分层架构和高度可组合的模块构建,一些应用虽然促进了的高利率,但也存在较高的风险。
Decentralized for your business
区块链框架
substrate
一个面向多链未来的区块链框架。
使开发人员能够快速轻松地构建针对任何用例优化的未来证明区块链。
fabric
Hyperledger Fabric旨在作为开发具有模块化体系结构的应用程序或解决方案的基础。Hyperledger Fabric允许组件,如共识和会员服务,即插即用。它的模块化和多功能设计满足了广泛的行业用例。它提供了一种独特的达成共识的方法,能够在保护隐私的同时实现大规模性能。
EOSIO
智能合约与DApp
substrate
基本原理
- 区块链基础知识 提供了有关与区块链开发相关的复杂性以及 Substrate 如何通过采用模块化,灵活和互操作方法来简化构建过程。
- Substrate 优点 讨论在传统智能合约平台上开发与使用底层开发的区别,以及为什么 Substrate 可能适合或可能不适合你的项目需求和目标。
- Substrate 架构设计 描述了 Substrate 节点架构的关键组件,以及这些组件与定制区块链的设计和架构的关系。
- 网络和区块链 定义了不同类型区块链部署场景的网络拓扑,以及它们如何基于 Substrate 构建区块链。
- Runtime 开发 强调了 Substrate 运行时的重要性,并介绍了核心应用接口,以及 Substrate 运行时开发所需的原语。
- 共识 描述了最常见的共识模型,以及你可以为 Substrate 区块链实现的共识类型。
- 交易和区块基础 介绍了交易类型和组成区块的组件。
- 交易生命周期 解释了交易如何被接收、排队和执行,以及最终被包含在一个块中。
- 状态转换和存储 描述了如何使用 trie 数据结构和键值数据库存储和管理运行时中处理的状态更改。
- Accounts、addresses和keys 解释了 Accounts,addresses 和 keys 之间的关系以及它们的使用方式。
- Rust for Substrate 强调了 Rust 特性,包括 traits、泛型、关联类型和宏,在构建基于 Substrate 的区块链时,你应该非常熟悉。
- 链下操作 探讨了处理一些链下操作的原因,以及执行这些链下操作的备选方案。
构建与编码
- 代码库的介绍 重点强调了核心节点库和它们的结构,以提供构建 Substrate 节点的模块化框架。
- 构建过程 深入研究了 Rust Code 如何编译为 Rust Binary 和 WebAssembly 目标,以及如何使用这两个目标来优化节点操作的细节。
- 运行时存储 提供了更详细的存储结构分析,以及如何查找存储在运行时中的数据。
- 交易、权重和费用 解释了权重和费用在执行交易中的作用,以及如何计算和退还费用的机制。
- 自定义 pallets 公开了构成构建自定义 pallets 基础的宏和属性。
- Pallet 耦合 描述了如何在运行时紧密或松散地耦合 Pallet。
- 事件和错误 解释了如何从运行时发出事件和错误。
- Randomness 建议了在基于 Substrate 的区块链上运行的应用程序中包含的随机性方法。
- Chain specification 讨论 chain specifications 的使用,包括可以修改和不能修改的内容,以及如何发布定制的 chain specifications。
- Privileged calls and origins 描述如何使用预定义或自定义源来标识函数调用的发起人。
- 远程过程调用 总结了如何使用远程过程调用和 RPC 方法与 Substrate 节点进行交互。
- 应用程序开发 介绍元数据和前端库作为运行在区块链之上,构建的应用程序的工具的作用。
- 升级运行时 说明了如何对运行时进行版本控制,以及存储迁移支持运行时升级,使你的区块链能够随着时间的推移而发展。
启动区块链网络
- 构建本地区块链 向你展示了如何在开发环境中设置本地节点并与之交互。
- 模拟网络 可以帮助你使用预定义帐户模拟两个节点的网络。
- 添加可信任的节点 演示如何生成密钥和派生一个 chain specification,以创建一个小型的可信验证器节点网络。
- 监视节点指标 突出显示了如何利用 Substrate 公开的节点指标。
- 升级一个运行中的网络 说明了通过修改正在运行的 Substrate 节点的运行时进行无分叉升级。
自定义pallet
- 添加一个pallet到运行时 演示向 node template 运行时添加简单预定义 pallet 的常见步骤。
- 配置合约pallet 演示了如何配置复杂托盘以使用智能合约。
- 在自定义pallet中使用宏 说明了如何使用宏创建自定义 pallet。
- 指定调用的来源 演示了如何指定函数调用发起者的帐户。
开发智能合约
- 准备第一份合约 描述了如何使用 ink! 编程语言来更新开发环境和创建智能合约项目。
- 开发一份智能合约 演示如何使用智能合约存储、增加和检索一些简单的值。
- 使用maps存储值 通过演示如何在智能合约中使用 maps 存储和检索值,扩展了上一个教程。
- 构建一个token合约 说明了如何构建一个简单的智能合约来转移 ERC-20 代币。
- 智能合约的问题排查 介绍编写和部署智能合约时可能遇到的一些常见问题。
桥接其它链
- 准备一个本地平行链的测试网 帮助你准备一个本地的中继链作为第一步,为了将一个单链转化为一个平行链。
- 连接本地平行链 展示了将本地平行链连接到中继链所需的步骤。
- 连接Rococo测试网 总结了将平行链连接到 Rococo 测试网的步骤。
工具集成
- 集成一个轻客户端节点 描述如何使用 Substrate Connect 轻客户端节点以达到同步,并从一个浏览器与 Substrate 链进行交互。
- 访问EVM帐户 说明了如何使用自定义的 Substrate 节点访问基于以太坊的帐户和合约。
- 以太坊集成
测试
- 单元测试 解释如何使用Rust测试框架和模拟运行时环境来执行单元测试,以验证代码的单个函数或模块。
- 调试 描述如何使用 Rust 日志功能调试运行时。
- 基准测试 解释了基准测试的作用,以及如何使用基准测试框架来评估代码中函数调用的性能,还有如何调整交易权重以准确反映执行时间。
基本原理
基本原理中的主题解释了 Substrate 开发环境的许多核心原则和独特功能,并重点介绍了作为区块链构建者你可以使用的一些设计决策。Substrate 提供了一个模块化和灵活的工具库,使你能够选择、组合、修改和复用组件,以满足你想要创建的区块链的需求目的,无论是私有网络还是发布一个可以通过 Polkadot 网络与其他区块链交互的区块链。
基本原理中旨在帮助你了解基于 Substrate 构建区块链时可能遇到的问题,以及 Substrate 如何帮助你构建最能满足你指定项目需求或商业模式的区块链。
substrate 概念
什么是区块链节点
在较高的级别上,所有区块链节点都需要以下核心组件:
- 作为交易结果记录的状态变化的数据存储。
- 节点间分散通信的点对点网络。
- 共识的方法来防止恶意活动,并确保链的持续进展。
- 用于排序和处理传入交易的逻辑。
- 用于为块生成哈希摘要以及用于签名和验证与交易相关的签名的加密。
由于构建区块链所需的核心组件所涉及的复杂性,大多数区块链项目都是从现有区块链代码库的完整副本开始的,这样开发人员就可以修改现有代码以添加新特性,而不是从头编写所有内容。例如,比特币仓库分叉,创建了莱特币、ZCash、Namecoin 和比特币现金。类似地,以太坊仓库被分叉以创建 Quorum、POA Network、KodakCoin 和 Musicoin。
但是,大多数区块链平台的设计不允许修改或定制。因此,通过fork构建新的区块链存在严重的限制,包括原始区块链代码中固有的可伸缩性等限制。在探索Substrate如何缓解与其他区块链项目相关的许多限制之前,重要的是要了解大多数区块链共享的一些常见属性。通过了解大多数区块链的运行方式,你将更好地了解Substrate如何为构建最适合你需求的区块链提供替代方案和功能。
状态转换和分歧
区块链本质上是一个状态机。在任何时间点,区块链都有当前的内部状态。在执行入站交易时,它们会导致状态的更改,因此区块链必须从当前状态转换到新状态。但是,可能有多个有效的转换会导致不同的未来状态,区块链必须选择一个可以商定的状态转换。要对转换后的状态达成一致,区块链中的所有操作都必须是确定性的。为了使链成功地进行,大多数节点必须对所有的状态转换达成一致,包括:
- 链的初始状态,称为创世纪或创世纪块。
- 由每个块中记录的执行交易产生的一系列状态转换。
- 将块包含在链条中的最终状态。
在集中式网络中,中央机构可以在相互排斥的国家过渡之间进行选择。例如,配置为主要权限的服务器可能会以其看到的顺序记录对状态过渡的更改,或者在发生冲突时使用权重过程在竞争替代方案之间进行选择。在分散的网络中,节点在不同的订单中查看交易,因此他们必须使用更详细的方法来选择交易并在冲突状态过渡之间进行选择。
区块链用来将交易分解为块的方法并选择哪个节点可以向链条提交块,称为区块链的共识模型或共识算法。最常用的共识模型称为工作证明共识模型。通过工作证明共识模型,首先完成计算问题的节点有权向链条提交块。
为了使区块链具有容错性,并在某些节点被恶意行为者破坏或网络中断时提供一致的状态视图,一些共识模型要求至少三分之二的节点始终对状态达成一致。这三分之二的多数保证了网络是容错的,并且可以承受一些网络参与者的不良行为,无论这种行为是有意的还是偶然的。
区块链经济
所有区块链都需要资源处理器、内存、存储和网络带宽来执行操作。参与网络的计算机(产生区块的节点)向区块链用户提供这些资源。这些节点创建了一个分布式的、去中心化的网络,以满足社区参与者的需求。
为了支持一个社区并使区块链可持续发展,大多数区块链要求用户以交易费用的形式为他们使用的网络资源付费。交易费用的支付要求用户身份与持有某种类型资产的账户相关联。区块链通常使用代币来代表账户中的资产价值,网络参与者通过交易所在链外购买代币。然后,网络参与者可以存入代币,使他们能够支付交易。
区块链治理
一些区块链允许网络参与者提交并投票影响网络运营或区块链社区的提案。通过对提案的提交和投票,区块链社区可以决定区块链如何在基本的民主进程中发展。然而,链上治理相对少见,为了参与,区块链可能需要用户在帐户中维护大量代币,或被选为其他用户的代表。
在区块链上运行应用程序
运行在区块链上的应用程序通常被称为去中心化应用程序或 dApps,它们通常是使用前端框架编写的web应用程序,但使用后端智能合约来更改区块链状态。
智能合约是在区块链上运行的程序,在特定条件下代表用户执行交易。开发人员可以编写智能合约,以确保通过编程执行的交易的结果被记录下来,不会被篡改。然而,仅使用智能合约,开发人员无法访问一些底层区块链功能,如共识、存储或交易层,相反,需要遵守链的固定规则和限制。智能合约开发者通常会接受这些限制,并将其作为一种权衡,即能够在更短的开发时间内做出更少的核心设计决策。
substrate 优点
所有的区块链都有一些共同的特征。虽然 Substrate 本身不是区块链,但它是区块链构建器的工具包,带有模块化的组件框架,可以创建自定义区块链。使用 Substrate,你可以使用常见的区块链组件,如存储、共识和密码学,并将它们组合起来以按原样使用它们提供的功能,或者修改它们以适应项目的目的。
区块链开发是复杂的。它涉及到复杂的技术,包括高级密码学和分布式网络通信,你必须正确使用这些技术,以便为运行应用程序和用户信任提供安全的平台。在规模、治理、互操作性和可升级性方面存在一些难以解决的问题。复杂性为开发人员创造了很高的准入门槛。考虑到这一点,要回答的第一个问题是:你想要构建什么?
Substrate 并不完全适合每个用例、应用程序或项目。但是,如果你想构建一个区块链,那么 Substrate 可能是最佳选择:
- 针对一个非常具体的用例进行定制
- 能够与其他可定制的区块链连接和通信
- 预定义的可组合模块化组件能够随着时间的升级而演变和变化
Substrate是一个专门设计的软件开发工具包(SDK),旨在为你提供区块链所需的所有基本组件,以便你能够专注于打造使你的区块链独特和创新的逻辑。与其他分布式账本平台不同,Substrate 是:
- 大多数区块链平台都有非常紧密耦合的、固执己见的子系统,很难解耦。基于另一个区块链分叉的链也存在风险,那些不明显的耦合可能从根本上破坏区块链系统本身。Substrate是一个完全模块化的区块链框架,通过选择适合你项目的网络堆栈、共识模型或治理方法或创建自己的组件,你可以使用显式解耦的组件组成链。使用 Substrate,你可以为你的规范设计部署和构建区块链,它也可以随你的需求变化而变化。
- 所有的 Substrate 架构和工具都可以在开源许可下使用。Substrate 框架的核心组件使用
libp2p
和jsonRPC
等开放协议,同时允许你决定对区块链架构的自定义程度。Substrate 还拥有一个大型的、活跃的、有帮助的构建者社区,为生态系统做出贡献。来自社区的贡献增强了你在自身区块链发展过程中融入的能力。 - 大多数区块链平台提供的与其他区块链网络交互的能力有限。所有基于 Substrate 的区块链都可以通过跨共识消息传递(XCM)与其他区块链互操作。Substrate 可用于创建作为独立网络的链(单链),也可以中继链紧密耦合,以共享其作为平行链的安全性。
- Substrate 是可升级、可组合和可适应的。Substrate 运行时的状态转换逻辑是一个自包含的 WebAssembly 对象。你的节点可以在特定条件下完全更改运行时本身,从而在网络范围内实现运行时升级。因此,可以进行“分叉”升级,因为在大多数情况下,节点在这个新运行时中都不需要任何操作。随着时间的推移,网络的运行时协议可以无缝地、甚至是彻底地随着用户的需求发展。
substrate 架构设计
由于节点是任何区块链的核心组件,因此了解 Substrate 节点的独特性非常重要,包括默认提供的核心服务和库,以及如何定制和扩展节点以适应不同的项目目标。
在去中心化网络中,所有节点既充当请求数据的客户机,又充当响应数据请求的服务器。在概念上和编程上,Substrate 体系结构按照类似的方式划分操作职责。下图以简化形式说明了这种职责分离,以帮助你可视化架构以及 Substrate 如何为构建区块链提供模块化框架。
在较高的层次上,Substrate 节点提供了具有两个主要元素的分层环境:
- 一种处理网络活动的外部节点,如对等点发现、管理交易请求、与对等点达成共识以及响应 RPC 调用。
- 一个包含用于执行区块链状态转换函数的所有业务逻辑的运行时。
外部节点
外部节点负责运行时之外发生的活动。例如,外部节点负责处理对等点发现、管理交易池、与其他节点通信以达成共识,以及应答来自外部世界的 RPC 调用或浏览器请求。
外部节点处理的一些最重要的活动涉及以下组件:
- 存储:外部节点使用简单而高效的键值存储层持久化 Substrate 区块链的演进状态。
- 点对点网络:外部节点使用 libp2p 网络栈的 Rust 实现与其他网络参与者通信。
- 共识:外部节点与其他网络参与者通信,以确保他们对区块链的状态达成一致。
- 远程过程调用(RPC)API:外部节点接受入站HTTP和WebSocket请求,以允许区块链用户与网络交互。
- 遥测:外部节点通过嵌入的 Prometheus 服务器收集并提供对节点指标的访问。
- 执行环境:外部节点负责为运行时选择要使用的执行环境 WebAssembly 或本地 Rust 程序,然后分发调用所选的运行时。
执行这些任务通常需要外部节点向运行时查询信息或向运行时提供信息。这种通信通过调用专门的运行时 APIs 来处理。
运行时
运行时确定交易是有效还是无效,并负责处理对区块链状态转换函数的更改。
因为运行时执行它接收到的函数,所以它控制如何将交易包含在块中,以及如何将块返回给外部节点以进行传播或导入到其他节点。本质上,运行时负责处理链上发生的所有事情。它也是构建 Substrate 区块链节点的核心组件。
Substrate 运行时被设计用来编译成 WebAssembly(Wasm)字节码。此设计决策会强制以下功能:
- 支持无分叉升级。
- 多平台兼容性。
- 运行时有效性检查。
- 中继链共识机制的验证证明。
与外部节点向运行时提供信息的方式类似,运行时使用专门的主机函数与外部节点或外部世界通信。
轻客户端节点
轻客户端或轻节点是只提供运行时和当前状态的 Substrate 节点的简化版本。轻节点使用户能够直接使用浏览器、浏览器扩展、移动设备或台式电脑直接连接到 Substrate 运行时。使用轻客户端节点,你可以使用 Rust,JavaScript 或其他语言编写的 RPC 端点连接到 WebAssembly 执行环境,以读取块头,提交交易并查看交易结果。
网络和区块链
在考虑构建区块链时,考虑到边界是定义网络的方法,这是有用的。例如,连接到单个路由器的一组计算机可以视为家庭网络。防火墙可能是定义企业网络的边界。较小的隔离网络可以通过公共通信协议连接到广域网。类似地,你可以将区块链网络视为由其边界与其他区块链的隔离或通信来定义的。
作为区块链的构建工具,Substrate 使你能够开发任何类型的区块链,并根据你的应用程序的特定需求定义其边界。考虑到这种灵活性,你需要做出的决定之一是要构建的网络类型以及不同节点在该网络中可能扮演的角色。
网络类型
基于 Substrate 的区块链可用于不同类型的网络架构。例如,Substrate 区块链用于构建以下网络类型:
- 私有网络:限制访问,受限节点集群的网络。
- 单独链:实现自己的安全协议,不与任何其他链连接或通信。比特币和以太坊是基于非 substrate 的单链的例子。
- 中继链:为连接到它们的其他链提供分散安全性和通信。Kusama 和 Polkadot 是中继链的例子。
- 平行链:连接到一个中继链,并有能力与使用同一中继链的其他链通信。因为平行链依赖于中继链来最终确定产生的区块,平行链必须实现与其目标中继链相同的共识协议。
节点类型
区块链需要对网络节点进行同步,以呈现区块链状态的一致和最新情况。每个同步节点存储一个区块链副本,并跟踪传入的交易。然而,保存整个区块链的完整副本需要大量的存储和计算资源,从创世区块到最近的区块,下载所有区块对于大多数用例来说并不实用。为了更容易地维护链的安全性和完整性,同时降低客户端希望访问区块链数据的资源需求,可以使用不同类型的节点与链进行交互。
- 全节点
- 归档节点
- 轻客户端节点
全节点
全节点是区块链网络基础设施的关键部分,是最常见的节点类型。全节点存储区块链数据,通常参与常见的区块链操作,例如创建和验证块,接收和验证交易,以及响应用户请求提供数据服务。
默认情况下,全节点配置为仅存储最近的256个块,并丢弃比该块旧的状态(创世块除外),以防止全节点无限增长并消耗所有可用磁盘空间。可以配置完整节点保留的块数。
虽然旧的块被丢弃,但全节点保留了从创世块到最近块的所有块头,以验证状态是否正确。因为全节点可以访问所有的块头,所以可以通过执行从创世块开始的所有块来重建整个区块链的状态。因此,检索以前某个状态的信息需要更多的计算,通常应该使用归档。
全节点允许你读取链的当前状态,并直接在网络上提交和验证交易。通过丢弃旧块的状态,全节点所需的磁盘空间比归档节点少得多。然而,一个全节点需要更多的计算资源来查询和检索关于以前某个状态的信息。如果需要查询历史块,应该清除整个节点,然后以存档模式重新启动它。
归档节点
归档节点类似于全节点,除了它们存储所有过去的块,每个块都有完整的状态可用。归档节点通常用于需要访问历史信息的实用程序,如区块链浏览器、钱包、论坛和类似应用程序。
由于存档节点保留历史状态,因此需要大量磁盘空间。由于运行归档节点需要大量的磁盘空间,因此归档节点不像全节点那样常见。然而,归档节点可以方便地在任何时间点查询链的过去状态。例如,你可以通过查询归档节点来查询某个区块中的帐户余额,或者查看导致特定状态更改的交易的详细信息。当你在归档节点中的数据上运行这些类型的查询时,它们会更快、更有效。
轻客户端节点
轻客户端节点使你能够以最少的硬件资源连接到 Substrate 网络。
由于轻客户端节点需要最少的系统资源,所以它们可以嵌入到基于 web 的应用程序、浏览器扩展程序、移动设备应用程序或物联网设备(IoT)中。轻客户端节点通过 RPC 端点提供运行时和对当前状态的访问。轻客户端节点的 RPC 端点可以用 Rust、JavaScript 或其他语言编写,用于读取块头、提交交易和查看交易结果。
轻客户端节点不参与区块链或网络操作。例如,轻客户端节点不负责区块的创建或验证、传播交易或达成共识。轻客户端节点不存储任何过去的块,因此如果不从拥有历史数据的节点请求历史数据,它就无法读取历史数据。
节点角色
根据启动节点时指定的命令行选项,节点可以在链的进程中扮演不同的角色,并可以提供对链上状态的不同级别的访问。例如,你可以限制被授权出新块的节点以及哪些节点可以与点对点节点通信。未被授权为出块的点对点节点可以导入新的区块,接收交易,并向其他节点发送和接收关于新交易的消息。还可以阻止节点连接到更广泛的网络,并限制节点与特定的节点通信。
Runtime 开发
如架构中所述,Substrate 节点的运行时包含执行交易、保存状态转换以及与外部节点交互的所有业务逻辑。Substrate 提供了构建常见区块链组件所需的所有工具,因此你可以专注于开发定义区块链行为的运行时逻辑。
状态转换和运行时
在最基本的层面上,每个区块链本质上都是在链上发生的每个变化的账本或记录。在基于 Substrate 的链中,这些对状态的变更被记录在运行时。由于运行时处理此操作,因此有时将运行时描述为提供状态转换的函数。
由于状态转换发生在运行时,因此运行时是你定义用于表示区块链状态的存储项,以及允许区块链用户更改此状态的交易。
Substrate 运行时确定哪些交易有效和无效,以及如何响应交易,更改链状态。
运行时接口
正如你在架构设计中所学到的,外部节点负责处理点对点网络的发现、交易池、区块与交易的传播、共识,以及响应来自外部世界的 RPC 调用。这些任务经常需要外部节点向运行时查询信息或向运行时提供信息。运行时API促进了外部节点和运行时之间的这种通信。
在Substrate中,sp_api
crate 提供了实现运行时 API 的接口。它旨在让你能够灵活地使用 impl_runtime_apis 宏来自定义接口。然而,每个运行时都必须实现 Core 和 Metadata 接口。除了这些必需的接口之外,大多数 Substrate 节点(如 node template)都实现了以下运行时接口:
- BlockBuilder: 用于构建区块所需的功能。
- TaggedTransactionQueue:用于验证交易。
- OffchainWorkerApi:用于启用链下操作。
- AuraApi:用于使用 round-robin 的共识方法进行区块的创建和验证。
- SessionKeys:用于生成和解码 session 密钥。
- GrandpaApi:用于在运行时确认区块的终结。
- AccountNonceApi:用于查询交易索引。
- TransactionPaymentApi:用于查询有关交易的信息。
- Benchmark:用于估计和衡量完成交易所需的执行时间。
核心原语
Substrate 还定义了运行时必须实现的核心原语。Substrate 框架对运行时必须提供给其他 Substrate 层的内容做了最少的假设。然而,有一些数据类型必须被定义,并且必须满足特定的接口才能在 Substrate 框架中工作。 这些核心原语是:
Hash
:一种对某些数据的密码摘要进行编码的类型。通常只有256位的数量。DigestItem
:一种类型,必须能够编码,共识和 change-tracking 相关的多个“hard-wired”备选方案之一,以及与运行时内特定模块相关的任意多个“soft-coded”的变体。Digest
:一系列 DigestItems。会编码与轻客户端在区块内相关的所有信息。Extrinsic
:表示可以被区块链识别的外部的单个数据的类型。这通常涉及一个或多个签名,以及某些编码指令(例如,用于转移资金所有权或调用智能合约)Header
:一种类型代表(以密码方式或其他方式)与一个块相关的所有信息。它包括 parent hash、the storage root 和 extrinsics trie root、the digest和区块号。Block
:本质上只是Header
和一系列Extrinsic
元素的组合,以及要使用的哈希算法的规范。BlockNumber
:一种对任何有效区块的历史总数进行编码的类型。通常是32位数量。
FRAME
作为运行时开发人员,FRAME 是可用的且最强大的工具之一。正如 Substrate 支持开发人员中提到的,FRAME 是模块化实体运行时聚合框架(Framework for Runtime Aggregation of Modularized Entities)的缩写,它包含了大量模块和支持库,也简化运行时开发。在 Substrate 中,这些被称为 pallets 的模块,可以为你提供希望在运行时中包含的不同用例和特性的自定义业务逻辑。例如,有一些 pallets 为 staking、共识、治理和其他常见活动提供了业务逻辑框架。
除了 pallets 之外,FRAME 还通过以下库和模块提供与运行时交互的服务:
- FRAME system crate
frame_system
:为运行时提供底层的类型,存储和功能。 - FRAME support crate
frame_support
:是 Rust宏、类型、特性和模块的集合,可简化 Substrate pallets 的开发。 - FRAME executive pallet
frame_executive
:在运行时编排各个 pallets 调用函数的执行。
下图说明了 FRAME 及其 system、support 和 executives modules 如何为运行时环境提供服务。
使用 pallets 构建运行时
你可以无需使用 FRAME,仅基于 Substrate 构建区块链。但是,FRAME pallets 可以让你使用预定义组件开始来构建你的自定义运行时逻辑。每个 pallet 定义特定 types、storage items 和 functions,为运行时实现一组特定的特性或功能。
下图说明了如何选择和组合 FRAME pallets 以组成运行时。
构建自定义 pallets
除了预构建 FRAME pallets 库之外,你还可以使用 FRAME 库和服务构建自己的自定义托盘。使用自定义托盘,你可以灵活地定义最适合你期望的运行时行为。由于每个 pallets 都有自己的独立逻辑,因此你可以将预构建和自定义 pallets 组合起来,以控制区块链提供的特性和功能,并实现你想要的结果。
例如,你可以在运行时包含 Balances pallet,以使用其加密货币相关的 storage items 和 functions 来管理 tokens;但在一个账余额改变时,也可以添加自定义逻辑调用你编写的 pallet。
大多数 pallet 由以下部分组成:
- Imports and dependencies
- Pallet type 声明
- Runtime configuration trait
- Runtime storage
- Runtime events
- Hooks:用于应该在特定上下文中执行的钩子逻辑
- Function:可用于执行交易的函数调用
例如,如果要定义自定义 pallet,可以从 pallet 的代码结构开始,如下所示:
// Add required imports and dependencies
pub use pallet::*;
#[frame_support::pallet]
pub mod pallet {
use frame_support::pallet_prelude::*;
use frame_system::pallet_prelude::*;
// Declare the pallet type
// This is a placeholder to implement traits and methods.
#[pallet::pallet]
#[pallet::generate_store(pub(super) trait Store)]
pub struct Pallet<T>(_);
// Add the runtime configuration trait
// All types and constants go here.
#[pallet::config]
pub trait Config: frame_system::Config { ... }
// Add runtime storage to declare storage items.
#[pallet::storage]
#[pallet::getter(fn something)]
pub type MyStorage<T: Config> = StorageValue<_, u32>;
// Add runtime events
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> { ... }
// Add hooks to define some logic that should be executed
// in a specific context, for example on_initialize.
#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> { ... }
// Add functions that are callable from outside the runtime.
#[pallet::call]
impl<T:Config> Pallet<T> { ... }
}
你可以根据需要使用部分或所有 pallets 去组合。在开始设计和构建自定义运行时时,你将了解更多关于 FRAME 库和用于定义 configuration traits、storage items、events和 errors 的运行时原语,以及如何编写分派给运行时执行的调用函数。
共识
所有区块链都需要某种类型的共识机制来商定区块链状态。由于 Substrate 提供了构建区块链的模块化框架,因此它支持节点达成共识的几种不同模型。一般来说,不同的共识模型有不同的权衡,因此选择要用于链的共识类型是一个重要的考虑因素。Substrate 默认支持的共识模型只要最少的配置,但如果需要,也可以构建自定义的共识模型。
两阶段的共识
与某些区块链不同,Substrate将达成共识的要求分为两个单独的阶段:
- Block authoring 是节点用于创建新块的过程。
- Block finalization 是用于处理分叉和选择合法链的过程。
Block authoring
在达成共识之前,区块链网络中的一些节点必须能够产生新的区块。区块链如何决定被授权的可以创建块的节点取决于你使用的是哪种共识模型。例如,在集中式网络中,单个节点可能创建所有块。在没有任何可信节点的完全去中心化网络中,算法必须在每个块高度选择区块创建者。
对于基于 Substrate 的区块链,你可以选择以下区块创建算法之一或你自己的创建区块的算法:
- 基于授权的 round-robin 调度(Aura)。
- 基于插槽 Blind assignment of blockchain extension(BABE)调度。
- 基于算力的工作量证明(PoW)调度。
Aura 和 BABE 共识模型要求你拥有一组已知的验证者节点,允许它们生成块。在这两种共识模型中,时间被划分为离散的插槽。在每个插槽中,只有一些验证者可以产生一个块。在 Aura 共识模型中,可以创建区块的验证者以 round-robin 方式不断轮询。在 BABE 共识模型中,验证者的选择是基于可验证的随机函数(VRF),而不是 round-robin 的选择方法。
在工作量证明的共识模型中,任何节点只要解决了一个计算密集型问题,就可以在任何时间产生一个块。解决这个问题需要花费 CPU 时间,因此节点只能产生的块与其计算资源成正比。Substrate 提供了一个工作量证明的区块生产引擎。
Finalization and forks
作为原始块,一个块包含块头和交易。每个块头都包含对其父块的引用,因此可以将链追溯到其创世块。当两个块引用同一个父块时,就会发生分叉。块终结是一种解决分叉的机制,使之只存在合法链。分叉选择规则是一种应该选择被扩展为最佳链的算法。Substrate 通过SelectChain trait 公开了这个分叉选择规则。你可以使用这个 trait 编写自定义的分叉选择规则,或者使用 GRANDPA,这个 Polkadot 和类似链中使用的终结机制。
在 GRANDPA 协议中,最长链规则简单地说,最佳的链就是最长的链。Substrate 用 LongestChain 结构体提供这个链选择规则。GRANDPA 在投票机制中使用最长链规则。
“The Greedy Heaviest Observed SubTree”(GHOST)规则说,从创世块开始,每个分叉都是通过选择递归构建最多块的分支,然后确定最佳链。
确定性终结
用户很自然地想知道交易何时完成,以及何时发出被签名的某些事件(如收据交付或文件签名)。然而,根据目前所描述的区块创建和分叉选择规则,交易还没有完全终结。总是有可能出现较长或更重的链会还原之前的交易。然而,在特定的块上构建的块越多,其被还原的可能性就越小。通过这种方式,块的创建以及适当的分叉选择规则提供了概率终结性。
如果你的区块链需要确定性的终结,则可以为区块链逻辑添加最终机制。例如,你可以让拥有固定权限的一些成员进行最终投票。当对某一区块投下足够的票时,该区块被视为最终区块。在大多数区块链中,该投票比例为三分之二。如果没有外部协调(如硬分叉),则无法还原已完成的块。
在一些共识模型中,结合了块生产和块最终性,并且在块 N
最终确定之前,不能创建新的块 N+1
。如你所见,在 Substrate 中,这两个过程是相互分开的。通过将块创建与块终结分离,Substrate 让你能够使用任何具有概率终结性的块创建算法,或将其与终结性机制结合以实现确定性终结性。
如果你的区块链使用终结性机制,则必须修改分叉选择规则以匹配终结性投票的结果。例如,一个节点不会使用只是最长链的原则,而是使用包含最近终结区块的最长链。
默认共识模型
虽然你可以实现自己的共识机制,但默认情况下, Substrate node template 包括用于区块创建的 Aura 和区块最终确定的 GRANDPA。Substrate 还提供了 BABE 和工作量证明共识模型的实现。
Aura
Aura 提供了一种基于插槽的区块创建机制。在 Aura 中是一组已知的授权轮询区块创建。
BABE
BABE 使用一组已知的验证者提供基于插槽的区块创建,通常用于权益证明区块链。与 Aura 不同,插槽分配基于对可验证的随机函数(VRF)的评估。每个验证者都会为一个 epoch 分配权重。这个 epoch 被分解成多个插槽,验证者在每个槽上计算它的 VRF。对于验证者的 VRF 输出低于其权重的每个槽,允许创建一个块。
因为多个验证者可以在同一个插槽中产生一个块,所以即使在良好的网络条件下,分叉在 BABE 中也比在 Aura 中更常见。
Substrate 的 BABE 实现还有一个回退机制,用于在给定的插槽内没有选择授权。这些次要插槽分配让 BABE 实现了恒定的出块时间。
Proof of work
Proof-of-work 创建区块不是基于插槽的,不需要已知的授权集。在工作量证明中,任何人都可以在任何时间生成一个块,只要他们能解决一个具有计算挑战性的问题(通常是哈希值逆向查找)。该问题的难度可以作为调整统计目标块的出块时间。
GRANDPA
GRANDPA 提供区块确定性终结。它有一个已知的权重授权集,像 BABE。然而,GRANDPA 不会创建区块。它只是监听关于节点生成区块传播的消息。GRANDPA 验证者在链上投票,而不是块上。GRANDPA 验证者对他们认为最好的区块进行投票,并将其投票应用于所有以前的区块。在三分之二的 GRANDPA 授权者投票支持某一指定的区块后,该区块将被视为最终区块。
所有确定性终结算法,包括 GRANDPA 算法,都需要至少 2f+1
个非故障节点,其中 f
是故障或恶意节点的数量。了解更多关于这个阈值来自何处以及为什么它是理想中的值,请参阅具有开创性的论文《Reaching Agreement in the Presence of Faults》或《Byzantine Fault》。
并非所有共识协议都定义一个单一的,合法的区块链。当具有相同父块的两个块没有冲突的状态变化时,一些协议证实有向无环图(DAG)。
交易和区块基础
在本文中,你将了解可以创建的不同类型的交易,以及如何在运行时使用它们。广义而言,交易决定了进入区块链中区块的数据。通过了解如何使用不同的交易类型,你可以更好地根据需求选择适当的类型。
什么是交易?
一般来说,交易提供了一种可在区块中更改状态的机制。在 Substrate 中有三种不同的交易类型:
- 签名交易
- 未签名交易
- 内部交易
在 Substrate 中,这三种交易类型通常更广泛地称为 extrinsics。术语 extrinsics 通常用于表示源于运行时之外的任何信息。
然而,出于实际目的,独立考虑每种交易类型并确定每种类型最适用的场景更为有用。
签名交易
已签名的交易必须包括发送入站请求以执行一些运行时调用的帐户的签名。通常,使用提交请求的帐户的私钥对请求进行签名。在大多数情况下,提交请求的账户也会支付交易费。然而,交易费用和交易处理的其他元素取决于运行时逻辑如何被定义。
签名交易是最常见的交易类型。例如,假设你有一个带有一些代币的帐户。如果你想将代币转给 Alice,那么可以在 Balances pallet 中调用 pallet_balances::Call::transfer
函数。因为你的帐户被用作此功能的调用,所以你的帐户密钥用于对交易进行签名。作为请求者,你通常需要支付一定的费用来处理你的请求。或者,你也可以提示区块创建者给你的交易更高的优先级。
未签名交易
未签名的交易不需要签名,也不包含任何关于提交交易的人的信息。
对于未签名的交易,没有经济约束来防止垃圾邮件或重播攻击。你必须定义验证未签名交易的条件,以及保护网络免受误用和攻击所需的逻辑。由于未签名交易需要自定义验证,因此此交易类型会比已签名交易类型消耗更多的资源。
这个 pallet_im_online::Call::heartbeat
功能使用未签名交易来使验证者节点向网络发送一个信号,表明该节点在线。此函数只能由在网络中注册为验证者的节点调用。该函数包含用于验证节点是否为验证者的内部逻辑,允许节点使用未签名交易调用该函数,以避免支付任何费用。
内部交易
Inherent transactions(有时称为inherents)是一种特殊类型的未签名交易。通过这种类型的交易,块创建节点可以直接向块添加信息。内部交易只能由调用它们的块创建节点插入到块中。通常,这种类型的交易不会传播给其他节点或存储在交易队列中。假设使用内部交易插入的数据是有效的,无需特定验证。
例如,如果块创建节点将时间戳插入到块中,则无法证明时间戳是准确的。相反,验证者可以根据时间戳是否在其自身系统时钟的可接受范围内来接受或拒绝该块。
例如,pallet_timestamp::Call::now
函数允许一个块创建节点在生成每个块中插入当前时间戳。同样,paras_inherent::Call::enter
函数允许一个平行链 collator 节点能够向其中继链发送中继链期望的验证数据。。
什么是块
在 Substrate 中,一个块由一个块头和一个交易数组组成。块头包含以下属性:
- Block height
- Parent hash
- Transaction root
- State root
- Digest
所有交易都作为一个系列捆绑在一起,按照在运行时中定义的方式执行。你将在交易生命周期中了解有关交易排序的更多信息。Transaction root 是本系列的加密摘要。此加密摘要有两个用途:
- 它防止在构建和分散标头后对一系列交易进行任何更改。
- 它使轻客户端能够简洁地验证任何给定的交易是否存在于仅给出区块头部信息的块中。
交易生命周期
在 Substrate 中,交易包含的数据被包含在块中。因为交易中的数据来源于运行时之外,所以交易有时更广泛地称为外部数据或extrinsics。然而,最常见的extrinsics是已签名的交易。因此,对交易生命周期的讨论是关注已签名交易的验证和执行方式。
你已经了解到,已签名的交易包括签名账户发送请求以执行某个运行时的调用。通常,使用提交请求帐户的私钥,对请求进行签名。在大多数情况下,提交请求的账户也会支付交易费。然而,交易费用和交易处理中的其他元素取决于如何定义运行时逻辑。
定义交易的地方
如运行时开发中所述,Substrate node 运行时包含定义交易属性的业务逻辑,包括如下:
- 什么构成有效交易。
- 交易是以签名还是未签名的方式发送。
- 交易如何改变链的状态。
通常,你可以使用 pallets 来组建运行时的功能,并实现你希望区块链支持的交易。在编译运行时后,用户通过提交处理交易的请求与区块链进行交互。例如,用户可能会提交从一个帐户向另一个帐户转账的请求。该请求将成为一个已签名的交易,其中包含该用户帐户的签名,如果用户帐户中有足够的资金支付该交易,则该交易将成功执行,并进行转账。
在区块创建的节点上如何处理交易
根据网络的配置,你可能会同时拥有授权创建块的节点和未授权创建块的节点。如果一个 Substrate 节点被授权生成块,它就可以处理接收到的已签名和未签名交易。下图说明了一个交易的生命周期,它由区块创建节点处理并提交到网络。
发送到非区块创建节点的任何已签名或未签名的交易都将被传播至网络中的其他节点,并进入它们的交易池,直到被区块创建节点接收为止。
验证和排队交易
如共识中所述,网络中的大多数节点必须就区块中的交易顺序达成一致,才能就区块链的状态达成一致,并继续安全地添加区块。为了达成共识,三分之二的节点必须就执行交易的顺序以及由此产生的状态变化达成一致。为了准备达成共识,首先对交易进行验证,并在本地节点上的交易池中排队。
验证交易池中的交易
使用运行时中定义的规则,交易池检查每个交易的有效性。这些检查确保只有满足特定条件的有效交易才会排队包含到区块中。例如,交易池可能会执行以下检查以确定交易是否有效:
- 交易索引也称为交易随机数,是否正确?
- 用于签名交易的账户是否有足够的资金支付相关费用?
- 用于签署交易的签名,是否有效?
在初始有效性检查之后,交易池定期检查池中的现有交易是否仍然有效。如果发现交易无效或已过期,则把该交易从池中删除。
交易池仅处理交易的有效性以及对放置在交易队列中的有效交易进行排序。有关验证机制如何工作的具体细节,包括处理费用、帐户或签名,可以在 validate_transaction
方法中找到。
向交易队列中添加有效的交易
如果交易被标记为有效,则交易池将该交易移动到交易队列中。有效交易有两个交易队列:
- ready queue 包含在新的待生成区块中的交易。如果运行时是用 FRAME 构建的,则交易必须符合它们在 ready queue 中的确切顺序。
- future queue 包含将来可能生效的交易。例如,如果交易的
nonce
对于其账户来说太高,它可以在 future queue 中等待,直到有一定数量交易的帐户已经被包含在链中。
无效交易的处理
如果交易是无效的,例如,因为它太大或不包含有效签名,它被拒绝添加到一个块中。交易可能因以下任何原因而被拒绝:
- 交易已经包含在一个块中,所以它被从验证队列中删除。
- 交易的签名无效,因此它将立即被拒绝。
- 该交易太大,无法放入当前块中,因此需要将其放回队列以进行新一轮验证。
按优先级排序的交易
如果某个节点是下一个区块的创建者,则该节点使用优先级系统为下一个区块排序交易。交易从高优先级到低优先级排序,直到块达到最大权重或长度。
交易优先级是在运行时计算的,并作为交易的标签提供给外部节点。在 FRAME 运行时中,使用一个特殊的 pallet,它根据与交易相关的权重和费用计算优先级。此优先级计算适用于所有类型的交易,但内部交易除外。内部交易始终优先使用 EnsureInherentsAreFirst trait。
基于帐户的交易排序
如果你的运行时是使用 FRAME 构建的,那么每个已签名的交易都包含一个 nonce,该 nonce 在指定的帐户每次进行新交易时递增。例如,新账户的第一笔交易的 nonce = 0
,同一账户的第二笔交易的 nonce = 1
。块创建节点可以在对包含在块中的交易进行排序时使用 nonce。
对于具有依赖关系的交易,排序要考虑交易支付的费用以及它所包含的对其他交易的依赖。例如:
- 如果存在
TransactionPriority::max_value()
的未签名交易和另一个已签名交易,未签名的交易则会被放在队列的第一位。 - 如果有来自不同发件人的两个交易,
priority
决定了哪个交易更重要,应该首先被包含在块中。 - 如果来自同一发送者的两项交易具有相同的
nonce
:在块中只能包含一个交易,因此,队列中只包含费用较高的交易。
执行交易和产生块
在将有效的交易放入交易队列后,一个单独的执行模块协调了如何执行交易以生成块。执行模块调用运行时模块中的函数,并按特定顺序执行这些函数。
作为运行时开发人员,了解执行模块如何与 system pallet 以及构成区块链业务逻辑的其他 pallet 交互非常重要,因为你可以插入执行模块的逻辑,以作为以下操作的一部分执行:
- 初始化一个块
- 执行要包含在块中的交易
- 最终确定一个块的构建
初始化一个块
为了初始化一个块,执行模块首先在 system pallet 中调用 on_initialize
函数,然后在所有其他运行时 pallets 中。on_initialize
函数允许你定义应该在执行交易之前完成的业务逻辑。system pallet on_initialize
函数始终首先会被执行。其余的 pallets 将按照它们在 construct_runtime!
宏 中定义的顺序被调用。
在执行所有 on_initialize
函数之后,执行模块会检查在块头和 trie root 中的父哈希,以验证信息是否正确。
执行交易
块初始化后,将按照交易优先级的顺序执行每个有效交易。重要的是要记住,在执行之前不会缓存状态。相反,状态更改在执行期间直接写入存储。如果交易在执行过程中失败,则在失败之前发生的任何状态更改都不会恢复,使块处于不可恢复状态。在向存储提交任何状态更改之前,运行时逻辑应执行所有必要的检查,以确保 extrinsic 成功。
请注意,事件也写入存储。因此,运行时逻辑不应在执行补充操作之前发出事件。如果交易在事件发出后失败,则不会恢复该交易。
最终确定一个块
在执行了所有已排序的交易之后,执行模块通过调用每个 pallet 的 on_idle
和 on_finalize
函数,来执行任何应该在块的末尾发生的最终业务逻辑。这个模块被再次按照它们在 construct_runtime!
宏 中定义的顺序执行。但在这种情况下,on_finalize
函数在 system pallet 中是最后被执行。
在所有 on_finalize
函数都已被执行之后,执行模块会检查块头中的摘要和存储根是否与块初始化时计算的相匹配。
on_idle
函数还通过块的剩余权重,以允许基于区块链继续使用执行。
区块创建和区块导入
到目前为止,你已经看到了交易如何被包含在本地节点生成的块中。如果授权本地节点生成块,则交易生命周期遵循如下方式:
- 本地节点监听网络上的交易。
- 每笔交易都要经过验证的。
- 有效的交易被放置在交易池中。
- 交易池在适当的交易队列中对有效交易进行排序,执行模块调用运行时以开始下一个块。
- 执行的交易与状态更改被存储在本地内存中。
- 将构造的区块发布到网络。
将块发布到网络后,其他节点可以导入该块。块导入队列是每个 Substrate node 作为外部节点的一部分。通过监听传入的块和共识相关消息把块导入队列,并将它们添加到池中。在池中,检查传入信息的有效性,如果无效则丢弃。在验证块或信息有效后,块导入队列,将传入信息导入为本地节点的状态,并将其添加到节点知道的块数据库中。
在大多数情况下,你不需要知道有关如何传播交易或网络上其他节点如何导入块的详细信息。然而,如果你打算编写任何自定义共识逻辑或想了解更多关于区块导入队列的实现信息,则可以在 Rust API 文档中找到详细信息。
状态转换和存储
Substrate 使用一个简单的键值数据存储,实现为支持可修改 Merkle 树的数据库。所有 Substrate 的 higher-level storage abstractions 都建立在这个简单的键值存储之上。
Key-Value 数据库
Substrate 基于 RocksDB 实现其存储数据库,一种用于快速存储需求环境下的持久化键值存储。它还支持处于实验中的 Parity DB。
这个数据库用于需要持久化存储的 Substrate 的所有组件,例如:
- Substrate clients
- Substrate light-clients
- Off-chain workers
Trie abstraction
使用简单键值存储的一个优点是,你可以轻松地在其上抽象存储结构。
Substrate 使用 'paritytech/trie' 的 Base-16 修改的 Merkle-Patricia 树(trie)来提供 trie 结构,该结构的内容可以修改,并且其根哈希很有效地进行重新计算。
Tries 允许有效地存储并且共享历史块状态。trie root 代表 trie 中的数据,也就是说,使用不同数据的两次 tries 将始终具有不同的 roots。因此,两个区块链节点可以很容易地通过简单地比较它们的 trie roots 来验证它们具有相同的状态。
访问 trie 数据代价很高。每个读操作花费 O(log N) 时间,其中 N 是存储在 trie 中的元素数量。为了减轻这种情况,我们使用键值存储缓存。
所有 trie 节点都存储在数据库中,并且可以修改部分 trie 状态。例如,当键值对超出非存档节点的修改范围时,可以将其从存储中删除。
State trie
基于 Substrate 的链有一个 main trie,称为状态 trie,其根哈希被放在每个块头中。可以很容易被用于验证区块链的状态,并为轻客户端需要的验证证据提供基础。
这个 trie 只存储合法链的内容,而不会分叉。有一个单独的 state_db
layer 维护 trie 状态,并在内存中计算出所有非法的引用内容。
Child trie
Substrate 还提供了一个API,可以生成有自己根哈希的 child tries,并且它们可以在运行时被使用。
Child tries 与 main state trie 完全相同,只是 child trie 的根作为节点存储和更新在 main trie 中,而不是在区块头中。由于它们的头是 main state trie 的一部分,因此当包含 child tries 时,验证完整节点状态仍然很容易。
当你希望自己的独立 trie 具有单独的根哈希,你可以使用该哈希来验证这个 trie 中的特定内容时,Child tries 非常有用。trie 的子部分没有自动满足这些需求的 root-hash-like 表示,因此使用 child trie 代替。
Querying storage
使用 Substrate 构建的区块链公开了可用于查询运行时存储的远程过程调用(RPC)服务器。当你使用 Substrate RPC访问存储项时,只需提供与该项关联的密钥。Substrate的运行时 runtime storage APIs
API公开了多种存储项类型;继续阅读,了解如何计算不同类型存储项的存储键。
Storage value keys
要计算简单 Storage Value 的密钥,获取包含存储值的 pallet 名称的 TwoX 128 hash ,并将存储值本身的名称的 TwoX 128 hash 附加到它。例如,Sudo pallet 公开名为 Key
的存储值项:
twox_128("Sudo") = "0x5c0d1176a568c1f92944340dbfed9e9c"
twox_128("Key") = "0x530ebca703c85910e7164cb7d1c9e47b"
twox_128("Sudo") + twox_128("Key") = "0x5c0d1176a568c1f92944340dbfed9e9c530ebca703c85910e7164cb7d1c9e47b"
如果熟悉的 Alice
帐户是 sudo 用户,读取 sudo pallet 的 Key
存储值的 RPC 请求和响应可以表示为:
state_getStorage("0x5c0d1176a568c1f92944340dbfed9e9c530ebca703c85910e7164cb7d1c9e47b") = "0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d"
在这种情况下,返回的值 ("0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d"
) 是 Alice's SCALE-encoded 账户的 ID (5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY
)。
你可能已经注意到,非加密的 TwoX 128 hash 算法用于生成 Storage Value 密钥。这是因为无需支付与加密哈希函数相关的执行成本,因为输入的哈希函数(pallet 和存储项目的名称)由运行时开发人员确定,而不是由区块链的潜在恶意用户确定。
Storage map keys
与 Storage Values 一样,Storage Maps 的键等于包含 map 的 pallet 名称的 TwoX 128 hash 值,该 map 在 Storage Map 本身名称的 TwoX 128 hash 之前。要从 map 中检索元素,请将所需 map 密钥的哈希追加到 Storage Map 的存储密钥。对于具有两个键的 maps(Storage Double Maps),将第一个 map 键的哈希和第二个 map 键的哈希追加到 Storage Double Map 的存储键。
与 Storage Values 一样,Substrate 使用 TwoX 128 hash 算法处理 pallet 和 Storage Map 名称,当确定 map 中元素的哈希键时,但你需要保证使用正确的哈希算法(在 #[pallet::storage]宏 中声明的一个)。
下面是一个示例,说明了从名为 Balances
的 pallet
中查询名为 FreeBalance
的存储映射以获得 Alice
帐户的余额。在这个例子中,FreeBalance
map 使用的是 the transparent Blake2 128 Concat hashing algorithm:
twox_128("Balances") = "0xc2261276cc9d1f8598ea4b6a74b15c2f"
twox_128("FreeBalance") = "0x6482b9ade7bc6657aaca787ba1add3b4"
scale_encode("5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY") = "0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d"
blake2_128_concat("0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d") = "0xde1e86a9a8c739864cf3cc5ec2bea59fd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d"
state_getStorage("0xc2261276cc9d1f8598ea4b6a74b15c2f6482b9ade7bc6657aaca787ba1add3b4de1e86a9a8c739864cf3cc5ec2bea59fd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d") = "0x0000a0dec5adc9353600000000000000"
存储查询返回的值(上例中为“0x0000a0dec5adc9353600000000000000”)是 Alice 账户余额的 SCALE 编码值(本例中为“1000000000000000000000”)。请注意,在对 Alice 的帐户 ID 进行哈希运算之前,必须对其进行 SCALE 编码。还要注意,blake2_128_concat
函数的输出由32个十六进制字符组成,然后是该函数的输入。这是因为 Blake2 128 Concat 是一种透明的哈希算法。
虽然上面的示例可能会使此特性看起来多余,但当目标是遍历 map 中的键时(而不是检索与单个键相关的值),它的实用性会变得更加明显了。为了让人们以一种看起来自然的方式使用 map(如 UIs),对 map 中的键进行遍历是一项常见的要求:首先用户会看到 map 中的元素列表,然后用户可以选择他们感兴趣的元素,并在 map 中查询关于该特定元素的更多细节。
下面是另一个使用相同示例 Storage Map 的示例(一个名为 FreeBalances
的 map,在名为 Balances
的 pallet 中使用 Blake2 128 Concat 哈希算法,该 pallet 演示了如何使用 Substrate RPC 通过 state_getKeys
RPC endpoint 查询 Storage Map 的密钥列表):
twox_128("Balances") = "0xc2261276cc9d1f8598ea4b6a74b15c2f"
twox_128("FreeBalance") = "0x6482b9ade7bc6657aaca787ba1add3b4"
state_getKeys("0xc2261276cc9d1f8598ea4b6a74b15c2f6482b9ade7bc6657aaca787ba1add3b4") = [
"0xc2261276cc9d1f8598ea4b6a74b15c2f6482b9ade7bc6657aaca787ba1add3b4de1e86a9a8c739864cf3cc5ec2bea59fd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d",
"0xc2261276cc9d1f8598ea4b6a74b15c2f6482b9ade7bc6657aaca787ba1add3b432a5935f6edc617ae178fef9eb1e211fbe5ddb1579b72e84524fc29e78609e3caf42e85aa118ebfe0b0ad404b5bdd25f",
...
]
Substrate RPC 的 state_getKeys
endpoint 返回的列表中的每个元素都可以直接用作 RPC 的 state_getStorage
endpoint 的输入。实际上,上面示例列表中的第一个元素等于上一个示例中用于 state_getStorage
查询的输入(用作为 Alice
找到余额)。因为这些键所属的 map 使用一个透明哈希算法来生成的,所以可以确定与列表中的第二个元素关联的帐户。注意,列表中的每个元素都是以相同的64个字符开头的十六进制值,这是因为每个列表元素表示同一 map 中的键,并且该 map 通过连接两个 TwoX 128 hashes 来识别,每个都是 128-bits 或 32个十六进制字符。在解除列表中第二个元素的这一部分之后,你只剩下了 0x32a5935f6edc617ae178fef9eb1e211fbe5ddb1579b72e84524fc29e78609e3caf42e85aa118ebfe0b0ad404b5bdd25f
。
你在前面的示例中看到,这表示某个 SCALE 编码的帐户 ID 的 Blake2 128 Concat hash。Blake 128 Concat 哈希算法包括将哈希算法对其 Blake 128 哈希的输入的追加(或称为串联)。这意味着 Blake2 128 Concat 哈希的前 128 bits (或32个十六进制字符)代表 Blake2 128 哈希。剩下的数表示传递给Blake 2 128 哈希算法的值。在本例中,在删除表示 Blake 2 128 哈希 (如 0x32a5935f6edc617ae178fef9eb1e211f)的前32个十六进制字符后,剩下的是十六进制值 0xbe5ddb1579b72e84524fc29e78609e3caf42e85aa118ebfe0b0ad404b5bdd25f
,这是一个 SCALE 编码的帐户 ID。解码该值的结果是 5GNJqTPyNqANBkUVMN1LPPrxXnFouWXoe2wNSmmEoLctxiZY
,这是很熟悉的 Alice_Stash
帐户的帐户ID。
运行时存储API
Substrate 的 FRAME Support crate
提供了为运行时存储项生成唯一确定性键的实用程序。这些存储项位于状态 trie 中,并且可以通过根据键查询 trie 来访问。
Accounts, addresses, and keys
账户通常代表能够进行交易或持有资金的个人或组织的身份。虽然账户通常用于对应着一个人,但情况并非如此。帐户可用于代表用户或其他实体执行操作,或者自主地执行操作。此外,任何个人或实体都可以有多个账户用于不同目的。例如,Polkadot 是一个基于 Substrate 的区块链,拥有专门的账户,用于持有与交易的账户分开的资金。如何实现和使用帐户完全取决于你作为区块链或平行链的开发人员。
公钥与私钥
通常,每个帐户都有一个所有者,该所有者拥有一个公共和私有密钥对。私钥是一个加密安全的随机生成的数字序列。为了便于阅读,私钥生成一个随机的单词序列,被称为 secret seed phrase 或 mnemonic。如果私钥丢失,帐户所有者可以使用此secret seed phrase恢复对帐户的访问。
对于大多数网络,与帐户相关联的公钥账户是如何在网络上被识别并将其用作交易的目标地址。然而,基于 Substrate 的链使用底层公钥来派生出一个或多个公共地址。Substrate 允许你为一个帐户生成多个地址和地址格式,而不是直接使用公钥。
Address encoding and chain-specific addresses
Substrate 使你能够使用单个公钥派生出多个地址,因此你可以与多个链交互,而无需为每个网络创建单独的公钥和私钥对。默认情况下,与帐户公钥关联的地址使用 Substrate SS58 地址格式。该地址格式基于 base-58 编码。除了允许你从同一公钥派生出多个地址之外,base-58 编码具有以下优点:
- 编码地址由58个字母数字字符组成。
- 字母数字字符串省略字符,如
0
,O
,I
和l
,在字符串中很难区分彼此。 - 网络信息,例如,可以在地址中编码指定的网络前缀。
- 可以使用 checksum 来检测输入错误,以确保正确输入的地址。
因为单个公钥可用于派生不同 Substrate 链的地址,单个帐户可以有多个 chain-specific 的地址。例如,如果检查 alice
帐户的地址,公钥 0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d
取决于 chain-specific 的地址类型。
|Chain address type|Address|
|:-----:|:-----:|
|Polkadot (SS58)|15oF4uVJwmo4TdGW7VfQxNLavjCXviqxT9S1MgbjMNHr6Sp5|
|Kusama (SS58)|HNZata7iMYWmk5RvZRTiAsSDhV8366zq2YGb3tLH5Upf74F|
|Generic Substrate chain (SS58)|5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY|
每个 Substrate 区块链可以注册自定义前缀,以创建 chain-specific 的地址类型。例如,所有 Polkadot 地址都以 1
开头,所有 Kusama 地址都以大写字母开头。所有未注册的 Substrate 链都从 5
开始。
你可以使用 subkey inspect
命令和 --network
选项或通过使用 Subscan 查找 chain-specific 地址的公钥。
有关生成公钥和私钥对以及检查地址的信息,可以参考 subkey。有关 chain-specific 地址的信息,可以参考 SS58 repository 的说明。
在 FRAME 中的账户信息
从概念上讲,帐户代表具有一个或一个或多个公共地址的公共/私钥对的身份。然而,在使用 FRAME 构建的运行时中,帐户被定义为具有32字节地址标识符和相应帐户信息的 storage map,例如该账户已进行的交易数量、取决于该账户的模块数量以及账户余额。
帐户属性(如 AccountId
)一般可以定义在 frame_system
模块中。然后在运行时实现中将泛型类型解析为特定类型,并最终指定一个特定值。然后在运行时实现中将泛型类型解析为特定类型,并最终指定一个特定值。例如,框架中的 Account
类型依赖于关联的 AccountId
类型。AccountId
类型保持为泛型类型,直到在运行时实现中为需要此信息的 pallet 分配了类型。
有关如何在 frame_system
pallet 中定义帐户以及 Account
storage map 中的帐户属性的更多信息,可参阅 Account data structures。有关使用泛型类型的详细信息,可参阅 Rust for Substrate。
Specialized accounts
虽然大多数账户用于表示控制资金或执行交易的公钥/私钥对,但 Substrate 支持一些专用账户来控制特定密钥对的使用方式。例如,你的帐户可能需要自定义加密方案,只能用于执行特定功能,或者只能访问特定 pallets。
Staking accounts and keys
在大多数情况下,在特定 FRAME pallet 的上下文中实现专用帐户。例如,指定权益证明(nominated proof-of-stake,NPoS)可能需要节点验证者和提名者持有大量 tokens。为了确保这些账户中的余额安全,Staking pallet 提供了一些账户抽象,用于分离执行特定操作所需的密钥对。
|账户类型|如何使用帐户|
|:-----:|:-----:|
|Stash account|stash account 表示定义验证器 staking 余额的公钥/私钥对。为了安全起见,你应该将账户密钥离线存放在冷存储中。你不应使用 stash account 进行频繁交易。因为此帐户的密钥保持离线状态,你可以指定一个 controller account 来做出 non-spending 决策,或者指定一个 keyless proxy account 来代表其在治理中投票。|
|Controller account|controller account 代表公钥/私钥对,该密钥对表示你可以有意图去成为验证者或提名者,设置首选项,如奖励、目的地和,对于验证者,设置会话密钥。controller account 只需支付交易费用,因此只需最少的资金。它永远不能用来花费它的 stash account 的资金。控制器采取的操作可能会导致削减,因此它仍然应该是安全的。|
|Session account|session account 表示验证者用于签名共识相关消息的公钥/私钥对。session account 不用于控制资金。这些键一般被定义在 Session pallet 中,并在运行时具体化。为了创建 session account 密钥对,你必须通过使用 controller 密钥签名交易并在链上发布 session 证书来证明该帐户代表你的 stash account 和提名者。你可以使用 session.setKeys 交易在链上生成并注册新的会话密钥。你可以使用 author_rotateKeys
RPC 调用更改 session 密钥。|
Keyless proxy accounts
在某些情况下,你可能希望创建一个与任何所有者分离的帐户,以便它可以用于执行自主交易。例如,你可以创建一个帐户,然后将授权给该帐户,这样它就可以在没有你的干预和访问你密钥的情况下发送函数调用。在创建具有授权权限的新帐户之后,该账户可作为接收者用于销毁资金,或持有等待执行或转账的 tokens。
你可以使用 Proxy pallet 创建一个帐户,该帐户具有使用作为签名来源的委托帐户 来发送某些类型调用的权限。
Rust for Substrate
使 Substrate 成为创建 mission-critical 软件的灵活和可扩展框架的主要原因是 Rust。作为 Substrate 的选择语言,Rust是一种首选的高性能编程语言,原因如下:
- Rust is fast:它在编译期是静态类型的,使得编译器可以针对速度优化代码,并且开发人员还可以针对指定的编译目标进行优化。
- Rust is portable:它被设计为在支持任何类型操作系统的嵌入式设备上运行。
- Rust is memory safe:它没有垃圾收集器,并且它检查你使用的每个变量和引用的每个内存地址,以避免任何内存泄漏。
- Rust is Wasm first:它对编译到 WebAssembly 具有一流的支持。
Rust in Substrate
在架构部分中,你将了解到,Substrate 由两个不同的构建组件组成:外部节点和运行时。虽然在外部节点代码中使用了 Rust 中更复杂的特性,例如多线程和异步 Rust,但它们不会直接暴露给运行时工程师,这使得运行时工程师更容易关注其节点的业务逻辑。
通常,根据他们的关注点,开发人员应该知道:
- 基本的 Rust习惯用法, 使用 no_std 工作 和有哪些宏以及为什么使用这些宏(用于运行时工程)。
- 异步Rust(适用于使用外部节点(客户端)代码的更高级开发人员)。
尽管在深入 Substrate 之前,一般对 Rust 的熟悉是必不可少的,并且有很多资源可以用来学习 Rust,包括 Rust Language Programming Book 和 Rust by Example,本节剩余部分将重点介绍 Substrate 如何使用 Rust 的一些核心特性,以帮助开发人员开始运行时工程的开发。
Compilation target
当构建 Substrate 节点时,我们使用 wasm32-unknown-unknown
编译目标,这意味着 Substrate 运行时工程师必须在约束范围内编写运行时,且必须编译为 Wasm 的运行时。这意味着你不能依赖于某些典型的标准库类型和函数,并且对于大多数运行时代码只能使用 no_std
兼容的 crates。Substrate 有许多自己的原始类型和相关特性,使其尽可能围绕 no_std
的要求工作。
宏
当你学习编写 FRAME pallets 时,你将很快遇到各种不同类型的宏,它们被设计用来抽象和满足任何特定于运行时的需求。有了它们,你可以专注于编写惯用的 Rust,最大限度地减少编写额外代码的开销,否则你需要编写正确地与运行时交互的代码。
Rust 宏是一个强大的工具,有助于确保满足某些要求(无需编写重复的代码),例如以特定方式格式化的逻辑,进行具体的检查,或者某些逻辑由特定的数据结构组成。这对于帮助开发人员编写能够与复杂的 Substrate 运行时集成的代码特别有用。例如,所有 FRAME pallets 中都需要 #[frame_system::pallet]
宏,以帮助你正确实现某些所需属性,诸如,存储项或外部可调用函数,并使其与 construct_runtime
中的构建过程兼容。
开发 Substrate 运行时需要大量使用 Rust 的属性宏,通常有两种风格:派生属性和自定义属性。当你开始使用 Substrate 时,准确地知道它们是如何工作的其实并不重要,而是要知道它们的存在,以便让你能够编写正确的运行时代码。
派生属性对于需要满足某些特性的自定义运行时类型非常有用,例如,在运行时执行期间,节点可以对类型进行解码。
其他的属性宏在整个 Substrate 的代码库中也很常见,用于:
- 告诉编译器代码段是否打算编译为
no_std
或是否可以访问std
库。 - 自定义 FRAME 支持用于编写 pallets 的宏。
- 指定运行时的内置方式。
泛型和配置 traits
通常与 Java 等语言中的接口相比,Rust 中的 traits 给类型提供了高级功能的方法。
如果你读过 pallets,你可能会注意到每个 pallet 都有一个 Config
trait,允许你定义 pallet 依赖的类型和接口。
该 trait 本身从 frame_system::pallet::Config
trait 继承了许多核心运行时类型,使编写运行时的逻辑时更容易访问常见类型。此外,在任何 FRAME pallet 中,Config
trait 都是基于 T
上的泛型(下一节将介绍更多泛型)。这些核心运行时类型的一些常见示例如:T::AccountId
,在运行时或 T::BlockNumber
中识别用户帐户的常见类型;T::BlockNumber
,运行时使用的区块号类型。
有关 Rust 中的泛型类型和 traits 的更多信息,请参阅 Rust 手册中关于泛型类型、Traits 和 Advanced traits 的章节。
通过 Rust 泛型,Substrate 运行时开发人员可以编写与特定实现细节完全无关的 pallets,从而充分利用 Substrate 的灵活性、可扩展性和模块性。
Config
trait 中的所有类型都是使用 trait 边界通用定义的,并在运行时中具体实现。这不仅意味着你可以编写支持同一类型不同规格的 pallets (例如,Substrate链和以太坊链的地址),但你也可以通过最小的成本来自定义你需求的通用实现(例如,将块号更改为 u32
)。
这使开发人员能够灵活地编写代码,而不必对你所做的核心区块链架构决策进行任何假设。
Substrate 最大限度地使用泛型类型,以提供最大的灵活性。你可以定义如何解析泛型类型以满足你的目的。
有关 Rust 中的泛型类型和 traits 的更多信息,可参阅 Rust 手册中有关泛型类型的章节。
链下操作
在许多用例中,你可能希望在更新链上状态之前查询来自链外的源数据或正处理的数据,而不使用链上资源。合并链外数据的通常方式一般会连接到 oracles 以从一些传统的数据来源处提供数据。尽管使用 oracles 是处理链外数据源的一种方法,但 oracles 所能提供的安全性、可扩展性和基础设施效率都有一些限制。
为了使链外数据的集成更加安全和高效,Substrate 通过以下特性支持链下操作:
- Offchain workers(链外工作机)是一个子系统的组件,可以执行长时间运行且可能是不确定性的任务,例如:
- 网站服务请求
- 数据的加密、解密和签名
- 随机数生成
- CPU 密集型计算
- 链上数据和链外工作机的枚举或聚合使你能够移动可能需要更多时间执行的任务,而不是允许出块处理管道中的任务。任何可能超过允许的最大块执行时间的任务都是合理的链外处理候选任务。
- Offchain storage(链下存储)是 Substrate 节点的本地存储,可以由链下工作机和链上逻辑访问
- 链下工作机对链下存储具有读写访问权限。
- 链上逻辑通过链下索引具有写访问权限,但没有读访问权限。链下存储允许不同的工作线程相互通信,并存储不需要在整个网络上达成共识的用户特定或节点特定数据。
- Offchain indexing(链下索引)是一项可选服务,允许运行时独立于链下工作机直接写入链下存储。链下索引为链上逻辑提供临时存储并补充了链上状态。
Off-chain workers
链下工作机在 Substrate 运行时之外的自己的 Wasm 执行环境中运行。这种运行方式分离的考虑确保了区块生产运行不会受到长期运行的链下任务的影响。然而,由于链下工作机与运行时在同一代码中声明,因此他们可以很容易地访问链上状态用于他们的计算。
链下工作机可以访问扩展的 API 与外部世界进行通信:
- 有能力 submit transactions(提交交易),签名或未签名,发布计算结果到链上。
- 一个功能齐全的 HTTP 客户端,允许工作机从外部服务访问和获取数据。
- 访问本地密钥库以签名和验证状态或交易。
- 另外,所有链下工作机共享本地键值数据库。
- 一个安全的,本地熵源用于随机数的生成。
- 访问节点的本地精确时间。
- 有能力去挂起和恢复任务。
请注意,链下工作机产生的结果不需要定期进行交易验证。因此,你应该保证链下操作,包括验证方法,以确定哪些信息进入链中。例如,你可以通过实现投票、权重均分或检查发送者签名的机制来验证链下交易。
在每次块导入过程中,都会生成链下工作机。然而,它们不会在初始区块链同步期间执行。
Offchain storage
链下存储始终是 Substrate 节点的本地存储,不与任何其他区块链节点在链上共享,也不受共识机制的约束。你可以使用具有读写访问权限的链下工作线程访问在链下存储中的数据,或者通过使用链下索引的链上逻辑访问数据。
由于在每次块导入期间都会产生一个链下工作机线程,因此在任何指定时间内都可以有多个链下工作机线程运行。与任何多线程编程环境一样,当链下工作机线程访问链下存储时,有一些实用程序可以对其进行互斥锁定,以确保数据一致性。
链下存储作为链下工作机线程相互通信,以及充当链下和链上逻辑之间通信的桥梁。还可以使用远程过程调用(RPC)读取它,因此它适合存储无限增长的数据而不会过度消耗链上存储空间。
Offchain indexing
在区块链的上下文中,存储通常与链上状态有关。然而,链上状态的是很昂贵的,因为它必须得到一致同意,并被转移到网络中的多个节点。因此,不应该使用链上存储去存储随着时间无限增长的历史数据或用户生成的数据。
为了满足访问历史数据或用户生成数据的需要,Substrate 提供通过使用链下索引可以对链下存储进行访问。链下索引允许运行时直接写入链下存储,而不需要使用链下工作机线程。你可以通过使用 --enable-offchain-indexing
命令行选项启动 Substrate 节点,以启用此功能来持久化数据。
与链下工作机不同,链下索引在每次处理区块时填充数据到链下存储。通过在每个块填充数据,链下索引可确保数据始终保持一致,并且对启用索引运行的每个节点,也都完全相同。
- Example: Offchain worker Pallet
- Example: Submit transactions
- Example: Use HTTP requests to fetch data
- Example: Offchain storage
- Example: Offchain indexing
构建与编码
本节中的主题提供了对用于构造运行时逻辑的编写代码的更详细探索,包括可用于构建和与节点交互的库和工具,以及如何编译逻辑以构建 Substrate 节点的更近一步的观察。
代码库的介绍
在使用 node template 时,你不需要知道任何有关正在使用的底层架构或库的信息,因为基本组件已经组装好并可以直接使用。
然而,如果你想要设计和构建自定义区块链,你可能需要熟悉可用的库,并了解这些不同库的功能。
在substrate 架构设计中,你了解了 Substrate 节点的核心组件以及节点的不同部分如何承担不同的责任。在技术层面上,节点不同层之间的职责分离反映在用于构建基于 Substrate 的区块链的核心库中。下图说明了库如何映射外节点和运行时职责,以及基本库如何提供两者之间的通信层。
核心节点库
使 Substrate 节点能够处理其网络职责的库,包括共识算法和块执行在内的是以使用 sc_
为前缀名的 Rust crates。例如,sc_service
库负责为 Substrate 区块链构建网络层,管理网络参与者和交易池之间的通信。
在外部节点和运行时之间提供通信层的库是以使用 sp_
为前缀名的 Rust crates。这些库编排了需要外部节点和运行时之间交互活动。例如,sp_std
库从Rust 标准库中获取有用的基本逻辑,并使其可用于依赖于运行时的任何代码。
使你能够构建运行时逻辑并对传入和传出运行时的信息进行编码和解码的库是 Rust crates,在 crate 名称中使用 frame_
前缀。frame_*
库为运行时提供了基础代码结构。例如,frame_system
库提供了一系列与其他 Substrate 组件交互的基本功能,以及 frame_support
允许你声明运行时存储项、错误和事件。
除了 frame_*
库提供的基础代码结构外,运行时还可以包括一个或多个 pallet_*
库。每个使用 pallet_
为前缀的 Rust crate 都代表一个 FRAME 模块。在大多数情况下,你使用 pallet_*
库来组装你想要加入区块链的功能,以满足你的项目。
你可以使用 sp_*
核心库公开的基础代码构建 Substrate 运行时,而无需使用 frame_*
或 pallet_*
库。然而,frame_*
或 pallet_*
库为构建 Substrate 运行时提供了最有效方法。
模块化架构
核心库的分离为编写区块链逻辑提供了灵活的模块化架构。基础代码库提供了一个外部节点和运行时都可以在此之上构建的方式,而无需彼此直接通信。基础类型和 traits 在它们各自独立的 crates 中公开,因此它们可用于外部节点和运行时组件,而不会引入循环依赖性问题。
前端代码库
除了使你能够基于 Substrate 区块链的核心库构建区块链外,你还可以使用客户端库与 Substrate 节点进行交互。你可以使用客户端库来构建特定应用程序的前端。通常,客户端库公开的功能是在 Substrate 远程过程调用(RPC)APIs 的顶部实现的。有关使用元数据和前端库来构建应用程序的更多信息,可参见应用程序开发。
构建过程
在架构设计中,你了解到 Substrate 节点由外部节点主机和运行时执行环境组成。这些节点组件通过调用运行时 API 和调用主机函数相互通信。在本节中,你将进一步了解如何将 Substrate 运行时编译为平台本机可执行文件和存储在区块链上的 WebAssembly(Wasm)二进制文件。在你了解了二进制文件如何编译的内部工作原理之后,你将进一步了解为什么有两个二进制文件,何时使用,以及如果需要,如何更改执行策略。
编译一个优化的组件
你可能已经知道,可以通过在 Substrate node 项目的根目录中运行 cargo build --release
命令来编译 Substrate 节点。此命令为项目构建平台特定的可执行文件和 WebAssembly 二进制文件,并生成优化后的可执行文件。生成优化的可执行文件包括一些编译后处理。
作为优化过程的一部分,WebAssembly 运行时二进制会通过一系列内部步骤进行编译和压缩,然后再将其包含在链中的 genesis 状态中。为了让你更好地理解该过程,下图总结了这些步骤。
以下各小节将更详细地描述构建过程。
构建 WebAssembly 二进制文件
WebAssembly中包含的功能
用于自定义构建过程的环境变量
用于自定义构建过程的环境变量
压缩 WebAssembly 二进制文件
执行策略
构建没有 native runtime 的 WebAssembly
不使用 WebAssembly 编译 Rust
运行时存储
存储项目
声明存储项目
访问存储项目
哈希算法
初始化配置
最佳实践
交易、权重和费用
如何计算费用
使用 transaction payment pallet
有特殊要求的交易
默认权重注释
提交派发权重校准
自定义费用
自定义 pallets
Pallet 宏和属性
有用的 FRAME traits
运行时实现
Pallet 耦合
紧密的耦合 Pallet
松散的耦合 Pallet
选择 Pallet 耦合策略
事件和错误
声明一个事件
向运行时公开事件
存下一个事件
支持的类型
监听一个事件
错误
Randomness
确定随机性
Substrate's randomness trait
安全属性
Chain specification
自定义外部节点配置
自定义初始配置
存储 chain specification 信息
通过一个 chain specification 启动节点
为运行时声明存储项
Raw chain specifications
Privileged calls and origins
Raw origins
Origin call
Custom origins
Next steps
远程过程调用
远程过程调用,或叫RPCs,是外部编程的一种方式,例如,一个浏览器或前端应用程序,与一个 Substrate 节点通信。他们通常被用作检查存储值、提交交易、并查询当前共识授权。Substrate 附带了几个默认的RPCs。在许多案例中,添加自定义 RPCs 到你的节点中是很有用的。
RPC扩展构建器
要将自定义 RPC 客户端连接到 Substrate 节点时,你必须提供一个被称为 RPC 扩展构建器的函数。此函数会采用一个参数,为节点应该接受还是拒绝不安全的 RPC 调用,并返回一个节点需要创建 JSON RPC 的 IoHandler
。关于更多上下文的内容,可通过查看 RpcExtensionBuilder
trait API 文档。
RPC类型
RPCs 可以是节点共识机制的接口,也可以是任何外部用户向区块链提交交易的接口。在所有情况下,重要的是要考虑 RPCs 公开哪些 endpoints。启动一个节点,并运行此命令以查看节点的 RPC API 的全部列表:
curl -H "Content-Type: application/json" -d '{"id":1, "jsonrpc":"2.0", "method": "rpc_methods"}' http://localhost:9933/
公开的RPCs
Substrate 节点提供以下命令行选项,允许你公开 RPC 接口:
--ws-external
--rpc-external
--unsafe-ws-external
--unsafe-rpc-external
默认情况下,如果你尝试暴露 RPC 接口并同时运行验证者节点,则该节点会拒绝启动;--unsafe-*
标签允许你取消此安全限制。暴露 RPC 接口会对外暴露一个巨大的攻击可能性,必须要仔细的审查。
有很多 RPC 方法可以用来控制节点的行为,但是你应该避免暴露它。例如,你不应该暴露下面的 RPC 方法:
author_submitExtrinsic
— 允许向本地交易池提交交易。author_insertKey
— 允许向本地密钥存储库插入私钥。author_rotateKeys
— session 密钥轮换。author_removeExtrinsic
— 从池中移除并禁用 extrinsic。system_addReservedPeer
— 添加保留节点。system_removeReservedPeer
— 移除保留节点。
你还应该避免暴露可能需要很长时间执行的 RPC 方法,这可能会组织客户端的状态同步。例如,你应该避免使用下面的 RPC 方法:
storage_keys_paged
— 获取状态中具有指定前缀和分页支持的所有 key。state_getPairs
— 获取状态中具有指定前缀的所有 key 及其值。
这些 RPCs 是使用 #[rpc(name = "rpc_method")]
宏声明的,其中 rpc_method
是函数的名称, 例如,author_submitExtrinsic
对应于 submit_extrinsic
。
如果请求来自不受信任的用户,过滤掉此类调用则至关重要。实现这一点的方法是通过 JSON-RPC 代理,该代理能够过滤检查调用并仅传递允许的 APIs。
RPCs for remote_externalities
在 remote_externalities
上下文中存在一种特殊类型的 RPCs 使用方法。rpc_api
允许你对 Substrate 节点进行一次性的调用,例如,对于使用 try_runtime
等工具进行测试非常有用。
Endpoints
当启用任何 Substrate 节点时,你可以使用下面两个 endpoints:
- HTTP endpoint:
http://localhost:9933
- WebSocket endpoint:
ws://localhost:9944
大多数 Substrate 前端库和工具都使用更强大的 Websocket endpoint 与区块链进行交互。通过 WebSockets,你可以订阅链的状态,诸如事件,以及你的区块链发生在无论任何条件下的所有改变,都会接收推送通知。
要调用 Metadata
端点,需要与运行节点一起运行下面这行命令:
curl -H "Content-Type: application/json" -d '{"id":1, "jsonrpc":"2.0", "method": "state_getMetadata"}' http://localhost:9933/
该命令的返回值不是 human-readable 的格式,基于此,它需要使用类型编码(SCALE)。
每个存储项都有一个相关联的存储 key,用于查询存储,这就是 RPC endpoints 知道怎么去查看。
示例
state_getMetadata
RPC 请求:
function get_metadata_request(endpoint) {
let request = new Request(endpoint, {
method: "POST",
body: JSON.stringify({
id: 1,
jsonrpc: "2.0",
method: "state_getMetadata",
}),
headers: { "Content-Type": "application/json" },
});
return request;
}
原文本解码:
function decode_metadata(metadata) {
return new TextDecoder().decode(util.hexToU8a(metadata));
}
state_getStorage
RPC request:
Request: {"id":1,"jsonrpc":"2.0","method":"state_getStorage",["{storage_key}"]}
其中 storage_key
是由对应名称的pallet,函数和 key(可选的)生成的参数。
function get_runtime_storage_parameter_with_key(module_name, function_name, key) {
// We use xxhash 128 for strings the runtime developer can control
let module_hash = util_crypto.xxhashAsU8a(module_name, 128);
let function_hash = util_crypto.xxhashAsU8a(function_name, 128);
// We use blake2 256 for strings the end user can control
let key_hash = util_crypto.blake2AsU8a(keyToBytes(key));
// Special syntax to concatenate Uint8Array
let final_key = new Uint8Array([...module_hash, ...function_hash, ...key_hash]);
// Return a hex string
return util.u8aToHex(final_key);
}
应用程序开发
元数据系统
元数据格式
RPC APIs
连接到一个节点
开始构建
前端用例
升级运行时
无分叉运行时升级是用于 Substrate 框架区块链开发的的一个定义特性。无需分叉代码库就可以更新运行时逻辑,这使你的区块链能够随着时间的推移而发展和改进。通过包含为区块链定义运行时执行环境(运行时 WebAssembly blob)状态中的一个元素,可以实现此功能。
因为运行时是区块链状态的一部分,所以网络维护人员可以利用区块链的无信任、去中心化共识功能来安全地增强运行时。
在用于运行时开发的 FRAME 系统中,系统库定义了用于更新运行时定义的 the set_code
call。升级一个运行中的网络教程演示了在不关闭节点或中断操作的情况下升级运行时的两种方法。但是,本节教程中的两个升级都演示了向运行时添加功能,而不是更新现有的运行时状态。如果运行时升级需要更改现有状态,则可能需要进行存储迁移。
运行时版本控制
在构建过程中,你了解了编译节点会同时生成平台的原生二进制文件和 WebAssembly 二进制文件,并且可以通过执行策略命令行选项来控制在区块生产过程的不同地方选择使用哪一个二进制文件。选择要与之通信的运行时执行环境的组件称为执行程序。尽管你可以覆盖自定义场景的默认执行策略,但在大多数情况下,或者执行程序通过评估原生二进制和 WebAssembly 二进制的以下信息选择适当的二进制文件来使用:
spec_name
spec_version
authoring_version
为了向执行程序进程提供此信息,运行时包含类似于以下的运行时版本结构:
pub const VERSION: RuntimeVersion = RuntimeVersion {
spec_name: create_runtime_str!("node-template"),
impl_name: create_runtime_str!("node-template"),
authoring_version: 1,
spec_version: 1,
impl_version: 1,
apis: RUNTIME_API_VERSIONS,
transaction_version: 1,
};
结构中的参数提供了以下信息:
|This parameter|Provides this|
|:-----|:-----|
|spec_name
|不同 Substrate 运行时的标识符。|
|impl_name
|spec 实现的名称。这对节点影响不大,只用于区分不同团队实现的代码。|
|authoring_version
|authorship 接口的版本。除非是它的原生运行时,否则生产节点不会尝试生成块。|
|spec_version
|运行时 specification 的版本。完整节点不会尝试使用它的原生运行时来替代链上的 Wasm 运行时,除非 Wasm 和原生二进制文件之间的所有 spec_name
、spec_version
和 authoring_version
都是相同的。spec_version
的更新可以作为一个 CI 过程自动化,就像 Polkadot 网络一样。当 transaction_version
有更新时,通常会递增此参数。|
|impl_version
|specification 的实现版本,节点可以忽略这一点,它只用于表明代码是不同的。只要 authoring_version
和 spec_version
是相同的,代码本身就可能会发生变化,但是原生二进制文件和 Wasm 二进制文件做的是相同的事情。通常只有逻辑之外的破坏性优化才会导致 impl_version
的更改。|
|transaction_version
|处理交易的接口版本。此参数可以用于同步硬件钱包或其他签名设备的固件更新,以验证运行时交易是否有效。该参数允许硬件钱包知道哪些交易可以安全签名。如果在 construct_runtime!
宏中的 pallet 索引发生变化,或者如果对可调用的函数有任何更改,这个数字就会碰撞,比如参数的数量或参数类型。如果更新了这个编号,那么 spec_version
也必须更新。|
|apis
|支持的运行时 APIs 及其版本列表。|
编排引擎,有时被称为执行程序,验证原生运行时是否具有相同的一致性,在 WebAssembly 选择执行之前,将驱动逻辑作为 WebAssembry。但是由于运行时版本是手动设置的,因此如果运行时版本被错误表示,编排引擎仍然可能做出不适当的决策。
访问运行时版本
FRAME 系统通过 state.getRuntimeVersion
RPC endpoint 公开运行时版本信息。endpoint 接受一个可选的块标识符。然而,在大多数情况下,你使用运行时元数据来理解运行时公开的 APIs 以及如何与这些 APIs 交互。只有当链的运行时 spec_version 更改时,运行时元数据才应该更改。
无分叉的运行时升级
传统区块链在升级其链的状态转换功能时,需要进行硬分叉。硬分叉要求所有节点操作员停止其节点并手动升级到最新的可执行文件。对于分布式生产网络,协调硬分叉升级可能是一个复杂的过程。
运行时版本属性使基于 Substrate 的区块链能够实时升级运行时逻辑,而不会造成网络分叉。
要执行无分叉运行时升级,Substrate 使用现有运行时逻辑将存储在区块链上的 Wasm 运行时更新为具有新逻辑的新共识迭代版本。作为共识过程的一部分,这种升级被推送到网络上的所有全节点。在 Wasm 运行时升级之后,编排引擎会发现本机运行时 spec_name
、spec_version
或 authoring_version
不再与新的 Wasm 运行时匹配。因此,编配引擎会执行规范的 Wasm 运行时,而不是在任何执行流程中使用原生本地运行时。
存储迁移
存储迁移是自定义的一次性函数,允许你更新存储以适应运行时中的更改。例如,如果运行时升级将用于表示用户余额的数据类型从无符号整数更改为有符号整数,存储迁移将读取现有值为无符号整数,并回写已转换为有符号整数的更新值。如果你没有在需要时对数据的存储方式进行此类更改,运行时就无法正确地解释存储值以包含在运行时状态中,并可能导致没有定义的行为。
使用FRAME进行存储迁移
使用 OnRuntimeUpgrade
trait 实现 FRAME 存储迁移。OnRuntimeUpgrade
trait 指定一个单独的函数,on_runtime_upgrade
,它允许你指定在运行时升级之后立即运行的逻辑,但要在任何 on_initialize
函数或交易被执行之前。
准备存储迁移
准备存储迁移意味着理解运行时升级所定义的更改。Substrate 存储库使用 E1-runtimemigration
标签来指定此类更改。
写一个迁移
每次存储迁移都是不同的,具有不同的需求和不同的复杂性级别。但是当你需要进行存储迁移时,可以参考以下推荐实践进行操作:
- 将迁移提取到可复用的函数中,并为它们编写测试。
- 包括在迁移中的登录以协助调试。
- 请记住迁移是在升级的运行时上下文中执行的。迁移代码可能需要包含已弃用的类型,如本例所示。
- 使用存储版本使迁移更具有声明性,从而使迁移更安全,如本例所示。
迁移的排序
默认情况下,FRAME 命令执行 on_runtime_upgrade
函数,根据 pallet 在 construct_runtime!
宏中出现的顺序。对于升级,函数以相反的顺序运行,从最先执行的最后一个 pallet 开始。如果需要你也可以强制执行自定义顺序(请参阅此处的示例)。
FRAME 存储迁移按照这个顺序运行:
- 如果使用自定义顺序,则需要自定义
on_runtime_upgrade
。 - 系统
frame_system::on_runtime_upgrade
函数。 - 所有的
on_runtime_upgrade
函数都是在运行时中定义的,从construct_runtime!
宏中的最后一个 pallet 开始。
测试迁移
测试存储迁移非常重要,以下是一些可用来测试存储迁移的工具:
- Substrate debug kit 包括一个 remote externalities 工具,该工具允许对实时的链数据安全地执行存储迁移单元测试。
- fork-off-substrate 脚本可以很容易地创建一个 chain specification,引导一个本地测试链来测试运行时升级和存储迁移。
启动区块链网络
本节教程演示了使用基于 Substrate 的区块链节点的基础知识,包括如何使节点在对等网络中相互通信,以及如何收集关于节点操作的指标。通常,你应该按照列出的顺序完成教程,因为它们为尝试后面的教程或执行更复杂的任务奠定了基础。后面的教程加强或扩展了在本教程中学习的基本主题。
构建本地区块链
编译Substrate节点
git clone https://github.com/substrate-developer-hub/substrate-node-template
cd substrate-node-template && git checkout polkadot-v0.9.26
rustup update
rustup update nightly
rustup target add wasm32-unknown-unknown --toolchain nightly
cargo build --release
启动节点
./target/release/node-template --dev
安装front-end template
node --version
yarn --version
npm install -g yarn
git clone https://github.com/substrate-developer-hub/substrate-front-end-template
cd substrate-front-end-template
yarn install
启动front-end template
yarn start
访问 http://localhost:8000/
停止本地节点
- 返回显示节点输出的终端。
- 按 Control-c 终止正在运行的进程。
- 验证终端是否返回到 substrate-node-template 目录中的终端提示。
模拟区块链私有网络
在本节教程中,你将看到权威共识模型在实践中是如何工作的,它使用两个预定义的权威帐户使节点能够生成块。在这个模拟网络中,两个节点使用不同的帐户和密钥启动,但在一台计算机上运行。
启动第一个区块链节点
本教程通过使用名为 alice 和 bob 的预定义帐户在单个本地计算机上运行两个 Substrate 节点来模拟私有网络。
清除之前旧链的数据
./target/release/node-template purge-chain --base-path /tmp/alice --chain local
# Are you sure to remove "/tmp/alice/chains/local_testnet/db"? [y/N]:
y
使用 alice 账户启动本地区块链节点
./target/release/node-template \
--base-path /tmp/alice \
--chain local \
--alice \
--port 30333 \
--ws-port 9945 \
--rpc-port 9933 \
--node-key 0000000000000000000000000000000000000000000000000000000000000001 \
--telemetry-url "wss://telemetry.polkadot.io/submit/ 0" \
--validator
回顾命令行选项
选项 | 描述 |
---|---|
--base-path | 指定用于存储与此链相关的所有数据的目录。 |
--chain local | 指定要使用的 chain specification,有效的预定义 chain specifications 包括 local 、development 和 staging 。 |
--alice | 将 alice 帐户的预定义密钥添加到节点的密钥库中。通过此设置,alice 帐户用于区块生成和最终确认。 |
--port 30333 | 指定要监听 peer-to-peer(p2p )通信的端口。由于本教程使用在同一物理计算机上运行的两个节点来模拟网络,所以你必须为至少一个帐户显式指定不同的端口。 |
--ws-port 9945 | 指定要监听传入 WebSocket 流量的端口,默认端口为 9944 ,本教程使用自定义 web socket 端口号(9945)。 |
--rpc-port 9933 | 指定要监听传入 RPC 通信的端口,默认端口是 9933 。 |
--node-key <key> | 指定用于 libp2p 网络的 Ed25519 密钥,你应该在开发和测试时使用此选项。 |
--telemetry-url | 指定发送遥测数据的位置。对于本教程,你可以将遥测数据发送到由 Parity 托管的服务器,该服务器可供任何人使用。 |
--validator | 指定此节点参与网络的区块生成和最终确认。 |
有关 node template 可用的命令行选项的详细信息,请通过运行以下命令查看用法帮助:
./target/release/node-template --help
查看显示的节点消息
如果节点成功启动,终端将显示描述网络操作的消息。例如,你应该看到类似的输出:
2022-08-16 15:29:55 Substrate Node
2022-08-16 15:29:55 ✌️ version 4.0.0-dev-de262935ede
2022-08-16 15:29:55 ❤️ by Substrate DevHub <https://github.com/substrate-developer-hub>, 2017-2022
2022-08-16 15:29:55 📋 Chain specification: Local Testnet
2022-08-16 15:29:55 🏷 Node name: Alice
2022-08-16 15:29:55 👤 Role: AUTHORITY
2022-08-16 15:29:55 💾 Database: RocksDb at /tmp/alice/chains/local_testnet/db/full
2022-08-16 15:29:55 ⛓ Native runtime: node-template-100 (node-template-1.tx1.au1)
2022-08-16 15:29:55 🔨 Initializing Genesis block/state (state: 0x6894…033d, header-hash: 0x2cdc…a07f)
2022-08-16 15:29:55 👴 Loading GRANDPA authority set from genesis on what appears to be first startup.
2022-08-16 15:29:56 Using default protocol ID "sup" because none is configured in the chain specs
2022-08-16 15:29:56 🏷 Local node identity is: 12D3KooWEyoppNCUx8Yx66oV9fJnriXwCcXwDDUA2kj6vnc6iDEp
2022-08-16 15:29:56 💻 Operating system: macos
2022-08-16 15:29:56 💻 CPU architecture: x86_64
2022-08-16 15:29:56 📦 Highest known block at #0
2022-08-16 15:29:56 〽️ Prometheus exporter started at 127.0.0.1:9615
2022-08-16 15:29:56 Running JSON-RPC HTTP server: addr=127.0.0.1:9933, allowed origins=Some(["http://localhost:*", "http://127.0.0.1:*", "https://localhost:*", "https://127.0.0.1:*", "https://polkadot.js.org"])
2022-08-16 15:29:56 Running JSON-RPC WS server: addr=127.0.0.1:9945, allowed origins=Some(["http://localhost:*", "http://127.0.0.1:*", "https://localhost:*", "https://127.0.0.1:*", "https://polkadot.js.org"])
2022-08-16 15:29:56 creating instance on iface 192.168.1.125
2022-08-16 15:30:01 💤 Idle (0 peers), best: #0 (0x2cdc…a07f), finalized #0 (0x2cdc…a07f), ⬇ 0 ⬆ 0
...
特别是,你应该注意输出中的以下消息:
🔨 Initializing Genesis block/state (state: 0xea47…9ba8, header-hash: 0x9d07…7cce)
标识节点正在开始初始化或生成创世块,当你启动下一个节点时,请验证这些值是否相同。🏷 Local node identity is: 12D3KooWEyoppNCUx8Yx66oV9fJnriXwCcXwDDUA2kj6vnc6iDEp
指定唯一标识此节点的字符串。该字符串由--node-key
确定,该键使用alice
帐户启动节点。你使用此字符串来识别启动第二个节点连接到该节点的网络。2021-03-10 17:34:37 💤 Idle (0 peers), best: #0 (0x9d07…7cce), finalized #0 (0x9d07…7cce), ⬇ 0 ⬆ 0
表明网络中没有其他节点,也没有生成区块。在开始生成块之前,另一个节点必须加入网络。
添加第二个节点到区块链网络
你使用 alice
帐户密钥启动的节点已经运行,现在你可以使用 bob
帐户将另一个节点添加到该网络中。因为你正在加入一个已经运行的网络,所以可以使用正在运行节点的标识将新节点加入的网络。这些命令与你之前使用的命令类似,但有一些重要区别。
./target/release/node-template purge-chain --base-path /tmp/bob --chain local -y
# 通过在命令中添加-y,您可以删除链数据,而无需提示您确认操作。
./target/release/node-template \
--base-path /tmp/bob \
--chain local \
--bob \
--port 30334 \
--ws-port 9946 \
--rpc-port 9934 \
--telemetry-url "wss://telemetry.polkadot.io/submit/ 0" \
--validator \
--bootnodes /ip4/127.0.0.1/tcp/30333/p2p/12D3KooWEyoppNCUx8Yx66oV9fJnriXwCcXwDDUA2kj6vnc6iDEp
注意此命令与上一个命令之间的以下差异:
- 因为两个节点运行在同一台物理计算机上,所以你必须指定不同的值给这些选项,
--base-path
、--port
、--ws-port
、--rpc-port
- 此命令包含
--bootnodes
选项,并且指定一个引导节点,该节点由alice
启动。
--bootnodes
选项由以下信息组成:
ip4
表示节点的IP地址使用IPv4格式。127.0.0.1
为运行的节点指定 IP 地址,在本案例中,表示localhost
的地址。tcp
表示将TCP
指定为用于 peer-to-peer 通信的协议。30333
表示指定用于 peer-to-peer 通信的端口号,在本案例中,表示 TCP 流量的端口号。12D3KooWEyoppNCUx8Yx66oV9fJnriXwCcXwDDUA2kj6vnc6iDEp
标识要与此网路通信的运行节点,在本案例中,表示为使用alice
账户启动节点的标识符。
验证区块是否已生成和已最终确认
启动第二个节点后,节点应作为对等节点相互连接,并开始生成块。
- 确认你在启动第一个节点的终端中看到与以下类似的行:
2022-08-16 15:32:33 discovered: 12D3KooWBCbmQovz78Hq7MzPxdx9d1gZzXMsn6HtWj29bW51YUKB /ip4/127.0.0.1/tcp/30334
2022-08-16 15:32:33 discovered: 12D3KooWBCbmQovz78Hq7MzPxdx9d1gZzXMsn6HtWj29bW51YUKB /ip6/::1/tcp/30334
2022-08-16 15:32:36 🙌 Starting consensus session on top of parent 0x2cdce15d31548063e89e10bd201faa63c623023bbc320346b9580ed3c40fa07f
2022-08-16 15:32:36 🎁 Prepared block for proposing at 1 (5 ms) [hash: 0x9ab34110e4617454da33a3616efc394eb1ce95ee4bf0daab69aa4cb392d4104b; parent_hash: 0x2cdc…a07f; extrinsics (1): [0x4634…cebf]]
2022-08-16 15:32:36 🔖 Pre-sealed block for proposal at 1. Hash now 0xf0869a5cb8ebd0fcc5f2bc194ced84ca782d9749604e888c8b9b515517179847, previously 0x9ab34110e4617454da33a3616efc394eb1ce95ee4bf0daab69aa4cb392d4104b.
2022-08-16 15:32:36 ✨ Imported #1 (0xf086…9847)
2022-08-16 15:32:36 💤 Idle (1 peers), best: #1 (0xf086…9847), finalized #0 (0x2cdc…a07f), ⬇ 1.0kiB/s ⬆ 1.0kiB/s
2022-08-16 15:32:41 💤 Idle (1 peers), best: #1 (0xf086…9847), finalized #0 (0x2cdc…a07f), ⬇ 0.6kiB/s ⬆ 0.6kiB/s
2022-08-16 15:32:42 ✨ Imported #2 (0x0d5e…2a7f)
2022-08-16 15:32:46 💤 Idle (1 peers), best: #2 (0x0d5e…2a7f), finalized #0 (0x2cdc…a07f), ⬇ 0.6kiB/s ⬆ 0.6kiB/s
2022-08-16 15:32:48 🙌 Starting consensus session on top of parent 0x0d5ef31979c2aa17fb88497018206d3057151119337293fe85d9526ebd1e2a7f
2022-08-16 15:32:48 🎁 Prepared block for proposing at 3 (0 ms) [hash: 0xa307c0112bce39e0dc689132452154da2079a27375b44c4d94790b46a601346a; parent_hash: 0x0d5e…2a7f; extrinsics (1): [0x63cc…39a6]]
2022-08-16 15:32:48 🔖 Pre-sealed block for proposal at 3. Hash now 0x0c55670e745dd12892c9e7d5205085a78ccea98df393a822fa9b3865cfb3d51b, previously 0xa307c0112bce39e0dc689132452154da2079a27375b44c4d94790b46a601346a.
2022-08-16 15:32:48 ✨ Imported #3 (0x0c55…d51b)
2022-08-16 15:32:51 💤 Idle (1 peers), best: #3 (0x0c55…d51b), finalized #1 (0xf086…9847), ⬇ 0.7kiB/s ⬆ 0.9kiB/s
...
在这些行中,你可以看到有关你的区块链的以下信息:
- 在这个网络中发现第二个节点的标识(
12D3KooWBCbmQovz78Hq7MzPxdx9d1gZzXMsn6HtWj29bW51YUKB
)。 - 该节点有一个peer(
1 peers
)。 - 节点产生了一些块(
best: #3 (0x0c55…d51b)
)。 - 这些块被最终确认(
finalized #1 (0xf086…9847)
)。
- 验证你在启动第二个节点的终端中看到类似的输出。
- 在终端中按下的 Control-c,关闭其中一个节点。关闭节点后,你将看到剩余节点现在没有对等节点,并且已停止生成块。例如:
2022-08-16 15:53:45 💤 Idle (1 peers), best: #143 (0x8f11…1684), finalized #141 (0x5fe3…5a25), ⬇ 0.8kiB/s ⬆ 0.7kiB/s
2022-08-16 15:53:50 💤 Idle (0 peers), best: #143 (0x8f11…1684), finalized #141 (0x5fe3…5a25), ⬇ 83 B/s ⬆ 83 B/s
- 通过在终端中按下 Control-c 关闭第二个节点。如果你使用
--dev
命令行选项在开发模式下启动节点的,则节点的所有状态都被清除。
添加可信任的节点
在本节教程中,你将为网络中的验证器节点生成自己的密钥。重要的是要记住,区块链网络中的每个参与者都负责生成唯一密钥。有几种方法可以生成密钥,例如,可以使用 node-template
子命令、独立的 subkey 命令行程序、Polkadot-JS 应用程序或者第三方密钥生成程序来生成密钥对。
虽然你可以使用预定义的密钥对来完成本教程,但你应该永远不会在生产环境中使用这些 key。本教程不使用预定义的密钥或更安全的 subkey
程序,而是演示如何使用 Substrate node template 和 key
子命令生成密钥。
使用 node template 生成本地密钥
./target/release/node-template key generate --scheme Sr25519 --password-interactive
该命令生成密钥并显示类似如下的输出:
Secret phrase: pig giraffe ceiling enter weird liar orange decline behind total despair fly
Secret seed: 0x0087016ebbdcf03d1b7b2ad9a958e14a43f2351cd42f2f0a973771b90fb0112f
Public key (hex): 0x1a4cc824f6585859851f818e71ac63cf6fdc81018189809814677b2a4699cf45
Account ID: 0x1a4cc824f6585859851f818e71ac63cf6fdc81018189809814677b2a4699cf45
Public key (SS58): 5CfBuoHDvZ4fd8jkLQicNL8tgjnK8pVG9AiuJrsNrRAx6CNW
SS58 Address: 5CfBuoHDvZ4fd8jkLQicNL8tgjnK8pVG9AiuJrsNrRAx6CNW
现在你有了 Sr25519 key,对于一个节点使用 aura
生成块。在本例中,帐户的 Sr25519 公钥为 5CfBuoHDvZ4fd8jkLQicNL8tgjnK8pVG9AiuJrsNrRAx6CNW
。
使用刚刚生成的帐户的 secret phrase,使用 Ed25519 签名方案派生密钥。
./target/release/node-template key inspect --password-interactive --scheme Ed25519 "pig giraffe ceiling enter weird liar orange decline behind total despair fly"
该命令显示类似于以下内容的输出:
Secret phrase `pig giraffe ceiling enter weird liar orange decline behind total despair fly` is account:
Secret seed: 0x0087016ebbdcf03d1b7b2ad9a958e14a43f2351cd42f2f0a973771b90fb0112f
Public key (hex): 0x2577ba03f47cdbea161851d737e41200e471cd7a31a5c88242a527837efc1e7b
Public key (SS58): 5CuqCGfwqhjGzSqz5mnq36tMe651mU9Ji8xQ4JRuUTvPcjVN
Account ID: 0x2577ba03f47cdbea161851d737e41200e471cd7a31a5c88242a527837efc1e7b
SS58 Address: 5CuqCGfwqhjGzSqz5mnq36tMe651mU9Ji8xQ4JRuUTvPcjVN
现在你有了 Ed25519 key,对于一个节点使用 grandpa
最终确认块。在本例中,帐户的 Ed25519 公钥为 5CuqCGfwqhjGzSqz5mnq36tMe651mU9Ji8xQ4JRuUTvPcjVN
。
生成第二组密钥
对于本教程,专用网络仅包含两个节点,因此你需要两组 key。 为了便于说明,本教程中使用的第二组关键点是:
- Sr25519: 5EJPj83tJuJtTVE2v7B9ehfM7jNT44CBFaPWicvBwYyUKBS6 (
aura
) - Ed25519: 5FeJQsfmbbJLTH1pvehBxrZrT5kHvJFj84ZaY5LK7NU87gZS (
grandpa
)
创建自定义 chain specification
为了简单起见,你在本节教程中创建的自定义 chain specification 是本地 chain specification 的修改版本,说明了如何创建双节点网络。如果你有所需的密钥,可以按照相同的步骤向网络添加更多节点。
修改本地 chain specification
你可以修改预定义的本地 chain specification,而不是编写全新的 chain specification。
在本地 chain specification 的基础上创建一个新的 chain specification:
./target/release/node-template build-spec --disable-default-bootnode --chain local > customSpec.json
如果你在文本编辑器中打开 customSpec.json
文件,则会看到它包含多个字段。其中一个字段是使用 cargo build --release
命令构建的运行时的 WebAssembly(Wasm)二进制文件。由于 WebAssembly(WASM)二进制是一个二进制大型对象(blob),因此你可以预览第一行和最后几行,以查看需要更改的字段。预览 customSpec.json
中的前几个字段,通过运行以下命令:
head customSpec.json
该命令显示文件中的前几个字段。例如:
{
"name": "Local Testnet",
"id": "local_testnet",
"chainType": "Local",
"bootNodes": [],
"telemetryEndpoints": null,
"protocolId": null,
"properties": null,
"consensusEngine": null,
"codeSubstitutes": {},
预览 customSpec.json
中的最后几个字段,通过运行下面的命令
tail -n 80 customSpec.json
此命令显示 Wasm 二进制字段后面的最后部分,包括运行时使用的几个 pallets(如 sudo
和 balances
)的详细信息。
在文本编辑器中打开 customSpec.json
文件,修改 name
字段以将此 chain specification 标识为自定义 chain specification。
"name": "My Custom Testnet",
修改 aura
字段,通过为每个网络参与者添加 Sr25519 SS58 地址 keys,指定有权创建块的节点。
"aura": { "authorities": [
"5CfBuoHDvZ4fd8jkLQicNL8tgjnK8pVG9AiuJrsNrRAx6CNW", "5CXGP4oPXC1Je3zf5wEDkYeAqGcGXyKWSRX2Jm14GdME5Xc5"
]
},
修改 grandpa
字段,以指定节点,并通过为每个网络参与者添加 Ed25519 SS58 地址 keys 来最终确定块。
"grandpa": {
"authorities": [
[
"5CuqCGfwqhjGzSqz5mnq36tMe651mU9Ji8xQ4JRuUTvPcjVN",
1
],
[
"5DpdMN4bVTMy67TfMMtinQTcUmLhZBWoWarHvEYPM4jYziqm",
1
]
]
},
请注意,在 grandpa
部分中有两个 authorities
字段的数据值。第一个值是地址键。第二个值用于支持 weighted votes。在本例中,每个验证者的权重为 1 票。
添加验证者
如刚才看到的,你可以通过修改 aura
和 grandpa
部分在 chain specification 中添加和更改授权地址。你可以使用此技术添加任意数量的验证者。
添加验证:
- 修改
aura
部分,使其包含 Sr25519 地址。 - 修改
grandpa
部分,使其包含 Ed25519 地址和一个投票权重。
确保为每个验证者使用唯一的键,如果两个验证器有相同的键,它们就会产生冲突的块。
转换 chain specification 为原始格式
在使用验证者信息准备 chain specification 之后,必须将其转换为 raw specification 格式,然后才能使用。raw chain specification 包含与未转换 specification 相同的信息。然而,raw chain specification 还包含编码的存储密钥,节点使用这些密钥来引用其本地存储中的数据。分配 raw chain specification 可确保每个节点使用适当的存储密钥存储数据。 将 chain specification 转换为使用原始格式:
./target/release/node-template build-spec --chain=customSpec.json --raw --disable-default-bootnode > customSpecRaw.json
与其他人共享 chain specification
如果你正在创建私人区块链网络以与其他参与者共享,请确保只有一个人创建 chain specification 并共享该 specification 的最终原始版本,例如 customSpecRaw.json
文件和网络中的所有其他验证器。
由于 Rust 编译器生成的优化的 WebAssembly 二进制文件在确定性上不可复制,因此生成 Wasm 运行时的人都会生成一个略微不同的 Wasm blob。为了确保确定性,区块链网络中的所有参与者必须使用完全相同的 raw chain specification 文件。
准备启动私有网络
要继续,请验证以下内容:
- 你已为至少两个授权帐户生成或收集了帐户密钥。
- 你已经更新了自定义 chain specification,以包括块生成的 key(
aura
)和块最终确认的 key(grandpa
)。 - 你已经将自定义 chain specification 转换为原始格式,并将原始 chain specification 分发给参与私有网络的节点。
如果你完成了这些步骤,就可以启动私有区块链中的第一个节点了。
开始第一个节点
作为私有区块链网络的第一个参与者,你负责启动第一个节点,被称为 bootnode。 通过运行与以下类似的命令,使用自定义 chain specification 启动第一个节点:
./target/release/node-template \
--base-path /tmp/node01 \
--chain ./customSpecRaw.json \
--port 30333 \
--ws-port 9945 \
--rpc-port 9933 \
--telemetry-url "wss://telemetry.polkadot.io/submit/ 0" \
--validator \
--rpc-methods Unsafe \
--name MyNode01 \
--password-interactive
注意以下命令行选项启动节点:
--base path
命令行选项指定与第一个节点关联链的自定义位置。--chain
命令行选项指定自定义 chain specification。--validator
命令行选项指示此节点已经被链授权。--rpc-methods Unsafe
命令行选项允许你使用不安全的通信模式,在本教程继续这样用,因为你的区块链未在生产设置中使用。--name
命令行选项使你能够在遥测 UI 中为节点提供一个方便阅读的名称。
此命令还使用自己的 keys 而不是预定义的帐户启动节点。由于你没有使用已知 keys 的预定义帐户,因此你需要在单独的步骤中将 keys 添加到密钥库中。
查看有关节点操作的信息
启动本地节点后,有关所执行操作的信息将显示在终端外壳中。在该终端中,验证看到的输出与以下类似:
2021-11-03 15:32:14 Substrate Node
2021-11-03 15:32:14 ✌️ version 3.0.0-monthly-2021-09+1-bf52814-x86_64-macos
2021-11-03 15:32:14 ❤️ by Substrate DevHub <https://github.com/substrate-developer-hub>, 2017-2021
2021-11-03 15:32:14 📋 Chain specification: My Custom Testnet
2021-11-03 15:32:14 🏷 Node name: MyNode01
2021-11-03 15:32:14 👤 Role: AUTHORITY
2021-11-03 15:32:14 💾 Database: RocksDb at /tmp/node01/chains/local_testnet/db
2021-11-03 15:32:14 ⛓ Native runtime: node-template-100 (node-template-1.tx1.au1)
2021-11-03 15:32:15 🔨 Initializing Genesis block/state (state: 0x2bde…8f66, header-hash: 0x6c78…37de)
2021-11-03 15:32:15 👴 Loading GRANDPA authority set from genesis on what appears to be first startup.
2021-11-03 15:32:15 ⏱ Loaded block-time = 6s from block 0x6c78abc724f83285d1487ddcb1f948a2773cb38219c4674f84c727833be737de
2021-11-03 15:32:15 Using default protocol ID "sup" because none is configured in the chain specs
2021-11-03 15:32:15 🏷 Local node identity is: 12D3KooWLmrYDLoNTyTYtRdDyZLWDe1paxzxTw5RgjmHLfzW96SX
2021-11-03 15:32:15 📦 Highest known block at #0
2021-11-03 15:32:15 〽️ Prometheus exporter started at 127.0.0.1:9615
2021-11-03 15:32:15 Listening for new connections on 127.0.0.1:9945.
2021-11-03 15:32:20 💤 Idle (0 peers), best: #0 (0x6c78…37de), finalized #0 (0x6c78…37de), ⬇ 0 ⬆ 0
注意以下信息:
- 输出表明所使用的 chain specification 是你使用
--chain
命令行选项创建和指定的自定义 chain specification。 - 输出表明该节点是有授权的,因为你使用
--validator
命令行选项启动了该节点。 - 输出显示了使用块哈希初始化的 创世块
(state: 0x2bde…8f66, header-hash: 0x6c78…37de)
。 - 输出指定了节点的 本地节点标识,在本例中,节点标识为
12D3KooWLmrYDLoNTyTYtRdDyZLWDe1paxzxTw5RgjmHLfzW96SX
。 - 输出指定用于节点的 IP 地址是本地主机
127.0.0.1
。
这些值用于本节教程示例中你的节点,你必须将节点的值提供给其他网络参与者以连接到 bootnode。现在你已经使用自己的密钥成功启动了一个验证器节点,并看到了节点标识,你可以继续下一步。但是,在将密钥添加到密钥库之前,请按 Control-c 停止该节点。
将 key 添加到密钥库
启动第一个节点后,尚未生成任何块。下一步是为网络中的每个节点向密钥库添加两种类型的 key。 对于每个节点:
- 添加
aura
授权 keys 以启用区块的生成。 - 添加
grandpa
授权 keys 以启用区块的最终确认。
有几种方法可以将 key 插入密钥库。对于本节教程,你可以使用 key
子命令插入本地生成的 key。
通过运行与以下类似的命令,插入从 key
子命令生成的 aura
密钥(key):
./target/release/node-template key insert --base-path /tmp/node01 \
--chain customSpecRaw.json \
--scheme Sr25519 \
--suri <your-secret-seed> \
--password-interactive \
--key-type aura
使用 secret phrase 或者 secret seed 替换 <your-secret-seed>
,在之前 使用 node template 生成本地密钥 的部分查找相关的第一个密钥对。
在本节教程中,secret phrase 是 pig giraffe ceiling enter weird liar orange decline behind total despair fly
,因此 --suri
命令行选项使用这段字符串将 key 插入密钥库。
--suri "pig giraffe ceiling enter weird liar orange decline behind total despair fly"
你也可以从指定的文件位置插入 key,有关可用命令行选项的信息,请运行以下命令:
./target/release/node-template key insert --help
输入生成密钥的密码。
通过运行与以下类似的命令,插入从 key 子命令生成的 grandpa
密钥(key):
./target/release/node-template key insert \
--base-path /tmp/node01 \
--chain customSpecRaw.json \
--scheme Ed25519 \
--suri <your-secret-key> \
--password-interactive \
--key-type gran
使用 secret phrase 或者 secret seed 替换 <your-secret-seed>
,在之前 使用 node template 生成本地密钥 的部分查找相关的第一个密钥对。
在本节教程中,secret phrase 是 pig giraffe ceiling enter weird liar orange decline behind total despair fly
,因此 --suri
命令行选项使用这段字符串将 key 插入密钥库。
--suri "pig giraffe ceiling enter weird liar orange decline behind total despair fly"
输入生成密钥的密码。
通过运行以下命令验证你的密钥(key)是否位于 node01
的密钥库中:
ls /tmp/node01/chains/local_testnet/keystore
该命令显示类似于以下的输出:
617572611441ddcb22724420b87ee295c6d47c5adff0ce598c87d3c749b776ba9a647f04
6772616e1441ddcb22724420b87ee295c6d47c5adff0ce598c87d3c749b776ba9a647f04
在 /tmp/node01 下的第一个节点的密钥库中添加了 keys 后,你可以使用先前在 开始第一个节点 中使用的命令重新启动节点。
开启允许其他参与者加入
现在可以使用 --bootnodes
和 --validator
命令行选项允许其他验证者加入网络。
向私有网络添加第二个验证者区块链节点:
./target/release/node-template \
--base-path /tmp/node02 \
--chain ./customSpecRaw.json \
--port 30334 \
--ws-port 9946 \
--rpc-port 9934 \
--telemetry-url "wss://telemetry.polkadot.io/submit/ 0" \
--validator \
--rpc-methods Unsafe \
--name MyNode02 \
--bootnodes /ip4/127.0.0.1/tcp/30333/p2p/12D3KooWLmrYDLoNTyTYtRdDyZLWDe1paxzxTw5RgjmHLfzW96SX \
--password-interactive
此命令使用 base-path
、name
和 validator
命令行选项将此节点标识为专用网络的第二个验证者。--chain
命令行选项指定要使用的 chain specification 文件,对于网络中的所有验证者,此文件必须完全相同。确保为 --bootnodes
命令行选项设置了正确的信息。特别是,请确保已从网络中的第一个节点指定了本地节点标识符。如果未设置正确的 bootnode
标识符,则会看到如下错误:
The bootnode you want to connect to at ... provided a different peer ID than the one you expect: ...
通过运行与以下类似的命令,插入从 key 子命令生成的 aura
密钥(key):
./target/release/node-template key insert --base-path /tmp/node02 \
--chain customSpecRaw.json \
--scheme Sr25519 \
--suri <second-participant-secret-seed> \
--password-interactive \
--key-type aura
使用 secret phrase 或者 secret seed 替换 <second-participant-secret-seed>
,在之前 生成第二组密钥 的部分查找相关的第二个密钥对。只有 --key-type
的值是 aura
才能开启区块生成功能。
输入生成密钥的密码。
通过运行与以下类似的命令,插入从 key 子命令生成的 grandpa
密钥(key):
./target/release/node-template key insert --base-path /tmp/node02 \
--chain customSpecRaw.json \
--scheme Ed25519 --suri <second-participant-secret-seed> \
--password-interactive \
--key-type gran
使用 secret phrase 或者 secret seed 替换 <second-participant-secret-seed>
,在之前 生成第二组密钥 的部分查找相关的第二个密钥对。只有 --key-type
的值是 gran
才能开启区块最终确认功能。
块终结需要至少三分之二的验证者将其密钥添加到各自的密钥库中。由于此网络在 chain specification 中配置了两个验证器,因此只有在第二个节点添加了其密钥后,才能开启块最终确认。
输入生成密钥的密码。
通过运行以下命令验证你的密钥(key)是否位于 node02
的密钥库中:
ls /tmp/node02/chains/local_testnet/keystore
该命令显示类似于以下的输出:
617572611a4cc824f6585859851f818e71ac63cf6fdc81018189809814677b2a4699cf45
6772616e1a4cc824f6585859851f818e71ac63cf6fdc81018189809814677b2a4699cf45
Substrate nodes 在插入 grandpa
密钥之后需要重新启动,因此,你必须关闭并重新启动节点,然后才能看到正在完成的块。按 Control-c 关闭节点。
通过运行以下命令重新启动第二个区块链节点:
./target/release/node-template \
--base-path /tmp/node02 \
--chain ./customSpecRaw.json \
--port 30334 \
--ws-port 9946 \
--rpc-port 9934 \
--telemetry-url 'wss://telemetry.polkadot.io/submit/ 0' \
--validator \
--rpc-methods Unsafe \
--name MyNode02 \
--bootnodes /ip4/127.0.0.1/tcp/30333/p2p/12D3KooWLmrYDLoNTyTYtRdDyZLWDe1paxzxTw5RgjmHLfzW96SX \
--password-interactive
在两个节点将其密钥 key 添加到位于 /tmp/node01 和 /tmp/node02 下的各自密钥库并重新启动后,你应该看到相同的 genesis 块和状态根哈希。你还应该看到,每个节点都有一个 peer(1 peers
),并且它们生成了一个块 proposal (best: #2 (0xe111…c084)
)。几秒钟后,你将看到两个节点上的新块都已最终确认。
授权特定节点
本节教程演示了许可网络的简化版本。在许可网络中,仅允许授权节点执行特定的网络活动。例如,你可以授予某些节点验证块的权限,而授予其他节点传播交易的权限。具有被授予特定权限的节点的区块链不同于公共或无许可的区块链。在无许可的区块链中,任何人都可以通过在合适的硬件上运行节点软件来加入网络。一般而言,无许可区块链提供了更大的网络去中心化。然而,在某些用例中,创建许可的区块链可能是合适的。例如,许可区块链适用于以下类型的项目:
- 用于私人或联盟网络,例如私人企业或非营利组织。
- 在高度监管的数据环境中,如医疗保健、金融或企业对企业之间的联盟。
- 用于大规模测试预公开区块链网络。
本节教程说明如何通过 node authorization pallet 使用 Substrate 构建一个许可网络。
节点授权和所有权
node-authorization
pallet 是一个预先构建的 FRAME pallet,使你能够管理网络的一组可配置节点。每个节点由一个 PeerId
标识,每个 PeerId
由一个且仅一个声明节点的 AccountId
拥有。有两种方式可以授权节点加入网络:
- 通过将
PeerId
添加到预定义节点的列表中,你必须获得网络中的 governance pallet 或 sudo pallet 的核准才能执行此操作。 - 通过请求来自特定节点 paired peer 连接,该节点可以是预定义的节点
PeerId
,也可以是普通节点。
请注意,任何用户都可以声称是 PeerId
的所有者,为了防止错误声明,应在启动节点之前声明节点。在你启动节点后,它的 PeerID
对网络是可见的,任何人随后都可以声明它。
作为节点的所有者,你可以添加和删除节点的连接。例如,你可以操纵预定义节点与你的节点之间的连接,或者操纵你的节点与其他非预定义节点之间的关系。你不能更改预定义节点的连接,因为他们总是被允许互相连接。
node-authorization
pallet 使用一个链下工作机配置其节点连接。请确保在启动节点时已经启用了链外工作机,因为对于非授权节点,它在默认情况下是禁用的。
构建节点模板
cd substrate-node-template
git checkout polkadot-v0.9.26
cargo build --release
添加节点授权pallet
由于 Substrate 运行时编译为包含标准库函数的原生 Rust 二进制文件和不包含标准库的 WebAssembly(Wasm)二进制文件,因此Cargo.toml 文件控制两条重要信息:
- 将 pallets 作为运行时的依赖项导入,包括要导入的 pallets 的位置和版本。
- 编译原生 Rust 二进制文件时应启用每个 pallet 中的功能。通过从每个 pallet 启用标准库(
std
)的特性,你可以编译运行时以包含函数、类型和原语,否则在构建 WebAssembly 二进制文件时会丢失这些函数、类型和原语。
有关在 Cargo.toml
文件中添加依赖项的一些信息,可参见 Cargo 文档中的依赖。有关启用和管理依赖包的功能的信息,可参见 Cargo 文档中的特性。
添加节点授权的依赖
在 [dependencies]
部分添加 pallet-node-authorization
crate,使其用于 node template 运行时:
[dependencies]
pallet-node-authorization = { default-features = false, version = "4.0.0-dev", git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.26" }
该行 pallet-node-authorization
crate 作为依赖导入,并且为该 crate 指定以下配置细节:
- 当编译运行时,默认情况下不启用 pallet 特性。
- 检索
pallet-node-authorization
crate 的存储库位置。 - 用于检索 crate 的 commit tag。
- crate 的版本标识符。
将 pallet-node-authorization/std
特性添加到 features
列表中,以便在编译运行时启用。
[features]
default = ['std']
std = [
...
"pallet-node-authorization/std", # add this line
...
]
如果你忘记更新 Cargo.toml
文件中的 features
部分,在编译运行时二进制文件时,你可能会看到 cannot find function
错误。
通过运行以下命令,检查新依赖项是否正确解析:
cargo check -p node-template-runtime
添加管理规则
为了在本节教程中模拟治理,你可以配置 pallet 使用 EnsureRoot
特权函数,然后就能被 Sudo pallet 调用 。默认情况下,Sudo pallet 包含在节点模板中,使你能够通过 root-level 管理帐户进行调用。在生产环境中,你将使用更接近现实的基于治理的方式去检查。
要在运行时启用 EnsureRoot
规则,需要在 runtime/src/lib.rs
文件中添加下面这一行:
use frame_system::EnsureRoot;
为pallet实现Config trait
每个 pallet 都有一个称为 Config
的 Rust trait,Config
trait 用于识别 pallet 所需的参数和类型。添加一个 pallet 所需的大多数 pallet-specific 代码都是使用 Config
trait 实现的。例如,查看你需要在 node-authorization pallet 中实现 Config
trait,你可以参考 pallet_node_authorization::Config
的 Rust 文档。
要在你的运行时实现 node-authorization
pallet ,打开 runtime/src/lib.rs
文件:
- 为 pallet 在
parameter_types
部分添加以下代码:
parameter_types! {
pub const MaxWellKnownNodes: u32 = 8;
pub const MaxPeerIdLength: u32 = 128;
}
- 使用以下代码为 pallet 的
Config
trait 添加impl
部分
impl pallet_node_authorization::Config for Runtime {
type Event = Event;
type MaxWellKnownNodes = MaxWellKnownNodes;
type MaxPeerIdLength = MaxPeerIdLength;
type AddOrigin = EnsureRoot<AccountId>;
type RemoveOrigin = EnsureRoot<AccountId>;
type SwapOrigin = EnsureRoot<AccountId>;
type ResetOrigin = EnsureRoot<AccountId>;
type WeightInfo = ();
}
- 使用以下代码行将 pallet 添加到
construct_runtime
宏:
construct_runtime!(
pub enum Runtime where
Block = Block,
NodeBlock = opaque::Block,
UncheckedExtrinsic = UncheckedExtrinsic
{
/*** Add This Line ***/
NodeAuthorization: pallet_node_authorization::{Pallet, Call, Storage, Event<T>, Config<T>},
}
);
-
保存代码并关闭文件。
-
通过运行以下命令检查配置是否可以编译:
cargo check -p node-template-runtime
为授权节点添加创世纪存储
在启动网络以使用节点授权之前,需要一些额外的配置来处理 peer 标识符和 account 标识符。例如,PeerId
以 bs58 的格式编码,因此你需要在 node/Cargo.toml
中为 bs58 库添加一个新的依赖项对 PeerId
进行解码以获取它的字节。为了简化操作,授权节点与预定义帐户相关联。
要为授权节点配置 genesis 存储,打开 node/Cargo.toml
文件,在 [dependencies] 部分为 node template 添加 bs58
库:
[dependencies]
bs58 = "0.4.0"
保存代码并关闭文件。
在编辑器中打开 node/src/chain_spec.rs
文件,使用以下代码为授权加入网络的节点添加 genesis 存储:
use sp_core::OpaquePeerId; // A struct wraps Vec<u8>, represents as our `PeerId`.
use node_template_runtime::NodeAuthorizationConfig; // The genesis config that serves for our pallet.
为 FRAME 模块配置初始存储状态的 testnet_genesis
函数:
/// Configure initial storage state for FRAME modules.
fn testnet_genesis(
wasm_binary: &[u8],
initial_authorities: Vec<(AuraId, GrandpaId)>,
root_key: AccountId,
endowed_accounts: Vec<AccountId>,
_enable_println: bool,
) -> GenesisConfig {
在 GenesisConfig
声明中,添加以下代码块:
node_authorization: NodeAuthorizationConfig {
nodes: vec![
(
OpaquePeerId(bs58::decode("12D3KooWBmAwcd4PJNJvfV89HwE48nwkRmAgo8Vy3uQEyNNHBox2").into_vec().unwrap()),
endowed_accounts[0].clone()
),
(
OpaquePeerId(bs58::decode("12D3KooWQYV9dGMFoRzNStwpXztXaBUjtPqi6aU76ZgUriHhKust").into_vec().unwrap()),
endowed_accounts[1].clone()
),
],
},
在这部分代码中,NodeAuthorizationConfig
包含一个 nodes
属性,代表一个拥有两个元素 tuple 的 vector。元组中第一个元素是 OpaquePeerId
,bs58::decode
操作转化为方便 human-readable PeerId
,如,12D3KooWBmAwcd4PJNJvfV89HwE48nwkRmAgo8Vy3uQEyNNHBox2
到字节。元组中第二个元素是 AccountId
,代表此节点的所有者,该示例使用预定义的 Alice 和 Bob,此处标识为 endowed 的帐户 [0] 和 [1]。
保存代码并关闭文件。
验证节点是否可编译
现在已经完成了代码更改,可以验证节点是否可编译。
cargo build --release
如果没有语法错误,则可以继续。如果存在错误,请按照编译输出中的说明进行修复,然后重新运行 cargo build
命令。
启动许可网络
你现在可以使用预定义帐户的节点密钥和对等标识符来启动许可网络并授权其他节点加入。在本节教程,你将启动四个节点。其中三个节点与预定义的帐户相关联,所有三个节点都允许生成和验证块。第四节点是一个子节点,仅有权在指定节点所有者的批准下从该节点读取数据。
获取节点 keys 和 peerIDs
你已经在 genesis 存储中配置了与 Alice 和 Bob 帐户关联的节点。你可以使用 subkey 程序检查与预定义帐户关联的密钥,并生成和检查你自己的密钥。然而,如果你运行 subkey generate-node-key
命令,那么你的节点 key 和 peer 标识符是随机生成的,与本节教程中使用的 key 并不匹配。由于本节教程使用预定义的帐户和 well-known node keys,下表总结了每个帐户的 key。
|账户|与账户关联的 Keys|
|:-----|:-----|
|Alice|Node key: c12b6d18942f5ee8528c8e2baf4e147b5c5c18710926ea492d09cbd9f6c9f82a|
||PeerID (generated from the node key): 12D3KooWBmAwcd4PJNJvfV89HwE48nwkRmAgo8Vy3uQEyNNHBox2|
||Decoded PeerID in hex: 0024080112201ce5f00ef6e89374afb625f1ae4c1546d31234e87e3c3f51a62b91dd6bfa57df|
|Bob|Node key: 6ce3be907dbcabf20a9a5a60a712b4256a54196000a8ed4050d352bc113f8c58
|
||PeerID (generated from the node key): 12D3KooWQYV9dGMFoRzNStwpXztXaBUjtPqi6aU76ZgUriHhKust|
||Decoded PeerID in hex: 002408011220dacde7714d8551f674b8bb4b54239383c76a2b286fa436e93b2b7eb226bf4de7|
另外两个开发帐户 Charlie 和 Dave 没有 well-known node keys 或 peer 标识符。出于演示目的,我们将使用以下的 keys: |账户|与账户关联的 Keys| |:-----|:-----| |Charlie|Node key: 3a9d5b35b9fb4c42aafadeca046f6bf56107bd2579687f069b42646684b94d9e| ||PeerID (generated from the node key): 12D3KooWJvyP3VJYymTqG7eH4PM5rN4T2agk5cdNCfNymAqwqcvZ| ||Decoded PeerID in hex: 002408011220876a7b4984f98006dc8d666e28b60de307309835d775e7755cc770328cdacf2e| |Dave|Node key: a99331ff4f0e0a0434a6263da0a5823ea3afcfffe590c9f3014e6cf620f2b19a| ||PeerID (generated from the node key): 12D3KooWPHWFrfaJzxPnqnAYAoRUyAHHKqACmEycGTVmeVhQYuZN| ||Decoded PeerID in hex: 002408011220c81bc1d7057a1511eb9496f056f6f53cdfe0e14c8bd5ffca47c70a8d76c1326d|
启动第一个节点
./target/release/node-template \
--chain=local \
--base-path /tmp/validator1 \
--alice \
--node-key=c12b6d18942f5ee8528c8e2baf4e147b5c5c18710926ea492d09cbd9f6c9f82a \
--port 30333 \
--ws-port 9944
在这个命令中,使用 --node-key
选项指定用于安全连接到网络的 key。该 key 还用于内部生成 human-readable PeerId,如上部分所示。--alice
将节点命名为 alice,并使节点成为可以生成块和完成块的验证者,是 --name alice --validator
的简写。
启动第二个节点
./target/release/node-template \
--chain=local \
--base-path /tmp/validator2 \
--bob \
--node-key=6ce3be907dbcabf20a9a5a60a712b4256a54196000a8ed4050d352bc113f8c58 \
--port 30334 \
--ws-port 9945
两个节点启动后,你应该能够在两个终端日志中看到生成和完成的新块。
添加第三个节点到已知节点列表中
node-authorization
pallet 使用链下工作机来配置节点连接。因为第三个节点不是一个 well-known 节点并且它将网络中的第四个节点配置为 read-only sub-node,因此你必须以命令行选项以启动链下工作机。
./target/release/node-template \
--chain=local \
--base-path /tmp/validator3 \
--name charlie \
--node-key=3a9d5b35b9fb4c42aafadeca046f6bf56107bd2579687f069b42646684b94d9e \
--port 30335 \
--ws-port=9946 \
--offchain-worker always
启动此节点后,你应该会看到该节点没有连接的 peers。由于这是一个许可网络,因此必须明确授权节点进行连接。Alice 和 Bob 节点被配置在创世纪 chain_spec.rs
的文件中。所有其它节点都必须通过 Sudo pallet 的调用来手动添加。
授权对第三节点的访问
本节教程使用 sudo pallet 进行治理。因此,你可以使用 sudo
pallet 调用 node-authorization
pallet 提供的 add_well_known_node
函数来添加第三个节点。
切换到 Developer 页面,Sudo 标签,在应用程序中,提交 nodeAuthorization
- add_well_known_node
调用,peer id为 Charlie 节点的十六进制,所有者为 Charlie。
在事务被包括在块中之后,你应该可以看到 charlie 节点被连接到 alice 和 bob 节点,并开始同步块。这三个节点可以使用本地网络中默认启用的 mDNS 发现机制找到彼此。
如果你的节点不在同一个本地网络上,你应该使用命令行选项 --no-mdns
来禁用它。
添加一个子节点
该网络中的第四节点不是 well-known 的节点。这个节点的所有者是用户 dave
,但它是 charlie
的 sub-node。子节点只能通过连接到 charlie
拥有的节点来访问网络。父节点负责任何子节点的授权连接,并在需要删除或审计该子节点时控制访问。
./target/release/node-template \
--chain=local \
--base-path /tmp/validator4 \
--name dave \
--node-key=a99331ff4f0e0a0434a6263da0a5823ea3afcfffe590c9f3014e6cf620f2b19a \
--port 30336 \
--ws-port 9947 \
--offchain-worker always
启动后,没有可用的连接。这是一个许可网络,因此首先,Charlie 需要配置他的节点以允许从 Dave 的节点进行连接。
切换到 Developer Extrinsics 页面,让 Charlie 去提交一个 addConnections
extrinsic。第一个 PeerId 是 Charlie 所在节点的十六进制 peer id。连接是 Charlie 节点允许的 peer ids 列表,这里我们只添加 Dave 的。
然后,Dave 需要配置他的节点以允许从 Charlie 的节点进行连接。但在他添加这一点之前,Dave 需要声明他的节点,证明这个节点的所有权。
类似地,Dave 可以从 Charlie 的节点添加连接。
你现在应该看到 Dave 正在捕获 blocks,且只有一个属于 Charlie 的同伴!重新启动 Dave 的节点,以防它无法立即与 Charlie 连接。
任何节点都可以发布影响其他节点行为的 extrinsics,只要它是用作引用链上的数据,并且你在密钥存储库中拥有可用于所需来源的相关帐户的 singing key。本演示中的所有节点都可以访问开发人员的 singing key,因此我们能够代表 charlie 从网络上的任何连接节点发出影响 charlie 子节点的命令。在实际应用中,节点操作员只能访问其节点 keys,并且是唯一能够正确签名和提交 extrinsics 的人,很可能来自他们自己的节点,在那里他们可以控制 key 的安全性。
现在,您已经学习了如何构建一个网络,其中某些节点具有有限的权限和对网络资源的访问权限。
监视节点指标
升级一个运行中的网络
与许多区块链不同,Substrate 开发框架支持对作为区块链核心的运行时进行无分叉升级。大多数区块链项目需要一个硬分叉的代码库,以支持新功能的持续开发或现有功能的增强。使用Substrate,你可以部署增强的运行时功能,在不使用硬分叉的情况下进行中断迭代。因为运行时的定义本身是 Substrate 链状态中的一个元素,网络参与者可以通过调用交易中的 set_code
函数来更新此值。由于运行时状态的更新是对区块链的共识机制和加密保证进行验证,网络参与者可以使用区块链本身分发更新或扩展的运行时逻辑,而无需分叉链或发布新的区块链客户端。
本节教程演示如何通过将以下更改部署到现有的 Substrate 运行时来执行无分叉升级:
- 将 Scheduler pallet 添加到运行时。
- 使用 Scheduler pallet 增加网络帐户的最低余额。
使用Sudo托盘授权升级
在 FRAME 中,Root
源标识着运行时管理员。只有此管理员才能通过调用 set_code
函数来更新运行时。要使用 Root
源调用此函数,可以使用 Sudo pallet 中的 sudo
函数来指定具有超级用户管理权限的帐户。
默认情况下,node template 的 chain specification 文件指定 alice
开发帐户是 Sudo 管理帐户的所有者。因此,本节教程使用 alice
帐户执行运行时升级。
运行时升级的资源核算
分发到 Substrate 运行时的函数调用总是与权重相关联,以反映资源使用情况。FRAME System 模块设置边界,可以使用这些交易的块长度和块权重。然而,set_code
函数被有意设计为消耗一个块所能容纳的最大权重。强制运行时升级以消耗整个块,可以防止同一块中的交易在运行时的不同版本上执行。
set_code
函数的权重注释还指定该函数位于 Operational
类中,因为它提供网络功能。标识为可操作的函数调用:
- 可以消耗一个块的全部权重限制。
- 优先级最高。
- 免除支付交易费用。
管理资源核算
在本节教程中,sudo_unchecked_weight
函数用于为运行时升级调用 set_code
函数。sudo_unchecked_weight
函数与 sudo
函数相同,只是它支持一个附加参数来指定用于调用的权重。此参数使你能够绕过资源核算保护措施,为分发 set_code
函数的调用指定零权重。此设置允许一个块花费无限时间进行计算,以确保运行时升级不会失败,无论操作多么复杂。它可能需要所有时间才能显示成功或失败。
升级运行时以添加调度程序pallet
node template 在其运行时中不包含 Scheduler pallet。为了演示运行时升级,让我们将 Scheduler pallet 添加到运行节点。
要升级运行时,通过运行下面的命令在开发模式下启动本地节点:
cargo run --release -- --dev
保持节点运行。你可以编辑并重新编译以升级运行时,而无需停止或重新启动正在运行的节点。
在第二个终端中,在编辑器中打开 substrate-node-template/runtime/Cargo.toml
文件,添加 Scheduler pallet 作为依赖项。
[dependencies]
...
pallet-scheduler = { version = "4.0.0-dev", default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.28" }
...
添加 Scheduler pallet 到 features
列表中。
[features]
default = ["std"]
std = [
...
"pallet-scheduler/std",
...
在编辑器中打开 substrate-node-template/runtime/src/lib.rs
文件,添加 Scheduler pallet 所需的类型。
parameter_types! {
pub MaximumSchedulerWeight: Weight = 10_000_000;
pub const MaxScheduledPerBlock: u32 = 50;
}
为 Scheduler pallet 的 Config trait 添加实现。
impl pallet_scheduler::Config for Runtime {
type Event = Event;
type Origin = Origin;
type PalletsOrigin = OriginCaller;
type Call = Call;
type MaximumWeight = MaximumSchedulerWeight;
type ScheduleOrigin = frame_system::EnsureRoot<AccountId>;
type MaxScheduledPerBlock = MaxScheduledPerBlock;
type WeightInfo = ();
type OriginPrivilegeCmp = EqualPrivilegeOnly;
type PreimageProvider = ();
type NoPreimagePostponement = ();
}
在 construct_runtime!
宏里添加 Scheduler pallet。
construct_runtime!(
pub enum Runtime where
Block = Block,
NodeBlock = opaque::Block,
UncheckedExtrinsic = UncheckedExtrinsic
{
/*** snip ***/
Scheduler: pallet_scheduler,
}
);
在文件的顶部添加以下 trait 依赖:
pub use frame_support::traits::EqualPrivilegeOnly;
增加 RuntimeVersion
结构中的 spec_version 以升级运行时版本。
pub const VERSION: RuntimeVersion = RuntimeVersion {
spec_name: create_runtime_str!("node-template"),
impl_name: create_runtime_str!("node-template"),
authoring_version: 1,
spec_version: 101, // *Increment* this value, the template uses 100 as a base
impl_version: 1,
apis: RUNTIME_API_VERSIONS,
transaction_version: 1,
};
检查 RuntimeVersion
结构体的组件:
spec_name
指定运行时的名称。impl_name
指定客户端的名称。authoring_version
指定 block authors 的版本。spec_version
指定运行时的版本。impl_version
指定客户端的版本。apis
指定支持的 APIs 列表。transaction_version
指定 dispatchable function 接口的版本。
要升级运行时,你必须递增 spec_version
的值。更多信息请参见 FRAME System 模块和 can_set_code
函数。
保存代码并关闭 substrate-node-template/runtime/src/lib.rs
文件。
在第二个终端窗口或选项卡中构建更新的运行时,而不停止正在运行的节点。
cargo build --release -p node-template-runtime
--release
命令行选项需要较长的编译时间。然而它生成了一个更小的构建工件,更适合提交到区块链网络。存储优化对于任何区块链都至关重要。使用此命令,构建工件将输出到 target/release
目录。
连接到本地节点使用新的构建构件升级运行时。 你可以使用 Polkadot-JS application 连接到本地节点。
选择 Alice 帐户提交对 sudoUncheckedWeight
函数的调用,并且从 system
pallet 调用 setCode
函数作为其参数。
选择 file upload
,然后选择或拖放运行时生成的 WebAssembly 文件。例如,点击选择 target/release/wbuild/node-template-runtime/node_template_runtime.compact.compressed.wasm
文件。保留 _weight
参数的默认值为 0
。
点击 Submit Transaction。
查看授权,然后点击 Sign and Submit。
在交易包含在一个块中之后,Polkadot-JS 应用程序中显示的版本号表明运行时版本现在是 101
。
如果你的本地节点在终端中生成的块与浏览器中显示的匹配,那么你已经成功完成了运行时升级。
下一步,我们将:
- 升级你的运行时版本
- 使用 Scheduler pallet 来调度正在链上运行的运行时升级
调度一个升级
既然 node template 已经升级到包含 Scheduler pallet,那么 schedule
函数 就可以用于执行下一次运行时升级。在之前的部分,我们使用 sudo_unchecked_weight
函数覆盖 set_code
函数的关联权重;在本节中,将安排运行时升级,以便它可以作为块中的唯一外部因素处理。
准备一个可升级的运行时
此升级比之前的升级更简单,只需要更新 runtime/src/lib.rs
中的单个值,除了运行时的 spec_version
之外。
pub const VERSION: RuntimeVersion = RuntimeVersion {
spec_name: create_runtime_str!("node-template"),
impl_name: create_runtime_str!("node-template"),
authoring_version: 1,
spec_version: 102, // *Increment* this value.
impl_version: 1,
apis: RUNTIME_API_VERSIONS,
transaction_version: 1,
};
/*** snip ***/
parameter_types! {
pub const ExistentialDeposit: u128 = 1000; // Update this value.
pub const MaxLocks: u32 = 50;
}
/*** snip ***/
这一变化增加了 Balances pallet 的 ExistentialDeposit
,从 Balances pallet 的角度来看,需要保持帐户存活的最低余额。
请记住,此更改不会导致所有在500到1000之间的余额都被获取,这将需要进行存储迁移,不在本节教程的范围。
构建一个可升级的运行时
cargo build --release -p node-template-runtime
这将覆盖任何以前的构建工件。因此如果你希望有最后一个运行时 Wasm 构建文件的副本,请确保已经将它们复制到其他地方。
升级运行时
在上一节中,Scheduler pallet
配置了 Root
源作为其 ScheduleOrigin
,这意味着可以使用 sudo
函数(而不是 sudo_unchecked_weight
)来调用 schedule
函数。使用此链接打开 Polkadot JS 应用程序 UI 的 Sudo
标签:https://polkadot.js.org/apps/#/sudo?rpc=ws://127.0.0.1:9944。
在提供 when
参数之前,等待所有其他字段都被填充。保留 maybe_periodic
参数为空,priority
参数的默认值为 0
。选择 System pallet 的 set_code
函数作为 call
参数,并像之前一样提供 Wasm 二进制文件。保留 with weight override 选项不激活。一旦填充了所有其他字段,以后使用大约10个区块(1分钟)的区块号来填充 when
参数并快速提交交易。
你可以使用模板节点的命令行输出或 Polkadot JS Apps UI block explorer 来监视这个预定调用的发生。
在目标区块被包含在链中之后,Polkadot JS 应用程序界面左上角的版本号应该反映出运行时的版本现在是 102
。
然后,你可以通过使用 Polkadot JS app UI Chain State 应用程序来观察升级中所做的具体变化,以从 Balances pallet 中查询 existentialDeposit
常量值。
自定义pallet
本教程的重点是如何使用 pallets 自定义运行时,包括如何将简单和复杂的 pallets 添加到运行时,以及如何将 pallets 与智能合约结合使用。
添加一个pallet到运行时
Nicks pallet 允许区块链用户支付押金,为他们控制的帐户保留昵称。它实现了以下功能:
set_name
函数用于收集押金,并为帐户设置名称,如果该名称尚未被使用。clear_name
函数用于删除与帐户关联的名称并退还押金。kill_name
函数用于强制删除帐户名称,但不会退还押金。
添加Nicks pallet的依赖
将 Nicks pallet 的依赖项添加到运行时,打开 runtime/Cargo.toml
文件,复制一个存在的 pallet 依赖项描述,然后把名字替换为 pallet-nicks
,使该 pallet 在 node template 运行时是可用的。例如,添加如下类似的一行:
pallet-nicks = { version = "4.0.0-dev", default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.26" }
- version 标识着你想导入的 crate 版本。
- 使用标准 Rust 库编译运行时的时候,是否包括 pallet 特性的默认行为。
- 用于检索
pallet-nicks
crate 的存储库位置。 - 用于检索 crate 的分支。
这些细节对于任何给定版本的 node tempalte 中的每个 pallet 都应该是相同的。
添加 pallet-nicks/std
特性到 features
列表中,以编译运行时的时候可以启用。
[features]
default = ["std"]
std = [
...
"pallet-aura/std",
"pallet-balances/std",
"pallet-nicks/std",
...
]
如果你忘记更新 Cargo.toml
的 features
部分,在编译运行时二进制文件时,你可能会看到 cannot find function
错误。
通过以下命令检查依赖项是否被正确解析:
cargo check -p node-template-runtime
审查余额的配置
例如,查看你需要实现的 nicks
pallet,你可以参考关于 pallet_nicks::Config
的 Rust 文档或者 Nicks pallet 源码
中的 trait 定义。
在本节教程中,你可以看到 nicks
pallet 中的 Config
trait 声明了以下类型:
``rust,editable,noplayground
pub trait Config: Config {
type Event: From<Event
确定 pallet 所需的类型后,需要向运行时添加代码以实现 `Config` 特性。查看为一个 pallet 怎样实现 `Config` trait,使用 **Balances** pallet 作为一个例子,打开 `runtime/src/lib.rs` 文件,找到 `Balances` pallet 的位置并注意它由以下实现(`impl`)代码块组成:
```rust,editable,noplayground
impl pallet_balances::Config for Runtime {
type MaxLocks = ConstU32<50>;
type MaxReserves = ();
type ReserveIdentifier = [u8; 8];
/// The type for recording an account's balance.
type Balance = Balance;
/// The ubiquitous event type.
type Event = Event;
/// The empty value, (), is used to specify a no-op callback function.
type DustRemoval = ();
/// Set the minimum balanced required for an account to exist on-chain
type ExistentialDeposit = ConstU128<500>;
/// The FRAME runtime system is used to track the accounts that hold balances.
type AccountStore = System;
/// Weight information is supplied to the Balances pallet by the node template runtime.
type WeightInfo = pallet_balances::weights::SubstrateWeight<Runtime>;
}
正如你看到的这个例子,impl pallet_balances::Config
代码块允许你为指定的 Balances pallet Config
trait 配置类型和参数。例如,此 impl
代码块配置 Balances pallet 使用 u128
类型来追述 balances。
为Nicks实现配置
要实现 nicks
pallet 在你的运行时,打开 runtime/src/libs.rs
文件,找到 Balances 代码块的最后一行,为 Nicks pallet 添加下面的代码块:
impl pallet_nicks::Config for Runtime {
// The Balances pallet implements the ReservableCurrency trait.
// `Balances` is defined in `construct_runtime!` macro.
type Currency = Balances;
// Set ReservationFee to a value.
type ReservationFee = ConstU128<100>;
// No action is taken when deposits are forfeited.
type Slashed = ();
// Configure the FRAME System Root origin as the Nick pallet admin.
// https://paritytech.github.io/substrate/master/frame_system/enum.RawOrigin.html#variant.Root
type ForceOrigin = frame_system::EnsureRoot<AccountId>;
// Set MinLength of nick name to a desired value.
type MinLength = ConstU32<8>;
// Set MaxLength of nick name to a desired value.
type MaxLength = ConstU32<32>;
// The ubiquitous event type.
type Event = Event;
}
添加 Nicks 到 construct_runtime!
宏中,如下代码:
construct_runtime!(
pub enum Runtime where
Block = Block,
NodeBlock = opaque::Block,
UncheckedExtrinsic = UncheckedExtrinsic
{
/* --snip-- */
Balances: pallet_balances,
/*** Add This Line ***/
Nicks: pallet_nicks,
}
);
通过运行下面的命令检查新的依赖是否被正确解析:
cargo check -p node-template-runtime
通过运行下面的命令以 release 模式编译 node:
cargo build --release
启动区块链节点
通过运行下面的命令以开发模式启动节点
./target/release/node-template --dev
在上面这种例子中,--dev
选项使用预构建的 development
chain specification 使节点运行在开发模式下。默认情况下,当你通过按下 Control-c 停止节点时,这个选项会删除所有活动数据,如 keys、区块链的数据库、网络信息。使用 --dev
选项确保在任何时候你停止或重启节点时都会有一个干净的网络状态。
通过查看终端中显示的输出,验证节点是否已启动并成功运行。如果块的最终确认后控制台输出中的数字还在增加,则你的区块链正在生成新的区块,并就它们所描述的状态达成共识。
启动front-end template
现在你已经添加一个新的 pallet 到你的运行时,你可以使用 Substrate front-end template 跟 node template 进行交互,并且访问 Nicks pallet。在你已安装的 front-end template 目录中,通过运行下面的命令为 front-end template 启动 web server:
yarn start
在浏览器中打开 http://localhost:8000
去查看 front-end template。
使用Nicks pallet设置昵称
在账户列表选择 Alice 账户,在 Pallet Interactor 组件确认 Extrinsic 是已经被选择的,从可调用的 pallets 列表中选择 nicks
,选择 setname
作为从 nicks
pallet 中调用的函数,输入一个不少于 MinNickLength
(8字符)且不超过 MaxNickLength
(32字符)的昵称。
点击 Signed 执行函数。
观察调用改变的状态,从 Ready 到 InBlock 到 Finalized,并且注意 Nicks pallet 发出的事件。
使用Nicks pallet查询账户的信息
在账户列表选择 Alice 账户,在 Pallet Interactor 组件中选择 Query 作为交互的类型,从可调用的 pallets 列表中选择 nicks
,选择 nameOf
作为要调用的函数,在 AccountId 字段中复制并粘贴 alice
账户的地址,然后点击 Query。
返回的类型是一个包含两个值的元组:
- Alice 账户的16进制编码的昵称
53756273747261746520737570657273746172202d20416c696365
。 - 从 Alice 的账户中预留的用于保存昵称的金额(
100
)。
如果你要查询 Nicks pallet 中 Bob 账户的昵称,你将看到返回一个 None
值,因为 Bob 没有调用 setname
函数去预留一个昵称。
探索其他功能
本节教程演示了如何向运行时添加一个简单的 pallet,并演示了如何使用前端模板与新 pallet 交互,你将 nicks
pallet 添加到运行时,并使用前端模板调用 set_name
和 nameOf
函数。nicks
pallet 还提供了两个附加函数:clear_name
函数和 kill_name
函数,使帐户所有者能够删除或保留名称,或者一个 root-level 用户能够强制删除帐户名称。通过探索这些功能的工作方式,你可以继续了解其他功能,如 Sudo pallet 和 origin accounts 的使用。
配置合约pallet
添加contracts pallet依赖
将 contracts pallet 的依赖项添加到运行时,导入 pallet-contracts
crate,打开 runtime/Cargo.toml
文件,复制一个存在的 pallet 依赖项描述,然后把名字替换为 pallet-contracts
,使该 pallet 在 node template 运行时是可用的。例如,添加如下类似的一行:
pallet-contracts = { version = "4.0.0-dev", default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.26" }
导入 pallet-contracts-primitives
crate,通过将其添加到依赖项列表中,使其可用于 node template 运行时。在大多数情况下,你在任何给定版本的 node template 中为每个托盘指定相同的信息。然而,如果编译器指定的版本与你所指定的版本不同,你可能需要修改依赖项以匹配编译器识别的版本。例如,如果编译器发现 pallet-contracts-primitives
crate 的版本是 6.0.0:
pallet-contracts-primitives = { version = "6.0.0", default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.26" }
将 Contracts pallet 添加到 std
特性列表中,这样当运行时作为平台原生二进制(非 Wasm 二进制)文件构建时,它的特性就包括在内了。
[features]
default = ["std"]
std = [
"codec/std",
"scale-info/std",
"frame-executive/std",
"frame-support/std",
"frame-system-rpc-runtime-api/std",
"frame-system/std",
"pallet-aura/std",
"pallet-balances/std",
"pallet-contracts/std",
"pallet-contracts-primitives/std",
]
保存代码并关闭 runtime/Cargo.toml
文件。
通过运行下面的命令检查你的运行时编译是否正确:
cargo check -p node-template-runtime
实现 Contracts configuration trait
如果你查看 pallet_contracts::Config
Rust API 文档,你将会注意到,该 pallet 不像 Nicks pallet 一样,有很多关联类型,因此本节教程的代码比之前的要更复杂。
为了在运行时给 Contracts pallet 实现 Config
trait,打开 runtime/src/lib.rs
,找到 pub use frame_support
代码块并添加 Nothing
到 traits 列表之中,如下:
pub use frame_support::{
construct_runtime, parameter_types,
traits::{
ConstU128, ConstU32, ConstU64, ConstU8, KeyOwnerProofSystem, Randomness, StorageInfo,Nothing
},
weights::{
constants::{BlockExecutionWeight, ExtrinsicBaseWeight, RocksDbWeight, WEIGHT_PER_SECOND},
IdentityFee, Weight,
},
StorageValue,
};
添加一行从 Contracts pallet 导入默认的 contract 权重,如下:
pub use frame_system::Call as SystemCall;
pub use pallet_balances::Call as BalancesCall;
pub use pallet_timestamp::Call as TimestampCall;
use pallet_transaction_payment::CurrencyAdapter;
use pallet_contracts::DefaultContractAccessWeight; // Add this line
添加 Contracts pallet 所需的常量添加到运行时,如下:
/* After this block */
// Time is measured by number of blocks.
pub const MINUTES: BlockNumber = 60_000 / (MILLISECS_PER_BLOCK as BlockNumber);
pub const HOURS: BlockNumber = MINUTES * 60;
pub const DAYS: BlockNumber = HOURS * 24;
/* Add this block */
// Contracts price units.
pub const MILLICENTS: Balance = 1_000_000_000;
pub const CENTS: Balance = 1_000 * MILLICENTS;
pub const DOLLARS: Balance = 100 * CENTS;
const fn deposit(items: u32, bytes: u32) -> Balance {
items as Balance * 15 * CENTS + (bytes as Balance) * 6 * CENTS
}
const AVERAGE_ON_INITIALIZE_RATIO: Perbill = Perbill::from_percent(10);
/*** End Added Block ***/
在运行时中为 pallet_contracts
添加参数类型和实现 Config
trait,如下
/*** Add a block similar to the following ***/
parameter_types! {
pub const DepositPerItem: Balance = deposit(1, 0);
pub const DepositPerByte: Balance = deposit(0, 1);
pub const DeletionQueueDepth: u32 = 128;
pub DeletionWeightLimit: Weight = AVERAGE_ON_INITIALIZE_RATIO * BlockWeights::get().max_block;
pub Schedule: pallet_contracts::Schedule<Runtime> = Default::default();
}
impl pallet_contracts::Config for Runtime {
type Time = Timestamp;
type Randomness = RandomnessCollectiveFlip;
type Currency = Balances;
type Event = Event;
type Call = Call;
type CallFilter = frame_support::traits::Nothing;
type WeightPrice = pallet_transaction_payment::Pallet<Self>;
type WeightInfo = pallet_contracts::weights::SubstrateWeight<Self>;
type ChainExtension = ();
type Schedule = Schedule;
type CallStack = [pallet_contracts::Frame<Self>; 31];
type DeletionQueueDepth = DeletionQueueDepth;
type DeletionWeightLimit = DeletionWeightLimit;
type DepositPerByte = DepositPerByte;
type DepositPerItem = DepositPerItem;
type AddressGenerator = pallet_contracts::DefaultAddressGenerator;
type ContractAccessWeight = DefaultContractAccessWeight<BlockWeights>;
type MaxCodeLen = ConstU32<{ 256 * 1024 }>;
type RelaxedMaxCodeLen = ConstU32<{ 512 * 1024 }>;
type MaxStorageKeyLen = ConstU32<{ 512 * 1024 }>;
}
/*** End added block ***/
关于 Contracts pallet 配置的更多信息以及类型和参数如何被使用的,可参考 Contracts pallet source code
添加 pallet_contracts
到 construct_runtime!
宏中,如下:
// Create the runtime by composing the FRAME pallets that were previously configured
construct_runtime!(
pub enum Runtime where
Block = Block,
NodeBlock = opaque::Block,
UncheckedExtrinsic = UncheckedExtrinsic
{
System: frame_system,
RandomnessCollectiveFlip: pallet_randomness_collective_flip,
Timestamp: pallet_timestamp,
Aura: pallet_aura,
Grandpa: pallet_grandpa,
Balances: pallet_balances,
TransactionPayment: pallet_transaction_payment,
Sudo: pallet_sudo,
Contracts: pallet_contracts,
}
);
保存代码并关闭 runtime/src/lib.rs
文件。
通过运行下面的命令检查你的运行时编译是否正确:
cargo check -p node-template-runtime
暴露合约的API
一些 pallets,包括 Contracts pallet,暴露了自定义运行时 APIs 和 RPC 端点。你无需在 Contracts pallet 上启用 RPC 调用就可以在链上使用它。然而,暴露 Contracts pallet 的 API 和 endpoints 很有用,因为这样做可以使你执行以下任务:
- 从链下读取合约状态。
- 在不进行交易的情况下调用节点存储。
要暴露合约 RPC API,打开 runtime/Cargo.toml
文件,使用与其他 pallets 相同的版本和分支信息,将 pallet-contracts-rpc-runtime-api
pallet 的相关代码描述添加到 [dependencies]
部分,如下:
pallet-contracts-rpc-runtime-api = { version = "4.0.0-dev", default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.26" }
将 pallet-contracts-rpc-runtime-api
添加到 std
特性列表中,以便在构建运行时为原生二进制时包含其特性。
[features]
default = ["std"]
std = [
"codec/std",
...
"pallet-contracts/std",
"pallet-contracts-primitives/std",
"pallet-contracts-rpc-runtime-api/std",
...
]
保存代码并关闭 runtime/Cargo.toml
文件。
打开 runtime/src/lib.rs
文件,并通过添加以下常量启用合约调试:
const CONTRACTS_DEBUG_OUTPUT: bool = true;
在 impl_runtime_apis!
宏中,接近运行时 lib.rs
文件末尾的地方,实现合约运行时 API,如在 impl_runtime_apis! { }
的部分:
/*** Add this block ***/
impl pallet_contracts_rpc_runtime_api::ContractsApi<Block, AccountId, Balance, BlockNumber, Hash>
for Runtime
{
fn call(
origin: AccountId,
dest: AccountId,
value: Balance,
gas_limit: u64,
storage_deposit_limit: Option<Balance>,
input_data: Vec<u8>,
) -> pallet_contracts_primitives::ContractExecResult<Balance> {
Contracts::bare_call(origin, dest, value, gas_limit, storage_deposit_limit, input_data, CONTRACTS_DEBUG_OUTPUT)
}
fn instantiate(
origin: AccountId,
value: Balance,
gas_limit: u64,
storage_deposit_limit: Option<Balance>,
code: pallet_contracts_primitives::Code<Hash>,
data: Vec<u8>,
salt: Vec<u8>,
) -> pallet_contracts_primitives::ContractInstantiateResult<AccountId, Balance> {
Contracts::bare_instantiate(origin, value, gas_limit, storage_deposit_limit, code, data, salt, CONTRACTS_DEBUG_OUTPUT)
}
fn upload_code(
origin: AccountId,
code: Vec<u8>,
storage_deposit_limit: Option<Balance>,
) -> pallet_contracts_primitives::CodeUploadResult<Hash, Balance> {
Contracts::bare_upload_code(origin, code, storage_deposit_limit)
}
fn get_storage(
address: AccountId,
key: Vec<u8>,
) -> pallet_contracts_primitives::GetStorageResult {
Contracts::get_storage(address, key)
}
}
保存代码并关闭 runtime/src/lib.rs
文件。
通过运行下面的命令检查你的运行时编译是否正确:
cargo check -p node-template-runtime
更新外部节点
要与 Contracts pallet 交互,必须扩展现有的 RPC server 以包括 Contracts pallet 和 Contracts RPC API。为了使 Contracts pallet 能够利用RPC endpoint API,你需要将自定义 RPC endpoint 添加到外部节点配置中。
将 RPC API 扩展添加到外部节点,打开 node/Cargo.toml
并且添加 pallet-contracts
和 pallet-contracts-rpc
到 [dependencies]
部分,如下:
pallet-contracts = { version = "4.0.0-dev", git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.26" }
pallet-contracts-rpc = { version = "4.0.0-dev", git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.26" }
由于你已经暴露了运行时API,并且现在正在为外部节点编写代码,不需要使用 no_std
配置,所以你不必维护专用的 std
特性列表。
保存代码并关闭 node/Cargo.toml
文件。
打开 node/src/rpc.rs
文件,并找到下面这一行。
use node_template_runtime::{opaque::Block, AccountId, Balance, Index};
将 use pallet_contracts_rpc
添加到文件中,如下:
use pallet_transaction_payment_rpc::{TransactionPayment, TransactionPaymentApiServer};
use substrate_frame_rpc_system::{System, SystemApiServer};
use pallet_contracts_rpc::{Contracts, ContractsApiServer}; // Add this line
为 RPC 扩展添加 Contracts RPC pallet 到 create_full
函数中,如下:
/// Instantiate all full RPC extensions.
pub fn create_full<C, P>(
deps: FullDeps<C, P>,
) -> Result<RpcModule<()>, Box<dyn std::error::Error + Send + Sync>>
where
C: ProvideRuntimeApi<Block>,
C: HeaderBackend<Block> + HeaderMetadata<Block, Error = BlockChainError> + 'static,
C: Send + Sync + 'static,
C::Api: substrate_frame_rpc_system::AccountNonceApi<Block, AccountId, Index>,
C::Api: pallet_transaction_payment_rpc::TransactionPaymentRuntimeApi<Block, Balance>,
C::Api: pallet_contracts_rpc::ContractsRuntimeApi<Block, AccountId, Balance, BlockNumber, Hash>, // Add this line
C::Api: BlockBuilder<Block>,
P: TransactionPool + 'static,
添加 Contracts RPC API 的扩展。
module.merge(System::new(client.clone(), pool.clone(), deny_unsafe).into_rpc())?;
module.merge(TransactionPayment::new(client.clone()).into_rpc())?;
module.merge(Contracts::new(client.clone()).into_rpc())?; // Add this line
保存代码并关闭 node/src/rpc.rs
文件。
通过运行下面的命令检查你的运行时编译是否正确:
cargo check -p node-template
如果检查没有错误,你可以通过运行下面的命令在 release 模式下编译节点:
cargo build --release
启动本地Substrate节点
节点编译完成之后,你可以从 Contracts pallet 增强功能的智能合约启动 Substrate 节点,并且使用 front-end template 与之进行交互。
在开发模式下启动本地节点,运行以下命令:
./target/release/node-template --dev
通过查看终端中显示的输出,验证节点是否已启动并成功运行。如果在 finalized
后数字不断在增加,你的区块链正在生成新的块,并且就他们描述的状态达成新的共识,说明是成功运行了。
通过运行下面的命令启动 front-end template 的 web 服务:
yarn start
在浏览器中打开 http://localhost:8000/
去访问 front-end template。
在 Pallet Interactor 组件中验证 Extrinsic
是被选中的,然后从可供调用的 pallets 列表中选择 contracts
。
在自定义pallet中使用宏
指定调用的来源
开发智能合约
本教程指导你如何使用 ink! 编程语言 用于构建在基于 Substrate 的区块链上运行的智能合约。本节中的教程使用预配置的 contracts-node
和托管 Contracts UI。如果要使用标准 node template,需要将 Contracts pallet 和一些其他组件添加到开发环境中。在配置合约pallet中详细介绍了为构建智能合约准备 node template 的过程。
准备第一份合约
更新你的Rust环境
对于本节教程,你需要添加一些Rust源代码到你的Substrate开发环境之中,通过运行下面的命令更新你的Rust环境:
rustup component add rust-src --toolchain nightly
通过运行下面的命令验证你已经安装了WebAssembly目标:
rustup target add wasm32-unknown-unknown --toolchain nightly
如果目标已安装且是最新的,则该命令将显示类似于以下内容的输出:
info: component 'rust-std' for target 'wasm32-unknown-unknown' is up to date
安装Substrate合约node
为了简化本节教程,你可以为 Linux 和 macOS 下载预编译的 Substrate 节点,预编译的二进制文件默认情况下为智能合约包括了FRAME pallet。或者,你可以通过在你的本地计算机上运行 cargo install contracts-node
手动构建预配置的 contracts-node
。
在 macOS 或 Linux 上安装对应 Releases 版本的合约节点,为本地计算机下载合适的压缩文件。
如果无法下载预编译节点,则可以使用类似于以下的命令在本地编译它:
cargo install contracts-node --git https://github.com/paritytech/substrate-contracts-node.git --tag <latest-tag> --force --locked
你可以在 Tags 页面上发现最新的 tag。
安装其它软件包
编译 contracts-node
包后,你需要安装另外两个软件包:
- 用于你的操作系统的 WebAssembly binaryen 包,为合约优化 WebAssembly 字节码。
- 用于配置智能合约项目的
cargo-contract
命令行接口。
安装WebAssembly优化器
在 Ubuntu 或 Debian 上运行下面的命令:
sudo apt install binaryen
在 macOS 上运行下面的命令:
brew install binaryen
对于其它的操作系统,你可以直接从 WebAssembly releases 上下载 binaryen
release。
安装cargo-contract包
在你安装 WebAssembly binaryen
包之后,你可以安装 cargo-contract
包。cargo-contract
包提供一个命令行接口,用于使用 ink!
语言处理智能合约。
安装 dylint-link
,需要它来 lint ink! contracts,注意你使用 API 的方式可能存在会导致安全的问题:
cargo install dylint-link
通过运行下面的命令安装 cargo-contract
:
cargo install cargo-contract --force
通过运行以下命令验证安装并尝试命令的可用选项:
cargo contract --help
创建一个新的智能合约项目
你现在可以开始开发新的智能合约项目了,为智能合约项目生成文件。通过运行下面的命令创建一个名叫 flipper
的新项目文件夹。
cargo contract new flipper
切换到新项目文件夹中并列出文件夹中所有的内容:
cd flipper/ && ls -al
你应该可以看到文件夹中包含下面的文件:
-rwxr-xr-x 1 dev-doc staff 285 Mar 4 14:49 .gitignore
-rwxr-xr-x 1 dev-doc staff 1023 Mar 4 14:49 Cargo.toml
-rwxr-xr-x 1 dev-doc staff 2262 Mar 4 14:49 lib.rs
像其它 Rust 项目一样,Cargo.tmol
文件被用作提供包的依赖关系和配置信息。lib.rs
文件被用作为智能合约编写业务逻辑。
浏览默认项目文件
默认情况下,创建一个新的智能合约项目生成一些模板源码。一个非常简单的合约,第一个函数 flip()
,会将布尔变量从 true 更改为 false;还有第二个函数 get()
,会获取当前布尔变量的值。lib.rs
文件还会包含两个为测试合约是否能按照期望工作的函数。
随着本节教程到最后,你会拥有一个更加高级完整的智能合约,像 Flipper example 一样。
打开 Cargo.toml
文件并为查看合约的依赖关系,如有必要,在 [dependencies]
部分,修改 scale
和 scale-info
的配置:
scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] }
scale-info = { version = "2", default-features = false, features = ["derive"], optional = true }
保存对 Cargo.tmol
的任何变更,并关闭该文件。
打开 lib.rs
文件,并查看为合约定义的函数。
测试默认的合约
在 lib.rs
原代码文件的底部,有一些简单的测试用例可以验证合约的功能性。你可以使用链下测试环境来测试此代码是否按照预期执行。
通过运行下面的命令,使用 test
子命令和 nightly
工具链来为 flipper
合约执行默认的测试用例:
cargo +nightly test
该命令应显示类似于以下的输出,以表示成功完成测试用例:
running 2 tests
test flipper::tests::it_works ... ok
test flipper::tests::default_works ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
构建合约
测试完默认的合约之后,你就可以开始编译这个项目到 WebAssembly 了。
为智能合约构建 WebAssembly,通过运行下面的命令,编译 flipper
智能合约:
cargo +nightly contract build
这个命令会为 flipper
项目构建一个 WebAssembly 二进制文件,一个元数据文件可以包含合约应用二进制接口(ABI),和一个你用来部署合约的 .contract
文件。例如,你应该可以看到类似于下面的输出:
Original wasm size: 47.9K, Optimized: 22.8K
The contract was built in DEBUG mode.
Your contract artifacts are ready. You can find them in:
/Users/dev-doc/flipper/target/ink
- flipper.contract (code + metadata)
- flipper.wasm (the contract's code)
- metadata.json (the contract's metadata)
The `.contract` file can be used for deploying your contract to your chain.
在 target/ink
目录中的 metadata.json
文件描述了你可以用来与此合约交互的所有接口。这个文件包含了几个重要的部分:
spec
部分包含有关函数的信息,像可被调用的 constructors 和 messages,可被发出的 events,以及可现实的任何文档。这个部分也包括了一个selector
字段,它包含一个函数名称的4字哈希,和用于将合约调用到正确的函数。storage
部分定义了由合约管理的所有存储项以及如何访问它们。types
部分提供 JSON 其余部分中使用的自定义数据类型。
启动Substrate智能合约节点
通过运行下面的命令在本地开发模式中启动预配置的 contracts-node
:
substrate-contracts-node --dev
你应该在终端中看到类似于以下内容的输出:
2022-03-07 14:46:25 Substrate Contracts Node
2022-03-07 14:46:25 ✌️ version 0.8.0-382b446-x86_64-macos
2022-03-07 14:46:25 ❤️ by Parity Technologies <admin@parity.io>, 2021-2022
2022-03-07 14:46:25 📋 Chain specification: Development
2022-03-07 14:46:25 🏷 Node name: possible-plants-8517
2022-03-07 14:46:25 👤 Role: AUTHORITY
2022-03-07 14:46:25 💾 Database: RocksDb at /var/folders/2_/g86ns85j5l7fdnl621ptzn500000gn/T/substrateEdrJW9/chains/dev/db/full
2022-03-07 14:46:25 ⛓ Native runtime: substrate-contracts-node-100 (substrate-contracts-node-1.tx1.au1)
2022-03-07 14:46:25 🔨 Initializing Genesis block/state (state: 0xe9f1…4b89, header-hash: 0xa1b6…0194)
2022-03-07 14:46:25 👴 Loading GRANDPA authority set from genesis on what appears to be first startup.
2022-03-07 14:46:26 🏷 Local node identity is: 12D3KooWQ3P8BH7Z1C1ZoNSXhdGPCiPR7irRSeQCQMFg5k3W9uVd
2022-03-07 14:46:26 📦 Highest known block at #0
在几秒钟后,你还会看到区块被最终确认的状态。为了与区块链交互,你需要连接本节点。你可以通过打开 Contracts UI 浏览器页面连接当前节点。然后点击 Yes allow this application access,选择本地节点。
部署合约
在 Substrate 中,合约部署过程分为两个步骤:
- 将合约代码上传到区块链。
- 创建合约的一个实例。
通过这种模式,你可以将智能合约(如 ERC20标准)的代码存储在区块链上一次,然后将其实例化任意次数。你不需要重复加载相同的源代码,因此你的智能合约不会消耗区块链上不必要的资源。
上传合约代码
在本节教程中,你将使用 Contracts UI 前端在 Substrate 链上部署 flipper
合约。
在 web 浏览器中打开 Contracts UI,确认你已经连接到了 Local Node,点击 Add New Contract,点击 Upload New Contract Code,选择用于创建合约实例的 Account,你可以选择任何已存在的账户,包括一个预定义的账户,如 alice。输入一个智能合约的的名称,例如,Flipper Contract。
浏览并选择或拖放 flipper.contract
文件,包含绑定的 Wasm blob 和元数据文件到 upload section。
然后点击 Next 继续。
在区块链上创建一个实例
智能合约作为 Substrate 区块链上账户系统的扩展而存在。当你创建此智能合约的实例时,Substrate 将创建一个新的 AccountId
,以存储智能合约管理的任何余额,并允许你与该合约进行交互。
在你上传智能合约并点击 Next 之后,Contracts UI 会显示有关智能合约内容的信息。
查看并接受智能合约初始版本的默认 Deployment Constructor
选项,查看并接受默认的 Max Gas Allowed 200000
。
点击 Next,交易现在已开始进入队列,如果需要进行更改,可以点击 Go Back 修改输入。
点击 Upload and Instantiate,根据你使用的帐户,可能会提示你输入帐户密码。如果你使用了预定义的帐户,则不需要提供密码。
调用智能合约
现在,你的合约已经部署在区块链上了,你可以与之交互。默认的 flipper
智能合约有两个函数 flip()
和 get()
,你可以使用 Contracts UI 来尝试使用它们。
get()函数
当你实例化合约时,将 flipper
合约 value
的初始值设置为 false
。你可以使用 get()
函数去验证当前值是否为 false
。
为了测试 get()
函数,从 Account 列表中选择任意账户,此合约对允许谁发送这个 get()
请求没有限制,从 Message to Send 列表中选择 get(): bool,点击 Read,验证调用结果的返回值是否为 false
。
flip()函数
flip()
函数把值从 false
改为 true
。
为了测试 flip()
函数,从 Account 列表中选择任意预定义账户,flip()
函数是一个改变链状态的交易,需要一个有资金的帐户来执行调用。因此,你应该选择具有预定义余额的帐户,例如 alice
帐户。从 Message to Send 列表中选择 flip(),点击 Call,验证调用结果中的交易是否成功。
从 Message to Send 列表中选择 get(): bool,点击 Read,验证调用结果中的新值是 true
。
开发一份智能合约
在本节教程中,你将开发一个新的智能合约,它将在每次执行函数调用时增加计数器值。
智能合约与ink!
在上一节教程(准备第一份智能合约)之后,你已经位命令行访问 ink! 编程语言安装了 cargo-contract
包。ink! 语言是嵌入式领域特定语言。此语言使你能够使用 Rust 编程语言编写基于 WebAssembly 的智能合约。
该语言使用标准的 Rust 模式和专用的 #[ink(...)]
属性宏。这些属性宏描述了智能合约的不同部分,这样它们就可以被转换为与 Substrate 兼容的 WebAssembly 字节码。
创建一份新的智能合约项目
本节教程中,你将会为 incrementer
智能合约创建一个新项目。创建的新项目会添加一个项目文件夹,在项目文件夹中有一个默认的启动文件,也可以叫做模板文件。你将会为 incrementer
项目修改这些启动模板文件,来构建智能合约所需的逻辑。
通过运行下面的命令创建一个名为 incrementer
的新项目:
cargo contract new incrementer
进入 incrementer 文件夹后,打开 lib.rs
文件,默认情况下,lib.rs
模板文件包含 flipper
智能合约的源代码,只是把 flipper
合约的实例重命名为 incrementer
。把默认模板的源代码替换为新的 incrementer 源码。保存 lib.rs
文件的改变并关闭。
打开 Cargo.toml
文件,并为合约检查相关的依赖包。在 [dependencies]
部分,如果需要的话,修改 scale
和 scale-info
配置内容。
scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] }
scale-info = { version = "2", default-features = false, features = ["derive"], optional = true }
保存代码并关闭 Cargo.toml
文件。
通过运行下面的命令,验证程序是否编译并通过了简单的测试:
cargo +nightly test
你可以忽略任何警告,因为此模板只是框架代码,该命令应显示类似以下的输出,以表示成功完成测试:
running 1 test
test incrementer::tests::default_works ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
通过运行以下命令,验证你可以为合约构建 WebAssembly。
cargo +nightly contract build
如果程序成功编译,你就可以开始编码了。
存储简单的值
现在已经有了一些 incrementer
智能合约的初始源代码,你可以再引入一些新的功能。例如,该合约需要存储简单的值。下面的代码解释了如何使用 #[ink(storage)]
属性宏为该合约存储简单的值。
#[ink(storage)]
pub struct MyContract {
// Store a bool
my_bool: bool,
// Store a number
my_number: u32,
}
支持的类型
Substrate 智能合约支持最常见的 Rust 数据类型,包括布尔类型,未签名和已签名的整数型,字符串,元组,以及数组。这些数据类型使用 Parity scale codec 进行编码和解码,以便可以在网络上高效的传输。
除了常见的 Rust 类型使用 scale codec 进行编码解码外,ink! 语言支持 Substrate 特性类型,像 AccountId
,Balance
,和 Hash
,就好像它们是原始类型。下面的代码说明了如何为该合约存储一个 AccountId
和 Balance
。
// We are importing the default ink! types
use ink_lang as ink;
#[ink::contract]
mod MyContract {
// Our struct will use those default ink! types
#[ink(storage)]
pub struct MyContract {
// Store some AccountId
my_account: AccountId,
// Store some Balance
my_balance: Balance,
}
/* --snip-- */
}
构造函数
每一个 ink! 智能合约必须至少有一个在创建合约时运行的构造函数。然而,如果你需要,一个智能合约也可以有多个多个构造函数。下面的代码说明了如何使用多个构造函数:
use ink_lang as ink;
#[ink::contract]
mod mycontract {
#[ink(storage)]
pub struct MyContract {
number: u32,
}
impl MyContract {
/// Constructor that initializes the `u32` value to the given `init_value`.
#[ink(constructor)]
pub fn new(init_value: u32) -> Self {
Self {
number: init_value,
}
}
/// Constructor that initializes the `u32` value to the `u32` default.
///
/// Constructors can delegate to other constructors.
#[ink(constructor)]
pub fn default() -> Self {
Self {
number: Default::default(),
}
}
/* --snip-- */
}
}
更新你的智能合约
现在你已经了解到如何存储简单的值,声明数据类型,以及使用构造函数,你可以更新你的智能合约源代码以实现下面的功能:
- 创建一个名为
value
的存储值,数据类型为i32
。 - 创建一个新的
Incrementer
构造函数,并且将它的value
设置为init_value
。 - 创建第二个名叫
default
的构造函数,该函数没有输入参数,并创建一个新的Incrementer
,将其value
设置为0
。
为了更新智能合约,打开 lib.rs
文件,通过声明名为 value
数据类型为 i32
的存储项,从而替换 Storage Declaration
注释。
#[ink(storage)]
pub struct Incrementer {
value: i32,
}
修改 Incrementer
构造函数,将其 value
设置为 init_value
。
impl Incrementer {
#[ink(constructor)]
pub fn new(init_value: i32) -> Self {
Self {
value: init_value,
}
}
}
添加第二个名为 default
的构造函数,该函数创建一个新的 Incrementer
,将其 value
设置为 0
。
#[ink(constructor)]
pub fn default() -> Self {
Self {
value: 0,
}
}
保存代码并关闭文件。
通过运行下面的命令使用 test
子命令和 nightly
工具链来测试你的工作。
cargo +nightly test
该命令应该显示类似如下的输出,以表明测试成功完成:
running 1 test
test incrementer::tests::default_works ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
添加一个函数来获取一个存储值
现在你已经创建并初始化了存储值,可以使用公共和私有函数与它进行交互。在本节教程中,你将添加一个公共函数来获取存储值。请注意,所有公共函数都必须使用 #[ink(message)]
属性宏。
在智能合约中添加公共函数,打开 lib.rs
文件,更新 get
公共函数,以返回数据类型为 i32
的存储项的 value
。
#[ink(message)]
pub fn get(&self) -> i32 {
self.value
}
因为这个函数只从合约存储中读取数据,所以它使用 &self
参数来访问合约函数和存储项,此函数不允许更改存储项 value
的状态。如果函数中的最后一个表达式没有分号(;),Rust 将则将其视为返回值。
将私有 default_works
函数中的 Test Your Contract
注释替换为测试 get
函数的代码。
fn default_works() {
let contract = Incrementer::default();
assert_eq!(contract.get(), 0);
}
保存代码并关闭文件。
通过运行下面的命令使用 test
子命令和 nightly
工具链来测试你的工作。
cargo +nightly test
添加一个函数以修改一个存储值
此时,智能合约不允许用户修改存储项,要使用户能够修改存储项,必须将 value
显式标记为可变变量。
添加一个函数以增加存储项的值,打开 lib.rs
文件,添加一个新的 inc
公共函数以增加 value
存储,使用数据类型是 i32
的 by
参数
#[ink(message)]
pub fn inc(&mut self, by: i32) {
self.value += by;
}
向源代码中添加一个新测试,以验证此函数。
#[ink::test]
fn it_works() {
let mut contract = Incrementer::new(42);
assert_eq!(contract.get(), 42);
contract.inc(5);
assert_eq!(contract.get(), 47);
contract.inc(-50);
assert_eq!(contract.get(), -3);
}
保存代码并关闭文件。
通过运行下面的命令使用 test
子命令和 nightly
工具链来测试你的工作。
cargo +nightly test
该命令应该显示类似如下的输出,以表明测试成功完成:
running 2 tests
test incrementer::tests::it_works ... ok
test incrementer::tests::default_works ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
为合约构建WebAssembly
在测试 incrementer
合约之后,就可以将此项目编译为 WebAssembly 了。将智能合约编译到 WebAssembly 后,你可以使用 Contracts UI 在本地合约节点上部署和测试智能合约。
为智能合约构建WebAssembly,在 incrementer
项目文件夹里,通过运行下面的命令编译 incrementer
智能合约:
cargo +nightly contract build
该命令显示如下类似的输出:
Your contract artifacts are ready. You can find them in:
/Users/dev-docs/incrementer/target/ink
- incrementer.contract (code + metadata)
- incrementer.wasm (the contract's code)
- metadata.json (the contract's metadata)
部署与测试智能合约
如果你已经在本地安装了 substrate-contracts-node
节点,你可以为智能合约启动一个本地区块链节点,然后使用 Contracts UI 去部署和测试智能合约。
通过运行以下命令,在本地开发模式下启动合约节点:
substrate-contracts-node --dev
打开 Contracts UI 并验证其是否连接到本地节点。
点击 Add New Contract。
点击 Upload New Contract Code。
选择 incrementer.contract
文件,然后点击 Next。
点击 Upload and Instantiate。
使用 Contracts UI 探索智能合约并与之交互。
完整的合约代码:
#![cfg_attr(not(feature = "std"), no_std)]
use ink_lang as ink;
#[ink::contract]
mod incrementer {
#[ink(storage)]
pub struct Incrementer {
value: i32,
}
impl Incrementer {
#[ink(constructor)]
pub fn new(init_value: i32) -> Self {
Self {
value: init_value,
}
}
#[ink(constructor)]
pub fn default() -> Self {
Self {
value: Default::default(),
}
}
#[ink(message)]
pub fn get(&self) -> i32 {
self.value
}
#[ink(message)]
pub fn inc(&mut self, by: i32) {
self.value += by;
}
}
#[cfg(test)]
mod tests {
use super::*;
use ink_lang as ink;
#[ink::test]
fn default_works() {
let contract = Incrementer::default();
assert_eq!(contract.get(), 0);
}
#[ink::test]
fn it_works() {
let mut contract = Incrementer::new(42);
assert_eq!(contract.get(), 42);
contract.inc(5);
assert_eq!(contract.get(), 47);
contract.inc(-50);
assert_eq!(contract.get(), -3);
}
}
}
使用maps存储值
本节教程说明了如何可以扩展智能合约的功能,以管理每个用户的一个号码。要添加这个功能,你将会使用 Mapping
类型。
ink! 语言提供 Mapping
类型,使你能够将数据存储为键值对。例如,下面的代码说明了映射一个用户到一个号码。
#[ink(storage)]
pub struct MyContract {
// Store a mapping from AccountIds to a u32
my_number_map: ink_storage::Mapping<AccountId, u32>,
}
使用 Mapping
数据类型,可以为每个键存储值的唯一实例。本节教程中,每一个 AccountId
代表一个键,该键映射到一个且仅一个存储的数字 my_value
。每个用户只能存储、增加和检索与其自己的 AccountId
关联的值。
初始化一个mapping
第一步是初始化 AccountId
和存储值之间的映射。在你的合约中使用映射之前,你必须始终初始化映射,以避免映射错误和不一致。要初始化映射,需要执行以下操作:
- 在存储结构上添加
SpreadAllocate
- 指定映射键和映射到它的值。
- 调用
ink_lang::utils::initalize_contract
函数来初始化合约的映射。
以下示例说明如何初始化一个映射并检索一个值:
#![cfg_attr(not(feature = "std"), no_std)]
use ink_lang as ink;
#[ink::contract]
mod mycontract {
use ink_storage::traits::SpreadAllocate;
#[ink(storage)]
#[derive(SpreadAllocate)]
pub struct MyContract {
// Store a mapping from AccountIds to a u32
map: ink_storage::Mapping<AccountId, u32>,
}
impl MyContract {
#[ink(constructor)]
pub fn new(count: u32) -> Self {
// This call is required to correctly initialize the
// Mapping of the contract.
ink_lang::utils::initialize_contract(|contract: &mut Self| {
let caller = Self::env().caller();
contract.map.insert(&caller, &count);
})
}
#[ink(constructor)]
pub fn default() -> Self {
ink_lang::utils::initialize_contract(|_| {})
}
// Get the number associated with the caller's AccountId, if it exists
#[ink(message)]
pub fn get(&self) -> u32 {
let caller = Self::env().caller();
self.map.get(&caller).unwrap_or_default()
}
}
}
识别合约的调用者
在前面的例子中,你可能注意到了 self.env().caller()
函数调用。这个函数在整个合约逻辑中都是可用的,并且总是返回 contract caller。重要的是要注意,contract caller 与 origin caller 是不同的。如果用户访问一个合约,然后再调用后来的合约,那么第二个合约中的 self.env().caller()
是第一个合约的地址,而不是 original user 的地址。
使用合约的调用者
在许多情况下,合约调用者都是有用的。例如,你可以使用 self.env().caller()
来创建一个访问控制层,该层只允许用户访问自己的值。你还可以使用 self.env().caller()
在合同部署期间保存合约所有者。例如:
#![cfg_attr(not(feature = "std"), no_std)]
use ink_lang as ink;
#[ink::contract]
mod mycontract {
#[ink(storage)]
pub struct MyContract {
// Store a contract owner
owner: AccountId,
}
impl MyContract {
#[ink(constructor)]
pub fn new() -> Self {
Self {
owner: Self::env().caller();
}
}
/* --snip-- */
}
}
因为你已经使用 owner
标识符保存了合约调用者,所以你可以再后面编写函数来检查当前合约调用者是否是合同的所有者。
添加mapping到智能合约中
为 incrementer
合约添加 storage map,在 incrementer
项目文件夹中,打开 lib.rs
,为你的合约导入 SpreadAllocate
特征并追加派生宏。
#[ink::contract]
mod incrementer {
use ink_storage::traits::SpreadAllocate;
#[ink(storage)]
#[derive(SpreadAllocate)]
将映射键从 AccountId
添加到存储为 my_value
的 i32
数据类型。
pub struct Incrementer {
value: i32,
my_value: ink_storage::Mapping<AccountId, i32>,
}
使用 initialize_contract
函数为合约中的 new
函数设置一个初始 value
和 my_value
。
#[ink(constructor)]
pub fn new(init_value: i32) -> Self {
ink_lang::utils::initialize_contract(|contract: &mut Self| {
contract.value = init_value;
let caller = Self::env().caller();
contract.my_value.insert(&caller, &0);
})
}
使用 initialize_contract
函数为合约中的 default
函数设置一个初始 value
。
#[ink(constructor)]
pub fn default() -> Self {
ink_lang::utils::initialize_contract(|contract: &mut Self| {
contract.value = Default::default();
})
}
添加一个 get_mine
函数,使用 Mapping API get
函数读取 my_value
,并为合约调用者返回 my_value
。
#[ink(message)]
pub fn get_mine(&self) -> i32 {
self.my_value.get(&self.env().caller()).unwrap_or_default()
}
向初始化帐户添加一个新测试。
#[ink::test]
fn my_value_works() {
let contract = Incrementer::new(11);
assert_eq!(contract.get(), 11);
assert_eq!(contract.get_mine(), 0);
}
保存代码并关闭文件。
通过运行下面的命令使用 test
子命令和 nightly
工具链来测试你的工作。
cargo +nightly test
该命令应该显示类似如下的输出,以表明测试成功完成:
running 3 tests
test incrementer::tests::default_works ... ok
test incrementer::tests::it_works ... ok
test incrementer::tests::my_value_works ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
插入,更新或删除值
ink_storage
映射提供对存储项的直接访问。例如,你可以用一个已存在的键调用 Mapping::insert()
来替换存储项之前保存的值。你也可以通过使用 Mapping::insert()
在存储中第一次读取值时,然后用 Mapping::insert()
更新这个值。如果不存在给定键的值,则 Mapping::get()
返回 None
。因为 Mapping API 提供了对存储的直接访问,所以可以使用 Mapping::remove()
方法从存储中删除给定键的值。
要添加插入和删除函数到合约中,在 incrementer
项目文件夹中,打开 lib.rs
文件,添加一个 inc_mine()
函数,它允许合约调用者获取 my_value
存储项,并且在映射中插入递增的 value
。
#[ink(message)]
pub fn inc_mine(&mut self, by: i32) {
let caller = self.env().caller();
let my_value = self.get_mine();
self.my_value.insert(caller, &(my_value + by));
}
添加一个 remove_mine()
函数,该函数允许合约调用者从存储中获取可删除的 my_value
存储项。
#[ink(message)]
pub fn remove_mine(&mut self) {
self.my_value.remove(&self.env().caller())
}
添加一个新测试,以验证 inc_mine()
函数是否按预期工作。
#[ink::test]
fn inc_mine_works() {
let mut contract = Incrementer::new(11);
assert_eq!(contract.get_mine(), 0);
contract.inc_mine(5);
assert_eq!(contract.get_mine(), 5);
contract.inc_mine(5);
assert_eq!(contract.get_mine(), 10);
}
通过运行下面的命令使用 test
子命令和 nightly
工具链来测试你的工作。
cargo +nightly test
该命令应该显示类似如下的输出,以表明测试成功完成:
running 5 tests
test incrementer::tests::default_works ... ok
test incrementer::tests::it_works ... ok
test incrementer::tests::remove_mine_works ... ok
test incrementer::tests::inc_mine_works ... ok
test incrementer::tests::my_value_works ... ok
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
完整的代码如下:
#![cfg_attr(not(feature = "std"), no_std)]
use ink_lang as ink;
#[ink::contract]
mod incrementer {
use ink_storage::traits::SpreadAllocate;
#[ink(storage)]
#[derive(SpreadAllocate)]
pub struct Incrementer {
value: i32,
my_value: ink_storage::Mapping<AccountId, i32>,
}
impl Incrementer {
#[ink(constructor)]
pub fn new(init_value: i32) -> Self {
// This call is required in order to correctly initialize the
// `Mapping`s of our contract.
ink_lang::utils::initialize_contract(|contract: &mut Self| {
contract.value = init_value;
let caller = Self::env().caller();
contract.my_value.insert(&caller, &0);
})
}
#[ink(constructor)]
pub fn default() -> Self {
// Even though we're not explicitly initializing the `Mapping`,
// we still need to call this
ink_lang::utils::initialize_contract(|contract: &mut Self| {
contract.value = Default::default();
})
}
#[ink(message)]
pub fn get(&self) -> i32 {
self.value
}
#[ink(message)]
pub fn inc(&mut self, by: i32) {
self.value += by;
}
#[ink(message)]
pub fn get_mine(&self) -> i32 {
self.my_value.get(&self.env().caller()).unwrap_or_default()
}
#[ink(message)]
pub fn inc_mine(&mut self, by: i32) {
let caller = self.env().caller();
let my_value = self.get_mine();
self.my_value.insert(caller, &(my_value + by));
}
#[ink(message)]
pub fn remove_mine(&self) {
self.my_value.remove(&self.env().caller())
}
}
#[cfg(test)]
mod tests {
use super::*;
use ink_lang as ink;
#[ink::test]
fn default_works() {
let contract = Incrementer::default();
assert_eq!(contract.get(), 0);
}
#[ink::test]
fn it_works() {
let mut contract = Incrementer::new(42);
assert_eq!(contract.get(), 42);
contract.inc(5);
assert_eq!(contract.get(), 47);
contract.inc(-50);
assert_eq!(contract.get(), -3);
}
#[ink::test]
fn my_value_works() {
let mut contract = Incrementer::new(11);
assert_eq!(contract.get(), 11);
assert_eq!(contract.get_mine(), 0);
contract.inc_mine(5);
assert_eq!(contract.get_mine(), 5);
contract.inc_mine(10);
assert_eq!(contract.get_mine(), 15);
}
#[ink::test]
fn inc_mine_works() {
let mut contract = Incrementer::new(11);
assert_eq!(contract.get_mine(), 0);
contract.inc_mine(5);
assert_eq!(contract.get_mine(), 5);
contract.inc_mine(5);
assert_eq!(contract.get_mine(), 10);
}
#[ink::test]
fn remove_mine_works() {
let mut contract = Incrementer::new(11);
assert_eq!(contract.get_mine(), 0);
contract.inc_mine(5);
assert_eq!(contract.get_mine(), 5);
contract.remove_mine();
assert_eq!(contract.get_mine(), 0);
}
}
}
构建一个token合约
本节教程演示了如何使用 ink! 语言构建 ERC-20 代币合约,ERC-20 规范定义了可替换代币的通用标准。拥有定义代币属性的标准,使遵循规范的开发人员,能够构建与其他产品和服务互操作的应用程序。ERC-20 代币标准不是唯一的代币标准,但它是最常用的标准之一。
ERC-20标准的基础知识
ERC-20 代币标准定义了运行在以太坊区块链上的大多数智能合约的接口。这些标准接口允许个人在现有的智能合约平台上部署自己的加密货币。
如果你查看该标准,你会发现有如下被定义的核心函数:
// ----------------------------------------------------------------------------
// ERC Token Standard #20 Interface
// https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md
// ----------------------------------------------------------------------------
contract ERC20Interface {
// Storage Getters
function totalSupply() public view returns (uint);
function balanceOf(address tokenOwner) public view returns (uint balance);
function allowance(address tokenOwner, address spender) public view returns (uint remaining);
// Public Functions
function transfer(address to, uint tokens) public returns (bool success);
function approve(address spender, uint tokens) public returns (bool success);
function transferFrom(address from, address to, uint tokens) public returns (bool success);
// Contract Events
event Transfer(address indexed from, address indexed to, uint tokens);
event Approval(address indexed tokenOwner, address indexed spender, uint tokens);
}
用户余额映射到帐户地址,接口允许用户转移他们拥有的代币或允许第三方代表他们转移代币。最重要的是,必须实现智能合约逻辑,以确保资金不会被无意创建或销毁,并保护用户的资金免受恶意参与者的攻击。请注意,所有公共函数都返回一个 bool
值,该 bool
值仅指示调用是否成功。在Rust中,这些函数通常会返回一个 Result
。
创建token供应
处理 ERC-20
代币的智能合约类似于上一节教程使用maps存储值的 Incrementer 合约。对于本教程,ERC-20 合约由固定的代币供应组成,当部署合同时,这些代币全部存入与合同所有者相关联的帐户。然后,合同所有者可以将代币分发给其他用户。在本教程中创建的简单 ERC-20 合约并不是制造和分发代币的唯一方法。然而,这个 ERC-20 合约为扩展你在其他教程中所学到的知识,以及如何使用 ink! 语言用于构建更健壮的智能合约,提供了一个良好的基础。
对于 ERC-20 代币合约,初始存储包括:
total_supply
代表合约中代币的总供应量。balances
表示每个账户的个人余额。
要构建一个 ERC-20 代币智能合约,通过运行以下命令创建一个名为 erc20
的新项目:
cargo contract new erc20
在 erc20
目录中打开 lib.rs
文件,使用新 erc20 源码替换默认的模板源码。
保存代码改变并关闭文件。
打开 Cargo.toml
文件,并检查合约的依赖关系。
如有必要,在 [dependencies]
部分,修改 scale
和 scale-info
的配置:
scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] }
scale-info = { version = "2", default-features = false, features = ["derive"], optional = true }
保存代码改变并关闭文件。
通过运行下面的命令,验证程序是否编译并通过了简单的测试:
cargo +nightly test
该命令应该显示类似如下的输出,以表明测试成功完成:
running 2 tests
test erc20::tests::new_works ... ok
test erc20::tests::balance_works ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
通过运行以下命令,验证你可以为合约构建 WebAssembly。
cargo +nightly contract build
如果程序编译成功,您就可以在当前状态上传它,或者开始向合约添加功能。
上传并实例化合约
如果你想测试到目前为止所有的内容,你可以使用 Contracts UI 上传该合约。
在添加新功能之前测试 ERC-20 合同:
- 启动本地合约节点。
- 上传
erc20.contract
文件。 - 为
new
构造函数指定代币的初始供应。 - 在运行的本地节点上实例化合同。
- 选择
totalSupply
作为要发送的消息,然后点击 Read 以验证代币的总供应与初始供应相同。 - 选择
balanceOf
作为要发送的消息。 - 选择用于实例化合约的帐户
AccountId
,然后点击 Read。 如果选择任何其他AccountId
,则点击 Read,余额为零,因为所有代币均由合约所有者所有。
转移tokens
此时,ERC-20 合约有一个用户帐户,该帐户拥有该合约代币的 total_supply。为了使该合约有用,合约所有者必须能够将代币转移到其他帐户。对于这个简单的 ERC-20 合约,你将添加一个公共 transfer
函数,使你作为合约调用者能够将你拥有的代币转移给另一个用户。
公共 transfer
函数调用私有 transfer_from_to()
函数。因为这是一个内部函数,所以不需要任何授权检查就可以调用它。但是,转账逻辑必须能够确定 from
帐户是否具有可转移到接收方 to
帐户的代币数量。transfer_from_to()
函数使用合约调用者(self.env().caller()
)作为 from
帐户。在此上下文中,transfer_from_to()
函数执行以下操作:
- 获取
from
帐户和to
帐户的当前余额。 - 检查
from
余额是否小于要发送的代币的value
。
let from_balance = self.balance_of(from);
if from_balance < value {
return Err(Error::InsufficientBalance)
}
从转移账户中减去该值,并将该值添加到接收账户。
self.balances.insert(from, &(from_balance - value));
let to_balance = self.balance_of(to);
self.balances.insert(to, &(to_balance + value));
要添加转移函数到智能合约,在 erc20
项目目录中,打开 lib.rs
文件,添加一个 Error
声明,以在帐户中没有足够的代币来完成转账时返回一个错误。
/// Specify ERC-20 error type.
#[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)]
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
pub enum Error {
/// Return if the balance cannot fulfill a request.
InsufficientBalance,
}
添加一个 Result
返回类型,以返回 InsufficientBalance
错误。
/// Specify the ERC-20 result type.
pub type Result<T> = core::result::Result<T, Error>;
添加 transfer()
公共函数,使合约调用者能够将代币转移到另一个帐户。
#[ink(message)]
pub fn transfer(&mut self, to: AccountId, value: Balance) -> Result<()> {
let from = self.env().caller();
self.transfer_from_to(&from, &to, value)
}
添加 transfer_from_to()
私有函数,将代币从与合约调用者关联的帐户转移到接收帐户。
fn transfer_from_to(
&mut self,
from: &AccountId,
to: &AccountId,
value: Balance,
) -> Result<()> {
let from_balance = self.balance_of_impl(from);
if from_balance < value {
return Err(Error::InsufficientBalance)
}
self.balances.insert(from, &(from_balance - value));
let to_balance = self.balance_of_impl(to);
self.balances.insert(to, &(to_balance + value));
Ok(())
}
这段代码片段使用了 balance_of_impl()
函数,balance_of_impl()
函数与 balance_of
函数相同,不同的是它使用引用在 WebAssembly 中以更有效的方式查找帐户余额。在智能合约中添加如下函数即可使用该功能:
#[inline]
fn balance_of_impl(&self, owner: &AccountId) -> Balance {
self.balances.get(owner).unwrap_or_default()
}
通过运行下面的命令,验证程序是否编译并通过了测试用例:
cargo +nightly test
该命令应该显示类似如下的输出,以表明测试成功完成:
running 3 tests
test erc20::tests::new_works ... ok
test erc20::tests::balance_works ... ok
test erc20::tests::transfer_works ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
创建事件
ERC-20 代币标准规定合约调用在提交一个交易时不能直接返回值。然而,你可能希望你的智能合约以某种方式发出已经发生事件的信号。例如,你可能希望你的智能合约显示交易完成的时间或转账通过的时间,你可以使用事件来发送这些类信号。你可以使用事件与任何类型的数据进行通信,为一个事件定义数据类似于定义 struct
。事件应该使用 #[ink(event)]
属性被声明。
添加一个转移事件
声明一个 Transfer
事件,以提供有关已完成转移操作的信息。Transfer
事件包含以下信息:
Balance
类型的值。from
帐户的一个 Option-wrappedAccountId
变量。to
帐户的一个 Option-wrappedAccountId
变量。
为了更快地访问事件数据,可以使用索引字段。你可以通过在该字段上使用 #[ink(topic)]
属性标签来实现这一点。
要添加一个 Transfer
事件,打开 lib.rs
文件,使用 #[ink(event)]
属性宏声明事件。
#[ink(event)]
pub struct Transfer {
#[ink(topic)]
from: Option<AccountId>,
#[ink(topic)]
to: Option<AccountId>,
value: Balance,
}
你可以使用 .unwrap_or()
函数为 Option<T>
变量检索数据。
发出事件
现在你已经声明了事件并定义了事件包含的信息,接下来需要添加发出事件的代码。你可以通过调用 self.env().emit_event()
来实现这一点,将事件名称作为调用的唯一参数。
在这个 ERC-20 合约中,你希望在每次发生转移时发出一个 Transfer
事件,代码中有两个地方出现了这种情况
- 在初始化合约的
new
调用期间。 - 每次调用
transfer_from_to
。
from
和 to
字段的值是 Option<AccountId>
数据类型。然而,在 tokens 的初始转移过程中,为 initialsupply 设置的值不是来自任何其他帐户。在本例中,Transfer 事件的 from
值为 None
。
要发出 Transfer 事件,打开 lib.rs
文件,在 new
构造函数中将 Transfer
事件添加到 new_init()
函数中。
fn new_init(&mut self, initial_supply: Balance) {
let caller = Self::env().caller();
self.balances.insert(&caller, &initial_supply);
self.total_supply = initial_supply;
Self::env().emit_event(Transfer {
from: None,
to: Some(caller),
value: initial_supply,
});
}
将 Transfer
事件添加到 transfer_from_to()
函数中。
self.balances.insert(from, &(from_balance - value));
let to_balance = self.balance_of_impl(to);
self.balances.insert(to, &(to_balance + value));
self.env().emit_event(Transfer {
from: Some(*from),
to: Some(*to),
value,
});
注意 value
不需要 Some()
,因为值没有存储在 Option
中。
添加一个将代币从一个帐户转移到另一个帐户的测试用例。
#[ink::test]
fn transfer_works() {
let mut erc20 = Erc20::new(100);
assert_eq!(erc20.balance_of(AccountId::from([0x0; 32])), 0);
assert_eq!(erc20.transfer((AccountId::from([0x0; 32])), 10), Ok(()));
assert_eq!(erc20.balance_of(AccountId::from([0x0; 32])), 10);
}
通过运行以下命令验证程序的编译并且通过所有测试用例:
cargo +nightly test
该命令应该显示类似以下的输出,以表示成功完成测试:
running 3 tests
test erc20::tests::new_works ... ok
test erc20::tests::balance_works ... ok
test erc20::tests::transfer_works ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
启用第三方转账
ERC-20 代币合约现在可以在账户之间转移代币,并在发生这种情况时发出事件。作为最后一步,你可以添加 approve
和 transfer_from
函数以启用第三方转账。
允许一个账户代表另一个账户消费代币,允许你的智能合约支持去中心化的交易所。你可以批准你所拥有的一些代币以你的名义进行交易,而不是在合同中直接将你的代币转让给另一个用户。当你等待交易执行时,如果需要,你仍然可以控制和使用你的代币。你还可以批准多个合约或用户访问你的代币,因此,如果一个合约提供了最好的交易,你不需要将代币从一个合约移动到另一个合约,这可能是一个高成本和耗时的过程。
为了确保批准和转移可以安全完成,ERC-20 代币合约使用两步流程,分别从操作中进行批准和转移。
添加批准逻辑
批准另一个帐户来使用你的代币是第三方转账流程的第一步。作为一个代币所有者,你可以指定任何帐户代表转账,以及转账任意数量的代币。你不需要批准帐户中的所有令牌,你可以指定批准一个帐户允许转账的最大数量。
当多次调用 approve
时,将用新值覆盖先前批准的值。默认情况下,任意两个帐户之间的批准值为 0
。如果你想撤销帐户中对代币的访问,可以调用值为 0
的 approve
函数。
要在 ERC-20 合约中存储批准,你需要使用稍微复杂一些的 Mapping
键。由于每个帐户可以有不同的批准金额供其他任何帐户使用,因此需要使用元组作为映射到余额值的键。例如:
pub struct Erc20 {
/// Balances that can be transferred by non-owners: (owner, spender) -> allowed
allowances: ink_storage::Mapping<(AccountId, AccountId), Balance>,
}
元组使用 (owner, spender)
来标识 spender
帐户,该帐户被允许在指定 allowance
代表 owner
访问代币。
要为智能合约添加批准逻辑,打开 lib.rs
文件,使用 #[ink(event)]
属性宏声明 Approval
事件。
#[ink(event)]
pub struct Approval {
#[ink(topic)]
owner: AccountId,
#[ink(topic)]
spender: AccountId,
value: Balance,
}
添加一个 Error
声明,以便在转账请求超过帐户限额时返回一个错误。
#[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)]
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
pub enum Error {
InsufficientBalance,
InsufficientAllowance,
}
将所有者和非所有者组合的存储映射添加到帐户余额。
allowances: Mapping<(AccountId, AccountId), Balance>,
添加 approve()
函数来授权 spender
帐户从调用者的帐户中提取代币,直到取到最大 value
。
#[ink(message)]
pub fn approve(&mut self, spender: AccountId, value: Balance) -> Result<()> {
let owner = self.env().caller();
self.allowances.insert((&owner, &spender), &value);
self.env().emit_event(Approval {
owner,
spender,
value,
});
Ok(())
}
添加一个 allowance()
函数来返回允许 spender
从 owner
帐户中提取的代币数量。
#[ink(message)]
pub fn allowance(&self, owner: AccountId, spender: AccountId) -> Balance {
self.allowance_impl(&owner, &spender)
}
这段代码片段使用了 allowance_impl()
函数。allowance_impl()
函数与 allowance
函数相同,不同之处是它使用引用来在 WebAssembly 中以更有效的方式查找代币限额。在智能合约中添加如下函数即可使用该功能:
#[inline]
fn allowance_impl(&self, owner: &AccountId, spender: &AccountId) -> Balance {
self.allowances.get((owner, spender)).unwrap_or_default()
}
从逻辑中添加转账
现在你已经为一个帐户设置了代表另一个帐户转移代币的批准,你需要创建一个 transfer_from
函数,以允许已批准的用户转移代币。transfer_from
函数调用私有 transfer_from_to
函数来执行大部分转移逻辑。授权非所有者转让代币有几个要求:
self.env().caller()
合约调用者分配的必须是from
帐户中可用的代币。- 分配的存储作为
allowance
必须大于要转移的值。
如果满足了这些要求,合约将更新的限额插入到 allowance
变量中,并使用指定的 from
和 to
帐户调用 transfer_from_to()
函数。
记住,在调用 transfer_from
时,self.env().caller()
和 from
帐户用于查找当前限额,但 transfer_from
函数是在指定的 from
和 to
帐户之间被调用的。
每当调用 transfer_from
时,将使用三个帐户变量,你需要确保正确使用它们。
要为智能合约添加 transfer_from
,打开 lib.rs
文件,添加 transfer_from()
函数,以代表 from
帐户到 to
帐户转移代币的 value
。
/// Transfers tokens on the behalf of the `from` account to the `to account
#[ink(message)]
pub fn transfer_from(
&mut self,
from: AccountId,
to: AccountId,
value: Balance,
) -> Result<()> {
let caller = self.env().caller();
let allowance = self.allowance_impl(&from, &caller);
if allowance < value {
return Err(Error::InsufficientAllowance)
}
self.transfer_from_to(&from, &to, value)?;
self.allowances
.insert((&from, &caller), &(allowance - value));
Ok(())
}
为 transfer_from()
函数添加一个测试用例。
#[ink::test]
fn transfer_from_works() {
let mut contract = Erc20::new(100);
assert_eq!(contract.balance_of(AccountId::from([0x1; 32])), 100);
contract.approve(AccountId::from([0x1; 32]), 20);
contract.transfer_from(AccountId::from([0x1; 32]), AccountId::from([0x0; 32]), 10);
assert_eq!(contract.balance_of(AccountId::from([0x0; 32])), 10);
}
通过运行以下命令验证程序的编译并且通过所有测试用例:
cargo +nightly test
该命令应显示类似如下的输出,以表示测试成功完成:
running 5 tests
test erc20::tests::new_works ... ok
test erc20::tests::balance_works ... ok
test erc20::tests::transfer_works ... ok
test erc20::tests::transfer_from_works ... ok
test erc20::tests::allowances_works ... ok
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
通过运行以下命令验证你可以为该合约构建 WebAssemnbly
cargo +nightly contract build
在构建了合约的 WebAssembly 之后,你可以使用 Contracts UI 上传并实例化它,如上传并实例化合约中所述。
编写测试用例
在本节教程中,你向 lib.rs
文件中添加了简单的单元测试。基本的测试用例通过给出指定的输入值并验证返回的结果来说明函数按预期工作。你可以通过编写额外的测试用例来提高代码的质量。例如,你可以添加测试,对无效输入、空值或超出预期范围的值进行错误处理。
erc20 最终代码:
#![cfg_attr(not(feature = "std"), no_std)]
use ink_lang as ink;
#[ink::contract]
mod erc20 {
use ink_storage::traits::SpreadAllocate;
#[ink(event)]
pub struct Transfer {
#[ink(topic)]
from: Option<AccountId>,
#[ink(topic)]
to: Option<AccountId>,
#[ink(topic)]
value: Balance,
}
#[ink(event)]
pub struct Approval {
#[ink(topic)]
owner: AccountId,
#[ink(topic)]
spender: AccountId,
#[ink(topic)]
value: Balance,
}
#[cfg(not(feature = "ink-as-dependency"))]
#[ink(storage)]
#[derive(SpreadAllocate)]
pub struct Erc20 {
/// The total supply.
total_supply: Balance,
/// The balance of each user.
balances: ink_storage::Mapping<AccountId, Balance>,
/// Approval spender on behalf of the message's sender.
allowances: ink_storage::Mapping<(AccountId, AccountId), Balance>,
}
impl Erc20 {
#[ink(constructor)]
pub fn new(initial_supply: Balance) -> Self {
ink_lang::utils::initialize_contract(|contract: &mut Self| {
contract.total_supply = initial_supply;
let caller = Self::env().caller();
contract.balances.insert(&caller, &initial_supply);
// NOTE: `allowances` is default initialized by `initialize_contract`, so we don't
// need to do anything here
Self::env().emit_event(Transfer {
from: None,
to: Some(caller),
value: initial_supply,
});
})
}
#[ink(message)]
pub fn total_supply(&self) -> Balance {
self.total_supply
}
#[ink(message)]
pub fn balance_of(&self, owner: AccountId) -> Balance {
self.balances.get(&owner).unwrap_or_default()
}
#[ink(message)]
pub fn approve(&mut self, spender: AccountId, value: Balance) -> bool {
// Record the new allowance.
let owner = self.env().caller();
self.allowances.insert(&(owner, spender), &value);
// Notify offchain users of the approval and report success.
self.env().emit_event(Approval {
owner,
spender,
value,
});
true
}
#[ink(message)]
pub fn allowance(&self, owner: AccountId, spender: AccountId) -> Balance {
self.allowance_of_or_zero(&owner, &spender)
}
#[ink(message)]
pub fn transfer_from(
&mut self,
from: AccountId,
to: AccountId,
value: Balance,
) -> bool {
// Ensure that a sufficient allowance exists.
let caller = self.env().caller();
let allowance = self.allowance_of_or_zero(&from, &caller);
if allowance < value {
return false
}
let transfer_result = self.transfer_from_to(from, to, value);
// Check `transfer_result` because `from` account may not have enough balance
// and return false.
if !transfer_result {
return false
}
// Decrease the value of the allowance and transfer the tokens.
self.allowances.insert((from, caller), &(allowance - value));
true
}
#[ink(message)]
pub fn transfer(&mut self, to: AccountId, value: Balance) -> bool {
self.transfer_from_to(self.env().caller(), to, value)
}
fn transfer_from_to(
&mut self,
from: AccountId,
to: AccountId,
value: Balance,
) -> bool {
let from_balance = self.balance_of(from);
if from_balance < value {
return false
}
// Update the sender's balance.
self.balances.insert(&from, &(from_balance - value));
// Update the receiver's balance.
let to_balance = self.balance_of(to);
self.balances.insert(&to, &(to_balance + value));
self.env().emit_event(Transfer {
from: Some(from),
to: Some(to),
value,
});
true
}
fn allowance_of_or_zero(
&self,
owner: &AccountId,
spender: &AccountId,
) -> Balance {
// If you are new to Rust, you may wonder what's the deal with all the asterisks and
// ampersends.
//
// In brief, using `&` if we want to get the address of a value (aka reference of the
// value), and using `*` if we have the reference of a value and want to get the value
// back (aka dereferencing).
//
// To read more: https://doc.rust-lang.org/book/ch04-02-references-and-borrowing.html
self.allowances.get(&(*owner, *spender)).unwrap_or_default()
}
}
#[cfg(test)]
mod tests {
use super::*;
use ink_lang as ink;
#[ink::test]
fn new_works() {
let contract = Erc20::new(777);
assert_eq!(contract.total_supply(), 777);
}
#[ink::test]
fn balance_works() {
let contract = Erc20::new(100);
assert_eq!(contract.total_supply(), 100);
assert_eq!(contract.balance_of(AccountId::from([0x1; 32])), 100);
assert_eq!(contract.balance_of(AccountId::from([0x0; 32])), 0);
}
#[ink::test]
fn transfer_works() {
let mut contract = Erc20::new(100);
assert_eq!(contract.balance_of(AccountId::from([0x1; 32])), 100);
assert!(contract.transfer(AccountId::from([0x0; 32]), 10));
assert_eq!(contract.balance_of(AccountId::from([0x0; 32])), 10);
assert!(!contract.transfer(AccountId::from([0x0; 32]), 100));
}
#[ink::test]
fn transfer_from_works() {
let mut contract = Erc20::new(100);
assert_eq!(contract.balance_of(AccountId::from([0x1; 32])), 100);
contract.approve(AccountId::from([0x1; 32]), 20);
contract.transfer_from(
AccountId::from([0x1; 32]),
AccountId::from([0x0; 32]),
10,
);
assert_eq!(contract.balance_of(AccountId::from([0x0; 32])), 10);
}
#[ink::test]
fn allowances_works() {
let mut contract = Erc20::new(100);
assert_eq!(contract.balance_of(AccountId::from([0x1; 32])), 100);
contract.approve(AccountId::from([0x1; 32]), 200);
assert_eq!(
contract
.allowance(AccountId::from([0x1; 32]), AccountId::from([0x1; 32])),
200
);
assert!(contract.transfer_from(
AccountId::from([0x1; 32]),
AccountId::from([0x0; 32]),
50
));
assert_eq!(contract.balance_of(AccountId::from([0x0; 32])), 50);
assert_eq!(
contract
.allowance(AccountId::from([0x1; 32]), AccountId::from([0x1; 32])),
150
);
assert!(!contract.transfer_from(
AccountId::from([0x1; 32]),
AccountId::from([0x0; 32]),
100
));
assert_eq!(contract.balance_of(AccountId::from([0x0; 32])), 50);
assert_eq!(
contract
.allowance(AccountId::from([0x1; 32]), AccountId::from([0x1; 32])),
150
);
}
}
}
智能合约的问题排查
本节介绍了在基于 Substrate 的区块链上编写和部署智能合约时可能遇到的一些常见问题,以及如何解决这些问题。
意外的运行错误
如果你在不正确的情况下中断了一个正在运行的节点,例如,通过关闭终端或计算机切换到睡眠模式,你可能会看到以下错误
ClientImport("Unexpected epoch change")
如果看到此错误,请使用以下命令重新启动节点:
substrate-contracts-node --dev
此命令清除所有正在运行的节点状态。重新启动节点后,重复关闭节点之前执行的任何步骤。例如,重新部署你之前上传的所有合约。
本地存储中的过期合约
Contracts UI 使用它自己的本地存储来跟踪你已经部署的合约。如果你使用 Contracts UI 部署合约,然后清除链上的节点数据,将提示你需要重置本地存储。重置 Contracts UI 的本地存储后,重复清除节点之前执行的所有步骤,并重新部署之前上传的所有合约。
桥接其它链
本教程深入探讨了超越单链开发的更高级主题,包括如何将当前链连接到其它链。
准备一个本地平行链的测试网(准备一个本地中继链)
在本教程中,你将配置一个本地中继链并连接一个平行链模板,以便在本地测试环境中使用。
重点注意匹配版本
你必须使用本教程中规定的确切版本。平行链与它们所连接的中继链代码库紧密耦合,因为它们共享许多共同的依赖项。在使用整个 Substrate 文档中的任一示例时,请确保在任何其他软件中使用 Polkadot 的相应版本。你必须与中继链同步升级,你的平行链才能继续成功运行。如果你没有保持中继链的更新,很可能你的网络将停止生产区块。
文档示例中的版本
文档中的所有教程都经过了测试,可以使用:
- Polkadot
v0.9.26
- Substrate Parachain Template
polkadot-v0.9.26
- Polkadot-JS Apps
v0.116.2-34
一般情况下托管的 Polkadot-JS Apps 插件 可以正常使用。如果你有问题,请在此标签的 version/commit 下自行构建并运行 UI。
构建中继链节点
在 rococo-local
网络配置中稍加修改的 Polkadot 内置版本将作为本节教程的中继链。
# Clone the Polkadot Repository, with correct version
git clone --depth 1 --branch release-v0.9.26 https://github.com/paritytech/polkadot.git
# Switch into the Polkadot directory
cd polkadot
# Build the relay chain Node
cargo b -r
编译节点可能需要十五分钟到六十分钟才能完成。
# Check if the help page prints to ensure the node is built correctly
./target/release/polkadot --help
如果打印了帮助页面,则说明你已经成功构建了 Polkadot 节点。
中继链specification
你需要一个中继链网络的 chain specification。当我们想要使用本地测试网时,可能需要一个自定义配置来为验证者、引导节点地址等设置开发或自定义键。
**一个中继链上运行的验证者(validator)节点必须比连接的平行链核对者(collator)的总数多一个。**为了测试,这些通常必须硬编码到你的 chain spec 中。例如,如果你想用一个 collator 连接两个平行链,请运行三个或更多的中继链验证者节点,并确保它们都在 chain spec 中指定。
无论你选择使用哪个 chain spec 文件,在下面的说明中,我们都将该文件简单地称为 chain-spec.json
。你将需要为你正在使用的 chain spec 提供正确的路径。
预先配置的chain spec文件
本节教程包括一个示例 chain specification 文件,其中两个验证者中继链节点 Alice 和 Bob 作为授权。你无需任何修改就可以对本地测试网络和单个平行链使用此示例 chain specification。这对于注册单个平行链非常有用:
你可以读取和编辑普通 chain specification 文件。然而 chain specification 文件必须被融合为 SCALE-encoded 编码的原始格式,然后才能用于启动节点。有关将 chain specification 转换为使用原始格式的信息,请参见Customize a chain specification指南。
示例 chain specification 仅对具有两个验证者节点的单个平行链有效。如果你添加其他验证者,向中继链添加额外的平行链,或者希望使用自定义的 non-development keys,你将需要创建一个适合你需求的自定义 chain specification。
启动你的中继链
在开始对一个平行链进行区块生产之前,你需要启动一个中继链来连接它们。本节描述如何使用 two-validator raw chain spec 启动两个节点,启动其他节点的步骤与此类似。
启动alice
验证者
# Start Relay `Alice` node
./target/release/polkadot \
--alice \
--validator \
--base-path /tmp/relay/alice \
--chain <path to spec json> \
--port 30333 \
--ws-port 9944
命令中指定的端口(port
)和 websocket 端口(ws-port
)使用默认值,可以省略。但是,这里包含这些值是为了提醒你可以检查这些值。节点启动后,同一台本地计算机上的其他节点不能使用这些端口。
🏷 Local node identity is: 12D3KooWGjsmVmZCM1jPtVNp6hRbbkGBK3LADYNniJAKJ19NUYiq
当节点启动时,你将看到一些日志消息,包括节点的 Peer ID。要注意这一点,因为在将其他节点连接到它时将需要它。
启动bob
验证者
启动第二个节点的命令类似于启动第一个节点的命令,但有一些重要的区别。
./target/release/polkadot \
--bob \
--validator \
--base-path /tmp/relay-bob \
--chain <path to spec json> \
--bootnodes /ip4/<Alice IP>/tcp/30333/p2p/<Alice Peer ID> \
--port 30334 \
--ws-port 9945
注意,该命令使用不同的基本路径(/tmp/relay-bob
)、验证器密钥(--bob
)和端口(30334
和 9945
)。
启动第二个节点的命令还包括 --bootnodes
命令行选项,用于指定第一个节点的 IP 地址和 peer 标识符。如果你在单个本地计算机上运行整个网络,那么 bootnodes
选项不是必须一定要的,但是当使用到非本地网络的连接而在 chain spec 中没有任何指定的 bootnodes 时,该选项是必要的,正如我们正在使用的 rococo-custom-2-plain.json 示例一样。
在本节教程中,你的最终 chain spec 文件名必须以 rococo
开头,否则节点将不知道要怎样包含运行时逻辑。
其它资源
手动构建和配置中继链是一个很好的练习。然而,在你完成了几次之后,你可能希望自动化该过程。有很多方法可以做到这一点,这里有一些方法可供参考:
- parachain-launch 是一个生成 docker compose 文件的脚本,允许你启动多个区块链节点的测试网络。
- zombienet 是一个 CLI 工具,使你能够生成短暂的 Polkadot/Substrate 网络并对其执行测试。
连接本地平行链
本节教程演示了如何使用本地中继链预留平行链标识符,以及如何将本地平行链连接到该中继链。
构建平行链模板
本节教程使用 Substrate parachain template 来演示如何启动一个平行链去连接一个本地中继链。平行链模板类似于单链开发中使用的 node template。你可以使用平行链模板作为开发自定义平行链项目的起点。
要构建平行链模板,下载用来配置中继链 release 分支相匹配的 substrate-parachain-template
存储库的分支。例如,如果你使用 release-v0.9.28
Polkadot release 分支来配置本地中继链,那么对应平行链模板的分支则使用 polkadot-v0.9.28
。
git clone --depth 1 --branch polkadot-v0.9.28 https://github.com/substrate-developer-hub/substrate-parachain-template.git
cd substrate-parachain-template
#你现在有一个分离的分支。如果要保存更改并使分支易于识别,可以通过运行类似于以下的命令创建新分支:
git switch -c my-branch-v0.9.28
cargo build --release
编译节点最多需要60分钟,具体取决于硬件和软件配置。
预留唯一标识符
每个平行链必须预留一个唯一的标识符 ParaID
,使其能够连接到指定的中继链上。每个中继链为连接到它的平行链管理它的唯一标识符集合。该标识符被称为 ParaID
,因为同一标识符可用于标识 parachain 占用的插槽或标识 parathread 占用的插槽。
你应该注意到,你必须有一个账户有足够的资金,以便在中继链上预留一个插槽。你可以通过检查该中继链的 paras_registrar
pallet 中的 ParaDeposit
配置来确定指定中继链需要的 tokens 数量。例如,Rococo 需要 5 ROC 预留一个标识符:
parameter_types! {
pub const ParaDeposit: Balance = 5 * DOLLARS;
pub const DataDepositPerByte: Balance = deposit(0, 1);
}
impl paras_registrar::Config for Runtime {
type Event = Event;
type Origin = Origin;
type Currency = Balances;
type OnSwap = (Crowdloan, Slots);
type ParaDeposit = ParaDeposit;
type DataDepositPerByte = DataDepositPerByte;
type WeightInfo = weights::runtime_common_paras_registrar::WeightInfo<Runtime>;
}
每个中继链分配自己的标识符从 2000
开始递增,为所有的非 common good parachains。common good parachains 使用不同的方法来分配插槽标识符。
要预留平行链标识符,验证本地中继链的验证器是否正常运行,在浏览器中打开 Polkadot/Substrate Portal,连接一个本地中继链节点,点击 Network 并选择 Parachains。
点击 Parathreads,再点击 ParaId。
查看交易的设置以预留标识符,然后点击 Submit。用于预留该标识符的帐户将是该交易的收费帐户,并且也是与该标识符关联的 parathread 的 origin 帐户。
点击 Sign and Submit 对交易进行授权。提交交易之后,点击 Network 并选择 Explorer。
检查最近成功的 registrar.Reserved
事件列表,点击事件以查看有关交易的详细信息。
现在可以准备 chain specification 并生成平行链使用预留标识符(paraId
2000
)连接到中继链所需的文件。
修改默认的chain specification
要在本地中继链中注册平行链,必须修改默认的 chain specification 以使用预留的平行链标识符。
通过运行以下命令为平行链模板节点生成纯文本 chain specification:
./target/release/parachain-template-node build-spec --disable-default-bootnode > plain-parachain-chainspec.json
打开纯文本 chain specification 文件,将 para_id
设置为之前预留的平行链标识符。例如,如果你预留的标识符是 2000
,那么就设置 para_id
的字段为 2000
:
...
"relay_chain": "rococo-local",
"para_id": 2000,
"codeSubstitutes": {},
"genesis": {
...
}
将 parachainId
设置为先前保留的平行链标识符。例如,如果你预留的标识符为 2000
,则将 parachainId
字段设置为 2000
:
...
"parachainSystem": null,
"parachainInfo": {
"parachainId": 2000
},
...
保存代码并关闭纯文本 chain specification 文件。
通过运行以下命令,从修改的 chain specification 文件生成原始 chain specification 文件:
./target/release/parachain-template-node build-spec --chain plain-parachain-chainspec.json --disable-default-bootnode --raw > raw-parachain-chainspec.json
该命令生成一个新的原始 chain specification 文件,其中包含两个 collator。
2022-08-30 13:00:50 Building chain spec
2022-08-30 13:00:50 assembling new collators for new session 0 at #0
2022-08-30 13:00:50 assembling new collators for new session 1 at #0
准备平行链的collator
随着本地中继链的运行和对平行链链模板的原始 chain specification 的更新,你就可以启动平行链 collator 节点并导出其运行时和初始状态的信息。
准备要注册的平行链 collator,导出平行链的 WebAssembly 运行时。中继链需要 parachain-specific 的运行时验证逻辑来验证平行链区块,通过运行类似于以下的命令,你可以导出 parachain collator 节点的 WebAssembly 运行时:
./target/release/parachain-template-node export-genesis-wasm --chain raw-parachain-chainspec.json para-2000-wasm
生成一个平行链的初始状态。为了注册一个平行链,中继链需要知道平行链的初始状态;通过运行类似于以下的命令,可以将整个初始状态(十六进制编码)导出到一个文件中:
./target/release/parachain-template-node export-genesis-state --chain raw-parachain-chainspec.json para-2000-genesis-state
你应该注意到了导出的运行时和状态必须是针对 genesis 块的,你不能将前一个状态的平行链连接到中继器上。所有平行链必须从中继链上的块 0 开始。有关如何创建平行链模板以及如何将这个链逻辑(而不是它的历史状态迁移)转换为平行链的详细信息,请参见 Convert a solo chain。
使用类似于以下的命令启动 collator 节点:
./target/release/parachain-template-node \
--alice \
--collator \
--force-authoring \
--chain raw-parachain-chainspec.json \
--base-path /tmp/parachain/alice \
--port 40333 \
--ws-port 8844 \
-- \
--execution wasm \
--chain ../polkadot/raw-local-chainspec.json \
--port 30343 \
--ws-port 9977
在这个命令中,在惟一的 --
参数之前传递的参数是用于平行链模板 collator 的。--
后面的参数用于嵌入式中继链节点。注意,这个命令同时指定了平行链的原始 chain specification 和中继链的原始 chain specification。在本例中,本地中继链的原始 chain specification 为 polkadot
目录中的 raw-local-chainspec.json
。确保第二个 --chain
命令行为本地中继链指定了原始 chain specification 的路径。
如果你为平行链启动另一个节点,你将使用相同的中继链 chain specification 文件,但是使用不同的基本路径和端口号。
在启动平行链模板节点的终端中,你应该能看到类似以下输出:
2022-08-30 13:49:17 Parachain Collator Template
2022-08-30 13:49:17 ✌️ version 0.1.0-fd9771eed9c
2022-08-30 13:49:17 ❤️ by Anonymous, 2020-2022
2022-08-30 13:49:17 📋 Chain specification: Local Parachain Testnet
2022-08-30 13:49:17 🏷 Node name: Alice
2022-08-30 13:49:17 👤 Role: AUTHORITY
2022-08-30 13:49:17 💾 Database: RocksDb at /tmp/parachain/alice/chains/local_testnet/db/full
2022-08-30 13:49:17 ⛓ Native runtime: template-parachain-1 (template-parachain-0.tx1.au1)
2022-08-30 13:51:58 Parachain id: Id(2000)
2022-08-30 13:51:58 Parachain Account: 5Ec4AhPUwPeyTFyuhGuBbD224mY85LKLMSqSSo33JYWCazU4
2022-08-30 13:51:58 Parachain genesis state: 0x0000000000000000000000000000000000000000000000000000000000000000003c28ac319eab2cac949139fd0376f16bc97f698d1cde1bc3f46c2ec0edd1b9fb03170a2e7597b7b7e3d84c05391d139a62b157e78786d8c082f29dcf4c11131400
2022-08-30 13:51:58 Is collating: yes
2022-08-30 13:52:00 [Relaychain] 🏷 Local node identity is: 12D3KooWNNP9Z1D86KKgrzht6Pvd3WjKqxQaNkC6HpW5wVTUEEKR
...
你应该看到模板 collator 节点作为独立节点运行,它的中继节点作为对等节点与本地中继链验证器节点连接。如果你没有看到嵌入式中继链与本地中继链节点对接,请尝试禁用防火墙或添加带有中继节点地址的 bootnodes
参数。
它还没有开始创建平行链区块,创建块将在 collator 实际注册到中继链上时才开始。
向本地中继链注册
当本地中继链和 collator 节点都正常运行时,你就可以在本地中继链上注册平行链了。在实时公共网络中,注册通常涉及平行链插槽拍卖。在本节教程和本地测试中,你可以使用 Sudo 交易和 Polkadot/Substrate Portal。使用 Sudo 交易可以绕过获取平行链或平行线程插槽所需的步骤。
要注册平行链,验证本地中继链验证器是否正常运行,在浏览器中打开 Polkadot/Substrate Portal,如果需要连接到本地中继链节点,点击 Developer 并选择 Sudo。
选择 paraSudoWrapper,然后选择 sudoScheduleParaInitialize(id, genesis) 来在下一个中继链会话开始时初始化预留的 paraID。
对于交易参数:
id
:输入你预留的 ParaId。对于本节教程,保留标识符是 2000。genesisHead
:点击 file upload,上传你为 parachain 导出的初始状态。对于本节教程,选择para-2000-genesis
文件。validationCode
:点击 file upload 并上传你为平行链导出的 WebAssembly 运行时。对于本节教程,选择para-2000-wasm
文件。parachain
:选择 Yes。
点击 Submit Sudo。
查看交易详细信息,然后点击 Sign and Submit 对交易进行授权。提交交易之后,点击 Network 并选择 Explorer。
检查最近的事件列表是否有成功的 sudo.Suid
和点击事件查看交易的详细信息。
初始化平行链后,通过点击 Network 然后选择 Parachains,你可以在 Polkadot/Substrate Portal 中看到它。
点击 Network,选择 Parachains,等待新的 epic 开始。中继链跟踪每个平行链最近区块的块头。当一个中继链区块完成时,已经完成验证过程的平行链区块也完成了。这就是 Polkadot 为平行链实现 pooled, shared security 的方式。在平行链连接到下一个 epoch 的中继链并完成其第一个区块之后,你可以在 Polkadot/Substrate Portal 中看到有关它的信息。
运行平行链的终端会显示类似如下的详细信息:
2022-09-01 12:58:12 [Parachain] Starting collation. relay_parent=0x1ba093a16f8276459629b29b2bcee2b40e360a72a15a714cb208a5f6be576262 at=0x99951a12bbb25bad6e8878b517601a0fb65741189903d503789e12cd6d81810b
2022-09-01 12:58:16 [Relaychain] 💤 Idle (2 peers), best: #117 (0x1ba0…6262), finalized #113 (0x0798…07c2), ⬇ 0.6kiB/s ⬆ 0.4kiB/s
2022-09-01 12:58:16 [Parachain] 💤 Idle (0 peers), best: #2 (0x9995…810b), finalized #1 (0x50e8…5acb), ⬇ 0 ⬆ 0
...
2022-09-01 12:58:24 [Parachain] 🙌 Starting consensus session on top of parent 0x99951a12bbb25bad6e8878b517601a0fb65741189903d503789e12cd6d81810b
2022-09-01 12:58:24 [Parachain] 🎁 Prepared block for proposing at 3 (0 ms) [hash: 0x614a7f68290d68ec8b441886dbc6bda95593028c856cf32a46a64ca85af5b51f; parent_hash: 0x9995…810b; extrinsics (2): [0xe698…6c13, 0x5225…a083]]
2022-09-01 12:58:24 [Parachain] 🔖 Pre-sealed block for proposal at 3. Hash now 0x7ea49c65781d6c9a04bd8ae4f89b0c7bd84c7b3302233024ffa54909dc977a32, previously 0x614a7f68290d68ec8b441886dbc6bda95593028c856cf32a46a64ca85af5b51f.
2022-09-01 12:58:24 [Parachain] ✨ Imported #3 (0x7ea4…7a32)
2022-09-01 12:58:24 [Parachain] PoV size { header: 0.1787109375kb, extrinsics: 2.7470703125kb, storage_proof: 2.6123046875kb }
2022-09-01 12:58:24 [Parachain] Compressed PoV size: 4.802734375kb
2022-09-01 12:58:24 [Parachain] Produced proof-of-validity candidate. block_hash=0x7ea49c65781d6c9a04bd8ae4f89b0c7bd84c7b3302233024ffa54909dc977a32
2022-09-01 12:58:26 [Relaychain] 💤 Idle (2 peers), best: #119 (0x20a1…5579), finalized #116 (0x0c12…2ba3), ⬇ 0.7kiB/s ⬆ 1.4kiB/s
2022-09-01 12:58:26 [Parachain] 💤 Idle (0 peers), best: #2 (0x9995…810b), finalized #1 (0x50e8…5acb), ⬇ 0 ⬆ 0
...
2022-09-01 12:58:36 [Relaychain] 👶 New epoch 12 launching at block 0xa2a3…253c (block slot 277010386 >= start slot 277010386).
2022-09-01 12:58:36 [Relaychain] 👶 Next epoch starts at slot 277010396
2022-09-01 12:58:36 [Relaychain] ✨ Imported #121 (0xa2a3…253c)
2022-09-01 12:58:36 [Relaychain] Advanced session window for approvals update=Advanced { prev_window_start: 6, prev_window_end: 11, new_window_start: 7, new_window_end: 12 }
2022-09-01 12:58:36 [Parachain] Starting collation. relay_parent=0xa2a3fabb974f673d49cc6b50605e6d90595234ce16fd7bd01bce808bbdf0253c at=0x7ea49c65781d6c9a04bd8ae4f89b0c7bd84c7b3302233024ffa54909dc977a32
2022-09-01 12:58:36 [Relaychain] 💤 Idle (2 peers), best: #121 (0xa2a3…253c), finalized #117 (0x1ba0…6262), ⬇ 1.0kiB/s ⬆ 0.6kiB/s
2022-09-01 12:58:36 [Parachain] 💤 Idle (0 peers), best: #3 (0x7ea4…7a32), finalized #2 (0x9995…810b), ⬇ 0 ⬆ 0
...
连接并提交交易
到目前为止,你已经使用 Polkadot/Substrate Portal 连接到本地网络,并将交易提交到本地中继链。现在已经有了一个运行的平行链并连接到中继链,你可以使用 Polkadot/Substrate Portal 向平行链提交交易。
要连接到平行链并提交交易,在浏览器中打开 Polkadot/Substrate Portal,点击应用程序左上角的网络选择器。
将自定义端点更改为连接到平行链的 WebSocket 端口。如果你按照本节教程中的设置,则连接到 8844 端口。
点击 Account,选择 Transfer,将资金从Alice账户转到另一个账户:
- 选择要将资金发送到的帐户。
- 打印金额。
- 点击 Make Transfer。
- 查看交易记录,然后点击 Sign and Submit 以授权转账。
点击 Accounts 以验证转账已完成且平行链交易成功。如果交易成功,你就有了一个正常工作的平行链。
重置区块链状态
本节教程中连接到中继链的平行链 collator 包含平行链的所有区块链数据。在这个平行链网络中只有一个节点,所以你提交的任何交易都只存储在这个节点上。中继链不存储任何平行链状态。中继链只存储与之相连的平行链的区块头信息。
出于测试目的,你可能希望定期清除区块链状态以重新开始。但是应该记住,如果清除链状态或手动删除数据库,你将无法恢复数据或恢复链状态。如果你有想要保存的数据,那么在清除平行链状态之前应该确保有一个副本。
如果你想重新使用一个干净的环境进行测试,你应该完全删除本地中继链节点和平行链节点的链状态。
要重置区块状态,在运行平行链模板节点的终端中,按 Control-c,通过运行以下命令清除平行链 collator 状态:
rm -rf /tmp/parachain
在 alice
验证者节点或 bob
验证者节点正在运行的终端中,按 Control-c,通过运行以下命令清除验证程序状态:
rm -rf /tm/relay
连接Rococo测试网(获取测试网插槽)
本节教程演示如何在公共测试网络(例如 Rococo 测试网络)上部署一个平行链。公共测试网络比私有网络有更高的使用门槛,但也表示准备将一个平行链项目转移到生产网络的重要一步。
从帐户和代币开始
要对 Rococo 执行任何操作,你需要 ROC 代币并存储代币,你必须能够访问与 Substrate-compatible 的数字货币钱包。你不能在任何公共设置中使用开发密钥和帐户进行操作。想要持有数字货币有很多选择,包括硬件钱包和基于浏览器的应用程序,其中一些比其他更有信誉。在选择一个之前,你应该自己做好调查。
但是,你可以使用 Polkadot/Substrate Portal 网站开始测试。
要准备一个账户,将Polkadot/Substrate Portal连接到 Rococo 网络,点击并选择 Accounts,点击 Add Account,复制你的 secret seed phrase,并把它放在一个安全的地方,然后点击 Next,输出一个账户的名称和密码,然后点击 Next,点击 Save,加入 Rococo Element channel,并使用 !drip
发送消息,为你的 Rococo 在钱包中获得 100 ROC 的公共地址。例如,发送类似以下内容的消息:
!drip 5CVYesFxbDBU5rkZXYTAA6BnADbCoSpQkvexBQZvbtvyGTP1
预留平行链标识符
你必须预留一个平行链标识符,然后才能注册为 Rococo 上的平行链。步骤与连接本地平行链以保留本地中继链上的标识符中的步骤类似。但是,对于公共测试网,你将会被分配到下一个真实可用的标识符。
要预留一个标识符,将Polkadot/Substrate Portal连接到 Rococo 网络,点击 Network 并选择 Parachains。
点击 Parathreads,然后点击 ParaId,检查交易并注意分配给你的平行链标识符,然后点击 Submit。输入您的密码以验证你的身份,然后点击 Sign and Submit。
点击 Network 并选择 Explorer ,检查成功的 registrar.Reserved
的最近事件列表。
修改chain specification文件
注册平行链所需的文件必须指定要连接的正确的中继链和已分配给你的平行链标识符。要进行这些修改,必须为平行链构建和修改 chain specification 文件。在本节教程中,中继链是 rococo
,而不是在连接本地平行链教程中使用的 rococo-local
,平行链标识符是 4105
。
要修改 chain specification,通过运行以下命令生成平行链模板节点的纯文本chain specification:
./target/release/parachain-template-node build-spec --disable-default-bootnode > plain-parachain-chainspec.json
打开平行链模板节点的纯文本chain specification文件,将 relay-chain
设置为 rococo
, para_id
设置为已分配的标识符。例如,如果你预留的标识符是 4105
,则将 para_id
字段设置为 4105
:
...
"relay_chain": "rococo",
"para_id": 4105,
"codeSubstitutes": {},
"genesis": {
...
}
将 parachainId
设置为你之前预留的平行链标识符。
...
"parachainSystem": null,
"parachainInfo": {
"parachainId": 4105
},
...
将你的帐户公钥添加到会话密钥部分。
...
"session": {
"keys": [
[
"5CVYesFxbDBU5rkZXYTAA6BnADbCoSpQkvexBQZvbtvyGTP1",
"5CVYesFxbDBU5rkZXYTAA6BnADbCoSpQkvexBQZvbtvyGTP1",
{
"aura": "5CVYesFxbDBU5rkZXYTAA6BnADbCoSpQkvexBQZvbtvyGTP1"
}
],
]
}
...
保存代码并关闭纯文本 chain specification 文件。
通过运行以下命令,从修改后的 chain specification 文件生成原始 chain specification 文件:
./target/release/parachain-template-node build-spec --chain plain-parachain-chainspec.json --disable-default-bootnode --raw > raw-parachain-chainspec.json
导出必要的文件
准备要注册的平行链 collator,通过运行类似如下的命令导出平行链的 WebAssembly 运行时:
./target/release/parachain-template-node export-genesis-wasm --chain raw-parachain-chainspec.json para-4105-wasm
通过运行类似如下的命令生成一个平行链初始状态:
./target/release/parachain-template-node export-genesis-state --chain raw-parachain-chainspec.json para-4105-genesis-state
启动collator节点
你必须具有 collator 公开可访问并且可发现的端口,以便使平行链节点与 Rococo 验证器节点进行 peer 连接,从而生成块。你可以使用 --port
命令行选项指定要使用的端口。例如,可以使用类似于以下的命令启动 collator:
--chain raw-parachain-chainspec.json \
--base-path /tmp/parachain/pubs-demo \
--port 50333 \
--ws-port 8855 \
-- \
--execution wasm \
--chain rococo \
--port 50343 \
--ws-port 9988
在此示例中,第一个 --port
为 collator 节点设置指定的端口,第二个 --port
指定嵌入式中继链节点的端口。第一个 --ws-port
设置指定可以使用 Polkadot-JS API 调用或 Polkadot/Substrate Portal 应用程序连接到 collator 的端口。第二个 --ws-port
指定使用 Polkadot-JS API 调用或 Polkadot/Substrate Portal 应用程序连接到嵌入式中继链的端口。
注册为一个平行线程
你在公共中继链上租赁一个插槽,成为一个平行链之前,首先必须注册为 Rococo 上的平行线程。
要注册为一个平行线程,将Polkadot/Substrate Portal连接到 Rococo 网络。点击 Network 并选择 Parachains。点击 Parathread,然后点击 ParaThread。
验证平行链的所有者和平行链的标识符,并上传包含 WebAssembly 验证函数和平行链初始状态的文件,然后点击 Submit。输入密码验证你的身份,然后点击 Sign and Submit。向下滚动平行线程列表,并验证你的平行线程注册是否为 Onboarding:
你也可以通过在 Network Explorer 中 registrar.Registered
的事件来验证你的注册请求。提交注册请求后,需要两个会话才能完成平行线程的加载。
请求一个平行链插槽
在平行链作为一个平行线程被激活后,相关的项目团队应该在 Rococo 上打开一个永久或临时平行链插槽的请求。
- 永久插槽通常分配给成功完成插槽租赁拍卖并在 Polkadot 上部署有插槽的平行链的团队。永久插槽使这些团队能够在实时公共环境中持续测试他们的代码库与最新 Polkadot 特性的兼容性。目前只有有限数量的永久插槽可用。
- 临时插槽是以连续轮循的方式动态分配的平行链插槽。从每个租期开始时,在一定的租期内如果不超过中继链配置中定义的最大平行线程数,将自动升级为平行链。在租期结束时活跃的平行链将自动降级为平行线程,以释放槽位供其他人在随后的租期使用。具有动态分配的临时插槽使在 Polkadot 上没有平行链插槽的团队能够在现实的网络环境中更频繁地测试他们的运行时。
提交一个插槽请求
Rococo 运行时需要 sudo
访问来分配插槽。例如,Rococo 运行时指定用于分配插槽的帐户必须具有 root 级别权限:
AssignSlotOrigin = EnsureRoot<Self::AccountId>;
最终,插槽分配将通过 Rococo 治理方式由社区驱动。然而,Rococo sudo
的密钥目前是由 Parity 公司控制。因此,你必须提交 Rococo Slot Request 接收插槽分配。分配插槽后,你将收到通知,并准备连接。
使用管理帐户分配一个插槽
如果你有一个具有 AssignSlotOrigin
origin 帐户,你可以使用该帐户在 Rococo 网络上分配一个临时插槽。
要分配一个临时插槽,将Polkadot/Substrate Portal连接到 Rococo 网络。点击 Developer 并选择 Extrinsics。选择要用于提交交易的帐户。选择 assignedSlots
pallet。选择 assignTempParachainSlot
函数。输入分配给你的预留平行链标识符。为 LeasePeriodStart
选择 Current
,如果当前的插槽已满,你将被分配到下一个可用的插槽。点击 Submit Transaction。输入密码以验证你的身份,然后点击 Sign and Submit。如果你的帐户没有足够的权限,则交易将失败,并出现 BadOrigin 错误。
租赁期限
为在 Rococo 上分配的平行链插槽,当前租赁期限和插槽可用配置是:
- 永久插槽至少持续时间:1年(365天)
- 临时插槽至少持续时间:3天
- 永久插槽的最大数量:最多25个永久插槽
- 临时插槽的最大数量:最多20个临时插槽
- 每个租赁期分配的最大临时插槽:每3天临时租赁期最多5个临时插槽
这些配置可能会根据社区的需求进行改变。
测试你的平行链
在一个插槽被分配并被激活后,你可以在 Rococo 测试网络上测试你的平行链。注意当临时插槽租期结束时,平行链会自动降级为平行线程。已注册和已批准的插槽以轮询的方式自动循环,因此你可以继续不断地以平行链的形式重新连接。
工具集成
本教程重点介绍了工一些具和节点扩展,这些工具和节点扩展使你能够部署与其他系统集成的节点,以及如何使用额外的生态系统工具,让你能够访问、同步,与你自己或其他区块链的信息进行交互。
集成一个轻客户端节点
本节教程演示如何使用浏览器中运行的 WebAssembly 轻客户端连接到基于 Substrate 的区块链。在本节教程中,你将使用 Substrate Connect 浏览器扩展与区块链交互,而不使用 RPC 服务。
什么是Substrate Connect
Substrate Connect是一个基于 WebAssembly 的轻客户端,可以直接在浏览器中运行。Substrate Connect light client 的核心软件组件是 smoldot。该软件需要的资源比完整节点更少,因此它可以在资源受限的环境中运行,包括浏览器、移动端点和物联网设备。轻客户端可以通过连接到一个完整的节点来同步链中的数据,而不是作为一个运行的 peer 并直接连接到区块链,以为了生产区块或导入区块。
安全同步
软件钱包允许用户通过可信任的中间第三方节点来与区块链交互,与之不同的是,轻客户端从完整节点下载区块头,这样他们就可以使用区块头中的 Merkle trie root 来验证正在同步的信息是否被篡改。Merkle trie root 作为数据没有被修改的加密证明,而不需要轻客户端信任整个节点。
Substrate Connect作为一个浏览器扩展
因为轻客户端不参与区块生成或共识,所以它们不需要在线,也不需要与网络进行持续通信。但是,如果你将轻客户端作为浏览器扩展来运行,那么你可以同时运行多个轻客户端,并且只要浏览器保持打开状态,就可以在浏览器会话中保持同步。
运行一个轻客户端作为浏览器扩展还避免了完整节点需要的使用 Transport Layer Security(TLS)和 Secure Socket Layer(SSL)证书。使用 Substrate Connect,同步在后台进行,而不需要通过 WebSocket 端口(一些浏览器会将其作为不安全连接并阻止)。运行Substrate Connect 作为浏览器扩展还提供了更好的应用程序性能和响应更快的用户体验。
使用Substrate Connect的应用程序和用户实践
如果你使用 Substrate Connect 构建应用程序,smoldot
客户端可以检测到用户是否拥有浏览器扩展,并在浏览器扩展可用时自动使用该扩展。如果用户没有安装浏览器扩展,smoldot
会自动在你的 web 应用程序中创建一个 WebAssembly 轻客户端。虽然将 Substrate Connect 作为浏览器扩展运行是可选的,该扩展提供了以下优势:
- 更好的资源使用效率。多个浏览器 tabs 可以共享一个连接到同一条链,而不是每个浏览器 tabs 或窗口打开自己的连接。
- 更好的同步速度。只要打开一个浏览器 tabs,浏览器扩展就自动开始与链同步,保持缓存以便连接到链,对于用户打开的每个新标签或浏览器窗口,同步几乎是瞬时的。如果没有浏览器扩展,同步一个链可能需要10到30秒。
- 更好的连通性。浏览器扩展可以连接到未安装 TLS/SSL 证书的节点。
下载Substrate Connect
由于 Substrate Connect 浏览器扩展提供的优势,第一步首先需要安装浏览器扩展。
- 使用 Chrome 或 Firefox 打开链接 https://substrate.io/developers/substrate-connect/。
- 点击 Chrome 或 Firefox。
- 点击 Add to Chrome 或 Add to Firefox,然后确认你想要将扩展添加到浏览器。
连接到一个众所周知的链
在 Substrate Connect 轻客户端可以连接到网络之前,你必须有一个 web 应用程序,该应用程序指定了请客户端应该连接到的网络,用于通信的节点,以及它在初始阶段必须具有的共识临界状态。这些信息可以在网络的 chain specification 文件中获得。
Substrate Connect 预先配置为识别在 WellKnownChain 列表中定义的几个链,这些知名的链是:
- Polkadot 被识别为
polkadot
- Kusama 被识别为
ksmcc3
- Rococo 被识别为
rococo_v2_2
- Westend 被识别为
westend2
要连接到这些链之一,通过以下命令克隆 empty-webapp
模板,创建使用 Substrate Connect 的 web 应用:
git clone https://github.com/bernardoaraujor/empty-webapp
cd empty-webapp
# 通过运行以下命令安装来自 Polkadot-JS RPC 提供的依赖项
yarn add @polkadot/rpc-provider
# 通过运行以下命令安装 Polkadot-JS API 中的依赖项
yarn add @polkadot/api
安装这些依赖项之后,就可以在示例应用程序中使用它们。
在编辑器中打开 empty-webapp/index.ts
文件,复制并粘贴以下应用程序代码,使用 substrate-connect
作为提供以创建一个 Substrate Connect 实例,使用 polkadot
chain specification 文件连接到 Polkadot
中继链。
import {
ScProvider,
WellKnownChain,
} from "@polkadot/rpc-provider/substrate-connect";
import { ApiPromise } from "@polkadot/api";
window.onload = () => {
void (async () => {
try {
const provider = new ScProvider(WellKnownChain.polkadot);
await provider.connect();
const api = await ApiPromise.create({ provider });
await api.rpc.chain.subscribeNewHeads(
(lastHeader: { number: unknown; hash: unknown }) => {
console.log(
`New block #${lastHeader.number} has hash ${lastHeader.hash}`
);
}
);
} catch (error) {
console.error(<Error>error);
}
})();
};
在 Polkadot-JS API 中,你可以像这样创建一个实例:
// Import
import { ApiPromise, WsProvider } from '@polkadot/api';
// Construct
const wsProvider = new WsProvider('wss://rpc.polkadot.io');
const api = await ApiPromise.create({ provider: wsProvider });
对于 Substrate Connect,你将 WebSocket(WsProvider
)provider 替换为 Substrate Connect(ScProvider
),并指定 Polkadot 网络(WellKnownChain.polkadot
)的 chain specification,而不是 WebSocket URL 客户端地址。
通过运行以下命令安装任何剩余的依赖项:
yarn
通过运行以下命令启动 web 应用程序:
yarn dev
如果在启动本地服务器时出现编译器错误,你可能会缺少当前 yarn
配置中没有考虑到的依赖。如果缺少依赖项,可以通过运行类似以下的命令添加依赖包:
yarn add -D buffer
打开 URL http://localhost:3001/
验证浏览器。
打开浏览器控制台。打开浏览器控制台的方法取决于所使用的浏览器和操作系统,例如,在 Chrome 上,选择更多工具,开发人员工具,然后单击控制台。
验证 smoldot
进程已初始化,然后是来自 Polkadot 传入块的哈希值。例如,控制台应该显示类似如下的日志信息:
[smoldot] Smoldot v0.6.25
smoldot-light.js:41 [smoldot] Chain initialization complete for polkadot. Name: "Polkadot". Genesis hash: 0x91b1…90c3. State root hash: 0x29d0d972cd27cbc511e9589fcb7a4506d5eb6a9e8df205f00472e5ab354a4e17. Network identity: 12D3KooWRse9u6Z9ukP4C92YCCH2gXziNm8ThRch2owaaFh9H6D1. Chain specification or database starting at: 0xae3e…f81d (#11228238)
...
New block #11322769 has hash 0x464c0199ede92a89920c54c21abc741ea47daca1d62d61d7b9af78062f04c7a3 index.ts:10
New block #11322770 has hash 0xd66c61e5417249df228798f38535a6dd17b8b268c165e0a6b0e72ba74e954f9d index.ts:10
这个简单的 web 应用程序只连接到 Polkadot 检索块哈希值。此应用程序的主要目的是演示在不使用中心化的网络入口点(例如特定 RPC 节点的 URL)的情况下连接到链。但是,你可以扩展这个应用程序来做更多的事情,因为在你将 WsProvider
替换为 ScProvider
之后,你可以简单地使用现有的Polkadot-JS API 为你的应用程序编写代码。
按 Control-c 停止 smoldot
轻客户端节点。
连接到自定义chain specification
连接到自定义 chain specification 或公共可访问的平行链类似于连接到 well-known 的链之一。代码中的主要区别在于,必须显式地标识要使用的 Substrate Connect 的 chain specification。这部分教程演示如何通过连接到 Statemint 平行链来连接到自定义 chain specification。Statemint 是一个连接到 Polkadot 的通用良好的平行链,并且有一个公开的 chain specification 文件。
要连接到这个链,从 cumulus repository 下载定制 chain specification 文件。将下载的 chain specification 复制到你在连接到一个众所周知的链中创建的empty-webapp
目录。
在编辑器中打开 index.ts
文件,删除当前内容。复制并粘贴以下应用程序代码:
import { ScProvider, WellKnownChain } from "@polkadot/rpc-provider/substrate-connect";import { ApiPromise } from "@polkadot/api";
import jsonParachainSpec from "./statemint.json";
window.onload = () => {
void (async () => {
try {
const relayProvider = new ScProvider(WellKnownChain.polkadot);
const parachainSpec = JSON.stringify(jsonParachainSpec);
const provider = new ScProvider(parachainSpec, relayProvider);
await provider.connect();
const api = await ApiPromise.create({ provider });
await api.rpc.chain.subscribeNewHeads((lastHeader: { number: unknown; hash: unknown }) => {
console.log(`New block #${lastHeader.number} has hash ${lastHeader.hash}`);
});
} catch (error) {
console.error(<Error>error);
}
})();
};
正如你看到的,这段代码有一些重要的区别。
statemint.json
chain specification 文件导入到jsonParachainSpec
对象中。- chain specification 被转换为一个 JSON-encoded 的字符串,并存储在
parachainSpec
变量中,这样它就可以与 web 服务器交换。
ScProvider
provider 是为 polkadot
中继链创建的,但它被用作创建和连接到平行链 provider 的参数。Substrate Connect 需要此信息来确定与平行链通信的中继链。
运行如下命令启动 web 应用程序
yarn dev
打开 URL http://localhost:3001/
验证浏览器。在浏览器中打开控制台。
验证 smoldot
进程已初始化,然后是来自 Polkadot 传入块的哈希值。例如,控制台应该显示类似如下的日志信息:
[smoldot] Parachain initialization complete for statemint. Name: "Statemint". Genesis hash: 0x68d5…de2f. State root hash: 0xc1ef26b567de07159e4ecd415fbbb0340c56a09c4d72c82516d0f3bc2b782c80. Network identity: 12D3KooWArq3iZHdK2jtRZSJzJkkWrKm17JTa9kjwjZkq9Htx5xR. Relay chain: polkadot (id: 1000 smoldot-light.js:41
[smoldot] Smoldot v0.6.25. Current memory usage: 140 MiB. Average download: 35.4 kiB/s. Average upload: 423 B/s.
New block #1785421 has hash 0x88885ed331f94b4324c5f2eae8413cd36170808ef904b0ec0867646fa53770f7 index.ts:13
New block #1785422 has hash 0x2ad4d96e061a681e27403694f1d870bb0c4e5c77b5be232a18c7a2e0b7fb2555 index.ts:13
高级应用程序开发
本节教程中的示例使用了 @polkadot/rpc-provider/substrate-connect
,因为该 provider 可以直接创建使用 Polkadot-JS API 与链进行交互的应用程序。对于不依赖 Polkadot-JS API 的更高级应用程序开发,你可以安装并使用 @substrate-connect
。例如,如果你正在构建自己的应用程序库或编程接口,则应通过运行以下命令安装 Substrate Connect 依赖项:
yarn add @substrate/connect
访问EVM账户
本节教程演示了如何使用 Frontier 项目中的 crates 来构建一个与以太坊兼容的区块链,该区块链可以访问基于以太坊的帐户并执行基于 Solidity 的智能合约。Frontier 项目的两个主要目标是使你能够执行以下操作:
- 使用本地 Substrate 节点不加修改地运行以太坊去中心化应用。
- 从以太坊主网络导入状态
本节教程使用预定义的节点模板提供的工作环境。模板是使用 Frontier release guide 中的说明生成的。
如果需要为自己生成独立的模板,可以使用 node-template-release.sh 模板生成脚本。如果你使用 frontier 仓库或模板生成脚本构建自己的节点,请注意 frontier 使用自己版本的 Substrate crates,你可能需要更新 Cargo
文件中的依赖,以匹配项目中的依赖。
创世配置
frontier-node-template
中的开发 chain specification 定义了一个创世块,该块已经为 alice
帐户预配置了一个 EVM 帐户。当你在开发模式中启动该节点时,alice
的 EVM 帐户将使用默认的 Ether 资金额度。你将使用该帐户查看 EVM 帐户详细信息并调用以太坊智能合约。启动节点后,你将能够使用 Polkadot-JS application 查看` 的 EVM 帐户的详细信息。
编译一个Frontier节点
要编译 Frontier node template,通过运行一下命令克隆 node template 仓库,并编译 node template:
git clone https://github.com/substrate-developer-hub/frontier-node-template.git
cd frontier-node-template
cargo build --release
连接到节点
在节点编译后,必须启动该节点才能开始研究预先配置的 EVM 帐户。
要启动本地 Substrate 节点,在 frontier-node-template
目录下,运行一下命令:
./target/release/frontier-template-node --dev
--dev
命令行选项指定节点使用预定义的 development
chain specification 运行,该 chain specification 包括 alice
预定义的 EVM 帐户和用于测试的其他帐户。
通过检查终端中显示的输出,验证你的节点已成功启动并运行,终端应该显示类似这样的输出:
2022-07-08 10:06:42 Frontier Node
2022-07-08 10:06:42 ✌️ version 0.0.0-1b6bff4-x86_64-macos
2022-07-08 10:06:42 ❤️ by Substrate DevHub <https://github.com/substrate-developer-hub>, 2021-2022
2022-07-08 10:06:42 📋 Chain specification: Development
2022-07-08 10:06:42 🏷 Node name: flippant-boat-0444
2022-07-08 10:06:42 👤 Role: AUTHORITY
...
使用 Polkadot-JS application 连接本地节点。
点击 Settings,然后点击 Developer
定义以下帐户信息来创建一个 EVM Account
类型,并允许该帐户发送交易和检查区块。要发送交易,你必须定义 Address
和 LookupSource
的值。要检查区块,你必须定义 Transaction
和 Signature
的值。
{
"Address": "MultiAddress",
"LookupSource": "MultiAddress",
"Account": {
"nonce": "U256",
"balance": "U256"
},
"Transaction": {
"nonce": "U256",
"action": "String",
"gas_price": "u64",
"gas_limit": "u64",
"value": "U256",
"input": "Vec<u8>",
"signature": "Signature"
},
"Signature": {
"v": "u64",
"r": "H256",
"s": "H256"
}
}
点击保存
使用RPC查询余额
在为 EVM 帐户配置对应的设置后,可以使用 Polkadot-JS 应用程序查看 alice
的 EVM 帐户信息。
- 验证你的节点仍在运行,并且 Polkadot-JS 应用程序已连接到该节点。
- 点击 Developer,然后选择 RPC calls。
- 在 Submission 选项卡上,选择 eth 作为要调用的端点。
- 从要调用的函数列表中选择 getBalance(address, number)。
- 为
alice
帐户的地址指定 EVM 帐户标识符。 预定义账户地址为0xd43593c715fdd31c61141abd04a99fd6822c8558
。帐户的地址是使用 Substrate EVM utilities 从alice
帐户的公钥计算出来的。 - 点击 Submit RPC call,该调用应该返回如下类似的输出:
2: eth.getBalance: U256
340,282,366,920,938,463,463,374,607,431,768,210,955
部署一个智能合约
现在你已经了解了如何查询以太坊地址的余额,你可能还想探索如何部署和调用以太坊智能合约并测试相关功能。这部分内容会使用一个 Truffle 示例合约来定义 ERC-20 token。你也可以使用 Polkadot JS SDK 和 Typescript 创建一个 ERC-20 token 合约。
创建 ERC-20 合约。为了方便起见,你可以使用 MyToken.json 中的 token 合约编译的 bytecode
,然后将合约部署到 Substrate 区块链上。
验证你的节点仍在运行,并且 Polkadot-JS application 已连接到该节点。
点击 Developer,然后选择 Extrinsics。
选择 ALICE 开发帐户作为用于提交交易的帐户。
选择 evm。
选择 create 函数。
配置该函数的参数:
|For this|Specify this|
|:-----|:-----|
|source
|0xd43593c715fdd31c61141abd04a99fd6822c8558|
|init
|MyToken.json
中的原始 bytecode
十六进制值|
|value
|0|
|gasLimit
|4294967295|
|maxFeePerGas
|100000000|
你可以将可选参数保留为空,nonce
的值将增加源帐户的已知 nonce 值,并从 0x0
开始。根据所选函数的不同,你可能需要删除未使用的参数。
点击 Submit Transaction。
点击 Sign and Submit 对交易进行授权。
查看智能合约
提交交易后,合约将部署到网络上,你可以使用 Polkadot-JS application 查看有关它的信息。
验证你的节点仍在运行,并且 Polkadot-JS application 已连接到该节点。
点击 Network,然后选择 Explorer。
点击 evm.Created 事件以验证新创建的合约的地址为 0x8a50db1e0f9452cfd91be8dc004ceb11cb08832f
。
您还可以使用浏览器的开发人员工具中的控制台查看有关事务的详细信息。因为EVM合同地址是由帐户标识符和合同创建者的nonce决定的,所以部署合同的地址是使用众所周知的帐户标识符0xd43593c715fdd31c61141abd04a99fd6822c8558和alice帐户的nonce 0x0计算的。
点击 Developer,然后选择 Chain State。
选择 evm 作为查询和 accountCodes 的状态。
为 alice
帐户指定帐户标识符 0xd43593c715fdd31c61141abd04a99fd6822c8558
,注意帐户代码为空(0x
)。
为你使用 alice
开发帐户部署的合约指定合约地址 0x8a50db1e0f9452cfd91be8dc004ceb11cb08832f
,注意合约帐户代码是来自 Solidity 合约的字节码。
查看账户存储
你部署的 ERC-20 合约是基于 OpenZeppelin ERC-20 implementation。该合约包括一个构造函数,该构造函数生成最大数量的 tokens,并将它们存储在与合约创建者关联的帐户中。
要查询智能合约关联的账户存储信息:
在以 evm 作为要查询状态的 Chain State 中,选择 accountStorages。
指定 ERC-20 合约地址 0x8a50db1e0f9452cfd91be8dc004ceb11cb08832f
作为第一个参数。
将要读取的存储插槽指定为第二个参数 0x045c0350b9cf0df39c4b40400c965118df2dca5ce0fbcf0de4aafc099aea4a14
。地址的存储槽是使用基于槽位 0 和帐户标识符 0xd43593c715fdd31c61141abd04a99fd6822c8558
的 Substrate EVM utilities 计算得到的。该值应该返回的是 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
。如果你在部署合同之后检查 alice
帐户的余额,你会看到从帐户中提取了一笔费用,并且 getBalance(address, number)
调用返回一个类似于下面的值:
340,282,366,920,938,463,463,374,603,530,233,757,803
转移代币
到目前为止,你只使用了 alice
开发帐户。接下来是使用部署的合约将 tokens 转移到另一个帐户。
验证你的节点仍在运行,并且 Polkadot-JS application 已连接到该节点。
点击 Developer,然后选择 Extrinsics。
选择 ALICE 开发帐户作为用于提交交易的帐户。
选择 evm。
选择 call 调用 ERC-20 合约上的 transfer(address, uint256)
函数。
配置该函数的参数:
|For this|Specify this|
|:-----|:-----|
|source
|0xd43593c715fdd31c61141abd04a99fd6822c8558|
|target
|0x8a50db1e0f9452cfd91be8dc004ceb11cb08832f|
|input
|0xa9059cbb0000000000000000000000008eaf04151687736326c9fea17e25fc528761369300000000000000000000000000000000000000000000000000000000000000dd|
|value
|0|
|gasLimit
|4294967295|
|maxFeePerGas
|100000000|
source
表示持有 tokens 的帐户。在本例中,source
是合约创建者 alice
的 EVM 帐户。target
是将 tokens 从 alice
转移到 bob
的合约地址。input
参数是一个 EVM ABI-encoded 的函数调用,它指定执行转移的函数调用(0xa9059cbb
)和函数所需的参数。对于这个函数,参数是 bob
EVM 帐户标识符(0x8eaf04151687736326c9fea17e25fc5287613693
)和要转移的 tokens
数量(221 或 0xdd
十六进制)。本节教程中的 input 值是使用 Remix web IDE 计算得到的。
点击 Submit Transaction。
点击 Sign and Submit 对交易进行授权。
验证代币转移
提交交易后,合约将部署到网络上,你可以使用 Polkadot-JS application 查看有关它的信息。
验证你的节点仍在运行,并且 Polkadot-JS application 已连接到该节点。
点击 Network,然后选择 Explorer。
点击 evm.Executed 事件来验证已执行的合约地址为 0x8a50db1e0f9452cfd91be8dc004ceb11cb08832f
。
点击 Developer,然后选择 Chain State。
选择 evm 作为要查询的状态和 accountStorages。
查看存储合约地址 0x8a50db1e0f9452cfd91be8dc004ceb11cb08832f
和存储槽 0x045c0350b9cf0df39c4b40400c965118df2dca5ce0fbcf0de4aafc099aea4a14
。
0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff22
如果你在部署合约之后检查 alice
帐户的余额,你会看到从帐户中提取了一笔费用,getBalance(address, number)
调用返回一个类似于下面的值:
340,282,366,920,938,463,463,374,603,530,233,366,411
以太坊集成
通过使用 Frontier 项目的 crates,并将 EVM 和 Ethereum pallets 添加到你的运行时中,你可以构建一个基于 Substrate 的区块链,它支持 Ethereum-based 的账户,并允许执行 Solidity-based 的智能合约。
以太坊虚拟机(EVM)是一种虚拟计算机,其组件使以太坊网络参与者能够存储数据并就数据的状态达成一致。对于一个基于 Substrate 的区块链,EVM 的核心职责在 EVM pallet 中实现的。EVM pallet 负责执行以太坊合约字节码,这些智能合约是用像 Solidity 这样的高级语言编写的,然后编译为 EVM 字节码。下面的图表提供了一个简单的概述,以说明如何将 EVM pallet 和以太坊 RPC 调用集成到你的 Substrate 运行时中。
除了 EVM pallet,Ethereum pallet 还负责存储 Ethereum-formatted 的块、交易收据和交易状态。当用户提交原始以太坊交易时,通过在运行时调用 pallet_ethereum
中的 transact
函数,该交易首先会被转换为一个 Substrate transaction。
请注意,使用单独一个私钥不能使以太坊账户和 Substrate 账户直接兼容。关于以太坊帐户、密钥映射到 Substrate 帐户、密钥的信息,请参见 Moonbeam 文档中的 Unified Accounts。
以太坊指定的运行时APIs和RPCs
运行时存储所有可以查询的 Ethereum-formatted 的信息。你可以调用运行时并使用节点 RPC 服务器、运行时 API 和 RPC 客户端调用检索该信息。
Frontier区块导入
测试
本节中的主题重点介绍了测试区块链逻辑的工具和技术。
单元测试
在为运行时构建逻辑时,你需要例行地测试逻辑是否按预期工作。你可以使用 Rust 提供的单元测试框架为运行时创建单元测试。在创建一个或多个单元测试之后,可以使用 cargo test 命令执行测试。例如,可以通过运行以下命令运行为运行时创建的所有测试:
cargo test
有关使用Rust cargo test 命令和测试框架的更多信息,可运行以下命令:
cargo help test
在一个mock运行时中测试pallet日志
除了可以使用 Rust 测试框架进行单元测试之外,还可以通过构建 mock 运行时环境来验证运行时中的逻辑。配置类型 Test
被定义为 Rust 枚举,其中包含模拟运行时中使用的每个 pallet configuration traits 的实现。
frame_support::construct_runtime!(
pub enum Test where
Block = Block,
NodeBlock = Block,
UncheckedExtrinsic = UncheckedExtrinsic,
{
System: frame_system::{Pallet, Call, Config, Storage, Event<T>},
TemplateModule: pallet_template::{Pallet, Call, Storage, Event<T>},
}
);
impl frame_system::Config for Test {
// -- snip --
type AccountId = u64;
}
如果 Test
实现了 pallet_balances::Config
,则可能使用 u64
作为 Balance
类型。例如:
impl pallet_balances::Config for Test {
// -- snip --
type Balance = u64;
}
通过分配 pallet_balances::Balance
和 frame_system::AccountId
为 u64
,测试帐户和余额仅需要在模拟运行时跟踪一个(AccountId: u64, Balance: u64
)映射。
在一个mock运行时中测试存储
sp-io
crate 公开了一个 TestExternalities
实现,你可以使用它在一个模拟环境中测试存储。它是内存中的类型别名,它是substrate_state_machine substrate_state_machine
中基于 hashmap 的外部性实现,称为 TestExternalities
。
下面的示例演示定义一个名为 ExtBuilder
的结构体来构建 TestExternalities
的实例,并将块号设置为 1。
pub struct ExtBuilder;
impl ExtBuilder {
pub fn build(self) -> sp_io::TestExternalities {
let mut t = system::GenesisConfig::default().build_storage::<TestRuntime>().unwrap();
let mut ext = sp_io::TestExternalities::new(t);
ext.execute_with(|| System::set_block_number(1));
ext
}
}
要在单元测试中创建测试环境,调用构建方法来使用默认的创世配置生成 TestExternalities
。
#[test]
fn fake_test_example() {
ExtBuilder::default().build_and_execute(|| {
// ...test logics...
});
}
Externalities 的自定义实现允许你构建提供对外部节点特性访问的运行时环境。另一个例子可以在 offchain
模块中找到。offchain
模块维护自己的 Externalities 实现。
创世配置
在前面的示例中,ExtBuilder::build()
方法使用默认的起源配置来构建模拟运行时环境。在许多情况下,在测试之前设置存储是很方便的。例如你可能希望在测试之前预设置帐户的余额。
在 frame_system::Config
的实现中,AccountId
和 Balance
都被设置为 u64
。你可以将 (u64, u64)
一对放在 balances
vec中,以 seed (AccountId, Balance)
一对作为帐户 balances。例如:
impl ExtBuilder {
pub fn build(self) -> sp_io::TestExternalities {
let mut t = frame_system::GenesisConfig::default().build_storage::<Test>().unwrap();
pallet_balances::GenesisConfig::<Test> {
balances: vec![
(1, 10),
(2, 20),
(3, 30),
(4, 40),
(5, 50),
(6, 60)
],
}
.assimilate_storage(&mut t)
.unwrap();
let mut ext = sp_io::TestExternalities::new(t);
ext.execute_with(|| System::set_block_number(1));
ext
}
}
在本例中,账户 1 有余额 10,账户 2 有余额 20,以此类推。
用于定义 pallet 的创世配置的确切结构取决于 pallet 的 GenesisConfig
结构定义。例如在 Balances pallet 中,它被定义为:
pub struct GenesisConfig<T: Config<I>, I: 'static = ()> {
pub balances: Vec<(T::AccountId, T::Balance)>,
}
区块生产
模拟区块生产以验证预期行为在区块生产中是否存在是有用的。
一种简单的方法是,使用 System::block_number()
作为唯一输入,在来自所有模块的 on_initialize
和 on_finalize
调用之间递增 System 模块的区块号。尽管对运行时代码来说缓存对存储或系统模块的调用是很重要的,但是测试环境脚手架应该优先考虑可读性,以促进未来可继续容易的维护。
fn run_to_block(n: u64) {
while System::block_number() < n {
if System::block_number() > 1 {
ExamplePallet::on_finalize(System::block_number());
System::on_finalize(System::block_number());
}
System::set_block_number(System::block_number() + 1);
System::on_initialize(System::block_number());
ExamplePallet::on_initialize(System::block_number());
}
}
on_finalize
和 on_initialize
方法只能从 ExamplePallet
调用,如果 pallet trait 实现了 frame_support::traits::{OnInitialize, OnFinalize}
traits,分别在每个块之前和之后执行运行时方法中编码的逻辑。
然后按以下方式调用此函数。
#[test]
fn my_runtime_test() {
with_externalities(&mut new_test_ext(), || {
assert_ok!(ExamplePallet::start_auction());
run_to_block(10);
assert_ok!(ExamplePallet::end_auction());
});
}
调试
在软件开发的各个阶段,调试都是必要的,区块链也不例外。大多数常见的 Rust 调试工具同样也适用于 Substrate。
日志工具
你可以使用 Rust 的日志 API 调试运行时,它附带了许多宏,包括 debug
和 info
。
例如在更新带有 log
crate 的 pallet 的 Cargo.toml
的文件之后,只需使用 log::info!
log 到你的 console:
pub fn do_something(origin) -> DispatchResult {
let who = ensure_signed(origin)?;
let my_val: u32 = 777;
Something::put(my_val);
log::info!("called by {:?}", who);
Self::deposit_event(RawEvent::SomethingStored(my_val, who));
Ok(())
}
可打印trait
可打印trait 是一种在 no_std
和在 std
情况下从运行时打印的方法。print
函数适用于实现了 Printable
trait 的任何类型。Substrate 默认为某些类型(u8
, u32
, u64
, usize
, &[u8]
, &str
)实现此特性。你也可以为你的自定义类型实现它,下面是一个使用 node-template 作为示例代码库为 pallet 的 Error
类型实现它的示例。
use sp_runtime::traits::Printable;
use sp_runtime::print;
#[frame_support::pallet]
pub mod pallet {
// The pallet's errors
#[pallet::error]
pub enum Error<T> {
/// Value was None
NoneValue,
/// Value reached maximum and cannot be incremented further
StorageOverflow,
}
impl<T: Config> Printable for Error<T> {
fn print(&self) {
match self {
Error::NoneValue => "Invalid Value".print(),
Error::StorageOverflow => "Value Exceeded and Overflowed".print(),
_ => "Invalid Error Case".print(),
}
}
}
}
/// takes no parameters, attempts to increment storage value, and possibly throws an error
pub fn cause_error(origin) -> dispatch::DispatchResult {
// Check it was signed and get the signer. See also: ensure_root and ensure_none
let _who = ensure_signed(origin)?;
print!("My Test Message");
match Something::get() {
None => {
print(Error::<T>::NoneValue);
Err(Error::<T>::NoneValue)?
}
Some(old) => {
let new = old.checked_add(1).ok_or(
{
print(Error::<T>::StorageOverflow);
Error::<T>::StorageOverflow
})?;
Something::put(new);
Ok(())
},
}
}
运行带有 RUST_LOG 环境变量的节点二进制文件以打印值。
RUST_LOG=runtime=debug ./target/release/node-template --dev
每次调用运行时函数时,这些值都打印在终端或标准输出中。
2020-01-01 tokio-blocking-driver DEBUG runtime My Test Message <-- str implements Printable by default
2020-01-01 tokio-blocking-driver DEBUG runtime Invalid Value <-- the custom string from NoneValue
2020-01-01 tokio-blocking-driver DEBUG runtime DispatchError
2020-01-01 tokio-blocking-driver DEBUG runtime 8
2020-01-01 tokio-blocking-driver DEBUG runtime 0 <-- index value from the Error enum definition
2020-01-01 tokio-blocking-driver DEBUG runtime NoneValue <-- str which holds the name of the ident of the error
请记住,向运行时添加打印函数会增加 Rust 和 Wasm 二进制文件的大小,在生产环境中不要加入调试代码。
Substrate自身的打印函数
对于传统用例,Substrate 为 Print
debugging(或 tracing)提供了额外的工具。你可以使用 print
function 记录运行时执行的状态。
use sp_runtime::print;
// --snip--
pub fn do_something(origin) -> DispatchResult {
print!("Execute do_something");
let who = ensure_signed(origin)?;
let my_val: u32 = 777;
Something::put(my_val);
print!("After storing my_val");
Self::deposit_event(RawEvent::SomethingStored(my_val, who));
Ok(())
}
// --snip--
使用 RUST_LOG
环境变量启动链,以查看打印日志。
RUST_LOG=runtime=debug ./target/release/node-template --dev
如果 Error 被触发,这些值将打印在终端或标准输出中。
2020-01-01 00:00:00 tokio-blocking-driver DEBUG runtime Execute do_something
2020-01-01 00:00:00 tokio-blocking-driver DEBUG runtime After storing my_val
If std
传统的 print
函数允许你打印并获得 Printable
trait 的实现。然而有一些传统的用例,可能想要做更多的事情,而不仅仅是打印,或者仅仅为了调试目的,而不必考虑 Substrate-specific traits。 if_std!
macro 在这种情况下是有用的。
使用此宏的一个警告是,只有当你实际运行 runtime 的 native 版本时,内部的代码才会执行。
use sp_std::if_std; // Import into scope the if_std! macro.
println!
语句应该在 if_std
宏中。
#[pallet::call]
impl<T: Config<I>, I: 'static> Pallet<T, I> {
// --snip--
pub fn do_something(origin) -> DispatchResult {
let who = ensure_signed(origin)?;
let my_val: u32 = 777;
Something::put(my_val);
if_std! {
// This code is only being compiled and executed when the `std` feature is enabled.
println!("Hello native world!");
println!("My value is: {:#?}", my_val);
println!("The caller account is: {:#?}", who);
}
Self::deposit_event(RawEvent::SomethingStored(my_val, who));
Ok(())
}
// --snip--
}
每次调用运行时函数时,这些值都打印在终端或标准输出中。
$ 2020-01-01 00:00:00 Substrate Node
2020-01-01 00:00:00 version x.y.z-x86_64-linux-gnu
2020-01-01 00:00:00 by Anonymous, 2017, 2020
2020-01-01 00:00:00 Chain specification: Development
2020-01-01 00:00:00 Node name: my-node-007
2020-01-01 00:00:00 Roles: AUTHORITY
2020-01-01 00:00:00 Imported 999 (0x3d7a…ab6e)
# --snip--
-> Hello native world!
-> My value is: 777
-> The caller account is: d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d (5GrwvaEF...)
# --snip--
2020-01-01 00:00:00 Imported 1000 (0x3d7a…ab6e)
基准测试
Substrate 和 FRAME 为你的区块链开发自定义逻辑提供了一个灵活的框架。这种灵活性使你能够设计复杂的交互式 pallet 并实现复杂的运行时逻辑。然而,确定分配给 pallet 中函数的适当权重可能是一项困难的任务。基准测试使你能够测量在运行时和不同条件下执行不同函数所需的时间。如果你使用基准测试为函数调用分配准确的权重,你可以防止你的区块链过载、无法生成区块或受到恶意参与者的拒绝服务(DoS)攻击。
为什么要对pallet进行基准测试
理解执行不同函数所需的计算资源是很重要的,包括像 on_initialize
和 verify_unsigned
这样的运行时函数,以保证运行时的安全,并允许运行时根据可用的资源需要包含哪些交易还是排除哪些交易。
基于可用资源包含或排除交易的能力确保运行时可以继续生成和导入区块,而不会中断服务。例如如果你有一个需要密集计算的函数调用,执行该调用可能会超过生成或导入块所允许的最大时间,从而中断块处理过程或完全停止区块链进程。基准测试帮助你验证不同函数所需的执行时间是否在合理的范围内。
同样恶意用户可能会试图通过反复执行需要密集计算或无法准确反映其所需计算的函数调用来破坏网络服务。如果执行函数调用的成本无法准确反映所消耗的计算资源,则无法阻止恶意用户攻击网络。由于基准测试可帮助你评估与执行交易相关的权重,因此它还可以帮助你确定适当的交易费用。根据你的基准测试,你可以设置代表通过对区块链执行指定调用所消耗的资源的费用。
开发一个线性模型
在高水平上,基准测试需要执行以下步骤:
- 编写自定义基准测试逻辑,为一个函数执行指定代码路径。
- 在 WebAssembly 执行环境中使用指定的硬件集合和指定的运行时配置执行基准测试逻辑。
- 在可能影响函数所需执行时间的受控范围内执行基准逻辑。
- 对函数中的每个组件执行多次基准测试,以隔离和删除异常值。
根据通过执行基准逻辑生成的结果,基准测试工具创建了一个函数的线性模型,该模型跨越所有组件。函数的线性模型使你能够估计执行指定代码路径所需的时间,并做出明智的决策,而无需在运行时实际花费大量资源。基准测试假设所有交易都具有线性复杂性,因为较高复杂性的函数被认为对运行时是危险的,因为随着运行时状态或输入变得过于复杂,这些函数的权重可能会爆炸。
基准测试和权重
如交易、权重和费用中所述,基于 Substrate 的链使用权重概念表示在区块中执行交易所需的时间。在交易中执行任何特定调用所需的时间取决于几个因素,包括以下几项:
- 计算复杂性。
- 存储复杂性。
- 数据库读写操作需要的。
- 使用的硬件。
要计算交易的适当权重,可以使用基准参数来测量在不同硬件上执行函数调用所需的时间,使用不同的变量值,并重复多次。然后你可以使用基准测试的结果来建立近似的最坏情况权重,以表示执行每个函数调用和每个代码路径所需的资源。费用基于最坏情况权重。如果实际调用的性能优于最坏情况,则调整权重并返回任何超额费用。
因为权重是基于指定物理机器的计算时间的通用度量单位,所以任何函数的权重都可以基于用于基准测试的指定硬件而改变。
通过建模每个运行时函数的预期权重,区块链能够计算在特定时间段内可以执行多少交易或系统级调用。
在 FRAME 内,可以调度的每个函数调用必须具有 #[weight]
注释,该注释可以返回给定输入的该函数最坏情况下执行的预期权重。基准测试框架会自动为你生成一个包含这些公式的文件。
基准测试工具
基准测试框架提供的工具可帮助你在运行时添加、测试、运行和分析函数的 benchmarks。帮助你确定执行函数调用所需时间的基准测试工具包括:
-
基准测试宏,帮助你编写、测试和添加运行时基准测试。
-
线性回归分析函数用于处理基准测试数据。
-
命令行接口(CLI)使你能够在节点上执行基准测试。
编译节点时,默认情况下禁用端到端基准测试管道。如果要运行基准测试,则需要编译一个带有 runtime-benchmarks
Rust 特性标志的节点。
编写基准测试
编写运行时基准测试类似于为 pallet 编写单元测试。与单元测试一样,基准测试必须在代码中执行特定的逻辑路径。在单元测试中,你将检查代码的成功和失败结果。对于基准测试,你希望执行 most computationally intensive 的路径。
在编写基准测试时,你应该考虑可能影响函数复杂性的特定条件,如存储或运行时状态。例如如果在 For
循环中触发更多迭代会增加数据库读写操作的数量,则应设置触发此条件的基准测试,以更准确地表示函数将如何执行。
如果一个函数根据用户输入或其他条件执行不同的代码路径,你可能不知道哪个路径是计算最密集的路径。为了帮助你了解代码中的复杂性可能变得难以管理的地方,你应该为每个可能的执行路径创建一个基准测试。基准测试可以帮助你确定代码中可能需要强制识别的边界,例如通过限制向量中的元素数量或限制 for
循环中的迭代次数来控制用户如何与 pallet 交互。
你可以在所有预构建的 FRAME pallets 中找到端到端基准的的示例。
测试基准测试
你可以使用为单元测试 pallet 创建类似的模拟运行时执行基准测试。使用的基准测试宏在你的 benchmarking.rs
模块中可以自动的生成测试函数。例如:
fn test_benchmark_[benchmark_name]<T>::() -> Result<(), &'static str>
你可以将基准测试函数添加到单元测试中,并确保函数的结果是 Ok(())
。
验证块
通常你只需要检查基准测试是否返回 Ok(())
,因为该结果表明函数已成功执行。但是如果你想要验证任何最终条件,比如运行时的最终状态,你可以选择在基准测试中包含一个 verify
块。额外的 verify
块不会影响你的最终基准测试过程的结果。
使用benchmarks运行单元测试
要运行基准测试,你需要指定要测试的包并启用 runtime-benchmarks
特性。例如你可以通过运行以下命令来测试 Balances pallet:
cargo test --package pallet-balances --features runtime-benchmarks
添加基准测试
每个 pallet 中包含的基准测试不会自动添加到节点中。要执行这些基准测试,你需要实现框架 frame_benchmarking::Benchmark
trait。你可以在 Substrate 节点中看到如何操作的示例。
假设你的节点上已经设置了一些基准测试,你只需要将 pallet 添加到 define_benchmarks!
宏:
#[cfg(feature = "runtime-benchmarks")]
mod benches {
define_benchmarks!(
[frame_benchmarking, BaselineBench::<Runtime>]
[pallet_assets, Assets]
[pallet_babe, Babe]
...
[pallet_mycustom, MyCustom]
...
在添加了 pallet 之后,使用 runtime-benchmarks
特性标志编译节点二进制文件。例如:
cd bin/node/cli
cargo build --profile=production --features runtime-benchmarks
production
配置文件应用了各种编译器优化。这些优化大大降低了编译过程的速度。如果你只是在测试,不需要最终的指标,请使用 --release
命令行选项而不是 production
配置文件。
运行基准测试
在编译了启用基准测试的节点二进制代码之后,你需要执行基准测试。如果使用 production
配置文件编译节点,则可以通过运行以下命令列出可用的基准测试:
./target/production/node-template benchmark pallet --list
对所有pallets中的所有函数进行基准测试
要执行运行时的所有基准测试,可以运行类似于以下的命令:
./target/production/node-template benchmark pallet \
--chain dev \
--execution=wasm \
--wasm-execution=compiled \
--pallet "*" \
--extrinsic "*" \
--steps 50 \
--repeat 20 \
--output pallets/all-weight.rs
在本例中该命令创建一个输出文件,名为 all-weight.rs
为你的运行时实现了 WeightInfo
trait。
在pallet中对特定的函数进行基准测试
要在特定 pallet 中为特定函数执行基准测试,可以运行类似于下面的命令:
./target/production/node-template benchmark pallet \
--chain dev \
--execution=wasm \
--wasm-execution=compiled \
--pallet pallet_balances \
--extrinsic transfer \
--steps 50 \
--repeat 20 \
--output pallets/transfer-weight.rs
该命令为选定的 pallet 创建一个输出文件,例如 transfer-weight.rs
为 pallet_balances
pallet 实现了 WeightInfo
trait。
使用一个格式化基准测试的模板
基准测试命令行接口使用 Handlebars 模板来格式化最终输出文件。你可以选择传递 --template
命令行选项来指定自定义模板而不是默认模板。在模板中,你可以访问基准测试命令行接口中 TemplateData
结构提供的所有数据。
输出生成中包含了一些自定义 Handlebars 帮助程序:
underscore
:将下划线添加到字符串右侧的第三个字符。主要用于界定大数。join
:为模板加入字符串数组,以空格分隔的字符串。主要用于连接传递给 CLI 的所有参数。
要获取 benchmark
子命令的完整列表,请运行:
./target/production/node-template benchmark --help
要获取 benchmark pallet
子命令可用选项的完整列表,请运行:
./target/production/node-template benchmark pallet --help
fabric
介绍
一般来说,区块链是一个由分布式网络中的节点维护的不可篡改的账本。这些节点通过执行被共识协议验证过的交易来各自维护一个账本的副本,账本以区块的形式存在,每个区块通过哈希和之前的区块相连。
第一个被广为人知的区块链应用是加密货币比特币,而其他应用都是从它衍生出来的。以太坊是另一种加密货币,它采用了不同方法,整合了许多类似比特币的特征,但是新增了智能合约为分布式应用创建了一个平台。比特币和以太坊属于同一类区块链,我们将其归类为公共非许可(Public Permissionless)区块链技术。这些基本上都是公共网络,允许任何人在上面匿名互动。
随着比特币、以太坊和其他一些衍生技术的普及,越来越多的人想要将区块链基础技术、分布式账本和分布式应用平台用到企业业务中去。但是,许多企业业务对性能要求较高,目前非许可区块链技术无法达到。此外,在许多业务中,对参与者身份要求比较严格,如在金融交易业务中,必须遵循“了解客户(Know-Your-Customer,KYC)”和“反洗钱(Anti-Money Laundering,AML)”的相关法规。
对于企业应用,我们需要考虑以下要求:
- 参与者必须是已认证的或者可识别的
- 网络需要获得许可
- 高交易吞吐量性能
- 交易确认低延迟
- 与商业交易有关的交易和数据的隐私和机密性
- 当前许多早期的区块链平台正在为企业应用做调整,而 Hyperledger Fabric 从一开始就设计为企业用途。下面的部分描述了 Hyperledger Fabric(Fabric)与其他区块链平台的不同,并讲解了其架构设计的一些理念。
Hyperledger Fabric
Hyperledger Fabric 是一个开源的企业级许可分布式账本技术(Distributed Ledger Technology,DLT)平台,专为在企业环境中使用而设计,与其他流行的分布式账本或区块链平台相比,它有一些主要的区别。
一个主要区别是 Hyperledger 是在 Linux 基金会下建立的,该基金会本身在开放式治理的模式下培育开源项目的历史悠久且非常成功,发展了强大的可持续社区和繁荣的生态系统。Hyperledger 由多元化的技术指导委员会进行管理,Hyperledger Fabric 项目由多个组织的不同的维护人员管理。从第一次提交以来,它的开发社区已经发展到超过35个组织和近200个开发人员。
Fabric 具有高度模块化和可配置的架构,可为各行各业的业务提供创新性、多样性和优化,其中包括银行、金融、保险、医疗保健、人力资源、供应链甚至数字音乐分发。
Fabric 是第一个支持通用编程语言编写智能合约(如 Java、Go 和 Node.js)的分布式账本平台,不受限于特定领域语言(Domain-Specific Languages,DSL)。这意味着大多数企业已经拥有开发智能合约所需的技能,并且不需要额外的培训来学习新的语言或特定领域语言。
Fabric 平台也是许可的,这意味着它与公共非许可网络不同,参与者彼此了解而不是匿名的或完全不信任的。也就是说,尽管参与者可能不会完全信任彼此(例如,同行业中的竞争对手),但网络可以在一个治理模式下运行,这个治理模式是建立在参与者之间确实存在的信任之上的,如处理纠纷的法律协议或框架。
该平台最重要的区别之一是它支持可插拔的共识协议,使得平台能够更有效地进行定制,以适应特定的业务场景和信任模型。例如,当部署在单个企业内或由可信任的权威机构管理时,完全拜占庭容错的共识可能是不必要的,并且大大降低了性能和吞吐量。在这种的情况下,崩溃容错(Crash Fault-Tolerant,CFT)共识协议可能就够了,而在去中心化的场景中,可能需要更传统的拜占庭容错(Byzantine Fault Tolerant,BFT)共识协议。
Fabric 可以利用不需要原生加密货币的共识协议来激励昂贵的挖矿或推动智能合约执行。不使用加密货币会降低系统的风险,并且没有挖矿操作意味着可以使用与任何其他分布式系统大致相同的运营成本来部署平台。
这些差异化设计特性的结合使 Fabric 成为当今交易处理和交易确认延迟方面性能较好的平台之一,并且它实现了交易的隐私和保密以及智能合约(Fabric 称之为“链码”)。
让我们更详细地探索这些区别。
模块化
Hyperledger Fabric 被专门设计为模块化架构。无论是可插拔的共识、可插拔的身份管理协议(如 LDAP 或 OpenID Connect)、密钥管理协议还是加密库,该平台的核心设计旨在满足企业业务需求的多样性。
总体来看,Fabric 由以下模块化的组件组成:
- 可插拔的排序服务对交易顺序建立共识,然后向节点广播区块;
- 可插拔的成员服务提供者负责将网络中的实体与加密身份相关联;
- 可选的P2P gossip 服务通过排序服务将区块发送到其他节点;
- 智能合约(“链码”)隔离运行在容器环境(例如 Docker)中。它们可以用标准编程语言编写,但不能直接访问账本状态;
- 账本可以通过配置支持多种 DBMS;
- 可插拔的背书和验证策略,每个应用程序可以独立配置。 业界一致公认,没有“可以一统天下的链(one blockchain to rule them all)”。Hyperledger Fabric 可以通过多种方式进行配置,以满足不同行业应用的需求。
许可和非许可区块链
在一个非许可区块链中,几乎任何人都可以参与,每个参与者都是匿名的。在这样的情况下,区块链状态达到不可变的区块深度前不存在信任。为了弥补这种信任的缺失,非许可区块链通常采用“挖矿”或交易费来提供经济激励,以抵消参与基于“工作量证明(PoW)”的拜占庭容错共识形式的特殊成本。
另一方面,许可区块链在一组已知的、已识别的且经常经过审查的参与者中操作区块链,这些参与者在产生一定程度信任的治理模型下运作。许可区块链提供了一种方法来保护具有共同目标,但可能彼此不完全信任的一组实体之间的交互。通过依赖参与者的身份,许可区块链可以使用更传统的崩溃容错(CFT)或拜占庭容错(BFT)共识协议,而不需要昂贵的挖掘。
另外,在许可的情况下,降低了参与者故意通过智能合约引入恶意代码的风险。首先,参与者彼此了解对方以及所有的操作,无论是提交交易、修改网络配置还是部署智能合约都根据网络中已经确定的背书策略和相关交易类型被记录在区块链上。与完全匿名相比,可以很容易地识别犯罪方,并根据治理模式的条款进行处理。
智能合约
智能合约,在 Fabric 中称之为“链码”,作为受信任的分布式应用程序,从区块链中获得信任,在节点中达成基本共识。它是区块链应用的业务逻辑。
有三个关键点适用于智能合约,尤其是应用于平台时:
多个智能合约在网络中同时运行, 它们可以动态部署(很多情况下任何人都可以部署), 应用代码应视为不被信任的,甚至可能是恶意的。 大多数现有的具有智能合约能力的区块链平台遵循顺序执行架构,其中共识协议:
验证并将交易排序,然后将它们传播到所有的节点, 每个节点按顺序执行交易。 几乎所有现有的区块链系统都可以找到顺序执行架构,从非许可平台,如 Ethereum(基于 PoW 共识)到许可平台,如 Tendermint、Chain 和 Quorum 。
采用顺序执行架构的区块链执行智能合约的结果一定是确定的,否则,可能永远不会达成共识。为了解决非确定性问题,许多平台要求智能合约以非标准或特定领域的语言(例如 Solidity)编写,以便消除非确定性操作。这阻碍了平台的广泛采用,因为它要求开发人员学习新语言来编写智能合约,而且可能会编写错误的程序。
此外,由于所有节点都按顺序执行所有交易,性能和规模被限制。事实上系统要求智能合约代码要在每个节点上都执行,这就需要采取复杂措施来保护整个系统免受恶意合约的影响,以确保整个系统的弹性。
一种新方法
针对交易 Fabric 引入了一种新的架构,我们称为执行-排序-验证。为了解决顺序执行模型面临的弹性、灵活性、可伸缩性、性能和机密性问题,它将交易流分为三个步骤:
执行一个交易并检查其正确性,从而给它背书, 通过(可插拔的)共识协议将交易排序, 提交交易到账本前先根据特定应用程序的背书策略验证交易 这种设计与顺序执行模式完全不同,因为 Fabric 在交易顺序达成最终一致前执行交易。
在 Fabric 中,特定应用程序的背书策略可以指定需要哪些节点或多少节点来保证给定的智能合约正确执行。因此,每个交易只需要由满足交易的背书策略所必需的节点的子集来执行(背书)。这样可以并行执行,从而提高系统的整体性能和规模。第一阶段也消除了任何非确定性,因为在排序之前可以过滤掉不一致的结果。
因为我们已经消除了非确定性,Fabric 是第一个能使用标准编程语言的区块链技术。
隐私和保密性
正如我们所讨论的,在一个公共的、非许可的区块链网络中,利用 PoW 作为其共识模型,交易在每个节点上执行。这意味着合约本身和他们处理的交易数据都不保密。每个交易以及实现它的代码,对于网络中的每个节点都是可见的。在这种情况下,我们得到了基于 PoW 的拜占庭容错共识却牺牲了合约和数据的保密性。
对于许多商业业务而言,缺乏保密性就会有问题。例如,在供应链合作伙伴组成的网络中,作为巩固关系或促进额外销售的手段,某些消费者可能会获得优惠利率。如果每个参与者都可以看到每个合约和交易,在一个完全透明的网络中就不可能维持这种商业关系,因为每个消费者都会想要优惠利率。
第二个例子考虑到证券行业,无论一个交易者建仓(或出仓)都会不希望她的竞争对手知道,否则他们将会试图入局,进而影响交易者的策略。
为了解决缺乏隐私和机密性的问题来满足企业业务需求,区块链平台采用了多种方法。所有方法都需要权衡利弊。
加密数据是提供保密性的一种方法;然而,在利用 PoW 达成共识的非许可网络中,加密数据位于每个节点上。如果有足够的时间和计算资源,加密可能会被破解。对于许多企业业务而言,不能接受信息可能受损的风险。
零知识证明(Zero Knowledge Proofs,ZKP)是正在探索解决该问题的另一个研究领域。目前这里的权衡是计算 ZKP 需要相当多的时间和计算资源。因此,在这种情况下需要权衡资源消耗与保密性能。
如果可以使用其他共识,或许可以探索将机密信息限制于授权节点内。
Hyperledger Fabric 是一个许可平台,通过其通道架构和 私有数据特性实现保密。在通道方面,Fabric 网络中的成员组建了一个子网络,在子网络中的成员可以看到其所参与到的交易。因此,参与到通道的节点才有权访问智能合约(链码)和交易数据,以此保证了隐私性和保密性。私有数据通过在通道中的成员间使用集合,实现了和通道相同的隐私能力并且不用创建和维护独立的通道。
可插拔共识
交易的排序被委托给模块化组件以达成共识,该组件在逻辑上与执行交易和维护帐本的节点解耦。具体来说,就是排序服务。由于共识是模块化的,可以根据特定部署或解决方案的信任假设来定制其实现。这种模块化架构允许平台依赖完善的工具包进行 CFT(崩溃容错)或 BFT(拜占庭容错)的排序。
Fabric 目前提供了一种基于etcd 库 中 Raft 协议 的 CFT 排序服务的实现。更多当前可用的排序服务请查阅排序服务概念文档。
另外,请注意,这些并不相互排斥。一个 Fabric 网络中可以有多种排序服务以支持不同的应用或应用需求。
性能和可扩展性
一个区块链平台的性能可能会受到许多因素的影响,例如交易大小、区块大小、网络大小以及硬件限制等。Hyperledger Fabric 性能和规模工作组 正在开发一个叫 Hyperledger Caliper的基准测试框架。
已经发表了一些研究和测试 Hyperledger Fabric 性能的文章。最新的一篇是 将 Fabric 扩展到 20000 笔交易每秒(Scaled Fabric to 20,000 transactions per second)。
关键概念
Hyperledger Fabric 模型
本节讲述了 Hyperledger Fabric 的关键设计特性,实现了全方位、可定制的企业级区块链解决方案:
- 资产 — 资产是可以通过网络交换的几乎所有具有价值的东西,从食品到古董车、货币期货。
- 链码 — 链码执行与交易排序分离,限制了跨节点类型所需的信任和验证级别,并优化了网络可扩展性和性能。
- 账本特性 — 不可变的共享账本为每个通道编码整个交易历史记录,并包括类似 SQL 的查询功能,以便高效审计和解决争议。
- 隐私 — 通道和私有数据集合实现了隐私且机密的多边交易,这些交易通常是在共同网络上交换资产的竞争企业和受监管行业所要求的。
- 安全和成员服务 — 许可成员资格提供可信的区块链网络,参与者知道所有交易都可以由授权的监管机构和审计员检测和跟踪。
- 共识 — 达成共识的独特方法可实现企业所需的灵活性和可扩展性。
资产
资产的范围可以从有形(房地产和硬件)到无形资产(合同和知识产权)。Hyperledger Fabric 提供使用链码交易来修改资产的功能。
资产在 Hyperledger Fabric 中表示为键值对的集合,状态更改记录为 Channel 账本上的交易。资产可以用二进制或 JSON 格式表示。
链码
链码是定义单项或多项资产的软件,和能修改资产的交易指令;换句话说,它是业务逻辑。链码强制执行读取或更改键值对或其他状态数据库信息的规则。链码函数针对账本的当前状态数据库执行,并通过交易提案启动。链码执行会产生一组用于写入的键值对(写集),可以被提交到网络并应用于所有节点的账本。
账本特性
账本是 Fabirc 中所有状态转换的有序的防篡改的记录。状态转换是参与方提交的链码调用(“交易”)的结果。每个交易都会生成一组资产键值对,这些键值对以创建、更新或删除形式提交到账本。
账本由区块链(“链”)组成,用于以区块的形式存储不可变的顺序记录,以及用于维护当前 Fabirc 状态的状态数据库。每个通道有一个账本。每个节点为其所属的每个通道维护一个账本的副本。
Fabric 账本的一些特点:
- 使用基于键的查找、范围查询和组合键查询来查询和更新账本
- 使用富查询语言进行只读查询(如果使用 CouchDB 作为状态数据库)
- 只读历史记录查询(查询一个键的账本历史记录)用于支持数据溯源场景
- 交易包括链码读取键/值(读集)的版本以及链码写入键/值(写集)的版本
- 交易包含每个背书节点的签名,并被提交给排序服务
- 交易按顺序打包到区块,并被排序服务“分发”到通道上的节点
- 节点根据背书策略验证交易并执行策略
- 在附加一个区块之前,会执行一次版本检查,以确保被读取的资产的状态自链码执行以来未发生更改
- 一旦交易被验证并提交,就具有不变性
- 一个通道的账本包含一个配置区块,用于定义策略、访问控制列表和其他相关信息
- 通道包含 Membership Service Provider 的实例,允许从不同的证书颁发机构(CA)生成加密材料 查看 账本 主题来更深地了解数据库、存储结构和 “查询能力”。
隐私
Hyperledger Fabric 在每个通道上使用不可变的账本,以及可操纵和修改资产当前状态(即更新键值对)的链码。账本存在于通道范围内,它可以在整个网络中共享(假设每个参与者都在同一个公共通道上),也可以被私有化,仅包括一组特定的参与者。
在后一种情况下,这些参与者将创建一个单独的通道,从而隔离他们的交易和账本。为了想在完全透明和隐私之间获得平衡的场景,可以仅在需要访问资产状态以执行读取和写入的节点上安装链码(换句话说,如果未在节点上安装链码,它将无法与账本正确连接)。
当该通道上的组织子集需要对其交易数据保密时,私有数据集合用于将此数据隔离在私有数据库中,在逻辑上与通道账本分开,只有经授权的组织子集才能访问。
因此,通道在更广泛的网络上保持交易的私密性,而集合则在通道上的组织子集之间保持数据的私密性。
为了进一步模糊数据,在将交易发送到排序服务并将区块附加到账本之前,可以使用诸如 AES 之类的通用加密算法对链码内的值进行加密(部分或全部)。一旦加密数据被写入账本,它就只能由拥有用于生成密文的相应密钥的用户解密。
有关如何在区块链网络上实现隐私的更多详细信息,请参阅 私有数据 主题。
安全和成员服务
Hyperledger Fabric 支持一个交易网络,在这个网络中,所有参与者都拥有已知的身份。公钥基础设施用于生成与组织、网络组件以及终端用户或客户端应用程序相关联的加密证书。因此,可以在更广泛的网络和通道级别上操纵和管理数据访问控制。Hyperledger Fabric 的这种“许可”概念,加上通道的存在和功能,有助于解决隐私和机密性要求较高的场景。
请参阅 成员服务提供者 (MSP) 主题,以更好地了解加密实现,以及 Hyperledger Fabric 中使用的签名、验证、身份认证方法。
共识
最近,在分布式账本技术中,共识已成为单个函数内特定算法的同义词。然而,共识不仅包括简单地就交易顺序达成一致,Hyperledger Fabric 通过其在整个交易流程中的基本角色,从提案和背书到排序、验证和提交,突出了这种区别。简而言之,共识被定义为组成区块的一组交易的正确性的闭环验证。
当区块中交易的顺序和结果满足明确的策略标准检查时,最终会达成共识。这些制衡措施是在交易的生命周期内进行的,包括使用背书策略来规定哪些特定成员必须背书某个交易类别,以及使用系统链码来确保这些策略得到执行和维护。在提交之前,节点将使用这些系统链码来确保存在足够的背书,并且它们来自适当的实体。此外,在将包含交易的任何区块附加到账本之前,将进行版本检查,以确保在此期间,账本的当前状态是能与交易中的信息达成共识的。该最终检查可防止双重花费操作和可能危及数据完整性的其他威胁,并允许针对非静态变量执行功能。
除了众多的背书、验证和版本检查外,交易流的各个方向上还会发生持续的身份验证。访问控制列表是在网络的分层上实现的(排序服务到通道),并且当一个交易提议通过不同的架构组件时,有效负载会被反复签名、验证和认证。总而言之,共识并不仅仅局限于一批交易的商定顺序;相反,它的首要特征是交易从提案到提交的过程中不断进行核查而附带实现的。
查看 交易流程 以获得共识的直观表示。
区块链网络
Move 与 Solidity
Move教程
欢迎来到 Move 语言教程,在本教程中,我们通过一些具体的步骤进行 Move 语言代码的开发,包括 Move 模块的设计、实现、单元测试和形式化验证。
整个过程共包含9个步骤:
0.安装 Move 开发环境
如果你还没有安装过 Move,首先打开命令终端(terminal) 并clone Move代码库:
git clone https://github.com/move-language/move.git
进入到 move
文件夹下,执行 dev_setup.sh
脚本:
cd move
./scripts/dev_setup.sh -ypt
根据脚本命令的提示,按顺序安装 Move 的所有依赖项。 脚本将会将(move命令所在路径)环境变量写入到 ~/.profile
文件中。
执行如下命令使环境变量生效:
source ~/.profile
然后执行如下命令来安装 Move 命令行工具:
cargo install --path language/tools/move-cli
通过如下运行命令可以检查 move 命令是否可正常:
move --help
你应该会看到类似这样的内容以及许多命令的列表和描述:
move-package
Execute a package command. Executed in the current directory or the closest containing Move package
USAGE:
move [OPTIONS] <SUBCOMMAND>
OPTIONS:
--abi Generate ABIs for packages
...
如果想了解有支持哪引命令及其作用, 执行命令或子命令时添加 --help
标记,此时会打印帮助文档。
在执行下一步骤之前,请先执行 cd 命令进入到教程对应目录下:
cd <path_to_move>/language/documentation/tutorial
Visual Studio Code Move 支持
Visual Studio Code 有正式的 Move 语言支持, 您需要先安装 move analyzer :cargo install --path language/move-analyzer
现在你可以打开 VS Code 并安装 Move 扩展插件了,在扩展页面下找到 move-analyzer 并安装即可。关于扩展的详细信息可以查看扩展的README。
1.编写第一个 Move 模块
切换当前目录到 step_1/BasicCoin
下,你将看到 sources
子目录 -- 这个包(package)下所有的 Move 代码都在此目录中,同时你还会看到一个 Move.toml
文件。该文件指定当前包的依赖列表和其他信息。 如果您熟悉 Rust
和 Cargo
,那 Move.toml
文件类似 Cargo.toml
文件,sources
目录类似 src
目录(它们的作用是一样的)
来一起看看 Move 语言代码内容! 用你的编辑器打开 sources/FirstModule.move
文件,会看到如下内容:
// sources/FirstModule.move
module 0xCAFE::BasicCoin {
...
}
这是一个 Move module(模块)的定义。模块是 Move 语言的代码块, 并且它使用指定的地址(address)进行定义 -- 模块只能在该地址下发布。 当前 BasicCoin
模块只能被发布在 0xCAFE
地址下。
再看这个文件的下一部分,这里定义了一个具有字段 value 的结构体 Coin
:
module 0xCAFE::BasicCoin {
struct Coin has key {
value: u64,
}
...
}
再看文件剩余部分,我们会看到一个函数,它会创建一个 Coin 结构体,并将其保存在某个账号(account)下:
module 0xCAFE::BasicCoin {
struct Coin has key {
value: u64,
}
public fun mint(account: signer, value: u64) {
move_to(&account, Coin { value })
}
}
让我们来看看这个函数和它的含义:
- 此函数需要一个
signer
参数 -- 表示不可伪造的 token 受此特定地址的控制; 和一个需要铸造的数量参数value
。 - 此函数使用给定的参数值铸造一个 Coin,然后通过
move_to
操作将其保存在(全局存储中)给定的account
账户下。
我们需要确保它真的执行,这可以通过在包文件夹(step_1/BasicCoin
)下的运行 build
命令来完成:
move build
进阶概念及参考引用
- 你可以通过以下命令创建一个空的 Move 包(move package):
move new <pkg_name>
- Move 代码也可以放在其他很多地方, 更多关于 Move 包系统的信息请参阅Move book
- 更多关于
Move.toml
文件的信息可以参阅package section of the Move book. - Move语言也支持命名地址的概念(named addresses), 命名地址是一种参数化 Move 源代码的方法, 就是如果对
NamedAddr
使用的不同赋值编译,编译后会获得部署到你控制地址的不同字节码. 这种用法很常见,一般都将地址变量其定义在Move.toml
文件 的[addresses]
部分. 例如:
[addresses]
SomeNamedAddress = "0xC0FFEE"
-
Move 结构体可以通过给类型设定不同的能力abilities让类型支持对应的行为. 有四种能力:
-
copy
: 允许此类型的值被复制 -
drop
: 允许此类型的值被弹出/丢弃 -
store
: 允许此类型的值存在于全局存储的某个结构体中 -
key
: 允许此类型作为全局存储中的键(具有key
能力的类型才能保存到全局存储中)所以
BasicCoin
模块下的Coin
结构体可以用作全局存储(global storage)的键(key), 因为它又不具备其他能力,它不能 被拷贝,不能被丢弃, 也不能作为非key来保存在(全局)存储里. 你无法复制Coin
,也不会意外弄丢它.
-
-
函数Functions默认是私有的(private), 也可以声明为
public
public(friend),public(script)
. 最后一个声明(指public(script)
)的函数可以被事务脚本调用。public(script)
函数也可以被其他public(script)
函数调用。(注意:在最新版本的 Move中,public(script)
已经被废弃,被public entry
取代,下同,译者注) -
move_to 是五种不同的全局存储操作之一
2.给模块(Module)添加单元测试
现在我们已经完成了我们的第一个 Move 模块,我们将切换到目录 step_2/BasicCoin
下并完成一个测试,确保铸币按我们预期的方式工作。 如果你熟悉它们(Move 和 Rust)的话,Move 中的单元测试类似于 Rust 中的单元测试 —— 测试代码使用 #[test] 注解,并像编写普通的 Move 函数一样。
可以通过 move test 命令来执行测试:
move test
现在我们来完成文件 FirstModule.move
的具体内容,你将看到的第一个新事项是这个测试:
module 0xCAFE::BasicCoin {
...
// Declare a unit test. It takes a signer called `account` with an
// address value of `0xC0FFEE`.
#[test(account = @0xC0FFEE)]
fun test_mint_10(account: signer) acquires Coin {
let addr = signer::address_of(&account);
mint(account, 10);
// Make sure there is a `Coin` resource under `addr` with a value of `10`.
// We can access this resource and its value since we are in the
// same module that defined the `Coin` resource.
assert!(borrow_global<Coin>(addr).value == 10, 0);
}
}
这里声明了一个命名为 test_mint_10
的单元测试,它在 account
账户地址下铸造了一个包含 value
为 10
的 Coin
,然后通过 assert!
断言检查已经铸造成功并保存在(全局)存储中的 Coin
的值是否与期望值一致。如果断言 assert
执行失败,则单元测试失败。
进阶概念及参考练习 (Advanced concepts and exercises)
- 很多测试相关的注解(annotations)都值得仔细探索, 参阅用法。 在
Step 5
中会看到更多用法. - 执行测试之前,需要设定Move标准库依赖关系,找到
Move.toml
并在[dependencies]
段内进行设定, 例如
[dependencies]
MoveStdlib = { local = "../../../../move-stdlib/", addr_subst = { "Std" = "0x1" } }
注意, 需要修改 <path_to_move>/language
中的内容来匹配实际 move-stdlib
所在的目录路径. 也可以用 git
方式指定依赖, 关于 Move 包依赖(package denpendices)信息可参阅package文档
练习
- 将断言值改为 11 将导致断言执行失败, 找一个可以传递给 move test 命令的标志,当测试失败时它会显示全局状态。看起来像这样:
┌── test_mint_10 ────── │ error[E11001]: test failure │ ┌─ ./sources/FirstModule.move:24:9 │ │ │ 18 │ fun test_mint_10(account: signer) acquires Coin { │ │ ------------ In this function in 0xcafe::BasicCoin │ · │ 24 │ assert!(borrow_global<Coin>(addr).value == 11, 0); │ │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Test was not expected to abort but it aborted with 0 here │ │ │ ────── Storage state at point of failure ────── │ 0xc0ffee: │ => key 0xcafe::BasicCoin::Coin { │ value: 10 │ } │ └──────────────────
- 找一个允许你收集测试覆盖率信息的标志,然后使用
move coverage
命令查看覆盖率统计信息和源码覆盖率。
3.设计自己的 BasicCoin
模块(Module)
在本节中,我们将设计一个具有基本代币和余额(balance)接口功能的模块,通过他们来实现币的挖矿铸造,不同地址之下钱包的转账。
Move 语言的 public function
签名如下:
/// Publish an empty balance resource under `account`'s address. This function must be called before
/// minting or transferring to the account.
public fun publish_balance(account: &signer) { ... }
/// Mint `amount` tokens to `mint_addr`. Mint must be approved by the module owner.
public fun mint(module_owner: &signer, mint_addr: address, amount: u64) acquires Balance { ... }
/// Returns the balance of `owner`.
public fun balance_of(owner: address): u64 acquires Balance { ... }
/// Transfers `amount` of tokens from `from` to `to`.
public fun transfer(from: &signer, to: address, amount: u64) acquires Balance { ... }
接下来再看本模块所需要各数据结构.
Move 语言的模块没有自己的数据存储,相反的是 Move 语言提供按地址(addresses) 索引的 全局存储 (也是就是我们所说的区块链状态(blockchain state)). 每个地址之下包含有 Move 模块(代码)和 Move 资源 (数据)。
在 Rust 语法中,全局存储看起来有点像这样:
#![allow(unused)] fn main() { struct GlobalStorage { resources: Map<address, Map<ResourceType, ResourceValue>> modules: Map<address, Map<ModuleName, ModuleBytecode>> } }
每个地址下的 Move 资源存储是一个类型到数值的映射。(细心的读者也许已经注意到每个地址, 每个类型下只能对应一个具体值)。这方便地为我们提供了一个按地址索引的本地映射。
在 BasicCoin
模块中,定义了每个 Balance
(钱包,余额)资源表示每个地址下持有的币的数量:
/// Struct representing the balance of each address.
struct Balance has key {
coin: Coin // same Coin from Step 1
}
区块链状态(Move blockchain state
)看起来大致如下:
进阶主题 (Advanced topics) :
public(script)
functions
只有public(script)
可见行的函数才能直接被交易调用,所以如果你要直接在交易内调用 transfer
方法,那么需要将函数签改成如下格式:
public(script) fun transfer(from: signer, to: address, amount: u64) acquires Balance { ... }
关于函数可见性的更多信息,请参阅Move function visibilities。
与 Ethereum/Solidity 的比较 (Comparison with Ethereum/Solidity)
在大多数以太坊ERC-20智能合约中,各个账户地址下的余额保存在类型为 mapping(address => uint256)
的 状态变量 中,此状态变量存储在具体的智能合约内部存储中。
以太坊区块链的状态看起来大致如下:
4.BasicCoin
模块(Module)的实现
我们已经在 step_4
文件夹上创建了名叫 BasicCoin
的 Move 包。sources
文件夹包含所有的 Move 包(package)的模块源码,包括 BasicCoin.move
。在本节中,我们将仔细研究 BasicCoin.move
内部方法的实现。
编译代码
首先尝试在文件夹step_4/BasicCoin中运行以下命令,使用 Move 包构建代码:
move build
方法的实现
现在仔细看看 BasicCoin.move
中内部方法的实现。
publish_balance
方法
此方法将 Balance
资源发布到指定地址名下。由于此资源需要通过铸造或转账来接收代币,必须由用户先调用方法 publish_balance
才能接收钱,包括模块所有者。
此方法使用 move_to
操作来发布资源:
let empty_coin = Coin { value: 0 };
move_to(account, Balance { coin: empty_coin });
mint
方法
mint
方法将代币铸造到指定的帐户。在此我们要求 mint
必须得到模块所有者的批准。我们使用 assert
语句强制执行此操作:
assert!(signer::address_of(&module_owner) == MODULE_OWNER, errors::requires_address(ENOT_MODULE_OWNER));
Move 中的 assert
语句可以这样使用:assert!(<predicate>, <abort_code>);
。这意味着如果 <predicate>
为假,则使用中止错误码 <abort_code>
来终止交易。此处的 MODULE_OWNER
和 ENOT_MODULE_OWNER
都是在模块开头定义的常量。errors
模块定义了我们可以使用的常见错误种类。重点是我们需要注意 Move 在其执行过程中是事务性的-- 因此,如果触发中止(abort),并不用回退已执行状态的,因为该事务的任何更改都不会持久保存到区块链。
然后将数量为 amount
的代币存入 mint_addr
的余额中。
deposit(mint_addr, Coin { value: amount });
balance_of
方法
我们使用全局存储操作之一的 borrow_global
从全局存储中读取资源(数据)。
borrow_global<Balance>(owner).coin.value
| | \ /
resource type address field names
transfer
方法
该函数从 from
的余额中提取代币并将代币存入 to
的余额中。我们仔细研究帮助函数 withdraw
:
fun withdraw(addr: address, amount: u64) : Coin acquires Balance {
let balance = balance_of(addr);
assert!(balance >= amount, EINSUFFICIENT_BALANCE);
let balance_ref = &mut borrow_global_mut<Balance>(addr).coin.value;
*balance_ref = balance - amount;
Coin { value: amount }
}
在方法开始,我们断言提款账户有足够的余额。然后我们使用 borrow_global_mut
来获得全局存储的可变引用,并用 &mut
创建结构体字段的可变引用。然后我们通过这个可变引用修改余额并返回一个带有提取金额的新代币。
练习
在模块中有两个 TODOs,留给读者练习:
- 完成
publish_balance
方法的实现。 - 实现
deposit
方法。
此练习的解决方案可以在step_4_sol
文件夹中找到。
额外练习
- 如果我们在余额中存入太多会发生什么?
5.给 BasicCoin
模块添加单元测试
在这一步中,来看看我们为覆盖在 step 4
中编写的代码而编写的所有不同的单元测试。还将看看我们可以用来帮助我们编写测试用例的一些工具。
首先,请在文件夹 step_5/BasicCoin
中运行 move test
命令。
move test
你应该看到如下内容:
INCLUDING DEPENDENCY MoveStdlib
BUILDING BasicCoin
Running Move unit tests
[ PASS ] 0xcafe::BasicCoin::can_withdraw_amount
[ PASS ] 0xcafe::BasicCoin::init_check_balance
[ PASS ] 0xcafe::BasicCoin::init_non_owner
[ PASS ] 0xcafe::BasicCoin::publish_balance_already_exists
[ PASS ] 0xcafe::BasicCoin::publish_balance_has_zero
[ PASS ] 0xcafe::BasicCoin::withdraw_dne
[ PASS ] 0xcafe::BasicCoin::withdraw_too_much
Test result: OK. Total tests: 7; passed: 7; failed: 0
(练习)
在查看测试之后,尝试在 BasicCoin
模块中编写一个单元测试 balance_of_dne
,测试地址没有 Balance
资源的情况,调用 balance_of
方法的执行结果。它应该只有几行代码。
练习的答案可以在step_5_sol
中找到。
6.使用泛型(generic)编写 BasicCoin
模块
在 Move 语言中,我们可以使用泛型来定义不同输入数据类型的函数和结构体。泛型是库代码的重要组成部分。在本节中,我们将使我们的简单 BasicCoin
模块泛型化,以便它可以用作其他用户模块可以使用的模块库。
首先,我们将类型参数添加到我们的数据结构中:
struct Coin<phantom CoinType> has store {
value: u64
}
struct Balance<phantom CoinType> has key {
coin: Coin<CoinType>
}
我们还以相同的方式将类型参数添加到我们的方法中。例如,withdraw
变成如下:
fun withdraw<CoinType>(addr: address, amount: u64) : Coin<CoinType> acquires Balance {
let balance = balance_of<CoinType>(addr);
assert!(balance >= amount, EINSUFFICIENT_BALANCE);
let balance_ref = &mut borrow_global_mut<Balance<CoinType>>(addr).coin.value;
*balance_ref = balance - amount;
Coin<CoinType> { value: amount }
}
查看step_6/BasicCoin/sources/BasicCoin.move
完整的实现。
此时,熟悉以太坊的读者可能会注意到,该模块的用途与ERC20 token standard类似,后者提供了在智能合约中实现可替代代币的接口。使用泛型的一个关键优势是能够重用代码,因为泛型模块库已经提供了标准实现,并且实例化模块可以通过包装标准实现提供定制化功能。
我们提供了一个称为MyOddCoin并实例化 Coin
类型并自定义其转移策略的小模块:只能转移奇数个代币。其还包括两个 tests 来测试这种行为。你可以使用在第 2 步和第 5 步中学到的命令来运行测试。
进阶主题:
phantom
类型参数
在 Coin
和 Balance
的定义中,我们将类型参数 CoinType
声明为phantom,因为 CoinType
没有在结构体定义中使用或仅用作 phantom 类型参数。
阅读更多有关 phantom 类型参数 信息.
进阶步骤
在继续下一步之前,确保您已安装所有的验证器依赖项。
尝试运行 boogie /version
。如果出现错误消息“找不到命令:boogie”,你将必须运行安装脚本并更新环境配置(source ~/.profile):
# run the following in move repo root directory
./scripts/dev_setup.sh -yp
source ~/.profile
7.使用 Move prover
部署在区块链上的智能合约可能会操纵高价值资产。作为一种使用严格的数学方式来描述计算机系统的行为和推理正确性的技术,形式化验证已被用于区块链,以防止智能合约中错误的产生。 Move验证器是一种在进化中、用Move 语言编写的智能合约形式化验证工具。用户可以使用Move语言规范(Move Specification Language (MSL))指定智能合约的功能属性,然后使用验证器自动静态检查它们。 为了说明如何使用验证器,我们在BasicCoin.move中添加了以下代码片段:
spec balance_of {
pragma aborts_if_is_strict;
}
通俗地说,代码块 spec balance_of {...}
包含 balance_of
方法的属性规范说明。
首先在BasicCoin directory目录中使用以下命令运行验证器。
move prove
它输出以下错误信息:
error: abort not covered by any of the `aborts_if` clauses
┌─ ./sources/BasicCoin.move:38:5
│
35 │ borrow_global<Balance<CoinType>>(owner).coin.value
│ ------------- 由于执行失败这里发生中止
·
38 │ ╭ spec balance_of {
39 │ │ pragma aborts_if_is_strict;
40 │ │ }
│ ╰─────^
│
= at ./sources/BasicCoin.move:34: balance_of
= owner = 0x29
= at ./sources/BasicCoin.move:35: balance_of
= 中止
Error: exiting with verification errors
验证器大体上告诉我们,我们需要明确指定函数 balance_of
中止的条件,中止原因是 owner
(函数调用者)在没有资源 Balance<CoinType>
的情况下调用 borrow_global
函数导致的。要去掉此错误信息,我们添加如下 aborts_if 条件:
spec balance_of {
pragma aborts_if_is_strict;
aborts_if !exists<Balance<CoinType>>(owner);
}
添加此条件后,再次尝试运行prove命令,确认没有验证错误:
move prove
除了中止条件,我们还想定义功能属性。在第 8 步中,我们将通过为定义 BasicCoin
模块的方法指定属性来更详细地介绍验证器。
8.为 BasicCoin
模块编写形式化规范(formal specification)
取款方法
取款(withdraw
) 方法的签名如下:
fun withdraw<CoinType>(addr: address, amount: u64) : Coin<CoinType> acquires Balance
该方法从地址 addr
中提取数量为 amount
的代币,然后创建数量为 amount
的代币并将其返回。当出现如下情况会中止:
- 地址
addr
没有资源Balance<CoinType>
,或 - 地址
addr
中的代币数量小于amount
时,withdraw
。
我们可以这样定义条件:
spec withdraw {
let balance = global<Balance<CoinType>>(addr).coin.value;
aborts_if !exists<Balance<CoinType>>(addr);
aborts_if balance < amount;
}
正如我们在这里看到的,一个 spec 块可以包含 let
绑定,它为表达式引入名称。
global<T>(address): T
是一个返回 addr
资源值的内置函数。balance
是 addr
拥有的代币数量。
exists<T>(address): bool
是一个内置函数,如果指定的地址(address)在(全局存储中)有资源 T
则返回 true
。
两个 aborts_if
子句对应上述两个条件。通常,如果一个函数有多个 aborts_if
条件,这些条件之间是相互对等的。默认情况下,如果用户想要指定中止条件,则需要列出所有可能的条件。否则验证器将产生验证错误。
但是,如果在 spec
代码块中定义了 pragma aborts_if_is_partial
,则组合中止条件(或对等的单个条件)仅 暗示 函数中止。
读者可以参考 MSL 文档了解更多信息。
下一步是定义功能属性,这些属性在下面的两个 ensures
子句中进行了描述。首先,通过使用 let post
绑定,balance_post
表示地址 addr
执行后的余额,应该等于 balance - amount
。那么,返回值(表示为 result
)应该是一个价值为 amount
的代币。
spec withdraw {
let balance = global<Balance<CoinType>>(addr).coin.value;
aborts_if !exists<Balance<CoinType>>(addr);
aborts_if balance < amount;
let post balance_post = global<Balance<CoinType>>(addr).coin.value;
ensures balance_post == balance - amount;
ensures result == Coin<CoinType> { value: amount };
}
存款方法
存款(deposit
)方法的签名如下:
fun deposit<CoinType>(addr: address, check: Coin<CoinType>) acquires Balance
该方法将代币 check
存入地址 addr
. 规范定义如下:
spec deposit {
let balance = global<Balance<CoinType>>(addr).coin.value;
let check_value = check.value;
aborts_if !exists<Balance<CoinType>>(addr);
aborts_if balance + check_value > MAX_U64;
let post balance_post = global<Balance<CoinType>>(addr).coin.value;
ensures balance_post == balance + check_value;
}
balance
表示 addr
执行前的代币数量,check_value
表示要存入的代币数量。方法出现如下情况将会中止:
1) 地址 addr
没有 Balance<CoinType>
资源, 或
2) balance
与 check_value
之和大于 u64
的最大值。
该功能属性检查执行后余额是否正确更新。
转账方法
转账(transfer
)方法的签名如下:
public fun transfer<CoinType: drop>(from: &signer, to: address, amount: u64, _witness: CoinType) acquires Balance
该方法将数量为 amount
的代币从帐户 from
转账给地址 to
。规范如下:
spec transfer {
let addr_from = signer::address_of(from);
let balance_from = global<Balance<CoinType>>(addr_from).coin.value;
let balance_to = global<Balance<CoinType>>(to).coin.value;
let post balance_from_post = global<Balance<CoinType>>(addr_from).coin.value;
let post balance_to_post = global<Balance<CoinType>>(to).coin.value;
ensures balance_from_post == balance_from - amount;
ensures balance_to_post == balance_to + amount;
}
addr_from
是账户 from
的地址,然后获取执行前两个地址 addr_from
和 to
的余额。
ensures
子句指定从 addr_from
减去 amount
数量的代币,添加到 to
。然而,验证器会生成以下错误:
error: post-condition does not hold
┌─ ./sources/BasicCoin.move:57:9
│
62 │ ensures balance_from_post == balance_from - amount;
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
│
...
当 addr_from
等于 to
时,这个属性无效。因此,我们可以在方法中添加一个断言,assert!(from_addr != to)
来确保 addr_from
不等于 to
。
练习
- Implement the
aborts_if
conditions for thetransfer
method. - 为
transfer
方法实现aborts_if
条件。 - Implement the specification for the
mint
andpublish_balance
method. - 为
mint
和publish_balance
方法实现规范。
练习的解答可以在 step_8_sol
中找到。
Solidity
Solidity-0.8.13 官网中文教程重新发行版
Hardhat教程
1. Hardhat概述
欢迎来到我们的以太坊合约和 dApp 开发初学者指南。本教程旨在快速让您从头开始构建一些东西。
为了协调这个过程,我们将使用 Hardhat,这是一种有助于在以太坊上构建的开发环境。它可以帮助开发人员管理和自动化构建智能合约和 dApp 过程中固有的重复性任务,并且它允许你围绕此工作流程轻松引入更多功能。这意味着 hardhat 在最核心的地方进行编译和测试。
Hardhat 还内置了 Hardhat 网络,这是一个专为开发而设计的本地以太坊网络。它允许你部署合约、运行测试和调试代码。
在本教程中,我们将引导您完成:
- 为以太坊开发设置 Node.js 环境
- 创建和配置 Hardhat 项目
- 实现 Solidity 智能合约代币
- 使用 Hardhat 为你的合约编写自动化测试
- 使用Hardhat EVM的
console.log()
调试 Solidity - 将您的合约部署到 Hardhat EVM 和以太坊测试网
要遵循本教程,你应该能够:
- 用 [JavaScript] 编写代码(https://developer.mozilla.org/en-US/docs/Learn/Getting_started_with_the_web/JavaScript_basics)
- 操作 terminal
- 使用 git
- 了解 智能合约 如何工作的基础知识
- 设置 Metamask 钱包
如果你无法执行上述任何操作,请点击链接并花一些时间学习基础知识。
2. 搭建环境
大多数以太坊库和工具都是用 JavaScript 编写的,Hardhat 也是如此。如果你不熟悉 Node.js,它是基于 Chrome 的 V8 JavaScript 引擎构建的 JavaScript 运行时。它是在 Web 浏览器之外运行 JavaScript 的最流行的解决方案,而 Hardhat 就是在它之上构建的。
:::提示
Hardhat for Visual Studio Code 是官方的 Hardhat 扩展,它为 VSCode 添加了对 Solidity 的高级支持。如果你使用 Visual Studio Code,可尝试一下!
:::
安装Node.js
如果你已经安装了的 Node.js >=16.0
,则可以跳到下一节。如果没有,请按照以下步骤在 Ubuntu,MacOS 和 Windows 上安装它。
Linux
Ubuntu
将以下命令复制并粘贴到终端中:
sudo apt update
sudo apt install curl git
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
sudo apt-get install -y nodejs
MacOS
确保你已安装 git
。否则,请遵循这些说明安装.
在 MacOS 上有多种安装 Node.js 的方法。我们将使用 Node 版本管理器(nvm)。将以下命令复制并粘贴到终端中:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash
nvm install 18
nvm use 18
nvm alias default 18
npm install npm --global # Upgrade npm to the latest version
Windows
如果你使用 Windows,我们强烈推荐你使用用于 Linux 的 Windows 子系统(也就是 WSL2)。你可以不用它来使用 Hardhat,但如果你使用它,效果会更好。
要使用 WSL2 来安装 Node.js,可参考这篇手册.
还要确保你已经在 WSL2 上安装了 git.
升级Node.js
如果你的 Node.js 版本很老的并且不被 Hardhat 支持,则需要通过以下指引升级。
Linux
Ubuntu
- 在终端运行
sudo apt remove nodejs
命令来删除 Node.js。 - 在这里查找你希望安装的 Node.js 版本并且按照指令来安装。
- 在终端运行
sudo apt update && sudo apt install nodejs
命令来再次安装 Node.js。
MacOS
你可以使用 nvm来切换你的 Node.js 版本。在终端运行以下命令升级到 Node.js 18.x
:
nvm install 18
nvm use 18
nvm alias default 18
npm install npm --global # 升级 npm 到最新的版本
Windows
你需要遵循与之前相同的安装说明,但选择不同的版本。你可以在此处查看所有可用版本的列表。
3. 创建新的Hardhat项目
我们将使用 Node.js 的包管理器(npm
)来安装 Hardhat,NPM 是一个 Node.js 软件包管理器和一个 JavaScript 代码库。
你可以使用 Node.js 的其它包管理器,但是我们建议你按照这个手册使用 npm 7 或者更高的版本。如果你遵循了之前的章节步骤,那么应该已经安装了它。
打开一个新终端,运行以下命令来创建一个新的目录:
mkdir hardhat-tutorial
cd hardhat-tutorial
然后初始化一个 npm 项目,如下所示。系统将提示你回答一些问题。
::::提示
使用代码段中的选项卡选择首选的包管理器。我们建议使用 npm7 或更高版本,因为它使安装 Hardhat 的依赖关系变得更容易。
::::
::::tabsgroup{options="npm 7+,npm 6,yarn"}
:::tab{value="npm 7+"}
npm init
:::
:::tab{value="npm 6"}
npm init
:::
:::tab{value=yarn}
yarn init
:::
::::
现在我们可以安装 Hardhat:
::::tabsgroup{options="npm 7+,npm 6,yarn"}
:::tab{value="npm 7+"}
npm install --save-dev hardhat
:::
:::tab{value="npm 6"}
npm install --save-dev hardhat
:::
:::tab{value=yarn}
yarn add --dev hardhat
:::
::::
在安装 Hardhat 的同一目录中运行以下命令:
npx hardhat
使用你的键盘选择 Create an empty hardhat.config.js
并且按下回车键。
$ npx hardhat
888 888 888 888 888
888 888 888 888 888
888 888 888 888 888
8888888888 8888b. 888d888 .d88888 88888b. 8888b. 888888
888 888 "88b 888P" d88" 888 888 "88b "88b 888
888 888 .d888888 888 888 888 888 888 .d888888 888
888 888 888 888 888 Y88b 888 888 888 888 888 Y88b.
888 888 "Y888888 888 "Y88888 888 888 "Y888888 "Y888
👷 Welcome to Hardhat v2.9.9 👷
? What do you want to do? …
Create a JavaScript project
Create a TypeScript project
❯ Create an empty hardhat.config.js
Quit
当 Hardhat 在运行时,它从当前工作目录开始搜索最近的 hardhat.config.js
文件。这个文件通常存在你项目的根目录下,而且一个空的 hardhat.config.js
文件已经足够可以让 Hardhat 去工作了。你的整个配置都包含在这个文件中。
Hardhat的架构
Hardhat 是围绕任务和插件的概念设计的。Hardhat 的大部分功能来自插件,你可以自由选择想要使用的插件。
任务
每次从命令行运行 Hardhat 时,都在运行一个任务。例如,npx hardhat compile
正在运行 compile
任务。要查看项目中当前可用的任务,运行 npx hardhat
。你可以通过运行 npx hardhat help [task]
来探索任何任务。
:::提示
你可以创建你自己的任务,可参阅创建任务指南。
:::
插件
就最终使用的工具而言,Hardhat 是独立的,但它确实有一些内置的默认值。所有这些都可以被覆盖。大多数情况下,使用给定工具的方法是使用将其集成到 Hardhat 中的插件。
在本教程中,我们将使用我们推荐的插件,@nomicfoundation/hardhat-toolbox
,它提供了开发智能合约所需的一切。
要安装它,请在项目目录中运行此命令:
::::tabsgroup{options="npm 7+,npm 6,yarn"}
:::tab{value="npm 7+"}
npm install --save-dev @nomicfoundation/hardhat-toolbox
:::
:::tab{value="npm 6"}
npm install --save-dev @nomicfoundation/hardhat-toolbox @nomicfoundation/hardhat-network-helpers @nomicfoundation/hardhat-chai-matchers @nomiclabs/hardhat-ethers @nomiclabs/hardhat-etherscan chai ethers hardhat-gas-reporter solidity-coverage @typechain/hardhat typechain @typechain/ethers-v5 @ethersproject/abi @ethersproject/providers
:::
:::tab{value=yarn}
yarn add --dev @nomicfoundation/hardhat-toolbox @nomicfoundation/hardhat-network-helpers @nomicfoundation/hardhat-chai-matchers @nomiclabs/hardhat-ethers @nomiclabs/hardhat-etherscan chai ethers hardhat-gas-reporter solidity-coverage @typechain/hardhat typechain @typechain/ethers-v5 @ethersproject/abi @ethersproject/providers
:::
::::
将高亮显示的行添加到 hardhat.config.js
中,使其看起来像这样:
require("@nomicfoundation/hardhat-toolbox");
/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
solidity: "0.8.9",
};
4. 编写和编译合约
我们将创建一个简单的智能合约,它会实现一个可以交易的 token。Token 合约最常用于交换和存储价值。在本教程中,我们不会深入研究合约的 Solidity 代码,但你应该知道我们实现的一些逻辑:
- Token 的总供应量是固定的,不能更改。
- Token 的整个供应量分配给部署合约的账户地址。
- 任何人都可以收到 Token。
- 任何拥有至少一个 Token 的人都可以转移 Token。
- Token 是不可分割的。你可以转移1、2、3或37个 Token,但不能转移2.5个。
:::提示
你可能听说过 ERC-20,这是以太坊的一个 token 标准。token 如 DAI 和 USDC 实现了 ERC-20 标准,这使得它们都可以与任何可以处理 ERC-20 token 的软件兼容。为了简单起见,我们将要构建的 token 不实现 ERC-20 标准。
:::
编写智能合约
开始创建一个名为 contracts
的新文件夹,并且在文件中创建一个名为 Token.sol
的文件。
粘贴下面的的代码到文件中,并且花一分钟通读以下这段代码。它很简单,而且充满了解释 Solidity 基本原理的注释。
:::提示
要在 Visual Studio Code 中为 solididity 获得语法高亮显示和编辑帮助,请尝试在Hardhat for Visual Studio Code。
:::
//SPDX-License-Identifier: UNLICENSED
// Solidity文件必须以这个pragma开头。
// 它将被Solidity编译器用来验证它的版本。
pragma solidity ^0.8.9;
// 这是智能合约的主要构建模块。
contract Token {
// 一些用于标识token的字符串类型变量。
string public name = "My Hardhat Token";
string public symbol = "MHT";
// 固定数量的tokens,存储在无符号整数类型变量中。
uint256 public totalSupply = 1000000;
// 一个地址类型变量被用作存储以太坊账户。
address public owner;
// 一个key/value键值对map映射。我们在这里存储每个账户的余额。
mapping(address => uint256) balances;
// 这个转让事件帮助链下应用理解在你的合约中发生的什么。
event Transfer(address indexed _from, address indexed _to, uint256 _value);
/**
* 合约初始化。
*/
constructor() {
// 这个总供应量被分配给交易发送者,它是部署这个合约的账户。
balances[msg.sender] = totalSupply;
owner = msg.sender;
}
/**
* 转移tokens的函数。
*
* 这个 `external` 修饰符会使一个函数仅可以在这个合约外部被调用。
*/
function transfer(address to, uint256 amount) external {
// 检查这个交易发送者是否有足够的tokens。
// 如果 `require` 的第一个参数等价于 `false` 那么这个交易将会恢复。
require(balances[msg.sender] >= amount, "Not enough tokens");
// 转移金额。
balances[msg.sender] -= amount;
balances[to] += amount;
// 将转移通知链下应用程序。
emit Transfer(msg.sender, to, amount);
}
/**
* 只读函数,用于检索给定账户的token余额。
* 这个 `view` 修饰符表明它不能修改合约的状态,它允许我们不需要执行一个交易而调用它。
*/
function balanceOf(address account) external view returns (uint256) {
return balances[account];
}
}
:::提示
*.sol
是Solidity合约文件的后缀。我们建议将文件名与其包含的合约名保持一致,这是一种常见的做法。
:::
编译智能合约
要编译合约,可在你的终端中运行 npx hardhat compile
。compile
任务是内置任务之一。
$ npx hardhat compile
Compiling 1 file with 0.8.9
Compilation finished successfully
这个合约已经成功编译了,它可以准备被使用了。
5. 测试合约
在构建智能合约时编写自动化测试是至关重要的,因为这关系到用户的钱。
为了测试我们的合约,我们将使用 Hardhat Network,这是一个为开发而设计的本地以太坊网络。它内置在 Hardhat 中,被用作默认网络。使用它不需要设置任何东西。
在我们的测试中,我将使用 ethers.js 与我们在上一节中构建的以太坊合约进行交互,并且我们将使用 Mocha 作为我们的测试运行者。
编写测试
在项目根目录中创建一个名为 test
的新目录,并在其中创建一个名为 Token.js
的新文件。
让我们从下面的代码开始。接下来我们将解释它,但现在将其粘贴到 Token.js
中:
const { expect } = require("chai");
describe("Token contract", function () {
it("Deployment should assign the total supply of tokens to the owner", async function () {
const [owner] = await ethers.getSigners();
const Token = await ethers.getContractFactory("Token");
const hardhatToken = await Token.deploy();
const ownerBalance = await hardhatToken.balanceOf(owner.address);
expect(await hardhatToken.totalSupply()).to.equal(ownerBalance);
});
});
在你的终端中运行 npx hardhat test
。你应该会看到如下的输出:
$ npx hardhat test
Token contract
✓ Deployment should assign the total supply of tokens to the owner (654ms)
1 passing (663ms)
这意味着测试通过了。现在让我们解释每一行代码:
const [owner] = await ethers.getSigners();
ethers.js 中的 Signer
是代表以太坊帐户的对象。它被用于向合约和其他账户发送交易。这里我们得到连接到了节点上的账户列表,在本例中是 Hardhat Network,我们只保留第一个。
ethers
变量在全局作用域中可用。如果你希望你的代码总是显式的,你可以在顶部添加这一行:
const { ethers } = require("hardhat");
:::提示
要学习更多关于 Signer
的内容,你可以查看 Signers 文档。
:::
const Token = await ethers.getContractFactory("Token");
ethers.js 中的 ContractFactory
是一个用于部署新智能合约的抽象,因此这里的 Token
是 token 合约实例的工厂。
const hardhatToken = await Token.deploy();
在 ContractFactory
上调用 deploy()
将启动部署,并且返回一个解决为 Contract
的 Promise
。这个对象为每个智能合约函数都提供了一个方法。
const ownerBalance = await hardhatToken.balanceOf(owner.address);
一旦这个合约被部署后,我们可以在 hardhatToken
上调用我们的合约方法。这里我们通过调用合约的 balanceOf()
方法,获得所有者账户的余额。
重新调用将会获得部署 token 的帐户的全部供应总量。默认情况下,ContractFactory
和 Contract
实例连接到第一个签名者。这意味着 owner
变量中的帐户执行了部署,并且 balanceOf()
应该返回整个供应总额。
expect(await hardhatToken.totalSupply()).to.equal(ownerBalance);
这里在我们的 Solidity 代码中,我们再次使用 Contract
实例去调用一个智能合约函数。totalSupply()
返回这个 token 供应总额,并且我们将检查它是否等价于 ownerBalance
,正如它应该的那样。
为此,我们使用了 Chai,这是一个流行的 JavaScript 断言库。这些断言函数被叫做 "matchers",我们在这里使用的是来自 @nomicfoundation/hardhat-chai-matchers
插件,它扩展了 Chai,提供了许多用于测试智能合约的匹配器。
使用不同的账户
如果你需要从默认帐户以外的其他帐户(或 ethers.js 中的 Signer
)发送交易来测试代码,则可以在 ethers.js 的 Contract
中使用 connect()
方法来将其连接到其他帐户,像这样:
const { expect } = require("chai");
describe("Token contract", function () {
// ...previous test...
it("Should transfer tokens between accounts", async function() {
const [owner, addr1, addr2] = await ethers.getSigners();
const Token = await ethers.getContractFactory("Token");
const hardhatToken = await Token.deploy();
// Transfer 50 tokens from owner to addr1
await hardhatToken.transfer(addr1.address, 50);
expect(await hardhatToken.balanceOf(addr1.address)).to.equal(50);
// Transfer 50 tokens from addr1 to addr2
await hardhatToken.connect(addr1).transfer(addr2.address, 50);
expect(await hardhatToken.balanceOf(addr2.address)).to.equal(50);
});
});
使用fixtures复用常见的测试配置
我们编写的两个测试从它们的设置开始,在本例中这意味着部署 token 合约。在更复杂的项目中,此设置可能涉及多个部署和其他事务。在每个测试中都这样做意味着大量的重复代码。另外,在每个测试开始时执行许多事务会使测试套件变得更慢。
通过使用 fixtures,你可以避免代码重复并提高测试套件的性能。fixture 是只在第一次调用时运行的设置函数。在随后的调用中,Hardhat 不会重新运行它,而是将网络的状态重置为 fixture 最初执行后的状态。
const { loadFixture } = require("@nomicfoundation/hardhat-network-helpers");
const { expect } = require("chai");
describe("Token contract", function () {
async function deployTokenFixture() {
const Token = await ethers.getContractFactory("Token");
const [owner, addr1, addr2] = await ethers.getSigners();
const hardhatToken = await Token.deploy();
await hardhatToken.deployed();
// Fixtures can return anything you consider useful for your tests
return { Token, hardhatToken, owner, addr1, addr2 };
}
it("Should assign the total supply of tokens to the owner", async function () {
const { hardhatToken, owner } = await loadFixture(deployTokenFixture);
const ownerBalance = await hardhatToken.balanceOf(owner.address);
expect(await hardhatToken.totalSupply()).to.equal(ownerBalance);
});
it("Should transfer tokens between accounts", async function () {
const { hardhatToken, owner, addr1, addr2 } = await loadFixture(
deployTokenFixture
);
// Transfer 50 tokens from owner to addr1
await expect(
hardhatToken.transfer(addr1.address, 50)
).to.changeTokenBalances(hardhatToken, [owner, addr1], [-50, 50]);
// Transfer 50 tokens from addr1 to addr2
// We use .connect(signer) to send a transaction from another account
await expect(
hardhatToken.connect(addr1).transfer(addr2.address, 50)
).to.changeTokenBalances(hardhatToken, [addr1, addr2], [-50, 50]);
});
});
在这里,我们编写了一个 deployTokenFixture
函数,该函数执行必要的设置并返回稍后在测试中使用的每个值。然后在每个测试中,我们使用 loadFixture
来运行 fixture 并获取这些值。loadFixture
将在第一次运行设置,并在其他测试中迅速返回到该状态。
完整的测试覆盖
现在我们已经介绍了测试合约所需的基本知识,下面是 token 的完整测试套件,其中包含大量关于 Mocha 以及如何组织测试的附加信息。我们建议你仔细阅读。
// This is an example test file. Hardhat will run every *.js file in `test/`,
// so feel free to add new ones.
// Hardhat tests are normally written with Mocha and Chai.
// We import Chai to use its asserting functions here.
const { expect } = require("chai");
// We use `loadFixture` to share common setups (or fixtures) between tests.
// Using this simplifies your tests and makes them run faster, by taking
// advantage of Hardhat Network's snapshot functionality.
const { loadFixture } = require("@nomicfoundation/hardhat-network-helpers");
// `describe` is a Mocha function that allows you to organize your tests.
// Having your tests organized makes debugging them easier. All Mocha
// functions are available in the global scope.
//
// `describe` receives the name of a section of your test suite, and a
// callback. The callback must define the tests of that section. This callback
// can't be an async function.
describe("Token contract", function () {
// We define a fixture to reuse the same setup in every test. We use
// loadFixture to run this setup once, snapshot that state, and reset Hardhat
// Network to that snapshot in every test.
async function deployTokenFixture() {
// Get the ContractFactory and Signers here.
const Token = await ethers.getContractFactory("Token");
const [owner, addr1, addr2] = await ethers.getSigners();
// To deploy our contract, we just have to call Token.deploy() and await
// its deployed() method, which happens once its transaction has been
// mined.
const hardhatToken = await Token.deploy();
await hardhatToken.deployed();
// Fixtures can return anything you consider useful for your tests
return { Token, hardhatToken, owner, addr1, addr2 };
}
// You can nest describe calls to create subsections.
describe("Deployment", function () {
// `it` is another Mocha function. This is the one you use to define each
// of your tests. It receives the test name, and a callback function.
//
// If the callback function is async, Mocha will `await` it.
it("Should set the right owner", async function () {
// We use loadFixture to setup our environment, and then assert that
// things went well
const { hardhatToken, owner } = await loadFixture(deployTokenFixture);
// `expect` receives a value and wraps it in an assertion object. These
// objects have a lot of utility methods to assert values.
// This test expects the owner variable stored in the contract to be
// equal to our Signer's owner.
expect(await hardhatToken.owner()).to.equal(owner.address);
});
it("Should assign the total supply of tokens to the owner", async function () {
const { hardhatToken, owner } = await loadFixture(deployTokenFixture);
const ownerBalance = await hardhatToken.balanceOf(owner.address);
expect(await hardhatToken.totalSupply()).to.equal(ownerBalance);
});
});
describe("Transactions", function () {
it("Should transfer tokens between accounts", async function () {
const { hardhatToken, owner, addr1, addr2 } = await loadFixture(
deployTokenFixture
);
// Transfer 50 tokens from owner to addr1
await expect(
hardhatToken.transfer(addr1.address, 50)
).to.changeTokenBalances(hardhatToken, [owner, addr1], [-50, 50]);
// Transfer 50 tokens from addr1 to addr2
// We use .connect(signer) to send a transaction from another account
await expect(
hardhatToken.connect(addr1).transfer(addr2.address, 50)
).to.changeTokenBalances(hardhatToken, [addr1, addr2], [-50, 50]);
});
it("should emit Transfer events", async function () {
const { hardhatToken, owner, addr1, addr2 } = await loadFixture(
deployTokenFixture
);
// Transfer 50 tokens from owner to addr1
await expect(hardhatToken.transfer(addr1.address, 50))
.to.emit(hardhatToken, "Transfer")
.withArgs(owner.address, addr1.address, 50);
// Transfer 50 tokens from addr1 to addr2
// We use .connect(signer) to send a transaction from another account
await expect(hardhatToken.connect(addr1).transfer(addr2.address, 50))
.to.emit(hardhatToken, "Transfer")
.withArgs(addr1.address, addr2.address, 50);
});
it("Should fail if sender doesn't have enough tokens", async function () {
const { hardhatToken, owner, addr1 } = await loadFixture(
deployTokenFixture
);
const initialOwnerBalance = await hardhatToken.balanceOf(owner.address);
// Try to send 1 token from addr1 (0 tokens) to owner.
// `require` will evaluate false and revert the transaction.
await expect(
hardhatToken.connect(addr1).transfer(owner.address, 1)
).to.be.revertedWith("Not enough tokens");
// Owner balance shouldn't have changed.
expect(await hardhatToken.balanceOf(owner.address)).to.equal(
initialOwnerBalance
);
});
});
});
这是针对完整测试套件的 npx hardhat test
的输出:
$ npx hardhat test
Token contract
Deployment
✓ Should set the right owner
✓ Should assign the total supply of tokens to the owner
Transactions
✓ Should transfer tokens between accounts (199ms)
✓ Should fail if sender doesn’t have enough tokens
✓ Should update balances after transfers (111ms)
5 passing (1s)
请记住,当你运行 npx hardhat test
时,如果自上次运行测试以来合约发生了更改,则会自动编译它们。
6. 使用Hardhat网络进行调试
Hardhat 内置了 Hardhat 网络,这是一个专为开发设计的以太坊网络。它允许你部署合约,运行测试和调试代码,所有这些都在本地机器的范围内。它是 Hardhat 所连接的默认网络,因此你无需任何设置即可工作。你只需运行测试就好。
Solidity console.log
在 Hardhat 网络上运行合约和测试时,你可以在 Solidity 代码中调用 console.log()
打印日志信息和合约变量。你必须先从合约代码中导入 hardhat/console.sol
再使用它。
就像这样:
pragma solidity ^0.8.9;
import "hardhat/console.sol";
contract Token {
//...
}
就像在 JavaScript 中使用一样,然后你只需在 transfer()
函数中添加一些 console.log
调用:
function transfer(address to, uint256 amount) external {
require(balances[msg.sender] >= amount, "Not enough tokens");
console.log(
"Transferring from %s to %s %s tokens",
msg.sender,
to,
amount
);
balances[msg.sender] -= amount;
balances[to] += amount;
emit Transfer(msg.sender, to, amount);
}
运行测试时将显示日志输出:
$ npx hardhat test
Token contract
Deployment
✓ Should set the right owner
✓ Should assign the total supply of tokens to the owner
Transactions
Transferring from 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 to 0x70997970c51812dc3a010c7d01b50e0d17dc79c8 50 tokens
Transferring from 0x70997970c51812dc3a010c7d01b50e0d17dc79c8 to 0x3c44cdddb6a900fa2b585dd299e03d12fa4293bc 50 tokens
✓ Should transfer tokens between accounts (373ms)
✓ Should fail if sender doesn’t have enough tokens
Transferring from 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 to 0x70997970c51812dc3a010c7d01b50e0d17dc79c8 50 tokens
Transferring from 0x70997970c51812dc3a010c7d01b50e0d17dc79c8 to 0x3c44cdddb6a900fa2b585dd299e03d12fa4293bc 50 tokens
✓ Should update balances after transfers (187ms)
5 passing (2s)
查看文档以了解更多关于该特性的信息。
7. 部署到真实网络
一旦你准备好与其他人分享 dApp 后,你可能要做的就是将其部署到一个真实的以太坊网络中。这样,其他人就可以访问不在本地系统上运行的实例了。
具有处理真实金钱的以太坊网络被称为“主网”,然后还有一些独立的“测试网”可以使用。这些测试网提供了共享的登录环境,可以很好地模拟真实世界的场景,而不需要投入真正的金钱,这里有几个以太坊测试网,像 Goerli 和 Sepolia。我们推荐你部署你的合约到 Goerli 测试网。
在软件级别,部署到测试网与部署到主网相同。唯一的区别是你连接到的哪个网络。让我们看看使用 ether.js 部署合约的代码是什么样的。
使用的主要概念是 Signer
、ContractFactory
和 Contract
,我们在测试部分中对此进行了解释。与测试相比,没有什么新东西需要做,因为当你测试合约时,实际上是在向开发网络进行部署。这使得代码非常相似,甚至完全相同。
让我们在项目根目录中创建一个新的目录 scripts
,并将以下内容粘贴到该目录中的 deploy.js
文件中:
async function main() {
const [deployer] = await ethers.getSigners();
console.log("Deploying contracts with the account:", deployer.address);
console.log("Account balance:", (await deployer.getBalance()).toString());
const Token = await ethers.getContractFactory("Token");
const token = await Token.deploy();
console.log("Token address:", token.address);
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
要告诉 Hardhat 连接到特定的以太坊网络,可以在运行任何任务时使用 --network
参数,如下所示
npx hardhat run scripts/deploy.js --network <network-name>
对于我们当前的配置,在没有 --network
参数的情况下运行它将导致代码针对 Hardhat 网络的嵌入式实例运行。在这种情况下,当 Hardhat 完成运行时,部署实际上会丢失,但测试我们的部署代码是否有效仍然很有用:
$ npx hardhat run scripts/deploy.js
Deploying contracts with the account: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Account balance: 10000000000000000000000
Token address: 0x5FbDB2315678afecb367f032d93F642f64180aa3
部署到远程网络
要部署到诸如主网或任何测试网之类的远程网络,你需要添加一个 network
入口到你的 hardhat.config.js
文件中。在这个例子中,我们将使用 Goerli,但是你也可以添加任何类似的网络:
require("@nomicfoundation/hardhat-toolbox");
// Go to https://www.alchemyapi.io, sign up, create
// a new App in its dashboard, and replace "KEY" with its key
const ALCHEMY_API_KEY = "KEY";
// Replace this private key with your Goerli account private key
// To export your private key from Metamask, open Metamask and
// go to Account Details > Export Private Key
// Beware: NEVER put real Ether into testing accounts
const GOERLI_PRIVATE_KEY = "YOUR GOERLI PRIVATE KEY";
module.exports = {
solidity: "0.8.9",
networks: {
goerli: {
url: `https://eth-goerli.alchemyapi.io/v2/${ALCHEMY_API_KEY}`,
accounts: [GOERLI_PRIVATE_KEY]
}
}
};
我们使用了 Alchemy, 但是你将 url
指向其他任何以太坊节点或网关都是可以的。请填入你自己的 ALCHEMY_API_KEY
。
要在 Goerli 上进行部署,你需要发送一些 Goerli ether 给将要进行部署的地址。你可以从水龙头获得测试网以太,这是一项免费分发测试 ETH 的服务。下面这些是给 Goerli 的:
你必须在交易前将 Metamask 的网络更改为 Goerli。
:::提示
你可以在 ethereum.org 网站上了解更多关于其他测试网的信息,并找到它们的水龙头链接。
:::
最终,运行:
npx hardhat run scripts/deploy.js --network goerli
如果一切正常,你应该会看到部署的合约地址。
8. 前端模板项目
如果你想要快速开始使用 dApp 或使用前端查看整个项目,可以使用我们的 boilerplate 库。
包含哪些内容
- 本次教程中我们使用的 Solidity 合约
- 测试合约的整个功能
- 使用 ethers.js 与合约进行交互的最小 React 前端
Solidity合约与测试
在代码库的根目录中,你将找到我们通过本教程与 Token
合约组合在一起的 Hardhat 项目。
- token 的总供应量是固定的,不能更改。
- 整个供应被分配给部署合约的地址。
- 任何人都可以接收 token。
- 任何人至少有一个 token 都可以转让 token。
- 这个 token 是不可以分割的。你可以转让1、2、3或37个代币,但不能转让2.5个。
前端app
在 frontend
你会发现一个简单的 app,允许用户做两件事:
- 检查连接钱包的余额
- 发送 token 到一个地址
它是一个独立的 npm 项目,它是用 create-react-app
创建的,这意味着它使用了 webpack 和 babel。
前端文件目录结构
src/
包含了所有代码src/components
包含了 react 组件Dapp.js
是唯一具有业务逻辑的文件。如果你把它用作为模板,可在此处用自己的代码替换它。- 其他组件仅渲染HTML,没有逻辑。
src/contracts
具有合约的 ABI 和地址,这些由部署脚本自动生成。
如何使用它
首先克隆代码库,然后准备部署合约:
cd hardhat-boilerplate
npm install
npx hardhat node
在这里,我们仅需要安装 npm 项目的依赖项,然后运行 npx hardhat node
启动一个可以供 MetaMask 连接的 Hardhat 网络。在同一目录下的另一个终端中运行如下命令:
npx hardhat --network localhost run scripts/deploy.js
这会将合约部署到 Hardhat 网络。完成后,启动 react web app:
cd frontend
npm install
npm run start
然后在你的浏览器中打开 http://127.0.0.1:3000/,你应该会看到如下所示:
将 MetaMask 中的网络设置为 127.0.0.1:8545
。
现在点击 web app 的按钮。你应该会看到如下所示:
这里发生的情况是,显示当前钱包余额,前端代码检测到余额为 0
,因此你无法尝试转账功能。通过运行如下命令:
npx hardhat --network localhost faucet <your address>
你将运行我们包含的自定义 Hardhat 任务,该任务使用部署帐户的余额向你的地址发送100个MHT和1个ETH。这将允许你向另一个地址发送 token。
你可以在 /tasks/faucet.js
查看该任务的代码,这是从 hardhat.config.js
中需要的。
$ npx hardhat --network localhost faucet 0x0987a41e73e69f60c5071ce3c8f7e730f9a60f90
Transferred 1 ETH and 100 tokens to 0x0987a41e73e69f60c5071ce3c8f7e730f9a60f90
在运行 npx hardhat node
的终端中,你还应该看到:
eth_sendTransaction
Contract call: Token#transfer
Transaction: 0x460526d98b86f7886cd0f218d6618c96d27de7c745462ff8141973253e89b7d4
From: 0xc783df8a850f42e7f7e57013759c285caa701eb6
To: 0x7c2c195cd6d34b8f845992d380aadb2730bb9c6f
Value: 0 ETH
Gas used: 37098 of 185490
Block #8: 0x6b6cd29029b31f30158bfbd12faf2c4ac4263068fd12b6130f5655e70d1bc257
console.log:
Transferring from 0xc783df8a850f42e7f7e57013759c285caa701eb6 to 0x0987a41e73e69f60c5071ce3c8f7e730f9a60f90 100 tokens
在我们的合约中显示 transfer()
函数的 console.log
输出,这就是运行水龙头任务后 web app 的样子:
尝试使用它并阅读代码。它充满了注释,解释了发生了什么,并清楚地指出什么是以太坊 boilerplate 代码,什么是 dApp 逻辑。这将使代码库易于为你的项目复用。
9. 最后的想法
恭喜你完成本教程!
这里有一些链接,你可能会发现在你的 dApp 开发旅程中有用:
- Hardhat's Boilerplate
- Hardhat's documentation site
- Hardhat Toolbox's documenation
- Hardhat Support Discord server
- Ethers.js Documentation
- Mocha Documentation
- Chai Documentation
Happy hacking!
隐私计算技术
一句话概念解释
- 零知识证明(Zero—Knowledge Proof):指证明者能够在不向验证者提供任何有用的信息的情况下,使验证者相信某个论断是正确的。
- MPC(Secure Multi-Party Computation):用于解决一组互不信任的参与方各自持有秘密数据,协同计算一个既定函数的问题。
- 差分隐私(Differential privacy,DP):是密码学中的一种手段,旨在提供一种当从统计数据库查询时,最大化数据查询的准确性,同时最大限度减少识别其记录的机会。
- 秘密共享:一种将秘密分割存储的密码技术,怎样更好的设计拆分和恢复,阻止秘密过于集中。
- 同态加密(Homomorphic Encryption):同态加密是基于数学难题的计算复杂性理论的密码学技术。对经过同态加密的数据进行处理得到一个输出,将这一输出进行解密,其结果与用同一方法处理未加密的原始数据得到的输出结果是一样的。
- 可信执行环境(Trusted execution environment,TEE):intel SGX、arm TrustZone、 amd SEV、TPM、HSM等技术实现。
- 联邦学习(Federated Learning):是一种分布式机器学习技术 [1] ,其核心思想是通过在多个拥有本地数据的数据源之间进行分布式模型训练,在不需要交换本地个体或样本数据的前提下,仅通过交换模型参数或中间结果的方式,构建基于虚拟融合数据下的全局模型,从而实现数据隐私保护和数据共享计算的平衡,即“数据可用不可见”、“数据不动模型动”。
目录
量子计算
目录
量子计算入门
什么是量子?
“量子物理学”是一个广泛使用但鲜为人知的术语。它是一种数学模型,最初用于描述实验室中小物体的行为,它暴露了先前“经典”物理学理论的空白。量子理论解释了这种行为,并为我们提供了更完整的宇宙图景。我们已经意识到我们可以使用这种以前无法解释的行为来执行我们以前认为不可能的某些计算。我们称之为量子计算。
量子计算是让您的可以深入了解量子物理学的完美方式。它将量子物理学的核心概念提炼成最简单的形式,去除了物理世界的复杂性。本节将带您踏上发现(并解释!)一些奇怪的量子现象的短途旅程,并让您领略什么是“量子”。
经典概率回顾
为了涵盖量子现象,我们需要首先提醒自己“经典”概率。从这个意义上说,“经典”只是指前量子,即你应该在学校看到的正态概率树。如果您已经熟悉此材料,则应快速浏览它。如果您对此不是很感兴趣,那么请不要担心——我们只会涵盖一些最简单的概率问题。
概率树
希望您会记住学校的概率树。这个想法很简单——我们用一张图画出每一种可能发生的情况,然后我们可以计算出它发生的可能性。
假设我们有一枚硬币,首先,我们将它放在手中。如果我们随后抛掷这枚公平的硬币并观察它,我们有 50% 的机会再次看到正面,而有 50% 的机会看到反面。我们可以像这样在概率树上绘制它:
我们在每个分支的末尾绘制结果,并在分支上绘制每次出现的概率。同样,如果我们从反面状态开始并抛硬币,我们将有 50% 的机会看到正面,50% 的机会看到反面。
我们可以通过尝试来测试它是否有效。你可以物理地拿出一枚硬币,抛多次,并记录每个结果;您最终会看到大约 50% 的结果是正面,50% 是反面。大约 500 到 1000 次抛掷应该足以获得可靠的结果。
更进一步
看起来我们的概率树模型正确地预测了我们的实验结果。我们可以更进一步,将我们的概率树链接在一起,以预测事件链的结果。例如,假设我们从正面开始,抛硬币,然后再抛硬币,我们会看到什么?我们可以使用树来计算:
你可能还记得在学校,我们沿着分支相乘来计算每个事件组合的概率:
然后我们将结果加在一起以计算每个结果的概率:
并且我们可以看到抛两次后看到正面的概率是 50%,抛两次之后看到反面的概率也是 50%。
双掷硬币也一样,经过足够多的抛掷,我们的结果符合预期:测量正面或反面的机会均等。
量子币
现在我们对经典硬币有了完整的描述,是时候介绍一种量子“硬币”了。我们的量子硬币被称为“量子比特”。
量子比特是你只能在实验室里玩的东西,因为它们很难操纵。多年的科技进步创造了我们今天拥有的量子位,但通过量子计算学习的美妙之处在于我们可以忽略物理上的复杂性,只要记住当我们测量一个量子位时,它会是两个中的一个状态:我们称我们的量子比特的两个状态为 0 和 1,而不是两个状态 正面 和 反面。
量子抛硬币
让我们试验一下我们的量子硬币,看看它的行为方式。我们将进行一次量子抛掷,测量硬币的状态并记录下来。这就像上一节中的经典抛硬币。
我们将尝试使用概率树来描述我们的量子硬币。这看起来像是,从 0 状态开始,抛硬币给了我们 50-50 的机会来测量 0 或 1。让我们像处理经典硬币一样在树上绘制它:
类似地,从状态 1 开始,抛硬币给了我们 50-50 的机会测量到 0 或 1。概率树如下所示:
双量子掷硬币
我们现在有一个模型可以预测量子硬币的行为。像优秀的科学家一样,我们现在想在新的场景中测试它,看看它是否成立。让我们像以前一样尝试抛两次硬币。就像经典硬币一样,我们的量子硬币模型预测有 50-50 的机会测量 0 或 1,无论我们从哪个状态开始:
所以让我们试试吧!我们将抛两次量子硬币:
这与我们的预测完全不符!我们的模型让我们失望了!这与物理学家在 20 世纪初遇到的问题相同。寻找答案导致了量子物理学的发展,这就是我们将用来描述我们的量子掷硬币的东西。
量子模型
简而言之,量子论是负数的概率论。
这是什么意思?我们不能有负概率,因为那没有意义。为了适应这一点,我们使用了一个新的数量,我们称之为振幅,并将它们绘制在树上。为了解决我们不能有负概率以及所有概率加起来必须为 1 的事实,我们使用了一个数学技巧:我们对幅值求平方来计算概率。
让我们看一个例子。我们单次量子硬币抛掷的振幅树如下所示:
我们可以看到,从状态 0 开始,量子掷硬币为两个结果分配了相等的振幅。当我们对这些振幅进行平方时,它们会为我们提供测量 0 或 1 的正确概率(50-50 机会)。我们怎么知道振幅是二分之一的开方?因为它们是给我们正确答案的价值观!
从状态 1 开始,振幅树就不同了:
在这里,我们可以看到我们的第一个负数出现在 1 结果的振幅中。当我们对振幅求平方来计算概率时,这个负号消失了(记住负数乘以负数就是正数),我们看到上面测量的 50-50 的机会。有趣的结果是当我们将这些概率链接在一起时。
解释双量子抛硬币
就像经典概率一样,我们沿着分支乘以振幅来计算每个结果的振幅:
为了计算出测量每个结果的概率,我们将这些振幅相加,然后平方:
我们可以看到在状态 1 中找硬币(量子比特)的振幅相互抵消,我们称这种效应为干扰。您应该自己验证此模型在初始状态为 1 时是否有效。
什么是量子计算?
这很酷,但它有什么用呢?事实证明,这些干扰效应可以为我们所用;我们可以结合诸如量子抛硬币之类的操作来构建更高效的算法。这些算法可以利用干扰效应让错误的答案迅速抵消,让我们有很高的概率测出正确答案。这就是量子计算背后的理念。
量子电路教程入门
电路基础
在这里,我们提供了使用 Qiskit 的概述。Qiskit 提供了对量子计算机进行编程所必需的基本构建块。Qiskit 的基本单元是量子电路。使用 Qiskit 的基本工作流程包括两个阶段:构建和运行。Build 允许您生成代表您正在解决的问题的不同量子电路,而 Run 允许您在不同的后端运行它们。作业运行后,根据所需的输出收集数据并进行后处理。
import numpy as np
from qiskit import QuantumCircuit
构建电路
您的第一个程序所需的基本元素是 QuantumCircuit。我们首先创建一个由三个量子位组成的 QuantumCircuit
。
# Create a Quantum Circuit acting on a quantum register of three qubits
circ = QuantumCircuit(3)
使用寄存器创建电路后,您可以添加门(“操作”)来操作寄存器。随着教程的进行,您会发现更多的门和电路;下面是产生三量子位 GHZ 状态的量子电路示例
$$|\psi\rangle = \left(|000\rangle+|111\rangle\right)/\sqrt{2}.$$
为了创建这样的状态,我们从一个三量子比特的量子寄存器开始。默认情况下,寄存器中的每个量子位都被初始化为 . 为了形成 GHZ 状态,我们应用以下门:
- 一个 Hadamard 门 H 在量子位 0 上,使其进入叠加态 .
- 一个 Controlled-NOT 操作 () 在量子位 0 和量子位 1 之间。
- 量子位 0 和量子位 2 之间的一个 Controlled-NOT 操作。
在理想的量子计算机上,运行该电路产生的状态将是上面的 GHZ 状态。
在 Qiskit 中,可以在电路中逐一添加操作,如下所示。
# Add a H gate on qubit 0, putting this qubit in superposition.
circ.h(0)
# Add a CX (CNOT) gate on control qubit 0 and target qubit 1, putting
# the qubits in a Bell state.
circ.cx(0, 1)
# Add a CX (CNOT) gate on control qubit 0 and target qubit 2, putting
# the qubits in a GHZ state.
circ.cx(0, 2)
<qiskit.circuit.instructionset.InstructionSet at 0x7f12b87bddf0>
可视化电路
您可以使用 Qiskit 可视化您的电路 QuantumCircuit.draw()
,它以许多教科书中的形式绘制电路。
circ.draw('mpl')
在此电路中,量子位按顺序排列,量子位 0 在顶部,量子位 2 在底部。从左到右读取电路(这意味着电路中较早应用的门显示在更左侧)。
在表示多量子位系统的状态时,Qiskit 中使用的张量阶与大多数物理教科书中使用的不同。假设有 个量子比特和 个量子位被标记,则表示 . Qiskit 使用一种排序方式 ,其中量子比特位于张量积的左侧,因此基向量标记为 .
例如,如果量子位 0 处于状态 0,量子位 1 处于状态 0,量子位 2 处于状态 1,Qiskit 会将此状态表示为 ,而许多物理教科书将其表示为 .
这种标记差异会影响多量子位运算表示为矩阵的方式。例如,Qiskit 表示一个 controlled-X () 以量子位 0 为控制,量子位 1 为目标的操作
模拟电路
为了模拟电路,我们使用 Qiskit 中的 quant_info 模块。该模拟器返回量子态,这是一个复杂的维度向量 , 在哪里 是量子位的数量(所以要小心使用它,因为它很快就会变得太大而无法在你的机器上运行)。
模拟器有两个阶段。第一个是设置输入状态,第二个是通过量子电路演化状态。
from qiskit.quantum_info import Statevector
# Set the intial state of the simulator to the ground state using from_int
state = Statevector.from_int(0, 2**3)
# Evolve the state by the quantum circuit
state = state.evolve(circ)
#draw using latex
state.draw('latex')
$$\frac{\sqrt{2}}{2} |000\rangle+\frac{\sqrt{2}}{2} |111\rangle$$
from qiskit.visualization import array_to_latex
#Alternative way of representing in latex
array_to_latex(state)
$$ \begin{bmatrix} \frac{\sqrt{2}}{2} & 0 & 0 & 0 & 0 & 0 & 0 & \frac{\sqrt{2}}{2} \ \end{bmatrix} $$
Qiskit 还提供了一个可视化工具箱,让您可以查看状态。
下面,我们使用可视化函数绘制 qsphere 和表示状态密度矩阵 的实部和虚部的 hinton。
state.draw('qsphere')
state.draw('hinton')
电路的单一表示
Qiskit 的 quant_info 模块也有一个运算符方法,可用于为电路制作单一运算符。这计算了 表示量子电路的矩阵。
from qiskit.quantum_info import Operator
U = Operator(circ)
# Show the results
U.data
数组([[ 0.70710678+0.j,0.70710678+0.j,0.+0.j,
0.+0.j,0.+0.j,0.+0.j,
0.+0.j, 0.+0.j],
[ 0.+0.j, 0.+0.j, 0.+0.j,
0.+0.j,0.+0.j,0.+0.j,
0.70710678+0.j, -0.70710678+0.j],
[ 0.+0.j, 0.+0.j, 0.70710678+0.j,
0.70710678+0.j, 0.+0.j, 0.+0.j,
0.+0.j, 0.+0.j],
[ 0.+0.j, 0.+0.j, 0.+0.j,
0.+0.j, 0.70710678+0.j, -0.70710678+0.j,
0.+0.j, 0.+0.j],
[ 0.+0.j, 0.+0.j, 0.+0.j,
0.+0.j, 0.70710678+0.j, 0.70710678+0.j,
0.+0.j, 0.+0.j],
[ 0.+0.j, 0.+0.j, 0.70710678+0.j,
-0.70710678+0.j, 0.+0.j, 0.+0.j,
0.+0.j, 0.+0.j],
[ 0.+0.j, 0.+0.j, 0.+0.j,
0.+0.j,0.+0.j,0.+0.j,
0.70710678+0.j, 0.70710678+0.j],
[ 0.70710678+0.j, -0.70710678+0.j, 0.+0.j,
0.+0.j,0.+0.j,0.+0.j,
0.+0.j, 0.+0.j]])
OpenQASM 后端
上面的模拟器很有用,因为它们提供了有关理想电路输出的状态和电路的矩阵表示的信息。然而,一个真正的实验通过测量每个量子位来终止(通常在计算 , 基础)。没有测量,我们就无法获得有关状态的信息。测量导致量子系统坍缩成经典比特。
例如,假设我们对三量子位 GHZ 状态的每个量子位进行独立测量
$$ |\psi\rangle = (|000\rangle +|111\rangle)/\sqrt{2}, $$
然后让 表示结果的位串。回想一下,在 Qiskit 使用的量子位标签下, 将对应于 qubit 2 的结果, 表示 qubit 1 的结果,和 表示 qubit 0 的结果。
注意:这种位串表示法将最高有效位 (MSB) 放在左侧,将最低有效位 (LSB) 放在右侧。这是二进制位串的标准排序。我们以相同的方式对量子位进行排序(表示 MSB 的量子位具有索引 0),这就是 Qiskit 使用非标准张量积顺序的原因。
回想一下,获得结果 的概率由下式给出
$$ \mathrm{Pr}(xyz) = |\langle xyz | \psi \rangle |^{2} $$
因此,获得 000 或 111 的 GHZ 状态概率均为 1/2。
要模拟包含测量的电路,我们需要将测量添加到上面的原始电路,并使用不同的 Aer 后端。
# Create a Quantum Circuit
meas = QuantumCircuit(3, 3)
meas.barrier(range(3))
# map the quantum measurement to the classical bits
meas.measure(range(3), range(3))
# The Qiskit circuit object supports composition.
# Here the meas has to be first and front=True (putting it before)
# as compose must put a smaller circuit into a larger one.
qc = meas.compose(circ, range(3), front=True)
#drawing the circuit
qc.draw('mpl')
该电路添加了一个经典寄存器和三个用于将量子位结果映射到经典位的测量值。
为了模拟这个电路,我们使用了 qasm_simulator
Qiskit Aer。该电路的每次运行都会产生位串 000 或 111。要建立关于位串分布的统计数据(例如,估计 ),我们需要多次重复电路。电路重复的次数可以通过关键字 shots
在 execute
函数中指定。
# Adding the transpiler to reduce the circuit to QASM instructions
# supported by the backend
from qiskit import transpile
# Use AerSimulator
from qiskit_aer import AerSimulator
backend = AerSimulator()
# First we have to transpile the quantum circuit
# to the low-level QASM instructions used by the
# backend
qc_compiled = transpile(qc, backend)
# Execute the circuit on the qasm simulator.
# We've set the number of repeats of the circuit
# to be 1024, which is the default.
job_sim = backend.run(qc_compiled, shots=1024)
# Grab the results from the job.
result_sim = job_sim.result()
一旦你获得对象结果后,你可以通过函数 get_counts(circuit)
访问统计次数。这为你提供了你提交的电路的聚合二进制结果。
counts = result_sim.get_counts(qc_compiled)
print(counts)
{'000': 486, '111': 538}
大约 50% 的时间,输出位串是 000。Qiskit 还提供了一个函数 plot_histogram
,可以让你查看结果。
from qiskit.visualization import plot_histogram
plot_histogram(counts)
估计 和 的结果概率是通过获取汇总计数并除以拍摄次数(重复电路的次数)来计算。尝试更改 execute
函数中的 shots
关键字,看看估计的概率如何变化。
import qiskit.tools.jupyter
%qiskit_version_table
%qiskit_copyright
Version Information
Qiskit Software | Version |
---|---|
qiskit-terra | 0.23.1 |
qiskit-aer | 0.11.2 |
qiskit-ibmq-provider | 0.20.0 |
qiskit | 0.41.0 |
qiskit-nature | 0.5.2 |
qiskit-finance | 0.3.4 |
qiskit-optimization | 0.5.0 |
qiskit-machine-learning | 0.5.0 |
System information | |
Python version | 3.8.16 |
Python compiler | GCC 11.3.0 |
Python build | default, Jan 11 2023 00:28:51 |
OS | Linux |
CPUs | 2 |
Memory (Gb) | 6.781219482421875 |
Fri Feb 03 18:56:55 2023 UTC |
量子状态与量子比特
简介
如果您认为量子力学听起来很有挑战性,那么您并不孤单。我们所有的直觉都基于日常经验,因此比原子或电子更能理解球和香蕉的行为。尽管量子物体起初看起来随机且混乱,但它们只是遵循一套不同的规则。一旦我们知道这些规则是什么,我们就可以使用它们来创造新的强大技术。量子计算将是这方面最具革命性的例子。
为了让您开始您的量子计算之旅,让我们测试一下您已经知道的内容。以下哪项是对 bit 的正确描述?
- 木匠使用的刀片。
- 最小的信息单位:0或1。
- 你放在马嘴里的东西。
实际上,它们都是正确的:这是一个非常多用途的词!但是,如果您选择了第二个,则表明您的思路已经正确。信息可以存储和处理为一系列 0 和 1 的想法是一个相当大的概念障碍,但这是今天大多数人甚至都没有考虑过的事情。以此为起点,我们可以开始想象遵守量子力学规则的比特。这些 quantum bits,或 qubits,将使我们能够以新的和不同的方式处理信息。
我们将开始更深入地研究量子比特的世界。为此,我们需要一些方法来跟踪我们应用门时他们在做什么。最有效的方法是使用向量和矩阵的数学语言。
本章对于已经熟悉向量和矩阵的读者来说最为有效。那些不熟悉的人也可能没问题,尽管不时查阅我们的量子计算线性代数简介可能会有用。
由于我们将使用基于 Python 的量子计算框架 Qiskit,因此了解 Python 的基础知识也很有用。需要入门的可以查阅Introduction to Python and Jupyter notebooks。
计算的原子
现在,任何人都可以在自己舒适的家中为量子计算机编程。
但是要创造什么?到底什么是量子程序?其实,什么是量子计算机?
这些问题可以通过与标准数字计算机进行比较来回答。不幸的是,大多数人实际上也不了解数字计算机的工作原理。在本文中,我们将了解这些设备背后的基本原理。为了帮助我们稍后过渡到量子计算,我们将使用与量子计算相同的工具来完成它。
如果我们想使用此页面中的代码,下面是我们需要运行的一些 Python 代码:
from qiskit import QuantumCircuit, assemble, Aer
from qiskit.visualization import plot_histogram
将信息拆分成比特
我们需要了解的第一件事是比特的概念。这些被设计成世界上最简单的字母表。只有两个字符,0 和 1,我们可以表示任何信息。
一个例子是数字。你可能习惯用0、1、2、3、4、5、6、7、8、9这十位数字的串来表示一个数,在这串数字中,每一位代表这个数的次数包含一定的十次方。例如,当我们写 9213 时,我们的意思是
$$ 9000 + 200 + 10 + 3 $$
或者,以强调十的幂的方式表达
$$ (9\times10^3) + (2\times10^2) + (1\times10^1) + (3\times10^0) $$
虽然我们通常使用基于数字 10 的系统,但我们也可以轻松地使用基于任何其他数字的系统。例如,二进制数字系统是基于数字二的。这意味着使用 0 和 1 这两个字符将数字表示为 2 的幂的倍数。例如,9213 变为 10001111111101,因为
$$ 9213 = (1 \times 2^{13}) + (0 \times 2^{12}) + (0 \times 2^{11})+ (0 \times 2^{10}) +(1 \times 2^9) + (1 \times 2^8) + (1 \times 2^7) \\ ,,, + (1 \times 2^6) + (1 \times 2^5) + (1 \times 2^4) + (1 \times 2^3) + (1 \times 2^2) + (0 \times 2^1) + (1 \times 2^0) $$
在这里,我们将数字表示为 2、4、8、16、32 等的倍数,而不是 10、100、1000 等。
from qiskit_textbook.widgets import binary_widget
binary_widget(nbits=5)
这些位串,称为二进制串,不仅可以用来表示数字。例如,有一种方法可以使用位来表示任何文本。对于您要使用的任何字母、数字或标点符号,您可以使用此表找到对应的最多八位的字符串。尽管这些标准相当武断,但这是一个得到广泛认可的标准。事实上,它就是用来通过互联网向您传送这篇文章的。
这就是所有信息在计算机中的表示方式。无论是数字、字母、图像还是声音,都以二进制字符串的形式存在。
与我们的标准数字计算机一样,量子计算机也是基于同样的基本思想。主要区别在于它们使用 qubits,这是比特对量子力学的扩展。在本教科书的其余部分,我们将探讨什么是量子比特、它们能做什么以及它们是如何做到的。然而,在本节中,我们根本不讨论量子。所以,我们只是像使用比特一样使用量子比特。
快速练习
- 想一个数字并尝试用二进制写下来。
- 如果你有n个bit,它们可以处于多少种不同的状态?
以图表形式计算
无论我们使用的是量子比特还是经典比特,我们都需要操作它们,以便将我们拥有的输入转化为我们需要的输出。对于比特数很少的最简单程序,用称为电路图的图表来表示此过程很有用。这些在左边有输入,在右边有输出,中间有神秘符号表示的操作。这些操作被称为“门”,主要是出于历史原因。
下面是标准的基于位的计算机的电路示例。您不需要了解它的作用。它应该只是让您了解这些电路的外观。
对于量子计算机,我们使用相同的基本思想,但对于如何表示输入、输出和用于运算的符号有不同的约定。这是代表与上述相同过程的量子电路。
在本节的其余部分,我们将解释如何构建电路。最后,您将了解如何创建上面的电路、它的作用以及它为什么有用。
你的第一个量子电路
在电路中,我们通常需要做三项工作:首先,对输入进行编码,然后进行一些实际计算,最后提取输出。对于您的第一个量子电路,我们将专注于这些工作中的最后一个。我们首先创建一个具有八个量子位和八个输出的电路。
qc_output = QuantumCircuit(8)
这个电路,我们称之为qc_output,是由 Qiskit 使用创建的QuantumCircuit。将QuantumCircuit量子电路中的量子比特数作为参数。
量子电路中输出的提取是使用称为 measure_all()
的操作完成的。每次测量都会告诉指定的量子位输出为指定的经典位。该命令 qc_output.measure_all()
向电路 qc_output
中的每个量子位添加一个测量值,还添加一些经典位以写入输出。
qc_output.measure_all()
现在我们的电路已经有了一些东西,让我们来看看它。
qc_output.draw(initial_state=True)
量子位总是被初始化输出为0。由于我们没有对上面电路中的量子比特做任何事情,这正是我们测量它们时得到的结果。我们可以通过多次运行电路并在直方图中绘制结果来看到这一点。我们会发现每个量子比特的结果总是 00000000: a 0。
sim = Aer.get_backend('aer_simulator')
result = sim.run(qc_output).result()
counts = result.get_counts()
plot_histogram(counts)
多次运行并将结果显示为直方图的原因是因为量子计算机的结果可能具有一些随机性。在这种情况下,由于我们没有做任何量子的事情,我们只能得到00000000的结果是确定的。
请注意,此结果来自量子模拟器,这是一台标准计算机,用于计算理想量子计算机的功能。模拟只能用于少量的量子位(~30 qubits),但在设计您的第一个量子电路时它们仍然是一个非常有用的工具。要在真实设备上运行,您只需替换 Aer.get_backend('aer_simulator')
为您要使用的设备的后端对象。
示例:创建加法器电路
编码一个输入
现在让我们看看如何将不同的二进制字符串编码为输入。为此,我们需要所谓的 NOT 门。这是您可以在计算机上执行的最基本的操作。它只是翻转位值:0成为1和1成为0。对于量子位,它是一个称为 X 的操作,可以完成 NOT 的作业。
下面我们创建一个专用于编码工作的新电路,并将其称为 qc_encode。现在,我们只指定量子位的数量。
qc_encode = QuantumCircuit(8)
qc_encode.x(7)
qc_encode.draw()
可以使用我们之前的电路来提取结果:qc_output。
qc_encode.measure_all()
qc_encode.draw()
现在我们可以运行组合电路并查看结果。
sim = Aer.get_backend('aer_simulator')
result = sim.run(qc_encode).result()
counts = result.get_counts()
plot_histogram(counts)
现在我们的计算机输出字符串10000000。
我们翻转的位来自量子位 7,位于字符串的最左侧。这是因为 Qiskit 从右到左对字符串中的位进行编号。有些人喜欢用相反的方式给他们的位编号,但是当我们使用位来表示数字时,Qiskit 的系统肯定有它的优势。具体来说,这意味着量子比特 7 告诉我们有多少个 2^7 有我们的数字。所以通过翻转这个位,我们现在已经在我们简单的 8 位计算机中写入了数字 128。
现在尝试为自己写另一个数字。例如,你可以做你的年龄。只需使用搜索引擎找出数字在二进制中的样子(如果它包含 0b
,请忽略它),然后如果你的年龄小于 128,则在左侧添加一些 0。
qc_encode = QuantumCircuit(8)
qc_encode.x(1)
qc_encode.x(5)
qc_encode.draw()
现在我们知道如何在计算机中对信息进行编码。下一步是处理它:获取我们已经编码的输入,并将其转换为我们需要的输出。
记住如何做加法
要了解将输入转化为输出,我们需要解决一个问题。让我们做一些基本的数学运算。在小学,您会学习到如何处理大型数学问题并将它们分解成易于管理的部分。例如,您将如何解决以下问题?
9213
+ 1854
= ????
一种方法是从右到左逐位进行。所以我们从 3+4 开始
9213
+ 1854
= ???7
然后1+5
9213
+ 1854
= ??67
那么我们有 2+8=10。由于这是一个两位数的答案,我们需要将其转移到下一栏。
9213
+ 1854
= ?067
最后我们有9+1+1=11,得到我们的答案
9213
+ 1854
= 11067
这可能只是简单的加法,但它演示了所有算法背后的原理。无论算法是设计用于解决数学问题还是处理文本或图像,我们总是将大任务分解为小而简单的步骤。
要在计算机上运行,需要将算法编译成尽可能最小和最简单的步骤。为了看看这些看起来像什么,让我们再次做上面的加法问题,但是用二进制。
10001111111101
+ 00011100111110
= ??????????????
请注意,第二个数字的左侧有一堆额外的 0。这只是为了使两个字符串的长度相同。
我们的第一个任务是对右侧的列执行 1+0。在二进制中,与在任何数字系统中一样,答案是 1。对于第二列的 0+1,我们得到相同的结果。
10001111111101
+ 00011100111110
= ????????????11
接下来,我们有 1+1。您一定会知道,1+1=2。在二进制中,数字 2 被写为10,因此需要两位。这意味着我们需要携带 1,就像我们携带十进制数 10 一样。
10001111111101
+ 00011100111110
= ???????????011
下一列现在要求我们计算1+1+1。这意味着将三个数字相加,因此对于我们的计算机而言,事情变得越来越复杂。但是我们仍然可以将它编译成更简单的操作,并且只需要我们将两个位加在一起。为此,我们可以只从前两个 1 开始。
1
+ 1
= 10
现在我们需要把这个 10 加到最后的 1,这可以使用我们通常的遍历列的方法来完成。
10
+ 01
= 11
最后的答案是11(也称为3)。
现在我们可以回到问题的其余部分了。有了 的答案 11,我们就有了另一个进位。
10001111111101
+ 00011100111110
= ??????????1011
所以现在我们还有另一个 1+1+1 要做。但我们已经知道如何做到这一点,所以这没什么大不了的。
事实上,到目前为止剩下的一切都是我们已经知道该怎么做的事情。这是因为,如果您将所有内容分解为仅添加两位,那么您将只需要计算四种可能的事情。这是四个基本求和(我们将用两位写出所有答案以保持一致)。
0+0 = 00 (in decimal, this is 0+0=0)
0+1 = 01 (in decimal, this is 0+1=1)
1+0 = 01 (in decimal, this is 1+0=1)
1+1 = 10 (in decimal, this is 1+1=2)
这称为半加法器。如果我们的计算机可以实现这一点,并且可以将它们都连接在一起,那么它就可以进行任何加法操作。
使用 Qiskit 的加法器
让我们使用 Qiskit 制作我们自己的半加法器。这将包括对输入进行编码的电路部分、执行算法的部分以及提取结果的部分。每当我们想要使用新输入时,都需要更改第一部分,但其余部分将始终保持不变。
我们要添加的两个比特编码在量子位0和1中。上面的例子在这两个量子位中都编码了1,因此它试图找到 1+1
的解。结果将是一个由两位组成的字符串,我们将从量子位 2 和 3 中读出,并分别存储在经典位 0 和 1 中。剩下的就是填写实际的程序,它位于中间的空白处。
图中的虚线只是为了区分电路的不同部分(尽管它们也可以有更有趣的用途)。它们是使用 barrier
命令制作的。
计算机的基本构建块称为逻辑门。我们已经使用了非门,但这还不足以构成我们的半加器。我们只能用它来手动写出答案。由于我们希望计算机为我们进行实际计算,因此我们需要一些更强大的门。
为了了解我们需要什么,让我们再看看我们的半加器需要做什么。
0+0 = 00
0+1 = 01
1+0 = 01
1+1 = 10
所有这四个答案中最右边的位完全取决于我们添加的两位是相同还是不同。所以对于0+0和1+1,当两位相等时,答案的最右边的位就出来了0。对于0+1和1+0,我们在其中添加不同的位值,最右边的位是1。
为了使解的这一部分正确,我们需要一些东西来确定两个位是否不同。传统上,在数字计算的研究中,这被称为异或门。
Input 1 | Input 2 | XOR Output |
---|---|---|
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 0 |
在量子计算机中,异或门的工作由受控非门完成。由于这个名字很长,我们通常将其称为 CNOT。在 Qiskit 中它的名字是cx,更短。在电路图中,它如下图所示。
qc_cnot = QuantumCircuit(2)
qc_cnot.cx(0,1)
qc_cnot.draw()
这适用于一对量子位。一个充当控制量子位(这是带有小点的那个)。另一个充当目标量子位(大圆圈里面有一个 +)。
有多种方法可以解释 CNOT 的影响。一种是说它查看它的两个输入位,看它们是否相同或不同。接下来,用结果覆盖目标量子位。如果它们相同,则目标位变为0,如果它们不同就变为1。
另一种解释 CNOT 的方法是说,如果控件是1,它对目标执行 NOT,否则不执行任何操作。这个解释与前一个解释一样有效(事实上,正是这个解释才有了门的名字)。
通过尝试每个可能的输入来亲自尝试 CNOT。例如,这是一个使用输入测试 CNOT 的电路01。
qc = QuantumCircuit(2,2)
qc.x(0)
qc.cx(0,1)
qc.measure(0,0)
qc.measure(1,1)
qc.draw()
如果你执行这个电路,你会发现输出是11。我们可以认为这种情况的发生是由于以下任一原因。
-
CNOT计算输入值是否不同,发现不同,则表示要输出1。它通过覆盖 qubit 1 的状态(记住,它位于位串的左侧),01变成11.
-
CNOT 看到量子位 0 处于状态1,因此将 NOT 应用于量子位 1。这会将量子位 1 的 0 翻转为 1,因此 01 变为 11。
下表显示了 CNOT 门的所有可能输入和相应输出:
Input (q1 q0) | Output (q1 q0) |
---|---|
00 | 00 |
01 | 11 |
10 | 10 |
11 | 01 |
对于我们的半加器,我们不想覆盖我们的输入之一。相反,我们想将结果写在一对不同的量子位上。为此,我们可以使用两个 CNOT。
qc_ha = QuantumCircuit(4,2)
# encode inputs in qubits 0 and 1
qc_ha.x(0) # For a=0, remove this line. For a=1, leave it.
qc_ha.x(1) # For b=0, remove this line. For b=1, leave it.
qc_ha.barrier()
# use cnots to write the XOR of the inputs on qubit 2
qc_ha.cx(0,2)
qc_ha.cx(1,2)
qc_ha.barrier()
# extract outputs
qc_ha.measure(2,0) # extract XOR value
qc_ha.measure(3,1)
qc_ha.draw()
我们现在已经完成了半加法器的一半工作。我们只剩下输出的另一部分要做:就是存在于量子位 3 上的那个。
如果您再次查看四个可能的总和,您会注意到只有一种情况是 1 而不是 0:1+1=10。只有当我们添加的两个位都是1.
要计算这部分输出,我们可以让我们的计算机查看两个输入是否都是1。如果它们是——并且仅当它们是——我们需要在量子位 3 上做一个非门。这将把它翻转到1仅适用于这种情况的所需值,为我们提供我们需要的输出。
为此,我们需要一个新门:类似于 CNOT,但控制在两个量子位上,而不是一个。只有当两个控件都处于状态 1 时,这才会对目标量子位执行 NOT 1。这座新城门叫做 Toffoli。对于那些熟悉布尔逻辑门的人来说,它基本上是一个 AND 门。
在 Qiskit 中,Toffoli 用 ccx 命令表示。
qc_ha = QuantumCircuit(4,2)
# encode inputs in qubits 0 and 1
qc_ha.x(0) # For a=0, remove the this line. For a=1, leave it.
qc_ha.x(1) # For b=0, remove the this line. For b=1, leave it.
qc_ha.barrier()
# use cnots to write the XOR of the inputs on qubit 2
qc_ha.cx(0,2)
qc_ha.cx(1,2)
# use ccx to write the AND of the inputs on qubit 3
qc_ha.ccx(0,1,3)
qc_ha.barrier()
# extract outputs
qc_ha.measure(2,0) # extract XOR value
qc_ha.measure(3,1) # extract AND value
qc_ha.draw()
在这个例子中,我们正在计算1+1,因为两个输入位都是1。让我们看看我们得到了什么。
qobj = assemble(qc_ha)
counts = sim.run(qobj).result().get_counts()
plot_histogram(counts)
/home/divs/.local/lib/python3.8/site-packages/qiskit/utils/deprecation.py:62: DeprecationWarning: Using a qobj for run() is deprecated as of qiskit-aer 0.9.0 and will be removed no sooner than 3 months from that release date. Transpiled circuits should now be passed directly using `backend.run(circuits, **run_options).
return func(*args, **kwargs)
结果是10,这是数字 2 的二进制表示。我们建造了一台可以解决著名数学问题 1+1 的计算机!
现在您可以尝试其他三个可能的输入,并证明我们的算法也能为这些输入提供正确的结果。
半加器包含加法所需的一切。使用 NOT、CNOT 和 Toffoli 门,我们可以创建程序来进行任意大小的任意数字集合的加法运算。
这三个门也足以完成计算中的所有其他事情。事实上,我们甚至可以不用 CNOT。此外,只有在创建具有值 1 的位时才真正需要非门。Toffoli 本质上是数学的原子。它是最简单的元素,其他任何解决问题的技巧都可以从它中编译出来。
正如我们所见,在量子计算中我们分裂了原子。
import qiskit.tools.jupyter
%qiskit_version_table
/home/divs/.local/lib/python3.8/site-packages/qiskit/aqua/__init__.py:86: DeprecationWarning: The package qiskit.aqua is deprecated. It was moved/refactored to qiskit-terra For more information see <https://github.com/Qiskit/qiskit-aqua/blob/main/README.md#migration-guide>
warn_package('aqua', 'qiskit-terra')
Version Information
Qiskit Software | Version |
---|---|
qiskit-terra | 0.18.2 |
qiskit-aer | 0.9.0 |
qiskit-ignis | 0.6.0 |
qiskit-ibmq-provider | 0.16.0 |
qiskit-aqua | 0.9.5 |
qiskit | 0.30.0 |
qiskit-nature | 0.2.1 |
qiskit-finance | 0.2.1 |
qiskit-optimization | 0.2.2 |
qiskit-machine-learning | 0.2.1 |
System information | |
Python | 3.8.10 (default, Jun 2 2021, 10:49:15) [GCC 9.4.0] |
OS | Linux |
CPUs | 2 |
Memory (Gb) | 7.521877288818359 |
Sun Oct 03 22:50:56 2021 IST |