Skip to content

feat(P1+P2+P4): session action permissions + dual-cluster + release infra - #51

Merged
onspeedhp merged 11 commits into
chore/cherry-pick-guardrailsfrom
feat/session-action-permissions
May 6, 2026
Merged

onspeedhp merged 11 commits into
chore/cherry-pick-guardrailsfrom
feat/session-action-permissions

Conversation

@onspeedhp

@onspeedhp onspeedhp commented May 6, 2026

Copy link
Copy Markdown
Member

Summary

Stacks P1, P2, and P4 of the slot-share migration plan onto P0 (#50). Brings program-v2 to feature parity with lazorkit-protocol for everything except the fee/admin surface.

Base: chore/cherry-pick-guardrails (PR #50). Merge that first.

P1 — Session action permissions (delta-audit)

Ports the 8 session action types + enforcement engine from lazorkit-protocol. The actual feature foundation contract clients will use.

4 commits

  • feat(state)program/src/state/action.rs (NEW, 697 lines, byte-identical with upstream). Defines 8 action types: SolLimit, SolRecurringLimit, SolMaxPerTx, TokenLimit, TokenRecurringLimit, TokenMaxPerTx, ProgramWhitelist, ProgramBlacklist. Type discriminators (1, 2, 3, 4, 5, 6, 10, 11) and 11-byte header (type:u8 | data_len:u16 LE | expires_at:u64 LE) preserved for SDK compat.
  • feat(session) — extends SessionAccount with optional action buffer; ParsedCreateSessionArgs parses [session_key(32)][expires_at(8)][actions_len(2)?][actions(N)?]; legacy 40-byte parser branch keeps backwards-compat for old clients.
  • chore(compact) — ports zero-copy parse_compact_instructions_ref_with_len + DecompressedInstructionRef from upstream for the Execute hot path.
  • feat(execute)processor/execute_actions.rs (NEW, 1644 lines, byte-identical with upstream): pre-CPI program whitelist/blacklist, token snapshot, vault metadata snapshot; post-CPI delta computation + SOL/token cap enforcement with saturating arithmetic + recurring window resets aligned to slot boundaries; vault invariant defenses (3030-3032) against System::Assign / SetAuthority escapes. processor/execute.rs replaced with upstream's processor/execute/immediate.rs (1 import-path line different due to flat-vs-nested layout).

P2 — Dual-cluster + security_txt (zero-audit, mechanical)

4 commits

  • feat(build) — Pattern D feature flags. cargo build-sbf --features mainnet embeds LazorjRFNavitUaBu5m3WaNPjU1maipvSW2rZfAFAKi (slot shared with lazorkit-protocol per slot-share strategy). --features devnet embeds FLb7…. No-feature build → compile_error!.
  • feat(program) — embeds security_txt! with program-v2-specific URLs + Accretion audit pointer.
  • chore(build) — adapts scripts/build-all.sh, tests-sdk/package.json, DEVELOPMENT.md to dual-cluster.
  • cisbf-cluster-check workflow verifies mainnet ≠ devnet binaries + no-feature build fails as expected.

P4 — Release infrastructure (no audit)

3 commits

  • docs(changelog) — records P1 + P2 additions under `[Unreleased]`.
  • docs(deploy) — mainnet deploy runbook + audit-frozen tag checklist (in docs/MAINNET_DEPLOY_RUNBOOK.md).
  • ci(release) — tagged-release workflow with verified-build hashes for both feature builds.

Test plan

  • CI `check-no-fee` passes (no fee surface leaked)
  • CI `sbf-cluster-check` passes (mainnet + devnet build differ; no-feature fails)
  • `cargo test --features devnet` passes locally (165 unit + 12 integration tests)
  • Verified locally with live `solana-test-validator` — all 65 vitest E2E tests pass on the next PR (feat/use-sdk-legacy) which depends on this

Audit context

This is the largest delta-audit ask in the plan. Action enforcement engine + execute integration are new code paths. See `docs/audit/DELTA_BRIEF.md` (added in next PR) for structured questions to Accretion. Byte-identity with already-audited `lazorkit-protocol` is documented per-file.

🤖 Generated with Claude Code

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.
@onspeedhp
onspeedhp force-pushed the chore/cherry-pick-guardrails branch from 3fc7a7d to f932074 Compare May 6, 2026 11:52
@onspeedhp
onspeedhp force-pushed the feat/session-action-permissions branch from b68a5eb to 9f77dcd Compare May 6, 2026 11:52
@onspeedhp
onspeedhp merged commit 79ef4d2 into chore/cherry-pick-guardrails May 6, 2026
2 checks passed
@onspeedhp
onspeedhp deleted the feat/session-action-permissions branch May 6, 2026 14:38
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