diff --git a/bech32.patch b/bech32.patch index 3a0aa0e..497e4b5 100644 --- a/bech32.patch +++ b/bech32.patch @@ -1,7 +1,16 @@ diff --git a/ref/c/segwit_addr.c b/ref/c/segwit_addr.c -index 390b3da..d6f100c 100644 +index 390b3da..bf199d5 100644 --- a/ref/c/segwit_addr.c +++ b/ref/c/segwit_addr.c +@@ -67,7 +67,7 @@ int bech32_encode(char *output, const char *hrp, const uint8_t *data, size_t dat + chk = bech32_polymod_step(chk) ^ (ch >> 5); + ++i; + } +- if (i + 7 + data_len > 90) return 0; ++ if (i + 7 + data_len > BECH32_MAX_LEN) return 0; + chk = bech32_polymod_step(chk); + while (*hrp != 0) { + chk = bech32_polymod_step(chk) ^ (*hrp & 0x1f); @@ -153,7 +153,7 @@ bech32_encoding bech32_decode(char* hrp, uint8_t *data, size_t *data_len, const } } @@ -12,10 +21,28 @@ index 390b3da..d6f100c 100644 int bits = 0; uint32_t maxv = (((uint32_t)1) << outbits) - 1; diff --git a/ref/c/segwit_addr.h b/ref/c/segwit_addr.h -index 096870f..9a70e41 100644 +index 096870f..32c0e7b 100644 --- a/ref/c/segwit_addr.h +++ b/ref/c/segwit_addr.h -@@ -131,4 +131,14 @@ bech32_encoding bech32_decode( +@@ -93,6 +93,17 @@ int segwit_addr_decode_detailed( + const char* addr + ); + ++/** Longest bech32/bech32m string this build will produce or accept from ++ * bech32_encode(). BIP-173 caps segwit addresses at 90; BIP-352 silent ++ * payment addresses are ~117. ++ */ ++#define BECH32_MAX_LEN 128 ++ ++/** Output buffer size bech32_encode() requires: strlen(hrp) + data_len + 8. ++ * Size every encode buffer with this so it tracks BECH32_MAX_LEN. ++ */ ++#define BECH32_BUF_SIZE (BECH32_MAX_LEN + 8) ++ + /** Encode a Bech32 or Bech32m string + * + * Out: output: Pointer to a buffer of size strlen(hrp) + data_len + 8 that +@@ -131,4 +142,14 @@ bech32_encoding bech32_decode( const char *input ); diff --git a/ngu/codecs.c b/ngu/codecs.c index e9868db..d203146 100644 --- a/ngu/codecs.c +++ b/ngu/codecs.c @@ -100,7 +100,7 @@ STATIC mp_obj_t c_segwit_encode(mp_obj_t hrp_in, mp_obj_t witver_in, mp_obj_t pr mp_get_buffer_raise(prog_in, &prog, MP_BUFFER_READ); - char tmp[127]; + char tmp[BECH32_BUF_SIZE]; int ok = segwit_addr_encode(tmp, hrp, witver, prog.buf, prog.len); if(!ok) { @@ -153,7 +153,7 @@ STATIC mp_obj_t c_nip19_encode(mp_obj_t hrp_in, mp_obj_t prog_in) size_t datalen = 0; convert_bits(data, &datalen, 5, prog.buf, prog.len, 8, 1); - char tmp[127]; + char tmp[BECH32_BUF_SIZE]; int ok = bech32_encode(tmp, hrp, data, datalen, enc); if(!ok) { mp_raise_ValueError(MP_ERROR_TEXT("nip19_encode")); @@ -187,6 +187,71 @@ STATIC mp_obj_t c_nip19_decode(mp_obj_t str_in) STATIC MP_DEFINE_CONST_FUN_OBJ_1(c_nip19_decode_obj, c_nip19_decode); +// BIP-352 Silent Payment address encoding +static mp_obj_t c_bip352_encode(size_t n_args, const mp_obj_t *args) +{ + // Args: hrp, scan_key, spend_key, [version] + // version is optional, defaults to 0 + const char *hrp = mp_obj_str_get_str(args[0]); + + mp_buffer_info_t scan_key, spend_key; + mp_get_buffer_raise(args[1], &scan_key, MP_BUFFER_READ); + mp_get_buffer_raise(args[2], &spend_key, MP_BUFFER_READ); + + // Get version (optional, defaults to 0) + int version = 0; + if (n_args >= 4) { + version = mp_obj_get_int_truncated(args[3]); + } + + // Validate version per BIP-352 + if (version < 0 || version > 31) { + mp_raise_ValueError(MP_ERROR_TEXT("version must be 0-31")); + } + + // Validate input sizes + // scan_key: 32 bytes (scan privkey) or 33 bytes (scan compressed pubkey) + // spend_key: 33 bytes (compressed pubkey) + // The 32-byte form is not a BIP-352 address: it carries the scan privkey for + // the watch-only spscan/tspscan export, and is only meaningful under those HRPs. + if ((scan_key.len != 32 && scan_key.len != 33) || spend_key.len != 33) { + mp_raise_ValueError(MP_ERROR_TEXT("scan_key must be 32 or 33 bytes, spend_key 33")); + } + + // Concatenate scan || spend (65 or 66 bytes) + size_t payload_len = scan_key.len + spend_key.len; + uint8_t payload[66]; + memcpy(payload, scan_key.buf, scan_key.len); + memcpy(payload + scan_key.len, spend_key.buf, spend_key.len); + + // Convert 8-bit to 5-bit encoding (max 106 symbols for 66 bytes) + uint8_t converted[106]; + size_t converted_len = 0; + int conv_ok = convert_bits(converted, &converted_len, 5, payload, payload_len, 8, 1); + if (!conv_ok) { + mp_raise_ValueError(MP_ERROR_TEXT("convert_bits failed")); + } + + // Build final data array with version prepended + // Total: 1 (version) + converted_len 5-bit values + uint8_t data[1 + sizeof(converted)]; + data[0] = version; + memcpy(data + 1, converted, converted_len); + size_t datalen = 1 + converted_len; + + bech32_encoding enc = BECH32_ENCODING_BECH32M; + + char tmp[BECH32_BUF_SIZE]; + int ok = bech32_encode(tmp, hrp, data, datalen, enc); + if(!ok) { + mp_raise_ValueError(MP_ERROR_TEXT("bip352_encode")); + } + + return mp_obj_new_str(tmp, strlen(tmp)); +} +static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(c_bip352_encode_obj, 3, 4, c_bip352_encode); + + STATIC const mp_rom_map_elem_t globals_table[] = { { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_codecs) }, @@ -201,6 +266,8 @@ STATIC const mp_rom_map_elem_t globals_table[] = { { MP_ROM_QSTR(MP_QSTR_nip19_encode), MP_ROM_PTR(&c_nip19_encode_obj) }, { MP_ROM_QSTR(MP_QSTR_nip19_decode), MP_ROM_PTR(&c_nip19_decode_obj) }, + + { MP_ROM_QSTR(MP_QSTR_bip352_encode), MP_ROM_PTR(&c_bip352_encode_obj) }, }; STATIC MP_DEFINE_CONST_DICT(globals_table_obj, globals_table); diff --git a/ngu/k1.c b/ngu/k1.c index 73471a8..cf8c460 100644 --- a/ngu/k1.c +++ b/ngu/k1.c @@ -7,6 +7,7 @@ // - see test_k1.py // #include "py/runtime.h" +#include "py/objint.h" #include "py/objlist.h" // For list-related functions #include "random.h" #include @@ -648,6 +649,99 @@ STATIC mp_obj_t s_keypair_ecdh_multiply(mp_obj_t self_in, mp_obj_t other_point_i } STATIC MP_DEFINE_CONST_FUN_OBJ_2(s_keypair_ecdh_multiply_obj, s_keypair_ecdh_multiply); +// BIP-352 Silent Payments: Scalar multiplication +// returns scalar * pubkey, as 33-byte compressed bytes +static mp_obj_t s_ec_pubkey_tweak_mul(mp_obj_t pubkey_in, mp_obj_t scalar_in) { + sec_setup_ctx(); + + // Parse input pubkey + mp_buffer_info_t pubkey_buf; + mp_get_buffer_raise(pubkey_in, &pubkey_buf, MP_BUFFER_READ); + + secp256k1_pubkey pubkey; + int ok = secp256k1_ec_pubkey_parse(lib_ctx, &pubkey, pubkey_buf.buf, + pubkey_buf.len); + if (!ok) { + mp_raise_ValueError(MP_ERROR_TEXT("secp256k1_ec_pubkey_parse")); + } + + // Get scalar (32 bytes) + mp_buffer_info_t scalar; + mp_get_buffer_raise(scalar_in, &scalar, MP_BUFFER_READ); + if (scalar.len != 32) { + mp_raise_ValueError(MP_ERROR_TEXT("scalar len != 32")); + } + + // Multiply: result = scalar * pubkey + ok = secp256k1_ec_pubkey_tweak_mul(lib_ctx, &pubkey, scalar.buf); + if (!ok) { + mp_raise_ValueError(MP_ERROR_TEXT("secp256k1_ec_pubkey_tweak_mul")); + } + + // Serialize result as compressed pubkey (33 bytes) + uint8_t output[33]; + size_t outlen = sizeof(output); + secp256k1_ec_pubkey_serialize(lib_ctx, output, &outlen, &pubkey, + SECP256K1_EC_COMPRESSED); + + return mp_obj_new_bytes(output, outlen); +} +static MP_DEFINE_CONST_FUN_OBJ_2(s_ec_pubkey_tweak_mul_obj, s_ec_pubkey_tweak_mul); + +// BIP-352 Silent Payments: N-ary point addition +// returns the sum of a list of points, as 33-byte compressed bytes +static mp_obj_t s_ec_pubkey_combine(mp_obj_t pubkeys_in) { + sec_setup_ctx(); + + size_t n; + mp_obj_t *items; + mp_obj_get_array(pubkeys_in, &n, &items); + if (n == 0) { + mp_raise_ValueError(MP_ERROR_TEXT("Empty pubkeys list")); + } + + // fold pairwise to minimize memory use with large list + secp256k1_pubkey result; + bool result_inf = true; // running sum starts at the point at infinity + for (size_t i = 0; i < n; i++) { + mp_buffer_info_t buf; + mp_get_buffer_raise(items[i], &buf, MP_BUFFER_READ); + + secp256k1_pubkey next; + if (!secp256k1_ec_pubkey_parse(lib_ctx, &next, buf.buf, buf.len)) { + mp_raise_ValueError(MP_ERROR_TEXT("secp256k1_ec_pubkey_parse")); + } + + if (result_inf) { + result = next; // infinity + next == next + result_inf = false; + } else { + // output must not alias an input: combine() zeroes it before reading + secp256k1_pubkey sum; + const secp256k1_pubkey *pair[2] = { &result, &next }; + + // a 2-way combine fails only when the sum is the point at infinity; + // track that so only an infinite *total* is an error + if (secp256k1_ec_pubkey_combine(lib_ctx, &sum, pair, 2)) { + result = sum; + } else { + result_inf = true; + } + } + } + + if (result_inf) { + mp_raise_ValueError(MP_ERROR_TEXT("secp256k1_ec_pubkey_combine")); + } + + uint8_t output[33]; + size_t outlen = sizeof(output); + secp256k1_ec_pubkey_serialize(lib_ctx, output, &outlen, &result, + SECP256K1_EC_COMPRESSED); + + return mp_obj_new_bytes(output, outlen); +} +static MP_DEFINE_CONST_FUN_OBJ_1(s_ec_pubkey_combine_obj, s_ec_pubkey_combine); // MuSig2 @@ -1322,6 +1416,41 @@ STATIC const mp_obj_type_t s_keypair_type = { .locals_dict = (void *)&s_keypair_locals_dict, }; +// Generator point G (33-byte compressed) +static mp_obj_t s_generator(void) { + // secp256k1 generator point G in compressed format + // 0x0279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798 + static const uint8_t generator_bytes[33] = { + 0x02, 0x79, 0xbe, 0x66, 0x7e, 0xf9, 0xdc, 0xbb, + 0xac, 0x55, 0xa0, 0x62, 0x95, 0xce, 0x87, 0x0b, + 0x07, 0x02, 0x9b, 0xfc, 0xdb, 0x2d, 0xce, 0x28, + 0xd9, 0x59, 0xf2, 0x81, 0x5b, 0x16, 0xf8, 0x17, + 0x98 + }; + return mp_obj_new_bytes(generator_bytes, 33); +} +static MP_DEFINE_CONST_FUN_OBJ_0(s_generator_obj, s_generator); + +// Curve order n constant +// 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 +static const uint8_t secp256k1_order_bytes[32] = { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, + 0xba, 0xae, 0xdc, 0xe6, 0xaf, 0x48, 0xa0, 0x3b, + 0xbf, 0xd2, 0x5e, 0x8c, 0xd0, 0x36, 0x41, 0x41 +}; + +// Curve order n (32 bytes, big-endian) +static mp_obj_t s_curve_order(void) { + return mp_obj_new_bytes(secp256k1_order_bytes, 32); +} +static MP_DEFINE_CONST_FUN_OBJ_0(s_curve_order_obj, s_curve_order); + +// Curve order n as integer (avoids int.from_bytes conversion in Python) +static mp_obj_t s_curve_order_int(void) { + return mp_obj_int_from_bytes_impl(true, 32, secp256k1_order_bytes); +} +static MP_DEFINE_CONST_FUN_OBJ_0(s_curve_order_int_obj, s_curve_order_int); STATIC const mp_rom_map_elem_t globals_table[] = { { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_secp256k1) }, @@ -1347,6 +1476,12 @@ STATIC const mp_rom_map_elem_t globals_table[] = { { MP_ROM_QSTR(MP_QSTR_musig_nonce_process), MP_ROM_PTR(&s_musig_nonce_process_obj) }, { MP_ROM_QSTR(MP_QSTR_musig_partial_sign), MP_ROM_PTR(&s_musig_partial_sign_obj) }, { MP_ROM_QSTR(MP_QSTR_musig_partial_sig_agg), MP_ROM_PTR(&s_musig_partial_sig_agg_obj) }, + + { MP_ROM_QSTR(MP_QSTR_ec_pubkey_tweak_mul), MP_ROM_PTR(&s_ec_pubkey_tweak_mul_obj) }, + { MP_ROM_QSTR(MP_QSTR_ec_pubkey_combine), MP_ROM_PTR(&s_ec_pubkey_combine_obj) }, + { MP_ROM_QSTR(MP_QSTR_generator), MP_ROM_PTR(&s_generator_obj) }, + { MP_ROM_QSTR(MP_QSTR_curve_order), MP_ROM_PTR(&s_curve_order_obj) }, + { MP_ROM_QSTR(MP_QSTR_curve_order_int), MP_ROM_PTR(&s_curve_order_int_obj) }, }; STATIC MP_DEFINE_CONST_DICT(globals_table_obj, globals_table); diff --git a/ngu/ngu_tests/test_k1.py b/ngu/ngu_tests/test_k1.py index d72bc1a..b8a6506 100644 --- a/ngu/ngu_tests/test_k1.py +++ b/ngu/ngu_tests/test_k1.py @@ -454,4 +454,101 @@ except TypeError as e: assert str(e) == err, str(e) +# +# BIP-352 / BIP-374 point arithmetic +# +G = ngu.secp256k1.generator() +n_bytes = ngu.secp256k1.curve_order() +n_int = ngu.secp256k1.curve_order_int() + +def _mul_G(k): + # scalar * G, via the long-standing keypair path + return ngu.secp256k1.keypair((k % n_int).to_bytes(32, 'big')).pubkey().to_bytes() + +def _neg(p): + # negate a compressed point by flipping the y parity, as dleq.py does + q = bytearray(p) + q[0] = 0x03 if p[0] == 0x02 else 0x02 + return bytes(q) + +# constants +assert len(G) == 33 and G == _mul_G(1) +assert len(n_bytes) == 32 +assert n_int == int.from_bytes(n_bytes, 'big') +assert n_int == 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141 + +# scalar multiply agrees with the keypair path +for k in (1, 2, 0x1234, n_int - 1): + assert ngu.secp256k1.ec_pubkey_tweak_mul(G, k.to_bytes(32, 'big')) == _mul_G(k) + +# accepts uncompressed input, always returns compressed +two = (2).to_bytes(32, 'big') +A = _mul_G(7) +A_uncompressed = ngu.secp256k1.pubkey(A).to_bytes(True) +assert len(A_uncompressed) == 65 +assert ngu.secp256k1.ec_pubkey_tweak_mul(A_uncompressed, two) == _mul_G(14) +assert ngu.secp256k1.ec_pubkey_tweak_mul(A, two) == _mul_G(14) + +# point addition: (a*G) + (b*G) == (a+b mod n)*G +a, b, c = 3, 5, n_int - 4 +assert ngu.secp256k1.ec_pubkey_combine([_mul_G(a), _mul_G(b)]) == _mul_G(8) +assert ngu.secp256k1.ec_pubkey_combine([_mul_G(a), _mul_G(b), _mul_G(c)]) == _mul_G(4) + +# order independent, and a single element is a no-op +assert ngu.secp256k1.ec_pubkey_combine([_mul_G(c), _mul_G(a), _mul_G(b)]) == _mul_G(4) +assert ngu.secp256k1.ec_pubkey_combine([A]) == A + +# an intermediate point at infinity must not fail the total +P, Q = _mul_G(11), _mul_G(13) +assert ngu.secp256k1.ec_pubkey_combine([P, _neg(P), Q, Q, _neg(Q)]) == Q +assert ngu.secp256k1.ec_pubkey_combine([P, Q, _neg(P)]) == Q + +# long list stays correct and must not exhaust stack or heap +assert ngu.secp256k1.ec_pubkey_combine([_mul_G(i) for i in range(1, 51)]) == _mul_G(1275) + +try: + ngu.secp256k1.ec_pubkey_combine([]) + assert False +except ValueError as e: + assert "Empty pubkeys list" in str(e) + +try: + # total sum is the point at infinity + ngu.secp256k1.ec_pubkey_combine([P, _neg(P)]) + assert False +except ValueError as e: + assert "secp256k1_ec_pubkey_combine" in str(e) + +try: + ngu.secp256k1.ec_pubkey_combine([A, bytes(33)]) + assert False +except ValueError as e: + assert "secp256k1_ec_pubkey_parse" in str(e) + +try: + ngu.secp256k1.ec_pubkey_tweak_mul(bytes(33), two) + assert False +except ValueError as e: + assert "secp256k1_ec_pubkey_parse" in str(e) + +try: + ngu.secp256k1.ec_pubkey_tweak_mul(G, bytes(31)) + assert False +except ValueError as e: + assert "scalar len != 32" in str(e) + +try: + # scalar is zero + ngu.secp256k1.ec_pubkey_tweak_mul(G, bytes(32)) + assert False +except ValueError as e: + assert "secp256k1_ec_pubkey_tweak_mul" in str(e) + +try: + # scalar is the curve order + ngu.secp256k1.ec_pubkey_tweak_mul(G, n_bytes) + assert False +except ValueError as e: + assert "secp256k1_ec_pubkey_tweak_mul" in str(e) + print("PASS - test_k1")