根據(jù)去中心化的課程評(píng)分系統(tǒng)白皮書(shū),我們草擬了評(píng)分系統(tǒng)合約,實(shí)現(xiàn)了打分可追溯和打分次數(shù)記錄等基本功能,并將其部署在了私有鏈上。此外,初步實(shí)現(xiàn)了DAPP功能,并用 node.js 搭建了頁(yè)面。未來(lái)的計(jì)劃包括:打分合約的撰寫(xiě)和部署,以及打分界面的設(shè)計(jì)和優(yōu)化。
實(shí)驗(yàn)依賴(lài)
Node.js
節(jié)點(diǎn)包管理器(NPM)在本次實(shí)驗(yàn)中作為web開(kāi)發(fā)的工具。家喻戶曉的npm這里就不再贅述了,用homebrew安裝node.js:
brew install node
顯示版本成功為安裝好了:
npm -v
Truffle
Truffle??(松露)——“聰明的合同更甜蜜”,是一個(gè)簡(jiǎn)潔的智能合約開(kāi)發(fā)框架。通過(guò)下載demo,我們可以很快的上手部署合約,并且Truffle最讓人驚嘆之處在于它甚至提供了和node.js 協(xié)同開(kāi)發(fā)web Ui的interface,真的因此十分符合初級(jí)Dapper的需求。
用node.js安裝Truffle:
npm install -g truffle
Ganache
Truffle Suite提供了一個(gè)很好用的私有鏈工具——Ganache。Ganache可以快速啟動(dòng)個(gè)人Ethereum區(qū)塊鏈,可以使用它來(lái)運(yùn)行測(cè)試、執(zhí)行命令和檢查狀態(tài),同時(shí)控制鏈的操作方式。這里用Ganache就是為了方便創(chuàng)建accounts。
Ganache可以從這里下載。
Metamask
”小狐貍??“——Metamask 是 Google Chrome 瀏覽器的擴(kuò)展,將以太坊與 Google Chrome 結(jié)合,在 Chrome 瀏覽器上運(yùn)行以太坊 DApps,以及身份識(shí)別的工具。于是,它就具備了類(lèi)似 Mist 的錢(qián)包功能,允許用戶管理自己的賬戶,通過(guò) Web3 JavaScript API,讓 DApp 與以太坊區(qū)塊鏈實(shí)現(xiàn)交互。從Chrome Extension Store里就能下載Metamask,注冊(cè)一個(gè)賬戶就能用了(雖然沒(méi)錢(qián)??)。當(dāng)然,我們之后用到的賬戶并不是這個(gè)注冊(cè)的賬戶,而是Ganache上的賬戶。在Chrome上注冊(cè)賬戶并登陸。
智能合約
Truffle框架
創(chuàng)建項(xiàng)目——Score Chain:
mkdir scorechain
cd scorechain
使用[truffleframework.com/boxes/][truffleframework.com/boxes/]快速啟動(dòng)和運(yùn)行。安裝寵物商店demo:
truffle unbox pet-shop
當(dāng)我們的框架安裝好了的時(shí)候,目錄結(jié)構(gòu)如圖:

truffle.js 是truffle框架和ganache網(wǎng)絡(luò)連接的配置文件,host一般用localhost,端口取決于ganache的RPC 服務(wù)器(如果端口錯(cuò)了后期設(shè)計(jì)網(wǎng)頁(yè)會(huì)一直 loading)。文件內(nèi)容為:
module.exports = {
networks: {
development: {
host: "127.0.0.1",
port: 7545,
network_id: "*"
}
}
};
src是網(wǎng)頁(yè)開(kāi)發(fā)源代碼目錄,暫時(shí)只用到了app.js 和 index.html 這個(gè)文件。app.js這個(gè)文件是前端和后端的interface,是整個(gè)Dapp十分重要的一部分。**index.html **是前端設(shè)計(jì)文件。
node_modules 是node.js的模塊,暫時(shí)不需要。
contracts 就是合約目錄啦!之后我們的合約就是在這里完成的。合約寫(xiě)好了之后需要部署在我們用Ganache創(chuàng)建的私有鏈上,需要在migrations文件夾下寫(xiě)部署文件。當(dāng)部署在私有鏈上的時(shí)候,truffle框架會(huì)為我們生成build,編譯我們的合約。
合約擬寫(xiě)
在contracts目錄下,建立合約文件,用>=0.4.20 <0.6.1 的 solidity編寫(xiě)合約Score(打分)。
構(gòu)建學(xué)生結(jié)構(gòu)體:
struct Student {
uint id;
string name;
uint selectCount;
}
建立關(guān)于學(xué)生和TA的映射,并創(chuàng)建變量被評(píng)次數(shù):
// Store TAs
mapping(address => bool) public TAs;
// Store Students
mapping(uint => Student) public students;
// Store Students Count
uint public scoredTimes;
定義添加學(xué)生和TA打分函數(shù):
function addStudent (string _name) private {
scoredTimes ++;
students[scoredTimes] = Student(scoredTimes, _name, 0);
}
function select (uint _studentId) public {
// require that they haven't selected before
require(!TAs[msg.sender]);
// require a valid student
require(_studentId > 0 && _studentId <= scoredTimes);
// record that TA has selected
TAs[msg.sender] = true;
// update student select Count
students[_studentId].selectCount ++;
// trigger selected event
selectEvent(_studentId);
}
最后還需要定義一個(gè)選擇學(xué)生事件:
// select event
event selectEvent (
uint indexed _studentId
);
至此,合約擬寫(xiě)成功!
合約部署
為了將合約部署到Ganache私有鏈上,還需要一個(gè)部署文件,在migrations目錄下部署合約:
var Score = artifacts.require("./Score.sol");
module.exports = function(deployer) {
deployer.deploy(Score);
};
打開(kāi)Ganache,看看RPC 服務(wù)器(7545)是否和truffle.js 對(duì)應(yīng):

部署合約到Ganache私有鏈:
truffle migrate --reset // 非首次部署要加reset
出現(xiàn)下圖為成功:
Using network 'development'.
Running migration: 1_initial_migration.js
Replacing Migrations...
... 0x8006a2052e71652571a823fc4a33f5f88ea1bc76972ef08dafbaade016e330ab
Migrations: 0x64745cba2a428767a9c6518da9bc5752492fec22
Saving successful migration to network...
... 0x42cc99e3517718bb12f89b90f8d86e3e4aad0b91a4a0b28331cf89c817de89c2
Saving artifacts...
Running migration: 2_deploy_contracts.js
Replacing Score...
... 0x24a274ae9c7fba6142290ebcf5b966faafbc4852f0b752022627679fb6bc8c08
Score: 0xebde42adb74d844988238720c4e50feada2f6f2f
Saving successful migration to network...
... 0x7d1aedce77df01a0e77f59ca9f55973fb90ba9cf7d61469f8fcc8dbfb7d1067b
Saving artifacts...
打開(kāi)truffle console,聲明合約實(shí)例,檢查我們部署的合約:
$ truffle console // 進(jìn)入console
聲明一個(gè)實(shí)例:
Score.deployed().then(function(instance) { app = instance })
看看我們有多少個(gè)學(xué)生(6個(gè)):
app.studentsNum()
// 顯示 BigNumber { s: 1, e: 0, c: [ 6 ] }
這就說(shuō)明部署成功了,我們?cè)倏纯茨J(rèn)的部署用戶,Ganache的用戶0:嗯,果然它從原來(lái)的100eth變少了,說(shuō)明部署合約確實(shí)有以太幣的花費(fèi)!

這時(shí)候truffle框架為我們自動(dòng)生成build/contracts,目錄下的Score.json是合約的可執(zhí)行文件。之后會(huì)在我們的客戶端開(kāi)發(fā)中用到。
測(cè)試文件
為了驗(yàn)證我們部署的合約是否正確,還需要設(shè)計(jì)幾個(gè)測(cè)試:
touch ./test/score.js
score.js是我們的測(cè)試文件。打開(kāi)文件,設(shè)計(jì)測(cè)試函數(shù)如下:
it("initializes with six students", function(){...};
it("it initializes the students with the correct values", function() {};
it("allows a TA to cast a select", function() {};
it("throws an exception for invalid students", function() {};
it("throws an exception for double selecting", function() {};
查看測(cè)試結(jié)果:
truffle test
五個(gè)測(cè)試都通過(guò)了?。?!

合約客戶端
接口設(shè)計(jì)
初始化Web3:
initWeb3: function() {
if (typeof web3 !== 'undefined') {
App.web3Provider = web3.currentProvider;
web3 = new Web3(web3.currentProvider);
} else {
App.web3Provider = new Web3.providers.HttpProvider('http://localhost:7545');
web3 = new Web3(App.web3Provider);
}
return App.initContract();
},
將合約初始化:
initContract: function() {
$.getJSON("Score.json", function(score) {
App.contracts.Score = TruffleContract(score);
App.contracts.Score.setProvider(App.web3Provider);
App.listenForEvents();
return App.render();
});
},
等待合約emit給觀察者(也就是我們):
// Listen for events emitted from the contract
listenForEvents: function() {
App.contracts.Score.deployed().then(function(instance) {
instance.selectedEvent({}, {
fromBlock: 0,
toBlock: 'latest'
}).watch(function(error, event) {
console.log("event triggered", event)
App.render();
});
});
},
render就是我們主要接口了:首先加載了6個(gè)學(xué)生的信息;然后加載了合約的內(nèi)容,包括可選學(xué)生和目前學(xué)生的評(píng)分情況。
render: function() {
var scoreInstance;
var loader = $("#loader");
var content = $("#content");
loader.show();
content.hide();
// Load account data
web3.eth.getCoinbase(function(err, account) {
if (err === null) {
App.account = account;
$("#accountAddress").html("Your Account: " + account);
}
});
// Load contract data
App.contracts.Score.deployed().then(function(instance) {
scoreInstance = instance;
return scoreInstance.studentsNum();
}).then(function(studentsNum) {
var studentsResults = $("#studentsResults");
studentsResults.empty();
var studentsSelect = $('#studentsSelect');
studentsSelect.empty();
for (var i = 1; i <= studentsNum; i++) {
scoreInstance.students(i).then(function(student) {
var id = student[0];
var name = student[1];
var scoredTimes = student[2];
// Render student Result
var studentTemplate = "<tr><th>" + id + "</th><td>" + name + "</td><td>" + scoredTimes + "</td></tr>"
studentsResults.append(studentTemplate);
// Render student ballot option
var studentOption = "<option value='" + id + "' >" + name + "</ option>"
studentsSelect.append(studentOption);
});
}
return scoreInstance.TAs(App.account);
}).then(function(hasSelected) {
// Do not allow a user to select
if(hasSelected) {
$('form').hide();
}
loader.hide();
content.show();
}).catch(function(error) {
console.warn(error);
});
},
最后定義了事件的發(fā)生:
castSelect: function() {
var studentId = $('#studentsSelect').val();
App.contracts.Score.deployed().then(function(instance) {
return instance.select(studentId, { from: App.account });
}).then(function(result) {
// Wait for selects to update
$("#content").hide();
$("#loader").show();
}).catch(function(err) {
console.error(err);
});
}
};
前端設(shè)計(jì)
最后我們還設(shè)計(jì)了一個(gè)和諧友好的前端:
<!DOCTYPE html>
<html lang="en">
<body background="https://ws3.sinaimg.cn/large/006tNbRwgy1fy9tabq6tsj30u00yqq3p.jpg">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->
<title>Score Chain</title>
<!-- Bootstrap -->
<link href="css/bootstrap.min.css" rel="stylesheet">
<!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script>
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
<![endif]-->
</head>
<body>
<div class="container" style="width: 500px;">
<div class="row">
<div class="col-lg-12">
<h1 class="text-center">Score Chain</h1>
<hr/>
<br/>
<div id="loader">
<p class="text-center">Loading...</p>
</div>
<div id="content" style="display: none;">
<table class="table">
<thead>
<tr>
<th scope="col">Id</th>
<th scope="col">Name</th>
<th scope="col">Selects</th>
</tr>
</thead>
<tbody id="studentsResults">
</tbody>
</table>
<hr/>
<form onSubmit="App.castSelect(); return false;">
<div class="form-group">
<label for="studentsSelect">Select Student</label>
<select class="form-control" id="studentsSelect">
</select>
</div>
<button type="submit" class="btn btn-primary">Select</button>
<hr />
</form>
<p id="accountAddress" class="text-center"></p>
</div>
</div>
</div>
</div>
<!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
<!-- Include all compiled plugins (below), or include individual files as needed -->
<script src="js/bootstrap.min.js"></script>
<script src="js/web3.min.js"></script>
<script src="js/truffle-contract.js"></script>
<script src="js/app.js"></script>
</body>
</html>
Localhost:3000 運(yùn)行我們的客戶端:
npm run dev
一直loading,這時(shí)候我們的小狐貍??——MetaMask就派上用場(chǎng)了!

我們用的是主以太坊網(wǎng)絡(luò),應(yīng)該用Ganache定義的私有鏈端口:

點(diǎn)擊Costume RPC設(shè)置http://127.0.01:7545,重新加載,頁(yè)面顯示正常:

但是我們注冊(cè)的賬戶是沒(méi)錢(qián)的,無(wú)法應(yīng)用合約,打開(kāi)Ganache私有鏈中的一個(gè)賬戶,復(fù)制私鑰??,在Metamask上導(dǎo)入新賬戶:

我們?yōu)镹ino進(jìn)行一次打分:選擇Nino,系統(tǒng)彈出一個(gè)標(biāo)簽,用來(lái)確認(rèn)交易。

玄學(xué)問(wèn)題:這個(gè)過(guò)程可能有時(shí)候會(huì)發(fā)生錯(cuò)誤,基本上是RPC網(wǎng)絡(luò)連接不佳、私有鏈連接不暢造成的,重啟端口或者更換一個(gè)賬號(hào)打分即可。*
為Nino成功打分,可以看見(jiàn),Select選擇框和按鍵沒(méi)有了(目前規(guī)定一個(gè)賬戶不能重復(fù)打分):

TA4打分是需要花錢(qián)的,因此可以看見(jiàn)錢(qián)變少了:

再用其他賬戶給學(xué)生們打分吧!

系統(tǒng)說(shuō)明
因?yàn)镽PC連接不穩(wěn)定,經(jīng)常會(huì)出現(xiàn)報(bào)錯(cuò):tx的nounce不正確,因此需要頻繁地更換賬戶,這個(gè)問(wèn)題影響了系統(tǒng)的實(shí)用性。另外這個(gè)系統(tǒng)暫時(shí)不允許同一個(gè)賬戶多次打分,為了確保每一次打分都可以被清晰地追溯。
參考資料
吃水不忘挖井人,在此感謝給我?guī)?lái)幫助的重要參考: