Skip to content

Commit 38d06e1

Browse files
committed
Merge bitcoin/bitcoin#26383: test: Add feature_taproot case involving invalid internal pubkey
5d413c8 Add feature_taproot case involved invalid internal pubkey (Pieter Wuille) Pull request description: Add a test case to feature_taproot which involves an output that is (incorrectly) constructed, using an invalid internal public key and valid script tree. It is designed to detect cases where the script path spending validation logic does not detect this case, and instead treats the internal public key as the point at infinity. Equivalent unit test case added in bitcoin-core/qa-assets#98. ACKs for top commit: instagibbs: ACK 5d413c8 aureleoules: reACK 5d413c8 Tree-SHA512: dfa014e383cd2743f3c9a996e1f2a2fceb9e244edf4b05dc0c110c4ba32a87684482222907805a4ca998aebcb42a197bb3e7967bfb5f0554fe9f1e5aa5463603
2 parents 85892f7 + 5d413c8 commit 38d06e1

File tree

2 files changed

+52
-4
lines changed

2 files changed

+52
-4
lines changed

test/functional/feature_taproot.py

+46-1
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,14 @@
9696
assert_equal,
9797
random_bytes,
9898
)
99-
from test_framework.key import generate_privkey, compute_xonly_pubkey, sign_schnorr, tweak_add_privkey, ECKey
99+
from test_framework.key import (
100+
generate_privkey,
101+
compute_xonly_pubkey,
102+
sign_schnorr,
103+
tweak_add_privkey,
104+
ECKey,
105+
SECP256K1
106+
)
100107
from test_framework.address import (
101108
hash160,
102109
program_to_witness,
@@ -661,6 +668,44 @@ def spenders_taproot_active():
661668
# Test with signature with bit flipped.
662669
add_spender(spenders, "sig/bitflip", tap=tap, key=secs[0], failure={"signature": bitflipper(default_signature)}, **ERR_SIG_SCHNORR)
663670

671+
# == Test involving an internal public key not on the curve ==
672+
673+
# X-only public keys are 32 bytes, but not every 32-byte array is a valid public key; only
674+
# around 50% of them are. This does not affect users using correct software; these "keys" have
675+
# no corresponding private key, and thus will never appear as output of key
676+
# generation/derivation/tweaking.
677+
#
678+
# Using an invalid public key as P2TR output key makes the UTXO unspendable. Revealing an
679+
# invalid public key as internal key in a P2TR script path spend also makes the spend invalid.
680+
# These conditions are explicitly spelled out in BIP341.
681+
#
682+
# It is however hard to create test vectors for this, because it involves "guessing" how a
683+
# hypothetical incorrect implementation deals with an obviously-invalid condition, and making
684+
# sure that guessed behavior (accepting it in certain condition) doesn't occur.
685+
#
686+
# The test case added here tries to detect a very specific bug a verifier could have: if they
687+
# don't verify whether or not a revealed internal public key in a script path spend is valid,
688+
# and (correctly) implement output_key == tweak(internal_key, tweakval) but (incorrectly) treat
689+
# tweak(invalid_key, tweakval) as equal the public key corresponding to private key tweakval.
690+
# This may seem like a far-fetched edge condition to test for, but in fact, the BIP341 wallet
691+
# pseudocode did exactly that (but obviously only triggerable by someone invoking the tweaking
692+
# function with an invalid public key, which shouldn't happen).
693+
694+
# Generate an invalid public key
695+
while True:
696+
invalid_pub = random_bytes(32)
697+
if not SECP256K1.is_x_coord(int.from_bytes(invalid_pub, 'big')):
698+
break
699+
700+
# Implement a test case that detects validation logic which maps invalid public keys to the
701+
# point at infinity in the tweaking logic.
702+
tap = taproot_construct(invalid_pub, [("true", CScript([OP_1]))], treat_internal_as_infinity=True)
703+
add_spender(spenders, "output/invalid_x", tap=tap, key_tweaked=tap.tweak, failure={"leaf": "true", "inputs": []}, **ERR_WITNESS_PROGRAM_MISMATCH)
704+
705+
# Do the same thing without invalid point, to make sure there is no mistake in the test logic.
706+
tap = taproot_construct(pubs[0], [("true", CScript([OP_1]))])
707+
add_spender(spenders, "output/invalid_x_mock", tap=tap, key=secs[0], leaf="true", inputs=[])
708+
664709
# == Tests for signature hashing ==
665710

666711
# Run all tests once with no annex, and once with a valid random annex.

test/functional/test_framework/script.py

+6-3
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import unittest
1313
from typing import List, Dict
1414

15-
from .key import TaggedHash, tweak_add_pubkey
15+
from .key import TaggedHash, tweak_add_pubkey, compute_xonly_pubkey
1616

1717
from .messages import (
1818
CTransaction,
@@ -872,7 +872,7 @@ def taproot_tree_helper(scripts):
872872
# - merklebranch: the merkle branch to use for this leaf (32*N bytes)
873873
TaprootLeafInfo = namedtuple("TaprootLeafInfo", "script,version,merklebranch,leaf_hash")
874874

875-
def taproot_construct(pubkey, scripts=None):
875+
def taproot_construct(pubkey, scripts=None, treat_internal_as_infinity=False):
876876
"""Construct a tree of Taproot spending conditions
877877
878878
pubkey: a 32-byte xonly pubkey for the internal pubkey (bytes)
@@ -891,7 +891,10 @@ def taproot_construct(pubkey, scripts=None):
891891

892892
ret, h = taproot_tree_helper(scripts)
893893
tweak = TaggedHash("TapTweak", pubkey + h)
894-
tweaked, negated = tweak_add_pubkey(pubkey, tweak)
894+
if treat_internal_as_infinity:
895+
tweaked, negated = compute_xonly_pubkey(tweak)
896+
else:
897+
tweaked, negated = tweak_add_pubkey(pubkey, tweak)
895898
leaves = dict((name, TaprootLeafInfo(script, version, merklebranch, leaf)) for name, version, script, merklebranch, leaf in ret)
896899
return TaprootInfo(CScript([OP_1, tweaked]), pubkey, negated + 0, tweak, leaves, h, tweaked)
897900

0 commit comments

Comments
 (0)