LapseCoin

Wallet integration

How a wallet turns a recovery phrase the user already has into a LapseCoin key, so one phrase can be restored in any wallet that follows this page.

Status

This is the recommended convention for wallet developers. The LapseCoin node itself does not derive keys from a phrase yet: it creates a random key and stores it encrypted under a passphrase. Nothing on the chain depends on this page, because a wallet only ever shows the network a public key and a signature. Following it makes a phrase portable between wallets, and wallets that do not follow it simply cannot restore each other's phrases.

Two kinds of twelve words

A LapseCoin address is twelve BIP-39 words, but they are a one-way hash of the public key (see address derivation). They cannot recreate a key and must never be treated as a backup.

A recovery phrase is the standard 12 to 24 word BIP-39 phrase the user already holds in their other wallets. Wallets should label the two clearly, since they look alike.

Derivation

StepWhat happens
1BIP-39: the phrase and optional passphrase become a 64-byte seed (PBKDF2-HMAC-SHA512, 2048 rounds, NFKD-normalized).
2BIP-32: the seed becomes the master key, as for any Bitcoin wallet (HMAC-SHA512 keyed "Bitcoin seed").
3BIP-85, HEX application: derive m/83696968'/128169'/48'/{index}' and take the 48 bytes of entropy. The index is 21000000 + account, with account counting up from 0.
4FALCON-512 key generation with those 48 bytes as the seed (next section). This gives the public key and secret key.
5The address is derived from the public key as described in address derivation.

The path for account 0 is m/83696968'/128169'/48'/21000000'.

Why BIP-85

  • It is a published standard with its own test vectors, so a wallet or device that already implements BIP-85 can supply the seed with no LapseCoin-specific code.
  • Every level is hardened and the output passes through a one-way HMAC. A leaked LapseCoin seed does not reveal the phrase or the keys of the user's other coins.
  • The HEX application can return up to 64 bytes. FALCON-512 key generation takes a 48-byte seed, so no extra expansion step is needed.

Why 48 bytes and an offset of 21000000

BIP-85 does not tie a HEX output to any coin, so two applications asking for the same length and index get the same bytes. 48 bytes is an unusual length, and starting the index at 21000000 (the supply cap in whole LAPSE) keeps LapseCoin away from the low indexes other software tends to use. Any index is cryptographically equivalent, so the choice only has to be fixed and shared, which this page does.

No public derivation

FALCON has no equivalent of BIP-32 public key derivation. There is no extended public key and no watch-only address generation. To restore a phrase, a wallet checks accounts in order (0, 1, 2, ...) and stops at the first account whose address has no history, using the address history endpoint.

Key generation from the seed

This step is where wallets can disagree, so it is pinned. Use the original FALCON-512 seeded key generation, the one in the NIST round 3 submission and in PQClean, where the 48-byte seed is what the reference code would draw from its random source. Do not use the seeded key generation from the draft FN-DSA standard: the same seed gives a different key pair there.

  • The public key is 897 bytes and the secret key is 1281 bytes, the same encoding the node and liboqs use.
  • Only key generation is deterministic. Signing stays randomized, as on the Transactions page.
  • Checked: PQClean's C implementation (falcon-512/clean, with its random source replaced by the seed) and @noble/post-quantum 0.7.1 (falcon512.keygen(seed)) produce byte-identical public keys for all three seeds in the test vectors below. A wallet using another library should reproduce those vectors before shipping.

Test vectors

All rows use the phrase below, which is public. Never put funds on a key derived from it.

abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about

BIP-39 seed with no passphrase: 5eb00bbddcf069084889a8ab9155568165f5c453ccb85e70811aaed6f6da5fc19a5ac40b389cd370d086206dec8aa6c43daea6690f20ad3d8d48b2d2ce9e38e4

BIP-39 seed with passphrase TREZOR: c55257c360c07c72029aebc1b53c05ed0362ada38ead3e3e9efa3708e53495531f09a6987599d18264c1e1c92f2cf141630c7a3c4ab7c81b2f001698e7463b04

Vector 1: no passphrase, account 0

Index
21000000
FALCON seed
fb49a518faeecb48d54921bd28ad8b9cee7b0e58f856f7968cdbe874f968ba07e39ac6960e48974e6913c4d353338e95
Public key SHA-256
e7f162aa47073308083eda3238a6817d53092ba55407c8e57f65df5e829ed515
Address
tree.mention.pride.mix.industry.lottery.can.swap.cram.share.park.wide

Vector 2: no passphrase, account 1

Index
21000001
FALCON seed
ba57d27131c984f025d2d0c5573ddee34dfc9ce0bbfe3e478613cf3269f6e379fb3c816df3252826eb726ff5b02fee0d
Public key SHA-256
027b9ca53959a5e1f7b3b3c1d55b1ea2df39131b865f2a53ba544ec8a586f378
Address
act.table.citizen.increase.olympic.valley.tell.island.scrub.print.sibling.east

Vector 3: passphrase TREZOR, account 0

Index
21000000
FALCON seed
201915b01be79dc6b659521e7e69858fc5a372dfe9e4d83fd1f1de631dff4629303447f83c01acb5fa74ef81b0f48d79
Public key SHA-256
da9cd9addb76ea3613dadfb9110cbbcfd4ecdf4764edd433657d406f7e8b3033
Address
surround.traffic.hidden.report.huge.brand.exhaust.fossil.ribbon.mask.confirm.panic

The BIP-85 step was checked against the official HEX vector in BIP-85 (m/83696968'/128169'/64'/0'), and the BIP-39 step against the standard seeds above.

Reference code

Steps 1 to 3 in Python, standard library only. It returns the seed for step 4.

import hashlib, hmac, unicodedata

ORDER = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
BASE_INDEX = 21_000_000

def bip39_seed(phrase: str, passphrase: str = "") -> bytes:
    nfkd = lambda s: unicodedata.normalize("NFKD", s).encode()
    return hashlib.pbkdf2_hmac("sha512", nfkd(phrase), nfkd("mnemonic" + passphrase), 2048)

def lapsecoin_falcon_seed(phrase: str, passphrase: str = "", account: int = 0) -> bytes:
    i = hmac.new(b"Bitcoin seed", bip39_seed(phrase, passphrase), hashlib.sha512).digest()
    key, chain = int.from_bytes(i[:32], "big"), i[32:]
    for n in (83696968, 128169, 48, BASE_INDEX + account):   # every level hardened
        data = b"\x00" + key.to_bytes(32, "big") + (n | 0x80000000).to_bytes(4, "big")
        i = hmac.new(chain, data, hashlib.sha512).digest()
        key, chain = (int.from_bytes(i[:32], "big") + key) % ORDER, i[32:]
    return hmac.new(b"bip-entropy-from-k", key.to_bytes(32, "big"), hashlib.sha512).digest()[:48]

phrase = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
lapsecoin_falcon_seed(phrase).hex()
# fb49a518faeecb48d54921bd28ad8b9cee7b0e58f856f7968cdbe874f968ba07e39ac6960e48974e6913c4d353338e95

The code skips BIP-32's invalid-child case, which has a probability of about 2-127.

Cautions

  • Anyone who knows the phrase and passphrase owns the LapseCoin key too. For large holdings, suggest a phrase used for nothing else.
  • Pass the user's BIP-39 passphrase through unchanged. A different passphrase gives a different key.
  • Show the phrase only on a trusted screen, and keep the derived secret key in memory only while signing.
  • Do not change the path, the 48-byte length or the 21000000 offset. A wallet that does cannot restore other wallets' keys.