pragma solidity ^0.4.0;
contract C {
struct s { uint a; uint b; }
uint x;
mapping(uint => mapping(uint => s)) data;
data[4][9].b 的位置将是 keccak256(uint256(9) . keccak256(uint256(4) . uint256(1))) + 1。
内存 中的存储结构
Solidity 保留了 4 个 32 字节的插槽(slot):
0x00 - 0x3f:用于保存方法(函数)哈希的临时空间
0x40 - 0x5f:当前已分配的 内存 大小(又名,空闲 内存 指针)
0x60 - 0x7f:0 值插槽
临时空间可以在语句之间使用(即在内联汇编之中)。0 值插槽则用来对动态内存数组进行初始化,且永远不会写入数据(因而可用的初始内存指针为 0x80)。
Solidity 总会把新对象保存在空闲 内存 指针的位置,所以这段内存实际上从来不会空闲(在未来可能会修改这个机制)。
Solidity 中有一些操作需要大于 64 字节的临时内存区域,因此这种数据无法保存到临时空间里。它们将被放置在空闲内存指向的位置,但由于这种数据的生命周期较短,这个指针不会即时更新。这部分内存可能会被清零也可能不会。所以我们不应该期望这些所谓的空闲内存总会被清零。
尽管使用 msize 来到达非零内存区域是个好主意,然而非临时性地使用这样的指针,而不更新可用内存指针也会产生有害的结果。
调用数据存储结构
当从一个账户调用已部署的 Solidity 合约时,调用数据的格式被认为会遵循 ABI 说明。
根据 ABI 说明的规定,参数需要被整理为 32 字节的倍数。而内部函数调用会使用不同规则。
内部机制 - 清理变量
如果一个数值不足 256 位,那么在某些情况下,不足的位必须被清除。
Solidity 编译器设计用于在执行任何操作之前清除这些剩余位中可能会造成不利影响的潜在垃圾。
例如,因为 内存 中的内容可以用于计算散列或作为消息调用的数据发送,所以在向 内存 写入数值之前,需要清除剩余的位。
同样,在向 存储 中保存数据之前,剩余的位也需要清除,否则就会看到被混淆的数值。
另一方面,如果接下来的操作不会被影响,那我们就不用清除这些位的数据。例如,因为任何非零值都会被 JUMPI 指令视为 true,
所以在布尔数据用做 JUMPI 的条件之前,我们就不用清除它们。
除了以上设计原理之外,Solidity 编译器在把输入数据加载到堆栈时会对它们进行清除剩余位的处理。
不同的数据类型有不同的清除无效值的规则:
内部机制 - 优化器
Solidity 优化器是在汇编语言级别工作的,所以它可以并且也被其他语言所使用。它通过 JUMP 和 JUMPDEST 语句将指令集序列分割为基础的代码块。在这些代码块内的指令集会被分析,并且对堆栈、内存或存储的每个修改都会被记录为表达式,这些表达式由一个指令和基本上是指向其他表达式的参数列表所组成。现在,主要的想法就是找到始终相等的表达式(在每个输入上)并将它们组合到一个表达式类中。优化器首先尝试在已知的表达式列表中查找每个新表达式。如果这不起作用,表达式会以 constant + constant = sum_of_constants 或 X * 1 = X 这样的规则进行简化。由于这是递归完成的,所以在我们知道第二个因子是一个更复杂的表达式,且此表达式总是等于 1 的情况下,也可以应用后一个规则。对存储和内存上某个具体位置的修改必须删除有关存储和内存位置的认知,这里边的区别并不为人所知:如果我们先在 x 位置写入,然后在 y 位置写入,且都是输入变量,则第二个可能会覆盖第一个,所以我们实际上并不知道在写入到 y 位置之后在 x 位置存储了什么。另一方面,如果对表达式 x - y 的简化,其结果为非零常数,那么我们知道我们可以保持关于 x 位置存储内容的认知。
在这个过程结束时,我们会知道最后哪些表达式必须在栈上,并且会得到一个修改内存和存储的列表。该信息与基本代码块一起存储并用来链接它们。此外,关于栈、存储和内存的配置信息会被转发到下一个代码块。如果我们知道所有 JUMP 和 JUMPI 指令的目标,我们就可以构建一个完整的程序流程图。 如果只有一个我们不知道的目标(原则上可能发生,跳转目标可以基于输入来计算),我们必须消除关于代码块输入状态的所有信息,因为它可能是未知的 JUMP 目标。如果一个 JUMPI 的条件等于一个常量,它将被转换为无条件跳转。
作为最后一步,每个块中的代码都会被完全重新生成。然后会从代码块的结尾处在栈上的表达式开始创建依赖关系图,且不是该图组成部分的每个操作实质上都会被丢弃。现在,生成的代码将按照原始代码中的顺序对内存和存储进行修改(舍弃不需要的修改),最终,生成需要在栈中的当前位置保存的所有值。
这些步骤适用于每个基本代码块,如果代码块较小,则新生成的代码将用作替换。如果一个基本代码块在 JUMPI 处被分割,且在分析过程中被评估为一个常数,则会根据常量的值来替换 JUMPI,因此,类似于
var x = 7;
data[7] = 9;
if (data[x] != x + 2)
return 2;
return 1;
的代码也就被简化地编译为
data[7] = 9;
return 1;
即使原始代码中包含一个跳转。
源代码映射
作为 AST 输出的一部分,编译器提供 AST 中相应节点所代表的源代码范围。这可以用于多种用途,比如从用于报告错误的 AST 静态分析工具到可以突出显示局部变量及其用途的调试工具。
此外,编译器还可以生成从字节码到生成该指令的源代码范围的映射。对于在字节码级别上运行的静态分析工具以及在调试器中显示源代码中的当前位置或处理断点,这都是同样重要的。
这两种源映射都使用整数标识符来引用源文件。这些是通常称为 “sourceList” 的源文件列表的常规数组索引,它们是 combined-json 和 json / npm 编译器输出的一部分。
在指令没有与任何特定的代码文件关联的情况下,源代码映射会将 -1 赋值给一个整数标识符。这会在字节码阶段发生,源于由编译器生成的内联汇编语句。
AST 内的源代码映射使用以下表示法:
s:l:f
其中,s 是源代码文件中范围起始处的字节偏移量,l 是源代码范围的长度(以字节为单位),f 是上述源代码索引。
针对字节码的源代码映射的编码方式更加复杂:它是由 ; 分隔的 s:l:f:j 列表。每个元素都对应一条指令,即不能使用字节偏移量,但必须使用指令偏移量(push 指令长于一个字节)。字段 s,l 和 f 如上所述,j 可以是 i,o 或 -,表示一个跳转指令是否进入一个函数、是否从一个函数返回或者是否是一个常规跳转的一部分,例如一个循环。
为了压缩这些源代码映射,特别是对字节码的映射,我们将使用以下规则:
如果一个字段为空,则使用前一个元素中对应位置的值。
如果缺少 :,则后续所有字段都被视为空。
这意味着以下的源代码映射是等价的:
1:2:1;1:9:1;2:1:2;2:1:2;2:1:2
1:2:1;:9;2:1:2;;
技巧和窍门
可以使用 delete 来删除数组中的所有元素。
对 struct 中的元素使用更短的数据类型,并对它们进行排序,以便将短数据类型组合在一起。这可以降低 gas 消耗,因为多个 SSTORE 操作可能会被合并成一个(SSTORE 消耗 5000 或 20000 的 gas,所以这应该是你想要优化的)。使用 gas 估算器(启用优化器)来检查!
将你的状态变量设置为 public ——编译器会为你自动创建 getters 。
如果你最终需要在函数开始位置检查很多输入条件或者状态变量的值,你可以尝试使用 函数 。
如果你的合约有一个 send 函数,但你想要使用内置的 send 函数,你可以使用 address(contractVariable).send(amount)。
使用一个赋值语句就可以初始化 struct:x = MyStruct({a: 1, b: 2});
如果存储结构具有“紧打包(tightly packed)”,可以用分开的赋值语句来初始化:x.a = 1; x.b = 2;。这样可以使优化器更容易地一次性更新存储,使赋值的开销更小。
速查表
操作符优先级
以下是按评估顺序列出的操作符优先级。
abi.encode(...) returns (bytes): ABI - 对给定参数进行编码
abi.encodePacked(...) returns (bytes):对给定参数执行 紧打包编码
abi.encodeWithSelector(bytes4 selector, ...) returns (bytes): ABI - 对给定参数进行编码,并以给定的函数选择器作为起始的 4 字节数据一起返回
abi.encodeWithSignature(string
signature, ...) returns (bytes):等价于 abi.encodeWithSelector(bytes4(keccak256(signature), ...)
block.blockhash(uint blockNumber) returns (bytes32):指定区块的区块哈希——仅可用于最新的 256 个区块且不包括当前区块;而 blocks 从 0.4.22 版本开始已经不推荐使用,由 blockhash(uint blockNumber) 代替
block.coinbase (address):挖出当前区块的矿工的地址
block.difficulty (uint):当前区块的难度值
block.gaslimit (uint):当前区块的 gas 上限
block.number (uint):当前区块的区块号
block.timestamp (uint):当前区块的时间戳
gasleft() returns (uint256):剩余的 gas
msg.data (bytes):完整的 calldata
msg.gas (uint):剩余的 gas - 自 0.4.21 版本开始已经不推荐使用,由 gesleft() 代替
msg.sender (address):消息发送方(当前调用)
msg.value (uint):随消息发送的 wei 的数量
now (uint):当前区块的时间戳(等价于 block.timestamp)
tx.gasprice (uint):交易的 gas price
tx.origin (address):交易发送方(完整调用链上的原始发送方)
assert(bool condition):如果条件值为 false 则中止执行并回退所有状态变更(用做内部错误)
require(bool condition):如果条件值为 false 则中止执行并回退所有状态变更(用做异常输入或外部组件错误)
require(bool condition, string message):如果条件值为 false 则中止执行并回退所有状态变更(用做异常输入或外部组件错误),可以同时提供错误消息
revert():中止执行并回复所有状态变更
revert(string message):中止执行并回复所有状态变更,可以同时提供错误消息
blockhash(uint blockNumber) returns (bytes32):指定区块的区块哈希——仅可用于最新的 256 个区块
keccak256(...) returns (bytes32):计算 紧打包编码 的 Ethereum-SHA-3(Keccak-256)哈希
sha3(...) returns (bytes32):等价于 keccak256
sha256(...) returns (bytes32):计算 紧打包编码 的 SHA-256 哈希
ripemd160(...) returns (bytes20):计算 紧打包编码 的 RIPEMD-160 哈希
ecrecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) returns (address):基于椭圆曲线签名找回与指定公钥关联的地址,发生错误的时候返回 0
addmod(uint x, uint y, uint k) returns (uint):计算 (x + y) % k 的值,其中加法的结果即使超过 2**256 也不会被截取。从 0.5.0 版本开始会加入对 k != 0 的 assert(即会在此函数开头执行 assert(k != 0); 作为参数检查,译者注)。
mulmod(uint x, uint y, uint k) returns (uint):计算 (x * y) % k 的值,其中乘法的结果即使超过 2**256 也不会被截取。从 0.5.0 版本开始会加入对 k != 0 的 assert(即会在此函数开头执行 assert(k != 0); 作为参数检查,译者注)。
this (类型为当前合约的变量):当前合约实例,可以准确地转换为 address
super:当前合约的上一级继承关系的合约
selfdestruct(address recipient):销毁当前合约,把余额发送到给定地址
suicide(address recipient):与 selfdestruct 等价,但已不推荐使用
<address>.balance (uint256): 地址类型 的余额,以 Wei 为单位
<address>.send(uint256 amount) returns (bool):向 地址类型 发送给定数量的 Wei,失败时返回 false
<address>.transfer(uint256 amount):向 地址类型 发送给定数量的 Wei,失败时会把错误抛出(throw)
不要用 block.timestamp、now 或者 blockhash 作为随机种子,除非你明确知道你在做什么。
时间戳和区块哈希都可以在一定程度上被矿工所影响。如果你用哈希值作为随机种子,那么例如挖矿团体中的坏人就可以使用给定的哈希来执行一个赌场功能,如果他们没赢钱,他们可以简单地换一个哈希再试。
当前区块的时间戳必须比前一个区块的时间戳大,但唯一可以确定的就是它会是权威链(主链或者主分支)上两个连续区块时间戳之间的一个数值。
出于扩展性的原因,你无法取得所有区块的哈希。只有最新的 256 个区块的哈希可以拿到,其他的都将为 0。
函数可见性说明符
function myFunction() <visibility specifier> returns (bool) {
return true;
public:内部、外部均可见(参考为存储/状态变量创建 getter 函数)
private:仅在当前合约内可见
external:仅在外部可见(仅可修饰函数)——就是说,仅可用于消息调用(即使在合约内调用,也只能通过 this.func 的方式)
internal:仅在内部可见(也就是在当前 Solidity 源代码文件内均可见,不仅限于当前合约内,译者注)
修改器
pure 修饰函数时:不允许修改或访问状态——但目前并不是强制的。
view 修饰函数时:不允许修改状态——但目前不是强制的。
payable 修饰函数时:允许从调用中接收 以太币 。
constant 修饰状态变量时:不允许赋值(除初始化以外),不会占据 存储插槽 。
constant 修饰函数时:与 view 等价。
anonymous 修饰事件时:不把事件签名作为 topic 存储。
indexed 修饰事件时:将参数作为 topic 存储。
保留字
以下是 Solidity 的保留字,未来可能会变为语法的一部分:
abstract, after, alias, apply, auto, case, catch, copyof, default,
define, final, immutable, implements, in, inline, let, macro, match,
mutable, null, of, override, partial, promise, reference, relocatable,
sealed, sizeof, static, supports, switch, try, type, typedef, typeof,
unchecked.
语法表
SourceUnit = (PragmaDirective | ImportDirective | ContractDefinition)*
// Pragma actually parses anything up to the trailing ';' to be fully forward-compatible.
PragmaDirective = 'pragma' Identifier ([^;]+) ';'
ImportDirective = 'import' StringLiteral ('as' Identifier)? ';'
| 'import' ('*' | Identifier) ('as' Identifier)? 'from' StringLiteral ';'
| 'import' '{' Identifier ('as' Identifier)? ( ',' Identifier ('as' Identifier)? )* '}' 'from' StringLiteral ';'
ContractDefinition = ( 'contract' | 'library' | 'interface' ) Identifier
( 'is' InheritanceSpecifier (',' InheritanceSpecifier )* )?
'{' ContractPart* '}'
ContractPart = StateVariableDeclaration | UsingForDeclaration
| StructDefinition | ModifierDefinition | FunctionDefinition | EventDefinition | EnumDefinition
InheritanceSpecifier = UserDefinedTypeName ( '(' Expression ( ',' Expression )* ')' )?
StateVariableDeclaration = TypeName ( 'public' | 'internal' | 'private' | 'constant' )* Identifier ('=' Expression)? ';'
UsingForDeclaration = 'using' Identifier 'for' ('*' | TypeName) ';'
StructDefinition = 'struct' Identifier '{'
( VariableDeclaration ';' (VariableDeclaration ';')* ) '}'
ModifierDefinition = 'modifier' Identifier ParameterList? Block
ModifierInvocation = Identifier ( '(' ExpressionList? ')' )?
FunctionDefinition = 'function' Identifier? ParameterList
( ModifierInvocation | StateMutability | 'external' | 'public' | 'internal' | 'private' )*
( 'returns' ParameterList )? ( ';' | Block )
EventDefinition = 'event' Identifier EventParameterList 'anonymous'? ';'
EnumValue = Identifier
EnumDefinition = 'enum' Identifier '{' EnumValue? (',' EnumValue)* '}'
ParameterList = '(' ( Parameter (',' Parameter)* )? ')'
Parameter = TypeName StorageLocation? Identifier?
EventParameterList = '(' ( EventParameter (',' EventParameter )* )? ')'
EventParameter = TypeName 'indexed'? Identifier?
FunctionTypeParameterList = '(' ( FunctionTypeParameter (',' FunctionTypeParameter )* )? ')'
FunctionTypeParameter = TypeName StorageLocation?
// semantic restriction: mappings and structs (recursively) containing mappings
// are not allowed in argument lists
VariableDeclaration = TypeName StorageLocation? Identifier
TypeName = ElementaryTypeName
| UserDefinedTypeName
| Mapping
| ArrayTypeName
| FunctionTypeName
UserDefinedTypeName = Identifier ( '.' Identifier )*
Mapping = 'mapping' '(' ElementaryTypeName '=>' TypeName ')'
ArrayTypeName = TypeName '[' Expression? ']'
FunctionTypeName = 'function' FunctionTypeParameterList ( 'internal' | 'external' | StateMutability )*
( 'returns' FunctionTypeParameterList )?
StorageLocation = 'memory' | 'storage' | 'calldata'
StateMutability = 'pure' | 'constant' | 'view' | 'payable'
Block = '{' Statement* '}'
Statement = IfStatement | WhileStatement | ForStatement | Block | InlineAssemblyStatement |
( DoWhileStatement | PlaceholderStatement | Continue | Break | Return |
Throw | EmitStatement | SimpleStatement ) ';'
ExpressionStatement = Expression
IfStatement = 'if' '(' Expression ')' Statement ( 'else' Statement )?
WhileStatement = 'while' '(' Expression ')' Statement
PlaceholderStatement = '_'
SimpleStatement = VariableDefinition | ExpressionStatement
ForStatement = 'for' '(' (SimpleStatement)? ';' (Expression)? ';' (ExpressionStatement)? ')' Statement
InlineAssemblyStatement = 'assembly' StringLiteral? InlineAssemblyBlock
DoWhileStatement = 'do' Statement 'while' '(' Expression ')'
Continue = 'continue'
Break = 'break'
Return = 'return' Expression?
Throw = 'throw'
EmitStatement = 'emit' FunctionCall
VariableDefinition = ('var' IdentifierList | VariableDeclaration | '(' VariableDeclaration? (',' VariableDeclaration? )* ')' ) ( '=' Expression )?
IdentifierList = '(' ( Identifier? ',' )* Identifier? ')'
// Precedence by order (see github.com/ethereum/solidity/pull/732)
Expression
= Expression ('++' | '--')
| NewExpression
| IndexAccess
| MemberAccess
| FunctionCall
| '(' Expression ')'
| ('!' | '~' | 'delete' | '++' | '--' | '+' | '-') Expression
| Expression '**' Expression
| Expression ('*' | '/' | '%') Expression
| Expression ('+' | '-') Expression
| Expression ('<<' | '>>') Expression
| Expression '&' Expression
| Expression '^' Expression
| Expression '|' Expression
| Expression ('<' | '>' | '<=' | '>=') Expression
| Expression ('==' | '!=') Expression
| Expression '&&' Expression
| Expression '||' Expression
| Expression '?' Expression ':' Expression
| Expression ('=' | '|=' | '^=' | '&=' | '<<=' | '>>=' | '+=' | '-=' | '*=' | '/=' | '%=') Expression
| PrimaryExpression
PrimaryExpression = BooleanLiteral
| NumberLiteral
| HexLiteral
| StringLiteral
| TupleExpression
| Identifier
| ElementaryTypeNameExpression
ExpressionList = Expression ( ',' Expression )*
NameValueList = Identifier ':' Expression ( ',' Identifier ':' Expression )*
FunctionCall = Expression '(' FunctionCallArguments ')'
FunctionCallArguments = '{' NameValueList? '}'
| ExpressionList?
NewExpression = 'new' TypeName
MemberAccess = Expression '.' Identifier
IndexAccess = Expression '[' Expression? ']'
BooleanLiteral = 'true' | 'false'
NumberLiteral = ( HexNumber | DecimalNumber ) (' ' NumberUnit)?
NumberUnit = 'wei' | 'szabo' | 'finney' | 'ether'
| 'seconds' | 'minutes' | 'hours' | 'days' | 'weeks' | 'years'
HexLiteral = 'hex' ('"' ([0-9a-fA-F]{2})* '"' | '\'' ([0-9a-fA-F]{2})* '\'')
StringLiteral = '"' ([^"\r\n\\] | '\\' .)* '"'
Identifier = [a-zA-Z_$] [a-zA-Z_$0-9]*
HexNumber = '0x' [0-9a-fA-F]+
DecimalNumber = [0-9]+ ( '.' [0-9]* )? ( [eE] [0-9]+ )?
TupleExpression = '(' ( Expression? ( ',' Expression? )* )? ')'
| '[' ( Expression ( ',' Expression )* )? ']'
ElementaryTypeNameExpression = ElementaryTypeName
ElementaryTypeName = 'address' | 'bool' | 'string' | 'var'
| Int | Uint | Byte | Fixed | Ufixed
Int = 'int' | 'int8' | 'int16' | 'int24' | 'int32' | 'int40' | 'int48' | 'int56' | 'int64' | 'int72' | 'int80' | 'int88' | 'int96' | 'int104' | 'int112' | 'int120' | 'int128' | 'int136' | 'int144' | 'int152' | 'int160' | 'int168' | 'int176' | 'int184' | 'int192' | 'int200' | 'int208' | 'int216' | 'int224' | 'int232' | 'int240' | 'int248' | 'int256'
Uint = 'uint' | 'uint8' | 'uint16' | 'uint24' | 'uint32' | 'uint40' | 'uint48' | 'uint56' | 'uint64' | 'uint72' | 'uint80' | 'uint88' | 'uint96' | 'uint104' | 'uint112' | 'uint120' | 'uint128' | 'uint136' | 'uint144' | 'uint152' | 'uint160' | 'uint168' | 'uint176' | 'uint184' | 'uint192' | 'uint200' | 'uint208' | 'uint216' | 'uint224' | 'uint232' | 'uint240' | 'uint248' | 'uint256'
Byte = 'byte' | 'bytes' | 'bytes1' | 'bytes2' | 'bytes3' | 'bytes4' | 'bytes5' | 'bytes6' | 'bytes7' | 'bytes8' | 'bytes9' | 'bytes10' | 'bytes11' | 'bytes12' | 'bytes13' | 'bytes14' | 'bytes15' | 'bytes16' | 'bytes17' | 'bytes18' | 'bytes19' | 'bytes20' | 'bytes21' | 'bytes22' | 'bytes23' | 'bytes24' | 'bytes25' | 'bytes26' | 'bytes27' | 'bytes28' | 'bytes29' | 'bytes30' | 'bytes31' | 'bytes32'
Fixed = 'fixed' | ( 'fixed' [0-9]+ 'x' [0-9]+ )
Ufixed = 'ufixed' | ( 'ufixed' [0-9]+ 'x' [0-9]+ )
InlineAssemblyBlock = '{' AssemblyItem* '}'
AssemblyItem = Identifier | FunctionalAssemblyExpression | InlineAssemblyBlock | AssemblyLocalBinding | AssemblyAssignment | AssemblyLabel | NumberLiteral | StringLiteral | HexLiteral
AssemblyLocalBinding = 'let' Identifier ':=' FunctionalAssemblyExpression
AssemblyAssignment = ( Identifier ':=' FunctionalAssemblyExpression ) | ( '=:' Identifier )
AssemblyLabel = Identifier ':'
FunctionalAssemblyExpression = Identifier '(' AssemblyItem? ( ',' AssemblyItem )* ')'