docs: privacy-preserving SPEL programs guide#86
Open
jimmy-claw wants to merge 90 commits into
Open
Conversation
…co#6) The #[account(signer)] and #[account(init)] constraints now generate runtime validation functions that are called before instruction dispatch: - signer: checks is_authorized flag, returns NssaError::Unauthorized - init: checks account == Account::default(), returns AccountAlreadyInitialized Validation functions are named __validate_{instruction_name} and called in the generated match arms. Includes 5 integration tests covering: - Authorized signer passes - Unauthorized signer fails with correct error - Uninitialized account passes init check - Already initialized account fails - Both checks run in order (init before signer) Closes #4 Co-authored-by: Jimmy <jimmy-claw@users.noreply.github.com>
…n = "path")] (logos-co#5) (logos-co#7) Programs can now bring their own Instruction enum instead of having the macro generate one: #[nssa_program(instruction = "my_crate::Instruction")] mod my_program { ... } When set, the macro generates `use path as Instruction;` instead of deriving its own enum. This allows shared-type patterns where the Instruction enum lives in a core crate used by both on-chain and CLI. Includes 2 tests verifying external instruction serialization and handler integration. Closes logos-co#5 Co-authored-by: Jimmy <jimmy-claw@users.noreply.github.com>
The CLI PDA computation now handles all three seed types: - const, account, arg (bytes32, u64, u128, string) Multi-seed PDAs combined via XOR. 6 unit tests. Partially addresses #1 Co-authored-by: Jimmy <jimmy-claw@users.noreply.github.com>
…ta> (logos-co#9) * feat: support variable-length account lists via Vec<AccountWithMetadata> (#3) Instructions can now accept a trailing Vec<AccountWithMetadata> parameter for variable-length account lists (e.g., member lists in multisig): #[instruction] pub fn create_multisig( #[account(init, pda = ...)] state: AccountWithMetadata, members: Vec<AccountWithMetadata>, threshold: u64, ) -> NssaResult { ... } Changes: - Macro: detect Vec<AccountWithMetadata> params, generate split_at destructuring - IDL: add "rest":true field to variable-length accounts - Core: add rest field to IdlAccountItem with serde skip_serializing_if Includes 4 tests for IDL serialization/deserialization of rest field. Closes #3 * docs: add variable-length account list to README --------- Co-authored-by: Jimmy <jimmy-claw@users.noreply.github.com>
- Make __validate_* functions pub so they are accessible from generated main() - Add missing rest field to IdlAccountItem in generate_idl_fn - Use .expect() instead of ? for validation calls (main returns (), not Result) These bugs caused scaffolded projects to fail compilation.
…main (logos-co#13) - Switch all Cargo.toml git deps to branch = "main" - Update scaffolded project template in init.rs - Fix type mismatch in tx.rs: get_pub_account_signing_key now takes AccountId by value This aligns nssa-framework with the latest lssa main branch, enabling use of sequencer_runner for e2e testing. Co-authored-by: Jimmy <jimmy-claw@users.noreply.github.com>
Binary is at methods/guest/target/... not target/... Co-authored-by: Jimmy <jimmy-claw@users.noreply.github.com>
- Auto-detect sequencer_runner from PATH, ~/bin, or ~/lssa/target - Start sequencer with lssa configs, clean state - Deploy scaffolded program via nssa-cli deploy - Attempt transaction submit (graceful failure if args needed) - Proper cleanup on exit Co-authored-by: Jimmy <jimmy-claw@users.noreply.github.com>
…odegen (logos-co#15) Also fix Vec account validation call to spread rest accounts instead of trying to put Vec into array literal. Co-authored-by: Jimmy <jimmy-claw@users.noreply.github.com>
Rename all crates and types: - nssa-framework → lez-framework - nssa-framework-core → lez-framework-core - nssa-framework-macros → lez-framework-macros - nssa-framework-cli → lez-cli - NssaOutput → LezOutput, NssaError → LezError, NssaResult → LezResult - NssaIdl → LezIdl - nssa_program macro → lez_program macro - nssa-cli binary → lez-cli binary Upstream deps (nssa_core, nssa from logos-blockchain/lssa) unchanged. Co-authored-by: Jimmy Claw <jimmy-claw@users.noreply.github.com>
…s-co#20) Implements client and C FFI code generation from LEZ program IDL JSON. - Typed Rust client with async methods per instruction - Correct account ordering from IDL (fixes hand-written FFI bugs) - PDA computation helpers generated from IDL seed specs - C FFI exports (extern "C" JSON-in/JSON-out pattern) - C header file generation - Proper type handling: - ProgramId [u32;8] with little-endian byte order - AccountId with base58 (native) + hex fallback - CLI tool: lez-client-gen --idl <path> --out-dir <dir> - 7 unit tests covering codegen, FFI, headers, account order Closes logos-co#19 Co-authored-by: Jimmy Claw <jimmy@claw.dev>
Extends the LEZ IDL format with fields from the lssa-lang IDL spec (discriminator, execution, variant, visibility, spec, metadata). All new fields are Optional/defaulted — fully backward compatible.
Wraps the #[lez_program] macro generated fn main() in #[cfg(not(test))] so guest programs can have inline #[cfg(test)] unit tests without hitting risc0 guest syscalls on the host. Closes logos-co#21
Add AccountId alongside ProgramId as a known primitive type in the macro, mapping to "account_id" in the IDL. Also update lez-client-gen/src/util.rs to match "account_id" (snake_case) so client-gen correctly handles IDLs produced by the updated macro. Original fix by @danisharora099 in logos-co#26. Closes logos-co#24
…logos-co#29) Adds end-to-end tests covering the full LEZ program development workflow: - Fixture program with initialize + transfer instructions and inline unit tests - e2e_build: cargo build the fixture - e2e_idl_generation: extract and validate IDL JSON - e2e_ffi_build: run lez-client-gen, assert client/FFI/header output - e2e_test: cargo test the fixture (validates cfg-gate fix from logos-co#25) CI split into unit-tests (fast) and e2e-tests (with logos-blockchain-circuits). Closes logos-co#27
…logos-co#30) Co-authored-by: Jimmy Claw <jimmy@claw.dev>
…ogos-co#32) * feat(ffi-codegen): emit tx-building FFI that calls generated client (closes issue logos-co#20 pattern) Previously ffi_codegen.rs emitted stub FFI functions that only returned {success: true, account_ids: [...], instruction_name: "..."} without actually building or submitting any transaction. Now the generated FFI: - Imports the generated client module via `use super::client::*` - Parses common connection args (sequencer_url, wallet_path, program_id) - Parses instruction-specific args and accounts from JSON - Builds the `{Instruction}Accounts` struct - Instantiates `{Program}Client::new(&wallet, program_id)` - Drives the async client method with `tokio::runtime::Runtime::new().block_on(...)` - Returns {"success": true, "tx_hash": "..."} on success This matches the pattern used in the hand-written lez-multisig-ffi/src/multisig.rs. The key JSON field for the program id is now the uniform `program_id` (not prefixed). Updated tests: - test_ffi_generation: checks for client import, tokio runtime, block_on, tx_hash - test_account_order_in_client: moved ordering check to client (where it lives now) - test_ffi_calls_client_methods: new test verifying client method calls are emitted - Removed obsolete checks for compute_pda/from_le_bytes in FFI (now in client) * feat: ffi_codegen emits full transaction building via WalletCore (logos-co#31) Generated FFI now includes: - Instruction enum (all variants with typed fields) - WalletCore init (wallet_path + sequencer_url from JSON args) - PDA derivation via SHA-256 (inline, self-contained) - Full async transaction: Message::try_new -> WitnessSet -> send_tx_public - tokio::runtime::Runtime blocking wrapper - Returns {"success": true, "tx_hash": "..."} JSON Generated FFI is now self-contained — no dependency on the generated Rust client module. Any LEZ program can get a fully functional C FFI from its IDL alone, with zero manual code. Closes logos-co#31 * feat: add parse_program_id helper for ProgramId-typed instruction args Generates a parse_program_id() fn (alias for parse_program_id_hex) so that IDL args typed as ProgramId are correctly parsed from hex strings in FFI. * ffi_codegen: fix APIs + add instruction_type support - Add instruction_type: Option<String> to LezIdl — lets programs specify their native instruction type (e.g. multisig_core::Instruction) so codegen imports it directly instead of generating a local enum - Fix WalletCore::from_env() instead of non-existent ::new(url) - Fix AccountId::new(arr) instead of ::from(arr) - Fix error_json to use format! with properly escaped braces - Closes logos-co#31 * ffi_codegen: fix error_json format string brace escaping The generated error_json function now uses a split approach to avoid nested brace escaping issues in format! strings. * fix(macros): add instruction_type: None to LezIdl struct literal in __program_idl macro --------- Co-authored-by: Jimmy Claw <jimmy@claw.dev>
Switch from branch="main" to pinned rev to ensure reproducible builds and alignment with logos-scaffold template default lssa_pin.
fix: pin lssa deps to dee3f7fa (matches logos-scaffold template)
…os-co#35) * fix: emit instruction_type in IDL when external instruction enum is used When #[lez_program(instruction = "some::Path")] is used, the generated IDL JSON and __program_idl() function now correctly populate instruction_type. Previously instruction_type was always None/missing, breaking FFI codegen which relies on this field to know the external instruction enum path. Fixes: - generate_idl_fn: emit instruction_type = Some(path) in __program_idl() - generate_idl_json: append ,"instruction_type":"..." to JSON output - expand_generate_idl: detect instruction= attr from source file, pass it through * fix: include rest:true in generated IDL JSON for Vec<AccountWithMetadata> params Previously Vec<AccountWithMetadata> parameters (variable-length trailing accounts) were correctly detected as rest accounts in the code generation (is_rest: true) but the rest field was omitted from the generated JSON IDL. Now generates "rest":true in the JSON for these accounts, consistent with the IdlAccountItem struct which has a rest: bool field. * fix: pin fixture_program nssa_core to rev=767b5afd (was branch=main, causing duplicate dep) --------- Co-authored-by: Jimmy Claw <jimmy@claw.dev>
) Add generate_pda_helpers(idl: &LezIdl) -> String to ffi_codegen.rs. The function emits one pub fn compute_{account}_pda(...) per unique account that has a pda field in the IDL. Seed handling: - const seeds: inlined as UTF-8 bytes, padded to 32 bytes with 0x00 - arg seeds: become function parameters (e.g. create_key: &[u8; 32]) - account seeds: TODO comment, skipped for now Multi-seed PDAs (>1 seed) use SHA-256(seed1 || seed2 || ...) to combine seeds into a single 32-byte value — matching the on-chain nssa derivation and lez-cli/src/pda.rs behaviour. generate_ffi() calls generate_pda_helpers() and appends the result so PDA helpers are part of the generated FFI output. Tests added in tests.rs: - test_pda_helpers_single_arg_seed: single arg seed generates direct PdaSeed (no SHA-256) - test_pda_helpers_multi_seed: const+arg seeds use SHA-256 combiner - test_pda_helpers_deduplication: same account across two instructions generates exactly one helper function - test_pda_helpers_in_ffi_output: PDA helpers appear in generate_ffi() output All 12 tests pass (cargo test -p lez-client-gen). Co-authored-by: jimmy-claw <jimmy-claw@users.noreply.github.com>
* fix: emit instruction_type in IDL when external instruction enum is used When #[lez_program(instruction = "some::Path")] is used, the generated IDL JSON and __program_idl() function now correctly populate instruction_type. Previously instruction_type was always None/missing, breaking FFI codegen which relies on this field to know the external instruction enum path. Fixes: - generate_idl_fn: emit instruction_type = Some(path) in __program_idl() - generate_idl_json: append ,"instruction_type":"..." to JSON output - expand_generate_idl: detect instruction= attr from source file, pass it through * fix: include rest:true in generated IDL JSON for Vec<AccountWithMetadata> params Previously Vec<AccountWithMetadata> parameters (variable-length trailing accounts) were correctly detected as rest accounts in the code generation (is_rest: true) but the rest field was omitted from the generated JSON IDL. Now generates "rest":true in the JSON for these accounts, consistent with the IdlAccountItem struct which has a rest: bool field. * fix: pin fixture_program nssa_core to rev=767b5afd (was branch=main, causing duplicate dep) * feat: lez-client-gen supports u64 arg seeds in PDA helpers Adds support for u64-typed arg seeds in generate_pda_helpers. Previously only [u8;32] args were supported. u64 seeds are converted via .to_le_bytes() before being included in the SHA-256 computation, matching the manual implementation in lez-multisig-framework. - u64 params are now passed by value (not by ref) in generated signatures - Single u64 seed: padded into [u8; 32] via to_le_bytes() - Multi u64 seed: hashed via hasher.update(&val.to_le_bytes()) - Adds tests: test_pda_helpers_u64_single_seed, test_pda_helpers_u64_multi_seed --------- Co-authored-by: Jimmy Claw <jimmy@claw.dev>
Co-authored-by: Jimmy Claw <jimmy@claw.dev>
* feat(lez-cli): add pda subcommand to compute PDAs from IDL (closes logos-co#46) * feat(lez-cli): add pda subcommand to compute PDAs from IDL Usage: <binary> --idl <IDL> --program <BIN> pda <account-name> [--<seed-arg> <value>] Example: multisig --idl multisig_idl.json --program multisig.bin pda vault --create-key demo-abc123 Closes logos-co#46 * fix(lez-cli): pda command accepts --program-id hex instead of requiring binary * fix(lez-cli): pda command accepts --program-id hex, no binary required * feat(lez-cli): --program-id as global top-level flag, alternative to --program * fix(lez-cli): --program-id skips binary loading for tx submission too --------- Co-authored-by: Jimmy Claw <jimmy@claw.dev>
…ogos-co#49) Usage: pda --program-id <hex> <seed1> [seed2] ... No IDL needed. Seeds are hex (32 bytes) or strings (zero-padded). Combined via SHA-256(seed1||seed2||...) matching on-chain derivation. Example: multisig pda --program-id abc123... multisig_vault__ <create_key_hex> Co-authored-by: Jimmy Claw <jimmy@claw.dev>
feat(client-gen): generate PDA compute and state query helpers
chore: add MIT and Apache-2.0 license files
Renames framework crates only. The #[lez_program] macro keeps its name since it refers to LEZ programs, not the SPEL framework itself.
rename: lez-* crates to spel-* (fixes logos-co#57)
- Update all LEZ git URLs: lssa -> logos-execution-zone - Bump LEZ rev to ffcbc15972adbf557939bf3e2852af276422631b - Fix spel-cli: send_tx_public -> send_transaction(NSSATransaction::Public) - Fix spel-client-gen codegen: updated API + RpcClient import - Fix spel-framework-macros: use ProgramOutput builder - Update smoke-test.sh: sequencer_service, new config paths - Add weekly LEZ compatibility CI workflow - Add test-spel-e2e/target/ to .gitignore - Update fixture program LEZ dep
…st-clean feat: update to latest LEZ (logos-execution-zone ffcbc159)
…ction (logos-co#92) * feat(spel-cli): detect Private/ prefix, build PrivacyPreservingTransaction Rebased on main after logos-co#91 (LEZ update). Conflicts resolved in tx.rs: - Kept send_transaction(NSSATransaction::Public) from logos-co#91 - Fixed privacy path: response.tx_hash -> hex::encode(response.0) - Fixed TxPoller: config().clone() -> config() - Added hex dep to Cargo.toml * ci: trigger conflict check * test: add unit tests for parse_account_id + privacy smoke script - 5 unit tests for Private/ prefix detection in hex.rs - scripts/smoke-test-privacy.sh: E2E privacy TX test with RISC0_DEV_MODE=1 * fix(init): update scaffold template to use logos-execution-zone Replace old lssa.git rev 767b5afd with logos-execution-zone.git rev ffcbc159 * fix(init): add std feature to risc0-zkvm in guest template Fixes getrandom build errors in fresh scaffolds. * test: add privacy smoke test script * ci: add privacy smoke test job * ci: add sequencer caching to privacy smoke test Cache sequencer_service binary keyed on LEZ commit hash. Builds only on cache miss - saves ~5-10 min per run. * ci: add risc0 toolchain to privacy smoke test * fix(smoke): unset RISC0_SKIP_BUILD during guest build * fix(smoke): print build.log on failure for debugging * fix(smoke): fix SpelError conversion in guest program * fix(smoke): pass greeting as JSON array for Vec<u8> arg * fix(smoke): use comma-separated bytes not JSON array for Vec<u8> * fix(smoke): use wallet-generated private accounts for ZK proof * fix(smoke): pipe WALLET_PASSWORD to wallet account new private * test(smoke): add auth-transfer init + proper end-to-end privacy TX test Full flow: scaffold → build → IDL → deploy → public TX → auth-transfer init → private TX * fix(smoke): only write data to default accounts, return unchanged for auth-transfer owned * fix(smoke): add 20s warm-up after sequencer starts * fix(smoke): poll for first block instead of fixed sleep * fix(ci): extract LEZ commit from Cargo.lock, checkout correct rev * fix(ci): generate Cargo.lock before extracting LEZ commit * fix(ci): correct LEZ extraction from Cargo.lock source line * fix(ci): properly escape sed capture groups in LEZ extraction * fix(ci): use cut instead of sed for LEZ extraction * fix(ci): use grep -o rev=[^#]* | cut -d= -f2 for LEZ extraction * fix(ci): hardcode LSSA_REF to match SPEL dependency * fix(ci): extract LEZ commit from Cargo.toml --------- Co-authored-by: Jimmy Claw <jimmy@claw.dev>
Co-authored-by: Jimmy Claw <jimmy@claw.dev>
…s-co#96) * fix(release): install logos-blockchain-circuits before build * fix(release): embed changelog in release body --------- Co-authored-by: Jimmy Claw <jimmy@claw.dev>
…o#97) Co-authored-by: Jimmy Claw <jimmy@claw.dev>
Co-authored-by: Jimmy Claw <jimmy@claw.dev>
Co-authored-by: Jimmy Claw <jimmy@claw.dev>
…-co#100) Co-authored-by: Jimmy Claw <jimmy@claw.dev>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
…rge (logos-co#102) Co-authored-by: Jimmy Claw <jimmy@claw.dev>
Co-authored-by: Jimmy Claw <jimmy@claw.dev>
…ction (logos-co#82) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
4055b13 to
50baf30
Compare
vpavlin
reviewed
Apr 1, 2026
| @@ -0,0 +1,119 @@ | |||
| # Privacy-Preserving Programs with SPEL | |||
|
|
|||
| SPEL programs are **privacy-agnostic** — the same program code works identically with both public and private accounts. Privacy is handled at the transaction layer, not the program layer. | |||
vpavlin
reviewed
Apr 1, 2026
|
|
||
| ### 2. Call any SPEL instruction with a private account | ||
|
|
||
| Simply pass the `Private/` prefixed account ID — `spel` detects it automatically and builds a `PrivacyPreservingTransaction`: |
Collaborator
There was a problem hiding this comment.
Is the type still PrivacyPreservingTransaction?
vpavlin
reviewed
Apr 1, 2026
|
|
||
| No special annotations needed. A simple program works with both: | ||
|
|
||
| ```rust |
Collaborator
There was a problem hiding this comment.
Is the program still correct?
vpavlin
reviewed
Apr 1, 2026
| wallet account get --account-id ... # read decrypted data | ||
| ``` | ||
|
|
||
| ## IDL Privacy Metadata (optional) |
Collaborator
There was a problem hiding this comment.
Did we actually do this? I think we said it does not make sense
vpavlin
reviewed
Apr 1, 2026
|
|
||
| ## Related | ||
|
|
||
| - [LEZ Privacy Technical Deep Dive](lez/lez-privacy-technical-deep-dive.md) |
vpavlin
reviewed
Apr 1, 2026
| ## Related | ||
|
|
||
| - [LEZ Privacy Technical Deep Dive](lez/lez-privacy-technical-deep-dive.md) | ||
| - [Private Multisig (LP-0002)](lez/lp-0002-rfc.md) |
vpavlin
reviewed
Apr 1, 2026
|
|
||
| - [LEZ Privacy Technical Deep Dive](lez/lez-privacy-technical-deep-dive.md) | ||
| - [Private Multisig (LP-0002)](lez/lp-0002-rfc.md) | ||
| - [SPEL PR #83](https://github.com/logos-co/spel/pull/83) — `Private/` prefix auto-detection |
Collaborator
There was a problem hiding this comment.
I don't thinks we need to reference this either
added 2 commits
April 1, 2026 14:07
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds docs/privacy.md explaining how to use private accounts with SPEL programs.
Key points:
Addresses spel-privacy-1 from the privacy integration plan.