智能合约Solidity编程语言

Solidity语言

本文全部来自《智能合约Solidity编程语言》,我只是做了搬运工搬过来方便我自己查看而已。
Solidity是一种智能合约高级语言,运行在Ethereum虚拟机(EVM)之上。

Solidity与其它语言相关的特点?

它的语法接近于Javascript,是一种面向对象的语言。但作为一种真正意义上运行在网络上的去中心合约,它又有很多的不同,下面列举一些:

  • 以太坊底层是基于帐户,而非UTXO的,所以有一个特殊的 Address的类型。用于定位用户,定位合约,定位合约的代码(合约本身也是一个帐户)。
  • 由于语言内嵌框架是支持支付的,所以提供了一些关键字,如 payable,可以在语言层面直接支持支付,而且超级简单。
  • 存储是使用网络上的区块链,数据的每一个状态都可以永久存储,所以需要确定变量使用内存,还是区块链。
  • 运行环境是在去中心化的网络上,会比较强调合约或函数执行的调用的方式。因为原来一个简单的函数调用变为了一个网络上的节点中的代码执行,分布式的感觉。
  • 最后一个非常大的不同则是它的异常机制,一旦出现异常,所有的执行都将会被回撤,这主要是为了保证合约执行的原子性,以避免中间状态出现的数据不一致。

Hello Wolrd!

听起来高大上,其实入手玩起来也可以很简单:

1
2
3
4
5
6
7
8
pragma solidity ^0.4.0;
contract HelloWorld{
uint balance;
function update(uint amount) returns (address, uint){
balance += amount;
return (msg.sender, balance);
}
}

通过读取参数输入的新值,并将之累加至合约的变量中,返回发送人的地址,和最终的累计值。

浏览器编译器Remix

使用无需安装的浏览器编译器 Remix 可以立即看到效果。

输入上述代码,点击Create按钮,就能在浏览器中创建能调用函数的按钮。在 update按钮旁输入入参,点击按钮,就能执行函数调用并打印出函数返回的结果了。

备注:如果出现错误,可以等待浏览器资源加载完成,或强制刷新后再试。

Solidity智能合约文件结构

版本申明

1
pragma solidity ^0.4.0

说明:

1 版本要高于0.4才可以编译

2 ^号表示高于0.5的版本则不可编译,第三位的版本号但可以变,留出来用做bug可以修复(如0.4.1的编译器有bug,可在0.4.2修复,现有合约不用改代码)。

引用其它源文件

  • 全局引入 *

    1
    import "filename";
  • 自定义命名空间引入 *

    1
    import * as symbolName from "filename"

分别定义引入

1
import  {symbol1 as alias, symbol2} from "filename"

非es6兼容的简写语法

1
import "filename" as symbolName

等同于上述

1
import * as symbolName from "filename"

关于路径

引入文件路径时要注意,非 .打头的路径会被认为是绝对路径,所以要引用同目录下的文件使用

1
import "./x" as x

也不要使用下述方式,这样会是在一个全局的目录下

1
import "x" as x;

为什么会有这个区别,是因为这取决于编译器,如果解析路径,通常来说目录层级结构并不与我们本地的文件一一对应,它非常有可能是通过ipfs,http,或git建立的一个网络上的虚拟目录。

编译器解析引用文件机制

各编译器提供了文件前缀映射机制。

  1. 可以将一个域名下的文件映射到本地,从而从本地的某个文件中读取

  2. 提供对同一实现的不同版本的支持(可能某版本的实现前后不兼容,需要区分)

  3. 如果前缀相同,取最长,

  4. 有一个”fallback-remapping”机制,空串会映射到”/usr/local/include/solidify”

solc编译器

命令行编译器,通过下述命令命名空间映射提供支持

1
context:prefix=target

上述的 context:=target是可选的。所有 context目录下的以 prefix开头的会被替换为 target

举例来说,如果你将 github.com/ethereum/dapp-bin拷到本地的 /usr/local/dapp-bin,并使用下述方式使用文件

1
import "github.com/ethereum/dapp-bin/library/iterable_mapping.sol" as it_mapping;

要编译这个文件,使用下述命令:

1
solc github.com/ethereum/dapp-bin=/usr/local/dapp-bin source.sol

另一个更复杂的例子,如果你使用一个更旧版本的dapp-bin,旧版本在/url/local/dapp-bin_old,那么,你可以使用下述命令编译

1
2
3
solc module1:github.com/ethereum/dapp-bin=/usr/local/dapp-bin  \
modeule2:github.com/ethereum/dapp-bin=/usr/local/dapp-bin_old \
source.sol

需要注意的是solc仅仅允许包含实际存在的文件。它必须存在于你重映射后目录里,或其子目录里。如果你想包含直接的绝对路径包含,那么可以将命名空间重映射为 =\

备注:如果有多个重映射指向了同一个文件,那么取最长的那个文件。

browser-solidity编译器:

browser-solidity编译器默认会自动映射到github上,然后会自动从网络上检索文件。例如:你可以通过下述方式引入一个迭代包:

1
import "github.com/ethereum/dapp-bin/library/iterable_mapping.sol" as it_mapping

备注:未来可能会支持其它的源码方式

代码注释

两种方式,单行( //),多行使用( /*…*/)

示例:

1
2
3
4
5
// this is a single-line comment
/*
this is a
mulit-line comment
*/

文档注释

写文档用。三个斜杠 ////** … */,可使用 Doxygen语法,以支持生成对文档的说明,参数验证的注解,或者是在用户调用这个函数时,弹出来的确认内容。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pragma solidity ^0.4.0
/** @title Shape calculator.*/
contract shapeCalculator{
/**
*@dev calculate a rectangle's suface and perimeter
*@param w width of the rectangles
*@param h height of the rectangles
*@return s surface of the rectangles
*@return p perimeter of the rectangles
*/
function rectangles(uint w, uint h) returns (uint s, uint p){
s = w * h;
p = 2 * ( w + h) ;
}
}

智能合约源文件的基本要素概览(Structure of a Contract)

  • 合约类似面向对象语言中的类。
  • 支持继承

每个合约中可包含 状态变量(State Variables)函数(Functions), 函数修饰符(Function Modifiers), 事件(Events), 结构类型(Structs Types)枚举类型(Enum Types)

状态变量(State Variables)

变量值会永久存储在合约的存储空间

1
2
3
4
5
pragma solidity ^0.4.0;
// simple store example
contract simpleStorage{
uint valueStore; //state variable
}

详情见 类型(Types)章节,关于所有支持的类型和变量相关的可见性(Visibility and Accessors)。

函数(Functions)

智能合约中的一个可执行单元。

示例:

1
2
3
4
5
6
7
pragma solidity ^0.4.0;
contract simpleMath{
//Simple add function,try a divide action?
function add(uint x, uint y) returns (uint z){
z = x + y;
}
}

上述示例展示了一个简单的加法函数。

函数调用可以设置为内部(Internal)的和外部(External)的。同时对于其它合同的不同级别的可见性和访问控制(Visibility and Accessors)。具体的情况详见后面类型中关于函数的章节。

函数修饰符(Function Modifiers)

函数修饰符用于增强语义。详情见函数修饰符相关章节。

事件(Events)

事件是以太坊虚拟机(EVM)日志基础设施提供的一个便利接口。用于获取当前发生的事件。

示例:

1
2
3
4
5
6
7
pragma solidity ^0.4.0;
contract SimpleAuction {
event aNewHigherBid(address bidder, uint amount);
function bid(uint bidValue) external {
aNewHigherBid(msg.sender, msg.value);
}
}

关于事件如何声明和使用,详见后面事件相关章节。

结构体类型(Structs Types)

自定义的将几个变量组合在一起形成的类型。详见关于结构体相关章节。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pragma solidity ^0.4.0;
contract Company {
//user defined `Employee` struct type
//group with serveral variables
struct employee{
string name;
uint age;
uint salary;
}
//User defined `manager` struct type
//group with serveral variables
struct manager{
employee employ;
string title;
}
}

枚举类型

特殊的自定义类型,类型的所有值可枚举的情况。详情见后续相关章节。

示例:

1
2
3
4
pragma solidity ^0.4.0;
contract Home {
enum Switch{On,Off}
}

值类型与引用类型

由于Solidity是一个静态类型的语言,所以编译时需明确指定变量的类型(包括 本地变量状态变量), Solidity编程语言提供了一些 基本类型(elementary types)可以用来组合成复杂类型。

类型可以与不同运算符组合,支持表达式运算,你可以通过 表达式的执行顺序(Order of Evaluation of Expressions)来了解执行顺序。

值类型(Value Type)

值类型包含

  • 布尔(Booleans)
  • 整型(Integer)
  • 地址(Address)
  • 定长字节数组(fixed byte arrays)
  • 有理数和整型(Rational and Integer LiteralsString literals)
  • 枚举类型(Enums)
  • 函数(Function Types)

为什么会叫 值类型,是因为上述这些类型在传值时,总是值传递。比如在函数传参数时,或进行变量赋值时。

引用类型(Reference Types)

复杂类型,占用空间较大的。在拷贝时占用空间较大。所以考虑通过引用传递。常见的引用类型有:

  • 不定长字节数组(bytes)
  • 字符串(string)
  • 数组(Array)
  • 结构体(Struts)

关于参数传递的相关方式的进一步了解: http://baike.baidu.com/item/参数传递

布尔(Booleans)

bool: 可能的取值为常量值 truefalse.

支持的运算符:

  • !逻辑非
  • && 逻辑与
  • || 逻辑或
  • == 等于
  • != 不等于

备注:运算符 &&||是短路运算符,如 f(x)||g(y),当 f(x)为真时,则不会继续执行 g(y)

整型(Integer)

int/uint:变长的有符号或无符号整型。变量支持的步长以 8递增,支持从 uint8uint256,以及 int8int256。需要注意的是, uintint默认代表的是 uint256int256

支持的运算符:

  • 比较: <=<==!=>=>,返回值为 bool类型。
  • 位运算符: &|,( ^异或),( ~非)。
  • 数学运算: +-,一元运算 +*/,( %求余),( **平方,比如 x ** y == y 个 x 相乘,2 ** 3 === 2 * 2 * 2)。

整数除法总是截断的,但如果运算符是字面量,则不会截断(后面会进一步提到)。另外除 0会抛异常 ,我们来看看下面的这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
pragma solidity ^0.4.0;
// simple store example
contract simpleStorage{
uint valueStore; //
function add(uint x, uint y) returns (uint z){
z = x + y;
}
function divide() returns (uint z){
uint x = 1;
uint y = 2;
z = x / y;
}
}

整数字面量

整数字面量,由包含0-9的数字序列组成,默认被解释成十进制。在 Solidity中不支持八进制,前导 0会被默认忽略,如 0100,会被认为是 100

小数由 .组成,在他的左边或右边至少要包含一个数字。如 1..11.3均是有效的小数。

字面量本身支持任意精度,也就是可以不会运算溢出,或除法截断。但当它被转换成对应的非字面量类型,如整数或小数。或者将他们与非字面量进行运算,则不能保证精度了。

1
2
3
4
5
6
7
8
9
10
11
pragma solidity ^0.4.0;
contract IntegerLiteral{
function integerTest() returns (uint, uint){
//超出运算字长了
var i = (2**800 + 1) - 2**800;
var j = 1/3*3;
//小数运算
var k = 0.5*8;
return (i, j);
}
}

总之来说就是,字面量怎么都计算都行,但一旦转为对应的变量后,再计算就不保证精度啦。

地址(Address)

地址: 以太坊地址的长度,大小20个字节,160位,所以可以用一个 uint160编码。地址是所有合约的基础,所有的合约都会继承地址对象,也可以随时将一个地址串,得到对应的代码进行调用。当然地址代表一个普通帐户时,就没有这么多丰富的功能啦。

支持的运算符

  • >=>==!=<=<

地址类型的成员

属性: balance

函数: send()call()delegatecall()callcode()

地址字面量

十六进制的字符串,凡是能通过地址合法性检查(address checksum test),就会被认为是地址,如 0xdCad3a6d3569DF655070DEd06cb7A1b2Ccd1D3AF。需要注意的是39到41位长的没有通过地址合法性检查的,会提示一个警告,但会被视为普通的有理数字面量。

balance

通过它能得到一个地址的余额。

1
2
3
4
5
6
pragma solidity ^0.4.0;
contract addressTest{
function getBalance(address addr) returns (uint){
return addr.balance;
}
}

我们可以把上述代码放入 remix中,看看效果,参见下面的操作演示:

address_demo

演示中的一个核心要点是,编译后,我们能得到当前合约的地址,并将该地址复制到输入框中,记得录入地址项时要加英文的双引号,否则会报 Error encoding arguments: SyntaxError: JSON Parse error: Expected ']'

this

如果只是想得到当前合约的余额,其实可以这样写:

1
2
3
4
5
6
pragma solidity ^0.4.0;
contract addressTest{
function getBalance() returns (uint){
return this.balance;
}
}

原因是对于合约来说,地址代表的就是合约本身,合约对象默认继承自地址对象,所以内部有地址的属性。

地址的方法 send()

用来向某个地址发送货币(货币单位是 wei)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pragma solidity ^0.4.0;
//请注意这个仅是Demo,请不要用到正式环境
contract PayTest {
//得到当前合约的余额
function getBalance() returns (uint) {
return this.balance;//0
}
//向当前合约存款
function deposit() payable returns(address addr, uint amount, bool success){
//msg.sender 全局变量,调用合约的发起方
//msg.value 全局变量,调用合约的发起方转发的货币量,以wei为单位。
//send() 执行的结果
return (msg.sender, msg.value, this.send(msg.value));
}
}

这个合约实现的是充值。 this.send(msg.value)意指向合约自身发送 msg.value量的以太币。 msg.value是合约调用方附带的以太币。

下面是操作演示:

address_demo_2

关于发送者的帐号,发送的以太币数量设置,需切换到Remix的小飞机图标的配置页。要在调用 deposit前在 Value输入项填入要发的以太币数量。在 getBalance时要记得将 value项内填的值去掉,因为 getBalance方法,并不是 payable的,不支持货币。send()方法执行时有一些风险:

  • 调用递归深度不能超1024。
  • 如果 gas不够,执行会失败。
  • 所以使用这个方法要检查成功与否。或为保险起见,货币操作时要使用一些最佳实践。

如果执行失败,将会回撤所有交易,所以务必留意返回结果。

call(), callcode()和 delegatecall()

为了同一些不支持ABI协议的进行直接交互(一般的 web3.jssoldity都是支持的)。可以使用 call()函数,用来向另一个合约发送原始数据。参数支持任何类型任意数量。每个参数会按规则(规则是按ABI)打包成32字节并一一拼接到一起。

call()方法支持ABI协议定义的函数选择器。如果第一个参数恰好4个字节,在这种情况下,会被认为根据ABI协议定义的函数器指定的函数签名。所以如果你只是想发送消息体,需要避免第一个参数是4个字节。

call方法返回一个 bool值,以表明执行成功还是失败。正常结束返回 true,异常终止返回 false。我们无法解析返回结果,因为这样我们得事前知道返回的数据的编码和数据大小(这里的潜在假设是不知道对方使用的协议格式,所以也不会知道返回的结果如何解析,有点祼协议测试的感觉)。

同样我们也可以使用 delegatecall(),它与 call方法的区别在于,仅仅是代码会执行,而其它方面,如(存储,余额等)都是用的当前的合约的数据。 delegatecall()方法的目的是用来执行另一个合约中的工具库。所以开发者需要保证两个合约中的存储变量能兼容,来保证 delegatecall()能顺利执行。

在homestead阶段之前,仅有一个受限的多样的 callcode()方法可用,但并未提供对 msg.sendermsg.value的访问权限。

上面的这三个方法 call()delegatecall()callcode()都是底层的消息传递调用,最好仅在万不得已才进行使用,因为他们破坏了Solidity的类型安全。

关于 call()函数究竟发的什么消息体,函数选择器究竟怎么用,参见 这个文章的挖掘。

字节数组(byte arrays)

定长字节数组(Fixed-size byte arrays)

bytes1, … , bytes32,允许值以步长 1递增。 byte默认表示 byte1

运算符

比较: <=<==!=>=>,返回值为 bool类型。

位运算符: &|^(异或), ~

支持序号的访问,与大多数语言一样,取值范围[0, n),其中 n表示长度。

成员变量

.length表示这个字节数组的长度(只读)。

动态大小的字节数组

bytes: 动态长度的字节数组,非值类型

string: 动态长度的UTF-8编码的字符类型,非值类型

一个好的使用原则是:

  • bytes用来存储任意长度的字节数据, string用来存储任意长度的 UTF-8编码的字符串数据。
  • 如果长度可以确定,尽量使用定长的如 byte1byte32中的一个,因为这样更省空间。

小数

定点数

定点数 Solidity还不完全支持,目前可以用来声明变量,但不可以用来赋值。fixed/ufixed: 表示有符号和无符号的定点数。对于关键字ufixedMxN 和 ufixedMxN,M表示这个类型占用的位数,范围为8到256,步长为8。N表示数点的个数,N的范围为0到80之间。ufixed和fixed分别默认代表ufixed128x19和fixed128x19。

小数字面量

如果字面量计算的结果不是一个整数,那么将会转换为一个对应的 ufixed,或 fixed类型。 Solidity会选择合适的大小,以能尽量包含小数部分。

例,在 var x = 1 / 4中, x的实际类型是 ufixed0x8。而在 var x = 1/ 3中,类型会是 ufixed0x256,因为这个结果表示是无限的,所以他只能是无限接近。

支持的运算符

适用于整型的操作符,同时适用于数字的字面量运算表达式,当操作结果是整数时。如果有任何一方是有理数,将不允许使用位操作符。如果指数是小数,还将不能进行取幂运算。

数字字面量

Solidity对每一个有理数都有一个数值字面量类型。整数字面量和有理数字面量从属于数字面量。所有的数字字面表式的结果都属于数字字面类型。所以 1 + 22 + 1都属于同样的有理数的数字字面类型 3

二进制表示

大多数含小数的十进制,均不可被二进制准确表达,比如 5.3743的类型可能是 ufixed8*248。如果你想使用这样的值,需要明确指定精度 x + ufixed(5.3743),否则会报类型转换错误。

字面量截断

整数上的字面量除法,在早期的版本中是被截断的,但现在可以被转为有理数了,如 5 /2的值为 2.5

字面量转换

数字的字面量表达式,一旦其中含有非字面量表达式,它就会被转为一个非字面量类型。下面代码中表达式的结果将会被认为是一个有理数:

1
2
3
4
5
6
7
8
pragma solidity ^0.4.0;
contract IntegerLiteralConvert{
function literalTest(){
uint128 a = 1;
//uint128 b = 2.5 + a + 0.5;
//Error: Operator + not compatible with types rational_const 5/2 and uint128
}
}

虽然我们知道上述表达式运算的结果将是一个整型,但最终被编译器认为是小数型,所以上述代码编译不能通过。

字符串(String literal)

字符串字面量

字符串字面量是指由单引号,或双引号引起来的字符串。字符串并不像C语言,包含结束符, foo这个字符串大小仅为三个字节。

定长字节数组

正如整数一样,字符串的长度类型可以是变长的。特殊之处在于,可以隐式的转换为 byte1,… byte32。下面来看看这个特性:

1
2
3
4
5
6
7
8
9
pragma solidity ^0.4.0;
contract StringConvert{
function test() returns (bytes3){
bytes3 a = "123";
//bytes3 b = "1234";
//Error: Type literal_string "1234" is not implicitly convertible to expected type bytes3.
return a;
}
}

上述的字符串字面量,会隐式转换为 bytes3。但这样不是理解成 bytes3的字面量方式一个意思。

转义字符

字符串字面量支持转义字符,比如 \n\xNN\uNNNN。其中 \xNN表式16进制值,最终录入合适的字节。而 \uNNNN表示 Unicode码点值,最终会转换为 UTF8的序列。

十六进制字面量

十六进制字面量,以关键字 hex打头,后面紧跟用单或双引号包裹的字符串。如 hex"001122ff"。在内部会被表示为二进制流。通过下面的例子来理解下是什么意思:

1
2
3
4
5
6
7
8
9
pragma solidity ^0.4.0;
contract HexLiteral{
function test() returns (string){
var a = hex"001122FF";
//var b = hex"A";
//Expected primary expression
return a;
}
}

由于一个字节是8位,所以一个 hex是由两个 [0-9a-z]字符组成的。所以 var b = hex"A";不是成双的字符串是会报错的。

转换

十六进制的字面量与字符串可以进行同样的类似操作:

1
2
3
4
5
6
7
pragma solidity ^0.4.0;
contract HexLiteralBytes{
function test() returns (bytes4, bytes1, bytes1, bytes1, bytes1){
bytes4 a = hex"001122FF";
return (a, a[0], a[1], a[2], a[3]);
}
}

可以发现,它可以隐式的转为 bytes,上述代码的执行结果如下:

1
2
3
4
5
6
7
8
9
Result: "0x001122ff00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011000000000000000000000000000000000000000000000000000000000000002200000000000000000000000000000000000000000000000000000000000000ff00000000000000000000000000000000000000000000000000000000000000"
Transaction cost: 21857 gas.
Execution cost: 585 gas.
Decoded:
bytes4: 0x001122ff
bytes1: 0x00
bytes1: 0x11
bytes1: 0x22
bytes1: 0xff

枚举

枚举类型是在Solidity中的一种用户自定义类型。他可以显示的转换与整数进行转换,但不能进行隐式转换。显示的转换会在运行时检查数值范围,如果不匹配,将会引起异常。枚举类型应至少有一名成员。我们来看看下面的例子吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
pragma solidity ^0.4.0;
contract test {
enum ActionChoices { GoLeft, GoRight, GoStraight, SitStill }
ActionChoices choice;
ActionChoices constant defaultChoice = ActionChoices.GoStraight;
function setGoStraight() {
choice = ActionChoices.GoStraight;
}
// Since enum types are not part of the ABI, the signature of "getChoice"
// will automatically be changed to "getChoice() returns (uint8)"
// for all matters external to Solidity. The integer type used is just
// large enough to hold all enum values, i.e. if you have more values,
// `uint16` will be used and so on.
function getChoice() returns (ActionChoices) {
return choice;
}
function getDefaultChoice() returns (uint) {
return uint(defaultChoice);
}
}

函数(Function Types)

函数类型即是函数这种特殊的类型。

  • 可以将一个函数赋值给一个变量,一个函数类型的变量。
  • 还可以将一个函数作为参数进行传递。
  • 也可以在函数调用中返回一个函数。

函数类型有两类;可分为 internalexternal函数。

内部函数(internal)

因为不能在当前合约的上下文环境以外的地方执行,内部函数只能在当前合约内被使用。如在当前的代码块内,包括内部库函数,和继承的函数中。

外部函数(External)

外部函数由地址和函数方法签名两部分组成。可作为 外部函数调用的参数,或者由 外部函数调用返回。

函数的定义

完整的函数的定义如下:

1
function () {internal(默认)|external} [constant] [payable] [returns ()]

若不写类型,默认的函数类型是 internal的。如果函数没有返回结果,则必须省略 returns关键字。下面我们通过一个例子来了解一下。

1
2
3
4
5
6
7
8
9
pragma solidity ^0.4.0;
contract Test{
//默认是internal类型的
function noParameter() returns (uint){}
//无返回结果
function noReturn1(uint x) {}
//如果无返回结果,必须省略`returns`关键字
//function noReturn2(uint x) returns {}
}

如果一个函数变量没有初始化,直接调用它将会产生异常。如果 delete了一个函数后调用,也会发生同样的异常。

如果 外部函数类型在 Solidity的上下文环境以外的地方使用,他们会被视为 function类型。编码为20字节的函数所在地址,紧跟4字节的函数方法签名的共占24字节的 bytes24类型。

合约中的 public的函数,可以使用 internalexternal两种方式来调用。下面来看看,两种方式的不同之处。

函数的 internal 与 external

调用一个函数 f()时,我们可以直接调用 f(),或者使用 this.f()。但两者有一个区别。前者是通过 internal的方式在调用,而后者是通过 external的方式在调用。请注意,这里关于 this的使用与大多数语言相背。下面通过一个例子来了解他们的不同:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
pragma solidity ^0.4.5;
contract FuntionTest{
function internalFunc() internal{}
function externalFunc() external{}
function callFunc(){
//直接使用内部的方式调用
internalFunc();
//不能在内部调用一个外部函数,会报编译错误。
//Error: Undeclared identifier.
//externalFunc();
//不能通过`external`的方式调用一个`internal`
//Member "internalFunc" not found or not visible after argument-dependent lookup in contract FuntionTest
//this.internalFunc();
//使用`this`以`external`的方式调用一个外部函数
this.externalFunc();
}
}
contract FunctionTest1{
function externalCall(FuntionTest ft){
//调用另一个合约的外部函数
ft.externalFunc();
//不能调用另一个合约的内部函数
//Error: Member "internalFunc" not found or not visible after argument-dependent lookup in contract FuntionTest
//ft.internalFunc();
}
}

函数例子(官方)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
pragma solidity ^0.4.0;
library ArrayUtils{
function range(uint length) internal returns (uint[] memory r){
r = new uint[](length);
for(uint i = 0; i < length; i++){
r[i] = i;
}
}
function map(uint[] memory self, function(uint) returns (uint) f) internal returns (uint[] memory r){
r = new uint[](self.length);
for(uint i = 0; i < self.length; i++){
r[i] = f(self[i]);
}
}
function reduce(uint[] memory self, function(uint x, uint y) returns(uint) f) internal returns (uint r){
r = self[0];
for(uint i = 1; i < self.length; i++){
r = f(r, self[i]);
}
}
}
contract Pyramid{
using ArrayUtils for *;
function pryamid(uint length) returns (uint){
return ArrayUtils.range(length).map(square).reduce(sum);
}
function square(uint x) returns (uint){
return x * x;
}
function sum(uint x, uint y) returns (uint){
return x + y;
}
}

Question?

  • library是什么呢?
  • library引入时为什么使用 using,这和文件引入的 import有何区别?
  • library内的函数全是 internal的?
  • library内的函数,他的参数函数为什么是 internal的,不应该是 external的?
  • uint[]是什么类型,不能写做 []uint
  • memory又是什么呢,为什么 map函数明明是两个参数,但只需要传一个呢?
  • http://solidity.readthedocs.io/en/develop/types.html#function-types
  • 函数签名的编码方式可查看函数选择器相关章节, 【文档翻译系列】ABI详解

引用类型(Reference Types)

复杂类型。不同于之前值类型,复杂类型占的空间更大,超过256字节,因为拷贝它们占用更多的空间。由此我们需要考虑将它们存储在什么位置 内存(memory,数据不是永久存在的)存储(storage,值类型中的状态变量)

后面我们会讲到常见的引用类型。

数据位置(Data location)

复杂类型,如 数组(arrays)数据结构(struct)在Solidity中有一个额外的属性,数据的存储位置。可选为 memorystorage

memory存储位置同我们普通程序的内存一致。即分配,即使用,越过作用域即不可被访问,等待被回收。而在区块链上,由于底层实现了图灵完备,故而会有非常多的状态需要永久记录下来。比如,参与众筹的所有参与者。那么我们就要使用 storage这种类型了,一旦使用这个类型,数据将永远存在。

基于程序的上下文,大多数时候这样的选择是默认的,我们可以通过指定关键字 storagememory修改它。

默认的函数参数,包括返回的参数,他们是 memory。默认的局部变量是 storage的。而默认的状态变量(合约声明的公有变量)是 storage

另外还有第三个存储位置 calldata。它存储的是函数参数,是只读的,不会永久存储的一个数据位置。 外部函数的参数(不包括返回参数)被强制指定为 calldata。效果与 memory差不多。

数据位置指定非常重要,因为不同数据位置变量赋值产生的结果也不同。在 memorystorage之间,以及它们和 状态变量(即便从另一个状态变量)中相互赋值,总是会创建一个完全不相关的拷贝。

将一个 storage的状态变量,赋值给一个 storage的局部变量,是通过引用传递。所以对于局部变量的修改,同时修改关联的状态变量。但另一方面,将一个 memory的引用类型赋值给另一个 memory的引用,不会创建另一个拷贝。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pragma solidity ^0.4.0;
contract DataLocation{
uint valueType;
mapping(uint => uint) public refrenceType;
function changeMemory(){
var tmp = valueType;
tmp = 100;
}
function changeStorage(){
var tmp = refrenceType;
tmp[1] = 100;
}
function getAll() returns (uint, uint){
return (valueType, refrenceType[1]);
}
}

下面来看下官方的例子说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
pragma solidity ^0.4.0;
contract C {
uint[] x; // the data location of x is storage
// the data location of memoryArray is memory
function f(uint[] memoryArray) {
x = memoryArray; // works, copies the whole array to storage
var y = x; // works, assigns a pointer, data location of y is storage
y[7]; // fine, returns the 8th element
y.length = 2; // fine, modifies x through y
delete x; // fine, clears the array, also modifies y
// The following does not work; it would need to create a new temporary /
// unnamed array in storage, but storage is "statically" allocated:
// y = memoryArray;
// This does not work either, since it would "reset" the pointer, but there
// is no sensible location it could point to.
// delete y;
g(x); // calls g, handing over a reference to x
h(x); // calls h and creates an independent, temporary copy in memory
}
function g(uint[] storage storageArray) internal {}
function h(uint[] memoryArray) {}
}

总结

强制的数据位置(Forced data location)

  • 外部函数(External function)的参数(不包括返回参数)强制为: calldata
  • 状态变量(State variables)强制为: storage

默认数据位置(Default data location)

  • 函数参数包括返回参数: memory
  • 所有其它的局部变量: storage

更多请查看关于数据位置的进一步挖掘: http://me.tryblockchain.org/solidity-data-location.html

参考资料

本地变量但是为什么是 stroage的,没有太想明白。 http://ethereum.stackexchange.com/questions/9781/solidity-default-data-location-for-local-vars

数组

数组可以声明时指定长度,或者是变长的。对 storage的数组来说,元素类型可以是任意的,类型可以是数组,映射类型,数据结构等。但对于 memory的数组来说。如果函数是对外可见的,那么函数参数不能是映射类型的数组,只能是支持ABI的类型。

一个类型为T,长度为k的数组,可以声明为 T[k],而一个变长的数组则声明为 T[]

你还可以声明一个多维数据,如一个类型为 uint的数组长度为5的变长数组,可以声明为 uint[][5] x。需要留心的是,相比非区块链语言,多维数组的长度声明是反的。

要访问第三个动态数据的,第二个元素,使用 x[2][1]。数组的序号是从0开始的,序号顺序与定义相反。

bytesstring是一种特殊的数组。 bytes类似 byte[],但在外部函数作为参数调用中,会进行压缩打包,更省空间,所以应该尽量使用 bytesstring类似 bytes,但不提供长度和按序号的访问方式。

由于 bytesstring,可以自由转换,你可以将字符串 s通过 bytes(s)转为一个 bytes。但需要注意的是通过这种方式访问到的是UTF-8编码的码流,并不是独立的一个个字符。比如中文编码是多字节,变长的,所以你访问到的很有可能只是其中的一个代码点。

类型为数组的状态变量,可以标记为 public类型,从而让 Solidity创建一个访问器,如果要访问数组的某个元素,指定数字下标就好了。

创建一个数组

可使用 new关键字创建一个 memory的数组。与 stroage数组不同的是,你不能通过 .length的长度来修改数组大小属性。我们来看看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pragma solidity ^0.4.0;
contract C {
function f() {
//创建一个memory的数组
uint[] memory a = new uint[](7);
//不能修改长度
//Error: Expression has to be an lvalue.
//a.length = 100;
}
//storage
uint[] b;
function g(){
b = new uint[](7);
//可以修改storage的数组
b.length = 10;
b[9] = 100;
}
}

在上面的代码中, f()方法尝试调整数组 a的长度,编译器报错 Error: Expression has to be an lvalue.。但在 g()方法中我们看到可以修改。

字面量及内联数组

数组字面量,是指以表达式方式隐式声明一个数组,并作为一个数组变量使用的方式。下面是一个简单的例子:

1
2
3
4
5
6
7
8
9
pragma solidity ^0.4.0;
contract C {
function f() {
g([uint(1), 2, 3]);
}
function g(uint[3] _data) {
// ...
}
}

通过数组字面量,创建的数组是 memory的,同时还是定长的。元素类型则是使用刚好能存储的元素的能用类型,比如代码里的 [1, 2, 3],只需要 uint8即可存储。由于 g()方法的参数需要的是 uint(默认的 uint表示的其实是 uint256),所以要使用 uint(1)来进行类型转换。

还需注意的一点是,定长数组,不能与变长数组相互赋值,我们来看下面的代码:

1
2
3
4
5
6
7
pragma solidity ^0.4.0;
contract C {
function f() {
// The next line creates a type error because uint[3] memory
// cannot be converted to uint[] memory.
uint[] x = [uint(1), 3, 4];
}

限制的主要原因是,ABI不能很好的支持数组,已经计划在未来移除这样的限制。(当前的ABI接口,不是已经能支持数组了?)

数组的属性和方法

length属性

数组有一个 .length属性,表示当前的数组长度。 storage的变长数组,可以通过给 .length赋值调整数组长度。 memory的变长数组不支持。

不能通过访问超出当前数组的长度的方式,来自动实现上面说的这种情况。 memory数组虽然可以通过参数,灵活指定大小,但一旦创建,大小不可调整,对于变长数组,可以通过参数在编译期指定数组大小。

push方法

storage的变长数组和 bytes都有一个 push(),用于附加新元素到数据末端,返回值为新的长度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pragma solidity ^0.4.0;
contract C {
uint[] u;
bytes b;
function testArryPush() returns (uint){
uint[3] memory a = [uint(1), 2, 3];
u = a;
return u.push(4);
}
function testBytesPush() returns (uint){
b = new bytes(3);
return b.push(4);
}
}

限制的情况

当前在外部函数中,不能使用多维数组。

另外,基于EVM的限制,不能通过外部函数返回动态的内容。

1
2
3
4
5
pragma solidity ^0.4.0;
contract C {
function f() returns (uint[]) {
}
}

在上面的例子中,通过web.js调用能返回数据,但在Solidity中不能返回数据。一种临时的解决办法,是使用一个非常大的静态数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
pragma solidity ^0.4.0;
contract ArrayContract {
//the orginal length of m_aLotOfIntegers is 2**20
//run it cause a out of gas,so change it to a much smaller 2**2 for test
uint[2**2] m_aLotOfIntegers;
// Note that the following is not a pair of arrays but an array of pairs.
bool[2][] m_pairsOfFlags;
// newPairs is stored in memory - the default for function arguments
function setAllFlagPairs(bool[2][] newPairs) {
// assignment to a storage array replaces the complete array
m_pairsOfFlags = newPairs;
}
function setFlagPair(uint index, bool flagA, bool flagB) {
// access to a non-existing index will throw an exception
m_pairsOfFlags[index][0] = flagA;
m_pairsOfFlags[index][1] = flagB;
}
function changeFlagArraySize(uint newSize) {
// if the new size is smaller, removed array elements will be cleared
m_pairsOfFlags.length = newSize;
}
function clear() {
// these clear the arrays completely
delete m_pairsOfFlags;
delete m_aLotOfIntegers;
// identical effect here
m_pairsOfFlags.length = 0;
}
function addFlag(bool[2] flag) returns (uint) {
return m_pairsOfFlags.push(flag);
}
function createMemoryArray(uint size) {
// Dynamic memory arrays are created using `new`:
bool[2][] memory arrayOfPairs = new bool[2][](size);
m_pairsOfFlags = arrayOfPairs;
}
}

更多请查看这里的重新梳理: http://me.tryblockchain.org/solidity-array.html

参考资料

变量声明时定义的数据位置属性。可以是memory的,也可以为storage的,详见: http://me.tryblockchain.org/data-location.org

函数的可见性说明,详见这里: http://me.tryblockchain.org/solidity-function-advanced1.html

了解更多关于ABI的信息, https://github.com/ethereum/wiki/wiki/Ethereum-Contract-ABI#types

因为字节数组在使用时会padding,所以不节省空间。 https://github.com/chriseth/solidity/blob/48588b5c4f21c8bb7c5d4319b05a93ee995b457d/_docs/faq_basic.md#what-is-the-difference-between-bytes-and-byte

http://stackoverflow.com/questions/33839122/in-ethereum-solidity-when-changing-an-array-length-i-get-value-must-be-an-lva

结构体(struct)

Solidity提供 struct来定义自定义类型。我们来看看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
pragma solidity ^0.4.0;
contract CrowdFunding{
struct Funder{
address addr;
uint amount;
}
struct Campaign{
address beneficiary;
uint goal;
uint amount;
uint funderNum;
mapping(uint => Funder) funders;
}
uint compaingnID;
mapping (uint => Campaign) campaigns;
function candidate(address beneficiary, uint goal) returns (uint compaingnID){
// initialize
campaigns[compaingnID++] = Campaign(beneficiary, goal, 0, 0);
}
function vote(uint compaingnID) payable {
Campaign c = campaigns[compaingnID];
//another way to initialize
c.funders[c.funderNum++] = Funder({addr: msg.sender, amount: msg.value});
c.amount += msg.value;
}
function check(uint comapingnId) returns (bool){
Campaign c = campaigns[comapingnId];
if(c.amount < c.goal){
return false;
}
uint amount = c.amount;
// incase send much more
c.amount = 0;
if(!c.beneficiary.send(amount)){
throw;
}
return true;
}
}

上面的代码向我们展示的一个简化版的众筹项目,其实包含了一些 struct的使用。 struct可以用于映射和数组中作为元素。其本身也可以包含映射和数组等类型。

我们不能声明一个 struct同时将这个 struct作为这个struct的一个成员。这个限制是基于结构体的大小必须是有限的。

虽然数据结构能作为一个 mapping的值,但数据类型不能包含它自身类型的成员,因为数据结构的大小必须是有限的。

需要注意的是在函数中,将一个 struct赋值给一个局部变量(默认是storage类型),实际是拷贝的引用,所以修改局部变量值时,会影响到原变量。

当然,你也可以直接通过访问成员修改值,而不用一定赋值给一个局部变量,如 campaigns[comapingnId].amount = 0

映射/字典(mappings)

映射或字典类型,一种键值对的映射关系存储结构。定义方式为 mapping(_KeyType => _KeyValue)。键的类型允许除 映射外的所有类型,如数组,合约,枚举,结构体。值的类型无限制。

映射可以被视作为一个哈希表,其中所有可能的键已被虚拟化的创建,被映射到一个默认值(二进制表示的零)。但在映射表中,我们并不存储键的数据,仅仅存储它的 keccak256哈希值,用来查找值时使用。

因此, 映射并没有长度,键集合(或列表),值集合(或列表)这样的概念。

映射类型,仅能用来定义 状态变量,或者是在内部函数中作为 storage类型的引用。引用是指你可以声明一个,如 var storage mappVal的用于存储状态变量的引用的对象,但你没办法使用非状态变量来初始化这个引用。

可以通过将 映射标记为 public,来让Solidity创建一个访问器。要想访问这样的 映射,需要提供一个键值做为参数。如果 映射的值类型也是 映射,使用访问器访问时,要提供这个 映射值所对应的键,不断重复这个过程。下面来看一个例子:

1
2
3
4
5
6
7
contract MappingExample{
mapping(address => uint) public balances;
function update(uint amount) returns (address addr){
balances[msg.sender] = amount;
return msg.sender;
}
}

由于调试时,你不一定方便知道自己的发起地址,所以把这个函数,略微调整了一下,以在调用时,返回调用者的地址。编译上述合同后,可以先调用 update(),执行成功后,查看调用信息,能看到你更新的地址,这样再查一下这个地址的在映射里存的值。

如果你想通过合约进行上述调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
pragma solidity ^0.4.0;
//file indeed for compile
//may store in somewhere and import
contract MappingExample{
mapping(address => uint) public balances;
function update(uint amount) returns (address addr){
balances[msg.sender] = amount;
return msg.sender;
}
}
contract MappingUser{
address conAddr;
address userAddr;
function f() returns (uint amount){
//address not resolved!
//tringing
conAddr = hex"0xf2bd5de8b57ebfc45dcee97524a7a08fccc80aef";
userAddr = hex"0xca35b7d915458ef540ade6068dfe2f44e8fa733c";
return MappingExample(conAddr).balances(userAddr);
}
}

映射并未提供迭代输出的方法,可以自行实现一个数据结构。参见 iterable mapping

左值的相关运算符

左值,是指位于表达式左边的变量,可以是与操作符直接结合的形成的,如自增,自减;也可以是赋值,位运算。

可以支持操作符有: -=, +=, *=, %=, |=, &=, ^=, ++, --

特殊的运算符delete

delete运算符,用于将某个变量重置为初始值。对于整数,运算符的效果等同于 a = 0。而对于定长数组,则是把数组中的每个元素置为初始值,变长数组则是将长度置为0。对于结构体,也是类似,是将所有的成员均重置为初始值。

delete对于映射类型几乎无影响,因为键可能是任意的,且往往不可知。所以如果你删除一个结构体,它会递归删除所有非mapping的成员。当然,你是可以单独删除映射里的某个键,以及这个键映射的某个值。

需要强调的是 delete a的行为更像赋值,为 a赋予一个新对象。我们来看看下文的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pragma solidity ^0.4.0;
contract DeleteExample {
uint data;
uint[] dataArray;
function f() {
//值传递
uint x = data;
//删除x不会影响data
delete x;
//删除data,同样也不会影响x,因为是值传递,它存的是一份原值的拷贝。
delete data;
//引用赋值
uint[] y = dataArray;
//删除dataArray会影响y,y也将被赋值为初值。
delete dataArray;
//下面的操作为报错,因为删除是一个赋值操作,不能向引用类型的storage直接赋值从而报错
//delete y;
}
}

通过上面的代码,我们可以看出,对于值类型,是值传递,删除 x不会影响到 data,同样的删除 data也不会影响到 x。因为他们都存了一份原值的拷贝。

而对于复杂类型略有不同,复杂类型在赋值时使用的是引用传递。删除会影响所有相关变量。比如上述代码中,删除 dataArray同样会影响到 y

由于 delete的行为更像是赋值操作,所以不能在上述代码中执行 delete y,因为不能对一个storage的引用赋值

参考资料

http://solidity.readthedocs.io/en/develop/types.html#operators-involving-lvalues

strorage的引用不能赋值原因,可以看看关于数据位置的这篇文章: http://me.tryblockchain.org/solidity-data-location.html。

基本类型间的转换

语言中经常会出现类型转换。如将一个数字字符串转为整型,或浮点数。这种转换常常分为,隐式转换和显式转换。

隐式转换

如果运算符支持两边不同的类型,编译器会尝试隐式转换类型,同理,赋值时也是类似。通常,隐式转换需要能保证不会丢失数据,且语义可通。如 uint8可以转化为 uint16, uint256。但 int8不能转为 uint256,因为 uint256不能表示 -1

此外,任何无符号整数,可以转换为相同或更大大小的字节值。比如,任何可以转换为 uint160的,也可以转换为 address

显式转换

如果编译器不允许隐式的自动转换,但你知道转换没有问题时,可以进行强转。需要注意的是,不正确的转换会带来错误,所以你要进行谨慎的测试。

1
2
3
4
5
6
7
8
9
pragma solidity ^0.4.0;
contract DeleteExample{
uint a;
function f() returns (uint){
int8 y = -3;
uint x = uint(y);
return x;
}
}

如果转换为一个更小的类型,高位将被截断。

1
2
uint32 a = 0x12345678;
uint16 b = uint16(a); // b will be 0x5678 now

参考资料

http://solidity.readthedocs.io/en/develop/types.html#conversions-between-elementary-types

类型推断(Type Deduction)

为了方便,并不总是需要明确指定一个变量的类型,编译器会通过第一个向这个对象赋予的值的类型来进行推断。

1
2
uint24 x = 0x123;
var y = x;

函数的参数,包括返回参数,不可以使用 var这种不指定类型的方式。

需要特别注意的是,由于类型推断是根据第一个变量进行的赋值。所以代码 for (var i = 0; i < 2000; i++) {}将是一个无限循环,因为一个 uint8i的将小于 2000

1
2
3
4
5
6
7
8
9
10
11
12
13
pragma solidity ^0.4.4;
contract Test{
function a() returns (uint){
uint count = 0;
for (var i = 0; i < 2000; i++) {
count++;
if(count >= 2100){
break;
}
}
return count;
}
}

参考资料

http://solidity.readthedocs.io/en/develop/types.html#type-deduction

货币单位(Ether Units)

一个字面量的数字,可以使用后缀 wei, finney, szaboether来在不同面额中转换。不含任何后缀的默认单位是 wei。如 2 ether == 2000 finney的结果是 true

1
2
3
4
5
6
7
8
9
10
pragma solidity ^0.4.0;
contract EthUnit{
uint a;
function f() returns (bool){
if (2 ether == 2000 finney){
return true;
}
return false;
}
}

时间单位(Time Units)

seconds, minutes, hours, days, weeks, years均可做为后缀,并进行相互转换,默认是 seconds为单位。

默认规则如下:

  • 1 == 1 seconds
  • 1 minutes == 60 seconds
  • 1 hours == 60 minutes
  • 1 days == 24 hours
  • 1 weeks = 7 days
  • 1 years = 365 days

如果你需要进行使用这些单位进行日期计算,需要特别小心,因为不是每年都是365天,且并不是每天都有24小时,因为还有闰秒。由于无法预测闰秒,必须由外部的oracle来更新从而得到一个精确的日历库(内部实现一个日期库也是消耗gas的)。

后缀不能用于变量。如果你想对输入的变量说明其不同的单位,可以使用下面的方式。

1
2
3
4
5
6
7
8
9
10
pragma solidity ^0.4.0;
contract DeleteExample{
function nowInSeconds() returns (uint256){
return now;
}
function f(uint start, uint daysAfter) {
if (now >= start + daysAfter * 1 days) {
}
}
}

特殊变量及函数(Special Variables and Functions)

有一些变量和函数存在于全局上下文中。主要用来提供一些区块链当前的信息。

区块和交易的属性(Block And Transaction Properties)

  • block.blockhash(uint blockNumber) returns (bytes32),给定区块号的哈希值,只支持最近256个区块,且不包含当前区块。
  • block.coinbase (address) 当前块矿工的地址。
  • block.difficulty (uint)当前块的难度。
  • block.gaslimit (uint)当前块的 gaslimit
  • block.number (uint)当前区块的块号。
  • block.timestamp (uint)当前块的时间戳。
  • msg.data (bytes)完整的调用数据(calldata)。
  • msg.gas (uint)当前还剩的 gas
  • msg.sender (address)当前调用发起人的地址。
  • msg.sig (bytes4)调用数据的前四个字节(函数标识符)。
  • msg.value (uint)这个消息所附带的货币量,单位为 wei
  • now (uint)当前块的时间戳,等同于 block.timestamp
  • tx.gasprice (uint) 交易的 gas价格。
  • tx.origin (address)交易的发送者(完整的调用链)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
pragma solidity ^0.4.0;
contract SpecialVariables{
function getAll() returns (bytes32, address, uint, uint, uint, uint, bytes, uint, address, bytes4, uint, uint, uint, address){
return (
block.blockhash(8),
block.coinbase,
block.difficulty,
block.gaslimit,
block.number,
block.timestamp,
msg.data,
msg.gas,
msg.sender,
msg.sig,
msg.value,
now,
tx.gasprice,
tx.origin);
}
}

msg的所有成员值,如 msg.sender, msg.value的值可以因为每一次外部函数调用,或库函数调用发生变化(因为 msg就是和调用相关的全局变量)。

如果你想在库函数中,用 msg.sender实现访问控制,你需要将 msg.sender做为参数(就是说不能使用默认的 msg.value,因为它可能被更改)。

为了可扩展性的原因,你只能查最近256个块,所有其它的将返回0.

数学和加密函数(Mathematical and Cryptographic Functions)

asser(bool condition): 如果条件不满足,抛出异常。

addmod(uint x, uint y, uint k) returns (uint): 计算 (x + y) % k。加法支持任意的精度。但不超过(wrap around?) 2**256

mulmod(uint x, uint y, uint k) returns (uint): 计算 (x * y) % k。乘法支持任意精度,但不超过(wrap around?) 2**256

keccak256(...) returns (bytes32): 使用以太坊的(Keccak-256)计算HASH值。紧密打包。

sha3(...) returns (bytes32): 等同于 keccak256()。紧密打包。

sha256(...) returns (bytes32): 使用SHA-256计算HASH值。紧密打包。

ripemd160(...) returns (bytes20): 使用RIPEMD-160计算HASH值。紧密打包。

ecrecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) returns (address): 通过签名信息恢复非对称加密算法公匙地址。如果出错会返回0,附录提供了一个例子.

revert(): 取消执行,并回撤状态变化。

需要注意的是参数是”紧密打包(tightly packed)”的,意思是说参数不会补位,就直接连接在一起的。下面来看一个例子:

1
2
3
4
5
6
7
keccak256("ab", "c")
keccak256("abc")
//hex
keccak256(0x616263)
keccak256(6382179)
//ascii
keccak256(97, 98, 99)

上述例子中,三种表达方式都是一致的。

如果需要补位,需要明确的类型转换,如 keccak256("\x00\x12")等同于 keccak256(uint16(0x12))

需要注意的是字面量会用,尽可能小的空间来存储它们。比如, keccak256(0) == keccak256(uint8(0))keccak256(0x12345678) == keccak256(uint32(0x12345678))

私链(private blockchain)上运行 sha256, ripemd160ecrecover可能会出现 Out-Of-Gas报错。因为它们实现了一种预编译的机制,但合约要在收到第一个消息后才会存在。向一个不存在的合约发送消息,非常昂贵,所以才会导致 Out-Of-Gas的问题。一种解决办法是每个在你真正使用它们前,先发送 1 wei到这些合约上来完成初始化。在官方和测试链上没有这个问题。

参考资料

使用ecrecover实现签名检验的例子。 http://me.tryblockchain.org/web3js-sign-ecrecover-decode.html

地址相关(Address Related)

1
2
3
4
5
6
<address>.balance (uint256):`Address`的余额,以 `wei`为单位。
<address>.transfer(uint256 amount):发送给定数量的 `ether`,以 `wei`为单位,到某个地址。失败时抛出异常。
<address>.send(uint256 amount) returns (bool):发送给定数量的 `ether`,以 `wei`为单位,到某个地址。失败时返回 `false`。
<address>.call(...) returns (bool):发起底层的 `call`调用。失败时返回 `false`。
<address>.callcode(...) returns (bool):发起底层的 `callcode`调用,失败时返回 `false`。
<address>.delegatecall(...) returns (bool):发起底层的 `delegatecall`调用,失败时返回 `false`。

更多信息参加Address章节。

使用 send方法需要注意,调用栈深不能超过1024,或gas不足,都将导致发送失败。使用为了保证你的 ether安全,要始终检查返回结果。当用户取款时,使用 transfer或使用最佳实践的模式。

合约相关

this(当前合约的类型):当前合约的类型,可以显式的转换为 Address

selfdestruct(address recipt):销毁当前合约,并把它所有资金发送到给定的地址。

另外,当前合约里的所有函数均可支持调用,包括当前函数本身。

参考资料

更新关于地址的call,callcode,delegateCall的介绍。 http://me.tryblockchain.org/Solidity-call-callcode-delegatecall.html

http://solidity.readthedocs.io/en/develop/common-patterns.html#withdrawal-from-contracts

入参和出参(Input Parameters and Output Parameters)

javascript一样,函数有输入参数,但与之不同的是,函数可能有任意数量的返回参数。

入参(Input Parameters)

入参(Input Parameter)与变量的定义方式一致,稍微不同的是,不会用到的参数可以省略变量名称。一种可接受两个整型参数的函数如下:

1
2
3
4
5
6
pragma solidity ^0.4.0;
contract Simple {
function taker(uint _a, uint) {
// do something with _a.
}
}

出参(Output Parameters)

出参(Output Paramets)returns关键字后定义,语法类似变量的定义方式。下面的例子展示的是,返回两个输入参数的求和,乘积的实现:

1
2
3
4
5
6
7
8
pragma solidity ^0.4.0;
contract Simple {
//return sum and product
function arithmetics(uint _a, uint _b) returns (uint o_sum, uint o_product) {
o_sum = _a + _b;
o_product = _a * _b;
}
}

出参的的名字可以省略。返回的值,同样可以通过 return关键字来指定。 return也可以同时返回多个值,参见 Returning Multiple Values。出参的默认值为0,如果没有明确被修改,它将一直是0。

入参和出参也可在函数体内用做表达式。它们也可被赋值。

返回多个值(Returning Multiple Values)

当返回多个参数时,使用 return (v0, v1, ..., vn)。返回结果的数量需要与定义的一致。

控制结构

不支持 switchgoto,支持 ifelsewhiledoforbreakcontinuereturn?:

条件判断中的括号不可省略,但在单行语句中的大括号可以省略。

需要注意的是,这里没有像C语言,和 javascript里的非Boolean类型的自动转换,比如 if(1){...}在Solidity中是无效的。

函数调用(Function Calls)

内部函数调用(Internal Function Calls)

在当前的合约中,函数可以直接调用(内部调用方式),包括也可递归调用,来看一个简单的示例:

1
2
3
4
contract C {
function g(uint a) returns (uint ret) { return f(); }
function f() returns (uint ret) { return g(7) + f(); }
}

这些函数调用在EVM中被翻译成简单的跳转指令。这样带来的一个好处是,当前的内存不会被回收。所以在一个内部调用时传递一个内存型引用效率将非常高。当然,仅仅是同一个合约的函数之间才可通过内部的方式进行调用。

外部函数调用(External Function Calls)

表达式 this.g(8);c.g(2)(这里的 c是一个合约实例)是外部调用函数的方式。实现上是通过一个消息调用,而不是直接通过EVM的指令跳转。需要注意的是,在合约的构造器中,不能使用 this调用函数,因为当前合约还没有创建完成。

其它合约的函数必须通过外部的方式调用。对于一个外部调用,所有函数的参数必须要拷贝到内存中。

当调用其它合约的函数时,可以通过选项 .value(),和 .gas()来分别指定,要发送的 ether量(以 wei为单位),和 gas值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pragma solidity ^0.4.0;
contract InfoFeed {
function info() payable returns (uint ret) {
return msg.value;
}
}
contract Consumer {
function deposit() payable returns (uint){
return msg.value;
}
function left() constant returns (uint){
return this.balance;
}
function callFeed(address addr) returns (uint) {
return InfoFeed(addr).info.value(1).gas(8000)();
}
}

上面的代码中,我们首先调用 deposit()Consumer合约存入一定量的 ether。然后调用 callFeed()通过 value(1)的方式,向 InfoFeed合约的 info()函数发送1 ether。需要注意的是,如果不先充值,由于合约余额为0,余额不足会报错 Invalid opcode

InfoFeed.info()函数,必须使用 payable关键字,否则不能通过 value()选项来接收 ether

代码 InfoFeed(addr)进行了一个显示的类型转换,声明了我们确定知道给定的地址是 InfoFeed类型。所以这里并不会执行构造器的初始化。显示的类型强制转换,需要极度小心,不要尝试调用一个你不知道类型的合约。

我们也可以使用 function setFeed(InfoFeed _feed) { feed = _feed; }来直接进行赋值。 .info.value(1).gas(8000)只是本地设置发送的数额和gas值,真正执行调用的是其后的括号 .info.value(1).gas(8000)()

如果被调用的合约不存在,或者是不包代码的帐户,或调用的合约产生了异常,或者gas不足,均会造成函数调用发生异常。

如果被调用的合约源码并不事前知道,和它们交互会有潜在的风险。当前合约会将自己的控制权交给被调用的合约,而对方几乎可以做任何事。即使被调用的合约是继承自一个已知的父合约,但继承的子合约仅仅被要求正确实现了接口。合约的实现,可以是任意的内容,由此会有风险。另外,准备好处理调用你自己系统中的其它合约,可能在第一调用结果未返回之前就返回了调用的合约。某种程度上意味着,被调用的合约可以改变调用合约的 状态变量(state variable)来标记当前的状态。如,写一个函数,只有当 状态变量(state variables)的值有对应的改变时,才调用外部函数,这样你的合约就不会有可重入性漏洞。

命名参数调用和匿名函数参数(Named Calls and Anonymous Function Paramters)

函数调用的参数,可以通过指定名字的方式调用,但可以以任意的顺序,使用方式是 {}包含。但参数的类型和数量要与定义一致。

1
2
3
4
5
6
7
8
pragma solidity ^0.4.0;
contract C {
function add(uint val1, uint val2) returns (uint) { return val1 + val2; }
function g() returns (uint){
// named arguments
return add({val2: 2, val1: 1});
}
}

省略函数名称(Omitted Function Parameter Names)

没有使用的参数名可以省略(一般常见于返回值)。这些名字在栈(stack)上存在,但不可访问。

1
2
3
4
5
6
7
pragma solidity ^0.4.0;
contract C {
// omitted name for parameter
function func(uint k, uint) returns(uint) {
return k;
}
}

参考资料

如何使用 Remix向合约发送ether参见这里。 http://me.tryblockchain.org/%E6%94%AF%E4%BB%98%E7%9B%B8%E5%85%B3.html

创建合约实例(Creating Contracts via new)

一个合约可以通过 new关键字来创建一个合约。要创建合约的完整代码,必须提前知道,所以递归创建依赖是不可能的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pragma solidity ^0.4.0;
contract Account{
uint accId;
//construction?
function Account(uint accountId) payable{
accId = accountId;
}
}
contract Initialize{
Account account = new Account(10);
function newAccount(uint accountId){
account = new Account(accountId);
}
function newAccountWithEther(uint accountId, uint amount){
account = (new Account).value(amount)(accountId);
}
}

从上面的例子可以看出来,可以在创建合约中,发送 ether,但不能限制gas的使用。如果创建因为 out-of-stack,或无足够的余额以及其它任何问题,会抛出一个异常。

表达式的执行顺序(Order of Evaluation of Expressions)

表达式的求值顺序并不是确定的,更正式的说法是,表达式树一个节点的某个节点在求值时的顺序是不确定的,但是它肯定会比节点本身先执行。我们仅仅保证语句(statements)按顺序执行和布尔表达式的短路运算的支持。查看 运算符的执行顺序了解更多。

赋值(Assignment)

解构赋值和返回多个结果(Destructing Assignments and Returning Multip Values)

Solidity内置支持 元组(tuple),也就是说支持一个可能的完全不同类型组成的一个列表,数量上是固定的(Tuple一般指两个,还有个Triple一般指三个)。

这种内置结构可以同时返回多个结果,也可用于同时赋值给多个变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
pragma solidity ^0.4.0;
contract C {
uint[] data;
function f() returns (uint, bool, uint) {
return (7, true, 2);
}
function g() {
// Declares and assigns the variables. Specifying the type explicitly is not possible.
var (x, b, y) = f();
// Assigns to a pre-existing variable.
(x, y) = (2, 7);
// Common trick to swap values -- does not work for non-value storage types.
(x, y) = (y, x);
// Components can be left out (also for variable declarations).
// If the tuple ends in an empty component,
// the rest of the values are discarded.
(data.length,) = f(); // Sets the length to 7
// The same can be done on the left side.
(,data[3]) = f(); // Sets data[3] to 2
// Components can only be left out at the left-hand-side of assignments, with
// one exception:
(x,) = (1,);
// (1,) is the only way to specify a 1-component tuple, because (1) is
// equivalent to 1.
}
}

数组和自定义结构体的复杂性(Complication for Arrays and Struts)

对于非值类型,比如数组和数组,赋值的语法有一些复杂。

  • 赋值给一个状态变量总是创建一个完全无关的拷贝。
  • 赋值给一个局部变量,仅对基本类型,如那些32字节以内的 静态类型(static types),创建一份完全无关拷贝。
  • 如果是数据结构或者数组(包括 bytesstring)类型,由状态变量赋值为一个局部变量,局部变量只是持有原始状态变量的一个引用。对这个局部变量再次赋值,并不会修改这个状态变量,只是修改了引用。但修改这个本地引用变量的成员值,会改变状态变量的值。

作用范围和声明(Scoping And Decarations)

一个变量在声明后都有初始值为字节表示的全0值。也就是所有类型的默认值是典型的 零态(zero-state)。举例来说,默认的 bool的值为 false, uintint的默认值为 0

对从 byte1byte32定长的字节数组,每个元素都被初始化为对应类型的初始值(一个字节的是一个字节长的全0值,多个字节长的是多个字节长的全零值)。对于变长的数组 bytesstring,默认值则为空数组和空字符串。

函数内定义的变量,在整个函数中均可用,无论它在哪里定义。因为Solidity使用了 javascript的变量作用范围的规则。与常规语言语法从定义处开始,到当前块结束为止不同。由此,下述代码编译时会抛出一个异常, Identifier already declared

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
pragma solidity ^0.4.0;
contract ScopingErrors {
function scoping() {
uint i = 0;
while (i++ < 1) {
uint same1 = 0;
}
while (i++ < 2) {
uint same1 = 0;// Illegal, second declaration of same1
}
}
function minimalScoping() {
{
uint same2 = 0;
}
{
uint same2 = 0;// Illegal, second declaration of same2
}
}
function forLoopScoping() {
for (uint same3 = 0; same3 < 1; same3++) {
}
for (uint same3 = 0; same3 < 1; same3++) {// Illegal, second declaration of same3
}
}
function crossFunction(){
uint same1 = 0;//Illegal
}
}

另外的,如果一个变量被声明了,它会在函数开始前被初始化为默认值。所以下述例子是合法的。

1
2
3
4
5
6
7
8
9
10
11
12
13
pragma solidity ^ 0.4.0;
contract C{
function foo() returns(uint) {
// baz is implicitly initialized as 0
uint bar = 5;
if (true) {
bar += baz;
} else {
uint baz = 10;// never executes
}
return bar;// returns 5
}
}

异常(Excepions)

有一些情况下,异常是自动抛出来的(见下),你也可以使用 throw来手动抛出一个异常。抛出异常的效果是当前的执行被终止且被撤销(值的改变和帐户余额的变化都会被回退)。异常还会通过Solidity的函数调用向上冒泡(bubbled up)传递。( send,和底层的函数调用 call, delegatecallcallcode是一个例外,当发生异常时,这些函数返回 false)。

捕捉异常是不可能的(或许因为异常时,需要强制回退的机制)。

在下面的例子中,我们将如展示如何使用 throw来回退转帐,以及演示如何检查 send的返回值。

1
2
3
4
5
6
7
8
pragma solidity ^0.4.0;
contract Sharer {
function sendHalf(address addr) payable returns (uint balance) {
if (!addr.send(msg.value / 2))
throw; // also reverts the transfer to Sharer
return this.balance;
}
}

当前,Solidity在下述场景中自动产生运行时异常。

  • 如果越界,或是负的序号值访问数组。
  • 如果访问一个定长的 bytesN,序号越界,或是负的序号值。
  • 如果你通过消息调用一个函数,但在调用的过程中,并没有正确结束(gas不足,没有匹配到对应的函数,或他自己出现异常)。底层操作如 call, send, delegatecallcallcode除外,它们不会抛出异常,但它们会通过返回 false来表示失败。
  • 如果在使用 new创建一个新合约时,但合约的初化化由于类似3中的原因没有正常完成。
  • 被除数为0。
  • 对一个二进制移动一个负的值。
  • 使用枚举时,将过大值,负值转为枚举类型。
  • 使用外部函数调用时,被调用的对象并不包含代码。
  • 如果你的 public的函数在没有 payable关键字时,却尝试在接收 ether(包括构造函数,和回退函数)。
  • 合约通过一个 publicgetter函数(public getter funciton)接收 ether
  • 调用一个未初始化的内部函数。
  • .transfer()执行失败
  • assert返回 false

当一个用户通过下述方式触发一个异常:

  • 调用 throw
  • 调用 require,但参数值为false。

当上述情况发生时,在Solidity会执行一个回退操作(指令 0xfd)。与之相对的是,如果发生运行时异常,或 assert失败时,将执行无效操作(指令 0xfe)。在上述的情况下,由此促使EVM撤回所有的状态改变。这样做的原因是,没有办法继续安全执行了,因为想要发生的事件并未发生。因为我们想保持交易的原子性(一致性),所以撤销所有操作,让整个交易没有任何影响。

通过 assert判断内部条件是否达成, require验证输入的有效性。这样的分析工具,可以假设正确的输入,减少错误。这样无效的操作码将永远不会出现。

内联汇编(Inline Assembly)

为了增强对语言的细粒度的控制,特别是在写通用库时,可以在一个语言中交错使用Solidity的语句来接近其中一个虚拟机。但由于EVM是基于栈执行的,所以有时很难定位到正确的栈槽位,从而提供正确的的参数或操作码。Solidit的内联汇编尝试解决这个问题,但也引入了其它的问题,当你通过下述特性进行手动的汇编时:

  • 函数式的操作码: mul(1, add(2, 3))代替 push1 3 push1 2 add push1 1 mul
  • 本地汇编变量: let x := add(2, 3) let y := mload(0x40) x := add(x, y)
  • 访问外部变量: function f(uint x){ assembly { x := sub(x,1)}}
  • 标签支持: let x := 10 repeat := sub(x, 1) jumpi(repeat, eq(x, 0))

Solidity Assembly是对内联汇编的详细介绍。

合约

Solidity中合约有点类似面向对象语言中的类。合约中有用于数据持久化的 状态变量(state variables),和可以操作他们的函数。调用另一个合约实例的函数时,会执行一个EVM函数调用,这个操作会切换执行时的上下文,这样,前一个合约的 状态变量(state variables)就不能访问了。

创建合约

合约可以通过Solidity,或不通过Solidity创建。当合约创建时,一个和合约同名的函数(构造器函数)会调用一次,用于初始化。

构造器函数是可选的。仅能有一个构造器,所以不支持重载。

如果不通过Solidity,我们可以通过 web3.js,使用JavaScript的API来完成合约创建:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Need to specify some source including contract name for the data param below
var source = "contract CONTRACT_NAME { function CONTRACT_NAME(unit a, uint b) {} }";
// The json abi array generated by the compiler
var abiArray = [
{
"inputs":[
{"name":"x","type":"uint256"},
{"name":"y","type":"uint256"}
],
"type":"constructor"
},
{
"constant":true,
"inputs":[],
"name":"x",
"outputs":[{"name":"","type":"bytes32"}],
"type":"function"
}
];
var MyContract_ = web3.eth.contract(source);
MyContract = web3.eth.contract(MyContract_.CONTRACT_NAME.info.abiDefinition);
// deploy new contract
var contractInstance = MyContract.new(
10,
11,
{from: myAccount, gas: 1000000}
);

具体内部实现里,构造器的参数是紧跟在合约代码的后面,但如果你使用 web3.js,可以不用关心这样的细节。

如果一个合约要创建另一个合约,它必须要知道源码。这意味着循环创建依赖是不可能的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
pragma solidity ^0.4.0;
contract OwnedToken {
// TokenCreator is a contract type that is defined below.
// It is fine to reference it as long as it is not used
// to create a new contract.
TokenCreator creator;
address owner;
bytes32 name;
// This is the constructor which registers the
// creator and the assigned name.
function OwnedToken(bytes32 _name) {
// State variables are accessed via their name
// and not via e.g. this.owner. This also applies
// to functions and especially in the constructors,
// you can only call them like that ("internall"),
// because the contract itself does not exist yet.
owner = msg.sender;
// We do an explicit type conversion from `address`
// to `TokenCreator` and assume that the type of
// the calling contract is TokenCreator, there is
// no real way to check that.
creator = TokenCreator(msg.sender);
name = _name;
}
function changeName(bytes32 newName) {
// Only the creator can alter the name --
// the comparison is possible since contracts
// are implicitly convertible to addresses.
if (msg.sender == address(creator))
name = newName;
}
function transfer(address newOwner) {
// Only the current owner can transfer the token.
if (msg.sender != owner) return;
// We also want to ask the creator if the transfer
// is fine. Note that this calls a function of the
// contract defined below. If the call fails (e.g.
// due to out-of-gas), the execution here stops
// immediately.
if (creator.isTokenTransferOK(owner, newOwner))
owner = newOwner;
}
}
contract TokenCreator {
function createToken(bytes32 name)
returns (OwnedToken tokenAddress)
{
// Create a new Token contract and return its address.
// From the JavaScript side, the return type is simply
// "address", as this is the closest type available in
// the ABI.
return new OwnedToken(name);
}
function changeName(OwnedToken tokenAddress, bytes32 name) {
// Again, the external type of "tokenAddress" is
// simply "address".
tokenAddress.changeName(name);
}
function isTokenTransferOK(
address currentOwner,
address newOwner
) returns (bool ok) {
// Check some arbitrary condition.
address tokenAddress = msg.sender;
return (keccak256(newOwner) & 0xff) == (bytes20(tokenAddress) & 0xff);
}
}

可见性或权限控制(Visibility And Accessors)

Solidity有两种函数调用方式,一种是内部调用,不会创建一个EVM调用(也叫做消息调用),另一种则是外部调用,会创建EVM调用(会发起消息调用)。Solidity对函数和状态变量提供了四种可见性。分别是 external, public, internal, private其中函数默认是 public。状态变量默认的可见性是 internal

可见性

external: 外部函数是合约接口的一部分,所以我们可以从其它合约或通过交易来发起调用。一个外部函数 f,不能通过内部的方式来发起调用,(如 f()不可以,但可以通过 this.f())。外部函数在接收大的数组数据时更加有效。

public: 公开函数是合约接口的一部分,可以通过内部,或者消息来进行调用。对于 public类型的状态变量,会自动创建一个访问器(详见下文)。

internal:这样声明的函数和状态变量只能通过内部访问。如在当前合约中调用,或继承的合约里调用。需要注意的是不能加前缀 this,前缀 this是表示通过外部方式访问。

private:私有函数和状态变量仅在当前合约中可以访问,在继承的合约内,不可访问。

备注:所有在合约内的东西对外部的观察者来说都是可见,将某些东西标记为 private仅仅阻止了其它合约来进行访问和修改,但并不能阻止其它人看到相关的信息。

可见性的标识符的定义位置,对于 state variable是在类型后面,函数是在参数列表和返回关键字中间。来看一个定义的例子:

1
2
3
4
5
6
pragma solidity ^0.4.0;
contract C {
function f(uint a) private returns (uint b) { return a + 1; }
function setData(uint a) internal { data = a; }
uint public data;
}

在下面的例子中, D可以调用 c.getData()来访问 data的值,但不能调用 f。合约 E继承自 C,所以它可以访问 compute函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
pragma solidity ^0.4.0;
contract C {
uint private data;
function f(uint a) private returns(uint b) { return a + 1; }
function setData(uint a) { data = a; }
function getData() public returns(uint) { return data; }
function compute(uint a, uint b) internal returns (uint) { return a+b; }
}
contract D {
function readData() {
C c = new C();
uint local = c.f(7); // error: member "f" is not visible
c.setData(3);
local = c.getData();
local = c.compute(3, 5); // error: member "compute" is not visible
}
}
contract E is C {
function g() {
C c = new C();
uint val = compute(3, 5); // acces to internal member (from derivated to parent contract)
}
}

访问函数(Getter Functions)

编译器为自动为所有的 public的状态变量创建访问函数。下面的合约例子中,编译器会生成一个名叫 data的无参,返回值是 uint的类型的值 data。状态变量的初始化可以在定义时完成。

1
2
3
4
5
6
7
8
9
10
pragma solidity ^0.4.0;
contract C{
uint public c = 10;
}
contract D{
C c = new C();
function getDataUsingAccessor() returns (uint){
return c.c();
}
}

访问函数有外部(external)可见性。如果通过内部(internal)的方式访问,比如直接访问,你可以直接把它当一个变量进行使用,但如果使用外部(external)的方式来访问,如通过 this.,那么它必须通过函数的方式来调用。

1
2
3
4
5
6
7
8
9
10
pragma solidity ^0.4.0;
contract C{
uint public c = 10;
function accessInternal() returns (uint){
return c;
}
function accessExternal() returns (uint){
return this.c();
}
}

acessExternal函数中,如果直接返回 return this.c;,会出现报错 Return argument type function () constant external returns (uint256) is not implicitly convertible to expected type (type of first return variable) uint256.。原因应该是通过外部(external)的方式只能访问到 this.c作为函数的对象,所以它认为你是想把一个函数转为 uint故而报错。

下面是一个更加复杂的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
pragma solidity ^0.4.0;
contract ComplexSimple{
struct Cat{
uint a;
bytes3 b;
mapping(uint => uint) map;
}
//
mapping(uint => mapping(bool => Cat)) public content;
function initial(){
content[0][true] = Cat(1, 1);
content[0][true].map[0] = 10;
}
function get() returns (uint, bytes3, uint){
return (content[0][true].a, content[0][true].b, content[0][true].map[0]);
}
}
contract Complex {
struct Data {
uint a;
bytes3 b;
mapping (uint => uint) map;
}
mapping (uint => mapping(bool => Data[])) public data;
}

文档中自带的的这个Demo始终跑不通,数组类型这里不知为何会抛 invalid jump。把这块简化了写了一个 ComplextSimple供参考。

需要注意的是 publicmapping默认访问参数是需要参数的,并不是之前说的访问函数都是无参的。

mapping类型的数据访问方式变为了 data[arg1][arg2][arg3].a

结构体(struct)里的 mapping初始化被省略了,因为并没有一个很好的方式来对键赋值。

函数修改器(Function Modifiers)

修改器(Modifiers)可以用来轻易的改变一个函数的行为。比如用于在函数执行前检查某种前置条件。修改器是一种合约属性,可被继承,同时还可被派生的合约 重写(override)。下面我们来看一段示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
pragma solidity ^0.4.0;
contract owned {
function owned() { owner = msg.sender; }
address owner;
// This contract only defines a modifier but does not use
// it - it will be used in derived contracts.
// The function body is inserted where the special symbol
// "_;" in the definition of a modifier appears.
// This means that if the owner calls this function, the
// function is executed and otherwise, an exception is
// thrown.
modifier onlyOwner {
if (msg.sender != owner)
throw;
_;
}
}
contract mortal is owned {
// This contract inherits the "onlyOwner"-modifier from
// "owned" and applies it to the "close"-function, which
// causes that calls to "close" only have an effect if
// they are made by the stored owner.
function close() onlyOwner {
selfdestruct(owner);
}
}
contract priced {
// Modifiers can receive arguments:
modifier costs(uint price) {
if (msg.value >= price) {
_;
}
}
}
contract Register is priced, owned {
mapping (address => bool) registeredAddresses;
uint price;
function Register(uint initialPrice) { price = initialPrice; }
// It is important to also provide the
// "payable" keyword here, otherwise the function will
// automatically reject all Ether sent to it.
function register() payable costs(price) {
registeredAddresses[msg.sender] = true;
}
function changePrice(uint _price) onlyOwner {
price = _price;
}
}

修改器可以被继承,使用将 modifier置于参数后,返回值前即可。

特殊 _表示使用修改符的函数体的替换位置。

从合约 Register可以看出全约可以多继承,通过 ,号分隔两个被继承的对象。

修改器也是可以接收参数的,如 pricedcosts

使用修改器实现的一个防重复进入的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pragma solidity ^0.4.0;
contract Mutex {
bool locked;
modifier noReentrancy() {
if (locked) throw;
locked = true;
_;
locked = false;
}
/// This function is protected by a mutex, which means that
/// reentrant calls from within msg.sender.call cannot call f again.
/// The `return 7` statement assigns 7 to the return value but still
/// executes the statement `locked = false` in the modifier.
function f() noReentrancy returns (uint) {
if (!msg.sender.call()) throw;
return 7;
}
}

例子中,由于 call()方法有可能会调回当前方法,修改器实现了防重入的检查。

如果同一个函数有多个修改器,他们之间以空格隔开,修饰器会依次检查执行。

在修改器中和函数体内的显式的 return语句,仅仅跳出当前的修改器和函数体。返回的变量会被赋值,但整个执行逻辑会在前一个修改器后面定义的”_”后继续执行。

修改器的参数可以是任意表达式。在对应的上下文中,所有的函数中引入的符号,在修改器中均可见。但修改器中引入的符号在函数中不可见,因为它们有可能被重写。

常量(constant state variables)

状态变量可以被定义为 constant,常量。这样的话,它必须在编译期间通过一个表达式赋值。赋值的表达式不允许:

1)访问 storage

2)区块链数据,如 nowthis.balanceblock.number

3)合约执行的中间数据,如 msg.gas

4)向外部合约发起调用。也许会造成内存分配副作用表达式是允许的,但不允许产生其它内存对象的副作用的表达式。内置的函数 keccak256keccak256ripemd160ecrecoveraddmodmulmod可以允许调用,即使它们是调用的外部合约。

允许内存分配,从而带来可能的副作用的原因是因为这将允许构建复杂的对象,比如,查找表。虽然当前的特性尚未完整支持。

编译器并不会为常量在 storage上预留空间,每个使用的常量都会被对应的常量表达式所替换(也许优化器会直接替换为常量表达式的结果值)。

不是所有的类型都支持常量,当前支持的仅有值类型和字符串。

1
2
3
4
5
6
pragma solidity ^0.4.0;
contract C {
uint constant x = 32**22 + 8;
string constant text = "abc";
bytes32 constant myHash = keccak256("abc");
}

常函数(Constant Functions)

函数也可被声明为常量,这类函数将承诺自己不修改区块链上任何状态。

1
2
3
4
5
6
pragma solidity ^0.4.0;
contract C {
function f(uint a, uint b) constant returns (uint) {
return a * (b + 42);
}
}

访问器(Accessor)方法默认被标记为 constant。当前编译器并未强制一个 constant的方法不能修改状态。但建议大家对于不会修改数据的标记为 constant

回退函数(fallback function)

每一个合约有且仅有一个没有名字的函数。这个函数无参数,也无返回值。如果调用合约时,没有匹配上任何一个函数(或者没有传哪怕一点数据),就会调用默认的回退函数。

此外,当合约收到 ether时(没有任何其它数据),这个函数也会被执行。在此时,一般仅有少量的gas剩余,用于执行这个函数(准确的说,还剩2300gas)。所以应该尽量保证回退函数使用少的gas。

下述提供给回退函数可执行的操作会比常规的花费得多一点。

  • 写入到存储(storage)
  • 创建一个合约
  • 执行一个外部(external)函数调用,会花费非常多的gas
  • 发送 ether

请在部署合约到网络前,保证透彻的测试你的回退函数,来保证函数执行的花费控制在2300gas以内。

一个没有定义一个回退函数的合约。如果接收ether,会触发异常,并返还ether(solidity v0.4.0开始)。所以合约要接收ether,必须实现回退函数。下面来看个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
pragma solidity ^0.4.0;
contract Test {
// This function is called for all messages sent to
// this contract (there is no other function).
// Sending Ether to this contract will cause an exception,
// because the fallback function does not have the "payable"
// modifier.
function() { x = 1; }
uint x;
}
// This contract keeps all Ether sent to it with no way
// to get it back.
contract Sink {
function() payable { }
}
contract Caller {
function callTest(Test test) {
test.call(0xabcdef01); // hash does not exist
// results in test.x becoming == 1.
// The following call will fail, reject the
// Ether and return false:
test.send(2 ether);
}
}

在浏览器中跑的话,记得要先存ether。

事件(Events)

事件是使用EVM日志内置功能的方便工具,在DAPP的接口中,它可以反过来调用Javascript的监听事件的回调。

事件在合约中可被继承。当被调用时,会触发参数存储到交易的日志中(一种区块链上的特殊数据结构)。这些日志与合约的地址关联,并合并到区块链中,只要区块可以访问就一直存在(至少Frontier,Homestead是这样,但Serenity也许也是这样)。日志和事件在合约内不可直接被访问,即使是创建日志的合约。

日志的SPV(简单支付验证)是可能的,如果一个外部的实体提供了一个这样证明的合约,它可以证明日志在区块链是否存在。但需要留意的是,由于合约中仅能访问最近的256个区块哈希,所以还需要提供区块头信息。

可以最多有三个参数被设置为 indexed,来设置是否被索引。设置为索引后,可以允许通过这个参数来查找日志,甚至可以按特定的值过滤。

如果数组(包括 stringbytes)类型被标记为索引项,会用它对应的 Keccak-256哈希值做为 topic

除非是匿名事件,否则事件签名(比如: Deposit(address,hash256,uint256))是其中一个 topic,同时也意味着对于匿名事件无法通过名字来过滤。

所有未被索引的参数将被做为日志的一部分被保存起来。

被索引的参数将不会保存它们自己,你可以搜索他们的值,但不能检索值本身。

下面是一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pragma solidity ^0.4.0;
contract ClientReceipt {
event Deposit(
address indexed _from,
bytes32 indexed _id,
uint _value
);
function deposit(bytes32 _id) {
// Any call to this function (even deeply nested) can
// be detected from the JavaScript API by filtering
// for `Deposit` to be called.
Deposit(msg.sender, _id, msg.value);
}
}

下述是使用javascript来获取日志的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var abi = /* abi as generated by the compiler */;
var ClientReceipt = web3.eth.contract(abi);
var clientReceipt = ClientReceipt.at(0x123 /* address */);
var event = clientReceipt.Deposit();
// watch for changes
event.watch(function(error, result){
// result will contain various information
// including the argumets given to the Deposit
// call.
if (!error)
console.log(result);
});
// Or pass a callback to start watching immediately
var event = clientReceipt.Deposit(function(error, result) {
if (!error)
console.log(result);
});

底层的日志接口(Low-level Interface to Logs)

通过函数 log0log1log2log3log4,可以直接访问底层的日志组件。 logi表示总共有带 i + 1个参数( i表示的就是可带参数的数目,只是是从0开始计数的)。其中第一个参数会被用来做为日志的数据部分,其它的会做为主题(topics)。前面例子中的事件可改为如下:

1
2
3
4
5
6
log3(
msg.value,
0x50cb9fe53daa9737b786ab3646f04d0150dc50ef4e75f59509d83667ad5adb20,
msg.sender,
_id
);

其中的长16进制串是事件的签名,计算方式是 keccak256("Deposit(address,hash256,uint256)")

更多的理解事件的资源

继承(Inheritance)

Solidity通过复制包括多态的代码来支持多重继承。

所有函数调用是 虚拟(virtual)的,这意味着最远的派生方式会被调用,除非明确指定了合约。

当一个合约从多个其它合约那里继承,在区块链上仅会创建一个合约,在父合约里的代码会复制来形成继承合约。

基本的继承体系与 python有些类似,特别是在处理多继承上面。

下面用一个例子来详细说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
pragma solidity ^0.4.0;
contract owned {
function owned() { owner = msg.sender; }
address owner;
}
// Use "is" to derive from another contract. Derived
// contracts can access all non-private members including
// internal functions and state variables. These cannot be
// accessed externally via `this`, though.
contract mortal is owned {
function kill() {
if (msg.sender == owner) selfdestruct(owner);
}
}
// These abstract contracts are only provided to make the
// interface known to the compiler. Note the function
// without body. If a contract does not implement all
// functions it can only be used as an interface.
contract Config {
function lookup(uint id) returns (address adr);
}
contract NameReg {
function register(bytes32 name);
function unregister();
}
// Multiple inheritance is possible. Note that "owned" is
// also a base class of "mortal", yet there is only a single
// instance of "owned" (as for virtual inheritance in C++).
contract named is owned, mortal {
function named(bytes32 name) {
Config config = Config(0xd5f9d8d94886e70b06e474c3fb14fd43e2f23970);
NameReg(config.lookup(1)).register(name);
}
// Functions can be overridden by another function with the same name and
// the same number/types of inputs. If the overriding function has different
// types of output parameters, that causes an error.
// Both local and message-based function calls take these overrides
// into account.
function kill() {
if (msg.sender == owner) {
Config config = Config(0xd5f9d8d94886e70b06e474c3fb14fd43e2f23970);
NameReg(config.lookup(1)).unregister();
// It is still possible to call a specific
// overridden function.
mortal.kill();
}
}
}
// If a constructor takes an argument, it needs to be
// provided in the header (or modifier-invocation-style at
// the constructor of the derived contract (see below)).
contract PriceFeed is owned, mortal, named("GoldFeed") {
function updateInfo(uint newInfo) {
if (msg.sender == owner) info = newInfo;
}
function get() constant returns(uint r) { return info; }
uint info;
}

上面的例子的 named合约的 kill()方法中,我们调用了 motal.kill()调用父合约的 销毁函数(destruction)。但这样可能什么引发一些小问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pragma solidity ^0.4.0;
contract mortal is owned {
function kill() {
if (msg.sender == owner) selfdestruct(owner);
}
}
contract Base1 is mortal {
function kill() { /* do cleanup 1 */ mortal.kill(); }
}
contract Base2 is mortal {
function kill() { /* do cleanup 2 */ mortal.kill(); }
}
contract Final is Base1, Base2 {
}

Final.kill()的调用只会调用 Base2.kill(),因为派生重写,会跳过 Base1.kill,因为它根本就不知道有 Base1。一个变通方法是使用 super

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pragma solidity ^0.4.0;
contract mortal is owned {
function kill() {
if (msg.sender == owner) selfdestruct(owner);
}
}
contract Base1 is mortal {
function kill() { /* do cleanup 1 */ super.kill(); }
}
contract Base2 is mortal {
function kill() { /* do cleanup 2 */ super.kill(); }
}
contract Final is Base2, Base1 {
}

如果 Base1调用了函数 super,它不会简单的调用基类的合约函数,它还会调用继承关系图谱上的下一个基类合约,所以会调用 Base2.kill()。需要注意的最终的继承图谱将会是:Final,Base1,Base2,mortal,owned。使用super时会调用的实际函数在使用它的类的上下文中是未知的,尽管它的类型是已知的。这类似于普通虚函数查找 (ordinary virtual method lookup)

基类构造器的方法(Arguments for Base Constructors)

派生的合约需要提供所有父合约需要的所有参数,所以用两种方式来做,见下面的例子:

1
2
3
4
5
6
7
8
9
pragma solidity ^0.4.0;
contract Base {
uint x;
function Base(uint _x) { x = _x; }
}
contract Derived is Base(7) {
function Derived(uint _y) Base(_y * _y) {
}
}

或者直接在继承列表中使用 is Base(7),或像 修改器(modifier)使用方式一样,做为派生构造器定义头的一部分 Base(_y * _y)。第一种方式对于构造器是常量的情况比较方便,可以大概说明合约的行为。第二种方式适用于构造的参数值由派生合约的指定的情况。在上述两种都用的情况下,第二种方式优先(一般情况只用其中一种方式就好了)。

多继承与线性化(Multiple Inheritance and Linearization)

实现多继承的编程语言需要解决几个问题,其中之一是 菱形继承问题又称 钻石问题,如下图。

Solidity的解决方案参考 Python,使用 C3_linearization来强制将基类合约转换一个有向无环图(DAG)的特定顺序。结果是我们希望的单调性,但却禁止了某些继承行为。特别是基类合约在 is后的顺序非常重要。下面的代码,Solidity会报错 Linearization of inheritance graph impossible

1
2
3
4
pragma solidity ^0.4.0;
contract X {}
contract A is X {}
contract C is A, X {}

原因是 C会请求 X来重写 A(因为继承定义的顺序是 A,X),但 A自身又是重写 X的,所以这是一个不可解决的矛盾。

一个简单的指定基类合约的继承顺序原则是从 most base-likemost derived

继承有相同名字的不同类型成员

当继承最终导致一个合约同时存在多个相同名字的修改器或函数,它将被视为一个错误。同新的如果事件与修改器重名,或者函数与事件重名都将产生错误。作为一个例外,状态变量的 getter可以覆盖一个 public的函数。

抽象(Abstract Contracts)

抽象函数是没有函数体的的函数。如下:

1
2
3
4
pragma solidity ^0.4.0;
contract Feline {
function utterance() returns (bytes32);
}

这样的合约不能通过编译,即使合约内也包含一些正常的函数。但它们可以做为基合约被继承。

1
2
3
4
5
6
7
8
9
10
pragma solidity ^0.4.0;
contract Feline {
function utterance() returns (bytes32);
function getContractName() returns (string){
return "Feline";
}
}
contract Cat is Feline {
function utterance() returns (bytes32) { return "miaow"; }
}

如果一个合约从一个 抽象合约里继承,但却没实现所有函数,那么它也是一个 抽象合约

接口

接口与抽象合约类似,与之不同的是,接口内没有任何函数是已实现的,同时还有如下限制:

  1. 不能继承其它合约,或接口。
  2. 不能定义构造器
  3. 不能定义变量
  4. 不能定义结构体
  5. 不能定义枚举类

其中的一些限制可能在未来放开。

接口基本上限制为合约ABI定义可以表示的内容,ABI和接口定义之间的转换应该是可能的,不会有任何信息丢失。

接口用自己的关键词表示:

1
2
3
interface Token {
function transfer(address recipient, uint amount);
}

合约可以继承于接口,因为他们可以继承于其它的合约。

库(Libraries)

库与合约类似,但它的目的是在一个指定的地址,且仅部署一次,然后通过EVM的特性 DELEGATECALL(Homestead之前是用 CALLCODE)来复用代码。这意味着库函数调用时,它的代码是在调用合约的上下文中执行。使用 this将会指向到调用合约,而且可以访问调用合约的 存储(storage)。因为一个合约是一个独立的代码块,它仅可以访问调用合约明确提供的 状态变量(state variables),否则除此之外,没有任何方法去知道这些状态变量。

使用库合约的合约,可以将库合约视为隐式的 父合约(base contracts),当然它们不会显式的出现在继承关系中。但调用库函数的方式非常类似,如库 L有函数 f(),使用 L.f()即可访问。此外, internal的库函数对所有合约可见,如果把库想像成一个父合约就能说得通了。当然调用内部函数使用的是 internal的调用惯例,这意味着所有 internal类型可以传进去, memory类型则通过引用传递,而不是拷贝的方式。为了在EVM中实现这一点, internal的库函数的代码和从其中调用的所有函数将被 拉取(pull into)到调用合约中,然后执行一个普通的 JUMP来代替 DELEGATECALL

下面的例子展示了如何使用库(后续在 using for章节有一个更适合的实现 Set的例子)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
pragma solidity ^0.4.0;
library Set {
// We define a new struct datatype that will be used to
// hold its data in the calling contract.
struct Data { mapping(uint => bool) flags; }
// Note that the first parameter is of type "storage
// reference" and thus only its storage address and not
// its contents is passed as part of the call. This is a
// special feature of library functions. It is idiomatic
// to call the first parameter 'self', if the function can
// be seen as a method of that object.
function insert(Data storage self, uint value)
returns (bool)
{
if (self.flags[value])
return false; // already there
self.flags[value] = true;
return true;
}
function remove(Data storage self, uint value)
returns (bool)
{
if (!self.flags[value])
return false; // not there
self.flags[value] = false;
return true;
}
function contains(Data storage self, uint value)
returns (bool)
{
return self.flags[value];
}
}
contract C {
Set.Data knownValues;
function register(uint value) {
// The library functions can be called without a
// specific instance of the library, since the
// "instance" will be the current contract.
if (!Set.insert(knownValues, value))
throw;
}
// In this contract, we can also directly access knownValues.flags, if we want.
}

上面的例子中:

  • Library定义了一个数据结构体,用来在调用的合约中使用(库本身并未实际存储的数据)。如果函数需要操作数据,这个数据一般是通过库函数的第一个参数传入,按惯例会把参数名定为 self
  • 另外一个需要留意的是上例中 self的类型是 storage,那么意味着传入的会是一个引用,而不是拷贝的值,那么修改它的值,会同步影响到其它地方,俗称引用传递,非值传递。
  • 库函数的使用不需要实例化, c.register中可以看出是直接使用 Set.insert。但实际上当前的这个合约本身就是它的一个实例。
  • 这个例子中, c可以直接访问, knownValues。虽然这个值主要是被库函数使用的。

当然,你完全可以不按上面的方式来使用库函数,可以不需要定义结构体,不需要使用 storage类型的参数,还可以在任何位置有多个 storage的引用类型的参数。

调用 Set.containsSet.removeSet.insert都会编译为以 DELEGATECALL的方式调用 external的合约和库。如果使用库,需要注意的是一个实实在在的外部函数调用发生了。尽管 msg.sendermsg.valuethis还会保持它们在此调用中的值(在 Homestead之前,由于实际使用的是 CALLCODEmsg.sendermsg.value会变化)。

下面的例子演示了如何使用 memory类型和 内部函数(inernal function),来实现一个自定义类型,但不会用到 外部函数调用(external function)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
pragma solidity ^0.4.0;
library BigInt {
struct bigint {
uint[] limbs;
}
function fromUint(uint x) internal returns (bigint r) {
r.limbs = new uint[](1);
r.limbs[0] = x;
}
function add(bigint _a, bigint _b) internal returns (bigint r) {
r.limbs = new uint[](max(_a.limbs.length, _b.limbs.length));
uint carry = 0;
for (uint i = 0; i < r.limbs.length; ++i) {
uint a = limb(_a, i);
uint b = limb(_b, i);
r.limbs[i] = a + b + carry;
if (a + b < a || (a + b == uint(-1) && carry > 0))
carry = 1;
else
carry = 0;
}
if (carry > 0) {
// too bad, we have to add a limb
uint[] memory newLimbs = new uint[](r.limbs.length + 1);
for (i = 0; i < r.limbs.length; ++i)
newLimbs[i] = r.limbs[i];
newLimbs[i] = carry;
r.limbs = newLimbs;
}
}
function limb(bigint _a, uint _limb) internal returns (uint) {
return _limb < _a.limbs.length ? _a.limbs[_limb] : 0;
}
function max(uint a, uint b) private returns (uint) {
return a > b ? a : b;
}
}
contract C {
using BigInt for BigInt.bigint;
function f() {
var x = BigInt.fromUint(7);
var y = BigInt.fromUint(uint(-1));
var z = x.add(y);
}
}

因为编译器并不知道库最终部署的地址。这些地址须由 linker填进最终的字节码中(使用 命令行编译器来进行联接)。如果地址没有以参数的方式正确给到编译器,编译后的字节码将会仍包含一个这样格式的占们符 _Set___(其中 Set是库的名称)。可以通过手动将所有的40个符号替换为库的十六进制地址。

对比普通合约来说,库的限制:

  • 状态变量(state variables)
  • 不能继承或被继承
  • 不能接收 ether

这些限制将来也可能被解除!

附着库(Using for)

指令 using A for B;用来附着库里定义的函数(从库 A)到任意类型 B。这些函数将会默认接收调用函数对象的实例作为第一个参数。语法类似, python中的 self变量一样。

using A for *的效果是,库 A中的函数被附着在做任意的类型上。

在这两种情形中,所有函数,即使那些第一个参数的类型与调用函数的对象类型不匹配的,也被附着上了。类型检查是在函数被真正调用时,函数重载检查也会执行。

using A for B;指令仅在当前的作用域有效,且暂时仅仅支持当前的合约这个作用域,后续也非常有可能解除这个限制,允许作用到全局范围。如果能作用到全局范围,通过引入一些模块(module),数据类型将能通过库函数扩展功能,而不需要每个地方都得写一遍类似的代码了。

下面我们来换个方式重写 set的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
pragma solidity ^0.4.0;
// This is the same code as before, just without comments
library Set {
struct Data { mapping(uint => bool) flags; }
function insert(Data storage self, uint value)
returns (bool)
{
if (self.flags[value])
return false; // already there
self.flags[value] = true;
return true;
}
function remove(Data storage self, uint value)
returns (bool)
{
if (!self.flags[value])
return false; // not there
self.flags[value] = false;
return true;
}
function contains(Data storage self, uint value)
returns (bool)
{
return self.flags[value];
}
}
contract C {
using Set for Set.Data; // this is the crucial change
Set.Data knownValues;
function register(uint value) {
// Here, all variables of type Set.Data have
// corresponding member functions.
// The following function call is identical to
// Set.insert(knownValues, value)
if (!knownValues.insert(value))
throw;
}
}

我们也可以通过这种方式来扩展 基本类型(elementary types)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
pragma solidity ^0.4.0;
library Search {
function indexOf(uint[] storage self, uint value) returns (uint) {
for (uint i = 0; i < self.length; i++)
if (self[i] == value) return i;
return uint(-1);
}
}
contract C {
using Search for uint[];
uint[] data;
function append(uint value) {
data.push(value);
}
function replace(uint _old, uint _new) {
// This performs the library function call
uint index = data.indexOf(_old);
if (index == uint(-1))
data.push(_new);
else
data[index] = _new;
}
}

需要注意的是所有库调用都实际上是EVM函数调用。这意味着,如果你传的是 memory类型的,或者是 值类型(vaue types),那么仅会传一份拷贝,即使是 self变量。变通之法就是使用 存储(storage)类型的变量,这样就不会拷贝内容。

Solidity Assembly

Solidity定义了一个汇编语言,可以不同Solidity一起使用。这个汇编语言还可以嵌入到Solidity源码中,以内联汇编的方式使用。下面我们将从内联汇编如何使用着手,介绍其与独立使用的汇编语言的不同,最后再介绍这门汇编语言。

文档尚待完善的补充的地方:待补充内联汇编的变量作用域的不同,尤其是使用含 internal的函数的库时所引入的复杂度。另外,还需补充,编译器定义的符号(symbols)。

内联汇编

通常我们通过库代码,来增强语言我,实现一些精细化的控制,Solidity为我们提供了一种接近于EVM底层的语言,内联汇编,允许与Solidity结合使用。由于EVM是栈式的,所以有时定位栈比较麻烦,Solidty的内联汇编为我们提供了下述的特性,来解决手写底层代码带来的各种问题:

  • 允许函数风格的操作码: mul(1, add(2, 3))等同于 push1 3 push1 2 add push1 1 mul
  • 内联局部变量: let x := add(2, 3) let y := mload(0x40) x := add(x, y)
  • 可访问外部变量: function f(uint x) { assembly { x := sub(x, 1) } }
  • 标签: let x := 10 repeat: x := sub(x, 1) jumpi(repeat, eq(x, 0))
  • 循环: for { let i := 0 } lt(i, x) { i := add(i, 1) } { y := mul(2, y) }
  • switch语句: switch x case 0 { y := mul(x, 2) } default { y := 0 }
  • 函数调用: function f(x) -> y { switch x case 0 { y := 1 } default { y := mul(x, f(sub(x, 1))) } }

下面将详细介绍内联编译(inline assembly)语言。

示例

下面的例子提供了一个库函数来访问另一个合约,并把它写入到一个 bytes变量中。有一些不能通过常规的Solidity语言完成,内联库可以用来在某些方面增强语言的能力。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pragma solidity ^0.4.0;
library GetCode {
function at(address _addr) returns (bytes o_code) {
assembly {
// retrieve the size of the code, this needs assembly
let size := extcodesize(_addr)
// allocate output byte array - this could also be done without assembly
// by using o_code = new bytes(size)
o_code := mload(0x40)
// new "memory end" including padding
mstore(0x40, add(o_code, and(add(add(size, 0x20), 0x1f), not(0x1f))))
// store length in memory
mstore(o_code, size)
// actually retrieve the code, this needs assembly
extcodecopy(_addr, add(o_code, 0x20), 0, size)
}
}
}

内联编译在当编译器没办法得到有效率的代码时非常有用。但需要留意的是内联编译语言写起来是比较难的,因为编译器不会进行一些检查,所以你应该只在复杂的,且你知道你在做什么的事情上使用它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pragma solidity ^0.4.0;
library VectorSum {
// This function is less efficient because the optimizer currently fails to
// remove the bounds checks in array access.
function sumSolidity(uint[] _data) returns (uint o_sum) {
for (uint i = 0; i < _data.length; ++i)
o_sum += _data[i];
}
// We know that we only access the array in bounds, so we can avoid the check.
// 0x20 needs to be added to an array because the first slot contains the
// array length.
function sumAsm(uint[] _data) returns (uint o_sum) {
for (uint i = 0; i < _data.length; ++i) {
assembly {
o_sum := mload(add(add(_data, 0x20), mul(i, 0x20)))
}
}
}
}

语法

内联编译语言也会像Solidity一样解析注释,字面量和标识符。所以你可以使用 ///**/的方式注释。内联编译的在Solidity中的语法是包裹在 assembly { ... },下面是可用的语法,后续有更详细的内容。

  • 字面量。如 0x12342abc(字符串最多是32个字符)
  • 操作码(指令的方式),如 mload sload dup1 sstore,后面有可支持的指令列表
  • 函数风格的操作码,如 add(1, mlod(0)
  • 标签,如 name:
  • 变量定义,如 let x := 7let x := add(y, 3)
  • 标识符(标签或内联局部变量或外部),如 jump(name)3 x add
  • 赋值(指令风格),如, 3 =: x
  • 函数风格的赋值,如 x := add(y, 3)
  • 支持块级的局部变量,如 { let x := 3 { let y := add(x, 1) } }

操作码

这个文档不想介绍EVM虚拟机的完整描述,但后面的列表可以做为EVM虚拟机的指令码的一个参考。

如果一个操作码有参数(通过在栈顶),那么他们会放在括号。需要注意的是参数的顺序可以颠倒(非函数风格,后面会详细说明)。用 -标记的操作码不会将一个参数推到栈顶,而标记为 *的是非常特殊的,所有其它的将且只将一个推到栈顶。

在后面的例子中, mem[a...b)表示成位置 a到位置 b(不包含)的 memory字节内容, storage[p]表示在位置 pstrorage内容。

操作码 pushijumpdest不能被直接使用。

在语法中,操作码被表示为预先定义的标识符。

操作码 说明
stop - stop execution, identical to return(0,0)
add(x, y) x + y
sub(x, y) x - y
mul(x, y) x * y
div(x, y) x / y
sdiv(x, y) x / y, for signed numbers in two’s complement
mod(x, y) x % y
smod(x, y) x % y, for signed numbers in two’s complement
exp(x, y) x to the power of y
not(x) ~x, every bit of x is negated
lt(x, y) 1 if x < y, 0 otherwise
gt(x, y) 1 if x > y, 0 otherwise
slt(x, y) 1 if x < y, 0 otherwise, for signed numbers in two’s complement
sgt(x, y) 1 if x > y, 0 otherwise, for signed numbers in two’s complement
eq(x, y) 1 if x == y, 0 otherwise
iszero(x) 1 if x == 0, 0 otherwise
and(x, y) bitwise and of x and y
or(x, y) bitwise or of x and y
xor(x, y) bitwise xor of x and y
byte(n, x) nth byte of x, where the most significant byte is the 0th byte
addmod(x, y, m) (x + y) % m with arbitrary precision arithmetics
mulmod(x, y, m) (x * y) % m with arbitrary precision arithmetics
signextend(i, x) sign extend from (i*8+7)th bit counting from least significant
keccak256(p, n) keccak(mem[p…(p+n)))
sha3(p, n) keccak(mem[p…(p+n)))
jump(label) - jump to label / code position
jumpi(label, cond) - jump to label if cond is nonzero
pc current position in code
pop(x) - remove the element pushed by x
dup1 … dup16 copy ith stack slot to the top (counting from top)
swap1 … swap16 * swap topmost and ith stack slot below it
mload(p) mem[p..(p+32))
mstore(p, v) - mem[p..(p+32)) := v
mstore8(p, v) - mem[p] := v & 0xff - only modifies a single byte
sload(p) storage[p]
sstore(p, v) - storage[p] := v
msize size of memory, i.e. largest accessed memory index
gas gas still available to execution
address address of the current contract / execution context
balance(a) wei balance at address a
caller call sender (excluding delegatecall)
callvalue wei sent together with the current call
calldataload(p) call data starting from position p (32 bytes)
calldatasize size of call data in bytes
calldatacopy(t, f, s) - copy s bytes from calldata at position f to mem at position t
codesize size of the code of the current contract / execution context
codecopy(t, f, s) - copy s bytes from code at position f to mem at position t
extcodesize(a) size of the code at address a
extcodecopy(a, t, f, s) - like codecopy(t, f, s) but take code at address a
returndatasize size of the last returndata
returndatacopy(t, f, s) - copy s bytes from returndata at position f to mem at position t
create(v, p, s) create new contract with code mem[p..(p+s)) and send v wei and return the new address
create2(v, n, p, s) create new contract with code mem[p..(p+s)) at address keccak256(. n . keccak256(mem[p..(p+s))) and send v wei and return the new address
call(g, a, v, in, insize, out, outsize) call contract at address a with input mem[in..(in+insize)) providing g gas and v wei and output area mem[out..(out+outsize)) returning 0 on error (eg. out of gas) and 1 on success
callcode(g, a, v, in, insize, out, outsize) identical to call but only use the code from a and stay in the context of the current contract otherwise
delegatecall(g, a, in, insize, out, outsize) identical to callcode but also keep caller and callvalue
staticcall(g, a, in, insize, out, outsize) identical to call(g, a, 0, in, insize, out, outsize) but do not allow state modifications
return(p, s) - end execution, return data mem[p..(p+s))
revert(p, s) - end execution, revert state changes, return data mem[p..(p+s))
selfdestruct(a) - end execution, destroy current contract and send funds to a
invalid - end execution with invalid instruction
log0(p, s) - log without topics and data mem[p..(p+s))
log1(p, s, t1) - log with topic t1 and data mem[p..(p+s))
log2(p, s, t1, t2) - log with topics t1, t2 and data mem[p..(p+s))
log3(p, s, t1, t2, t3) - log with topics t1, t2, t3 and data mem[p..(p+s))
log4(p, s, t1, t2, t3, t4) - log with topics t1, t2, t3, t4 and data mem[p..(p+s))
origin transaction sender
gasprice gas price of the transaction
blockhash(b) hash of block nr b - only for last 256 blocks excluding current
coinbase current mining beneficiary
timestamp timestamp of the current block in seconds since the epoch
number current block number
difficulty difficulty of the current block
gaslimit block gas limit of the current block

字面量

你可以使用整数常量,通过直接以十进制或16进制的表示方式,将会自动生成恰当的 pushi指令。

1
assembly { 2 3 add "abc" and }

上面的例子中,将会先加2,3得到5,然后再与字符串 abc进行与运算。字符串按左对齐存储,且不能超过32字节。

函数风格

你可以在操作码后接着输入操作码,它们最终都会生成正确的字节码。比如:

1
3 0x80 mload add 0x80 mstore

下面将会添加 3memory中位置 0x80的值。

由于经常很难直观的看到某个操作码真正的参数,Solidity内联编译提供了一个函数风格的表达式,上面的代码与下述等同:

1
mstore(0x80, add(mload(0x80), 3))

函数风格的表达式不能在内部使用指令风格,如 1 2 mstore(0x80, add)将不是合法的,必须被写为 mstore(0x80, add(2, 1))。那些不带参数的操作码,括号可以忽略。

需要注意的是函数风格的参数与指令风格的参数是反的。如果使用函数风格,第一个参数将会出现在栈顶。

访问外部函数与变量

Solidity中的变量和其它标识符,可以简单的通过名称引用。对于 memory变量,这将会把地址而不是值推到栈上。 Storage的则有所不同,由于对应的值不一定会占满整个 storage槽位,所以它的地址由槽和实际存储位置相对起始字节偏移。要搜索变量 x指向的槽位,使用 x_slot,得到变量相对槽位起始位置的偏移使用 x_offset

在赋值中(见下文),我们甚至可以直接向Solidity变量赋值。

还可以访问内联编译的外部函数:内联编译会推入整个的入口的 label(应用虚函数解析的方式)。Solidity中的调用语义如下:

  • 调用者推入返回的 label,arg1,arg2, … argn
  • 调用返回ret1,ret2,…, retm

这个功能使用起来还是有点麻烦,因为堆栈偏移量在调用过程中基本上有变化,因此对局部变量的引用将是错误的。

1
2
3
4
5
6
7
8
9
pragma solidity ^0.4.11;
contract C {
uint b;
function f(uint x) returns (uint r) {
assembly {
r := mul(x, sload(b_slot)) // ignore the offset, we know it is zero
}
}
}

标签

另一个在EVM的汇编的问题是 jumpjumpi使用了绝对地址,可以很容易的变化。Solidity内联汇编提供了标签来让 jump跳转更加容易。需要注意的是标签是非常底层的特性,尽量使用内联汇编函数,循环,Switch指令来代替。下面是一个求Fibonacci的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
let n := calldataload(4)
let a := 1
let b := a
loop:
jumpi(loopend, eq(n, 0))
a add swap1
n := sub(n, 1)
jump(loop)
loopend:
mstore(0, a)
return(0, 0x20)
}

需要注意的是自动访问栈元素需要内联者知道当前的栈高。这在跳转的源和目标之间有不同栈高时将失败。当然你也仍然可以在这种情况下使用jump,但你最好不要在这种情况下访问栈上的变量(即使是内联变量)。

此外,栈高分析器会一个操作码接着一个操作码的分析代码(而不是根据控制流),所以在下面的情况下,汇编程序将对标签 two的堆栈高度产生错误的判断:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
let x := 8
jump(two)
one:
// Here the stack height is 2 (because we pushed x and 7),
// but the assembler thinks it is 1 because it reads
// from top to bottom.
// Accessing the stack variable x here will lead to errors.
x := 9
jump(three)
two:
7 // push something onto the stack
jump(one)
three:
}

这个问题可以通过手动调整栈高来解决。你可以在标签前添加栈高需要的增量。需要注意的是,你没有必要关心这此,如果你只是使用循环或汇编级的函数。

下面的例子展示了,在极端的情况下,你可以通过上面说的解决这个问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
let x := 8
jump(two)
0 // This code is unreachable but will adjust the stack height correctly
one:
x := 9 // Now x can be accessed properly.
jump(three)
pop // Similar negative correction.
two:
7 // push something onto the stack
jump(one)
three:
pop // We have to pop the manually pushed value here again.
}

定义汇编-局部变量

你可以通过 let关键字来定义在内联汇编中有效的变量,实际上它只是在 {}中有效。内部实现上是,在 let指令出现时会在栈上创建一个新槽位,来保存定义的临时变量,在块结束时,会自动在栈上移除对应变量。你需要为变量提供一个初始值,比如 0,但也可以是复杂的函数表达式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pragma solidity ^0.4.0;
contract C {
function f(uint x) returns (uint b) {
assembly {
let v := add(x, 1)
mstore(0x80, v)
{
let y := add(sload(v), 1)
b := y
} // y is "deallocated" here
b := add(b, v)
} // v is "deallocated" here
}
}

赋值

你可以向内联局部变量赋值,或者函数局部变量。需要注意的是当你向一个指向 memorystorage赋值时,你只是修改了对应指针而不是对应的数据。

有两种方式的赋值方式:函数风格和指令风格。函数风格,比如 variable := value,你必须在函数风格的表达式中提供一个变量,最终将得到一个栈变量。指令风格 =: variable,值则直接从栈底取。以于两种方式冒号指向的都是变量名称(译者注:注意语法中冒号的位置)。赋值的效果是将栈上的变量值替换为新值。

1
2
3
4
5
6
assembly {
let v := 0 // functional-style assignment as part of variable declaration
let g := add(v, 2)
sload(10)
=: v // instruction style assignment, puts the result of sload(10) into v
}

Switch

你可以使用 switch语句来作为一个基础版本的 if/else语句。它需要取一个值,用它来与多个常量进行对比。每个分支对应的是对应切尔西到的常量。与某些语言容易出错的行为相反,控制流不会自动从一个判断情景到下一个场景(译者注:默认是break的)。最后有个叫 default的兜底。

1
2
3
4
5
6
7
8
9
10
11
assembly {
let x := 0
switch calldataload(4)
case 0 {
x := calldataload(0x24)
}
default {
x := calldataload(0x44)
}
sstore(0, div(x, 2))
}

可以有的 case不需要包裹到大括号中,但每个 case需要用大括号的包裹。

循环

内联编译支持一个简单的 for风格的循环。 for风格的循环的头部有三个部分,一个是初始部分,一个条件和一个后叠加部分。条件必须是一个函数风格的表达式,而其它两个部分用大括号包裹。如果在初始化的块中定义了任何变量,这些变量的作用域会被默认扩展到循环体内(条件,与后面的叠加部分定义的变量也类似。译者注:因为默认是块作用域,所以这里是一种特殊情况)。

1
2
3
4
5
6
assembly {
let x := 0
for { let i := 0 } lt(i, 0x100) { i := add(i, 0x20) } {
x := add(x, mload(i))
}
}

函数

汇编语言允许定义底层的函数。这些需要在栈上取参数(以及一个返回的代码行),也会将结果存到栈上。调用一个函数与执行一个函数风格的操作码看起来是一样的。

函数可以在任何地方定义,可以在定义的块中可见。在函数内,你不能访问一个在函数外定义的一个局部变量。同时也没有明确的 return语句。

如果你调用一个函数,并返回了多个值,你可以将他们赋值给一个元组,使用 a, b := f(x)let a, b := f(x)

下面的例子中通过平方乘来实现一个指数函数。

1
2
3
4
5
6
7
8
9
10
11
12
assembly {
function power(base, exponent) -> result {
switch exponent
case 0 { result := 1 }
case 1 { result := base }
default {
result := power(mul(base, base), div(exponent, 2))
switch mod(exponent, 2)
case 1 { result := mul(base, result) }
}
}
}

内联汇编中要注意的事

内联汇编语言使用中需要一个比较高的视野,但它又是非常底层的语法。函数调用,循环, switch被转换为简单的重写规则,另外一个语言提供的是重安排函数风格的操作码,管理了 jump标签,计算了栈高以方便变量的访问,同时在块结束时,移除块内定义的块内的局部变量。特别需要注意的是最后两个情况。你必须清醒的知道,汇编语言只提供了从开始到结束的栈高计算,它没有根据你的逻辑去计算栈高(译者注:这常常导致错误)。此外,像交换这样的操作,仅仅交换栈里的内容,并不是变量的位置。

Solidity中的惯例

与EVM汇编不同,Solidity知道类型少于256字节,如, uint24。为了让他们更高效,大多数的数学操作仅仅是把也们当成是一个256字节的数字进行计算,高位的字节只在需要的时候才会清理,比如在写入内存前,或者在需要比较时。这意味着如果你在内联汇编中访问这样的变量,你必须要手动清除高位的无效字节。

Solidity以非常简单的方式来管理内存:内部存在一个空间内存的指针在内存位置 0x40。如果你想分配内存,可以直接使用从那个位置的内存,并相应的更新指针。

Solidity中的内存数组元素,总是占用多个32字节的内存(也就是说 byte[]也是这样,但是 bytesstring不是这样)。多维的 memory的数组是指向 memory的数组。一个动态数组的长度存储在数据的第一个槽位,紧接着就是数组的元素。

独立的汇编语言

上面介绍的在Solidity中嵌入的内联汇编语言也可以单独使用。实际上,它是被计划用来作为编译器的一种中间语言。在这个目的下,它尝试达到下述的目标:

  1. 使用它编写的代码要可读,即使代码是从Solidity编译得到的。
  2. 从汇编语言转为字节码应该尽可能的少坑。
  3. 控制流应该容易检测来帮助进行形式验证与优化。

为了达到第一条和最后一条的目标,Solidity汇编语言提供了高层级的组件比如, for循环, switch语句和函数调用。这样的话,可以不直接使用 SWAP, DUP, JUMP, JUMPI语句,因为前两个有混淆的数据流,后两个有混淆的控制流。此外,函数形式的语句如 mul(add(x, y), 7)比纯的指令码的形式 7 y x add num更加可读。

第二个目标是通过引入一个绝对阶段来实现,该阶段只能以非常规则的方式去除较高级别的构造,并且仍允许检查生成的低级汇编代码。Solidity汇编语言提供的非原生的操作是用户定义的标识符的命名查找(函数名,变量名等),这些都遵循简单和常规的作用域规则,会清理栈上的局部变量。

作用域:一个标识符(标签,变量,函数,汇编)在定义的地方,均只有块级作用域(作用域会延伸到,所在块所嵌套的块)。跨函数边界访问局部变量是不合法的,即使可能在作用域内(译者注:这里可能说的是,函数内定义多个函数的情况,JavaScript有这种语法)。不允许 shadowing。局部变量不能在定义前被访问,但标签,函数和汇编可以。汇编是非常特殊的块结构可以用来,如,返回运行时的代码,或创建合约。外部定义的汇编变量在子汇编内不可见。

如果控制流来到了块的结束,局部变量数匹配的 pop指令会插入到栈底(译者注:移除局部变量,因为局部变量失效了)。无论何时引用局部变量,代码生成器需要知道其当前在堆栈中的相对位置,因此需要跟踪当前所谓的堆栈高度。由于所有的局部变量在块结束时会被移除,因此在进入块之前和之后的栈高应该是不变的,如果不是这样的,将会抛出一个警告。

我们为什么要使用高层级的构造器,比如 switchfor和函数。

使用 switchfor和函数,可以在不用 jumpjumpi的情况下写出来复杂的代码。这会让分析控制流更加容易,也可以进行更多的形式验证及优化。

此外,如果手动使用 jumps,计算栈高是非常复杂的。栈内所有的局部变量的位置必须是已知的,否则指向本地变量的引用,或者在块结束时自动删除局部变量都不会正常工作。脱机处理机制正确的在块内不可达的地方插入合适的操作以修正栈高来避免出现 jump时非连续的控制流带来的栈高计算不准确的问题。

示例:

我们从一个例子来看一下Solidity到这种中间的脱机汇编结果。我们可以一起来考虑下下述Soldity程序的字节码:

1
2
3
4
5
6
7
contract C {
function f(uint x) returns (uint y) {
y = 1;
for (uint i = 0; i < x; i++)
y = 2 * y;
}
}

它将生成下述的汇编内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
mstore(0x40, 0x60) // store the "free memory pointer"
// function dispatcher
switch div(calldataload(0), exp(2, 226))
case 0xb3de648b {
let (r) = f(calldataload(4))
let ret := $allocate(0x20)
mstore(ret, r)
return(ret, 0x20)
}
default { revert(0, 0) }
// memory allocator
function $allocate(size) -> pos {
pos := mload(0x40)
mstore(0x40, add(pos, size))
}
// the contract function
function f(x) -> y {
y := 1
for { let i := 0 } lt(i, x) { i := add(i, 1) } {
y := mul(2, y)
}
}
}

在经过脱机汇编阶段,它会编译成下述的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
{
mstore(0x40, 0x60)
{
let $0 := div(calldataload(0), exp(2, 226))
jumpi($case1, eq($0, 0xb3de648b))
jump($caseDefault)
$case1:
{
// the function call - we put return label and arguments on the stack
$ret1 calldataload(4) jump(f)
// This is unreachable code. Opcodes are added that mirror the
// effect of the function on the stack height: Arguments are
// removed and return values are introduced.
pop pop
let r := 0
$ret1: // the actual return point
$ret2 0x20 jump($allocate)
pop pop let ret := 0
$ret2:
mstore(ret, r)
return(ret, 0x20)
// although it is useless, the jump is automatically inserted,
// since the desugaring process is a purely syntactic operation that
// does not analyze control-flow
jump($endswitch)
}
$caseDefault:
{
revert(0, 0)
jump($endswitch)
}
$endswitch:
}
jump($afterFunction)
allocate:
{
// we jump over the unreachable code that introduces the function arguments
jump($start)
let $retpos := 0 let size := 0
$start:
// output variables live in the same scope as the arguments and is
// actually allocated.
let pos := 0
{
pos := mload(0x40)
mstore(0x40, add(pos, size))
}
// This code replaces the arguments by the return values and jumps back.
swap1 pop swap1 jump
// Again unreachable code that corrects stack height.
0 0
}
f:
{
jump($start)
let $retpos := 0 let x := 0
$start:
let y := 0
{
let i := 0
$for_begin:
jumpi($for_end, iszero(lt(i, x)))
{
y := mul(2, y)
}
$for_continue:
{ i := add(i, 1) }
jump($for_begin)
$for_end:
} // Here, a pop instruction will be inserted for i
swap1 pop swap1 jump
0 0
}
$afterFunction:
stop
}

汇编有下面四个阶段:

  1. 解析
  2. 脱汇编(移除switch,for和函数)
  3. 生成指令流
  4. 生成字节码

我们将简单的以步骤1到3指定步骤。更加详细的步骤将在后面说明。

解析、语法

解析的任务如下:

  • 将字节流转为符号流,去掉其中的C++风格的注释(一种特殊的源代码引用的注释,这里不打算深入讨论)。
  • 将符号流转为下述定义的语法结构的AST。
  • 注册块中定义的标识符,标注从哪里开始(根据AST节点的注解),变量可以被访问。

组合词典遵循由Solidity本身定义的词组。

空格用于分隔标记,它由空格,制表符和换行符组成。 注释是常规的JavaScript / C ++注释,并以与Whitespace相同的方式进行解释。

语法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
AssemblyBlock = '{' AssemblyItem* '}'
AssemblyItem =
Identifier |
AssemblyBlock |
FunctionalAssemblyExpression |
AssemblyLocalDefinition |
FunctionalAssemblyAssignment |
AssemblyAssignment |
LabelDefinition |
AssemblySwitch |
AssemblyFunctionDefinition |
AssemblyFor |
'break' | 'continue' |
SubAssembly | 'dataSize' '(' Identifier ')' |
LinkerSymbol |
'errorLabel' | 'bytecodeSize' |
NumberLiteral | StringLiteral | HexLiteral
Identifier = [a-zA-Z_$] [a-zA-Z_0-9]*
FunctionalAssemblyExpression = Identifier '(' ( AssemblyItem ( ',' AssemblyItem )* )? ')'
AssemblyLocalDefinition = 'let' IdentifierOrList ':=' FunctionalAssemblyExpression
FunctionalAssemblyAssignment = IdentifierOrList ':=' FunctionalAssemblyExpression
IdentifierOrList = Identifier | '(' IdentifierList ')'
IdentifierList = Identifier ( ',' Identifier)*
AssemblyAssignment = '=:' Identifier
LabelDefinition = Identifier ':'
AssemblySwitch = 'switch' FunctionalAssemblyExpression AssemblyCase*
( 'default' AssemblyBlock )?
AssemblyCase = 'case' FunctionalAssemblyExpression AssemblyBlock
AssemblyFunctionDefinition = 'function' Identifier '(' IdentifierList? ')'
( '->' '(' IdentifierList ')' )? AssemblyBlock
AssemblyFor = 'for' ( AssemblyBlock | FunctionalAssemblyExpression)
FunctionalAssemblyExpression ( AssemblyBlock | FunctionalAssemblyExpression) AssemblyBlock
SubAssembly = 'assembly' Identifier AssemblyBlock
LinkerSymbol = 'linkerSymbol' '(' StringLiteral ')'
NumberLiteral = HexNumber | DecimalNumber
HexLiteral = 'hex' ('"' ([0-9a-fA-F]{2})* '"' | '\'' ([0-9a-fA-F]{2})* '\'')
StringLiteral = '"' ([^"\r\n\\] | '\\' .)* '"'
HexNumber = '0x' [0-9a-fA-F]+
DecimalNumber = [0-9]+

脱汇编

一个AST转换,移除其中的 forswitch和函数构建。结果仍由同一个解析器,但它不确定使用什么构造。如果添加仅跳转到并且不继续的jumpdests,则添加有关堆栈内容的信息,除非没有局部变量访问到外部作用域或栈高度与上一条指令相同。伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
desugar item: AST -> AST =
match item {
AssemblyFunctionDefinition('function' name '(' arg1, ..., argn ')' '->' ( '(' ret1, ..., retm ')' body) ->
:
{
jump($_start)
let $retPC := 0 let argn := 0 ... let arg1 := 0
$_start:
let ret1 := 0 ... let retm := 0
{ desugar(body) }
swap and pop items so that only ret1, ... retm, $retPC are left on the stack
jump
0 (1 + n times) to compensate removal of arg1, ..., argn and $retPC
}
AssemblyFor('for' { init } condition post body) ->
{
init // cannot be its own block because we want variable scope to extend into the body
// find I such that there are no labels $forI_*
$forI_begin:
jumpi($forI_end, iszero(condition))
{ body }
$forI_continue:
{ post }
jump($forI_begin)
$forI_end:
}
'break' ->
{
// find nearest enclosing scope with label $forI_end
pop all local variables that are defined at the current point
but not at $forI_end
jump($forI_end)
0 (as many as variables were removed above)
}
'continue' ->
{
// find nearest enclosing scope with label $forI_continue
pop all local variables that are defined at the current point
but not at $forI_continue
jump($forI_continue)
0 (as many as variables were removed above)
}
AssemblySwitch(switch condition cases ( default: defaultBlock )? ) ->
{
// find I such that there is no $switchI* label or variable
let $switchI_value := condition
for each of cases match {
case val: -> jumpi($switchI_caseJ, eq($switchI_value, val))
}
if default block present: ->
{ defaultBlock jump($switchI_end) }
for each of cases match {
case val: { body } -> $switchI_caseJ: { body jump($switchI_end) }
}
$switchI_end:
}
FunctionalAssemblyExpression( identifier(arg1, arg2, ..., argn) ) ->
{
if identifier is function with n args and m ret values ->
{
// find I such that $funcallI_* does not exist
$funcallI_return argn ... arg2 arg1 jump()
pop (n + 1 times)
if the current context is `let (id1, ..., idm) := f(...)` ->
let id1 := 0 ... let idm := 0
$funcallI_return:
else ->
0 (m times)
$funcallI_return:
turn the functional expression that leads to the function call
into a statement stream
}
else -> desugar(children of node)
}
default node ->
desugar(children of node)
}

生成操作码流

在操作码流生成期间,我们在一个计数器中跟踪当前的栈高,所以通过名称访问栈的变量是可能的。栈高在会修改栈的操作码后或每一个标签后进行栈调整。当每一个新局部变量被引入时,它都会用当前的栈高进行注册。如果要访问一个变量(或者拷贝其值,或者对其赋值),会根据当前栈高与变量引入时的当时栈高的不同来选择合适的 DUPSWAP指令。

伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
codegen item: AST -> opcode_stream =
match item {
AssemblyBlock({ items }) ->
join(codegen(item) for item in items)
if last generated opcode has continuing control flow:
POP for all local variables registered at the block (including variables
introduced by labels)
warn if the stack height at this point is not the same as at the start of the block
Identifier(id) ->
lookup id in the syntactic stack of blocks
match type of id
Local Variable ->
DUPi where i = 1 + stack_height - stack_height_of_identifier(id)
Label ->
// reference to be resolved during bytecode generation
PUSH
SubAssembly ->
PUSH
FunctionalAssemblyExpression(id ( arguments ) ) ->
join(codegen(arg) for arg in arguments.reversed())
id (which has to be an opcode, might be a function name later)
AssemblyLocalDefinition(let (id1, ..., idn) := expr) ->
register identifiers id1, ..., idn as locals in current block at current stack height
codegen(expr) - assert that expr returns n items to the stack
FunctionalAssemblyAssignment((id1, ..., idn) := expr) ->
lookup id1, ..., idn in the syntactic stack of blocks, assert that they are variables
codegen(expr)
for j = n, ..., i:
SWAPi where i = 1 + stack_height - stack_height_of_identifier(idj)
POP
AssemblyAssignment(=: id) ->
look up id in the syntactic stack of blocks, assert that it is a variable
SWAPi where i = 1 + stack_height - stack_height_of_identifier(id)
POP
LabelDefinition(name:) ->
JUMPDEST
NumberLiteral(num) ->
PUSH
HexLiteral(lit) ->
PUSH32
StringLiteral(lit) ->
PUSH32
SubAssembly(assembly block) ->
append codegen(block) at the end of the code
dataSize() ->
assert that is a subassembly ->
PUSH32>
linkerSymbol() ->
PUSH32 and append position to linker table
}

状态变量的存储模型(Layout of State Variables in Storage)

大小固定的变量(除了 映射变长数组以外的所有类型)在存储(storage)中是依次连续从位置 0开始排列的。如果多个变量占用的大小少于32字节,会尽可能的打包到单个 storage槽位里,具体规则如下:

  • 在storage槽中第一项是按低位对齐存储(lower-order aligned)(译者注:意味着是大端序了,因为是按书写顺序。)。
  • 基本类型存储时仅占用其实际需要的字节。
  • 如果基本类型不能放入某个槽位余下的空间,它将被放入下一个槽位。
  • 结构体数组总是使用一个全新的槽位,并占用整个槽(但在结构体内或数组内的每个项仍遵从上述规则)

优化建议

当使用的元素占用少于32字节,你的合约的gas使用也许更高。这是因为EVM每次操作32字节。因此,如果元素比这小,EVM需要更多操作来从32字节减少到需要的大小。

因为编译器会将多个元素打包到一个 storage槽位,这样就可以将多次读或写组合进一次操作中,只有在这时,通过缩减变量大小来优化存储结构才有意义。当操作函数参数和 memory的变量时,因为编译器不会这样优化,所以没有上述的意义。

最后,为了方便EVM进行优化,尝试有意识排序 storage的变量和结构体的成员,从而让他们能打包得更紧密。比如,按这样的顺序定义, uint128, uint128, uint256,而不是 uint128, uint256, uint128。因为后一种会占用三个槽位。

非固定大小

结构体和数组里的元素按它们给定的顺序存储。

由于它们不可预知的大小。 映射变长数组类型,使用 Keccak-256哈希运算来找真正数据存储的起始位置。这些起始位置往往是完整的堆栈槽。

映射动态数组根据上述规则在位置 p占用一个未满的槽位(对 映射里嵌套 映射,数组中嵌套数组的情况则递归应用上述规则)。对一个动态数组,位置 p这个槽位存储数组的元素个数(字节数组和字符串例外,见下文)。而对于 映射,这个槽位没有填充任何数据(但这是必要的,因为两个挨着的 映射将会得到不同的哈希值)。数组的原始数据位置是 keccak256(p);而 映射类型的某个键 k,它的数据存储位置则是 keccak256(k . p),其中的 .表示连接符号。如果定位到的值以是一个非基本类型,则继续运用上述规则,是基于 keccak256(k . p)的新的偏移 offset

bytesstring占用的字节大小如果足够小,会把其自身长度和原始数据存在当前的槽位。具体来说,如果数据最多31位长,高位存数据(左对齐),低位存储长度 lenght * 2。如果再长一点,主槽位就只存 lenght * 2 + 1。原始数据按普通规则存储在 keccak256(slot)

所以对于接下来的代码片段:

1
2
3
4
5
6
pragma solidity ^0.4.4;
contract C {
struct s { uint a; uint b; }
uint x;
mapping(uint => mapping(uint => s)) data;
}

按上面的代码来看,结构体从位置0开始,这里定义了一个结构体,但并没有对应的结构体变量,故不占用空间。 uint x实际是 uint256,单个占32字节,占用槽位0,所以映射 data将从槽位1开始。

data[4][9].b的位置在 keccak256(uint256(9) . keccak256(uint256(4) . uint256(1))) + 1

有人在这里尝试直接读取区块链的存储值, https://github.com/ethereum/solidity/issues/1550

内存变量的布局(Layout in Memory)

Solidity预留了3个32字节大小的槽位:

  • 0-64:哈希方法的 暂存空间(scratch space)
  • 64-96:当前已分配内存大小(也称 空闲内存指针(free memory pointer))

暂存空间可在语句之间使用(如在内联编译时使用)

Solidity总是在 空闲内存指针所在位置创建一个新对象,且对应的内存永远不会被释放(也许未来会改变这种做法)。

有一些在Solidity中的操作需要超过64字节的临时空间,这样就会超过预留的 暂存空间。他们就将会分配到 空闲内存指针所在的地方,但由于他们自身的特点,生命周期相对较短,且指针本身不能更新,内存也许会,也许不会被 清零(zerod out)。因此,大家不应该认为空闲的内存一定已经是 清零(zeroed out)的。

调用数据的布局(Layout of CallData)

当Solidity合约被部署后,从某个帐户调用这个合约,需要输入的数据是需要符合 the ABI specification, 中文翻译见这里: http://me.tryblockchain.org/Solidity-abi-abstraction.html。ABI规范需要参数被填充为多个32字节。内部的函数调用,则使用了不同的约定。

内部机制

内部机制 - 清理变量(Internals - Cleaning Up Variables)

当一个值占用的位数小于32字节时,那些没有用到的位必须被清除掉。Solidity编译器设计实现为,在任何可能受到潜在的残存数据带来的副作用之前,清理掉这些脏数据。比如,在向内存写入一个值前,不需要的字节位需要被清除掉,因为没有用到的内存位可能被用来计算哈希,或作为消息调用的发送的数据存储。同样的,在向 storage中存储时,未用到的字节位需要被清理掉,否则这些脏数据会带来意想不到的事情。

另一方面,如果接下来的后述操作不会产生副作用,我们不会主动清理这些字节位。比如,由于任何非0的值被 JUMP指令认为是 true。在它作用 JUMPI指令的条件前,我们在不会清理这个布尔值。

在上述设计准则之外,Solidity编译器会在输入数据加载到栈上后清理掉它们。

不同的类型,有不同的无效值的清理规则。

类型 有效值 无效值意味着
有n的成员的枚举类型 0到(n - 1) 异常(exception)
布尔 0或1 1
有符号整数 sign-extended word 当前静默的包装了结果,以后会以异常的形式抛出来
无符号整数 高位节是0 当前静默的包装了结果,以后会以异常的形式抛出来

内部机制 - 优化(Internals - The Optimizer)

Solidity是基于汇编优化的,所以它可以,同时也被其它编程语言所使用(译者注:其它语言编译为汇编)。编译器会在 JUMPJUMPDEST处拆分基本的指令序列为一个个的基本块。在这些代码块内,所有的指令都被分析。所有的对栈,内存或存储的操作被记录成由指令及其参数组成的一个个表达式,这些表达式又会指向另一个表达式。核心目的是找到一些表达式在任何输入的情况下都恒等,然后将它们组合成一个表达式类。优化器首先尝试在一系列已知的表达式中,找出来一些全新的表达式。如果找不到,表达式通过一些简单的原则进行简化,比如 constant + constant = sum_of_constantsX * 1 = X。由于这一切是递归进行的,我们可以在第二项是一个更复杂的表达时,应用上述后续规则。对内存或存储的修改,存储的位置经常会被擦除,由此我们并不知道存的数据有什么不同:如果我们首先写入一个值x,再写入另一个值y,这两个都是输入变量,第二个写入时会覆盖第一个,所以我们实际在写入第二个值时,不知道第一个值是什么了。所以,如果一个简单的表达式 x-y指向一个非0的常量,这样我们就能在操作y时知道x内存储的值。

在流程最后,我们知道哪一个表达式会在栈顶,并且有一系列的对内存或存储的修改。这些信息与基本的块存在一起以方便的用来连接他们。此外,关于栈,存储和内存配置的信息会传递到下一个块。如果我们知道所有JUMP和JUMPI指令的目标,我们可以构建程序的完整的控制流程图。如果有任何一个我们不知道目标的跳转(因为目标是通过输入参数进行计算的,所以原则上可能发生),我们必须擦除块知识的输入,因为他有可能是某个跳转的目的地(译者注:因为可能某个跳转在运行时会指向他,修改他的状态,所以他的推算状态是错误的)。如果某个 JUMPI被发现他的条件是常量,它会被转化为一个无状态的跳转。

在最后一步,每个块中的代码都将重新生成。在某个块结束时,将生成栈上表达式的依赖树,不在这个树上的操作就被丢弃了。在我们原始代码中想要应用的对内存、存储想要的修改顺序的代码就生成出来了(被丢弃的修改被认为是完全不需要的),最终,生成了所有的需要在栈上存在的值。

这些步骤应用于每个基本的块,如果新生成的代码更小,将会替换现有的代码。如果一个块在分析期间在 JUMPI处分裂,条件被证实为一个常量, JUMPI将可以基于常量值被替换掉,比如下述代码:

1
2
3
4
5
6
var x = 7;
data[7] = 9;
if (data[x] != x + 2)
return 2;
else
return 1;

简化的代码可以被编译为:

1
2
data[7] = 9;
return 1;

尽管上述代码在一开始有一个跳转指令。

特殊特性(Esoteric Features)

在Solidity的类型系统里面有一些类型有一些在其它语言中没有的语法。其中之一就是函数类型。但依然,使用 var时,可以把函数类型作为本地变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
contract FunctionSelector {
function select(bool useB, uint x) returns (uint z) {
var f = a;
if (useB) f = b;
return f(x);
}
function a(uint x) returns (uint z) {
return x * x;
}
function b(uint x) returns (uint z) {
return 2 * x;
}
}

可以对var赋值为不同的函数。

源文件映射

作为AST输出的一部分,编译器会提供AST某个节点以应的源代码的范围。这可以被用来做基于AST的静态代码错误分析,可以高亮本地变量,和他们对应使用的调试工具。

此外,编译器也可以生成字节码到生成指令源代码的范围映射。这对静态分析工具来说非常重要,它们在字节码级别分析,可以来在调试工具内显示对应代码位置,或支持断点操作。

上述的源代码映射都使用整数来引用源代码。

Solidity的完整语法

入门说明

值类型

引用类型

杂项

单位

语言内置特性

进阶

合约详解

其它

您的支持将鼓励我继续创作!
0%