前言
《最小可行性区块链设计系列》的第三讲(http://www.jianshu.com/p/82e7a6de1f02) 实现了区块的构造。
本文的代码地址:https://github.com/qikh/mini-block-chain/commit/90c141eb3098f2f7f91e218cc47ecc988cfac12c (开发语言为Kotlin,更简洁的Java)
正文
Alice向Bob发起一笔100元的转账,这笔转账记录被发布到区块链网络上等待区块链客户端登账,如果没有人来完成登账的动作,转账就没有完成。为了激励大家来登记账本,完成登账的客户端可以获得2元的奖励(Incentive)。Charlie和David都运行着区块链的客户端,希望通过登账来获得2元/区块(Block)的奖励。我们把Charlie和David的登账行为称之为“挖矿”,Charlie和David则被成为“矿工”。
上一讲我们看到新区块(Block)的作用,就是把交易数据(Transaction)包含进区块,并且更新发送方和接收方的账户状态(余额),逻辑十分简单。构造新区快既然有了奖励,就需要增加竞争机制来决定谁的新区块最终完成登账,算力证明(Proof of Work)是区块链领域的主要竞争算法之一。
我们参考比特币的协议来设计我们的POW算法:
- 所有的区块链客户端统一一个目标数字(target),例如:0x00000FFFF0000000000000000000000000000000000000000000000000000000
- 不断计算区块的哈希值,希望得到一个小于目标数字的哈希值,例如:0x00006853cc0fc1161a7c12da76866fd79095556aed267f883872df4be859f204。
- 第一个算出小于目标数字的客户端写入区块,并向区块链网络广播消息,其他客户端放弃本次挖矿,开始计算下一个区块。
区块的哈希算法并不是简单地对区块内容进行哈希运算,而是对Block Header进行哈希运算,Block Header包括:
version:协议版本号
parentHash:上一个区块的哈希值
merkleRoot:交易数据(Transaction)的Merkle Root Hash,代表了交易数据的唯一性,Merkle Tree算法的其他优点我们随后还会讨论。
time:时间戳
difficulty:挖矿难度值,可以推算出目标数字
nonce:工作量证明随机数
/**
* 区块(Block)类,包含了区块高度(height),上一个区块哈希值(parentHash),旷工账户地址(minerAddress),交易列表(transactions)和时间戳(time)。
*/
class Block(val height: Long, val parentHash: ByteArray, val minerAddress: String, val transactions: List<Transaction>,
val time: DateTime) {
val version: Int = 1
val merkleRoot: ByteArray
get() = CryptoUtil.merkleRoot(transactions)
var difficulty: Int = 0
var nonce: Int = 0
/**
* 区块(Block)的哈希值(KECCAK-256)
*/
val hash: ByteArray
get() = CryptoUtil.hashBlock(this)
}
其中nonce的初始值为1,每次哈希运算如果没有获得小于target的数字,nonce就加1,然后再次运算哈希值,直到获得一个小于target的数字。
/**
* 挖矿,返回nonce值和target值。目前采用阻塞模型,后期修改为更合理的异步模型。
*/
fun mine(block: Block): MineResult {
val ver = block.version
val parentHash = block.parentHash
val merkleRoot = block.merkleRoot
val time = (block.time.millis/1000).toInt() // Current timestamp as seconds since 1970-01-01T00:00 UTC
val difficulty = currentDifficulty // difficulty
// 挖矿难度的算法:https://en.bitcoin.it/wiki/Difficulty
val exp = difficulty shr 24
val mant = difficulty and 0xffffff
val target = BigInteger.valueOf(mant.toLong()).multiply(BigInteger.valueOf(2).pow(8 * (exp - 3)))
val targetStr = "%064x".format(target)
var nonce = 0
while (nonce < 0x100000000) {
val headerBuffer = ByteBuffer.allocate(4 + 32 + 32 + 4 + 4 + 4)
headerBuffer.put(ByteBuffer.allocate(4).putInt(ver).array()) // version
headerBuffer.put(parentHash) // parentHash
headerBuffer.put(merkleRoot) // merkleRoot
headerBuffer.put(ByteBuffer.allocate(4).putInt(time).array()) // time
headerBuffer.put(ByteBuffer.allocate(4).putInt(difficulty).array()) // difficulty(current difficulty)
headerBuffer.put(ByteBuffer.allocate(4).putInt(nonce).array()) // nonce
val header = headerBuffer.array()
val hit = Hex.toHexString(CryptoUtil.sha256(CryptoUtil.sha256(header)))
if (hit < targetStr) {
break
}
nonce += 1
}
val result = MineResult(currentDifficulty, nonce)
return result
}
挖矿的难度difficulty和算出的nonce会写入区块的Header,其他客户端可以再次计算区块Header的哈希值来确定这次运算的有效性。
我们POW所采用的哈希算法与比特币相同,为两次SHA-256运算。这种POW设计的缺点是哈希算法很容易用ASIC集成电路来实现硬件加速,也就是所谓的矿机,CPU挖矿与ASIC挖矿相比性能差别百万倍。为了解决这个问题,以太坊的POW采用了Dagger Hashimoto算法,结合了Memory Hard的Dagger算法和IO-bound的Hashimoto算法,来阻止ASIC芯片硬件加速。Dagger Hashimoto算法的实现相对复杂,后期我们会作为POW算法的一个选项来实现。
下一讲我们将会讨论在分布式环境下如何取得网络共识(Consensus)。