原文連接 https://github.com/liyue201/b...node
總所周知,比特幣全節點的實現目前有兩個版本,一個是中本聰c++寫的原版bitcoin core,一個是go語言寫的btcd。這兩個版本從功能上並無多大差別,都實現了比特幣協議。從理論上講,比特幣網絡中的每一個全節點只要遵循相同的比特幣協議,至於用什麼語言實現,用什麼方式存儲都是無所謂的。bitcoin core和btcd都沒有實現通用的錢包服務,這裏說的錢包服務指的是一個能夠給錢包app提供所數據的服務,私鑰保存在錢包app中,用戶交易在錢包app中籤名,再將簽名數據發給錢包服務,再由錢包服務轉發到全節點上鍊,如圖:c++
所幸的是全節點提供了一些JSON RPC接口,經過這些接口能夠讀取區塊數據和發起轉帳交易。可是能讀到的數據仍是太原始了,就連獲取某個地址的餘額都作不到。由於比特幣使用的是UTXO帳號體系,並無一個統一的地方存儲地址餘額,只有UTXO。而UTXO是分散在每一個區塊中的,要想知道某個地址的UTXO,須要從第一個區塊開始遍歷。有些人說這樣設計能夠更好的併發執行,這個鄙人真的不敢苟同。說能夠更好的溯源卻是真的,由於每一筆交易均可以追溯到上一筆交易。git
好了,廢話很少說,咱們看一下一個錢包服務應該具有什麼樣的功能。github
第(1)(2)點其實是一個問題,知道UTXO天然就知道餘額了。第(3)(5)能夠直接調全節點接口。因此錢包服務的難點只剩下(2)(4)這兩個功能。錢包服務要作的就是從全節點從第一個區塊開始遍歷讀取全部的交易記錄,並推導出每一個地址當前區塊高度的UTXO,再將這個信息保存到數據庫中方便錢包使用。web
bitcoin core 雖然是原版的比特幣全節點,可是他只提供了http接口,沒有websocket接口,不能實時推送未上鍊的交易。一個體驗好的錢包不只要獲取已經上鍊的交易記錄,也要知道未上鍊的交易。btcd不只提供了http接口,還有websocket,因此btcd應該是更好的選擇。sql
到目前爲止(2018-12-26)比特幣的交易量是3.6億,由於一筆交易涉及到多個輸入和多個輸出地址(若是你研究過區塊鏈瀏覽器上的數據,會發現一筆交易幾千個輸入和幾千個輸出都是很正常的,這樣能夠省礦工費,多是交易所轉帳),解析出來後大約是16.6億條(這個我本身跑的數據)。如果用傳統的關係型數據庫確定扛不住的,因此通常人都會往nosql或者列式數據庫方向考慮,好比用mongo或cassandra等。雖然可以解決問題,可是面對這麼大的數據,這類型的數據庫都須要部署分片集羣。對於一個小創業公司來講,若是用戶不是不少,從運維成本上來看,是否是都點殺雞用牛刀大感受呢。有沒有經濟實惠的方案?固然有,好比leveldb和rocksdb這類kv嵌入式數據庫,只須要單個機器就能夠知足要求。在幾億數據量狀況下,leveldb在普通硬盤上讀寫速度能夠達到幾萬每秒,使用ssd硬盤更快,並且內存佔用極少。固然使用leveldb或rocksdb,也有其缺點,就是單機數據安全的問題,若是硬盤壞了怎麼辦。剛好咱們這個業務對數據的安全性要求並不高,由於全部數據都是從全節點來的,要是硬盤壞了,大不了從新跑一遍數據。rocksdb是facebook的開源產品,聽說是對leveldb作了優化,性能更強。rocksdb是c++寫的,而leveldb除了c++版本以外,還有go語言版本,比特幣全節點btcd和以太坊全節點geth都是用的go版本的leveldb。最終我選擇了leveldb,由於個人服務是用go寫的,雖然go也能夠經過cgo調用C++,可是代碼寫起來沒有純go那麼舒服。數據庫
leveldb是kv數據庫,沒有表的概念,因此數據結構的設計很是關鍵。好比地址的歷史交易記錄,在關係型數據庫中能夠建一個以地址爲索引的歷史交易記錄表。而kv數據庫則沒有索引的概念。好在level的key是有序的,能夠根據key的順序讀取內容。根據這個特性,咱們能夠這樣設計索引,每一個地址的交易是一組key,這組key在數據庫的全部key中是緊挨着的。這樣只要知道某個地址的第一個索引key就能夠按順序讀到這個地址的其餘交易。由於一個交易涉及到多個輸入地址和輸出地址因此須要索引跟數據分開,對於utxo只關聯到一個地址,就不須要單獨設計索引了,索引跟數據能夠放在一塊兒。json
數據結構設計好了,該怎麼存。這個就是序列化的問題了,能夠是json,或者go自帶的gob,也能夠本身寫。最後從編碼難度,執行效率和壓縮率等方面綜合考慮採用了protobuf。這是個人proto文件瀏覽器
syntax = "proto3"; package models; //key: utxo-{addr}-{Transaction.id}-{Transaction.vout} message Utxo { string txId = 1; uint32 vout = 2; int64 amount = 3; string scriptPubKey = 4; } //context //key: context message Context { int64 blockHeight = 1; // 當前處理的區塊高度 string blockHash = 2; // 當前處理的區塊hash int32 blockTotalTx = 3; // 當前區塊的交易數 int32 BlockProcessedTx = 4; // 當前區塊已經處理的交易數 int64 totalTx = 5; // 總共的交易數 int64 totalTxNode = 6; // } message TxInput { string txId = 1; //上一筆交易id uint32 vout = 2; //上一筆交易輸出的idx string addr = 3; //經過查詢得到 int64 amount = 4; //經過查詢得到 } message TxOutput { string addr = 1; int64 amount = 2; uint32 vout = 3; string scriptPubKey = 4; } //key: tx-{Transaction.txId} message Transaction { int64 id = 1; //自增di string txId = 2; int64 blockTime = 3; string blockHash = 4; int64 blockHeight = 5; repeated TxInput inputs = 6; repeated TxOutput outputs = 7; } //key: txnode-{addr}-{TxNode.id} message TxNode{ int64 id = 1; //自增di string txId = 2; uint32 type = 3; //1-intput 2-output 3-intput|output }
這個項目的難點不在編碼而是技術選型,固然還有調試。代碼寫完後通過漫長的同步,大概半個月才同步完成。 只有同步完成才能進行完整的功能測試,若果有數據的bug還得從頭開始同步。須要注意的是這裏只是設計了已經確認的區塊,對於未確認的區塊和內存池的交易要單獨考慮。安全