前言
区块链太复杂,那我们就讲点简单的。用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:22 throw new Error('You cannot sign transactions for other wallets!')
显然,第一次操作并签名的是自己的账户,有私钥能成功;第二次操作的是别人的账户,私钥不对,无法提交转账。私钥就是区块链世界的唯一密码、唯一通行证,只有拥有私钥的人才能拥有对应的资产,这也许就是真正的私有财产神圣不可侵犯。
总结
如果看到这里,是不是觉得区块链很简单?是的,没有想象中复杂,但其实也没那么简单。事实上,区块链真正核心的共识机制(分布式协调一致性)以及去中心化治理,本文并未涉及。本文只是简单介绍了区块链的基本结构、基本概念和大致交易过程,帮忙大家初步认识区块链,解开区块链神秘的面纱。而区块链本身是一个宏大的主题,还需要更多的研究,更多的思考和探索。