智能合约概述

概述

在大致搞清楚比特币的基本概念之后,再来理解以太坊的概念就容易很多,其中最主要的就是搞清楚什么是智能合约。

只是合约其实就是存储在区块链上并运行在矿工机上的一段代码,由于以太坊的限制,这段代码并不能过大,也不能过于复杂。

那么智能合约大致是怎么工作的呢?

目前的理解是以太坊存在两种账户,一种是外部账户,一种是合约账户,二者都有自己的地址和余额,唯一的区别就是合约账户上存储着智能合约合约的codehash,实际的合约代码已经通过一笔交易被矿工存储到了区块链上,所以发布合约实际上也是一笔交易。可以通过向全零地址发送一笔交易来创建合约,然后根据合约内容生成合约的地址用于标识合约账户,这个过程与创建外部账户不同,外部账户地址的生成是有公钥生成私钥,再由私钥计算出地址,这个过程不可逆,这也是为什么只有掌握了私钥就等于掌握了地址;但是合约账户的地址生成依赖的主要是合约代码的hash。

当我们在dapp上通过界面发出一条指令时,可能就是向某个指定的合约账户发起了一场交易,矿工收到交易消息后通过codehash找到合约代码并运行,当然这笔交易也可以向合约账户转账。矿工运行合约之后在交易的基础上收取一定的费用,之后通过挖矿将这笔交易确认后就可以获取这笔费用。

并不是说每一笔交易都必须发向某一个合约账户,也可以直接发向某个外部账户,那收到这个交易信息的矿工只需要确认这比交易是否可以成立,比如发送者简单的余额是否足够就可以,不需要去运行合约代码。

我们可以简单的吧DApp理解为一个前后端分离的项目,前端就是我们平时见过的正常的前端,但是他的后台代码是智能合约,而且二者之间的通信也不是平时我们用到的类似Axios这种工具,而是通过web3.js来进行通信。

详细文档摘要

什么是智能合约

以太坊是一个可编程的区块链应用,像比特币一样,以太坊基于分布式计算机组成的点对点网络协议。但是以太坊不像比特币一样,提供某种特定的功能,事实上,以太坊没有提供任何现成的功能,包括交易,反而是让用户在它的基础上自行编程开发功能,然后将开发好的代码存储在区块链上并提供给所有人使用,这些程序运行在以太坊虚拟机上,也就是所有以太坊的节点组成的网络上,由整个以太坊的网络提供算力,并向最终实际提供算力的节点支付费用,这些运行在以太坊虚拟机上的程序就是智能合约。

比特币网络中有大量计算机节点负责维护和更新区块链,在ETH中同样存在,被称为EVM(Eth虚拟机)。可以把EVM看作一台超级计算机,拥有网络中所有节点的计算能力,该计算能力用于在区块链上运行智能合约。EVM向用户收取非常小的ETH代币维护费作为回报,以提供智能合约需要使用的计算能力,这个费用被称为“gas”。所以ETH代币的核心意义不是作为一般等价物的货币,而是类似于石油一样作为动力驱动ETH网络。

简单的智能合约

我们通过官网的示例来进行解释

存储

1
2
3
4
5
6
7
8
9
10
11
12
13
pragma solidity ^0.4.0;

contract SimpleStorage {
uint storedData;

function set(uint x) public {
storedData = x;
}

function get() public view returns (uint) {
return storedData;
}
}

第一行就是告诉大家源代码使用Solidity版本0.4.0写的,并且使用0.4.0以上版本运行也没问题(最高到0.5.0,但是不包含0.5.0)。这是为了确保合约不会在新的编译器版本中突然行为异常。关键字 pragma 的含义是,一般来说,pragmas(编译指令)是告知编译器如何处理源代码的指令的(例如, pragma once )。

Solidity中合约的含义就是一组代码(它的 函数 )和数据(它的 状态 ),它们位于以太坊区块链的一个特定地址上。 代码行 uint storedData; 声明一个类型为 uint (256位无符号整数)的状态变量,叫做 storedData 。 你可以认为它是数据库里的一个位置,可以通过调用管理数据库代码的函数进行查询和变更。对于以太坊来说,上述的合约就是拥有合约(owning contract)。在这种情况下,函数 set 和 get 可以用来变更或取出变量的值。

要访问一个状态变量,并不需要像 this. 这样的前缀,虽然这是其他语言常见的做法。

该合约能完成的事情并不多(由于以太坊构建的基础架构的原因):它能允许任何人在合约中存储一个单独的数字,并且这个数字可以被世界上任何人访问,且没有可行的办法阻止你发布这个数字。当然,任何人都可以再次调用 set ,传入不同的值,覆盖你的数字,但是这个数字仍会被存储在区块链的历史记录中。随后,我们会看到怎样施加访问限制,以确保只有你才能改变这个数字。

注解

所有的标识符(合约名称,函数名称和变量名称)都只能使用ASCII字符集。UTF-8编码的数据可以用字符串变量的形式存储。

警告

小心使用Unicode文本,因为有些字符虽然长得相像(甚至一样),但其字符码是不同的,其编码后的字符数组也会不一样。

子货币(Subcurrency)例子

下面的合约实现了一个最简单的加密货币。这里,币确实可以无中生有地产生,但是只有创建合约的人才能做到(实现一个不同的发行计划也不难)。而且,任何人都可以给其他人转币,不需要注册用户名和密码 —— 所需要的只是以太坊密钥对。

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
pragma solidity ^0.4.21;

contract Coin {
// 关键字“public”让这些变量可以从外部读取
address public minter;
mapping (address => uint) public balances;

// 轻客户端可以通过事件针对变化作出高效的反应
event Sent(address from, address to, uint amount);

// 这是构造函数,只有当合约创建时运行
function Coin() public {
minter = msg.sender;
}

function mint(address receiver, uint amount) public {
if (msg.sender != minter) return;
balances[receiver] += amount;
}

function send(address receiver, uint amount) public {
if (balances[msg.sender] < amount) return;
balances[msg.sender] -= amount;
balances[receiver] += amount;
emit Sent(msg.sender, receiver, amount);
}
}

这个合约引入了一些新的概念,让我们逐一解读。

address public minter; 这一行声明了一个可以被公开访问的 address 类型的状态变量。 address 类型是一个160位的值,且不允许任何算数操作。这种类型适合存储合约地址或外部人员的密钥对。关键字 public 自动生成一个函数,允许你在这个合约之外访问这个状态变量的当前值。如果没有这个关键字,其他的合约没有办法访问这个变量。由编译器生成的函数的代码大致如下所示:

1
function minter() returns (address) { return minter; }

当然,加一个和上面完全一样的函数是行不通的,因为我们会有同名的一个函数和一个变量,这里,主要是希望你能明白——编译器已经帮你实现了。

下一行, mapping (address => uint) public balances; 也创建一个公共状态变量,但它是一个更复杂的数据类型。 该类型将address映射为无符号整数。 Mappings 可以看作是一个 哈希表 它会执行虚拟初始化,以使所有可能存在的键都映射到一个字节表示为全零的值。 但是,这种类比并不太恰当,因为它既不能获得映射的所有键的列表,也不能获得所有值的列表。 因此,要么记住你添加到mapping中的数据(使用列表或更高级的数据类型会更好),要么在不需要键列表或值列表的上下文中使用它,就如本例。 而由 public 关键字创建的getter函数 getter function 则是更复杂一些的情况, 它大致如下所示:

1
2
3
function balances(address _account) public view returns (uint) {
return balances[_account];
}

正如你所看到的,你可以通过该函数轻松地查询到账户的余额。

event Sent(address from, address to, uint amount); 这行声明了一个所谓的“事件(event)”,它会在 send 函数的最后一行被发出。用户界面(当然也包括服务器应用程序)可以监听区块链上正在发送的事件,而不会花费太多成本。一旦它被发出,监听该事件的listener都将收到通知。而所有的事件都包含了 from , to 和 amount 三个参数,可方便追踪事务。 为了监听这个事件,你可以使用如下代码:

1
2
3
4
5
6
7
8
9
10
Coin.Sent().watch({}, '', function(error, result) {
if (!error) {
console.log("Coin transfer: " + result.args.amount +
" coins were sent from " + result.args.from +
" to " + result.args.to + ".");
console.log("Balances now:\n" +
"Sender: " + Coin.balances.call(result.args.from) +
"Receiver: " + Coin.balances.call(result.args.to));
}
})

这里请注意自动生成的 balances 函数是如何从用户界面调用的。

特殊函数 Coin 是在创建合约期间运行的构造函数,不能在事后调用。 它永久存储创建合约的人的地址: msg (以及 tx 和 block ) 是一个神奇的全局变量,其中包含一些允许访问区块链的属性。 msg.sender 始终是当前(外部)函数调用的来源地址。

最后,真正被用户或其他合约所调用的,以完成本合约功能的方法是 mint 和 send。 如果 mint 被合约创建者外的其他人调用则什么也不会发生。 另一方面, send 函数可被任何人用于向他人发送币 (当然,前提是发送者拥有这些币)。记住,如果你使用合约发送币给一个地址,当你在区块链浏览器上查看该地址时是看不到任何相关信息的。因为,实际上你发送币和更改余额的信息仅仅存储在特定合约的数据存储器中。通过使用事件,你可以非常简单地为你的新币创建一个“区块链浏览器”来追踪交易和余额。

以太坊虚拟机

概述

以太坊虚拟机 EVM 是智能合约的运行环境。它不仅是沙盒封装的,而且是完全隔离的,也就是说在 EVM 中运行代码是无法访问网络、文件系统和其他进程的。甚至智能合约之间的访问也是受限的。

账户

以太坊中有两类账户(它们共用同一个地址空间): 外部账户 由公钥-私钥对(也就是人)控制; 合约账户 由和账户一起存储的代码控制.

外部账户的地址是由公钥决定的,而合约账户的地址是在创建该合约时确定的(这个地址通过合约创建者的地址和从该地址发出过的交易数量计算得到的,也就是所谓的“nonce”)

无论帐户是否存储代码,这两类账户对 EVM 来说是一样的。

每个账户都有一个键值对形式的持久化存储。其中 key 和 value 的长度都是256位,我们称之为 存储

此外,每个账户有一个以太币余额( balance )(单位是“Wei”),余额会因为发送包含以太币的交易而改变。

交易

交易可以看作是从一个帐户发送到另一个帐户的消息(这里的账户,可能是相同的或特殊的零帐户,请参阅下文)。它能包含一个二进制数据(合约负载)和以太币。

如果目标账户含有代码,此代码会被执行,并以 payload 作为入参。

如果目标账户是零账户(账户地址为 0 ),此交易将创建一个 新合约 。 如前文所述,合约的地址不是零地址,而是通过合约创建者的地址和从该地址发出过的交易数量计算得到的(所谓的“nonce”)。 这个用来创建合约的交易的 payload 会被转换为 EVM 字节码并执行。执行的输出将作为合约代码被永久存储。这意味着,为创建一个合约,你不需要发送实际的合约代码,而是发送能够产生合约代码的代码。

注解

在合约创建的过程中,它的代码还是空的。所以直到构造函数执行结束,你都不应该在其中调用合约自己函数。

Gas

一经创建,每笔交易都收取一定数量的 gas ,目的是限制执行交易所需要的工作量和为交易支付手续费。EVM 执行交易时,gas 将按特定规则逐渐耗尽。

gas price 是交易发送者设置的一个值,发送者账户需要预付的手续费= gas_price * gas 。如果交易执行后还有剩余, gas 会原路返还。

无论执行到什么位置,一旦 gas 被耗尽(比如降为负值),将会触发一个 out-of-gas 异常。当前调用帧(call frame)所做的所有状态修改都将被回滚。

那么可能就会有一个问题,如果gas price是交易发送者自己制定的,那不是越低越省钱?每个人都不想多付钱。要解决这个问题首先要搞明白三个概念:

Gas

Gas对应于一个交易(Transaction)中以太坊虚拟机(EVM)的实际运算步数。 越简单的交易,例如单纯的

以太币转帐交易,需要的运算步数越少, Gas亦会需要的少一点。 反之,如果要计算一些复杂运算,Gas的消耗

量就会大。 所以你提交的交易需要EVM进行的计算量越大,所需的Gas消耗量就越高了。

Gas Price

Gas Price就是你愿意为一个单位的Gas出多少Eth,一般用Gwei作单位。 所以Gas Price 越高,

就表示交易中每运算一步,会支付更多的Eth。

大家可能对Gwei 这个单位感到陌生,Gwei 其实就是10 ^ -9 Eth,也就是说1 Gwei = 0.000000001 Eth。

所以,当你设定Gas price = 20 Gwei ,就意味着你愿意为单步运算支付0.00000002 Eth。

说到这里,聪明如你就会意识到以太坊的手续费计算公式很简单:

交易手续费(Tx Fee) = 实际运行步数(Actual Gas Used) * 单步价格(Gas Price)

例如你的交易需要以太坊执行50步完成运算,假设你设定的Gas Price是2 Gwei ,那么整个交易的手续费

就是50 * 2 = 100 Gwei 了。

Gas Limit

Gas Limit就是一次交易中Gas的可用上限,也就是你的交易中最多会执行多少步运算。 由于交易复杂程度各有不同,

确切的Gas消耗量是在完成交易后才会知道,因此在你提交交易之前,需要为交易设定一个Gas用量的上限。

如果说你提交的交易尚未完成,消耗的Gas就已经超过你设定的Gas Limit,那么这次交易就会被取消,而

已经消耗的手续费同样被扣取 —— 因为要奖励已经付出劳动的矿工。 而如果交易已经完成,消耗的Gas未达到Gas Limit,

那么只会按实际消耗的Gas 收取交易服务费。 换句话说,一个交易可能被收取的最高服务费就是Gas Limit * Gas Price 了。

最后值得一提的是Gas Price 越高,你提交的交易会越快被矿工接纳。 但通常人们都不愿多支付手续费

委托调用/代码调用和库

有一种特殊类型的消息调用,被称为 委托调用(delegatecall) 。它和一般的消息调用的区别在于,目标地址的代码将在发起调用的合约的上下文中执行,并且 msg.sender 和 msg.value 不变。 这意味着一个合约可以在运行时从另外一个地址动态加载代码。存储、当前地址和余额都指向发起调用的合约,只有代码是从被调用地址获取的。 这使得 Solidity 可以实现”库“能力:可复用的代码库可以放在一个合约的存储上,如用来实现复杂的数据结构的库。

日志

有一种特殊的可索引的数据结构,其存储的数据可以一路映射直到区块层级。这个特性被称为 日志(logs) ,Solidity用它来实现 事件(events) 。合约创建之后就无法访问日志数据,但是这些数据可以从区块链外高效的访问。因为部分日志数据被存储在 布隆过滤器(Bloom filter) 中,我们可以高效并且加密安全地搜索日志,所以那些没有下载整个区块链的网络节点(轻客户端)也可以找到这些日志。

创建

合约甚至可以通过一个特殊的指令来创建其他合约(不是简单的调用零地址)。创建合约的调用 create calls 和普通消息调用的唯一区别在于,负载会被执行,执行的结果被存储为合约代码,调用者/创建者在栈上得到新合约的地址。

自毁

合约代码从区块链上移除的唯一方式是合约在合约地址上的执行自毁操作 selfdestruct 。合约账户上剩余的以太币会发送给指定的目标,然后其存储和代码从状态中被移除。

重点概念再次解释

账户

以太坊引入了账户的概念取代比特币UTXO模型。以太坊中有两类账户,外部账户和合约账户,两类账户对于EVM来说没有区别。每个账户都有一个与之关联的账户状态和一个20字节地址,都可以存储以太币。

外部账户:由私钥控制,没有代码与之关联,地址由公钥决定。私钥可用于对交易签名从而主动向其他账户发起交易(transaction)进行消息传递,

合约账户:由合约代码控制,有代码与之关联,其地址由合约创建者的地址和该地址发出过的交易数量nonce共同决定。不可以主动向其他账户发起交易,但可以“响应”其他账户进行消息调用(message call)。

外部账户之间的消息传递是价值转移的过程,外部账户到合约账户的交易或合约账户到合约账户的消息会激发合约账户代码的执行,允许它执行如转移代币,写入内部存储,执行运算,创建合约等各种操作。

账户状态

不论账户类型,账户状态都包含以下四个字段:

nonce:随机数,账户发出的交易数及创建的合约数量之和。

Balance:余额,账户拥有以太币数量,单位为Wei,1Ether=10^18Wei。

storageRoot:存储根节点,账户内容的MerklePatricia 树根节点的哈希编码。

codeHash:代码哈希,与账户关联的EVM代码的哈希值,外部账户的codeHash为一个空字符串的哈希,创建后不可更改。状态数据库中包含所有代码片段哈希, 以便后续使用。

img

交易(Transaction)

外部账户向其他账户发送签名数据包。每一笔交易都会改变以太坊的状态,都将被序列化,经矿工验证广播后记入区块链,因此,交易是异步的,可以即时返回的值只有transaction hash。交易可以分为创建合约和传递消息两类。一个交易的完成可能会需要触发多条消息及消息调用。

交易包含:

∙交易的接收者

∙可识别交易发送者、证明这是一笔发送者通过区块链发送到接收者的交易的签名

∙ VALUE,需转移的以太币数量(wei)

∙ Gas Limit(有时被称为StartGas),允许交易执行时消耗的最大gas数量

∙ Gas Price,交易发送者指定的单位gas的价格(用以太币计算)

消息(Message)

两个账户间传递的数据和值(以太币)。不一定会改变以太坊的状态,只存在于以太坊执行环境的虚拟对象,不会被序列化也不会被记入区块链,消息是同步的,可以即时得到返回值。

消息调用(Message Call)

将消息从一个帐户传递到另一个帐户的行为,调用形式类似Transaction,但是只存在于以太坊执行环境的虚拟对象不会被记入区块链,可以类比函数调用。如果目标账户是合约账户,则合约账户的EVM代码被激发执行,如果两个账户都是合约账户,则调用中可以传递所有虚拟机的返回值。

消息包含:

∙消息的发送者(隐式的)

∙消息的接收者

∙ VALUE,随消息传递到合约地址的以太币数量(wei)

∙可选数据字段,作为合约的输入

∙ STARTGAS,用来限制这个消息触发的代码执行能消耗的最大gas数量

消息调用和消息通常同义,没有必要严格区分。

交易和消息不是包含关系,而是部分重合关系:交易发送者不经过合约直接发送交易到以太坊指定地址创建合约的操作,没有消息调用的过程,只属于交易;交易发送者通过调用合约从一个账户向另一个账户转账的操作,既属于交易又属于消息调用;合约账户受到外部账户激发而进行创建合约的操作,只属于消息调用不属于交易。

交易一定是由外部账户发起,一个交易可能会引发一系列“消息调用”,合约账户为“响应”来自其他账户的“消息调用”而执行代码继而激发新的“消息调用”,因此,本质上所有“消息调用”及以太坊的状态改变都是由外部账户激发,即,以太坊在整体上可以看作一个基于交易的状态机:起始于一个创世块(Genesis)状态,然后随着交易的执行状态逐步改变一直到最终状态, 这个最终状态是以太坊世界的权威版本。

题外话

全零地址

以太坊中有个地址拥有非常多的以太币,这个地址就是全零地址,那么这个地址为什么会有这么多的以太币呢?

这就要从以太坊的创世区块讲起。

ETH最主要的来源并不是挖矿,而是当年众筹时发行的。

即使在以太坊正式发布近三年后的今天,创世区块中发行的7200w个ETH依然占到73.4%。

那么,这个全零地址中的ETH,是不是来自创世区块中发行的ETH呢?

实际上并不是的。

然而,解析下创世区块,可以发现,挖出创世区块的miner,居然是这个全零的地址。

创世区块不是被挖出来的,它是被人为创建出来,并作为整条区块链的起点。

既然不是被挖出来的,使用全零的地址作为占位符也是合理的。

创世区块因为不是挖矿所得,无人为此消耗算力,自然也不会有任何区块奖励。

转了一圈,这个全零地址中的ETH和创世区块并无关系。

除了创世区块中发行的ETH,新的ETH只有一种产生途径,那就是挖矿。

为了更加精确的计算,以下单位均使用ETH的最小单位Wei,关于单位问题,可以参看这里《以太币(Ether)的单位》。

搜索下miner为这个全零地址的区块,还真不少呢。

这个全零地址共挖出主链区块94个:

那么问题来了,为什么有人会用全零地址挖矿,这样自己消耗了电力,最终获益的却是全零地址。

全零地址最早挖出的一个区块是5305,难度值仅有199,485,740,316,不到0.2T,还是1060显卡为例,仅需要不到3小时即可发现一个区块。

那时候,区块是如此容易挖掘,几乎任何一台有独立显卡的中端PC,都可以挖到区块。

于是,很多人本着试试看的心态,使用自家的普通PC尝试挖掘,几乎0成本。

这群人中,有相当一部分是仅会复制粘贴的小白,悲催的事儿来了,他们忘记设置收款地址。有些专业矿工在调试机器稳定性时,也会出于方便,没有对挖矿软件进行配置。

部分钱包软件在solo挖矿时,如果不设置收款地址,就会默认使用全零地址挖矿。

这也就解释了,为什么会有如此多的区块是全零地址挖到的。

而且要注意的是,全零地址是一个黑洞地址,没有一个人手中掌握了全零地址的私钥,我们也无法通过全零来反向推算出全零地址的私钥,所以理论上来说,进入了黑洞地址的以太币再也无法转出了。

智能合约如何运行

比特币交易非常简单。你可以只做一件事。一种类型的交易。略过一些细节,一切都可以归结为TO (付给,谁在收钱),FROM (来自于,谁在付钱)和 AMOUNT(数量,多少钱)。这使得比特币成为一种价值储存手段,能够在网络参与者之间传递价值。

以太坊的不同之处在于交易还有一个「DATA」(数据)字段。该「DATA」字段支持三种类型的交易:

价值转移(和比特币一样)

  • TO接收地址;
  • DATA字段为空或包含要附加的任何消息;
  • FROM
  • AMOUNT 是你要发送的以太坊数量

创建智能合约

  • TO 字段为空(它触发智能合约的创建)
  • DATA 字段包含编译为字节代码的智能合约代码(所以实际上我们编写的代码是用来创建合约代码的,而不是真正存储到链上的合约代码)
  • FROM 你
  • AMOUNT 可以是 0 或者任意你想放在合约里的以太币数量

调用智能合约

  • TO 字段是智能合约账户地址
  • DATA 字段包含函数名称和参数 —— 如何调用智能合约
  • FROM 你
  • AMOUNT 可以是 0 或者任何数量的以太币,比如你需要为一项服务合约支付的数目

合约与合约账户

合约是代码与账户当前状态的一个集合,每次交易之后,当前的合约都会

参考文章:

https://blog.csdn.net/sportshark/article/details/52249607

https://solidity-cn.readthedocs.io/zh/develop/solidity-by-example.html

https://learnblockchain.cn/2018/01/04/understanding-smart-contracts/

https://blog.csdn.net/jiang_xinxing/article/details/80289694

https://zhuanlan.zhihu.com/p/34363341

https://www.chainnews.com/articles/891681682372.htm