“我總偏向?qū)?quán)利分散于網(wǎng)絡(luò)。這樣一來葡缰,就沒任何組織能輕松獲取控制淆衷。我不相信巨大的中央組織缸榄,天性使然∽U”——Bob Tayor, ARPANET締造者
今年年初的“三點鐘區(qū)塊鏈群”徹底激起了以加密數(shù)字貨幣的主的區(qū)塊鏈浪潮甚带,資本的驅(qū)動加深了人們對區(qū)塊鏈技術(shù)的狂熱。然而佳头,作為區(qū)塊鏈3.0的時代的更廣的應(yīng)用來臨鹰贵,技術(shù)如何落地也是在初期需要解決的第一道坎。
所以今天就來試一下如何在以太坊上建立智能合約應(yīng)用(Dapp)畜晰,開發(fā)一個普通應(yīng)用的該有的登錄注冊砾莱,以便我們第一時間嘗鮮瑞筐。
為什么要使用區(qū)塊鏈凄鼻?
作為一名標準的web開發(fā)人員,在開始一門新技術(shù)之前聚假,需要仔細考慮一個問題就是:基于現(xiàn)有的業(yè)務(wù)如果用上區(qū)塊鏈會更好嗎块蚌?
回答這個問題之前,你需要了解區(qū)塊鏈是什么膘格?優(yōu)勢是什么峭范?這個問題就不在這里展開了,這是個相當大的話題瘪贱,不清楚的同學可以移步到這里參考一下纱控。《區(qū)塊鏈-百度百科》辆毡、《區(qū)塊鏈技術(shù)是什么?未來可能用于哪些方面甜害?》
那么區(qū)塊鏈本質(zhì)上就是一個去中心化的數(shù)據(jù)庫舶掖,只不過這個數(shù)據(jù)庫沒有中心服務(wù)器、數(shù)據(jù)無法篡改同時一定程度上能夠很好的保護數(shù)據(jù)隱私尔店。對如今的互聯(lián)網(wǎng)來說眨攘,聽起來很具有革命性的技術(shù)。所以如果對于一款涉及到數(shù)據(jù)私密性嚣州、永久性安全性高的應(yīng)用鲫售,這個確實是非常適合的。
現(xiàn)在在金融该肴、醫(yī)療情竹、溯源、社交等等領(lǐng)域匀哄,很多公司逐漸開始試水更廣泛的應(yīng)用鲤妥。而只靠發(fā)幣炒幣,這畢竟是種投機取巧的行為拱雏。
以太坊入門必備基礎(chǔ)
接下來將會從零開始搭建基于以太坊web3js項目棉安,開始閱讀之前,你需要熟練前端或后臺JavaScript語法铸抑,熟悉區(qū)塊鏈思想和原理贡耽,如果能了解solidity語法更好,因為接下來我們會用到它鹊汛,和js很像的一門語言蒲赂。
為了能夠方便大家能夠快速的了解,提供了下面幾個資料供參考:
- 《ethereum官網(wǎng)》以太坊官網(wǎng)刁憋。
- 《sails官方文檔》一款后臺的nodejs框架滥嘴。
- 《 we3.js 文檔1.0版本》以太坊上的前端框架,可實現(xiàn)與合約交互至耻。
- 《solidity 文檔》以太坊的智能合約語言若皱,熟悉常用語法,和JavaScript語法類似尘颓。
了解上面的知識之后走触,就可以開始DAPP搭建之旅了,將從下面的路線講解:
- 搭建以太坊環(huán)境疤苹。
- 創(chuàng)建創(chuàng)世區(qū)塊互广。
- 簡單的挖礦、創(chuàng)建賬戶。
- 利用以太坊錢包查詢賬戶信息惫皱。
- 編寫智能合約像樊。
- web3.js與合約交互。
- 登錄注冊業(yè)務(wù)邏輯實現(xiàn)旅敷。
- postman接口測試
項目代碼可點擊查看https://github.com/Elliottssu/web3js-api
一凶硅、以太坊環(huán)境搭建
如果已經(jīng)有以太坊環(huán)境的同學可以跳過,接下來以mac系統(tǒng)為例介紹扫皱,windows也差不多足绅。
通過Homebrew來安裝go-ethereum
brew tap ethereum/ethereum
可以添加--devel以下命令來安裝開發(fā)分支(建議用這個):
brew install ethereum --devel
執(zhí)行geth version
查看版本號,如果正常的話即安裝成功韩脑。
二氢妈、新建創(chuàng)世區(qū)塊
在比特幣系統(tǒng)里,這個創(chuàng)世塊是被寫入源碼段多,但對于以太坊而言首量,創(chuàng)世塊可以是任何你喜歡的東西。你也可以把這個當成是系統(tǒng)的一個漏洞进苍。但是共識算法確保其它人除非納入你的創(chuàng)世塊加缘,否則是不會有效的。
創(chuàng)世區(qū)塊的目的是搭建私有鏈觉啊,作為鏈上的第一個塊拣宏,如果直接運行節(jié)點的話會同步公鏈的數(shù)據(jù),數(shù)據(jù)量會非常大杠人。如果想在同一個網(wǎng)絡(luò)中獲取數(shù)據(jù)勋乾,創(chuàng)世區(qū)塊也必須要一樣。
新建genesis.json文件內(nèi)容如下:
{
"config": {},
"nonce": "0x0000000000000042",
"mixhash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"difficulty": "0x100",
"alloc": {},
"coinbase": "0x0000000000000000000000000000000000000000",
"timestamp": "0x00",
"parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"extraData": "0x00",
"gasLimit": "0xffffffffffff"
}
上面定義了一些如挖礦難度嗡善、以太幣數(shù)量辑莫、gas消耗限制等等信息。
在當前目錄下執(zhí)行geth init genesis.json
來初始化創(chuàng)世區(qū)塊節(jié)點罩引。
至此各吨,環(huán)境配置方面已經(jīng)完成。我們可以通過下面這個命令在8545端口來啟動節(jié)點:
geth --rpc --rpccorsdomain "*" --rpcapi "personal,web3,eth,net" console
三袁铐、創(chuàng)建賬戶以及挖礦
首先我們需要創(chuàng)建第一個賬戶密碼是12345678揭蜒,執(zhí)行命令:
personal.newAccount('12345678')
[圖片上傳失敗...(image-f7dd5d-1556177082135)]
創(chuàng)建賬戶之后就可以挖礦了,注意如果有多個帳戶昭躺,挖到的以太幣默認都會進入第一個賬戶的余額里忌锯。
miner.start()
啟動挖礦,miner.stop()
停止挖礦
[圖片上傳失敗...(image-74da39-1556177082135)]
四领炫、利用以太坊錢包查詢賬戶信息
截止到現(xiàn)在,我們已經(jīng)成功的啟動以太坊節(jié)點张咳,并可以通過命令來新建賬戶帝洪,執(zhí)行挖礦來獲取以太幣操作似舵。可是通過命令我們可能無法直觀的感受在以太坊上賬戶和余額的變化葱峡。
現(xiàn)在通過以太坊官方提供的錢包砚哗,來管理賬戶和余額。下載地址https://github.com/ethereum/mist/releases
注意:推薦安裝V0.8.10版本砰奕,可以刪除已經(jīng)部署的合約蛛芥,方便調(diào)試,最新版的移除掉了改功能军援。
如果有創(chuàng)世區(qū)塊仅淑,是私有鏈的話,以太坊錢包會默認開啟私有節(jié)點胸哥,否則默認同步公鏈上的數(shù)據(jù)涯竟。
[圖片上傳失敗...(image-b382a0-1556177082135)]
自己可以嘗試用主賬號給其他賬戶轉(zhuǎn)賬,也可以新建賬號和查詢賬戶余額空厌。
五庐船、Solidity編寫智能合約
首先我們需要清楚一個問題,什么是智能合約嘲更?智能合約概念可以概括為: 一段代碼 (智能合約)筐钟,運行在可復制、共享的賬本上的計算機程序赋朦,可以處理信息盗棵,接收、儲存和發(fā)送價值北发。通俗的來講就是可以在區(qū)塊鏈上執(zhí)行的代碼纹因,因為以太坊以前的區(qū)塊鏈只能存儲比特幣上的交易信息,無法做其他事情琳拨。而智能合約的出現(xiàn)瞭恰,可以在鏈上執(zhí)行簡單的業(yè)務(wù)邏輯,這也是區(qū)塊鏈應(yīng)用落地的關(guān)鍵狱庇。
我們基礎(chǔ)已經(jīng)準備就緒惊畏,接下來就用solidity語言來寫數(shù)據(jù)的增加和查詢邏輯。
Solidity中合約的含義就是一組代碼(它的 函數(shù) )和數(shù)據(jù)(它的 狀態(tài) )密任,它們位于以太坊區(qū)塊鏈的一個特定地址上颜启。 代碼行 uint time
; 聲明一個類型為 uint
(256位無符號整數(shù))的狀態(tài)變量,叫做 time 浪讳。 你可以認為它是數(shù)據(jù)庫里的一個位置缰盏,可以通過調(diào)用管理數(shù)據(jù)庫代碼的函數(shù)進行查詢和變更。對于以太坊來說,上述的合約就是擁有合約(owning contract)口猜。在這種情況下负溪,函數(shù) set
和 get
可以用來變更或取出變量的值。
1. 定義數(shù)據(jù)結(jié)構(gòu)和變量
這里只做一個最簡單的賬戶體系济炎,定義個一個用戶的數(shù)據(jù)結(jié)構(gòu)包含用戶名川抡、用戶地址和注冊時間。
定義用戶列表數(shù)據(jù)結(jié)構(gòu)是為了存儲一個用戶名->用戶地址的映射须尚。
//user.sol
//定義用戶數(shù)據(jù)結(jié)構(gòu)
struct UserStruct {
address userAddress;
string username;
uint time;
uint index;
}
//定義用戶列表數(shù)據(jù)結(jié)構(gòu)
struct UserListStruct {
address userAddress;
uint index;
}
address[] public userAddresses; //所有地址集合
string[] private usernames; //所有用戶名集合
mapping(address => UserStruct) private userStruct; //賬戶個人信息
mapping(string => UserListStruct) private userListStruct; //用戶名映射地址
address[] private userAddresses;
這一行聲明了一個不可以被公開訪問的 address 類型的狀態(tài)變量崖堤。 address 類型是一個160位的值,且不允許任何算數(shù)操作耐床。這種類型適合存儲合約地址或外部人員的密鑰對密幔。如果是關(guān)鍵字 public 允許則你在這個合約之外訪問這個狀態(tài)變量的當前值。
mapping(address => UserStruct) private userStruct;
mapping映射將地址映射到用戶數(shù)據(jù)結(jié)構(gòu)咙咽,這個可以初略理解為一個地址所對應(yīng)的值有哪些老玛。
2. 判斷用戶名或地址是否存在
//user.sol
//判斷用戶地址是否存在
function isExitUserAddress(address _userAddress) public constant returns(bool isIndeed) {
if (userAddresses.length == 0) return false;
return (userAddresses[userStruct[_userAddress].index] == _userAddress);
}
//判斷用戶名是否存在
function isExitUsername(string _username) public constant returns(bool isIndeed) {
if (usernames.length == 0) return false;
return (keccak256(usernames[userListStruct[_username].index]) == keccak256(_username));
}
這里我們分別去判斷用戶名和地址是否存在,判斷依據(jù)是看用戶名或地址是否存在于所對應(yīng)的數(shù)組钧敞。
需要注意的是蜡豹,在JavaScript中判斷一個值是否在數(shù)組中用到的indexOf()
,但是在solidity是不支持該函數(shù)溉苛。有兩種方案:一種是循環(huán)集合來判斷是否存在镜廉,第二種是創(chuàng)建的時候為每條數(shù)據(jù)加index索引,只需按索引取值愚战。
因為第一種需要遍歷整個數(shù)組娇唯,當數(shù)據(jù)量非常大的時候效率不高,所以通過索引取值的方式更加快速寂玲。
3. 新建數(shù)據(jù)和查詢數(shù)據(jù)
對于數(shù)據(jù)的插入和查詢塔插,其實就是往數(shù)組集合中添加和讀取數(shù)據(jù)。
//user.sol
//根據(jù)用戶名查找對于的address
function findUserAddressByUsername(string _username) public constant returns (address userAddress) {
require(isExitUsername(_username));
return userListStruct[_username].userAddress;
}
//創(chuàng)建用戶信息
function createUser(address _userAddress, string _username) public returns (uint index) {
require(!isExitUserAddress(_userAddress)); //如果地址已存在則不允許再創(chuàng)建
userAddresses.push(_userAddress); //地址集合push新地址
userStruct[_userAddress] = UserStruct(_userAddress, _username, now, userAddresses.length - 1);
usernames.push(_username); //用戶名集合push新用戶
userListStruct[_username] = UserListStruct(_userAddress, usernames.length - 1); //用戶所對應(yīng)的地址集合
return userAddresses.length - 1;
}
//獲取用戶個人信息
function findUser(address _userAddress) public constant returns (address userAddresses, string username, uint time, uint index) {
require(isExitUserAddress(_userAddress));
return (
userStruct[_userAddress].userAddress,
userStruct[_userAddress].username,
userStruct[_userAddress].time,
userStruct[_userAddress].index);
}
當然拓哟,除了增加和查詢之外想许,還可對相應(yīng)的數(shù)組進行修改和刪除。這里的修改和刪除操作其實并不是真正的更改數(shù)據(jù)断序,因為區(qū)塊鏈上的數(shù)據(jù)是無法篡改的流纹。當然除非迫不得已的話,不建議直接在鏈上修改和刪除數(shù)據(jù)违诗。
六漱凝、web3.js與合約交互
現(xiàn)在我們把智能合約已經(jīng)寫好了,可以通過js來讀取和添加數(shù)據(jù)了诸迟,但在這之前需要我們部署剛才寫的合約茸炒。部署合約有一種比較快捷方便的方法愕乎,就是在以太坊錢包里部署。
[圖片上傳失敗...(image-56d6ab-1556177082135)]
需要注意的是扣典,部署完成后妆毕,需要執(zhí)行挖礦才能成功慎玖,因為部署合約(包括寫數(shù)據(jù))贮尖,需要節(jié)點通過挖礦來確認交易。
完了之后我們可以在合約列表中找到剛才部署的合約趁怔。
tips: 第一次合約部署完成湿硝,如果想要推出要執(zhí)行一次exit
,否則合約無法保存润努。
這時候可以點進去关斜,執(zhí)行寫入數(shù)據(jù)和讀取數(shù)據(jù)操作了。那么怎樣才能使用代碼進行操作呢铺浇?
先提前看一下sails文件目錄:
[圖片上傳失敗...(image-83b87-1556177082135)]
1. 安裝truffle
truffle可以將solidity語言的智能合約痢畜,編譯成.json格式的配置文件,可以用它來和web3.js交互鳍侣。
全局安裝truffle丁稀,npm install -g truffle
編譯solidity智能合約,truffle compile
執(zhí)行之后會在build目錄下輸出編譯后的結(jié)果倚聚。
2. 拷貝編譯后的文件中的abi的值
我們編譯的目的是為了拿到abi屬性所對于的配置參數(shù)线衫,手動拷貝到,nodejs的配置文件中惑折。
ps: 這種做法雖然有些傻瓜授账,但是項目官方推薦的合約部署與讀取要簡單很多。
3. web3.js讀取與創(chuàng)建合約內(nèi)容
先看看web3.js上是如何調(diào)用合約的:
讀取用methods.myMethod.call
惨驶,將調(diào)用“constant”方法并在EVM中執(zhí)行其智能合約方法白热,而不發(fā)送任何事務(wù)。注意調(diào)用不能改變智能合約狀態(tài)粗卜;修改用methods.myMethod. send
屋确,將交易發(fā)送到智能合約并執(zhí)行其方法。請注意休建,這可以改變智能合約狀態(tài)乍恐。
那現(xiàn)在就根據(jù)以太坊的合約內(nèi)容,封裝一些web3.js調(diào)用智能合約的類测砂。
//Contract.js
const web3Util = require('./Web3Util.js')
class Contract {
constructor() {
}
//user 合約
/**
* 判斷用戶名是否存在
*/
static isExitUsername(username, cb) {
web3Util.contractUser.methods.isExitUsername(username).call()
.then(result => {
cb(null, result)
})
.catch(err => {
cb(err.message)
});
}
/**
* 根據(jù)用戶名查找對于的地址
*/
static findUserAddressByUsername(username, cb) {
web3Util.contractUser.methods.findUserAddressByUsername(username).call()
.then(result => {
cb(null, result)
})
.catch(err => {
cb(err.message)
});
}
/**
* 查找用戶信息
*/
static findUser(userAddress, cb) {
web3Util.contractUser.methods.findUser(userAddress).call()
.then(result => {
cb(null, result)
})
.catch(err => {
cb(err.message)
});
}
/**
* 創(chuàng)建用戶信息 (發(fā)送合約需要先解鎖)
*/
static createUser(userAddress, username, cb) {
let options = {
from: Web3Util.ACCOUNT_ADDRESS_MAIN, //創(chuàng)建賬戶用主賬號
gas: 10000000 //最大的gas數(shù)值
}
web3Util.contractUser.methods.createUser(userAddress, username).send(options)
.then(result => {
cb(null, result)
})
.catch(err => {
cb(err.message)
});
}
}
module.exports = Contract;
上面的文件中在Web3Util.js定義了一些公共常量茵烈,如合約地址,賬戶地址等等砌些。需要注意的是在使用.send()
來創(chuàng)建合約內(nèi)容的時候要給gas
即小費呜投,讀取內(nèi)容的時候不需要加匈,這個是以太坊智能合約的必填項,關(guān)于gas是如何消耗的大家可以查閱相關(guān)資料了解仑荐。
七雕拼、登錄注冊業(yè)務(wù)邏輯實現(xiàn)
截止到目前為止,我們已經(jīng)成功的將js與solidity連接在一起并且實現(xiàn)互動粘招,那接下來就是實現(xiàn)登錄和注冊啥寇。
登錄其實就是看能否解鎖用戶,然后將用戶的個人資料返回洒扎,注冊就是調(diào)取智能合約來寫入一條記錄辑甜。
解鎖賬戶(只有解鎖才能執(zhí)行合約)方法:
//Web3Util.js
/**
* 解鎖賬戶
* @param account 賬戶名
* @param password 密碼
*/
static unlockAccount(account, password, cb) {
Web3.eth.personal.unlockAccount(account, password, 600)
.then(result => {
cb(null, result)
})
.catch(err => {
cb(err.message)
});
}
登錄注冊執(zhí)行代碼:
//AccountController.js
module.exports = {
//判斷用戶名是否存在
isExitUsername: (req, res) => {
let username = req.query.username;
if (!username) return res.json(Message.errMessage('用戶名不能為空'));
Contract.isExitUsername(username, (err, result) => {
Message.handleResult(res, err, result)
})
},
//登錄(用戶名或地址登錄)
login: (req, res) => {
let account = req.body.account
let password = req.body.password;
if (!account || !password) return res.json(Message.errMessage('用戶名或密碼不能為空'));
if (Web3.utils.isAddress(account)) { //account is address
Web3Util.unlockAccount(account, password, (err, result) => {
if (err) return res.json(Message.errMessage('用戶名或密碼錯誤'));
Contract.findUser(account, (err, result) => {
Message.handleResult(res, err, result)
})
})
} else { //account is username
Contract.findUserAddressByUsername(account, (err, address) => {
if (err) return res.json(Message.errMessage('用戶名或密碼錯誤'));
Web3Util.unlockAccount(address, password, (err, result) => {
if (err) return res.json(Message.errMessage('用戶名或密碼錯誤'));
Contract.findUser(address, (err, result) => {
Message.handleResult(res, err, result)
})
})
})
}
},
/**
* 注冊賬戶,在以太坊生成address,用戶名會寫在合約中
*/
register: (req, res) => {
let username = req.body.username
let password = req.body.password;
if (!username || !password) return res.json(Message.errMessage('用戶名或密碼不能為空'));
async.waterfall([
function (callback) { //檢查用戶名是否存在
Contract.isExitUsername(username, (err, result) => {
if (result) return res.json(Message.errMessage('用戶名已存在'));
callback(null, result)
})
},
function (result, callback) { //創(chuàng)建用戶 > 生成地址
Web3.eth.personal.newAccount(password).then(address => {
callback(null, address)
})
},
function (address, callback) { //解鎖主賬戶并合約注冊信息
Web3Util.unlockAccount(Web3Util.ACCOUNT_ADDRESS_MAIN, Web3Util.ACCOUNT__PASSWORD_MAIN, (err, result) => {
if (err) return res.json(Message.errMessage(err));
Contract.createUser(address, username, (err, result) => {
if (err) return res.json(Message.errMessage(err));
callback(err, result)
})
})
},
], (err, result) => {
Message.handleResult(res, err, result)
})
},
};
八袍冷、postman接口測試
我們已經(jīng)在router中配置好了路由磷醋,接下來使用接口調(diào)試工具來測試一下,這里使用postman來測試:
注意胡诗,開始測試之前需要開啟以太坊節(jié)點邓线,保證8545端口開啟:geth --rpc --rpccorsdomain "*" --rpcapi "personal,web3,eth,net" console
因為注冊需要更改合約數(shù)據(jù),需要挖礦來確定交易煌恢,所以為了方便調(diào)試骇陈,順便開啟挖礦:miner.start()
1.注冊賬號
[圖片上傳失敗...(image-bb913f-1556177082135)]
因為是執(zhí)行合約交易,注冊完了之后會返回本次交易詳情如塊症虑、消耗的gas等等缩歪。如果本次交易失敗,比如再注冊重復的用戶名谍憔,在solidity中做了攔截匪蝙,本次交易會失敗,失敗的標志是返回的gas是自己設(shè)置的最大值习贫。
這樣我們就在鏈上創(chuàng)建了一個address逛球,以及這個相對應(yīng)的用戶名和注冊時間信息。
2.登錄賬號(賬號同時支持address和用戶名)
[圖片上傳失敗...(image-fcc1be-1556177082135)]
后續(xù)
現(xiàn)在以及能夠通過接口與智能合約交互了苫昌,我們可以稍微加個前端頁面颤绕,就可以當成一個正常app了,只是數(shù)據(jù)庫是區(qū)塊鏈祟身,是不是很酷奥务。
當然區(qū)塊鏈上只能存儲很少的數(shù)據(jù),如果要存儲視頻或者圖片袜硫,可以借助IPFS氯葬,(是永久的、去中心化保存和共享文件的方法婉陷,這是一種內(nèi)容可尋址帚称、版本化官研、點對點超媒體的分布式協(xié)議。)配合著區(qū)塊鏈能夠?qū)崿F(xiàn)更加豐富的功能闯睹。
目前的缺點在于戏羽,讀取和存儲交易數(shù)據(jù)比較慢,這也是目前Dapp應(yīng)用無法大規(guī)模的開展的一部分原因楼吃,但這個并不會阻礙區(qū)塊鏈技術(shù)的發(fā)展始花,因為它解決的是生產(chǎn)關(guān)系,它的思想在于去中心化來防止中央組織的濫用所刀。
在我構(gòu)思這篇文章的時候衙荐,正好是Facebook創(chuàng)始人扎克伯格因數(shù)據(jù)泄漏丑聞在聽證會被輪流質(zhì)問捞挥,利用幾百萬用戶數(shù)據(jù)來干涉總統(tǒng)大選浮创。用戶隱私數(shù)據(jù)一旦被攻破或濫用或商業(yè)分析推薦,后果也是非称龊可怕斩披,這也是當今互聯(lián)網(wǎng)全球化所帶來的弊端。
所以讹俊,如果想要區(qū)塊鏈解決這樣的問題還需要多長的路要走垦沉?