CKB 脚本编程简介第三弹:自定义代币 | 技术帖

跟随 CKB-VM 的核心开发者 Xuejie 学习 CKB 脚本编程,详细了解灵活又好玩的自定义代币。

Xuejie 是 CKB-VM 的核心开发者,他在自己的博客「Less is More」中,创作了一系列介绍 CKB 脚本编程的文章,用于补充白皮书中编写 CKB 脚本所需的所有缺失的细节实现。本文是该系列的第三篇,详细介绍了灵活又好玩的自定义代币,快来一起玩耍吧 ~^.^~

作者:Xuejie

原文链接:https://xuejie.space/2019_09_06_introduction_to_ckb_script_programming_udt/

译者:Joey

CKB 的 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 应该真的属于收款人,为什么余额仍然留在一个中心呢?

这引出了我们提出的设计:

  1. 一个特殊的 Type Script 表示此 Cell 存储 UDT;
  2. 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.jsdeleted 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_scriptpry(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 开发者创造出让我们震惊的有趣用法。

                    CKB 脚本编程简介第三弹:自定义代币 | 技术帖

                    Xuejie 在自己的博客中,已经更新了该系列的第四篇「WebAssembly on CKB」以及第五篇「Debugging」,另外,还开启了全新的系列:「一起建立一个最小区块链」,欢迎大家点击阅读原文或该链接:https://xuejie.space/,和 Xuejie 一起探索!

                    转载声明:本文 由CoinON抓取收录,观点仅代表作者本人,不代表CoinON资讯立场,CoinON不对所包含内容的准确性、可靠性或完整性提供任何明示或暗示的保证。若以此作为投资依据,请自行承担全部责任。

                    声明:图文来源于网络,如有侵权请联系删除

                    风险提示:投资有风险,入市需谨慎。本资讯不作为投资理财建议。

                    (0)
                    打赏 微信扫一扫 微信扫一扫
                    上一篇 2019年12月13日 下午5:03
                    下一篇 2019年12月13日 下午5:03

                    相关推荐

                    CKB 脚本编程简介第三弹:自定义代币 | 技术帖

                    星期五 2019-12-13 17:03:16

                    Xuejie 是 CKB-VM 的核心开发者,他在自己的博客「Less is More」中,创作了一系列介绍 CKB 脚本编程的文章,用于补充白皮书中编写 CKB 脚本所需的所有缺失的细节实现。本文是该系列的第三篇,详细介绍了灵活又好玩的自定义代币,快来一起玩耍吧 ~^.^~

                    作者:Xuejie

                    原文链接:https://xuejie.space/2019_09_06_introduction_to_ckb_script_programming_udt/

                    译者:Joey

                    CKB 的 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 应该真的属于收款人,为什么余额仍然留在一个中心呢?

                    这引出了我们提出的设计:

                    1. 一个特殊的 Type Script 表示此 Cell 存储 UDT;
                    2. 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.jsdeleted 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_scriptpry(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 开发者创造出让我们震惊的有趣用法。

                                      CKB 脚本编程简介第三弹:自定义代币 | 技术帖

                                      Xuejie 在自己的博客中,已经更新了该系列的第四篇「WebAssembly on CKB」以及第五篇「Debugging」,另外,还开启了全新的系列:「一起建立一个最小区块链」,欢迎大家点击阅读原文或该链接:https://xuejie.space/,和 Xuejie 一起探索!