了解DApp开发过程

  • DApp是什么
  • DApp的开发准备
    • 环境
    • 编程语言
      • 智能合约
      • 业务逻辑
  • DApp原理
    • 部署
    • 调用

DApp是什么

DApp( Decentralized Application )即分布式应用或去中心化应用。DApp就是智能合约+App。

一个真正的DApp应用,需要同时满足以下几个条件:

  1. 应用必须完全开源、自治,且没有一个实体控制该应用超过51%的- Token。该应用必须能够根据用户的反馈及技术要求进行升级,且应用升级必须由大部分用户达成共识之后方可进行;
  2. 应用的数据必须加密后存储在公开的区块链上;
  3. 应用必须拥有Token机制,矿工或应用维护节点需得到代币奖励;
  4. 应用代币的产生必须依据标准的加密算法,有价值的节点可以根据该算法获取应用的代币奖励。

DApp的开发准备

环境

  1. Remix,这是一个强大的IDE环境,不仅可以支持Solidity语法高亮和提示,还可以部署合约,调用和调试合约,你甚至可以不必设置区块链数据源,因为其内嵌了一个模拟区块链的虚拟机叫JavaScript VM。使用教程点这里

  2. 以太坊私链,也就是搭建属于自己的区块链,最常见的方式就是通过Geth(Go-Ethereum)搭建了,他可以算是比较真实的区块链平台了,唯一区别就是不需要挖矿,也就是不需要共识,Geth搭建私链时,我们需要配置创世块、bootnode、启动节点、挖矿等操作,搭建私链教程可点击此处。

  3. 测试环境中的公链,一些组织为了让开发者更方便的开发DApp,搭建了自己的以太坊平台并对外开放,这些测试平台的以太币可以免费获取,但每个账户获取的数量有限,不过也足以我们用来测试了,在下文的实践中用的也是这种方案。而测试平台有哪些呢,在我们安装了MetaMask插件以后,可以在里面找到,如下图所示。

  4. 以太坊,最后的环境肯定是以太坊了,不过我们在上线前一定要做好测试,因为区块链具有不可篡改性,我们的DApp在上线后出bug可是无法修复的,只能通过发布一个新的DApp,然后通知你的用户以后使用新地址,所以我们在设计合约时应当设置有无效标志位,尽早地告知调用者该合约已经废弃。

编程语言

编写一个DApp可以说是包括两部分,合约部分和业务逻辑部分。

智能合约

Solidity,类JavaScript,这是以太坊推荐的语言,也是最流行的智能合约语言,具体用法参考http://solidity.readthedocs.io/en/latest/ ,本文所有合约都使用该语言编写,另外测试、调试Solidity有一个非常好的在线IDE–Remix ,由以太坊团队推出的。由于考虑在线网络比较慢,可以使用本地IDE,使用教程点这里。

业务逻辑

业务逻辑部分即提供客户端与智能合约交互的接口,相当于目前BS结构中的后台逻辑,因此业务逻辑部分可部署在中心服务器中,而且在以太坊中每个智能合约函数的每一行代码都有固定的gas费用以及延时的,一些简单的逻辑应该交由业务逻辑处理,编写业务逻辑目前提供有以下几种语言:

  • JavaScript,主要是基于Web3.js这个库调用智能合约,本文例子也是使用JavaScript编写的。
  • Go,上述提到的以太坊私链搭建工具geth就是使用Go编写的。
  • Python
  • Java
  • Ruby
  • Haskell
  • Rust

DApp原理

一个DApp被调用之前需要先部署到以太坊上,不管是私链,公链还是联盟链。故本章节分为两部分,DApp部署原理和调用原理。

部署

一个DApp由多个智能合约组成,部署一个DApp也就是同时部署多个智能合约,这里讲述一下部署一个智能合约的流程,如下图所示。

  1. 将编写好的Solidity智能合约通过RPC调用以太坊钱包或Web3.js等工具。
    Web3.js发送合约源码到部署在以太坊节点的Solc编译器。
  2. 编译器返回合约字节码。
  3. 发送合约字节码和初始化参数到以太坊节点。
  4. 以太坊节点上EVM验证完成后,部署到全网的所有节点,完成后返回合约地址和应用二进制接口(ABI)。

这里推荐使用Remix进行部署,因为Remix不仅可以连接浏览器内嵌的以太坊VM,还可以和MetaMask联动,使用MetaMask当前所连的以太坊网络,而且Remix还可以调试部署好的合约,十分方便。在Remix上部署十分简单,选择部署的以太坊网络和填好合约初始化参数后,点击create按钮即可。如果我们选的是MetaMask当前的以太坊网络,则会跳转交易界面,因为部署合约本质上也是一笔交易,我们需要付交易手续费,如下图所示。

部署后的合约其实外部还不能调用,还需要我们上传源代码进行验证,不然别人不可能在不清楚源码情况下向合约发起交易,向你转账,如下所示。

调用

部署好的合约我们就可以调用了,根据调用方式的不同,本文分为前端调用和后端调用。

前端调用相对来说简单一点,因为有MetaMask这个强大的插件,我们不必操心以太坊数据源,直接调用web3.currentProvider即可,下图展示了前端调用合约的一般流程,由于前端连接的Web3 Provider是与特定的以太坊节点相连,前端不需要管方法的签名,只需无脑调用合约中的方法即可。
至于后台调用就麻烦一点了,由于后台没有MetaMask这么方便的工具可调用,因此要是调用公链上的智能合约,只能使用特定账户的私钥签名方法后,并且以该账户的身份调用合约,流程如下图所示。
以太坊Web3.js提供调用合约的方法一共有四种:

  • call: 这是最简单的调用方式,适用于调用只读的方法,也就是调用过程不会修改区块链上的数据,因为它只读取本地数据即可,因此不会消耗gas,而且可以立刻获得返回值,适用于前端调用,具体例子如下。
//合约声明contract test { function multiply(uint a) returns(uint d) { return a * 7; }}//合约调用var Multiply7 = eth.contract(contract.info.abiDefinition);var myMultiply7 = Multiply7.at(address);myMultiply7.multiply.call(3)
  • sendTransaction: 可调用读或写方法,调用过程会创建一个交易,调用之后会返回一个交易hash值,它会广播到网络,等待矿工打包, 它会消耗gas,而且该调用不能立刻获得返回值,只能从event log中获取,也是适用于前端调用,代码如下。
wheelOfFortune.makeBet.sendTransaction(num - 1, betCount, tips, { from: web3.eth.accounts[0], value: betUnit \* betCount + tips }).then(function (result) {if (result.logs.length > 0) {var eventobj = result.logs[0].args;$.ajax({url: '/Wheel/makeBet/' + eventobj.pieceIdx + '/' + betCount + '/' + tips + '/' + web3.eth.accounts[0],success: function (data) {var nextRound = JSON.parse(data);$("#currentTotal").text(nextRound.betPool.toLocaleString());playersNumberOfPiece = nextRound.playersNumberOfPiece;alert('成功');}});}});
  • 直接调用: 这是一种特殊调用,可以说是前两种调用的结合,因为当合约方法有constant修饰时,直接调用会等同于call,否则等同于sendTransaction。
var winIdx = (Math.random() \* wheel.config.pieceCount) >> 0;// 后台调用infura部署的合约必须用sendRawTransactionvar coder = require('web3/lib/solidity/coder');var CryptoJS = require('crypto-js');var Tx = require('ethereumjs-tx');var privateKey = new Buffer("71112e795325d5cbf14d665091ce4626f26c8342b8038f1adcdfff26be04a220", 'hex');var functionName = 'finishRound';var types = ['uint'];var args = [winIdx];var fullName = functionName + '(' + types.join() + ')';var signature = CryptoJS.SHA3(fullName, { outputLength: 256 }).toString(CryptoJS.enc.Hex).slice(0, 8);var dataHex = signature + coder.encodeParams(types, args);var data = '0x' + dataHex;var account = "0x4BEB9EA54fc912B619D5C682BA1cB3524bc80955";var nonce = web3.toHex(web3.eth.getTransactionCount(account));var gasPrice = web3.toHex(web3.eth.gasPrice);var gasLimitHex = web3.toHex(3000000);var rawTx = { 'nonce': nonce, 'gasPrice': gasPrice, 'gasLimit': gasLimitHex, 'from': account, 'to': contractAddress, 'data': data }var tx = new Tx(rawTx)tx.sign(privateKey)var serializedTx = '0x' + tx.serialize().toString('hex')web3.eth.sendRawTransaction(serializedTx, function (err, txHash) {if (!err) {console.log(JSON.stringify({ "transactionHash": txHash }));} else {console.log("finish round error " + err);}});
  • sendRawTransaction: 前两种方法都不需要调用者提供交易发起者的私钥进行方法签名,因为MetaMask或本地以太坊节点提供了,但是当我们没有MetaMask时调用公链合约,我们只能调用sendRawTransaction使用指定账户的私钥签名方法后才能调用合约,值得注意的是,该方法我们无法获得返回值,即使在event log中也拿不到,只能在得到transaction的hash后再读取区块链信息才可以,一般在后台调用方法时用到,代码如下。

调用时需要注意的几点:

当调用者给予Gas不足时,EVM会报出out of gas的错误,这时候会回滚本地交易所做过的所有修改,好在以太坊还提供了estimategas方法,可以让我们在调用之前预估交易所需的Gas,因为Solc编译器会算出每一句代码的价格,不过有时会不准确,特别是出现死循环或者违反了修饰方法的限制条件时。
调用频率不能过快,一方面所有调用方法都是异步的,返回时间可能会较长,另一方面有可能第一次调用的transaction还没被挖到区块中,下一次调用就来了,而且两次调用的hash可能是一样的,所以就会报replacement transaction underpriced错误。
当我们调用了不存在的方法时,EVM会自动调用合约中的Fallback方法作为兜底,而且会将Gas全耗完,如果没有定义Fallback方法,才会报错回滚。