Skip to content

Instantly share code, notes, and snippets.

@quake
Last active February 26, 2020 09:37
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save quake/f8db4d01dfe65af932b85b76cb32ae13 to your computer and use it in GitHub Desktop.
Save quake/f8db4d01dfe65af932b85b76cb32ae13 to your computer and use it in GitHub Desktop.

在本篇文档里,我们会使用 mruby 来作为编程语言演示如何在 CKB 实现一个最基础 UDT (User Defined Token) 合约

Dev Chain

首先我们需要启动一个本地的开发链来方便调试

配置矿工信息

我们先用 ckb-cli 命令生成一个新账户用于挖矿,运行 ckb-cli,然后输入

account new

会得到如下的类似结果,其中包含 lock_arg

address:
  mainnet: ckb1qyqtyqhu65hhuknlmj5ky5jydc0gcvq89pks8url9f
  testnet: ckt1qyqtyqhu65hhuknlmj5ky5jydc0gcvq89pks6eaqf4
lock_arg: 0xb202fcd52f7e5a7fdca96252446e1e8c3007286d

初始化 Dev Chain

将前面得到的 lock_arg 作为参数来初始化

./ckb init -c dev --ba-arg 0xb202fcd52f7e5a7fdca96252446e1e8c3007286d

dev chain 挖矿用的是固定间隔出块的 Dummy 模式,默认出块间隔是 5 秒,为了方便测试和调试,我们希望出块能够快一些,可以修改 ckb-miner.toml 里面的配置,将默认值 5000 (毫秒) 改成 1000 或者其他更小的值

[[miner.workers]]
worker_type = "Dummy"
delay_type = "Constant"
value = 5000

因为难度调整的关系,为了避免出块间隔时间改太小之后可能出现的难度溢出,还需要修改一下 specs/dev.toml 的配置,将这个被注释掉的参数取消注释

# permanent_difficulty_in_dummy = true

然后启动 ckb 节点

./ckb run

再开一个窗口执行本地的挖矿

./ckb miner

等几秒钟,如果在挖矿窗口看到 Total nonces found: 1 这样的输出,就代表配置都正常了

mruby 解释器

编译

因为 mruby 是一个解释型语言,我们还需要编译一个 mruby 的解释器部署在 CKB 上

相关的代码仓库在 https://github.com/xxuejie/ckb-mruby ,克隆之后用 docker 进行编译

git clone --recursive https://github.com/xxuejie/ckb-mruby
cd ckb-mruby
sudo docker run --rm -it -v `pwd`:/code nervos/ckb-riscv-gnu-toolchain:bionic-20191012 bash

在下面的命令编译完成之后,你会在 build 目录下面找到编译好的解析器 entry 文件

apt-get update
apt-get install -y ruby
cd /code
make
exit

部署

因为部署 mruby 解释器需要一些 CKB 容量,我们可以直接用 dev chain 在创世块里面预分配的账户容量 https://github.com/nervosnetwork/ckb/blob/develop/resource/specs/dev.toml#L70

我们需要将预分配的账户私钥写入到一个文件

echo d00c06bfd800d27397002dca6fb0993d5ba6399b4238b2f29ee9deb97593d2bc > /tmp/issued_cell_private_key

然后将这个私钥通过 ckb-cli 导入

account import --privkey-path /tmp/issued_cell_private_key

导入成功后,你将会看到这样的账户信息

  address:
    mainnet: ckb1qyqvsv5240xeh85wvnau2eky8pwrhh4jr8ts6f6daz
    testnet: ckt1qyqvsv5240xeh85wvnau2eky8pwrhh4jr8ts8vyj37
  lock_arg: 0xc8328aabcd9b9e8e64fbc566c4385c3bdeb219d7

查看一下编译好的 mruby 解析器 entry 文件大小,是 460600 Byte,我们用 ckb-cli 来执行操作,同时将这个解析器的内容作为 data 保存在 tx output,下面的命令多加 100 Byte 是因为 CKB 交易自己本身还需要需要一些容量

wallet transfer --to-address ckt1qyqvsv5240xeh85wvnau2eky8pwrhh4jr8ts8vyj37 --from-account 0xc8328aabcd9b9e8e64fbc566c4385c3bdeb219d7 --to-data-path /tmp/ckb-mruby/build/entry --capacity 460700 --tx-fee 0.01

这样就部署好了,请记录下这个命令产生的 tx hash,后面我们会一直用到这个值,这里假设生成的 tx hash 是0xdf4b4ee062684c212c25eeb7f0110bcaf3aa9e547cfb6913d0c928f32acb5e86

Hello World

在开始写 UDT 之前,我们先写一个最简单的 mruby 合约来测试看看之前部署是否一切正常,这个合约会校验第一个 output data 里面的内容是不是保存了 'Hello World' 字符串。CKB 的 output 数据结构有两个字段 locktype , 他们都对应到一个 Script 结构 ( https://github.com/nervosnetwork/rfcs/blob/master/rfcs/0019-data-structures/0019-data-structures.md#Script ) ,lock 字段通常用来做交易的所有权校验,type 字段通常用来校验交易 cell 转换前后数据是否满足规则 ( input / output ),从而可以实现任意预定义规则的交易,这篇文档里面演示的合约就是通过 type 来实现,而 lock 字段还是使用 CKB 默认提供的 secp256k1 签名校验

将下面的合约写入一个文件 /tmp/hello_world.rb

cell_data = CKB::CellData::new(CKB::Source::OUTPUT, 0)
raise 'boom' if cell_data.readall != 'Hello World'

我们将会用 ruby sdk 来部署合约 ( https://github.com/nervosnetwork/ckb-sdk-ruby ) 克隆这个项目之后,使用 ruby console

git clone https://github.com/nervosnetwork/ckb-sdk-ruby
cd ckb-sdk-ruby
bundle exec bin/console

在 ruby console 里面执行如下的代码

mruby_tx_hash = '0xdf4b4ee062684c212c25eeb7f0110bcaf3aa9e547cfb6913d0c928f32acb5e86'
mruby_dep = CKB::Types::CellDep.new(out_point: CKB::Types::OutPoint.new(tx_hash: mruby_tx_hash, index: 0))
mruby_data_hash = CKB::Blake2b.hexdigest(File.read('/tmp/ckb-mruby/build/entry'))
hello_world = CKB::Types::Script.new(code_hash: mruby_data_hash, args: CKB::Utils.bin_to_hex(File.read("/tmp/hello_world.rb")))

api = CKB::API.new
wallet = CKB::Wallet.from_hex(api, '0xd00c06bfd800d27397002dca6fb0993d5ba6399b4238b2f29ee9deb97593d2bc')
data = CKB::Utils.bin_to_hex('Hello World')
tx = wallet.generate_tx(wallet.address, CKB::Utils.byte_to_shannon(300), data, fee: 5000)
tx.cell_deps.push(mruby_dep)
tx.outputs[0].type = hello_world
api.send_transaction(tx.sign(wallet.key))

如果之前部署一切正常的话,将会得到一个 tx hash,你也可以尝试将 output data 改成其他内容,这样提交交易就无法通过校验了,会得到如下的错误

CKB::RPCError: jsonrpc error: {:code=>-3, :message=>"Script: ValidationFailure(-2)"}

UDT

接下来我们来实现一个实现最基本功能的 UDT 合约,它能够发行,转账,以及销毁 UDT

首先需要限制只有某个私钥才能增发和销毁这个 UDT,因为 CKB 默认的 lock script 已经提供了签名覆盖,所以可以通过判断 lock script hash 来实现这个限制

先通过 ruby console 获取私钥对应的 lock script hash

api = CKB::API.new
wallet = CKB::Wallet.from_hex(api, '0xd00c06bfd800d27397002dca6fb0993d5ba6399b4238b2f29ee9deb97593d2bc')
wallet.lock_hash

会得到 0x32e555f3ff8e135cece1351a6a2971518392c1e30375c1e006ad0ce8eac07947,我们将这个值的对比判断写成一个合约方法

def is_issuer(lock_hash)
    index = 0
    while true
        begin
            input_lock_hash = CKB::CellField.new(CKB::Source::INPUT, index, CKB::CellField::LOCK_HASH)
            if input_lock_hash.readall != lock_hash
                return false
            else
                index += 1
            end
        rescue CKB::IndexOutOfBound
            return true
        end
    end
end

类似前面的 Hello World 例子,我们可以将 UDT 的数量存储在 output data,在转账的时候检查所有的 input 和 output UDT 数量相等即可,再写一个合约方法:

def udt_amount(field)
    index = 0
    total = 0
    while true
        begin
            cell_data = CKB::CellData::new(field, index)
            total += cell_data.readall.to_i
            index += 1
        rescue CKB::IndexOutOfBound
            return total
        end
    end
end

然后将这两个条件判断组合在一起,就完成了整个 UDT 合约

if !is_issuer(['32e555f3ff8e135cece1351a6a2971518392c1e30375c1e006ad0ce8eac07947'].pack('H*')) && udt_amount(CKB::Source::GROUP_INPUT) != udt_amount(CKB::Source::GROUP_OUTPUT)
  raise 'invalid udt tx'
end

将上面这3段代码写入到一个合约文件 /tmp/udt.rb,在 ruby console 里面执行如下的代码来发行 10000 UDT 到一个随机私钥地址

mruby_tx_hash = '0xdf4b4ee062684c212c25eeb7f0110bcaf3aa9e547cfb6913d0c928f32acb5e86'
mruby_dep = CKB::Types::CellDep.new(out_point: CKB::Types::OutPoint.new(tx_hash: mruby_tx_hash, index: 0))
mruby_data_hash = CKB::Blake2b.hexdigest(File.read('/tmp/ckb-mruby/build/entry'))
udt = CKB::Types::Script.new(code_hash: mruby_data_hash, args: CKB::Utils.bin_to_hex(File.read("/tmp/udt.rb")))

api = CKB::API.new
wallet = CKB::Wallet.from_hex(api, '0xd00c06bfd800d27397002dca6fb0993d5ba6399b4238b2f29ee9deb97593d2bc')
alice = CKB::Wallet.from_hex(api, CKB::Key.random_private_key)
issue_amount = CKB::Utils.bin_to_hex('10000')
tx = wallet.generate_tx(alice.address, CKB::Utils.byte_to_shannon(8000), issue_amount, fee: 5000)
tx.cell_deps.push(mruby_dep)
tx.outputs[0].type = udt
api.send_transaction(tx.sign(wallet.key))

成功发行之后,我们将这 10000 个 UDT 中的 3000 个转移到另外一个随机私钥地址

bob = CKB::Wallet.from_hex(api, CKB::Key.random_private_key)
transfer_udt_amount = CKB::Utils.bin_to_hex('3000')
remain_udt_amount = CKB::Utils.bin_to_hex('7000')
alice.skip_data_and_type = false
tx = alice.generate_tx(bob.address, CKB::Utils.byte_to_shannon(4000), transfer_udt_amount, fee: 5000)
tx.cell_deps.push(mruby_dep)
tx.outputs[0].type = udt
tx.outputs[1].type = udt
tx.outputs_data[1] = remain_udt_amount
api.send_transaction(tx.sign(alice.key))

需要注意的是,这里使用了 CKB ruby sdk Wallet 相关的方法来实现交易的组装,更灵活的方式是手工构建一个交易结构 ( https://github.com/nervosnetwork/rfcs/blob/master/rfcs/0022-transaction-structure/0022-transaction-structure.md ) ,能够更灵活地实现 UDT 收集和转账,但是相关演示的 ruby 代码会变得很冗长,在实际运用中,可以用 ruby sdk 封装出更好的 TX Geneartor

改进

我们用了 33 行 ruby 代码完成了一个基本的 UDT 合约,但这里有很多可以改进的点,举3个例子

  1. 这个演示的 UDT 合约使用了 Script 结构上的 args 来保存合约代码,会导致需要很多 CKB capacity 来构建 UDT tx,同时还把 issuer script hash 写死在了合约里面,不够灵活。我们可以通过改进 mruby 解析器,将原先从 args 读取合约代码的方式改成从 deps 读取 ( https://github.com/xxuejie/ckb-mruby/blob/6e0329c5a1c9002525a6b1b935c2ff99e5238edf/c/entry.c#L30 ) 这样就可以先部署一个合约,然后将它作为 tx deps 引用,Script 的结构可以优化成如下数据,任何人都可以引用这个 deps,通过修改 args 来一键发行属于自己的 UDT
Script:
    code_hash = 'udt contract hash'
    hash_type = 'data'
    args = 'issuer script hash'
  1. mruby 使用了抛出异常 raise 'xxxx' 来统一返回错误码 -2,不方便反馈具体的错误给用户,可以通过改进 mruby 解析器,改成用不同的返回值 https://github.com/xxuejie/ckb-mruby/blob/6e0329c5a1c9002525a6b1b935c2ff99e5238edf/c/entry.c#L41-L52

  2. CKB Ruby SDK 不够灵活,目前收集 UDT 余额和构建交易比较繁琐,我们可以在 Ruby SDK 基础上实现一个 UDT generator,方便我们进行 UDT 发行和转账

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment