Release notes¶
Notable changes are documented here. CHANGELOG.md is the record behind them: this file says what a user has to act on, that one says what changed.
Versions are calendar versions, YYYY.M.D: the
number says when a release was cut, and promises nothing about
compatibility, so a breaking change is announced in this file — read it
before upgrading, rather than a digit.
v2026.11.8¶
The schemes are top-level modules of
ecc, andecc.eccis removed (closes #482).import ecc.eccandfrom ecc.ecc import dsaraiseImportError, and no alias is kept.from ecc.ecc import dsabecomesfrom ecc import dsa, andecc.ecc.dsa.signbecomesecc.dsa.sign. The same holds for each moduleecc.eccpublished.from ecc.ecc import diffie_hellmanbecomesfrom ecc.dh import diffie_hellman.from ecc.ecc import second_generatorbecomesfrom ecc.pedersen import second_generator.
Act on it if you import from
ecc.ecc: change the import as above.Act on it if you install btclib, btclib-wallet, bip32-descriptors or btclib-node. Their latest releases on PyPI import
ecc.eccand set no upper bound onecc, so a fresh install resolves this release, and their modules that importecc.eccraiseImportErrorwhen imported. Pinecc<2026.11.8until that package releases the move (btclib-org/btclib#2954, btclib-org/btclib-wallet#729, btclib-org/bip32-descriptors#139, btclib-org/btclib-node#2242).
CHANGELOG.md’s own v2026.11.8 section has the rest.
v2026.11.7¶
Variable-time integer parameters are typed
ecc.alias.Public(closes #420, closes #419).PublicisNewType("Public", int), and mypy refuses a plainintwhere it is asked for. The run time converts what it converted before. They are:in
number_theory,aandbofxgcd_var, andaofmod_inv_var,legendre_symbol_var,mod_sqrt_varandtonelli_var, which took anint, andaofmod_inv_batch_var, which took aSequence[int]and takes aSequence[Public];xofCurveGroup.y_var,y_even_var,y_low_varandy_quadratic_residue_var, and ofcurves.curve.is_x_coordinate_var, which took anint;in
curves.curve,uandvofdouble_mult_var,toftweak_add_varandTweakChain.point, andscalarsofmulti_mult_var, aSequence[Public]where it was aSequence[Integer];tofcurves.sec_point.PubKeyTweakChain.tweak_add_varand ofssa.pub_key_tweak_add_var, which took anInteger.
Act on it if you type-check code that calls them: wrap each such argument as
Public(x), which also says the value is public. mypy refuses octets or a string where anIntegerbecamePublic: convert them to anintfirst.dsa.anti_exfil_host_verifyanswersFalsefor a signature whose s is above n // 2 (closes #465). Plaindsa.verify_still accepts both forms.Act on it if your device signs with
anti_exfil_sign(..., lower_s=False), or the host relied on a high s passing: sign with a low s.dsa.anti_exfil_host_verifyraises for a structurally invalid key or message hash, andssa.anti_exfil_host_verifyfor a structurally invalid key or signature, as theirverify_do; their docstrings promisedFalse(closes #466).
CHANGELOG.md’s own v2026.11.7 section has the rest.
v2026.11.6¶
No breaking changes. ecc.curves.PubKeyTweakChain is new: it adds
successive tweaks to a compressed SEC public key, as BIP32’s public
derivation does, and where the bindings serve it builds no Python point
(issue #456). CHANGELOG.md’s own v2026.11.6 section has the rest.
v2026.11.5¶
Empty sequences answer what their BIPs answer (closes #447, closes #448, closes #449).
ssa.batch_verifyandbatch_verify_answerTruefor an empty batch, andassert_batch_as_validandassert_batch_as_valid_return, as BIP340’s BatchVerify does. All four raisedEccValueError.ssa_halfagg.verifyanswersTrue, andassert_as_validreturns, for 32 zero bytes and no pairs, the aggregate of no signature, as BIP458’s verify vector 0 expects. Both raisedEccValueError.musig2.nonce_agg_varandfrost.nonce_agg_varraiseEccValueErrorfor an empty list, where BIP327 and BIP445 fail. Both returned 66 zero bytes.musig2.key_agg_varraises anEccValueErrorthat names the empty list, where its message was “not a multi_mult_var”.key_agg_and_tweak_var, andsession_valueson a session with no public keys, raise it too.musig2.partial_sig_aggandpartial_sig_agg_adaptorraiseEccValueErrorfor apsigslist whose length differs from the session’s public keys. With none, they returned a signature, or a pre-signature, whose s was the tweak term alone.
Act on it if you relied on the refusal to catch an empty batch or aggregate built by mistake: check the length before verifying.
Act on it if you pass musig2 or frost an empty list of keys or nonces, or
musig2.partial_sig_aggorpartial_sig_agg_adaptorapsigslist of another length: expectEccValueError. If you match “not a multi_mult_var”, match the new message.The
secp256k1extra asks forbtclib-secp256k1at 0.8.0.14 or later (closes #383). Itsdsarefuses a signature whose r or s is outside 1..n-1.Act on it if you pin
btclib-secp256k1below 0.8.0.14: drop the pin and upgrade, aspip install --upgrade 'ecc[secp256k1]'does. A plainpip installleaves an installedeccand its bindings where they are.
CHANGELOG.md’s own v2026.11.5 section has the rest.
v2026.11.2¶
No breaking changes. CHANGELOG.md’s own v2026.11.2 section has what the
release adds.
v2026.10.30¶
negateandnegate_jacraiseEccValueErrorfor a tuple of the wrong length (closes #377). They are methods ofCurveGroup, and so of everyCurve, and raisedEccTypeError.Act on it if you catch
TypeErrororEccTypeErrorthere: catchEccValueError,ValueErrororEccException.
CHANGELOG.md’s own v2026.10.30 section has the rest.
v2026.10.28¶
The release’s attestation names its signer workflow at a commit (issue btclib-org/.github#1870).
release.ymlcallsreusable-build.ymlofbtclib-org/.githubat the commit of its tagv2026.10.26, where it called it atrefs/heads/main.Act on it if you verify a release with
--signer-workflownamingreusable-build.yml@refs/heads/main: name the workflow without a ref, asSECURITY.mddoes.musig2 and frost refuse a
session_ctxthat is not aSessionContextasEccTypeError(closes #367).session_values,sign,partial_sig_verify_andpartial_sig_aggof both modules, andpartial_sig_agg_adaptor,adaptandextract_adaptorof musig2, raisedAttributeErrorwhere they read a field of it.Act on it if you catch
AttributeErrorthere: catchEccTypeError.
CHANGELOG.md’s own v2026.10.28 section has the rest.
v2026.10.23¶
No breaking changes. The secp256k1 extra asks for btclib-secp256k1 at
0.8.0.12 or later, past GHSA-8h6f-34jj-7p6c: upgrade btclib-secp256k1, or
install ecc[secp256k1].
v2026.10.20¶
The package is
ecc, on PyPI and on import, with no alias (issue #306). The distributionbtclib-eccisecc, and the import packagebtclib_eccisecc; the module tree under it is unchanged, sobtclib_ecc.ecc.dsaisecc.ecc.dsa.import btclib_eccfails withModuleNotFoundErrorwhere onlyeccis installed.Act on it: install
eccin place ofbtclib-ecc, and importeccwherever you importedbtclib_ecc.The exception classes drop the
BTClibprefix, with no alias.BTClibEccException,BTClibEccValueError,BTClibEccTypeErrorandBTClibEccRuntimeErrorareEccException,EccValueError,EccTypeErrorandEccRuntimeError, inecc.exceptions. Each derives from the same built-in as before.Act on it if you catch or raise one by name: use the new name.
The switch that refuses the bindings is
ECC_NO_LIBSECP256K1.BTCLIB_ECC_NO_LIBSECP256K1is ignored.Act on it if you set it: set
ECC_NO_LIBSECP256K1instead.ellswift.create_varisellswift.create, with no alias (closes #312). The signature is unchanged.Act on it if you call it: rename the import and the call.
Public names whose work is variable-time take the
_varsuffix, with no alias (issue btclib-org/btclib#2687). Inecc.ecc:rangeproof.RangeProof.pubk_rings,commit_nonce.commit_point_,dlc_oracle.sig_point_,ssa_adaptor.extractandextract_adaptor,pedersen.commitment_from_octets,generator_from_octets,bytes_from_commitmentandbytes_from_generator,musig2.key_agg,apply_tweak,nonce_aggandkey_agg_and_tweak, andfrost.tweak_ctx_init,apply_tweak,nonce_aggandthresh_pubkey_and_tweakare the same names with_var. A trailing underscore stays last:commit_point_var_,sig_point_var_.Act on it if you call one: add the suffix.
The release’s files are named after
ecc. The sdist, the wheel and the bill of materials areecc-<version>.tar.gz,ecc-<version>-py3-none-any.whlandecc-<version>.cdx.json, where earlier releases attachbtclib_ecc-<version>….gh attestation verifytakes--repo btclib-org/ecc, with the same--signer-workflowand--source-ref. Releases v2026.9.28 to v2026.10.17 take--repo btclib-org/btclib-ecc, the name their certificates carry, and v2026.9.26 takes--repo btclib-org/ellipticcurves.Act on it if a script downloads or verifies a release by name: use the new names for this release on.
The nonce derivations give up after 256 candidates (closes #275).
rfc6979_nonce,bip340_nonceandcommit_nonceraiseEccRuntimeErrorwhere a hash function never yields a candidate in 1..n-1, sodsa.sign_andssa.sign_raise where they looped for ever.Act on it if you sign with such a hash function: pass one that behaves as a hash.
RangeProofrefuses a ring commitment or a header field that is no integer asEccTypeError(issue #276, issue #277). A bool or float ring commitment was accepted, and a header field of the wrong type left as a builtinTypeErrororAttributeErrorundercheck_validity=False.max_valuerefuses amin_valueof 0.0 or “”, which it read as 0.Act on it if you build a
RangeProofby hand: pass ints.rangeproof’s refusals no longer quote the value they refuse (closes #301, closes #310). The exponent, minimum value, bit count and mantissa are out of the messages.Act on it if you match the old text: match the new one.
On PyPy, the Python arithmetic leaves gmpy2 unused (closes #290). A gmpy2 installed there no longer makes
Curve.aff_from_jac_varraise, and thenumber_theoryfunctions refuse anmpzoperand.Act on it if you run PyPy and uninstalled gmpy2 for v2026.10.16: nothing needs it uninstalled now. Pass
ints tonumber_theory.
v2026.10.17¶
rangeproofrefuses more malformed proofs (closes #279).RangeProof.assert_valid, which the constructor runs by default, refuses a sign bit that is no bool asBTClibEccTypeError; it took0and1.serialize(check_validity=False)refuses asBTClibEccValueErroran exponent outside 0..31 beside a nonzero mantissa, amin_valueoutside 0..2**64-1, sign bits or ring commitments the mantissa does not ask for, and a ring commitment wider than the curve’s field. It returned octets for an exponent from 32 to 255, too few sign bits or a wrong count of ring commitments; it raisedIndexErrorfor too many sign bits,ValueErrorfor any other exponent, andOverflowErrorfor the rest. A sign bit that is no bool is refused there too, asBTClibEccTypeError.assert_validandserializeno longer quote amin_valuethey refuse.Act on it if you build a
RangeProofby hand: pass bools as sign bits, and catchBTClibEccValueErrorwhere you caughtOverflowErrororIndexError.The low-R grind of
dsarefuses after 256 signatures (closes #278). Withgrind=True, the default,dsa.sign,dsa.sign_anddsa.Signer’ssignandsign_raiseBTClibEccRuntimeErrorwhere 256 signatures in a row have a high r. They looped for ever, as a hash function that ignores its input makes them.Act on it if you sign with such a hash function: pass
grind=False.
v2026.10.16¶
Two functions are renamed, with no alias (closes #232).
sec_point.point_from_octetsispoint_from_octets_var, and so is its re-exportcurves.point_from_octets.ssa.point_from_bip340pub_keyispoint_from_bip340pub_key_var. The suffix says that the time follows the key. Code that uses an old name fails withImportErrororAttributeError.Act on it if you call either: rename the import and the call, and hand it a public key only. btclib 2026.9.29 to 2026.10.9 and btclib-wallet 2026.9.30 to 2026.10.9 import the old names, so
import btclib.keyandimport btclib_wallet.bip32fail with this release. Use a release of each that names the new ones. Until then, keep btclib-ecc 2026.10.10.rangeproofrefuses an argument that is no integer asBTClibEccTypeError(closes #270).signtook a boolmin_value,expormin_bits, a floatexpand amin_bitsof 0.0.RangeProof.sign_key_idxandRangeProof.nonce_chaintook a boolvalue.RangeProoftook a boolexpormantissa, and a bool or floatmin_value.verifyof aRangeProofbuilt withcheck_validity=Falseraises where it answered False for a boolexpormantissaor aNonemantissa.Act on it if you passed one of these: pass an int.
frostrefuses more inputs (closes #261, closes #262, closes #252).nonce_genrefuses asigner_idof 128 or more.deterministic_signrefuses asigner_idoutsideidsasBTClibEccValueError, where -1 and 2**32 and above were a bareOverflowError.signrefuses asecnoncethat is not 64 bytes, anddeterministic_signanaux_randthat is not 32.Act on it if you passed one of these at another length or value: pass one BIP445 accepts.
ssa.batch_verifyandbatch_verify_raise for an empty batch (closes #222). They answered False.Act on it if you may pass an empty batch: test for it first.
The messages of some refusals change (closes #254, closes #256, closes #246).
str_from_stringof non-ASCII bytes andEnvelope.b64decodeof a non-ASCII envelope saynon-ascii character in <what> at position <n>, andhex_stringof a negative integer saysnegative integer. None of them quotes the input, and none chains the codec’s error.Act on it if you match the old text: match the new one.
On PyPy, part of the Python arithmetic raises
TypeErrorwhere gmpy2 is installed (issue #290). The Python arithmetic uses gmpy2 wherever gmpy2 imports, and PyPy’spow(a, -1, m)refuses gmpy2’s integers (pypy/pypy#5207):Curve.aff_from_jac_varraises where v2026.10.10 returned the point. Thegmpy2extra does not install gmpy2 on PyPy.Act on it if you run PyPy with gmpy2 installed: uninstall gmpy2.
v2026.10.10¶
In v2026.10.8 and earlier, FROST’s
deterministic_signcould leak a signer’s secret share (closes #168). It derived one nonce for a threshold key and for its negation, which share an x-only key. A coordinator that sent one session under both, with no public shares, got two partial signatures over one nonce, and their difference is the share. BIP445 0.11.0 derives the nonce from the share assignuses it, andfrostfollows BIP445 0.12.0.Act on it if you called
deterministic_signwithout public shares, with a coordinator you do not trust: upgrade, and treat that secret share as exposed.frostfollows BIP445 0.12.0’s interface (closes #168).my_idissigner_idinsign,deterministic_signandpartial_sig_verify_.nonce_genandnonce_gen_takesigner_idas their second parameter, beforepub_share. The nonces ofnonce_gen_anddeterministic_signdiffer from v2026.10.8’s for the same inputs, and so do the error messages.deterministic_signrefuses anagg_other_noncegiven for one signer, and one missing for more.Act on it if you use
frost: passsigner_id=where you passedmy_id=; passnonce_gen_asigner_idsecond,Nonewhere you have none; passnonce_gen’s arguments aftersec_shareby keyword. Re-pin any stored nonce, partial signature or error message.musig2.extract_adaptorreturns the adaptor secret as an int, not as its 32-byte big-endian encoding (closes #189).Act on it if you use the bytes: call
t.to_bytes(32, "big")on the result.
v2026.10.8¶
RewoundandNonceChainno longer print their fields (GHSA-2wgq-4cfh-5gqh, low). In v2026.10.2 and earlier, the default repr ofRewound(fromrangeproof.rewind) printed the blinding factor, the value and the message, and that ofNonceChain(fromRangeProof.nonce_chain) printed the blinding factors and the draws, which with the proof give the blinding factors back.Act on it if you log or print those results: upgrade, and treat a blinding factor, value or message that such a log holds as exposed.
v2026.10.2 fixed GHSA-r5pw-9wrg-m3mj: on a curve of cofactor above 1 (secp112r2, secp128r2),
ssarefuses a public key outside the subgroup generated by G, and batch verification refuses anrwhose point is outside it. Before, a forged signature verified under such a key, which nobody holds. secp256k1 is unchanged.Act on it if you verified
ssasignatures on one of these curves with an earlier release: verify them again. One under a key outside the subgroup now fails.The attestation bundle is attached as
v<version>.intoto.jsonlfrom v2026.10.2 on, where earlier releases attachv<version>.attestation.jsonl. A script that downloads it by name, or passes it togh attestation verify --bundle, uses the new name.
v2026.10.2¶
mult_pub_key,ecies.derive_keys,dsa.assert_as_valid,dsa.assert_as_valid_anddsa.sign_’spub_keyrefuse a hybrid public key (prefix0x06or0x07) asnot a public key(closes #75). Withbtclib-secp256k1installed they accepted it, and without it they refused it.Act on it if you pass such a key: convert it first, with
point_from_octets(key, hybrid=True), and pass the point.pedersen.verifyraisesBTClibEccValueErrorfor a generator off the curve, at infinity or, on a cofactor curve, outside the subgroup generated by G (closes #76). Before, it answered False for an off-curve generator and True for an opening under INF, which opens to every value. On a cofactor curvesecond_generatorreturns a different H where its old H was outside the subgroup, as at sha256 on secp112r2 and secp128r2.Act on it if you pass a generator of your own: pass one from
second_generatororgenerator_from_seed, or catchBTClibEccValueError. Commitments made under that old H no longer verify.Verifying a release’s attestation names a new signer and the tag.
gh attestation verifytakes--signer-workflow btclib-org/.github/.github/workflows/reusable-build.yml@refs/heads/mainand--source-ref refs/tags/v<version>; SECURITY.md has the command. Earlier releases keepreusable-attest.ymland take the same--source-ref.ssasigns with a different nonce on a curve whose order is longer than the hash’s output, such as sha256 on secp384r1 or secp521r1 (GHSA-m38m-987v-j55h). Every other pair, secp256k1 with sha256 included, signs as before, and old signatures still verify.Act on it if you signed with
ssaon such a pair: rotate the key, and re-pin any deterministic signature you stored.musig2.partial_sig_verifyraisesBTClibEccValueErrorfor a signer index outside0..len(pub_keys)-1(closes #78). A negative index used to select a signer from the end.Act on it if you pass one: pass the index counted from the front.
FROST takes only an int where it takes an integer (closes #77, closes #79).
n,t, an id and the signer index raiseBTClibEccTypeErrorfor anything else, a bool, a float or a str included, whereint()convertedn,tand the ids.partial_sig_verify_raises for an id not inids.Act on it if you pass such a value: pass an int.
v2026.9.30¶
No breaking changes. An installed btclib-secp256k1 older than the floor
the secp256k1 extra names still fails the import, and its ImportError
now names the installed version and that floor: upgrade btclib-secp256k1,
or install btclib-ecc[secp256k1].
v2026.9.28¶
Breaking changes¶
_utils.int_from_integerandhex_stringrefuse a0xstring whose digits are not ASCII hex, asinvalid hex integer: what follows 0x is not ASCII hex digits(closes #21). Before,int(i, 16)alone read0x1_0, and0xfollowed by fullwidth or Arabic-Indic digits, as 16, and quoted the string in its refusal. Only ASCII whitespace is stripped now: a0xstring padded with U+00A0, U+3000 or another characterstr.isspacecounts, ahead of the prefix or after its digits, is refused too — by the message above, or bybytes.fromhex’s own where the padding leaves neither a0xspelling nor valid hex. EveryIntegerparameter reads through here,curve.mult’s private keymamong them.Act on it if you pass such a string, or match this refusal’s text: pass ASCII hex digits, and match
invalid hex integeralone rather than the string it used to quote.
v2026.9.26¶
The first release of btclib-ecc: there is no earlier version of it
to upgrade from.