本白皮书主要叙述了 Gear 技术方面内容,Gear 是一条基于 Substrate 框架搭建的Polkadot/Kusama 平行链网络, 同时还可以运行任何人编写的智能合约。Gear 开发了一条完整的平行链和一条独立桥接网络。Gear 网络可以运行由许多流行编程语言(如 C/C++、Rust 等)编译的WebAssembly 程序(智能合同)。Gear 技术架构采用了目前主流的 Actor 通信模型, 为不可更改的程序提供永久存储, 同时也为区块链上下文提供小巧、直观且易用的api接口。
第(2)章概述了 Substrate 区块链框架。
第(3)章概述了 Polkadot 网络和 Gear 在其中的作用。
第(4)章介绍了 Gear 网络状态的特征和组成部分。
第(5)章详细介绍了 Gear 网络状态的演变。
第(6)章介绍了余额转移、Gas 经济和 DoS 保护。
第(7)章介绍了在 Gear 网络状态下,使用 Actor 模型进行跨进程(或跨合约)通信,以及它带来的分布式计算。
第(8)章介绍了使用 async/await 模式进行异步通信。
第(9)章介绍了 Gear 引擎如何保证消息并行处理和硬件资源的高效利用。
第(10)章讨论了 Gear 如何处理用户与程序交互的输入的典型场景。
第(11)章介绍了在 Gear 网络中使用的 WebAssembly 虚拟机(VM),以及 VM 实现时的显著特性。
第(12)章介绍了有效使用 Gear 的一些案例
Substrate 区块链框架是 Polkadot 网络的重要组成部分。它可以让一个区块链技术团队无需从头构建一个区块链系统。
Substrate 涵盖了一致性(网络层)和容错(共识机制)。这些层面的技术描述超出了本文的范围。请参见 Substrate 技术文档。
Gear 在底层使用 Substrate 框架。它能满足企业级分布式项目大部分的需要,即: 容错、一致性、通证化、不可篡改、数据安全性和可用于生产环境的跨平台永久存储数据库。
Gear 本身是基于 Substrate runtime 实现的。同时也包含了内置的本地扩展(通过宿主函数)以提高性能。
基于 Substrate 开发的 Polkadot/Kusama 网络的平行链/平行线程可以很轻松快速的连接 Gear 的节点。
Polkadot 是下一代区块链协议,是一个异构多链的区块链网络,使它们能够大规模无缝运行。
Polkadot 架构中有几个组件,分别是:Relay Chain,Parachains 和 Bridges。
Relay Chain 是 Polkadot 的核心,负责网络的安全性、共识和跨链互操作性。
平行链是一条独立自主的区块链网络,可以拥有自己的通证并针对特定场景优化其特性。平行链要想互相通讯必须连接到 Relay Chain 以确保与其他网络的互操作性和安全性。因此,平行链可以随用随付或租用一个插槽。
Bridges 是一种特殊的连接,允许 Polkadot 生态系统连接到外部网络并与之通信,如以太坊、比特币等。这种连接允许将通证或任意数据从一个区块链转移到另一个区块链。
Polkadot 网络的关键之处在于它能够在不同链之间发送任意消息。消息的传递流程也非常的简单: 协商确定两条平行链接之间的通道后就可以通过它发送异步消息。Polkadot Relay Chain 和 Gear 最终使用同一种消息模式 (异步消息)。基于 Gear 项目可以将他们的解决方案无缝地整合到整个 Polkadot/Kusama 生态系统中。异步消息传递体系结构允许 Gear 成为 Polkadot 网络的一个有效且易于使用的平行链:
-
用户将程序部署到 Gear 网络。
-
可以跟任何其他平行链或者桥建立连接。
-
整个Gear的平行链通过它们进行通信。这种架构可以驱动各个网络的状态变化,并且也很符合这种去中心网络的特性。
与其他区块链系统类似,Gear 维护全局状态。运行时代码会被编译为 WebAssembly 模块成为区块链存储状态的一部分。
Gear 确保了其中一项定义特性——无分叉 Runtime 升级。如果使用 finality gadget,也能保证最终网络状态完成。
存储状态组件:
- 程序和内存(包括程序代码和私有内存)
- 消息队列(网络的全局消息队列)
- 账户(网络账户及其余额)
程序是 Gear 实例状态中的一等公民。
程序代码编译为不可变的 Wasm 二进制代码被存储。每个程序开辟了一个固定大小的内存空间来处理消息(所谓的静态区域)。
程序也可以从 Gear 实例提供的内存池中申请更多内存。一个特定的程序只能在专门分配给它的内存中读写。
Gear 节点为每个程序保存单独的内存空间,并且保证它的持久性。一个程序只能在自己的内存空间内读写,不能访问其他程序的内存空间。单独的内存空间是在程序初始化时为程序保留的,不需要额外的费用(包含在程序初始化费用中)。
程序可以以 64KB 的块为单位分配所需的内存量。每个内存块分配都需要费用。每个页面(64KB)单独存储在分布式数据库后端,但在运行时,Gear 节点构建连续运行时内存并允许程序在其上运行而无需重新加载。
Gear 实例保存一个全局消息队列。使用 Gear 节点,用户可以用包含一条或多条消息的交易发送给指定的一个程序或者多个程序。这些消息进入队列。在块构造过程中,消息队列内的消息被逐一消费并路由给指定的程序。
对于一个公开的网络,可以通过支付手续费来防止 DOS 攻击。Gear 提供了一个余额模块可以存储用户和程序的余额来支付交易费用。
一般来说,Gear 网络可以被应用在共有链和联盟链场景中。在联盟链的场景中, 是不需要余额模块的。
每个系统状态变化都是依据某个规则的。在网络处理新输入的数据时,根据状态转换规则对状态进行提前处理。这个输入数据被封装在一个被称为交易的信息中。
Gear 节点维护和同步一个接收所有新交易的交易池。当任何节点(验证者或非验证者)接收到交易时,该节点将交易广播给所有已连接的其他节点。要进一步了解交易池是如何工作的,参见 Substrate 文档。
当 Gear 验证者节点开始生成新块时,交易池中的一些(或全部)交易会被打包到一个区块中,并且网络通过这个块进行状态转换。在上一个区块中没有被采取的交易将保留在池中,直到下一个区块产生。
Gear 支持以下类型的交易:
创建程序(用户上传新程序-智能合约)
发送消息(程序填满消息队列)
消息出列(验证者(区块生产者)出列多条消息,运行相关程序)
余额转移(Gear 引擎执行用户-程序-验证者余额转账)
在块构建/导入时间的保留空间中进行消息处理。保证消息处理将在每个块中执行,并且至少以当前实例设置固定速率执行。
任何项目方或者个人都可以创建一个程序发布到Gear网络来保存对应的状态。在公有链网络中,每个程序都会有一个自己的余额。这个余额也就是它的初始余额(现有存款)。
用户要想与程序交互并拿到对应的结果,需要向 Gear 网络发送消息。发送到 Gear 网络的消息最终会进入一个全局消息队列。这个队列可以被看作是一个运行时驱动的交易队列,已确保接受到的消息最终都会被处理。将消息放入队列需要收取费用,因此可以保证消息会被正常分发。
验证者能够在它们出块时选择处理哪些消息。这样每个验证者可以不需要维护完整内存状态。消息的处理仅发生在每个块的末尾。在处理当前消息的期间,可以生成新消息。它们也可以在这个阶段被处理,也可以留在队列中等待下一次出块时被处理(和另一个验证者)。
常规的余额转账调用的是 Substrate 的内置模块。详情参见下一章。
下图展示了 Gear 消息机制的整个生命周期。正如Actor模型所设计的那样,没有什么是共享的,只有消息。标记为 "system" 的消息会打印在日志中,以便在用户进行查看。
常规余额转账在 Substrate 余额模块内进行。余额在用户、程序和验证者帐户之间转移。
除了常规的余额转账,Gear 网络会将收取gas费奖励给到验证者节点,收取gas费也可有效的防止网络的 DoS 攻击。
Gear 节点通过处理消息来赚取手续费。下面详细描述消息处理过程。
Gear 网络内的所有交互都是通过消息传递完成的。Gear 中的消息遵循通用接口范式,含以下参数:
- source account
- target account
- payload
- gas_limit
- value Gear 网络中使用了五种类型的消息:
- 来自用户的特殊消息,用于将新程序上传到网络。Payload 必须包含程序本身的 Wasm 文件。不得指定 target account——它将作为处理消息发布的一部分创建。
- 从用户到程序
- 从程序到程序
- 从程序到用户
- 从用户到用户
发送消息函数的最后一个参数是要转移到 target account 的 value。在初始程序上传的特殊消息中,value 将转移到为程序新创建的帐户的余额中。
消息处理包括两个步骤:
Gear 网络尝试将消息发布到队列中,旨在验证 source account 有足够的余额来支付 value 和 gas_limit 的发送以及支付小额处理费用,该费用与有效负载大小呈线性关系(这是 Substrate 框架的标准包含费用,详见 Substrate 文档)。
验证后,Gear 网络的区块生产者将消息发布到区块中,将 value 转移到 target account,将小额处理费用转移到验证者帐户,并将剩余的费用返还给 source account。
程序消息由网络通过以下方式分发:
上传程序时,用户指定 gas_limit 和可选的 value 转到程序帐户。gas_limit 是程序初始化上的最大花费。
当一个程序被 Gear 节点初始化时,每个内存页面分配和每个 cpu 指令都会消耗 gas 费用。它增加内部计数器 - gas_spent。
Gear 节点检查 gas_spent 的每个增量的 value 是否低于初始化消息中指定的 gas_limit,如果超过,则停止程序执行。通过这种方式 gas_limit 可以保障用户余额,因此没有程序会消耗超过用户预期的费用。
程序还可以为自己的余额保留额外的 gas,以便将来或在初始化期间向其他程序发送消息。可以通过使用特殊指令 gas_charge 来完成。指定后,Gear 节点会自动检查 gas_spent 加 gas_charge 是否低于消息中指定的 gas_limit。增加程序余额的另一种方法是使用初始化消息的最后一个参数明确指定转移 value 到其余额。
程序初始化后,gas_spent 会从用户账户中扣除到验证者的账户中,gas_charge 从用户账户中扣除到程序账户中。
gas_limit 和 gas_spent 加 gas_charge 的区别是 gas_left 并且它的 value 在 source account 上是未保留的。
Gas 费用是线性的——每分配 64KB 内存页需要消耗 64000 gas 还有检测 Wasm 指令 1000 gas。
对于目标是程序的标准消息,以上所有 gas 消耗规则均适用,以及通过增加 gas_spent 值收取额外的内存费用。此内存租用费用类似于内存分配费用,但价格低于初始分配的费用-每个已分配页面 1000 gas。由于节点必须从/向网络状态加载/保存程序内存,因此收取内存租金。
下图显示了更详细的余额转账示例:
并非所有传入消息都可以在一个周期内处理并出现在单个块中。会发生这种情况,所有消息产生所需的 gas 超过块 gas 限制。在这种情况下,消息可以出现在下一个块中(考虑到更多新消息即将到来)。
Gear 节点选择哪些交易与消息,将最终放入队列中。来自具有最高费用的交易的消息首先被获取。在这种情况下,来自费用最低的交易的消息可能会延迟,甚至永远不会进入处理队列。偏离标准的交易处理算法可能会导致不利的经济情况。
并发系统的主要挑战之一是并发控制。它需要保证不同程序间正确读写顺序,并协调对共享资源的访问。竞争条件、死锁和资源匮乏都是其中的潜在的问题。
并发计算系统主要分为两类:
共享内存通信 -- 并发程序通过更改共享内存位置的内容进行通信。
消息传递通信 -- 通过消息交换进行并发程序通信。消息传递并发比共享内存并发更容易理解。它通常被认为是一种更加鲁棒性的并发编程形式。
通常,消息传递并发性比共享内存具有更好的性能。在消息传递系统中,每个进程的内存开销和任务切换开销较低。
有很多数学理论来诠释消息传递系统,包括 Actor 模型。
Gear的跨进程通信采用了 Actor 模型。随着Actor模型的不断被采用,它也被很多编程语言作为实现的首选。Actor 模型的原则是程序从不共享任何状态,而是通过消息传递达到共享。
而在一个普通的 Actor 模型中,消息顺序没有保证,Gear 做了一些额外的处理,以保留两个特定程序之间的消息顺序。
使用 Actor 模型方法提供了一种在程序(智能合约)逻辑中实现基于 Actor 的并发性的方法。这可以利用各种语言构造进行异步编程(例如, Rust 中的 Futures 和 async-await)。
与其他并发模型不同的是,Actor 一次只允许一个任务访问它们的可变状态,这使得多个任务中的代码可以安全地与同一个 Actor 实例交互。
异步函数虽然简化了并发管理,但它们不处理死锁或状态损坏的情况。为避免死锁或状态损坏,异步函数应避免调用可能阻塞其线程的函数。为了实现它,他们使用了 await 表达式。
目前,在典型的智能合约代码中缺乏对 async/await 模式的正常支持,给智能合约开发者带来了很多问题。实际上,通过添加手工功能(在 Solidity 智能合约中),或多或少可以在智能合约程序流程中实现更好的控制。但是合约中的许多函数的问题在于,人们很容易混淆哪个函数可以在合约生命周期的哪个阶段被调用。
Gear 本身为任何程序提供任意的 async/await 语法。它极大的简化了开发和测试,并减少了智能合约开发中出错的可能性。如果程序逻辑需要,Gear API 还允许通过不使用 await 表达式来使用同步消息。
每个 Gear 节点上可以在各自独立内存空间内处理各自的消息。并行处理流的数量等于 CPU 的核数。一组程序集合的交互就代表了一个流消息。这些消息来自于其他程序或从外部用户(用户的交易)。
举个例子,在一个消息队列里包含了 100 条发送给不同程序的消息,Gear 网络中的某个节点配置了 2 个处理线程。Gear 引擎在运行时启动时决定流的数量(等于验证者机器上的 CPU 核数),将发送给目标程序的总数除以流数,并为每个流创建一个消息池(这里就是每个流 50 个程序)。
程序被分发到指定的流处理,并且具有相同目标程序的消息会被分配到一个流中。因此,调用相同的程序的所有消息都出现在单个处理流中。
在每次循环中,一个目标程序可以有多个对应的消息,一个流也会处理多个程序的消息。每个流消息处理的结果会作为一组新消息添加到消息队列中,然后循环往复。
消息处理过程中产生的结果消息通常被发送到另一个地址(原路返回或其他程序)。
让我们来看一下运行一个程序 Gear 虚拟机是如何工作的。
Gear 可以运行任何编译为 WebAssembly 的语言程序,例如这个capacitor.rs
:
static mut CHARGE: u32 = 0
static mut LIMIT: u32 = 0;
static mut DISCHARGE_HISTORY: Vec<u32> = Vec::new();
#[no_mangle]
pub unsafe extern "C" fn handle() {
let new_msg = String::from_utf8(msg::load()).expect("Invalid message: should be utf-8");
let to_add = u32::from_str(&new_msg).expect("Invalid number");
CHARGE += to_add;
if CHARGE >= LIMIT {
DISCHARGE_HISTORY.push(CHARGE);
msg::send(0.into(), format!("Discharged: {}", CHARGE).as_bytes(), 1000000000);
CHARGE = 0;
}
}
完整示例,请参见我们的测试样例。 这是一个简单的程序,对传入的消息“charge”,当“charge”总量超过初始化中提供的某个限度时“discharges”。
让我们来看看下面的几张图,看看用户是如何创建一个程序,然后与它交互的:
- 程序的创建,用户只需发送带有程序和初始化参数的交易:
- 然后 Gear 处理这个新输入:
Gear 的底层使用 WebAssembly(或 Wasm)技术。所有的 Gear 程序都是 WebAssembly 格式。
WebAssembly 是一种用于部署程序的代码格式。在 Gear 中智能合约就是一个 WebAssembly 程序。
WebAssembly 具有以下优点:
-
原生速度。 因为它转换为实际的硬件指令。
-
可移植。 它可以在不同的硬件上运行。
-
安全。 被限制运行在一个安全的沙盒执行环境中(由规范保证)。 WebAssembly 是一项全球性的工业技术,优秀的原因有很多:
-
它是在其领域的所有主要竞争对手之间合作设计和实施的。
-
它是与完整的数学、机器验证的形式化一起设计和发布的。
多年前,软件开发从单体架构逐渐演变成微服务架构。解决了以前模块耦合、代码关联相关的复杂性问题。近年来,纳米服务已成为解决微服务复杂性的主要解决方案。
任何编译为 Wasm 的语言都可以编写纳米服务的函数并编译为程序上传到 Gear的网络中。Gear 会在一定时间内执行代码来处理某个任务。Gear 根据需要处理的任务数量自动进行扩展。您只需为函数消耗的资源付费。
在与 Gear 的其他纳米服务交互时,你相当于获得了一个开箱即用的服务。你只需专注于那些对你有用的功能即可,Gear 会处理其余的部分。