一种基于以太坊的溯源方案,可以运行demo感受以太坊特性。如图所示是本项目Demo的整体架构设计,分为三层:应用层(APP),服务层(Nodejs-service),区块链层(以太坊),,该设计思路与方案的可见下面内容。应用层通过请求服务层,将操作区块链的操作交给服务层去做,避免了直接操作区块链,在服务层又可以作很多事情,如权限控制检查用户操作、消息队列避免拥塞等,最核心的功能是将来自APP的请求根据业务的不同来分离,进一步操作智能合约上不同的方法。区块链网络层运行以太坊节点,在其上部署智能合约,并处理来自服务层的数据。
图0-1溯源项目架构设计
基于以上设计架构,本文分为三部分进行阐述:
第一部分,以太坊网络的搭建,介绍以太坊私链的搭建过程,不论后面合约逻辑如何,本章是搭建以太坊私链所通用的技术。
第二部分,合约开发和部署,介绍合约的设计逻辑和方案,并附上源码。
第三部分,后台开发,即是基于nodejs的服务层开发,作为用户App和区块链网络之间的媒介,介绍如何响应用户请求并随之操作区块链。随后介绍用户如何利用该层的API服务与区块链交互。
第一部分 以太坊网络搭建
1.下载geth客户端
(1) 下载
进入网站https://geth.ethereum.org/downloads/,进行geth客户端下载,可根据不同的操作系统自行下载匹配的geth。
(2) 安装
直接点击二进制包进行安装。
2.使用geth生成账户
(1) 使用geth命令:geth account new
(2) 输入密码,geth会根据用户输入自动生成生成keystore文件,(默认路径为C:\Users\Administrator.xxx\AppData\Roaming\Ethereum)
(3) 在系统任意位置创建数据目录data0,作为同步以太坊数据的目录。
(4) 将上述keystore文件复制到data0文件夹下
(5) 按照上述方式预先生成n个账户,并将每个账户对应的密码文件(以回车换行)放入data0同级目录,存为pwd。
提示:挖矿者的账户的keystore一定要存在于 "extraData",如:
"extraData": "0x0000000000000000000000000000000000000000000000000000000000000000e578252579e5f43fe124fe1d8236f0e5250c11970000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"其中红色部分为miner的地址。
3.配置创世区块
在data0同级目录下创建创始区块配置文件,并将上述生成的账户填入配置文件alloc字段中,以自定义分配初始以太币:
4.初始化
进入到以太坊数据目录,然后使用geth命令:
geth --datadir data0 init Genesis.json
5.启动geth命令
(各参数详细解释,请使用geth --help查询)
6.其他
以太坊节点启动正常后,使用内置API(web3)可进行如下操作:
开始挖矿:miner.start()
停止挖矿:miner.stop()
生成新的账户:personal.newAccount()
查看挖矿奖励者:eth.coinbase
账户之间转账交易:
amount = web3.toWei(5,'ether') personal.unlockAccount(eth.accounts[0]) eth.sendTransaction({from:eth.accounts[0],to:eth.accounts[1],value:amount})
第二部分 合约开发和部署
1. 业务需求
我们以药品溯源为例,整个业务场景中,业务节点可分为三类,药品生产商、代理商以及零售商;数据可分为两部分,药品信息及流转信息。药品信息是由药品生产商生产药品时所添加,药品的流转信息由流转渠道上的代理商及零售商等所添加,如图2-1所示。向区块链添加数据时,使用该商品的ID+本业务节点信息上传即可。
图2-1 药品溯源业务
2.合约数据结构
合约使用solidity开发,基于以上业务需求,使用solidity内置的struct及mapping数据类型,并进行嵌套。得到药品溯源可用的数据结构:
溯源信息的存储结构如图2-2所示,由mapping+struct嵌套结构,左边是一个mapping集合,每个mapping元素为一个键值对,”键”为ID值,通过该ID可以获得对应的Struct, 该Struct中包含“药品信息”和“溯源信息”两部分。当查询时,通过ID号码(如二维码编码的NWRuxXJ75fBWUjBZi)查出Mapping中“键”为“NWRuxXJ75fBWUjBZi”的Struct,该Strcut数据为{ dragInfo:999感冒药,哈药六厂, traceInfo:代理商A=>代理商B=>零售商X}
图2-2 合约存储结构
3.开发准备
(1) Solidity
Solidity是一种类似JavaScript的智能合约高级语言,运行在Ethereum虚拟机(EVM)之上,它被设计成以编译的方式生成以太坊虚拟机代码,是开发以太坊智能合约官方推荐语言。
(2) Truffle
Truffle是一个世界级的开发环境,测试框架,以太坊的资源管理通道,致力于让以太坊上的开发变得简单,Truffle有以下:
内置的智能合约编译,链接,部署和二进制文件的管理。
快速开发下的自动合约测试。
脚本化的,可扩展的部署与发布框架。
部署到不管多少的公网或私网的网络环境管理功能
使用EthPM&NPM提供的包管理,使用ERC190标准。
与合约直接通信的直接交互控制台(写完合约就可以命令行里验证了)。
可配的构建流程,支持紧密集成。
在Truffle环境里支持执行外部的脚本。
4.合约代码
说明:由于solidity0.4版本不支持嵌套结构mapping的遍历,因此以字符拼接的方式添加溯源信息,方便直接展示。
pragma solidity ^0.4.24; import "./strings.sol"; contract testTraceability_addTime { using strings for *; string constant optStr = "==>"; string constant leftOpt = "("; string constant rightOpt = ")"; event SavedProducer(uint256 dragId,string dragInfo,string[] traceInfo); struct Drag { string dragInfo; string traceInfo; } mapping(string => Drag) allDragsInfo; constructor () public{ } function getDrugInformationWithID(string id) returns(string memory,string memory){ return(allDragsInfo[id].dragInfo,allDragsInfo[id].traceInfo); } function addDragInfo_init(string ID,string memory dragInfomation,string memory company,string memory time) public { allDragsInfo[ID].dragInfo = dragInfomation; string memory info1 = time.toSlice().concat(rightOpt.toSlice()); string memory info2 = leftOpt.toSlice().concat(info1.toSlice()); string memory info3 = company.toSlice().concat(info2.toSlice()); string memory concatStr = info3.toSlice().concat(optStr.toSlice()); allDragsInfo[ID].traceInfo = concatStr; } function addDragInfo_A(string ID,string memory company,string memory time) public { string memory info1 = time.toSlice().concat(rightOpt.toSlice()); string memory info2 = leftOpt.toSlice().concat(info1.toSlice()); string memory info3 = company.toSlice().concat(info2.toSlice()); string memory concatStr = info3.toSlice().concat(optStr.toSlice()); allDragsInfo[ID].traceInfo = allDragsInfo[ID].traceInfo.toSlice().concat(concatStr.toSlice()); } function addDragInfo_B(string ID,string memory company,string memory time) public { string memory info1 = time.toSlice().concat(rightOpt.toSlice()); string memory info2 = leftOpt.toSlice().concat(info1.toSlice()); string memory info3 = company.toSlice().concat(info2.toSlice()); string memory concatStr = info3.toSlice().concat(optStr.toSlice()); allDragsInfo[ID].traceInfo = allDragsInfo[ID].traceInfo.toSlice().concat(concatStr.toSlice()); } function addDragInfo_C(string ID,string memory company,string memory time) public { string memory info1 = time.toSlice().concat(rightOpt.toSlice()); string memory info2 = leftOpt.toSlice().concat(info1.toSlice()); string memory info3 = company.toSlice().concat(info2.toSlice()); string memory concatStr = info3.toSlice().concat(optStr.toSlice()); allDragsInfo[ID].traceInfo = allDragsInfo[ID].traceInfo.toSlice().concat(concatStr.toSlice()); } }
5.合约说明
对于溯源的数据结构操作,有addDragInfo_init,addDragInfo_A,addDragInfo_B,addDragInfo_C以及getDrugInformationWithID C等操作方法,addDragInfo_init为药品生产商添加信息方法,addDragInfo_A,addDragInfo_B,addDragInfo_C三个方法为流转渠道上节点操作,最后getDrugInformationWithID方法为用户使用产品的ID去获取全部溯源信息的操作。
6.使用truffle进行合约部署
(1) 安装truffle
全局安装truffle的npm包:
npm install truffle -g
(2) truffle初始化
进入工程目录,并运行以下命令:
truffle init
(3) 修改相关配置文件
修改如图2-3所示三个文件:
图2-3 truffle运行相关配置文件
介绍下这三个文件:
1_initial_migration.js(默认迁移合约)
var Migrations = artifacts.require("./Migrations.sol"); module.exports = function(deployer) { deployer.deploy(Migrations); };
2_deploy_contracts.js(配置需要部署的合约)
//var testTraceability = artifacts.require("./testTraceability.sol"); var testTraceability_addTime = artifacts.require("./testTraceability_addTime.sol"); var strings = artifacts.require("./strings.sol"); module.exports = function(deployer) { deployer.deploy(strings, {from:"0xe578252579e5f43fe124fe1d8236f0e5250c1197"});// 部署合约,使用旷工地址(保证gas费充足) deployer.link(strings, testTraceability); // 库链接 deployer.deploy(testTraceability, {from:"0xe578252579e5f43fe124fe1d8236f0e5250c1197"});// 部署合约 deployer.deploy(testTraceability_addTime, {from:"0xe578252579e5f43fe124fe1d8236f0e5250c1197"});// 部署合约 };
3_truffle.js(配置目标以太坊相关等信息)
/* * NB: since truffle-hdwallet-provider 0.0.5 you must wrap HDWallet providers in a * function when declaring them. Failure to do so will cause commands to hang. ex: * ``` * mainnet: { * provider: function() { * return new HDWalletProvider(mnemonic, 'https://mainnet.infura.io/<infura-key>') * }, * network_id: '1', * gas: 4500000, * gasPrice: 10000000000, * }, */ // Allows us to use ES6 in our migrations and tests. require('babel-register')({ ignore: /node_modules\/(?!zeppelin-solidity)/ }); require('babel-polyfill'); module.exports = { //See <http://truffleframework.com/docs/advanced/configuration> //to customize your Truffle configuration! networks: { development: { host: "127.0.0.1",//合约部署的以太坊节点ip port: 7545, //合约部署的以太坊节点端口 network_id: "*", // 以太坊网络id gas: 8000000 // 合约部署gas上限 } }, mocha: { useColors: true }, solc: { optimizer: { enabled: true, runs: 200 } } };
(4) 编译合约
在工程目录下运行:
truffle compile
truffle compile --all (全部重新编译)
(5) 迁移合约
在工程目录下运行
truffle migrate
truffle migrate --reset (全部重新执行迁移脚本)
第三部分 后台开发
1.后台开发说明
本区块链网络是基于以太坊私链,因此为了保证数据安全和可靠,用户不能直接操作区块链,必须通过中间件服务。因此开发node中间件,可以管控上链数据,并且可以做一些其他必要业务,如权限管理,消息队列,区块数据暂存等等。
2.需求分析
由第二部分已知,三类业务节点(生产商、销售商、零售商)需要分别向区块链中上数据,并且必须基于中间服务,而非直接操作区块链。所以,采用图3-1所示的后台架构,即B端以及C端所有的用户均基于中间件node服务去间接操作或访问区块链。因此,根据各个用户职能的不同开放不同API服务,各个使用者在区块链上进行不同的事务。如图中所示,简单而言,就是药品生产商、代理商A、代理商B、零售商向区块链写数据,用户从区块链读数据。
图3-1 node后台架构图
3.开发准备
(1) Nodejs
在整个开源社区JavaScript应用得最为广泛,目前JavaScript语言所对应的大部分项目都是基于Node.js平台的,所以Nodejs是当之无愧的、最流行的开发平台之一。Nodejs是一个搭建中Chrome V8上的JavaScript即时运行平台,采用的是时间驱动和非阻塞I/O模型,既轻量又高效,非常适合数据密集型、实时的应用,如股票、基金、加密货币等。尤其是Nodejs的包管理工具npm,已经成为全球最活跃的社区,有非常多的第三方包可供开发人员选用,区块链相关的绝大多数组件更是以npm包的形式被开发和应用,因此Nodejs是区块链开发的强大工具。
使用:Nodejs环境安装
(2) Express
Express 是一个简洁而灵活的 node.js Web应用框架, 提供了一系列强大特性帮助你创建各种 Web 应用,和丰富的 HTTP 工具。使用 Express 可以快速地搭建一个完整功能的网站,并且支持各种 HTTP 实用工具和中间件,快速方便地创建强大的 API。
使用:引入npm包。
(3) web3 API
以太坊网络是由节点组成的,每一个节点都包含了区块链的一份拷贝。当我们需要调用一份智能合约中的方法时,需要从其中一个节点中查找并告诉它,而以太坊节点只能识别一种叫做 JSON-RPC 的语言,这种语言可读性并不好。Web3.js 可以把这些晦涩难懂的查询语句都隐藏起来了,只需要与方便易懂的 JavaScript 界面进行交互即可。
web3.js是以太坊提供的一个Javascript库,它封装了以太坊的JSON RPC API,提供了一系列与区块链交互的Javascript对象和函数,包括查看网络状态,查看本地账户、查看交易和区块、发送交易、编译/部署智能合约、调用智能合约等,其中最重要的就是与智能合约交互的API。因此,要开发基于以太坊的项目必须熟悉web3js中的常用API。
使用:引入npm包。
4.node代码
通过nodejs+express+web3js开发简单的node server,向外界暴露API接口。外部通过Http请求调用API服务,操作区块链。
const web3 = require('web3'); const ethRPC = "ws://localhost:8546"; const serverPort = 10999; const provider = new web3.providers.WebsocketProvider(ethRPC); const web3Client = new web3(provider); const coinbase = '0xe578252579e5f43fe124fe1d8236f0e5250c1197'; const contract = require("truffle-contract"); const express = require('express'); const app = express(); const cors = require('cors'); app.use(cors()); var Resp = (function(yj) { yj = function() { this.dragId = ''; this.dragName = ''; this.trace = ''; }; yj.prototype = { toJson: function() { return JSON.stringify(this); } }; return yj; }(Resp || function(){})); function eraseErrorOfContract(arr) { for (let contrc of arr) { if (typeof contrc.currentProvider.sendAsync !== "function") { contrc.currentProvider.sendAsync = function () { return contrc.currentProvider.send.apply( contrc.currentProvider, arguments ); } } } } const traceabilityJSON = require('../build/contracts/testTraceability_addTime'); const traceability = contract(traceabilityJSON); traceability.setProvider(provider); var traceabilityInst; eraseErrorOfContract([traceability]); traceability.deployed({ from: coinbase, gas: 600000 }).then(instance => { traceabilityInst = instance; // traceabilityInst.addDragInfo_init('NWRuxXJ75fBWUjBZiDIyR16PdFLIOG/odIos/Z8ggxNqrvQyPMcnXjAVB0JAmHYu4LGxyOaA4FIlOJgzGrPvcIndf0EbYZ80l',"999pai","hayao",{from:coinbase,gas:600000}).then(result => { // console.log(result); // return traceabilityInst.addDragInfo_A('NWRuxXJ75fBWUjBZiDIyR16PdFLIOG/odIos/Z8ggxNqrvQyPMcnXjAVB0JAmHYu4LGxyOaA4FIlOJgzGrPvcIndf0EbYZ80l',"经销商1",{from:coinbase,gas:600000}) // }).then(result2 => { // console.log(result2); // return traceabilityInst.getDrugInformationWithID.call('NWRuxXJ75fBWUjBZiDIyR16PdFLIOG/odIos/Z8ggxNqrvQyPMcnXjAVB0JAmHYu4LGxyOaA4FIlOJgzGrPvcIndf0EbYZ80l',{from:coinbase,gas:600000}); // }).then(result3 =>{ // console.log(result3); // }).catch(err => { // console.log(err) // }); }); app.get('/addDragInfo_produce',function (req,res) { // app => router let currentTime = new Date().toLocaleString(); traceabilityInst.addDragInfo_init(req.query.id,req.query.dragInfo,req.query.company,currentTime,{from:coinbase,gas:600000}).then(result => { console.log(result); res.end("您(生产商)已将药品信息上链!交易hash:"+result.tx); }).catch(err => { console.log(err); res.end(JSON.stringify(err)) }); }); app.get('/addDragInfo_A',function (req,res) { // app => router let currentTime = new Date().toLocaleString(); traceabilityInst.addDragInfo_A(req.query.id,req.query.company,currentTime,{from:coinbase,gas:600000}).then(result => { res.end("您(销售商A)已将信息上链!交易hash:"+result.tx) }).catch(err => { res.end(JSON.stringify(err)) }); }); app.get('/addDragInfo_B',function (req,res) { // app => router let currentTime = new Date().toLocaleString(); traceabilityInst.addDragInfo_B(req.query.id,req.query.company,currentTime,{from:coinbase,gas:600000}).then(result => { res.end("您(销售商B)已将信息上链!交易hash:"+result.tx) }).catch(err => { res.end(JSON.stringify(err)) }); }); app.get('/addDragInfo_C',function (req,res) { // app => router let currentTime = new Date().toLocaleString(); traceabilityInst.addDragInfo_C(req.query.id,req.query.company,currentTime,{from:coinbase,gas:600000}).then(result => { res.end("您(销售商C)已将信息上链!交易hash:"+result.tx) }).catch(err => { res.end(JSON.stringify(err)) }); }); app.get('/getDrugInformationWithID',function (req,res) { // app => router let ret = new Resp(); traceabilityInst.getDrugInformationWithID.call(req.query.id,{from:coinbase,gas:600000}).then(result => { ret.dragId = req.query.id; ret.dragName = result[0] ; ret.trace = result[1]; res.setHeader('Content-Type', 'text/plain;charset=utf-8'); res.end("药品ID:"+ret.dragId+'\n'+"药品名称:"+ret.dragName+'\n'+"溯源信息:"+ret.trace); }).catch(err => { res.end(JSON.stringify(err)) }); }); var server = app.listen(serverPort, function () { var host = server.address().address; var port = server.address().port; console.log( 'Express started on http://'+ host +':' + port ); });
5.node服务说明
有需求分析可知,药品生产商、代理商、零售商向区块链写数据,用户从区块链读数据。因此,通过不同的用户通过http访问不同的API,达到各自的业务目的,如下所示:
生产商数据上链操作:http://localhost:8546/addDragInfo_produce
代理商A数据上链操作:http://localhost:8546/addDragInfo_A
代理商B数据上链操作:http://localhost:8546/addDragInfo_B
用户查询溯源信息操作:http://localhost:8546/getDrugInformationWithID