Dapps have been asking for message signing to prove key control — identity
verification, SIWX-style login, and publishing a signed statement on chain. This
adds the construction and the verification; the protocol wiring follows
separately.
The known workaround is a dummy transaction: one input with a null outpoint, its
prevout script set to the P2PKH of the key being proven, one OP_RETURN output
carrying a server nonce, signed but never broadcast. It covers login, but not the
rest. A signature made that way is bound to a transaction, so verifying it means
rebuilding that exact transaction and knowing how it was serialised — it cannot
be published in an OP_RETURN and checked later by a third party holding only the
message, the signature and an address. It also asks a wallet to sign a real
transaction preimage, which is one bug away from signing a genuine spend.
So this produces the portable form: the standard "Bitcoin Signed Message"
construction. BCH wallets kept Bitcoin's magic string verbatim, so a signature
made here verifies in Electron Cash, Electrum and `bitcoin-cli verifymessage`.
The magic prefix also guarantees the digest can never coincide with a transaction
sighash, which makes signing a message categorically safer to approve than
signing a dummy transaction.
Address-level API, because that is what verification actually looks like
elsewhere: Electron Cash exposes only `verify_message(address, sig, message)`,
and a third party pulling a proof off an explorer has an address, not a public
key. verifyMessageSignatureForAddress accepts CashAddr with or without a prefix
and legacy base58, and rejects P2SH — no message signature can prove control of a
script hash.
recoverMessageSigner returns { publicKey, compressed } rather than a bare key.
The header byte declares which serialisation was used, and a key's compressed and
uncompressed forms hash to DIFFERENT addresses. Dropping that bit is how a
signature proving control of one address gets accepted as proof of another;
message-signing.test.ts pins the case in both directions.
signBitcoinMessage takes the private key as an argument and never retains it. It
exists so a wallet calls one function instead of reassembling the magic string,
both compactSize prefixes and the header byte — the parts third-party verifiers
check, and the parts covered by the tests here.
Testing: the byte layout is not asserted against our own reimplementation of the
spec, because that catches a coding mistake but not a misreading of it.
message-signing.vectors.json holds 32 vectors generated by a real Electron Cash
4.4.5 install (contrib/generate-message-signing-vectors.py) — two keys, both
compression forms, eight messages including empty, multi-byte UTF-8, multi-line
and the 252/253-byte compactSize boundary. Every preimage hash must match byte
for byte, and every signature must verify. Signature bytes are NOT portable
across implementations — Electron Cash and libauth derive the ECDSA nonce
differently — so the reproducible quantity is the hash.
message-signing.compat.test.ts drives `electron-cash verifymessage` directly,
closing the loop that vectors cannot: that our OUTPUT is accepted. Not part of
`npm test` (each assertion spawns a full Electron Cash process); run
`npm run test:compat -w @wizardconnect/core`. Skips when Electron Cash is
absent, so CI is unaffected.
77 tests, 426 in core.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Splits ProtocolMessages exceeding NIP-44's 65,535-byte plaintext ceiling
across multiple gift-wrapped events. Symmetric (both directions),
fire-and-forget, backward-compatible via a new transport-level
\`extensions\` field on \`dapp_ready\` and \`wallet_ready\`. Resolves the
\"Failed to swap: invalid plaintext size\" error on aggregated swap
requests and enables signed-tx responses up to the 1 MB BCH consensus
limit (~2 MB hex).
The package nostr-dev-kit was using a dependency (tseep) which
downstream would flag as not CSP-safe.
Additionally nostr-tools footprint is much smaller.
Canonical encoding for BigInt (<bigint: Xn>) and Uint8Array (hex or
<Uint8Array: 0x...>) used in the relay protocol. Provides both
serialization (sourceOutputToRelay, transactionToHex) and deserialization
(parseExtendedJson, toUint8Array, toBigInt) so dapps and wallets
don't have to implement this independently.