# Umbra: Private UTXO Payment System
## Technical Overview
### Table of Contents
1. [Architecture Overview](#architecture-overview)
2. [System Components](#system-components)
3. [Privacy Model](#privacy-model)
4. [Cryptographic Primitives](#cryptographic-primitives)
5. [Zero-Knowledge Proofs](#zero-knowledge-proofs)
6. [Encrypted Storage](#encrypted-storage)
7. [Security Model](#security-model)
---
## Architecture Overview
Umbra is a privacy-preserving payment system built on Ethereum (Sepolia) using a UTXO model with zero-knowledge proofs. It enables private transfers of USDC where transaction amounts and relationships between sender/receiver are hidden from observers.
### High-Level Architecture
```
┌──────────────────────────────────────────────────────────┐
│ ETHEREUM (Sepolia) - Source of Truth │
│ ┌────────────────────────────────────────────────────┐ │
│ │ PrivateUTXOLedger Contract │ │
│ │ - SP1 Groth16 Verifier (on-chain proof verify) │ │
│ │ - UTXO Merkle Tree State │ │
│ │ - Nullifier Registry (double-spend prevention) │ │
│ │ - USDC Custody │ │
│ └────────────────────────────────────────────────────┘ │
│ ┌─────────────────────┐ ┌────────────────────────────┐ │
│ │EncryptedContacts │ │PrivatePaymentRequests │ │
│ │- Tag-based lookup │ │- Encrypted payment reqs │ │
│ │- ECIES encryption │ │- Tag-based recipient IDs │ │
│ └─────────────────────┘ └────────────────────────────┘ │
└──────────────────────────────────────────────────────────┘
▲
│ Submit proven transactions
│ (Gasless via Account Abstraction)
┌─────────────────────────┴─────────────────────────────┐
│ │
▼ │
┌──────────────────┐ │
│ Relayer Server │ │
│ - Alchemy Smart │ │
│ Account (AA) │ │
│ - Gas Sponsored │ │
│ │ │
└──────────────────┘ │
▲ │
│ Proof + encrypted notes │
│ │
┌─────────────────────────────────────────────────────────────────┐ │
│ Wallet UI (Next.js) │ │
│ - Key derivation from MetaMask signature │─────────┘
│ - UTXO scanning and decryption (client-side) │ Read state
│ - Transaction construction │ (events, roots)
│ - Dual signature model (NullifierSig + TxSig) │
│ - All cryptography happens client-side │
└─────────────────────────────────────────────────────────────────┘
│
│ Witness data (notes, signatures, indices)
▼
┌──────────────────┐
│ Prover Server │
│ - Express.js │
│ - Spawns Rust │
│ prover-host │
└──────────────────┘
│
│ Proof request via SP1 SDK
▼
┌──────────────────┐
│ Succinct Prover │
│ Network │
│ - zkVM Execution │
│ - Groth16 Proof │
│ (~30-60 seconds) │
└──────────────────┘
```
### Data Flow
1. **Login**: User signs a fixed message with MetaMask → signature hashed → secp256k1 keypair derived
2. **Scan**: Wallet reads `OutputCommitted` events from contract, attempts ECIES decryption with private key
3. **Send**: User constructs transaction, signs commitments (NullifierSig + TxSig), sends witness to Prover Server
4. **Prove**: Prover Server precomputes values, submits to Succinct Network for Groth16 proof generation
5. **Submit**: Wallet receives proof, sends to Relayer (gasless)
6. **Verify**: Contract verifies SP1 proof on-chain, checks nullifiers unused, updates Merkle state
---
## System Components
| Component | Role | Trust Level |
|-----------|------|-------------|
| **PrivateUTXOLedger** | On-chain source of truth. Verifies SP1 proofs, manages UTXO state, prevents double-spends | Trustless (code is law) |
| **EncryptedContacts** | On-chain encrypted address book storage | Trustless (encryption is client-side) |
| **PaymentRequests** | On-chain encrypted payment request storage | Trustless (encryption is client-side) |
| **Wallet UI** | All client-side cryptography: key derivation, UTXO scanning, encryption, signing | Self-custody (keys never leave browser) |
| **Prover Server** | Orchestrates proof generation via Succinct Network | Not trusted - proofs verified on-chain |
| **Relayer** | Submits transactions via Alchemy Smart Account, pays gas | Cannot steal funds or modify proofs |
| **Succinct Network** | Generates Groth16 proofs from SP1 zkVM execution | Not trusted - proofs verified on-chain |
### Contract Addresses (Sepolia)
| Contract | Address |
|----------|---------|
| PrivateUTXOLedger | `0x42ae920DFD0d25Ac014DFd751bd2ff2D2fBa0443` |
| EncryptedContacts | `0x813e453D13dE769922aFc40780FADeF3AC6d939D` |
| PaymentRequests | `0x3c4d73f028d99eC10eB15fED99AC5080C99A4a4d` |
---
## Privacy Model
### What's Hidden vs. Public
| Data | On-Chain Visibility | Who Can Decrypt |
|------|---------------------|-----------------|
| Note commitment | Public | No one (hash) |
| Encrypted note data | Public (in events) | Only recipient |
| Nullifier | Public (when spent) | No one (hash of signature) |
| Transaction amount | Hidden | Sender & Recipient |
| Sender identity | Hidden | Recipient only (via contact name in note) |
| Recipient identity | Hidden | Sender only |
| Contacts | Encrypted on-chain | Only owner |
| Payment requests | Encrypted on-chain | Only recipient |
### Key Privacy Properties
- **Amount Privacy**: Transaction values are never revealed on-chain
- **Sender Privacy**: Cannot determine who sent a payment from on-chain data
- **Recipient Privacy**: Cannot determine who received a payment
- **Unlinkability**: Cannot link inputs to outputs in a transaction (nullifiers hide source notes)
---
## Cryptographic Primitives
### Key Derivation
Users derive a deterministic keypair from their Ethereum wallet signature:
```
User signs fixed message → SHA-256(domain + signature) → secp256k1 Private Key → Public Key
```
```typescript
const domain = 'utxo-prototype-v1-key-derivation:'
const privateKey = sha256(domain + signature)
const publicKey = secp256k1.getPublicKey(privateKey, true) // compressed
const address = '0x' + hex(publicKey) // "Private Address"
```
- Keys exist only in browser memory - never transmitted
- Same MetaMask signature always produces same keypair (deterministic)
### Note Structure
A note represents a private UTXO:
```rust
Note {
amount: u64, // Value in USDC micro-units (6 decimals)
owner_pubkey: [u8; 32], // Recipient's X-coordinate (from compressed pubkey)
blinding: [u8; 32] // Random factor for commitment uniqueness
}
```
### Note Commitment
Domain-separated Blake3 hash ensuring collision resistance:
```
Commitment = Blake3(DOMAIN_NOTE_COMMITMENT || amount_le(8) || owner_pubkey(32) || blinding(32))
```
Where:
- `DOMAIN_NOTE_COMMITMENT = b"NOTE_COMMITMENT_v1"`
- `amount_le` = 8-byte little-endian encoding
- Commitment is stored on-chain in Merkle tree
- Blinding factor ensures two notes with same amount/owner have different commitments
### Nullifier Construction
Prevents double-spending while hiding which note is being spent:
```
Nullifier = Blake3(DOMAIN_NULLIFIER || NullifierSignature)
```
Where:
- `DOMAIN_NULLIFIER = b"NULLIFIER_v1"`
- `NullifierSignature` = ECDSA signature over `ethSignedMessage(keccak256(commitment))`
**Security Properties:**
- Only the owner can compute the nullifier (requires private key to sign)
- Cannot be computed from commitment alone
- Published on-chain when note is spent → prevents double-spend
- Deterministic: same note + same key always produces same nullifier
### Dual Signature Model
Each input note requires TWO signatures for security:
1. **NullifierSig**: Used to derive the nullifier (privacy)
- Signs: `keccak256(commitment)` with Ethereum prefix
- Purpose: Deterministic nullifier derivation
2. **TxSig**: Proves ownership for the ZK circuit (anti-theft)
- Signs: `keccak256(commitment)` with Ethereum prefix
- Purpose: Recovered pubkey must match note's `owner_pubkey`
Both signatures use the same message but serve different purposes in the security model.
### ECIES Encryption (Notes)
Notes are encrypted to the recipient's public key using ECIES:
```
1. Generate ephemeral keypair (r, R = r*G)
2. Compute shared secret: S = ECDH(r, recipient_pubkey)
3. Derive AES key: K = HKDF-SHA256(S, info="utxo-prototype-v1-encryption")
4. Encrypt: AES-256-GCM(K, nonce, plaintext)
5. Output: (R || nonce || ciphertext)
```
- Ephemeral pubkey (R) is stored on-chain in `OutputCommitted` events
- Only recipient can decrypt using their private key
- Forward secrecy via ephemeral keys
### ECIES Encryption (Contacts & Payment Requests)
Contacts and payment requests use a similar ECIES scheme:
```
1. Generate ephemeral keypair
2. ECDH shared secret with owner/recipient pubkey
3. HKDF-SHA256 key derivation (domain: "utxo-contacts-v1" or "utxo-requests-v1")
4. AES-256-GCM encryption
5. Store: ephemeralPub(33) || nonce(12) || ciphertext
```
---
## Zero-Knowledge Proofs
### SP1 zkVM
We use Succinct's SP1 zkVM which allows writing ZK circuits in Rust. The circuit is compiled to a RISC-V ELF binary and executed in a zkVM that generates Groth16 proofs verifiable on Ethereum.
### Optimized Proving Path
The system uses an **optimized path** where expensive ECDSA operations are performed on the host (prover server) rather than inside the zkVM:
```
Host (Rust):
- Verify signatures via k256 library
- Compute nullifiers from signatures
- Compute commitments from note data
- Pass precomputed values to zkVM
zkVM (SP1):
- Verify precomputed commitments match note data (Blake3)
- Use precomputed nullifiers (no ECDSA in circuit)
- Verify value conservation
- Output ABI-encoded public values
```
This reduces proving time by 40-60% compared to in-circuit ECDSA.
### What the Circuit Proves
| Statement | What It Proves | Why It Matters |
|-----------|----------------|----------------|
| Commitment Validity | Input commitments match `Hash(note)` | Cannot spend notes that don't exist |
| Nullifier Correctness | Nullifiers derived from valid signatures | Deterministic spend tracking |
| Output Validity | Output commitments properly formed | New notes are correctly structured |
| Value Conservation | `sum(inputs) >= sum(outputs)` | Cannot create money from nothing |
### Circuit Logic (Simplified)
```rust
// VERIFY precomputed values match note data
for (note, precomputed_commitment) in inputs {
assert!(commit(note) == precomputed_commitment);
}
// VERIFY value conservation
assert!(sum(input_amounts) >= sum(output_amounts));
// OUTPUT (public, ABI-encoded):
// - old_root (contract verifies this matches current state)
// - new_root (new Merkle root after adding outputs)
// - nullifiers (marks inputs as spent)
// - output_commitments (new notes added to tree)
```
### Security Note
The standard path (in-circuit ECDSA) is **disabled** for security. The system requires precomputed values where:
- Host verifies signatures before proving
- zkVM verifies precomputed values via hash matching
- Contract verifies the proof and checks nullifiers haven't been used
---
## Encrypted Storage
### EncryptedContacts Contract
Stores encrypted address book entries on-chain:
```solidity
mapping(bytes8 => uint256[]) public contactsByOwner; // ownerTag => contactIds
mapping(uint256 => Contact) public contacts;
struct Contact {
bytes8 ownerTag; // First 8 bytes of keccak256(owner_pubkey)
bytes encryptedData; // ECIES-encrypted contact data
uint256 timestamp;
}
```
**Owner Tag Computation:**
```typescript
ownerTag = keccak256(publicKeyHex).slice(0, 8) // First 8 bytes
```
### PaymentRequests Contract
Stores encrypted payment requests:
```solidity
mapping(bytes8 => uint256[]) public requestsByRecipient; // recipientTag => requestIds
mapping(uint256 => Request) public requests;
struct Request {
bytes8 recipientTag;
bytes encryptedPayload; // ECIES-encrypted: {requesterName, requesterAddress, amount, reference, message}
uint256 timestamp;
RequestStatus status; // Pending, Approved, Rejected, Expired
}
```
---
## Security Model
### Threat Model
| Threat | Mitigation |
|--------|------------|
| Double-spend | Nullifier registry on-chain - each nullifier used once |
| Forged ownership | ECDSA signature verification (host + zkVM hash check) |
| Invalid amounts | Value conservation check in ZK circuit |
| Malicious prover | All proofs verified on-chain by SP1 Verifier |
| Front-running | Nullifiers hide which specific note is being spent |
| Key theft | Keys derived from wallet signature, exist only in browser |
| Replay attacks | Nullifiers are one-time use, nonces in Permit2 |
| Proof-binding bypass | Contract decodes outputs from `publicValues` (not separate params) |
### Trust Assumptions
| Component | Security Trust | Liveness Trust |
|-----------|----------------|----------------|
| Smart Contract | Full (verified on-chain) | Ethereum availability |
| Succinct Network | None (proofs verified on-chain) | Required for proof generation |
| Relayer | None (cannot modify proofs) | Required for gasless tx submission |
| Wallet UI | Self-custody | User's device |
### Cryptographic Assumptions
- **secp256k1 ECDSA**: Discrete log hardness
- **Blake3**: Collision resistance, preimage resistance
- **Groth16**: Knowledge-of-exponent assumption, q-PKE
- **AES-256-GCM**: Standard symmetric encryption security
- **HKDF-SHA256**: PRF security of HMAC-SHA256
### Key Security Properties
- **Soundness**: Cannot create valid proof without knowing private keys
- **Zero-Knowledge**: Proof reveals nothing about transaction details
- **Unlinkability**: Cannot link sender to recipient from on-chain data
- **Non-Malleability**: Proofs cannot be modified without invalidation
---
## Gasless Transactions
All user-facing transactions are gasless via Alchemy Account Abstraction:
### Relayer Architecture
```javascript
// Alchemy Smart Account with Gas Policy
const smartAccountClient = await createLightAccountAlchemyClient({
chain: sepolia,
signer: LocalAccountSigner.privateKeyToAccountSigner(RELAYER_PRIVATE_KEY),
policyId: GAS_POLICY_ID, // Alchemy gas sponsorship
});
```
### Supported Gasless Operations
| Endpoint | Operation |
|----------|-----------|
| `/api/deposit-with-permit` | Deposit USDC (Permit2 signature) |
| `/api/submit-tx` | Private transfer (proof required) |
| `/api/withdraw` | Withdraw to public address (proof required) |
| `/api/save-contact` | Save encrypted contact |
| `/api/create-payment-request` | Create encrypted payment request |
---
## References
- [SP1 Documentation](https://docs.succinct.xyz/)
- [Groth16 Paper](https://eprint.iacr.org/2016/260)
- [ECIES Specification](https://cryptopp.com/wiki/Elliptic_Curve_Integrated_Encryption_Scheme)
- [Blake3 Specification](https://github.com/BLAKE3-team/BLAKE3-specs)
- [Alchemy Account Kit](https://accountkit.alchemy.com/)
- [Permit2](https://github.com/Uniswap/permit2)