diff --git a/README.md b/README.md index a5f26bf..834cce6 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ make all ## Examples +* [Demo Script: Simple UDT](./tests/ckb_js_tests/test_data/simple_udt.js) * [Fibonacci Number](./tests/examples/fib.js) * [Calculate PI](./tests/examples/pi_bigint.js) diff --git a/docs/intro.md b/docs/intro.md index 87c6c1f..b5b574e 100644 --- a/docs/intro.md +++ b/docs/intro.md @@ -77,3 +77,15 @@ binary. Finally, it is written as `hello.bc`. `ckb-js-vm` can transparently run JavaScript bytecode or source files, which can also be in file systems. + +## Script +A ckb-js-vm script contains following data structure: + +``` +code_hash: +hash_type: +args: +``` + +The tailing bytes are JavaScript code arguments which can be used by JavaScript. +Note: 2 bytes ckb-js-vm args are reserved for further use. diff --git a/docs/syscalls.md b/docs/syscalls.md index 588611d..55a6639 100644 --- a/docs/syscalls.md +++ b/docs/syscalls.md @@ -77,7 +77,7 @@ and `index`. Example: ```js -ckb.mount(source, index) +ckb.mount(index, source) ``` Arguments: source (the source of the cell to load), index (the index of the cell @@ -404,18 +404,6 @@ Return value(s): memory size in bytes See also: [`ckb_current_memory` syscall](https://github.com/nervosnetwork/rfcs/pull/418/files) -#### ckb.mount -Description: Load the file system in the cell. - -Example: -```js -ckb.mount(2, ckb.SOURCE_CELL_DEP) -``` - -Arguments: index (the index of the cell), source (the source of the cell) - -Return value(s): none - ## Exported Constants Most constants here are directly taken from [ckb_consts.h](https://github.com/nervosnetwork/ckb-system-scripts/blob/master/c/ckb_consts.h): diff --git a/quickjs/ckb_module.c b/quickjs/ckb_module.c index 1feab1a..2507eb7 100644 --- a/quickjs/ckb_module.c +++ b/quickjs/ckb_module.c @@ -28,6 +28,26 @@ static JSValue syscall_exit(JSContext *ctx, JSValueConst this_val, int argc, JSV return JS_UNDEFINED; } +static JSValue ThrowError(JSContext *ctx, int32_t error_code, const char *message) { + JSValue obj, ret; + obj = JS_NewError(ctx); + if (unlikely(JS_IsException(obj))) { + /* out of memory: throw JS_NULL to avoid recursing */ + obj = JS_NULL; + } else { + JS_DefinePropertyValueStr(ctx, obj, "message", JS_NewString(ctx, message), + JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); + JS_DefinePropertyValueStr(ctx, obj, "error_code", JS_NewInt32(ctx, error_code), + JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); + } + // TODO + // if (add_backtrace) { + // build_backtrace(ctx, obj, NULL, 0, 0); + // } + ret = JS_Throw(ctx, obj); + return ret; +} + static void my_free(JSRuntime *rt, void *opaque, void *_ptr) { free(opaque); } struct LoadData; typedef int (*LoadFunc)(void *addr, uint64_t *len, struct LoadData *data); @@ -51,8 +71,8 @@ static JSValue parse_args(JSContext *ctx, LoadData *data, bool has_field, int ar if (JS_ToInt64(ctx, &index, argv[0])) { return JS_EXCEPTION; } - if (JS_ToInt64(ctx, &source, argv[1])) { - return JS_EXCEPTION; + if (JS_ToBigInt64(ctx, &source, argv[1])) { + if (JS_ToInt64(ctx, &source, argv[1])) return JS_EXCEPTION; } int var_arg_index = 2; if (has_field) { @@ -112,6 +132,7 @@ static JSValue syscall_load(JSContext *ctx, LoadData *data) { ret = JS_NewArrayBuffer(ctx, addr, real_len, my_free, addr, false); exit: if (err != 0) { + ThrowError(ctx, err, "ckb syscall error"); return JS_EXCEPTION; } else { return ret; @@ -470,6 +491,7 @@ static JSValue mount(JSContext *ctx, JSValueConst this_value, int argc, JSValueC uint8_t *addr = JS_GetArrayBuffer(ctx, &psize, buf); int err = ckb_load_fs(addr, psize); if (err != 0) { + ThrowError(ctx, SyscallErrorUnknown, "ckb_load_fs failed"); return JS_EXCEPTION; } else { return JS_UNDEFINED; @@ -515,10 +537,8 @@ int js_init_module_ckb(JSContext *ctx) { JS_SetPropertyStr(ctx, ckb, "set_content", JS_NewCFunction(ctx, syscall_set_content, "set_content", 1)); JS_SetPropertyStr(ctx, ckb, "get_memory_limit", JS_NewCFunction(ctx, syscall_get_memory_limit, "get_memory_limit", 0)); - JS_SetPropertyStr(ctx, ckb, "current_memory", - JS_NewCFunction(ctx, syscall_current_memory, "current_memory", 0)); - JS_SetPropertyStr(ctx, ckb, "mount", - JS_NewCFunction(ctx, mount, "mount", 2)); + JS_SetPropertyStr(ctx, ckb, "current_memory", JS_NewCFunction(ctx, syscall_current_memory, "current_memory", 0)); + JS_SetPropertyStr(ctx, ckb, "mount", JS_NewCFunction(ctx, mount, "mount", 2)); JS_SetPropertyStr(ctx, ckb, "SOURCE_INPUT", JS_NewInt64(ctx, CKB_SOURCE_INPUT)); JS_SetPropertyStr(ctx, ckb, "SOURCE_OUTPUT", JS_NewInt64(ctx, CKB_SOURCE_OUTPUT)); JS_SetPropertyStr(ctx, ckb, "SOURCE_CELL_DEP", JS_NewInt64(ctx, CKB_SOURCE_CELL_DEP)); diff --git a/quickjs/qjs.c b/quickjs/qjs.c index 2760ce1..a10517c 100644 --- a/quickjs/qjs.c +++ b/quickjs/qjs.c @@ -126,7 +126,7 @@ void js_std_dump_error(JSContext *ctx) { void js_std_loop(JSContext *ctx) { JSContext *ctx1; int err; - for(;;) { + for (;;) { err = JS_ExecutePendingJob(JS_GetRuntime(ctx), &ctx1); if (err <= 0) { if (err < 0) { diff --git a/tests/ckb_js_tests/Makefile b/tests/ckb_js_tests/Makefile index 09927c9..232f457 100644 --- a/tests/ckb_js_tests/Makefile +++ b/tests/ckb_js_tests/Makefile @@ -7,7 +7,9 @@ all: \ cargo_test \ file_system \ syscall \ - fs_bytecode + fs_bytecode\ + simple_udt\ + fs_mount cargo_test: cargo test @@ -40,6 +42,9 @@ fs_mount: cd test_data/fs_module_mount && lua ../../../../tools/fs.lua pack ../../../../build/fib_module.bin fib_module.js cargo run --bin module_mount | ${CKB_DEBUGGER} --tx-file=- -s lock +simple_udt: + cargo run --bin simple_udt | $(CKB_DEBUGGER) --tx-file=- --script-group-type type --cell-type output --cell-index 0 + install-lua: sudo apt install lua5.4 diff --git a/tests/ckb_js_tests/src/bin/simple_udt.rs b/tests/ckb_js_tests/src/bin/simple_udt.rs new file mode 100644 index 0000000..ca223c7 --- /dev/null +++ b/tests/ckb_js_tests/src/bin/simple_udt.rs @@ -0,0 +1,9 @@ +use ckb_js_tests::read_tx_template; + +pub fn main() -> Result<(), Box> { + let tx = read_tx_template("templates/simple_udt.json")?; + + let json = serde_json::to_string_pretty(&tx).unwrap(); + println!("{}", json); + Ok(()) +} diff --git a/tests/ckb_js_tests/templates/simple_udt.json b/tests/ckb_js_tests/templates/simple_udt.json new file mode 100644 index 0000000..dea800c --- /dev/null +++ b/tests/ckb_js_tests/templates/simple_udt.json @@ -0,0 +1,72 @@ +{ + "mock_info": { + "inputs": [ + { + "output": { + "capacity": "0x10000000", + "lock": { + "args": "0x", + "code_hash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "hash_type": "data1" + }, + "type": { + "args": "0x0000{{ ref_type js-code-file }}01", + "code_hash": "0x{{ ref_type ckb-js-vm }}", + "hash_type": "type" + } + }, + "data": "0x11000000000000000000000000000000" + } + ], + "cell_deps": [ + { + "output": { + "capacity": "0x10000000", + "lock": { + "args": "0x00AE9DF3447C404A645BC48BEA4B7643B95AC5C3AE", + "code_hash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "hash_type": "data1" + }, + "type": "{{ def_type ckb-js-vm }}" + }, + "data": "0x{{ data ../../../build/ckb-js-vm }}" + }, + { + "output": { + "capacity": "0x10000000", + "lock": { + "args": "0x00AE9DF3447C404A645BC48BEA4B7643B95AC5C3AE", + "code_hash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "hash_type": "data1" + }, + "type": "{{ def_type js-code-file }}" + }, + "data": "0x{{ data ../test_data/simple_udt.js }}" + } + ], + "header_deps": [] + }, + "tx": { + "outputs": [ + { + "capacity": "0x0", + "lock": { + "args": "0x", + "code_hash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "hash_type": "type" + }, + "type": { + "args": "0x0000{{ ref_type js-code-file }}01", + "code_hash": "0x{{ ref_type ckb-js-vm }}", + "hash_type": "type" + } + } + ], + "witnesses": [ + "0x0001020304050607" + ], + "outputs_data": [ + "0x11000000000000000000000000000000" + ] + } +} diff --git a/tests/ckb_js_tests/test_data/simple_udt.js b/tests/ckb_js_tests/test_data/simple_udt.js new file mode 100644 index 0000000..31592c6 --- /dev/null +++ b/tests/ckb_js_tests/test_data/simple_udt.js @@ -0,0 +1,114 @@ + +const CKB_INDEX_OUT_OF_BOUND = 1; +const ERROR_AMOUNT = -52; + +function assert(cond, obj1) { + if (!cond) { + throw Error(obj1); + } +} + +function compare_array(a, b) { + if (a.byteLength != b.byteLength) { + return false; + } + for (let i = 0; i < a.byteLength; i++) { + if (a[i] != b[i]) { + return false; + } + } + return true; +} + +function unpack_script(buf) { + let script = new Uint32Array(buf); + let raw_data = new Uint8Array(buf); + + let full_size = script[0]; + assert(full_size == buf.byteLength, 'full_size == buf.byteLength'); + let code_hash_offset = script[1]; + let code_hash = buf.slice(code_hash_offset, code_hash_offset + 32); + let hash_type_offset = script[2]; + let hash_type = raw_data[hash_type_offset]; + let args_offset = script[3]; + let args = buf.slice(args_offset + 4); + return {'code_hash': code_hash, 'hash_type': hash_type, 'args': args}; +} + +function* iterate_field(source, field) { + let index = 0; + while (true) { + try { + let ret = ckb.load_cell_by_field(index, source, field); + yield ret; + index++; + } catch (e) { + if (e.error_code == CKB_INDEX_OUT_OF_BOUND) { + break; + } else { + throw e; + } + } + } +} + +function* iterate_cell_data(source) { + let index = 0; + while (true) { + try { + let ret = ckb.load_cell_data(index, source); + yield ret; + index++; + } catch (e) { + if (e.error_code == CKB_INDEX_OUT_OF_BOUND) { + break; + } else { + throw e; + } + } + } +} + +function main() { + console.log('simple UDT ...'); + let buf = ckb.load_script(); + let script = unpack_script(buf); + let owner_mode = false; + // ckb-js-vm has leading 35 bytes args + let real_args = script.args.slice(35); + for (let lock_hash of iterate_field(ckb.SOURCE_INPUT, ckb.CKB_CELL_FIELD_LOCK_HASH)) { + if (compare_array(lock_hash, real_args)) { + owner_mode = true; + } + } + if (owner_mode) { + return 0; + } + let input_amount = 0n; + + for (let data of iterate_cell_data(ckb.SOURCE_GROUP_INPUT)) { + if (data.byteLength != 16) { + throw `Invalid data length: ${data.byteLength}`; + } + let n = new BigUint64Array(data); + let current_amount = n[0] | (n[1] << 64n); + input_amount += current_amount; + } + let output_amount = 0n; + for (let data of iterate_cell_data(ckb.SOURCE_GROUP_OUTPUT)) { + if (data.byteLength != 16) { + throw `Invalid data length: ${data.byteLength}`; + } + let n = new BigUint64Array(data); + let current_amount = n[0] | (n[1] << 64n); + output_amount += current_amount; + } + console.log(`verifying amount: ${input_amount} and ${output_amount}`); + if (input_amount < output_amount) { + return ERROR_AMOUNT; + } + console.log('Simple UDT quit successfully'); + return 0; +} + +ckb.exit(main()); diff --git a/tests/ckb_js_tests/test_data/syscall.js b/tests/ckb_js_tests/test_data/syscall.js index ee513b9..a805047 100644 --- a/tests/ckb_js_tests/test_data/syscall.js +++ b/tests/ckb_js_tests/test_data/syscall.js @@ -12,12 +12,15 @@ function expect_array(a, b) { function must_throw_exception(f) { let has_exception = false; + let error_code = 0; try { f(); } catch (e) { has_exception = true; + error_code = e.error_code; } console.assert(has_exception, 'Error, no exception found'); + return error_code; } function test_partial_loading(load_func) { @@ -35,10 +38,12 @@ function test_partial_loading(load_func) { data = load_func(0, ckb.SOURCE_OUTPUT, 7, 1); expect_array(data, ARRAY8.slice(1, 8)); - must_throw_exception(() => { - load_func(1001n, ckb.SOURCE_OUTPUT); + let error_code = must_throw_exception(() => { + load_func(1001, ckb.SOURCE_OUTPUT); }); - must_throw_exception(() => { + // CKB_INDEX_OUT_OF_BOUND + console.log(error_code === 1); + error_code = must_throw_exception(() => { load_func(0, ckb.SOURCE_OUTPUT + 1000n); }); console.log('test_partial_loading done');