Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
2716642
feat(state): port session action permission types from lazorkit-protocol
onspeedhp Apr 30, 2026
3ff167f
feat(session): support optional action permission buffer at session c…
onspeedhp Apr 30, 2026
a3930f2
chore(compact): port zero-copy ref-based instruction parser from upst…
onspeedhp Apr 30, 2026
644c5d6
feat(execute): enforce session action permissions at execute time
onspeedhp Apr 30, 2026
4cbe03d
feat(build): dual-cluster Cargo features for compile-time program ID
onspeedhp Apr 30, 2026
57358e8
feat(program): embed security.txt with audit + contact info
onspeedhp Apr 30, 2026
2708a24
chore(build): adapt dev workflow to dual-cluster features
onspeedhp Apr 30, 2026
3525c83
ci: port sbf-cluster-check workflow from lazorkit-protocol
onspeedhp Apr 30, 2026
d384fd6
docs(changelog): record P1 + P2 additions under [Unreleased]
onspeedhp Apr 30, 2026
fe249b8
docs(deploy): mainnet deploy runbook + audit-frozen tag checklist
onspeedhp Apr 30, 2026
9f77dcd
ci(release): tagged-release workflow with verified-build hashes
onspeedhp Apr 30, 2026
67cc1f6
refactor(tests-sdk): migrate from solita-client to @lazorkit/sdk-legacy
onspeedhp May 4, 2026
e81f126
refactor(tests-sdk): thread programId through call sites + adapt secp…
onspeedhp May 4, 2026
781f89e
chore: delete sdk/solita-client (replaced by @lazorkit/sdk-legacy)
onspeedhp May 4, 2026
db8909a
docs: scrub solita-client references; point to @lazorkit/sdk-legacy
onspeedhp May 4, 2026
85abe82
test(actions): E2E session-action enforcement against live validator
onspeedhp May 4, 2026
07bc824
fix(tests-sdk): pass PROGRAM_ID explicitly to LazorKitClient constructor
onspeedhp May 4, 2026
1a347a0
feat(auth): port Secp256r1 auth to clientDataJSON-embedding format
onspeedhp May 6, 2026
19b52f9
feat(processor): port wallet/authority/execute/session processors fro…
onspeedhp May 6, 2026
62ad927
fix(tests-sdk): include expiry_offset in deferred-execution signed pa…
onspeedhp May 6, 2026
cbf80cf
chore(instruction): sync Shank IDL declarations from upstream (strip …
onspeedhp May 6, 2026
0554ae2
docs(audit): delta brief + diff bundle for Accretion follow-up
onspeedhp May 6, 2026
770ad2d
docs(changelog): record P5 (auth + processor port) and P6 (audit prep…
onspeedhp May 6, 2026
1abb98c
chore: remove Solana Foundation references from content
onspeedhp May 6, 2026
79ef4d2
Merge pull request #51 from lazor-kit/feat/session-action-permissions
onspeedhp May 6, 2026
349837b
Merge pull request #52 from lazor-kit/feat/use-sdk-legacy
onspeedhp May 6, 2026
e007788
docs: refresh markdown to match current codebase + dual-cluster progr…
onspeedhp May 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 153 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
name: release

# Triggered by pushing an `audit-frozen-v*` tag (or a `v*` semver release tag).
# Builds the mainnet and devnet SBF binaries, records their SHA-256 hashes,
# and attaches the .so files + a manifest to the GitHub Release.
#
# The release artifacts are the source of truth for what gets deployed to
# mainnet — production deploys must use a binary downloaded from a release,
# not a locally-built one. See docs/MAINNET_DEPLOY.md.

on:
push:
tags:
- 'audit-frozen-v*'
- 'v*.*.*'

jobs:
release:
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: write # for creating GitHub Releases

steps:
- uses: actions/checkout@v4
with:
# Full history so source_revision in security_txt embeds the right SHA.
fetch-depth: 0

- name: Install Solana toolchain
run: |
sh -c "$(curl -sSfL https://release.anza.xyz/stable/install)"
echo "$HOME/.local/share/solana/install/active_release/bin" >> "$GITHUB_PATH"

- name: Pin Solana CLI to the version declared in Cargo.toml
run: |
DECLARED=$(grep -A1 'workspace.metadata.cli' Cargo.toml | grep solana | sed -E 's/.*"([^"]+)".*/\1/')
INSTALLED=$(solana --version | awk '{print $2}')
echo "Declared: $DECLARED"
echo "Installed: $INSTALLED"
if [ "$DECLARED" != "$INSTALLED" ]; then
echo "::warning::Installed Solana CLI ($INSTALLED) does not match declared ($DECLARED). Verified-build hashes may differ from what consumers reproduce."
fi

- name: Cache cargo build
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: release-${{ hashFiles('**/Cargo.lock') }}-${{ github.ref_name }}

- name: Build mainnet binary
working-directory: program
env:
GITHUB_SHA: ${{ github.sha }}
GITHUB_REF_NAME: ${{ github.ref_name }}
run: cargo build-sbf --features mainnet

- name: Hash + stage mainnet artifact
run: |
mkdir -p release-artifacts
cp target/deploy/lazorkit_program.so release-artifacts/lazorkit_program-mainnet.so
MAINNET_SHA=$(shasum -a 256 release-artifacts/lazorkit_program-mainnet.so | awk '{print $1}')
echo "MAINNET_SHA=$MAINNET_SHA" >> "$GITHUB_ENV"
echo "mainnet sha256: $MAINNET_SHA"

- name: Build devnet binary
working-directory: program
env:
GITHUB_SHA: ${{ github.sha }}
GITHUB_REF_NAME: ${{ github.ref_name }}
run: cargo build-sbf --features devnet

- name: Hash + stage devnet artifact
run: |
cp target/deploy/lazorkit_program.so release-artifacts/lazorkit_program-devnet.so
DEVNET_SHA=$(shasum -a 256 release-artifacts/lazorkit_program-devnet.so | awk '{print $1}')
echo "DEVNET_SHA=$DEVNET_SHA" >> "$GITHUB_ENV"
echo "devnet sha256: $DEVNET_SHA"

- name: Verify binaries differ
run: |
if [ "$MAINNET_SHA" = "$DEVNET_SHA" ]; then
echo "::error::mainnet and devnet binaries are identical — dual-cluster mechanism broken"
exit 1
fi

- name: Stage IDL + keypair
run: |
cp program/idl.json release-artifacts/idl.json
# The keypair file is regenerated per build; useful as a record but
# NOT for deployment (the actual mainnet keypair is held off-CI).
if [ -f target/deploy/lazorkit_program-keypair.json ]; then
cp target/deploy/lazorkit_program-keypair.json release-artifacts/build-keypair.json
fi

- name: Write release manifest
run: |
cat > release-artifacts/MANIFEST.txt <<EOF
LazorKit program-v2 release manifest
tag: ${{ github.ref_name }}
commit: ${{ github.sha }}
built: $(date -u +"%Y-%m-%dT%H:%M:%SZ")
solana-cli: $(solana --version)
rust-toolchain: $(rustc --version)

mainnet binary: lazorkit_program-mainnet.so
mainnet sha256: $MAINNET_SHA
mainnet program ID: LazorjRFNavitUaBu5m3WaNPjU1maipvSW2rZfAFAKi

devnet binary: lazorkit_program-devnet.so
devnet sha256: $DEVNET_SHA
devnet program ID: FLb7fyAtkfA4TSa2uYcAT8QKHd2pkoMHgmqfnXFXo7ao

To reproduce these hashes locally:
git checkout ${{ github.ref_name }}
cd program
cargo build-sbf --features mainnet # → mainnet sha256 above
cargo build-sbf --features devnet # → devnet sha256 above

To deploy: see docs/MAINNET_DEPLOY.md
EOF
cat release-artifacts/MANIFEST.txt

- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
name: ${{ github.ref_name }}
tag_name: ${{ github.ref_name }}
body: |
**Tag:** `${{ github.ref_name }}`
**Commit:** `${{ github.sha }}`

**Mainnet binary:** `lazorkit_program-mainnet.so`
**Mainnet sha256:** `${{ env.MAINNET_SHA }}`
**Mainnet program ID:** `LazorjRFNavitUaBu5m3WaNPjU1maipvSW2rZfAFAKi` (slot shared with `lazorkit-protocol`)

**Devnet binary:** `lazorkit_program-devnet.so`
**Devnet sha256:** `${{ env.DEVNET_SHA }}`
**Devnet program ID:** `FLb7fyAtkfA4TSa2uYcAT8QKHd2pkoMHgmqfnXFXo7ao`

See [`MANIFEST.txt`](./MANIFEST.txt) for build environment + reproduction
commands and [`docs/MAINNET_DEPLOY.md`](../docs/MAINNET_DEPLOY.md) for
deployment procedure.
files: |
release-artifacts/lazorkit_program-mainnet.so
release-artifacts/lazorkit_program-devnet.so
release-artifacts/idl.json
release-artifacts/MANIFEST.txt
draft: ${{ startsWith(github.ref_name, 'audit-frozen-') }}
prerelease: ${{ startsWith(github.ref_name, 'audit-frozen-') }}
122 changes: 122 additions & 0 deletions .github/workflows/sbf-cluster-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
name: SBF cluster feature check

# Verifies the Pattern D feature-flag mechanism in `assertions/src/lib.rs`:
#
# 1. `cargo build-sbf --features mainnet` succeeds and produces a binary
# embedding the mainnet vanity ID.
# 2. `cargo build-sbf --features devnet` succeeds and produces a binary
# embedding the devnet ID.
# 3. The two binaries differ (otherwise the feature flag has been
# neutralised by a refactor and Pattern D no longer protects against
# cross-cluster deploys).
# 4. `cargo build-sbf` with no feature flag fails with the expected
# `compile_error!` (otherwise nothing prevents an unflagged build
# from silently embedding whichever ID happens to be the default).
#
# Runs on every PR touching the program / assertions crate or this workflow.

on:
pull_request:
paths:
- 'program/**'
- 'assertions/**'
- '.github/workflows/sbf-cluster-check.yml'
push:
branches: [main]

jobs:
cluster-check:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4

- name: Install Solana toolchain
run: |
sh -c "$(curl -sSfL https://release.anza.xyz/stable/install)"
echo "$HOME/.local/share/solana/install/active_release/bin" >> "$GITHUB_PATH"

- name: Cache cargo build
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: sbf-${{ hashFiles('**/Cargo.lock') }}

- name: Build mainnet binary
working-directory: program
run: cargo build-sbf --features mainnet

- name: Record mainnet hash
id: mainnet
run: |
M=$(shasum -a 256 target/deploy/lazorkit_program.so | awk '{print $1}')
echo "sha=$M" >> "$GITHUB_OUTPUT"
echo "mainnet SBF: $M"

- name: Build devnet binary
working-directory: program
run: cargo build-sbf --features devnet

- name: Record devnet hash
id: devnet
run: |
D=$(shasum -a 256 target/deploy/lazorkit_program.so | awk '{print $1}')
echo "sha=$D" >> "$GITHUB_OUTPUT"
echo "devnet SBF: $D"

- name: Verify binaries differ
run: |
if [ "${{ steps.mainnet.outputs.sha }}" = "${{ steps.devnet.outputs.sha }}" ]; then
echo "ERROR: mainnet + devnet SBF binaries are identical."
echo " Pattern D's compile-time cluster switch has been"
echo " neutralised — likely a refactor removed the cfg gate"
echo " on declare_id! in assertions/src/lib.rs."
exit 1
fi
echo "✓ binaries differ as expected"

- name: Verify no-feature build fails with compile_error!
working-directory: program
run: |
# Capture exit code separately — `cmd | tee` returns tee's exit
# (always 0), masking cargo's failure. `set -o pipefail` would
# also work, but capturing to a file gives us the log to search
# afterwards regardless of pipeline state.
set +e
cargo build-sbf > /tmp/build.log 2>&1
BUILD_EXIT=$?
set -e
cat /tmp/build.log
if [ "$BUILD_EXIT" -eq 0 ]; then
echo "ERROR: cargo build-sbf without --features mainnet/devnet succeeded."
echo " The compile_error! in assertions/src/lib.rs is no longer firing."
exit 1
fi
if ! grep -q "pick exactly one cluster" /tmp/build.log; then
echo "ERROR: build failed but not with the expected compile_error message."
echo " Expected: 'pick exactly one cluster — --features mainnet OR --features devnet'"
exit 1
fi
echo "✓ no-feature build correctly rejected by compile_error!"

- name: Verify both-features build fails
working-directory: program
run: |
set +e
cargo build-sbf --features mainnet --features devnet > /tmp/build-both.log 2>&1
BUILD_EXIT=$?
set -e
cat /tmp/build-both.log
if [ "$BUILD_EXIT" -eq 0 ]; then
echo "ERROR: cargo build-sbf with BOTH features succeeded."
echo " The compile_error! mutual-exclusion guard is broken."
exit 1
fi
if ! grep -q "pick exactly one cluster" /tmp/build-both.log; then
echo "ERROR: build failed but not with the expected compile_error message."
exit 1
fi
echo "✓ both-features build correctly rejected"
26 changes: 25 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,25 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

### Added

- End-to-end vitest tests for session-action enforcement (`tests-sdk/tests/12-actions.test.ts`, 9 cases): `programWhitelist` allow + reject (3021), `programBlacklist` allow + reject (3022), `solMaxPerTx` allow at-cap + reject over-cap (3023), `solLimit` lifetime budget exhaustion (3024), and combined-rules enforcement. Runs against a live `solana-test-validator` with the foundation binary loaded and uses `@lazorkit/sdk-legacy`'s `Actions` builder to dogfood the full encode → on-chain enforce path.
- `docs/audit/` artifacts for an Accretion delta-audit follow-up: `DELTA_BRIEF.md` summarises the changes from the previous audited baseline by phase with explicit audit asks; `program-src.diff` is the full unified diff of `program/`; `program-src.diff.stat` is a per-file changed-line summary; `upstream-parity.txt` reports byte-identity vs the already-audited `lazorkit-protocol` per file (13/19 changed files identical).
- Local git tags `audit-baseline-2026-02-accretion` (previous Accretion-audited state, commit `d1eaaeb`) and `audit-pending-v1` (the current consolidated state ready for delta review).
- Session action permissions: 8 immutable permission rules attachable at session creation — `SolLimit`, `SolRecurringLimit`, `SolMaxPerTx`, `TokenLimit`, `TokenRecurringLimit`, `TokenMaxPerTx`, `ProgramWhitelist`, `ProgramBlacklist`. Action discriminators (1, 2, 3, 4, 5, 6, 10, 11) and the 11-byte header layout match `lazorkit-protocol` so the unified SDK can encode actions identically for both builds.
- `SessionAccount` is now variable-size: a session can carry a trailing action buffer (max 16 actions, ≤ 2048 bytes) validated at creation time.
- `CreateSession` instruction data accepts the new `[actions_len: u16][actions: N]` extension after the legacy 40-byte args; old 40-byte clients continue to work via the legacy parser branch.
- Pre-CPI action enforcement at `Execute` time: program whitelist/blacklist checks against each CPI target.
- Post-CPI action enforcement: SOL/token spending caps with saturating arithmetic; recurring-window resets aligned to slot boundaries; per-execute SOL outflow tracked across all CPIs for `SolMaxPerTx`.
- Vault-invariant defenses against `System::Assign` / `SetAuthority` / `Approve` escapes: vault owner + data-length snapshotted pre-CPI and verified unchanged post-CPI; vault-owned token accounts on listed mints have their owner / delegate / close_authority fields snapshotted and verified.
- Anti-CPI guard for session-authenticated `Execute`: stack-height must be 1 (rejects wrapper programs chaining through `Execute`).
- Error codes 3020–3029 (action validation + enforcement) and 3030–3032 (`SessionVaultOwnerChanged`, `SessionVaultDataLenChanged`, `SessionTokenAuthorityChanged`).
- Dual-cluster Cargo features (`mainnet`, `devnet`): the embedded program ID is chosen at compile time via a feature flag with a `compile_error!` if neither / both is set. The `mainnet` feature embeds `LazorjRFNavitUaBu5m3WaNPjU1maipvSW2rZfAFAKi` (same slot as `lazorkit-protocol`) for the foundation deployment; `devnet` keeps `FLb7fyAtkfA4TSa2uYcAT8QKHd2pkoMHgmqfnXFXo7ao`.
- `security.txt` block embedded via `solana-security-txt` macro: links to SECURITY.md, contact email, source repo, source revision (from `GITHUB_SHA`), and the Accretion audit PDF.
- Zero-copy `CompactInstructionRef` parser (`parse_compact_instructions_ref_with_len`) used by the Execute hot path — no per-instruction `Vec<u8>` allocations for account-index bytes or instruction data.
- Cherry-pick guardrails: `scripts/fee-paths.txt` declares forbidden fee-surface paths and symbols, `scripts/check-no-fee.sh` verifies the working tree (used by CI), `scripts/strip-fee.sh` auto-removes fee files post-cherry-pick.
- CI workflow `check-no-fee` runs the verifier on every PR.
- CI workflow `sbf-cluster-check` builds both mainnet and devnet SBF binaries, verifies their hashes differ, and asserts that an unflagged `cargo build-sbf` fails with the expected `compile_error!`.
- `scripts/build-all.sh <devnet|mainnet>` now drives a feature-flagged build + IDL regen + SDK regen in one step. The previous `scripts/sync-program-id.sh` is removed (program ID is now a compile-time feature, not a sed target).
- `solana-security-txt` and `default-env` dependencies, `[workspace.metadata.cli]` pinning Solana CLI 3.0.4 for verified builds.
- Unified SDK API with discriminated union signer types (`ed25519()`, `secp256r1()`, `session()` helper constructors)
- `CreateWalletOwner` union type: single `createWallet()` method for both Ed25519 and Secp256r1
- `AdminSigner` union type for admin operations (addAuthority, removeAuthority, transferOwnership, createSession)
Expand Down Expand Up @@ -36,7 +55,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- Odometer counter replay protection for Secp256r1 (monotonic u32 per authority)
- program_id included in challenge hash (cross-program replay prevention)
- rpId stored on authority account at creation (saves ~14 bytes per transaction)
- TypeScript SDK (`sdk/solita-client`) with Solita code generation
- TypeScript SDK: standardised on `@lazorkit/sdk-legacy` (lives in sibling `lazorkit-protocol` repo); the in-tree `sdk/solita-client` has been removed
- Integration + security test suite (`tests-sdk/`) with 56 tests across 11 files
- Benchmark script for CU and transaction size measurements
- CompactInstructions accounts hash for anti-reordering protection
Expand All @@ -51,6 +70,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

### Changed

- Secp256r1 auth payload format: replaces the older `typeAndFlags` byte at `auth_payload[13]` with full raw `clientDataJSON` embedded in the payload. The on-chain auth verifier now parses the JSON directly rather than reconstructing it from `typeAndFlags + rpId`. Aligns with `lazorkit-protocol` byte-for-byte and is required for binary-swap compatibility at the shared mainnet slot.
- Secp256r1 authority on-chain layout: replaces the previously stored variable-length raw `rpId` with a precomputed 32-byte `rpIdHash` (SHA-256 digest computed at registration). New layout: `header(48) + cred_hash(32) + pubkey(33) + rpIdHash(32) = 145 bytes`. Saves one `sol_sha256` syscall per `Execute`. Existing wallets created on the upstream commercial binary remain readable after binary swap.
- Shank IDL declarations on the `ProgramIx` enum (account metadata: `writable` modifiers, account positions, descriptions) resynced with `lazorkit-protocol`. Five fee-related variants (disc 10–14: `InitializeProtocol`, `UpdateProtocol`, `RegisterPayer`, `WithdrawTreasury`, `InitializeTreasuryShard`) stripped — `program-v2` keeps disc 0–9 only. Runtime not affected (`@lazorkit/sdk-legacy` uses hand-written builders rather than the generated IDL).
- SDK API: unified all methods via discriminated unions (breaking: removed `createWalletEd25519`, `createWalletSecp256r1`, `addAuthoritySecp256r1`, `removeAuthoritySecp256r1`, `executeEd25519`, `executeSecp256r1`, `executeSession`, `createSessionSecp256r1`, `transferOwnershipSecp256r1`, `authorizeSecp256r1`)
- SDK API: all methods now return `{ instructions: TransactionInstruction[]; ...extraPdas }` consistently
- SDK API: `createSession` now takes `sessionKey: PublicKey` instead of `Uint8Array`
Expand All @@ -71,6 +93,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

### Fixed

- `tests-sdk` integration tests now pass `PROGRAM_ID` explicitly to the `LazorKitClient` constructor. `@lazorkit/sdk-legacy`'s URL-based program-ID inference defaulted localhost to the commercial devnet ID (`4h3X…`); against a local validator loading the foundation binary at the keypair's pubkey this caused all txs to fail with "Attempt to load a program that does not exist". `tests/common.ts` now resolves `PROGRAM_ID` from (1) `PROGRAM_ID` env override, (2) the keypair file at `target/deploy/lazorkit_program-keypair.json`, or (3) the foundation devnet fallback `FLb7…`.
- `tests-sdk/tests/08-deferred.test.ts` builds the `Authorize` `signed_payload` as `instructions_hash || accounts_hash || expiry_offset (u16 LE)` to match what the on-chain verifier hashes. The test code was missing the 2-byte expiry buffer at all 6 sign sites, causing all 7 deferred tests to fail with `InvalidMessageHash` (3005). After the fix, all 65 vitest E2E tests pass against a live validator.
- Authorize signed payload now includes `expiry_offset` (66 bytes total), preventing relayers from modifying the expiry window
- `sol_assert_bytes_eq` now uses the `len` parameter instead of `left.len()` (latent OOB read on-chain)
- `reclaim_deferred` uses `checked_add` for lamports (consistent with `execute_deferred` and `manage_authority`)
Expand Down
Loading
Loading