Designing a stateless, recoverably-secure eCash protocol on Ethereum—routed through on-chain encrypted announcements—requires reconciling two different cryptographic domains: secp256k1 (for standard EVM wallets and stealth cryptography) and BN254 / alt_bn128 (for EVM-native Zero-Knowledge precompiles). By marrying the deterministic derivation of Chaumian eCash with the ERC-5564 Stealth Address standard, we can create a system where the client holds no brittle state. If a user loses their device, their entire wallet—including received off-chain eCash tokens—can be reconstructed simply from their seed phrase and on-chain event logs. Here is the detailed technical architecture and implementation plan for the protocol.
- Cryptographic Foundations & Curve Separation Because the EVM's native precompiles (0x06 for ecAdd, 0x07 for ecMul, and 0x08 for ecPairing) strictly operate on the BN254 curve, the eCash blind signatures must be constructed on BN254. However, the encrypted messaging and stealth address routing will utilize secp256k1 to remain compatible with standard Ethereum keypairs and SDKs like KEthereum or Trust Wallet Core.
- eCash Domain (BN254): Used for token secrets, blind signatures, and smart contract verification.
- Stealth Domain (secp256k1): Used for Diffie-Hellman key exchange, generating ephemeral keys, and encrypting the token payloads for transfer.
- The Stateless Derivation Engine (Deterministic Secrets) To achieve statelessness, every piece of eCash data must be deterministically generated from the user's BIP-39 Master Seed. We utilize a Hierarchical Deterministic (HD) path or an HKDF (HMAC-based Extract-and-Expand Key Derivation Function). For every token index i, the client derives:
- Token Secret (x_i): A 32-byte scalar.
- Blinding Factor (r_i): A 32-byte scalar.
- Nullifier Key (nk_i): An ECDSA private key used as the "Smart Nullifier" to mathematically bind the token to a specific withdrawal transaction, preventing MEV front-running. Recovery Mechanism: If the user resets their app, the client simply re-runs the derivation loop from i = 0 to n, regenerating the exact same secrets, and checking the smart contract to see which nullifiers are already marked as spent.
- Minting: The Blind Diffie-Hellman Flow When Alice locks USDC in the Smart Contract Vault to mint a token, she interacts with the off-chain Mint (TEE Enclave).
- Hash to Curve: Alice takes her deterministic secret x_i and maps it to a point on the BN254 curve: Y = \text{hash_to_curve}(x_i).
- Blinding: Alice masks the point using her deterministic blinding factor r_i and the curve generator G: B' = Y + r_iG
- Signing: The Mint verifies Alice's on-chain deposit, and blindly signs the point with its private key sk: C' = sk \times B'
- Unblinding: Alice receives C' and unblinds it locally to retrieve the valid token signature C: C = C' - r_i(sk \times G) Alice now holds a valid eCash token represented by the tuple: (Secret: x_i, Signature: C, Denomination_ID).
- Transfer: Encrypted On-Chain Announcements (ERC-5564) To send the token to Bob statelessly, Alice must transfer the (x_i, C) tuple to him over a public channel without anyone else intercepting it. She will utilize an ERC-5564-style announcement registry. Step A: Ephemeral Key Exchange Alice obtains Bob's Stealth Meta-Address, which contains his public Viewing Key (V_{pub}) and Spending Key (S_{pub}) on secp256k1.
- Alice generates a random ephemeral private key (ek) and its corresponding public key (EK_{pub}).
- Alice performs an Elliptic Curve Diffie-Hellman (ECDH) exchange: SharedSecret = ECDH(ek, V_{pub}). Step B: Payload Encryption Alice derives a symmetric encryption key from the SharedSecret (e.g., via HKDF). She then encrypts the eCash token tuple using AES-256-GCM or ChaCha20-Poly1305.
- Ciphertext Payload: Encrypt(SymmetricKey, {x_i, C, Denomination_ID}) Step C: On-Chain Publishing Alice submits a transaction to the Ethereum network calling the Announce function on the standard Singleton Registry contract. function announce( uint256 schemeId, address stealthAddress, bytes memory ephemeralPubKey, bytes memory ciphertext ) external;
Note: The transaction fee for this announcement can be subsidized by a relayer or paid by Alice's standard EVM wallet. 5. Discovery & State Recovery (The Receiver) Bob's mobile client remains completely stateless. To find his funds, it passively scans the Announce events emitted by the registry contract.
- Event Parsing: For every event, Bob's client reads the ephemeralPubKey (EK_{pub}).
- ECDH Trial: Bob combines EK_{pub} with his private Viewing Key (v_{priv}) to compute the SharedSecret.
- Trial Decryption: Bob attempts to decrypt the ciphertext. If the MAC check passes, the decryption succeeds.
- Token Extraction: Bob has successfully recovered the eCash token (x_i, C). He can verify its validity locally by checking e(C, G_2) == e(\text{hash_to_curve}(x_i), PK_{mint}). If Bob drops his phone in a lake, he simply imports his seed phrase into a new device. The app re-derives his Viewing Key, parses the chain history, and instantly decrypts all of his historical inbound tokens.
- On-Chain Redemption & MEV Protection When Bob is ready to cash out his tokens back into USDC on his public Ethereum address, he constructs a withdrawal proof.
- BLS Aggregation: If Bob has 5 tokens, he adds the 5 BN254 signature points together off-chain: \sigma_{agg} = \sum C_i.
- MEV Lock: To prevent mempool snipers from replacing his destination address, Bob uses the ECDSA "Smart Nullifier" key associated with each token (derived deterministically from x_i) to sign the payload: "Pay to: <Bob's_Public_Address>".
- Submission: Bob submits the nullifiers (the public keys of the Smart Nullifiers), the ECDSA signatures, and the aggregated BLS signature \sigma_{agg} to the Vault contract. The Vault Contract Execution:
- Verifies that the nullifiers have not been spent.
- Recovers the ECDSA signatures (ecrecover) to prove the transaction intent wasn't hijacked.
- Hashes the nullifiers to the BN254 curve and sums them: M_{agg} = \sum \text{hash_to_curve}(nullifier_i).
- Executes the native ecPairing precompile: e(\sigma_{agg}, G_2) == e(M_{agg}, PK_{mint}).
- If mathematically sound, dispenses the USDC to Bob's address. Technical Stack & Library Mapping To build this on Android/Kotlin per the referenced ecosystem:
- BIP-39 & Key Derivation: kotlin-bip39 (Electric Coin Company) and kotlin-bip for HD path generation.
- secp256k1 (Stealth & ECDSA): secp256k1-kmp (ACINQ) or Trust Wallet Core via JNI for highly optimized ECDH shared secret generation and standard signing.
- BN254 (Blind Signatures): A Kotlin multiplatform wrapper around mcl-wasm (or a direct JNI C++ bridge to an optimized pairings library) will be required to handle the hash_to_curve, point addition, and unblinding logic locally before submitting to the EVM.
Can we pass a hash of smart nullifier's public key as the blinded token secret? That way once we reveal the actual secret for redemption we can sign a message with the matching secret key without public key being known to the mint beforehand, preserving the privacy.
This is a brilliant cryptographic insight, and it actually represents a significant optimization over the previous design. By using the Smart Nullifier's public identity as the blinded secret itself, you create an unbreakable, intrinsic link between the token's validity (the Chaumian blind signature) and the transaction's MEV protection (the ECDSA signature). Because we are building on Ethereum, we can take your idea one step further to make it perfectly optimized for the EVM: instead of using the raw public key, we use the Ethereum Address derived from the public key. Here is exactly how this refined, highly optimized flow works. The "Address-as-Secret" Optimization Ethereum's native ecrecover precompile does not return a full public key; it returns a 20-byte Ethereum Address. If we use this Address as the fundamental secret x_i, we can eliminate redundant variables and shrink the smart contract calldata.
- The Minting Phase (Off-Chain)
- Key Generation: The client deterministically generates a temporary ECDSA "Burner Keypair" for the token. Let's say the public key corresponds to the Ethereum Address 0xabc...123.
- Hash to Curve: The client maps this 20-byte address directly to the BN254 curve: Y = \text{hash_to_curve}(0xabc...123)
- Blinding: The client blinds Y with a random factor r: B' = Y + rG_1
- Signing: The Mint blindly signs B' without ever knowing the underlying address 0xabc...123.
- Unblinding: The client unblinds the signature locally, resulting in a valid point C. The eCash token is now strictly defined by the pair: (Address: 0xabc...123, Signature: C).
- The Redemption Phase (On-Chain) When the user wants to cash out, the beauty of your proposal shines. The user does not need to pass the "secret" to the smart contract at all. They only pass the ECDSA signature. The Payload:
- recipient: The destination address for the USDC.
- clientSigs: An array of standard ECDSA signatures (signed by the Burner Keys over the message "Pay to: ").
- aggregatedSignature: The summed BLS signatures (\sigma_{agg} = \sum C_i).
-
The Smart Contract Execution (Solidity) The Vault contract verifies the MEV protection and recovers the secret "nullifier" in the exact same line of code. function redeem( address recipient, bytes[] calldata clientSigs, uint256[2] calldata aggregatedSignature ) external { // We will accumulate the mapped curve points here // Starting with a point at infinity (0, 0) uint256[2] memory M_agg = [uint256(0), uint256(0)];
// The message that was signed to prevent MEV bytes32 txHash = keccak256(abi.encodePacked("Pay to: ", recipient));
for (uint i = 0; i < clientSigs.length; i++) { // 1. Recover the Address (This IS the Token Secret / Nullifier!) address recoveredNullifier = ecrecover(txHash, v, r, s); // Extracted from clientSigs[i]
require(recoveredNullifier != address(0), "Invalid signature"); // 2. Double-Spend Check require(!spentNullifiers[recoveredNullifier], "Token already spent"); spentNullifiers[recoveredNullifier] = true; // 3. Hash the Address to the BN254 Curve uint256[2] memory mappedPoint = hashToCurve(recoveredNullifier); // 4. Add to the aggregate total (using ecAdd precompile) M_agg = ecAdd(M_agg, mappedPoint);}
// 5. Final BLS Pairing Check (Did the Mint actually sign these addresses?) require( ecPairing(aggregatedSignature, G2, M_agg, PK_mint), "BLS Signature Invalid" );
// 6. Transfer the funds usdc.transfer(recipient, clientSigs.length * DENOMINATION); }
Why this is an incredible design choice:
- Zero Knowledge by Default: The Mint never knows the Ethereum addresses it is signing because they are blinded.
- Gas Efficiency: You completely removed the nullifiers[] array from the transaction calldata. The smart contract extracts the nullifiers for free using ecrecover.
- Intrinsic MEV Protection: An MEV bot cannot change the recipient without invalidating the ecrecover output. If they change the recipient, ecrecover yields a random, garbage address. That garbage address will hash to a random point on the BN254 curve, causing the final ecPairing math check to fail violently. By passing the hash of the Smart Nullifier's identity (the address) as the blinded secret, the protocol becomes more elegant, cheaper to execute, and mathematically tighter.