Tutorial: Run JavaScript Code on CKB
Tutorial Overview
As you have learned before, it's possible to use any programming language to write a Script (Smart contract) for CKB.
But how practical is it in reality? In this tutorial, you will see a full example of using JavaScript to write and execute Scripts within the CKB-VM
The process is as follows:
- Port a JavaScript Engine: You will learn how to port a JavaScript engine as a base Script to run on CKB.
- Write Business Logic in JavaScript: You will learn how to build and execute JavaScript business logic within this base Script on the CKB-VM.
This might sound complex, but thanks to the CKB-VM team, we already have a fully runnable JavaScript engine called ckb-js-vm, which was ported from quick.js and optimized for CKB-VM. We will deploy the engine on a CKB devnet and use it to run JavaScript-based Scripts on CKB.
Follow the step-by-step guide below, or check out the full code example on Github.
Get ckb-js-vm Binary
The ckb-js-vm
is a binary that can be used both in the CLI and in the on-chain CKB-VM. Let's first build the
binary and give it a try to see if it works as expected.
You will need Clang ≥v18
to build the ckb-js-vm
binary:
git clone https://github.com/nervosnetwork/ckb-js-vm
cd ckb-js-vm
git submodule update --init
make all
Now, the binary is in the build/
folder. Without writing any codes, we can use the
CKB-Debugger(another CLI tool that
enables off-chain Script development, as the name suggests) to run the ckb-js-vm
binary for a
quick test.
Install CKB-Debugger
Install CKB-Debugger using cargo. We recommend using ≥v0.113.0.
cargo install --git https://github.com/nervosnetwork/ckb-standalone-debugger ckb-debugger
On MacOS, the protoc
binary must be available to compile ckb-vm-pprof-converter
. This can be installed via Homebrew:
brew install protobuf
Quick Test with CKB-Debugger
Now let's run the ckb-js-vm
with some JS test codes.
Make sure you are in the root of the ckb-vm-js
project folder:
- Command
- Response
ckb-debugger --read-file tests/examples/hello.js --bin build/ckb-js-vm -- -r
Script log: Run from file, local access enabled. For Testing only.
Script log: hello, world
Run result: 0
All cycles: 3016765(2.9M)
With the -r
option, ckb-js-vm
will read a local JS file via CKB-Debugger. This function is
intended for testing purposes and does not function in a production environment. However, we can see the
running output, which includes a hello, world
message. The run result is 0, indicating that the hello.js
Script executes successfully.
Also, you can see how many cycles(the overhead required to execute a Script) are needed to run the JS Script in the output as well.
Integrate ckb-js-vm
ckb-js-vm
offers different ways to be integrated into your own Scripts. In the next step, we will set
up a project and writing codes to integrate ckb-js-vm
with JavaScript code to gain a deeper
understanding.
The first step is to create a new Script project. Init a folder named ckb-js-script
.
mkdir ckb-js-script
Our project relies on ckb-js-vm
, so we need to include it in the project. Create a new folder named
deps
in the root of our workspace:
cd ckb-js-script
mkdir deps
Copy ckb-js-vm/tools/compile.awk
and the ckb-js-vm
binary we built before into the deps
folder. When you're done, it should look like this:
--deps
--compile.awk
--ckb-js-vm
...
Everything looks good now!
The simplest way to run JavaScript code using ckb-js-vm
is via a Script.
Let's take a look at ckb-js-vm
Script structure:
code_hash: <code_hash to ckb-js-vm cell>
hash_type: <hash_type>
args: <ckb-js-vm args, 2 bytes> <code_hash to JavaScript code cell, 32 bytes> <hash_type to
javascript code cell, 1 byte> <javascript code args, variable length>
2 bytes ckb-js-vm args are reserved for further use
We can pass the JavaScript code as script_args
to the ckb-js-vm
base Script. In this way, we can execute our own js code and integrate the ckb-js-vm
in our project.
Write a simple hello.js
Script
cd ckb-js-script
mkdir js/build
touch js/hello.js
Fill the hello.js
with the following code:
console.log("hello, ckb-js-script!");
Compile the hello.js
into binary with CKB-Debugger
ckb-debugger --read-file js/hello.js --bin deps/ckb-js-vm -- -c | awk -f deps/compile.awk | xxd -r -p > js/build/hello.bc
If you are using Mac, change awk
to gawk
which can be installed by brew install gawk
:
ckb-debugger --read-file js/hello.js --bin deps/ckb-js-vm -- -c | gawk -f deps/compile.awk | xxd -r -p > js/build/hello.bc
Test hello.js
Script on Devnet
Now let's assemble all the Scripts and run them in a single CKB transaction.
We will use offckb
and CCC
SDK, which allows us to test it locally.
First, start a local blockchain:
offckb node
Open a new terminal, deploy the ckb-js-vm
and hello.bc
binaries to the blockchain, resulting in
2 Scripts in Live Cells.
- Command
- Response
offckb deploy --target ./deps/ckb-js-vm --type-id
wait for tx confirmed on-chain...
tx committed.
ckb-js-vm deployment.toml file /Library/Application Support/offckb-nodejs/devnet/contracts/ckb-js-vm/deployment.toml generated successfully.
ckb-js-vm migration json file /Library/Application Support/offckb-nodejs/devnet/contracts/ckb-js-vm/migrations/2024-11-18-195031.json generated successfully.
done.
- Command
- Response
offckb deploy --target ./js/build/hello.bc --type-id
wait for tx confirmed on-chain...
tx committed.
hello.bc deployment.toml file /Library/Application Support/offckb-nodejs/devnet/contracts/hello.bc/deployment.toml generated successfully.
hello.bc migration json file /Library/Application Support/offckb-nodejs/devnet/contracts/hello.bc/migrations/2024-11-18-195031.json generated successfully.
done.
Open another terminal and start a offckb Nodejs Repl to build the transaction:
offckb repl -r
Welcome to OffCKB REPL!
[[ Default Network: devnet, enableProxyRPC: true, CCC SDK: 0.0.16-alpha.3 ]]
Type 'help()' to learn how to use.
OffCKB >
Build the transaction to test our hello Script.
First, we will construct an output Cell that carries a special Type Script to execute the hello.js
codes.
The code_hash
and hash_type
in the Type Script reference the ckb-js-vm
Script Cell. The information can directly access from the REPL with myScripts
variable:
OffCKB > let baseScriptArgs = `0x0000${myScripts['hello.bc'].codeHash.slice(2)}0${ccc.hashTypeToBytes(myScripts['hello.bc'].hashType).toString()}`;
OffCKB > let baseScript = ccc.Script.from({
... codeHash: myScripts["ckb-js-vm"].codeHash,
... hashType: myScripts["ckb-js-vm"].hashType,
... args: baseScriptArgs,
... })
The key here is the args of the Type Script. We locate the Cell that carries our hello.js
codes and
insert the reference information—which includes code_hash
and hash_type
–of that Cell into the args,
following the args structure of ckb-js-vm
.
OffCKB > let tx = ccc.Transaction.from({
... outputs: [
... {
... lock: accounts[0].lockScript,
... type: baseScript
... },
... ],
... cellDeps: [
... ...myScripts["ckb-js-vm"].cellDeps.map(c => c.cellDep),
... ...myScripts['hello.bc'].cellDeps.map(c => c.cellDep)
... ],
... });
Also, don't forget to add all the Live Cells containing the related Scripts in the cellDeps
in the transaction.
Finally, we sign and send the transaction:
OffCKB > let signer = new ccc.SignerCkbPrivateKey(client, accounts[0].privkey);
OffCKB > await tx.completeInputsByCapacity(signer);
2
OffCKB > await tx.completeFeeBy(signer, 1000);
[ 0, true ]
OffCKB > await signer.sendTransaction(tx);
'0x1464fec71bd95c4caa2a2640f1573850b25ba9c696c1bfdbe0dc7459717c734b'
OffCKB >
Debug the transaction to See If It works expected
- Command
- Response
offckb debug --tx-hash 0x1464fec71bd95c4caa2a2640f1573850b25ba9c696c1bfdbe0dc7459717c734b
Dump transaction successfully
******************************
****** Input[0].Lock ******
Run result: 0
All cycles: 1640617(1.6M)
******************************
****** Input[1].Lock ******
Run result: 0
All cycles: 1640617(1.6M)
******************************
****** Output[0].Type ******
Script log: hello, ckb-js-script!
Run result: 0
All cycles: 2993892(2.9M)
✨ Done in 1.50s.
The logs show hello, ckb-js-script!
, indicating our JavaScript code executed successfully.
Write a fib.js
Script
We can try a different JavaScript example. Let's write a fib.js
in the js
folder:
console.log("testing fib");
function fib(n) {
if (n <= 0) return 0;
else if (n == 1) return 1;
else return fib(n - 1) + fib(n - 2);
}
var value = fib(10);
console.assert(value == 55, "fib(10) = 55");
Compile the fib.js
into Binary with CKB-Debugger
ckb-debugger --read-file js/fib.js --bin deps/ckb-js-vm -- -c | awk '/Run result: 0/{exit} {print}' | xxd -r -p > js/build/fib.bc
Build a New Transaction to Test fib.js
Script
We repeat the same process as above:
Deploy the fib Script:
- Command
- Response
offckb deploy --target ./js/build/fib.bc --type-id
wait for tx confirmed on-chain...
tx committed.
fib.bc deployment.toml file /Library/Application Support/offckb-nodejs/devnet/contracts/fib.bc/deployment.toml generated successfully.
fib.bc migration json file /Library/Application Support/offckb-nodejs/devnet/contracts/fib.bc/migrations/2024-11-18-195031.json generated successfully.
done.
Build and send the transaction:
OffCKB > let baseScriptArgs = `0x0000${myScripts['fib.bc'].codeHash.slice(2)}0${ccc.hashTypeToBytes(myScripts['fib.bc'].hashType).toString()}`;
OffCKB > let baseScript = ccc.Script.from({
... codeHash: myScripts["ckb-js-vm"].codeHash,
... hashType: myScripts["ckb-js-vm"].hashType,
... args: baseScriptArgs,
... })
OffCKB > let tx = ccc.Transaction.from({
... outputs: [
... {
... lock: accounts[0].lockScript,
... type: baseScript
... },
... ],
... cellDeps: [
... ...myScripts["ckb-js-vm"].cellDeps.map(c => c.cellDep),
... ...myScripts['fib.bc'].cellDeps.map(c => c.cellDep)
... ],
... });
OffCKB > let signer = new ccc.SignerCkbPrivateKey(client, accounts[0].privkey);
OffCKB > await tx.completeInputsByCapacity(signer);
2
OffCKB > await tx.completeFeeBy(signer, 1000);
[ 0, true ]
OffCKB > await signer.sendTransaction(tx);
'0x3f8d8f0e4ef3be052ecb2784d058a5e88d700831783524f7295b23df047897d0'
OffCKB >
Debug the transaction to See If It works expected
- Command
- Response
offckb debug --tx-hash 0x1464fec71bd95c4caa2a2640f1573850b25ba9c696c1bfdbe0dc7459717c734b
Dump transaction successfully
******************************
****** Input[0].Lock ******
Run result: 0
All cycles: 1652553(1.6M)
******************************
****** Output[0].Type ******
Script log: testing fib
Run result: 0
All cycles: 3190302(3.0M)
✨ Done in 1.71s.
Congratulations!
By following this tutorial so far, you have mastered how to write Scripts that integrates
ckb-js-vm
to execute JavaScript codes on CKB. Here's a quick recap:
- Use
offckb
REPL to quickly prototype with JavaScript code and CKB bundles - Use Script
code_hash
andhash_type
to build the base layer layer ofckb-js-vm
. - Build args for the Script to carry the reference info of the JavaScript code Cell
Additional Resources
- Full source code of this tutorial: ckb-js-script
- More about
ckb-js-vm
: ckb-js-vm docs - CKB transaction structure: RFC-0022-transaction-structure
- CKB data structure basics: RFC-0019-data-structure