前言

在上一篇文章《[计算机科学] 从零开始创建一个区块链》,我们介绍了什么是区块链,如何创建区块和如何将区块链起来。

这一节我们将介绍什么是transaction,和如何用密钥来保障我们的区块链数据安全。

在后来的时候,我发现上一篇中的引用文献从零开始创建一个区块链,其实很大程度来自YouTuber - Simply Explained,所以这里将直接用Simply Explained的介绍轨迹来走。Simply Explained用了5个视频来解释了Blockchain这个主题,Building a blockchain with Javascript

  1. 用JS来创建一个区块链
  2. Blockchain的工作证明
  3. Blockchain的工作证明奖励和transaction
  4. Blockchain的签名
  5. 如何用Angular来写一个Blockchain前端

第一篇文章《[计算机科学] 从零开始创建一个区块链》,说的便是1与2的内容;我们这篇文章将会说3与4的内容,应该不会再讲5的内容了,5只是一个前段的implementation而已。

那我们开始吧 :D

正文

1. 复习源码

我们的Block和 Blockchain源码相比于之前而言,变化了许多,如下(同时我们这里用的是Javascript,上一篇用的是Python):

const SHA256 = require('crypto-js/sha256');
const EC = require('elliptic').ec;
const ec = new EC('secp256k1');

class Record {
    constructor(uniqueID, MAC, publicKey) {
        this.uniqueID = uniqueID;
        this.MAC = MAC;
        this.publicKey = publicKey;
    }

    calculateHash() {
        return SHA256(this.uniqueID + this.MAC).toString();
    }

    signRecord(key) {
        const hash = this.calculateHash();
        const sig = key.sign(hash, 'base64'); 
        this.signature = sig.toDER('hex');
    }

    isValid() {
        if(!this.signature || this.signature.length === 0) {
            throw new Error('No signature in this record.');
        }

        const publicKey = ec.keyFromPublic(this.publicKey, 'hex');
        return publicKey.verify(this.calculateHash(), this.signature);
    }
}

class Block {
    constructor(records) {
        this.timestamp = Date.now();
        this.records = records;
        this.previousHash = '';
        this.hash = this.calculateHash();
        this.nonce = 0;
    }

    calculateHash() {
        return SHA256(this.previousHash + this.timestamp + JSON.stringify(this.records) + this.nonce).toString();
    }

    hasValidRecords() {
        for(const record of this.records) {
            if(record.isValid()) {
                return false;
            }
        }
        return true;
    }

    mineBlock(difficulty) {
        while(this.hash.substring(0, difficulty) !== Array(difficulty + 1).join("0")) {
            this.nonce++;
            this.hash = this.calculateHash();
        }
        console.log('Block mined: ' + this.hash);
    }

    setPreviousHash(previousHash) {
        this.previousHash = previousHash;
    }
}

class Blockchain {
    constructor() {
        this.difficulty = 3;
        this.chain = [this.createGenesisBlock()];
        this.pendingRecords = [];
    }

    createGenesisBlock() {
        let genesisBlock = new Block("Genesis Block");
        genesisBlock.setPreviousHash("0000000000000000000000000000000000000000000000000000000000000000");
        genesisBlock.mineBlock(this.difficulty);
        return genesisBlock;
    }

    getLastestBlock() {
        return this.chain[this.chain.length - 1];
    }

    minePendingRecords() {
        let block = new Block(this.pendingRecords)
        block.setPreviousHash(this.getLastestBlock().hash);
        console.log('Block mining...');
        block.mineBlock(this.difficulty);
        this.chain.push(block);
        console.log('Block successfully mined!');
        this.pendingRecords = [];
    }

    addRecord(record) {
        if(!record.uniqueID) {
            throw new Error('Record unique identifier missing!')
        }
        if(!record.MAC) {
            throw new Error('Record signed MAC missing!')
        }
        if(!record.isValid()) {
            throw new Error('Cannot add invalid record to the chain')
        }
        this.pendingRecords.push(record);
    }

    isChainValid() {
        for(let i = 1; i < this.chain.length; i++) {
            const currentBlock = this.chain[i];
            const previousBlock = this.chain[i - 1];
            // If 

            // If current block hash is still valid
            if(currentBlock.hash !== currentBlock.calculateHash()) {
                return false;
            }
            // If current block and previous block are not linked
            if(currentBlock.previousHash !== previousBlock.hash) {
                return false;
            }
        }
        return true; 
    }
}

module.exports.Blockchain = Blockchain;
module.exports.Record = Record;

这里我们会发现,我们除了本来有的Block和Blockchain以外,我们多了一个叫做Record的东西,这就是我们要讲的第一个东西,Transactions。

2. Transactions

为什么会有一个Transaction类呢,这是因为如果我们把Blockchain比做一个记账本,那么Block就是记账本的每一页,那我们不可能每一笔账目,都会用记账本的一整页吧,那会不会是有点太多了?这对于Blockchain来说就是,每一笔交易,你都要mine一次,那么就会用掉很多很多的空间去mine仅仅一单的交易,这是很浪费的。所以我们这引入Transaction类作为一笔交易,当一个Block里面Transaction数量足够多的时候,才会进行mining,否则就存入一个PendingTransaction一个暂存的transaction中。

那么,有了这样一个前提的条件,我们来看看我们的代码:

2.1 Class Record

先来看看Record类的代码

class Record {
    constructor(uniqueID, MAC, publicKey) {
        this.uniqueID = uniqueID;
        this.MAC = MAC;
        this.publicKey = publicKey;
    }

    calculateHash() {
        return SHA256(this.uniqueID + this.MAC).toString();
    }

    signRecord(key) {
        const hash = this.calculateHash();
        const sig = key.sign(hash, 'base64'); 
        this.signature = sig.toDER('hex');
    }

    isValid() {
        if(!this.signature || this.signature.length === 0) {
            throw new Error('No signature in this record.');
        }

        const publicKey = ec.keyFromPublic(this.publicKey, 'hex');
        return publicKey.verify(this.calculateHash(), this.signature);
    }
}

代码中有一个构造函数constructor(),中有Record类的属性:uniqueIDMACpublicKey。这是因为这其实是一个Software Bill Of Materials (SBOM)的数据,uniqueID是SBOM特有的ID,MAC是指SBOM代表的整个程序的MAC值,publicKey是指SBOM所属签名者的公钥。

calculateHash()是为了计算record的hash值,具体为何要计算record的hash值一个是为了防篡改,另一个是为了方便签名,至于签名部分我们会在密钥加密部分提交。同时,signRecord()isValid()我们也会在后续关于密钥加密部分再次提及。

2.2 Class Block

下面的Class Block的代码

class Block {
    constructor(records) {
        this.timestamp = Date.now();
        this.records = records;
        this.previousHash = '';
        this.hash = this.calculateHash();
        this.nonce = 0;
    }

    calculateHash() {
        return SHA256(this.previousHash + this.timestamp + JSON.stringify(this.records) + this.nonce).toString();
    }

    hasValidRecords() {
        for(const record of this.records) {
            if(record.isValid()) {
                return false;
            }
        }
        return true;
    }

    mineBlock(difficulty) {
        while(this.hash.substring(0, difficulty) !== Array(difficulty + 1).join("0")) {
            this.nonce++;
            this.hash = this.calculateHash();
        }
        console.log('Block mined: ' + this.hash);
    }

    setPreviousHash(previousHash) {
        this.previousHash = previousHash;
    }
}

constructor()是Block的属性,有时间戳、记录组、之前Block的hash、当前自己的hash和nonce(nonce是为了工作证明,也是为了isValid()认证,hash值需要根据nonce算出。)

calculateHash()是计算区块自己的hash值。

hasValidRecords()在后续密钥加密会提到,只是循环遍历自己的记录组,调用每个记录的isValid(),看他们是否都是合法的。

mineBlock(difficulty)挖矿,在文章《[计算机科学] 从零开始创建一个区块链》中提到过,是为了工作证明。

setPreviousHash(previousHash)为previousHash属性的setter方法,如果属性都为私有private的话,那么属性将会需要Setter方法或者Getter方法。

2.3 Class Blockchain

接下来是Blockchain的代码,

class Blockchain {
    constructor() {
        this.difficulty = 3;
        this.chain = [this.createGenesisBlock()];
        this.pendingRecords = [];
    }

    createGenesisBlock() {
        let genesisBlock = new Block("Genesis Block");
        genesisBlock.setPreviousHash("0000000000000000000000000000000000000000000000000000000000000000");
        genesisBlock.mineBlock(this.difficulty);
        return genesisBlock;
    }

    getLastestBlock() {
        return this.chain[this.chain.length - 1];
    }

    minePendingRecords() {
        let block = new Block(this.pendingRecords)
        block.setPreviousHash(this.getLastestBlock().hash);
        console.log('Block mining...');
        block.mineBlock(this.difficulty);
        this.chain.push(block);
        console.log('Block successfully mined!');
        this.pendingRecords = [];
    }

    addRecord(record) {
        if(!record.uniqueID) {
            throw new Error('Record unique identifier missing!')
        }
        if(!record.MAC) {
            throw new Error('Record signed MAC missing!')
        }
        if(!record.isValid()) {
            throw new Error('Cannot add invalid record to the chain')
        }
        this.pendingRecords.push(record);
    }

    isChainValid() {
        for(let i = 1; i < this.chain.length; i++) {
            const currentBlock = this.chain[i];
            const previousBlock = this.chain[i - 1];
            // If all records in block is valid
            if(!currentBlock.hasValidRecords()) {
                return false;
            }
            // If current block hash is still valid
            if(currentBlock.hash !== currentBlock.calculateHash()) {
                return false;
            }
            // If current block and previous block are not linked
            if(currentBlock.previousHash !== previousBlock.hash) {
                return false;
            }
        }
        return true; 
    }
}

constructor()中代表了Blockchain类所具有的属性分别为difficulty(简单而言就是hash值要满足前面有多少个阿拉伯数字0)、chain为区块数组(也就是区块链里面的区块)、pendingRecords(为下一个区块mine之前暂存的记录数组)。

createGenesisBlock()我们需要自己创造一个独立的创世区块,也在上一篇文章中讲过。

getLastestBlock()获得最后一个区块。

minePendingRecords()将当前所有的暂存记录打包,开始生成下一个区块,也就是俗称的“挖矿”。

addRecord(record)增加一个record记录到暂存的pendingRecords数组之中。

isChainValid()是来检查链是否是正确的。

2.4 如果你们implement了?

到目前为止,如果你们直接复制了源码并且运行,你们会发现如果一切配置正确的话,一个正确的区块链用isChainValid()会带来false,而不会带来true。

这是为什么呢?大家可以思考一下,然后我会在下面给出结果。

那是因为我们需要改一个东西,在Class Block里面犯了一个很低级的错误。在Class Block的hasValidRecords()中,我们只要改成如下这样就可以了。和以前有什么不同呢?

hasValidRecords() {
        for(const record of this.records) {
            if(!record.isValid()) {
                return false;
            }
        }
        return true;
    }

其实没有什么不同,我们只是在原来的if(record.isValid()),前面加了一个感叹号而已,sigh =。=...。

3. 密钥加密

先来说明一下,我们为什么需要密钥?因为区块链这个技术带给我们的只有不可篡改性,可是他并没有对任何人进行认证,也就是说,任何人都可以随意在上面加信息,那么如果有假信息混淆在真实信息里面的话,我们就无法区分哪些是假信息,哪些是真信息了。这个很显然是我们不想要的。于是乎,我们就需要有认证过程,而认证过程就设计了,密钥加密。

密钥一般分为对称密码和非对称密码,对于区块链而言,我们想要做认证,用对称密码是不太可行的,因为对称密码意味着每个人都要有你可以用于加密的密码,那么很有可能就有不信任的人进行恶意行为,那么很明显是我们不想要的,然而非对称密码由于公钥和私钥的存在,这样的话不信任的人拿公钥,我们自己保管我们的私钥,这就是一个很安全的事情了。那么废话不多说下面开始代码。

3.1 公共密钥生成

公共密钥生成需要用到一个javascript的包elliptic,我们这里用的是secp256k1算法,这又称为椭圆曲线算法,是用来计算比特币的公钥算法。

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();
console.log('Private Key: ', privateKey);

console.log();
console.log('Public Key: ', publicKey);

代码内容没有什么可以说的,很简单的生成了公钥和私钥,我们可以将生成的公钥、私钥存下来,方便我们之后使用。(现实生活中请不要将任何私钥以任何形式放于网络之上!)

{
    "Key": [
        {
            "Leuven": {
                "Private Key": "502e811875a91e33c4386fae566dcae6bd02c539ecc834f012121f017760774f",
                "Public Key": "046bde69a47cbbf0a6f24a71108005cb58eb04cd8254783a535b98ae584cae8461a0c12ffeb3d5e2ab96d2d8dfed711f635ce6e51d8b41f319fd7572ee8dfbde00"
            }
        },
        {
            "ESAT": {
                "Private Key": "fefc83c37db24b44f61fb04eea8a5dc23cf345aeff7415d8c38e3265ddb44e8a",
                "Public Key": "0458464684db664b5de2373e69185606c8041f2087c15ef644f14736a5f723b34ae3d1a73ebb716e552b65b92c33a5013dce732754cfd81abf06b6906eeb9a0c4a"
            }
        },
        {
            "Xinhai Zou": {
                "Private Key": "d2ed48c0ac7efd493712ce950e8eec0fef20201ae04136be0dbc3ec01a91e292",
                "Public Key": "04b505256b319320e6fa92f4a39535763868ce15b5ad8be58353b7383156a14cc49180882575c2d07698f56a148c501b65391e8464c3d866fc5ba6ccd886e76f80"
            }
        }
    ]
}

3.2 Class Record的一点补充

这里回来说一些对于Class Record的一点密码学方法的补充。

class Record {
    constructor(uniqueID, MAC, publicKey) {
        this.uniqueID = uniqueID;
        this.MAC = MAC;
        this.publicKey = publicKey;
    }

    calculateHash() {
        return SHA256(this.uniqueID + this.MAC).toString();
    }

    signRecord(key) {
        const hash = this.calculateHash();
        const sig = key.sign(hash, 'base64'); 
        this.signature = sig.toDER('hex');
    }

    isValid() {
        if(!this.signature || this.signature.length === 0) {
            throw new Error('No signature in this record.');
        }

        const publicKey = ec.keyFromPublic(this.publicKey, 'hex');
        return publicKey.verify(this.calculateHash(), this.signature);
    }
}

signRecord(key)是用来签名的,key是private key(私钥),用于对文件、hash值来签名,这也正是为什么我们需要对hash值计算的原因。

isValid()是用public key(公钥)来验证,是否文件的签名真的来自于正确的用户私钥,true则是,false则不是。

4. 验证

4.1 无attack

const {Blockchain, Record} = require('./blockchain');
const EC = require('elliptic').ec;
const ec = new EC('secp256k1');

// Leuven
const leuvenPrivateKey = ec.keyFromPrivate('502e811875a91e33c4386fae566dcae6bd02c539ecc834f012121f017760774f');
const leuvenPublicKey = leuvenPrivateKey.getPublic('hex');
leuvenRecord = new Record('SBOM-Leuven', '123456', leuvenPublicKey);
leuvenRecord.signRecord(leuvenPrivateKey);
// ESAT
const esatPrivateKey = ec.keyFromPrivate('fefc83c37db24b44f61fb04eea8a5dc23cf345aeff7415d8c38e3265ddb44e8a');
const esatPublicKey = esatPrivateKey.getPublic('hex');
esatRecord = new Record('SBOM-ESAT', '122333', esatPublicKey);
esatRecord.signRecord(esatPrivateKey);
// Xinhai Zou
const xzouPrivateKey = ec.keyFromPrivate('d2ed48c0ac7efd493712ce950e8eec0fef20201ae04136be0dbc3ec01a91e292');
const xzouPublicKey = xzouPrivateKey.getPublic('hex');
xzouRecord = new Record('SBOM-XHZ', '111336', xzouPublicKey);
xzouRecord.signRecord(xzouPrivateKey);

let projectL = new Blockchain();
projectL.addRecord(leuvenRecord);
projectL.addRecord(esatRecord);
projectL.minePendingRecords();
projectL.addRecord(xzouRecord);
projectL.minePendingRecords();

console.log('Is projectL still valid? ' + projectL.isChainValid());

// projectL.chain[1].records[0] = new Record('SBOM-Leuven','123333',leuvenPublicKey);
// console.log('Is projectL still valid? ' + projectL.isChainValid());

我们得到的结果是

Block mined: 000b10e6365d0dd622d7c67277a56f04e224bf12974d99ebd130468f4503f680
Block mining...
Block mined: 00066182520ab18a2b5603cf91a285dc761dd1013c829d6d6ff06962c028993d
Block successfully mined!
Block mining...
Block mined: 000a5f534b2f18c00883693dca28465351ffbd12419e90c0436ff64d3e2067ac
Block successfully mined!
Is projectL still valid? true

4.2 直接更改数据attack

这个时候如果我们妄想改变任何一个记录,比如说projectL.chain[1].records[0] = new Record('SBOM-Leuven','123333',leuvenPublicKey);的话,结果甚至会变成

Error: No signature in this record.
    at Record.isValid (/Users/seanzou/Documents/NodeJS/blockchain/blockchain.js:24:19)
    at Block.hasValidRecords (/Users/seanzou/Documents/NodeJS/blockchain/blockchain.js:47:24)
    at Blockchain.isChainValid (/Users/seanzou/Documents/NodeJS/blockchain/blockchain.js:113:30)
    at Object.<anonymous> (/Users/seanzou/Documents/NodeJS/blockchain/main.js:34:52)
    at Module._compile (node:internal/modules/cjs/loader:1256:14)
    at Module._extensions..js (node:internal/modules/cjs/loader:1310:10)
    at Module.load (node:internal/modules/cjs/loader:1119:32)
    at Module._load (node:internal/modules/cjs/loader:960:12)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12)
    at node:internal/main/run_main_module:23:47

4.3 未认证signature attack

没有Signature,当然如果你自己任意创建一个Signature去的话,也会提示Signature不合法,代码如下:

// Sign change attack
// Malivious
const malPrivateKey = ec.keyFromPrivate('b0c42b19d558a9519814671c884fb02e2baad878e23b914d804bdb9b9084a3b0');
projectL.chain[1].records[0].signRecord(malPrivateKey);

我们用一个不是区块链上面的签名去签名的话,结果仍然是会显示false的。第一个true是正确的签名,第二个false是错误的签名。

Block mined: 000384cfb0ff3530768109e7fca057a900d6af7680464c9ea85e89804e3f3861
Block mining...
Block mined: 0003c5755949fd689ab3db138856b26e5b59e57a52cea6791c94c971e759e5f0
Block successfully mined!
Block mining...
Block mined: 000b89984312903a56f52a0da8838e0eb6ec189ffc38e2e3d300d985efa7f8df
Block successfully mined!
Is projectL still valid? true
Is projectL still valid? false

4.4 signature,public一切都改变了的attack

我们下面一个除了用非法的私钥进行签名了以外,还将区块链上面的public key给改了,代码如下:

// Sign and message both change
const malPrivateKey = ec.keyFromPrivate('b0c42b19d558a9519814671c884fb02e2baad878e23b914d804bdb9b9084a3b0');
const malPublicKey = malPrivateKey.getPublic('hex');
leuvenRecord = new Record('SBOM-Leuven', '123456', malPublicKey);
leuvenRecord.signRecord(malPrivateKey);
projectL.chain[1].records[0] = leuvenRecord;

结果如下,我们可以看出仍然被区块链认为是非法区块,这是因为即使我们现在区块的签名可以被认证通过,但是我们的区块链完整性无法保证,区块后面的hash是需要签名的public key的属性的,所以如果我们将区块中的public key改为malicious的public key的话,区块链的完整性将会被破坏,这也会被认为是非法的。

Block mined: 000d104bba1927b9d1dfcd28e075ac7fcb69f4474d01f91f56e6a35307cd72d3
Block mining...
Block mined: 0004a4f5cdb9f87f74fbbaa0f38ec416d9ff7be3e0de1d6da2d68afe2c03307a
Block successfully mined!
Block mining...
Block mined: 000e26c769a9209f055da408c96787ca510e05eb0c42401efb62a70998fd5391
Block successfully mined!
Is projectL still valid? true
Is projectL still valid? false

总结

本文介绍了区块链的transaction和密钥加密。

我们可以看出在implement了密钥加密的区块链上,其完整性和验证性是很安全的。

最后的最后,还有一个挺重要的就是介绍了如何implement密钥算法。

参考

[1] 利用Javascript创建一个区块链(区块链,第一部分)
[2] Implementing Proof-of-Work in Javascript (Blockchain, part 2)
[3] Mining rewards & transactions - Blockchain in Javascript (part 3)
[4] Signing transactions - Blockchain in Javascript (part 4)
[5] Angular frontend - Blockchain in Javascript (part 5)

Q.E.D.


立志做一个有趣的碳水化合物