btclib.curves package

Submodules

btclib.curves.curve module

The prime-order Curve and the multiplications built on it.

mult, double_mult_var and multi_mult_var dispatch secp256k1 to the libsecp256k1 bindings and answer every other curve – and the cases the bindings cannot express – with curve_group’s arithmetic.

What this module exports is the class, the three multiplications, PreparedPoint – the one way a caller has of saying that a point of its own will come back, so that the tables built for it are kept – the catalogue, the four standards it is the union of – which is where btclib.curves keeps them, a standard being a question about a curve and not a way of finding one – and the *_params2 each of those four is built from, which is what test_catalogued_curves rebuilds every curve out of with both expensive checks on.

datadir stays out: it is where this package keeps the four json files loaded below, so it answers a question about the installation rather than about a curve, and btclib.network has a datadir of its own for the network files. The loader’s own names are underscored, a with target and a for target being module globals like any other.

class btclib.curves.curve.Curve(p: bytes | str | bytearray | memoryview | int, a: bytes | str | bytearray | memoryview | int, b: bytes | str | bytearray | memoryview | int, G: tuple[int, int], n: bytes | str | bytearray | memoryview | int, cofactor: int, weakness_check: bool = True, order_check: bool = True, name: str | None = None)[source]

Bases: CurveGroup

Cyclic subgroup of prime order n, generated by G, of the curve points.

The subgroup is ⟨G⟩ = {INF, G, 2G, …, (n-1)G}, of prime order n, inside the group of all the points of the curve — the CurveGroup this is built on, whose order is n times the cofactor h. The curve is that group and this is a subgroup of it, so the name is the wrong way round on purpose: this is the only group anything else in btclib multiplies in, and Curve is what a caller asks for.

Two things follow from n being prime, and btclib.ecc relies on both: the integers modulo n are a field, so every scalar but zero has an inverse — the nonce and s that ecdsa inverts, the challenge that ssa’s key recovery does — and the subgroup has no subgroup other than itself and {INF}, so every point of it but INF is a generator and no confinement to a small subgroup is possible.

n is a parameter and not a computed quantity: the order of G is the order of the whole group divided by the index of ⟨G⟩ in it, and counting the points of a curve this size is Schoof-Elkies-Atkin rather than arithmetic on p, a and b. What the constructor does compute is that the parameter is the order, and that is where the primality of n earns its second keep. nG = INF proves only that n annihilates G, i.e. that the order of G divides n; pinning it to n in general asks for (n/q)G ≠ INF for every prime q dividing n, which is the factorization of n — a number no curve publishes alongside its parameters. With n prime and G ≠ INF the single nG = INF is the whole of it.

Which is also why the two classes are the whole hierarchy, with no third one between them for the cyclic subgroup of unstated order: ⟨G⟩ carrying an n it cannot verify, or no n at all, is a cyclic group that cannot say how many elements it has, cannot reduce a scalar, and cannot bound a private key. Nothing here could take one as an argument.

class btclib.curves.curve.PreparedPoint(point: tuple[int, int], ec: Curve = Curve('FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE FFFFFC2F', 0, 7, ('79BE667E F9DCBBAC 55A06295 CE870B07 029BFCDB 2DCE28D9 59F2815B 16F81798', '483ADA77 26A3C465 5DA4FBFC 0E1108A8 FD17B448 A6855419 9C47D08F FB10D4B8'), 'FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE BAAEDCE6 AF48A03B BFD25E8C D0364141', 1))[source]

Bases: object

A point whose multiplication tables are kept, because it will come back.

The tables of curve_group are memoized on (point, curve, width) already, so a repeated point would find its own: what is missing is anyone to say that a point is repeated. Only the generator is assumed to be, and everything else is treated as arriving once – which is right for most callers and wrong for a few, and no measurement can tell which a caller is. This is where a caller says so.

Two tables answer to it, one per operation:

  • mult takes the fixed-base ladder of the generator instead of the GLV endomorphism, near four times cheaper a call once the per-position tables are built – 43 positions of 64 points on secp256k1, some 366 KiB. Break-even is 23 multiplications of the one point – dh.diffie_hellman against a counterparty, a taproot internal key tweaked repeatedly, pedersen against a fixed second generator.

  • a verification under it – dsa and ssa both take one where they take a public key – memoizes the wNAF tables of the key’s two endomorphism halves at _FIXED_POINT_W instead of rebuilding them at _DOUBLE_MULT_W per signature: 2 tables built per verification become 0, and the verification a fifth cheaper for ECDSA and for BIP340 alike. Break-even is 22 signatures under the one key, the first verification costing several times what a bare key’s does.

Both are the Python arithmetic. On secp256k1 with the bindings available neither is reached – libsecp256k1 verifies in a fraction of either and holds its own tables in its own context – so what this is for is the Python path: another curve, another hash function, or a deployment without the compiled bindings. Handing one in on the delegated path is not an error and costs nothing; it simply buys nothing.

Preparing is a caller’s word and never inferred, and the memory is why: the tables are per distinct point, so a library that memoized whatever public key arrived would hold a few MB for keys nobody will see again – issue #287, the bound _cached_base58_decode and pedersen.second_generator hold too. What bounds it here is that nothing is prepared unless asked, and beyond that the lru_cache maxsize the tables live under.

Nothing is built by the constructor. It parses and validates the point, which is the other half of what a verifier repeats – a decompression per signature, on the Python path where a compressed key is a field square root – and leaves the tables to the first multiplication that wants them, because which of the two families above is wanted is a question only that call answers.

Measured on an Apple M5, macOS 26.6, arm64, CPython 3.14, with curve._libsecp256k1_available set to False; best of five alternating rounds of 300 to 800 calls, and the median of seven for the cold rows, each on a freshly derived point so that the tables are built rather than found. A working desktop rather than a quiesced machine: a ratio, not a figure to quote.

point

the point, as the constructor validated it.

Type:

tuple[int, int]

ec

the curve it was validated against.

Type:

btclib.curves.curve.Curve

fixed

the Jacobian set a verification hands straight down, derived by the constructor from the two above; the comment beside the field is what it holds and why it is derived.

Type:

frozenset[tuple[int, int, int]]

Parameters:
  • point – the point to prepare, on the curve and not infinity.

  • ec – the curve it belongs to.

Raises:

BTClibValueError – if the point is not on the curve, or is infinity, which has no tables and multiplies to itself.

mult(m_int: bytes | str | bytearray | memoryview | int) tuple[int, int][source]

Return m*point, through the tables this point keeps.

curve.mult with the fixed-base arm taken for this point instead of only for the generator; everything else about the call, the dispatch to the bindings included, is the same.

btclib.curves.curve.double_mult_var(u: bytes | str | bytearray | memoryview | int, H: tuple[int, int], v: bytes | str | bytearray | memoryview | int, Q: tuple[int, int], ec: Curve = Curve('FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE FFFFFC2F', 0, 7, ('79BE667E F9DCBBAC 55A06295 CE870B07 029BFCDB 2DCE28D9 59F2815B 16F81798', '483ADA77 26A3C465 5DA4FBFC 0E1108A8 FD17B448 A6855419 9C47D08F FB10D4B8'), 'FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE BAAEDCE6 AF48A03B BFD25E8C D0364141', 1)) tuple[int, int][source]

Double scalar multiplication (u*H + v*Q).

btclib.curves.curve.is_libsecp256k1_serving() bool[source]

Return True if the bindings are what this process delegates to.

One question and not two: installed, and not refused. A caller has no use for the difference – what it can act on is whether the answer it is about to get comes from libsecp256k1 or from the Python arithmetic – and two observable states where there is one is how a caller comes to handle only the state it happened to meet.

The public reading of the seam every dispatch consults. It is what a project built on btclib asks when it must not check libsecp256k1 with libsecp256k1: Bitcoin Core’s own test framework keeps that rule – crypto/secp256k1.py is “designed for ease of understanding, not performance” – and issue #198 is btclib’s side of it.

btclib.curves.curve.mult(m_int: bytes | str | bytearray | memoryview | int, Q: tuple[int, int] | None = None, ec: Curve = Curve('FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE FFFFFC2F', 0, 7, ('79BE667E F9DCBBAC 55A06295 CE870B07 029BFCDB 2DCE28D9 59F2815B 16F81798', '483ADA77 26A3C465 5DA4FBFC 0E1108A8 FD17B448 A6855419 9C47D08F FB10D4B8'), 'FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE BAAEDCE6 AF48A03B BFD25E8C D0364141', 1)) tuple[int, int][source]

Elliptic curve scalar multiplication.

btclib.curves.curve.multi_mult_var(scalars: Sequence[bytes | str | bytearray | memoryview | int], points: Sequence[tuple[int, int]], ec: Curve = Curve('FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE FFFFFC2F', 0, 7, ('79BE667E F9DCBBAC 55A06295 CE870B07 029BFCDB 2DCE28D9 59F2815B 16F81798', '483ADA77 26A3C465 5DA4FBFC 0E1108A8 FD17B448 A6855419 9C47D08F FB10D4B8'), 'FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE BAAEDCE6 AF48A03B BFD25E8C D0364141', 1)) tuple[int, int][source]

Return the multi scalar multiplication u1*Q1 + … + un*Qn.

Interleaved wNAF on few scalars, Bos-Coster on many: curve_group’s _multi_mult_var dispatches on the count, at the size the two measure the same. On secp256k1 the bindings serve the whole sum instead.

ssa’s batch verification is what hands many scalars over at once, libsecp256k1 exposing no batch verification of its own, and it is the Python arm of that sum which arrives here: the delegated arm reaches _libsecp256k1_multi_mult_ below with its terms as octets, this signature taking points and a point being what its terms would have to be lifted into for the multiplication to write them straight back out.

btclib.curves.curve.set_libsecp256k1_serving(*, serving: bool) None[source]

Ask for the bindings, or for the Python arithmetic, from here on.

Process-wide and immediate: every dispatch in the package asks _libsecp256k1_serves, and that predicate reads the global this assigns, so nothing has to be re-imported and no module keeps an answer of its own.

serving=True with the bindings not installed is a request that cannot be served, and is refused rather than silently ignored: a caller that asked for C and got Python would be timing Python and calling it C. is_libsecp256k1_serving is how the answer is read back.

The environment variable is the other way in, and the earlier one: BTCLIB_NO_LIBSECP256K1 set to a non-empty value makes the initial state False, which is what a test runner wants – it settles before the first import, where this function cannot.

btclib.curves.curve_group module

Elliptic CurveGroup class and functions.

CurveGroup is every point of the curve, a group that needs be neither cyclic nor of prime order. Curve, in the btclib.curves.curve module, is the cyclic subgroup of prime order generated by G, which is the group everything built on curves multiplies in; the two are the whole of the hierarchy, and the class docstring of Curve says what separates them.

class btclib.curves.curve_group.CurveGroup(p: bytes | str | bytearray | memoryview | int, a: bytes | str | bytearray | memoryview | int, b: bytes | str | bytearray | memoryview | int)[source]

Bases: object

Finite group of the points of an elliptic curve over Fp.

The elliptic curve is the set of points (x, y) that are solutions to a Weierstrass equation y^2 = x^3 + a*x + b, with x, y, a, and b in Fp (p being a prime), together with a point at infinity INF. The constants a, b must satisfy the relationship 4 a^3 + 27 b^2 ≠ 0.

The group is defined by the point addition group law, INF being its neutral element, and it is finite and abelian — hence a product of at most two cyclic groups, Z_n1 x Z_n2 with n2 dividing n1, so neither cyclic nor of prime order in general.

How many points it holds is not among its data, and cannot be computed from them: the order is p+1 minus the trace of Frobenius, which Hasse’s theorem bounds by twice the square root of p and nothing here counts. Nor is any point of the group distinguished from the others. The generator G, the prime order n of the cyclic subgroup it generates, and the cofactor relating that n to the order of this group are the parameters of Curve, in btclib.curves.curve, which is what a caller of the library multiplies in; what this class is for is the arithmetic underneath, shared by the two and defined by p, a and b alone.

add_aff_var(Q: tuple[int, int], R: tuple[int, int]) tuple[int, int][source]

Return the sum of two affine points, assumed on the curve.

One modular inversion; the special cases are branched on, the comment below saying why affine coordinates leave no choice.

add_jac(Q: tuple[int, int, int], R: tuple[int, int, int]) tuple[int, int, int][source]

Return the sum of two Jacobian points, branch-free.

The input points are assumed to be on the curve. One sequence of operations whatever the operands – infinity and doubling included, the comment below saying why that is load-bearing.

add_jac_aff(Q: tuple[int, int, int], R: tuple[int, int]) tuple[int, int, int][source]

Return the sum of a Jacobian point and an affine one, branch-free.

add_jac with the second operand’s Z known to be one, which is what an affine point is: five of the sixteen products become multiplications by one – R’s two powers of Z, the two that put Q in R’s frame, and one factor of the answer’s Z. libsecp256k1 keeps its tables in affine coordinates for exactly this and states secp256k1_gej_add_ge_var as 8 mul and 3 sqr against the 12 and 4 of secp256k1_gej_add_var.

The affine operand is a Point and not a JacPoint whose Z happens to be one, so the precondition is in the signature rather than in a sentence nothing checks.

The input points are assumed to be on the curve. Every case add_jac answers is answered here the same way and for the same reasons, which its comments carry: infinity through a stand-in and a selection at the end, spelled R[1] == 0 because that is what infinity is in affine coordinates, and the doubling and the sum that is infinity through the one branch on V.

add_var(Q1: tuple[int, int], Q2: tuple[int, int]) tuple[int, int][source]

Return the sum of two points.

The input points must be on the curve.

aff_from_jac_batch_var(Qs: Sequence[tuple[int, int, int]]) list[tuple[int, int]][source]

Return the affine points: one modular inversion for all of them.

aff_from_jac_var over a sequence, with mod_inv_batch_var in place of the one inverse each: the conversion is two products a point once the inverse is in hand, so a caller holding several Jacobian points pays one extended Euclid instead of one per point.

The input points are assumed to be on the curve. Infinity is not in the batch, having no Z to invert, and comes back as INF where it stood.

aff_from_jac_var(Q: tuple[int, int, int]) tuple[int, int][source]

Return the affine point: one modular inversion.

The input point is assumed to be on the curve; infinity comes back as INF, its affine spelling.

double_aff_var(Q: tuple[int, int]) tuple[int, int][source]

Return twice the affine point, assumed to be on the curve.

double_jac(Q: tuple[int, int, int]) tuple[int, int, int][source]

Return twice the Jacobian point, assumed to be on the curve.

is_jac_equal(QJ: tuple[int, int, int], PJ: tuple[int, int, int]) bool[source]

Return True if Jacobian points are equal in affine coordinates.

The input points are assumed to be on curve.

is_on_curve(Q: tuple[int, int]) bool[source]

Return True if the point is on the curve.

negate(Q: tuple[int, int]) tuple[int, int][source]

Return the opposite point.

The input point is not checked to be on the curve.

negate_jac(Q: tuple[int, int, int]) tuple[int, int, int][source]

Return the opposite Jacobian point.

The input point is not checked to be on the curve.

require_on_curve(Q: tuple[int, int]) None[source]

Require the input curve Point to be on the curve.

An Error is raised if not.

x_aff_from_jac_var(Q: tuple[int, int, int]) int[source]

Return the affine x alone, without the products y costs.

One inversion, as aff_from_jac_var, of Z^2 rather than of Z: the power x wants is the one inverted, so nothing is rebuilt from it. The input point is assumed to be on the curve; infinity has no x and is refused.

y_aff_from_jac_var(Q: tuple[int, int, int]) int[source]

Return the affine y alone, without the product x costs.

One inversion, as aff_from_jac_var, of Z^3 rather than of Z, for the same reason x_aff_from_jac_var inverts Z^2. The input point is assumed to be on the curve; infinity has no y and is refused.

y_even_var(x: int) int[source]

Return the odd/even affine y-coordinate associated to x.

y_low_var(x: int) int[source]

Return the low/high affine y-coordinate associated to x.

y_quadratic_residue_var(x: int) int[source]

Return the quadratic residue affine y-coordinate.

y_var(x: int) int[source]

Return the y coordinate from x, as in (x, y).

btclib.curves.curve_group.signed_odd_digits(m: int, w: int, size: int) list[int][source]

Return the size signed odd base-2^w digits of an odd m.

Regular recoding, Joye-Tunstall: m = sum(digits[i] * 2^(w*i)) with every digit odd and in {±1, ±3, …, ±(2^w - 1)}, least significant first, as _wNAF_of_m returns its own. Two properties are what it is for, and neither is the wNAF’s:

  • no digit is zero, so the multiplication that indexes them makes one addition per digit whatever m is, where a wNAF adds on a nonzero digit and so once per unit of the recoded weight

  • the count is size and not the length of m, so a scalar’s size stops being visible too

The digits are signed, and that is what buys the first property: 2^w odd values in -(2^w - 1)..2^w - 1 name 2^(w-1) points and their opposites, and negating a Jacobian point is one modular subtraction.

m must be odd – a sum of odd digits weighted by powers of 2 has the parity of digits[0], so no even number has such a form at all – and must fit the size asked for, which is m < 2^(w*size): the last digit is what is left of m and is not reduced further.

btclib.curves.curve_group_2 module

Elliptic curve point multiplication functions.

The implemented algorithms are:

  • Montgomery Ladder

  • Scalar multiplication on basis 3

  • Fixed window

  • Regular window, signed odd digits (Joye-Tunstall recoding)

  • Sliding window

  • w-ary non-adjacent form (wNAF)

  • Interleaved-wNAF double multiplication (HMV algorithm 3.51)

  • Regular-window double multiplication

  • GLV endomorphism multiplication for secp256k1 (HMV algorithm 3.77)

  • GLV endomorphism double multiplication for secp256k1

References

Further improvements, and the material for them. These are not bibliography: each is an improvement this module could take, with what would be needed to take it, kept next to the code it is about. The point-addition special cases are settled where they live, in curve_group’s own comments, and issue 183 tracks the smaller follow-ups; what is here is the rest:

Measured against libsecp256k1 and not taken. Each of these is an algorithm that library carries and this one does not, with the result that decided it, so that what the next reader has is the verdict and not the measurement to make again. Best of five on secp256k1’s p, Python 3.14.6, macOS arm64:

  • the field inverse by an addition chain, Peter Dettman’s and Brian Smith’s, and libsecp256k1’s own safegcd beside them: pow(a, -1, p) is CPython’s extended Euclid in C, where 255 modular squarings in bytecode – fewer than any chain needs – cost several times what it does. pow(a, p - 2, p) costs about what those squarings do, which is why mod_inv_var does not spell Fermat either:

  • fast reduction for a pseudo-Mersenne p: the Solinas form for 2^256 - 2^32 - 977, two products by a 33-bit constant and a conditional subtraction, costs more than x % p does. CPython’s division is C, and 512 bits by 256 is small

  • limbs with delayed reduction, libsecp256k1’s 5 by 52 bits and its magnitude tracking: the Python analogue is letting intermediates grow, which add_jac’s own comment measured at 2.0x to 3.0x the wrong way – an integer costs what its size costs

  • a separate squaring routine: CPython’s long_mul already takes the squaring path when both operands are the same object, a*a % p costing measurably less than a*b % p

  • the masked table lookup of secp256k1_ecmult_table_get_ge, which reads every entry of a table under a cmov: a list index is a list index

  • the lambda split’s division by a multiply and a shift, secp256k1_scalar_mul_shift_var: _multiplier_decomposer rounds with (_B2 * m + n // 2) // n, 384 bits by 256, where libsecp256k1 multiplies by a precomputed reciprocal instead. The rounding is the dearer of the two and is paid once per multiplication, which is three orders of magnitude above either

  • the table built with no inversion at all, secp256k1_ecmult_odd_multiples_table with secp256k1_ge_table_set_globalz: the odd multiples are formed on an isomorphic curve where the doubled point is affine, the z-ratios are kept as they go, and the entries reach one common Z by products alone. What it would remove is the single inversion a call now spends, which is 1% of a _mult and 0.2% of what a 16-point _multi_mult_var takes; the rest of that conversion is the three products an entry costs, and the isomorphic construction pays those in its own coin. An accumulator that has to live in that frame and be rescaled out of it at the end, for those two ceilings

  • the square root by an addition chain is the one of these that measures positive and is still not here: pow(a, (p + 1) // 4, p) is 1.16x libsecp256k1’s chain, for some twenty lines holding for secp256k1’s p alone, on a function a point decompression away from these loops

btclib.curves.curve_group_f module

CurveGroup explorer functions, for low-cardinality didactic curves.

Enumerating the points of a group is feasible only when the group is tiny, which is what makes these useful in teaching and useless – and never used – in the rest of the library.

btclib.curves.curve_group_f.find_all_points(ec: CurveGroup) list[tuple[int, int]][source]

Attempt to find all group points, if p is low.

Very unsofisticated walk-through approach, for didactic sake only.

btclib.curves.curve_group_f.find_subgroup_points(ec: CurveGroup, G: tuple[int, int]) list[tuple[int, int]][source]

Attempt to count all G-generated subgroup points, if p is low.

Very unsofisticated walk-through approach, for didactic sake only.

btclib.curves.sec_point module

SEC compressed/uncompressed point representation.

btclib.curves.sec_point.bytes_from_point(Q: tuple[int, int], ec: Curve = Curve('FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE FFFFFC2F', 0, 7, ('79BE667E F9DCBBAC 55A06295 CE870B07 029BFCDB 2DCE28D9 59F2815B 16F81798', '483ADA77 26A3C465 5DA4FBFC 0E1108A8 FD17B448 A6855419 9C47D08F FB10D4B8'), 'FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE BAAEDCE6 AF48A03B BFD25E8C D0364141', 1), compressed: bool = True) bytes[source]

Return a point as compressed/uncompressed octet sequence.

Return a point as compressed (0x02, 0x03) or uncompressed (0x04) octet sequence, according to SEC 1 v.2, section 2.3.3.

btclib.curves.sec_point.bytes_from_prv_key_int(prv_key_int: bytes | str | bytearray | memoryview | int, ec: Curve = Curve('FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE FFFFFC2F', 0, 7, ('79BE667E F9DCBBAC 55A06295 CE870B07 029BFCDB 2DCE28D9 59F2815B 16F81798', '483ADA77 26A3C465 5DA4FBFC 0E1108A8 FD17B448 A6855419 9C47D08F FB10D4B8'), 'FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE BAAEDCE6 AF48A03B BFD25E8C D0364141', 1), compressed: bool = True) bytes[source]

Return the public key of a scalar, as SEC octets.

This is bytes_from_point(mult(prv_key_int, ec.G, ec), ec, compressed) and answers what that answers, the edges included: the scalar is reduced mod n, and zero – the infinity point – has no representation and raises.

That composition is what BIP32 derivation and every private-to-public conversion do once per key (issue #127). For secp256k1 this never materializes the point: keys.pubkey_from_prvkey is one secp256k1_ec_pubkey_create plus one serialize, with the compressed flag passed straight through, so the bindings are the ones writing the compressed encoding rather than btclib slicing it out of the uncompressed one (issue #459).

btclib.curves.sec_point.point_from_octets(pub_key: bytes | str | bytearray | memoryview, ec: Curve = Curve('FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE FFFFFC2F', 0, 7, ('79BE667E F9DCBBAC 55A06295 CE870B07 029BFCDB 2DCE28D9 59F2815B 16F81798', '483ADA77 26A3C465 5DA4FBFC 0E1108A8 FD17B448 A6855419 9C47D08F FB10D4B8'), 'FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE BAAEDCE6 AF48A03B BFD25E8C D0364141', 1), *, hybrid: bool = False) tuple[int, int][source]

Return a tuple (x_Q, y_Q) that belongs to the curve.

Return a tuple (x_Q, y_Q) that belongs to the curve according to SEC 1 v.2, section 2.3.4.

The compressed prefixes are the only branch libsecp256k1 serves, and the whole cost of the function is there: lifting x to a point is a modular square root, nearly all of that cost on either arm, and delegated it costs a small fraction of what the Python one does, while the 65-byte forms carry the y and cost the same either way (issue 284).

hybrid admits the 0x06 and 0x07 prefixes of that same section, which carry both coordinates like 0x04 does and repeat the parity of y in the prefix. It is off by default, and not out of squeamishness: the point is a point, and libsecp256k1’s ec_pubkey_parse takes all three 65-byte prefixes (eckey_impl.h). What decides is where the parsed key goes next – addresses, WIF and the descriptor language have no hybrid form to render, and nothing in bitcoin produces one. Consensus has to accept what was mined instead: Core rejects hybrid keys only under STRICTENC, so the script engine is the one caller that asks for them (issue #129). It is refused rather than read for its truth: its True is the permissive value, and a non-bool is true, so hybrid=”no” would parse the very prefixes it was written down to keep out.

btclib.curves.sec_point.scalar_from_prv_key(prv_key: bytes | str | bytearray | memoryview | int, ec: Curve = Curve('FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE FFFFFC2F', 0, 7, ('79BE667E F9DCBBAC 55A06295 CE870B07 029BFCDB 2DCE28D9 59F2815B 16F81798', '483ADA77 26A3C465 5DA4FBFC 0E1108A8 FD17B448 A6855419 9C47D08F FB10D4B8'), 'FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE BAAEDCE6 AF48A03B BFD25E8C D0364141', 1)) int[source]

Return a verified-as-valid private key integer.

Here rather than in a converter, and beside bytes_from_prv_key_int for its reason: a scalar in 1..n-1 is a fact about the curve and nothing above it knows more about it than this file does.

Integer and not a PrvKey of this module’s own, which would be that same union of types under a second name and so nothing mypy could check; the parameter carries the role the way mult’s m_int does. The spellings this takes are narrower than the ones Integer names – int_from_integer reads “0xc0ffee” and a short hex string, where a key is n_size octets or nothing. What rules those out is reading them with bytes_from_octets and the size handed to it, rather than with int_from_integer; the annotation could not, the two unions being one. A WIF and an extended key are not among the spellings – they are b58’s and bip32’s objects, and turning one into a scalar is a call a caller makes rather than a spelling this layer guesses at (issue #1188).

The range is checked in Python for every curve. keys.prvkey_verify is libsecp256k1’s answer to the same question and is not called for want of anything to gain: a comparison on a value that is already a Python int, with no constant-time argument to pay for the call with, since whether a key is in range is precisely what the caller is being told. to_prv_key.int_from_prv_key carries the measurement behind that, and carries it until issue #1188’s last step removes it.

Module contents

Module btclib.curves.

The arithmetic. btclib.curves holds the elliptic curve itself: the field and group operations of curve_group, the Curve built on them, the catalogued curves (secp256k1 among them), and the SEC encoding of a point. Nothing here knows what a signature is.

What is built on a curve lives in btclib.ecc – dsa, ssa, bms, borromean, pedersen, Diffie-Hellman, the nonces – and the rule between the two is that direction: ecc imports curves, never the other way round.

The two names are easy to conflate – everything in ecc is also about curves – so the anchor is worth stating: from btclib.curves import mult, from btclib.ecc import dsa.

What this package exports is the curve API: a Curve, the catalogue they are looked up in, the three scalar multiplications, the SEC point codec, and PreparedPoint, which is a point plus a caller’s word that it will be multiplied again – the one thing the memoized tables below cannot infer and the whole of what stands between a repeated point and the treatment the generator gets.

CURVES is what makes the catalogue reachable – secp256k1 is exported by name because nearly every caller wants that one, and every other curve is CURVES[“secp256r1”] – so exporting the name and not the dictionary left the paragraph above naming curves a caller could not get at without importing the module the name is defined in. The four catalogues it is the union of (SEC2v1, SEC2v2, NIST, Brainpool) stay where they are defined: which standard a curve comes from is a question about a curve, not a way of finding one.

The other multiplications of btclib.curves.curve_group and btclib.curves.curve_group_2 – _mult_aff_var, _mult_jac_var, _mult_base_3_var, _mult_mont_ladder_var, the two _mult_recursive_*_var, the two _mult_fixed_window*_var, _mult_fixed_base, _mult_regular_window, _mult_sliding_window_var, _mult_w_NAF_var, the three _double_mult_*, and the two _mult_endomorphism_secp256k1* – are private, as are the _multiples, _cached_multiples, _cached_fixed_base_multiples, _odd_multiples and _jac_from_aff they are built on.

The _var suffix on most of those is libsecp256k1’s, and it says the same thing: the number of point operations such a multiplication makes depends on the coefficient it is given, where _mult_regular_window, _mult_fixed_base and _double_mult_regular_window make the same number for every scalar of the curve. CONTRIBUTING.md states the rule, and states what its absence does not promise: nothing here is constant-time.

Which one runs is not a setting, and a census says why it is not (issue #849). On the generator the regular form is also the faster one, by a factor of four: _mult_fixed_base makes no doubling at all, where a wNAF makes one per bit however wide its table is and however thoroughly it is cached, the factor holding over the memoized odd multiples of G at w=8, w=10 and w=12 alike. So the arm every key derivation, every BIP32 child and every signing nonce runs has no variable-time alternative to offer. What is left is a variable-base mult with a secret scalar, which in this package is ecc.dh: 1.13x on secp256k1 and 1.03x on nistp256, the second being that measurement’s own noise. Beside it, the blinded nonce inverse is a fraction of a percent of a signature, the projective blinding of curve_group._blinded_jac is 1%, and every verification is already _var. The 1.13x was measured with the bindings switched off. btclib_secp256k1 is the secp256k1 extra rather than a required dependency, so a caller who skips it is already in that state, with nothing to switch.

They are implementations of one operation, kept side by side to be measured against each other, and a menu of them is not an API: a caller reading them would find every way of multiplying a point this package implements and nothing to say that mult is the one to use, that it dispatches to libsecp256k1 for secp256k1 and the generator, and that _mult_jac_var is not the faster alternative its name suggests.

The underscore says the second thing too, which is what decided it: each takes a point it assumes to be on the curve and checks nothing, so a malformed one is answered with a point rather than refused. mult, double_mult_var and multi_mult_var are where require_on_curve runs, on every argument and every path, and reaching past them is reaching past that. The test suite takes each variant from the module that defines it, which is what a private name is still good for.

Beside the two point conversions the codec carries bytes_from_prv_key_int, the composition of a multiplication and an encoding that every private-to-public conversion is, answered for secp256k1 out of the bindings’ own serialization, without materializing the point (issue #127); and scalar_from_prv_key, which reads a private key the other way, as the scalar in 1..n-1 that it is. What a private key may be spelled as at this layer is the integer, its n_size octets, or their hex. That is narrower than everything Integer takes – int_from_integer reads a “0x” prefix and a short hex string, which a key of a fixed size must not be – and the annotation is Integer because the two are the same union of types and a second name for it would say nothing mypy could check. A WIF and an extended key are not among the spellings at all, belonging to b58 and bip32, above here (issue #1188).

class btclib.curves.Curve(p: bytes | str | bytearray | memoryview | int, a: bytes | str | bytearray | memoryview | int, b: bytes | str | bytearray | memoryview | int, G: tuple[int, int], n: bytes | str | bytearray | memoryview | int, cofactor: int, weakness_check: bool = True, order_check: bool = True, name: str | None = None)[source]

Bases: CurveGroup

Cyclic subgroup of prime order n, generated by G, of the curve points.

The subgroup is ⟨G⟩ = {INF, G, 2G, …, (n-1)G}, of prime order n, inside the group of all the points of the curve — the CurveGroup this is built on, whose order is n times the cofactor h. The curve is that group and this is a subgroup of it, so the name is the wrong way round on purpose: this is the only group anything else in btclib multiplies in, and Curve is what a caller asks for.

Two things follow from n being prime, and btclib.ecc relies on both: the integers modulo n are a field, so every scalar but zero has an inverse — the nonce and s that ecdsa inverts, the challenge that ssa’s key recovery does — and the subgroup has no subgroup other than itself and {INF}, so every point of it but INF is a generator and no confinement to a small subgroup is possible.

n is a parameter and not a computed quantity: the order of G is the order of the whole group divided by the index of ⟨G⟩ in it, and counting the points of a curve this size is Schoof-Elkies-Atkin rather than arithmetic on p, a and b. What the constructor does compute is that the parameter is the order, and that is where the primality of n earns its second keep. nG = INF proves only that n annihilates G, i.e. that the order of G divides n; pinning it to n in general asks for (n/q)G ≠ INF for every prime q dividing n, which is the factorization of n — a number no curve publishes alongside its parameters. With n prime and G ≠ INF the single nG = INF is the whole of it.

Which is also why the two classes are the whole hierarchy, with no third one between them for the cyclic subgroup of unstated order: ⟨G⟩ carrying an n it cannot verify, or no n at all, is a cyclic group that cannot say how many elements it has, cannot reduce a scalar, and cannot bound a private key. Nothing here could take one as an argument.

class btclib.curves.CurveGroup(p: bytes | str | bytearray | memoryview | int, a: bytes | str | bytearray | memoryview | int, b: bytes | str | bytearray | memoryview | int)[source]

Bases: object

Finite group of the points of an elliptic curve over Fp.

The elliptic curve is the set of points (x, y) that are solutions to a Weierstrass equation y^2 = x^3 + a*x + b, with x, y, a, and b in Fp (p being a prime), together with a point at infinity INF. The constants a, b must satisfy the relationship 4 a^3 + 27 b^2 ≠ 0.

The group is defined by the point addition group law, INF being its neutral element, and it is finite and abelian — hence a product of at most two cyclic groups, Z_n1 x Z_n2 with n2 dividing n1, so neither cyclic nor of prime order in general.

How many points it holds is not among its data, and cannot be computed from them: the order is p+1 minus the trace of Frobenius, which Hasse’s theorem bounds by twice the square root of p and nothing here counts. Nor is any point of the group distinguished from the others. The generator G, the prime order n of the cyclic subgroup it generates, and the cofactor relating that n to the order of this group are the parameters of Curve, in btclib.curves.curve, which is what a caller of the library multiplies in; what this class is for is the arithmetic underneath, shared by the two and defined by p, a and b alone.

add_aff_var(Q: tuple[int, int], R: tuple[int, int]) tuple[int, int][source]

Return the sum of two affine points, assumed on the curve.

One modular inversion; the special cases are branched on, the comment below saying why affine coordinates leave no choice.

add_jac(Q: tuple[int, int, int], R: tuple[int, int, int]) tuple[int, int, int][source]

Return the sum of two Jacobian points, branch-free.

The input points are assumed to be on the curve. One sequence of operations whatever the operands – infinity and doubling included, the comment below saying why that is load-bearing.

add_jac_aff(Q: tuple[int, int, int], R: tuple[int, int]) tuple[int, int, int][source]

Return the sum of a Jacobian point and an affine one, branch-free.

add_jac with the second operand’s Z known to be one, which is what an affine point is: five of the sixteen products become multiplications by one – R’s two powers of Z, the two that put Q in R’s frame, and one factor of the answer’s Z. libsecp256k1 keeps its tables in affine coordinates for exactly this and states secp256k1_gej_add_ge_var as 8 mul and 3 sqr against the 12 and 4 of secp256k1_gej_add_var.

The affine operand is a Point and not a JacPoint whose Z happens to be one, so the precondition is in the signature rather than in a sentence nothing checks.

The input points are assumed to be on the curve. Every case add_jac answers is answered here the same way and for the same reasons, which its comments carry: infinity through a stand-in and a selection at the end, spelled R[1] == 0 because that is what infinity is in affine coordinates, and the doubling and the sum that is infinity through the one branch on V.

add_var(Q1: tuple[int, int], Q2: tuple[int, int]) tuple[int, int][source]

Return the sum of two points.

The input points must be on the curve.

aff_from_jac_batch_var(Qs: Sequence[tuple[int, int, int]]) list[tuple[int, int]][source]

Return the affine points: one modular inversion for all of them.

aff_from_jac_var over a sequence, with mod_inv_batch_var in place of the one inverse each: the conversion is two products a point once the inverse is in hand, so a caller holding several Jacobian points pays one extended Euclid instead of one per point.

The input points are assumed to be on the curve. Infinity is not in the batch, having no Z to invert, and comes back as INF where it stood.

aff_from_jac_var(Q: tuple[int, int, int]) tuple[int, int][source]

Return the affine point: one modular inversion.

The input point is assumed to be on the curve; infinity comes back as INF, its affine spelling.

double_aff_var(Q: tuple[int, int]) tuple[int, int][source]

Return twice the affine point, assumed to be on the curve.

double_jac(Q: tuple[int, int, int]) tuple[int, int, int][source]

Return twice the Jacobian point, assumed to be on the curve.

is_jac_equal(QJ: tuple[int, int, int], PJ: tuple[int, int, int]) bool[source]

Return True if Jacobian points are equal in affine coordinates.

The input points are assumed to be on curve.

is_on_curve(Q: tuple[int, int]) bool[source]

Return True if the point is on the curve.

negate(Q: tuple[int, int]) tuple[int, int][source]

Return the opposite point.

The input point is not checked to be on the curve.

negate_jac(Q: tuple[int, int, int]) tuple[int, int, int][source]

Return the opposite Jacobian point.

The input point is not checked to be on the curve.

require_on_curve(Q: tuple[int, int]) None[source]

Require the input curve Point to be on the curve.

An Error is raised if not.

x_aff_from_jac_var(Q: tuple[int, int, int]) int[source]

Return the affine x alone, without the products y costs.

One inversion, as aff_from_jac_var, of Z^2 rather than of Z: the power x wants is the one inverted, so nothing is rebuilt from it. The input point is assumed to be on the curve; infinity has no x and is refused.

y_aff_from_jac_var(Q: tuple[int, int, int]) int[source]

Return the affine y alone, without the product x costs.

One inversion, as aff_from_jac_var, of Z^3 rather than of Z, for the same reason x_aff_from_jac_var inverts Z^2. The input point is assumed to be on the curve; infinity has no y and is refused.

y_even_var(x: int) int[source]

Return the odd/even affine y-coordinate associated to x.

y_low_var(x: int) int[source]

Return the low/high affine y-coordinate associated to x.

y_quadratic_residue_var(x: int) int[source]

Return the quadratic residue affine y-coordinate.

y_var(x: int) int[source]

Return the y coordinate from x, as in (x, y).

class btclib.curves.PreparedPoint(point: tuple[int, int], ec: Curve = Curve('FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE FFFFFC2F', 0, 7, ('79BE667E F9DCBBAC 55A06295 CE870B07 029BFCDB 2DCE28D9 59F2815B 16F81798', '483ADA77 26A3C465 5DA4FBFC 0E1108A8 FD17B448 A6855419 9C47D08F FB10D4B8'), 'FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE BAAEDCE6 AF48A03B BFD25E8C D0364141', 1))[source]

Bases: object

A point whose multiplication tables are kept, because it will come back.

The tables of curve_group are memoized on (point, curve, width) already, so a repeated point would find its own: what is missing is anyone to say that a point is repeated. Only the generator is assumed to be, and everything else is treated as arriving once – which is right for most callers and wrong for a few, and no measurement can tell which a caller is. This is where a caller says so.

Two tables answer to it, one per operation:

  • mult takes the fixed-base ladder of the generator instead of the GLV endomorphism, near four times cheaper a call once the per-position tables are built – 43 positions of 64 points on secp256k1, some 366 KiB. Break-even is 23 multiplications of the one point – dh.diffie_hellman against a counterparty, a taproot internal key tweaked repeatedly, pedersen against a fixed second generator.

  • a verification under it – dsa and ssa both take one where they take a public key – memoizes the wNAF tables of the key’s two endomorphism halves at _FIXED_POINT_W instead of rebuilding them at _DOUBLE_MULT_W per signature: 2 tables built per verification become 0, and the verification a fifth cheaper for ECDSA and for BIP340 alike. Break-even is 22 signatures under the one key, the first verification costing several times what a bare key’s does.

Both are the Python arithmetic. On secp256k1 with the bindings available neither is reached – libsecp256k1 verifies in a fraction of either and holds its own tables in its own context – so what this is for is the Python path: another curve, another hash function, or a deployment without the compiled bindings. Handing one in on the delegated path is not an error and costs nothing; it simply buys nothing.

Preparing is a caller’s word and never inferred, and the memory is why: the tables are per distinct point, so a library that memoized whatever public key arrived would hold a few MB for keys nobody will see again – issue #287, the bound _cached_base58_decode and pedersen.second_generator hold too. What bounds it here is that nothing is prepared unless asked, and beyond that the lru_cache maxsize the tables live under.

Nothing is built by the constructor. It parses and validates the point, which is the other half of what a verifier repeats – a decompression per signature, on the Python path where a compressed key is a field square root – and leaves the tables to the first multiplication that wants them, because which of the two families above is wanted is a question only that call answers.

Measured on an Apple M5, macOS 26.6, arm64, CPython 3.14, with curve._libsecp256k1_available set to False; best of five alternating rounds of 300 to 800 calls, and the median of seven for the cold rows, each on a freshly derived point so that the tables are built rather than found. A working desktop rather than a quiesced machine: a ratio, not a figure to quote.

point

the point, as the constructor validated it.

Type:

tuple[int, int]

ec

the curve it was validated against.

Type:

btclib.curves.curve.Curve

fixed

the Jacobian set a verification hands straight down, derived by the constructor from the two above; the comment beside the field is what it holds and why it is derived.

Type:

frozenset[tuple[int, int, int]]

Parameters:
  • point – the point to prepare, on the curve and not infinity.

  • ec – the curve it belongs to.

Raises:

BTClibValueError – if the point is not on the curve, or is infinity, which has no tables and multiplies to itself.

mult(m_int: bytes | str | bytearray | memoryview | int) tuple[int, int][source]

Return m*point, through the tables this point keeps.

curve.mult with the fixed-base arm taken for this point instead of only for the generator; everything else about the call, the dispatch to the bindings included, is the same.

btclib.curves.bytes_from_point(Q: tuple[int, int], ec: Curve = Curve('FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE FFFFFC2F', 0, 7, ('79BE667E F9DCBBAC 55A06295 CE870B07 029BFCDB 2DCE28D9 59F2815B 16F81798', '483ADA77 26A3C465 5DA4FBFC 0E1108A8 FD17B448 A6855419 9C47D08F FB10D4B8'), 'FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE BAAEDCE6 AF48A03B BFD25E8C D0364141', 1), compressed: bool = True) bytes[source]

Return a point as compressed/uncompressed octet sequence.

Return a point as compressed (0x02, 0x03) or uncompressed (0x04) octet sequence, according to SEC 1 v.2, section 2.3.3.

btclib.curves.bytes_from_prv_key_int(prv_key_int: bytes | str | bytearray | memoryview | int, ec: Curve = Curve('FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE FFFFFC2F', 0, 7, ('79BE667E F9DCBBAC 55A06295 CE870B07 029BFCDB 2DCE28D9 59F2815B 16F81798', '483ADA77 26A3C465 5DA4FBFC 0E1108A8 FD17B448 A6855419 9C47D08F FB10D4B8'), 'FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE BAAEDCE6 AF48A03B BFD25E8C D0364141', 1), compressed: bool = True) bytes[source]

Return the public key of a scalar, as SEC octets.

This is bytes_from_point(mult(prv_key_int, ec.G, ec), ec, compressed) and answers what that answers, the edges included: the scalar is reduced mod n, and zero – the infinity point – has no representation and raises.

That composition is what BIP32 derivation and every private-to-public conversion do once per key (issue #127). For secp256k1 this never materializes the point: keys.pubkey_from_prvkey is one secp256k1_ec_pubkey_create plus one serialize, with the compressed flag passed straight through, so the bindings are the ones writing the compressed encoding rather than btclib slicing it out of the uncompressed one (issue #459).

btclib.curves.double_mult_var(u: bytes | str | bytearray | memoryview | int, H: tuple[int, int], v: bytes | str | bytearray | memoryview | int, Q: tuple[int, int], ec: Curve = Curve('FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE FFFFFC2F', 0, 7, ('79BE667E F9DCBBAC 55A06295 CE870B07 029BFCDB 2DCE28D9 59F2815B 16F81798', '483ADA77 26A3C465 5DA4FBFC 0E1108A8 FD17B448 A6855419 9C47D08F FB10D4B8'), 'FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE BAAEDCE6 AF48A03B BFD25E8C D0364141', 1)) tuple[int, int][source]

Double scalar multiplication (u*H + v*Q).

btclib.curves.find_all_points(ec: CurveGroup) list[tuple[int, int]][source]

Attempt to find all group points, if p is low.

Very unsofisticated walk-through approach, for didactic sake only.

btclib.curves.find_subgroup_points(ec: CurveGroup, G: tuple[int, int]) list[tuple[int, int]][source]

Attempt to count all G-generated subgroup points, if p is low.

Very unsofisticated walk-through approach, for didactic sake only.

btclib.curves.is_libsecp256k1_serving() bool[source]

Return True if the bindings are what this process delegates to.

One question and not two: installed, and not refused. A caller has no use for the difference – what it can act on is whether the answer it is about to get comes from libsecp256k1 or from the Python arithmetic – and two observable states where there is one is how a caller comes to handle only the state it happened to meet.

The public reading of the seam every dispatch consults. It is what a project built on btclib asks when it must not check libsecp256k1 with libsecp256k1: Bitcoin Core’s own test framework keeps that rule – crypto/secp256k1.py is “designed for ease of understanding, not performance” – and issue #198 is btclib’s side of it.

btclib.curves.mult(m_int: bytes | str | bytearray | memoryview | int, Q: tuple[int, int] | None = None, ec: Curve = Curve('FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE FFFFFC2F', 0, 7, ('79BE667E F9DCBBAC 55A06295 CE870B07 029BFCDB 2DCE28D9 59F2815B 16F81798', '483ADA77 26A3C465 5DA4FBFC 0E1108A8 FD17B448 A6855419 9C47D08F FB10D4B8'), 'FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE BAAEDCE6 AF48A03B BFD25E8C D0364141', 1)) tuple[int, int][source]

Elliptic curve scalar multiplication.

btclib.curves.multi_mult_var(scalars: Sequence[bytes | str | bytearray | memoryview | int], points: Sequence[tuple[int, int]], ec: Curve = Curve('FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE FFFFFC2F', 0, 7, ('79BE667E F9DCBBAC 55A06295 CE870B07 029BFCDB 2DCE28D9 59F2815B 16F81798', '483ADA77 26A3C465 5DA4FBFC 0E1108A8 FD17B448 A6855419 9C47D08F FB10D4B8'), 'FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE BAAEDCE6 AF48A03B BFD25E8C D0364141', 1)) tuple[int, int][source]

Return the multi scalar multiplication u1*Q1 + … + un*Qn.

Interleaved wNAF on few scalars, Bos-Coster on many: curve_group’s _multi_mult_var dispatches on the count, at the size the two measure the same. On secp256k1 the bindings serve the whole sum instead.

ssa’s batch verification is what hands many scalars over at once, libsecp256k1 exposing no batch verification of its own, and it is the Python arm of that sum which arrives here: the delegated arm reaches _libsecp256k1_multi_mult_ below with its terms as octets, this signature taking points and a point being what its terms would have to be lifted into for the multiplication to write them straight back out.

btclib.curves.point_from_octets(pub_key: bytes | str | bytearray | memoryview, ec: Curve = Curve('FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE FFFFFC2F', 0, 7, ('79BE667E F9DCBBAC 55A06295 CE870B07 029BFCDB 2DCE28D9 59F2815B 16F81798', '483ADA77 26A3C465 5DA4FBFC 0E1108A8 FD17B448 A6855419 9C47D08F FB10D4B8'), 'FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE BAAEDCE6 AF48A03B BFD25E8C D0364141', 1), *, hybrid: bool = False) tuple[int, int][source]

Return a tuple (x_Q, y_Q) that belongs to the curve.

Return a tuple (x_Q, y_Q) that belongs to the curve according to SEC 1 v.2, section 2.3.4.

The compressed prefixes are the only branch libsecp256k1 serves, and the whole cost of the function is there: lifting x to a point is a modular square root, nearly all of that cost on either arm, and delegated it costs a small fraction of what the Python one does, while the 65-byte forms carry the y and cost the same either way (issue 284).

hybrid admits the 0x06 and 0x07 prefixes of that same section, which carry both coordinates like 0x04 does and repeat the parity of y in the prefix. It is off by default, and not out of squeamishness: the point is a point, and libsecp256k1’s ec_pubkey_parse takes all three 65-byte prefixes (eckey_impl.h). What decides is where the parsed key goes next – addresses, WIF and the descriptor language have no hybrid form to render, and nothing in bitcoin produces one. Consensus has to accept what was mined instead: Core rejects hybrid keys only under STRICTENC, so the script engine is the one caller that asks for them (issue #129). It is refused rather than read for its truth: its True is the permissive value, and a non-bool is true, so hybrid=”no” would parse the very prefixes it was written down to keep out.

btclib.curves.scalar_from_prv_key(prv_key: bytes | str | bytearray | memoryview | int, ec: Curve = Curve('FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE FFFFFC2F', 0, 7, ('79BE667E F9DCBBAC 55A06295 CE870B07 029BFCDB 2DCE28D9 59F2815B 16F81798', '483ADA77 26A3C465 5DA4FBFC 0E1108A8 FD17B448 A6855419 9C47D08F FB10D4B8'), 'FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE BAAEDCE6 AF48A03B BFD25E8C D0364141', 1)) int[source]

Return a verified-as-valid private key integer.

Here rather than in a converter, and beside bytes_from_prv_key_int for its reason: a scalar in 1..n-1 is a fact about the curve and nothing above it knows more about it than this file does.

Integer and not a PrvKey of this module’s own, which would be that same union of types under a second name and so nothing mypy could check; the parameter carries the role the way mult’s m_int does. The spellings this takes are narrower than the ones Integer names – int_from_integer reads “0xc0ffee” and a short hex string, where a key is n_size octets or nothing. What rules those out is reading them with bytes_from_octets and the size handed to it, rather than with int_from_integer; the annotation could not, the two unions being one. A WIF and an extended key are not among the spellings – they are b58’s and bip32’s objects, and turning one into a scalar is a call a caller makes rather than a spelling this layer guesses at (issue #1188).

The range is checked in Python for every curve. keys.prvkey_verify is libsecp256k1’s answer to the same question and is not called for want of anything to gain: a comparison on a value that is already a Python int, with no constant-time argument to pay for the call with, since whether a key is in range is precisely what the caller is being told. to_prv_key.int_from_prv_key carries the measurement behind that, and carries it until issue #1188’s last step removes it.

btclib.curves.set_libsecp256k1_serving(*, serving: bool) None[source]

Ask for the bindings, or for the Python arithmetic, from here on.

Process-wide and immediate: every dispatch in the package asks _libsecp256k1_serves, and that predicate reads the global this assigns, so nothing has to be re-imported and no module keeps an answer of its own.

serving=True with the bindings not installed is a request that cannot be served, and is refused rather than silently ignored: a caller that asked for C and got Python would be timing Python and calling it C. is_libsecp256k1_serving is how the answer is read back.

The environment variable is the other way in, and the earlier one: BTCLIB_NO_LIBSECP256K1 set to a non-empty value makes the initial state False, which is what a test runner wants – it settles before the first import, where this function cannot.