Skip to content

z0s:/docs

no break shownCAnot live yetFollow @usez0s

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 dd, the public key is a curve point, and the curve is fixed:

E:  y2=x3+7   over   Fp,p=2256−232−977,Q=d⋅GE:\; y^2 = x^3 + 7 \;\text{ over }\; \mathbb{F}_p,\qquad p = 2^{256} - 2^{32} - 977,\qquad Q = d \cdot G
(1)

Solana signs with Ed25519 on a twisted Edwards curve. Its secret scalar aa gives the public point AA, and on Solana that point is the address itself:

−x2+y2=1+d x2y2   over   F2255−19,A=[a] B,ℓ=2252+27742317777372353535851937790883648493-x^2 + y^2 = 1 + d\,x^2y^2 \;\text{ over }\; \mathbb{F}_{2^{255}-19},\qquad A = [a]\,B,\qquad \ell = 2^{252} + 27742317777372353535851937790883648493
(2)

Recovering dd from QQ 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:

Tρ≈πn/4  ≈  2127.8 group operations for n≈2256T_{\rho} \approx \sqrt{\pi n / 4} \;\approx\; 2^{127.8}\ \text{group operations for } n \approx 2^{256}
(3)

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

q  ≤  9n+2⌈log⁡2n⌉+10  =  2330  logical qubits for n=256q \;\le\; 9n + 2\lceil \log_2 n \rceil + 10 \;=\; 2330 \ \text{ logical qubits for } n = 256
(4)

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 kk-bit preimage, Grover’s algorithm gives only a quadratic speedup,

Tclassical=O(2k)⟶TGrover=O(2k/2)T_{\text{classical}} = O(2^{k}) \quad\longrightarrow\quad T_{\text{Grover}} = O(2^{k/2})
(5)

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:

Bitcoin P2PKH:a=RIPEMD160⁡ ⁣(SHA256⁡(Q))Ethereum:a=Keccak256⁡(Qx ∥ Qy)[12, 32)Solana:a=A\begin{aligned} \text{Bitcoin P2PKH:}\quad & a = \operatorname{RIPEMD160}\!\big(\operatorname{SHA256}(Q)\big)\\ \text{Ethereum:}\quad & a = \operatorname{Keccak256}(Q_x \,\|\, Q_y)_{[12,\,32)}\\ \text{Solana:}\quad & a = A \end{aligned}
(6)
chain / typekey hidden whilekey visible once
BTC P2PKH, P2WPKHthe address has never spentany output is spent (the signature carries QQ)
BTC P2SH, P2WSHnever spent (only a script hash is public)spent: the script and its keys are revealed
BTC P2TR (Taproot)neveralways: the output key is in the address
BTC P2PKneveralways: coins are paid straight to QQ
ETH accountnonce is 0 and no delegation signedit sends a transaction or signs an EIP-7702 delegation
SOL walletneveralways: the address is the public key
contracts, programs, PDAsno 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 n=224n = 224 bits:

H(x)=Keccak256⁡(x)[0, 28),Hk=H∘⋯∘H⏟k,H0=idH(x) = \operatorname{Keccak256}(x)_{[0,\,28)},\qquad H^{k} = \underbrace{H \circ \cdots \circ H}_{k},\qquad H^{0} = \mathrm{id}
(7)
parametervaluemeaning
nn224 bits (28 bytes)size of every chain value
ww256Winternitz parameter: one digit per message byte
ℓ1\ell_132message digits (one Keccak-256 digest)
ℓ2\ell_22checksum digits
ℓ\ell34chains per key, so a signature is 34 × 28 = 952 bytes

The checksum length follows the standard formula:

ℓ1=⌈256log⁡2w⌉=32,ℓ2=⌊log⁡2 ⁣(ℓ1(w−1))log⁡2w⌋+1=⌊log⁡281608⌋+1=2\ell_1 = \left\lceil \frac{256}{\log_2 w} \right\rceil = 32,\qquad \ell_2 = \left\lfloor \frac{\log_2\!\big(\ell_1 (w-1)\big)}{\log_2 w} \right\rfloor + 1 = \left\lfloor \frac{\log_2 8160}{8} \right\rfloor + 1 = 2
(8)

Keys

Every vault key is derived from one 32-byte master seed and the vault’s index jj, 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.

si=Keccak256⁡(seed ∥ u32le⁡(j) ∥ u8⁡(i))[0, 28),pi=H256(si),i∈[0,34)s_i = \operatorname{Keccak256}\big(\text{seed} \,\|\, \operatorname{u32_{le}}(j) \,\|\, \operatorname{u8}(i)\big)_{[0,\,28)},\qquad p_i = H^{256}(s_i),\qquad i \in [0, 34)
(9)

The public key is compressed to a single 32-byte root:

R=Keccak256⁡(p0 ∥ p1 ∥ ⋯ ∥ p33)R = \operatorname{Keccak256}\big(p_0 \,\|\, p_1 \,\|\, \cdots \,\|\, p_{33}\big)
(10)

Signing

A message mm becomes 34 base-256 digits: the 32 bytes of its digest, then a two-digit checksum that rises whenever a message digit falls.

(d0,…,d31)=Keccak256⁡(m),C=∑i=031(255−di)∈[0,8160](d_0, \ldots, d_{31}) = \operatorname{Keccak256}(m),\qquad C = \sum_{i=0}^{31} (255 - d_i) \in [0, 8160]
(11)
d32=⌊C/256⌋,d33=C mod 256d_{32} = \lfloor C / 256 \rfloor,\qquad d_{33} = C \bmod 256
(12)

Each chain is walked part of the way, by its digit:

σi=H 256−di(si),σ=σ0 ∥ ⋯ ∥ σ33\sigma_i = H^{\,256 - d_i}(s_i),\qquad \sigma = \sigma_0 \,\|\, \cdots \,\|\, \sigma_{33}
(13)
  s_i ──H──▶ ··· ──H──▶ σ_i ──H──▶ ··· ──H──▶ p_i
   │                    │                    │
   └──── 256 − d_i ─────┘──────── d_i ───────┘
        signer hashes        verifier hashes

Verifying

The verifier finishes every chain and checks that the result hashes to the root. It never needs the secrets:

H di(σi)=H di(H 256−di(si))=H256(si)=piH^{\,d_i}(\sigma_i) = H^{\,d_i}\big(H^{\,256-d_i}(s_i)\big) = H^{256}(s_i) = p_i
(14)
Verify⁡(m,σ,R)  ⟺  Keccak256⁡(Hd0(σ0) ∥ ⋯ ∥ Hd33(σ33))=R\operatorname{Verify}(m, \sigma, R) \iff \operatorname{Keccak256}\big(H^{d_0}(\sigma_0) \,\|\, \cdots \,\|\, H^{d_{33}}(\sigma_{33})\big) = R
(15)

Verification costs ∑idi\sum_i d_i hash calls. For a random digest that is about 32×127.5+15+127.5≈4,22032 \times 127.5 + 15 + 127.5 \approx 4{,}220 Keccak evaluations, which the program does in roughly 622,000 compute units.

Why the checksum

A signature reveals intermediate chain values. From σi\sigma_i 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 mm can produce a valid signature on any m′m' whose digits are all no larger: di′≤did'_i \le d_i for every ii.

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

P=∏i=031di+1256,E ⁣[log⁡2P]=32⋅log⁡2256!−256⋅8256≈−45.5P = \prod_{i=0}^{31} \frac{d_i + 1}{256},\qquad \mathbb{E}\!\left[\log_2 P\right] = 32 \cdot \frac{\log_2 256! - 256 \cdot 8}{256} \approx -45.5
(16)

For a typical signature that is about 2452^{45} tries, within reach of commodity hardware. For signatures with large digits it collapses: if every di≥200d_i \ge 200, then P≥(201/256)32≈2−11P \ge (201/256)^{32} \approx 2^{-11}, about two thousand tries. We demonstrated a real forgery against that implementation in 4.8 seconds on a laptop.

With the checksum

Claim. If m′≠mm' \ne m have different digests, then some digit of m′m' is larger than the matching digit of mm.

Proof. Suppose di′≤did'_i \le d_i for all i<32i < 32. The digests differ, so at least one inequality is strict, and each lower digit adds to the checksum:

C′=∑i=031(255−di′)  >  ∑i=031(255−di)=CC' = \sum_{i=0}^{31} (255 - d'_i) \;>\; \sum_{i=0}^{31} (255 - d_i) = C
(17)

Both checksums are two-digit base-256 numbers, so C′>CC' > C forces d32′>d32d'_{32} > d_{32}, or d32′=d32d'_{32} = d_{32} and d33′>d33d'_{33} > d_{33}. Either way a checksum digit increases. ■\blacksquare

Signing a larger digit means walking a chain backward, which requires a preimage of HH. 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 T=34×256≈213T = 34 \times 256 \approx 2^{13} chain values, which a multi-target search can aim at all at once:

preimage:2224/T≈2211 (classical),2224/T≈2106 (Grover)\text{preimage:}\quad 2^{224}/T \approx 2^{211}\ \text{(classical)},\qquad \sqrt{2^{224}/T} \approx 2^{106}\ \text{(Grover)}
(18)
digest collision:2128 (classical)\text{digest collision:}\quad 2^{128}\ \text{(classical)}
(19)

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:

vault=SHA256⁡(R ∥ b ∥ program_id ∥ "ProgramDerivedAddress"),off the Ed25519 curve\text{vault} = \operatorname{SHA256}\big(R \,\|\, b \,\|\, \text{program\_id} \,\|\, \texttt{"ProgramDerivedAddress"}\big),\quad \text{off the Ed25519 curve}
(20)

The bump bb 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 RR.

Its instructions, accounts, and byte layouts are listed under Smart contract below.

On withdrawal the signed message is the destination account itself, m=refund∈{0,1}256m = \text{refund} \in \{0,1\}^{256}. 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.

PDA⁡(Recover⁡(σ,refund), b)=?vault\operatorname{PDA}\big(\operatorname{Recover}(\sigma, \text{refund}),\, b\big) \overset{?}{=} \text{vault}
(21)

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
networkSolana mainnet-beta
loaderBPF Loader Upgradeable
program dataDPsJLQRTpo6homGnQoigdCcKe4YGwRmiiSWvkpvLP9oD
buildPinocchio 0.7, sBPF v3, 20,792 bytes
deployedslot 454,723,739
sourcegithub.com/z0s-app/z0s-vault (MIT)
binary sha-25657bc3e6e8d7250775724d2a6f8b0a2d34d948b67719a71a14d8652fffa7d9afb
upgrade authority3mR75xXhaJ6qgXBubc7ZZRk7D46VWzjJb5Fg4KJ5YrKD 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.

tagdata after the tagaccountseffect
0 openroot (32) + bump (1)payer (signer, writable), vault (writable), system programCreates the vault account at the program address of its key's root, rent paid by the opener.
2 closesignature (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 splitsignature (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.
depositsystem transferpayer, vaultA 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:

provena concrete result, construction, or attack demonstrated in the paper
stateda claim, proposal, or analysis that does not demonstrate a break
speculationa 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 UU at the live Jupiter price pp, rounding up so the burn is never worth less than the price:

z=⌈Up⋅10δ⌉ raw units,Uwatch=$5 per 30 days,Uvault=$3 per vault openedz = \left\lceil \frac{U}{p} \cdot 10^{\delta} \right\rceil \ \text{raw units},\qquad U_{\text{watch}} = \$5 \text{ per 30 days},\qquad U_{\text{vault}} = \$3 \text{ per vault opened}
(22)

The server accepts a burn only if all of the following hold:

finalized  ∧  no error  ∧  instruction∈{Burn,BurnChecked}∧  mint=z0s  ∧  authority=signed-in wallet  ∧  ∑amount≥z∧  tland∈[ tq,  tq+90 s ]  ∧  signature never used before\begin{aligned} &\text{finalized} \;\wedge\; \text{no error} \;\wedge\; \text{instruction} \in \{\texttt{Burn}, \texttt{BurnChecked}\}\\ &\wedge\; \text{mint} = \text{z0s} \;\wedge\; \text{authority} = \text{signed-in wallet} \;\wedge\; \textstyle\sum \text{amount} \ge z\\ &\wedge\; t_{\text{land}} \in [\,t_q,\; t_q + 90\,\text{s}\,] \;\wedge\; \text{signature never used before} \end{aligned}
(23)

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:

B(t)=109−S(t),market cap(t)=p(t)⋅S(t)B(t) = 10^{9} - S(t),\qquad \text{market cap}(t) = p(t) \cdot S(t)
(24)

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

  1. L. Lamport, Constructing Digital Signatures from a One Way Function, SRI International, 1979.
  2. R. Merkle, A Certified Digital Signature, CRYPTO '89. Credits the Winternitz improvement to R. Winternitz.
  3. A. Hülsing, D. Butin, S. Gazdag, J. Rijneveld, A. Mohaisen, RFC 8391: XMSS, eXtended Merkle Signature Scheme, IRTF, 2018.
  4. P. Shor, Polynomial-Time Algorithms for Prime Factorization and Discrete Logarithms on a Quantum Computer, 1994.
  5. L. Grover, A Fast Quantum Mechanical Algorithm for Database Search, 1996.
  6. M. Roetteler, M. Naehrig, K. Svore, K. Lauter, Quantum Resource Estimates for Computing Elliptic Curve Discrete Logarithms, ASIACRYPT 2017.
  7. J. Pollard, Monte Carlo Methods for Index Computation (mod p), 1978.
  8. Solana, Quantum readiness, solana.com.
  9. Anza, Securing Solana against a powerful quantum adversary, anza.xyz, 2026.