【在 Nervos CKB 上作開發】Nervos CKB腳本編程簡介[2]:腳本基礎

CKB腳本編程簡介[2]:腳本基礎

原文做者:Xuejie
原文連接:Introduction to CKB Script Programming 2: Script
本文譯者:Shooter,Jason,Orange (排名不分前後)html

上一篇咱們介紹了當前 CKB 的驗證模型。這一篇會更加有趣一點,咱們要向你們展現如何將腳本代碼真正部署到 CKB 網絡上去。我但願在你看完本文後,你能夠有能力自行去探索 CKB 的世界並按照你本身的意願去編寫新的腳本代碼。git

須要注意的是,儘管我相信目前的 CKB 的編程模型已經相對穩定了,可是開發仍在進行中,所以將來還可能會有一些變化。我將盡力確保本文始終處於最新的狀態,可是若是在過程到任何疑惑,本文以 此版本下的 CKB 做爲依據。程序員

警告:這是一篇很長的文章,由於我想爲下週更有趣的話題提供充足的內容。因此若是你沒有充足的時間,你沒必要立刻完成它。我在試着把它分紅幾個獨立的不凡,這樣你就能夠一次嘗試一個。

github

語法

在繼續以前,咱們先來區分兩個術語:腳本(script)和腳本代碼(script code)docker

在本文以及整個系列文章內,咱們將區分腳本和腳本代碼。腳本代碼其實是指你編寫和編譯並在 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。json

請注意,在大多數狀況下,腳本和腳本代碼是能夠互換使用的,可是若是你在某些地方感到了困惑,那麼你可能有必要考慮一下二者間的區別。

ubuntu

一個最小的 CKB 腳本代碼

你可能以前就已經聽所過了,CKB (編者注:此處指的應該是 CKB VM)是基於開源的 RISC-V 指令集編寫的。但這到底意味着什麼呢?用我本身的話來講,這意味着咱們(在某種程度上)在 CKB 中嵌入了一臺真正的微型計算機,而不是一臺虛擬機。一臺真正的計算機的好處是,你能夠用任何語言編寫任何你想寫的邏輯。在這裏,咱們展現的前面幾個例子將會用 C語言編寫,以保持簡單性(我是說工具鏈中的簡單性,而不是語言),以後咱們還會切換到基於 JavaScript 的腳本代碼,並但願在本系列中展現更多的語言。記住,在 CKB 上有無限的可能!api

正如咱們提到的,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 中並運行它的。

將腳本部署到 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 能夠始終保持安全。

分析默認 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 腳本中使用的方案與如今比特幣中使用的方案很是類似。

介紹 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 代碼。

一道思考題

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

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

下集預告

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

若是你感興趣,請繼續關注 :)

加入 Nervos Community

Nervos Community 致力於成爲最好的 Nervos 社區,咱們將持續地推廣和普 及 Nervos 技術,深刻挖掘 Nervos 的內在價值,開拓 Nervos 的無限可能, 爲每一位想要深刻了解 Nervos Network 的人提供一個優質的平臺。

添加微信號:BitcoinDog 便可加入 Nervos Community,若是是程序員請備註,還會將您拉入開發者羣。

相關文章
相關標籤/搜索