Merge bitcoin/bitcoin#26383: test: Add feature_taproot case involving invalid internal pubkey

5d413c8e79 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 https://github.com/bitcoin-core/qa-assets/pull/98.

ACKs for top commit:
  instagibbs:
    ACK 5d413c8e79
  aureleoules:
    reACK 5d413c8e79

Tree-SHA512: dfa014e383cd2743f3c9a996e1f2a2fceb9e244edf4b05dc0c110c4ba32a87684482222907805a4ca998aebcb42a197bb3e7967bfb5f0554fe9f1e5aa5463603
pull/21917/head
fanquake 2 years ago
commit 38d06e1561
No known key found for this signature in database
GPG Key ID: 2EEB9F5CC09526C1

@ -96,7 +96,14 @@ from test_framework.util import (
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,
@ -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.

@ -12,7 +12,7 @@ import struct
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,
@ -872,7 +872,7 @@ TaprootInfo = namedtuple("TaprootInfo", "scriptPubKey,internal_pubkey,negflag,tw
# - 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)
@ -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)

Loading…
Cancel
Save