Skip to content

Encode BIP137 SegWit message signature headers#454

Open
j0ntz wants to merge 1 commit into
masterfrom
jon/bip137-btc-message-signing
Open

Encode BIP137 SegWit message signature headers#454
j0ntz wants to merge 1 commit into
masterfrom
jon/bip137-btc-message-signing

Conversation

@j0ntz

@j0ntz j0ntz commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

CHANGELOG

Does this branch warrant an entry to the CHANGELOG?

  • Yes
  • No

Dependencies

none

Description

Asana task

Edge's BTC message signing (used for CEX/ramp withdrawal ownership proofs, e.g. Bringin / NiceHash Travel Rule) produced a signature whose header byte always encoded a legacy P2PKH type (27-34), regardless of the signing address. BIP137 verifiers reject a legacy-header signature that comes from a SegWit address: nested SegWit (P2SH-P2WPKH) requires header 35-38 and native SegWit (P2WPKH / bech32) requires 39-42. Because Edge BTC wallets default to SegWit, the signatures Edge emitted were rejected by Bringin; a user had to run a script to rewrite the header to BIP137 before it was accepted, or fall back to selfie/ID verification.

signMessageBase64 in keymanager.ts called bitcoinMessage.sign(message, privKey, compressed) with no segwitType, so the header byte only reflected the compressed flag. This threads the address's derivation format through and selects the matching segwitType:

  • bip84 (native SegWit / P2WPKH) -> header 39-42
  • bip49 (nested SegWit / P2SH-P2WPKH) -> header 35-38
  • bip44 / bip32 (legacy P2PKH) -> header 31-34 (unchanged)

The ECDSA signature and message hash are unchanged; only the header byte differs, so legacy P2PKH signing is byte-for-byte identical. The fix is general across all UTXO coins that support SegWit, since path.format reflects the actual address derivation.

Testing

  • Unit tests assert the exact BIP137 header byte for bip44, bip49, and bip84 (extended signMessageTests in the keymanager spec). tsc, eslint, and the full mocha suite pass.
  • Offline round-trip: each format's signature verifies via bitcoinjs-message.verify against its real P2PKH / P2SH-P2WPKH / bech32 address.
  • In-app (iOS sim, edge-funds BTC wallet): drove the real EdgeCurrencyWallet.signMessage path with this build linked, on the wallet's own receive addresses. The nested-SegWit 3... address produced header 35 and the native-SegWit bc1q... address produced header 40, both verifying against their respective addresses (screenshot attached). Before the fix both emitted header 31, which fails BIP137 verification for a SegWit address.

Asana: https://app.asana.com/1/9976422036640/project/1215088146871429/task/1216403654258303


Note

Medium Risk
Changes cryptographic message output for SegWit and all coins using coin-specific prefixes; legacy bip44 Bitcoin ECDSA payload is unchanged but integrators comparing old signatures on altcoins may see differences.

Overview
Fixes UTXO message signing so ramp/CEX ownership proofs verify on BIP137-compliant checkers (e.g. Bringin) when users sign from SegWit receive addresses.

signMessageBase64 now takes the wallet path.format and coin, maps bip84 / bip49 to the correct segwitType for bitcoinjs-message, and hashes with each coin’s messagePrefix instead of Bitcoin-only defaults. UtxoWalletTools.signMessageBase64 passes path.format and the plugin coin id through.

Legacy bip44 signing is unchanged aside from explicit coin prefixes (which can change signatures for non-Bitcoin coins vs the old implicit behavior). Tests add bip44/bip49/bip84 vectors for Bitcoin and Litecoin.

Reviewed by Cursor Bugbot for commit 9594db2. Bugbot is set up for automated code reviews on this repo. Configure here.

@j0ntz

j0ntz commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

📸 Test evidence: in-app BIP137 signatures (iOS sim, edge-funds BTC wallet)

agent proof 1216403654258303 01 bip137 signatures

agent proof 1216403654258303 01 bip137 signatures

Captured by the agent's in-app test run (build-and-test).

@j0ntz j0ntz left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Automated code review (workflow-backed, high effort). All findings are advisory; the core BIP137 header fix is correct and matches the task requirement. Three notes below.

Comment thread src/common/utxobased/keymanager/keymanager.ts
// SegWit (bip84 / P2WPKH) and nested SegWit (bip49 / P2SH-P2WPKH) each have
// their own header range; verifiers such as Bringin reject a SegWit address
// whose message was signed with a legacy (bip44) header byte.
const segwitType =

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Minor (robustness): the format -> segwitType mapping compares raw 'bip84'/'bip49' string literals rather than routing through the codebase's canonical BIP43PurposeTypeEnum / bip43PurposeNumberToTypeEnum. If a new SegWit CurrencyFormat is ever added, this ternary is easy to miss (no compiler linkage) and the new format silently falls through to undefined, producing a legacy header that BIP137 verifiers reject. Consider deriving the segwit kind from the purpose enum so the two stay in lockstep.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Leaving open as an optional cleanup. The coin-prefix fix (the operator's requested scope) does not depend on it, so this change stays minimal; routing the segwitType selection through BIP43PurposeTypeEnum is a good follow-up but out of scope for this task.

Comment thread test/common/utxobased/keymanager/coins/keymanagertest.spec.ts Outdated
signMessageBase64 always emitted a legacy header byte (27-34) regardless
of the signing address type, so BIP137 verifiers (e.g. Bringin) rejected
signatures from SegWit addresses. Thread the address format through and
select the matching segwitType: bip49 -> p2sh(p2wpkh) (header 35-38),
bip84 -> p2wpkh (header 39-42), bip44/bip32 unchanged (31-34).
@j0ntz j0ntz force-pushed the jon/bip137-btc-message-signing branch from 0c9e6a2 to 9594db2 Compare July 9, 2026 20:15
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.

1 participant