02

V1 Mainnet · One signature, every key

Key derivation

Menoid never asks for a new seed phrase. Your wallet signs one readable message, and every private key is derived from that signature, inside the browser tab. Sign again on another device and the same keys come back — nothing is stored on a server, nothing needs a backup.

The message you sign

Message
Menoid: create my private account keys.
 
Only sign this on https://app.menoid.xyz
Anyone holding this signature can spend your private balance.
 
Network: mainnet
Key index: 0

On testnet.menoid.xyz the origin line and the network say testnet — and every key comes out different.

The derivation

signature = wallet.signMessage(message)
+ the network name
seed — 32 bytes, kept in memory only
one hash per purpose

Spending

sk = H(spend, seed) mod l

↓

spendPk = sk · Base8

↓

nk = Poseidon(2, sk)

Spends notes. sk only ever enters a proof.

Incoming viewing

incomingSecret = H(incoming, seed)

↓

ivk(e) = H(ivk, incomingSecret, e)

↓

ivkPub(e) = ivk(e) · G

Opens the notes people send you.

Outgoing viewing

outgoingSecret = H(outgoing, seed)

↓

ovk(e) = H(ovk, outgoingSecret, e)

Opens your own record of who you paid.

spendPk + your address

userCommitment = Poseidon(1, address, spendPk.x, spendPk.y)

Your private identity — registered on-chain together with ivkPub.

Exactly
seed           = keccak256("menoid/v1m/seed" ‖ network ‖ signature)
sk             = keccak256("menoid/v1m/spend" ‖ seed) mod l   // BabyJubJub scalar
spendPk        = sk · Base8                                   // BabyJubJub point (x, y)
nk             = Poseidon(2, sk)                              // nullifier key
incomingSecret = keccak256("menoid/v1m/incoming" ‖ seed)
outgoingSecret = keccak256("menoid/v1m/outgoing" ‖ seed)
ivk(e)         = keccak256("menoid/v1m/ivk" ‖ incomingSecret ‖ e) mod (n − 1) + 1
ivkPub(e)      = ivk(e) · G                                   // secp256k1 point, 65 bytes
ovk(e)         = keccak256("menoid/v1m/ovk" ‖ outgoingSecret ‖ e)
userCommitment = Poseidon(1, address, spendPk.x, spendPk.y)

‖ is Solidity’s packed encoding (solidityPackedKeccak256). l is BabyJubJub’s subgroup order, n is secp256k1’s order, e is the viewing epoch (a uint32 starting at 0). In the tree above, H(tag, …) is the same keccak256 with the tag menoid/v1m/….

Each field, and why

message

Plain text naming the network, the one site that should ask for it, and a key index.

WhyA phishing site’s request looks wrong at a glance; the network and the index choose which set of keys you get.

signature

Your wallet’s 65-byte signature over the message. The same wallet signs it the same way every time.

WhyIt seeds everything: no new seed phrase, and recovering on another device is just one more signature.

seed

keccak256 of a tag, the network name and the signature.

WhyMixes the network in a second time, so a mainnet signature handed to testnet code still gives unrelated keys.

sk

The spending key: a number below l, BabyJubJub’s subgroup order.

WhyBabyJubJub lives in the BN254 field the proofs use, so a circuit can rebuild spendPk from sk cheaply. sk is only ever a private input of a proof.

spendPk

sk · Base8 — a point (x, y) on BabyJubJub.

WhyThe public half of sk. It goes into your userCommitment; it never appears on-chain by itself.

nk

Poseidon(2, sk): the nullifier key.

WhyNullifiers use nk, not sk — so an auditor can be shown what you spent without getting the power to spend.

incomingSecret

A keccak256 of a tag and the seed.

WhyThe root of every incoming viewing key, one per epoch.

ivk · ivkPub

A secp256k1 key pair for epoch e. ivkPub is registered on-chain.

WhyNotes are encrypted to ivkPub (ECIES); only ivk opens them. “mod (n − 1) + 1” keeps ivk a valid private key.

outgoingSecret · ovk

A tag and the seed, then one 32-byte key per epoch.

WhyEncrypts your own record of each payment, so your history also shows who you paid.

userCommitment

Poseidon(1, address, spendPk.x, spendPk.y).

WhyYour private identity: it binds your public address to your spending key. Senders lock notes to it; proofs rebuild it to show ownership.

key index

A number in the message, 0 to start.

WhyrotateKeys moves you to the next index — a whole new identity, if your signature ever leaked.

viewing epoch

A number mixed into ivk and ovk, 0 to start.

WhyupdateViewingKey moves you to the next epoch: fresh viewing keys, same identity, same balance.

A small example: what changes what

You…sk · nk · userCommitmentivk · ovk
Sign again on another deviceSameSame
Sign on testnet instead of mainnetAll differentAll different
Move to key index 1 (rotateKeys)All newAll new
Move to viewing epoch 1 (updateViewingKey)SameNew, for epoch 1 on

The signature is the master key

Anyone holding it can derive sk and spend your private balance. The app keeps it in memory in this tab only and never sends it anywhere — reload and you sign again. Sign the Menoid message only on app.menoid.xyz (or testnet.menoid.xyz for testnet keys).