Skip to main content

Keys & Identity

Every Orbinum user operates two independent key pairs. Understanding why is most of understanding the privacy model: the key that signs your transactions is not the key that owns your notes.


Two key systems

EVM keypairBaby JubJub keypair
Curvesecp256k1Baby JubJub (twisted Edwards)
Where it livesEthereum wallet (MetaMask, etc.)Derived inside the wallet
Public identifierEVM address (0xf24ff3a9…)BJJ point x-coordinate (0x1d4a09a1…)
PurposeSigns transactions, pays gasOwns shielded notes, used in ZK circuits

The reason for the second curve is cost. A scalar multiplication on Baby JubJub costs ~3,000–4,000 R1CS constraints inside a Groth16 circuit; the same operation on secp256k1 costs hundreds of thousands. Notes use BJJ keys because the circuit has to do that arithmetic on every spend.


The derivation tree

You never manage the second keypair. One signature from your EVM wallet produces a master secret, and every shielded-pool key branches off it:

EVM wallet
└─ EIP-712 typed data (domain "Orbinum Shielded Pool", version 2,
│ verifyingContract = shielded pool precompile)
│ └─ fallback for signers without EIP-712:
│ signMessage("orbinum-spending-key-v2\n<chainId>\n<address>")
└─ HKDF-SHA256(ikm=signature, info="orbinum-sk-v2:<chainId>:<address>")
└─ master (32 bytes, never used directly)
├─ spending_key = HKDF(master, "orbinum-spend-v3") → owner_pk = spending_key · G
├─ ivsk = HKDF(master, "orbinum-ivk-v3") → ivk (packed BJJ point)
├─ ovk = HKDF(master, "orbinum-ovk-v3")
└─ vault_key = HKDF(master, "orbinum-vault-key-v1")

The keys never leave the wallet, and the derivation is deterministic — the same wallet on the same chain always reproduces it. That is what makes vault recovery possible without a separate seed phrase.

The signed message still says v2

orbinum-sk-v2 is deliberate, not a leftover. The identity scheme is v3, but only the branches below the master changed — the same signature still produces the same master. Bumping the digest would have forced every user to re-sign for an identical result.


What each branch does

KeyCapability
spending_keySpends notes. Derives owner_pk, your public receiving identity.
ivskivkIncoming viewing key. Decrypts notes sent to you.
ovkOutgoing viewing key. Recovers what you sent.
vault_keyEncrypts the local note database in your browser.

Siblings, not a chain

The branches are siblings: no one of them derives another. This is the design decision the rest of the system leans on.

Because a viewing key cannot be walked back to the spending key, it can be handed out on its own — which is exactly what a watch-only wallet is. A derivation chain could not express that; anyone holding the parent would hold everything below it.

Two consequences follow:

  • ovk is the most sensitive of the three. The incoming viewing key reveals what arrived. The outgoing one reveals who you paid — the payment graph, not just a list of amounts.
  • Spending needs two branches. The scalar that spends a received note is derived from the shared secret, which comes from the viewing key. A spending key alone decrypts nothing and spends nothing. Back up the root, not one key.

Your published key is not what appears in your notes

owner_pk above is what you publish as your receiving identity. It is not what appears in the commitment of a note somebody sends you.

Each received note carries a distinct stealth owner key, derived from the sender's ECDH shared secret. Two notes sent to you by the same person show two unrelated owner keys on-chain, and neither can be linked to your published one.

This is why a disclosure key proves one note's contents rather than "these notes belong to the same person" — see Note Discovery → Stealth addresses.