diff --git a/btclib/alias.py b/btclib/alias.py index ea0e2dd1..0712dc45 100644 --- a/btclib/alias.py +++ b/btclib/alias.py @@ -73,6 +73,7 @@ # Hash digest constructor: it may be any name suitable to hashlib.new() HashF = Callable[[], Any] # HashF = Callable[[Any], Any] +H160_Net = Tuple[bytes, str] # Elliptic curve point in affine coordinates. # Warning: to make Point a NamedTuple would slow down the code @@ -97,3 +98,10 @@ # of the INF Point # QJ = Q[0], Q[1], 1 if Q[1] else 0 INFJ = 7, 0, 0 + +# TODO add type hinting to script_tree +# unfortunately recursive type hinting is not supported +# https://github.com/python/mypy/issues/731 +# TaprootLeaf = Tuple[int, Script] +# TaprootScriptTree = List[Union[Any, TaprootLeaf]] +TaprootScriptTree = Any diff --git a/btclib/b32.py b/btclib/b32.py index 0211a959..0a337f66 100644 --- a/btclib/b32.py +++ b/btclib/b32.py @@ -46,12 +46,12 @@ from typing import Iterable -from btclib.alias import Octets, String +from btclib.alias import Octets, String, TaprootScriptTree from btclib.bech32 import decode, encode from btclib.exceptions import BTClibValueError from btclib.hashes import hash160, sha256 from btclib.network import NETWORKS, network_from_key_value -from btclib.script import TaprootScriptTree, output_pubkey +from btclib.script import output_pubkey from btclib.to_pub_key import Key, pub_keyinfo_from_key from btclib.utils import bytes_from_octets diff --git a/btclib/hashes.py b/btclib/hashes.py index 595bec94..28bb1611 100644 --- a/btclib/hashes.py +++ b/btclib/hashes.py @@ -12,13 +12,11 @@ from __future__ import annotations import hashlib -from typing import Callable, Sequence, Tuple +from typing import Callable, Sequence from btclib.alias import HashF, Octets from btclib.utils import bytes_from_octets -H160_Net = Tuple[bytes, str] - # see https://bugs.python.org/issue47101 # With OpenSSL 3.x, hashlib still includes ripemd160 # but it is not usable unless the legacy provider is loaded. diff --git a/btclib/psbt/psbt.py b/btclib/psbt/psbt.py index dbd6d81d..b64481c2 100644 --- a/btclib/psbt/psbt.py +++ b/btclib/psbt/psbt.py @@ -101,12 +101,12 @@ def __init__( def assert_valid(self) -> None: """Assert logical self-consistency.""" - self.tx.assert_valid() - # ensure a non-null tx has been included if not (self.tx.vin and self.tx.vout): raise BTClibValueError("null transaction") + self.tx.assert_valid() + # ensure the tx is unsigned if any(tx_in.script_sig or tx_in.script_witness for tx_in in self.tx.vin): raise BTClibValueError("non empty script_sig or witness") diff --git a/btclib/script/__init__.py b/btclib/script/__init__.py index 958e5c0a..803d59a3 100644 --- a/btclib/script/__init__.py +++ b/btclib/script/__init__.py @@ -11,7 +11,17 @@ """Module btclib.script.""" from btclib.script.script import Command, Script, op_int, parse, serialize -from btclib.script.script_pub_key import ( +from btclib.script.taproot import ( + TaprootScriptTree, + check_output_pubkey, + input_script_sig, + output_prvkey, + output_pubkey, +) +from btclib.script.witness import Witness + +# hack to prevent circular import +from btclib.script.script_pub_key import ( # isort:skip ScriptPubKey, address, assert_nulldata, @@ -31,14 +41,7 @@ is_p2wsh, type_and_payload, ) -from btclib.script.taproot import ( - TaprootScriptTree, - check_output_pubkey, - input_script_sig, - output_prvkey, - output_pubkey, -) -from btclib.script.witness import Witness + __all__ = [ "Command", diff --git a/btclib/script/taproot.py b/btclib/script/taproot.py index 09a3d1f1..31dc11a9 100644 --- a/btclib/script/taproot.py +++ b/btclib/script/taproot.py @@ -16,7 +16,7 @@ from typing import Any from btclib import var_bytes -from btclib.alias import Octets +from btclib.alias import Octets, TaprootScriptTree from btclib.ec import Curve, mult, secp256k1 from btclib.exceptions import BTClibValueError from btclib.hashes import tagged_hash @@ -25,13 +25,6 @@ from btclib.to_pub_key import Key, pub_keyinfo_from_key from btclib.utils import bytes_from_octets -# TODO add type hinting to script_tree -# unfortunately recursive type hinting is not supported -# https://github.com/python/mypy/issues/731 -# TaprootLeaf = Tuple[int, Script] -# TaprootScriptTree = List[Union[Any, TaprootLeaf]] -TaprootScriptTree = Any - def tree_helper(script_tree: TaprootScriptTree) -> tuple[Any, bytes]: if len(script_tree) == 1: diff --git a/btclib/tx/tx.py b/btclib/tx/tx.py index 0d7545a8..dc2d0569 100644 --- a/btclib/tx/tx.py +++ b/btclib/tx/tx.py @@ -36,7 +36,7 @@ from btclib.alias import BinaryData from btclib.exceptions import BTClibValueError from btclib.hashes import hash256 -from btclib.script import Witness +from btclib.script.witness import Witness from btclib.tx.tx_in import TX_IN_COMPARES_WITNESS, TxIn from btclib.tx.tx_out import TxOut from btclib.utils import bytesio_from_binarydata @@ -193,9 +193,13 @@ def assert_valid(self) -> None: if not 0 <= self.lock_time <= 0xFFFFFFFF: raise BTClibValueError(f"invalid lock time: {self.lock_time}") + if not self.vin: + raise BTClibValueError("Missing inputs") for tx_in in self.vin: tx_in.assert_valid() + if not self.vout: + raise BTClibValueError("Missing outputs") for tx_out in self.vout: tx_out.assert_valid() diff --git a/tests/tx/test_tx.py b/tests/tx/test_tx.py index 4e10cdab..55a1443d 100644 --- a/tests/tx/test_tx.py +++ b/tests/tx/test_tx.py @@ -22,7 +22,7 @@ def test_tx() -> None: # default constructor - tx = Tx() + tx = Tx(check_validity=False) assert not tx.is_segwit() assert not any(bool(w) for w in tx.vwitness) assert not any(bool(tx_in.script_witness) for tx_in in tx.vin) @@ -40,15 +40,22 @@ def test_tx() -> None: assert tx.vsize == tx.size assert tx.weight == tx.size * 4 - tx_2 = Tx.from_dict(tx.to_dict()) + with pytest.raises(BTClibValueError, match="Missing inputs"): + tx.assert_valid() + + tx_2 = Tx.from_dict(tx.to_dict(check_validity=False), check_validity=False) assert tx_2.is_segwit() == tx.is_segwit() assert tx_2 == tx - tx_2 = Tx.parse(tx.serialize(include_witness=True)) + tx_2 = Tx.parse( + tx.serialize(include_witness=True, check_validity=False), check_validity=False + ) assert tx_2.is_segwit() == tx.is_segwit() assert tx_2 == tx - tx_2 = Tx.parse(tx.serialize(include_witness=False)) + tx_2 = Tx.parse( + tx.serialize(include_witness=False, check_validity=False), check_validity=False + ) assert not tx_2.is_segwit() assert tx_2 == tx @@ -60,6 +67,10 @@ def test_tx() -> None: sequence = 0xFFFFFFFF tx_in = TxIn(prev_out, script_sig, sequence) + tx.vin = [tx_in] + with pytest.raises(BTClibValueError, match="Missing outputs"): + tx.assert_valid() + tx_out1 = TxOut(2500000, "a914f987c321394968be164053d352fc49763b2be55c87") tx_out2 = TxOut( 6381891, "0020701a8d401c84fb13e6baf169d59684e17abd9fa216c8cc5b9fc63d622ff8c58d"