初识区块链——用JS构建你自己的区块链

前言

区块链太复杂,那我们就讲点简单的。用JS来构建你自己的区块链系统,寥寥几行代码就可以说明区块链的底层数据结构、POW挖矿思想和交易过程等。当然了,真实的场景远远远比这复杂。本文的目的仅限于让大家初步了解、初步认识区块链。
感谢原作者,本文在原视频基础上做了修改补充,并加入了个人理解。

认识区块链

区块链顾名思义是由区块连接而成的链,因此最基本的数据结构是Block。每个Block都含有timestamp、data、hash、previousHash等信息。其中data用来存储数据,previousHash是前一个区块的hash值。示意如下:
初识区块链——用JS构建你自己的区块链
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!')
显然,第一次操作并签名的是自己的账户,有私钥能成功;第二次操作的是别人的账户,私钥不对,无法提交转账。私钥就是区块链世界的唯一密码、唯一通行证,只有拥有私钥的人才能拥有对应的资产,这也许就是真正的私有财产神圣不可侵犯。

总结

如果看到这里,是不是觉得区块链很简单?是的,没有想象中复杂,但其实也没那么简单。事实上,区块链真正核心的共识机制(分布式协调一致性)以及去中心化治理,本文并未涉及。本文只是简单介绍了区块链的基本结构、基本概念和大致交易过程,帮忙大家初步认识区块链,解开区块链神秘的面纱。而区块链本身是一个宏大的主题,还需要更多的研究,更多的思考和探索。
上一篇:我的博客即将入驻“云栖社区”,诚邀技术同仁一同入驻。


下一篇:git 常用命令