diff --git a/bitcointx/__init__.py b/bitcointx/__init__.py index 20ef9bbf..6c8025c6 100644 --- a/bitcointx/__init__.py +++ b/bitcointx/__init__.py @@ -1,5 +1,5 @@ # Copyright (C) 2012-2018 The python-bitcoinlib developers -# Copyright (C) 2018-2019 The python-bitcointx developers +# Copyright (C) 2018-2021 The python-bitcointx developers # # This file is part of python-bitcointx. # @@ -310,6 +310,33 @@ def select_chain_params(params: Union[str, ChainParamsBase, return prev_params, params +def allow_secp256k1_experimental_modules() -> None: + """ + python-bitcointx uses libsecp256k1 via ABI using ctypes, using dynamic + library loading. This means that the correctness of the calls to secp256k1 + functions depend on these function definitions in python-bitcointx code + to be in-sync with the actual C language definitions in the library when + it was compiled. Libsecp256k1 ABI is mostly stable, but still has no + officially released version at the moment. But for schnorr signatures + and x-only pubkeys, we have to use the modules of libsecp256k1 that are + currently marked as 'experimental'. These modules being experimental + mean that their ABI can change at any moment. Therefore, to use + python-bitcointx with this functionality, it is highly recommended to + compile your own version libsecp256k1 using the git commit that is + recommended for particular version of python-bitcointx, and then make + sure that python-bitcointx will load that exact version of libsecp256k1 + with set_custom_secp256k1_path() or LD_LIBRARY_PATH environment variable. + + But since using experimental modules with some over version of libsecp256k1 + may lead to crashes, using them is disabled by default. One have to + explicitly enable this by calling this function, or setting the + environment variable + "PYTHON_BITCOINTX_ALLOW_LIBSECP256K1_EXPERIMENTAL_MODULES_USE" to the + value "1". + """ + bitcointx.util._allow_secp256k1_experimental_modules = True + + def set_custom_secp256k1_path(path: str) -> None: """Set the custom path that will be used to load secp256k1 library by bitcointx.core.secp256k1 module. For the calling of this @@ -355,6 +382,14 @@ class ChainParamsContextVar(bitcointx.util.ContextVarsCompat): _chain_params_context = ChainParamsContextVar(params=BitcoinMainnetParams()) +_secp256k1_experimental_modues_enable_var = os.environ.get( + 'PYTHON_BITCOINTX_ALLOW_LIBSECP256K1_EXPERIMENTAL_MODULES_USE') + +if _secp256k1_experimental_modues_enable_var is not None: + assert _secp256k1_experimental_modues_enable_var in ("0", "1") + if int(_secp256k1_experimental_modues_enable_var): + allow_secp256k1_experimental_modules() + __all__ = ( 'ChainParamsBase', @@ -369,4 +404,5 @@ class ChainParamsContextVar(bitcointx.util.ContextVarsCompat): 'find_chain_params', 'set_custom_secp256k1_path', 'get_custom_secp256k1_path', + 'allow_secp256k1_experimental_modules', ) diff --git a/bitcointx/core/__init__.py b/bitcointx/core/__init__.py index b9c28f70..58b34ed8 100644 --- a/bitcointx/core/__init__.py +++ b/bitcointx/core/__init__.py @@ -33,7 +33,8 @@ from ..util import ( no_bool_use_as_property, ClassMappingDispatcher, activate_class_dispatcher, - dispatcher_wrap_methods, classgetter, ensure_isinstance, ContextVarsCompat + dispatcher_wrap_methods, classgetter, ensure_isinstance, ContextVarsCompat, + tagged_hasher ) # NOTE: due to custom class dispatching and mutable/immmutable @@ -216,11 +217,23 @@ class CoreCoinParams(CoreCoinClass, next_dispatch_final=True): def MAX_MONEY(cls) -> int: return 21000000 * cls.COIN + TAPROOT_LEAF_TAPSCRIPT: int + taptweak_hasher: Callable[[bytes], bytes] + tapleaf_hasher: Callable[[bytes], bytes] + tapbranch_hasher: Callable[[bytes], bytes] + tap_sighash_hasher: Callable[[bytes], bytes] + class CoreBitcoinParams(CoreCoinParams, CoreBitcoinClass): PSBT_MAGIC_HEADER_BYTES = b'psbt\xff' PSBT_MAGIC_HEADER_BASE64 = 'cHNidP' + TAPROOT_LEAF_TAPSCRIPT = 0xc0 + taptweak_hasher = tagged_hasher(b'TapTweak') + tapleaf_hasher = tagged_hasher(b'TapLeaf') + tapbranch_hasher = tagged_hasher(b'TapBranch') + tap_sighash_hasher = tagged_hasher(b'TapSighash') + def MoneyRange(nValue: int) -> bool: # check is in satoshis, supplying float might indicate that diff --git a/bitcointx/core/bitcoinconsensus.py b/bitcointx/core/bitcoinconsensus.py index 92b81cc8..4c0eb79d 100644 --- a/bitcointx/core/bitcoinconsensus.py +++ b/bitcointx/core/bitcoinconsensus.py @@ -19,15 +19,16 @@ """ import ctypes -from typing import Union, Tuple, Set, Optional +from typing import Union, Tuple, Set, Optional, Sequence from bitcointx.util import ensure_isinstance -from bitcointx.core import MoneyRange, CTransaction +from bitcointx.core import MoneyRange, CTransaction, CTxOut from bitcointx.core.script import CScript, CScriptWitness from bitcointx.core.scripteval import ( SCRIPT_VERIFY_P2SH, SCRIPT_VERIFY_DERSIG, SCRIPT_VERIFY_NULLDUMMY, SCRIPT_VERIFY_CHECKLOCKTIMEVERIFY, SCRIPT_VERIFY_CHECKSEQUENCEVERIFY, SCRIPT_VERIFY_WITNESS, + SCRIPT_VERIFY_TAPROOT, ALL_SCRIPT_VERIFY_FLAGS, ScriptVerifyFlag_Type, VerifyScriptError, script_verify_flags_to_string ) @@ -68,6 +69,7 @@ bitcoinconsensus_SCRIPT_FLAGS_VERIFY_CHECKSEQUENCEVERIFY = 1 << 10 # enable WITNESS (BIP141) bitcoinconsensus_SCRIPT_FLAGS_VERIFY_WITNESS = 1 << 11 +bitcoinconsensus_SCRIPT_FLAGS_VERIFY_TAPROOT = 1 << 17 BITCOINCONSENSUS_FLAG_MAPPING = { SCRIPT_VERIFY_P2SH: bitcoinconsensus_SCRIPT_FLAGS_VERIFY_P2SH, @@ -75,7 +77,8 @@ SCRIPT_VERIFY_NULLDUMMY: bitcoinconsensus_SCRIPT_FLAGS_VERIFY_NULLDUMMY, SCRIPT_VERIFY_CHECKLOCKTIMEVERIFY: bitcoinconsensus_SCRIPT_FLAGS_VERIFY_CHECKLOCKTIMEVERIFY, SCRIPT_VERIFY_CHECKSEQUENCEVERIFY: bitcoinconsensus_SCRIPT_FLAGS_VERIFY_CHECKSEQUENCEVERIFY, - SCRIPT_VERIFY_WITNESS: bitcoinconsensus_SCRIPT_FLAGS_VERIFY_WITNESS + SCRIPT_VERIFY_WITNESS: bitcoinconsensus_SCRIPT_FLAGS_VERIFY_WITNESS, + SCRIPT_VERIFY_TAPROOT: bitcoinconsensus_SCRIPT_FLAGS_VERIFY_TAPROOT } BITCOINCONSENSUS_ACCEPTED_FLAGS = set(BITCOINCONSENSUS_FLAG_MAPPING.keys()) @@ -123,6 +126,20 @@ def _add_function_definitions(handle: ctypes.CDLL) -> None: ctypes.POINTER(ctypes.c_uint) # bitcoinconsensus_error* err ] + # handle.bitcoinconsensus_verify_script_taproot.restype = ctypes.c_int + # handle.bitcoinconsensus_verify_script_taproot.argtypes = [ + # ctypes.c_char_p, # const unsigned char *scriptPubKey + # ctypes.c_uint, # unsigned int scriptPubKeyLen + # ctypes.c_int64, # int64_t amount + # ctypes.c_char_p, # const unsigned char *txTo + # ctypes.c_uint, # unsigned int txToLen + # ctypes.c_char_p, # const unsigned char *spentOutputs + # ctypes.c_uint, # unsigned int spentOutputsLen + # ctypes.c_uint, # unsigned int nIn + # ctypes.c_uint, # unsigned int flags + # ctypes.POINTER(ctypes.c_uint) # bitcoinconsensus_error* err + # ] + handle.bitcoinconsensus_version.restype = ctypes.c_int handle.bitcoinconsensus_version.argtypes = [] @@ -173,12 +190,15 @@ def load_bitcoinconsensus_library(library_name: Optional[str] = None, def ConsensusVerifyScript( - scriptSig: CScript, scriptPubKey: CScript, - txTo: CTransaction, inIdx: int, + scriptSig: CScript, + scriptPubKey: CScript, + txTo: CTransaction, + inIdx: int, flags: Union[Tuple[ScriptVerifyFlag_Type, ...], Set[ScriptVerifyFlag_Type]] = (), amount: int = 0, witness: Optional[CScriptWitness] = None, + spent_outputs: Optional[Sequence[CTxOut]] = None, consensus_library_hanlde: Optional[ctypes.CDLL] = None ) -> None: @@ -266,11 +286,26 @@ def ConsensusVerifyScript( error_code = ctypes.c_uint() error_code.value = 0 - result = handle.bitcoinconsensus_verify_script_with_amount( - scriptPubKey, len(scriptPubKey), amount, - tx_data, len(tx_data), inIdx, libconsensus_flags, - ctypes.byref(error_code) - ) + if spent_outputs: + raise NotImplementedError( + 'no taproot support for libbitcoinconsensus yet') + if len(spent_outputs) != len(txTo.vin): + raise ValueError('number of spent_outputs must equal ' + 'the number of inputs in transacton') + + spent_outs_data = b''.join(out.serialize() for out in spent_outputs) + result = handle.bitcoinconsensus_verify_script_taproot( + scriptPubKey, len(scriptPubKey), amount, + tx_data, len(tx_data), spent_outs_data, len(spent_outs_data), + inIdx, libconsensus_flags, + ctypes.byref(error_code) + ) + else: + result = handle.bitcoinconsensus_verify_script_with_amount( + scriptPubKey, len(scriptPubKey), amount, + tx_data, len(tx_data), inIdx, libconsensus_flags, + ctypes.byref(error_code) + ) if result == 1: # script was verified successfully - just return, no exception raised. diff --git a/bitcointx/core/key.py b/bitcointx/core/key.py index cc26468e..c192505e 100644 --- a/bitcointx/core/key.py +++ b/bitcointx/core/key.py @@ -1,6 +1,6 @@ # Copyright (C) 2011 Sam Rushing # Copyright (C) 2012-2015 The python-bitcoinlib developers -# Copyright (C) 2018-2019 The python-bitcointx developers +# Copyright (C) 2018-2021 The python-bitcointx developers # # This file is part of python-bitcointx. # @@ -42,7 +42,8 @@ PUBLIC_KEY_SIZE, COMPRESSED_PUBLIC_KEY_SIZE, SECP256K1_EC_COMPRESSED, SECP256K1_EC_UNCOMPRESSED, secp256k1_has_pubkey_recovery, secp256k1_has_ecdh, - secp256k1_has_privkey_negate, secp256k1_has_pubkey_negate + secp256k1_has_privkey_negate, secp256k1_has_pubkey_negate, + secp256k1_has_xonly_pubkeys, secp256k1_has_schnorrsig ) BIP32_HARDENED_KEY_OFFSET = 0x80000000 @@ -53,6 +54,7 @@ T_CExtPubKeyBase = TypeVar('T_CExtPubKeyBase', bound='CExtPubKeyBase') T_unbounded = TypeVar('T_unbounded') + _openssl_library_handle: Optional[ctypes.CDLL] = None @@ -64,6 +66,18 @@ class KeyDerivationFailException(RuntimeError): pass +def _experimental_module_unavailable_error(msg: str, module_name: str) -> str: + return ( + f'{msg} handling functions from libsecp256k1 is not available. ' + f'You should use newer version of secp256k1 library, ' + f'configure it with --enable-experimental and ' + f'--enable-module-{module_name}, and also enable the use ' + f'of functions from libsecp256k1 experimental modules by ' + f'python-bitcointx (see docstring for ' + f'allow_secp256k1_experimental_modules() function)' + ) + + # Thx to Sam Devlin for the ctypes magic 64-bit fix (FIXME: should this # be applied to every OpenSSL call whose return type is a pointer?) def _check_res_openssl_void_p(val, func, args): # type: ignore @@ -202,6 +216,10 @@ def secret_bytes(self) -> bytes: def pub(self) -> 'CPubKey': return self.__pub + @property + def xonly_pub(self) -> 'XOnlyPubKey': + return XOnlyPubKey(self.__pub) + def sign(self, hash: Union[bytes, bytearray], *, _ecdsa_sig_grind_low_r: bool = True, _ecdsa_sig_extra_entropy: int = 0 @@ -283,12 +301,154 @@ def sign_compact(self, hash: Union[bytes, bytearray]) -> Tuple[bytes, int]: # p return output.raw, recid.value + def sign_schnorr_no_tweak( + self, hash: Union[bytes, bytearray], + *, + aux: Optional[bytes] = None + ) -> bytes: + """ + Produce Schnorr signature of the supplied `hash` with this key. + No tweak is applied to the key before signing. + This is mostly useful when the signature is going to be checked + within the script by CHECKSIG-related opcodes, or for other generic + Schnorr signing needs + """ + return self._sign_schnorr_internal(hash, aux=aux) + + def _sign_schnorr_internal( + self, hash: Union[bytes, bytearray], + *, + merkle_root: Optional[bytes] = None, + aux: Optional[bytes] = None + ) -> bytes: + """ + Internal function to produce Schnorr signature. + It is not supposed to be called by the external code. + + Note on merkle_root argument: values of None, b'' and <32 bytes> + all have different meaning. + - None means no tweak is applied to the key before signing. + This is mostly useful when the signature is going to be checked + within the script by CHECKSIG-related opcodes, or for other + generic Schnorr signing needs + - b'' means that the tweak will be applied, with merkle_root + being generated as the tagged hash of the x-only pubkey + corresponding to this private key. This is mostly useful + when signing keypath spends when there is no script path + - <32 bytes> are used directly as a tweak. This is mostly useful + when signing keypath spends when there is also a script path + present + """ + + ensure_isinstance(hash, (bytes, bytearray), 'hash') + if len(hash) != 32: + raise ValueError('Hash must be exactly 32 bytes long') + + if not secp256k1_has_schnorrsig: + raise RuntimeError( + _experimental_module_unavailable_error( + 'schnorr signature', 'schnorrsig')) + + if aux is not None: + ensure_isinstance(aux, (bytes, bytearray), 'aux') + if len(aux) != 32: + raise ValueError('aux must be exactly 32 bytes long') + + sizeof_keypair = 96 + keypair_buf = ctypes.create_string_buffer(sizeof_keypair) + + result = _secp256k1.secp256k1_keypair_create( + secp256k1_context_sign, keypair_buf, self) + + if 1 != result: + assert(result == 0) + raise RuntimeError('secp256k1_keypair_create returned failure') + + pubkey_buf = ctypes.create_string_buffer(64) + + if merkle_root is not None: + ensure_isinstance(merkle_root, (bytes, bytearray), 'merkle_root') + + result = _secp256k1.secp256k1_keypair_xonly_pub( + secp256k1_context_sign, pubkey_buf, None, keypair_buf) + + if 1 != result: + assert(result == 0) + raise RuntimeError('secp256k1_keypair_xonly_pub returned failure') + + # It should take one less secp256k1 call if we just take self.pub + # here, because XOnlyPubKey(self.pub) will just drop the first + # byte of self.pub data and will make x-only pubkey from that. + # But the code is translated from CKey::SignSchnorr in Bitcon Core, + # so one extra secp256k1 call here to be close to original source + + serialized_pubkey_buf = ctypes.create_string_buffer(32) + result = _secp256k1.secp256k1_xonly_pubkey_serialize( + secp256k1_context_verify, serialized_pubkey_buf, pubkey_buf) + + if 1 != result: + assert(result == 0) + raise RuntimeError('secp256k1_xonly_pubkey_serialize returned failure') + + tweak = compute_tap_tweak_hash( + XOnlyPubKey(serialized_pubkey_buf.raw), + merkle_root=merkle_root) + + result = _secp256k1.secp256k1_keypair_xonly_tweak_add( + secp256k1_context_sign, keypair_buf, tweak) + + if 1 != result: + assert(result == 0) + raise RuntimeError('secp256k1_keypair_xonly_tweak_add returned failure') + + sig_buf = ctypes.create_string_buffer(64) + result = _secp256k1.secp256k1_schnorrsig_sign( + secp256k1_context_sign, sig_buf, hash, keypair_buf, aux + ) + + if 1 != result: + assert(result == 0) + raise RuntimeError('secp256k1_schnorrsig_sign returned failure') + + # The pubkey may be tweaked, so extract it from keypair + # to do verification after signing + result = _secp256k1.secp256k1_keypair_xonly_pub( + secp256k1_context_sign, pubkey_buf, None, keypair_buf) + + if 1 != result: + assert(result == 0) + raise RuntimeError('secp256k1_keypair_xonly_pub returned failure') + + # This check is not in Bitcoin Core's `CKey::SignSchnorr`, but + # is recommended in BIP340 if the computation cost is not a concern + result = _secp256k1.secp256k1_schnorrsig_verify( + secp256k1_context_verify, + sig_buf.raw, hash, 32, pubkey_buf + ) + + if result != 1: + assert result == 0 + raise RuntimeError( + 'secp256k1_schnorrsig_verify failed after signing') + + # There's no C compiler that can optimize out the 'superfluous' memset, + # so we don't need special memory_cleanse() function. + # Not that it matters much in python where we don't have control over + # memory and the keydata is probably spread all over the place anyway, + # but still, do this to be close to the original source + ctypes.memset(keypair_buf, 0, sizeof_keypair) + + return sig_buf.raw + def verify(self, hash: bytes, sig: bytes) -> bool: return self.pub.verify(hash, sig) def verify_nonstrict(self, hash: bytes, sig: bytes) -> bool: return self.pub.verify_nonstrict(hash, sig) + def verify_schnorr(self, msg: bytes, sig: bytes) -> bool: + return XOnlyPubKey(self.pub).verify_schnorr(msg, sig) + def ECDH(self, pub: Optional['CPubKey'] = None) -> bytes: if not secp256k1_has_ecdh: raise RuntimeError( @@ -383,12 +543,6 @@ class CPubKey(bytes): Attributes: - is_nonempty() - Corresponds to CPubKey.IsValid() - - is_fullyvalid() - Corresponds to CPubKey.IsFullyValid() - - is_compressed() - Corresponds to CPubKey.IsCompressed() - key_id - Hash160(pubkey) """ @@ -403,6 +557,7 @@ def __new__(cls: Type[T_CPubKey], buf: bytes = b'') -> T_CPubKey: tmp_pub = ctypes.create_string_buffer(64) result = _secp256k1.secp256k1_ec_pubkey_parse( secp256k1_context_verify, tmp_pub, self, len(self)) + assert result in (1, 0) self.__fullyvalid = (result == 1) self.__key_id = bitcointx.core.Hash160(self) @@ -490,7 +645,11 @@ def is_valid(self) -> bool: @no_bool_use_as_property def is_nonempty(self) -> bool: - return len(self) > 0 + return not self.is_null() + + @no_bool_use_as_property + def is_null(self) -> bool: + return len(self) == 0 @no_bool_use_as_property def is_fullyvalid(self) -> bool: @@ -512,6 +671,9 @@ def verify(self, hash: bytes, sig: bytes) -> bool: ensure_isinstance(sig, (bytes, bytearray), 'signature') ensure_isinstance(hash, (bytes, bytearray), 'hash') + if len(hash) != 32: + raise ValueError('Hash must be exactly 32 bytes long') + if not sig: return False @@ -555,6 +717,9 @@ def verify_nonstrict(self, hash: bytes, sig: bytes) -> bool: ensure_isinstance(sig, (bytes, bytearray), 'signature') ensure_isinstance(hash, (bytes, bytearray), 'hash') + if len(hash) != 32: + raise ValueError('Hash must be exactly 32 bytes long') + if not sig: return False @@ -593,15 +758,21 @@ def verify_nonstrict(self, hash: bytes, sig: bytes) -> bool: return self.verify(hash, norm_der.raw) + def verify_schnorr(self, msg: bytes, sig: bytes) -> bool: + return XOnlyPubKey(self).verify_schnorr(msg, sig) + @classmethod def combine(cls: Type[T_CPubKey], *pubkeys: T_CPubKey, compressed: bool = True) -> T_CPubKey: if len(pubkeys) <= 1: raise ValueError( 'number of pubkeys to combine must be more than one') - if not all(isinstance(p, CPubKey) for p in pubkeys): - raise ValueError( - 'each supplied pubkey must be an instance of CPubKey') + for p in pubkeys: + if not isinstance(p, CPubKey): + raise ValueError( + 'each supplied pubkey must be an instance of CPubKey') + if not p.is_fullyvalid(): + raise ValueError('each supplied pubkey must be valid') pubkey_arr = (ctypes.c_char_p*len(pubkeys))() for i, p in enumerate(pubkeys): @@ -621,6 +792,10 @@ def negated(self: T_CPubKey) -> T_CPubKey: raise RuntimeError( 'secp256k1 does not export pubkey negation function. ' 'You should use newer version of secp256k1 library') + + if not self.is_fullyvalid(): + raise ValueError('cannot negate an invalid pubkey') + pubkey_buf = self._to_ctypes_char_array() ret = _secp256k1.secp256k1_ec_pubkey_negate(secp256k1_context_verify, pubkey_buf) if 1 != ret: @@ -1905,6 +2080,190 @@ def get_pubkey(self, key_id: bytes, return None +T_XOnlyPubKey = TypeVar('T_XOnlyPubKey', bound='XOnlyPubKey') + + +class XOnlyPubKey(bytes): + """An encapsulated X-Only public key""" + + __fullyvalid: bool + + def __new__(cls: Type[T_XOnlyPubKey], + keydata: Union[bytes, CPubKey] = b'') -> T_XOnlyPubKey: + + if not secp256k1_has_xonly_pubkeys: + raise RuntimeError( + _experimental_module_unavailable_error( + 'x-only pubkey', 'extrakeys')) + + if len(keydata) in (32, 0): + ensure_isinstance(keydata, bytes, 'x-only pubkey data') + elif len(keydata) == 33: + ensure_isinstance(keydata, CPubKey, + 'x-only pubkey data of 33 bytes') + keydata = keydata[1:33] + else: + raise ValueError('unrecognized pubkey data length') + + self = super().__new__(cls, keydata) + + self.__fullyvalid = False + + if self.is_nonempty(): + tmpbuf = ctypes.create_string_buffer(64) + result = _secp256k1.secp256k1_xonly_pubkey_parse( + secp256k1_context_verify, tmpbuf, self) + assert result in (1, 0) + self.__fullyvalid = (result == 1) + + return self + + @no_bool_use_as_property + def is_fullyvalid(self) -> bool: + return self.__fullyvalid + + @no_bool_use_as_property + def is_nonempty(self) -> bool: + return not self.is_null() + + @no_bool_use_as_property + def is_null(self) -> bool: + return len(self) == 0 + + def verify_schnorr(self, hash: bytes, sigbytes: bytes) -> bool: + if not secp256k1_has_schnorrsig: + raise RuntimeError( + _experimental_module_unavailable_error( + 'schnorr signature', 'schnorrsig')) + + ensure_isinstance(sigbytes, (bytes, bytearray), 'signature') + ensure_isinstance(hash, (bytes, bytearray), 'hash') + + if len(hash) != 32: + raise ValueError('Hash must be exactly 32 bytes long') + + if not sigbytes: + return False + + if not self.is_fullyvalid(): + return False + + if len(sigbytes) != 64: + raise ValueError('Signature must be exactly 64 bytes long') + + result = _secp256k1.secp256k1_schnorrsig_verify( + secp256k1_context_verify, + sigbytes, hash, 32, self._to_ctypes_char_array() + ) + + if result != 1: + assert result == 0 + return False + + return True + + def _to_ctypes_char_array(self) -> 'ctypes.Array[ctypes.c_char]': + assert self.is_fullyvalid() + raw_pub = ctypes.create_string_buffer(64) + result = _secp256k1.secp256k1_xonly_pubkey_parse( + secp256k1_context_verify, raw_pub, self) + if 1 != result: + assert(result == 0) + raise RuntimeError('secp256k1_xonly_pubkey_parse returned failure') + return raw_pub + + def __str__(self) -> str: + return repr(self) + + def __repr__(self) -> str: + return f"{self.__class__.__name__}(x('{bitcointx.core.b2x(self)}'))" + + +def compute_tap_tweak_hash( + pub: XOnlyPubKey, *, merkle_root: bytes = b'' +) -> bytes: + ensure_isinstance(merkle_root, bytes, 'merkle_root') + + if not merkle_root: + return bitcointx.core.CoreCoinParams.taptweak_hasher(pub) + + if len(merkle_root) != 32: + raise ValueError('non-empty merkle_root must be 32 bytes long') + + return bitcointx.core.CoreCoinParams.taptweak_hasher( + pub + merkle_root) + + +def check_tap_tweak(tweaked_pub: XOnlyPubKey, internal_pub: XOnlyPubKey, + *, + merkle_root: bytes = b'', + parity: bool) -> bool: + + if not tweaked_pub.is_fullyvalid(): + raise ValueError('supplied tweaked_pub must be valid') + + if not internal_pub.is_fullyvalid(): + raise ValueError('supplied internal_pub must be valid') + + tweak = compute_tap_tweak_hash(internal_pub, merkle_root=merkle_root) + + result = _secp256k1.secp256k1_xonly_pubkey_tweak_add_check( + secp256k1_context_verify, tweaked_pub, int(bool(parity)), + internal_pub._to_ctypes_char_array(), tweak) + + if result != 1: + assert result == 0 + return False + + return True + + +# in BitcoinCore, the same function is called `CreateTapTweak`. But +# in this case it makes sense to deviate from followin Core's naming +# conventions, because `create_tap_tweak` could be perceived as something +# that creates tweak hash, rather than tweaks the pubkey. +def tap_tweak_pubkey(pub: XOnlyPubKey, *, merkle_root: bytes = b'', + ) -> Optional[Tuple[XOnlyPubKey, bool]]: + + if not pub.is_fullyvalid(): + raise ValueError('pubkey must be valid') + + base_point = pub._to_ctypes_char_array() + tweak = compute_tap_tweak_hash(pub, merkle_root=merkle_root) + out = ctypes.create_string_buffer(64) + + result = _secp256k1.secp256k1_xonly_pubkey_tweak_add( + secp256k1_context_verify, out, base_point, tweak) + + if result != 1: + assert result == 0 + return None + + out_xonly = ctypes.create_string_buffer(64) + + parity_ret = ctypes.c_int() + parity_ret.value = -1 + + result = _secp256k1.secp256k1_xonly_pubkey_from_pubkey( + secp256k1_context_verify, out_xonly, ctypes.byref(parity_ret), + out) + + if result != 1: + assert result == 0 + return None + + assert parity_ret.value in (0, 1) + parity = bool(parity_ret.value) + + out_xonly_serialized = ctypes.create_string_buffer(32) + + result = _secp256k1.secp256k1_xonly_pubkey_serialize( + secp256k1_context_verify, out_xonly_serialized, out_xonly) + assert result == 1 + + return XOnlyPubKey(out_xonly_serialized.raw), parity + + __all__ = ( 'CKey', 'CPubKey', @@ -1917,5 +2276,9 @@ def get_pubkey(self, key_id: bytes, 'BIP32PathTemplate', 'BIP32PathTemplateIndex', 'KeyDerivationInfo', - 'KeyStore' + 'KeyStore', + 'XOnlyPubKey', + 'compute_tap_tweak_hash', + 'check_tap_tweak', + 'tap_tweak_pubkey', ) diff --git a/bitcointx/core/script.py b/bitcointx/core/script.py index e1957c4b..bba94ab3 100644 --- a/bitcointx/core/script.py +++ b/bitcointx/core/script.py @@ -40,6 +40,10 @@ ensure_isinstance ) +_conventional_leaf_versions = \ + tuple(range(0xc0, 0x100, 2)) + \ + (0x66, 0x7e, 0x80, 0x84, 0x96, 0x98, 0xba, 0xbc, 0xbe) + MAX_SCRIPT_SIZE = 10000 MAX_SCRIPT_ELEMENT_SIZE = 520 MAX_STANDARD_P2WSH_SCRIPT_SIZE = 3600 @@ -65,12 +69,19 @@ class SIGVERSION_Type(int): SIGVERSION_BASE: SIGVERSION_Type = SIGVERSION_Type(0) SIGVERSION_WITNESS_V0: SIGVERSION_Type = SIGVERSION_Type(1) +SIGVERSION_TAPROOT: SIGVERSION_Type = SIGVERSION_Type(2) +SIGVERSION_TAPSCRIPT: SIGVERSION_Type = SIGVERSION_Type(3) T_int = TypeVar('T_int', bound=int) class SIGHASH_Bitflag_Type(int): + def __init__(self, value: int) -> None: + super().__init__() + if value & (value - 1) != 0: + raise ValueError('bitflag must be power of 2 (or 0 for no flags)') + def __or__(self, other: T_int) -> T_int: return cast(T_int, super().__or__(other)) @@ -100,6 +111,14 @@ def register_type(cls: Type[T_SIGHASH_Type], value: int) -> T_SIGHASH_Type: cls._known_values = tuple(list(cls._known_values) + [value]) return cls(value) + @property + def output_type(self: T_SIGHASH_Type) -> T_SIGHASH_Type: + return self.__class__(self & ~self._known_bitflags) + + @property + def input_type(self) -> SIGHASH_Bitflag_Type: + return SIGHASH_Bitflag_Type(self & self._known_bitflags) + # The type of 'other' is intentionally incompatible wit supertype 'int' # because we do not want that or-ing with anything but bitflag type # to produce SIGHASH_Type result. @@ -348,6 +367,9 @@ def __new__(cls, n: int) -> 'CScriptOp': OP_NOP9 = CScriptOp(0xb8) OP_NOP10 = CScriptOp(0xb9) +# Opcode added by BIP 342 (Tapscript) +OP_CHECKSIGADD = CScriptOp(0xba) + # template matching params OP_SMALLINTEGER = CScriptOp(0xfa) OP_PUBKEYS = CScriptOp(0xfb) @@ -684,6 +706,7 @@ class CScript(bytes, ScriptCoinClass, next_dispatch_final=True): iter(script) however does iterate by opcode. """ + @classmethod def __coerce_instance(cls, other: ScriptElement_Type) -> bytes: # Coerce other into bytes @@ -722,11 +745,12 @@ def join(self, iterable: Any) -> None: # type: ignore raise NotImplementedError def __new__(cls: Type[T_CScript], - value: Iterable[ScriptElement_Type] = b'' + value: Iterable[ScriptElement_Type] = b'', + *, name: Optional[str] = None, ) -> T_CScript: if isinstance(value, (bytes, bytearray)): - return super().__new__(cls, value) + instance = super().__new__(cls, value) else: def coerce_iterable(iterable: Iterable[ScriptElement_Type] ) -> Generator[bytes, None, None]: @@ -736,9 +760,21 @@ def coerce_iterable(iterable: Iterable[ScriptElement_Type] # Annoyingly bytes.join() always # returns a bytes instance even when subclassed. - return super().__new__( + instance = super().__new__( cls, b''.join(coerce_iterable(value))) + if name is not None: + ensure_isinstance(name, str, 'name') + instance._script_name = name # type: ignore + + return instance + + # we only create the _script_name slot if the name is specified, + # so we use this @property to make access to 'name' convenient + @property + def name(self) -> Optional[str]: + return cast(Optional[str], getattr(self, '_script_name', None)) + def raw_iter(self) -> Generator[Tuple[CScriptOp, Optional[bytes], int], None, None]: """Raw iteration @@ -848,7 +884,12 @@ def _repr(o: Any) -> str: if op is not None: ops.append(op) - return "%s([%s])" % (self.__class__.__name__, ', '.join(ops)) + args = f'[{", ".join(ops)}]' + + if self.name: + args += f', name={repr(self.name)}' + + return f'{self.__class__.__name__}({args})' @no_bool_use_as_property def is_p2sh(self) -> bool: @@ -1104,6 +1145,27 @@ def raw_sighash(self, txTo: 'bitcointx.core.CTransaction', inIdx: int, return RawBitcoinSignatureHash(self, txTo, inIdx, hashtype, amount=amount, sigversion=sigversion) + def sighash_schnorr(self, txTo: 'bitcointx.core.CTransaction', inIdx: int, + spent_outputs: Sequence['bitcointx.core.CTxOut'], + *, + hashtype: Optional[SIGHASH_Type] = None, + codeseparator_pos: int = -1, + annex_hash: Optional[bytes] = None + ) -> bytes: + + # Only BIP342 tapscript signing is supported for now. + leaf_version = bitcointx.core.CoreCoinParams.TAPROOT_LEAF_TAPSCRIPT + tapleaf_hash = bitcointx.core.CoreCoinParams.tapleaf_hasher( + bytes([leaf_version]) + BytesSerializer.serialize(self) + ) + + return SignatureHashSchnorr(txTo, inIdx, spent_outputs, + hashtype=hashtype, + sigversion=SIGVERSION_TAPSCRIPT, + tapleaf_hash=tapleaf_hash, + codeseparator_pos=codeseparator_pos, + annex_hash=annex_hash) + class CScriptWitness(ImmutableSerializable): """An encoding of the data elements on the initial stack for (segregated @@ -1241,6 +1303,12 @@ def RawBitcoinSignatureHash(script: CScript, txTo: 'bitcointx.core.CTransaction' if sigversion not in (SIGVERSION_BASE, SIGVERSION_WITNESS_V0): raise ValueError('unsupported sigversion') + if inIdx < 0: + raise ValueError('input index must not be negative') + + if amount is not None and amount < 0: + raise ValueError('amount must not be negative') + HASH_ONE = b'\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' if sigversion == SIGVERSION_WITNESS_V0: @@ -1369,6 +1437,128 @@ def SignatureHash(script: CScript, txTo: 'bitcointx.core.CTransaction', inIdx: i return h +def SignatureHashSchnorr( + txTo: 'bitcointx.core.CTransaction', + inIdx: int, + spent_outputs: Sequence['bitcointx.core.CTxOut'], + *, + hashtype: Optional[SIGHASH_Type] = None, + sigversion: SIGVERSION_Type = SIGVERSION_TAPROOT, + tapleaf_hash: Optional[bytes] = None, + codeseparator_pos: int = -1, + annex_hash: Optional[bytes] = None +) -> bytes: + if inIdx < 0: + raise ValueError('input index must not be negative') + + if inIdx >= len(txTo.vin): + raise ValueError(f'inIdx {inIdx} out of range ({len(txTo.vin)})') + + if tapleaf_hash is not None: + ensure_isinstance(tapleaf_hash, bytes, 'tapleaf_hash') + if len(tapleaf_hash) != 32: + raise ValueError('tapleaf_hash must be exactly 32 bytes long') + + if annex_hash is not None: + ensure_isinstance(annex_hash, bytes, 'annex_hash') + if len(annex_hash) != 32: + raise ValueError('annex_hash must be exactly 32 bytes long') + + if sigversion == SIGVERSION_TAPROOT: + ext_flag = 0 + elif sigversion == SIGVERSION_TAPSCRIPT: + ext_flag = 1 + # key_version is always 0 in Core at the moment, representing the + # current version of 32-byte public keys in the tapscript signature + # opcode execution. An upgradable public key version (with a size not + # 32-byte) may request a different key_version with a new sigversion. + key_version = 0 + else: + raise ValueError('unsupported sigversion') + + if len(spent_outputs) != len(txTo.vin): + raise ValueError( + 'number of spent_outputs is not equal to number of inputs') + + f = BytesIO() + + # Epoch + f.write(bytes([0])) + + # Hash type + if hashtype is None: + hashtype = SIGHASH_ALL + hashtype_byte = b'\x00' + else: + ensure_isinstance(hashtype, SIGHASH_Type, 'hashtype') + hashtype_byte = bytes([hashtype]) + + input_type = hashtype.input_type + output_type = hashtype.output_type + + # Transaction level data + f.write(hashtype_byte) + f.write(struct.pack(" len(txTo.vout): + raise ValueError(f'outIdx {outIdx} out of range ({len(txTo.vout)})') + f.write(hashlib.sha256(txTo.vout[outIdx].serialize()).digest()) + + if sigversion == SIGVERSION_TAPSCRIPT: + if tapleaf_hash is None: + raise ValueError('tapleaf_hash must be specified for SIGVERSION_TAPSCRIPT') + if codeseparator_pos is None: + raise ValueError('codeseparator_pos must be specified for SIGVERSION_TAPSCRIPT') + f.write(tapleaf_hash) + f.write(bytes([key_version])) + f.write(struct.pack(" None: + """ + 'leaves' is a sequence of instances of CScript or TaprootScriptTree. + + 'internal_pubkey' can be supplied (or set later) for + get_script_with_control_block() method to be available + + 'leaf_version' can be supplied for all the scripts in the tree to + have this version + + 'accept_unconventional_leaf_version' is mostly for debug, and + setting it to True will allow to supply versions that are not + adhere to the constraints set in BIP341 + """ + + if len(leaves) == 0: + raise ValueError('List of leaves must not be empty') + + if len(leaves) == 1 and isinstance(leaves[0], TaprootScriptTree): + raise ValueError( + 'A single TaprootScriptTree leaf within another ' + 'TaprootScriptTree is meaningless, therefore it is ' + 'treated as an error') + + if leaf_version is None: + leaf_version = bitcointx.core.CoreCoinParams.TAPROOT_LEAF_TAPSCRIPT + else: + if not any(isinstance(leaf, CScript) for leaf in leaves): + raise ValueError( + 'leaf_version was supplied, but none of the leaves ' + 'are instances of CScript. Leaf version only has ' + 'meaning if there are CScript leaves') + + if leaf_version & 1: + raise ValueError('leaf_version cannot be odd') + + if not (0 <= leaf_version <= 254): + raise ValueError('leaf_version must be between 0 and 254') + + if not accept_unconventional_leaf_version: + if leaf_version not in _conventional_leaf_versions: + raise ValueError( + 'leaf_version value is not conventional ' + '(see values listed in BIP341 footnote 7). To skip ' + 'this check, specify ' + 'accept_unconventional_leaf_version=True') + + self.leaf_version = leaf_version + mr, collect_paths = self._traverse(leaves) + self.merkle_root = mr + merkle_paths = collect_paths(tuple()) + + assert len(merkle_paths) == len(leaves) + self._leaves_with_paths = tuple( + (leaf, merkle_paths[i]) for i, leaf in enumerate(leaves) + ) + + self.set_internal_pubkey(internal_pubkey) + + def __repr__(self) -> str: + return ( + f'{self.__class__.__name__}([' + f'{", ".join(repr(leaf) for leaf, _ in self._leaves_with_paths)}])' + ) + + def set_internal_pubkey( + self, internal_pubkey: Optional['bitcointx.core.key.XOnlyPubKey'] + ) -> None: + """ + Set internal_pubkey that shall be associated with this script tree. + will also set output_pubkey and parity fields. + """ + self.internal_pubkey = internal_pubkey + self.output_pubkey = None + self.parity = None + + if internal_pubkey: + tt_res = bitcointx.core.key.tap_tweak_pubkey( + internal_pubkey, merkle_root=self.merkle_root) + + if not tt_res: + raise ValueError( + 'Failed to create tweaked key with supplied ' + 'internal pubkey and computed merkle root') + + self.output_pubkey = tt_res[0] + self.parity = tt_res[1] + + def get_script_with_control_block( + self, name: str + ) -> Optional[Tuple[CScript, bytes]]: + """Return a tuple of (script, control_block) for the script with + with the supplied name. If the script with that name is not found + in the tree, None will be returned""" + + if not self.internal_pubkey: + raise ValueError(f'This instance of {self.__class__.__name__} ' + f'does not have internal_pubkey') + + assert self.parity is not None + + result = self.get_script_with_path_and_leaf_version(name) + if result: + s, mp, lv = result + return s, bytes([lv + self.parity]) + self.internal_pubkey + mp + + return None + + def get_script_with_path_and_leaf_version( + self, name: str + ) -> Optional[Tuple[CScript, bytes, int]]: + """Return a tuple of (script, merkle_path, leaf_version) for the script + with with the supplied name. If the script with that name is not found + in the tree, None will be returned + """ + + for leaf, path in self._leaves_with_paths: + if isinstance(leaf, CScript) and leaf.name == name: + return leaf, b''.join(path), self.leaf_version + elif isinstance(leaf, TaprootScriptTree): + result = leaf.get_script_with_path_and_leaf_version(name) + if result: + return (result[0], + result[1] + b''.join(path[1:]), + result[2]) + + return None + + def get_script(self, name: str) -> Optional[CScript]: + """Return a script with the supplied name. If the script with that name + is not found in the tree, None will be returned + """ + result = self.get_script_with_path_and_leaf_version(name) + if result: + return result[0] + return None + + def get_script_path(self, name: str) -> Optional[bytes]: + """Return a path of the script with the supplied name. If the script + with that name is not found in the tree, None will be returned + """ + result = self.get_script_with_path_and_leaf_version(name) + if result: + return result[1] + return None + + def get_script_leaf_version(self, name: str) -> Optional[int]: + """Return leaf version of the script with the supplied name. If the + script with that name is not found in the tree, None will be returned + """ + result = self.get_script_with_path_and_leaf_version(name) + if result: + return result[2] + return None + + def _traverse( + self, + leaves: Sequence[TaprootScriptTreeLeaf_Type] + ) -> Tuple[bytes, Callable[[Tuple[bytes, ...]], List[Tuple[bytes, ...]]]]: + + if len(leaves) == 1: + leaf = leaves[0] + if isinstance(leaf, CScript): + leaf_hash = bitcointx.core.CoreCoinParams.tapleaf_hasher( + bytes([self.leaf_version]) + + BytesSerializer.serialize(leaf)) + return ( + leaf_hash, + lambda parent_path: [(b'', ) + parent_path] + ) + elif isinstance(leaf, TaprootScriptTree): + if len(leaf._leaves_with_paths) == 1: + assert isinstance( + leaf._leaves_with_paths[0][0], CScript + ), ("Single TaprootScriptTree leaf within another tree is " + "meaningless and constructing such TaprootScriptTree " + "should have raisen an error") + + # Treat TaprootScriptTree that contains a single script + # as the script itself + path = b'' + else: + path = leaf.merkle_root + + return ( + leaf.merkle_root, + lambda parent_path: [(path, ) + parent_path] + ) + + raise ValueError( + f'Unrecognized type for the leaf: {type(leaf)}') + + split_pos = len(leaves) // 2 + left = leaves[:split_pos] + right = leaves[split_pos:] + + left_h, left_collector = self._traverse(left) + right_h, right_collector = self._traverse(right) + + def collector( + parent_path: Tuple[bytes, ...] + ) -> List[Tuple[bytes, ...]]: + lp = left_collector((right_h, ) + parent_path) + rp = right_collector((left_h, ) + parent_path) + return lp + rp + + tbh = bitcointx.core.CoreCoinParams.tapbranch_hasher + + if right_h < left_h: + branch_hash = tbh(right_h + left_h) + else: + branch_hash = tbh(left_h + right_h) + + return (branch_hash, collector) + + +class TaprootBitcoinScriptTree(TaprootScriptTree, ScriptBitcoinClass): + ... + + # default dispatcher for the module activate_class_dispatcher(ScriptBitcoinClassDispatcher) @@ -1843,6 +2298,7 @@ def collect_sig(self, pub: 'bitcointx.core.key.CPubKey', 'SIGHASH_SINGLE', 'SIGHASH_ANYONECANPAY', 'SIGHASH_Type', + 'SIGHASH_Bitflag_Type', 'FindAndDelete', 'RawSignatureHash', 'RawBitcoinSignatureHash', @@ -1863,4 +2319,5 @@ def collect_sig(self, pub: 'bitcointx.core.key.CPubKey', 'standard_witness_v0_scriptpubkey', 'ComplexScriptSignatureHelper', 'StandardMultisigSignatureHelper', + 'TaprootScriptTree', ) diff --git a/bitcointx/core/scripteval.py b/bitcointx/core/scripteval.py index bb093579..1d5b1bb4 100644 --- a/bitcointx/core/scripteval.py +++ b/bitcointx/core/scripteval.py @@ -82,6 +82,7 @@ class ScriptVerifyFlag_Type: SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_WITNESS_PROGRAM = ScriptVerifyFlag_Type() SCRIPT_VERIFY_WITNESS_PUBKEYTYPE = ScriptVerifyFlag_Type() SCRIPT_VERIFY_CONST_SCRIPTCODE = ScriptVerifyFlag_Type() +SCRIPT_VERIFY_TAPROOT = ScriptVerifyFlag_Type() _STRICT_ENCODING_FLAGS = set((SCRIPT_VERIFY_DERSIG, SCRIPT_VERIFY_LOW_S, SCRIPT_VERIFY_STRICTENC)) @@ -110,7 +111,8 @@ class ScriptVerifyFlag_Type: SCRIPT_VERIFY_WITNESS, SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_WITNESS_PROGRAM, SCRIPT_VERIFY_WITNESS_PUBKEYTYPE, - SCRIPT_VERIFY_CONST_SCRIPTCODE + SCRIPT_VERIFY_CONST_SCRIPTCODE, + SCRIPT_VERIFY_TAPROOT } ALL_SCRIPT_VERIFY_FLAGS = STANDARD_SCRIPT_VERIFY_FLAGS | { diff --git a/bitcointx/core/secp256k1.py b/bitcointx/core/secp256k1.py index 7a8e609e..14176df9 100644 --- a/bitcointx/core/secp256k1.py +++ b/bitcointx/core/secp256k1.py @@ -92,6 +92,8 @@ def _check_ressecp256k1_void_p(val: int, _func: FunctionType, secp256k1_has_privkey_negate = False secp256k1_has_pubkey_negate = False secp256k1_has_ecdh = False +secp256k1_has_xonly_pubkeys = False +secp256k1_has_schnorrsig = False def _add_function_definitions(_secp256k1: ctypes.CDLL) -> None: @@ -99,6 +101,8 @@ def _add_function_definitions(_secp256k1: ctypes.CDLL) -> None: global secp256k1_has_privkey_negate global secp256k1_has_pubkey_negate global secp256k1_has_ecdh + global secp256k1_has_xonly_pubkeys + global secp256k1_has_schnorrsig if getattr(_secp256k1, 'secp256k1_ecdsa_sign_recoverable', None): secp256k1_has_pubkey_recovery = True @@ -157,6 +161,9 @@ def _add_function_definitions(_secp256k1: ctypes.CDLL) -> None: _secp256k1.secp256k1_ec_privkey_tweak_add.restype = ctypes.c_int _secp256k1.secp256k1_ec_privkey_tweak_add.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p] + _secp256k1.secp256k1_ec_pubkey_serialize.restype = ctypes.c_int + _secp256k1.secp256k1_ec_pubkey_serialize.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.POINTER(ctypes.c_size_t), ctypes.c_char_p, ctypes.c_uint] + if getattr(_secp256k1, 'secp256k1_ec_pubkey_negate', None): secp256k1_has_pubkey_negate = True _secp256k1.secp256k1_ec_pubkey_negate.restype = ctypes.c_int @@ -175,6 +182,44 @@ def _add_function_definitions(_secp256k1: ctypes.CDLL) -> None: _secp256k1.secp256k1_ecdh.restype = ctypes.c_int _secp256k1.secp256k1_ecdh.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_void_p, ctypes.c_void_p] + if bitcointx.util._allow_secp256k1_experimental_modules: + if getattr(_secp256k1, 'secp256k1_xonly_pubkey_parse', None): + secp256k1_has_xonly_pubkeys = True + _secp256k1.secp256k1_xonly_pubkey_parse.restype = ctypes.c_int + _secp256k1.secp256k1_xonly_pubkey_parse.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p] + _secp256k1.secp256k1_xonly_pubkey_tweak_add_check.restype = ctypes.c_int + _secp256k1.secp256k1_xonly_pubkey_tweak_add_check.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_int, ctypes.c_char_p, ctypes.c_char_p] + _secp256k1.secp256k1_xonly_pubkey_tweak_add.restype = ctypes.c_int + _secp256k1.secp256k1_xonly_pubkey_tweak_add.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_char_p] + _secp256k1.secp256k1_xonly_pubkey_from_pubkey.restype = ctypes.c_int + _secp256k1.secp256k1_xonly_pubkey_from_pubkey.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.POINTER(ctypes.c_int), ctypes.c_char_p] + _secp256k1.secp256k1_xonly_pubkey_serialize.restype = ctypes.c_int + _secp256k1.secp256k1_xonly_pubkey_serialize.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p] + _secp256k1.secp256k1_keypair_create.restype = ctypes.c_int + _secp256k1.secp256k1_keypair_create.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p] + _secp256k1.secp256k1_keypair_xonly_pub.restype = ctypes.c_int + _secp256k1.secp256k1_keypair_xonly_pub.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.POINTER(ctypes.c_int), ctypes.c_char_p] + _secp256k1.secp256k1_keypair_xonly_tweak_add.restype = ctypes.c_int + _secp256k1.secp256k1_keypair_xonly_tweak_add.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p] + + # Note that we check specifically for secp256k1_schnorrsig_sign_custom + # to avoid incompatibility with earlier version of libsecp256k1. + # Before secp256k1_schnorrsig_sign_custom was itroduced, + # secp256k1_schnorrsig_sign had different signature, and using it + # with this signature will result in segfault. + # Supporting older versions of libsecp256k1 will be burdensome, and given + # that currently schnorrsig module is experimental, even the current + # signature can change unexpectedly. Such is the woes of being lightweight + # in linking C libraries, as we cannot look into C headers as CFFI would + # do. We will need to wait for libsecp256k1 ABI stabilization for this + # potential problem go go away. + if getattr(_secp256k1, 'secp256k1_schnorrsig_sign_custom', None): + secp256k1_has_schnorrsig = True + _secp256k1.secp256k1_schnorrsig_verify.restype = ctypes.c_int + _secp256k1.secp256k1_schnorrsig_verify.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_int, ctypes.c_char_p] + _secp256k1.secp256k1_schnorrsig_sign.restype = ctypes.c_int + _secp256k1.secp256k1_schnorrsig_sign.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_char_p] + class _secp256k1_context: """dummy type for typecheck purposes""" @@ -266,4 +311,9 @@ def load_secp256k1_library(path: Optional[str] = None) -> ctypes.CDLL: 'SECP256K1_CONTEXT_SIGN', 'SECP256K1_CONTEXT_VERIFY', 'secp256k1_has_pubkey_recovery', + 'secp256k1_has_privkey_negate', + 'secp256k1_has_pubkey_negate', + 'secp256k1_has_ecdh', + 'secp256k1_has_xonly_pubkeys', + 'secp256k1_has_schnorrsig', ) diff --git a/bitcointx/core/serialize.py b/bitcointx/core/serialize.py index dadee66d..09774252 100644 --- a/bitcointx/core/serialize.py +++ b/bitcointx/core/serialize.py @@ -425,22 +425,6 @@ def stream_deserialize(cls, f: ByteStream_Type, **kwargs: Any) -> List[int]: return ints -class VarBytesSerializer(Serializer[bytes]): - """Serialize variable length byte strings""" - @classmethod - def stream_serialize(cls, obj: bytes, f: ByteStream_Type, - **kwargs: Any) -> None: - b = obj - datalen = len(b) - VarIntSerializer.stream_serialize(datalen, f, **kwargs) - f.write(b) - - @classmethod - def stream_deserialize(cls, f: ByteStream_Type, **kwargs: Any) -> bytes: - datalen = VarIntSerializer.stream_deserialize(f, **kwargs) - return ser_read(f, datalen) - - def uint256_from_bytes(s: bytes) -> int: """Convert bytes to uint256""" r = 0 @@ -494,7 +478,6 @@ def make_mutable(cls: Type[T_ImmutableSerializable] 'VectorSerializer', 'uint256VectorSerializer', 'intVectorSerializer', - 'VarBytesSerializer', 'uint256_from_bytes', 'uint256_to_bytes', 'uint256_to_shortstr', diff --git a/bitcointx/tests/data/bip341-wallet-test-vectors.json b/bitcointx/tests/data/bip341-wallet-test-vectors.json new file mode 100644 index 00000000..11261b00 --- /dev/null +++ b/bitcointx/tests/data/bip341-wallet-test-vectors.json @@ -0,0 +1,452 @@ +{ + "version": 1, + "scriptPubKey": [ + { + "given": { + "internalPubkey": "d6889cb081036e0faefa3a35157ad71086b123b2b144b649798b494c300a961d", + "scriptTree": null + }, + "intermediary": { + "merkleRoot": null, + "tweak": "b86e7be8f39bab32a6f2c0443abbc210f0edac0e2c53d501b36b64437d9c6c70", + "tweakedPubkey": "53a1f6e454df1aa2776a2814a721372d6258050de330b3c6d10ee8f4e0dda343" + }, + "expected": { + "scriptPubKey": "512053a1f6e454df1aa2776a2814a721372d6258050de330b3c6d10ee8f4e0dda343", + "bip350Address": "bc1p2wsldez5mud2yam29q22wgfh9439spgduvct83k3pm50fcxa5dps59h4z5" + } + }, + { + "given": { + "internalPubkey": "187791b6f712a8ea41c8ecdd0ee77fab3e85263b37e1ec18a3651926b3a6cf27", + "scriptTree": { + "id": 0, + "script": "20d85a959b0290bf19bb89ed43c916be835475d013da4b362117393e25a48229b8ac", + "leafVersion": 192 + } + }, + "intermediary": { + "leafHashes": [ + "5b75adecf53548f3ec6ad7d78383bf84cc57b55a3127c72b9a2481752dd88b21" + ], + "merkleRoot": "5b75adecf53548f3ec6ad7d78383bf84cc57b55a3127c72b9a2481752dd88b21", + "tweak": "cbd8679ba636c1110ea247542cfbd964131a6be84f873f7f3b62a777528ed001", + "tweakedPubkey": "147c9c57132f6e7ecddba9800bb0c4449251c92a1e60371ee77557b6620f3ea3" + }, + "expected": { + "scriptPubKey": "5120147c9c57132f6e7ecddba9800bb0c4449251c92a1e60371ee77557b6620f3ea3", + "bip350Address": "bc1pz37fc4cn9ah8anwm4xqqhvxygjf9rjf2resrw8h8w4tmvcs0863sa2e586", + "scriptPathControlBlocks": [ + "c1187791b6f712a8ea41c8ecdd0ee77fab3e85263b37e1ec18a3651926b3a6cf27" + ] + } + }, + { + "given": { + "internalPubkey": "93478e9488f956df2396be2ce6c5cced75f900dfa18e7dabd2428aae78451820", + "scriptTree": { + "id": 0, + "script": "20b617298552a72ade070667e86ca63b8f5789a9fe8731ef91202a91c9f3459007ac", + "leafVersion": 192 + } + }, + "intermediary": { + "leafHashes": [ + "c525714a7f49c28aedbbba78c005931a81c234b2f6c99a73e4d06082adc8bf2b" + ], + "merkleRoot": "c525714a7f49c28aedbbba78c005931a81c234b2f6c99a73e4d06082adc8bf2b", + "tweak": "6af9e28dbf9d6aaf027696e2598a5b3d056f5fd2355a7fd5a37a0e5008132d30", + "tweakedPubkey": "e4d810fd50586274face62b8a807eb9719cef49c04177cc6b76a9a4251d5450e" + }, + "expected": { + "scriptPubKey": "5120e4d810fd50586274face62b8a807eb9719cef49c04177cc6b76a9a4251d5450e", + "bip350Address": "bc1punvppl2stp38f7kwv2u2spltjuvuaayuqsthe34hd2dyy5w4g58qqfuag5", + "scriptPathControlBlocks": [ + "c093478e9488f956df2396be2ce6c5cced75f900dfa18e7dabd2428aae78451820" + ] + } + }, + { + "given": { + "internalPubkey": "ee4fe085983462a184015d1f782d6a5f8b9c2b60130aff050ce221ecf3786592", + "scriptTree": [ + { + "id": 0, + "script": "20387671353e273264c495656e27e39ba899ea8fee3bb69fb2a680e22093447d48ac", + "leafVersion": 192 + }, + { + "id": 1, + "script": "06424950333431", + "leafVersion": 250 + } + ] + }, + "intermediary": { + "leafHashes": [ + "8ad69ec7cf41c2a4001fd1f738bf1e505ce2277acdcaa63fe4765192497f47a7", + "f224a923cd0021ab202ab139cc56802ddb92dcfc172b9212261a539df79a112a" + ], + "merkleRoot": "6c2dc106ab816b73f9d07e3cd1ef2c8c1256f519748e0813e4edd2405d277bef", + "tweak": "9e0517edc8259bb3359255400b23ca9507f2a91cd1e4250ba068b4eafceba4a9", + "tweakedPubkey": "712447206d7a5238acc7ff53fbe94a3b64539ad291c7cdbc490b7577e4b17df5" + }, + "expected": { + "scriptPubKey": "5120712447206d7a5238acc7ff53fbe94a3b64539ad291c7cdbc490b7577e4b17df5", + "bip350Address": "bc1pwyjywgrd0ffr3tx8laflh6228dj98xkjj8rum0zfpd6h0e930h6saqxrrm", + "scriptPathControlBlocks": [ + "c0ee4fe085983462a184015d1f782d6a5f8b9c2b60130aff050ce221ecf3786592f224a923cd0021ab202ab139cc56802ddb92dcfc172b9212261a539df79a112a", + "faee4fe085983462a184015d1f782d6a5f8b9c2b60130aff050ce221ecf37865928ad69ec7cf41c2a4001fd1f738bf1e505ce2277acdcaa63fe4765192497f47a7" + ] + } + }, + { + "given": { + "internalPubkey": "f9f400803e683727b14f463836e1e78e1c64417638aa066919291a225f0e8dd8", + "scriptTree": [ + { + "id": 0, + "script": "2044b178d64c32c4a05cc4f4d1407268f764c940d20ce97abfd44db5c3592b72fdac", + "leafVersion": 192 + }, + { + "id": 1, + "script": "07546170726f6f74", + "leafVersion": 192 + } + ] + }, + "intermediary": { + "leafHashes": [ + "64512fecdb5afa04f98839b50e6f0cb7b1e539bf6f205f67934083cdcc3c8d89", + "2cb2b90daa543b544161530c925f285b06196940d6085ca9474d41dc3822c5cb" + ], + "merkleRoot": "ab179431c28d3b68fb798957faf5497d69c883c6fb1e1cd9f81483d87bac90cc", + "tweak": "639f0281b7ac49e742cd25b7f188657626da1ad169209078e2761cefd91fd65e", + "tweakedPubkey": "77e30a5522dd9f894c3f8b8bd4c4b2cf82ca7da8a3ea6a239655c39c050ab220" + }, + "expected": { + "scriptPubKey": "512077e30a5522dd9f894c3f8b8bd4c4b2cf82ca7da8a3ea6a239655c39c050ab220", + "bip350Address": "bc1pwl3s54fzmk0cjnpl3w9af39je7pv5ldg504x5guk2hpecpg2kgsqaqstjq", + "scriptPathControlBlocks": [ + "c1f9f400803e683727b14f463836e1e78e1c64417638aa066919291a225f0e8dd82cb2b90daa543b544161530c925f285b06196940d6085ca9474d41dc3822c5cb", + "c1f9f400803e683727b14f463836e1e78e1c64417638aa066919291a225f0e8dd864512fecdb5afa04f98839b50e6f0cb7b1e539bf6f205f67934083cdcc3c8d89" + ] + } + }, + { + "given": { + "internalPubkey": "e0dfe2300b0dd746a3f8674dfd4525623639042569d829c7f0eed9602d263e6f", + "scriptTree": [ + { + "id": 0, + "script": "2072ea6adcf1d371dea8fba1035a09f3d24ed5a059799bae114084130ee5898e69ac", + "leafVersion": 192 + }, + [ + { + "id": 1, + "script": "202352d137f2f3ab38d1eaa976758873377fa5ebb817372c71e2c542313d4abda8ac", + "leafVersion": 192 + }, + { + "id": 2, + "script": "207337c0dd4253cb86f2c43a2351aadd82cccb12a172cd120452b9bb8324f2186aac", + "leafVersion": 192 + } + ] + ] + }, + "intermediary": { + "leafHashes": [ + "2645a02e0aac1fe69d69755733a9b7621b694bb5b5cde2bbfc94066ed62b9817", + "ba982a91d4fc552163cb1c0da03676102d5b7a014304c01f0c77b2b8e888de1c", + "9e31407bffa15fefbf5090b149d53959ecdf3f62b1246780238c24501d5ceaf6" + ], + "merkleRoot": "ccbd66c6f7e8fdab47b3a486f59d28262be857f30d4773f2d5ea47f7761ce0e2", + "tweak": "b57bfa183d28eeb6ad688ddaabb265b4a41fbf68e5fed2c72c74de70d5a786f4", + "tweakedPubkey": "91b64d5324723a985170e4dc5a0f84c041804f2cd12660fa5dec09fc21783605" + }, + "expected": { + "scriptPubKey": "512091b64d5324723a985170e4dc5a0f84c041804f2cd12660fa5dec09fc21783605", + "bip350Address": "bc1pjxmy65eywgafs5tsunw95ruycpqcqnev6ynxp7jaasylcgtcxczs6n332e", + "scriptPathControlBlocks": [ + "c0e0dfe2300b0dd746a3f8674dfd4525623639042569d829c7f0eed9602d263e6fffe578e9ea769027e4f5a3de40732f75a88a6353a09d767ddeb66accef85e553", + "c0e0dfe2300b0dd746a3f8674dfd4525623639042569d829c7f0eed9602d263e6f9e31407bffa15fefbf5090b149d53959ecdf3f62b1246780238c24501d5ceaf62645a02e0aac1fe69d69755733a9b7621b694bb5b5cde2bbfc94066ed62b9817", + "c0e0dfe2300b0dd746a3f8674dfd4525623639042569d829c7f0eed9602d263e6fba982a91d4fc552163cb1c0da03676102d5b7a014304c01f0c77b2b8e888de1c2645a02e0aac1fe69d69755733a9b7621b694bb5b5cde2bbfc94066ed62b9817" + ] + } + }, + { + "given": { + "internalPubkey": "55adf4e8967fbd2e29f20ac896e60c3b0f1d5b0efa9d34941b5958c7b0a0312d", + "scriptTree": [ + { + "id": 0, + "script": "2071981521ad9fc9036687364118fb6ccd2035b96a423c59c5430e98310a11abe2ac", + "leafVersion": 192 + }, + [ + { + "id": 1, + "script": "20d5094d2dbe9b76e2c245a2b89b6006888952e2faa6a149ae318d69e520617748ac", + "leafVersion": 192 + }, + { + "id": 2, + "script": "20c440b462ad48c7a77f94cd4532d8f2119dcebbd7c9764557e62726419b08ad4cac", + "leafVersion": 192 + } + ] + ] + }, + "intermediary": { + "leafHashes": [ + "f154e8e8e17c31d3462d7132589ed29353c6fafdb884c5a6e04ea938834f0d9d", + "737ed1fe30bc42b8022d717b44f0d93516617af64a64753b7a06bf16b26cd711", + "d7485025fceb78b9ed667db36ed8b8dc7b1f0b307ac167fa516fe4352b9f4ef7" + ], + "merkleRoot": "2f6b2c5397b6d68ca18e09a3f05161668ffe93a988582d55c6f07bd5b3329def", + "tweak": "6579138e7976dc13b6a92f7bfd5a2fc7684f5ea42419d43368301470f3b74ed9", + "tweakedPubkey": "75169f4001aa68f15bbed28b218df1d0a62cbbcf1188c6665110c293c907b831" + }, + "expected": { + "scriptPubKey": "512075169f4001aa68f15bbed28b218df1d0a62cbbcf1188c6665110c293c907b831", + "bip350Address": "bc1pw5tf7sqp4f50zka7629jrr036znzew70zxyvvej3zrpf8jg8hqcssyuewe", + "scriptPathControlBlocks": [ + "c155adf4e8967fbd2e29f20ac896e60c3b0f1d5b0efa9d34941b5958c7b0a0312d3cd369a528b326bc9d2133cbd2ac21451acb31681a410434672c8e34fe757e91", + "c155adf4e8967fbd2e29f20ac896e60c3b0f1d5b0efa9d34941b5958c7b0a0312dd7485025fceb78b9ed667db36ed8b8dc7b1f0b307ac167fa516fe4352b9f4ef7f154e8e8e17c31d3462d7132589ed29353c6fafdb884c5a6e04ea938834f0d9d", + "c155adf4e8967fbd2e29f20ac896e60c3b0f1d5b0efa9d34941b5958c7b0a0312d737ed1fe30bc42b8022d717b44f0d93516617af64a64753b7a06bf16b26cd711f154e8e8e17c31d3462d7132589ed29353c6fafdb884c5a6e04ea938834f0d9d" + ] + } + } + ], + "keyPathSpending": [ + { + "given": { + "rawUnsignedTx": "02000000097de20cbff686da83a54981d2b9bab3586f4ca7e48f57f5b55963115f3b334e9c010000000000000000d7b7cab57b1393ace2d064f4d4a2cb8af6def61273e127517d44759b6dafdd990000000000fffffffff8e1f583384333689228c5d28eac13366be082dc57441760d957275419a418420000000000fffffffff0689180aa63b30cb162a73c6d2a38b7eeda2a83ece74310fda0843ad604853b0100000000feffffffaa5202bdf6d8ccd2ee0f0202afbbb7461d9264a25e5bfd3c5a52ee1239e0ba6c0000000000feffffff956149bdc66faa968eb2be2d2faa29718acbfe3941215893a2a3446d32acd050000000000000000000e664b9773b88c09c32cb70a2a3e4da0ced63b7ba3b22f848531bbb1d5d5f4c94010000000000000000e9aa6b8e6c9de67619e6a3924ae25696bb7b694bb677a632a74ef7eadfd4eabf0000000000ffffffffa778eb6a263dc090464cd125c466b5a99667720b1c110468831d058aa1b82af10100000000ffffffff0200ca9a3b000000001976a91406afd46bcdfd22ef94ac122aa11f241244a37ecc88ac807840cb0000000020ac9a87f5594be208f8532db38cff670c450ed2fea8fcdefcc9a663f78bab962b0065cd1d", + "utxosSpent": [ + { + "scriptPubKey": "512053a1f6e454df1aa2776a2814a721372d6258050de330b3c6d10ee8f4e0dda343", + "amountSats": 420000000 + }, + { + "scriptPubKey": "5120147c9c57132f6e7ecddba9800bb0c4449251c92a1e60371ee77557b6620f3ea3", + "amountSats": 462000000 + }, + { + "scriptPubKey": "76a914751e76e8199196d454941c45d1b3a323f1433bd688ac", + "amountSats": 294000000 + }, + { + "scriptPubKey": "5120e4d810fd50586274face62b8a807eb9719cef49c04177cc6b76a9a4251d5450e", + "amountSats": 504000000 + }, + { + "scriptPubKey": "512091b64d5324723a985170e4dc5a0f84c041804f2cd12660fa5dec09fc21783605", + "amountSats": 630000000 + }, + { + "scriptPubKey": "00147dd65592d0ab2fe0d0257d571abf032cd9db93dc", + "amountSats": 378000000 + }, + { + "scriptPubKey": "512075169f4001aa68f15bbed28b218df1d0a62cbbcf1188c6665110c293c907b831", + "amountSats": 672000000 + }, + { + "scriptPubKey": "5120712447206d7a5238acc7ff53fbe94a3b64539ad291c7cdbc490b7577e4b17df5", + "amountSats": 546000000 + }, + { + "scriptPubKey": "512077e30a5522dd9f894c3f8b8bd4c4b2cf82ca7da8a3ea6a239655c39c050ab220", + "amountSats": 588000000 + } + ] + }, + "intermediary": { + "hashAmounts": "58a6964a4f5f8f0b642ded0a8a553be7622a719da71d1f5befcefcdee8e0fde6", + "hashOutputs": "a2e6dab7c1f0dcd297c8d61647fd17d821541ea69c3cc37dcbad7f90d4eb4bc5", + "hashPrevouts": "e3b33bb4ef3a52ad1fffb555c0d82828eb22737036eaeb02a235d82b909c4c3f", + "hashScriptPubkeys": "23ad0f61ad2bca5ba6a7693f50fce988e17c3780bf2b1e720cfbb38fbdd52e21", + "hashSequences": "18959c7221ab5ce9e26c3cd67b22c24f8baa54bac281d8e6b05e400e6c3a957e" + }, + "inputSpending": [ + { + "given": { + "txinIndex": 0, + "internalPrivkey": "6b973d88838f27366ed61c9ad6367663045cb456e28335c109e30717ae0c6baa", + "merkleRoot": null, + "hashType": 3 + }, + "intermediary": { + "internalPubkey": "d6889cb081036e0faefa3a35157ad71086b123b2b144b649798b494c300a961d", + "tweak": "b86e7be8f39bab32a6f2c0443abbc210f0edac0e2c53d501b36b64437d9c6c70", + "tweakedPrivkey": "2405b971772ad26915c8dcdf10f238753a9b837e5f8e6a86fd7c0cce5b7296d9", + "sigMsg": "0003020000000065cd1de3b33bb4ef3a52ad1fffb555c0d82828eb22737036eaeb02a235d82b909c4c3f58a6964a4f5f8f0b642ded0a8a553be7622a719da71d1f5befcefcdee8e0fde623ad0f61ad2bca5ba6a7693f50fce988e17c3780bf2b1e720cfbb38fbdd52e2118959c7221ab5ce9e26c3cd67b22c24f8baa54bac281d8e6b05e400e6c3a957e0000000000d0418f0e9a36245b9a50ec87f8bf5be5bcae434337b87139c3a5b1f56e33cba0", + "precomputedUsed": [ + "hashAmounts", + "hashPrevouts", + "hashScriptPubkeys", + "hashSequences" + ], + "sigHash": "2514a6272f85cfa0f45eb907fcb0d121b808ed37c6ea160a5a9046ed5526d555" + }, + "expected": { + "witness": [ + "ed7c1647cb97379e76892be0cacff57ec4a7102aa24296ca39af7541246d8ff14d38958d4cc1e2e478e4d4a764bbfd835b16d4e314b72937b29833060b87276c03" + ] + } + }, + { + "given": { + "txinIndex": 1, + "internalPrivkey": "1e4da49f6aaf4e5cd175fe08a32bb5cb4863d963921255f33d3bc31e1343907f", + "merkleRoot": "5b75adecf53548f3ec6ad7d78383bf84cc57b55a3127c72b9a2481752dd88b21", + "hashType": 131 + }, + "intermediary": { + "internalPubkey": "187791b6f712a8ea41c8ecdd0ee77fab3e85263b37e1ec18a3651926b3a6cf27", + "tweak": "cbd8679ba636c1110ea247542cfbd964131a6be84f873f7f3b62a777528ed001", + "tweakedPrivkey": "ea260c3b10e60f6de018455cd0278f2f5b7e454be1999572789e6a9565d26080", + "sigMsg": "0083020000000065cd1d00d7b7cab57b1393ace2d064f4d4a2cb8af6def61273e127517d44759b6dafdd9900000000808f891b00000000225120147c9c57132f6e7ecddba9800bb0c4449251c92a1e60371ee77557b6620f3ea3ffffffffffcef8fb4ca7efc5433f591ecfc57391811ce1e186a3793024def5c884cba51d", + "precomputedUsed": [], + "sigHash": "325a644af47e8a5a2591cda0ab0723978537318f10e6a63d4eed783b96a71a4d" + }, + "expected": { + "witness": [ + "052aedffc554b41f52b521071793a6b88d6dbca9dba94cf34c83696de0c1ec35ca9c5ed4ab28059bd606a4f3a657eec0bb96661d42921b5f50a95ad33675b54f83" + ] + } + }, + { + "given": { + "txinIndex": 3, + "internalPrivkey": "d3c7af07da2d54f7a7735d3d0fc4f0a73164db638b2f2f7c43f711f6d4aa7e64", + "merkleRoot": "c525714a7f49c28aedbbba78c005931a81c234b2f6c99a73e4d06082adc8bf2b", + "hashType": 1 + }, + "intermediary": { + "internalPubkey": "93478e9488f956df2396be2ce6c5cced75f900dfa18e7dabd2428aae78451820", + "tweak": "6af9e28dbf9d6aaf027696e2598a5b3d056f5fd2355a7fd5a37a0e5008132d30", + "tweakedPrivkey": "97323385e57015b75b0339a549c56a948eb961555973f0951f555ae6039ef00d", + "sigMsg": "0001020000000065cd1de3b33bb4ef3a52ad1fffb555c0d82828eb22737036eaeb02a235d82b909c4c3f58a6964a4f5f8f0b642ded0a8a553be7622a719da71d1f5befcefcdee8e0fde623ad0f61ad2bca5ba6a7693f50fce988e17c3780bf2b1e720cfbb38fbdd52e2118959c7221ab5ce9e26c3cd67b22c24f8baa54bac281d8e6b05e400e6c3a957ea2e6dab7c1f0dcd297c8d61647fd17d821541ea69c3cc37dcbad7f90d4eb4bc50003000000", + "precomputedUsed": [ + "hashAmounts", + "hashOutputs", + "hashPrevouts", + "hashScriptPubkeys", + "hashSequences" + ], + "sigHash": "bf013ea93474aa67815b1b6cc441d23b64fa310911d991e713cd34c7f5d46669" + }, + "expected": { + "witness": [ + "ff45f742a876139946a149ab4d9185574b98dc919d2eb6754f8abaa59d18b025637a3aa043b91817739554f4ed2026cf8022dbd83e351ce1fabc272841d2510a01" + ] + } + }, + { + "given": { + "txinIndex": 4, + "internalPrivkey": "f36bb07a11e469ce941d16b63b11b9b9120a84d9d87cff2c84a8d4affb438f4e", + "merkleRoot": "ccbd66c6f7e8fdab47b3a486f59d28262be857f30d4773f2d5ea47f7761ce0e2", + "hashType": 0 + }, + "intermediary": { + "internalPubkey": "e0dfe2300b0dd746a3f8674dfd4525623639042569d829c7f0eed9602d263e6f", + "tweak": "b57bfa183d28eeb6ad688ddaabb265b4a41fbf68e5fed2c72c74de70d5a786f4", + "tweakedPrivkey": "a8e7aa924f0d58854185a490e6c41f6efb7b675c0f3331b7f14b549400b4d501", + "sigMsg": "0000020000000065cd1de3b33bb4ef3a52ad1fffb555c0d82828eb22737036eaeb02a235d82b909c4c3f58a6964a4f5f8f0b642ded0a8a553be7622a719da71d1f5befcefcdee8e0fde623ad0f61ad2bca5ba6a7693f50fce988e17c3780bf2b1e720cfbb38fbdd52e2118959c7221ab5ce9e26c3cd67b22c24f8baa54bac281d8e6b05e400e6c3a957ea2e6dab7c1f0dcd297c8d61647fd17d821541ea69c3cc37dcbad7f90d4eb4bc50004000000", + "precomputedUsed": [ + "hashAmounts", + "hashOutputs", + "hashPrevouts", + "hashScriptPubkeys", + "hashSequences" + ], + "sigHash": "4f900a0bae3f1446fd48490c2958b5a023228f01661cda3496a11da502a7f7ef" + }, + "expected": { + "witness": [ + "b4010dd48a617db09926f729e79c33ae0b4e94b79f04a1ae93ede6315eb3669de185a17d2b0ac9ee09fd4c64b678a0b61a0a86fa888a273c8511be83bfd6810f" + ] + } + }, + { + "given": { + "txinIndex": 6, + "internalPrivkey": "415cfe9c15d9cea27d8104d5517c06e9de48e2f986b695e4f5ffebf230e725d8", + "merkleRoot": "2f6b2c5397b6d68ca18e09a3f05161668ffe93a988582d55c6f07bd5b3329def", + "hashType": 2 + }, + "intermediary": { + "internalPubkey": "55adf4e8967fbd2e29f20ac896e60c3b0f1d5b0efa9d34941b5958c7b0a0312d", + "tweak": "6579138e7976dc13b6a92f7bfd5a2fc7684f5ea42419d43368301470f3b74ed9", + "tweakedPrivkey": "241c14f2639d0d7139282aa6abde28dd8a067baa9d633e4e7230287ec2d02901", + "sigMsg": "0002020000000065cd1de3b33bb4ef3a52ad1fffb555c0d82828eb22737036eaeb02a235d82b909c4c3f58a6964a4f5f8f0b642ded0a8a553be7622a719da71d1f5befcefcdee8e0fde623ad0f61ad2bca5ba6a7693f50fce988e17c3780bf2b1e720cfbb38fbdd52e2118959c7221ab5ce9e26c3cd67b22c24f8baa54bac281d8e6b05e400e6c3a957e0006000000", + "precomputedUsed": [ + "hashAmounts", + "hashPrevouts", + "hashScriptPubkeys", + "hashSequences" + ], + "sigHash": "15f25c298eb5cdc7eb1d638dd2d45c97c4c59dcaec6679cfc16ad84f30876b85" + }, + "expected": { + "witness": [ + "a3785919a2ce3c4ce26f298c3d51619bc474ae24014bcdd31328cd8cfbab2eff3395fa0a16fe5f486d12f22a9cedded5ae74feb4bbe5351346508c5405bcfee002" + ] + } + }, + { + "given": { + "txinIndex": 7, + "internalPrivkey": "c7b0e81f0a9a0b0499e112279d718cca98e79a12e2f137c72ae5b213aad0d103", + "merkleRoot": "6c2dc106ab816b73f9d07e3cd1ef2c8c1256f519748e0813e4edd2405d277bef", + "hashType": 130 + }, + "intermediary": { + "internalPubkey": "ee4fe085983462a184015d1f782d6a5f8b9c2b60130aff050ce221ecf3786592", + "tweak": "9e0517edc8259bb3359255400b23ca9507f2a91cd1e4250ba068b4eafceba4a9", + "tweakedPrivkey": "65b6000cd2bfa6b7cf736767a8955760e62b6649058cbc970b7c0871d786346b", + "sigMsg": "0082020000000065cd1d00e9aa6b8e6c9de67619e6a3924ae25696bb7b694bb677a632a74ef7eadfd4eabf00000000804c8b2000000000225120712447206d7a5238acc7ff53fbe94a3b64539ad291c7cdbc490b7577e4b17df5ffffffff", + "precomputedUsed": [], + "sigHash": "cd292de50313804dabe4685e83f923d2969577191a3e1d2882220dca88cbeb10" + }, + "expected": { + "witness": [ + "ea0c6ba90763c2d3a296ad82ba45881abb4f426b3f87af162dd24d5109edc1cdd11915095ba47c3a9963dc1e6c432939872bc49212fe34c632cd3ab9fed429c482" + ] + } + }, + { + "given": { + "txinIndex": 8, + "internalPrivkey": "77863416be0d0665e517e1c375fd6f75839544eca553675ef7fdf4949518ebaa", + "merkleRoot": "ab179431c28d3b68fb798957faf5497d69c883c6fb1e1cd9f81483d87bac90cc", + "hashType": 129 + }, + "intermediary": { + "internalPubkey": "f9f400803e683727b14f463836e1e78e1c64417638aa066919291a225f0e8dd8", + "tweak": "639f0281b7ac49e742cd25b7f188657626da1ad169209078e2761cefd91fd65e", + "tweakedPrivkey": "ec18ce6af99f43815db543f47b8af5ff5df3b2cb7315c955aa4a86e8143d2bf5", + "sigMsg": "0081020000000065cd1da2e6dab7c1f0dcd297c8d61647fd17d821541ea69c3cc37dcbad7f90d4eb4bc500a778eb6a263dc090464cd125c466b5a99667720b1c110468831d058aa1b82af101000000002b0c230000000022512077e30a5522dd9f894c3f8b8bd4c4b2cf82ca7da8a3ea6a239655c39c050ab220ffffffff", + "precomputedUsed": [ + "hashOutputs" + ], + "sigHash": "cccb739eca6c13a8a89e6e5cd317ffe55669bbda23f2fd37b0f18755e008edd2" + }, + "expected": { + "witness": [ + "bbc9584a11074e83bc8c6759ec55401f0ae7b03ef290c3139814f545b58a9f8127258000874f44bc46db7646322107d4d86aec8e73b8719a61fff761d75b5dd981" + ] + } + } + ], + "auxiliary": { + "fullySignedTx": "020000000001097de20cbff686da83a54981d2b9bab3586f4ca7e48f57f5b55963115f3b334e9c010000000000000000d7b7cab57b1393ace2d064f4d4a2cb8af6def61273e127517d44759b6dafdd990000000000fffffffff8e1f583384333689228c5d28eac13366be082dc57441760d957275419a41842000000006b4830450221008f3b8f8f0537c420654d2283673a761b7ee2ea3c130753103e08ce79201cf32a022079e7ab904a1980ef1c5890b648c8783f4d10103dd62f740d13daa79e298d50c201210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798fffffffff0689180aa63b30cb162a73c6d2a38b7eeda2a83ece74310fda0843ad604853b0100000000feffffffaa5202bdf6d8ccd2ee0f0202afbbb7461d9264a25e5bfd3c5a52ee1239e0ba6c0000000000feffffff956149bdc66faa968eb2be2d2faa29718acbfe3941215893a2a3446d32acd050000000000000000000e664b9773b88c09c32cb70a2a3e4da0ced63b7ba3b22f848531bbb1d5d5f4c94010000000000000000e9aa6b8e6c9de67619e6a3924ae25696bb7b694bb677a632a74ef7eadfd4eabf0000000000ffffffffa778eb6a263dc090464cd125c466b5a99667720b1c110468831d058aa1b82af10100000000ffffffff0200ca9a3b000000001976a91406afd46bcdfd22ef94ac122aa11f241244a37ecc88ac807840cb0000000020ac9a87f5594be208f8532db38cff670c450ed2fea8fcdefcc9a663f78bab962b0141ed7c1647cb97379e76892be0cacff57ec4a7102aa24296ca39af7541246d8ff14d38958d4cc1e2e478e4d4a764bbfd835b16d4e314b72937b29833060b87276c030141052aedffc554b41f52b521071793a6b88d6dbca9dba94cf34c83696de0c1ec35ca9c5ed4ab28059bd606a4f3a657eec0bb96661d42921b5f50a95ad33675b54f83000141ff45f742a876139946a149ab4d9185574b98dc919d2eb6754f8abaa59d18b025637a3aa043b91817739554f4ed2026cf8022dbd83e351ce1fabc272841d2510a010140b4010dd48a617db09926f729e79c33ae0b4e94b79f04a1ae93ede6315eb3669de185a17d2b0ac9ee09fd4c64b678a0b61a0a86fa888a273c8511be83bfd6810f0247304402202b795e4de72646d76eab3f0ab27dfa30b810e856ff3a46c9a702df53bb0d8cc302203ccc4d822edab5f35caddb10af1be93583526ccfbade4b4ead350781e2f8adcd012102f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f90141a3785919a2ce3c4ce26f298c3d51619bc474ae24014bcdd31328cd8cfbab2eff3395fa0a16fe5f486d12f22a9cedded5ae74feb4bbe5351346508c5405bcfee0020141ea0c6ba90763c2d3a296ad82ba45881abb4f426b3f87af162dd24d5109edc1cdd11915095ba47c3a9963dc1e6c432939872bc49212fe34c632cd3ab9fed429c4820141bbc9584a11074e83bc8c6759ec55401f0ae7b03ef290c3139814f545b58a9f8127258000874f44bc46db7646322107d4d86aec8e73b8719a61fff761d75b5dd9810065cd1d" + } + } + ] +} diff --git a/bitcointx/tests/data/schnorr-sig-test-vectors.csv b/bitcointx/tests/data/schnorr-sig-test-vectors.csv new file mode 100644 index 00000000..a1a63e12 --- /dev/null +++ b/bitcointx/tests/data/schnorr-sig-test-vectors.csv @@ -0,0 +1,16 @@ +index,secret key,public key,aux_rand,message,signature,verification result,comment +0,0000000000000000000000000000000000000000000000000000000000000003,F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9,0000000000000000000000000000000000000000000000000000000000000000,0000000000000000000000000000000000000000000000000000000000000000,E907831F80848D1069A5371B402410364BDF1C5F8307B0084C55F1CE2DCA821525F66A4A85EA8B71E482A74F382D2CE5EBEEE8FDB2172F477DF4900D310536C0,TRUE, +1,B7E151628AED2A6ABF7158809CF4F3C762E7160F38B4DA56A784D9045190CFEF,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,0000000000000000000000000000000000000000000000000000000000000001,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,6896BD60EEAE296DB48A229FF71DFE071BDE413E6D43F917DC8DCF8C78DE33418906D11AC976ABCCB20B091292BFF4EA897EFCB639EA871CFA95F6DE339E4B0A,TRUE, +2,C90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B14E5C9,DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EB8,C87AA53824B4D7AE2EB035A2B5BBBCCC080E76CDC6D1692C4B0B62D798E6D906,7E2D58D8B3BCDF1ABADEC7829054F90DDA9805AAB56C77333024B9D0A508B75C,5831AAEED7B44BB74E5EAB94BA9D4294C49BCF2A60728D8B4C200F50DD313C1BAB745879A5AD954A72C45A91C3A51D3C7ADEA98D82F8481E0E1E03674A6F3FB7,TRUE, +3,0B432B2677937381AEF05BB02A66ECD012773062CF3FA2549E44F58ED2401710,25D1DFF95105F5253C4022F628A996AD3A0D95FBF21D468A1B33F8C160D8F517,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF,7EB0509757E246F19449885651611CB965ECC1A187DD51B64FDA1EDC9637D5EC97582B9CB13DB3933705B32BA982AF5AF25FD78881EBB32771FC5922EFC66EA3,TRUE,test fails if msg is reduced modulo p or n +4,,D69C3509BB99E412E68B0FE8544E72837DFA30746D8BE2AA65975F29D22DC7B9,,4DF3C3F68FCC83B27E9D42C90431A72499F17875C81A599B566C9889B9696703,00000000000000000000003B78CE563F89A0ED9414F5AA28AD0D96D6795F9C6376AFB1548AF603B3EB45C9F8207DEE1060CB71C04E80F593060B07D28308D7F4,TRUE, +5,,EEFDEA4CDB677750A420FEE807EACF21EB9898AE79B9768766E4FAA04A2D4A34,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E17776969E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B,FALSE,public key not on the curve +6,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,FFF97BD5755EEEA420453A14355235D382F6472F8568A18B2F057A14602975563CC27944640AC607CD107AE10923D9EF7A73C643E166BE5EBEAFA34B1AC553E2,FALSE,has_even_y(R) is false +7,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,1FA62E331EDBC21C394792D2AB1100A7B432B013DF3F6FF4F99FCB33E0E1515F28890B3EDB6E7189B630448B515CE4F8622A954CFE545735AAEA5134FCCDB2BD,FALSE,negated message +8,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E177769961764B3AA9B2FFCB6EF947B6887A226E8D7C93E00C5ED0C1834FF0D0C2E6DA6,FALSE,negated s value +9,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,0000000000000000000000000000000000000000000000000000000000000000123DDA8328AF9C23A94C1FEECFD123BA4FB73476F0D594DCB65C6425BD186051,FALSE,sG - eP is infinite. Test fails in single verification if has_even_y(inf) is defined as true and x(inf) as 0 +10,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,00000000000000000000000000000000000000000000000000000000000000017615FBAF5AE28864013C099742DEADB4DBA87F11AC6754F93780D5A1837CF197,FALSE,sG - eP is infinite. Test fails in single verification if has_even_y(inf) is defined as true and x(inf) as 1 +11,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,4A298DACAE57395A15D0795DDBFD1DCB564DA82B0F269BC70A74F8220429BA1D69E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B,FALSE,sig[0:32] is not an X coordinate on the curve +12,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F69E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B,FALSE,sig[0:32] is equal to field size +13,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E177769FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141,FALSE,sig[32:64] is equal to curve order +14,,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC30,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E17776969E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B,FALSE,public key is not a valid X coordinate because it exceeds the field size diff --git a/bitcointx/tests/test_key.py b/bitcointx/tests/test_key.py index c2fcf06b..486b241e 100644 --- a/bitcointx/tests/test_key.py +++ b/bitcointx/tests/test_key.py @@ -11,11 +11,15 @@ # pylama:ignore=E501 +import os +import csv import unittest import warnings import hashlib -from bitcointx.core.key import CKey, CPubKey +import bitcointx.util + +from bitcointx.core.key import CKey, CPubKey, XOnlyPubKey from bitcointx.core import x from bitcointx.core.secp256k1 import secp256k1_has_pubkey_negate @@ -158,3 +162,39 @@ def test_signature_grind(self) -> None: small_sig_found = True self.assertTrue(small_sig_found) + + def test_schnorr(self) -> None: + if not bitcointx.util._allow_secp256k1_experimental_modules: + self.skipTest("secp256k1 experimental modules are not available") + # adapted from reference code of BIP340 + # at https://github.com/bitcoin/bips/blob/master/bip-0340/reference.py + with open(os.path.dirname(__file__) + '/data/schnorr-sig-test-vectors.csv', 'r') as fd: + reader = csv.reader(fd) + reader.__next__() + for row in reader: + ( + _testcase_idx, + seckey_hex, pubkey_hex, aux_rand_hex, msg_hex, sig_hex, + result_str, _comment + ) = row + + pubkey = XOnlyPubKey(x(pubkey_hex)) + msg = x(msg_hex) + assert len(msg) == 32 + sig = x(sig_hex) + result = (result_str == 'TRUE') + + if seckey_hex != '': + seckey = CKey(x(seckey_hex)) + pubkey_actual = seckey.xonly_pub + self.assertEqual(pubkey, pubkey_actual) + aux_rand = x(aux_rand_hex) + sig_actual = seckey.sign_schnorr_no_tweak( + msg, aux=aux_rand) + self.assertEqual(sig, sig_actual) + if pubkey.is_fullyvalid(): + result_actual = pubkey.verify_schnorr(msg, sig) + else: + result_actual = False + + self.assertEqual(result, result_actual) diff --git a/bitcointx/tests/test_scripteval.py b/bitcointx/tests/test_scripteval.py index 569c0157..8772ab6b 100644 --- a/bitcointx/tests/test_scripteval.py +++ b/bitcointx/tests/test_scripteval.py @@ -17,20 +17,24 @@ import unittest import warnings import ctypes +import random -from typing import List, Iterator, Tuple, Set, Optional +from typing import List, Iterator, Tuple, Set, Optional, Sequence, Dict, Union from binascii import unhexlify +import bitcointx.util + from bitcointx.core import ( - x, ValidationError, + coins_to_satoshi, x, ValidationError, CTxOut, CTxIn, CTransaction, COutPoint, CTxWitness, CTxInWitness ) -from bitcointx.core.key import CKey +from bitcointx.core.key import CKey, tap_tweak_pubkey from bitcointx.core.script import ( OPCODES_BY_NAME, CScript, CScriptWitness, - OP_0, SIGHASH_ALL, SIGVERSION_BASE, SIGVERSION_WITNESS_V0, + OP_0, SIGHASH_ALL, SIGVERSION_BASE, SIGVERSION_WITNESS_V0, OP_CHECKSIG, standard_multisig_redeem_script, standard_multisig_witness_stack, + TaprootScriptTree, TaprootScriptTreeLeaf_Type, SignatureHashSchnorr ) from bitcointx.core.scripteval import ( VerifyScript, SCRIPT_VERIFY_FLAGS_BY_NAME, SCRIPT_VERIFY_P2SH, @@ -40,6 +44,13 @@ ConsensusVerifyScript, BITCOINCONSENSUS_ACCEPTED_FLAGS, load_bitcoinconsensus_library ) +from bitcointx.wallet import P2TRCoinAddress, CCoinKey + +TestDataIterator = Iterator[ + Tuple[CScript, CScript, CScript, CScriptWitness, int, + Optional[Sequence[CTxOut]], Set[ScriptVerifyFlag_Type], + str, str, str] +] def parse_script(s: str) -> CScript: @@ -71,12 +82,7 @@ def ishex(s: str) -> bool: return CScript(b''.join(r)) -def load_test_vectors( - name: str, skip_fixme: bool = True -) -> Iterator[ - Tuple[CScript, CScript, CScriptWitness, int, Set[ScriptVerifyFlag_Type], - str, str, str] -]: +def load_test_vectors(name: str, skip_fixme: bool = True) -> TestDataIterator: with open(os.path.dirname(__file__) + '/data/' + name, 'r') as fd: fixme_comment = None num_skipped = 0 @@ -136,7 +142,7 @@ def load_test_vectors( flag_set.add(flag) - yield (scriptSig, scriptPubKey, witness, nValue, + yield (scriptSig, scriptPubKey, CScript(), witness, nValue, None, flag_set, expected_result, comment, test_case) if fixme_comment is not None: @@ -144,25 +150,158 @@ def load_test_vectors( class Test_EvalScript(unittest.TestCase): + + def setUp(self) -> None: + try: + handle = load_bitcoinconsensus_library() + except ImportError: + warnings.warn( + "libbitcoinconsensus library is not avaliable, " + "not testing bitcoinconsensus module and taproot scripts") + handle = None + + self._bitcoinconsensus_handle = handle + + # IMPORANT: The code inside this function (TaprootScriptTree functionality) + # is what actually being tested here, not the libbitcoinconsensus + # interface, so this should not be changed into something that just + # loads data from a json file. + def generate_taproot_test_scripts(self) -> TestDataIterator: + k = CCoinKey.from_secret_bytes(os.urandom(32)) + + xopub = k.xonly_pub + + spk = P2TRCoinAddress.from_xonly_pubkey(xopub).to_scriptPubKey() + nValue = 100 + (txCredit, txSpend) = self.create_test_txs( + CScript(), spk, spk, CScriptWitness(), nValue) + sh = SignatureHashSchnorr(txSpend, 0, txCredit.vout) + sig = k.sign_schnorr_tweaked(sh) + yield (CScript(), spk, spk, CScriptWitness([sig]), nValue, + txCredit.vout, BITCOINCONSENSUS_ACCEPTED_FLAGS, + 'OK', '', 'simple taproot spend') + + random_tweak = os.urandom(32) + + tt_res = tap_tweak_pubkey(xopub, merkle_root=random_tweak) + assert tt_res is not None + rnd_twpub, _ = tt_res + + t = TaprootScriptTree( + [CScript([rnd_twpub, OP_CHECKSIG], name='simplespend')]) + t.set_internal_pubkey(xopub) + + swcb = t.get_script_with_control_block('simplespend') + assert swcb is not None + s, cb = swcb + spk = P2TRCoinAddress.from_script_tree(t).to_scriptPubKey() + (txCredit, txSpend) = self.create_test_txs( + CScript(), spk, spk, CScriptWitness(), nValue) + sh = s.sighash_schnorr(txSpend, 0, txCredit.vout) + sig = k.sign_schnorr_tweaked(sh, merkle_root=random_tweak) + + yield (CScript(), spk, spk, CScriptWitness([sig, s, cb]), nValue, + txCredit.vout, BITCOINCONSENSUS_ACCEPTED_FLAGS, + 'OK', '', 'simple taproot script spend') + + def gen_leaves(num_leaves: int, prefix: str = '' + ) -> Tuple[List[TaprootScriptTreeLeaf_Type], + Dict[str, bytes]]: + leaves: List[TaprootScriptTreeLeaf_Type] = [] + tweaks = {} + for leaf_idx in range(num_leaves): + tw = os.urandom(32) + tt_res = tap_tweak_pubkey(xopub, merkle_root=tw) + assert tt_res is not None + twpub, _ = tt_res + sname = f'{prefix}leaf_{leaf_idx}' + tweaks[sname] = tw + leaves.append(CScript([twpub, OP_CHECKSIG], name=sname)) + + return leaves, tweaks + + def yield_leaves(leaves: Sequence[Union[CScript, TaprootScriptTree]], + tweaks: Dict[str, bytes]) -> TestDataIterator: + t = TaprootScriptTree(leaves, internal_pubkey=xopub) + + spk = P2TRCoinAddress.from_script_tree(t).to_scriptPubKey() + + for sname, tweak in tweaks.items(): + nValue = random.randint(1, coins_to_satoshi(21000000)) + swcb = t.get_script_with_control_block(sname) + assert swcb is not None + s, cb = swcb + (txCredit, txSpend) = self.create_test_txs( + CScript(), spk, spk, CScriptWitness(), nValue) + sh = s.sighash_schnorr(txSpend, 0, txCredit.vout) + sig = k.sign_schnorr_tweaked(sh, merkle_root=tweak) + + yield (CScript(), spk, spk, CScriptWitness([sig, s, cb]), nValue, + txCredit.vout, BITCOINCONSENSUS_ACCEPTED_FLAGS, + 'OK', '', f'taproot script spend leaf {sname}') + + # Test simple balanced tree + for num_leaves in range(1, 12): + leaves, tweaks = gen_leaves(num_leaves) + for data in yield_leaves(leaves, tweaks): + yield data + + leaves, tweaks = gen_leaves(7) + for data in yield_leaves(leaves, tweaks): + yield data + + # Test un-balanced geterogenous tree + lvdict: Dict[str, List[TaprootScriptTreeLeaf_Type]] = {} + scripts = {} + for num_scripts, pfx in ((5, 'level1'), (7, 'level2a'), (6, 'level2b'), + (8, 'level3a'), (11, 'level3b'), + (3, 'level3c')): + + lvdict[pfx], new_scripts = gen_leaves(num_scripts, pfx + '/') + scripts.update(new_scripts) + + l2a = (lvdict['level2a'][:3] + + [TaprootScriptTree(lvdict['level3a'][:4])] + + [lvdict['level2a'][3]] + + [TaprootScriptTree(lvdict['level3a'][4:])] + + lvdict['level2a'][4:] + + [TaprootScriptTree(lvdict['level3b'][:10])] + + [TaprootScriptTree([lvdict['level3b'][10]])]) + + l2b: List[TaprootScriptTreeLeaf_Type] + l2b = [TaprootScriptTree(lvdict['level3c'])] + l2b.extend(lvdict['level2b']) + + TaprootScriptTree(l2b) + + l1 = (lvdict['level1'][:1] + [TaprootScriptTree(l2a)] + + lvdict['level1'][1:2] + [TaprootScriptTree(l2b)] + + lvdict['level1'][2:]) + + for data in yield_leaves(l1, scripts): + yield data + def create_test_txs( self, scriptSig: CScript, scriptPubKey: CScript, - witness: CScriptWitness, nValue: int + dst_scriptPubKey: CScript, witness: CScriptWitness, nValue: int ) -> Tuple[CTransaction, CTransaction]: txCredit = CTransaction([CTxIn(COutPoint(), CScript([OP_0, OP_0]), nSequence=0xFFFFFFFF)], [CTxOut(nValue, scriptPubKey)], witness=CTxWitness(), nLockTime=0, nVersion=1) + txSpend = CTransaction([CTxIn(COutPoint(txCredit.GetTxid(), 0), scriptSig, nSequence=0xFFFFFFFF)], - [CTxOut(nValue, CScript())], + [CTxOut(nValue, dst_scriptPubKey)], nLockTime=0, nVersion=1, witness=CTxWitness([CTxInWitness(witness)])) return (txCredit, txSpend) def test_script(self) -> None: for t in load_test_vectors('script_tests.json'): - (scriptSig, scriptPubKey, witness, nValue, - flags, expected_result, comment, test_case) = t - (txCredit, txSpend) = self.create_test_txs(scriptSig, scriptPubKey, witness, nValue) + (scriptSig, scriptPubKey, dst_scriptPubKey, witness, nValue, + spent_outputs, flags, expected_result, comment, test_case) = t + (txCredit, txSpend) = self.create_test_txs( + scriptSig, scriptPubKey, dst_scriptPubKey, witness, nValue) try: VerifyScript(scriptSig, scriptPubKey, txSpend, 0, flags, amount=nValue, witness=witness) @@ -174,38 +313,56 @@ def test_script(self) -> None: if expected_result != 'OK': self.fail('Expected %r to fail (%s)' % (test_case, expected_result)) - def test_script_bitcoinconsensus(self) -> None: - try: - handle = load_bitcoinconsensus_library() - except ImportError: - warnings.warn("libbitcoinconsensus library is not avaliable, not testing bitcoinconsensus module") - return + def _do_test_bicoinconsensus( + self, handle: Optional[ctypes.CDLL], + test_data_iterator: TestDataIterator + ) -> None: + for t in test_data_iterator: + (scriptSig, scriptPubKey, dst_scriptPubKey, witness, nValue, spent_outputs, + flags, expected_result, comment, test_case) = t - def do_test_bicoinconsensus(handle: Optional[ctypes.CDLL]) -> None: - for t in load_test_vectors('script_tests.json', skip_fixme=False): - (scriptSig, scriptPubKey, witness, nValue, - flags, expected_result, comment, test_case) = t - (txCredit, txSpend) = self.create_test_txs(scriptSig, scriptPubKey, witness, nValue) + (txCredit, txSpend) = self.create_test_txs( + scriptSig, scriptPubKey, dst_scriptPubKey, witness, nValue) - libconsensus_flags = (flags & BITCOINCONSENSUS_ACCEPTED_FLAGS) - if flags != libconsensus_flags: - continue + libconsensus_flags = (flags & BITCOINCONSENSUS_ACCEPTED_FLAGS) + if flags != libconsensus_flags: + continue - try: - ConsensusVerifyScript(scriptSig, scriptPubKey, txSpend, 0, - libconsensus_flags, amount=nValue, - witness=witness, - consensus_library_hanlde=handle) - except ValidationError as err: - if expected_result == 'OK': - self.fail('Script FAILED: %r %r %r with exception %r\n\nTest data: %r' % (scriptSig, scriptPubKey, comment, err, test_case)) - continue + try: + ConsensusVerifyScript(scriptSig, scriptPubKey, txSpend, 0, + libconsensus_flags, amount=nValue, + witness=witness, + spent_outputs=spent_outputs, + consensus_library_hanlde=handle) + except ValidationError as err: + if expected_result == 'OK': + self.fail('Script FAILED: %r %r %r with exception %r\n\nTest data: %r' % (scriptSig, scriptPubKey, comment, err, test_case)) + continue - if expected_result != 'OK': - self.fail('Expected %r to fail (%s)' % (test_case, expected_result)) + if expected_result != 'OK': + self.fail('Expected %r to fail (%s)' % (test_case, expected_result)) - do_test_bicoinconsensus(handle) # test with supplied handle - do_test_bicoinconsensus(None) # test with default-loaded handle + def test_script_bitcoinconsensus(self) -> None: + if not self._bitcoinconsensus_handle: + self.skipTest("bitcoinconsensus library is not available") + + test_data_iterator = load_test_vectors('script_tests.json', + skip_fixme=False) + # test with supplied handle + self._do_test_bicoinconsensus(self._bitcoinconsensus_handle, + test_data_iterator) + # test with default-loaded handle + self._do_test_bicoinconsensus(None, test_data_iterator) + + @unittest.skipIf( + not bitcointx.util._allow_secp256k1_experimental_modules, + "secp256k1_experimental_module is not available or not enabled" + ) + def test_script_bitcoinconsensus_taproot_scripts(self) -> None: + if not self._bitcoinconsensus_handle: + self.skipTest("bitcoinconsensus library is not available") + # disabled until libbitcoinconsensus can handle taproot scripts + # self._do_test_bicoinconsensus(self._bitcoinconsensus_handle, self.generate_taproot_test_scripts()) def test_p2sh_redeemscript(self) -> None: def T(required: int, total: int, alt_total: Optional[int] = None) -> None: @@ -224,7 +381,7 @@ def T(required: int, total: int, alt_total: Optional[int] = None) -> None: scriptPubKey = redeem_script.to_p2sh_scriptPubKey() - (_, tx) = self.create_test_txs(CScript(), scriptPubKey, + (_, tx) = self.create_test_txs(CScript(), scriptPubKey, CScript(), CScriptWitness([]), amount) tx = tx.to_mutable() @@ -246,7 +403,7 @@ def T(required: int, total: int, alt_total: Optional[int] = None) -> None: scriptPubKey = redeem_script.to_p2wsh_scriptPubKey() - (_, tx) = self.create_test_txs(CScript(), scriptPubKey, + (_, tx) = self.create_test_txs(CScript(), scriptPubKey, CScript(), CScriptWitness([]), amount) tx = tx.to_mutable() @@ -271,7 +428,7 @@ def T(required: int, total: int, alt_total: Optional[int] = None) -> None: scriptPubKey = redeem_script.to_p2wsh_scriptPubKey() - (_, tx) = self.create_test_txs(CScript(), scriptPubKey, + (_, tx) = self.create_test_txs(CScript(), scriptPubKey, CScript(), CScriptWitness([]), amount) tx = tx.to_mutable() diff --git a/bitcointx/tests/test_wallet.py b/bitcointx/tests/test_wallet.py index 86890f80..6ff28d58 100644 --- a/bitcointx/tests/test_wallet.py +++ b/bitcointx/tests/test_wallet.py @@ -12,10 +12,12 @@ # pylama:ignore=E501 +import os +import json import hashlib import unittest -from typing import Iterable, Optional, Callable, Union, Type +from typing import Iterable, Optional, Callable, Union, Type, List, Any import bitcointx from bitcointx import ( @@ -27,9 +29,18 @@ select_chain_params ) from bitcointx.util import dispatcher_mapped_list -from bitcointx.core import b2x, x, Hash160 -from bitcointx.core.script import CScript, IsLowDERSignature -from bitcointx.core.key import CPubKey +from bitcointx.core import ( + b2x, x, Hash160, CTransaction, CMutableTransaction, CTxOut, + CMutableTxInWitness, CoreCoinParams +) +from bitcointx.core.script import ( + CScript, IsLowDERSignature, TaprootScriptTree, + SignatureHashSchnorr, CScriptWitness, SIGHASH_Type, + TaprootScriptTreeLeaf_Type +) +from bitcointx.core.key import ( + CPubKey, XOnlyPubKey, compute_tap_tweak_hash +) from bitcointx.wallet import ( CCoinAddressError as CBitcoinAddressError, CCoinAddress, @@ -46,7 +57,7 @@ P2WPKHBitcoinAddress, P2WSHBitcoinAddress, P2TRBitcoinAddress, - CBitcoinKey, + CBitcoinKey, CCoinKey ) @@ -68,14 +79,20 @@ def recursive_check(aclass: type) -> None: else: a = None - if getattr(aclass, 'from_pubkey', None): + if getattr(aclass, 'from_xonly_pubkey', None): + if bitcointx.util._allow_secp256k1_experimental_modules: + xa = aclass.from_xonly_pubkey(XOnlyPubKey(pub)) + a = aclass.from_pubkey(pub) + test.assertEqual(a, xa) + xoa = aclass.from_xonly_output_pubkey(XOnlyPubKey(pub)) + a_from_spk = aclass.from_scriptPubKey( + CScript(b'\x51\x20' + pub[1:])) + test.assertEqual(a_from_spk, xoa) + elif getattr(aclass, 'from_pubkey', None): a = aclass.from_pubkey(pub) elif getattr(aclass, 'from_redeemScript', None): a = aclass.from_redeemScript( CScript(b'\xa9' + Hash160(pub) + b'\x87')) - elif issubclass(aclass, P2TRCoinAddress): - a = aclass.from_scriptPubKey( - CScript(b'\x51\x20' + pub[1:])) else: assert len(dispatcher_mapped_list(aclass)) > 0,\ ("dispatcher mapped list for {} " @@ -118,6 +135,10 @@ def test_get_output_size(self) -> None: CScript(b'\xa9' + Hash160(pub) + b'\x87')) self.assertEqual(P2WSHCoinAddress.get_output_size(), 43) self.assertEqual(a3.get_output_size(), 43) + if bitcointx.util._allow_secp256k1_experimental_modules: + a4 = P2TRCoinAddress.from_pubkey(pub) + self.assertEqual(P2TRCoinAddress.get_output_size(), 43) + self.assertEqual(a4.get_output_size(), 43) def test_scriptpubkey_type(self) -> None: for l1_cls in dispatcher_mapped_list(CCoinAddress): @@ -327,17 +348,26 @@ def test_from_valid_pubkey(self) -> None: def T( pubkey: bytes, expected_str_addr: str, - cls: Union[Type[P2PKHCoinAddress], Type[P2WPKHCoinAddress]], + cls: Union[Type[P2PKHCoinAddress], Type[P2WPKHCoinAddress], Type[P2TRCoinAddress]], accept_uncompressed: bool = False ) -> None: - addr: Union[P2PKHCoinAddress, P2WPKHCoinAddress] - if accept_uncompressed: - assert len(pubkey) == 65 - assert issubclass(cls, P2PKHCoinAddress) - addr = cls.from_pubkey(pubkey, accept_uncompressed=accept_uncompressed) + if len(pubkey) == 32: + if not bitcointx.util._allow_secp256k1_experimental_modules: + return + addr = cls.from_output_pubkey(pubkey) else: - assert len(pubkey) == 33 - addr = cls.from_pubkey(pubkey) + if accept_uncompressed: + assert len(pubkey) == 65 + assert issubclass(cls, P2PKHCoinAddress) + addr = cls.from_pubkey(pubkey, accept_uncompressed=accept_uncompressed) + else: + assert len(pubkey) == 33 + if issubclass(cls, P2TRCoinAddress): + if not bitcointx.util._allow_secp256k1_experimental_modules: + return + addr = cls.from_output_pubkey(pubkey) + else: + addr = cls.from_pubkey(pubkey) self.assertEqual(str(addr), expected_str_addr) T(x('0378d430274f8c5ec1321338151e9f27f4c676a008bdf8638d07c0b6be9ab35c71'), @@ -348,6 +378,15 @@ def T( T(x('0378d430274f8c5ec1321338151e9f27f4c676a008bdf8638d07c0b6be9ab35c71'), 'bc1q08alc0e5ua69scxhvyma568nvguqccrv4cc9n4', P2WPKHBitcoinAddress) + # P2TR from ordinary pubkey + T(x('0378d430274f8c5ec1321338151e9f27f4c676a008bdf8638d07c0b6be9ab35c71'), + 'bc1p0r2rqf60330vzvsn8q23a8e87nr8dgqghhux8rg8czmtax4nt3csc0rfcn', + P2TRBitcoinAddress) + # P2TR from x-only pubkey + T(x('78d430274f8c5ec1321338151e9f27f4c676a008bdf8638d07c0b6be9ab35c71'), + 'bc1p0r2rqf60330vzvsn8q23a8e87nr8dgqghhux8rg8czmtax4nt3csc0rfcn', + P2TRBitcoinAddress) + T(CPubKey(x('0378d430274f8c5ec1321338151e9f27f4c676a008bdf8638d07c0b6be9ab35c71')), '1C7zdTfnkzmr13HfA2vNm5SJYRK6nEKyq8', P2PKHBitcoinAddress) T(CPubKey(x('0478d430274f8c5ec1321338151e9f27f4c676a008bdf8638d07c0b6be9ab35c71a1518063243acd4dfe96b66e3f2ec8013c8e072cd09b3834a19f81f659cc3455')), @@ -355,16 +394,31 @@ def T( T(CPubKey(x('0378d430274f8c5ec1321338151e9f27f4c676a008bdf8638d07c0b6be9ab35c71')), 'bc1q08alc0e5ua69scxhvyma568nvguqccrv4cc9n4', P2WPKHBitcoinAddress) + T(CPubKey(x('0378d430274f8c5ec1321338151e9f27f4c676a008bdf8638d07c0b6be9ab35c71')), + 'bc1p0r2rqf60330vzvsn8q23a8e87nr8dgqghhux8rg8czmtax4nt3csc0rfcn', + P2TRBitcoinAddress) + + if bitcointx.util._allow_secp256k1_experimental_modules: + T(XOnlyPubKey(CPubKey(x('0378d430274f8c5ec1321338151e9f27f4c676a008bdf8638d07c0b6be9ab35c71'))), + 'bc1p0r2rqf60330vzvsn8q23a8e87nr8dgqghhux8rg8czmtax4nt3csc0rfcn', + P2TRBitcoinAddress) def test_from_invalid_pubkeys(self) -> None: """Create P2PKHBitcoinAddress's from invalid pubkeys""" # first test with accept_invalid=True def T(invalid_pubkey: bytes, expected_str_addr: str, - cls: Union[Type[P2PKHBitcoinAddress], Type[P2WPKHBitcoinAddress]] + cls: Union[Type[P2PKHBitcoinAddress], Type[P2WPKHBitcoinAddress], + Type[P2TRBitcoinAddress]] ) -> None: - addr: Union[P2PKHBitcoinAddress, P2WPKHBitcoinAddress] - addr = cls.from_pubkey(invalid_pubkey, accept_invalid=True) + addr: Union[P2PKHBitcoinAddress, P2WPKHBitcoinAddress, + P2TRBitcoinAddress] + if issubclass(cls, P2TRCoinAddress): + if not bitcointx.util._allow_secp256k1_experimental_modules: + return + addr = cls.from_output_pubkey(invalid_pubkey, accept_invalid=True) + else: + addr = cls.from_pubkey(invalid_pubkey, accept_invalid=True) self.assertEqual(str(addr), expected_str_addr) @@ -375,13 +429,33 @@ def T(invalid_pubkey: bytes, expected_str_addr: str, T(x(''), 'bc1qk3e2yekshkyuzdcx5sfjena3da7rh87t4thq9p', P2WPKHBitcoinAddress) T(inv_pub_bytes, 'bc1q6gzj82cpgy0pe9jgh0utalfp2kyvvm72m0zlrt', P2WPKHBitcoinAddress) + T(b'\x00'*32, 'bc1pqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqpqqenm', + P2TRBitcoinAddress) + T(CPubKey(inv_pub_bytes), 'bc1p0r2rqf60330vzvsn8q23a8e87nr8dgqghhux8rg8czmtax4nt3eqznytxx', + P2TRBitcoinAddress) # With accept_invalid=False we should get CBitcoinAddressError's for inv_pub in (x(''), inv_pub_bytes, CPubKey(inv_pub_bytes)): - for cls in (P2PKHBitcoinAddress, P2WPKHBitcoinAddress): + for cls in (P2PKHBitcoinAddress, P2WPKHBitcoinAddress, + P2TRBitcoinAddress): + with self.assertRaises(CBitcoinAddressError): + if issubclass(cls, P2TRCoinAddress): + cls.from_output_pubkey(inv_pub) + else: + cls.from_pubkey(inv_pub) + + if bitcointx.util._allow_secp256k1_experimental_modules: + with self.assertRaises(CBitcoinAddressError): + P2TRCoinAddress.from_pubkey(inv_pub) + + if bitcointx.util._allow_secp256k1_experimental_modules: + for inv_pub in (x(''), inv_pub_bytes[1:], + XOnlyPubKey(inv_pub_bytes[1:])): + with self.assertRaises(CBitcoinAddressError): + P2TRCoinAddress.from_xonly_output_pubkey(inv_pub) with self.assertRaises(CBitcoinAddressError): - cls.from_pubkey(inv_pub) + P2TRCoinAddress.from_xonly_pubkey(inv_pub) class Test_P2PKHBitcoinAddress(unittest.TestCase): @@ -575,3 +649,151 @@ def test_select_chain_params(self) -> None: def tearDown(self) -> None: select_chain_params(self.current_params) + + +class Test_BIP341_standard_vectors(unittest.TestCase): + def setUp(self) -> None: + with open(os.path.dirname(__file__) + '/data/bip341-wallet-test-vectors.json', 'r') as fd: + data = json.load(fd) + assert data['version'] == 1 + + self.spk_cases = data['scriptPubKey'] + self.keypath_spending_cases = data['keyPathSpending'] + + def test_bip341_scriptPubKey(self) -> None: + if not bitcointx.util._allow_secp256k1_experimental_modules: + self.skipTest("secp256k1 experimental modules are not available") + for tcase in self.spk_cases: + given = tcase['given'] + intermediary = tcase['intermediary'] + expected = tcase['expected'] + int_pub = XOnlyPubKey(x(given['internalPubkey'])) + stree_data = given['scriptTree'] + scripts = {} + stree = None + + if stree_data: + if isinstance(stree_data, dict): + stree_data = [stree_data] + + assert isinstance(stree_data, list) + + def process_leaves(leaves_data: List[Any] + ) -> List[TaprootScriptTreeLeaf_Type]: + leaves = [] + for ld in leaves_data: + leaf: TaprootScriptTreeLeaf_Type + if isinstance(ld, dict): + sname = f'id_{ld["id"]}' + leaf = CScript(x(ld['script']), name=sname) + scripts[sname] = leaf + if ld["leafVersion"] != CoreCoinParams.TAPROOT_LEAF_TAPSCRIPT: + leaf = TaprootScriptTree( + [leaf], leaf_version=ld["leafVersion"]) + else: + assert isinstance(ld, list) + leaf = TaprootScriptTree(process_leaves(ld)) + + leaves.append(leaf) + + return leaves + + stree = TaprootScriptTree(process_leaves(stree_data), + internal_pubkey=int_pub) + merkle_root = stree.merkle_root + adr = P2TRCoinAddress.from_script_tree(stree) + else: + merkle_root = b'' + adr = P2TRCoinAddress.from_pubkey(int_pub) + + if intermediary['merkleRoot']: + self.assertEqual(merkle_root.hex(), intermediary['merkleRoot']) + + tweak = compute_tap_tweak_hash(int_pub, merkle_root=merkle_root) + + self.assertEqual(tweak.hex(), intermediary['tweak']) + self.assertEqual(adr.hex(), intermediary['tweakedPubkey']) + + spk = adr.to_scriptPubKey() + + self.assertEqual(str(adr), expected['bip350Address']) + self.assertEqual(b2x(spk), expected['scriptPubKey']) + + cblocks = expected.get('scriptPathControlBlocks', []) + + for s_id, expected_cb in enumerate(cblocks): + sname = f'id_{s_id}' + assert stree is not None + swcb = stree.get_script_with_control_block(sname) + assert swcb is not None + s, cb = swcb + assert s == scripts[sname] + self.assertEqual(b2x(cb), expected_cb) + + def test_bip341_keyPathSpending(self) -> None: + if not bitcointx.util._allow_secp256k1_experimental_modules: + self.skipTest("secp256k1 experimental modules are not available") + for tcase in self.keypath_spending_cases: + tx = CMutableTransaction.deserialize( + x(tcase['given']['rawUnsignedTx'])) + spent_outputs = [CTxOut(u['amountSats'], + CScript(x(u['scriptPubKey']))) + for u in tcase['given']['utxosSpent']] + + signed_inputs = set() + for inp_tcase in tcase['inputSpending']: + given = inp_tcase['given'] + intermediary = inp_tcase['intermediary'] + expected = inp_tcase['expected'] + in_idx = given['txinIndex'] + signed_inputs.add(in_idx) + k = CCoinKey.from_secret_bytes(x(given['internalPrivkey'])) + + self.assertEqual(k.xonly_pub.hex(), + intermediary['internalPubkey']) + + ht = None + if given['hashType']: + ht = SIGHASH_Type(given['hashType']) + + if given['merkleRoot']: + mr = x(given['merkleRoot']) + else: + mr = b'' + + tweak = compute_tap_tweak_hash(k.xonly_pub, merkle_root=mr) + self.assertEqual(tweak.hex(), intermediary['tweak']) + + # No check for intermediary['tweakedPrivkey'], + # we would need to do secp256k1_keypair_* stuff here for that + + sh = SignatureHashSchnorr(tx, in_idx, spent_outputs, + hashtype=ht) + + # No check for intermediary['sigMsg'], + # we would need to adjust SignatureHashSchnorr to return + # non-hashed data for this + + self.assertEqual(sh.hex(), intermediary['sigHash']) + + sig = k.sign_schnorr_tweaked(sh, merkle_root=mr) + if ht: + wstack = [(sig + bytes([ht]))] + else: + wstack = [sig] + + self.assertEqual([elt.hex() for elt in wstack], + expected['witness']) + + tx.wit.vtxinwit[in_idx] = CMutableTxInWitness(CScriptWitness(wstack)) + + signed_tx = CTransaction.deserialize( + x(tcase['auxiliary']['fullySignedTx'])) + + for in_idx, inp in enumerate(signed_tx.vin): + if in_idx not in signed_inputs: + tx.vin[in_idx].scriptSig = inp.scriptSig + tx.wit.vtxinwit[in_idx] = signed_tx.wit.vtxinwit[in_idx].to_mutable() + + self.assertEqual(tx.serialize().hex(), + tcase['auxiliary']['fullySignedTx']) diff --git a/bitcointx/util.py b/bitcointx/util.py index 539b03d8..7e4d1db4 100644 --- a/bitcointx/util.py +++ b/bitcointx/util.py @@ -1,4 +1,4 @@ -# Copyright (C) 2018-2019 The python-bitcointx developers +# Copyright (C) 2018-2021 The python-bitcointx developers # # This file is part of python-bitcointx. # @@ -18,6 +18,7 @@ import threading has_contextvars = False +import hashlib import functools from enum import Enum from types import FunctionType @@ -27,6 +28,7 @@ TypeVar, Generic, cast, NoReturn ) +_allow_secp256k1_experimental_modules = False _secp256k1_library_path: Optional[str] = None _openssl_library_path: Optional[str] = None @@ -615,6 +617,15 @@ def set_dispatcher_class(self, identity: str, setattr(self, identity, value) +def tagged_hasher(tag: bytes) -> Callable[[bytes], bytes]: + thash = hashlib.sha256(tag).digest() * 2 + + def hasher(data: bytes) -> bytes: + return hashlib.sha256(thash+data).digest() + + return hasher + + class_mapping_dispatch_data = ContextLocalClassDispatchers() __all__ = ( @@ -631,4 +642,5 @@ def set_dispatcher_class(self, identity: str, 'ReadOnlyField', 'WriteableField', 'ContextVarsCompat', + 'tagged_hasher', ) diff --git a/bitcointx/wallet.py b/bitcointx/wallet.py index 48eb42f7..9df917a4 100644 --- a/bitcointx/wallet.py +++ b/bitcointx/wallet.py @@ -31,10 +31,12 @@ ensure_isinstance ) from bitcointx.core.key import ( - CPubKey, CKeyBase, CExtKeyBase, CExtPubKeyBase + CPubKey, CKeyBase, CExtKeyBase, CExtPubKeyBase, XOnlyPubKey, + tap_tweak_pubkey ) from bitcointx.core.script import ( - CScript, standard_keyhash_scriptpubkey, standard_scripthash_scriptpubkey + CScript, standard_keyhash_scriptpubkey, standard_scripthash_scriptpubkey, + TaprootScriptTree ) @@ -490,6 +492,131 @@ class P2TRCoinAddress(CBech32CoinAddress, next_dispatch_final=True): bech32_witness_version = 1 _scriptpubkey_type = 'witness_v1_taproot' + @classmethod + def from_xonly_output_pubkey( + cls: Type[T_P2TRCoinAddress], + pubkey: Union[XOnlyPubKey, bytes, bytearray], + *, + accept_invalid: bool = False + ) -> T_P2TRCoinAddress: + """Create a P2TR address from x-only pubkey that is already tweaked, + the "output pubkey" in the terms of BIP341 + + Raises CCoinAddressError if pubkey is invalid, unless accept_invalid + is True. + """ + ensure_isinstance(pubkey, (XOnlyPubKey, bytes, bytearray), 'pubkey') + + if not accept_invalid: + if not isinstance(pubkey, XOnlyPubKey): + try: + pubkey = XOnlyPubKey(pubkey) + except ValueError as e: + raise P2TRCoinAddressError(f'problem with pubkey: {e}') + if not pubkey.is_fullyvalid(): + raise P2TRCoinAddressError('invalid x-only pubkey') + + return cls.from_bytes(pubkey) + + @classmethod + def from_xonly_pubkey( + cls: Type[T_P2TRCoinAddress], + pubkey: Union[XOnlyPubKey, bytes, bytearray] + ) -> T_P2TRCoinAddress: + """Create a P2TR address from x-only "internal" pubkey (in BIP341 terms). + The pubkey will be tweaked with the tagged hash of itself, to make + the output pubkey commit to an unspendable script path, as recommended + by BIP341 (see note 22 in BIP341). + + Raises CCoinAddressError if pubkey is invalid + """ + ensure_isinstance(pubkey, (XOnlyPubKey, bytes, bytearray), 'pubkey') + + if not isinstance(pubkey, XOnlyPubKey): + pubkey = XOnlyPubKey(pubkey) + + if not pubkey.is_fullyvalid(): + raise P2TRCoinAddressError('invalid pubkey') + + tt_res = tap_tweak_pubkey(pubkey) + + if not tt_res: + raise P2TRCoinAddressError('cannot create tap tweak from supplied pubkey') + + out_pub, _ = tt_res + + return cls.from_xonly_output_pubkey(out_pub) + + @classmethod + def from_output_pubkey(cls: Type[T_P2TRCoinAddress], + pubkey: Union[CPubKey, bytes, bytearray], + *, + accept_invalid: bool = False) -> T_P2TRCoinAddress: + """Create a P2TR address from a pubkey that is already tweaked, + the "output pubkey" in the terms of BIP341 + + Raises CCoinAddressError if pubkey is invalid, unless accept_invalid + is True. + """ + ensure_isinstance(pubkey, (XOnlyPubKey, CPubKey, bytes, bytearray), + 'pubkey') + + if len(pubkey) == 32: + if not isinstance(pubkey, CPubKey): # might be invalid CPubKey + return cls.from_xonly_output_pubkey( + pubkey, accept_invalid=accept_invalid) + + if not accept_invalid: + if not isinstance(pubkey, CPubKey): + pubkey = CPubKey(pubkey) + if not pubkey.is_fullyvalid(): + raise P2TRCoinAddressError('invalid pubkey') + if not pubkey.is_compressed(): + raise P2TRCoinAddressError( + 'Uncompressed pubkeys are not allowed') + + return cls.from_xonly_output_pubkey(XOnlyPubKey(pubkey), + accept_invalid=accept_invalid) + + @classmethod + def from_pubkey(cls: Type[T_P2TRCoinAddress], + pubkey: Union[XOnlyPubKey, CPubKey, bytes, bytearray], + ) -> T_P2TRCoinAddress: + """Create a P2TR address from "internal" pubkey (in BIP341 terms) + + Raises CCoinAddressError if pubkey is invalid + """ + ensure_isinstance(pubkey, (XOnlyPubKey, CPubKey, bytes, bytearray), + 'pubkey') + + if not isinstance(pubkey, CPubKey): + + if len(pubkey) == 32: + return cls.from_xonly_pubkey(pubkey) + + pubkey = CPubKey(pubkey) + + if not pubkey.is_fullyvalid(): + raise P2TRCoinAddressError('invalid pubkey') + + if not pubkey.is_compressed(): + raise P2TRCoinAddressError( + 'Uncompressed pubkeys are not allowed') + + return cls.from_xonly_pubkey(XOnlyPubKey(pubkey)) + + @classmethod + def from_script_tree(cls: Type[T_P2TRCoinAddress], + stree: TaprootScriptTree) -> T_P2TRCoinAddress: + """Create a P2TR address from TaprootScriptTree instance + """ + if not stree.internal_pubkey: + raise P2TRCoinAddressError( + f'The supplied instance of {stree.__class__.__name__} ' + f'does not have internal_pubkey') + assert stree.output_pubkey is not None + return cls.from_xonly_output_pubkey(stree.output_pubkey) + @classmethod def from_scriptPubKey(cls: Type[T_P2TRCoinAddress], scriptPubKey: CScript) -> T_P2TRCoinAddress: @@ -497,6 +624,9 @@ def from_scriptPubKey(cls: Type[T_P2TRCoinAddress], Raises CCoinAddressError if the scriptPubKey isn't of the correct form. + + Note that there is no check if the x-only pubkey included in the + scriptPubKey is a valid pubkey """ if scriptPubKey.is_witness_v1_taproot(): return cls.from_bytes(scriptPubKey[2:34]) @@ -706,6 +836,26 @@ def to_uncompressed(self: T_CCoinKey) -> T_CCoinKey: return self return self.__class__.from_secret_bytes(self[:32], False) + def sign_schnorr_tweaked( + self, hash: Union[bytes, bytearray], + *, + merkle_root: bytes = b'', + aux: Optional[bytes] = None + ) -> bytes: + """Schnorr-sign with the key that is tweaked before signing. + + When merkle_root is empty bytes, the tweak will be generated + as a tagged hash of the x-only pubkey that corresponds to this + private key. Supplying empty-bytes merkle_root (the default) is + mostly useful when signing keypath spends when there is no script path. + + When merkle_root is 32 bytes, it will be directly used as a tweak. + This is mostly useful when signing keypath spends when there is also + a script path present + """ + return self._sign_schnorr_internal( + hash, merkle_root=merkle_root, aux=aux) + class CBitcoinKey(CCoinKey, WalletBitcoinClass): base58_prefix = bytes([128]) diff --git a/release-notes.md b/release-notes.md index 1edff66b..90c481ef 100644 --- a/release-notes.md +++ b/release-notes.md @@ -2,6 +2,27 @@ ## v1.1.4.dev0 +Removed bitcointx.core.serialize.VarBytesSerializer - it was a duplicate of BytesSerializer + +Fixed base58 and bech32 prefixes for signet addresses and keys (they had values based on some early +signet branch of Bitcoin Core, and they were changed afterwards, now they are the same as for testnet) + +Added support for P2TR addresses (bech32m encoding, segwit v1) + +Added support for taproot inputs spending: + * Currently have to be enabled with `allow_secp256k1_experimental_modules()` or + PYTHON_BITCOINTX_ALLOW_LIBSECP256K1_EXPERIMENTAL_MODULES_USE=1 environment variable, + and appropriate libsecp256k1 version supplied with `set_custom_secp256k1_path()` or + LD_LIBRARY_PATH environment variable. + Recommended commit for libsecp256k1: 7006f1b97fd8dbf4ef75771dd7c15185811c3f50 + * `CScript` now have `name` field and `is_witness_v1_taproot()`, `sighash_schnorr()` methods + * `TaprootScriptTree` class in bitcointx.core.script + * `XOnlyPukey` class in bitcointx.core.key + * `CKey` now has `xonly_pub` field and `sign_schnorr_no_tweak()`, `verify_schnorr()` methods + * `CCoinKey` now has `sign_schnorr_tweaked()` method (in addition to all `CKey` methods, of course) + * `CPubKey` now has `verify_schnorr()` and `is_null()` methods + * `SignatureHashSchnorr()` function to compute sighash for schnorr when no script is present + ## v1.1.3 Fixed base58 and bech32 prefixes for signet addresses and keys (they had values based on some early