Solidity 基础知识和概述

Solidity 是运行于以太坊(Ethereum) 区块链上的智能合约语言,它是图灵完备的,意味着可以用它写一些任意复杂度的程序并运行于区块链中。

1. 区块链基础知识

区块链加密货币的技术基础,本文属于 Solidity,对此不过多介绍,可能在以后会在其他文章进行介绍。

1.1 交易

区块链形象的来说就是一个全球共享的交易数据库,这意味着每个人都可以访问这个数据库并发起更改,这个更改就是 交易

区块链对交易有单一性保证,也就是当你的交易正在提交到数据库的时候,其他的交易不能影响你的交易。

同时,区块链对交易有完成保证,意思就是一个交易,要不就 全部完成,要不就 都不完成。不会出现一方余额变动,而另一方却不变的情况。

其次,一个交易总是由发起方进行密码学方面的签名(signed),这也就保证交易的来源方的可信赖性。只有拥有对应的密钥键值对,才能从账户中转钱。

1.2 区块

交易数据库都有一个需要处理的基本问题:如果两个交易都想清空一个账户的余额怎么办?这在比特币的术语中叫做 “doble-spend attack”,也就是交易之间出现了冲突。

区块链对此作出的回答是, 你不需要担心这种问题
区块链会对交易的顺序作出选择,此时,这些交易会被捆绑进一个 区块 中,当两个交易出现冲突的时候,排在后面的交易就会被抛弃而不会进入区块中。

这些区块在时间上呈现出一种线性的形状,因而我们将这些区块所构成的系统,也就是上面的交易数据库称为 区块链

而区块链中所应用的交易选择机制,也就是交易的公证机制,我们称其为 挖矿

之所以称其为挖矿,原因在于新的包含 你所承认的交易 的区块是通过一系列的计算得到的,这个新区块的生成很类似从一堆数据中把金子挖出来的过程。

区块计算成功后,区块链系统会给予挖矿者奖励,在比特币系统中是赠与比特币,以太币系统则是奖励以太币。

当然,一个区块也有可能会被退回(reverted),不过是仅当这个区块位于区块链的头部的时候;当越来越多的区块被加到区块链的头部之后,你所计算出的区块被退回的可能性就会越来越低。

2. 以太坊虚拟机(EVM)

以太坊虚拟机(Ethereum Virtual Machine) 是以太坊合约(contract)的运行环境,也即 Solidity 的运行环境。

和普通的虚拟机不同的是,EVM 不是一个沙盒系统,而是 完全独立的

运行在 EVM 中的合约不能访问互联网、文件系统或者其他的进程,只能和运行于 EVM 的其他合约进行交互。

EVM 中有如下概念:

2.1 账户(Accounts)

EVM 中有着两种账户:
一种称为外来账户(External owned Accounts),是使用公私有的键值对控制访问的,也就是真实人类控制的帐号。

另一种称为合约账户(Contract Accounts),是含有代码的合约控制的帐号,代码被存储在合约中。

账户通过地址来进行标识;
外部账户的地址通过其 public key 来确定;
合约账户的地址是在其被创建的时候确定的,通过它的创建者(即交易的发送者)的地址和从创建者地址发送的交易数量来确定。

EVM 对于这两种账户都是平等对待的,不管它存不存储着代码。

每个账户都有着一个持久化的 key-value mapping(类似 HashMap)。keyvalue 分别是 256bit words 和 256bit words。这个 mapping 被称为 storage

同时,每个账户都具有 以太币 的余额(balance),可以通过发送以太币的交易来修改。

两种账户的对比

  • 外部账户(External Accounts)
    • 具有以太币余额
    • 可以发送交易(可以发送或者触发合约代码)
    • 使用键值对来控制
    • 不储存有代码

  • 合约账户(Contract Accounts)
    • 具有以太币余额
    • 储存有代码
    • 其代码的执行通过交易或者其他合约发送的信息来触发
    • 当其代码执行时,可以:
      • 执行任意复杂度的操作(图灵完备)
      • 修改其自身的持久性存储(storage)
      • 调用其他合约

2.2 交易(Transactions)

交易是一个账户发给另一个帐号的消息,交易可以包含二进制的数据(称为它的负载),和以太币

如果目标账户具有代码,那么这个代码就会被执行,并且交易会提供其负载充当代码的输入数据。

如果目标账户是 零账户(zero-account)(它的账户的地址是 0),那么,该交易就会创建一个 新的合约。上面已经说过,合约的地址是通过发送者的地址来确定的。

此时,交易的负载就会充当合约的构建参数,此时,EVM 开始执行构造函数,进行合约的构建,其结果即合约的代码,被存入合约账户中。

也就是说,不需要传入合约本身的代码即可完成合约的构建

2.3 汽油(Gas)

汽油是以太坊用于衡量执行交易的工作量的单位。

由于发起交易有可能导致合约的执行,代码执行就需要 CS 领域中的时间与空间,即需要矿工的算力来作为支撑。

为了保证网络中的算力不被大规模消耗和锁死,以太坊中的每一个交易都需要消耗汽油来完成,即交易的 手续费

之所以称之为 汽油,是因为这个“手续费”是需要事先从交易发起者的账户中扣除掉,与该交易绑定,很类似一个汽车加油的过程。

唯一不同的是,交易的发起者可以自定义汽油的价格

也就是说,交易发起者通过事先从账户中扣除一定量的 以太,作为充入的汽油;
充入的以太费用 = 汽油量 ×\times 自定义的汽油价格,汽油量实际上是通过充入的以太费用倒推得到的。

这也就类似于一个加油的过程。

然后,矿工开始处理交易,并按照一定的规则 不断消耗汽油

当计算完成时,区块被生成,并加入区块链中,矿工得到所消耗的汽油的以太费用作为交易的手续费;
同时, 多余的汽油会被退还回交易发起者的账户

但是,如果汽油耗尽,交易还未处理完成的话,那么矿工就会 回退所有修改,并将该交易作为 失败的交易 加入到区块链中,同时, 收取所有的汽油费用,不退换给发起者。

2.4 存储, 内存和栈

2.4.1 存储(Storage)

每一个帐号都会具有一个 256bit -> 256 bit 的键值对,这个键值对被称作 storage
在合约中进行 storage 的遍历和枚举是不可能的,而且在 storage的读写操作都是相对昂贵的。

即通常用于存储一些持久化的数据,所以称作 storage

事实上 storage 的读写是十分昂贵的,它需要 20000 gas 进行一次初始化,需要 5000 gas 来进行数据的修改,同时还需要 200 gas 进行一个 word 的读取。

为什么需要这么贵呢?是因为存储在 storage 的数据是永久保存在区块链中的,需要真实的存储开销。

2.4.2 内存(Memory)

第二个存储类型是 memory,就像内存一样,memory 仅在合约运行中有效,当合约运行完成时,内存就会被清空重置。

内存是线性的并被字节编码;
对于读取操作来说,只能一次性读取 256bit 的数据,即一个 word;
而对于写入操作来说,可以写入 8bit 或者 256bit。

当你读写超过了一个 word 的时候,内存以 word(256bit) 的级别扩大;
当然,随着内存的扩大,就要相应收取 gas 作为费用。
需要注意的是,内存每扩大一个数量级,都是平方级别的,所以不要过多使用内存,否则会消耗很多 gas。

相比 storage 来说,memory 的处理开销就便宜很多。
它只需要 3 gas 来读写数据,如果内存扩大了那么就收取一些扩容费用的 gas。

一般来说,内存就是通常的工作用地,基本的,不需要永久存储的东西都可以放到内存中。

2.4.3 栈(Stack)

EVM 不像传统的计算机是一个以寄存器为主的机器,而是以栈为主的机器,所有的计算都在一个被称作 stack 的空间中进行。

这个栈具有 1024 个元素的容量,而且包含着一些 word。

对于栈的访问仅限于前 16 个元素;
在前 16 个元素中,你可以将任意一个复制到顶部,或者将任意一个元素和顶部的元素做交换。

其他的操作则是提取顶部元素(可以不止提取一个)进行计算并将结果压入栈中。

当然,你也可以将栈中的元素移到内存和存储中,不过对于比前 16 个更深一点的元素就不能访问到了,除非你将前 16 个元素移除。

通常,这个栈中的元素不会使用到,就像函数栈一样由编译器或者解释器来操作。

2.5 指令集

EVM 的指令集比较简短,所有的指令都是对基本数据类型和 256bit 的字的操作,包含了一般的算术运算、位运算、逻辑运算和比较运算等,同时还可以进行条件跳转和非条件跳转。

同时,合约还可以访问它所在区块的一些信息比如说区块的编号和区块的时间戳。

2.6 信息调用(Message Calls)

合约可以通过 信息调用 来调用其他的合约或者给一个非合约账户发送以太币。

信息调用和交易类似,都具备一个发送者,目标者,数据负载,以太币,汽油和返回的数据。

事实上,每一个交易都是由 top-level 的信息调用组成的,top-level 的信息调用可以创建其他信息调用。

合约可以决定通过信息调用所传递的 gas 数量,如果一个 out-of-gas exception 发生的话,调用栈中就会压入一个 error value 来标识异常的发生。

此时,只有通过该调用传送的 gas 会被消耗掉。同时,发起信息调用的合约会手动引起一个异常,以保证异常栈的呈现。

上面也说过,被调用的合约会收到一个新鲜的 memory 实例,并可以访问随调用传来的数据负载;

此时,系统会提供一个额外的空间用于存储这种数据负载,叫做 calldata

当合约代码执行完毕后,它可以将数据返回,而返回的数据会存储在调用者的内存中。

调用的深度被限制在 1024,所以对于一些比较复杂的操作,使用循环会比使用递归要好。

2.7 委托调用(Delegatecall)/调用代码(Callcode)和库

委托调用是一种特别的信息调用,它可以将调用者的上下文暴露给被调用者。

下面举一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
contract D {
unit public n;
address public sender;

function delegatecallSetN(address _e, unit _n) {
_e.delegatecall(byte4(sha3("setN(unit256)")), _n)
}
}

contract E {
unit public n;
address public sender;
funciton setN(unit _n) {
n = _n;
sender = msg.sender;
}
}

当一个合约 C 调用 D 的方法时,是 Dsender 被设置成了 C ,而不是 E 的方法被设置。

这就是 delegatecall 和普通调用的区别,它相当于将其他合约的函数引入到了当前合约的作用域中。

引入这种调用之后,我们就可以在合约中动态调用函数,这也为我们实现 Solidity 的函数库提供了途径。

不过需要提醒的是,这个 delegatecall 方法是相当低级的方法, 如果不做深入开发可以不管它

2.8 日志

EVM 也提供从底层直至区块层级的日志功能,用这些功能来实现 事件系统

但是,合约在它被创建之后就不能访问日志数据,不过日志数据可以从区块链的外部被访问。

一些日志数据被存储在布隆过滤器(bloom filter)中,所以一些轻量级的客户端也可以访问部分的区块链日志。

2.9 合约创建

合约除了通过信息调用来创建以外,还可以通过一个特别的指令来创建。

指令创建和普通的信息调用创建的区别在于,在指令创建完毕之后,创建者可以获取到新合约的地址。

2.10 自毁

想要去除区块链中的代码的唯一途径就是通过合约的自毁。

当合约调用析构指令(selfdestruct) 时,合约账户中剩余的以太币会被发往制定的目标,然后,合约的 storage 和代码就会从区块链中删除。

即使合约代码中不包含 selfdestruct 指令,它也可以通过调用 delegatecall 或者 callcode 指令来执行

以太坊客户端似乎还未实现旧合约和旧代码的删除功能。存储节点可以自行选择是否删除合约。

当期,外部账户是无法被删除的。