基本原理

基本原理中的主题解释了 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 框架的核心组件使用 libp2pjsonRPC 等开放协议,同时允许你决定对区块链架构的自定义程度。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 宏来自定义接口。然而,每个运行时都必须实现 CoreMetadata 接口。除了这些必需的接口之外,大多数 Substrate 节点(如 node template)都实现了以下运行时接口:

核心原语

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、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_idleon_finalize 函数,来执行任何应该在块的末尾发生的最终业务逻辑。这个模块被再次按照它们在 construct_runtime!宏 中定义的顺序执行。但在这种情况下,on_finalize 函数在 system pallet 中是最后被执行。

在所有 on_finalize 函数都已被执行之后,执行模块会检查块头中的摘要和存储根是否与块初始化时计算的相匹配。

on_idle 函数还通过块的剩余权重,以允许基于区块链继续使用执行。

区块创建和区块导入

到目前为止,你已经看到了交易如何被包含在本地节点生成的块中。如果授权本地节点生成块,则交易生命周期遵循如下方式:

  1. 本地节点监听网络上的交易。
  2. 每笔交易都要经过验证的。
  3. 有效的交易被放置在交易池中。
  4. 交易池在适当的交易队列中对有效交易进行排序,执行模块调用运行时以开始下一个块。
  5. 执行的交易与状态更改被存储在本地内存中。
  6. 将构造的区块发布到网络。

将块发布到网络后,其他节点可以导入该块。块导入队列是每个 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]宏 中声明的一个)。

下面是一个示例,说明了从名为 Balancespallet 中查询名为 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 phrasemnemonic。如果私钥丢失,帐户所有者可以使用此secret seed phrase恢复对帐户的访问。

对于大多数网络,与帐户相关联的公钥账户是如何在网络上被识别并将其用作交易的目标地址。然而,基于 Substrate 的链使用底层公钥来派生出一个或多个公共地址。Substrate 允许你为一个帐户生成多个地址和地址格式,而不是直接使用公钥。

Address encoding and chain-specific addresses

Substrate 使你能够使用单个公钥派生出多个地址,因此你可以与多个链交互,而无需为每个网络创建单独的公钥和私钥对。默认情况下,与帐户公钥关联的地址使用 Substrate SS58 地址格式。该地址格式基于 base-58 编码。除了允许你从同一公钥派生出多个地址之外,base-58 编码具有以下优点:

  • 编码地址由58个字母数字字符组成。
  • 字母数字字符串省略字符,如 0OIl,在字符串中很难区分彼此。
  • 网络信息,例如,可以在地址中编码指定的网络前缀。
  • 可以使用 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 BookRust 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 手册中关于泛型类型TraitsAdvanced 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 节点,以启用此功能来持久化数据。

与链下工作机不同,链下索引在每次处理区块时填充数据到链下存储。通过在每个块填充数据,链下索引可确保数据始终保持一致,并且对启用索引运行的每个节点,也都完全相同。