Private wallets
How a Grail private wallet is created, stored, restored and rotated, and why it no longer comes from a wallet signature.
Creating a wallet
A private wallet is a Railgun wallet with a 0zk address. Grail creates it the way Railgun's wallet guide describes, from a BIP-39 mnemonic with 16 bytes of entropy:
Mnemonic.fromEntropy(crypto.getRandomValues(new Uint8Array(16))).phraseThe phrase comes from the browser's random number generator. It has no link to the connected wallet, and nothing about it reaches a server. The 0zk address is derived locally and appears within seconds, before the engine has started.
Storing it
| What | Where | Protection |
|---|---|---|
| Recovery phrase, at rest | The browser's local storage, per connected wallet | AES-256-GCM, with a key from your password through PBKDF2-SHA256 at 600,000 iterations |
| Unlocked secrets | The tab's session storage, so a reload needs no password | Cleared when the tab closes or you lock the wallet |
| Railgun wallet data | The browser's IndexedDB | Encrypted by the SDK with a key derived from the phrase |
| On Grail's servers | Nothing | There are no keys and no mapping between 0x and 0zk addresses |
You must save the phrase and confirm two of its words before your first deposit. The phrase is a standard mnemonic with the SDK's default derivation, so it is not tied to Grail. We have not yet tested importing it into other Railgun wallets.
Restoring it
Entering a phrase on a new browser creates a new encrypted copy under a new password. A restored wallet does not know when it was created, so it scans all of Railgun's history for notes it can decrypt. On mainnet this took 776 seconds with the tree download and 372 seconds with the tree already present. The balance reads Searching until the scan ends. A wallet created on Grail stores its creation block and scans only from there.
Rotating keys
If a phrase or a device may be exposed, the old keys need to stop mattering. Railgun addresses cannot change their keys, so rotation means moving to a new wallet:
- 1The browser creates a new random phrase, and the user saves it.
- 2It rescans, then sweeps every spendable balance to the new
0zkin one private transfer through the broadcaster. Private transfers carry no Railgun shield or unshield fee. - 3The new vault keeps the old phrase, still encrypted, because the old wallet has to submit the Proof of Innocence for the sweep before the funds become spendable in the new one.
- 4The browser keeps proving until they are, including after a reload or at the next unlock.
The sweep is available only when no funds are in standby. A private transfer does not show which wallets are involved. Earlier versions switched wallets immediately, which left moved funds pending forever, because nothing was left to submit the old wallet's proof.
Why not a wallet signature
Our first prototype derived the mnemonic from a wallet signature over a fixed, versioned message. It was convenient: the same wallet recreated the same 0zk on any device, and rotation was a matter of changing a number in the message.
It also meant that one signature was the whole private wallet. Any site that got a user to sign that message, for example through phishing, could derive the same keys and take the funds, and a signature request looks harmless. When we described this design, a Railgun contributor told us plainly that a signature-based approach is not a good one.
They were right, so we removed it before mainnet. Grail now has no signature step at all. The connected wallet plays no part in the private keys, and a phishing page gains nothing from a signature. The trade-off is that users have to back up a phrase, which is the normal cost of holding their own keys. The current design is our own implementation and has not been reviewed by Railgun contributors.
Grail is an independent project built on Railgun's public contracts and SDK. Railgun contributors do not run, review or endorse Grail, and they did not write this documentation. Their own documentation is at docs.railgun.org.