前言
筆者做了一段時間的區塊鏈底層開發,深知架構設計的重要性。對於高手來說,沒有的輪子是可以自己造的,造個大規模消息/任務隊列都只是想不想寫的事情。但在企業中開發,追求的是穩定、性能、成本等等,所以通常希望使用開源組件,二次開發。
解析過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()即可啓動。
主要有兩步:
- 從本地數據庫中恢復當前節點已同步的區塊
- 同步區塊
其中,同步區塊方面有3個核心步驟:
- 從其他節點同步,執行dpos.SyncHeight(Height)
- 當區塊同步失敗,嘗試3次,3次之後判斷是否超級節點
- 如果當前節點同步失敗,且是超級節點,則通過投票結果來同步區塊,執行dpos.startDelegateThread()進入打包區塊的流程
主網啓動流程圖如下:
數據同步與恢復
一般是剛啓動的節點從其他節點同步數據。
- 第一步:GetRound()獲取當前打包節點信息
- 第二步:循環向各個節點發送請求,執行getBlockHeader()獲取區塊數據
- 第三步:再請求該區塊的投票結果,執行getVotes()獲取投票結果
- 第四步:執行Validate()校驗投票結果的完整性和真實性,不合法重複第二步
- 第五步:校驗合法後,執行getBlockEvents()獲取交易明細數據,再執行ValidateNextBlock()驗證交易明細數據和區塊數據是否合法,不合法重複第二步
- 第六步:以上都合法,執行RecieveVoteResult()寫入區塊
流程圖如下:
後續會對RecieveVoteResult()單獨分析,該函數集成的功能較多,包括:驗證投票、管理區塊、改變狀態、記錄打包間隔、寫區塊等功能。
本地數據恢復流程,一圖可以描述,不再多說。
結語
這篇文章的內容已經足夠長且多。如果反響不錯,會繼續深入share一些有價值的點。
真正理解,還需要多多閱讀源碼。