Class 7: Advanced Duktape Examples
I've introduced duktape before, shown how you can run JavaScript code on Nervos CKB. But up to this point, the code I've shown is all single piece of code with very simple logic. What if we need to parse CKB data structures? What if I need external libraries in my script? In this post we will create a duktape-powered CKB script with the following requirements:
- External library dependency
- Serialization/Deserialization of CKB data structures
- Hashing
Before continuing on this post, I want to mention that the major work used in this post, is not written by me. The credit really goes to one of my colleagues, who spent the effort putting together a very nice template we can use here, so we can have a streamlined CKB script development experience via JavaScript & duktape.
This post is written based on current CKB Lina mainnet version now.
Scope
In this post, we will write a simple HTLC script in JavaScript. Let me admit that I'm not the world's best teacher, there're many, many people who are better than me in explaining HTLC. So if you want to know what HTLC is, feel free to check other places first and come back here later.
Now I will assume you know what HTLC is :P The HTLC script we create here, will be unlocked if either one of the following conditions is met:
- A correct secret string, and a valid signature for public key A are provided;
- Certain amount of time is passed, and a valid signature for public key B is provided
And there are also several points made in the design of our HTLC script:
- For simplicity, we will use a trick to do signature verification here: instead of doing signature verification directly in JavaScript, we will rely on a separate cell to provide that a signature of the correct public key is provided. Later in this post we will explain the consequence and consideration regarding signature verifi2ation in JavaScript;
- A hash of the correct secret string will be included in
args
part of the CKB HTLC script structure, so when the script runs, it can run a hashing function on the provided secret string, testing if it is correct; - The amount of time is always set as 100 blocks. To verify 100 blocks has passed, the unlock transaction should include a block header which at least 100 blocks after the cell to unlock is committed on chain.
With the design set in stone, let's jump to the implementation now.
Getting Our Hands Dirty
While you are certainly welcome to craft the skeleton on your own, a decent template has already been prepared by one of my colleagues to save us the time. In this post, we will start from the already built template here:
$ export TOP=$(pwd)
$ git clone https://github.com/xxuejie/ckb-duktape-template htlc-template
$ cd htlc-template
$ npm install
# now you can try building the script first to ensure everything works
$ 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:
$ 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)
We will modify this file to add the logic we need.
Script Debugger Preparation
To aid script programming, let's put together a debugging environment. The debugging environment will serve 2 purposes:
- Prepare a complete transaction that can be loaded to CKB debugger;
- Create transactions and relay them to CKB
Let's first create the environment skeleton:
$ 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
Now let's create a transaction skeleton for debugger usage:
$ 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"
]
}
}
You might notice that the skeleton skips dep cell data part, this is because as we develop the HTLC script, we might need to insert different contents in the skeleton. Hence a runner here is needed to prepare the skeleton to a full transaction, then run it via CKB debugger:
$ 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`)
We need to compile duktape here:
$ 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
And also CKB debugger:
$ cd $TOP
$ git clone --recursive https://github.com/xxuejie/ckb-standalone-debugger
$ cd ckb-standalone-debugger/bins
$ cargo build --release
Now you can try running generated script:
$ 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)
This will prepare the transaction to run from duktape binary and JS script, then run it via CKB debugger, debug outputs and final results will be printed to stdout.
Or if you find a REPL more helpful, you can use the following line to execute the script and then start a REPL:
$ cd $TOP/htlc-runner
$ RUST_LOG=debug `./runner.js ../ckb-duktape/build/repl0 ../htlc-template/build/duktape.js`
duk>
With the debugger ready, let's now start to implement the HTLC script.