Skip to content

Instantly share code, notes, and snippets.

@IvanAnishchuk
Created March 13, 2026 23:49
Show Gist options
  • Select an option

  • Save IvanAnishchuk/35acbb3aca3e06021a2eb89651dac57b to your computer and use it in GitHub Desktop.

Select an option

Save IvanAnishchuk/35acbb3aca3e06021a2eb89651dac57b to your computer and use it in GitHub Desktop.
ecash-arch-v0.1.1.md

You are completely right, and I appreciate the reality check. It is a strict constant-factor reduction ($65N$ bytes instead of $97N$ bytes), not a change in the asymptotic complexity. The calldata size is still fundamentally $O(N)$ with respect to the number of tokens being redeemed. It's a neat trick for shaving off gas costs, but calling it a "massive elimination" overhypes a standard EVM optimization.

Let's strip out the marketing speak, ground the engineering claims in reality, and finalize this architecture.

Here is the complete, sober, and technically precise Master Document, incorporating the standard Stealth Address terminology, the encrypted delivery channel, and the correct mathematical constraints.


Native Android Privacy-Preserving Ethereum Wallet & Ghost-Tip Protocol Master Plan

This document serves as the self-contained engineering blueprint and R&D output for building a privacy-first, native Android Web3 wallet. It details the implementation plan for the Ghost-Tip Protocol: a stateless, untraceable eCash system for creator micropayments on the EVM.


Part 1: Android Web3 Wallet Architecture

Building privacy-focused financial tools on Android requires reconciling high-performance cryptography with mobile OS constraints.

Core Connectivity & SDK Layer

To handle standard EVM operations alongside zero-knowledge cryptography without drastically bloating the APK:

  • Modular Kotlin-First (KEthereum): Prioritizes a Kotlin-native implementation, avoiding heavy Java JVM dependencies for standard RPC interactions and transaction building.
  • High-Performance JNI (Trust Wallet Core / libsodium): A robust, C++ based cross-blockchain library accessed via Kotlin/JNI bindings. Essential for fast execution of elliptic curve operations (secp256k1) and standard AES-GCM encryption for the stealth routing channel.

Deterministic State Management

Ghost-Tip wallets are entirely stateless. By utilizing a Hierarchical Deterministic (HD) derivation path (via kotlin-bip39), the wallet generates all eCash secrets deterministically from the user's Master Seed and a sequential token_index. If a user loses their device, the entire wallet state is recovered by regenerating the keys and parsing the blockchain's event history.


Part 2: The Ghost-Tip Protocol Architecture

Ghost-Tip is a privacy-first micropayment protocol that utilizes Chaumian blind signatures over the BN254 curve for transaction anonymity, and standard ECDSA signatures to mathematically prevent MEV front-running on public mempools.

The Dual-Key "Address-as-Secret" Concept

In standard eCash, a token is a random string (the secret) and a blind signature. Ghost-Tip replaces the random string with an active cryptographic identity built on the ERC-5564 Stealth Address dual-key model. For every token, the client derives two distinct secp256k1 keypairs:

  1. The Spend Keypair: The Ethereum Address of this keypair serves as the core token secret and the on-chain nullifier. By signing the redemption payload ("Pay to: 0xDestination") with the Spend Private Key, the user proves ownership and binds the transaction to the destination, defeating MEV bots.
  2. The View Keypair: Used strictly to establish a secure, unlinkable on-chain communication channel with the Mint to receive the blind signature.

Protocol Step-by-Step Flow

Phase 1: Deterministic Derivation & Deposit (User $\rightarrow$ Smart Contract)

The user wants to mint token #42. The client uses the Master Seed to deterministically generate three elements:

  1. The Spend Keypair (secp256k1): Its Ethereum Address ($A$) is the identity of the token.
  2. The Blinding Factor ($r$): A 32-byte scalar used to mask the token during minting.
  3. The View Keypair ($V_{pub}, V_{priv}$): A secp256k1 keypair used to securely receive the Mint's signature.

Execution:

  1. The client maps Address $A$ to the BN254 curve: $Y = H_G(A)$.
  2. The client blinds $Y$ using $r$: $B' = Y + rG$.
  3. The client calls deposit() on the Vault contract, locking 1 USDC and passing $B'$ and $V_{pub}$ in the calldata.
  4. The Vault emits a DepositLocked(B', V_{pub}) event.

Phase 2: Encrypted On-Chain Delivery (Mint $\rightarrow$ Smart Contract)

The Mint is a stateless server monitoring the blockchain. It holds a static identity keypair (secp256k1) and the protocol's BN254 private key ($sk$).

  1. Blind Signing: The Mint multiplies the blinded point $B'$ by its private key: $C' = sk \times B'$.
  2. ECDH Key Exchange: The Mint reads $V_{pub}$ from the event. It uses its static private key to perform an Elliptic Curve Diffie-Hellman exchange: SharedSecret = ECDH(Mint_Priv, V_{pub}).
  3. Encryption: The Mint encrypts $C'$ using AES-256-GCM keyed with the SharedSecret.
  4. On-Chain Delivery: The Mint submits a transaction to the Vault to fulfill the request. The Vault emits a MintFulfilled(V_{pub}, Encrypted_C') event.

Phase 3: Discovery & Stateless Unblinding (Client-Side)

The user recovers their tokens statelessly by scanning the blockchain history.

  1. The client recalculates $V_{pub}$ for historical indices (e.g., 0 to 50) and scans event logs for matching MintFulfilled events.
  2. Upon finding a match, the client recalculates the SharedSecret using $V_{priv}$ and the Mint's known public key, decrypting the envelope to reveal $C'$.
  3. The client recalculates $r$ and unblinds the signature locally: $C = C' - r(sk \times G)$.
  4. The user now holds a valid, unlinkable eCash token: (SpendPrivateKey, C).

Phase 4: On-Chain Redemption

The user wants to withdraw 5 tokens to a public address (0xDestination).

  1. BLS Aggregation: The client mathematically sums the 5 BN254 signature points: $\sigma_{agg} = \sum C_i$.
  2. MEV Locking: For each of the 5 tokens, the client uses the respective SpendPrivateKey to sign the payload "Pay to: 0xDestination".
  3. The client submits the 5 ECDSA signatures and the 1 aggregated BLS signature ($\sigma_{agg}$) to the Vault contract.

Part 3: Engineering Master Plan & Code Implementation

Milestone 1: The Smart Contract Vault (Solidity)

The Vault contract verifies the MEV protection and dynamically recovers the secret "nullifier" via ecrecover.

Optimization Note: While the calldata size still scales $O(N)$ with the number of tokens due to the 65-byte ECDSA signatures, omitting the explicit array of addresses saves a constant 32 bytes per token.

pragma solidity ^0.8.19;

contract GhostTipVault {
    mapping(address => bool) public spentNullifiers;
    
    // Hardcoded Public Key of the Mint on BN254 G2
    uint256[4] public PK_mint; 

    function redeem(
        address recipient,
        bytes[] calldata clientSigs,
        uint256[2] calldata aggregatedSignature
    ) external {
        uint256[2] memory M_agg = [uint256(0), uint256(0)];
        bytes32 txHash = keccak256(abi.encodePacked("Pay to: ", recipient));

        for (uint i = 0; i < clientSigs.length; i++) {
            // 1. ecrecover the Address (This is the Spend Address & the Nullifier)
            address recoveredNullifier = recoverSigner(txHash, clientSigs[i]);
            require(recoveredNullifier != address(0), "Invalid ECDSA signature");

            // 2. Strict 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. Aggregate the points using EVM ecAdd precompile
            M_agg = ecAdd(M_agg, mappedPoint);
        }

        // 5. Final BLS Pairing Check (ecPairing precompile)
        require(
            ecPairing(aggregatedSignature, G2, M_agg, PK_mint), 
            "BLS Blind Signature Invalid"
        );

        // 6. Dispense funds
        usdc.transfer(recipient, clientSigs.length * 1e6); // Assuming $1 per token
    }
}

Milestone 2: The Client Wallet SDK (Python Conceptual)

The local client handles the deterministic derivation of the Spend Keypair, the View Keypair, and the scalar blinding factor.

from eth_keys import keys
from eth_utils import keccak
import os
# Mocking a BN254 library wrapper (e.g., mapping to mcl-wasm or py_ecc)
from mock_bn254 import G1, multiply, add, subtract, hash_to_curve
from mock_crypto import ecdh, aes_gcm_decrypt

class GhostTipClient:
    def __init__(self, master_seed: bytes):
        self.master_seed = master_seed
        self.mint_pub_key_g1 = fetch_mint_pub_key_g1()
        self.mint_identity_pub = fetch_mint_identity_pub() # secp256k1
    
    def derive_token_data(self, token_index: int):
        """Derives the Spend Key, View Key, and Scalar Blinding Factor."""
        base_material = keccak(self.master_seed + token_index.to_bytes(4, 'big'))
        
        # 1. Spend Keypair (Token Identity / Nullifier)
        spend_priv = keys.PrivateKey(keccak(b"spend" + base_material))
        spend_address = bytes.fromhex(spend_priv.public_key.to_address()[2:])
        
        # 2. View Keypair (Stealth Return Channel)
        view_priv = keys.PrivateKey(keccak(b"view" + base_material))
        view_pub = view_priv.public_key.to_bytes()
        
        # 3. Blinding Factor
        r = int.from_bytes(keccak(b"blind" + base_material), 'big') % BN254_ORDER
        
        return spend_priv, spend_address, view_priv, view_pub, r

    def prepare_deposit(self, token_index: int) -> tuple:
        _, spend_address, _, view_pub, r = self.derive_token_data(token_index)
        
        # Y = H_G(SpendAddress)
        Y = hash_to_curve(spend_address)
        
        # B' = Y + rG
        blinded_point_B_prime = add(Y, multiply(G1, r))
        
        # Sent to smart contract calldata
        return blinded_point_B_prime, view_pub 

    def decrypt_and_unblind_token(self, token_index: int, encrypted_envelope: bytes):
        spend_priv, _, view_priv, _, r = self.derive_token_data(token_index)
        
        # 1. Reconstruct Shared Secret & Decrypt using View Key
        shared_secret = ecdh(view_priv, self.mint_identity_pub)
        blind_sig_C_prime = aes_gcm_decrypt(shared_secret, encrypted_envelope)
        
        # 2. C = C' - r(MintPubKey_G1)
        r_pk = multiply(self.mint_pub_key_g1, r)
        unblinded_sig_C = subtract(blind_sig_C_prime, r_pk)
        
        return spend_priv, unblinded_sig_C

Milestone 3: The Off-Chain Mint (Python/FastAPI)

The mint encrypts its response against the user's provided View Public Key, ensuring that observers cannot link the mint's output ($C'$) back to the unblinded token ($C$) submitted during withdrawal.

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from mock_bn254 import multiply
from mock_crypto import ecdh, aes_gcm_encrypt

app = FastAPI()
MINT_BN254_PRIVATE_KEY = load_secure_mint_key() 
MINT_IDENTITY_PRIVATE_KEY = load_secp256k1_identity_key() 

class MintFulfillmentRequest(BaseModel):
    deposit_tx_hash: str
    blinded_point: tuple # B' from the smart contract event
    view_pub: bytes      # V_pub from the smart contract event

@app.post("/fulfill")
async def fulfill_mint(req: MintFulfillmentRequest):
    # 1. Verify deposit logic on-chain
    if not await verify_on_chain_deposit(req.deposit_tx_hash, req.blinded_point):
        raise HTTPException(status_code=400, detail="Invalid deposit")
    
    # 2. C' = sk * B'
    blind_signature = multiply(req.blinded_point, MINT_BN254_PRIVATE_KEY)
    
    # 3. ECDH Encryption against the user's View Key
    shared_secret = ecdh(MINT_IDENTITY_PRIVATE_KEY, req.view_pub)
    encrypted_envelope = aes_gcm_encrypt(shared_secret, blind_signature)
    
    # 4. Broadcast the encrypted envelope back to the smart contract
    await broadcast_fulfillment_to_chain(req.view_pub, encrypted_envelope)
    
    return {"status": "Success, encrypted signature broadcasted"}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment