深度详解 Revive 和 Precompile 技术路径

Revive 和 Precompile 介绍

Polkadot 在 2.0 里面引入了新的 PolkaVM 来支持智能合约的运行,并且使用 Revive Pallet 兼容 EVM。 Revive 作为 polkaVM 最核心的 Pallet,开发和重构一直都很活跃。自从上一篇文章 给大家介绍了 precompile 的实现原理和调用之后。precompile 部分又有了很大的改动。

为了不对一些词汇产生歧义,列出其英文和中文意思,并在之后用英文来描述。

  • Precompile: 预编译智能合约
  • Builtin: 内置的预编译智能合约
  • External: 外部预编译智能合约
  • Multiple:一组预编译智能合约

主要改动包括以下几个部分

  • 对 precompile 分成了二个类型,buildin 和 external。buidin 由 revive 来定义,任何包含了 revive 的链都有。 external 可以定义在任何地方,通过 runtime 来加入。一般来说,如果你的pallet想把自身的功能通过 evm abi 接口暴露出来,由其他的智能合约来调用,那么就可以通过一个 struct,实现 precompile trait 来实现。
  • 实现了 precompile 调用接口和一般合约的一致性,都是通过 call 函数来调用。即使 precompile 在匹配到了地址之后,不在需要这个信息,也把 address 作为一个参数,只是不会用到它。calldata 作为参数放到函数的第二个参数。
  • 实现了对 multiple 的实现,如果你需要暴露一组合约,类似我们将到看到的 ERC20 的实现。它可以分配一段连续的地址给 multiple。它可以通过 suffix 来匹配是否是这个 multiple,然后通过 prefix 来区别具体哪个合约。
  • Precompile 可以用映射到一个帐号,存放合约状态到链上,接收 token。也可以没有这个帐号,通过一个的 HAS_CONTRACT_INFO 的属性来控制。

代码分析

precompiles 的结构和定义

External 的注入,在链的 runtime 定义时,pallet_revive 有一个属性就是 precompiles,用来加入 external。比如这个 runtime 加入了 ERC20 和 XCM 二种类型。

对于 builtin 的,是不用在此定义的,它默认会包含在里面。所有的 precompiles 是这样定义的。它是一个包含二个元素的 Tuple,第一个是所有 builtin,第二个是 external。

这个数据在每次构造合约的执行环境的时候,会传递给 VM,每次调用合约的时候会使用地址来看是否是这个 Tuple 里面的。它的比较方式是实现了一个对整个 Tuple 的 get 方法,如果找到了就返回 Some。它还默认实现了对地址冲突检测。


Precompile 地址空间和匹配算法

首先对于任何 precompile,都可以是一个单独的合约,或者是 multiple。在代码中我们用 Fixed 和 Prefix 来描述。地址的分配逻辑比较复杂,也存在一些不合理的地方。我们先来看 builtin 部分。

对于一个单一的 builtin 合约,它占用四个字节,可以转换成一个 u32 的数值,注意它不能是 0。那么它的 EVM 地址格式就是前面1 6 个字节都是 0,后面四个是 builtin 合约编号,共同组成一个 20 字节地址。

如果是一组 builtin 合约,它后面四个字节是相同的,但是会把前面的四个字节保留下来,作为单独合约的地址参数。具体的使用方法会在下一篇介绍 ERC20 precompile 实现详细解释。

External 的定义也是类似的,代码如下

不同的是它只接收一个长度为 16 的数作为参数,把它放在第 17 和 18 二个字节。19 和 20 都是 0,这样可以和 buitin 分开。但是由于 builtin 的地址长度是 4 个字节,其实也存在冲突的可能性。因此在代码注释部分有说明。要求值不要超过 u16 的上限。这个实现方式笔者认为不太合理,如果这样最好只使用最后二个字节,6 万多个地址可以使用,也完全足够。这个实现可能是因为想和 external 对齐地址长度。好在 builtin 的合约是有 revive pallet 来提供,一般不会使用超出这个范围的值,维护者有足够了解这个限制。

External 也分单一合约和 multiple 的情形,实现基本相同,不在赘述。

当所有 precompiles 都有了自己的地址空间,匹配就很简单了,根据传入的地址就知道是否是 precompile。

统一调用方法 Call

在之前的实现中,precompile 是通过定义一个 execute 方法作为入口。在重构之后所有的合约调用都使用 call 函数。函数的定义如下

这个函数定义在一个叫 PrimitivePrecompile 的 trait 中。cal 函数定义十分简单,包含三个参数,地址,输入参数和执行环境。 输入参数对应于 EVM 的 calldata,它是具体合约里面函数的 selector 和输入参数序列号之后的字节数组。

第三个参数 Ext 作为执行环境提供了和链提供的一些方法交互,比如取调用的 caller,origin,合约的 balance,当前区块号等等信息。

和call类此的方法是 call_with_info,它的第三个参数是 ExtWithInfo,它主要用在一个 precompile 有相应的数据存放在链上的场景。它多出来的几个方法是 instantiate,实例化一个合约。 set_storage 存储数据到链上等等。实例化主要是生成对应的帐号,并存放基本信息上链。

对于 precompile,address 这个参数在调用的时候是用不上,因为地址完成了匹配才会调用到 precompile。所以对应单一合约这个参数直接被忽略。但对于 multiple 来说,地址的前面四个字节包含了我们自己定义的某些信息,这个时候就需要解析它了。

Precomile trait 和 SolInterface

对于 builtin,前面介绍的 PrimitivePrecompile 就足够了,因为它的实现足够简单,对应于 EVM 里面直接使用合约地址和 calldata 来调用,那么它就会调用到call函数里面,calldata 就是参数的字节序列。

对应复杂的合约,里面会包含很多不同的函数,在合约内部根据函数的 selector 来分发。Precompile 里面的 Solinterface 就是起到这个作用,通过这个 interface,可以找到合约里面的具体方法,并解码参数。这里使用了 Alloy 的 Rust 库。

HAS_CONTRACT_INFO 如何影响合约的执行

从 builtin 的实现,我们可以看到有一个属性 HAS_CONTRACT_INFO 都是设置为 false 的。那么它的作用是什么,什么情况下应该设置为true呢。

如果当它设置为 true,那么当这个 precompile 首次被调用的时候,会创建一个对应的帐号,通过 H160 向 AccountId 的映射。还会把合约的基本信息放到链上。 有了帐号,那么 token 的操作就成为可能,同时也把合约的状态写到了链上。

当 precompile的 HAS_CONTRACT_INFO 是 true 的时候,每次就会调用 call_with_info 了,因为只有它的第三个参数需要有我们提到的 instantiate, set_storage 等方法和链上存贮交互。

这个实现看上去有些复杂和难以理解,应该有通过 Option 来重构的空间,使得调用更加一致。

如何调用 precompile

**对应 builtin 的调用,我们可以写一个简单的合约,去调用 sha256。**这个合约和上一篇文章一样,地址也没有改变。所以说重构对开发者并没有什么影响,所有接口都是兼容的。


我们打开 polkaVM 的 remix,在浏览器中输入 remix.polkadot.io

把这个合约加到一个源文件中,并编译和部署。得到地址后,调用 callH256 函数,测试数据可以从这个文件得到, 里面包含输入和输出的正确结果。 运行结果截图如下

更多的 precompile 调用可以参考这个文档。对于 external 调用,我们会在下一篇文章中介绍。

总结

通过对 Revive 最新代码的分析,特别是 precompile 的实现,我们看到了更具扩展性的实现。从 runtime 当中的所有 pallet 的方法,都可以通过 Solidity 的 ABI 接口暴露出来,我们可以任意的组合,外部的合约可以调用 runtime 里面的任意方法,或者是一些方法的组合

下一个篇文章将会主要介绍 external 的实现细节,也会有对应的代码示例。

相关推荐
Nejosi_念旧2 小时前
解读 Go 中的 constraints包
后端·golang·go
风无雨2 小时前
GO 启动 简单服务
开发语言·后端·golang
小明的小名叫小明2 小时前
Go从入门到精通(19)-协程(goroutine)与通道(channel)
后端·golang
斯普信专业组2 小时前
Go语言包管理完全指南:从基础到最佳实践
开发语言·后端·golang
前端_学习之路4 小时前
React--Fiber 架构
前端·react.js·架构
一只叫煤球的猫4 小时前
【🤣离谱整活】我写了一篇程序员掉进 Java 异世界的短篇小说
java·后端·程序员
你的人类朋友5 小时前
🫏光速入门cURL
前端·后端·程序员
aramae7 小时前
C++ -- STL -- vector
开发语言·c++·笔记·后端·visual studio
lifallen7 小时前
Paimon 原子提交实现
java·大数据·数据结构·数据库·后端·算法
怀揣小梦想8 小时前
微服务项目远程调用时的负载均衡是如何实现的?
微服务·架构·负载均衡