LazorKit is a high-performance smart wallet on Solana with passkey (WebAuthn) authentication, role-based access control, and session keys. Built with pinocchio for zero-copy serialization.
- Zero-Copy: pinocchio casts raw bytes to Rust structs, no Borsh.
- NoPadding: custom derive ensures memory safety and tight packing.
- Separated Storage: each authority gets its own PDA (unlimited per wallet, no resize).
- Strict RBAC: Owner (0), Admin (1), Spender (2).
- CompactInstructions: index-based instruction referencing for Execute.
- Secp256r1 (Primary: Odometer Counter): Program-controlled u32 counter per authority. Client submits
stored_counter + 1. The WebAuthn hardware counter is intentionally NOT used -- synced passkeys (iCloud, Google) return unreliable values. Counter is committed only after successful signature verification. - Secp256r1 (Secondary: Clock-based Slot Freshness): Slot from auth_payload must be within 150 slots of
Clock::get(). Provides freshness without stateful nonces or the SlotHashes sysvar. - Secp256r1 (CPI Protection): stack_height check prevents authentication via CPI.
- Secp256r1 (Signature Binding): Challenge hash binds signature to specific instruction, payer, accounts, counter, and program_id.
- Ed25519: Standard Solana runtime signer verification. No counter needed.
- Sessions: Absolute slot-based expiry. Max duration ~30 days.
SHA256(discriminator || auth_payload || signed_payload || slot || payer || counter || program_id)
7 elements, computed on-chain via sol_sha256 syscall.
- Reconstructs clientDataJSON on-chain from packed flags.
- Verifies authenticatorData flags (User Presence / User Verification).
- Uses Secp256r1SigVerify precompile via sysvar introspection.
- Stores 33-byte compressed public keys (not 64-byte uncompressed).
#[repr(u8)]
pub enum AccountDiscriminator {
Wallet = 1,
Authority = 2,
Session = 3,
DeferredExec = 4,
}Seeds: ["wallet", user_seed]
#[repr(C, align(8))]
pub struct WalletAccount {
pub discriminator: u8, // 1 = Wallet
pub bump: u8,
pub version: u8,
pub _padding: [u8; 5],
}
// Total: 8 bytesSeeds: ["authority", wallet_pubkey, id_hash]
#[repr(C, align(8))]
pub struct AuthorityAccountHeader {
pub discriminator: u8, // 2 = Authority
pub authority_type: u8, // 0=Ed25519, 1=Secp256r1
pub role: u8, // 0=Owner, 1=Admin, 2=Spender
pub bump: u8,
pub version: u8,
pub _padding1: [u8; 3],
pub counter: u32, // Monotonic u32 odometer for Secp256r1 replay protection
pub _padding2: [u8; 4], // Alignment padding (wallet stays at offset 16)
pub wallet: Pubkey, // 32 bytes
}
// Header: 1+1+1+1+1+3+4+4+32 = 48 bytes (same size, wallet at same offset)Variable data after header:
- Ed25519:
[pubkey: [u8; 32]]-- total 80 bytes. - Secp256r1:
[credential_id_hash: [u8; 32]] [compressed_pubkey: [u8; 33]] [rpIdLen: u8] [rpId: [u8; N]]-- total 114+ bytes (rpId stored on-chain to avoid per-tx transmission).
Seeds: ["session", wallet_pubkey, session_key]
#[repr(C, align(8))]
pub struct SessionAccount {
pub discriminator: u8, // 3 = Session
pub bump: u8,
pub version: u8,
pub _padding: [u8; 5],
pub wallet: Pubkey, // 32 bytes
pub session_key: Pubkey, // 32 bytes
pub expires_at: u64, // Absolute slot height
}
// Total: 1+1+1+5+32+32+8 = 80 bytesSeeds: ["deferred", wallet_pubkey, authority_pubkey, counter_le(4)]
#[repr(C, align(8))]
pub struct DeferredExecAccount {
pub discriminator: u8, // 4 = DeferredExec
pub version: u8,
pub bump: u8,
pub _padding: [u8; 5],
pub instructions_hash: [u8; 32], // SHA256 of serialized compact instructions
pub accounts_hash: [u8; 32], // SHA256 of all account pubkeys referenced
pub wallet: Pubkey, // 32 bytes
pub authority: Pubkey, // 32 bytes — the authority that authorized
pub payer: Pubkey, // 32 bytes — receives rent refund on close
pub expires_at: u64, // Absolute slot at which this expires
}
// Total: 1+1+1+5+32+32+32+32+32+8 = 176 bytesTemporary account created during Authorize (tx1) and closed during ExecuteDeferred (tx2). Uses the authority's odometer counter as a seed nonce, ensuring unique PDAs per authorization. Expired accounts can be reclaimed via ReclaimDeferred.
Seeds: ["vault", wallet_pubkey]
No data allocated. Holds SOL. Program signs for it via PDA seeds during Execute.
A key design property: different authorities on the same wallet can execute transactions in parallel on Solana's runtime.
During Execute, the only account written to is the authority PDA (odometer counter increment). The wallet PDA and vault PDA are read-only:
| Account | Access | Shared across authorities? |
|---|---|---|
| Authority PDA | Writable (counter++) | No -- each authority has its own PDA |
| Wallet PDA | Read-only | Yes, but no write lock |
| Vault PDA | Signer-only (CPI) | Yes, but no write lock |
Since each authority is a separate PDA, Solana's scheduler sees no writable overlap and runs them concurrently.
| Scenario | Parallel? | Reason |
|---|---|---|
| Authority A + Authority B (same wallet) | Yes | Different writable PDAs |
| Session key + Secp256r1 authority (same wallet) | Yes | Different writable PDAs |
| Same authority, 2 transactions | No | Same writable PDA + counter conflict |
| Authority A (wallet 1) + Authority B (wallet 2) | Yes | Entirely separate accounts |
This enables high-throughput wallets where multiple authorized parties (e.g., an admin managing permissions while a spender sends payments, or multiple session keys operating concurrently) never block each other. The per-authority odometer counter provides replay protection without creating a shared bottleneck.
- Creates Wallet PDA, Vault PDA (derived only), and first Authority PDA.
- Transfer-Allocate-Assign pattern to prevent pre-funding DoS.
- Accounts: payer, wallet, vault, authority, system_program, rent_sysvar.
- Creates new Authority PDA.
- Requires Admin or Owner authentication.
- Owner can add any role; Admin can only add Spender.
- Accounts: payer, wallet, admin_authority, new_authority, system_program, rent_sysvar [+ sysvar_instructions for Secp256r1].
- Closes Authority PDA, refunds rent to specified destination.
- Prevents self-removal and owner removal (ownership must be transferred).
- Accounts: payer, wallet, admin_authority, target_authority, refund_destination.
- Atomically closes old owner and creates new owner.
- Accounts: payer, wallet, current_owner, new_owner_authority, system_program, rent_sysvar.
- Executes CompactInstructions via CPI with vault PDA signing.
- Supports 3 auth modes: Ed25519 signer, Secp256r1 (with precompile), Session key.
- Self-reentrancy protection: rejects CPI back into this program.
- Accounts: payer, wallet, authority/session, vault, [remaining accounts...].
- Creates ephemeral Session PDA with slot-based expiry.
- Requires Admin or Owner.
- Validates expires_at: must be in future, max ~30 days.
- Accounts: payer, wallet, authorizer, session, system_program, rent_sysvar.
- Creates a DeferredExec PDA storing pre-authorized instruction/account hashes.
- Only Secp256r1 Owner/Admin can authorize (not Ed25519, not Spender).
- Signed payload:
instructions_hash || accounts_hash || expiry_offset(66 bytes). - Expiry offset bounded to 10-9,000 slots (~4 seconds to ~1 hour).
- Uses the authority's odometer counter (post-increment) as PDA seed nonce.
- Instruction data:
[instructions_hash(32)][accounts_hash(32)][expiry_offset(2)][auth_payload(variable)]. - Accounts: payer, wallet, authority, deferred_exec, system_program, rent_sysvar, sysvar_instructions.
- Verifies compact instructions against stored hashes, executes via CPI with vault PDA signing.
- Closes the DeferredExec account before CPI (close-before-execute pattern).
- Verifies both instructions_hash and accounts_hash match stored values.
- Checks expiry (must not be past
expires_atslot). - Refunds rent to the original payer (stored in DeferredExec).
- Self-reentrancy protection: rejects CPI back into this program.
- Instruction data:
[compact_instructions(variable)]. - Accounts: payer, wallet, vault, deferred_exec, refund_destination, [remaining accounts...].
- Closes an expired DeferredExec account and refunds rent to the original payer.
- Only the original payer (stored in
deferred.payer) can reclaim. - Can only be called after
expires_athas passed. - No instruction data (discriminator only).
- Accounts: payer, deferred_exec, refund_destination.
- Closes a session account early (before expiry), refunding rent.
- Only Owner or Admin can revoke (Spender cannot).
- Session can be revoked regardless of whether it is expired or active.
- Signature bound to specific session PDA + refund destination (prevents replay).
- Accounts: payer, wallet, admin_authority, session, refund_destination [+ auth_extra].
Binary format for packing multiple instructions into Execute:
[num_instructions: u8]
For each instruction:
[program_id_index: u8] // Index into transaction accounts
[num_accounts: u8]
[account_indexes: u8[]] // Indexes into transaction accounts
[data_len: u16 LE]
[instruction_data: u8[]]
Overhead per instruction: 4 bytes + num_accounts. Replaces 32-byte pubkeys with 1-byte indexes.
For Secp256r1 Execute, the signed payload includes a SHA256 hash of all account pubkeys referenced by the compact instructions. This prevents account reordering attacks where an attacker could swap recipient addresses while keeping the signature valid.
2-transaction flow for payloads exceeding the ~574 bytes available in a single Secp256r1 Execute transaction (e.g., Jupiter swaps with complex routing).
- TX1 (Authorize): Client computes
instructions_hash = SHA256(packed_compact_instructions)andaccounts_hash = SHA256(all_referenced_pubkeys). These hashes are signed via Secp256r1 and stored in a DeferredExec PDA. The authority's odometer counter is incremented. - TX2 (ExecuteDeferred): Any signer submits the full compact instructions. The program verifies both hashes match, checks expiry, closes the DeferredExec account, and executes via CPI with vault signing.
| Path | Inner Ix Capacity | Total CU | Tx Fee |
|---|---|---|---|
| Immediate Execute | ~574 bytes | 9,441 | 0.000005 SOL |
| Deferred (2 txs) | ~1,100 bytes (1.9x) | 15,613 | 0.00001 SOL |
- Hash binding: Both instruction content and account ordering are hash-verified.
- Replay protection: Odometer counter used as PDA seed nonce — each authorization gets a unique PDA.
- Expiry: 10-9,000 slot window (~4s to ~1h). Prevents stale authorizations.
- Role gating: Only Secp256r1 Owner/Admin can authorize.
- Close-before-CPI: DeferredExec account is closed before CPI execution, avoiding stale-pointer issues with
invoke_signed_unchecked. Transaction reverts atomically if any CPI fails. - Rent recovery:
ReclaimDeferredallows original payer to reclaim rent from expired, unexecuted authorizations.
[slot: u64 LE] // 8 bytes -- Clock-based slot freshness
[counter: u32 LE] // 4 bytes -- odometer value (stored + 1)
[sysvar_ix_index: u8] // 1 byte -- index of sysvar_instructions in accounts
[type_and_flags: u8] // 1 byte -- WebAuthn type + flags
[authenticator_data: u8[]] // M bytes -- WebAuthn authenticator data (min 37 bytes)
Compared to the previous layout, 3 optimizations reduce the per-transaction payload:
- Counter: u64 -> u32 (saves 4 bytes; 4 billion ops per authority is sufficient)
- SlotHashes index: removed (slot freshness via
Clock::get()instead of sysvar lookup) - rpId: stored on the authority account at creation, not sent per-tx (saves ~12 bytes)
program/
src/
auth/
ed25519.rs Native signer verification
secp256r1/
mod.rs Passkey authenticator with odometer + Clock-based slot check
introspection.rs Precompile instruction verification
webauthn.rs ClientDataJSON reconstruction + AuthDataParser
traits.rs Authenticator trait
processor/
create_wallet.rs
manage_authority.rs AddAuthority + RemoveAuthority
execute.rs CompactInstruction execution (immediate)
authorize.rs Deferred execution TX1 (creates DeferredExec PDA)
execute_deferred.rs Deferred execution TX2 (verifies + executes)
reclaim_deferred.rs Closes expired DeferredExec accounts
create_session.rs
transfer_ownership.rs
state/
wallet.rs WalletAccount (8 bytes)
authority.rs AuthorityAccountHeader (48 bytes)
session.rs SessionAccount (80 bytes)
deferred.rs DeferredExecAccount (176 bytes)
compact.rs CompactInstruction serialization
utils.rs PDA initialization, stack_height check
error.rs AuthError enum (3001-3018)
entrypoint.rs Instruction routing
sdk/solita-client/
src/
generated/ Solita-generated instructions, accounts, errors
utils/
instructions.ts Low-level instruction builders
client.ts LazorKitClient high-level API (unified)
types.ts Discriminated union signer types + helper constructors
signing.ts Secp256r1 signing utilities
compact.ts CompactInstruction layout builder
pdas.ts PDA derivation helpers
secp256r1.ts Challenge hash + auth payload builders
packing.ts CompactInstruction packing
errors.ts Error code mapping
tests-sdk/ Integration + security tests (vitest, 56 tests)