对“初链”混合共识、双链结构和抗ASIC挖矿算法的详解

前段时间写了篇博客说了说自己对“初链”白皮书和黄皮书的解读,其中一部分涉及到对“初链”混合共识和双链技术的解读,由于是从说明文档中获取的信息,难免会有误解,虽然大体上和实际的处理逻辑相符合,但在细节上还是有一定的出入。出于对混合共识技术和双链逻辑的好奇,博主我下来抽空余时间仔细阅读了下“初链”项目共识部分的代码,感悟颇深,遂写文分享。本篇博客的目的是让对区块链有一定了解且有一定技术开发经验的朋友快速、深入地了解“初链”混合共识机制的处理逻辑,主要以源代码翻译伪代码(基于“初链”Beta版本)的形式讲解各个处理过程,因为只有用伪代码的方式才能直观的表述一个处理过程的细节,但我并不会过分深入每一个细节。本篇博客包括以下内容:

  • 快链(FastChain)、慢链(SnailChain)的区块结构、区块验证和交互机制
  • 慢链(SnailChain)的区块打包、挖矿和奖励分配机制
  • 快链(FastChain)的区块打包和拜占庭委员会(PBFT)的共识机制
  • 拜占庭委员会(PBFT)的选举和验证机制
  • 抗ASIC专用矿机的POW挖矿算法(TrueHash)
  • “初链”和以太坊的关系
  • “初链”的代码审查意见
  • 总结

> 快链(FastChain)、慢链(SnailChain)的区块结构、区块验证和交互机制

“初链”采用双链结构设计,目的是为了分离交易确认和算力保护。快链区块直接打包交易,交易打包成区块后经过拜占庭委员会(PBFT)的共识即被确认,此过程很快。慢链区块包含快链区块的内容,通过挖矿完成慢链区块的打包,慢链采用工作量证明(POW)机制,旨在通过算力保护整个区块链和拜占庭委员会的安全。

快链区块结构(FastBlock:code/types/block.go)<

// Header represents a block header in the true Fastblockchain.
type Header struct {
    ParentHash  common.Hash `json:"parentHash"       gencodec:"required"`
    Root        common.Hash `json:"stateRoot"        gencodec:"required"`
    TxHash      common.Hash `json:"transactionsRoot" gencodec:"required"`
    ReceiptHash common.Hash `json:"receiptsRoot"     gencodec:"required"`
    Bloom       Bloom       `json:"logsBloom"        gencodec:"required"`
    SnailHash   common.Hash `json:"snailHash"        gencodec:"required"`
    SnailNumber *big.Int    `json:"snailNumber"      gencodec:"required"`
    Number      *big.Int    `json:"number"           gencodec:"required"`
    GasLimit    uint64      `json:"gasLimit"         gencodec:"required"`
    GasUsed     uint64      `json:"gasUsed"          gencodec:"required"`
    Time        *big.Int    `json:"timestamp"        gencodec:"required"`
    Extra       []byte      `json:"extraData"        gencodec:"required"`
}

// FastBlock represents an entire block in the Ethereum blockchain.
type Block struct {
    header       *Header
    transactions Transactions

    uncles []*Header // reserved for compile

    signs PbftSigns

    // caches
    hash atomic.Value
    size atomic.Value

    // Td is used by package core to store the total difficulty
    // of the chain up to and including the block.
    // td *big.Int

    // These fields are used by package etrue to track
    // inter-peer block relay.
    ReceivedAt   time.Time
    ReceivedFrom interface{}
}

在快链区块结构中,最重要的两个属性就是"transactions"和"signs",在区块头中也可以看到有“TxHash”、“GasLimit”和“GasUsed”这三个属性,这表明了快链区块的主要作用是执行交易、收集交易和收集拜占庭委员会成员的签名。

慢链区块结构(FastBlock:code/types/block.go)<

// Header represents a block header in the Ethereum truechain.
type SnailHeader struct {
    ParentHash  common.Hash    `json:"parentHash"       gencodec:"required"`
    UncleHash   common.Hash    `json:"sha3Uncles"       gencodec:"required"`
    Coinbase    common.Address `json:"miner"            gencodec:"required"`
    PointerHash common.Hash    `json:"PointerHash"      gencodec:"required"`
    FruitsHash  common.Hash    `json:"fruitsHash"       gencodec:"required"`
    FastHash    common.Hash    `json:"fastHash"         gencodec:"required"`
    FastNumber  *big.Int       `json:"fastNumber"       gencodec:"required"`
    SignHash    common.Hash    `json:"signHash"  		gencodec:"required"`
    Bloom       Bloom          `json:"logsBloom"        gencodec:"required"`
    Difficulty  *big.Int       `json:"difficulty"       gencodec:"required"`
    Number      *big.Int       `json:"number"           gencodec:"required"`
    Publickey   []byte         `json:"Publickey"        gencodec:"required"`
    ToElect     bool           `json:"ToElect"          gencodec:"required"`
    Time        *big.Int       `json:"timestamp"        gencodec:"required"`
    Extra       []byte         `json:"extraData"        gencodec:"required"`
    MixDigest   common.Hash    `json:"mixHash"          gencodec:"required"`
    Nonce       BlockNonce     `json:"nonce"            gencodec:"required"`

    Fruit bool
}

// Block represents an entire block in the Ethereum blockchain.
type SnailBlock struct {
    header *SnailHeader
    fruits SnailBlocks
    signs  PbftSigns

    uncles []*SnailHeader

    // caches
    hash atomic.Value
    size atomic.Value

    // Td is used by package core to store the total difficulty
    // of the chain up to and including the block.
    td *big.Int

    // These fields are used by package etrue to track
    // inter-peer block relay.
    ReceivedAt   time.Time
    ReceivedFrom interface{}
}

慢链区块有点特殊,在“初链”中包含“水果(Fruit)”的概念,而水果也是通过慢链区块来表示的。在慢链区块中包含“fruits”、“signs”属性,在区块头中则包含很多属性,其中包括有“Coinbase”、“PointerHash”、“FruitsHash”、“FastHash”、“SignHash”、“ToElect”和“Fruit”属性。这些里面最能直观反应慢链区块的作用的就是块中的“fruits”、“signs”属性和头中“Fruit”属性。区块头中的“Fruit”布尔属性表明该区块是不是一个水果,如果是水果,那在块中就有“signs”属性填充,否则就是一个普通区块,在块中有“fruits”属性填充。这三个属性直观地表明了“水果”和“区块”的关系:水果是包含了一个快链区块(FastBlock)中拜占庭委员会成员签名的区块,而真正意义上的区块则是包含了多个连续排列的水果(头中“FastNumber”属性顺序排列)的区块。

快链区块的验证(VerifyFastBlock:etrue/pbft_agent.go)<

拜占庭共识委员会在收集P2P网络中的交易并产生快链区块后,或者收到其他委员会广播的区块后,会进行区块验证,该算法的流程大致是:

  1. 验证父区块是否存在
  2. 验证区块头
  3. 验证区块体
  4. 验证交易和状态数据库

伪代码如下:

// 传入快链区块 => fb type.FastBlock,bc type.BlockChain
// 验证父区块
parent = bc.GetBlock(fb.ParentHash(), fb.Number()-1)
if parent == nil {
    return error("height not yet")
}
// 验证区块头
header = fb.Header()
if len(header.Extra) > 32 {
    return error("extra too long")
}
if header.Time - Time.Now() > 15 * Time.Second {
    return error("future block")
}
if header.Time < parent.Time {
    return error("zero block time")
}
if header.GasLimit > 0x7fffffffffffffff {
    return error("invalid gas used")
}
if abs(parent.GasLimit - header.GasLimit) >= (parent.GasLimit / 1024) || header.GasLimit < 5000 {
    return error("invalid gas limit")
}
if header.Number - parent.Number != 1 {
    return error("invalid number")
}
//  验证区块体
if Hash(fb.Transactions()) != header.TxHash {
    return error("transaction root hash mismatch")
}
// 验证交易和状态数据库
stateDB = fb.State()
receipts, usedGas = bc.Process(fb.Transactions(), stateDB)
if fb.GasUsed() != usedGas {
    return error("invalid gas used")
}
if CreateBloomHash(receipts) != header.Bloom {
    return error("invalid bloom")
}
if Hash(receipts) != header.ReceiptHash {
    return error("nvalid receipt root hash")
}
if Hash(state) != header.Root {
    return error("invalid merkle root")
}
return nil

慢链区块的验证(ValidateFruit:core/snailchain/block_validator.go、VerifySnailHeader:consensus/minerva/consensus.go)<

慢链区块由于分为水果和区块,所以有两种验证过程:

  1. 区块
    a) 验证区块头
    b) 验证区块体
  2. 水果
    a) 验证委员会签名
    b) 验证水果新鲜度
    c) 验证区块头

验证区块伪代码如下:

// 传入慢链区块 => sb type.SnailBlock,bc type.SnailBlockChain
// 验证区块头
parent = bc.GetBlock(sb.ParentHash(), sb.Number()-1)
if parent == nil {
    return error("unkown ancestor")
}
header = fb.Header()
if len(header.Extra) > 32 {
    return error("extra too long")
}
if header.Time - Time.Now() > 15 * Time.Second {
    return error("future block")
}
if header.Time < parent.Time {
    return error("zero block time")
}
expectedDifficulty = CalcSnailDifficulty(bc, header.Time, parent)
if expectedDifficulty != header.Difficulty {
    return error("invalid difficulty")
}
digest, hashResult = truehash(header.HashWithoutNonce(), header.Nonce)	// 挖矿时计算的数据指纹和哈希结果
if header.MixDigest != Hash(digest) {
    return error("invalid mixdigest")
}
maxUint128 = 2 ^ 128 - 1
if hashResult.SubBytes(0, 15) > (maxUint128 / header.Difficulty) {    // hashResult取前16字节
    return error("invalid pow")
}
// 验证区块体
for fruit in range sb.Fruits() {
    if err = ValidateFruit(fruit, bc); err != nil {
        return err
    }
}
return nil

验证水果伪代码如下:

// 传入水果 => f type.SnailBlock,bc type.SnailBlockChain
// 验证委员会签名
header = f.Header()
if Hash(f.Signs()) != header.SignHash {
    return error("invalid sign")
}
// 验证水果新鲜度
pointer = bc.GetBlockByHash(f.PointerHash())	     // pointer区块是在水果生成时从当前慢链区块往前数7个的那个区块
if pointer == nil {
    return error("invalid pointer")
}
if bc.CurrentBlock().Number() - pointer.Number() > 17 {    // 生成水果时指定的区块高高度比最新的区块高度低17位时则水果过期
    return error("invalid freshness")	    // 水果的生存周期只有(17-7)=10个区块,每个区块生产周期为10分钟,所以水果的新鲜度只有平均1个小时40分钟左右
}
// 验证区块头
parent = bc.GetBlock(f.ParentHash(), f.Number()-1)
if parent == nil {
    return error("unkown ancestor")
}
if len(header.Extra) > 32 {
    return error("extra too long")
}
digest, hashResult = truehash(header.HashWithoutNonce(), header.Nonce)
if header.MixDigest != Hash(digest) {
    return error("invalid mixdigest")
}
fruitDifficulty = pointer.Difficulty / 600     // 水果的难度是正常区块难度的1/600
maxUint128 = 2 ^ 128 - 1
if hashResult.SubBytes(16) > (maxUint128 / fruitDifficulty) {    // hashResult取后16字节
    return error("invalid pow")
}
return nil

快链和慢链的交互机制 <

快链收集所有的快链区块,每一个快链都包含交易信息和拜占庭委员会签名信息,这些数据越快被快链区块打包并收纳进快链中,则交易的确认速度越快,但这个过程只经过了一次拜占庭委员会之间的共识,并没有任何挖矿操作,所以快链上的信息很容易被串改。为了保护快链的安全,“初链”引入了慢链,该链采用POW共识,从快链中收集委员会签名信息放入慢链区块,然后通过挖矿操作向区块注入算力以保护区块的安全,慢链区块的安全被保护了起来那其中拜占庭委员会的签名数据也被保护了起来,在签名数据中包含了快链区块的区块号和区块哈希值,即也意味着快链被保护了起来。该过程与中本聪区块链最大的不同就是“初链”引入了拜占庭委员会,这样交易的确认就从全网范围缩小到了1~40个节点的范围。虽然交易的确认速度大幅度提升,但由于PBFT协议的引入,区块链的容错率从原本的50%降至33%,这就意味着“初链”对PBFT拜占庭共识委员会的要求就必须是作恶委员会节点数量不能超过全体委员会成员数量的1/3。由于拜占庭委员会成员每1440个慢链区块完成一次换届(1440个区块的慢链出块时间差不多10天的样子),期间只要有势力能占领1440个区块中超过1/3的区块,那么就很有可能造成委员会成员的污染。会造成这种情况目前有两个方式,一个是算力集中(比如矿池),另一个就是女巫攻击。由于中本聪POW共识算法天生能防女巫攻击但却防不了算力集中,所以只要出现形成矿池的情况,那就会威胁到区块链的安全。矿池的形成可以从两方面瓦解,一个是设计反ASIC芯片的挖矿算法(运用大内存的内存困难型算法或者具有周期性变化的算法),另一个就是降低形成矿池的动力。“初链”提出“水果”的方式就是为了降低形成矿池的动力(“初链”的挖矿算法也是周期性可变的,后面会讲到),从最初的“中本聪链”转变为目前的“水果链”。水果的挖矿难度是正常区块挖矿难度的1/600,这个难度值是很小的,普通CPU都能轻松挖到,所以传统的慢链区块就被拆成两部分,一部分为包含委员会签名的水果块,另一部分为包含水果的正常区块。包含水果的正常区块的挖矿奖励会分别分摊给拜占庭委员会节点、水果矿工和区块矿工,所以挖出的水果在被打包后其背后的矿工也能获得奖励。通过降低挖矿难度但却不影响挖矿收益的方式降低矿池的形成动力是一个非常聪明的做法,并且由于仍然存在挖矿过程,所以女巫攻击也能避免。慢链区块计算哈希的POW过程没有本质改变,其对整个区块链提供的算力保护不会因为水果的引入而受到影响。由此,“初链”的快慢双链交互机制被确定为:

  1. 拜占庭委员会成员收集交易构建FastBlock,收纳执行交易后产生的交易费
  2. FastBlock在委员会成员之间完成PBFT共识收集委员会成员的签名数据Sign并广播
  3. 节点持续收集P2P网络中的FastBlock和Fruit
  4. 节点通过组装FastBlock中的签名数据Sign和Fruit来形成挖矿区块开始挖矿,同时挖取区块和水果
  5. 如果挖出SnailBlock则去掉其中的拜占庭委员会签名部分然后全网广播
  6. 如果挖出Fruit则去掉其中的水果部分然后全网广播
  7. 拜占庭委员会在构建FastBlock时检测已经经过12个区块确认的SnailBlock并分发其区块奖励

交互机制用示意图表述如下:
在这里插入图片描述


> 慢链(SnailChain)的区块打包、挖矿和奖励分配机制

矿工会收集区块链网络中的快链区块(FastBlock)、拜占庭委员会签名(Sign)、水果(Fruit/SnailBlock)和慢链区块(SnailBlock),将其中的快链区块和水果放入snail_pool中等待打包。矿工在挖矿之前会先准备挖矿环境,从snail_pool中取出一个FastBlock和多个升序排列的水果(区块头中的“FastNumber”属性升序排列并且序列的起始值比当前已打包水果中的最大“FastNumber”值多1)放入一个新构建的区块中,随后设定随机nonce值就开始挖矿。在挖矿期间不停的计算区块头的哈希值,小于区块的难度值就挖取一个慢链区块,小于水果的难度值就挖取一个水果,所以慢链区块和水果是同时挖取的。不管是挖出了慢链区块还是水果都不会立即分配奖励,而是待挖取的慢链区块被之后的12个区块确认后才会分配,这很自然是出于安全的考虑。比特币的区块确认高度是6个区块,而“初链”是12个,多了一倍,原因是比特币的区块在被确认后其内部打包的交易UTXO记录才跟着被确认,为了兼顾效率和安全性才选择一个小时(6个区块的确认时间平均为一个小时)作为确认周期。而交易在“初链”中经过PBFT委员会共识产生快链区块后就已确认,所以慢链区块的确认速度不影响交易的确认速度,自然可以将慢链区块的确认速度降低一倍来注入更多的算力保护以提高抗攻击性。下面我用三段伪代码来分别说明区块打包、矿工挖矿和奖励分配的处理流程。

区块打包(创建挖矿区块)的处理过程(commitNewWork:miner/worker.go) <

伪代码如下:

// 传入参数 => bc type.SnailBlockChain, snailPool type.SnailPool
// 确保实际打包时间不能超前
timeStart = time.Now()
parent = bc.CurrentBlock()
if parent.Time() >= timeStart {
    timeStart = parent.Time() + 1
}
if timeStart - time.Now() > 1 {
    time.Sleep(timeStart - time.Now())
}
// 计算挖矿难度
x = max(1 - (timeStart - parent.Time()) / 600, -1)    // 600为可配置{DurationLimit}参数
y = parent.Difficulty / 32        // 32为可配置{DifficultyBoundDivisor}参数
x = max(x * y + parent.Difficulty, 256)    // 256为可配置的最低挖矿难度{MinimumDifficulty}参数
// 创建区块头并填充参数
header = new(type.SnailHeader)
header.ParentHash = parent.Hash()
header.ToElect = self.toElect    // 是否参与委员会选举
header.Publickey = self.publicKey    // 矿工公钥
header.Number = parent.Number() + 1    // 区块号
header.Extra = self.extra
header.Time = timeStart
header.Coinbase = self.coinbase    // 矿工地址
header.PointerHash = bc.GetBlockByNumber(parent.Number() - 7).Hash()    // 7为可配置{pointerHashFresh}参数
// 打包FastBlock中的委员会签名数据
signs = new([]type.PbftSign)
for fb in range snailPool.GetFastBlocks() {
    if fb.Number() > self.FastBlockNumber {    //FastBlockNumber为上一次挖取到的水果的区块头里的FastNumber值
        header.FastNumber = fb.Number()
        header.FastHash = fb.Hash()
        CopyTo(signs, fb.Signs())
        break
    }
}
// 打包顺序排列的Fruit
fruits = new([]type.SnailBlock)
lastFastNumber = parent.Fruits().Last().FastNubmer() or 0
startCopyFruit = false
for f in range snailPool.GetFruits() {
    if lastFastNumber == 0 || f.FastNumber() == lastFastNumber + 1 {
        startCopyFruit = true
    }
    if startCopyFruit && f.FastNumber() > lastFastNumber {
        if fruits.Empty() || fruits.Last().FastNumber() == f.FastNumber() - 1 {
            pointerBlock = bc.GetBlockByHash(f.PointerHash())
            if pointerBlock != nil && (header.Number - pointerBlock.Number()) > 17 {    // 17为可配置的{fruitFreshness}参数
                append(fruits, f)
            }
        }
    }
}
// 确保接下来要挖的这个区块不是水果就是慢链区块
if header.FastNumber == 0 && fruits.Empty() {
    return error("has no fruits and fastblocks to start mining")
}
// 构建最终的挖矿区块
block = new(type.SnailBlock)
CopyTo(block.header, header)
if len(fruits) > 0 {
    block.header.FruitsHash = Hash(fruits)
    CopyTo(block.fruits, fruits)
}
if len(signs) > 0 {
    block.header.SignHash = Hash(signs)
    CopyTo(block.signs, signs)
}
return block

可见慢链区块的打包过程大致是:

  1. 确认打包时间点
  2. 根据打包时间点和父区块难度计算当前区块难度值
  3. 填充区块头(包括难度值、父区块哈希和公钥等信息)
  4. 填充一个快链区块中的委员会签名数据和填充集合的哈希值
  5. 填充所有能升序排列的水果和填充集合的哈希值

区块挖掘(计算哈希值)的处理过程(ConSeal、mineSnail:consensus/minerva/sealer.go) <

伪代码如下:

// 传入参数 => bc type.SnailBlockChain, miningBlock type.SnailBlock, found <-type.Chan
// 获得随机nonce值,开启多线程并行挖矿
seed = rand()
threads = GetCpuNum() - 1    // 留一个CPU处理快链区块
pointerBlock = bc.GetBlockByHash(header.PointerHash)
if pointerBlock == nil {
    return error("invalid pointer hash")
}
for i = 0, threads {
    go MineSnail(miningBlock, pointerBlock.Difficulty(), seed, found)    // 实现单独罗列在下面
}
return nil

--------------------------------------------------------------------------------------------
// MineSnail() <= miningBlock, pointerDifficulty, seed, found
--------------------------------------------------------------------------------------------
// 计算区块和水果的挖矿目标
maxUint128 = 2 ^ 128 - 1
fruitDifficulty = pointerDifficulty / 600    // 600为可配置的{FruitBlockRatio}参数
blockTarget = maxUint128 / header.Difficulty
fruitTarget = maxUint128 / fruitDifficulty
// 开启正式挖矿操作
nonce = seed
for {
    digest, hashResult = truehash(miningBlock.header.HashWithoutNonce(), nonce)
    if hashResult.SubBytes(0, 15) <= blockTarget {
        // 挖到区块是符合规范的
        if miningblock.Fruits() != nil {
            snailBlock = new(type.SnailBlock)
            CopyTo(snailBlock.header, miningBlock.header)
            snailBlock.header.Nonce = nonce
            snailBlock.header.MixDigest = Hash(digest)
            // 表明我们挖取到的是慢链区块
            snailBlock.header.Fruit = false
            snailBlock.fruits = miningBlock.fruits
            snailBlock.signs = nil
            // 通知我们找到了区块
            found <- snailBlock
            break
        }
    } else if hashResult.SubBytes(16) <= fruitTarget {
        // 挖到的水果是符合规范的
        if header.FastNumber != 0 {
            fruit = new(type.SnailBlock)
            CopyTo(fruit.header, miningBlock.header)
            fruit.header.Nonce = nonce
            fruit.header.MixDigest = Hash(digest)
            // 表明我们挖取到的是水果
            fruit.header.Fruit = true
            fruit.fruits = nil
            fruit.signs = miningBlock.signs
            // 通知我们找到了水果
            found <- fruit
        }
    }
    nonce++    // 找寻下一个nonce值
}

可见区块挖掘的过程大致是:

  1. 开启挖矿线程并获得起始nonce值
  2. 计算本次区块挖矿的目标值和水果挖矿的目标值
  3. 不停自增nonce值来计算区块头哈希值,直到满足挖取区块的条件或者挖取水果的条件为止
  4. 构建符合要求的区块或水果并通知网络模块

区块奖励的分配过程(accumulateRewardsFast:consensus/minerva/consensus.go) <

矿工挖出的慢链区块的奖励分配是通过拜占庭委员会生成快链区块时进行检测和触发的。在触发了慢链区块的奖励分发操作时,本次对应生成的快链区块将会记录这个慢链区块的区块哈希值和区块号。本阶段只讲慢链区块的奖励分发逻辑,伪代码如下:

// 传入参数 <= election type.CommitteeElection, state type.StateDB, fastHeader type.Header, snailBlock type.SnailBlock
baseCoin = power(0.98, fastHeader.Number / 4500) * 115555555555555 * 1e6    // 4500为可配置{SnailBlockRewardsChangeInterval}参数、115555555555555为可配置{SnailBlockRewardsBase}参数
// 计算委员会成员、区块矿工、水果矿工各得多少(目前的分配比例配置是82开)
committeeCoin = 0.2 * baseCoin
minerBlockCoin = 0.8 * baseCoin * (2 / 3)
minerFruitCoin = 0.8 * baseCoin * (1 / 3)
// 给区块矿工分发奖励
state.AddBalance(snailBlock.Coinbase(), minerBlockCoin)
// 给水果矿工分发奖励
blockFruits = snailBlock.Fruits()
if len(blockFruits) > 0 {
    coinPerFruit = minerFruitCoin / len(blockFruits)
    for f in range blockFruits {
        state.AddBalance(f.Coinbase(), coinPerFruit)
    }
} else {
    return error("invalid snail block")
}
// 给委员会分发奖励
coinPerCommittee = committeeCoin / len(blockFruits)
for f in range blockFruits {
    fruitSigns = f.Signs()
    // 获取委员会成员
    committeeMembers = new(type.CommitteeMember)
    for sign in range fruitSigns {
        publicKey = election.ExtractPublicKey(sign)
        member = election.GetMemberByPubKey(publicKey)
        append(committeeMembers, member)
    }
    // 获取通过投票的委员会成员地址
    committeeAdresses = new(type.Adress)
    for i, member in range committeeMembers {
        if fruitSigns[i].Result == VoteAgree {
            append(committeeAdresses, member.Coinbase)
        }
    }
    if len(committeeAdresses) == 0 {
        return error("invalid sign length")
    }
    // 给委员会成员分发奖励
    coinPerMember = coinPerCommittee / len(committeeAdresses)
    for adress in range committeeAdresses {
        state.AddBalance(adress, coinPerMember)
    }
}
return nil

由此可见慢链区块奖励分配的处理流程大致如下:

  1. 计算总的区块奖励
  2. 计算分摊给委员会、区块矿工和水果矿工的奖励
  3. 给区块矿工分发奖励
  4. 给所有被区块打包的水果背后的水果矿工分发分发奖励
  5. 给所有被水果打包的签名数据背后的委员会成员分发奖励

> 快链(FastChain)的区块打包和拜占庭委员会(PBFT)的共识机制

拜占庭委员会节点会收集区块链网络中的交易数据放入交易池tx_pool中,而快链区块正是从tx_pool中抽取交易数据来完成打包操作。拜占庭委员会在换届时,新的委员会成员中的第一个成员会被推举为本届委员会的leader节点。leader节点负责生成快链区块验证请求并在委员会之间按照PBFT实用拜占庭协议的要求完成共识,经过共识的快链区块会成为真正有效的区块在区块链网络中传播,每个验证过该区块的委员会成员也会广播自己对该区块的签名信息。矿工节点会收集快链区块和广播的签名信息,只有签名信息数量超过本届委员会成员数量的2/3时该快链区块才会被正式确认为有效区块。下面我将详细讲解快链区块的打包流程和拜占庭委员会的PBFT共识机制。

区块打包的处理过程(FetchFastBlock:etrue/pbft_agent.go) <

伪代码如下:

// 传入参数 <= bc type.BlockChain, sbc type.SnailkBlockChain, txPool type.TxPool, election type.Election
// 获取构建区块时间
timeStart = time.Now()
parent = bc.CurrentBlock()
if parent.Time() >= timeStart {
    timeStart = parent.Time() + 1
}
// 构建区块头
header = new(type.Header)
header.ParentHash = parent.Hash()
header.Number = parent.Number() + 1
header.Time = timeStart
// 计算GasLimit
contrib = (parent.GasUsed() + parent.GasUsed() / 2) / 1024    // 1024为可配置{GasLimitBoundDivisor}参数
decay = parent.GasLimit() / (1024 - 1) 
header.GasLimit = max(parent.GasLimit() - decay + contrib, 5000)     // 5000为可配置{MinGasLimit}参数
if header.GasLimit < 4712388 {    // 4712388为可配置{GenesisGasLimit}参数
    header.GasLimit = min(parent.GasLimit() + decay, 4712388)
}
// 从各个交易来源中抽取头部交易
headTxs = new([]type.Transaction)
for from, txs in range txPool.GetTransactions() {
    append(headTxs, txs[0])
}
SortByGasPriceDown(&headTxs)    // 实际的代码是按照大顶堆的结构组织交易序列
// 执行交易
stateDb = bc.StateAt(parent.Root())
if stateDb == nil {
    return error("invalid state hash")
}
header.GasUsed = 0
blockTxs = new([]type.Transaction)
blockReceipts = new([]type.Receipt)
feeTxs = 0
for tx in range headTxs {
    snapshot = stateDb.Snapshot()
    receipt, fee, gasUsed, err = ApplyTransaction(bc, &stateDb, tx)
    if err != nil  {
        stateDb.RevertToSnapshot(snapshot)
    } else {
        append(blockTxs, tx)
        append(blockReceipts, receipt)
        header.GasUsed += gasUsed
        feeTxs += fee
    }
}
// 检测区块奖励
header.SnailNumber = 0
header.SnailHash = nil
if (sbc.CurrentBlock().Number() - bc.CurrentReward()) >= 12 {    // 12为可配置{blockRewardSpace}参数
    header.SnailNumber = bc.CurrentReward()
    header.SnailHash = sbc.GetBlockByNumber(snailNumber).Hash()
}
if header.SnailNumber != 0 && header.SnailHash != nil {
    snailBlock = sbc.GetBlock(header.SnailHash, header.SnailNumber)
    if snailBlock {
        accumulateRewardsFast(election, stateDb, header, snailBlock)
    }
}
// 分发交易费
committee = election.GetCommittee(header.Number)    // 该过程涉及委员会选举操作
if committee.Empty() {
    return error("not have committee")
}
coinPerMember = feeTxs / len(committee)
for member in range committee {
    stateDb.AddBalance(member.Coinbase, coinPerMember)
}
// 构建快链区块
fastBlock = new(type.Block)
header.Root = Hash(stateDb)
header.TxHash = Hash(blockTxs)
header.ReceiptHash = Hash(blockReceipts)
header.Bloom = CreateBloom(blockReceipts)
CopyTo(fastBlock.header, header)
CopyTo(fastBlock.transactions, blockTxs)
// 生成区块签名数据并放入区块
voteSign = new(type.PbftSign)
voteSign.Result = enum.VoteAgree
voteSign.FastHeight = fastBlock.Number()
voteSign.FastHash = fastBlock.Hash()
voteSign.Sign = Sign(voteSign.HashWithoutSign(), self.privateKey)
append(fastBlock.signs, voteSign)
return fastBlock

快链区块打包交易的流程要比慢链区块复杂一些,主要涉及到以下几步:

  1. 构建快链区块
  2. 根据父区块计算当前块Gas上限值
  3. 从交易池中获取每个交易地址下的首个交易组成交易集合,并按照交易中的Gas价格降序排列
  4. 执行交易集合中的所有交易,收集收据、消耗的Gas和交易费,通过执行交易改变父区块快照下的世界状态数据库形成新的状态快照
  5. 将总的交易费平均分发给当前拜占庭委员会的所有成员
  6. 将总的收据集合和交易集合并写入区块
  7. 构建拜占庭签名数据并签名,该数据包含当前区块状态的高度和哈希值
  8. 将签名数据放入区块,至此,一个完整的快链区块构建完毕,等待被Request请求装包完成PBFT拜占庭共识流程

区块的拜占庭共识处理过程 <

在了解“初链”的拜占庭共识流程之前,需要了解PBFT(实用拜占庭容错)共识算法:
拜占庭容错流程
这张是非常经典的拜占庭容错过程示意图,该图形象地描述了PBFT共识算法的5个阶段:Request、PrePrepare、Prepare、Commit、Reply。C表示发起验证请求的客户端,0到3表示处理拜占庭共识的4台服务器,其中编号为3的服务器处于宕机状态。

  1. 客户端发起Request请求
  2. 接收到Request消息的服务器群发PrePrepare消息并进入Prepare阶段
  3. 其他服务器在接受到网络中的PrePrepare消息时群发Prepare消息并进入Prepare阶段
  4. 所有服务器在接收到超过2/3个服务器节点的Prepare消息时群发Commit消息并进入Commit阶段
  5. 所有服务器在接收到超过2/3个服务器节点的Commit消息时向客户端回复Reply消息
  6. 所有服务器进入Idle阶段等待下一轮共识

PBFT共识算法的一个重要前提就是要求“N ≥ 3F + 1”等式成立,其中N为总服务器数量,F为其中无法正常工作的服务器数量。用最通俗的语言转述这个等式的意义就是在所有运行PBFT协议的服务器网络中,有问题的服务器数量不能超过总服务器数量的1/3,即容错率为33%。"初链"的快链区块验证过程与上图所示的过程一致,拜占庭委员会中的leader节点就是图中的客户端节点,其余成员节点就是图中的服务器节点。不过“初链”不一样的地方是作为leader节点的客户端节点也会作为一个服务器节点参与PBFT共识,所有服务器节点在Reply阶段时都会向全网广播共识后的快链区块和它的签名数据。伪代码如下:

  1. 发送Request的处理(GetRequest:pbftserver/pbftserver.go)
// 传入参数 <= commitID type.Int
// 验证PBFT服务信息
server = self.servers[commitID]
if server == nil {
    return error("wrong committ ID")
}
if server.leader != self.publicKey {
    return error("local node must be leader")
}
// 打包快链区块
fastBlock = FetchFastBlock()
if fastBlock == nil {
    return error("wrong fetch")
}
if self.blocks[fastBlock.Number()] != nil {
    return error("same height")
}
self.blocks[fastBlock.Number()] = fastBlock
// 创建Request消息
reqMsg = new(type.RequestMsg)
reqMsg.ClientId = server.nodeID
reqMsg.Timestamp = time.Now()
reqMsg.Operation = ToHex(fastBlock.Bytes())    // 快链区块16进制编码
reqMsg.Height = fastBlock.Number()
// 广播Request消息
self.Broadcast(reqMsg)
return nil

流程可以大致总结为:

  1. 确认本节点是否是leader节点
  2. 打包交易生成等待PBFT共识的快链区块
  3. 构建Request请求,将快链区块放入其中
  4. 广播Request请求
  1. 接收Request、发送PrePrepare的处理(GetReq:pbftserver/network/node.go)
// 传入参数 <= reqMsg type.RequestMsg
// 创建本轮共识状态
lastSequenceID = 0    // 序列ID以于保证共识的进程是按照时钟稳步推进的
if len(self.committedMsgs) == 0 {    // committedMsgs属性是该节点最后确认的commit投票消息的集合(Commit阶段会填充)
    lastSequenceID = -1
} else {
    lastSequenceID = self.committedMsgs.Last().SequenceID
}
consensus = new(type.State)
consensus.log = new(type.MsgLogs)    // 共识日志,用于记录收集到的Request、Prepare和Commit消息
consensus.log.reqMsg = nil
consensus.log.prepareVotes = new(map[NodeId]type.VoteMsg)
consensus.log.commitVotes = new(map[NodeId]type.VoteMsg)
consensus.lastSequenceId = lastSequenceID
consensus.blockResult = nil
self.consensusStates[reqMsg.Height % 1000] = consensus    // 1000为可配置{StateMax}参数,意味着委员会能同时处理1000次共识
// 从共识状态集中获取本轮共识状态(必定成功)
consensus = self.consensusStates[reqMsg.Height % 1000]
if consensus == nil {
    return error("wrong consensus state")
}
// 处理接收到的Request数据
reqMsg.SequenceID = time.Now().Nano()
if consensus.lastSequenceId != -1 {
    reqMsg.SequenceID = max(reqMsg.SequenceID, consensus.lastSequenceID + 1)
}
consensus.log.reqMsg = reqMsg
// 进入PrePrepare阶段
digest = digest(reqMsg)    // 计算Request消息的数据指纹用于之后的验证
if digest == nil {
    return error("cannot make req digest")
}
consensus.currentStage = enum.PrePrepared
// 创建PrePrepare消息
prePreMsg = new(type.PrePrepareMsg)
prePreMsg.SequenceID = reqMsg.SequenceID
prePreMsg.Digest = digest
prePreMsg.RequestMsg = reqMsg
prePreMsg.Height = reqMsg.Height
// 广播PrePrepare消息
self.Broadcast(prePreMsg)
return nil

流程可以大致总结为:

  1. 收到并验证Request请求,跟据快链区块高度构建对应的共识状态
  2. 构建PrePrepare请求,将Request请求放入其中
  3. 广播PrePrepare请求
  1. 接收PrePrepare、发送Prepare的处理(GetPrePrepare:pbftserver/network/node.go)
// 传入参数 <= prePreMsg type.PrePrepareMsg
// 创建本轮共识状态(同上,因为发送Request请求的委员会leader还处在Idle状态,作为客户端的leader也需要参与共识)
... ()
// 从共识状态集中获取本轮共识状态(必定成功)
consensus = self.consensusStates[prePreMsg.Height % 1000]
if consensus == nil {
    return error("wrong consensus state")
}
// 抽取区块并暂存
fastBlock = FromHex(prePreMsg.RequestMsg.Operation)
self.blocksCache[fastBlock.Number()] = fastBlock
// 验证PrePrepare消息
if consensus.lastSequenceID != -1 {
    if consensus.lastSequenceID >= prePreMsg.SequenceID {
        return error("wrong last sequence id")
    }
}
if digest(prePreMsg.RequestMsg) != prePreMsg.Digest {
    return error("wrong digest")
}
// 进入PrePrepare阶段
consensus.log.reqMsg = prePreMsg.RequestMsg
consensus.currentStage = enum.PrePrepared
// 创建Prepare投票消息
prepareVote = new(type.VoteMsg)
prepareVote.NodeID = self.nodeID    // 投票时需要提供自己的节点ID
prepareVote.SequenceID = prePreMsg.SequenceID
prepareVote.Digest = prePreMsg.Digest
prepareVote.MsgType = enum.PrepareMsg    // 标识该投票是一次Prepare投票
prepareVote.Height = prePreMsg.Height
// 将自己创建的Prepare投票加入共识日志
append(consensus.log.prepareVotes, prepareVote)
// 广播Prepare投票消息
self.Broadcast(prepareVote)
return nil

流程可以大致总结为:

  1. 收到并验证PrePrepare请求,抽取其中的快链区块放入缓存
  2. 构建Prepare投票,将PrePrepare请求放入其中
  3. 广播Prepare投票
  1. 接收Prepare、发送Commit的处理(GetPrepare:pbftserver/network/node.go)
// 传入参数 <= prepareVote type.VoteMsg
// 从共识状态集中获取本轮共识状态
consensus = self.consensusStates[prepareVote.Height % 1000]
if consensus == nil || consensus.log.reqMsg.SequenceId != prepareVote.SequenceId {
    return error("wrong consensus state")
}
// 计算容错节点数
f = len(self.NodeTable) / 3    // 对,你没有看错,不是(len(self.NodeTable) - 1) / 3
consensus.log.prepareVotes[prepareVote.NodeID] = prepareVote
// 验证Prepare消息
if consensus.lastSequenceID != -1 {
    if consensus.lastSequenceID >= prepareVote.SequenceID {
        return error("wrong last sequence id")
    }
}
if digest(prepareVote.RequestMsg) != prepareVote.Digest {
    return error("wrong digest")
}
if len(consensus.log.prepareVotes) < 2 * f {
    return error("need to wait more prepare votes")
}
// 创建Commit投票消息
commitVote = new(type.VoteMsg)
commitVote.NodeID = self.nodeID
commitVote.SequenceID = prepareVote.SequenceID
commitVote.Digest = prepareVote.Digest 
commitVote.MsgType = enum.CommitMsg
commitVote.Height = prepareVote.Height
// 如果该节点已投过Commit投票,则直接回发给Prepare投票的发起节点
if consensus.currentStage == enum.Prepared {
    self.BroadcastTo(prepareVote.NodeID, commitVote)
// 否则走正常的广播流程
} else {
    voteResult = enum.VoteAgree
    // 从区块缓存中获取区块并验证
    fastHeight = consensus.log.reqMsg.Height
    fastBlock = self.blocksCache[fastHeight]
    if fastBlock == nil {
        voteResult = enum.VoteAgreeAgainst
    } else if err = VerifyFastBlock(fastBlock); err != nil {
        voteResult = enum.VoteAgreeAgainst
    }
    // 对区块签名
    voteSign = new(type.SignedVoteMsg)
    voteSign.Sign = Sign({fastBlock.Hash(), fastHeight, voteResult, self.publicKey}, self.privateKey)
    voteSign.FastHeight = fastHeight
    voteSign.Result = voteResult
    commitVote.Pass = voteSign
    // 进入Prepare阶段
    consensus.blockResult = commitVote.Pass
    consensus.currentStage = enum.Prepared
    // 广播Commit投票消息
    self.Broadcast(commitVote)
}
return nil

流程可以大致总结为:

  1. 收到并验证Prepare投票,持续收集直到数量达到委员会总节点数的2/3
  2. 从区块缓存中获取对应的快链区块,验证区块并签名
  3. 根据区块验证结果同意或者拒绝这个Prepare投票
  4. 构建Commit投票,将Prepare投票、同意或者拒绝Prepare投票的结果和区块签名放入其中
  5. 广播Commit投票
  1. 接收Commit的处理(GetCommit:pbftserver/network/node.go)
// 传入参数 <= commitVote type.VoteMsg
// 从共识状态集中获取本轮共识状态
consensus = self.consensusStates[commitVote.Height % 1000]
if consensus == nil {
    return error("wrong consensus state")
}
// 只在共识状态不为Commited时继续处理
consensus.log.commitVotes[commitVote.NodeID] = commitVote
if consensus.currentStage != enum.Committed {
    // 计算容错节点数
    f = len(self.NodeTable) / 3
    // 验证Commit消息
    if consensus.lastSequenceID != -1 {
        if consensus.lastSequenceID >= commitVote.SequenceID {
            return error("wrong last sequence id")
        }
    }
    if digest(commitVote.RequestMsg) != commitVote.Digest {
        return error("wrong digest")
    }
    if len(consensus.log.prepareVotes) < 2 * f
        || len(consensus.log.commitVotes) < 2 * f  {
        return error("need to wait more prepare votes or commit votes")
    }
    passCount = 0
    for v in range consensus.log.commitVotes {
        if v.Pass != nil && v.Pass.Result == enum.VoteAgree {
            passCount += 1
        }
    }
    if passCount < 2 * f {
        return error("need to wait more passed commit votes")
    }
    // 进入Commit阶段
    consensus.currentStage = enum.Committed
    append(self.committedMsgs, commitVote)
    // 获取区块并生成区块签名
    fastBlock = self.blocksCache[consensus.log.reqMsg.Height]
    if fastBlock != nil {
        return error("cannot find block from cache")
    }
    voteSign = new(type.PbftSign)
    voteSign.Result = enum.VoteAgree
    voteSign.FastHeight = fastBlock.Number()
    voteSign.FastHash = fastBlock.Hash()
    voteSign.Sign = Sign(voteSign.HashWithoutSign(), self.privateKey)
    // 广播区块和区块签名
    self.BroadcastFastBlock(fastBlock)
    self.BroadcastSign(voteSign)
}
return nil

流程可以大致总结为:

  1. 收到并验证Commit投票,持续收集直到同意的票数达到委员会总节点数的2/3
  2. 从区块缓存中获取对应的快链区块,生成最终的PBFT签名
  3. 广播快链区块和PBFT签名
  4. 回到第一阶段

> 拜占庭委员会(PBFT)的选举和验证机制

了解了拜占庭委员会的共识机制后,我们肯定会好奇:拜占庭委员会是如何选举出来的?前面部分我有提到过,拜占庭委员会是在慢链上选举出来的,每隔1440个慢链区块就会进行一次委员会换届选举,当然在“初链”初期肯定会像存在“创世区块”一样存在“创世拜占庭委员会”。那下面我们就来探究拜占庭委员会是如何从1440个慢链区块中选举出来的。

拜占庭委员会的选举过程(getCommittee、electCommittee:etrue/election.go) <

选举的伪代码如下:

// 传入参数 <= snailBeginNumber type.Int, snailEndNumber type.Int, bc type.SnailBlockChain
totalDifficulty = 0
electionSeed = new([]type.Byte)
fruitsCount = new(map[type.Address]type.Int)
candidateMembers = new([]type.CandidateMember)
// 获取选举候选人名单
for n = snailBeginNumber; n <= snailEndNumber; n++ {
    snailBlock = bc.GetBlockByNumber(n)
    if snailBlock == nil {
        return nil
    }
    append(electionSeed, snailBlock.Hash())
    for f in range snailBlock.Fruits() {
        if f.ToElect() {      // 是否有意愿参与选举
            address = ToAddress(f.GetPubKey())
            actualDifficulty, targetFruitDifficulty = GetDifficulty(f.Header().HashWithoutNunce(), f.Nonce())    // 具体的难度算法参考挖矿算法
            // 添加候选人水果记录
            fruitsCount[address]++
            if fruitsCount[address] >= 1 {    // 1为可配置{fruitThreshold}参数
                // 添加选举候选人
                candidate = new(type.CandidateMember)
                candidate.Coinbase = f.Coinbase()
                candidate.PublicKey = f.GetPubKey()
                candidate.Address = address
                candidate.Difficulty = actualDifficulty - targetFruitDifficulty    // 这里不会出现负数
                append(candidateMembers, candidate)
                // 记录总难度值
                totalDifficulty += candidate.Difficulty
            }
        }
    }
}
// 计算各候选人的挖矿难度在256位数值里的区间分布
maxUint256 = 2 ^ 256 - 1
rate = maxUint256 / totalDifficulty
d = 0
for member in range candidateMembers {
    member.Lower = d * rate
    if member == candidateMembers.Last() {
        member.Upper = maxUint256
    } else {
        d += member.Difficulty
        member.Upper = d * rate
    }
}
// 从候选人名单中选出最终的委员会成员
round = 0
addressCache = new(map[type.Address]type.Int)
committeeMembers = new([]type.CommitteeMember)
for {
    randomSeed = ToInt(seed) + round    // 可验证的随机值种子
    randomValue = ToInt(Hash(randomSeed)) / maxUint256    // 可验证的随机值
    for c in range candidateMembers {
        // 命中某个候选人的难度空间
        if randomValue < c.Lower || randomValue >= c.Upper {
            continue
        }
        // 不能重复被选
        if addressCache[c.Address] != nil {
            break
        }
        addressCache[c.Address] = 1
        // 加入拜占庭共识委员会
        member = new(type.CommitteeMember)
        member.Coinbase = c.Coinbase
        member.PublicKey = c.PublicKey
        append(committeeMembers, member)
        break
    }
    round += 1
    if round >= 40 {    // 40为可配置{maxCommitteeNumber}参数
        if len(candidateMembers) >= 1 {    // 1为可配置{minCommitteeNumber}参数
            break
        }
    }
}
return committeeMembers

由伪代码可以看出,共识委员会的选举是先确定慢链的区块区间,在这个区间内将所有愿意参加选举的水果矿工放入候选人名单中,根据他们贡献的多出的水果难度值计算它们在256位数值空间中的分布,最后通过VRF(可验证随机函数)随机在256位数值空间中选取命中的候选人成为正式的共识委员会成员。那现在的问题是,那个慢链的区块区间是如何确定的?我们还是用伪代码来阐述,获取拜占庭共识委员会处理过程的伪代码如下:

//  传入参数 <= bc type.BlockChain, sbc type.SnailBlockChain
// 双链头部区块号
headFastNumber = bc.CurrentHeader().Number
headSnailNumber = sbc.CurrentHeader().Number
// 计算当前委员会编号和委员会换届检查点
committeeID = headSnailNumber / 1440    // 1440为可配置{z}参数
switchCheckPoint = committeeID * 1440
electEndNumber = switchCheckPoint - 12    // 12为可配置{lamada}参数,选举区块区间要比检查点滞后12个区块(约2小时,为换届提供检查间隙)
electBeginNumber = electEndNumber - 1440 + 1
// 开始创建拜占庭共识委员会
committee = new(type.Committee)
committee.ID = committeeID
lastFastNumber = sbc.GetBlockByNumber(electEndNumber).Fruits().Last().Number()
// 如果上一届共识委员会完成共识的最后一个快链区块不为我所知,则仍保持上一届委员会不变
if lastFastNumber > headFastNumber {
    electEndNumber, electBeginNumber -= 1440, 1440
    preLastFastNumber = sbc.GetBlockByNumber(electEndNumber).Fruits().Last().Number()
    // 获取拜占庭委员会成员
    committeeMembers = self.electCommittee(electBeginNumber, electEndNumber)
    committee.FastBeginNumber = preLastFastNumber + 1    // 该委员会包含的下一段1440个区块中的水果的起始编号
    committee.FastEndNumber = lastFastNumber    // 该委员会包含的下一段1440个区块中的水果的结束编号(后面会时刻检测后续的快链区块是否达到该高度,一旦达到立刻换届)
    committee.SwitchCheckPoint = switchCheckPoint    // 委员会换届检查区块号
    committee.Members = committeeMembers
} else {
    // 获取拜占庭委员会成员
    committeeMembers = self.electCommittee(electBeginNumber, electEndNumber)
    committee.FastBeginNumber = lastFastNumber + 1
    committee.FastEndNumber = 0    // 由于下一段1440个区块还没成型,所以水果的结束编号无法确定
    committee.SwitchCheckPoint = switchCheckPoint + 1440    // 委员会换届检查区块号(后面会时刻检测后续的慢链区块是否达到该高度,一旦达到立刻开启委员会换届流程)
    committee.Members = committeeMembers
}
committee.SnailBeginNumber = electBeginNumber
committee.SnailEndNumber = electEndNumber
return committee

实际的拜占庭委员会选举过程要比伪代码中描述的更为复杂,主要涉及到监听广播的慢链区块和快链区块到达本节点后对委员会操作的影响,比如慢链区块到达换届检查点时开始换届、快链区块达到本届结束点时直接启用新一届委员会。详细的情况建议直接去看源代码,翻译成伪代码的话会比较复杂。

拜占庭委员会的验证过程(VerifySigns:etrue/election.go) <

拜占庭委员会的签名数据被水果打包,所以验证水果中的签名数据就是在验证拜占庭委员会成员的有效性。验证的伪代码如下:

// 传入参数 <= pbftSigns []type.PbftSign
for sign in range pbftSigns {
    committeeMembers = self.GetCommittee(sign.FastHeight)    // 根据快链区块号获得历史委员会名单
    if committeeMembers == nil {
        return error("invalid committee")
    }
    publicKeyInSign = SigToPub(sign.HashWithoutSign(), sign.Sign)
    if publicKeyInSign == nil {
        return error("cannot resolve publickey from sign")
    }
    find = false
    for m in range committeeMembers {
        if m.PublicKey == publicKeyInSign {
            find = true
            break
        }
    }
    if find == false {
        return error("invalid pbft sign")
    }
}
return nil

> 抗ASIC专用矿机的POW挖矿算法(TrueHash)

前面提到要抵制矿池的形成,要么设计抗ASIC矿机的挖矿算法,要么降低矿池的形成动力。读完这一章我们就能了解到,“初链”不仅通过引入“水果”来降低矿池的形成动力,它还自研了一套专门针对ASIC矿机的挖矿算法来同步抵制矿池的形成。以太坊的挖矿算法是传统的读大表的内存困难性算法,它的哈希计算过程需要耗费大量的内存,这算是ASIC矿机的一个痛点,但是这并不能从根本上抵制矿机,最多就是延缓矿机的形成。目前针对以太坊的专用矿机已被成功生产,与传统机器相比挖矿效率提升了13%左右。

如何设计能抵制矿机的挖矿算法? <

既然说到抗ASIC专用矿机,那到底何为矿机?要明白矿机的原理,还得从传统的“冯诺依曼瓶颈”说起。在传统的冯诺依曼架构中,储存单元(内存)和计算单元(CPU)由多条总线连接,程序代码在执行时需要从储存单元经过总线传输至计算单元中来完成计算。总线让储存单元和计算单元分离,这种架构极大地提高了硬件系统的可扩展性,但总线的传输带宽总是有限的,有限的总线带宽所带来的效率瓶颈,我们称之为“冯诺依曼瓶颈”。再者,目前市面上CPU的指令集繁多,在执行Hash运算时那些不相干的指令在执行过程中占据了大头,而真正留给执行Hash运算的指令却很少,即计算Hash的指令在现有的CPU上执行率较低。针对以上两个问题,如果将计算程序直接写死在计算单元内,并且只留存计算Hash的指令,那可想而知,在Hash运算的耗电效率上,这种特制的机器会成倍的高于传统机器,而这种特制的机器就被叫做ASIC(Application Specific Integrated Circuit)专用矿机。
在这里插入图片描述
既然ASIC矿机是将计算Hash的程序直接写死在计算单元内来突破冯诺依曼瓶颈的,那是不是将这种计算Hash的程序设定为可变的那就能彻底抵制矿机了呢?我们来看看门罗区块链是怎么做的。门罗的做法是通过社区投票让区块链产生周期性的硬分叉,从而实现Hash算法的可变,但这种做法在“初链”看来太没技术含量,所以“初链”就自研了一套可以跟据区块高度自动地并且随机地切换算法要素的Hash算法,名为“TrueHash”。由于初链挖矿算法的实现过程过于专业,博主本人无奈高数一般,实在搞不懂背后的数学原理,所以这里只能抽象地介绍“TrueHash”的实现过程,具体的实现过程建议去看源代码。

TrueHash挖矿算法(fchainmining:consensus/minerva/algorithm_truehash.go) <

伪代码如下:

// 传入参数 <= 、乱序查找表 plookup type.[]uint64, 区块头 header type.[]byte, 本轮尝试值 nonce type.uint64
seed = MakeSeed(header, nonce)    // 通过区块头和验证nonce值生成哈希种子
seedHash = SHA512(seed)
permuteSource = ByteReverse(seedHash)
permute = MakePermute(permuteSource)     // 跟据哈希值生成置换组
scrambledPermute = Scramble(permute, plookup)  // 实现哈希算法可变的关键步骤:通过乱序查找表打乱置换组
dataSource = MakeSource(scrambledPermute)  // 使用打乱后的置换组生成哈希数据源
return SHA256(dataSource)

“MakeSeed”、“MakePermute”和“MakeSource”是一系列使用“For循环”的数组查找和位操作过程,它们都是直接展开在“fchainmining”函数中的,这里我将他们封装成函数调用来抽象说明各自的计算目的(感兴趣的朋友可以直接翻阅源代码),而函数“ByteReverse”和“Scramble”则事实上存在,其中“Scramble”函数是让整个哈希计算具备可变性的核心函数。虽然“Scramble”函数的执行过程固定,但传入的“plookup”参数会每隔12000个慢链区块就进行一次切换,也就是说除非等到第12000个慢链区块(其实到第10240个慢链区块就可以了,具体看伪代码)生成完毕,不然谁也不知道接下来的“plookup”参数会是个什么样子。要挖出所有12000个慢链区块需要花费大约83天的时间,这样即使真有人要针对“初链”生产专用矿机,那这种矿机会每隔83天就需要重做一遍,这显然是不划算的,因为要想制造一款能够包含所有切换值的矿机,那冯诺依曼架构就始终无法绕过,可想而知,谁又愿意制造一款具有“冯诺依曼瓶颈”的矿机呢?更何况制造矿机的初衷本来就是为了绕过“冯诺依曼瓶颈”。那下面我们再来讲讲最关键的“plookup”乱序查找表的切换过程,伪代码如下:

乱序查找表的切换(generate:consensus/minerva/minerva.go) <

// dataset wraps an truehash dataset with some metadata to allow easier concurrent use.
type dataset struct {
    epoch       uint64
    dataset     []uint64
    once        sync.Once
    dateInit    int
}
...
// 传入参数 <= blockNumber type.uint64, ds type.dataset
// 数据集还未初始化则开启初始化流程
if (ds.dateInit == 0) {
    if (blockNumber <= 12000) {        // 12000为可配置的{UPDATABLOCKLENGTH}参数
        // 初始化数据集,具体算法可查看源代码
        truehashTableInit(ds.evenDataset)
    } else {
        // 选取开始更新数据集的区块高度,其中10240为可配置的{STARTUPDATENUM}参数
        blockNumberToUpdate = (blockNumber / 12000 - 1) * 12000 + 10240 + 1
        // 跟据传入的慢链区块高度确定本次12000个区块的所在区间,然后再根据前10240个区块的区块头更新dataset数据集,具体算法可查看源代码
        flag, dataset = updateLookupTable(blockNumberToUpdate, ds.dataset)
        if (flag) {
            ds.dataset = dataset
        }
    }
    ds.dateInit = 1
}
// 当慢链区块的高度在新一轮的12000个区块区间内已超过10240个区块时,则跟据这10240个区块更新dataset数据集
if (blockNumber % 12000 >= 10240) {
    flag, dataset = updateLookupTable(blockNumberToUpdate, ds.dataset)
    if (flag) {
        ds.dataset = dataset
    }
}

> “初链”和以太坊的关系

“初链”是构建在以太坊之上的区块链,但和以太坊拥有不同的共识策略。“初链”的底层诸如RLP编码、MPT字典树、Kadelima节点发现机制和EVM虚拟机等均是以太坊的底层基础设施,所以“初链”目前是在以太坊之上进行共识修改的一个区块链项目,拥有相同的地址结构(ERC20),这是目前国内绝大多数区块链项目的现状。后期“初链”会逐步、逐层地替换掉以太坊的基础设施,到那时“初链”就是一条完全原创的、富有竞争力的国产区块链。目前对于“初链”代码,最有研究价值的部分就是其混合共识和双链机制。如果你熟悉以太坊的代码,那阅读“初链”的代码对你来说就会是一件比较轻松的事情,如果你完全没接触过任何区块链代码,那建议先从了解以太坊的工作原理和代码结构开始。在阅读一个区块链项目的代码之前,先从阅读其白皮书和黄皮书(如果有的话)开始。

> “初链”的代码审查意见

为了理清“初链”整个混合共识的细节,我花了一周多的时间来精读“初链”的代码,这才有了本篇博客(本博客写了5天)。但在阅读过程中确实也发现了诸多问题,在这里简单罗列出来(纯个人见解):

1.代码整体风格不统一,排版混乱,非常影响阅读体验
2.代码凌乱,各种注释和调试代码,极其影响阅读体验
3.很多细节的处理逻辑比较混乱,明明可以用简单方式实现的却偏偏用复杂和臃肿的方式去实现,这增加调试难度,影响代码维护,也可能导致潜在的BUG
4.代码的测试覆盖度不足,建议后续的单例测试逐步跟上
5.模块的分离度不足,耦合度较高,在职责划分上不是很清晰
6.大量使用分散的channel组织事件回调,没有统一的事件处理,增加调试和维护难度
7.项目文档不足,尤其是对于混合共识和双链交互的描述,不是稀少就是晦涩
8.在代码设计上明显感觉到没有一个牵头人去管理和审核,各自为政,导致代码的上手难度很高,打开项目后简直无从下手
9.代码的说明性注释有些地方有偷懒的嫌疑,文不对版,但总体来说注释的提供程度较高,有利于代码阅读

最后是一些流程处理上的意见和建议:

1.建议在Beta版本稳定后重构整个项目,为以后项目的稳定运行和维护打好基础
2.拜占庭委员会的leader节点统一为第一个委员会成员,这应该是出于时钟同步难度较高的考虑,后期可以考虑研究节点间时钟同步的课题,通过VRF算法配合逻辑时钟实现leader节点的可验证随机性,这可以提高委员会的安全性
3.水果和慢链区块使用同一个结构表示,这应该是出于挖矿统一性的考虑,但建议区分成两个,避免后期单独改进水果或者慢链区块时很难下手的情况
4.拜占庭委员会达成共识期间对于消息的验证貌似只有时钟ID和Request数据指纹两个维度,建议增加更多的维度以提高委员会的安全性,比如委员会成员之间身份验证信息
5.拜占庭委员会在Prepare和Commit阶段的容错性检测参数“f=N/3”与PBFT算法给出的标准公式“f=(N-1)/3”在极个别情况下会表现不同,不太清楚会不会有问题
6.投入精力研究分片储存功能,进一步提高区块链网络的运行效率

> 总结

总体来说本篇博客以源代码翻译伪代码的方式纯技术讲解“初链”混合共识的处理细节。采用伪代码的方式主要是因为要理解一个程序的执行细节除了看代码之外没有其他方法,纯文字的讲解会存在很多误解和疑虑,如果直接贴源代码又会变得晦涩难懂,所以综合考虑采用源代码翻译伪代码方式是最合适的,但对于更加细致的处理过程和详细的交互逻辑就需要下来阅读“初链”的源代码才能知晓。由于“初链”项目对于混合共识部分的文档少之又少,目前只有黄皮书和水果链论文两篇文档,所以本篇博客的目的一方面是总结“初链”的混合共识和双链逻辑,另一方面是打算做成“初链”的混合共识和双链机制的详细文档,丰富项目的生态,也为打算采用混合共识作为共识决策的区块链项目提供参考意见。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章