Skip to content

Commit

Permalink
taproot: implement p2tr scriptpubkey generation
Browse files Browse the repository at this point in the history
  • Loading branch information
jgriffiths committed Dec 12, 2024
1 parent 5f07455 commit 03d92c6
Show file tree
Hide file tree
Showing 10 changed files with 112 additions and 9 deletions.
6 changes: 6 additions & 0 deletions include/wally.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -1671,6 +1671,12 @@ inline int scriptpubkey_p2sh_from_bytes(const BYTES& bytes, uint32_t flags, BYTE
return detail::check_ret(__FUNCTION__, ret);
}

template <class BYTES, class BYTES_OUT>
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 <class SCRIPTPUBKEY>
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);
Expand Down
25 changes: 23 additions & 2 deletions include/wally_script.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
39 changes: 39 additions & 0 deletions src/script.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
1 change: 1 addition & 0 deletions src/swig_java/swig.i
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions src/swig_python/python_extra.py_in
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
45 changes: 38 additions & 7 deletions src/test/test_script.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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))

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/test/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]),
Expand Down
1 change: 1 addition & 0 deletions src/wasm_package/src/functions.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/wasm_package/src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions tools/wasm_exports.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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' \
Expand Down

0 comments on commit 03d92c6

Please sign in to comment.