Skip to content

Ask a value what it is before asking what it holds - #765

Merged
fametrano merged 2 commits into
sig-hash-widthsfrom
block-type-guards
Aug 13, 2026
Merged

Ask a value what it is before asking what it holds#765
fametrano merged 2 commits into
sig-hash-widthsfrom
block-type-guards

Conversation

@fametrano

@fametrano fametrano commented Aug 13, 2026

Copy link
Copy Markdown
Member

The fourth slice of #744.
Stacked on #764, which is
stacked on #763. GitHub
retargets each as its base merges; review the last commit.

What was open

Ten places compared, added to, or read an attribute off an argument
nothing had checked:

what leaked example
a bare TypeError about operands "5" <= 16, "2015" + 1, "hard" <= 0 — raised from underneath the library, naming neither the parameter nor the function
an AttributeError .tzinfo on a str, and total_seconds on the int two unix timestamps subtract to — outside both halves of the contract, so nothing a caller is told to catch would have caught it

The guard is var_int.serialize's, and the vocabulary is
utils.is_integer's: a bool is not a number, because it would be the
height one, the block count one, the leaf index one.

The ten

  • block.bip34_commitment, and Block.assert_valid_coinbase_height
    through it
  • BlockHeader.assert_valid's timestamp — the default
    check_validity=True path was as exposed as the explicit one
  • BlockHeader.assert_valid_time's now
  • mining.mine's max_tries and its header, which
    dataclasses.replace used to complain about ("should be called on
    dataclass instances", about a call the caller never made)
  • proof_of_work.next_bits's two datetimes
  • proof_of_work.retarget_first_height
  • all three numbers of proof_of_work.hash_rate — a difficulty and a
    timespan are float, and an integer is one of those, so the check is
    "a number, and not a bool"; a block count is a count, so it is
    is_integer
  • hashes.merkle_root_from_branch's leaf index, which is what
    merkle_proof.assert_as_valid and merkle_proof.verify reach it
    through — one fix for three entry points
  • utils.encode_num

Worth a reviewer's eye

  • BlockHeader._assert_valid_types is an extraction, not only a
    check.
    assert_valid was one branch under C901's ten; pyproject's
    mccabe comment prefers a refactor to a noqa. It gathers the two
    is_integer checks that were already there with the new datetime one.
  • One # type: ignore[unreachable], on the timestamp check:
    self.time is annotated datetime, so mypy cannot see the caller
    this exists for. Local, at the one real exception, rather than a
    blanket disable.
  • merkle_proof.verify's comment stays true. It says a TypeError
    is a caller error and not a verdict; BTClibTypeError is a
    TypeError, so it still propagates through the except ValueError
    unchanged — the class is narrower, the control flow identical.
  • Eight new _CASES in tests/integer_policy_test.py, beside the
    twenty-five already there, with their positive assertions: a test that
    only checks refusals passes just as well when the field refuses
    everything. hash_rate(1, 600) == hash_rate(1.0, 600.0) is the one
    that pins the int-is-a-number half.

Gates

  • uv run pytest — 26505 passed, coverage 100.00%
  • uv run pre-commit run --all-files — exit 0
  • sphinx-build -W --keep-going — exit 0

Summary by Sourcery

Add explicit type validation across block, proof-of-work, hashing, mining, and utility APIs so incorrect argument types fail with BTClibTypeError instead of leaking bare TypeError/AttributeError from underlying operations.

Enhancements:

  • Refactor BlockHeader validity checks into a dedicated _assert_valid_types helper that also validates the header timestamp type.
  • Tighten integer and datetime handling for bip34_commitment, encode_num, proof-of-work retargeting, difficulty adjustment, and hash_rate calculations, rejecting bools and non-numeric inputs.
  • Ensure merkle_root_from_branch validates the leaf index type before sign and range checks, aligning with the library’s exception contract.
  • Add explicit type checks for mining.mine header and max_tries parameters to prevent dataclasses.replace and range from raising unexpected errors.

Documentation:

  • Document the new type validation behavior and affected entry points in the changelog under the public API section.

Tests:

  • Extend integer policy, block, block context, mining, and proof_of_work tests to cover the new type validation paths and confirm accepted numeric inputs still behave as before.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @fametrano, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@sourcery-ai

sourcery-ai Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

This PR adds explicit type validation and consistent BTClibTypeError/BTClibValueError handling around ten call sites that previously relied on bare Python TypeError/AttributeError, and factors BlockHeader type checks into a helper, with tests and changelog updated to match the new integer/number policy.

Sequence diagram for merkle_proof verification using merkle_root_from_branch

sequenceDiagram
    actor Caller
    participant merkle_proof
    participant hashes

    Caller->>merkle_proof: verify(index, branch, root)
    merkle_proof->>hashes: merkle_root_from_branch(index, branch, root)
    alt [index not is_integer]
        hashes-->>Caller: BTClibTypeError
    else [index is_integer]
        hashes-->>merkle_proof: computed_root
        merkle_proof-->>Caller: verification_result
    end
Loading

File-Level Changes

Change Details Files
BlockHeader now validates its numeric and timestamp field types via a dedicated helper and assert_valid_time checks the type of now before timezone logic.
  • Added _assert_valid_types to centralize type checks for version, nonce, and time, using is_integer and isinstance(datetime).
  • Updated assert_valid to call _assert_valid_types instead of inlined integer checks.
  • Extended assert_valid_time to raise BTClibTypeError when now is not a datetime before accessing tzinfo, and added a targeted type: ignore for unreachable timestamp check in mypy.
btclib/block/block_header.py
tests/block/block_test.py
tests/block/block_context_test.py
Proof-of-work helpers now validate argument types (heights, datetimes, and hash rate parameters) and raise BTClibTypeError instead of leaking bare TypeError/AttributeError.
  • retarget_first_height now uses is_integer and raises BTClibTypeError for non-integer heights.
  • next_bits now validates its first and last block times as datetime instances before subtracting and calling total_seconds.
  • hash_rate now enforces float-or-int (but not bool) for difficulty and timespan, and integer-only for block_count, raising BTClibTypeError on violations.
  • Tests added to cover new type checks and expected BTClibTypeError behavior.
btclib/block/proof_of_work.py
tests/block/proof_of_work_test.py
Mining now validates the candidate header and max_tries types up front, aligning errors with BTClibTypeError/BTClibValueError and avoiding dataclasses.replace and range-related runtime errors.
  • mine checks that header is a BlockHeader instance and raises BTClibTypeError otherwise.
  • mine checks max_tries with is_integer and raises BTClibTypeError for non-integers before the numeric comparison and range usage.
  • Added tests that exercise invalid header and max_tries inputs and assert on BTClibTypeError messages.
btclib/block/mining.py
tests/block/mining_test.py
Block-level bip34_commitment now enforces integer heights with is_integer, preventing bool/non-numeric values from reaching comparisons or op_int, and tests ensure correct behavior.
  • Added an is_integer-based type guard on height in bip34_commitment that raises BTClibTypeError for invalid types.
  • Extended integer policy tests to include bip34_commitment with both refusal cases and positive behavior.
  • Imported is_integer and BTClibTypeError where needed.
btclib/block/block.py
tests/integer_policy_test.py
Merkle branch handling enforces integer leaf indices using is_integer and raises BTClibTypeError on invalid types, with integer policy tests covering the behavior.
  • merkle_root_from_branch guards index with is_integer and raises BTClibTypeError for non-integer/boolean-like indices before sign checks.
  • Integer policy tests add a "merkle leaf index" case and confirm valid index behavior remains unchanged.
btclib/hashes.py
tests/integer_policy_test.py
encode_num now applies the shared integer policy before range checks, refusing non-integer or bool inputs with BTClibTypeError, and tests verify both rejection and acceptance paths.
  • Added is_integer-based guard in encode_num that raises BTClibTypeError for non-integer script numbers prior to bound comparisons.
  • Extended integer policy tests with a "script number" case and positive assertions for encode_num(1).
  • Updated imports in utils and integer_policy_test to include encode_num and is_integer where required.
btclib/utils.py
tests/integer_policy_test.py
Integer policy test suite has been extended with new call sites (bip34_commitment, hash_rate, retarget_first_height, mine, merkle_root_from_branch, encode_num) to ensure bool refusal does not accidentally reject valid integers and to pin numeric behavior.
  • Added multiple _CASES entries in integer_policy_test to cover the newly guarded functions and their error modes when given bool or non-integer inputs.
  • Added positive assertions confirming all new sites accept legitimate integer inputs and that int/float equivalence for hash_rate is preserved.
  • Adjusted imports to pull in the new functions under test and associated helpers.
tests/integer_policy_test.py
Changelog documents the ten newly guarded call sites and clarifies the integer/number policy used (is_integer, bool is not a number) and the exception contract implications. CHANGELOG.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Ten places in `block/`, `hashes` and `utils` compared, added to or read
an attribute off an argument nothing had checked.  `"5" <= 16`,
`"2015" + 1` and `"hard" <= 0` are bare TypeErrors about operands,
raised from underneath the library and naming neither the parameter nor
the function; `.tzinfo` on a str is an AttributeError, outside *both*
halves of this library's exception contract, so nothing a caller is
told to catch would have caught it; and two datetimes given as unix
timestamps subtract to an int, whose missing `total_seconds` is an
AttributeError again.

The guard is `var_int.serialize`'s and the vocabulary `is_integer`'s: a
bool is not a number, it being the height one, the block count one, the
leaf index one.

`bip34_commitment`, and `Block.assert_valid_coinbase_height` through
it; `BlockHeader.assert_valid`'s timestamp and `assert_valid_time`'s
`now`; `mining.mine`'s max_tries and its header, which
`dataclasses.replace` used to complain about; `next_bits`'s two
datetimes; `retarget_first_height`; all three numbers of `hash_rate`,
where a difficulty and a timespan are float and an integer is one of
those; `hashes.merkle_root_from_branch`'s leaf index, which is what
`merkle_proof.assert_as_valid` and `merkle_proof.verify` reach it
through; and `utils.encode_num`.

`BlockHeader._assert_valid_types` is an extraction and not only a
check: `assert_valid` was one branch under C901's ten.  The bool half
of the eight new integer guards goes in `tests/integer_policy_test.py`,
where the other twenty-five already are.

The fourth slice of #744.
It is the coercion every `Octets` parameter of the library runs
through, and it had two holes.  A hex string that is not one left
through `bytes.fromhex`'s bare ValueError -- the class the contract
promises, with nothing saying it came from here.  And anything that was
not a `str` went through *untouched*, so `len` of a tuple of 33 ints
was 33, which is how `taproot.assert_valid_control_block` accepted one
as a control block size and how `bin_str_entropy_from_entropy(())` was
reported as zero bits.

Both are refused now.  Every buffer is still taken, and returned as it
came: a read must not rewrite the field it reads, which is what
`bytes()` here would do to a bytearray a caller built.

The message is `bytes.fromhex`'s own, which names a position and never
the string: an Octets parameter is candidate key material as often as
not, and `to_prv_key` puts this very message inside its own "not a
private key" (issue #137).  `to_prv_key`'s two "it must be octets"
fallbacks catch a TypeError beside the ValueError, as `to_pub_key`
already did.

`mnemonic.entropy`'s five `int(x, 2)` parses and its `int(x, 16)` and
`int(x)` ones are BTClibValueError too, and say a base rather than the
digits.  `borromean.sign` checks that its rings, signing indexes and
nonces are of one length rather than leaving it to `zip(strict=True)`,
whose message named "argument 3" and no parameter of the function;
strict=True stays, as the assertion that the check and the loops cannot
drift apart.

Six test files stop asserting the bare class, `docs/source/guide.rst`
shows the new spelling, and HISTORY.md carries the breaking-changes
bullet for this and for the three slices before it.

The last slice of #744.
@fametrano
fametrano merged commit 0720f48 into sig-hash-widths Aug 13, 2026
5 of 6 checks passed
@fametrano
fametrano deleted the block-type-guards branch August 13, 2026 19:39
fametrano added a commit that referenced this pull request Aug 13, 2026
* Ask a value what it is before asking what it holds

Ten places in `block/`, `hashes` and `utils` compared, added to or read
an attribute off an argument nothing had checked.  `"5" <= 16`,
`"2015" + 1` and `"hard" <= 0` are bare TypeErrors about operands,
raised from underneath the library and naming neither the parameter nor
the function; `.tzinfo` on a str is an AttributeError, outside *both*
halves of this library's exception contract, so nothing a caller is
told to catch would have caught it; and two datetimes given as unix
timestamps subtract to an int, whose missing `total_seconds` is an
AttributeError again.

The guard is `var_int.serialize`'s and the vocabulary `is_integer`'s: a
bool is not a number, it being the height one, the block count one, the
leaf index one.

`bip34_commitment`, and `Block.assert_valid_coinbase_height` through
it; `BlockHeader.assert_valid`'s timestamp and `assert_valid_time`'s
`now`; `mining.mine`'s max_tries and its header, which
`dataclasses.replace` used to complain about; `next_bits`'s two
datetimes; `retarget_first_height`; all three numbers of `hash_rate`,
where a difficulty and a timespan are float and an integer is one of
those; `hashes.merkle_root_from_branch`'s leaf index, which is what
`merkle_proof.assert_as_valid` and `merkle_proof.verify` reach it
through; and `utils.encode_num`.

`BlockHeader._assert_valid_types` is an extraction and not only a
check: `assert_valid` was one branch under C901's ten.  The bool half
of the eight new integer guards goes in `tests/integer_policy_test.py`,
where the other twenty-five already are.

The fourth slice of #744.

* Make `bytes_from_octets` the whole of what Octets means (#766)

It is the coercion every `Octets` parameter of the library runs
through, and it had two holes.  A hex string that is not one left
through `bytes.fromhex`'s bare ValueError -- the class the contract
promises, with nothing saying it came from here.  And anything that was
not a `str` went through *untouched*, so `len` of a tuple of 33 ints
was 33, which is how `taproot.assert_valid_control_block` accepted one
as a control block size and how `bin_str_entropy_from_entropy(())` was
reported as zero bits.

Both are refused now.  Every buffer is still taken, and returned as it
came: a read must not rewrite the field it reads, which is what
`bytes()` here would do to a bytearray a caller built.

The message is `bytes.fromhex`'s own, which names a position and never
the string: an Octets parameter is candidate key material as often as
not, and `to_prv_key` puts this very message inside its own "not a
private key" (issue #137).  `to_prv_key`'s two "it must be octets"
fallbacks catch a TypeError beside the ValueError, as `to_pub_key`
already did.

`mnemonic.entropy`'s five `int(x, 2)` parses and its `int(x, 16)` and
`int(x)` ones are BTClibValueError too, and say a base rather than the
digits.  `borromean.sign` checks that its rings, signing indexes and
nonces are of one length rather than leaving it to `zip(strict=True)`,
whose message named "argument 3" and no parameter of the function;
strict=True stays, as the assertion that the check and the loops cannot
drift apart.

Six test files stop asserting the bare class, `docs/source/guide.rst`
shows the new spelling, and HISTORY.md carries the breaking-changes
bullet for this and for the three slices before it.

The last slice of #744.
fametrano added a commit that referenced this pull request Aug 13, 2026
* Ask a value what it is before asking what it holds

Ten places in `block/`, `hashes` and `utils` compared, added to or read
an attribute off an argument nothing had checked.  `"5" <= 16`,
`"2015" + 1` and `"hard" <= 0` are bare TypeErrors about operands,
raised from underneath the library and naming neither the parameter nor
the function; `.tzinfo` on a str is an AttributeError, outside *both*
halves of this library's exception contract, so nothing a caller is
told to catch would have caught it; and two datetimes given as unix
timestamps subtract to an int, whose missing `total_seconds` is an
AttributeError again.

The guard is `var_int.serialize`'s and the vocabulary `is_integer`'s: a
bool is not a number, it being the height one, the block count one, the
leaf index one.

`bip34_commitment`, and `Block.assert_valid_coinbase_height` through
it; `BlockHeader.assert_valid`'s timestamp and `assert_valid_time`'s
`now`; `mining.mine`'s max_tries and its header, which
`dataclasses.replace` used to complain about; `next_bits`'s two
datetimes; `retarget_first_height`; all three numbers of `hash_rate`,
where a difficulty and a timespan are float and an integer is one of
those; `hashes.merkle_root_from_branch`'s leaf index, which is what
`merkle_proof.assert_as_valid` and `merkle_proof.verify` reach it
through; and `utils.encode_num`.

`BlockHeader._assert_valid_types` is an extraction and not only a
check: `assert_valid` was one branch under C901's ten.  The bool half
of the eight new integer guards goes in `tests/integer_policy_test.py`,
where the other twenty-five already are.

The fourth slice of #744.

* Make `bytes_from_octets` the whole of what Octets means (#766)

It is the coercion every `Octets` parameter of the library runs
through, and it had two holes.  A hex string that is not one left
through `bytes.fromhex`'s bare ValueError -- the class the contract
promises, with nothing saying it came from here.  And anything that was
not a `str` went through *untouched*, so `len` of a tuple of 33 ints
was 33, which is how `taproot.assert_valid_control_block` accepted one
as a control block size and how `bin_str_entropy_from_entropy(())` was
reported as zero bits.

Both are refused now.  Every buffer is still taken, and returned as it
came: a read must not rewrite the field it reads, which is what
`bytes()` here would do to a bytearray a caller built.

The message is `bytes.fromhex`'s own, which names a position and never
the string: an Octets parameter is candidate key material as often as
not, and `to_prv_key` puts this very message inside its own "not a
private key" (issue #137).  `to_prv_key`'s two "it must be octets"
fallbacks catch a TypeError beside the ValueError, as `to_pub_key`
already did.

`mnemonic.entropy`'s five `int(x, 2)` parses and its `int(x, 16)` and
`int(x)` ones are BTClibValueError too, and say a base rather than the
digits.  `borromean.sign` checks that its rings, signing indexes and
nonces are of one length rather than leaving it to `zip(strict=True)`,
whose message named "argument 3" and no parameter of the function;
strict=True stays, as the assertion that the check and the loops cannot
drift apart.

Six test files stop asserting the bare class, `docs/source/guide.rst`
shows the new spelling, and HISTORY.md carries the breaking-changes
bullet for this and for the three slices before it.

The last slice of #744.
fametrano added a commit that referenced this pull request Aug 13, 2026
* Check every width a sig_hash preimage writes, and every index

`int.to_bytes` answers a field too wide for it with an OverflowError,
an ArithmeticError that no `except BTClibValueError` catches; a list
index out of range is an IndexError, a LookupError, outside it too.

#724 made the version and lock-time checks unconditional in
`Tx.serialize`, which closed those two fields for `legacy` -- the one
sig_hash routed through it.  `segwit_v0` and `taproot` assemble their
preimage from their own `to_bytes` calls and never reach it, and
`TxIn.serialize` and `TxOut.serialize` check nothing when told not to,
so the same leak survived on the sequence, on the output value and on
the outpoint's vout.

The checks go in the serializations rather than in the callers --
`_serialized_4_byte_field`, `_serialized_camount`,
`_serialized_out_point`, `_serialized_output`, `_serialized_spend_type`
-- so `PrecomputedTxData` and both `sha_`/`hash_` families get them for
free.  `legacy` checks what its branches leave behind, NONE dropping
the outputs it does not commit to.

`vin_i` is bounded in `legacy`, `segwit_v0`, `from_tx` and
`taproot_annex_and_ext` as it is in `taproot`; `from_tx` also refuses a
prevout list of a different length from the vin, with the message
`PrecomputedTxData` gives for the same mismatch.  `ext_flag` is seven
bits, and `message_extension` takes Octets like every other octets
parameter here.

`_assert_valid_4_byte_field` is imported from `tx.tx` rather than
written a second time, and `_serialized_spend_type` is an extraction
and not only a check: `taproot` was one branch under C901's ten.

The third slice of #744.

* Ask a value what it is before asking what it holds (#765)

* Ask a value what it is before asking what it holds

Ten places in `block/`, `hashes` and `utils` compared, added to or read
an attribute off an argument nothing had checked.  `"5" <= 16`,
`"2015" + 1` and `"hard" <= 0` are bare TypeErrors about operands,
raised from underneath the library and naming neither the parameter nor
the function; `.tzinfo` on a str is an AttributeError, outside *both*
halves of this library's exception contract, so nothing a caller is
told to catch would have caught it; and two datetimes given as unix
timestamps subtract to an int, whose missing `total_seconds` is an
AttributeError again.

The guard is `var_int.serialize`'s and the vocabulary `is_integer`'s: a
bool is not a number, it being the height one, the block count one, the
leaf index one.

`bip34_commitment`, and `Block.assert_valid_coinbase_height` through
it; `BlockHeader.assert_valid`'s timestamp and `assert_valid_time`'s
`now`; `mining.mine`'s max_tries and its header, which
`dataclasses.replace` used to complain about; `next_bits`'s two
datetimes; `retarget_first_height`; all three numbers of `hash_rate`,
where a difficulty and a timespan are float and an integer is one of
those; `hashes.merkle_root_from_branch`'s leaf index, which is what
`merkle_proof.assert_as_valid` and `merkle_proof.verify` reach it
through; and `utils.encode_num`.

`BlockHeader._assert_valid_types` is an extraction and not only a
check: `assert_valid` was one branch under C901's ten.  The bool half
of the eight new integer guards goes in `tests/integer_policy_test.py`,
where the other twenty-five already are.

The fourth slice of #744.

* Make `bytes_from_octets` the whole of what Octets means (#766)

It is the coercion every `Octets` parameter of the library runs
through, and it had two holes.  A hex string that is not one left
through `bytes.fromhex`'s bare ValueError -- the class the contract
promises, with nothing saying it came from here.  And anything that was
not a `str` went through *untouched*, so `len` of a tuple of 33 ints
was 33, which is how `taproot.assert_valid_control_block` accepted one
as a control block size and how `bin_str_entropy_from_entropy(())` was
reported as zero bits.

Both are refused now.  Every buffer is still taken, and returned as it
came: a read must not rewrite the field it reads, which is what
`bytes()` here would do to a bytearray a caller built.

The message is `bytes.fromhex`'s own, which names a position and never
the string: an Octets parameter is candidate key material as often as
not, and `to_prv_key` puts this very message inside its own "not a
private key" (issue #137).  `to_prv_key`'s two "it must be octets"
fallbacks catch a TypeError beside the ValueError, as `to_pub_key`
already did.

`mnemonic.entropy`'s five `int(x, 2)` parses and its `int(x, 16)` and
`int(x)` ones are BTClibValueError too, and say a base rather than the
digits.  `borromean.sign` checks that its rings, signing indexes and
nonces are of one length rather than leaving it to `zip(strict=True)`,
whose message named "argument 3" and no parameter of the function;
strict=True stays, as the assertion that the check and the loops cannot
drift apart.

Six test files stop asserting the bare class, `docs/source/guide.rst`
shows the new spelling, and HISTORY.md carries the breaking-changes
bullet for this and for the three slices before it.

The last slice of #744.

* Gate the input-validation rule, and let the gate enumerate (#774)

`tests/input_validation_test.py` holds every public module-level
function whose required parameters are all library input types to the
rule of #744: a malformed argument leaves as a BTClibException.  One
predicate and not a tuple, which is what #743's base class was landed
for.

It calls with every argument malformed at once, and that is what makes
it automatic: no valid values have to be tabulated -- a valid Octets is
20 bytes here, 32 there and any length elsewhere -- and whichever
argument the function refuses first, the rule says it must refuse it as
a btclib error.

Three lists carry what the run finds, and each ratchets one way.
`_MALFORMED` is the vocabulary, and a type renamed out of it fails
rather than shrinking the walk in silence.  `_EXCLUDED` is the nine
`is_p2*` predicates, with the reason `script_pub_key._is_funct` already
gives.  `_OPEN` is what the census of #744 has left, each entry naming
the class that escapes; an entry that has become compliant fails the
run, as RUF100 fails an unused noqa, so a fix cannot land without
deleting its line.

What the walk cannot reach is stated rather than omitted: a parameter
behind a default is never driven, `hf` and `network` among them, and a
function taking a Tx, a Psbt or a callback needs an instance the
vocabulary cannot build.

It found what the reading missed: `ecc.dleq.verify_proof` answers False
for a pub key that is None, where its own comment says a caller error
must raise -- the shape #745 closed in five other verifications, in a
function written after that census was taken.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant