Skip to content

feat(confidential): Noir revoke_operator circuit#732

Merged
brozorec merged 15 commits into
mainfrom
feat/confidential-revoke-operator-circuit
May 29, 2026
Merged

feat(confidential): Noir revoke_operator circuit#732
brozorec merged 15 commits into
mainfrom
feat/confidential-revoke-operator-circuit

Conversation

@brozorec
Copy link
Copy Markdown
Collaborator

@brozorec brozorec commented May 27, 2026

Summary

Implements the RevokeOperator Noir circuit from design doc §7.9, closing issue #709. The circuit lets an owner reclaim a previously-allowed amount v_a from an operator's allowance commitment C_a back into the owner's spendable commitment C_spend, proves ownership of the spending key, opens both commitments, range-checks the reclaim and the resulting balance, and produces the owner-auditor visibility block (δ_aud_s channel with the two-squeeze sponge masking both the reclaimed amount and the post-reclaim balance checkpoint per §8.5).

Commits

  1. RevokeOperator circuit (V1–V10, V_a1–V_a5). Owner key ownership (V1), wrapper-bound viewing key (V2), delegation viewing key derivation (V3), opening of the on-chain allowance commitment with r_a bound to (dvk_i, σ_a) (V4, mirrors O3), spendable balance opening (V5), range validity on v_s, v_a, and v_s + v_a (V9), new spendable commitment folding the reclaimed allowance back in (V6/V7), encrypted balance scalar (V8), non-zero ephemeral scalar (V10), and the owner-auditor channel block (V_a1–V_a5) with the two-squeeze sponge over δ_aud_s emitting both the reclaimed amount and the post-reclaim balance checkpoint (§8.5).
  2. Publish revoke-operator VK. vks/revoke_operator.vk.json extracted via scripts/extract_vks.sh; the script now iterates over register, withdraw, and revoke_operator.

Soundness-critical guard

The v_s + v_a range check in V9 is the line of defense against a field overflow that would let the owner mint tokens on the reclaim path; rejects_v_sum_overflow pins v_s = 2^127 - 1 and v_a = 1 so the individual range checks pass and the sum check is the sole guard.

Acceptance criteria

  • Implementation matches the design-doc-frozen public-input ordering for §7.9
  • Constraints V1–V10 and V_a1–V_a5 covered by unit tests (one negative test per constraint; see test docstrings for constraint-to-test mapping)
  • Soundness-critical v_s + v_a overflow path isolated by rejects_v_sum_overflow
  • Fixtures pinned via revoke_operator_fixtures_match_lib to catch drift in commit / dvk_from_vk_op / derive_allow_r / sponge_squeeze_2 before the rest of the harness runs
  • VK extracted and published per parent's VK pipeline
  • PR links back to issue Noir circuit: RevokeOperator #709

Test plan

  • nargo test — revoke_operator + register + withdraw + lib all pass
  • cargo check --package stellar-tokens — Rust side unaffected
  • LC_ALL=C nargo info | grep '^|' | LC_ALL=C sort matches constraints.baseline
  • bash scripts/extract_vks.sh regenerates vks/register.vk.json and vks/withdraw.vk.json byte-identical and produces vks/revoke_operator.vk.json

Closes #709.

Summary by CodeRabbit

  • New Features

    • Added a new zero-knowledge circuit for operator revocation, enabling validation of spending allowances and encrypted balance transfers with auditor privacy protection.
  • Tests

    • Added comprehensive test suite with positive and negative test cases covering constraint validation, allowance verification, and range checks.
  • Chores

    • Updated build configuration to include the new circuit and generate its verification key artifacts.

Review Change Stack

brozorec added 13 commits May 22, 2026 12:02
Replaces the per-plaintext encrypt_auditor_tx / encrypt_auditor_bal
helpers with the per-channel Poseidon2 sponge described in design doc
v0.5 -> v0.6 (Section 2.5, Section 8.1). One absorb of
(delta_channel, S.x, sigma) is followed by N squeezes from the same
permutation; rate=3 lets every current call serve all squeezes without
a second permutation. AUDITOR_TX and AUDITOR_BALANCE domain tags are
dropped in favour of AUDITOR_SENDER (delta_aud_s) and AUDITOR_RECIPIENT
(delta_aud_r) at the same numeric slots (10, 11).

Adds the sponge_squeeze_2 gadget for nargo info accounting and pins
the new sponge outputs in testdata/sponge_squeeze_{1,2}.json. The
constraints baseline gains one row; no existing primitive's output
changed.
Implements the spend-side block of the Withdraw circuit from design doc
Section 7.5: owner key ownership (W1), wrapper-bound viewing key (W2),
spendable balance opening (W3), range validity on v, a, and v - a (W4),
deterministic randomness for the new commitment (W5), the refreshed
spendable commitment (W6), and the encrypted balance scalar (W7).

The auditor block (W_a1-W_a5, K_aud_s + r_e + R_e + b_tilde_aud_s) lands
in a follow-up commit so this one stays focused on balance conservation
and key ownership; once that lands the public input signature grows
from 10 to the design doc's frozen 15 fields.

Tests reuse the lib's pinned (sk, wrap, sigma, v, r) fixtures so any
drift in commit / vk_from_sk / encrypt_balance / derive_spend_r is
caught by `withdraw_fixtures_match_lib` before the rest of the harness
runs. Negative coverage: under-funded withdrawal (W4), v or a out of
range, wrong sk (W1), wrong balance opening (W3), wrong wrap (W2/W6/W7
chain), tampered b_tilde (W7), tampered C_spend' (W6). VK extraction
script and constraints baseline updated.
Adds the sender-auditor visibility block to the Withdraw circuit
(design doc Section 7.5, Section 8.1): the ephemeral key R_e = r_e * H
(W_a1), the sender-auditor ECDH shared secret S_{a,s} = r_e * K_{aud,s}
(W_a2), the single-squeeze sender-channel sponge mask m_b (W_a3), the
sender-auditor balance checkpoint ciphertext b_tilde_aud_s = (v - a) +
m_b (W_a4), and the r_e != 0 non-collapse constraint (W_a5).

The public input signature grows from 10 to the design-doc-frozen 15
fields, in canonical order (C_spend, Y, wrap, K_aud_s, a, C_spend',
sigma, b_tilde, R_e, b_tilde_aud_s). K_aud_s carries an explicit
on-curve + non-identity check at the entrypoint because the verifier
doesn't check curve membership and an off-curve K_aud_s would break the
soundness of W_a2 (Section 10.8).

`encrypt_auditor_sender_balance` lands in the lib as the canonical
W_a3+W_a4 composition; the same helper covers RevokeOperator V_a3/V_a5
and SetOperator S_a3/S_a5 when those circuits land. Pinned via testdata
and a lib round-trip test that ties it back to sponge_squeeze_1.

Auditor-side negative coverage: r_e = 0 (W_a5), wrong r_e (W_a1),
off-curve K_aud_s, identity K_aud_s, valid-but-wrong K_aud_s, tampered
b_tilde_aud_s (W_a4). Closes the under-funded-withdrawal + tampered-
ciphertext criteria from issue #705.
VK extracted via `bash scripts/extract_vks.sh` after the Withdraw circuit
landed in the workspace. Same UltraHonk universal-SRS pipeline as the
register VK -- no new trusted-setup contribution required. The committed
JSON is the integration contract with the on-chain verifier (#701); CI
re-runs the extraction script and diffs against this copy.
Aligns W4 with design doc §7.5 / §2.6 / §3.4: the value domain is the
SEP-41 non-negative i128 range [0, 2^127), not [0, 2^128). The circuit
now uses Noir stdlib's `Field::assert_max_bit_size::<127>()` directly
(the 127-bit decomposition spelled out in §2.6) instead of the lib's
u128-cast helper. Doc comments for W4 and the public-input table for
`a` are restated against the tighter bound and reference §3.4 for the
entrypoint-level `a >= 0` check.

Test boundaries retarget to 2^127 (the smallest value W4 must reject),
not 2^128. Withdraw VK regenerated; constraint count drops from 130 to
92 ACIR opcodes (stdlib's assert_max_bit_size is cheaper than the
u128 round-trip).

The lib's `assert_range_128` primitive is now unused by Withdraw but
stays in place for backwards compat -- a separate cleanup can remove
it once no callers reference it.
The test previously fired W3 (commit(v_huge, R) != stale C_SPEND) before
W4 ever ran, so the range check was never the load-bearing assertion.
Recompute C_spend from v_huge so W3 passes and the very first W4 line
(v.assert_max_bit_size::<127>()) becomes the failing constraint.

rejects_a_out_of_range already isolates W4 (V is unchanged, W3 passes,
W4 hits a.assert_max_bit_size::<127>() on a_huge = 2^127) so it is left
as-is.
The design doc added delta_addr = 1 (§2.7), shifting every other tag up
by one. Realign the lib constants, re-pin all Poseidon-derived fixtures
and testdata, regenerate VKs, and fix a stale W_a5 label in the withdraw
test (now W8).
Implements the RevokeOperator circuit from design doc Section 7.9: owner
key ownership (V1), wrapper-bound viewing key (V2), delegation viewing
key derivation (V3), opening of the on-chain allowance commitment with
r_a bound to (dvk_i, sigma_a) (V4, mirrors O3), spendable balance
opening (V5), range validity on v_s, v_a, and v_s + v_a (V9), new
spendable commitment folding the reclaimed allowance back in (V6/V7),
encrypted balance scalar (V8), non-zero ephemeral scalar (V10), and the
owner-auditor channel block (V_a1-V_a5) with the two-squeeze sponge over
delta_aud_s emitting both the reclaimed amount and the post-reclaim
balance checkpoint (Section 8.5).

The v_s + v_a sum range check is the soundness-critical line of defense
against a field overflow that would let the owner mint tokens on the
reclaim path; `rejects_v_sum_overflow` pins v_s = 2^127 - 1 and v_a = 1
so the individual range checks pass and the sum check is the sole
guard. Tests reuse the lib's pinned (sk, wrap, sigma, sigma_a, op_i,
v_s, r_s, v_a, r_e) fixtures so drift in commit / dvk_from_vk_op /
derive_allow_r / sponge_squeeze_2 is caught by
`revoke_operator_fixtures_match_lib` before the rest of the harness
runs. Negative coverage spans every constraint (see test docstrings for
the constraint-to-test mapping). Closes #709.
VK extracted via `bash scripts/extract_vks.sh` after the RevokeOperator
circuit landed in the workspace. Same UltraHonk universal-SRS pipeline
as the register and withdraw VKs -- no new trusted-setup contribution
required. The committed JSON is the integration contract with the
on-chain verifier (#701); CI re-runs the extraction script and diffs
against this copy.
@brozorec brozorec self-assigned this May 27, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented May 27, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.71%. Comparing base (6e81d75) to head (9669a63).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #732   +/-   ##
=======================================
  Coverage   96.71%   96.71%           
=======================================
  Files          67       67           
  Lines        6798     6798           
=======================================
  Hits         6575     6575           
  Misses        223      223           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

121 ACIR opcodes (vs withdraw's 92): the extra ~29 rows come from the
delegation viewing key derivation (V3), the allowance commitment opening
(V4), the second sponge squeeze for the post-reclaim balance checkpoint
(V_a5), and the v_s + v_a sum range check (V9).
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 27, 2026

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 78f10610-ae24-4bf5-a9df-be96e0a403d1

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

A new Noir circuit named RevokeOperator proves reclamation of escrowed allowance after operator delegation revocation. The implementation includes constraint logic validating owner key ownership, balance openings, amount range checks, and auditor ECDH-encrypted outputs, alongside 25 test cases covering fixture consistency, positive paths, and constraint violations.

Changes

RevokeOperator Circuit

Layer / File(s) Summary
Workspace & Circuit Project Setup
packages/tokens/src/confidential/circuits/Nargo.toml, packages/tokens/src/confidential/circuits/revoke_operator/Nargo.toml, packages/tokens/src/confidential/circuits/constraints.baseline
Register revoke_operator in workspace members, define circuit package metadata with stellar_confidential_lib dependency, and record baseline constraint/opcode counts.
RevokeOperator Circuit Constraints
packages/tokens/src/confidential/circuits/revoke_operator/src/main.nr
Implement core constraint logic: owner key derivation and validation, allowance opening with derived randomness, spend balance opening, amount range enforcement (127-bit max), new spendable commitment, encrypted balance, auditor ephemeral key validation, ECDH shared secret derivation, sponge-derived masks, and auditor-channel encrypted outputs.
Circuit Test Suite
packages/tokens/src/confidential/circuits/revoke_operator/src/tests.nr
Pinned fixture setup and library consistency validation (revoke_operator_fixtures_match_lib, revoke_operator_auditor_fixtures_match_lib). Positive tests: matches_fixture (known-good witness), alternate_opening (independent derivation). Negative tests (20 cases): wrong secret key, wrong wrapper, wrong delegation key, invalid allowance/spend openings, wrong sigma, range violations (v_s, v_a, sum overflow), invalid ephemeral key (zero/wrong value), invalid auditor key (off-curve/identity), tampered ciphertexts (b_tilde, c_spend_new, auditor amounts), and wrong valid auditor key.
Build Integration & Verification Key Extraction
packages/tokens/src/confidential/circuits/scripts/extract_vks.sh, packages/tokens/src/confidential/circuits/vks/revoke_operator.vk.json
Add revoke_operator to circuit compilation list in VK extraction pipeline and publish generated UltraHonk verification key artifact.

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly Related PRs

Suggested Reviewers

  • ozgunozerk
  • bidzyyys

Poem

🐰 A circuit blooms with secrets bound,
Where revoked allowances are found,
With ranges checked and keys derived,
Through auditor's masks, the truth's alive—
One more checkpoint on the chain! 🔐✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: implementing the RevokeOperator Noir circuit as specified in the design doc.
Linked Issues check ✅ Passed All coding requirements from issue #709 are met: constraints V1–V10 and V_a1–V_a5 implemented with comprehensive tests, v_s + v_a overflow isolated, fixtures pinned, VK extracted and published, and design-doc public-input ordering followed.
Out of Scope Changes check ✅ Passed All changes directly support the RevokeOperator circuit implementation: circuit code, tests, configuration updates, baseline entries, and VK extraction—no extraneous modifications present.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description check ✅ Passed The pull request description is comprehensive and well-structured, providing clear context, commit details, soundness-critical information, and acceptance criteria with checkmarks.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/confidential-revoke-operator-circuit

Comment @coderabbitai help to get the list of available commands and usage tips.

@brozorec brozorec requested a review from bidzyyys May 27, 2026 13:50
Copy link
Copy Markdown

@bidzyyys bidzyyys left a comment

Choose a reason for hiding this comment

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

Looks good to me!

…oke-operator-circuit

# Conflicts:
#	packages/tokens/src/confidential/circuits/Nargo.toml
#	packages/tokens/src/confidential/circuits/constraints.baseline
#	packages/tokens/src/confidential/circuits/scripts/extract_vks.sh
@brozorec brozorec merged commit 637c53a into main May 29, 2026
8 checks passed
@brozorec brozorec deleted the feat/confidential-revoke-operator-circuit branch May 29, 2026 10:14
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.

Noir circuit: RevokeOperator

2 participants