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.
- btclib_ecc/__init__.py +98 -0
- btclib_ecc/_libsecp256k1.py +142 -0
- btclib_ecc/_utils.py +418 -0
- btclib_ecc/alias.py +200 -0
- btclib_ecc/curves/__init__.py +157 -0
- btclib_ecc/curves/_data/ec_Brainpool.json +79 -0
- btclib_ecc/curves/_data/ec_NIST.json +57 -0
- btclib_ecc/curves/_data/ec_SEC2v1_insecure.json +79 -0
- btclib_ecc/curves/_data/ec_SEC2v2.json +90 -0
- btclib_ecc/curves/curve.py +1496 -0
- btclib_ecc/curves/curve_group.py +1810 -0
- btclib_ecc/curves/curve_group_2.py +631 -0
- btclib_ecc/curves/curve_group_f.py +63 -0
- btclib_ecc/curves/sec_point.py +388 -0
- btclib_ecc/ecc/__init__.py +98 -0
- btclib_ecc/ecc/bip340_nonce.py +116 -0
- btclib_ecc/ecc/borromean.py +829 -0
- btclib_ecc/ecc/commit_nonce.py +191 -0
- btclib_ecc/ecc/dh.py +103 -0
- btclib_ecc/ecc/dleq.py +296 -0
- btclib_ecc/ecc/dsa.py +2558 -0
- btclib_ecc/ecc/ecies.py +418 -0
- btclib_ecc/ecc/ellswift.py +312 -0
- btclib_ecc/ecc/frost.py +1064 -0
- btclib_ecc/ecc/musig2.py +1204 -0
- btclib_ecc/ecc/pedersen.py +548 -0
- btclib_ecc/ecc/rangeproof.py +1619 -0
- btclib_ecc/ecc/rfc6979_nonce.py +179 -0
- btclib_ecc/ecc/ssa.py +1602 -0
- btclib_ecc/exceptions.py +139 -0
- btclib_ecc/hashes.py +99 -0
- btclib_ecc/kdf.py +193 -0
- btclib_ecc/number_theory.py +433 -0
- btclib_ecc/py.typed +0 -0
- btclib_ecc-2026.9.26.dist-info/METADATA +188 -0
- btclib_ecc-2026.9.26.dist-info/RECORD +39 -0
- btclib_ecc-2026.9.26.dist-info/WHEEL +4 -0
- btclib_ecc-2026.9.26.dist-info/licenses/AUTHORS.md +9 -0
- 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()
|