How To Turn a Nostr Nsec Into a Hardened Non-Taproot Address In Prod

Finally with passphrase salt code! The end of the npub/nsec to p2pkh/p2wpkh derivation series?
How To Turn a Nostr Nsec Into a Hardened Non-Taproot Address In Prod

Previously…
Thanks again to ChatGPT, if you’re concerned about CRQC coming true & compromising your UTXOs under the exposed pubkey of even-y derived p2pkh & p2wpkh addresses from your Nostr nsec/npub…

(And obviously, if you’re concerned about CRQC at all, you aren’t touching Taproot/p2tr/bc1p…)

Here’s how you take your nsec & fight CRQC with “perfectly okay” HKDF-SHA256 to get a passphrase-salted keypair:

#
# Completely dependency-free single-file script
#
# Features:
# - Decode Nostr nsec
# - Derive hardened BTC private key from:
#       nsec + passphrase
# - Deterministic HKDF-SHA256 derivation
# - secp256k1 public key derivation
# - WIF generation
# - P2PKH address
# - P2WPKH address
#
# No pip installs required.
#
# Uses only:
#   hashlib
#   hmac
#   secrets
#
# WARNING:
# This is educational/minimal code.
# Do not trust large funds to unaudited crypto code.
#

import hashlib
import hmac


# ============================================================
# Base58
# ============================================================

BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"


def b58encode(b):

    n = int.from_bytes(b, "big")

    out = ""

    while n > 0:
        n, r = divmod(n, 58)
        out = BASE58_ALPHABET[r] + out

    pad = 0

    for c in b:
        if c == 0:
            pad += 1
        else:
            break

    return "1" * pad + out


# ============================================================
# Bech32
# ============================================================

BECH32_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"


def bech32_polymod(values):

    GEN = [
        0x3b6a57b2,
        0x26508e6d,
        0x1ea119fa,
        0x3d4233dd,
        0x2a1462b3
    ]

    chk = 1

    for v in values:

        top = chk >> 25

        chk = ((chk & 0x1ffffff) << 5) ^ v

        for i in range(5):
            if ((top >> i) & 1):
                chk ^= GEN[i]

    return chk


def bech32_hrp_expand(hrp):

    return [ord(x) >> 5 for x in hrp] + [0] + [
        ord(x) & 31 for x in hrp
    ]


def bech32_verify_checksum(hrp, data):

    return bech32_polymod(
        bech32_hrp_expand(hrp) + data
    ) == 1


def bech32_decode(bech):

    bech = bech.lower()

    pos = bech.rfind("1")

    if pos < 1:
        return None, None

    hrp = bech[:pos]

    data = []

    for c in bech[pos + 1:]:

        if c not in BECH32_CHARSET:
            return None, None

        data.append(
            BECH32_CHARSET.find(c)
        )

    if not bech32_verify_checksum(hrp, data):
        return None, None

    return hrp, data[:-6]


def bech32_create_checksum(hrp, data):

    values = bech32_hrp_expand(hrp) + data

    polymod = bech32_polymod(
        values + [0, 0, 0, 0, 0, 0]
    ) ^ 1

    return [
        (polymod >> 5 * (5 - i)) & 31
        for i in range(6)
    ]


def bech32_encode(hrp, data):

    combined = data + bech32_create_checksum(
        hrp,
        data
    )

    return hrp + "1" + "".join(
        [BECH32_CHARSET[d] for d in combined]
    )


def convertbits(data, frombits, tobits, pad=True):

    acc = 0
    bits = 0
    ret = []

    maxv = (1 << tobits) - 1

    for value in data:

        acc = (acc << frombits) | value
        bits += frombits

        while bits >= tobits:

            bits -= tobits

            ret.append(
                (acc >> bits) & maxv
            )

    if pad:

        if bits:
            ret.append(
                (acc << (tobits - bits)) & maxv
            )

    elif bits >= frombits or (
        (acc << (tobits - bits)) & maxv
    ):
        return None

    return ret


# ============================================================
# secp256k1
# ============================================================

P = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141

GX = 55066263022277343669578718895168534326250603453777594175500187360389116729240
GY = 32670510020758816978083085130507043184471273380659243275938904335757337482424


def inverse_mod(a, p):

    return pow(a, p - 2, p)


def point_add(p1, p2):

    if p1 is None:
        return p2

    if p2 is None:
        return p1

    x1, y1 = p1
    x2, y2 = p2

    if x1 == x2 and y1 != y2:
        return None

    if p1 == p2:

        m = (
            (3 * x1 * x1)
            * inverse_mod(2 * y1, P)
        ) % P

    else:

        m = (
            (y2 - y1)
            * inverse_mod(x2 - x1, P)
        ) % P

    x3 = (m * m - x1 - x2) % P
    y3 = (m * (x1 - x3) - y1) % P

    return (x3, y3)


def scalar_mult(k, point):

    result = None

    addend = point

    while k:

        if k & 1:
            result = point_add(result, addend)

        addend = point_add(addend, addend)

        k >>= 1

    return result

# ============================================================
# Pure Python RIPEMD160
# ============================================================

def _rol(x, n):
    return ((x << n) | (x >> (32 - n))) & 0xffffffff


def ripemd160(msg):

    # --------------------------------------------------------
    # Constants
    # --------------------------------------------------------

    r1 = [
         0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13,14,15,
         7, 4,13, 1,10, 6,15, 3,12, 0, 9, 5, 2,14,11, 8,
         3,10,14, 4, 9,15, 8, 1, 2, 7, 0, 6,13,11, 5,12,
         1, 9,11,10, 0, 8,12, 4,13, 3, 7,15,14, 5, 6, 2,
         4, 0, 5, 9, 7,12, 2,10,14, 1, 3, 8,11, 6,15,13
    ]

    r2 = [
         5,14, 7, 0, 9, 2,11, 4,13, 6,15, 8, 1,10, 3,12,
         6,11, 3, 7, 0,13, 5,10,14,15, 8,12, 4, 9, 1, 2,
        15, 5, 1, 3, 7,14, 6, 9,11, 8,12, 2,10, 0, 4,13,
         8, 6, 4, 1, 3,11,15, 0, 5,12, 2,13, 9, 7,10,14,
        12,15,10, 4, 1, 5, 8, 7, 6, 2,13,14, 0, 3, 9,11
    ]

    s1 = [
        11,14,15,12, 5, 8, 7, 9,11,13,14,15, 6, 7, 9, 8,
         7, 6, 8,13,11, 9, 7,15, 7,12,15, 9,11, 7,13,12,
        11,13, 6, 7,14, 9,13,15,14, 8,13, 6, 5,12, 7, 5,
        11,12,14,15,14,15, 9, 8, 9,14, 5, 6, 8, 6, 5,12,
         9,15, 5,11, 6, 8,13,12, 5,12,13,14,11, 8, 5, 6
    ]

    s2 = [
         8, 9, 9,11,13,15,15, 5, 7, 7, 8,11,14,14,12, 6,
         9,13,15, 7,12, 8, 9,11, 7, 7,12, 7, 6,15,13,11,
         9, 7,15,11, 8, 6, 6,14,12,13, 5,14,13,13, 7, 5,
        15, 5, 8,11,14,14, 6,14, 6, 9,12, 9,12, 5,15, 8,
         8, 5,12, 9,12, 5,14, 6, 8,13, 6, 5,15,13,11,11
    ]

    # --------------------------------------------------------
    # Functions
    # --------------------------------------------------------

    def f(j, x, y, z):

        if 0 <= j <= 15:
            return x ^ y ^ z

        if 16 <= j <= 31:
            return (x & y) | (~x & z)

        if 32 <= j <= 47:
            return (x | ~y) ^ z

        if 48 <= j <= 63:
            return (x & z) | (y & ~z)

        return x ^ (y | ~z)

    def K1(j):

        if 0 <= j <= 15:
            return 0x00000000

        if 16 <= j <= 31:
            return 0x5A827999

        if 32 <= j <= 47:
            return 0x6ED9EBA1

        if 48 <= j <= 63:
            return 0x8F1BBCDC

        return 0xA953FD4E

    def K2(j):

        if 0 <= j <= 15:
            return 0x50A28BE6

        if 16 <= j <= 31:
            return 0x5C4DD124

        if 32 <= j <= 47:
            return 0x6D703EF3

        if 48 <= j <= 63:
            return 0x7A6D76E9

        return 0x00000000

    # --------------------------------------------------------
    # Padding
    # --------------------------------------------------------

    ml = len(msg) * 8

    msg += b"\x80"

    while (len(msg) % 64) != 56:
        msg += b"\x00"

    msg += ml.to_bytes(8, "little")

    # --------------------------------------------------------
    # Initial state
    # --------------------------------------------------------

    h0 = 0x67452301
    h1 = 0xEFCDAB89
    h2 = 0x98BADCFE
    h3 = 0x10325476
    h4 = 0xC3D2E1F0

    # --------------------------------------------------------
    # Process blocks
    # --------------------------------------------------------

    for offset in range(0, len(msg), 64):

        block = msg[offset:offset + 64]

        X = [
            int.from_bytes(
                block[i:i+4],
                "little"
            )
            for i in range(0, 64, 4)
        ]

        A1 = h0
        B1 = h1
        C1 = h2
        D1 = h3
        E1 = h4

        A2 = h0
        B2 = h1
        C2 = h2
        D2 = h3
        E2 = h4

        for j in range(80):

            T = (
                _rol(
                    (
                        A1
                        + f(j, B1, C1, D1)
                        + X[r1[j]]
                        + K1(j)
                    ) & 0xffffffff,
                    s1[j]
                )
                + E1
            ) & 0xffffffff

            A1, E1, D1, C1, B1 = (
                E1,
                D1,
                _rol(C1, 10),
                B1,
                T
            )

            T = (
                _rol(
                    (
                        A2
                        + f(79 - j, B2, C2, D2)
                        + X[r2[j]]
                        + K2(j)
                    ) & 0xffffffff,
                    s2[j]
                )
                + E2
            ) & 0xffffffff

            A2, E2, D2, C2, B2 = (
                E2,
                D2,
                _rol(C2, 10),
                B2,
                T
            )

        T = (h1 + C1 + D2) & 0xffffffff

        h1 = (h2 + D1 + E2) & 0xffffffff
        h2 = (h3 + E1 + A2) & 0xffffffff
        h3 = (h4 + A1 + B2) & 0xffffffff
        h4 = (h0 + B1 + C2) & 0xffffffff
        h0 = T

    return (
        h0.to_bytes(4, "little")
        + h1.to_bytes(4, "little")
        + h2.to_bytes(4, "little")
        + h3.to_bytes(4, "little")
        + h4.to_bytes(4, "little")
    )


# ============================================================
# HASH160
# ============================================================

def hash160(data):

    sha = hashlib.sha256(data).digest()

    return ripemd160(sha)


# ============================================================
# Decode nsec
# ============================================================

def nsec_to_privkey(nsec):

    hrp, data = bech32_decode(nsec)

    if hrp != "nsec":
        raise ValueError("Invalid nsec")

    decoded = convertbits(
        data,
        5,
        8,
        False
    )

    if decoded is None:
        raise ValueError("Bad convertbits")

    raw = bytes(decoded)

    if len(raw) != 32:
        raise ValueError("Expected 32-byte key")

    return raw


# ============================================================
# HKDF-SHA256
# ============================================================

def hkdf_extract(salt, ikm):

    return hmac.new(
        salt,
        ikm,
        hashlib.sha256
    ).digest()


def hkdf_expand(prk, info, length=32):

    output = b""

    t = b""

    counter = 1

    while len(output) < length:

        t = hmac.new(
            prk,
            t + info + bytes([counter]),
            hashlib.sha256
        ).digest()

        output += t

        counter += 1

    return output[:length]


def derive_hardened_btc_privkey(
    nsec,
    passphrase
):

    nostr_privkey = nsec_to_privkey(
        nsec
    )

    salt = hashlib.sha256(
        passphrase.encode()
    ).digest()

    prk = hkdf_extract(
        salt,
        nostr_privkey
    )

    return hkdf_expand(
        prk,
        b"nostr-to-bitcoin-v1",
        32
    )


# ============================================================
# Pubkey
# ============================================================

def privkey_to_pubkey(privkey_bytes):

    k = int.from_bytes(
        privkey_bytes,
        "big"
    )

    x, y = scalar_mult(
        k,
        (GX, GY)
    )

    prefix = b"\x02" if y % 2 == 0 else b"\x03"

    return prefix + x.to_bytes(32, "big")


# ============================================================
# WIF
# ============================================================

def privkey_to_wif(privkey):

    payload = (
        b"\x80"
        + privkey
        + b"\x01"
    )

    checksum = hashlib.sha256(
        hashlib.sha256(payload).digest()
    ).digest()[:4]

    return b58encode(
        payload + checksum
    )


# ============================================================
# P2PKH
# ============================================================

def pubkey_to_p2pkh(pubkey):

    h160 = hash160(pubkey)

    payload = b"\x00" + h160

    checksum = hashlib.sha256(
        hashlib.sha256(payload).digest()
    ).digest()[:4]

    return b58encode(
        payload + checksum
    )


# ============================================================
# P2WPKH
# ============================================================

def pubkey_to_p2wpkh(pubkey):

    h160 = hash160(pubkey)

    data = [0] + convertbits(
        h160,
        8,
        5
    )

    return bech32_encode(
        "bc",
        data
    )


# ============================================================
# MAIN
# ============================================================

if __name__ == "__main__":

    my_nsec = "YOUR_NSEC_HERE"

    passphrase = "correct horse battery staple"

    btc_privkey = derive_hardened_btc_privkey(
        my_nsec,
        passphrase
    )

    wif = privkey_to_wif(
        btc_privkey
    )

    pubkey = privkey_to_pubkey(
        btc_privkey
    )

    p2pkh = pubkey_to_p2pkh(
        pubkey
    )

    p2wpkh = pubkey_to_p2wpkh(
        pubkey
    )

    print()
    print("===== Hardened BTC Derivation =====")
    print()

    print("WIF:")
    print(wif)
    print()

    print("Compressed Public Key:")
    print(pubkey.hex())
    print()

    print("P2PKH:")
    print(p2pkh)
    print()

    print("P2WPKH:")
    print(p2wpkh)
    print()

And Argon2id, better against GPU/ASIC cracking (install dependencies: pip install base58 bech32 ecdsa argon2-cffi):

import hashlib
import base58

from bech32 import bech32_encode, bech32_decode, convertbits
from ecdsa import SigningKey, SECP256k1

from argon2.low_level import hash_secret_raw, Type


# ============================================================
# Decode Nostr nsec
# ============================================================

def nsec_to_privkey(nsec_str):
    hrp, data5 = bech32_decode(nsec_str)

    if hrp != "nsec":
        raise ValueError("Invalid nsec")

    return bytes(convertbits(data5, 5, 8, False))


# ============================================================
# Argon2id hardened derivation
# ============================================================

def derive_hardened_btc_privkey(nsec_str, passphrase):

    nostr_privkey = nsec_to_privkey(nsec_str)

    # Domain separation
    domain = b"nostr-to-bitcoin-v1"

    # Salt for Argon2id
    #
    # Deterministic:
    # same nsec + same passphrase => same wallet
    #
    salt = hashlib.sha256(
        domain + passphrase.encode()
    ).digest()

    # Argon2id derivation
    #
    # memory_cost is in KiB
    #
    derived_key = hash_secret_raw(
        secret=nostr_privkey,
        salt=salt,
        time_cost=6,
        memory_cost=262144,   # 256 MiB
        parallelism=1,
        hash_len=32,
        type=Type.ID
    )

    return derived_key


# ============================================================
# Pure Python RIPEMD160
# ============================================================

def _rol(x, n):
    return ((x << n) | (x >> (32 - n))) & 0xffffffff


def ripemd160(msg):

    # --------------------------------------------------------
    # Constants
    # --------------------------------------------------------

    r1 = [
         0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13,14,15,
         7, 4,13, 1,10, 6,15, 3,12, 0, 9, 5, 2,14,11, 8,
         3,10,14, 4, 9,15, 8, 1, 2, 7, 0, 6,13,11, 5,12,
         1, 9,11,10, 0, 8,12, 4,13, 3, 7,15,14, 5, 6, 2,
         4, 0, 5, 9, 7,12, 2,10,14, 1, 3, 8,11, 6,15,13
    ]

    r2 = [
         5,14, 7, 0, 9, 2,11, 4,13, 6,15, 8, 1,10, 3,12,
         6,11, 3, 7, 0,13, 5,10,14,15, 8,12, 4, 9, 1, 2,
        15, 5, 1, 3, 7,14, 6, 9,11, 8,12, 2,10, 0, 4,13,
         8, 6, 4, 1, 3,11,15, 0, 5,12, 2,13, 9, 7,10,14,
        12,15,10, 4, 1, 5, 8, 7, 6, 2,13,14, 0, 3, 9,11
    ]

    s1 = [
        11,14,15,12, 5, 8, 7, 9,11,13,14,15, 6, 7, 9, 8,
         7, 6, 8,13,11, 9, 7,15, 7,12,15, 9,11, 7,13,12,
        11,13, 6, 7,14, 9,13,15,14, 8,13, 6, 5,12, 7, 5,
        11,12,14,15,14,15, 9, 8, 9,14, 5, 6, 8, 6, 5,12,
         9,15, 5,11, 6, 8,13,12, 5,12,13,14,11, 8, 5, 6
    ]

    s2 = [
         8, 9, 9,11,13,15,15, 5, 7, 7, 8,11,14,14,12, 6,
         9,13,15, 7,12, 8, 9,11, 7, 7,12, 7, 6,15,13,11,
         9, 7,15,11, 8, 6, 6,14,12,13, 5,14,13,13, 7, 5,
        15, 5, 8,11,14,14, 6,14, 6, 9,12, 9,12, 5,15, 8,
         8, 5,12, 9,12, 5,14, 6, 8,13, 6, 5,15,13,11,11
    ]

    # --------------------------------------------------------
    # Functions
    # --------------------------------------------------------

    def f(j, x, y, z):

        if 0 <= j <= 15:
            return x ^ y ^ z

        if 16 <= j <= 31:
            return (x & y) | (~x & z)

        if 32 <= j <= 47:
            return (x | ~y) ^ z

        if 48 <= j <= 63:
            return (x & z) | (y & ~z)

        return x ^ (y | ~z)

    def K1(j):

        if 0 <= j <= 15:
            return 0x00000000

        if 16 <= j <= 31:
            return 0x5A827999

        if 32 <= j <= 47:
            return 0x6ED9EBA1

        if 48 <= j <= 63:
            return 0x8F1BBCDC

        return 0xA953FD4E

    def K2(j):

        if 0 <= j <= 15:
            return 0x50A28BE6

        if 16 <= j <= 31:
            return 0x5C4DD124

        if 32 <= j <= 47:
            return 0x6D703EF3

        if 48 <= j <= 63:
            return 0x7A6D76E9

        return 0x00000000

    # --------------------------------------------------------
    # Padding
    # --------------------------------------------------------

    ml = len(msg) * 8

    msg += b"\x80"

    while (len(msg) % 64) != 56:
        msg += b"\x00"

    msg += ml.to_bytes(8, "little")

    # --------------------------------------------------------
    # Initial state
    # --------------------------------------------------------

    h0 = 0x67452301
    h1 = 0xEFCDAB89
    h2 = 0x98BADCFE
    h3 = 0x10325476
    h4 = 0xC3D2E1F0

    # --------------------------------------------------------
    # Process blocks
    # --------------------------------------------------------

    for offset in range(0, len(msg), 64):

        block = msg[offset:offset + 64]

        X = [
            int.from_bytes(
                block[i:i+4],
                "little"
            )
            for i in range(0, 64, 4)
        ]

        A1 = h0
        B1 = h1
        C1 = h2
        D1 = h3
        E1 = h4

        A2 = h0
        B2 = h1
        C2 = h2
        D2 = h3
        E2 = h4

        for j in range(80):

            T = (
                _rol(
                    (
                        A1
                        + f(j, B1, C1, D1)
                        + X[r1[j]]
                        + K1(j)
                    ) & 0xffffffff,
                    s1[j]
                )
                + E1
            ) & 0xffffffff

            A1, E1, D1, C1, B1 = (
                E1,
                D1,
                _rol(C1, 10),
                B1,
                T
            )

            T = (
                _rol(
                    (
                        A2
                        + f(79 - j, B2, C2, D2)
                        + X[r2[j]]
                        + K2(j)
                    ) & 0xffffffff,
                    s2[j]
                )
                + E2
            ) & 0xffffffff

            A2, E2, D2, C2, B2 = (
                E2,
                D2,
                _rol(C2, 10),
                B2,
                T
            )

        T = (h1 + C1 + D2) & 0xffffffff

        h1 = (h2 + D1 + E2) & 0xffffffff
        h2 = (h3 + E1 + A2) & 0xffffffff
        h3 = (h4 + A1 + B2) & 0xffffffff
        h4 = (h0 + B1 + C2) & 0xffffffff
        h0 = T

    return (
        h0.to_bytes(4, "little")
        + h1.to_bytes(4, "little")
        + h2.to_bytes(4, "little")
        + h3.to_bytes(4, "little")
        + h4.to_bytes(4, "little")
    )

# ============================================================
# HASH160
# ============================================================

def hash160(data):

    sha = hashlib.sha256(data).digest()

    return ripemd160(sha)


# ============================================================
# Compressed SEC pubkey
# ============================================================

def privkey_to_compressed_pubkey(privkey_bytes):

    sk = SigningKey.from_string(
        privkey_bytes,
        curve=SECP256k1
    )

    vk = sk.verifying_key

    x = vk.pubkey.point.x()
    y = vk.pubkey.point.y()

    prefix = b"\x02" if y % 2 == 0 else b"\x03"

    return prefix + x.to_bytes(32, "big")


# ============================================================
# WIF
# ============================================================

def privkey_to_wif(privkey_bytes, compressed=True):

    payload = b"\x80" + privkey_bytes

    if compressed:
        payload += b"\x01"

    checksum = hashlib.sha256(
        hashlib.sha256(payload).digest()
    ).digest()[:4]

    return base58.b58encode(
        payload + checksum
    ).decode()


# ============================================================
# P2PKH
# ============================================================

def pubkey_to_p2pkh(pubkey_bytes):

    h160 = hash160(pubkey_bytes)

    payload = b"\x00" + h160

    checksum = hashlib.sha256(
        hashlib.sha256(payload).digest()
    ).digest()[:4]

    return base58.b58encode(
        payload + checksum
    ).decode()


# ============================================================
# Native SegWit P2WPKH
# ============================================================

def pubkey_to_p2wpkh(pubkey_bytes):

    h160 = hash160(pubkey_bytes)

    data = [0] + convertbits(h160, 8, 5)

    return bech32_encode("bc", data)


# ============================================================
# MAIN
# ============================================================

if __name__ == "__main__":

    my_nsec = "YOUR_NSEC_HERE"

    passphrase = "correct horse battery staple"

    # Derive hardened BTC private key
    btc_privkey = derive_hardened_btc_privkey(
        my_nsec,
        passphrase
    )

    # WIF
    wif = privkey_to_wif(btc_privkey)

    # Compressed pubkey
    pubkey = privkey_to_compressed_pubkey(
        btc_privkey
    )

    # Addresses
    p2pkh = pubkey_to_p2pkh(pubkey)

    p2wpkh = pubkey_to_p2wpkh(pubkey)

    # Output
    print()
    print("===== Hardened BTC Derivation =====")
    print()

    print("WIF:")
    print(wif)
    print()

    print("Compressed Public Key:")
    print(pubkey.hex())
    print()

    print("P2PKH Address (Legacy):")
    print(p2pkh)
    print()

    print("P2WPKH Address (Native SegWit):")
    print(p2wpkh)
    print()

Per ChatGPT:

The Argon2id parameters above are intentionally fairly expensive:

time_cost=6
memory_cost=262144   # 256 MiB

But you can tune them:

Device Suggested memory_cost
low-RAM VPS 65536
laptop/desktop 262144
high-end workstation 524288+

The passphrase is NOT merely “extra entropy”; it becomes part of the Argon2id salt namespace.

That means:

  • identical nsec

  • but different passphrase

produces completely unrelated Bitcoin wallets.

And:

  • leaked npub

  • leaked nsec

  • future secp256k1 break

still do not reveal the Bitcoin private key without the passphrase.

Is this the end of the series? Please let it be…



Looking for comments…

Searching Nostr relays. This may take a moment the first time this article is opened.