1. 程式人生 > >【在 Nervos CKB 上做開發】Nervos CKB指令碼程式設計簡介[2]:指令碼基礎

【在 Nervos CKB 上做開發】Nervos CKB指令碼程式設計簡介[2]:指令碼基礎

CKB指令碼程式設計簡介[2]:指令碼基礎

原文作者:Xuejie 原文連結:Introduction to CKB Script Programming 2: Script 本文譯者:Shooter,Jason,Orange (排名不分先後)

上一篇我們介紹了當前 CKB 的驗證模型。這一篇會更加有趣一點,我們要向大家展示如何將指令碼程式碼真正部署到 CKB 網路上去。我希望在你看完本文後,你可以有能力自行去探索 CKB 的世界並按照你自己的意願去編寫新的指令碼程式碼。

需要注意的是,儘管我相信目前的 CKB 的程式設計模型已經相對穩定了,但是開發仍在進行中,因此未來還可能會有一些變化。我將盡力確保本文始終處於最新的狀態,但是如果在過程到任何疑惑,本文以

此版本下的 CKB 作為依據。

警告:這是一篇很長的文章,因為我想為下週更有趣的話題提供充足的內容。所以如果你沒有充足的時間,你不必馬上完成它。我在試著把它分成幾個獨立的不凡,這樣你就可以一次嘗試一個。 <br>

語法

在繼續之前,我們先來區分兩個術語:指令碼(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。

請注意,在大多數情況下,指令碼和指令碼程式碼是可以互換使用的,但是如果你在某些地方感到了困惑,那麼你可能有必要考慮一下兩者間的區別。 <br>

一個最小的 CKB 指令碼程式碼

你可能之前就已經聽所過了,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。最重要的是,您可以只獲取這個標頭檔案並使用包裝函式來建立您想要的系統呼叫。

現在有了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

  3. 為了保證最大的靈活性,CKB 使用系統呼叫的返回值來表示資料抓取狀態:0 (or CKB_SUCCESS) 意味著成功,1 (or CKB_INDEX_OUT_OF_BOUND) 意味著您已經通過一種方式獲取了所有的索引,2 (orCKB_ITEM_MISSING) 意味著不存在一個實體,比如從一個不包含該 type 指令碼的 cell 中獲取該 type 的指令碼。

概況一下,這個指令碼將迴圈遍歷交易中的所有輸出 cells,載入每個 cell data 的前6個位元組,並測試這些位元組是否和carrot匹配。如果找到匹配,指令碼將返回-1,表示錯誤狀態;如果沒有找到匹配,指令碼將返回0退出,表示執行成功。

為了執行該迴圈,該指令碼將儲存一個index變數,在每次迴圈迭代中,它將試圖讓 syscall 獲取 cell 中目前採用的index值,如果 syscall 返回 CKB_INDEX_OUT_OF_BOUND,這意味著指令碼已經遍歷所有的 cell,之後會退出迴圈;否則,迴圈將繼續,每測試 cell data 一次,index變數就會遞增一次。

這是第一個有用的 CKB 指令碼程式碼!在下一節中,我們將看到我們是如何將其部署到 CKB 中並執行它的。 <br>

將指令碼部署到 CKB 上

首先,我們需要編譯上面寫的關於胡蘿蔔的原始碼。由於 GCC 已經提供了 RISC-V 的支援,您當然可以使用官方的 GCC 來建立指令碼程式碼。或者你也可以使用我們準備的 docker 映象來避免編譯 GCC 的麻煩:

$ ls
carrot.c  ckb_consts.h  ckb_syscalls.h
$ sudo docker run --rm -it -v `pwd`:/code nervos/ckb-riscv-gnu-toolchain:xenial bash
root@dc2c0c209dcd:/# cd /code
root@dc2c0c209dcd:/code# riscv64-unknown-elf-gcc -Os carrot.c -o carrot
root@dc2c0c209dcd:/code# exit
exit
$ ls
carrot*  carrot.c  ckb_consts.h  ckb_syscalls.h

就是這樣,CKB 可以直接使用 GCC 編譯的可執行檔案作為鏈上的指令碼,無需進一步處理。我們現在可以在鏈上部署它了。注意,我將使用 CKB 的 Ruby SDK,因為我曾經是一名 Ruby 程式設計師,當然 Ruby 對我來說是最自然的(但不一定是最好的)。如何設定請參考官方 Readme 檔案

要將指令碼部署到 CKB,我們只需建立一個新的 cell,把指令碼程式碼設為 cell data 部分:

pry(main)> data = File.read("carrot")
pry(main)> data.bytesize
=> 6864
pry(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.clear
pry(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 指令碼有一個 bug,任何人(包括您)都無法解鎖您的 cell。

在這裡我們可以提供的一個技巧是,始終將您的指令碼作為一個 type 指令碼附加到你交易的一個 output cell 中去進行測試,這樣,發生錯誤時,您可以立即知道,並且您的 token 可以始終保持安全。 <br>

分析預設 lock 指令碼程式碼

根據已經掌握的知識,讓我們看看 CKB 中包含的預設的 lock 指令碼程式碼。 為了避免混淆,我們正在檢視 lock 指令碼程式碼在 這個commit

預設的 lock 指令碼程式碼將迴圈遍歷與自身具有相同 lock 指令碼的所有的 input cell,並執行以下步驟:

  • 它通過提供的 syscall 獲取當前的交易 hash

  • 它獲取相應的 witness 資料作為當前輸入

  • 對於預設 lock 指令碼,假設 witness 中的第一個引數包含由 cell 所有者簽名的可恢復簽名,其餘引數是使用者提供的可選引數

  • 預設的 lock 指令碼執行 由交易 hash 連結的二進位制程式的 blake2b hash, 還有所有使用者提供的引數(如果存在的話)

  • 將 blake2b hash 結果用作 secp256k1 簽名驗證的訊息部分。注意,witness 資料結構中的第一個引數提供了實際的簽名。

  • 如果簽名驗證失敗,指令碼退出並返回錯誤碼。否則它將繼續下一個迭代。

注意,我們在前面討論了指令碼和指令碼程式碼之間的區別。每一個不同的公鑰 hash 都會產生不同的 lock 指令碼,因此,如果一個交易的輸入 cell 具有相同的預設 lock 指令碼程式碼,但具有不同的公鑰 hash(因此具有不同的 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 將跳過資料填充,只提供可用的資料長度和正確的返回碼用於處理。

  • 在這個 carrot 的例子中,我們迴圈遍歷交易中的所有輸入, 但這裡我們只關心具有相同 lock 指令碼的輸入cell。 CKB將具有相同鎖定(或型別)指令碼的cell命名為group。 我們可以使用 CKB_SOURCE_GROUP_INPUT 代替 CKB_SOURCE_INPUT, 來表示只計算同一組中的 cell,舉個例子,即具有與當前 cell 相同的 lock 指令碼的 cells。

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的文件是你最好的朋友。

/* 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 指令碼中使用的方案與現在比特幣中使用的方案非常相似。 <br>

介紹 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,使其越來越快,:P

要在 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 bash
root@0d31cad7a539:~# cd /code
root@0d31cad7a539:/code# make
riscv64-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.o
riscv64-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,-s
root@0d31cad7a539:/code# exit
exit
$ ls build/duktape
build/duktape*

與 carrot 示例一樣,這裡的第一步是在 CKB cell 中部署 duktape 指令碼程式碼:

pry(main)> data = File.read("../ckb-duktape/build/duktape")
pry(main)> duktape_data.bytesize
=> 269064
pry(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))

與 carrot 的例子不同,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.clear
pry(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 程式碼。 <br>

一道思考題

現在你已經熟悉了 CKB 指令碼的基礎知識,下面是一個思考: 在本文中,您已經看到了一個 always-success 的指令碼是什麼樣子的,但是一個 always-failure 的指令碼呢?一個 always-failure 指令碼(和指令碼程式碼)能有多小?

提示:這不是 gcc 優化比賽,這只是一個思考。 <br>

下集預告

我知道這是一個很長的帖子,我希望你已經嘗試過,併成功地部署了一個指令碼到 CKB。在下一篇文章中,我們將介紹一個重要的主題:如何在 CKB 定義自己的使用者定義 token(UDT)。CKB 上 udt 最好的部分是,每個使用者都可以將自己的 udt 儲存在自己的 cell 中,這與 Ethereum 上的 ERC20 令牌不同,在 Ethereum 上,每個人的 token 都必須位於 token 發起者的單個地址中。所有這些都可以通過單獨使用 type script 來實現。

如果你感興趣,請繼續關注 :) <br>

加入 Nervos Community

Nervos Community 致力於成為最好的 Nervos 社群,我們將持續地推廣和普 及 Nervos 技術,深入挖掘 Nervos 的內在價值,開拓 Nervos 的無限可能, 為每一位想要深入瞭解 Nervos Network 的人提供一