diff --git a/tests/requirements.txt b/tests/requirements.txt index 51e26f396..0a458f52e 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,3 +1,4 @@ coverage==7.6.12 pytest==7.4.2 pytest-cov==4.1.0 +hypothesis==6.100.1 diff --git a/tests/test_psbt_parser.py b/tests/test_psbt_parser.py index a3c1ab913..fec4a1a50 100644 --- a/tests/test_psbt_parser.py +++ b/tests/test_psbt_parser.py @@ -1,7 +1,17 @@ import pytest import random +import sys +import types from binascii import a2b_base64 + +# Other test modules may insert a MagicMock into sys.modules["numpy"]. +# Hypothesis treats any present "numpy" as real and attempts to import +# numpy.random, so remove non-module stubs before using Hypothesis. +if "numpy" in sys.modules and not isinstance(sys.modules["numpy"], types.ModuleType): + del sys.modules["numpy"] + +from hypothesis import given, settings, strategies as st from embit import bip32 from embit.psbt import PSBT from embit.descriptor import Descriptor @@ -13,6 +23,20 @@ from psbt_testing_util import PSBTTestData, create_output +KNOWN_GOOD_SINGLE_SIG_PSBT_BLOBS = ( + PSBTTestData.SINGLE_SIG_NATIVE_SEGWIT_1_INPUT, + PSBTTestData.SINGLE_SIG_NESTED_SEGWIT_1_INPUT, + PSBTTestData.SINGLE_SIG_TAPROOT_1_INPUT, + PSBTTestData.SINGLE_SIG_LEGACY_P2PKH_1_INPUT, +) + + +@pytest.fixture(autouse=True) +def _remove_mock_numpy_module(): + if "numpy" in sys.modules and not isinstance(sys.modules["numpy"], types.ModuleType): + del sys.modules["numpy"] + + class TestPSBTParser: """ @@ -485,3 +509,18 @@ def test_parse_op_return_content(): assert psbt_parser.change_amount == 99992296 assert psbt_parser.destination_addresses == [] assert psbt_parser.destination_amounts == [] + + +@settings(max_examples=100, deadline=None) +@given(blob_index=st.integers(min_value=0, max_value=len(KNOWN_GOOD_SINGLE_SIG_PSBT_BLOBS) - 1)) +def test_hypothesis_known_single_sig_amount_invariant(blob_index: int): + """ + Competency test: + Replay known-good single-sig PSBT fixtures and ensure parser amount + accounting always satisfies the core invariant. + """ + psbt_base64 = KNOWN_GOOD_SINGLE_SIG_PSBT_BLOBS[blob_index] + tx = PSBT.parse(a2b_base64(psbt_base64)) + parser = PSBTParser(p=tx, seed=PSBTTestData.seed, network=SettingsConstants.REGTEST) + + assert parser.input_amount == parser.spend_amount + parser.change_amount + parser.fee_amount