From 3c4662da12e88fcbff541d355073d0b0b657d50f Mon Sep 17 00:00:00 2001 From: Dmitry Petukhov Date: Tue, 13 Oct 2020 21:20:28 +0500 Subject: [PATCH 01/14] Reset version for further development --- bitcointx/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bitcointx/__init__.py b/bitcointx/__init__.py index 498573cc..a09fb54f 100644 --- a/bitcointx/__init__.py +++ b/bitcointx/__init__.py @@ -27,7 +27,7 @@ # Note that setup.py can break if __init__.py imports any external # dependencies, as these might not be installed when setup.py runs. In this # case __version__ could be moved to a separate version.py and imported here. -__version__ = '1.1.1.post2' +__version__ = '1.1.2.dev0' # initialized at the end of the module, because it From 2f5632b5bf911aee9a039bca9621408d7b2596e9 Mon Sep 17 00:00:00 2001 From: Janus Date: Mon, 12 Oct 2020 22:10:20 -0500 Subject: [PATCH 02/14] Use startswith instead of slicing This built-in function is faster, and using it, we can avoid copying as much data around, since each slice will be copying. It also is easier to read. --- bitcointx/base58.py | 2 +- bitcointx/core/psbt.py | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/bitcointx/base58.py b/bitcointx/base58.py index 67c0e42b..6139a09e 100644 --- a/bitcointx/base58.py +++ b/bitcointx/base58.py @@ -162,7 +162,7 @@ def base58_from_bytes_match_prefix(cls: Type[T_CBase58Data], data: bytes return candidate.base58_from_bytes_match_prefix(data) except UnexpectedBase58PrefixError: pass - elif data[:len(pfx)] == pfx: + elif data.startswith(pfx): return candidate.from_bytes(data[len(pfx):]) if len(candidates) == 1: diff --git a/bitcointx/core/psbt.py b/bitcointx/core/psbt.py index 00b39da1..79c367bf 100644 --- a/bitcointx/core/psbt.py +++ b/bitcointx/core/psbt.py @@ -1979,10 +1979,9 @@ def from_base64_or_binary( else: raise TypeError('type of data is not str or bytes') - if data_b[:len(PSBT_MAGIC_HEADER_BYTES)] == PSBT_MAGIC_HEADER_BYTES: + if data_b.startswith(PSBT_MAGIC_HEADER_BYTES): return cls.from_binary(bytes(data_b), **kwargs) - elif (data_b[:len(PSBT_MAGIC_HEADER_BASE64)] == - PSBT_MAGIC_HEADER_BASE64.encode('ascii')): + elif data_b.startswith(PSBT_MAGIC_HEADER_BASE64.encode('ascii')): return cls.deserialize(base64.b64decode(data_b.decode('ascii'), validate=validate), **kwargs) From 52da6d61af2d0bb7ceff9fe14a9ab8a91e37ed24 Mon Sep 17 00:00:00 2001 From: Dmitry Petukhov Date: Wed, 9 Sep 2020 14:22:58 +0500 Subject: [PATCH 03/14] Fix: ancestor of ScriptBitcoinClass should be ScriptCoinClass --- bitcointx/core/script.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bitcointx/core/script.py b/bitcointx/core/script.py index 3870a321..d46f6c10 100644 --- a/bitcointx/core/script.py +++ b/bitcointx/core/script.py @@ -140,7 +140,7 @@ class ScriptBitcoinClassDispatcher(ScriptCoinClassDispatcher): ... -class ScriptBitcoinClass(metaclass=ScriptBitcoinClassDispatcher): +class ScriptBitcoinClass(ScriptCoinClass, metaclass=ScriptBitcoinClassDispatcher): ... From a930e266baca1b5b1a3ab1d1aa0b696179b80846 Mon Sep 17 00:00:00 2001 From: Dmitry Petukhov Date: Thu, 10 Sep 2020 13:45:19 +0500 Subject: [PATCH 04/14] Avoid assigning instances of dispatched classes as default kwarg values --- bitcointx/core/__init__.py | 8 +++++-- bitcointx/core/psbt.py | 49 ++++++++++++++++++++++++++++---------- 2 files changed, 42 insertions(+), 15 deletions(-) diff --git a/bitcointx/core/__init__.py b/bitcointx/core/__init__.py index 62571960..c9a16f89 100644 --- a/bitcointx/core/__init__.py +++ b/bitcointx/core/__init__.py @@ -801,8 +801,12 @@ class CTxInWitness(CoreCoinClass, next_dispatch_final=True): to_mutable: Callable[['CTxInWitness'], 'CMutableTxInWitness'] to_immutable: Callable[['CTxInWitness'], 'CTxInWitness'] - def __init__(self, scriptWitness: script.CScriptWitness = script.CScriptWitness()): - ensure_isinstance(scriptWitness, script.CScriptWitness, 'scriptWitness') + def __init__(self, scriptWitness: Optional[script.CScriptWitness] = None): + if scriptWitness is None: + scriptWitness = script.CScriptWitness() + else: + ensure_isinstance(scriptWitness, script.CScriptWitness, 'scriptWitness') + object.__setattr__(self, 'scriptWitness', scriptWitness) @no_bool_use_as_property diff --git a/bitcointx/core/psbt.py b/bitcointx/core/psbt.py index 79c367bf..88574726 100644 --- a/bitcointx/core/psbt.py +++ b/bitcointx/core/psbt.py @@ -376,11 +376,11 @@ def __init__( utxo: Optional[Union[CTransaction, CTxOut]] = None, partial_sigs: Optional[Dict[CPubKey, bytes]] = None, sighash_type: Optional[int] = None, - redeem_script: CScript = CScript(), - witness_script: CScript = CScript(), + redeem_script: Optional[CScript] = None, + witness_script: Optional[CScript] = None, derivation_map: Optional[Dict[CPubKey, PSBT_KeyDerivationInfo]] = None, final_script_sig: bytes = b'', - final_script_witness: CScriptWitness = CScriptWitness(), + final_script_witness: Optional[CScriptWitness] = None, proof_of_reserves_commitment: bytes = b'', proprietary_fields: Optional[Dict[ bytes, List[PSBT_ProprietaryTypeData] @@ -438,10 +438,18 @@ def descr(msg: str) -> str: self.sighash_type = sighash_type - ensure_isinstance(redeem_script, CScript, descr('redeem script')) + if redeem_script is None: + redeem_script = CScript() + else: + ensure_isinstance(redeem_script, CScript, descr('redeem script')) + self.redeem_script = redeem_script - ensure_isinstance(witness_script, CScript, descr('witness script')) + if witness_script is None: + witness_script = CScript() + else: + ensure_isinstance(witness_script, CScript, descr('witness script')) + self.witness_script = witness_script if derivation_map is None: @@ -459,8 +467,12 @@ def descr(msg: str) -> str: ensure_isinstance(final_script_sig, bytes, descr('final script sig')) self.final_script_sig = final_script_sig - ensure_isinstance(final_script_witness, CScriptWitness, - descr('final script witness')) + if final_script_witness is None: + final_script_witness = CScriptWitness() + else: + ensure_isinstance(final_script_witness, CScriptWitness, + descr('final script witness')) + self.final_script_witness = final_script_witness ensure_isinstance(proof_of_reserves_commitment, @@ -1396,8 +1408,8 @@ class PSBT_Output(Serializable): def __init__( self, *, - redeem_script: CScript = CScript(), - witness_script: CScript = CScript(), + redeem_script: Optional[CScript] = None, + witness_script: Optional[CScript] = None, derivation_map: Optional[Dict[CPubKey, PSBT_KeyDerivationInfo]] = None, proprietary_fields: Optional[Dict[ bytes, List[PSBT_ProprietaryTypeData] @@ -1416,10 +1428,18 @@ def descr(msg: str) -> str: self.index = index - ensure_isinstance(redeem_script, CScript, descr('redeem script')) + if redeem_script is None: + redeem_script = CScript() + else: + ensure_isinstance(redeem_script, CScript, descr('redeem script')) + self.redeem_script = redeem_script - ensure_isinstance(witness_script, CScript, descr('witness script')) + if witness_script is None: + witness_script = CScript() + else: + ensure_isinstance(witness_script, CScript, descr('witness script')) + self.witness_script = witness_script if derivation_map is None: @@ -1658,7 +1678,7 @@ def __init__(self, *, version: int = 0, inputs: Optional[List[PSBT_Input]] = None, outputs: Optional[List[PSBT_Output]] = None, - unsigned_tx: CTransaction = CTransaction(), + unsigned_tx: Optional[CTransaction] = None, xpubs: Optional[Dict[ CCoinExtPubKey, PSBT_KeyDerivationInfo ]] = None, @@ -1674,7 +1694,10 @@ def __init__(self, *, raise ValueError('Unsupported PSBT version') self.version = version - ensure_isinstance(unsigned_tx, CTransaction, 'unsigned_tx') + if unsigned_tx is None: + unsigned_tx = CTransaction() + else: + ensure_isinstance(unsigned_tx, CTransaction, 'unsigned_tx') if inputs is None: num_inputs = len(unsigned_tx.vin) From 5f52f0cd82333af034bf8b81ea5ea6f2226d4713 Mon Sep 17 00:00:00 2001 From: Dmitry Petukhov Date: Fri, 11 Sep 2020 12:33:25 +0500 Subject: [PATCH 05/14] Remove reference to non-existing function from the comment --- bitcointx/core/secp256k1.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bitcointx/core/secp256k1.py b/bitcointx/core/secp256k1.py index 7e58a352..50ae9fd4 100644 --- a/bitcointx/core/secp256k1.py +++ b/bitcointx/core/secp256k1.py @@ -14,7 +14,7 @@ # NOTE: for simplicity, when we need to pass an array of structs to secp256k1 # function, we will build an array of bytes out of elements, and then pass # this array. we are dealing with 32 or 64-byte aligned data, -# so this should be safe. You can use build_aligned_data_array() for this. +# so this should be safe. # NOTE: special care should be taken with functions that may write to parts # of their arguments, like secp256k1_pedersen_blind_generator_blind_sum, From beecab0a68c2d7475ee0be5fb204c2d8148efdab Mon Sep 17 00:00:00 2001 From: Dmitry Petukhov Date: Fri, 11 Sep 2020 13:29:12 +0500 Subject: [PATCH 06/14] Remove unnecessary check from CCoinAddress.get_output_size() @classmethod parameter is always a type, so the check was meaningless --- bitcointx/wallet.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/bitcointx/wallet.py b/bitcointx/wallet.py index 129d30d0..d8551441 100644 --- a/bitcointx/wallet.py +++ b/bitcointx/wallet.py @@ -131,17 +131,12 @@ def from_scriptPubKey(cls: Type[T_CCoinAddress], 'scriptPubKey is not in a recognized address format') @classmethod - def get_output_size(cls_or_inst: Type[T_CCoinAddress]) -> int: - if isinstance(cls_or_inst, type): - cls = cls_or_inst - data_length = getattr(cls, '_data_length', None) - if not data_length: - raise TypeError('output size is not available for {}' - .format(cls.__name__)) - inst = cls.from_bytes(b'\x00'*data_length) - else: - inst = cls_or_inst - + def get_output_size(cls: Type[T_CCoinAddress]) -> int: + data_length = getattr(cls, '_data_length', None) + if not data_length: + raise TypeError('output size is not available for {}' + .format(cls.__name__)) + inst = cls.from_bytes(b'\x00'*data_length) txo = bitcointx.core.CTxOut(scriptPubKey=inst.to_scriptPubKey()) f = BytesIO() txo.stream_serialize(f) From 7f0b294f6d656ef1bd4e95cf7062dcbe64b4f5b2 Mon Sep 17 00:00:00 2001 From: Dmitry Petukhov Date: Wed, 9 Sep 2020 13:47:28 +0500 Subject: [PATCH 07/14] Make PartiallySignedTransaction a dispatched class --- bitcointx/__init__.py | 36 +++++--- bitcointx/core/__init__.py | 3 +- bitcointx/core/psbt.py | 160 +++++++++++++++++++++++------------ bitcointx/tests/test_psbt.py | 10 +-- bitcointx/util.py | 2 +- 5 files changed, 137 insertions(+), 74 deletions(-) diff --git a/bitcointx/__init__.py b/bitcointx/__init__.py index a09fb54f..97e4c451 100644 --- a/bitcointx/__init__.py +++ b/bitcointx/__init__.py @@ -68,9 +68,6 @@ def __new__(cls: Type[T_ChainParamsMeta], cls_name: str, if 'RPC_PORT' in dct and not isinstance(dct['RPC_PORT'], int): raise TypeError(f'{cls_name}: RPC_PORT is not an int') - # Cannot check the type of WALLET_DISPATCHER without - # importing bitcointx.wallet, this check has to be done at runtime - if name is not None: if isinstance(name, str): names = [name] @@ -122,6 +119,11 @@ class ChainParamsBase(metaclass=ChainParamsMeta): Type['bitcointx.wallet.WalletCoinClassDispatcher'], Callable[[object], Type['bitcointx.wallet.WalletCoinClassDispatcher']] ] + PSBT_DISPATCHER: Union[ + Type['bitcointx.core.psbt.PSBT_CoinClassDispatcher'], + Callable[[object], + Type['bitcointx.core.psbt.PSBT_CoinClassDispatcher']] + ] def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__() @@ -190,6 +192,12 @@ class BitcoinMainnetParams(ChainParamsBase, Callable[[object], Type['bitcointx.wallet.WalletCoinClassDispatcher']] ] = lambda _: bitcointx.wallet.WalletBitcoinClassDispatcher + PSBT_DISPATCHER: Union[ + Type['bitcointx.core.psbt.PSBT_CoinClassDispatcher'], + Callable[[object], + Type['bitcointx.core.psbt.PSBT_CoinClassDispatcher']] + ] = lambda _: bitcointx.core.psbt.PSBT_BitcoinClassDispatcher + class BitcoinTestnetParams(BitcoinMainnetParams, name='bitcoin/testnet'): RPC_PORT = 18332 @@ -281,19 +289,23 @@ def select_chain_params(params: Union[str, ChainParamsBase, prev_params = _chain_params_context.params _chain_params_context.params = params - # It might not be imported yet import bitcointx.wallet + import bitcointx.core.psbt - wd = params.WALLET_DISPATCHER - if isinstance(wd, types.MethodType): - wd_cls = wd() - else: - assert isinstance(wd, type) - wd_cls = wd + for disp_name in ('WALLET', 'PSBT'): + disp = getattr(params, f'{disp_name}_DISPATCHER', None) + if disp is None: + continue + + if isinstance(disp, types.MethodType): + disp_cls = disp() + else: + assert isinstance(disp, type) + disp_cls = disp - assert issubclass(wd_cls, bitcointx.wallet.WalletCoinClassDispatcher) + assert issubclass(disp_cls, bitcointx.util.ClassMappingDispatcher) - bitcointx.util.activate_class_dispatcher(wd_cls) + bitcointx.util.activate_class_dispatcher(disp_cls) return prev_params, params diff --git a/bitcointx/core/__init__.py b/bitcointx/core/__init__.py index c9a16f89..e3682606 100644 --- a/bitcointx/core/__init__.py +++ b/bitcointx/core/__init__.py @@ -214,7 +214,8 @@ def MAX_MONEY(cls) -> int: class CoreBitcoinParams(CoreCoinParams, CoreBitcoinClass): - ... + PSBT_MAGIC_HEADER_BYTES = b'psbt\xff' + PSBT_MAGIC_HEADER_BASE64 = 'cHNidP' def MoneyRange(nValue: int) -> bool: diff --git a/bitcointx/core/psbt.py b/bitcointx/core/psbt.py index 88574726..1140e6f4 100644 --- a/bitcointx/core/psbt.py +++ b/bitcointx/core/psbt.py @@ -20,15 +20,19 @@ import struct from enum import Enum from collections import OrderedDict +from abc import abstractmethod from .serialize import ( BytesSerializer, VarIntSerializer, ByteStream_Type, SerializationError, SerializationTruncationError, ser_read, Serializable, ImmutableSerializable ) + +import bitcointx.core + from . import ( CTransaction, CTxIn, CTxOut, CTxInWitness, CTxWitness, b2x, CMutableTxIn, CMutableTxOut, CheckTransaction, MoneyRange, - CheckTransactionError + CheckTransactionError, CoreCoinParams ) from .key import CPubKey, BIP32Path, KeyDerivationInfo, KeyStore from .script import ( @@ -39,10 +43,41 @@ ) from ..wallet import CCoinExtPubKey -from ..util import ensure_isinstance, no_bool_use_as_property +from ..util import ( + ensure_isinstance, no_bool_use_as_property, + ClassMappingDispatcher, activate_class_dispatcher +) + + +class PSBT_CoinClassDispatcher( + ClassMappingDispatcher, identity='psbt', + depends=[bitcointx.core.CoreCoinClassDispatcher] +): + ... + + +class PSBT_CoinClass(Serializable, metaclass=PSBT_CoinClassDispatcher): + ... + + +class PSBT_BitcoinClassDispatcher( + PSBT_CoinClassDispatcher, + depends=[bitcointx.core.CoreBitcoinClassDispatcher] +): + ... + + +class PSBT_BitcoinClass(PSBT_CoinClass, metaclass=PSBT_BitcoinClassDispatcher): + + @abstractmethod + def _repr_dict(self) -> 'OrderedDict[str, str]': + ... + + def __repr__(self) -> str: + contents = ', '.join(f'{k}={v}' for k, v in self._repr_dict().items()) + return f"{self.__class__.__name__}({contents})" + -PSBT_MAGIC_HEADER_BYTES = b'psbt\xff' -PSBT_MAGIC_HEADER_BASE64 = 'cHNidP' PSBT_SEPARATOR = b'\x00' PSBT_PROPRIETARY_TYPE = 0xFC @@ -354,7 +389,7 @@ def stream_serialize(self, f: ByteStream_Type, **kwargs: Any) -> None: T_PSBT_Input = TypeVar('T_PSBT_Input', bound='PSBT_Input') -class PSBT_Input(Serializable): +class PSBT_Input(PSBT_CoinClass, next_dispatch_final=True): index: Optional[int] _utxo: Optional[Union[CTransaction, CTxOut]] _witness_utxo: Optional[CTxOut] @@ -1373,31 +1408,35 @@ def get_amount(self, unsigned_tx: Optional[CTransaction]) -> int: return self.utxo.vout[prevout_index].nValue - def __repr__(self) -> str: + def _repr_dict(self) -> 'OrderedDict[str, str]': partial_sigs = (', '.join(f"x('{b2x(k)}'): x('{b2x(v)}')" for k, v in self.partial_sigs.items())) - return ( - f"{self.__class__.__name__}(utxo={self.utxo}, " - f"partial_sigs={{{partial_sigs}}}, " - f"sighash_type={self.sighash_type}, " - f"redeem_script={repr(self.redeem_script)}, " - f"witness_script={repr(self.witness_script)}, " - f"derivation_map={{{derivation_map_repr(self.derivation_map)}}}, " - f"final_script_sig=x('{b2x(self.final_script_sig)}'), " - f"final_script_witness={repr(self.final_script_witness)}, " - f"proof_of_reserves_commitment=" - f"x('{b2x(self.proof_of_reserves_commitment)}'), " - f"proprietary_fields=" - f"{{{proprietary_field_repr(self.proprietary_fields)}}}, " - f"unknown_fields=[{unknown_fields_repr(self.unknown_fields)}]" - f")" - ) + return OrderedDict({ + 'utxo': f'{self.utxo}', + 'partial_sigs': f'{{{partial_sigs}}}', + 'sighash_type': f'{self.sighash_type}', + 'redeem_script': repr(self.redeem_script), + 'witness_script': repr(self.witness_script), + 'derivation_map': + f'{{{derivation_map_repr(self.derivation_map)}}}', + 'final_script_sig': f"x('{b2x(self.final_script_sig)}')", + 'final_script_witness': repr(self.final_script_witness), + 'proof_of_reserves_commitment': + f"x('{b2x(self.proof_of_reserves_commitment)}')", + 'proprietary_fields': + f"{{{proprietary_field_repr(self.proprietary_fields)}}}", + 'unknown_fields': f'[{unknown_fields_repr(self.unknown_fields)}]' + }) + + +class PSBT_BitcoinInput(PSBT_Input, PSBT_BitcoinClass): + ... T_PSBT_Output = TypeVar('T_PSBT_Output', bound='PSBT_Output') -class PSBT_Output(Serializable): +class PSBT_Output(PSBT_CoinClass, next_dispatch_final=True): index: Optional[int] redeem_script: CScript witness_script: CScript @@ -1647,24 +1686,27 @@ def stream_serialize(self, f: ByteStream_Type, **kwargs: Any) -> None: f.write(PSBT_SEPARATOR) - def __repr__(self) -> str: - return ( - f"{self.__class__.__name__}(" - f"redeem_script={self.redeem_script!r}, " - f"witness_script={self.witness_script!r}, " - f"derivation_map={{{derivation_map_repr(self.derivation_map)}}}, " - f"proprietary_fields=" - f"{{{proprietary_field_repr(self.proprietary_fields)}}}, " - f"unknown_fields=[{unknown_fields_repr(self.unknown_fields)}]" - f")" - ) + def _repr_dict(self) -> 'OrderedDict[str, str]': + return OrderedDict({ + 'redeem_script': repr(self.redeem_script), + 'witness_script': repr(self.witness_script), + 'derivation_map': + f'{{{derivation_map_repr(self.derivation_map)}}}', + 'proprietary_fields': + f"{{{proprietary_field_repr(self.proprietary_fields)}}}", + 'unknown_fields': f"[{unknown_fields_repr(self.unknown_fields)}]" + }) + + +class PSBT_BitcoinOutput(PSBT_Output, PSBT_BitcoinClass): + ... T_PartiallySignedTransaction = TypeVar('T_PartiallySignedTransaction', bound='PartiallySignedTransaction') -class PartiallySignedTransaction(Serializable): +class PartiallySignedTransaction(PSBT_CoinClass, next_dispatch_final=True): version: int inputs: List[PSBT_Input] outputs: List[PSBT_Output] @@ -1991,8 +2033,8 @@ def from_base64_or_binary( validate: bool = True, **kwargs: Any ) -> T_PartiallySignedTransaction: if isinstance(data, str): - if data[:len(PSBT_MAGIC_HEADER_BASE64)] != \ - PSBT_MAGIC_HEADER_BASE64: + if data[:len(CoreCoinParams.PSBT_MAGIC_HEADER_BASE64)] != \ + CoreCoinParams.PSBT_MAGIC_HEADER_BASE64: raise ValueError( 'got data of type str, but magic bytes at the start ' 'do not match base64-encoded PSBT magic bytes') @@ -2002,9 +2044,10 @@ def from_base64_or_binary( else: raise TypeError('type of data is not str or bytes') - if data_b.startswith(PSBT_MAGIC_HEADER_BYTES): + if data_b.startswith(CoreCoinParams.PSBT_MAGIC_HEADER_BYTES): return cls.from_binary(bytes(data_b), **kwargs) - elif data_b.startswith(PSBT_MAGIC_HEADER_BASE64.encode('ascii')): + elif data_b.startswith(CoreCoinParams.PSBT_MAGIC_HEADER_BASE64 + .encode('ascii')): return cls.deserialize(base64.b64decode(data_b.decode('ascii'), validate=validate), **kwargs) @@ -2046,8 +2089,9 @@ def stream_deserialize(cls: Type[T_PartiallySignedTransaction], **kwargs: Any) -> T_PartiallySignedTransaction: magic = ser_read(f, 5) - if magic != PSBT_MAGIC_HEADER_BYTES: - raise SerializationError('Invalid PSBT magic bytes') + if magic != CoreCoinParams.PSBT_MAGIC_HEADER_BYTES: + raise SerializationError( + 'Invalid partially-signed transaction header') proprietary_fields: Dict[bytes, List[PSBT_ProprietaryTypeData]] = \ OrderedDict() @@ -2127,7 +2171,7 @@ def stream_serialize(self, f: ByteStream_Type, if not relaxed_sanity_checks: self._check_sanity() - f.write(PSBT_MAGIC_HEADER_BYTES) + f.write(CoreCoinParams.PSBT_MAGIC_HEADER_BYTES) stream_serialize_field( PSBT_GlobalKeyType.UNSIGNED_TX, f, @@ -2240,25 +2284,31 @@ def get_fee(self, allow_negative: bool = False) -> int: f'sum of output amounts {outputs_sum}') return fee - def __repr__(self) -> str: + def _repr_dict(self) -> 'OrderedDict[str, str]': xpubs = ( ', '.join( f"'{str(k)}': (x('{b2x(v.master_fp)}'), \"{str(v.path)}\")" for k, v in self.xpubs.items())) - return ( - f"{self.__class__.__name__}(" - f"version={self.version}, " - f"inputs={self.inputs}, " - f"outputs={self.outputs}, " - f"unsigned_tx={self.unsigned_tx}, " - f"xpubs={{{xpubs}}}, " - f"proprietary_fields=" - f"{{{proprietary_field_repr(self.proprietary_fields)}}}, " - f"unknown_fields=[{unknown_fields_repr(self.unknown_fields)}]" - f")" - ) + return OrderedDict({ + 'version': f'{self.version}', + 'inputs': repr(self.inputs), + 'outputs': repr(self.outputs), + 'unsigned_tx': repr(self.unsigned_tx), + 'xpubs': f'{{{xpubs}}}', + 'proprietary_fields': + f"{{{proprietary_field_repr(self.proprietary_fields)}}}", + 'unknown_fields': f"[{unknown_fields_repr(self.unknown_fields)}]" + }) + + +class PartiallySignedBitcoinTransaction(PartiallySignedTransaction, + PSBT_BitcoinClass): + ... + +# default dispatcher for the module +activate_class_dispatcher(PSBT_BitcoinClassDispatcher) __all__ = ( 'PartiallySignedTransaction', diff --git a/bitcointx/tests/test_psbt.py b/bitcointx/tests/test_psbt.py index fe04b43b..9ed51f3c 100644 --- a/bitcointx/tests/test_psbt.py +++ b/bitcointx/tests/test_psbt.py @@ -21,7 +21,7 @@ from bitcointx.wallet import CCoinExtKey, CCoinExtPubKey, P2PKHCoinAddress from bitcointx.core import ( x, lx, b2x, CTransaction, CTxOut, CMutableTxOut, CTxIn, COutPoint, - coins_to_satoshi + coins_to_satoshi, CoreCoinParams ) from bitcointx.core.key import CPubKey, KeyStore, BIP32Path from bitcointx.core.script import ( @@ -34,8 +34,8 @@ ) from bitcointx.core.psbt import ( PartiallySignedTransaction, PSBT_KeyDerivationInfo, - PSBT_ProprietaryTypeData, PSBT_MAGIC_HEADER_BYTES, - read_psbt_keymap, PSBT_GlobalKeyType, PSBT_InKeyType + PSBT_ProprietaryTypeData, read_psbt_keymap, + PSBT_GlobalKeyType, PSBT_InKeyType ) @@ -47,7 +47,7 @@ def deserialize(hex_data: str) -> PartiallySignedTransaction: # Network transaction, not PSBT format with self.assertRaisesRegex(SerializationError, - 'Invalid PSBT magic bytes'): + 'Invalid partially-signed transaction header'): deserialize('0200000001268171371edff285e937adeea4b37b78000c0566cbb3ad64641713ca42171bf6000000006a473044022070b2245123e6bf474d60c5b50c043d4c691a5d2435f09a34a7662a9dc251790a022001329ca9dacf280bdf30740ec0390422422c81cb45839457aeb76fc12edd95b3012102657d118d3357b8e0f4c2cd46db7b39f6d9c38d9a70abcb9b2de5dc8dbfe4ce31feffffff02d3dff505000000001976a914d0c59903c5bac2868760e90fd521a4665aa7652088ac00e1f5050000000017a9143545e6e33b832c47050f24d3eeb93c9c03948bc787b32e1300') # PSBT missing outputs @@ -676,7 +676,7 @@ def test_both_witness_and_nonwitness_utxo_present(self) -> None: def get_input_key_types(psbt_bytes: bytes) -> Set[PSBT_InKeyType]: f = BytesIO(psbt_bytes) magic = ser_read(f, 5) - assert magic == PSBT_MAGIC_HEADER_BYTES + assert magic == CoreCoinParams.PSBT_MAGIC_HEADER_BYTES unsigned_tx = None keys_seen: Set[bytes] = set() for key_type, key_data, value in \ diff --git a/bitcointx/util.py b/bitcointx/util.py index c6c6c5ae..8f42c3aa 100644 --- a/bitcointx/util.py +++ b/bitcointx/util.py @@ -563,7 +563,7 @@ def __setattr__(self, name: str, value: Any) -> None: class ContextLocalClassDispatchers(ContextVarsCompat): - _known_identities = ('core', 'wallet', 'script') + _known_identities = ('core', 'wallet', 'script', 'psbt') core: Type[ClassMappingDispatcher] wallet: Type[ClassMappingDispatcher] From df095a27bdde4824f6dd481e35737844a21c371e Mon Sep 17 00:00:00 2001 From: Dmitry Petukhov Date: Wed, 14 Oct 2020 00:41:44 +0500 Subject: [PATCH 08/14] Add util.assert_never for static checks of must-be-unreachable code --- bitcointx/core/psbt.py | 37 ++++++++++++++---------------------- bitcointx/tests/test_psbt.py | 10 +++++----- bitcointx/util.py | 26 ++++++++++++++++++++++++- 3 files changed, 44 insertions(+), 29 deletions(-) diff --git a/bitcointx/core/psbt.py b/bitcointx/core/psbt.py index 1140e6f4..652e0c39 100644 --- a/bitcointx/core/psbt.py +++ b/bitcointx/core/psbt.py @@ -44,7 +44,7 @@ from ..wallet import CCoinExtPubKey from ..util import ( - ensure_isinstance, no_bool_use_as_property, + ensure_isinstance, no_bool_use_as_property, assert_never, ClassMappingDispatcher, activate_class_dispatcher ) @@ -135,13 +135,8 @@ class PSBT_OutKeyType(Enum): ('key_type', int), ('key_data', bytes), ('value', bytes) ]) -T_KeyTypeEnum_Type = Union[ - Type[PSBT_GlobalKeyType], - Type[PSBT_OutKeyType], - Type[PSBT_InKeyType], -] - -T_KeyTypeEnum = Union[PSBT_GlobalKeyType, PSBT_OutKeyType, PSBT_InKeyType] +T_KeyTypeEnum = TypeVar( + 'T_KeyTypeEnum', PSBT_GlobalKeyType, PSBT_OutKeyType, PSBT_InKeyType) def proprietary_field_repr( @@ -296,7 +291,7 @@ def merge_unknown_fields( def read_psbt_keymap( f: ByteStream_Type, keys_seen: Set[bytes], - keys_enum_class: T_KeyTypeEnum_Type, + keys_enum_class: Type[T_KeyTypeEnum], proprietary_fields: Dict[bytes, List[PSBT_ProprietaryTypeData]], unknown_fields: List[PSBT_UnknownTypeData] ) -> Generator[Tuple[T_KeyTypeEnum, bytes, bytes], None, None]: @@ -1293,10 +1288,7 @@ def check_witness_and_nonwitness_utxo_in_sync( ensure_empty_key_data(key_type, key_data, descr('')) proof_of_reserves_commitment = value else: - raise AssertionError( - f'If key type {key_type} is recognized, ' - f'it must be handled, and this statement ' - f'should not be reached.') + assert_never(key_type) # non_witness_utxo is preferred over witness_utxo for `utxo` kwarg # because non_witness_utxo is a full transaction, @@ -1644,13 +1636,13 @@ def descr(msg: str) -> str: read_psbt_keymap(f, keys_seen, PSBT_OutKeyType, proprietary_fields, unknown_fields): - if key_type == PSBT_OutKeyType.REDEEM_SCRIPT: + if key_type is PSBT_OutKeyType.REDEEM_SCRIPT: ensure_empty_key_data(key_type, key_data, descr('')) redeem_script = CScript(value) - elif key_type == PSBT_OutKeyType.WITNESS_SCRIPT: + elif key_type is PSBT_OutKeyType.WITNESS_SCRIPT: ensure_empty_key_data(key_type, key_data, descr('')) witness_script = CScript(value) - elif key_type == PSBT_OutKeyType.BIP32_DERIVATION: + elif key_type is PSBT_OutKeyType.BIP32_DERIVATION: pub = CPubKey(key_data) if not pub.is_fullyvalid(): raise SerializationError( @@ -1660,6 +1652,8 @@ def descr(msg: str) -> str: ("duplicate keys should have been catched " "inside read_psbt_keymap()") derivation_map[pub] = PSBT_KeyDerivationInfo.deserialize(value) + else: + assert_never(key_type) return cls(redeem_script=redeem_script, witness_script=witness_script, derivation_map=derivation_map, @@ -2105,10 +2099,10 @@ def stream_deserialize(cls: Type[T_PartiallySignedTransaction], read_psbt_keymap(f, keys_seen, PSBT_GlobalKeyType, proprietary_fields, unknown_fields): - if key_type == PSBT_GlobalKeyType.UNSIGNED_TX: + if key_type is PSBT_GlobalKeyType.UNSIGNED_TX: ensure_empty_key_data(key_type, key_data) unsigned_tx = CTransaction.deserialize(value) - elif key_type == PSBT_GlobalKeyType.XPUB: + elif key_type is PSBT_GlobalKeyType.XPUB: if key_data[:4] != CCoinExtPubKey.base58_prefix: raise ValueError( f'One of global xpubs has unknown prefix: expected ' @@ -2119,17 +2113,14 @@ def stream_deserialize(cls: Type[T_PartiallySignedTransaction], ("duplicate keys should have been catched " "inside read_psbt_keymap()") xpubs[xpub] = PSBT_KeyDerivationInfo.deserialize(value) - elif key_type == PSBT_GlobalKeyType.VERSION: + elif key_type is PSBT_GlobalKeyType.VERSION: ensure_empty_key_data(key_type, key_data) if len(value) != 4: raise SerializationError( f'Incorrect data length for {key_type.name}') version = struct.unpack(b' Set[PSBT_InKeyType]: assert magic == CoreCoinParams.PSBT_MAGIC_HEADER_BYTES unsigned_tx = None keys_seen: Set[bytes] = set() - for key_type, key_data, value in \ + for key_type_g, key_data, value in \ read_psbt_keymap(f, keys_seen, PSBT_GlobalKeyType, OrderedDict(), list()): - if key_type == PSBT_GlobalKeyType.UNSIGNED_TX: + if key_type_g == PSBT_GlobalKeyType.UNSIGNED_TX: unsigned_tx = CTransaction.deserialize(value) assert unsigned_tx keys_seen = set() key_types_seen: Set[PSBT_InKeyType] = set() assert len(unsigned_tx.vin) == 1 - for key_type, key_data, value in \ + for key_type_in, key_data, value in \ read_psbt_keymap(f, keys_seen, PSBT_InKeyType, OrderedDict(), list()): - assert isinstance(key_type, PSBT_InKeyType) - key_types_seen.add(key_type) + assert isinstance(key_type_in, PSBT_InKeyType) + key_types_seen.add(key_type_in) return key_types_seen diff --git a/bitcointx/util.py b/bitcointx/util.py index 8f42c3aa..924b6d87 100644 --- a/bitcointx/util.py +++ b/bitcointx/util.py @@ -19,11 +19,12 @@ has_contextvars = False import functools +from enum import Enum from types import FunctionType from abc import ABCMeta, ABC, abstractmethod from typing import ( Type, Set, Tuple, List, Dict, Union, Any, Callable, Iterable, Optional, - TypeVar, Generic, cast + TypeVar, Generic, cast, NoReturn ) _secp256k1_library_path: Optional[str] = None @@ -471,6 +472,28 @@ def ensure_isinstance(var: object, raise TypeError(msg) +def assert_never(x: NoReturn) -> NoReturn: + """For use with static checking. The checker such as mypy will raise + error if the statement `assert_never(...)` is reached. At runtime, + an `AssertionError` will be raised. + Useful to ensure that all variants of Enum is handled. + Might become useful in other ways, and because of this, the message + for `AssertionError` at runtime can differ on actual type of the argument. + For full control of the message, just pass a string as the argument. + """ + + if isinstance(x, Enum): + msg = f'Enum {x} is not handled' + elif isinstance(x, str): + msg = x + elif isinstance(x, type): + msg = f'{x.__name__} is not handled' + else: + msg = f'{x.__class__.__name__} is not handled' + + raise AssertionError(msg) + + class ReadOnlyFieldGuard(ABC): """A unique class that is used as a guard type for ReadOnlyField. It cannot be instantiated at runtime, and the static check will also @@ -604,6 +627,7 @@ def set_dispatcher_class(self, identity: str, 'ClassMappingDispatcher', 'classgetter', 'ensure_isinstance', + 'assert_never', 'ReadOnlyField', 'WriteableField', 'ContextVarsCompat', From 3bd0ce82683ccb180dc10451d5ac96b727004131 Mon Sep 17 00:00:00 2001 From: Dmitry Petukhov Date: Tue, 20 Oct 2020 13:19:54 +0500 Subject: [PATCH 09/14] Make CTransaction.deserialize() logic the same as in Bitcoin Core source This also removes the need for the stream to be seekable --- bitcointx/core/__init__.py | 55 +++++++++++++++++++++++--------------- 1 file changed, 33 insertions(+), 22 deletions(-) diff --git a/bitcointx/core/__init__.py b/bitcointx/core/__init__.py index e3682606..d4b95959 100644 --- a/bitcointx/core/__init__.py +++ b/bitcointx/core/__init__.py @@ -1100,21 +1100,35 @@ def stream_deserialize(cls: Type[T_CTransaction], f: ByteStream_Type, inputs. If the behavior of DecodeHexTx() is needed it could be added, but not here. """ - # FIXME can't assume f is seekable nVersion = struct.unpack(b" Date: Tue, 20 Oct 2020 13:32:10 +0500 Subject: [PATCH 10/14] Allow stream in read_psbt_keymap() to not provide tell() f.tell() was used to get the stream position for better error message, but this means the stream has to be seekable. Instead of this constraint, just check if the stream has 'tell' attribute and provide a lambda that return '' for the error message instead of the position --- bitcointx/core/psbt.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bitcointx/core/psbt.py b/bitcointx/core/psbt.py index 652e0c39..c57be211 100644 --- a/bitcointx/core/psbt.py +++ b/bitcointx/core/psbt.py @@ -301,8 +301,9 @@ def read_psbt_keymap( return if key_data in keys_seen: + tellf = getattr(f, 'tell', lambda: '') raise SerializationError( - f'Duplicate key encountered at position {f.tell()}') + f'Duplicate key encountered at position {tellf()}') keys_seen.add(key_data) From d3ff6a85b6593350882166bb381dd96665650646 Mon Sep 17 00:00:00 2001 From: Dmitry Petukhov Date: Tue, 20 Oct 2020 15:16:34 +0500 Subject: [PATCH 11/14] Improve logic around empty witness in CTransaction.__init__ Previously the empty witness that was supplied to CTransaction.__init__ was not checked for compatibility with currently selected witness class for mutable transaction, it just replaced with new CTxWitness() instance. After this change, it is checked that the supplied instance was compatible with new instance --- bitcointx/core/__init__.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/bitcointx/core/__init__.py b/bitcointx/core/__init__.py index d4b95959..ba8a1785 100644 --- a/bitcointx/core/__init__.py +++ b/bitcointx/core/__init__.py @@ -1020,13 +1020,18 @@ def __init__(self, vin: Iterable[CTxIn] = (), vout: Iterable[CTxOut] = (), else: ensure_isinstance(nVersion, int, 'nVersion') - if witness is None or witness.is_null(): - if witness is None and self.is_immutable(): - witness = CTxWitness() + if witness is None or (witness.is_null() and self.is_mutable()): + if self.is_mutable(): + new_witness = CTxWitness([CTxInWitness() for dummy in vin], + [CTxOutWitness() for dummy in vout]) + if witness is not None: + ensure_isinstance(witness, new_witness._immutable_cls, + 'witness') + witness = new_witness else: - witness = CTxWitness( - [CTxInWitness() for dummy in vin], - [CTxOutWitness() for dummy in vout]) + assert witness is None + witness = CTxWitness() + else: witness = CTxWitness.from_witness(witness) From ea73dd9db14e4b9c5de5b175af5bb2f3e8b0cede Mon Sep 17 00:00:00 2001 From: Dmitry Petukhov Date: Tue, 20 Oct 2020 18:28:06 +0500 Subject: [PATCH 12/14] Use unserscore prefix for helper function in tests so pytest ignores it --- bitcointx/tests/test_wallet.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bitcointx/tests/test_wallet.py b/bitcointx/tests/test_wallet.py index f713597c..afe30127 100644 --- a/bitcointx/tests/test_wallet.py +++ b/bitcointx/tests/test_wallet.py @@ -48,7 +48,7 @@ ) -def test_address_implementations( +def _test_address_implementations( test: unittest.TestCase, paramclasses: Optional[Iterable[type]] = None, extra_addr_testfunc: Callable[..., bool] = lambda *args: False @@ -95,7 +95,7 @@ def recursive_check(aclass: type) -> None: class Test_CCoinAddress(unittest.TestCase): def test_address_implementations(self) -> None: - test_address_implementations(self) + _test_address_implementations(self) def test_get_output_size(self) -> None: pub = CPubKey(x('0378d430274f8c5ec1321338151e9f27f4c676a008bdf8638d07c0b6be9ab35c71')) From ee9c1a09b8525530b2d063980972315296c96749 Mon Sep 17 00:00:00 2001 From: Dmitry Petukhov Date: Tue, 20 Oct 2020 18:39:59 +0500 Subject: [PATCH 13/14] Use warnings.warn instead of log.warning in tests --- bitcointx/tests/test_async_and_threading.py | 6 ++---- bitcointx/tests/test_key.py | 10 ++++------ bitcointx/tests/test_scripteval.py | 12 ++++-------- 3 files changed, 10 insertions(+), 18 deletions(-) diff --git a/bitcointx/tests/test_async_and_threading.py b/bitcointx/tests/test_async_and_threading.py index 30cd75b9..a8288904 100644 --- a/bitcointx/tests/test_async_and_threading.py +++ b/bitcointx/tests/test_async_and_threading.py @@ -12,7 +12,7 @@ # pylama:ignore=E501,C901 import unittest -import logging +import warnings import threading import asyncio import ctypes @@ -177,9 +177,7 @@ async def async_regtest() -> None: BitcoinMainnetParams) if issubclass(ContextVarsCompat, threading.local): - logging.basicConfig() - log = logging.getLogger("Test_Threading") - log.warning( + warnings.warn( 'contextvars.ContextVar is unavailable, asyncio contexts ' 'when switching chain params will be broken. ' 'Use python >= 3.7 if you want asyncio compatibility, or ' diff --git a/bitcointx/tests/test_key.py b/bitcointx/tests/test_key.py index d6d23d64..693a45c0 100644 --- a/bitcointx/tests/test_key.py +++ b/bitcointx/tests/test_key.py @@ -12,7 +12,7 @@ # pylama:ignore=E501 import unittest -import logging +import warnings from bitcointx.core.key import CKey, CPubKey from bitcointx.core import x @@ -101,11 +101,9 @@ def test_add_sub(self) -> None: CPubKey.combine(k1.pub, k2.pub, k1.pub.negated(), k2.pub.negated()) else: - logging.basicConfig() - log = logging.getLogger("Test_CKey") - log.warning('secp256k1 does not export pubkey negation function. ' - 'You should use newer version of secp256k1 library. ' - 'Tests that involve key substraction are skipped') + warnings.warn('secp256k1 does not export pubkey negation function. ' + 'You should use newer version of secp256k1 library. ' + 'Tests that involve key substraction are skipped') def test_invalid_key(self) -> None: with self.assertRaises(ValueError): diff --git a/bitcointx/tests/test_scripteval.py b/bitcointx/tests/test_scripteval.py index 76caf903..569c0157 100644 --- a/bitcointx/tests/test_scripteval.py +++ b/bitcointx/tests/test_scripteval.py @@ -15,7 +15,7 @@ import json import os import unittest -import logging +import warnings import ctypes from typing import List, Iterator, Tuple, Set, Optional @@ -77,8 +77,6 @@ def load_test_vectors( Tuple[CScript, CScript, CScriptWitness, int, Set[ScriptVerifyFlag_Type], str, str, str] ]: - logging.basicConfig() - log = logging.getLogger("Test_EvalScript") with open(os.path.dirname(__file__) + '/data/' + name, 'r') as fd: fixme_comment = None num_skipped = 0 @@ -95,8 +93,8 @@ def load_test_vectors( fixme_comment = test_case[1] continue if test_case[0] == 'FIXME_END': - log.warning("SKIPPED {} tests: {}" - .format(num_skipped, fixme_comment)) + warnings.warn("SKIPPED {} tests: {}" + .format(num_skipped, fixme_comment)) fixme_comment = None num_skipped = 0 continue @@ -180,9 +178,7 @@ def test_script_bitcoinconsensus(self) -> None: try: handle = load_bitcoinconsensus_library() except ImportError: - logging.basicConfig() - log = logging.getLogger("Test_EvalScript") - log.warning("libbitcoinconsensus library is not avaliable, not testing bitcoinconsensus module") + warnings.warn("libbitcoinconsensus library is not avaliable, not testing bitcoinconsensus module") return def do_test_bicoinconsensus(handle: Optional[ctypes.CDLL]) -> None: From e0d48b6a55493596ae68fccef774047109000ccb Mon Sep 17 00:00:00 2001 From: Dmitry Petukhov Date: Wed, 21 Oct 2020 19:06:35 +0500 Subject: [PATCH 14/14] Allow to grind ECDSA signature to have low-R --- bitcointx/core/key.py | 59 ++++++++++++++++++++++++++++++---- bitcointx/core/secp256k1.py | 3 ++ bitcointx/tests/test_key.py | 38 ++++++++++++++++++++++ bitcointx/tests/test_psbt.py | 8 ++--- bitcointx/tests/test_wallet.py | 3 +- 5 files changed, 99 insertions(+), 12 deletions(-) diff --git a/bitcointx/core/key.py b/bitcointx/core/key.py index 0b1e4395..6a17304f 100644 --- a/bitcointx/core/key.py +++ b/bitcointx/core/key.py @@ -135,6 +135,23 @@ def load_openssl_library(path: Optional[str] = None) -> Optional[ctypes.CDLL]: return handle +def _raw_sig_has_low_r(raw_sig: bytes) -> bool: + compact_sig = ctypes.create_string_buffer(64) + result = _secp256k1.secp256k1_ecdsa_signature_serialize_compact( + secp256k1_context_sign, compact_sig, raw_sig) + assert result == 1 + + # In DER serialization, all values are interpreted as big-endian, + # signed integers. The highest bit in the integer indicates + # its signed-ness; 0 is positive, 1 is negative. + # When the value is interpreted as a negative integer, + # it must be converted to a positive value by prepending a 0x00 byte + # so that the highest bit is 0. We can avoid this prepending by ensuring + # that our highest bit is always 0, and thus we must check that the + # first byte is less than 0x80. + return compact_sig.raw[0] < 0x80 + + class CKeyBase: """An encapsulated private key @@ -185,25 +202,53 @@ def secret_bytes(self) -> bytes: def pub(self) -> 'CPubKey': return self.__pub - def sign(self, hash: Union[bytes, bytearray]) -> bytes: + def sign(self, hash: Union[bytes, bytearray], *, + _ecdsa_sig_grind_low_r: bool = True, + _ecdsa_sig_extra_entropy: int = 0 + ) -> bytes: + ensure_isinstance(hash, (bytes, bytearray), 'hash') if len(hash) != 32: raise ValueError('Hash must be exactly 32 bytes long') raw_sig = ctypes.create_string_buffer(64) - result = _secp256k1.secp256k1_ecdsa_sign( - secp256k1_context_sign, raw_sig, hash, self.secret_bytes, None, None) - if 1 != result: - assert(result == 0) - raise RuntimeError('secp256k1_ecdsa_sign returned failure') + + if _ecdsa_sig_grind_low_r: + counter = 0 + else: + counter = _ecdsa_sig_extra_entropy + + def maybe_extra_entropy() -> Optional[bytes]: + if counter == 0: + return None + + # mimic Bitcoin Core that uses 32-bit counter for the entropy + assert counter < 2**32 + return counter.to_bytes(4, byteorder="little") + b'\x00'*28 + + while True: + result = _secp256k1.secp256k1_ecdsa_sign( + secp256k1_context_sign, raw_sig, hash, self.secret_bytes, None, + maybe_extra_entropy()) + if 1 != result: + assert(result == 0) + raise RuntimeError('secp256k1_ecdsa_sign returned failure') + + if not _ecdsa_sig_grind_low_r or _raw_sig_has_low_r(raw_sig.raw): + break + + counter += 1 + sig_size0 = ctypes.c_size_t() sig_size0.value = SIGNATURE_SIZE - mb_sig = ctypes.create_string_buffer(sig_size0.value) + mb_sig = ctypes.create_string_buffer(SIGNATURE_SIZE) + result = _secp256k1.secp256k1_ecdsa_signature_serialize_der( secp256k1_context_sign, mb_sig, ctypes.byref(sig_size0), raw_sig) if 1 != result: assert(result == 0) raise RuntimeError('secp256k1_ecdsa_signature_parse_der returned failure') + # secp256k1 creates signatures already in lower-S form, no further # conversion needed. return mb_sig.raw[:sig_size0.value] diff --git a/bitcointx/core/secp256k1.py b/bitcointx/core/secp256k1.py index 50ae9fd4..f969073e 100644 --- a/bitcointx/core/secp256k1.py +++ b/bitcointx/core/secp256k1.py @@ -108,6 +108,9 @@ def _add_function_definitions(_secp256k1: ctypes.CDLL) -> None: _secp256k1.secp256k1_ecdsa_recoverable_signature_serialize_compact.restype = ctypes.c_int _secp256k1.secp256k1_ecdsa_recoverable_signature_serialize_compact.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.POINTER(ctypes.c_int), ctypes.c_char_p] + _secp256k1.secp256k1_ecdsa_signature_serialize_compact.restype = ctypes.c_int + _secp256k1.secp256k1_ecdsa_signature_serialize_compact.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p] + _secp256k1.secp256k1_ecdsa_recover.restype = ctypes.c_int _secp256k1.secp256k1_ecdsa_recover.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_char_p] diff --git a/bitcointx/tests/test_key.py b/bitcointx/tests/test_key.py index 693a45c0..a5d0054f 100644 --- a/bitcointx/tests/test_key.py +++ b/bitcointx/tests/test_key.py @@ -13,6 +13,7 @@ import unittest import warnings +import hashlib from bitcointx.core.key import CKey, CPubKey from bitcointx.core import x @@ -111,3 +112,40 @@ def test_invalid_key(self) -> None: with self.assertRaises(ValueError): CKey(b'\xff'*32) + + def test_signature_grind(self) -> None: + k = CKey(x('12b004fff7f4b69ef8650e767f18f11ede158148b425660723b9f9a66e61f747')) + + msg = "A message to be signed" + msg_hash = hashlib.sha256(msg.encode('ascii')).digest() + + # When explicit entropy is specified, we should see at least one + # high R signature within 20 signatures + high_r_found = False + for i in range(1, 21): + sig = k.sign(msg_hash, + _ecdsa_sig_grind_low_r=False, + _ecdsa_sig_extra_entropy=i) + if sig[3] == 0x21: + self.assertEqual(sig[4], 0x00) + high_r_found = True + break + + self.assertTrue(high_r_found) + + # When grinding for low-R, we should always see low R signatures + # that are less than 70 bytes in 256 tries + # We should see at least one signature that is less than 70 bytes. + small_sig_found = False + for i in range(256): + msg = "A message to be signed" + str(i) + msg_hash = hashlib.sha256(msg.encode('ascii')).digest() + sig = k.sign(msg_hash) + + self.assertLessEqual(len(sig), 70) + self.assertLessEqual(sig[3], 0x20) + + if len(sig) < 70: + small_sig_found = True + + self.assertTrue(small_sig_found) diff --git a/bitcointx/tests/test_psbt.py b/bitcointx/tests/test_psbt.py index 26e1586f..2f7f6903 100644 --- a/bitcointx/tests/test_psbt.py +++ b/bitcointx/tests/test_psbt.py @@ -496,11 +496,11 @@ def test_flow(self) -> None: pclone2.sign(kstore, finalize=False) self.assertEqual(b2x(pclone2.serialize()), - '70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f618765000000220202dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d7483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01010304010000000104475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae2206029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f10d90c6a4f000000800000008000000080220602dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d710d90c6a4f0000008000000080010000800001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e8872202023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e73473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d2010103040100000001042200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903010547522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae2206023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7310d90c6a4f000000800000008003000080220603089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc10d90c6a4f00000080000000800200008000220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000') + '70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f618765000000220202dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d74730440220631a989fe738a92ad01986023312c19214fe2802b39e5cbc1ac3678806c692c3022039db6c387bd267716dfdb3d4d8da50b8e85d213326ba7c7daaa4c0ce41eb922301010304010000000104475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae2206029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f10d90c6a4f000000800000008000000080220602dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d710d90c6a4f0000008000000080010000800001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e8872202023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e73473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d2010103040100000001042200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903010547522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae2206023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7310d90c6a4f000000800000008003000080220603089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc10d90c6a4f00000080000000800200008000220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000') combined_psbt = pclone1.combine(pclone2) self.assertEqual(b2x(combined_psbt.serialize()), - '70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000002202029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01220202dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d7483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01010304010000000104475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae2206029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f10d90c6a4f000000800000008000000080220602dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d710d90c6a4f0000008000000080010000800001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e887220203089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f012202023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e73473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d2010103040100000001042200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903010547522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae2206023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7310d90c6a4f000000800000008003000080220603089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc10d90c6a4f00000080000000800200008000220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000') + '70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000002202029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01220202dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d74730440220631a989fe738a92ad01986023312c19214fe2802b39e5cbc1ac3678806c692c3022039db6c387bd267716dfdb3d4d8da50b8e85d213326ba7c7daaa4c0ce41eb922301010304010000000104475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae2206029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f10d90c6a4f000000800000008000000080220602dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d710d90c6a4f0000008000000080010000800001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e887220203089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f012202023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e73473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d2010103040100000001042200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903010547522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae2206023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7310d90c6a4f000000800000008003000080220603089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc10d90c6a4f00000080000000800200008000220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000') # check signer with keystore that has xpriv, that should sign # all the inputs at once. @@ -520,11 +520,11 @@ def test_flow(self) -> None: # Check finalized PSBT self.assertEqual(b2x(combined_psbt.serialize()), - '70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000000107da00473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae0001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e8870107232200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b20289030108da0400473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f01473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d20147522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae00220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000') + '70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000000107d900473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c014730440220631a989fe738a92ad01986023312c19214fe2802b39e5cbc1ac3678806c692c3022039db6c387bd267716dfdb3d4d8da50b8e85d213326ba7c7daaa4c0ce41eb922301475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae0001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e8870107232200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b20289030108da0400473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f01473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d20147522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae00220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000') # Check extracted CTransaction self.assertEqual(b2x(combined_psbt.extract_transaction().serialize()), - '0200000000010258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd7500000000da00473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752aeffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d01000000232200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f000400473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f01473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d20147522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae00000000') + '0200000000010258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd7500000000d900473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c014730440220631a989fe738a92ad01986023312c19214fe2802b39e5cbc1ac3678806c692c3022039db6c387bd267716dfdb3d4d8da50b8e85d213326ba7c7daaa4c0ce41eb922301475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752aeffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d01000000232200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f000400473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f01473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d20147522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae00000000') self.assertTrue(combined_psbt.is_final()) self.assertTrue(combined_psbt.inputs[0].is_final()) diff --git a/bitcointx/tests/test_wallet.py b/bitcointx/tests/test_wallet.py index afe30127..2fe291bc 100644 --- a/bitcointx/tests/test_wallet.py +++ b/bitcointx/tests/test_wallet.py @@ -443,7 +443,8 @@ def test(self) -> None: ] for vector in test_vectors: secret = CBitcoinKey.from_secret_bytes(x('{:064x}'.format(vector[0]))) - encoded_sig = secret.sign(hashlib.sha256(vector[1].encode('utf8')).digest()) + encoded_sig = secret.sign(hashlib.sha256(vector[1].encode('utf8')).digest(), + _ecdsa_sig_grind_low_r=False) assert(encoded_sig[0] == 0x30) assert(encoded_sig[1] == len(encoded_sig)-2)