1. 程式人生 > >對“初鏈”混合共識和雙鏈結構的詳解

對“初鏈”混合共識和雙鏈結構的詳解

前段時間寫了篇部落格說了說自己對“初鏈”白皮書和黃皮書的解讀,其中一部分涉及到對“初鏈”混合共識和雙鏈技術的解讀,由於是從說明文件中獲取的資訊,難免會有誤解,雖然大體上和實際的處理邏輯相符合,但在細節上還是有一定的出入。出於對混合共識技術和雙鏈邏輯的好奇,博主我下來抽空餘時間仔細閱讀了下“初鏈”專案共識部分的程式碼,感悟頗深,遂寫文分享。本篇部落格的目的是讓對區塊鏈有一定了解且有一定技術開發經驗的朋友快速、深入地瞭解“初鏈”混合共識機制的處理邏輯,主要以原始碼翻譯虛擬碼(基於“初鏈”Beta版本)的形式講解各個處理過程,因為只有用虛擬碼的方式才能直觀的表述一個處理過程的細節,但我並不會過分深入每一個細節。本篇部落格包括以下內容:

  • 快鏈(FastChain)、慢鏈(SnailChain)的區塊結構、區塊驗證和互動機制
  • 慢鏈(SnailChain)的區塊打包、挖礦和獎勵分配機制
  • 快鏈(FastChain)的區塊打包和拜占庭委員會(PBFT)的共識機制
  • 拜占庭委員會(PBFT)的選舉和驗證機制
  • “初鏈”和以太坊的關係
  • “初鏈”的程式碼審查意見
  • 總結

> 快鏈(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個區塊的慢鏈出塊時間差不多3個月的樣子),期間只要有勢力能佔領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(core/snail_pool.go)中等待打包。礦工在挖礦之前會先準備挖礦環境,從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. 填充所有能升序排列的水果
  6. 分別計算和填充簽名集合和水果集合的雜湊值

區塊挖掘(計算雜湊值)的處理過程(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 ^