区块链安全

区块链安全第 3 课——智能合约编程:Hello Solidity

@snowming

0x01 本课导言

在本文中,将从使用 Solidity 输出 Hello World 开始,逐步深入到对链上数据的读写操作,最终系统地学习一遍 Solidity 的基本语法。


你会发现在 Solidity 中输出 Hello World 并无需像 C 或者 Python 一样调用 printf/print,编译器已经帮我们实现了对于状态变量的 getter() 方法……你还会见到一些熟悉的老朋友比如 enum、struct、循环……语法基本类似于 C 但是比 C 更简单,也会有一些新朋友比如 function modifier、event、function selector……话不多说,快来一起领略 Solidity 的魅力!

0x02 Hello World

每一种语言的学习都是从 hello world 开始的。来看看如何用 Solidity 写 Hello World 吧!

pragma solidity ^0.4.9;
contract HelloWorld{
string public greet = "Hello World!";
}

编译、部署&&运行之后,使用 Remix 获取这个「状态变量」。状态变量是永久地存储在合约存储中的值。定义方式是:<类型> <属性> <变量名> = <值>。

状态变量有两个特征:

  • 在函数外声明
  • 存储在区块链上,也就是会上链

如上,点击 greet 按钮的提示语是:
CALL[call]from: 0x9746391f7B9DD17cf887D434F1aE44fb9Bd6D3e4to: HelloWorld.greet()data: 0xcfa...e3217Debug
这意味着刚刚我们调用了 HelloWorld.greet(),且返回值是 string: Hello World!

为什么明明是访问了状态变量,但是显示的是调用了同名方法呢?

这是因为 public 关键字的作用。
关键字 public 自动生成一个函数,允许在这个合约之外访问这个状态变量的当前值。如果没有这个关键字,其他的合约没有办法访问这个变量。所以这句代码:string public greet = "Hello World!"; 实际由编译器生成的函数的代码大致如下所示:
function greet() returns (string) { return greet; }

当然,加一个和上面完全一样的函数是行不通的,因为我们会有同名的一个函数和一个变量。这里,只是要解释为什么明明是变量但是是通过函数访问的 —— 因为编译器已经帮你实现了 get() 方法。

0x03 get() 和 set() 函数的上链问题

来看这个简单的合约:

pragma solidity ^0.4.19;

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

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

代码非常简单,相信通过前两课的铺垫,可以很容易的看出:

  • set 方法是修改 storedData 的值的
  • get 方法是获取 storedData 的值的

部署此合约之后来尝试调用这两个方法:

我在操作的时候,先填写了 788 的参数,然后调用了 set 方法;然后在交易 pending 的期间调用了 get 方法。结果返回的值是 0。

但其实链上的交易顺序是基于 nonce 的。set 是第 n 币交易,则 get 是第 n+1 笔交易。理论上在区块链上只有第 n 笔交易完成后,才会执行第 n+1 笔交易。但是很显然在这里,get 函数被直接先于 set 函数调用了。

而且查看合约交易历史,只能看到 set 方法的调用,而看不到 get 方法的调用:

但是当 set 方法执行完毕后,再次执行 get 方法,就能得到被改变的 storedData 值,这说明 get 方法也是执行成功了的。但是区块链上却看不到交易历史。

这是为什么呢?
通俗一点来说:读不上链写上链。也就是对于合约数据的读取不会作为区块链中的交易,而写或者修改这些操作才会作为区块链中的交易。
因为声明了 storedData 变量之后默认初始化值为 0,所以 get 调用返回 0。而且不需要参与链上交易的排队。

另外可以说一下 get() 方法中的这个 view 关键字
function get() public view returns (uint)

可以将函数声明为 view 类型,这种情况下要保证不修改状态。
下面的语句被认为是修改状态:

  1. 修改状态变量。
  2. 产生事件。
  3. 创建其它合约。
  4. 使用 selfdestruct(合约自毁)。
  5. 通过调用发送以太币。
  6. 调用任何没有标记为 view 或者 pure 的函数。
  7. 使用低级调用。
  8. 使用包含特定操作码的内联汇编。

注:constant 是 view 的别名。

0x04 What’s more

现在你已经零零散散的了解了一些关于智能合约编程的基本知识了,但是可能在上面的内容中有一些模糊不清的困惑。所以我仍认为你应该系统学习一下 Solidity 编程的基本语法。
这篇博客之所以跟第二篇之间隔了好几天的时间,是因为我把这部分语法刷完了才发出来。简单的说就是要阅读代码、理解概念,个人比较喜欢这种 learn by example 的形式。

下面是我刷过的知识点的列表:

希望你也刷一遍,然后再和我一起进行下一步的学习,有任何问题可以在评论中留言。

下次课我们将换换口味,暂时从大段代码中松口气。我将介绍以太坊客户端,进行以太坊客户端的下载、安装、区块链同步以及通过 JSON-RPC 接口向客户端发起请求、获取数据。