

概述
在前面对 PolkaVM 和 Revive 的文章中,我们介绍了很多技术细节,开发工具。还对比 EVM,知道了 PolkaVM 的优势。很多同学还是对 Polkadot SDK 为什么可以运行 EVM 兼容的智能合约,以及交易处理的整个流程不太清楚。这篇文章将会带着大家梳理一下整个过程,在熟悉它之后,对于今后 Debug 代码,找到出错原因都会有很大好处。
整个处理的流程主要有下面几个模块组成,我们来逐一分析
-
RPC Server 接收交易,并转发到节点
-
Runtime 定义交易的格式扩展
-
Revive 做交易的转换,从 Eth 交易转成 Polkadot SDK 的 extrinsic
-
最终的 bytecode 在 PolkaVM 中的执行

RPC Server
和普通的 Extrinsic 不同,也有别于之前在 Polkadot SDK 上出现的Frontier,Ink 的支持。在提交 Solidity 的交易的时候,我们会启动一个单独的RPC Server来接收交易。因此我们在配置本地的测试环境或者 Passet Hub 的时候,都需要这个 URL。
那么这个 Server 是怎么工作的呢?它的代码也在 Revive Pallet下,package 名字是 pallet-revive-eth-rpc。它主要有三个组件,一个 subxt 的客户端,一个 RPC 服务器,还有一个用来缓存数据的 sqlite 内存数据库。
subxt 主要是和 node 来交互,比如向区块链提交交易,查询数据等。 RPC 服务器来实现以太坊的 Web3 服务接口,大部分函数定义都在 EthRpc 这个 Trait 可以找到。其他还有 debug api 和 health api。
当 server 收到请求,都会转换成对区块链的请求,通过 subxt 的客户端发出,收到结果后返回给调用者。
这里分别举二个例子,一个是查询余额,一个提交交易。

这里的接口定义是 web3 的一致的,通过区块号或者 Hash 来查询余额。下面我们来看下它的实现。

这里 client 就是 subxt 的客户端,它把传进来的区块号,哈希或者 Tag,统一转换成区块哈希,进行查询,并返回结果。这里不用担心 Decimal 的问题,在 Node 一侧,当要查询的是一个Eth格式的地址时,就已经做好了转换。
下面是对发送交易的实现,它会组装一个 Pallet 是 Revive,Extrinsic 是 eth_transact 的交易,然后发给 Node。这个 Extrinsic 非常的特殊,我们接下来会在分析 Revive 代码的时候看下它是怎么处理的。


Sqlite 主要来缓存一些交易的数据,还有事件日志等。

Runtime
在 Runtime 中,需要包含 Revive Pallet 才能处理Eth的交易,还有对Runtime API的实现。一个最重要的代码实现是对 UncheckedExtrinsic 的重新定义,对于不需要支持 Eth 的交易,普通的 sp_runtime::generic::UncheckedExtrinsic 就可以了。为了支持对 Eth 的交易,需要使用 Revive 里面 UncheckedExtrinsic 作为包含中 Block 中的类型。
这样区块链在处理交易池里面的 Eth 交易就可以识别他们,在验证交易的时候把它转换成普通的 Extrinsic。

Revive Pallet
Revive 当然是处理交易的核心,有些设计还比较的巧妙,我们先来看它如何转换Eth的交易。对于一个提交的 Eth 交易,下面这个函数用来对它进行转换,成为普通的 Extrinsic。




首先尝试解码 payload 成一个含有 Eth 签名的交易,并得到发送交易的以太坊地址。随后做一个基本的检查,比 如nonce,gas,chain ID 等等。
下一步就是要转换成 Call 了, 这里首先有一个特殊的地址 RUNTIME_PALLETS_ADDR,它被用来直接调用 Runtime 里面的任何交易。它把输入的数据直接解码成 Call 并调用,除此之外的都会转换成 Revive 的 Call 方法,调用某个智能合约里面的一个方法。如果没有调用的合约地址,那么只能是一个部署合约的方法,会把它尝试转换成一个 eth_instantiate_with_code 的方法。
从这里我们可以看出,从 Eth 来的交易,没有只 upload_code 的方法,当然这个也不在以太坊接口的定义里面。
在下面就是对于 gas_price, gas_fee 的处理,还会把多给的 gas_fee 作 为tip。
最终返回一个 CheckedExtrinsic,它也在这里有了使用 Polkadot 帐号的签名,它来自发送交易者映射到 Polkadot 地址格式的帐号。
这里有一个非常有意思,和难理解的就是 eth_transact 方法,它除了给出一个错误之外没有任何其他逻辑。那么它的作用是什么呢?首先,它提供了一个接口给外部调用,比如我们在 RPC Server 里面使用 subxt 来调用它。其次,我们使用它就可以把提交来的交易解码成这个 Call,得到它的 payload 部分,在用 payload 解码成封装的 Eth 交易。
但是在从 UncheckedExtrinsic 到 CheckedExtrinsic 的转换中,它就被处理了,转换成了其他的方法。call 或者 eth_instantiate_with_code,因此在正常情况下不会有 dispatch 到这个方法里面的交易。这就是为什么它只有报错处理。


当然在 Revive 里面还有很多处理逻辑,包括我们前面文章提到的precompile,这里不再重复介绍。

交易在 VM 的执行
在 Revive 中处理交易中最核心的是一个 Stack 的数据结构,从这个名字来看,这里依然要模拟的还是 EVM 基于栈的处理模型。它的代码如下:


除去基本信息比如区块号,时间戳,还有就是对于计算和存贮的计量。通过 frames 来记录每次对合约更深一层的调用,和函数的逐层调用一样。
transient_storage 是一个在 EVM 中非常特殊的存贮,它在整个的交易中都是有效的,因此它的存贮也在 frame 之外,单独存放。
在进行完一系列繁琐的环境准备之后,代码最终在 PolkaVM 的开始执行。下面是一个非常重要的数据结构 PreparedCall,我们通过它来调用 PolkaVM 去执行合约编译后的 RISC-V 代码,或者说 PolkaVM(它和标准的 RISC-V 有着细小的差别). Module 主要包含的是编译好的程序代码, RawInstance 是一个 PolkaVM 的实例,用来运行虚拟机。Runtime 则是区块链节点提供给它的执行环境。


总结
在这个文章中,我们介绍了交易从发送到 RPC Server,到最终在 PolkaVM 里面运行的整个过程。希望能帮助大家在调试代码的时候有所帮助,能够根据错误信息,找到对应的模块,解决问题。