開發者如何利用 CKB-VM 進行智能合約開發

Nervos 底層公鏈 CKB 的虛擬機(CKB-VM)是基於 RISC-V 打造的區塊鏈虛擬機。在前三節課中,我們介紹了 CKB 虛擬機的設計理念及優勢。那麼,怎樣才能利用 CKB-VM 更好的開發呢?本文是實現 CKB 背後腳手架-技術系列的最後一篇文章,CKB-VM 設計者肖雪潔會以三種不同的方式展示 CKB-VM 的合約示例,它會幫助你更好的在 CKB-VM 上玩耍~

祕猿科技區塊鏈小課堂第 24 期


最簡化智能合約示例

以下代碼示例爲可以在 CKB-VM 上運行的最簡化智能合約:

int main()
{
 return 0;
}

以下代碼可以通過 GCC 編譯:

riscv64-unknown-elf-gcc main.c -o main

CKB 的智能合約是一個遵循傳統 Unix 調用方式的二進制文件。可以通過 argc/argv 輸入參數,以 main 函數的返回值來表示輸出結果。

若返回值爲 0 表示合約調用成功,返回值爲其它表示合約調用失敗。

爲了簡化說明,我們以 C 語言爲例來實現示例中的合約。但實際上任何可以編譯成 RISC-V 指令集的語言均可以直接用來開發 CKB 的智能合約

  • 最新版的 Rust Stable 已經有 RISC-V
  • Go 語言的 RISC-V 支持也在開發中:
  • 對於更高級的語言,我們可以直接將其 C 語言的實現編譯爲 RISC-V 二進制文件,並通過 「VM 中的 VM」 技術,在 CKB 上啓用以這些語言編寫的智能合約。舉個例子,我們可以將 mruby 編譯爲 RISC-V 二進制文件,來啓用基於 Ruby 的合約開發。基於 MicroPython 的 Python 語言或基於 Duktape 的 JavaScript 語言也可以使用同樣的方式,在 CKB 上開發智能合約。

即使是編譯爲 EVM 字節碼或 Bitcoin 腳本的智能合約也可以編譯爲 CKB-VM 字節碼。當然我們可以清晰地看到這些傳統合約遷移到更有效字節碼上的優勢,並且,與使用較低級編程語言實現的智能合約相比,在 CKB 上的這些合約可能具有更大的運行開銷(CPU cycles),但是對於一些不同的應用場景來說,這裏節省下來的開發時間以及安全性優勢,可能比在運行開銷更有價值。

CKB 還滿足了哪些需求?

除了最簡化的合約之外,CKB 也會提供一個系統庫來滿足如下需求:

  • 支持 libc 核心庫;
  • 支持動態鏈接,以減少當前合約佔用的空間,比如可以通過動態鏈接在 VM 中加載 system Cell 的方式來調用庫;
  • 讀取交易數據後,CKB-VM 中會有類似比特幣的 SIGHASH 功能,以最大限度地提高合約的靈活性。

下圖顯示了基於前面系統庫的 CKB 智能合約驗證模型:

圖片描述

如上圖所示,CKB 的交易由 Input 和 Output 構成。雖然交易也可能包含 Deps(包含運行合約時所需的數據或代碼的依賴項),但它們僅會影響智能合約的實現,並且會從交易模型中刪除。

交易的每個 Input 都會引用一個現有 Cell,一筆交易可以覆蓋、銷燬或生成一個 Cell。共識規則強制規定交易中所有 Output Cell 的容量總和不能超過所有 Input Cell 的容量總和。

驗證智能合約的標準

CKB-VM 使用以下標準來驗證智能合約:

  • 每個 Input 中都會包含一個解鎖腳本(Unlock Script),用於驗證交易發起者是否可以使用當前 Input 所引用到的 Cell。Unlock Script 中包含由交易發起者生成的簽名,且 Unlock Script 通常會有指定的簽名算法(如 SIGHASH-ALL-SHA3-SECP256K1)。CKB 會通過 VM 運行 Unlock Script 進行驗證:智能合約會通過一個 API 來讀取交易數據以實現 SIGHASH 相關的計算,從而提供最大的靈活性。
  • 每個 Cell 中包含一個驗證腳本,用於驗證當前 Cell Data 是否滿足先前指定的條件,例如我們可以創建一個 Cell 來保存用戶自定義代幣(User Defined Token,簡稱 UDT)。在 Cell 創建完畢後,我們需要驗證 Input Cell 中所有 Token 總和是否大於或等於 Output Cell 中所有 Token 的總和,以確保交易中不會生成新的 Token。爲增強安全性,CKB 的合約開發人員還可以利用特殊合約來確保創建 Cell 後,Cell 的驗證腳本不會被修改。

Input Cell 驗證示例

基於上述對 CKB-VM 安全模型的描述,我們可以首先實現一個完整的 SIGHASH-ALL-SHA3-SECP256K1 合約,來驗證所提供的簽名以及驗證提供簽名的交易發起者是否可以使用當前的 Cell:

// For simplicity, we are keeping pubkey in the contract, however this
// solution has a potential problem: even though many contracts might share
// the very same structure, keeping pubkey here will make each contract
// quite different, preventing common contract sharing. In CKB we will
// provide ways to share common contract while still enabling each user
// to embed their own pubkey.
char* PUBKEY = "this is a pubkey";
int main(int argc, char* argv[])
{
 // We need 2 arguments for this contract
 // * The first argument is contract name, this is for compatibility issue
 // * The second argument is signature for current contract input
 if (argc < 2) {
   return -1;
 }
// This function loads current transaction into VM memory, and returns the
 // pointer to serialized transaction data. Notice ckb_mmap might preprocess
 // the transaction a bit, such as removing signatures in all tx inputs to
 // avoid chicken-egg problem when signing signature.
 int length = 0;
 char* tx = ckb_mmap(CKB_TX, &length);
 if (tx == NULL) {
   return -2;
 }
// This function dynamically links sha3 library to current VM memory space
 void *sha3_handle = ckb_dlopen("sha3");
 void (*sha3_func)(const char*, int, char*) = ckb_dlsym("sha3_256");
// Here we run sha3 on all the transaction data, simulating a SIGHASH_ALL process,
 // a different contract might choose to deserialize and only hash certain part
 // of the transaction
 char hash[32];
 sha3_func(tx, length, hash);
// Now we load secp256k1 module.
 void *secp_handle = ckb_dlopen("secp256k1");
 int (*secp_verify_func)(const char*, int, const char*, int, const char*, int) =
     ckb_dlsym("secp256k1_verify");
int result = secp_verify_func(argv[1], strlen(argv[1]),
                               PUBKEY, strlen(PUBKEY),
                               hash, 32);
if (result == 1) {
   // Verification success, we are returning 0 to indicate contract succeeds
   return 0;
 } else {
   // Verification failure
   return -3;
 }
}

在此示例中,我們首先將所有的交易數據讀入 CKB-VM 中以獲取交易數據的 SHA3 哈希,並將此交易數據的 SHA3 哈希、指定的公鑰和交易發起者提供的簽名提供給 secp256k1 模塊,以驗證合約中指定的公鑰是否已對提供的交易數據進行了簽名。

如果此驗證成功,則交易發起者可以使用當前 Input 引用的 Cell,合約成功執行。 如果此驗證不成功,則合約執行和交易驗證會失敗。

用戶自定義代幣(UDT)示例

下面的示例中,演示了一個 Cell 驗證腳本實現類似 ERC-20 用戶自定義代幣的過程。Cell 驗證腳本也可以實現其他 UDT 功能,爲簡單起見,以下示例中僅包含 UDT 的轉移功能:

int main(int argc, char* argv[]) {
 size_t input_cell_length;
 void* input_cell_data = ckb_mmap_cell(CKB_CELL_INPUT, 0, &input_cell_length);
size_t output_cell_length;
 void* output_cell_data = ckb_mmap_cell(CKB_CELL_OUTPUT, 0, &output_cell_length);
if (input_cell_data == NULL || output_cell_data == NULL) {
   return -1;
 }
void* udt_handle = ckb_dlopen("udt");
 data_t* (*udt_parse)(const char*, size_t) =
     ckb_dlsym(udt_handle, "udt_parse");
 int (*udt_transfer)(data_t *, const char*, const char*, int64_t) =
     ckb_dlsym(udt_handle, "udt_transfer");
data_t* input_cell = udt_parse(input_cell_data, input_cell_length);
 data_t* output_cell = udt_parse(output_cell_data, output_cell_length);
if (input_cell == NULL || output_cell == NULL) {
   return -2;
 }
ret = udt_transfer(input_cell, from, to, tokens);
 if (ret != 0) {
   return ret;
 }
int (*udt_compare)(const data_t *, const data_t *);
 if (udt_compare(input_cell, output_cell) != 0) {
   return -1;
 }
 return 0;
}

在這段代碼中,首先我們通過調用系統庫讀取了 Input 與 Output Cell 中的內容,然後我們動態加載了 UDT 的實現,並使用轉移方式對 Input 進行轉換。

轉換後,Input 與 Output Cell 中的內容應該完全匹配,否則我們得到的驗證結果會是:當前交易不符合驗證腳本中指定的條件,合約執行即爲失敗。

注意:以上示例僅用於展示 CKB-VM 的功能,並不代表此實現方式爲 CKB 上 UDT 實現的最佳實踐。

在 Ruby 中的 Unlock Script 示例

雖然上面的示例都是通過 C 語言來編寫的,但是實際上,CKB-VM 上編寫智能合約並不僅限於用 C 語言。例如我們可以將 mruby 這個針對嵌入式平臺的 Ruby 實現編譯爲 RISC-V 二進制文件,並以它作爲通用系統庫,這樣我們就可以使用 Ruby 在 CKB 上編寫智能合約。Unlock Script 示例如下:

if ARGV.length < 2
 raise "Not enough arguments!"
end
tx = CKB.load_tx
sha3 = Sha3.new
sha3.update(tx["version"].to_s)
tx["deps"].each do |dep|
 sha3.update(dep["hash"])
 sha3.update(dep["index"].to_s)
end
tx["inputs"].each do |input|
 sha3.update(input["hash"])
 sha3.update(input["index"].to_s)
 sha3.update(input["unlock"]["version"].to_s)
 # First argument here is signature
 input["unlock"]["arguments"].drop(1).each do |argument|
   sha3.update(argument)
 end
end
tx["outputs"].each do |output|
 sha3.update(output["capacity"].to_s)
 sha3.update(output["lock"])
end
hash = sha3.final
pubkey = ARGV[0]
signature = ARGV[1]
unless Secp256k1.verify(pubkey, signature, hash)
 raise "Signature verification error!"
End

社區驅動的 CKB-VM

以上爲 CKB-VM 上三種不同方式的智能合約實現示例,它們也許會幫助你更好的在 CKB-VM 上玩耍。通過 CKB-VM 的設計,我們的目標是建立一個圍繞 CKB 的社區,該社區可以自由地發展和適應新技術的進步,並且可以最大限度地減少人工干預(例如硬分叉)。 我們相信 CKB-VM 可以實現這一願景。

注:CKB-VM 與 CKB 一樣爲開源項目,目前 CKB-VM 仍在開發過程中,儘管 CKB-VM 的大部分設計已經敲定,但某些設計也可能會在將來由於你的加入而有新的進步。這篇文章是爲了讓我們的社區更加了解 CKB-VM,這樣人人都可以在裏面更好的玩耍並做出貢獻啦!

CKB-VM:https://github.com/nervosnetw...

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章