Refuse the malformed input twelve places answered anyway - #763
Merged
Conversation
Contributor
There was a problem hiding this comment.
Sorry @fametrano, you have reached your weekly rate limit of 500000 diff characters.
Please try again later or upgrade to continue using Sourcery
Contributor
Reviewer's GuideThis PR hardens twelve previously under-validated call sites across the library so that malformed numeric and index-like inputs are refused with BTClib-specific errors instead of silently producing plausible but wrong results or leaking Python-native exceptions. It does so by introducing small, localized validation helpers, widening some APIs to accept generic octet inputs, and adding focused tests (including central integer-policy tests) that verify both correct behavior and new failure modes. Sequence diagram for tx_for_network network resolution and labelingsequenceDiagram
actor Caller
participant tx_for_network
participant network_from_name
participant NETWORKS
Caller->>tx_for_network: tx_for_network(tx, network)
tx_for_network->>network_from_name: network_from_name(network)
network_from_name-->>tx_for_network: Network
tx_for_network->>NETWORKS: NETWORKS["mainnet"]
alt resolved network is mainnet
tx_for_network-->>Caller: tx (unchanged)
else other network
tx_for_network->>tx_for_network: construct new TxOut labels
tx_for_network-->>Caller: relabeled tx
end
Sequence diagram for Psbt.weight_estimate with psbt validationsequenceDiagram
actor Caller
participant Psbt
Caller->>Psbt: weight_estimate(sizer)
Psbt->>Psbt: assert_valid()
Psbt-->>Psbt: psbt data validated
Psbt->>Psbt: build transaction from psbt fields
Psbt-->>Caller: estimated weight (int)
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
This was referenced Aug 13, 2026
fametrano
force-pushed
the
wrong-answers
branch
from
August 13, 2026 17:46
624ba26 to
991d0ef
Compare
Not an exception of the wrong class: no exception at all, and a value the caller goes on to use. `sig_hash.taproot` hashed an input index past the end of the vin, two out-of-range indexes giving two different hashes; `bech32.encode` indexed its alphabet from the end for a negative digit and wrote another address; `input_script_sig` read -1 as the last leaf and proved it; `indexes_from_der_path` handed `[-5]` back; `pub_key_derivation_tweaks` never looked at the key for a path of no steps; `miniscript_solver` solved the input at the other end; `weight_estimate` estimated an incoherent psbt; `number_theory`'s five answered a float with a float; `int_from_json_number` truncated 1.5; `bin_str_entropy_from_wordlist_indexes` carried an index no word answers to; `tx_for_network` baked a network no table has into every output; `assert_valid_control_block` measured characters. The first slice of issue #744, and the one it puts first: the only category of that census that can cost a user money. Two tests asserted the defect and now assert the fix, both in bip32: `derive(xprv, 2**32)` was an `OverflowError` and is a `BTClibValueError`, and `BIP32KeyOrigin`'s out-of-range index is refused by the path reader before `assert_valid` sees it -- which still asks, the field being a list the frozen dataclass cannot stop an append to. The bool half of the six new integer guards goes in `tests/integer_policy_test.py`, beside the twenty-five parameters already there rather than in six files. Closes part of #744.
fametrano
force-pushed
the
wrong-answers
branch
from
August 13, 2026 19:32
991d0ef to
fe83e2b
Compare
This was referenced Aug 13, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The first slice of #744, and
the one the issue puts first: the only category of that census that
can cost a user money. Not an exception of the wrong class — no
exception at all, and a value the caller goes on to use.
The twelve
script/sig_hash.pytaprootinput_indexpast the end of the vin. Nothing dereferences it outside the ANYONECANPAY branch, so 99 and 100 on a two-input transaction gave two different 32-byte hashes, both returned. The bound was in the SIGHASH_SINGLE branch alone, and against the voutscript/taproot.pyinput_script_sigscript_num=-1picked the last leaf,-2the one before it, each with a control block that correctly proves the leaf nobody asked forscript/taproot.pyassert_valid_control_blocklenof whatever it was handed:"é" * 33is 33 characters and 66 octets of UTF-8, and passed as a control block sizebech32.pyencode_ALPHABETfrom the end and wrote a different address, correctly checksummed. Above 31 it wasIndexError, aLookupErrorbip32/der_path.pyindexes_from_der_path[-5]for[-5]. The text spelling checks0 <= index < 0x80000000per step; the int and the iterable checked nothingbip32/bip32.pypub_key_derivation_tweaks[]for 33 bytes that are no public key:if indexes:guarded the only code that ever looked at itdescriptors/descriptors.pyminiscript_solvervin_isolved the input at the other endpsbt/psbt.pyPsbt.weight_estimatenumber_theory.pyxgcd,mod_inv,legendre_symbol,mod_sqrt,tonellimod_inv(3.0, 7)is5.0, out of a signature that saysint; a modulus of zero wasZeroDivisionErrorutils.pyint_from_json_number1.5truncated to 1, silentlymnemonic/entropy.pybin_str_entropy_from_wordlist_indexesbasearithmetic has no out of range, 2048 in a 2048-word list being a carryfetch/fetcher.pytx_for_networkcheck_validity=FalseDecisions worth a reviewer's eye
number_theorychecks in each of the five, rather than five privatetwins. A pair of
isinstancecalls measured 0.065 µs against the9.9 µs of a 256-bit
mod_inv— 0.65%, and the alternative is fivemore names. The comment says so without the numbers, which go stale.
bech32.encodewalks the digits a second time. The range checkalone is ~10%, with
is_integer~40%, on a 3.5 µs function calledonce per address — under the key derivation that produced the digits
by orders of magnitude. Correctness of an address won.
assert_valid_control_blockwidens toOctets, matchingcheck_output_pubkeyright above it, which coerces the sameargument. A
strthat is no hex string now reaches a bareValueErrorfrombytes.fromhexrather than passing; tightening thatclass is issue 88 public functions still let a malformed input through: the census #684 left open #744's last slice, and the test asserts
ValueErrorsoit stays true across it.
tx_for_networkresolves the name instead of comparing it, so" MainNet "is the short-circuit it always should have been.Two tests asserted the defect
Both in bip32, and both now assert the fix:
derive(xprv, 2**32)wasOverflowError: int too big to convert;it is a
BTClibValueErrornaming the index.BIP32KeyOrigin("deadbeef", [0xFFFFFFFF + 1])reachedassert_valid's own check; the path reader refuses it first. Thatcheck still earns its keep —
der_pathis annotatedSequence[int]and holds a list, so the frozen dataclass stops a rebinding and not an
append — and a test reaches it that way.
The bool half of the six new integer guards goes in
tests/integer_policy_test.py, beside the twenty-five parametersalready there rather than in six files.
Gates
uv run pytest— 26479 passed, coverage 100.00%uv run pre-commit run --all-files— exit 0sphinx-build -W --keep-going— exit 0Part of #744, which stays
open: the sig_hash widths, the
block/guards andbytes_from_octetsare three more slices.
Summary by Sourcery
Tighten validation across multiple modules so malformed inputs are consistently rejected instead of producing seemingly valid hashes, addresses, entropy, weights, BIP32 paths, arithmetic results, taproot scripts, PSBT estimates, and network-labelled transactions.
Bug Fixes:
Enhancements:
Documentation: