Skip to content

docs: refresh markdown for current codebase (dual-cluster IDs, action permissions, current test counts) - #53

Merged
onspeedhp merged 27 commits into
mainfrom
chore/docs-refresh
May 6, 2026
Merged

onspeedhp merged 27 commits into
mainfrom
chore/docs-refresh

Conversation

@onspeedhp

Copy link
Copy Markdown
Member

Summary

Stale-reference cleanup pass over the project's markdown after the recent
P0–P6 work landed on `chore/cherry-pick-guardrails`. Brings docs into
agreement with the codebase as it stands.

Note on base branch: this PR targets `main` but its commit graph
sits on top of `chore/cherry-pick-guardrails` (which carries the
merged content of #50 + #51 + #52). `main` itself is still at #50;
the consolidated stack on `chore/cherry-pick-guardrails` needs to land
on `main` first (or this PR's base updated to `chore/cherry-pick-guardrails`).

Files updated

  • README.md — explicit dual-cluster program ID table (mainnet `LazorjRF…`, devnet `FLb7…`); slot-share note pointing at `lazorkit-protocol`; build commands now show `--features mainnet/devnet`; test count 56 → 65; instruction count 9 → 10; authority size 125 → 145 bytes (rpIdHash); session description expanded to cover action permissions
  • DEVELOPMENT.md — test count 56 → 65 in run-tests command
  • docs/Architecture.md — `SessionAccount` section now covers actions buffer + table of all 8 action types; Secp256r1 authority data layout updated to rpIdHash 32 bytes (total 145B); WebAuthn description updated to embedded raw clientDataJSON (no longer "reconstructs from packed flags"); processor tree updated for `execute_actions.rs` + `revoke_session.rs`; `AuthError` range `3001-3018` → `3001-3032`; project structure mentions `action.rs` + zero-copy compact ref variants; entrypoint mentions disc 0–9
  • docs/Costs.md — program ID line now lists both mainnet and devnet; Secp256r1 authority size updated to 145 bytes / rpIdHash; smoke test exercises 10 instructions

Test plan

Documentation only — no code changes. Reviewer needs to verify:

  • All cited counts/sizes/discriminators match the code
  • All `scripts/*` references in docs match files actually present
  • All program IDs match `assertions/src/lib.rs` per cluster

Port the 8 session action types (SolLimit, SolRecurringLimit, SolMaxPerTx,
TokenLimit, TokenRecurringLimit, TokenMaxPerTx, ProgramWhitelist,
ProgramBlacklist) plus parser, validator, and 30 unit tests verbatim from
lazorkit-protocol. Type discriminators (1, 2, 3, 4, 5, 6, 10, 11) and the
11-byte header layout are preserved so the unified SDK can encode actions
identically for both programs.

Foundation only — does not yet wire actions into CreateSession or enforce
them at Execute. Those land in follow-up commits (P1b, P1c).

Notes:
- action.rs is byte-identical to lazorkit-protocol@HEAD for audit
  comparability; existing harmless `unused_mut` warning ported as-is.
- AuthError numeric codes 3020-3029 mirror lazorkit-protocol so the
  unified SDK can decode action errors uniformly.
…reation

Port from lazorkit-protocol the variable-size SessionAccount and the
ParsedCreateSessionArgs flow that lets a CreateSession instruction carry
an optional 8-action permission buffer.

state/session.rs is now byte-identical to upstream:
- exposes SESSION_HEADER_SIZE (80) constant
- adds has_actions() and actions_slice() helpers
- documents that optional actions follow the fixed header

processor/create_session.rs:
- replaces CreateSessionArgs with ParsedCreateSessionArgs that parses
  [session_key(32)][expires_at(8)][actions_len(2)?][actions(N)?]
- caps actions_len at 2048 to prevent BPF heap exhaustion
- runs validate_actions_buffer() at creation time
- session PDA size is now SESSION_HEADER_SIZE + actions_bytes.len()
- ed25519 + secp256r1 signed payloads include actions_bytes so the
  permission set is bound to the signature

Wire-up only: actions are stored on-chain but not yet enforced at
Execute time. P1c will port execute/actions.rs to add enforcement.

Backwards compatibility note: clients still sending exactly 40 bytes
(no actions_len header) continue to work via the legacy parser branch,
matching upstream behavior.
…ream

Adds CompactInstructionRef::{from_bytes, decompress}, DecompressedInstructionRef,
and parse_compact_instructions_ref_with_len from lazorkit-protocol. Lets the
Execute hot path parse and decompress without per-instruction Vec<u8> allocs
for account-index bytes or instruction data.

Existing parse_compact_instructions / serialize_compact_instructions remain
for callers (execute_deferred) that still need owned copies. File is now
byte-identical to upstream compact.rs for audit comparability.
Wires the 8 session action types (ported in earlier commits) into the
Execute instruction. Sessions with attached actions now have spending
limits, recurring caps, per-tx maxes, and program whitelist/blacklist
rules enforced around the CPI loop.

Files:
- processor/execute_actions.rs (NEW, 1644 lines, byte-identical to upstream
  processor/execute/actions.rs): the enforcement engine — pre-CPI program
  whitelist/blacklist checks, token-balance + token-authority snapshots,
  post-CPI delta computation, SOL/token cap enforcement with saturating
  arithmetic, recurring window resets aligned to slot boundaries.
- processor/execute.rs (REPLACED with upstream processor/execute/immediate.rs):
  integrates pre/post action evaluation around the CPI loop, snapshots
  vault metadata + token authorities for invariant checks, tracks gross
  SOL outflow per CPI for SolMaxPerTx. Adds L5 anti-CPI guard for
  session-authenticated Execute (stack height must be 1).
- error.rs: adds SessionVaultOwnerChanged (3030), SessionVaultDataLenChanged
  (3031), SessionTokenAuthorityChanged (3032) for the vault-invariant
  defenses against System::Assign and SetAuthority escapes.
- processor/mod.rs: wires execute_actions module.

Verification:
- 111 unit tests pass (action validator + execute_actions helpers)
- 18 litesvm integration tests pass (wallet_lifecycle, sessions, etc.)
- Existing devnet program ID FLb7… still works for backward compatibility

Note on file structure: upstream organizes execute as a subdir
(processor/execute/{actions,immediate,...}.rs); program-v2 keeps the flat
layout for now, putting the helpers in processor/execute_actions.rs and
the immediate-execute logic in processor/execute.rs. Future P2 may
restructure to subdir for closer upstream alignment.
Apply Pattern D from lazorkit-protocol PR #9: the embedded program ID
is now chosen by `--features mainnet` or `--features devnet`, with a
`compile_error!` if neither (or both) is set. Prevents accidental
cross-cluster deploys — a binary compiled with one ID malfunctions if
deployed to the other cluster's slot.

Mainnet feature embeds LazorjRFNavitUaBu5m3WaNPjU1maipvSW2rZfAFAKi —
the SAME program ID as lazorkit-protocol. program-v2 (foundation,
no-fee build) occupies that mainnet slot for the duration of the
foundation contract; at contract end the upgrade authority swaps the
binary at the same slot to lazorkit-protocol's commercial build.
dApp integrators keep one stable program ID through the transition.

Devnet feature keeps program-v2's existing FLb7fyAtkfA4TSa2uYcAT8QKHd2pkoMHgmqfnXFXo7ao.

Build verification:
  cargo build-sbf --features mainnet  →  sha differs from devnet
  cargo build-sbf --features devnet   →  sha differs from mainnet
  cargo build-sbf                     →  fails with compile_error
  cargo build-sbf --features mainnet --features devnet → fails
Add solana-security-txt + default-env deps and embed a `security_txt!`
block in program/src/lib.rs so on-chain inspectors (and security
researchers) get a self-described pointer to the SECURITY.md, contact
endpoints, source repo, and audit report.

Identifies the binary as the "Foundation Build" of LazorKit Smart Wallet
to distinguish it from the lazorkit-protocol commercial binary that may
later occupy the same mainnet slot. source_revision and source_release
are populated from GITHUB_SHA / GITHUB_REF_NAME at CI build time.

Audit pointer is the existing Accretion Labs report shipped under
audits/.
- scripts/build-all.sh now takes a cluster argument (mainnet|devnet) and
  passes it through to cargo build-sbf. The program ID for IDL generation
  is derived from the resulting keypair instead of being passed in.
- tests-sdk/package.json validator:start now builds with --features devnet
  before launching solana-test-validator and reads the program ID via
  solana-keygen pubkey on the keypair file (matching upstream pattern).
- DEVELOPMENT.md updated with the new build invocations, --features devnet
  for cargo test, and a "Mainnet Deploy Strategy" section documenting the
  slot-sharing arrangement with lazorkit-protocol and the binary swap at
  contract end.
- Cargo.lock regenerated for the security-txt deps added in the previous
  commit.
Verifies the dual-cluster mechanism stays intact:
  1. cargo build-sbf --features mainnet  succeeds
  2. cargo build-sbf --features devnet   succeeds
  3. The two .so binaries differ (catches a refactor that neutralises
     the cfg gate on declare_id!)
  4. cargo build-sbf with no feature fails with the expected
     "pick exactly one cluster" compile_error
  5. cargo build-sbf with both features fails the same way

Workflow is byte-identical to upstream's. Triggers on PRs touching
program/ or assertions/, and on pushes to main.
Captures everything landed since the last audit cycle:
- 8 session action permission types (state + parser + validator + tests)
- SessionAccount variable-size + CreateSession action buffer support
- Pre-CPI program whitelist/blacklist + post-CPI spending caps
- Vault + token-account invariant defenses (3030-3032 errors)
- Anti-CPI guard for session-authenticated Execute
- Zero-copy CompactInstructionRef parser
- Dual-cluster Cargo features (mainnet/devnet) with compile_error guard
- security.txt embedded via solana-security-txt
- Cherry-pick guardrails (fee-paths.txt + scripts + CI workflow)
- sbf-cluster-check CI workflow
- build-all.sh refactored for feature-flagged builds; sync-program-id.sh removed
Two operational documents covering the path from audit submission to
production deploy:

docs/MAINNET_DEPLOY.md — runbook for the foundation deploy and the binary
swap to lazorkit-protocol at contract end. Both binaries occupy the same
mainnet slot (LazorjRF…) at different times. Covers initial deploy,
routine upgrades, binary swap, rollback, pre-existing wallet account
behavior across the swap, and final upgrade-authority lock-down.

docs/AUDIT_PREP.md — pre-tag checklist run before submitting a revision
to Accretion. Covers code state hygiene, dual-feature build verification,
test suite, fee-surface invariant (check-no-fee), documentation
alignment, diff bounding, and the audit-packet contents to deliver. Also
defines the audit-frozen-vN tag naming convention and the post-audit
fix-up flow.
Triggered by audit-frozen-v* and v*.*.* tags. Builds both mainnet and
devnet SBF binaries, records SHA-256 hashes, asserts they differ
(catches a regression of the dual-cluster mechanism), and uploads the
binaries + IDL + a manifest as GitHub Release assets.

Manifest captures the build environment (Solana CLI version, rustc
version, commit SHA) and the reproduction commands so anyone can
locally rebuild and confirm the published hashes match.

audit-frozen-v* tags create draft + prerelease releases (the artifact
is for the auditor, not for end-users); v*.*.* tags create normal
releases. Production deploys must consume binaries from a release —
see docs/MAINNET_DEPLOY.md.

GITHUB_SHA + GITHUB_REF_NAME are passed through to cargo build-sbf so
the embedded security.txt advertises the exact source revision.
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).
feat(P1+P2+P4): session action permissions + dual-cluster + release infra
feat(P3+P5+P6): SDK consolidation + auth/processor port + audit prep
…am IDs

Updates README, DEVELOPMENT, Architecture, and Costs docs:

- README: explicit dual-cluster program ID table (mainnet LazorjRF…, devnet
  FLb7…), slot-share note pointing at lazorkit-protocol; build commands now
  show --features mainnet/devnet; test count 56 → 65; instruction count
  9 → 10; authority size 125 → 145 bytes (rpIdHash); session description
  expanded to cover action permissions
- DEVELOPMENT: test count 56 → 65 in run-tests command
- Architecture: SessionAccount section now covers actions buffer + table
  of all 8 action types; Secp256r1 authority data layout updated to
  rpIdHash 32 bytes (total 145B); WebAuthn description updated to embedded
  raw clientDataJSON (no longer 'reconstructs from packed flags');
  processor tree updated for execute_actions.rs + revoke_session.rs;
  AuthError range 3001-3018 → 3001-3032; project structure mentions
  action.rs + zero-copy compact ref variants; entrypoint mentions disc 0–9
- Costs: program ID line now lists both mainnet and devnet; Secp256r1
  authority size updated to 145 bytes / rpIdHash; smoke test exercises
  10 instructions
@onspeedhp
onspeedhp merged commit 5d68d56 into main May 6, 2026
5 checks passed
@onspeedhp
onspeedhp deleted the chore/docs-refresh branch May 6, 2026 15:03
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