Introduction

CKB-VM is a virtual machine based on the RISC-V instruction set that executes on-chain script on CKB, providing developers with maximum flexibility and power while maintaining security and performance. This approach allows for seamless integration of new cryptographic primitives without hard forks and supports development in any programming language that can target RISC-V, enabling developers to use familiar tools and existing libraries rather than building everything from scratch.

Currently, the main languages used for on-chain script development are C and Rust. While C is accessible to learn, it lacks high-level abstractions, making it challenging to write code that is free from undefined behaviors and security vulnerabilities. Rust offers stronger safety guarantees and modern language features, but its steep learning curve limits widespread adoption among developers.

With the project ckb-js-vm, we aim to enable on-chain script development using TypeScript, one of the world's most popular programming languages. This approach significantly lowers the barrier to entry for blockchain developers, allowing them to leverage the vast JavaScript/TypeScript ecosystem, including its rich libraries, tools, and community support. By providing a unified language for both development and testing, ckb-js-vm creates a streamlined, one-stop solution that eliminates context-switching between languages and accelerates the development cycle for CKB on-chain script programming.

Early Attempt

In the early years, we demonstrated the possibility of using JavaScript on CKB-VM through Duktape, a compact JavaScript engine with readable code and comprehensive documentation. However, this approach suffered from significant performance limitations, with on-chain scripts consuming between 100-1000 M cycles, making it impractical for production environments. These early experiments, while promising conceptually, highlighted the need for a more efficient JavaScript execution solution.

QuickJS, A Fast JavaScript Engine

QuickJS is a lightweight JavaScript engine developed by legendary programmer Fabrice Bellard. It provides impressive performance without relying on JIT compilation, making it ideal for CKB-VM which implements W^X security features that prevent JIT-based optimizations. With QuickJS, we've achieved a significant performance breakthrough, reducing execution costs to under 50M cycles for typical JavaScript operations—a dramatic improvement over previous approaches.

CKB imposes a 500KB size limitation for binaries deployed to a single cell, making QuickJS's focus on code size efficiency particularly valuable for on-chain scripts. QuickJS places a strong emphasis on minimizing code size. For instance, the manual states:

  • The complete regular expression library requires only ~15 KiB (x86 code), excluding Unicode support
  • The full Unicode library adds approximately 45 KiB (x86 code)

Total Solution

With a JavaScript engine, it is still very difficult to develop a full featured on-chain script. We also provide following tools and libraries:

  • libraries in TypeScript
  • building tools
  • framework for unit tests We will introduce these features later.

Comprehensive Development Ecosystem

While integrating a JavaScript engine is a significant advancement, developing production-ready on-chain scripts requires a complete ecosystem of tools and libraries. The ckb-js-vm project delivers a comprehensive solution that includes:

  • A rich set of TypeScript libraries providing CKB-specific abstractions
  • Streamlined build tooling that optimizes code size and performance
  • A robust testing framework enabling comprehensive unit testing
  • Scaffold tools that generate project templates and boilerplate code for rapid development

This end-to-end approach ensures developers can focus on writing business logic rather than wrestling with infrastructure concerns. The subsequent sections of this documentation will explore each component of this ecosystem in detail, demonstrating how they work together to create a seamless development experience.

Get Started

Before we introduce the core concepts and detailed implementation, let's familiarize ourselves with the basic workflow of developing on-chain scripts with ckb-js-vm.

We can use the scaffolding tool create-ckb-js-vm-app to quickly set up a new project. Before we start, make sure the following tools are installed:

pnpm create ckb-js-vm-app

When prompted, choose the default project name: my-ckb-script. After a few moments, your project will be created.

Project Structure

Let's explore the project structure to understand its components:

  • packages/on-chain-script - Contains the TypeScript code that will be compiled and deployed on the CKB blockchain
  • packages/on-chain-script-tests - Contains the off-chain TypeScript code for testing your on-chain script

Building and Testing

To build your project, run:

pnpm build

This command compiles your TypeScript code and prepares the on-chain script.

To run the test suite:

pnpm test

The tests will verify that your on-chain script behaves as expected in various scenarios.

Key Output Files

After building your project, two important files are generated:

  • packages/on-chain-script/dist/index.js - The bundled JavaScript code compiled from your TypeScript source
  • packages/on-chain-script/dist/index.bc - The bytecode representation of your script, which is significantly smaller and more efficient. This is the file that will be deployed on-chain.

The build process is defined in packages/on-chain-script/package.json, which configures esbuild and ckb-debugger to transform your TypeScript code into deployable bytecode. We'll explore these tools in more detail in later chapters.

Core Concepts Explained

In the last chapter, we learned how to use the tool to create a blank project. In this chapter, we'll explain how it works.

Let's first examine the pnpm build command in the package.json file:

tsc --noEmit && esbuild --platform=neutral --minify --bundle --external:@ckb-js-std/bindings --target=es2022 src/index.ts --outfile=dist/index.js && ckb-debugger --read-file dist/index.js --bin ../../build/ckb-js-vm -- -c dist/index.bc

This can be split into 3 distinct commands:

tsc --noEmit
esbuild --platform=neutral --minify --bundle --external:@ckb-js-std/bindings --target=es2022 src/index.ts --outfile=dist/index.js
ckb-debugger --read-file dist/index.js --bin ../../build/ckb-js-vm -- -c dist/index.bc

Build Process Breakdown

1. TypeScript Type Checking

The first command performs type checking on TypeScript code. This helps catch syntax errors early in the development process. At this stage, no code is output.

2. JavaScript Bundling

The second command uses esbuild to bundle the code:

  • --minify minimizes the generated code size, which is critical since larger storage costs more money on ckb-vm
  • --external:@ckb-js-std/bindings tells esbuild to skip this dependency, as it's just a binding from JavaScript to C with no JavaScript implementation
  • --target=es2022 sets the target to ES2022, which QuickJS supports
  • The final output is dist/index.js, which contains all the code needed to run

While we could run this JavaScript file directly with ckb-js-vm, the performance wouldn't be optimal. The next step improves performance and further minimizes code size.

It's perfectly fine to switch to other bundling tools if you prefer. The only constraint is that the output .js file must be able to run without external dependencies.

3. Bytecode Compilation

The third command converts JavaScript code into QuickJS bytecode:

  • ckb-debugger is a ckb-vm runner and debugger that can read and write local files (note that on a real CKB node, ckb-vm cannot do this)
  • We've implemented a special feature in the ckb-js-vm binary to compile JavaScript code into QuickJS bytecode
  • This approach ensures the generated code is always compatible with the on-chain script
  • The final output is dist/index.bc, which is the binary that will be deployed and used

Testing

The pnpm test command runs Jest for unit testing. During this phase:

  • The binary ckb-js-vm and dist/index.bc are used
  • The .ts and .js files are not involved
  • It uses the ckb-testtool package, which we'll explain in a later chapter

Working with ckb-js-vm

The ckb-js-vm is the binary name of an on-chain script that integrates QuickJS with additional glue code. It functions similarly to the node binary as a JavaScript engine and runtime. However, compared to node, it has more limited capabilities and is designed to run specifically in the CKB-VM environment. During development, you can run it on your local machine using ckb-debugger.

How to Build

To build ckb-js-vm, run:

git submodule update --init
make all

clang-18 is required for compilation. After building, the binary will be available at build/ckb-js-vm.

If you need a reproducible build (ensuring the same binary is generated regardless of build environment), you can use:

bash reproducible_build.sh

ckb-js-vm Command Line Options

When an on-chain script is invoked by exec or spawn syscalls, it can accept command line arguments. The ckb-js-vm supports the following options to control its execution behavior:

  • -c <filename>: Compile JavaScript source code to bytecode, making it more efficient for on-chain execution
  • -e <code>: Execute JavaScript code directly from the command line string
  • -r <filename>: Read and execute JavaScript code from the specified file
  • -t <target>: Specify the target resource cell's code_hash and hash_type in hexadecimal format
  • -f: Enable file system mode, which provides support for JavaScript modules and imports

Note, the -c and -r options can only work with ckb-debugger. The -c option is particularly useful for preparing optimized bytecode as described in the previous chapter. When no options are specified, ckb-js-vm runs in its default mode. These command line options provide valuable debugging capabilities during development.

Compiling JavaScript into Bytecode

The ckb-js-vm includes built-in functionality for compiling JavaScript code into bytecode, which improves execution efficiency on-chain. You can use this feature as follows:

ckb-debugger --read-file hello.js --bin build/ckb-js-vm -- -c hello.bc

This command:

  1. Uses --read-file hello.js to provide the JavaScript source file to ckb-debugger
  2. Specifies the ckb-js-vm binary with --bin build/ckb-js-vm
  3. Passes the -c hello.bc option to ckb-js-vm (everything after --)

The process compiles hello.js and outputs the bytecode to hello.bc. The --read-file option is specific to ckb-debugger and allows it to read a file as a data source. Command line arguments after the -- separator are passed directly to the on-chain script, enabling the use of the -c compilation flag.

Note that this compilation functionality requires the ckb-debugger environment and cannot work independently.

QuickJS bytecode is version-specific and not portable between different QuickJS versions. This compilation approach ensures that generated bytecode is always compatible with the exact QuickJS version used in ckb-js-vm.

ckb-js-vm args Explanation

The ckb-js-vm script structure in molecule is below:

code_hash: <code hash of ckb-js-vm, 32 bytes>
hash_type: <hash type of ckb-js-vm, 1 byte>
args: <ckb-js-vm flags, 2 bytes> <code hash of resource cell, 32 bytes> <hash type of resource cell, 1 byte>

The first 2 bytes are parsed into an int16_t in C using little-endian format (referred to as ckb-js-vm flags). If the lowest bit of these flags is set (v & 0x01 == 1), the file system is enabled. File system functionality will be described in another chapter.

The subsequent code_hash and hash_type point to a resource cell which may contain:

  1. A file system
  2. JavaScript source code
  3. QuickJS bytecode

When the file system flag is enabled, the resource cell contains a file system that can also include JavaScript code. For most scenarios, QuickJS bytecode is stored in the resource cell. When an on-chain script requires extra args, they can be stored beginning at offset 35 (2 + 32 + 1). Compared to normal on-chain scripts in other languages, ckb-js-vm requires these extra 35 bytes.

QuickJS Integration

ckb-js-vm is built on QuickJS, a small and embeddable JavaScript engine developed by Fabrice Bellard. QuickJS features:

  • Fast and lightweight JavaScript interpreter
  • Support for ES2022 features
  • Small footprint suitable for embedded systems
  • Efficient memory management

ckb-js-vm leverages QuickJS to provide a JavaScript runtime environment within the CKB. This integration enables:

  • Running JavaScript code directly on CKB-VM
  • Compiling JavaScript to bytecode for more efficient execution
  • Calling syscalls

Bindings

ckb-js-vm provides bindings that allow JavaScript code to interact with the CKB blockchain through the @ckb-js-std/bindings module. These bindings expose CKB syscalls and other functionality to JavaScript:

  • Syscalls defined in the RFC
  • Hashing functions: SHA2-256, Keccak256, Blake2b, RIPEMD-160
  • Cryptographic algorithms: secp256k1, Schnorr
  • Miscellaneous functions: hex, base64, and SMT (Sparse Merkle Tree)

JavaScript Module System

ckb-js-vm exclusively supports ECMAScript Modules (ESM) and does not support CommonJS. This means you must use the modern ES import syntax for all module operations.

Supported Import Syntax

Use the ES import syntax to import modules:

// Importing the entire module
import * as bindings from "@ckb-js-std/bindings";

// Named imports
import { hex } from "@ckb-js-std/bindings";

// Default import (if the module has a default export)
import defaultExport from "module-name";

Unsupported CommonJS Syntax

The following CommonJS patterns are not supported and will result in errors:

// ❌ This will not work in ckb-js-vm
const bindings = require("@ckb-js-std/bindings");

// ❌ This will also not work
module.exports = { /* ... */ };

Module Resolution Rules

When importing modules in ckb-js-vm:

  1. Built-in modules like @ckb-js-std/bindings are resolved automatically
  2. Relative imports (starting with ./ or ../) are resolved relative to the current file
  3. Bare imports (like import x from "module-name") require the file system mode to be enabled

When using file system mode, make sure your module structure follows ESM conventions with .jsor .bc file extensions explicitly included in import statements.

ckb-js-std Library Reference

Overview

The ckb-js-std ecosystem consists of two TypeScript libraries designed to work with ckb-js-vm:

  1. @ckb-js-std/bindings
  2. @ckb-js-std/core

@ckb-js-std/bindings

This library provides low-level bindings to the C implementation of ckb-js-vm. It serves as the foundation layer that enables JavaScript/TypeScript to interact with the underlying C code. Key characteristics:

  • Contains declarations for binding functions to C implementations
  • Has no TypeScript implementation of its own
  • Primarily used as a dependency for higher-level libraries

Errors thrown by bindings functions

It is possible that bindings functions throw exceptions that can be handled gracefully. The CKB VM defines standard error codes that your code should be prepared to handle:

CKB_INDEX_OUT_OF_BOUND 1
CKB_ITEM_MISSING 2

Common scenarios where these errors occur:

  • CKB_INDEX_OUT_OF_BOUND (1): Occurs when iterating beyond available items, such as when looping over cells, scripts, witnesses, etc. This error is expected and should be caught to terminate iteration loops.
  • CKB_ITEM_MISSING (2): Occurs when a type script is missing. This can be a valid state in some on-chain scrips.

You can handle these exceptions by checking the errorCode property of the thrown exception. Here's an example of properly handling the out-of-bounds case in an iterator:

next(): IteratorResult<T> {
  try {
    const item = this.queryFn(this.index, this.source);
    this.index += 1;
    return { value: item, done: false };
  } catch (err: any) {
    if (err.errorCode === bindings.INDEX_OUT_OF_BOUND) {
      // End iteration gracefully when we've reached the end of available items
      return { value: undefined, done: true };
    }
    // Re-throw any other errors with additional context
    throw new Error(`QueryIter error: ${err.message || err}`);
  }
}

@ckb-js-std/core

Built on top of @ckb-js-std/bindings, this library offers a more developer-friendly interface with:

  • Enhanced TypeScript types for better code completion and error checking
  • Higher-level utility functions that simplify common operations
  • Abstractions that make working with ckb-js-vm more intuitive
  • Recommended for most application development scenarios

The @ckb-js-std/core library contains several important sub-modules that provide specialized functionality:

  • HighLevel: A convenient wrapper around the "bindings" module that simplifies common operations with an easy-to-use API.
  • hasher: Provides cryptographic hashing functions essential for blockchain operations, including SHA256 and Blake2b implementations.
  • log: Contains logging utilities for debugging and monitoring your on-chain script during development and production.
  • molecule: Implements molecule serialization and deserialization, the standard data encoding format used in the CKB ecosystem.
  • num: Offers utilities for serializing and deserializing numeric values, handling the conversion between JavaScript numbers and their binary representations.

We recommend exploring these sub-modules before starting your project to understand the full capabilities available to you.

Usage Recommendations

For most projects, we recommend using @ckb-js-std/core as it provides a more ergonomic developer experience while maintaining access to the full capabilities of ckb-js-vm.

Only use @ckb-js-std/bindings directly when you need precise control over low-level operations or are developing custom extensions to the ecosystem.

CommonJS Modules (require)

For some scenarios, you might need to write code in JavaScript and use the CommonJS require syntax to load modules. This can be done as follows for @ckb-js-std/bindings(already embedded in ckb-js-vm):

const bindings = require("@ckb-js-std/bindings");

However, we generally recommend using ES modules (import/export) instead of CommonJS for the following reasons:

  • Better compatibility with modern JavaScript tooling
  • Enables tree-shaking in bundling tools like esbuild
  • Provides clearer static analysis for IDEs and type checking

For other library, you can do it as follows:

import * as core from '@ckb-js-std/core';
globalThis.__ckb_core = core;
require = function (name) {
if (name === '@ckb-js-std/core') {
  return globalThis.__ckb_module_core; }\
    throw new Error('cannot find the module: ' + name);
}

The globalThis global property contains the global this value, which is usually akin to the global object.

Writing Effective Unit Tests

The mission of the ckb-js-vm project is to enable developers to write on-chain scripts using a single language: TypeScript. In the previous chapter, we learned how to write on-chain scripts in TypeScript. This chapter will demonstrate how you can also write unit tests in TypeScript, allowing you to use just one language for your entire development workflow.

ckb-testtool

While Rust developers have been using ckb-testtool for testing, we now have a TypeScript version of ckb-testtool available. This tool leverages two important components:

  1. ccc - A transaction assembler written in TypeScript
  2. ckb-debugger - A debugger and execution environment

The workflow is straightforward:

  • Use ccc to assemble transactions in TypeScript, outputting them in JSON format
  • Use ckb-debugger to execute and validate these transactions
  • Write assertions to verify the expected behavior

This combination provides a complete unit testing framework for CKB scripts written in TypeScript.

Examples

describe("example", () => {
  test("alwaysSuccess", () => {
    const resource = Resource.default();
    const tx = Transaction.default();

    // deploy a cell with risc-v binary, return a script.
    const lockScript = resource.deployCell(
      hexFrom(readFileSync(DEFAULT_SCRIPT_ALWAYS_SUCCESS)),
      tx,
      false,
    );
    // update args
    lockScript.args = "0xEEFF";

    // mock a input cell with the created script as lock script
    const inputCell = resource.mockCell(lockScript);

    // add input cell to the transaction
    tx.inputs.push(Resource.createCellInput(inputCell));
    // add output cell to the transaction
    tx.outputs.push(Resource.createCellOutput(lockScript));
    // add output data to the transaction
    tx.outputsData.push(hexFrom("0x"));

    // verify the transaction
    const verifier = Verifier.from(resource, tx);
    verifier.verifySuccess();
  });

  test("alwaysFailure", () => {
    const resource = Resource.default();
    const tx = Transaction.default();

    const lockScript = resource.deployCell(
      hexFrom(readFileSync(DEFAULT_SCRIPT_ALWAYS_FAILURE)),
      tx,
      false,
    );
    const inputCell = resource.mockCell(lockScript);
    tx.inputs.push(Resource.createCellInput(inputCell));

    const verifier = Verifier.from(resource, tx);
    verifier.verifyFailure();
    verifier.verifyFailure(-1);
  });
});

In the example above, we're testing on-chain script with two test cases:

  1. A test case which succeeds
  2. A test case which fails

This pattern allows you to verify both the positive and negative cases for your script's validation logic, ensuring robust behavior in all scenarios.

Pre-compiled Test Binaries

To simplify testing, the ckb-js-vm project provides several pre-compiled binaries that you can use in your test cases:

  1. Always Success Script - A script that always returns success (exit code 0)

    • Access via DEFAULT_SCRIPT_ALWAYS_SUCCESS
  2. Always Failure Script - A script that always returns failure (exit code -1)

    • Access via DEFAULT_SCRIPT_ALWAYS_FAILURE
  3. ckb-js-vm Script - The main ckb-js-vm runtime for testing TypeScript scripts

    • Access via DEFAULT_SCRIPT_CKB_JS_VM

These binaries can be imported directly in your tests without needing to compile them yourself, making it easier to create test fixtures and validation scenarios.

Example usage:

// Import the binary
const alwaysSuccessScript = hexFrom(readFileSync(DEFAULT_SCRIPT_ALWAYS_SUCCESS));

// Deploy it in your test
const lockScript = resource.deployCell(alwaysSuccessScript, tx, false);

⚠️ SECURITY WARNING: These pre-compiled binaries are intended for testing purposes only. Never deploy them in a production environment. For production use, always compile your scripts from source code to ensure security and integrity.

Simple File System and Modules

In addition to executing individual JavaScript files, ckb-js-vm also supports JavaScript modules through its Simple File System. Files within this file system are made available for JavaScript to read, import, and execute, enabling module imports like import { * } from "./module.js". Each Simple File System must contain at least one entry file named index.bc (or index.js), which ckb-js-vm loads from any cell and executes.

A file system is represented as a binary file with a specific format described in this document. You can use the ckb-fs-packer tool to create a file system from your source files or to unpack an existing file system.

How to create a Simple File System

Consider the following two files:

// File index.js
import { fib } from "./fib_module.js";
console.log("fib(10)=", fib(10));
// File fib_module.js
export function fib(n) {
    if (n <= 0)
        return 0;
    else if (n == 1)
        return 1;
    else
        return fib(n - 1) + fib(n - 2);
}

If we want ckb-js-vm to execute this code smoothly, we must package them into a file system first. To pack them within the current directory into fib.fs, you may run

npx ckb-fs-packer pack fib.fs index.js fib_module.js

Note that all file paths provided to fs-packer must be in relative path format. The absolute path of a file in your local filesystem is usually meaningless within the Simple File System.

You can also rename files when adding them to the filesystem by using the source:destination syntax:

npx ckb-fs-packer pack archive.fs 1.js:lib/1.js 2.js:lib/2.js

In this example, the local files 1.js and 2.js will be stored in the Simple File System as lib/1.js and lib/2.js respectively.

How to deploy and use Simple File System

While it's often more resource-efficient to write all JavaScript code in a single file, you can enable file system support in ckb-js-vm through either:

  • Executing or spawning ckb-js-vm with the "-f" parameter
  • Using ckb-js-vm flags with file system enabled (see the Working with ckb-js-vm chapter for details)

Unpacking a Simple File System

To extract files from an existing file system, run:

npx ckb-fs-packer unpack fib.fs .

Simple File System On-disk Representation

The on-disk representation of a Simple File System consists of three parts:

  1. A file count: A number representing the total files contained in the file system
  2. Metadata array: Stores information about each file's name and content
  3. Payload array: Binary objects (blobs) containing the actual file contents

Each metadata entry contains offset and length information for both a file's name and content. For each file, the metadata stores four uint32_t values:

  • The offset of the file name in the payload array
  • The length of the file name
  • The offset of the file content in the payload array
  • The length of the file content

We can represent these structures using C-like syntax:

struct Blob {
    uint32_t offset;
    uint32_t length;
}

struct Metadata {
    struct Blob file_name;
    struct Blob file_content;
}

struct SimpleFileSystem {
    uint32_t file_count;
    struct Metadata metadata[..];
    uint8_t payload[..];
}

When serializing the file system into a file, all integers are encoded as a 32-bit little-endian number. The file names are stored as null terminated strings.

QuickJS Null Termination Workaround

Due to an issue in QuickJS, JavaScript source code strings must be null-terminated. To address this requirement, ckb-js-vm automatically adds a null byte (\0) to every file without including it in the reported length value.

For example, consider this simple JavaScript code:

console.log("hi")

While the content length is 17 characters, when cast to a C-style string (const char*), an additional \0 character is appended after the final ) character. This ensures QuickJS can properly process the source code.

Using init.bc/init.js Files

The ckb-js-vm supports special initialization files named init.bc or init.js that are loaded and executed before index.bc or index.js. This feature helps solve issues related to JavaScript module hoisting.

Consider this example code:

import * as bindings from "@ckb-js-std/bindings";
bindings.mount(2, bindings.SOURCE_CELL_DEP, "/")
import * as module from './fib_module.js';

Due to JavaScript's hoisting behavior, import statements are processed before other code executes. The code effectively becomes:

import * as bindings from "@ckb-js-std/bindings";
import * as module from './fib_module.js';

bindings.mount(2, bindings.SOURCE_CELL_DEP, "/")

This will fail because the import attempts to access ./fib_module.js before the file system is mounted. To solve this problem, place the bindings.mount statement in an init.bc or init.js file, which will execute before any imports are processed in the main file.

Security Best Practices

In this chapter, we will introduce some background and useful security tips for ckb-js-vm.

Stack and Heap Memory

For normal native C programs, there is no method to control the stack size. However, QuickJS provides this capability through its JS_SetMaxStackSize function. This is a critical feature to prevent stack/heap collisions.

Before explaining our memory organization design, let's understand the memory layout of ckb-vm, which follows these rules:

  • Total memory is 4M
  • From address 0 to the address specified by symbol _end, there are ELF sections (.data, .rss, .text, etc.)
  • The stack begins at 4M and grows backward toward lower addresses

In ckb-js-vm, we carefully organize memory regions as follows:

  • From address 0 to _end: ELF sections
  • From address _end to 3M: Heap memory for malloc
  • From address 3M+4K to 4M: Stack

The 4K serves as a margin area. This organization prevents stack/heap collisions when the stack grows too large.

Exit Code

When bytecode or JavaScript code throws an uncaught exception, ckb-js-vm will exit with error code (-1). You can write JavaScript code without explicitly checking for exceptions—simply let them throw naturally.

QuickJS treats every file as a module. Since it implements Top level await, the evaluation result of a module is a promise. This means the code below doesn't return -100 as expected:

function main() {
  // ...
  return -100;
}
main();

Instead, it unexpectedly returns zero. To ensure your exit code is properly returned, use this pattern instead:

function main() {
  // ...
  return -100;
}
bindings.exit(main());

Another tip: always write test cases for failure scenarios. Make sure the error codes returned match what you expect in these situations.

Dynamic Loading

JavaScript provides the ability to load modules dynamically at runtime through the evalJsScript function in the @ckb-js-std/bindings package. This powerful feature enables extension mechanisms, plugin architectures, and code splitting in ckb-js-vm. However, it comes with significant security implications. When modules are loaded from untrusted sources (such as other cells on-chain), they may contain malicious code. A simple exit(0) statement could cause your entire script to exit with a success status, bypassing your validation logic. Bytecode is particularly problematic as it's extremely difficult to inspect and verify.

If you must use dynamic loading, follow these precautions: only load from trusted sources you control, implement permission restrictions for loaded code, validate module integrity with cryptographic signatures when possible, and consider a pattern like this for safer loading:

// Example of safer dynamic loading with basic validation
function loadModule(moduleSource, allowedAPIs) {
  const wrappedSource = `
    (function(restrictedBindings) {
      ${moduleSource}
    })({ ...allowedAPIs });
  `;
  return bindings.evalJsScript(wrappedSource);
}

Remember that even with these safeguards, dynamic loading should be used cautiously in security-critical applications, and avoided entirely when working with untrusted inputs.

Performance and Bytecode Size Benchmarks

The performance and bytecode size are critical factors for on-chain scripts in the CKB ecosystem. Understanding these constraints helps developers build efficient and deployable solutions:

  • Execution Cycles: Each transaction has a limited cycle budget (execution time). If a script exceeds this limit, the transaction cannot be processed by the network.

  • Bytecode Size: On-chain storage is expensive and limited. Each cell has a size limit of approximately 500KB, making code optimization essential.

Below are benchmark examples of on-chain scripts with their respective sizes and cycle consumption:

ScriptSizeCycles
secp256k1/blake2b26KB14M cycles
simple_udt26KB12M cycles
silent berry AccountBook70KB20M-40M cycles

These benchmarks can help you gauge the resource requirements when developing your own ckb-js-vm scripts.

Bytecode Size Impacts Performance

Unlike languages such as C and Rust where binary size has minimal impact on runtime performance, in ckb-js-vm the bytecode size directly affects execution efficiency. Our testing reveals that larger bytecode significantly increases ckb-js-vm boot time, potentially consuming 20-30M cycles.

This insight provides a valuable optimization strategy: reducing your overall bundle size delivers dual benefits of smaller bytecode footprint and improved performance. When developing on-chain scripts for ckb-js-vm, code size optimization should be considered a performance optimization as well.

Reproducible Build and Deployment

When deploying an on-chain script, it's essential to build it from scratch. A key requirement during this process is ensuring that different builds of the same source code produce identical binaries - this is known as a "reproducible build."

You can achieve this with ckb-js-vm using the following command:

bash reproducible_build.sh

Deployment

The script has been deployed on the testnet with these parameters:

ParameterValue
code_hash0x3e9b6bead927bef62fcb56f0c79f4fbd1b739f32dd222beac10d346f2918bed7
hash_typetype
tx_hash0x9f6558e91efa7580bfe97830d11cd94ca5d614bbf4a10b36f3a5b9d092749353
index0x0
dep_typecode

The corresponding SHA256 checksum in checksums.txt is: 32d1db56b9d6f3188c1defe94fbfaa16159b46652a2c9e45e9eb167f0e083cd2