javascript
初识区块链——用JS构建你自己的区块链
初識區塊鏈——用JS構建你自己的區塊鏈
區塊鏈太復雜,那我們就講點簡單的。用JS來構建你自己的區塊鏈系統,寥寥幾行代碼就可以說明區塊鏈的底層數據結構、POW挖礦思想和交易過程等。當然了,真實的場景遠遠遠比這復雜。本文的目的僅限于讓大家初步了解、初步認識區塊鏈。
文章內容主要參考視頻:Building a blockchain with Javascript?(https://www.youtube.com/playlist?list=PLzvRQMJ9HDiTqZmbtFisdXFxul5k0F-Q4)
感謝原作者,本文在原視頻基礎上做了修改補充,并加入了個人理解。
認識區塊鏈
?
區塊鏈顧名思義是由區塊連接而成的鏈,因此最基本的數據結構是Block。每個Block都含有timestamp、data、hash、previousHash等信息。其中data用來存儲數據,previousHash是前一個區塊的hash值。示意如下:
hash是對區塊信息的摘要存儲,hash的好處是任意長度的信息經過hash都可以映射成固定長度的字符串,如可用sha256:
calculateHash() {return SHA256(this.previousHash+ this.timestamp + JSON.stringify(this.data)).toString(); }Block的數據結構
?
Block的最基本數據結構如下:
class Block {constructor(timestamp, data, previousHash = '') {this.timestamp = timestamp;this.data = data;this.previousHash = previousHash;//對hash的計算必須放在最后,保證所有數據賦值正確后再計算this.hash = this.calculateHash(); }calculateHash() {return SHA256(this.previousHash + this.timestamp + JSON.stringify(this.data)).toString();} }BlockChain的數據結構
?
多個Block鏈接而成BlockChain,顯然可用用數組或鏈表來表示,如:
class BlockChain {constructor() {this.chain = [];} }創世區塊
?
正所謂萬物始于一,區塊鏈的第一個區塊總是需要人為來手動創建,這個區塊的previousHash為空,如:
createGenesisBlock() {return new Block("2018-11-11 00:00:00", "Genesis block of simple chain", ""); }區塊鏈的構造方法也應改為:
class BlockChain {constructor() {this.chain = [this.createGenesisBlock()];} }添加區塊
?
每新加一個區塊,必須保證與原有區塊鏈連接起來,即:
class BlockChain {getLatestBlock() {return this.chain[this.chain.length - 1];}addBlock(newBlock) {//新區塊的前一個hash值是現有區塊鏈的最后一個區塊的hash值;newBlock.previousHash = this.getLatestBlock().hash;//重新計算新區塊的hash值(因為指定了previousHash);newBlock.hash = newBlock.calculateHash(); //把新區塊加入到鏈中;this.chain.push(newBlock); }... }?校驗區塊鏈
?
區塊鏈數據結構的核心是保證前后鏈接、無法篡改,但是如果有人真的篡改了某個區塊,我們該如何校驗發現呢?最笨也是最自然是想法就是遍歷所有情況,逐一校驗,如:
isChainValid() {//遍歷所有區塊for (let i = 1; i < this.chain.length; i++) {const currentBlock = this.chain[i];const previousBlock = this.chain[i - 1];//重新計算當前區塊的hash值,若發現hash值對不上,說明該區塊有數據被篡改,hash值未重新計算if (currentBlock.hash !== currentBlock.calculateHash()) {console.error("hash not equal: " + JSON.stringify(currentBlock));return false;}//判斷當前區塊的previousHash是否真的等于前一個區塊的hash,若不等,說明前一個區塊被篡改,雖然hash值被重新計算正確,但是后續區塊的hash值并未重新計算,導致整個鏈斷裂if (currentBlock.previousHash !== previousBlock.calculateHash) {console.error("previous hash not right: " + JSON.stringify(currentBlock));return false;}}return true; }Just run it
?
跑起來看看,即:
let simpleChain = new BlockChain(); simpleChain.addBlock(new Block("2018-11-11 00:00:01", {amount: 10})); simpleChain.addBlock(new Block("2018-11-11 00:00:02", {amount: 20}));console.log(JSON.stringify(simpleChain, null, 4));console.log("is the chain valid? " + simpleChain.isChainValid());結果如下:
ali-186590cc4a7f:simple-chain shanyao$ node main_1.js {"chain": [{"timestamp": "2018-11-11 00:00:00","data": "Genesis block of simple chain","previousHash": "","hash": "fd56967ff621a4090ff71ce88fdd456547d1c92d2e93766b7e8791f7a5f91f89"},{"timestamp": "2018-11-11 00:00:01","data": {"amount": 10},"previousHash": "fd56967ff621a4090ff71ce88fdd456547d1c92d2e93766b7e8791f7a5f91f89","hash": "150b196268a0152e9f0e719ac131a722472a809f49bd507965029a78c7400529"},{"timestamp": "2018-11-11 00:00:02","data": {"amount": 20},"previousHash": "150b196268a0152e9f0e719ac131a722472a809f49bd507965029a78c7400529","hash": "274a7a13ed20118e8cb745654934a7e24a4d59333ba17dfbf5d4cfe0fa8a6e34"}] } is the chain valid? true注意看其中的previousHash與hash,確實是當前區塊的previousHash指向前一個區塊的hash。
篡改下試試
?
都說區塊鏈不可篡改,是真的嗎?讓我們篡改第2個區塊試試,如:
let simpleChain = new BlockChain(); simpleChain.addBlock(new Block("2018-11-11 00:00:01", {amount: 10})); simpleChain.addBlock(new Block("2018-11-11 00:00:02", {amount: 20}));console.log("is the chain valid? " + simpleChain.isChainValid());//將第2個區塊的數據,由10改為15 simpleChain.chain[1].data = {amount: 15};console.log("is the chain still valid? " + simpleChain.isChainValid()); console.log(JSON.stringify(simpleChain, null, 4));結果如下:
ali-186590cc4a7f:simple-chain shanyao$ node main_1.js is the chain valid? true hash not equal: {"timestamp":"2018-11-11 00:00:01","data":{"amount":15},"previousHash":"fd56967ff621a4090ff71ce88fdd456547d1c92d2e93766b7e8791f7a5f91f89","hash":"150b196268a0152e9f0e719ac131a722472a809f49bd507965029a78c7400529"} is the chain still valid? false {"chain": [{"timestamp": "2018-11-11 00:00:00","data": "Genesis block of simple chain","previousHash": "","hash": "fd56967ff621a4090ff71ce88fdd456547d1c92d2e93766b7e8791f7a5f91f89"},{"timestamp": "2018-11-11 00:00:01","data": {"amount": 15},"previousHash": "fd56967ff621a4090ff71ce88fdd456547d1c92d2e93766b7e8791f7a5f91f89","hash": "150b196268a0152e9f0e719ac131a722472a809f49bd507965029a78c7400529"},{"timestamp": "2018-11-11 00:00:02","data": {"amount": 20},"previousHash": "150b196268a0152e9f0e719ac131a722472a809f49bd507965029a78c7400529","hash": "274a7a13ed20118e8cb745654934a7e24a4d59333ba17dfbf5d4cfe0fa8a6e34"}] }顯然,篡改了數據之后,hash值并未重新計算,導致該區塊的hash值對不上。
再篡改下試試
?
那么,如果我們聰明點,篡改后把hash值也重新計算會如何?
let simpleChain = new BlockChain(); simpleChain.addBlock(new Block("2018-11-11 00:00:01", {amount: 10})); simpleChain.addBlock(new Block("2018-11-11 00:00:02", {amount: 20}));console.log("is the chain valid? " + simpleChain.isChainValid()); //篡改后重新計算hash值 simpleChain.chain[1].data = {amount: 15}; simpleChain.chain[1].hash = simpleChain.chain[1].calculateHash(); console.log("is the chain still valid? " + simpleChain.isChainValid()); console.log(JSON.stringify(simpleChain, null, 4));結果如下:
ali-186590cc4a7f:simple-chain shanyao$ node main_1.js is the chain valid? true previous hash not right: {"timestamp":"2018-11-11 00:00:02","data":{"amount":20},"previousHash":"150b196268a0152e9f0e719ac131a722472a809f49bd507965029a78c7400529","hash":"274a7a13ed20118e8cb745654934a7e24a4d59333ba17dfbf5d4cfe0fa8a6e34"} is the chain still valid? false {"chain": [{"timestamp": "2018-11-11 00:00:00","data": "Genesis block of simple chain","previousHash": "","hash": "fd56967ff621a4090ff71ce88fdd456547d1c92d2e93766b7e8791f7a5f91f89"},{"timestamp": "2018-11-11 00:00:01","data": {"amount": 15},"previousHash": "fd56967ff621a4090ff71ce88fdd456547d1c92d2e93766b7e8791f7a5f91f89","hash": "74d139274fb692495b7c805dd5822faa0c5b5e6058b6beef96e87e18ab83a6b1"},{"timestamp": "2018-11-11 00:00:02","data": {"amount": 20},"previousHash": "150b196268a0152e9f0e719ac131a722472a809f49bd507965029a78c7400529","hash": "274a7a13ed20118e8cb745654934a7e24a4d59333ba17dfbf5d4cfe0fa8a6e34"}] }顯然,第3個區塊的previousHash并未指向第2個區塊的hash。
是真的無法篡改嗎
?
其實并不是,如果我們再聰明一點,把后續區塊的hash值也重新計算一下,不就OK了嗎? 確實如此,如:
let simpleChain = new BlockChain(); simpleChain.addBlock(new Block("2018-11-11 00:00:01", {amount: 10})); simpleChain.addBlock(new Block("2018-11-11 00:00:02", {amount: 20}));console.log("is the chain valid? " + simpleChain.isChainValid()); //篡改第2個區塊 simpleChain.chain[1].data = {amount: 15}; simpleChain.chain[1].hash = simpleChain.chain[1].calculateHash(); //并把第3個區塊也重新計算 simpleChain.chain[2].previousHash = simpleChain.chain[1].hash; simpleChain.chain[2].hash = simpleChain.chain[2].calculateHash(); console.log("is the chain still valid? " + simpleChain.isChainValid()); console.log(JSON.stringify(simpleChain, null, 4));結果如下:
ali-186590cc4a7f:simple-chain shanyao$ node main_1.js is the chain valid? true is the chain still valid? true {"chain": [{"timestamp": "2018-11-11 00:00:00","data": "Genesis block of simple chain","previousHash": "","hash": "fd56967ff621a4090ff71ce88fdd456547d1c92d2e93766b7e8791f7a5f91f89"},{"timestamp": "2018-11-11 00:00:01","data": {"amount": 15},"previousHash": "fd56967ff621a4090ff71ce88fdd456547d1c92d2e93766b7e8791f7a5f91f89","hash": "74d139274fb692495b7c805dd5822faa0c5b5e6058b6beef96e87e18ab83a6b1"},{"timestamp": "2018-11-11 00:00:02","data": {"amount": 20},"previousHash": "74d139274fb692495b7c805dd5822faa0c5b5e6058b6beef96e87e18ab83a6b1","hash": "cc294e763c51e9357bf22d96073e643f4d51e07dd0de6e9b15d1d4f6bf6b45a8"}] }現在看來,整個區塊鏈確實完全被篡改了!!!事實上,如果你能做到篡改某個區塊的時候,把后續所有的區塊一起篡改掉,即可把整個區塊鏈篡改掉。只不過,有的時候后續區塊很多,你還要篡改的足夠快,篡改的成本也非常高。因此,區塊鏈并非完全不能被篡改,篡改是有“價格”的,更多是經濟學上的考慮。在區塊鏈的設計上,會盡可能地提高篡改的成本,讓篡改的成本遠遠大于篡改的潛在收益,這樣,整個區塊鏈就可以被認為是安全的、不可篡改的。
工作量證明(Proof-of-Work)讓區塊鏈更安全
?
如前所述,區塊鏈并非完全不可篡改,只是要提高篡改的成本。那如何提高成本呢?最笨的辦法似乎就是人為地設置障礙,即:你想參與記賬嗎?那請先把這道復雜的數學題解出來,以證明你的實力和意愿。這就是最簡單最樸素的工作量證明的思想。
出一道題
?
是什么樣的數學題呢?并不是什么高深的題目,只是看起來傻傻的,只能靠猜、靠試才能解決的題目,比如:請保證hash值的前10位全是0。
大家都知道,hash計算具備如下典型特征:
-
任意長度的信息,不管是一句話、一篇文章、還是一首歌,都可以計算出唯一的一串數字與之對應。
-
這串數字的長度是固定的。
-
計算過程是不可逆的,即你可以很容易計算出一段文本的hash值,但是你沒有辦法知道某個hash值對應的原始信息是什么。
因此,如果給你一段文本,允許你在文本最后加上一個隨機數(nonce),來保證這段文本+隨機數的hash值的前10位都是0,你沒有什么好辦法,只能不斷地嘗試不同的數字,然后期盼著運氣好的話,能盡快試出來。
為區塊增加隨機數nonce
?
前面區塊的hash計算是固定的,即:
calculateHash() {return SHA256(this.previousHash + this.timestamp + JSON.stringify(this.data)).toString();}該值無法改變,為了保證能解題,需要人為地在區塊中加入隨機數,即:
constructor(timestamp, data, previousHash = '') {this.timestamp = timestamp;this.data = data;this.previousHash = previousHash;this.nonce = 0;this.hash = this.calculateHash(); }該隨機數nonce并沒有什么特別的含義,只是為了能改變下生成不同的hash值,以使得hash值滿足要求。
相應的hash計算也做修改,即:
calculateHash() {return SHA256(this.previousHash + this.timestamp + JSON.stringify(this.data) + this.nonce).toString(); }?解題即挖礦
?
如前所述,題目類似:請改變隨機數nonce,以保證得出的hash值的前10位全是0。這用代碼簡單表達如下:
this.hash.substring(0, 10) === Array(10 + 1).join("0")即hash值開頭前10位全是0。
而至于到底是前10位還是前5位呢?顯然,位數不同,難度不同。保證前10位為0的難度遠遠大于保證前5位為0。這個位數可以被稱為難度系數(difficulty)。而挖礦的過程就是不同嘗試nonce,以使得hash值滿足條件的過程,即:
mineBlock(difficulty) {while (this.hash.substring(0, difficulty) !== Array(difficulty + 1).join("0")) {this.nonce++;this.hash = this.calculateHash();}console.log("Block mined, nonce: " + this.nonce + ", hash: " + this.hash); }?簡單起見,可以把difficulty作為區塊鏈的固定參數(注:事實上,在比特幣中difficulty是動態調整的,這樣來保證出塊時間大致是10分鐘),如:
constructor() {this.chain = [this.createGenesisBlock()];this.difficulty = 2; }而添加區塊的過程,不再是簡單直接的add,而變成了挖礦的過程,即:
addBlock(newBlock) {newBlock.previousHash = this.getLatestBlock().hash;newBlock.mineBlock(this.difficulty);this.chain.push(newBlock); }只有符合要求的區塊才能被添加。
Just run it
?
跑起來試試,即:
let simpleChain = new BlockChain(); console.log("Mining block 1..."); simpleChain.addBlock(new Block("2018-11-11 00:00:01", {amount: 10})); console.log("Mining block 2..."); simpleChain.addBlock(new Block("2018-11-11 00:00:02", {amount: 20}));console.log(JSON.stringify(simpleChain, null, 4));會發現,整個世界慢了下來,出塊明顯沒有之前快速了,結果如下:
ali-186590cc4a7f:simple-chain shanyao$ node main_2.js Mining block 1... Block mined, nonce: 464064, hash: 0000e7e1aae4fae9d245f8d4b8ce030ffe13270218c362511db6840a824a1cdb Mining block 2... Block mined, nonce: 4305, hash: 000047b449537483d7f2861a12b53a59c971d3a928b2c0110a5945bff1a82616 {"chain": [{"timestamp": 0,"data": "2018-11-11 00:00:00","previousHash": "Genesis block of simple chain","nonce": 0,"hash": "8a7b66d194b1b0b795b0c45b3f11b60e8aa97d3668c831f39ec3343c83ae41c0"},{"timestamp": "2018-11-11 00:00:01","data": {"amount": 10},"previousHash": "8a7b66d194b1b0b795b0c45b3f11b60e8aa97d3668c831f39ec3343c83ae41c0","nonce": 464064,"hash": "0000e7e1aae4fae9d245f8d4b8ce030ffe13270218c362511db6840a824a1cdb"},{"timestamp": "2018-11-11 00:00:02","data": {"amount": 20},"previousHash": "0000e7e1aae4fae9d245f8d4b8ce030ffe13270218c362511db6840a824a1cdb","nonce": 4305,"hash": "000047b449537483d7f2861a12b53a59c971d3a928b2c0110a5945bff1a82616"}],"difficulty": 4 }顯然,這里difficulty為4,所得到的區塊hash開頭為4個0。
POW的思想
?
是的,這就是整個proof-of-work的思想。看似很笨很傻的思想,事實上已經被證明,足夠的有效、足夠的安全。比特幣的pow在完全無人主導的情況下,協調了數百萬臺機器的一致性,歷經10年沒有出現過一次錯誤,這不能不說是個偉大的奇跡。事實上,這個最簡單的思想背后蘊藏著更深刻的思想,pow的本質是一個cpu投一票(one-cpu-one-vote),即請用你的cpu(算力)來表達你的看法和意見。為什么是CPU,而不是one-ip-one-vote?因為IP太廉價、造假成本太低,你很容易虛擬出大量ip。之所以選擇CPU,是因為在當時(2008年)看來,CPU資源是相當昂貴的資源,以此來保證挖礦的難度和公平性(這部分在比特幣白皮書上中本聰已經說的非常清楚了https://bitcoin.org/bitcoin.pdf)。當然,中本聰可能當時沒有想到ASIC等特定算法芯片的出現已經讓普通的CPU挖礦變得越來越難,這里篇幅有限不做擴展。
因此POW的本質是什么?本質是提供了一種錨定。將虛擬世界的比特幣與現實物理世界的CPU在某種程度上做了錨定,用現實物理世界的昂貴資源來保證比特幣的安全性。有人說比特幣挖礦太費電,完全是浪費。這其實是一種偏見,換一個角度講,比特幣可能是這個世界上最廉價的貨幣體系了。畢竟,美元的發行經歷了流血與戰爭,背后還有巨大的昂貴的國家機器、航空母艦在做后盾。而比特幣,只是消耗了一些算力、一些電費,并且這種消耗并非是完全無意義的,算力越大整個比特幣體系也會越安全。
實際上,共識機制除了POW之外,比較常見的還有DPOS(delegate-proof-of-stake)等,甚至在聯盟鏈中還有pfbt(Practical Byzantine Fault Tolerance)、raft等,這里不做擴展。
挖礦回報——利益驅動讓區塊鏈走得更遠
?
如前所述的區塊鏈過于簡單,有如下大問題:
-
每個區塊只包含一次交易。這樣會導致成本交易成本很高,事實上真實的區塊鏈,每個區塊會包含多筆交易,多筆交易會被同時打包到一個區塊中。
-
挖礦沒有回報。如果挖礦沒有回報,這個世界上誰會買礦機、耗電費為你的交易做校驗、打包呢?世界需要雷鋒,但世界的運轉不能依靠雷鋒,需要依靠的是實實在在的利益誘惑。合適的制度設計和激勵制度是區塊鏈穩健的根本。其實,在很多POW的加密貨幣中,挖礦是加密貨幣發行的唯一方式。比如比特幣總共只有2100萬個,只能通過挖礦不斷挖出來,才能進入二級市場流通。
下面就會著重解決這兩點。
定義Transaction
?
一個Transaction最基本的信息應包含:從誰轉到了誰,轉了多少錢,即:
class Transaction {constructor(fromAddress, toAddress, amount) {this.fromAddress = fromAddress;this.toAddress = toAddress;this.amount = amount;} }而每個block應包含多個Transactions,即把之前的data改為transactions:
class Block {constructor(timestamp, transactions, previousHash = '') {this.timestamp = timestamp;this.transactions = transactions;this.previousHash = previousHash;this.nonce = 0;this.hash = this.calculateHash();}.... }而blockchain的數據結構也需要做相應升級,需要增加待處理transactions和每次挖礦報酬額,即:
class BlockChain {constructor() {this.chain = [this.createGenesisBlock()];this.difficulty = 3;this.pendingTransactions = [];this.miningReward = 100;}.... }請注意這種結構關系:
-
1個Chain包含多個Block;
-
1個Block包含多個Transaction;
挖礦
?
相應地,前面的addBlock方法應該被升級為minePendingTransactions,與之前相比的最大不同在于:
-
新加的不是單純的一個區塊,而是包含了所有待處理交易信息的區塊。(這里簡單起見,把所有pendingTranactions都打包了一個區塊中,真實場景并非如此,如比特幣的原始區塊大小只有1M,裝不下的就要等待下一個區塊打包了;另外礦工實際上通常是誰付費高就優先處理誰的交易)
-
為礦工付費。一般而言,礦工挖出當前區塊之后,會生成一批向礦工地址轉賬的交易,等待下個區塊打包的時候轉賬。
如下:
//傳入礦工地址 minePendingTransactions(miningRewardAddress) {//將所有待處理交易一起打包到同一個區塊let block = new Block(Date.now(), this.pendingTransactions);//挖礦,即不斷嘗試nonce,以使得hash值滿足要求block.mineBlock(this.difficulty);console.log('Block successfully mined!');this.chain.push(block);//將礦工費交易放入到pendingTransactions,待下次處理;礦工費交易的特點是來源賬號為空;this.pendingTransactions = [new Transaction(null, miningRewardAddress, this.miningReward)]; } //創建交易,即將交易放入待處理交易池 createTransaction(transaction) {this.pendingTransactions.push(transaction); }查詢余額
?
既然有了轉賬,就自然會有查詢某個賬戶余額的需求。不過在區塊鏈中可能并不存在真的賬戶,常見的有比特幣的UTXO模型和以太坊的賬戶余額模型。顯然,在我們這里,也并不真的存在所謂的賬戶。這里的區塊鏈交易只記錄了從誰轉到誰,轉了多少錢,并沒有記錄哪個賬戶現在有多少錢。怎么才能知道某個賬戶的余額呢?最笨的方法就是遍歷區塊鏈所有的交易信息,根據from/to/amount來推算出某個賬戶的余額,即:
getBalanceOfAddress(address) {let balance = 0;for (const block of this.chain) {for (const transaction of block.transactions) {//賬戶轉出,余額減少if (transaction.fromAddress === address) {balance -= transaction.amount;}//賬戶轉入,余額增加if (transaction.toAddress === address) {balance += transaction.amount;}}}return balance;Just run it
?
跑起來看看效果,轉賬真的成功了嗎?礦工收到礦工費了嗎?即:
let simpleCoin = new BlockChain(); //首先創建兩筆交易,address1先向address2轉賬100,address2又向address1轉賬60。 simpleCoin.createTransaction(new Transaction('address1', 'address2', 100)); simpleCoin.createTransaction(new Transaction('address2', 'address1', 60));console.log('starting the miner...'); simpleCoin.minePendingTransactions('worker1-address'); //顯然如果成功,address2應該有40。 console.log('Balance of address2 is: ', simpleCoin.getBalanceOfAddress('address2')); //礦工賬戶應該有多少錢呢?按說應該是礦工費100 console.log('Balance of miner is: ', simpleCoin.getBalanceOfAddress('worker1-address'));//再創建一筆交易,address2又向address1轉賬10 simpleCoin.createTransaction(new Transaction('address2', 'address1', 10));console.log('starting the miner again...'); simpleCoin.minePendingTransactions('worker1-address'); //顯然如果成功,address2應該還剩30 console.log('Balance of address2 is: ', simpleCoin.getBalanceOfAddress('address2')); //此時礦工費應該多少呢?處理兩個區塊,應該有200吧? console.log('Balance of miner is: ', simpleCoin.getBalanceOfAddress('worker1-address'));結果如下:
ali-186590cc4a7f:simple-chain shanyao$ node main_3.js starting the miner... Block mined, nonce: 2121, hash: 000cd629157ee59494dfc08329d4cf265180c26010935993171b6881f9bae578 Block successfully mined! Balance of address2 is: 40 Balance of miner is: 0 starting the miner again... Block mined, nonce: 1196, hash: 000d5f8278ea9bf4f30c9cc05b4cc36aab8831dc5860e42c775360eb85bc238e Block successfully mined! Balance of address2 is: 30 Balance of miner is: 100可見,address2的余額符合預期;唯一稍特別的是曠工余額,為什么成功打包了,曠工余額還是0,沒收到打包費呢?因為礦工費轉賬放入到了下一個區塊,只有下一個區塊被成功打包,前一個區塊的曠工才能收到礦工費。
交易簽名與驗證
?
如前所述,似乎誰都可以發起交易,比如我想發起一筆從中本聰賬戶到我的賬戶交易,轉賬100個。是否可行呢?在前面的模型中,確實似乎誰都可以發起交易。事實上,這當然不可能是真的。截止目前的模型,還缺少重要一環,即必須對交易進行簽名,以保證:你只能從你的自己的賬戶轉出錢,你沒有別人賬戶的密碼就不可能操作別人的賬戶。
無法找回的密碼
?
密碼其實是現實世界的概念,比如銀行卡密碼、淘寶登錄密碼、自動門禁的密碼,你必須妥善保管,一旦被人知道了財產可能損失;當然,你如果懷疑密碼被盜,可以趕緊改下密碼;如果真的記不起密碼,還可以帶上身份證去銀行修改密碼。
然而,在區塊鏈的世界中,不存在改密碼、找回密碼的說法。更重要的是,在區塊鏈的世界中沒有身份證,密碼本身就是身份證。
非對稱加密
?
區塊鏈世界的唯一密碼就是私鑰。私鑰是如何而來的?是通過非對稱加密生成的,非對稱的意思就是加密和解密使用不同的密鑰。聽起來很復雜,其實思想很簡單,如下:
-
非對稱加密算法會生成一對密鑰,一個是公開密鑰publicKey,一個是私有密鑰privateKey。二者可以互相加密解密,即公鑰加密的,只有對應私鑰才能解開;私鑰加密的,只有對應公鑰才能打開。
-
無法從公鑰推導出私鑰;但可以從私鑰推導出公鑰。(絕大多數對RSA的實現,都遵循pkcs的標準,即私鑰能推出公鑰,但公鑰不能推出私鑰)
-
公鑰用于加密,私鑰用于解密:用公鑰加密的數據,只有用相應的私鑰才能解密。公鑰類似郵箱地址,所有人都知道,誰都可以往里面寄信;但只有郵箱的主人才擁有密鑰才能打開。
-
私鑰用于簽名,公鑰用于驗證:東邪收到西毒的來信,但怎么確定這信真的是西毒寫的呢?西毒把信用自己的密鑰簽名(其實就是加密),東邪收到信息之后,拿公開的西毒的公鑰去試試能否解密,若能解密則確信是西毒的來信。
讓我們簡單地生成一對公鑰私鑰來看看,即:
const EC = require('elliptic').ec; const ec = new EC('secp256k1');const key = ec.genKeyPair(); const publicKey = key.getPublic('hex'); const privateKey = key.getPrivate('hex');console.log('Public key', publicKey); console.log('Private key', privateKey);結果:
Public key 04f1aa4d934e7f2035a6c2a2ebc9daf0e9ca7d13855c2a0fb8696ab8763e5ee263c803dfa7ac5ae23b25fb98151c99f91c55e89586717965758e6663772ebccd1b Private key 1c258d67b50bda9377c1badddd33bc815eeac8fcb9aee5d097ad6cedc3d2310c這個privateKey就是你的唯一密碼,有32字節。而publicKey看起來似乎更長。但是平時看到的比特幣地址似乎很短啊?是的,這里的publicKey有65字節,而開頭1個字節是固定的0x04,除此之外的前32字節是橢圓曲線的X坐標,后32字節是橢圓曲線的Y坐標。比特幣地址之所以更短,是因為又經過了SHA256加密、RIPEMD160加密和BASE58編碼等等一系列的轉化,最后生成了類似“1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa”這樣的base58地址,這里簡單起見不做擴展。通常而言,publicKey就是你的賬戶地址,只是格式的不同,可以進行可逆轉化。
簽名你的交易
?
首先,我們需要用自己的私鑰,對發起的交易進行簽名,以表明交易確實是由本人發起的,如:
class Transaction {//計算hash,為了做簽名,因為不是直接對原始信息進行簽名,而是對hash值簽名。calculateHash() {return SHA256(this.fromAddress + this.toAddress + this.amount).toString();}//傳入私鑰signTransaction(signingKey) {//校驗來源賬戶是否是本人的地址,即來源地址是否是該私鑰對應的公鑰if (signingKey.getPublic('hex') !== this.fromAddress) {throw new Error('You cannot sign transactions for other wallets!')}const txHash = this.calculateHash();//用私鑰對交易hash進行簽名const sig = signingKey.sign(txHash, 'base64');//將簽名轉成der格式this.signature = sig.toDER('hex');console.log("signature: "+this.signature)}... }驗證交易
?
隨后,其他人收到該交易信息時,需要驗證交易是否有效,即用來源賬戶的公鑰來驗證這筆交易的簽名是否正確、是否真的是來自于fromAddress,如下:
class Transaction {isValid() {//礦工費交易fromAddress為空,不做校驗if (this.fromAddress === null) return true;//判斷簽名是否存在if (!this.signature || this.signature.length === 0) {throw new Error('No signature in this transaction');}//對fromAddress轉碼,得到公鑰(這一過程是可逆的,只是格式轉化)const publicKey = ec.keyFromPublic(this.fromAddress, 'hex');//用公鑰驗證簽名是否正確,即交易是否真的從fromAddress發起的return publicKey.verify(this.calculateHash(), this.signature);}... }上面對單個交易的有效性進行了驗證,而一個區塊包含多筆交易,所以也需要增加對區塊內所有交易驗證的方法,如:
class Block {hasValidTransactions() {//遍歷區塊內所有交易,逐一驗證for (const tx of this.transactions) {if (!tx.isValid()) {return false;}}return true;}... }相應的,createTransaction也升級為addTransaction,即不再直接創建交易,而是要對已簽名交易進行驗證,有效的交易才提交。如:
class BlockChain {addTransaction(transaction) {if (!transaction.fromAddress || !transaction.toAddress) {throw new Error('Transaction must include from and to address');}//驗證交易是否有效,有效的才能提交到交易池中if (!transaction.isValid()) {throw new Error('Cannot add invalid transaction to the chain');}this.pendingTransactions.push(transaction);}... }相應的blockchain的isChainValid方法也應升級,加入區塊內所有交易的驗證,即:
class BlockChain {isChainValid() {for (let i = 1; i < this.chain.length; i++) {const currentBlock = this.chain[i];const previousBlock = this.chain[i - 1];//校驗區塊內的所有交易是否有效if (!currentBlock.hasValidTransactions()) {return false;}if (currentBlock.hash !== currentBlock.calculateHash()) {console.error("hash not equal: " + JSON.stringify(currentBlock));return false;}if (currentBlock.previousHash !== previousBlock.calculateHash()) {console.error("previous hash not right: " + JSON.stringify(currentBlock));return false;}}return true;}... }Just run it
?
跑起來試試,如下:
const {BlockChain, Transaction} = require('./blockchain'); const EC = require('elliptic').ec; const ec = new EC('secp256k1'); //用工具生成一對私鑰和公鑰 const myPrivateKey = '1c258d67b50bda9377c1badddd33bc815eeac8fcb9aee5d097ad6cedc3d2310c'; const myPublicKey = '04f1aa4d934e7f2035a6c2a2ebc9daf0e9ca7d13855c2a0fb8696ab8763e5ee263c803dfa7ac5ae23b25fb98151c99f91c55e89586717965758e6663772ebccd1b';const myKey = ec.keyFromPrivate(myPrivateKey); //從私鑰推導出公鑰 const myWalletAddress = myKey.getPublic('hex'); //輸出看下,確實從私鑰得到了公鑰 console.log("is the myWalletAddress from privateKey equals to publicKey?", myWalletAddress === myPublicKey);let simpleCoin = new BlockChain();const trumpPublicKey = '047058e794dcd7d9fb0a256349a5e2d4d724b50ab8cfba2258e1759e5bd4c81bb6ac1b0490518287ac48f0f10a58dc00cda03ffd6d03d67158f8923847c8ad4e7d'; //發起交易,從自己賬戶向trump轉賬60 const tx1 = new Transaction(myWalletAddress, trumpPublicKey, 60); //用私鑰簽名 tx1.signTransaction(myKey); //提交交易 simpleCoin.addTransaction(tx1);console.log('starting the miner...'); simpleCoin.minePendingTransactions(myWalletAddress); //若轉賬成功,trump賬戶余額應是60 console.log('Balance of trump is: ', simpleCoin.getBalanceOfAddress(trumpPublicKey));//發起交易,從trump賬戶向你的賬戶轉回20 const tx2 = new Transaction(trumpPublicKey, myWalletAddress, 20); //仍用你的私鑰簽名,這里會報錯,你并不知道trump的密鑰,無法操作其賬戶,即你的密鑰打不開trump的賬戶; tx2.signTransaction(myKey); simpleCoin.minePendingTransactions(myWalletAddress); console.log('Balance of trump is: ', simpleCoin.getBalanceOfAddress(trumpPublicKey));結果如下:
ali-186590cc4a7f:simple-chain shanyao$ node main.js is the myWalletAddress from privateKey equals to publicKey? true signature: 3045022100b87a9199c2b3fa31ac4092b27a41a616d99df884732dfd65972dc9eacd12da7702201f7957ef25d42c17cb2f6fb2888e6a0d5c521225d9b8851ba2d228f96d878f85 starting the miner... Block mined, nonce: 15812, hash: 00081837c2ae46a1310a0873f5e3d6a1b14b072e3d32a538748fac71e0bfd91e Block successfully mined! Balance of trump is: 60 /Users/shanyao/front/simple-chain/blockchain.js:22throw new Error('You cannot sign transactions for other wallets!')顯然,第一次操作并簽名的是自己的賬戶,有私鑰能成功;第二次操作的是別人的賬戶,私鑰不對,無法提交轉賬。私鑰就是區塊鏈世界的唯一密碼、唯一通行證,只有擁有私鑰的人才能擁有對應的資產,這也許就是真正的私有財產神圣不可侵犯。
總結
?
如果看到這里,是不是覺得區塊鏈很簡單?是的,沒有想象中復雜,但其實也沒那么簡單。事實上,區塊鏈真正核心的共識機制(分布式協調一致性)以及去中心化治理,本文并未涉及。本文只是簡單介紹了區塊鏈的基本結構、基本概念和大致交易過程,幫忙大家初步認識區塊鏈,解開區塊鏈神秘的面紗。而區塊鏈本身是一個宏大的主題,還需要更多的研究,更多的思考和探索。
總結
以上是生活随笔為你收集整理的初识区块链——用JS构建你自己的区块链的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: blockchain 区块链200行代码
- 下一篇: SpringBoot + Vue + n