NOXYDocs
GitHub
Docs/Developers/Wallets and keystores

Wallets and keystores

Generate a NOXY wallet from a BIP-39 mnemonic, derive an ML-DSA-44 account key, and store it in an encrypted keystore.

Updated 2026-08-03Status ReferenceSource content/wiki/developers/wallet.mdx

Wallets and keystores

A wallet derives an ML-DSA-44 account key from a BIP-39 mnemonic and stores the expanded secret key in an AES-256-GCM keystore. Two implementations of this derivation are public today: the browser wallet and the @noxy/core TypeScript package. A mnemonic created in one restores the same account in the other.

Creating and restoring

The wallet generates a BIP-39 mnemonic (24 words by default), derives the ML-DSA-44 account key at the default path m/44'/8800'/0'/0/0, and encrypts the expanded secret key under a passphrase. An empty passphrase is rejected.

Restoring from a mnemonic re-runs the same derivation, so it always reproduces the same account key. The restored keystore gets a fresh id, salt, and nonce; the bytes differ from the original file, but the key inside is identical.

Derivation

The path has six segments and is fixed in shape:

m/44'/8800'/account'/change/index

44' and 8800' are required (8800 is the NOXY coin type). account is hardened; change and index are plain u32. Any other shape is rejected.

Seed to account key:

bip39_seed   = BIP39(mnemonic, bip39_passphrase)          # 64 bytes
account_seed = HKDF-SHA256(
                 salt = "NOXY-L0/wallet-seed/v1",
                 ikm  = bip39_seed,
                 info = derivation_path,
                 len  = 32)
keypair      = ML-DSA-44.KeyGen_internal(xi = account_seed)  # FIPS 204

ML-DSA-44 sizes: public key 1312 bytes, signature 2420 bytes. The keystore stores the canonical expanded secret key, 2560 bytes.

Same mnemonic and same path always produce the same account key. Changing the path, including change or index, produces a different key and therefore a different account.

account_id

The account ID is derived from the public key, not stored:

account_id = HASH("account-id/v1",
                  BLAKE3(public_key) || account_type_u16_le || salt)

HASH(domain, msg) is BLAKE3("NOXY-L0/v0.1/" || domain || 0x00 || msg). Wallets use account_type = 1 (User) and an empty salt.

The account ID is stable across key rotation: rotating the signing key does not change it. See Accounts for the full model.

Address and verifier

The 32-byte account ID renders three ways. Hex is the canonical wire form; bech32m and the verifier are for display.

hex      f63052cfff6e7141bf90b7f9170a589f0674771a2d44173549fe60ba6aebf251
bech32m  tnxy17cc99nlldec5r0usklu3wzjcnur8gac694zpwd2flest56ht7fg…
verifier general · leisure · vote · 97

bech32m uses HRP noxy on mainnet and tnxy on a testnet, with a BIP-350 checksum that rejects typos and mixed case. An address encoded under one HRP fails to decode under another.

The verifier is three BIP-39 words plus a two-digit number, derived from the account ID:

digest = BLAKE3("NOXY-L0/v0.1/account-verifier/v1\0" || account_id)
word_i = u16_le(digest[2i .. 2i+2]) & 0x07ff   for i in 0,1,2
number = u16_le(digest[6..8]) % 100

Read the four tokens aloud to confirm a recipient instead of comparing 64 hex characters. The verifier is display only: transactions bind the raw account ID.

Account names

An account can also be reached by a registered name such as alice@veylith. Names live in an on-chain registry with leases: a name is registered for a term and can be renewed, retargeted, transferred, or released. The wallet and the @noxy/core SDK resolve names directly from state; there is no public HTTP resolve endpoint yet.

Keystore format

{
  "version": 1,
  "id": "<uuid v4>",
  "algorithm": "ML-DSA-44",
  "public_key_hex": "…",
  "derivation_path": "m/44'/8800'/0'/0/0",
  "crypto": {
    "cipher": "aes-256-gcm",
    "ciphertext": "<hex>",
    "nonce": "<hex, 12 bytes>",
    "kdf": "argon2id",
    "kdfparams": {
      "salt": "<hex, 16 bytes>",
      "memory_cost_kib": 19456,
      "time_cost": 3,
      "parallelism": 1,
      "output_len": 32
    }
  }
}

The encryption key is Argon2id (v1.3) over the passphrase with the parameters above: 19 MiB memory, 3 passes, 1 lane, 32-byte output. AES-256-GCM binds the keystore metadata as associated data:

NOXY-L0/keystore/v1|id={id}|algorithm=ML-DSA-44|public_key={public_key_hex}|path={derivation_path}

Tampering with id, public_key_hex, or derivation_path breaks decryption. The plaintext is the 2560-byte expanded ML-DSA-44 secret key.

Decryption fails on a wrong passphrase, any altered ciphertext or AAD field, or output_len != 32.

Not included

This is account-key material, not a full custody product. There is no guardian recovery, hardware-wallet support, or WalletConnect. Creating an account on-chain still requires submitting a signed transaction; the wallet only produces the key.