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:
- A guardian starts recovery with a valid proof.
- Additional guardians submit proofs until threshold is met.
- A challenge period allows the owner to cancel unauthorized recovery.
- 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
- New to the project: Getting Started
- Integrating a wallet: Wallet Integration
- Using the SDK right away: SDK Quickstart
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
- Deploy shared verifiers, RecoveryManager implementation, and factory.
- In your wallet flow, deploy a per-wallet RecoveryManager via factory.
- Authorize that RecoveryManager inside the wallet.
- Use SDK clients/adapters to run start/submit/execute/cancel flows.
- 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(NofM)challengePeriod(seconds)
Recovery intent
Guardians sign/prove over an EIP-712 RecoveryIntent:
walletnewOwnernoncedeadlinechainIdrecoveryManager
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:
intentHashnewOwnerdeadlinethresholdMetAtapprovalCount
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
- Choose guardian set and threshold.
- Deploy RecoveryManager for the wallet via factory.
- 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,
thresholdMetAtis 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.
| Type | Identifier on-chain | Proof source | Privacy |
|---|---|---|---|
| EOA | Padded address | EIP-712 signature | Address revealed |
| Passkey | `keccak256(pubKeyX | pubKeyY)` | |
| zkJWT | Poseidon2(email_hash, salt) | Noir proof from JWT | Email 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):
- Checks adapter wallet address matches
guardianIdentifier - Signs typed data using
walletClient.signTypedData - 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):
- Validates identifier matches configured public key.
- Uses
hashRecoveryIntent(intent)as WebAuthn challenge. - Requests assertion (
navigator.credentials.get). - 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):
- Decodes JWT and reads
email - Recomputes commitment and checks it equals
guardianIdentifier - Resolves JWT signing key (injected JWK or fetched Google JWKS)
- Computes
intent_hash = hashRecoveryIntent(intent) % BN254_SCALAR_FIELD_MODULUS - Generates Noir/UltraHonk proof
- Encodes
(rawProof, bytes32[18] modulusLimbs)forZkJwtVerifier
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-claimsonly for explicit debug workflows.
SDK Guide
The SDK is the integration surface for applications and wallet frontends.
Primary exports:
RecoveryClient- contract orchestrationPolicyBuilder- policy construction + validationAuthManager- adapter registryEoaAdapter,PasskeyAdapter,ZkJwtAdapter- proof generationcreateRecoveryIntent,hashRecoveryIntent- EIP-712 helpers
Recommended usage model
- Build/deploy policy with owner client.
- Create
RecoveryIntentfrom on-chain nonce and challenge period. - Have guardian-specific clients generate proofs.
- Call
startRecovery/submitProoffrom guardian wallets. - 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.recoveryManagerequals client RecoveryManagerintent.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
EoaAdapterPasskeyAdapterZkJwtAdapter
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 logicRecoveryManagerFactory.sol- deploys minimal proxy instancesverifiers/PasskeyVerifier.sol- passkey proof verificationverifiers/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
PasskeyVerifierZKTranscriptLib(for Honk verifier linking)HonkVerifierZkJwtVerifierRecoveryManagerimplementationRecoveryManagerFactory
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:
- Owner state (
owner()) - Authorized owner mutation (
setOwner(newOwner)) - 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
- Deploy wallet contract.
- Deploy wallet-specific RecoveryManager via factory.
- Authorize that RecoveryManager in wallet state.
- 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.
Recommended UX surfaces
- 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
IWalletcompatibility - 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, andrecoveryManagerin 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,RecoveryExecutedevents. - 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
- Contracts tests
- SDK build + unit/integration tests
- Circuit tests
- SDK e2e tests
Troubleshooting
Recovery intent is invalid
Common causes:
intent.recoveryManagerdoes not match target RecoveryManager addressdeadline <= 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-verifierdependency for the network.
WalletClient required for write operations
Cause:
RecoveryClientcreated withoutwalletClientbut 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
nargoand 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.