z0s / docs
How Zero Surface works
The whole system, from the elliptic-curve assumption it hedges against to the bytes the vault program checks on-chain. Every parameter on this page is the one running in production.
Overview
A wallet is safe for as long as nobody can turn its public key back into its private key. On Bitcoin, Ethereum, and Solana that inversion is the elliptic-curve discrete logarithm problem, and no efficient classical algorithm for it is known. Zero Surface (z0s) is built around one question: what if that assumption weakens?
It answers with three working tools and one rule:
- Measure. The exposure checker reads public chain data and tells you whether an address’s public key is already on the record.
- Watch. The watch agent reads new cryptography research daily and labels each result proven, stated, or speculation.
- Shelter. The vault holds SOL behind a checksummed Winternitz one-time signature, a scheme whose security rests on a hash function rather than on a curve.
- The rule. Claims follow evidence. The vault is resistant to an elliptic-curve break as long as the hash function holds, and it is unaudited, which is why each vault is capped at 0.1 SOL.
The threat model
Bitcoin and Ethereum sign with ECDSA over secp256k1. A private key is a scalar , the public key is a curve point, and the curve is fixed:
Solana signs with Ed25519 on a twisted Edwards curve. Its secret scalar gives the public point , and on Solana that point is the address itself:
Recovering from is the elliptic-curve discrete logarithm problem (ECDLP). The best generic classical attack, Pollard’s rho, needs on the order of the square root of the group order:
Shor’s algorithm solves the ECDLP in polynomial time on a large fault-tolerant quantum computer. Published estimates for a 256-bit curve are on the order of a few thousand logical qubits; Roetteler et al. give an upper bound of
No such machine exists, and no classical break of ECDSA or Ed25519 has been published. The debate is about lead time. Hash functions degrade far more gently: against a -bit preimage, Grover’s algorithm gives only a quadratic speedup,
That gap is the whole argument for hash-based keys: a break that would be total for curves leaves a well-sized hash with a large margin.
Exposure, chain by chain
A curve key can only be attacked once its public half is known. Where a chain hides the public key behind a hash, the key stays hidden until the first signature reveals it, and from then on it is on-chain permanently:
| chain / type | key hidden while | key visible once |
|---|---|---|
| BTC P2PKH, P2WPKH | the address has never spent | any output is spent (the signature carries ) |
| BTC P2SH, P2WSH | never spent (only a script hash is public) | spent: the script and its keys are revealed |
| BTC P2TR (Taproot) | never | always: the output key is in the address |
| BTC P2PK | never | always: coins are paid straight to |
| ETH account | nonce is 0 and no delegation signed | it sends a transaction or signs an EIP-7702 delegation |
| SOL wallet | never | always: the address is the public key |
| contracts, programs, PDAs | no private key exists, so there is nothing to expose | |
The checker applies exactly these rules to public data (mempool.space and Blockstream for Bitcoin, public RPC for Ethereum and Solana), up to 50 addresses at a time. It never asks for a wallet connection, a seed, or a signature. A verdict is a statement about today’s chain, not a prediction, and “hidden” is not a promise of safety.
Winternitz one-time signatures
The vault signs with the Winternitz one-time signature scheme (WOTS), the building block of the hash-based schemes standardized in RFC 8391. Its only primitive is a hash function. z0s uses Keccak-256 truncated to bits:
| parameter | value | meaning |
|---|---|---|
| 224 bits (28 bytes) | size of every chain value | |
| 256 | Winternitz parameter: one digit per message byte | |
| 32 | message digits (one Keccak-256 digest) | |
| 2 | checksum digits | |
| 34 | chains per key, so a signature is 34 × 28 = 952 bytes |
The checksum length follows the standard formula:
Keys
Every vault key is derived from one 32-byte master seed and the vault’s index , so a seed alone rebuilds every vault. The seed is random and independent of the wallet: a vault derived from the Ed25519 key it shelters against would shelter nothing.
The public key is compressed to a single 32-byte root:
Signing
A message becomes 34 base-256 digits: the 32 bytes of its digest, then a two-digit checksum that rises whenever a message digit falls.
Each chain is walked part of the way, by its digit:
s_i ──H──▶ ··· ──H──▶ σ_i ──H──▶ ··· ──H──▶ p_i
│ │ │
└──── 256 − d_i ─────┘──────── d_i ───────┘
signer hashes verifier hashesVerifying
The verifier finishes every chain and checks that the result hashes to the root. It never needs the secrets:
Verification costs hash calls. For a random digest that is about Keccak evaluations, which the program does in roughly 622,000 compute units.
Why the checksum
A signature reveals intermediate chain values. From anyone can hash forward, never backward:
known: σ_i = H^(256−d_i)(s_i) can go: ──▶ forward only (hash again) cannot: ◀── backward (needs a preimage of H) so a forger can only sign digits d'_i ≤ d_i
So a forger holding a signature on can produce a valid signature on any whose digits are all no larger: for every .
Without a checksum
The widely shared Solana implementation signs the 32 digest digits alone. Then a forgery only needs a message whose digest digits all sit below the signed ones, and each try succeeds with probability
For a typical signature that is about tries, within reach of commodity hardware. For signatures with large digits it collapses: if every , then , about two thousand tries. We demonstrated a real forgery against that implementation in 4.8 seconds on a laptop.
With the checksum
Claim. If have different digests, then some digit of is larger than the matching digit of .
Proof. Suppose for all . The digests differ, so at least one inequality is strict, and each lower digit adds to the checksum:
Both checksums are two-digit base-256 numbers, so forces , or and . Either way a checksum digit increases.
Signing a larger digit means walking a chain backward, which requires a preimage of . The only other route is a second message with the same Keccak-256 digest. The checksum turns a search problem into a hash-inversion problem.
Security level
Under generic attacks, forging a vault signature means inverting the 224-bit chain function or finding a Keccak-256 collision. One signature exposes at most chain values, which a multi-target search can aim at all at once:
These are generic bounds, not a proof about this implementation. They say the design is sound if Keccak behaves as a one-way function; an audit is what checks that the code matches the design.
One key signs once. Two signatures from the same key would let a forger sign any digit up to the larger of the two on every chain, and the checksum argument no longer holds. So the vault closes on its first withdrawal and the key is retired with it.
The vault program
The program is written with Pinocchio, compiled for sBPF v3, and deployed on Solana mainnet at EvB3Ssbh15KNTH6rsE3qQnidimUBNvT8hypfxfZakZDn. A vault is a program-derived address of its key’s root:
The bump is the largest value that lands off the curve, so no Ed25519 private key exists for the vault. Funds can leave only through the program, and the program releases them only for a valid Winternitz signature against .
Its instructions, accounts, and byte layouts are listed under Smart contract below.
On withdrawal the signed message is the destination account itself, . Anyone may submit the transaction, but changing the destination changes every digit and invalidates the signature, so a front-runner cannot redirect the funds. The program recomputes equations (11)–(15) with the Keccak and SHA-256 syscalls and accepts only if the recovered root derives exactly this vault’s address.
A withdrawal transaction is 1,230 of Solana’s 1,232 bytes and is sent with a 1.4 million compute-unit limit. That leaves no room for a change output, which is why partial spends wait on an address lookup table.
Smart contract
The vault is one Solana program. Everything below was read from mainnet; you can check each line yourself with the commands at the end of this section.
| program id | solscan explorer |
| network | Solana mainnet-beta |
| loader | BPF Loader Upgradeable |
| program data | DPsJLQRTpo6homGnQoigdCcKe4YGwRmiiSWvkpvLP9oD |
| build | Pinocchio 0.7, sBPF v3, 20,792 bytes |
| deployed | slot 454,723,739 |
| source | github.com/z0s-app/z0s-vault (MIT) |
| binary sha-256 | 57bc3e6e8d7250775724d2a6f8b0a2d34d948b67719a71a14d8652fffa7d9afb |
| upgrade authority | 3mR75xXhaJ6qgXBubc7ZZRk7D46VWzjJb5Fg4KJ5YrKD upgradeable |
On upgrades. The program is upgradeable: the authority key can replace its code. That is normal before an audit, and it means you are trusting that key. The plan is to make the program immutable once it is audited.
A fork of Dean Little's solana-winternitz-vault (MIT) with one change: the signature scheme carries the standard Winternitz checksum.
Instructions
The first byte of instruction data selects the instruction. Data is read in place from the input buffer, never copied to the stack, so the 952-byte signature fits the SBF frame.
| tag | data after the tag | accounts | effect |
|---|---|---|---|
| 0 open | root (32) + bump (1) | payer (signer, writable), vault (writable), system program | Creates the vault account at the program address of its key's root, rent paid by the opener. |
| 2 close | signature (952) + bump (1) | vault (writable), refund (writable) | Recovers the root from a signature over the refund address, checks it derives this vault, sends the whole balance to refund, and closes the vault. |
| 1 split | signature (952) + amount (8) + bump (1) | vault, split, refund (all writable) | Pays part of the balance and moves the change. In the program, not yet in the interface: its transaction is over Solana's size limit until address lookup tables are added. |
| deposit | system transfer | payer, vault | A plain SOL transfer into the vault. The interface caps a vault at 0.1 SOL. |
The verifier
Withdrawal is the only path out, and it is short. Condensed from the deployed source (loops tidied, error paths shortened), the program rebuilds the digits of equations (11) and (12), finishes each chain of equation (14), and compares the resulting address with the vault, equation (21):
// close: data = signature (952) || bump (1); the signed message is the refund address pub fn close_vault(accounts: &[AccountInfo], data: &[u8]) -> ProgramResult { let signature: &[u8; SIG_LEN] = data[..SIG_LEN].try_into().unwrap(); let bump = data[SIG_LEN]; let [vault, refund] = accounts else { return Err(NotEnoughAccountKeys) }; let root = recover_root(signature, refund.key()); // eqs (11)-(15) if !is_vault(&root, bump, vault.key()) { // eq (21) return Err(ProgramError::MissingRequiredSignature); } *refund.try_borrow_mut_lamports()? += vault.lamports(); // whole balance vault.close() // key retired } // digits: 32 digest bytes, then the checksum C = sum(255 - d_i) as two bytes fn digits(message: &[u8]) -> [u16; 34] { let digest = keccak(&[message]); let mut checksum: u16 = 0; for &byte in digest.iter().take(32) { checksum += 255 - byte as u16; } let mut out = [0u16; 34]; for i in 0..32 { out[i] = digest[i] as u16; } out[32] = (checksum >> 8) & 0xff; out[33] = checksum & 0xff; out } // finish every chain d_i more steps, then hash the 34 tips into the root pub fn recover_root(signature: &[u8; 952], message: &[u8]) -> [u8; 32] { let d = digits(message); let mut tips = [0u8; 952]; for i in 0..34 { let mut v: [u8; 28] = signature[i * 28..i * 28 + 28].try_into().unwrap(); for _ in 0..d[i] { v.copy_from_slice(&keccak(&[&v])[..28]); } tips[i * 28..i * 28 + 28].copy_from_slice(&v); } keccak(&[&tips]) }
Hashing goes through the runtime’s own sol_keccak256 and sol_sha256 syscalls. A failed check returns MissingRequiredSignature and moves nothing.
Check it yourself
$ solana program show EvB3Ssbh15KNTH6rsE3qQnidimUBNvT8hypfxfZakZDn $ solana program dump EvB3Ssbh15KNTH6rsE3qQnidimUBNvT8hypfxfZakZDn onchain.so $ git clone https://github.com/z0s-app/z0s-vault && cd z0s-vault $ cargo build-sbf --arch v3 $ shasum -a 256 target/deploy/z0s_vault.so ../onchain.so 57bc3e6e8d7250775724d2a6f8b0a2d34d948b67719a71a14d8652fffa7d9afb target/deploy/z0s_vault.so 57bc3e6e8d7250775724d2a6f8b0a2d34d948b67719a71a14d8652fffa7d9afb ../onchain.so
The first command shows the authority, deploy slot, and size above. The second saves the deployed bytecode. The last three build the published source and hash both: with cargo-build-sbf 4.1.0, platform-tools v1.54, the two binaries are identical, byte for byte. Because the program is upgradeable, run the check again before you rely on it.
The watch agent
Every day the agent pulls new papers from arXiv and the IACR ePrint archive, keeps those about breaking or weakening elliptic-curve or RSA signatures, post-quantum signatures, or quantum attacks on cryptography, and labels each one:
| proven | a concrete result, construction, or attack demonstrated in the paper |
| stated | a claim, proposal, or analysis that does not demonstrate a break |
| speculation | a possibility raised or discussed, not established |
The model may only cite papers it was given: every title and link is checked against the fetched feed, so it cannot invent a source. Digests are stored, so the free view (labels and sources) and the full view (with the agent’s analysis) always come from the same run.
Burns and the token
Paid features are priced in dollars and paid by burning z0s. A quote converts the price at the live Jupiter price , rounding up so the burn is never worth less than the price:
The server accepts a burn only if all of the following hold:
The full amount is destroyed; nothing is paid to anyone. One vault burn opens one vault. That burn is checked by the z0s interface, not by the program: the program is open, and anyone calling it directly can skip the interface and its price. Withdrawing never needs a burn.
Total supply starts at one billion, so the amount burned by everyone is read straight from the chain:
Checking an address, the free watch view, bug reports, and every vault withdrawal cost nothing. Holding z0s does not protect a wallet.
What never leaves your device
- The vault seed. Generated in your browser, shown once, kept only in session storage. Keys and signatures are computed on your device.
- Your wallet key. The wallet pays fees and receives withdrawals. It never signs for the vault itself.
- Sign-in. Signing in signs a text message, not a transaction. It proves a wallet is yours and cannot move funds.
- Server keys. Database and RPC credentials live only on the server. The database refuses every request that does not come from it.
Limits
- The vault program is unaudited. Each vault is capped at 0.1 SOL until it is.
- It holds SOL only, and a withdrawal takes the whole balance.
- Its security rests on Keccak. It is resistant to an elliptic-curve break as long as the hash function holds.
- Every Solana transaction is still paid for with an Ed25519 signature. If curves break, the network itself needs an upgrade before anyone can transact safely.
- Lose the seed and the funds are gone. Nobody can recover them, including us.
- It cannot protect the market value of what it holds.
References
- L. Lamport, Constructing Digital Signatures from a One Way Function, SRI International, 1979.
- R. Merkle, A Certified Digital Signature, CRYPTO '89. Credits the Winternitz improvement to R. Winternitz.
- A. Hülsing, D. Butin, S. Gazdag, J. Rijneveld, A. Mohaisen, RFC 8391: XMSS, eXtended Merkle Signature Scheme, IRTF, 2018.
- P. Shor, Polynomial-Time Algorithms for Prime Factorization and Discrete Logarithms on a Quantum Computer, 1994.
- L. Grover, A Fast Quantum Mechanical Algorithm for Database Search, 1996.
- M. Roetteler, M. Naehrig, K. Svore, K. Lauter, Quantum Resource Estimates for Computing Elliptic Curve Discrete Logarithms, ASIACRYPT 2017.
- J. Pollard, Monte Carlo Methods for Index Computation (mod p), 1978.
- Solana, Quantum readiness, solana.com.
- Anza, Securing Solana against a powerful quantum adversary, anza.xyz, 2026.