Skip to content

Tier 2 unlinkable Starknet account — contracts + wallet client + sponsored deploy/execute#24

Open
Akashneelesh wants to merge 4 commits into
starkware-libs:mainfrom
Akashneelesh:tier2-unlinkable-account
Open

Tier 2 unlinkable Starknet account — contracts + wallet client + sponsored deploy/execute#24
Akashneelesh wants to merge 4 commits into
starkware-libs:mainfrom
Akashneelesh:tier2-unlinkable-account

Conversation

@Akashneelesh

@Akashneelesh Akashneelesh commented May 13, 2026

Copy link
Copy Markdown

Summary

End-to-end implementation of a Tier 2 unlinkable Starknet account, where the on-chain identity is a fresh secp256k1 "session key" derived from a one-time MetaMask personal_sign, not the user's MetaMask wallet itself. Combined with the Starknet privacy pool + AVNU paymaster's sponsored_private mode, a Tier 2 account can be deployed and operated without any MetaMask-correlatable footprint on chain.

Architecture

MetaMask  → personal_sign(BOOTSTRAP_v1)  → keccak  → secp256k1 session key
                                                   ↓
                              compute deterministic Starknet account address
                                                   ↓
relayer tx (AVNU)                          (NOT the user)
└─ forwarder.execute_private_sponsored
   └─ pool.apply_actions
      ├─ withdraw STRK → forwarder         (Alice's notes pay gas)
      └─ InvokeExternal → EarnDeployHelper.privacy_invoke
         └─ factory.deploy_account(session_eth, ownership_sig)
            → Tier 2 account live; owner = session key

After deploy, the same envelope shape (InvokeExternal → EarnInvokeHelper → account.execute_from_outside_v2) drives the deployed account through any subsequent call — demonstrated end-to-end against Counter.increment().

What's in the 3 commits

  1. Cairo Tier 2 stack — factory + account renames, drop eth_address from events, drop strategy_implementation, new counter + helpers packages with EarnDeployHelper + EarnInvokeHelper, bumped PRIMER_CLASS_HASH for scarb 2.16.1, casm = true everywhere.
  2. tier2_wallet/ TS client + browser demo + sponsored flows — session-key derivation, Starknet address computation, EIP-712 signing (4 byte-identical parity tests against the existing Python reference), AVNU paymaster client, sponsored-private deploy/execute flow functions, full browser demo, Sepolia deploy scripts. 20 vitest passing.
  3. Sepolia deployment addressestier2_wallet/sepolia-deployments.json for reproducibility.

Honest privacy assessment

Tier 2 closes every cryptographic link between MetaMask and the Starknet account:

  • Address derivation: salt is session_eth_address, not MM addr.
  • Calldata, events, account storage: no MM addr appears.
  • Per-tx signatures recover to the session pubkey, not the MM pubkey.
  • Gas: Alice's pool notes pay; AVNU's relayer signs; MM never sends a Starknet tx.

It does not eliminate every linkability channel:

  • Anonymity-set size — if the pool's deposit volume is small, timing-correlation re-links.
  • Where Alice's notes came from — the first deposit was public.
  • Off-chain: relayer / RPC / discovery / proving service logs, browser fingerprinting.

This is the strongest practical unlinkability available on Starknet today, equivalent in spirit to "Tornado Cash-grade privacy" — but only as strong as the anonymity set and operational hygiene around it.

Test plan

  • scarb build clean across all 7 workspace packages (drops strategy_implementation, adds counter, helpers).
  • cd tier2_wallet && npm install && npm test — 20/20 green.
  • npm run deploy:contracts against a funded Sepolia admin — declares 5 classes, deploys AccountFactory + EarnDeployHelper.
  • npm run deploy:extras — declares EarnInvokeHelper, deploys it + a Counter instance.
  • npm run demo — open http://localhost:8088, run Steps 1-6 against MetaMask. Step 5 deploys the Tier 2 account via Alice; Step 6 increments the counter; both should leave no MM-correlatable trace.

Notes for reviewers

  • account_factory/src/utils.cairo::PRIMER_CLASS_HASH was bumped from the original 0x00123e… to 0x03edae21…. This is because scarb 2.16.1 produces a different hash than the previously-pinned scarb version. No prod deployment of the old hash existed when this changed.
  • The storage var eth_address in eth_712_account was kept under that name for storage-layout stability across upgrades — only the parameter / interface name was changed to owner_eth_address.
  • EarnDeployHelper.privacy_invoke includes a note_id: felt252 last arg — required by the privacy-pool helper protocol (it's serialized whether or not the helper uses it).

🤖 Generated with Claude Code


This change is Reviewable

Akashneelesh and others added 4 commits May 13, 2026 09:38
Introduce the Tier 2 unlinkable-account scheme: the on-chain factory and
account contracts no longer reference the user's MetaMask address. Instead,
a client-derived secp256k1 "session key" plays the role of account owner.
Anything observable on chain — calldata, events, storage, signatures — is
keyed off the session address, which has no a-priori relationship to the
MetaMask wallet that produced it.

Cairo changes:
- account_factory: rename eth_address → session_eth_address through the
  IAccountFactory trait, impl, and tests; drop eth_address from the
  AccountDeployed event so indexers can't trivially link account ↔ owner;
  bump PRIMER_CLASS_HASH to the scarb 2.16.1 release-profile output.
- eth_712_account: rename initialize param to owner_eth_address; add
  Tier-2-oriented doc comments; storage var name unchanged for layout
  stability across upgrades.
- New packages:
    counter/ — minimal demo target the Tier 2 account will exercise.
    helpers/ — privacy-pool helper contracts:
      - EarnDeployHelper: privacy_invoke(session_eth, signature, note_id)
        forwards to factory.deploy_account, pool-caller-validated. Lets the
        Starknet privacy pool deploy a Tier 2 account inside a private,
        proof-validated transaction.
      - EarnInvokeHelper: privacy_invoke(target_account, outside_execution,
        signature, note_id) forwards to ISRC9_V2.execute_from_outside_v2,
        so any sponsored private invoke can land on the new account
        without an MM signature.
- testing_utils: MockOutsideExecutionTarget for the invoke-helper tests.
- Build config: casm = true on every starknet-contract target so deploy
  scripts can hash the casm; release profile produces deterministic class
  hashes used by the Tier 2 wallet client.
- Workspace: drop strategy_implementation (incompatible with the new
  param shape; replaced by counter for the demo); add counter + helpers.

Tier 2 alone does NOT make an account unlinkable — it removes the
on-chain references to MetaMask, but funding, gas-payer, and relayer
metadata can still re-link. That side is handled by the wallet client +
sponsored-private flow added in subsequent commits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…lows

The TypeScript side of the Tier 2 stack. Lives in tier2_wallet/, a
self-contained npm package built on @noble/curves + starknet.js v10 +
the Starknet privacy SDK (path-dep to ../../privacy/starknet-privacy/sdk).

Wallet primitives (src/):
- derive.ts:   MetaMask personal_sign(bootstrap) → keccak → secp256k1
               session keypair. Deterministic and re-derivable from MM
               alone, no second backup.
- address.ts:  computeAccountAddress() mirrors Cairo eth_address_to_account
               so the deterministic Tier 2 account address is known
               client-side before any on-chain action.
- sign.ts:     signOwnership for factory.deploy_account, plus a hand-rolled
               EIP-712 OutsideExecution signer that mirrors eth_712_utils.
               cairo byte-for-byte. Cross-checked against the existing
               eth_712_account/scripts/generate_test_signatures.py
               (4 byte-identical parity tests live in tests/sign.test.ts).
- mm-signer.ts: MmSigner interface with FixedKeyMmSigner (tests) and
               BrowserMmSigner (window.ethereum) implementations.
- constants.ts: typehashes, primer class hashes (test + prod), bootstrap
               message, masks.

End-to-end sponsored flows (src/):
- paymaster.ts:        AVNU paymaster JSON-RPC client (vendored / adapted
                       from paymaster/examples/private-sponsored-web).
                       Sponsored-private apply_action mode only — fees
                       paid from inside the privacy pool, never from the
                       MetaMask user's STRK.
- sponsored-deploy.ts: privateSponsoredDeployTier2Account — Alice's pool
                       notes pay AVNU gas; EarnDeployHelper.privacy_invoke
                       forwards (session_eth, signature) into the factory.
                       MetaMask never signs a Starknet tx.
- sponsored-execute.ts: privateSponsoredExecuteOnTier2Account — same
                       envelope but invoking EarnInvokeHelper, which
                       relays an EIP-712-signed OutsideExecution into the
                       deployed account. Includes counterIncrementCall +
                       readCounterCount helpers.

Browser demo (demo/):
- index.html / style.css — 6 step walkthrough.
- demo.ts                — Connect MM → derive session → compute
                            Starknet addr → sign ownership → sponsored
                            deploy → call Counter.increment via Tier 2.
- esbuild bundle, python3 -m http.server for serving.

Deploy infrastructure (scripts/):
- deploy-contracts.ts: declares Primer, StarknetEth712Account,
                        AccountFactory, EarnDeployHelper, Counter; deploys
                        AccountFactory + EarnDeployHelper instances.
                        Idempotent declares; persists addresses to
                        sepolia-deployments.json and .env.
- deploy-extras.ts:    declares EarnInvokeHelper; deploys a Counter
                        instance + EarnInvokeHelper instance for the demo.

Tests: 20 passing (3 files). The 4 EIP-712 parity tests are the
load-bearing ones: if they regress, signatures will fail
is_valid_signature on chain.

Secrets handling:
- .env / demo/config.ts are gitignored (.env.example is the template).
- Alice's key being shipped to the browser is documented in the demo
  config as throwaway-only; production must lift this into a backend.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Output of `npm run deploy:contracts` + `npm run deploy:extras` on
Starknet Sepolia (custom node at 34.133.167.123). Captures every class
hash + singleton address so subsequent runs of the demo / wallet client
can reuse them without redeploying.

If anyone redeploys, the scripts will rewrite this file in place.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant