CKB腳本編程簡介[2]:腳本基礎(chǔ)
原文作者:Xuejie
原文鏈接:Introduction to CKB Script Programming 2: Script
本文譯者:Shooter,Jason,Orange (排名不分先后)
上一篇我們介紹了當(dāng)前 CKB 的驗(yàn)證模型。這一篇會更加有趣一點(diǎn),我們要向大家展示如何將腳本代碼真正部署到 CKB 網(wǎng)絡(luò)上去。我希望在你看完本文后,你可以有能力自行去探索 CKB 的世界并按照你自己的意愿去編寫新的腳本代碼。
需要注意的是,盡管我相信目前的 CKB 的編程模型已經(jīng)相對穩(wěn)定了,但是開發(fā)仍在進(jìn)行中,因此未來還可能會有一些變化。我將盡力確保本文始終處于最新的狀態(tài),但是如果在過程到任何疑惑,本文以 此版本下的 CKB 作為依據(jù)。
警告:這是一篇很長的文章,因?yàn)槲蚁霝橄轮芨腥さ脑掝}提供充足的內(nèi)容。所以如果你沒有充足的時間,你不必馬上完成它。我在試著把它分成幾個獨(dú)立的不凡,這樣你就可以一次嘗試一個。
語法
在繼續(xù)之前,我們先來區(qū)分兩個術(shù)語:腳本(script)和腳本代碼(script code)
在本文以及整個系列文章內(nèi),我們將區(qū)分腳本和腳本代碼。腳本代碼實(shí)際上是指你編寫和編譯并在 CKB 上運(yùn)行的程序。而腳本,實(shí)際上是指 CKB 中使用的腳本數(shù)據(jù)結(jié)構(gòu),它會比腳本代碼稍微多一點(diǎn)點(diǎn):
pub struct Script {
pub args: Vec<Bytes>,
pub code_hash: H256,
pub hash_type: ScriptHashType,
}
我們目前可以先忽略hash_type,之后的文章再來解釋什么是hash_type以及它有什么有趣的用法。在這篇文章的后面,我們會說明code_hash實(shí)際上是用來標(biāo)識腳本代碼的,所以目前我們可以只把它當(dāng)成腳本代碼。那腳本還包括什么呢?腳本還包括args這個部分,它是用來區(qū)分腳本和腳本代碼的。args在這里可以用來給一個 CKB 腳本提供額外的參數(shù),比如:雖然大家可能都會使用相同的默認(rèn)的 lock script code,但是每個人可能都有自己的 pubkey hash,args 就是用來保存 pubkey hash 的位置。這樣,每一個CKB 的用戶都可以擁有不同的 lock script ,但是卻可以共用同樣的 lock script code。
請注意,在大多數(shù)情況下,腳本和腳本代碼是可以互換使用的,但是如果你在某些地方感到了困惑,那么你可能有必要考慮一下兩者間的區(qū)別。
一個最小的 CKB 腳本代碼
你可能之前就已經(jīng)聽所過了,CKB (編者注:此處指的應(yīng)該是 CKB VM)是基于開源的 RISC-V 指令集編寫的。但這到底意味著什么呢?用我自己的話來說,這意味著我們(在某種程度上)在 CKB 中嵌入了一臺真正的微型計(jì)算機(jī),而不是一臺虛擬機(jī)。一臺真正的計(jì)算機(jī)的好處是,你可以用任何語言編寫任何你想寫的邏輯。在這里,我們展示的前面幾個例子將會用 C語言編寫,以保持簡單性(我是說工具鏈中的簡單性,而不是語言),之后我們還會切換到基于 JavaScript 的腳本代碼,并希望在本系列中展示更多的語言。記住,在 CKB 上有無限的可能!
正如我們提到的,CKB VM 更像是一臺真正的微型計(jì)算機(jī)。CKB 的代碼腳本看起來也更像是我們在電腦上跑的一個常見的 Unix 風(fēng)格的可執(zhí)行程序。
int main(int argc, char* argv[])
{
return 0;
}
當(dāng)你的代碼通過 C 編譯器編譯時,它將成為可以在 CKB 上運(yùn)行的腳本代碼。換句話說,CKB 只是采用了普通的舊式 Unix 風(fēng)格的可執(zhí)行程序(但使用的是 RISC-V 體系結(jié)構(gòu),而不是流行的 x86 體系結(jié)構(gòu)),并在虛擬機(jī)環(huán)境中運(yùn)行它。如果程序的返回代碼是 0 ,我們認(rèn)為腳本成功了,所有非零的返回代碼都將被視為失敗腳本。
在上面的例子中,我們展示了一個總是成功的腳本代碼。因?yàn)榉祷卮a總是 0。但是請不要使用這個作為您的 lock script code ,否則您的 token 可能會被任何人拿走。
但是顯然上面的例子并不有趣,這里我們從一個有趣的想法開始:我個人不是很喜歡胡蘿卜。我知道胡蘿卜從營養(yǎng)的角度來看是很好的,但我還是想要避免它的味道。如果現(xiàn)在我想設(shè)定一個規(guī)則,比如我想讓我在 CKB 上的 Cell 里面都沒有以carrot開頭的數(shù)據(jù)?讓我們編寫一個腳本代碼來實(shí)現(xiàn)這一點(diǎn)。
為了確保沒有一個 cell 在 cell data
中包含carrot,我們首先需要一種方法來讀取腳本中的 cell data。CKB 提供了syscalls來幫助解決這個問題。
為了確保 CKB 腳本的安全性,每個腳本都必須在與運(yùn)行 CKB 的主計(jì)算機(jī)完全分離的隔離環(huán)境中運(yùn)行。這樣它就不能訪問它不需要的數(shù)據(jù),比如你的私鑰或密碼。然而,要使得腳本有用,必須有特定的數(shù)據(jù)要訪問,比如腳本保護(hù)的 cell 或腳本驗(yàn)證的事務(wù)。CKB 提供了syscalls來確保這一點(diǎn),syscalls是在 RISC-V 的標(biāo)準(zhǔn)中定義的,它們提供了訪問環(huán)境中某些資源的方法。在正常情況下,這里的環(huán)境指的是操作系統(tǒng),但是在 CKB VM 中,環(huán)境指的是實(shí)際的 CKB 進(jìn)程。使用syscalls, CKB腳本可以訪問包含自身的整個事務(wù),包括輸入(inputs)、輸出(outpus)、見證(witnesses)和 deps。
好消息是,我們已經(jīng)將syscalls封裝在了一個易于使用的頭文件中,非常歡迎您在這里查看這個文件,了解如何實(shí)現(xiàn)syscalls。最重要的是,您可以只獲取這個頭文件并使用包裝函數(shù)來創(chuàng)建您想要的系統(tǒng)調(diào)用。
現(xiàn)在有了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;
}
以下幾點(diǎn)需要解釋一下:
由于 C 語言的怪癖,
len字段需要標(biāo)記為volatile。我們會同時使用它作為輸入和輸出參數(shù),CKB VM 只能在它還保存在內(nèi)存中時,才可以把它設(shè)置輸出參數(shù)。而volatile可以確保 C 編譯器將它保存為基于 RISC-V 內(nèi)存的變量。在使用
syscall時,我們需要提供以下功能:一個緩沖區(qū)來保存syscall提供的數(shù)據(jù);一個len字段,來表示系統(tǒng)調(diào)用返回的緩沖區(qū)長度和可用數(shù)據(jù)長度;一個輸入數(shù)據(jù)緩沖區(qū)中的偏移量,以及幾個我們在交易中需要獲取的確切字段的參數(shù)。詳情請參閱我們的RFC。為了保證最大的靈活性,CKB 使用系統(tǒng)調(diào)用的返回值來表示數(shù)據(jù)抓取狀態(tài):0 (or
CKB_SUCCESS) 意味著成功,1 (orCKB_INDEX_OUT_OF_BOUND) 意味著您已經(jīng)通過一種方式獲取了所有的索引,2 (orCKB_ITEM_MISSING) 意味著不存在一個實(shí)體,比如從一個不包含該 type 腳本的 cell 中獲取該 type 的腳本。
概況一下,這個腳本將循環(huán)遍歷交易中的所有輸出 cells,加載每個 cell data 的前6個字節(jié),并測試這些字節(jié)是否和carrot匹配。如果找到匹配,腳本將返回-1,表示錯誤狀態(tài);如果沒有找到匹配,腳本將返回0退出,表示執(zhí)行成功。
為了執(zhí)行該循環(huán),該腳本將保存一個index變量,在每次循環(huán)迭代中,它將試圖讓 syscall 獲取 cell 中目前采用的index值,如果 syscall 返回 CKB_INDEX_OUT_OF_BOUND,這意味著腳本已經(jīng)遍歷所有的 cell,之后會退出循環(huán);否則,循環(huán)將繼續(xù),每測試 cell data 一次,index變量就會遞增一次。
這是第一個有用的 CKB 腳本代碼!在下一節(jié)中,我們將看到我們是如何將其部署到 CKB 中并運(yùn)行它的。
將腳本部署到 CKB 上
首先,我們需要編譯上面寫的關(guān)于胡蘿卜的源代碼。由于 GCC 已經(jīng)提供了 RISC-V 的支持,您當(dāng)然可以使用官方的 GCC 來創(chuàng)建腳本代碼。或者你也可以使用我們準(zhǔn)備的 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 編譯的可執(zhí)行文件作為鏈上的腳本,無需進(jìn)一步處理。我們現(xiàn)在可以在鏈上部署它了。注意,我將使用 CKB 的 Ruby SDK,因?yàn)槲以?jīng)是一名 Ruby 程序員,當(dāng)然 Ruby 對我來說是最自然的(但不一定是最好的)。如何設(shè)置請參考官方 Readme 文件。
要將腳本部署到 CKB,我們只需創(chuàng)建一個新的 cell,把腳本代碼設(shè)為 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))
在這里,我首先要通過向自己發(fā)送 token 來創(chuàng)建一個容量足夠的新的 cell。現(xiàn)在我們可以創(chuàng)建包含胡蘿卜腳本代碼的腳本:
pry(main)> carrot_data_hash = CKB::Blake2b.hexdigest(data)
pry(main)> carrot_type_script = CKB::Types::Script.new(code_hash: carrot_data_hash, args: [])
回憶一下腳本數(shù)據(jù)結(jié)構(gòu):
pub struct Script {
pub args: Vec<Bytes>,
pub code_hash: H256,
pub hash_type: ScriptHashType,
}
我們可以看到,我們沒有直接將腳本代碼嵌入到腳本數(shù)據(jù)結(jié)構(gòu)中,而是只包含了代碼的哈希,這是實(shí)際腳本二進(jìn)制代碼的 Blake2b 哈希。由于胡蘿卜腳本不使用參數(shù),我們可以對args部分使用空數(shù)組。
注意,這里仍然忽略了 hash_type,我們將在后面的文章中通過另一種方式討論指定代碼哈希?,F(xiàn)在,讓我們盡量保持簡單。
要運(yùn)行胡蘿卜腳本,我們需要創(chuàng)建一個新的交易,并將胡蘿卜 type 腳本設(shè)置為其中一個輸出 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)
我們還需要進(jìn)行一個步驟:為了讓 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)
現(xiàn)在我們準(zhǔn)備簽名并發(fā)送交易:
[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 腳本將驗(yàn)證成功?,F(xiàn)在讓我們嘗試一個不同的交易,它確實(shí)含有一個以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 中包含胡蘿卜的交易?,F(xiàn)在我可以使用這個腳本來確保所有的 cell 中都不含胡蘿卜!
所以,總結(jié)一下,部署和運(yùn)行一個 type 腳本的腳本,我們需要做的是:
- 將腳本編譯為 RISC-V 可執(zhí)行的二進(jìn)制文件
- 在 cell 的 data 部分部署二進(jìn)制文件
- 創(chuàng)建一個 type 腳本數(shù)據(jù)結(jié)構(gòu),使用二進(jìn)制文件的 blake2b 散列作為
code hash,補(bǔ)齊args部分中腳本代碼的需要的參數(shù) - 用生成的 cell 中設(shè)置的 type 腳本創(chuàng)建一個新的交易
- 將包含腳本代碼的 cell 的 outpoint 寫入到一筆交易的 deps 中去
這就是你所有需要的!如果您的腳本遇到任何問題,您需要檢查這些要點(diǎn)。
雖然在這里我們只討論了 type 腳本,但是 lock 腳本的工作方式完全相同。您惟一需要記住的是,當(dāng)您使用特定的 lock 腳本創(chuàng)建 cell 時,lock 腳本不會在這里運(yùn)行,它只在您使用 cell 時運(yùn)行。因此, type 腳本可以用于構(gòu)造創(chuàng)建 cell 時運(yùn)行的邏輯,而 lock 腳本用于構(gòu)造銷毀 cell 時運(yùn)行的邏輯。考慮到這一點(diǎn),請確保您的 lock 腳本是正確的,否則您可能會在以下場景中丟失 token:
您的 lock 腳本有一個其他人也可以解鎖您的 cell 的 bug。
您的 lock 腳本有一個 bug,任何人(包括您)都無法解鎖您的 cell。
在這里我們可以提供的一個技巧是,始終將您的腳本作為一個 type 腳本附加到你交易的一個 output cell 中去進(jìn)行測試,這樣,發(fā)生錯誤時,您可以立即知道,并且您的 token 可以始終保持安全。
分析默認(rèn) lock 腳本代碼
根據(jù)已經(jīng)掌握的知識,讓我們看看 CKB 中包含的默認(rèn)的 lock 腳本代碼。 為了避免混淆,我們正在查看 lock 腳本代碼在 這個commit。
默認(rèn)的 lock 腳本代碼將循環(huán)遍歷與自身具有相同 lock 腳本的所有的 input cell,并執(zhí)行以下步驟:
它通過提供的 syscall 獲取當(dāng)前的交易 hash
它獲取相應(yīng)的 witness 數(shù)據(jù)作為當(dāng)前輸入
對于默認(rèn) lock 腳本,假設(shè) witness 中的第一個參數(shù)包含由 cell 所有者簽名的可恢復(fù)簽名,其余參數(shù)是用戶提供的可選參數(shù)
默認(rèn)的 lock 腳本運(yùn)行 由交易 hash 鏈接的二進(jìn)制程序的 blake2b hash, 還有所有用戶提供的參數(shù)(如果存在的話)
將 blake2b hash 結(jié)果用作 secp256k1 簽名驗(yàn)證的消息部分。注意,witness 數(shù)據(jù)結(jié)構(gòu)中的第一個參數(shù)提供了實(shí)際的簽名。
如果簽名驗(yàn)證失敗,腳本退出并返回錯誤碼。否則它將繼續(xù)下一個迭代。
注意,我們在前面討論了腳本和腳本代碼之間的區(qū)別。每一個不同的公鑰 hash 都會產(chǎn)生不同的 lock 腳本,因此,如果一個交易的輸入 cell 具有相同的默認(rèn) lock 腳本代碼,但具有不同的公鑰 hash(因此具有不同的 lock 腳本),將執(zhí)行默認(rèn) lock 腳本代碼的多個實(shí)例,每個實(shí)例都有一組共享相同 lock 腳本的 cell。
現(xiàn)在我們可以遍歷默認(rèn) 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;
}
當(dāng)參數(shù)包含在 Script數(shù)據(jù)結(jié)構(gòu)的 args部分, 它們通過 Unix 傳統(tǒng)的arc/argv方式發(fā)送給實(shí)際運(yùn)行的腳本程序。為了進(jìn)一步保持約定,我們在argv[0] 處插入一個偽參數(shù),所以 第一個包含的參數(shù)從argv[1]開始。在默認(rèn) lock 腳本代碼的情況下,它接受一個參數(shù),即從所有者的私鑰生成的公鑰 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;
}
使用與胡蘿卜這個例子相同的技術(shù),我們檢查是否有更多的輸入 cell 要測試。與之前的例子有兩個不同:
如果我們只想知道一個 cell 是否存在并且不需要任何數(shù)據(jù),我們只需要傳入
NULL作為數(shù)據(jù)緩沖區(qū),一個len變量的值是 0。
通過這種方式,syscall 將跳過數(shù)據(jù)填充,只提供可用的數(shù)據(jù)長度和正確的返回碼用于處理。在這個 carrot 的例子中,我們循環(huán)遍歷交易中的所有輸入, 但這里我們只關(guān)心具有相同 lock 腳本的輸入cell。 CKB將具有相同鎖定(或類型)腳本的
cell命名為group。 我們可以使用CKB_SOURCE_GROUP_INPUT代替CKB_SOURCE_INPUT, 來表示只計(jì)算同一組中的 cell,舉個例子,即具有與當(dāng)前 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;
}
繼續(xù)沿著這個路徑,我們正在加載當(dāng)前輸入的 witness。 對應(yīng)的 witness 和輸入具有相同的索引?,F(xiàn)在 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 中的第一個參數(shù)是要加載的簽名,而其余的參數(shù)(如果提供的話)被附加到用于 blake2b 操作的交易 hash 中。
secp256k1_pubkey pubkey;
if (secp256k1_ecdsa_recover(&context, &pubkey, &signature, temp) != 1) {
return ERROR_SECP_RECOVER_PUBKEY;
}
然后使用哈希后的 blake2b 結(jié)果作為信息,進(jìn)行 secp256 簽名驗(yàn)證。
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;
}
最后同樣重要的是,我們還需要檢查可恢復(fù)簽名中包含的 pubkey 確實(shí)是用于生成 lock 腳本參數(shù)中包含的 pubkey hash 的 pubkey。否則,可能會有人使用另一個公鑰生成的簽名來竊取你的 token。
簡而言之,默認(rèn) lock 腳本中使用的方案與現(xiàn)在比特幣中使用的方案非常相似。
介紹 Duktape
我相信你和我現(xiàn)在的感覺一樣: 我們可以用 C 語言寫合約,這很好,但是 C 語言總是讓人覺得有點(diǎn)乏味,而且,讓我們面對現(xiàn)實(shí),它很危險(xiǎn)。
有更好的方法嗎?
當(dāng)然! 我們上面提到的 CKB VM 本質(zhì)上是一臺微型計(jì)算機(jī),我們可以探索很多解決方案。 我們在這里做的一件事是,使用 JavaScript 編寫 CKB 腳本代碼。 是的,你說對了,簡單的 ES5 (是的,我知道,但這只是一個例子,你可以使用轉(zhuǎn)換器) JavaScript。
這怎么可能呢? 由于我們有 C 編譯器,我們只需為嵌入式系統(tǒng)使用一個 JavaScript 實(shí)現(xiàn),在我們的例子中,duktape 將它從 C 編譯成 RISC-V 二進(jìn)制文件,把它放在鏈上,我們就可以在 CKB 上運(yùn)行 JavaScript 了!因?yàn)槲覀兪褂玫氖且慌_真正的微型計(jì)算機(jī),所以沒有什么可以阻止我們將另一個 VM 作為 CKB 腳本嵌入到 CKB VM 中,并在 VM 路徑上探索這個 VM。
從這條路徑展開,我們可以通過 duktape 在 CKB 上使用 JavaScript,我們也可以通過 mruby在 ckb 上使用 Ruby, 我們甚至可以將比特幣腳本或EVM放到鏈上,我們只需要編譯他們的虛擬機(jī),并把它放在鏈上。這確保了 CKB VM 既能幫助我們保存資產(chǎn),又能構(gòu)建一個多樣化的生態(tài)系統(tǒng)。所有的語言都應(yīng)該在 CKB 上被平等對待,自由應(yīng)該掌握在區(qū)塊鏈合約的開發(fā)者手中。
在這個階段,你可能想問: 是的,這是可能的,但是 VM 之上的 VM 不會很慢嗎? 我相信這取決于你的例子是否很慢。我堅(jiān)信,基準(zhǔn)測試沒有任何意義,除非我們將它放在具有標(biāo)準(zhǔn)硬件需求的實(shí)際用例中。 所以我們需要有時間檢驗(yàn)這是否真的會成為一個問題。 在我看來,高級語言更可能用于 type scripts 來保護(hù) cell 轉(zhuǎn)換,在這種情況下,我懷疑它會很慢。此外,我們也在這個領(lǐng)域努力工作,以優(yōu)化 CKB VM 和 VMs 之上的 CKB VM,使其越來越快,:P
要在 CKB 上使用 duktape,首先需要將 duktape 本身編譯成 RISC-V 可執(zhí)行二進(jìn)制文件:
$ 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 腳本代碼現(xiàn)在需要一個參數(shù): 要執(zhí)行的 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!\")")])
注意,使用不同的參數(shù),你可以為不同的用例創(chuàng)建不同的 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 腳本代碼在鏈上提供不同的功能。
現(xiàn)在我們可以創(chuàng)建一個 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"
我們可以看到腳本執(zhí)行成功,如果在ckb.toml 文件中將 ckb-script日志模塊的級別設(shè)置為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!
現(xiàn)在您已經(jīng)成功地在 CKB 上部署了一個 JavaScript 引擎,并在 CKB 上運(yùn)行基于 JavaScript 的腳本!
你可以在這里嘗試認(rèn)識的 JavaScript 代碼。
一道思考題
現(xiàn)在你已經(jīng)熟悉了 CKB 腳本的基礎(chǔ)知識,下面是一個思考:
在本文中,您已經(jīng)看到了一個 always-success 的腳本是什么樣子的,但是一個 always-failure 的腳本呢?一個 always-failure 腳本(和腳本代碼)能有多小?
提示:這不是 gcc 優(yōu)化比賽,這只是一個思考。
下集預(yù)告
我知道這是一個很長的帖子,我希望你已經(jīng)嘗試過,并成功地部署了一個腳本到 CKB。在下一篇文章中,我們將介紹一個重要的主題:如何在 CKB 定義自己的用戶定義 token(UDT)。CKB 上 udt 最好的部分是,每個用戶都可以將自己的 udt 存儲在自己的 cell 中,這與 Ethereum 上的 ERC20 令牌不同,在 Ethereum 上,每個人的 token 都必須位于 token 發(fā)起者的單個地址中。所有這些都可以通過單獨(dú)使用 type script 來實(shí)現(xiàn)。
如果你感興趣,請繼續(xù)關(guān)注 :)
加入 Nervos Community
Nervos Community 致力于成為最好的 Nervos 社區(qū),我們將持續(xù)地推廣和普 及 Nervos 技術(shù),深入挖掘 Nervos 的內(nèi)在價(jià)值,開拓 Nervos 的無限可能, 為每一位想要深入了解 Nervos Network 的人提供一個優(yōu)質(zhì)的平臺。
添加微信號:BitcoinDog 即可加入 Nervos Community,如果是程序員請備注,還會將您拉入開發(fā)者群。