Keys & Identity
Every Orbinum user holds two independent key systems: the account key that signs public transactions, and a set of Baby JubJub keys that own shielded notes. This page covers how the second set is derived and what each key can do.
Two key systems
| Account key | Shielded-pool keys | |
|---|---|---|
| Curve | secp256k1 (EVM wallet), sr25519 (Substrate wallet) or ed25519 (Substrate, Ledger or Solana wallet) | Baby JubJub (twisted Edwards, over the BN254 scalar field) |
| Where it lives | The wallet extension | Derived inside the app from one signature of the account key |
| Public identifier | 0x… address or SS58 account — see Accounts | orbpriv3 privacy address |
| Purpose | Signs shields and public transactions, and pays their gas | Owns notes. Transfers and unshields are unsigned: no account signs them, and the proof built with these keys is their authorisation |
The second curve exists for cost: a Baby JubJub scalar multiplication costs a few thousand R1CS constraints inside a Groth16 circuit, against hundreds of thousands on secp256k1, and every spend proves note ownership with that arithmetic.
Derived from a wallet signature
There is no seed phrase. To unlock the vault, the app asks the connected account for one signature over a fixed payload and derives every shielded-pool key from it:
- EVM accounts sign a Sign-In With Ethereum message (EIP-4361, through
personal_sign): the domainapp.orbinum.network, the account in EIP-55 form, the warning, the chain id, and a fixedNonceandIssued At. - sr25519 accounts sign a text message (the warning,
orbinum-spending-key-v2, the chain id and the account's public key) with the extension's VRF signer. The VRF context includes the web origin, so an sr25519 identity is bound to the official app's origin. - ed25519 accounts in a Substrate extension or Ledger sign the same text with a plain ed25519 signature through
signRaw, which wraps it in<Bytes>…</Bytes>. - Solana wallets sign a Sign-In With Solana message (
solana:signIn): the app's domain, the account, the warning, and a fixedNonceandIssued At. The domain is part of the signed bytes, and Phantom refuses to sign that message for any page but the one it names.
The signature must be deterministic, or the keys would change between sessions and every note would become unspendable. Each account type signs differently: an EVM account signs a Sign-In With Ethereum message, an sr25519 account a VRF signature (plain sr25519 signatures are randomized), and an ed25519 account (Ledger, Substrate extension or Solana wallet) a plain signature, which is deterministic by design. The app checks determinism before deriving (signing twice on EVM and ed25519, verifying the VRF proof on sr25519) and refuses wallets that fail, such as smart-contract and MPC wallets. With both an EVM and a Substrate account connected, the EVM account is used.
So the account is the backup: signing again from the same account on the same chain reproduces every key.
Origin binding. A sr25519 VRF output depends on the requesting origin, so a phishing page that gets one gets a useless value. A plain ed25519 signature does not: a page that gets an ed25519 account to sign the derivation text holds its spending key. For EVM accounts, MetaMask and Talisman compare the Sign-In With Ethereum domain with the requesting page and show a danger alert when they differ; the user can still sign past it, and other EVM wallets show nothing, so the app warns before they sign. For Solana accounts the Sign-In With Solana message closes this in wallets that check its domain against the requesting page — Phantom does; Solflare, MetaMask and Talisman do not yet, and the app warns before they sign. Substrate extensions and Ledger have no such check.
A Solana wallet signs a Sign-In With Solana message and a Substrate extension signs the derivation text inside <Bytes>…</Bytes>, so the same ed25519 key derives two different privacy identities through the two, each with its own vault. Notes shielded under one cannot be spent from the other. Keep each ed25519 key to one kind of wallet.
Whoever holds this signature holds the spending key. Only sign it in the official app.
The derivation tree
wallet signature
└─ HKDF-SHA256(ikm = signature, info = "orbinum-sk-v2:<chainId>:<account>")
(EVM, Sign-In With Ethereum: "orbinum-sk-siwe-v1:<chainId>:<account>")
(Solana, Sign-In With Solana: "orbinum-sk-siws-v1:<chainId>:<account>")
└─ master (32 bytes, never used directly)
├─ spending_key = HKDF(master, "orbinum-spend-v3") mod l (0 maps to 1)
│ └─ owner_pk = (spending_key · Base8).x
├─ ivsk = HKDF(master, "orbinum-ivk-v3")
│ └─ ivk = packed point (ivsk mod l) · Base8
├─ ovk = HKDF(master, "orbinum-ovk-v3")
└─ vault_key = HKDF(master, "orbinum-vault-key-v1")
<account> is the lowercase 0x address (EVM) or the hex public key (Substrate), so an SS58 prefix change does not change the identity. The quoted strings are fixed domain labels; with the chain id and account in them, two chains or accounts never share keys.
| Key | Role |
|---|---|
spending_key | Spends notes. Derives owner_pk, the public half of your receiving identity |
ivsk → ivk | Incoming viewing key. ivsk opens memos sealed to you; ivk is published in your address |
ovk | Outgoing viewing key. Recognises the notes you sent and reads who you paid |
vault_key | Encrypts the local note store in the browser |
Siblings, not a chain
None of the four branches derives another, so:
- A viewing key can be handed out alone. It cannot be walked back to the spending key; in a chain, whoever held a parent would hold everything below it.
- Spending needs two branches. A received note is owned by a one-time stealth key whose spending scalar is
(stealth scalar + spending_key) mod l, and the stealth scalar comes from the memo's shared secret, which onlyivskrecovers. See Stealth addresses. - Naming yourself as sender by address also takes
ivskandspending_key— see Authenticating an address without a signature.
Key rules
Base8 generates a subgroup of prime order l (BABYJUB_SUBORDER, about 2^250.6); the full curve is eight times larger. The circuits and the note encryption assume every key lies in that subgroup.
Spending keys are canonical: spending_key < l. The wallet reduces the HKDF output mod l (0 maps to 1), and stealth spending scalars the same way. The transfer and unshield circuits refuse any key at or above l: k and k + l derive the same owner_pk but different nullifiers, so one note could otherwise be spent twice. A non-canonical key from an imported note fails before proving; it is never reduced silently, which would change its nullifier. See Canonical spending key.
Public keys are checked before use. A recipient's viewing key is checked when an address is decoded and again before a memo or payment slip is sealed to it. Zero, the identity, small-order and mixed-order points are refused: an ECDH against such a key has at most eight results, so anyone could open the memo. owner_pk must be a non-zero prime-subgroup point. In @orbinum/protocol the check is unpackSubgroupViewingKey, built on the [l]·P = O test isInPrimeSubgroup.
The privacy address
The address is what you share to receive. It carries only public values:
orbpriv3:<owner_pk>:<ivk>:<checksum>
| Field | Content |
|---|---|
owner_pk | 32-byte hex, the Baby JubJub x-coordinate of spending_key · Base8 |
ivk | 32-byte hex, the packed incoming viewing public key (little-endian) |
checksum | 8 hex characters: the first 4 bytes of SHA-256 over orbpriv3:<owner_pk>:<ivk> |
A sender commits the note to a stealth key derived from owner_pk and encrypts its memo to ivk, so your published owner_pk never appears in a commitment. The checksum makes a corrupted address fail to decode instead of paying into a note nobody can spend; the prefix is inside the checksummed body, so an address cannot be relabelled. It is unkeyed: it detects corruption, not forgery. An address lets its holder pay you and nothing more.
Viewing-only access
Because the branches are siblings, read access separates from spend authority by construction: a credential without the spending key cannot spend, whatever software holds it.
| Credential | Can | Cannot |
|---|---|---|
ivsk (incoming viewing key) | Open every memo sealed to you: amounts, assets and the sender identity each payer chose. It also finds your own shields and change notes | Spend; compute nullifiers, so it cannot by itself tell which notes were spent; see who you paid; name you as a sender |
ivsk + ovk | All of the above, plus recognise the notes you sent and read their recipients and amounts | Spend; reopen a payment you sent that named you by address, whose blinding needs the spending key |
The outgoing key is the more sensitive: it reveals who you paid — the payment graph, a larger secret than a list of amounts.
The key separation makes viewing-only access possible, but the app has no watch-only mode: it unlocks only by signing with your account, which derives every branch. To prove one note to someone without sharing a key, use a disclosure key.
Related
- Note Discovery — memos, view tags, stealth keys and the outgoing viewing key
- Notes — what
owner_pkand the nullifier are committed into - Transfer circuit — how the circuit derives the owner key from the spending key
- Accounts — the account key side
- Private Vault — unlocking the vault in the app