對“初鏈”混合共識、雙鏈結構和抗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.投入精力研究分片儲存功能,進一步提高區塊鏈網絡的運行效率

> 總結

總體來說本篇博客以源代碼翻譯僞代碼的方式純技術講解“初鏈”混合共識的處理細節。採用僞代碼的方式主要是因爲要理解一個程序的執行細節除了看代碼之外沒有其他方法,純文字的講解會存在很多誤解和疑慮,如果直接貼源代碼又會變得晦澀難懂,所以綜合考慮採用源代碼翻譯僞代碼方式是最合適的,但對於更加細緻的處理過程和詳細的交互邏輯就需要下來閱讀“初鏈”的源代碼才能知曉。由於“初鏈”項目對於混合共識部分的文檔少之又少,目前只有黃皮書和水果鏈論文兩篇文檔,所以本篇博客的目的一方面是總結“初鏈”的混合共識和雙鏈邏輯,另一方面是打算做成“初鏈”的混合共識和雙鏈機制的詳細文檔,豐富項目的生態,也爲打算採用混合共識作爲共識決策的區塊鏈項目提供參考意見。

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