diff --git a/bitcointx/__init__.py b/bitcointx/__init__.py index 498573cc..97e4c451 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 @@ -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/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/__init__.py b/bitcointx/core/__init__.py index 62571960..ba8a1785 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: @@ -801,8 +802,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 @@ -1015,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) @@ -1095,21 +1105,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" 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/psbt.py b/bitcointx/core/psbt.py index 00b39da1..c57be211 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, assert_never, + 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 @@ -100,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( @@ -261,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]: @@ -271,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) @@ -354,7 +385,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] @@ -376,11 +407,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 +469,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 +498,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, @@ -1246,10 +1289,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, @@ -1361,31 +1401,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 @@ -1396,8 +1440,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 +1460,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: @@ -1585,13 +1637,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( @@ -1601,6 +1653,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, @@ -1627,24 +1681,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] @@ -1658,7 +1715,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 +1731,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) @@ -1968,8 +2028,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') @@ -1979,10 +2039,10 @@ 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(CoreCoinParams.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(CoreCoinParams.PSBT_MAGIC_HEADER_BASE64 + .encode('ascii')): return cls.deserialize(base64.b64decode(data_b.decode('ascii'), validate=validate), **kwargs) @@ -2024,8 +2084,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() @@ -2039,10 +2100,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 ' @@ -2053,17 +2114,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' 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/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): ... diff --git a/bitcointx/core/secp256k1.py b/bitcointx/core/secp256k1.py index 7e58a352..f969073e 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, @@ -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_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..a5d0054f 100644 --- a/bitcointx/tests/test_key.py +++ b/bitcointx/tests/test_key.py @@ -12,7 +12,8 @@ # pylama:ignore=E501 import unittest -import logging +import warnings +import hashlib from bitcointx.core.key import CKey, CPubKey from bitcointx.core import x @@ -101,11 +102,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): @@ -113,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 fe04b43b..2f7f6903 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 @@ -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()) @@ -676,23 +676,23 @@ 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 \ + 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/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: diff --git a/bitcointx/tests/test_wallet.py b/bitcointx/tests/test_wallet.py index f713597c..2fe291bc 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')) @@ -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) diff --git a/bitcointx/util.py b/bitcointx/util.py index c6c6c5ae..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 @@ -563,7 +586,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] @@ -604,6 +627,7 @@ def set_dispatcher_class(self, identity: str, 'ClassMappingDispatcher', 'classgetter', 'ensure_isinstance', + 'assert_never', 'ReadOnlyField', 'WriteableField', 'ContextVarsCompat', 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)