Cairo · STARKs · Shielded Privacy · iOS Wallet
StarkVeil is a purely native cypherpunk iOS wallet that enforces total financial privacy on Starknet. Unlike standard web3 wallets, StarkVeil removes the need for Trusted Execution Environments (TEEs) and external wallet apps. It brings Zero-Knowledge STARK proof synthesis directly onto A-series silicon via a Rust SDK, gives users a fully self-contained shielded account (no ArgentX needed), and uses an original Shielded Note commitment scheme for private transfers.
Current status (Phase 21 — Live on Sepolia): PrivacyPool contract deployed on Starknet Sepolia testnet. Full U↔S cycle functional: shield, unshield, private transfer and shielded-to-shielded sends all work end-to-end. Wallet uses a clean U/S model inspired by Zashi. Total balance card shows U+S inline breakdown. 3 action buttons: Send, Receive, Shield. Unified Send auto-detects svk: prefix for private transfers vs 0x for public sends. Shield/Unshield is a single toggle view. Receive shows two clearly labelled addresses: S (svk:0x<ivk>:0x<pubkey> — for private receives) and U (0x… — for exchanges). 4-tab nav: Wallet | Swap | Activity | Settings. Activity feed correctly shows +/− prefixes and colours for all 5 event kinds. Phase 21: Completely replaced the hackathon mock proof verifier with a production-grade Circle STARK proving system. The iOS Swift client now synthesizes a 68-column M31/QM31 algebraic execution trace locally via the Rust stwo framework. The Starknet Sepolia PrivacyPool contract natively verifies this STARK proof via Fiat-Shamir FRI layering, guaranteeing balance conservation and unforgeable ownership on-chain without TEEs.
Note: Since this is a native iOS app, you'll have to follow the steps below to reproduce it on your end. No worries, it's just a one-time thing and you do not have to deploy the pool contract or compile the Rust Prover!
-
Clone the repository:
git clone https://github.com/adidshaft/starkveil.git cd starkveil -
Open the project in Xcode:
open ios/StarkVeil/StarkVeil.xcodeproj
-
Run the App:
- Easiest Method (No Apple Developer Account needed): Select any iOS Simulator (e.g., iPhone 15 Pro) at the top of Xcode and press ⌘R (Run).
- On a Physical iPhone (Requires free Apple ID): Click on the
StarkVeilproject in the left sidebar → Signing & Capabilities → Check Automatically manage signing → Select your Personal Team → Choose your plugged-in iPhone at the top → Press ⌘R.
(See Comprehensive Technical Verification below to see exactly how to verify the cryptographic proofs on-chain).
contracts/: The Cairo smart contract that handles the appending of the UTXO Poseidon hashes and validates STARK nullifier proofs to prevent double-spending.prover/: A standard Rust Cargo library that compiles to static binaries (libstarkveil_prover.a) leveraging FFICstrings to pass Proofs to Swift.ios/StarkVeil/: The native Apple SwiftUI application interface. Handles@MainActorthread-safe background light-client syncing, unspent note caching, and premium glassmorphic visual interactions.
The PrivacyPool contract is already deployed on Sepolia. No local node required.
| Network | Starknet Sepolia (v0.14.1) |
| Contract address | 0x019f2e14dfc8133b17c532e8990fb65efd5a596482b209b89c8b5bb6947ff91c |
| Class hash | 0x0316cf3ac017db1524ea7a1195ebd60e48c058439b8c5804fd2e1017dc021de1 |
| Compiler | Scarb / Cairo 2.16.0 · Sierra 1.7.0 |
| Deployed | 2026-03-07 |
| RPC (primary) | https://api.cartridge.gg/x/starknet/sepolia (Cartridge, v0.9.0) |
| Contract on Voyager | View on Voyager |
| Declare tx | View on Voyager |
| Deploy tx | View on Voyager |
The current Sepolia deployment has been exercised end-to-end with live user flows:
| Flow | Status | Reference |
|---|---|---|
Shield 1.0 STRK |
ACCEPTED_ON_L2, SUCCEEDED |
Voyager tx |
| Private transfer proof verification | ACCEPTED_ON_L2, SUCCEEDED |
Voyager tx |
Observed on-device proof generation from the app:
- Proof type:
Private Transfer Proof - Proving system:
Poseidon-based Circle STARKviastwo - Proof size:
1826 felt252 elements - Device-side proving time:
215.8 ms
cd prover
./build_ios.sh
# Expected output: prover/target/StarkVeilProver.xcframeworkcd contracts
scarb build # Produces Sierra + CASM in target/dev/open ios/StarkVeil/StarkVeil.xcodeproj
# Select iPhone 15 Simulator or physical device → ⌘RThe app points to Sepolia by default. No configuration needed.
- Create or import a wallet in the app.
- Tap the avatar → Activate Wallet to see your Starknet address.
- Fund it with Sepolia STRK from the Starknet Faucet.
- Tap Activate — this deploys your OpenZeppelin account on-chain.
- Tap Shield on the main screen.
- Enter an amount (e.g.
0.1) and tap Shield. - Approve + shield multicall submits in one tx.
- On success: a banner appears with a tappable tx hash linking to Voyager.
- Your shielded balance updates immediately.
- Ask the recipient to share their SVK from Receive → Shielded Address (S).
- Tap Send → select Shielded (S) mode.
- Paste the
svk:0x<ivk>:0x<pubkey>address and amount. - Keep some unshielded STRK (
U) in the sender wallet for gas. Private sends are paid from public balance, not shielded notes. - Tap Send (Private) — no amounts or addresses visible on-chain.
- Tap Shield → switch to Unshield tab.
- Enter the amount to withdraw. If there is no exact matching note, the app first splits a larger note into an exact note plus change, then unshields the exact note.
- Keep some unshielded STRK (
U) for gas. - Tap Unshield — STRK returns to your public balance.
For local chain testing with Katana:
katana --dev # RPC: http://127.0.0.1:5050Then re-deploy the contract:
cd /path/to/starknetWallet
node deploy_contract.js # Declares + deploys PrivacyPool via starknet.jsUpdate NetworkEnvironment.swift local case with your new contract address.
| Checkpoint | Expected Result |
|---|---|
| Tap Create Wallet | 12-word BIP-39 mnemonic displayed |
| Write down the 12 words, confirm | Wallet created, proceeds to Activation |
| Import Wallet with same words | Recovers identical Starknet address |
| Checkpoint | Expected Result |
|---|---|
Open AccountActivationView |
Displays computed Starknet address (deterministic) |
| Copy address → fund it via Katana or faucet | ETH balance appears in the view |
| Tap Activate Wallet | Deploys OZ v0.8 account contract on-chain |
| Tap Check Status | AccountActivated state transitions to VaultView |
| Checkpoint | Expected Result |
|---|---|
Tap Shield in ShieldedBalanceCard |
Opens ShieldView with amount + memo fields |
| Enter amount + memo, tap Shield | Tx submitted; isShielding = true spinner shows |
| Wait 5 s (one SyncEngine poll cycle) | Note appears in balance; Activity tab shows green Deposit row |
| Toggle balance visibility (eye icon) | Amount revealed/hidden with animation |
On-chain verification:
sncast --profile katana_test call \ --contract-address <CONTRACT_ADDRESS> \ --function get_mt_next_index # Value increments by 1 per shield
| Checkpoint | Expected Result |
|---|---|
| Tap Private Transfer button (below action grid) | PrivateTransferView opens as full-screen cover |
Enter: recipient shielded address (svk:<ivk>:<pubkey>), amount, optional memo |
Fields validated live |
| Tap Send Privately | On-chain Transfer event emitted with encrypted output commitment |
| Recipient's SyncEngine polls Transfer events | Recipient trial-decrypts the encrypted note with their IVK — note appears in their balance |
| Activity tab on sender's device | Shows 🔒 Transfer row with tx hash |
Privacy confirmation: Query
starknet_getEventsfor the Transfer event — you will see only opaque commitment hashes, no amounts, no addresses.
| Checkpoint | Expected Result |
|---|---|
| Tap Unshield | UnshieldFormView opens |
| Enter recipient public address + amount | Validates against UTXO balance |
| Tap Unshield | Nullifier posted on-chain; ERC-20 transferred to recipient |
| Check on-chain nullifier registry | is_nullifier_spent returns true for that nullifier |
| Try to unshield the same note again | App throws noteAlreadySpent immediately (pre-flight check) |
| Activity tab | Shows 🔓 Unshield row |
On-chain verification:
sncast --profile katana_test call \ --contract-address <CONTRACT_ADDRESS> \ --function is_nullifier_spent \ --calldata <NULLIFIER_HEX> # Returns 1 (true) after a successful unshield
| Checkpoint | Expected Result |
|---|---|
| Shield a note, then unshield it | Succeeds ✅ |
| Attempt to unshield the same note again | App shows noteAlreadySpent error immediately |
| Manually craft duplicate unshield tx on-chain | Cairo contract rejects with Note already spent panic |
| Checkpoint | Expected Result |
|---|---|
| Switch from Sepolia to Mainnet in Settings | All UTXO notes clear instantly |
| Switch back to Sepolia | Sepolia notes reload from SwiftData |
| Mainnet and Sepolia notes never mix | Confirmed by networkId scoping in StoredNote |
| Checkpoint | Expected Result |
|---|---|
| Delete the app | All notes removed from device |
| Reinstall app → Import Wallet with same 12 words | Same Starknet address derived |
| Activate → VaultView | SyncEngine re-scans from block 0, trial-decrypts all shielded events, restores UTXO balance |
This is the current live flow on Starknet Sepolia, using the deployed Stwo verifier and the native iOS prover.
- Shield (
U -> S): public STRK is transferred intoPrivacyPool, which appends a new note commitment to the Merkle tree. - Private transfer (
S -> S): the device generates a Circle STARK proof locally, the contract verifies it on-chain, and the pool appends new opaque commitments plus marks nullifiers as spent. - Unshield (
S -> U): the device proves ownership of a shielded note while binding the public recipient and amount as public inputs. - Gas model: account deployment, public send, shield, private transfer, and unshield are all paid from unshielded STRK (
U). Shielded notes do not pay gas directly. - Discovery model: recipients discover incoming private notes by trial-decrypting encrypted note payloads with their IVK during sync.
- Shield verification example: 0x17af0103f0d1c99f1fc130915d8b1449540b1f33e1227fb103f2b34dc7afb51
- Private transfer verification example: 0x6dce741b675b17a86960a4c79c3d1f6acba5099cd5c78095ba012916049dc37
- Observed local proof generation:
1826 felt252 elementsin about215.8 ms
What happens: A public Starknet account deposits STRK into the PrivacyPool contract.
- Action: User inputs an amount (e.g.
0.1 STRK) and taps Shield. - Algorithms:
- Note Commitment:
c = Poseidon(value, asset_id, owner_pubkey, nonce)- Why Poseidon? It is a ZK-friendly hash function, making it orders of magnitude cheaper to compute inside a STARK circuit later.
- Memo Encryption:
AES-256-GCM(key = HKDF(IVK), plaintext = memo)- Why AES-256-GCM? Standard symmetric encryption to allow the owner's
SyncEngineto recover the memo string. The key is derived purely from the incoming viewing key (IVK).
- Why AES-256-GCM? Standard symmetric encryption to allow the owner's
- Note Commitment:
- On-Chain Verification:
- Query the
PrivacyPoolcontract on Voyager or viasncast. - Find the
Shieldedevent. You will see the depositedamount, theassetcontract address, and the opaquecommitment. - Privacy check: Notice that the
commitmentreveals nothing about the user's IVK or the memo. The wallet's public address is visible as the sender of the transaction, which is expected for a public-to-private deposit.
- Query the
What happens: Moving shielded STRK to another user inside the privacy pool without revealing sender, receiver, or amount.
- Action: User inputs the recipient's Shielded Address (
svk:0x<ivk>:0x<pubkey>) and an amount, then taps Send. - Operational note: The sender still needs public STRK (
U) for gas. The app now warns and shows an estimated STRK requirement when the public balance is too low. - Algorithms:
- Nullifier Generation:
nf = Poseidon(commitment, spending_key)- Why Nullifier? It proves the note is spent without revealing which note was spent. The contract enforces
!nullifiers[nf]to stop double-spends.
- Why Nullifier? It proves the note is spent without revealing which note was spent. The contract enforces
- Output Commitments: New
Poseidonhashes are generated for the recipient's note and the sender's change note. - STARK Proof: The
libstarkveil_proverRust core generates a Cairo-compatible Circle STARK proof asserting:Σ Input = Σ Output + Fee, nullifier correctness, and Merkle inclusion for all spent notes.- Why STARKs? Quantum-resistant, highly scalable, and require no trusted setup (unlike SNARKs).
- Nullifier Generation:
- On-Chain Verification:
- Find the
Transferevent on-chain. - Privacy check: You will only see the proof payload, a list of
nullifiers(spent signals), and a list ofnew_commitments. - Notice that the transaction does not contain any amounts, asset IDs, or recipient addresses. To an outside observer, this transaction is completely opaque.
- Find the
What happens: The recipient's wallet detects the incoming private transfer.
- Action: The wallet's
SyncEnginepolls the RPC every 5 seconds for newTransferevents. - Algorithms:
- Trial Decryption: The wallet attempts to decrypt the
encrypted_notepayload of every new commitment using its ownIVK.if decrypt(ciphertext, IVK) == success: add_to_wallet_balance()- Why Trial Decryption? Since the destination address is not on-chain, the wallet must attempt to unlock every new note. If it succeeds, the note belongs to the user.
- Trial Decryption: The wallet attempts to decrypt the
- Verification:
- The recipient's wallet balance updates automatically. Without the
IVK, no one else can decrypt the memo or know who received the transfer.
- The recipient's wallet balance updates automatically. Without the
What happens: Withdrawing shielded STRK back to a public Starknet account.
- Action: User inputs a public Starknet address (
0x...) and an amount matching a single note, then taps Unshield. - Operational note: Unshield also consumes public STRK (
U) for gas, even though the withdrawn funds come from shielded balance. - Algorithms:
- Nullifier Generation: Same as private transfer, a nullifier is generated to destroy the shielded note.
- STARK Proof: The proof asserts the note's validity and, crucially, binds the
recipient_addressandamountas public inputs.- Why Public Inputs? This cryptographically forces the smart contract to transfer the exact
amountto the exactrecipient_address. A malicious front-runner cannot alter the destination without invalidating the cryptographic proof.
- Why Public Inputs? This cryptographically forces the smart contract to transfer the exact
- On-Chain Verification:
- Find the
Unshieldedevent on-chain. - Privacy check: You will see the
recipientaddress, theamount, and thenullifier. However, you cannot cryptographically link thisnullifierback to the originalcommitmentfrom Stage 1. The identity of the depositor is completely severed from the identity of the withdrawer. - Call
is_nullifier_spent(nf)on the contract; it will return1(true).
- Find the
For an extensive deep-dive into the StarkVeil architecture, the underlying cryptographic mechanics (Merkle Trees, ZK Proof binding, FFI integration), and the local UTXO model, please refer to the core mechanics document:
StarkVeil was continuously audited throughout development. For full security assessments (including vulnerability patches like RFC-6979 deterministic nonces), threat modeling, and formal cryptographic analysis, check the audits directory:
