Filscan: FEVM合约验证的那些事

Filscan
媒体专栏
热度: 6462

Filscan: FEVM合约验证的那些事

合约TL;DR

合约验证是对给定高级语言(例如 Solidity )智能合约的源代码进行验证,确保给定源代码编译生成的字节码与在合约地址上字节码相同。源代码验证不仅提供了对智能合约的验证,还提供了代码相关的文档。通过阅读源代码和代码文档,用户可以方便地了解智能合约的设计目的和具体实现逻辑。合约验证确保了链上实际部署的智能合约与项目声称的功能一致,增强了合约代码的可信度,预防了潜在的漏洞和错误,并减少了对第三方的信任。


为什么要合约验证

区块链具有去中心化、无须许可、不可篡改和不可否认的特性被广为接受。智能合约扩展了区块链的功能,允许了在没有第三方的情况下进行可信交易,当某种触发条件,合约会自动执行部署时代码中定义的业务逻辑,而且合约一旦部署,任何人无法篡改合约代码,确保了行为可信。

虽然每个智能合约的编译字节码都可以在区块链上公开获得,但低级语言对于开发人员和用户来说都很难理解。对于去信任的智能合约,合约代码应该可供独立验证。进行合约验证有如下的优点:

确保代码执行的一致性:合约验证的主要目的是确保宣传的合约源代码与实际部署在区块链上执行的字节码一致。这是因为在部署合约之前,源代码经过编译成字节码,而字节码才是在以太坊虚拟机(EVM)中执行的指令。

  1. 增强合约的可信度:合约验证有助于增强智能合约的可信度和透明度。在去信任的区块链环境中,用户和参与者依赖于合约的正确执行和安全性。通过公开验证合约的源代码,用户可以独立审查合约的逻辑和功能,可以帮助发现合约中的潜在漏洞、错误或不当行为,确保其符合预期和安全要求。这种可验证性有助于建立信任和吸引更多用户和参与者加入到区块链网络中。
  2. 减少信任假设:合约的源代码验证通过公开和透明的方式减少对第三方的信任假设。在区块链上合约的执行逻辑是由合约代码确定的,而不是由中介机构或信任方来控制。通过发布合约的源代码,项目团队向用户和参与者表明他们愿意接受公开的审查和验证,减少了对他们的信任依赖。

因此,合约验证在区块链中非常重要,确保了合约代码的一致性和可信度,预防了潜在的漏洞和错误,并减少了对第三方的信任。通过合约验证,用户和参与者可以更加放心地与智能合约进行交互,并在去中心化的网络中进行可信交易。

合约源代码验证是提升在以太坊虚拟机(EVM)上部署的智能合约透明度和安全性的有效方法。合约通过验证过后能增强用户使用体验:

  • 提高透明度:合约源代码验证后,用户可以在 filscan 浏览器上查看智能合约的 Solidity 源代码及 ABI(应用二进制接口), 从而实现更高的透明度。这使得任何人都可以审查合约逻辑,确保其符合预期,没有隐藏的漏洞或不当行为。
  • 便捷地与合约交互:验证后的合约可以直接在 filscan 浏览器的合约详情页进行交互。用户可以调用合约方法读取合约状态,或通过写方法更新合约状态。这提供了更便捷和直接的方式与合约进行交互。


合约是如何在EVM中运行起来的


要了解合约验证的过程需要我们了解智能合约是如何创造的,在区块链中是怎么样个形态,在链上是如何存储的。理解init code的作用,才会明白合约验证为什么要进行字节码对比。


智能合约部署


在网络中,交易可以分为普通转账交易、消息调用交易和合约创建交易。

  1. 消息调用交易:向智能合约发送消息以执行其特定函数或操作的交易。这类交易在现有的合约上触发相应的操作。消息调用交易可以实现各种功能,例如转移代币、查询合约状态或执行合约逻辑。
  2. 合约创建交易:通过发送一笔交易来在区块链网络上创建新的智能合约。这类交易会包含合约的初始代码和初始化参数。当交易被验证和执行后,一个新的合约地址将被创建,如图所示,并且该地址将与合约的代码和状态相关联。 合约创建包括直接通过用户交易和通过工厂合约两种方式来创建新的合约,将字节码部署到区块链中:

a)用户交易:这是最基本和直接的方式,用户通过发送一笔交易将合约的字节码部署到区块链网络上。

b) 工厂合约:用户通过发送交易调用工厂合约的函数来创建新的智能合约。工厂合约会根据预定义的逻辑和参数创建一个新的合约实例,并返回该合约的地址。这种方式允许用户使用相同的合约模板来创建多个合约实例,提供了更高的可扩展性和便利性。 并且,工厂合约可以通过create2操作码提供部署者地址、salt、部署合约初始化代码即可预先计算,确定想要部署的合约地址。

以下是部署交易的流程:

  • 客户端构造交易:客户端将部署请求的相关信息(合约二进制代码、构造参数等)作为交易的输入数据,创建一笔交易。
  • 交易签名:交易经过 RLP(Recursive Length Prefix)编码,将其转换为字节序列,并由发送者使用其私钥对交易进行签名,确保交易的背书。
  • 发送交易:已签名的交易被发送到区块链网络中的节点,可以通过使用特定的协议将交易广播到网络中的其他节点。
  • 交易验证和存储:接收到交易的节点对其进行验证,包括验证交易的签名、验证发送者账户的余额是否足够支付交易费用以及检查交易的有效性和合法性。通过验证的交易将被存储在交易池中,等待被打包进区块。
  • 区块打包和广播:通过某种共识算法选出一个节点进行出块,它从交易池中选择一批交易,并将这些交易构建成一个新的区块。该节点将区块广播给网络中的其他节点,使其它节点知道新区块的存在。

合约如图所示,在智能合约部署过程中,合约的编译后的字节码以及经过ABI(Application Binary Interface)编码的构造函数的参数一起作为交易的 data 字段通过发送交易的方式提交到区块链中,改变了区块链的世界状态。


智能合约执行

以太坊虚拟机 (EVM)是智能合约在区块链上的运行环境,用于执行链上的智能合约。但是EVM并不能直接解释高级指令,因此智能合约的逻辑必须先被编译成字节码(即低级机器指令),然后才能在 EVM 中执行。当交易包含对合约的调用时,EVM 会按照合约的字节码指令来执行相应的操作,并对状态进行修改。

合约EVM 作为一个堆栈机(opens in a new tab)运行,其栈的深度为 1024 个项。 每个项目都是 256 位字,为了便于使用,选择了 256 位加密技术(如 Keccak-256 哈希或 secp256k1 签名)。在执行期间,EVM 会维护一个瞬态内存(作为字可寻址的字节数组),该内存不会在交易之间持久存在。已编译的智能合约字节码作为许多 EVM opcodes执行,它们执行标准的堆栈操作,例如 XOR、AND、ADD、SUB等。


常见的验证方式


相关知识介绍


· NatSpec Document

以太坊自然语言规范格式,自然语言来描述测试用例的行为和预期结果。建议使用 NatSpec 对所有公共接口(ABI 中的所有内容)对 Solidity 合约进行完全注释。 可用于输出更规范,更可读的文档。

· Metadata

Solidity 编译器编译过程中生成的 JSON 文件。该文件包含有关已编译合约的两种信息:

  • 如何与合约交互:ABI 和 NatSpec 文档(用户文档及开发者文档)。
  • 如何重现编译并验证已部署的合约:编译器版本、编译器设置(优化选项)和使用的源文件等等。

其中源文件通过 keccak256 来锁定文件,确保文件内容不会发生变化。以 remix 编辑器中默认的 1_Storage.sol 合约为例,编译过后生成的 Metadata 如下所示:

合约

· 字节码组成

  1. .code:这一部分是创建合约时执行的汇编指令,它通常包括合约的构造函数以及在合约部署过程中需要执行的初始化代码。一旦合约部署完成,.code 部分的指令将不再被执行。
  2. .data:这一部分是合约内容的汇编指令,包含了合约的主要执行逻辑,包括函数的定义和实现、变量的初始化和操作、事件的触发等。这些指令会在合约被调用时被执行,用于处理交易和状态的改变。

通常 Solidity 编译器会将经过CBOR方式编码的 Metadata 文件的哈希值及一些其他信息(如 ipfs、solc 版本等)附加到合约字节码的末尾,字节码末尾的 Metadata 哈希成为锁定整个编译过程所涉及的相关信息的指纹。

当然可以通过命令行标志--no-cbor-metadata可用于跳过将 Metadata 附加到已部署字节码的末尾。同样,可以通过标准 JSON 输入中的布尔字段settings.metadata.appendCBOR设置为 false 进行跳过。

我们使用 https://cbor.me/ 来对字节码末尾的附加内容进行解析进行观察,以 remix 编辑器中默认的 1_Storage.sol 合约为例,我们从编译过后的字节码中取出 CBOR 编码内容,a26469706673582212205774c71bc1f1fa9ac0bd2216cf5308f60b734e4d6647ca359475919ba8422fb564736f6c63430008120033,其中0033为CBOR长度,即前面51个字节与 Metadata 数据相关。将该51个字节进行解析:

合约通过解析我们可以看到 solc 的版本及 Metadata 文件在 ipfs 上的地址,通过该地址访问 ipfs 文件系统,可以获得 Metadata 文件(前提是我们需要将该文件进行发布),注意,CBOR 字段有时候不止包括上述两个字段,长度不一定都是51个字节,请参考https://docs.soliditylang.org/en/latest/metadata.html#encoding-of-the-metadata-hash-in-the-bytecode 。

完全匹配验证

源代码的某些部分(如注释或变量名称)不会影响编译后的字节码。恶意行为者就可以在源代码中添加欺骗性注释或给出误导性的变量名称,并使用与原始源代码不同的源代码来验证合约。

Metadata 文件包含有关合约编译的信息,包括源文件及其哈希值。如果任何编译优化信息或者源文件中增加或修改了任何内容,Metadata 文件就会发生变化。因此,附加到字节码的 Metadata 哈希值也会发生变化。因此,如果给定的源代码与对应的编译设置编译过后,与链上字节码没有一个字节不同,则称为完全匹配。在完全匹配的情况下,我们可以根据链上字节码获取 Metadata 的 ipfs 地址,进而获取 Metadata 文件与合约源文件,进行编译验证。

通过上面描述我们可以知道,更改源代码中任意一字节 → 源代码哈希变化 → Metadata 变化 → Metadata 哈希变化 → 部署的字节码变化。

部分匹配验证

部分匹配是指链上合约部署的字节码与除 Metadata 哈希之外的通过 Metadata 和源文件重新编译得到的字节码完全匹配。部署的合约和给定的源代码+ Metadata 功能完全相同,但在源代码注释、变量名称或 Metadata 其他字段(例如源路径)方面可能存在差异。这是目前验证合约的常见方法。由于大多数开发者不知道完整的验证,源文件可能有些许变动,也没有保留其编译的 Metadata 文件,因此部分验证已经成为迄今为止验证合约的事实上的方法。

这种类型的匹配类似于 Etherscan 验证合约的方式。理论上匹配的源代码的功能与部署的合约相同,但显示的源代码可能会产生误导。

合约验证原理及过程

  1. 将源文件和编译设置输入到编译器中;
  2. 编译器输出合约的字节码;
  3. 获取给定地址已部署合约的字节码;
  4. 将部署的字节码与重新编译的字节码进行比较。如果字节码包括 Metadata 哈希均匹配,则为完全匹配。如果除了 Metadata 哈希外的字节码匹配,则为部分匹配。

与 Solidity 编译器交互的推荐方法是 JSON 输入输出接口,特别是对于更复杂和自动化的设置。编译器的所有发行版都提供相同的接口。其中有些字段可能会发生变化,有些字段是可选的,但 solidity 编译器官方只进行向后兼容的更改。编译器 API 要求使用JSON格式作为输入,并且以 JSON 格式输出编译结果。无论是否存在错误,该进程都会以”成功”状态终止,不使用标准错误输出。所有错误都会以 JSON 输出的形式进行报告。以下是通过 Metadata 方式进行合约验证中的一个流程:

合约影响验证结果的因素

在合约验证过程中,需要重点考虑一些影响编译结果的关键因素。对这些因素的忽视可能会导致编译出的字节码与预期不同,从而导致合约验证失败。这些关键因素包括下面这些部分。

· 库文件

库代码是在发起合约(主动发起 DELEGATECALL 调用的合约)的上下文中执行的,使用 this 将会指向到主调合约,而且库代码可以访问主调合约的存储( storage )。库合约是一个独立的代码,它仅可以访问主调合约明确提供的状态变量。

如果调用 solc 时带有 --link 选项,所有输入文件都被编译成格式为 __$53aea86b7d70b31448b230b20ae141a537$__ 形式的未链接的二进制文件(十六进制编码),并被本地链接(如果从标准输入(stdin)读取输入,则被写到标准输出(stdout))。

当您的合约中有使用库合约, 您会注意到字节码中含有 __$53aea86b7d70b31448b230b20ae141a537$__ 形式的字符串。 这些是实际库的地址的占位符。此占位符是完全限定库名的keccak256哈希的十六进制编码的34个字符前缀。注意,从Solidity v0.5.0开始,占位符的格式为__$<keccak256LibraryNameHash>$__,之前的格式为__<LibraryName>__。

完全限定的库名是其源文件的路径和用 : 分隔的库名,可以通过--libraries "file.sol:Math=0x1234567890123456789012345678901234567890 file.sol:Heap=0xabCD567890123456789012345678901234567890" 来为每个库提供一个地址(用逗号或空格作为分隔符),也可以将字符串存储在一个文件中(每行一个库), 用 -libraries fileName 进行指定替换文件。不鼓励在生成的字节码上手动链接库,因为它不会更新合约 Metadata 。由于Metadata 包含编译时指定的库列表,而字节码包含 Metadata 哈希,因此您将获得不同的二进制文件,具体取决于执行链接的时间。

合约替换占位符的过程通常涉及将占位符与实际的库地址进行映射,并将映射关系应用于合约编译结果中的相应位置,这确保了验证过程能够正确地定位和访问库的相关代码和功能。因此在验证合约时,必须确保这些占位符被正确替换。如果占位符没有被正确替换,验证过程可能无法识别库的存在,从而导致验证失败。

· 编译优化

在合约编译过程中,优化选项允许编译器对源代码进行一系列优化,以提高合约的执行效率和最终生成的字节码的质量。solidity 编译器使用两种不同的优化方式,老的优化器直接优化编译过后的 opcode,新的优化器优化 Yul IR 代码。

当您编译时使用 solc --optimize --bin sourceFile.sol 激活优化器。 优化器将假设合约的每个操作码在其生命周期内被调用200次。 如果您想让最初的合约部署更便宜,而后来的函数执行更昂贵,请设置为 --optimize-runs=1。 如果您期望有很多交易,并且不在乎更高的部署成本和输出大小,那么把 --optimize-runs 设置成一个高的数字。 这个参数对以下方面有影响(将来可能会改变):

  • 函数调度程序中二进制搜索的大小
  • 像大数字或字符串等常量的存储方式

常见的优化操作还包括:

  • 冗余代码消除:编译器可以分析源代码并识别出其中的冗余代码,即在执行过程中没有实际作用的代码。通过消除这些冗余代码,编译器可以减少合约的执行时间和消耗的燃料(gas)。
  • 常量表达式计算:编译器可以在编译时计算常量表达式的值,并将其替换为计算结果。这样可以减少合约在执行时对计算的依赖,提高执行效率。
  • 内联函数:编译器可以将一些函数内联展开,即将函数调用替换为函数体的复制。这样可以避免函数调用的开销,提高执行效率。

总的来说,优化器旨在简化复杂的表达式,以减少代码大小和执行成本,从而降低部署合约和进行外部调用所需的 gas 费用。优化器还会针对函数进行专门优化或内联处理,以优化可能导致更多代码操作的情况,进而提高合约的效率和性能。

合约

合约· 路径解析

为了能够在所有平台上支持可重复的构建,Solidity 编译器必须抽象出存储源文件的文件系统的细节。 编译器维护一个内部数据库( 虚拟文件系统,VFS ), 每个源单元被分配一个唯一的源单元名称,这是一个不透明的非结构化的标识符。 当您使用 import 语句时, 您需要指定引用源单元名称的导入路径。

solc contract.sol \

--base-path . \

--include-path node_modules/ \

--include-path /usr/local/lib/node_modules/

当访问文件系统搜索导入文件时, --base-path 用于指定基本路径,其路径内的所有内容都是允许访问的, --include-path 选项指定相对于基本路径搜寻的目录。通过这些选项添加的路径部分将不会出现在合约Metadata中。

出于安全考虑,编译器限制了它可以访问的目录。 在命令行中只有指定的源文件的目录和重映射的目标路径是允许访问的。 通过 --allow-paths /sample/path,/another/sample/path 选项可以允许访问其他路径。导入重映射允许您将导入重定向到虚拟文件系统的不同位置。 关于重映射的信息被存储在合约Metadata中。 由于编译器产生的二进制文件中嵌入了Metadata的哈希值,对重映射的任何修改都会导致不同的字节码。因此,注意不要在重映射目标中包含任何本地信息。 例如,如果您的库位于 /home/user/packages/mymath/math.sol, 像 @math/=/home/user/packages/mymath/ 这样的重映射会导致您的主目录被包含在Metadata中。 为了能够在不同的机器上用这样的重映射重现相同的字节码,您需要在VFS和(如果您依赖主机文件系统加载器)主机文件系统中重新创建您的本地目录结构。

为了避免Metadata中嵌入您的本地目录结构,建议将包含库的目录指定为 include paths。 例如,在上面的例子中, --include-path /home/user/packages/ 会让您使用以 mymath/ 开始的导入。 与重映射不同,该选项本身不会使 mymath 显示为 @math, 但这可以通过创建符号链接或重命名软件包子目录来实现。


Filscan验证过程


尽管合约验证过程中可能会遇到诸多细节性问题,但好消息是, filscan 浏览器为您屏蔽了这些繁琐细节。您只需简单几步,便能轻松完成验证,无需为烦杂配置项等问题而烦恼。合约验证地址:https://filscan.io/contract/verify/

合约验证仅需要开发者提供合约一些合约相关信息即可完成:

  • 合约地址:0x 地址,目前只支持主网,测试网合约地址;
  • License:合约所采用的许可证类型;
  • Solidity:编译器版本;
  • 编译优化配置:编译时是否打开了优化选项,runs 是多少;
  • 合约源码:被验证合约对应的源码,需要注意如果合约中引入了其他文件中的合约,需要将所有文件代码 flatten 到单个文件中。您也可以上传多个在同一目录下的文件,或者多文件配合 Metadata 上传。

合约合约是否有额外的优化参数


· Optimize、Run

官方不推荐使用 enabled 和 runs 字段,仅用于向后兼容。

但在很多情况下,我们编译合约没有使用更复杂编译优化选项 detail,因此我们采取第一种模式,即可通过验证。


· 多文件 Flatten

通常情况下我们要处理的不是单一的 Solidity 文件,因为我们在智能合约编写中经常会引入其他的合约,接口和库。这个命令会生成一个新的文件叫做“PriceFeedConsumer_flat.sol”,这个文件将会把所有的 import 都换成被引入合约,接口或者库的源代码。

在 remix 中通过 flatten 插件,右键点击需要验证的合约,进行 flatten 即能得到整合过后的文件。

合约假如开发者的合约项目使用 hardhat,则可以使用如下方式获取 flatten 后的合约。

$ npx hardhat flatten ./contracts/yourContract.sol > yourContract_flatten.sol

该命令执行完之后,项目根目录下会有一个完整的合约代码的 yourContract_flatten.sol 文件。


Metadata


如果您在编译的时候使用其他优化参数。因此,简单的优化 runs 参数不能够完成验证任务,您需要在上传 Metadata 来辅助合约验证,因为 Metadata 包括您在编译合约时所需要的全部相关配置。如您在 remix 中编译时使用配置文件的方式如下所示:

合约编译配置如图所示

合约


Hardhat 支持

为了更好的方便用户进行合约的验证,做到无感知、无配置,零基础可以上手,我们可以选择第三种hardhat支持专用模式。Hardhat 将编译输出存储在项目内的artifacts/build-info/目录中。 该目录包含一个 .json 文件,其中包含所有合约的 Standard JSON Input-Output,这是与 Solidity 编译器交互的推荐方法,特别是在高级和自动化配置中。将其 json 文件上传即可进行验证。

合约注意:代理合约的验证只能验证代理合约的源代码经过编译后是否与链上的字节码一致,但实际上代理合约通过 delegatecall 来让逻辑合约完成实际的操作,因此,为了确保可靠性,需要对逻辑合约也经过验证。即不能仅仅通过验证过的单个代理合约文件来判断合约的可靠性,请仔细鉴别。


FQA


为什么合约验证不通过?

合约验证不通过,大概率是因为某个参数设置不正确,比如编译器版本,是否开启优化,优化参数,库的链接方式等。推荐使用 Metadata 方式及 hardhat 支持。

Reference Link:

  1. https://ethereum.org/zh/developers/docs/smart-contracts/verifying/
  2. https://docs.soliditylang.org/en/latest/metadata.html
  3. https://takenobu-hs.github.io/downloads/ethereum_evm_illustrated.pdf
  4. https://github.com/ethereum/solidity/blob/develop/libevmasm/RuleList.h

联系我们

主页https://filscan.io/home/

推特https://twitter.com/FilscanOfficial

Telegramhttp://t.me/filscanofficial

声明:本文为入驻“MarsBit 专栏”作者作品,不代表MarsBit官方立场。
转载请联系网页底部:内容合作栏目,邮件进行授权。授权后转载时请注明出处、作者和本文链接。未经许可擅自转载本站文章,将追究相关法律责任,侵权必究。
提示:投资有风险,入市须谨慎,本资讯不作为投资理财建议。
免责声明:本文不构成投资建议,用户应考虑本文中的任何意见、观点或结论是否符合其特定状况,及遵守所在国家和地区的相关法律法规。