btclib.ecc package

Submodules

btclib.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.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.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.

btclib.ecc.bms module

Bitcoin message signing (BMS): address-based signatures over text.

A BMS signature proves control of the private key behind an address. The scheme is ECDSA under an envelope: the message is prefixed by the magic “Bitcoin Signed Message:n” string – so that a signed message can never double as a transaction signature – and the hash of the envelope is what is signed. The serialization is the compact 65-byte [1-byte recovery flag][32-byte r][32-byte s], customarily exchanged as base64 text, not the DER of transaction signatures.

A vague statement can be replayed out of the context it was signed for, so a message worth signing names its signer, its date, its addressee, and its purpose.

The scheme works on key pairs, the address only identifying one: the signer needs the private key behind the address, from a wallet or supplied directly. The verifier needs no public key at all, ECDSA allowing recovery: the candidate keys are implied by the signature, and the recovery flag says which candidate – and which address type – the signer meant. Explicitly, the recovery flag value is:

key_id + (4 if compressed else 0) + 27

where:

  • key_id is the index in the [0, 3] range identifying which of the recovered public keys is the one associated to the address

  • compressed indicates if the address is the hash of the compressed public key representation

  • 27 identifies a p2pkh address, which is the only kind of address supported by Bitcoin Core; when the recovery flag is in the [31, 34] range of compressed addresses, Electrum also checks for p2wpkh-p2sh and p2wpkh (segwit always uses compressed public keys); BIP137 (Trezor) uses, instead, 35 and 39 instead of 27 for p2wpkh-p2sh and p2wpkh (respectively)

rec flag

key id

address type

27

0

p2pkh uncompressed

28

1

p2pkh uncompressed

29

2

p2pkh uncompressed

30

3

p2pkh uncompressed

31

0

p2pkh compressed (also Electrum p2wpkh-p2sh/p2wpkh)

32

1

p2pkh compressed (also Electrum p2wpkh-p2sh/p2wpkh)

33

2

p2pkh compressed (also Electrum p2wpkh-p2sh/p2wpkh)

34

3

p2pkh compressed (also Electrum p2wpkh-p2sh/p2wpkh)

35

0

BIP137 (Trezor) p2wpkh-p2sh

36

1

BIP137 (Trezor) p2wpkh-p2sh

37

2

BIP137 (Trezor) p2wpkh-p2sh

38

3

BIP137 (Trezor) p2wpkh-p2sh

39

0

BIP137 (Trezor) p2wpkh

40

1

BIP137 (Trezor) p2wpkh

41

2

BIP137 (Trezor) p2wpkh

42

3

BIP137 (Trezor) p2wpkh

This implementation endorses the Electrum approach: a signature generated with a compressed WIF (i.e. without explicit address or with a compressed p2pkh address) is valid also for the p2wpkh-p2sh and p2wpkh addresses derived from the same WIF.

The BIP137 behaviour is available all the same: a compressed WIF supplemented at signing time with a p2wpkh-p2sh or p2wpkh address yields a signature valid for that address alone.

The message is signed and verified byte-for-byte as provided: btclib does not strip whitespace, because the signature must commit to the exact bytes it names. Bitcoin Core behaves the same everywhere, its Sign/Verify Message gui dialog included, and so does the Electrum CLI; the Electrum gui instead deliberately strips leading and trailing blanks from the message (https://github.com/spesmilo/electrum/issues/4327), so on a whitespace-padded message it disagrees with all of the above: a signature it produces commits to the stripped text, and a signature over the exact bytes never verifies there. Two mutually exclusive pull requests put both resolutions in front of Electrum: https://github.com/spesmilo/electrum/pull/10787 (strip in the one gui path that misses it, qml signing) and https://github.com/spesmilo/electrum/pull/10788 (never strip, matching Core and btclib).

https://github.com/bitcoin/bitcoin/pull/524

https://github.com/bitcoin/bips/blob/master/bip-0137.mediawiki

class btclib.ecc.bms.Sig(rf: int, dsa_sig: Sig, *, check_validity: bool = True)[source]

Bases: object

A Bitcoin Message Signature: recovery flag and ECDSA signature.

The flag, 27 to 42, carries the recovery key id and the address type the signer claims; the signature is an ordinary dsa.Sig on secp256k1. The wire form is the 65-byte compact serialization, customarily exchanged as base64 (b64encode/b64decode).

assert_valid() None[source]

Refuse a flag outside 27..42, a curve that is not secp256k1.

classmethod b64decode(data: bytes | str | bytearray | memoryview, *, check_validity: bool = True) Sig[source]

Return the verified components of the provided BMS signature.

The address-based BMS signature can be represented as (rf, r, s) tuple or as base64-encoding of the compact format [1-byte rf][32-bytes r][32-bytes s].

b64encode(*, check_validity: bool = True) str[source]

Return the BMS address-based signature as base64-encoding.

First off, the signature is serialized in the [1-byte rf][32-bytes r][32-bytes s] compact format, then it is base64-encoded.

classmethod parse(data: BytesIO | bytes | str | bytearray | memoryview, *, check_validity: bool = True) Sig[source]

Build a Sig from the 65-byte compact serialization.

serialize(*, check_validity: bool = True) bytes[source]

Return the 65-byte compact form: flag, then r, then s.

btclib.ecc.bms.assert_as_valid(msg: bytes | str | bytearray | memoryview, addr: bytes | str | bytearray | memoryview, sig: Sig | bytes | str | bytearray | memoryview) None[source]

Refuse a signature that does not open to the address.

The public key is recovered from the signature under the magic message envelope, then rendered as the address type the recovery flag claims; anything short of a match is an error, verify being the boolean answer.

btclib.ecc.bms.gen_keys(prv_key: int | bytes | str | bytearray | memoryview | BIP32KeyData | None = None, network: str | None = None, compressed: bool | None = None) tuple[str, str][source]

Return a private/public key pair.

The private key is a WIF, the public key is a base58 p2pkh address.

btclib.ecc.bms.sign(msg: bytes | str | bytearray | memoryview, prv_key: int | bytes | str | bytearray | memoryview | BIP32KeyData, addr: bytes | str | bytearray | memoryview | None = None) Sig[source]

Generate address-based compact signature for the provided message.

btclib.ecc.bms.verify(msg: bytes | str | bytearray | memoryview, addr: bytes | str | bytearray | memoryview, sig: Sig | bytes | str | bytearray | memoryview) bool[source]

Verify address-based compact signature for the provided message.

btclib.ecc.borromean module

Borromean ring signature functions.

References

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 – which is the only other implementation of this construction anything reads, 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. It did not always: issue #1070 found the message and the point swapped, and every signature this module produced before that fix does not verify after it 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 RELEASE_NOTES.md’s breaking-changes list has the “before” and “after” this cost.

_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 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. With the challenge hash aligned too (issue #1070), the primitive now interoperates over secp256k1 with sha256: a signature this module produces there verifies under zkp’s secp256k1_borromean_verify, and one zkp produces verifies here. That 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. Issue #1072 is that remaining distance, filed and decided wanted – after btclib-org/btclib-secp256k1#283 gives it something to check the answer against.

class btclib.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: object

A 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 why that is not only a format but, since issue #1070, an interoperable one.

assert_valid() None[source]

Refuse a bad curve, no rings, an empty ring, or an out-of-range s.

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 is what makes this the interoperable spelling. 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 183) rather than contradicting it – the two agree wherever ec is secp256k1, zkp’s only curve.

btclib.ecc.borromean.assert_as_valid(msg: bytes | str | bytearray | memoryview, sig: ~btclib.ecc.borromean.BorromeanSig | bytes | str | bytearray | memoryview, pubk_rings: ~collections.abc.Sequence[~collections.abc.Sequence[tuple[int, int]]], ec: ~btclib.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.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 BTClibValueError 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 #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 #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.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.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.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 #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 BTClibValueError naming the ring, the index and the ring’s size otherwise – a ring with no keys has none, whatever the index (issue #1094), and a value the ring’s size does not reach either (issue #1095).

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.borromean.verify(msg: bytes | str | bytearray | memoryview, sig: ~btclib.ecc.borromean.BorromeanSig | bytes | str | bytearray | memoryview, pubk_rings: ~collections.abc.Sequence[~collections.abc.Sequence[tuple[int, int]]], ec: ~btclib.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.alias.HashObject] = <built-in function openssl_sha256>) bool[source]

Return whether sig is a valid borromean ring signature of msg.

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.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. libsecp256k1’s own s2c module states it as the reason it refuses a custom 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.commit_nonce.commit_entropy_(commit_hash: bytes | str | bytearray | memoryview, tag: bytes, hf: Callable[[], ~btclib.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.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.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.commit_nonce.commit_point_(commit_hash: bytes | str | bytearray | memoryview, receipt: tuple[int, int], tag: bytes, ec: ~btclib.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.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.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.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 in btclib, and this is the place that says so (issue 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 – keys.pubkey_tweak_mul, which is that same C multiplication, in constant time – and 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;

  • silent_payments.shared_secret answers the point itself: BIP352 tags it with a counter afterwards, and a BIP375 psbt carries it as a point;

  • ecc.ellswift.xdh is the exception that proves the rule. BIP324 defines the hash, libsecp256k1 implements that definition, and it is delegated whole – ellswift.xdh is one call there.

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. Three of the four here do not, and the fourth already does.

btclib.ecc.dh.diffie_hellman(dU: int, QV: tuple[int, int], size: int, shared_info: bytes | None = None, ec: ~btclib.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.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 the multiplication of a point that is not the generator, which is the one case mult does not delegate: on secp256k1 it is secp256k1_ec_pubkey_tweak_mul that computes it here, at a fraction of what the Python endomorphism path costs and, dU being a secret, in constant time – which that path is not.

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 all four of btclib’s ECDH-shaped computations.

btclib.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.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.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.dleq.assert_proof_as_valid(A: bytes | str | bytearray | memoryview | BIP32KeyData | tuple[int, int] | PreparedPoint, B: bytes | str | bytearray | memoryview | BIP32KeyData | tuple[int, int] | PreparedPoint, C: bytes | str | bytearray | memoryview | BIP32KeyData | tuple[int, int] | PreparedPoint, proof: bytes | str | bytearray | memoryview, G: bytes | str | bytearray | memoryview | BIP32KeyData | 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.dleq.generate_proof(a: bytes | str | bytearray | memoryview | int, B: bytes | str | bytearray | memoryview | BIP32KeyData | tuple[int, int] | PreparedPoint, aux: bytes | str | bytearray | memoryview | None = None, G: bytes | str | bytearray | memoryview | BIP32KeyData | 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 BTClibValueError 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.dleq.verify_proof(A: bytes | str | bytearray | memoryview | BIP32KeyData | tuple[int, int] | PreparedPoint, B: bytes | str | bytearray | memoryview | BIP32KeyData | tuple[int, int] | PreparedPoint, C: bytes | str | bytearray | memoryview | BIP32KeyData | tuple[int, int] | PreparedPoint, proof: bytes | str | bytearray | memoryview, G: bytes | str | bytearray | memoryview | BIP32KeyData | 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.

btclib.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 the script engine’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.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.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: object

ECDSA 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 encoding is not delegated, and it is the one thing about a signature that is not (issue 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 680 and 667 being two libraries that got it wrong in the other direction.

assert_valid() None[source]

Refuse an r or s outside 1..n-1, or an r no x is congruent to.

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.

serialize(*, check_validity: bool = True) bytes[source]

Serialize an ECDSA signature to strict ASN.1 DER representation.

class btclib.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.alias.HashObject]=<built-in function openssl_sha256>)[source]

Bases: object

Sign 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 #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-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-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.dsa.anti_exfil_host_commit(rho: bytes | str | bytearray | memoryview, hf: Callable[[], ~btclib.alias.HashObject]=<built-in function openssl_sha256>) bytes[source]

Return the host’s commitment to rho: step 1 of the anti-exfil protocol.

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:

  1. the host draws rho and sends anti_exfil_host_commit(rho)

  2. the device answers with anti_exfil_signer_commit(msg_hash, prv_key, commitment), the point R its nonce will have

  3. the host reveals rho

  4. the device signs, anti_exfil_sign(msg_hash, prv_key, rho)

  5. the host checks anti_exfil_host_verify against 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 libsecp256k1 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.dsa.anti_exfil_host_verify(msg_hash: bytes | str | bytearray | memoryview, key: bytes | str | bytearray | memoryview | ~btclib.bip32.bip32.BIP32KeyData | tuple[int, int] | ~btclib.curves.curve.PreparedPoint, sig: ~btclib.ecc.dsa.Sig | bytes | str | bytearray | memoryview, rho: bytes | str | bytearray | memoryview, receipt: tuple[int, int], hf: ~collections.abc.Callable[[], ~btclib.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 libsecp256k1 calls the opening. 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.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.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.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.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.dsa.assert_as_valid(msg: bytes | str | bytearray | memoryview, key: bytes | str | bytearray | memoryview | ~btclib.bip32.bip32.BIP32KeyData | tuple[int, int] | ~btclib.curves.curve.PreparedPoint, sig: Sig | bytes | str | bytearray | memoryview, hf: Callable[[], ~btclib.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.dsa.assert_as_valid_(msg_hash: bytes | str | bytearray | memoryview, key: bytes | str | bytearray | memoryview | ~btclib.bip32.bip32.BIP32KeyData | tuple[int, int] | ~btclib.curves.curve.PreparedPoint, sig: Sig | bytes | str | bytearray | memoryview, hf: Callable[[], ~btclib.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 – sign applies it – and to the script engine, which reads it off its own flags.

btclib.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.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.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.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.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.dsa.recover_pub_key(key_id: int, msg: bytes | str | bytearray | memoryview, sig: Sig | bytes | str | bytearray | memoryview, hf: Callable[[], ~btclib.alias.HashObject]=<built-in function openssl_sha256>) tuple[int, int][source]

ECDSA public key recovery (SEC 1 v.2 section 4.1.6).

See Also: - https://crypto.stackexchange.com/questions/18105/how-does-recovering-the-public-key-from-an-ecdsa-signature-work/18106#18106

btclib.ecc.dsa.recover_pub_key_(key_id: int, msg_hash: bytes | str | bytearray | memoryview, sig: Sig | bytes | str | bytearray | memoryview, hf: Callable[[], ~btclib.alias.HashObject]=<built-in function openssl_sha256>) tuple[int, int][source]

ECDSA public key recovery (SEC 1 v.2 section 4.1.6).

See Also: - https://crypto.stackexchange.com/questions/18105/how-does-recovering-the-public-key-from-an-ecdsa-signature-work/18106#18106

btclib.ecc.dsa.recover_pub_keys(msg: bytes | str | bytearray | memoryview, sig: Sig | bytes | str | bytearray | memoryview, hf: Callable[[], ~btclib.alias.HashObject]=<built-in function openssl_sha256>) list[tuple[int, int]][source]

ECDSA public key recovery (SEC 1 v.2 section 4.1.6).

See Also: - https://crypto.stackexchange.com/questions/18105/how-does-recovering-the-public-key-from-an-ecdsa-signature-work/18106#18106

btclib.ecc.dsa.recover_pub_keys_(msg_hash: bytes | str | bytearray | memoryview, sig: Sig | bytes | str | bytearray | memoryview, hf: Callable[[], ~btclib.alias.HashObject]=<built-in function openssl_sha256>) list[tuple[int, int]][source]

ECDSA public key recovery (SEC 1 v.2 section 4.1.6).

See Also: - https://crypto.stackexchange.com/questions/18105/how-does-recovering-the-public-key-from-an-ecdsa-signature-work/18106#18106

btclib.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.alias.HashObject]=<built-in function openssl_sha256>, *, grind: bool = True, verify: bool = True, pub_key: bytes | str | bytearray | memoryview | ~btclib.bip32.bip32.BIP32KeyData | tuple[int, int] | ~btclib.curves.curve.PreparedPoint | None=None, commit: None = None) Sig[source]
btclib.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.alias.HashObject]=<built-in function openssl_sha256>, *, grind: bool = True, verify: bool = True, pub_key: bytes | str | bytearray | memoryview | ~btclib.bip32.bip32.BIP32KeyData | tuple[int, int] | ~btclib.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.

See https://www.rfc-editor.org/rfc/rfc6979.html#section-3.2

btclib.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.alias.HashObject]=<built-in function openssl_sha256>, *, grind: bool = True, verify: bool = True, pub_key: bytes | str | bytearray | memoryview | ~btclib.bip32.bip32.BIP32KeyData | tuple[int, int] | ~btclib.curves.curve.PreparedPoint | None=None, commit_hash: None = None) Sig[source]
btclib.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.alias.HashObject]=<built-in function openssl_sha256>, *, grind: bool = True, verify: bool = True, pub_key: bytes | str | bytearray | memoryview | ~btclib.bip32.bip32.BIP32KeyData | tuple[int, int] | ~btclib.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 btclib signature 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.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.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.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.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.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.dsa.verify(msg: bytes | str | bytearray | memoryview, key: bytes | str | bytearray | memoryview | ~btclib.bip32.bip32.BIP32KeyData | tuple[int, int] | ~btclib.curves.curve.PreparedPoint, sig: Sig | bytes | str | bytearray | memoryview, hf: Callable[[], ~btclib.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.

btclib.ecc.dsa.verify_(msg_hash: bytes | str | bytearray | memoryview, key: bytes | str | bytearray | memoryview | ~btclib.bip32.bip32.BIP32KeyData | tuple[int, int] | ~btclib.curves.curve.PreparedPoint, sig: Sig | bytes | str | bytearray | memoryview, hf: Callable[[], ~btclib.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.

btclib.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.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. btclib 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 btclib 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 btclib’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 btclib’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: 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.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: object

The 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.

b64encode(*, check_validity: bool = True) str[source]

Return the envelope as its base64 armor.

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.

serialize(*, check_validity: bool = True) bytes[source]

Return the envelope as the octets that go under the base64.

btclib.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 BTClibRuntimeError and the caller’s cipher never runs.

btclib.ecc.ecies.derive_keys(prv_key: bytes | str | bytearray | memoryview | int, pub_key: bytes | str | bytearray | memoryview | BIP32KeyData | 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.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.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.ecies.encrypt(msg: bytes, pub_key: bytes | str | bytearray | memoryview | BIP32KeyData | 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 btclib 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.

btclib.ecc.ellswift module

ElligatorSwift encoding of a public key, and the x-only ECDH on it.

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 ElligatorSwift key encoding and x-only ECDH, and each piece of BIP324’s v2 transport it leaves out has a reason of its own (issue 1066). The key schedule’s HKDF-SHA256 is not one of them: it is a construction over a hash, hmac and hashlib and nothing else, and it is kdf.hkdf.

  • ChaCha20-Poly1305 is the cipher, and ecc.ecies is where the rule about a cipher is stated: btclib takes one from its caller rather than shipping one. A cipher in the standard library is what would change that; a hand-rolled one is not, being the only implementation, on by default, for every installation, on a network path.

  • Forward-secure rekeying and length obfuscation are cipher invocations, so a caller-supplied cipher leaves them written against something no test here can exercise. They follow the cipher and cannot precede it.

  • Packet framing is the transport itself, which belongs beside a P2P client that btclib does not provide.

A complete transport therefore belongs in a separate optional package or extra, backed by an established cryptographic implementation and BIP324’s packet vectors.

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 btclib 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 three 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.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.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.ellswift.encode_var(pub_key: bytes | str | bytearray | memoryview | BIP32KeyData | 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.ellswift.xdh(ell_a: bytes | str | bytearray | memoryview, ell_b: bytes | str | bytearray | memoryview, prv_key: bytes | str | bytearray | memoryview | int, party: 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 the x-only ECDH shared secret of two ElligatorSwift keys.

party says which of the two encodings is the caller’s – 0 for ell_a, 1 for ell_b – because the secret is a hash of both encodings in a fixed order, and the other one is the key to multiply. The correspondence between the private key and the caller’s encoding is not checked: the two parties reach the same 32 bytes when it holds, and nothing here can tell that it does.

btclib.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.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. 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.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: 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.

Cross-validating this construction against zkp is btclib-org/btclib-secp256k1#156’s open question, not answered here: the vendored library, mainline bitcoin-core/secp256k1, has no adaptor support at all, so the round-trip tests below are this module’s only check for now. What would supply that delegation, btclib-org/btclib-secp256k1#283, is decided and waits on secp256k1-zkp#330 upstream.

class btclib.ecc.musig2.KeyAggContext(Q: tuple[int, int], gacc: int, tacc: int)[source]

Bases: object

The 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

property x_only_pub_key: bytes

Return the 32-byte x-only aggregate key to verify against.

class btclib.ecc.musig2.PreSignature(r: int, s: int)[source]

Bases: object

A 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.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.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: object

Everything 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.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: object

What 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 #1069, the same shape #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.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.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.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.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.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.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.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.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.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.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.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 btclib’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.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.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.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.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.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 btclib’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 #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 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 “Cross-validating this construction against zkp” paragraph), 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 BTClibValueError a caller sees does not depend on which arm answered it.

btclib.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 btclib/psbt/musig2.py and 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_coeffsign 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.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.

btclib.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.

btclib.ecc.pedersen.assert_as_valid(r: bytes | str | bytearray | memoryview | int, v: bytes | str | bytearray | memoryview | int, commitment: tuple[int, int], ec: ~btclib.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.alias.HashObject] = <built-in function openssl_sha256>) 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 #814). is_on_curve is deliberately not asked – that would refuse a wrong value too.

btclib.ecc.pedersen.commit(r: bytes | str | bytearray | memoryview | int, v: 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.alias.HashObject]=<built-in function openssl_sha256>) tuple[int, int][source]

Commit to v under blinding factor r, returning rG+vH.

H is second_generator, whose docstring has why nobody can open this to a different (r, v). r=0 mod n is refused: it commits with no blinding at all, Q is then v*H, 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 #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.

Checked here and nowhere else in the module: assert_as_valid recomputes the commitment through this function, and verify already turns the BTClibValueError this raises into False, the same way it does for every other invalid (r, v).

btclib.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.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 – 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 #287).

For (secp256k1, sha256), the pair used everywhere else in this module by default, 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; no published constant exists to pin it 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.pedersen.verify(r: bytes | str | bytearray | memoryview | int, v: bytes | str | bytearray | memoryview | int, commitment: tuple[int, int], ec: ~btclib.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.alias.HashObject] = <built-in function openssl_sha256>) bool[source]

Open the commitment and return True if valid.

btclib.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.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.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.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.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 as aux_rand32ndata being the name the C function underneath it gives the same 32 octets.

btclib.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.

class btclib.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: object

A 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.

assert_valid() None[source]

Refuse an r that is no x-coordinate, or an s outside 0..n-1.

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 btclib’s own script engine does after reading it.

serialize(*, check_validity: bool = True) bytes[source]

Return BIP340’s fixed-size r || s, 64 bytes on secp256k1.

class btclib.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.alias.HashObject]=<built-in function openssl_sha256>)[source]

Bases: object

Sign 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 the CHANGELOG 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.ssa.assert_as_valid(msg: bytes | str | bytearray | memoryview, Q: int | bytes | str | bytearray | memoryview | tuple[int, int] | ~btclib.curves.curve.PreparedPoint, sig: Sig | bytes | str | bytearray | memoryview, hf: Callable[[], ~btclib.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.ssa.assert_as_valid_(msg: bytes | str | bytearray | memoryview, Q: int | bytes | str | bytearray | memoryview | tuple[int, int] | ~btclib.curves.curve.PreparedPoint, sig: Sig | bytes | str | bytearray | memoryview, hf: Callable[[], ~btclib.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.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.curves.curve.PreparedPoint], sigs: ~collections.abc.Sequence[~btclib.ecc.ssa.Sig], hf: ~collections.abc.Callable[[], ~btclib.alias.HashObject] = <built-in function openssl_sha256>) None[source]

Refuse an invalid signature in a batch, reducing each message.

btclib.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.curves.curve.PreparedPoint], sigs: ~collections.abc.Sequence[~btclib.ecc.ssa.Sig], hf: ~collections.abc.Callable[[], ~btclib.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 the CHANGELOG 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.ssa.batch_verify(ms: ~collections.abc.Sequence[bytes | str | bytearray | memoryview], Qs: ~collections.abc.Sequence[int | bytes | str | bytearray | memoryview | tuple[int, int] | ~btclib.curves.curve.PreparedPoint], sigs: ~collections.abc.Sequence[~btclib.ecc.ssa.Sig], hf: ~collections.abc.Callable[[], ~btclib.alias.HashObject] = <built-in function openssl_sha256>) bool[source]

Batch verification of BIP340 signatures.

btclib.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.curves.curve.PreparedPoint], sigs: ~collections.abc.Sequence[~btclib.ecc.ssa.Sig], hf: ~collections.abc.Callable[[], ~btclib.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_; a failed verification and a malformed input are both False, a caller error still raises.

btclib.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.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.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.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.alias.HashObject]=<built-in function openssl_sha256>, *, verify: bool = True, commit: None = None) Sig[source]
btclib.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.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.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.alias.HashObject]=<built-in function openssl_sha256>, *, verify: bool = True, commit_hash: None = None) Sig[source]
btclib.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.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 btclib’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 BTClibRuntimeError 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-secp256k1#224 is where that is measured, and #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.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.ssa.verify(msg: bytes | str | bytearray | memoryview, Q: int | bytes | str | bytearray | memoryview | tuple[int, int] | ~btclib.curves.curve.PreparedPoint, sig: Sig | bytes | str | bytearray | memoryview, hf: Callable[[], ~btclib.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.

btclib.ecc.ssa.verify_(msg: bytes | str | bytearray | memoryview, Q: int | bytes | str | bytearray | memoryview | tuple[int, int] | ~btclib.curves.curve.PreparedPoint, sig: Sig | bytes | str | bytearray | memoryview, hf: Callable[[], ~btclib.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.

Module contents

Module btclib.ecc.

The schemes. btclib.ecc holds what is built on an elliptic curve: dsa, ssa, bms and borromean signatures, the MuSig2 aggregation of many ssa signers into one, pedersen commitments, the Diffie-Hellman key agreement, the BIE1 ECIES built on top of it, the ElligatorSwift encoding of a public key with the x-only ECDH on it, 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.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.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.curves import mult, from btclib.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 followed by btclib.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.rfc6979_nonce is the spelling, as btclib.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.xdh. The loose helpers __all__ names above are the whole of the exception. So the trailing underscore is no part of the decision, and for four 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 the four would collide – as one of sign would, which five of these modules define, or of gen_keys, which three do.

bms does from btclib.ecc import dsa, i.e. it imports a name from the package that is importing it, and the order of the line below does not have to work around it: a from package import name whose name is not yet an attribute falls back to importing package.name as a submodule, which is what happens here. tests/imports_test.py imports every module of the library with nothing else in sys.modules, which is the order that would find it if it did not.

Secrets. This is the package a private key is handed to, and what holds around it is conditional. A signature of secp256k1 with sha256 and a nonce btclib derives is one libsecp256k1 call; another curve, another hash function or a nonce of the caller’s 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.diffie_hellman(dU: int, QV: tuple[int, int], size: int, shared_info: bytes | None = None, ec: ~btclib.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.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 the multiplication of a point that is not the generator, which is the one case mult does not delegate: on secp256k1 it is secp256k1_ec_pubkey_tweak_mul that computes it here, at a fraction of what the Python endomorphism path costs and, dU being a secret, in constant time – which that path is not.

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 all four of btclib’s ECDH-shaped computations.

btclib.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.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 – 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 #287).

For (secp256k1, sha256), the pair used everywhere else in this module by default, 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; no published constant exists to pin it 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