deps: @noble v3, proven byte-identical on the signing path - #85
Merged
Conversation
@noble/secp256k1 2.3.0 -> 3.1.0 and @noble/hashes 1.8.0 -> 2.3.0. Both are
breaking majors on the code that decides whether a vote is real, so the
interesting part is not the upgrade — it is the evidence that it changed
nothing.
The tests could not supply that evidence. Every one of them signed and
verified with the same code, so a change that altered the wire format on both
sides would have passed all fifteen while silently invalidating every
signature a real wallet produces. Four known-answer tests now close that:
- Address derivation is anchored to BIP173's own published vector. The key is
secret exponent 1, so its public key is the secp256k1 generator and its
P2WPKH address is the one written into the spec.
- The signed-message digest is rebuilt from the spec inside the test rather
than imported, and compared byte for byte.
- The signature base64 is asserted literally. RFC6979 makes signing
deterministic, so this is stable across versions unless nonce derivation or
encoding moved.
- OpenSSL, via node:crypto, verifies the signature independently. It is the
only check in the repo that is not noble verifying noble.
All four were written and run against v2 first, so they are known to hold
before the upgrade rather than fitted to it afterwards. They pass unchanged on
v3: the golden signature is the same base64, so every signature already stored
in the treasury's audit trail still verifies.
The API moves themselves:
- hmac wiring is now secp.hashes.{sha256,hmacSha256}, and hmacSha256 takes a
single message instead of varargs.
- sign() returns bytes, not a Signature. Recovery comes from
format: 'recovered' (65 bytes, recovery || r || s), and prehash: false is
now load-bearing — the Bitcoin digest is already double-sha256, and letting
the library hash again would sign the wrong value.
- Signature.fromCompact().recoverPublicKey() became the top-level
recoverPublicKey(), so verification rebuilds the recovered encoding from the
Bitcoin header || r || s layout.
- randomPrivateKey -> randomSecretKey; secret keys are bytes, no longer hex.
- @noble/hashes v2 moved its submodules: sha256 to sha2.js, ripemd160 to
legacy.js, and every deep import now needs the .js extension.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
# Conflicts: # package-lock.json # package.json
This was referenced Aug 15, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
@noble/secp256k12.3.0 → 3.1.0,@noble/hashes1.8.0 → 2.3.0.Both are breaking majors, and they land on the code that decides whether a
vote is real. So the interesting part of this PR is not the upgrade — it is
the evidence that it changed nothing.
The tests could not have caught a break
Every existing test signs and verifies with the same code. A change that
altered the wire format on both sides would have passed all fifteen of them
while silently invalidating every signature a real wallet produces — and every
signature already sitting in the audit trail.
Four known-answer tests now close that hole. All four were written and run
against v2 first, so they are known to hold before the upgrade rather than
fitted to it afterwards:
bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4node:crypto, verifies it independently — the only check in the repo that isn't noble verifying nobleThey pass unchanged on v3. The golden signature is the same base64, so every
signature already stored still verifies.
The API moves
secp.hashes.{sha256,hmacSha256}, andhmacSha256takes a single message instead of varargs.
sign()returns bytes, not aSignature. Recovery comes fromformat: 'recovered'(65 bytes,recovery || r || s), andprehash: falseis load-bearing — the Bitcoin digest is already double-sha256, and letting
the library hash again would sign the wrong value.
Signature.fromCompact().recoverPublicKey()became the top-levelrecoverPublicKey(), so verification rebuilds the recovered encoding fromthe Bitcoin
header || r || slayout.randomPrivateKey→randomSecretKey; secret keys are bytes now, not hex.@noble/hashesv2 moved its submodules:sha256→sha2.js,ripemd160→legacy.js, and every deep import needs the.jsextension.Verification
npm run verify— exit 0 (37 passed, 2 skipped)npm run build— compiled successfully