CKB 腳本編程簡介第四彈:在 CKB 上實現 WebAssembly

CKB 腳本編程簡介第四彈:在 CKB 上實現 WebAssembly | 火星技術帖

免責聲明:本文旨在傳遞更多市場信息,不構成任何投資建議。文章僅代表作者觀點,不代表火星財經官方立場。

Xuejie 是 CKB-VM的核心開發者,他在自己的博客「Less is More」中,創作了一系列介紹 CKB 腳本編程的文章,用於補充白皮書中編寫 CKB 腳本所需的所有缺失的細節實現。本文是該系列的第四篇,詳細介紹瞭如何在 CKB 中實現 WebAssembly,是不是很酷 快來一起玩耍吧 ~^.^~

https://xuejie.space/2019_10_09_introduction_to_ckb_script_programming_wasm_on_ckb/

自從我們選擇使用 RISC-V構建CKB-VM(Virtual Machine 虛擬機)以來,我們幾乎每一天都會被問及這樣一個問題:為什麼不像別人那樣在 WebAssembly 上構建你的虛擬機呢?

在這個選擇的背後其實有非常多的原因,可能需要另一篇文章或者一次演講才能解釋完其中的原因。從根本上來說,有一個相當重要的原因:構建軟件最主要的就是找到正確的抽象概念,我們相信對於無需許可的區塊鏈而言 RISC-V 比 WebAssembly 是一個更好抽象概念。

當然 WebAssembly 相比於其他更高級的編程語言和第一代區塊鏈虛擬機而言已經是一個巨大的進步了,但 RISC-V 的運行級別還要比 WebAssembly 低得多,這使得它非常適合用於那些希望未來在運行幾十年的公有鏈。

但還有一個問題沒有得到回答:目前區塊鏈行業很大一部分人都押注在 WebAssembly 上,(可以說) 基於 WebAssembly 的 dapps 構建了一個很好的生態系統。那麼 CKB 如何與之競爭呢?正如上面所提到的,RISC-V 實際上位於一個比 WebAssembly 更低的抽象級別上,我們可以移植現有的 WebAssembly 的程序,並直接在 CKB-VM 上運行他們。通過這種方式,我們既可以享受 RISC-V 提供的靈活性和穩定性,同時也可以擁抱 WebAssembly 的生態系統。

在本文中,我們將展示如何在 CKB-VM 中運行 WebAssembly 程序,我們還會展示通過這種方式運行(程序),會比直接使用 WebAssembly VM 具有更多的優勢。

就我個人而言,雖然我相信 WebAssembly 會有一些有趣的特性來支持不同的用例,但是我不相信 WebAssembly 會在區塊鏈領域創建一個更好的生態。環顧四周,在基於 WebAssembly 的區塊鏈中構建 DApp 可能只有兩種成熟的選擇:Rust 和 AssemblyScript。

人們一直在吹噓 WebAssembly 在單個抽象的 VM 中支持任意語言的能力(我個人拒絕將WebAssembly 稱為 low-level 的虛擬機),但是在這裡創建一個真正的 DApp,只能在這兩種語言內選擇其一。我認為,如果將僅支持 2 種的編程語言的虛擬機稱為良好的 VM 生態系統,那麼我們可能會有不同的定義。當然還有一些其他語言也在迎頭趕上,但它們還沒有穩定到可以被認為是一個繁榮的生態系統的階段。雖然一些有趣的語言在基於 WebAssembly 的環境中具有潛力,但是還沒有人注意到,並去支持它們。

如果你仔細觀察,就會發現,使用 WebAssembly 的兩個不同的區塊鏈項目之間是否可以彼此共享合約,這目前仍然是個問題。當然,有人可能會說:「嗯,這只是一個時間問題,隨著時間的推移,更有活力的 WebAssembly 生態系統將會發芽。」但同樣的論點也適用於任何地方:為什麼隨著時間的推移,RISC-V 的生態系統不會變得更好?

咆哮到此為止,現在我們只是假設,WebAssembly 確實擁有一個區塊鏈生態系統,我們可以證明,其中兩個被廣泛使用的語言:AssemblyScript 和 Rust,都可以在 CKB-VM 環境中得到支持。

我相信沒有比演示更能說明問題的了。所以,讓我們試試官方的 AssemblyScript,然後在 CKB 上運行編譯好的程序。我們將只使用 AssemblyScript 簡介頁面中的官方示例:

<code>$ cat fib.ts/<code><code>export function fib(n: i32): i32 {/<code><code>  var a = 0, b = 1;/<code><code>    for (let i = 0; i /<code><code>        let t = a + b; a = b; b = t;/<code><code>  }/<code><code>  return b;/<code><code>}/<code>

關於如何安裝,請參考 AssemblyScripts 的文檔。為了方便起見,我提供了一些步驟,您可以在這裡複製粘貼。

<code>$ git clone https://github.com/AssemblyScript/assemblyscript.git/<code><code>$ cd assemblyscript/<code><code>$ npm install/<code><code>$ bin/asc ../fib.ts -b ../fib.wasm -O3/<code><code>$ cd ../<code>

這樣我們就有了一個編譯好的 WebAssembly 程序,我們可以調用一個名為 wasm2c 的程序將它編譯成 C 語言的源文件,然後通過 RISC-V 編譯器將它編譯成 RISC-V 程序,並在 CKB-VM 上運行。

我敢肯定你會說:這是一個黑客行為!它這裡對 WASM 程序進行了反編譯,然後使它可以運行,你這是在作弊。這個問題的答案是,是但又不是:

  • 一方面,我是在作弊,但我要提出的問題是:我們應該關心的是最終的結果,如果結果足夠好,我們為什麼要關心這是否是作弊呢?另外,現代編譯器已經足夠複雜了,就像一個完全的黑盒,我們怎麼能確定這個反編譯會得到更糟糕的結果呢?

  • 另一方面,這只是將 WebAssembly 轉換為 RISC-V 的一種方法。還有許多其他方法都可以實現相同的結果。我們將在後面的重述部分再次討論這一點。

啟動 <code>wasm2c/<code>然後轉換 WebAssembly 程序:

<code>$ git clone --recursive https://github.com/WebAssembly/wabt/<code><code>$ cd wabt/<code><code>$ mkdir build/<code><code>$ cd build/<code><code>$ cmake ../<code><code>$ cmake --build ./<code><code>$ cd ../../<code><code>$ wabt/bin/wasm2c fib.wasm -o fib.c/<code>

您將在當前目錄中看到一對 <code>fib.c/<code>和<code>fib.h/<code>文件,它們包含 WebAssembly 程序轉換的結果,當編譯和調用正確時,它們將實現與 WebAssembly 程序相同的功能。我們可以使用一個小的包裝器 C 文件來調用 WebAssembly 程序:

<code>$ cat main.c/<code><code>#include /<code><code>#include /<code>
<code>#include "fib.h"/<code>

<code>int main(int argc, char** argv) {/<code><code> if (argc 2) return 2;/<code>
<code> u8 x = atoi(argv[1]);/<code>
<code> init;/<code>
<code> u8 result = Z_fibZ_ii(x);/<code>
<code> return result;/<code><code>}/<code>

這只是從 CLI 參數中讀取一個整數,在 WebAssembly 程序中調用 Fibonacci 函數,然後返回結果。讓我們先來編譯它:

<code>$ sudo docker run --rm -it -v `pwd`:/code nervos/ckb-riscv-gnu-toolchain:xenial bash/<code><code>(docker) $ cd /code/<code><code>(docker) $ riscv64-unknown-elf-gcc -o fib_riscv64 -O3 -g main.c fib.c /code/wabt/wasm2c/wasm-rt-impl.c -I /code/wabt/wasm2c/<code><code>/riscv/lib/gcc/riscv64-unknown-elf/8.3.0/../../../../riscv64-unknown-elf/bin/ld: /tmp/ccfUDYhE.o: in function `__retain':/<code><code>/code/fib.c:1602: undefined reference to `Z_envZ_abortZ_viiii'/<code><code>/riscv/lib/gcc/riscv64-unknown-elf/8.3.0/../../../../riscv64-unknown-elf/bin/ld: /tmp/ccfUDYhE.o: in function `i32_load':/<code><code>/code/fib.c:42: undefined reference to `Z_envZ_abortZ_viiii'/<code><code>/riscv/lib/gcc/riscv64-unknown-elf/8.3.0/../../../../riscv64-unknown-elf/bin/ld: /tmp/ccfUDYhE.o: in function `f17':/<code><code>/code/fib.c:1564: undefined reference to `Z_envZ_abortZ_viiii'/<code><code>/riscv/lib/gcc/riscv64-unknown-elf/8.3.0/../../../../riscv64-unknown-elf/bin/ld: /code/fib.c:1564: undefined reference to `Z_envZ_abortZ_viiii'/<code><code>/riscv/lib/gcc/riscv64-unknown-elf/8.3.0/../../../../riscv64-unknown-elf/bin/ld: /tmp/ccfUDYhE.o: in function `f6':/<code><code>/code/fib.c:1011: undefined reference to `Z_envZ_abortZ_viiii'/<code><code>/riscv/lib/gcc/riscv64-unknown-elf/8.3.0/../../../../riscv64-unknown-elf/bin/ld: /tmp/ccfUDYhE.o:/code/fib.c:1012: more undefined references to `Z_envZ_abortZ_viiii' follow/<code><code>collect2: error: ld returned 1 exit status/<code><code>(docker) $ exit/<code>

如上所示,這裡有一個報錯。它告訴我們有一個 <code>Z_ENVZ_ABORTZ_VIII/<code>函數沒有定義。讓我們深入瞭解為什麼會發生這種情況。首先,讓我們將原始的 WebAssembly 文件轉換為可讀的形式:

<code>$ wabt/bin/wasm2wat fib.wasm -o fib.wast/<code><code>$ cat fib.wast | grep "(import"/<code><code>(import "env" "abort" (func (;0;) (type 2)))/<code>

那麼問題來了,WebAssembly 可以導入外部函數,在調用的時候,提供了額外的功能,事實上,著名的 WASI 就是基於 <code>import/<code>功能實現的,後面我們會看到<code>import/<code>可以用來實現更多基於 WebAssembly 的區塊鏈虛擬機不可能實現的有趣功能。

現在,讓我們嘗試一個 abort 執行,來修復報錯:

<code>$ cat main.c/<code><code>#include /<code><code>#include /<code>
<code>#include "fib.h"/<code>
<code>void (*Z_envZ_abortZ_viiii)(u32, u32, u32, u32);/<code>
<code>void env_abort(u32 a, u32 b, u32 c, u32 d) {/<code><code> abort;/<code><code>}/<code>
<code>int main(int argc, char** argv) {/<code><code> if (argc 2) return 2;/<code>
<code> u8 x = atoi(argv[1]);/<code>
<code> Z_envZ_abortZ_viiii = &env_abort;/<code>
<code> init;/<code>
<code> u8 result = Z_fibZ_ii(x);/<code>
<code> return result;/<code><code>}/<code><code>$ sudo docker run --rm -it -v `pwd`:/code nervos/ckb-riscv-gnu-toolchain:xenial bash/<code><code>(docker) $ cd /code/<code><code>(docker) $ riscv64-unknown-elf-gcc -o fib_riscv64 -O3 -g main.c fib.c /code/wabt/wasm2c/wasm-rt-impl.c -I /code/wabt/wasm2c/<code><code>(docker) $ exit/<code>

當然,您可以在 CKB 上測試已編譯好的 <code>fib_riscv64/<code>程序。但是,有一個小技巧,在測試套件中有一個簡單的 CKB-VM 二進制文件,我們可以使用它來運行這個特定的程序。值得一提的是,這個 CKB-VM 二進制文件的工作方式與 CKB 中的 VM 略有不同。在當前示例中測試 WebAssembly 程序已經足夠了。但是為了測試正確的 CKB 腳本,您可能希望使用新構建的獨立調試器,它遵循所有 CKB 的語義。後面的文章將解釋調試器是如何工作的。讓我們在測試套件中編譯二進制文件並運行程序:

<code>$ git clone --recursive https://github.com/nervosnetwork/ckb-vm-test-suite/<code><code>$ cd ckb-vm-test-suite/<code><code>$ git clone https://github.com/nervosnetwork/ckb-vm/<code><code>$ cd binary/<code><code>$ cargo build --release/<code><code>$ cd ../../<code><code>$ ckb-vm-test-suite/binary/target/release/asm64 fib_riscv64 5/<code><code>Error result: Ok(8)/<code><code>$ ckb-vm-test-suite/binary/target/release/asm64 fib_riscv64 10/<code><code>Error result: Ok(89)/<code>

這裡的報錯稍微有點誤導,二進制將把程序中的任何非零結果都視為錯誤。由於測試的程序返回斐波那契計算結果作為返回值,二進制會把返回值(很可能不是零)視為錯誤,但是我們可以看到實際的錯誤值包含正確的斐波那契值。現在我們證明 AssemblyScript 程序確實可以在 CKB-VM 上工作!我確信更復雜的程序可能會遇到需要單獨調整的錯誤,但是您已經瞭解了整個流程,並且知道在發生錯誤時應該去哪裡查找 :)

Rust

我們已經在 AssemblyScript 部分看到了一些簡單的例子。讓我們在 Rust 部分嘗試一些更有趣的東西:我們可以在 Rust 代碼中實現一個完整的簽名驗證嗎?

事實證明我們可以!但這遠遠超出了我們在本博客文章中所能包含的內容。我已經準備好了一個演示項目來展示這一點。它用純 Rust 語言實現了使用 secp256k1 庫進行簽名驗證的過程。如果您按照文件中的說明進行操作,您應該可以重現以下具體步驟:

  • 將複雜的 Rust 程序編譯到 WebAssembly 中
  • 將 WebAssembly 程序轉換為 RISC-V
  • 在 CKB 虛擬機上運行生成的 RISC-V 程序

還有一件事我們需要提到:如果您檢查了 Rust secp256k1 演示庫的 <code>Bindgen/<code>分支,並嘗試相同的步驟,您將遇到以下錯誤:

<code>/riscv/lib/gcc/riscv64-unknown-elf/8.3.0/../../../../riscv64-unknown-elf/bin/ld: /tmp/ccYMiL3C.o: in function `core::result::unwrap_failed':/<code><code>/code/secp.c:342: undefined reference to `Z___wbindgen_placeholder__Z___wbindgen_describeZ_vi'/<code><code>/riscv/lib/gcc/riscv64-unknown-elf/8.3.0/../../../../riscv64-unknown-elf/bin/ld: /code/secp.c:344: undefined reference to `Z___wbindgen_placeholder__Z___wbindgen_describeZ_vi'/<code><code>/riscv/lib/gcc/riscv64-unknown-elf/8.3.0/../../../../riscv64-unknown-elf/bin/ld: /code/secp.c:344: undefined reference to `Z___wbindgen_placeholder__Z___wbindgen_describeZ_vi'/<code><code>/riscv/lib/gcc/riscv64-unknown-elf/8.3.0/../../../../riscv64-unknown-elf/bin/ld: /code/secp.c:347: undefined reference to `Z___wbindgen_placeholder__Z___wbindgen_describeZ_vi'/<code><code>/riscv/lib/gcc/riscv64-unknown-elf/8.3.0/../../../../riscv64-unknown-elf/bin/ld: /code/secp.c:350: undefined reference to `Z___wbindgen_placeholder__Z___wbindgen_describeZ_vi'/<code><code>/riscv/lib/gcc/riscv64-unknown-elf/8.3.0/../../../../riscv64-unknown-elf/bin/ld: /tmp/ccYMiL3C.o:/code/secp.c:353: more undefined references to `Z___wbindgen_placeholder__Z___wbindgen_describeZ_vi' follow/<code><code>/riscv/lib/gcc/riscv64-unknown-elf/8.3.0/../../../../riscv64-unknown-elf/bin/ld: /tmp/ccYMiL3C.o: in function `i32_store':/<code><code>/code/secp.c:56: undefined reference to `Z___wbindgen_anyref_xform__Z___wbindgen_anyref_table_set_nullZ_vi'/<code><code>/riscv/lib/gcc/riscv64-unknown-elf/8.3.0/../../../../riscv64-unknown-elf/bin/ld: /tmp/ccYMiL3C.o: in function `i32_load':/<code><code>/code/secp.c:42: undefined reference to `Z___wbindgen_anyref_xform__Z___wbindgen_anyref_table_set_nullZ_vi'/<code><code>/riscv/lib/gcc/riscv64-unknown-elf/8.3.0/../../../../riscv64-unknown-elf/bin/ld: /tmp/ccYMiL3C.o: in function `i32_store':/<code><code>/code/secp.c:56: undefined reference to `Z___wbindgen_anyref_xform__Z___wbindgen_anyref_table_set_nullZ_vi'/<code><code>/riscv/lib/gcc/riscv64-unknown-elf/8.3.0/../../../../riscv64-unknown-elf/bin/ld: /code/secp.c:56: undefined reference to `Z___wbindgen_anyref_xform__Z___wbindgen_anyref_table_set_nullZ_vi'/<code><code>/riscv/lib/gcc/riscv64-unknown-elf/8.3.0/../../../../riscv64-unknown-elf/bin/ld: /tmp/ccYMiL3C.o: in function `i32_load':/<code><code>/code/secp.c:42: undefined reference to `Z___wbindgen_anyref_xform__Z___wbindgen_anyref_table_growZ_ii'/<code><code>/riscv/lib/gcc/riscv64-unknown-elf/8.3.0/../../../../riscv64-unknown-elf/bin/ld: /code/secp.c:42: undefined reference to `Z___wbindgen_anyref_xform__Z___wbindgen_anyref_table_growZ_ii'/<code><code>collect2: error: ld returned 1 exit status/<code>

按照 AssemblyScript 示例中的相同步驟,我們可以在 WebAssembly 文件中進行某些 <code>imports/<code>:

<code>$ wabt/bin/wasm2wat wasm_secp256k1_test.wasm -o secp.wat/<code><code>$ cat secp.wat | grep "(import"/<code><code>(import "__wbindgen_placeholder__" "__wbindgen_describe" (func $__wbindgen_describe (type 3)))/<code><code>(import "__wbindgen_anyref_xform__" "__wbindgen_anyref_table_grow" (func $__wbindgen_anyref_table_grow (type 4)))/<code><code>(import "__wbindgen_anyref_xform__" "__wbindgen_anyref_table_set_null" (func $__wbindgen_anyref_table_set_null (type 3)))/<code>

這些實際上是在 Rust wasm-bindgen 中所需的綁定環境的函數。我們將繼續努力提供與 CKB 環境兼容的綁定。但是現在讓我們退一步想想:這裡所需要的環境功能並不是 WebAssembly 標準的一部分。標準中要求的是,當找不到導入條目時,WebAssembly VM 將停止執行,並出現錯誤。為了實現不同的特性,不同的基於 WebAssembly 的區塊鏈可能會在這裡注入不同的導入。使得編寫一個兼容不同區塊鏈的 WebAssembly 程序變得困難。然而,在 CKB 環境中,我們可以根據需要加入任何環境函數。因此,支持所有針對不同區塊鏈的 WebAssembly 程序。更重要的是,我們可以使用 imports 來為現有的 WebAssembly 程序引入新的特性。由於導入函數是與 WebAssembly 程序一起提供的,因此 CKB 本身不需要做任何事情來支持它。所有的奇蹟都發生在一個 CKB 腳本中。對於支持 WebAssembly 的區塊鏈,這些環境函數最有可能是固定的,並且是共識規則的一部分。你不能隨意引進新的。類似地,這種在 CKB 上基於轉換的流程,將使得支持新的 WebAssembly 功能(如垃圾收集或者線程處理)變得更加容易,這實際上只是將所需的支持功能寫進 CKB 腳本的問題,這意味著當 WebAssembly 虛擬機更新時,你不需要再等待 6 個月,等待進行下一個硬分叉後(才能實現這些新的功能)。

您可能會有一個疑問:「我明白了,您在 RISC-V 創建了 WebAssembly,但我也可以在 WebAssembly 再現 RISC-V!WebAssembly 是靈活的!」從某種意義上說,這是可行的,一旦一種語言或虛擬機超過了一定的靈活性,它就可以被用來構建許多(甚至是瘋狂的)東西。jslinux 的第一個版本,甚至可以用純 JavaScript 模擬了完整的 x86。但這個問題的另一面是,易於實施的。在 RISC-V 上構建 WebAssembly 感覺更加自然,因為 WebAssembly 被抽象到了更高的級別,具有許多高級特性。例如更高級別的控制流、垃圾回收等。另一方面,RISC-V 模擬了真正的 CPU 所能做的事情,它是在計算機內部運行的實際 CPU 之上的一個非常薄的層。因此,雖然這兩個方向確實都是可能的,但在 RISC-V 上搭建的 WebAssembly ,某些功能是更容易實現的。而在WebAssembly 上實現 RISC-V,可能會遇到很多問題。

一個可供選擇的例子是 EVM,EVM 多年來一直在提倡圖靈完備,但可悲的事實是,它幾乎不可能在 EVM 上構建任意複雜的算法:要麼是編碼部分太難,要麼是 gas 的消耗將是不合理的。人們不得不想出各種方法來介紹 EVM 上的最新算法,當伊斯坦布爾硬分叉完成時,我們只能在 EVM 中使用 Blake2b 算法。但是許多其他算法呢?所有這些都是我們選擇 RISC-V 的理由:我們希望在這一代的 CPU 架構上找到最小的一層,而 RISC-V 是我們在確保安全性和性能的同時,能夠在區塊鏈世界中找到的最顯然的可選擇模型。任何不同的模型,比如 WebAssembly、EVM 等,都應該是 RISC-V模型之上的一層,可以通過 RISC-V 模型自然地實現。然而,另一個方向可能根本不會讓人感覺如此順暢。

在這裡,我們演示了您可以在 CKB-VM 上運行 WebAssembly 程序,但我們想要指出的是,此流程並不是完全沒有問題。其中一個問題是性能,我們的初步測試表明,基於 WebAssembly 的 secp256k1 演示運行速度比直接編譯到 CKB-VM 的類似的基於 C 語言的實現慢了 30 倍。經過一些調查,我們認為這是由於以下問題:

  • 由於 WebAssembly 中內存的工作方式,wasm2c 必須首先將數據段放在純 C 數組的代碼中,然後在引導時,分配足夠的內存,然後執行字符串拷貝將數據複製到分配的內存中。對於 secp256k1 示例,這意味著程序的每次引導都必須複製 1MB 的預計算乘法表。結合我們的 RISC-V 程序現在使用 newlib 的事實,newlib 包含一個簡潔的 memcpy 實現,它針對代碼大小和速度進行了優化。這會大大降低程序的運行速度。
  • 雖然 wasm2c 可以為更簡單的程序提供良好的性能,但對於 secp256k1 這樣複雜且高度優化的算法,這意味著轉換層可能失去許多優化的可能,因此會比直接編譯到 RISC-V 的直接實現更慢。

幸運的是,這裡的問題是完全可以解決的。上面提到的工作流是我們可以將 WebAssembly 程序轉換為 RISC-V 程序的一種方式,但它絕對不是實現這一目標的唯一方式。就像我們上面提到的,轉換層阻礙了優化可能,如果我們引入現代編譯器來釋放這裡所有可能的優化呢?已經有人在通過 LLVM 將 WebAssembly 程序轉換為源代碼方面取得了進展。這裡實現的性能確實很好。由於 LLVM9 現在已經正式支持 RISC-V,因此完全可以更改代碼,通過 LLVM 直接生成 RISC-V 程序集,而不是 x86_64 程序集。這樣,我們就可以通過 LLVM 直接將 WebAssembly 程序轉換為 RISC-V 程序,享受 LLVM 在我們的代碼上執行的所有高級優化。因此,在這篇文章中我們提出的目前的解決方案,是完全可能的,同時為許多現有的案例實現足夠好的性能(例如,許多類型的腳本為了安全可以用 Rust 編寫,這樣性能就不再是一個大問題),這種新的 LLVM 解決方案可以在未來為相同的流程提供更好的性能。而實現這些,對我們而言只是時間問題。


分享到:


相關文章: