btclib_ecc.ecc package¶
Submodules¶
btclib_ecc.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.
- btclib_ecc.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[[], ~btclib_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.
btclib_ecc.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/ecc/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 btclib_ecc.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.
- btclib_ecc.ecc.borromean.assert_as_valid(msg: bytes | str | bytearray | memoryview, sig: ~btclib_ecc.ecc.borromean.BorromeanSig | bytes | str | bytearray | memoryview, pubk_rings: ~collections.abc.Sequence[~collections.abc.Sequence[tuple[int, int]]], ec: ~btclib_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[[], ~btclib_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 BTClibEccValueError 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.
- btclib_ecc.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: ~btclib_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[[], ~btclib_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 BTClibEccValueError 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.
- btclib_ecc.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: ~btclib_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[[], ~btclib_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.
- btclib_ecc.ecc.borromean.verify(msg: bytes | str | bytearray | memoryview, sig: ~btclib_ecc.ecc.borromean.BorromeanSig | bytes | str | bytearray | memoryview, pubk_rings: ~collections.abc.Sequence[~collections.abc.Sequence[tuple[int, int]]], ec: ~btclib_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[[], ~btclib_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.
btclib_ecc.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.
- btclib_ecc.ecc.commit_nonce.commit_entropy_(commit_hash: bytes | str | bytearray | memoryview, tag: bytes, hf: Callable[[], ~btclib_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.
- btclib_ecc.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[[], ~btclib_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.
- btclib_ecc.ecc.commit_nonce.commit_point_(commit_hash: bytes | str | bytearray | memoryview, receipt: tuple[int, int], tag: bytes, ec: ~btclib_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[[], ~btclib_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.
btclib_ecc.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 btclib_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. Every ECDH-shaped computation here 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.
- btclib_ecc.ecc.dh.diffie_hellman(dU: bytes | str | bytearray | memoryview | int, QV: tuple[int, int], size: int, shared_info: bytes | None = None, ec: ~btclib_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[[], ~btclib_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 of this package’s ECDH-shaped computations.
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/btclib-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/btclib-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.
btclib_ecc.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 curve, and the vectors exercise arbitrary generators. What is not an argument is the curve or the hash function, as in btclib_ecc.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 btclib_ecc.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.
- btclib_ecc.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.
- btclib_ecc.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.
A BTClibEccValueError 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.
- btclib_ecc.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_.
btclib_ecc.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. Verification 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, and in the leading-underscore
functions a test asks for it with.
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 btclib_ecc.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 btclib_ecc.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 greater than 2^128, a 33rd byte is added in front. Bitcoin has a “low s” rule for the s value to be below ec.n, 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 four things 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;
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;
and the two do not answer the same question. secp256k1_der_parse_integer treats an integer whose high bit is set as an overflow rather than as a malformed encoding: it zeroes the scalar and reports success, so a negative r parses to r = 0 instead of being refused. BIP66 refuses it, and so does the parser above. tests/ecc/der_test.py puts the whole malformed corpus to dsa.signature_verify and pins that one difference, the rest agreeing rule for rule – which is the pairing that bites others, issues btclib-org/btclib#680 and btclib-org/btclib#667 being two libraries that got it wrong in the other direction.
- 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 btclib_ecc.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[[], ~btclib_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(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.
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.
- btclib_ecc.ecc.dsa.anti_exfil_host_commit(rho: bytes | str | bytearray | memoryview, hf: Callable[[], ~btclib_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.
- btclib_ecc.ecc.dsa.anti_exfil_host_verify(msg_hash: bytes | str | bytearray | memoryview, key: bytes | str | bytearray | memoryview | tuple[int, int] | ~btclib_ecc.curves.curve.PreparedPoint, sig: ~btclib_ecc.ecc.dsa.Sig | bytes | str | bytearray | memoryview, rho: bytes | str | bytearray | memoryview, receipt: tuple[int, int], hf: ~collections.abc.Callable[[], ~btclib_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. False and not an exception for everything that fails, as
verify_answers: a rho of the wrong size is a rho this commitment does not open to.
- btclib_ecc.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[[], ~btclib_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.
- btclib_ecc.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[[], ~btclib_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.
- btclib_ecc.ecc.dsa.assert_as_valid(msg: bytes | str | bytearray | memoryview, key: bytes | str | bytearray | memoryview | tuple[int, int] | ~btclib_ecc.curves.curve.PreparedPoint, sig: Sig | bytes | str | bytearray | memoryview, hf: Callable[[], ~btclib_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.
- btclib_ecc.ecc.dsa.assert_as_valid_(msg_hash: bytes | str | bytearray | memoryview, key: bytes | str | bytearray | memoryview | tuple[int, int] | ~btclib_ecc.curves.curve.PreparedPoint, sig: Sig | bytes | str | bytearray | memoryview, hf: Callable[[], ~btclib_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 low-s rule belongs to the signer –
signapplies it – and to a script interpreter, which reads it off its own flags.
- btclib_ecc.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[[], ~btclib_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.
- btclib_ecc.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[[], ~btclib_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.
- btclib_ecc.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.
- btclib_ecc.ecc.dsa.recover_pub_key(key_id: int, msg: bytes | str | bytearray | memoryview, sig: Sig | bytes | str | bytearray | memoryview, hf: Callable[[], ~btclib_ecc.alias.HashObject]=<built-in function openssl_sha256>) tuple[int, int][source]¶
ECDSA public key recovery (SEC 1 v.2 section 4.1.6).
- btclib_ecc.ecc.dsa.recover_pub_key_(key_id: int, msg_hash: bytes | str | bytearray | memoryview, sig: Sig | bytes | str | bytearray | memoryview, hf: Callable[[], ~btclib_ecc.alias.HashObject]=<built-in function openssl_sha256>) tuple[int, int][source]¶
ECDSA public key recovery (SEC 1 v.2 section 4.1.6).
- btclib_ecc.ecc.dsa.recover_pub_keys(msg: bytes | str | bytearray | memoryview, sig: Sig | bytes | str | bytearray | memoryview, hf: Callable[[], ~btclib_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).
- btclib_ecc.ecc.dsa.recover_pub_keys_(msg_hash: bytes | str | bytearray | memoryview, sig: Sig | bytes | str | bytearray | memoryview, hf: Callable[[], ~btclib_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).
- btclib_ecc.ecc.dsa.recover_sec(key_id: int, msg: bytes | str | bytearray | memoryview, sig: Sig | bytes | str | bytearray | memoryview, hf: Callable[[], ~btclib_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.
- btclib_ecc.ecc.dsa.recover_sec_(key_id: int, msg_hash: bytes | str | bytearray | memoryview, sig: Sig | bytes | str | bytearray | memoryview, hf: Callable[[], ~btclib_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.
- btclib_ecc.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[[], ~btclib_ecc.alias.HashObject]=<built-in function openssl_sha256>, *, grind: bool = True, verify: bool = True, pub_key: bytes | str | bytearray | memoryview | tuple[int, int] | ~btclib_ecc.curves.curve.PreparedPoint | None=None, commit: None = None) Sig[source]¶
- btclib_ecc.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[[], ~btclib_ecc.alias.HashObject]=<built-in function openssl_sha256>, *, grind: bool = True, verify: bool = True, pub_key: bytes | str | bytearray | memoryview | tuple[int, int] | ~btclib_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.
- btclib_ecc.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[[], ~btclib_ecc.alias.HashObject]=<built-in function openssl_sha256>, *, grind: bool = True, verify: bool = True, pub_key: bytes | str | bytearray | memoryview | tuple[int, int] | ~btclib_ecc.curves.curve.PreparedPoint | None=None, commit_hash: None = None) Sig[source]¶
- btclib_ecc.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[[], ~btclib_ecc.alias.HashObject]=<built-in function openssl_sha256>, *, grind: bool = True, verify: bool = True, pub_key: bytes | str | bytearray | memoryview | tuple[int, int] | ~btclib_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 btclib_ecc.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.
- btclib_ecc.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[[], ~btclib_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.
- btclib_ecc.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[[], ~btclib_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.
- btclib_ecc.ecc.dsa.verify(msg: bytes | str | bytearray | memoryview, key: bytes | str | bytearray | memoryview | tuple[int, int] | ~btclib_ecc.curves.curve.PreparedPoint, sig: Sig | bytes | str | bytearray | memoryview, hf: Callable[[], ~btclib_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.
- btclib_ecc.ecc.dsa.verify_(msg_hash: bytes | str | bytearray | memoryview, key: bytes | str | bytearray | memoryview | tuple[int, int] | ~btclib_ecc.curves.curve.PreparedPoint, sig: Sig | bytes | str | bytearray | memoryview, hf: Callable[[], ~btclib_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 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 key that does not lift, a malformed DER encoding, an equation that does not hold. See _assert_structurally_valid_, the comment every boolean verification in this library points at.
btclib_ecc.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 btclib_ecc.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 the rest of
btclib_ecc.ecc: 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 btclib_ecc.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.
- btclib_ecc.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 BTClibEccRuntimeError and the caller’s cipher never runs.
- btclib_ecc.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, which is
btclib_ecc.ecc.dh’s verdict for every ECDH-shaped computation here: ecdh.shared_secret of the bindings hashes with SHA256 and BIE1 wants sha512 cut three ways.No infinity check on the shared point, unlike
btclib_ecc.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.
- btclib_ecc.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/btclib-ecc#11).
btclib_ecc.ecc.ellswift module¶
ElligatorSwift encoding of a public key: the SwiftEC map and its inverse.
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 the map. What BIP324 builds on it – the x-only ECDH over two encodings, the key schedule, the cipher and the packet framing – is a protocol’s, and not a fact about a curve.
decode is the map, and it is deterministic. encode 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.
- btclib_ecc.ecc.ellswift.create_var(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 private key is its own entropy for the encoding, which is what secp256k1_ellswift_create is for and what makes it preferable to encoding the public key: a caller cannot supply randomness that is a function of the key, because it supplies none.
- btclib_ecc.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.
- btclib_ecc.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. create is the better call when the private key is at hand.
btclib_ecc.ecc.frost module¶
FROST threshold Schnorr signing, according to BIP445.
https://github.com/bitcoin/bips/pull/2070, bip-0445.md at v0.10.0, head 8e25d579 on siv2r:bip-frost-signing. The BIP is a draft and not in bitcoin/bips master; tests/_data/README.md records why v0.10.0 is where this module pins.
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 btclib_ecc.ecc.ssa and with any other BIP340 verifier – the same output shape btclib_ecc.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. The vectors this module is tested against are generated by a trusted dealer, and 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#257 is where interactive threshold signing is tracked; it stays open, upstream-dependent, and is the hook that brings key generation and ChillDKG back once BIP445 or BIP-FROST-DKG settle further.
The round shape mirrors btclib_ecc.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 and apply_tweak, or thresh_pubkey_and_tweak for both in one call, answering a TweakContext;
round 1, the nonces: nonce_gen for each of the u selected signers, then nonce_agg 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 everywhere else in btclib_ecc.ecc, 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 btclib_ecc.ecc.ssa.challenge_.
Nothing here is shared with `btclib_ecc.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, apply_tweak – 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 btclib_ecc.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 btclib_ecc.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
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 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 btclib_ecc.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#257 is where producing it – a trusted dealer, ChillDKG – is tracked.
- class btclib_ecc.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 takes one key and not a list.
- btclib_ecc.ecc.frost.apply_tweak(tweak_ctx: TweakContext, tweak: bytes | str | bytearray | memoryview, is_xonly: bool) TweakContext[source]¶
Return the context tweaked by t, x-only or plain.
musig2.apply_tweak’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.
- btclib_ecc.ecc.frost.deterministic_sign(sec_share: bytes | str | bytearray | memoryview, my_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.
agg_other_nonce is omitted (None) by a sole signer, who has no other signers’ nonces to aggregate; 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.
- btclib_ecc.ecc.frost.nonce_agg(pub_nonces: Sequence[bytes | str | bytearray | memoryview]) bytes[source]¶
Aggregate the public nonces of round 1 into the 66-byte aggnonce.
musig2.nonce_agg’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.
- btclib_ecc.ecc.frost.nonce_gen(sec_share: bytes | str | bytearray | memoryview | 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.
- btclib_ecc.ecc.frost.nonce_gen_(rand_: bytes | str | bytearray | memoryview, sec_share: bytes | str | bytearray | memoryview | 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, pub_share and thresh_pk_xonly have no empty value at all, each being of a fixed size.
- btclib_ecc.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 btclib_ecc.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.
- btclib_ecc.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, tests/_data/README.md’s cited summary of the vendored vectors makes the same point of BIP445’s own reference.
- btclib_ecc.ecc.frost.partial_sig_verify_(psig: bytes | str | bytearray | memoryview, my_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.
- btclib_ecc.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.
- btclib_ecc.ecc.frost.sign(sec_nonce: bytearray, sec_share: bytes | str | bytearray | memoryview, my_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.
session_values runs first, for the same reason musig2.sign’s own docstring gives: a session that does not assemble raises before the nonce is touched, so the same bytearray may be used for the corrected session.
sec_nonce is checked exactly bytearray first, for the reason musig2.sign’s own docstring gives (issue btclib-org/btclib-ecc#11).
- btclib_ecc.ecc.frost.thresh_pubkey_and_tweak(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.
- btclib_ecc.ecc.frost.tweak_ctx_init(thresh_pk: bytes | str | bytearray | memoryview) TweakContext[source]¶
Return a fresh TweakContext over an untweaked threshold public key.
- btclib_ecc.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.
No test vector exercises this: tests/_data/README.md records why.
btclib_ecc.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 btclib_ecc.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, apply_tweak and key_agg_and_tweak, answering a KeyAggContext;
round 1, the nonces: nonce_gen for each signer, then nonce_agg 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 everywhere else in btclib_ecc.ecc. 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 btclib_ecc.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/ecc/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 btclib_ecc.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 btclib_ecc.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 (btclib_ecc.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 btclib_ecc.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 btclib_ecc.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
- btclib_ecc.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.
- btclib_ecc.ecc.musig2.apply_tweak(key_agg_ctx: KeyAggContext, tweak: bytes | str | bytearray | memoryview, is_xonly: bool) KeyAggContext[source]¶
Return the context tweaked by t, x-only or plain.
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.
- btclib_ecc.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.
- btclib_ecc.ecc.musig2.extract_adaptor(sig: Sig, pre_sig: PreSignature, session_ctx: SessionContext) bytes[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.
- btclib_ecc.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.
- btclib_ecc.ecc.musig2.key_agg(pub_keys: Sequence[bytes | str | bytearray | memoryview]) KeyAggContext[source]¶
Aggregate plain public keys into a KeyAggContext.
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.
- btclib_ecc.ecc.musig2.key_agg_and_tweak(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 in order.
- btclib_ecc.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.
- btclib_ecc.ecc.musig2.nonce_agg(pub_nonces: Sequence[bytes | str | bytearray | memoryview]) bytes[source]¶
Aggregate the public nonces of round 1 into the 66-byte aggnonce.
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.
- btclib_ecc.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.
- btclib_ecc.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.
- btclib_ecc.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 btclib_ecc.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.
- btclib_ecc.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.
- btclib_ecc.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.
- btclib_ecc.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. Nothing else in this module is delegated – key_agg, key_sort and nonce_agg 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 BTClibEccValueError a caller sees does not depend on which arm answered it.
- btclib_ecc.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’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 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.
- btclib_ecc.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 BTClibEccTypeError for a bytes or a list may simply retry with the bytearray this takes (issue btclib-org/btclib-ecc#11).
btclib_ecc.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 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.
- btclib_ecc.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.
- btclib_ecc.ecc.pedersen.bytes_from_commitment(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.
- btclib_ecc.ecc.pedersen.bytes_from_generator(Q: tuple[int, int]) bytes[source]¶
Return a generator as libsecp256k1-zkp serializes one.
secp256k1_generator_serialize, which is bytes_from_commitment’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.
- btclib_ecc.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 BTClibEccValueError 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.
- btclib_ecc.ecc.pedersen.commitment_from_octets(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, assert_as_valid, verify.
- btclib_ecc.ecc.pedersen.generator_from_octets(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.
- btclib_ecc.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 their duration follows neither; 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
- btclib_ecc.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[[], ~btclib_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/ecc/pedersen_test.py::test_second_generator pins that value against a literal, and tests/ecc/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
- btclib_ecc.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_.
btclib_ecc.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 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, and the min_value offset pubk_rings 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/ecc/rangeproof_test.py reads. ecc.pedersen._bytes_from_point 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 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’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 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 btclib_ecc.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 btclib_ecc.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.
- 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’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 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(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 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’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.
- 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.
- 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 btclib_ecc.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.
- btclib_ecc.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’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.
- btclib_ecc.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 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, which carries none of it.
- btclib_ecc.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.
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 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.
- btclib_ecc.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.
- btclib_ecc.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 and a state that refuses is False here and a BTClibEccValueError from assert_as_valid. See _proof_from.
btclib_ecc.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.
- btclib_ecc.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[[], ~btclib_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.
- btclib_ecc.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[[], ~btclib_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.
btclib_ecc.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 btclib_ecc.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 btclib_ecc.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 btclib_ecc.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[[], ~btclib_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 every signature is sign_’s, so wipe has no keypair to overwrite and the with still reads the same – what it does on that arm 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.
- btclib_ecc.ecc.ssa.anti_exfil_host_commit(rho: bytes | str | bytearray | memoryview, hf: Callable[[], ~btclib_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_.
- btclib_ecc.ecc.ssa.anti_exfil_host_verify(msg: bytes | str | bytearray | memoryview, Q: int | bytes | str | bytearray | memoryview | tuple[int, int] | ~btclib_ecc.curves.curve.PreparedPoint, sig: ~btclib_ecc.ecc.ssa.Sig | bytes | str | bytearray | memoryview, rho: bytes | str | bytearray | memoryview, receipt: tuple[int, int], hf: ~collections.abc.Callable[[], ~btclib_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. False and not an exception for everything that fails, as
verify_answers: a rho of the wrong size is a rho this commitment does not open to.
- btclib_ecc.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[[], ~btclib_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.
- btclib_ecc.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[[], ~btclib_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.
- btclib_ecc.ecc.ssa.assert_as_valid(msg: bytes | str | bytearray | memoryview, Q: int | bytes | str | bytearray | memoryview | tuple[int, int] | ~btclib_ecc.curves.curve.PreparedPoint, sig: Sig | bytes | str | bytearray | memoryview, hf: Callable[[], ~btclib_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”’.
- btclib_ecc.ecc.ssa.assert_as_valid_(msg: bytes | str | bytearray | memoryview, Q: int | bytes | str | bytearray | memoryview | tuple[int, int] | ~btclib_ecc.curves.curve.PreparedPoint, sig: Sig | bytes | str | bytearray | memoryview, hf: Callable[[], ~btclib_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.
- btclib_ecc.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] | ~btclib_ecc.curves.curve.PreparedPoint], sigs: ~collections.abc.Sequence[~btclib_ecc.ecc.ssa.Sig], hf: ~collections.abc.Callable[[], ~btclib_ecc.alias.HashObject] = <built-in function openssl_sha256>) None[source]¶
Refuse an invalid signature in a batch, reducing each message.
- btclib_ecc.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] | ~btclib_ecc.curves.curve.PreparedPoint], sigs: ~collections.abc.Sequence[~btclib_ecc.ecc.ssa.Sig], hf: ~collections.abc.Callable[[], ~btclib_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.
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.
- btclib_ecc.ecc.ssa.batch_verify(ms: ~collections.abc.Sequence[bytes | str | bytearray | memoryview], Qs: ~collections.abc.Sequence[int | bytes | str | bytearray | memoryview | tuple[int, int] | ~btclib_ecc.curves.curve.PreparedPoint], sigs: ~collections.abc.Sequence[~btclib_ecc.ecc.ssa.Sig], hf: ~collections.abc.Callable[[], ~btclib_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.
- btclib_ecc.ecc.ssa.batch_verify_(msgs: ~collections.abc.Sequence[bytes | str | bytearray | memoryview], Qs: ~collections.abc.Sequence[int | bytes | str | bytearray | memoryview | tuple[int, int] | ~btclib_ecc.curves.curve.PreparedPoint], sigs: ~collections.abc.Sequence[~btclib_ecc.ecc.ssa.Sig], hf: ~collections.abc.Callable[[], ~btclib_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_.Raises where a public key is structurally invalid – spelled in a way BIP340 has no reading for – and answers False otherwise: a failed verification and a well-formed key or scalar that is merely not authentic are both False, see _assert_structurally_valid_.
- btclib_ecc.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.
- btclib_ecc.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.
- btclib_ecc.ecc.ssa.point_from_bip340pub_key(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
- btclib_ecc.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[[], ~btclib_ecc.alias.HashObject]=<built-in function openssl_sha256>, *, verify: bool = True, commit: None = None) Sig[source]¶
- btclib_ecc.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[[], ~btclib_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.
- btclib_ecc.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[[], ~btclib_ecc.alias.HashObject]=<built-in function openssl_sha256>, *, verify: bool = True, commit_hash: None = None) Sig[source]¶
- btclib_ecc.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[[], ~btclib_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 BTClibEccRuntimeError 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 btclib_ecc.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.
- btclib_ecc.ecc.ssa.verify(msg: bytes | str | bytearray | memoryview, Q: int | bytes | str | bytearray | memoryview | tuple[int, int] | ~btclib_ecc.curves.curve.PreparedPoint, sig: Sig | bytes | str | bytearray | memoryview, hf: Callable[[], ~btclib_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.
- btclib_ecc.ecc.ssa.verify_(msg: bytes | str | bytearray | memoryview, Q: int | bytes | str | bytearray | memoryview | tuple[int, int] | ~btclib_ecc.curves.curve.PreparedPoint, sig: Sig | bytes | str | bytearray | memoryview, hf: Callable[[], ~btclib_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_.
Module contents¶
Module btclib_ecc.ecc.
The schemes. btclib_ecc.ecc holds what is built on an elliptic curve: dsa, ssa and borromean signatures, the MuSig2 aggregation of many ssa signers into one, the FROST threshold signing in which a subset of a group sharing one key signs for all of it, pedersen commitments, the Confidential Transactions rangeproof built over one and the rewind that reads a value, a blinding factor and a message back out of it, the Diffie-Hellman key agreement, the BIE1 ECIES built on top of it, the ElligatorSwift encoding of a public key, the BIP374 proof that two points share one discrete logarithm, and the RFC6979, BIP340 and sign-to-contract nonces. The curve arithmetic underneath is btclib_ecc.curves, and the rule between the two is that direction: ecc imports curves, never the other way round. The key derivation functions the agreement uses are btclib_ecc.kdf: a KDF is a hash construction with no curve in it, and its own module says why it is not here.
The two names are easy to conflate – everything here is also about curves – so the anchor is worth stating: from btclib_ecc.curves import mult, from btclib_ecc.ecc import dsa.
The schemes are what this package is for, so __all__ names them and
the import below binds each as a package attribute: without it, import
btclib_ecc.ecc followed by btclib_ecc.ecc.dsa.sign(…) would raise
AttributeError until something else in the process happened to import the
submodule, and the package would advertise the loose helpers alone
instead of the schemes behind them.
The three nonces are named the same way, as modules. A nonce derivation is a scheme of its own – RFC6979 has test vectors, BIP340’s auxiliary randomness is part of the signing standard, and sign-to-contract has its commitment and its opening – so btclib_ecc.ecc.rfc6979_nonce is the spelling, as btclib_ecc.ecc.dsa is.
What is not here is a module’s own functions, plain or prepared:
dsa.sign and dsa.sign_ are both in dsa.__all__ and neither is in
this one, and nor are musig2.key_agg, ecies.encrypt or
ellswift.decode_var. The loose helpers __all__ names above are the whole
of the exception. So the trailing underscore is no part of the
decision, and for some names there is no decision to make: dsa and ssa
both define sign_, verify_ and assert_as_valid_, ssa and
rfc6979_nonce both define challenge_, and a package-level export of
any of them would collide – as one of sign or of gen_keys would,
each defined by more than one of these modules.
Secrets. This is the package a private key is handed to, and what holds around it is conditional. What decides whether an operation here is one libsecp256k1 call is curves.curve._libsecp256k1_serves – the process-wide dispatch switch, the curve and the hash function – with the conditions of the call site anded onto it; whatever the conjunction declines runs the Python arithmetic, which the suite validates against the bindings but which is not constant-time. Nor is a Python object holding a secret zeroized, on either path. SECURITY.md’s limitations section states each condition, argument by argument, and README.md carries the short form.
- btclib_ecc.ecc.diffie_hellman(dU: bytes | str | bytearray | memoryview | int, QV: tuple[int, int], size: int, shared_info: bytes | None = None, ec: ~btclib_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[[], ~btclib_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 of this package’s ECDH-shaped computations.
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/btclib-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/btclib-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.
- btclib_ecc.ecc.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[[], ~btclib_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/ecc/pedersen_test.py::test_second_generator pins that value against a literal, and tests/ecc/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