Skip to main content

Architecture

The wallet consists of a browser extension that is the signing authority, and wallet.quantum.systems which connects to the extension via EIP-6963 when present. The extension holds the encrypted vault and performs all signing; the web app acts as a dApp interface to it.

Key storage and lifetimes

Two layers of storage, with explicit lifetime rules for every kind of key material.

┌─────────────────────────────────────────────────┐
│ Persistent storage (disk) │
│ ┌─────────────────────────────────────────────┐ │
│ │ Vault (AES-256-GCM encrypted) │ │
│ │ { encryptedSeed, salt, iv, iterations, │ │
│ │ kdf: "pbkdf2", version: 1, authMethod } │ │
│ └─────────────────────────────────────────────┘ │
│ Never contains plaintext key material. │
└─────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────┐
│ Ephemeral session storage (memory-only) │
│ ┌─────────────────────────────────────────────┐ │
│ │ VMK (Vault Master Key) │ │
│ │ Derived from password via PBKDF2. │ │
│ │ Used to decrypt seed on-demand for signing.│ │
│ └─────────────────────────────────────────────┘ │
│ Cleared when browser closes. │
└─────────────────────────────────────────────────┘

Unlock flow:

  1. User enters password → PBKDF2-HMAC-SHA256 (100,000 iterations) → VMK.
  2. VMK stored in session storage (cleared on browser close).
  3. The wallet is now "unlocked" — the VMK is available for signing.

Signing flow (per-transaction):

  1. Read VMK from session → decrypt seed from vault → derive ML-DSA-44 key.
  2. Sign the UserOp hash.
  3. Immediately zero the seed bytes and private key bytes.
  4. Return the signature only.

Auto-lock:

  • Configurable timer (1 / 5 / 15 / 30 minutes of inactivity).
  • On trigger: clear VMK from session storage, set vault state to locked, emit accountsChanged([]) to connected dApps.

Key material lifetime rules:

  • Encrypted seed: persisted in storage (always encrypted).
  • VMK: in session storage only while unlocked.
  • Decrypted seed: in memory only during the signing operation (milliseconds).
  • ML-DSA private key bytes: in memory only during the signing operation (milliseconds).
  • After signing: explicit zeroing of key bytes. Do not rely on garbage collection.

Bundler and contracts

  • Production bundler: Pimlico (hosted). Supports Arbitrum Sepolia and mainnet, EntryPoint v0.7, and has been tested with 3,309-byte composite signatures.
  • Contracts: Safe7579, PQValidatorModule, VerifierGateway. Deployed via CREATE2 with deterministic salts so addresses match across chains.

See Deployed addresses for current deployments.

Chain rollout criteria

A chain is enabled only when all six criteria are met:

#CriterionVerification
1ML-DSA-65 verifier deployedSource verification on block explorer
2Gas validated (ML-DSA-65 verification < 30M on target chain)Fork test against target chain
3Safe infra + PQValidatorModule deployed at deterministic CREATE2 addresseseth_getCode at expected addresses
4Bundler accepts UserOps with 3,309-byte composite signaturesSubmit test UserOp
5Testnet E2E validated (deploy account → send ETH → ERC-20 → co-signer)Automated test suite
6Block explorer available for transaction receipts and UserOp tracesManual verification

Optional but recommended:

  • P256 precompile check — if RIP-7212 is native, use it for passkey co-signing; otherwise fall back to Solidity P256 verification.
  • Paymaster available — improves UX, not required.

A chain is added to the registry only after all six criteria pass and engineering and product sign off.