Animagus 系列文章[1]: 开篇介绍
Xuejie Xiao
Nervos Core Team我们相信 Nervos CKB 在新一代的区块链中具有无限的潜能。但同时也需要独特的开发工具才能将 CKB 的潜能充分激发。 Animagus 就是在这种情况下应运而生。
「animagus」的名字来自《哈利·波特》,如果巫师具有将自己变成动物再变回来的本领,就会被称为「animagus」,不同的巫师可以变的动物也有所不同。我个人认为为我们现在的这个项目取名 animagus(阿尼玛格斯)是很适合的,希望大家看完本文后也会有这样的感觉。
什么是 Animagus?
表述 Animagus 的功能可以有很多说法(接下来提及 Animagus 时,意指 CKB 的软件项目,而不是 Harry Potter 中的 animagus )。有人说这是一个 dapp 框架,也有人说这是 CKB 之上的一层,而目前我比较倾向的描述是「 CKB 的 account 层 」。
如今主流的区块链使用的是 account 模型,而 CKB 借鉴了比特币的设计思路,使用了类似于 UTXO 的模型。 虽然 UTXO 模型有很多优点,但与 account 模型相比,有一个明显的缺点:编程难度更大。
这正是 Animagus 要解决的问题。在本文及以后的文章中你将会看到, Animagus 为 UTXO 模型中遇到的大多数编程难度问题提供解决方案 。我们希望 Animagus 能够填补 UTXO 编程模型的空白,真正发挥出 CKB 的全部优势。
从技术角度讲,Animagus 可以看作是 「AST runner」 :
开发者将自己想要实现的功能设计为 AST(Abstract Syntax Tree)。(如果你对 AST 感到陌生,暂时你可以假定它只是一个简短的程序)
AST 将以 Protocol Buffers 的格式表示,因此 Animagus 仍然是language agnostic。
启动时,Animagus 加载 AST。根据 AST 的定义,Animagus 可以提供以下不同的行为:
它可以读取 CKB 状态并为符合预定义条件的某些 cells 建立索引,例如所有存入 Nervos DAO 的 cells;
它可以提供一些 RPC,开发者可以从其他服务中调用这些 RPC 以获取某些结果,例如获取账户余额或者组建一笔转账交易;
它可以根据 AST 定义生成链上使用的验证器智能合约。
举例(Example)
这些听起来很复杂,请允许我用一个真实的例子来说明这是如何工作的:在 UTXO 模型中,account 可以看作是一组有效 UTXO(或 CKB 术语中的 cells), CKB 中的账户余额就是这组有效 cells 的 capacities 之和。
CKB 从未提供过可以获取任意账户余额的 RPC 接口。目前 CKB 节点的索引器可以获取索引 cells 的当前余额,但是如果我们想要获取任一随机账户的余额怎么办?这时就可以通过 Animagus 来帮忙:我们会在此处设计 AST,以便在 Animagus 启动时提供一个RPC,该 RPC 可用于获取任何账户的 CKB 余额。
为了使我们的示例帮助你更好地理解 Animagus,我们还创建了以下限制:
我们的 AST 仅支持使用 CKB 默认锁定脚本的账户。
args
实际定义一个 cell 的所属账户。在这种情况下,我们将编写 RPC 结构,使其接受一系列字节,然后当我们将这一系列字节作为args
(当使用默认锁脚本时,code_hash
和hash_type
是固定值),RPC 将返回帐户余额。
如上所述,Animagus 是 language agnostic。虽然 Animagus 本身是用 Go 编写的,但是只要你选择的语言支持 GRPC,你就可以自由使用任何语言来构建 AST 和调用 Animagus 。在本系列的示例中,我们将使用 Ruby (当然并不仅限于 Ruby)构建 AST 并调用 Animagus。
构建 Animagus AST 需要两个部分:查询和转换。
查询(Querying)
查询在 Animagus 中进行有两个用途:
索引器使用查询来索引所需的 cells
RPC 服务器使用查询部分来获取服务 RPC 所需的 cells
在上面的余额示例中,查询的是指定账户的所有 cells,实际上查询完全取决于定义的 AST。 不同定义的 AST 所要查询的 cells 会不同 ,可能是查询多个账户中的 cells,或者是查询具有其他共同点的 cells 等。稍后在转换阶段,我们将从所有查询到的 cells 中提取 capacities,并进行求和以获得最终余额。
要在 Animagus 中定义查询部分,我们需要做的就是 定义一个函数 ,该函数对于我们想要查询的 cells 返回 true
,否则返回 false
。以下代码片段显示了在 Ruby 中如何定义这样的函数:
PARAMS = ["0x12345678ab12345678ab12345678ab12345678ab"]def filter(cell) cell.lock.code_hash == "0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8" && cell.lock.hash_type == "type" && cell.lock.args == PARAMS[0]
我们使用 PARAMS
表示 RPC 客户端提供的参数。在这种情况下,RPC 请求接受的参数表示为账户所有者的脚本 args
部分。为了使它更像 AST,我们可以稍微修改一下 Ruby 代码:
PARAMS = ["0x12345678ab12345678ab12345678ab12345678ab"]ARGS = [cell]def filterARGS[0].lock.code_hash == " 0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8" &&ARGS[0].lock.hash_type == "type" &&ARGS[0].lock.args == PARAMS[0]
虽然通常情况我们不会以这种方式编写代码,但它看上去会更接近于与实际的 AST :特殊的 ARGS
和 PARAMS
节点用于表示传递给函数的实参和形参(通过父 AST 节点或 RPC 用户传递)。
提示:注意这里使用了两个 args :我们用 ARGS
来表示一个特殊的 Animagus AST 节点,代表 AST 的一部分,实际上是一个函数;执行 AST 时, ARGS
将由父节点填写。而 script args
表示 CKB 脚本结构中的 args
部分,表示账户所有者,由用户填写。
现在我们可以基于以上代码片段来构建 AST:
这就是 AST 的含义 :它是一段代码的树形表示,其中代码中的值成为了树中的节点,代码中的操作/函数成为了带有子节点参数的节点。AST 实际上广泛应用于编译器技术中,几乎所有我们使用的编程语言都是在执行更多操作之前先解析成 AST 的。
有了查询 AST 函数后,我们可以将其提供给一个特殊的 QueryCell
节点,该节点随后完成查询部分。
出于空间考虑,我们省略了前面的 AST 图中已经存在的某些子节点。
Why AST?
你可能会有一个问题:为什么这里要直接使用 AST? 为什么不是构建一种小型的编程语言呢? 我们的确在设计阶段就考虑过这个问题,但出于以下几个原因,我们最终持反对票:
我们的编程语言太多了 。无论是在通用编程语言还是智能合约专用语言上,似乎每个人都在忙着实现自己的语言,每个人在语法方面都有自己的偏好。现在的结果是,每当有人想做一个新的区块链项目,要做的第一件事就是学习一种新的编程语言。而我们想要改变这一点,所以 我们只定义这里的核心 AST 。每个人都可以自由地实现自己想要的语言风格,并将自己的迷你语言编译成 Animagus AST。这样,我们就可以在使用 Animagus 时确保最大的自由。
可以直接编译成 AST 的不只是编程语言 。有深度学习背景的人都知道,计算图和 DSL 在某些领域非常流行。在这些情况下,你可以继续使用现有的语言,并使用各种 DSL 来构建所需的逻辑。之后你可以导出通过 DSL 定义的复杂逻辑,并在现代 GPU 上运行它们。后续我们将展示如何在纯 Ruby 语言中以这种方式定义 Animagus AST。
AST 的功能也不仅仅体现于编程语言 。在不同的领域,很多人甚至在不用编程的情况下就可以构建出非常棒的东西。例如,有才能的游戏设计师每天都在通过 Unreal Blueprint 之类的工具开发极具特色的新游戏,他们并不需要使用编程语言来实现他们对游戏机制的选择。 我们相信在一个多样化的未来,通过直接提供 Animagus AST,也可以实现蓝图式的 dapp 构建,没有编程技能的人才也可以制作他们需要的 dapp。
简而言之, 正如 CKB 为区块链世界提供了最大的灵活性一样,我们相信基于 AST 的设计也为 Animagus 提供了最大的灵活性,从而释放出更多的可能性。
转换(Transforming)
有了查询部分,我们就可以开始创建转换阶段了。这一阶段还需要另外两个函数。第一个函数用于从 cell 中提取 capacity:
第二个函数是把两个 capacity 值加在一起:
正如这里所看到的,并不是所有的 AST 都要非常复杂,有些可以非常简单,只要它们能够满足其目的。
现在我们可以把转换部分组合在一起:
上图中有 2 种新的节点类型:
- 「Map」接受列表(在本示例中,指 QueryCell 节点返回的 cells 列表)和函数。它将该函数应用于列表中的每个元素并返回结果列表。
- 「Reduce」接受列表、函数和初始值。它应用于具有初值和元素的函数,使用返回值替换初值,并一直执行下去,直到列表中的元素用完为止。「reduce」的结果是一个单独的值。
如果你熟悉函数式编程,那么你一定会喜欢这些简单的 map/reduce 函数。这样, reduce 节点包含转换后的值,即指定账户中所有 cell 的总 capacity。
拼构(Piecing Together)
剩下的工作就比较简单了,我们给 RPC 方法起一个名字,将其放在一个 Call
节点中,然后创建一个包含它的 Root
节点:
当 Animagus 启动时,它接受 Root
节点的 protobuf 序列化消息。一个 Root
节点可能有多个 Call
节点(后续我们将看到它也可能包含多个 Stream
节点,但这超出了本文的范围)。Animagus 会对每个 Call
节点进行分析并执行每个操作:
- Animagus 会从所有
Call
节点中提取所有QueryCell
节点(注意一个Call
节点可以有多个QueryCell
节点)。对于每个块中的每个新 cell,Animagus 会根据QueryCell
节点中描述的功能运行这个 cell。如果该函数返回 true,则 Animagus 将存储这个 cell 的记录(也称为这个 cell 的索引)。 - 当向 Animagus 发出 RPC 请求时,Animagus 会找出相应的
Call
节点并运行 AST。当遇到QueryCell
节点时,Animagus 将基于索引记录获取相应的 cells。
如上所示, 我们实现了一种通过索引 cells 并提供 RPC 调用来获取每个账户的 CKB 余额的方法。
小结(Conclusion)
通过设计 Animagus,我开始意识到,从编程的角度来看,account 模型和 UTXO 模型可能并没有太大的不同。虽然 account 模型直接提供 account 状态,但 UTXO 模型只是将 account 状态散布到一组 UTXOs(或 CKB 的有效 cells)中。像 account 模型一样,我们可以使用 Animagus 之类的工具将 UTXOs 组织成可直接读取的形式。
在下一篇文章中,我将向大家展示如何在 Ruby 中构建 balance AST,并在 Animagus 中真正地运行。