Skip to content

feat(confidential): Noir operator_transfer circuit#731

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

feat(confidential): Noir operator_transfer circuit#731
brozorec merged 18 commits into
mainfrom
feat/confidential-operator-transfer-circuit

Conversation

@brozorec
Copy link
Copy Markdown
Collaborator

@brozorec brozorec commented May 27, 2026

Summary

Implements the confidential operator-transfer circuit (closing issue #708, design doc §7.8):

  • Operator + allowance opening (O1-O4): operator key ownership, opening of C_a via dvk_i, allowance randomness anchored to Poseidon2(δ_allow_r, dvk_i, σ_a) (the wrapper-binding hinge -- see §7.8 "Wrapper binding"), 127-bit range validity on v_a, v_tx, and v_a - v_tx.
  • Recipient ECDH + transfer commitment (O5-O9, O13): in-circuit recipient ECDH, ephemeral R_e, anti-poisoning binding of r_tx via Poseidon2(δ_tx_blind, S.x, σ_a) (Section 5.4), transfer commitment, encrypted amount, r_e ≠ 0.
  • New allowance (O10-O12): fresh deterministic r_a' = Poseidon2(δ_allow_r, dvk_i, σ_a'), new allowance commitment C_a' = (v_a - v_tx)·G + r_a'·H, encrypted allowance scalar.
  • Auditor block (O_a1-O_a8): dual-channel ECDH with reused r_e, per-channel SpongeSqueeze_2 keyed by σ_a, recipient-auditor (ṽ_aud,r, r̃_aud,r) and owner-auditor (ṽ_aud,s, ã_aud,s) -- the visibility model points balance- and allowance-checkpoint ciphertexts at the funds' owner, not the operator (§8.4).

24 public inputs in design-doc canonical order. Auditor keys (the only proof-less entry, Section 10.8 path 3) are validated on-curve + non-identity in-circuit before consumption by O_a1 / O_a5. PVK_recipient is path (2) -- proof-constrained at registration (R3) and trusted on read -- so no re-check, matching the transfer circuit's doctrine.

Wrapper binding (the subtle one): OperatorTransfer does not constrain vk directly; binding is inherited indirectly via the allowance commitment chain. SetOperator derives dvk_i from the wrapper-specific vk (S2, S5), which determines r_a (S6) and thus C_a (S7). O3 verifies the operator's claimed dvk_i against the on-chain C_a via σ_a. A proof generated against one wrapper's C_a cannot verify against another's. Exercised by rejects_wrong_dvk_against_wrapper.

Stacked on #728 (transfer) -- diff against main will include those changes until the parent lands.

Closes #708.

Test plan

  • nargo test --package circuit_operator_transfer (29 tests pass: 2 happy paths with different v_tx, full_allowance_transfer boundary, wrapper-binding regression, wrong sk_op / wrong allowance opening / wrong σ_a, under-funded transfer + range overflow on v_a and v_tx, anti-poisoning on C_tx, every tampered ciphertext, r_e = 0, off-curve + non-identity rejection of both auditor keys, wrong recipient PVK)
  • nargo test workspace-wide (no regressions in lib, register, withdraw, transfer)
  • ./scripts/extract_vks.sh reproduces the committed vks/operator_transfer.vk.json byte-for-byte with the pinned bb 0.87.0; existing register / withdraw VKs re-extract byte-identically

Summary by CodeRabbit

  • New Features

    • Introduced operator transfer circuit with comprehensive constraint validation and cryptographic checks.
  • Tests

    • Added extensive test suite with happy-path scenarios and edge-case coverage for operator transfer.
  • Chores

    • Updated configuration and build scripts to compile and generate verification keys for operator transfer circuit.

Review Change Stack

brozorec added 15 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 OperatorTransfer constraint set from DESIGN.github.md
Section 7.8 plus the dual-channel auditor block from Section 8.4.
Wrapper binding is inherited indirectly through O3 (allowance-randomness
Poseidon pinned to dvk_i, sigma_a), so the circuit does not constrain
vk -- the operator never sees the owner's sk. Public-input ordering
matches the parent epic's frozen 24-field layout.
Adds the operator_transfer entry to extract_vks.sh and commits the
generated vks/operator_transfer.vk.json so CI's regenerate-and-diff
contract covers the new circuit (closes #708).

Closes #708
Drop the defensive on-curve check on PVK_recipient. Per design doc §10.8,
PVK is path (1)/(2) -- proof-constrained at registration (R3) and trusted
on read -- not path (3). The transfer circuit follows this doctrine; the
operator-transfer circuit now matches. VK regenerated.
@brozorec brozorec self-assigned this May 27, 2026
@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: 0876309c-95e3-40e8-be6a-837bf520a9a2

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

This PR implements the OperatorTransfer Noir circuit enabling operator-initiated confidential transfers from a delegated allowance. The circuit validates operator key ownership, allowance commitment opening, transfer amount bounds, recipient ECDH-derived encryption, new allowance state, and dual-channel auditor visibility via Poseidon2 sponges. Verification key is extracted and published.

Changes

Operator Transfer Circuit

Layer / File(s) Summary
Circuit package setup and integration
packages/tokens/src/confidential/circuits/Nargo.toml, packages/tokens/src/confidential/circuits/operator_transfer/Nargo.toml, packages/tokens/src/confidential/circuits/scripts/extract_vks.sh, packages/tokens/src/confidential/circuits/constraints.baseline, packages/tokens/src/confidential/circuits/vks/operator_transfer.vk.json
Workspace membership registration, circuit manifest with stellar_confidential_lib dependency, VK extraction pipeline integration, constraints baseline opcode recording, and generated verification-key asset.
Circuit design and constraint implementation
packages/tokens/src/confidential/circuits/operator_transfer/src/main.nr
Circuit entrypoint signature with 6 private witnesses and 24 public inputs; design documentation for operators O1–O13 and O_a1–O_a8; sequential constraint blocks: operator key ownership and allowance-binding verification via dvk_i, allowance commitment and range checks for transfer amounts, recipient ECDH and transfer commitment with encrypted amount, new allowance with deterministic randomness, auditor ECDH key validation and dual-channel sponge-based visibility.
Test infrastructure and fixture validation
packages/tokens/src/confidential/circuits/operator_transfer/src/tests.nr (lines 1–315)
Canonical fixture constants, fixture-printing utility, fixture-consistency tests re-deriving core and auditor-channel values from lib primitives, and test harnesses (run_main, run_fixture, matches_fixture).
Comprehensive test coverage
packages/tokens/src/confidential/circuits/operator_transfer/src/tests.nr (lines 316–1447)
Two happy-path transfer scenarios and 26 constraint-violation cases covering operator key/allowance binding, transfer range validation, recipient key and transfer ciphertext integrity, ephemeral key validation, auditor key curve/identity checks, and auditor-channel encrypted-value tampering.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

The PR introduces a substantial, self-contained cryptographic circuit with dense constraint logic spanning 311 lines, comprehensive test coverage with 1447 test lines, fixture generation, and integration into the build pipeline. Review requires understanding multiple cryptographic primitives (ECDH, commitments, sponge squeezes, allowance binding) and verifying constraint correctness across happy path and 26 distinct failure modes.

Possibly related issues

  • Noir circuit: OperatorTransfer #708: Directly implements the OperatorTransfer circuit subtask specified in the epic, including all constraints O1–O13 and O_a1–O_a8, wrapper-binding regression test, and VK extraction pipeline integration as outlined in the acceptance criteria.

Possibly related PRs

  • OpenZeppelin/stellar-contracts#715: Both PRs extend the shared confidential-circuits infrastructure by adding new circuit package entries, updating the constraints.baseline with circuit-specific opcode counts, and integrating into the common VK extraction pipeline via scripts/extract_vks.sh.

Suggested reviewers

  • ozgunozerk
  • bidzyyys

Poem

🐰 Hops through constraints, eight operators deep,
Allowance bindings the secrets to keep,
ECDH whispers to auditor's eyes,
Poseidon sponges squeeze truth from the skies—
OperatorTransfer now works, verified and free!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(confidential): Noir operator_transfer circuit' accurately summarizes the main change—implementing a new Noir circuit for operator transfers in the confidential module.
Description check ✅ Passed The description comprehensively covers the implementation of OperatorTransfer circuit constraints (O1–O13 and O_a1–O_a8), references the design doc, documents test coverage (29 tests), and closes issue #708. All required template sections are addressed.
Linked Issues check ✅ Passed The PR implements all objectives from issue #708: operator key ownership (O1), allowance opening via dvk_i (O2–O3), range checks (O4), recipient ECDH and transfer commitment (O5–O9), new allowance with deterministic randomness (O10–O12), ephemeral key validation (O13), and dual-channel auditor blocks (O_a1–O_a8). Tests cover happy paths, wrapper-binding regression, and negative cases. Public-input ordering and VK extraction are confirmed.
Out of Scope Changes check ✅ Passed All changes are directly in scope: new operator_transfer circuit implementation, test suite, VK extraction configuration, baseline update, and workspace configuration. No extraneous modifications to other circuits or unrelated components detected.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ 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-operator-transfer-circuit

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

@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 (4ffa219) to head (b527099).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #731   +/-   ##
=======================================
  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.

The regeneration command in the file's own comment strips everything but
the table rows; re-prepend the header block so the regen instructions
survive future updates.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/tokens/src/confidential/circuits/constraints.baseline`:
- Line 5: The baseline file has a stale opcode count: update the entry for
circuit_operator_transfer | main in constraints.baseline from 131 to the current
build value 129 to match the `nargo info` output; locate the line containing
"circuit_operator_transfer   | main" and replace the opcode count field
(previously 131) with 129 so the baseline matches the current ACIR opcode count.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 00e0e9aa-9819-4da0-bb0c-204e515f448f

📥 Commits

Reviewing files that changed from the base of the PR and between c819794 and f73f251.

📒 Files selected for processing (7)
  • packages/tokens/src/confidential/circuits/Nargo.toml
  • packages/tokens/src/confidential/circuits/constraints.baseline
  • packages/tokens/src/confidential/circuits/operator_transfer/Nargo.toml
  • packages/tokens/src/confidential/circuits/operator_transfer/src/main.nr
  • packages/tokens/src/confidential/circuits/operator_transfer/src/tests.nr
  • packages/tokens/src/confidential/circuits/scripts/extract_vks.sh
  • packages/tokens/src/confidential/circuits/vks/operator_transfer.vk.json

Comment thread packages/tokens/src/confidential/circuits/constraints.baseline
@brozorec brozorec requested a review from bidzyyys May 27, 2026 13:32
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!

@brozorec brozorec merged commit 6e81d75 into main May 29, 2026
8 checks passed
@brozorec brozorec deleted the feat/confidential-operator-transfer-circuit branch May 29, 2026 10:07
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: OperatorTransfer

2 participants