Optional Compatibility Layer for Legacy Bitcoin Addresses (Non-normative)

Who else wants deterministic legacy address derivation from x-only public keys (npub)? Mostly ChatGPT & me, may have come up with that reference implementation...

🔫 Alwin 🦌🏴

20. Mai 2026May 20, 2026 · 5 Min. Lesezeit5 min read · Aktualisiert am 9. Oktober 2026Updated October 9, 2026

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:

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:

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:

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:

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.

Summary

This proposal adds:

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:

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:

Thus, omission does not avoid the behavior — it only prevents it from being deterministic.

Why inclusion is safe#

This proposal:

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.]

#bitcoin#compatibility layer#github#legacy addresses#legacy bitcoin addresses#nip#nostr#nostr implementation possibilities#on-chain zaps#p2pkh#p2wpkh

Written by

Comments

No comments yet. Start the conversation.

Contents

6 sections

  1. Context
  2. Optional Wallet Interoperability (Private Key Export)
  3. Objection 1: “Why force even-y? Isn’t that arbitrary?”
  4. Objection 2: “Why not use both 02 and 03 and pick one?”
  5. Objection 3: “Why include legacy address derivation at all?”
  6. Response to “Out of scope / should be omitted”