Solidity编程 一 之智能合约的介绍

admin
admin
admin
2162
文章
5
评论
2020年6月8日15:39:29Solidity编程 一 之智能合约的介绍已关闭评论 792 6795字阅读22分39秒

写在前面:

最新在学习以太坊相关的东西,Solidity是基础,所以对 http://solidity.readthedocs.io/en/latest/installing-solidity.html 里的文章进行了翻译。争取这篇文档都能完成翻译。由于自己的英语水平有限,如果在翻译的过程中有什么错误的,请留言,非常感谢!

1. 一个智能合约的例子

   我们从一个基础的solidity例子开始。开始的时候,你可能看不懂每一行具体的意思,但是没关系,我们会在后续的讲解中介绍每一个细节

Storage
Solidity编程 一 之智能合约的介绍
pragma solidity ^0.4.0;
 
contract SimpleStorage {
    uint storedData;
 
    function set(uint x) {
        storedData = x;
    }
 
    function get() constant returns (uint) {
        return storedData;
    }
}
Solidity编程 一 之智能合约的介绍

第一行告诉该合约用的是0.4.0版本的solidity编写,并且这些代码具有向上兼容性。保证不会在不同solidity编译版本下编译会出现不同的行为。

  从Solidity角度来看,合约就是存在于以太坊区块链中的一个特定地址中的代码和数据集合。uint storedData 声明了一个类型为 uint(256位的无符号整型)的变量,变量名称为 storedData。你可以把它想象为数据库中的一个字段,该字段是可以被数据库中的方法进行查询,修改。在以太坊中,这个字段是属于一个合约字段。在这个例子中,该变量可以通过提供的get,set方法进行获取或是修改。Solidity中,访问一个变量是不需要通过this来引用的。

  这个合约很简单,只是允许以太坊上的任何人来存储一个数据到某个节点,同时把这个操作发布到以太坊中,当然,以太坊上的其他节点同样可以通过调用set方法来修改你已经存储好的值。虽然有被修改,但是对该值操作的任何历史记录都是保存在以太坊中的。不用担心你的存储记录或是修改记录会丢失。后面我们会将到如何对合约进行限制,只允许你一个人修改这个数据

2. 子货币例子

  下面的例子将实现一个简单的加密货币例子。无中生币不在是梦想,当然只有合约的创建人才有这个特权。此外,任何人只要有一个以太坊密钥对就可以进行货币交易,根本不需要注册用户名和密码。

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
pragma solidity ^0.4.0;
 
contract Coin {
 
    //public关键字可以让外部访问该变量
    address public minter;
    mapping (address => uint) public balances;
 
    //事件可以让轻客户端快速的响应变化
    event Sent(address from, address to, uint amount);
 
    // 构造方法
    function Coin() {
        minter = msg.sender;
    }
 
    function mint(address receiver, uint amount) {
        if (msg.sender != minter) return;
        balances[receiver] += amount;
    }
 
    function send(address receiver, uint amount) {
        if (balances[msg.sender] < amount) return;
        balances[msg.sender] -= amount;
        balances[receiver] += amount;
        Sent(msg.sender, receiver, amount);
    }
}

  这个合约引入了一些新的概念,让我们一个个都过一遍。

  address public minter; 

   声明了一个public,类型为address的状态变量。Address类型是一个160位的值,不允许任何的算术操作。它适合于存储合约地址或是其他人的密钥对。Public关键字会自动产生用于外部访问该变量值的方法。如果不声明public,其他的合约是无法访问该变量的。自动产生的方法类似于:

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

   当然如果你增加了一个和上面完全一样的方法是没有任何作用的,我们需要变量和产生的方法名完全一致。这块其实编译器会帮助我们完成,不需要我们自己动手编写,我们只要知道这个概念就可以。

  mapping (address => uint) public balances;

   还是创建了一个公有状态变量,这是一个比address更复杂的数据类型,类似java里的Map<address,uint>,它描述了一个地址和一个uinit数据类型的map关系。Mappings的关系可以看成是一个hash表,所有可能的key都对应了一个0的值。当然在map里不存在只有key值或是只有value值的情况。所以我们需要记住添加了一个什么样的map关系或是像这个例子一样,如何使用它。因为这是个public变量,所以系统会自动为它生成一个get方法,类似于:

   function balances(address _account) returns (uint) {

        return balances[_account];

    }

  通过上面的方法我们可以很容易的查询一个账号的余额。

  event Sent(address from, address to, uint amount);

   这一行创建了一个名为event 的事件。该事件会在该示例的最后一行被触发。用户或是server应用可以花很低的代价(后面会讲代价是什么)来监听事件的触发。一旦这个事件被触发了,监听者接收到三个参数: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可以被任何账号(必须有以太币的账号)调用并发送以太币给另外一个账号。注意,如果你用合约发送以太币到另外一个账号,通过区块链浏览器查看是查看不到任何变化的,因为发送以太币的过程和金额的变化都被存储在了特殊的以太币合约里。而不是体现在账号上。通过使用事件,可以很容易的创建一个区块链浏览器,用来查看交易和账号余额。

3. 区块链基础

 

   对于编程者来说,区块链不是一个很难理解的概念。因为最难懂的那部分(包括挖矿,哈希,椭圆加密,p2p网络)都只是提供了一系列的特性和约束。一旦你知道了这些特性和约束,你不必去理解这些特性或是约束背后的实现原理。

  交易

     区块链是全局共享,交易数据库。这就意味着任何人只要参与到这个网络中就可以访问到这个数据库。如果你要修改数据库中的数据,你需要创建一个被其他所有在这个网络里的人所认可的交易。交易说明对数据的修改要么没有任何进行要么就完全完成,不会出现部分完成,部分未完成的情况。而且一旦交易完成,被记录在数据库中,谁也无法修改这个交易。

     举个例子,想象在一个电子货币里,用表列举出所有账号余额。当进行一个账号和另外一个账号进行交易,交易数据库要确保交易金额要从交易发送方减去,并且在交易接收方增加同样的交易金额。如果交易过程中出现了任何原因导致交易失败,交易发送方增加金额的行为失败,那接收方的金额也不应该发生变化。

     而且发送方都会对发起的交易进行签名加密。这直接地保证了数据库只能被指定的修改所修改。在电子货币的例子中,简单的检查能够确保只有持有这个账号的秘钥者才能对金额进行转移。

  区块

     在比特币术语中,一个比较难理解的是 “双花攻击”问题:当网络中的两笔交易想同时清空同一个账号的余额会发生什么?一个冲突?

     理论上,你不需要去关心这种情况。系统会为你选择一个交易,这些交易会绑定在一个叫做 “区块”里,然后这些交易被执行并且分发到所有参与的节点。如果两笔交易相互冲突,后面执行的交易会被拒绝,不会成为区块的一部分。

      每一个区块都会及时的用线性组织起来,这就是区块链的最初来源。每一个区块都在一个固定时间加入到区块链中,对于以太坊来说,间隔为17秒。作为“序列化选择机制”(被称做挖矿)的一部分,可能会发生区块反复回滚情况,但是这种情况只是出现在区块链的末端。越多的区块被加入到末端,回滚操作就会越少。所以你的交易可能被回滚或是从区块链上被删除,但是你只要等的时间越长,这种情况发生的就会越少。

4.  以太坊虚拟机

  概述

     以太坊虚拟机(EVM)是以太网上智能合约的运行环境。这不仅仅是个沙盒,更确实的是一个完全独立的环境,也就是说代码运行在EVM里是没有网络,文件系统或是其他进程的。智能合约甚至被限制访问其他的智能合约

  账号 

     在以太坊中有两种账号共享地址空间:外部账号和合约账号。外部账号是由公钥和私钥控制的(如人),合约账号是由账号存储的代码所控制。

外部账号的地址是由公钥决定的,而合约地址是在智能合约被创建的时候决定的(这个地址由创建者的地址和发送方发送过来的交易数字衍生而来,这个数字通常被叫做“nonce”)

    不管是否账号存有代码(合约账号存储了代码,而外部账号没有),对于EVM来说这两种账号是相等的。

每一个账号都有持久化存储一个key和value长度都为256位字的键值对,被称为“storage”

而且,在以太坊中,每个账号都有一个余额(确切的是用“Wei”来作为基本单位)  ,该余额可以被发送方发送过来带有以太币的交易所更改。

  交易 

   交易是一个账号和另外一个账号之间的信息交换。它包含了二进制数据(消费数据)和以太数据。如果目标账号包含了代码,这个代码一旦被执行,那么它的消费数据就会作为一个输入数据。如果目标账号是一个0账号(地址为0的账号),交易会生成一个新的合约。这个合约的地址不为0,但是是来源于发送方,之后这个账号的交易数据会被发送。这个合约消费会被编译为EVM的二进制代码,并执行。这次的执行会被作为这个合约的代码持久化。这就是说:为了创建一个合约,你不需要发送真正的代码到这个合约上,事实上是代码的返回作为合约代码。

  Gas

      以太坊上的每笔进行一笔交易都会被收取一定数量的Gas.这是为了限制交易的数量,同时对每一笔交易的进行支付额外费用。当EVM执行一个交易,交易发起方就会根据定义的规则消耗对应的Gas。

  交易的创造者定义了的Gas 价格。所以交易发起方每次需要支付 gas_price * gas 。如果有gas在执行后有剩余,会以同样的方法返回给交易发起方。

  如果gas在任何时候消耗完,out-of-gas 异常会被抛出,那当前的这边交易所执行的后的状态全部会被回滚到初始状态。

  存储,主存和栈

       每个账号都有持久化的内存空间叫做存储. 存储是一个key和value长度都为256位的key-value键值对。从一个合约里列举存储是不大可能的。读取存储里的内容是需要一定的代价的,修改storage里的内容代价则会更大。一个合约只能读取或是修改自己的存储内容。

  第二内存区域叫做主存。系统会为每个消息的调用分配一个新的,被清空的主存空间。主存是线性并且以字节粒度寻址。读的粒度为32字节(256位),写可以是1个字节(8位)或是32个字节(256字节)。当访问一个字(256位)内存时,主存会按照字的大小来扩展。主存扩展时候,消耗Gas也必须要支付,主存的开销会随着其增长而增大(指数增长)。

      EVM不是一个基于寄存器,而是基于栈的。所以所有的计算都是在栈中执行。最大的size为1024个元素,每个元素为256位的字。栈的访问限于顶端,按照如下方式:允许拷贝最上面的16个元素中的一个到栈顶或是栈顶和它下面的16个元素中的一个进行交换。所有其他操作会从栈中取出两个(有可能是1个,多个,取决于操作)元素,把操作结果在放回栈中。当然也有可能把栈中元素放入到存储或是主存中,但是不可能在没有移除上层元素的时候,随意访问下层元素。

  指令集

    为了避免错误的实现而导致的一致性问题,EVM的指令集保留最小集合。所有的指令操作都是基于256位的字。包含有常用的算术,位操作,逻辑操作和比较操作。条件跳转或是非条件跳转都是允许的。而且合约可以访问当前区块的相关属性比如编号和时间戳。

  消息调用

        合约可以通过消息调用来实现调用其他合约或是发送以太币到非合约账号。消息调用和交易类似,他们都有一个源,一个目标,数据负载,以太币,gas和返回的数据。事实上,每个交易都包含有一个顶层消息调用,这个顶层消息可以依次创建更多的消息调用。

       一个合约可以定义内部消息调用需要消耗多少gas,多少gas需要被保留。如果在内部消息调用中出现out-of-gas异常,合约会被通知,会在栈里用一个错误值来标记。这种情况只是这次调用的gas被消耗完。在Solidity,这种情况下调用合约会引起一个人为异常,这种异常会抛出栈的信息。

      上面提到,调用合约会被分配到一个新的,并且是清空的主存,并能访问调用的负载。调用负载时被称为calldata的一个独立区域。调用结束后,返回一个存储在调用主存空间里的数据。这个存储空间是被调用者预先分配好的。调用限制的深度为1024.对于更加复杂的操作,我们更倾向于使用循环而不是递归。

  代理调用/ 代码调用和库

    存在一种特殊的消息调用,叫做代理调用。除了目标地址的代码在调用方的上下文中被执行,而且msg.sender和msg.value不会改变他们的值,其他都和消息调用一样。这就意味着合约可以在运行时动态的加载其他地址的代码。存储,当前地址,余额都和调用合约有关系。只有代码是从被调用方中获取。这就使得我们可以在Solidity中使用库。比如为了实现复杂的数据结构,可重用的代码可以应用于合约存储中。

  日志

   我们可以把数据存储在一个特殊索引的数据结构中。这个结构映射到区块层面的各个地方。为了实现这个事件,在Solidity把这个特性称为日志。合约在被创建出来后是不可以访问日志数据的。但是他们可以从区块链外面有效的访问这些数据。因为日志的部分数据是存储在bloom filters上。我们可以用有效并且安全加密的方式来查询这些数据。即使不用下载整个区块链数据(轻客户端)也能找到这些日志

  创建

   合约可以通过特殊的指令来创建其他合约。这些创建调用指令和普通的消息调用唯一区别是:负载数据被执行,结果作为代码被存储,调用者在栈里收到了新合约的地址。

  自毁

    从区块链中移除代码的唯一方法是合约在它的地址上执行了selfdestruct操作。这个账号下剩余的以太币会发送给指定的目标,存储和代码从栈中删除。