本文源自我在昨天的 HiBlock 活动上的分享,同时也可以算是到目前为止来自实际项目的一线总结,希望其中的内容能够帮助后来者少踩些坑,节约宝贵的时间。
相比起传统应用而言,以太坊开发引入了新的基础设施,由此必不可少的带来了部署和运维的复杂度,比如作为系统设计者,我们需要做出选择:
由于加入了新的设计单元:智能合约,我们将面对
并且,以太坊本身的限制同样也会影响到整个应用系统的设计和选型:
相比起传统 CS 编程,与以太坊进行交互要复杂得多:
对于区块链本身的定位,同样也会影响设计:
这一点尤其差异巨大,不单单像传统开发那样仅仅只需要了解用户的业务就可以开足马力前进。Token 设计本身需要一定的经济常识,虽说这部分可以由专业背景的人来设计,但对于开发者和架构师而言,不了解必要的基础知识肯定会对开发的顺利进行有阻碍。
这种架构非常明了,客户端直接与部署在以太坊节点上的智能合约打交道就好了。它的优点和缺点都很明显:
这种架构相当于传统 CS(注:这里的传统相对于区块链应用而言,因此像桌面客户端 + 服务器、Web 系统、前后端、移动互联网应用等都属于本文中所说的传统应用。)融入了区块链,客户端和服务器都和区块链直接交互。
为什么客户端也需要跟区块链直接交互?原因很简单:区块链应用的账户信息(尤其是私钥)一般都由用户自己保管,不会放在服务器上。服务器上只会存放系统自己的账户信息。
这种系统的优缺点如下:
这种架构相当于上面的一种变体:客户端委托服务器完成与区块链相关的交互,甚至于客户端完全都不知道区块链的存在。为何不推荐采用这种架构呢?原因很明显:它要求客户端绝对信任服务器。
这种架构的优缺点如下:
最后,说说关于密钥的存放:
同时出于保障资金安全的角度:
以太坊应用的开发流程如下图,相比起传统开发流程没有本质的区别,只是测试过程相对繁琐:先本地环境测试,再上测试网试运行,最后部署于主网。只是由于合约的更新麻烦,因此建议尽量提前多做一些测试,将问题提前消灭掉。
谈完差异,看过架构和展示了开发流程之后,接下来就进入正题,说说本文的重点:以太坊开发的那些坑。
智能合约开发的常用工具:
关于合约的执行成本,我之前写过一篇文章有详细介绍,这里就不再赘述,请参见原文,避免不必要的金钱损失。
关于合约的安全,我在这篇文章中略有提及。但远远不够,这段时间以来,我也翻阅了相关资料,整理如下:
至于合约的设计和组织:
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);
对于 Java 和 Android 开发者,如果要开发以太坊应用,离不开 web3j,它的大致使用流程如下:
但请注意:
对于新手,一个常常犯的错误就是选错 TransactionManager,它一旦选错,将交易导致。假如你发起交易,而交易没有发出去,同时报诸如:TransactionHashMissMatched,那么十有八九就是这个问题。
TransactionManager 有两种:
在使用 RawTransactionManager 时,需要注意设置好合适的 chainid。
有时,交易发出之后,发现长时间处于 Pending 状态,那么请检查(假如不是网络拥堵的情况):
同时,还需要留意有多少节点接受了交易所在区块。接受的节点越多,交易越不可能被回滚。确认算法:当前区块高度 - TX 所处区块高度 > 指定块数,对于 Ganache 测试环境,这个值可以是 0。
假如你的交易比较重要,可能需要根据交易的重要程度,动态调整这个值。
最后,避免使用 send 方法,使用 sendAsync,并结合 CompletableFuture。
假如你的工具栈是 javascript/typescript,那么:
从某些方面来讲,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();
});
从大的方面讲,合约部署有两种选择,但各自都有其优缺点:
这里面没有谁优谁劣,只能根据自己的需求权衡后选择。
总的来讲:
觉得有帮助的话,不妨考虑购买付费文章来支持我们 🙂 :
付费文章