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 blockchainpackages/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 sourcepackages/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
anddist/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:
- Uses
--read-file hello.js
to provide the JavaScript source file to ckb-debugger - Specifies the ckb-js-vm binary with
--bin build/ckb-js-vm
- 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:
- A file system
- JavaScript source code
- 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:
- Built-in modules like
@ckb-js-std/bindings
are resolved automatically - Relative imports (starting with
./
or../
) are resolved relative to the current file - 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 .js
or .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:
@ckb-js-std/bindings
@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:
- ccc - A transaction assembler written in TypeScript
- 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:
- A test case which succeeds
- 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:
-
Always Success Script - A script that always returns success (exit code 0)
- Access via
DEFAULT_SCRIPT_ALWAYS_SUCCESS
- Access via
-
Always Failure Script - A script that always returns failure (exit code -1)
- Access via
DEFAULT_SCRIPT_ALWAYS_FAILURE
- Access via
-
ckb-js-vm Script - The main ckb-js-vm runtime for testing TypeScript scripts
- Access via
DEFAULT_SCRIPT_CKB_JS_VM
- Access via
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:
- A file count: A number representing the total files contained in the file system
- Metadata array: Stores information about each file's name and content
- 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:
Script | Size | Cycles |
---|---|---|
secp256k1/blake2b | 26KB | 14M cycles |
simple_udt | 26KB | 12M cycles |
silent berry AccountBook | 70KB | 20M-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:
Parameter | Value |
---|---|
code_hash | 0x3e9b6bead927bef62fcb56f0c79f4fbd1b739f32dd222beac10d346f2918bed7 |
hash_type | type |
tx_hash | 0x9f6558e91efa7580bfe97830d11cd94ca5d614bbf4a10b36f3a5b9d092749353 |
index | 0x0 |
dep_type | code |
The corresponding SHA256 checksum in checksums.txt
is: 32d1db56b9d6f3188c1defe94fbfaa16159b46652a2c9e45e9eb167f0e083cd2