Optional Compatibility Layer for Legacy Bitcoin Addresses (Non-normative)
- Optional Compatibility Layer for Legacy Bitcoin Addresses (Non-normative)
- Proposed Compatibility Rule (Deterministic)
- Why this design choice (anticipated objections)
- Full Reference Implementation (Python)
- Relationship to Taproot (important scope clarification)
- Summary
Originally posted at https://github.com/nostr-protocol/nips/pull/2332#issuecomment-4496373357
Optional Compatibility Layer for Legacy Bitcoin Addresses (Non-normative)
Thanks for the work on this PR — the Taproot mapping is clean and well-scoped.
While reviewing the use of x-only secp256k1 keys (BIP340-style), I ran into a practical interoperability edge case that may be worth documenting as an optional, non-normative compatibility note.
Context
Nostr public keys (npub) are 32-byte x-only secp256k1 values. However, legacy Bitcoin address formats:
-
P2PKH (Base58Check)
-
P2WPKH (SegWit v0)
require a compressed secp256k1 public key, which includes a parity byte:
0x02 = even y
0x03 = odd y
Since x-only keys omit parity, a deterministic rule is required if legacy address display is desired from only an npub.
Proposed Compatibility Rule (Deterministic)
If implementers wish to derive legacy Bitcoin addresses from an x-only Nostr pubkey:
The x-only pubkey SHALL be interpreted as a compressed secp256k1 public key by prefixing it with 0x02 (even-y assumption).
This yields a deterministic mapping:
compressed_pubkey = 0x02 || xonly_pubkey
Optional Wallet Interoperability (Private Key Export)
Nostr private keys are not defined in Bitcoin wallet formats. However, for interoperability with existing Bitcoin software:
Nostr clients MAY optionally expose Bitcoin compatibility by deriving WIF from nsec for import into existing Bitcoin wallets.
This is strictly an implementation convenience and is not part of the Nostr protocol or key format specification.
Why this design choice (anticipated objections)
Objection 1: “Why force even-y? Isn’t that arbitrary?”
Yes — but parity is not recoverable from x-only keys, so any legacy mapping must choose a convention.
We choose 0x02 because:
-
it is deterministic and globally consistent
-
it avoids branching logic or ambiguity
-
it aligns with a simple “default compressed form” assumption
-
it prevents wallet fragmentation across implementations
Importantly:
This does NOT claim to recover the original secp256k1 point — it only defines a deterministic display/compatibility encoding.
Objection 2: “Why not use both 02 and 03 and pick one?”
Using both would produce:
-
two valid P2PKH addresses
-
two valid P2WPKH addresses
-
inconsistent UX across implementations
-
inability to define a canonical output
This would defeat determinism, which is required for interoperable wallet behavior.
Objection 3: “Why include legacy address derivation at all?”
This is strictly optional and not part of Taproot behavior.
It is included only because:
-
some existing wallets / tooling still rely on P2PKH or P2WPKH
-
users may expect “Bitcoin address equivalents” of their Nostr identity
-
migration tooling benefits from deterministic mapping
If not needed, implementations MAY ignore this entirely and use only Taproot (P2TR).
Full Reference Implementation (Python)
import hashlib
import base58
from Crypto.Hash import RIPEMD160
from bech32 import bech32_encode, convertbits
# -----------------------------
# HASH160 (Bitcoin standard)
# -----------------------------
def hash160(data: bytes) -> bytes:
sha = hashlib.sha256(data).digest()
ripemd = RIPEMD160.new()
ripemd.update(sha)
return ripemd.digest()
# -----------------------------
# x-only -> compressed pubkey
# -----------------------------
def xonly_to_compressed_pubkey(xonly_pubkey: bytes) -> bytes:
"""
Deterministic compatibility rule:
Prefix x-only secp256k1 pubkey with 0x02 (even-y assumption)
to produce a valid compressed ECDSA public key.
This is a convention ONLY and not a reconstruction of the
original elliptic curve point.
"""
if len(xonly_pubkey) != 32:
raise ValueError("Invalid x-only pubkey length")
return b"\x02" + xonly_pubkey
# -----------------------------
# P2PKH (legacy)
# -----------------------------
def pubkey_to_p2pkh(pubkey: bytes) -> str:
h160 = hash160(pubkey)
payload = b"\x00" + h160 # mainnet version byte
checksum = hashlib.sha256(
hashlib.sha256(payload).digest()
).digest()[:4]
return base58.b58encode(payload + checksum).decode()
# -----------------------------
# P2WPKH (SegWit v0)
# -----------------------------
def pubkey_to_p2wpkh(pubkey: bytes) -> str:
h160 = hash160(pubkey)
data = [0] + convertbits(h160, 8, 5)
return bech32_encode("bc", data)
# -----------------------------
# Compatibility API
# -----------------------------
def nostr_xonly_to_legacy_bitcoin_addresses(xonly_pubkey: bytes) -> dict:
"""
Optional compatibility layer.
Not part of Taproot or core Nostr semantics.
Provides deterministic legacy Bitcoin address derivation
from x-only secp256k1 pubkeys.
"""
compressed_pubkey = xonly_to_compressed_pubkey(xonly_pubkey)
return {
"compressed_pubkey_hex": compressed_pubkey.hex(),
"p2pkh": pubkey_to_p2pkh(compressed_pubkey),
"p2wpkh": pubkey_to_p2wpkh(compressed_pubkey),
}
Relationship to Taproot (important scope clarification)
This does not modify Taproot behavior.
-
Taproot (P2TR) remains canonical for x-only keys
-
This compatibility layer is purely for legacy Bitcoin interoperability
-
It does not affect key generation, signing, or address validity
Summary
This proposal adds:
-
deterministic legacy address derivation from x-only keys
-
no changes to core Nostr or Taproot semantics
-
a single, explicit convention (0x02) to avoid ambiguity
-
optionality for implementers
Response to “Out of scope / should be omitted”
A likely objection is that legacy Bitcoin address derivation should not be included in a Taproot-focused or Nostr-focused specification.
However, this note does not introduce new functionality or expand protocol semantics. It only formalizes an existing ambiguity in implementation space.
Key point
Given an x-only secp256k1 public key, any system that attempts to derive a legacy Bitcoin address must choose one of:
-
0x02 || x(even-y assumption) -
0x03 || x(odd-y assumption)
This choice already exists implicitly in any implementation that supports such conversion. Without standardization, different clients will produce different addresses for the same npub.
Why omission does not remove the problem
If this note is omitted:
-
implementations will still perform compression
-
they will simply do so inconsistently
-
resulting in non-interoperable legacy address outputs
Thus, omission does not avoid the behavior — it only prevents it from being deterministic.
Why inclusion is safe
This proposal:
-
does NOT modify Taproot behavior
-
does NOT modify Nostr key semantics
-
does NOT introduce new cryptographic assumptions
-
only fixes a deterministic rule for an unavoidable encoding step
Design principle
This follows the same principle used in other standards:
When a lossy representation is unavoidable, define a canonical default rather than leaving it implementation-defined.
[With help or help? from ChatGPT, including a bunch of code iterations & runs on https://playcode.io/python-playground - since I can barely code & read console output… I beg forgiveness/steelman/for a pointer anywhere else I should post this. Thanks in advance.]
Write a comment