【在 Nervos CKB 上做開發(fā)】Nervos CKB腳本編程簡(jiǎn)介[2]:腳本基礎(chǔ)

CKB腳本編程簡(jiǎn)介[2]:腳本基礎(chǔ)

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

上一篇我們介紹了當(dāng)前 CKB 的驗(yàn)證模型砚尽。這一篇會(huì)更加有趣一點(diǎn)镜悉,我們要向大家展示如何將腳本代碼真正部署到 CKB 網(wǎng)絡(luò)上去。我希望在你看完本文后禽绪,你可以有能力自行去探索 CKB 的世界并按照你自己的意愿去編寫新的腳本代碼蓖救。

需要注意的是,盡管我相信目前的 CKB 的編程模型已經(jīng)相對(duì)穩(wěn)定了印屁,但是開發(fā)仍在進(jìn)行中循捺,因此未來(lái)還可能會(huì)有一些變化。我將盡力確保本文始終處于最新的狀態(tài)雄人,但是如果在過(guò)程到任何疑惑从橘,本文以 此版本下的 CKB 作為依據(jù)。

警告:這是一篇很長(zhǎng)的文章础钠,因?yàn)槲蚁霝橄轮芨腥さ脑掝}提供充足的內(nèi)容恰力。所以如果你沒有充足的時(shí)間,你不必馬上完成它旗吁。我在試著把它分成幾個(gè)獨(dú)立的不凡踩萎,這樣你就可以一次嘗試一個(gè)。

語(yǔ)法

在繼續(xù)之前很钓,我們先來(lái)區(qū)分兩個(gè)術(shù)語(yǔ):腳本(script)和腳本代碼(script code)

在本文以及整個(gè)系列文章內(nèi)驻民,我們將區(qū)分腳本和腳本代碼。腳本代碼實(shí)際上是指你編寫和編譯并在 CKB 上運(yùn)行的程序履怯。而腳本回还,實(shí)際上是指 CKB 中使用的腳本數(shù)據(jù)結(jié)構(gòu),它會(huì)比腳本代碼稍微多一點(diǎn)點(diǎn):

pub struct Script {
    pub args: Vec<Bytes>,
    pub code_hash: H256,
    pub hash_type: ScriptHashType,
    }

我們目前可以先忽略hash_type叹洲,之后的文章再來(lái)解釋什么是hash_type以及它有什么有趣的用法柠硕。在這篇文章的后面,我們會(huì)說(shuō)明code_hash實(shí)際上是用來(lái)標(biāo)識(shí)腳本代碼的,所以目前我們可以只把它當(dāng)成腳本代碼蝗柔。那腳本還包括什么呢?腳本還包括args這個(gè)部分闻葵,它是用來(lái)區(qū)分腳本和腳本代碼的。args在這里可以用來(lái)給一個(gè) CKB 腳本提供額外的參數(shù)癣丧,比如:雖然大家可能都會(huì)使用相同的默認(rèn)的 lock script code槽畔,但是每個(gè)人可能都有自己的 pubkey hash,args 就是用來(lái)保存 pubkey hash 的位置胁编。這樣厢钧,每一個(gè)CKB 的用戶都可以擁有不同的 lock script ,但是卻可以共用同樣的 lock script code嬉橙。

請(qǐng)注意早直,在大多數(shù)情況下,腳本和腳本代碼是可以互換使用的市框,但是如果你在某些地方感到了困惑霞扬,那么你可能有必要考慮一下兩者間的區(qū)別。

一個(gè)最小的 CKB 腳本代碼

你可能之前就已經(jīng)聽所過(guò)了枫振,CKB (編者注:此處指的應(yīng)該是 CKB VM)是基于開源的 RISC-V 指令集編寫的喻圃。但這到底意味著什么呢?用我自己的話來(lái)說(shuō)粪滤,這意味著我們(在某種程度上)在 CKB 中嵌入了一臺(tái)真正的微型計(jì)算機(jī)斧拍,而不是一臺(tái)虛擬機(jī)。一臺(tái)真正的計(jì)算機(jī)的好處是额衙,你可以用任何語(yǔ)言編寫任何你想寫的邏輯饮焦。在這里,我們展示的前面幾個(gè)例子將會(huì)用 C語(yǔ)言編寫窍侧,以保持簡(jiǎn)單性(我是說(shuō)工具鏈中的簡(jiǎn)單性县踢,而不是語(yǔ)言),之后我們還會(huì)切換到基于 JavaScript 的腳本代碼伟件,并希望在本系列中展示更多的語(yǔ)言硼啤。記住,在 CKB 上有無(wú)限的可能斧账!

正如我們提到的谴返,CKB VM 更像是一臺(tái)真正的微型計(jì)算機(jī)。CKB 的代碼腳本看起來(lái)也更像是我們?cè)陔娔X上跑的一個(gè)常見的 Unix 風(fēng)格的可執(zhí)行程序咧织。

int main(int argc, char* argv[])
{
  return 0;
}

當(dāng)你的代碼通過(guò) C 編譯器編譯時(shí)嗓袱,它將成為可以在 CKB 上運(yùn)行的腳本代碼。換句話說(shuō)习绢,CKB 只是采用了普通的舊式 Unix 風(fēng)格的可執(zhí)行程序(但使用的是 RISC-V 體系結(jié)構(gòu)渠抹,而不是流行的 x86 體系結(jié)構(gòu))蝙昙,并在虛擬機(jī)環(huán)境中運(yùn)行它。如果程序的返回代碼是 0 梧却,我們認(rèn)為腳本成功了奇颠,所有非零的返回代碼都將被視為失敗腳本。

在上面的例子中放航,我們展示了一個(gè)總是成功的腳本代碼烈拒。因?yàn)榉祷卮a總是 0。但是請(qǐng)不要使用這個(gè)作為您的 lock script code 广鳍,否則您的 token 可能會(huì)被任何人拿走荆几。

但是顯然上面的例子并不有趣,這里我們從一個(gè)有趣的想法開始:我個(gè)人不是很喜歡胡蘿卜搜锰。我知道胡蘿卜從營(yíng)養(yǎng)的角度來(lái)看是很好的伴郁,但我還是想要避免它的味道耿战。如果現(xiàn)在我想設(shè)定一個(gè)規(guī)則蛋叼,比如我想讓我在 CKB 上的 Cell 里面都沒有以carrot開頭的數(shù)據(jù)?讓我們編寫一個(gè)腳本代碼來(lái)實(shí)現(xiàn)這一點(diǎn)。

為了確保沒有一個(gè) cell 在 cell data
中包含carrot剂陡,我們首先需要一種方法來(lái)讀取腳本中的 cell data狈涮。CKB 提供了syscalls來(lái)幫助解決這個(gè)問題。

為了確保 CKB 腳本的安全性鸭栖,每個(gè)腳本都必須在與運(yùn)行 CKB 的主計(jì)算機(jī)完全分離的隔離環(huán)境中運(yùn)行歌馍。這樣它就不能訪問它不需要的數(shù)據(jù),比如你的私鑰或密碼晕鹊。然而松却,要使得腳本有用,必須有特定的數(shù)據(jù)要訪問溅话,比如腳本保護(hù)的 cell 或腳本驗(yàn)證的事務(wù)晓锻。CKB 提供了syscalls來(lái)確保這一點(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腳本可以訪問包含自身的整個(gè)事務(wù)战转,包括輸入(inputs)、輸出(outpus)以躯、見證(witnesses)和 deps槐秧。

好消息是,我們已經(jīng)將syscalls封裝在了一個(gè)易于使用的頭文件中,非常歡迎您在這里查看這個(gè)文件色鸳,了解如何實(shí)現(xiàn)syscalls社痛。最重要的是,您可以只獲取這個(gè)頭文件并使用包裝函數(shù)來(lái)創(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)需要解釋一下:

  1. 由于 C 語(yǔ)言的怪癖,len字段需要標(biāo)記為volatile吏砂。我們會(huì)同時(shí)使用它作為輸入和輸出參數(shù)撵儿,CKB VM 只能在它還保存在內(nèi)存中時(shí),才可以把它設(shè)置輸出參數(shù)狐血。而volatile可以確保 C 編譯器將它保存為基于 RISC-V 內(nèi)存的變量淀歇。

  2. 在使用syscall時(shí),我們需要提供以下功能:一個(gè)緩沖區(qū)來(lái)保存syscall提供的數(shù)據(jù)匈织;一個(gè)len字段浪默,來(lái)表示系統(tǒng)調(diào)用返回的緩沖區(qū)長(zhǎng)度和可用數(shù)據(jù)長(zhǎng)度;一個(gè)輸入數(shù)據(jù)緩沖區(qū)中的偏移量缀匕,以及幾個(gè)我們?cè)诮灰字行枰@取的確切字段的參數(shù)渔扎。詳情請(qǐng)參閱我們的RFC贺嫂。

  3. 為了保證最大的靈活性饰抒,CKB 使用系統(tǒng)調(diào)用的返回值來(lái)表示數(shù)據(jù)抓取狀態(tài):0 (or CKB_SUCCESS) 意味著成功奢啥,1 (or CKB_INDEX_OUT_OF_BOUND) 意味著您已經(jīng)通過(guò)一種方式獲取了所有的索引,2 (orCKB_ITEM_MISSING) 意味著不存在一個(gè)實(shí)體满钟,比如從一個(gè)不包含該 type 腳本的 cell 中獲取該 type 的腳本胜榔。

概況一下,這個(gè)腳本將循環(huán)遍歷交易中的所有輸出 cells湃番,加載每個(gè) cell data 的前6個(gè)字節(jié)夭织,并測(cè)試這些字節(jié)是否和carrot匹配。如果找到匹配牵辣,腳本將返回-1摔癣,表示錯(cuò)誤狀態(tài);如果沒有找到匹配纬向,腳本將返回0退出择浊,表示執(zhí)行成功。

為了執(zhí)行該循環(huán)逾条,該腳本將保存一個(gè)index變量琢岩,在每次循環(huán)迭代中,它將試圖讓 syscall 獲取 cell 中目前采用的index值师脂,如果 syscall 返回 CKB_INDEX_OUT_OF_BOUND担孔,這意味著腳本已經(jīng)遍歷所有的 cell江锨,之后會(huì)退出循環(huán);否則糕篇,循環(huán)將繼續(xù)啄育,每測(cè)試 cell data 一次,index變量就會(huì)遞增一次拌消。

這是第一個(gè)有用的 CKB 腳本代碼挑豌!在下一節(jié)中,我們將看到我們是如何將其部署到 CKB 中并運(yùn)行它的墩崩。

將腳本部署到 CKB 上

首先氓英,我們需要編譯上面寫的關(guān)于胡蘿卜的源代碼。由于 GCC 已經(jīng)提供了 RISC-V 的支持鹦筹,您當(dāng)然可以使用官方的 GCC 來(lái)創(chuàng)建腳本代碼铝阐。或者你也可以使用我們準(zhǔn)備的 docker 鏡像來(lái)避免編譯 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í)行文件作為鏈上的腳本徘键,無(wú)需進(jìn)一步處理。我們現(xiàn)在可以在鏈上部署它了余舶。注意啊鸭,我將使用 CKB 的 Ruby SDK锹淌,因?yàn)槲以?jīng)是一名 Ruby 程序員匿值,當(dāng)然 Ruby 對(duì)我來(lái)說(shuō)是最自然的(但不一定是最好的)。如何設(shè)置請(qǐng)參考官方 Readme 文件赂摆。

要將腳本部署到 CKB挟憔,我們只需創(chuàng)建一個(gè)新的 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))

在這里烟号,我首先要通過(guò)向自己發(fā)送 token 來(lái)創(chuàng)建一個(gè)容量足夠的新的 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ù)迫筑,我們可以對(duì)args部分使用空數(shù)組宪赶。

注意,這里仍然忽略了 hash_type脯燃,我們將在后面的文章中通過(guò)另一種方式討論指定代碼哈下蓿。現(xiàn)在辕棚,讓我們盡量保持簡(jiǎn)單欲主。

要運(yùn)行胡蘿卜腳本邓厕,我們需要?jiǎng)?chuàng)建一個(gè)新的交易,并將胡蘿卜 type 腳本設(shè)置為其中一個(gè)輸出 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)行一個(gè)步驟:為了讓 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 中沒有任何一個(gè)的 cell data 包含carrot详恼,因此 type 腳本將驗(yàn)證成功。現(xiàn)在讓我們嘗試一個(gè)不同的交易引几,它確實(shí)含有一個(gè)以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 中包含胡蘿卜的交易。現(xiàn)在我可以使用這個(gè)腳本來(lái)確保所有的 cell 中都不含胡蘿卜!

所以她紫,總結(jié)一下硅堆,部署和運(yùn)行一個(gè) type 腳本的腳本,我們需要做的是:

  1. 將腳本編譯為 RISC-V 可執(zhí)行的二進(jìn)制文件
  2. 在 cell 的 data 部分部署二進(jìn)制文件
  3. 創(chuàng)建一個(gè) type 腳本數(shù)據(jù)結(jié)構(gòu)贿讹,使用二進(jìn)制文件的 blake2b 散列作為code hash渐逃,補(bǔ)齊args部分中腳本代碼的需要的參數(shù)
  4. 用生成的 cell 中設(shè)置的 type 腳本創(chuàng)建一個(gè)新的交易
  5. 將包含腳本代碼的 cell 的 outpoint 寫入到一筆交易的 deps 中去

這就是你所有需要的!如果您的腳本遇到任何問題民褂,您需要檢查這些要點(diǎn)茄菊。

雖然在這里我們只討論了 type 腳本,但是 lock 腳本的工作方式完全相同赊堪。您惟一需要記住的是面殖,當(dāng)您使用特定的 lock 腳本創(chuàng)建 cell 時(shí),lock 腳本不會(huì)在這里運(yùn)行哭廉,它只在您使用 cell 時(shí)運(yùn)行脊僚。因此, type 腳本可以用于構(gòu)造創(chuàng)建 cell 時(shí)運(yùn)行的邏輯遵绰,而 lock 腳本用于構(gòu)造銷毀 cell 時(shí)運(yùn)行的邏輯辽幌。考慮到這一點(diǎn)椿访,請(qǐng)確保您的 lock 腳本是正確的乌企,否則您可能會(huì)在以下場(chǎng)景中丟失 token:

您的 lock 腳本有一個(gè)其他人也可以解鎖您的 cell 的 bug。
您的 lock 腳本有一個(gè) bug成玫,任何人(包括您)都無(wú)法解鎖您的 cell加酵。

在這里我們可以提供的一個(gè)技巧是,始終將您的腳本作為一個(gè) type 腳本附加到你交易的一個(gè) output cell 中去進(jìn)行測(cè)試哭当,這樣猪腕,發(fā)生錯(cuò)誤時(shí),您可以立即知道荣病,并且您的 token 可以始終保持安全码撰。

分析默認(rèn) lock 腳本代碼

根據(jù)已經(jīng)掌握的知識(shí),讓我們看看 CKB 中包含的默認(rèn)的 lock 腳本代碼个盆。 為了避免混淆脖岛,我們正在查看 lock 腳本代碼在 這個(gè)commit朵栖。

默認(rèn)的 lock 腳本代碼將循環(huán)遍歷與自身具有相同 lock 腳本的所有的 input cell,并執(zhí)行以下步驟:

  • 它通過(guò)提供的 syscall 獲取當(dāng)前的交易 hash

  • 它獲取相應(yīng)的 witness 數(shù)據(jù)作為當(dāng)前輸入

  • 對(duì)于默認(rèn) lock 腳本柴梆,假設(shè) witness 中的第一個(gè)參數(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)中的第一個(gè)參數(shù)提供了實(shí)際的簽名。

  • 如果簽名驗(yàn)證失敗偿渡,腳本退出并返回錯(cuò)誤碼臼寄。否則它將繼續(xù)下一個(gè)迭代。

注意溜宽,我們?cè)谇懊嬗懻摿四_本和腳本代碼之間的區(qū)別吉拳。每一個(gè)不同的公鑰 hash 都會(huì)產(chǎn)生不同的 lock 腳本,因此适揉,如果一個(gè)交易的輸入 cell 具有相同的默認(rèn) lock 腳本代碼留攒,但具有不同的公鑰 hash(因此具有不同的 lock 腳本),將執(zhí)行默認(rèn) lock 腳本代碼的多個(gè)實(shí)例嫉嘀,每個(gè)實(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部分, 它們通過(guò) Unix 傳統(tǒng)的arc/argv方式發(fā)送給實(shí)際運(yùn)行的腳本程序剪侮。為了進(jìn)一步保持約定拭宁,我們?cè)?code>argv[0] 處插入一個(gè)偽參數(shù),所以 第一個(gè)包含的參數(shù)從argv[1]開始票彪。在默認(rèn) lock 腳本代碼的情況下红淡,它接受一個(gè)參數(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;
}

使用與胡蘿卜這個(gè)例子相同的技術(shù),我們檢查是否有更多的輸入 cell 要測(cè)試摇零。與之前的例子有兩個(gè)不同:

  • 如果我們只想知道一個(gè) cell 是否存在并且不需要任何數(shù)據(jù)推掸,我們只需要傳入NULL 作為數(shù)據(jù)緩沖區(qū),一個(gè) len 變量的值是 0驻仅。
    通過(guò)這種方式谅畅,syscall 將跳過(guò)數(shù)據(jù)填充,只提供可用的數(shù)據(jù)長(zhǎng)度和正確的返回碼用于處理噪服。

  • 在這個(gè) carrot 的例子中毡泻,我們循環(huán)遍歷交易中的所有輸入, 但這里我們只關(guān)心具有相同 lock 腳本的輸入cell粘优。 CKB將具有相同鎖定(或類型)腳本的cell命名為group仇味。 我們可以使用 CKB_SOURCE_GROUP_INPUT 代替 CKB_SOURCE_INPUT呻顽, 來(lái)表示只計(jì)算同一組中的 cell,舉個(gè)例子丹墨,即具有與當(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ù)沿著這個(gè)路徑,我們正在加載當(dāng)前輸入的 witness贩挣。 對(duì)應(yīng)的 witness 和輸入具有相同的索引『砬埃現(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 中的第一個(gè)參數(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论颅。否則,可能會(huì)有人使用另一個(gè)公鑰生成的簽名來(lái)竊取你的 token囱嫩。

簡(jiǎn)而言之恃疯,默認(rèn) lock 腳本中使用的方案與現(xiàn)在比特幣中使用的方案非常相似。

介紹 Duktape

我相信你和我現(xiàn)在的感覺一樣: 我們可以用 C 語(yǔ)言寫合約墨闲,這很好今妄,但是 C 語(yǔ)言總是讓人覺得有點(diǎn)乏味,而且鸳碧,讓我們面對(duì)現(xiàn)實(shí)盾鳞,它很危險(xiǎn)。
有更好的方法嗎?

當(dāng)然瞻离! 我們上面提到的 CKB VM 本質(zhì)上是一臺(tái)微型計(jì)算機(jī)腾仅,我們可以探索很多解決方案。 我們?cè)谶@里做的一件事是套利,使用 JavaScript 編寫 CKB 腳本代碼推励。 是的,你說(shuō)對(duì)了肉迫,簡(jiǎn)單的 ES5 (是的验辞,我知道,但這只是一個(gè)例子喊衫,你可以使用轉(zhuǎn)換器) JavaScript跌造。

這怎么可能呢? 由于我們有 C 編譯器,我們只需為嵌入式系統(tǒng)使用一個(gè) JavaScript 實(shí)現(xiàn),在我們的例子中族购,duktape 將它從 C 編譯成 RISC-V 二進(jìn)制文件壳贪,把它放在鏈上陵珍,我們就可以在 CKB 上運(yùn)行 JavaScript 了!因?yàn)槲覀兪褂玫氖且慌_(tái)真正的微型計(jì)算機(jī),所以沒有什么可以阻止我們將另一個(gè) VM 作為 CKB 腳本嵌入到 CKB VM 中撑碴,并在 VM 路徑上探索這個(gè) VM撑教。

從這條路徑展開,我們可以通過(guò) duktape 在 CKB 上使用 JavaScript醉拓,我們也可以通過(guò) mruby在 ckb 上使用 Ruby伟姐, 我們甚至可以將比特幣腳本或EVM放到鏈上,我們只需要編譯他們的虛擬機(jī)亿卤,并把它放在鏈上愤兵。這確保了 CKB VM 既能幫助我們保存資產(chǎn),又能構(gòu)建一個(gè)多樣化的生態(tài)系統(tǒng)排吴。所有的語(yǔ)言都應(yīng)該在 CKB 上被平等對(duì)待秆乳,自由應(yīng)該掌握在區(qū)塊鏈合約的開發(fā)者手中。

在這個(gè)階段钻哩,你可能想問: 是的屹堰,這是可能的,但是 VM 之上的 VM 不會(huì)很慢嗎? 我相信這取決于你的例子是否很慢街氢。我堅(jiān)信扯键,基準(zhǔn)測(cè)試沒有任何意義,除非我們將它放在具有標(biāo)準(zhǔn)硬件需求的實(shí)際用例中珊肃。 所以我們需要有時(shí)間檢驗(yàn)這是否真的會(huì)成為一個(gè)問題荣刑。 在我看來(lái),高級(jí)語(yǔ)言更可能用于 type scripts 來(lái)保護(hù) cell 轉(zhuǎn)換伦乔,在這種情況下厉亏,我懷疑它會(huì)很慢。此外烈和,我們也在這個(gè)領(lǐng)域努力工作爱只,以優(yōu)化 CKB VM 和 VMs 之上的 CKB VM,使其越來(lái)越快斥杜,: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)在需要一個(gè)參數(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)建一個(gè) 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日志模塊的級(jí)別設(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 上部署了一個(gè) JavaScript 引擎宣赔,并在 CKB 上運(yùn)行基于 JavaScript 的腳本!

你可以在這里嘗試認(rèn)識(shí)的 JavaScript 代碼。

一道思考題

現(xiàn)在你已經(jīng)熟悉了 CKB 腳本的基礎(chǔ)知識(shí)瞪浸,下面是一個(gè)思考:
在本文中儒将,您已經(jīng)看到了一個(gè) always-success 的腳本是什么樣子的,但是一個(gè) always-failure 的腳本呢?一個(gè) always-failure 腳本(和腳本代碼)能有多小?

提示:這不是 gcc 優(yōu)化比賽对蒲,這只是一個(gè)思考钩蚊。

下集預(yù)告

我知道這是一個(gè)很長(zhǎng)的帖子,我希望你已經(jīng)嘗試過(guò)蹈矮,并成功地部署了一個(gè)腳本到 CKB砰逻。在下一篇文章中,我們將介紹一個(gè)重要的主題:如何在 CKB 定義自己的用戶定義 token(UDT)泛鸟。CKB 上 udt 最好的部分是蝠咆,每個(gè)用戶都可以將自己的 udt 存儲(chǔ)在自己的 cell 中,這與 Ethereum 上的 ERC20 令牌不同北滥,在 Ethereum 上刚操,每個(gè)人的 token 都必須位于 token 發(fā)起者的單個(gè)地址中。所有這些都可以通過(guò)單獨(dú)使用 type script 來(lái)實(shí)現(xiàn)再芋。

如果你感興趣菊霜,請(qǐng)繼續(xù)關(guān)注 :)

加入 Nervos Community

Nervos Community 致力于成為最好的 Nervos 社區(qū),我們將持續(xù)地推廣和普 及 Nervos 技術(shù)祝闻,深入挖掘 Nervos 的內(nèi)在價(jià)值占卧,開拓 Nervos 的無(wú)限可能, 為每一位想要深入了解 Nervos Network 的人提供一個(gè)優(yōu)質(zhì)的平臺(tái)联喘。

添加微信號(hào):BitcoinDog 即可加入 Nervos Community华蜒,如果是程序員請(qǐng)備注,還會(huì)將您拉入開發(fā)者群豁遭。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末叭喜,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子蓖谢,更是在濱河造成了極大的恐慌捂蕴,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,378評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件闪幽,死亡現(xiàn)場(chǎng)離奇詭異啥辨,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)盯腌,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門溉知,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事级乍∩嗬停” “怎么了?”我有些...
    開封第一講書人閱讀 152,702評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵玫荣,是天一觀的道長(zhǎng)甚淡。 經(jīng)常有香客問我,道長(zhǎng)捅厂,這世上最難降的妖魔是什么贯卦? 我笑而不...
    開封第一講書人閱讀 55,259評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮恒傻,結(jié)果婚禮上脸侥,老公的妹妹穿的比我還像新娘。我一直安慰自己盈厘,他們只是感情好睁枕,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,263評(píng)論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著沸手,像睡著了一般外遇。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上契吉,一...
    開封第一講書人閱讀 49,036評(píng)論 1 285
  • 那天跳仿,我揣著相機(jī)與錄音,去河邊找鬼捐晶。 笑死菲语,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的惑灵。 我是一名探鬼主播山上,決...
    沈念sama閱讀 38,349評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼英支!你這毒婦竟也來(lái)了佩憾?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,979評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤干花,失蹤者是張志新(化名)和其女友劉穎妄帘,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體池凄,經(jīng)...
    沈念sama閱讀 43,469評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡抡驼,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,938評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了肿仑。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片婶恼。...
    茶點(diǎn)故事閱讀 38,059評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡桑阶,死狀恐怖柏副,靈堂內(nèi)的尸體忽然破棺而出勾邦,到底是詐尸還是另有隱情,我是刑警寧澤割择,帶...
    沈念sama閱讀 33,703評(píng)論 4 323
  • 正文 年R本政府宣布眷篇,位于F島的核電站,受9級(jí)特大地震影響荔泳,放射性物質(zhì)發(fā)生泄漏蕉饼。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,257評(píng)論 3 307
  • 文/蒙蒙 一玛歌、第九天 我趴在偏房一處隱蔽的房頂上張望昧港。 院中可真熱鬧,春花似錦支子、人聲如沸创肥。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)叹侄。三九已至,卻和暖如春昨登,著一層夾襖步出監(jiān)牢的瞬間趾代,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工丰辣, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留撒强,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,501評(píng)論 2 354
  • 正文 我出身青樓笙什,卻偏偏與公主長(zhǎng)得像飘哨,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子得湘,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,792評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容