Skip to content

feat(P3+P5+P6): SDK consolidation + auth/processor port + audit prep - #52

Merged
onspeedhp merged 13 commits into
chore/cherry-pick-guardrailsfrom
feat/use-sdk-legacy
May 6, 2026
Merged

onspeedhp merged 13 commits into
chore/cherry-pick-guardrailsfrom
feat/use-sdk-legacy

Conversation

@onspeedhp

Copy link
Copy Markdown
Member

Summary

Stacks P3, P5, and P6 of the slot-share migration plan. Top of the 3-PR stack.

Base: `feat/session-action-permissions` (PR #51). Merge that first.

P3 — SDK consolidation (no audit)

Removes `sdk/solita-client` entirely; the unified `@lazorkit/sdk-legacy` (shared with `lazorkit-protocol`) is now the single SDK for both binaries.

5 commits

  • `refactor(tests-sdk)` — migrate test imports from `../../sdk/solita-client/src` → `@lazorkit/sdk-legacy`
  • `refactor(tests-sdk)` — thread `programId` through call sites + adapt secp256r1 mocks for new SDK API
  • `chore` — `git rm -r sdk/solita-client/` (replaced by `@lazorkit/sdk-legacy` from npm)
  • `docs` — scrub solita-client references; point README/DEVELOPMENT/SECURITY/CHANGELOG/Architecture/build-all.sh to `@lazorkit/sdk-legacy`
  • `test(actions)` — new `12-actions.test.ts`: 9 E2E tests for session-action enforcement against live validator (programWhitelist/Blacklist, SolMaxPerTx, SolLimit, combined). All pass.

P5 — Consolidate (auth + processor port + IDL sync)

Aligns the remaining processor + auth files with upstream so the slot-share strategy works end-to-end. Most files in this PR are byte-identical with the already-audited `lazorkit-protocol` repo — see `docs/audit/upstream-parity.txt`.

5 commits

  • `fix(tests-sdk)` — pass `PROGRAM_ID` explicitly to `LazorKitClient` constructor (sdk-legacy's URL-based auto-infer defaulted localhost to commercial `4h3X…`, broke local validator tests)
  • `feat(auth)` — port `auth/secp256r1/{mod,webauthn,introspection}.rs` from upstream. Replaces older typeAndFlags-format auth (which reconstructed clientDataJSON server-side from a single byte at `auth_payload[13]`) with the format that embeds full raw clientDataJSON in the auth payload. Byte-identical with upstream.
  • `feat(processor)` — port 5 processors (`create_wallet`, `manage_authority`, `transfer_ownership`, `execute_deferred`, `revoke_session`). Brings authority data layout to: `Secp256r1 authority = header(48) + cred_hash(32) + pubkey(33) + rpIdHash(32) = 145B` (precomputed SHA256 digest at offset 113, saves one syscall per Execute). Critical for slot-share: existing wallets created on `lazorkit-protocol` must remain readable after binary swap.
  • `fix(tests-sdk)` — include `expiry_offset` in deferred-execution `signedPayload` (Authorize binds expiry to Secp256r1 signature; test code was missing the 2-byte expiry buffer, causing 7 deferred tests to fail with InvalidMessageHash 3005). All 6 sign sites fixed.
  • `chore(instruction)` — sync Shank IDL declarations from upstream (account metadata: writable modifiers, positions, descriptions); strip 5 protocol-mgmt instruction variants (disc 10-14). Runtime not affected (sdk-legacy uses hand-written builders, not generated IDL).

P6 — Audit prep (delta-audit)

1 commit

  • `docs(audit)` — adds `docs/audit/`:
    • `DELTA_BRIEF.md` — structured summary by phase, audit asks, byte-identity claims vs upstream, slot-share strategy context (~250 lines)
    • `program-src.diff` — full unified diff of `program/` between `audit-baseline-2026-02-accretion` (d1eaaeb) and current state (~5600 lines)
    • `program-src.diff.stat` — per-file changed-line summary
    • `upstream-parity.txt` — byte-identity report: 13/19 changed files identical with upstream; 6 differ for fee-strip / cosmetic reasons only

Local git tags created (not pushed): `audit-baseline-2026-02-accretion` (d1eaaeb) and `audit-pending-v1` (this branch's HEAD).

Test plan

Verified locally with `solana-test-validator`:

  • All 65 vitest E2E tests pass (12 test files, ~60s)
  • Rust: 165 lib tests + 4+1+5+2+4 integration tests = 181 pass
  • `cargo build --features devnet` clean
  • `cargo build --features mainnet` clean
  • `cargo build` (no features) → `compile_error!` as expected
  • `cargo build-sbf --features devnet` clean
  • `bash scripts/check-no-fee.sh` clean

Audit ask

Per `docs/audit/DELTA_BRIEF.md`:

  1. Confirm P1 action enforcement engine introduces no new vulnerabilities
  2. Confirm P5 auth port (typeAndFlags → embedded clientDataJSON) is safe in this context
  3. Confirm authority layout change (raw rpId → rpIdHash) preserves binding properties
  4. Confirm slot-share compatibility — existing wallets remain valid after binary swap

🤖 Generated with Claude Code

onspeedhp added 13 commits May 4, 2026 14:19
Switches the integration test imports from the in-repo Solita-generated
client to @lazorkit/sdk-legacy as a file: dependency, on the way to
deleting sdk/solita-client entirely.

Path changes:
- '../../sdk/solita-client/src{,/utils/*,/generated/accounts}'
  → '@lazorkit/sdk-legacy'
- PROGRAM_ID is now imported from ./common (which already hardcodes the
  foundation-devnet ID FLb7…) rather than from the SDK, since the SDK's
  exports differ between solita-client (single PROGRAM_ID) and
  sdk-legacy (PROGRAM_ID_MAINNET / _DEVNET / _FOUNDATION_DEVNET).

API differences absorbed:
- await added on async client.createWallet calls (sdk-legacy probes
  ProtocolConfig before building the tx; solita-client returned sync).

API differences NOT yet absorbed (~100 type errors remaining; tracked
as follow-up):
- Standalone instruction builders (createCreateWalletIx, createExecuteIx,
  etc.) and PDA finders (findWalletPda, findAuthorityPda, etc.) now
  require an explicit `programId` arg in sdk-legacy (post lazorkit-protocol
  PR #9). Previously solita-client used an ambient PROGRAM_ID. Each call
  site needs PROGRAM_ID threaded through; that's a separate mechanical
  pass.

The vitest suite will not pass until those ~100 call sites are updated.
The sdk/solita-client directory deletion is gated on that completion.
…256r1 mocks

Final pass of the migration to @lazorkit/sdk-legacy. tsc now passes (0 errors).

Three classes of fixes:

1. PDA finders (find{Wallet,Vault,Authority,Session,DeferredExec}Pda):
   sdk-legacy requires explicit programId after lazorkit-protocol PR #9.
   Threaded `PROGRAM_ID` (from ./common) through all call sites.

2. Instruction builders (createCreateWalletIx, createExecuteIx, etc.):
   Same — added `programId: PROGRAM_ID` to the object args of every call.

3. secp256r1 mock signer:
   sdk-legacy's WebAuthn signing flow embeds clientDataJson directly
   into the auth payload (vs solita-client's older typeAndFlags shortcut).
   Replaced secp256r1Utils.ts with the version from lazorkit-protocol's
   tests-sdk and imported the helpers from @lazorkit/sdk-legacy. Added
   backwards-compat aliases (createMockSigner = createMockRawSigner,
   signSecp256r1 = signSecp256r1Raw) so existing test code is unchanged.

Other touch-ups:
- tests-sdk/package.json description updated.
- benchmark.ts: PROGRAM_ID moved to ./common import.
- devnet-smoke.ts: missing await on client.executeDeferredFromPayload.

The sdk/solita-client directory can now be deleted in a follow-up commit
once vitest is run end-to-end against the migrated tests (requires a
local-validator + the program-v2 SBF binary).
The Solita-generated client is no longer needed. tests-sdk now depends
on @lazorkit/sdk-legacy (file: link to ../../lazorkit-protocol/sdk/sdk-legacy
during local dev; npm-published version after release).

Anyone targeting program-v2 from TypeScript should:
  npm install @lazorkit/sdk-legacy

The SDK probes the on-chain ProtocolConfig PDA on first use:
  - foundation binary at the slot → no PDA → no fee accounts → no fee
  - commercial binary at the slot → PDA present → fee accounts appended
  - same SDK code works against either binary, transparently

scripts/build-all.sh and DEVELOPMENT.md still reference solita-client
in places — separate cleanup commit follows.
Followup to deleting sdk/solita-client. Updated:

- README.md: install/usage examples + project structure now reference
  @lazorkit/sdk-legacy. Added a one-liner explaining the SDK's
  flavor-blind probing behavior so foundation + commercial users see
  the same DX.
- DEVELOPMENT.md: removed solita-client from project structure, removed
  the SDK regeneration workflow (the SDK is hand-written upstream),
  added a note on local file: link setup.
- SECURITY.md: in-scope SDK reference updated.
- CHANGELOG.md: replaced the v0.1.0 line claiming Solita codegen with a
  pointer to the published @lazorkit/sdk-legacy.
- docs/Architecture.md: removed the in-tree SDK module tree from the
  layout diagram, added a pointer to the sibling repo.
- scripts/build-all.sh: removed step 3 (Solita SDK regeneration). Build
  is now Rust + IDL only.

Only remaining "solita-client" mention is in the CHANGELOG entry that
documents this removal — intentional.
9 vitest cases dogfooding @lazorkit/sdk-legacy's Actions builder against
program-v2's session enforcement engine. All pass against
solana-test-validator with the foundation binary loaded.

Coverage:
- session without actions = unrestricted (baseline)
- ProgramWhitelist: allow whitelisted, reject non-whitelisted (3021)
- ProgramBlacklist: allow non-blacklisted, reject blacklisted (3022)
- SolMaxPerTx: allow at-cap, reject over-cap (3023)
- SolLimit (lifetime): allow within budget, reject when exhausted (3024),
  then accept exact remaining
- Combined ProgramWhitelist + SolMaxPerTx: both rules enforced

Asserts vault-balance delta rather than recipient balance (recipient is
the test payer, an existing funded account, to sidestep the rent-exempt
minimum that fresh accounts hit on a tiny SOL transfer). Action checks
fire before the inner CPI, so this doesn't weaken what's being tested.

Also fixes tests/common.ts to resolve PROGRAM_ID dynamically:

  1. PROGRAM_ID env var (CI override)
  2. Pubkey of target/deploy/lazorkit_program-keypair.json (matches what
     `npm run validator:start` loads the program at)
  3. FLb7… fallback (typecheck-only)

Previously hardcoded to FLb7…, which broke for any locally built binary
because cargo build-sbf generates a fresh keypair on first build.
LazorKitClient is constructed with `new LazorKitClient(connection,
PROGRAM_ID)` to override its URL-based auto-inference (which defaults
localhost to the commercial 4h3X… ID).

Verified end-to-end: built program-v2 SBF binary, ran solana-test-validator,
ran `npx vitest run tests/12-actions.test.ts` → 9/9 pass in ~9 seconds.
sdk-legacy's LazorKitClient infers programId from the RPC URL when the
second arg is omitted, defaulting localhost to the commercial devnet ID
(4h3X…). Tests target the program-v2 binary loaded at the keypair pubkey,
so the inference returns the wrong ID and all txs fail with "Attempt to
load a program that does not exist".

Pass PROGRAM_ID explicitly across all 9 test files. Also added the
PROGRAM_ID import to 02-authority, 03-execute, 04-session, 07-e2e,
09-permissions, 10-session-execute (the others already had it).

After this fix, vitest results against a live local validator:
  35 passed | 28 failed | 2 skipped (65 total)

The 28 remaining failures are ALL Secp256r1 paths. Root cause: program-v2's
on-chain auth code still uses the OLD typeAndFlags format (extracts a
single byte from auth_payload[13] and reconstructs clientDataJson on-chain),
while sdk-legacy's mock signer uses the NEW format (embeds full
clientDataJson directly in the payload). lazorkit-protocol's auth was
upgraded to the new format; program-v2's wasn't ported. Fixing this
requires porting lazorkit-protocol/program/src/auth/secp256r1/ to
program-v2 — substantial change with audit attention. Tracked as
follow-up.

Ed25519 paths all pass. The 9 new E2E action tests (12-actions.test.ts)
all pass since they use Ed25519 admin signers.
Byte-identical with lazorkit-protocol/program/src/auth/secp256r1/. Replaces
the older typeAndFlags format (which reconstructed clientDataJSON server-side
from a single byte at auth_payload[13]) with the format that embeds the full
raw clientDataJSON in the auth payload.

Required for slot-share strategy: a wallet created on either binary
(commercial or foundation) must remain verifiable after binary swap. Both
binaries now share the same auth verification logic + on-chain authority
account layout.

Verification:
- 58/65 vitest E2E tests pass against live validator (up from 12/65 before
  port). The 7 remaining failures are in 08-deferred.test.ts and reflect a
  test-side bug (missing expiryBuf in signedPayload), addressed in P5.3.
…m upstream

Byte-identical with lazorkit-protocol/program/src/processor/{wallet/create,
authority/manage, authority/transfer_ownership, execute/deferred,
session/revoke}.rs.

Brings the on-chain authority data layout into alignment with upstream:
  Secp256r1 authority = header(48) + cred_hash(32) + pubkey(33) + rpIdHash(32) = 145B
Previously program-v2 stored variable-length raw rpId; the new layout stores
a precomputed SHA256 digest at offset 113. Saves one sol_sha256 syscall per
Execute. Critical for slot-share: existing wallets created on lazorkit-protocol
must remain readable after binary swap.

File names stay flat (program-v2 keeps `processor/create_wallet.rs` rather
than upstream's `processor/wallet/create.rs`); content identical.
…yload

The Authorize instruction binds expiry_offset to the Secp256r1 signature
hash via signed_payload = instructions_hash || accounts_hash || expiry_offset
(u16 LE). Test code was building signed_payload without the expiryBuf,
causing all 7 deferred tests to fail with InvalidMessageHash (3005). Add
expiryBuf at all 6 sign sites; values match the corresponding
createAuthorizeIx expiryOffset arg (4 × 300, 1 × 9000, 1 × 10).

Verification: 65/65 vitest pass against live validator (was 58/65 before).
…fee ix)

instruction.rs's ProgramIx enum declarations (account metadata: writable
modifiers, positions, descriptions) had drifted from lazorkit-protocol.
Runtime not affected — sdk-legacy uses hand-written instruction builders,
not the generated IDL. Resync now to keep IDL output (program/idl.json)
faithful to actual on-chain account expectations.

Strip 5 protocol-mgmt instruction variants (disc 10-14): InitializeProtocol,
UpdateProtocol, RegisterPayer, WithdrawTreasury, InitializeTreasuryShard.
program-v2 keeps disc 0-9 only (matches entrypoint dispatch).

Verification:
- cargo build --features devnet → clean
- bash scripts/check-no-fee.sh → clean
Prepares the consolidated state at audit-pending-v1 for Accretion's
delta-audit review. Local-only artifacts; not published, not pushed.

Deliverables under docs/audit/:
- DELTA_BRIEF.md: structured summary by phase (P0-P5), audit asks per phase,
  byte-identity claims vs upstream lazorkit-protocol, slot-share strategy
  context, contact + reproducibility info
- program-src.diff: full unified diff of program/ between
  audit-baseline-2026-02-accretion (d1eaaeb, the prior audited state)
  and audit-pending-v1 (9c97fe2, the new state)
- program-src.diff.stat: per-file changed-line summary
- upstream-parity.txt: byte-identity report — 13/19 changed files
  byte-identical with already-audited lazorkit-protocol; 6 differ only
  for fee-strip / cosmetic reasons (URLs, layout)

Local git tags created (not pushed):
- audit-baseline-2026-02-accretion → d1eaaeb (prior audit baseline)
- audit-pending-v1 → 9c97fe2 (current consolidated state)

Audit ask is explicit per phase:
- P1 action enforcement engine: confirm no new vulnerabilities
- P5 auth/processor port: confirm Accretion's prior review of byte-identical
  upstream files extends to program-v2
- Slot-share compatibility: confirm state account layouts forward-compatible
  for binary swap at LazorjRF… mainnet slot

Per user direction, NOT publishing or pushing yet — awaiting explicit
permission for those operational steps.
Cite the audit firm as 'Accretion' / 'Accretion Labs' only — README,
SECURITY policy, on-chain security_txt, and the audit delta brief.
Audit PDF filename retained (already an immutable artifact).
@onspeedhp
onspeedhp force-pushed the feat/session-action-permissions branch from b68a5eb to 9f77dcd Compare May 6, 2026 11:52
@onspeedhp
onspeedhp force-pushed the feat/use-sdk-legacy branch from 31fbb2f to 1abb98c Compare May 6, 2026 11:52
Base automatically changed from feat/session-action-permissions to chore/cherry-pick-guardrails May 6, 2026 14:38
@onspeedhp
onspeedhp merged commit 349837b into chore/cherry-pick-guardrails May 6, 2026
2 checks passed
@onspeedhp
onspeedhp deleted the feat/use-sdk-legacy branch May 6, 2026 14:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant