Skip to content

Commit

Permalink
Merge bitcoin#26383: test: Add feature_taproot case involving invalid…
Browse files Browse the repository at this point in the history
… 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
  • Loading branch information
fanquake committed Nov 22, 2022
2 parents 85892f7 + 5d413c8 commit 38d06e1
Show file tree
Hide file tree
Showing 2 changed files with 52 additions and 4 deletions.
47 changes: 46 additions & 1 deletion test/functional/feature_taproot.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,14 @@
assert_equal,
random_bytes,
)
from test_framework.key import generate_privkey, compute_xonly_pubkey, sign_schnorr, tweak_add_privkey, ECKey
from test_framework.key import (
generate_privkey,
compute_xonly_pubkey,
sign_schnorr,
tweak_add_privkey,
ECKey,
SECP256K1
)
from test_framework.address import (
hash160,
program_to_witness,
Expand Down Expand Up @@ -661,6 +668,44 @@ def spenders_taproot_active():
# Test with signature with bit flipped.
add_spender(spenders, "sig/bitflip", tap=tap, key=secs[0], failure={"signature": bitflipper(default_signature)}, **ERR_SIG_SCHNORR)

# == Test involving an internal public key not on the curve ==

# X-only public keys are 32 bytes, but not every 32-byte array is a valid public key; only
# around 50% of them are. This does not affect users using correct software; these "keys" have
# no corresponding private key, and thus will never appear as output of key
# generation/derivation/tweaking.
#
# Using an invalid public key as P2TR output key makes the UTXO unspendable. Revealing an
# invalid public key as internal key in a P2TR script path spend also makes the spend invalid.
# These conditions are explicitly spelled out in BIP341.
#
# It is however hard to create test vectors for this, because it involves "guessing" how a
# hypothetical incorrect implementation deals with an obviously-invalid condition, and making
# sure that guessed behavior (accepting it in certain condition) doesn't occur.
#
# The test case added here tries to detect a very specific bug a verifier could have: if they
# don't verify whether or not a revealed internal public key in a script path spend is valid,
# and (correctly) implement output_key == tweak(internal_key, tweakval) but (incorrectly) treat
# tweak(invalid_key, tweakval) as equal the public key corresponding to private key tweakval.
# This may seem like a far-fetched edge condition to test for, but in fact, the BIP341 wallet
# pseudocode did exactly that (but obviously only triggerable by someone invoking the tweaking
# function with an invalid public key, which shouldn't happen).

# Generate an invalid public key
while True:
invalid_pub = random_bytes(32)
if not SECP256K1.is_x_coord(int.from_bytes(invalid_pub, 'big')):
break

# Implement a test case that detects validation logic which maps invalid public keys to the
# point at infinity in the tweaking logic.
tap = taproot_construct(invalid_pub, [("true", CScript([OP_1]))], treat_internal_as_infinity=True)
add_spender(spenders, "output/invalid_x", tap=tap, key_tweaked=tap.tweak, failure={"leaf": "true", "inputs": []}, **ERR_WITNESS_PROGRAM_MISMATCH)

# Do the same thing without invalid point, to make sure there is no mistake in the test logic.
tap = taproot_construct(pubs[0], [("true", CScript([OP_1]))])
add_spender(spenders, "output/invalid_x_mock", tap=tap, key=secs[0], leaf="true", inputs=[])

# == Tests for signature hashing ==

# Run all tests once with no annex, and once with a valid random annex.
Expand Down
9 changes: 6 additions & 3 deletions test/functional/test_framework/script.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import unittest
from typing import List, Dict

from .key import TaggedHash, tweak_add_pubkey
from .key import TaggedHash, tweak_add_pubkey, compute_xonly_pubkey

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

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

ret, h = taproot_tree_helper(scripts)
tweak = TaggedHash("TapTweak", pubkey + h)
tweaked, negated = tweak_add_pubkey(pubkey, tweak)
if treat_internal_as_infinity:
tweaked, negated = compute_xonly_pubkey(tweak)
else:
tweaked, negated = tweak_add_pubkey(pubkey, tweak)
leaves = dict((name, TaprootLeafInfo(script, version, merklebranch, leaf)) for name, version, script, merklebranch, leaf in ret)
return TaprootInfo(CScript([OP_1, tweaked]), pubkey, negated + 0, tweak, leaves, h, tweaked)

Expand Down

0 comments on commit 38d06e1

Please sign in to comment.