Skip to content

Solution: LP-0005 — Private Balance Attestation#64

Open
edenbd1 wants to merge 3 commits into
logos-co:masterfrom
edenbd1:lp-0005-private-balance-attestation-submission
Open

Solution: LP-0005 — Private Balance Attestation#64
edenbd1 wants to merge 3 commits into
logos-co:masterfrom
edenbd1:lp-0005-private-balance-attestation-submission

Conversation

@edenbd1

@edenbd1 edenbd1 commented May 24, 2026

Copy link
Copy Markdown

Implementation of LP-0005: a reusable private token balance attestation primitive for the Logos Execution Zone.

✅ Deployed live on the public Logos Execution Zone testnet — with a confirmed on-chain gated_check

Re-verified 2026-07-19. The public testnet was reset and upgraded to LEZ v0.2.0, which wiped the
earlier transaction history. Everything has been re-deployed against the current chain. A LEZ program
deployment tx hash is SHA256(borsh(bytecode)), so it is content addressed, and re-submitting the
byte-identical binaries reproduced the same hashes. Rows 1 to 3 below are therefore unchanged. The
gated_check in row 4 is a signed transaction carrying a nonce, so that one has a new hash.

Nothing was recompiled: the binaries built in May were re-deployed as is
and the end-to-end gated_check confirmed on the upgraded chain, so the primitive survives a LEZ
minor version upgrade unchanged.

Public on-chain evidence — every tx independently verifiable via getTransaction JSON-RPC or by clicking the explorer link:

# Action Tx hash
1 wallet deploy-program — attestation circuit (Risc0 guest proving balance ≥ N, ImageID dbc40b94…6a9d4d) 4593060b…3db989d
2 wallet deploy-program — verifier program v2 (SPEL, deep gate via ChainedCall, ImageID 7715f791…d8a1db429) 2bf10138…23723a9
3 wallet deploy-program — verifier program v3 (SPEL, shallow gate, ImageID b32c6662…df85952a) a0ec45bb…d341c5ca
4 spel gated_check against the v3 shallow gate. Host-side checks only: context, threshold and an ECDSA challenge response. It verifies no zero-knowledge proof fd9869f7…eafb306d
5 wallet deploy-program — LEZ-native attestation program (ImageID 9b6be465…6c27da) 674aa03a8a51a2eba660ec2ab136a1b6c9ca17817c7bb3160b68904375726652
6 wallet deploy-program — deep verifier (ImageID 1047297a…8261b27c) 7a4e46cfcab3a956a159d3c82a781222bdf093faa7ef8d42723f1a95e06eec0d
7 spel gated_check, privacy-preserving. The proof is verified on chain, and the enforced policy is bound into the marker e8ed66c79373ebbea77a254db866793d68fd1b71357731ed93d70bade7bbb4ab

An earlier deep verifier (4e2ac5c3…) and its gated_check (b9488de0…) remain on chain and still resolve, but are superseded: that gate bound neither the signer to the attested account nor the enforced policy to the marker. Both are recorded, struck through and explained, in docs/DEPLOYMENT.md.

Full deployment record (with reproduction commands) in docs/DEPLOYMENT.md.

Summary

A presenter holding a shielded LEZ token account can prove balance ≥ N without revealing the account's npk, exact balance, or identity. Two verification paths:

  • On-chain — SPEL #[lez_program] (crates/verifier-program-spel/methods/guest-deep/src/bin/attestation_verifier_deep.rs), which declares a ChainedCall to the LEZ-native attestation program so the privacy circuit composes it with env::verify, and gates on the anchored balance of the account the witness attests to. Confirmed end to end, row 7 above.
  • Off-chain — Rust library (crates/verifier-offchain/) that runs the same check_gate rules locally against a Risc0 receipt delivered over any transport (the working backend is waku_rest, which reaches the Waku network Logos Delivery runs on; inmem is for unit tests only).

Highlights vs the spec

  • Verifiable public-testnet deployment — the tx hashes above, all live on https://testnet.lez.logos.co. Anyone can getTransaction them or open the explorer link.
  • What the gate actually enforces. The proof's own statement is relative to a Merkle root the prover chooses, so on its own it is not a claim about a real balance. Four things the caller cannot forge are bound on chain: the signer must be the account the witness attests to (3010, and meeting it requires that account's spending key, since LEZ derives the account id from nsk); the owning program is pinned to LEZ's authenticated_transfer, so the balance is denominated (3011); the balance is read from anchored pre_state and LEZ requires its commitment digest to be in root_history (3009); and the enforced floor and context are folded into the marker PDA's seed, so the public artifact records the policy rather than merely that a gate ran (3012). Re-derive the PDA at a lower floor and it lands on an unclaimed address. crates/cu-bench/tests/deep_gate_rejects.rs runs the deployed bytecode through the sequencer's own execution path and requires each rejection to fire, with the honest call as the control.
  • The proof is verified on chain. Rows 5 to 7 are the deep gate, on the privacy-preserving path: the client proves locally, LEZ's privacy circuit discharges the chained call with a real env::verify (lee/privacy_preserving_circuit/src/execution_state.rs:149), and the sequencer verifies the receipt against the pinned PRIVACY_PRESERVING_CIRCUIT_ID before applying any state. Rows 1 to 4 are the earlier shallow gate, which verifies no proof, and no program on the public path could: a public transaction re-executes rather than proves (lee/state_machine/src/program.rs:73-77). Check it from public data with ./scripts/verify-onchain-proof.sh, which confirms the deployed bytecode hashes to the committed binaries, that the transaction is PrivacyPreserving rather than Public, that the receipt is Succinct rather than a dev-mode fake, and that the marker PDA derived from the verifier ImageID and the enforced policy is owned by the verifier.
  • Off-chain over Logos Messaging. Logos Delivery is a Waku node, so the transport publishes and subscribes over a Waku node's REST interface with the same content topics and envelope its send() builds. scripts/demo-offchain-gating.sh starts two nodes, peers them, and gates admission to a chat group with a credential that crosses between them, refusing replay, an intercepted proof, and an under-funded attestation.
  • Real Risc0 proofsscripts/demo.sh exports RISC0_DEV_MODE=0 as its first non-comment line. Composite receipt ~300 KB in ~6.5 s; the Groth16 wrap is 1,479 bytes, which is what the off-chain demo uses since Waku caps a message at 153,600 bytes.
  • Four reusable crates published on crates.ioattestation-core, attestation-verifier-program, attestation-sequencer-client, attestation-delivery-transport. External builders can cargo add directly.
  • Basecamp .lgx publishedapp/lp-0005-attestation.lgx 2.1 MB, lgx verify ✅, SHA-256 193a903a…94c89770.
  • Four in-tree integrations under integrations/ — governance-gate, chat-gate, premium-features and nostr-auth-gate. Each is an independently consumable crate; none hard-codes a deployed program id. Two are demonstrated end to end rather than merely compiled: chat-gate over a real two-node Waku network, and the full on-chain round trip from an empty chain via scripts/e2e-local.sh.
  • 90 tests passing, 0 failing across the workspace (cargo test --workspace), CI green on main, including an e2e job that boots a standalone LEZ sequencer, deploys three programs from an empty chain, proves under RISC0_DEV_MODE=0 and requires gated_check to confirm on chain, then runs the deep gate's rejection tests against the deployed bytecode.
  • 30-second reproducible demoscripts/demo.sh runs the full pipeline against the public testnet from a clean clone, requires only curl, python3, jq, and cargo.

Per-criterion compliance map

See docs/criteria-checklist.md for the row-by-row mapping from every line of the prize text to the code, test, or transaction hash that satisfies it.

Architecture

LEZ verifier as a SPEL program (justified vs zone-SDK in ADR-001): a single gated_check instruction takes the flattened journal + ECDSA challenge response + caller-pinned context/threshold, runs the host-side gate, and (in the deep variant) declares a ChainedCall so the LEZ PPE pipeline composes the inner Risc0 receipt via env::verify. Three-piece structure (portable kernel + deep guest + shallow guest) covers both the architecturally-ideal path and the confirmable-today path. Decision write-up: ADR-002.

@github-actions

github-actions Bot commented May 24, 2026

Copy link
Copy Markdown

⚠️ Validation script did not produce output.

@edenbd1
edenbd1 force-pushed the lp-0005-private-balance-attestation-submission branch from c9cb327 to e39303b Compare May 24, 2026 14:31
fryorcraken added a commit that referenced this pull request May 28, 2026
Aligns the L-Prize repo with logos-co/rfp PR #64, which standardised
dependency declarations as a structured `dependencies:` field in YAML
frontmatter (a list of `{ id, reason }` objects).

- LP-0013: adds frontmatter with the RFP-001 dependency; removes the
  markdown `## Dependencies` section.
- LP-0001, LP-0004, LP-0006: drop the markdown `## Dependencies`
  section. Their hard deps are platform features (LEZ NFT Program,
  LEZ timelock, Logos Delivery, Logos Chat) with no canonical
  RFP-/LP- ID, so no frontmatter entry is added — matching the
  RFP-003 precedent in logos-co/rfp PR #64. The dependency
  information is already captured in each file's status banner.
- LP-0000.md (template): replaces the markdown `## Dependencies`
  section with a frontmatter example in an HTML comment, noting that
  the field is included only when there are real hard deps with
  canonical IDs and is otherwise omitted entirely (no `[]`), per the
  L-Prize convention.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fryorcraken added a commit that referenced this pull request May 28, 2026
Previous convention omitted the `dependencies:` field entirely when an
LP had no hard deps. This change makes the field mandatory on every
LP: use `dependencies: []` for standalone LPs. A missing field now
signals the author hasn't considered deps yet (lintable); `[]` signals
considered and none.

This aligns with the convention in logos-co/rfp PR #64.

- LP-0000.md (template): replace the conditional frontmatter example
  with `dependencies: []` and a comment explaining the new convention.
- LP-0001..LP-0017 (excluding LP-0007 which does not exist and LP-0013
  which already declares an RFP-001 hard dep): add `dependencies: []`
  YAML frontmatter at the top of each file.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@weboko

weboko commented Jul 19, 2026

Copy link
Copy Markdown
Collaborator

Hi @edenbd1 . Your submission looks really promising. Before we proceed, can you, please, re-submit transactions as they are not available anymore (e.g link)

@edenbd1

edenbd1 commented Jul 19, 2026

Copy link
Copy Markdown
Author

Hey @weboko, thanks for flagging this.

Re-submitted, all four are live again:

Program Tx
attestation circuit 4593060b…3db989d
verifier v2 (deep gate) 2bf10138…23723a9
verifier v3 (shallow gate) a0ec45bb…d341c5ca
gated_check, confirmed fd9869f7…eafb306d

The testnet had been reset and upgraded to v0.2.0, which wiped the old history. Deployment tx hashes are content addressed (SHA256 of the bytecode), so redeploying the same binaries gave back the same three hashes. The gated_check is signed with a nonce, so that one is new.

I did not recompile anything. The May binaries went back up as they were and the gated_check confirmed on the upgraded chain.

@edenbd1

edenbd1 commented Jul 20, 2026

Copy link
Copy Markdown
Author

Update on the on-chain path, replacing the transactions in my previous note.

After posting those I kept auditing the gate and found it under-bound. It checked the balance of whichever account signed, without requiring that to be the account the witness attests to, and the marker it left on chain recorded that a gate had run without recording what it demanded, so a caller pinning a floor of zero produced a marker indistinguishable from an honest one. Both are fixed and the verifier is redeployed.

Program Tx
attestation (LEZ-native) 674aa03a8a51a2eba660ec2ab136a1b6c9ca17817c7bb3160b68904375726652
deep verifier 7a4e46cfcab3a956a159d3c82a781222bdf093faa7ef8d42723f1a95e06eec0d
gated_check, proof verified e8ed66c79373ebbea77a254db866793d68fd1b71357731ed93d70bade7bbb4ab

The gate now binds four things the caller cannot forge: the signer must be the account the witness attests to, its owning program is pinned to authenticated_transfer so the balance is denominated, the balance is read from anchored pre_state, and the enforced floor and context are folded into the marker PDA seed. Re-derive that PDA at a lower floor and it lands on an unclaimed address, so a marker earned at zero cannot pass for one earned at a real threshold.

Run ./scripts/verify-onchain-proof.sh to check it from public data, and crates/cu-bench/tests/deep_gate_rejects.rs runs the deployed bytecode through the sequencer's execution path and requires each rejection to fire.

The last two hashes are plain rather than linked because the explorer does not show them yet, though getTransaction returns both. Yesterday's deploy did appear, so this may be indexing lag, but yesterday's gated_check never did and the marker PDA page still shows the default program owner where the RPC reports the verifier. Worth a look on your side.

Off-chain over Logos Messaging is also in: Logos Delivery is a Waku node, so the transport publishes and subscribes over its REST interface with the same content topics and envelope its send() builds. scripts/demo-offchain-gating.sh gates chat admission with a credential crossing two real nodes, using the 1,479 byte Groth16 wrap since a composite receipt exceeds Waku's cap.

edenbd1 and others added 2 commits July 20, 2026 15:54
Corrects claims that no longer held, and records the work done since the original
submission.

The dead transaction: the write-up still cited 262bbe95...6babfd5e twice with
explorer links. That transaction was wiped by the testnet reset, so both links
were dead. This is the same problem raised in review, still present in the
submission document itself.

The on-chain claim: the write-up said the v3 verifier 'validated a real
Risc0-receipt-bound ECDSA signature' and presented that as the on-chain path. The
v3 gate verifies no zero-knowledge proof, and no program on the LEZ public path
could, since the sequencer re-executes rather than proves. The document now
describes the deep gate on the privacy-preserving path, where LEZ's privacy
circuit discharges a chained call with a real env::verify and the sequencer
verifies the receipt against the pinned PRIVACY_PRESERVING_CIRCUIT_ID, and where
the threshold is checked against the balance LEZ itself anchors rather than the
caller-supplied witness.

Also corrected: the transaction table (4 rows to 7), 'Eight on-chain
transactions', the superseded outside-party criterion, E2E tests described as
being in CI while pointing at ignored tests, 57 tests (now 76), 7 CLI subcommands
(now 8), and the off-chain path described as an in-memory backend with a stubbed
Qt bridge rather than the Waku transport that now exists.

Every transaction hash in the document resolves, and every link was opened.
The deep verifier is redeployed after an audit found the gate under-bound: it
read the balance of whichever account signed without requiring that to be the
account the witness attests to, and the marker it left on chain recorded that a
gate ran without recording what it demanded.

Updates the superseded identifiers, states the four bindings the gate now
enforces instead of the anchoring claim that did not hold, documents the
deployed 3xxx error codes, corrects the test count to 90 and separates user
cycles from proving cycles. Also removes a truncated transaction hash that no
longer resolves.
@weboko

weboko commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

@edenbd1 make sure CI passes 🙏

@edenbd1

edenbd1 commented Jul 20, 2026

Copy link
Copy Markdown
Author

@weboko the red check is not coming from the submission. The job dies at step 3, Checkout PR head, and Run validation is skipped, so the workflow never reads the file.

actions/checkout v4.4.0 went out today at 15:36 UTC and backported a guard that refuses fork checkouts under pull_request_target. The previous v4 release was v4.3.1 from November, so the floating @v4 tag picked it up on its own. Our 14:09 run used the old one and passed, the 16:15 run used v4.4.0 and failed, same branch.

I reproduced it rather than guess, one variable at a time: https://github.com/test-readme/checkout-guard-repro. A fork PR on @v4 fails, the same PR pinned to @v4.3.1 passes, the same PR with the opt-in passes, and a same-repo PR passes without it.

Fix is one line, opened as #109 so you only need to merge it. It affects every fork PR on the repo, not just this one.

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.

2 participants