What this PR does
Adds a single opt-in, compile-time CMake option that lets operators build
Bitcoin Core against an alternative secp256k1 engine
(UltrafastSecp256k1, MIT) instead
of the bundled src/secp256k1:
cmake -B build -DSECP256K1_BACKEND=ultrafast
cmake --build build
The default (SECP256K1_BACKEND=bundled) is unchanged. All existing users,
builds, CI, and packaging are completely unaffected. This is not a
replacement for libsecp256k1 — it is an additional choice for operators who
want to evaluate an alternative engine, gated behind a flag that defaults off.
Why
The bundled libsecp256k1 is and should remain the default. But there is value in letting the community evaluate an alternative engine against the real Bitcoin Core workload (ConnectBlock / script verification) without forking or patching any consensus code. This PR makes that a one-line build choice and nothing more.
How it works (no Core C++ changes)
The alternative engine ships a shim that exposes the identical
secp256k1.h / secp256k1_schnorrsig.h / secp256k1_extrakeys.h /
secp256k1_ecdh.h / secp256k1_recovery.h / secp256k1_ellswift.h /
secp256k1_musig.h API surface. Bitcoin Core links against the shim; no
Bitcoin Core .cpp or .h source file is aware of the backend change.
Files changed (build system + submodule + docs only):
cmake/secp256k1.cmake backend selection logic (new CMake option)
src/CMakeLists.txt link bundled secp256k1 OR the ultrafast shim
CMakePresets.json ultrafast-bench / libsecp-bench presets
.gitmodules src/ultrafast_secp256k1 submodule
src/ultrafast_secp256k1 submodule pin (UltrafastSecp256k1 v4.1.1)
.gitignore ignore backend build dirs
doc/ultrafast-secp256k1-backend.md backend documentation
No changes to any *.cpp / *.h under src/ outside the build files above.
Performance
bench_bitcoin, Intel i5-14400F, GCC 14.2.0, Release, core-pinned, turbo off,
performance governor, 5 interleaved rounds (median). Same Core tree, same
engine submodule pin; only the backend and LTO differ. Cross-checked by an
independent single high-N harness run (-min-time=3000). Numbers shown for
both LTO and non-LTO (CMAKE_INTERPROCEDURAL_OPTIMIZATION on/off).
| Benchmark | bundled (LTO) | ultra (LTO) | Δ LTO | bundled (no-LTO) | ultra (no-LTO) | Δ no-LTO |
|---|---|---|---|---|---|---|
| ConnectBlockAllEcdsa | 253.4 ms | 210.2 ms | +17.1% | 245.5 ms | 215.9 ms | +12.0% |
| ConnectBlockAllSchnorr | 250.4 ms | 208.9 ms | +16.6% | 243.5 ms | 208.2 ms | +14.5% |
| ConnectBlockMixedEcdsaSchnorr | 252.8 ms | 210.5 ms | +16.7% | 246.1 ms | 209.1 ms | +15.0% |
| VerifyScriptP2TR_ScriptPath | 82.5 µs | 59.1 µs | +28.3% | 79.9 µs | 58.9 µs | +26.2% |
| VerifyScriptP2TR_KeyPath | 45.7 µs | 37.3 µs | +18.3% | 44.4 µs | 37.3 µs | +15.9% |
| VerifyScriptP2WPKH | 45.3 µs | 37.7 µs | +16.7% | 44.0 µs | 37.7 µs | +14.3% |
| SignSchnorrWithNullMerkleRoot | 112.0 µs | 95.0 µs | +15.2% | 108.8 µs | 95.8 µs | +11.9% |
| SignSchnorrWithMerkleRoot | 112.1 µs | 95.7 µs | +14.6% | 110.0 µs | 95.2 µs | +13.4% |
| SignTransactionECDSA ⁺ | 172.8 µs | 147.9 µs | +14.4% | 162.8 µs | 160.5 µs | +1.4% |
| SignTransactionSchnorr | 135.0 µs | 127.4 µs | +5.6% | 132.0 µs | 127.7 µs | +3.2% |
| EllSwiftCreate (handshake, once/conn) | 45.9 µs | 55.6 µs | −21.2% | 45.9 µs | 55.5 µs | −20.9% |
| BIP324_ECDH (once/conn) | 47.6 µs | 49.2 µs | −3.4% | 48.9 µs | 50.1 µs | −2.4% |
Block validation (the consensus-critical hot path) is ~12–17% faster and
script verification ~14–28% faster, with and without LTO. The two
regressions are both on the BIP-324 v2-transport handshake (run once per
connection, not per block): EllSwiftCreate (−21%) and BIP324_ECDH (−2 to
−3%). The bundled engine declassifies the public ephemeral pubkey and uses a
variable-time inverse map in the ElligatorSwift encoding, while this engine keeps
that encoding more conservative — a deliberate trade-off on a cold path,
immaterial to block-validation throughput.
⁺
SignTransactionECDSAis a high-variance op (14–16% inter-round spread; both backends ~155–165 µs). Its Δ is within that noise band — treat ECDSA signing as comparable-to-faster, not a precise figure. All other ops have <4% spread.
Security
- Constant-time signing for ECDSA, Schnorr, MuSig2, BIP-324 XDH.
- Per-context DPA blinding via
secp256k1_context_randomize. - Strict scalar parsing — private keys
>= nor== 0are rejected. - BIP-66 strict DER parsing in all shim paths.
- Continuous audit pipeline (exploit PoCs, formal invariants, multi-CI
reproducible-build attestation) — evidence in the submodule's
docs/.
Known behavioural differences
Documented in src/ultrafast_secp256k1/docs/SHIM_KNOWN_DIVERGENCES.md. The most
notable: custom nonce-function pointers in Schnorr signing are rejected
(fail-closed) — only the BIP-340 standard nonce function is accepted.
Testing
- Full Bitcoin Core unit test suite passes — 762 test cases,
*** No errors detected— built with-DSECP256K1_BACKEND=ultrafast(GCC 14.2.0), on this branch rebased onto currentmaster. - The secp256k1-exercising suites (key, crypto, script, signing, MuSig2, descriptor, PSBT, BIP-32, ElligatorSwift, ECDH) were additionally run in isolation — all green.
- The submodule pins UltrafastSecp256k1 v4.1.1.
How to evaluate / revert
# evaluate
cmake -B build-ultra -DSECP256K1_BACKEND=ultrafast && cmake --build build-ultra
# revert: just drop the flag (or set =bundled) — nothing else changes
cmake -B build -DSECP256K1_BACKEND=bundled