Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Social Recovery SDK

The Social Recovery SDK is a composable stack for adding social recovery to smart wallets.

It combines:

  • On-chain recovery contracts (contracts/)
  • A TypeScript SDK for orchestration and proof generation (sdk/)
  • A Noir circuit for privacy-preserving email guardians (circuits/zkjwt/)

What this project solves

If a wallet owner loses access to their key, designated guardians can recover ownership through a controlled process:

  1. A guardian starts recovery with a valid proof.
  2. Additional guardians submit proofs until threshold is met.
  3. A challenge period allows the owner to cancel unauthorized recovery.
  4. Recovery executes on-chain and ownership is updated.

Supported guardian types

  • EOA signatures (EIP-712)
  • Passkeys (WebAuthn / P-256)
  • zkJWT commitments (Google JWT + Noir proof)

Where to start

Getting Started

Prerequisites

  • Node.js 20+
  • npm
  • Foundry (forge, cast)
  • Noir toolchain (nargo, bb) for circuit work
  • Anvil for local end-to-end testing

Repository quick check

From the project root:

# Contracts tests
cd contracts && forge test --offline

# SDK unit/integration tests
cd ../sdk && npm install && npm run build && npm test

# Full SDK <-> contracts e2e
npm run test:e2e

# Circuit tests
cd ../circuits/zkjwt && nargo test

Key entry points

  • Spec: SPEC.md
  • Architecture map: ARCHITECTURE.md
  • SDK package: sdk/src/index.ts
  • Recovery contracts: contracts/src/RecoveryManager.sol

Typical integration path

  1. Deploy shared verifiers, RecoveryManager implementation, and factory.
  2. In your wallet flow, deploy a per-wallet RecoveryManager via factory.
  3. Authorize that RecoveryManager inside the wallet.
  4. Use SDK clients/adapters to run start/submit/execute/cancel flows.
  5. Add monitoring for recovery events and owner-side cancellation UX.

Core Concepts

Guardian

A guardian is an identity that can approve recovery. Each guardian has:

  • guardianType (EOA, Passkey, ZkJWT)
  • identifier (bytes32)

Identifier formats:

  • EOA: bytes32(uint256(uint160(address)))
  • Passkey: keccak256(pubKeyX || pubKeyY)
  • ZkJWT: Poseidon2(email_hash, salt)

Recovery policy

A policy is configured per wallet and includes:

  • guardians[]
  • threshold (N of M)
  • challengePeriod (seconds)

Recovery intent

Guardians sign/prove over an EIP-712 RecoveryIntent:

  • wallet
  • newOwner
  • nonce
  • deadline
  • chainId
  • recoveryManager

This prevents replay across nonce/chain/contract and binds proofs to a specific ownership change.

Recovery session

Only one active session per wallet is allowed. Session state tracks:

  • intentHash
  • newOwner
  • deadline
  • thresholdMetAt
  • approvalCount

Nonce and replay safety

nonce increments when a session is completed/canceled/cleared and when policy updates occur. Old proofs become invalid.

Challenge period

After threshold is met, execution is delayed by challengePeriod. This gives the owner time to cancel malicious recovery.

System Architecture

High-level flow

Wallet (owner + authorized RecoveryManager)
  -> RecoveryManager (per wallet instance)
     -> Verifier path by guardian type
        - EOA: ecrecover
        - Passkey: PasskeyVerifier
        - zkJWT: ZkJwtVerifier -> HonkVerifier

On-chain components

  • RecoveryManager.sol
    • Policy storage
    • Session lifecycle (startRecovery, submitProof, executeRecovery, cancelRecovery, clearExpiredRecovery)
  • RecoveryManagerFactory.sol
    • Deploys per-wallet RecoveryManager proxies
  • verifiers/PasskeyVerifier.sol
    • Verifies WebAuthn/P-256 proof payloads
  • verifiers/ZkJwtVerifier.sol
    • Verifies Noir proofs and binds public inputs to intent + guardian commitment

Off-chain SDK components

  • recovery/RecoveryClient.ts
    • Deployment helpers, recovery tx orchestration, readiness checks
  • recovery/PolicyBuilder.ts
    • Fluent builder + validation for policy config
  • auth/AuthManager.ts
    • Adapter registry and proof generation delegation
  • auth/adapters/*
    • Guardian-specific proof generation (EOA, passkey, zkJWT)

Circuit component

  • circuits/zkjwt/src/main.nr
    • JWT verification and zk commitment constraints
  • circuits/zkjwt/scripts/
    • Prover input generation utilities and fixtures

Recovery Lifecycle

1. Setup

  1. Choose guardian set and threshold.
  2. Deploy RecoveryManager for the wallet via factory.
  3. Authorize RecoveryManager in wallet logic.

2. Start recovery

A guardian calls startRecovery(intent, guardianIndex, proof).

Key constraints enforced by contract:

  • No active session already
  • Intent fields match wallet/nonce/chain/contract
  • intent.deadline > block.timestamp + challengePeriod
  • Guardian index exists and proof verifies

3. Submit additional proofs

Other guardians call submitProof(guardianIndex, proof).

  • Duplicate approvals are rejected.
  • When approvals reach threshold, thresholdMetAt is set.

4. Challenge period window

Until execution:

  • Owner can call cancelRecovery().
  • If deadline passes, anyone can call clearExpiredRecovery().

5. Execute recovery

After challenge period and before deadline, anyone can call executeRecovery().

  • RecoveryManager calls wallet setOwner(newOwner).
  • Session is cleared and nonce increments.

Session reset cases

Session state is reset and nonce increments on:

  • executeRecovery()
  • cancelRecovery()
  • clearExpiredRecovery()
  • updatePolicy()

Authentication Methods

The system supports three guardian authentication modes.

TypeIdentifier on-chainProof sourcePrivacy
EOAPadded addressEIP-712 signatureAddress revealed
Passkey`keccak256(pubKeyXpubKeyY)`
zkJWTPoseidon2(email_hash, salt)Noir proof from JWTEmail hidden

All methods are bound to the same RecoveryIntent to prevent replay.

Choosing a mix

  • EOA: easiest operationally, lowest complexity.
  • Passkey: strong UX for non-crypto users, browser/WebAuthn requirements.
  • zkJWT: strongest privacy, highest proving/tooling complexity.

Most production policies use mixed guardian types for resilience.

EOA Guardians

EOA guardians sign the EIP-712 RecoveryIntent.

Identifier

Computed as left-padded address:

identifier = bytes32(uint256(uint160(address)))

In SDK this is EoaAdapter.computeIdentifier(address).

Proof generation

EoaAdapter.generateProof(intent, guardianIdentifier):

  1. Checks adapter wallet address matches guardianIdentifier
  2. Signs typed data using walletClient.signTypedData
  3. Encodes (v, r, s) for contract submission

On-chain verification

RecoveryManager decodes proof as (uint8 v, bytes32 r, bytes32 s) and verifies signer via ecrecover.

Passkey Guardians

Passkey guardians prove control of a WebAuthn credential (P-256 key).

Identifier

identifier = keccak256(abi.encodePacked(pubKeyX, pubKeyY))

PasskeyAdapter.computeIdentifier(publicKey) computes this.

Proof generation

PasskeyAdapter.generateProof(intent, guardianIdentifier):

  1. Validates identifier matches configured public key.
  2. Uses hashRecoveryIntent(intent) as WebAuthn challenge.
  3. Requests assertion (navigator.credentials.get).
  4. Parses DER signature and encodes proof payload expected by PasskeyVerifier.

Runtime dependency

Passkey verification depends on the deterministic p256-verifier deployment at:

0xc2b78104907F722DABAc4C69f826a522B2754De4

RecoveryClient checks code exists at this address before sending passkey proof txs.

If missing, passkey start/submit operations fail early with a descriptive error.

zkJWT Guardians

zkJWT guardians use an email-based commitment and zero-knowledge proof.

Identifier

email_hash = Poseidon2(packed_email_fields, email_len)
identifier = Poseidon2(email_hash, salt)

This identifier is the on-chain guardian value.

Proof generation in SDK

ZkJwtAdapter.generateProof(intent, guardianIdentifier):

  1. Decodes JWT and reads email
  2. Recomputes commitment and checks it equals guardianIdentifier
  3. Resolves JWT signing key (injected JWK or fetched Google JWKS)
  4. Computes intent_hash = hashRecoveryIntent(intent) % BN254_SCALAR_FIELD_MODULUS
  5. Generates Noir/UltraHonk proof
  6. Encodes (rawProof, bytes32[18] modulusLimbs) for ZkJwtVerifier

Circuit/public input binding

On-chain verifier expects public inputs in this order:

  • [0..17] RSA modulus limbs
  • [18] reduced intent hash
  • [19] guardian commitment

Claim handling notes

  • Circuit enforces email_verified == true.
  • Commitment is intentionally time-independent.
  • Current circuit design does not enforce JWT exp.
  • Input generator now rejects expired/unverified Google tokens by default; use --allow-insecure-claims only for explicit debug workflows.

SDK Guide

The SDK is the integration surface for applications and wallet frontends.

Primary exports:

  • RecoveryClient - contract orchestration
  • PolicyBuilder - policy construction + validation
  • AuthManager - adapter registry
  • EoaAdapter, PasskeyAdapter, ZkJwtAdapter - proof generation
  • createRecoveryIntent, hashRecoveryIntent - EIP-712 helpers
  1. Build/deploy policy with owner client.
  2. Create RecoveryIntent from on-chain nonce and challenge period.
  3. Have guardian-specific clients generate proofs.
  4. Call startRecovery/submitProof from guardian wallets.
  5. Execute after challenge period.

SDK Quickstart

1. Install and import

import {
  RecoveryClient,
  PolicyBuilder,
  EoaAdapter,
  createRecoveryIntent,
} from '@pse/social-recovery-sdk';

2. Deploy per-wallet RecoveryManager

const ownerClient = new RecoveryClient({
  publicClient,
  walletClient: ownerWalletClient,
  factoryAddress,
});

const policy = new PolicyBuilder()
  .setWallet(walletAddress)
  .addEoaGuardian(guardian1Address)
  .addEoaGuardian(guardian2Address)
  .setThreshold(2)
  .setChallengePeriod(600)
  .build();

const recoveryManagerAddress = await ownerClient.deployRecoveryManager(policy);

3. Authorize RecoveryManager in wallet

Your wallet contract must authorize this recovery manager to call setOwner.

4. Create intent from live chain state

const guardianClient = new RecoveryClient({
  publicClient,
  walletClient: guardianWalletClient,
  recoveryManagerAddress,
});

const nonce = await guardianClient.getNonce();
const chainId = BigInt(await publicClient.getChainId());
const challengePeriod = (await guardianClient.getPolicy()).challengePeriod;

const intent = createRecoveryIntent({
  wallet: walletAddress,
  newOwner: proposedOwner,
  recoveryManager: recoveryManagerAddress,
  nonce,
  chainId,
  challengePeriodSeconds: challengePeriod,
});

challengePeriodSeconds keeps deadline generation compatible with on-chain startRecovery validation.

5. Start recovery with guardian proof

const adapter = new EoaAdapter({ walletClient: guardianWalletClient });
const guardianIdentifier = adapter.computeIdentifier(guardianWalletClient.account!.address);
const proofResult = await adapter.generateProof(intent, guardianIdentifier);
if (!proofResult.success) throw new Error(proofResult.error);

await guardianClient.startRecovery({
  intent,
  guardianIndex: 0n,
  proof: proofResult.proof!,
});

6. Submit more proofs and execute

  • Additional guardians call submitProof.
  • After threshold + challenge period, call executeRecovery.
const ready = await guardianClient.isReadyToExecute();
if (ready) {
  await guardianClient.executeRecovery();
}

SDK API Reference

RecoveryClient

Configuration:

  • publicClient (required)
  • walletClient (required for writes)
  • factoryAddress (for deployments)
  • recoveryManagerAddress (for recovery flows)

Deployment:

  • deployRecoveryManager(policy): Promise<Address>

Recovery tx methods:

  • startRecovery({ intent, guardianIndex, proof })
  • submitProof({ guardianIndex, proof })
  • executeRecovery()
  • cancelRecovery()
  • clearExpiredRecovery()
  • updatePolicy({ guardians, threshold, challengePeriod })

Read/query methods:

  • getSession()
  • isRecoveryActive()
  • getPolicy()
  • getNonce()
  • isReadyToExecute()

Utility:

  • setRecoveryManager(address)
  • getAuthManager()

Validation behaviors in startRecovery

Before tx submission, SDK checks:

  • Intent addresses are non-zero
  • intent.recoveryManager equals client RecoveryManager
  • intent.deadline > now + challengePeriod
  • For passkey proofs: required P-256 verifier bytecode exists

PolicyBuilder

  • setWallet(address)
  • addEoaGuardian(address)
  • addPasskeyGuardian({x,y})
  • addZkJwtGuardian(commitment)
  • setThreshold(number|bigint)
  • setChallengePeriod(number|bigint)
  • build(): RecoveryPolicy

build() rejects invalid policies (zero wallet, zero guardians, zero threshold, threshold > guardian count, duplicate/zero identifiers).

AuthManager

  • registerAdapter(adapter)
  • generateProof(guardianType, intent, guardianIdentifier)
  • computeIdentifier(guardianType, credentials)
  • getAdapter(...), hasAdapter(...)

Adapters

  • EoaAdapter
  • PasskeyAdapter
  • ZkJwtAdapter

All adapters implement:

  • computeIdentifier(credentials)
  • generateProof(intent, guardianIdentifier)

EIP-712 helpers

  • hashRecoveryIntent(intent)
  • createRecoveryIntent(params)
  • isValidIntent(intent, options)

createRecoveryIntent supports challengePeriodSeconds to enforce safe deadlines.

Contracts Guide

Main contracts

  • RecoveryManager.sol - per-wallet policy/session state and execution logic
  • RecoveryManagerFactory.sol - deploys minimal proxy instances
  • verifiers/PasskeyVerifier.sol - passkey proof verification
  • verifiers/ZkJwtVerifier.sol - zk proof verification wrapper around Honk verifier

Wallet-facing integration contract

Wallets integrate by implementing IWallet:

  • owner()
  • setOwner(address)
  • isRecoveryAuthorized(address)

RecoveryManager.executeRecovery() calls wallet setOwner(newOwner).

Lifecycle-critical invariants

  • One active session per wallet
  • Nonce replay protection
  • Threshold + challenge period gating
  • Deadline expiry prevents execution and further approvals
  • Owner-only cancellation

Deployment Guide

Deployment order

  1. PasskeyVerifier
  2. ZKTranscriptLib (for Honk verifier linking)
  3. HonkVerifier
  4. ZkJwtVerifier
  5. RecoveryManager implementation
  6. RecoveryManagerFactory

The provided script contracts/scripts/deploy.sh performs this order.

Required environment

RPC_URL=...
PRIVATE_KEY=...
ETHERSCAN_API_KEY=...
CHAIN=sepolia

P-256 dependency check

Passkey verification depends on deterministic p256-verifier deployment at:

0xc2b78104907F722DABAc4C69f826a522B2754De4

The deploy script checks this address and fails fast if bytecode is missing.

Build profile

For large verifier artifacts (notably Honk verifier), use deploy profile where needed:

FOUNDRY_PROFILE=deploy forge build

Wallet Integration

This chapter describes the minimum contract and product integration surface for wallets.

Contract requirements

Your wallet must support:

  1. Owner state (owner())
  2. Authorized owner mutation (setOwner(newOwner))
  3. Authorization check (isRecoveryAuthorized(account))

In practice, wallets also expose owner-only methods to authorize/revoke recovery managers.

Minimal Solidity shape

interface IWallet {
    function owner() external view returns (address);
    function setOwner(address newOwner) external;
    function isRecoveryAuthorized(address account) external view returns (bool);
}

contract WalletLike is IWallet {
    address public owner;
    mapping(address => bool) private recoveryAuthorized;

    function setOwner(address newOwner) external {
        require(msg.sender == owner || recoveryAuthorized[msg.sender], "not authorized");
        require(newOwner != address(0), "zero owner");
        owner = newOwner;
    }

    function authorizeRecoveryManager(address rm) external {
        require(msg.sender == owner, "only owner");
        recoveryAuthorized[rm] = true;
    }

    function revokeRecoveryManager(address rm) external {
        require(msg.sender == owner, "only owner");
        delete recoveryAuthorized[rm];
    }

    function isRecoveryAuthorized(address account) external view returns (bool) {
        return recoveryAuthorized[account];
    }
}

Minimal integration pattern

  1. Deploy wallet contract.
  2. Deploy wallet-specific RecoveryManager via factory.
  3. Authorize that RecoveryManager in wallet state.
  4. Expose UX for guardian setup, policy updates, and recovery monitoring.

Execution semantics

RecoveryManager does not need to be msg.sender == owner; it only needs wallet-level authorization to call setOwner.

  • Recovery setup wizard (guardian list + threshold + challenge period)
  • Event monitoring (show active sessions and countdown)
  • Owner cancellation action while session is active
  • Post-recovery key import flow for new owner

Integration checklist

  • Wallet implements IWallet compatibility
  • RecoveryManager authorization path exists
  • Challenge period and deadline displayed clearly in UI
  • Owner notification path on RecoveryStarted
  • Cancel flow tested end-to-end
  • Passkey dependency present if passkey guardians enabled

Security Model

Core protections

  • Replay protection via nonce, chainId, and recoveryManager in intent
  • Session exclusivity (one active recovery at a time)
  • Threshold approval requirement
  • Challenge period delay before execution
  • Owner cancellation during active sessions

Threat model highlights

  • Guardian collusion remains an economic/social risk by design; mitigate with threshold and trusted guardian selection.
  • Single guardian compromise is insufficient when threshold > 1.
  • Deadline-based session clearing avoids deadlocks from stale sessions.

Operational recommendations

  • Use non-zero challenge periods in production.
  • Monitor RecoveryStarted, ThresholdMet, RecoveryCancelled, RecoveryExecuted events.
  • Keep guardian metadata and off-chain coordination channels secure.
  • Audit custom wallet authorization logic around setOwner.

zkJWT-specific notes

  • On-chain guardian identifier is commitment only.
  • Salt secrecy affects unlinkability/privacy properties.
  • Proof correctness depends on circuit/verifier artifact consistency.

Testing Guide

Contracts

cd contracts
forge test --offline

If your Foundry environment is stable without external signature lookup issues, forge test is also fine.

SDK unit/integration

cd sdk
npm test

SDK end-to-end

cd sdk
npm run test:e2e

This script runs local Anvil + contract deploy + full flow tests.

Circuit tests

cd circuits/zkjwt
nargo test

Suggested CI order

  1. Contracts tests
  2. SDK build + unit/integration tests
  3. Circuit tests
  4. SDK e2e tests

Troubleshooting

Recovery intent is invalid

Common causes:

  • intent.recoveryManager does not match target RecoveryManager address
  • deadline <= now + challengePeriod
  • stale nonce
  • zero address fields

Fix:

  • Re-read nonce/policy from chain and recreate intent with challengePeriodSeconds.

Passkey flow error: missing P-256 verifier bytecode

Cause:

  • No contract code at 0xc2b78104907F722DABAc4C69f826a522B2754De4.

Fix:

  • Deploy deterministic p256-verifier dependency for the network.

WalletClient required for write operations

Cause:

  • RecoveryClient created without walletClient but write method called.

Fix:

  • Provide signer-enabled client for tx submission paths.

WebAuthn not available in tests

Cause:

  • Node test environment has no browser WebAuthn APIs.

Fix:

  • Use mocked tests for adapter logic and run browser-integrated tests separately.

nargo cache lock / permission issues

Cause:

  • Local environment permissions around Noir cache/dependency directories.

Fix:

  • Ensure writable cache/home paths for nargo and rerun.

FAQ

Does recovery move assets?

No. Recovery changes wallet ownership/authority only.

Can anyone execute recovery?

Yes, but only after threshold is met and challenge period elapsed, and before deadline.

Can owner stop a malicious recovery?

Yes. Owner can call cancelRecovery() while session is active.

What happens if a session expires?

Anyone can call clearExpiredRecovery() to remove stale session and allow new attempts.

Are zkJWT guardian emails revealed on-chain?

No. Only commitment and zero-knowledge proof verification data are used.

Is passkey support plug-and-play on every chain?

Not automatically. The deterministic p256-verifier dependency must exist on the target network.