From 03d92c6a33e98bf563c21b1be140a1e18a69a3d4 Mon Sep 17 00:00:00 2001 From: Jon Griffiths Date: Thu, 12 Dec 2024 14:37:42 +1300 Subject: [PATCH] taproot: implement p2tr scriptpubkey generation --- include/wally.hpp | 6 ++++ include/wally_script.h | 25 +++++++++++++++-- src/script.c | 39 ++++++++++++++++++++++++++ src/swig_java/swig.i | 1 + src/swig_python/python_extra.py_in | 1 + src/test/test_script.py | 45 +++++++++++++++++++++++++----- src/test/util.py | 1 + src/wasm_package/src/functions.js | 1 + src/wasm_package/src/index.d.ts | 1 + tools/wasm_exports.sh | 1 + 10 files changed, 112 insertions(+), 9 deletions(-) diff --git a/include/wally.hpp b/include/wally.hpp index b4dab840c..fbfe4b0ce 100644 --- a/include/wally.hpp +++ b/include/wally.hpp @@ -1671,6 +1671,12 @@ inline int scriptpubkey_p2sh_from_bytes(const BYTES& bytes, uint32_t flags, BYTE return detail::check_ret(__FUNCTION__, ret); } +template +inline int scriptpubkey_p2tr_from_bytes(const BYTES& bytes, uint32_t flags, BYTES_OUT& bytes_out, size_t* written) { + int ret = ::wally_scriptpubkey_p2tr_from_bytes(bytes.data(), bytes.size(), flags, bytes_out.data(), bytes_out.size(), written); + return detail::check_ret(__FUNCTION__, ret); +} + template inline int scriptpubkey_to_address(const SCRIPTPUBKEY& scriptpubkey, uint32_t network, char** output) { int ret = ::wally_scriptpubkey_to_address(scriptpubkey.data(), scriptpubkey.size(), network, output); diff --git a/include/wally_script.h b/include/wally_script.h index 9f91b4b33..e8805fcb5 100644 --- a/include/wally_script.h +++ b/include/wally_script.h @@ -296,8 +296,29 @@ WALLY_CORE_API int wally_witness_p2wpkh_from_der( struct wally_tx_witness_stack **witness); /** - * Create a P2TR keyspend witness from a BIP340 signature plus - * optional sighash. + * Create a P2TR scriptPubkey from a compressed or x-only public key. + * + * :param bytes: Compressed or x-only public key to create a scriptPubkey for. + * :param bytes_len: The length of ``bytes`` in bytes. Must be ``EC_PUBLIC_KEY_LEN`` + *| or ``EC_XONLY_PUBLIC_KEY_LEN``. + * :param flags: Must be 0. + * :param bytes_out: Destination for the resulting scriptPubkey. + * MAX_SIZED_OUTPUT(len, bytes_out, WALLY_SCRIPTPUBKEY_P2TR_LEN) + * :param written: Destination for the number of bytes written to ``bytes_out``. + * + * .. note:: Compressed pubkeys are tweaked according to BIP341. X-only + *| pubkeys are assumed to already be tweaked, and are used as-is. + */ +WALLY_CORE_API int wally_scriptpubkey_p2tr_from_bytes( + const unsigned char *bytes, + size_t bytes_len, + uint32_t flags, + unsigned char *bytes_out, + size_t len, + size_t *written); + +/** + * Create a P2TR keyspend witness from a BIP340 signature plus optional sighash. * * :param sig: The BIP340-encoded keyspend signature, including a sighash byte *| for non `WALLY_SIGHASH_DEFAULT` sighashes. diff --git a/src/script.c b/src/script.c index 8811942ea..308580f83 100644 --- a/src/script.c +++ b/src/script.c @@ -1295,6 +1295,45 @@ int wally_witness_p2wpkh_from_sig( return ret; } +int wally_scriptpubkey_p2tr_from_bytes(const unsigned char *bytes, size_t bytes_len, + uint32_t flags, + unsigned char *bytes_out, size_t len, + size_t *written) +{ + unsigned char tweaked[EC_PUBLIC_KEY_LEN]; + + if (written) + *written = 0; + + /* FIXME: Support EC_FLAG_ELEMENTS for Elements P2TR */ + if (!bytes || flags || !bytes_out || !written) + return WALLY_EINVAL; + + if (len < WALLY_SCRIPTPUBKEY_P2TR_LEN) { + /* Tell the caller their buffer is too short */ + *written = WALLY_SCRIPTPUBKEY_P2TR_LEN; + return WALLY_OK; + } + + if (bytes_len == EC_PUBLIC_KEY_LEN) { + /* An untweaked public key, tweak it */ + int ret = wally_ec_public_key_bip341_tweak(bytes, bytes_len, NULL, 0, + 0, tweaked, sizeof(tweaked)); + if (ret != WALLY_OK) + return ret; + bytes = tweaked + 1; /* Convert to x-only */ + bytes_len = EC_XONLY_PUBLIC_KEY_LEN; + } + if (bytes_len != EC_XONLY_PUBLIC_KEY_LEN) + return WALLY_EINVAL; /* Not an x-only public key */ + + bytes_out[0] = OP_1; + bytes_out[1] = bytes_len; + memcpy(bytes_out + 2, bytes, bytes_len); + *written = WALLY_SCRIPTPUBKEY_P2TR_LEN; + return WALLY_OK; +} + int wally_witness_p2tr_from_sig(const unsigned char *sig, size_t sig_len, struct wally_tx_witness_stack **witness) { diff --git a/src/swig_java/swig.i b/src/swig_java/swig.i index bfb4f757e..37d6aca82 100644 --- a/src/swig_java/swig.i +++ b/src/swig_java/swig.i @@ -950,6 +950,7 @@ static jobjectArray create_jstringArray(JNIEnv *jenv, char **p, size_t len) { %returns_size_t(wally_scriptpubkey_op_return_from_bytes); %returns_size_t(wally_scriptpubkey_p2pkh_from_bytes); %returns_size_t(wally_scriptpubkey_p2sh_from_bytes); +%returns_size_t(wally_scriptpubkey_p2tr_from_bytes); %returns_size_t(wally_scriptpubkey_multisig_from_bytes); %returns_size_t(wally_scriptsig_p2pkh_from_sig); %returns_size_t(wally_scriptsig_p2pkh_from_der); diff --git a/src/swig_python/python_extra.py_in b/src/swig_python/python_extra.py_in index fdfc5c094..2be4d700a 100644 --- a/src/swig_python/python_extra.py_in +++ b/src/swig_python/python_extra.py_in @@ -216,6 +216,7 @@ scriptpubkey_multisig_from_bytes = _wrap_bin(scriptpubkey_multisig_from_bytes, s scriptpubkey_op_return_from_bytes = _wrap_bin(scriptpubkey_op_return_from_bytes, WALLY_SCRIPTPUBKEY_OP_RETURN_MAX_LEN, resize=True) scriptpubkey_p2pkh_from_bytes = _wrap_bin(scriptpubkey_p2pkh_from_bytes, WALLY_SCRIPTPUBKEY_P2PKH_LEN, resize=True) scriptpubkey_p2sh_from_bytes = _wrap_bin(scriptpubkey_p2sh_from_bytes, WALLY_SCRIPTPUBKEY_P2SH_LEN, resize=True) +scriptpubkey_p2tr_from_bytes = _wrap_bin(scriptpubkey_p2tr_from_bytes, WALLY_SCRIPTPUBKEY_P2TR_LEN, resize=True) scriptsig_multisig_from_bytes = _wrap_bin(scriptsig_multisig_from_bytes, scriptsig_multisig_from_bytes_len, resize=True) scriptsig_p2pkh_from_der = _wrap_bin(scriptsig_p2pkh_from_der, WALLY_SCRIPTSIG_P2PKH_MAX_LEN, resize=True) scriptsig_p2pkh_from_sig = _wrap_bin(scriptsig_p2pkh_from_sig, WALLY_SCRIPTSIG_P2PKH_MAX_LEN, resize=True) diff --git a/src/test/test_script.py b/src/test/test_script.py index c24f36d8c..89cefd5b7 100755 --- a/src/test/test_script.py +++ b/src/test/test_script.py @@ -22,11 +22,13 @@ SCRIPTPUBKEY_OP_RETURN_MAX_LEN = 83 SCRIPTPUBKEY_P2PKH_LEN = 25 SCRIPTPUBKEY_P2SH_LEN = 23 +SCRIPTPUBKEY_P2TR_LEN = 34 HASH160_LEN = 20 SCRIPTSIG_P2PKH_MAX_LEN = 140 -PK, PK_LEN = make_cbuffer('11' * 33) # Fake compressed pubkey +PK, PK_LEN = make_cbuffer('02' * 33) # Fake compressed pubkey PKU, PKU_LEN = make_cbuffer('11' * 65) # Fake uncompressed pubkey +PKX, PKX_LEN = make_cbuffer('02' * 32) # Fake x-only pubkey SH, SH_LEN = make_cbuffer('11' * 20) # Fake script hash MPK_2, MPK_2_LEN = make_cbuffer('11' * 33 * 2) # Fake multiple (2) pubkeys MPK_3, MPK_3_LEN = make_cbuffer('11' * 33 * 3) # Fake multiple (3) pubkeys @@ -124,15 +126,14 @@ def test_scriptpubkey_p2pkh_from_bytes(self): # Valid cases valid_args = [ - [(PK, PK_LEN, SCRIPT_HASH160, out, out_len),'76a9148ec4cf3ee160b054e0abb6f5c8177b9ee56fa51e88ac'], + [(PK, PK_LEN, SCRIPT_HASH160, out, out_len),'76a91451814f108670aced2d77c1805ddd6634bc9d473188ac'], [(PKU, PKU_LEN, SCRIPT_HASH160, out, out_len),'76a914e723a0f62396b8b03dbd9e48e9b9efe2eb704aab88ac'], [(PKU, HASH160_LEN, 0, out, out_len),'76a914111111111111111111111111111111111111111188ac'], ] for args, exp_script in valid_args: ret = wally_scriptpubkey_p2pkh_from_bytes(*args) self.assertEqual(ret, (WALLY_OK, SCRIPTPUBKEY_P2PKH_LEN)) - exp_script, _ = make_cbuffer(exp_script) - self.assertEqual(args[3], exp_script) + self.assertEqual(h(args[3]), utf8(exp_script)) ret = wally_scriptpubkey_get_type(out, SCRIPTPUBKEY_P2PKH_LEN) self.assertEqual(ret, (WALLY_OK, SCRIPT_TYPE_P2PKH)) @@ -173,6 +174,37 @@ def test_scriptpubkey_p2sh_from_bytes(self): ret = wally_scriptpubkey_get_type(out, SCRIPTPUBKEY_P2SH_LEN) self.assertEqual(ret, (WALLY_OK, SCRIPT_TYPE_P2SH)) + def test_scriptpubkey_p2tr_from_bytes(self): + """Tests for creating p2tr scriptPubKeys""" + # Invalid args + out, out_len = make_cbuffer('00' * SCRIPTPUBKEY_P2TR_LEN) + invalid_args = [ + (None, PK_LEN, 0, out, out_len), # Null bytes + (PK, 0, 0, out, out_len), # Empty bytes + (PK, PK_LEN, 0x8, out, out_len), # Unsupported flags + (PK, PK_LEN+1, 0, out, out_len), # Invalid pubkey len + (PK, PK_LEN, 0, None, out_len), # Null output + (PK, PK_LEN, 0, out, out_len-1), # Short output len + ] + for args in invalid_args: + ret = wally_scriptpubkey_p2tr_from_bytes(*args) + if ret == (WALLY_OK, SCRIPTPUBKEY_P2TR_LEN): + self.assertTrue(args[-1] < out_len) + else: + self.assertEqual(ret, (WALLY_EINVAL, 0)) + + # Valid cases + valid_args = [ + [(PK, PK_LEN, 0, out, out_len), '51203b6ec3adc4917224b2da531904a1d12c2ad47cabaa88fa54adc55aa2d7d29571'], + [(PKX, PKX_LEN, 0, out, out_len), '51200202020202020202020202020202020202020202020202020202020202020202'], + ] + for args, exp_script in valid_args: + ret = wally_scriptpubkey_p2tr_from_bytes(*args) + self.assertEqual(ret, (WALLY_OK, SCRIPTPUBKEY_P2TR_LEN)) + self.assertEqual(h(args[3]), utf8(exp_script)) + ret = wally_scriptpubkey_get_type(out, SCRIPTPUBKEY_P2TR_LEN) + self.assertEqual(ret, (WALLY_OK, SCRIPT_TYPE_P2TR)) + def test_scriptpubkey_multisig_from_bytes(self): """Tests for creating multisig scriptPubKeys""" # Invalid args @@ -303,14 +335,13 @@ def test_scriptsig_p2pkh(self): # Valid cases valid_args = [ - [(PK, PK_LEN, SIG_DER, SIG_DER_LEN, out, out_len), '4730450220'+'11'*32+'0220'+'11'*32+'0121'+'11'*33], + [(PK, PK_LEN, SIG_DER, SIG_DER_LEN, out, out_len), '4730450220'+'11'*32+'0220'+'11'*32+'0121'+'02'*33], [(PKU, PKU_LEN, SIG_DER, SIG_DER_LEN, out, out_len), '4730450220'+'11'*32+'0220'+'11'*32+'0141'+'11'*65], ] for args, exp_script in valid_args: ret = wally_scriptsig_p2pkh_from_der(*args) self.assertEqual(ret, (WALLY_OK, args[1] + args[3] + 2)) - exp_script, _ = make_cbuffer(exp_script) - self.assertEqual(args[4][:(args[1] + args[3] + 2)], exp_script) + self.assertEqual(h(args[4][:(args[1] + args[3] + 2)]), utf8(exp_script)) # From sig # Invalid args diff --git a/src/test/util.py b/src/test/util.py index 465570a5a..c2f709d26 100755 --- a/src/test/util.py +++ b/src/test/util.py @@ -632,6 +632,7 @@ class wally_psbt(Structure): ('wally_scriptpubkey_op_return_from_bytes', c_int, [c_void_p, c_size_t, c_uint32, c_void_p, c_size_t, c_size_t_p]), ('wally_scriptpubkey_p2pkh_from_bytes', c_int, [c_void_p, c_size_t, c_uint32, c_void_p, c_size_t, c_size_t_p]), ('wally_scriptpubkey_p2sh_from_bytes', c_int, [c_void_p, c_size_t, c_uint32, c_void_p, c_size_t, c_size_t_p]), + ('wally_scriptpubkey_p2tr_from_bytes', c_int, [c_void_p, c_size_t, c_uint32, c_void_p, c_size_t, c_size_t_p]), ('wally_scriptpubkey_to_address', c_int, [c_void_p, c_size_t, c_uint32, c_char_p_p]), ('wally_scriptsig_multisig_from_bytes', c_int, [c_void_p, c_size_t, c_void_p, c_size_t, POINTER(c_uint32), c_size_t, c_uint32, c_void_p, c_size_t, c_size_t_p]), ('wally_scriptsig_p2pkh_from_der', c_int, [c_void_p, c_size_t, c_void_p, c_size_t, c_void_p, c_size_t, c_size_t_p]), diff --git a/src/wasm_package/src/functions.js b/src/wasm_package/src/functions.js index 00735dc85..692b43a62 100644 --- a/src/wasm_package/src/functions.js +++ b/src/wasm_package/src/functions.js @@ -602,6 +602,7 @@ export const scriptpubkey_multisig_from_bytes = wrap('wally_scriptpubkey_multisi export const scriptpubkey_op_return_from_bytes = wrap('wally_scriptpubkey_op_return_from_bytes', [T.Bytes, T.Int32, T.DestPtrVarLen(T.Bytes, C.WALLY_SCRIPTPUBKEY_OP_RETURN_MAX_LEN, true)]); export const scriptpubkey_p2pkh_from_bytes = wrap('wally_scriptpubkey_p2pkh_from_bytes', [T.Bytes, T.Int32, T.DestPtrVarLen(T.Bytes, C.WALLY_SCRIPTPUBKEY_P2PKH_LEN, true)]); export const scriptpubkey_p2sh_from_bytes = wrap('wally_scriptpubkey_p2sh_from_bytes', [T.Bytes, T.Int32, T.DestPtrVarLen(T.Bytes, C.WALLY_SCRIPTPUBKEY_P2SH_LEN, true)]); +export const scriptpubkey_p2tr_from_bytes = wrap('wally_scriptpubkey_p2tr_from_bytes', [T.Bytes, T.Int32, T.DestPtrVarLen(T.Bytes, C.WALLY_SCRIPTPUBKEY_P2TR_LEN, true)]); export const scriptpubkey_to_address = wrap('wally_scriptpubkey_to_address', [T.Bytes, T.Int32, T.DestPtrPtr(T.String)]); export const scriptsig_multisig_from_bytes = wrap('wally_scriptsig_multisig_from_bytes', [T.Bytes, T.Bytes, T.Uint32Array, T.Int32, T.DestPtrVarLen(T.Bytes, scriptsig_multisig_from_bytes_len, true)]); export const scriptsig_p2pkh_from_der = wrap('wally_scriptsig_p2pkh_from_der', [T.Bytes, T.Bytes, T.DestPtrVarLen(T.Bytes, C.WALLY_SCRIPTSIG_P2PKH_MAX_LEN, true)]); diff --git a/src/wasm_package/src/index.d.ts b/src/wasm_package/src/index.d.ts index 4a6d85824..707338a53 100644 --- a/src/wasm_package/src/index.d.ts +++ b/src/wasm_package/src/index.d.ts @@ -562,6 +562,7 @@ export function scriptpubkey_multisig_from_bytes(bytes: Buffer|Uint8Array, thres export function scriptpubkey_op_return_from_bytes(bytes: Buffer|Uint8Array, flags: number): Buffer; export function scriptpubkey_p2pkh_from_bytes(bytes: Buffer|Uint8Array, flags: number): Buffer; export function scriptpubkey_p2sh_from_bytes(bytes: Buffer|Uint8Array, flags: number): Buffer; +export function scriptpubkey_p2tr_from_bytes(bytes: Buffer|Uint8Array, flags: number): Buffer; export function scriptpubkey_to_address(scriptpubkey: Buffer|Uint8Array, network: number): string; export function scriptsig_multisig_from_bytes(script: Buffer|Uint8Array, bytes: Buffer|Uint8Array, sighash: Uint32Array|number[], flags: number): Buffer; export function scriptsig_p2pkh_from_der(pub_key: Buffer|Uint8Array, sig: Buffer|Uint8Array): Buffer; diff --git a/tools/wasm_exports.sh b/tools/wasm_exports.sh index 5fdc21cad..d2a91c79f 100644 --- a/tools/wasm_exports.sh +++ b/tools/wasm_exports.sh @@ -361,6 +361,7 @@ EXPORTED_FUNCTIONS="['_malloc','_free','_bip32_key_free' \ ,'_wally_scriptpubkey_op_return_from_bytes' \ ,'_wally_scriptpubkey_p2pkh_from_bytes' \ ,'_wally_scriptpubkey_p2sh_from_bytes' \ +,'_wally_scriptpubkey_p2tr_from_bytes' \ ,'_wally_scriptpubkey_to_address' \ ,'_wally_scriptsig_multisig_from_bytes' \ ,'_wally_scriptsig_p2pkh_from_der' \