如果您不了解Moonbeam,用一句话简单概括来说Moonbeam是跨链通信的中心枢纽。像AxelarLayerZeroHyperlane等的协议允许不同EVM上的智能合约互相通信,为Web3 dApp解锁功能方面前所未见的规模。但就目前来说,上述的几个协议的智能合约通信仅限于EVM链。这就是为什么Wormhole与像Solana、NEAR、以及Algorand的非EVM链通信的能力在Moonbeam的互能合约世界中备受关注。

大多数多链dApp目前作为跨多个不同EVM协议的整合接口。使用Wormhole的VAA(Verifiable Action Approval)消息传递系统,dApp能够实现多链部署,在原先孤立的生态系统之间实现进阶的互操作性和功能。

Wormhole与其他Moonbeam涵盖的跨链协议相当不同,所以即便您已熟悉其他跨链协议,您仍需注意以下操作步骤。作为示范,您将完成一个Demo演示,其将通过Wormhole将字符串从一个测试网发到另一个上。该项目需要您安装Docker以运行一个叫中继器的组件。如果您还未设置MetaMask,您可以在文档网站添加Moonbase Alpha网络。

1Wormhole概览

Wormhole是一个多签解决方案协议,通过VAA验证并保护跨链通信。VAA(Verifiable Action Approval)是Wormhole的跨链消息传递格式。简单地说,Wormhole的协议有19个守护者(又称节点/验证者),他们收取并验证跨链消息。如果这19个守护者中的13个验证了某条消息,该消息就会被批准并可在其他链上收到。

与守护者网络(其以Wormhole协议的验证人身份运作)相邻的是网络间谍。他们不进行任何验证工作,只负责机密性的间谍监测行动。他们会监视守护者网络并作为一个页面以允许用户和应用查看哪些VAA已获得批准。

正如其他Moonbeam涵盖的跨链协议,当消息被验证后,称为中继器的链下参与者可以将数据以无信任方式发送至其他区块链。但不同于其他Moonbeam涵盖的跨链协议,Wormhole目前没有一个完成的通用中继器。

中继器的职责是为目标链的执行支付费用,且在许多通用中继器中,中继器又是由用户支付费用。Wormhole目前还没有该功能,所以Wormhole的架构需要dApp开发者创建并维护其自己的专用中继器。一个开发者如果想要合约调用者为目标链上支付gas费用的,则需要设计自己的系统。这可能看起来工作量很大,但这允许对消息处理的方式进行微调。举例而言,一个中继器可以同时发送相同的消息至多条链。

图片来源于Wormhole

要发送跨链消息,你将需要使用一个智能合约。每条连接至Wormhole的链都会有某种Wormhole核心桥接的实现,其功能为发布和验证VAA。每一个核心桥接合约(每条链一个)的实现均由守护者网络上的守护者监视,这便是他们得知何时开始验证一条消息的方式。

作为一名开发人员,您将通过发送和验证VAA直接与核心Wormhole智能合约交互。您还需要运行一个非验证间谍节点和一个专用中继器。本文会将会手把手教你如何进行操作。

2连接SimpleGeneralMessage合约

不同于其他跨链合约,Wormhole并不提供用户可以继承并在其上构建的母合约。这是因为Wormhole的第一条链Solana,不像Solidity一样在其智能合约中提供特有的继承。为了让每条链上的设计体验相似,Wormhole让他们的Solidity开发者直接与EVM链上的Wormhole核心桥接智能合约交互。

本文中要部署的智能合约储存在一个由Wormhole的Relayer Engine存储库分叉出来的Git存储库中。您将通过Remix部署合约,且该合约可自动通过此Remix链接访问。

首先,该智能合约中的代码是基于Wormhole的最佳开发文档,但某些方面(如安全性)已被简化。当您编写用于生产环境的智能合约时,请检查文档以更好地了解其标准。请注意,以下智能合约不能用于生产环境。本教程中的操作仅作演示用途,以便更好地了解系统运作原理。

3发送VAA

在源文件或是下方图片中查看智能合约的消息发送部分。面向外部的函数包括了三个参数:消息字符串、目标地址以及目标链ID。区块链映射的destChainId值可在Wormhole文档中找到。

请注意目标地址是地址类型而不是bytes32类型。这很重要,因为像NEAR、Algorand和Solana这些链用的是bytes32地址(该地址空间更大)。SimpleGeneralMessage智能合约使用地址类型是因为之前的博客中也使用该类型地址,但若你要构建自己的dApp,我们建议用bytes32address输入,从而能够与非EVM区块链进行通信。

函数本身有两行。第一行调用一个将于Wormhole核心桥接交互的私密函数。该功能从sendMessage函数独立出来是因为Wormhole有一个从所有消息发布函数返回一个序列值的设定。该序列值代表一个Wormhole消息被从一个智能合约发送的次数。例如,发送第一条消息会返回0,第二条则会返回1等等。该值对于按顺序接收消息非常有用,本文将不展开描述。

第二行增加全局nonce值(该数值只使用一次)。Nonce可以被再次使用以批处理VAA。在这个情况下,每条消息会有一个唯一的nonce值以确保该nonce实际上是一个nonce。虽然可以将所有消息的nonce都保留为0,系统仍可以运行。

继续深入消息发送的解析,sendMessage和_sendMessageToRecipient调用的私有函数将在下方提及。第一行代码将数据编码为字节负载。这将收件人地址、消息应发送至的chainId、消息发送者以及消息本身进行编码。解析该信息对于中继器和目标互连合约都很重要。

第二行代码与Wormhole核心桥接合约交互以发布消息。请注意输入内容包括一个nonce、负载以及一致性级别值。该nonce值从sendMessage注入且对该情况无任何作用。负载是在前一行代码中编码的内容。一致性级别控制从交易最初发生以来,守护者网络在消息开始验证过程之前应等待的区块数量。出于安全原因,这可能会有所帮助,因为某些区块的最终确定性比其他区块(如以太坊)要慢。若只是用于测试,1应该就足够了。

这就是发送消息的所有内容。如果你尝试过其他的互连合约协议的话,那你可能会注意到没有用源链的Token为目标链的交易支付费用的方式。这是因为在任何可以这么做的协议中,中继器都是由协议管理的。中继器为目标链的执行支付费用,而中继器由用户支付费用。然而,在本示例中,您将运行您自己的中继器。若您想要合约调用者为目标链支付gas费用的话,您需要设计自己的系统。

接下来我们看看SimpleGeneralMessage智能合约是如何接收VAA。

4接收VAA

Wormhole建议将VAA发射器以某种形式列入白名单,这样接收到VAA的函数就可以检查受信任地址。要添加受信任地址,SimpleGeneralMessage包括了辅助函数addTrustedAddress。在生产环境,该辅助函数还应当检查调用者是否有权限添加受信任地址(例如使用OpenZeppelin的onlyOwner修饰符),但对于测试网来说,已是能够达到安全性的最大极限。

接下来是比较重要的部分。processMyMessage(如下方所示)是SimpleGeneralMessage接收VAA的方法。该名字可随意设置,因为自定义中继器可以调用具有任何名字的任何函数。在其他连接的合约协议中,公共通用中继器处理目标链的执行,通常需要一个专门命名的面向外部的函数。然而因为Wormhole dApp开发者运行其专用中继器,接收函数可根据开发者需求设定。

processMyMessage接受的唯一参数是一个叫做VAA的字节对象。开发者无需手动创建该VAA对象,因为Wormhole基本上是自动创建的。VAA带有许多信息,若有效,这将被编码至VM结构。您可以在Wormhole的存储库中完整查看该结构。

第一行代码带有core_bridge.parseAndVerifyVM,通过Wormhole核心桥接链上验证VAA包含的签署是否正确。其返回解析后的数据,一个布尔值来表示成功或失败,若发生错误则会返回一串字符串。如果parseAndVerifyVM函数为其布尔值返回false(VAA无效),则第二行代码将恢复并显示失败原因。

第三行代码,带有myTrustedContracts的require语句,调用上述提及的白名单功能。受信任的合约存储于嵌套映射中,并根据VAA发射器对其检查以确保发射器(源链上的互连合约)是可信的。

第四行代码,另一个带有processedMessages的require语句,检查确保VAA尚未处理。请注意任何有间谍节点的人均可使用VAA,且如果有多个想做同一件事的中继器,则该消息将尝试多次处理。您将在之后看到(第八行代码)processMyMessage函数会用VAA哈希写入以确保其处理的消息不会被处理超过一次。

带有abi.decode的第五行代码解码负载至四个值,以便可以存储消息。注意,其解码方式与编码相同方式相同。

第六行代码,是一个带有intendedRecipient的require语句,检查以确保包含在负载里该消息的intendedRecipient与智能合约地址相同。通过Wormhole协议发送的跨链消息仅仅是被验证发生。其并不验证一条信息是否要发送给一个或多个特定合约。这就是为什么合约必须手动检查收到的信息是否使用于它。相似的,第七行代码检查以确保信息是发给正确的链。

第八行代码,添加到processedMessages映射,写入智能合约中的映射,以确保相同的信息不会被解析两次(如上所提及)。目前该信息尚未完全处理,但建议完成此步骤,以免进一步复杂交互导致重入攻击影响其他重要的合约。

最后是带有lastMessage的第九行代码,从源链发送到目标链的字符串被写入智能合约。

实践是最好的学习方式,因此请您尝试跟随此演示教程,自己在Moonbase Alpha上部署和执行消息传递。

5通过Remix在Moonbase Alpha上部署Wormhole合约

部署一个Demo合约最简单的方式是通过Remix。您将需要DEV以在Moonbase Alpoha上部署,如果您尚未拥有DEV,您可以从我们提供的Faucet获取。

如需部署脚本,首先请将合约复制并粘贴至Remix,或通过Remix链接访问。

  1. 前往Solidity Compiler标签

  2. 点击Compile按钮

  3. 然后进入Remix的Deploy & Run Transactions标签

  4. 将环境设置为Injected Web3,此设置将会把MetaMask作为Web3的提供者。请确保您已将MetaMask连接至Moonbase Alpha网络

如需在每条链上部署,您将需要Wormhole核心桥接的本地实例以及提及的每条链的链ID。如下所示为选定的几个测试网提供了这些数据。您可以在Wormhole的文档网站找到其他网络的端点。请注意,为本文设计的智能合约和中继器仅支持EVM,因此在本次演示只能使用EVM。

当您在Moonbase Alpha上成功部署合约后,请确保复制其地址并使用连接到Wormhole的任何EVM测试网重复该流程,以确保它可以跨链发送消息。

6将Moonbase Alpha的互连合约列入白名单

如前所述,Wormhole建议在互连合约中包含一个白名单系统,您将在尝试发送跨链消息前用于SimpleGeneralMessage。

请注意若要添加一个白名单的合约,您必须调用addTrustedAddress函数,这需要一个bytes32格式的address以及一个链ID。您可以在上述表格以及Wormhole的文档网站找到链ID。

您可能会注意到发送者参数是bytes32而非一般地址。虽然本文只打算使用EVM,但Wormhole的VAA以bytes32的形式提供发射器(源)地址,所以它们以bytes32的形式存储和检查。

若要将地址类型转换为bytes32,您将需要再加上24个0。这是因为地址的值是20个bytes,小于bytes32的32个。每个byte有两个十六进制字节,所以:

(32 bytes — 20 bytes) * 添加2个0/byte = 添加24个0

例如,如果您的互连合约地址是0xaf108eF646c8214c9DD9C13CBC5fadf964Bbe293,您需要将下列内容输入至Remix:

0x000000000000000000000000af108ef646c8214c9dd9c13cbc5fadf964bbe293

现在我们将继续使用Remix来确保您的两个互连合约相互信任。若您要来回发送消息,您必须对已部署的两个合约执行此操作。若要在不同链上切换合约,通过MetaMask连接至目标网络。

  1. 确保您处于Injected Provider环境

  2. 确保您使用的是正确账户

  3. 确保合约仍是SimpleGeneralMessage

  4. 最后,将目标合约的地址粘贴至At Address输入框

添加受信任的远程地址:

  1. 在部署的合约中找到并打开addTrustedAddress函数

  2. 当您在Moonbase Alpha上时,将发件人设置为您在部署在其他EVM测试网上合约的正确格式地址(即添加了24个0的格式)

  3. 将_chainId设置为另一个合约部署至的链的Wormhole chainId。之后,在MetaMask中交易并确认

当您在另一个EVM测试网时,将发送人设置为您在Moonbase Alpha部署的合约的正确格式的地址(即添加24个0的格式)。将_chainId设置为Moonbase Alpha的Wormhole chainId (16)。最后,在MetaMask中交易并确认。

在本章节中,您应该已经将两条链上的两笔交易发送到两个合约中的白名单地址。之后,您应该被允许在互连合约之间发送消息。

7如何运行一个Wormhole测试网中继器 & 守护者网络间谍

现在您将为Wormhole运行一个测试网中继器!本教程是基于Wormhole的relayer-engine Github存储库的,截至本文撰写时,该存储库已提交至dac6012。目前仍处于开发阶段,文件夹结构可能会发生比较大的变化。

克隆专门为与SimpleGeneralMessage交互的relayer-engine分叉。运行该中继器需要Docker和npm,所以请确保您已将其安装到您的设备上。

首先需要设置。使用npm包管理器通过命令行安装依赖项。

完成之后,查看不同的文件夹。有两种主要的文件夹:relayer-engine和example-project。relayer-engine包含帮助运行中继器的组件,而example-project文件夹包含特定于SimpleGeneralMessage智能合约的插件脚本和配置文件。此外,在根目录中还有一个README.md文件,其将包含有关中继器以及如何设置中继器的更多信息。

但是在深入了解如何运行任何东西或任何插件脚本如何工作之前,您首先需要了解中继器的不同组件以及中继器的作用。

中继器过滤并接收来自守护者网络的VAA,并对此进行处理。在此情况下,收集器会过滤来自您部署的互连合约通过批准的消息,然后解析该VAA,确定其目的地,最终尝试在目的地执行一个叫做processMyMessage的函数。您需要了解的是来自其他参与者的中继器可以收到这个VAA且其他中继器可以通过任何方式执行任何VAA。

从技术角度来看,该中继器的实现分为四个部分。

  1. 一个监视所有VAA的Wormhole守护者网络的非验证间谍节点

  2. 一个称为监听器的组件,其收集任何间谍节点的输出,过滤出与中继器相关信息,然后将其打包至工作流对象中

  3. 存储监听器输出的工作流对象的Redis数据库

  4. 一个称为执行者的组件,从数据库中弹出工作流并以某种方式处理(在此情况下,在目标链上发送交易

有一个负责间谍节点的很容易启动的docker容器。relayer-engine包存储于一个类似于命名的文件夹中,其中包含用于监听器和数据库的大部分代码。执行器的大部分逻辑都将依赖于开发者编写的插件(即从存储库克隆的插件),但大多样板代码仍由relayer-engine包处理。

最好按顺序处理这四个组件的配置和设置,所以从间谍节点开始。首先,在命令行,确保您目前位于example-project目录中。间谍节点使用docker,所以在尝试运行节点前确保docker处于活跃状态。启动容器的命令很长,所以为了简化步骤,已经以npm脚本形式添加。您只需运行:

首先,您应该能看到一些来自于docker容器启动的日志。然后,很多日志应该会向控制台发送垃圾信息。这些都是通过Wormhole测试网的所有VAA,可以看到其数量之庞大!但您无需解析任何日志,代码可以帮助我们完成此步骤。让它在后台运行并获取另一个终端实例以进行下一步。

8监听器组件

现在我们要解析中继器的自定义代码以及可配置组件。监听器组件名副其实将监听间谍代码以获取相关消息。若要定义什么是相关消息,您必须编辑一个配置文件。

在example-project/plugins/simplegeneralmessage_plugin/config/devnet.json中,有一个叫做spyServiceFilters的数组。数组中的每一个对象会将与中继器相关的合约VAA列入白名单。该对象获取一个chainId(Wormhole chainId)以及一个emitterAddress。例如,在下图中,第一个对象会监视由0x428097dCddCB00Ab65e63AB9bc56Bb48d106ECBE在Moonbase Alpha (Wormhole chainId是16)上发送的VAA。

请确保编辑spyServiceFilters数组以便中继器监听您部署的两个合约。

在simplegeneralmessage_plugin文件夹中,打开src/plugin.ts。该文件包含监听器和执行器组件的插件代码,但注释已明确说明哪些函数与哪个组件相关。该文件的片段如下所示,请遵循教程操作。若没有执行操作,您可以在Github repository获取整个文件。

接下来看下方的getFilters函数。spyServiceFilters对象被注入至getFilters所属的插件类别中。注意该过程中没有发生任何过滤,这仅仅是过滤器的准备工作。VAA的真正过滤发生在relayer-engine包中,使用此getFilters函数来了解要过滤的内容。

如果开发者想要向过滤器添加额外的逻辑,可在此处完成。就目前而言,只需列出一些硬编码的地址即可。

过滤完后,监听器需要在下方consumeEvent函数中将工作流数据写入Redis数据库。

工作流其实仅仅是执行器用于正确执行所需的来自于监听器的数据。在这种情况下,添加至工作流的唯一信息是收到VAA的时间以及VAA本身的解析数据本身。如果开发者想要将更多相关信息添加至工作流,他们可以在workflowData对象中操作。

nextStagingArea对象是使用的事件(过滤后的VAA)相互影响的一种方法。例如,如果一名开发者想要将两个VAA打包至一个工作流,他们不会每次都返回一个workflowData。相反,他们会将VAA留在nextStagingArea对象中。下一次使用事件时,注入到consumeEvent函数的stagingArea对象可以使用之前的VAA。在这种情况下,中继器只是按顺序处理每个工作流。

这就是所有关于监听器组件的所需内容。大部分代码藏于relayer-engine包中,对用户不可见。

如果您还记得组件列表的话,第三个是Redis数据库组件。所有与数据库相关的内容也对用户隐藏,因为relayer-engine包会从其写入和阅读,并将任何相关数据注入插件代码。

9执行器组件

最后,您必须处理执行器组件。执行器组件从Redis数据库获取工作流数据并对该数据进行某种执行操作。对于大多数中继器来说,该执行会包括一个链上交易,因为一个中继器相当于VAA的无信任预言机。

relayer-engine包帮助插件处理钱包。目前,该包仅支持Solana和EVM钱包,随着进一步发展将会支持更多链。但将NEAR或Algorand集成至中继器并非无可能,因为除了包已经提供的钱包处理系统外,您只需要再编写一个自己的钱包处理系统即可。

若要使用包提供的内置钱包处理系统,在example-project/relayer-engine-config/executor.json.example打开文件。该示例脚本旨在为您展示如何格式化您的密钥(当前的密钥由Worhole提供)。

将示例文件重命名为executor.json。在executor.json的对象privateKeys中,将每个数组的内容替换为您的密钥。密钥条目的账号会是在执行器组件中支付执行费用的账号。

泄露密钥可能导致资金流失,因此请妥善保管您的密钥。虽然executor.json在存储库中被git忽略,请确保您在测试网使用的钱包中没有任何主网资金。

如果您正在使用一条并未在上方EVM测试网列表中列出的链,您将需要添加您自己的数组。该数组的密钥应该在另一个您之前决定部署的EVM的Wormhole chainId。例如,如果您在Fantom TestNet上部署,您将添加以下对象,因为Fantom TestNet的Wormhole chainId是10。

现在执行器的钱包已处理完成,我们看看执行器代码本身,其存在于example-project/plugins/simplegeneralmessage_plugin/src/plugin.ts文件中。若您没有跟随操作,可以在Github存储库获取整个文件。

handleWorkflow函数是逻辑中心,不过其下也有一些辅助函数。这是relayer-engine包在Redis数据库中有工作流要被使用时调用的函数。注意注入函数的三个参数:workflow、providers和execute。

  • workflow对象在监听器组件的consumeEvent函数执行时提供存放在数据库的数据。在这种境况下,仅有VAA以及其被接收的时间会被存放至数据库,其被存放在本地负载变量中

  • providers对象注入ethers以及其他链提供商,这对于查询链上数据或进行其他区块链相关操作可能有帮助。如之前所述,目前被该包支持的提供商仅有Solana和EVM。providers对象不被用于实现

  • execute对象中目前有两个函数:onEVM和onSolana。这些函数需要一个Wormhole chainId和一个有钱包注入的回调函数。包含的钱包是基于在executor.json文件中配置的密钥

该函数要做的第一件实质性的事情是解析负载对象,然后通过一些辅助函数解析其VAA。之后,它带着负载,将其转换至十六进制格式,并使用ethers实用程序将负载ABI解码至其在智能合约中定义的独立值。

有了ethers解码的数据,我们可以知道负载所传送至的目标合约以及目标链,因为数据被打包至消息中了。该函数检查指定的目标chainID是否属于一个EVM,并将使用上述的execute.onEVM函数执行。否则,它将记录一个错误,因为系统会因简单起见而不与非EVM链交互。

在回调函数中,其使用ethers包创建一个合约对象。其导入的ABI从SimpleGeneralMessage合约的编译中导出,所以该代码假设VAA中指定的消息接收者是或从SimpleGeneralMessage合约继承。

然后,该代码会尝试用VAA执行函数processMyMessage,这之前被定义为消息中继至的函数。该函数名字是为智能合约随意设置的,因为中继器可以指定调用任何函数。这说明了开发者能够更改该中继器代码的能力!

最后一步就是检查example-project/relayer-engine-config/common.json。该配置文件控制着整个中继器的执行。请确保您在使用的TestNet EVM是列出在该文件中supportedChains对象内的。如果其未被列出,该插件不会正常运行。如果您在运行的一条链未被列出,您将需要以下列格式从Wormhole的开发者文档导入数据至配置文件。

该中继器还有许多其他配置。例如,模式字符串设置为“BOTH”以确保使用监听器和执行器插件,也可根据开发者需求选择只运行其中一个。此外,还有多个日志级别可供指定,如错误消息的“error”。然而,在本次演示中只需保留配置设置即可。

配置这样就行了!现在需要运行它。在您的终端实例(未运行间谍节点的实例),找到example-project文件夹。运行下列命令:

您应该会在控制台中看到类似以下日志内容。

一切就绪后,我们可以开始下一步操作。

10通过Wormhole从Moonbase传送跨链消息

现在,要想发送一条跨链消息,您只需要调用sendMessage函数。

使用Remix接口。此范例将向Fantom测试网发送跨链消息,但您可以将destChainId替换成您想要的任何EVM。接着,检查以下事项:

  1. 环境为Injected Provider,网络为1287(Moonbase Alpha)

  2. 您的钱包里有来自faucet的大量资金,以支付交易成本和目标链Gas包含的DEV

  3. 在sendMessage调用的message输入框中输入您选择的短消息(在本示例中为“this is a message”)

  4. 将您的SimpleGeneralMessage实例的地址放在目标链的destAddress输入框中

  5. 将目标链的Wormhole chainId放入sendMessage调用的destChainId输入框中

  6. 当完成所有步骤后,请执行交易并在MetaMask中确认

11追踪跨链消息

在您发送交易后,您应该能够进入Moonbase区块浏览器使用其交易哈希查看交易。如果一切顺利,交易应当能够被成功确认,而当您以UTF-8格式查看时,您将能够在最底部看到交易输入的痕迹。

在一般的交易中,交易的状态和数据将可在一个浏览器的页面上看到。但是,由于这是跨链消息传递,因此实际上在两条链上发生了两笔EVM交易。

在Moonbase Alpha发送消息后的60秒内,您应该能够在控制台中看到您的中继器输出一些日志。请注意中继器打印出许多关于VAA的信息,包括消息本身。任何中继器都能够看到所有您的VAA中的信息。

如果一切顺利,交易将被批准,您将能够看到跨链交易成功后在源链中更新的lastMessage!如果它没有自动更新,请不要担心。一般来说,交易完成大约需要几秒钟的时间。

如果您想查看储存在合约中的消息,您可以通过Remix操作。首先,通过MetaMask连接到目标网络。接着,确保您处于Injected Provider环境中,并且选择的合约仍然是“SimpleGeneralMessage”。然后,获取目标合约的地址,并将其粘贴到At Address输入栏中。

  1. 点击后您应该可以使用结果合约来查看lastMessage按钮

  2. 在输入框中粘贴您的钱包地址

  3. 最后,点击call来查看结果出现的消息

12深入了解跨链互连合约

Moonbeam对成为可互操作网络中心的愿景不止于此。您可以在Wormhole网站上了解更多信息:https://wormhole.com/

包括Wormhole文档中的如何发送跨链消息:https://book.wormhole.com/

您也可以阅读了解互连合约是如何将Moonbeam定位为区块链互操作性的领导者:https://moonbeam.network/builders/connected-contracts/