Skip to content

Commit 5d413c8

Browse files
committed
Add feature_taproot case involved invalid internal pubkey
1 parent 1c5c951 commit 5d413c8

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)