Skip to content

Conversation

@a1denvalu3
Copy link
Collaborator

@a1denvalu3 a1denvalu3 commented Dec 3, 2025

Migrate from secp256k1-py to coincurve bindings for the secp256k1 library.

fixes #592

@a1denvalu3 a1denvalu3 marked this pull request as draft December 3, 2025 21:08
@a1denvalu3 a1denvalu3 marked this pull request as ready for review December 9, 2025 10:07
@a1denvalu3 a1denvalu3 requested a review from callebtc December 9, 2025 10:07
further fixes

serialize() -> to_hex()

fix step2_bob_dleq

more fixes

fix p2pk nonce + format

test_crypto fix

fix tests && non-determinism of schnorr signing

more fixes

type: ignore
PublicKey.__neg__ = PublicKeyExt.__neg__ # type: ignore
PublicKey.__sub__ = PublicKeyExt.__sub__ # type: ignore
PublicKey.mult = PublicKeyExt.mult # type: ignore
PublicKey.__mul__ = PublicKeyExt.__mul__ # type: ignore
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dumb comment: but it's nice when things fit nicely. 😄

@TheRealCheebs
Copy link
Contributor

This PR addresses #592

signature: str,
) -> bool:
pubkey = PublicKey(bytes.fromhex(public_key), raw=True)
pubkey = PublicKeyXOnly(bytes.fromhex(public_key)[1:])
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a PublicKeyCompressed in coincurve? Should be a tiny bit faster.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I followed https://ofek.dev/coincurve/api/ and it doesn't appear to have it.

@TheRealCheebs
Copy link
Contributor

awesome addition of benchmarks!

@callebtc
Copy link
Collaborator

callebtc commented Dec 20, 2025

nice benchmarks! this confirms that coincurve doesn't cause any performance issues. seems like it is more than 2x slower for the main BDHKE, but faster for schnorr sigs. that's a hefty price to pay. btw, I think the code for the benchmarks doesn't need to be included in the PR.

here are the results
image

@a1denvalu3
Copy link
Collaborator Author

Benchmarks show a drastic drop in performance. I'm questioning whether we should go ahead with this regardless.

@TheRealCheebs
Copy link
Contributor

The original motivation behind this move was because coincurve was better maintained than secp256k1 and others like CLN used it for their python plugins? That is at least what was in the initial issue/suggestion from July 2024. Are there other driving factors that came up in the last 18 months?

@a1denvalu3
Copy link
Collaborator Author

The original motivation behind this move was because coincurve was better maintained than secp256k1-py

Yes this was the motivation, but we also want to keep performance in mind.

@TheRealCheebs
Copy link
Contributor

TheRealCheebs commented Dec 23, 2025

I'm changing my take. secp256k1-py was last released November 2021. That was a bit more than a year before the first official release (0.2.0) of the secp256k1 library its composed of. They are now at 0.7.0, so a considerable amount of work has continued. And that work is missing from secp256k1-py. On the other hand, coincurve 21 explicitly upgraded the internal libsecp256k1 to version 0.6.0. It sounds like coincurve has moved towards removing all external runtime dependencies in v21 to reduce attack vectors. Its testing against modern python versions and has made a bunch of other security focused changes.

It seems like it's a big hit, especially when we look at the benchmarks, but I guess I wonder how it feels in the "real world". And I would personally be happy to trade some performance for safety in this space.

edit:
And there will continue to be things like #806 if we continue with secp256k1 (which feel less than ideal).

@a1denvalu3
Copy link
Collaborator Author

Benchmarks are now moved to a different branch

@prusnak
Copy link
Contributor

prusnak commented Dec 29, 2025

Benchmarks show a drastic drop in performance

Any ideas why this is happening? Both packages just wrap the native C code. Is the performance hit in the C code (old libsecp256k1 vs new one) or is it because of binding code in Python?

@TheRealCheebs
Copy link
Contributor

@prusnak From a line of Gemini questioning. It was pointing to Python-to-C translation layer and object lifecycle management as the slowdown. And it was stating that bench-marking is "over-blowing" the slowness vs what anyone would see in real world use cases.

  1. Object Instantiation vs. Static Calls

The primary difference is that secp256k1-py often allowed for "rawer" access to functions, whereas coincurve is strictly object-oriented.

The Hit: Every time you create a PrivateKey or PublicKey object in coincurve, Python has to allocate memory, run the init method, and perform validation checks (like ensuring the key is within the valid curve range).

The Scale: If you are signing 10,000 messages and creating a new PrivateKey object for each one instead of reusing a single instance, the overhead of Python object creation will far outweigh the microseconds spent on the actual C-level ECDSA math.

  1. Defensive Validation (The "Robustness" Cost)
    coincurve is designed to be "safest by default."

Point Validation: When you load a public key, coincurve doesn't just treat it as a blob of bytes. It calls secp256k1_ec_pubkey_parse to ensure the point actually lies on the curve.

Normalization: It often performs extra checks to ensure signatures are in the "Lower-S" form (to prevent malleability), which is a requirement for modern blockchains like Ethereum but adds a few extra logic gates in the wrapper code.

Comparison: secp256k1-py frequently bypassed these checks if you used the lower-level functional interface, essentially "trusting" that your input data was already sanitized.

  1. CFFI vs. C-Extensions (The Bridge)
    Both libraries use CFFI (C Foreign Function Interface), but they use it differently:

Type Marshalling: coincurve does more "translation" between Python types (like bytes) and C types (like unsigned char*). It ensures that memory is correctly pinned and that pointers are handled safely to avoid segfaults.

The Context Object: libsecp256k1 requires a "context" (a pre-computed table of values) to run fast. coincurve manages a global context for you. While this is generally a performance win, the internal logic to ensure that context is thread-safe and properly initialized adds a tiny layer of "bookkeeping" to every call.

And the areas to clean up if we see real world performance hits would be around

Reuse Objects: Never call PrivateKey(seed) inside a loop. Create the object once and call .sign() repeatedly.

Batch Verification: If you are verifying many signatures, look for batching opportunities. coincurve is optimized for repeated use of the same PublicKey object.

Avoid Re-parsing: If you have public keys in byte format, parse them into PublicKey objects once and store the objects in a cache rather than re-parsing the bytes every time you need to verify a signature.

It looks like Coincurve is the definitive standard now for python bindings to the secp256k1 curve, and I think the trade offs between safety and robustness/maintained code between the two should trump the performance hits we see when thrown in sandboxed benchmarks.

@prusnak
Copy link
Contributor

prusnak commented Jan 5, 2026

Point Validation: When you load a public key, coincurve doesn't just treat it as a blob of bytes. It calls secp256k1_ec_pubkey_parse to ensure the point actually lies on the curve.

I think this might be the cause of most of the performance issue we probably.

If you are signing 10,000 messages and creating a new PrivateKey object for each one instead of reusing a single instance, the overhead of Python object creation will far outweigh the microseconds spent on the actual C-level ECDSA math.

+1

And it was stating that bench-marking is "over-blowing" the slowness vs what anyone would see in real world use cases.

+1

It looks like Coincurve is the definitive standard now for python bindings to the secp256k1 curve, and I think the trade offs between safety and robustness/maintained code between the two should trump the performance hits we see when thrown in sandboxed benchmarks.

+1

@callebtc
Copy link
Collaborator

callebtc commented Jan 6, 2026

LGTM

@a1denvalu3 a1denvalu3 merged commit 6eea646 into cashubtc:main Jan 6, 2026
180 of 186 checks passed
@a1denvalu3 a1denvalu3 deleted the refactor/coincurve branch January 6, 2026 10:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

migrate from secp256k1 to coincurve

4 participants