前言
在上一篇文章《[计算机科学] 从零开始创建一个区块链》,我们介绍了什么是区块链,如何创建区块和如何将区块链起来。
这一节我们将介绍什么是transaction,和如何用密钥来保障我们的区块链数据安全。
在后来的时候,我发现上一篇中的引用文献从零开始创建一个区块链,其实很大程度来自YouTuber - Simply Explained,所以这里将直接用Simply Explained的介绍轨迹来走。Simply Explained用了5个视频来解释了Blockchain这个主题,Building a blockchain with Javascript:
- 用JS来创建一个区块链
- Blockchain的工作证明
- Blockchain的工作证明奖励和transaction
- Blockchain的签名
- 如何用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类的属性:uniqueID
、MAC
和publicKey
。这是因为这其实是一个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.