【劉文彬】區塊鏈 + 大資料:EOS儲存

圓方圓區塊鏈發表於2018-12-07

原文連結:醒者呆的部落格園,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,配置好效果如下:

【劉文彬】區塊鏈 + 大資料:EOS儲存


相關文章和視訊推薦

【許曉笛】EOS 資料庫與持久化 API —— 實戰

圓方圓學院彙集大批區塊鏈名師,打造精品的區塊鏈技術課程。 在各大平臺都長期有優質免費公開課,歡迎報名收看。

公開課地址:ke.qq.com/course/3451…

相關文章