(全文参考)
Solidity官方文档
以太坊白皮书_ZH
以太坊白皮书_EN
发现网上的资料太过琐碎, 惊奇的发现官方有详细的教程, 和例子.
虽说, 是E文, 自己慢慢啃吧,也算是半学习半翻译吧
对说区块链一定形式上是分布式数据库的理解 引用
pragma solidity ^0.4.0;
contract SimpleStorage {
uint storedData;
function set(uint x) {
storedData = x;
}
function get() constant returns (uint) {
return storedData;
}
}
第一行告诉该合约用的是0.4.0版本的solidity编写,并且这些代码具有向上兼容性。保证不会在不同solidity编译版本下编译会出现不同的行为。
从Solidity角度来看,合约就是存在于以太坊区块链中的一个特定地址中的代码和数据集合。uint storedData 声明了一个类型为 uint(256位的无符号整型)的变量,变量名称为 storedData。你可以把它想象为数据库中的一个字段,该字段是可以被数据库中的方法进行查询,修改。在以太坊中,这个字段是属于一个合约字段。在这个例子中,该变量可以通过提供的get,set方法进行获取或是修改。 在Solidity中,访问一个状态变量是不需要通过this来引用的。
这个合约很简单,只是允许以太坊上的任何人来存储一个数据到某个节点,同时把这个操作发布到以太坊中,当然,以太坊上的其他节点同样可以通过调用set方法来修改你已经存储好的值。虽然有被修改,但是对该值操作的任何历史记录都是保存在以太坊中的。不用担心你的存储记录或是修改记录会丢失。后面我们会将到如何对合约进行限制,只允许你一个人修改这个数据
这个例程是用于实现一个简单的加密货币, 这里种币的无中生有是可能的. 但是,只有它的合约创建人, 才可以实现这个币的签发. 不过还好发送币到其他的地址, 不需要注册账户, 只是一个 ETH 密钥对就好
pragma solidity ^0.4.0;
contract Coin {
// The keyword "public" makes those variables
// readable from outside.
address public minter;
mapping (address => uint) public balances;
// Events allow light clients to react on
// changes efficiently.
event Sent(address from, address to, uint amount);
// This is the constructor whose code is
// run only when the contract is created.
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;
Sent(msg.sender, receiver, amount);
}
}
这个合约包含着一些新的概念, 下面一个个的明确一下
address public minter;
这里声明了一个地址类型的变量, 其访问类型是Public. address 类型是一个没有算数操作符的160位的值. 它用来存储合约地址,或者外部用户的密钥对. public
关键字自动的使得该变量是可以被外部访问的. 没有 Public
的话,其他合约是无法访问这个变量的
function minter() returns (address) { return minter; }
例如执行这个函数. 当然, 添加这样的一个函数可能是没用的, 因为他的名称和我们之前已经声明的变量一样了,不过编译器将会支出问题, 不慌…
下面这一行
mapping (address => uint) public balances;
也是创建了一个公有变量, 但是这个是一种比 address 更复杂的数据类型. 这种类型建立了 address 和 unsigned int 的映射关系.
可以想象是一个 哈希表 被初始化为, 每一个已经存在的键,都和一个字节数据全是零的值相对应. 他们是成对出现的, 所以不可能得到全部是键, 或者全部是值的 一个list. (….这段好鬼复杂,后面看不懂了)
在这种情况下, getter 函数 将会被Public这个关键字自动创建, 看着和下面类似
function balances(address _account) public view returns (uint) {
return balances[_account];
}
正如你所见, 你可以使用这种方法 很容易的查询一个账户的余额
event Sent(address from, address to, uint amount);
正如event这个名字一样, 这行创建了一个事件 , 而他会在最后一行的 Send
函数触发. 当这些事件在链上被触发的时候, 用户接口(或者服务器应用)可以监听这些事件而并不需要花费很多(GAS) .
当事件被触发的呼唤, 监听这同时会收到, 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));
}
}) // watch 右括号
注意自动生成的balance
函数式怎么被用户接口调用的
这在这个结构中特定的 coin
函数,是在这个合约创建的时候执行的, 而不可以在以后调用.
所以这个函数用于永久的保存,合约部分属性. 通过msg 这个全局变量( 连同一起保存的 有 tx and block
).
例如: 无论从哪里调用合约函数, msg.Sender 即合约创建者的地址
最终的, 合约中真正完成功能的,并且可以被其他合约和用户调用的是 mint
和 send
这两个函数.
如果 mint
被非合约创建者(地址,用户) ,什么都不会发生[合约中代码可见, 直接返回了].
另一方面, send
可以被任何人(只要之前拥有这个子币的) 用于发送到其他的地址上去.
注意 ,如果你使用这份合约来发送代币到其他的地址, 你通过区块浏览器是什么也看不到的, 因为事实上你所发送的代币,和余额的变化, 只是储存在特定的代币合约的数据字段, 通过 事件的使用 ,就可以相当容易的创建一个区块浏览器, 来跟踪这个新的代币的余额和交易了
(事实上 通过第三方网站ETHSCAN好像可以添加新的token)
区块链其实作为一个概念理解起来编程实现并不难. 因为大多数的难题(complication)(挖矿, 哈希, 椭圆曲线加密, 点对点网络等) 都只是提供了一些概念和特性. 一旦你了解了这些特性, 就不用去理解其深层次的具体实现, 比如你使用 亚马逊的 AWS 云, 完全不用了解他的内部网络实现对吧?
区块链是一个全球的 共享交易数据库. 这就意味着, 每个人加入了这个网络,就可以读取整个网络的数据. 这样的话, 你如果需要改变一点内容, 那你就需要创建一个被其他所有人接受的所谓的交易(transaction). 交易这个词暗示着要不就是没有,要不就是被全部应用(全部节点记录).而且, 当你的交易被记录在数据库中了,就没有人能改变它.
举个例子, 想象有一个表, 列出了所有的用户的一直数字货币的余额. 如果有一个转账请求, 数据库实际上,就是从这一个账户的余额减去(Subtract)值, 另一个账户增加值.如果因为一些原因, 在另一账户增加余额不可行, 那么原账户也是不会改变的
而且, 一次交易都会被发送者进行数字签名(RSA 体系下,即用自己的秘钥去加密内容,别人用公钥验证签名).这就简单的保证了, 合法的数据库修改. 在数字货币中这个简单的检查,确保了用于了账户的秘钥(私钥)的人,才能从账户转账
在比特币体系中, 一个主要的要克服的障碍(obstacle)是 双花攻击(Double-spend).发生在当一个当网络中的两笔交易想同时清空同一个账号的余额会发生什么?也许一个冲突?(这里可能是指的同时的进行a 对 b , a对 c 的转账)
理论上(abstract)你是不必要关心这个问题的. 你的交易将会有一个特定的顺序, 这些交易将会被打包(bundle)在一个叫做区块的东西里, 然后区块将会被处理(execute)和分发到所有的在线节点上. 如果两个交易互相冲突(contradict), 后来的那个将会被节点拒绝, 并且不会成为节点的一部分
这些区块按时间的先后组成了一个线性的序列, 所有这也是 `区块链` 这个词的出处. 新的区块被加入链中的时间间隔(intervals)差别不大, 以以太坊为例, 大约每17秒一个.
作为"序列排序机"(`order selection mechanism`)的一部分(被称之为挖矿的行为), 可能发生,区块回滚(reverted). 但是仅仅会在区块的链尾(tip),. 随着更多的区块产生, 回滚的可能性也越小. 所以, 你的交易数据可能会被回滚, 甚至从区块链中移除(..这点没懂, 是不是叔块和重链的原因?)
以太坊虚拟机(EVM) 是以太坊的智能合约的运行时环境(runtime environment).它不仅仅是一个沙箱, 并可以实现代码运行和网络, 文件系统, 其他进程 的完全隔离. 智能合约之间甚至也可以互相隔离.
在以太坊中有两种账户共享同一地址空间,
合约账户 被账号存储的代码所控制
外部账户 是被公钥所确定的, 而合约账户是在合约创建时确定, (合约账户产生于(derive from) 合约创建者的地址,而且创建者在其中转入一些余额叫做nonce
)
不论是否账户储存了代码, 这两种类型的账户都是被EVM 等同的对待.
一次交易,是一个消息,从一个账户发送到另一个账户(可能是自身, 或者特殊的黑洞账户(zero-account), 见下), 消息可以包含二进制数据(payload)和ETHer(以太币)
如果目标账户包含代码, 并且代码被执行,那么这些数据将会被视为执行的输入数据
如果目标账户是零账户(zero-account)(地址是 0 的账户), 这个交易将会创建一个新的合约. 正如上面提到的, 新的合约地址不是 0 , 而是从创建者的账户的交易发送产生. 这个合约所包含的载荷(payload)被转换为EVM的操作码, 并且执行. 执行的输出将会永久的保存在合约的代码里.
这就意味着为了创建一个合约, 你不需要发送真正的代码到合约, 使用的仅仅是这个合约执行的返回值
(这个感觉很重要, 因为之前部署的命令没看懂, 这个就想到于保存了句柄对吧?)
(个人理解, 就是合约创建时候的结果将会执行,并保存)
每一次交易都会包含一定量的GAS.其目的是限制转账或者合约执行的工作量. 当EVM 执行交易的时候, GAS会被按照一定规则的消耗(deplete)
gas的值是可以被交易的发起者所设置的, 发起者需要支付 ` gas_price * gas ` 的量.如果在执行交易后有GAS剩余, 那么也将会返还到发起者的账户上.
如果gas被耗尽,(等同于(i.e.) it is negative), 一个 `out-of-gas` 的异常将会被抛出, 并且由此次交易发生的所有改变将会被回滚.
每一个用户,都会有自己固定的内存空间, 称之为存储(storge). 储存是一个 256bit映射256bit 的键值对.通过合约去列举存储是不可能的, 因为它所消耗的代价是很大的.一个合约只能做到,读或者写它自己的存储区域内容
第二存储区是被称为内存的地方, 每一个合约每一次消息调用都拥有一个,新的被清空的实例(instance).内存是线性的,并可以做到字节等级的寻址, 但是读取位宽限制在 256bit ,同时,写入宽度是 8b 或者 256b.内存拓展是以字长为单位(256b). 当访问一个之前不可达(untouched)的内存字的时候, 一定量的gas 将会被消耗.内存增长和gas消耗是呈指数的关系的.
EVM 不是基于寄存器的, 而是基于栈的 机器, 所以所有的计算是通过一个称之为栈(stack)的东西执行的.其最大值是1024个元素(element),每个元素256bit.如栈的结构, 只可被顶端访问, 并且遵循如下规则: 允许拷贝最上面的16个元素中的一个到栈顶或是栈顶和它下面的16个元素中的一个进行交换。所有其他操作会从栈中取出两个(有可能是1个,多个,取决于操作)元素,把操作结果在放回栈中。当然也有可能把栈中元素放入到存储或是主存中,但是不可能在没有移除上层元素的时候,随意访问下层元素。
EVM指令集保留最小序列,用于避免不正确不完整的不一致问题. 所有的指令基于基础数据类型 ,256b word. 可以实现常规的算术, 位 及逻辑运算. 条件和无条件跳转也是OK的.而且(furthermore),合约可以访问当前块的相关属性, 像是 序号(number) 时间戳(timestamp)
合约可以调用其他的合约 或者发送以太币到其他的账户通过消息调用的功能.消息调用和交易类似, 一样的拥有 源(source), 目标(target), 载荷(payload), 以太币, gas, 和返回数据.事实上, 每一次交易都是由顶层的消息调用,继而创建更多的调用.
一个合约可以定义内部消息调用需要消耗多少gas,多少gas需要被保留。如果在内部消息调用中出现out-of-gas异常,合约会被通知,会在栈里用一个错误值来标记。这种情况只是这次调用的gas被消耗完。在Solidity,这种情况下调用合约会引起一个人为异常,这种异常会抛出栈的信息。
上面提到,调用合约会被分配到一个新的,并且是清空的主存,并能访问调用的负载。调用负载时被称为calldata的一个独立区域。调用结束后,返回一个存储在调用主存空间里的数据。这个存储空间是被调用者预先分配好的。调用限制的深度为1024.对于更加复杂的操作,我们更倾向于使用循环而不是递归。
存在一种特殊的消息调用,叫做代理调用。除了目标地址的代码在调用方的上下文中被执行,而且msg.sender和msg.value不会改变他们的值,其他都和消息调用一样。这就意味着合约可以在运行时动态的加载其他地址的代码。存储,当前地址,余额都和调用合约有关系。只有代码是从被调用方中获取。这就使得我们可以在Solidity中使用库。比如为了实现复杂的数据结构,可重用的代码可以应用于合约存储中。
我们可以把数据存储在一个特殊索引的数据结构中。这个结构映射到区块层面的各个地方。为了实现这个事件,在Solidity把这个特性称为日志。合约在被创建出来后是不可以访问日志数据的。但是他们可以从区块链外面有效的访问这些数据。因为日志的部分数据是存储在bloom filters上。我们可以用有效并且安全加密的方式来查询这些数据。即使不用下载整个区块链数据(轻客户端)也能找到这些日志
合约可以通过特殊的指令来创建其他合约。这些创建调用指令和普通的消息调用唯一区别是:负载数据被执行,结果作为代码被存储,调用者在栈里收到了新合约的地址。
从区块链中移除代码的唯一方法是合约在它的地址上执行了selfdestruct操作。这个账号下剩余的以太币会发送给指定的目标,存储和代码从栈中删除。
终于算是差不多的自己翻译了第一篇文章, 翻译的同事夜场学到了不少东西, 具体后面的EVM的架构理解还是需要自己深入研究一下, 翻译质量很差,我自己都快看不懂了...
自己算是动手翻译不易,要是有帮助或者支持的话,扔个硬币吧(码字不易QVQ)
ADDR: 0xf1aF694c9A24963110A334B5cede1f1BD0047041
(ETH)