voidcashdocs

Keys and addresses

The vault root, the spend and viewing keys derived from it, the void address and recovery key formats, and how the root is sealed in the browser.

A Voidcash vault is one random 32-byte root. Every key comes from it, and nothing comes from the user's wallet: a phished wallet signature gives no access to the vault.

The root

The app draws the root's 32 bytes from crypto.getRandomValues in the browser. The root is the only secret: whoever holds it can rebuild every key and spend every note.

Derived keys

derive(info, len) = HKDF-SHA256(ikm = root, salt = none, info, len bytes)

spendKey   = big-endian integer of derive("voidcash/v1/spend", 64)  mod r
viewSecret = derive("voidcash/v1/view", 32)                          an X25519 secret key
viewPublic = X25519 public key of viewSecret                         32 bytes
notePubKey = Poseidon(spendKey)                                      one field element

r = 0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593f0000001   (BN254 scalar field)
KeyWhat it doesWho needs it
spendKeySpends notes. The circuit proves knowledge of it, and every nullifier depends on itOnly the vault
notePubKeyThe owner field of every note sent to this vaultAnyone sending to the vault, through the address
viewSecretOpens the ciphertexts of notes sent to this vault, change notes included. It cannot spendOnly the vault
viewPublicSenders encrypt new notes to itAnyone sending to the vault, through the address

The spend key is 64 bytes reduced mod r, so it is uniform in the field. Poseidon is circomlib's, over BN254, the same the circuit and the program's sol_poseidon syscall use.

A nullifier is Poseidon(commitment, index, Poseidon(spendKey, commitment, index)) (see Notes), so the viewing secret alone cannot tell which notes are spent.

The private address

A private address lets anyone send notes to the vault. It carries the note public key and the viewing public key:

payload  = 0x01 (version) ‖ notePubKey (32 bytes, big-endian) ‖ viewPublic (32 bytes)
checksum = SHA-256(SHA-256(payload))[0..4]
address  = "void" ‖ base58(payload ‖ checksum)                    69 bytes before encoding

Base58 uses the Bitcoin alphabet, the one Solana addresses use. The app refuses an address without the void prefix, of the wrong length, with a wrong checksum ("check for a typo"), with another version, or whose note public key is not below r.

An address is public by design. Sending to it reveals nothing on-chain: the recipient's note public key is hidden in a commitment, and the ciphertext carries a fresh ephemeral key.

The recovery key

recovery key = base58(0x81 (version) ‖ root (32 bytes) ‖ SHA-256(SHA-256(0x81 ‖ root))[0..4])

It has no prefix. The same checksum rules apply, and a different version byte is refused. The app shows it once, when the vault is created, and restores the vault from it on any device with a new passphrase.

The recovery key is the root itself. Anyone who has it can spend every note in the vault, and without it a vault lost from its browser cannot be restored. Nobody else holds a copy.

The vault at rest

The app seals the root with the user's passphrase through WebCrypto:

salt = 16 random bytes
iv   = 12 random bytes
key  = PBKDF2(SHA-256, passphrase, salt, 600,000 iterations) -> AES-256-GCM key, not extractable
data = AES-256-GCM(key, iv).encrypt(root)                       32 bytes + 16-byte tag

stored = { "v": 1, "salt": base64, "iv": base64, "data": base64 }

A wrong passphrase fails the GCM tag check ("wrong passphrase"). The root is held in memory only while the vault is unlocked.

localStorage keyContentsEncrypted
voidcash.vault.v1The sealed root, as aboveYes, with the passphrase
voidcash.history.<tag>The vault's activity list, kept in this browser only (the chain cannot give it back)No

The history's tag is the first 16 hex digits of Poseidon(notePubKey, 7), so the key name does not show the vault's address. Removing the vault from the browser deletes every key that starts with voidcash..

Notes are not stored: the app finds them again on every unlock by scanning the pool's leaves with the viewing secret.

On this page