Skip to main content

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 keyShielded-pool keys
Curvesecp256k1 (EVM wallet), sr25519 (Substrate wallet) or ed25519 (Substrate, Ledger or Solana wallet)Baby JubJub (twisted Edwards, over the BN254 scalar field)
Where it livesThe wallet extensionDerived inside the app from one signature of the account key
Public identifier0x… address or SS58 account — see Accountsorbpriv3 privacy address
PurposeSigns shields and public transactions, and pays their gasOwns 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 domain app.orbinum.network, the account in EIP-55 form, the warning, the chain id, and a fixed Nonce and Issued 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 fixed Nonce and Issued 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.

One wallet per ed25519 key

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.

Signing grants full control

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.

KeyRole
spending_keySpends notes. Derives owner_pk, the public half of your receiving identity
ivsk → ivkIncoming viewing key. ivsk opens memos sealed to you; ivk is published in your address
ovkOutgoing viewing key. Recognises the notes you sent and reads who you paid
vault_keyEncrypts 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 only ivsk recovers. See Stealth addresses.
  • Naming yourself as sender by address also takes ivsk and spending_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>
FieldContent
owner_pk32-byte hex, the Baby JubJub x-coordinate of spending_key · Base8
ivk32-byte hex, the packed incoming viewing public key (little-endian)
checksum8 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.

CredentialCanCannot
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 notesSpend; compute nullifiers, so it cannot by itself tell which notes were spent; see who you paid; name you as a sender
ivsk + ovkAll of the above, plus recognise the notes you sent and read their recipients and amountsSpend; 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.

Read-only access is a protocol property

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.


  • Note Discovery — memos, view tags, stealth keys and the outgoing viewing key
  • Notes — what owner_pk and 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