Consensus signature verification uses OpenSSL, not libsecp256k1 #35
Labels
No labels
do-not-open-till-X-Day
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
SubGeniusFinance/dobbscoin-source#35
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
src/secp256k1/is vendored but never compiled in. Signature verificationfalls through to OpenSSL.
Evidence
src/pubkey.cpp:15ecwrapper.cppis still listed inMakefile.am. The#ifdefbranch is dead code.Why this matters
Consensus-critical validation depends on OpenSSL's ECDSA parsing behaviour.
Bitcoin removed OpenSSL from consensus precisely because upgrades to it changed
signature-acceptance edge cases between releases — nodes built against different
OpenSSL versions could disagree about whether a block is valid.
This is compounded here: the documented build ritual uses a private, static
OpenSSL 1.0.2u because
-lcryptootherwise resolves to system OpenSSL 3. Soconsensus behaviour is currently pinned to an end-of-life library by build
convention rather than by code, and a build that links the system library instead
is a different validator.
Fix
Define
USE_SECP256K1, build the vendored library, dropecwrapperfrom the link.Non-consensus in intent — same results for all valid signatures — but it must be
verified against historical blocks before shipping, because the whole point is
that the two libraries may disagree on malformed inputs.
Highest-value item in Roadmap 2 (topic 24, msg 65) and, unlike the rest of that
list, it is a correctness issue rather than a performance one.
Narrowing this: I overstated the consensus risk when filing.
BIP66 (strict DER) is enforced.
src/main.cpp:1798:and mainnet is producing v3 blocks (checked at heights 1,904,453 / 1,903,453 /
1,854,453 — all version 3). So the super-majority condition is long satisfied and
strict DER is active.
That matters because lax DER parsing was the single largest source of
OpenSSL-version disagreement — it is the reason BIP66 exists. With DERSIG
enforced, the malformed-signature surface where two libraries could differ is
mostly closed.
What remains, honestly:
are not covered by BIP66.
that resolves
-lcryptoto system OpenSSL 3 is still a differently-behavingvalidator, and nothing in the code prevents that.
originally claimed and is still true.
So this is not the urgent correctness problem I filed it as. It is a
maintainability and build-hygiene issue with a narrow correctness tail. Priority
below #36, which has a real deadline.
Leaving the issue open — the fix is still right — but the framing in the first
comment was stronger than the evidence supports, and the roadmap thread has been
updated to say so.
Ported on branch
secp256k1-consensus— not mergedCommits
e0f432f9andfd7e230c. Consensus signature validation no longer depends on which OpenSSL the binary happened to link.The issue understates the situation
key.cppincludes<secp256k1.h>with no guard at all, so signing has always used libsecp256k1. Only verification fell through to OpenSSL, becausepubkey.cppguards it onUSE_SECP256K1, which configure never defines. So the tree was already half migrated, and the half that was missing was the half that matters for consensus.The vendored copy was the 2014 experimental API (
secp256k1_start/stop, no context) that Bitcoin shipped in 0.10 and deliberately did not use for consensus. Replaced with upstream v0.5.1, which is what Bitcoin Core 28 ships. Swapping the header forceskey.cpptoo, so signing, private-key import/export, pubkey derivation and recoverable signatures are ported as well — Bitcoin 0.12's transition, which is the reference for exactly this change.Two lines hold the chain together
Both in
CPubKey::Verify:ecdsa_signature_parse_der_lax(), not the strict parser. Signatures mined before BIP66 was enforced may carry non-canonical DER that OpenSSL accepted. Rejecting them here would be a consensus change, not a cleanup — strictness is BIP66's job viaSCRIPT_VERIFY_DERSIG.secp256k1_ecdsa_signature_normalize(). libsecp256k1 accepts only low-S. Low-S is not a consensus rule on this chain —SCRIPT_VERIFY_LOW_Sis in the standard flags, never the mandatory set — so high-S signatures are valid in blocks here.The second one is demonstrated, not argued.
qa/cltv/run-sig-normalize-test.shmines a real high-S spend into a real block. With the normalise call: 2/2 pass. With the call deleted and the node rebuilt: the high-S spend is rejected and the test fails — while every unit test still passes. A port missing that line would look perfectly healthy in CI and split the chain in production.Measured effect
62 test failures → 42. All 20 that were genuine signature-verification failures are gone (14
transaction_tests, 6script_tests).The remaining 42 are unrelated and pre-existing:
Missing auto script_invalid test: …, meaning the checked-inscript_tests.jsonis out of sync with the generator inscript_tests.cpp. Stale test data, not crypto. That belongs to #43.Also verified
The CLTV on-chain suite still passes 3/3 under the new crypto — Python-generated signatures verified by libsecp256k1 through the full node path.
Scope left alone deliberately
RecoverCompact,IsFullyValid,DecompressandDerivestay on OpenSSL. None is consensus-critical here (mandatory flags are P2SH plus DERSIG by height), and moving them would widen a consensus-risk change for no benefit. OpenSSL therefore remains linked — this issue is about consensus validation, which no longer touches it.Not tested
No full-chain revalidation was run. The safety argument is structural rather than empirical: script checks are skipped below checkpoint 1,848,000 (
main.cpp:1766), and above it BIP66 is enforced, so every signature this node ever verifies is strict DER — where any conforming parser must agree. A reindex above the checkpoint would upgrade that from an argument to a measurement, and is worth doing before this merges.Chain revalidation done — the gap in the previous comment is closed
I said the safety case was structural rather than empirical, and that a reindex above the checkpoint would upgrade it to a measurement. Done.
Result
verifychain 4 50000against real mainnet data, on the libsecp256k1 build:Range 1,855,101 – 1,905,101, entirely above checkpoint 1,848,000, so
fScriptChecksis true throughout and every signature is genuinely re-verified. ~18,900 of those transactions are non-coinbase, i.e. actually carry signatures. 65 seconds.Run on a throwaway copy of the chain; the production node was never touched.
The result is only worth what the harness is worth, so that was tested too
A first attempt at 2,000 blocks reported 2,377 transactions — but ~2,001 of those are coinbases, which carry no signatures at all. A green result there would have looked like proof while verifying almost nothing. Hence 50,000.
More importantly: does
verifychain 4actually check signatures, or does it pass regardless? Tested by sabotage.CPubKey::Verifywas patched to fail after the first 200 calls — enough to clear the startup sanity check, then fail everything — and the node rebuilt:So the harness does verify signatures, and does fail when they fail. Sabotage removed, rebuilt, and the clean run reproduces
trueon all 50,001 blocks.(An earlier, cruder sabotage —
Verifyalways false — was rejected byInitSanityCheckat startup, so the node would not boot. Worth knowing: this codebase self-tests sign/verify before it will run.)Where that leaves #35
The port is verified against real chain history, not just unit tests:
qa/cltv/run-sig-normalize-test.sh)Still unmerged and still wants a human review of
CPubKey::Verify— it is about fifteen lines and the lax-parse and normalise calls are the whole ballgame.Merged to
mainMerge commit
d211fb91, 2026-09-16 (e0f432f9+fd7e230c). Consensus signature validation no longer depends on which OpenSSL the binary happened to link.Evidence, since a port like this looks healthy right up until it splits the chain
Chain revalidation.
verifychain 4 50000on the new build against real mainnet data, range 1,855,101–1,905,101: "No coin database inconsistencies in last 50001 blocks (68863 transactions)", roughly 18,900 of them non-coinbase. The whole range is above checkpoint 1,848,000, sofScriptChecksis true and signatures are genuinely verified. A shorter run proves much less: a 2,000-block window is mostly coinbases with no signatures in it at all.The harness was itself tested. Patching
CPubKey::Verifyto fail after its first 200 calls, so that startup sanity still passes, makesverifychain 4return false withCScriptCheck ... VerifySignature failed. A blunt always-fail will not even boot, becauseInitSanityCheckself-tests sign and verify at startup.Mutation test on the line that matters.
qa/cltv/run-sig-normalize-test.shmines a real high-S spend into a real block. Withsecp256k1_ecdsa_signature_normalize()present it passes; delete that one line and rebuild and the spend is rejected, while every unit test still passes. Low-S is not consensus here (SCRIPT_VERIFY_LOW_Sis standard-flags only), so high-S signatures are valid in existing blocks and a port without that call rejects them.ecdsa_signature_parse_der_lax()is used rather than the strict parser, for the same reason: strictness is BIP66's job viaSCRIPT_VERIFY_DERSIG, not the parser's.A/B against production. A shadow node running the new build has followed mainnet alongside a node running the old one since 2026-09-15, compared every five minutes on tip hash rather than height, since two nodes at equal height on different chains is exactly what a split looks like. Zero divergences to date.
Correction to the original report
key.cppincludes<secp256k1.h>with no guard, so signing has always used libsecp256k1. Only verification fell through to OpenSSL, becausepubkey.cppguards it onUSE_SECP256K1, which configure never defines. The tree was already half migrated, and the missing half was the half that matters for consensus.RecoverCompact,IsFullyValid,DecompressandDeriveare left on OpenSSL on purpose; none is consensus-critical. OpenSSL stays linked.Side effect worth recording
test_dobbscoinwent from 62 failures to 42. All 20 genuine verification failures are gone (14transaction_tests, 6script_tests) — they were asserting that non-canonical DER is accepted, which OpenSSL 1.0.x did and OpenSSL 3 refuses. The remaining 42 are stalescript_tests.jsondata, which is #43 and not crypto.Merging is not shipping, and unlike #34 this one changes validation the moment a node runs it. Closing the port; the release is the gate.