原文作者:Xuejie

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

Nervos CKB 脚本编程简介[3]:自定义代币

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

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

  • 一个特殊的 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 独特的设计模式,一切都可以搞定。

为简单起见,我们这里将在纯 JavaScript 中编写 UDT 脚本,虽说 C 版本可能有助于节省 Cycles ,但功能其实是一样的。

首先,我们需要遍历所有输入 Cell 并收集 UDT 的总和:

diff --git a/udt.js b/udt.js
index 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.js
index 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 完整验证流程如下:

  • 首先收集输入 Cell 中所有 UDT 的总和以及输出 Cell 中所有 UDT 的总和,如果它们相等,则 Type Script 将以成功状态退出;
  • 检查 Type Script 的第一个参数是否与当前交易中的第一个 OutPoint 匹配,如果它们匹配,则以成功状态退出;
  • 否则以失败状态退出。

如果你现在还跟得上我,那么一定可以看出:步骤 1 对应于正常的 UDT 交易,而步骤 2 对应于初始 Token 创建过程。

这就是我们所说的 CKB 独特的设计模式:通过使用输入 OutPoint 作为 Script 参数,我们可以创建一个无法再伪造的独特 Script:

  • 如果攻击者试图使用相同的参数,则 Script 将验证出交易中的第一个输入 OutPoint 与参数不匹配,从而使交易无效;
  • 如果攻击者试图使用相同的参数并填充参数作为第一个输入 OutPoint,它将造成一个双花错误,也会使交易无效;
  • 如果攻击者试图使用不同的参数,CKB 将识别出不同的参数导致不同的 Type Script,从而生成不同的UDT。

这种简单而强大的模式确保了 UDT 的安全,同时也带来了可以在不同 Cell 之间自由交易的好处。据我们所知,这种模式在其他很多声称灵活或可编程的区块链中是不可能实现的。

现在我们终于可以完成我们的 UDT 脚本了:

diff --git a/contract.js b/contract.js
deleted file mode 100644
index e69de29..0000000
diff --git a/udt.js b/udt.js
index 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.js
if (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.clear
pry(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.clear
pry(main)> tx.witnesses.push(CKB::Types::Witness.new(data: []))
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.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_script
pry(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 开发者创造出让我们震惊的有趣用法。

加入 Nervos Community

Nervos Community 致力于成为最好的 Nervos 社区,我们将持续地推广和普 及 Nervos 技术,深入挖掘 Nervos 的内在价值,开拓 Nervos 的无限可能, 为每一位想要深入了解 Nervos Network 的人提供一个优质的平台。

添加微信号:BitcoinDog 即可加入 Nervos Community,如果是程序员请备注,还会将您拉入开发者群。

【在 Nervos CKB 上做开发】Nervos CKB 脚本编程简介[3]:自定义代币的更多相关文章

  1. 【在 Nervos CKB 上做开发】Nervos CKB脚本编程简介[2]:脚本基础

    CKB脚本编程简介[2]:脚本基础 原文作者:Xuejie 原文链接:Introduction to CKB Script Programming 2: Script 本文译者:Shooter,Jas ...

  2. 【在 Nervos CKB 上做开发】Nervos CKB 脚本编程简介[5]:调试 debug

    作者:Xuejie 原文链接:https://xuejie.space/2019_10_18_introduction_to_ckb_script_programming_debugging/ Ner ...

  3. 【在 Nervos CKB 上做开发】Nervos CKB 脚本编程简介[1]:验证模型

    CKB 脚本编程简介[1]: 验证模型 本文作者:Xuejie 原文链接:Introduction to CKB Script Programming 1: Validation Model 本文译者 ...

  4. 【在 Nervos CKB 上做开发】Nervos CKB 脚本编程简介[4]:在 CKB 上实现 WebAssembly

    作者:Xuejie 原文链接:https://xuejie.space/2019_10_09_introduction_to_ckb_script_programming_wasm_on_ckb/ N ...

  5. 软件开发架构,网络编程简介,OSI七层协议,TCP和UDP协议

    软件开发架构 什么是软件开发架构 1.软件架构是一个系统的草图. 2.软件架构描述的对象是直接构成系统的抽象组件. 3.各个组件之间的连接则明确和相对细致地描述组件之间的通讯. 4.在实现阶段,这些抽 ...

  6. 以太坊钱包开发系列4 - 发送Token(代币)

    以太坊去中心化网页钱包开发系列,将从零开始开发出一个可以实际使用的钱包,本系列文章是理论与实战相结合,一共有四篇:创建钱包账号.账号Keystore文件导入导出.展示钱包信息及发起签名交易.发送Tok ...

  7. 【CKB.DEV 茶话会】如何在 CKB 上实现用户自定义 Token

    本贴内容主要来自于 CKB.DEV 茶话会第一期,本期主题是:如何在 CKB 上实现 UDT,分享人是:Cipher 王博. 茶话会现场视频: https://v.qq.com/x/page/x303 ...

  8. Atitit.技术管理者要不要自己做开发??

    Atitit.技术管理者要不要自己做开发?? 1. 为什么很多管理者不能自己亲自做了1 1.1. 沟通成本多了1 1.2. .组织分散. 1 1.3. 会议多 .协调多 1 1.4. 问题的根源在于我 ...

  9. 转行做开发的Wiki:找好方向

    案 我是一个从建筑行业转行过来的后端工程师,转行来写代码了.最近发现经常有同学和网上的朋友问我一些转行的问题,零零散散地回答莫不如写一篇文章,以后回答此类问题就方便多了. 我的专业是给排水,属于非常传 ...

随机推荐

  1. hack the box -- sizzle 渗透过程总结,之前对涉及到域内证书啥的还不怎么了解

     把之前的笔记搬运过来 ---   1 开了443,用smbclient建立空连接查看共享 smbclient -N -L \\\\1.1.1.1 Department Shares Operatio ...

  2. 模拟80(a)

    其实隔壁的那套题比这套难的多....一道都不会.. T1 题目中已经给出了递推公式,那么这题就没什么了,直接矩阵乘就完了. 然而考场上并没有看出矩阵,主要是用了好久发明crt,我知道原理,但是不会打了 ...

  3. 20190630模拟赛B(单调队列优化dp)

    .dp无疑了其实. 在考场上,我写了一个错解,但是数据小都能过,只是会爆空间,考场上想着怎么用滚动数组优化来着....把错解的方程列出来吧 ;i<=n;i++) { ;j<=k;j++) ...

  4. Linux初体验--配置网络(CentOS7)

    在安装好虚拟机和操作系统后,一台合格的网络设备当然是要冲浪啊. 一.记下自己的网络配置 二.打开终端,输入命令 三.修改文件 保存后退出(wq). 四.重启网络服务 systemctl restart ...

  5. python——直接插入排序

    直接插入排序 直接插入排序原理 直接插入排序过程 代码 时间复杂度分析 排序稳定性

  6. Requests库使用总结

    概述 Requests是python中一个很Pythonic的HTTP库,用于构建HTTP请求与解析响应 Requests开发哲学 Beautiful is better than ugly.(美丽优 ...

  7. mysql select自增变量(包括读取当前第几行)

    mysql select自增变量(包括读取当前第几行) SET @rownum =0;select id,@rownum := @rownum +1 as i from ceshi order by ...

  8. python while指令

    while指令 1.它后面可以带一个 Ture 表示一直是真的,这样程序会在while循环里无限进行下去 eg: while Ture: print("打印这个...") 2.它后 ...

  9. Vue学习笔记:Vue组件的核心概念(下)

    1.双向绑定和单向数据流: 本质上还是单向数据流 视图<——>数据 v-model:仅仅是一个简写,用更少代码去实现功能. 自定义事件 .sync 修饰符 2.虚拟DOM及KEY属性作用 ...

  10. java里的一些名词的意思

    JDK   java开发工具包(java development kit) JRE  java运行环境 (java runtime environment)