区块链100讲:面向老程序员的Solidity摘要

Posted by

区块链100讲:面向老程序员的Solidity摘要

本文作者:HiBlock区块链技术布道群-胡键

原文发布于简书

原文链接:

https://www.jianshu.com/p/ec5ad71e28aa

加微信baobaotalk_com,加入技术布道群

区块链100讲:面向老程序员的Solidity摘要

开发以太坊DApp,Solidity是必经之路。然而,对于跟我一样的那些有多年开发经验的以太坊新人来讲,Solidity学习固然是一方面,但更重要的是快速了解它的惯例和套路,以及一些值得注意的事项。这,正是本文试图达到的目标。至于详尽的语法文档,参考HiBlock区块链社区翻译的solidity中文文档(https://solidity-cn.readthedocs.io/zh/develop/)。

1

EVM和字节码

与Java代码类似,Solidity代码会先被编译成字节码,然后再由EVM负责执行。从逻辑上来讲,可以将以太坊视为一台计算机,其中的每个EVM节点类似在计算机中执行的进程,分布式账本则是这台计算机的存储。

一旦部署成功,其代码会被复制到以太坊上其他节点,并可以通过命令查看其源码。以Truffle开发环境为例:

  • truffle develop

  • 部署MyCoin合约,deploy

  • MyCoin.at(地址),从其返回的json对象中的source属性即可看到合约代码。

合约部署之后就无法更新,这就给开发者带来了相当大的挑战:

  • 如何开发出高质量的合约,尽可能的没有Bug?

  • 如何设计可升级的合约?

2

执行代价

说到程序执行的代价,一般指的都是花多少内存、存储和CPU时间。但执行以太坊上的代码,除了这些通常意义的代价之外,还需要真金白银。这是因为以太坊上的交易确认都是需要花钱的!它们主要是那些改变以太坊状态的操作,如:

  • 账户转账

  • 部署合约

  • 合约内的写操作

而且,与其他系统不同,这些操作的执行结果并不会立刻生效。它们会以交易的形式提交到交易池中等待矿工确认,这便是交易费的由来。并且,这个价格也不是一个固定值,它随着市场行情的波动上下浮动。如果你的交易长时间没有结果,那么可以看看是否是因为交易费过低。

关于交易费的行情,可以从最新的交易(https://etherscan.io/txs)中了解。

这也给开发者带来了挑战:如何在实现功能的前提下尽可能的降低交易成本?

3

账户

要在以太坊上进行操作,必需要有以太坊账户。当前有两类账户类型:

  • 外部账户,可简单认为是“人类用户”,有私钥和余额,交易发送前会用私钥先签名。

  • 合约账户,合约部署之后,会随之对应有一个账户,由余额和相应的合约状态数据。它由外部消息来触发执行。触发源来自外部账户或其他合约账户。

这里也带来了一些关于安全性方面的概念转变:

  • 私钥是终极秘密,一定是本地存储,否则都是不安全的。

  • 由于合约是公开的,谁都可以发起执行。如果要实现“只有xxx才能执行本合约”,必需要在合约内部代码中进行控制。

4

合约语法

合约类似Java中的类,但与类不同之处在于,它的构造函数只会被调用一次,即部署合约的时候。

合约的状态变量相当于类的实例变量,但同样是持久化的。并且,mapping只能声明成状态变量但可在函数内引用。

变量类型同样也分值类型和引用类型,其中引用类型包括:数组和结构体,后者给自定义类型提供方案。

函数可以返回一个值或多个值,同时可以指定返回的变量。如:

function arithmetic(uint _a, uint _b)

public

pure

returns (uint o_sum, uint o_product)

{

o_sum = _a + _b;

o_product = _a * _b;

}

函数修改器(Modifier)类似AOP中的拦截器,提供了修改函数执行流程的机会,一般用来做验证和检查。其中“_”用来将控制流返还给被修改的函数,如下例:

modifier onlySeller() { // Modifier

require(

msg.sender == seller,

“Only seller can call this.”

);

_;}

function abort() public onlySeller { // Modifier usage

// …

}

几个重要的修改器:

  • payable,接收以太的函数必需加上

  • view或pure,表示函数不会改变以太坊状态

事件提供了让外部应用了解合约状态变化的途径,一般使用流程是:

  • 合约内部发出事件

  • 外部应用利用web3监听事件

可见性:

  • external,仅适用于函数,表示其可被外部合约或交易调用,但不能被内部调用。

  • public

    函数缺省的可见性,可被内部调用和通过消息调用,

    状态变量,EVM会为其自动产生getter

  • internal,函数和状态变量可被当前合约和其子合约调用

    状态变量的缺省可见性

  • private,函数和状态变量仅被当前合约调用

合约支持多重继承。

EVM提供了4种数据位置用来存放数据:

  • storage,持久化,存储于整个以太坊

  • memory,函数的本地内存,非持久化

  • calldata,函数入参,非持久化

  • stack,EVM的调用栈

规则:

  • 状态变量:storage

  • external函数入参:calldata

  • 函数入参:memory

  • 函数局部变量:

    引用类型,缺省为storage,但可被覆盖

    值类型,memory,不可被覆盖

    mapping类型,指向外部的状态变量

  • 状态变量之间赋值,将产生独立副本,即相互更改不受引用。

  • storage和memory变量之间相互赋值,总产生独立副本。

  • memory变量之间赋值

    值类型,产生独立副本

    引用类型,指向同一地址

由于合约执行是有成本的,需要警惕循环语句。

对于多重继承的合约,需要明确指明顺序,如:

contract X {}contract A is X {}contract C is A, X {}

fallback函数没有函数名,无法直接调用,但在两个情况下会被触发:

  • 合约中无任何函数匹配调用者发过来的请求时

  • 合约接收以太时,此时,fallback函数需使用payable

由于其无法被外部调用,EVM限制其只能最多消耗2300的gas,若超过,则fallback函数失败。因此,记得要测试合约的fallback函数是否会超过这个限制。

并且,fallback是安全事故的高发地,需要对其进行必要的安全相关的测试。

接口和抽象合约跟Java中的接口和抽象类差别不大,库(library)是一段可复用的代码,在调用它的合约上下文内执行:

  • 它不能用状态变量

  • 不能继承或被继承

  • 不能接收以太

合约抛出异常之后,状态回滚,当前有3种方式:

  • require(表达式),若表达式为false,则抛出异常,未使用的gas退回

  • 适合验证函数的入参

  • assert(表达式),同上,但未使用的gas不会退回,将全部被消耗

  • 适合验证内部状态

  • revert(),直接抛出异常

5

常见模式

鉴于以太坊应用的以下特点,编写solidity代码时需要非常小心:

  • 执行消耗真金白银

  • 合约公开可见,即使是private

  • 合约不可篡改,一旦发布无法变更

常见的编码套路有:

  • 对于支付,优先采用“取款”,而不是“转账”(即send或transfer),避免接收合约恶意fallback函数。

  • 对于支付,采用CDI模式,避免重入问题。即:

    检查 -> 更改本合约状态 ->支付。

  • 善用Modifier进行权限控制。

  • 使用mapping类型保存合约数据,甚至为了方便升级,单独分离出两类:

    数据合约,仅包含mapping,保留操作mapping的函数,客观上类似数据表。

    控制合约,仅包含逻辑控制,通过数据合约的接口操作数据。若逻辑有问题,只需升级本合约即可,数据仍然得以保留。

  • 使用代理合约,参见这里(https://blog.zeppelin.solutions/proxy-libraries-in-solidity-79fbe4b970fd)。

最后,也是最省事的方式:使用成熟类库,如OpenZeppelin。关于Solidity的好东西,可以通过其Awesome List(https://github.com/bkrem/awesome-solidity)来了解。

— 线上课程推荐 —

区块链100讲:面向老程序员的Solidity摘要

始发于微信公众号: 区块链社区HiBlock