From f54b5fb7fc745d63a83e675a65e01665e1e4fd61 Mon Sep 17 00:00:00 2001 From: Lyndon Date: Thu, 9 Jan 2025 11:00:07 +0800 Subject: [PATCH 1/4] feat: smt bindings * hex encoding * base64 encoding --- Makefile | 1 + src/ckb_smt.h | 748 ++++++++++++++++++++++++++++++++++++++ src/misc_module.c | 324 +++++++++++++++++ src/misc_module.h | 12 + src/qjs.c | 3 + tests/module/Makefile | 3 +- tests/module/test_misc.js | 163 +++++++++ 7 files changed, 1253 insertions(+), 1 deletion(-) create mode 100644 src/ckb_smt.h create mode 100644 src/misc_module.c create mode 100644 src/misc_module.h create mode 100644 tests/module/test_misc.js diff --git a/Makefile b/Makefile index 43e0af9..f2c22d8 100644 --- a/Makefile +++ b/Makefile @@ -110,6 +110,7 @@ build/ckb-js-vm: build/ckb-c-stdlib/impl.o \ build/src/ckb_module.o \ build/src/secp256k1_module.o \ build/src/hash_module.o \ + build/src/misc_module.o \ build/src/qjs.o \ build/src/std_module.o \ deps/compiler-rt-builtins-riscv/build/libcompiler-rt.a diff --git a/src/ckb_smt.h b/src/ckb_smt.h new file mode 100644 index 0000000..5c93d73 --- /dev/null +++ b/src/ckb_smt.h @@ -0,0 +1,748 @@ +// +// C implementation of SMT verification: +// https://github.com/nervosnetwork/sparse-merkle-tree +// +// origin from: +// https://github.com/nervosnetwork/godwoken/blob/6c9b92b9b06068a8678864b35a3272545ed7909e/c/gw_smt.h#L1 +#ifndef _CKB_SPARSE_MERKLE_TREE_H_ +#define _CKB_SPARSE_MERKLE_TREE_H_ + +#ifdef __GNUC__ +#define RESTRICT __restrict__ +#else +#define RESTRICT +#endif + +// The faster version of memset & memcpy implementations used here are from +// the awesome musl libc project: https://www.musl-libc.org/ +void *_smt_fast_memset(void *dest, int c, size_t n) { + unsigned char *s = (unsigned char *)dest; + size_t k; + + /* Fill head and tail with minimal branching. Each + * conditional ensures that all the subsequently used + * offsets are well-defined and in the dest region. */ + + if (!n) return dest; + s[0] = c; + s[n - 1] = c; + if (n <= 2) return dest; + s[1] = c; + s[2] = c; + s[n - 2] = c; + s[n - 3] = c; + if (n <= 6) return dest; + s[3] = c; + s[n - 4] = c; + if (n <= 8) return dest; + + /* Advance pointer to align it at a 4-byte boundary, + * and truncate n to a multiple of 4. The previous code + * already took care of any head/tail that get cut off + * by the alignment. */ + + k = -(uintptr_t)s & 3; + s += k; + n -= k; + n &= -4; + +#ifdef __GNUC__ + typedef uint32_t __attribute__((__may_alias__)) u32; + typedef uint64_t __attribute__((__may_alias__)) u64; + + u32 c32 = ((u32)-1) / 255 * (unsigned char)c; + + /* In preparation to copy 32 bytes at a time, aligned on + * an 8-byte bounary, fill head/tail up to 28 bytes each. + * As in the initial byte-based head/tail fill, each + * conditional below ensures that the subsequent offsets + * are valid (e.g. !(n<=24) implies n>=28). */ + + *(u32 *)(s + 0) = c32; + *(u32 *)(s + n - 4) = c32; + if (n <= 8) return dest; + *(u32 *)(s + 4) = c32; + *(u32 *)(s + 8) = c32; + *(u32 *)(s + n - 12) = c32; + *(u32 *)(s + n - 8) = c32; + if (n <= 24) return dest; + *(u32 *)(s + 12) = c32; + *(u32 *)(s + 16) = c32; + *(u32 *)(s + 20) = c32; + *(u32 *)(s + 24) = c32; + *(u32 *)(s + n - 28) = c32; + *(u32 *)(s + n - 24) = c32; + *(u32 *)(s + n - 20) = c32; + *(u32 *)(s + n - 16) = c32; + + /* Align to a multiple of 8 so we can fill 64 bits at a time, + * and avoid writing the same bytes twice as much as is + * practical without introducing additional branching. */ + + k = 24 + ((uintptr_t)s & 4); + s += k; + n -= k; + + /* If this loop is reached, 28 tail bytes have already been + * filled, so any remainder when n drops below 32 can be + * safely ignored. */ + + u64 c64 = c32 | ((u64)c32 << 32); + for (; n >= 32; n -= 32, s += 32) { + *(u64 *)(s + 0) = c64; + *(u64 *)(s + 8) = c64; + *(u64 *)(s + 16) = c64; + *(u64 *)(s + 24) = c64; + } +#else + /* Pure C fallback with no aliasing violations. */ + for (; n; n--, s++) *s = c; +#endif + + return dest; +} + +void *_smt_fast_memcpy(void *RESTRICT dest, const void *RESTRICT src, size_t n) { + unsigned char *d = (unsigned char *)dest; + const unsigned char *s = (unsigned char *)src; + +#ifdef __GNUC__ + +#if __BYTE_ORDER == __LITTLE_ENDIAN +#define LS >> +#define RS << +#else +#define LS << +#define RS >> +#endif + + typedef uint32_t __attribute__((__may_alias__)) u32; + uint32_t w, x; + + for (; (uintptr_t)s % 4 && n; n--) *d++ = *s++; + + if ((uintptr_t)d % 4 == 0) { + for (; n >= 16; s += 16, d += 16, n -= 16) { + *(u32 *)(d + 0) = *(u32 *)(s + 0); + *(u32 *)(d + 4) = *(u32 *)(s + 4); + *(u32 *)(d + 8) = *(u32 *)(s + 8); + *(u32 *)(d + 12) = *(u32 *)(s + 12); + } + if (n & 8) { + *(u32 *)(d + 0) = *(u32 *)(s + 0); + *(u32 *)(d + 4) = *(u32 *)(s + 4); + d += 8; + s += 8; + } + if (n & 4) { + *(u32 *)(d + 0) = *(u32 *)(s + 0); + d += 4; + s += 4; + } + if (n & 2) { + *d++ = *s++; + *d++ = *s++; + } + if (n & 1) { + *d = *s; + } + return dest; + } + + if (n >= 32) switch ((uintptr_t)d % 4) { + case 1: + w = *(u32 *)s; + *d++ = *s++; + *d++ = *s++; + *d++ = *s++; + n -= 3; + for (; n >= 17; s += 16, d += 16, n -= 16) { + x = *(u32 *)(s + 1); + *(u32 *)(d + 0) = (w LS 24) | (x RS 8); + w = *(u32 *)(s + 5); + *(u32 *)(d + 4) = (x LS 24) | (w RS 8); + x = *(u32 *)(s + 9); + *(u32 *)(d + 8) = (w LS 24) | (x RS 8); + w = *(u32 *)(s + 13); + *(u32 *)(d + 12) = (x LS 24) | (w RS 8); + } + break; + case 2: + w = *(u32 *)s; + *d++ = *s++; + *d++ = *s++; + n -= 2; + for (; n >= 18; s += 16, d += 16, n -= 16) { + x = *(u32 *)(s + 2); + *(u32 *)(d + 0) = (w LS 16) | (x RS 16); + w = *(u32 *)(s + 6); + *(u32 *)(d + 4) = (x LS 16) | (w RS 16); + x = *(u32 *)(s + 10); + *(u32 *)(d + 8) = (w LS 16) | (x RS 16); + w = *(u32 *)(s + 14); + *(u32 *)(d + 12) = (x LS 16) | (w RS 16); + } + break; + case 3: + w = *(u32 *)s; + *d++ = *s++; + n -= 1; + for (; n >= 19; s += 16, d += 16, n -= 16) { + x = *(u32 *)(s + 3); + *(u32 *)(d + 0) = (w LS 8) | (x RS 24); + w = *(u32 *)(s + 7); + *(u32 *)(d + 4) = (x LS 8) | (w RS 24); + x = *(u32 *)(s + 11); + *(u32 *)(d + 8) = (w LS 8) | (x RS 24); + w = *(u32 *)(s + 15); + *(u32 *)(d + 12) = (x LS 8) | (w RS 24); + } + break; + } + if (n & 16) { + *d++ = *s++; + *d++ = *s++; + *d++ = *s++; + *d++ = *s++; + *d++ = *s++; + *d++ = *s++; + *d++ = *s++; + *d++ = *s++; + *d++ = *s++; + *d++ = *s++; + *d++ = *s++; + *d++ = *s++; + *d++ = *s++; + *d++ = *s++; + *d++ = *s++; + *d++ = *s++; + } + if (n & 8) { + *d++ = *s++; + *d++ = *s++; + *d++ = *s++; + *d++ = *s++; + *d++ = *s++; + *d++ = *s++; + *d++ = *s++; + *d++ = *s++; + } + if (n & 4) { + *d++ = *s++; + *d++ = *s++; + *d++ = *s++; + *d++ = *s++; + } + if (n & 2) { + *d++ = *s++; + *d++ = *s++; + } + if (n & 1) { + *d = *s; + } + return dest; +#endif + + for (; n; n--) *d++ = *s++; + return dest; +} + +#undef RESTRICT + +// users can define a new stack size if needed +#ifndef SMT_STACK_SIZE +#define SMT_STACK_SIZE 257 +#endif + +#define SMT_KEY_BYTES 32 +#define SMT_VALUE_BYTES 32 + +enum SMTErrorCode { + // SMT + ERROR_INSUFFICIENT_CAPACITY = 80, + ERROR_NOT_FOUND, + ERROR_INVALID_STACK, + ERROR_INVALID_SIBLING, + ERROR_INVALID_PROOF +}; + +/* Key Value Pairs */ +typedef struct { + uint8_t key[SMT_KEY_BYTES]; + uint8_t value[SMT_VALUE_BYTES]; + uint32_t order; +} smt_pair_t; + +typedef struct { + smt_pair_t *pairs; + uint32_t len; + uint32_t capacity; +} smt_state_t; + +void smt_state_init(smt_state_t *state, smt_pair_t *buffer, uint32_t capacity) { + state->pairs = buffer; + state->len = 0; + state->capacity = capacity; +} + +int smt_state_insert(smt_state_t *state, const uint8_t *key, const uint8_t *value) { + if (state->len < state->capacity) { + /* shortcut, append at end */ + _smt_fast_memcpy(state->pairs[state->len].key, key, SMT_KEY_BYTES); + _smt_fast_memcpy(state->pairs[state->len].value, value, SMT_KEY_BYTES); + state->len++; + return 0; + } + + /* Find a matched key and overwritten it */ + int32_t i = state->len - 1; + for (; i >= 0; i--) { + if (memcmp(key, state->pairs[i].key, SMT_KEY_BYTES) == 0) { + break; + } + } + + if (i < 0) { + return ERROR_INSUFFICIENT_CAPACITY; + } + + _smt_fast_memcpy(state->pairs[i].value, value, SMT_VALUE_BYTES); + return 0; +} + +int smt_state_fetch(smt_state_t *state, const uint8_t *key, uint8_t *value) { + int32_t i = state->len - 1; + for (; i >= 0; i--) { + if (memcmp(key, state->pairs[i].key, SMT_KEY_BYTES) == 0) { + _smt_fast_memcpy(value, state->pairs[i].value, SMT_VALUE_BYTES); + return 0; + } + } + return ERROR_NOT_FOUND; +} + +int _smt_pair_cmp(const void *a, const void *b) { + const smt_pair_t *pa = (const smt_pair_t *)a; + const smt_pair_t *pb = (const smt_pair_t *)b; + + for (int i = SMT_KEY_BYTES - 1; i >= 0; i--) { + int cmp_result = pa->key[i] - pb->key[i]; + if (cmp_result != 0) { + return cmp_result; + } + } + return pa->order - pb->order; +} + +void smt_state_normalize(smt_state_t *state) { + for (uint32_t i = 0; i < state->len; i++) { + state->pairs[i].order = state->len - i; + } + qsort(state->pairs, state->len, sizeof(smt_pair_t), _smt_pair_cmp); + /* Remove duplicate ones */ + int32_t sorted = 0, next = 0; + while (next < (int32_t)state->len) { + int32_t item_index = next++; + while (next < (int32_t)state->len && + memcmp(state->pairs[item_index].key, state->pairs[next].key, SMT_KEY_BYTES) == 0) { + next++; + } + if (item_index != sorted) { + _smt_fast_memcpy(state->pairs[sorted].key, state->pairs[item_index].key, SMT_KEY_BYTES); + _smt_fast_memcpy(state->pairs[sorted].value, state->pairs[item_index].value, SMT_VALUE_BYTES); + } + sorted++; + } + state->len = sorted; +} + +/* SMT */ + +int _smt_get_bit(const uint8_t *data, int offset) { + int byte_pos = offset / 8; + int bit_pos = offset % 8; + return ((data[byte_pos] >> bit_pos) & 1) != 0; +} + +void _smt_set_bit(uint8_t *data, int offset) { + int byte_pos = offset / 8; + int bit_pos = offset % 8; + data[byte_pos] |= 1 << bit_pos; +} + +void _smt_clear_bit(uint8_t *data, int offset) { + int byte_pos = offset / 8; + int bit_pos = offset % 8; + data[byte_pos] &= (uint8_t)(~(1 << bit_pos)); +} + +void _smt_copy_bits(uint8_t *source, int first_kept_bit) { + int first_byte = first_kept_bit / 8; + _smt_fast_memset(source, 0, first_byte); + for (int i = first_byte * 8; i < first_kept_bit; i++) { + _smt_clear_bit(source, i); + } +} + +void _smt_parent_path(uint8_t *key, uint8_t height) { + if (height == 255) { + _smt_fast_memset(key, 0, 32); + } else { + _smt_copy_bits(key, height + 1); + } +} + +int _smt_is_zero_hash(const uint8_t *value) { + for (int i = 0; i < 32; i++) { + if (value[i] != 0) { + return 0; + } + } + return 1; +} + +#define _SMT_MERGE_VALUE_ZERO 0 +#define _SMT_MERGE_VALUE_VALUE 1 +#define _SMT_MERGE_VALUE_MERGE_WITH_ZERO 2 + +typedef struct { + /* + * A _smt_merge_value_t typed value might be in any of the following + * 3 types: + * + * * When t is _SMT_MERGE_VALUE_ZERO, current variable represents a zero + * hash. value will still be set to all zero, it's just that testing t + * provides a quicker way to check against zero hashes + * * When t is _SMT_MERGE_VALUE_VALUE, current variable represents a non-zero + * hash value. + * * When t is _SMT_MERGE_VALUE_MERGE_WITH_ZERO, current variable represents + * a hash which is combined from a base node value with multiple zeros. + */ + uint8_t t; + + uint8_t value[SMT_VALUE_BYTES]; + uint8_t zero_bits[SMT_KEY_BYTES]; + uint8_t zero_count; +} _smt_merge_value_t; + +void _smt_merge_value_zero(_smt_merge_value_t *out) { + out->t = _SMT_MERGE_VALUE_ZERO; + _smt_fast_memset(out->value, 0, SMT_VALUE_BYTES); +} + +void _smt_merge_value_from_h256(const uint8_t *v, _smt_merge_value_t *out) { + if (_smt_is_zero_hash(v)) { + _smt_merge_value_zero(out); + } else { + out->t = _SMT_MERGE_VALUE_VALUE; + _smt_fast_memcpy(out->value, v, SMT_VALUE_BYTES); + } +} + +int _smt_merge_value_is_zero(const _smt_merge_value_t *v) { return v->t == _SMT_MERGE_VALUE_ZERO; } + +const uint8_t _SMT_MERGE_NORMAL = 1; +const uint8_t _SMT_MERGE_ZEROS = 2; + +/* Hash base node into a H256 */ +void _smt_hash_base_node(uint8_t base_height, const uint8_t *base_key, const uint8_t *base_value, + uint8_t out[SMT_VALUE_BYTES]) { + blake2b_state blake2b_ctx; + ckb_blake2b_init(&blake2b_ctx, SMT_VALUE_BYTES); + + blake2b_update(&blake2b_ctx, &base_height, 1); + blake2b_update(&blake2b_ctx, base_key, SMT_KEY_BYTES); + blake2b_update(&blake2b_ctx, base_value, SMT_VALUE_BYTES); + blake2b_final(&blake2b_ctx, out, SMT_VALUE_BYTES); +} + +void _smt_merge_value_hash(const _smt_merge_value_t *v, uint8_t *out) { + if (v->t == _SMT_MERGE_VALUE_MERGE_WITH_ZERO) { + blake2b_state blake2b_ctx; + ckb_blake2b_init(&blake2b_ctx, SMT_VALUE_BYTES); + + blake2b_update(&blake2b_ctx, &_SMT_MERGE_ZEROS, 1); + blake2b_update(&blake2b_ctx, v->value, SMT_VALUE_BYTES); + blake2b_update(&blake2b_ctx, v->zero_bits, SMT_KEY_BYTES); + blake2b_update(&blake2b_ctx, &(v->zero_count), 1); + blake2b_final(&blake2b_ctx, out, SMT_VALUE_BYTES); + } else { + _smt_fast_memcpy(out, v->value, SMT_VALUE_BYTES); + } +} + +void _smt_merge_with_zero(uint8_t height, const uint8_t *node_key, const _smt_merge_value_t *v, int set_bit, + _smt_merge_value_t *out) { + if (v->t == _SMT_MERGE_VALUE_MERGE_WITH_ZERO) { + if (out != v) { + _smt_fast_memcpy(out, v, sizeof(_smt_merge_value_t)); + } + if (set_bit) { + _smt_set_bit(out->zero_bits, height); + } + out->zero_count++; + } else { + out->t = _SMT_MERGE_VALUE_MERGE_WITH_ZERO; + _smt_hash_base_node(height, node_key, v->value, out->value); + _smt_fast_memset(out->zero_bits, 0, 32); + if (set_bit) { + _smt_set_bit(out->zero_bits, height); + } + out->zero_count = 1; + } +} + +/* Notice that output might collide with one of lhs, or rhs */ +void _smt_merge(uint8_t height, const uint8_t *node_key, const _smt_merge_value_t *lhs, const _smt_merge_value_t *rhs, + _smt_merge_value_t *out) { + int lhs_zero = _smt_merge_value_is_zero(lhs); + int rhs_zero = _smt_merge_value_is_zero(rhs); + + if (lhs_zero && rhs_zero) { + _smt_merge_value_zero(out); + return; + } + if (lhs_zero) { + _smt_merge_with_zero(height, node_key, rhs, 1, out); + return; + } + if (rhs_zero) { + _smt_merge_with_zero(height, node_key, lhs, 0, out); + return; + } + + blake2b_state blake2b_ctx; + ckb_blake2b_init(&blake2b_ctx, SMT_VALUE_BYTES); + uint8_t data[SMT_VALUE_BYTES]; + + blake2b_update(&blake2b_ctx, &_SMT_MERGE_NORMAL, 1); + blake2b_update(&blake2b_ctx, &height, 1); + blake2b_update(&blake2b_ctx, node_key, SMT_KEY_BYTES); + _smt_merge_value_hash(lhs, data); + blake2b_update(&blake2b_ctx, data, SMT_VALUE_BYTES); + _smt_merge_value_hash(rhs, data); + blake2b_update(&blake2b_ctx, data, SMT_VALUE_BYTES); + + blake2b_final(&blake2b_ctx, data, SMT_VALUE_BYTES); + _smt_merge_value_from_h256(data, out); +} + +const _smt_merge_value_t SMT_ZERO = {.t = _SMT_MERGE_VALUE_ZERO, .value = {0}}; + +/* + * Theoretically, a stack size of x should be able to process as many as + * 2 ** (x - 1) updates. In this case with a stack size of 32, we can deal + * with 2 ** 31 == 2147483648 updates, which is more than enough. + */ +int smt_calculate_root(uint8_t *buffer, const smt_state_t *pairs, const uint8_t *proof, uint32_t proof_length) { + uint8_t stack_keys[SMT_STACK_SIZE][SMT_KEY_BYTES]; + _smt_merge_value_t stack_values[SMT_STACK_SIZE]; + uint16_t stack_heights[SMT_STACK_SIZE] = {0}; + + uint32_t proof_index = 0; + uint32_t leave_index = 0; + uint32_t stack_top = 0; + + while (proof_index < proof_length) { + switch (proof[proof_index++]) { + case 0x4C: { + if (stack_top >= SMT_STACK_SIZE) { + return ERROR_INVALID_STACK; + } + if (leave_index >= pairs->len) { + return ERROR_INVALID_PROOF; + } + _smt_fast_memcpy(stack_keys[stack_top], pairs->pairs[leave_index].key, SMT_KEY_BYTES); + _smt_merge_value_from_h256(pairs->pairs[leave_index].value, &stack_values[stack_top]); + stack_heights[stack_top] = 0; + stack_top++; + leave_index++; + } break; + case 0x50: { + if (stack_top == 0) { + return ERROR_INVALID_STACK; + } + if (proof_index + 32 > proof_length) { + return ERROR_INVALID_PROOF; + } + _smt_merge_value_t sibling_node; + _smt_merge_value_from_h256(&proof[proof_index], &sibling_node); + proof_index += 32; + uint8_t *key = stack_keys[stack_top - 1]; + _smt_merge_value_t *value = &stack_values[stack_top - 1]; + uint16_t height = stack_heights[stack_top - 1]; + uint16_t *height_ptr = &stack_heights[stack_top - 1]; + if (height > 255) { + return ERROR_INVALID_PROOF; + } + uint8_t parent_key[SMT_KEY_BYTES]; + _smt_fast_memcpy(parent_key, key, SMT_KEY_BYTES); + _smt_parent_path(parent_key, height); + + // push value + if (_smt_get_bit(key, height)) { + _smt_merge((uint8_t)height, parent_key, &sibling_node, value, value); + } else { + _smt_merge((uint8_t)height, parent_key, value, &sibling_node, value); + } + // push key + _smt_parent_path(key, height); + // push height + *height_ptr = height + 1; + } break; + case 0x51: { + if (stack_top == 0) { + return ERROR_INVALID_STACK; + } + if (proof_index + 65 > proof_length) { + return ERROR_INVALID_PROOF; + } + _smt_merge_value_t sibling_node; + sibling_node.t = _SMT_MERGE_VALUE_MERGE_WITH_ZERO; + sibling_node.zero_count = proof[proof_index]; + _smt_fast_memcpy(&sibling_node.value, &proof[proof_index + 1], 32); + _smt_fast_memcpy(&sibling_node.zero_bits, &proof[proof_index + 33], 32); + proof_index += 65; + uint8_t *key = stack_keys[stack_top - 1]; + _smt_merge_value_t *value = &stack_values[stack_top - 1]; + uint16_t height = stack_heights[stack_top - 1]; + uint16_t *height_ptr = &stack_heights[stack_top - 1]; + if (height > 255) { + return ERROR_INVALID_PROOF; + } + uint8_t parent_key[SMT_KEY_BYTES]; + _smt_fast_memcpy(parent_key, key, SMT_KEY_BYTES); + _smt_parent_path(parent_key, height); + + // push value + if (_smt_get_bit(key, height)) { + _smt_merge((uint8_t)height, parent_key, &sibling_node, value, value); + } else { + _smt_merge((uint8_t)height, parent_key, value, &sibling_node, value); + } + // push key + _smt_parent_path(key, height); + // push height + *height_ptr = height + 1; + } break; + case 0x48: { + if (stack_top < 2) { + return ERROR_INVALID_STACK; + } + uint16_t *height_a_ptr = &stack_heights[stack_top - 2]; + + uint16_t height_a = stack_heights[stack_top - 2]; + uint8_t *key_a = stack_keys[stack_top - 2]; + _smt_merge_value_t *value_a = &stack_values[stack_top - 2]; + + uint16_t height_b = stack_heights[stack_top - 1]; + uint8_t *key_b = stack_keys[stack_top - 1]; + _smt_merge_value_t *value_b = &stack_values[stack_top - 1]; + stack_top -= 2; + if (height_a != height_b) { + return ERROR_INVALID_PROOF; + } + if (height_a > 255) { + return ERROR_INVALID_PROOF; + } + uint8_t parent_key[SMT_KEY_BYTES]; + _smt_fast_memcpy(parent_key, key_a, SMT_KEY_BYTES); + _smt_parent_path(parent_key, (uint8_t)height_a); + + // 2 keys should have same parent keys + _smt_parent_path(key_b, (uint8_t)height_b); + if (memcmp(parent_key, key_b, SMT_KEY_BYTES) != 0) { + return ERROR_INVALID_PROOF; + } + // push value + if (_smt_get_bit(key_a, height_a)) { + _smt_merge(height_a, parent_key, value_b, value_a, value_a); + } else { + _smt_merge(height_a, parent_key, value_a, value_b, value_a); + } + // push key + _smt_fast_memcpy(key_a, parent_key, SMT_KEY_BYTES); + // push height + *height_a_ptr = height_a + 1; + stack_top++; + } break; + case 0x4F: { + if (stack_top < 1) { + return ERROR_INVALID_STACK; + } + if (proof_index >= proof_length) { + return ERROR_INVALID_PROOF; + } + uint16_t n = proof[proof_index]; + proof_index++; + uint16_t zero_count = 0; + if (n == 0) { + zero_count = 256; + } else { + zero_count = n; + } + uint16_t *base_height_ptr = &stack_heights[stack_top - 1]; + uint16_t base_height = stack_heights[stack_top - 1]; + uint8_t *key = stack_keys[stack_top - 1]; + _smt_merge_value_t *value = &stack_values[stack_top - 1]; + if (base_height > 255) { + return ERROR_INVALID_PROOF; + } + uint8_t parent_key[SMT_KEY_BYTES]; + _smt_fast_memcpy(parent_key, key, SMT_KEY_BYTES); + uint16_t height_u16 = base_height; + for (uint16_t idx = 0; idx < zero_count; idx++) { + height_u16 = base_height + idx; + if (height_u16 > 255) { + return ERROR_INVALID_PROOF; + } + // the following code can be omitted: + // _smt_fast_memcpy(parent_key, key, SMT_KEY_BYTES); + // A key's parent's parent can be calculated from parent. + // it's not needed to do it from scratch. + // Make sure height_u16 is in increase order + _smt_parent_path(parent_key, (uint8_t)height_u16); + // push value + if (_smt_get_bit(key, (uint8_t)height_u16)) { + _smt_merge((uint8_t)height_u16, parent_key, &SMT_ZERO, value, value); + } else { + _smt_merge((uint8_t)height_u16, parent_key, value, &SMT_ZERO, value); + } + } + // push key + _smt_fast_memcpy(key, parent_key, SMT_KEY_BYTES); + // push height + *base_height_ptr = height_u16 + 1; + } break; + default: + return ERROR_INVALID_PROOF; + } + } + if (stack_top != 1) { + return ERROR_INVALID_STACK; + } + if (stack_heights[0] != 256) { + return ERROR_INVALID_PROOF; + } + /* All leaves must be used */ + if (leave_index != pairs->len) { + return ERROR_INVALID_PROOF; + } + + _smt_merge_value_hash(&stack_values[0], buffer); + return 0; +} + +int smt_verify(const uint8_t *hash, const smt_state_t *state, const uint8_t *proof, uint32_t proof_length) { + uint8_t buffer[32]; + int ret = smt_calculate_root(buffer, state, proof, proof_length); + if (ret != 0) { + return ret; + } + if (memcmp(buffer, hash, 32) != 0) { + return ERROR_INVALID_PROOF; + } + return 0; +} + +#endif diff --git a/src/misc_module.c b/src/misc_module.c new file mode 100644 index 0000000..588e3da --- /dev/null +++ b/src/misc_module.c @@ -0,0 +1,324 @@ +#include +#include +#include "cutils.h" +#include "misc_module.h" +#define BLAKE2_IMPL_H +#define BLAKE2_REF_C +#include "blake2b.h" +#include "ckb_smt.h" + +typedef struct { + uint8_t key[SMT_KEY_BYTES]; + uint8_t value[SMT_VALUE_BYTES]; +} KeyValuePair; + +typedef struct { + KeyValuePair *pairs; + size_t len; + size_t capacity; +} SMTWrapper; + +// Constructor for Smt class +static JSValue js_smt_constructor(JSContext *ctx, JSValueConst new_target, int argc, JSValueConst *argv) { + JSValue obj = JS_NewObjectClass(ctx, js_smt_class_id); + if (JS_IsException(obj)) return obj; + + // Initialize with empty dynamic array + SMTWrapper *wrapper = js_mallocz(ctx, sizeof(SMTWrapper)); + if (!wrapper) { + JS_FreeValue(ctx, obj); + return JS_EXCEPTION; + } + + wrapper->pairs = NULL; + wrapper->len = 0; + wrapper->capacity = 0; + + JS_SetOpaque(obj, wrapper); + return obj; +} + +// Insert method for Smt class +static JSValue js_smt_insert(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + SMTWrapper *w = JS_GetOpaque2(ctx, this_val, js_smt_class_id); + if (!w) return JS_EXCEPTION; + + size_t key_len, value_len; + uint8_t *key = JS_GetArrayBuffer(ctx, &key_len, argv[0]); + uint8_t *value = JS_GetArrayBuffer(ctx, &value_len, argv[1]); + + if (!key || !value || key_len != SMT_KEY_BYTES || value_len != SMT_VALUE_BYTES) + return JS_ThrowTypeError(ctx, "Invalid key or value format"); + + // Grow array if needed + if (w->len >= w->capacity) { + size_t new_capacity = w->capacity == 0 ? 16 : w->capacity * 2; + KeyValuePair *new_pairs = js_realloc(ctx, w->pairs, new_capacity * sizeof(KeyValuePair)); + if (!new_pairs) return JS_ThrowOutOfMemory(ctx); + + w->pairs = new_pairs; + w->capacity = new_capacity; + } + + // Store new pair + _smt_fast_memcpy(w->pairs[w->len].key, key, SMT_KEY_BYTES); + _smt_fast_memcpy(w->pairs[w->len].value, value, SMT_VALUE_BYTES); + w->len++; + + return JS_UNDEFINED; +} + +// Verify method for Smt class +static JSValue js_smt_verify(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + SMTWrapper *w = JS_GetOpaque2(ctx, this_val, js_smt_class_id); + if (!w) return JS_EXCEPTION; + + size_t root_len, proof_len; + uint8_t *root = JS_GetArrayBuffer(ctx, &root_len, argv[0]); + uint8_t *proof = JS_GetArrayBuffer(ctx, &proof_len, argv[1]); + + if (!root || root_len != SMT_VALUE_BYTES || !proof) return JS_ThrowTypeError(ctx, "Invalid root or proof format"); + + // Create temporary smt_state with collected pairs + smt_pair_t *temp_pairs = js_mallocz(ctx, w->len * sizeof(smt_pair_t)); + if (!temp_pairs) return JS_ThrowOutOfMemory(ctx); + + smt_state_t state; + smt_state_init(&state, temp_pairs, w->len); + + // Insert all collected pairs + for (size_t i = 0; i < w->len; i++) { + int ret = smt_state_insert(&state, w->pairs[i].key, w->pairs[i].value); + if (ret != 0) { + js_free(ctx, temp_pairs); + return JS_ThrowRangeError(ctx, "SMT insertion failed"); + } + } + smt_state_normalize(&state); + // Verify the proof + int ret = smt_verify(root, &state, proof, proof_len); + + js_free(ctx, temp_pairs); + return JS_NewBool(ctx, ret == 0); +} + +// Finalizer for Smt class +static void js_smt_finalizer(JSRuntime *rt, JSValue val) { + SMTWrapper *w = JS_GetOpaque(val, js_smt_class_id); + if (w) { + if (w->pairs) js_free_rt(rt, w->pairs); + js_free_rt(rt, w); + } +} + +// Convert ArrayBuffer to hex string +static JSValue js_encode_hex(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + size_t data_len; + uint8_t *data = JS_GetArrayBuffer(ctx, &data_len, argv[0]); + if (!data) return JS_ThrowTypeError(ctx, "Expected ArrayBuffer"); + + // Each byte becomes 2 hex characters + char *hex = js_malloc(ctx, data_len * 2 + 1); + if (!hex) return JS_ThrowOutOfMemory(ctx); + + for (size_t i = 0; i < data_len; i++) { + sprintf(hex + (i * 2), "%02x", data[i]); + } + + JSValue result = JS_NewString(ctx, hex); + js_free(ctx, hex); + return result; +} + +static uint8_t hex_char_to_int(char c) { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + return 0xFF; // Invalid hex character marker +} + +// Convert hex string to ArrayBuffer +static JSValue js_decode_hex(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + const char *hex = JS_ToCString(ctx, argv[0]); + if (!hex) return JS_ThrowTypeError(ctx, "Expected string"); + + size_t hex_len = strlen(hex); + if (hex_len % 2 != 0) { + JS_FreeCString(ctx, hex); + return JS_ThrowTypeError(ctx, "Invalid hex string length"); + } + + size_t data_len = hex_len / 2; + uint8_t *data = js_malloc(ctx, data_len); + if (!data) { + JS_FreeCString(ctx, hex); + return JS_ThrowOutOfMemory(ctx); + } + + for (size_t i = 0; i < data_len; i++) { + char high = hex[i * 2]; + char low = hex[i * 2 + 1]; + uint8_t high_val = hex_char_to_int(high); + uint8_t low_val = hex_char_to_int(low); + + if (high_val == 0xFF || low_val == 0xFF) { + js_free(ctx, data); + JS_FreeCString(ctx, hex); + return JS_ThrowTypeError(ctx, "Invalid hex character"); + } + + data[i] = (high_val << 4) | low_val; + } + + JS_FreeCString(ctx, hex); + return JS_NewArrayBufferCopy(ctx, data, data_len); +} + +// Convert ArrayBuffer to base64 string +static JSValue js_encode_base64(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + size_t data_len; + uint8_t *data = JS_GetArrayBuffer(ctx, &data_len, argv[0]); + if (!data) return JS_ThrowTypeError(ctx, "Expected ArrayBuffer"); + + static const char base64_chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + + size_t encoded_len = ((data_len + 2) / 3) * 4; + char *base64 = js_malloc(ctx, encoded_len + 1); + if (!base64) return JS_ThrowOutOfMemory(ctx); + + size_t i = 0, j = 0; + while (i < data_len) { + uint32_t octet_a = i < data_len ? data[i++] : 0; + uint32_t octet_b = i < data_len ? data[i++] : 0; + uint32_t octet_c = i < data_len ? data[i++] : 0; + + uint32_t triple = (octet_a << 16) + (octet_b << 8) + octet_c; + + base64[j++] = base64_chars[(triple >> 18) & 0x3F]; + base64[j++] = base64_chars[(triple >> 12) & 0x3F]; + base64[j++] = base64_chars[(triple >> 6) & 0x3F]; + base64[j++] = base64_chars[triple & 0x3F]; + } + + // Add padding + if (data_len % 3 >= 1) base64[encoded_len - 1] = '='; + if (data_len % 3 == 1) base64[encoded_len - 2] = '='; + base64[encoded_len] = '\0'; + + JSValue result = JS_NewString(ctx, base64); + js_free(ctx, base64); + return result; +} + +// Convert base64 string to ArrayBuffer +static JSValue js_decode_base64(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { + const char *base64 = JS_ToCString(ctx, argv[0]); + if (!base64) return JS_ThrowTypeError(ctx, "Expected string"); + + static const uint8_t base64_table[256] = { + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 62, 64, 64, 64, 63, 52, 53, 54, 55, 56, 57, + 58, 59, 60, 61, 64, 64, 64, 64, 64, 64, 64, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 64, 64, 64, 64, 64, 64, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, + 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64}; + + size_t base64_len = strlen(base64); + if (base64_len % 4 != 0) { + JS_FreeCString(ctx, base64); + return JS_ThrowTypeError(ctx, "Invalid base64 string length"); + } + + size_t padding = 0; + if (base64_len > 0 && base64[base64_len - 1] == '=') padding++; + if (base64_len > 1 && base64[base64_len - 2] == '=') padding++; + + size_t data_len = (base64_len / 4) * 3 - padding; + uint8_t *data = js_malloc(ctx, data_len); + if (!data) { + JS_FreeCString(ctx, base64); + return JS_ThrowOutOfMemory(ctx); + } + + size_t i = 0, j = 0; + while (i < base64_len) { + uint32_t sextet_a = base64_table[(uint8_t)base64[i++]]; + uint32_t sextet_b = base64_table[(uint8_t)base64[i++]]; + uint32_t sextet_c = base64_table[(uint8_t)base64[i++]]; + uint32_t sextet_d = base64_table[(uint8_t)base64[i++]]; + + if (sextet_a == 64 || sextet_b == 64 || sextet_c == 64 || sextet_d == 64) { + js_free(ctx, data); + JS_FreeCString(ctx, base64); + return JS_ThrowTypeError(ctx, "Invalid base64 character"); + } + + uint32_t triple = (sextet_a << 18) + (sextet_b << 12) + (sextet_c << 6) + sextet_d; + + if (j < data_len) data[j++] = (triple >> 16) & 0xFF; + if (j < data_len) data[j++] = (triple >> 8) & 0xFF; + if (j < data_len) data[j++] = triple & 0xFF; + } + + JS_FreeCString(ctx, base64); + return JS_NewArrayBufferCopy(ctx, data, data_len); +} + +// SMT class definition +static const JSCFunctionListEntry js_smt_proto_funcs[] = { + JS_CFUNC_DEF("insert", 2, js_smt_insert), + JS_CFUNC_DEF("verify", 2, js_smt_verify), +}; + +static const JSClassDef js_smt_class = { + "Smt", + .finalizer = js_smt_finalizer, +}; + +// Update the function list to include both SMT and encoding functions +static const JSCFunctionListEntry js_misc_funcs[] = { + JS_CFUNC_DEF("encodeHex", 1, js_encode_hex), + JS_CFUNC_DEF("decodeHex", 1, js_decode_hex), + JS_CFUNC_DEF("encodeBase64", 1, js_encode_base64), + JS_CFUNC_DEF("decodeBase64", 1, js_decode_base64), +}; + +static int js_misc_init(JSContext *ctx, JSModuleDef *m) { + JSValue proto, obj; + + // Initialize SMT class + JS_NewClassID(&js_smt_class_id); + JS_NewClass(JS_GetRuntime(ctx), js_smt_class_id, &js_smt_class); + + // Create and setup prototype + proto = JS_NewObject(ctx); + JS_SetPropertyFunctionList(ctx, proto, js_smt_proto_funcs, countof(js_smt_proto_funcs)); + + // Create constructor + obj = JS_NewCFunction2(ctx, js_smt_constructor, "Smt", 0, JS_CFUNC_constructor, 0); + JS_SetConstructor(ctx, obj, proto); + JS_SetClassProto(ctx, js_smt_class_id, proto); + + // Export the SMT constructor and encoding functions + JS_SetModuleExport(ctx, m, "Smt", obj); + + // Create module object for encoding functions + JSValue encoding = JS_NewObject(ctx); + JS_SetPropertyFunctionList(ctx, encoding, js_misc_funcs, countof(js_misc_funcs)); + JS_SetModuleExport(ctx, m, "encoding", encoding); + + return 0; +} + +int js_init_module_misc(JSContext *ctx) { + JSModuleDef *m; + m = JS_NewCModule(ctx, "misc", js_misc_init); + if (!m) return -1; + + JS_AddModuleExport(ctx, m, "Smt"); + JS_AddModuleExport(ctx, m, "encoding"); + return 0; +} diff --git a/src/misc_module.h b/src/misc_module.h new file mode 100644 index 0000000..117d83c --- /dev/null +++ b/src/misc_module.h @@ -0,0 +1,12 @@ +#ifndef JS_MISC_MODULE_H +#define JS_MISC_MODULE_H + +#include "quickjs.h" + +// Class ID for Smt +static JSClassID js_smt_class_id; + +// Module initialization +int js_init_module_misc(JSContext *ctx); + +#endif diff --git a/src/qjs.c b/src/qjs.c index 4498d46..79841e4 100644 --- a/src/qjs.c +++ b/src/qjs.c @@ -34,6 +34,7 @@ #include "ckb_module.h" #include "secp256k1_module.h" #include "hash_module.h" +#include "misc_module.h" #include "ckb_exec.h" #include "cmdopt.h" #include "qjs.h" @@ -370,6 +371,8 @@ int main(int argc, const char **argv) { CHECK(err); err = js_init_module_hash(ctx); CHECK(err); + err = js_init_module_misc(ctx); + CHECK(err); bool c_bool = cmdopt_has(co, "c"); const char *e_data = cmdopt_get(co, "e"); diff --git a/tests/module/Makefile b/tests/module/Makefile index 1e4299f..0b68716 100644 --- a/tests/module/Makefile +++ b/tests/module/Makefile @@ -17,4 +17,5 @@ endef all: $(call run,test_secp256k1.js) - $(call run,test_hash.js) \ No newline at end of file + $(call run,test_hash.js) + $(call debug,test_misc.js) \ No newline at end of file diff --git a/tests/module/test_misc.js b/tests/module/test_misc.js new file mode 100644 index 0000000..e1054e6 --- /dev/null +++ b/tests/module/test_misc.js @@ -0,0 +1,163 @@ +import * as misc from 'misc'; +import * as ckb from 'ckb'; + +function test_ckb_smt_verify1() { + // Test vectors from the Rust example + const keyHex = + '381dc5391dab099da5e28acd1ad859a051cf18ace804d037f12819c6fbc0e18b'; + const valueHex = + '9158ce9b0e11dd150ba2ae5d55c1db04b1c5986ec626f2e38a93fe8ad0b2923b'; + const rootHashHex = + 'ebe0fab376cd802d364eeb44af20c67a74d6183a33928fead163120ef12e6e06'; + const proofHex = + '4c4fff51ff322de8a89fe589987f97220cfcb6820bd798b31a0b56ffea221093d35f909e580b00000000000000000000000000000000000000000000000000000000000000'; + + // Update the conversions to use misc.encoding.decodeHex + const key = misc.encoding.decodeHex(keyHex); + const value = misc.encoding.decodeHex(valueHex); + const rootHash = misc.encoding.decodeHex(rootHashHex); + const proof = misc.encoding.decodeHex(proofHex); + + // Create new SMT instance + const smt = new misc.Smt(); + + // Insert key-value pair and measure cycles + const startInsert = ckb.current_cycles(); + smt.insert(key, value); + const endInsert = ckb.current_cycles(); + console.log(`SMT insert cycles: ${endInsert - startInsert}`); + + // Verify proof and measure cycles + const startVerify = ckb.current_cycles(); + const isValid = smt.verify(rootHash, proof); + const endVerify = ckb.current_cycles(); + console.log(`SMT verify cycles: ${endVerify - startVerify}`); + + console.assert(isValid === true, 'SMT verification failed'); + console.log('test_ckb_smt_verify1 ok'); +} + +function test_ckb_smt_verify2() { + const keyHex = + 'a9bb945be71f0bd2757d33d2465b6387383da42f321072e47472f0c9c7428a8a'; + const valueHex = + 'a939a47335f777eac4c40fbc0970e25f832a24e1d55adc45a7b76d63fe364e82'; + const rootHashHex = + '6e5c722644cd55cef8c4ed886cd8b44027ae9ed129e70a4b67d87be1c6857842'; + const proofHex = + '4c4fff51fa8aaa2aece17b92ec3f202a40a09f7286522bae1e5581a2a49195ab6781b1b8090000000000000000000000000000000000000000000000000000000000000000'; + + const key = misc.encoding.decodeHex(keyHex); + const value = misc.encoding.decodeHex(valueHex); + const rootHash = misc.encoding.decodeHex(rootHashHex); + const proof = misc.encoding.decodeHex(proofHex); + + const smt = new misc.Smt(); + + const startInsert = ckb.current_cycles(); + smt.insert(key, value); + const endInsert = ckb.current_cycles(); + console.log(`SMT verify2 insert cycles: ${endInsert - startInsert}`); + + const startVerify = ckb.current_cycles(); + const isValid = smt.verify(rootHash, proof); + const endVerify = ckb.current_cycles(); + console.log(`SMT verify2 verify cycles: ${endVerify - startVerify}`); + + console.assert(isValid === true, 'SMT verification2 failed'); + console.log('test_ckb_smt_verify2 ok'); +} + +function test_ckb_smt_verify3() { + const keyHex = + 'e8c0265680a02b680b6cbc880348f062b825b28e237da7169aded4bcac0a04e5'; + const valueHex = + '2ca41595841e46ce8e74ad749e5c3f1d17202150f99c3d8631233ebdd19b19eb'; + const rootHashHex = + 'c8f513901e34383bcec57c368628ce66da7496df0a180ee1e021df3d97cb8f7b'; + const proofHex = + '4c4fff51fa8aaa2aece17b92ec3f202a40a09f7286522bae1e5581a2a49195ab6781b1b8090000000000000000000000000000000000000000000000000000000000000000'; + + const key = misc.encoding.decodeHex(keyHex); + const value = misc.encoding.decodeHex(valueHex); + const rootHash = misc.encoding.decodeHex(rootHashHex); + const proof = misc.encoding.decodeHex(proofHex); + + const smt = new misc.Smt(); + + const startInsert = ckb.current_cycles(); + smt.insert(key, value); + const endInsert = ckb.current_cycles(); + console.log(`SMT verify3 insert cycles: ${endInsert - startInsert}`); + + const startVerify = ckb.current_cycles(); + const isValid = smt.verify(rootHash, proof); + const endVerify = ckb.current_cycles(); + console.log(`SMT verify3 verify cycles: ${endVerify - startVerify}`); + + console.assert(isValid === true, 'SMT verification3 failed'); + console.log('test_ckb_smt_verify3 ok'); +} + +function test_ckb_smt_verify_invalid() { + const keyHex = + 'e8c0265680a02b680b6cbc880348f062b825b28e237da7169aded4bcac0a04e5'; + const valueHex = + '2ca41595841e46ce8e74ad749e5c3f1d17202150f99c3d8631233ebdd19b19eb'; + const rootHashHex = + 'a4cbf1b69a848396ac759f362679e2b185ac87a17cba747d2db1ef6fd929042f'; + const proofHex = + '4c50fe32845309d34f132cd6f7ac6a7881962401adc35c19a18d4fffeb511b97eabf86'; + + const key = misc.encoding.decodeHex(keyHex); + const value = misc.encoding.decodeHex(valueHex); + const rootHash = misc.encoding.decodeHex(rootHashHex); + const proof = misc.encoding.decodeHex(proofHex); + + const smt = new misc.Smt(); + + const startInsert = ckb.current_cycles(); + smt.insert(key, value); + const endInsert = ckb.current_cycles(); + console.log(`SMT verify invalid insert cycles: ${endInsert - startInsert}`); + + const startVerify = ckb.current_cycles(); + const isValid = smt.verify(rootHash, proof); + const endVerify = ckb.current_cycles(); + console.log(`SMT verify invalid verify cycles: ${endVerify - startVerify}`); + + console.assert(isValid === false, 'SMT invalid verification should fail'); + console.log('test_ckb_smt_verify_invalid ok'); +} + +function test_base64_encode() { + const inputHex = '48656c6c6f20576f726c6421'; // "Hello World!" in hex + const expectedBase64 = 'SGVsbG8gV29ybGQh'; + + const input = misc.encoding.decodeHex(inputHex); + const encoded = misc.encoding.encodeBase64(input); + + console.assert(encoded === expectedBase64, 'Base64 encoding failed'); + console.log('test_base64_encode ok'); +} + +function test_base64_decode() { + const base64Input = 'SGVsbG8gV29ybGQh'; // "Hello World!" in base64 + const expectedHex = '48656c6c6f20576f726c6421'; + + const decoded = misc.encoding.decodeBase64(base64Input); + const result = misc.encoding.encodeHex(decoded); + + console.assert(result === expectedHex, 'Base64 decoding failed'); + console.log('test_base64_decode ok'); +} + +// Add the new test cases to the main execution +console.log('test_misc.js ...'); +test_ckb_smt_verify1(); +test_ckb_smt_verify2(); +test_ckb_smt_verify3(); +test_ckb_smt_verify_invalid(); +test_base64_encode(); +test_base64_decode(); +console.log('test_misc.js ok'); From be9b87b08955fb077b62a9d87dc5c6fc20d747ae Mon Sep 17 00:00:00 2001 From: Lyndon Date: Thu, 9 Jan 2025 11:26:38 +0800 Subject: [PATCH 2/4] fix: use misc.hex.encode styles from misc.encoding.encodeHex --- src/misc_module.c | 34 ++++++++++++++++++++----------- tests/module/Makefile | 2 +- tests/module/test_misc.js | 42 +++++++++++++++++++-------------------- 3 files changed, 44 insertions(+), 34 deletions(-) diff --git a/src/misc_module.c b/src/misc_module.c index 588e3da..af4db3b 100644 --- a/src/misc_module.c +++ b/src/misc_module.c @@ -267,6 +267,17 @@ static JSValue js_decode_base64(JSContext *ctx, JSValueConst this_val, int argc, return JS_NewArrayBufferCopy(ctx, data, data_len); } +// Update the function list to include hex and base64 functions under separate objects +static const JSCFunctionListEntry js_hex_funcs[] = { + JS_CFUNC_DEF("encode", 1, js_encode_hex), + JS_CFUNC_DEF("decode", 1, js_decode_hex), +}; + +static const JSCFunctionListEntry js_base64_funcs[] = { + JS_CFUNC_DEF("encode", 1, js_encode_base64), + JS_CFUNC_DEF("decode", 1, js_decode_base64), +}; + // SMT class definition static const JSCFunctionListEntry js_smt_proto_funcs[] = { JS_CFUNC_DEF("insert", 2, js_smt_insert), @@ -278,13 +289,6 @@ static const JSClassDef js_smt_class = { .finalizer = js_smt_finalizer, }; -// Update the function list to include both SMT and encoding functions -static const JSCFunctionListEntry js_misc_funcs[] = { - JS_CFUNC_DEF("encodeHex", 1, js_encode_hex), - JS_CFUNC_DEF("decodeHex", 1, js_decode_hex), - JS_CFUNC_DEF("encodeBase64", 1, js_encode_base64), - JS_CFUNC_DEF("decodeBase64", 1, js_decode_base64), -}; static int js_misc_init(JSContext *ctx, JSModuleDef *m) { JSValue proto, obj; @@ -305,10 +309,15 @@ static int js_misc_init(JSContext *ctx, JSModuleDef *m) { // Export the SMT constructor and encoding functions JS_SetModuleExport(ctx, m, "Smt", obj); - // Create module object for encoding functions - JSValue encoding = JS_NewObject(ctx); - JS_SetPropertyFunctionList(ctx, encoding, js_misc_funcs, countof(js_misc_funcs)); - JS_SetModuleExport(ctx, m, "encoding", encoding); + // Create hex object and add functions + JSValue hex = JS_NewObject(ctx); + JS_SetPropertyFunctionList(ctx, hex, js_hex_funcs, countof(js_hex_funcs)); + JS_SetModuleExport(ctx, m, "hex", hex); + + // Create base64 object and add functions + JSValue base64 = JS_NewObject(ctx); + JS_SetPropertyFunctionList(ctx, base64, js_base64_funcs, countof(js_base64_funcs)); + JS_SetModuleExport(ctx, m, "base64", base64); return 0; } @@ -319,6 +328,7 @@ int js_init_module_misc(JSContext *ctx) { if (!m) return -1; JS_AddModuleExport(ctx, m, "Smt"); - JS_AddModuleExport(ctx, m, "encoding"); + JS_AddModuleExport(ctx, m, "hex"); + JS_AddModuleExport(ctx, m, "base64"); return 0; } diff --git a/tests/module/Makefile b/tests/module/Makefile index 0b68716..d81aa7f 100644 --- a/tests/module/Makefile +++ b/tests/module/Makefile @@ -18,4 +18,4 @@ endef all: $(call run,test_secp256k1.js) $(call run,test_hash.js) - $(call debug,test_misc.js) \ No newline at end of file + $(call run,test_misc.js) diff --git a/tests/module/test_misc.js b/tests/module/test_misc.js index e1054e6..5132346 100644 --- a/tests/module/test_misc.js +++ b/tests/module/test_misc.js @@ -12,11 +12,11 @@ function test_ckb_smt_verify1() { const proofHex = '4c4fff51ff322de8a89fe589987f97220cfcb6820bd798b31a0b56ffea221093d35f909e580b00000000000000000000000000000000000000000000000000000000000000'; - // Update the conversions to use misc.encoding.decodeHex - const key = misc.encoding.decodeHex(keyHex); - const value = misc.encoding.decodeHex(valueHex); - const rootHash = misc.encoding.decodeHex(rootHashHex); - const proof = misc.encoding.decodeHex(proofHex); + // Update the conversions to use misc.hex.decode + const key = misc.hex.decode(keyHex); + const value = misc.hex.decode(valueHex); + const rootHash = misc.hex.decode(rootHashHex); + const proof = misc.hex.decode(proofHex); // Create new SMT instance const smt = new misc.Smt(); @@ -47,10 +47,10 @@ function test_ckb_smt_verify2() { const proofHex = '4c4fff51fa8aaa2aece17b92ec3f202a40a09f7286522bae1e5581a2a49195ab6781b1b8090000000000000000000000000000000000000000000000000000000000000000'; - const key = misc.encoding.decodeHex(keyHex); - const value = misc.encoding.decodeHex(valueHex); - const rootHash = misc.encoding.decodeHex(rootHashHex); - const proof = misc.encoding.decodeHex(proofHex); + const key = misc.hex.decode(keyHex); + const value = misc.hex.decode(valueHex); + const rootHash = misc.hex.decode(rootHashHex); + const proof = misc.hex.decode(proofHex); const smt = new misc.Smt(); @@ -78,10 +78,10 @@ function test_ckb_smt_verify3() { const proofHex = '4c4fff51fa8aaa2aece17b92ec3f202a40a09f7286522bae1e5581a2a49195ab6781b1b8090000000000000000000000000000000000000000000000000000000000000000'; - const key = misc.encoding.decodeHex(keyHex); - const value = misc.encoding.decodeHex(valueHex); - const rootHash = misc.encoding.decodeHex(rootHashHex); - const proof = misc.encoding.decodeHex(proofHex); + const key = misc.hex.decode(keyHex); + const value = misc.hex.decode(valueHex); + const rootHash = misc.hex.decode(rootHashHex); + const proof = misc.hex.decode(proofHex); const smt = new misc.Smt(); @@ -109,10 +109,10 @@ function test_ckb_smt_verify_invalid() { const proofHex = '4c50fe32845309d34f132cd6f7ac6a7881962401adc35c19a18d4fffeb511b97eabf86'; - const key = misc.encoding.decodeHex(keyHex); - const value = misc.encoding.decodeHex(valueHex); - const rootHash = misc.encoding.decodeHex(rootHashHex); - const proof = misc.encoding.decodeHex(proofHex); + const key = misc.hex.decode(keyHex); + const value = misc.hex.decode(valueHex); + const rootHash = misc.hex.decode(rootHashHex); + const proof = misc.hex.decode(proofHex); const smt = new misc.Smt(); @@ -134,8 +134,8 @@ function test_base64_encode() { const inputHex = '48656c6c6f20576f726c6421'; // "Hello World!" in hex const expectedBase64 = 'SGVsbG8gV29ybGQh'; - const input = misc.encoding.decodeHex(inputHex); - const encoded = misc.encoding.encodeBase64(input); + const input = misc.hex.decode(inputHex); + const encoded = misc.base64.encode(input); console.assert(encoded === expectedBase64, 'Base64 encoding failed'); console.log('test_base64_encode ok'); @@ -145,8 +145,8 @@ function test_base64_decode() { const base64Input = 'SGVsbG8gV29ybGQh'; // "Hello World!" in base64 const expectedHex = '48656c6c6f20576f726c6421'; - const decoded = misc.encoding.decodeBase64(base64Input); - const result = misc.encoding.encodeHex(decoded); + const decoded = misc.base64.decode(base64Input); + const result = misc.hex.encode(decoded); console.assert(result === expectedHex, 'Base64 decoding failed'); console.log('test_base64_decode ok'); From 56a9ab7ad0b932bfb8551aebbd84729afa510c39 Mon Sep 17 00:00:00 2001 From: Lyndon Date: Thu, 9 Jan 2025 13:27:57 +0800 Subject: [PATCH 3/4] fix: use lookup tables for hex --- src/misc_module.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/misc_module.c b/src/misc_module.c index af4db3b..71c939c 100644 --- a/src/misc_module.c +++ b/src/misc_module.c @@ -121,9 +121,14 @@ static JSValue js_encode_hex(JSContext *ctx, JSValueConst this_val, int argc, JS char *hex = js_malloc(ctx, data_len * 2 + 1); if (!hex) return JS_ThrowOutOfMemory(ctx); + // Lookup table for hex conversion + static const char hex_chars[] = "0123456789abcdef"; + for (size_t i = 0; i < data_len; i++) { - sprintf(hex + (i * 2), "%02x", data[i]); + hex[i * 2] = hex_chars[data[i] >> 4]; + hex[i * 2 + 1] = hex_chars[data[i] & 0x0F]; } + hex[data_len * 2] = '\0'; JSValue result = JS_NewString(ctx, hex); js_free(ctx, hex); From 440028f89b5b5d7515ef44308ac0a6745b6b1482 Mon Sep 17 00:00:00 2001 From: Lyndon Date: Fri, 10 Jan 2025 09:49:43 +0800 Subject: [PATCH 4/4] tests: add failure cases --- tests/module/test_misc.js | 58 ++++++++++----- tests/module/test_secp256k1.js | 132 +++++++++++++++++++-------------- 2 files changed, 118 insertions(+), 72 deletions(-) diff --git a/tests/module/test_misc.js b/tests/module/test_misc.js index 5132346..8913197 100644 --- a/tests/module/test_misc.js +++ b/tests/module/test_misc.js @@ -1,7 +1,7 @@ import * as misc from 'misc'; import * as ckb from 'ckb'; -function test_ckb_smt_verify1() { +function test_ckb_smt_verify1(failure) { // Test vectors from the Rust example const keyHex = '381dc5391dab099da5e28acd1ad859a051cf18ace804d037f12819c6fbc0e18b'; @@ -27,17 +27,28 @@ function test_ckb_smt_verify1() { const endInsert = ckb.current_cycles(); console.log(`SMT insert cycles: ${endInsert - startInsert}`); - // Verify proof and measure cycles - const startVerify = ckb.current_cycles(); - const isValid = smt.verify(rootHash, proof); - const endVerify = ckb.current_cycles(); - console.log(`SMT verify cycles: ${endVerify - startVerify}`); + if (failure) { + // Verify proof and measure cycles + const startVerify = ckb.current_cycles(); + const wrongRootHash = misc.hex.decode( + '0000000000000000000000000000000000000000000000000000000000000000'); + const isValid = smt.verify(wrongRootHash, proof); + const endVerify = ckb.current_cycles(); + console.log(`SMT verify cycles: ${endVerify - startVerify}`); + console.assert(isValid === false, 'SMT verification should fail'); + } else { + // Verify proof and measure cycles + const startVerify = ckb.current_cycles(); + const isValid = smt.verify(rootHash, proof); + const endVerify = ckb.current_cycles(); + console.log(`SMT verify cycles: ${endVerify - startVerify}`); + console.assert(isValid === true, 'SMT verification failed'); + } - console.assert(isValid === true, 'SMT verification failed'); console.log('test_ckb_smt_verify1 ok'); } -function test_ckb_smt_verify2() { +function test_ckb_smt_verify2(failure) { const keyHex = 'a9bb945be71f0bd2757d33d2465b6387383da42f321072e47472f0c9c7428a8a'; const valueHex = @@ -59,12 +70,21 @@ function test_ckb_smt_verify2() { const endInsert = ckb.current_cycles(); console.log(`SMT verify2 insert cycles: ${endInsert - startInsert}`); - const startVerify = ckb.current_cycles(); - const isValid = smt.verify(rootHash, proof); - const endVerify = ckb.current_cycles(); - console.log(`SMT verify2 verify cycles: ${endVerify - startVerify}`); - - console.assert(isValid === true, 'SMT verification2 failed'); + if (failure) { + const startVerify = ckb.current_cycles(); + const wrongProof = misc.hex.decode( + '0000000000000000000000000000000000000000000000000000000000000000'); + const isValid = smt.verify(rootHash, wrongProof); + const endVerify = ckb.current_cycles(); + console.log(`SMT verify2 verify cycles: ${endVerify - startVerify}`); + console.assert(isValid === false, 'SMT verification2 should fail'); + } else { + const startVerify = ckb.current_cycles(); + const isValid = smt.verify(rootHash, proof); + const endVerify = ckb.current_cycles(); + console.log(`SMT verify2 verify cycles: ${endVerify - startVerify}`); + console.assert(isValid === true, 'SMT verification2 failed'); + } console.log('test_ckb_smt_verify2 ok'); } @@ -131,7 +151,7 @@ function test_ckb_smt_verify_invalid() { } function test_base64_encode() { - const inputHex = '48656c6c6f20576f726c6421'; // "Hello World!" in hex + const inputHex = '48656c6c6f20576f726c6421'; // "Hello World!" in hex const expectedBase64 = 'SGVsbG8gV29ybGQh'; const input = misc.hex.decode(inputHex); @@ -142,7 +162,7 @@ function test_base64_encode() { } function test_base64_decode() { - const base64Input = 'SGVsbG8gV29ybGQh'; // "Hello World!" in base64 + const base64Input = 'SGVsbG8gV29ybGQh'; // "Hello World!" in base64 const expectedHex = '48656c6c6f20576f726c6421'; const decoded = misc.base64.decode(base64Input); @@ -154,8 +174,10 @@ function test_base64_decode() { // Add the new test cases to the main execution console.log('test_misc.js ...'); -test_ckb_smt_verify1(); -test_ckb_smt_verify2(); +test_ckb_smt_verify1(true); +test_ckb_smt_verify1(false); +test_ckb_smt_verify2(true); +test_ckb_smt_verify2(false); test_ckb_smt_verify3(); test_ckb_smt_verify_invalid(); test_base64_encode(); diff --git a/tests/module/test_secp256k1.js b/tests/module/test_secp256k1.js index e6bfaf8..18f1d67 100644 --- a/tests/module/test_secp256k1.js +++ b/tests/module/test_secp256k1.js @@ -1,69 +1,91 @@ import * as secp256k1 from 'secp256k1'; import * as ckb from 'ckb'; +import * as misc from 'misc'; -function hexStringToUint8Array(hexString) { - // Remove any non-hex characters (like spaces and commas) - hexString = hexString.replace(/[^0-9A-Fa-f]/g, ''); - const bytes = new Uint8Array(hexString.length / 2); - for (let i = 0; i < hexString.length; i += 2) { - bytes[i / 2] = parseInt(hexString.substr(i, 2), 16); - } - return bytes; -} - -function arrayBufferToHexString(buffer) { - return Array.from(new Uint8Array(buffer)) - .map(b => b.toString(16).padStart(2, '0')) - .join(''); -} - -function test_recovery() { +function test_recovery(failure) { const recid = 1; - const msg = hexStringToUint8Array( + const msg = misc.hex.decode( '6a0024347e28905e2587c4c7598332a39' + 'ba6684bb6b74653511656a02bd20edb'); - const sig = hexStringToUint8Array( + const sig = misc.hex.decode( '76e6d0e5ea61b46fe10443fe5b4d1bc6' + 'ce2d0d49d55e810312f7c22702e0548a' + '3969ce72940a34632f93ebd1b8d591c3' + '775428f035c6577e4adf8068b04819f0'); const s = ckb.current_cycles(); - const expected_pubkey = hexStringToUint8Array( + const expected_pubkey = misc.hex.decode( 'aca98c5822b997c15f8c974386a11b14' + 'a0d009a4d5156e145644573e82ef7e7b' + '226b9eb6173d6b4504606eb8d9558bde' + '98d12100836e92d306a40f337ed8a0f3'); const e = ckb.current_cycles(); - console.log(`hexStringToUint8Array: ${e - s}`); + console.log(`misc.hex.decode: ${e - s}`); // Verify the signature - const start = ckb.current_cycles(); - const pubkey = secp256k1.recover(sig.buffer, recid, msg.buffer); - const end = ckb.current_cycles(); - console.log(`recover cycles: ${end - start}`); - console.assert( - arrayBufferToHexString(pubkey) === - arrayBufferToHexString(expected_pubkey), - 'Signature recovery failed'); + if (failure) { + let success = false; + const wrong_sig = misc.hex.decode( + '00000000000000000000000000000000' + + '00000000000000000000000000000000' + + '00000000000000000000000000000000' + + '00000000000000000000000000000000'); + try { + success = secp256k1.recover(wrong_sig, recid, msg); + } catch (e) { + success = true; + } + console.assert(success, 'Signature recovery should fail'); + } else { + const start = ckb.current_cycles(); + const pubkey = secp256k1.recover(sig, recid, msg); + const end = ckb.current_cycles(); + console.log(`recover cycles: ${end - start}`); + console.assert( + misc.hex.encode(pubkey) === misc.hex.encode(expected_pubkey), + 'Signature recovery failed'); + } console.log('test_recovery ok'); } +function test_recovery_failure() { + const recid = 1; + const wrong_msg = misc.hex.decode( + '00000000000000000000000000000000' + + '00000000000000000000000000000000'); + const sig = misc.hex.decode( + '76e6d0e5ea61b46fe10443fe5b4d1bc6' + + 'ce2d0d49d55e810312f7c22702e0548a' + + '3969ce72940a34632f93ebd1b8d591c3' + + '775428f035c6577e4adf8068b04819f0'); + const expected_pubkey = misc.hex.decode( + 'aca98c5822b997c15f8c974386a11b14' + + 'a0d009a4d5156e145644573e82ef7e7b' + + '226b9eb6173d6b4504606eb8d9558bde' + + '98d12100836e92d306a40f337ed8a0f3'); + + const pubkey = secp256k1.recover(sig, recid, wrong_msg); + console.assert( + misc.hex.encode(pubkey) !== misc.hex.encode(expected_pubkey), + 'Signature recovery should fail'); + console.log('test_recovery_failure ok'); +} + function test_verify() { - const sig = hexStringToUint8Array( + const sig = misc.hex.decode( '76e6d0e5ea61b46fe10443fe5b4d1bc6' + 'ce2d0d49d55e810312f7c22702e0548a' + '3969ce72940a34632f93ebd1b8d591c3' + '775428f035c6577e4adf8068b04819f0'); - const msg = hexStringToUint8Array( + const msg = misc.hex.decode( '6a0024347e28905e2587c4c7598332a39' + 'ba6684bb6b74653511656a02bd20edb'); - const pubkey = hexStringToUint8Array( + const pubkey = misc.hex.decode( 'aca98c5822b997c15f8c974386a11b14' + 'a0d009a4d5156e145644573e82ef7e7b' + '226b9eb6173d6b4504606eb8d9558bde' + '98d12100836e92d306a40f337ed8a0f3'); const start = ckb.current_cycles(); - const success = secp256k1.verify(sig.buffer, msg.buffer, pubkey.buffer); + const success = secp256k1.verify(sig, msg, pubkey); const end = ckb.current_cycles(); console.log(`verify cycles: ${end - start}`); console.assert(success, 'test_verify failed'); @@ -72,51 +94,51 @@ function test_verify() { } function test_parse_pubkey() { - const pubkey = hexStringToUint8Array( + const pubkey = misc.hex.decode( '0375fbccbf29be9408ed96ca232fb941' + 'b358e6158ace9fbfe8214c994d38bd9ff9'); const start = ckb.current_cycles(); - const out_pubkey = secp256k1.parsePubkey(pubkey.buffer); + const out_pubkey = secp256k1.parsePubkey(pubkey); const end = ckb.current_cycles(); console.log(`parsePubkey cycles: ${end - start}`); console.assert( - arrayBufferToHexString(out_pubkey) === - "f99fbd384d994c21e8bf9fce8a15e658" + - "b341b92f23ca96ed0894be29bfccfb75" + - "3d797a1b2ce723964030b3ef1e31656b" + - "04a9c3fadcf100a613b385fec85620d1", + misc.hex.encode(out_pubkey) === + 'f99fbd384d994c21e8bf9fce8a15e658' + + 'b341b92f23ca96ed0894be29bfccfb75' + + '3d797a1b2ce723964030b3ef1e31656b' + + '04a9c3fadcf100a613b385fec85620d1', 'parsePubkey failed'); - console.log("test_parse_pubkey ok"); + console.log('test_parse_pubkey ok'); } function test_serialize_pubkey() { - const pubkey = hexStringToUint8Array( + const pubkey = misc.hex.decode( 'f99fbd384d994c21e8bf9fce8a15e658' + 'b341b92f23ca96ed0894be29bfccfb75' + '3d797a1b2ce723964030b3ef1e31656b' + '04a9c3fadcf100a613b385fec85620d1'); const start = ckb.current_cycles(); - const out_pubkey = secp256k1.serializePubkey(pubkey.buffer, true); + const out_pubkey = secp256k1.serializePubkey(pubkey, true); const end = ckb.current_cycles(); console.log(`serializePubkey: ${end - start}`); console.assert( - arrayBufferToHexString(out_pubkey) === - '0375fbccbf29be9408ed96ca232fb941' + - 'b358e6158ace9fbfe8214c994d38bd9ff9', + misc.hex.encode(out_pubkey) === + '0375fbccbf29be9408ed96ca232fb941' + + 'b358e6158ace9fbfe8214c994d38bd9ff9', 'serializePubkey failed'); - const pubkey2 = hexStringToUint8Array( + const pubkey2 = misc.hex.decode( '11dae3a18c58627d4564aff118f9b49f' + 'c1dc48992e0f615cef19732b7d8842d4' + '95a0eb41da0b71a3e18dd063e9097d40' + '944936ee93f498b26188faaa02276bde'); - const out_pubkey2 = secp256k1.serializePubkey(pubkey2.buffer, false); + const out_pubkey2 = secp256k1.serializePubkey(pubkey2, false); console.assert( - arrayBufferToHexString(out_pubkey2) === - '04d442887d2b7319ef5c610f2e9948dc' + - 'c19fb4f918f1af64457d62588ca1e3da' + - '11de6b2702aafa8861b298f493ee3649' + - '94407d09e963d08de1a3710bda41eba095', + misc.hex.encode(out_pubkey2) === + '04d442887d2b7319ef5c610f2e9948dc' + + 'c19fb4f918f1af64457d62588ca1e3da' + + '11de6b2702aafa8861b298f493ee3649' + + '94407d09e963d08de1a3710bda41eba095', 'serializePubkey failed'); console.log('test_serialize_pubkey ok'); @@ -134,7 +156,9 @@ function test_func_not_found() { } console.log('test_secp256k1.js ...'); -test_recovery(); +test_recovery(true); +test_recovery(false); +test_recovery_failure(); test_verify(); test_parse_pubkey(); test_serialize_pubkey();