【翻译】介绍 CKB Script编程7:Duktape 高级编程(1)

原文来自:
Introduction to CKB Script Programming 7: Advanced Duktape Examples

由于文章过长,超过了字数限制,所以分成2篇来发。

此文为第1篇,第2篇文章: Duktape 高级编程(2)

我在之前介绍过 duktape,展示了如何在Nervos CKB上运行 JavaScript代码。但到目前为止,我所展示的代码都是非常简单逻辑的单个代码段。
如果我们要解析CKB 数据结构呢?如果我们需要在脚本中引入外部的库呢?在这篇文章里, 我们将创建一个 CKB 脚本项目:duktape-powered,
这个项目有以下需求:

  • 外部库依赖
  • CKB数据结构的序列化/反序列化
  • 进行哈希计算

在继续这篇文章之前,我想说的是,这篇文章中的主要工作不是我写的。这要归功于我的一位同事,他花了很多的精力写了一个非常好的模版,我们才可以在这里使用,我们才可以通过JavaScript和duktape获得精简的CKB脚本开发体验。

这篇文章是基于现在的CKB Lina 主网版本写的。

Scope 范围

在这篇文章中, 我们用JavaScript写一个简单的 HTLC脚本。
我不得不承认,我不是世界上最好的老师,有很多很多人在 解释HTLC上做的比我好。 所以如果你想知道什么是HTLC,可以先查看其他资料,然后再回来。

现在我假设你已经弄明白了 HTLC,我们在这里创建HTLC脚本,如果满足以下任何一个条件,脚本将被解锁:

  • 提供正确的密码字符串和有效的公钥A签名;
  • 经过一定时间后,提供公钥B的有效签名

在设计我们的HTLC脚本时,还有几点需要注意:

  1. 为了简单起见,这里我们将使用一个技巧来进行签名验证:我们将依赖一个单独的cell来提供正确公钥的签名,而不是直接在JavaScript中进行签名验证。在这篇文章的后面,我们将解释在JavaScript中签名验证的后果和考虑;

  2. CKB HTLC脚本结构的args部分将包含正确的秘密字符串的散列,因此当脚本运行时,它可以对提供的秘密字符串运行散列函数,测试它是否正确;

  3. 时间量总是设置为100个块。为了验证已经通过了100个块,解锁事务应该包含一个区块头,该区块头在将要解锁的cell提交到链上之后至少100个块。

Getting Our Hands Dirty 直接动手

虽然我们当然欢迎您自己动手构建框架,但是为了节省时间,我的一位同事已经准备了一个不错的模板。在这篇文章中,我们将从已经建立的模板开始:

$ export TOP=$(pwd)
$ git clone https://github.com/xxuejie/ckb-duktape-template htlc-template
$ cd htlc-template
$ npm install
# 现在可以尝试先构建脚本,以确保一切正常
$ npm run build

Now you can use your favorite editor to open src/index.js file in htlc-template repo, the current content of the file looks like this:

现在可以用你最喜欢的编辑器打开 htlc-template项目中的src/index.js 文件,文件的内容如下:

$ cd $TOP/htlc-template
$ cat src/index.js
const { Molecule } = require('molecule-javascript')
const schema = require('../schema/blockchain-combined.json')

const names = schema.declarations.map(declaration => declaration.name)
const scriptTypeIndex = names.indexOf('Script')
const scriptType = schema.declarations[scriptTypeIndex]

// Write your script logic here.
CKB.debug(scriptType)

我们将修改这个文件以添加我们需要的逻辑。

Script Debugger Preparation 脚本调试器准备工作

为了帮助脚本编程,让我们组合一个调试环境。调试环境有两个目的:

  • 准备一个完整的交易,可以加载到CKB调试器;
  • 创建交易并将其转发给CKB

让我们先创建环境骨架:

$ cd $TOP
$ mkdir htlc-runner
$ cd htlc-runner
$ npm init
$ npm install --save @nervosnetwork/ckb-sdk-core
$ npm install --save @nervosnetwork/ckb-sdk-utils
$ npm install --save molecule-javascript
$ npm install --save crc32

现在让我们创建一个用于调试器使用的交易框架:

$ cd $TOP/htlc-runner
$ cat skeleton.json
{
  "mock_info": {
    "inputs": [
      {
        "input": {
          "previous_output": {
            "tx_hash": "0xa98c57135830e1b91345948df6c4b8870828199a786b26f09f7dec4bc27a73da",
            "index": "0x0"
          },
          "since": "0x0"
        },
        "output": {
          "capacity": "0x4b9f96b00",
          "lock": {
            "args": "0x32e555f3ff8e135cece1351a6a2971518392c1e30375c1e006ad0ce8eac07947c219351b150b900e50a7039f1e448b844110927e5fd9bd30425806cb8ddff1fd970dd9a8",
            "code_hash": "@DUKTAPE_HASH",
            "hash_type": "data"
          },
          "type": null
        },
        "data": "0x"
      }
    ],
    "cell_deps": [
      {
        "cell_dep": {
          "out_point": {
            "tx_hash": "0xfcd1b3ddcca92b1e49783769e9bf606112b3f8cf36b96cac05bf44edcf5377e6",
            "index": "0x0"
          },
          "dep_type": "code"
        },
        "output": {
          "capacity": "0x702198d000",
          "lock": {
            "args": "0x",
            "code_hash": "0x0000000000000000000000000000000000000000000000000000000000000000",
            "hash_type": "data"
          },
          "type": null
        },
        "data": "@SCRIPT_CODE"
      },
      {
        "cell_dep": {
          "out_point": {
            "tx_hash": "0xfcd1b3ddcca92b1e49783769e9bf606112b3f8cf36b96cac05bf44edcf5377e6",
            "index": "0x1"
          },
          "dep_type": "code"
        },
        "output": {
          "capacity": "0x702198d000",
          "lock": {
            "args": "0x",
            "code_hash": "0x0000000000000000000000000000000000000000000000000000000000000000",
            "hash_type": "data"
          },
          "type": null
        },
        "data": "@DUKTAPE_CODE"
      }
    ],
    "header_deps": [
      {
        "compact_target": "0x1a1e4c2f",
        "hash": "0x51d199c4060f703344eab3c9b8794e6c60195ae9093986c35dba7c3486224409",
        "number": "0xd8fc4",
        "parent_hash": "0xc02e01eb57b205c6618c9870667ed90e13adb7e9a7ae00e7a780067a6bfa6a7b",
        "nonce": "0xca8c7caa8100003400231b4f9d6e0300",
        "timestamp": "0x17061eab69e",
        "transactions_root": "0xffb0863f4ae1f3026ba99b2458de2fa69881f7508599e2ff1ee51a54c88b5f88",
        "proposals_hash": "0x0000000000000000000000000000000000000000000000000000000000000000",
        "uncles_hash": "0x0000000000000000000000000000000000000000000000000000000000000000",
        "version": "0x0",
        "epoch": "0x53f00fa000232",
        "dao": "0x4bfe53a5a9bb9a30c88898b9dfe22300a58f2bafed47680000d3b9f5b6630107"
      }
    ]
  },
  "tx": {
    "version": "0x0",
    "cell_deps": [
      {
        "out_point": {
          "tx_hash": "0xfcd1b3ddcca92b1e49783769e9bf606112b3f8cf36b96cac05bf44edcf5377e6",
          "index": "0x0"
        },
        "dep_type": "code"
      },
      {
        "out_point": {
          "tx_hash": "0xfcd1b3ddcca92b1e49783769e9bf606112b3f8cf36b96cac05bf44edcf5377e6",
          "index": "0x1"
        },
        "dep_type": "code"
      }
    ],
    "header_deps": [
      "0x51d199c4060f703344eab3c9b8794e6c60195ae9093986c35dba7c3486224409"
    ],
    "inputs": [
      {
        "previous_output": {
          "tx_hash": "0xa98c57135830e1b91345948df6c4b8870828199a786b26f09f7dec4bc27a73da",
          "index": "0x0"
        },
        "since": "0x0"
      }
    ],
    "outputs": [
      {
        "capacity": "0x0",
        "lock": {
          "args": "0x",
          "code_hash": "0x0000000000000000000000000000000000000000000000000000000000000000",
          "hash_type": "data"
        },
        "type": null
      }
    ],
    "witnesses": [
      "0x210000000c0000001d0000000d0000006920616d20612073656372657400000000"
    ],
    "outputs_data": [
      "0x"
    ]
  }
}

你可能会注意到骨架会跳过dep cell data部分,这是因为在开发HTLC脚本时,可能需要在骨架中插入不同的内容。因此,这里需要一个运行器来准备一个完整的交易骨架,然后通过CKB调试器运行它:

$ cd $TOP/htlc-runner
$ cat runner.js
#!/usr/bin/env node

const { Molecule } = require('molecule-javascript')
const schema = require('../htlc-template/schema/blockchain-combined.json')
const utils = require("@nervosnetwork/ckb-sdk-utils")
const process = require('process')
const fs = require('fs')

function blake2b(buffer) {
  return utils.blake2b(32, null, null, utils.PERSONAL).update(buffer).digest('binary')
}

if (process.argv.length !== 4) {
  console.log(`Usage: ${process.argv[1]} <duktape load0 binary> <js script>`)
  process.exit(1)
}

const duktape_binary = fs.readFileSync(process.argv[2])
const duktape_hash = blake2b(duktape_binary)
const js_script = fs.readFileSync(process.argv[3])

const data = fs.readFileSync('skeleton.json', 'utf8').
      replace("@DUKTAPE_HASH", utils.bytesToHex(duktape_hash)).
      replace("@SCRIPT_CODE", utils.bytesToHex(js_script)).
      replace("@DUKTAPE_CODE", utils.bytesToHex(duktape_binary))

fs.writeFileSync('tx.json', data)

const resolved_tx = JSON.parse(data)
const json_lock_script = resolved_tx.mock_info.inputs[0].output.lock
const lock_script = {
  codeHash: json_lock_script.code_hash,
  hashType: json_lock_script.hash_type,
  args: json_lock_script.args
}
const lock_script_hash = blake2b(utils.hexToBytes(utils.serializeScript(lock_script)))

console.log(`../ckb-standalone-debugger/bins/target/release/ckb-debugger -g lock -h ${utils.bytesToHex(lock_script_hash)} -t tx.json`)

我们需要在这里编译duktape:

$ cd $TOP
$ git clone --recursive https://github.com/xxuejie/ckb-duktape
$ cd ckb-duktape
$ sudo docker run --rm -it -v `pwd`:/code nervos/ckb-riscv-gnu-toolchain:bionic-20191209 bash
root@18d4b1952624:/# cd /code
root@18d4b1952624:/code# make
root@18d4b1952624:/code# exit

还有 CKB 调试器:

$ cd $TOP
$ git clone --recursive https://github.com/xxuejie/ckb-standalone-debugger
$ cd ckb-standalone-debugger/bins
$ cargo build --release

现在你可以尝试运行生成的脚本:

$ cd $TOP/htlc-runner
$ chmod +x runner.js
$ RUST_LOG=debug `./runner.js ../ckb-duktape/build/load0 ../htlc-template/build/duktape.js`
DEBUG:<unknown>: script group: Byte32(0x8209891745eb858abd6f5e53c99b4f101bca221bd150a2ece58a389b7b4f8fa7) DEBUG OUTPUT: [object Object]
Run result: Ok(0)

这将准备从duktape二进制文件和JS脚本运行的交易,然后通过CKB调试器运行它,调试输出和最终结果将打印到stdout。
或者,如果你觉得REPL更有帮助,你可以使用以下代码来执行脚本,然后启动REPL:

$ cd $TOP/htlc-runner
$ RUST_LOG=debug `./runner.js ../ckb-duktape/build/repl0 ../htlc-template/build/duktape.js`
duk>

调试器准备好之后,现在让我们实现HTLC脚本。

Custom Arguments 自定义参数

在CKB上运行的脚本上,提供了2个位置用于保存参数:

  • args 字段在Script结构中

  • witnesses 字段在 Transaction 结构中

它们之间的区别是,args字段用于保存对同一脚本的所有使用都保持相同的参数,而witness字段用于一次性交易验证中使用的临时参数。

这里的一个例子是:对于进行签名验证的脚本,args字段通常用于存储公钥散列,而witness字段用于保存有效签名。

为了获得最大的灵活性,args字段和witness数组字段中的每个项都是纯原始字节。

dapp开发人员需要设计他们想要保存的数据的实际格式。在HTLC的脚本中,我们使用 molecule序列化格式。 Molecule在CKB中得到了广泛的应用。 如果你想要与CKB交互,例如读取当前交易中使用的某些cell/脚本,你将需要处理molecule格式。

现在,这是一个很好的机会,来解释一个人如何通过molecule与CKB相互作用,因此,我们将以molecule格式实现argswitness使用的自定义结构。

尽管你可以在自己的脚本中自由使用任何序列化格式。

让我们先创建一个文件,需要2个数据结构:


$ cd $TOP/htlc-template

$ cat htlc.mol

array Uint32 [byte; 4];

array Byte32 [byte; 32];

vector Bytes <byte>;

struct HtlcArgs {

a: Byte32,

b: Byte32,

hash: Uint32,

}

table HtlcWitness {

s: Bytes,

i: Uint32,

}

关于molecule的更多信息,请参阅 RFC。这里我们定义了两种结构,要求如下:

  • HtlcArgs需要2个32字节长的原始字节来存储两个公钥(稍后,我们将在这里的HTLC脚本实际上从这个设计中泛化了一点),以及一个32位的整数值来存储哈希。为简单起见,我们的HTLC将使用CRC32作为哈希函数,但在生产环境中,这远远不是一个安全的解决方案,你肯定要使用一个适当的安全哈希函数;

  • HtlcWitness有2个可选参数(由table结构表示):它要么包含一个可变长度的字符串,该字符串包含HTLC的密码字符串,要么包含一个32位的整数值,该整数值表示用于检查100块规则的区块头。

Deserializing in Molecule Molecule的反序列化

有了自定义数据结构的molecule定义,我们需要首先把它们转换成一种格式,可以被molecule的JavaScript实现使用:


$ cd $TOP/htlc-template

$ cargo install moleculec

$ moleculec --language - --format json --schema-file htlc.mol > src/htlc.json

$ npx moleculec-js -ns src/htlc.json > src/htlc-combined.json

现在我们可以填充加载当前脚本的代码,并将序列化的args解析成一个有效的结构:


$ cd $TOP/htlc-template

$ cat src/index.js

const { Molecule } = require('molecule-javascript')

const schema = require('../schema/blockchain-combined.json')

const names = schema.declarations.map(declaration => declaration.name)

const scriptTypeIndex = names.indexOf('Script')

const scriptType = new Molecule(schema.declarations[scriptTypeIndex])

// Write your script logic here.

const customSchema = require('./htlc-combined.json')

const customNames = customSchema.declarations.map(d => d.name)

const htlcArgsIndex = customNames.indexOf('HtlcArgs')

const htlcArgsType = new Molecule(customSchema.declarations[htlcArgsIndex])

function bytesToHex(b) {

return "0x" + Array.prototype.map.call(

new Uint8Array(b),

function(x) {

return ('00' + x.toString(16)).slice(-2)

}

).join('')

}

function hexStringArrayToHexString(a) {

let s = "0x";

for (let i = 0; i < a.length; i++) {

s = s + a[i].substr(2)

}

return s

}

const current_script = scriptType.deserialize(bytesToHex(CKB.load_script(0)))

const args = hexStringArrayToHexString(current_script[2][1])

const htlcArgs = htlcArgsType.deserialize(args)

CKB.debug(`a: ${hexStringArrayToHexString(htlcArgs[0][1])}`)

CKB.debug(`b: ${hexStringArrayToHexString(htlcArgs[1][1])}`)

CKB.debug(`c: ${hexStringArrayToHexString(htlcArgs[2][1])}`)

如果我们暂时忽略簿记代码,这里重要的是,我们首先使用CKB系统调用加载脚本,解析脚本结构,然后得到args:


const current_script = scriptType.deserialize(bytesToHex(CKB.load_script(0)))

const args = hexStringArrayToHexString(current_script[2][1])

const htlcArgs = htlcArgsType.deserialize(args)

我们假设脚本args包含上面定义的序列化 HtlcArgs 结构,然后我们应用类似的方法来精确化它们:


const htlcArgs = htlcArgsType.deserialize(args)

我已经在框架中提供了一些有意义的数据,因此,如果我们尝试执行脚本:


$ cd $TOP/htlc-template

$ npm run build

$ cd $TOP/htlc-runner

$ RUST_LOG=debug `./runner.js ../ckb-duktape/build/load0 ../htlc-template/build/duktape.js`

DEBUG:<unknown>: script group: Byte32(0x35ab3d033e66c426573ed4b7ce816e248cb042d908fd8cfe7bba27acb37fb108) DEBUG OUTPUT: a: 0x32e555f3ff8e135cece1351a6a2971518392c1e30375c1e006ad0ce8eac07947

DEBUG:<unknown>: script group: Byte32(0x35ab3d033e66c426573ed4b7ce816e248cb042d908fd8cfe7bba27acb37fb108) DEBUG OUTPUT: b: 0xc219351b150b900e50a7039f1e448b844110927e5fd9bd30425806cb8ddff1fd

DEBUG:<unknown>: script group: Byte32(0x35ab3d033e66c426573ed4b7ce816e248cb042d908fd8cfe7bba27acb37fb108) DEBUG OUTPUT: c: 0x970dd9a8

Run result: Ok(0)

我们可以从调试日志中找到解析后的结果。

1 Like