ecc 2026.10.20__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.
ecc/__init__.py ADDED
@@ -0,0 +1,101 @@
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 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: `ecc.__version__` is how a caller
23
+ reads the version and `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
+ 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 `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 ecc import curves`, `import
38
+ ecc.curves`, `from 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
+ # the one literal the metadata lookups here and in _libsecp256k1 read: a
47
+ # rehearsal publishes to TestPyPI as `ecc-test`, and the dev-version
48
+ # action rewrites this line with pyproject.toml's `name`
49
+ name = "ecc"
50
+ try:
51
+ __version__ = version(name)
52
+ except PackageNotFoundError:
53
+ # a source tree with no metadata beside it: git clone and import, or
54
+ # read the docs, which builds without installing this package. Any
55
+ # number here would be a guess, and reading pyproject.toml back is not
56
+ # the way to stop guessing: the file is not in the wheel. Importing has
57
+ # to keep working, so the version says it does not know
58
+ __version__ = "unknown"
59
+
60
+ __all__ = [
61
+ "alias",
62
+ "curves",
63
+ "ecc",
64
+ "exceptions",
65
+ "hashes",
66
+ "kdf",
67
+ "number_theory",
68
+ ]
69
+
70
+
71
+ def __getattr__(published: str) -> ModuleType:
72
+ """Import a published module the first time it is asked for.
73
+
74
+ PEP 562: this runs only for a name the package does not already have,
75
+ so it answers `ecc.curves` once and the import machinery's
76
+ own attribute answers it from then on -- and it never runs for
77
+ `import ecc.curves` or `from ecc import curves`,
78
+ which import the submodule themselves. What it makes work is
79
+ `getattr(ecc, "curves")` on a fresh interpreter, which is
80
+ how a walker reading `__all__` descends, and `from ecc
81
+ import *`, which asks for each name in the list.
82
+
83
+ Anything not in `__all__` raises `AttributeError`, private modules
84
+ included: `import ecc._utils` still reaches one. The
85
+ message is the interpreter's own wording, so a typo reads as it does
86
+ anywhere else.
87
+ """
88
+ if published in __all__:
89
+ return import_module(f"{__name__}.{published}")
90
+ raise AttributeError(f"module {__name__!r} has no attribute {published!r}")
91
+
92
+
93
+ def __dir__() -> list[str]:
94
+ """Answer with the published tree beside what the package already has.
95
+
96
+ `dir(ecc)` consults this rather than the namespace, so
97
+ without it a module not yet imported is missing from the completion a
98
+ caller gets at an interactive prompt -- the same names `__getattr__`
99
+ above will answer for.
100
+ """
101
+ return sorted({*__all__, *globals()})
ecc/_libsecp256k1.py ADDED
@@ -0,0 +1,336 @@
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 `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 of this package but `exceptions` and the
28
+ root's `name`, and is therefore below every module that reads it. What it
29
+ imports from the bindings is the whole of the surface this package uses, so
30
+ this file is also the answer to "what does this package need these bindings
31
+ for", which no reader has to assemble from the delegating modules' own
32
+ imports.
33
+
34
+ The names are re-exported under the bindings' own spelling and the caller
35
+ aliases them as it always did: a caller holding `keys` and reaching
36
+ through it, and a caller naming `pubkey_sum` directly, both keep the call
37
+ they had. Neither shape costs an attribute lookup it did not cost before,
38
+ which matters where `curves.curve` counts tenths of a microsecond.
39
+
40
+ With the bindings absent every name here is None. Nothing may call one:
41
+ `_libsecp256k1_serves` is False in that configuration, and it is the
42
+ predicate in front of every delegation -- which is a rule about the
43
+ package rather than about this file, and is what `tests/no_bindings_test.py`
44
+ checks by importing this package with the bindings out of reach.
45
+
46
+ Absent and installed-but-too-old are different failures, and only the
47
+ first one is this fallback (btclib-org/ecc#25). `btclib_secp256k1`
48
+ itself failing to import is `ModuleNotFoundError` with its `name` naming
49
+ the top-level package -- nothing else is. A name this module asks for that
50
+ an installed, too-old package does not have raises a plain `ImportError`
51
+ instead (the package is found; the attribute inside it is not), or a
52
+ `ModuleNotFoundError` naming a submodule rather than the top-level package
53
+ (an old package missing a whole submodule this floor needs). Reading
54
+ either of those as "absent" is the defect: it is what let
55
+ `btclib-secp256k1` 0.8.0.6, which has no `btclib_secp256k1.ecdh.shared_point`,
56
+ answer `INSTALLED = False` instead of raising. So those two raise, which is
57
+ a loud failure at import time rather than a silent, slower fallback nobody
58
+ is told about.
59
+
60
+ What they raise is an `ImportError` that says what to do about it, chained
61
+ from the one the import system raised (btclib-org/ecc#56): the
62
+ installed `btclib-secp256k1` version, the floor this package declares for it,
63
+ and the two ways to a release that meets it. The floor is read back from this
64
+ distribution's own metadata, where `pyproject.toml`'s `secp256k1` extra put
65
+ it, so no second copy of it is kept here to drift; a source tree with no
66
+ metadata beside it is told to look in the extra. Only an `ImportError` naming
67
+ `btclib_secp256k1` or one of its submodules is taken for a bindings mismatch:
68
+ a dependency of the bindings that is missing, or a shared library that will
69
+ not load, propagates as it was raised.
70
+ """
71
+
72
+ from __future__ import annotations
73
+
74
+ import os
75
+ import re
76
+ from hashlib import sha256
77
+ from importlib import metadata
78
+
79
+ from ecc import name as _distribution
80
+ from ecc.exceptions import EccRuntimeError
81
+
82
+ # the environment variable that refuses the bindings without uninstalling
83
+ # them, read once and here, when this module is first imported. `import
84
+ # ecc` alone does not import it, the root importing lazily; the first
85
+ # import of `ecc.curves`, of `ecc.ecc` or of a module under them does, so a
86
+ # caller settling the variable after that has settled it after this module
87
+ # answered. The package's name as its prefix, and "set to ask for it": an
88
+ # empty value is not set, so `ECC_NO_LIBSECP256K1=` leaves them
89
+ # serving
90
+ NO_LIBSECP256K1 = "ECC_NO_LIBSECP256K1"
91
+
92
+ __all__ = [
93
+ "ENABLED",
94
+ "INSTALLED",
95
+ "NO_LIBSECP256K1",
96
+ "PubkeyTweakChain",
97
+ "dsa",
98
+ "ellswift",
99
+ "ffi",
100
+ "keys",
101
+ "musig",
102
+ "pubkey_from_prvkey",
103
+ "pubkey_sum",
104
+ "pubkey_tweak_add",
105
+ "pubkey_tweak_mul_sum",
106
+ "recovery",
107
+ "shared_point",
108
+ "ssa",
109
+ "xonly_prvkey_tweak_add",
110
+ "xonly_pubkey_verify",
111
+ "xonly_to_pubkey",
112
+ "xonly_tweak_add",
113
+ ]
114
+
115
+ _BINDINGS_MODULE = "btclib_secp256k1"
116
+ _BINDINGS_DISTRIBUTION = "btclib-secp256k1"
117
+
118
+
119
+ def _floor() -> str | None:
120
+ """Return the specifier the `secp256k1` extra puts on the bindings.
121
+
122
+ Read from this distribution's own metadata, which is where
123
+ `pyproject.toml` declares it, and `None` where there is no metadata to
124
+ read: a source tree that was never installed, or an installed one whose
125
+ `Requires-Dist` no longer lists the extra.
126
+ """
127
+ try:
128
+ requirements = metadata.requires(_distribution)
129
+ except metadata.PackageNotFoundError:
130
+ return None
131
+ for requirement in requirements or ():
132
+ head, _, marker = requirement.partition(";")
133
+ if not re.search(r"""extra\s*==\s*["']secp256k1["']""", marker):
134
+ continue
135
+ name = re.match(r"[A-Za-z0-9._-]+", head.strip())
136
+ if name is None:
137
+ continue
138
+ if re.sub(r"[-_.]+", "-", name.group()).lower() != _BINDINGS_DISTRIBUTION:
139
+ continue
140
+ specifier = head.strip()[name.end() :].strip()
141
+ return specifier or None
142
+ return None
143
+
144
+
145
+ def _mismatch(exc: ImportError) -> ImportError | None:
146
+ """Return the error to raise for a bindings mismatch, `None` for any other.
147
+
148
+ A mismatch is an `ImportError` that names the bindings or one of their
149
+ submodules: the package was found, and something this module asks of it
150
+ was not there. Anything else -- a dependency of the bindings that is
151
+ missing, a shared library that will not load -- is not this module's to
152
+ reword, and the caller re-raises it as it came.
153
+ """
154
+ if exc.name != _BINDINGS_MODULE and not (exc.name or "").startswith(
155
+ _BINDINGS_MODULE + "."
156
+ ):
157
+ return None
158
+ try:
159
+ installed = f"version {metadata.version(_BINDINGS_DISTRIBUTION)}"
160
+ except metadata.PackageNotFoundError:
161
+ installed = "a version it could not read"
162
+ floor = _floor()
163
+ required = (
164
+ f"{_BINDINGS_DISTRIBUTION}{floor}"
165
+ if floor
166
+ else f"the release the `secp256k1` extra of {_distribution} names"
167
+ )
168
+ return ImportError(
169
+ f"{_BINDINGS_DISTRIBUTION} is installed, {installed}, but it lacks what "
170
+ f"{_distribution} imports from it ({exc}); {_distribution} requires {required}. "
171
+ f"Upgrade it with `pip install --upgrade {_BINDINGS_DISTRIBUTION}` or "
172
+ f"install `{_distribution}[secp256k1]`",
173
+ name=exc.name,
174
+ )
175
+
176
+
177
+ try:
178
+ # `ffi` is the one object here that is not a wrapped call: issue
179
+ # btclib-org/btclib#1009 is why it is needed at all -- `ecc.ecc.dsa.Signer`
180
+ # builds its own `unsigned char[32]` with it, to hold a private key in
181
+ # memory this package can overwrite, the way `ssa.Signer` already can
182
+ # through the keypair `ssa` itself builds. Nothing else here needs it,
183
+ # `dsa.sign` and every other wrapped call taking that buffer as the `prvkey`
184
+ # a caller may already hold (btclib-org/btclib-secp256k1#253)
185
+ from btclib_secp256k1 import (
186
+ dsa,
187
+ ellswift,
188
+ ffi,
189
+ keys,
190
+ musig,
191
+ recovery,
192
+ ssa,
193
+ )
194
+ from btclib_secp256k1.ecdh import shared_point
195
+ from btclib_secp256k1.keys import (
196
+ PubkeyTweakChain,
197
+ pubkey_from_prvkey,
198
+ pubkey_sum,
199
+ pubkey_tweak_add,
200
+ pubkey_tweak_mul_sum,
201
+ )
202
+ from btclib_secp256k1.xonly import prvkey_tweak_add as xonly_prvkey_tweak_add
203
+ from btclib_secp256k1.xonly import pubkey_verify as xonly_pubkey_verify
204
+ from btclib_secp256k1.xonly import to_pubkey as xonly_to_pubkey
205
+ from btclib_secp256k1.xonly import tweak_add as xonly_tweak_add
206
+
207
+ INSTALLED = True
208
+ # issue btclib-org/btclib#1002 measured this branch rather than assuming it
209
+ # stays transitional: `test.yml`'s `no-bindings` job executes it every run, and
210
+ # `coverage-union` combines that run's data with the `coverage` job's. The
211
+ # combined report is 100% with this pragma removed -- so the branch is reached
212
+ # and is not dead code -- and the pragma still belongs here regardless, because
213
+ # `coverage-union` is a second gate beside the `coverage` job's, not instead of
214
+ # it: that job's own report, `pytest --cov` on this configuration alone, has
215
+ # bindings installed by construction, a `ModuleNotFoundError` only reachable by
216
+ # actually removing them, and a subprocess that does
217
+ # (`tests/no_bindings_test.py`) whose coverage that job does not collect. So
218
+ # this branch is a structural miss in that report regardless of the union, and
219
+ # removing the pragma would fail the one gate this issue chose to leave
220
+ # unchanged
221
+ except (
222
+ ImportError
223
+ ) as exc: # pragma: no cover -- only a child interpreter or no-bindings reaches this
224
+ # `exc.name` is the top-level package's own name, on a
225
+ # `ModuleNotFoundError`, only when the import system never found it at
226
+ # all. A submodule of an installed package failing to import names that
227
+ # submodule instead (`btclib_secp256k1.ecdh`, say), and a name missing
228
+ # from a module that *was* found is a plain `ImportError`. Either of
229
+ # those is an installed package too old for what this module asks of it
230
+ # (btclib-org/ecc#25), and the caller is told so and what to do
231
+ # (btclib-org/ecc#56) rather than silently handed the slower
232
+ # Python arithmetic
233
+ if not (isinstance(exc, ModuleNotFoundError) and exc.name == _BINDINGS_MODULE):
234
+ mismatch = _mismatch(exc)
235
+ if mismatch is None:
236
+ raise
237
+ raise mismatch from exc
238
+ # None and not a callable that raises: what would raise is never
239
+ # called, so the object would be a second thing to keep true. The
240
+ # ignore is on the assignment and not on the module: every other
241
+ # name here keeps the type the try branch gave it
242
+ dsa = ellswift = ffi = keys = musig = recovery = ssa = None # type: ignore[assignment]
243
+ shared_point = None # type: ignore[assignment]
244
+ # a class rather than a function, so mypy calls it an assignment to
245
+ # a type and wants the second code as well
246
+ PubkeyTweakChain = None # type: ignore[misc, assignment]
247
+ pubkey_from_prvkey = pubkey_sum = None # type: ignore[assignment]
248
+ pubkey_tweak_add = pubkey_tweak_mul_sum = None # type: ignore[assignment]
249
+ xonly_pubkey_verify = xonly_to_pubkey = None # type: ignore[assignment]
250
+ xonly_prvkey_tweak_add = xonly_tweak_add = None # type: ignore[assignment]
251
+
252
+ INSTALLED = False
253
+
254
+ # installed and not refused, which is one state and not two: a caller
255
+ # asking whether the bindings serve has no use for the difference, and
256
+ # `curves.curve` starts its seam from this
257
+ ENABLED = INSTALLED and not os.environ.get(NO_LIBSECP256K1)
258
+
259
+
260
+ def _wrong_bindings(what: str, *, setter: bool) -> EccRuntimeError:
261
+ """Return the error for a wrong answer of the bindings.
262
+
263
+ At import the variable is the way out; from the setter it is already
264
+ set, and not asking for the bindings is.
265
+ """
266
+ pure = "keep `serving=False`" if setter else f"set {NO_LIBSECP256K1}"
267
+ return EccRuntimeError(
268
+ f"the libsecp256k1 bindings {what}; ecc cannot use them. "
269
+ f"Reinstall them with `pip install --force-reinstall --no-cache-dir "
270
+ f"{_BINDINGS_DISTRIBUTION}`, or {pure} to use the pure Python arithmetic"
271
+ )
272
+
273
+
274
+ def _check_bindings(*, setter: bool) -> None:
275
+ """Raise unless the bindings answer fixed vectors correctly.
276
+
277
+ A misbuilt or mismatched wheel could derive keys or verify signatures
278
+ wrongly with no error, so the bindings, when first selected, are asked a
279
+ public key, one ECDSA verification and its refusal over another message,
280
+ and one BIP340 verification. The answers are published, not taken from
281
+ the bindings. A `raise`, not an `assert`, so
282
+ that `python -O` keeps it.
283
+ """
284
+ # the public key of the private key 1 is the generator, SEC 2 section 2.4.1
285
+ g = "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"
286
+ if pubkey_from_prvkey(1, True).hex() != g:
287
+ raise _wrong_bindings("derive a wrong public key", setter=setter)
288
+
289
+ # the first valid vector of the first group of Wycheproof 0.9rc5,
290
+ # tests/schemes/_data/ecdsa_secp256k1_sha256_bitcoin_test.json, with its key
291
+ # compressed and its signature as r || s, as
292
+ # `dsa.verify` passes it: `compact=True, normalize=True`
293
+ pub_key = bytes.fromhex(
294
+ "03b838ff44e5bc177bf21189d0766082fc9d843226887fc9760371100b7ee20a6f"
295
+ )
296
+ sig = bytes.fromhex(
297
+ "813ef79ccefa9a56f7ba805f0e478584fe5f0dd5f567bc09b5123ccbc9832365"
298
+ "6ff18a52dcc0336f7af62400a6dd9b810732baf1ff758000d6f613a556eb31ba"
299
+ )
300
+ msg_hash = sha256(bytes.fromhex("313233343030")).digest()
301
+ if not dsa.verify(msg_hash, pub_key, sig, normalize=True, compact=True):
302
+ raise _wrong_bindings("do not verify a known ECDSA signature", setter=setter)
303
+ if dsa.verify(
304
+ sha256(b"another message").digest(), pub_key, sig, normalize=True, compact=True
305
+ ):
306
+ raise _wrong_bindings(
307
+ "verify a known ECDSA signature over another message", setter=setter
308
+ )
309
+
310
+ # BIP340 test vector 0, tests/schemes/_data/bip340_test_vectors.csv
311
+ x_only = bytes.fromhex(
312
+ "F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9"
313
+ )
314
+ sig = bytes.fromhex(
315
+ "E907831F80848D1069A5371B402410364BDF1C5F8307B0084C55F1CE2DCA8215"
316
+ "25F66A4A85EA8B71E482A74F382D2CE5EBEEE8FDB2172F477DF4900D310536C0"
317
+ )
318
+ if not ssa.verify(bytes(32), x_only, sig):
319
+ raise _wrong_bindings("do not verify a known BIP340 signature", setter=setter)
320
+
321
+
322
+ _checked = False
323
+
324
+
325
+ def _check_once(*, setter: bool) -> None:
326
+ """Run `_check_bindings` unless it has already passed."""
327
+ global _checked # noqa: PLW0603
328
+ if not _checked:
329
+ _check_bindings(setter=setter)
330
+ _checked = True
331
+
332
+
333
+ # once, when the bindings are first selected: here, or in
334
+ # `curves.curve.set_libsecp256k1_serving`
335
+ if ENABLED:
336
+ _check_once(setter=False)