调试的技巧与窍门

调试 CCIP dApps 的简要概述

欢迎来到关于调试 Chainlink CCIP dApps 和排查跨链消息的发送与接收问题的附加章节。编写代码只是开始,真正的技能往往在于有效地排查和解决出现的问题。

开始

要了解问题出现的地方或可能出现的问题,您首先需要收藏整个跨链消息传输流程:Processing CCIP Messages

第二个最重要的事情是理解 EVM 中的 ABI 编码/解码工作原理。当您编译 Solidity 代码时,两个主要输出是应用程序二进制接口(ABI)和 EVM 字节码。

EVM 字节码是以太坊虚拟机执行的机器码,该字节码会被部署到以太坊区块链上。它是一组底层的、十六进制编码的指令集,EVM 可以解释并运行这些指令。字节码以可执行的形式表示智能合约的实际逻辑。

ABI 本质上是一个 JSON 格式的文本文件,描述了您的智能合约中的函数和变量。当您想与已部署的合约交互时(例如,从 Web 应用程序调用一个函数),ABI 用于将函数调用编码成 EVM 可以理解的格式。它是高级应用程序(如 JavaScript 前端)和在以太坊上运行的低级字节码之间的接口。ABI 包括关于每个函数的名称、返回类型、可见性(公共、私有等)以及其参数类型的详细信息。

解码错误信息

每当由于接收方的错误导致您的消息未送达时,CCIP浏览器将显示错误消息。然而,如果浏览器不知道 ABI(例如,您尚未在区块浏览器上验证智能合约的源代码),它将显示原始内容而不是人类可读的错误消息。如果我们了解 ABI 编码/解码的工作原理,这仍然没问题,因为有很多工具可以帮助我们。

所以,乍一看这个消息时,您可能会陷入恐慌,以为当下 CCIP 不工作了或出现类似情况。那么让我们解码错误消息,看看出了什么问题。

由浏览器可知错误代码是:0xbf3f9389000000000000000000000000cd936a39336a2e2c5a011137e46c8120dcae0d65 这本质上是一个携带很多信息的十六进制值。但如果我们知道接收方合约的 ABI便可以很简单地解码它。

您可以使用的一些工具:

在这个示例中,我们使用 https://bia.is/tools/abi-decoder/ 在线解码器。我们需要提供合约的 ABI和上述错误代码,然后点击解码以获得人类可读的错误消息。

解码后的输出明确地告诉我们:抛出了 Solidity 自定义错误 SenderNotWhitelisted(),这很可能意味着我们忘记调用接收方智能合约的 allowlistSender() 函数。

{
  "name": "SenderNotWhitelisted",
  "params": [
    {
      "name": "sender",
      "value": "0xcd936a39336a2e2c5a011137e46c8120dcae0d65",
      "type": "address"
    }
  ]
}

使用CCIP Revert Reason脚本

如果您更具技术背景,可以使用官方 CCIP GitHub 仓库中的一个脚本来完成相同的任务,也还可以实现更多功能。脚本地址:https://github.com/smartcontractkit/ccip/blob/ccip-develop/core/scripts/ccip/ccip-revert-reason/main.go

在调试时,您需要提供一个错误代码字符串,或者提供 chainId、txHash 和 txRequester 以及 .env 文件中的 JSON RPC URL(推荐使用归档节点)。

package main

import (
	"fmt"

	"github.com/ethereum/go-ethereum/ethclient"
	"github.com/joho/godotenv"

	"github.com/smartcontractkit/chainlink/core/scripts/ccip/revert-reason/handler"
	"github.com/smartcontractkit/chainlink/core/scripts/ccip/secrets"
)
// 如何使用
// 设置一个错误代码字符串或设置chainId、txHash和txRequester。
// 设置错误代码允许脚本离线运行且不需要任何RPC终端。
// 使用chainId、txHash和txRequester则需要一个RPC终端,如果交易较旧,节点需要运行在归档模式下。
// 设置变量并运行 main.go。脚本将尝试将错误代码与各种CCIP合约的ABI匹配。如果找到匹配项,它将检查是否是CCIP包装的错误,
// 如ExecutionError和TokenRateLimitError,如果是,将解码内部错误。
// 要配置RPC终端,请将RPC_<chain_id>环境变量设置为RPC终端。例如:RPC_420=https://rpc.<chain_id>.com

const (
	ErrorCodeString = "0x4e487b710000000000000000000000000000000000000000000000000000000000000032"

	// 以下输入仅在ERROR_CODE_STRING为空时使用  
	// 需要一个节点的URL  
	// 注意:如果交易较旧,该节点需要运行在归档模式下
	ChainId     = uint64(420)
	TxHash      = "0x97be8559164442595aba46b5f849c23257905b78e72ee43d9b998b28eee78b84"
	TxRequester = "0xe88ff73814fb891bb0e149f5578796fa41f20242"
	EnvFileName = ".env"
)

func main() {
	errorString, err := getErrorString()
	if err != nil {
		panic(err)
	}
	decodedError, err := handler.DecodeErrorStringFromABI(errorString)
	if err != nil {
		panic(err)
	}

	fmt.Println(decodedError)
}

func getErrorString() (string, error) {
	errorCodeString := ErrorCodeString

	if errorCodeString == "" {
		// 尝试从.env文件中载入环境变量
		err := godotenv.Load(EnvFileName)
		if err != nil {
			fmt.Println("No .env file found, using env vars from shell")
		}

		ec, err := ethclient.Dial(secrets.GetRPC(ChainId))
		if err != nil {
			return "", err
		}
		errorCodeString, err = handler.GetErrorForTx(ec, TxHash, TxRequester)
		if err != nil {
			return "", err
		}
	}

	return errorCodeString, nil
}

如果您此时直接运行 go run main.go,使用这些预定义值的输出应该是:If you access an array, bytesN or an array slice at an out-of-bounds or negative index (i.e. x[i] where i >= x.length or i < 0).

这意味着抛出了 EVM Panic 代码 0x32,这是一个非常有帮助的错误消息。

计算gas消耗

一个最常见的错误可能是:ReceiverError. This may be due to an out of gas error on the destination chain. Error code: 0x

这很可能意味着您使用了 extraArgs: "" 语法。默认情况下 gasLimit 为 200,000,而您的 ccipReceive 函数所消耗的 gas 要大于该默认值。

为了解决这个错误,您可能只需要连接您的钱包到 CCIP浏览器并设置“Gas limit override”字段为适当的值,并点击蓝色按钮手动执行此功能。

为了防止将来出现这个问题,您可以使用以下语法,例如将 gas 限制设置为 500,000 gas

extraArgs: Client._argsToBytes(
    Client.EVMExtraArgsV1({gasLimit: 500_000, strict: false})
)

请记住:

Tenderly是你的朋友

通常,当您想在测试网上实时调试或模拟交易时,Tenderly 是一个不错的选择。全追踪功能允许您查看函数实际revert的位置,这比区块浏览器中的错误消息要有帮助得多。

在处理未验证的智能合约时,一个最大的难题就是函数追踪。虽然这个智能合约没有被验证,但函数选择器仍然是已知的,因此 Polygonscan 能够显示它。这有时对调试非常有帮助。

函数选择器是函数签名的 Keccak-256 哈希值的前四个字节。函数签名包括函数名称和括号内的参数类型列表。当进行函数调用时,EVM 通过这前四个字节来确定合约中应该执行的具体函数。

所以调用失败的函数是在未验证的智能合约调用 CCIP Router 合约的过程中触发的,且其函数选择器是 0x96f4e9f9。如果我们有该智能合约的 ABI,找到函数选择器将非常简单。但如果没有呢?我们可以使用 OpenChain,这个网站有一个已知函数签名的数据库,因此我们可以尝试搜索我们的函数签名。

看起来我们已经找到了revert的函数。现在我们可以查看其代码实现并找出问题所在。尽管从 Tenderly 的界面已经很清楚,问题在于 CCIP 发送方合约没有资金来支付 CCIP 费用——这是最常见的用户错误之一。

Last updated