主页 > 苹果怎么下载imtoken钱包 > 《在 CKB 上开发》Nervos CKB 脚本编程入门《3》:自定义 Tokens

《在 CKB 上开发》Nervos CKB 脚本编程入门《3》:自定义 Tokens

苹果怎么下载imtoken钱包 2023-11-11 05:13:29

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

CKB 的 Cell 模型和 VM 支持许多新的用例。 然而,这并不意味着我们需要放弃我们拥有的一切。 今天在区块链中的一个常见用途是代币发行者发行具有特殊用途/意义的新代币。 在以太坊中,我们称之为 ERC20 Token,让我们看看我们如何在 CKB 中构建一个类似的概念。 为了区别于ERC20,CKB中的Token被称为User Defined Token,简称UDT。

本文使用 CKB v0.20.0 版本进行演示。 具体来说,我在每个项目中使用以下提交:

数据模型

与以太坊为每个合约账户提供单独的存储空间不同,CKB 在多个单元之间传输数据。 Cell 的 Lock Script 和 Type Script 指示 Cell 属于哪个帐户以及如何与 Cell 交互。 因此,CKB 不会像 ERC20 一样将所有 Token 用户的余额存储在 ERC20 合约的存储空间中。 在 CKB 中,我们需要一个新的设计来存储 UDT 用户的余额。

当然我们也可以构造一个特殊的Cell来保存所有UDT用户的余额。 这个解决方案看起来很像以太坊的 ERC20 设计。 但是这中间有几个问题:

这导致我们提出的设计:

这种设计有几个含义:

每个 Token 用户将自己的 UDT 保存在自己的 Cell 中。 他们负责为 UDT 提供存储空间,并确保自己的 Token 安全。 这样UDT才能真正属于每个UDT用户。

但是这里还有一个问题:如果这个Token存放在属于每个用户的很多不同的Cell中,而不是统一存放,我们如何确定这个Token确实是这个发行者发行的呢? 如果有人自己伪造Token怎么办? 在以太坊中,这可能是个问题,但正如我们将在本文中看到的,CKB 中的 TypeScript 可以防止所有这些攻击并确保代币安全。

编写 UDT 脚本

鉴于上述设计,最小的 UDT Type Script 应遵循以下规则:

这听起来可能有点自大,但我们将展示使用 TypeScript 和 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 要求我们使用循环遍历同一组中的所有 Input 并获取数据。 在 C 中,我们将使用 ckbloadcelldata,它被包装到 JS 函数 ckb.rawloadcelldata 中。 正如 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。 那不是很好吗?

但是有一个问题:当我们说没有人可以伪造新的代币时,我们真正的意思是没有人,包括代币发行者! 这样不好,所以我们需要添加一个例外,让代币发行者先发行代币,然后就没人可以了。 那么有没有办法做到这一点?

当然有! 但答案就像一个谜语,所以请仔细阅读这一段:TypeScript 由两部分组成:代表实际代码的代码哈希,以及 TypeScript 使用的参数。 具有不同参数的两个 TypeScript 将被视为两个不同的 TypeScript。 这里的技巧是让Token发行者用一个新的TypeScript创建一个Cell,但是没有人可以重新创建,所以如果我们在参数部分放一些不能再包含的东西,那么问题就解决了~

现在想想这个问题:什么东西不能被二次写入区块链? 交易输入中的OutPoint! 我们第一次包含OutPoint作为交易输入时,引用的Cell会被消费,如果后面有人再次包含它,就会出现双花错误,这就是我们使用区块链的原因。

我们现在有了答案! CKB 中最小的 UDT 的 Type Script 完整验证流程如下:

如果你现在能跟上我的进度,你一定能看到:第一步对应的是正常的UDT交易,而第二步对应的是初始Token的创建过程。

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

这种简单而强大的模型保证了 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 缩小器我们应该能够获得更紧凑的 TypeScript。 诚然,这是一个生产就绪的 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))

首先,让我们创建一个包含 1,000,000 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)

如果我们尝试再次提交相同的交易,双重支出将阻止我们伪造相同的令牌:

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)))"}

现在我们尝试发送 1,000,000 UDT 给另一个用户,同时为发送者自己保留 1,000,000 UDT,显然这会触发错误,因为我们正在尝试伪造更多的令牌。 但稍微修改一下,我们可以看到,如果遵循金额验证规则,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 脚本只是一个例子,在现实中,dApps 可能更复杂,需要更多的功能。 您还可以根据需要向 UDT 脚本添加更多功能,一些示例包括:

请注意以太坊代币创建,这些只是一些示例,实际应用 CKB 脚本的方式没有界限。 我们很高兴看到更多 CKB dApp 开发者在未来创造出令我们震惊的有趣用法。

加入 Nervos 社区

Nervos 社区致力于成为最好的 Nervos 社区。 我们将持续推广和普及 Nervos 技术,深挖 Nervos 的内在价值,开启 Nervos 的无限可能,提供优质的平台。