Files
seedsigner/tests
kdmukai cf18a0b9f2 Admit bare p2sh outputs as nested single sig change
A p2sh-p2wpkh output's scriptPubKey is a bare p2sh hash. BIP-174 leaves the
redeem script optional on an output, and without it _get_policy types the output
as plain p2sh, which does not match a p2sh-p2wpkh wallet policy. The output then
never reaches the ownership check at all. Two things follow. The user's own
change is displayed as a payment out to a stranger. And an output that keeps a
genuine claim on this seed while repointing its scriptPubKey escapes the
ownership-contradiction refusal by dropping one optional field.

Nested single sig is the only script type whose identity depends on an optional
field its proof does not read: the rebuild is p2sh(p2wpkh(K)) from our own seed
at the claimed path and the inputs' policy, so the missing field is a
discriminator rather than evidence. For p2sh-p2wsh the discriminator and the
proof material are the same field, so its absence is genuine silence and
correctly out of reach.

_policy_shape_matches becomes _is_change_candidate, an instance method, since it
now reads the inputs' policy and the output's verified derivation paths. Beside
the unchanged shape comparison it admits a bare p2sh output under a p2sh-p2wpkh
wallet that carries no m-of-n, supplies exactly one derivation path entry, and
holds one verified claim on this seed. Everything below is unchanged: the
rebuild decides, honest change is counted as change, and a repointed output
raises PSBTOutputOwnershipContradictionError.

The single-entry conditions are what keep that refusal safe. A genuine multisig
output carries one derivation path entry per cosigner, so it never reaches the
contradiction check by this route. The shape that would is a bare p2sh output
withholding both scripts while annotating a single key of a multisig, and no
surveyed coordinator emits one. Every coordinator but Bitcoin Core annotates
only its own change output. Core annotates an output because a descriptor solved
its script, so it holds all n key origins and writes them.
2026-09-29 12:05:35 -05:00
..
2025-10-23 14:23:27 -05:00
2024-11-06 14:00:36 -06:00
2024-11-05 12:19:35 -06:00
2025-12-17 21:11:14 +01:00
2025-12-26 07:13:00 -06:00
2024-03-02 07:46:35 -06:00
2024-07-14 13:21:23 -05:00
2025-01-02 18:38:03 -06:00

Running Tests

The tests are designed to be run on non-Raspi hardware.

Setup

On your testing machine you'll have to install:

# general dependencies
pip3 install -r requirements.txt

# test suite dependencies
pip3 install -r tests/requirements.txt

Then make the seedsigner python module visible/importable to the tests by installing it:

pip3 install -e .

Running all tests, calculating overall test coverage

tldr: just run the convenience script from the project root:

./tests/run_full_coverage.sh

Running tests manually

Run the whole test suite:

pytest

Run a specific test file:

pytest tests/test_this_file.py

Run a specific test:

pytest tests/test_this_file.py::test_this_specific_test

Force pytest to show logging output:

pytest tests/test_this_file.py::test_this_specific_test -o log_cli=1

# or (same result)

pytest tests/test_this_file.py::test_this_specific_test --log-cli-level=DEBUG

Annoying complications:

  • If you want to see print() statements that are in a test file, add -s
  • Better idea: use a proper logger in the test file and use one of the above options to display logs

Screenshot generator

The screenshot generator is meant to mostly be a utility and not really part of the test suite. However, it is actually implemented to be run by pytest.

see: Screenshot generator README

Generate coverage manually

Run tests and generate test coverage

coverage run -m pytest

The screenshots can generate their own separate coverage report:

coverage run -m pytest tests/screenshot_generator/generator.py --locale es

Show the resulting test coverage details:

coverage report

Generate the interactive html report:

coverage html