Repository navigation
feat(lee,lez)!: add private execution of immutable programs and shadow programs - #824
Merged
Merged
Conversation
jonesmarvin8
marked this pull request as draft
September 2, 2026 20:41
jonesmarvin8
force-pushed
the
marvin/remove-program-deployment
branch
3 times, most recently
from
September 3, 2026 21:26
08385cb to
aa10df2
Compare
jonesmarvin8
changed the base branch from
marvin/remove-program-deployment
to
dev
September 8, 2026 20:35
jonesmarvin8
pushed a commit
that referenced
this pull request
Sep 8, 2026
… programs Reimplements the marvin/privacy-and-shadow-executions feature (PR #824) as a single commit directly on current dev, rather than rebasing the branch's full 84-commit history: the vast majority of that history was foundational program-as-account/program_loader plumbing that has since landed on dev independently through other means, leaving only this feature's own ~850-line delta as genuinely new work. Once a program's header becomes permanently immutable (immutable: true, set at CreateHeader or flipped by UpdateHeader), program_loader_core emits a commitment mirroring its finalized ProgramHeader directly into private state - no proof, nullifier, or ciphertext needed, since every validating node can independently recompute it from the same public transaction. The mirror is seeded by the header's own account_id, so the commitment anchors a specific deployment rather than any header with coincidentally identical data. ProgramImageClaim splits into Public (today's public lookup) and Private (anchored against the emitted commitment via a membership proof instead). The sequencer independently reconstructs either claim before accepting a proof, on both the deploy-time write path and the later privacy-transaction verify path. Shadow programs add a third way to reference a program in a privacy- preserving transaction: one never deployed anywhere, public or private. AccountId::for_shadow_program derives its dispatch address purely from its image_id, domain-separated from every other PDA formula. Its identity is established fresh, in-circuit, on every dispatch from an ELF supplied as a witness. ProgramWithDependencies gains as_shadow_program/with_shadow_dependency and as_private_program/with_private_dependency so callers can opt a program into either path. Every API this touches was independently reshaped on dev since the original branch forked (AccountPostState/Claim -> AccountStateDiff/BalanceDiff, the ProgramWithDependencies.self_account_id rename, program_loader_core's Data/ProgramHeader/ProgramSegment codec), so this is a from-scratch port against dev's current shapes rather than a mechanical cherry-pick. Rebuilds every guest artifact and the prebuilt sequencer test fixture, since lee_core (compiled into every guest) changed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
jonesmarvin8
force-pushed
the
marvin/privacy-and-shadow-executions
branch
from
September 8, 2026 21:39
dfb3c5f to
5cc58e1
Compare
jonesmarvin8
pushed a commit
that referenced
this pull request
Sep 9, 2026
… programs Reimplements the marvin/privacy-and-shadow-executions feature (PR #824) as a single commit directly on current dev, rather than rebasing the branch's full 84-commit history: the vast majority of that history was foundational program-as-account/program_loader plumbing that has since landed on dev independently through other means, leaving only this feature's own delta as genuinely new work. Once a program's header becomes permanently immutable (immutable: true, set at CreateHeader or flipped by UpdateHeader), program_loader_core emits a commitment mirroring its finalized ProgramHeader directly into private state - no proof, nullifier, or ciphertext needed, since every validating node can independently recompute it from the same public transaction. The mirror is seeded by the header's own account_id, so the commitment anchors a specific deployment rather than any header with coincidentally identical data. ProgramImageClaim splits into Public (today's public lookup) and Private (anchored against the emitted commitment via a membership proof instead). The sequencer independently reconstructs either claim before accepting a proof, on both the deploy-time write path and the later privacy-transaction verify path. Shadow programs add a third way to reference a program in a privacy- preserving transaction: one never deployed anywhere, public or private. AccountId::for_shadow_program derives its dispatch address purely from its image_id, domain-separated from every other PDA formula. Its identity is established fresh, in-circuit, on every dispatch from an ELF supplied as a witness. ProgramWithDependencies gains as_shadow_program/with_shadow_dependency and as_private_program/with_private_dependency so callers can opt a program into either path. Every API this touches was independently reshaped on dev since the original branch forked (AccountPostState/Claim -> AccountStateDiff/BalanceDiff, the ProgramWithDependencies.self_account_id rename, program_loader_core's Data/ProgramHeader/ProgramSegment codec), so this is a from-scratch port against dev's current shapes rather than a mechanical cherry-pick. Rebuilds every guest artifact and the prebuilt sequencer test fixture, since lee_core (compiled into every guest) changed. Trims the test suite to what's actually load-bearing: drops the unused, dead force_insert_commitment test helper and 3 of 4 for_shadow_program tests that were fully implied by the remaining pinned-value test, adds a regular-private-account variant of the shadow-program-ownership test so a future regression can be isolated to PDA binding vs. shadow-identity resolution, and trims added doc/inline comments for concision.
jonesmarvin8
force-pushed
the
marvin/privacy-and-shadow-executions
branch
from
September 9, 2026 18:00
fdeea79 to
48bc7b9
Compare
jonesmarvin8
requested review from
Arjentix,
Pravdyvy,
agureev,
erhant,
moudyellaz and
schouhy
September 9, 2026 19:55
jonesmarvin8
marked this pull request as ready for review
September 9, 2026 19:55
moudyellaz
reviewed
Sep 11, 2026
jonesmarvin8
pushed a commit
that referenced
this pull request
Sep 11, 2026
…d in-circuit Addresses PR #824 review feedback: - ProgramImageClaim::Private previously echoed the full account_id and ProgramHeader into the public journal, and had its "membership" checked by the sequencer doing a local existence lookup rather than a real Merkle proof — so it leaked more than Public claims while proving nothing. Splits the circuit input into ProgramImageWitness (carries account_id/program_header/ membership_proof, never published) and a trimmed ProgramImageClaim (Private now only publishes a commitment-tree root), with membership verified in-circuit the same way private account updates already are. The sequencer now only checks that root is one the tree has actually had (V03State::is_known_commitment_root, shared with check_nullifiers_are_valid). - Shadow program witnesses were resolved (ELF decoded and hashed) lazily, once per dispatch, so a shadow program invoked N times in one call graph paid the cost N times. Resolution now happens once per witness up front, merged into image_id_by_account_id alongside the claims. - Adds a negative test asserting a shadow witness whose ELF doesn't hash to its declared account_id is rejected even if the call graph never dispatches it, closing the gap the PR's own Future Work list flagged. Relocates the immutable-mirror commitment derivation from program_loader_core into lee_core (program_loader_core re-exports it), since the circuit needs it too and can't depend on a specific program's core crate. Rebuilds affected RISC0 artifacts and the test fixture.
moudyellaz
approved these changes
Sep 17, 2026
agureev
requested changes
Sep 18, 2026
agureev
left a comment
Contributor
There was a problem hiding this comment.
Generally very cool! Very smart with the domain separator for shadow programs, makes sense, even if I am unsure how viable it is in terms of computation.
Also left a bunch of abstarction/doc comments/suggestions. Doc comments are merely suggestions, feel free to skip, but some parts I feel would be good like getting the benchmarks in and removing some redundant/unstructured data.
Thank you beforehand!
This was referenced Sep 19, 2026
jonesmarvin8
pushed a commit
that referenced
this pull request
Sep 22, 2026
…ependency Addresses agureev's PR #824 review comment: ProgramWithDependencies tracked each account's Public/Shadow/Private status across three independent collections (dependencies, shadow_account_ids, private_program_headers), so an account could end up in more than one of them at once with no coherent meaning, never enforced anywhere. Introduces ProgramKind (Public/Shadow/Private) and Dependency { program, kind }, collapsing the three collections into one dependencies: HashMap<AccountId, Dependency> plus a self_kind field for the top-level program. One value per account instead of three independent memberships, so the "both shadow and private" state is no longer representable at all. The builder API (new/as_shadow_program/with_shadow_dependency/ as_private_program/with_private_dependency/From<Program>) keeps its exact existing signatures, so every existing caller (wallet, cycle_bench, examples/program_deployment, lee's own tests) compiles unchanged. The witness-building logic in execute_and_prove_with_padded_inputs collapses from a two-pass filter/chain into a single match over each account's one kind. Also extends wallet-ffi's C surface, which never gained shadow/private support when PR #824 introduced it upstream (it was only ever touched enough to keep compiling). Adds FfiProgramKind/FfiProgramHeader/ FfiMembershipProof/FfiDependency following this crate's existing tag-plus-flat-struct convention, and threads them through FfiProgramWithDependencies. account_id is always derived from (program, kind), never caller-supplied, so it can't disagree with what the circuit itself computes.
jonesmarvin8
added a commit
that referenced
this pull request
Sep 22, 2026
* refactor(lee,lez): unify ProgramWithDependencies around ProgramKind/Dependency Addresses agureev's PR #824 review comment: ProgramWithDependencies tracked each account's Public/Shadow/Private status across three independent collections (dependencies, shadow_account_ids, private_program_headers), so an account could end up in more than one of them at once with no coherent meaning, never enforced anywhere. Introduces ProgramKind (Public/Shadow/Private) and Dependency { program, kind }, collapsing the three collections into one dependencies: HashMap<AccountId, Dependency> plus a self_kind field for the top-level program. One value per account instead of three independent memberships, so the "both shadow and private" state is no longer representable at all. The builder API (new/as_shadow_program/with_shadow_dependency/ as_private_program/with_private_dependency/From<Program>) keeps its exact existing signatures, so every existing caller (wallet, cycle_bench, examples/program_deployment, lee's own tests) compiles unchanged. The witness-building logic in execute_and_prove_with_padded_inputs collapses from a two-pass filter/chain into a single match over each account's one kind. Also extends wallet-ffi's C surface, which never gained shadow/private support when PR #824 introduced it upstream (it was only ever touched enough to keep compiling). Adds FfiProgramKind/FfiProgramHeader/ FfiMembershipProof/FfiDependency following this crate's existing tag-plus-flat-struct convention, and threads them through FfiProgramWithDependencies. account_id is always derived from (program, kind), never caller-supplied, so it can't disagree with what the circuit itself computes. * fix(wallet-ffi): avoid FfiProgramKind/FfiAccountIdentityKind enum collision FfiProgramKind's Public variant collided with FfiAccountIdentityKind's existing Public at the C level: unscoped C/C++ enums put their enumerators in the enclosing scope, so two enums both defining PUBLIC in the same header is a hard redefinition error, not just a lint. Confirmed by actually compiling the generated header standalone (it now compiles cleanly). Renames the variants to ProgramPublic/ProgramShadow/ProgramPrivate (PROGRAM_PUBLIC/PROGRAM_SHADOW/PROGRAM_PRIVATE in the generated header). --------- Co-authored-by: jonesmarvin8 <marvin.jones.phd@gmail.com>
agureev
approved these changes
Sep 22, 2026
agureev
left a comment
Contributor
There was a problem hiding this comment.
lgtm, thank you for addressing the comments!
schouhy
approved these changes
Sep 22, 2026
schouhy
left a comment
Collaborator
There was a problem hiding this comment.
hey lgtm! left some comments
jonesmarvin8
pushed a commit
that referenced
this pull request
Sep 22, 2026
…programs Renamed ProgramImageWitness/ProgramImageClaim/ProgramKind's Public/Private variants to Disclosed/Undisclosed: both are publicly deployed programs, the distinction is only whether the claim discloses which one, and the old names read as if Private meant not publicly deployed. Asserted program_header.immutable in to_claim()'s Undisclosed arm, where prover-supplied witness data first crosses the trust boundary -- fail-fast defense in depth alongside the real invariant every other caller of immutable_mirror_commitment already enforces. Added account_id/self_account_id fields to FfiDependency and FfiProgramWithDependencies: ffi_kind_account_id derived every account id from kind + bytecode, silently assuming a Disclosed/Undisclosed program's header always lives at its own bijection address. It doesn't -- CreateHeader lets a deployer place a header at any address, and the native ProgramWithDependencies API already takes that address as an explicit parameter, but the FFI had no way to express it. Shadow keeps deriving from the bytecode, since that's definitional and there's no header to consult. Required is_authorized unconditionally for CreateHeader targets in finalize_header, matching UpdateHeader's existing rule. CreateHeader previously let a deployer place a header at any empty account with no authorization check at all; since AccountId::for_shadow_program is a plain forward hash, an attacker could compute a target program's shadow address in advance, deploy an unrelated "honest" header there to collect real public trust and funds, then dispatch a weaker program privately under the same address later to drain them -- no hash needs breaking, since the deployer chooses the target directly. The production wallet facade already always signs for the header's own key, so this closes the gap without touching any real deployment path; genesis bypasses CreateHeader entirely via insert_program, so it's unaffected too. Rebuilt the privacy_preserving_circuit RISC0 artifact, last regenerated at #903, one commit before #905 changed lee_core's circuit-input types -- the checked-in guest binary no longer matched the host's current encoding of PrivacyPreservingCircuitInput, so any shadow-program proof failed in-guest with a Borsh length mismatch.
added 2 commits
September 23, 2026 09:17
…w programs Adds two new ways a program can be invoked in a privacy-preserving transaction alongside the existing publicly-disclosed path: - Undisclosed: an immutable, publicly-deployed program whose identity isn't revealed on the resulting claim -- membership against the header's mirrored commitment is proven in-circuit instead of disclosing which image_id it is. - Shadow: a program never deployed to public state at all, resolved fresh each proof from its bytecode via a witness, dispatched at AccountId::for_shadow_program(image_id). Renamed the Public/Private naming throughout (ProgramImageWitness, ProgramImageClaim, ProgramKind, and their wallet-ffi mirrors) to Disclosed/Undisclosed: both are publicly deployed programs, the distinction is only whether the claim discloses which one. Security fixes from review: - program_loader's CreateHeader now requires is_authorized for its target unconditionally, closing an attack where a deployer could precompute a target program's shadow address, park an unrelated "honest" header there to collect real public trust and funds, then dispatch the real program privately under the same address later to drain them. - ProgramImageWitness::to_claim returns a Result instead of panicking when an Undisclosed claim's header isn't immutable, keeping the panic-or-not decision at the guest call site rather than baked into a shared type. - wallet-ffi's FfiDependency/FfiProgramWithDependencies gained explicit account_id/self_account_id fields: a program's header can live at any address a deployer was authorized for, never assumed to be its bytecode's bijection address, and the FFI had no way to express that before. Rebuilt the privacy_preserving_circuit RISC0 artifact to match.
…uit artifact Rebasing onto dev's shard-based account model required source changes to the guest-visible circuit_io/program_loader/state_machine code, which invalidates the previously committed guest artifact. Rebuilds privacy_preserving_circuit.bin against the rebased source and fixes clippy/fmt violations surfaced by dev's restriction lints (arbitrary_source_item_ordering, shadow_unrelated) plus a stale double-kernel-wrap bug in force_insert_segment_chain that broke segment-chain test fixtures with "Invalid Magic Bytes" errors.
jonesmarvin8
force-pushed
the
marvin/privacy-and-shadow-executions
branch
from
September 23, 2026 14:20
70beae5 to
4f9c996
Compare
added 2 commits
September 23, 2026 10:47
CI's artifacts check caught these: the shard-based account-model rebase changed lee_core types shared by every lez program's guest code, so all of their compiled RISC0 binaries were stale even though only privacy_preserving_circuit's artifact had been rebuilt. Rebuilds via `just build-artifacts` and refreshes the fixture dump to match.
…ss bijection addresses private_pda's and scoped_reads' program deployments targeted AccountId::from_builtin_program(image_id) via CreateHeader without holding its key, relying on the permissionless header-creation model the shadow-account-collision fix (comment #2) closed. Production deployment (lez/wallet/src/program_facades/program_loader.rs) always signs for the header's own key already; genesis bypasses CreateHeader entirely via insert_program. Fixes both tests by generating a real key for the deployment target instead, matching how their own segment accounts are already deployed.
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.
🎯 Purpose
Builds off of program deployment (in terms of linked list). Currently, from PR 815/812, privacy execution must disclose each program that is executed. In this PR, we introduce function privacy for immutable programs and shadow programs.
Once a program becomes immutable (
ProgramHeader.immutable = true), a commitment is emitted to the private state. This commitment can be used to (1) prove program's existence forprogram_ownerin private state, (2)image_idstored in committedDatais used with provided program elf.A shadow program is a program that has never been emitted to the public state. Privacy execution allows for execution of such programs by providing the elf. This is a feature that was possible in previous versions of LEZ.
BREAKING CHANGE!: Modifies privacy circuit; thus, proofs generated before this PR will no longer verify.
⚙️ Approach
create_header/update_headeremit a private mirror commitment the moment a header'simmutablebecomestrue, seeded by the header's ownaccount_id(not just its content).ProgramImageClaimsplits intoPublic(today's public lookup) /Private(anchored against the emitted commitment via membership proof). Sequencer verifies both at deploy-time and privacy-tx time.AccountId::for_shadow_program+ShadowProgramWitness: a third way to reference a program never deployed anywhere, identity established fresh in-circuit from an ELF witness each dispatch.ProgramWithDependencies::as_shadow_program/with_shadow_dependency/as_private_program/with_private_dependencyso callers can opt a program into either path.🧪 How to Test
All previous tests should pass; new tests added:
lez::programs::program_loader::core/lee::state_machine::src::state::tests::deploy:create_header_immutable_from_birth_lands_immutable_mirror_commitmentcreate_header_mutable_leaves_commitment_tree_unchangedupdate_header_flip_to_immutable_lands_immutable_mirror_commitmentlee::state_machine::core::src::program::tests:for_shadow_program_matches_pinned_valuelee::state_machine::src::state::tests::circuit:private_claim_matching_a_real_commitment_passes_verificationprivate_claim_with_no_matching_commitment_is_rejectedshadow_program_claims_a_private_pda_it_legitimately_ownsshadow_program_claims_a_public_pda_it_legitimately_owns🔗 Dependencies
N/A
🔜 Future Work
Privateclaims (currently lossy/display-only in that direction).ShadowProgramWitnesswhose ELF doesn't hash to its declaredaccount_id.📋 PR Completion Checklist