Block Chain -- Proof Of Work

本文最后更新于:8 个月前

区块链共识机制 - POW 工作量证明 Proof Of Work 学习

工作量证明PoW(Proof of Work),通过算力的比拼来选取一个节点,由该节点决定下一轮共识的区块内容(记账权)。PoW要求节点消耗自身算力尝试不同的随机数(nonce),从而寻找符合算力难度要求的哈希值,不断重复尝试不同随机数直到找到符合要求为止,此过程称为“挖矿”。具体的流程如下图:

img

第一个找到合适的nonce的节点获得记账权。节点生成新区块后广播给其他节点,其他节点对此区块进行验证,若通过验证则接受该区块,完成本轮共识,否则拒绝该区块,继续寻找合适的nonce。

来自–一文读懂主流共识机制:PoW、PoS和DPoS - 知乎 (zhihu.com)

这里通过NodeJs来实现–程序员来讲讲什么是区块链 | 小白也能听懂的通俗解释 | 区块链原理 | 比特币 | 数字货币_哔哩哔哩_bilibili

1、简易区块链搭建

区块链,顾名思义是由一个个区块相连接而成组成的链式结构

所以我们先定义Chain和Block类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
const sha256 = require('crypto-js/sha256')

class Block{

constructor(data, preHash){
this.data = data // 区块储存的数据
this.preHash = preHash // 储存上一个区块数据的哈希值
this.hash = this.computeHash() // 当前区块数据的哈希值
}

// 生成该区块的Hash
computeHash() {
return sha256(
this.data +
this.preHash
).toString()
}
}

class Chain{
constructor(){
this.chain = [this.makeGenesis()] // 一个链中应由多个区块组成所以赋值成数组
}

// 生成起始区间
makeGenesis(){
return new Block('Origin','') // 起始区块,索引为0,所以preHash为0
}
}

const fanChain = new Chain()
const block1 = new Block('b1', '111')
console.log(fanChain)
console.log(block1)

运行得:

image-20231124142339969

此时链上只有起始区块

那要把我们定义的block1加到fanChain上,就要在chain上构造相应方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/////////// class Chain ///////////

// 获得最新的区块
getLatestBlock() {
return this.chain[this.chain.length - 1]
}

// 手动增加区块
addBlock(newBlock) {
newBlock.preHash = this.getLatestBlock().hash
// 因为在外部无法得知此时的preHash是什么,所以手动赋值
newBlock.hash = newBlock.computeHash()
// computeHash()在Block类是构造时候调用的,但是newBlock传入的时候没有preHash,所以构造的Hash是错的,需要重新生成
this.chain.push(newBlock) // 别忘了往链上添加
}

const fanChain = new Chain()
const block1 = new Block('b1', '111')
const block2 = new Block('b2', '2')
fanChain.addBlock(block1)
fanChain.addBlock(block2)
console.log(fanChain)

image-20231124145754763

可以看到:

  • b1preHash与上一个区块Originhash一致
  • b2preHash与上一个区块b1hash一致
  • 成功链接

这时候需要考虑一个问题,因为区块链具有不可篡改性

但是如果我们直接通过fanChain.chain[1].data = 'fake'进行更改data数据

image-20231124150247884

会发现b1的数据被篡改了,但是hash依旧不变,所以我们要对链中的区块增加校验过程

校验要进行的过程有:

  • 对比Block上储存的hash(也就是刚创建区块时候的hash)与通过当前Block数据再次生成的hash,以判断区块的data有没有被篡改
  • 上一个区块的hash与当前区块的preHash是否一致,也就是是否构成链式结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/////////// class Chain ///////////

// 对区块进行校验
validateChain() {
// 因为起始区块没有preHash所以单独校验
if (this.chain.length === 1) {
if (this.chain[0].hash !== this.chain[0].computeHash()) return false
// 对初始进行校验,原始的Hash与再次生成的Hash进行对比
return true
}
for (let i = 1; i < this.chain.length; i++) {

// 判断当前Block的data是否被篡改
const blockToValidate = this.chain[i]
if (blockToValidate.hash !== blockToValidate.computeHash()) return false

// 比对前一个Block与当前Block的hash值是否相同
const preBlock = this.chain[i - 1]
if (preBlock.hash !== blockToValidate.preHash) return false
}
return true
}

const fanChain = new Chain()
const block1 = new Block('b1', '111')
const block2 = new Block('b2', '2')
fanChain.addBlock(block1)
fanChain.addBlock(block2)
console.log(fanChain.validateChain())

// 尝试篡改区块
fanChain.chain[1].data = 'fake'
console.log(fanChain.validateChain())
console.log(fanChain)

image-20231124150847683

可以发现fanChain在被人工篡改之后,被校验出来了

其次,我们也可以同时篡改区块的hash

fanChain.chain[1].hash = fanChain.chain[1].computeHash()

image-20231124151938859

但是发现依然校验失败,这是因为b1hash改变了,所以导致b2preHashb1hash不一致了,上下无法链接

2、实现工作量证明机制

上述实现了一个简单的区块链结构,我们只要通过computeHash算出哈希值就可以把区块加到链上

但是比特币中,需要生成的hash满足特定条件,比如哈希前缀三位全为0,等…

所以如果需要满足的条件越多,需要的算力就越大

  • 这里我们定义nonce作为改变量
  • mine()进行穷举比对
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
/////////// class Block ///////////

constructor(data, preHash) {
this.data = data // 区块储存的数据
this.preHash = preHash // 储存上一个区块数据的哈希值
this.nonce = 0 // 挖(比对)的次数
this.hash = this.computeHash() // 当前区块数据的哈希值
}

// 生成该区块的Hash
computeHash() {
return sha256(this.data + this.nonce + this.preHash).toString() // 增加nonce作为改变量
}

// 生成挖矿所比对的前缀 difficulty越大需要时间越长
getAnswer(difficulty) {
let Ans = ''
for (let u = 0; u < difficulty; u++) {
Ans += '0'
}
return Ans
}

// 挖!
mine(difficulty) {
while (true) {
this.hash = this.computeHash()
if (this.hash.substring(0, difficulty) === this.getAnswer(difficulty))
// 进行前缀比对
break
this.nonce++
}
}


/////////// class Chain ///////////

constructor() {
this.chain = [this.makeGenesis()] // 一个链中应由多个区块组成所以赋值成数组
this.difficulty = 3 // 设置难度为3,也就是需要hash满足前缀三位全为0
}

// 手动增加区块
addBlock(newBlock) {
newBlock.preHash = this.getLatestBlock().hash
// 因为在外部无法得知此时的preHash是什么,所以手动赋值
newBlock.hash = newBlock.computeHash()
// computeHash()在Block类是构造时候调用的,但是newBlock传入的时候没有preHash,所以构造的Hash是错的,需要重新生成
newBlock.mine(this.difficulty) // 进行比对前缀
this.chain.push(newBlock) // 别忘了往链上添加
}


const fanChain = new Chain()
const block1 = new Block('b1', '111')
const block2 = new Block('b2', '2')
fanChain.addBlock(block1)
fanChain.addBlock(block2)
console.log(fanChain)

image-20231124154532552

观察发现在nonce为4649和3687时得到可行的hash

3、创造自己的数字货币

既然说货币,那便是用来交易的

所以我们可以把上述的Block类中的data改变为交易内容Transaction,以实现转账

所以我们追加一个Transaction类,既然是交易,那必然包含

  • 转账人
  • 收款人
  • 数额
  • 时间
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Transaction {
constructor(from, to, amount, timeStamp) {
this.from = from // 发送者公钥
this.to = to // 接收者公钥
this.amount = amount // 转账数量
this.timeStamp = timeStamp
}

// 生成对应数据的hash
computeHash() {
return sha256(this.from + this.to + this.amount + this.timeStamp ).toString()
}
}

/////////// class Block ///////////

constructor(transactions/* data */, preHash) {
this.transactions = transactions // 本来是this.data = data
this.preHash = preHash
this.nonce = 0 // 挖的次数
this.hash = this.computeHash()
}

// 生成该区块的Hash
computeHash() {
return sha256(
JSON.stringify(this.transactions) + // Transaction是对象,所以用JSON.stringify将其字符化
this.preHash +
this.nonce
).toString()
}

但是上述的时间戳timeStamp在比特币作者的论文里,是要在block中进行生成的,而不是在Transaction,所以进行修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Transaction {
constructor(from, to, amount, timeStamp) {
this.from = from // 发送者公钥
this.to = to // 接收者公钥
this.amount = amount // 转账数量
// this.timeStamp = timeStamp
}

// 生成对应数据的hash
computeHash() {
return sha256(this.from + this.to + this.amount /* + this.timeStamp */).toString()
}
}

/////////// class Block ///////////

constructor(transactions/* data */, preHash) {
this.transactions = transactions // 本来是this.data = data
this.preHash = preHash
this.nonce = 0 // 挖的次数
this.hash = this.computeHash()
this.timeStamp = Date.now() // 在Block类中进行生成
}

// 生成该区块的Hash
computeHash() {
return sha256(
JSON.stringify(this.transactions) + // Transaction是对象,所以用JSON.stringify将其字符化
this.preHash +
this.nonce +
this.timeStamp // 在这里添加
).toString()
}

上面写道把data改为Transaction交易数据

所以现在的这个区块链相当于一个池子(TransactionPool)装满了交易(Transaction)

1
2
3
4
5
6
7
8
/////////// class Chain ///////////

constructor() {
this.chain = [this.makeGenesis()] // 一个链中应由多个区块组成所以赋值成数组
this.difficulty = 3 // 设置难度为3,也就是需要hash满足前缀三位全为0
this.transactionPool = [] // 装载交易数据的pool
this.minerReward = 50 // 奖励数额,随着挖的区块数量越多随之减半
}

这时候我们回到先前的addBlock

1
2
3
4
5
6
7
8
9
// 手动增加区块
addBlock(newBlock) {
newBlock.preHash = this.getLatestBlock().hash
// 因为在外部无法得知此时的preHash是什么,所以手动赋值
newBlock.hash = newBlock.computeHash()
// computeHash()在Block类是构造时候调用的,但是newBlock传入的时候没有preHash,所以构造的Hash是错的,需要重新生成
newBlock.mine(this.difficulty) // 进行比对前缀
this.chain.push(newBlock) // 别忘了往链上添加
}

这个方法实际上是为了我们调试用的,实际运用中并不能直接增加

实际中,我们是在慢慢的挖区块,这里假定有个矿工在帮我们挖矿,当他挖到区块链便会获得相应的矿工奖励,奖励的发放依然通过Transaction来发放

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 交易池
mineTransactionPool(minerAddr) {
// 先给矿工发钱
const minerTransaction = new Transaction('', minerAddr, this.minerReward) // 因为矿工奖励由区块链本体发放,所以地址为空
this.transactionPool.push(minerTransaction) // 往pool储存
// 开挖
const newBlock = new Block(this.transactionPool, this.getLatestBlock().hash)
newBlock.mine(this.difficulty)
// 把区块加到区块链当中
// 同时清空此时的Pool
this.chain.push(newBlock) // 把挖到的区块放入链
this.transactionPool = [] // 当挖到合法区块,且放入链后,清空pool
}

// 方便添加Transaction到Pool
addTransaction(transaction) {
this.transactionPool.push(transaction)
}


const fanCoin = new Chain()
const t1 = new Transaction('addr1', 'addr2', 10)
const t2 = new Transaction('addr2', 'addr1', 20)
fanCoin.addTransaction(t1)
fanCoin.addTransaction(t2)
fanCoin.mineTransactionPool('fan')
console.log(fanCoin)
console.log(fanCoin.chain[1].transactions)

image-20231126001059276

4、数字签名

上节我们实现了简易的数字货币,但是可以发现Transaction(交易)可以由任何人发起,而且也可以不经过你的批准而使用你钱包的钱,这显然不行,怎么能动我的小金库

所以在发起转账前,需要做出相应的验证措施,这就要用到了本节的数字签名

这里我们使用的是非对称加密

  • 通过公开TransactionPublic KeySignature,可以让其他所有人校验这个签名的合法性,证明这个交易属于你
  • 同时,只有验证通过才能执行转账操作,保护了你的小金库
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
const ecLib = require('elliptic').ec
const ec = new ecLib('secp256k1') // 椭圆曲线

/////////// class Transaction ///////////

constructor(from, to, amount) {
// if (from === null || from == '') // 视频里没说,但是我觉得应该检验一下(这样检验不太行
// throw new Error('Transaction cannot be empty!')
this.from = from // 这里从id改成了发送者公钥
this.to = to // 同理,接收者公钥
this.amount = amount // 转账数量
// this.timeStamp = timeStamp
}

// 对交易进行签名
// 签名的意义是将公钥和Transaction公开使所有人都能对此交易进行校验,凭此证明其合法性
sign(key) {
this.signature = key.sign(this.computeHash(), 'base64').toDER('hex')
}

// 检查交易合法性
isValid() {
// 对矿工奖励进行特殊判断
// 因为矿工奖励是区块链给矿工转账,所以公钥是空
if (this.from === '') return true

const keyObj = ec.keyFromPublic(this.from, 'hex')
return keyObj.verify(this.computeHash(), this.signature) // 检验Signature是否合法
}

/////////// class Chain ///////////

// 方便添加Transaction到Pool
addTransaction(transaction) {
// 检验transaction是否合法
if (!transaction.isValid())
throw new Error('Found InValid Transaction before adding to pool!')
this.transactionPool.push(transaction)
}

image-20231216163810985

image-20231216162204074

可以看到验证成功,现在我们手动篡改一下,尝试一下

image-20231216163925741

amount改成20

image-20231216163843479

可以看到输出了false,并丢出来错误,且并没有往Transactions中放入错误的交易

这时候,在之前验证区块是否一一相连时,我们也应该加上验证

由于区块中可能有多个Transaction,所以采用穷举依次验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/////////// class Block ///////////


// 逐个验证transaction是否合法
validateTransaction() {
for (const transaction of this.transactions) {
if (!transaction.isValid)
// throw new Error("Found inValid Transaction!")
return false
}
return true
}

// 挖!
mine(difficulty) {
this.validateTransaction()
while (true) {
this.hash = this.computeHash()
if (this.hash.substring(0, difficulty) === this.getAnswer(difficulty))
// 进行前缀比对
break
this.nonce++
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/////////// class Chain ///////////

// 对区块进行校验
validateChain() {
if (this.chain.length === 1) {
if (this.chain[0].hash !== this.chain[0].computeHash()) return false
// 对初始进行校验,原始的Hash与再次生成的Hash进行对比
return true
}
for (let i = 1; i < this.chain.length; i++) {
// 验证Block的每个Transaction是否合法
if (!this.chain[i].validateTransaction())
throw new Error('Found InValid Transaction while validating Chain')

// 判断当前Block的data是否被篡改8
const blockToValidate = this.chain[i]
if (blockToValidate.hash !== blockToValidate.computeHash()) return false

// 比对前一个Block与当前Block的hash值是否相同
const preBlock = this.chain[i - 1]
if (preBlock.hash !== blockToValidate.preHash) return false
}
return true
}

image-20231217025145782

image-20231217025119348

The End


Block Chain -- Proof Of Work
http://example.com/posts/32752/
作者
Fanllspd
发布于
2023年11月24日
更新于
2024年1月8日
许可协议