以太坊开发需知
本文源自我在昨天的HiBlock活动上的分享,同时也可以算是到目前为止来自实际项目的一线总结,希望其中的内容能够帮助后来者少踩些坑,节约宝贵的时间。
以太坊开发与传统应用开发的差异
相比起传统应用而言,以太坊开发引入了新的基础设施,由此必不可少的带来了部署和运维的复杂度,比如作为系统设计者,我们需要做出选择:
- 自建节点,还是信任第三方节点?
- 公有链、联盟链、私有链?
由于加入了新的设计单元:智能合约,我们将面对
- 设计的复杂度
- 合约的升级问题:因为智能合约一旦发布就无法更改,万一需要更新合约错误或规则,怎么办?
- 合约的组织问题。
- 与一般代码不同,合约的好坏直接与金钱挂钩
- 不安全的合约会造成客户的金钱损失,立竿见影。
- 合约的每一步都需要消耗gas,不讲究的合约会造成执行成本高居不下。
并且,以太坊本身的限制同样也会影响到整个应用系统的设计和选型:
- 交易确认需要时间:20笔/秒
- 交易易受外界影响
- 交易费的高低
- 流行应用会造成网络拥堵,从影响交易的确认
相比起传统CS编程,与以太坊进行交互要复杂得多:
- 需要有钱包账户
- 发出去的交易需要签名
- 由于整个过程是异步为主,因此交易需要验证
对于区块链本身的定位,同样也会影响设计:
- 仅仅用作数据共享和防篡改的基础设施?
- 围绕区块链打造价值网络?
- Token设计模式
- Token引入对于业务本身带来的影响
这一点尤其差异巨大,不单单像传统开发那样仅仅只需要了解用户的业务就可以开足马力前进。Token设计本身需要一定的经济常识,虽说这部分可以由专业背景的人来设计,但对于开发者和架构师而言,不了解必要的基础知识肯定会对开发的顺利进行有阻碍。
以太坊Dapp的典型技术架构
Serverless风格
![](https://img.haomeiwen.com/i1793586/570c0e3812c8da58.png)
这种架构非常明了,客户端直接与部署在以太坊节点上的智能合约打交道就好了。它的优点和缺点都很明显:
- 优点
- 轻量级
- 运维简单
- 彻底的去中心化
- 缺点:
- 胖客户端:交互 + 业务逻辑
- 智能合约难以承载复杂业务逻辑
- 典型场景:投票、博彩、小游戏等
CS + 区块链
![](https://img.haomeiwen.com/i1793586/130c4e8f9643fd2c.png)
这种架构相当于传统CS(注:这里的传统相对于区块链应用而言,因此像桌面客户端 + 服务器、Web系统、前后端、移动互联网应用等都属于本文中所说的传统应用。)融入了区块链,客户端和服务器都和区块链直接交互。
为什么客户端也需要跟区块链直接交互?原因很简单:区块链应用的账户信息(尤其是私钥)一般都由用户自己保管,不会放在服务器上。服务器上只会存放系统自己的账户信息。
这种系统的优缺点如下:
- 优点
- 传统应用和区块链融合
- 适用复杂业务逻辑,服务器完全可以包含复杂业务逻辑,合约只承载与价值流转相关的商业规则。
- 缺点
- 重量级
- 运维负担重
- 部分中心化,话说回来,在我看来,中心化算不上太坏,因为中心化本身代表了专业化。
- 典型场景:具有复杂业务逻辑的应用系统,如物品溯源、信用质押、供应链金融等等。
Server + 区块链
![](https://img.haomeiwen.com/i1793586/7f7e771918ce3dd1.png)
这种架构相当于上面的一种变体:客户端委托服务器完成与区块链相关的交互,甚至于客户端完全都不知道区块链的存在。为何不推荐采用这种架构呢?原因很明显:它要求客户端绝对信任服务器。
这种架构的优缺点如下:
- 优点
- 对客户端屏蔽了区块链的复杂度
- 缺点
- 私钥中心化管理
- 典型场景:客户端绝对信任服务器
最后,说说关于密钥的存放:
- 若合约部署于第三方节点,如Infura,毫无疑问只能是自己管理。
- 假如是自建节点,那么你有两种选择
- 方式1:托管
- 方式2:自管
同时出于保障资金安全的角度:
- 控制托管账户的可用资金,每当可用资金用完,从自管账户中转入
- 对于自管账户,最好也分散风险,建立多个自管账户,将资金分散其中,避免被一锅端。
以太坊应用的开发流程
以太坊应用的开发流程如下图,相比起传统开发流程没有本质的区别,只是测试过程相对繁琐:先本地环境测试,再上测试网试运行,最后部署于主网。只是由于合约的更新麻烦,因此建议尽量提前多做一些测试,将问题提前消灭掉。
![](https://img.haomeiwen.com/i1793586/849f2e76a183be05.png)
以太坊开发注意事项
谈完差异,看过架构和展示了开发流程之后,接下来就进入正题,说说本文的重点:以太坊开发的那些坑。
智能合约
智能合约开发的常用工具:
- Solidity + Truffle + VS Code
- 常用类库:
- Token和ICO相关:OpenZepplin和TokenMarketNet/ICO
- 可升级合约:ZOS
关于合约的执行成本,我之前写过一篇文章有详细介绍,这里就不再赘述,请参见原文,避免不必要的金钱损失。
关于合约的安全,我在这篇文章中略有提及。但远远不够,这段时间以来,我也翻阅了相关资料,整理如下:
- Overflow & Underflow,使用OpenZeppelin的SafeMath lib
- 可见性和delegatecall说明如下,相关推荐:优先external, 并留意避免在delegatecall中包含恶意代码
- public,无限制
- external,仅外部调用
- private,仅本合约内
- internal,类似protect
- delegatecall,类似js中的apply,被调用代码和调用合约处于一个上下文
- 可重入性(DAO攻击),利用CDI模式
- 检查 -> 更改合约状态 -> 支付
- 优先使用pull模式,而非push/send模式
- withdraw 优于 send/transfer
- 避免使用随机数、now和block.blockhash作为合约逻辑
- 分布式网络的时钟问题
- 注意短地址攻击,检查message.data的合法性
- 地址不足会用金额部分数据补0
- 利用Modifier完成权限方面的校验
至于合约的设计和组织:
- 单一大合约 VS 合约模块化
- Hub – Spoke模式
- 使用mapping保存合约数据
- 合约升级的主要模式
- Proxy
- 数据合约 + 控制合约
Truffle
Truffle作为开发智能合约的利器,不仅仅提供了对于合约开发和测试的支持,它还可以作为合约迁移和部署的工具。这里主要讲讲部署的常用套路。
一般的Truffle例子中大多只是部署单个合约,但有时我们需要部署多个合约,并且这些合约之间有先后依赖关系时,需要顺序部署:
var Storage = artifacts.require("./Storage.sol");
var InfoManager = artifacts.require("./InfoManager.sol");
module.exports = function(deployer) {
deployer.deploy(Storage)
.then(() => Storage.deployed())
// deployer.deploy(`ContractName`, [`constructor params`])
.then(() => deployer.deploy(InfoManager, Storage.address));
}
假如要在部署之后立即执行合约代码:
deployer.deploy(Storage)
.then(() => Storage.deployed())
.then((instance) => {
instance.addData("Hello", "world")
})
如果要部署到不同的网络环境,可以采用如下命令:
truffle migrate --network network_id
此时需要在truffle.js中设置好合适的network_id,部署脚本如下:
module.exports = function(deployer, network) {
if (network == "live") {
// do one thing
} else if (network == "development") {
// do other thing
}
}
如果要换账户部署,则:
module.exports = function(deployer, network, accounts) {
var defaultAccount;
if (network == "live") {
defaultAccount = accounts[0]
} else {
defaultAccount = accounts[1]
}
}
并且往往会跟HDWalletProvider结合使用。
同时把合约部署到Infura上也会用到它:
const HDWalletProvider = require("truffle-hdwallet-provider");
module.exports = {
networks: {
"ropsten-infura": {
provider: () => new HDWalletProvider("<passphrase>", "https://ropsten.infura.io/<key>"),
network_id: 3,
gas: 4700000
}
}
};
如果合约用到了lib,则:
deployer.deploy(MyLibrary);
deployer.link(MyLibrary, MyContract);
deployer.deploy(MyContract);
WEB3J
对于Java和Android开发者,如果要开发以太坊应用,离不开web3j,它的大致使用流程如下:
![](https://img.haomeiwen.com/i1793586/6502c6c6fd0dc4a9.png)
但请注意:
- 合约的部署建议直接用Truffle完成,如前所述,Truffle不仅仅只是开发,它提供了对于合约的一整套生命周期管理。
- 生成钱包可选步骤,可以使用外部现有钱包账户
- 合约本身的测试建议用Truffle
- 此处测试专注于应用本身逻辑和合约逻辑的集成测试
- 测试建议用Ganache
对于新手,一个常常犯的错误就是选错TransactionManager,它一旦选错,将交易导致。假如你发起交易,而交易没有发出去,同时报诸如:TransactionHashMissMatched,那么十有八九就是这个问题。
TransactionManager有两种:
- ClientTransactionManager,适用于私钥放在以太坊客户端,由它来签名并发送交易的场合。典型如:geth和ganache中账户。
- RawTransactionManager,适用于由应用客户端自己签名并发送交易的场合。典型如:私钥不在自有geth节点和使用第三方节点。
在使用RawTransactionManager时,需要注意设置好合适的chainid。
有时,交易发出之后,发现长时间处于Pending状态,那么请检查(假如不是网络拥堵的情况):
- 是否设置了合适的gasprice
- 是否设置了合适的nonce,它有点类似数据库中的sequence,一旦用过就不能再被使用。
同时,还需要留意有多少节点接受了交易所在区块。接受的节点越多,交易越不可能被回滚。确认算法:当前区块高度 - TX所处区块高度 > 指定块数,对于Ganache测试环境,这个值可以是0。
假如你的交易比较重要,可能需要根据交易的重要程度,动态调整这个值。
最后,避免使用send方法,使用sendAsync,并结合CompletableFuture。
其他工具
假如你的工具栈是javascript/typescript,那么:
- web3.js,基础
- truffle-contract,更好的合约抽象
- ethers.js,更高抽象层次的以太坊交互接口
从某些方面来讲,ethers.js与web3.js有重叠,但前者对钱包开发提供了更友好的接口。
假如前端页面想将MetaMask直接集成进来,即遇到以太坊交互时直接激活MetaMask,那么可以用下面的代码:
// Adapted from https://github.com/MetaMask/faq/blob/master/DEVELOPERS.md#partly_sunny-web3---ethereum-browser-environment-check
window.addEventListener('load', function() {
// Checking if Web3 has been injected by the browser (Mist/MetaMask)
if (typeof web3 !== 'undefined') {
// Use Mist/MetaMask's provider
window.web3 = new Web3(web3.currentProvider);
} else {
// fallback - use your fallback strategy (local node / hosted node + in-dapp id mgmt / fail)
window.web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545"));
}
// Now you can start your app & access web3 freely:
startApp()
})
合约部署
从大的方面讲,合约部署有两种选择,但各自都有其优缺点:
- 私有节点
- 优点
- 完全掌控
- 适用于应用不能随意访问第三方节点的场合
- 缺点
- 节点安全自我保证
- 账本同步问题,如断电重启;节点与外部断开一段时间才发现,由此导致的分叉
- 若私钥寄存在节点,存在安全风险
- 优点
- 第三方(如infura)
- 优点
- 运维负担甩给第三方
- 不需分享私钥
- 缺点
- 需要信任第三方节点,因为第三方有可能不把交易发出去,返回伪造信息。
- 非完整API,如infura不支持filter
- API调用频率有限制
- 优点
这里面没有谁优谁劣,只能根据自己的需求权衡后选择。
总结
总的来讲:
- 区块链开发与传统应用开发差异很大
- 智能合约设计不等同于
- 数据库设计
- 传统OO设计
- 与以太坊的交互不是简单的请求调用
- 实践出真知
- 多看、多听、多交流
- 选择优秀类库
- 测试、测试、再测试
参考链接
- Web3J文档
- Trffule文档
- OpenZeppelin文档
- 以太坊开发极简入门
- 面向老程序员的Solidity摘要
- OpenZeppelin周记:打开地图
- Designing the architecture for your Ethereum application
- Dapp Architecture Designs
- How to Secure Your Smart Contracts: 6 Solidity Vulnerabilities and how to avoid them (Part 1)
- How to Secure Your Smart Contracts: 6 Solidity Vulnerabilities and how to avoid them (Part 2)
- 遗忘的亚特兰蒂斯:以太坊短地址攻击详解