原文連結:醒者呆的部落格園,www.cnblogs.com/Evsward/p/s…
談到區塊鏈的儲存,我們很容易聯想到它的鏈式儲存結構,然而區塊鏈從比特幣發展到今日當紅的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 key
cleos wallet import 5JA9oDotJHoKnjUV6NrAMx4g5gWTCVCRLybTnG1XVU3EKGZZeNY
cleos wallet import --private-key 5JA9oDotJHoKnjUV6NrAMx4g5gWTCVCRLybTnG1XVU3EKGZZeNY
cleos wallet keys
cleos wallet import --private-key 5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3
cleos wallet keys
cleos create account eosio usertesta1 EOS761KfjhYy3FSypZGG5hePrR8K2wzmw75JCDXgypKt2DLXZoZPW
cleos create account eosio eosio.token EOS761KfjhYy3FSypZGG5hePrR8K2wzmw75JCDXgypKt2DLXZoZPW
cleos 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.token
cleos push action eosio.token issue '["usertesta1","10000000.0000 SYS"]' -p eosio
cleos get currency balance eosio.token usertesta1
cleos get table eosio.token usertesta1 accounts
cleos 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
普通模式
- sudo mongod
- mongo
服務模式
我們也可以使用ubuntu系統的服務模式。
曾經我們要定義一個系統啟動時自啟動服務的方式是在/etc/init.d 目錄下寫一個指令碼來執行,現在在ubuntu的服務模式下,我們可以丟棄那種方式,服務模式的命令是service,而現在的ubuntu系統推崇使用的systemctl命令,他倆的使用方法的區別就在於引數的順序。
複製程式碼
-
定義一個配置檔案mongod.conf
-
定義一個服務檔案,放置在/etc/systemd/system/
sudo vi /etc/systemd/system/mongodb.service 複製程式碼
[Unit]
Description=High-performance, schema-free document-oriented database
After=network.target
[Service]
User=mongodb
ExecStart=['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,配置好效果如下:
相關文章和視訊推薦
圓方圓學院彙集大批區塊鏈名師,打造精品的區塊鏈技術課程。 在各大平臺都長期有優質免費公開課,歡迎報名收看。
公開課地址:ke.qq.com/course/3451…