CKB 脚本编程简介[7]: Duktape 高级编程
Xuejie Xiao
Nervos Core Team我在之前介绍过 duktape,展示了如何在Nervos CKB上运行 JavaScript代码。但到目前为止,我所展示的代码都是非常简单逻辑的单个代码段。如果我们要解析CKB 数据结构呢?如果我们需要在脚本中引入外部的库呢?
在这篇文章里,我们将创建一个 CKB 脚本项目:duktape-powered,这个项目有以下需求:
- 外部库依赖
- CKB数据结构的序列化/反序列化
- 进行哈希计算
在继续这篇文章之前,我想说的是,这篇文章中的主要工作不是我写的。这要归功于我的一位同事,他花了很多的精力写了一个非常好的模版,我们才可以在这里使用,我们才可以通过JavaScript和duktape获得精简的CKB脚本开发体验。
这篇文章是基于现在的CKB Lina 主网版本写的。
Scope 范围
在这篇文章中, 我们用JavaScript写一个简单的 HTLC脚本。我不得不承认,我不是世界上最好的老师,有很多很多人在 解释HTLC上做的比我好。 所以如果你想知道什么是HTLC,可以先查看其他资料,然后再回来。
现在我假设你已经弄明白了 HTLC,我们在这里创建 HTLC 脚本,如果满足以下任何一个条件,脚本将被解锁:
- 提供正确的密码字符串和有效的公钥 A 签名;
- 经过一定时间后,提供公钥 B 的有效签名
在设计我们的 HTLC 脚本时,还有几点需要注意:
为了简单起见,这里我们将使用一个技巧来进行签名验证:我们将依赖一个单独的 cell 来提供正确公钥的签名,而不是直接在 JavaScript 中进行签名验证。在这篇文章的后面,我们将解释在 JavaScript 中签名验证的后果和考虑;
CKB HTLC 脚本结构的
args
部分将包含正确的秘密字符串的散列,因此当脚本运行时,它可以对提供的秘密字符串运行散列函数,测试它是否正确;时间量总是设置为 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
现在可以用你最喜欢的编辑器打开 htlc-template
项目中的src/index.js
文件,文件的内容如下:
$ cd $TOP/htlc-template$ cat src/index.jsconst { 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)
我们将修改这个文件以添加我们需要的逻辑。
脚本调试器准备工作
为了帮助脚本编程,让我们组合一个调试环境。调试环境有两个目的:
- 准备一个完整的交易,可以加载到 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.lockconst 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 bashroot@18d4b1952624:/# cd /coderoot@18d4b1952624:/code# makeroot@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 脚本。
自定义参数
在 CKB 上运行的脚本上,提供了 2 个位置用于保存参数:
args
字段在Script
结构中witnesses
字段在Transaction
结构中
它们之间的区别是,args
字段用于保存对同一脚本的所有使用都保持相同的参数,而 witness
字段用于一次性交易验证中使用的临时参数。这里的一个例子是:对于进行签名验证的脚本,args
字段通常用于存储公钥散列,而 witness
字段用于保存有效签名。
为了获得最大的灵活性,args
字段和 witness
数组字段中的每个项都是纯原始字节。dapp 开发人员需要设计他们想要保存的数据的实际格式。在HTLC的脚本中,我们使用 molecule序列化格式。 Molecule 在 CKB 中得到了广泛的应用。 如果你想要与 CKB 交互,例如读取当前交易中使用的某些 cell/脚本,你将需要处理 molecule 格式。现在,这是一个很好的机会,来解释一个人如何通过 molecule 与 CKB 相互作用,因此,我们将以 molecule 格式实现 args
和 witness
使用的自定义结构。尽管你可以在自己的脚本中自由使用任何序列化格式。
让我们先创建一个文件,需要2个数据结构:
$ cd $TOP/htlc-template$ cat htlc.molarray 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 块规则的区块头。
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.jsconst { 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])}`)
如果我们暂时忽略 bookkeeping 代码,这里重要的是,我们首先使用 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: 0x32e555f3ff8e135cece1351a6a2971518392c1e30375c1e006ad0ce8eac07947DEBUG:<unknown>: script group: Byte32(0x35ab3d033e66c426573ed4b7ce816e248cb042d908fd8cfe7bba27acb37fb108) DEBUG OUTPUT: b: 0xc219351b150b900e50a7039f1e448b844110927e5fd9bd30425806cb8ddff1fdDEBUG:<unknown>: script group: Byte32(0x35ab3d033e66c426573ed4b7ce816e248cb042d908fd8cfe7bba27acb37fb108) DEBUG OUTPUT: c: 0x970dd9a8Run result: Ok(0)
我们可以从调试日志中找到解析后的结果。
Adding new library 添加新库
我想在这里展示的另一件事是,你可以在npm上引入许多库,假设:
- 该库有一个 ES5 版本(或者可以实际调整 webpack pipeline 以添加 polyfills);
- 它完全是在 JavaScript 中实现的,没有本地代码
在 HTLC 脚本中,我将添加crc32,并使用 crc32 计算密码字符串的哈希值。我想在这里再次提到,CRC32 一直都不是安全的哈希函数。我们选择它是出于简单性,而不是安全性。在生产环境中,应该使用真正的安全哈希函数,而不是使用 JavaScript。但是现在,crc32 对于我们的教程来说是非常合适的 :P
让我们在模板中包含 crc32,并编写一些调试代码来测试它:
$ cd $TOP/htlc-template$ npm install --save crc32$ cat src/index.jsconst { 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(`c: ${hexStringArrayToHexString(htlcArgs[2][1])}`)
const crc32 = require('crc32')CKB.debug(crc32('i am a secret'))$ 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: c: 0x970dd9a8DEBUG:<unknown>: script group: Byte32(0x35ab3d033e66c426573ed4b7ce816e248cb042d908fd8cfe7bba27acb37fb108) DEBUG OUTPUT: 970dd9a8Run result: Ok(0)
你可能会注意到,我们在这里打印的2个值是完全相同的! 这是因为 i am a secret
正是我在准备框架时选择的密码字符串。
把合约合并起来
有了所有的库和所需的知识,我们现在可以完成实现脚本了:
$ cd $TOP/htlc-template$ cat src/index.jsconst { 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 hexStringArrayToString(a) { let s = ""; for (let i = 0; i < a.length; i++) { s = s + String.fromCharCode(parseInt(a[i])) } return s}
function hexStringArrayToHexString(a) { let s = "0x"; for (let i = 0; i < a.length; i++) { s = s + a[i].substr(2) } return s}
function parseLittleEndianHexStringArray(a) { let v = 0 const l = a.length for (let i = 0; i < l; i++) { v = (v << 8) | parseInt(a[l - i - 1]) } return v}
const current_script = scriptType.deserialize(bytesToHex(CKB.load_script(0)))const args = hexStringArrayToHexString(current_script[2][1])const htlcArgs = htlcArgsType.deserialize(args)
// Load and parse witness data using the same method as aboveconst htlcWitnessIndex = customNames.indexOf('HtlcWitness')const htlcWitnessType = new Molecule(customSchema.declarations[htlcWitnessIndex])
const rawWitness = CKB.load_witness(0, 0, CKB.SOURCE.GROUP_INPUT)if (typeof rawWitness === 'number') { throw new Error(`Invalid response when loading witness: ${rawWitness}`)}const htlcWitness = htlcWitnessType.deserialize(bytesToHex(rawWitness))
let lockHashToMatch;if (htlcWitness[0][1].length > 0) { // Test secret string hash const crc32 = require('crc32') const hash = '0x' + crc32(hexStringArrayToString(htlcWitness[0][1])) if (hash !== hexStringArrayToHexString(htlcArgs[2][1])) { throw new Error(`Invalid secret string!`) } lockHashToMatch = hexStringArrayToHexString(htlcArgs[0][1])} else { // Test header block const headerTypeIndex = names.indexOf('Header') const headerType = new Molecule(schema.declarations[headerTypeIndex])
// Load header for current input first const rawInputHeader = CKB.load_header(0, 0, CKB.SOURCE.GROUP_INPUT) if (typeof rawWitness === 'number') { throw new Error(`Invalid response when loading input header: ${rawInputHeader}`) } const inputHeader = headerType.deserialize(bytesToHex(rawInputHeader)) const inputHeaderNumber = parseLittleEndianHexStringArray(inputHeader[0][1][3][1])
const targetHeaderIndex = parseLittleEndianHexStringArray(htlcWitness[1][1]) const rawTargetHeader = CKB.load_header(0, targetHeaderIndex, CKB.SOURCE.HEADER_DEP) if (typeof rawTargetHeader === 'number') { throw new Error(`Invalid response when loading target header: ${rawTargetHeader}`) } const targetHeader = headerType.deserialize(bytesToHex(rawTargetHeader)) const targetHeaderNumber = parseLittleEndianHexStringArray(targetHeader[0][1][3][1])
if (targetHeaderNumber < inputHeaderNumber + 100) { throw new Error(`Timeout period has not reached!`) } lockHashToMatch = hexStringArrayToHexString(htlcArgs[1][1])}
// Now we know which lock hash to test against, we look for an input cell// with the specified lock hashlet i = 0while (true) { const rawHash = CKB.load_cell_by_field(0, i, CKB.SOURCE.INPUT, CKB.CELL.LOCK_HASH) if (rawHash == CKB.CODE.INDEX_OUT_OF_BOUND) { throw new Error(`Cannot find input cell using lock hash ${lockHashToMatch}`) } if (typeof rawHash === 'number') { throw new Error(`Invalid response when loading input cell: ${rawHash}`) } if (bytesToHex(rawHash) == lockHashToMatch) { break } i += 1}
它使用类似于上面所示的技术来解析见证和块标头,它们也是 molecule 格式的。
有一个技巧值得一提:在 HTLC 脚本的设计中,我提到脚本需要对给定的公钥进行签名验证。我们在这里的实际实现对此设计进行了概括:
我们不是测试给定的公钥,而是测试整个锁定脚本哈希。 虽然这肯定满足了我们的要求,但它提供了更多的可能性:如果每个人都使用默认的 secp256k1 锁定脚本,则不同的公钥将反映在脚本 args 部分中,从而导致不同的锁定脚本。 因此,测试锁脚本可以确保使用不同的公钥。 另一方面,并不是每个人都使用默认的 secp256k1 锁定脚本,因此直接测试锁定脚本哈希,可以提高 HTLC 脚本使用的灵活性。
虽然可以肯定地将签名验证逻辑嵌入 HTLC 脚本中,但是我们在这里选择了另一种更简单的解决方案:我们只测试其中一个输入 cell 是否具有指定的锁脚本。 根据 CKB 的验证规则,如果交易被区块链接受,则每个输入 cell 的锁定脚本必须通过验证,这意味着 HTLC 脚本中指定的锁定脚本也将通过验证,从而满足 HTLC 脚本的验证规则。
总而言之,我们实际上展示了两个模式,可以方便地在 CKB 上设计 dapps:
不需要测试公钥的签名验证,可以测试锁定脚本的验证以实现灵活性。
不需要复制不同的锁定脚本,可以使用相同的锁定 检查输入cell是否存在,并将验证工作委派给输入cell的锁脚本。
从根本上讲,这取决于你的用例,来查看这些模式是否可以适用。 稍后,我们还可以通过动态链接到供应模式2来构建真正的可组合脚本。 但是,当你可以简单地通过它们进行设计时,将这些存储在你的工具库中,可能会很有用。
始终审计你的脚本
最后一点需要注意的是,在部署脚本并将真正的 token 放入脚本之前,应该始终记住审计脚本。上面的 HTLC 脚本主要用于介绍。我很容易就能发现其中的一些弱点。你不应该直接在CKB主网上使用它。但是,它确实提供了一个非常好的练习,所以如果你感兴趣,可以随意阅读脚本,看自己能否发现漏洞 :P
在链上运行 HTLC 脚本
对 CKB 脚本的测试分为两部分:之前,我们使用了一个 off-chain 调试器环境来测试脚本,以加快迭代速度。 现在我们有了完整的 HTLC 脚本,我们还应该将它部署到开发链上,并测试整个工作流。毕竟任何区块链智能合约都不能单独存在,它们必须有一个环境,来帮助准备交易并在链上调用它们。CKB 更是如此,因为 CKB 使用独立的验证器-生成器模型。
为了在链上测试我们的 HTLC 脚本,我们将再次使用 htlc-runner
环境,并编写一些可以在链上部署和测试 HTLC 脚本的节点可执行程序。 我们将编写的第一个可执行文件,这个可执行文件,可以在链上部署 duktape 二进制文件以及我们的 HTLC 脚本:
$ cd $TOP/htlc-runner$ cat deploy_scripts.js#!/usr/bin/env node
const CKB = require("@nervosnetwork/ckb-sdk-core").defaultconst utils = require("@nervosnetwork/ckb-sdk-utils")const process = require('process')const fs = require('fs')
if (process.argv.length !== 6) { console.log(`Usage: ${process.argv[1]} <duktape load0 binary> <js script> <private key> <node URL>`) process.exit(1)}
const duktapeBinary = fs.readFileSync(process.argv[2])const jsScript = fs.readFileSync(process.argv[3])
const privateKey = process.argv[4]const nodeUrl = process.argv[5]
const run = async () => { const ckb = new CKB(nodeUrl) const secp256k1Dep = await ckb.loadSecp256k1Dep()
const publicKey = ckb.utils.privateKeyToPublicKey(privateKey) const publicKeyHash = `0x${ckb.utils.blake160(publicKey, 'hex')}`
const lockScript = { hashType: secp256k1Dep.hashType, codeHash: secp256k1Dep.codeHash, args: publicKeyHash } const lockHash = ckb.utils.scriptToHash(lockScript)
const unspentCells = await ckb.loadCells({ lockHash }) const totalCapacity = unspentCells.reduce((sum, cell) => sum + BigInt(cell.capacity), 0n)
// For simplicity, we will just use 1 CKB as fee. On a real setup you // might not want to do this. const fee = 100000000n const duktapeBinaryCapacity = BigInt(duktapeBinary.length) * 100000000n + 4100000000n const jsScriptCapacity = BigInt(jsScript.length) * 100000000n + 4100000000n
const outputs = [ { lock: { codeHash: '0x0000000000000000000000000000000000000000000000000000000000000000', hashType: 'data', args: '0x' }, type: null, capacity: '0x' + duktapeBinaryCapacity.toString(16) }, { lock: { codeHash: '0x0000000000000000000000000000000000000000000000000000000000000000', hashType: 'data', args: '0x' }, type: null, capacity: '0x' + jsScriptCapacity.toString(16) }, { lock: lockScript, type: null, capacity: '0x' + (totalCapacity - jsScriptCapacity - duktapeBinaryCapacity - fee).toString(16) } ] const outputsData = [ utils.bytesToHex(duktapeBinary), utils.bytesToHex(jsScript), '0x' ]
const transaction = { version: '0x0', cellDeps: [ { outPoint: secp256k1Dep.outPoint, depType: 'depGroup' } ], headerDeps: [], inputs: unspentCells.map(cell => ({ previousOutput: cell.outPoint, since: '0x0' })), outputs, witnesses: [ { lock: '', inputType: '', outputType: '' } ], outputsData } const signedTransaction = ckb.signTransaction(privateKey)(transaction)
const txHash = await ckb.rpc.sendTransaction(signedTransaction, 'passthrough')
console.log(`Transaction hash: ${txHash}`) fs.writeFileSync('deploy_scripts_result.txt', txHash)}
run()
第二个可执行文件使用 HTLC 脚本创建一个 cell 作为锁:
$ cd $TOP/htlc-runner$ cat create_htlc_cell.js#!/usr/bin/env node
const { Molecule } = require('molecule-javascript')const crc32 = require('crc32')const CKB = require("@nervosnetwork/ckb-sdk-core").defaultconst 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 !== 8) { console.log(`Usage: ${process.argv[1]} <duktape load0 binary> <deployed tx hash> <private key> <node URL> <lock hash A> <lock hash B>`) process.exit(1)}
const duktapeBinary = fs.readFileSync(process.argv[2])const duktapeHash = blake2b(duktapeBinary)
const deployedTxHash = process.argv[3]const privateKey = process.argv[4]const nodeUrl = process.argv[5]const lockHashA = process.argv[6]const lockHashB = process.argv[7]
function hexStringToHexStringArray(s) { let arr = [] for (let i = 2; i < s.length; i += 2) { arr.push('0x' + s.substr(i, 2)) } return arr}
const run = async () => { const ckb = new CKB(nodeUrl) const secp256k1Dep = await ckb.loadSecp256k1Dep()
const publicKey = ckb.utils.privateKeyToPublicKey(privateKey) const publicKeyHash = `0x${ckb.utils.blake160(publicKey, 'hex')}`
const lockScript = { hashType: secp256k1Dep.hashType, codeHash: secp256k1Dep.codeHash, args: publicKeyHash } const lockHash = ckb.utils.scriptToHash(lockScript)
const unspentCells = await ckb.loadCells({ lockHash }) const totalCapacity = unspentCells.reduce((sum, cell) => sum + BigInt(cell.capacity), 0n)
// For simplicity, we will just use 1 CKB as fee. On a real setup you // might not want to do this. const fee = 100000000n const htlcCellCapacity = 200000000000n
const customSchema = JSON.parse(fs.readFileSync('../htlc-template/src/htlc-combined.json')) const htlcArgsType = new Molecule( customSchema.declarations.find(d => d.name == "HtlcArgs")) const htlcScriptArgs = htlcArgsType.serialize([ ['a', hexStringToHexStringArray(lockHashA)], ['b', hexStringToHexStringArray(lockHashB)], ['hash', hexStringToHexStringArray('0x' + crc32('i am a secret'))] ])
const transaction = { version: '0x0', cellDeps: [ { outPoint: secp256k1Dep.outPoint, depType: 'depGroup' } ], headerDeps: [], inputs: unspentCells.map(cell => ({ previousOutput: cell.outPoint, since: '0x0' })), outputs: [ { lock: { codeHash: utils.bytesToHex(duktapeHash), hashType: 'data', args: htlcScriptArgs }, type: null, capacity: '0x' + htlcCellCapacity.toString(16) }, { lock: lockScript, type: null, capacity: '0x' + (totalCapacity - fee - htlcCellCapacity).toString(16) } ], witnesses: [ { lock: '', inputType: '', outputType: '' } ], outputsData: [ '0x', '0x' ] } const signedTransaction = ckb.signTransaction(privateKey)(transaction)
const txHash = await ckb.rpc.sendTransaction(signedTransaction, 'passthrough')
console.log(`Transaction hash: ${txHash}`) fs.writeFileSync('create_htlc_cell_result.txt', txHash)}
run()
值得一提的是,这个可执行文件展示了我们如何序列化一个 molecule 格式的数据结构:
// ...
function hexStringToHexStringArray(s) { let arr = [] for (let i = 2; i < s.length; i += 2) { arr.push('0x' + s.substr(i, 2)) } return arr}
// ...
const customSchema = JSON.parse(fs.readFileSync('../htlc-template/src/htlc-combined.json'))const htlcArgsType = new Molecule( customSchema.declarations.find(d => d.name == "HtlcArgs"))const htlcScriptArgs = htlcArgsType.serialize([ ['a', hexStringToHexStringArray(lockHashA)], ['b', hexStringToHexStringArray(lockHashB)], ['hash', hexStringToHexStringArray('0x' + crc32('i am a secret'))]])
// ...
现在一个可执行文件试图解锁 HTLC 保护 cell 提供加密字符串:
$ cd $TOP/htlc-runner$ cat unlock_via_secret_string.js#!/usr/bin/env node
const { Molecule } = require('molecule-javascript')const crc32 = require('crc32')const CKB = require("@nervosnetwork/ckb-sdk-core").defaultconst 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 !== 8) { console.log(`Usage: ${process.argv[1]} <deployed tx hash> <htlc cell tx hash> <private key> <node URL> <secret string> <dry run>`) process.exit(1)}
const deployedTxHash = process.argv[2]const htlcCellTxHash = process.argv[3]const privateKey = process.argv[4]const nodeUrl = process.argv[5]const secretString = process.argv[6]const dryrun = process.argv[7] === 'true'
function stringToHexStringArray(s) { let a = [] for (let i = 0; i < s.length; i++) { a.push('0x' + ('00' + s.charCodeAt(i).toString(16)).slice(-2)) } return a}
const run = async () => { const ckb = new CKB(nodeUrl) const secp256k1Dep = await ckb.loadSecp256k1Dep()
const publicKey = ckb.utils.privateKeyToPublicKey(privateKey) const publicKeyHash = `0x${ckb.utils.blake160(publicKey, 'hex')}`
const lockScript = { hashType: secp256k1Dep.hashType, codeHash: secp256k1Dep.codeHash, args: publicKeyHash } const lockHash = ckb.utils.scriptToHash(lockScript)
const unspentCells = await ckb.loadCells({ lockHash }) const totalCapacity = unspentCells.reduce((sum, cell) => sum + BigInt(cell.capacity), 0n)
// For simplicity, we will just use 1 CKB as fee. On a real setup you // might not want to do this. const fee = 100000000n const htlcCellCapacity = 200000000000n
const customSchema = JSON.parse(fs.readFileSync('../htlc-template/src/htlc-combined.json')) const htlcWitnessType = new Molecule( customSchema.declarations.find(d => d.name == "HtlcWitness")) const htlcWitness = htlcWitnessType.serialize([ ['s', stringToHexStringArray(secretString)], ['i', ['0x0', '0x0', '0x0', '0x0']] ])
const transaction = { version: '0x0', cellDeps: [ // Due to the requirement of load0 duktape binary, JavaScript source cell // should be the first one in cell deps { outPoint: { txHash: deployedTxHash, index: "0x1" }, depType: 'code' }, { outPoint: { txHash: deployedTxHash, index: "0x0" }, depType: 'code' }, { outPoint: secp256k1Dep.outPoint, depType: 'depGroup' } ], headerDeps: [], inputs: unspentCells.map(cell => ({ previousOutput: cell.outPoint, since: '0x0' })), outputs: [ { lock: lockScript, type: null, capacity: '0x' + (totalCapacity + htlcCellCapacity - fee).toString(16) } ], witnesses: unspentCells.map(_cell => '0x'), outputsData: [ '0x', '0x' ] } transaction.inputs.push({ previousOutput: { txHash: htlcCellTxHash, index: "0x0" }, since: '0x0' }) transaction.witnesses[0] = { lock: '', inputType: '', outputType: '' } const signedTransaction = ckb.signTransaction(privateKey)(transaction) signedTransaction.witnesses.push(htlcWitness)
if (dryrun) { try { const result = await ckb.rpc.dryRunTransaction(signedTransaction) console.log(`Dry run success result: ${JSON.stringify(result, null, 2)}`) } catch (e) { console.log(`Dry run failure result: ${JSON.stringify(JSON.parse(e.message), null, 2)}`) } } else { const txHash = await ckb.rpc.sendTransaction(signedTransaction, 'passthrough')
console.log(`Transaction hash: ${txHash}`) fs.writeFileSync('unlock_via_secret_string_result.txt', txHash) }}
run()
最后一个可执行文件试图解锁 HTLC 保护的 cell,假设等待期已经过去:
$ cd $TOP/htlc-runner$ cat unlock_via_timeout.js#!/usr/bin/env node
const { Molecule } = require('molecule-javascript')const crc32 = require('crc32')const CKB = require("@nervosnetwork/ckb-sdk-core").defaultconst 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 !== 8) { console.log(`Usage: ${process.argv[1]} <deployed tx hash> <htlc cell tx hash> <private key> <node URL> <header hash> <dry run>`) process.exit(1)}
const deployedTxHash = process.argv[2]const htlcCellTxHash = process.argv[3]const privateKey = process.argv[4]const nodeUrl = process.argv[5]const headerHash = process.argv[6]const dryrun = process.argv[7] === 'true'
const run = async () => { const ckb = new CKB(nodeUrl) const secp256k1Dep = await ckb.loadSecp256k1Dep()
const htlcCellTx = await ckb.rpc.getTransaction(htlcCellTxHash) const htlcCellHeaderHash = htlcCellTx.txStatus.blockHash
const publicKey = ckb.utils.privateKeyToPublicKey(privateKey) const publicKeyHash = `0x${ckb.utils.blake160(publicKey, 'hex')}`
const lockScript = { hashType: secp256k1Dep.hashType, codeHash: secp256k1Dep.codeHash, args: publicKeyHash } const lockHash = ckb.utils.scriptToHash(lockScript)
const unspentCells = await ckb.loadCells({ lockHash }) const totalCapacity = unspentCells.reduce((sum, cell) => sum + BigInt(cell.capacity), 0n)
// For simplicity, we will just use 1 CKB as fee. On a real setup you // might not want to do this. const fee = 100000000n const htlcCellCapacity = 200000000000n
const customSchema = JSON.parse(fs.readFileSync('../htlc-template/src/htlc-combined.json')) const htlcWitnessType = new Molecule( customSchema.declarations.find(d => d.name == "HtlcWitness")) const htlcWitness = htlcWitnessType.serialize([ ['s', []], ['i', ['0x1', '0x0', '0x0', '0x0']] ])
const transaction = { version: '0x0', cellDeps: [ // Due to the requirement of load0 duktape binary, JavaScript source cell // should be the first one in cell deps { outPoint: { txHash: deployedTxHash, index: "0x1" }, depType: 'code' }, { outPoint: { txHash: deployedTxHash, index: "0x0" }, depType: 'code' }, { outPoint: secp256k1Dep.outPoint, depType: 'depGroup' } ], headerDeps: [ htlcCellHeaderHash, headerHash, ], inputs: unspentCells.map(cell => ({ previousOutput: cell.outPoint, since: '0x0' })), outputs: [ { lock: lockScript, type: null, capacity: '0x' + (totalCapacity + htlcCellCapacity - fee).toString(16) } ], witnesses: unspentCells.map(_cell => '0x'), outputsData: [ '0x', '0x' ] } transaction.inputs.push({ previousOutput: { txHash: htlcCellTxHash, index: "0x0" }, since: '0x0' }) transaction.witnesses[0] = { lock: '', inputType: '', outputType: '' } const signedTransaction = ckb.signTransaction(privateKey)(transaction) signedTransaction.witnesses.push(htlcWitness)
if (dryrun) { try { const result = await ckb.rpc.dryRunTransaction(signedTransaction) console.log(`Dry run success result: ${JSON.stringify(result, null, 2)}`) } catch (e) { console.log(`Dry run failure result: ${JSON.stringify(JSON.parse(e.message), null, 2)}`) } } else { const txHash = await ckb.rpc.sendTransaction(signedTransaction, 'passthrough')
console.log(`Transaction hash: ${txHash}`) fs.writeFileSync('unlock_via_timeout_result.txt', txHash) }}
run()
我们将 HTLC 输入 cell 的区块头 dep 放在索引 0 处,将测试当前时间戳的头放置在索引 1 处,因此,当我们准备 witness 数据时,我们将 0x01000000 用于i
,这是1的小字端表示。
这也提供了不同的启发。要在 CKB 中证明某个时间已经过去,可以使用 Nervos DAO validator 脚本中所示的since
字段,也可以在链上包括一个区块头,并依靠区块头的区块高度或时间戳来证明已经达到了某个时间。这取决于你的用例,才能确定哪一个是更好的选择。
在这里准备好所有 4 个可执行文件之后,我们就可以开始使用 HTLC 脚本了。 但首先,让我们先运行一个新的 CKB 开发链。
$ cd $TOP$ export CKB="<path to your ckb binary>"$ $CKB --versionckb 0.28.0 (728eff2 2020-02-04)# Block assembler args configured here correspond to the following private key:# 0x0a14c6fd7af6a3f13c9e2aacad80d78968de5d068a342828080650084bf20104$ $CKB init -c dev -C ckb-data --ba-arg 0x5a7487f529b8b8fd4d4a57c12dc0c70f7958a196$ $CKB run -C ckb-data
在另一个终端,我们启动一个挖矿实例:
$ cd $TOP$ $CKB miner -C ckb-data
我们使用 CKB 的开发链,因为已经有两个带有余额的方便的地址,在测试前我们不需要挖矿。此外,使用开发链,可以自定义出块速度。只要你愿意,也可以使用测试网,需要记住的是不要使用主网进行测试。
随着 CKB 实例的运行,可以对 HTLC 脚本进行部署和测试。
# Make sure the HTLC script is successfully built first$ cd $TOP/htlc-template$ npm run build# Ensure all scripts are runnable$ cd $TOP/htlc-runner$ chmod +x deploy_scripts.js$ chmod +x create_htlc_cell.js$ chmod +x unlock_via_secret_string.js$ chmod +x unlock_via_timeout.js
# Let's first deploy duktape binary and JS scripts$ ./deploy_scripts.js \ ../ckb-duktape/build/load0 \ ../htlc-template/build/duktape.js \ 0xd00c06bfd800d27397002dca6fb0993d5ba6399b4238b2f29ee9deb97593d2bc \ "http://127.0.0.1:8114/"This method is only for demo, don't use it in productionTransaction hash: 0xf30e1e8989fc3a4cb1e52dacc85090f8ff74b05e008d636b8c9154f5c296e1f4
# Let's create a HTLC cell$ ./create_htlc_cell.js \ ../ckb-duktape/build/load0 \ 0xf30e1e8989fc3a4cb1e52dacc85090f8ff74b05e008d636b8c9154f5c296e1f4 \ 0xd00c06bfd800d27397002dca6fb0993d5ba6399b4238b2f29ee9deb97593d2bc \ "http://127.0.0.1:8114/" \ 0x32e555f3ff8e135cece1351a6a2971518392c1e30375c1e006ad0ce8eac07947 \ 0xc219351b150b900e50a7039f1e448b844110927e5fd9bd30425806cb8ddff1fdThis method is only for demo, don't use it in productionTransaction hash: 0x7de8ea6b0d6cb9941e76976d1d55edf844c4fa81485e00fb8eba2d161b5830cd
# To save us the hassle of recreating cells, both unlock executables support# a dry run mode, where we only does full transaction verification, but do not# commit the success ones on chain.# First let's show that we can unlock a HTLC cell given the right secret string# and lock script$ ./unlock_via_secret_string.js \ 0xf30e1e8989fc3a4cb1e52dacc85090f8ff74b05e008d636b8c9154f5c296e1f4 \ 0x7de8ea6b0d6cb9941e76976d1d55edf844c4fa81485e00fb8eba2d161b5830cd \ 0xd00c06bfd800d27397002dca6fb0993d5ba6399b4238b2f29ee9deb97593d2bc \ "http://127.0.0.1:8114/" \ "i am a secret" \ trueThis method is only for demo, don't use it in productionDry run success result: { "cycles": "0xb1acc38"}
# Given an invalid secret string, the transaction would fail the validation.# If you have enabled debug output in CKB's configuration like mentioned here:# https://docs.nervos.org/dev-guide/debugging-ckb-script.html#debug-syscall# you can notice the failure lines in CKB's debug logs.$ ./unlock_via_secret_string.js \ 0xf30e1e8989fc3a4cb1e52dacc85090f8ff74b05e008d636b8c9154f5c296e1f4 \ 0x7de8ea6b0d6cb9941e76976d1d55edf844c4fa81485e00fb8eba2d161b5830cd \ 0xd00c06bfd800d27397002dca6fb0993d5ba6399b4238b2f29ee9deb97593d2bc \ "http://127.0.0.1:8114/" \ "invalid secret" \ trueDry run failure result: { "code": -3, "message": "Error { kind: ValidationFailure(-2) ...}"}
# Given the correct secret string but an invalid public key, this would still# fail the validation:$ ./unlock_via_secret_string.js \ 0xf30e1e8989fc3a4cb1e52dacc85090f8ff74b05e008d636b8c9154f5c296e1f4 \ 0x7de8ea6b0d6cb9941e76976d1d55edf844c4fa81485e00fb8eba2d161b5830cd \ 0x63d86723e08f0f813a36ce6aa123bb2289d90680ae1e99d4de8cdb334553f24d \ "http://127.0.0.1:8114/" \ "i am a secret" \ trueDry run failure result: { "code": -3, "message": "Error { kind: ValidationFailure(-2) ...}"}
# Now we've tested unlocking by providing secret string, let's try unlocking# via waiting enough time. In my setup, I have the following values:# HTLC cell is packed in transaction:# 0xf30e1e8989fc3a4cb1e52dacc85090f8ff74b05e008d636b8c9154f5c296e1f4# which is commited in block:# 0x04539cff3e1a106773bc1ec35804340c0981804093ce8d7a17e9ebc37a3268ff# whose block number is 399.## I'm gonna test it with block:# 0xe93ebb311d156847fbcdc159d1fa3c38f12613121e51582272d909379c4d1a60# whose block number is 409, and block:# 0x665ccfab2d854afa035f4697a2301f2bad9d4aa86506090b104f8ed18772ca01# whose block number is 510.# Let's first try block 510 to verify that we can unlock the HTLC cell this way:$ ./unlock_via_timeout.js \ 0xf30e1e8989fc3a4cb1e52dacc85090f8ff74b05e008d636b8c9154f5c296e1f4 \ 0x7de8ea6b0d6cb9941e76976d1d55edf844c4fa81485e00fb8eba2d161b5830cd \ 0x63d86723e08f0f813a36ce6aa123bb2289d90680ae1e99d4de8cdb334553f24d \ "http://127.0.0.1:8114/" \ 0x665ccfab2d854afa035f4697a2301f2bad9d4aa86506090b104f8ed18772ca01 \ trueThis method is only for demo, don't use it in productionDry run success result: { "cycles": "0x16c500ba" }# Notice here we are unlocking using lock script hash:# 0x63d86723e08f0f813a36ce6aa123bb2289d90680ae1e99d4de8cdb334553f24d# which is different from unlocking by providing secret string.
# Now let's try block 409 here:$ ./unlock_via_timeout.js \ 0xf30e1e8989fc3a4cb1e52dacc85090f8ff74b05e008d636b8c9154f5c296e1f4 \ 0x7de8ea6b0d6cb9941e76976d1d55edf844c4fa81485e00fb8eba2d161b5830cd \ 0x63d86723e08f0f813a36ce6aa123bb2289d90680ae1e99d4de8cdb334553f24d \ "http://127.0.0.1:8114/" \ 0xe93ebb311d156847fbcdc159d1fa3c38f12613121e51582272d909379c4d1a60 \ trueDry run failure result: { "code": -3, "message": "Error { kind: ValidationFailure(-2) ...}"}# As expected, this fails validatin, and if we check CKB's debug log(if you# have enabled it), we can find log lines containing "Timeout period has not# reached!", proving our script works as expected.
# One final step would checking unlocking with enough waiting, but using the# wrong public key.$ ./unlock_via_timeout.js \ 0xf30e1e8989fc3a4cb1e52dacc85090f8ff74b05e008d636b8c9154f5c296e1f4 \ 0x7de8ea6b0d6cb9941e76976d1d55edf844c4fa81485e00fb8eba2d161b5830cd \ 0xd00c06bfd800d27397002dca6fb0993d5ba6399b4238b2f29ee9deb97593d2bc \ "http://127.0.0.1:8114/" \ 0x665ccfab2d854afa035f4697a2301f2bad9d4aa86506090b104f8ed18772ca01 \ trueDry run failure result: { "code": -3, "message": "Error { kind: ValidationFailure(-2) ...}"}# As expected, this also fails validation.
请注意,在每次不同的运行过程中,生成的交易哈希可能不同。所以一定要根据需要调整 cell 的参数。
到此,我们的 HTLC 脚本将按预期运行(当然不包括那些糟糕的情况),万岁!
JavaScript 中的计算密集型代码
让我们往回跳一下。我一直避免在 HTLC 脚本中使用 JavaScript 编写签名验证代码。你可能会注意到,我们还使用了非常简单的 CRC32 哈希算法,而不是像 blake2b
这样更安全的哈希算法。 虽然我这么做的一个主要原因是为了这篇文章的简单(如果你读到这里,你会发现这篇文章已经很长了!),但仍不建议在 JavaScript 中进行这些操作,因为:
加密算法需要精确的实现,虽然我并不是说你不能做到这一点,但它确实需要在更高级的语言(如 JavaScript)中要更小心地构建加密算法。最好利用现有的以 C 或 Rust 编写的经过测试的库。
密码算法是典型的计算密集型代码,因为我们在 duktape 中运行 JavaScript 代码,它可以很容易地将代码速度降低 10 倍甚至 100 倍。本地实现可以更快,并且可以节省大量 CKB cycles。
现在这里使用的 duktape发行版只包含 duktape,没有外部库。在将来,我可能会添加某些正式版本的加密算法,如 secp256k1 和 blake2b。通过这种方式,你将能够在 JavaScript 中使用运行快速并且安全的加密算法。但是请记住,有时上面提到的委托模式可能更适合你的用例。
扼要重述
我真诚地希望你已经读到这里,没有跳过文章。这是一个可笑的长帖子,但它包含了很多有用的信息,当在 CKB 构建脚本时:
- 如何准备一个调试环境,已帮助编写脚本
- 如何建立 molecule 格式的自定义数据结构
- 如何序列化/反序列化 molecule 数据结构
- 如何在 npm 上包含外部库并打包单个 JavaScript 以供 CKB 使用
如果我发现有许多有趣的东西可以写,我可能还会在这个系列中增加更多的帖子,但我确信这个系列中现有的 7 篇帖子,加上同事们发表的许多其他很棒的帖子,已经为你在 CKB 上创建精彩的东西做好了充分的准备。我们期待你在 CKB 上开发出让大家惊叹的作品 :)