VOSA is a privacy pattern that trades fund-flow privacy for simplicity and compliance. Essentially: UTXO without Merkle trees — using O(1) spent-markers with epoch cleanup.
We have a working implementation and are exploring whether this is worth formalizing as an EIP. Looking for feedback.
What is VOSA?
-
Virtual: Addresses exist only as mapping keys, not real EVM accounts
-
One-time: Each address used exactly once (like UTXO notes)
-
Sub-Account: Derived from master key using stealth addresses (ERC-5564)
State is a single flat mapping:
mapping(address => bytes32) balanceCommitmentHash;
// bytes32(0) → never used
// Poseidon(amt, blind, ts) → has balance (timestamp for replay protection)
// 0xDEADDEAD...|block# → spent (prefix + spent block number, deletable after configurable window)
State machine: UNUSED → ACTIVE → SPENT → (cleanup) → UNUSED
No Merkle trees. No nullifier sets. Spent-tracking is O(1) and cleanable.
How It Works
Each VOSA holds a Poseidon hash commitment hiding the balance amount. Operations use standard ECDSA (MetaMask works directly) + Groth16 ZK proofs (on-chain verification):
-
Deposit: Transfer ERC-20 in → ZK proof verifies
commitment == Poseidon(amount, blinder, timestamp)→ store at fresh VOSA -
Transfer: ECDSA signature + ZK proof that
sum(inputs) == sum(outputs)→ mark inputs SPENT, create outputs -
Withdraw: ECDSA signature + ZK proof → mark SPENT, transfer ERC-20 out
-
Consolidate: ECDSA signatures + ZK proof → merge multiple VOSAs into one (simplifies account management, reduces future gas)
-
Cleanup: SPENT markers encode
block.number. After a configurable window (default ~1 month), anyone can callcleanup()to delete expired entries and save gas
Why Poseidon over Pedersen? ~350 constraints vs ~3,400 — proof time drops from ~500ms to ~92ms, storage from 64 bytes to 32 bytes.
Key Design Choice: SPENT_MARKER vs Nullifier
| Aspect | Traditional UTXO (Tornado/Railgun) | VOSA |
|---|---|---|
| Double-spend prevention | Nullifier set | SPENT_MARKER |
| State lookup | O(log n) Merkle proof | O(1) mapping |
| State growth | Unbounded (nullifiers forever) | Bounded (cleanup) |
| Fund flow | Hidden | Visible (by design) |
| 10-year storage (10B txs) | ~320 GB | ~2.7 GB |
VOSA deliberately exposes fund flow in exchange for:
-
~10x fewer ZK circuit constraints
-
O(1) state operations
-
97% storage savings with Epoch cleanup
-
Simpler client implementation
Privacy Model
Hidden:
Amounts,
Balances,
Real-world identity of VOSA holders (via stealth addresses)
Visible:
VOSA-to-VOSA transfer graph (input→output links),
Depositor address,
Withdrawal recipient
This is NOT a bug — it’s designed for compliance-friendly privacy.
Performance
Groth16 + snarkjs WASM, Apple M2. Gas includes ~200K for on-chain Groth16 verification.
| Operation | Proof Time | Gas (L2) |
|---|---|---|
| Deposit (0→1) | ~92ms | ~300K |
| Transfer (1→2) | ~95ms | ~350K |
| Transfer (2→2) | ~147ms | ~370K |
| Withdraw (partial) | ~92ms | ~330K |
| Consolidate (5→1) | ~210ms | ~475K |
Use Cases
-
Corporate balance privacy (hide amounts from competitors) -
Personal wallet privacy (hide holdings) -
Compliant private transactions (auditable fund flow) -
Full anonymity (use Tornado/Railgun instead)
Questions for Discussion
-
Is this pattern useful enough to formalize? Or do existing EIPs already cover this?
-
Is SPENT_MARKER sound? Any security concerns with this approach vs nullifiers?
-
Standalone or combined? Should VOSA be an Informational EIP (the pattern), with a separate ERC for “VOSA-20” (wrapped private ERC-20)?
-
Epoch cleanup incentives — Gas savings sufficient motivation, or need additional incentives?
Related Work
-
ERC-5564 (Stealth Addresses)
-
Tornado Cash (Merkle + Nullifier)
-
Railgun (UTXO with full privacy)
-
Aztec (Private L2)