技術篇:關於EKT的一些設計

前言

筆者做了一段時間的區塊鏈底層開發,深知架構設計的重要性。對於高手來說,沒有的輪子是可以自己造的,造個大規模消息/任務隊列都只是想不想寫的事情。但在企業中開發,追求的是穩定、性能、成本等等,所以通常希望使用開源組件,二次開發。

解析過EKT項目,鑑於自己還不是高手,把自己認爲有用的點都總結下。希望對來往的看官老爺有用。

懂分享的人,一定會快樂!

賬戶設計

和ETH類似,用了賬戶模型,結合Merkle樹進行設計,通過記錄nonce值防止雙花攻擊。

核心邏輯:

func GenerateKeyPair() (pubkey, privkey []byte) {
	key, err := ecdsa.GenerateKey(S256(), rand.Reader)
	if err != nil {
		panic(err)
	}
	pubkey = elliptic.Marshal(S256(), key.X, key.Y)
	return pubkey, math.PaddedBigBytes(key.D, 32)
}

EKT採用ECDSA(橢圓曲線數字簽名算法)生成地址,secp256k1方法作爲該算法參數。

工程中,ecdsa和sha3_256算是兩個主流加密算法。ecdsa(橢圓曲線數字簽名算法)是一種非對稱公鑰加密算法,也是數字簽名算法類比中的佼佼者,用於防止數據串改和驗證數據真實性,對標RSA算法。sha3_256是一種哈希算法,也叫摘要技術,防止數據被篡改。

ECDSA相比於RSA有如下特點:

  • ECDSA的加密密鑰更短
  • ECDSA的加密運算更快而安全性和RSA相當
  • RSA的私鑰和公鑰是可以互換加解密的,但ECDSA只能私鑰加密公鑰解密

ECDSA的核心是利用數論中大數分解比較困難。筆者在微信公衆號後臺,列出一些推薦的擴展閱讀,回覆"ecdsa"可以獲取到資源。

存儲相關

EKT的數據庫採用LevelDB和sync.map。LevelDB是Key-Value型數據庫,用於數據持久化。sync.map是一種GO語言的數據結構,可用於緩存。EKT封裝了sync.map,開發了自己的內存型K-V數據庫。

早期,有兩個核心的文件:db/levedb.go和db/MemKVDatabase.go。

在實際代碼中,EKT將本地KV和內存KV組裝在一起,構成混合型KV數據庫。核心文件db/ComposedKVDatabase.go 代碼:

type ComposedKVDatabase struct {
	mem     *MemKVDatabase    // 引用內存型K-V數據庫
	levelDB *LevelDB          // 引用本地K-V數據庫
}

// 抽象該混合型KV數據庫
func NewComposedKVDatabase(filePath string) *ComposedKVDatabase {
	return &ComposedKVDatabase{
		mem:     NewMemKVDatabase(),
		levelDB: NewLevelDB(filePath),
	}
}

該數據庫只有三個常用方法:

  • Set(key, value []byte):插入數據
  • Get(key []byte):查找數據
  • Delete(key []byte):刪除數據

因爲採用線性結構的區塊鏈基因,所以並不會涉及update。

隨着代碼的迭代,筆者實測後發現:數據存在丟失的情況。之後,EKT官方去掉了自己的內存型K-V數據庫,僅保留了leveldb相關。

這裏,再次證明,穩定性好的東西,實在不好做。

鏈結構相關

鏈的結構包含了14個元素,依賴了外部包:i_consensus/consensus.go, pool/TxPool.go, police.go, block_manager.go

type BlockChain struct {
	ChainId       int64
	Consensus     i_consensus.ConsensusType    // 確認採用DPoS,Pow, Pos
	currentLocker sync.RWMutex
	currentBlock  Block
	currentHeight int64
	Locker        sync.RWMutex
	Status        int
	Fee           int64
	Difficulty    []byte
	Pool          *pool.TxPool                // 交易池
	BlockInterval time.Duration
	Police        BlockPolice                 // 用於記錄從其他節點過來的block
	BlockManager  *BlockManager               // 區塊管理器
	PackLock      sync.RWMutex
}

各字段的解釋官方沒有給出,之後通過對代碼的詳細分析,再給出精準定義。

簡單提下創世過程。當主鏈在啓動時發現沒有區塊的時候,將執行寫創世區塊的功能。

創世核心源碼:

// 將創世塊寫入數據庫
accounts := conf.EKTConfig.GenesisBlockAccounts
block = &blockchain.Block{
    Height:       0,
    Nonce:        0,
    Fee:          dpos.Blockchain.Fee,
    TotalFee:     0,
    PreviousHash: nil,
    CurrentHash:  nil,
    BlockBody:    blockchain.NewBlockBody(),
    Body:         nil,
    Timestamp:    0,
    Locker:       sync.RWMutex{},
    StatTree:     MPTPlus.NewMTP(db.GetDBInst()),
    StatRoot:     nil,
    TxTree:       MPTPlus.NewMTP(db.GetDBInst()),
    TxRoot:       nil,
    TokenTree:    MPTPlus.NewMTP(db.GetDBInst()),
    TokenRoot:    nil,
}

// 爲每個創世賬戶更新默克爾樹根
for _, account := range accounts {
    block.CreateGenesisAccount(account)
}

// 更新默克爾樹根,改變StatRoot,使得block.StatRoot = block.StatTree.Root
block.UpdateMPTPlusRoot()

// 計算當前區塊Hash值
block.CaculateHash()

// 持久化
dpos.Blockchain.SaveBlock(*block)

獲取創世區塊的賬戶(可以是多個賬戶),由主鏈啓動時配置得到。生成首個區塊的數據,做了一些改動後,寫入數據庫。

主鏈啓動

經過對主鏈、共識機制的初始化,再運行共識模塊的Run()即可啓動。

主要有兩步:

  1. 從本地數據庫中恢復當前節點已同步的區塊
  2. 同步區塊

其中,同步區塊方面有3個核心步驟:

  • 從其他節點同步,執行dpos.SyncHeight(Height)
  • 當區塊同步失敗,嘗試3次,3次之後判斷是否超級節點
  • 如果當前節點同步失敗,且是超級節點,則通過投票結果來同步區塊,執行dpos.startDelegateThread()進入打包區塊的流程

主網啓動流程圖如下:
在這裏插入圖片描述

數據同步與恢復

一般是剛啓動的節點從其他節點同步數據。

  • 第一步:GetRound()獲取當前打包節點信息
  • 第二步:循環向各個節點發送請求,執行getBlockHeader()獲取區塊數據
  • 第三步:再請求該區塊的投票結果,執行getVotes()獲取投票結果
  • 第四步:執行Validate()校驗投票結果的完整性和真實性,不合法重複第二步
  • 第五步:校驗合法後,執行getBlockEvents()獲取交易明細數據,再執行ValidateNextBlock()驗證交易明細數據和區塊數據是否合法,不合法重複第二步
  • 第六步:以上都合法,執行RecieveVoteResult()寫入區塊

流程圖如下:
在這裏插入圖片描述

後續會對RecieveVoteResult()單獨分析,該函數集成的功能較多,包括:驗證投票、管理區塊、改變狀態、記錄打包間隔、寫區塊等功能。

本地數據恢復流程,一圖可以描述,不再多說。
在這裏插入圖片描述

結語

這篇文章的內容已經足夠長且多。如果反響不錯,會繼續深入share一些有價值的點。

真正理解,還需要多多閱讀源碼。
在這裏插入圖片描述

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