Skip to content

Commit 75116a6

Browse files
authored
Merge pull request #46 from lazor-kit/feat/deferred-execution
feat: deferred execution, odometer replay protection, Solita SDK, tx optimization
2 parents af1114d + 8c9b92b commit 75116a6

36 files changed

Lines changed: 2078 additions & 47 deletions

CHANGELOG.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
88

99
### Added
1010

11+
- Deferred Execution: 2-transaction flow for large payloads exceeding the ~574-byte limit of a single Secp256r1 Execute tx
12+
- Authorize instruction (disc=6): TX1 signs over instruction/account hashes, creates DeferredExec PDA
13+
- ExecuteDeferred instruction (disc=7): TX2 verifies hashes and executes via CPI with vault signing
14+
- ReclaimDeferred instruction (disc=8): closes expired DeferredExec accounts, refunds rent to original payer
15+
- DeferredExecAccount (176 bytes): stores instruction/account hashes, wallet, authority, payer, expiry
16+
- Deferred execution benchmarks (CU + tx size measurements for TX1/TX2)
17+
- Error codes 3014-3018 for deferred execution (expired, hash mismatch, invalid expiry, unauthorized reclaim)
18+
- SDK builders: `createAuthorizeIx`, `createExecuteDeferredIx`, `createReclaimDeferredIx`
19+
- SDK helpers: `findDeferredExecPda`, `computeInstructionsHash`
20+
- LazorKitClient methods: `authorizeSecp256r1`, `executeDeferredSecp256r1`, `reclaimDeferred`
21+
- 7 new integration tests for deferred execution (tests-sdk, total now 35)
1122
- Odometer counter replay protection for Secp256r1 (monotonic u32 per authority)
1223
- program_id included in challenge hash (cross-program replay prevention)
1324
- rpId stored on authority account at creation (saves ~14 bytes per transaction)
1425
- TypeScript SDK (`sdk/solita-client`) with Solita code generation
15-
- Integration test suite (`tests-sdk/`) with 28 tests across 7 files
26+
- Integration test suite (`tests-sdk/`) with 35 tests across 8 files
1627
- Benchmark script for CU and transaction size measurements
1728
- CompactInstructions accounts hash for anti-reordering protection
1829
- Session expiry validation (future check + 30-day max duration)

DEVELOPMENT.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ cargo test
3838
### C. IDL Generation (using Shank)
3939

4040
```bash
41-
cd program && shank idl -o . --out-filename idl.json -p 2m47smrvCRpuqAyX2dLqPxpAC1658n1BAQga1wRCsQiT
41+
cd program && shank idl -o . --out-filename idl.json -p FLb7fyAtkfA4TSa2uYcAT8QKHd2pkoMHgmqfnXFXo7ao
4242
```
4343

4444
### D. SDK Generation (using Solita)
@@ -55,7 +55,7 @@ The generate.mjs script reads the Shank IDL, enriches it with accounts/errors/ty
5555
# Terminal 1: Start local validator with program loaded
5656
cd tests-sdk && npm run validator:start
5757

58-
# Terminal 2: Run all 28 tests
58+
# Terminal 2: Run all 35 tests
5959
cd tests-sdk && npm test
6060
```
6161

@@ -65,7 +65,7 @@ cd tests-sdk && npm test
6565
cd tests-sdk && npm run benchmark
6666
```
6767

68-
Measures CU usage and transaction sizes for all instructions.
68+
Measures CU usage and transaction sizes for all instructions, including deferred execution (Authorize TX1 + ExecuteDeferred TX2).
6969

7070
### G. Program ID Sync
7171

README.md

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
A high-performance smart wallet program on Solana with passkey (WebAuthn/Secp256r1) authentication, role-based access control, session keys, and replay-safe odometer counters. Built with [pinocchio](https://github.com/febo/pinocchio) for zero-copy serialization.
44

5-
**Program ID**: `2m47smrvCRpuqAyX2dLqPxpAC1658n1BAQga1wRCsQiT`
5+
**Program ID**: `FLb7fyAtkfA4TSa2uYcAT8QKHd2pkoMHgmqfnXFXo7ao`
66

77
---
88

@@ -15,6 +15,7 @@ A high-performance smart wallet program on Solana with passkey (WebAuthn/Secp256
1515
- **Clock-Based Slot Freshness**: 150-slot window via `Clock::get()` — no SlotHashes sysvar needed
1616
- **Zero-Copy Serialization**: Raw byte casting via pinocchio, no Borsh overhead
1717
- **CompactInstructions**: Index-based instruction packing for multi-call payloads within Solana's 1,232-byte tx limit
18+
- **Deferred Execution**: 2-transaction flow for payloads exceeding the tx limit (e.g., Jupiter swaps) -- TX1 authorizes via signature, TX2 executes with full inner instruction space (~1,100 bytes)
1819
- **CPI Reentrancy Protection**: stack_height check prevents cross-program authentication attacks
1920

2021
---
@@ -23,13 +24,24 @@ A high-performance smart wallet program on Solana with passkey (WebAuthn/Secp256
2324

2425
| Metric | Normal Transfer | LazorKit (Secp256r1) | LazorKit (Session) |
2526
|---|---|---|---|
26-
| Compute Units | 150 | 10,941 | 4,483 |
27+
| Compute Units | 150 | 12,441 | 8,983 |
2728
| Transaction Size | 215 bytes | 658 bytes | 452 bytes |
2829
| Accounts | 2 | 7 | 7 |
2930
| Instructions | 1 | 2 | 1 |
3031
| Transaction Fee | 0.000005 SOL | 0.000005 SOL | 0.000005 SOL |
3132

32-
Session keys are ideal for frequent transactions — they skip the Secp256r1 precompile and use a simple Ed25519 signer, resulting in lower CU and smaller transactions.
33+
Session keys are ideal for frequent transactions -- they skip the Secp256r1 precompile and use a simple Ed25519 signer, resulting in lower CU and smaller transactions.
34+
35+
### Deferred Execution (Large Payloads)
36+
37+
For operations exceeding the ~574 bytes available in a single Secp256r1 Execute tx (e.g., Jupiter swaps):
38+
39+
| Metric | Immediate Execute | Deferred (2 txs) |
40+
|---|---|---|
41+
| Total CU | 12,441 | 18,613 (11,709 + 6,904) |
42+
| Inner Ix Capacity | ~574 bytes | ~1,100 bytes (1.9x) |
43+
| Tx Fee | 0.000005 SOL | 0.00001 SOL |
44+
| Temp Rent | -- | 0.00212 SOL (refunded) |
3345

3446
See [docs/Costs.md](docs/Costs.md) for full cost analysis, session key costs, and CU benchmarks for all instructions.
3547

@@ -45,6 +57,7 @@ See [docs/Costs.md](docs/Costs.md) for full cost analysis, session key costs, an
4557
| Authority (Ed25519) | 80 bytes | 0.001448 |
4658
| Authority (Secp256r1) | ~125 bytes | 0.001761 |
4759
| Session | 80 bytes | 0.001448 |
60+
| DeferredExec | 176 bytes | 0.002116 (temporary, refunded) |
4861

4962
### Total Wallet Creation
5063

@@ -72,6 +85,7 @@ Session rent is refundable after expiry. Ongoing Execute transactions cost only
7285
| Vault PDA | `["vault", wallet]` | Holds SOL/tokens, program signs via PDA |
7386
| Authority PDA | `["authority", wallet, id_hash]` | Per-key auth with role + counter |
7487
| Session PDA | `["session", wallet, session_key]` | Ephemeral sub-key with expiry |
88+
| DeferredExec PDA | `["deferred", wallet, authority, counter]` | Temporary pre-authorized execution (176 bytes) |
7589

7690
See [docs/Architecture.md](docs/Architecture.md) for struct definitions, security mechanisms, and instruction reference.
7791

@@ -82,12 +96,12 @@ See [docs/Architecture.md](docs/Architecture.md) for struct definitions, securit
8296
```
8397
program/src/ Rust smart contract (pinocchio, zero-copy)
8498
auth/ Ed25519 + Secp256r1/WebAuthn authentication
85-
processor/ 6 instruction handlers
99+
processor/ 9 instruction handlers
86100
state/ Account data structures (NoPadding)
87101
sdk/solita-client/ TypeScript SDK (Solita-generated + hand-written utils)
88102
src/generated/ Auto-generated instructions, accounts, errors
89103
src/utils/ Instruction builders, PDA helpers, signing utils
90-
tests-sdk/ Integration tests (vitest, 28 tests)
104+
tests-sdk/ Integration tests (vitest, 35 tests)
91105
docs/ Architecture, cost analysis
92106
audits/ Audit reports
93107
```
@@ -138,14 +152,14 @@ See [sdk/solita-client/README.md](sdk/solita-client/README.md) for full API refe
138152
# Start local validator with program loaded
139153
cd tests-sdk && npm run validator:start
140154

141-
# Run all 28 integration tests
155+
# Run all 35 integration tests
142156
npm test
143157

144158
# Run CU benchmarks
145159
npm run benchmark
146160
```
147161

148-
Tests cover: wallet lifecycle, authority management, execute, sessions, replay protection, counter edge cases, and end-to-end workflows.
162+
Tests cover: wallet lifecycle, authority management, execute, deferred execution, sessions, replay protection, counter edge cases, and end-to-end workflows.
149163

150164
See [DEVELOPMENT.md](DEVELOPMENT.md) for full development workflow.
151165

assertions/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use pinocchio_pubkey::declare_id;
1111
use pinocchio_system::ID as SYSTEM_ID;
1212

1313
// LazorKit Program ID
14-
declare_id!("2m47smrvCRpuqAyX2dLqPxpAC1658n1BAQga1wRCsQiT");
14+
declare_id!("FLb7fyAtkfA4TSa2uYcAT8QKHd2pkoMHgmqfnXFXo7ao");
1515

1616
#[allow(unused_imports)]
1717
use std::mem::MaybeUninit;

docs/Architecture.md

Lines changed: 89 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ pub enum AccountDiscriminator {
4848
Wallet = 1,
4949
Authority = 2,
5050
Session = 3,
51+
DeferredExec = 4,
5152
}
5253
```
5354

@@ -109,13 +110,36 @@ pub struct SessionAccount {
109110
// Total: 1+1+1+5+32+32+8 = 80 bytes
110111
```
111112

112-
### D. Vault PDA
113+
### D. DeferredExecAccount (176 bytes)
114+
115+
Seeds: `["deferred", wallet_pubkey, authority_pubkey, counter_le(4)]`
116+
117+
```rust
118+
#[repr(C, align(8))]
119+
pub struct DeferredExecAccount {
120+
pub discriminator: u8, // 4 = DeferredExec
121+
pub version: u8,
122+
pub bump: u8,
123+
pub _padding: [u8; 5],
124+
pub instructions_hash: [u8; 32], // SHA256 of serialized compact instructions
125+
pub accounts_hash: [u8; 32], // SHA256 of all account pubkeys referenced
126+
pub wallet: Pubkey, // 32 bytes
127+
pub authority: Pubkey, // 32 bytes — the authority that authorized
128+
pub payer: Pubkey, // 32 bytes — receives rent refund on close
129+
pub expires_at: u64, // Absolute slot at which this expires
130+
}
131+
// Total: 1+1+1+5+32+32+32+32+32+8 = 176 bytes
132+
```
133+
134+
Temporary 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`.
135+
136+
### E. Vault PDA
113137

114138
Seeds: `["vault", wallet_pubkey]`
115139

116140
No data allocated. Holds SOL. Program signs for it via PDA seeds during Execute.
117141

118-
## 5. Instructions (6 total)
142+
## 5. Instructions (9 total)
119143

120144
### CreateWallet (discriminator: 0)
121145

@@ -155,6 +179,35 @@ No data allocated. Holds SOL. Program signs for it via PDA seeds during Execute.
155179
- Validates expires_at: must be in future, max ~30 days.
156180
- Accounts: payer, wallet, authorizer, session, system_program, rent_sysvar.
157181

182+
### Authorize (discriminator: 6) — Deferred Execution TX1
183+
184+
- Creates a DeferredExec PDA storing pre-authorized instruction/account hashes.
185+
- Only Secp256r1 Owner/Admin can authorize (not Ed25519, not Spender).
186+
- Signed payload: `instructions_hash || accounts_hash` (64 bytes).
187+
- Expiry offset bounded to 10-9,000 slots (~4 seconds to ~1 hour).
188+
- Uses the authority's odometer counter (post-increment) as PDA seed nonce.
189+
- Instruction data: `[instructions_hash(32)][accounts_hash(32)][expiry_offset(2)][auth_payload(variable)]`.
190+
- Accounts: payer, wallet, authority, deferred_exec, system_program, rent_sysvar, sysvar_instructions.
191+
192+
### ExecuteDeferred (discriminator: 7) — Deferred Execution TX2
193+
194+
- Verifies compact instructions against stored hashes, executes via CPI with vault PDA signing.
195+
- Closes the DeferredExec account before CPI (close-before-execute pattern).
196+
- Verifies both instructions_hash and accounts_hash match stored values.
197+
- Checks expiry (must not be past `expires_at` slot).
198+
- Refunds rent to the original payer (stored in DeferredExec).
199+
- Self-reentrancy protection: rejects CPI back into this program.
200+
- Instruction data: `[compact_instructions(variable)]`.
201+
- Accounts: payer, wallet, vault, deferred_exec, refund_destination, [remaining accounts...].
202+
203+
### ReclaimDeferred (discriminator: 8)
204+
205+
- Closes an expired DeferredExec account and refunds rent to the original payer.
206+
- Only the original payer (stored in `deferred.payer`) can reclaim.
207+
- Can only be called after `expires_at` has passed.
208+
- No instruction data (discriminator only).
209+
- Accounts: payer, deferred_exec, refund_destination.
210+
158211
## 6. CompactInstructions Format
159212

160213
Binary format for packing multiple instructions into Execute:
@@ -175,7 +228,32 @@ Overhead per instruction: 4 bytes + num_accounts. Replaces 32-byte pubkeys with
175228

176229
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.
177230

178-
## 7. Auth Payload Layout (Secp256r1)
231+
## 7. Deferred Execution
232+
233+
2-transaction flow for payloads exceeding the ~574 bytes available in a single Secp256r1 Execute transaction (e.g., Jupiter swaps with complex routing).
234+
235+
### Flow
236+
237+
1. **TX1 (Authorize)**: Client computes `instructions_hash = SHA256(packed_compact_instructions)` and `accounts_hash = SHA256(all_referenced_pubkeys)`. These hashes are signed via Secp256r1 and stored in a DeferredExec PDA. The authority's odometer counter is incremented.
238+
2. **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.
239+
240+
### Capacity
241+
242+
| Path | Inner Ix Capacity | Total CU | Tx Fee |
243+
|---|---|---|---|
244+
| Immediate Execute | ~574 bytes | 12,441 | 0.000005 SOL |
245+
| Deferred (2 txs) | ~1,100 bytes (1.9x) | 18,613 | 0.00001 SOL |
246+
247+
### Security Properties
248+
249+
- **Hash binding**: Both instruction content and account ordering are hash-verified.
250+
- **Replay protection**: Odometer counter used as PDA seed nonce — each authorization gets a unique PDA.
251+
- **Expiry**: 10-9,000 slot window (~4s to ~1h). Prevents stale authorizations.
252+
- **Role gating**: Only Secp256r1 Owner/Admin can authorize.
253+
- **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.
254+
- **Rent recovery**: `ReclaimDeferred` allows original payer to reclaim rent from expired, unexecuted authorizations.
255+
256+
## 8. Auth Payload Layout (Secp256r1)
179257

180258
```
181259
[slot: u64 LE] // 8 bytes -- Clock-based slot freshness
@@ -190,7 +268,7 @@ Compared to the previous layout, 3 optimizations reduce the per-transaction payl
190268
- **SlotHashes index**: removed (slot freshness via `Clock::get()` instead of sysvar lookup)
191269
- **rpId**: stored on the authority account at creation, not sent per-tx (saves ~12 bytes)
192270

193-
## 8. Project Structure
271+
## 9. Project Structure
194272

195273
```
196274
program/
@@ -205,16 +283,20 @@ program/
205283
processor/
206284
create_wallet.rs
207285
manage_authority.rs AddAuthority + RemoveAuthority
208-
execute.rs CompactInstruction execution
286+
execute.rs CompactInstruction execution (immediate)
287+
authorize.rs Deferred execution TX1 (creates DeferredExec PDA)
288+
execute_deferred.rs Deferred execution TX2 (verifies + executes)
289+
reclaim_deferred.rs Closes expired DeferredExec accounts
209290
create_session.rs
210291
transfer_ownership.rs
211292
state/
212293
wallet.rs WalletAccount (8 bytes)
213294
authority.rs AuthorityAccountHeader (48 bytes)
214295
session.rs SessionAccount (80 bytes)
296+
deferred.rs DeferredExecAccount (176 bytes)
215297
compact.rs CompactInstruction serialization
216298
utils.rs PDA initialization, stack_height check
217-
error.rs AuthError enum (3001-3013)
299+
error.rs AuthError enum (3001-3018)
218300
entrypoint.rs Instruction routing
219301
sdk/solita-client/
220302
src/
@@ -226,5 +308,5 @@ sdk/solita-client/
226308
secp256r1.ts Challenge hash + auth payload builders
227309
packing.ts CompactInstruction packing
228310
errors.ts Error code mapping
229-
tests-sdk/ Integration tests (vitest, 28+ tests)
311+
tests-sdk/ Integration tests (vitest, 35 tests)
230312
```

0 commit comments

Comments
 (0)