CKB 指令碼程式設計簡介第二彈:指令碼基礎

買賣虛擬貨幣
Xuejie 是 CKB-VM 的核心開發者,他在自己的部落格「Less is More」中,創作了一系列介紹 CKB 指令碼程式設計的文章,用於補充白皮書中編寫 CKB 指令碼所需的所有缺失的細節實現。本文是該系列的第二篇,詳細講解了如何將指令碼程式碼部署到 CKB 網路上,快來檢視吧 在上一篇文章裡,我介紹了當前 CKB 的驗證模型。這一篇會更加有趣一點,我們要向大家展示如何將指令碼程式碼真正部署到 CKB 網路上去。我希望在你看完本文後,你可以有能力自行去探索 CKB 的世界並按照你自己的意願去編寫新的指令碼程式碼。但是請注意,儘管我相信目前的 CKB 的程式設計模型已經相對穩定了,但是開發仍在進行中,因此未來還可能會有一些變化。我將盡力確保本文始終處於最新的狀態,但是如果在過程到任何疑惑,本文以此版本下的 CKB 作為依據:https://github.com/nervosnetwork/ckb/commit/80b51a9851b5d535625c5d144e1accd38c32876bTips:這是一篇很長的文章,因為我想為下週更有趣的話題提供充足的內容。所以如果你時間不太夠,你不必馬上讀完。我會試著把它分成幾個獨立的部分,這樣你就可以一次讀其中一部分。語法在開始之前,我們先來區分兩個術語:指令碼(Script)和指令碼程式碼(Script Code)。
在本文以及整個系列文章內,我們將區分指令碼和指令碼程式碼。指令碼程式碼實際上是指你編寫和編譯並在 CKB 上執行的程式。而指令碼,實際上是指 CKB 中使用的指令碼資料結構,它會比指令碼程式碼稍微多一點點:pub struct Script {    pub args: Vec<Bytes>,    pub code_hash: H256,    pub hash_type: ScriptHashType,}
我們目前可以先忽略 hash_type,之後的文章再來解釋什是 hash_type 以及它有什麼有趣的用法。在這篇文章的後面,我們會說明 code_hash 實際上是用來標識指令碼程式碼的,所以目前我們可以只把它當成指令碼程式碼。那指令碼還包括什麼呢?指令碼還包括 args 這個部分,它是用來區分指令碼和指令碼程式碼的。args 在這裡可以用來給一個 CKB 指令碼提供額外的引數,比如:雖然大家可能都會使用相同的預設的 Lock Script Code,但是每個人可能都有自己的 Pubkey Hash,args 就是用來儲存 Pubkey Hash 的位置。這樣,每一個CKB 的使用者都可以擁有不同的 Lock Script ,但是卻可以共用同樣的 Lock Script Code。請注意,在大多數情況下,指令碼和指令碼程式碼是可以互換使用的,但是如果你在某些地方感到了困惑,那麼你可能有必要考慮一下兩者間的區別。一個最小的 CKB 指令碼程式碼可能你之前聽說過,CKB-VM 是基於開源的 RISC-V 指令集編寫的。但這到底意味著什麼呢?用我自己的話來說,這意味著我們(在某種程度上)在 CKB 中嵌入了一臺真正的微型計算機,而不是一臺虛擬機器。一臺真正的計算機的好處是,你可以用任何語言編寫任何你想寫的邏輯。在這裡,我們會先展示幾個用 C 語言編寫的例子,以保持簡單性(我是說工具鏈中的簡單性,而不是語言),之後我們還會切換到基於 JavaScript 的指令碼程式碼,並希望在本系列中展示更多的語言。記住,在 CKB 上有無限的可能!正如我們提到的,CKB-VM 更像是一臺真正的微型計算機。CKB 的程式碼指令碼看起來也更像是我們在電腦上跑的一個常見的 Unix 風格的可執行程式。int main(int argc, char* argv[])
{  return 0;}當你的程式碼透過 C 編譯器編譯時,它將成為可以在 CKB 上執行的指令碼程式碼。換句話說,CKB 只是採用了普通的舊式 Unix 風格的可執行程式 (但使用的是 RISC-V 體系結構,而不是流行的 x86 體系結構),並在虛擬機器環境中執行它。如果程式的返回程式碼是 0 ,我們認為指令碼成功了,所有非零的返回程式碼都將被視為失敗指令碼。在上面的例子中,我們展示了一個總是成功的指令碼程式碼。因為返回程式碼總是 0。但是請不要使用這個作為您的 Lock Script Code ,否則您的 Token 可能會被任何人拿走。但是顯然,上面的例子並不有趣,這裡我們從一個有趣的想法開始:我個人不是很喜歡胡蘿蔔。我知道胡蘿蔔從營養的角度來看是很好的,但我還是不喜歡它的味道。如果現在我想設定一個規則,比如我想讓我在 CKB 上的 Cell 裡面都沒有以 carrot 開頭的資料,這該怎麼實現?讓我們編寫一個指令碼程式碼來試試。
為了確保沒有一個 Cell 在 Cell Data 中包含 carrot,我們首先需要一種方法來讀取指令碼中的 Cell Data。CKB 提供了 syscalls 來幫助解決這個問題。為了確保 CKB 指令碼的安全性,每個指令碼都必須在與執行 CKB 的主計算機完全分離的隔離環境中執行。這樣它就不能訪問它不需要的資料,比如你的私鑰或密碼。然而,想要讓指令碼有用,必須要訪問特定的資料,比如指令碼保護的 cell 或指令碼驗證。CKB 提供了 syscalls 來確保這一點,syscalls 是在 RISC-V 的標準中定義的,它們提供了訪問環境中某些資源的方法。在正常情況下,這裡的環境指的是作業系統,但是在 CKB VM 中,環境指的是實際的 CKB 程序。使用 syscalls, CKB指令碼可以訪問包含自身的整個資料,包括輸入(Inputs)、輸出(Outpus)、見證(Witnesses)和 Deps。更棒的是,我們已經將 syscalls 封裝在了一個易於使用的標頭檔案中,非常歡迎您在這裡檢視這個檔案,瞭解如何實現 syscalls:https://github.com/nervosnetwork/ckb-system-scripts/blob/66d7da8ec72dffaa7e9c55904833951eca2422a9/c/ckb_syscalls.h最重要的是,您可以只獲取這個標頭檔案並使用包裝函式來建立您想要的系統呼叫。現在有了 syscalls,我們可以從禁止使用 carrot 的指令碼開始:#include <memory.h>#include "ckb_syscalls.h"
int main(int argc, char* argv[]) {  int ret;  size_t index = 0;  volatile uint64_t len = 0; /* (1) */  unsigned char buffer[6];  while (1) {
    len = 6;    memset(buffer, 0, 6);    ret = ckb_load_cell_by_field(buffer, &len, 0, index, CKB_SOURCE_OUTPUT,                                 CKB_CELL_FIELD_DATA); /* (2) */    if (ret == CKB_INDEX_OUT_OF_BOUND) {               /* (3) */      break;
    }    if (memcmp(buffer, "carrot", 6) == 0) {      return -1;    }    index++;  }
  return 0;}以下幾點需要解釋一下:1. 由於 C 語言的怪癖,len 欄位需要標記為 volatile。我們會同時使用它作為輸入和輸出引數,CKB-VM 只能在它還儲存在記憶體中時,才可以把它設定為輸出引數。而 volatile 可以確保 C 編譯器將它儲存為基於 RISC-V 記憶體的變數。2. 在使用 syscall 時,我們需要提供以下功能:一個緩衝區來儲存 syscall 提供的資料;一個 len 欄位,來表示系統呼叫返回的緩衝區長度和可用資料長度;一個輸入資料緩衝區中的偏移量,以及幾個我們在交易中需要獲取的確切欄位的引數。詳情請參閱我們的 RFC:https://github.com/nervosnetwork/rfcs/blob/master/rfcs/0009-vm-syscalls/0009-vm-syscalls.md
3. 為了保證最大的靈活性,CKB 使用系統呼叫的返回值來表示資料抓取狀態:0 (or CKB_SUCCESS) 意味著成功,1(or CKB_INDEX_OUT_OF_BOUND)意味著您已經透過一種方式獲取了所有的索引,2 (or CKB_ITEM_MISSING) 意味著不存在一個實體,比如從一個不包含該 Type 指令碼的 Cell 中獲取該 Type 的指令碼。概括一下,這個指令碼將迴圈遍歷交易中的所有輸出 Cell,載入每個 Cell Data 的前 6 個位元組,並測試這些位元組是否和 carrot 匹配。如果找到匹配,指令碼將返回 -1,表示錯誤狀態;如果沒有找到匹配,指令碼將返回 0 退出,表示執行成功。為了執行該迴圈,該指令碼將儲存一個 index 變數,在每次迴圈迭代中,它將試圖讓 Syscall 獲取 Cell 中目前採用的 index 值,如果 Syscall 返回 CKB_INDEX_OUT_OF_BOUND,這意味著指令碼已經遍歷所有的 Cell,之後會退出迴圈;否則,迴圈將繼續,每測試 Cell Data 一次,index 變數就會遞增一次。這是第一個有用的 CKB 指令碼程式碼!在下一節中,我們將展示如何將其部署到 CKB 中並執行它。將指令碼部署到 CKB 上首先,我們需要編譯上面寫的關於胡蘿蔔的原始碼。由於 GCC 已經提供了 RISC-V 的支援,您可以使用官方的 GCC 來建立指令碼程式碼。或者你也可以使用我們準備的 Docker 映象(https://hub.docker.com/r/nervos/ckb-riscv-gnu-toolchain)來避免編譯 GCC 的麻煩:
$ lscarrot.c  ckb_consts.h  ckb_syscalls.h$ sudo docker run --rm -it -v `pwd`:/code nervos/ckb-riscv-gnu-toolchain:xenial bashadmin@chaindaily:/# cd /codeadmin@chaindaily:/code# riscv64-unknown-elf-gcc -Os carrot.c -o carrotadmin@chaindaily:/code# exit
exit$ lscarrot*  carrot.c  ckb_consts.h  ckb_syscalls.h就像上面展示的這樣,CKB 可以直接使用 GCC 編譯的可執行檔案作為鏈上的指令碼,無需進一步處理。我們現在可以在鏈上部署它了。注意,我將使用 CKB 的 Ruby SDK,因為我曾經是一名 Ruby 程式設計師,所以 Ruby 對我來說是最習慣的(但不一定是最好的)。如何設定請參考官方 Readme 檔案:https://github.com/nervosnetwork/ckb-sdk-ruby/blob/develop/README.md要將指令碼部署到 CKB,我們只需建立一個新的 Cell,把指令碼程式碼設為 Cell Data 部分:
pry(main)> data = File.read("carrot")pry(main)> data.bytesize=> 6864pry(main)> carrot_tx_hash = wallet.send_capacity(wallet.address, CKB::Utils.byte_to_shannon(8000), CKB::Utils.bin_to_hex(data))在這裡,我首先要透過向自己傳送 Token 來建立一個容量足夠的新的 Cell。現在我們可以建立包含胡蘿蔔指令碼程式碼的指令碼:pry(main)> carrot_data_hash = CKB::Blake2b.hexdigest(data)
pry(main)> carrot_type_script = CKB::Types::Script.new(code_hash: carrot_data_hash, args: [])回憶一下指令碼資料結構:pub struct Script {    pub args: Vec<Bytes>,    pub code_hash: H256,    pub hash_type: ScriptHashType,
}可以看到,我沒有直接將指令碼程式碼嵌入到指令碼資料結構中,而是隻包含了程式碼的雜湊,這是實際指令碼二進位制程式碼的 Blake2b 雜湊。由於胡蘿蔔指令碼不使用引數,我們可以對 args 部分使用空陣列。注意,這裡仍然忽略了 hash_type,我們將在後面的文章中透過另一種方式討論指定程式碼雜湊。現在,讓我們儘量保持簡潔。要執行胡蘿蔔指令碼,我們需要建立一個新的交易,並將胡蘿蔔 Type 指令碼設定為其中一個輸出 Cell 的 Type 指令碼:pry(main)> tx = wallet.generate_tx(wallet2.address, CKB::Utils.byte_to_shannon(200))pry(main)> tx.outputs[0].instance_variable_set(:@type, carrot_type_script.dup)
我們還需要進行一個步驟:為了讓 CKB 可以找到胡蘿蔔指令碼,我們需要在一筆交易的 Deps 中引用包含胡蘿蔔指令碼的 Cell:pry(main)> carrot_out_point = CKB::Types::OutPoint.new(cell: CKB::Types::CellOutPoint.new(tx_hash: carrot_tx_hash, index: 0))pry(main)> tx.deps.push(carrot_out_point.dup)現在我們準備簽名併傳送交易:[44] pry(main)> tx.witnesses[0].data.clear[46] pry(main)> tx = tx.sign(wallet.key, api.compute_transaction_hash(tx))
[19] pry(main)> api.send_transaction(tx)=> "0xd7b0fea7c1527cde27cc4e7a2e055e494690a384db14cc35cd2e51ec6f078163"由於該交易的 Cell 中沒有任何一個的 Cell Data 包含 carrot,因此 Type 指令碼將驗證成功。現在讓我們嘗試一個不同的交易,它確實含有一個以 carrot 開頭的 Cell:pry(main)> tx2 = wallet.generate_tx(wallet2.address, CKB::Utils.byte_to_shannon(200))pry(main)> tx2.deps.push(carrot_out_point.dup)pry(main)> tx2.outputs[0].instance_variable_set(:@type, carrot_type_script.dup)
pry(main)> tx2.outputs[0].instance_variable_set(:@data, CKB::Utils.bin_to_hex("carrot123"))pry(main)> tx2.witnesses[0].data.clearpry(main)> tx2 = tx2.sign(wallet.key, api.compute_transaction_hash(tx2))pry(main)> api.send_transaction(tx2)CKB::RPCError: jsonrpc error: {:code=>-3, :message=>"InvalidTx(ScriptFailure(ValidationFailure(-1)))"}from /home/ubuntu/code/ckb-sdk-ruby/lib/ckb/rpc.rb:164:in `rpc_request'
我們可以看到,胡蘿蔔指令碼拒絕了一筆生成的 Cell 中包含胡蘿蔔的交易。現在我可以使用這個指令碼來確保所有的 Cell 中都不含胡蘿蔔!所以,總結一下,部署和執行一個 Type 指令碼的指令碼,我們需要做的是:1. 將指令碼編譯為 RISC-V 可執行的二進位制檔案;2. 在 Cell 的 Data 部分部署二進位制檔案;3. 建立一個 Type 指令碼資料結構,使用二進位制檔案的 Blake2b 雜湊作為 code hash,補齊 args 部分中指令碼程式碼的需要的引數;4. 用生成的 Cell 中設定的 Type 指令碼建立一個新的交易;
5. 將包含指令碼程式碼的 Cell 的 Outpoint 寫入到一筆交易的 Deps 中去。雖然在這裡我們只討論了 Type 指令碼,但是 Lock 指令碼的工作方式完全相同。你唯一需要記住的是,當你使用特定的 Lock 指令碼建立 Cell 時,Lock 指令碼不會在這裡執行,它只在你使用 Cell 時執行。因此, Type 指令碼可以用於構造建立 Cell 時執行的邏輯,而 Lock 指令碼用於構造銷燬 Cell 時執行的邏輯。考慮到這一點,請確保你的 Lock 指令碼是正確的,否則你可能會在以下場景中丟失 Token:您的 Lock 指令碼有一個其他人也可以解鎖你的 Cell 的 Bug。您的 Lock 指令碼有一個 任何人(包括您)都無法解鎖你的 Cell 的 Bug。在這裡我可以提供一個技巧,始終將您的指令碼作為一個 Type 指令碼附加到你交易的一個 Output Cell 中去進行測試,這樣,發生錯誤時,您可以立即知道,並且您的 Token 可以始終保持安全。分析預設 Lock 指令碼程式碼
根據已經掌握的知識,讓我們看看 CKB 中包含的預設的 Lock 指令碼程式碼。為了避免混淆,我們可以在這個 Commit 裡檢視 Lock 指令碼程式碼:https://github.com/nervosnetwork/ckb-system-scripts/blob/66e2b3fc4fa3e80235e4b4f94a16e81352a812f7/c/secp256k1_blake160_sighash_all.c預設的 Lock 指令碼程式碼將迴圈遍歷與自身具有相同 Lock 指令碼的所有的 Input Cell,並執行以下步驟:它透過提供的 Syscall 獲取當前的交易 Hash它獲取相應的 Witness 資料作為當前輸入對於預設 Lock 指令碼,假設 Witness 中的第一個引數包含由 Cell 所有者簽名的可恢復簽名,其餘引數是使用者提供的可選引數
預設的 Lock 指令碼執行由交易 Hash 連結的二進位制程式的 Blake2b Hash, 還有所有使用者提供的引數(如果存在的話)將 Blake2b Hash 結果用作 Secp256k1 簽名驗證的訊息部分。注意,Witness 資料結構中的第一個引數提供了實際的簽名。如果簽名驗證失敗,指令碼退出並返回錯誤碼。否則它將繼續下一個迭代。注意,我們在前面討論了指令碼和指令碼程式碼之間的區別。每一個不同的公鑰雜湊都會產生不同的 Lock 指令碼,因此,如果一個交易的輸入 Cell 具有相同的預設 Lock 指令碼程式碼,但具有不同的公鑰雜湊(因此具有不同的 Lock 指令碼),將執行預設 Lock 指令碼程式碼的多個例項,每個例項都有一組共享相同 Lock 指令碼的 Cell。現在我們可以遍歷預設 Lock 指令碼程式碼的不同部分:if (argc != 2) {
  return ERROR_WRONG_NUMBER_OF_ARGUMENTS;}secp256k1_context context;if (secp256k1_context_initialize(&context, SECP256K1_CONTEXT_VERIFY) == 0) {  return ERROR_SECP_INITIALIZE;}
len = BLAKE2B_BLOCK_SIZE;ret = ckb_load_tx_hash(tx_hash, &len, 0);if (ret != CKB_SUCCESS) {  return ERROR_SYSCALL;當引數包含在 Script 資料結構的 args 部分,它們透過 Unix 傳統的 arc/ argv 方式傳送給實際執行的指令碼程式。為了進一步保持約定,我們在 argv[0] 處插入一個偽引數,所以,第一個包含的引數從 argv[1] 開始。在預設 Lock 指令碼程式碼的情況下,它接受一個引數,即從所有者的私鑰生成的公鑰 Hash。ret = ckb_load_input_by_field(NULL, &len, 0, index, CKB_SOURCE_GROUP_INPUT,
                             CKB_INPUT_FIELD_SINCE);if (ret == CKB_INDEX_OUT_OF_BOUND) {  return 0;}if (ret != CKB_SUCCESS) {  return ERROR_SYSCALL;
}我們再來看下胡蘿蔔這個例子,我們檢查是否有更多的輸入 Cell 要測試。與之前的例子有兩個不同:如果我們只想知道一個 Cell 是否存在並且不需要任何資料,我們只需要傳入 NULL 作為資料緩衝區,一個 len 變數的值是 0。透過這種方式,Syscall 將跳過資料填充,只提供可用的資料長度和正確的返回碼用於處理。在這個胡蘿蔔的例子中,我們迴圈遍歷交易中的所有輸入,但這裡我們只關心具有相同 Lock 指令碼的輸入 Cell。CKB 將具有相同鎖定(或型別)指令碼的 Cell 命名為 group。我們可以使用 CKB_SOURCE_GROUP_INPUT 代替 CKB_SOURCE_INPUT, 來表示只計算同一組中的 Cell,舉個例子,即具有與當前 Cell 相同的 Lock 指令碼的 Cell。len = WITNESS_SIZE;ret = ckb_load_witness(witness, &len, 0, index, CKB_SOURCE_GROUP_INPUT);
if (ret != CKB_SUCCESS) {return ERROR_SYSCALL;}if (len > WITNESS_SIZE) {return ERROR_WITNESS_TOO_LONG;}
if (!(witness_table = ns(Witness_as_root(witness)))) {return ERROR_ENCODING;}args = ns(Witness_data(witness_table));if (ns(Bytes_vec_len(args)) < 1) {return ERROR_WRONG_NUMBER_OF_ARGUMENTS;
}繼續沿著這個路徑,我們正在載入當前輸入的 Witness。對應的 Witness 和輸入具有相同的索引。現在 CKB 在 Syscalls 中使用flatbuffer作為序列化格式,所以如果你很好奇,Flatcc 的文件(https://github.com/dvidelabs/flatcc)可以幫助你理解。/* Load signature */len = TEMP_SIZE;ret = extract_bytes(ns(Bytes_vec_at(args, 0)), temp, &len);if (ret != CKB_SUCCESS) {
  return ERROR_ENCODING;}/* The 65th byte is recid according to contract spec.*/recid = temp[RECID_INDEX];/* Recover pubkey */secp256k1_ecdsa_recoverable_signature signature;
if (secp256k1_ecdsa_recoverable_signature_parse_compact(&context, &signature, temp, recid) == 0) {  return ERROR_SECP_PARSE_SIGNATURE;}blake2b_state blake2b_ctx;blake2b_init(&blake2b_ctx, BLAKE2B_BLOCK_SIZE);blake2b_update(&blake2b_ctx, tx_hash, BLAKE2B_BLOCK_SIZE);
for (size_t i = 1; i < ns(Bytes_vec_len(args)); i++) {  len = TEMP_SIZE;  ret = extract_bytes(ns(Bytes_vec_at(args, i)), temp, &len);  if (ret != CKB_SUCCESS) {    return ERROR_ENCODING;  }
  blake2b_update(&blake2b_ctx, temp, len);}blake2b_final(&blake2b_ctx, temp, BLAKE2B_BLOCK_SIZE);Witness 中的第一個引數是要載入的簽名,而其餘的引數(如果提供的話)被附加到用於 Blake2b 操作的交易 Hash 中。secp256k1_pubkey pubkey;if (secp256k1_ecdsa_recover(&context, &pubkey, &signature, temp) != 1) {
  return ERROR_SECP_RECOVER_PUBKEY;}然後使用雜湊後的 Blake2b 結果作為資訊,進行 Secp256 簽名驗證。size_t pubkey_size = PUBKEY_SIZE;if (secp256k1_ec_pubkey_serialize(&context, temp, &pubkey_size, &pubkey, SECP256K1_EC_COMPRESSED) != 1 ) {  return ERROR_SECP_SERIALIZE_PUBKEY;
}len = PUBKEY_SIZE;blake2b_init(&blake2b_ctx, BLAKE2B_BLOCK_SIZE);blake2b_update(&blake2b_ctx, temp, len);blake2b_final(&blake2b_ctx, temp, BLAKE2B_BLOCK_SIZE);if (memcmp(argv[1], temp, BLAKE160_SIZE) != 0) {
  return ERROR_PUBKEY_BLAKE160_HASH;}最後,同樣重要的是,我們還需要檢查可恢復簽名中包含的 Pubkey 確實是用於生成 Lock 指令碼引數中包含的 Pubkey Hash 的 Pubkey。否則,可能會有人使用另一個公鑰生成的簽名來竊取你的 Token。簡而言之,預設 Lock 指令碼中使用的方案與現在比特幣中使用的方案非常相似。介紹 Duktape我相信你和我現在的感覺一樣:我們可以用 C 語言寫合約,這很好,但是 C 語言總是讓人覺得有點乏味,而且,更現實一點說,它很危險。有更好的方法嗎?
當然!我們上面提到的 CKB-VM 本質上是一臺微型計算機,我們可以探索很多解決方案。我們現在做的一件事是,使用 JavaScript 編寫 CKB 指令碼程式碼。是的,你說對了,簡單的 ES5(是的,我知道,但這只是一個例子,你可以使用轉換器)JavaScript。這怎麼可能呢?由於我們有 C 編譯器,我們只需為嵌入式系統使用一個 JavaScript 實現,在我們的例子中,Duktape 將它從 C 編譯成 RISC-V 二進位制檔案,把它放在鏈上,我們就可以在 CKB 上執行 JavaScript 了!因為我們使用的是一臺真正的微型計算機,所以沒有什麼可以阻止我們將另一個 VM 作為 CKB 指令碼嵌入到 CKB-VM 中,並在 VM 路徑上探索這個 VM。從這條路徑展開,我們可以透過 Duktape 在 CKB 上使用 JavaScript,我們也可以透過 Mruby 在 CKB 上使用 Ruby, 我們甚至可以將比特幣指令碼或 EVM 放到鏈上,我們只需要編譯他們的虛擬機器,並把它放在鏈上。這確保了 CKB-VM 既能幫助我們儲存資產,又能構建一個多樣化的生態系統。所有的語言都應該在 CKB 上被平等對待,自由應該掌握在區塊鏈合約的開發者手中。在這個階段,你可能覺得:是的,這是可能的,但是 VM 之上的 VM 不會很慢嗎?我相信這取決於你的例子是否很慢。我堅信,基準測試沒有任何意義,除非我們將它放在具有標準硬體需求的實際用例中。所以我們需要讓時間來檢驗這是否真的會成為一個問題。在我看來,高階語言更可能用於 Type Scripts 來保護 Cell 轉換,在這種情況下,我懷疑它會很慢。此外,我們也在這個領域努力,以最佳化 CKB-VM 和 VMs 之上的 CKB-VM,使其越來越快要在 CKB 上使用 Duktape,首先需要將 Duktape 本身編譯成 RISC-V 可執行二進位制檔案:$ git clone https://github.com/nervosnetwork/ckb-duktape
$ cd ckb-duktape$ sudo docker run --rm -it -v `pwd`:/code nervos/ckb-riscv-gnu-toolchain:xenial bashadmin@chaindaily:~# cd /codeadmin@chaindaily:/code# makeriscv64-unknown-elf-gcc -Os -DCKB_NO_MMU -D__riscv_soft_float -D__riscv_float_abi_soft -Iduktape -Ic -Wall -Werror c/entry.c -c -o build/entry.oriscv64-unknown-elf-gcc -Os -DCKB_NO_MMU -D__riscv_soft_float -D__riscv_float_abi_soft -Iduktape -Ic -Wall -Werror duktape/duktape.c -c -o build/duktape.o
riscv64-unknown-elf-gcc build/entry.o build/duktape.o -o build/duktape -lm -Wl,-static -fdata-sections -ffunction-sections -Wl,--gc-sections -Wl,-sadmin@chaindaily:/code# exitexit$ ls build/duktapebuild/duktape*與胡蘿蔔的示例一樣,這裡的第一步是在 CKB Cell 中部署 Duktape 指令碼程式碼:
pry(main)> data = File.read("../ckb-duktape/build/duktape")pry(main)> duktape_data.bytesize=> 269064pry(main)> duktape_tx_hash = wallet.send_capacity(wallet.address, CKB::Utils.byte_to_shannon(280000), CKB::Utils.bin_to_hex(duktape_data))pry(main)> duktape_data_hash = CKB::Blake2b.hexdigest(duktape_data)pry(main)> duktape_out_point = CKB::Types::OutPoint.new(cell: CKB::Types::CellOutPoint.new(tx_hash: duktape_tx_hash, index: 0))
但是,和和胡蘿蔔例子不同的是,Duktape 指令碼程式碼現在需要一個引數:要執行的 JavaScript 原始碼:pry(main)> duktape_hello_type_script = CKB::Types::Script.new(code_hash: duktape_data_hash, args: [CKB::Utils.bin_to_hex("CKB.debug(\"I'm running in JS!\")")])注意,使用不同的引數,你可以為不同的用例建立不同的 Duktape 支援的 Type Script:pry(main)> duktape_hello_type_script = CKB::Types::Script.new(code_hash: duktape_data_hash, args: [CKB::Utils.bin_to_hex("var a = 1;\nvar b = a + 2;")])這反映了上面提到的指令碼程式碼與指令碼之間的差異:這裡 Duktape 作為提供 JavaScript 引擎的指令碼程式碼,而不同的指令碼利用 Duktape 指令碼程式碼在鏈上提供不同的功能。現在我們可以建立一個 Cell 與 Duktape 的 Type Script 附件:
pry(main)> tx = wallet.generate_tx(wallet2.address, CKB::Utils.byte_to_shannon(200))pry(main)> tx.deps.push(duktape_out_point.dup)pry(main)> tx.outputs[0].instance_variable_set(:@type, duktape_hello_type_script.dup)pry(main)> tx.witnesses[0].data.clearpry(main)> tx = tx.sign(wallet.key, api.compute_transaction_hash(tx))pry(main)> api.send_transaction(tx)
=> "0x2e4d3aab4284bc52fc6f07df66e7c8fc0e236916b8a8b8417abb2a2c60824028"我們可以看到指令碼執行成功,如果在 ckb.toml 檔案中將 ckb-script 日誌模組的級別設定為 debug,你可以看到以下日誌:2019-07-15 05:59:13.551 +00:00 http.worker8 DEBUG ckb-script  script group: c35b9fed5fc0dd6eaef5a918cd7a4e4b77ea93398bece4d4572b67a474874641 DEBUG OUTPUT: I'm running in JS!現在你已經成功地在 CKB 上部署了一個 JavaScript 引擎,並在 CKB 上執行基於 JavaScript 的指令碼!你可以在這裡嘗試認識的 JavaScript 程式碼。一道思考題現在你已經熟悉了 CKB 指令碼的基礎知識,下面是一個思考:
在本文中,您已經看到了一個 Always-success 的指令碼是什麼樣子的,但是一個 Always-failure 的指令碼呢?一個 Always-failure 指令碼(和指令碼程式碼)能有多小?提示:這不是 GCC 最佳化比賽,這只是一個思考。下集預告我知道這是一個很長的帖子,我希望你已經嘗試過併成功地部署了一個指令碼到 CKB。在下一篇文章中,我們將介紹一個重要的主題:如何在 CKB 中讓自己的使用者定義 Token(UDT)。CKB 上 UDT 最好的部分是,每個使用者都可以將自己的 UDT 儲存在自己的 Cell 中,這與 Ethereum 上的 ERC20 令牌不同,在 Ethereum 上,每個人的 Token 都必須位於 Token 發起者的單個地址中。所有這些都可以透過單獨使用 Type Script 來實現。

免責聲明:

  1. 本文版權歸原作者所有,僅代表作者本人觀點,不代表鏈報觀點或立場。
  2. 如發現文章、圖片等侵權行爲,侵權責任將由作者本人承擔。
  3. 鏈報僅提供相關項目信息,不構成任何投資建議

推荐阅读

;