写在前面:HiBlock区块链社区成立了翻译小组(以太坊中文社区),翻译区块链相关的技术文档及资料,本文为solidity官方文档翻译的第一部分《智能合约概述》,特发布出来邀请solidity爱好者、开发者作公开的审校,您能够添加微信baobaotalk_com,验证输入“solidity”,而后将您的意见和建议发送给咱们,也能够在文末“留言”区留言,有效的建议咱们会采纳及合并进下一版本,同时将送一份小礼物给您以示感谢。html
让咱们先看一下最基本的例子。如今就算你都不理解也没关系,后面咱们会有更深刻的讲解。程序员
存储数据库
pragma solidity ^0.4.0;浏览器
contract SimpleStorage { uint storedData;安全
function set(uint x) public { storedData = x; }服务器
function get() public constant 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 ,传入不一样的值,覆盖你的数字,可是这个数字仍会被存储在区块链的历史记录中。随后,咱们会看到怎样施加访问限制,以确保只有你才能改变这个数字。
子货币(Subcurrency)例子
下面的合约实现了一个最简单的加密货币。这里,币确实能够无中生有地产生,可是只有建立合约的人才能作到(实现一个不一样的发行计划也不难)。并且,任何人均可以给其余人转币,不须要注册用户名和密码 —— 所须要的只是以太坊密钥对。
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 自动生成一个函数,容许你在这个合约以外访问这个状态变量的当前值。若是没有这个关键字,其余的合约没有办法访问这个变量。由编译器生成的函数的代码大体以下所示:
function minter() returns (address) { return minter; }
固然,加一个和上面彻底同样的函数是行不通的,由于咱们会有同名的一个函数和一个变量,这里,主要是但愿你能明白——编译器已经帮你实现了。
下一行,
mapping (address => uint) public balances;
也建立一个公共状态变量,但它是一个更复杂的数据类型。 该类型将address映射为无符号整数。 Mappings 能够看做是一个 哈希表 它会执行虚拟初始化,以使全部可能存在的键都映射到一个字节表示为全零的值。 可是,这种类比并不太恰当,由于它既不能得到映射的全部键的列表,也不能得到全部值的列表。 所以,要么记住你添加到mapping中的数据(使用列表或更高级的数据类型会更好),要么在不须要键列表或值列表的上下文中使用它,就如本例。 而由 public 关键字建立的getter函数 getter function 则是更复杂一些的状况, 它大体以下所示:
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 三个参数,可方便追踪事务。 为了监听这个事件,你可使用以下代码:
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 函数可被任何人用于向他人发送币 (固然,前提是发送者拥有这些币)。记住,若是你使用合约发送币给一个地址,当你在区块链浏览器上查看该地址时是看不到任何相关信息的。由于,实际上你发送币和更改余额的信息仅仅存储在特定合约的数据存储器中。经过使用事件,你能够很是简单地为你的新币建立一个“区块链浏览器”来追踪交易和余额。
对于程序员来讲,区块链这个概念并不难理解,这是由于大多数难懂的东西 (挖矿, 哈希 ,椭圆曲线密码学 ,点对点网络(P2P) 等) 都只是用于提供特定的功能和承诺。你只需接受这些既有的特性功能,没必要关心底层技术,好比,难道你必须知道亚马逊的 AWS 内部原理,你才能使用它吗?
交易/事务
区块链是全球共享的事务性数据库,这意味着每一个人均可加入网络来阅读数据库中的记录。若是你想改变数据库中的某些东西,你必须建立一个被全部其余人所接受的事务。事务一词意味着你想作的(假设您想要同时更改两个值),要么一点没作,要么所有完成。此外,当你的事务被应用到数据库时,其余事务不能修改数据库。
举个例子,设想一张表,列出电子货币中全部帐户的余额。若是请求从一个帐户转移到另外一个帐户,数据库的事务特性确保了若是从一个帐户扣除金额,它总被添加到另外一个帐户。若是因为某些缘由,没法添加金额到目标帐户时,源帐户也不会发生任何变化。
此外,交易老是由发送人(建立者)签名。
这样,就可很是简单地为数据库的特定修改增长访问保护机制。 在电子货币的例子中,一个简单的检查能够确保只有持有帐户密钥的人才能从中转帐。
区块
在比特币中,要解决的一个主要难题,被称为“双花攻击 (double-spend attack)”:若是网络存在两笔交易,都想花光同一个帐户的钱时(即所谓的冲突)会发生什么状况?交易互相冲突?
简单的回答是你没必要在意此问题。网络会为你自动选择一条交易序列,并打包到所谓的“区块”中,而后它们将在全部参与节点中执行和分发。若是两笔交易互相矛盾,那么最终被确认为后发生的交易将被拒绝,不会被包含到区块中。
这些块按时间造成了一个线性序列,这正是“区块链”这个词的来源。区块以必定的时间间隔添加到链上 —— 对于以太坊,这间隔大约是17秒。
做为“顺序选择机制”(也就是所谓的“挖矿”)的一部分,可能会发生这样的状况:块不时地被回滚,但只发生在区块链的“末端”。在末端涉及回滚区块越多,其发生的几率越小。因此你的交易有可能被回滚,甚至从区块链中抹除,但你等待的时间越长,这种状况发生的几率就越小。
概述
以太坊虚拟机 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 异常。当前调用帧所作的全部状态修改都将被回滚。
存储,内存和栈
每一个帐户有一块持久化内存区被称为 存储。 存储是一个 key-value 的键值对 ,其存储着一个由256位的键到256位的值的映射. 在合约中,不能枚举户中的存储,且存储的读操做相对开销高,修改存储开销更高。一个合约只能对它本身的存储进行读写。
第二个内存区称为 内存,合约会试图为每一次消息调用获取一块被从新擦拭干净的内存实例。 内存是线性的,可按字节级寻址,但读的长度被限制为256位,而写的长度能够是8位或256位。当访问(不管是读仍是写)以前从未访问过的内存字(word)时(不管是偏移到该字内的任何位置),内存将按字进行扩展(每一个字是256 bit)。扩容也将消耗必定的gas。 内存越大,费用就越高(平方级别)。
EVM 不是基于寄存器的,而是基于栈的,所以全部的计算都在一个被称为 stack 的区域执行。 栈最大有1024个元素,每一个元素长度是一个字(256 bit)。对栈的访问只限于其顶端,限制方式为:容许拷贝最顶端的16个元素中的一个到栈顶,或者是交换栈顶元素和下面16个元素中的一个。全部其余操做都只能取最顶的两个(或一个,或更多,取决于具体的操做)元素,运算后,把结果压入栈顶。固然能够把栈上的元素放到存储或内存中。可是没法只访问栈上指定深度的那个元素,除非先从栈顶移除其余元素。
指令集
EVM的指令集量应尽可能少,以最大限度地避免可能致使共识问题的错误实现。全部的指令都是针对"256位的字(word)"这个基本的数据类型来进行操做。具有经常使用的算术、位、逻辑和比较操做。也能够作到有条件和无条件跳转。此外,合约能够访问当前区块的相关属性,好比它的编号和时间戳。
消息调用
合约能够经过消息调用的方式来调用其它合约或者发送以太币到非合约帐户。消息调用和交易很是相似,它们都有一个源、目标、数据、以太币、gas和返回数据。事实上每一个交易都由一个顶层消息调用组成,这个消息调用又可建立更多的消息调用。
合约能够决定在其内部的消息调用中,对于剩余的 gas,应发送和保留多少。若是在内部消息调用时发生了out-of-gas异常(或其余任何异常),这将由一个被压入栈顶的错误值所指明。此时,只有与该内部消息调用一块儿发送的gas会被消耗掉。而且,Solidity中,发起调用的合约默认会触发一个手工的异常,以便异常能够从调用栈里“冒泡出来”。 如前文所述,被调用的合约(能够和调用者是同一个合约)会得到一块刚刚清空过的内存,并能够访问调用的payload——由被称为 calldata 的独立区域所提供的数据。调用执行结束后,返回数据将被存放在调用方预先分配好的一块内存中。 调用深度被 限制 为 1024 ,所以对于更加复杂的操做,咱们应使用循环而不是递归。
委托调用/代码调用和库
有一种特殊类型的消息调用,被称为 委托调用(delegatecall)。它和通常的消息调用的区别在于,目标地址的代码将在发起调用的合约的上下文中执行,而且 msg.sender 和 msg.value 不变。 这意味着一个合约能够在运行时从另一个地址动态加载代码。存储、当前地址和余额都指向发起调用的合约,只有代码是从被调用地址获取的。 这使得 Solidity 能够实现”库“能力:可复用的代码库能够放在一个合约的存储上,如用来实现复杂的数据结构的库。
日志
有一种特殊的可索引的数据结构,其存储的数据能够一路映射直到区块层级。这个特性被称为 日志(logs),Solidity用它来实现 事件(events)。合约建立以后就没法访问日志数据,可是这些数据能够从区块链外高效的访问。由于部分日志数据被存储在 布隆过滤器(Bloom filter) 中,咱们能够高效而且加密安全地搜索日志,因此那些没有下载整个区块链的网络节点(轻客户端)也能够找到这些日志。
建立
合约甚至能够经过一个特殊的指令来建立其余合约(不是简单的调用零地址)。建立合约的调用 create calls 和普通消息调用的惟一区别在于,负载会被执行,执行的结果被存储为合约代码,调用者/建立者在栈上获得新合约的地址。
自毁
合约代码从区块链上移除的惟一方式是合约在合约地址上的执行自毁操做 selfdestruct 。合约帐户上剩余的以太币会发送给指定的目标,而后其存储和代码从状态中被移除。
注:本文为solidity翻译的第一部分《智能合约概述》,特发布出来邀请solidity爱好者、开发者作公开的审校,您能够添加微信baobaotalk_com,验证输入“solidity”,而后将您的意见和建议发送给咱们,也可在文末“留言”区留言,或经过原文连接访问咱们的Github。有效的建议咱们会收纳并及时改进,同时将送一份小礼物给您以示感谢。
点击“阅读原文”便可查看翻译原文