Skip to content

docs: privacy-preserving SPEL programs guide#86

Open
jimmy-claw wants to merge 90 commits into
logos-co:mainfrom
jimmy-claw:jimmy/privacy-docs
Open

docs: privacy-preserving SPEL programs guide#86
jimmy-claw wants to merge 90 commits into
logos-co:mainfrom
jimmy-claw:jimmy/privacy-docs

Conversation

@jimmy-claw

Copy link
Copy Markdown
Contributor

Adds docs/privacy.md explaining how to use private accounts with SPEL programs.

Key points:

Addresses spel-privacy-1 from the privacy integration plan.

jimmy-claw and others added 30 commits February 20, 2026 12:59
…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
…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>
* 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>
vpavlin and others added 19 commits March 20, 2026 10:55
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.
- 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>
Co-authored-by: Jimmy Claw <jimmy@claw.dev>
Co-authored-by: Jimmy Claw <jimmy@claw.dev>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
…ction (logos-co#82)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@jimmy-claw jimmy-claw force-pushed the jimmy/privacy-docs branch from 4055b13 to 50baf30 Compare April 1, 2026 13:58
Comment thread docs/privacy.md
@@ -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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LEZ programs

Comment thread docs/privacy.md

### 2. Call any SPEL instruction with a private account

Simply pass the `Private/` prefixed account ID — `spel` detects it automatically and builds a `PrivacyPreservingTransaction`:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the type still PrivacyPreservingTransaction?

Comment thread docs/privacy.md

No special annotations needed. A simple program works with both:

```rust

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the program still correct?

Comment thread docs/privacy.md Outdated
wallet account get --account-id ... # read decrypted data
```

## IDL Privacy Metadata (optional)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did we actually do this? I think we said it does not make sense

Comment thread docs/privacy.md Outdated

## Related

- [LEZ Privacy Technical Deep Dive](lez/lez-privacy-technical-deep-dive.md)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this?

Comment thread docs/privacy.md Outdated
## Related

- [LEZ Privacy Technical Deep Dive](lez/lez-privacy-technical-deep-dive.md)
- [Private Multisig (LP-0002)](lez/lp-0002-rfc.md)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove

Comment thread docs/privacy.md Outdated

- [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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't thinks we need to reference this either

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.

4 participants