btclib-ecc 2026.9.26__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. btclib_ecc/__init__.py +98 -0
  2. btclib_ecc/_libsecp256k1.py +142 -0
  3. btclib_ecc/_utils.py +418 -0
  4. btclib_ecc/alias.py +200 -0
  5. btclib_ecc/curves/__init__.py +157 -0
  6. btclib_ecc/curves/_data/ec_Brainpool.json +79 -0
  7. btclib_ecc/curves/_data/ec_NIST.json +57 -0
  8. btclib_ecc/curves/_data/ec_SEC2v1_insecure.json +79 -0
  9. btclib_ecc/curves/_data/ec_SEC2v2.json +90 -0
  10. btclib_ecc/curves/curve.py +1496 -0
  11. btclib_ecc/curves/curve_group.py +1810 -0
  12. btclib_ecc/curves/curve_group_2.py +631 -0
  13. btclib_ecc/curves/curve_group_f.py +63 -0
  14. btclib_ecc/curves/sec_point.py +388 -0
  15. btclib_ecc/ecc/__init__.py +98 -0
  16. btclib_ecc/ecc/bip340_nonce.py +116 -0
  17. btclib_ecc/ecc/borromean.py +829 -0
  18. btclib_ecc/ecc/commit_nonce.py +191 -0
  19. btclib_ecc/ecc/dh.py +103 -0
  20. btclib_ecc/ecc/dleq.py +296 -0
  21. btclib_ecc/ecc/dsa.py +2558 -0
  22. btclib_ecc/ecc/ecies.py +418 -0
  23. btclib_ecc/ecc/ellswift.py +312 -0
  24. btclib_ecc/ecc/frost.py +1064 -0
  25. btclib_ecc/ecc/musig2.py +1204 -0
  26. btclib_ecc/ecc/pedersen.py +548 -0
  27. btclib_ecc/ecc/rangeproof.py +1619 -0
  28. btclib_ecc/ecc/rfc6979_nonce.py +179 -0
  29. btclib_ecc/ecc/ssa.py +1602 -0
  30. btclib_ecc/exceptions.py +139 -0
  31. btclib_ecc/hashes.py +99 -0
  32. btclib_ecc/kdf.py +193 -0
  33. btclib_ecc/number_theory.py +433 -0
  34. btclib_ecc/py.typed +0 -0
  35. btclib_ecc-2026.9.26.dist-info/METADATA +188 -0
  36. btclib_ecc-2026.9.26.dist-info/RECORD +39 -0
  37. btclib_ecc-2026.9.26.dist-info/WHEEL +4 -0
  38. btclib_ecc-2026.9.26.dist-info/licenses/AUTHORS.md +9 -0
  39. btclib_ecc-2026.9.26.dist-info/licenses/LICENSE +21 -0
btclib_ecc/__init__.py ADDED
@@ -0,0 +1,98 @@
1
+ # Copyright (c) The btclib developers
2
+ # Distributed under the MIT software license, see the accompanying
3
+ # LICENSE file or https://opensource.org/license/mit for the full text.
4
+
5
+ """The btclib_ecc package: what it publishes, and the version metadata.
6
+
7
+ Elliptic curve arithmetic and the signature, commitment and key-agreement
8
+ schemes built on it, over any curve in short Weierstrass form, with
9
+ libsecp256k1 accelerating secp256k1 where the optional `btclib-secp256k1`
10
+ bindings are installed.
11
+
12
+ `__all__` here is the root of the package's public tree: the packages and
13
+ top-level modules a caller reaches from this name. Each of those, and each
14
+ module below them, declares its own `__all__`, so a walk that starts here
15
+ has a declared surface at every node -- which is why the list is not
16
+ `pkgutil.iter_modules`: discovery would answer the file tree, and a module
17
+ added to the directory would publish itself rather than being published.
18
+
19
+ `name` is not in it, nor are the metadata dunders. `name` is the
20
+ distribution's name and not a member of the tree, `__version__` bound by
21
+ a star import would overwrite the importing module's own, and each is
22
+ still an attribute here: `btclib_ecc.__version__` is how a caller
23
+ reads the version and `btclib_ecc.name` how it reads the name.
24
+
25
+ Nothing is imported eagerly. A module is imported when it is first asked
26
+ for, through the `__getattr__` at the bottom of this file, so `import
27
+ btclib_ecc` is the metadata lookup below and nothing else, and the
28
+ import graph keeps its shape: importing every module here would put the
29
+ whole package in `sys.modules` before any single module of it could be
30
+ imported first, which is the situation tests/imports_test.py exists to
31
+ make impossible.
32
+
33
+ What that costs is worth stating: mypy reads a module-level `__getattr__`
34
+ as a promise that any attribute may exist, so `btclib_ecc.cruves` is
35
+ `Any` to it and a misspelling on this package is a runtime
36
+ `AttributeError` rather than a reported error. The spellings a caller
37
+ actually writes -- `from btclib_ecc import curves`, `import
38
+ btclib_ecc.curves`, `from btclib_ecc.curves import mult` --
39
+ resolve against the real modules and stay checked.
40
+ """
41
+
42
+ from importlib import import_module
43
+ from importlib.metadata import PackageNotFoundError, version
44
+ from types import ModuleType
45
+
46
+ name = "btclib-ecc"
47
+ try:
48
+ __version__ = version("btclib-ecc")
49
+ except PackageNotFoundError:
50
+ # a source tree with no metadata beside it: git clone and import, or
51
+ # read the docs, which builds without installing this package. Any
52
+ # number here would be a guess, and reading pyproject.toml back is not
53
+ # the way to stop guessing: the file is not in the wheel. Importing has
54
+ # to keep working, so the version says it does not know
55
+ __version__ = "unknown"
56
+
57
+ __all__ = [
58
+ "alias",
59
+ "curves",
60
+ "ecc",
61
+ "exceptions",
62
+ "hashes",
63
+ "kdf",
64
+ "number_theory",
65
+ ]
66
+
67
+
68
+ def __getattr__(published: str) -> ModuleType:
69
+ """Import a published module the first time it is asked for.
70
+
71
+ PEP 562: this runs only for a name the package does not already have,
72
+ so it answers `btclib_ecc.curves` once and the import machinery's
73
+ own attribute answers it from then on -- and it never runs for
74
+ `import btclib_ecc.curves` or `from btclib_ecc import curves`,
75
+ which import the submodule themselves. What it makes work is
76
+ `getattr(btclib_ecc, "curves")` on a fresh interpreter, which is
77
+ how a walker reading `__all__` descends, and `from btclib_ecc
78
+ import *`, which asks for each name in the list.
79
+
80
+ Anything not in `__all__` raises `AttributeError`, private modules
81
+ included: `import btclib_ecc._utils` still reaches one. The
82
+ message is the interpreter's own wording, so a typo reads as it does
83
+ anywhere else.
84
+ """
85
+ if published in __all__:
86
+ return import_module(f"{__name__}.{published}")
87
+ raise AttributeError(f"module {__name__!r} has no attribute {published!r}")
88
+
89
+
90
+ def __dir__() -> list[str]:
91
+ """Answer with the published tree beside what the package already has.
92
+
93
+ `dir(btclib_ecc)` consults this rather than the namespace, so
94
+ without it a module not yet imported is missing from the completion a
95
+ caller gets at an interactive prompt -- the same names `__getattr__`
96
+ above will answer for.
97
+ """
98
+ return sorted({*__all__, *globals()})
@@ -0,0 +1,142 @@
1
+ # Copyright (c) The btclib developers
2
+ # Distributed under the MIT software license, see the accompanying
3
+ # LICENSE file or https://opensource.org/license/mit for the full text.
4
+
5
+ """Everything this package asks of libsecp256k1, imported in one place.
6
+
7
+ The modules that delegate to the bindings import none of them directly:
8
+ they import from here, so the question "are the bindings installed" is
9
+ asked once, at one import, and answered by `INSTALLED` rather than by a
10
+ `try` block in each of them, which could drift apart. `ENABLED` is that
11
+ answer with `BTCLIB_ECC_NO_LIBSECP256K1` applied -- installed, and not
12
+ refused -- and `curves.curve` reads it into `_libsecp256k1_available`,
13
+ which is the seam every dispatch in the package consults, so an absent
14
+ binding is a dispatch that declines rather than an `ImportError` raised
15
+ before any dispatch is reached.
16
+
17
+ Two names because the seam moves and this import does not. `ENABLED` is
18
+ only the state `curves.curve` starts in, and `set_libsecp256k1_serving`
19
+ may have moved it since; `INSTALLED` is settled here and stays, so it is
20
+ what that function reads to refuse `serving=True` where there is nothing
21
+ to serve, and what the suite skips its `bindings` marker on. The
22
+ difference is this module's and the seam's, not a caller's:
23
+ `curves.is_libsecp256k1_serving` publishes one answer -- whether the next
24
+ call goes to libsecp256k1 or to the Python arithmetic -- which is the
25
+ only difference a caller can act on.
26
+
27
+ A module that imports nothing else of this package, and is therefore
28
+ below every module that reads it. What it does import is the whole of
29
+ the surface this package uses, so this file is also the answer to "what
30
+ does this package need these bindings for", which no reader has to
31
+ assemble from the delegating modules' own imports.
32
+
33
+ The names are re-exported under the bindings' own spelling and the caller
34
+ aliases them as it always did: a caller holding `keys` and reaching
35
+ through it, and a caller naming `pubkey_sum` directly, both keep the call
36
+ they had. Neither shape costs an attribute lookup it did not cost before,
37
+ which matters where `curves.curve` counts tenths of a microsecond.
38
+
39
+ With the bindings absent every name here is None. Nothing may call one:
40
+ `_libsecp256k1_serves` is False in that configuration, and it is the
41
+ predicate in front of every delegation -- which is a rule about the
42
+ package rather than about this file, and is what `tests/no_bindings_test.py`
43
+ checks by importing this package with the bindings out of reach.
44
+ """
45
+
46
+ from __future__ import annotations
47
+
48
+ import os
49
+
50
+ # the environment variable that refuses the bindings without uninstalling
51
+ # them, read once and here, when this module is first imported. `import
52
+ # btclib_ecc` alone does not import it, the root importing lazily; the
53
+ # first import of `curves`, of `ecc` or of a module under them does, so a
54
+ # caller settling the variable after that has settled it after this module
55
+ # answered. The package's name as its prefix, and "set to ask for it": an
56
+ # empty value is not set, so `BTCLIB_ECC_NO_LIBSECP256K1=` leaves them
57
+ # serving
58
+ NO_LIBSECP256K1 = "BTCLIB_ECC_NO_LIBSECP256K1"
59
+
60
+ __all__ = [
61
+ "ENABLED",
62
+ "INSTALLED",
63
+ "NO_LIBSECP256K1",
64
+ "PubkeyTweakChain",
65
+ "dsa",
66
+ "ellswift",
67
+ "ffi",
68
+ "keys",
69
+ "musig",
70
+ "pubkey_from_prvkey",
71
+ "pubkey_sum",
72
+ "pubkey_tweak_add",
73
+ "pubkey_tweak_mul_sum",
74
+ "recovery",
75
+ "shared_point",
76
+ "ssa",
77
+ "xonly_pubkey_verify",
78
+ "xonly_to_pubkey",
79
+ ]
80
+
81
+ try:
82
+ # `ffi` is the one object here that is not a wrapped call: issue
83
+ # btclib-org/btclib#1009 is why it is needed at all -- `ecc.dsa.Signer`
84
+ # builds its own `unsigned char[32]` with it, to hold a private key in
85
+ # memory this package can overwrite, the way `ssa.Signer` already can
86
+ # through the keypair `ssa` itself builds. Nothing else here needs it,
87
+ # `dsa.sign` and every other wrapped call taking that buffer as the `prvkey`
88
+ # a caller may already hold (btclib-org/btclib-secp256k1#253)
89
+ from btclib_secp256k1 import (
90
+ dsa,
91
+ ellswift,
92
+ ffi,
93
+ keys,
94
+ musig,
95
+ recovery,
96
+ ssa,
97
+ )
98
+ from btclib_secp256k1.ecdh import shared_point
99
+ from btclib_secp256k1.keys import (
100
+ PubkeyTweakChain,
101
+ pubkey_from_prvkey,
102
+ pubkey_sum,
103
+ pubkey_tweak_add,
104
+ pubkey_tweak_mul_sum,
105
+ )
106
+ from btclib_secp256k1.xonly import pubkey_verify as xonly_pubkey_verify
107
+ from btclib_secp256k1.xonly import to_pubkey as xonly_to_pubkey
108
+
109
+ INSTALLED = True
110
+ # issue btclib-org/btclib#1002 measured this branch rather than assuming it
111
+ # stays transitional: `test.yml`'s `no-bindings` job executes it every run, and
112
+ # `coverage-union` combines that run's data with the `coverage` job's. The
113
+ # combined report is 100% with this pragma removed -- so the branch is reached
114
+ # and is not dead code -- and the pragma still belongs here regardless, because
115
+ # `coverage-union` is a second gate beside the `coverage` job's, not instead of
116
+ # it: that job's own report, `pytest --cov` on this configuration alone, has
117
+ # bindings installed by construction, an `ImportError` only reachable by
118
+ # actually removing them, and a subprocess that does
119
+ # (`tests/no_bindings_test.py`) whose coverage that job does not collect. So
120
+ # this branch is a structural miss in that report regardless of the union, and
121
+ # removing the pragma would fail the one gate this issue chose to leave
122
+ # unchanged
123
+ except ImportError: # pragma: no cover -- only the no-bindings job reaches this
124
+ # None and not a callable that raises: what would raise is never
125
+ # called, so the object would be a second thing to keep true. The
126
+ # ignore is on the assignment and not on the module: every other
127
+ # name here keeps the type the try branch gave it
128
+ dsa = ellswift = ffi = keys = musig = recovery = ssa = None # type: ignore[assignment]
129
+ shared_point = None # type: ignore[assignment]
130
+ # a class rather than a function, so mypy calls it an assignment to
131
+ # a type and wants the second code as well
132
+ PubkeyTweakChain = None # type: ignore[misc, assignment]
133
+ pubkey_from_prvkey = pubkey_sum = None # type: ignore[assignment]
134
+ pubkey_tweak_add = pubkey_tweak_mul_sum = None # type: ignore[assignment]
135
+ xonly_pubkey_verify = xonly_to_pubkey = None # type: ignore[assignment]
136
+
137
+ INSTALLED = False
138
+
139
+ # installed and not refused, which is one state and not two: a caller
140
+ # asking whether the bindings serve has no use for the difference, and
141
+ # `curves.curve` starts its seam from this
142
+ ENABLED = INSTALLED and not os.environ.get(NO_LIBSECP256K1)
btclib_ecc/_utils.py ADDED
@@ -0,0 +1,418 @@
1
+ # Copyright (c) The btclib developers
2
+ # Distributed under the MIT software license, see the accompanying
3
+ # LICENSE file or https://opensource.org/license/mit for the full text.
4
+
5
+ """The coercions every public function runs its inputs through.
6
+
7
+ Private: these are the conversions an `Octets`, a `String`, an `Integer`
8
+ and a `BinaryData` parameter owe their caller, and the refusals of a
9
+ short read and of trailing bytes, raised through this package's own
10
+ exceptions.
11
+
12
+ `read_exactly` and `assert_no_trailing` are the two halves of what every
13
+ `parse` owes its caller:
14
+
15
+ - a field is as long as its encoding says it is, so a short read is an
16
+ error and not a value
17
+ - octets are one whole object, so bytes after it are refused; a caller's
18
+ stream is not, so parsing consumes the object and leaves the stream on
19
+ the byte after it
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ from collections.abc import Iterable
25
+ from io import BytesIO
26
+ from typing import Any, BinaryIO
27
+
28
+ from typing_extensions import TypeIs
29
+
30
+ from btclib_ecc.alias import BinaryData, Integer, Octets, String
31
+ from btclib_ecc.exceptions import BTClibEccTypeError, BTClibEccValueError
32
+
33
+ __all__ = [
34
+ "NoneOneOrMoreInt",
35
+ "assert_no_trailing",
36
+ "assert_type",
37
+ "bytes_from_octets",
38
+ "bytesio_from_binarydata",
39
+ "hex_string",
40
+ "int_from_bits",
41
+ "int_from_integer",
42
+ "is_integer",
43
+ "is_octets",
44
+ "read_exactly",
45
+ "str_from_string",
46
+ ]
47
+
48
+ NoneOneOrMoreInt = int | Iterable[int] | None
49
+
50
+
51
+ def _assert_byte_shaped(octets: bytes | bytearray | memoryview) -> None:
52
+ """Refuse a memoryview whose layout or format is not plain bytes.
53
+
54
+ `bytes` and `bytearray` are always C-contiguous and always format
55
+ "B" (unsigned bytes); a `memoryview` need not be either, and the
56
+ copy `bytes_from_octets` makes takes both without a word. A strided
57
+ slice such as `mv[::2]` is not C-contiguous, and `bytes()` of one
58
+ gathers its strides into a run that no buffer holds.
59
+
60
+ Contiguity is not the only property the octets a caller counts rest on: a
61
+ `memoryview` cast to a signed `"b"`, or built over an array of a wider
62
+ format such as `"I"`, is C-contiguous and still not the octets it looks like
63
+ -- `len()` counts elements where `bytes()` yields `itemsize` of them each,
64
+ so the caller's count of its own octets and the library's disagree (issue
65
+ btclib-org/btclib#1430). Format `"B"` is the one every buffer this function
66
+ accepts shares, `bytes`, `bytearray` and an unformatted `memoryview`
67
+ included, so it is the property asked for rather than `itemsize == 1`, which
68
+ a signed `"b"` view or a `"c"` one (bytes objects, not ints) would still
69
+ pass.
70
+ """
71
+ if not isinstance(octets, memoryview):
72
+ return
73
+ if not octets.c_contiguous:
74
+ err_msg = "invalid octets: non-contiguous memoryview"
75
+ raise BTClibEccValueError(err_msg)
76
+ if octets.format != "B":
77
+ err_msg = f"invalid octets: memoryview format {octets.format!r} instead of 'B'"
78
+ raise BTClibEccValueError(err_msg)
79
+
80
+
81
+ def bytes_from_octets(octets: Octets, out_size: NoneOneOrMoreInt = None) -> bytes:
82
+ """Return bytes from a hex-string, stripping leading/trailing spaces.
83
+
84
+ A `bytearray` or a `memoryview` is copied, which is what makes the
85
+ `bytes` this promises true: handed back as it came, either is still
86
+ the caller's own object, so a write to it afterwards reaches into
87
+ whatever kept the return value -- a key, and every encoding of it
88
+ computed afterwards. The copy is
89
+ also what reads as octets everywhere the result goes: a `memoryview`
90
+ has no `+` to concatenate with, and a `bytearray` keys no dict.
91
+
92
+ Optionally, it also ensures required output size: one size, or any
93
+ iterable of them, and a bool is neither -- `out_size=True` would
94
+ accept a single octet and say it had checked a size.
95
+
96
+ A non-contiguous `memoryview` -- what a strided slice such as
97
+ `mv[::2]` gives -- is refused rather than copied, and so is one whose
98
+ format is not unsigned bytes: `_assert_byte_shaped` says why neither
99
+ is the octets it looks like. The refusal is raised through the
100
+ exception contract at the one place every `Octets` parameter passes,
101
+ rather than left to whichever consumer trips over it first.
102
+ """
103
+ if isinstance(octets, str): # hex string
104
+ # `bytes.fromhex` raises a bare ValueError -- the same class the
105
+ # contract promises, so what was lost is only that it came from
106
+ # here. This is the one coercion every `Octets` parameter of the
107
+ # library runs through, so it is the one place worth saying it in.
108
+ # The message is fromhex's own, which names a position and never
109
+ # the string: an Octets parameter is candidate key material as
110
+ # often as not (issue btclib-org/btclib#137)
111
+ try:
112
+ octets = bytes.fromhex(octets)
113
+ except ValueError as e:
114
+ raise BTClibEccValueError(f"invalid hex string: {e}") from e
115
+ elif isinstance(octets, (bytes, bytearray, memoryview)):
116
+ _assert_byte_shaped(octets)
117
+ # the copy that makes the annotation true (issue
118
+ # btclib-org/btclib#1255). `bytes(b)` is `b` itself where `b` is already
119
+ # `bytes`, which is what nearly every call passes, so the arm every
120
+ # `Octets` parameter of the library runs through allocates nothing
121
+ octets = bytes(octets)
122
+ else:
123
+ # what is neither went through untouched and reached whatever the
124
+ # caller went on to do with it: `len` of a tuple of 33 ints is 33,
125
+ # so a size check alone would accept one as a compressed key
126
+ err_msg = f"invalid octets type: {type(octets).__name__}" # type: ignore[unreachable]
127
+ raise BTClibEccTypeError(err_msg)
128
+
129
+ if out_size is None:
130
+ return octets
131
+
132
+ # one size or an iterable of them, and nothing else: `tuple()` on
133
+ # whatever is left would refuse a float with a bare TypeError about
134
+ # iteration -- a complaint about the wrong thing, and from underneath
135
+ # the library rather than through its exception contract
136
+ if isinstance(out_size, int):
137
+ sizes: tuple[int, ...] = (out_size,)
138
+ elif isinstance(out_size, Iterable):
139
+ sizes = tuple(out_size)
140
+ else:
141
+ err_msg = f"invalid output size type: {type(out_size).__name__}" # type: ignore[unreachable]
142
+ raise BTClibEccTypeError(err_msg)
143
+
144
+ for size in sizes:
145
+ if not is_integer(size):
146
+ err_msg = f"invalid output size type: {type(size).__name__}"
147
+ raise BTClibEccTypeError(err_msg)
148
+
149
+ size = len(octets)
150
+ if size in sizes:
151
+ return octets
152
+
153
+ err_msg = f"invalid size: {size} bytes instead of {out_size}"
154
+ raise BTClibEccValueError(err_msg)
155
+
156
+
157
+ def str_from_string(s: String, what: str) -> str:
158
+ """Return the text of a String, whether it came as text or as ascii bytes.
159
+
160
+ What `bytes_from_octets` is to `Octets`, for text: the base64 armor
161
+ of an envelope is ascii, so a byte outside it is an invalid character
162
+ like any other and gets the same answer -- a UnicodeDecodeError let
163
+ out would fly past every caller written to catch a
164
+ BTClibEccValueError.
165
+
166
+ `what` names the string in both messages, the caller knowing what it
167
+ was reading and this not, exactly as `read_exactly` names a field.
168
+
169
+ Nothing is stripped and nothing is lowered: which of those is right
170
+ is the caller's to know.
171
+ """
172
+ if isinstance(s, str):
173
+ return s
174
+
175
+ if not isinstance(s, (bytes, bytearray, memoryview)):
176
+ # what is neither went through untouched, to fail on `len` or on
177
+ # a method of str that the value does not have -- a complaint
178
+ # about a builtin rather than about the argument
179
+ err_msg = f"invalid {what} type: {type(s).__name__}" # type: ignore[unreachable]
180
+ raise BTClibEccTypeError(err_msg)
181
+
182
+ try:
183
+ return bytes(s).decode("ascii")
184
+ except UnicodeDecodeError as e:
185
+ raise BTClibEccValueError(f"non-ascii character in {what}: {e}") from e
186
+
187
+
188
+ def bytesio_from_binarydata(stream: BinaryData) -> BytesIO:
189
+ """Return a BytesIO stream object from a BytesIO or from Octets.
190
+
191
+ A `BytesIO` is the caller's own and is handed back as it came, the
192
+ position it is left at being how a transaction is read out of a
193
+ block. Anything else is octets, and is wrapped in one.
194
+
195
+ A `BytesIO` and not any binary stream: `deserialize_map` asks the
196
+ result for `getbuffer()`, which a file object does not have, so
197
+ accepting one here would only move the failure. `read_exactly` is
198
+ the one that takes a `BinaryIO`, and its docstring says why.
199
+ """
200
+ if isinstance(stream, BytesIO):
201
+ return stream
202
+
203
+ # the refusal of what is neither a stream nor octets is
204
+ # bytes_from_octets's to give: without it, what is neither would go
205
+ # through untouched and be returned as it came, so `parse` would
206
+ # answer a None with a None and the caller would fail on `.read`
207
+ # rather than here
208
+ return BytesIO(bytes_from_octets(stream))
209
+
210
+
211
+ def read_exactly(stream: BinaryIO, size: int, what: str) -> bytes:
212
+ """Return size octets from the stream, or raise: a short read is truncation.
213
+
214
+ `BytesIO.read` answers with whatever is left when the buffer holds
215
+ less than was asked for, and `int.from_bytes` takes the short answer
216
+ without a word. The size is what makes the field boundary, so it is
217
+ checked whatever `check_validity` says: see this module's docstring
218
+ for why that is not the same question.
219
+
220
+ `what` names the field in the error message, the caller knowing which
221
+ one it was reading and the stream not.
222
+
223
+ `BinaryIO` and not the `BytesIO` of `alias.BinaryData`: `.read` is
224
+ the whole of what a short read is about, so a file object is as much
225
+ an answer here as a buffer.
226
+ """
227
+ data = stream.read(size)
228
+ if len(data) != size:
229
+ err_msg = f"not enough data for the {what}: "
230
+ err_msg += f"{len(data)} bytes instead of {size}"
231
+ raise BTClibEccValueError(err_msg)
232
+ return data
233
+
234
+
235
+ def assert_no_trailing(data: BinaryData, stream: BytesIO, what: str) -> None:
236
+ """Refuse bytes left over after a complete octet encoding.
237
+
238
+ Octets are one whole object, so what follows the object in them is
239
+ malleability: two buffers deserializing to the one object that
240
+ serializes back to only the shorter of them. A caller's stream is the
241
+ other case, and nothing is checked there -- what follows in it is the
242
+ caller's, a transaction inside a block being read from the very stream
243
+ the block is read from -- so `parse` leaves the stream on the byte
244
+ after the object.
245
+
246
+ Bitcoin Core splits the two the same way, between `Unserialize` and
247
+ `DecodeRawPSBT`'s "extra data after PSBT".
248
+ """
249
+ if isinstance(data, BytesIO):
250
+ return
251
+
252
+ trailing = stream.read()
253
+ if trailing:
254
+ raise BTClibEccValueError(f"{len(trailing)} bytes after the {what}")
255
+
256
+
257
+ def is_integer(value: Any) -> bool:
258
+ """Return whether the value is an integer, a bool not being one.
259
+
260
+ `isinstance(x, int)` is True for `True` and `False`, `bool` being a
261
+ subclass of `int` -- so every field of this library whose contract is
262
+ an integer quantity accepted a boolean as the number one or zero, and
263
+ `int(True) == True` slips through a conversion-and-equality check as
264
+ well. What makes that worth a refusal rather than a shrug is the json
265
+ boundary: `true` decodes to `True`, so a schema mistake became one
266
+ satoshi, one virtual byte, one index or a one-sat/kvB fee rate instead
267
+ of failing next to the input that caused it.
268
+
269
+ A boolean is not another spelling of a number, which is the difference
270
+ from the strings and bytes much of this library accepts: "1" is a
271
+ number written down, `True` is a different type that Python's
272
+ inheritance makes indistinguishable from one.
273
+
274
+ `isinstance` and not `type(value) is int`, so an `IntEnum` -- what issue
275
+ btclib-org/btclib#273 asks about for the sighash types -- and any other
276
+ deliberate integer subclass stay integers. `bool` is the one subclass
277
+ excluded, and by name.
278
+ """
279
+ return isinstance(value, int) and not isinstance(value, bool)
280
+
281
+
282
+ def is_octets(value: Any) -> TypeIs[Octets]:
283
+ """Return whether the value is one Octets, rather than a sequence of them.
284
+
285
+ An `Octets` -- `str`, `bytes`, `bytearray` or `memoryview` -- is
286
+ itself iterable, so a function that takes a sequence of them and
287
+ guards against being handed one instead cannot ask `isinstance(value,
288
+ Sequence)`: every `Octets` answers that too. The guard asks this
289
+ question instead, once, so a spelling `Octets` gains later is refused
290
+ at every caller of this rather than at whichever remembered to list
291
+ it (issue btclib-org/btclib#1261).
292
+
293
+ `TypeIs` rather than `bool`: a caller dispatching on this narrows on
294
+ both branches, `str | bytes | bytearray | memoryview` where it is
295
+ true and whatever is left of the wider type where it is false, which
296
+ is what lets a site written as a hand-listed `isinstance` tuple --
297
+ invisible to a census keyed on that tuple's own element order -- call
298
+ this instead without losing the narrowing mypy strict mode otherwise
299
+ needs the tuple for (issue btclib-org/btclib#1433).
300
+ """
301
+ return isinstance(value, Octets)
302
+
303
+
304
+ def assert_type(value: Any, expected: Any, what: str) -> None:
305
+ """Refuse a value of a type the signature does not declare.
306
+
307
+ `expected` is what `isinstance` takes: one type, or a tuple of them.
308
+ `bytes_from_octets` and `str_from_string` are the two coercions this
309
+ library has, and each refuses what it cannot convert; this is the
310
+ refusal for a position that takes neither -- a `bool` flag deciding
311
+ which of two serializations is written, the text of a URI or a
312
+ descriptor, the magic bytes an envelope is read against. Every one of
313
+ those was compared, walked or handed to a builtin unasked, and left
314
+ as a complaint about that builtin.
315
+
316
+ `value` is `Any` rather than the declared type, which is what makes
317
+ the check reachable: mypy proves the argument cannot be wrong, and
318
+ the caller who has not run mypy is who this is for.
319
+ """
320
+ if not isinstance(value, expected):
321
+ raise BTClibEccTypeError(f"invalid {what} type: {type(value).__name__}")
322
+
323
+
324
+ def int_from_bits(octets: Octets, nlen: int) -> int:
325
+ """Return the leftmost nlen bits.
326
+
327
+ Take as input a sequence of blen bits and calculate a
328
+ non-negative integer i that is less than 2^nlen according to
329
+ SEC 1 v.2 section 4.1.3 (5); ensuring 0 < i < n would take a
330
+ further reduction modulo n, which is the caller's.
331
+
332
+ int_from_bits is not the reverse of i.to_bytes, even
333
+ for input sequences of length nlen: i.to_bytes will add some
334
+ bits on the left, while int_from_bits will discard some bits on the
335
+ right. i.to_bytes is the reverse of int_from_bits only when
336
+ nlen is a multiple of 8 and bit sequences already have length nlen.
337
+ See:
338
+ - https://www.rfc-editor.org/rfc/rfc6979.html#section-2.3.5
339
+ """
340
+ octets = bytes_from_octets(octets)
341
+ i = int.from_bytes(octets, byteorder="big", signed=False)
342
+
343
+ blen = len(octets) * 8 # bits
344
+ n = (blen - nlen) if blen >= nlen else 0
345
+ return i >> n
346
+
347
+
348
+ def int_from_integer(i: Integer) -> int:
349
+ r"""Return an int from many possible integer representations.
350
+
351
+ A bool is not one of them, `is_integer` being where this library
352
+ says so: every `Integer` parameter runs through here, so the refusal
353
+ is stated once and inherited (issue btclib-org/btclib#1206).
354
+
355
+ Allowed integer representations are:
356
+
357
+ * 3735928559
358
+ * -3735928559
359
+ * "0xdeadbeef"
360
+ * "-0xdeadbeef"
361
+ * "deadbeef"
362
+ * b'\xde\xad\xbe\xef'
363
+
364
+ A str is always read as a hex-string, with or without the "0x" prefix:
365
+ int_from_integer("1234") is 4660, not one thousand two hundred and
366
+ thirty-four, and "9" raises ValueError for being a hex-string of odd
367
+ length rather than evaluating to nine. A decimal representation is
368
+ what int itself is for, so pass int("1234").
369
+
370
+ The binary representation is not allowed because there is no way to
371
+ discriminate it from a valid hex-string
372
+ (e.g. "0b11011110101011011011111011101111").
373
+ """
374
+ if isinstance(i, int):
375
+ # `is_integer` and not the `isinstance` above: a bool is an int to
376
+ # Python and is not a number to this library, which is the rule issue
377
+ # btclib-org/btclib#326 gave every integer field and issue
378
+ # btclib-org/btclib#1206 found the key path without. Here rather than at
379
+ # each caller, this being the one coercion every `Integer` parameter
380
+ # runs through
381
+ if not is_integer(i):
382
+ raise BTClibEccTypeError(f"non-integer: {i}")
383
+ return i
384
+
385
+ if isinstance(i, str):
386
+ i = i.strip().lower()
387
+ if i.startswith(("0x", "-0x")):
388
+ # the same bare ValueError bytes_from_octets names below, out
389
+ # of the one spelling that does not reach it
390
+ try:
391
+ return int(i, 16)
392
+ except ValueError as e:
393
+ raise BTClibEccValueError(f"invalid hex integer: {i!r}") from e
394
+
395
+ # the hex string, and the refusal of what is neither that nor bytes,
396
+ # both being bytes_from_octets's to give
397
+ return int.from_bytes(bytes_from_octets(i), "big", signed=False)
398
+
399
+
400
+ def hex_string(i: Integer) -> str:
401
+ """Return a hex-string from many positive integer representations.
402
+
403
+ Negative integers are not allowed.
404
+
405
+ The resulting hex-string has an even number of hex-digits and
406
+ includes a space every four bytes (i.e. every eight hex-digits).
407
+ """
408
+ int_ = int_from_integer(i)
409
+ if int_ < 0:
410
+ raise BTClibEccValueError(f"negative integer: {int_}")
411
+ a_str = f"{int_:x}"
412
+ if len(a_str) % 2 != 0:
413
+ a_str = f"0{a_str}"
414
+
415
+ indexes = list(reversed(range(len(a_str), 0, -8)))
416
+ lresult = [(a_str[max(0, i - 8) : i]) for i in indexes]
417
+ result = " ".join(lresult)
418
+ return result.upper()