From 7774c314fb3c342eb5d48015b1c1b8b66d3d87db Mon Sep 17 00:00:00 2001 From: Sebastian Falbesoner Date: Thu, 9 May 2024 18:52:27 +0200 Subject: [PATCH 1/4] test: refactor: return TaprootInfo from P2TR address creation routine Rather than only returning the internal key from the P2TR anyone-can-spend address creation routine, provide the whole TaprootInfo object, which in turn contains a dictionary of TaprootLeafInfo object for named leaves. This data is used in MiniWallet for the default ADDRESS_OP_TRUE mode, in order to deduplicate the witness script and leaf version of the control block. --- test/functional/test_framework/address.py | 7 ++++--- test/functional/test_framework/wallet.py | 10 +++++++--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/test/functional/test_framework/address.py b/test/functional/test_framework/address.py index bcb38b21cd..968069ff18 100644 --- a/test/functional/test_framework/address.py +++ b/test/functional/test_framework/address.py @@ -53,13 +53,14 @@ def create_deterministic_address_bcrt1_p2tr_op_true(explicit_internal_key=None): can be spent with a witness stack of OP_TRUE and the control block with internal public key (script-path spending). - Returns a tuple with the generated address and the internal key. + Returns a tuple with the generated address and the TaprootInfo object. """ internal_key = explicit_internal_key or (1).to_bytes(32, 'big') - address = output_key_to_p2tr(taproot_construct(internal_key, [(None, CScript([OP_TRUE]))]).output_pubkey) + taproot_info = taproot_construct(internal_key, [("only-path", CScript([OP_TRUE]))]) + address = output_key_to_p2tr(taproot_info.output_pubkey) if explicit_internal_key is None: assert_equal(address, 'bcrt1p9yfmy5h72durp7zrhlw9lf7jpwjgvwdg0jr0lqmmjtgg83266lqsekaqka') - return (address, internal_key) + return (address, taproot_info) def byte_to_base58(b, version): diff --git a/test/functional/test_framework/wallet.py b/test/functional/test_framework/wallet.py index 7d4f4a3392..82553c6fc4 100644 --- a/test/functional/test_framework/wallet.py +++ b/test/functional/test_framework/wallet.py @@ -39,7 +39,6 @@ from test_framework.messages import ( ) from test_framework.script import ( CScript, - LEAF_VERSION_TAPSCRIPT, OP_1, OP_NOP, OP_RETURN, @@ -107,7 +106,7 @@ class MiniWallet: self._scriptPubKey = key_to_p2pk_script(pub_key.get_bytes()) elif mode == MiniWalletMode.ADDRESS_OP_TRUE: internal_key = None if tag_name is None else hash256(tag_name.encode()) - self._address, self._internal_key = create_deterministic_address_bcrt1_p2tr_op_true(internal_key) + self._address, self._taproot_info = create_deterministic_address_bcrt1_p2tr_op_true(internal_key) self._scriptPubKey = address_to_scriptpubkey(self._address) # When the pre-mined test framework chain is used, it contains coinbase @@ -195,7 +194,12 @@ class MiniWallet: elif self._mode == MiniWalletMode.ADDRESS_OP_TRUE: tx.wit.vtxinwit = [CTxInWitness()] * len(tx.vin) for i in tx.wit.vtxinwit: - i.scriptWitness.stack = [CScript([OP_TRUE]), bytes([LEAF_VERSION_TAPSCRIPT]) + self._internal_key] + assert_equal(len(self._taproot_info.leaves), 1) + leaf_info = list(self._taproot_info.leaves.values())[0] + i.scriptWitness.stack = [ + leaf_info.script, + bytes([leaf_info.version]) + self._taproot_info.internal_pubkey, + ] else: assert False From c9f7364ab2bccad56a4473dbd18d9c80eaf651d4 Mon Sep 17 00:00:00 2001 From: Sebastian Falbesoner Date: Thu, 9 May 2024 19:17:57 +0200 Subject: [PATCH 2/4] test: fix MiniWallet script-path spend (missing parity bit in leaf version) This commit fixes a dormant bug in MiniWallet that exists since support for P2TR was initially added in #23371 (see commit 041abfebe49ae5e3e882c00cc5caea1365a27a49). In the course of spending the output, the leaf version byte of the control block in the witness stack doesn't set the parity bit, i.e. we were so far just lucky that the used combinations of relevant data (internal pubkey, leaf script / version) didn't result in a tweaked pubkey with odd y-parity. If that was the case, we'd get the following validation error: `mandatory-script-verify-flag-failed (Witness program hash mismatch) (-26)` Since MiniWallets can now optionally be tagged (#29939), resulting in different internal pubkeys, the issue is more prevalent now. Fix it by passing the parity bit, as specified in BIP341. --- test/functional/test_framework/wallet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/functional/test_framework/wallet.py b/test/functional/test_framework/wallet.py index 82553c6fc4..e816f6fedd 100644 --- a/test/functional/test_framework/wallet.py +++ b/test/functional/test_framework/wallet.py @@ -198,7 +198,7 @@ class MiniWallet: leaf_info = list(self._taproot_info.leaves.values())[0] i.scriptWitness.stack = [ leaf_info.script, - bytes([leaf_info.version]) + self._taproot_info.internal_pubkey, + bytes([leaf_info.version | self._taproot_info.negflag]) + self._taproot_info.internal_pubkey, ] else: assert False From 3162c917e93fde4bea6e4627bb0c3c7cdc37386c Mon Sep 17 00:00:00 2001 From: Sebastian Falbesoner Date: Tue, 11 Jun 2024 02:13:18 +0200 Subject: [PATCH 3/4] test: fix MiniWallet internal key derivation for tagged instances Not every pseudorandom hash result is a valid x-only public key, so the pubkey tweaking in the course of creating the output public key would fail about every second time. Fix this by treating the hash result as private key and calculate the x-only public key out of that, to be used then as internal key. --- test/functional/test_framework/wallet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/functional/test_framework/wallet.py b/test/functional/test_framework/wallet.py index e816f6fedd..0d37b7ffb5 100644 --- a/test/functional/test_framework/wallet.py +++ b/test/functional/test_framework/wallet.py @@ -105,7 +105,7 @@ class MiniWallet: pub_key = self._priv_key.get_pubkey() self._scriptPubKey = key_to_p2pk_script(pub_key.get_bytes()) elif mode == MiniWalletMode.ADDRESS_OP_TRUE: - internal_key = None if tag_name is None else hash256(tag_name.encode()) + internal_key = None if tag_name is None else compute_xonly_pubkey(hash256(tag_name.encode()))[0] self._address, self._taproot_info = create_deterministic_address_bcrt1_p2tr_op_true(internal_key) self._scriptPubKey = address_to_scriptpubkey(self._address) From e4b0dabb2115dc74e9c5794ddca3822cd8301c72 Mon Sep 17 00:00:00 2001 From: Sebastian Falbesoner Date: Tue, 11 Jun 2024 02:19:54 +0200 Subject: [PATCH 4/4] test: add functional test for tagged MiniWallet instances --- .../functional/feature_framework_miniwallet.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/test/functional/feature_framework_miniwallet.py b/test/functional/feature_framework_miniwallet.py index f108289018..55c837a1b7 100755 --- a/test/functional/feature_framework_miniwallet.py +++ b/test/functional/feature_framework_miniwallet.py @@ -3,6 +3,9 @@ # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test MiniWallet.""" +import random +import string + from test_framework.blocktools import COINBASE_MATURITY from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( @@ -31,6 +34,20 @@ class FeatureFrameworkMiniWalletTest(BitcoinTestFramework): assert_greater_than_or_equal(tx.get_weight(), target_weight) assert_greater_than_or_equal(target_weight + 3, tx.get_weight()) + def test_wallet_tagging(self): + """Verify that tagged wallet instances are able to send funds.""" + self.log.info(f"Test tagged wallet instances...") + node = self.nodes[0] + untagged_wallet = self.wallets[0][1] + for i in range(10): + tag = ''.join(random.choice(string.ascii_letters) for _ in range(20)) + self.log.debug(f"-> ({i}) tag name: {tag}") + tagged_wallet = MiniWallet(node, tag_name=tag) + untagged_wallet.send_to(from_node=node, scriptPubKey=tagged_wallet.get_scriptPubKey(), amount=100000) + tagged_wallet.rescan_utxos() + tagged_wallet.send_self_transfer(from_node=node) + self.generate(node, 1) # clear mempool + def run_test(self): node = self.nodes[0] self.wallets = [ @@ -43,6 +60,7 @@ class FeatureFrameworkMiniWalletTest(BitcoinTestFramework): self.generate(wallet, COINBASE_MATURITY) self.test_tx_padding() + self.test_wallet_tagging() if __name__ == '__main__':