同大多数编程语言一样,用Solidity编写的智能合约无法直接在以太坊虚拟机(EVM)上运行,必须先将其编译成字节码。
同大多数编程语言一样,用 Solidity
编写的智能合约无法直接在以太坊虚拟机(EVM
)上运行,必须先将其编译成字节码。编译需要使用 Solidity 编译器。
Solidity 编译器是 c++
语言编写的,因此对于操作系统而言,可以很轻易的编译成二进制命令行程序。而对于浏览器环境则需要使用工具 Emscripten 将 c++
代码编译成 js
代码。
相关仓库:
nodejs
版本的编译器 solc-js,是对被编译成的 js
代码二次封装, 新增了命令行, 支持模块化等。智能合约开发框架 hardhat
正是用的该框架进行智能合约的编译。具体的编译过程则是将指定结构的 JSON
数据输入到编译器中, 编译完成后输出编译结果。
在开始之前需要先介绍下抽象语法树(AST)。
由 Solidity
转换为字节码,可以看成是由一种语言转换成另一种语言的过程。因此需要一种中间介质来承载原语言解析的结果,再将中间介质转换为目标语言,这种介质就是抽象语法树。也是进行语法转换的基础。如果你是一名 js
开发者,相信对这个概念并不陌生。
抽象语法树的构建:
token
),token
是语言中的基本语法单位,例如关键字、标识符、运算符和标点符号等。Solidity
的语法规则,将这些 token
组合成语法结构,如表达式、声明和语句等。例如对于智能合约
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.21;
contract Main { function hack() public { } }
转换成的抽象语法树为
const ast = {
absolutePath: 'main',
exportedSymbols: {
Main: [6]
},
id: 7,
license: 'GPL-3.0',
nodeType: 'SourceUnit',
nodes: [
{
id: 1,
literals: ['solidity', '^', '0.8', '.21'],
nodeType: 'PragmaDirective',
src: '42:24:0'
},
{
abstract: false,
baseContracts: [],
canonicalName: 'Main',
contractDependencies: [],
contractKind: 'contract',
fullyImplemented: true,
id: 6,
linearizedBaseContracts: [6],
name: 'Main',
nameLocation: '82:4:0',
nodeType: 'ContractDefinition',
nodes: [
{
body: {
id: 4,
nodeType: 'Block',
src: '112:3:0',
statements: []
},
functionSelector: '4de260a2',
id: 5,
implemented: true,
kind: 'function',
modifiers: [],
name: 'hack',
nameLocation: '98:4:0',
nodeType: 'FunctionDefinition',
parameters: {
id: 2,
nodeType: 'ParameterList',
parameters: [],
src: '102:2:0'
},
returnParameters: {
id: 3,
nodeType: 'ParameterList',
parameters: [],
src: '112:0:0'
},
scope: 6,
src: '89:26:0',
stateMutability: 'nonpayable',
virtual: false,
visibility: 'public'
}
],
scope: 7,
src: '73:44:0',
usedErrors: [],
usedEvents: []
}
],
src: '42:75:0'
}
Solidity
编译是将指定结构的 JSON
数据输入到编译器中, 编译完成后输出编译结果。
编译输入包括下列字段
数据类型见源码中 parseInput 方法
使用的代码语言, 支持 Solidity
、Yul
、SolidityAST
Yul
是一种底层语言,它的抽象级别更低,更接近以太坊虚拟机的操作。可以在 Solidity
中通过内联汇编的方式直接编写 Yul
代码。同时 Yul
存在优化器,可以执行诸如简化表达式、消除未被使用的代码以及合并相同代码等操作,以减小生成的字节码大小和运行时的 Gas 消耗。
SolidityAST
则是直接输入 AST 到编译器中, 省略了编译成 AST 的过程。
代码字符串, 结构为
sources: {
'main.sol': {
keccak256: '0x...',
content: `// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.21;
contract Main { uint a = 1; function add() public { a += 1; } }`
urls: [
"bzzr://...",
"ipfs://...",
"/path"
]
},
'file-key': {
...
},
}
sources
字段提供需要编译的文件 key
和文件内容的映射,文件 key
通常设置为文件名。文件内容有以下字段
keccak256
, 可选,源码内容的keccak256
哈希值content
: 需要编译的源码内容字符串urls
: 需要编译的源码内容链接, content
和 urls
只需要存在一个即可。同时存在时则只读取 content
, 忽略 urls
详细编译配置, 包括以下字段:
在达到给定阶段后停止编译, 目前仅支持传入 parsing
编译阶段有
enum State {
Empty, // 初始状态
SourcesSet, // 设置需要编译的源码内容
Parsed, // 解析AST完成
ParsedAndImported, // 语言为 SolidityAST 时存在的阶段, 表示已解析并导入了AST
AnalysisSuccessful, // 对AST进行分析完成, 包括语法检查、注释识别等
CompilationSuccessful // 编译完成
};
如果传入了 stopAfter
, 则设置该字段值为 Parsed
, 表示 AST
解析完成后停止编译。
if (settings.isMember("stopAfter"))
{
if (!settings["stopAfter"].isString())
return formatFatalError(Error::Type::JSONError, "\"settings.stopAfter\" must be a string.");
if (settings["stopAfter"].asString() != "parsing")
return formatFatalError(Error::Type::JSONError, "Invalid value for \"settings.stopAfter\". Only valid value is \"parsing\".");
ret.stopAfter = CompilerStack::State::Parsed;
}
默认为 CompilationSuccessful
, 表示编译完成则停止。
是否通过 IR 编译,默认为 false
。
IR(Intermediate Representation) 翻译为中间表示, 是编译器设计和编程语言理论领域的一个重要概念。它作为程序代码的中间形式存在,位于高级源代码和底层机器代码之间。
在以太坊智能合约编译的背景下,中间表示 代表 Yul
代码。如果 viaIR
为 true
, 则会将 Solidity
源代码转换为 Yul
中间代码,并在转换为最终的字节码之前使用 Yul
优化器优化 Yul
代码,因此建议将该字段设置为 true
以太坊虚拟机版本, 存在以下版本:
homestead
tangerineWhistle
spuriousDragon
byzantium
constantinople
petersburg
istanbul
berlin
london
paris
shanghai
仅支持 constantinople
以上的版本
EVM Object Format(EOF)的版本, 一种新的以太坊虚拟机字节码格式,目前仅支持设置为 1
调试设置, 包括以下字段
revertStrings
: 如何处理编译器自动插入以及合约代码中的 revert
和 require
报错的字符串, 有以下取值:
default
默认值,不注入编译器生成的 revert
字符串,仅保留合约代码中提供的报错字符串strip
移除所有 revert
字符串debug
仅注入编译器生成的 revert
字符串verboseDebug
暂未实现debugInfo
: 包含的调试位置信息, 值为字符串数组, 取值
location
以@src <index>:<start>:<end>
形式注释,指示原始 Solidity
文件中相应元素的位置snippet
由@src
指示的位置处的单行代码片段*
包括以上两种路径映射, 在编译的合约文件中以更简洁的方式导入其他文件。例如有合约的路径是 /usr/local/contracts/Animal.sol
, 定义 remappings
字段为
{
"remappings": [
"libs/=usr/local/contracts/"
]
}
则在需要编译的合约中通过路径映射的方式引入,而不必写入完整路径
import "libs/Animal.sol";
contract Cat is Animal {}
优化器设置。
Solidity
编译器中的优化器会试图简化复杂的表达式,减少代码大小和执行成本,即可以减少合约部署以及对合约进行外部调用所需的 gas
。
目前存在两种不同的优化器模块:
Yul
IR 代码的新优化器在编译输入中, 优化器设置可以传入下列字段:
enabled
:是否启用优化器, 默认为false
,runs
: 已部署代码在合约的生命周期内预计会被执行的次数。这个值帮助优化器决定应该如何平衡代码的初始部署成本和执行成本。如果合约会频繁执行,可以设置一个较高的值,使得优化器倾向于优化执行时的gas
消耗,也就意味着部署成本会更高。相反的,较低的值则会减少部署成本并增加执行成本。通常设置为 200,v3-core 则设置为了 800details
: 优化细节, 存在值时则会忽略enabled
peephole
: 是否启用窥孔优化, 默认开启inliner
: 是否启用内联函数, 例如在合约代码中多次调用某个函数, 则内联会把函数调用替换为函数实现。内联的决策基于 runs
参数,优化器会计算合并后的代码存储成本和执行成本,并与执行了 runs
次数的预估成本比较,如果内联后整体更省成本,则会进行内联操作。jumpdestRemover
: 是否移除不必要的 JUMPDEST
指令orderLiterals
: 是否重新排列交换操作中的数据deduplicate
: 是否删除重复的代码块cse
: 是否开启 Common Subexpression Eliminator
, 其负责识别和合并在多个地方重复出现的相同表达式。并将这些表达式计算一次然后复用其结果constantOptimizer
: 是否启用常量优化,用于处理常量表达式, 如代码中有 3 + 5
这样的常量表达式,constantOptimizer
会将其直接替换为计算结果 8,以此减少每次调用时的计算开销yul
: 启用 yul
优化器, 在 Solidity 0.6.0
前需手动启用simpleCounterForLoopUncheckedIncrement
: 在 for
循环中对每次增加的循环次数忽略溢出检查yulDetails
: yul
优化器设置, 当 yul
设置为 true
时生效stackAllocation
: 是否改进变量的堆栈插槽分配,可以提前释放堆栈插槽optimizerSteps
: 优化步骤, 见optimizer-steps在合约代码中,如果引入了库,并且调用的库函数是 internal
时,则编译器会将库函数的代码直接嵌入到调用的合约中。如果库包含 public
或 external
函数,则可以被部署到区块链上,在调用库中的 public
或 external
函数时,编译器则会通过库地址去调用库函数。
libraries
字段指定了合约中调用的库合约地址,如
"libraries": {
// main.sol 对应于 sources 字段中的文件 key
"main.sol": {
"SafeMath": "0x..." // 库名称及其地址
}
}
通过编译器命令 solc main.sol --metadata
可以生成合约的 metadata
数据, 如下所示
{
"compiler": {
"version": "0.8.22+commit.4fc1097e"
},
"language": "Solidity",
"output": {
"abi": [...],
// 见 https://docs.soliditylang.org/en/latest/natspec-format.html
"devdoc": {...},
"userdoc": {...}
},
"settings": {
"compilationTarget": {
"main.sol": "Main"
},
"evmVersion": "shanghai",
"libraries": {},
"metadata": {
"bytecodeHash": "ipfs"
},
"optimizer": {
"enabled": false,
"runs": 200
},
"remappings": []
},
"sources": {
"main.sol": {
"keccak256": "0x9f3260bd62699741761323058d94da76ee11a7b7e35387b9bafae9e901b0aa18",
"license": "GPL-3.0",
"urls": [
"bzz-raw://93271a80165a0846ac1fb451e7d99d1700963758a21b6b3d20ee1b2374cec839",
"dweb:/ipfs/Qme4eQFfPX88wjF7xCKCxzNnoNPp1ubuGbnJgz74kNhSpi"
]
}
},
"version": 1
}
metadata
包含的字段有
compiler
: 编译器版本language
: 代码语言output
: 编译输出, 主要包括合约的 ABI 数据settings
: 编译合约时设置的编译器配置sources
: 编译输入的源码当编译输入时,针对 metadata
数据存在以下设置
appendCBOR
: CBOR, 全称是Concise Binary Object Representation, 意为简明二进制对象展现,一种二进制数据序列化格式。 该字段意为是否在编译后的字节码末尾添加metadata hash
。设置为 true
时, 首先根据下方 bytecodeHash
哈希方法计算 metadata
内容的哈希值。再通过 CBOR 编码格式添加到编译后的字节码的末尾。useLiteralContent
:是否在生成的 metadata
包含合约内容。当设置为 true
时, metadata
中的 sources
会移除 urls
, 而增加 content
字段, 用来显示合约内容bytecodeHash
:定义计算 metadata hash
的方法 支持以下取值。
none
: 不计算ipfs
: 默认值, 使用 ipfs
的哈希算法计算bzzr1
: 使用 bzzr1
的哈希算法计算默认情况下编译后的字节码包含三部分内容, 并以 fe
分割
init code
或称作 creation code
, 这部分包括了合约初始化、构造函数等操作码。runtime code
运行时字节码,主要包括合约方法相关的字节码。当合约被调用时,这部分代码将会被执行metadata hash
理解这个字段前,先介绍下 SMTChecker
, 一个静态分析工具。主要用于检查智能合约中的潜在错误。
SMT
是 Satisfiability Modulo Theories(可满足性模理论) 的缩写。这个工具使用形式化验证技术来分析智能合约中的代码。形式化验证是一种验证方法,它使用数学和逻辑上的技术来证明智能合约代码在逻辑上的正确性,以及是否按照预期的结果执行。
modelChecker
是形式化验证使用的具体的数学模型配置,包含了以下字段。
engine
: 使用的引擎
bmc
:全称是 Bounded Model Checker
, 单独分析合约中的每个函数是否存在错误chc
:全称是 Constrained Horn Clauses
, 在分析合约中的函数时,会考虑整个合约在无限数量的交易中的行为,因此更适合于全面的检查all
: 默认值,同时使用两种引擎none
: 不检查bmcLoopIterations
: 在合约代码中的 for
循环可能会存在无限次数, 该字段则在 bmc
检查时对循环次数设置一个上限。contracts
: 对哪个合约进行检查divModNoSlacks
: 进行除法或者取模运算时是否引入松弛(slack)变量。extCalls
: 如何处理外部调用, 可设置为 trusted
或 untrusted
; trusted
会假定所有外部调用都是可信的且不会引入安全风险或不会有恶意行为,当你对合约的外部调用的安全性足够确定时使用,而且可以减少模型检查器的分析范围,提高分析速度。invariants
: 验证特定的不变性(初始状态下为真,并且每次合约状态变化后仍然保持为真), 有:
contract
: 合约不变性,合约状态变量在每个可能的交易前后都是正确的reentrancy
: 重入不变性, 外部调用前后状态变量值都是正确的printQuery
: 是否在模型检查器运行时输出查询信息。设置为 true
会打印出发送给 SMT
求解器的每个查询showProvedSafe
: 是否输出验证通过的信息showUnproved
: 是否输出无法确定安全性的信息showUnsupported
: 是否输出模型检查器不支持的功能的信息solvers
: 指定用于模型检查的求解器,求解器是用来确定逻辑公式在给定某些约束下是否可满足的工具。常见的求解器有 z3
、cvc4
等targets
: 检查哪些属性,有:
constantCondition
: 验证条件语句不是常数,以避免无用的代码。underflow
: 检查算术运算是否存在下溢的风险。overflow
: 检查算术运算是否存在溢出的风险。divByZero
: 确保除法运算中没有除以零的情况。balance
: 检查智能合约的余额操作是否符合预期,防止意外的余额变动。assert
: 验证 assert
语句永远不会失败。popEmptyArray
: 检查空数组不能 pop
元素。outOfBounds
: 验证数组越界/字节索引越界all
: 检查以上全部属性timeout
: 验证超时设置对输入的合约指定输出结果, 对象结构配置,如下所示:
"outputSelection": {
"main.sol": { // 输入时指定的文件key
"Main": [ "abi", "evm.bytecode.opcodes" ], // 文件内的合约名称及其对应的期望输出结果
"": [ "ast" ] // 输出文件生成的抽象语法树
},
"*": { // 所有文件
"*": [ // 所有合约
"metadata", "evm.bytecode", "evm.bytecode.sourceMap"
],
"": [ "ast"] // 输出所有文件生成的抽象语法树
}
}
key
为 sources
字段指定的文件 key
, value
为编译文件中合约名称及其对应的期望输出结果。或者配置为 *
表示对所有文件生效。对于空合约名称则用于整个文件的输出配置(如 AST)。
针对某个具体的合约来说,输出可以配置为
*
:输出下面所有选项abi
:输出合约 abi
内容devdoc
:输出合约 devdoc
userdoc
:输出合约 userdoc
metadata
:输出合约 metadata
ir
:输出合约被编译成的 yul
代码irAst
:输出合约被编译成的 yul
代码生成的抽象语法树irOptimized
:输出合约被编译成的 yul
代码优化后的结果irOptimizedAst
:输出合约被编译成的 yul
代码优化后生成的抽象语法树storageLayout
:输出合约状态变量的存储布局,包括存储槽编号, 占据的字节大小evm
:输出下面所有以 evm
开头的选项
evm.assembly
:输出合约的底层汇编代码evm.legacyAssembly
:输出合约的旧版本底层汇编代码 JSON
结构evm.bytecode
:输出下面所有以 evm.bytecode
开头的选项evm.bytecode.functionDebugData
:输出合约函数的调试数据evm.bytecode.generatedSources
:输出合约编译过程中生成的源代码evm.bytecode.linkReferences
:输出合约链接的库信息evm.bytecode.object
:输出合约编译后的字节码, 即待部署的合约字节码evm.bytecode.opcodes
:输出合约编译后的操作码evm.bytecode.sourceMap
:输出合约编译后的字节码与原始源代码之间的映射evm.deployedBytecode
:输出下面以 evm.deployedBytecode
开头的选项,deployedBytecode
是上文提到的运行时字节码(runtime code
),需要在链上存储的代码。evm.deployedBytecode.functionDebugData
:输出合约运行时代码中函数的调试数据evm.deployedBytecode.generatedSources
:输出合约运行时代码编译过程中生成的源代码evm.deployedBytecode.immutableReferences
:输出合约用 immutable
声明的变量在抽象语法树中的映射evm.deployedBytecode.linkReferences
:输出合约运行时字节码中链接的库信息evm.deployedBytecode.object
:输出合约编译后运行时字节码evm.deployedBytecode.opcodes
:输出合约编译后运行时字节码的操作码evm.deployedBytecode.sourceMap
:输出合约编译后运行时字节码与原始源代码之间的映射evm.methodIdentifiers
:输出合约函数 selector
evm.gasEstimates
:输出合约预估 gas
信息,包括部署 creation
和外部调用 external
。编译器编译后的输出结果包括下列字段:
errors
: 编译过程中出现的错误信息,例如编译输入配置错误、合约缺少了 License
、代码出现语法错误等sources
: 文件级别的输出,如果在 outputSelection
配置了 ast
, 则输出合约文件生成的 ast
信息contracts
: 合约级别的输出, 输出结果来自于 outputSelection
中对具体合约的配置本文详细介绍了 Solidity
编译器编译输入中每个配置字段以及编译输出的每个字段所代表的含义,从基础的设置如代码语言和优化选项,到更高级的功能如元数据的包含和链接库的处理,每一部分都进行了全面的解析。这些知识对于理解和优化 Solidity
智能合约至关重要,不仅有助于提升合约的性能,也有助于增强其安全性和可维护性。
尽管合约编译配置复杂,但在多数应用场景中,使用默认配置即可。只有在需要特定优化时,才需调整配置。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!