談到區塊鏈的儲存,我們很容易聯想到它的鏈式儲存結構,然而區塊鏈從比特幣發展到今日當紅的EOS,技術形態已經演化了10年之久。目前的EOS的儲存除了確認結構的鏈式儲存以外,在狀態儲存方面有了很大的進步,尤其是引入了MongoDB plugin以後,可以將功能有限的狀態庫搭上大資料的班車。本文將全面介紹EOS的儲存技術。
EOS 儲存,Merkle Tree,mongodb,chainbase,源碼學習,context_free_actions
EOS的鏈式儲存結構
EOS的區塊資料結構如下:
field |
explanation |
timestamp |
時間戳記 |
producer |
生產者 |
confirmed |
生產者確認數 |
previous |
鏈式結構前一個區塊的id |
transaction_mroot |
交易默克爾樹根 |
action_mroot |
動作默克爾樹根 |
schedule_version |
生產者版本排序號 |
new_producers |
下一個生產者 |
header_extensions |
區塊頭擴充欄位 |
producer_signature |
區塊簽名,由生產者簽名 |
transactions |
塊打包交易內容,是數組結構,可以多個 |
block_extensions |
區塊擴充欄位 |
id |
當前塊id |
block_num |
當前塊高度 |
ref_block_prefix |
引用區塊的區塊頭 |
Merkle Tree
默克爾樹的演化路線是 Hash => Hash Tree => Merkle Tree ,他們都是為解決資料一致性而存在的,具體的含義如下:
- Hash 是我們都熟知的技術了,它可以為一個檔案或其他資料產生一個Hash值,我們在下載一個檔案時,通常會在下載頁面看到這個檔案的Hash值以及該Hash值的演算法,下載完畢以後,我們可以在本地對整個檔案進行同樣的Hash演算法得到Hash值,然後與網頁上的Hash值進行對比,如果相同,則說明檔案完整,是網頁上的源檔案,如果不匹配則說明檔案損壞,被修改或者不完整。
- 仍舊是檔案完整性的校正需求,當這個檔案特別大的時候,對這個檔案進行Hash演算法是能耗巨大的,所以可以將檔案切割成很多的小塊,每一個小塊都有一個Hash,然後將所有小塊的Hash值拼在一起再次進行Hash演算法得到的就是Root Hash。這樣一來,我們在下載大檔案的時候,會先下載一個包含Root Hash的Hash list,通過校正Root Hash可以確定Hash list的正確性,確定Hash list正確以後,再逐個下載小塊檔案並逐一驗證Hash,當發現某個小塊Hash不匹配的時候,就可以單獨重新下載該小塊即可,而不必重新下載全部。Hash list的結構實際上是一個Root Hash 為根,小塊Hashs為葉子節點的樹高為2的Hash Tree。
- Merkle Tree實際上是對Hash List的最佳化,它極大的提高了效能。它的結構是一個二叉樹(也可以是多叉樹,效能最佳化的關鍵點是它的高度是大於等於2的),每個節點最多隻有兩個子節點,只有分葉節點是根據小塊檔案做的Hash,每兩個相鄰的分葉節點的父節點是由這兩個Hash做的父Hash,如果分葉節點的總數是單數,則會剩餘一個,逐級而上,最終會有一個的根節點,這個根節點就是Merkle Root。這樣以來,我們在下載大檔案的時候,會首先下載一個Merkle Tree,從最左下分葉節點進行校正,逐級而上,將整個Merkle Tree校正完畢。這裡面不同於上面Hash Tree的是,只要最左下相鄰的兩個分葉節點的Hash值與他們的父節點的Hash通過了匹配,則可以立即開始下載這兩個分葉節點對應的檔案塊,並行地,再校正其他分葉節點,這就提高了效能,不必校正完整的Merkle Tree之後再下載檔案。
Merkle Tree 與 區塊鏈
上面的區塊資料結構中包含了兩個與Merkle Tree相關的欄位:
- transaction_mroot,一個區塊中的transactions欄位可以包含多筆交易,區塊中的transaction_mroot是所有該區塊內打包的交易的Merkle Root,可以用來校正其中的每筆交易的正確性。如果該區塊中不包含任何交易,則該欄位的值為0000000000000000000000000000000000000000000000000000000000000000。節點同步資料的時候,會先將交易的Merkle Tree下載並通過Merkle Root來校正,而不是將所有的交易主體全部下載下來,這樣可以節省輕節點的資料量。
- action_mroot,建立一個基於所有分發的action的根,在區塊內接收交易時進行評估。用在輕用戶端的校正,功能同上。
action_mroot是始終有值的,哪怕transaction_mroot是0,這是因為出塊本身也是一個action動作onblock,這個動作調用的是system合約的onblock函數。TODO:源碼分析
/*** At the start of each block we notify the system contract with a transaction that passes in* the block header of the prior block (which is currently our head block)*/signed_transaction get_on_block_transaction(){ action on_block_act; on_block_act.account = config::system_account_name; on_block_act.name = N(onblock); on_block_act.authorization = vector<permission_level>{{config::system_account_name, config::active_name}}; on_block_act.data = fc::raw::pack(self.head_block_header()); signed_transaction trx; trx.actions.emplace_back(std::move(on_block_act)); trx.set_reference_block(self.head_block_id()); trx.expiration = self.pending_block_time() + fc::microseconds(999'999); // Round up to nearest second to avoid appearing expired return trx;}
context_free_actions
通過對eosio.null賬戶的nouce動作,可以將無簽名的資料打包進入context_free_action欄位,結果區塊資訊如下:
evsward@evsward-TM1701:~/work/src/github.com/eos/tutorials/bios-boot-tutorial$ cleos --wallet-url http://127.0.0.1:6666 --url http://127.0.0.1:8000 get block 440{ "timestamp": "2018-08-14T08:47:09.000", "producer": "eosio", "confirmed": 0, "previous": "000001b760e4a6610d122c5aa5d855aa49e29f3052ac3e40b9e1ef78e0f1fd02", "transaction_mroot": "32cb43abd7863f162f4d8f3ab9026623ea99d3f8261d2c8b4d8bf920ab97e3d1", "action_mroot": "09afeaf40d6988a14e9e92817d2ccf4023b280075c99f13782a6535ccc58cbb0", "schedule_version": 0, "new_producers": null, "header_extensions": [], "producer_signature": "SIG_K1_K2eFDzbxCg3hmQzpzPuLYmiesrciPmTHdeNsQDyFgcHUMFeMC3PntXTqiup5VuNmyb7qmH18FBdMuNKsc7jgCm1TSPFbaj", "transactions": [{ "status": "executed", "cpu_usage_us": 290, "net_usage_words": 16, "trx": { "id": "d74843749d1e255f13572b7a3b95af9ddd6df23d1d0ad19d88e1496091d4be2b", "signatures": [ "SIG_K1_KVzwg3QRH6ZmempNsvAxpPQa42hF4tDpV5cqwqo7EY4oSU7NMrEFwG7gdSDCnUHHhmH1EwtVAmV1z9bqtTvvQNSXiSgaWG" ], "compression": "none", "packed_context_free_data": "", "context_free_data": [], "packed_trx": "8497725bb601973ea96f0000000100408c7a02ea3055000000000085269d000706686168616861010082c95865ea3055000000000000806b010082c95865ea305500000000a8ed3232080000000000d08cf200", "transaction": { "expiration": "2018-08-14T08:49:08", "ref_block_num": 438, "ref_block_prefix": 1873362583, "max_net_usage_words": 0, "max_cpu_usage_ms": 0, "delay_sec": 0, "context_free_actions": [{ "account": "eosio.null", "name": "nonce", "authorization": [], "data": "06686168616861" } ], "actions": [{ "account": "eosiotesta1", "name": "hi", "authorization": [{ "actor": "eosiotesta1", "permission": "active" } ], "data": { "user": "yeah" }, "hex_data": "0000000000d08cf2" } ], "transaction_extensions": [] } } } ], "block_extensions": [], "id": "000001b8d299602b289a9194bd698476c5d39c5ad88235460908e9d43d04edc8", "block_num": 440, "ref_block_prefix": 2492570152}
正常的actions的內容是hi智能合約的調用,而context_free_action中包含了無簽名的data資料,是已做數字摘要後的形態。源碼中的操作:
// lets also push a context free action, the multi chain test will then also include a context free action("context_free_actions", fc::variants({ fc::mutable_variant_object() ("account", name(config::null_account_name)) ("name", "nonce") ("data", fc::raw::pack(v)) }) );
EOS的StateDB
我們來設想一個情境:
A賬戶轉賬給B賬戶100個SYS,如何查看A賬戶的餘額?
對於不知道以上動作何時發生的我們來講,我們要如何做呢:
- 首先是從頭掃描區塊內的交易,交易內的action,直到找到A賬戶被建立的action所對應的區塊號。
- 從這個區塊號開始繼續掃描,要將所有A賬戶的轉賬,包括收入和支出的所有action記錄下來並統計。
- 算出A的當前餘額。
以上步驟很容易出錯且繁瑣,每一次的餘額查詢都要重複這些操作實在是毫無意義,因此StateDB就誕生了,這個庫顧名思義就是用來儲存狀態資料的,如果有了StateDB,上面的情境的解決辦法就是:
- 從A賬戶被第一次收入SYS開始,為A賬戶在StateDB中建立一個table,儲存A賬戶的餘額,每當A賬戶發生轉賬的action,都會同步更新StateDB中相關table中A賬戶的餘額的值,當我們需要知道A賬戶的餘額時,我們可以直接尋找這個餘額state即可。
測試案例
這裡為大家提供一個測試方法,也是我的命令history:
cleos create keycleos wallet import 5JA9oDotJHoKnjUV6NrAMx4g5gWTCVCRLybTnG1XVU3EKGZZeNYcleos wallet import --private-key 5JA9oDotJHoKnjUV6NrAMx4g5gWTCVCRLybTnG1XVU3EKGZZeNYcleos wallet keyscleos wallet import --private-key 5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3cleos wallet keyscleos create account eosio usertesta1 EOS761KfjhYy3FSypZGG5hePrR8K2wzmw75JCDXgypKt2DLXZoZPWcleos create account eosio eosio.token EOS761KfjhYy3FSypZGG5hePrR8K2wzmw75JCDXgypKt2DLXZoZPWcleos set contract eosio.token work/src/github.com/eos/build/contracts/eosio.token/cleos push action eosio.token create '["eosio","100000000.0000 SYS"]'cleos push action eosio.token issue cleos push action eosio.token issue '["usertesta1","10000000.0000 SYS"]' -p eosio.tokencleos push action eosio.token issue '["usertesta1","10000000.0000 SYS"]' -p eosiocleos get currency balance eosio.token usertesta1cleos get table eosio.token usertesta1 accountscleos get table eosio.token eosio accounts
可以看到當我想獲得usertesta1賬戶的餘額時,是通過查詢StateDB的table來擷取的,而不是最開始的那種掃塊的笨方法。
鏈式儲存和StateDB儲存的區別
- 鏈式儲存,儲存的是固定結構的資料:Block=> Block Header/ transactions=>actions,一個action的結構例子:
{ "account": "eosiotesta1", "name": "hi", "authorization": [{ "actor": "eosiotesta1", "permission": "active" }], "data": { "user": "yeah" }, "hex_data": "0000000000d08cf2"}
這個例子中,我們調用了hello合約的hi函數,data傳入的格式是hi函數中自訂的,所以在鏈式儲存中,留給我們發揮的空間也即在此。
- StateDB,儲存的是一個最終要記錄的狀態,這個狀態資料必須是有意義的,是有人關心的,無關緊要的資料請不要放在StateDB中去,所以StateDB是可以增刪改查的,就像一個普通資料庫那樣,在合約中通過multi_index來操作,具體請參照文章EOS技術研究:合約與資料庫互動
很多人搞不明白為什麼區塊鏈不可篡改,卻在StateDB中好像可以修改還能刪除?
其實不是這樣的,鏈式儲存的內容會將所有的動作action全部記錄下來,是所有的過程資料,是流水帳,中繼資料,這些資料一旦上鏈是不可修改,不可刪除的。而StateDB只是為了儲存一個狀態資訊,這個狀態資訊的修改與刪除並不影響區塊鏈的不可篡改的特性。
目前StateDB的主流實現方式是將它放在記憶體中,而當有些人對StateDB的認識有偏差造成濫用的時候,會引發記憶體過載,因此一方面我們要非常清楚的理解StateDB的含義,一方面EOSIO協助我們提供了一個mongodb-plugin外掛程式來同步StateDB資料。
mongodb安裝
- 下載tgz安裝包
- 解壓安裝到/usr/local/bin(或者其他某路徑)
- sudo mkdir /data/db
普通模式
服務模式
我們也可以使用ubuntu系統的服務模式。
曾經我們要定義一個系統啟動時自啟動服務的方式是在/etc/init.d 目錄下寫一個指令碼來執行,現在在ubuntu的服務模式下,我們可以丟棄那種方式,服務模式的命令是service,而現在的ubuntu系統推崇使用的systemctl命令,他倆的使用方法的區別就在於參數的順序。
[Unit]Description=High-performance, schema-free document-oriented databaseAfter=network.target [Service]User=mongodbExecStart=['mongod' command location] --quiet --config /etc/mongod.conf [Install]WantedBy=multi-user.target
尋找服務狀態
systemctl list-unit-files
查詢mongodb服務的啟用狀態
systemctl is-enabled mongodb
啟用系統自啟動服務
sudo systemctl enable mongodb
啟動mongodb服務
sudo systemctl start mongodb
查詢mongodb服務狀態
sudo systemctl status mongodb
停止mongodb服務
sudo systemctl stop mongodb
偵錯模式
IDE選擇CLion,EOS源碼下載最新的,保證本地可以使用指令碼編譯通過,安裝了相關依賴包,然後在CLion中匯入EOS,CLion會自動識別CMakeList.txt檔案產生makefile檔案並make編譯執行。編譯時間可能會遇到錯誤,一般來講要麼是環境依賴沒有配置好,要麼就是CMakeList.txt要有修改,例如mongodb-plugin匯入時要在總開關配置上開啟。
set(BUILD_MONGO_DB_PLUGIN "true")
全部編譯成功以後,會自動識別出可以debug的target,與EOS中配備CMakeList.txt的模組一一對應。
安裝Mongo Explorer外掛程式
上面我們介紹了MongoDB的安裝方法,以及啟動nodeos時的配置方法(除了上文提到的總開關,當然要在config.ini檔案末尾設定上plugin = eosio::mongo_db_plugin,這部分內容演練多次,這裡不再贅述。)鏈啟動開始出塊以後,會同步到mongodb中去(注意要預先啟動mongod守護進程,可以理解為服務端),通過mongo命令接入可使用mongo命令查詢資料,但這樣很不方便。可以在CLion中安裝mongo-plugin,配置好效果如下: