ecc package¶
Subpackages¶
- ecc.curves package
- Submodules
- ecc.curves.curve module
- ecc.curves.curve_group module
CurveGroupCurveGroup.add_aff_var()CurveGroup.add_jac()CurveGroup.add_jac_aff()CurveGroup.add_var()CurveGroup.aff_from_jac_batch_var()CurveGroup.aff_from_jac_var()CurveGroup.double_aff_var()CurveGroup.double_jac()CurveGroup.is_jac_equal()CurveGroup.is_on_curve()CurveGroup.negate()CurveGroup.negate_jac()CurveGroup.require_on_curve()CurveGroup.x_aff_from_jac_var()CurveGroup.y_aff_from_jac_var()CurveGroup.y_even_var()CurveGroup.y_low_var()CurveGroup.y_quadratic_residue_var()CurveGroup.y_var()
signed_odd_digits()
- ecc.curves.curve_group_2 module
- ecc.curves.curve_group_f module
- ecc.curves.sec_point module
- Module contents
CurveCurveGroupCurveGroup.add_aff_var()CurveGroup.add_jac()CurveGroup.add_jac_aff()CurveGroup.add_var()CurveGroup.aff_from_jac_batch_var()CurveGroup.aff_from_jac_var()CurveGroup.double_aff_var()CurveGroup.double_jac()CurveGroup.is_jac_equal()CurveGroup.is_on_curve()CurveGroup.negate()CurveGroup.negate_jac()CurveGroup.require_on_curve()CurveGroup.x_aff_from_jac_var()CurveGroup.y_aff_from_jac_var()CurveGroup.y_even_var()CurveGroup.y_low_var()CurveGroup.y_quadratic_residue_var()CurveGroup.y_var()
PreparedPointPubKeyTweakChainTweakChainadd()bytes_from_point()bytes_from_prv_key_int()double_mult_var()find_all_points()find_subgroup_points()is_libsecp256k1_serving()is_x_coordinate_var()mult()mult_pub_key()multi_mult_var()point_from_octets_var()point_from_pub_key()prv_key_tweak_add()scalar_from_prv_key()set_libsecp256k1_serving()sum_var()tweak_add_var()
Submodules¶
ecc.alias module¶
The type aliases of the public API, and the input conventions they name.
Octets and String below are the same union, so mypy cannot tell one from the other: passing a text string where a hex-string is expected is a type error this file names but no checker can catch. The distinction is enforced at run time instead, by the converter each function calls on its way in – bytes_from_octets for Octets, str_from_string for String – and it is documented here because that is the only place it can be read as one piece.
Making Octets and String NewTypes would let mypy separate them, at the cost of every caller having to wrap its literals: Octets(“deadbeef”) instead of “deadbeef”, throughout a public API whose whole style is to accept whatever is convertible. For those two that is a different package, not a fix to this one. Public below is a NewType because there the wrap is the point: it is where a caller declares a value public.
- class ecc.alias.HashObject(*args, **kwargs)[source]¶
Bases:
ProtocolThe slice of a hashlib object this package reads, as a Protocol.
Structural: anything hashlib.new returns satisfies it, and the members carry hashlib’s own meanings.
- copy() HashObject[source]¶
Return a clone that can absorb independently.
- ecc.alias.Octets = bytes | str | bytearray | memoryview¶
Bytes, or the hex-string that decodes to them, wherever raw bytes are asked for.
ecc.bip340_nonce module¶
Generation of the ephemeral key (nonce) following BIP340.
https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki
BIP340 derives the nonce from the private key, the public key, and the message, all behind a tagged hash, plus auxiliary randomness a:
nonce = TaggedHash(‘BIP0340/nonce’, t||x_Q||msg) with t = q xor TaggedHash(‘BIP0340/aux’, a)
Where:
TaggedHash(tag, x) = SHA256(SHA256(tag)||SHA256(tag)||x)
This is the synthetic nonce: the deterministic derivation is the security floor – with no randomness at signing time (a counter as a, even all zeros) a nonce still cannot repeat across different messages – and fresh randomness is the hardening BIP340 recommends on top, against fault injection and side-channel attacks. The key is masked with the hashed randomness by xor, rather than hashed together with it, to keep the number of operations touching the actual secret low. The dedicated tag is domain separation: RFC6979 is not reused because sharing a derivation (and a key) with deterministic ECDSA could leak the key through nonce reuse across the two schemes. Any deterministic derivation, BIP340 warns, remains insecure in multi-party signing.
- ecc.bip340_nonce.bip340_nonce_(msg: bytes | str | bytearray | memoryview, prv_key: bytes | str | bytearray | memoryview | int, aux: bytes | str | bytearray | memoryview | 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), hf: Callable[[], ~ecc.alias.HashObject]=<built-in function openssl_sha256>) tuple[int, int, int, int][source]¶
Return a BIP340 ephemeral key (nonce), synthetic by default.
The message is of any size: BIP340 puts no size restriction on it, and the nonce tagged hash absorbs any length just as the challenge does.
A hash function shorter than the curve order gets as many tagged hashes as make at least nlen + 64 bits, reduced into 1..n-1: one digest would leave every nonce shorter than n. Its aux mask is stretched the same way to the size of n, so that it covers all of the key.
ecc.borromean module¶
Borromean ring signature functions.
References
https://github.com/ElementsProject/borromean-signatures-writeup
https://github.com/Blockstream/borromean_paper/blob/master/borromean_draft_0.01_9ade1e49.pdf
Both are also cited inside sign, which is not where a reader looks first; they are here because the module is what one arrives at.
_hash’s challenge preimage matches secp256k1-zkp’s rangeproof module – secp256k1_borromean_hash, src/modules/rangeproof/borromean_impl.h – Elements and Confidential Transactions among its callers: e || m || ring || pos, the point or e0 bytes first, then the message hash, then the ring index and the position each as 4 bytes big-endian. Signatures btclib-org/btclib’s borromean module produced before the fix of issue btclib-org/btclib#1070, which found the message and the point swapped, do not verify here and never will – there is no version byte in the wire format to switch on, sign and assert_as_valid compute this hash the same way every time, and btclib-org/btclib’s RELEASE_NOTES.md has the “before” and “after” this cost in its breaking-changes list.
e0 is hf(r_0 || … || r_last || m), the order secp256k1_borromean_sign writes its own sha256_e0 in: each ring’s last point as the ring loop closes that ring, the message once the loop ends, and secp256k1_borromean_verify_impl recomputes it the same way (issue btclib-org/btclib#1940). No ring a verifier is handed reaches it – e0 is written into the signature before any verifier sees it – so a signature carrying an e0 over any other order is refused here and there alike, and btclib-org/btclib’s RELEASE_NOTES.md has what settling on this one cost in its breaking-changes list.
_get_msg_format was checked against zkp too, in the same issue, and has nothing to align with: zkp’s rangeproof never hashes a caller’s message together with caller-supplied pubkey rings the way this function does, because it reconstructs its rings from a value commitment and hashes that commitment, the generator point and the proof’s own header bytes instead. There is no zkp construction here to diverge from or to match, so this one is untouched.
BorromeanSig.serialize follows zkp’s rangeproof module’s own layout for this signature: e0 followed by every s, ec.n_size bytes each, ring-major, no other framing – generalizing zkp’s hardcoded 32 to ec.n_size (issue btclib-org/btclib#183) rather than contradicting it, the two agreeing wherever ec is secp256k1, zkp’s only curve. BorromeanSig.parse reads that layout back at secp256k1 and sha256 always, as ssa.Sig.parse reads BIP340’s one curve: the serialization does not name either, so a BorromeanSig on another curve or hash function is built directly rather than parsed.
This module and zkp’s rangeproof therefore agree over secp256k1 with
sha256 on both challenge preimages and on the wire layout of the ring
signature itself – the primitive, as against the rangeproof built on
top of it. That secp256k1_borromean_verify then takes what sign_
writes is measured rather than asserted:
zkp.rangeproof.borromean_verify wraps it over serialized arguments in
the flagged zkp extension, and tests/schemes/borromean_test.py puts a
signature to it under a ring and under that same ring negated key by
key, over one ring of one key and over several rings alike. The negated
ring is the one it takes.
Negating the ring is what a crossing signature asks for, and what it answers is the public key of a ring being the other one of the pair: this module recovers a ring point as s*G - e*Q and gives the real signer s = k + q*e, where secp256k1_borromean_verify_impl recovers s*G + e*P and secp256k1_borromean_sign gives s = k - sec*e, so what one calls P the other calls -P, and only a ring negated key by key meets the equation that closes it (issue btclib-org/btclib#1895).
Verifying is the only direction in reach at all. secp256k1_borromean_sign is declared static in src/modules/rangeproof/borromean.h, an internal header with no counterpart under zkp’s include/, and static is internal linkage, so no cffi cdef binds it. Reaching the verifier takes a fork: the secp256k1-zkp submodule of the bindings is fametrano/secp256k1-zkp, which declares a wrapper of that name in include/secp256k1_rangeproof.h and has it proposed upstream as BlockstreamResearch/secp256k1-zkp#373, unmerged there.
Agreeing on the primitive is not the same as producing a Confidential Transactions rangeproof: rangeproof_impl.h wraps this signature in a digit decomposition, a value commitment and an exponent/mantissa/min-value/sign-bits header this module has no counterpart for, rebuilding its pubkey rings from that commitment where this module takes them as an explicit argument.
ecc.rangeproof is that wrapping. RangeProof.parse takes the flags, the exponent, the mantissa, the min_value, the sign bits and the ring commitments of such a header, and holds the e0 and the s values inside the proof as a BorromeanSig of this module’s, and sign writes one – the digit decomposition, the ring commitments and the header included.
It closes those rings on its own rather than through this module, and
ecc.rangeproof._borromean_sign is where the differences are stated:
the ring convention above, the forged s values taken from the proof’s
own nonce chain where sign_ draws them, and, in
ecc.rangeproof._challenge, a challenge at or past n abandoning the
proof where this module reduces it. What the rings are signed over is
a rangeproof’s own: the value commitment, the generator, the proof’s
header and the ring commitments it states, with no pubkey ring in the
preimage, where _get_msg_format below hashes the caller’s rings into
the caller’s message.
ecc.rangeproof’s own _borromean_verify is the verifying walk. Separating it from assert_as_valid below are the ring convention, the challenge at or past n, that preimage, and the challenges themselves: a rangeproof rewind consumes the per-key challenges the walk produces, where verify below answers a bool. Its own docstring has the refusals it makes before the walk goes on from a key.
zkp.rangeproof.sign and zkp.rangeproof.verify are wrapped, and the proof they write and read carries this signature inside it – secp256k1_rangeproof_sign_impl and secp256k1_rangeproof_verify_impl call secp256k1_borromean_sign and secp256k1_borromean_verify_impl, which is what the wrapped secp256k1_borromean_verify delegates to.
- class ecc.borromean.BorromeanSig(e0: bytes, s: Sequence[list[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), *, check_validity: bool = True)[source]¶
Bases:
objectA borromean ring signature: e0 and one s per ring member.
s is one tuple of scalars per ring, in the ring’s own order: s[i][j] is the value for pubk_rings[i][j] wherever this signature is later handed to verify or assert_as_valid along with the pubkey rings argument. e0 is the hash that pins where every ring starts, an hf digest and not a curve value – its width is hf().digest_size, not ec.n_size, so a BorromeanSig does not say by itself which hash function produced it, the same way it does not carry hf as a field: sign, verify and assert_as_valid all take hf as their own argument, as ssa.sign/verify take theirs.
serialize/parse follow secp256k1-zkp’s rangeproof module’s own layout for this signature – the module docstring has what that alignment covers, and what about it is asserted rather than checked.
- classmethod parse(data: BytesIO | bytes | str | bytearray | memoryview, rsizes: Sequence[int] = (), *, check_validity: bool = True) BorromeanSig[source]¶
Build a BorromeanSig from e0 || s, secp256k1 and sha256 – zkp’s own.
The serialization does not name its curve or its hash function, the same reason ssa.Sig.parse reads BIP340’s alone: e0 is a 32-byte sha256 digest and each s is secp256k1’s 32-byte scalar, which are the widths zkp’s own layout fixes. A BorromeanSig on another curve or another hash function is built directly, as ssa.Sig.parse’s docstring says for BIP340’s one curve.
rsizes is how many scalars each ring holds – the ring sizes a verifier’s own pubk_rings argument already carries – because the wire format has no framing of its own to recover them from: zkp’s secp256k1_borromean_verify takes rsizes as a caller-supplied argument for the same reason, rather than reading it out of the proof. It defaults to () only so a wrong-typed data is refused ahead of it, the same as every other parse’s own extra argument in tests/serialization_boundary_test.py’s table – not because a signature with no rings is a value worth building: assert_valid refuses one, so a caller who leaves rsizes out and hands over real octets is refused too, either there or by the trailing bytes those octets still carry.
- serialize(*, check_validity: bool = True) bytes[source]¶
Return e0 followed by every s, ec.n_size bytes each, ring-major.
secp256k1-zkp’s own layout for this signature: e0, then one secp256k1_scalar_get_b32 per public key in ring order. ec.n_size generalizes zkp’s hardcoded 32 (issue btclib-org/btclib#183) rather than contradicting it – the two agree wherever ec is secp256k1, zkp’s only curve.
- ecc.borromean.assert_as_valid(msg: bytes | str | bytearray | memoryview, sig: ~ecc.borromean.BorromeanSig | bytes | str | bytearray | memoryview, pubk_rings: ~collections.abc.Sequence[~collections.abc.Sequence[tuple[int, int]]], ec: ~ecc.curves.curve.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), hf: ~collections.abc.Callable[[], ~ecc.alias.HashObject] = <built-in function openssl_sha256>) None[source]¶
Refuse an invalid borromean ring signature.
The rings are walked forward from sig.e0 and must close on the e0 they started from; errors carry the reason and which ring and position it happened at (BorromeanRingError), verify being the boolean answer. A sig whose ring shape disagrees with pubk_rings – a different number of rings, or a ring with a different number of s-values than it has keys – is refused first, as a plain EccValueError naming the ring and the counts: two arguments that do not describe the same object is not a check that ran and failed, so it is not a BorromeanRingError (issue btclib-org/btclib#1088). A sig carrying a ring with no keys is refused too, by sig.assert_valid below rather than here – it proves nothing about any key regardless of what pubk_rings says, so BorromeanSig’s own invariant is what refuses it (issue btclib-org/btclib#1094).
sig as octets is parsed with BorromeanSig.parse, which reads secp256k1 and sha256 always – ec and hf are then not what decides the curve or the hash, the same limit ssa.verify has for a Sig | Octets argument and for the same reason: the wire format does not name either. They still decide what a BorromeanSig argument is checked against if it says otherwise: ec only for its type (sig.ec is what is actually used, below), hf for real, to recompute the challenge of a signature sign made with another one.
- ecc.borromean.sign(msg: bytes | str | bytearray | memoryview, ks: ~collections.abc.Sequence[bytes | str | bytearray | memoryview | int], sign_key_idx: ~collections.abc.Sequence[int], sign_keys: ~collections.abc.Sequence[bytes | str | bytearray | memoryview | int], pubk_rings: ~collections.abc.Sequence[~collections.abc.Sequence[tuple[int, int]]], ec: ~ecc.curves.curve.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), hf: ~collections.abc.Callable[[], ~ecc.alias.HashObject] = <built-in function openssl_sha256>) BorromeanSig[source]¶
Sign msg with a borromean ring signature, one key per ring.
https://github.com/ElementsProject/borromean-signatures-writeup https://github.com/Blockstream/borromean_paper/blob/master/borromean_draft_0.01_9ade1e49.pdf
ks is one nonce per ring, sign_key_idx the position of the real key in each ring, sign_keys the real private key of each ring – sign_keys[i] signs at pubk_rings[i][sign_key_idx[i]] – and pubk_rings the full public rings, real key included.
ks and sign_keys are scalars, spelled as Integer the way dsa and ssa spell one, and each is read through curves.scalar_from_prv_key: in 1..n-1, or refused (issue btclib-org/btclib#1243). sign_key_idx is not one of them and stays int – it indexes a ring, and an index is not a scalar written in hex. sign_key_idx[i] must be a valid index into pubk_rings[i], refused with EccValueError naming the ring, the index and the ring’s size otherwise – a ring with no keys has none, whatever the index (issue btclib-org/btclib#1094), and a value the ring’s size does not reach either (issue btclib-org/btclib#1095).
msg and pubk_rings are reduced here to the one hash the ring walk binds, _get_msg_format’s;
sign_is the spelling that takes that hash instead, for a caller whose own preimage has another shape.A BorromeanSig, because that is what it is: the result verifies with assert_as_valid/verify, serializes with BorromeanSig.serialize, and handing back a bare (bytes, SValues) tuple would only make the caller build one to do either.
- ecc.borromean.sign_(msg_hash: bytes | str | bytearray | memoryview, ks: ~collections.abc.Sequence[bytes | str | bytearray | memoryview | int], sign_key_idx: ~collections.abc.Sequence[int], sign_keys: ~collections.abc.Sequence[bytes | str | bytearray | memoryview | int], pubk_rings: ~collections.abc.Sequence[~collections.abc.Sequence[tuple[int, int]]], ec: ~ecc.curves.curve.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), hf: ~collections.abc.Callable[[], ~ecc.alias.HashObject] = <built-in function openssl_sha256>) BorromeanSig[source]¶
Sign a prepared message hash, one key per ring.
msg_hash is the value every position of the ring walk hashes, hf().digest_size octets of it; sign is the spelling that builds one from msg and pubk_rings with _get_msg_format.
Binding the rings into that hash is the caller’s, and it is what a borromean signature proves nothing without: zkp says so of the same argument – “Message must contain pubkeys or a pubkey commitment”, the header comment on secp256k1_borromean_verify_impl – because a preimage that does not reach pubk_rings leaves the ring free to be swapped for another one under the signature made over it.
A rangeproof is where a preimage of that shape comes from: it is the value commitment, the generator and the proof’s own header, with no pubkey ring in it at all, and ecc.rangeproof rebuilds the rings from that commitment instead. It closes its own rings rather than calling this, for the reasons ecc.rangeproof._borromean_sign gives; what stands between a signature written here and one secp256k1_borromean_verify takes is the module docstring above.
pubk_rings is an argument here as it is in sign: the walk multiplies by those points, and a hash cannot give them back. So what the trailing underscore offers is the preimage the caller has already built, never a ring it has already checked – every key of every ring is read as a point of ec before anything is signed.
Every other argument is sign’s, and is read the way sign reads it.
- ecc.borromean.verify(msg: bytes | str | bytearray | memoryview, sig: ~ecc.borromean.BorromeanSig | bytes | str | bytearray | memoryview, pubk_rings: ~collections.abc.Sequence[~collections.abc.Sequence[tuple[int, int]]], ec: ~ecc.curves.curve.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), hf: ~collections.abc.Callable[[], ~ecc.alias.HashObject] = <built-in function openssl_sha256>) bool[source]¶
Return whether sig is a valid borromean ring signature of msg.
Raises where sig is octets that are no signature over pubk_rings, and answers False for a signature that is well formed and merely does not verify. See _assert_structurally_valid_.
sig is a BorromeanSig, or its serialize`d octets – parsed with `BorromeanSig.parse, secp256k1 and sha256 always, as ssa.verify parses a Sig | Octets over BIP340’s one curve. A BorromeanSig argument keeps its own ec, which need not be the one this function defaults to; assert_as_valid’s docstring has what ec and hf are for either way.
ecc.commit_nonce module¶
Commitment-tweaked ephemeral key (nonce): sign-to-contract.
A signature commits to a value by tweaking its nonce with it, at no cost in size: what comes out is an ordinary DSA or SSA signature, and only whoever is shown the committed value and the receipt can tell that it commits to anything.
Let commit_hash be the commitment value and R a curve point, then
e = hash(R||commit_hash)
is a commitment operation.
When signing, an ephemeral secret key k is generated and its corresponding curve point R = kG is used. Here, instead of using (k, R), compute the commitment to commit_hash
e = hash(R||commit_hash),
tweak k with e and consequently substitute R with W = (k+e)G = R+eG, then proceed signing in the standard way, using (k+e, W).
When the committer/signer will reveal R and commit_hash, the verifier will check that
W.x = (R+eG).x
with e = hash(R||commit_hash)) and W.x being known from the signature.
R is the receipt, and it is the signer’s to keep: nothing in the signature says what it was. dsa.sign and ssa.sign take the commitment as a parameter and return the receipt beside the signature, dsa.verify and ssa.verify take the two back to open the commitment, and this module is the tweak behind all four.
The committed value must also reach the nonce derivation, which is the half of the scheme that is easy to leave out and fatal to leave out. Tweaking alone leaves the untweaked k a function of the message and the key only, so two signatures over one message with two commitments have nonces differing by e2-e1 – a value the openings make public. Two ECDSA signatures over one message with a known nonce difference are two equations in the two unknowns k and the private key, and the same holds for BIP340. secp256k1-zkp states it in secp256k1_ecdsa_sign_inner (src/secp256k1.c) as the reason a sign-to-contract commitment works only with the default nonce function: “an attacker can exfiltrate the secret key by signing the same message thrice with different commitments”. So commit_entropy_ is not optional garnish, and the two schemes each feed it to their nonce derivation before calling commit_nonce_: dsa through RFC6979’s section 3.6 additional data, ssa through BIP340’s auxiliary randomness.
Both hashes are tagged, and the tags are the scheme’s: they are what keeps a tweak from being read as a challenge, or an ECDSA commitment as a BIP340 one. Each caller passes its own, because they differ per scheme and a default would be the wrong one for somebody.
- ecc.commit_nonce.commit_entropy_(commit_hash: bytes | str | bytearray | memoryview, tag: bytes, hf: Callable[[], ~ecc.alias.HashObject]=<built-in function openssl_sha256>) bytes[source]¶
Return the committed value as entropy for a nonce derivation.
Hashed, and not passed on as it is, so that a nonce can be derived by someone who knows a hash of the value and not the value itself: that is what lets a signing device commit to a host’s randomness before the host reveals it, which is the ordering the anti-exfil protocol is built on.
- ecc.commit_nonce.commit_nonce_(commit_hash: bytes | str | bytearray | memoryview, nonce: bytes | str | bytearray | memoryview | int, tag: bytes, 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), hf: Callable[[], ~ecc.alias.HashObject]=<built-in function openssl_sha256>) tuple[int, tuple[int, int]][source]¶
Return the commitment-tweaked nonce, and the receipt to reveal.
The receipt is the point of the nonce as it came in: it is what the tweak hashes, so a verifier given the committed value can recompute the tweak and reach the point the signature carries.
- ecc.commit_nonce.commit_point_var_(commit_hash: bytes | str | bytearray | memoryview, receipt: tuple[int, int], tag: bytes, ec: ~ecc.curves.curve.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), hf: ~collections.abc.Callable[[], ~ecc.alias.HashObject] = <built-in function openssl_sha256>) tuple[int, int][source]¶
Return W = R + hash(R||commit_hash)G, the point the nonce became.
Its x-coordinate is what the signature’s r was built from – dsa reduces that coordinate modulo the group order and ssa keeps the field element – so the comparison against r belongs to each scheme and not here.
ecc.dahlias module¶
DahLIAS fully aggregated signatures, according to BIP459.
https://github.com/bitcoin/bips/blob/master/bip-0459.mediawiki
Each of u signers signs its own 32-byte message under its own x-only key, and the signers jointly produce one 64-byte signature for the ordered list of (public key, message) pairs. Unlike ecc.musig2, no key is aggregated: verify takes the whole list, and the signature verifies with nothing else. It does not verify with ecc.ssa.
Signing is interactive, so this module offers one function per step:
round 1: nonce_gen for each signer, then nonce_agg_var over the public nonces;
round 2: sign for each signer, over a SessionContext holding the aggregate nonce and the ordered lists of keys, messages and public nonces; partial_sig_verify to find a signer whose partial signature is wrong; sig_agg for the aggregate signature.
A secret nonce signs once. Partial signatures from three sessions signed with one secnonce reveal the secret key. sign zeroes the bytearray it is given as soon as it has read it.
sign checks only the signer’s own entry: its R_2 appears exactly once among the public nonces, and the key and message at that position are its own. Whatever the caller requires of the other entries is the caller’s to check before calling it.
A signer may use a tweaked key pair: plain_tweak (BIP32 unhardened derivation) and xonly_tweak (BIP341 key path) answer the tweaked secret key and the x-only key that goes in the list, and plain_tweak_pub_key_var and xonly_tweak_pub_key_var answer the tweaked key from the public key alone.
secp256k1 and sha256 are not parameters, as in ecc.musig2: BIP459 is defined for that pair alone.
- class ecc.dahlias.SessionContext(agg_nonce: bytes | str | bytearray | memoryview, pub_keys: Sequence[bytes | str | bytearray | memoryview], msgs: Sequence[bytes | str | bytearray | memoryview], pub_nonces: Sequence[bytes | str | bytearray | memoryview])[source]¶
Bases:
objectWhat the signers of one session agree on.
The aggregate nonce, and the ordered lists of x-only public keys, 32-byte messages and public nonces, all of one length u >= 1. BIP459 calls these the session parameters.
- class ecc.dahlias.SessionValues(R: tuple[int, int], b: int)[source]¶
Bases:
objectBIP459’s session values: the nonce point R and its coefficient b.
- ecc.dahlias.assert_as_valid(pub_keys: Sequence[bytes | str | bytearray | memoryview], msgs: Sequence[bytes | str | bytearray | memoryview], sig: bytes | str | bytearray | memoryview) None[source]¶
Refuse an aggregate signature that does not verify: BIP459’s Verify.
verify is the spelling that answers a bool.
- ecc.dahlias.nonce_agg_var(pub_nonces: Sequence[bytes | str | bytearray | memoryview]) bytes[source]¶
Return the 66-byte aggregate of the public nonces: BIP459’s NonceAgg.
Either half summing to infinity is refused: BIP459 has no encoding for it.
- ecc.dahlias.nonce_gen(prv_key: bytes | str | bytearray | memoryview | int | None = None, extra_in: bytes | str | bytearray | memoryview | None = None) tuple[bytearray, bytes][source]¶
Return the (secnonce, pubnonce) pair of one signer.
Fresh randomness is drawn here;
nonce_gen_takes it instead.
- ecc.dahlias.nonce_gen_(rand_: bytes | str | bytearray | memoryview, prv_key: bytes | str | bytearray | memoryview | int | None = None, extra_in: bytes | str | bytearray | memoryview | None = None) tuple[bytearray, bytes][source]¶
Return the (secnonce, pubnonce) pair of one signer, given
rand_.BIP459’s NonceGen, with its fresh randomness as the argument; nonce_gen is the spelling that draws it, and the one to call.
The private key and
extra_inare optional defences against a broken random generator; pass them where known.The secnonce is a 64-byte bytearray, which sign zeroes.
- ecc.dahlias.partial_sig_verify(psig: bytes | str | bytearray | memoryview, pub_keys: Sequence[bytes | str | bytearray | memoryview], msgs: Sequence[bytes | str | bytearray | memoryview], pub_nonces: Sequence[bytes | str | bytearray | memoryview], i: int) bool[source]¶
Verify the partial signature of signer i: BIP459’s PartialSigVerify.
The aggregate nonce is computed here from the public nonces, so the coordinator cannot influence the answer.
Raises for an input of the wrong size or type. Every failure BIP459 lists is False, lists of different or zero length and an index out of range included: a list declares no size.
- ecc.dahlias.plain_tweak(prv_key: bytes | str | bytearray | memoryview | int, tweak: bytes | str | bytearray | memoryview) tuple[int, bytes][source]¶
Return the plainly tweaked secret key and its 32-byte x-only key.
BIP459’s plain tweaking, as in BIP32 unhardened derivation: sk’ = sk + t. The x-only key is what goes in the list of public keys.
- ecc.dahlias.plain_tweak_pub_key_var(pub_key: bytes | str | bytearray | memoryview, tweak: bytes | str | bytearray | memoryview) bytes[source]¶
Return P + t*G for a 33-byte compressed P, as 33 bytes.
The public half of plain_tweak. The parity is kept so that tweaks can be chained; the list of public keys takes the last 32 bytes.
- ecc.dahlias.session_values(session_ctx: SessionContext) SessionValues[source]¶
Return (R, b) for the session: BIP459’s GetSessionValues.
Memoized on session_ctx. Nothing in it is secret.
- ecc.dahlias.sig_agg(psigs: Sequence[bytes | str | bytearray | memoryview], session_ctx: SessionContext) bytes[source]¶
Return the 64-byte aggregate signature: BIP459’s SigAgg.
- ecc.dahlias.sign(sec_nonce: bytearray, prv_key: bytes | str | bytearray | memoryview | int, msg: bytes | str | bytearray | memoryview, session_ctx: SessionContext) bytes[source]¶
Return the 32-byte partial signature of one signer: BIP459’s Sign.
The secnonce’s type and size and the message’s size are checked, then the session values are derived, so a session that does not assemble leaves the secnonce untouched. Then the secnonce is read and zeroed, before its values, the key and the lists are checked: calling this twice with one bytearray raises instead of signing twice.
The signer’s R_2 must appear exactly once among the public nonces, and the key and message at that index must be its own.
- ecc.dahlias.verify(pub_keys: Sequence[bytes | str | bytearray | memoryview], msgs: Sequence[bytes | str | bytearray | memoryview], sig: bytes | str | bytearray | memoryview) bool[source]¶
Verify an aggregate signature over the ordered (key, message) list.
BIP459’s Verify. Raises for an input of the wrong size or type; answers False for anything else that does not verify, a key or an r that does not lift and lists of different or zero length included: a list declares no size. assert_as_valid says why.
- ecc.dahlias.xonly_tweak(prv_key: bytes | str | bytearray | memoryview | int, tweak: bytes | str | bytearray | memoryview) tuple[int, bytes][source]¶
Return the x-only tweaked secret key and its 32-byte x-only key.
BIP459’s x-only tweaking, as in BIP341 key path spending: the secret key is negated where its point has odd y, then sk’ = sk + t.
- ecc.dahlias.xonly_tweak_pub_key_var(pub_key: bytes | str | bytearray | memoryview, tweak: bytes | str | bytearray | memoryview) bytes[source]¶
Return lift_x(P) + t*G for a 32-byte x-only P, as 33 bytes.
The public half of xonly_tweak. The parity is the one BIP341’s control block commits to; the list of public keys takes the last 32 bytes.
ecc.dh module¶
Diffie-Hellman elliptic curve key agreement, per SEC 1 v.2.
Two parties, each holding the other’s public key, compute the same shared secret – their key pair times the other’s public point – and derive symmetric keying data from it through a key derivation function. The curve and the KDF are the two things the parties must agree on beforehand; SEC 1’s KDF is kdf.ansi_x9_63_kdf, which ecc.kdf holds beside RFC 5869’s, and diffie_hellman is the agreement built on it.
Why `ecdh.shared_secret` of the bindings has no caller here, and this is the place that says so (issue btclib-org/btclib#909). That function multiplies and hashes in one call, and the hash is SHA256 of the compressed shared point with no way to change it: libsecp256k1 takes it as a C callback, so exposing it would mean calling back into python from the middle of the computation. Each computation listed below derives differently, so what is delegated is the multiplication – ecdh.shared_point, the same secp256k1_ecdh call answering the point instead of its hash, and so the same secp256k1_ecmult_const, constant time in the scalar. The derivation stays in python:
diffie_hellman below runs SEC 1’s ANSI-X9.63-KDF over the x-coordinate, under the hash function the caller passed;
ecc.ecies.derive_keys hashes the compressed point with sha512 and cuts the 64 bytes three ways, which is BIE1’s shape and not this one.
So the verdict is not that the function is wrong: it is that a shared secret is a protocol’s own derivation, and only a protocol agreeing with libsecp256k1’s default can hand the whole of it over. Neither of the two here does.
ecc.ellswift.xdh hands its whole derivation over, through another function: libsecp256k1 ships BIP324’s hash with secp256k1_ellswift_xdh, and ellswift.xdh of the bindings is that one call.
- ecc.dh.diffie_hellman(dU: bytes | str | bytearray | memoryview | int, QV: tuple[int, int], size: int, shared_info: bytes | None = None, ec: ~ecc.curves.curve.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), hf: ~collections.abc.Callable[[], ~ecc.alias.HashObject] = <built-in function openssl_sha256>) bytes[source]¶
Diffie-Hellman elliptic curve key agreement scheme.
http://www.secg.org/sec1-v2.pdf, section 6.1
The shared point is a point that is not the generator multiplied by a secret, and on secp256k1 ecdh.shared_point of the bindings computes it here: secp256k1_ecdh, whose secp256k1_ecmult_const is constant time in dU, at a fraction of what the Python endomorphism path costs.
ecdh.shared_secret of the bindings is a different function and not a substitute: it hashes the compressed shared point with SHA256, where this derives through ANSI-X9.63-KDF. The module docstring above has that verdict for both computations it lists.
Cofactor Diffie-Hellman (SEC 1 v.2, section 3.3.2) on a curve whose cofactor is above 1: QV is multiplied by the cofactor before dU multiplies the product, so a component of QV outside ⟨G⟩ – one bytes_from_point still serializes, nothing here confining QV to ⟨G⟩ the way sec_point.point_from_pub_key does – is annihilated by h·QV rather than surviving into the point dU multiplies and leaking dU’s residue modulo that component’s order (issue btclib-org/ecc#15). h == 1 on every curve without a cofactor, where this is QV unchanged.
dU is read through curves.scalar_from_prv_key, which validates it into 1..n-1 before either arithmetic arm sees it, so a bool, a float, a negative int or a value at or above ec.n is refused identically whichever arm ends up serving the call (issue btclib-org/ecc#10). QV is refused the same way on both arms when it is the infinity point: nothing here otherwise confines it to a serializable point, but INF is the one value neither arm can turn into a shared secret, and checking it once ahead of the dispatch is what keeps the two arms agreeing on it.
ecc.dlc_oracle module¶
DLC oracle signatures: BIP340 with a nonce committed in advance.
https://github.com/discreetlogcontracts/dlcspecs/blob/master/Oracle.md
A DLC oracle announces the x-only point R = k*G of a nonce k before an event, and signs the outcome with k once the event happens. The signature is an ordinary BIP340 one, which ssa.verify_ accepts. Before the oracle signs, anyone holding its public key and R computes the signature point s*G of each possible outcome with sig_point_var_, and a DLC encrypts each outcome’s transaction under it.
A nonce signs one message. Two signatures with one k reveal the private key. That is why sign_ lives here and not in ssa, whose signers derive their nonce: a BIP340 signature that is not an oracle’s is ssa.sign_’s.
The message is BIP340’s, of any size, signed as it is. The specification’s tagged hash of an outcome is the caller’s to compute. The curve, secp256k1, and the hash, sha256, are not arguments, as in ecc.ecdsa_adaptor.
Timing. The key and the nonce go through mult and integer products, as in ssa’s Python path; what sig_point_var_ handles is public and uses the _var arithmetic. Python here is not constant-time.
- ecc.dlc_oracle.sig_point_var_(msg: bytes | str | bytearray | memoryview, pub_key: int | bytes | str | bytearray | memoryview | tuple[int, int] | PreparedPoint, pub_nonce: int | bytes | str | bytearray | memoryview | tuple[int, int] | PreparedPoint) tuple[int, int][source]¶
Return s*G for the signature of msg that pub_nonce commits to.
s*G = R + c*P, with P and R the even-y points of pub_key and pub_nonce and c the BIP340 challenge: it needs no secret, and equals s*G for the s that sign_ returns with the nonce of R.
- ecc.dlc_oracle.sign_(msg: bytes | str | bytearray | memoryview, prv_key: bytes | str | bytearray | memoryview | int, nonce: bytes | str | bytearray | memoryview | int) Sig[source]¶
Return the BIP340 signature of msg with the caller’s nonce.
A nonce whose point has odd y is negated, as BIP340 negates its own, so the signature’s r is the x-coordinate of nonce*G either way. The signature is verified before it is returned.
Never sign two messages with one nonce: it reveals the private key.
ecc.dleq module¶
Discrete logarithm equality proofs, according to BIP374.
https://github.com/bitcoin/bips/blob/master/bip-0374.mediawiki
A DLEQ proof is 64 bytes saying that two points share one discrete logarithm: for A = a*G and C = a*B, it proves that the prover knows an a satisfying both, and reveals nothing else about it. The shape is a Schnorr signature – a nonce commitment folded into a challenge, and an s that opens it – computed twice over, once with G as the base and once with B, so that a single s answers for both.
What it attests, and what it does not. The proof says that the same scalar relates A to G and C to B. It says nothing about the value of that scalar, nothing about who holds it, and nothing about whether C is the point either party wanted: a prover who deliberately picks the wrong B proves equivalence over that B and the proof is valid. Its use is the one BIP352 has for it, and the reason BIP374 exists: an ECDH shared secret C computed from the key A that signed an input is provably the right shared secret, so a wrong output script is caught before it is broadcast rather than after the funds are gone. A signature would not catch it – a wrongly derived output script is consensus-valid.
The generator is an argument, which no other BIP in this library makes it: BIP374 passes G in so that the algorithm serves another generator on secp256k1, and the vectors exercise arbitrary generators. What is not an argument is the curve or the hash function, as in ecc.musig2: BIP374 is defined for secp256k1 with sha256, the 33-byte compressed points, the 32-byte scalars and the three tags below are that pair’s serialization, and there is no other pair for which a test vector exists.
The message is optional and, when present, exactly 32 bytes – BIP374’s own restriction, and unlike the arbitrary-size message of ecc.ssa. It binds the proof to a statement of the protocol above, so that a proof of knowledge cannot be replayed as a proof of knowledge and of that statement.
- ecc.dleq.assert_proof_as_valid(A: bytes | str | bytearray | memoryview | tuple[int, int] | PreparedPoint, B: bytes | str | bytearray | memoryview | tuple[int, int] | PreparedPoint, C: bytes | str | bytearray | memoryview | tuple[int, int] | PreparedPoint, proof: bytes | str | bytearray | memoryview, G: bytes | str | bytearray | memoryview | tuple[int, int] | PreparedPoint = (55066263022277343669578718895168534326250603453777594175500187360389116729240, 32670510020758816978083085130507043184471273380659243275938904335757337482424), msg: bytes | str | bytearray | memoryview | None = None) None[source]¶
Raise unless the proof holds for A, B, C under G and msg.
verify_proof is the spelling that answers True or False; this one says why, which is the difference between a proof that does not hold and an argument that is no point or no 64-byte proof at all.
- ecc.dleq.generate_proof(a: bytes | str | bytearray | memoryview | int, B: bytes | str | bytearray | memoryview | tuple[int, int] | PreparedPoint, aux: bytes | str | bytearray | memoryview | None = None, G: bytes | str | bytearray | memoryview | tuple[int, int] | PreparedPoint = (55066263022277343669578718895168534326250603453777594175500187360389116729240, 32670510020758816978083085130507043184471273380659243275938904335757337482424), msg: bytes | str | bytearray | memoryview | None = None) bytes[source]¶
Return the 64-byte DLEQ proof for A = a*G and C = a*B.
aux is BIP374’s auxiliary random data, 32 bytes, and is fresh randomness when it is not given: the same recommendation BIP340 makes for its own, the derivation being deterministic underneath – a counter, or all zeros, still cannot repeat a nonce across two messages – and the randomness the hardening on top of it.
An EccValueError for an a outside 1..n-1 or a B, G that is no public key, which are BIP374’s two “fail” conditions before any arithmetic happens.
- ecc.dleq.verify_proof(A: bytes | str | bytearray | memoryview | tuple[int, int] | PreparedPoint, B: bytes | str | bytearray | memoryview | tuple[int, int] | PreparedPoint, C: bytes | str | bytearray | memoryview | tuple[int, int] | PreparedPoint, proof: bytes | str | bytearray | memoryview, G: bytes | str | bytearray | memoryview | tuple[int, int] | PreparedPoint = (55066263022277343669578718895168534326250603453777594175500187360389116729240, 32670510020758816978083085130507043184471273380659243275938904335757337482424), msg: bytes | str | bytearray | memoryview | None = None) bool[source]¶
Return True if the proof holds for A, B, C under G and msg.
Raises where a point, the proof or the message is structurally invalid – a spelling that is no point, a proof that is not 64 octets, a message that is neither absent nor 32 – and answers False for material that is well formed and merely does not prove the equality. See _assert_structurally_valid_.
ecc.dsa module¶
Elliptic Curve Digital Signature Algorithm (ECDSA).
Implementation according to SEC 1 v.2:
http://www.secg.org/sec1-v2.pdf
specialized with bitcoin canonical ‘lower-s’ form, which is what sign
produces: a high-s signature is non-standard and does not relay, so
normalizing is the signer’s job. Plain verification – verify,
verify_ and assert_as_valid_ – and recovery take both forms and
offer no flag to refuse either: which form s carries was decided by
whoever signed, and refusing one refuses a signature that signer was free
to make. The rule survives where it belongs: in sign, in a script
interpreter’s own flags, in the leading-underscore functions a test asks
for it with, and in anti_exfil_host_verify, where the device signs
and is the party not trusted to choose.
sign also grinds for a low-R signature – one byte shorter in DER –
by default, as Core does; _grind_low_r is the loop and says why. Its
default goes with the nonce’s: grind=True and nonce=None, so a
caller who wants the nonce asks for grind=False and gets an error
rather than a guess if they forget.
sign also takes a value to commit to inside the nonce,
sign-to-contract style (see ecc.commit_nonce for the tweak), and
the four anti_exfil_* functions here are the protocol that
construction exists to support: the ECDSA Anti-Exfil Protocol, whose
five steps and reasoning are in anti_exfil_host_commit.
- class ecc.dsa.Sig(r: int, s: 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), *, check_validity: bool = True)[source]¶
Bases:
objectECDSA signature with strict ASN.1 DER serialization.
Strict, because BIP66 mandates it: lax DER validation (e.g. OpenSSL ignores extra padding) leaves the encoding malleable, and with it the transaction hash.
source: https://github.com/bitcoin/bips/blob/master/bip-0066.mediawiki
BIP66 mandates a strict DER format:
Format: [0x30] [data-size][0x02][r-size][r][0x02][s-size][s]
0x30: header byte to indicate compound structure
data-size: 1-byte size descriptor of the following data
0x02: header byte indicating an integer
r-size: 1-byte size descriptor of the r value that follows
- r: arbitrary-size big-endian r value.
It must use the shortest possible encoding for a positive integers: no null bytes at the start, except a single one when the next byte has its highest bit set (to avoid being interpreted as a negative number)
0x02: header byte indicating an integer
s-size: 1-byte size descriptor of the s value that follows
s: arbitrary-size big-endian s value. Same rules as for r apply
There are 7 bytes of meta-data:
compound header, compound size,
value header, r-value size,
value header, s-value size
The ECDSA signature (r, s) should be 64 bytes, r and s being 32 bytes integers each; however, integers in DER are signed, so if the value being encoded is 2^255 or more, a 33rd byte is added in front. Bitcoin has a “low s” rule for the s value to be at most ec.n // 2, but it is only a standardness rule miners are allowed to ignore. Moreover, no such rule exists for r.
The size descriptors above are one octet each because BIP66 fixes the format to secp256k1, whose widest signature body is 70 octets – DER’s one-octet length form covers up to 127. serialize takes any Curve, and a sequence or an element reaching 128 octets writes X.690 8.1.3’s long form instead: 0x80 | k followed by k octets of length. parse binds ec = secp256k1 and refuses that long form, matching Core’s IsValidSignatureEncoding, so the length forms serialize can write are not all forms parse reads (issue btclib-org/btclib#2130).
The encoding is not delegated, and it is the one thing about a signature that is not (issue btclib-org/btclib#911). The bindings have the same encoding as C – dsa.to_der, to_compact, normalize and is_low_s – and these say no:
a Sig carries an ec and writes its DER for ec.n_size, where the bindings answer for secp256k1 alone. A delegation is a second path gated like the others, for a computation with no arithmetic in it;
there is nothing to win. Serializing here is cheaper than to_der; parsing is dearer than to_compact, and what to_compact answers is r || s, which this side would still have to split and build a Sig from. lower_s is one comparison against n // 2, an order of magnitude under is_low_s;
and the exception messages are a public contract. Sig.parse names which rule the encoding broke, one message each; libsecp256k1 returns a single 0, and the bindings’ parse_der says “invalid DER signature” for all of them.
- classmethod parse(data: BytesIO | bytes | str | bytearray | memoryview, *, check_validity: bool = True, strict: bool = True) Sig[source]¶
Return a Sig by parsing binary data.
Deserialize a strict ASN.1 DER representation of an ECDSA signature.
strict says whether the encoding must be the canonical one, which is Bitcoin Core’s IsValidSignatureEncoding and covers what comes after the sequence as well as what is in it: a byte too many is not a DER signature either, and neither is a scalar written with a leading zero it does not need or without one it does. What it does not cover, and what a caller has to strip, is a sighash type byte – a script signature and a psbt partial signature both carry one, and neither is a bare DER encoding.
It is read for its truth and not asked for its type, which is the classification tests/bool_parameter_test.py records: the flag decides whether the call refuses, and the signature parsed out of an encoding both readings accept is one signature. Worth knowing which direction each accident goes, though, because they are not symmetric: “false” out of a configuration file is truthy and therefore strict, while a None from a lookup that found nothing is the lax one – so a caller who means the canonical encoding should pass True rather than whatever a table answered.
- class ecc.dsa.Signer(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), hf: Callable[[], ~ecc.alias.HashObject]=<built-in function openssl_sha256>)[source]¶
Bases:
objectSign several messages under one key, building the public key once.
sign_ derives the public key, when verify asks for the check – or the bindings do, on their own arm – and parses it again where the check is a compressed key’s, throwing both away once the call returns. This holds one across calls instead: mult once at construction, the generator multiplication gen_keys pays, and on the delegated arm the SEC encoding of it besides, so that _delegated_sign_ hands the bindings octets rather than a point to re-derive. Both are read and never recomputed, which is the floor issue btclib-org/btclib#982 measured and this class is built to reach: a signature is the arithmetic of _sign_ or one call into the bindings either way, and the check is what a held key removes from it.
This hands the caller the lifetime of a secret on both arms. ECDSA has no persistent object in libsecp256k1 the way BIP340 does – secp256k1_ecdsa_sign reads the private key from a bare pointer on every call rather than from a secp256k1_keypair built once – so a Signer holding one copy of the key needs somewhere of its own to keep it. dsa.sign’s prvkey argument takes a cffi array of exactly 32 octets and passes it through unconverted (btclib-org/btclib-secp256k1#253), so a caller who owns the buffer keeps owning it. This class builds one such buffer at construction – ffi.new(“unsigned char[32]”, …) – and hands the bindings that same pointer on every signature it makes, so there is one copy of the secret this holds throughout its life, on both arms, and wipe overwrites it on both: the buffer’s own 32 octets here, secp256k1_keypair’s there.
On the delegated arm, secp256k1 with sha256 by default and therefore the common case, wipe zeroes that buffer. On any other curve or hash function – the bindings declining, or curves.set_libsecp256k1_serving(serving=False) turning them off for the whole process – every signature is the Python arithmetic of _sign_, which never crosses into the bindings at all: the secret this object holds there is a plain integer the whole of its life, and wipe lets go of it – an int’s own limit rather than this class’s, since it cannot be overwritten, only dropped, which SECURITY.md’s limitations section states for the library at large. Which arm a given instance uses is decided once, at construction, from ec and hf alone: sign_ and sign take no nonce, no lower-s override and no commitment, unlike the free functions, precisely so that nothing a caller passes afterwards could move an instance from one arm to the other – wipe’s promise would otherwise depend on an argument to a later call rather than on the object itself. required is the one exception: SECURITY.md says how.
No pub_key argument either, matching ssa.Signer: the point of holding one is that every signature checks under it, so there is nothing for a caller to supply that this object does not already have parsed.
- sign(msg: bytes | str | bytearray | memoryview, *, grind: bool = True, verify: bool = True) bytes[source]¶
Return the signature of a message, reducing it with hf first.
- sign_(msg_hash: bytes | str | bytearray | memoryview, *, grind: bool = True, verify: bool = True) bytes[source]¶
Return the signature of a hf_len bytes message, as DER octets.
grind and verify are sign_’s own, over the key and public key this signer already holds: grinding is Core’s low-R search and the default pairs with it, verify is the post-sign check BIP66 does not ask for and Bitcoin Core’s CKey::Sign always makes, and declining it is the caller’s to do, exactly as the free function documents. No nonce, lower_s override or commit_hash – see the class docstring for why the arm this instance uses has to stay the one decided at construction.
- wipe() None[source]¶
Let go of the key, where letting go means something.
On the Python arm – self._pub_key_sec is None – the scalar this object signs with is the whole of what it holds, and dropping the reference is genuinely the end of its lifetime here: nothing else in this package still has it.
On the delegated arm the buffer built at construction is what is overwritten, ffi.buffer(self._prvkey_buffer)[:] = bytes(32) (btclib-org/btclib-secp256k1#253 is what makes it the same memory every signature reads, rather than a copy the bindings threw away): this arm’s every signature has read the same 32 octets this instance holds, so wiping them reaches every one of them at once, unlike the free sign this class replaces, which never held a buffer to wipe in the first place.
Either way a wiped signer refuses to sign rather than signing with the zeros. Idempotent, so that a caller may wipe a signer it is not sure about; the with statement is the customary way to run one.
- ecc.dsa.anti_exfil_host_commit(rho: bytes | str | bytearray | memoryview, hf: Callable[[], ~ecc.alias.HashObject]=<built-in function openssl_sha256>) bytes[source]¶
Return the host’s commitment to rho: step 1 of the anti-exfil protocol.
The protocol is specified in include/secp256k1_ecdsa_s2c.h of BlockstreamResearch/secp256k1-zkp.
A signing device that picks its own nonce can leak the private key through the nonces themselves, a few bits per signature, and no signature says that it did. The ECDSA Anti-Exfil Protocol takes that choice away: the host contributes randomness to the nonce derivation, so the device has nothing left to grind. Which only holds if the device publishes the nonce’s point before it learns the randomness – otherwise it grinds the randomness against candidate nonces until one carries the bits it wants out – so the exchange is a commit-reveal handshake of five steps:
the host draws rho and sends
anti_exfil_host_commit(rho)the device answers with
anti_exfil_signer_commit(msg_hash, prv_key, commitment), the point R its nonce will havethe host reveals rho
the device signs,
anti_exfil_sign(msg_hash, prv_key, rho)the host checks
anti_exfil_host_verifyagainst the R of step 2 and the rho it drew in step 1
rho is hf_len bytes from a cryptographically secure generator, and it stays secret until step 2 has been answered: revealed earlier it is the device’s to grind, which is the whole of what this prevents.
Restarting the protocol takes exactly the same rho, and the host checks that the device answers step 2 with exactly the same R. A device that could make the host draw again by failing would be choosing which nonces reach real signatures, one abort at a time – selective aborting is a bias like any other, and secp256k1_ecdsa_s2c.h puts the scale on it: some hundred aborts before there is a plausible attack, accumulating across a replacement of every device involved, though not across a replacement of the keys.
The commitment is the committed value as it enters the nonce derivation –
commit_entropy_under the sign-to-contract data tag, and nothing else – which is what lets step 2 and step 4 reach one nonce: the device derives it from this hash, and recomputes the same hash from rho when it signs.
- ecc.dsa.anti_exfil_host_verify(msg_hash: bytes | str | bytearray | memoryview, key: bytes | str | bytearray | memoryview | tuple[int, int] | ~ecc.curves.curve.PreparedPoint, sig: ~ecc.dsa.Sig | bytes | str | bytearray | memoryview, rho: bytes | str | bytearray | memoryview, receipt: tuple[int, int], hf: ~collections.abc.Callable[[], ~ecc.alias.HashObject] = <built-in function openssl_sha256>) bool[source]¶
Check the signature against R and rho: step 5 of the anti-exfil protocol.
Two questions answered as one, and the host needs both: that this is a valid signature, and that its nonce is the R of step 2 tweaked by the rho of step 1. Either alone is worth nothing – a valid signature over a nonce nobody constrained is the exfiltration this protects against, and a commitment that opens under an invalid signature is not a signature. Which
verify_already does in one call, both checks running against the same r.receipt is the R of step 2, what secp256k1-zkp’s secp256k1_ecdsa_s2c_opening holds.
A high s is False here, though
verify_accepts it. The device signs, and if the host took both s and n - s, the device could pick one of the two and send out one bit per signature. secp256k1-zkp’s secp256k1_anti_exfil_host_verify refuses a high s too, since it ends in secp256k1_ecdsa_verify, which accepts the lower s only.Otherwise it raises and answers False for the same reasons
verify_does: a key or a message hash that is structurally invalid raises, and a rho of the wrong size is False, a rho this commitment does not open to.
- ecc.dsa.anti_exfil_sign(msg_hash: bytes | str | bytearray | memoryview, prv_key: bytes | str | bytearray | memoryview | int, rho: bytes | str | bytearray | memoryview, lower_s: bool = True, 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), hf: Callable[[], ~ecc.alias.HashObject]=<built-in function openssl_sha256>) Sig[source]¶
Sign committing to the host’s rho: step 4 of the anti-exfil protocol.
Sign-to-contract with rho as the committed value, which is all step 4 is:
sign_with that commitment. The receipt it returns is dropped rather than passed on, because the host has it already – it is the R of step 2, and a host taking the device’s word for it here would be accepting a nonce point chosen after rho was revealed, which is the one thing the ordering exists to rule out.The device keeps no state between step 2 and step 4. It does not check rho against the commitment it was given: it re-derives the commitment from rho, and the nonce from that. A rho that does not match yields a different nonce, so the host’s step 5 fails and the exchange is over – and because the R of step 2 belonged to the commitment it was derived from, no nonce is ever used twice and the device’s key is never the thing at risk.
With
lower_s=Falsethe signature fails step 5 whenever its s is the high one:anti_exfil_host_verifysays why.
- ecc.dsa.anti_exfil_signer_commit(msg_hash: bytes | str | bytearray | memoryview, prv_key: bytes | str | bytearray | memoryview | int, host_commitment: 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), hf: Callable[[], ~ecc.alias.HashObject]=<built-in function openssl_sha256>) tuple[int, int][source]¶
Return the signer’s public nonce R: step 2 of the anti-exfil protocol.
The point of the nonce the device is going to use, published before the host reveals what its commitment commits to. Nothing is signed here, and that is the shape the protocol needs: R is a promise, and step 4 is what keeps it.
The commitment travels as RFC6979 section 3.6 additional data, exactly as the committed value does in
sign_, so the two derive one nonce and the R below is the receipt that signature will open with.
- ecc.dsa.assert_as_valid(msg: bytes | str | bytearray | memoryview, key: bytes | str | bytearray | memoryview | tuple[int, int] | ~ecc.curves.curve.PreparedPoint, sig: Sig | bytes | str | bytearray | memoryview, hf: Callable[[], ~ecc.alias.HashObject]=<built-in function openssl_sha256>, *, commit: bytes | str | bytearray | memoryview | None = None, receipt: tuple[int, int] | None=None) None[source]¶
Refuse an invalid ECDSA signature, reducing the message with hf.
- ecc.dsa.assert_as_valid_(msg_hash: bytes | str | bytearray | memoryview, key: bytes | str | bytearray | memoryview | tuple[int, int] | ~ecc.curves.curve.PreparedPoint, sig: Sig | bytes | str | bytearray | memoryview, hf: Callable[[], ~ecc.alias.HashObject]=<built-in function openssl_sha256>, *, commit_hash: bytes | str | bytearray | memoryview | None = None, receipt: tuple[int, int] | None=None) None[source]¶
Refuse an invalid ECDSA signature over a message hash.
The message enters already reduced – msg_hash, not the message – which is what the trailing underscore says; assert_as_valid is the spelling that reduces with hf first. Errors carry the reason,
verify_being the boolean answer. With commit_hash and receipt the sign-to-contract commitment is opened as well.Both forms of s are accepted, and there is no flag to ask otherwise: which of the two a signature carries was decided by whoever signed it, so a verifier refusing one is refusing a signature the signer was free to make. The module docstring says where the low-s rule belongs.
- ecc.dsa.crack_prv_key_var(msg1: bytes | str | bytearray | memoryview, sig1: Sig | bytes | str | bytearray | memoryview, msg2: bytes | str | bytearray | memoryview, sig2: Sig | bytes | str | bytearray | memoryview, hf: Callable[[], ~ecc.alias.HashObject]=<built-in function openssl_sha256>) tuple[int, int][source]¶
Return (private key, nonce) from two signatures sharing a nonce.
As
crack_prv_key_var_, with each message reduced by hf first.
- ecc.dsa.crack_prv_key_var_(msg_hash1: bytes | str | bytearray | memoryview, sig1: Sig | bytes | str | bytearray | memoryview, msg_hash2: bytes | str | bytearray | memoryview, sig2: Sig | bytes | str | bytearray | memoryview, hf: Callable[[], ~ecc.alias.HashObject]=<built-in function openssl_sha256>) tuple[int, int][source]¶
Return (private key, nonce) from two signatures sharing a nonce.
The classic nonce-reuse break: two signatures with one r over two message hashes are two linear equations in the nonce and the key. The messages enter already reduced; crack_prv_key_var reduces first.
- ecc.dsa.gen_keys(prv_key: bytes | str | bytearray | memoryview | 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, tuple[int, int]][source]¶
Return a private/public (int, Point) key-pair.
- ecc.dsa.recover_pub_key(key_id: int, msg: bytes | str | bytearray | memoryview, sig: Sig | bytes | str | bytearray | memoryview, hf: Callable[[], ~ecc.alias.HashObject]=<built-in function openssl_sha256>) tuple[int, int][source]¶
ECDSA public key recovery (SEC 1 v.2 section 4.1.6).
- ecc.dsa.recover_pub_key_(key_id: int, msg_hash: bytes | str | bytearray | memoryview, sig: Sig | bytes | str | bytearray | memoryview, hf: Callable[[], ~ecc.alias.HashObject]=<built-in function openssl_sha256>) tuple[int, int][source]¶
ECDSA public key recovery (SEC 1 v.2 section 4.1.6).
- ecc.dsa.recover_pub_keys(msg: bytes | str | bytearray | memoryview, sig: Sig | bytes | str | bytearray | memoryview, hf: Callable[[], ~ecc.alias.HashObject]=<built-in function openssl_sha256>) list[tuple[int, int]][source]¶
ECDSA public key recovery (SEC 1 v.2 section 4.1.6).
- ecc.dsa.recover_pub_keys_(msg_hash: bytes | str | bytearray | memoryview, sig: Sig | bytes | str | bytearray | memoryview, hf: Callable[[], ~ecc.alias.HashObject]=<built-in function openssl_sha256>) list[tuple[int, int]][source]¶
ECDSA public key recovery (SEC 1 v.2 section 4.1.6).
- ecc.dsa.recover_sec(key_id: int, msg: bytes | str | bytearray | memoryview, sig: Sig | bytes | str | bytearray | memoryview, hf: Callable[[], ~ecc.alias.HashObject]=<built-in function openssl_sha256>, *, compressed: bool = True) bytes[source]¶
ECDSA public key recovery, answered as SEC 1 v.2 octets.
recover_sec_ for a caller holding the message rather than its hash.
- ecc.dsa.recover_sec_(key_id: int, msg_hash: bytes | str | bytearray | memoryview, sig: Sig | bytes | str | bytearray | memoryview, hf: Callable[[], ~ecc.alias.HashObject]=<built-in function openssl_sha256>, *, compressed: bool = True) bytes[source]¶
ECDSA public key recovery, answered as SEC 1 v.2 octets.
The key recover_pub_key_ recovers, written as bytes_from_point writes it: the form a caller hashing the key wants, which is how a message signature is checked against an address. Where the bindings serve, they write these octets themselves and no point is built.
- ecc.dsa.sign(msg: bytes | str | bytearray | memoryview, prv_key: bytes | str | bytearray | memoryview | int, nonce: bytes | str | bytearray | memoryview | int | None = None, lower_s: bool = True, 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), hf: Callable[[], ~ecc.alias.HashObject]=<built-in function openssl_sha256>, *, grind: bool = True, verify: bool = True, pub_key: bytes | str | bytearray | memoryview | tuple[int, int] | ~ecc.curves.curve.PreparedPoint | None=None, commit: None = None) Sig[source]¶
- ecc.dsa.sign(msg: bytes | str | bytearray | memoryview, prv_key: bytes | str | bytearray | memoryview | int, nonce: bytes | str | bytearray | memoryview | int | None = None, lower_s: bool = True, 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), hf: Callable[[], ~ecc.alias.HashObject]=<built-in function openssl_sha256>, *, grind: bool = True, verify: bool = True, pub_key: bytes | str | bytearray | memoryview | tuple[int, int] | ~ecc.curves.curve.PreparedPoint | None=None, commit: bytes | str | bytearray | memoryview) tuple[Sig, tuple[int, int]]
ECDSA signature with canonical low-s preference.
Implemented according to SEC 1 v.2 The message msg is first processed by hf, yielding the value
msg_hash = hf(msg),
a sequence of bits of length hf_len.
Normally, hf is chosen such that its output length hf_len is roughly equal to nlen, the bit-length of the group order n, since the overall security of the signature scheme will depend on the smallest of hf_len and nlen; however, the ECDSA standard supports all combinations of hf_len and nlen.
RFC6979 is used for deterministic nonce.
grind asks for a low-R signature, as in sign_, which is where the loop and the default are explained.
verify asks for the signature to be checked before it is answered with and pub_key is the key it is checked under, both as in sign_, which is where the rule and the reason for the default are written.
commit is a value to commit to inside the nonce, and is reduced by hf as msg is: sign_ is the spelling that takes the two hashes.
- ecc.dsa.sign_(msg_hash: bytes | str | bytearray | memoryview, prv_key: bytes | str | bytearray | memoryview | int, nonce: bytes | str | bytearray | memoryview | int | None = None, lower_s: bool = True, 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), hf: Callable[[], ~ecc.alias.HashObject]=<built-in function openssl_sha256>, *, grind: bool = True, verify: bool = True, pub_key: bytes | str | bytearray | memoryview | tuple[int, int] | ~ecc.curves.curve.PreparedPoint | None=None, commit_hash: None = None) Sig[source]¶
- ecc.dsa.sign_(msg_hash: bytes | str | bytearray | memoryview, prv_key: bytes | str | bytearray | memoryview | int, nonce: bytes | str | bytearray | memoryview | int | None = None, lower_s: bool = True, 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), hf: Callable[[], ~ecc.alias.HashObject]=<built-in function openssl_sha256>, *, grind: bool = True, verify: bool = True, pub_key: bytes | str | bytearray | memoryview | tuple[int, int] | ~ecc.curves.curve.PreparedPoint | None=None, commit_hash: bytes | str | bytearray | memoryview) tuple[Sig, tuple[int, int]]
Sign a hf_len bytes message according to ECDSA signature algorithm.
If the deterministic nonce is not provided, the RFC6979 specification is used.
grind asks for a low-R signature, one byte shorter in DER: _grind_low_r is the loop, Core’s since its 0.17 and its default there and here, so that a signature made here is the one Core would have made. Its default pairs with the nonce’s: grind=True and nonce=None, the signature of a key and a message and nothing else.
A caller who wants the nonce – or a commitment, which owns the extra entropy the counter travels through – asks for grind=False in so many words, because grinding is a search over nonces and a nonce that is given leaves nothing to search. The two together are refused rather than one of them quietly winning: which one would win is exactly what a caller pinning a signature cannot afford to guess.
Keyword-only, rather than beside lower_s where it belongs by subject: ec and hf are positional here and a flag inserted before them would renumber both.
verify asks for the signature to be checked before it is answered with, and defaults to True on both implementations: Bitcoin Core’s CKey::Sign does it without offering a way out, and what it catches is not a bad argument – those have all raised by then – but a computation that went wrong, whose cost is a published signature that is invalid and may say something about the key. It is a whole verification, so the flag exists for the caller who has measured that against their own threat model rather than for the one who has not.
pub_key is the key the check verifies under, for a caller who already holds it: without it the check derives one per signature and throws it away, and that generator multiplication is most of what ECDSA’s check costs over BIP340’s – which is why ssa’s own sign_ has no such argument and its absence there is a decision. It is taken on trust and never checked against the private key, _abort_unless_checked being where that trust is described and paid for, and it is parsed before anything is signed so that a mistyped argument is not reported as a check on a signature the caller is now holding. Refused beside verify=False, which declines the check it is for.
commit_hash is a value to commit to inside the nonce, sign-to-contract style: the signature is an ordinary one, and the receipt returned beside it is what opens the commitment (see ecc.commit_nonce). Keyword-only, and the only argument that changes what is returned, so that neither is easy to pass by accident. A commitment derives its own nonce and cannot be given one: the derivation is where half of the scheme’s security is.
- ecc.dsa.sign_recoverable(msg: bytes | str | bytearray | memoryview, prv_key: bytes | str | bytearray | memoryview | int, nonce: bytes | str | bytearray | memoryview | int | None = None, lower_s: bool = True, 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), hf: Callable[[], ~ecc.alias.HashObject]=<built-in function openssl_sha256>) tuple[Sig, int][source]¶
ECDSA signature and the key_id that recovers the signing key.
sign with the key_id beside it; sign_recoverable_ is the spelling that takes the message hash.
- ecc.dsa.sign_recoverable_(msg_hash: bytes | str | bytearray | memoryview, prv_key: bytes | str | bytearray | memoryview | int, nonce: bytes | str | bytearray | memoryview | int | None = None, lower_s: bool = True, 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), hf: Callable[[], ~ecc.alias.HashObject]=<built-in function openssl_sha256>) tuple[Sig, int][source]¶
Sign a hf_len bytes message, naming the key_id that recovers the key.
The signature sign_ gives, and beside it the key_id recover_pub_key_ takes to answer the signer’s own public key: the same value the recovery flag of a message signature carries.
A spelling of its own rather than a flag on sign_, as libsecp256k1 has ecdsa_sign_recoverable beside ecdsa_sign: a second argument changing what is returned would multiply into four return shapes with the commitment, which is also why no commitment is taken here (see sign_). Nothing is published that a plain signature keeps: the key_id is derivable from any signature by recovering the four candidates and seeing which is the signer’s, so what this saves is that search and not a secret.
No grind either, and here it is not a matter of the shape: a recoverable signature is 65 bytes of r, s and the flag, with no DER pad for a low r to save. Core has the same asymmetry, CKey::Sign grinding and CKey::SignCompact passing a null ndata and looping over nothing.
- ecc.dsa.verify(msg: bytes | str | bytearray | memoryview, key: bytes | str | bytearray | memoryview | tuple[int, int] | ~ecc.curves.curve.PreparedPoint, sig: Sig | bytes | str | bytearray | memoryview, hf: Callable[[], ~ecc.alias.HashObject]=<built-in function openssl_sha256>, *, commit: bytes | str | bytearray | memoryview | None = None, receipt: tuple[int, int] | None=None) bool[source]¶
ECDSA signature verification (SEC 1 v.2 section 4.1.4).
commit is reduced by hf as msg is; verify_ is the spelling that takes the two hashes. Raises and answers False for the same reasons verify_ does, less the digest: msg is of any size here and is reduced by hf, so there is no declared length for it to miss. It raises where the bindings are required and decline hf, as verify_ does.
- ecc.dsa.verify_(msg_hash: bytes | str | bytearray | memoryview, key: bytes | str | bytearray | memoryview | tuple[int, int] | ~ecc.curves.curve.PreparedPoint, sig: Sig | bytes | str | bytearray | memoryview, hf: Callable[[], ~ecc.alias.HashObject]=<built-in function openssl_sha256>, *, commit_hash: bytes | str | bytearray | memoryview | None = None, receipt: tuple[int, int] | None=None) bool[source]¶
ECDSA signature verification (SEC 1 v.2 section 4.1.4).
commit_hash and receipt open the commitment the nonce carries, and a signature that does not commit to that value is False as a forged one is: the answer is about this signature and this commitment, both.
Raises where the public key or the message hash is structurally invalid – a key spelled in a way no conversion accepts or that does not lift, or a digest that is not hf’s size – and answers False for a well-formed signature, key and digest that are merely not authentic: a scalar out of range, a malformed DER encoding, an equation that does not hold. See _assert_structurally_valid_, the comment every boolean verification in this library points at.
Where the bindings are required (curves.set_libsecp256k1_serving with required=True) and decline hf, it raises EccRuntimeError and does not answer False: nothing was checked.
ecc.ecdsa_adaptor module¶
ECDSA adaptor signatures, according to the DLC specification.
https://github.com/discreetlogcontracts/dlcspecs/blob/master/ECDSA-adaptor.md
An adaptor signature is an ECDSA signature encrypted under a point Y: whoever knows y, with Y = y*G, decrypt`s it into an ordinary signature, and whoever holds both that signature and the adaptor signature `recover`s y. `verify checks, without y, that the adaptor signature decrypts to a valid signature by the signing key.
The scheme is for DLCs. Each adaptor signature leaks the Diffie-Hellman key of the signing key X and of Y (the specification’s warning), and no proof of knowledge of y is attached: a DLC’s Y is an oracle’s anticipated signature, which the oracle knows by construction. Do not use it where Y comes from somebody who need not know y.
The wire format is the specification’s 162 octets, R || R_a || s_a || e || s: R = k*Y and R_a = k*G, both 33-octet compressed points; s_a = k^-1 (m + r*x) a scalar, r being the x-coordinate of R modulo n; and (e, s), the 64-octet proof that R_a and R share the discrete logarithm k over G and Y. The proof is not BIP374’s (ecc.dleq): its tag, its challenge input and its encoding are the specification’s, which is libsecp256k1-zkp’s, so it lives here. The curve, secp256k1, and the hash, sha256, are not arguments, as in ecc.dleq.
Nonces follow libsecp256k1-zkp’s ecdsa_adaptor module (src/modules/ecdsa_adaptor/main_impl.h), the specification leaving sample_nonce open: a BIP340-style tagged hash, with aux masking the key, for k and for the proof’s own nonce. encrypt draws a fresh aux when it is not given. Passing nonce instead fixes k, and the proof’s nonce follows from it: a k used for two messages reveals the signing key.
Timing. k and y go through mult and mod_inv, and x only through integer products, as in dsa; what verify handles is public and uses the _var arithmetic. As everywhere in Python here, that is not constant-time.
- ecc.ecdsa_adaptor.assert_as_valid(adaptor_sig: bytes | str | bytearray | memoryview, msg_hash: bytes | str | bytearray | memoryview, pub_key: bytes | str | bytearray | memoryview | tuple[int, int] | PreparedPoint, enc_key: bytes | str | bytearray | memoryview | tuple[int, int] | PreparedPoint) None[source]¶
Raise unless adaptor_sig is a valid encryption under enc_key.
verify is the spelling that answers True or False; this one says why. An EccValueError for a proof that does not hold or an equation that does not balance, and for an argument that is malformed.
- ecc.ecdsa_adaptor.decrypt(adaptor_sig: bytes | str | bytearray | memoryview, dec_key: bytes | str | bytearray | memoryview | int) Sig[source]¶
Return the low-s ECDSA signature adaptor_sig decrypts to under dec_key.
The result is a signature of the message only if verify accepted the adaptor signature and dec_key is the discrete logarithm of its enc_key: nothing here checks either.
- ecc.ecdsa_adaptor.encrypt(msg_hash: bytes | str | bytearray | memoryview, prv_key: bytes | str | bytearray | memoryview | int, enc_key: bytes | str | bytearray | memoryview | tuple[int, int] | PreparedPoint, aux: bytes | str | bytearray | memoryview | None = None, *, nonce: bytes | str | bytearray | memoryview | int | None = None) bytes[source]¶
Return the 162-octet adaptor signature of msg_hash under enc_key.
msg_hash is 32 octets, the digest to be signed. aux is 32 octets of auxiliary randomness, fresh when absent. nonce replaces k, and with it the derivation: see the module docstring.
- ecc.ecdsa_adaptor.recover(adaptor_sig: bytes | str | bytearray | memoryview, sig: Sig | bytes | str | bytearray | memoryview, enc_key: bytes | str | bytearray | memoryview | tuple[int, int] | PreparedPoint) int[source]¶
Return the decryption key of enc_key, from adaptor_sig and sig.
sig is the signature decrypt made, in either form of s, or its DER encoding. An EccValueError where its r is not the adaptor signature’s, or where the key it yields is neither enc_key’s nor its negation: the signature is not a decryption of this adaptor signature under this key.
- ecc.ecdsa_adaptor.verify(adaptor_sig: bytes | str | bytearray | memoryview, msg_hash: bytes | str | bytearray | memoryview, pub_key: bytes | str | bytearray | memoryview | tuple[int, int] | PreparedPoint, enc_key: bytes | str | bytearray | memoryview | tuple[int, int] | PreparedPoint) bool[source]¶
Return True if adaptor_sig encrypts a signature of msg_hash by pub_key.
enc_key is the encryption key: its discrete logarithm decrypts it.
Raises where an argument cannot be read as what it is: an adaptor signature that is not 162 octets, a digest not of 32 octets, a key that is no point. Answers False for an adaptor signature of the right size that the specification does not accept: an R or R_a that is no point, an R whose x-coordinate is zero modulo n, an s_a or a proof s out of range, a proof or an equation that does not hold. assert_as_valid says which.
ecc.ecies module¶
ECIES in the BIE1 layout, with the block cipher supplied by the caller.
BIE1 is the ECIES variant the bitcoin world converged on: Electrum’s
encrypt / decrypt commands, bitcore and bitcoinjs all speak it, and it
is what turns the shared secret of ecc.dh into an actual
encrypted message. A sender who has the recipient’s public key:
generates an ephemeral key pair and runs ECDH against that public key
takes the sha512 of the compressed shared point and splits the 64 bytes into iv | key_e | key_m, 16 | 16 | 32
encrypts the message with AES-128-CBC and PKCS#7 padding, under key_e and that iv
frames it as
b"BIE1" + ephemeral pub key + ciphertext, appends the HMAC-SHA256 of that framing under key_m, and base64-encodes the lot
The recipient re-derives the same three values from the ephemeral public key carried in the envelope, so nothing but the envelope has to travel.
Why the cipher is a parameter. This package has no cryptographic dependency: hashlib and the secp256k1 bindings are the whole of it, and its install story is “Python plus the bindings”. AES is not in the standard library, so shipping BIE1 whole would mean taking cryptography or pycryptodome for one convenience function, on every user, forever. A pure-Python AES inside this package is the worse answer rather than the cheaper one: a table-driven block cipher leaks its key through cache timing, and a timing-vulnerable cipher is a worse thing to ship than no cipher at all. So this module implements the half that is this package’s business – the key agreement, the derivation, the framing, the MAC and the armor – and takes the other half as two callables. Interoperability is preserved for anyone who brings their own AES, and the dependency is theirs to choose.
The contract those callables must honour. Both are called
positionally, as f(key, iv, data), with a 16-byte key_e and a 16-byte
iv:
encrypt_f(key, iv, plaintext)returns AES-128-CBC ciphertext with PKCS#7 padding already applied. The padding is not optional and not this package’s to add: it is what makes the result a whole number of 16-byte blocks, and PKCS#7 appends a full block when the plaintext is already block-aligned, so the ciphertext is always strictly longer than the plaintext.encrypt()checks both of those and refuses a cipher that does not pad, which is the mistake that otherwise ships an envelope no other implementation can read back.decrypt_f(key, iv, ciphertext)is the inverse, and strips the padding. It is called only after the MAC has verified, so it is never handed a ciphertext this library has not authenticated.
Anything other than AES-128-CBC with PKCS#7 will round-trip against itself and interoperate with nothing, which is the whole point of the scheme; the callables are a way to source AES, not a choice of cipher.
What BIE1 is not. It is not a BIP and has no specification: its definition
is its implementations, of which Electrum’s is the most read. This module is
written against ecies_encrypt_message and ecies_decrypt_message in
electrum/crypto.py and matches them byte for byte, and the test suite decrypts
ciphertexts taken from Electrum’s own test vectors. That is also why nothing
here is parameterized by curve or hash function, unlike ecc.dsa and
ecc.ssa: every implementation that exists is secp256k1 with sha512 and
HMAC-SHA256, so a parameter would advertise an interoperability that has no
other end.
The magic bytes are a parameter, though, because Electrum itself varies them: BIE1 for a user password and BIE2 for an xpub-derived one, over an otherwise identical layout.
- class ecc.ecies.Envelope(magic: bytes | str | bytearray | memoryview, eph_pub_key: bytes | str | bytearray | memoryview, ciphertext: bytes | str | bytearray | memoryview, mac: bytes | str | bytearray | memoryview, *, check_validity: bool = True)[source]¶
Bases:
objectThe BIE1 framing: magic, ephemeral public key, ciphertext, MAC.
Everything here is cipher-free. The ciphertext is carried as opaque bytes, so an envelope can be parsed, validated, MAC-checked and re-serialized by code that has no AES at all.
- assert_valid() None[source]¶
Raise unless every field has the size and shape BIE1 gives it.
This is the structure alone: whether the MAC is the right MAC is
assert_valid_mac(), which needs a key this object does not have.
- assert_valid_mac(key_m: bytes | str | bytearray | memoryview) None[source]¶
Raise unless the MAC is the one key_m produces over this envelope.
A failure here is the one error the scheme reports for two different causes, and it cannot tell them apart: the envelope was tampered with, or it was addressed to somebody else. A wrong private key derives a wrong key_m, and a wrong key_m is exactly what a forged MAC looks like.
- classmethod b64decode(data: bytes | str | bytearray | memoryview, *, magic: bytes = b'BIE1', check_validity: bool = True) Envelope[source]¶
Return the envelope a base64 armor carries.
- classmethod from_ciphertext(eph_pub_key: bytes | str | bytearray | memoryview, ciphertext: bytes | str | bytearray | memoryview, key_m: bytes | str | bytearray | memoryview, *, magic: bytes | str | bytearray | memoryview = b'BIE1') Envelope[source]¶
Frame an already-encrypted ciphertext and MAC it under key_m.
The magic is coerced here and not left to the constructor below, which would take it: this method concatenates before it builds, so a magic that is not bytes failed on the + rather than as the argument it is.
- mac_from_key(key_m: bytes | str | bytearray | memoryview) bytes[source]¶
Return the HMAC-SHA256 the framed envelope has under key_m.
- classmethod parse(data: bytes | str | bytearray | memoryview, *, magic: bytes = b'BIE1', check_validity: bool = True) Envelope[source]¶
Return the envelope the octets carry, at BIE1’s fixed offsets.
The magic is compared here rather than left to the caller: the first four bytes are what answer “is this a BIE1 envelope at all”, and every offset below is meaningless if they do not.
Its type is asked before that comparison, and b64decode is covered by the same question, handing this one what it was given: a magic of no bytes type is unequal to whatever the buffer starts with, so every envelope would have been refused for the bytes it does carry rather than for the argument that cannot be any.
- ecc.ecies.decrypt(armor: bytes | str | bytearray | memoryview, prv_key: bytes | str | bytearray | memoryview | int, decrypt_f: Callable[[bytes, bytes, bytes], bytes], *, magic: bytes = b'BIE1') bytes[source]¶
Decrypt a BIE1 base64 armor with the recipient private key.
decrypt_f must be AES-128-CBC with PKCS#7 padding; the module docstring has the contract in full. It is reached only once the MAC has verified, so a wrong key or a tampered envelope raises EccRuntimeError and the caller’s cipher never runs.
- ecc.ecies.derive_keys(prv_key: bytes | str | bytearray | memoryview | int, pub_key: bytes | str | bytearray | memoryview | tuple[int, int] | PreparedPoint) tuple[bytes, bytes, bytes][source]¶
Return the (iv, key_e, key_m) triple BIE1 derives from an ECDH exchange.
The shared point is serialized compressed and hashed with sha512; the 64 bytes are cut 16 | 16 | 32. Both ends call this: the sender with the ephemeral private key and the recipient’s public key, the recipient with its own private key and the ephemeral public key from the envelope.
The multiplication is delegated and the derivation is not;
ecc.dhsays why.No infinity check on the shared point, unlike
ecc.dh.diffie_hellman(), which takes a bare int: the scalars that could produce one are exactly those scalar_from_prv_key rejects, and both inputs here go through a validating conversion.
- ecc.ecies.encrypt(msg: bytes, pub_key: bytes | str | bytearray | memoryview | tuple[int, int] | PreparedPoint, encrypt_f: Callable[[bytes, bytes, bytes], bytes], *, eph_prv_key: bytes | str | bytearray | memoryview | int | None = None, magic: bytes = b'BIE1') str[source]¶
Encrypt a message to a public key, returning the BIE1 base64 armor.
encrypt_f must be AES-128-CBC with PKCS#7 padding; the module docstring has the contract in full. eph_prv_key overrides the random ephemeral key, which is what makes a fixed test vector reproducible – reusing one across two messages to the same recipient reuses the whole key stream, so leave it alone outside of tests.
The plaintext is bytes and not Octets, alone among the message parameters of this library: a hex string is how this package spells binary everywhere else, and reading “deadbeef” as four bytes rather than as the eight characters somebody meant to hide is not a mistake the recipient can notice. assert_type is what refuses a caller’s mistake here instead of leaking whatever encrypt_f does with a non-bytes plaintext (issue btclib-org/ecc#11).
ecc.ellswift module¶
ElligatorSwift: the SwiftEC map, its inverse and BIP324’s x-only ECDH.
An ElligatorSwift encoding is 64 bytes that are indistinguishable from random: a pair of field elements (u, t) that the SwiftEC map takes to an x-coordinate of the curve. Every 64-byte string decodes – there is no invalid encoding to recognize – which is what an observer is left with, and what BIP324’s v2 handshake needs of the keys it carries.
https://github.com/bitcoin/bips/blob/master/bip-0324.mediawiki
This module stops at xdh, BIP324’s x-only ECDH over two encodings. What BIP324 builds on its secret – the key schedule, the cipher and the packet framing – is a protocol’s, and not a fact about a curve.
decode_var is the map, and it is deterministic. encode_var and create are its inverse, and are not: up to eight (u, t) pairs decode to one x-coordinate, one of them is picked at random, and the randomness is the point – an encoding derived from the key alone would be recognizable by anyone who could derive it too. So there is no deterministic option here the way dsa.sign takes a nonce: RFC6979 specifies the nonce it derives, while nothing specifies a derivation for this, and one invented for this package would be a promise the specification does not make.
The map wants a curve with a == 0, y^2 = x^3 + b, and every function here refuses one with a != 0. secp256k1 is such a curve and so are the other Koblitz curves of the catalogue, which is why these take an ec at all: the Python arithmetic below is what serves them, where secp256k1 is handed to the libsecp256k1 bindings.
- ecc.ellswift.create(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)) bytes[source]¶
Return an ElligatorSwift encoding of the private key’s public key.
The caller supplies no randomness, so it cannot hand in any that is derived from the key. Where the bindings serve, secp256k1_ellswift_create draws the encoding from a hash of the private key and fresh randomness, so the key adds entropy of its own. The Python path draws it from secrets alone, as encode_var does.
The name is plain because the private key reaches only mult, whose algorithm does not follow its value. The variable-time encoding works on the public key.
- ecc.ellswift.decode_var(ell: 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)) tuple[int, int][source]¶
Return the point an ElligatorSwift encoding decodes to.
Every encoding of the right size decodes, there being no invalid one to refuse: that is the property the scheme is built on.
- ecc.ellswift.encode_var(pub_key: bytes | str | bytearray | memoryview | tuple[int, int] | PreparedPoint, 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)) bytes[source]¶
Return an ElligatorSwift encoding of the public key.
The randomness is drawn here and is not a function of the key, which is what BIP324 requires of it: two encodings of one key are unlinkable, and an encoding nobody can recompute is what makes them so. Where the private key is at hand, create is the call, and its docstring says why.
- ecc.ellswift.xdh(ell_a: bytes | str | bytearray | memoryview, ell_b: bytes | str | bytearray | memoryview, prv_key: bytes | str | bytearray | memoryview | int, initiating: bool, 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)) bytes[source]¶
Return BIP324’s shared secret of two ElligatorSwift encodings.
ell_a is the initiator’s encoding and ell_b the responder’s, in that order whichever party calls. prv_key is the caller’s own, and initiating says which of the two encodings is the caller’s: True for ell_a. That correspondence is not checked, as libsecp256k1 does not check it.
The secret is the tagged hash bip324_ellswift_xonly_ecdh of ell_a, ell_b and the x-coordinate of the private key times the other party’s point. Where the bindings serve, this is one call, secp256k1_ellswift_xdh with BIP324’s hash function: the shared point stays in C, and only the secret comes back. The Python path multiplies with mult.
A curve whose cofactor is not 1 is refused.
The name is plain because the private key reaches only mult or libsecp256k1. The variable-time decoding works on the other party’s public encoding.
ecc.exceptions module¶
Exception classes.
These exist only to tell an exception raised by ecc from one raised by any other code: each derives from the built-in that says what kind of failure it is, and adds nothing to it.
EccException is what makes that telling apart a single except rather than a tuple of three a caller has to keep in step with this hierarchy. It is inherited beside the built-in and not instead of it, which is the half that matters: EccValueError is a ValueError, so code catching the built-in catches it, and json.JSONDecodeError is the standard library doing the same.
It is caught and never raised: every raise below is one of the three, and which one answers a question the base cannot carry – whether the value was wrong, the type was, or neither was and a check failed anyway. A caller with something to do about that difference names the specific class; except EccException is for the caller who only needs to know it came from here.
The two classes carrying a field hand every constructor argument to BaseException.__init__ and compose their message in __str__, which is what subprocess.CalledProcessError and UnicodeDecodeError do, and what makes them picklable: BaseException.__reduce__ returns (cls, self.args), so a class whose args is the composed message alone is rebuilt by calling it with one argument, and one argument is not what it takes. That is a TypeError out of pickle, out of copy.copy and out of copy.deepcopy – and out of a ProcessPoolExecutor, which cannot send the exception back and reports a broken pool instead of the failure the worker died of.
- exception ecc.exceptions.BorromeanRingError(message: str, ring: int | None, position: int | None)[source]¶
Bases:
EccRuntimeErrorA borromean ring signature check failed, and where names it.
ring is the index into pubk_rings and position the index within that ring: an e-value landing on zero, and the point at infinity its one-in-n neighbour lands a ring’s nonce or its r on instead, each happen at one ring and one position, and ecc.borromean.sign and assert_as_valid already have both in hand at every one of their raises. Naming them is what tells a caller building on this primitive which key rejected the signature rather than only that the whole thing did.
Both are None for the one failure with no ring of its own: the final e0 not matching what every ring converges on is a property of the whole signature, not of any single ring in it.
An EccRuntimeError and not InvalidContributionError: that class names a party to an interactive multi-round protocol – MuSig2 – and which of its contributions was wrong, where a borromean ring signature is not interactive and has no parties to accuse, only positions in a signature that either close their ring or do not. An EccRuntimeError still, so code catching that keeps catching this, as verify already does.
- exception ecc.exceptions.EccException[source]¶
Bases:
ExceptionAnything ecc raised, whatever kind of failure it is.
The one name to catch for a caller who handles the standard library’s exceptions anyway and needs to know which came from here. Never raised: the three below it are, and each says which kind of failure it was.
- exception ecc.exceptions.EccRuntimeError[source]¶
Bases:
EccException,RuntimeErrorA check that failed on valid inputs, e.g. a failed verification.
- exception ecc.exceptions.EccTypeError[source]¶
Bases:
EccException,TypeErrorAn input of a type no conversion accepts: a caller error.
- exception ecc.exceptions.EccValueError[source]¶
Bases:
EccException,ValueErrorA value no valid input could carry; the package’s usual refusal.
- exception ecc.exceptions.InvalidContributionError(signer: int | None, contrib: str)[source]¶
Bases:
EccRuntimeErrorA party to an interactive protocol sent a value that does not check out.
Which party, and which of its contributions: signer is the index in the list the caller passed, None for the aggregator – who has no index, having no key – and contrib names what was wrong, one of “pubkey”, “pubnonce”, “aggnonce”, “aggothernonce”, “psig” or “adaptor”. That is the whole point of the class: a multi-round protocol that merely fails leaves every participant a suspect, and the answer a caller needs is who to hold accountable and to exclude from the next attempt.
An EccRuntimeError and not an EccValueError, which is the other obvious base and the one BIP327 keeps separate: its reference implementation raises ValueError for an argument that breaks a precondition – the caller’s own mistake, a 33-byte tweak – and this for a peer misbehaving, and the MuSig2 test vectors distinguish the two case by case. Sharing a base would put the two beyond telling apart by except, and would let every except ValueError in the package swallow an accusation.
ecc.frost module¶
FROST threshold Schnorr signing, according to BIP445.
https://github.com/siv2r/bip-frost-signing, README.md at v0.12.1, head ba299badfbe647e670686de75578dd501a627028. The BIP is a draft, not in bitcoin/bips master; tests/_data/README.md records why this module pins that repository rather than its pull request, https://github.com/bitcoin/bips/pull/2070.
FROST turns t of n participants into one signer: a threshold secret key is Shamir-shared among n participants at key generation, and any t of them can cooperatively produce a signature that verifies under a single x-only public key with ecc.ssa and with any other BIP340 verifier – the same output shape ecc.musig2 produces for an n-of-n group, with a chosen subset standing in for the whole group.
Key generation is out of scope, here as in the BIP. This module takes a threshold secret key already split into shares – a secshare per participant, all of them scalar multiples of the same secret polynomial’s constant term – and never splits one itself. It is tested on BIP445’s vectors, which a trusted dealer generated, and on the output of ChillDKG’s reference. validate_threshold_info checks key material a dealer or a DKG produced rather than producing any: it asks whether a threshold info’s own public shares reconstruct its own threshold public key, which functional compatibility needs and which is not a statement about how that key material came to exist. btclib-org/btclib#2200 is where key generation is tracked: a trusted dealer, then ChillDKG, once BIP445 and BIP-FROST-DKG settle.
The round shape mirrors ecc.musig2: a primitive per round and no function that signs, because signing is interactive. Key material and nonces come from the caller, not from a session this module holds:
setup, once per group: validate_threshold_info on the n-participant ThresholdInfo a dealer or a DKG produced;
tweaking, optional and once per session: tweak_ctx_init_var and apply_tweak_var, or thresh_pubkey_and_tweak_var for both in one call, answering a TweakContext;
round 1, the nonces: nonce_gen for each of the u selected signers, then nonce_agg_var over what they published, which anyone can do;
round 2, the partial signatures: sign (or deterministic_sign) for each signer, partial_sig_verify to hold a signer to what it sent, and partial_sig_agg for the aggregate signature.
A secret nonce signs once, exactly as in musig2: sign zeroes the 64-byte bytearray it is given before using it, so calling it twice with the same buffer reads two zero scalars and raises rather than signing twice – which is why its argument is a bytearray and not bytes.
Participant vs. signer. n participants exist from key generation on, each with an identifier in 0 .. n-1, a secshare and a pubshare. A signing session selects u of them, t <= u <= n; that subset is the signers, and every ids argument below names their identifiers, sorted or not as the caller built the list. ThresholdInfo.pub_shares is indexed by identifier over all n participants and may hold None where a share is unknown; a SessionContext’s own pub_shares, when given at all, is the u-long list for the selected signers alone – absent entirely is also valid, a signer able to produce and verify against its own partial signature without it, partial_sig_verify alone requiring the list because there is no other way to check a signature against a key it was not given.
secp256k1 and sha256 are not parameters here, unlike in dsa and ssa, for the reason musig2’s own docstring gives: BIP445 is defined for that pair alone, and a Curve argument would advertise a genericity no vector could check.
The upper bound of 128 participants (n <= 128) is a security requirement and not a format one: FROST3’s unforgeability proof assumes the set of compromised participants is fixed before key generation, and an adversary that instead chooses whom to compromise after seeing the public shares can, for large n, forge with fewer than t compromised participants – provided it solves a search problem BIP445 cites as provably hard whenever n <= 131. 128 stays inside that bound with a margin; it is not raised by passing a larger n to any function here.
The tags below are BIP445’s own and disjoint from BIP327’s: BIP0445/aux, BIP0445/nonce, BIP0445/noncecoef and BIP0445/deterministic/nonce. Only BIP0340/challenge is shared, and it is BIP340’s, read the same way musig2 reads it – through ecc.ssa.challenge_.
Nothing here is shared with `ecc.musig2` by import, though BIP445’s own reference code says in its header that it is “copied or modified from” BIP327’s: this module’s tag bytes, several error messages (the tweak-range message among them) and the shape of ThresholdInfo/interpolation differ from BIP327’s, and the functions that do look alike – nonce_agg_var, apply_tweak_var – were checked line by line against BIP445’s own reference rather than assumed identical from the name. Where a check found the same construction, this module keeps its own copy rather than importing musig2’s private helpers: the two modules are each a self-contained mirror of one BIP, which is the shape musig2.py itself already has no sibling to share with.
- class ecc.frost.SessionContext(n: int, t: int, ids: Sequence[int], pub_shares: Sequence[bytes | str | bytearray | memoryview] | None, thresh_pk: bytes | str | bytearray | memoryview, agg_nonce: bytes | str | bytearray | memoryview, tweaks: Sequence[bytes | str | bytearray | memoryview], is_xonly: Sequence[bool], msg: bytes | str | bytearray | memoryview)[source]¶
Bases:
objectEverything the signers of one session have to agree on.
The aggregate nonce, the total and threshold participant counts, the selected signers’ identifiers, their public shares (or None, a session that does not carry them), the threshold public key, the tweaks with their kinds, and the message. musig2.SessionContext’s own shape, with FROST’s extra fields – n, t, ids, pub_shares – in place of MuSig2’s aggregated pub_keys: nothing here aggregates a key list, the threshold public key already being one key from key generation.
- class ecc.frost.SessionValues(Q: tuple[int, int], gacc: int, tacc: int, b: int, R: tuple[int, int], e: int)[source]¶
Bases:
objectWhat every signer derives from a SessionContext before signing.
Q, gacc and tacc are the tweaked threshold public key and its tweak accumulators, from thresh_pubkey_and_tweak_var
b is the coefficient combining the two halves of the nonce
R is the effective nonce point, R_1 + b*R_2
e is the BIP340 challenge, over R, Q and the message
Unlike musig2.SessionValues, there is no cached key-aggregation coefficient here: _derive_interpolating_value_var is a pure function of session_ctx.ids and one signer’s own id, and both sign and partial_sig_verify_ read ids straight off the context rather than off a value cached here.
- class ecc.frost.ThresholdInfo(t: int, thresh_pk: bytes | str | bytearray | memoryview, pub_shares: Sequence[bytes | str | bytearray | memoryview | None])[source]¶
Bases:
objectKey material a signer holds after key generation, common to a session.
t, the threshold public key, and every participant’s public share it knows – pub_shares is n long, indexed by identifier, None where a share is unknown to this holder. validate_threshold_info checks it: that every subset of t present shares it selects reconstructs the same threshold public key, which is what makes the rest of this module answer the same signature regardless of which t of the present shares a caller happened to check against.
A key generation protocol answers this, and this module never builds one: btclib-org/btclib#2200 is where producing it – a trusted dealer, ChillDKG – is tracked.
- class ecc.frost.TweakContext(Q: tuple[int, int], gacc: int, tacc: int)[source]¶
Bases:
objectThe threshold public key, and what tweaking it has accumulated.
musig2.KeyAggContext’s own shape and reason: Q is the tweaked point, gacc the product of the negations x-only tweaking forced, tacc the sum of the tweaks – so that the partial signatures still add up under the even-y Q a BIP340 verifier assumes, and partial_sig_agg adds the tweak contribution in once for the group rather than each signer adding a share of it. Unlike KeyAggContext, this is not built by aggregating several keys – FROST’s threshold public key comes from key generation whole – which is why tweak_ctx_init_var takes one key and not a list.
- ecc.frost.apply_tweak_var(tweak_ctx: TweakContext, tweak: bytes | str | bytearray | memoryview, is_xonly: bool) TweakContext[source]¶
Return the context tweaked by t, x-only or plain: BIP445’s ApplyTweak.
musig2.apply_tweak_var’s own reasoning for the negation and the accumulators holds identically here; only the range-error message differs from BIP327’s, per the module docstring.
- ecc.frost.deterministic_sign(sec_share: bytes | str | bytearray | memoryview, signer_id: int, agg_other_nonce: bytes | str | bytearray | memoryview | None, n: int, t: int, ids: Sequence[int], pub_shares: Sequence[bytes | str | bytearray | memoryview] | None, thresh_pk: bytes | str | bytearray | memoryview, tweaks: Sequence[bytes | str | bytearray | memoryview], is_xonly: Sequence[bool], msg: bytes | str | bytearray | memoryview, aux_rand: bytes | str | bytearray | memoryview | None = None) tuple[bytes, bytes][source]¶
Return the (pubnonce, partial signature) of a signer with no RNG.
musig2.deterministic_sign’s own reason: the two rounds collapse into one for the last signer to act, given the aggregate of everybody else’s nonces. Unlike MuSig2, which is always n-of-n, the derived nonce also binds the signer set ids – without that, a malicious coordinator could recover the secret share by replaying the session with the signer set varied each time, which cannot happen to MuSig2’s DeterministicSign because its signer set is fixed by the protocol.
The nonce is derived from the secret share as sign uses it, negated where the tweaked threshold public key has an odd y and multiplied by gacc. Were it derived from the share as given, a coordinator replaying the session under the key and under its negation, with no public shares, would get two partial signatures over one nonce, and their difference is the share.
agg_other_nonce is None for a sole signer, who has no other signers’ nonces to aggregate, and for nobody else; passed, it should be the aggregate of every other signer’s pubnonce, and may come from an untrusted coordinator – sign, reached at the end of this, is what catches a session it does not assemble into.
- ecc.frost.nonce_agg_var(pub_nonces: Sequence[bytes | str | bytearray | memoryview]) bytes[source]¶
Aggregate the public nonces of round 1 into the 66-byte aggnonce.
BIP445’s NonceAgg.
musig2.nonce_agg_var’s own construction, checked line by line against BIP445’s reference rather than assumed identical from the name: both sum each half of the pair independently and serialize a half that sums to infinity as the 33-zero-byte placeholder, refusing that from one signer alone would let it stop the session at will.
An empty list is refused: BIP445 asks for at least one nonce.
- ecc.frost.nonce_gen(sec_share: bytes | str | bytearray | memoryview | None = None, signer_id: int | None = None, pub_share: bytes | str | bytearray | memoryview | None = None, thresh_pk_xonly: bytes | str | bytearray | memoryview | None = None, msg: bytes | str | bytearray | memoryview | None = None, extra_in: bytes | str | bytearray | memoryview | None = None) tuple[bytearray, bytes][source]¶
Return the (secnonce, pubnonce) pair of one signer.
Fresh randomness is drawn here;
nonce_gen_is the spelling that takes it, for the test vectors.
- ecc.frost.nonce_gen_(rand_: bytes | str | bytearray | memoryview, sec_share: bytes | str | bytearray | memoryview | None, signer_id: int | None, pub_share: bytes | str | bytearray | memoryview | None, thresh_pk_xonly: bytes | str | bytearray | memoryview | None = None, msg: bytes | str | bytearray | memoryview | None = None, extra_in: bytes | str | bytearray | memoryview | None = None) tuple[bytearray, bytes][source]¶
Return the (secnonce, pubnonce) pair of one signer, given
rand_.musig2.nonce_gen_’s own reasons for the trailing underscore and for the returned secnonce being a mutable bytearray hold identically here. Where MuSig2’s secnonce also carries the signer’s own plain public key – 97 bytes, so sign can check it was not swapped – FROST’s is 64 bytes exactly, k_1 || k_2 and nothing else: BIP445 defines no such check, and this module raises nothing this pair does not already answer.
Every optional argument beside rand_ is a defense-in-depth measure and none, sec_share included, is required. msg is the one whose absence differs from an empty value: absent hashes as a single mode byte, present as the other mode byte followed by the length and the bytes. An absent extra_in and an empty one are the same input, and sec_share, signer_id, pub_share and thresh_pk_xonly have no empty value at all, each being of a fixed size.
- ecc.frost.partial_sig_agg(psigs: Sequence[bytes | str | bytearray | memoryview], session_ctx: SessionContext) Sig[source]¶
Aggregate the partial signatures into one BIP340 signature.
musig2.partial_sig_agg’s own reason for answering an ssa.Sig rather than 64 bytes: the result verifies under the x-only tweaked threshold public key with ecc.ssa.verify_ and with every other BIP340 verifier, and handing back raw bytes would only make the caller parse them again to find out.
- ecc.frost.partial_sig_verify(psig: bytes | str | bytearray | memoryview, pub_nonces: Sequence[bytes | str | bytearray | memoryview], n: int, t: int, ids: Sequence[int], pub_shares: Sequence[bytes | str | bytearray | memoryview], thresh_pk: bytes | str | bytearray | memoryview, tweaks: Sequence[bytes | str | bytearray | memoryview], is_xonly: Sequence[bool], msg: bytes | str | bytearray | memoryview, i: int) bool[source]¶
Verify the partial signature of signer i, against its own nonce.
pub_shares is required here where every other entry point takes it optionally: there is no other way to check a signature against a key it was not given, and siv2r/bip-frost-signing’s python/vectors/test_vectors_summary.md says the same of BIP445’s own reference.
- ecc.frost.partial_sig_verify_(psig: bytes | str | bytearray | memoryview, signer_id: int, pub_nonce: bytes | str | bytearray | memoryview, pub_share: bytes | str | bytearray | memoryview, session_ctx: SessionContext) bool[source]¶
Verify a partial signature against a prepared session context.
partial_sig_verify is the other spelling: it aggregates the public nonces itself. No bindings back this, unlike musig2’s: BIP445 has no C implementation this library wraps, so every case here is the Python arithmetic and stays that way.
- ecc.frost.session_values(session_ctx: SessionContext) SessionValues[source]¶
Derive the session values from the context, as every party does.
Memoized on session_ctx, for musig2.session_values’s own reason: sign, partial_sig_verify_ and partial_sig_agg each call this once per signer a session touches, and the tweaked threshold public key does not change between them.
A session_ctx that is not a SessionContext is refused here as EccTypeError, rather than leaking an AttributeError from the first field read (issue btclib-org/ecc#367).
- ecc.frost.sign(sec_nonce: bytearray, sec_share: bytes | str | bytearray | memoryview, signer_id: int, session_ctx: SessionContext) bytes[source]¶
Return the 32-byte partial signature of one signer.
The secnonce is consumed exactly as musig2.sign consumes its own: zeroed the moment it is read, before either half is used, so calling this twice with the same bytearray raises “out of range” instead of handing out the secret share.
The lengths of the secnonce and the secshare are checked, and session_values runs, before the nonce is read, for the reason musig2.sign’s own docstring gives: a call refused before the read leaves the bytearray untouched, so it may be used again.
sec_nonce is checked exactly bytearray first, for the reason musig2.sign’s own docstring gives (issue btclib-org/ecc#11).
- ecc.frost.thresh_pubkey_and_tweak_var(thresh_pk: bytes | str | bytearray | memoryview, tweaks: Sequence[bytes | str | bytearray | memoryview], is_xonly: Sequence[bool]) TweakContext[source]¶
Build a TweakContext and apply the tweaks in order.
BIP445’s TweakCtxInit followed by ApplyTweak for each tweak.
- ecc.frost.tweak_ctx_init_var(thresh_pk: bytes | str | bytearray | memoryview) TweakContext[source]¶
Return a fresh TweakContext over an untweaked threshold public key.
BIP445’s TweakCtxInit.
- ecc.frost.validate_threshold_info(info: ThresholdInfo) None[source]¶
Check a ThresholdInfo’s own shares reconstruct its own key.
Passing this is functional compatibility with the signing protocol, not a statement about the key generation protocol that produced the shares: a dealer or a DKG that already colluded against the group could still pass it, having every incentive to (BIP445’s own warning). What it rules out is key material that cannot be signed with consistently – shares that do not sit on one polynomial, or that sit on one whose constant term is not the given threshold public key.
ecc.hashes module¶
The hashing the schemes do.
BIP340’s tagged hash, SEC 1’s reduction of a message to one digest, and the check that a hash function is a constructor. Each is hashlib and nothing else.
- ecc.hashes.reduce_to_hlen(msg: bytes | str | bytearray | memoryview, hf: Callable[[], ~ecc.alias.HashObject]=<built-in function openssl_sha256>) bytes[source]¶
Return the message digested by hf, one digest long.
Step 4 of SEC 1 v.2 section 4.1.3: what the un-underscored signing and verifying spellings do to a message before handing it to their trailing-underscore twins.
- ecc.hashes.tagged_hash(tag: bytes, m: bytes, hf: Callable[[], ~ecc.alias.HashObject]=<built-in function openssl_sha256>) bytes[source]¶
Return BIP340’s tagged hash: hf(hf(tag) || hf(tag) || m).
The doubled tag digest is what makes a hash under one tag invalid under every other.
tag and m are bytes and not Octets: unlike every octets parameter this package converts through bytes_from_octets, neither is ever meant to be read as a hex-string, so assert_type is what refuses a caller’s mistake here rather than a coercion (issue btclib-org/ecc#11).
ecc.kdf module¶
Key derivation functions: SEC 1’s ANSI-X9.63-KDF and RFC 5869’s HKDF.
A KDF stretches one secret into keying data of the length a protocol asks for, under a hash function the caller chooses. ansi_x9_63_kdf is SEC 1’s, one hash per block over a four-octet counter, and ecc.dh.diffie_hellman is the agreement built on it. hkdf is RFC 5869’s, extract then expand, and it is the one piece of BIP324’s key schedule this package implements: a construction over a hash rather than over a cipher, which is the near side of the line ecc.ellswift draws and ecc.ecies argues.
Both are hashlib and hmac and nothing else: no elliptic curve, and no bitcoin.
Why they are here rather than beside the agreement that uses one (issue btclib-org/btclib#1080). SEC 1 defines ANSI-X9.63-KDF in section 3.6.1 for the agreement of section 6.1, so ecc.dh is where it reads naturally, and with one KDF in the tree that costs nothing. HKDF has no such tie – RFC 5869 has no Diffie-Hellman in it, and nothing that wants HKDF here holds a public key – so a drawer among the schemes leaves either a KDF reached through a scheme or two KDFs in two places for the next one to have to choose between. One drawer, at the layer its contents belong to, is the answer to both; btclib-org/btclib’s RELEASE_NOTES.md has the spellings it costs.
- ecc.kdf.ansi_x9_63_kdf(z: bytes, size: int, hf: Callable[[], HashObject], shared_info: bytes | None) bytes[source]¶
Return keying data according to ANSI-X9.63-KDF.
Return a keying data octet sequence of the requested size according to ANSI-X9.63-KDF specifications for the key derivation function.
size is a positive number of octets, SEC 1’s keydatalen: an EccTypeError if it is no integer, an EccValueError if it is zero, negative, or above what the hash function can derive.
http://www.secg.org/sec1-v2.pdf, section 3.6.1
z and shared_info are bytes and not Octets: assert_type is what refuses a caller’s mistake here, hf never reaching a bare hf() on something that is not callable (issue btclib-org/ecc#11).
- ecc.kdf.hkdf(ikm: bytes, size: int, hf: Callable[[], HashObject], salt: bytes | None, info: bytes | None) bytes[source]¶
Return keying data according to HKDF, extract then expand.
The composition of the two steps above, which is how RFC 5869 is used unless the caller already holds a uniformly random key: extract concentrates the entropy of ikm under salt, expand stretches the result to size octets under info.
https://www.rfc-editor.org/rfc/rfc5869.html, section 2
- ecc.kdf.hkdf_expand(prk: bytes, size: int, hf: Callable[[], HashObject], info: bytes | None) bytes[source]¶
Return output keying material of the requested size.
prk is a pseudorandom key, hkdf_extract’s answer or a uniformly random key of at least a digest’s length. info is optional context – a protocol label, a party’s identity – which binds the output to an application, so that one prk yields independent keys.
size is a positive number of octets, RFC 5869’s L: an EccTypeError if it is no integer, an EccValueError if it is zero, negative, or above 255 digests. A prk below a digest is an EccValueError too.
https://www.rfc-editor.org/rfc/rfc5869.html, section 2.3
prk and info are bytes and not Octets, for the reason ansi_x9_63_kdf’s docstring gives (issue btclib-org/ecc#11).
- ecc.kdf.hkdf_extract(ikm: bytes, salt: bytes | None, hf: Callable[[], HashObject]) bytes[source]¶
Return the pseudorandom key of HKDF’s extract step.
ikm is the input keying material, which need not be uniformly distributed – concentrating whatever entropy it has into one digest is what this step is for. salt is an optional non-secret value, which may be reused. The answer is one digest of hf, and is what hkdf_expand takes as its prk.
https://www.rfc-editor.org/rfc/rfc5869.html, section 2.2
An ikm that is already a uniformly random key of full length does not need this step: RFC 5869 section 3.3 says to skip it and expand that key directly, which hkdf_expand is separately public for.
ikm and salt are bytes and not Octets, for the reason ansi_x9_63_kdf’s docstring gives (issue btclib-org/ecc#11).
ecc.musig2 module¶
MuSig2 key and signature aggregation, according to BIP327.
https://github.com/bitcoin/bips/blob/master/bip-0327.mediawiki
MuSig2 turns many signers into one: their public keys aggregate into a single x-only key Q, and their partial signatures into a single BIP340 signature that verifies under Q with ecc.ssa and with any other BIP340 verifier. A verifier – and the chain – sees an ordinary single-key Schnorr signature, which is where the privacy and the size come from.
What is offered here is a primitive per round, not a function that signs, because signing is interactive and no library call can be: the signers exchange nonces, then partial signatures, and each exchange is somebody else’s network. The rounds are
key aggregation, once per group: key_sort, key_agg_var, apply_tweak_var and key_agg_and_tweak_var, answering a KeyAggContext;
round 1, the nonces: nonce_gen for each signer, then nonce_agg_var over what they published, which anyone can do;
round 2, the partial signatures: sign (or deterministic_sign) for each signer, partial_sig_verify to hold a signer to what it sent, and partial_sig_agg for the aggregate signature.
The nonce is a pair of points, and that is the whole of what MuSig2 adds to its predecessor. A one-round scheme in which each signer publishes a single R_i is broken by Wagner’s generalized birthday attack: an adversary opening many concurrent sessions solves for a forgery on a message nobody signed. Committing two points and combining them as R_1 + b*R_2, with b a hash of the aggregate nonce, the aggregate key and the message, makes the effective nonce depend on values the adversary cannot fix in advance – and buys back the round that the earlier commit-then-reveal defence had to spend. That defence was itself a correction: MuSig’s first proposal had two rounds and no nonce commitment, and it was withdrawn once a signer choosing its nonce adaptively – after seeing everyone else’s – was shown to defeat the proof.
Key aggregation is not a plain sum either: sum(P_i) lets a rogue signer publish P_n = P - sum(P_1..P_n-1) for a P it controls, and sign alone for the group. Each key therefore enters with a coefficient a_i = hash(L, P_i), L being the hash of the whole list, so no signer can choose its key knowing the others’.
Tweaking is what makes the aggregate key usable: a plain tweak is BIP32 derivation on top of the group key, an x-only tweak is a BIP341 taproot commitment. KeyAggContext carries gacc and tacc across tweaks – the accumulated negation and the accumulated tweak – so that the partial signatures still add up to a signature valid under the tweaked key.
secp256k1 and sha256 are not parameters here, unlike in dsa and ssa. BIP327 is defined for that pair alone: the tags below, the 33-byte compressed points, the 32-byte scalars and the 66-byte nonces are its serialization, and there is no other curve for which a test vector exists. A Curve argument would advertise a genericity the specification does not define and no vector could check.
The message is of any size, as in ecc.ssa: BIP327 states no restriction, and two of its own vectors are an empty message and a 38-byte one.
One rule outlives every abstraction here: a secret nonce signs once. Two signatures under one secnonce hand out the private key by elementary algebra, which is why sign zeroes the bytearray it is given rather than merely reading it.
Adaptor signatures are the one capability BIP327 does not cover: SessionContext.adaptor carries a public point T, and partial_sig_agg_adaptor – the spelling for a session that carries one, partial_sig_agg itself refusing that session – answers a pre-signature, a PreSignature rather than an ssa.Sig because it does not verify, that adapt turns into a real signature given the secret t behind T, or that extract_adaptor recovers t from, given both the pre-signature and the signature adapt produced. Splitting the entry point rather than widening partial_sig_agg’s return type keeps every existing caller narrowing nothing, and matches where the C library is going: BlockstreamResearch/secp256k1-zkp#330 is splitting musig_nonce_process the same way, back to five arguments plus a separate musig_nonce_process_adaptor, so the base API stays uncontaminated by a capability most callers never touch. A DLC’s contract execution transaction, an atomic swap across two chains and a payment channel’s revocation all build on the same trade: whoever completes the signature is whoever knew t, and revealing the signature beside its pre-signature reveals t to anyone holding both.
There is no BIP for this and no vector file, so the construction is read off secp256k1-zkp, the de facto specification: https://github.com/BlockstreamResearch/secp256k1-zkp/blob/master/src/modules/musig/session_impl.h and …/adaptor_impl.h beside it. Two details are where an implementation goes wrong and zkp does not: T is added to R_1 before b is hashed, not to the final R afterward, so the challenge itself commits to T and it cannot be swapped in once b is fixed; and adapt negates t exactly when the final nonce has odd y, because the pre-signature’s nonce is then really -(r+t)*G rather than (r+t)*G – adaptor_impl.h’s own comment gives this reasoning in full.
This construction is cross-validated against secp256k1-zkp’s own musig module, btclib_secp256k1.zkp.musig, in tests/schemes/musig2_test.py: partial_sig_agg_adaptor, adapt and extract_adaptor against its Session.partial_sig_agg, adapt and extract_adaptor, both directions and both parities of the final nonce, over fresh keys and an adaptor secret each run. zkp.musig.Session.nonce_parity() is what zkp asks its caller to track and pass to its own adapt/extract_adaptor; session_values(session_ctx).R[1] % 2 is the identical bit, derived here instead of carried by the caller, and the two are asserted equal before anything downstream of it is compared. Mainline bitcoin-core/secp256k1, _bindings_session’s own btclib_secp256k1.musig, still has no adaptor support at all – which is why a session that carries one takes the Python arm in partial_sig_verify_ below regardless of whether the bindings would otherwise serve – so this cross-validation is against zkp specifically, the one implementation this module has to check against for the capability BIP327 itself does not cover.
- class ecc.musig2.KeyAggContext(Q: tuple[int, int], gacc: int, tacc: int)[source]¶
Bases:
objectThe aggregate public key, and what tweaking it has accumulated.
Q is the aggregate point, tweaks included
gacc is the product of the negations x-only tweaking has forced, 1 or n-1: a signer multiplies its key by it, so that the partial signatures add up under the even-y Q a BIP340 verifier assumes
tacc is the sum of the tweaks, which partial_sig_agg adds in once for the group rather than each signer adding a share of it
- class ecc.musig2.PreSignature(r: int, s: int)[source]¶
Bases:
objectA MuSig2 pre-signature: (x_R, s_pre), over a session with an adaptor.
partial_sig_agg_adaptor answers one of these; ssa.Sig’s whole point is a value that verifies, and a pre-signature does not, until adapt supplies the secret behind the adaptor. Keeping it a different class from ssa.Sig, rather than the same class either call might answer, is what stops a caller from handing this to ssa.verify_ and reading a failure as “bad signature” rather than “not adapted yet” – and what keeps partial_sig_agg’s own return type exactly what it already was. It carries no serialize/parse of its own: BIP373 has no field for a pre-signature (ecc.musig2’s own docstring says so), so there is no wire format to answer to yet, and one invented here would have no caller to check it against.
- class ecc.musig2.SessionContext(agg_nonce: bytes | str | bytearray | memoryview, pub_keys: Sequence[bytes | str | bytearray | memoryview], tweaks: Sequence[bytes | str | bytearray | memoryview], is_xonly: Sequence[bool], msg: bytes | str | bytearray | memoryview, adaptor: bytes | str | bytearray | memoryview | None = None)[source]¶
Bases:
objectEverything the signers of one session have to agree on.
The aggregate nonce, the public keys in the order they aggregate in, the tweaks with their kinds, and the message. Two signers with different session contexts produce partial signatures that do not add up, which partial_sig_verify is there to catch.
adaptor is None for an ordinary session and a 33-byte compressed point T for an adaptor one – session data every party signs under, not an argument to sign, since a signer that did not agree to T must not sign a pre-signature it becomes valid for. Optional and last, so an existing positional call is still five arguments and still means what it meant.
- class ecc.musig2.SessionValues(Q: tuple[int, int], gacc: int, tacc: int, b: int, R: tuple[int, int], e: int, L: bytes, second: bytes, pub_keys_set: frozenset[bytes])[source]¶
Bases:
objectWhat every party derives from a SessionContext before signing.
Q, gacc and tacc are the aggregate key and its tweak accumulators
b is the coefficient combining the two halves of the nonce
R is the effective nonce point, R_1 + b*R_2
e is the BIP340 challenge, over R, Q and the message
L, second and pub_keys_set are what a per-signer key-aggregation coefficient is computed from – issue btclib-org/btclib#1069, the same shape btclib-org/btclib#1045 solved for the fields above: fixed for the session, so computed here once rather than by every one of _session_key_agg_coeff’s 2n callers
- ecc.musig2.adapt(pre_sig: PreSignature, t: bytes | str | bytearray | memoryview | int, session_ctx: SessionContext) Sig[source]¶
Complete a pre-signature into a signature, given the secret adaptor.
session_ctx is the session pre_sig was built from – the same one partial_sig_agg_adaptor took – because the one bit this needs and does not carry on its own is the parity session_values already derived, R[1] % 2 on the final nonce R = R_1 + T + b*R_2.
BIP340 signs the even-y nonce; when the parity is odd, the pre-signature’s x-only nonce is really that of -(r+t)*G rather than (r+t)*G, so completing it needs -t rather than t. secp256k1_musig_adapt (adaptor_impl.h) is the source of this and negates the same way, and is also this function’s authority: no BIP or vector file covers adaptor signatures.
Nothing here re-verifies the result: a wrong t returns a Sig that fails ssa.assert_as_valid_, which is the caller’s to run against the aggregate key it already has.
- ecc.musig2.apply_tweak_var(key_agg_ctx: KeyAggContext, tweak: bytes | str | bytearray | memoryview, is_xonly: bool) KeyAggContext[source]¶
Return the context tweaked by t, x-only or plain: BIP327’s ApplyTweak.
An x-only tweak is a BIP341 taproot commitment: it applies to the even-y point, so an odd-y Q is negated first and the negation is accumulated in gacc for the signers to apply to their keys. A plain tweak is BIP32 derivation on the group key, and takes Q as it is.
Which of the two is a bool and nothing else: the line below reads it beside the parity of Q, so a value read for its truth would tweak an odd-y key the other way and answer another aggregate key.
- ecc.musig2.deterministic_sign(prv_key: bytes | str | bytearray | memoryview | int, agg_other_nonce: bytes | str | bytearray | memoryview, pub_keys: Sequence[bytes | str | bytearray | memoryview], tweaks: Sequence[bytes | str | bytearray | memoryview], is_xonly: Sequence[bool], msg: bytes | str | bytearray | memoryview, rand: bytes | str | bytearray | memoryview | None = None) tuple[bytes, bytes][source]¶
Return the (pubnonce, partial signature) of a signer with no RNG.
The two rounds collapse into one for the last signer to act: given the aggregate of everybody else’s nonces, it derives its own from the secret key and the session rather than from randomness, and publishes nonce and partial signature together. That is the answer for a signing device that has no entropy source, and it is safe exactly once per (key, session): the derivation is a function of its inputs, so signing twice over different other-nonces with the same key is what a deterministic scheme must not do – pass rand when there is any doubt.
- ecc.musig2.extract_adaptor(sig: Sig, pre_sig: PreSignature, session_ctx: SessionContext) int[source]¶
Return the secret adaptor a signature reveals against its pre-signature.
The inverse of adapt, over the same session_ctx: whoever holds a valid signature and the pre-signature it was adapted from recovers exactly the t that adapt consumed. That is the second half of what makes an adaptor signature useful – releasing the signature is releasing the secret – and it is why sig is not checked against the aggregate key here: extraction is arithmetic on two scalars, and a sig that does not verify still yields the t that would make it the one adapt would have produced from a matching pre_sig.
zkp.musig’s extract_adaptor answers the same t as 32 big-endian bytes.
- ecc.musig2.individual_pub_key(prv_key: bytes | str | bytearray | memoryview | int) bytes[source]¶
Return the plain (33-byte, compressed) public key of a signer.
- ecc.musig2.key_agg_and_tweak_var(pub_keys: Sequence[bytes | str | bytearray | memoryview], tweaks: Sequence[bytes | str | bytearray | memoryview], is_xonly: Sequence[bool]) KeyAggContext[source]¶
Aggregate the keys, then apply the tweaks: KeyAgg, then ApplyTweak.
- ecc.musig2.key_agg_var(pub_keys: Sequence[bytes | str | bytearray | memoryview]) KeyAggContext[source]¶
Aggregate plain public keys into a KeyAggContext: BIP327’s KeyAgg.
The order of the list is part of the key: aggregate the same keys in another order and the group is another group. key_sort is the usual way to agree on one.
BIP327 asks for at least one key, so an empty list is refused.
- ecc.musig2.key_sort(pub_keys: Sequence[bytes | str | bytearray | memoryview]) list[bytes][source]¶
Return the public keys in lexicographic order.
The order the signers agree on is theirs to choose – key aggregation commits to the list as given, and a different order is a different aggregate key – but sorting is the convention that lets a group reach the same key without a further round.
A new list, where BIP327’s reference sorts in place: a function that reorders its argument makes the key of whoever kept a reference to that list change under them.
- ecc.musig2.nonce_agg_var(pub_nonces: Sequence[bytes | str | bytearray | memoryview]) bytes[source]¶
Aggregate the public nonces of round 1 into the 66-byte aggnonce.
BIP327’s NonceAgg.
Anybody can do this – there is no secret in it – and a signer that disagrees with the result is free to recompute it: the aggregate nonce is checked by the signature it produces.
Either half can come out the infinity point, which is where the 33-zero-byte placeholder comes from: refusing it here would let one signer, by publishing the negation of what the others published, stop the session at will.
An empty list is refused: BIP327 asks for at least one nonce.
- ecc.musig2.nonce_gen(prv_key: bytes | str | bytearray | memoryview | int | None, pub_key: bytes | str | bytearray | memoryview, agg_x_only_pub_key: bytes | str | bytearray | memoryview | None = None, msg: bytes | str | bytearray | memoryview | None = None, extra_in: bytes | str | bytearray | memoryview | None = None) tuple[bytearray, bytes][source]¶
Return the (secnonce, pubnonce) pair of one signer.
Fresh randomness is drawn here;
nonce_gen_is the spelling that takes it, for the test vectors.
- ecc.musig2.nonce_gen_(rand_: bytes | str | bytearray | memoryview, prv_key: bytes | str | bytearray | memoryview | int | None, pub_key: bytes | str | bytearray | memoryview, agg_x_only_pub_key: bytes | str | bytearray | memoryview | None = None, msg: bytes | str | bytearray | memoryview | None = None, extra_in: bytes | str | bytearray | memoryview | None = None) tuple[bytearray, bytes][source]¶
Return the (secnonce, pubnonce) pair of one signer, given
rand_.Double backticks because rst reads a trailing underscore as a link reference: bare, that name makes sphinx -W fail with ‘Unknown target name: “rand”’.
The randomness is the argument, which is what makes BIP327’s nonce vectors reproducible; nonce_gen is the spelling that draws it, and is the one to call. That is this package’s trailing underscore again: whether the caller prepared the input or the library does it.
Every other input is optional and every one of them is a defence: the private key masks the randomness, so that a broken RNG alone does not repeat a nonce; the aggregate key, the message and extra_in (a counter, a clock) separate a nonce from the nonce of another session. None is not the empty value – an absent message and an empty message are different inputs to the hash, by a prefix byte – so pass what is known and leave the rest out.
The returned secnonce is a bytearray, and mutable on purpose: sign zeroes it, which is the only mechanism here that can stop the same nonce signing twice.
- ecc.musig2.partial_sig_agg(psigs: Sequence[bytes | str | bytearray | memoryview], session_ctx: SessionContext) Sig[source]¶
Aggregate the partial signatures into one BIP340 signature.
An ssa.Sig, because that is what it is: the result verifies under the x-only aggregate key with ecc.ssa.verify_ and with every other BIP340 verifier, and handing back 64 bytes would only make the caller parse them again to find out.
Refuses a session that carries an adaptor: the sum is still missing the adaptor’s secret, so it does not verify, and answering an ssa.Sig for it would claim otherwise – partial_sig_agg_adaptor is the spelling for that session, answering a PreSignature instead.
- ecc.musig2.partial_sig_agg_adaptor(psigs: Sequence[bytes | str | bytearray | memoryview], session_ctx: SessionContext) PreSignature[source]¶
Aggregate the partial signatures into a MuSig2 pre-signature.
partial_sig_agg’s own arithmetic, over a session that carries an adaptor: the sum does not verify until adapt supplies the secret behind it, which is what makes the result a PreSignature and not an ssa.Sig.
Refuses a session with no adaptor – partial_sig_agg is the spelling for that one, answering an ssa.Sig the sum already is.
- ecc.musig2.partial_sig_verify(psig: bytes | str | bytearray | memoryview, pub_nonces: Sequence[bytes | str | bytearray | memoryview], pub_keys: Sequence[bytes | str | bytearray | memoryview], tweaks: Sequence[bytes | str | bytearray | memoryview], is_xonly: Sequence[bool], msg: bytes | str | bytearray | memoryview, i: int) bool[source]¶
Verify the partial signature of signer i, against its own nonce.
Every signer should verify every other signer’s partial signature before aggregating: an aggregate signature that does not verify says only that somebody misbehaved, while this says who.
This spelling builds a fresh SessionContext on every call, so session_values’s memoization buys it nothing: calling this once per signer re-aggregates the keys once per signer. A caller that verifies every signer of one session should build the SessionContext once and call partial_sig_verify_ with it instead, which is what shares the cached session values across those calls.
- ecc.musig2.partial_sig_verify_(psig: bytes | str | bytearray | memoryview, pub_nonce: bytes | str | bytearray | memoryview, pub_key: bytes | str | bytearray | memoryview, session_ctx: SessionContext) bool[source]¶
Verify a partial signature against a prepared session context.
partial_sig_verify is the other spelling: it aggregates the public nonces itself, which is what this package’s trailing underscore distinguishes – whether the caller prepared the input or the library does it.
Delegated to btclib_secp256k1.musig for secp256k1, sha256, a 32-byte message and a session with no adaptor (issue btclib-org/btclib#1049): the three point multiplications below are what a signer pays once per session and a verifier once per signer it checks, and are 3.7x slower here than in the bindings. secp256k1 and sha256 are this module’s only pair (its own docstring), so _libsecp256k1_serves reduces to whether the bindings are installed and enabled. musig_nonce_process takes a fixed 32-byte msg32 with no length parameter – the shape of issue btclib-org/btclib#169 without the sign_custom that resolved it there, and BIP327’s own empty-message and 38-byte-message vectors are what keeps this Python arm live and validated for a message of any size. The bindings have no adaptor extension at all (this module’s own docstring’s paragraph on cross-validating against zkp), so a session that carries one takes the Python arm regardless of the other two conditions, or raises where the bindings are required (curves.set_libsecp256k1_serving). Nothing else in this module is delegated – key_agg_var, key_sort and nonce_agg_var measured too close to their Python cost, or run once per session already, to be worth a second code path.
The signer’s pubkey has to be one of the session’s – a membership test with no C equivalent, musig_partial_sig_verify answering a verdict for whatever pubkey it is given rather than asking whether it was ever aggregated. _session_key_agg_coeff is what checks it, and the delegated arm below calls it for that check alone, discarding the coefficient the bindings compute on their own – after the delegated call, once C has parsed pub_nonce and pub_key without refusing them, exactly where the Python arm below checks it once it has computed P from the same two. Both arms therefore refuse a key that is malformed the same way – the parse failure, whichever arm’s parse finds it – and a key that is well-formed but foreign the same other way, _SIGNER_PK_ERR, so which EccValueError a caller sees does not depend on which arm answered it.
- ecc.musig2.session_values(session_ctx: SessionContext) SessionValues[source]¶
Derive the session values from the context, as every party does.
Memoized on session_ctx: sign, partial_sig_verify_ and whichever of partial_sig_agg or partial_sig_agg_adaptor a session uses each call this once per signer, so one session that signs, verifies every partial signature and aggregates would otherwise run the O(n) key aggregation below 2n+1 times over inputs that never change – one call to aggregate either way, the two never both reaching the same session. SessionContext._values is declared compare=False, so it is excluded from the dataclass’s generated __eq__ and __hash__ by construction, and two contexts spelling the same session remain equal regardless of which one has already been used to sign; object.__setattr__ reaches past frozen=True to set it, exactly as SessionContext.__init__ already does for its declared fields.
L, second and pub_keys_set are computed here too, once, and not because deriving them needs anything above – they are pure functions of session_ctx.pub_keys alone, the same as key_agg_var’s own L and second, which it likewise computes once and reuses for every key. Computed again here rather than threaded out of key_agg_and_tweak_var above: doing that would widen KeyAggContext, which callers outside this module already read, for a value only _session_key_agg_coeff wants. What earns them a place on SessionValues instead of a second cached field on SessionContext is that every caller of _session_key_agg_coeff – sign and partial_sig_verify_ – already calls this function first, so a SessionValues field costs nothing beyond what assembling the session already paid for.
A session_ctx that is not a SessionContext is refused here as EccTypeError, rather than leaking an AttributeError from the first field read (issue btclib-org/ecc#367).
- ecc.musig2.sign(sec_nonce: bytearray, prv_key: bytes | str | bytearray | memoryview | int, session_ctx: SessionContext) bytes[source]¶
Return the 32-byte partial signature of one signer.
The secnonce is consumed: its first 64 bytes are zeroed the moment they are read, before either is used for anything, so that calling this twice with the same bytearray reads two zero scalars and raises “out of range” instead of handing out the private key. That is why the argument is a bytearray and not bytes – an immutable secnonce is one nothing can spend – and why a caller must not keep a copy.
session_values runs first, and that is the one thing this order leaves spendable: a session that does not assemble – a pubnonce that is no point, a tweak out of range – raises before the nonce is touched, so the same bytearray may be used for the corrected session. Nothing was signed with it, which is what makes reuse safe there and only there, and BIP327’s reference implementation has the two calls in this order for the same reason.
sec_nonce is checked exactly bytearray, ahead of session_values and of every read below: it is never zeroed and never spent, so a caller handed a clean EccTypeError for a bytes or a list may simply retry with the bytearray this takes (issue btclib-org/ecc#11).
ecc.number_theory module¶
Number theory and modular arithmetic functions.
Implementations originally from https://en.wikibooks.org/wiki/Algorithm_Implementation/Mathematics/Extended_Euclidean_algorithm and https://codereview.stackexchange.com/questions/43210/tonelli_var-shanks-algorithm-implementation-of-prime-modular-square-root/43267 with the following modifications:
type annotated Python3
minor improvements
added extensive unit test
- ecc.number_theory.legendre_symbol_var(a: Public, p: int) int[source]¶
Compute the Legendre symbol a|p, as a binary Jacobi symbol.
p is a prime, a is relatively prime to p (if p divides a, then a|p = 0). It returns 1 if a has a square root modulo p, -1 otherwise. The Jacobi symbol is what is computed, and for a prime modulus the two are the same number.
By the reciprocity recursion rather than by Euler’s criterion, which is pow(a, (p - 1) // 2, p) – an exponentiation the size of the square root the caller is asking about, where this is a gcd. The factors of two come out all at once, a & -a being the lowest set bit, which is libsecp256k1’s secp256k1_ctz64_var; asking a gcd rather than an exponent is what its secp256k1_fe_is_square_var does, through secp256k1_jacobi64_maybe_var and never through a power. Its own recursion is the safegcd one, which in bytecode loses as every safegcd does – curves.curve_group_2 keeps the list.
The loop’s length follows a, where an exponentiation’s did not. SECURITY.md publishes the Python path as variable-time, and nothing in the tree asks this about a value that has to stay hidden. curves.curve._is_x_coordinate_var reaches it for a signature’s r, the x-coordinate of a serialized xpub, or the -x-u that an ElligatorSwift encoding is tested against, and ecc.ellswift reaches it directly for the fractions and radicands its map and its inverse test, each of them public and on secp256k1 each answered by the bindings before this is reached; ecc.pedersen and ecc.rangeproof reach it for the residuosity octet of a point they are about to write down.
- ecc.number_theory.mod_inv(a: int, m: int) int[source]¶
Return the inverse of a (mod m), timed on a random value instead.
What mod_inv_var is for an operand that is public, this is for one that is secret. The extended Euclid under that one takes the iterations its input asks for, and for an operand drawn uniformly below m what its duration carries is the operand’s bit-length: on secp256k1’s order a 256-bit scalar takes longer than a 128-bit one, falling at every step between them. That correlation is what the Minerva attack collects – an ECDSA nonce is such a scalar, and a few thousand signing times sorted by it are a lattice away from the private key.
(b*a)^-1 * b is a^-1 for every b invertible mod m, so drawing b at random leaves an inverse whose iteration count follows b and tells an observer nothing about a. What it adds is two multiplications, two reductions and a draw from secrets. Fermat’s pow(a, m - 2, m) is the alternative and is flat for a different reason, its ladder running on the fixed exponent rather than on a random operand; it is not the one chosen.
The draw is what costs, so a small modulus pays proportionally more: on an order of 11 the Euclid is a few iterations and secrets is the same syscall. Nothing signs with such an order outside the test suite.
Not a constant-time inverse, and no more claiming to be one than curve_group._blinded_jac does: the duration is still an extended Euclid’s and still visible, and what the blinding changes is whose it is. SECURITY.md publishes the Python path as variable-time, and CONTRIBUTING.md has what a name in this library does and does not promise about duration.
m does not have to be a prime, and the blinding holds for any m. The factor’s gcd with m, a second Euclid timed on the secret factor, is taken only when the product has no inverse: a factor that is not a unit, which only a composite m draws, is then redrawn, and an operand with no inverse raises. The expected number of draws is (m - 1) / phi(m). mod_inv_var is never handed the operand itself.
- ecc.number_theory.mod_inv_batch(a: Sequence[int], m: int) list[int][source]¶
Return every inverse, each timed on a random value instead.
What mod_inv is to mod_inv_var, this is to mod_inv_batch_var: the twin a secret may be handed. Montgomery’s trick inverts one running product for the whole sequence, so the single extended Euclid it spends is timed on a value every element went into – which is the channel, the same one and no smaller for being shared.
Each element is blinded with a factor of its own rather than the sequence with one: inv(a_i * b_i) * b_i is inv(a_i), and the product the batch forms is then a product of blinded values. One factor for all of them would blind that product and leave the ratios a_i / a_j in the peeled-back inverses, which is what the running products are made of.
So it costs n draws from secrets and 2n multiplications on top of the trick.
It is still the trick, which is the point of it being a batch at all: one extended Euclid for the whole sequence, where mod_inv on each element spends one each.
On a composite modulus a factor can be a zero divisor, and an element can have no inverse: either fails the batch, which is then answered by mod_inv on each element in turn, so the cost is at worst n independent calls.
Not constant-time, for the reasons mod_inv gives at length. The empty sequence is not an error here either.
- ecc.number_theory.mod_inv_batch_var(a: Sequence[Public], m: int) list[int][source]¶
Return the inverse of every element of a (mod m), in its order.
m does not have to be a prime, and every element has to be invertible modulo it, as mod_inv_var requires of its one operand.
Montgomery’s trick, which libsecp256k1 runs inside secp256k1_ge_set_all_gej_var: the running products a[0], a[0]*a[1], …, a[0]*…*a[n-1] are formed, the last of them is inverted once, and the individual inverses are peeled back off it. So n inverses cost one inverse and 3(n-1) products, where n calls to mod_inv_var are n extended Euclids – an inverse modulo a 256-bit prime being, on an int, some thirty times a product.
An empty sequence has no inverses and is not an error: it is what a caller that filtered its own input is left with.
- ecc.number_theory.mod_inv_var(a: Public, m: int) int[source]¶
Return the inverse of a (mod m).
m does not have to be a prime.
pow(a, -1, m) is an Extended Euclidean Algorithm too: on an int, CPython’s long_invmod, which is the loop xgcd_var runs with the second cofactor dropped; on gmpy2’s mpz, GMP’s mpz_invert. Either way it delegates the algorithm to C rather than interpreting it, and that is the whole of why it is called. It is not a constant-time inverse and neither was the bytecode one; SECURITY.md publishes the Python path as variable-time.
Its duration follows the operand, so a secret one goes to mod_inv instead: what that duration carries is described there, and it is enough to recover a private key from a signature.
What pow does not carry is this module’s contract, so the checks stay above it and the message below it: a non-invertible operand leaves pow as a bare ValueError naming neither operand, and rebuilding the message says more than chaining that one would.
- ecc.number_theory.mod_sqrt_var(a: Public, p: int) int[source]¶
Return a quadratic residue (mod p) of a; p must be a prime.
- Solve the equation:
x^2 = a mod p
and return x; p - x is also a root.
If a simple solution is not available for p, then the Tonelli-Shanks algorithm is used.
ecc.pedersen module¶
Pedersen commitment functions.
In a commitment scheme the committer:
decides (or is given) a secret message v
decides a random secret r
commits to v by applying the public commitment scheme algorithm and producing a commitment C=Commit(r,v)
makes C public
Later, when he reveals r and v, the verifier opens the commitment checking if indeed C=Commit(r,v).
Pedersen commitment uses a public group of large order n in which the discrete logarithm is hard. In the case of an elliptic curve group, the generator G is supplemented with a second random generator H and the commitment algorithm is Commit(r,v)=rG+vH. It is crucial for H to be Nothing-Up-My-Sleeve (NUMS), i.e. the discrete logarithm of H with respect to G must be unknown.
The octets say residuosity, where a SEC prefix says parity. libsecp256k1-zkp writes a commitment as one octet and then x, and that octet reports whether y is a quadratic residue: secp256k1_pedersen_commitment_save writes 9 ^ secp256k1_fe_is_square_var(&ge->y) and secp256k1_generator_serialize writes 11 ^ the same bit, both in src/modules/generator/main_impl.h; secp256k1_rangeproof_serialize_point writes 1 ^ it, in src/modules/rangeproof/rangeproof_impl.h. That third tag is the base ecc.rangeproof hashes a point under and lifts a ring commitment’s x against, and that module reaches the codec below for both rather than carrying one of its own. Lifting such an octet by the parity the 02/03 of curves.point_from_octets_var carries answers the same point for some x and its negation for others, with nothing in the octets to say which reading wrote them, so the two are not one function under two names.
The codec below is secp256k1’s and takes no ec: the tags and the width of x are libsecp256k1-zkp’s format, and that library is secp256k1’s. generator_from_seed takes none for a different reason: the map it runs is written around sqrt(-3) and the curve’s own b, so it is a construction of this field rather than a format read in it. commit takes an ec because rG+v*gen is a sum of points, which every curve has, and takes the generator rather than deriving one because a commitment is only as binding as the generator behind it and a caller with several assets has one per asset.
- ecc.pedersen.assert_as_valid(r: bytes | str | bytearray | memoryview | int, v: bytes | str | bytearray | memoryview | int, commitment: tuple[int, int], gen: 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)) None[source]¶
Refuse a commitment that (r, v) does not open.
The commitment is recomputed and compared; verify is the boolean answer.
The type of the commitment is checked and its value is not: a None compares unequal to every point, so verify reported a commitment of no type at all as one that does not open, where a pair of ints that is no commitment is exactly what False is for (issue btclib-org/btclib#814). is_on_curve is deliberately not asked – that would refuse a wrong value too.
- ecc.pedersen.bytes_from_commitment_var(Q: tuple[int, int]) bytes[source]¶
Return a commitment as libsecp256k1-zkp serializes one.
secp256k1_pedersen_commitment_serialize hands back what _save stored: 08 where the y of the commitment is a quadratic residue and 09 where it is not, then x. curves.bytes_from_point writes the parity of that same y, so the two answer the same octets only where the y that is a square is also the even one.
- ecc.pedersen.bytes_from_generator_var(Q: tuple[int, int]) bytes[source]¶
Return a generator as libsecp256k1-zkp serializes one.
secp256k1_generator_serialize, which is bytes_from_commitment_var’s octet at another tag: 0a for a y that is a quadratic residue and 0b for one that is not. second_generator is the generator this library computes; a blinded asset generator is what else this format carries.
- ecc.pedersen.commit(r: bytes | str | bytearray | memoryview | int, v: bytes | str | bytearray | memoryview | int, gen: 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]¶
Commit to v under blinding factor r, returning rG+v*gen.
gen is the generator the commitment is made under and the caller names it, as secp256k1_pedersen_commit takes its own gen: second_generator is the one this library derives for a curve and a hash function, generator_from_seed the map that derives any other, and nobody can open the commitment to a different (r, v) for as long as log_G(gen) is unknown, which is what both derivations buy.
No hash function, where second_generator takes one: a hash is how a generator is derived and says nothing about a sum of points.
r=0 mod n is refused: it commits with no blinding at all, Q is then v*gen, a point anyone who guesses v can recompute. The check is on r alone and not on its range, because the sum of two blinding factors is a blinding factor too – a Pedersen commitment is additively homomorphic – and is routinely >= ec.n (issue btclib-org/btclib#1250). It also subsumes the former separate check for r and v both landing on INF: with r=0 mod n excluded, Q lands there only if v is 0 mod n too, which is an ordinary commitment to a zero value and not a blinding failure.
The check on r is here and nowhere else in the module: assert_as_valid recomputes the commitment through this function, and verify turns the EccValueError an invalid r raises into False.
gen is refused here, and by verify ahead of its try, when it is off the curve, at INF, or on a curve of cofactor above 1 outside the subgroup generated by G. At INF the commitment is rG for every v, which opens to any value; outside the subgroup n*Q cycles with v, which leaks v mod the cofactor.
- ecc.pedersen.commitment_from_octets_var(octets: bytes | str | bytearray | memoryview) tuple[int, int][source]¶
Return the commitment libsecp256k1-zkp’s octets name.
secp256k1_pedersen_commitment_parse. An Elements output carries these, and this is the lift a caller holding them makes before calling anything that takes the commitment as a point – ecc.rangeproof.RangeProof.pubk_rings_var, assert_as_valid, verify.
- ecc.pedersen.generator_from_octets_var(octets: bytes | str | bytearray | memoryview) tuple[int, int][source]¶
Return the generator libsecp256k1-zkp’s octets name.
secp256k1_generator_parse, whose (input[0] & 0xFE) != 10 refuses a commitment’s own leading octet – which is what test_generator_fixed_vector puts to it in src/modules/generator/tests_impl.h. The x is the same octets either way, so the tag is the whole of what keeps the two formats apart.
- ecc.pedersen.generator_from_seed(seed: bytes | str | bytearray | memoryview, blind: bytes | str | bytearray | memoryview | int | None = None) tuple[int, int][source]¶
Return the generator that seed derives, blinded where asked.
secp256k1_generator_generate_internal: the seed is hashed under two prefixes, each digest is read as a field element and mapped to the curve by _shallue_van_de_woestijne, and the two points are summed. secp256k1_generator.h publishes the result as distributed uniformly over the curve, with no known discrete logarithm with respect to G or to any other generator this answers – which is what a confidential transaction committing each of several assets under its own generator asks for, commit and ecc.rangeproof taking the generator they work at.
blind adds blind*G to that sum, and one function takes it where the C publishes a call per case: secp256k1_generator_generate_blinded at a blinding factor of zero is secp256k1_generator_generate, which is what test_generator_generate asserts of each of its own vectors. A factor at or past n is refused, secp256k1_scalar_set_b32’s overflow being what refuses it there; zero is not, the C answering secp256k1_generator_generate’s generator for it.
The seed and the blinding factor are secrets, as zkp treats them: the generator is published to hide which asset an output carries, and the seed is that asset’s tag. blind*G is curves.mult and each sum curves.curve._add, so on the bindings arm the libsecp256k1 calls they make are constant time in both, and the Python around those calls is not. The map is Python on either arm, and regular in the seed rather than constant time, _shallue_van_de_woestijne says how. Constant time in both is btclib_secp256k1.zkp.generator.generate_blinded, where the bindings are built with the zkp extension.
second_generator is the other generator this module names, and the two are unrelated constructions: that one increments the hash of G until it lands on the curve, where this maps a digest onto it.
source: https://github.com/BlockstreamResearch/secp256k1-zkp/blob/master/src/modules/generator/main_impl.h
- ecc.pedersen.second_generator(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), hf: Callable[[], ~ecc.alias.HashObject]=<built-in function openssl_sha256>) tuple[int, int][source]¶
Second (with respect to G) Nothing-Up-My-Sleeve (NUMS) generator.
A commitment rG+vH is only binding if nobody knows log_G(H): a committer who did could open the same commitment to any (r, v) of their choosing. H is therefore not chosen but derived – the hash of G is read as a candidate x-coordinate, and the candidate is incremented until it lands on the curve, and on a curve of cofactor above 1 in the subgroup generated by G too – so that computing a discrete logarithm relating H to G is the only way to a value this function could also have produced, and nobody has one.
The result is cached on (ec, hf): it is a constant for that pair, recomputing it on every call cost 71% of a commitment (issue btclib-org/btclib#287).
For (secp256k1, sha256), this function’s own default pair, the derived H equals the H hardcoded as secp256k1_generator_h in libsecp256k1-zkp – the H of Elements and of Confidential Transactions. tests/schemes/pedersen_test.py::test_second_generator pins that value against a literal, and tests/schemes/pedersen_test.py::test_second_generator_matches_zkp against btclib_secp256k1.zkp.generator.h(), which is that library answering with its own copy rather than a transcription of it. No published constant exists to pin either against on another curve or hash function.
idea: https://crypto.stackexchange.com/questions/25581/second-generator-for-secp256k1-curve
source: https://github.com/BlockstreamResearch/secp256k1-zkp/blob/master/src/modules/generator/main_impl.h
- ecc.pedersen.verify(r: bytes | str | bytearray | memoryview | int, v: bytes | str | bytearray | memoryview | int, commitment: tuple[int, int], gen: 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)) bool[source]¶
Open the commitment and return True if valid.
Raises where r or v is spelled in a way no integer is, or where gen is off the curve, at INF or outside the subgroup generated by G; answers False for an opening that is well formed and merely does not open this commitment. See _assert_structurally_valid_.
ecc.rangeproof module¶
The Confidential Transactions rangeproof, and what this package writes of it.
RangeProof.parse reads what secp256k1-zkp’s rangeproof module writes – secp256k1_rangeproof_sign_impl, src/modules/rangeproof/rangeproof_impl.h – and RangeProof.serialize writes those octets back.
sign writes a proof for a blinded value: _prove_params chooses the header, the mantissa decides the rings and the value’s digits which key of each one the prover opens, and _borromean_sign closes them. sign_public_value is that walk at an exp of -1, where the value is stated in the clear and the single ring holds the key the commitment itself gives. RangeProof.pubk_rings_var rebuilds, from a proof and the value commitment it was written against, the rings secp256k1_borromean_verify_impl is handed, and RangeProof.sign_key_idx says which key of each ring a value’s own digit names. RangeProof.nonce_chain derives what secp256k1_rangeproof_genrand draws for those rings: each ring’s blinding factor, and the scalar behind each of its keys.
verify answers whether a proof holds for a commitment, and assert_as_valid is the same question raising its reason; the range proven is the proof’s own min_value and max_value, which the header states and a parse reads. rewind is what a recipient holding the nonce calls: it answers the blinding factor, the value and the message sign embedded. Those two are one piece and not two – secp256k1_rangeproof_verify_impl calls secp256k1_rangeproof_rewind_inner from inside itself and hands it the per-key challenges its own borromean walk produced, commented there as used during a rewind alone – so _verify_rings answers the challenges where verify answers a bool.
extra_commit binds octets of a caller’s outside the proof into the challenge, and it enters the ring message alone: secp256k1_rangeproof_sign_impl and secp256k1_rangeproof_verify_impl write it into their sha256_m after the ring commitments and immediately before finalizing, and hand secp256k1_rangeproof_genrand no such argument. So a proof’s octets change with it and the chain behind them does not.
gen, secp256k1_rangeproof.h’s own argument of that name and zkp.rangeproof’s gen_bytes, is the generator the commitment was made under, and every entry point below takes it: it enters the octets hashed into the message every ring is signed over, the weight each ring’s keys step down by in _pub_expand_var, and the min_value offset pubk_rings_var subtracts. ecc.pedersen.commit takes it too, so a caller holding one generator per asset commits and proves under each of them.
Elements and Liquid use this format, and a design starting today would choose bulletproofs, which zkp carries as its bppp module.
Each direction is held to what can hold it. A parse answers to zkp.rangeproof.info, field for field over the same octets, and what that call is silent about – the sign bits, the ring commitments and the signature – to the octets going back out unchanged. The writer answers to something stronger, and that is what makes this the shape to write first: a rangeproof draws nothing, its nonce being the hash chain rangeproof_genrand derives from the caller’s, so the octets are a function of the arguments and the proof zkp signs for those same arguments is the answer, byte for byte.
secp256k1_rangeproof_sign_impl writes, in order:
one octet of flags and exponent: bit 7 zero, bit 6 has_nz_range, bit 5 has_min, bits 0-4 the exponent;
the mantissa less one, one octet, where has_nz_range is set;
min_value, eight octets big-endian, where has_min is set;
the sign bit of every ring commitment, packed signs[i >> 3] |= quadness << (i & 7), padded to an octet;
the x coordinate of every ring commitment but the last, thirty-two octets each;
e0 and one s per public key, ring-major, thirty-two octets each.
The last row is a BorromeanSig, which is why one is a field here rather than an e0 and an s of this module’s own: ecc.borromean already writes and reads that layout. What the rangeproof adds is rsizes – how the mantissa decides the ring structure – which secp256k1_rangeproof_verify_impl computes and hands to secp256k1_borromean_verify_impl rather than reading out of the proof, and which _rsizes below computes the same way.
The sign bit is quadratic residuosity, not parity. secp256k1_rangeproof_serialize_point writes data[0] = !secp256k1_fe_is_square_var(&point->y), so a set bit says the ring commitment’s y is not a square. That is not the even/odd convention curves.bytes_from_point, BIP340 and the 02/03 SEC prefix carry, and the two disagree on the vectors tests/schemes/rangeproof_test.py reads. ecc.pedersen._bytes_from_point_var at _RANGEPROOF_TAG is that codec, and every point entering a proof’s hash goes through it: the value commitment, the generator, and each ring commitment sign states. A parse reads those bits back and a serialize writes them out, so what a RangeProof holds is the octets’ own answer. Resolving an x back to a point is that convention read the other way, and ecc.pedersen._point_from_x_var is what does it: secp256k1_ge_set_xquad takes the y that is a square, and secp256k1_rangeproof_verify_impl negates it where the sign bit says the y is not one.
The digit decomposition is rangeproof_pub_expand: a ring’s keys step down from the ring commitment by the weight of the digit that ring proves, 10**exp for the first and four times its predecessor for each ring after it. The last ring states no commitment of its own – secp256k1_rangeproof_verify_impl recovers it as the value commitment less min_value times the generator and less every ring commitment before it, which is the digit a prover does not have to send. So the value commitment is what the whole structure hangs from, and it is pubk_rings_var’s argument.
An x is an integer of the width the format fixes wherever a proof is parsed, serialized or held against itself: assert_valid refuses the headers secp256k1_rangeproof_getheader_impl refuses, and a body this object could not write the octets of, and an x naming no point on the curve is neither. pubk_rings_var is where an x has to name a point, and is what turns down one that does not. Whether the signature over those rings verifies, this module does not ask.
- class ecc.rangeproof.NonceChain(blinding_factors: tuple[int, ...], draws: tuple[tuple[int, ...], ...])[source]¶
Bases:
NamedTupleWhat secp256k1_rangeproof_genrand answers for one proof’s rings.
blinding_factors is one scalar per ring, the one that ring’s commitment is written under. Every ring but the last draws its own and the last is minus the sum of those, so once secp256k1_rangeproof_sign_impl has added the caller’s own to that last one they sum to it – which is what lets a verifier recover the ring commitment the proof leaves out. What is here is the chain’s answer, before that addition.
draws is one scalar per public key, ring-major, the shape BorromeanSig.s carries. At the key the value’s own digit names, sign_impl moves the draw into the nonce it closes that ring with; at every other key the draw is the s the proof states.
- class ecc.rangeproof.RangeProof(exp: int, mantissa: int, min_value: int | None, signs: Sequence[bool], ring_commitments: Sequence[int], sig: BorromeanSig, *, check_validity: bool = True)[source]¶
Bases:
objectA Confidential Transactions rangeproof, as its octets state it.
exp is -1 exactly where the proof states its value in the clear, and mantissa is 0 there and in [1, 64] everywhere else: the two move together because one flag carries both, bit 6 of the first octet being what says the range is blinded at all.
min_value is None where that octet’s bit 5 is clear and the proven range therefore starts at zero, and an integer – zero included – where the field is present. The two are not the same octets and zkp.rangeproof.info answers zero for both, so an integer of its own is what lets serialize write back what parse read.
signs and ring_commitments are aligned, one entry each per ring but the last: the wire packs the bits together ahead of the x coordinates, which is why they are two fields rather than pairs. sig is the borromean signature over the rings the mantissa describes, and it is where this proof’s curve is: an x is written at sig.ec.p_size octets, which generalizes zkp’s hardcoded thirty-two the way BorromeanSig.serialize generalizes its scalar width, the two agreeing wherever the curve is secp256k1.
- assert_valid() None[source]¶
Refuse a header no proof carries, or a body it does not describe.
The header is _assert_valid_header’s, which is zkp’s own refusals. The rest is this object against itself: a sign bit with no ring commitment beside it, an x wider than the field it is written in, or a signature whose rings are not the ones the mantissa describes – each of them a state serialize could not write the octets of.
- property max_value: int¶
Return the largest value this proof’s header proves for.
A header this format has no octets for is refused here, in the spelling assert_valid refuses it in: check_validity=False leaves a caller holding such a proof, and there is no largest value to answer for one. A header field that is no integer, an absent min_value aside, is an EccTypeError.
- nonce_chain(commitment: tuple[int, int], value: int, nonce: bytes | str | bytearray | memoryview, gen: tuple[int, int], message: bytes | str | bytearray | memoryview = b'', *, check_validity: bool = True) NonceChain[source]¶
Return the scalars this proof’s nonce derives.
secp256k1_rangeproof_genrand, seeded as secp256k1_rangeproof_sign_impl seeds it: the caller’s nonce, then the value commitment and the generator at _RANGEPROOF_TAG, then the header octets this proof’s own exponent, mantissa and min_value write. So whoever holds the nonce answers every ring commitment the proof states – each is its ring’s blinding factor times G plus that ring’s own digit times the generator – and every s the signature left as the chain drew it.
commitment is the Pedersen commitment the proof was written against, pubk_rings_var’s argument of that name, gen the generator it was made under, and value what it commits to. The value is read for more than the digits: wherever the last ring holds more than one key, one of them carries the value inside its own draw, so the chain is not a function of the header alone. A value this proof has no digit for is refused, sign_key_idx’s own refusal.
A commitment at infinity is refused too, where pubk_rings_var answers rings for one: the seed carries the octets secp256k1_rangeproof_serialize_point writes, and infinity has no x to write. The require_on_curve above lets it through, (x, 0) being how this library spells infinity in affine coordinates.
message is what sign embedded, and it belongs here because the draws carry it: one scalar per key of every ring before the last. _message_room bounds it.
- classmethod parse(data: BytesIO | bytes | str | bytearray | memoryview, *, check_validity: bool = True) RangeProof[source]¶
Build a RangeProof from the octets zkp’s rangeproof module writes.
secp256k1 and sha256, as BorromeanSig.parse reads its own layout and for the same reason: the encoding names neither, and both widths are what zkp fixes for the one curve it has.
Three of zkp’s refusals do not wait on check_validity. Bit 7 of the first octet, which the format holds at zero, and a set bit above the last sign bit, which secp256k1_rangeproof_verify_impl refuses as mutation, are neither of them a field: a parse that accepted either would build an object serializing back to different octets. A mantissa past 64 is the third, and for read_exactly’s own reason – it is what says how many rings follow, so it makes the field boundary rather than states a value.
The exponent’s cap and the max_value arithmetic decide no length, and are assert_valid’s as everywhere else.
- pubk_rings_var(commitment: tuple[int, int], gen: tuple[int, int], *, check_validity: bool = True) tuple[Sequence[tuple[int, int]], ...][source]¶
Return the rings of keys this proof’s signature is over.
secp256k1_rangeproof_verify_impl up to the point where it hands those rings to secp256k1_borromean_verify_impl: every ring commitment the proof states, resolved against the residuosity convention the module docstring gives; the one it does not state, recovered from commitment; and _pub_expand_var over all of them. Nothing here reads e0 or an s, so a proof whose signature is wrong answers the same rings as one whose signature is right.
gen is the generator that commitment was made under, and both the min_value offset below and _pub_expand_var’s own weights are multiples of it.
commitment is the Pedersen commitment the proof was written against, ecc.pedersen.commit’s own point rather than octets – the ones secp256k1_pedersen_commitment_serialize hands back carry the residuosity bit too, secp256k1_pedersen_commitment_save writing it at 9 ^ is_square(y) where secp256k1_rangeproof_serialize_point writes 1 ^ is_square(y).
A commitment that leaves the last ring at infinity is refused, as secp256k1_rangeproof_verify_impl refuses it where its own pubs[npub] lands there: infinity is no public key. So is an x at or past the field prime, which is secp256k1_fe_set_b32_limit’s refusal and Curve.y_var’s own.
A header this format has no ring structure for is refused too, self.rsizes below carrying that refusal for check_validity=False as much as for True.
- property rsizes: tuple[int, ...]¶
Return how many public keys each of this proof’s rings holds.
A header this format has no ring structure for is refused here too: check_validity=False leaves a caller holding such a proof, and there is no ring count to answer for one. A mantissa that is no integer is an EccTypeError.
- serialize(*, check_validity: bool = True) bytes[source]¶
Return the header, the sign bits, the commitments and the signature.
secp256k1_rangeproof_sign_impl’s own order, and its own two conditions: the mantissa octet where the range is blinded, the min_value octets where the header carries one.
A header this format has no octets for is refused too, by _header_octets below rather than by check_validity: at False there is still no octet to hand back for it. A header field or ring commitment that is no integer is an EccTypeError.
- sign_key_idx(value: int) tuple[int, ...][source]¶
Return where in each ring the key of that value sits.
secp256k1_range_proveparams’ secidx, and ecc.borromean.sign’s argument of this name: the base-4 digits of the value less min_value over 10**exp, least significant ring first. A ring of two takes the odd bit an odd mantissa leaves over, and the digit there is below two because the value is within the range the header states.
A value this proof has no digit for is refused.
- class ecc.rangeproof.Rewound(blind: int, value: int, message: bytes)[source]¶
Bases:
NamedTupleWhat whoever holds a proof’s nonce reads back out of it.
blind is the blinding factor the commitment was written under and value what it commits to, so the two open the commitment. message is what the draws carry: what sign embedded, followed by the zeros _prep left after it. Nothing in a proof states how long a message was, so the padding is the caller’s to know the length of or to strip.
- ecc.rangeproof.assert_as_valid(commitment: tuple[int, int], proof: RangeProof | bytes | str | bytearray | memoryview, gen: tuple[int, int], *, extra_commit: bytes | str | bytearray | memoryview = b'') None[source]¶
Refuse a proof that does not hold for that commitment.
commitment is the Pedersen commitment the proof was written against and gen the generator it was made under, both RangeProof.pubk_rings_var’s arguments of those names: a proof written under one generator does not hold under another, the whole structure hanging from it. The range proven is the proof’s own min_value and max_value, which the header states and a parse reads; what this adds is that the header is that commitment’s, the last ring commitment being recovered from it and the whole signature walked against the rings that follow. So a commitment a proof holds for opens at some value in min_value..max_value.
zkp.rangeproof.verify answers that range instead of a verdict, having only the octets to answer from. verify here is the boolean answer, as ecc.borromean.verify is to ecc.borromean.assert_as_valid.
extra_commit is what the proof was written under, and a proof holds for the octets sign was given and for no others: they close the message the rings are signed over, so any other value walks the rings to some e0 the signature does not state. Keyword-only, as the sign-to-contract commit of ecc.dsa and ecc.ssa is on the same pair of calls.
- ecc.rangeproof.rewind(commitment: tuple[int, int], proof: RangeProof | bytes | str | bytearray | memoryview, nonce: bytes | str | bytearray | memoryview, gen: tuple[int, int], *, extra_commit: bytes | str | bytearray | memoryview = b'') Rewound[source]¶
Return what the author of that proof put in it for its recipient.
secp256k1_rangeproof_rewind_inner, which secp256k1_rangeproof_verify_impl calls from inside itself: the proof is verified first, by _verify_rings, and the per-key challenges that walk answers are half of what this needs. The other half is the chain re-derived from nonce over a prep of zeros, which answers the draws themselves – the proof’s own s at a forged key is one of those draws with a message octet or the value encoding xored into it, and at the one key a ring opens it is the nonce that key was closed with.
A single ring of a single key is the public-value proof, and only the blinding factor is recoverable from it: the value is the header’s min_value, stated in the clear, and there is no second key to carry a message.
Everywhere else _value_encoding_var reads the value out of the last ring, and two refusals follow it. The position it was found at and the last ring’s own digit have to differ – _prep writes the encoding at whichever of the two keys the digit does not take, so a proof stating a value that lands on it states a value it was not written for. And a digit above the last ring’s own size names no key of it, which is where an odd mantissa leaves a ring of two.
The blinding factor is then _recover_x at the last ring’s digit, less the chain’s own factor for that ring: what the ring was signed under is the sum of the two, which is what lets a verifier recover the ring commitment the proof leaves out.
The message is every remaining key of every ring, the two the last ring spends excepted: the nonce at the key a ring opens, recovered by _recover_k, and the stated s everywhere else, each xored against the draw behind it. Rewound.message has what that buffer holds past the message itself.
Refused where the value and the blinding factor recovered do not open commitment, which is what says the nonce was the proof’s own: secp256k1_rangeproof_verify_impl rebuilds the commitment from them and compares.
gen is assert_as_valid’s argument of that name, and it is what the recovered value and blinding factor are put back against in the refusal above.
extra_commit is assert_as_valid’s argument of that name, and it is spent on the verification this opens with: the chain re-derived below is seeded by _hashed_prefix_var, which carries none of it.
- ecc.rangeproof.sign(blind: bytes | str | bytearray | memoryview | int, value: int, nonce: bytes | str | bytearray | memoryview, gen: tuple[int, int], *, min_value: int = 0, exp: int = 0, min_bits: int = 0, message: bytes | str | bytearray | memoryview = b'', extra_commit: bytes | str | bytearray | memoryview = b'') RangeProof[source]¶
Return the proof that a blinded value lies in a stated range.
secp256k1_rangeproof_sign_impl. The commitment the proof is written against is ecc.pedersen.commit over blind and value, the same point secp256k1_pedersen_commit answers, and the range the header states is _prove_params’ rather than the caller’s: min_value, exp and min_bits are what is asked for, and the proof carries what the format has room for.
value and min_value are what a uint64 holds and min_value no more than value, nonce the 32 octets rangeproof_genrand seeds its chain with, exp in -1..18 and min_bits in 0..64 – the bounds secp256k1_rangeproof_sign_impl reads before it asks for a header. A value, min_value, exp or min_bits that is no integer, a bool included, is an EccTypeError.
nonce must be unique to each call, never reused, and kept secret except from parties allowed to rewind the proof: anyone who learns it recovers value and blind, and reusing it across two proofs may expose blind even to a party that never learns it. See https://github.com/BlockstreamResearch/secp256k1-zkp/pull/370.
blind is a scalar, read through curves.scalar_from_prv_key, which refuses at or past n as zkp does and refuses zero where zkp signs one: a zero blinding factor leaves the last ring’s own commitment blinded by the chain’s factor alone, and zkp turns that down only where the sum vanishes, which is the single-ring proof.
Deterministic in every one of them, a rangeproof drawing nothing, so what comes back over the arguments both accept is the octets zkp.rangeproof.sign answers for those same arguments.
message is embedded for rewind to read back out: it rides at the head of the buffer _prep builds, one scalar per key of every ring before the last. _message_room is what bounds it, and the octets are otherwise those of a proof carrying none.
extra_commit is octets of the caller’s own – the output a commitment belongs to, say – bound into the proof rather than carried by it, and a verifier is handed them again or the proof does not hold for it. It closes the message the rings are signed over and nothing else: secp256k1_rangeproof_sign_impl writes it into sha256_m after the ring commitments and immediately before finalizing, where secp256k1_rangeproof_genrand takes no such argument, so the draws are the same and what a rewind reads back out is unchanged.
gen is the generator the commitment this proof is written against is made under, which secp256k1_rangeproof.h documents on each of its calls as the “additional generator ‘h’”. It reaches the commitment, the octets _hashed_prefix_var writes and the weight each ring’s keys step down by, so a proof is bound to it: verify and rewind are handed it again or the proof does not hold.
- ecc.rangeproof.sign_public_value(blind: bytes | str | bytearray | memoryview | int, value: int, nonce: bytes | str | bytearray | memoryview, gen: tuple[int, int]) RangeProof[source]¶
Return the proof that states its value in the clear.
sign at an exp of -1, which secp256k1_range_proveparams answers with one ring holding the single key the commitment itself gives. So this proof asserts no range: it says what the commitment commits to, and that whoever wrote it knows the blinding factor. The mantissa, the digit decomposition, rangeproof_pub_expand and the sign bits are all absent from it, a proof carrying one ring commitment per ring but the last and this one having a single ring.
A value of zero writes the flags octet and nothing after it; any other writes min_value in the eight octets behind bit 5, the value being its own floor here.
A spelling of its own because min_value, exp and min_bits name nothing this proof has: its header is fixed.
- ecc.rangeproof.verify(commitment: tuple[int, int], proof: RangeProof | bytes | str | bytearray | memoryview, gen: tuple[int, int], *, extra_commit: bytes | str | bytearray | memoryview = b'') bool[source]¶
Return whether the proof holds for that commitment.
assert_as_valid’s docstring has what extra_commit is, and a proof holds for the octets sign was given and for no others.
Raises where proof is octets no proof has, and answers False for a proof that is well formed and merely does not hold. See _assert_structurally_valid_.
A RangeProof argument has no octets to be wrong, so what it is asked is assert_valid. A state it refuses as a value is False here and an EccValueError from assert_as_valid; a header field or ring commitment that is no integer is an EccTypeError from both. See _proof_from.
ecc.rfc6979_nonce module¶
Deterministic generation of the ephemeral key following RFC6979.
https://www.rfc-editor.org/rfc/rfc6979.html
Every ECDSA and ECSSA signature needs a fresh ephemeral key (nonce), chosen randomly and uniformly from the scalars by a cryptographically secure process: even a slight bias in that process can be turned into an attack on the scheme, and reusing a nonce across two messages signed with one private key reveals the key – dsa.crack_prv_key_var is that computation.
RFC6979 removes the need for a randomness source by deriving the nonce deterministically from the private key and the message, which also makes signing testable against fixed vectors. The derivation keeps the properties a signature scheme expects: to whoever does not know the private key, the message-to-nonce mapping is computationally indistinguishable from a uniformly random function.
BIP340 uses a different algorithm for the generation of the ephemeral key: bip340_nonce.py.
- ecc.rfc6979_nonce.challenge_(msg_hash: 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), hf: Callable[[], ~ecc.alias.HashObject]=<built-in function openssl_sha256>) int[source]¶
Return the ECDSA challenge scalar from a message hash.
Bits2int of SEC 1 and RFC6979: the leftmost nlen bits of the hash, reduced mod n. The message enters already reduced – a digest of hf’s size, which is what the trailing underscore says.
- ecc.rfc6979_nonce.rfc6979_nonce_(msg_hash: bytes | str | bytearray | memoryview, 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), hf: Callable[[], ~ecc.alias.HashObject]=<built-in function openssl_sha256>, extra_entropy: bytes | str | bytearray | memoryview | None = None) int[source]¶
Return an RFC6979 deterministic ephemeral key (nonce).
see https://www.rfc-editor.org/rfc/rfc6979.html section 3.2
extra_entropy is the section 3.6 additional data: two callers with the same key and message reach different nonces by passing different values, and the derivation stays deterministic in all of its inputs. It is what a commitment travels through in commit_nonce, what the low-R grinding of
dsa.sign_puts its counter in, and what the bindings take asaux_rand32– ndata being the name the C function underneath it gives the same 32 octets.
ecc.ssa module¶
Elliptic Curve Schnorr Signature Algorithm (ECSSA).
This implementation is according to BIP340-Schnorr:
https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki
The public key is the x-coordinate – a field element – of the curve point associated to the private key 0 < q < n, so on secp256k1 a public key is 32 bytes. Knowing q as the discrete logarithm of Q is knowing n-q as the discrete logarithm of -Q, so {q, n-q} acts as one private key and {Q, -Q} as the public key their shared x_Q names.
The dropped 02/03 prefix is implicit, not lost: verification needs an unambiguous Y, so BIP340 fixes it as even, and the x-only key is the compressed key 02||x with its prefix left unsaid. Halving the set of valid public keys costs no security – whoever breaks an x-only key breaks the full key at the price of a negation – and taking the bare x as the key refuses a malleability: a verifier that accepted a point and negated its odd Y would make every signature valid for two keys.
The hash function is BIP340’s tagged SHA256, TaggedHash(tag, x) = SHA256(SHA256(tag)||SHA256(tag)||x), which makes a BIP340 hash invalid under any other tag and any other scheme; the challenge uses tag ‘BIP0340/challenge’, the deterministic nonce ‘BIP0340/aux’ and ‘BIP0340/nonce’.
The challenge commits to the public key as well as to the nonce point, c = TaggedHash(‘BIP0340/challenge’, x_k||x_Q||msg), which rules out public key recovery and is what makes batch verification sound.
The challenge commits to the nonce point as well, and that dependency is the Fiat-Shamir transform itself: hashing the commitment x_k replaces the fresh challenge that an interactive verifier would send only after receiving the commitment. Were c and the nonce chosen independently, no private key would be needed: K = s*G - c*Q satisfies verification for any Q.
The deterministic nonce is BIP340’s own, not RFC6979’s:
nonce = TaggedHash(‘BIP0340/nonce’, t||x_Q||msg) with t = q xor TaggedHash(‘BIP0340/aux’, a), a the auxiliary randomness
The serialization is the fixed-size [r][s] – p-size plus n-size bytes, 64 on secp256k1 – not the loosely specified ASN.1 DER of ECDSA.
sign_ also takes a value to commit to inside the nonce,
sign-to-contract style (see ecc.commit_nonce for the tweak), and
the anti_exfil_* functions here are the protocol that construction
exists to support: the handshake bitcoin-core/secp256k1#1140 proposes
for BIP340, whose five steps and reasoning are in
anti_exfil_host_commit.
- class ecc.ssa.Sig(r: int, s: 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), *, check_validity: bool = True)[source]¶
Bases:
objectA BIP340-Schnorr signature: (r, s).
r is a field element, 0 <= r < ec.p, the x-coordinate of the nonce point; s is a scalar, 0 <= s < ec.n, and zero is valid here where dsa.Sig refuses it, BIP340 placing no lower bound.
- classmethod parse(data: BytesIO | bytes | str | bytearray | memoryview, *, check_validity: bool = True) Sig[source]¶
Build a Sig from BIP340’s r || s bytes, on secp256k1.
The serialization does not name its curve, so parse reads the one BIP340 is defined over; a Sig on another curve is built directly.
Sixty-four octets exactly, and a witness signature is not one: BIP341 appends the sighash type to it, which is a byte about the transaction and not part of the signature. Stripping it is the caller’s, signature[:64], as a script interpreter does after reading it.
- class ecc.ssa.Signer(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), hf: Callable[[], ~ecc.alias.HashObject]=<built-in function openssl_sha256>)[source]¶
Bases:
objectSign several messages under one key, building the keypair once.
sign_ builds a secp256k1_keypair and wipes it before returning, so a second signature under the same key builds it again – and that keypair is a multiplication of the generator, about half of what a BIP340 signature costs. This holds one across calls instead, which is worth better than half a signature each time. The measurement per batch size is in btclib-org/btclib’s CHANGELOG, in the entry that introduced this class, an entry being read as the history of a release where this is read as a statement about the code as it stands.
The same signatures come out, sign here being sign_ over the keypair the signer holds, with the same default aux – 32 octets drawn afresh per signature where the caller names none. The octets are what comes back rather than a Sig, that being what the callers of this want: a psbt writes a signature into a field, and building a Sig to serialize it again is the round trip this saves beside the keypair.
What this hands the caller is the lifetime of a secret. sign_ owns a keypair for the length of a call and wipes it in a finally; a signer holds one until told to let go. wipe is that instruction and the with statement is how to give it without having to remember:
with ssa.Signer(prv_key) as signer: ...
which wipes on the way out whether the block ended in a signature or in an exception. A wiped signer refuses to sign rather than signing with the zeros, and cannot be revived. So this is for a caller that already holds the secret for several signatures – SoftwareSigner signing every leaf a psbt names one key in – and a lone signature that drops the key afterwards is what sign_ already is.
A curve or a hash function the bindings do not serve has no keypair to hold: there the signer holds the key’s public point, computed once. On that arm wipe has no keypair to overwrite and the with still reads the same – what it does there is drop the scalar and stop the signing, which is all it can. And the scalar is held on that arm alone: where a keypair exists it holds the same secret in memory that can be overwritten, so a second copy as a python int would be kept for nothing. What is not solved either way is the object the private key arrived in, nor that scalar where the Python arm needs it – an int cannot be overwritten, only dropped, and SECURITY.md’s limitations section is where that is stated for the library at large.
- sign(msg: bytes | str | bytearray | memoryview, aux: bytes | str | bytearray | memoryview | None = None, *, verify: bool = True) bytes[source]¶
Return the signature of a message, reducing it with hf first.
- sign_(msg: bytes | str | bytearray | memoryview, aux: bytes | str | bytearray | memoryview | None = None, *, verify: bool = True) bytes[source]¶
Return the signature of a prepared message, as its octets.
The message is signed as it is, of any size, which is what the trailing underscore says throughout this module.
verify is the free function’s, and this is the call where it is the largest share of what it turns off: the keypair was built when this signer was, so the signature is the cheap part and the check is not. See sign_ for the default and for why no pub_key joins it.
- wipe() None[source]¶
Let go of the key, and refuse to sign afterwards.
The keypair is overwritten where there is one, which is the half of this that really erases: it is libsecp256k1’s own memory and the bindings write zeros over it. The scalar this was built from is a python int and is dropped rather than erased – rebinding the attribute is the whole of what a caller can do about an immutable object, and SECURITY.md’s limitations section is where that is stated for the library at large. So a wiped signer is one that cannot sign and holds no keypair, not one that has scrubbed every copy of the secret from the process.
Idempotent, so that a caller may wipe a signer it is not sure about; the with statement is the customary way to run one.
- ecc.ssa.anti_exfil_host_commit(rho: bytes | str | bytearray | memoryview, hf: Callable[[], ~ecc.alias.HashObject]=<built-in function openssl_sha256>) bytes[source]¶
Return the host’s commitment to rho: step 1 of the anti-exfil protocol.
The protocol is proposed for include/secp256k1_schnorrsig.h of bitcoin-core/secp256k1#1140, carrying over the ECDSA Anti-Exfil Protocol BlockstreamResearch/secp256k1-zkp’s secp256k1_ecdsa_s2c.h specifies – see dsa.anti_exfil_host_commit – to BIP340.
A signing device that picks its own nonce can leak the private key through the nonces themselves, and the protocol takes that choice away by having the host contribute randomness to the nonce derivation before the device commits to a nonce point, in a commit-reveal handshake of five steps:
the host draws rho and sends
anti_exfil_host_commit(rho)the device answers with
anti_exfil_signer_commit(msg, prv_key, commitment), the point R its nonce will havethe host reveals rho
the device signs,
anti_exfil_sign(msg, prv_key, rho)the host checks
anti_exfil_host_verifyagainst the R of step 2 and the rho it drew in step 1
rho is hf_len bytes from a cryptographically secure generator, and it stays secret until step 2 has been answered: revealed earlier it is the device’s to grind, which is the whole of what this prevents.
Restarting the protocol takes exactly the same rho, and the host checks that the device answers step 2 with exactly the same R – see dsa.anti_exfil_host_commit for what a device that could dodge this check would buy itself.
The commitment is the committed value as it enters the nonce derivation –
commit_entropy_under the sign-to-contract data tag, and nothing else – which is what lets step 2 and step 4 reach one nonce: the device derives it from this hash, and recomputes the same hash from rho when it signs.sign_’s own commit_hash argument reaches the nonce mixed with the caller’s own aux instead, which is the right shape for an ordinary sign-to-contract commitment and the wrong one for this handshake: step 2 has nothing to mix in but the commitment, so the four functions here derive the nonce directly rather than going throughsign_.
- ecc.ssa.anti_exfil_host_verify(msg: bytes | str | bytearray | memoryview, Q: int | bytes | str | bytearray | memoryview | tuple[int, int] | ~ecc.curves.curve.PreparedPoint, sig: ~ecc.ssa.Sig | bytes | str | bytearray | memoryview, rho: bytes | str | bytearray | memoryview, receipt: tuple[int, int], hf: ~collections.abc.Callable[[], ~ecc.alias.HashObject] = <built-in function openssl_sha256>) bool[source]¶
Check the signature against R and rho: step 5 of the anti-exfil protocol.
Two questions answered as one, and the host needs both: that this is a valid signature, and that its nonce is the R of step 2 tweaked by the rho of step 1. Either alone is worth nothing – a valid signature over a nonce nobody constrained is the exfiltration this protects against, and a commitment that opens under an invalid signature is not a signature. Which
verify_already does in one call, both checks running against the same r.receipt is the R of step 2. It raises and answers False for the same reasons
verify_does: a key or a signature that is structurally invalid raises, and a rho of the wrong size is False, a rho this commitment does not open to.
- ecc.ssa.anti_exfil_sign(msg: bytes | str | bytearray | memoryview, prv_key: bytes | str | bytearray | memoryview | int, rho: 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), hf: Callable[[], ~ecc.alias.HashObject]=<built-in function openssl_sha256>) Sig[source]¶
Sign committing to the host’s rho: step 4 of the anti-exfil protocol.
Sign-to-contract with rho as the committed value and nothing else reaching the nonce derivation –
sign_(msg, prv_key, commit_hash=rho)would mix in a fresh random aux instead, breaking the property this function exists for. The receipt is dropped rather than passed on, because the host has it already – it is the R of step 2, and a host taking the device’s word for it here would be accepting a nonce point chosen after rho was revealed, which is the one thing the ordering exists to rule out.The device keeps no state between step 2 and step 4. It does not check rho against the commitment it was given: it re-derives the commitment from rho, and the nonce from that. A rho that does not match yields a different nonce, so the host’s step 5 fails and the exchange is over – and because the R of step 2 belonged to the commitment it was derived from, no nonce is ever used twice and the device’s key is never the thing at risk.
- ecc.ssa.anti_exfil_signer_commit(msg: bytes | str | bytearray | memoryview, prv_key: bytes | str | bytearray | memoryview | int, host_commitment: 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), hf: Callable[[], ~ecc.alias.HashObject]=<built-in function openssl_sha256>) tuple[int, int][source]¶
Return the signer’s public nonce R: step 2 of the anti-exfil protocol.
The point of the nonce the device is going to use, published before the host reveals what its commitment commits to. Nothing is signed here, and that is the shape the protocol needs: R is a promise, and step 4 is what keeps it.
The commitment travels as BIP340’s own auxiliary randomness, alone – exactly what it becomes inside
anti_exfil_sign’s nonce derivation – so the two reach one nonce and the R below is the receipt that signature will open with.
- ecc.ssa.assert_as_valid(msg: bytes | str | bytearray | memoryview, Q: int | bytes | str | bytearray | memoryview | tuple[int, int] | ~ecc.curves.curve.PreparedPoint, sig: Sig | bytes | str | bytearray | memoryview, hf: Callable[[], ~ecc.alias.HashObject]=<built-in function openssl_sha256>, *, commit: bytes | str | bytearray | memoryview | None = None, receipt: tuple[int, int] | None=None) None[source]¶
Verify the BIP340 signature of hf(msg).
The other spelling,
assert_as_valid_, takes the BIP340 message itself, of any size. This one reduces msg with hf first, and commit with it.Double backticks because rst reads a trailing underscore as a link reference: bare, this name makes sphinx -W fail with ‘Unknown target name: “assert_as_valid”’.
- ecc.ssa.assert_as_valid_(msg: bytes | str | bytearray | memoryview, Q: int | bytes | str | bytearray | memoryview | tuple[int, int] | ~ecc.curves.curve.PreparedPoint, sig: Sig | bytes | str | bytearray | memoryview, hf: Callable[[], ~ecc.alias.HashObject]=<built-in function openssl_sha256>, *, commit_hash: bytes | str | bytearray | memoryview | None = None, receipt: tuple[int, int] | None=None) None[source]¶
Refuse an invalid BIP340 signature over a prepared message.
The message enters as it is, of any size; assert_as_valid is the spelling that reduces with hf first. Errors carry the reason,
verify_being the boolean answer. With commit_hash and receipt the sign-to-contract commitment is opened as well, an independent check of the same r.
- ecc.ssa.assert_batch_as_valid(ms: ~collections.abc.Sequence[bytes | str | bytearray | memoryview], Qs: ~collections.abc.Sequence[int | bytes | str | bytearray | memoryview | tuple[int, int] | ~ecc.curves.curve.PreparedPoint], sigs: ~collections.abc.Sequence[~ecc.ssa.Sig], hf: ~collections.abc.Callable[[], ~ecc.alias.HashObject] = <built-in function openssl_sha256>) None[source]¶
Refuse an invalid signature in a batch, reducing each message.
- ecc.ssa.assert_batch_as_valid_(msgs: ~collections.abc.Sequence[bytes | str | bytearray | memoryview], Qs: ~collections.abc.Sequence[int | bytes | str | bytearray | memoryview | tuple[int, int] | ~ecc.curves.curve.PreparedPoint], sigs: ~collections.abc.Sequence[~ecc.ssa.Sig], hf: ~collections.abc.Callable[[], ~ecc.alias.HashObject] = <built-in function openssl_sha256>) None[source]¶
Refuse an invalid signature in a batch of prepared messages.
BIP340’s batch verification: one multi-scalar equation over random coefficients – and which signature failed is not in the answer, only that one did. Messages enter as they are; every signature must share one curve. An empty batch is valid, as in BIP340.
It is not the fast way to verify n signatures of secp256k1. Measured against n delegated verify_ calls, the batch costs about twice a verify_ a signature, both flat in n, so there is no crossover at any batch size. libsecp256k1 has no batch verification to delegate to, checked at 687155df upstream and at the tip of secp256k1-zkp, whose half-aggregation is a different construction; so what the batch saves in multiplications it spends on a Python term per signature, against a whole verification that is one C call.
Where it does win is the arithmetic it was written for. With the bindings switched off – every other curve, and every other hash function – the equation is one multi-scalar multiplication where n verifications are n double multiplications, and it overtakes them between four signatures and eight. That is the reason it stays, beside its being BIP340’s own algorithm and the reference the delegated path is read against. Both measurements per batch size are in btclib-org/btclib’s CHANGELOG, in the entry that took them, an entry being read as the history of a release where this is read as a statement about the code as it stands.
Which leaves the question of why secp256k1 runs the equation at all, rather than a loop of `verify_`. That loop would be twice as fast and would say which signature failed, where this says only that one did, so it is not obviously the wrong answer – and it is not taken. What a caller asks of this function is BIP340 batch verification: one equation over random coefficients, the construction with the security argument the BIP makes, and the thing an implementation is compared against. A dispatch that answered it with n independent verifications would answer the same verdict by a different computation, and would leave nothing running the equation on the curve where the equation is checkable against libsecp256k1 – which is what test_batch_validation_on_the_python_path uses it for. A caller who wants n verifications has verify_ and a loop, and the figures above are here so that the choice is an informed one.
Every signature is asked whether it is one, this being a public function handed objects somebody else built: the equation below is not that question, and answers a different one for an s outside 0..n-1.
- ecc.ssa.batch_verify(ms: ~collections.abc.Sequence[bytes | str | bytearray | memoryview], Qs: ~collections.abc.Sequence[int | bytes | str | bytearray | memoryview | tuple[int, int] | ~ecc.curves.curve.PreparedPoint], sigs: ~collections.abc.Sequence[~ecc.ssa.Sig], hf: ~collections.abc.Callable[[], ~ecc.alias.HashObject] = <built-in function openssl_sha256>) bool[source]¶
Batch verification of BIP340 signatures.
Raises and answers False for the same reasons batch_verify_ does.
- ecc.ssa.batch_verify_(msgs: ~collections.abc.Sequence[bytes | str | bytearray | memoryview], Qs: ~collections.abc.Sequence[int | bytes | str | bytearray | memoryview | tuple[int, int] | ~ecc.curves.curve.PreparedPoint], sigs: ~collections.abc.Sequence[~ecc.ssa.Sig], hf: ~collections.abc.Callable[[], ~ecc.alias.HashObject] = <built-in function openssl_sha256>) bool[source]¶
Answer whether every signature in the batch verifies.
Messages enter prepared, as in
assert_batch_as_valid_.An empty batch answers True: a caller that expects some number of signatures checks the count itself.
False means an invalid batch: a failed verification and a well-formed key or scalar that is merely not authentic are both False, see _assert_structurally_valid_. Raises where a public key is structurally invalid – spelled in a way BIP340 has no reading for.
Where the bindings are required (curves.set_libsecp256k1_serving with required=True) and decline hf, a batch that is not empty raises EccRuntimeError and does not answer False: nothing was checked.
- ecc.ssa.challenge_(msg: bytes | str | bytearray | memoryview, x_Q: int, x_K: int, ec: Curve, hf: Callable[[], HashObject]) int[source]¶
Return the BIP340 challenge scalar over a prepared message.
TaggedHash(BIP0340/challenge, x_K || x_Q || msg), reduced mod n. The message enters as it is, of any size, which is what the trailing underscore says throughout this module: no reduction by hf happens here.
- ecc.ssa.gen_keys(prv_key: bytes | str | bytearray | memoryview | 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]¶
Return a BIP340 private/public (int, int) key-pair.
- ecc.ssa.point_from_bip340pub_key_var(x_Q: int | bytes | str | bytearray | memoryview | tuple[int, int] | PreparedPoint, 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 a verified-as-valid BIP340 public key as Point tuple.
It supports:
an int, the x-coordinate itself
BIP340 Octets (bytes or hex-string, p-size Point x-coordinate)
SEC Octets (bytes or hex-string, with 02, 03, or 04 prefix)
a PreparedPoint, read as the point it holds
native tuple
- ecc.ssa.prv_key_tweak_add(prv_key: bytes | str | bytearray | memoryview | int, t: 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 the private key of the key pub_key_tweak_add_var answers.
BIP341’s taproot output private key: the key is negated where its point has an odd y, as gen_keys does, and the tweak is added. The tweak is in 0..n-1, and a sum of zero is refused.
On secp256k1 the bindings compute it, secp256k1_keypair_xonly_tweak_add, in constant time in the key; the tweak is not treated as a secret, and libsecp256k1 multiplies the generator by it in variable time. The Python checks and conversions around that call are not constant time, nor is the Python arm. SECURITY.md’s limitations name the Python steps. curves.prv_key_tweak_add is the sum without the negation.
- ecc.ssa.pub_key_tweak_add_var(x_Q: int | bytes | str | bytearray | memoryview | tuple[int, int] | PreparedPoint, t: Public, 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 x-only key P + t*G, P the point of x_Q, and its y parity.
BIP341’s taproot output key: x_Q is the internal key, t the TapTweak, and the parity is what a script path spending commits to in its control block. The tweak is in 0..n-1. A sum at infinity, where t is the private key of -P, is refused.
Checking a taproot commitment is this call and a comparison of what it answers, which is what secp256k1_xonly_pubkey_tweak_add_check does too.
- ecc.ssa.sign(msg: bytes | str | bytearray | memoryview, prv_key: bytes | str | bytearray | memoryview | int, aux: bytes | str | bytearray | memoryview | 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), hf: Callable[[], ~ecc.alias.HashObject]=<built-in function openssl_sha256>, *, verify: bool = True, commit: None = None) Sig[source]¶
- ecc.ssa.sign(msg: bytes | str | bytearray | memoryview, prv_key: bytes | str | bytearray | memoryview | int, aux: bytes | str | bytearray | memoryview | 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), hf: Callable[[], ~ecc.alias.HashObject]=<built-in function openssl_sha256>, *, verify: bool = True, commit: bytes | str | bytearray | memoryview) tuple[Sig, tuple[int, int]]
Sign message according to BIP340 signature algorithm.
The message msg is first processed by hf, yielding the value
msg_hash = hf(msg),
a sequence of bits of length hf_len.
Normally, hf is chosen such that its output length hf_len is roughly equal to nlen, the bit-length of the group order n, since the overall security of the signature scheme will depend on the smallest of hf_len and nlen; however, ECSSA supports all combinations of hf_len and nlen.
The BIP340 deterministic nonce (not RFC6979) is used.
verify asks for the signature to be checked before it is answered with, as in sign_, which is where the default and the absence of a pub_key beside it are explained.
commit is a value to commit to inside the nonce, and is reduced by hf as msg is: sign_ is the spelling that takes the two hashes.
- ecc.ssa.sign_(msg: bytes | str | bytearray | memoryview, prv_key: bytes | str | bytearray | memoryview | int, aux: bytes | str | bytearray | memoryview | 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), hf: Callable[[], ~ecc.alias.HashObject]=<built-in function openssl_sha256>, *, verify: bool = True, commit_hash: None = None) Sig[source]¶
- ecc.ssa.sign_(msg: bytes | str | bytearray | memoryview, prv_key: bytes | str | bytearray | memoryview | int, aux: bytes | str | bytearray | memoryview | 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), hf: Callable[[], ~ecc.alias.HashObject]=<built-in function openssl_sha256>, *, verify: bool = True, commit_hash: bytes | str | bytearray | memoryview) tuple[Sig, tuple[int, int]]
Sign a message of any size according to BIP340 signature algorithm.
The message is signed as it is: BIP340 puts no size restriction on it, so this takes the BIP340 message itself, of any length. sign is the other spelling: it reduces its argument with hf first, which is what this package’s trailing underscore distinguishes – whether the caller prepared the input or the library does it.
If the deterministic nonce is not provided, the BIP340 specification (not RFC6979) is used.
verify asks for the signature to be checked before it is answered with, and defaults to True. Here the default is the specification’s and not only this library’s policy: BIP340 puts the step inside Default Signing – “If Verify(bytes(P), m, sig) returns failure, abort” – where ECDSA’s comes from Bitcoin Core. The flag exists for the caller who has measured the check against their own threat model, and dsa.sign_ is where the same keyword is described at length. A check that fails raises EccRuntimeError saying that signing produced a signature that does not verify – the words both arms use and dsa’s own – with what the verification saw kept as the cause.
No pub_key beside it, and that absence is a decision rather than an omission. What such an argument buys in dsa is the generator multiplication the check would otherwise do per signature; here there is none to save, the keypair holding the point already, so the check costs what it costs whether the caller holds the key or not – btclib-org/btclib-secp256k1#224 is where that is measured, and btclib-org/btclib#982 is where the two schemes are put side by side. It would buy nothing and sell one thing: a second reason a check can fail, and with it the discrimination step dsa._abort_unless_checked has to pay for.
commit_hash is a value to commit to inside the nonce, sign-to-contract style: the signature is an ordinary BIP340 one, and the receipt returned beside it is what opens the commitment (see ecc.commit_nonce). Keyword-only, and the only argument that changes what is returned, so that neither is easy to pass by accident. A commitment does not displace aux: it joins it, both of them reaching the nonce as BIP340’s auxiliary randomness.
- ecc.ssa.verify(msg: bytes | str | bytearray | memoryview, Q: int | bytes | str | bytearray | memoryview | tuple[int, int] | ~ecc.curves.curve.PreparedPoint, sig: Sig | bytes | str | bytearray | memoryview, hf: Callable[[], ~ecc.alias.HashObject]=<built-in function openssl_sha256>, *, commit: bytes | str | bytearray | memoryview | None = None, receipt: tuple[int, int] | None=None) bool[source]¶
Verify the BIP340 signature of hf(msg).
commit is reduced by hf as msg is; verify_ is the spelling that takes the two hashes. Raises and answers False for the same reasons verify_ does, and raises where the bindings are required and decline hf.
- ecc.ssa.verify_(msg: bytes | str | bytearray | memoryview, Q: int | bytes | str | bytearray | memoryview | tuple[int, int] | ~ecc.curves.curve.PreparedPoint, sig: Sig | bytes | str | bytearray | memoryview, hf: Callable[[], ~ecc.alias.HashObject]=<built-in function openssl_sha256>, *, commit_hash: bytes | str | bytearray | memoryview | None = None, receipt: tuple[int, int] | None=None) bool[source]¶
Verify the BIP340 signature of a message of any size.
The message is taken as it is; verify is the spelling that reduces it with hf first.
commit_hash and receipt open the commitment the nonce carries, and a signature that does not commit to that value is False as a forged one is: the answer is about this signature and this commitment, both.
Raises where the signature or the public key is structurally invalid – the wrong length, or spelled in a way BIP340 has no reading for – and answers False for a well-formed one that is merely not authentic: a scalar at or above the group order, a key that does not lift, an equation that does not hold. See _assert_structurally_valid_.
Where the bindings are required (curves.set_libsecp256k1_serving with required=True) and decline hf, it raises EccRuntimeError and does not answer False: nothing was checked.
ecc.ssa_adaptor module¶
Single-signer BIP340 adaptor signatures.
A pre-signature is made for an adaptor point T = t*G. Whoever knows t adapt`s it into an ordinary BIP340 signature, and whoever holds both the pre-signature and that signature recovers t with `extract_adaptor_var. extract_var returns the T a pre-signature is for.
The functions follow the schnorr_adaptor module that BlockstreamResearch/secp256k1-zkp#299 proposes (include/secp256k1_schnorr_adaptor.h). That pull request is open and unmerged, so it is a reference and not an authority: its encoding and its nonce can still change.
Apart from the _var suffix, one name and its argument order depart from it: extract_adaptor_var(sig, pre_sig) is PR 299’s extract_sec(pre_sig, sig). The name and the order are those of musig2.extract_adaptor, as in secp256k1-zkp’s MuSig2 module.
The reference has no pre-signature verification: its extract returns the adaptor point instead. verify here is that extract_var compared with the expected T.
The wire format is 65 octets, R’ || s. R’ = k*G + T is a 33-octet compressed point, and s = k + e*d where R’ has an even y, -k + e*d where it has an odd one. d is the private key of the x-only public key, and e the BIP340 challenge over x(R’), that key and the 32-octet message. Adding t, or -t for an odd R’, makes (x(R’), s) a BIP340 signature.
Nonces follow the reference’s nonce function: BIP340’s derivation, under the tags SchnorrAdaptor/aux and SchnorrAdaptor/nonce, with the compressed T between the masked key and the public key. presign draws a fresh aux when it is not given. Passing nonce fixes k instead: one k used for two pre-signatures reveals the private key.
Timing. k and d go through mult, and k, d and t through integer arithmetic. What extract_var handles is public and uses the _var arithmetic. As everywhere in Python here, that is not constant-time.
- ecc.ssa_adaptor.adapt(pre_sig: bytes | str | bytearray | memoryview, sec_adaptor: bytes | str | bytearray | memoryview | int) Sig[source]¶
Return the BIP340 signature a pre-signature adapts to under sec_adaptor.
Nothing here checks the result: where sec_adaptor is not the secret of the pre-signature’s adaptor point, the signature does not verify. ssa.verify_ is the caller’s to run.
- ecc.ssa_adaptor.assert_as_valid(pre_sig: bytes | str | bytearray | memoryview, msg: bytes | str | bytearray | memoryview, pub_key: int | bytes | str | bytearray | memoryview | tuple[int, int] | PreparedPoint, adaptor: bytes | str | bytearray | memoryview | tuple[int, int] | PreparedPoint) None[source]¶
Raise unless pre_sig is a pre-signature of msg by pub_key for adaptor.
verify is the spelling that answers True or False; this one says why.
- ecc.ssa_adaptor.extract_adaptor_var(sig: Sig | bytes | str | bytearray | memoryview, pre_sig: bytes | str | bytearray | memoryview) int[source]¶
Return the adaptor secret, from a pre-signature and its adaptation.
sig is an ssa.Sig or its 64 octets. An EccValueError where its r is not the x-coordinate of the pre-signature’s R’. An s that does not come from adapt yields a wrong secret, and nothing here can tell.
- ecc.ssa_adaptor.extract_var(pre_sig: bytes | str | bytearray | memoryview, msg: bytes | str | bytearray | memoryview, pub_key: int | bytes | str | bytearray | memoryview | tuple[int, int] | PreparedPoint) tuple[int, int][source]¶
Return the adaptor point of a pre-signature of msg by pub_key.
Any pre-signature that parses yields a point. Where the pre-signature was not made for this message and key, that point is not the adaptor point: verify is the call that compares.
An EccValueError for an R’ that is no point, an s not below n, and an R’ whose k*G or T is infinity.
- ecc.ssa_adaptor.presign(msg: bytes | str | bytearray | memoryview, prv_key: bytes | str | bytearray | memoryview | int, adaptor: bytes | str | bytearray | memoryview | tuple[int, int] | PreparedPoint, aux: bytes | str | bytearray | memoryview | None = None, *, nonce: bytes | str | bytearray | memoryview | int | None = None) bytes[source]¶
Return the 65-octet pre-signature of msg for the adaptor point.
msg is the 32-octet message, signed as it is. aux is 32 octets of auxiliary randomness, fresh when absent. nonce replaces k: see the module docstring.
- ecc.ssa_adaptor.verify(pre_sig: bytes | str | bytearray | memoryview, msg: bytes | str | bytearray | memoryview, pub_key: int | bytes | str | bytearray | memoryview | tuple[int, int] | PreparedPoint, adaptor: bytes | str | bytearray | memoryview | tuple[int, int] | PreparedPoint) bool[source]¶
Return True if pre_sig adapts, under adaptor’s secret, to a signature.
The signature is a BIP340 signature of msg by pub_key.
Raises where an argument cannot be read as what it is: a pre-signature that is not 65 octets, a message not of 32 octets, a key that is no key. Answers False for a pre-signature of the right size that does not hold: an R’ that is no point, an s not below n, or one that yields no adaptor point or another one. assert_as_valid says which.
ecc.ssa_halfagg module¶
Half-aggregation of BIP340 signatures, according to BIP458.
https://github.com/bitcoin/bips/blob/master/bip-0458.mediawiki
Half-aggregation turns u BIP340 signatures into one aggregate signature of 32*(u+1) bytes: the r of each signature, in order, then one scalar s, the sum of the signatures’ s values, each weighted by a randomizer. It needs no signer: anyone holding the signatures, the public keys and the messages can aggregate them. The aggregate is verified against the list of (public key, message) pairs, in the order they were aggregated.
inc_aggregate adds signatures to an existing aggregate, and aggregate is inc_aggregate starting from the aggregate of no signature. BIP458 does not specify how to merge two aggregates, and neither does this module.
Aggregating checks no signature. If every input signature is valid, the aggregate verifies. The converse does not hold: inputs that include an invalid signature can still give an aggregate that verifies, as BIP458 states. A caller who needs every input valid verifies each one with ssa.verify_ first.
Like ecc.musig2 and ecc.dleq, the curve and the hash function are not arguments: BIP458 is defined for secp256k1 and sha256 only. A public key is a 32-byte x-only key, a message is exactly 32 bytes, and a signature is BIP340’s 64 bytes.
- ecc.ssa_halfagg.aggregate(pms: Sequence[tuple[bytes | str | bytearray | memoryview, bytes | str | bytearray | memoryview, bytes | str | bytearray | memoryview]]) bytes[source]¶
Return the aggregate of (public key, message, signature) triples.
BIP458’s Aggregate: inc_aggregate starting from the aggregate of no signature, 32 zero bytes, and raising where it raises. No input signature is verified.
- ecc.ssa_halfagg.assert_as_valid(aggsig: bytes | str | bytearray | memoryview, pm_aggd: Sequence[tuple[bytes | str | bytearray | memoryview, bytes | str | bytearray | memoryview]]) None[source]¶
Refuse an aggregate signature that does not verify against pm_aggd.
BIP458’s VerifyAggregate, raising where it fails. pm_aggd lists the (public key, message) pairs in the order they were aggregated. verify is the spelling that answers True or False.
- ecc.ssa_halfagg.inc_aggregate(aggsig: bytes | str | bytearray | memoryview, pm_aggd: Sequence[tuple[bytes | str | bytearray | memoryview, bytes | str | bytearray | memoryview]], pms_to_agg: Sequence[tuple[bytes | str | bytearray | memoryview, bytes | str | bytearray | memoryview, bytes | str | bytearray | memoryview]]) bytes[source]¶
Return aggsig with the signatures of pms_to_agg aggregated into it.
BIP458’s IncAggregate. pm_aggd lists the (public key, message) pairs aggsig already aggregates, in order; pms_to_agg lists the (public key, message, signature) triples to add. If aggsig verifies against pm_aggd and every added signature is valid, the result verifies against pm_aggd followed by the pairs of pms_to_agg.
Raises EccValueError for an aggsig whose length is not 32 times the number of pairs plus one, for an s at or above the group order, in aggsig or in a signature, for a key, a message or a signature of the wrong size, and for a tuple in pm_aggd or pms_to_agg of the wrong arity. Neither aggsig nor the added signatures are verified, and no r or key is checked to be an x-coordinate.
- ecc.ssa_halfagg.verify(aggsig: bytes | str | bytearray | memoryview, pm_aggd: Sequence[tuple[bytes | str | bytearray | memoryview, bytes | str | bytearray | memoryview]]) bool[source]¶
Return True if the aggregate signature verifies against pm_aggd.
BIP458’s VerifyAggregate. pm_aggd lists the (public key, message) pairs in the order they were aggregated.
Raises where the input cannot be an aggregate signature and its pairs: a key or a message that is not 32 bytes, a tuple of the wrong arity, and an aggsig whose length is not 32 times the number of pairs plus one. Answers False for well-formed input that does not verify: an s at or above the group order, an r or a key that is no x-coordinate, an equation that does not hold. An empty pm_aggd is BIP458’s aggregate of no signature: True for 32 zero bytes, False for any other s.
Module contents¶
The ecc package: what it publishes, and the version metadata.
Elliptic curve arithmetic and the signature, commitment and key-agreement schemes built on it, over any curve in short Weierstrass form, with libsecp256k1 accelerating secp256k1 where the optional btclib-secp256k1 bindings are installed.
__all__ here is the root of the package’s public tree: the packages and top-level modules a caller reaches from this name. Each of those, and each module below them, declares its own __all__, so a walk that starts here has a declared surface at every node – which is why the list is not pkgutil.iter_modules: discovery would answer the file tree, and a module added to the directory would publish itself rather than being published.
name is not in it, nor are the metadata dunders. name is the distribution’s name and not a member of the tree, __version__ bound by a star import would overwrite the importing module’s own, and each is still an attribute here: ecc.__version__ is how a caller reads the version and ecc.name how it reads the name.
Nothing is imported eagerly. A module is imported when it is first asked for, through the __getattr__ at the bottom of this file, so import ecc is the metadata lookup below and nothing else, and the import graph keeps its shape: importing every module here would put the whole package in sys.modules before any single module of it could be imported first, which is the situation tests/imports_test.py exists to make impossible.
What that costs is worth stating: mypy reads a module-level __getattr__ as a promise that any attribute may exist, so ecc.cruves is Any to it and a misspelling on this package is a runtime AttributeError rather than a reported error. The spellings a caller actually writes – from ecc import curves, import ecc.curves, from ecc.curves import mult – resolve against the real modules and stay checked.