CKB 指令碼程式設計簡介第三彈:自定義代幣

買賣虛擬貨幣
Xuejie 是 CKB-VM 的核心開發者,他在自己的部落格「Less is More」中,創作了一系列介紹 CKB 指令碼程式設計的文章,用於補充白皮書中編寫 CKB 指令碼所需的所有缺失的細節實現。本文是該系列的第三篇,詳細介紹了靈活又好玩的自定義代幣,快來一起玩耍吧!作者:Xuejie原文連結:https://xuejie.space/2019_09_06_introduction_to_ckb_script_programming_udt/譯者:JoeyCKB 的 Cell 模型和 VM 支援許多新的用例。然而,這並不意味著我們需要拋棄現有的一切。如今區塊鏈中的一個常見用途是 Token 發行者釋出具有特殊目的/意義的新 Token。在以太坊中,我們稱之為 ERC20 Token,下面讓我們看看我們如何在 CKB 中構建類似的概念。為了與 ERC20 區分,在 CKB中的 Token 我們稱之為 user defined token,簡稱 UDT。
本文使用 CKB v0.20.0 版本來演示。具體來說,我會在每個專案中使用以下提交的版本:· ckb: 472252ac5333b2b19ea3ec50d54e68b627bf6ac5· ckb-duktape: 55849c20b43a212120e0df7ad5d64b2c70ea51ac· ckb-sdk-ruby: 1c2a3c3f925e47e421f9e3c07164ececf3b6b9f6資料模型以太坊會為每個合約賬戶提供單獨的儲存空間,CKB 與之不同,CKB 是在多個 Cell 之間傳遞資料。Cell 的 Lock Sript 和 Type Sript 會標明 Cell 屬於哪個帳戶,以及如何與 Cell 進行互動。其結果是,CKB 不會像 ERC20 那樣將所有 Token 使用者的餘額儲存在 ERC20 合約的儲存空間中,在 CKB 中,我們需要一種新的設計來儲存 UDT 使用者的餘額。
當然,我們也可以構造一個特殊的 Cell 來儲存所有 UDT 使用者的餘額。這個解決方案看起來很像以太坊的 ERC20 設計。但是這中間存在幾個問題:· Token 的發行者必須提供足夠的儲存空間以儲存所有使用者的餘額。隨著使用者數量的增長,儲存空間也將增長,這在 CKB 的經濟模型中,不是一個高效的設計。· 考慮到 CKB 中 Cell 的更新實際上是在銷燬舊 Cell 並重新生成新的 Cell ,因此儲存所有餘額的單個 Cell 會遇到一個困境:一旦更新 UDT 餘額那就不得不更新這一個且是唯一的 Cell,而且每一步的操作都需要更新, 那麼使用者在使用過程中將會產生衝突。雖然有一些解決方案確實可以緩解甚至能解決上述問題,但我們還是開始質疑這裡的基本設計:將所有 UDT 儲存在一個地方真的有意義嗎?一旦轉賬,UDT 應該真的屬於收款人,為什麼餘額仍然留在一箇中心呢?這引出了我們提出的設計:一個特殊的 Type Script 表示此 Cell 儲存 UDT;
Cell 資料的前 4 個位元組包含當前 Cell 中的 UDT 數量。這種設計有幾個含義:· UDT Cell 的儲存成本始終是恆定的,與儲存在 Cell 中的 UDT 數量無關;· 使用者可以將 Cell 中的全部或部分 UDT 轉賬給其他人;· 實際上,可能有許多 Cell 包含相同的 UDT;· 用於保護 UDT 的 Lock Script 與 UDT 本身分離。
每個 Token 使用者將其 UDT 儲存在自己的 Cell 中。他們負責為 UDT 提供儲存空間,並確保他們自己的 Token 是安全的。這樣 UDT 就可以真正屬於每個 UDT 使用者。但這裡還有一個問題:如果 Token 儲存在屬於各個使用者的眾多不同 Cell 中,而不是統一儲存,我們如何確定這個 Token 確實由這個發行者發行呢?如果有人自己偽造 Token 怎麼辦?在以太坊中,這可能是一個問題,但正如我們將在本文中看到的,CKB 中的 Type Script 可以防止所有這些攻擊,確保 Token 是安全的。編寫 UDT 指令碼鑑於上述設計,最小 UDT Type Script 應該遵循以下規則:· 在一個 UDT 轉賬交易中,輸出 Cell 中的 UDT 總和應等於輸入 Cell 中 UDT 的總和;· 只有發行者可以在初始Token建立過程中生成新的 Token。
這可能聽起來有點狂妄,但我們會證明,透過 Type Script 和 CKB 獨特的設計模式,一切都可以搞定:P為簡單起見,我們這裡將在純 JavaScript 中編寫 UDT 指令碼,雖說 C 版本可能有助於節省 Cycles ,但功能其實是一樣的。首先,我們需要遍歷所有輸入 Cell 並收集 UDT 的總和:diff --git a/udt.js b/udt.jsindex e69de29..4a20bd0 100644--- a/udt.js
+++ b/udt.js@@ -0,0 +1,17 @@+var input_index = 0;+var input_coins = 0;+var buffer = new ArrayBuffer(4);+var ret = CKB.CODE.INDEX_OUT_OF_BOUND;
++while (true) {+  ret = CKB.raw_load_cell_data(buffer, 0, input_index, CKB.SOURCE.GROUP_INPUT);+  if (ret === CKB.CODE.INDEX_OUT_OF_BOUND) {+    break;+  }
+  if (ret !== 4) {+    throw "Invalid input cell!";+  }+  var view = new DataView(buffer);+  input_coins += view.getUint32(0, true);+  input_index += 1;
+}正如前一篇文章中所解釋的,CKB 要求我們使用迴圈來迭代同一 group 中所有的 Input 並獲取資料。在 C 中我們將使用 ckb_load_cell_data,它被包裝到 JS 函式 ckb.raw_load_cell_data 中。正如 ArrayBuffer 所示,我們只對 Cell 資料的前 4 個位元組感興趣,因為這 4 個位元組將包含 UDT 的數量。注意,這裡我們對 input_coins 執行了一個簡單的 Add 操作,這風險很高。之所以這樣做只是為了簡單起見。在真實的生產環境中,您應該檢查該值是否保持在 32 位整數值中。如果有必要,應使用更高精度的數字型別。同樣地,我們可以獲取輸出的UDT的總和並進行比較:diff --git a/udt.js b/udt.jsindex 4a20bd0..e02b993 100644
--- a/udt.js+++ b/udt.js@@ -15,3 +15,23 @@ while (true) {   input_coins += view.getUint32(0);   input_index += 1; }
++var output_index = 0;+var output_coins = 0;++while (true) {+  ret = CKB.raw_load_cell_data(buffer, 0, output_index, CKB.SOURCE.GROUP_OUTPUT);
+  if (ret === CKB.CODE.INDEX_OUT_OF_BOUND) {+    break;+  }+  if (ret !== 4) {+    throw "Invalid output cell!";+  }
+  var view = new DataView(buffer);+  output_coins += view.getUint32(0, true);+  output_index += 1;+}++if (input_coins !== output_coins) {
+  throw "Input coins do not equal output coins!";+}以上幾乎就是驗證第一條規則所需的全部內容:輸出 Cell 中 UDT 的總和應等於輸入 Cell 中 UDT 的總和。換句話說,使用這種 Type Script,等於沒有人能夠偽造任何新的 Token。這不是很棒嗎?但有一個問題:當我們說沒有人能夠偽造新的 Token 時候,我們說的真的是指沒有任何人,包括代幣發行人!那就不太好了,所以我們需要新增一個例外,讓 Token 發行者可以先發行 Token,但之後就沒有人能夠這麼做了。那有沒有辦法做到這一點?當然有!但答案就像一個謎語,所以請仔細閱讀本段:Type Script 由兩部分組成:表示實際程式碼的程式碼雜湊,以及 Type Script 使用的引數。具有不同引數的兩種 Type Script 將被視為兩種不同 Type Script。這裡的技巧是允許 Token 發行者建立一個具有新 Type Script 的 Cell,但沒有人能夠再次建立,所以如果我們在引數部分放置一些不能再包含的東西,那麼問題就被解決了~現在想想這個問題:什麼東西不能被包含在區塊鏈中兩次?交易輸入中的 OutPoint!第一次的時候,我們將 OutPoint 作為交易輸入包含在內,引用的 Cell 將被消耗,如果有人稍後再次包含它,則會產生雙花錯誤,這正是我們使用區塊鏈的原因。
我們現在有答案了!CKB 中最小 UDT 的 Type Script 完整驗證流程如下:1. 首先收集輸入 Cell 中所有 UDT 的總和以及輸出 Cell 中所有 UDT 的總和,如果它們相等,則 Type Script 將以成功狀態退出;2. 檢查 Type Script 的第一個引數是否與當前交易中的第一個 OutPoint 匹配,如果它們匹配,則以成功狀態退出;3. 否則以失敗狀態退出。如果你現在還跟得上我,那麼一定可以看出:步驟 1 對應於正常的 UDT 交易,而步驟 2 對應於初始 Token 建立過程。這就是我們所說的 CKB 獨特的設計模式:透過使用輸入 OutPoint 作為 Script 引數,我們可以建立一個無法再偽造的獨特 Script:
1. 如果攻擊者試圖使用相同的引數,則 Script 將驗證出交易中的第一個輸入 OutPoint 與引數不匹配,從而使交易無效;2. 如果攻擊者試圖使用相同的引數並填充引數作為第一個輸入 OutPoint,它將造成一個雙花錯誤,也會使交易無效;3. 如果攻擊者試圖使用不同的引數,CKB 將識別出不同的引數導致不同的 Type Script,從而生成不同的UDT。這種簡單而強大的模式確保了 UDT 的安全,同時也帶來了可以在不同 Cell 之間自由交易的好處。據我們所知,這種模式在其他很多聲稱靈活或可程式設計的區塊鏈中是不可能實現的。現在我們終於可以完成我們的 UDT 指令碼了:diff --git a/contract.js b/contract.js
deleted file mode 100644index e69de29..0000000diff --git a/udt.js b/udt.jsindex e02b993..cd443bf 100644--- a/udt.js+++ b/udt.js
@@ -1,3 +1,7 @@+if (CKB.ARGV.length !== 1) {+  throw "Requires only one argument!";+}+ var input_index = 0;
 var input_coins = 0; var buffer = new ArrayBuffer(4);@@ -33,5 +37,17 @@ while (true) { } if (input_coins !== output_coins) {-  throw "Input coins do not equal output coins!";
+  if (!((input_index === 0) && (output_index === 1))) {+    throw "Invalid token issuing mode!";+  }+  var first_input = CKB.load_input(0, 0, CKB.SOURCE.INPUT);+  if (typeof first_input === "number") {+    throw "Cannot fetch the first input";
+  }+  var hex_input = Array.prototype.map.call(+    new Uint8Array(first_input),+    function(x) { return ('00' + x.toString(16)).slice(-2); }).join('');+  if (CKB.ARGV[0] != hex_input) {+    throw "Invalid creation argument!";
+  } }就是上面這樣,一共有 53 行程式碼 1372 位元組,我們就在 CKB 中完成了一個最小的 UDT Type Script。注意,在這裡我甚至還沒有使用壓縮工具,如果使用任何一個合適的 JS 壓縮工具,我們應該能夠獲得更緊湊的 Type Script 。當然了,這是一個可用於生產環境的 Type Script ,但它足以顯示一個簡單的 Type Script 可以處理 CKB 中的很多重要任務。部署到 CKB 網路我不像某些專案,只知道扔出來一個影片或者非常氣人的帖子,也不說清楚是怎麼做的或者怎麼解決問題的。如果沒有實際的程式碼和使用它的步驟,那我覺得這個帖子其實是很沒意思的。以下是如何在 CKB 上使用上述 UDT 指令碼:這裡還有沒有 Diff 格式的完整 UDT 指令碼,有需自取:
$ cat udt.jsif (CKB.ARGV.length !== 1) {  throw "Requires only one argument!";}var input_index = 0;var input_coins = 0;
var buffer = new ArrayBuffer(4);var ret = CKB.CODE.INDEX_OUT_OF_BOUND;while (true) {  ret = CKB.raw_load_cell_data(buffer, 0, input_index, CKB.SOURCE.GROUP_INPUT);  if (ret === CKB.CODE.INDEX_OUT_OF_BOUND) {    break;
  }  if (ret !== 4) {    throw "Invalid input cell!";  }  var view = new DataView(buffer);  input_coins += view.getUint32(0, true);
  input_index += 1;}var output_index = 0;var output_coins = 0;while (true) {  ret = CKB.raw_load_cell_data(buffer, 0, output_index, CKB.SOURCE.GROUP_OUTPUT);
  if (ret === CKB.CODE.INDEX_OUT_OF_BOUND) {    break;  }  if (ret !== 4) {    throw "Invalid output cell!";  }
  var view = new DataView(buffer);  output_coins += view.getUint32(0, true);  output_index += 1;}if (input_coins !== output_coins) {  if (!((input_index === 0) && (output_index === 1))) {
    throw "Invalid token issuing mode!";  }  var first_input = CKB.load_input(0, 0, CKB.SOURCE.INPUT);  if (typeof first_input === "number") {    throw "Cannot fetch the first input";  }
  var hex_input = Array.prototype.map.call(    new Uint8Array(first_input),    function(x) { return ('00' + x.toString(16)).slice(-2); }).join('');  if (CKB.ARGV[0] != hex_input) {    throw "Invalid creation argument!";  }
}為了能在 CKB 上執行 JavaScript,讓我們首先在 CKB 上部署 duktape:pry(main)> data = File.read("../ckb-duktape/build/duktape")pry(main)> duktape_tx_hash = wallet.send_capacity(wallet.address, CKB::Utils.byte_to_shannon(300000), CKB::Utils.bin_to_hex(duktape_data))pry(main)> duktape_data_hash = CKB::Blake2b.hexdigest(duktape_data)pry(main)> duktape_out_point = CKB::Types::CellDep.new(out_point: CKB::Types::OutPoint.new(tx_hash: duktape_tx_hash, index: 0))
首先,讓我們建立一個包含 1000000 Token 的 UDT:pry(main)> tx = wallet.generate_tx(wallet.address, CKB::Utils.byte_to_shannon(20000))pry(main)> tx.cell_deps.push(duktape_out_point.dup)pry(main)> arg = CKB::Utils.bin_to_hex(CKB::Serializers::InputSerializer.new(tx.inputs[0]).serialize)pry(main)> duktape_udt_script = CKB::Types::Script.new(code_hash: duktape_data_hash, args: [CKB::Utils.bin_to_hex(File.read("udt.js")), arg])pry(main)> tx.outputs[0].type = duktape_udt_script
pry(main)> tx.outputs_data[0] = CKB::Utils.bin_to_hex([1000000].pack("L<"))pry(main)> tx.witnesses[0].data.clearpry(main)> signed_tx = tx.sign(wallet.key, api.compute_transaction_hash(tx))pry(main)> root_udt_tx_hash = api.send_transaction(signed_tx)如果我們再次嘗試提交相同的交易,雙花將阻止我們偽造相同的 Token :pry(main)> api.send_transaction(signed_tx)
CKB::RPCError: jsonrpc error: {:code=>-3, :message=>"UnresolvableTransaction(Dead(OutPoint(0x0b607e9599f23a8140d428bd24880e5079de1f0ee931618b2f84decf2600383601000000)))"}無論我們如何嘗試,我們都無法建立另一個想要偽造相同 UDT Token 的 Cell。現在我們可以嘗試將 UDT 轉移到另一個帳戶。首先讓我們嘗試建立一個輸出 UDT 比輸入 UDT 更多的 UDT 交易:pry(main)> udt_out_point = CKB::Types::OutPoint.new(tx_hash: root_udt_tx_hash, index: 0)pry(main)> tx = wallet.generate_tx(wallet2.address, CKB::Utils.byte_to_shannon(20000))pry(main)> tx.cell_deps.push(duktape_out_point.dup)
pry(main)> tx.witnesses[0].data.clearpry(main)> tx.witnesses.push(CKB::Types::Witness.new(data: []))pry(main)> tx.outputs[0].type = duktape_udt_scriptpry(main)> tx.outputs_data[0] = CKB::Utils.bin_to_hex([1000000].pack("L<"))pry(main)> tx.inputs.push(CKB::Types::Input.new(previous_output: udt_out_point, since: "0"))pry(main)> tx.outputs.push(tx.outputs[1].dup)
pry(main)> tx.outputs[2].capacity = CKB::Utils::byte_to_shannon(20000)pry(main)> tx.outputs[2].type = duktape_udt_scriptpry(main)> tx.outputs_data.push(CKB::Utils.bin_to_hex([1000000].pack("L<")))pry(main)> signed_tx = tx.sign(wallet.key, api.compute_transaction_hash(tx))pry(main)> api.send_transaction(signed_tx)CKB::RPCError: jsonrpc error: {:code=>-3, :message=>"InvalidTx(ScriptFailure(ValidationFailure(-2)))"}
現在我們嘗試傳送 1000000 UDT 給另一個使用者,同時為傳送者本人保留 1000000 UDT,很明顯這會觸發錯誤,因為我們正在嘗試偽造更多的 Token。但是如果稍作修改,我們可以看到,如果您遵守總和驗證規則,UDT 轉移交易是有效的:pry(main)> tx.outputs_data[0] = CKB::Utils.bin_to_hex([900000].pack("L<"))pry(main)> tx.outputs_data[2] = CKB::Utils.bin_to_hex([100000].pack("L<"))pry(main)> signed_tx = tx.sign(wallet.key, api.compute_transaction_hash(tx))pry(main)> api.send_transaction(signed_tx)靈活的規則
這裡顯示的 UDT 指令碼僅作為示例,實際上,dApp 可能更復雜並且需要更多功能。您還可以根據需要為 UDT 指令碼新增更多功能,其中一些示例包括:· 這裡,我們嚴格確保輸出 UDT 的總和等於輸入 UDT 的總和,但在某些情況下,僅僅確保輸出 UDT 的總和不超過輸入 UDT的總和就足夠了。換句話說,當不需要時,使用者可以選擇為空間燒燬一部分 UDT;· 上述 UDT 指令碼不允許在初始建立完成後再發行更多的 Token,但可能存在另一種型別的 UDT,允許 Token 發行者繼續增發。這當然也可以在 CKB 上執行,但是我想把這個解決方案的探索任務留給大家,當做練習;· 在這裡,我們將指令碼限制為僅在初始 Token 建立過程中建立一個 Cell ,實際上也可以建立多個 Cell , 分別用於不同的用途;· 雖然我們只在這裡介紹 ERC20,但 ERC721 也應該是完全可能的。請注意,這裡只是一些例子,CKB 指令碼的實際應用方式是沒有邊界的。我們非常高興看到將來有更多的 CKB dApp 開發者創造出讓我們震驚的有趣用法。
Xuejie 在自己的部落格中,已經更新了該系列的第四篇「WebAssembly on CKB」以及第五篇「Debugging」,另外,還開啟了全新的系列:「一起建立一個最小區塊鏈」,歡迎大家點選閱讀原文或該連結:https://xuejie.space/,和 Xuejie 一起探索!

免責聲明:

  1. 本文版權歸原作者所有,僅代表作者本人觀點,不代表鏈報觀點或立場。
  2. 如發現文章、圖片等侵權行爲,侵權責任將由作者本人承擔。
  3. 鏈報僅提供相關項目信息,不構成任何投資建議

推荐阅读

;