From ac3e879e677ceb40a061dd994ac92cc407f45b8f Mon Sep 17 00:00:00 2001 From: brozorec <9572072+brozorec@users.noreply.github.com> Date: Fri, 22 May 2026 12:02:02 +0200 Subject: [PATCH 01/16] feat(confidential): per-channel auditor sponge primitives 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. --- .../src/confidential/circuits/Nargo.toml | 1 + .../circuits/constraints.baseline | 1 + .../gadgets/sponge_squeeze_2/Nargo.toml | 8 + .../gadgets/sponge_squeeze_2/src/main.nr | 11 ++ .../src/confidential/circuits/lib/src/lib.nr | 67 ++++++--- .../confidential/circuits/lib/src/tests.nr | 142 ++++++++++++++---- .../lib/testdata/encrypt_auditor_bal.json | 11 -- .../lib/testdata/encrypt_auditor_tx.json | 11 -- .../lib/testdata/sponge_squeeze_1.json | 11 ++ .../lib/testdata/sponge_squeeze_2.json | 21 +++ 10 files changed, 208 insertions(+), 76 deletions(-) create mode 100644 packages/tokens/src/confidential/circuits/gadgets/sponge_squeeze_2/Nargo.toml create mode 100644 packages/tokens/src/confidential/circuits/gadgets/sponge_squeeze_2/src/main.nr delete mode 100644 packages/tokens/src/confidential/circuits/lib/testdata/encrypt_auditor_bal.json delete mode 100644 packages/tokens/src/confidential/circuits/lib/testdata/encrypt_auditor_tx.json create mode 100644 packages/tokens/src/confidential/circuits/lib/testdata/sponge_squeeze_1.json create mode 100644 packages/tokens/src/confidential/circuits/lib/testdata/sponge_squeeze_2.json diff --git a/packages/tokens/src/confidential/circuits/Nargo.toml b/packages/tokens/src/confidential/circuits/Nargo.toml index 71cabcb62..31245f543 100644 --- a/packages/tokens/src/confidential/circuits/Nargo.toml +++ b/packages/tokens/src/confidential/circuits/Nargo.toml @@ -9,4 +9,5 @@ members = [ "gadgets/encrypt_amount", "gadgets/range_128", "gadgets/poseidon_with_domain", + "gadgets/sponge_squeeze_2", ] diff --git a/packages/tokens/src/confidential/circuits/constraints.baseline b/packages/tokens/src/confidential/circuits/constraints.baseline index 30230740a..7f55cb966 100644 --- a/packages/tokens/src/confidential/circuits/constraints.baseline +++ b/packages/tokens/src/confidential/circuits/constraints.baseline @@ -34,4 +34,5 @@ | gadget_range_128 | directive_integer_quotient | N/A | N/A | 8 | | gadget_range_128 | directive_invert | N/A | N/A | 9 | | gadget_range_128 | main | Bounded { width: 4 } | 14 | 17 | +| gadget_sponge_squeeze_2 | main | Bounded { width: 4 } | 4 | 0 | | gadget_vk_from_sk | main | Bounded { width: 4 } | 4 | 0 | diff --git a/packages/tokens/src/confidential/circuits/gadgets/sponge_squeeze_2/Nargo.toml b/packages/tokens/src/confidential/circuits/gadgets/sponge_squeeze_2/Nargo.toml new file mode 100644 index 000000000..766e33042 --- /dev/null +++ b/packages/tokens/src/confidential/circuits/gadgets/sponge_squeeze_2/Nargo.toml @@ -0,0 +1,8 @@ +[package] +name = "gadget_sponge_squeeze_2" +type = "bin" +authors = ["OpenZeppelin"] +compiler_version = ">=0.30.0" + +[dependencies] +stellar_confidential_lib = { path = "../../lib" } diff --git a/packages/tokens/src/confidential/circuits/gadgets/sponge_squeeze_2/src/main.nr b/packages/tokens/src/confidential/circuits/gadgets/sponge_squeeze_2/src/main.nr new file mode 100644 index 000000000..df766bf48 --- /dev/null +++ b/packages/tokens/src/confidential/circuits/gadgets/sponge_squeeze_2/src/main.nr @@ -0,0 +1,11 @@ +use stellar_confidential_lib::sponge_squeeze_2; + +// Minimal circuit wrapping `sponge_squeeze_2(d, s_x, sigma) -> [Field; 2]`. +// Used by `nargo info` to report the per-call constraint cost of the +// two-squeeze auditor channel sponge (Section 2.5). The one-squeeze case +// shares the `poseidon_with_domain` gadget already in this directory. +fn main(d: pub Field, s_x: pub Field, sigma: pub Field, m0: pub Field, m1: pub Field) { + let out = sponge_squeeze_2(d, s_x, sigma); + assert(out[0] == m0); + assert(out[1] == m1); +} diff --git a/packages/tokens/src/confidential/circuits/lib/src/lib.nr b/packages/tokens/src/confidential/circuits/lib/src/lib.nr index e686aacaf..639d7a9a9 100644 --- a/packages/tokens/src/confidential/circuits/lib/src/lib.nr +++ b/packages/tokens/src/confidential/circuits/lib/src/lib.nr @@ -86,8 +86,8 @@ global POSEIDON2_IV_BASE: Field = 18446744073709551616; // 2^64 /// | `delta_enc_allow` | 7 | `ENCRYPTED_ALLOWANCE` | /// | `delta_allow_r` | 8 | `ALLOWANCE_RANDOMNESS` | /// | `delta_esc_dvk` | 9 | `ESCROWED_DELEGATION_VIEWING_KEY` | -/// | `delta_aud_tx` | 10 | `AUDITOR_TX` | -/// | `delta_aud_bal` | 11 | `AUDITOR_BALANCE` | +/// | `delta_aud_s` | 10 | `AUDITOR_SENDER` | +/// | `delta_aud_r` | 11 | `AUDITOR_RECIPIENT` | mod domain { /// Viewing-key derivation: `vk = Poseidon2(VIEWING_KEY, sk, wrap)`. /// Section 4.2 (`delta_vk`). @@ -120,14 +120,16 @@ mod domain { /// `Poseidon2(ESCROWED_DELEGATION_VIEWING_KEY, s, op_i)`. /// Constraint S12, Section 7.11 (`delta_esc_dvk`). pub(crate) global ESCROWED_DELEGATION_VIEWING_KEY: Field = 9; - /// Auditor mask for transfer amount: - /// `v_tilde_aud = v_tx + Poseidon2(AUDITOR_TX, s_a, sigma)`. - /// Constraints T_a2 / T_a4 / O_a2 / O_a4 / S_a3 / V_a3 (`delta_aud_tx`). - pub(crate) global AUDITOR_TX: Field = 10; - /// Auditor mask for balance / allowance checkpoint: - /// `b_tilde_aud = v_new + Poseidon2(AUDITOR_BALANCE, s_a, sigma)`. - /// Constraints W_a3 / T_a5 / S_a4 / V_a4 / O_a5 (`delta_aud_bal`). - pub(crate) global AUDITOR_BALANCE: Field = 11; + /// Sender or owner-auditor channel tag for Poseidon2 sponge masks + /// (Section 2.5, Section 8.1). Squeeze 1 yields the amount mask (where + /// applicable); squeeze 2 yields the balance/allowance checkpoint mask. + /// Constraints W_a3 / T_a6 / S_a3 / V_a3 / O_a6 (`delta_aud_s`). + pub(crate) global AUDITOR_SENDER: Field = 10; + /// Recipient-auditor channel tag for Poseidon2 sponge masks + /// (Section 2.5, Section 8.1). Squeeze 1 yields the amount mask; squeeze + /// 2 yields the per-transfer Pedersen randomness mask. + /// Constraints T_a2 / O_a2 (`delta_aud_r`). + pub(crate) global AUDITOR_RECIPIENT: Field = 11; } // ################## CORE PRIMITIVES ################## @@ -299,22 +301,39 @@ pub fn encrypt_allowance(v_a: Field, dvk: Field, sigma_a: Field) -> Field { v_a + poseidon_with_domain(domain::ENCRYPTED_ALLOWANCE, [dvk, sigma_a]) } -/// Encrypts the transfer amount under an auditor's ECDH shared secret. -/// Constraints T_a2 / T_a4 / O_a2 / O_a4 / S_a3 / V_a3. -/// `v_tilde_aud = v_tx + Poseidon2(AUD_TX, s_a, sigma)`. -pub fn encrypt_auditor_tx(v_tx: Field, s_a: Field, sigma: Field) -> Field { - v_tx + poseidon_with_domain(domain::AUDITOR_TX, [s_a, sigma]) -} - -/// Encrypts the post-operation balance (or allowance) under an auditor's ECDH -/// shared secret. Constraints W_a3 / T_a5 / S_a4 / V_a4 / O_a5. -/// `b_tilde_aud = v_new + Poseidon2(AUD_BAL, s_a, sigma)`. -pub fn encrypt_auditor_bal(v_new: Field, s_a: Field, sigma: Field) -> Field { - v_new + poseidon_with_domain(domain::AUDITOR_BALANCE, [s_a, sigma]) -} - /// Delegation-key escrow mask for the operator handoff at `set_operator`. Section 7.11. /// `escrowed_dvk = dvk + Poseidon2(ESC_DVK, s, op_i)` where `s = (r_e * Y_op).x`. pub fn encrypt_esc_dvk(dvk: Field, s: Field, op_i: Field) -> Field { dvk + poseidon_with_domain(domain::ESCROWED_DELEGATION_VIEWING_KEY, [s, op_i]) } + +// ################## AUDITOR-CHANNEL SPONGE ################## + +/// Poseidon2 sponge with a single squeeze. Absorbs `(d, s_x, sigma)` and +/// returns the first rate-element of the post-permutation state. +/// +/// Equivalent to `poseidon_with_domain(d, [s_x, sigma])` -- the alias exists so +/// design-doc constraints written as `m = SpongeSqueeze_1(delta, S.x, sigma)` +/// (Section 2.5) translate 1:1 to source. The single-squeeze case is the +/// sender-auditor channel for Withdraw (W_a3), RevokeOperator (V_a3), and +/// SetOperator (S_a3), which need only the balance/allowance mask. +pub fn sponge_squeeze_1(d: Field, s_x: Field, sigma: Field) -> Field { + poseidon_with_domain(d, [s_x, sigma]) +} + +/// Poseidon2 sponge with two squeezes. Absorbs `(d, s_x, sigma)` and returns +/// the first two rate-elements of the post-permutation state in order. +/// +/// Implements `SpongeSqueeze_2(delta, S.x, sigma)` from Section 2.5. The canonical +/// squeeze order is: index 0 = amount mask, index 1 = balance / randomness +/// mask, fixed by Sections 7 and 8. With rate = 3 the absorb fits one block +/// and both outputs are served from the same permutation, so the constraint +/// cost is exactly one Poseidon2 permutation per call. +/// +/// Used by: Transfer (T_a2 recipient-auditor amount + randomness, T_a6 +/// sender-auditor amount + balance) and OperatorTransfer (O_a2, O_a6). +pub fn sponge_squeeze_2(d: Field, s_x: Field, sigma: Field) -> [Field; 2] { + let iv: Field = 3 * POSEIDON2_IV_BASE; + let state = poseidon2_permutation([d, s_x, sigma, iv], 4); + [state[0], state[1]] +} diff --git a/packages/tokens/src/confidential/circuits/lib/src/tests.nr b/packages/tokens/src/confidential/circuits/lib/src/tests.nr index fa5f75c3e..92bca9b01 100644 --- a/packages/tokens/src/confidential/circuits/lib/src/tests.nr +++ b/packages/tokens/src/confidential/circuits/lib/src/tests.nr @@ -1,8 +1,8 @@ use crate::{ assert_on_curve, assert_on_curve_non_identity, assert_range_128, commit, derive_allow_r, derive_spend_r, derive_tx_blind, domain, dvk_from_vk_op, ecdh, encrypt_allowance, - encrypt_amount, encrypt_auditor_bal, encrypt_auditor_tx, encrypt_balance, encrypt_esc_dvk, - G, H, poseidon_with_domain, pvk_from_vk, scalar_mul, vk_from_sk, + encrypt_amount, encrypt_balance, encrypt_esc_dvk, G, H, poseidon_with_domain, pvk_from_vk, + scalar_mul, sponge_squeeze_1, sponge_squeeze_2, vk_from_sk, }; use std::embedded_curve_ops::EmbeddedCurvePoint; use std::hash::{derive_generators, pedersen_commitment}; @@ -185,8 +185,8 @@ fn domain_separation_distinct() { let h_ea = poseidon_with_domain(domain::ENCRYPTED_ALLOWANCE, [a, b]); let h_ar = poseidon_with_domain(domain::ALLOWANCE_RANDOMNESS, [a, b]); let h_ed = poseidon_with_domain(domain::ESCROWED_DELEGATION_VIEWING_KEY, [a, b]); - let h_at = poseidon_with_domain(domain::AUDITOR_TX, [a, b]); - let h_ab = poseidon_with_domain(domain::AUDITOR_BALANCE, [a, b]); + let h_as = poseidon_with_domain(domain::AUDITOR_SENDER, [a, b]); + let h_ar2 = poseidon_with_domain(domain::AUDITOR_RECIPIENT, [a, b]); // Distinct from VK. assert(h_vk != h_dvk); @@ -197,8 +197,13 @@ fn domain_separation_distinct() { assert(h_vk != h_ea); assert(h_vk != h_ar); assert(h_vk != h_ed); - assert(h_vk != h_at); - assert(h_vk != h_ab); + assert(h_vk != h_as); + assert(h_vk != h_ar2); + + // The two auditor channels must yield distinct masks under identical + // inputs -- if they collide, the same shared-secret `S.x` would leak the + // same ciphertext on both channels, defeating channel separation. + assert(h_as != h_ar2); } #[test] @@ -222,19 +227,63 @@ fn encrypt_decrypt_round_trip() { let mask_allow = poseidon_with_domain(domain::ENCRYPTED_ALLOWANCE, [s, sigma]); assert(a_tilde - mask_allow == v); - let v_tilde_aud = encrypt_auditor_tx(v, s, sigma); - let mask_aud_tx = poseidon_with_domain(domain::AUDITOR_TX, [s, sigma]); - assert(v_tilde_aud - mask_aud_tx == v); - - let b_tilde_aud = encrypt_auditor_bal(v, s, sigma); - let mask_aud_bal = poseidon_with_domain(domain::AUDITOR_BALANCE, [s, sigma]); - assert(b_tilde_aud - mask_aud_bal == v); - let dvk_escrowed = encrypt_esc_dvk(v, s, sigma); let mask_esc = poseidon_with_domain(domain::ESCROWED_DELEGATION_VIEWING_KEY, [s, sigma]); assert(dvk_escrowed - mask_esc == v); } +#[test] +fn sponge_squeeze_1_matches_poseidon_with_domain() { + // sponge_squeeze_1 is documented as an alias of poseidon_with_domain on + // a 2-element payload; the canonical-form check here is what lets every + // SpongeSqueeze_1(...) call site in the design doc translate to either + // helper without ambiguity. + let d: Field = domain::AUDITOR_SENDER; + let s_x: Field = 0xfeed; + let sigma: Field = 0xbeef; + assert(sponge_squeeze_1(d, s_x, sigma) == poseidon_with_domain(d, [s_x, sigma])); +} + +#[test] +fn sponge_squeeze_2_first_matches_squeeze_1() { + // SpongeSqueeze_n shares a single absorb (Section 2.5); n=1 and n=2 must + // produce identical first masks. If this diverges, a circuit doing + // SpongeSqueeze_1 cannot be substituted for the first output of + // SpongeSqueeze_2 and vice versa. + let d: Field = domain::AUDITOR_RECIPIENT; + let s_x: Field = 0x12345; + let sigma: Field = 0x6789; + let m2 = sponge_squeeze_2(d, s_x, sigma); + assert(m2[0] == sponge_squeeze_1(d, s_x, sigma)); +} + +#[test] +fn sponge_squeeze_2_outputs_distinct() { + // The two squeezed masks come from different rate positions of the same + // permutation. They must differ -- a collision would let an attacker who + // recovers one mask recover the other, breaking the per-ciphertext + // independence assumption that justifies the dual-mask construction + // (Section 8.1, the recipient-auditor ciphertext pair). + let d: Field = domain::AUDITOR_SENDER; + let s_x: Field = 0x42; + let sigma: Field = 0x07; + let m = sponge_squeeze_2(d, s_x, sigma); + assert(m[0] != m[1]); +} + +#[test] +fn sponge_squeeze_channel_separation() { + // Same (S.x, sigma) under the two channel domains must produce different + // mask pairs. This is the property that makes a leaked sender-channel + // ciphertext useless for decrypting the recipient channel and vice versa. + let s_x: Field = 0xcafe; + let sigma: Field = 0xdead; + let m_s = sponge_squeeze_2(domain::AUDITOR_SENDER, s_x, sigma); + let m_r = sponge_squeeze_2(domain::AUDITOR_RECIPIENT, s_x, sigma); + assert(m_s[0] != m_r[0]); + assert(m_s[1] != m_r[1]); +} + /// One-shot fixture-generation harness. Prints every primitive's output for a /// deterministic input set. Captured into `testdata/*.json` (the cross-language /// contract for the SDK) and mirrored in `fixtures_match_testdata` below. Run @@ -311,18 +360,28 @@ fn print_fixtures() { let a_tilde = encrypt_allowance(v_a, dvk, sigma_a); println(f"encrypt_allowance = {a_tilde}"); - // encrypt_auditor_tx(v_tx, s, sigma) - let v_aud = encrypt_auditor_tx(v_tx, s_ecdh, sigma); - println(f"encrypt_auditor_tx = {v_aud}"); - - // encrypt_auditor_bal(v, s, sigma) - let b_aud = encrypt_auditor_bal(v, s_ecdh, sigma); - println(f"encrypt_auditor_bal = {b_aud}"); - // encrypt_esc_dvk(dvk, s, op_i) let esc = encrypt_esc_dvk(dvk, s_ecdh, op_i); println(f"encrypt_esc_dvk = {esc}"); + // sponge_squeeze_1(AUDITOR_SENDER, s, sigma) + let ss1_s = sponge_squeeze_1(domain::AUDITOR_SENDER, s_ecdh, sigma); + println(f"sponge_squeeze_1_AUDITOR_SENDER = {ss1_s}"); + + // sponge_squeeze_2(AUDITOR_SENDER, s, sigma) -- (amount, balance) masks + let ss2_s = sponge_squeeze_2(domain::AUDITOR_SENDER, s_ecdh, sigma); + let ss2_s0 = ss2_s[0]; + let ss2_s1 = ss2_s[1]; + println(f"sponge_squeeze_2_AUDITOR_SENDER_0 = {ss2_s0}"); + println(f"sponge_squeeze_2_AUDITOR_SENDER_1 = {ss2_s1}"); + + // sponge_squeeze_2(AUDITOR_RECIPIENT, s, sigma) -- (amount, r_tx) masks + let ss2_r = sponge_squeeze_2(domain::AUDITOR_RECIPIENT, s_ecdh, sigma); + let ss2_r0 = ss2_r[0]; + let ss2_r1 = ss2_r[1]; + println(f"sponge_squeeze_2_AUDITOR_RECIPIENT_0 = {ss2_r0}"); + println(f"sponge_squeeze_2_AUDITOR_RECIPIENT_1 = {ss2_r1}"); + // poseidon_with_domain(VK, [sk, wrap]) -- direct funnel access, equals vk_from_sk let pwd = poseidon_with_domain(domain::VIEWING_KEY, [sk, wrap]); println(f"poseidon_with_domain_VK_2 = {pwd}"); @@ -394,23 +453,46 @@ fn fixtures_match_testdata() { encrypt_allowance(v_a, dvk, sigma_a) == 0x235db43a283e4e000337cdb80a99168ef4b718622cc06de4ab98b3d2bd5736d5, ); - assert( - encrypt_auditor_tx(v_tx, s_ecdh, sigma) - == 0x266e76dccd3d7c654ab7a48045cca920c497b1a6758981acfa6f217427ac30d8, - ); - assert( - encrypt_auditor_bal(v, s_ecdh, sigma) - == 0x2787d1e01bbca7828e13e9b2b3fa11cfcfe9c3d2b121b9e17543146822fe23d3, - ); assert( encrypt_esc_dvk(dvk, s_ecdh, op_i) == 0x2468d02775ad21aa5928d049770287249af2a03dbaf4d18e22e3fe71fd292003, ); + // Per-channel auditor sponge (v0.6, Section 2.5). Values pinned below are + // produced by `nargo test print_fixtures` against the same fixture inputs. + assert( + sponge_squeeze_1(domain::AUDITOR_SENDER, s_ecdh, sigma) + == SPONGE_SQUEEZE_1_AUDITOR_SENDER, + ); + let ss2_s = sponge_squeeze_2(domain::AUDITOR_SENDER, s_ecdh, sigma); + assert(ss2_s[0] == SPONGE_SQUEEZE_2_AUDITOR_SENDER_0); + assert(ss2_s[1] == SPONGE_SQUEEZE_2_AUDITOR_SENDER_1); + let ss2_r = sponge_squeeze_2(domain::AUDITOR_RECIPIENT, s_ecdh, sigma); + assert(ss2_r[0] == SPONGE_SQUEEZE_2_AUDITOR_RECIPIENT_0); + assert(ss2_r[1] == SPONGE_SQUEEZE_2_AUDITOR_RECIPIENT_1); + + // Cross-form invariant: SpongeSqueeze_1 must equal the first squeeze of + // SpongeSqueeze_2 on the same absorb (single permutation, same state[0]). + assert(ss2_s[0] == sponge_squeeze_1(domain::AUDITOR_SENDER, s_ecdh, sigma)); + // poseidon_with_domain funnel is the same operation that backs vk_from_sk. assert(poseidon_with_domain(domain::VIEWING_KEY, [sk, wrap]) == vk); } +// Sponge fixture outputs -- pinned by `print_fixtures` and mirrored in +// `testdata/sponge_squeeze_*.json`. Update both sides together if the lib's +// sponge construction or domain constants change. +global SPONGE_SQUEEZE_1_AUDITOR_SENDER: Field = + 0x266e76dccd3d7c654ab7a48045cca920c497b1a6758981acfa6f217427ac3074; +global SPONGE_SQUEEZE_2_AUDITOR_SENDER_0: Field = + 0x266e76dccd3d7c654ab7a48045cca920c497b1a6758981acfa6f217427ac3074; +global SPONGE_SQUEEZE_2_AUDITOR_SENDER_1: Field = + 0x248b757f9fec92895bd3fb91b1a5c9c17467df6f0f55ab574d85e943df8e5cf3; +global SPONGE_SQUEEZE_2_AUDITOR_RECIPIENT_0: Field = + 0x2787d1e01bbca7828e13e9b2b3fa11cfcfe9c3d2b121b9e17543146822fe1feb; +global SPONGE_SQUEEZE_2_AUDITOR_RECIPIENT_1: Field = + 0x27f3739a132c6353cd5af3edac0ac75faf7fc606acb61367774e4f764ec17b77; + #[test] fn derive_helpers_deterministic_and_distinct() { // derive_spend_r, derive_allow_r, derive_tx_blind must each be diff --git a/packages/tokens/src/confidential/circuits/lib/testdata/encrypt_auditor_bal.json b/packages/tokens/src/confidential/circuits/lib/testdata/encrypt_auditor_bal.json deleted file mode 100644 index 22f456547..000000000 --- a/packages/tokens/src/confidential/circuits/lib/testdata/encrypt_auditor_bal.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "primitive": "encrypt_auditor_bal", - "design_doc_refs": ["Constraint W_a3", "Constraint T_a5", "Constraint S_a4", "Constraint V_a4", "Constraint O_a5"], - "description": "Auditor-encrypted balance checkpoint: b_tilde_aud = v_new + Poseidon2(AUD_BAL, s_a, sigma).", - "vectors": [ - { - "inputs": { "v_new": "0x3e8", "s_a": "0x12345", "sigma": "0x01" }, - "output": "0x2787d1e01bbca7828e13e9b2b3fa11cfcfe9c3d2b121b9e17543146822fe23d3" - } - ] -} diff --git a/packages/tokens/src/confidential/circuits/lib/testdata/encrypt_auditor_tx.json b/packages/tokens/src/confidential/circuits/lib/testdata/encrypt_auditor_tx.json deleted file mode 100644 index 33dc4ed7d..000000000 --- a/packages/tokens/src/confidential/circuits/lib/testdata/encrypt_auditor_tx.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "primitive": "encrypt_auditor_tx", - "design_doc_refs": ["Constraint T_a2", "Constraint T_a4", "Constraint O_a2", "Constraint O_a4", "Constraint S_a3", "Constraint V_a3"], - "description": "Auditor-encrypted transfer amount: v_tilde_aud = v_tx + Poseidon2(AUD_TX, s_a, sigma).", - "vectors": [ - { - "inputs": { "v_tx": "0x64", "s_a": "0x12345", "sigma": "0x01" }, - "output": "0x266e76dccd3d7c654ab7a48045cca920c497b1a6758981acfa6f217427ac30d8" - } - ] -} diff --git a/packages/tokens/src/confidential/circuits/lib/testdata/sponge_squeeze_1.json b/packages/tokens/src/confidential/circuits/lib/testdata/sponge_squeeze_1.json new file mode 100644 index 000000000..741608e1d --- /dev/null +++ b/packages/tokens/src/confidential/circuits/lib/testdata/sponge_squeeze_1.json @@ -0,0 +1,11 @@ +{ + "primitive": "sponge_squeeze_1", + "design_doc_refs": ["Section 2.5", "Constraint W_a3", "Constraint S_a3", "Constraint V_a3"], + "description": "Poseidon2 sponge with a single squeeze (one absorb of (d, s_x, sigma), permute, output state[0]). Used by sender-auditor channels that emit only the balance/allowance mask. Equivalent to poseidon_with_domain(d, [s_x, sigma]).", + "vectors": [ + { + "inputs": { "d": "0x0a", "s_x": "0x12345", "sigma": "0x01" }, + "output": "0x266e76dccd3d7c654ab7a48045cca920c497b1a6758981acfa6f217427ac3074" + } + ] +} diff --git a/packages/tokens/src/confidential/circuits/lib/testdata/sponge_squeeze_2.json b/packages/tokens/src/confidential/circuits/lib/testdata/sponge_squeeze_2.json new file mode 100644 index 000000000..d95baa829 --- /dev/null +++ b/packages/tokens/src/confidential/circuits/lib/testdata/sponge_squeeze_2.json @@ -0,0 +1,21 @@ +{ + "primitive": "sponge_squeeze_2", + "design_doc_refs": ["Section 2.5", "Constraint T_a2", "Constraint T_a6", "Constraint O_a2", "Constraint O_a6"], + "description": "Poseidon2 sponge with two squeezes (one absorb of (d, s_x, sigma), permute, output (state[0], state[1])). Canonical squeeze order: index 0 = amount mask, index 1 = balance / per-transfer Pedersen randomness mask, fixed per design doc Sections 7 and 8. The sender-auditor channel uses d = AUDITOR_SENDER (10); the recipient-auditor channel uses d = AUDITOR_RECIPIENT (11). The first squeeze must match sponge_squeeze_1 on the same inputs.", + "vectors": [ + { + "inputs": { "d": "0x0a", "s_x": "0x12345", "sigma": "0x01" }, + "output": [ + "0x266e76dccd3d7c654ab7a48045cca920c497b1a6758981acfa6f217427ac3074", + "0x248b757f9fec92895bd3fb91b1a5c9c17467df6f0f55ab574d85e943df8e5cf3" + ] + }, + { + "inputs": { "d": "0x0b", "s_x": "0x12345", "sigma": "0x01" }, + "output": [ + "0x2787d1e01bbca7828e13e9b2b3fa11cfcfe9c3d2b121b9e17543146822fe1feb", + "0x27f3739a132c6353cd5af3edac0ac75faf7fc606acb61367774e4f764ec17b77" + ] + } + ] +} From c29f874ab1f5fd0b8d46feb9c0dfa6d4a63e43fc Mon Sep 17 00:00:00 2001 From: brozorec <9572072+brozorec@users.noreply.github.com> Date: Fri, 22 May 2026 12:08:56 +0200 Subject: [PATCH 02/16] feat(confidential): withdraw circuit main constraints (W1-W7) 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. --- .../src/confidential/circuits/Nargo.toml | 1 + .../circuits/constraints.baseline | 5 + .../circuits/scripts/extract_vks.sh | 1 + .../confidential/circuits/withdraw/Nargo.toml | 8 + .../circuits/withdraw/src/main.nr | 103 ++++++ .../circuits/withdraw/src/tests.nr | 303 ++++++++++++++++++ 6 files changed, 421 insertions(+) create mode 100644 packages/tokens/src/confidential/circuits/withdraw/Nargo.toml create mode 100644 packages/tokens/src/confidential/circuits/withdraw/src/main.nr create mode 100644 packages/tokens/src/confidential/circuits/withdraw/src/tests.nr diff --git a/packages/tokens/src/confidential/circuits/Nargo.toml b/packages/tokens/src/confidential/circuits/Nargo.toml index 31245f543..49acde248 100644 --- a/packages/tokens/src/confidential/circuits/Nargo.toml +++ b/packages/tokens/src/confidential/circuits/Nargo.toml @@ -2,6 +2,7 @@ members = [ "lib", "register", + "withdraw", "gadgets/assert_on_curve", "gadgets/commit", "gadgets/ecdh", diff --git a/packages/tokens/src/confidential/circuits/constraints.baseline b/packages/tokens/src/confidential/circuits/constraints.baseline index 7f55cb966..eadf61687 100644 --- a/packages/tokens/src/confidential/circuits/constraints.baseline +++ b/packages/tokens/src/confidential/circuits/constraints.baseline @@ -22,6 +22,11 @@ | circuit_register | directive_invert | N/A | N/A | 9 | | circuit_register | lte_hint | N/A | N/A | 33 | | circuit_register | main | Bounded { width: 4 } | 33 | 72 | +| circuit_withdraw | decompose_hint | N/A | N/A | 30 | +| circuit_withdraw | directive_integer_quotient | N/A | N/A | 8 | +| circuit_withdraw | directive_invert | N/A | N/A | 9 | +| circuit_withdraw | lte_hint | N/A | N/A | 33 | +| circuit_withdraw | main | Bounded { width: 4 } | 109 | 80 | | gadget_assert_on_curve | main | Bounded { width: 4 } | 2 | 0 | | gadget_commit | decompose_hint | N/A | N/A | 30 | | gadget_commit | lte_hint | N/A | N/A | 33 | diff --git a/packages/tokens/src/confidential/circuits/scripts/extract_vks.sh b/packages/tokens/src/confidential/circuits/scripts/extract_vks.sh index 5244befb9..f9a5fca9e 100755 --- a/packages/tokens/src/confidential/circuits/scripts/extract_vks.sh +++ b/packages/tokens/src/confidential/circuits/scripts/extract_vks.sh @@ -23,6 +23,7 @@ cd "$(dirname "$0")/.." CIRCUITS=( "register" + "withdraw" ) OUT_DIR="vks" diff --git a/packages/tokens/src/confidential/circuits/withdraw/Nargo.toml b/packages/tokens/src/confidential/circuits/withdraw/Nargo.toml new file mode 100644 index 000000000..47b30c97e --- /dev/null +++ b/packages/tokens/src/confidential/circuits/withdraw/Nargo.toml @@ -0,0 +1,8 @@ +[package] +name = "circuit_withdraw" +type = "bin" +authors = ["OpenZeppelin"] +compiler_version = ">=0.30.0" + +[dependencies] +stellar_confidential_lib = { path = "../lib" } diff --git a/packages/tokens/src/confidential/circuits/withdraw/src/main.nr b/packages/tokens/src/confidential/circuits/withdraw/src/main.nr new file mode 100644 index 000000000..ebafd70fa --- /dev/null +++ b/packages/tokens/src/confidential/circuits/withdraw/src/main.nr @@ -0,0 +1,103 @@ +use stellar_confidential_lib::{ + H, assert_range_128, commit, derive_spend_r, encrypt_balance, scalar_mul, vk_from_sk, +}; + +mod tests; + +// Withdraw circuit -- design doc Section 7.5. +// +// Constraints (main block W1-W7; auditor block W_a1-W_a5 follows) +// --------------------------------------------------------------- +// W1 Y = sk * H owner key ownership +// W2 vk = Poseidon2(delta_vk, sk, wrap) wrapper-bound viewing +// key +// W3 C_spend = v * G + r * H opening of current +// spendable balance +// W4 v, a, v - a in [0, 2^128) range validity +// (Section 2.6) +// W5 r' = Poseidon2(delta_spend_r, vk, sigma) deterministic +// randomness for the +// new balance +// W6 C_spend' = (v - a) * G + r' * H new spendable +// commitment +// W7 b_tilde = (v - a) +// + Poseidon2(delta_enc_bal, vk, sigma) +// encrypted balance +// scalar (emitted) +// +// On-curve checks for Y, C_spend, and C_spend' are not needed: all three +// are bound to in-circuit multi_scalar_mul outputs (Y in W1, C_spend in W3, +// C_spend' in W6), which are on-curve by construction. See design doc +// Section 10.8 for the system-wide point-validation doctrine. +// +// Public inputs (this block: 10 fields; the auditor block will add 5 more +// in design-doc canonical order before this is shipped end-to-end) +// --------------------------------------------------------------------- +// Idx Param Symbol Source / Note +// --- ----- ------ --------------------------------------- +// 0 c_spend_x C_spend.x Loaded from `from.spendable_balance`. +// 1 c_spend_y C_spend.y +// 2 y_x Y.x Loaded from `from.spending_key`. +// 3 y_y Y.y +// 4 wrap wrap `env.current_contract_address()`. +// 5 a a Public withdrawal amount; the wrapper +// pre-checks a >= 0 at the entrypoint, +// W4 then closes a < 2^128 in-circuit. +// 6 c_spend_new_x C_spend'.x Prover-supplied; written to +// 7 c_spend_new_y C_spend'.y `from.spendable_balance` on success. +// 8 sigma sigma Prover-supplied random salt; emitted. +// 9 b_tilde b_tilde Prover-supplied encrypted balance +// scalar; emitted. +// +// Private witnesses +// ----------------- +// sk Spending secret scalar. +// v Plaintext spendable-balance value. +// r Plaintext blinding factor for C_spend. + +fn main( + sk: Field, + v: Field, + r: Field, + c_spend_x: pub Field, + c_spend_y: pub Field, + y_x: pub Field, + y_y: pub Field, + wrap: pub Field, + a: pub Field, + c_spend_new_x: pub Field, + c_spend_new_y: pub Field, + sigma: pub Field, + b_tilde: pub Field, +) { + // W1 + let y_derived = scalar_mul(sk, H); + assert(y_derived.x == y_x); + assert(y_derived.y == y_y); + + // W2 + let vk = vk_from_sk(sk, wrap); + + // W3 + let c_spend_derived = commit(v, r); + assert(c_spend_derived.x == c_spend_x); + assert(c_spend_derived.y == c_spend_y); + + // W4 + assert_range_128(v); + assert_range_128(a); + let v_new = v - a; + assert_range_128(v_new); + + // W5 + let r_new = derive_spend_r(vk, sigma); + + // W6 + let c_spend_new_derived = commit(v_new, r_new); + assert(c_spend_new_derived.x == c_spend_new_x); + assert(c_spend_new_derived.y == c_spend_new_y); + + // W7 + let b_tilde_derived = encrypt_balance(v_new, vk, sigma); + assert(b_tilde_derived == b_tilde); +} diff --git a/packages/tokens/src/confidential/circuits/withdraw/src/tests.nr b/packages/tokens/src/confidential/circuits/withdraw/src/tests.nr new file mode 100644 index 000000000..a09d8ed4c --- /dev/null +++ b/packages/tokens/src/confidential/circuits/withdraw/src/tests.nr @@ -0,0 +1,303 @@ +use crate::main; +use stellar_confidential_lib::{commit, derive_spend_r, encrypt_balance, H, scalar_mul, vk_from_sk}; + +// Canonical fixture inputs. +// +// SK / WRAP / SIGMA / V / R reuse the lib's pinned vectors (lib/testdata) +// so that Y, C_spend, vk, r_new, and b_tilde are derivable from -- and +// pinned by -- the lib's `fixtures_match_testdata` test. A is chosen to +// be strictly less than V so the happy path exercises a non-trivial +// `v_new = v - a` while still satisfying W4. +global SK: Field = 0xdead; +global WRAP: Field = 0xbeef; +global SIGMA: Field = 0x01; +global V: Field = 1000; +global R: Field = 42; +global A: Field = 300; +global V_NEW: Field = 700; + +// Y = sk*H (= lib `scalar_mul_Y` fixture). +global Y_X: Field = 0x1b46b003b88a6c34549dc74115f088f4b231a151397526bc10cbf1d15b457646; +global Y_Y: Field = 0x29116280600c10ead1fdbd9ab4b571896030679bf554d7f1ebf681e5147de21b; + +// C_spend = commit(1000, 42) (= lib `commit` fixture). +global C_SPEND_X: Field = + 0x195a5d8ecd032fe1696054b28f0852b5f03a613681991ace823245ab2f97ed05; +global C_SPEND_Y: Field = + 0x0e67489ecfee0e581dce7db22a383c635886cfb616c3b3bb033bff0c46e9789e; + +// C_spend' and b_tilde are pinned by `withdraw_fixtures_match_lib` below, +// which re-derives them from the lib primitives and asserts the values +// declared here -- if either side drifts the test fails immediately. +global C_SPEND_NEW_X: Field = + 0x2fb2d785d71d4c62ce4130675a6c44020c03a236ab21450b6f20282549caadac; +global C_SPEND_NEW_Y: Field = + 0x069a978c07b99e97858a6e94cef5b3d2c49f1df0d2aa5e04dffcefaca9320814; +global B_TILDE: Field = + 0x2b7f92c72c05ae8ecf7207fdaecea32ef748cd4dd43ca9ed6dc6d226865b4283; + +#[test] +fn print_fixtures() { + // One-shot harness: prints C_spend', b_tilde for the chosen (SK, WRAP, + // SIGMA, V, R, A) tuple. Run with + // `nargo test --package circuit_withdraw print_fixtures --show-output` + // and paste the values into the C_SPEND_NEW_* / B_TILDE globals above. + let vk = vk_from_sk(SK, WRAP); + let r_new = derive_spend_r(vk, SIGMA); + let c_new = commit(V_NEW, r_new); + let b_tilde = encrypt_balance(V_NEW, vk, SIGMA); + let cx = c_new.x; + let cy = c_new.y; + println(f"C_SPEND_NEW_X = {cx}"); + println(f"C_SPEND_NEW_Y = {cy}"); + println(f"B_TILDE = {b_tilde}"); +} + +#[test] +fn withdraw_fixtures_match_lib() { + // Cross-check the hard-coded fixture values against the lib's + // primitives. If a lib primitive's output changes the fixture + // assertions break here -- which is exactly when the rest of the + // withdraw tests would silently start using stale public inputs. + let vk = vk_from_sk(SK, WRAP); + let r_new = derive_spend_r(vk, SIGMA); + let c_new = commit(V_NEW, r_new); + let b_tilde = encrypt_balance(V_NEW, vk, SIGMA); + let y = scalar_mul(SK, H); + let c_spend = commit(V, R); + + assert(c_new.x == C_SPEND_NEW_X); + assert(c_new.y == C_SPEND_NEW_Y); + assert(b_tilde == B_TILDE); + assert(y.x == Y_X); + assert(y.y == Y_Y); + assert(c_spend.x == C_SPEND_X); + assert(c_spend.y == C_SPEND_Y); +} + +#[test] +fn matches_fixture() { + main( + SK, + V, + R, + C_SPEND_X, + C_SPEND_Y, + Y_X, + Y_Y, + WRAP, + A, + C_SPEND_NEW_X, + C_SPEND_NEW_Y, + SIGMA, + B_TILDE, + ); +} + +#[test] +fn full_withdrawal() { + // Boundary: a = v, so v_new = 0. C_spend' commits to (0, r_new) which + // is a valid commitment to zero (and is non-identity because r_new is + // a Poseidon output, vanishingly unlikely to equal 0). W6 binds it. + let vk = vk_from_sk(SK, WRAP); + let r_new = derive_spend_r(vk, SIGMA); + let c_new = commit(0, r_new); + let b_tilde = encrypt_balance(0, vk, SIGMA); + main( + SK, + V, + R, + C_SPEND_X, + C_SPEND_Y, + Y_X, + Y_Y, + WRAP, + V, + c_new.x, + c_new.y, + SIGMA, + b_tilde, + ); +} + +#[test(should_fail)] +fn rejects_under_funded_withdrawal() { + // a > v: v - a underflows in the field to a value >= 2^128. W4's + // range check on v_new fires before any downstream constraint, so + // this is exactly the under-funded-withdraw soundness gap closed. + let a_too_large: Field = V + 1; + // C_spend_new is whatever the (necessarily wrong) prover supplies; + // W6 would also fail, but W4 should fail first. Pick C_spend_new for + // some valid v_new derivation just to keep the test focused on W4. + let vk = vk_from_sk(SK, WRAP); + let r_new = derive_spend_r(vk, SIGMA); + let c_new_invalid = commit(V - a_too_large, r_new); + let b_tilde_invalid = encrypt_balance(V - a_too_large, vk, SIGMA); + main( + SK, + V, + R, + C_SPEND_X, + C_SPEND_Y, + Y_X, + Y_Y, + WRAP, + a_too_large, + c_new_invalid.x, + c_new_invalid.y, + SIGMA, + b_tilde_invalid, + ); +} + +#[test(should_fail)] +fn rejects_v_out_of_range() { + // v claimed by the prover is >= 2^128: W4 rejects on the v check + // before W3 even looks at C_spend. + let v_huge: Field = 0x100000000000000000000000000000000; // 2^128 + main( + SK, + v_huge, + R, + C_SPEND_X, + C_SPEND_Y, + Y_X, + Y_Y, + WRAP, + A, + C_SPEND_NEW_X, + C_SPEND_NEW_Y, + SIGMA, + B_TILDE, + ); +} + +#[test(should_fail)] +fn rejects_a_out_of_range() { + // a >= 2^128: W4's range check on a fails. (The wrapper's a >= 0 + // entrypoint check is out-of-scope for the circuit; here we only + // close the upper bound.) + let a_huge: Field = 0x100000000000000000000000000000000; + main( + SK, + V, + R, + C_SPEND_X, + C_SPEND_Y, + Y_X, + Y_Y, + WRAP, + a_huge, + C_SPEND_NEW_X, + C_SPEND_NEW_Y, + SIGMA, + B_TILDE, + ); +} + +#[test(should_fail)] +fn rejects_wrong_sk() { + // sk does not derive the claimed Y: W1 fails. + main( + SK + 1, + V, + R, + C_SPEND_X, + C_SPEND_Y, + Y_X, + Y_Y, + WRAP, + A, + C_SPEND_NEW_X, + C_SPEND_NEW_Y, + SIGMA, + B_TILDE, + ); +} + +#[test(should_fail)] +fn rejects_wrong_balance_opening() { + // (v, r) supplied don't open C_spend: W3 fails. Use a v that still + // passes the W4 range check so the failure is squarely W3. + main( + SK, + V + 1, + R, + C_SPEND_X, + C_SPEND_Y, + Y_X, + Y_Y, + WRAP, + A, + C_SPEND_NEW_X, + C_SPEND_NEW_Y, + SIGMA, + B_TILDE, + ); +} + +#[test(should_fail)] +fn rejects_wrong_wrap() { + // wrap != WRAP under which Y was derived. vk recomputes under the + // new wrap, the new vk produces a different r_new (W5) and a + // different b_tilde (W7). W6 / W7 fire. (W1 still passes because Y + // is the prover-claimed Y and sk*H is recomputed against it without + // using wrap.) + main( + SK, + V, + R, + C_SPEND_X, + C_SPEND_Y, + Y_X, + Y_Y, + 0xcafe, + A, + C_SPEND_NEW_X, + C_SPEND_NEW_Y, + SIGMA, + B_TILDE, + ); +} + +#[test(should_fail)] +fn rejects_tampered_b_tilde() { + // b_tilde mutated by +1: W7 fails because encrypt_balance derives a + // specific scalar that no longer matches. + main( + SK, + V, + R, + C_SPEND_X, + C_SPEND_Y, + Y_X, + Y_Y, + WRAP, + A, + C_SPEND_NEW_X, + C_SPEND_NEW_Y, + SIGMA, + B_TILDE + 1, + ); +} + +#[test(should_fail)] +fn rejects_tampered_c_spend_new() { + // C_spend' supplied with wrong x: W6 fails because the prover's + // claim no longer matches the in-circuit (v - a)*G + r'*H. + main( + SK, + V, + R, + C_SPEND_X, + C_SPEND_Y, + Y_X, + Y_Y, + WRAP, + A, + C_SPEND_NEW_X + 1, + C_SPEND_NEW_Y, + SIGMA, + B_TILDE, + ); +} From 2ee2a6a41efc6d897d61872da27b207bd1e5fee8 Mon Sep 17 00:00:00 2001 From: brozorec <9572072+brozorec@users.noreply.github.com> Date: Fri, 22 May 2026 12:16:18 +0200 Subject: [PATCH 03/16] feat(confidential): withdraw auditor block (W_a1-W_a5) 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. --- .../circuits/constraints.baseline | 2 +- .../src/confidential/circuits/lib/src/lib.nr | 12 + .../confidential/circuits/lib/src/tests.nr | 19 +- .../encrypt_auditor_sender_balance.json | 15 + .../circuits/withdraw/src/main.nr | 110 ++++-- .../circuits/withdraw/src/tests.nr | 329 ++++++++++++++++-- 6 files changed, 421 insertions(+), 66 deletions(-) create mode 100644 packages/tokens/src/confidential/circuits/lib/testdata/encrypt_auditor_sender_balance.json diff --git a/packages/tokens/src/confidential/circuits/constraints.baseline b/packages/tokens/src/confidential/circuits/constraints.baseline index eadf61687..512ad4a2c 100644 --- a/packages/tokens/src/confidential/circuits/constraints.baseline +++ b/packages/tokens/src/confidential/circuits/constraints.baseline @@ -26,7 +26,7 @@ | circuit_withdraw | directive_integer_quotient | N/A | N/A | 8 | | circuit_withdraw | directive_invert | N/A | N/A | 9 | | circuit_withdraw | lte_hint | N/A | N/A | 33 | -| circuit_withdraw | main | Bounded { width: 4 } | 109 | 80 | +| circuit_withdraw | main | Bounded { width: 4 } | 130 | 80 | | gadget_assert_on_curve | main | Bounded { width: 4 } | 2 | 0 | | gadget_commit | decompose_hint | N/A | N/A | 30 | | gadget_commit | lte_hint | N/A | N/A | 33 | diff --git a/packages/tokens/src/confidential/circuits/lib/src/lib.nr b/packages/tokens/src/confidential/circuits/lib/src/lib.nr index 639d7a9a9..0317a4d7f 100644 --- a/packages/tokens/src/confidential/circuits/lib/src/lib.nr +++ b/packages/tokens/src/confidential/circuits/lib/src/lib.nr @@ -337,3 +337,15 @@ pub fn sponge_squeeze_2(d: Field, s_x: Field, sigma: Field) -> [Field; 2] { let state = poseidon2_permutation([d, s_x, sigma, iv], 4); [state[0], state[1]] } + +/// Sender-auditor encrypted balance/allowance checkpoint for circuits whose +/// auditor block emits a single ciphertext (Withdraw W_a3/W_a4, RevokeOperator +/// V_a3/V_a5, SetOperator S_a3/S_a5). +/// +/// `b_tilde_aud_s = v_new + SpongeSqueeze_1(AUDITOR_SENDER, s_a_s_x, sigma)`. +/// The two-squeeze sender-channel form (Transfer T_a6-T_a8, OperatorTransfer +/// O_a6-O_a8) is a different composition that callers spell out directly with +/// `sponge_squeeze_2`. +pub fn encrypt_auditor_sender_balance(v_new: Field, s_a_s_x: Field, sigma: Field) -> Field { + v_new + sponge_squeeze_1(domain::AUDITOR_SENDER, s_a_s_x, sigma) +} diff --git a/packages/tokens/src/confidential/circuits/lib/src/tests.nr b/packages/tokens/src/confidential/circuits/lib/src/tests.nr index 92bca9b01..25bf1c701 100644 --- a/packages/tokens/src/confidential/circuits/lib/src/tests.nr +++ b/packages/tokens/src/confidential/circuits/lib/src/tests.nr @@ -1,8 +1,8 @@ use crate::{ assert_on_curve, assert_on_curve_non_identity, assert_range_128, commit, derive_allow_r, derive_spend_r, derive_tx_blind, domain, dvk_from_vk_op, ecdh, encrypt_allowance, - encrypt_amount, encrypt_balance, encrypt_esc_dvk, G, H, poseidon_with_domain, pvk_from_vk, - scalar_mul, sponge_squeeze_1, sponge_squeeze_2, vk_from_sk, + encrypt_amount, encrypt_auditor_sender_balance, encrypt_balance, encrypt_esc_dvk, G, H, + poseidon_with_domain, pvk_from_vk, scalar_mul, sponge_squeeze_1, sponge_squeeze_2, vk_from_sk, }; use std::embedded_curve_ops::EmbeddedCurvePoint; use std::hash::{derive_generators, pedersen_commitment}; @@ -230,6 +230,10 @@ fn encrypt_decrypt_round_trip() { let dvk_escrowed = encrypt_esc_dvk(v, s, sigma); let mask_esc = poseidon_with_domain(domain::ESCROWED_DELEGATION_VIEWING_KEY, [s, sigma]); assert(dvk_escrowed - mask_esc == v); + + let b_tilde_aud_s = encrypt_auditor_sender_balance(v, s, sigma); + let mask_aud_s = sponge_squeeze_1(domain::AUDITOR_SENDER, s, sigma); + assert(b_tilde_aud_s - mask_aud_s == v); } #[test] @@ -368,6 +372,10 @@ fn print_fixtures() { let ss1_s = sponge_squeeze_1(domain::AUDITOR_SENDER, s_ecdh, sigma); println(f"sponge_squeeze_1_AUDITOR_SENDER = {ss1_s}"); + // encrypt_auditor_sender_balance(v, s, sigma) + let b_aud_s = encrypt_auditor_sender_balance(v, s_ecdh, sigma); + println(f"encrypt_auditor_sender_balance = {b_aud_s}"); + // sponge_squeeze_2(AUDITOR_SENDER, s, sigma) -- (amount, balance) masks let ss2_s = sponge_squeeze_2(domain::AUDITOR_SENDER, s_ecdh, sigma); let ss2_s0 = ss2_s[0]; @@ -464,6 +472,13 @@ fn fixtures_match_testdata() { sponge_squeeze_1(domain::AUDITOR_SENDER, s_ecdh, sigma) == SPONGE_SQUEEZE_1_AUDITOR_SENDER, ); + // encrypt_auditor_sender_balance is the canonical composition: + // v + SpongeSqueeze_1(AUDITOR_SENDER, ...) -- pin so the lib helper and + // its raw sponge cousin can't drift apart. + assert( + encrypt_auditor_sender_balance(v, s_ecdh, sigma) + == v + SPONGE_SQUEEZE_1_AUDITOR_SENDER, + ); let ss2_s = sponge_squeeze_2(domain::AUDITOR_SENDER, s_ecdh, sigma); assert(ss2_s[0] == SPONGE_SQUEEZE_2_AUDITOR_SENDER_0); assert(ss2_s[1] == SPONGE_SQUEEZE_2_AUDITOR_SENDER_1); diff --git a/packages/tokens/src/confidential/circuits/lib/testdata/encrypt_auditor_sender_balance.json b/packages/tokens/src/confidential/circuits/lib/testdata/encrypt_auditor_sender_balance.json new file mode 100644 index 000000000..6aecdd011 --- /dev/null +++ b/packages/tokens/src/confidential/circuits/lib/testdata/encrypt_auditor_sender_balance.json @@ -0,0 +1,15 @@ +{ + "primitive": "encrypt_auditor_sender_balance", + "design_doc_refs": ["Constraint W_a3", "Constraint W_a4", "Constraint S_a3", "Constraint S_a5", "Constraint V_a3", "Constraint V_a5"], + "description": "Sender-auditor encrypted balance/allowance checkpoint via the single-squeeze sender channel sponge: b_tilde_aud_s = v_new + SpongeSqueeze_1(AUDITOR_SENDER, s_a_s_x, sigma). Used by Withdraw, RevokeOperator, and SetOperator -- the three sender-side proofs whose auditor block emits only a balance mask.", + "vectors": [ + { + "inputs": { + "v_new": "0x3e8", + "s_a_s_x": "0x12345", + "sigma": "0x01" + }, + "output": "0x266e76dccd3d7c654ab7a48045cca920c497b1a6758981acfa6f217427ac345c" + } + ] +} diff --git a/packages/tokens/src/confidential/circuits/withdraw/src/main.nr b/packages/tokens/src/confidential/circuits/withdraw/src/main.nr index ebafd70fa..f005c0623 100644 --- a/packages/tokens/src/confidential/circuits/withdraw/src/main.nr +++ b/packages/tokens/src/confidential/circuits/withdraw/src/main.nr @@ -1,13 +1,16 @@ use stellar_confidential_lib::{ - H, assert_range_128, commit, derive_spend_r, encrypt_balance, scalar_mul, vk_from_sk, + H, assert_on_curve_non_identity, assert_range_128, commit, derive_spend_r, ecdh, + encrypt_auditor_sender_balance, encrypt_balance, scalar_mul, vk_from_sk, }; +use std::embedded_curve_ops::EmbeddedCurvePoint; mod tests; // Withdraw circuit -- design doc Section 7.5. // -// Constraints (main block W1-W7; auditor block W_a1-W_a5 follows) -// --------------------------------------------------------------- +// Constraints +// ----------- +// Main block (balance conservation + ownership) // W1 Y = sk * H owner key ownership // W2 vk = Poseidon2(delta_vk, sk, wrap) wrapper-bound viewing // key @@ -24,52 +27,91 @@ mod tests; // + Poseidon2(delta_enc_bal, vk, sigma) // encrypted balance // scalar (emitted) +// Auditor block (sender-auditor visibility, Section 8.1) +// W_a1 R_e = r_e * H ephemeral key for +// auditor ECDH +// W_a2 S_{a,s} = r_e * K_{aud,s} sender-auditor ECDH +// shared secret +// W_a3 m_b = SpongeSqueeze_1(delta_aud_s, +// S_{a,s}.x, sigma) sender-channel +// sponge, single squeeze +// W_a4 b_tilde_aud_s = (v - a) + m_b sender-auditor +// encrypted balance +// checkpoint (emitted) +// W_a5 r_e != 0 rules out R_e = O and +// S_{a,s} = O, which +// would collapse m_b to +// a constant function +// of sigma +// +// W_a3 + W_a4 are encapsulated by `encrypt_auditor_sender_balance` in the lib. // -// On-curve checks for Y, C_spend, and C_spend' are not needed: all three -// are bound to in-circuit multi_scalar_mul outputs (Y in W1, C_spend in W3, -// C_spend' in W6), which are on-curve by construction. See design doc -// Section 10.8 for the system-wide point-validation doctrine. +// Point-validation doctrine (Section 10.8) +// ---------------------------------------- +// Y, C_spend, C_spend', and R_e are each bound to in-circuit multi_scalar_mul +// outputs (W1, W3, W6, W_a1) and are therefore on-curve by construction -- +// no explicit check needed. K_aud_s is a public-input key consumed by an +// ECDH multiplication; the verifier doesn't check curve membership, so an +// off-curve K_aud_s would break the soundness of W_a2. This file explicitly +// validates K_aud_s on-curve AND non-identity before W_a2. // -// Public inputs (this block: 10 fields; the auditor block will add 5 more -// in design-doc canonical order before this is shipped end-to-end) -// --------------------------------------------------------------------- -// Idx Param Symbol Source / Note -// --- ----- ------ --------------------------------------- -// 0 c_spend_x C_spend.x Loaded from `from.spendable_balance`. +// Public inputs (15 fields, in design-doc canonical order) +// -------------------------------------------------------- +// Idx Param Symbol Source / Note +// --- ----- ------ ------------------------------------- +// 0 c_spend_x C_spend.x Loaded from `from.spendable_balance`. // 1 c_spend_y C_spend.y -// 2 y_x Y.x Loaded from `from.spending_key`. +// 2 y_x Y.x Loaded from `from.spending_key`. // 3 y_y Y.y -// 4 wrap wrap `env.current_contract_address()`. -// 5 a a Public withdrawal amount; the wrapper -// pre-checks a >= 0 at the entrypoint, -// W4 then closes a < 2^128 in-circuit. -// 6 c_spend_new_x C_spend'.x Prover-supplied; written to -// 7 c_spend_new_y C_spend'.y `from.spendable_balance` on success. -// 8 sigma sigma Prover-supplied random salt; emitted. -// 9 b_tilde b_tilde Prover-supplied encrypted balance -// scalar; emitted. +// 4 wrap wrap `env.current_contract_address()`. +// 5 k_aud_s_x K_aud_s.x Fetched from the auditor contract by +// 6 k_aud_s_y K_aud_s.y `from.auditor_id`. +// 7 a a Public withdrawal amount; the wrapper +// pre-checks a >= 0 at the entrypoint, +// W4 then closes a < 2^128 in-circuit. +// 8 c_spend_new_x C_spend'.x Prover-supplied; written to +// 9 c_spend_new_y C_spend'.y `from.spendable_balance` on success. +// 10 sigma sigma Prover-supplied random salt; emitted. +// 11 b_tilde b_tilde Prover-supplied encrypted balance +// scalar; emitted. +// 12 r_e_x R_e.x Prover-supplied ephemeral key for +// 13 r_e_y R_e.y auditor ECDH; emitted. +// 14 b_tilde_aud_s b_tilde_aud_s Prover-supplied sender-auditor +// encrypted balance checkpoint; emitted. // // Private witnesses // ----------------- -// sk Spending secret scalar. -// v Plaintext spendable-balance value. -// r Plaintext blinding factor for C_spend. +// sk Spending secret scalar. +// v Plaintext spendable-balance value. +// r Plaintext blinding factor for C_spend. +// r_e Ephemeral scalar for the auditor ECDH; must satisfy r_e != 0 +// (W_a5). fn main( sk: Field, v: Field, r: Field, + r_e: Field, c_spend_x: pub Field, c_spend_y: pub Field, y_x: pub Field, y_y: pub Field, wrap: pub Field, + k_aud_s_x: pub Field, + k_aud_s_y: pub Field, a: pub Field, c_spend_new_x: pub Field, c_spend_new_y: pub Field, sigma: pub Field, b_tilde: pub Field, + r_e_x: pub Field, + r_e_y: pub Field, + b_tilde_aud_s: pub Field, ) { + // W_a5 -- runs first so the r_e = 0 attack is rejected before any + // scalar mul against it could quietly produce the identity. + assert(r_e != 0); + // W1 let y_derived = scalar_mul(sk, H); assert(y_derived.x == y_x); @@ -100,4 +142,20 @@ fn main( // W7 let b_tilde_derived = encrypt_balance(v_new, vk, sigma); assert(b_tilde_derived == b_tilde); + + // W_a1 + let r_e_derived = scalar_mul(r_e, H); + assert(r_e_derived.x == r_e_x); + assert(r_e_derived.y == r_e_y); + + // K_aud_s point validation (Section 10.8: public-input key). + let k_aud_s = EmbeddedCurvePoint { x: k_aud_s_x, y: k_aud_s_y, is_infinite: false }; + assert_on_curve_non_identity(k_aud_s); + + // W_a2 (shared secret x-coordinate) + let s_a_s_x = ecdh(r_e, k_aud_s); + + // W_a3 + W_a4 + let b_tilde_aud_s_derived = encrypt_auditor_sender_balance(v_new, s_a_s_x, sigma); + assert(b_tilde_aud_s_derived == b_tilde_aud_s); } diff --git a/packages/tokens/src/confidential/circuits/withdraw/src/tests.nr b/packages/tokens/src/confidential/circuits/withdraw/src/tests.nr index a09d8ed4c..ccdacffd9 100644 --- a/packages/tokens/src/confidential/circuits/withdraw/src/tests.nr +++ b/packages/tokens/src/confidential/circuits/withdraw/src/tests.nr @@ -1,13 +1,18 @@ use crate::main; -use stellar_confidential_lib::{commit, derive_spend_r, encrypt_balance, H, scalar_mul, vk_from_sk}; +use stellar_confidential_lib::{ + commit, derive_spend_r, ecdh, encrypt_auditor_sender_balance, encrypt_balance, H, scalar_mul, + vk_from_sk, +}; +use std::embedded_curve_ops::EmbeddedCurvePoint; // Canonical fixture inputs. // // SK / WRAP / SIGMA / V / R reuse the lib's pinned vectors (lib/testdata) // so that Y, C_spend, vk, r_new, and b_tilde are derivable from -- and -// pinned by -- the lib's `fixtures_match_testdata` test. A is chosen to -// be strictly less than V so the happy path exercises a non-trivial -// `v_new = v - a` while still satisfying W4. +// pinned by -- the lib's `fixtures_match_testdata` test. A is chosen +// strictly less than V so the happy path exercises a non-trivial +// `v_new = v - a` while still satisfying W4. R_E and K_AUD_S are local +// to this circuit (no auditor-side primitives in lib's fixtures). global SK: Field = 0xdead; global WRAP: Field = 0xbeef; global SIGMA: Field = 0x01; @@ -15,6 +20,12 @@ global V: Field = 1000; global R: Field = 42; global A: Field = 300; global V_NEW: Field = 700; +global R_E: Field = 0xfeedface; + +// K_aud_s = `sk_aud * H` for a placeholder auditor secret 0xc0ffee. Any +// non-identity Grumpkin point would do; reusing scalar_mul on a constant +// scalar keeps the value derivable from lib primitives. +global K_AUD_S_SCALAR: Field = 0xc0ffee; // Y = sk*H (= lib `scalar_mul_Y` fixture). global Y_X: Field = 0x1b46b003b88a6c34549dc74115f088f4b231a151397526bc10cbf1d15b457646; @@ -36,29 +47,51 @@ global C_SPEND_NEW_Y: Field = global B_TILDE: Field = 0x2b7f92c72c05ae8ecf7207fdaecea32ef748cd4dd43ca9ed6dc6d226865b4283; +// Auditor-block fixtures. Pinned by `withdraw_auditor_fixtures_match_lib`. +// R_E_X (= (R_E * H).x) matches the lib's `ecdh` fixture (`ecdh(r_e, H)` for +// the same r_e), pinning the cross-circuit relationship between R_e and the +// lib's scalar-mul output. +global K_AUD_S_X: Field = + 0x22502c7b20b64aeaaaa4d4fa5f2f8600f9734e828f7284a8d1431da36b33803a; +global K_AUD_S_Y: Field = + 0x0419089353d24334f03f4a1c9c9b19282e32f6879c5c69b9cdffba6a13a7c5ed; +global R_E_X: Field = 0x114ed4fcf2c57014eb678c577aa02f30ef590b713d7a6a5e87702d1c7f71957f; +global R_E_Y: Field = 0x07a70cf826350d4f438c7a3c5e8761b0ae6cb63de757f0c96815f4057b9205f4; +global B_TILDE_AUD_S: Field = + 0x216ab12ea2182712721d79c9a9222a261452ab06a90309e23efaa6ecb0e20714; + #[test] fn print_fixtures() { - // One-shot harness: prints C_spend', b_tilde for the chosen (SK, WRAP, - // SIGMA, V, R, A) tuple. Run with + // One-shot harness: prints C_spend', b_tilde, K_aud_s, R_e, b_tilde_aud_s + // for the chosen fixture tuple. Run with // `nargo test --package circuit_withdraw print_fixtures --show-output` - // and paste the values into the C_SPEND_NEW_* / B_TILDE globals above. + // and paste the values into the globals above. let vk = vk_from_sk(SK, WRAP); let r_new = derive_spend_r(vk, SIGMA); let c_new = commit(V_NEW, r_new); let b_tilde = encrypt_balance(V_NEW, vk, SIGMA); + let k_aud_s = scalar_mul(K_AUD_S_SCALAR, H); + let r_e_pt = scalar_mul(R_E, H); + let s_a_s_x = ecdh(R_E, k_aud_s); + let b_tilde_aud_s = encrypt_auditor_sender_balance(V_NEW, s_a_s_x, SIGMA); let cx = c_new.x; let cy = c_new.y; + let kx = k_aud_s.x; + let ky = k_aud_s.y; + let rx = r_e_pt.x; + let ry = r_e_pt.y; println(f"C_SPEND_NEW_X = {cx}"); println(f"C_SPEND_NEW_Y = {cy}"); println(f"B_TILDE = {b_tilde}"); + println(f"K_AUD_S_X = {kx}"); + println(f"K_AUD_S_Y = {ky}"); + println(f"R_E_X = {rx}"); + println(f"R_E_Y = {ry}"); + println(f"B_TILDE_AUD_S = {b_tilde_aud_s}"); } #[test] fn withdraw_fixtures_match_lib() { - // Cross-check the hard-coded fixture values against the lib's - // primitives. If a lib primitive's output changes the fixture - // assertions break here -- which is exactly when the rest of the - // withdraw tests would silently start using stale public inputs. let vk = vk_from_sk(SK, WRAP); let r_new = derive_spend_r(vk, SIGMA); let c_new = commit(V_NEW, r_new); @@ -75,229 +108,451 @@ fn withdraw_fixtures_match_lib() { assert(c_spend.y == C_SPEND_Y); } +#[test] +fn withdraw_auditor_fixtures_match_lib() { + let k_aud_s = scalar_mul(K_AUD_S_SCALAR, H); + let r_e_pt = scalar_mul(R_E, H); + let s_a_s_x = ecdh(R_E, k_aud_s); + let b_tilde_aud_s = encrypt_auditor_sender_balance(V_NEW, s_a_s_x, SIGMA); + assert(k_aud_s.x == K_AUD_S_X); + assert(k_aud_s.y == K_AUD_S_Y); + assert(r_e_pt.x == R_E_X); + assert(r_e_pt.y == R_E_Y); + assert(b_tilde_aud_s == B_TILDE_AUD_S); +} + #[test] fn matches_fixture() { main( SK, V, R, + R_E, C_SPEND_X, C_SPEND_Y, Y_X, Y_Y, WRAP, + K_AUD_S_X, + K_AUD_S_Y, A, C_SPEND_NEW_X, C_SPEND_NEW_Y, SIGMA, B_TILDE, + R_E_X, + R_E_Y, + B_TILDE_AUD_S, ); } #[test] fn full_withdrawal() { - // Boundary: a = v, so v_new = 0. C_spend' commits to (0, r_new) which - // is a valid commitment to zero (and is non-identity because r_new is - // a Poseidon output, vanishingly unlikely to equal 0). W6 binds it. + // Boundary: a = v, so v_new = 0. C_spend' commits to (0, r_new) which is + // a valid commitment to zero. b_tilde and b_tilde_aud_s both bind to 0 + // (plus mask) so the auditor sees the post-withdrawal balance reach zero. let vk = vk_from_sk(SK, WRAP); let r_new = derive_spend_r(vk, SIGMA); let c_new = commit(0, r_new); let b_tilde = encrypt_balance(0, vk, SIGMA); + let k_aud_s = EmbeddedCurvePoint { x: K_AUD_S_X, y: K_AUD_S_Y, is_infinite: false }; + let s_a_s_x = ecdh(R_E, k_aud_s); + let b_tilde_aud_s = encrypt_auditor_sender_balance(0, s_a_s_x, SIGMA); main( SK, V, R, + R_E, C_SPEND_X, C_SPEND_Y, Y_X, Y_Y, WRAP, + K_AUD_S_X, + K_AUD_S_Y, V, c_new.x, c_new.y, SIGMA, b_tilde, + R_E_X, + R_E_Y, + b_tilde_aud_s, ); } #[test(should_fail)] fn rejects_under_funded_withdrawal() { - // a > v: v - a underflows in the field to a value >= 2^128. W4's - // range check on v_new fires before any downstream constraint, so - // this is exactly the under-funded-withdraw soundness gap closed. + // a > v: v - a underflows in the field to a value >= 2^128. W4 fires. let a_too_large: Field = V + 1; - // C_spend_new is whatever the (necessarily wrong) prover supplies; - // W6 would also fail, but W4 should fail first. Pick C_spend_new for - // some valid v_new derivation just to keep the test focused on W4. let vk = vk_from_sk(SK, WRAP); let r_new = derive_spend_r(vk, SIGMA); let c_new_invalid = commit(V - a_too_large, r_new); let b_tilde_invalid = encrypt_balance(V - a_too_large, vk, SIGMA); + let k_aud_s = EmbeddedCurvePoint { x: K_AUD_S_X, y: K_AUD_S_Y, is_infinite: false }; + let s_a_s_x = ecdh(R_E, k_aud_s); + let b_tilde_aud_s_invalid = encrypt_auditor_sender_balance(V - a_too_large, s_a_s_x, SIGMA); main( SK, V, R, + R_E, C_SPEND_X, C_SPEND_Y, Y_X, Y_Y, WRAP, + K_AUD_S_X, + K_AUD_S_Y, a_too_large, c_new_invalid.x, c_new_invalid.y, SIGMA, b_tilde_invalid, + R_E_X, + R_E_Y, + b_tilde_aud_s_invalid, ); } #[test(should_fail)] fn rejects_v_out_of_range() { - // v claimed by the prover is >= 2^128: W4 rejects on the v check - // before W3 even looks at C_spend. let v_huge: Field = 0x100000000000000000000000000000000; // 2^128 main( SK, v_huge, R, + R_E, C_SPEND_X, C_SPEND_Y, Y_X, Y_Y, WRAP, + K_AUD_S_X, + K_AUD_S_Y, A, C_SPEND_NEW_X, C_SPEND_NEW_Y, SIGMA, B_TILDE, + R_E_X, + R_E_Y, + B_TILDE_AUD_S, ); } #[test(should_fail)] fn rejects_a_out_of_range() { - // a >= 2^128: W4's range check on a fails. (The wrapper's a >= 0 - // entrypoint check is out-of-scope for the circuit; here we only - // close the upper bound.) let a_huge: Field = 0x100000000000000000000000000000000; main( SK, V, R, + R_E, C_SPEND_X, C_SPEND_Y, Y_X, Y_Y, WRAP, + K_AUD_S_X, + K_AUD_S_Y, a_huge, C_SPEND_NEW_X, C_SPEND_NEW_Y, SIGMA, B_TILDE, + R_E_X, + R_E_Y, + B_TILDE_AUD_S, ); } #[test(should_fail)] fn rejects_wrong_sk() { - // sk does not derive the claimed Y: W1 fails. main( SK + 1, V, R, + R_E, C_SPEND_X, C_SPEND_Y, Y_X, Y_Y, WRAP, + K_AUD_S_X, + K_AUD_S_Y, A, C_SPEND_NEW_X, C_SPEND_NEW_Y, SIGMA, B_TILDE, + R_E_X, + R_E_Y, + B_TILDE_AUD_S, ); } #[test(should_fail)] fn rejects_wrong_balance_opening() { - // (v, r) supplied don't open C_spend: W3 fails. Use a v that still - // passes the W4 range check so the failure is squarely W3. main( SK, V + 1, R, + R_E, C_SPEND_X, C_SPEND_Y, Y_X, Y_Y, WRAP, + K_AUD_S_X, + K_AUD_S_Y, A, C_SPEND_NEW_X, C_SPEND_NEW_Y, SIGMA, B_TILDE, + R_E_X, + R_E_Y, + B_TILDE_AUD_S, ); } #[test(should_fail)] fn rejects_wrong_wrap() { - // wrap != WRAP under which Y was derived. vk recomputes under the - // new wrap, the new vk produces a different r_new (W5) and a - // different b_tilde (W7). W6 / W7 fire. (W1 still passes because Y - // is the prover-claimed Y and sk*H is recomputed against it without - // using wrap.) main( SK, V, R, + R_E, C_SPEND_X, C_SPEND_Y, Y_X, Y_Y, 0xcafe, + K_AUD_S_X, + K_AUD_S_Y, A, C_SPEND_NEW_X, C_SPEND_NEW_Y, SIGMA, B_TILDE, + R_E_X, + R_E_Y, + B_TILDE_AUD_S, ); } #[test(should_fail)] fn rejects_tampered_b_tilde() { - // b_tilde mutated by +1: W7 fails because encrypt_balance derives a - // specific scalar that no longer matches. main( SK, V, R, + R_E, C_SPEND_X, C_SPEND_Y, Y_X, Y_Y, WRAP, + K_AUD_S_X, + K_AUD_S_Y, A, C_SPEND_NEW_X, C_SPEND_NEW_Y, SIGMA, B_TILDE + 1, + R_E_X, + R_E_Y, + B_TILDE_AUD_S, ); } #[test(should_fail)] fn rejects_tampered_c_spend_new() { - // C_spend' supplied with wrong x: W6 fails because the prover's - // claim no longer matches the in-circuit (v - a)*G + r'*H. main( SK, V, R, + R_E, C_SPEND_X, C_SPEND_Y, Y_X, Y_Y, WRAP, + K_AUD_S_X, + K_AUD_S_Y, A, C_SPEND_NEW_X + 1, C_SPEND_NEW_Y, SIGMA, B_TILDE, + R_E_X, + R_E_Y, + B_TILDE_AUD_S, + ); +} + +#[test(should_fail)] +fn rejects_r_e_zero() { + // W_a5 fires: r_e = 0 would force R_e = O (identity) and collapse + // S_a,s to O, making m_b a knowable constant function of sigma. + main( + SK, + V, + R, + 0, + C_SPEND_X, + C_SPEND_Y, + Y_X, + Y_Y, + WRAP, + K_AUD_S_X, + K_AUD_S_Y, + A, + C_SPEND_NEW_X, + C_SPEND_NEW_Y, + SIGMA, + B_TILDE, + R_E_X, + R_E_Y, + B_TILDE_AUD_S, + ); +} + +#[test(should_fail)] +fn rejects_wrong_r_e() { + // r_e mismatches the public R_e: W_a1 fails because scalar_mul(r_e, H) + // no longer equals the claimed (R_e_x, R_e_y). + main( + SK, + V, + R, + R_E + 1, + C_SPEND_X, + C_SPEND_Y, + Y_X, + Y_Y, + WRAP, + K_AUD_S_X, + K_AUD_S_Y, + A, + C_SPEND_NEW_X, + C_SPEND_NEW_Y, + SIGMA, + B_TILDE, + R_E_X, + R_E_Y, + B_TILDE_AUD_S, + ); +} + +#[test(should_fail)] +fn rejects_off_curve_k_aud_s() { + // (1, 2) is off-curve: 4 != 1 - 17. The on-curve check in main() + // fires before W_a2 ever consumes K_aud_s. + main( + SK, + V, + R, + R_E, + C_SPEND_X, + C_SPEND_Y, + Y_X, + Y_Y, + WRAP, + 1, + 2, + A, + C_SPEND_NEW_X, + C_SPEND_NEW_Y, + SIGMA, + B_TILDE, + R_E_X, + R_E_Y, + B_TILDE_AUD_S, + ); +} + +#[test(should_fail)] +fn rejects_identity_k_aud_s() { + // K_aud_s = identity collapses ECDH (S = r_e * O = O), so the on-curve + // check explicitly rejects the identity for keys. + main( + SK, + V, + R, + R_E, + C_SPEND_X, + C_SPEND_Y, + Y_X, + Y_Y, + WRAP, + 0, + 0, + A, + C_SPEND_NEW_X, + C_SPEND_NEW_Y, + SIGMA, + B_TILDE, + R_E_X, + R_E_Y, + B_TILDE_AUD_S, + ); +} + +#[test(should_fail)] +fn rejects_tampered_b_tilde_aud_s() { + // b_tilde_aud_s mutated by +1: W_a3+W_a4 fail because + // encrypt_auditor_sender_balance derives a specific scalar that no + // longer matches the prover's claim. Closes the + // tampered-ciphertext criterion in the issue. + main( + SK, + V, + R, + R_E, + C_SPEND_X, + C_SPEND_Y, + Y_X, + Y_Y, + WRAP, + K_AUD_S_X, + K_AUD_S_Y, + A, + C_SPEND_NEW_X, + C_SPEND_NEW_Y, + SIGMA, + B_TILDE, + R_E_X, + R_E_Y, + B_TILDE_AUD_S + 1, + ); +} + +#[test(should_fail)] +fn rejects_wrong_k_aud_s() { + // K_aud_s replaced by a *valid* but different on-curve key (Y_X, Y_Y is + // sk*H from the fixtures, definitely on-curve and non-identity). The + // shared secret changes, m_b changes, W_a4 fails. + main( + SK, + V, + R, + R_E, + C_SPEND_X, + C_SPEND_Y, + Y_X, + Y_Y, + WRAP, + Y_X, + Y_Y, + A, + C_SPEND_NEW_X, + C_SPEND_NEW_Y, + SIGMA, + B_TILDE, + R_E_X, + R_E_Y, + B_TILDE_AUD_S, ); } From ca2d248635bf0df0488cbfa872950d0f9c9ee388 Mon Sep 17 00:00:00 2001 From: brozorec <9572072+brozorec@users.noreply.github.com> Date: Fri, 22 May 2026 12:17:25 +0200 Subject: [PATCH 04/16] feat(confidential): publish withdraw verification key 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. --- packages/tokens/src/confidential/circuits/vks/withdraw.vk.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 packages/tokens/src/confidential/circuits/vks/withdraw.vk.json diff --git a/packages/tokens/src/confidential/circuits/vks/withdraw.vk.json b/packages/tokens/src/confidential/circuits/vks/withdraw.vk.json new file mode 100644 index 000000000..7eb07aada --- /dev/null +++ b/packages/tokens/src/confidential/circuits/vks/withdraw.vk.json @@ -0,0 +1 @@ +["0x0000000000000000000000000000000000000000000000000000000000008000","0x000000000000000000000000000000000000000000000000000000000000001f","0x0000000000000000000000000000000000000000000000000000000000000001","0x000000000000000000000000000000000000000000000000000000000000000f","0x00000000000000000000000000000089a373ad605ece6a3fe747d2480745e0cc","0x000000000000000000000000000000000005a17fb6eb6073e8d6beeb6c1b9197","0x000000000000000000000000000000904e32c9d3717a6c6d319630fc1fe4b7ae","0x00000000000000000000000000000000002b788c60ecb83b731a2bc623e51bb5","0x0000000000000000000000000000006570ff78b488437ebfa1f4dcbc055d1044","0x00000000000000000000000000000000000f2ab92beb0c1b02472274cebc2541","0x000000000000000000000000000000fb733f474eae795512fec5aa2bded0da8b","0x00000000000000000000000000000000000dbe274e3bab2b97bcc3da51162950","0x000000000000000000000000000000335ae9830c388b0afcca37990478161780","0x000000000000000000000000000000000014790edfe8ec563d36ccc113f79a1b","0x0000000000000000000000000000001964e8eb5b7700b35b381589c55c0feb6a","0x000000000000000000000000000000000026775d2d1fd9d69ec8e262317506c4","0x0000000000000000000000000000003fb155d9788484afa4fe369b3bfa7386fa","0x000000000000000000000000000000000008bc103927def69c43cd94a08efdae","0x00000000000000000000000000000063619d1ad3cde1f50ac8941e8580b8ee08","0x00000000000000000000000000000000002492bee4fc2c6765c990b5079b4b78","0x0000000000000000000000000000005932a81b749fb08e27c389a204a0c9a66c","0x0000000000000000000000000000000000268bd34c8b97d624a3bba40deaf76e","0x00000000000000000000000000000054dd9da135d37ffaa710640414c309c17b","0x00000000000000000000000000000000001a30e0a54ac45727d3a83948960850","0x00000000000000000000000000000072c314a26f9ce5a3818b0168245760e9a2","0x000000000000000000000000000000000002d4d4b4ab3de4b3cd0960b83521f7","0x000000000000000000000000000000d4eaebcabfc8fff8a8677ddcd19142ad44","0x0000000000000000000000000000000000228117aa076f6b0924133e5d887709","0x000000000000000000000000000000176df2b9f9816b99b87dcd27314f248492","0x00000000000000000000000000000000000396fae9cfc50f85ebfe6bf44327d7","0x0000000000000000000000000000000107a790c778e6a2ffbae75e5764ee8cc2","0x00000000000000000000000000000000000e0f2756936ef0c0b2175663320713","0x00000000000000000000000000000023f9d11f3ecfe7f62be9524c863c1dc874","0x00000000000000000000000000000000002da3916ecb67c00f42a962473b0d60","0x0000000000000000000000000000008cbb64648b81e2d88c2e2acd9411de2c04","0x000000000000000000000000000000000003e3716c735f53222476c645f8c1df","0x000000000000000000000000000000c0deabd5bc078f4c2e34347e51dc08665d","0x00000000000000000000000000000000000b1e3794549547ce84cf504ef1f790","0x00000000000000000000000000000017435a1130a8f09b7f008b039f980e497f","0x0000000000000000000000000000000000183d7725e4086d845e7a073c091bed","0x000000000000000000000000000000f203bc6dd4bbf94d23f2c1134d42812ef2","0x00000000000000000000000000000000001c8ae3984efef7332b3e2f8e5bdb11","0x00000000000000000000000000000055523cf125d6c105b01a81e18a93385d73","0x000000000000000000000000000000000027af0adb42d6dfe987a5eda810f1ac","0x00000000000000000000000000000045b2c7e86bebc1bc2cc666e0c899eb8188","0x00000000000000000000000000000000001f955cb230f7dbec9465ee91468b40","0x00000000000000000000000000000057cccf985e9af0dadeef55c723765d87b6","0x0000000000000000000000000000000000197705eab64da334874994ccd8e0e4","0x00000000000000000000000000000068a2c3be28e298ec24ee52aea586306737","0x00000000000000000000000000000000000946c0d07552c2907bf4dbd07f23b3","0x000000000000000000000000000000527b1f269e0f9182b056a4c4f1bad72734","0x0000000000000000000000000000000000123fcf7b03c0f520b4c02284aa7263","0x000000000000000000000000000000aa7318c0a706dc9935f6254f820a290f8c","0x00000000000000000000000000000000002fe7f885bd76d63a52630afb2eb96c","0x0000000000000000000000000000000b2806b41403a451356b2a8ec66f451d10","0x00000000000000000000000000000000000a177c20fe67c97e7e49f73699070d","0x000000000000000000000000000000923eb1e0f63ad4e00142a457137470095c","0x000000000000000000000000000000000024978f1ed1f0abaf316b3e968d35ed","0x000000000000000000000000000000baf6b35de3a424c3557a63eae09eac22f9","0x00000000000000000000000000000000002647f354f0072d3519269dd79fa14b","0x000000000000000000000000000000c2d9700620ae22fd6bdf4d528eef1d35ae","0x000000000000000000000000000000000016eb34c2d447770351e801cc9b004c","0x000000000000000000000000000000f9b479b749dfebf01b58c841109945b9d8","0x0000000000000000000000000000000000135fbaabb54c672d9f580e958f8a2a","0x0000000000000000000000000000000cc5e4dfb7bdeef3d214b54354f9a0adb4","0x000000000000000000000000000000000002d0a67f80ae376ffa582cc0316ea9","0x000000000000000000000000000000258db7eba59c614306d884b705c74f36c5","0x00000000000000000000000000000000002f438da1a6fed88d2cb38b02f67f4b","0x00000000000000000000000000000020809242a456386d49aeb7d2fdee2dc874","0x0000000000000000000000000000000000264d7ba415e912a7e32c327970770a","0x00000000000000000000000000000030e4c165de1305183c731034d89d1df666","0x000000000000000000000000000000000023b6d9eaded11e563905a463b38a18","0x0000000000000000000000000000005bae16c4c2d40aee47692aa29579359e57","0x00000000000000000000000000000000000c64d0f337645b360ac89550b52eab","0x000000000000000000000000000000e447cb5ccd9365e3a14e6f8dd3f45b32d5","0x00000000000000000000000000000000002e23a5d1c317ee92d18515798bfc8d","0x00000000000000000000000000000098ccdc7fcb669a7d2e8558ec750e2fa612","0x00000000000000000000000000000000000c92ae44da6be8e0f691d2c4528e88","0x0000000000000000000000000000007e6dfab12df0cafc95f92ebb4512d23a9b","0x00000000000000000000000000000000002b452643d7967666962604fb48a4b4","0x0000000000000000000000000000002f14092777e6d6fb8684664e7df7cb6a33","0x000000000000000000000000000000000012242645114a2ae75392abb64a0235","0x00000000000000000000000000000074ea6aca418b22396785498539cd785fab","0x000000000000000000000000000000000009988525d643f69d57290850acf393","0x0000000000000000000000000000004f37d2f15db09dae0845148569ddc663fd","0x00000000000000000000000000000000002fc85d102a6c77e4d471e84d0502af","0x0000000000000000000000000000007b7fc7044729e116cb97a297baaf1eba3f","0x0000000000000000000000000000000000138d5b07df3374d865bf8ec55d3ccc","0x000000000000000000000000000000984d80910016772cb83ed3881f90ebcc8c","0x00000000000000000000000000000000001b6540dd28b967d06897d2fe4f4732","0x000000000000000000000000000000b7445646b5160c380130b66e34450aa736","0x00000000000000000000000000000000000ef8897e2c65d0ec75dc09f85edeac","0x00000000000000000000000000000045688ef774d85c8bd41f9665bad2d794c8","0x00000000000000000000000000000000002ac07a74c19ad88489ed6f1e93dc9e","0x000000000000000000000000000000a5a32649850aae3c839626ec285717f399","0x000000000000000000000000000000000010cdf0710877e231d375a2ef4dc3d3","0x000000000000000000000000000000160d4e95b954b96077723f1800f420dc77","0x0000000000000000000000000000000000280ae74a2dae2f5e68706031174b76","0x000000000000000000000000000000440bb5fc5471f6a37dbe12fbef6a37c009","0x0000000000000000000000000000000000106864b3460bfded9d5dc448fe29e8","0x000000000000000000000000000000d29de5d4acc99c6c72fa17e0a80bf88401","0x000000000000000000000000000000000007e83dfa443e984d53e0323523f742","0x0000000000000000000000000000001cfe095fa8e5d91145e1222746c27083ad","0x000000000000000000000000000000000006a7d963d96d78bc035faf0a744f8e","0x0000000000000000000000000000000000000000000000000000000000000001","0x0000000000000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000000000000000000000000000000000000002","0x0000000000000000000000000000000000000000000000000000000000000000","0x000000000000000000000000000000de9e600862f91873e12f0d906fe6fb4b35","0x0000000000000000000000000000000000268e1172734cd620010a0e7bd33318","0x0000000000000000000000000000006222cac69b3a4024afa430991710a6fe53","0x000000000000000000000000000000000018c0b8f57b190c18dcd096aa188b5d"] \ No newline at end of file From eb3dfda71b4f7dcfba19c94a3906d320318a065f Mon Sep 17 00:00:00 2001 From: brozorec <9572072+brozorec@users.noreply.github.com> Date: Fri, 22 May 2026 15:00:09 +0200 Subject: [PATCH 05/16] feat(confidential): tighten withdraw range to [0, 2^127) per design doc MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../circuits/constraints.baseline | 3 +-- .../circuits/vks/withdraw.vk.json | 2 +- .../circuits/withdraw/src/main.nr | 27 ++++++++++++------- .../circuits/withdraw/src/tests.nr | 8 ++++-- 4 files changed, 25 insertions(+), 15 deletions(-) diff --git a/packages/tokens/src/confidential/circuits/constraints.baseline b/packages/tokens/src/confidential/circuits/constraints.baseline index 512ad4a2c..37eb2232c 100644 --- a/packages/tokens/src/confidential/circuits/constraints.baseline +++ b/packages/tokens/src/confidential/circuits/constraints.baseline @@ -23,10 +23,9 @@ | circuit_register | lte_hint | N/A | N/A | 33 | | circuit_register | main | Bounded { width: 4 } | 33 | 72 | | circuit_withdraw | decompose_hint | N/A | N/A | 30 | -| circuit_withdraw | directive_integer_quotient | N/A | N/A | 8 | | circuit_withdraw | directive_invert | N/A | N/A | 9 | | circuit_withdraw | lte_hint | N/A | N/A | 33 | -| circuit_withdraw | main | Bounded { width: 4 } | 130 | 80 | +| circuit_withdraw | main | Bounded { width: 4 } | 92 | 72 | | gadget_assert_on_curve | main | Bounded { width: 4 } | 2 | 0 | | gadget_commit | decompose_hint | N/A | N/A | 30 | | gadget_commit | lte_hint | N/A | N/A | 33 | diff --git a/packages/tokens/src/confidential/circuits/vks/withdraw.vk.json b/packages/tokens/src/confidential/circuits/vks/withdraw.vk.json index 7eb07aada..abc61d4a2 100644 --- a/packages/tokens/src/confidential/circuits/vks/withdraw.vk.json +++ b/packages/tokens/src/confidential/circuits/vks/withdraw.vk.json @@ -1 +1 @@ -["0x0000000000000000000000000000000000000000000000000000000000008000","0x000000000000000000000000000000000000000000000000000000000000001f","0x0000000000000000000000000000000000000000000000000000000000000001","0x000000000000000000000000000000000000000000000000000000000000000f","0x00000000000000000000000000000089a373ad605ece6a3fe747d2480745e0cc","0x000000000000000000000000000000000005a17fb6eb6073e8d6beeb6c1b9197","0x000000000000000000000000000000904e32c9d3717a6c6d319630fc1fe4b7ae","0x00000000000000000000000000000000002b788c60ecb83b731a2bc623e51bb5","0x0000000000000000000000000000006570ff78b488437ebfa1f4dcbc055d1044","0x00000000000000000000000000000000000f2ab92beb0c1b02472274cebc2541","0x000000000000000000000000000000fb733f474eae795512fec5aa2bded0da8b","0x00000000000000000000000000000000000dbe274e3bab2b97bcc3da51162950","0x000000000000000000000000000000335ae9830c388b0afcca37990478161780","0x000000000000000000000000000000000014790edfe8ec563d36ccc113f79a1b","0x0000000000000000000000000000001964e8eb5b7700b35b381589c55c0feb6a","0x000000000000000000000000000000000026775d2d1fd9d69ec8e262317506c4","0x0000000000000000000000000000003fb155d9788484afa4fe369b3bfa7386fa","0x000000000000000000000000000000000008bc103927def69c43cd94a08efdae","0x00000000000000000000000000000063619d1ad3cde1f50ac8941e8580b8ee08","0x00000000000000000000000000000000002492bee4fc2c6765c990b5079b4b78","0x0000000000000000000000000000005932a81b749fb08e27c389a204a0c9a66c","0x0000000000000000000000000000000000268bd34c8b97d624a3bba40deaf76e","0x00000000000000000000000000000054dd9da135d37ffaa710640414c309c17b","0x00000000000000000000000000000000001a30e0a54ac45727d3a83948960850","0x00000000000000000000000000000072c314a26f9ce5a3818b0168245760e9a2","0x000000000000000000000000000000000002d4d4b4ab3de4b3cd0960b83521f7","0x000000000000000000000000000000d4eaebcabfc8fff8a8677ddcd19142ad44","0x0000000000000000000000000000000000228117aa076f6b0924133e5d887709","0x000000000000000000000000000000176df2b9f9816b99b87dcd27314f248492","0x00000000000000000000000000000000000396fae9cfc50f85ebfe6bf44327d7","0x0000000000000000000000000000000107a790c778e6a2ffbae75e5764ee8cc2","0x00000000000000000000000000000000000e0f2756936ef0c0b2175663320713","0x00000000000000000000000000000023f9d11f3ecfe7f62be9524c863c1dc874","0x00000000000000000000000000000000002da3916ecb67c00f42a962473b0d60","0x0000000000000000000000000000008cbb64648b81e2d88c2e2acd9411de2c04","0x000000000000000000000000000000000003e3716c735f53222476c645f8c1df","0x000000000000000000000000000000c0deabd5bc078f4c2e34347e51dc08665d","0x00000000000000000000000000000000000b1e3794549547ce84cf504ef1f790","0x00000000000000000000000000000017435a1130a8f09b7f008b039f980e497f","0x0000000000000000000000000000000000183d7725e4086d845e7a073c091bed","0x000000000000000000000000000000f203bc6dd4bbf94d23f2c1134d42812ef2","0x00000000000000000000000000000000001c8ae3984efef7332b3e2f8e5bdb11","0x00000000000000000000000000000055523cf125d6c105b01a81e18a93385d73","0x000000000000000000000000000000000027af0adb42d6dfe987a5eda810f1ac","0x00000000000000000000000000000045b2c7e86bebc1bc2cc666e0c899eb8188","0x00000000000000000000000000000000001f955cb230f7dbec9465ee91468b40","0x00000000000000000000000000000057cccf985e9af0dadeef55c723765d87b6","0x0000000000000000000000000000000000197705eab64da334874994ccd8e0e4","0x00000000000000000000000000000068a2c3be28e298ec24ee52aea586306737","0x00000000000000000000000000000000000946c0d07552c2907bf4dbd07f23b3","0x000000000000000000000000000000527b1f269e0f9182b056a4c4f1bad72734","0x0000000000000000000000000000000000123fcf7b03c0f520b4c02284aa7263","0x000000000000000000000000000000aa7318c0a706dc9935f6254f820a290f8c","0x00000000000000000000000000000000002fe7f885bd76d63a52630afb2eb96c","0x0000000000000000000000000000000b2806b41403a451356b2a8ec66f451d10","0x00000000000000000000000000000000000a177c20fe67c97e7e49f73699070d","0x000000000000000000000000000000923eb1e0f63ad4e00142a457137470095c","0x000000000000000000000000000000000024978f1ed1f0abaf316b3e968d35ed","0x000000000000000000000000000000baf6b35de3a424c3557a63eae09eac22f9","0x00000000000000000000000000000000002647f354f0072d3519269dd79fa14b","0x000000000000000000000000000000c2d9700620ae22fd6bdf4d528eef1d35ae","0x000000000000000000000000000000000016eb34c2d447770351e801cc9b004c","0x000000000000000000000000000000f9b479b749dfebf01b58c841109945b9d8","0x0000000000000000000000000000000000135fbaabb54c672d9f580e958f8a2a","0x0000000000000000000000000000000cc5e4dfb7bdeef3d214b54354f9a0adb4","0x000000000000000000000000000000000002d0a67f80ae376ffa582cc0316ea9","0x000000000000000000000000000000258db7eba59c614306d884b705c74f36c5","0x00000000000000000000000000000000002f438da1a6fed88d2cb38b02f67f4b","0x00000000000000000000000000000020809242a456386d49aeb7d2fdee2dc874","0x0000000000000000000000000000000000264d7ba415e912a7e32c327970770a","0x00000000000000000000000000000030e4c165de1305183c731034d89d1df666","0x000000000000000000000000000000000023b6d9eaded11e563905a463b38a18","0x0000000000000000000000000000005bae16c4c2d40aee47692aa29579359e57","0x00000000000000000000000000000000000c64d0f337645b360ac89550b52eab","0x000000000000000000000000000000e447cb5ccd9365e3a14e6f8dd3f45b32d5","0x00000000000000000000000000000000002e23a5d1c317ee92d18515798bfc8d","0x00000000000000000000000000000098ccdc7fcb669a7d2e8558ec750e2fa612","0x00000000000000000000000000000000000c92ae44da6be8e0f691d2c4528e88","0x0000000000000000000000000000007e6dfab12df0cafc95f92ebb4512d23a9b","0x00000000000000000000000000000000002b452643d7967666962604fb48a4b4","0x0000000000000000000000000000002f14092777e6d6fb8684664e7df7cb6a33","0x000000000000000000000000000000000012242645114a2ae75392abb64a0235","0x00000000000000000000000000000074ea6aca418b22396785498539cd785fab","0x000000000000000000000000000000000009988525d643f69d57290850acf393","0x0000000000000000000000000000004f37d2f15db09dae0845148569ddc663fd","0x00000000000000000000000000000000002fc85d102a6c77e4d471e84d0502af","0x0000000000000000000000000000007b7fc7044729e116cb97a297baaf1eba3f","0x0000000000000000000000000000000000138d5b07df3374d865bf8ec55d3ccc","0x000000000000000000000000000000984d80910016772cb83ed3881f90ebcc8c","0x00000000000000000000000000000000001b6540dd28b967d06897d2fe4f4732","0x000000000000000000000000000000b7445646b5160c380130b66e34450aa736","0x00000000000000000000000000000000000ef8897e2c65d0ec75dc09f85edeac","0x00000000000000000000000000000045688ef774d85c8bd41f9665bad2d794c8","0x00000000000000000000000000000000002ac07a74c19ad88489ed6f1e93dc9e","0x000000000000000000000000000000a5a32649850aae3c839626ec285717f399","0x000000000000000000000000000000000010cdf0710877e231d375a2ef4dc3d3","0x000000000000000000000000000000160d4e95b954b96077723f1800f420dc77","0x0000000000000000000000000000000000280ae74a2dae2f5e68706031174b76","0x000000000000000000000000000000440bb5fc5471f6a37dbe12fbef6a37c009","0x0000000000000000000000000000000000106864b3460bfded9d5dc448fe29e8","0x000000000000000000000000000000d29de5d4acc99c6c72fa17e0a80bf88401","0x000000000000000000000000000000000007e83dfa443e984d53e0323523f742","0x0000000000000000000000000000001cfe095fa8e5d91145e1222746c27083ad","0x000000000000000000000000000000000006a7d963d96d78bc035faf0a744f8e","0x0000000000000000000000000000000000000000000000000000000000000001","0x0000000000000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000000000000000000000000000000000000002","0x0000000000000000000000000000000000000000000000000000000000000000","0x000000000000000000000000000000de9e600862f91873e12f0d906fe6fb4b35","0x0000000000000000000000000000000000268e1172734cd620010a0e7bd33318","0x0000000000000000000000000000006222cac69b3a4024afa430991710a6fe53","0x000000000000000000000000000000000018c0b8f57b190c18dcd096aa188b5d"] \ No newline at end of file +["0x0000000000000000000000000000000000000000000000000000000000008000","0x000000000000000000000000000000000000000000000000000000000000001f","0x0000000000000000000000000000000000000000000000000000000000000001","0x000000000000000000000000000000000000000000000000000000000000000f","0x000000000000000000000000000000eeba595d642c330ff0052a30356aef944d","0x000000000000000000000000000000000010e3400ec4848ce654b0033bd1909c","0x0000000000000000000000000000002b7a9a001dcd869f8a0f57a9463fbda32e","0x00000000000000000000000000000000000a1c18400fcebbffedebc9f1d7957f","0x000000000000000000000000000000037dca156d5c0e70124cea51e227a0146b","0x00000000000000000000000000000000002a4b62dbc27de48f7031e5aab1583e","0x000000000000000000000000000000136c427b7f48da876c44c2adc3a2369e5d","0x000000000000000000000000000000000022e1a1a0c6e0adb02dbed58ed9333b","0x00000000000000000000000000000086be6e47fb05c8a34ee650da276874a16e","0x00000000000000000000000000000000002ffcdf47719489739c0ccd79a094a4","0x0000000000000000000000000000001d83a98627294b99ea6b8b987226bca454","0x0000000000000000000000000000000000112246923b49d0709e410042c4860b","0x00000000000000000000000000000038f6ec2a41271e1da7b722ee6abc79ff09","0x0000000000000000000000000000000000083462567704201deb6dcc33c9cfe4","0x000000000000000000000000000000401aaa7d6ab87aa284f84f8ad30ac71c49","0x00000000000000000000000000000000002249363ebb246b83846ad9ac5b3ed6","0x000000000000000000000000000000bae8ada0ee2188885f5b8effd9e61b02a1","0x0000000000000000000000000000000000286bf8187ddec53c2ec9ac55e9cdd4","0x000000000000000000000000000000c101fd8b4d894b84208bc3720568bdca84","0x00000000000000000000000000000000002b0edb22bc1d39cba3be724c04018d","0x000000000000000000000000000000b06aacb1989ba061c490d4f2ec0857d429","0x00000000000000000000000000000000001df228e6da104ccd52eb0f004112bd","0x000000000000000000000000000000f803b078140485be6e5e6fe54b54758dd6","0x00000000000000000000000000000000002bdae37d3fae349df4b291c8b28987","0x000000000000000000000000000000176df2b9f9816b99b87dcd27314f248492","0x00000000000000000000000000000000000396fae9cfc50f85ebfe6bf44327d7","0x0000000000000000000000000000000107a790c778e6a2ffbae75e5764ee8cc2","0x00000000000000000000000000000000000e0f2756936ef0c0b2175663320713","0x000000000000000000000000000000df66bc3e9f02258d5ab62bdc7217cf2d8d","0x0000000000000000000000000000000000071538f67f71e31ecf217d87ac3038","0x000000000000000000000000000000e36eddb6fd7bbbbe99f71ff10df7ef86fa","0x00000000000000000000000000000000000a42c0ff54a34a80cb110dcb90f724","0x00000000000000000000000000000068bdc47e00f54f7343c34b2c48368ebde6","0x00000000000000000000000000000000001d418813d959151e7f2eba7d7f5c51","0x000000000000000000000000000000cb095b6f7caee08e4e5e30eea991d86920","0x0000000000000000000000000000000000106af79cd3966436c97d57b5b3fc2a","0x000000000000000000000000000000bd02c58e4ee0c2bb494de03af6bc7526ab","0x00000000000000000000000000000000000d6405504013ca3e28a415a7f71669","0x000000000000000000000000000000685cb2ece723d459108a821240d3fc2262","0x000000000000000000000000000000000029244ec479537ad66815d0389fc4d2","0x0000000000000000000000000000004415003ff210bb2ce2f3bef905b903520b","0x00000000000000000000000000000000001542f730190d952f86315d3d29457a","0x00000000000000000000000000000054d142734abe1f06533e7316a0ddd16496","0x00000000000000000000000000000000001873d1a3f4ddbf6e0958e5bb1106af","0x000000000000000000000000000000a98543fb0eb78f681eb4cb0d3e14d2a9dd","0x000000000000000000000000000000000014fcb07f82d6efdd47ca3f7248c61e","0x00000000000000000000000000000005ba5d932d7b77bef91294a42ef37aa1b1","0x00000000000000000000000000000000000b627e9389774df7f4bd5563897614","0x000000000000000000000000000000f01a7d58ea57541ff46b6bfe1193f521c7","0x00000000000000000000000000000000002b084ad4761ae968a05151fed32ac2","0x0000000000000000000000000000009df8752bd4bc40071d8a3821dea722997d","0x000000000000000000000000000000000003cc62c1a745e20ef2af508d94d96a","0x000000000000000000000000000000e87bcab37cd4e308aa243f2f00bbcdb146","0x00000000000000000000000000000000000f5f0933ac6e90a320c03886ce5ef5","0x00000000000000000000000000000010c95aed0ecbe9e328bc0022428fe2b9d1","0x0000000000000000000000000000000000261e1356aa63e9f3b929351ef34520","0x000000000000000000000000000000d0316171b1fcbc63d492e5c921e1d85534","0x0000000000000000000000000000000000135bf1d5004195d5aee992f4e8d2ef","0x000000000000000000000000000000e2f72da69c7cbf9bd22617ef95bc07db46","0x00000000000000000000000000000000000f4c6d60625829f72ad52827157819","0x000000000000000000000000000000aed2ec42f4730788107991640341ecec01","0x00000000000000000000000000000000002ccb40475a3d4394cf306d8e187b46","0x0000000000000000000000000000004fbe69b6da34d7526ca644393f340acb1f","0x00000000000000000000000000000000002055a571e8058e11fe5bd7a70df194","0x0000000000000000000000000000002a827b802fdc3fa2bb96e45de06250fec6","0x0000000000000000000000000000000000283d3c31b996ab705d67d593823e23","0x000000000000000000000000000000bc1001981431c53496a849a2259d03b457","0x000000000000000000000000000000000016245614bd6436e48273825ec30279","0x000000000000000000000000000000d0b237370730e5c12b49202f8eed05caa0","0x000000000000000000000000000000000011834ef1f9a10486f2ca206bc00455","0x000000000000000000000000000000cda55dfbdd2802299831bca66dc058e055","0x0000000000000000000000000000000000207f3ebb920fc90d406d3a612a2daf","0x000000000000000000000000000000e13bb20551a2af5f8ac36ebaac7876a1c4","0x000000000000000000000000000000000028017bb468f2ed75a6887614e5a9b1","0x000000000000000000000000000000b5a4ceab339d24da55f579aa04f192eab2","0x0000000000000000000000000000000000040edbf67c3d6d99fa176b85dae651","0x000000000000000000000000000000cfb60fe92fb07d24b9de89186c75086f1e","0x00000000000000000000000000000000000a4cc323ddffb2bde796024f39bc52","0x0000000000000000000000000000006fb079278fc9ad6c879493fc3bc60da717","0x00000000000000000000000000000000001588a466568529e8a6c5b33fcc26e3","0x000000000000000000000000000000bf3de286969e366510b04f43ee02b06117","0x000000000000000000000000000000000008175e28145dca69106c8791d8a2e5","0x0000000000000000000000000000002c116fa0beca775474a5fe7b78fc2f2bba","0x00000000000000000000000000000000000c999129c3c06772c6fbbc7355c09c","0x000000000000000000000000000000984d80910016772cb83ed3881f90ebcc8c","0x00000000000000000000000000000000001b6540dd28b967d06897d2fe4f4732","0x000000000000000000000000000000b7445646b5160c380130b66e34450aa736","0x00000000000000000000000000000000000ef8897e2c65d0ec75dc09f85edeac","0x00000000000000000000000000000045688ef774d85c8bd41f9665bad2d794c8","0x00000000000000000000000000000000002ac07a74c19ad88489ed6f1e93dc9e","0x000000000000000000000000000000a5a32649850aae3c839626ec285717f399","0x000000000000000000000000000000000010cdf0710877e231d375a2ef4dc3d3","0x000000000000000000000000000000160d4e95b954b96077723f1800f420dc77","0x0000000000000000000000000000000000280ae74a2dae2f5e68706031174b76","0x000000000000000000000000000000440bb5fc5471f6a37dbe12fbef6a37c009","0x0000000000000000000000000000000000106864b3460bfded9d5dc448fe29e8","0x000000000000000000000000000000d29de5d4acc99c6c72fa17e0a80bf88401","0x000000000000000000000000000000000007e83dfa443e984d53e0323523f742","0x0000000000000000000000000000001cfe095fa8e5d91145e1222746c27083ad","0x000000000000000000000000000000000006a7d963d96d78bc035faf0a744f8e","0x0000000000000000000000000000000000000000000000000000000000000001","0x0000000000000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000000000000000000000000000000000000002","0x0000000000000000000000000000000000000000000000000000000000000000","0x000000000000000000000000000000f0632125fa5d1bd312c714447d2ee3f921","0x00000000000000000000000000000000000e3504c34901d8cf39ca6008e672b7","0x00000000000000000000000000000015d3e4e604b8962bdca2dff461051dca20","0x00000000000000000000000000000000002d2b8c20d3ca4c73e2f8b6854b8c67"] \ No newline at end of file diff --git a/packages/tokens/src/confidential/circuits/withdraw/src/main.nr b/packages/tokens/src/confidential/circuits/withdraw/src/main.nr index f005c0623..2e5057e09 100644 --- a/packages/tokens/src/confidential/circuits/withdraw/src/main.nr +++ b/packages/tokens/src/confidential/circuits/withdraw/src/main.nr @@ -1,6 +1,6 @@ use stellar_confidential_lib::{ - H, assert_on_curve_non_identity, assert_range_128, commit, derive_spend_r, ecdh, - encrypt_auditor_sender_balance, encrypt_balance, scalar_mul, vk_from_sk, + H, assert_on_curve_non_identity, commit, derive_spend_r, ecdh, encrypt_auditor_sender_balance, + encrypt_balance, scalar_mul, vk_from_sk, }; use std::embedded_curve_ops::EmbeddedCurvePoint; @@ -16,8 +16,12 @@ mod tests; // key // W3 C_spend = v * G + r * H opening of current // spendable balance -// W4 v, a, v - a in [0, 2^128) range validity -// (Section 2.6) +// W4 v, a, v - a in [0, 2^127) range validity +// (Section 2.6); the +// [0, 2^127) bound is +// the SEP-41 i128 +// non-negative range +// (Section 3.4) // W5 r' = Poseidon2(delta_spend_r, vk, sigma) deterministic // randomness for the // new balance @@ -67,8 +71,9 @@ mod tests; // 5 k_aud_s_x K_aud_s.x Fetched from the auditor contract by // 6 k_aud_s_y K_aud_s.y `from.auditor_id`. // 7 a a Public withdrawal amount; the wrapper -// pre-checks a >= 0 at the entrypoint, -// W4 then closes a < 2^128 in-circuit. +// pre-checks a >= 0 at the entrypoint +// (Section 3.4), W4 then closes +// a < 2^127 in-circuit. // 8 c_spend_new_x C_spend'.x Prover-supplied; written to // 9 c_spend_new_y C_spend'.y `from.spendable_balance` on success. // 10 sigma sigma Prover-supplied random salt; emitted. @@ -125,11 +130,13 @@ fn main( assert(c_spend_derived.x == c_spend_x); assert(c_spend_derived.y == c_spend_y); - // W4 - assert_range_128(v); - assert_range_128(a); + // W4 -- Section 2.6 spells out the 127-bit decomposition / recomposition + // pattern; `assert_max_bit_size` is the Noir stdlib primitive that + // implements it directly. + v.assert_max_bit_size::<127>(); + a.assert_max_bit_size::<127>(); let v_new = v - a; - assert_range_128(v_new); + v_new.assert_max_bit_size::<127>(); // W5 let r_new = derive_spend_r(vk, sigma); diff --git a/packages/tokens/src/confidential/circuits/withdraw/src/tests.nr b/packages/tokens/src/confidential/circuits/withdraw/src/tests.nr index ccdacffd9..e994848ab 100644 --- a/packages/tokens/src/confidential/circuits/withdraw/src/tests.nr +++ b/packages/tokens/src/confidential/circuits/withdraw/src/tests.nr @@ -217,7 +217,10 @@ fn rejects_under_funded_withdrawal() { #[test(should_fail)] fn rejects_v_out_of_range() { - let v_huge: Field = 0x100000000000000000000000000000000; // 2^128 + // 2^127 is exactly the boundary W4 rules out (Section 2.6 / 3.4 -- SEP-41 + // i128 non-negative range is [0, 2^127), so 2^127 is the smallest value + // that must fail). + let v_huge: Field = 0x80000000000000000000000000000000; main( SK, v_huge, @@ -243,7 +246,8 @@ fn rejects_v_out_of_range() { #[test(should_fail)] fn rejects_a_out_of_range() { - let a_huge: Field = 0x100000000000000000000000000000000; + // a = 2^127: W4's range check on a fires at the exact SEP-41 boundary. + let a_huge: Field = 0x80000000000000000000000000000000; main( SK, V, From 6ece1f12e0f206081f6c0187bca8750674c63fed Mon Sep 17 00:00:00 2001 From: brozorec <9572072+brozorec@users.noreply.github.com> Date: Fri, 22 May 2026 16:07:45 +0200 Subject: [PATCH 06/16] docs: fix inline --- .../circuits/withdraw/src/main.nr | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/packages/tokens/src/confidential/circuits/withdraw/src/main.nr b/packages/tokens/src/confidential/circuits/withdraw/src/main.nr index 2e5057e09..4e041b3e0 100644 --- a/packages/tokens/src/confidential/circuits/withdraw/src/main.nr +++ b/packages/tokens/src/confidential/circuits/withdraw/src/main.nr @@ -11,42 +11,42 @@ mod tests; // Constraints // ----------- // Main block (balance conservation + ownership) -// W1 Y = sk * H owner key ownership -// W2 vk = Poseidon2(delta_vk, sk, wrap) wrapper-bound viewing -// key -// W3 C_spend = v * G + r * H opening of current -// spendable balance -// W4 v, a, v - a in [0, 2^127) range validity +// W1 Y = sk * H Owner key ownership. +// W2 vk = Poseidon2(delta_vk, sk, wrap) Wrapper-bound viewing +// key. +// W3 C_spend = v * G + r * H Opening of current +// spendable balance. +// W4 v, a, v - a in [0, 2^127) Range validity // (Section 2.6); the // [0, 2^127) bound is // the SEP-41 i128 // non-negative range // (Section 3.4) -// W5 r' = Poseidon2(delta_spend_r, vk, sigma) deterministic +// W5 r' = Poseidon2(delta_spend_r, vk, sigma) Deterministic // randomness for the -// new balance -// W6 C_spend' = (v - a) * G + r' * H new spendable -// commitment +// new balance. +// W6 C_spend' = (v - a) * G + r' * H New spendable +// commitment. // W7 b_tilde = (v - a) // + Poseidon2(delta_enc_bal, vk, sigma) -// encrypted balance -// scalar (emitted) -// Auditor block (sender-auditor visibility, Section 8.1) -// W_a1 R_e = r_e * H ephemeral key for -// auditor ECDH -// W_a2 S_{a,s} = r_e * K_{aud,s} sender-auditor ECDH -// shared secret -// W_a3 m_b = SpongeSqueeze_1(delta_aud_s, -// S_{a,s}.x, sigma) sender-channel -// sponge, single squeeze -// W_a4 b_tilde_aud_s = (v - a) + m_b sender-auditor -// encrypted balance -// checkpoint (emitted) -// W_a5 r_e != 0 rules out R_e = O and +// Encrypted balance +// scalar (emitted). +// W8 r_e != 0 Rules out R_e = O and // S_{a,s} = O, which // would collapse m_b to // a constant function -// of sigma +// of sigma. +// Auditor block (sender-auditor visibility, Section 8.1) +// W_a1 R_e = r_e * H Ephemeral key for +// auditor ECDH. +// W_a2 S_{a,s} = r_e * K_{aud,s} Sender-auditor ECDH +// shared secret. +// W_a3 m_b = SpongeSqueeze_1(delta_aud_s, +// S_{a,s}.x, sigma) Sender-channel +// sponge, single squeeze. +// W_a4 b_tilde_aud_s = (v - a) + m_b Sender-auditor +// encrypted balance +// checkpoint (emitted). // // W_a3 + W_a4 are encapsulated by `encrypt_auditor_sender_balance` in the lib. // @@ -90,7 +90,7 @@ mod tests; // v Plaintext spendable-balance value. // r Plaintext blinding factor for C_spend. // r_e Ephemeral scalar for the auditor ECDH; must satisfy r_e != 0 -// (W_a5). +// (W8). fn main( sk: Field, @@ -113,7 +113,7 @@ fn main( r_e_y: pub Field, b_tilde_aud_s: pub Field, ) { - // W_a5 -- runs first so the r_e = 0 attack is rejected before any + // W8 -- runs first so the r_e = 0 attack is rejected before any // scalar mul against it could quietly produce the identity. assert(r_e != 0); From 89e054ab61fc6bb72efc7c459336870144d54749 Mon Sep 17 00:00:00 2001 From: brozorec <9572072+brozorec@users.noreply.github.com> Date: Fri, 22 May 2026 16:20:13 +0200 Subject: [PATCH 07/16] test(confidential): isolate W4 in rejects_v_out_of_range 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. --- .../src/confidential/circuits/withdraw/src/tests.nr | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/tokens/src/confidential/circuits/withdraw/src/tests.nr b/packages/tokens/src/confidential/circuits/withdraw/src/tests.nr index e994848ab..32cb0874e 100644 --- a/packages/tokens/src/confidential/circuits/withdraw/src/tests.nr +++ b/packages/tokens/src/confidential/circuits/withdraw/src/tests.nr @@ -219,15 +219,18 @@ fn rejects_under_funded_withdrawal() { fn rejects_v_out_of_range() { // 2^127 is exactly the boundary W4 rules out (Section 2.6 / 3.4 -- SEP-41 // i128 non-negative range is [0, 2^127), so 2^127 is the smallest value - // that must fail). + // that must fail). C_spend must be recomputed against v_huge so W3 + // passes and W4 is the load-bearing assertion; downstream constraints + // (W6/W7/W_a4) are never reached. let v_huge: Field = 0x80000000000000000000000000000000; + let c_spend = commit(v_huge, R); main( SK, v_huge, R, R_E, - C_SPEND_X, - C_SPEND_Y, + c_spend.x, + c_spend.y, Y_X, Y_Y, WRAP, From d719111560dcda29d16b8fb065378833eb169d63 Mon Sep 17 00:00:00 2001 From: brozorec <9572072+brozorec@users.noreply.github.com> Date: Tue, 26 May 2026 10:20:45 +0200 Subject: [PATCH 08/16] =?UTF-8?q?chore(confidential):=20align=20Poseidon?= =?UTF-8?q?=20domain=20tags=20with=20DESIGN.md=20=C2=A713?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- .../src/confidential/circuits/lib/src/lib.nr | 50 +++++++++++-------- .../confidential/circuits/lib/src/tests.nr | 34 +++++++------ .../circuits/lib/testdata/derive_allow_r.json | 4 +- .../circuits/lib/testdata/derive_spend_r.json | 4 +- .../lib/testdata/derive_tx_blind.json | 2 +- .../circuits/lib/testdata/dvk_from_vk_op.json | 4 +- .../lib/testdata/encrypt_allowance.json | 4 +- .../circuits/lib/testdata/encrypt_amount.json | 2 +- .../encrypt_auditor_sender_balance.json | 2 +- .../lib/testdata/encrypt_balance.json | 4 +- .../lib/testdata/encrypt_esc_dvk.json | 4 +- .../lib/testdata/poseidon_with_domain.json | 4 +- .../circuits/lib/testdata/pvk_from_vk.json | 6 +-- .../lib/testdata/sponge_squeeze_1.json | 4 +- .../lib/testdata/sponge_squeeze_2.json | 14 +++--- .../circuits/lib/testdata/vk_from_sk.json | 2 +- .../circuits/register/src/tests.nr | 4 +- .../circuits/vks/register.vk.json | 2 +- .../circuits/vks/withdraw.vk.json | 2 +- .../circuits/withdraw/src/tests.nr | 10 ++-- 20 files changed, 85 insertions(+), 77 deletions(-) diff --git a/packages/tokens/src/confidential/circuits/lib/src/lib.nr b/packages/tokens/src/confidential/circuits/lib/src/lib.nr index 0317a4d7f..cbafc8c26 100644 --- a/packages/tokens/src/confidential/circuits/lib/src/lib.nr +++ b/packages/tokens/src/confidential/circuits/lib/src/lib.nr @@ -77,59 +77,65 @@ global POSEIDON2_IV_BASE: Field = 18446744073709551616; // 2^64 /// /// | Design doc | Value | Constant | /// |:------------------|:------|:----------------------------------| -/// | `delta_vk` | 1 | `VIEWING_KEY` | -/// | `delta_dvk` | 2 | `DELEGATION_VIEWING_KEY` | -/// | `delta_spend_r` | 3 | `SPEND_RANDOMNESS` | -/// | `delta_tx_blind` | 4 | `TX_BLINDING` | -/// | `delta_tx_amount` | 5 | `TX_AMOUNT` | -/// | `delta_enc_bal` | 6 | `ENCRYPTED_BALANCE` | -/// | `delta_enc_allow` | 7 | `ENCRYPTED_ALLOWANCE` | -/// | `delta_allow_r` | 8 | `ALLOWANCE_RANDOMNESS` | -/// | `delta_esc_dvk` | 9 | `ESCROWED_DELEGATION_VIEWING_KEY` | -/// | `delta_aud_s` | 10 | `AUDITOR_SENDER` | -/// | `delta_aud_r` | 11 | `AUDITOR_RECIPIENT` | +/// | `delta_addr` | 1 | `ADDRESS` | +/// | `delta_vk` | 2 | `VIEWING_KEY` | +/// | `delta_dvk` | 3 | `DELEGATION_VIEWING_KEY` | +/// | `delta_spend_r` | 4 | `SPEND_RANDOMNESS` | +/// | `delta_tx_blind` | 5 | `TX_BLINDING` | +/// | `delta_tx_amount` | 6 | `TX_AMOUNT` | +/// | `delta_enc_bal` | 7 | `ENCRYPTED_BALANCE` | +/// | `delta_enc_allow` | 8 | `ENCRYPTED_ALLOWANCE` | +/// | `delta_allow_r` | 9 | `ALLOWANCE_RANDOMNESS` | +/// | `delta_esc_dvk` | 10 | `ESCROWED_DELEGATION_VIEWING_KEY` | +/// | `delta_aud_s` | 11 | `AUDITOR_SENDER` | +/// | `delta_aud_r` | 12 | `AUDITOR_RECIPIENT` | mod domain { + /// Soroban Address compression into a single `F_r` Field: + /// `address_to_field(a) = Poseidon2(ADDRESS, lo(a), hi(a))`. + /// Section 2.7 (`delta_addr`). The wrapper computes `wrap` and `op_i` + /// off-circuit via this tag; circuits consume the resulting Field directly. + pub(crate) global ADDRESS: Field = 1; /// Viewing-key derivation: `vk = Poseidon2(VIEWING_KEY, sk, wrap)`. /// Section 4.2 (`delta_vk`). - pub(crate) global VIEWING_KEY: Field = 1; + pub(crate) global VIEWING_KEY: Field = 2; /// Delegation viewing-key derivation: /// `dvk = Poseidon2(DELEGATION_VIEWING_KEY, vk, op_i)`. Section 4.4 (`delta_dvk`). - pub(crate) global DELEGATION_VIEWING_KEY: Field = 2; + pub(crate) global DELEGATION_VIEWING_KEY: Field = 3; /// Deterministic randomness for the new spendable balance: /// `r' = Poseidon2(SPEND_RANDOMNESS, vk, sigma)`. Constraints W5/T10/S9/V6 (`delta_spend_r`). - pub(crate) global SPEND_RANDOMNESS: Field = 3; + pub(crate) global SPEND_RANDOMNESS: Field = 4; /// ECDH-derived blinding for the transfer commitment: /// `r_tx = Poseidon2(TX_BLINDING, s, sigma)`. Constraints T7/O7 (`delta_tx_blind`). - pub(crate) global TX_BLINDING: Field = 4; + pub(crate) global TX_BLINDING: Field = 5; /// Mask for the encrypted transfer amount: /// `v_tilde = v_tx + Poseidon2(TX_AMOUNT, s, sigma)`. Constraints T9/O9 (`delta_tx_amount`). - pub(crate) global TX_AMOUNT: Field = 5; + pub(crate) global TX_AMOUNT: Field = 6; /// Mask for the encrypted balance scalar: /// `b_tilde = v_new + Poseidon2(ENCRYPTED_BALANCE, vk, sigma)`. /// Constraints W7/T12/S11/V8 (`delta_enc_bal`). - pub(crate) global ENCRYPTED_BALANCE: Field = 6; + pub(crate) global ENCRYPTED_BALANCE: Field = 7; /// Mask for the encrypted allowance scalar: /// `a_tilde = v_a + Poseidon2(ENCRYPTED_ALLOWANCE, dvk, sigma_a)`. /// Constraints S8/O12 (`delta_enc_allow`). - pub(crate) global ENCRYPTED_ALLOWANCE: Field = 7; + pub(crate) global ENCRYPTED_ALLOWANCE: Field = 8; /// Deterministic randomness for the operator allowance: /// `r_a = Poseidon2(ALLOWANCE_RANDOMNESS, dvk, sigma_a)`. /// Constraints S6/O3/O10 (`delta_allow_r`). - pub(crate) global ALLOWANCE_RANDOMNESS: Field = 8; + pub(crate) global ALLOWANCE_RANDOMNESS: Field = 9; /// Delegation-key escrow mask (operator ECDH): /// `Poseidon2(ESCROWED_DELEGATION_VIEWING_KEY, s, op_i)`. /// Constraint S12, Section 7.11 (`delta_esc_dvk`). - pub(crate) global ESCROWED_DELEGATION_VIEWING_KEY: Field = 9; + pub(crate) global ESCROWED_DELEGATION_VIEWING_KEY: Field = 10; /// Sender or owner-auditor channel tag for Poseidon2 sponge masks /// (Section 2.5, Section 8.1). Squeeze 1 yields the amount mask (where /// applicable); squeeze 2 yields the balance/allowance checkpoint mask. /// Constraints W_a3 / T_a6 / S_a3 / V_a3 / O_a6 (`delta_aud_s`). - pub(crate) global AUDITOR_SENDER: Field = 10; + pub(crate) global AUDITOR_SENDER: Field = 11; /// Recipient-auditor channel tag for Poseidon2 sponge masks /// (Section 2.5, Section 8.1). Squeeze 1 yields the amount mask; squeeze /// 2 yields the per-transfer Pedersen randomness mask. /// Constraints T_a2 / O_a2 (`delta_aud_r`). - pub(crate) global AUDITOR_RECIPIENT: Field = 11; + pub(crate) global AUDITOR_RECIPIENT: Field = 12; } // ################## CORE PRIMITIVES ################## diff --git a/packages/tokens/src/confidential/circuits/lib/src/tests.nr b/packages/tokens/src/confidential/circuits/lib/src/tests.nr index 25bf1c701..0f1ecf6e4 100644 --- a/packages/tokens/src/confidential/circuits/lib/src/tests.nr +++ b/packages/tokens/src/confidential/circuits/lib/src/tests.nr @@ -176,6 +176,7 @@ fn domain_separation_distinct() { // Touches every domain tag at least once. let a: Field = 42; let b: Field = 99; + let h_addr = poseidon_with_domain(domain::ADDRESS, [a, b]); let h_vk = poseidon_with_domain(domain::VIEWING_KEY, [a, b]); let h_dvk = poseidon_with_domain(domain::DELEGATION_VIEWING_KEY, [a, b]); let h_sr = poseidon_with_domain(domain::SPEND_RANDOMNESS, [a, b]); @@ -189,6 +190,7 @@ fn domain_separation_distinct() { let h_ar2 = poseidon_with_domain(domain::AUDITOR_RECIPIENT, [a, b]); // Distinct from VK. + assert(h_vk != h_addr); assert(h_vk != h_dvk); assert(h_vk != h_sr); assert(h_vk != h_tb); @@ -427,43 +429,43 @@ fn fixtures_match_testdata() { ); let vk = vk_from_sk(sk, wrap); - assert(vk == 0x0ac6ade91aea36f4de35eb5a06a67e1b9c3dab9842479f1cce85caf826194ec2); + assert(vk == 0x208fbdb70d2faacf04f987b54f12aeeaeb432acc29d650c86ce0f6275b958eb8); let pvk = pvk_from_vk(vk); - assert(pvk.x == 0x01de20bfe21ad660e073b14a106feae4afce4e78a020953ca16b767e0615331b); - assert(pvk.y == 0x12cf407b75fccb5c7721c5fdadc3d1a22b5440e36b08fe52658ab885529e92fd); + assert(pvk.x == 0x2e7421c0e86a4c8eed823edf851c364ecee448bf70eae2a95befe3dd9364cc45); + assert(pvk.y == 0x0b060655740aabef84e7305bb886b75da8d258f346381f03925d69645292fe7a); let dvk = dvk_from_vk_op(vk, op_i); - assert(dvk == 0x1dec1da326d325cf918eed33f097cfb854b41aac8ece9e6e8bd54b7a7c448785); + assert(dvk == 0x1a088264ebf7269160bbf34d5a3f94d7dec37efc609ef76c5dbfa8690af3eae9); assert( derive_spend_r(vk, sigma) - == 0x1b9d813c7c14c78969edb37cce24f3bb867f6e64efe73d950cfbbe8ed4c31482, + == 0x0218f93b7ea0735d4934904f2bf26fc20c33dc01fb032a8f00aa67147132ec91, ); assert( derive_allow_r(dvk, sigma_a) - == 0x199b47ff8b24b165411fa44b06d541b9d1b74d0d202fd651712e24c11e05cb35, + == 0x0afda20e062a277afc0c86e31ea22597ee2c094b1b9cd08b8d8a2a212a8af1ee, ); assert( derive_tx_blind(s_ecdh, sigma) - == 0x15388ad70c7a48cfed4fb9cfbd633ff18617c9e52829b5d51eaab938baae33a7, + == 0x274c4f45d57544efca9f6f505e6b29ae098f1d30243de28f7cef62d05484c83d, ); assert( encrypt_amount(v_tx, s_ecdh, sigma) - == 0x274c4f45d57544efca9f6f505e6b29ae098f1d30243de28f7cef62d05484c8a1, + == 0x2a07070eb651b03ecc22453ad1cc69a39567ca27fb4224e8727a5fbc83091526, ); assert( encrypt_balance(v, vk, sigma) - == 0x2b7f92c72c05ae8ecf7207fdaecea32ef748cd4dd43ca9ed6dc6d226865b43af, + == 0x04d1659db899a50a94dcfc54a18b8adaf6e9e8e3046bd11893fbd4a86a7c579c, ); assert( encrypt_allowance(v_a, dvk, sigma_a) - == 0x235db43a283e4e000337cdb80a99168ef4b718622cc06de4ab98b3d2bd5736d5, + == 0x2440e4fcb575aa4433cd53a37a7396d5022f8c287298cacc0cfc0857250ef02a, ); assert( encrypt_esc_dvk(dvk, s_ecdh, op_i) - == 0x2468d02775ad21aa5928d049770287249af2a03dbaf4d18e22e3fe71fd292003, + == 0x15f3b867cbe94f0b6371e46752431be550f907ba8448c6191b7ba1f47c2cb83e, ); // Per-channel auditor sponge (v0.6, Section 2.5). Values pinned below are @@ -498,15 +500,15 @@ fn fixtures_match_testdata() { // `testdata/sponge_squeeze_*.json`. Update both sides together if the lib's // sponge construction or domain constants change. global SPONGE_SQUEEZE_1_AUDITOR_SENDER: Field = - 0x266e76dccd3d7c654ab7a48045cca920c497b1a6758981acfa6f217427ac3074; + 0x2787d1e01bbca7828e13e9b2b3fa11cfcfe9c3d2b121b9e17543146822fe1feb; global SPONGE_SQUEEZE_2_AUDITOR_SENDER_0: Field = - 0x266e76dccd3d7c654ab7a48045cca920c497b1a6758981acfa6f217427ac3074; + 0x2787d1e01bbca7828e13e9b2b3fa11cfcfe9c3d2b121b9e17543146822fe1feb; global SPONGE_SQUEEZE_2_AUDITOR_SENDER_1: Field = - 0x248b757f9fec92895bd3fb91b1a5c9c17467df6f0f55ab574d85e943df8e5cf3; + 0x27f3739a132c6353cd5af3edac0ac75faf7fc606acb61367774e4f764ec17b77; global SPONGE_SQUEEZE_2_AUDITOR_RECIPIENT_0: Field = - 0x2787d1e01bbca7828e13e9b2b3fa11cfcfe9c3d2b121b9e17543146822fe1feb; + 0x0801e6d7184cea11e6225fcfb4e88a90674cb085da8ebc04918c57cc44b9c25d; global SPONGE_SQUEEZE_2_AUDITOR_RECIPIENT_1: Field = - 0x27f3739a132c6353cd5af3edac0ac75faf7fc606acb61367774e4f764ec17b77; + 0x131187e2ac296c1c54c2be2a5cbe614644f01c754108ce25359212d67b36ecc3; #[test] fn derive_helpers_deterministic_and_distinct() { diff --git a/packages/tokens/src/confidential/circuits/lib/testdata/derive_allow_r.json b/packages/tokens/src/confidential/circuits/lib/testdata/derive_allow_r.json index 6c8f0d8fb..ba6dfdbdc 100644 --- a/packages/tokens/src/confidential/circuits/lib/testdata/derive_allow_r.json +++ b/packages/tokens/src/confidential/circuits/lib/testdata/derive_allow_r.json @@ -5,10 +5,10 @@ "vectors": [ { "inputs": { - "dvk": "0x1dec1da326d325cf918eed33f097cfb854b41aac8ece9e6e8bd54b7a7c448785", + "dvk": "0x1a088264ebf7269160bbf34d5a3f94d7dec37efc609ef76c5dbfa8690af3eae9", "sigma_a": "0x02" }, - "output": "0x199b47ff8b24b165411fa44b06d541b9d1b74d0d202fd651712e24c11e05cb35" + "output": "0x0afda20e062a277afc0c86e31ea22597ee2c094b1b9cd08b8d8a2a212a8af1ee" } ] } diff --git a/packages/tokens/src/confidential/circuits/lib/testdata/derive_spend_r.json b/packages/tokens/src/confidential/circuits/lib/testdata/derive_spend_r.json index a47bc553e..64b3f5809 100644 --- a/packages/tokens/src/confidential/circuits/lib/testdata/derive_spend_r.json +++ b/packages/tokens/src/confidential/circuits/lib/testdata/derive_spend_r.json @@ -5,10 +5,10 @@ "vectors": [ { "inputs": { - "vk": "0x0ac6ade91aea36f4de35eb5a06a67e1b9c3dab9842479f1cce85caf826194ec2", + "vk": "0x208fbdb70d2faacf04f987b54f12aeeaeb432acc29d650c86ce0f6275b958eb8", "sigma": "0x01" }, - "output": "0x1b9d813c7c14c78969edb37cce24f3bb867f6e64efe73d950cfbbe8ed4c31482" + "output": "0x0218f93b7ea0735d4934904f2bf26fc20c33dc01fb032a8f00aa67147132ec91" } ] } diff --git a/packages/tokens/src/confidential/circuits/lib/testdata/derive_tx_blind.json b/packages/tokens/src/confidential/circuits/lib/testdata/derive_tx_blind.json index 2f7fa3d57..3ae424005 100644 --- a/packages/tokens/src/confidential/circuits/lib/testdata/derive_tx_blind.json +++ b/packages/tokens/src/confidential/circuits/lib/testdata/derive_tx_blind.json @@ -5,7 +5,7 @@ "vectors": [ { "inputs": { "s": "0x12345", "sigma": "0x01" }, - "output": "0x15388ad70c7a48cfed4fb9cfbd633ff18617c9e52829b5d51eaab938baae33a7" + "output": "0x274c4f45d57544efca9f6f505e6b29ae098f1d30243de28f7cef62d05484c83d" } ] } diff --git a/packages/tokens/src/confidential/circuits/lib/testdata/dvk_from_vk_op.json b/packages/tokens/src/confidential/circuits/lib/testdata/dvk_from_vk_op.json index 43fbdc89c..3c43f6a23 100644 --- a/packages/tokens/src/confidential/circuits/lib/testdata/dvk_from_vk_op.json +++ b/packages/tokens/src/confidential/circuits/lib/testdata/dvk_from_vk_op.json @@ -5,10 +5,10 @@ "vectors": [ { "inputs": { - "vk": "0x0ac6ade91aea36f4de35eb5a06a67e1b9c3dab9842479f1cce85caf826194ec2", + "vk": "0x208fbdb70d2faacf04f987b54f12aeeaeb432acc29d650c86ce0f6275b958eb8", "op_i": "0xabcd" }, - "output": "0x1dec1da326d325cf918eed33f097cfb854b41aac8ece9e6e8bd54b7a7c448785" + "output": "0x1a088264ebf7269160bbf34d5a3f94d7dec37efc609ef76c5dbfa8690af3eae9" } ] } diff --git a/packages/tokens/src/confidential/circuits/lib/testdata/encrypt_allowance.json b/packages/tokens/src/confidential/circuits/lib/testdata/encrypt_allowance.json index 3fe63afa1..5e7d30c3c 100644 --- a/packages/tokens/src/confidential/circuits/lib/testdata/encrypt_allowance.json +++ b/packages/tokens/src/confidential/circuits/lib/testdata/encrypt_allowance.json @@ -6,10 +6,10 @@ { "inputs": { "v_a": "0x1f4", - "dvk": "0x1dec1da326d325cf918eed33f097cfb854b41aac8ece9e6e8bd54b7a7c448785", + "dvk": "0x1a088264ebf7269160bbf34d5a3f94d7dec37efc609ef76c5dbfa8690af3eae9", "sigma_a": "0x02" }, - "output": "0x235db43a283e4e000337cdb80a99168ef4b718622cc06de4ab98b3d2bd5736d5" + "output": "0x2440e4fcb575aa4433cd53a37a7396d5022f8c287298cacc0cfc0857250ef02a" } ] } diff --git a/packages/tokens/src/confidential/circuits/lib/testdata/encrypt_amount.json b/packages/tokens/src/confidential/circuits/lib/testdata/encrypt_amount.json index 788b164b7..3465d75ea 100644 --- a/packages/tokens/src/confidential/circuits/lib/testdata/encrypt_amount.json +++ b/packages/tokens/src/confidential/circuits/lib/testdata/encrypt_amount.json @@ -5,7 +5,7 @@ "vectors": [ { "inputs": { "v_tx": "0x64", "s": "0x12345", "sigma": "0x01" }, - "output": "0x274c4f45d57544efca9f6f505e6b29ae098f1d30243de28f7cef62d05484c8a1" + "output": "0x2a07070eb651b03ecc22453ad1cc69a39567ca27fb4224e8727a5fbc83091526" } ] } diff --git a/packages/tokens/src/confidential/circuits/lib/testdata/encrypt_auditor_sender_balance.json b/packages/tokens/src/confidential/circuits/lib/testdata/encrypt_auditor_sender_balance.json index 6aecdd011..ff24253ab 100644 --- a/packages/tokens/src/confidential/circuits/lib/testdata/encrypt_auditor_sender_balance.json +++ b/packages/tokens/src/confidential/circuits/lib/testdata/encrypt_auditor_sender_balance.json @@ -9,7 +9,7 @@ "s_a_s_x": "0x12345", "sigma": "0x01" }, - "output": "0x266e76dccd3d7c654ab7a48045cca920c497b1a6758981acfa6f217427ac345c" + "output": "0x2787d1e01bbca7828e13e9b2b3fa11cfcfe9c3d2b121b9e17543146822fe23d3" } ] } diff --git a/packages/tokens/src/confidential/circuits/lib/testdata/encrypt_balance.json b/packages/tokens/src/confidential/circuits/lib/testdata/encrypt_balance.json index 78bf2922d..ec4cf51f4 100644 --- a/packages/tokens/src/confidential/circuits/lib/testdata/encrypt_balance.json +++ b/packages/tokens/src/confidential/circuits/lib/testdata/encrypt_balance.json @@ -6,10 +6,10 @@ { "inputs": { "v_new": "0x3e8", - "vk": "0x0ac6ade91aea36f4de35eb5a06a67e1b9c3dab9842479f1cce85caf826194ec2", + "vk": "0x208fbdb70d2faacf04f987b54f12aeeaeb432acc29d650c86ce0f6275b958eb8", "sigma": "0x01" }, - "output": "0x2b7f92c72c05ae8ecf7207fdaecea32ef748cd4dd43ca9ed6dc6d226865b43af" + "output": "0x04d1659db899a50a94dcfc54a18b8adaf6e9e8e3046bd11893fbd4a86a7c579c" } ] } diff --git a/packages/tokens/src/confidential/circuits/lib/testdata/encrypt_esc_dvk.json b/packages/tokens/src/confidential/circuits/lib/testdata/encrypt_esc_dvk.json index ed85fb202..edd5124ac 100644 --- a/packages/tokens/src/confidential/circuits/lib/testdata/encrypt_esc_dvk.json +++ b/packages/tokens/src/confidential/circuits/lib/testdata/encrypt_esc_dvk.json @@ -5,11 +5,11 @@ "vectors": [ { "inputs": { - "dvk": "0x1dec1da326d325cf918eed33f097cfb854b41aac8ece9e6e8bd54b7a7c448785", + "dvk": "0x1a088264ebf7269160bbf34d5a3f94d7dec37efc609ef76c5dbfa8690af3eae9", "s": "0x12345", "op_i": "0xabcd" }, - "output": "0x2468d02775ad21aa5928d049770287249af2a03dbaf4d18e22e3fe71fd292003" + "output": "0x15f3b867cbe94f0b6371e46752431be550f907ba8448c6191b7ba1f47c2cb83e" } ] } diff --git a/packages/tokens/src/confidential/circuits/lib/testdata/poseidon_with_domain.json b/packages/tokens/src/confidential/circuits/lib/testdata/poseidon_with_domain.json index 9139c2ee6..156b45991 100644 --- a/packages/tokens/src/confidential/circuits/lib/testdata/poseidon_with_domain.json +++ b/packages/tokens/src/confidential/circuits/lib/testdata/poseidon_with_domain.json @@ -4,8 +4,8 @@ "description": "The single Poseidon2 entry point. The domain tag is the FIRST absorbed element; the sponge uses rate=3, capacity=1, IV = (input_length+1) * 2^64. Vector below pins the VK-domain hash of [sk, wrap] -- equal to vk_from_sk by construction.", "vectors": [ { - "inputs": { "domain": "0x01", "inputs": ["0xdead", "0xbeef"] }, - "output": "0x0ac6ade91aea36f4de35eb5a06a67e1b9c3dab9842479f1cce85caf826194ec2" + "inputs": { "domain": "0x02", "inputs": ["0xdead", "0xbeef"] }, + "output": "0x208fbdb70d2faacf04f987b54f12aeeaeb432acc29d650c86ce0f6275b958eb8" } ] } diff --git a/packages/tokens/src/confidential/circuits/lib/testdata/pvk_from_vk.json b/packages/tokens/src/confidential/circuits/lib/testdata/pvk_from_vk.json index d44cd2df7..21a97c9b2 100644 --- a/packages/tokens/src/confidential/circuits/lib/testdata/pvk_from_vk.json +++ b/packages/tokens/src/confidential/circuits/lib/testdata/pvk_from_vk.json @@ -4,10 +4,10 @@ "description": "Public viewing key PVK = vk*H. Inputs use the vk produced by vk_from_sk(0xdead, 0xbeef).", "vectors": [ { - "inputs": { "vk": "0x0ac6ade91aea36f4de35eb5a06a67e1b9c3dab9842479f1cce85caf826194ec2" }, + "inputs": { "vk": "0x208fbdb70d2faacf04f987b54f12aeeaeb432acc29d650c86ce0f6275b958eb8" }, "output": { - "x": "0x01de20bfe21ad660e073b14a106feae4afce4e78a020953ca16b767e0615331b", - "y": "0x12cf407b75fccb5c7721c5fdadc3d1a22b5440e36b08fe52658ab885529e92fd" + "x": "0x2e7421c0e86a4c8eed823edf851c364ecee448bf70eae2a95befe3dd9364cc45", + "y": "0x0b060655740aabef84e7305bb886b75da8d258f346381f03925d69645292fe7a" } } ] diff --git a/packages/tokens/src/confidential/circuits/lib/testdata/sponge_squeeze_1.json b/packages/tokens/src/confidential/circuits/lib/testdata/sponge_squeeze_1.json index 741608e1d..ef920defb 100644 --- a/packages/tokens/src/confidential/circuits/lib/testdata/sponge_squeeze_1.json +++ b/packages/tokens/src/confidential/circuits/lib/testdata/sponge_squeeze_1.json @@ -4,8 +4,8 @@ "description": "Poseidon2 sponge with a single squeeze (one absorb of (d, s_x, sigma), permute, output state[0]). Used by sender-auditor channels that emit only the balance/allowance mask. Equivalent to poseidon_with_domain(d, [s_x, sigma]).", "vectors": [ { - "inputs": { "d": "0x0a", "s_x": "0x12345", "sigma": "0x01" }, - "output": "0x266e76dccd3d7c654ab7a48045cca920c497b1a6758981acfa6f217427ac3074" + "inputs": { "d": "0x0b", "s_x": "0x12345", "sigma": "0x01" }, + "output": "0x2787d1e01bbca7828e13e9b2b3fa11cfcfe9c3d2b121b9e17543146822fe1feb" } ] } diff --git a/packages/tokens/src/confidential/circuits/lib/testdata/sponge_squeeze_2.json b/packages/tokens/src/confidential/circuits/lib/testdata/sponge_squeeze_2.json index d95baa829..10774d8cf 100644 --- a/packages/tokens/src/confidential/circuits/lib/testdata/sponge_squeeze_2.json +++ b/packages/tokens/src/confidential/circuits/lib/testdata/sponge_squeeze_2.json @@ -1,20 +1,20 @@ { "primitive": "sponge_squeeze_2", "design_doc_refs": ["Section 2.5", "Constraint T_a2", "Constraint T_a6", "Constraint O_a2", "Constraint O_a6"], - "description": "Poseidon2 sponge with two squeezes (one absorb of (d, s_x, sigma), permute, output (state[0], state[1])). Canonical squeeze order: index 0 = amount mask, index 1 = balance / per-transfer Pedersen randomness mask, fixed per design doc Sections 7 and 8. The sender-auditor channel uses d = AUDITOR_SENDER (10); the recipient-auditor channel uses d = AUDITOR_RECIPIENT (11). The first squeeze must match sponge_squeeze_1 on the same inputs.", + "description": "Poseidon2 sponge with two squeezes (one absorb of (d, s_x, sigma), permute, output (state[0], state[1])). Canonical squeeze order: index 0 = amount mask, index 1 = balance / per-transfer Pedersen randomness mask, fixed per design doc Sections 7 and 8. The sender-auditor channel uses d = AUDITOR_SENDER (11); the recipient-auditor channel uses d = AUDITOR_RECIPIENT (12). The first squeeze must match sponge_squeeze_1 on the same inputs.", "vectors": [ { - "inputs": { "d": "0x0a", "s_x": "0x12345", "sigma": "0x01" }, + "inputs": { "d": "0x0b", "s_x": "0x12345", "sigma": "0x01" }, "output": [ - "0x266e76dccd3d7c654ab7a48045cca920c497b1a6758981acfa6f217427ac3074", - "0x248b757f9fec92895bd3fb91b1a5c9c17467df6f0f55ab574d85e943df8e5cf3" + "0x2787d1e01bbca7828e13e9b2b3fa11cfcfe9c3d2b121b9e17543146822fe1feb", + "0x27f3739a132c6353cd5af3edac0ac75faf7fc606acb61367774e4f764ec17b77" ] }, { - "inputs": { "d": "0x0b", "s_x": "0x12345", "sigma": "0x01" }, + "inputs": { "d": "0x0c", "s_x": "0x12345", "sigma": "0x01" }, "output": [ - "0x2787d1e01bbca7828e13e9b2b3fa11cfcfe9c3d2b121b9e17543146822fe1feb", - "0x27f3739a132c6353cd5af3edac0ac75faf7fc606acb61367774e4f764ec17b77" + "0x0801e6d7184cea11e6225fcfb4e88a90674cb085da8ebc04918c57cc44b9c25d", + "0x131187e2ac296c1c54c2be2a5cbe614644f01c754108ce25359212d67b36ecc3" ] } ] diff --git a/packages/tokens/src/confidential/circuits/lib/testdata/vk_from_sk.json b/packages/tokens/src/confidential/circuits/lib/testdata/vk_from_sk.json index d14589841..93ba86d3f 100644 --- a/packages/tokens/src/confidential/circuits/lib/testdata/vk_from_sk.json +++ b/packages/tokens/src/confidential/circuits/lib/testdata/vk_from_sk.json @@ -5,7 +5,7 @@ "vectors": [ { "inputs": { "sk": "0xdead", "wrap": "0xbeef" }, - "output": "0x0ac6ade91aea36f4de35eb5a06a67e1b9c3dab9842479f1cce85caf826194ec2" + "output": "0x208fbdb70d2faacf04f987b54f12aeeaeb432acc29d650c86ce0f6275b958eb8" } ] } diff --git a/packages/tokens/src/confidential/circuits/register/src/tests.nr b/packages/tokens/src/confidential/circuits/register/src/tests.nr index 873c0e364..273aa95ef 100644 --- a/packages/tokens/src/confidential/circuits/register/src/tests.nr +++ b/packages/tokens/src/confidential/circuits/register/src/tests.nr @@ -8,8 +8,8 @@ global SK: Field = 0xdead; global WRAP: Field = 0xbeef; global Y_X: Field = 0x1b46b003b88a6c34549dc74115f088f4b231a151397526bc10cbf1d15b457646; global Y_Y: Field = 0x29116280600c10ead1fdbd9ab4b571896030679bf554d7f1ebf681e5147de21b; -global PVK_X: Field = 0x01de20bfe21ad660e073b14a106feae4afce4e78a020953ca16b767e0615331b; -global PVK_Y: Field = 0x12cf407b75fccb5c7721c5fdadc3d1a22b5440e36b08fe52658ab885529e92fd; +global PVK_X: Field = 0x2e7421c0e86a4c8eed823edf851c364ecee448bf70eae2a95befe3dd9364cc45; +global PVK_Y: Field = 0x0b060655740aabef84e7305bb886b75da8d258f346381f03925d69645292fe7a; #[test] fn matches_fixture() { diff --git a/packages/tokens/src/confidential/circuits/vks/register.vk.json b/packages/tokens/src/confidential/circuits/vks/register.vk.json index b65f1c014..c11960427 100644 --- a/packages/tokens/src/confidential/circuits/vks/register.vk.json +++ b/packages/tokens/src/confidential/circuits/vks/register.vk.json @@ -1 +1 @@ -["0x0000000000000000000000000000000000000000000000000000000000004000","0x0000000000000000000000000000000000000000000000000000000000000015","0x0000000000000000000000000000000000000000000000000000000000000001","0x0000000000000000000000000000000000000000000000000000000000000005","0x0000000000000000000000000000002cbf39eee2723f9fd840e1be0073c9a0d8","0x00000000000000000000000000000000001fee7af92f1d1fe10d4507fea42c5c","0x00000000000000000000000000000034815c8250548e54c39459c0f35df59516","0x000000000000000000000000000000000014612d716b73f420884f6b65cb2092","0x00000000000000000000000000000084ddf10a4264d5a79b103864954fa1d7dd","0x00000000000000000000000000000000000c725aba00f697419328c8ceda9710","0x00000000000000000000000000000084e78e08219563e2ce1b061c8219833403","0x0000000000000000000000000000000000175f185390e0e0c084fa44ab6a2039","0x00000000000000000000000000000021f39c3408fbba505949cf04721654a138","0x000000000000000000000000000000000018e20c47b723ffc3185c63df4a960a","0x000000000000000000000000000000aa30f41cdaf9bc53efa32cf1c9d90bb299","0x0000000000000000000000000000000000257cdd46a6d1d26398d70eff5dfaf6","0x0000000000000000000000000000007024e0cbe6450e723eefec65d12ae336bf","0x00000000000000000000000000000000000b42731d30961e77fc3ee6b53899bf","0x00000000000000000000000000000051ea98fa660d79e507aa4df62c9acba779","0x00000000000000000000000000000000002ae867d624fe18c0d0b4971588d976","0x0000000000000000000000000000001b7f4bad0a6c8e45276b7814c024f7c9fd","0x00000000000000000000000000000000001bc30b5c66509f1d2964271ef1eb98","0x0000000000000000000000000000002ed2c54aaa10ffde5a628116db02aaab31","0x000000000000000000000000000000000016e5e189f92184662ea20dafab34e2","0x000000000000000000000000000000c4f695d2fad3de645b505e9c3ea13dbe6f","0x00000000000000000000000000000000000a21752b9c6889920f7b05735d1544","0x000000000000000000000000000000446f25b2aba628a48c507d12f2b3143de9","0x000000000000000000000000000000000023beea8c6369e5bc8819b8afb04185","0x0000000000000000000000000000007c95b756ba3f04a9a6236fc1bebd926514","0x00000000000000000000000000000000001a10c0124a906a4cf2c2236344a54b","0x0000000000000000000000000000005213c7017cafc3ceab9dfd97d4fb66d1df","0x000000000000000000000000000000000001e32415cdfbf774e6196ddfbda4de","0x000000000000000000000000000000338676eb0cc763115ecac4dd45bdc17164","0x00000000000000000000000000000000000ce497f32915d01fda667b3c6d799c","0x000000000000000000000000000000e5b4d8e652291809988391f81a6af916a7","0x000000000000000000000000000000000006a9fd0417e9e672dc84984a51b001","0x000000000000000000000000000000c452b4abb0969024ff9f5fb003effe52d0","0x000000000000000000000000000000000026807f50b5c6c81b5643858053666d","0x000000000000000000000000000000c39e2dabfdaffe0978ed8c724fd854f27b","0x000000000000000000000000000000000012adfcf55e199a695c3cdbb6902efc","0x000000000000000000000000000000412414171bd511ac2129f3977337d89cc1","0x00000000000000000000000000000000001232469b084c2d27f258c4d5a0bc1a","0x000000000000000000000000000000ccfe85419d4bdb653815d0fbe16bec5efc","0x000000000000000000000000000000000006eb3430321f2a4af6f2f7202abb46","0x000000000000000000000000000000d0877cbf0a99c90b279f2889be5a9c3d14","0x00000000000000000000000000000000001ab486acd1fd2b40fb4847cb6695e2","0x00000000000000000000000000000027b513136689042c8221316f7c9d5a5a4a","0x00000000000000000000000000000000000d721e1df10a48bb88ccd47413d17e","0x0000000000000000000000000000000d0d790cf23fea38aac3715871c55cacb0","0x0000000000000000000000000000000000126c54e31961c330e06723d0b2208f","0x000000000000000000000000000000b6bb83bf8dada900f753d36df669ad6383","0x000000000000000000000000000000000003c07ae02fc82655e8fc0aee0a76e9","0x00000000000000000000000000000082842a04f62c7139e1efc0cb5270b9a872","0x000000000000000000000000000000000014522d46d2ef48173369eaa47d6343","0x00000000000000000000000000000055b1a94580a80541573ac2dbb50782dd14","0x00000000000000000000000000000000001e552c529eaf605f4b3b241597dc18","0x000000000000000000000000000000515ae87c58f7eb9bdd60b760155d0a409a","0x00000000000000000000000000000000001fcf7f08f01d459c59821d4bf76db6","0x000000000000000000000000000000add18c995ed7eb094a3bc745cf5c30519c","0x00000000000000000000000000000000001f791c8ebe227a23deb44b06ba96af","0x000000000000000000000000000000c1c8289f80b3c171fe5578d3551f8b8d4e","0x00000000000000000000000000000000000d6da4dcdd5465a0d61023cc6b5227","0x000000000000000000000000000000ad23f7c5bfc2c6931a061f64e3c24a48fe","0x00000000000000000000000000000000001eca9d2193d5fbebcc681885b07054","0x00000000000000000000000000000097efd70a12ea08026f082fc11ab509c280","0x000000000000000000000000000000000002b3247eaf66784f054afe75611f82","0x00000000000000000000000000000010b0cd3a669da156eba716595b4a5c4931","0x00000000000000000000000000000000001f171afbf27b15694eb516465565a3","0x000000000000000000000000000000220061473e1e9d9e5aa9537b49f564b634","0x0000000000000000000000000000000000133881339d3be5c2390edfe02b848c","0x0000000000000000000000000000004bb99b2941ee77765d3bc7ed3d9e4d7c9e","0x00000000000000000000000000000000000b678e0d0531eb37f8338d6aeb13f4","0x000000000000000000000000000000f053b8f7b82096e72fcfed3d55876c6c4e","0x00000000000000000000000000000000001562d96fb60ab98c85c36a335a95a4","0x0000000000000000000000000000006cb17d0094e6206e728a4c1aede3bc281e","0x0000000000000000000000000000000000124c1ec8970ec15a72b1b3d716b1a9","0x0000000000000000000000000000007ce29ab1a4f1fbba3c6f4de5d60e69131c","0x00000000000000000000000000000000001196f4343331de2928257b9870b6e3","0x000000000000000000000000000000fd905a34c508e3e8f877bf285ff7ac9886","0x0000000000000000000000000000000000124b5fbc96921997e41d65baa728e8","0x00000000000000000000000000000028c4dccde75229b33bb507fd7202ba6536","0x00000000000000000000000000000000001136c39b4299c9bf077678616fd21f","0x0000000000000000000000000000007b96f1d38a502a7badb64ee24527c38820","0x0000000000000000000000000000000000164e2e9219df91e1eca116e53c6091","0x000000000000000000000000000000d3572867fb40929727b712e10e7db49fd4","0x00000000000000000000000000000000001b842060fea61eaf6109589ce43078","0x0000000000000000000000000000003eeb74a384aeb7e0bbe60b61ad40bfcc68","0x00000000000000000000000000000000000d48c7f2460f21f2a513b08d99a94c","0x0000000000000000000000000000002f66a9f2427c95067caac7d93cd4929e7d","0x00000000000000000000000000000000000066f0bab7045a560d3e725caa9525","0x000000000000000000000000000000fb7362318e8282249a59bf00ada02d4894","0x0000000000000000000000000000000000229a8d83dea8c91787a007c8d696f7","0x000000000000000000000000000000bcada96cae4677ac7116fa48614e16acb2","0x0000000000000000000000000000000000163a4c6dde0c86fbac73aa094d615a","0x0000000000000000000000000000004d06f9b0fe8d6228210c07a1129fd1d155","0x0000000000000000000000000000000000172798c37bcc9250b4bd39284d81c3","0x00000000000000000000000000000042c99d04c29e4be877301f6f3e8c7d2c18","0x0000000000000000000000000000000000006f42f2c23ec38f554f3214626aa9","0x00000000000000000000000000000040bfbb330c9a3785efe5c975f671d8d614","0x00000000000000000000000000000000000d096d6738835b10ce28926833472a","0x0000000000000000000000000000009bd462667dffaa43f0f9c0fec12932acc0","0x00000000000000000000000000000000000c0de5268cdf1735f9718f55cc97de","0x000000000000000000000000000000cccd853a292a87e8486ebb28dbbf688186","0x0000000000000000000000000000000000294abdcae7bddcd7761a18f1d61fdd","0x0000000000000000000000000000000000000000000000000000000000000001","0x0000000000000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000000000000000000000000000000000000002","0x0000000000000000000000000000000000000000000000000000000000000000","0x000000000000000000000000000000861740f11b4cff0861eb3852f8e8c388b3","0x00000000000000000000000000000000001e539cab87b3086755837b966cf562","0x00000000000000000000000000000062d0b8637e66dbd7e971f29af06b93d710","0x000000000000000000000000000000000015621e2ef72fd514bb7f6592490a80"] \ No newline at end of file +["0x0000000000000000000000000000000000000000000000000000000000004000","0x0000000000000000000000000000000000000000000000000000000000000015","0x0000000000000000000000000000000000000000000000000000000000000001","0x0000000000000000000000000000000000000000000000000000000000000005","0x0000000000000000000000000000002cbf39eee2723f9fd840e1be0073c9a0d8","0x00000000000000000000000000000000001fee7af92f1d1fe10d4507fea42c5c","0x00000000000000000000000000000034815c8250548e54c39459c0f35df59516","0x000000000000000000000000000000000014612d716b73f420884f6b65cb2092","0x00000000000000000000000000000042ed3cf1a9bc788fef9e0b52c8a2f11179","0x00000000000000000000000000000000002c375859a69f4cb15e38e98441ee4c","0x0000000000000000000000000000002ad0211f029358945b2b71d177524f921d","0x00000000000000000000000000000000001bfcadc0e39f57896c854c49594c0a","0x00000000000000000000000000000021f39c3408fbba505949cf04721654a138","0x000000000000000000000000000000000018e20c47b723ffc3185c63df4a960a","0x000000000000000000000000000000aa30f41cdaf9bc53efa32cf1c9d90bb299","0x0000000000000000000000000000000000257cdd46a6d1d26398d70eff5dfaf6","0x0000000000000000000000000000007024e0cbe6450e723eefec65d12ae336bf","0x00000000000000000000000000000000000b42731d30961e77fc3ee6b53899bf","0x00000000000000000000000000000051ea98fa660d79e507aa4df62c9acba779","0x00000000000000000000000000000000002ae867d624fe18c0d0b4971588d976","0x0000000000000000000000000000001b7f4bad0a6c8e45276b7814c024f7c9fd","0x00000000000000000000000000000000001bc30b5c66509f1d2964271ef1eb98","0x0000000000000000000000000000002ed2c54aaa10ffde5a628116db02aaab31","0x000000000000000000000000000000000016e5e189f92184662ea20dafab34e2","0x000000000000000000000000000000c4f695d2fad3de645b505e9c3ea13dbe6f","0x00000000000000000000000000000000000a21752b9c6889920f7b05735d1544","0x000000000000000000000000000000446f25b2aba628a48c507d12f2b3143de9","0x000000000000000000000000000000000023beea8c6369e5bc8819b8afb04185","0x0000000000000000000000000000007c95b756ba3f04a9a6236fc1bebd926514","0x00000000000000000000000000000000001a10c0124a906a4cf2c2236344a54b","0x0000000000000000000000000000005213c7017cafc3ceab9dfd97d4fb66d1df","0x000000000000000000000000000000000001e32415cdfbf774e6196ddfbda4de","0x000000000000000000000000000000338676eb0cc763115ecac4dd45bdc17164","0x00000000000000000000000000000000000ce497f32915d01fda667b3c6d799c","0x000000000000000000000000000000e5b4d8e652291809988391f81a6af916a7","0x000000000000000000000000000000000006a9fd0417e9e672dc84984a51b001","0x000000000000000000000000000000c452b4abb0969024ff9f5fb003effe52d0","0x000000000000000000000000000000000026807f50b5c6c81b5643858053666d","0x000000000000000000000000000000c39e2dabfdaffe0978ed8c724fd854f27b","0x000000000000000000000000000000000012adfcf55e199a695c3cdbb6902efc","0x000000000000000000000000000000412414171bd511ac2129f3977337d89cc1","0x00000000000000000000000000000000001232469b084c2d27f258c4d5a0bc1a","0x000000000000000000000000000000ccfe85419d4bdb653815d0fbe16bec5efc","0x000000000000000000000000000000000006eb3430321f2a4af6f2f7202abb46","0x000000000000000000000000000000d0877cbf0a99c90b279f2889be5a9c3d14","0x00000000000000000000000000000000001ab486acd1fd2b40fb4847cb6695e2","0x00000000000000000000000000000027b513136689042c8221316f7c9d5a5a4a","0x00000000000000000000000000000000000d721e1df10a48bb88ccd47413d17e","0x0000000000000000000000000000000d0d790cf23fea38aac3715871c55cacb0","0x0000000000000000000000000000000000126c54e31961c330e06723d0b2208f","0x000000000000000000000000000000b6bb83bf8dada900f753d36df669ad6383","0x000000000000000000000000000000000003c07ae02fc82655e8fc0aee0a76e9","0x00000000000000000000000000000082842a04f62c7139e1efc0cb5270b9a872","0x000000000000000000000000000000000014522d46d2ef48173369eaa47d6343","0x00000000000000000000000000000055b1a94580a80541573ac2dbb50782dd14","0x00000000000000000000000000000000001e552c529eaf605f4b3b241597dc18","0x000000000000000000000000000000515ae87c58f7eb9bdd60b760155d0a409a","0x00000000000000000000000000000000001fcf7f08f01d459c59821d4bf76db6","0x000000000000000000000000000000add18c995ed7eb094a3bc745cf5c30519c","0x00000000000000000000000000000000001f791c8ebe227a23deb44b06ba96af","0x000000000000000000000000000000c1c8289f80b3c171fe5578d3551f8b8d4e","0x00000000000000000000000000000000000d6da4dcdd5465a0d61023cc6b5227","0x000000000000000000000000000000ad23f7c5bfc2c6931a061f64e3c24a48fe","0x00000000000000000000000000000000001eca9d2193d5fbebcc681885b07054","0x00000000000000000000000000000097efd70a12ea08026f082fc11ab509c280","0x000000000000000000000000000000000002b3247eaf66784f054afe75611f82","0x00000000000000000000000000000010b0cd3a669da156eba716595b4a5c4931","0x00000000000000000000000000000000001f171afbf27b15694eb516465565a3","0x000000000000000000000000000000220061473e1e9d9e5aa9537b49f564b634","0x0000000000000000000000000000000000133881339d3be5c2390edfe02b848c","0x0000000000000000000000000000004bb99b2941ee77765d3bc7ed3d9e4d7c9e","0x00000000000000000000000000000000000b678e0d0531eb37f8338d6aeb13f4","0x000000000000000000000000000000f053b8f7b82096e72fcfed3d55876c6c4e","0x00000000000000000000000000000000001562d96fb60ab98c85c36a335a95a4","0x0000000000000000000000000000006cb17d0094e6206e728a4c1aede3bc281e","0x0000000000000000000000000000000000124c1ec8970ec15a72b1b3d716b1a9","0x0000000000000000000000000000007ce29ab1a4f1fbba3c6f4de5d60e69131c","0x00000000000000000000000000000000001196f4343331de2928257b9870b6e3","0x000000000000000000000000000000fd905a34c508e3e8f877bf285ff7ac9886","0x0000000000000000000000000000000000124b5fbc96921997e41d65baa728e8","0x00000000000000000000000000000028c4dccde75229b33bb507fd7202ba6536","0x00000000000000000000000000000000001136c39b4299c9bf077678616fd21f","0x0000000000000000000000000000007b96f1d38a502a7badb64ee24527c38820","0x0000000000000000000000000000000000164e2e9219df91e1eca116e53c6091","0x000000000000000000000000000000d3572867fb40929727b712e10e7db49fd4","0x00000000000000000000000000000000001b842060fea61eaf6109589ce43078","0x0000000000000000000000000000003eeb74a384aeb7e0bbe60b61ad40bfcc68","0x00000000000000000000000000000000000d48c7f2460f21f2a513b08d99a94c","0x0000000000000000000000000000002f66a9f2427c95067caac7d93cd4929e7d","0x00000000000000000000000000000000000066f0bab7045a560d3e725caa9525","0x000000000000000000000000000000fb7362318e8282249a59bf00ada02d4894","0x0000000000000000000000000000000000229a8d83dea8c91787a007c8d696f7","0x000000000000000000000000000000bcada96cae4677ac7116fa48614e16acb2","0x0000000000000000000000000000000000163a4c6dde0c86fbac73aa094d615a","0x0000000000000000000000000000004d06f9b0fe8d6228210c07a1129fd1d155","0x0000000000000000000000000000000000172798c37bcc9250b4bd39284d81c3","0x00000000000000000000000000000042c99d04c29e4be877301f6f3e8c7d2c18","0x0000000000000000000000000000000000006f42f2c23ec38f554f3214626aa9","0x00000000000000000000000000000040bfbb330c9a3785efe5c975f671d8d614","0x00000000000000000000000000000000000d096d6738835b10ce28926833472a","0x0000000000000000000000000000009bd462667dffaa43f0f9c0fec12932acc0","0x00000000000000000000000000000000000c0de5268cdf1735f9718f55cc97de","0x000000000000000000000000000000cccd853a292a87e8486ebb28dbbf688186","0x0000000000000000000000000000000000294abdcae7bddcd7761a18f1d61fdd","0x0000000000000000000000000000000000000000000000000000000000000001","0x0000000000000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000000000000000000000000000000000000002","0x0000000000000000000000000000000000000000000000000000000000000000","0x000000000000000000000000000000861740f11b4cff0861eb3852f8e8c388b3","0x00000000000000000000000000000000001e539cab87b3086755837b966cf562","0x00000000000000000000000000000062d0b8637e66dbd7e971f29af06b93d710","0x000000000000000000000000000000000015621e2ef72fd514bb7f6592490a80"] \ No newline at end of file diff --git a/packages/tokens/src/confidential/circuits/vks/withdraw.vk.json b/packages/tokens/src/confidential/circuits/vks/withdraw.vk.json index abc61d4a2..cbf5d95b8 100644 --- a/packages/tokens/src/confidential/circuits/vks/withdraw.vk.json +++ b/packages/tokens/src/confidential/circuits/vks/withdraw.vk.json @@ -1 +1 @@ -["0x0000000000000000000000000000000000000000000000000000000000008000","0x000000000000000000000000000000000000000000000000000000000000001f","0x0000000000000000000000000000000000000000000000000000000000000001","0x000000000000000000000000000000000000000000000000000000000000000f","0x000000000000000000000000000000eeba595d642c330ff0052a30356aef944d","0x000000000000000000000000000000000010e3400ec4848ce654b0033bd1909c","0x0000000000000000000000000000002b7a9a001dcd869f8a0f57a9463fbda32e","0x00000000000000000000000000000000000a1c18400fcebbffedebc9f1d7957f","0x000000000000000000000000000000037dca156d5c0e70124cea51e227a0146b","0x00000000000000000000000000000000002a4b62dbc27de48f7031e5aab1583e","0x000000000000000000000000000000136c427b7f48da876c44c2adc3a2369e5d","0x000000000000000000000000000000000022e1a1a0c6e0adb02dbed58ed9333b","0x00000000000000000000000000000086be6e47fb05c8a34ee650da276874a16e","0x00000000000000000000000000000000002ffcdf47719489739c0ccd79a094a4","0x0000000000000000000000000000001d83a98627294b99ea6b8b987226bca454","0x0000000000000000000000000000000000112246923b49d0709e410042c4860b","0x00000000000000000000000000000038f6ec2a41271e1da7b722ee6abc79ff09","0x0000000000000000000000000000000000083462567704201deb6dcc33c9cfe4","0x000000000000000000000000000000401aaa7d6ab87aa284f84f8ad30ac71c49","0x00000000000000000000000000000000002249363ebb246b83846ad9ac5b3ed6","0x000000000000000000000000000000bae8ada0ee2188885f5b8effd9e61b02a1","0x0000000000000000000000000000000000286bf8187ddec53c2ec9ac55e9cdd4","0x000000000000000000000000000000c101fd8b4d894b84208bc3720568bdca84","0x00000000000000000000000000000000002b0edb22bc1d39cba3be724c04018d","0x000000000000000000000000000000b06aacb1989ba061c490d4f2ec0857d429","0x00000000000000000000000000000000001df228e6da104ccd52eb0f004112bd","0x000000000000000000000000000000f803b078140485be6e5e6fe54b54758dd6","0x00000000000000000000000000000000002bdae37d3fae349df4b291c8b28987","0x000000000000000000000000000000176df2b9f9816b99b87dcd27314f248492","0x00000000000000000000000000000000000396fae9cfc50f85ebfe6bf44327d7","0x0000000000000000000000000000000107a790c778e6a2ffbae75e5764ee8cc2","0x00000000000000000000000000000000000e0f2756936ef0c0b2175663320713","0x000000000000000000000000000000df66bc3e9f02258d5ab62bdc7217cf2d8d","0x0000000000000000000000000000000000071538f67f71e31ecf217d87ac3038","0x000000000000000000000000000000e36eddb6fd7bbbbe99f71ff10df7ef86fa","0x00000000000000000000000000000000000a42c0ff54a34a80cb110dcb90f724","0x00000000000000000000000000000068bdc47e00f54f7343c34b2c48368ebde6","0x00000000000000000000000000000000001d418813d959151e7f2eba7d7f5c51","0x000000000000000000000000000000cb095b6f7caee08e4e5e30eea991d86920","0x0000000000000000000000000000000000106af79cd3966436c97d57b5b3fc2a","0x000000000000000000000000000000bd02c58e4ee0c2bb494de03af6bc7526ab","0x00000000000000000000000000000000000d6405504013ca3e28a415a7f71669","0x000000000000000000000000000000685cb2ece723d459108a821240d3fc2262","0x000000000000000000000000000000000029244ec479537ad66815d0389fc4d2","0x0000000000000000000000000000004415003ff210bb2ce2f3bef905b903520b","0x00000000000000000000000000000000001542f730190d952f86315d3d29457a","0x00000000000000000000000000000054d142734abe1f06533e7316a0ddd16496","0x00000000000000000000000000000000001873d1a3f4ddbf6e0958e5bb1106af","0x000000000000000000000000000000a98543fb0eb78f681eb4cb0d3e14d2a9dd","0x000000000000000000000000000000000014fcb07f82d6efdd47ca3f7248c61e","0x00000000000000000000000000000005ba5d932d7b77bef91294a42ef37aa1b1","0x00000000000000000000000000000000000b627e9389774df7f4bd5563897614","0x000000000000000000000000000000f01a7d58ea57541ff46b6bfe1193f521c7","0x00000000000000000000000000000000002b084ad4761ae968a05151fed32ac2","0x0000000000000000000000000000009df8752bd4bc40071d8a3821dea722997d","0x000000000000000000000000000000000003cc62c1a745e20ef2af508d94d96a","0x000000000000000000000000000000e87bcab37cd4e308aa243f2f00bbcdb146","0x00000000000000000000000000000000000f5f0933ac6e90a320c03886ce5ef5","0x00000000000000000000000000000010c95aed0ecbe9e328bc0022428fe2b9d1","0x0000000000000000000000000000000000261e1356aa63e9f3b929351ef34520","0x000000000000000000000000000000d0316171b1fcbc63d492e5c921e1d85534","0x0000000000000000000000000000000000135bf1d5004195d5aee992f4e8d2ef","0x000000000000000000000000000000e2f72da69c7cbf9bd22617ef95bc07db46","0x00000000000000000000000000000000000f4c6d60625829f72ad52827157819","0x000000000000000000000000000000aed2ec42f4730788107991640341ecec01","0x00000000000000000000000000000000002ccb40475a3d4394cf306d8e187b46","0x0000000000000000000000000000004fbe69b6da34d7526ca644393f340acb1f","0x00000000000000000000000000000000002055a571e8058e11fe5bd7a70df194","0x0000000000000000000000000000002a827b802fdc3fa2bb96e45de06250fec6","0x0000000000000000000000000000000000283d3c31b996ab705d67d593823e23","0x000000000000000000000000000000bc1001981431c53496a849a2259d03b457","0x000000000000000000000000000000000016245614bd6436e48273825ec30279","0x000000000000000000000000000000d0b237370730e5c12b49202f8eed05caa0","0x000000000000000000000000000000000011834ef1f9a10486f2ca206bc00455","0x000000000000000000000000000000cda55dfbdd2802299831bca66dc058e055","0x0000000000000000000000000000000000207f3ebb920fc90d406d3a612a2daf","0x000000000000000000000000000000e13bb20551a2af5f8ac36ebaac7876a1c4","0x000000000000000000000000000000000028017bb468f2ed75a6887614e5a9b1","0x000000000000000000000000000000b5a4ceab339d24da55f579aa04f192eab2","0x0000000000000000000000000000000000040edbf67c3d6d99fa176b85dae651","0x000000000000000000000000000000cfb60fe92fb07d24b9de89186c75086f1e","0x00000000000000000000000000000000000a4cc323ddffb2bde796024f39bc52","0x0000000000000000000000000000006fb079278fc9ad6c879493fc3bc60da717","0x00000000000000000000000000000000001588a466568529e8a6c5b33fcc26e3","0x000000000000000000000000000000bf3de286969e366510b04f43ee02b06117","0x000000000000000000000000000000000008175e28145dca69106c8791d8a2e5","0x0000000000000000000000000000002c116fa0beca775474a5fe7b78fc2f2bba","0x00000000000000000000000000000000000c999129c3c06772c6fbbc7355c09c","0x000000000000000000000000000000984d80910016772cb83ed3881f90ebcc8c","0x00000000000000000000000000000000001b6540dd28b967d06897d2fe4f4732","0x000000000000000000000000000000b7445646b5160c380130b66e34450aa736","0x00000000000000000000000000000000000ef8897e2c65d0ec75dc09f85edeac","0x00000000000000000000000000000045688ef774d85c8bd41f9665bad2d794c8","0x00000000000000000000000000000000002ac07a74c19ad88489ed6f1e93dc9e","0x000000000000000000000000000000a5a32649850aae3c839626ec285717f399","0x000000000000000000000000000000000010cdf0710877e231d375a2ef4dc3d3","0x000000000000000000000000000000160d4e95b954b96077723f1800f420dc77","0x0000000000000000000000000000000000280ae74a2dae2f5e68706031174b76","0x000000000000000000000000000000440bb5fc5471f6a37dbe12fbef6a37c009","0x0000000000000000000000000000000000106864b3460bfded9d5dc448fe29e8","0x000000000000000000000000000000d29de5d4acc99c6c72fa17e0a80bf88401","0x000000000000000000000000000000000007e83dfa443e984d53e0323523f742","0x0000000000000000000000000000001cfe095fa8e5d91145e1222746c27083ad","0x000000000000000000000000000000000006a7d963d96d78bc035faf0a744f8e","0x0000000000000000000000000000000000000000000000000000000000000001","0x0000000000000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000000000000000000000000000000000000002","0x0000000000000000000000000000000000000000000000000000000000000000","0x000000000000000000000000000000f0632125fa5d1bd312c714447d2ee3f921","0x00000000000000000000000000000000000e3504c34901d8cf39ca6008e672b7","0x00000000000000000000000000000015d3e4e604b8962bdca2dff461051dca20","0x00000000000000000000000000000000002d2b8c20d3ca4c73e2f8b6854b8c67"] \ No newline at end of file +["0x0000000000000000000000000000000000000000000000000000000000008000","0x000000000000000000000000000000000000000000000000000000000000001f","0x0000000000000000000000000000000000000000000000000000000000000001","0x000000000000000000000000000000000000000000000000000000000000000f","0x000000000000000000000000000000eeba595d642c330ff0052a30356aef944d","0x000000000000000000000000000000000010e3400ec4848ce654b0033bd1909c","0x0000000000000000000000000000002b7a9a001dcd869f8a0f57a9463fbda32e","0x00000000000000000000000000000000000a1c18400fcebbffedebc9f1d7957f","0x000000000000000000000000000000cd6295581e4781b58cf58eb5da08908d71","0x00000000000000000000000000000000000a91d2db4017d8061778a2683223c8","0x000000000000000000000000000000141af02cedab14ac75e6dc574fe15c9dbc","0x00000000000000000000000000000000002666cf01eee43af06bb16f2374d0a8","0x00000000000000000000000000000086be6e47fb05c8a34ee650da276874a16e","0x00000000000000000000000000000000002ffcdf47719489739c0ccd79a094a4","0x0000000000000000000000000000001d83a98627294b99ea6b8b987226bca454","0x0000000000000000000000000000000000112246923b49d0709e410042c4860b","0x00000000000000000000000000000038f6ec2a41271e1da7b722ee6abc79ff09","0x0000000000000000000000000000000000083462567704201deb6dcc33c9cfe4","0x000000000000000000000000000000401aaa7d6ab87aa284f84f8ad30ac71c49","0x00000000000000000000000000000000002249363ebb246b83846ad9ac5b3ed6","0x000000000000000000000000000000bae8ada0ee2188885f5b8effd9e61b02a1","0x0000000000000000000000000000000000286bf8187ddec53c2ec9ac55e9cdd4","0x000000000000000000000000000000c101fd8b4d894b84208bc3720568bdca84","0x00000000000000000000000000000000002b0edb22bc1d39cba3be724c04018d","0x000000000000000000000000000000b06aacb1989ba061c490d4f2ec0857d429","0x00000000000000000000000000000000001df228e6da104ccd52eb0f004112bd","0x000000000000000000000000000000f803b078140485be6e5e6fe54b54758dd6","0x00000000000000000000000000000000002bdae37d3fae349df4b291c8b28987","0x000000000000000000000000000000176df2b9f9816b99b87dcd27314f248492","0x00000000000000000000000000000000000396fae9cfc50f85ebfe6bf44327d7","0x0000000000000000000000000000000107a790c778e6a2ffbae75e5764ee8cc2","0x00000000000000000000000000000000000e0f2756936ef0c0b2175663320713","0x000000000000000000000000000000df66bc3e9f02258d5ab62bdc7217cf2d8d","0x0000000000000000000000000000000000071538f67f71e31ecf217d87ac3038","0x000000000000000000000000000000e36eddb6fd7bbbbe99f71ff10df7ef86fa","0x00000000000000000000000000000000000a42c0ff54a34a80cb110dcb90f724","0x00000000000000000000000000000068bdc47e00f54f7343c34b2c48368ebde6","0x00000000000000000000000000000000001d418813d959151e7f2eba7d7f5c51","0x000000000000000000000000000000cb095b6f7caee08e4e5e30eea991d86920","0x0000000000000000000000000000000000106af79cd3966436c97d57b5b3fc2a","0x000000000000000000000000000000bd02c58e4ee0c2bb494de03af6bc7526ab","0x00000000000000000000000000000000000d6405504013ca3e28a415a7f71669","0x000000000000000000000000000000685cb2ece723d459108a821240d3fc2262","0x000000000000000000000000000000000029244ec479537ad66815d0389fc4d2","0x0000000000000000000000000000004415003ff210bb2ce2f3bef905b903520b","0x00000000000000000000000000000000001542f730190d952f86315d3d29457a","0x00000000000000000000000000000054d142734abe1f06533e7316a0ddd16496","0x00000000000000000000000000000000001873d1a3f4ddbf6e0958e5bb1106af","0x000000000000000000000000000000a98543fb0eb78f681eb4cb0d3e14d2a9dd","0x000000000000000000000000000000000014fcb07f82d6efdd47ca3f7248c61e","0x00000000000000000000000000000005ba5d932d7b77bef91294a42ef37aa1b1","0x00000000000000000000000000000000000b627e9389774df7f4bd5563897614","0x000000000000000000000000000000f01a7d58ea57541ff46b6bfe1193f521c7","0x00000000000000000000000000000000002b084ad4761ae968a05151fed32ac2","0x0000000000000000000000000000009df8752bd4bc40071d8a3821dea722997d","0x000000000000000000000000000000000003cc62c1a745e20ef2af508d94d96a","0x000000000000000000000000000000e87bcab37cd4e308aa243f2f00bbcdb146","0x00000000000000000000000000000000000f5f0933ac6e90a320c03886ce5ef5","0x00000000000000000000000000000010c95aed0ecbe9e328bc0022428fe2b9d1","0x0000000000000000000000000000000000261e1356aa63e9f3b929351ef34520","0x000000000000000000000000000000d0316171b1fcbc63d492e5c921e1d85534","0x0000000000000000000000000000000000135bf1d5004195d5aee992f4e8d2ef","0x000000000000000000000000000000e2f72da69c7cbf9bd22617ef95bc07db46","0x00000000000000000000000000000000000f4c6d60625829f72ad52827157819","0x000000000000000000000000000000aed2ec42f4730788107991640341ecec01","0x00000000000000000000000000000000002ccb40475a3d4394cf306d8e187b46","0x0000000000000000000000000000004fbe69b6da34d7526ca644393f340acb1f","0x00000000000000000000000000000000002055a571e8058e11fe5bd7a70df194","0x0000000000000000000000000000002a827b802fdc3fa2bb96e45de06250fec6","0x0000000000000000000000000000000000283d3c31b996ab705d67d593823e23","0x000000000000000000000000000000bc1001981431c53496a849a2259d03b457","0x000000000000000000000000000000000016245614bd6436e48273825ec30279","0x000000000000000000000000000000d0b237370730e5c12b49202f8eed05caa0","0x000000000000000000000000000000000011834ef1f9a10486f2ca206bc00455","0x000000000000000000000000000000cda55dfbdd2802299831bca66dc058e055","0x0000000000000000000000000000000000207f3ebb920fc90d406d3a612a2daf","0x000000000000000000000000000000e13bb20551a2af5f8ac36ebaac7876a1c4","0x000000000000000000000000000000000028017bb468f2ed75a6887614e5a9b1","0x000000000000000000000000000000b5a4ceab339d24da55f579aa04f192eab2","0x0000000000000000000000000000000000040edbf67c3d6d99fa176b85dae651","0x000000000000000000000000000000cfb60fe92fb07d24b9de89186c75086f1e","0x00000000000000000000000000000000000a4cc323ddffb2bde796024f39bc52","0x0000000000000000000000000000006fb079278fc9ad6c879493fc3bc60da717","0x00000000000000000000000000000000001588a466568529e8a6c5b33fcc26e3","0x000000000000000000000000000000bf3de286969e366510b04f43ee02b06117","0x000000000000000000000000000000000008175e28145dca69106c8791d8a2e5","0x0000000000000000000000000000002c116fa0beca775474a5fe7b78fc2f2bba","0x00000000000000000000000000000000000c999129c3c06772c6fbbc7355c09c","0x000000000000000000000000000000984d80910016772cb83ed3881f90ebcc8c","0x00000000000000000000000000000000001b6540dd28b967d06897d2fe4f4732","0x000000000000000000000000000000b7445646b5160c380130b66e34450aa736","0x00000000000000000000000000000000000ef8897e2c65d0ec75dc09f85edeac","0x00000000000000000000000000000045688ef774d85c8bd41f9665bad2d794c8","0x00000000000000000000000000000000002ac07a74c19ad88489ed6f1e93dc9e","0x000000000000000000000000000000a5a32649850aae3c839626ec285717f399","0x000000000000000000000000000000000010cdf0710877e231d375a2ef4dc3d3","0x000000000000000000000000000000160d4e95b954b96077723f1800f420dc77","0x0000000000000000000000000000000000280ae74a2dae2f5e68706031174b76","0x000000000000000000000000000000440bb5fc5471f6a37dbe12fbef6a37c009","0x0000000000000000000000000000000000106864b3460bfded9d5dc448fe29e8","0x000000000000000000000000000000d29de5d4acc99c6c72fa17e0a80bf88401","0x000000000000000000000000000000000007e83dfa443e984d53e0323523f742","0x0000000000000000000000000000001cfe095fa8e5d91145e1222746c27083ad","0x000000000000000000000000000000000006a7d963d96d78bc035faf0a744f8e","0x0000000000000000000000000000000000000000000000000000000000000001","0x0000000000000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000000000000000000000000000000000000002","0x0000000000000000000000000000000000000000000000000000000000000000","0x000000000000000000000000000000f0632125fa5d1bd312c714447d2ee3f921","0x00000000000000000000000000000000000e3504c34901d8cf39ca6008e672b7","0x00000000000000000000000000000015d3e4e604b8962bdca2dff461051dca20","0x00000000000000000000000000000000002d2b8c20d3ca4c73e2f8b6854b8c67"] \ No newline at end of file diff --git a/packages/tokens/src/confidential/circuits/withdraw/src/tests.nr b/packages/tokens/src/confidential/circuits/withdraw/src/tests.nr index 32cb0874e..d481f0619 100644 --- a/packages/tokens/src/confidential/circuits/withdraw/src/tests.nr +++ b/packages/tokens/src/confidential/circuits/withdraw/src/tests.nr @@ -41,11 +41,11 @@ global C_SPEND_Y: Field = // which re-derives them from the lib primitives and asserts the values // declared here -- if either side drifts the test fails immediately. global C_SPEND_NEW_X: Field = - 0x2fb2d785d71d4c62ce4130675a6c44020c03a236ab21450b6f20282549caadac; + 0x22f167015ee0e4b33dcb278cdc824c336c919dc4cd06f391850305d5083336e6; global C_SPEND_NEW_Y: Field = - 0x069a978c07b99e97858a6e94cef5b3d2c49f1df0d2aa5e04dffcefaca9320814; + 0x0be3d0fda4f8f70b04e2d609db4e29b30345da9288bf208c7560572b2bef172d; global B_TILDE: Field = - 0x2b7f92c72c05ae8ecf7207fdaecea32ef748cd4dd43ca9ed6dc6d226865b4283; + 0x04d1659db899a50a94dcfc54a18b8adaf6e9e8e3046bd11893fbd4a86a7c5670; // Auditor-block fixtures. Pinned by `withdraw_auditor_fixtures_match_lib`. // R_E_X (= (R_E * H).x) matches the lib's `ecdh` fixture (`ecdh(r_e, H)` for @@ -58,7 +58,7 @@ global K_AUD_S_Y: Field = global R_E_X: Field = 0x114ed4fcf2c57014eb678c577aa02f30ef590b713d7a6a5e87702d1c7f71957f; global R_E_Y: Field = 0x07a70cf826350d4f438c7a3c5e8761b0ae6cb63de757f0c96815f4057b9205f4; global B_TILDE_AUD_S: Field = - 0x216ab12ea2182712721d79c9a9222a261452ab06a90309e23efaa6ecb0e20714; + 0x0a607feb31e56e89ed94a3e1dc3811d3d23aaf10b2f27874ce670878b9f6f071; #[test] fn print_fixtures() { @@ -401,7 +401,7 @@ fn rejects_tampered_c_spend_new() { #[test(should_fail)] fn rejects_r_e_zero() { - // W_a5 fires: r_e = 0 would force R_e = O (identity) and collapse + // W8 fires: r_e = 0 would force R_e = O (identity) and collapse // S_a,s to O, making m_b a knowable constant function of sigma. main( SK, From a1d04af0175b16ce0f72354520c98ea18d014b94 Mon Sep 17 00:00:00 2001 From: brozorec <9572072+brozorec@users.noreply.github.com> Date: Tue, 26 May 2026 11:56:47 +0200 Subject: [PATCH 09/16] docs: add design and compliance docs --- .../confidential/docs/COMPLIANCE.github.md | 179 +++ .../src/confidential/docs/DESIGN.github.md | 1313 +++++++++++++++++ 2 files changed, 1492 insertions(+) create mode 100644 packages/tokens/src/confidential/docs/COMPLIANCE.github.md create mode 100644 packages/tokens/src/confidential/docs/DESIGN.github.md diff --git a/packages/tokens/src/confidential/docs/COMPLIANCE.github.md b/packages/tokens/src/confidential/docs/COMPLIANCE.github.md new file mode 100644 index 000000000..91ba661a1 --- /dev/null +++ b/packages/tokens/src/confidential/docs/COMPLIANCE.github.md @@ -0,0 +1,179 @@ +# Confidential Token Wrapper: Compliance Extensions + +## Abstract + +This document specifies optional, deployer-configurable controls layered on top of the core Confidential Token Wrapper (see [DESIGN.md](DESIGN.md)). It covers account freezing, SAC authorization passthrough, pluggable authorization policies, unregistered-deposit handling, and the pooled-custody clawback flow. + +All controls are configured at construction time through a single `compliance: Option` entry. A vanilla deployment leaves the entry empty and pays no compliance overhead. Regulated deployments populate the slot once; subsequent state changes (freeze toggles, admin rotation, policy swap) flow through admin-gated entry points. + +--- + +## 1. Configuration + +```rust +struct ComplianceConfig { + policy: Option
, // ยง3 + sac_passthrough: bool, // ยง2 + permit_unregistered_deposit: bool, // ยง4 +} +``` + +| Field | Purpose | +|:---|:---| +| `policy` | Optional external authorization contract (ยง3). `None` means no policy gate. | +| `sac_passthrough` | When `true`, every state-modifying operation additionally consults the underlying SAC's `authorized()` check (ยง2). | +| `permit_unregistered_deposit` | When `true`, `deposit` skips policy and freeze checks on an unregistered `from` (ยง4). | + +The constructor takes `compliance: Option`. When `None`, the wrapper behaves exactly as `DESIGN.md` specifies: no pre-checks run, and the admin-gated entry points in ยง6 revert with `NotConfigured`. + +### 1.1 Admin Authority + +This document refers to an "admin" as the authority gating freeze, unfreeze, configuration rotation, and clawback. The wrapper does not prescribe how that authority is structured. Implementors can compose with an access-control module from the OpenZeppelin Soroban library (e.g., `ownable` for a single-owner model or `access_control` for role-based separation between freeze, policy, and clawback authorities). Admin-gated entry points invoke the chosen module's check (`only_owner`, `only_role`, etc.) at the top of the function. + +Deployments that need separation of duties (distinct freeze, policy, and clawback signers) reach for RBAC; deployments with a single jurisdictional authority use ownable. The wrapper sees only the result of the access check. + +--- + +## 2. Wrapper-Level Freeze + +The wrapper maintains a `frozen(account) -> bool` entry per account. Before applying any state change, every state-modifying operation runs `check_not_frozen` against each account it names (sender, recipient, operator). A frozen account cannot send, receive, deposit, withdraw, or participate as an operator. The check reverts at the contract boundary. + +Full freeze (rather than outbound-only) keeps semantics clean: no further accumulation is possible after the freeze takes effect. + +### 2.1 Core Interface Additions + +Three functions are added to the core wrapper interface: + +```rust +impl Wrapper { + fn freeze(e: Env, account: Address, admin: Address); // admin auth + fn unfreeze(e: Env, account: Address, admin: Address); // admin auth + fn is_frozen(e: Env, account: Address) -> bool; +} +``` + +`freeze` and `unfreeze` are gated by the implementor's access-control module (ยง1.1) and revert when `compliance.is_none()`. `is_frozen` is a public read; it returns `false` when compliance is not configured. + +### 2.2 SAC Authorization Passthrough + +When `sac_passthrough = true` and the underlying SEP-41 is a Stellar Asset Contract, every state-modifying operation additionally calls `sac.authorized(account)` for each named account and reverts on `false`. This composes the wrapper's freeze with the issuer's freeze without requiring the admin to mirror state: + +$$\text{permitted}(a) = \neg \text{frozen}(a) \;\land\; \text{policy\\\_ok}(a) \;\land\; (\neg \text{sac\\\_passthrough} \;\lor\; \text{sac.authorized}(a))$$ + +Off by default. Issuer-led deployments using a SAC underlying opt in at construction. The cost is one extra cross-contract invocation per named account per operation. + +--- + +## 3. Policy Contract + +When `compliance.policy = Some(addr)`, every state-modifying operation invokes `policy.is_authorized(account, wrapper) -> bool` on the configured contract for each named account, reverting on `false`. The policy is consulted in addition to the freeze check and (where enabled) the SAC passthrough. + +```rust +trait Policy { + fn is_authorized(e: Env, account: Address, wrapper: Address) -> bool; +} +``` + +This single hook covers the common deployment modes without baking them into the wrapper: + +- **Allowlist:** the policy returns `true` only for listed addresses. +- **Denylist:** the policy returns `true` for everything except listed addresses. +- **KYC / ASP / sanctions screening:** the policy delegates to an identity registry, attestation provider, or sanctions oracle. + +Membership management, list semantics, and identity proofs live entirely inside the policy contract. The wrapper's only contract with the policy is the boolean return value. + +Externalizing the policy also lets a single registry serve multiple wrappers. An issuer running several confidential tokens (different denominations, jurisdictions, or product lines) can point every wrapper at the same KYC or sanctions contract and maintain one source of truth, rather than mirroring lists into each wrapper. The `wrapper` argument to `is_authorized` lets the registry apply per-wrapper rules when needed (e.g., a jurisdiction filter) without giving up the shared baseline. + +The policy address is rotatable via `set_compliance_config` (ยง6) under admin auth (ยง1.1). Setting it to `None` disables the gate. The policy is part of the deployment's trust surface. + +--- + +## 4. Unregistered Deposits + +`deposit` is the one operation where `from` may legitimately be an address that has never registered with the wrapper (the depositor only needs to hold the underlying SEP-41). Two cases: + +1. **`permit_unregistered_deposit = false` (default).** Unregistered `from` fails the policy check by default (most policy contracts will not list addresses they have not seen). The operation reverts. +2. **`permit_unregistered_deposit = true`.** When `from` is unregistered, the wrapper skips the policy and freeze checks on `from` and proceeds with the deposit. Checks on `to` (the registered recipient) are unaffected. + +This carve-out lets regulated deployments accept inbound payments from non-listed external counterparties (e.g., an exchange wallet depositing into a payroll pool) while keeping the recipient-side guarantees intact. Deployments that need a stricter posture leave the flag `false` and force every depositor through registration first. + +The flag is set at construction and rotatable via `set_compliance_config` (ยง6) under admin auth. + +--- + +## 5. Clawback (Outline Only) + +### 5.1 The Pooled-Custody Problem + +Once an account deposits into the wrapper, the underlying SEP-41 ledger lists the wrapper contract as the holder of those funds, not the depositor. An issuer's SAC-level `clawback(wrapper_address, amount)` call would drain the pool, debiting unrelated accounts. The wrapper therefore does not forward SAC-level clawback to individual confidential accounts; it must instead extract value from a single targeted account's confidential balance and settle that value to the issuer through a transparent path. + +The challenge is that the wrapper does not know the targeted account's balance. The balance is held as a Pedersen commitment whose opening is private to the owner. The clawback amount must be validated against the actual encrypted value without exposing it on-chain and without trusting the admin to choose a value at random. + +### 5.2 Admin + Auditor Coordination + +The clawback flow requires cooperation between two roles: + +- **Admin** authorizes the action on-chain: freezes the target, calls the clawback entry point, and settles the recovered amount to the issuer. +- **Auditor** unlocks knowledge of the target's balance. Two halves of the target's confidential position are covered by the two auditor channels (see `DESIGN.md` ยง8.1, ยง8.2). The **sender-auditor** decrypts the spendable-balance checkpoint $\tilde{b}_{\text{aud,s}}$ from the target's most recent owner-initiated event, recovering $v_s$. The **recipient-auditor** decrypts the per-transfer pairs $(v_{\text{tx},i}, r_{\text{tx},i})$ from every inbound transfer and operator-transfer since the last merge, recovering the full Pedersen opening $(v_r, r_r)$ of the target's `receiving_balance`. The auditor then produces a zero-knowledge proof bounding the clawback amount by $v_s + v_r$, without revealing either summand. + +Neither party can act alone: the admin cannot produce the proof, and the auditor cannot freeze the account or move funds. This is the same trust separation present in the core protocol (admin governs state transitions, auditor governs visibility) extended to a write surface. + +The admin role here is the same access-control surface introduced in ยง1.1; deployments typically place it under a dedicated `clawback` role in RBAC, separate from the freeze role. + +**Auditor routing.** The recipient-auditor and the sender-auditor roles for a single account are served by the same key: each account binds a single `auditor_id` at registration (`DESIGN.md` ยง6.1) which the wrapper uses for both the sender-channel ciphertexts on the account's outgoing operations and the recipient-channel ciphertexts on the account's incoming transfers (the two channels are separated by domain tags $\delta_{\text{aud\\\_s}}$ and $\delta_{\text{aud\\\_r}}$, not by distinct keys). Deployments that intend to use clawback therefore need only ensure the off-chain custodian of that key is operationally capable of producing both halves of the witness โ€” the spendable-balance checkpoint decryption and the per-transfer $r_{\text{tx},i}$ replay โ€” when the admin initiates a seizure. + +### 5.3 New Circuit + +The clawback proof is a constant-size circuit deployed through the existing Verifier surface. It binds the seize amount $\alpha$ by the sum of the spendable and receiving balances of the target account, refreshes the spendable-balance checkpoint, and rewrites `receiving_balance` to a zero commitment so the seized inbound flow is consumed atomically. + +**Public inputs.** $C_{\text{spend}}, C_{\text{receive}}, K_{\text{aud,s}}, \tilde{b}_{\text{aud,s}}^{\text{old}}, R_e^{\text{old}}, \sigma^{\text{old}}, \alpha, \tilde{b}_{\text{aud,s}}^{\text{new}}, R_e^{\text{new}}, \sigma^{\text{new}}, \text{wrap}$. + +**Private witnesses.** $k_{\text{aud,s}}, v_s, r_s, v_r, r_r, r_e^{\text{new}}$, plus the sponge outputs from old and new auditor-channel sponge calls. The recipient-auditor's secret key does not appear in the witness because the recipient-channel decryption (recovery of $(v_r, r_r)$ from per-transfer events) is performed off-chain by the auditor; the circuit only re-verifies the resulting Pedersen opening of $C_{\text{receive}}$ (constraint 1). + +**Constraints (sketch).** + +1. **Receiving-balance opening.** $C_{\text{receive}} = v_r \cdot G + r_r \cdot H$. The recipient-auditor reconstructs $(v_r, r_r)$ off-chain from per-transfer events; the proof asserts knowledge of this opening. +2. **Spendable-balance decryption.** $(m_{v,s}^{\text{old}}, m_{b,s}^{\text{old}}) = \text{SpongeSqueeze}_2(\delta_{\text{aud\\\_s}}, (k_{\text{aud,s}} \cdot R_e^{\text{old}}).x, \sigma^{\text{old}})$ and $v_s = \tilde{b}_{\text{aud,s}}^{\text{old}} - m_{b,s}^{\text{old}}$. The spendable-balance opening $(v_s, r_s)$ is consistent with $C_{\text{spend}} = v_s \cdot G + r_s \cdot H$ where $r_s$ is recovered via the same path the wallet uses for checkpoint recovery (`DESIGN.md` ยง5.2): $r_s = \text{Poseidon}(\delta_{\text{spend\\\_r}}, vk_A, \sigma^{\text{old}})$. Because the clawback circuit does not have access to $vk_A$, the spendable-balance side of the proof binds via the consistency of $\tilde{b}_{\text{aud,s}}^{\text{old}}$ with $C_{\text{spend}}$ at the time of the last owner-initiated proof. The follow-up revision will pin down whether $r_s$ is supplied as a private witness with an auxiliary opening proof or derived in-circuit from a separately escrowed value. +3. **Range and bound.** $\alpha, v_s, v_r \in [0, 2^{127})$ and $\alpha \le v_s + v_r$. +4. **Refreshed checkpoint.** $R_e^{\text{new}} = r_e^{\text{new}} \cdot H$, $r_e^{\text{new}} \neq 0$, and $\tilde{b}_{\text{aud,s}}^{\text{new}} = (v_s + v_r - \alpha) + m_{b,s}^{\text{new}}$ where $(m_{v,s}^{\text{new}}, m_{b,s}^{\text{new}}) = \text{SpongeSqueeze}_2(\delta_{\text{aud\\\_s}}, (k_{\text{aud,s}} \cdot R_e^{\text{new}}).x, \sigma^{\text{new}})$. + +**Post-verification.** The wrapper sets $C_{\text{spend}} \leftarrow (v_s + v_r - \alpha) \cdot G + r_s' \cdot H$ under fresh deterministic randomness $r_s'$ (admin-derived, since $vk_A$ is unavailable), zeroes $C_{\text{receive}}$, transfers $\alpha$ of the underlying SEP-41 token to the issuer, and emits an event carrying $(\tilde{b}_{\text{aud,s}}^{\text{new}}, R_e^{\text{new}}, \sigma^{\text{new}})$ so the sender-auditor sees the new checkpoint. + +**Anti-replay.** The wrapper consumes $C_{\text{spend}}$ and $C_{\text{receive}}$ as proof public inputs at verification time. If either commitment changes between proof construction and submission (e.g., an inbound transfer arrives), verification fails because the proof was bound to a different $C_{\text{receive}}$. The ยง2 wrapper-level freeze applied to the target per ยง5.2's flow blocks both spending and receiving, so neither $C_{\text{spend}}$ nor $C_{\text{receive}}$ can change between proof construction and submission, and the proof's bindings hold across the isolate-then-settle handshake. + +**What is no longer needed.** The earlier sketch of an on-chain receiving-side accumulator and a per-transfer compliance hook on `confidential_transfer`, `confidential_transfer_from`, and `deposit` is not required. The recipient-auditor's opening of $C_{\text{receive}}$ is reconstructed entirely off-chain from event scans (`DESIGN.md` ยง8.1). + +Detailed encoding, the precise treatment of $r_s$, and the two-phase isolate-then-settle entry-point sequencing are deferred to a follow-up revision of this document. + +--- + +## 6. Interface Summary + +```rust +impl Wrapper { + fn __constructor(e: Env, /* core args */, compliance: Option); + + // Freeze (ยง2) + fn freeze(e: Env, account: Address, admin: Address); + fn unfreeze(e: Env, account: Address, admin: Address); + fn is_frozen(e: Env, account: Address) -> bool; + + // Config rotation (admin auth per ยง1.1, reverts when compliance.is_none()) + // Replaces the entire ComplianceConfig in one call. + fn set_compliance_config(e: Env, config: ComplianceConfig, admin: Address); + + // Reads + fn compliance_config(e: Env) -> Option; +} +``` + +`set_compliance_config` overwrites all three fields atomically. Callers that want to toggle a single field read the current config, modify the relevant field, and pass the updated struct back. This keeps the admin-gated surface to one entry point and avoids per-field rotation helpers. + +### 6.1 Events + +| Event | Fields | +|:---|:---| +| `Frozen`, `Unfrozen` | `account` | +| `ComplianceConfigChanged` | `policy`, `sac_passthrough`, `permit_unregistered_deposit` | + +Clawback-related events are specified alongside the clawback flow in the follow-up revision. diff --git a/packages/tokens/src/confidential/docs/DESIGN.github.md b/packages/tokens/src/confidential/docs/DESIGN.github.md new file mode 100644 index 000000000..90eb3ee21 --- /dev/null +++ b/packages/tokens/src/confidential/docs/DESIGN.github.md @@ -0,0 +1,1313 @@ +# Confidential Token Wrapper + +## Abstract + +We present a confidential token wrapper for Soroban that adds private balances and transfers to any SEP-41 token. Balances are stored as unchunked Pedersen commitments as single elliptic curve points, and updated homomorphically by the contract without decryption. Zero-knowledge proofs (Noir/UltraHonk) accompany each spending operation to prove correctness without revealing amounts. Transfer recipients and auditors recover amounts and blinding factors via per-transfer ephemeral ECDH key agreement over Grumpkin. A dual-balance model (spendable/receiving) prevents griefing: incoming transfers accumulate in a receiving commitment that third parties cannot use to invalidate in-flight spend proofs. A dual-auditor model provides per-account audit visibility: each transfer produces ciphertexts under two auditor keys, giving the recipient's auditor the transfer amount and the sender's auditor the transfer amount plus the sender's post-transfer balance (or post-transfer allowance for operator transfers), enabling real-time auditing. Account owners can delegate spending to time-limited operators via escrowed allowances with derived delegation viewing keys. The system uses 6 Noir circuits, and works seamlessly with Soroban's BN254 host functions (leveraging the recently added CAP-80), and requires approximately 288 bytes of on-chain storage per account. + +--- + +## Project Documents + +This project is composed of the following documents: + +- Confidential Token Wrapper (this document) +- Confidential Token Wrapper: [Compliance Extensions](./COMPLIANCE.md) +- Confidential Token Wrapper: User Flows Overview (to be added) +- Indexing and Off-Chain State Recovery (to be added) +- SDK (to be added) + +--- + +## 1. Introduction + +### 1.1 Background + +Confidential transfers on blockchain require balances and amounts to be hidden from public observers while remaining verifiable by the contract. The standard approach uses additively homomorphic encryption - the contract operates on ciphertexts (adding deposits, subtracting transfers) without learning the underlying values, and zero-knowledge proofs guarantee that operations are valid (sufficient funds, consistent encryption, non-negative balances). + +This document defines a standalone Soroban contract that wraps any SEP-41 token to provide confidential balances and transfers. It is not an extension to the fungible token standard; it is a separate contract that holds tokens on behalf of users and manages encrypted state independently. The wrapper path is chosen over native token integration for three reasons: it works with existing assets, it can evolve independently of the token standard, and it keeps confidentiality complexity separate from the token layer. + +### 1.2 Design Goals + +**Amount and balance confidentiality.** An observer can see that account $A$ transferred to account $B$, and how much each party deposited or withdrew, but not how much moved between them or what their balances are. The system provides confidentiality, not anonymity - sender and recipient addresses remain visible on-chain. + +**Griefing resistance.** A third party must not be able to prevent an account owner from spending by spamming transfers. The balance model must isolate incoming funds from the state that spend proofs reference. + +**No mandatory maintenance operations.** Receiving funds should not require the owner to perform a costly ZK proof before those funds become accessible. The merge operation that makes received funds spendable must be lightweight and non-frontrunnable. + +**Selective auditing.** Each account selects an auditor at registration. Each transfer produces ciphertexts under two auditor keys: the recipient's auditor receives the transfer amount, while the sender's auditor receives the transfer amount and sender's post-transfer balance. This dual-auditor model enables real-time visibility for both parties' auditors without granting access to uninvolved accounts' historical balances. + +**Delegated spending.** Account owners can authorize operators (separate addresses) to spend from escrowed allowances, enabling use cases like automated market makers and custodial services without sharing the owner's secret key. + +### 1.3 Approach + +The design is built on three interlocking mechanisms: + +1. **Pedersen commitments.** Each balance is a single elliptic curve point $C = v \cdot G + r \cdot H$. There is no chunking, no discrete logarithm to solve for decryption, and no overflow from repeated homomorphic additions. The owner maintains the commitment opening $(v, r)$ as local wallet state, updated incrementally from on-chain events. + +2. **ECDH-derived blinding.** When a sender transfers to a recipient, the blinding factor of the transfer commitment is derived from an ephemeral Diffie-Hellman key exchange with the recipient's public viewing key. The circuit enforces correct derivation, ensuring the recipient can always compute the blinding. The same ephemeral scalar is reused for an ECDH exchange with auditors' public key. + +3. **Proof-less merge.** Incoming funds accumulate in a receiving balance that is separate from the spendable balance. To make received funds spendable, the owner authorizes a merge - no ZK proof is required. Since merge requires owner authorization and incoming transfers touch only the receiving balance, neither the spend path nor the merge path can be front-run by a third party. + +Six Noir/UltraHonk circuits cover registration, withdrawal, confidential transfer, operator transfer, operator delegation, and operator revocation. The proof system leverages the Grumpkinโ€“BN254 curve cycle: Grumpkin point arithmetic is native inside Noir circuits (no field emulation), while Soroban natively supports BN254 operations for UltraHonk proof verification. + +--- + +## 2. Preliminaries + +### 2.1 Notation + +| Symbol | Definition | +|:---|:---| +| $\mathbb{G}$ | Grumpkin elliptic curve group (prime order) | +| $\mathbb{F}_r$ | BN254 scalar field $= \mathbb{G}$'s base field | +| $\mathbb{F}_q$ | BN254 base field $= \mathbb{G}$'s scalar field | +| $G, H \in \mathbb{G}$ | Independent generators with no known discrete log relation | +| $\mathcal{O}$ | Identity element (point at infinity), encoded as $(0, 0)$ on-chain | +| $P.x$ | The $x$-coordinate of point $P$, an element of $\mathbb{F}_r$ | +| $\text{Poseidon}(\cdot)$ | Poseidon2 hash function over $\mathbb{F}_r$ (Section 2.5) | +| $\delta_{\ast}$ | Domain separation constants (subscript identifies the domain) | +| $[n]$ | The set $\{0, 1, \ldots, n-1\}$ | + +### 2.2 Grumpkinโ€“BN254 Cycle + +Grumpkin is defined by $y^2 = x^3 - 17$ over $\mathbb{F}_r$. It forms a 2-cycle with BN254: + +$$\text{base}(\mathbb{G}) = \mathbb{F}_r^{\text{BN254}}, \qquad \text{scalar}(\mathbb{G}) = \mathbb{F}_q^{\text{BN254}}$$ + +A Grumpkin point is a pair $(x, y) \in \mathbb{F}_r^2$. Noir's native `Field` type is $\mathbb{F}_r$, so Grumpkin point arithmetic inside UltraHonk circuits incurs no non-native field emulation. On-chain, the Soroban host provides BN254 $\mathbb{F}_r$ arithmetic (`bn254_fr_{add, sub, mul, inv}` via CAP-80), which suffices for Grumpkin affine point operations. + +**Scalar sampling.** Grumpkin scalars live in $\mathbb{F}_q$, which is slightly larger than $\mathbb{F}_r$. All secret scalars in this design ($sk$, $r_e$, $\sigma$, $\sigma_a$) are sampled by the **rejection sampling** procedure, which produces a uniform draw from $\mathbb{F}_r$: + +1. Draw 32 bytes (256 bits) from a CSPRNG. +2. Mask the top 2 bits to zero, yielding a 254-bit candidate $x \in [0, 2^{254})$. +3. If $x \geq r$, reject and return to step 1. +4. If the call site requires $x \neq 0$ and $x = 0$, reject and return to step 1. +5. Output $x$ in its canonical form -- as a Noir `Field` for in-circuit use, or as 32 big-endian bytes (`BytesN<32>`) for storage and event emission. Per ยง10.8 / ยง11 this canonical encoding is what the Soroban host's `bn254_fr_*` deserialization accepts; non-canonical values (i.e. $x \geq r$) are rejected at the host boundary. + +### 2.3 Pedersen Commitments + +A Pedersen commitment to a value $v$ with a blinding factor $r$, viewed as scalars in Grumpkin's scalar field $\mathbb{F}_q$, is: + +$$\text{Com}(v, r) = v \cdot G + r \cdot H$$ + +In this design both $v$ and $r$ are drawn from $\mathbb{F}_r \subset \mathbb{F}_q$ (ยง2.2): $v$ is a non-negative integer below $2^{127}$ (ยง2.6) and $r$ is a Poseidon2 output or an $\mathbb{F}_r$-sampled CSPRNG draw; the group law operates in $\mathbb{F}_q$. + +**Binding.** Finding $(v', r') \neq (v, r)$ such that $\text{Com}(v, r) = \text{Com}(v', r')$ requires computing $\log_G H$, which is infeasible under the discrete logarithm assumption. + +**Hiding.** For any $v$, the commitment $\text{Com}(v, r)$ with uniformly random $r \in \mathbb{F}_q$ is uniformly distributed over $\mathbb{G}$, revealing nothing about $v$. Sampling $r$ from $\mathbb{F}_r \subset \mathbb{F}_q$ instead of full $\mathbb{F}_q$ (ยง2.2) makes the commitment distribution **statistically close** to uniform over $\mathbb{G}$, with total-variation distance bounded by $(|\mathbb{F}_q| - |\mathbb{F}_r|)/|\mathbb{F}_q| \approx 2^{-127}$. + +**Homomorphism.** $\text{Com}(v_1, r_1) + \text{Com}(v_2, r_2) = \text{Com}(v_1 + v_2, r_1 + r_2)$. Scalar addition in the commitment relation is over $\mathbb{F}_q^{\text{BN254}}$ -- the scalar field of $\mathbb{G}$, equivalently the order of the Grumpkin group. Since every committed value is bounded by $2^{127}$ (ยง2.6) and the number of additions across the lifetime of any one commitment is far below $2^{127}$, the value component never wraps in $\mathbb{F}_q$ and the homomorphic relation holds in $\mathbb{Z}$ for values. The blinding component is added in $\mathbb{F}_q$ and may reduce mod $q$ on accumulation; the only place this has operational consequences is the wallet's post-merge spend witness, where the canonical $\mathbb{F}_q$ representative of $r_s + r_r$ can land in $[r, q)$ with probability bounded at $(q-r)/q \approx 2^{-127}$ per merge (see ยง10.4 *Post-merge witness availability*). + +**Generators.** $G$ and $H$ are inherited from Barretenberg's standard Grumpkin Pedersen instantiation (the same generators that the toolchain's `pedersen_commitment` and `pedersen_hash` primitives use). Their provenance is part of the toolchain's audited surface, so the wrapper inherits both the generators and the soundness assumption that $\log_G H$ is unknown. The Noir circuits import them as `embedded_curve_ops::generator()`. + +### 2.4 Elliptic Curve Diffie-Hellman + +Given a long-term keypair $(a, A = a \cdot H)$ and an ephemeral keypair $(r_e, R_e = r_e \cdot H)$, the ECDH shared secret is: + +$$S = r_e \cdot A = a \cdot R_e = a \cdot r_e \cdot H \in \mathbb{G}$$ + +Both parties compute $S$ independently. We extract the scalar $s = S.x \in \mathbb{F}_r$ for use as a Poseidon input. + +### 2.5 Poseidon2 Hash + +The system uses **Poseidon2**, the algebraic hash function native to Noir's standard library and implemented as a custom gate in Barretenberg. For the complete parameter specification of the Noir/Barretenberg instantiation, see: + +- [Poseidon2 paper](https://eprint.iacr.org/2023/323) (Grassi, Khovratovich, Schofnegger, AFRICACRYPT 2023) - parameter derivation and security analysis +- [Barretenberg `poseidon2_params.hpp`](https://github.com/AztecProtocol/aztec-packages/blob/next/barretenberg/cpp/src/barretenberg/crypto/poseidon2/poseidon2_params.hpp) - concrete round constants and matrix entries +- [HorizenLabs reference implementation](https://github.com/HorizenLabs/poseidon2) - parameter generation script (`poseidon2_rust_params.sage`) +- [Noir stdlib `hash/mod.nr`](https://github.com/noir-lang/noir/blob/master/noir_stdlib/src/hash/mod.nr) - sponge construction wrapping the `Poseidon2Permutation` ACIR opcode + +**Usage in this system:** + +- Key derivation: $vk = \text{Poseidon2}(\delta_{\text{vk}}, sk, \text{wrap})$ +- Randomness derivation: $r = \text{Poseidon2}(\delta_{\text{spend\\\_r}}, vk, \sigma)$ +- Symmetric encryption: $\tilde{v} = v + \text{Poseidon2}(\delta_{\text{tx\\\_amount}}, s, \sigma)$ +- Domain separation: each invocation includes a leading constant $\delta$ to prevent cross-context collisions + +**Sponge mode for auditor channels.** The per-transfer auditor ciphertexts (Section 8) use Poseidon2 in sponge mode. A single absorb of $(\delta_{\text{channel}}, S.x, \sigma)$ is followed by $n$ sequential squeezes producing $(m_1, \ldots, m_n) \in \mathbb{F}_r^n$, denoted $\text{SpongeSqueeze}_n(\delta_{\text{channel}}, S.x, \sigma)$. Two channel tags are used: $\delta_{\text{aud\\\_s}}$ for the sender-auditor channel keyed by $S_{a,s}.x = (r_e \cdot K_{\text{aud,s}}).x$, and $\delta_{\text{aud\\\_r}}$ for the recipient-auditor channel keyed by $S_{a,r}.x = (r_e \cdot K_{\text{aud,r}}).x$. + +Squeeze order is canonical. Where $n = 2$, the first squeezed mask is the amount mask and the second is the balance or randomness mask, fixed per operation by the formulas in Sections 7 and 8. + +All references to "Poseidon" in this document denote this Poseidon2 instantiation. + +### 2.6 Integer Embedding and Range Proofs + +**The problem.** Noir circuits operate over $\mathbb{F}_r$, where every element is a non-negative integer modulo $r \approx 2^{254}$. The statement "$v \geq 0$" is vacuously true for all $v \in \mathbb{F}_r$, and "$v_A \geq v_{\text{tx}}$" is undefined without specifying how integers are embedded in the field. Without explicit range constraints, a prover can claim a balance of 1 and transfer 1,000,000: the "new balance" $1 - 1{,}000{,}000 \equiv r - 999{,}999 \pmod{r}$ is a valid field element, and the commitment equation holds. The attacker has minted 999,999 tokens. + +**Integer embedding.** We define a canonical embedding $\iota: [0, 2^{127}) \to \mathbb{F}_r$ mapping non-negative integers to their natural field representatives. A field element $x \in \mathbb{F}_r$ represents a valid balance or transfer amount if and only if $x < 2^{127}$. + +**Range proof mechanism.** A range proof for $x \in [0, 2^{127})$ is implemented by decomposing $x$ into 127 bits inside the circuit and checking the recomposition: + +$$x = \sum_{i=0}^{126} b_i \cdot 2^i, \qquad b_i \in \{0, 1\} \;\forall\, i$$ + +Each $b_i$ is constrained to be Boolean ($b_i \cdot (b_i - 1) = 0$) and the recomposition is checked against $x$. Noir's standard library exposes this directly: + +```noir +// Range check: [0, 2^127) +value.assert_max_bit_size::<127>(); +``` + +**Sufficiency argument.** If the prover supplies $v_A$ and $v_{\text{tx}}$ such that the circuit verifies: + +1. $v_A \in [0, 2^{127})$ (the opening of $C_{\text{spend}}$) +2. $v_{\text{tx}} \in [0, 2^{127})$ (the transfer amount) +3. $v_A - v_{\text{tx}} \in [0, 2^{127})$ (the new balance) + +then $v_A - v_{\text{tx}}$ is a non-negative integer less than $2^{127}$, which is only possible if the integer subtraction did not underflow. This is because $v_A < 2^{127}$ and $v_{\text{tx}} < 2^{127}$, so if $v_A < v_{\text{tx}}$ as integers, then $v_A - v_{\text{tx}} \pmod{r}$ would be $r - (v_{\text{tx}} - v_A)$, which is at least $r - 2^{127} \gg 2^{127}$, failing constraint (3). + +**Value capacity.** Both balances and transfer amounts are constrained to $[0, 2^{127})$. These bounds are enforced in every circuit that manipulates values. The bound is exactly the SEP-41 non-negative `i128` range, so the wrapper's value domain matches the underlying token's domain by construction. The gap between $2^{127}$ and $|\mathbb{F}_r| \approx 2^{254}$ ensures that modular wrap-around is detectable by the range check. + +**Receiving balance (unproven accumulation).** The receiving balance $C_{\text{receive}}$ is updated by contract-side point addition without any proof from the recipient. Therefore, the receiving balance's committed value $v_r$ is never directly range-checked by any circuit. + +This is safe because $v_r$ is *indirectly* bounded: + +1. Each deposit adds a public `i128` amount validated by the contract ($\ge 0$, hence $< 2^{127}$). +2. Each incoming transfer adds a commitment whose sender circuit proved $v_{\text{tx}} \in [0, 2^{127})$ (constraint T4 / O4). +3. All tokens in the wrapper entered through deposits, so the sum of all committed values is bounded by the underlying token's total supply ($< 2^{127}$). No single account can receive more than the total supply. +4. For the field-arithmetic concern (could $v_r$ reach $r$ and wrap around), that would require $r / 2^{127} > 2^{127}$ incoming transfers, which is computationally infeasible. + +When the owner spends after a merge, the spend proof constrains the full post-merge opening: $v_s + v_r \in [0, 2^{127})$ (via constraint W4 or T4 on the spendable balance). This provides an implicit range check at the next spend boundary. + +### 2.7 Address-to-Field Encoding + +In Soroban, the SDK's `Address` host type covers exactly the two `ScAddressType` variants the wrapper interacts with as actors: `Account` (Stellar ed25519 account) and `Contract` (Soroban contract instance). The protocol encodes those addresses via their **canonical Stellar strkey** (SEP-23) representation: + +$$\text{enc}(a) \;=\; \text{Address::to\\\_string}(a)\text{.to\\\_bytes}() \;\in\; \{\text{ASCII}\}^{56}$$ + +This is the 56-character ASCII strkey produced by the host's `address_to_strkey` function: a 1-byte version tag (`G` = `0x47` for `Account`, `C` = `0x43` for `Contract`), a 32-byte payload (ed25519 public key or contract hash), and a 2-byte CRC16 checksum, all base32-encoded into 56 ASCII characters. The byte string is fixed-length, canonical, and reproducible in every Stellar SDK via the language's stellar-strkey library; the protocol commits to these 56 ASCII bytes. + +The Poseidon-compressed Field encoding splits the 56-byte string into two 28-byte limbs (each $\le 2^{224} \ll r \approx 2^{254}$, hence trivially in $\mathbb{F}_r$): + +$$\text{address\\\_to\\\_field}(a) \;=\; \text{Poseidon2}\big(\delta_{\text{addr}}, \;\text{lo}(a), \;\text{hi}(a)\big)$$ + +where $\text{lo}(a) = \sum_{i=0}^{27} 256^{\,i} \cdot \text{enc}(a)[i]$ and $\text{hi}(a) = \sum_{i=0}^{27} 256^{\,i} \cdot \text{enc}(a)[28 + i]$ interpret the lower and upper 28 bytes of the strkey in little-endian byte order. + +The wrapper, the SDK, the wallet, and any indexer reproduce the same Field value from the same Address by running their language's stellar-strkey encoder over the same `(version, payload)` pair and applying the same limb decomposition. No implementation needs to handle `ScAddress` XDR or the inner `AccountID` / `ContractID` union nesting. + +**Usage sites.** + +| Site | When computed | Storage | +|:---|:---|:---| +| $\text{wrap}$ | Once, by the wrapper's `__constructor` over `env.current_contract_address()` | Stored as a single Field in the wrapper's **instance storage** (ยง3.5); read on every proof verification | +| $\text{op}_i$ | Per-call, by the wrapper at `set_operator` and `revoke_operator` over the `operator` argument | Not stored; recomputed each call. The circuit binds it via S5 / V3 | + +--- + +## 3. System Model + +### 3.1 Components + +The system comprises three contracts deployed on Soroban: + +**Wrapper contract.** Holds SEP-41 token balances, manages encrypted account state, and delegates proof verification via cross-contract calls. Performs Grumpkin point arithmetic through $\mathbb{F}_r$ host operations for homomorphic balance updates. + +**Verifier contract.** A modified [UltraHonk verifier](https://github.com/indextree/ultrahonk_soroban_contract) storing one verification key per circuit type. Accepts a circuit identifier, serialized public inputs, and a proof blob; returns success or failure. + +**Auditor contract.** Manages auditor encryption keys independently of the wrapper. One auditor contract serves multiple token wrappers. Stores Grumpkin public keys as full affine points $(x, y)$ indexed by `auditor_id`. The contract validates that stored keys are non-identity curve points; a zero or identity key would make ECDH-derived ciphertexts trivially decryptable (since $\sigma$ is public). The wrapper fetches the active auditor key at operation time and passes it as a public input to the relevant circuit. + +### 3.2 Threat Model + +- The contract execution environment is trusted for correctness but not for privacy: all on-chain state and invocation inputs are public. +- Proof verification is sound: a valid proof guarantees the proven statement holds. This depends on the UltraHonk knowledge soundness assumption *and* the integrity of the Structured Reference String (Section 10.6). +- The discrete logarithm problem on Grumpkin is hard. +- Poseidon2 (Section 2.5) is a pseudorandom function (PRF) and is preimage-resistant over $\mathbb{F}_r$ at the parameterized round count ($R_F = 8$, $R_P = 56$, 128-bit security target). +- Third parties may submit arbitrary transactions, including spam transfers to any registered account. + +### 3.3 Trust Assumptions + +The wrapper, verifier, and auditor contracts are trusted code. Users trust that the verification keys embedded in the verifier correspond to the correct circuits and were derived from a honestly generated Structured Reference String (Section 10.6). The auditor is trusted to protect its decryption key and exercise access only upon legitimate regulatory request. + +### 3.4 Underlying Token Assumptions + +The wrapper holds units of an underlying SEP-41 token on behalf of its users. The confidential accounting invariant (Section 9.3) implicitly assumes: + +$$\sum_i v_{\text{committed},i} \;\le\; \text{token.balance}(\text{wrapper})$$ + +i.e., the total committed value across all confidential accounts never exceeds the public token balance held by the wrapper. The deployer's choice of underlying token determines whether that invariant is actually preserved over time. The wrapper itself does not, and cannot, defend against every misbehavior of the wrapped asset. + +**Required properties of the underlying token.** + +- *Non-rebasing.* The token's balance attributed to the wrapper address changes only as a result of explicit operations that the wrapper itself originated. Tokens whose balances change as a function of supply, oracle data, or external triggers break the accounting invariant and are unsupported. +- *No fee-on-transfer.* `token.transfer(from, to, amount)` MUST move exactly `amount` units. A fee deducted in transit would leave the wrapper's confidential accounting larger than its public backing. +- *Deterministic revert.* A failed `token.transfer` MUST cause the enclosing wrapper invocation (`deposit` or `withdraw`) to revert atomically, so confidential state is never updated against a token transfer that did not happen. +- *Underlying clawback / freeze / deauthorization.* If the underlying SEP-41 (especially a Stellar Asset Contract) supports issuer-level clawback or freeze that can reduce or block the wrapper's holdings, confidential accounting at the wrapper layer may temporarily or permanently exceed the wrapper's accessible backing. This is an operational risk borne by the deployer's choice of underlying token. The wrapper layer offers its own freeze and per-account clawback flows that operate inside the confidential surface; see [COMPLIANCE.md](./COMPLIANCE.md) ยง2 (wrapper-level freeze) and ยง5 (admin + auditor clawback). [COMPLIANCE.md](./COMPLIANCE.md) ยง2.2 additionally specifies SAC authorization passthrough, which composes the wrapper's freeze with the issuer's freeze without requiring the admin to mirror state. + +**Non-negativity check.** The wrapper's public interface uses `i128` end-to-end, matching SEP-41. Every entrypoint that accepts a public amount (`deposit`, `withdraw`) MUST reject `amount < 0` and revert. The in-circuit range constraint (Section 2.6) bounds the same value at $2^{127}$ from above; together they pin the wrapper's value domain to $[0, 2^{127}) = [0, \text{i128::MAX}]$, matching SEP-41 exactly. No conversion at the SEP-41 boundary is needed. + +### 3.5 Governance and Upgradeability + +The constructor binds the wrapper to fixed `admin`, `token`, `verifier`, and `auditor` addresses. It additionally computes and stores $\text{wrap} = \text{address\\\_to\\\_field}(\text{env.current\\\_contract\\\_address}())$ (ยง2.7) in **instance storage** as a single canonical $\mathbb{F}_r$ Field; this is the value every owner-initiated proof references via constraints R2 / W2 / T2 / S2 / V2. The compressed `wrap` Field is computed once at construction (not recomputed per call) to ensure all proofs across the wrapper's lifetime bind to the same Field representative of the wrapper's address. Beyond that, this specification does not prescribe a governance policy for upgrading these components or for rotating per-circuit verification keys. Concrete deployments differ widely in operator structure, regulatory posture, and emergency-response requirements, so these decisions are deliberately left to implementers. + +Questions an implementer must answer: + +- May `admin` replace the `verifier` contract, or the `auditor` contract after deployment? +- Are per-circuit verification keys immutable for the lifetime of the deployment, or may they be updated? +- If any of the above is upgradeable, what authorization (single key, multisig), timelock, and event-emission rules apply? +- How do users independently reproduce a deployed VK from circuit source, toolchain, and SRS (Section 10.6)? + +**Recommendation.** The strongest soundness posture is full immutability: `token`, `verifier`, `auditor`, and per-circuit verification keys all fixed at deployment, with any circuit or verifier change requiring a fresh deployment and an explicit user-side migration. Where operational realities make full immutability impractical (for example, a discovered soundness bug in a circuit or verifier that needs a fast fix), implementers may expose admin-guarded upgrade entrypoints for the `verifier` address or per-circuit VKs. In that case the upgrade path should be gated. + +--- + +## 4. Key Hierarchy + +All keys derive from a single spending secret $sk \in \mathbb{F}_r$. + +### 4.1 Spending Key + +$$Y = sk \cdot H$$ + +The spending public key is stored on-chain at registration. Knowledge of $sk$ is required to authorize transfers, withdrawals, operator delegations, and merges. + +### 4.2 Viewing Key + +$$vk = \text{Poseidon}(\delta_{\text{vk}}, sk, \text{wrap})$$ + +A scalar in $\mathbb{F}_r$, unique per $(sk, \text{wrap})$ pair. Enables balance decryption without spending authority. Cannot recover $sk$ (Poseidon preimage resistance). Because $\text{wrap}$ is bound into the derivation, proofs that constrain $vk$ (R2, W2, T2, S2, V2) are inherently bound to the wrapper contract, eliminating the need for explicit per-circuit context binding. + +### 4.3 Public Viewing Key + +$$\text{PVK} = vk \cdot H$$ + +A Grumpkin point stored on-chain at registration. Serves as the recipient's ECDH public key for incoming transfers. The registration proof constrains $\text{PVK} = vk \cdot H$ where $vk = \text{Poseidon}(\delta_{\text{vk}}, sk, \text{wrap})$ and $Y = sk \cdot H$, preventing a user from registering an unrelated $\text{PVK}$. + +### 4.4 Delegation Viewing Key + +For operator $i$ with address $\text{op}_i$, the owner derives: + +$$dvk_i = \text{Poseidon}(\delta_{\text{dvk}}, vk, \text{op}_i)$$ + +Properties: +- $dvk_i$ reveals only this operator's allowance state in this wrapper context ($vk$ is wrapper-specific, Section 4.2). +- $dvk_i$ cannot recover $vk$ (preimage resistance). +- Different $(vk, \text{op}_i)$ tuples yield independent keys. + +--- + +## 5. Commitment Scheme + +The following symbols are used throughout this section: + +| Symbol | Definition | +|:---|:---| +| $C_{\text{spend}}$ | On-chain spendable balance commitment (Pedersen point) | +| $C_{\text{receive}}$ | On-chain receiving balance commitment (Pedersen point) | +| $C_{\text{tx}}$ | Transfer commitment added to recipient's $C_{\text{receive}}$ | +| $\text{Com}(v, r)$ | Pedersen commitment $v \cdot G + r \cdot H$ | +| $v_s, r_s$ | Value and blinding factor of $C_{\text{spend}}$ (off-chain wallet state) | +| $v_r, r_r$ | Value and blinding factor of $C_{\text{receive}}$ (off-chain wallet state) | +| $v_{\text{tx}}$ | Transfer amount (private) | +| $r_{\text{tx}}$ | ECDH-derived blinding factor for $C_{\text{tx}}$ | +| $W_{\text{spend}}, W_{\text{receive}}$ | Wallet-side accumulators: $(v, r)$ pairs tracking commitment openings | +| $r_e$ | Ephemeral scalar sampled per transfer | +| $R_e$ | Ephemeral public key $r_e \cdot H$ (published in event data) | +| $S$ | ECDH shared secret point $r_e \cdot \text{PVK}_B$ | +| $s$ | Scalar extracted from shared secret: $S.x \in \mathbb{F}_r$ | +| $\tilde{v}$ | Encrypted transfer amount: $v_{\text{tx}} + \text{Poseidon}(\delta_{\text{tx\\\_amount}}, s, \sigma)$ | +| $\tilde{b}$ | Encrypted balance scalar: $v_{\text{new}} + \text{Poseidon}(\delta_{\text{enc\\\_bal}}, vk, \sigma)$ | +| $\sigma$ | Prover-chosen random salt, sampled per operation via the rejection sampling procedure of ยง2.2; canonical $\mathbb{F}_r$ representative encoded as `BytesN<32>` | + +### 5.1 Balance Commitments + +Each balance is a single Pedersen commitment $C = \text{Com}(v, r) \in \mathbb{G}$, represented on-chain as an uncompressed affine point $(x, y) \in \mathbb{F}_r^2$ (64 bytes). The identity $\mathcal{O}$ is encoded as $(0, 0)$ and handled as a special case in point arithmetic. + +The committed value $v$ can represent the full range of practical balances (up to $2^{127} - 1$, bounded by the SEP-41 `i128` interface) without discrete logarithm concerns, because the owner maintains the commitment opening off-chain (Section 5.2) and the auditor reads an encrypted scalar (Section 5.5). + +### 5.2 Off-Chain Opening Maintenance {#opening-maintenance} + +A Pedersen commitment $C = \text{Com}(v, r)$ hides its opening $(v, r)$. The owner must know this opening to construct spend proofs so they must maintain $(v, r)$ as local wallet state, updated incrementally as balance-modifying events occur. + +**Definition** (Wallet state). The owner's wallet maintains two running accumulators: + +$$W_{\text{spend}} = (v_s, r_s) \quad \text{such that} \quad C_{\text{spend}} = v_s \cdot G + r_s \cdot H$$ +$$W_{\text{receive}} = (v_r, r_r) \quad \text{such that} \quad C_{\text{receive}} = v_r \cdot G + r_r \cdot H$$ + +**Initialization.** At registration, $C_{\text{spend}} = C_{\text{receive}} = \mathcal{O}$. The wallet sets $W_{\text{spend}} = W_{\text{receive}} = (0, 0)$. + +**Update rules.** Each balance-modifying event updates exactly one accumulator: + +| Event | Accumulator update | +|:---|:---| +| Deposit of public amount $a$ to this account | $W_{\text{receive}} \mathrel{+}= (a, 0)$ | +| Incoming transfer with event $(R_e, \tilde{v}, \sigma)$ | Compute $S = vk \cdot R_e$, $s = S.x$; derive $v_{\text{tx}} = \tilde{v} - \text{Poseidon}(\delta_{\text{tx\\\_amount}}, s, \sigma)$ and $r_{\text{tx}} = \text{Poseidon}(\delta_{\text{tx\\\_blind}}, s, \sigma)$. Then $W_{\text{receive}} \mathrel{+}= (v_{\text{tx}}, r_{\text{tx}})$ | +| Outgoing transfer/withdrawal of amount $a$ | Proof outputs new commitment with deterministic randomness. $W_{\text{spend}} \leftarrow (v_s - a, \; \text{Poseidon}(\delta_{\text{spend\\\_r}}, vk, \sigma))$ | +| Merge | $W_{\text{spend}} \leftarrow (v_s + v_r, \; r_s + r_r)$; $W_{\text{receive}} \leftarrow (0, 0)$ | +| Set operator (escrow amount $a$) | Proof outputs new commitment. $W_{\text{spend}} \leftarrow (v_s - a, \; \text{Poseidon}(\delta_{\text{spend\\\_r}}, vk, \sigma))$ | +| Revoke operator (reclaim amount $a$) | Proof outputs new commitment. $W_{\text{spend}} \leftarrow (v_s + a, \; \text{Poseidon}(\delta_{\text{spend\\\_r}}, vk, \sigma))$ | + +The Merge row uses exact integer addition; $W_{\text{spend}}.r$ is not reduced modulo $r$ or $q$ as merges accumulate. At proof-construction time the wallet reduces $W_{\text{spend}}.r$ modulo $q$ and encodes the canonical $\mathbb{F}_q$ representative as a single $\mathbb{F}_r$ `Field`. This encoding succeeds when the representative lies in $[0, r)$, with probability $\geq 1 - 2^{-127}$ per merge; the complementary case is acknowledged in ยง10.4 *Post-merge witness availability*. + +After every owner-initiated operation that produces a proof, $r_s$ resets to a deterministic value. This is the **normalization** property: the spendable balance's blinding factor is always recoverable from $(vk, \sigma)$ at spend boundaries. Together with $\tilde{b}$, both emitted in the spend-boundary event, each spend boundary forms a **checkpoint** from which the spendable opening $(v_s, r_s)$ is recoverable via a single event lookup, with no exhaustive history replay needed for $W_{\text{spend}}$. Recovering $W_{\text{receive}}$, and folding in any post-checkpoint merges, still requires replaying events emitted after the checkpoint (see Recovery below). + +**Consistency check.** At any time, the wallet can verify its state: $C_{\text{spend}} \stackrel{?}{=} v_s \cdot G + r_s \cdot H$ and $C_{\text{receive}} \stackrel{?}{=} v_r \cdot G + r_r \cdot H$, where $C_{\text{spend}}$ and $C_{\text{receive}}$ are read from on-chain state. + +**Recovery.** If the wallet loses local state, it recovers from the **last checkpoint**: the most recent owner-initiated proof operation (`withdraw`, `confidential_transfer`, `set_operator`, or `revoke_operator`), which emitted both $\tilde{b}$ and $\sigma$ in its event. By construction, only deposits, incoming transfers, and merges can occur after this event; any later owner-initiated proof operation would itself become the new checkpoint. Steps 1-4 recover the spendable balance using $\tilde{b}$, $\sigma$ (both from the event), and $vk$. Event replay (steps 5-6) folds in the bounded post-checkpoint activity: + +1. Fetch $(\tilde{b}, \sigma)$ from the most recent **checkpoint event** for this account, where a checkpoint event is exactly one of `Withdraw`, `Transfer` (where the account is the `from`), `SetOperator`, or `RevokeOperator` -- the four event types that carry a proof-bound $(\tilde{b}, \sigma)$ for the account's spendable balance. `Deposit`, `Transfer` (where the account is the `to`), `OperatorTransfer` (recipient side), and `Merge` are explicitly **not** checkpoints: they either carry no $(\tilde{b}, \sigma)$ at all or carry one that is bound to a different account's spendable balance. **No-checkpoint case:** if the account has no checkpoint event since `Register`, initialize $W_{\text{spend}} \leftarrow (0, 0)$ and skip to step 5 with the replay window starting at the `Register` event. +2. Recover the spendable balance value: $v_s = \tilde{b} - \text{Poseidon}(\delta_{\text{enc\\\_bal}}, vk, \sigma)$. +3. Recover the spendable balance blinding: $r_s = \text{Poseidon}(\delta_{\text{spend\\\_r}}, vk, \sigma)$. +4. Set $W_{\text{spend}} \leftarrow (v_s, r_s)$ and $W_{\text{receive}} \leftarrow (0, 0)$. +5. Replay all events since the checkpoint in ledger order. For each event: + - **Incoming transfer** $(R_e, \tilde{v}, \sigma_{\text{sender}})$: compute $S = vk \cdot R_e$, derive $v_{\text{tx}}$ and $r_{\text{tx}}$. Accumulate $W_{\text{receive}} \mathrel{+}= (v_{\text{tx}}, r_{\text{tx}})$. + - **Deposit** of amount $a$: accumulate $W_{\text{receive}} \mathrel{+}= (a, 0)$. + - **Merge**: fold $W_{\text{spend}} \leftarrow (W_{\text{spend}}.v + W_{\text{receive}}.v, \; W_{\text{spend}}.r + W_{\text{receive}}.r)$, reset $W_{\text{receive}} \leftarrow (0, 0)$. +6. Verify consistency: $C_{\text{spend}} \stackrel{?}{=} W_{\text{spend}}.v \cdot G + W_{\text{spend}}.r \cdot H$ and $C_{\text{receive}} \stackrel{?}{=} W_{\text{receive}}.v \cdot G + W_{\text{receive}}.r \cdot H$. + +Steps 1-3 require $(\tilde{b}, \sigma)$ from the latest owner event and $vk$. No full event replay is needed. Step 5 replays only events since the last checkpoint and correctly handles any number of interleaved deposits, transfers, and merges. A wallet that spends regularly produces frequent checkpoints, bounding the replay window. In the worst case (funds received but never spent), the replay window extends back to registration. + +**Event durability requirement.** Recovery depends on the wallet being able to retrieve every event since the last checkpoint, plus the checkpoint event itself, in ledger order. Stellar RPC retains event history for a 7-days window only, so a wallet that loses local state after that window cannot recover from RPC alone. The protocol therefore assumes a durable event archive that retains the full per-account history of `Withdraw`, `Transfer` (both directions), `OperatorTransfer` (recipient side), `Deposit`, `Merge`, `SetOperator`, and `RevokeOperator` events forever. The data model, ingestion contract, retention obligations, and recommended API surface for that archive are specified in [INDEXER.md](INDEXER.md). Wallets and SDKs MUST consume an indexer that meets that contract for recovery. + +### 5.3 ECDH-Derived Blinding + +When a sender (spending key $sk_A$) transfers to a recipient with public viewing key $\text{PVK}_B$, the transfer commitment uses blinding derived from an ephemeral ECDH exchange. + +**Definition 1** (Transfer blinding derivation). The sender samples $r_e, \sigma \in \mathbb{F}_r$ via the rejection sampling procedure (ยง2.2), then computes: + +$$R_e = r_e \cdot H$$ +$$S = r_e \cdot \text{PVK}_B$$ +$$s = S.x \in \mathbb{F}_r$$ +$$r_{\text{tx}} = \text{Poseidon}(\delta_{\text{tx\\\_blind}}, s, \sigma)$$ +$$\tilde{v} = v_{\text{tx}} + \text{Poseidon}(\delta_{\text{tx\\\_amount}}, s, \sigma)$$ + +where $v_{\text{tx}}$ is the transfer amount. The transfer commitment is $C_{\text{tx}} = \text{Com}(v_{\text{tx}}, r_{\text{tx}})$. The ephemeral public key $R_e$, encrypted amount $\tilde{v}$, and $\sigma$ are published in the transaction event data so recipients can derive both $v_{\text{tx}}$ and $r_{\text{tx}}$ during replay. + +Since $vk_B \cdot R_e = r_e \cdot \text{PVK}_B = S$ by ECDH commutativity, both sender and recipient can independently derive $r_{\text{tx}}$ and decrypt $v_{\text{tx}} = \tilde{v} - \text{Poseidon}(\delta_{\text{tx\\\_amount}}, s, \sigma)$, provided they know $\sigma$ emitted with the event. The auditor decrypts the transfer amount via a separate ECDH channel (Section 8.1). + +**Note.** Each transfer involves two auditor ECDH exchanges: one with the recipient's auditor key ($S_{a,r} = r_e \cdot K_{\text{aud,r}}$) and one with the sender's auditor key ($S_{a,s} = r_e \cdot K_{\text{aud,s}}$). Both reuse the ephemeral scalar $r_e$, as does the $dvk_i$ escrow ECDH in `set_operator` (ยง7.11) when one is present. Neither auditor recovers any account's viewing key. + +**Why reusing $r_e$ is safe.** Each ECDH channel keyed from the same $r_e$ produces a distinct shared scalar because the counterparty public keys are distinct ($\text{PVK}_B$, $K_{\text{aud,r}}$, $K_{\text{aud,s}}$, $Y_{\text{op}}$ are independent Grumpkin points, none derivable from one another). Each channel further uses a distinct Poseidon domain tag ($\delta_{\text{tx\\\_blind}}/\delta_{\text{tx\\\_amount}}$ for the recipient channel, $\delta_{\text{aud\\\_r}}$ and $\delta_{\text{aud\\\_s}}$ for the two auditor channels, $\delta_{\text{esc\\\_dvk}}$ for the operator escrow), so masks across channels are independent under the PRF assumption on Poseidon (ยง3.2). The channel masks are used as one-time pads against fresh per-transfer randomness ($\sigma$ or $\sigma_a$), and each per-channel sponge re-absorbs that nonce, so a given mask is never reused even for the same counterparty across two operations. Together these three properties (distinct shared scalars, distinct domains, fresh per-operation nonce) close the standard ECDH key-reuse attack surface; the wrapper's enumeration of channels in ยง13 satisfies the domain-distinctness condition. + +### 5.4 Anti-Poisoning Constraint + +The transfer circuit enforces that $C_{\text{tx}}$ was constructed using the ECDH-derived $r_{\text{tx}}$: + +$$C_{\text{tx}} = v_{\text{tx}} \cdot G + r_{\text{tx}} \cdot H \quad \text{where} \quad r_{\text{tx}} = \text{Poseidon}(\delta_{\text{tx\\\_blind}}, s, \sigma)$$ + +This prevents a malicious sender from committing with arbitrary blinding, which would cause the recipient to lose track of their accumulated blinding factor and be unable to spend. + +### 5.5 Encrypted Balance Scalar + +Owner-initiated operations (transfers, withdrawals) produce a new spendable balance commitment with deterministic randomness (Section 7). To enable wallet recovery without full event replay, the proof also outputs an **encrypted balance scalar**: + +$$\tilde{b} = v_{\text{new}} + \text{Poseidon}(\delta_{\text{enc\\\_bal}}, vk, \sigma)$$ + +where $v_{\text{new}}$ is the new spendable balance and $\sigma$ is the prover-chosen random salt. The contract emits $\tilde{b}$ in the operation's event (Section 11.2) rather than storing it on-chain; the contract never reads it after the proof has bound it to $C_{\text{spend}}$. Anyone with $vk$ recovers $v_{\text{new}} = \tilde{b} - \text{Poseidon}(\delta_{\text{enc\\\_bal}}, vk, \sigma)$ from the event. The primary consumer is the owner's wallet for checkpoint recovery (Section 5.2); auditors do not hold $vk$ and instead read balances via per-transfer ECDH ciphertexts (Section 8.1). The circuit enforces consistency between $\tilde{b}$ and the committed value in $C_{\text{spend}}$. + +--- + +## 6. Account State + +### 6.1 Account Data Model + +Each registered account stores a `ConfidentialAccount` in persistent storage, keyed by `Address`: + +```rust +ConfidentialAccount { + spending_key: BytesN<64>, // Y = sk ยท H + viewing_public_key: BytesN<64>, // PVK = vk ยท H + spendable_balance: BytesN<64>, // C_spend: single Pedersen commitment + receiving_balance: BytesN<64>, // C_receive: single Pedersen commitment + auditor_id: u32, +} +``` + +**`spending_key`** + +$Y = sk \cdot H$. Set once at registration. Authorizes all spending operations. + +**`viewing_public_key`** + +$\text{PVK} = vk \cdot H$. Set once at registration. Used by senders for ECDH key agreement. The registration proof enforces derivation from the same $sk$ as $Y$. + +**`spendable_balance`** + +The commitment the owner can spend from. Modified only by owner-authorized operations: transfers out, withdrawals, merge, `set_operator`, `revoke_operator`. Encoded as a single Grumpkin affine point (64 bytes). + + +**`receiving_balance`** + +Accumulates incoming deposits and transfers via homomorphic addition. The contract adds to this without any proof from the recipient. Reset to $\mathcal{O}$ on merge. Encoded as a single Grumpkin affine point (64 bytes). + +**`auditor_id`** + +Index into the auditor contract's key store. Set once at registration. Used by the wrapper to fetch the correct auditor public key when building transfer public inputs. For incoming transfers, the recipient's `auditor_id` determines the key under which the transfer amount is encrypted. For outgoing transfers (and operator transfers), the sender's (or owner's) `auditor_id` determines the key under which the transfer amount and post-transfer balance (or allowance) are encrypted. + +### 6.2 Operator Delegation + +Operator delegations are stored in persistent storage, keyed by `(owner, operator)`: + +```rust +OperatorDelegation { + allowance_commitment: BytesN<64>, // Single Pedersen commitment + encrypted_allowance: BytesN<32>, // Poseidon-encrypted allowance scalar + escrowed_dvk: BytesN<64>, // ECDH escrow of dvk_i under operator key + allowance_salt: BytesN<32>, + expiration_ledger: u32, +} +``` + +**`allowance_commitment`** + +The operator's remaining escrowed allowance, a single Pedersen commitment: $C_a = \text{Com}(v_a, r_a)$ where $r_a = \text{Poseidon}(\delta_{\text{allow\\\_r}}, dvk_i, \sigma_a)$. One Grumpkin point (64 bytes). + +**`encrypted_allowance`** + +Poseidon-encrypted allowance scalar: $\tilde{a} = v_a + \text{Poseidon}(\delta_{\text{enc\\\_allow}}, dvk_i, \sigma_a)$. Enables the operator (who holds $dvk_i$ via `escrowed_dvk`) to read the current allowance without DLP when constructing an `OperatorTransfer` witness. The owner can also read it via $vk \rightarrow dvk_i$. The auditor does not consume this field; allowance visibility for the auditor is provided by the per-event ciphertexts (Section 8.5). + +**`escrowed_dvk`** + +$dvk_i$ encrypted under the operator's spending key via ECDH. (64 bytes) + +**`allowance_salt`** + +Per-delegation salt for allowance randomness derivation, encoded as `BytesN<32>` (canonical $\mathbb{F}_r$ representative). $\sigma_a$ is sampled by the rejection sampling procedure of ยง2.2 (same as $\sigma$) and is the sole freshness input to all allowance Poseidon derivations. Set by the owner at `set_operator` and replaced by the operator on every `confidential_transfer_from` (the operator samples a fresh `new_allowance_salt` and that becomes the stored value alongside the updated `allowance_commitment`). The salt is bound to the current commitment: when the commitment changes, the salt changes with it. It is stored on-chain so the owner can decrypt the allowance at revocation without depending on event history. + +**Dual role.** In operator transfers, $\sigma_a$ also serves as the nonce for the recipient ECDH encryption (O7, O9) and the auditor channel sponges (O\_a2 and O\_a6, which absorb $\sigma_a$ alongside the channel shared scalar). This is safe because ECDH confidentiality derives from the shared secret $S.x$ (or $S_{a,r}.x$, $S_{a,s}.x$), not from $\sigma_a$ being secret. However, this couples the allowance salt to the transfer event: the event must emit $\sigma_a$ so that the recipient and auditor can decrypt. Any change to how the salt is stored or exposed must preserve this invariant. + +**`expiration_ledger`** + +Ledger sequence number after which the delegation is no longer valid. Checked on every `confidential_transfer_from`. The delegation persists in storage until explicitly revoked (if it were in temporary storage automatic cleanup would destroy escrowed funds). + +**Single-slot semantics.** The `(owner, operator)` slot holds at most one delegation. `set_operator` (Section 7.7) reverts if a delegation already exists for that pair, regardless of whether the existing delegation is past `expiration_ledger`. Expiry only prevents the operator from spending; the escrowed value persists on-chain until `revoke_operator` (Section 7.9) folds it back into the owner's spendable balance. Re-delegating to the same operator therefore requires the sequence: `revoke_operator` then `set_operator`. This rule is what keeps the balance-conservation invariant (Section 9.3) ranging cleanly over stored delegations: every delegation is either active, expired-pending-revoke, or absent, and the escrowed value is never silently dropped. + +--- + +## 7. Operations + +### 7.1 Public Input Sources + +UltraHonk verifies the relation between a proof and its public-input vector. The verifier sees only field elements -- it has no knowledge of which account, wrapper, or auditor those values are supposed to describe. Binding each public input to the correct provenance is the wrapper's responsibility. If the wrapper takes a value that should come from trusted state (e.g. the sender's `spending_key`) and instead reads it from caller-controlled invocation inputs, a soundly proven statement can verify for the wrong account. + +Each operation below lists, for every public input, where the wrapper loads it from -- persistent account storage, the delegation entry, the wrapper's own contract address, an auditor-contract lookup, an invocation argument, or a prover-supplied value that the circuit binds. + +**Trust-boundary rule.** Public inputs that derive from trusted state (account storage, delegation storage, the current contract address, or auditor-contract lookups) MUST be loaded by the wrapper itself. The wrapper MUST NOT accept these values from the caller's `data` payload. Only invocation arguments (which are bound under `require_auth()` per ยง11.1) and prover-supplied values (which the circuit binds to its constraints) may originate from the caller. Violating this rule breaks soundness even with a perfectly sound circuit. + +### 7.2 Registration + +An account provides a Grumpkin spending key $Y$, a public viewing key $\text{PVK}$, and a chosen `auditor_id`, accompanied by a proof of key well-formedness. + +**Circuit constraints (Register):** + +| # | Constraint | +|:--|:---| +| R1 | $Y = sk \cdot H$ (spending key well-formed) | +| R2 | $vk = \text{Poseidon}(\delta_{\text{vk}}, sk, \text{wrap})$ (viewing key correctly derived, binds proof to wrapper) | +| R3 | $\text{PVK} = vk \cdot H$ (public viewing key matches $vk$) | +| R4 | $sk \neq 0$ (rules out $Y = \mathcal{O}$) | +| R5 | $vk \neq 0$ (rules out $\text{PVK} = \mathcal{O}$, which would collapse every incoming-transfer ECDH) | + +**Public inputs:** + +| Input | Notes | +|:---|:---| +| $Y$, $\text{PVK}$ | Prover-supplied; written to `account.spending_key` and `account.viewing_public_key` on success | +| $\text{wrap}$ | Loaded from instance storage; set once at construction (ยง3.5) | + +**Private witnesses:** $sk$. + +**Post-verification state:** The contract validates that `auditor_id` exists in the auditor contract and points to a valid key, then stores `spending_key`, `viewing_public_key`, `auditor_id`, and initializes `spendable_balance = receiving_balance = ` $\mathcal{O}$. + +### 7.3 Deposit + +Transparent tokens flow from the depositor to the wrapper via `token.transfer(from, self, amount)`. The amount $a$ is public and typed as `i128`. The wrapper checks $a \ge 0$ at the entrypoint and reverts on violation (Section 3.4). The contract then computes the deposit commitment with zero blinding: + +$$C_{\text{dep}} = a \cdot G + 0 \cdot H = a \cdot G$$ + +and adds it to the recipient's receiving balance: + +$$C_{\text{receive}} \leftarrow C_{\text{receive}} + C_{\text{dep}}$$ + +No proof required. The recipient `to` **must** be registered: the receiving-balance update writes into `to`'s `ConfidentialAccount` slot and the wrapper reverts if no slot exists. The depositor `from` does **not** need a registered confidential account; only the SEP-41 `token.transfer(from, self, a)` authorization is required. The recipient's off-chain state updates: $v_{\text{receive}} \mathrel{+}= a$, $r_{\text{receive}} \mathrel{+}= 0$. + +### 7.4 Merge + +The owner folds the receiving balance into the spendable balance. + +**Contract logic (no proof):** + +``` +require account.require_auth() +C_spend โ† C_spend + C_receive +C_receive โ† O +``` + +**Proposition 1** (Merge correctness). If $C_{\text{spend}} = \text{Com}(v_s, r_s)$ and $C_{\text{receive}} = \text{Com}(v_r, r_r)$ before merge, then after merge $C_{\text{spend}} = \text{Com}(v_s + v_r, r_s + r_r)$ and $C_{\text{receive}} = \mathcal{O} = \text{Com}(0, 0)$. + +*Proof.* By the homomorphic property of Pedersen commitments: +$$C_{\text{spend}} + C_{\text{receive}} = (v_s \cdot G + r_s \cdot H) + (v_r \cdot G + r_r \cdot H) = (v_s + v_r) \cdot G + (r_s + r_r) \cdot H = \text{Com}(v_s + v_r, r_s + r_r)$$ +No value is created or destroyed. $\square$ + +**Owner state update.** The owner knows the opening of the post-merge commitment: $v_{\text{spend}}' = v_s + v_r$, $r_{\text{spend}}' = r_s + r_r$. The owner knows $v_r$ and $r_r$ from processing incoming transfer and deposit events into $W_{\text{receive}}$ (Section 5.2, *Update rules*; the per-transfer derivation is Definition 1 in Section 5.3). The values $v_s$ and $r_s$ are known from the owner's last proof output. + +**Griefing analysis.** Merge requires `account.require_auth()`. No third party can invoke it. Incoming transfers that arrive between proof construction and submission modify only $C_{\text{receive}}$, which is not referenced by spend proofs. Therefore merge is not front-runnable and incoming transfers cannot invalidate spend proofs (Proposition 2, Section 9.1). + +**Encrypted balance.** Merge emits no $\tilde{b}$ (there is no proof to enforce consistency between $\tilde{b}$ and the post-merge $C_{\text{spend}}$). The next owner-initiated proof operation issues a fresh checkpoint. The auditor tracks incoming amounts independently from transfer events. + +### 7.5 Withdrawal + +The owner withdraws a public amount $a$ (typed `i128`) from their spendable balance. The W4 range constraint bounds $a$ at $2^{127}$ in-circuit; the wrapper additionally checks $a \ge 0$ at the entrypoint (Section 3.4). + +**Circuit constraints (Withdraw):** + +| # | Constraint | +|:--|:---| +| W1 | $Y = sk \cdot H$ (owner key ownership) | +| W2 | $vk = \text{Poseidon}(\delta_{\text{vk}}, sk, \text{wrap})$ (binds proof to wrapper) | +| W3 | The prover knows the opening $(v, r)$ of $C_{\text{spend}}$: $C_{\text{spend}} = v \cdot G + r \cdot H$ | +| W4 | $v \in [0, 2^{127})$, $a \in [0, 2^{127})$, $v - a \in [0, 2^{127})$ (range validity, Section 2.6) | +| W5 | $r' = \text{Poseidon}(\delta_{\text{spend\\\_r}}, vk, \sigma)$ (deterministic randomness for new balance) | +| W6 | $C_{\text{spend}}' = (v - a) \cdot G + r' \cdot H$ (new spendable commitment) | +| W7 | $\tilde{b} = (v - a) + \text{Poseidon}(\delta_{\text{enc\\\_bal}}, vk, \sigma)$ (encrypted balance scalar) | +| W8 | $r_e \neq 0$ (rules out $R_e = \mathcal{O}$ and $S_{a,s} = \mathcal{O}$, which would reduce $m_b$ to a constant function of $\sigma$) | +| W\_a1 | $R_e = r_e \cdot H$ (ephemeral key for auditor ECDH) | +| W\_a2 | $S_{a,s} = r_e \cdot K_{\text{aud,s}}$ (sender-auditor ECDH shared secret) | +| W\_a3 | $m_b = \text{SpongeSqueeze}_1(\delta_{\text{aud\\\_s}}, S_{a,s}.x, \sigma)$ (sender-auditor channel sponge, single squeeze) | +| W\_a4 | $\tilde{b}_{\text{aud,s}} = (v - a) + m_b$ (sender-auditor encrypted balance checkpoint) | + +**Public inputs (15 fields):** + +| Input | Notes | +|:---|:---| +| $C_{\text{spend}}$ | Loaded from `from.spendable_balance` | +| $Y$ | Loaded from `from.spending_key` | +| $\text{wrap}$ | Loaded from instance storage; set once at construction (ยง3.5) | +| $K_{\text{aud,s}}$ | Fetched from the auditor contract using `from.auditor_id` | +| $a$ | Public withdrawal amount from invocation inputs | +| $C_{\text{spend}}'$, $\sigma$, $\tilde{b}$, $R_e$, $\tilde{b}_{\text{aud,s}}$ | Prover-supplied; $C_{\text{spend}}'$ written to `from.spendable_balance`, the rest emitted in event | + +$\text{to}$ is bound under `from.require_auth()` and does not appear in the proof. + +**Private witnesses:** $sk$, $vk$, $v$, $r$, $r_e$. + +**Post-verification:** The contract verifies the proof, sets `from`.`spendable_balance` $= C_{\text{spend}}'$, and calls `token.transfer(self, to, a)`. Emits event with $(R_e, \sigma, \tilde{b}, \tilde{b}_{\text{aud,s}})$. + +### 7.6 Confidential Transfer + +The sender (account $A$, spending key $sk_A$) transfers a hidden amount $v_{\text{tx}}$ to recipient $B$ (public viewing key $\text{PVK}_B$). + +**Sender computation:** + +1. Sample ephemeral scalar $r_e \in \mathbb{F}_r$ via the rejection sampling procedure (ยง2.2); sample $\sigma \in \mathbb{F}_r$ via the same procedure +2. Compute $R_e = r_e \cdot H$ +3. Compute $S = r_e \cdot \text{PVK}_B$, extract $s = S.x$ +4. Derive transfer blinding: $r_{\text{tx}} = \text{Poseidon}(\delta_{\text{tx\\\_blind}}, s, \sigma)$ +5. Derive encrypted amount: $\tilde{v} = v_{\text{tx}} + \text{Poseidon}(\delta_{\text{tx\\\_amount}}, s, \sigma)$ +6. Compute transfer commitment: $C_{\text{tx}} = v_{\text{tx}} \cdot G + r_{\text{tx}} \cdot H$ +7. Compute new spendable commitment with deterministic randomness: + - $r_A' = \text{Poseidon}(\delta_{\text{spend\\\_r}}, vk_A, \sigma)$ + - $C_{\text{spend}}' = (v_A - v_{\text{tx}}) \cdot G + r_A' \cdot H$ +8. Compute encrypted balance scalar: $\tilde{b} = (v_A - v_{\text{tx}}) + \text{Poseidon}(\delta_{\text{enc\\\_bal}}, vk_A, \sigma)$ +9. Compute recipient-auditor ECDH shared secret: $S_{a,r} = r_e \cdot K_{\text{aud,r}}$, extract $s_{a,r} = S_{a,r}.x$ +10. Squeeze recipient-auditor channel masks: $(m_{v,r}, m_{r,r}) = \text{SpongeSqueeze}_2(\delta_{\text{aud\\\_r}}, s_{a,r}, \sigma)$ +11. Compute recipient-auditor ciphertexts: $\tilde{v}_{\text{aud,r}} = v_{\text{tx}} + m_{v,r}$ and $\tilde{r}_{\text{aud,r}} = r_{\text{tx}} + m_{r,r}$ +12. Compute sender-auditor ECDH shared secret: $S_{a,s} = r_e \cdot K_{\text{aud,s}}$, extract $s_{a,s} = S_{a,s}.x$ +13. Squeeze sender-auditor channel masks: $(m_{v,s}, m_{b,s}) = \text{SpongeSqueeze}_2(\delta_{\text{aud\\\_s}}, s_{a,s}, \sigma)$ +14. Compute sender-auditor ciphertexts: $\tilde{v}_{\text{aud,s}} = v_{\text{tx}} + m_{v,s}$ and $\tilde{b}_{\text{aud,s}} = (v_A - v_{\text{tx}}) + m_{b,s}$ + +**Circuit constraints (Transfer):** + +| # | Constraint | +|:--|:---| +| T1 | $Y_A = sk_A \cdot H$ (sender key ownership) | +| T2 | $vk_A = \text{Poseidon}(\delta_{\text{vk}}, sk_A, \text{wrap})$ (binds proof to wrapper) | +| T3 | Prover knows opening $(v_A, r_A)$ of $C_{\text{spend}}^A$ | +| T4 | $v_A \in [0, 2^{127})$, $v_{\text{tx}} \in [0, 2^{127})$, $v_A - v_{\text{tx}} \in [0, 2^{127})$ (range validity, Section 2.6) | +| T5 | $S = r_e \cdot \text{PVK}_B$ (ECDH correctly computed) | +| T6 | $R_e = r_e \cdot H$ (ephemeral key well-formed) | +| T7 | $r_{\text{tx}} = \text{Poseidon}(\delta_{\text{tx\\\_blind}}, S.x, \sigma)$ (blinding correctly derived) | +| T8 | $C_{\text{tx}} = v_{\text{tx}} \cdot G + r_{\text{tx}} \cdot H$ (transfer commitment well-formed) | +| T9 | $\tilde{v} = v_{\text{tx}} + \text{Poseidon}(\delta_{\text{tx\\\_amount}}, S.x, \sigma)$ (encrypted amount correct) | +| T10 | $r_A' = \text{Poseidon}(\delta_{\text{spend\\\_r}}, vk_A, \sigma)$ (deterministic randomness) | +| T11 | $C_{\text{spend}}' = (v_A - v_{\text{tx}}) \cdot G + r_A' \cdot H$ (new sender balance) | +| T12 | $\tilde{b} = (v_A - v_{\text{tx}}) + \text{Poseidon}(\delta_{\text{enc\\\_bal}}, vk_A, \sigma)$ (encrypted balance scalar) | +| T13 | $r_e \neq 0$ (rules out $R_e = \mathcal{O}$ and $S, S_{a,r}, S_{a,s} = \mathcal{O}$; otherwise every ECDH mask in this transfer collapses to a constant function of $\sigma$) | +| T\_a1 | $S_{a,r} = r_e \cdot K_{\text{aud,r}}$ (recipient-auditor ECDH shared secret, reuses ephemeral scalar) | +| T\_a2 | $(m_{v,r}, m_{r,r}) = \text{SpongeSqueeze}_2(\delta_{\text{aud\\\_r}}, S_{a,r}.x, \sigma)$ (recipient-auditor channel masks) | +| T\_a3 | $\tilde{v}_{\text{aud,r}} = v_{\text{tx}} + m_{v,r}$ (recipient-auditor encrypted transfer amount) | +| T\_a4 | $\tilde{r}_{\text{aud,r}} = r_{\text{tx}} + m_{r,r}$ (recipient-auditor encrypted transfer randomness, enables Pedersen-opening reconstruction of $C_{\text{receive}}$, see Section 8.1) | +| T\_a5 | $S_{a,s} = r_e \cdot K_{\text{aud,s}}$ (sender-auditor ECDH shared secret, reuses ephemeral scalar) | +| T\_a6 | $(m_{v,s}, m_{b,s}) = \text{SpongeSqueeze}_2(\delta_{\text{aud\\\_s}}, S_{a,s}.x, \sigma)$ (sender-auditor channel masks) | +| T\_a7 | $\tilde{v}_{\text{aud,s}} = v_{\text{tx}} + m_{v,s}$ (sender-auditor encrypted transfer amount) | +| T\_a8 | $\tilde{b}_{\text{aud,s}} = (v_A - v_{\text{tx}}) + m_{b,s}$ (sender-auditor encrypted balance checkpoint) | + +**Public inputs (24 fields, counting each Grumpkin point as two $\mathbb{F}_r$ coordinates):** + +| Input | Notes | +|:---|:---| +| $C_{\text{spend}}^A$ | Loaded from sender's `spendable_balance` | +| $Y_A$ | Loaded from sender's `spending_key` | +| $\text{PVK}_B$ | Loaded from recipient's `viewing_public_key`. Recipient must be registered. | +| $\text{wrap}$ | Loaded from instance storage; set once at construction (ยง3.5) | +| $K_{\text{aud,r}}$ | Fetched from the auditor contract using recipient's `auditor_id` | +| $K_{\text{aud,s}}$ | Fetched from the auditor contract using sender's `auditor_id` | +| $C_{\text{spend}}'$, $C_{\text{tx}}$, $R_e$, $\tilde{v}$, $\tilde{b}$, $\sigma$, $\tilde{v}_{\text{aud,r}}$, $\tilde{r}_{\text{aud,r}}$, $\tilde{v}_{\text{aud,s}}$, $\tilde{b}_{\text{aud,s}}$ | Prover-supplied; $C_{\text{spend}}'$ written to sender's `spendable_balance`, $C_{\text{tx}}$ added to recipient's `receiving_balance`, the rest emitted in event | + +**Private witnesses:** $sk_A$, $vk_A$, $v_A$, $r_A$, $v_{\text{tx}}$, $r_e$. + +**Post-verification:** The contract verifies the proof, then: +- Sets $A$`.spendable_balance` $= C_{\text{spend}}'$ +- Adds to recipient: $B$`.receiving_balance` $\mathrel{+}= C_{\text{tx}}$ +- Emits event with $(R_e, \tilde{v}, \sigma, \tilde{b}, \tilde{v}_{\text{aud,r}}, \tilde{r}_{\text{aud,r}}, \tilde{v}_{\text{aud,s}}, \tilde{b}_{\text{aud,s}})$ + +**Recipient processing.** Upon observing the event, the recipient computes $S = vk \cdot R_e$, derives amount and blinding. The decryption flow is independent of whether the sender was the owner or an operator. + +### 7.7 Set Operator + +The owner locks funds from their spendable balance into a per-operator escrow. The operator must be a registered account in the wrapper, so that $Y_{\text{op}}$ (needed for $dvk_i$ escrow) can be looked up from the operator's stored `spending_key`. + +**Circuit constraints (SetOperator):** + +| # | Constraint | +|:--|:---| +| S1 | $Y = sk \cdot H$ (owner key ownership) | +| S2 | $vk = \text{Poseidon}(\delta_{\text{vk}}, sk, \text{wrap})$ (binds proof to wrapper) | +| S3 | Prover knows opening $(v, r)$ of $C_{\text{spend}}$ | +| S4 | $v \in [0, 2^{127})$, $v_a \in [0, 2^{127})$, $v - v_a \in [0, 2^{127})$ (range validity, Section 2.6) | +| S5 | $dvk_i = \text{Poseidon}(\delta_{\text{dvk}}, vk, \text{op}_i)$ (delegation key derivation; wrapper-bound via $vk$) | +| S6 | $r_a = \text{Poseidon}(\delta_{\text{allow\\\_r}}, dvk_i, \sigma_a)$ (allowance blinding) | +| S7 | $C_a = v_a \cdot G + r_a \cdot H$ (allowance commitment) | +| S8 | $\tilde{a} = v_a + \text{Poseidon}(\delta_{\text{enc\\\_allow}}, dvk_i, \sigma_a)$ (encrypted allowance) | +| S9 | $r' = \text{Poseidon}(\delta_{\text{spend\\\_r}}, vk, \sigma)$ (new balance randomness) | +| S10 | $C_{\text{spend}}' = (v - v_a) \cdot G + r' \cdot H$ (new spendable balance) | +| S11 | $\tilde{b} = (v - v_a) + \text{Poseidon}(\delta_{\text{enc\\\_bal}}, vk, \sigma)$ (encrypted balance) | +| S12 | Escrowed $dvk_i$ correctly encrypts under $Y_{\text{op}}$ via ECDH | +| S13 | $r_e \neq 0$ (rules out $R_e = \mathcal{O}$ and $S_{a,s} = \mathcal{O}$; the same $r_e$ is reused for the $dvk_i$ escrow ECDH in Section 7.11, so this also rules out a trivial escrow shared secret) | +| S\_a1 | $R_e = r_e \cdot H$ (ephemeral key for auditor ECDH) | +| S\_a2 | $S_{a,s} = r_e \cdot K_{\text{aud,s}}$ (owner-auditor ECDH shared secret) | +| S\_a3 | $(m_v, m_b) = \text{SpongeSqueeze}_2(\delta_{\text{aud\\\_s}}, S_{a,s}.x, \sigma)$ (owner-auditor channel masks) | +| S\_a4 | $\tilde{v}_{\text{aud,s}} = v_a + m_v$ (owner-auditor encrypted escrow amount) | +| S\_a5 | $\tilde{b}_{\text{aud,s}} = (v - v_a) + m_b$ (owner-auditor encrypted balance checkpoint) | + +**Public inputs (24 fields):** + +| Input | Notes | +|:---|:---| +| $C_{\text{spend}}$ | Loaded from owner's `spendable_balance` | +| $Y$ | Loaded from owner's `spending_key` | +| $Y_{\text{op}}$ | Loaded from operator account's `spending_key`. Operator must be registered. | +| $\text{op}_i$ | $\text{address\\\_to\\\_field}$(`operator` argument), computed per-call by the wrapper (ยง2.7) | +| $\text{wrap}$ | Loaded from instance storage; set once at construction (ยง3.5) | +| $K_{\text{aud,s}}$ | Fetched from the auditor contract using owner's `auditor_id` | +| $C_{\text{spend}}'$, $C_a$, escrowed\_dvk, $\tilde{b}$, $\tilde{a}$, $\sigma$, $\sigma_a$, $R_e$, $\tilde{v}_{\text{aud,s}}$, $\tilde{b}_{\text{aud,s}}$ | Prover-supplied; $C_{\text{spend}}'$ written to owner's `spendable_balance`, the delegation fields written to storage, the rest emitted in event | + +**Private witnesses:** $sk$, $vk$, $v$, $r$, $v_a$, $r_e$. + +**Post-verification:** The contract verifies the proof, sets `spendable_balance` $= C_{\text{spend}}'$ and stores the `OperatorDelegation`. Emits event with $(R_e, \sigma, \tilde{b}, \tilde{v}_{\text{aud,s}}, \tilde{b}_{\text{aud,s}})$. + +### 7.8 Operator Transfer + +The operator transfers from the owner's escrowed allowance to a recipient. + +**Circuit constraints (OperatorTransfer):** + +| # | Constraint | +|:--|:---| +| O1 | $Y_{\text{op}} = sk_{\text{op}} \cdot H$ (operator key ownership) | +| O2 | Prover knows $dvk_i$ and the opening $(v_a, r_a)$ of $C_a$ | +| O3 | $r_a = \text{Poseidon}(\delta_{\text{allow\\\_r}}, dvk_i, \sigma_a)$ (allowance randomness matches stored state) | +| O4 | $v_a \in [0, 2^{127})$, $v_{\text{tx}} \in [0, 2^{127})$, $v_a - v_{\text{tx}} \in [0, 2^{127})$ (range validity, Section 2.6) | +| O5 | $S = r_e \cdot \text{PVK}_{\text{recipient}}$ (ECDH for recipient) | +| O6 | $R_e = r_e \cdot H$ | +| O7 | $r_{\text{tx}} = \text{Poseidon}(\delta_{\text{tx\\\_blind}}, S.x, \sigma_a)$ (transfer blinding) | +| O8 | $C_{\text{tx}} = v_{\text{tx}} \cdot G + r_{\text{tx}} \cdot H$ | +| O9 | $\tilde{v} = v_{\text{tx}} + \text{Poseidon}(\delta_{\text{tx\\\_amount}}, S.x, \sigma_a)$ (encrypted amount) | +| O10 | $r_a' = \text{Poseidon}(\delta_{\text{allow\\\_r}}, dvk_i, \sigma_a')$ (new allowance randomness) | +| O11 | $C_a' = (v_a - v_{\text{tx}}) \cdot G + r_a' \cdot H$ (new allowance) | +| O12 | $\tilde{a}' = (v_a - v_{\text{tx}}) + \text{Poseidon}(\delta_{\text{enc\\\_allow}}, dvk_i, \sigma_a')$ (encrypted allowance) | +| O13 | $r_e \neq 0$ (rules out $R_e = \mathcal{O}$ and $S, S_{a,r}, S_{a,s} = \mathcal{O}$; otherwise every ECDH mask in this transfer collapses to a constant function of $\sigma_a$) | +| O\_a1 | $S_{a,r} = r_e \cdot K_{\text{aud,r}}$ (recipient-auditor ECDH shared secret, reuses ephemeral scalar) | +| O\_a2 | $(m_{v,r}, m_{r,r}) = \text{SpongeSqueeze}_2(\delta_{\text{aud\\\_r}}, S_{a,r}.x, \sigma_a)$ (recipient-auditor channel masks) | +| O\_a3 | $\tilde{v}_{\text{aud,r}} = v_{\text{tx}} + m_{v,r}$ (recipient-auditor encrypted transfer amount) | +| O\_a4 | $\tilde{r}_{\text{aud,r}} = r_{\text{tx}} + m_{r,r}$ (recipient-auditor encrypted transfer randomness, enables Pedersen-opening reconstruction of $C_{\text{receive}}$, see Section 8.1) | +| O\_a5 | $S_{a,s} = r_e \cdot K_{\text{aud,s}}$ (owner-auditor ECDH shared secret, reuses ephemeral scalar) | +| O\_a6 | $(m_{v,s}, m_{a,s}) = \text{SpongeSqueeze}_2(\delta_{\text{aud\\\_s}}, S_{a,s}.x, \sigma_a)$ (owner-auditor channel masks) | +| O\_a7 | $\tilde{v}_{\text{aud,s}} = v_{\text{tx}} + m_{v,s}$ (owner-auditor encrypted transfer amount) | +| O\_a8 | $\tilde{a}_{\text{aud,s}} = (v_a - v_{\text{tx}}) + m_{a,s}$ (owner-auditor encrypted post-transfer allowance) | + +**Public inputs (24 fields):** + +| Input | Notes | +|:---|:---| +| $C_a$, $\sigma_a$ | Loaded from the `(from, operator)` delegation entry | +| $Y_{\text{op}}$ | Loaded from operator's `spending_key`; matches the auth principal | +| $\text{PVK}_{\text{recipient}}$ | Loaded from recipient's `viewing_public_key` | +| $K_{\text{aud,r}}$ | Fetched from the auditor contract using recipient's `auditor_id` | +| $K_{\text{aud,s}}$ | Fetched from the auditor contract using **owner's** `auditor_id`, not operator's. The visibility model points balance- and allowance-checkpoint ciphertexts at the funds' owner. | +| $C_a'$, $C_{\text{tx}}$, $R_e$, $\tilde{v}$, $\tilde{a}'$, $\sigma_a'$, $\tilde{v}_{\text{aud,r}}$, $\tilde{r}_{\text{aud,r}}$, $\tilde{v}_{\text{aud,s}}$, $\tilde{a}_{\text{aud,s}}$ | Prover-supplied; allowance fields written to delegation storage, $C_{\text{tx}}$ added to recipient's `receiving_balance`, the rest emitted in event | + +**Private witnesses:** $sk_{\text{op}}$, $dvk_i$, $v_a$, $r_a$ (single-limb $\mathbb{F}_r$; pinned by O3 to $\text{Poseidon}(\delta_{\text{allow\\\_r}}, dvk_i, \sigma_a)$), $v_{\text{tx}}$, $r_e$. + +**Post-verification:** The contract checks `ledger.sequence() <= expiration_ledger`, updates `allowance_commitment`, `encrypted_allowance`, stores `new_allowance_salt`, and adds $C_{\text{tx}}$ to the recipient's `receiving_balance`. Emits event with $(R_e, \tilde{v}, \sigma_a, \tilde{v}_{\text{aud,r}}, \tilde{r}_{\text{aud,r}}, \tilde{v}_{\text{aud,s}}, \tilde{a}_{\text{aud,s}})$. + +**Recipient uniformity.** The recipient processes the incoming transfer identically to a direct transfer: compute $S = vk \cdot R_e$, derive amount and blinding. The decryption flow is independent of whether the sender was the owner or an operator. + +**Wrapper binding.** Unlike owner-initiated circuits, the OperatorTransfer circuit does not constrain the $vk$ derivation (the operator has no access to the owner's $sk$). Wrapper binding is instead inherited indirectly through the allowance commitment chain: the SetOperator circuit derives $dvk_i$ from the wrapper-specific $vk$ (S2, S5), which determines $r_a$ (S6) and thus $C_a$ (S7). The OperatorTransfer circuit verifies $dvk_i$ against $C_a$ via $\sigma_a$ (O3). Since $C_a$ is a public input and was constructed with wrapper-specific randomness, a proof generated against one wrapper's $C_a$ cannot verify against another's. + +### 7.9 Revoke Operator + +The owner reclaims the remaining escrowed allowance. + +**Circuit constraints (RevokeOperator):** + +| # | Constraint | +|:--|:---| +| V1 | $Y = sk \cdot H$ (owner key ownership) | +| V2 | $vk = \text{Poseidon}(\delta_{\text{vk}}, sk, \text{wrap})$ (binds proof to wrapper) | +| V3 | $dvk_i = \text{Poseidon}(\delta_{\text{dvk}}, vk, \text{op}_i)$ | +| V4 | Prover knows opening $(v_a, r_a)$ of $C_a$, with $r_a = \text{Poseidon}(\delta_{\text{allow\\\_r}}, dvk_i, \sigma_a)$ (allowance randomness matches stored state, mirrors O3) | +| V5 | Prover knows opening $(v_s, r_s)$ of $C_{\text{spend}}$ | +| V6 | $r' = \text{Poseidon}(\delta_{\text{spend\\\_r}}, vk, \sigma)$ | +| V7 | $C_{\text{spend}}' = (v_s + v_a) \cdot G + r' \cdot H$ | +| V8 | $\tilde{b} = (v_s + v_a) + \text{Poseidon}(\delta_{\text{enc\\\_bal}}, vk, \sigma)$ | +| V9 | $v_s \in [0, 2^{127})$, $v_a \in [0, 2^{127})$, $v_s + v_a \in [0, 2^{127})$ (range validity, Section 2.6) | +| V10 | $r_e \neq 0$ (rules out $R_e = \mathcal{O}$ and $S_{a,s} = \mathcal{O}$, which would reduce $m_v$ and $m_b$ to constant functions of $\sigma$) | +| V\_a1 | $R_e = r_e \cdot H$ (ephemeral key for auditor ECDH) | +| V\_a2 | $S_{a,s} = r_e \cdot K_{\text{aud,s}}$ (owner-auditor ECDH shared secret) | +| V\_a3 | $(m_v, m_b) = \text{SpongeSqueeze}_2(\delta_{\text{aud\\\_s}}, S_{a,s}.x, \sigma)$ (owner-auditor channel masks) | +| V\_a4 | $\tilde{v}_{\text{aud,s}} = v_a + m_v$ (owner-auditor encrypted reclaimed amount) | +| V\_a5 | $\tilde{b}_{\text{aud,s}} = (v_s + v_a) + m_b$ (owner-auditor encrypted balance checkpoint) | + +**Public inputs (19 fields):** + +| Input | Notes | +|:---|:---| +| $C_{\text{spend}}$ | Loaded from owner's `spendable_balance` | +| $C_a$, $\sigma_a$ | Loaded from the `(account, operator)` delegation entry | +| $Y$ | Loaded from owner's `spending_key` | +| $\text{op}_i$ | $\text{address\\\_to\\\_field}$(`operator` argument), computed per-call by the wrapper (ยง2.7) | +| $\text{wrap}$ | Loaded from instance storage; set once at construction (ยง3.5) | +| $K_{\text{aud,s}}$ | Fetched from the auditor contract using owner's `auditor_id` | +| $C_{\text{spend}}'$, $\tilde{b}$, $\sigma$, $R_e$, $\tilde{v}_{\text{aud,s}}$, $\tilde{b}_{\text{aud,s}}$ | Prover-supplied; $C_{\text{spend}}'$ written to owner's `spendable_balance`, delegation entry deleted, the rest emitted in event | + +**Private witnesses:** $sk$, $vk$, $dvk_i$, $v_a$, $r_a$, $v_s$, $r_s$ (input spendable-balance blinding, encoded as a single $\mathbb{F}_r$ `Field`; see ยง10.4 *Post-merge witness availability* for the acknowledged $2^{-127}$-per-merge case affecting $r_s$), $r_e$. + +**Post-verification:** The contract verifies the proof, sets `spendable_balance` $= C_{\text{spend}}'$ and deletes the delegation. Emits event with $(R_e, \sigma, \tilde{b}, \tilde{v}_{\text{aud,s}}, \tilde{b}_{\text{aud,s}})$. + +### 7.10 Owner Operations with Active Operators + +Owner transfers, withdrawals, and merges proceed identically to the no-operator case. Operator allowances are independently escrowed - no synchronization is needed. The owner's spendable balance and operator allowances are fully isolated. + +### 7.11 Delegation Key Escrow + +At `set_operator`, the owner escrows $dvk_i$ to the operator on-chain via ECDH, eliminating off-chain key sharing: + +1. Owner picks ephemeral $r_e$ (reused from the `set_operator` proof's outer ECDH; see ยง5.3 Why reusing $r_e$ is safe) and computes $R = r_e \cdot H$. +2. Shared secret: $s = (r_e \cdot Y_{\text{op}}).x$ +3. Escrowed key: $\text{escrowed\\\_dvk} = (R.x, \; \text{Poseidon}(\delta_{\text{esc\\\_dvk}}, s, \text{op}_i) + dvk_i)$ + +**Encoding.** `escrowed_dvk` is a `BytesN<64>` consisting of two 32-byte $\mathbb{F}_r$ representatives: `R_x` (the $x$-coordinate of $R$) followed by `dvk_cipher` (the masked $dvk_i$). $R.y$ is **not** stored. This is sound because ECDH on Grumpkin recovers only the $x$-coordinate of the shared secret: $\pm R$ both have $x = R.x$, and $sk_{\text{op}} \cdot R$ and $sk_{\text{op}} \cdot (-R)$ are inverse points with the same $x$-coordinate. The operator reconstructs the curve point by solving $y^2 = R.x^3 - 17$ in $\mathbb{F}_r$, picks either root, and proceeds; both choices produce the same $s = (sk_{\text{op}} \cdot R).x$ and therefore the same Poseidon mask. + +The operator decrypts using $sk_{\text{op}}$. The `set_operator` proof enforces escrow correctness via constraint S12, which expands to three sub-constraints over the prover-supplied `escrowed_dvk = (R_x, dvk_cipher)`: + +- $R_x = (r_e \cdot H).x$ +- $s_{\text{esc}} = (r_e \cdot Y_{\text{op}}).x$ +- $\text{dvk\\\_cipher} = \text{Poseidon}(\delta_{\text{esc\\\_dvk}}, s_{\text{esc}}, \text{op}_i) + dvk_i$ + +The $r_e$ here is the same scalar S\_a1 commits to ($R_e = r_e \cdot H$), so the escrow's $R_x$ and the auditor channel's $R_e.x$ are forced equal. + +### 7.12 Expiry and Revert Safety + +Delegations use persistent storage and persist until explicitly revoked. `expiration_ledger` is checked on every operator transfer. Allowance randomness includes `allowance_salt` to prevent deterministic-randomness reuse after reverted transactions. + +--- + +## 8. Auditing + +### 8.1 Per-Transfer Auditor Ciphertexts + +Each confidential transfer produces ciphertexts under two auditor keys via ECDH, using the same ephemeral scalar $r_e$ used for recipient ECDH. Each auditor channel runs Poseidon2 in sponge mode (Section 2.5), absorbing the channel's domain tag, the ECDH shared scalar, and $\sigma$, and squeezing two masks per call. + +**Recipient's auditor** ($K_{\text{aud,r}}$, from the recipient's `auditor_id`) receives the transfer amount and the per-transfer Pedersen randomness: + +$$S_{a,r} = r_e \cdot K_{\text{aud,r}}, \qquad s_{a,r} = S_{a,r}.x$$ +$$(m_{v,r}, m_{r,r}) = \text{SpongeSqueeze}_2(\delta_{\text{aud\\\_r}}, s_{a,r}, \sigma)$$ +$$\tilde{v}_{\text{aud,r}} = v_{\text{tx}} + m_{v,r}, \qquad \tilde{r}_{\text{aud,r}} = r_{\text{tx}} + m_{r,r}$$ + +**Sender's auditor** ($K_{\text{aud,s}}$, from the sender's `auditor_id`) receives the transfer amount and the sender's post-transfer balance: + +$$S_{a,s} = r_e \cdot K_{\text{aud,s}}, \qquad s_{a,s} = S_{a,s}.x$$ +$$(m_{v,s}, m_{b,s}) = \text{SpongeSqueeze}_2(\delta_{\text{aud\\\_s}}, s_{a,s}, \sigma)$$ +$$\tilde{v}_{\text{aud,s}} = v_{\text{tx}} + m_{v,s}, \qquad \tilde{b}_{\text{aud,s}} = (v_A - v_{\text{tx}}) + m_{b,s}$$ + +The transfer circuit (constraints T\_a1--T\_a8) enforces correct computation. The wrapper fetches both auditor keys from the auditor contract using the respective account `auditor_id` fields; neither the sender nor the recipient can substitute a different key. + +Each auditor decrypts using their secret key $k$. For example, the sender's auditor: + +$$S_{a,s} = k \cdot R_e, \qquad s_{a,s} = S_{a,s}.x$$ +$$(m_{v,s}, m_{b,s}) = \text{SpongeSqueeze}_2(\delta_{\text{aud\\\_s}}, s_{a,s}, \sigma)$$ +$$v_{\text{tx}} = \tilde{v}_{\text{aud,s}} - m_{v,s}, \qquad v_{\text{new}} = \tilde{b}_{\text{aud,s}} - m_{b,s}$$ + +where $R_e$ and $\sigma$ are published in the Transfer event. The recipient's auditor follows the same pattern with $\delta_{\text{aud\\\_r}}$ to recover the pair $(v_{\text{tx}}, r_{\text{tx}})$. + +**Recipient-auditor opening capability.** Because the recipient-auditor recovers $r_{\text{tx}}$ for every inbound transfer, and because deposits add to `receiving_balance` with $r = 0$ (Section 7.3), the recipient-auditor can reconstruct the full Pedersen opening of $C_{\text{receive}}$ between merges: + +$$v_r = \sum_i v_{\text{tx},i} + \sum_j a_j, \qquad r_r = \sum_i r_{\text{tx},i}$$ + +where $i$ ranges over inbound transfers and operator-transfers since the last merge and $j$ ranges over deposits. This is a full Pedersen *opening* of $C_{\text{receive}}$: both the value and the blinding are reconstructed by the auditor. + +The opening capability does not extend to $C_{\text{spend}}$. The auditor knows the *value* $v_s$ at every spend boundary via $\tilde{b}_{\text{aud,s}}$ (Section 5.5), and can extend that with the known $v_r$ contribution at each merge. This bounded opening is what enables the clawback flow specified in [COMPLIANCE.md](./COMPLIANCE.md) ยง5: the recipient-auditor is the seize-enabling party for inbound flows while $C_{\text{receive}}$ has not yet been merged. After merge the auditor still tracks $C_{\text{spend}}$'s value via $\tilde{b}_{\text{aud,s}}$ updates. + +### 8.2 Auditor Visibility Properties + +**Transfer amounts.** Both auditors see the transfer amount in real time. The recipient's auditor decrypts $v_{\text{tx}}$ from $\tilde{v}_{\text{aud,r}}$; the sender's auditor decrypts it from $\tilde{v}_{\text{aud,s}}$. + +**Balance checkpoints.** The sender's auditor receives an encrypted balance checkpoint at every owner-initiated operation that produces a proof: + +- **Outgoing transfer**: auditor decrypts post-transfer balance $(v_A - v_{\text{tx}})$ from $\tilde{b}_{\text{aud,s}}$ (constraints T\_a5--T\_a8). +- **Withdrawal**: auditor decrypts post-withdrawal balance $(v - a)$ from $\tilde{b}_{\text{aud,s}}$ (constraints W\_a1--W\_a4). The withdrawal amount $a$ is also visible as a public input. +- **Set operator**: auditor decrypts escrowed amount $v_a$ from $\tilde{v}_{\text{aud,s}}$ and post-escrow balance $(v - v_a)$ from $\tilde{b}_{\text{aud,s}}$ (constraints S\_a1--S\_a5). +- **Revoke operator**: auditor decrypts reclaimed amount $v_a$ from $\tilde{v}_{\text{aud,s}}$ and post-reclaim balance $(v_s + v_a)$ from $\tilde{b}_{\text{aud,s}}$ (constraints V\_a1--V\_a5). + +The recipient's auditor does not see the sender's balance in any of these operations. + +**Per-transfer Pedersen randomness (recipient-auditor only).** Beyond the transfer amount, the recipient's auditor also decrypts the per-transfer Pedersen blinding $r_{\text{tx}}$ from $\tilde{r}_{\text{aud,r}}$ on every confidential transfer and operator-transfer (constraints T\_a4 and O\_a4). Combined with $v_{\text{tx}}$ this is a full Pedersen opening of each $C_{\text{tx},i}$ and, by homomorphism, of the recipient's `receiving_balance` $C_{\text{receive}}$ between merges (Section 8.1). The sender's auditor does not see $r_{\text{tx}}$. + +**Key rotation.** When the auditor contract sets a new key under the account's `auditor_id` (ยง8.3), the new key sees the balance checkpoint at the next owner-initiated operation, with no event replay or bootstrapping required. The balance checkpoint is self-contained: it depends only on the auditor's ECDH secret key and the published $(R_e, \sigma)$. Note that `auditor_id` itself is immutable per account (ยง6.1); only the key under that `auditor_id` rotates. + +**No viewing-key escrow on the sender side.** The sender-auditor does not hold any account's viewing key, and compromise of a sender-auditor key exposes only per-operation amounts and balance checkpoints from operations that occurred while the compromised key was active. Historical balances under prior keys, and the recipient's `spendable_balance` (whose blinding derives from $vk_A$, not from any auditor channel), remain opaque. + +**Recipient-side opening capability.** The recipient-auditor additionally learns the per-transfer Pedersen randomness $r_{\text{tx}}$ for every inbound transfer and operator-transfer. This is capability-equivalent to holding the opening of every $C_{\text{tx},i}$ and, by summation, of $C_{\text{receive}}$. The capability is bounded in two ways: + +- **Forward-only.** Only events emitted while the auditor key was active are decryptable. +- **Receiving-side only (opening).** The full $(v_r, r_r)$ opening covers `receiving_balance`. It does not extend to a full opening of `spendable_balance`, whose blinding $r_s = \text{Poseidon}(\delta_{\text{spend\\\_r}}, vk_A, \sigma)$ depends on $vk_A$ and is not derivable from any auditor key. The auditor still knows the *value* $v_s$ of `spendable_balance` at every spend boundary via $\tilde{b}_{\text{aud,s}}$. + +This is the trust position that supports the clawback flow in [COMPLIANCE.md](./COMPLIANCE.md) ยง5: the recipient-auditor is the seize-enabling party for inbound flows, while the sender-auditor remains the seize-enabling party for the spendable-balance side via $\tilde{b}_{\text{aud,s}}$. + +### 8.3 Auditor Key Management and Rotation + +The auditor contract stores Grumpkin public keys as full affine points $(x, y)$ indexed by `auditor_id`. The contract validates that every inserted key is canonical, on-curve ($y^2 \equiv x^3 - 17 \pmod{r}$), and non-identity at insertion time (Section 3.1, Section 10.8). Each `auditor_id` MAY maintain a sequence of versions, each carrying its activation ledger. Rotation appends a new entry rather than overwriting the previous one. + +When building public inputs for any operation that produces auditor ciphertexts (transfers, withdrawals, set/revoke operator), the wrapper fetches the relevant auditor keys for the recipient's and/or sender's `auditor_id`. The wrapper passes the full Grumpkin point as a public input; the circuit constrains the ECDH ciphertexts against that exact point. The wrapper and the circuit are version-agnostic: they verify against whichever key the auditor contract currently exposes. + +**In-flight proofs across rotation.** A proof constructed against version $v$ becomes unverifiable the instant the auditor contract activates version $v+1$. The $K_{\text{aud}}$ public input the wrapper fetches at verification no longer matches the value the prover committed to, so UltraHonk verification fails and the invocation **reverts at the proof-verification boundary**. The caller (sender, owner, or operator) reconstructs the proof against the new $K_{\text{aud}}$ and resubmits. The rejection is benign: the wrapper's spendable balance, receiving balance, and delegation state are unchanged by the reverted call, $\sigma$ is freshly sampled on retry (Section 9.6), and an observer cannot correlate the rejected attempt with the resubmission. + +**Auditor's off-chain obligation.** The auditor MUST retain the secret key for every historical version it has issued. To decrypt an event at ledger $L$, the auditor queries the auditor contract for the version of its `auditor_id` whose activation ledger is the largest value not exceeding $L$, then uses the corresponding off-chain secret key against the $R_e$ and $\sigma$ (or $\sigma_a$) emitted in the event. + +### 8.4 Operator Transfer Auditing + +Each operator transfer produces auditor ciphertexts under two keys (constraints O\_a1--O\_a8), following the same dual-auditor sponge model as owner transfers. The recipient's auditor decrypts the transfer amount and the per-transfer Pedersen randomness: + +$$(m_{v,r}, m_{r,r}) = \text{SpongeSqueeze}_2(\delta_{\text{aud\\\_r}}, s_{a,r}, \sigma_a)$$ +$$v_{\text{tx}} = \tilde{v}_{\text{aud,r}} - m_{v,r}, \qquad r_{\text{tx}} = \tilde{r}_{\text{aud,r}} - m_{r,r}$$ + +The owner's auditor decrypts the transfer amount and post-transfer allowance: + +$$(m_{v,s}, m_{a,s}) = \text{SpongeSqueeze}_2(\delta_{\text{aud\\\_s}}, s_{a,s}, \sigma_a)$$ +$$v_{\text{tx}} = \tilde{v}_{\text{aud,s}} - m_{v,s}, \qquad v_a' = \tilde{a}_{\text{aud,s}} - m_{a,s}$$ + +where $s_{a,r}$, $s_{a,s}$, and $\sigma_a$ are recovered from the event as in Section 8.1. The recipient-auditor opening capability stated in Section 8.1 extends to operator-transfer inbound flows: $r_{\text{tx}}$ from operator-transfers contributes to $r_r$ in $C_{\text{receive}}$ identically to owner-transfer inbound flows. + +### 8.5 Operator Allowance Auditing + +The auditor tracks each allowance's current value through the per-event ciphertexts produced at every state-changing operation: `set_operator` reveals the escrowed amount $v_a$ (Section 8.2), `confidential_transfer_from` reveals the transfer amount and post-transfer allowance $v_a'$ (Section 8.4), and `revoke_operator` reveals the reclaimed amount (Section 8.2). + +**Key rotation.** Visibility is forward-only at the event level, matching the spendable-balance model (ยง8.2). A new key under the account's existing `auditor_id` sees an allowance at the next state-changing operation, when a fresh ciphertext is produced under the new key. + +--- + +## 9. Security Analysis + +### 9.1 Griefing Resistance + +**Proposition 2** (Spend-proof stability). *No third party can invalidate an honest owner's in-flight spend proof.* + +*Proof.* A spend proof (Section 7.6) references $C_{\text{spend}}^A$ as a public input. The contract modifies $C_{\text{spend}}^A$ only through: +1. Owner-initiated operations (transfer, withdrawal, `set_operator`, `revoke_operator`) - all require `account.require_auth()`. +2. Merge - requires `account.require_auth()`. + +Incoming transfers modify only $C_{\text{receive}}^A$, which does not appear in the spend proof's public inputs. Therefore, between proof construction and submission, no third-party action can alter $C_{\text{spend}}^A$. The proof remains valid. $\square$ + +**Corollary.** There is no counter cap on incoming transfers. An account can receive an unbounded number of transfers without any mandatory owner action. The receiving balance is a single point whose committed value grows monotonically; there is no chunk overflow because Pedersen commitments operate over the full scalar field ($|\mathbb{F}_q| \approx 2^{254}$). + +### 9.2 Merge Safety + +**Proposition 3** (Merge cannot be weaponized). *A third party cannot invoke merge on another account.* + +*Proof.* The `merge()` function requires `account.require_auth()`. Only the account holder can authorize it. $\square$ + +**Proposition 4** (Merge does not create or destroy value). *Follows directly from Proposition 1 and the homomorphic property of Pedersen commitments.* + +### 9.3 Balance Conservation + +**Invariant.** For any account at any time: + +$$\sum_{j} d_j - \sum_{k} w_k = v_{\text{spend}} + v_{\text{receive}} + \sum_{i} v_{\text{allowance}_i}$$ + +where $d_j$ are deposits, $w_k$ are withdrawals, and the right-hand side sums committed values across the spendable balance, the receiving balance, and every stored (not-yet-revoked) operator allowance. Expired-but-not-revoked allowances are included: expiration prevents the operator from spending the allowance, but the escrowed value still resides on-chain in $C_a$ until `revoke_operator` reclaims it (Section 6.2, Single-slot semantics). + +This invariant is maintained by: +- **Deposits** increase $v_{\text{receive}}$ by $d_j$ (Section 7.3). +- **Withdrawals** decrease $v_{\text{spend}}$ by $w_k$, enforced by circuit constraint W4. +- **Transfers** decrease sender's $v_{\text{spend}}$ and increase recipient's $v_{\text{receive}}$ by the same $v_{\text{tx}}$, enforced by circuit constraints T3โ€“T8. +- **Merge** moves value from $v_{\text{receive}}$ to $v_{\text{spend}}$ (Proposition 1); the sum is unchanged. +- **Set operator** moves value from $v_{\text{spend}}$ to $v_{\text{allowance}_i}$; enforced by S3โ€“S7. +- **Operator transfer** decreases $v_{\text{allowance}_i}$ and increases recipient's $v_{\text{receive}}$ by $v_{\text{tx}}$; enforced by O2โ€“O8. +- **Revoke** moves remaining $v_{\text{allowance}_i}$ back to $v_{\text{spend}}$; enforced by V4โ€“V7. + +### 9.4 Privacy Properties + +**Amount confidentiality.** Transfer amounts are hidden inside Pedersen commitments (computationally hiding under DL). The encrypted amount $\tilde{v}$ is masked by $\text{Poseidon}(\delta_{\text{tx\\\_amount}}, s, \sigma)$, which is pseudorandom to anyone who does not know $s$ (the ECDH shared secret). + +**Balance confidentiality.** The spendable balance commitment hides both value and blinding. The encrypted balance scalar $\tilde{b}$ emitted in spend-boundary events is masked by $\text{Poseidon}(\delta_{\text{enc\\\_bal}}, vk, \sigma)$, pseudorandom without $vk$. + +**Sender-recipient linkage.** Sender and recipient addresses are visible on-chain. The system provides amount and balance confidentiality, not anonymity. + +**Viewing key compromise.** Since $vk$ is wrapper-specific (Section 4.2), compromise of one wrapper's viewing key does not affect the owner's accounts in other wrappers. Within the compromised wrapper, the attacker can: read all spendable balance snapshots (via $\tilde{b}$ emitted in spend-boundary events), decrypt all incoming transfer amounts (via ECDH with $R_e$ from events), and derive all $dvk_i$ to read operator allowances. The attacker **cannot** authorize any spending operation (requires $sk$, and $vk$ cannot recover $sk$ by Poseidon preimage resistance). + +**Auditor key compromise.** If a sender's auditor key is compromised, the attacker can decrypt amounts and balance checkpoints ($\tilde{b}_{\text{aud,s}}$) for all operations (transfers, withdrawals, set/revoke operator) from accounts that used the compromised key, but cannot construct openings of any commitment. If a recipient's auditor key is compromised, the attacker recovers both the transfer amount and the per-transfer Pedersen randomness ($\tilde{v}_{\text{aud,r}}$, $\tilde{r}_{\text{aud,r}}$) for every incoming transfer to accounts that used the compromised key. This is capability-equivalent to holding the opening of every $C_{\text{tx},i}$ and, by summation, of the receiving-balance commitment $C_{\text{receive}}$; see Section 8.2 for the bounded scope (forward-only, receiving-side only). Merge folds $r_r$ into the spendable-balance randomness ($r_{\text{spend}}' = r_s + r_r$, Section 7.4) and emits no checkpoint, so the recipient-auditor's $r_r$ knowledge does not extend to a post-merge opening of $C_{\text{spend}}$: $r_s$ depends on $vk_A$ and is not derivable from any auditor key. In neither case can the attacker recover viewing keys, post-merge spendable-balance openings, historical data from before the key was active, or authorize any spending. After key rotation, new operations are protected by the new key. + +### 9.5 State Recovery + +The recovery model is built around **checkpoints**: each owner-initiated operation that produces a proof emits $(\tilde{b}, \sigma)$ in its event, creating a point from which the full spendable balance opening is recoverable using $\tilde{b}$, $\sigma$, and $vk$. Event replay is bounded to the window between the most recent checkpoint and the current ledger. + +A checkpoint is concretely an event of type `Withdraw`, `Transfer` (as sender), `SetOperator`, or `RevokeOperator`: exactly the events that carry $(\tilde{b}, \sigma)$ for the owner's spendable balance. `Deposit`, incoming `Transfer`, and `OperatorTransfer` do not touch the owner's spendable balance and are therefore not checkpoints. `Merge` does update the spendable balance ($C_{\text{spend}} \leftarrow C_{\text{spend}} + C_{\text{receive}}$, Section 7.4) but is not a checkpoint either: it carries no proof and emits no $(\tilde{b}, \sigma)$, so consistency between $\tilde{b}$ and the post-merge commitment cannot be enforced. Any merge activity is absorbed into the next owner-initiated proof operation, which issues a fresh checkpoint. + +**Checkpoint recovery (one event lookup).** At every spend boundary, the spendable balance has deterministic randomness: $r = \text{Poseidon}(\delta_{\text{spend\\\_r}}, vk, \sigma)$. The owner fetches $(\tilde{b}, \sigma)$ from the most recent checkpoint event for their account (`Withdraw`, sender-side `Transfer`, `SetOperator`, or `RevokeOperator`), then recovers $v = \tilde{b} - \text{Poseidon}(\delta_{\text{enc\\\_bal}}, vk, \sigma)$. Consistency is verifiable: $C_{\text{spend}} \stackrel{?}{=} v \cdot G + r \cdot H$. + +**Post-checkpoint recovery (bounded event replay).** Between a checkpoint and the next spend, the receiving balance may have accumulated incoming transfers and deposits, and a merge may have folded them into the spendable balance. The owner reconstructs the current state by replaying only events since the last checkpoint: + +1. Start from the checkpoint: set $W_{\text{spend}} \leftarrow (v_n, r_n)$ recovered from $(\tilde{b}, \sigma)$ in the latest checkpoint event and the deterministic blinding derivation. Set $W_{\text{receive}} \leftarrow (0, 0)$. +2. Replay all events since the checkpoint in ledger order. For each: incoming transfers and deposits accumulate into $W_{\text{receive}}$; merge events fold $W_{\text{receive}}$ into $W_{\text{spend}}$ and reset $W_{\text{receive}} \leftarrow (0, 0)$. This correctly handles any number of interleaved events. +3. Verify: $C_{\text{spend}} \stackrel{?}{=} W_{\text{spend}}.v \cdot G + W_{\text{spend}}.r \cdot H$ and $C_{\text{receive}} \stackrel{?}{=} W_{\text{receive}}.v \cdot G + W_{\text{receive}}.r \cdot H$. + +The replay window is bounded by the owner's spending frequency. An account that spends or withdraws regularly produces frequent checkpoints, keeping the replay window short. In the worst case (funds received but never spent), the window extends back to registration. + +**Data-availability dependency.** Recovery from seed alone (i.e., after the wallet's local cache is destroyed) requires access to the full event history since the last checkpoint, which Stellar RPC does not guarantee. The protocol therefore requires a durable indexer; the data model, retention obligations, and recommended API are specified in [INDEXER.md](INDEXER.md). Without such an indexer a user can still see that their funds exist on-chain (the commitment remains), but cannot reconstruct the opening required to spend. + +**Incoming-transfer spam.** A third party can spam an account with confidential transfers (including zero-value transfers, see Section 9.1 Corollary) without invalidating the recipient's spend proofs. The cost to the spammer is the Soroban transaction fee per transfer, which bounds the rate. The cost to the recipient is per-event indexer storage and wallet replay work. Both costs are linear in the number of incoming transfers and bounded by the replay window; neither breaks correctness. + +### 9.6 Revert Safety + +Because $\sigma$ is sampled fresh via CSPRNG for every operation, a retry after a reverted transaction naturally uses a different $\sigma$. This means the deterministic randomness $r = \text{Poseidon}(\delta_{\text{spend\\\_r}}, vk, \sigma)$ is always fresh, and an observer cannot correlate reverted and retried commitments. + +**Retry procedure.** On revert, the wallet simply picks a new random $\sigma$ and recomputes the proof. No special-case logic is needed. The $\sigma$ is a public input and emitted in events so the auditor and owner can reconstruct randomness. + +### 9.7 Replay Protection + +**Proposition 5** (Proof non-replayability). *A valid proof cannot be replayed to execute the same operation twice.* + +*Proof.* Every spending proof includes the current on-chain commitment ($C_{\text{spend}}$ or $C_a$) as a public input. Upon successful verification, the contract replaces this commitment with the proof's output commitment ($C_{\text{spend}}'$ or $C_a'$). A replayed proof references the old commitment, which no longer matches the stored state, so verification fails. The same argument applies to operator transfers via $C_a$. $\square$ + +**Corollary.** No explicit nullifier or nonce is needed. State binding through commitment chaining provides replay protection as an inherent property of the protocol. + +--- + +## 10. Proof System + +### 10.1 Circuits + +All proof logic is written in Noir, compiled to UltraHonk circuits. The system uses 6 circuits: + +```rust +#[contracttype] +#[repr(u32)] +pub enum CircuitType { + Register = 0, + Withdraw = 1, + Transfer = 2, + OperatorTransfer = 3, + SetOperator = 4, + RevokeOperator = 5, +} +``` + +### 10.2 Circuit Summary + +| Circuit | What it proves | +|:---|:---| +| `Register` | Spending key well-formedness; wrapper-bound viewing key derivation from $sk$; public viewing key consistency with the derived $vk$ | +| `Withdraw` | Balance sufficiency; new spendable commitment with deterministic randomness; encrypted balance scalar; sender-auditor ECDH ciphertext (balance checkpoint); owner key ownership | +| `Transfer` | Balance conservation; ECDH-derived blinding and encrypted amount for recipient; dual-auditor channel sponges (recipient auditor: amount + per-transfer Pedersen randomness; sender auditor: amount + balance); deterministic randomness for new sender balance; encrypted balance scalar; sender key ownership; range validity (balance $\in [0, 2^{127})$, amount $\in [0, 2^{127})$) | +| `OperatorTransfer` | Allowance sufficiency; ECDH-derived blinding and encrypted amount for recipient; dual-auditor channel sponges (recipient auditor: amount + per-transfer Pedersen randomness; owner auditor: amount + allowance); deterministic randomness for new allowance; encrypted allowance scalar; operator key ownership; wrapper-bound indirectly via $C_a$ chain (Section 7.8) | +| `SetOperator` | Balance split; $dvk_i$ derivation; ECDH escrow of $dvk_i$; allowance commitment with deterministic randomness; encrypted balance and allowance scalars; owner-auditor ECDH ciphertexts (escrow amount + balance checkpoint); owner key ownership; wrapper-bound via $vk$ derivation | +| `RevokeOperator` | Allowance decryption via $dvk_i$; balance merge; deterministic randomness for new balance; encrypted balance scalar; owner-auditor ECDH ciphertexts (reclaimed amount + balance checkpoint); owner key ownership; wrapper-bound via $vk$ derivation | + +### 10.3 Circuit Cost Analysis + +The dominant cost in Noir circuits is elliptic curve scalar multiplication. With Barretenberg's native Grumpkin support via `multi_scalar_mul`, each scalar multiplication costs approximately 64 UltraPlonk-equivalent constraints (with ECC VM) or 4,700โ€“6,250 without. + +The Transfer circuit requires approximately 7 scalar multiplications: spending key verification, spendable balance opening, recipient ECDH shared secret, ephemeral key derivation, transfer commitment construction, recipient-auditor ECDH shared secret, and sender-auditor ECDH shared secret. The OperatorTransfer circuit requires approximately 7 scalar multiplications: operator key verification, allowance commitment opening, recipient ECDH shared secret, ephemeral key derivation, transfer commitment construction, recipient-auditor ECDH shared secret, and owner-auditor ECDH shared secret. The Withdraw, SetOperator, and RevokeOperator circuits each require 2 additional scalar multiplications for auditor ECDH (ephemeral key derivation + auditor shared secret), bringing their totals to approximately 4, 6, and 5 respectively. The ECDH computations add scalar multiplications compared to a random-blinding scheme, but the unchunked design eliminates all per-chunk constraints (which, in a chunked scheme, would involve 8+ scalar multiplications for balance chunks and per-chunk range proofs). + +### 10.4 Noir Primitives + +```rust +use std::embedded_curve_ops::{EmbeddedCurvePoint, EmbeddedCurveScalar, multi_scalar_mul}; + +/// Barretenberg's Pedersen generator at index 0 of the "DEFAULT_DOMAIN_SEPARATOR" +/// domain. Equivalent to `derive_generators("DEFAULT_DOMAIN_SEPARATOR", 0)[0]`. +global G: EmbeddedCurvePoint = EmbeddedCurvePoint { + x: 0x083e7911d835097629f0067531fc15cafd79a89beecb39903f69572c636f4a5a, + y: 0x1a7f5efaad7f315c25a918f30cc8d7333fccab7ad7c90f14de81bcc528f9935d, + is_infinite: false, +}; + +/// Barretenberg's Pedersen generator at index 1 of the "DEFAULT_DOMAIN_SEPARATOR" +/// domain. Equivalent to `derive_generators("DEFAULT_DOMAIN_SEPARATOR", 0)[1]`. +/// No known discrete-log relation to G (each is the output of Barretenberg's +/// hash-to-curve on the domain separator + index). +global H: EmbeddedCurvePoint = EmbeddedCurvePoint { + x: 0x054aa86a73cb8a34525e5bbed6e43ba1198e860f5f3950268f71df4591bde402, + y: 0x209dcfbf2cfb57f9f6046f44d71ac6faf87254afc7407c04eb621a6287cac126, + is_infinite: false, +}; + +/// Pedersen commitment, used uniformly for every opening witnessed in any +/// circuit (input or output). Both scalars are encoded as single-limb F_r +/// `Field` values: Poseidon outputs or rejection-sampled CSPRNG draws for fresh +/// blindings, and (for the spend-side input opening of C_spend in W3/T3/S3/V5) +/// the canonical F_q reduction of the wallet's post-merge integer blinding, +/// which lies in F_r with probability >= 1 - 2^-127 per merge. The complementary +/// case is acknowledged below in *Post-merge witness availability*. +fn commit(value: Field, randomness: Field) -> EmbeddedCurvePoint { + multi_scalar_mul( + [G, H], + [EmbeddedCurveScalar::from_field(value), + EmbeddedCurveScalar::from_field(randomness)] + ) +} + +/// ECDH: scalar * point. All ECDH scalars in this protocol are F_r-sampled +/// (sk, vk, r_e), so single-limb conversion is sound. +fn ecdh(scalar: Field, point: EmbeddedCurvePoint) -> EmbeddedCurvePoint { + multi_scalar_mul( + [point], + [EmbeddedCurveScalar::from_field(scalar)] + ) +} +``` + +**Post-merge witness availability.** Every opening witnessed in any circuit is encoded as a single $\mathbb{F}_r$ `Field` via the same `commit` primitive. After a `Merge` (ยง7.4), the spendable-balance blinding is $r_s + r_r$ over $\mathbb{F}_q$. Its canonical $\mathbb{F}_q$ representative lies in $[0, r)$ -- representable as a Noir `Field` -- with probability $\geq 1 - (q - r)/q \approx 1 - 2^{-127}$ per merge (ยง2.3). With the complementary probability $\approx 2^{-127}$ it lies in $[r, q)$. In that case the on-chain state remains well-formed (the commitment is a valid Grumpkin point), but the wallet's local opening witness is unencodable as a `Field`, so no spend / transfer / set-operator / revoke-operator proof can be constructed against the affected $C_{\text{spend}}$ until further accumulation shifts the blinding back into $\mathbb{F}_r$. + +**Soft recovery.** Every subsequent inbound confidential transfer or operator transfer, once merged, adds a fresh $\mathbb{F}_r$-derived blinding. For each transfer-derived addend, the new canonical $\mathbb{F}_q$ representative falls in $[0, r)$ with probability $\geq 1 - 2^{-127}$ regardless of the current stuck value (worst case: the current value sits at the lower edge of $[r, q)$, requiring the new $\mathbb{F}_r$ addend to cross the mod-$q$ boundary; the probability of failing to do so is bounded by $(q - r)/r \approx 2^{-127}$). For accounts that continue to receive confidential transfers, the unspendable window is self-resolving at the next merge with overwhelming probability; accounts whose only inflows are deposits remain stuck until a confidential transfer arrives. + +### 10.5 Verification Flow + +1. Wrapper reads on-chain state (commitments, public keys) +2. Encodes state as public inputs: Grumpkin point coordinates as 32-byte $\mathbb{F}_r$ values +3. Cross-contract call: `verifier.verify_proof(circuit_type, public_inputs, proof)` +4. Verifier deserializes stored VK, runs UltraHonk verification (BN254 G1/G2 pairings, Fiat-Shamir, sumcheck) +5. Wrapper applies homomorphic balance updates (Grumpkin point arithmetic via $\mathbb{F}_r$ ops) + +### 10.6 Structured Reference String {#srs} + +UltraHonk is a PLONK-family proving system. Its knowledge soundness guarantee depends on a **Structured Reference String (SRS)** -- a sequence of BN254 G1 and G2 points derived from a secret scalar $\tau$ (the "toxic waste"): + +$$\text{SRS} = \bigl([1]_1, [\tau]_1, [\tau^2]_1, \ldots, [\tau^{N-1}]_1, \; [1]_2, [\tau]_2\bigr)$$ + +where $[x]_1 = x \cdot G_1$ and $[x]_2 = x \cdot G_2$ are BN254 group elements. The SRS is **universal**: a single SRS supports any circuit up to size $N$, and circuit-specific verification keys are derived from it deterministically. The SRS is used during both proof generation (client-side) and verification key derivation (one-time setup). + +**Security requirement.** If $\tau$ is known to an attacker, they can forge proofs for arbitrary false statements: minting tokens, draining accounts, bypassing all circuit constraints. The knowledge soundness of the entire system reduces to the assumption that $\tau$ was destroyed after SRS generation. + +**Multi-party ceremony.** The standard mitigation is a multi-party computation (MPC) ceremony in which $N$ participants each contribute randomness. The resulting SRS is secure if *at least one* participant honestly destroyed their contribution. + +**SRS used in this system.** The Noir/Barretenberg toolchain uses the **Aztec Ignition SRS** by default. Barretenberg downloads the required SRS points from a public transcript on first use. The Ignition ceremony transcript, participant attestations, and verification code are publicly available. The SRS supports circuits up to $2^{28}$ gates, well above the expected circuit sizes for this system ($< 2^{20}$). + +**Deployment considerations.** The verifier contract does not store or reference the full SRS. Circuit-specific verification keys are derived offline from the SRS during circuit compilation and embedded in the verifier contract at deployment. The correctness of these VKs can be independently verified by anyone with access to the circuit source code and the public SRS transcript. + +**Risk assessment.** The Ignition ceremony had 176 independent participants across multiple jurisdictions, hardware platforms, and operating systems. Compromise requires collusion of *all* 176 participants. + +### 10.7 Dependency: CAP-80 + +[CAP-80](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0080.md) introduces host functions required for efficient UltraHonk verification and on-chain Grumpkin point arithmetic: + +- `bn254_g1_msm`: Batched scalar-point multiplication on BN254 G1. +- `bn254_fr_{add, sub, mul, inv, pow}`: $\mathbb{F}_r$ scalar arithmetic. + +### 10.8 On-Chain Point Arithmetic + +The wrapper performs Grumpkin affine point addition and subtraction for homomorphic balance updates. Since Grumpkin coordinates are $\mathbb{F}_r^{\text{BN254}}$ elements, these reduce to Fr field operations. + +**Curve coefficients.** Grumpkin $y^2 = x^3 - 17$ (Section 2.2) is in short Weierstrass form $y^2 = x^3 + a x + b$ with $a = 0$ and $b = -17$. Only $a$ enters the point arithmetic slope formulas below; $b$ enters only the on-curve check. + +The contract distinguishes the following cases when computing $P_3 = P_1 + P_2$: + +| Case | Condition | Result | +|:--|:--|:--| +| Left identity | $P_1 = \mathcal{O}$ | $P_3 = P_2$ | +| Right identity | $P_2 = \mathcal{O}$ | $P_3 = P_1$ | +| Inverse | $P_1, P_2 \neq \mathcal{O}$, $x_1 = x_2$, $y_1 = -y_2 \bmod r$ | $P_3 = \mathcal{O}$ | +| Doubling | $P_1, P_2 \neq \mathcal{O}$, $P_1 = P_2$ (so $y_1 \neq 0$) | slope formula with $\lambda_{\text{dbl}}$ below | +| Generic | $P_1, P_2 \neq \mathcal{O}$, $x_1 \neq x_2$ | slope formula with $\lambda_{\text{add}}$ below | + +The inverse case must be detected and short-circuited before the generic slope formula, because $x_1 - x_2 = 0$ would otherwise force a division by zero in $\mathbb{F}_r$. + +**Slope.** + +$$\lambda_{\text{add}} = (y_2 - y_1)(x_2 - x_1)^{-1} \pmod{r}$$ + +$$\lambda_{\text{dbl}} = (3 x_1^2 + a)(2 y_1)^{-1} = 3 x_1^2 \cdot (2 y_1)^{-1} \pmod{r} \qquad (a = 0 \text{ for Grumpkin})$$ + +**Resulting coordinates.** With $\lambda$ selected per the case above: + +$$x_3 = \lambda^2 - x_1 - x_2 \pmod{r}$$ +$$y_3 = \lambda (x_1 - x_3) - y_1 \pmod{r}$$ + +Requires `bn254_fr_{add, sub, mul, inv}` host calls (CAP-80, Section 10.7). + +**Point subtraction** $P_3 = P_1 - P_2$: if $P_2 = \mathcal{O}$ set $-P_2 = \mathcal{O}$, else $-P_2 = (x_2, -y_2 \bmod r)$; then apply the addition cases above. Subtraction of a point from itself yields $\mathcal{O}$ via the inverse case, never the doubling branch. + +**Point validation.** Grumpkin points enter the system through three boundaries; on-curve and non-identity checks live at the boundary that owns each one. The wrapper itself performs no per-call on-curve check. + +1. **Proof-constrained points (the dominant case).** Every public input that the corresponding circuit also derives via `multi_scalar_mul` is on-curve by construction -- Noir's embedded-curve operations cannot produce an off-curve Grumpkin point. This covers $Y$ (R1), $\text{PVK}$ (R3), $R_e$ (T6, O6, W_a1, S_a1, V_a1), $C_{\text{tx}}$ (T8, O8), $C_{\text{spend}}'$ (T11, W6, S10, V7), $C_a$ / $C_a'$ (S7, O11), and the ECDH shared secrets. Non-identity is enforced *in-circuit* by explicit nonzero-scalar constraints: $sk \neq 0$ and $vk \neq 0$ at registration (R4, R5), and $r_e \neq 0$ in every circuit that produces an ephemeral key (W8, T13, S13, O13, V10). Without these constraints an adversary could publish $Y = \mathcal{O}$, $\text{PVK} = \mathcal{O}$, or $R_e = \mathcal{O}$ and collapse ECDH (every shared secret becomes $\mathcal{O}$, every Poseidon mask becomes a constant function of $\sigma$, every ciphertext becomes trivially decryptable). +2. **Points read from prior on-chain state.** $C_{\text{spend}}$, $C_{\text{receive}}$, stored $Y$ / $\text{PVK}$, and allowance commitments were validated through path (1) when first written. The wrapper trusts them on subsequent reads. +3. **Auditor keys (the only proof-less entry point).** $K_{\text{aud}}$ is registered in the auditor contract by the auditor itself, with no accompanying proof. The auditor contract performs canonical encoding, on-curve ($y^2 \equiv x^3 - 17 \pmod{r}$), and non-identity checks at insertion (Section 3.1); the wrapper trusts the fetched value. + +**Canonical encoding** ($x, y \in [0, r)$ as 32-byte representatives) is enforced at the XDR / Soroban host boundary when bytes are deserialized into `BnScalar`; no additional check is needed inside the wrapper. + +--- + +## 11. Interface + +Based on [EIP-7984](https://eips.ethereum.org/EIPS/eip-7984), adapted for Soroban. The `data: Bytes` parameter carries XDR-encoded proof payloads. + +**Canonical encoding.** The `data` payloads are `#[contracttype]` structs and enums declared in the wrapper crate. Their on-chain byte representation is fixed by Soroban's XDR rules, which are canonical: every value has exactly one valid byte encoding. Named struct fields are serialised as an `ScMap` in declaration order; unnamed fields and tuple-enum variants as an `ScVec` in declaration order; map keys are host-enforced into a canonical sorted form. As a consequence, independent implementations that compile against the same `#[contracttype]` definitions produce byte-identical `data` payloads. The authoritative schemas for the underlying `ScVal`, `ScMap`, `ScVec` live in the [stellar/stellar-xdr](https://github.com/stellar/stellar-xdr) repository; the encoding rules are summarised in the [Stellar XDR documentation](https://developers.stellar.org/docs/learn/fundamentals/data-format/xdr) and the [`#[contracttype]` mapping reference](https://developers.stellar.org/docs/learn/fundamentals/contract-development/types/custom-types). + +```rust +trait ConfidentialTokenWrapper { + fn __constructor(e: Env, admin: Address, token: Address, + verifier: Address, auditor: Address); + + fn register(e: Env, account: Address, auditor_id: u32, data: Bytes); + + fn deposit(e: Env, from: Address, to: Address, amount: i128); + + fn merge(e: Env, account: Address); + + fn withdraw(e: Env, from: Address, to: Address, amount: i128, data: Bytes); + + fn confidential_transfer(e: Env, from: Address, to: Address, data: Bytes); + + fn confidential_transfer_from(e: Env, operator: Address, + from: Address, to: Address, data: Bytes); + + fn set_operator(e: Env, account: Address, operator: Address, + expiration_ledger: u32, data: Bytes); + + fn revoke_operator(e: Env, account: Address, operator: Address, + data: Bytes); + + fn confidential_balance(e: Env, account: Address) -> Bytes; + + fn is_operator(e: Env, account: Address, operator: Address) -> bool; + + fn get_operator(e: Env, account: Address, operator: Address) -> Bytes; +} +``` + +This table is authoritative: every entry is exactly the set of prover-supplied public inputs from the corresponding Section 7 operation (the wrapper loads the remaining public inputs from trusted state per ยง7.1), plus the `proof` blob. Names map directly to the Section 7 symbols. + +| Operation | `data` contents | +|:---|:---| +| `register` | $Y$, $\text{PVK}$, `proof` | +| `withdraw` | $C_{\text{spend}}'$, $\tilde{b}$, $R_e$, $\sigma$, $\tilde{b}_{\text{aud,s}}$, `proof` | +| `confidential_transfer` | $C_{\text{spend}}'$, $C_{\text{tx}}$, $R_e$, $\tilde{v}$, $\tilde{b}$, $\sigma$, $\tilde{v}_{\text{aud,r}}$, $\tilde{r}_{\text{aud,r}}$, $\tilde{v}_{\text{aud,s}}$, $\tilde{b}_{\text{aud,s}}$, `proof` | +| `confidential_transfer_from` | $C_a'$, $C_{\text{tx}}$, $R_e$, $\tilde{v}$, $\tilde{a}'$, $\sigma_a'$, $\tilde{v}_{\text{aud,r}}$, $\tilde{r}_{\text{aud,r}}$, $\tilde{v}_{\text{aud,s}}$, $\tilde{a}_{\text{aud,s}}$, `proof` | +| `set_operator` | $C_{\text{spend}}'$, $C_a$, $\text{escrowed\\\_dvk}$, $\tilde{b}$, $\tilde{a}$, $R_e$, $\sigma$, $\sigma_a$, $\tilde{v}_{\text{aud,s}}$, $\tilde{b}_{\text{aud,s}}$, `proof` | +| `revoke_operator` | $C_{\text{spend}}'$, $\tilde{b}$, $R_e$, $\sigma$, $\tilde{v}_{\text{aud,s}}$, $\tilde{b}_{\text{aud,s}}$, `proof` | + +For `confidential_transfer_from`, the stored allowance salt $\sigma_a$ is **not** carried in `data`: the wrapper loads it from the `(from, operator)` delegation entry (ยง7.8 public-input table). Only the prover-chosen replacement $\sigma_a'$ travels in `data`, gets bound by constraint O10, and is then written back to the delegation entry as the new `allowance_salt` (ยง6.2). This keeps the trust-boundary rule of ยง7.1 intact: caller-controlled bytes never overwrite the live $\sigma_a$ used to verify the proof. `set_operator`, by contrast, has no prior delegation entry to load from, so its $\sigma_a$ is prover-supplied and bound by S6. + +### 11.1 Authorization Model + +Soroban `address.require_auth()` proves that the named principal authorized the current invocation; it binds the full invocation (function name and all arguments) by default. ZK proof verification proves that the prover knows a witness satisfying the circuit's constraints over public inputs the wrapper itself supplies. The two are complementary: every state-changing operation requires **both** the appropriate `require_auth()` and (where applicable) a valid proof. + +| Operation | `require_auth()` principal | +|:---|:---| +| `register(account, auditor_id, data)` | `account` | +| `deposit(from, to, amount)` | `from` | +| `merge(account)` | `account` | +| `withdraw(from, to, amount, data)` | `from` | +| `confidential_transfer(from, to, data)` | `from` | +| `confidential_transfer_from(operator, from, to, data)` | `operator` (not `from`) | +| `set_operator(account, operator, expiration_ledger, data)` | `account` | +| `revoke_operator(account, operator, data)` | `account` | +| `confidential_balance`, `is_operator`, `get_operator` | none (read-only) | + +**`register` is single-use.** It reverts if `account` is already registered. Combined with `account.require_auth()`, this prevents a third party from binding attacker-controlled $(Y, \text{PVK})$ to `account`'s `ConfidentialAccount` slot. + +**`set_operator` rejects replacement.** It reverts if a non-revoked delegation already exists for `(account, operator)` -- see ยง6.2. + +**`confidential_transfer_from` is operator-authorized.** The owner's authorization was granted out-of-band at `set_operator` and persists in the on-chain delegation entry until expiry or revocation. The operator's `require_auth()` binds `from`, `to`, and `data`. + +### 11.2 Event Schema + +Each state-modifying operation emits a structured event. Events carry the data needed for recipient decryption, auditor decryption, and wallet recovery. + +| Event | Fields | +|:---|:---| +| `Register` | `account`, `auditor_id` | +| `Deposit` | `from`, `to`, `amount` | +| `Merge` | `account` | +| `Withdraw` | `from`, `to`, `amount`, $R_e$, $\sigma$, $\tilde{b}$, $\tilde{b}_{\text{aud,s}}$ | +| `Transfer` | `from`, `to`, $R_e$, $\tilde{v}$, $\sigma$, $\tilde{b}$, $\tilde{v}_{\text{aud,r}}$, $\tilde{r}_{\text{aud,r}}$, $\tilde{v}_{\text{aud,s}}$, $\tilde{b}_{\text{aud,s}}$ | +| `OperatorTransfer` | `operator`, `from`, `to`, $R_e$, $\tilde{v}$, $\sigma_a$, $\tilde{v}_{\text{aud,r}}$, $\tilde{r}_{\text{aud,r}}$, $\tilde{v}_{\text{aud,s}}$, $\tilde{a}_{\text{aud,s}}$ | +| `SetOperator` | `account`, `operator`, `expiration_ledger`, $R_e$, $\sigma$, $\tilde{b}$, $\tilde{v}_{\text{aud,s}}$, $\tilde{b}_{\text{aud,s}}$ | +| `RevokeOperator` | `account`, `operator`, $R_e$, $\sigma$, $\tilde{b}$, $\tilde{v}_{\text{aud,s}}$, $\tilde{b}_{\text{aud,s}}$ | + +Amount fields in `Deposit` and `Withdraw` are typed `i128`, matching SEP-41. + +**Usage by consumers:** + +- **Recipient wallet**: processes `Transfer` and `OperatorTransfer` events using $(R_e, \tilde{v}, \sigma)$ to derive $v_{\text{tx}}$ and $r_{\text{tx}}$ (Section 5.3). +- **Owner wallet**: processes all events for recovery (Section 5.2). The $(\tilde{b}, \sigma)$ pair from the most recent owner-initiated event forms a checkpoint. +- **Auditor**: processes events containing $R_e$ to compute ECDH shared secrets and decrypt amounts and balance checkpoints (Section 8.1, 8.2). + +### 11.3 Read Methods + +**`confidential_balance(account) -> Bytes`.** Returns the XDR-serialized `ConfidentialAccount` struct for the given account (ยง6.1), i.e. the tuple `(spending_key, viewing_public_key, spendable_balance, receiving_balance, auditor_id)`. Reverts if `account` is not registered. Wallets bootstrap from this call (single round-trip to obtain both Pedersen commitments plus the keys needed to identify the account and its bound auditor); indexers use it to verify consistency between their replayed accumulators and on-chain state (ยง5.2 "Consistency check"). + +**`is_operator(account, operator) -> bool`.** Returns `true` iff a delegation entry exists for `(account, operator)` **and** `ledger.sequence() <= expiration_ledger`. Returns `false` for: + +- pairs with no delegation entry, +- pairs whose entry has `ledger.sequence() > expiration_ledger` (expired-but-not-yet-revoked: the escrowed value still resides on-chain in $C_a$ until `revoke_operator` reclaims it -- ยง6.2 *Single-slot semantics* -- but the operator can no longer spend), +- pairs whose entry was revoked (deleted) by `revoke_operator`. + +The function returns the *spending-authority* state, not the *escrow-existence* state. Consumers that need to distinguish "no delegation" from "expired delegation" inspect `get_operator` (below) or replay `SetOperator` / `RevokeOperator` events. + +**`get_operator(account, operator) -> Bytes`.** Returns the XDR-serialized `OperatorDelegation` struct (ยง6.2) for the `(account, operator)` pair, i.e. `(allowance_commitment, encrypted_allowance, escrowed_dvk, allowance_salt, expiration_ledger)`. Reverts if no delegation entry exists for the pair. Unlike `is_operator`, this surfaces the raw on-chain delegation state without applying the expiry filter, so callers can distinguish "no delegation" (revert) from "active delegation" (`ledger.sequence() <= expiration_ledger`) from "expired-but-not-yet-revoked delegation" (`ledger.sequence() > expiration_ledger`, escrowed value still pending reclaim). Primary consumers: + +- **Operator wallet:** fetches `allowance_commitment`, `encrypted_allowance`, `escrowed_dvk`, and `allowance_salt` to recover $dvk_i$ via ยง7.11 decryption, then reads the current allowance via $\tilde{a} = v_a + \text{Poseidon}(\delta_{\text{enc\\\_allow}}, dvk_i, \sigma_a)$ to construct the next `confidential_transfer_from` witness. +- **Owner wallet:** reads the same fields after losing local state, or before calling `revoke_operator`, to confirm the on-chain entry matches its records. +- **Indexers:** verify their replayed delegation state against the live commitment, in the same way `confidential_balance` is used for account state (ยง5.2 "Consistency check"). + +The auditor's allowance tracking does **not** use this method: per-event allowance ciphertexts (ยง8.5) are the auditor's data path; `encrypted_allowance` is keyed to $dvk_i$ and is unreadable without it. + +--- + +## 12. Dependencies + +| Dependency | Status | Impact | +|:---|:---|:---| +| **Protocol 25** (BN254 native support) | Available | `bn254.g1_add()`, `g1_mul()`, `pairing_check()`, `BnScalar` Fr arithmetic | +| **CAP-80** (BN254 host functions) | Available | Required for efficient UltraHonk verification and Grumpkin point arithmetic | +| **Modified UltraHonk verifier** | To be built | Multi-VK support (one per circuit type) | +| **Noir circuits** | To be built | 6 circuits using `std::embedded_curve_ops` for Grumpkin | +| **Grumpkin point arithmetic library** | To be built | On-chain point add/sub using BN254 Fr ops, identity handling | +| **Auditor contract** | To be built | Independent key management contract | +| **Nargo / Barretenberg** | Available (`nargo 1.0.0-beta.11`, `bb v0.87.0`) | Off-chain proof generation | +| **Client library** | To be built | ECDH key agreement, Poseidon-based amount encryption/decryption, event processing, off-chain balance tracking | + +--- + +## 13. Domain Separation Constants + +Each $\delta$ is a small positive integer in $\mathbb{F}_r$, fixed for the protocol version and used as a Poseidon2 leading-input domain tag. Numeric values are assigned sequentially from 1; the protocol version is implicit in the deployment, and any change to a circuit's constraint that uses these tags requires a new deployment with a fresh verification key (ยง3.5, ยง10.6). + +| $\delta$ | Value | Context | +|:---|:---:|:---| +| $\delta_{\text{addr}}$ | 1 | Soroban Address compression into a single $\mathbb{F}_r$ Field (ยง2.7) | +| $\delta_{\text{vk}}$ | 2 | Viewing key derivation from spending key and wrapper address (ยง4.2) | +| $\delta_{\text{dvk}}$ | 3 | Delegation viewing key derivation (ยง4.4) | +| $\delta_{\text{spend\\\_r}}$ | 4 | Deterministic randomness for spendable balance commitments (ยง5.2 *Update rules*) | +| $\delta_{\text{tx\\\_blind}}$ | 5 | ECDH-derived transfer blinding factor (ยง5.3 Definition 1) | +| $\delta_{\text{tx\\\_amount}}$ | 6 | ECDH-derived transfer amount encryption (ยง5.3 Definition 1) | +| $\delta_{\text{enc\\\_bal}}$ | 7 | Encrypted balance scalar masking (ยง5.5) | +| $\delta_{\text{enc\\\_allow}}$ | 8 | Encrypted allowance scalar masking (ยง6.2 *encrypted\_allowance*) | +| $\delta_{\text{allow\\\_r}}$ | 9 | Deterministic randomness for operator allowance commitments (ยง6.2 *allowance\_commitment*) | +| $\delta_{\text{esc\\\_dvk}}$ | 10 | Delegation key escrow (operator ECDH) (ยง7.11) | +| $\delta_{\text{aud\\\_s}}$ | 11 | Sender / owner-auditor channel sponge (ยง2.5, ยง8.1) | +| $\delta_{\text{aud\\\_r}}$ | 12 | Recipient-auditor channel sponge (ยง2.5, ยง8.1) | + +**Provenance.** Sequential small integers are the simplest assignment that satisfies the requirement of *distinctness* across all Poseidon2 invocations in this protocol -- Poseidon2 is collision-resistant under the assumption of ยง3.2, so any two distinct leading inputs (independent of size) produce independent outputs. The values themselves carry no semantic meaning; the binding is purely positional and the table is the only authoritative source. Implementations MUST hardcode these exact numeric values; deviations break cross-implementation derivation of $vk$, $dvk_i$, $\tilde{v}$, $\tilde{b}$, $\tilde{a}$, $r_{\text{tx}}$, $r_a$, and all auditor masks. + +**Cross-protocol collision.** Future protocols that share Grumpkin / BN254 / Poseidon2 with this wrapper -- e.g. an unrelated payments protocol that uses small-integer Poseidon2 domains -- could in principle pick the same numeric values for unrelated purposes. The protocol assumes that the surrounding inputs to Poseidon2 (key material, structural witnesses) sufficiently disambiguate even in such a case; no Poseidon2 invocation in this protocol is keyed solely on a $\delta$ value. If stronger isolation is desired, implementers may instead use the alternate scheme $\delta_X = \text{Poseidon2}(0, \text{ASCII}(\text{"openzeppelin/confidential-token-wrapper/v1:X"}))$, but this is a deployment-time choice that must be applied uniformly and disclosed in the deployment's circuit-binding documentation. From 1400304e638b7638ae9d2251febd1e5bbc84c016 Mon Sep 17 00:00:00 2001 From: brozorec <9572072+brozorec@users.noreply.github.com> Date: Wed, 27 May 2026 09:15:07 +0200 Subject: [PATCH 10/16] fix: domains visibility --- .../src/confidential/circuits/lib/src/lib.nr | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/tokens/src/confidential/circuits/lib/src/lib.nr b/packages/tokens/src/confidential/circuits/lib/src/lib.nr index cbafc8c26..beb16401c 100644 --- a/packages/tokens/src/confidential/circuits/lib/src/lib.nr +++ b/packages/tokens/src/confidential/circuits/lib/src/lib.nr @@ -89,53 +89,53 @@ global POSEIDON2_IV_BASE: Field = 18446744073709551616; // 2^64 /// | `delta_esc_dvk` | 10 | `ESCROWED_DELEGATION_VIEWING_KEY` | /// | `delta_aud_s` | 11 | `AUDITOR_SENDER` | /// | `delta_aud_r` | 12 | `AUDITOR_RECIPIENT` | -mod domain { +pub mod domain { /// Soroban Address compression into a single `F_r` Field: /// `address_to_field(a) = Poseidon2(ADDRESS, lo(a), hi(a))`. /// Section 2.7 (`delta_addr`). The wrapper computes `wrap` and `op_i` /// off-circuit via this tag; circuits consume the resulting Field directly. - pub(crate) global ADDRESS: Field = 1; + pub global ADDRESS: Field = 1; /// Viewing-key derivation: `vk = Poseidon2(VIEWING_KEY, sk, wrap)`. /// Section 4.2 (`delta_vk`). - pub(crate) global VIEWING_KEY: Field = 2; + pub global VIEWING_KEY: Field = 2; /// Delegation viewing-key derivation: /// `dvk = Poseidon2(DELEGATION_VIEWING_KEY, vk, op_i)`. Section 4.4 (`delta_dvk`). - pub(crate) global DELEGATION_VIEWING_KEY: Field = 3; + pub global DELEGATION_VIEWING_KEY: Field = 3; /// Deterministic randomness for the new spendable balance: /// `r' = Poseidon2(SPEND_RANDOMNESS, vk, sigma)`. Constraints W5/T10/S9/V6 (`delta_spend_r`). - pub(crate) global SPEND_RANDOMNESS: Field = 4; + pub global SPEND_RANDOMNESS: Field = 4; /// ECDH-derived blinding for the transfer commitment: /// `r_tx = Poseidon2(TX_BLINDING, s, sigma)`. Constraints T7/O7 (`delta_tx_blind`). - pub(crate) global TX_BLINDING: Field = 5; + pub global TX_BLINDING: Field = 5; /// Mask for the encrypted transfer amount: /// `v_tilde = v_tx + Poseidon2(TX_AMOUNT, s, sigma)`. Constraints T9/O9 (`delta_tx_amount`). - pub(crate) global TX_AMOUNT: Field = 6; + pub global TX_AMOUNT: Field = 6; /// Mask for the encrypted balance scalar: /// `b_tilde = v_new + Poseidon2(ENCRYPTED_BALANCE, vk, sigma)`. /// Constraints W7/T12/S11/V8 (`delta_enc_bal`). - pub(crate) global ENCRYPTED_BALANCE: Field = 7; + pub global ENCRYPTED_BALANCE: Field = 7; /// Mask for the encrypted allowance scalar: /// `a_tilde = v_a + Poseidon2(ENCRYPTED_ALLOWANCE, dvk, sigma_a)`. /// Constraints S8/O12 (`delta_enc_allow`). - pub(crate) global ENCRYPTED_ALLOWANCE: Field = 8; + pub global ENCRYPTED_ALLOWANCE: Field = 8; /// Deterministic randomness for the operator allowance: /// `r_a = Poseidon2(ALLOWANCE_RANDOMNESS, dvk, sigma_a)`. /// Constraints S6/O3/O10 (`delta_allow_r`). - pub(crate) global ALLOWANCE_RANDOMNESS: Field = 9; + pub global ALLOWANCE_RANDOMNESS: Field = 9; /// Delegation-key escrow mask (operator ECDH): /// `Poseidon2(ESCROWED_DELEGATION_VIEWING_KEY, s, op_i)`. /// Constraint S12, Section 7.11 (`delta_esc_dvk`). - pub(crate) global ESCROWED_DELEGATION_VIEWING_KEY: Field = 10; + pub global ESCROWED_DELEGATION_VIEWING_KEY: Field = 10; /// Sender or owner-auditor channel tag for Poseidon2 sponge masks /// (Section 2.5, Section 8.1). Squeeze 1 yields the amount mask (where /// applicable); squeeze 2 yields the balance/allowance checkpoint mask. /// Constraints W_a3 / T_a6 / S_a3 / V_a3 / O_a6 (`delta_aud_s`). - pub(crate) global AUDITOR_SENDER: Field = 11; + pub global AUDITOR_SENDER: Field = 11; /// Recipient-auditor channel tag for Poseidon2 sponge masks /// (Section 2.5, Section 8.1). Squeeze 1 yields the amount mask; squeeze /// 2 yields the per-transfer Pedersen randomness mask. /// Constraints T_a2 / O_a2 (`delta_aud_r`). - pub(crate) global AUDITOR_RECIPIENT: Field = 12; + pub global AUDITOR_RECIPIENT: Field = 12; } // ################## CORE PRIMITIVES ################## From 66e9903d0a3a8648bf284a775bf9910c50be77ce Mon Sep 17 00:00:00 2001 From: brozorec <9572072+brozorec@users.noreply.github.com> Date: Wed, 27 May 2026 11:22:51 +0200 Subject: [PATCH 11/16] feat(confidential): operator-transfer circuit (O1-O13, O_a1-O_a8) 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. --- .../src/confidential/circuits/Nargo.toml | 1 + .../circuits/operator_transfer/Nargo.toml | 8 + .../circuits/operator_transfer/src/main.nr | 314 ++++ .../circuits/operator_transfer/src/tests.nr | 1485 +++++++++++++++++ 4 files changed, 1808 insertions(+) create mode 100644 packages/tokens/src/confidential/circuits/operator_transfer/Nargo.toml create mode 100644 packages/tokens/src/confidential/circuits/operator_transfer/src/main.nr create mode 100644 packages/tokens/src/confidential/circuits/operator_transfer/src/tests.nr diff --git a/packages/tokens/src/confidential/circuits/Nargo.toml b/packages/tokens/src/confidential/circuits/Nargo.toml index 49acde248..0d0fa25a6 100644 --- a/packages/tokens/src/confidential/circuits/Nargo.toml +++ b/packages/tokens/src/confidential/circuits/Nargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ "lib", + "operator_transfer", "register", "withdraw", "gadgets/assert_on_curve", diff --git a/packages/tokens/src/confidential/circuits/operator_transfer/Nargo.toml b/packages/tokens/src/confidential/circuits/operator_transfer/Nargo.toml new file mode 100644 index 000000000..dcff4c8e1 --- /dev/null +++ b/packages/tokens/src/confidential/circuits/operator_transfer/Nargo.toml @@ -0,0 +1,8 @@ +[package] +name = "circuit_operator_transfer" +type = "bin" +authors = ["OpenZeppelin"] +compiler_version = ">=0.30.0" + +[dependencies] +stellar_confidential_lib = { path = "../lib" } diff --git a/packages/tokens/src/confidential/circuits/operator_transfer/src/main.nr b/packages/tokens/src/confidential/circuits/operator_transfer/src/main.nr new file mode 100644 index 000000000..ef565b599 --- /dev/null +++ b/packages/tokens/src/confidential/circuits/operator_transfer/src/main.nr @@ -0,0 +1,314 @@ +use stellar_confidential_lib::{ + H, assert_on_curve_non_identity, commit, derive_allow_r, derive_tx_blind, domain, ecdh, + encrypt_allowance, encrypt_amount, scalar_mul, sponge_squeeze_2, +}; +use std::embedded_curve_ops::EmbeddedCurvePoint; + +mod tests; + +// OperatorTransfer circuit -- design doc Section 7.8. +// +// Constraints +// ----------- +// Operator key ownership +// O1 Y_op = sk_op * H Operator key +// ownership. +// Allowance state (current) +// O2 C_a = v_a * G + r_a * H Opening of the +// stored allowance +// commitment. +// O3 r_a = Poseidon2(delta_allow_r, dvk_i, +// sigma_a) Allowance +// randomness matches +// stored state (the +// wrapper-binding +// anchor, see below). +// O4 v_a, v_tx, v_a - v_tx in [0, 2^127) Range validity +// (Section 2.6); the +// [0, 2^127) bound is +// the SEP-41 i128 +// non-negative range +// (Section 3.4). +// Recipient ECDH + transfer commitment (Section 5.3, 5.4) +// O5 S = r_e * PVK_recipient Recipient ECDH +// shared secret. +// O6 R_e = r_e * H Ephemeral public +// key. +// O7 r_tx = Poseidon2(delta_tx_blind, S.x, +// sigma_a) Anti-poisoning +// binding for the +// transfer blinding. +// O8 C_tx = v_tx * G + r_tx * H Transfer +// commitment. +// O9 v_tilde = v_tx +// + Poseidon2(delta_tx_amount, S.x, +// sigma_a) Encrypted amount +// (emitted). +// Allowance state (new) +// O10 r_a' = Poseidon2(delta_allow_r, dvk_i, +// sigma_a') New allowance +// randomness. +// O11 C_a' = (v_a - v_tx) * G + r_a' * H New allowance +// commitment. +// O12 a_tilde' = (v_a - v_tx) +// + Poseidon2(delta_enc_allow, dvk_i, +// sigma_a') Encrypted new +// allowance scalar +// (emitted). +// Nonzero ephemeral +// O13 r_e != 0 Rules out R_e = O +// and S, S_{a,r}, +// S_{a,s} = O. +// Auditor block (dual-channel visibility, Section 8.1 + Section 8.4) +// O_a1 S_{a,r} = r_e * K_aud_r Recipient-auditor +// ECDH shared secret +// (reuses r_e). +// O_a2 (m_v_r, m_r_r) +// = SpongeSqueeze_2(delta_aud_r, +// S_{a,r}.x, sigma_a) Recipient-channel +// sponge: two masks. +// O_a3 v_tilde_aud_r = v_tx + m_v_r Recipient-auditor +// encrypted amount. +// O_a4 r_tilde_aud_r = r_tx + m_r_r Recipient-auditor +// encrypted transfer +// randomness. +// O_a5 S_{a,s} = r_e * K_aud_s Owner-auditor ECDH +// shared secret +// (reuses r_e); the +// sender-channel +// visibility points +// at the funds' +// OWNER, not the +// operator (Section +// 8.4). +// O_a6 (m_v_s, m_a_s) +// = SpongeSqueeze_2(delta_aud_s, +// S_{a,s}.x, sigma_a) Owner-channel +// sponge: two masks. +// O_a7 v_tilde_aud_s = v_tx + m_v_s Owner-auditor +// encrypted amount. +// O_a8 a_tilde_aud_s = (v_a - v_tx) + m_a_s Owner-auditor +// encrypted +// post-transfer +// allowance. +// +// Wrapper binding (Section 7.8 final paragraph) +// --------------------------------------------- +// Unlike owner-initiated circuits, OperatorTransfer does NOT constrain the vk +// derivation -- the operator has no access to the owner's sk. Wrapper binding +// is inherited indirectly through the allowance commitment chain: SetOperator +// derived `dvk_i` from the wrapper-specific `vk` (S2, S5), which determined +// `r_a` (S6) and thus `C_a` (S7). Here O3 verifies the operator's claimed +// `dvk_i` against the on-chain `C_a` via `sigma_a`. Because `C_a` is a public +// input and was constructed with wrapper-specific randomness, a proof +// generated against one wrapper's `C_a` cannot verify against another's. +// +// Channel-nonce reuse (Section 6.2 *Dual role*) +// --------------------------------------------- +// `sigma_a` serves as the freshness nonce for THREE distinct uses in this +// circuit: the allowance-randomness Poseidon (O3), the recipient ECDH chain +// (O7 / O9), and both auditor-channel sponges (O_a2 / O_a6). Soundness derives +// from ECDH shared-secret unpredictability (S.x is unknown to anyone but the +// recipient and the prover); `sigma_a` itself is public and need not be +// secret. Only the new-allowance constraints O10 and O12 use the fresh +// `sigma_a'`. +// +// Point-validation doctrine (Section 10.8) +// ---------------------------------------- +// Y_op, C_a, C_a', C_tx, R_e, S, S_{a,r}, and S_{a,s} are bound to in-circuit +// multi_scalar_mul outputs (O1, O2, O11, O8, O6, O5, O_a1, O_a5) and are +// therefore on-curve by construction -- no explicit check needed. +// PVK_recipient, K_aud_r, and K_aud_s are public-input keys consumed by ECDH +// multiplications (Section 10.8 path (3)); the verifier doesn't check curve +// membership and an off-curve key would break the soundness of O5 / O_a1 / +// O_a5. This file explicitly validates all three on-curve AND non-identity +// before the corresponding ECDH consumes them. (PVK_recipient is path (2) of +// Section 10.8 -- proof-constrained at the recipient's registration and +// trusted on read from `to.viewing_public_key` -- but the explicit check +// keeps the operator-side circuit defensive even if the recipient slot is +// ever loaded outside the wrapper.) +// +// Public inputs (24 fields, in design-doc canonical order) +// -------------------------------------------------------- +// Idx Param Symbol Source / Note +// --- ----- ------ ------------------------------- +// 0 c_a_x C_a.x Loaded from the +// 1 c_a_y C_a.y `(from, operator)` delegation +// entry's allowance_commitment. +// 2 sigma_a sigma_a Loaded from the delegation +// entry's allowance_salt. +// 3 y_op_x Y_op.x Loaded from +// 4 y_op_y Y_op.y `operator.spending_key`; +// matches the auth principal. +// 5 pvk_recipient_x PVK_recipient.x Loaded from +// 6 pvk_recipient_y PVK_recipient.y `to.viewing_public_key`. +// Recipient must be registered. +// 7 k_aud_r_x K_aud_r.x Fetched from the auditor +// 8 k_aud_r_y K_aud_r.y contract by `to.auditor_id`. +// 9 k_aud_s_x K_aud_s.x Fetched from the auditor +// 10 k_aud_s_y K_aud_s.y contract by the OWNER's +// `auditor_id`, not the +// operator's (Section 7.8). +// 11 c_a_new_x C_a'.x Prover-supplied; written to +// 12 c_a_new_y C_a'.y the delegation entry's new +// allowance_commitment. +// 13 c_tx_x C_tx.x Prover-supplied; added to +// 14 c_tx_y C_tx.y `to.receiving_balance`. +// 15 r_e_x R_e.x Prover-supplied ephemeral key; +// 16 r_e_y R_e.y emitted. +// 17 v_tilde v_tilde Prover-supplied encrypted +// transfer amount; emitted. +// 18 a_tilde_new a_tilde' Prover-supplied encrypted new +// allowance scalar; stored. +// 19 sigma_a_new sigma_a' Prover-supplied fresh salt; +// written to allowance_salt. +// 20 v_tilde_aud_r v_tilde_aud_r Prover-supplied recipient- +// auditor encrypted amount; +// emitted. +// 21 r_tilde_aud_r r_tilde_aud_r Prover-supplied recipient- +// auditor encrypted transfer +// randomness; emitted. +// 22 v_tilde_aud_s v_tilde_aud_s Prover-supplied owner-auditor +// encrypted amount; emitted. +// 23 a_tilde_aud_s a_tilde_aud_s Prover-supplied owner-auditor +// encrypted post-transfer +// allowance; emitted. +// +// Private witnesses +// ----------------- +// sk_op Operator's spending secret scalar. +// dvk_i Delegation viewing key for this (owner, operator) pair, pinned by +// O3 to Poseidon2(delta_allow_r, dvk_i, sigma_a) = r_a. +// v_a Plaintext current allowance value. +// r_a Plaintext blinding factor for C_a (single-limb F_r; pinned by O3). +// v_tx Plaintext transfer amount. +// r_e Ephemeral scalar for recipient + auditor ECDH; must satisfy +// r_e != 0 (O13). + +fn main( + sk_op: Field, + dvk_i: Field, + v_a: Field, + r_a: Field, + v_tx: Field, + r_e: Field, + c_a_x: pub Field, + c_a_y: pub Field, + sigma_a: pub Field, + y_op_x: pub Field, + y_op_y: pub Field, + pvk_recipient_x: pub Field, + pvk_recipient_y: pub Field, + k_aud_r_x: pub Field, + k_aud_r_y: pub Field, + k_aud_s_x: pub Field, + k_aud_s_y: pub Field, + c_a_new_x: pub Field, + c_a_new_y: pub Field, + c_tx_x: pub Field, + c_tx_y: pub Field, + r_e_x: pub Field, + r_e_y: pub Field, + v_tilde: pub Field, + a_tilde_new: pub Field, + sigma_a_new: pub Field, + v_tilde_aud_r: pub Field, + r_tilde_aud_r: pub Field, + v_tilde_aud_s: pub Field, + a_tilde_aud_s: pub Field, +) { + // O13 -- runs first so the r_e = 0 attack is rejected before any + // scalar mul against it could quietly produce the identity. + assert(r_e != 0); + + // O1 + let y_op_derived = scalar_mul(sk_op, H); + assert(y_op_derived.x == y_op_x); + assert(y_op_derived.y == y_op_y); + + // O3 -- runs before O2 so the wrapper-binding check (r_a is pinned to + // dvk_i, sigma_a) is in scope before O2 uses r_a as the blinding under + // the Pedersen commitment. + let r_a_derived = derive_allow_r(dvk_i, sigma_a); + assert(r_a_derived == r_a); + + // O2 + let c_a_derived = commit(v_a, r_a); + assert(c_a_derived.x == c_a_x); + assert(c_a_derived.y == c_a_y); + + // O4 -- Section 2.6 spells out the 127-bit decomposition / recomposition + // pattern; `assert_max_bit_size` is the Noir stdlib primitive that + // implements it directly. + v_a.assert_max_bit_size::<127>(); + v_tx.assert_max_bit_size::<127>(); + let v_a_new = v_a - v_tx; + v_a_new.assert_max_bit_size::<127>(); + + // O6 + let r_e_derived = scalar_mul(r_e, H); + assert(r_e_derived.x == r_e_x); + assert(r_e_derived.y == r_e_y); + + // O5 -- PVK_recipient validated on-curve + non-identity before consumption + // (Section 10.8 path (3) wrt this circuit; defensive even though the + // recipient's PVK was proof-constrained at registration, R3). + let pvk_recipient = + EmbeddedCurvePoint { x: pvk_recipient_x, y: pvk_recipient_y, is_infinite: false }; + assert_on_curve_non_identity(pvk_recipient); + let s_x = ecdh(r_e, pvk_recipient); + + // O7 (anti-poisoning, Section 5.4) + let r_tx = derive_tx_blind(s_x, sigma_a); + + // O8 + let c_tx_derived = commit(v_tx, r_tx); + assert(c_tx_derived.x == c_tx_x); + assert(c_tx_derived.y == c_tx_y); + + // O9 + let v_tilde_derived = encrypt_amount(v_tx, s_x, sigma_a); + assert(v_tilde_derived == v_tilde); + + // O10 + let r_a_new = derive_allow_r(dvk_i, sigma_a_new); + + // O11 + let c_a_new_derived = commit(v_a_new, r_a_new); + assert(c_a_new_derived.x == c_a_new_x); + assert(c_a_new_derived.y == c_a_new_y); + + // O12 + let a_tilde_new_derived = encrypt_allowance(v_a_new, dvk_i, sigma_a_new); + assert(a_tilde_new_derived == a_tilde_new); + + // K_aud_r / K_aud_s point validation (Section 10.8: public-input keys). + let k_aud_r = EmbeddedCurvePoint { x: k_aud_r_x, y: k_aud_r_y, is_infinite: false }; + assert_on_curve_non_identity(k_aud_r); + let k_aud_s = EmbeddedCurvePoint { x: k_aud_s_x, y: k_aud_s_y, is_infinite: false }; + assert_on_curve_non_identity(k_aud_s); + + // O_a1 (recipient-auditor shared secret x-coordinate) + let s_a_r_x = ecdh(r_e, k_aud_r); + + // O_a2 (recipient-channel masks: amount, then r_tx) + let m_r = sponge_squeeze_2(domain::AUDITOR_RECIPIENT, s_a_r_x, sigma_a); + + // O_a3 + assert(v_tx + m_r[0] == v_tilde_aud_r); + + // O_a4 + assert(r_tx + m_r[1] == r_tilde_aud_r); + + // O_a5 (owner-auditor shared secret x-coordinate) + let s_a_s_x = ecdh(r_e, k_aud_s); + + // O_a6 (owner-channel masks: amount, then post-transfer allowance) + let m_s = sponge_squeeze_2(domain::AUDITOR_SENDER, s_a_s_x, sigma_a); + + // O_a7 + assert(v_tx + m_s[0] == v_tilde_aud_s); + + // O_a8 + assert(v_a_new + m_s[1] == a_tilde_aud_s); +} diff --git a/packages/tokens/src/confidential/circuits/operator_transfer/src/tests.nr b/packages/tokens/src/confidential/circuits/operator_transfer/src/tests.nr new file mode 100644 index 000000000..be5112271 --- /dev/null +++ b/packages/tokens/src/confidential/circuits/operator_transfer/src/tests.nr @@ -0,0 +1,1485 @@ +use crate::main; +use stellar_confidential_lib::{ + commit, derive_allow_r, derive_tx_blind, domain, ecdh, encrypt_allowance, encrypt_amount, H, + scalar_mul, sponge_squeeze_2, +}; +use std::embedded_curve_ops::EmbeddedCurvePoint; + +// Canonical fixture inputs. +// +// SIGMA_A is the channel nonce reused across the allowance Poseidon (O3), +// the recipient ECDH chain (O7 / O9), and both auditor sponges (O_a2 / O_a6) +// -- see main.nr "Channel-nonce reuse". SIGMA_A_NEW is the fresh salt for the +// post-transfer allowance derivations only (O10, O12). V_A / V_TX / V_A_NEW +// give a non-trivial `v_a - v_tx` so O4 is exercised on a real subtraction. +// +// Y_OP_X / Y_OP_Y and the auditor / recipient keys share fixture values with +// withdraw/transfer (sk_op = 0xdead, K_aud_s/r and pvk scalars identical to +// transfer) so the three circuits share an ecosystem of pinned curve points +// for cross-circuit tooling. DVK_I is a placeholder field; in production it +// comes from S5 of SetOperator and the operator decrypts it from +// `escrowed_dvk` (Section 7.11). +global SK_OP: Field = 0xdead; +global DVK_I: Field = 0x44767669; // ASCII "Dvki" +global SIGMA_A: Field = 0x01; +global SIGMA_A_NEW: Field = 0x02; +global V_A: Field = 1000; +global V_TX: Field = 100; +global V_A_NEW: Field = 900; +global R_E: Field = 0xfeedface; + +// Placeholder recipient viewing-key scalar; PVK_RECIPIENT = VK_RECIPIENT * H. +global VK_RECIPIENT: Field = 0xfeed; +// Placeholder auditor secret scalars; K_aud_{r,s} = scalar * H. Distinct so +// the dual-channel separation is exercised by the happy path; both shared +// with the transfer fixtures for cross-circuit tooling consistency. +global K_AUD_R_SCALAR: Field = 0xc0ffee01; +global K_AUD_S_SCALAR: Field = 0xc0ffee; + +// Y_op = sk_op * H (= lib `scalar_mul_Y` fixture for sk = 0xdead). +global Y_OP_X: Field = 0x1b46b003b88a6c34549dc74115f088f4b231a151397526bc10cbf1d15b457646; +global Y_OP_Y: Field = 0x29116280600c10ead1fdbd9ab4b571896030679bf554d7f1ebf681e5147de21b; + +// All values below are pinned by `operator_transfer_fixtures_match_lib` and +// `operator_transfer_auditor_fixtures_match_lib`, which re-derive them from +// the lib primitives and assert the values declared here -- if either side +// drifts the test fails immediately. They are emitted by `print_fixtures`. +global R_A: Field = 0x1528d8006892ad6b484d6f18381c687304d6b4cb9a215992918dd601ad4acf9d; +global C_A_X: Field = 0x1d8739d710d3cc5188faa1e64058cb4986513f46edbbe17c33e2a25da2bb657f; +global C_A_Y: Field = 0x142b6a3fa6772624471dd3e938bbff4a58a17f8bee976a5fcc949c8f487ec209; +global PVK_RECIPIENT_X: Field = + 0x176667da789ef2e73e193b32ff0198687cadb0e5d1843fa8ca3140cbf352aaf0; +global PVK_RECIPIENT_Y: Field = + 0x017ad8f680161d399bcb7034881f3cd6288f99138d51bddf3331a92257698b85; +global K_AUD_R_X: Field = 0x07782f9cf649fcc3690b758cd826426732ae8dad766ec62860ca0330d8b0e3fe; +global K_AUD_R_Y: Field = 0x26a06cc59b1da39f1c50a1deae9bb5289b36c0350a9942ac4bf5a6079717dbd8; +global K_AUD_S_X: Field = 0x22502c7b20b64aeaaaa4d4fa5f2f8600f9734e828f7284a8d1431da36b33803a; +global K_AUD_S_Y: Field = 0x0419089353d24334f03f4a1c9c9b19282e32f6879c5c69b9cdffba6a13a7c5ed; +global C_A_NEW_X: Field = 0x2d19aa5933ba41ebadb28d4c784ca4e405a1a6efcb5cf15e2c8d5e9a0552f178; +global C_A_NEW_Y: Field = 0x0214719083236d5ecfc551fbc2b6d04fa8c95b8986fcfc7bbb8c0ff3cecd3dd1; +global C_TX_X: Field = 0x1679ef51dbb01793592348ed9dcc453f82aba5d4777d786ea09979e06b965dfc; +global C_TX_Y: Field = 0x1ec2eb1cc4b0cfd172637f34166074f3e15e6620d814cc22bae87eeb2a40270d; +global R_E_X: Field = 0x114ed4fcf2c57014eb678c577aa02f30ef590b713d7a6a5e87702d1c7f71957f; +global R_E_Y: Field = 0x07a70cf826350d4f438c7a3c5e8761b0ae6cb63de757f0c96815f4057b9205f4; +global V_TILDE: Field = 0x2861fc81fda926afd33517cb5509df867f324abf81c82ab9d7eca9a94a16f07b; +global A_TILDE_NEW: Field = + 0x1d2e286a8d510a7c4164d0142ceea3202a957d49248426435f311a348f681147; +global V_TILDE_AUD_R: Field = + 0x2d80e4f8a46472bb837956b13028b7d95979a8f5e0f657c50a7d6721f9a19f66; +global R_TILDE_AUD_R: Field = + 0x0b9fb0b33e05f9c93da3802075248e25b59f1b2416c997ffbedd6cd71e1c1385; +global V_TILDE_AUD_S: Field = + 0x0a607feb31e56e89ed94a3e1dc3811d3d23aaf10b2f27874ce670878b9f6ee19; +global A_TILDE_AUD_S: Field = + 0x0b586defafdf4583d0f338855fdf8391102adaac4634a3b80594aea6f788e1cb; + +#[test] +fn print_fixtures() { + // One-shot harness: prints every derived public input for the chosen + // fixture tuple. Run with + // `nargo test --package circuit_operator_transfer print_fixtures --show-output` + // and paste the values into the globals above. + let y_op = scalar_mul(SK_OP, H); + let r_a = derive_allow_r(DVK_I, SIGMA_A); + let c_a = commit(V_A, r_a); + + let pvk_recipient = scalar_mul(VK_RECIPIENT, H); + let k_aud_r = scalar_mul(K_AUD_R_SCALAR, H); + let k_aud_s = scalar_mul(K_AUD_S_SCALAR, H); + let r_e_pt = scalar_mul(R_E, H); + + let s_x = ecdh(R_E, pvk_recipient); + let r_tx = derive_tx_blind(s_x, SIGMA_A); + let c_tx = commit(V_TX, r_tx); + let v_tilde = encrypt_amount(V_TX, s_x, SIGMA_A); + + let r_a_new = derive_allow_r(DVK_I, SIGMA_A_NEW); + let c_a_new = commit(V_A_NEW, r_a_new); + let a_tilde_new = encrypt_allowance(V_A_NEW, DVK_I, SIGMA_A_NEW); + + let s_a_r_x = ecdh(R_E, k_aud_r); + let m_r = sponge_squeeze_2(domain::AUDITOR_RECIPIENT, s_a_r_x, SIGMA_A); + let v_tilde_aud_r = V_TX + m_r[0]; + let r_tilde_aud_r = r_tx + m_r[1]; + + let s_a_s_x = ecdh(R_E, k_aud_s); + let m_s = sponge_squeeze_2(domain::AUDITOR_SENDER, s_a_s_x, SIGMA_A); + let v_tilde_aud_s = V_TX + m_s[0]; + let a_tilde_aud_s = V_A_NEW + m_s[1]; + + let yopx = y_op.x; + let yopy = y_op.y; + let cax = c_a.x; + let cay = c_a.y; + let pvkx = pvk_recipient.x; + let pvky = pvk_recipient.y; + let kr_x = k_aud_r.x; + let kr_y = k_aud_r.y; + let ks_x = k_aud_s.x; + let ks_y = k_aud_s.y; + let canx = c_a_new.x; + let cany = c_a_new.y; + let ctxx = c_tx.x; + let ctxy = c_tx.y; + let rex = r_e_pt.x; + let rey = r_e_pt.y; + println(f"Y_OP_X = {yopx}"); + println(f"Y_OP_Y = {yopy}"); + println(f"R_A = {r_a}"); + println(f"C_A_X = {cax}"); + println(f"C_A_Y = {cay}"); + println(f"PVK_RECIPIENT_X = {pvkx}"); + println(f"PVK_RECIPIENT_Y = {pvky}"); + println(f"K_AUD_R_X = {kr_x}"); + println(f"K_AUD_R_Y = {kr_y}"); + println(f"K_AUD_S_X = {ks_x}"); + println(f"K_AUD_S_Y = {ks_y}"); + println(f"C_A_NEW_X = {canx}"); + println(f"C_A_NEW_Y = {cany}"); + println(f"C_TX_X = {ctxx}"); + println(f"C_TX_Y = {ctxy}"); + println(f"R_E_X = {rex}"); + println(f"R_E_Y = {rey}"); + println(f"V_TILDE = {v_tilde}"); + println(f"A_TILDE_NEW = {a_tilde_new}"); + println(f"V_TILDE_AUD_R = {v_tilde_aud_r}"); + println(f"R_TILDE_AUD_R = {r_tilde_aud_r}"); + println(f"V_TILDE_AUD_S = {v_tilde_aud_s}"); + println(f"A_TILDE_AUD_S = {a_tilde_aud_s}"); +} + +#[test] +fn operator_transfer_fixtures_match_lib() { + let y_op = scalar_mul(SK_OP, H); + let r_a = derive_allow_r(DVK_I, SIGMA_A); + let c_a = commit(V_A, r_a); + let pvk_recipient = scalar_mul(VK_RECIPIENT, H); + let s_x = ecdh(R_E, pvk_recipient); + let r_tx = derive_tx_blind(s_x, SIGMA_A); + let c_tx = commit(V_TX, r_tx); + let v_tilde = encrypt_amount(V_TX, s_x, SIGMA_A); + let r_a_new = derive_allow_r(DVK_I, SIGMA_A_NEW); + let c_a_new = commit(V_A_NEW, r_a_new); + let a_tilde_new = encrypt_allowance(V_A_NEW, DVK_I, SIGMA_A_NEW); + + assert(y_op.x == Y_OP_X); + assert(y_op.y == Y_OP_Y); + assert(r_a == R_A); + assert(c_a.x == C_A_X); + assert(c_a.y == C_A_Y); + assert(pvk_recipient.x == PVK_RECIPIENT_X); + assert(pvk_recipient.y == PVK_RECIPIENT_Y); + assert(c_tx.x == C_TX_X); + assert(c_tx.y == C_TX_Y); + assert(v_tilde == V_TILDE); + assert(c_a_new.x == C_A_NEW_X); + assert(c_a_new.y == C_A_NEW_Y); + assert(a_tilde_new == A_TILDE_NEW); +} + +#[test] +fn operator_transfer_auditor_fixtures_match_lib() { + let k_aud_r = scalar_mul(K_AUD_R_SCALAR, H); + let k_aud_s = scalar_mul(K_AUD_S_SCALAR, H); + let r_e_pt = scalar_mul(R_E, H); + let pvk_recipient = scalar_mul(VK_RECIPIENT, H); + let s_x = ecdh(R_E, pvk_recipient); + let r_tx = derive_tx_blind(s_x, SIGMA_A); + let s_a_r_x = ecdh(R_E, k_aud_r); + let m_r = sponge_squeeze_2(domain::AUDITOR_RECIPIENT, s_a_r_x, SIGMA_A); + let s_a_s_x = ecdh(R_E, k_aud_s); + let m_s = sponge_squeeze_2(domain::AUDITOR_SENDER, s_a_s_x, SIGMA_A); + + assert(k_aud_r.x == K_AUD_R_X); + assert(k_aud_r.y == K_AUD_R_Y); + assert(k_aud_s.x == K_AUD_S_X); + assert(k_aud_s.y == K_AUD_S_Y); + assert(r_e_pt.x == R_E_X); + assert(r_e_pt.y == R_E_Y); + assert(V_TX + m_r[0] == V_TILDE_AUD_R); + assert(r_tx + m_r[1] == R_TILDE_AUD_R); + assert(V_TX + m_s[0] == V_TILDE_AUD_S); + assert(V_A_NEW + m_s[1] == A_TILDE_AUD_S); +} + +// `run_main` accepts every public input as a parameter -- nothing baked in. +// Tests that target a specific constraint construct the public tuple so only +// that constraint can fail: tamper the witness or public input under test +// AND recompute every downstream public that depends on it, so no cascaded +// mismatch coincidentally lights up the same `should_fail`. +fn run_main( + sk_op_in: Field, + dvk_i_in: Field, + v_a_in: Field, + r_a_in: Field, + v_tx_in: Field, + r_e_in: Field, + c_a_x_in: Field, + c_a_y_in: Field, + sigma_a_in: Field, + y_op_x_in: Field, + y_op_y_in: Field, + pvk_recipient_x_in: Field, + pvk_recipient_y_in: Field, + k_aud_r_x_in: Field, + k_aud_r_y_in: Field, + k_aud_s_x_in: Field, + k_aud_s_y_in: Field, + c_a_new_x_in: Field, + c_a_new_y_in: Field, + c_tx_x_in: Field, + c_tx_y_in: Field, + r_e_x_in: Field, + r_e_y_in: Field, + v_tilde_in: Field, + a_tilde_new_in: Field, + sigma_a_new_in: Field, + v_tilde_aud_r_in: Field, + r_tilde_aud_r_in: Field, + v_tilde_aud_s_in: Field, + a_tilde_aud_s_in: Field, +) { + main( + sk_op_in, + dvk_i_in, + v_a_in, + r_a_in, + v_tx_in, + r_e_in, + c_a_x_in, + c_a_y_in, + sigma_a_in, + y_op_x_in, + y_op_y_in, + pvk_recipient_x_in, + pvk_recipient_y_in, + k_aud_r_x_in, + k_aud_r_y_in, + k_aud_s_x_in, + k_aud_s_y_in, + c_a_new_x_in, + c_a_new_y_in, + c_tx_x_in, + c_tx_y_in, + r_e_x_in, + r_e_y_in, + v_tilde_in, + a_tilde_new_in, + sigma_a_new_in, + v_tilde_aud_r_in, + r_tilde_aud_r_in, + v_tilde_aud_s_in, + a_tilde_aud_s_in, + ); +} + +fn run_fixture() { + run_main( + SK_OP, + DVK_I, + V_A, + R_A, + V_TX, + R_E, + C_A_X, + C_A_Y, + SIGMA_A, + Y_OP_X, + Y_OP_Y, + PVK_RECIPIENT_X, + PVK_RECIPIENT_Y, + K_AUD_R_X, + K_AUD_R_Y, + K_AUD_S_X, + K_AUD_S_Y, + C_A_NEW_X, + C_A_NEW_Y, + C_TX_X, + C_TX_Y, + R_E_X, + R_E_Y, + V_TILDE, + A_TILDE_NEW, + SIGMA_A_NEW, + V_TILDE_AUD_R, + R_TILDE_AUD_R, + V_TILDE_AUD_S, + A_TILDE_AUD_S, + ); +} + +#[test] +fn matches_fixture() { + run_fixture(); +} + +#[test] +fn second_transfer_against_same_allowance() { + // Second happy-path point: same C_a (= same dvk_i, same v_a, same + // sigma_a) with a different v_tx. Exercises the case where the operator + // hasn't yet rotated the allowance commitment but produces a fresh + // sigma_a_new and a fresh r_e on every call. The transfer-side ECDH + // chain (O5..O9) is fully rederived; only O11/O12 share dvk_i + the new + // sigma_a_new with the canonical fixture. + let v_tx_2: Field = 250; + let v_a_new_2: Field = V_A - v_tx_2; // 750 + let sigma_a_new_2: Field = 0x03; + let r_e_2: Field = 0xfacecafe; + + let pvk_recipient = + EmbeddedCurvePoint { x: PVK_RECIPIENT_X, y: PVK_RECIPIENT_Y, is_infinite: false }; + let k_aud_r = EmbeddedCurvePoint { x: K_AUD_R_X, y: K_AUD_R_Y, is_infinite: false }; + let k_aud_s = EmbeddedCurvePoint { x: K_AUD_S_X, y: K_AUD_S_Y, is_infinite: false }; + + let s_x = ecdh(r_e_2, pvk_recipient); + let r_tx = derive_tx_blind(s_x, SIGMA_A); + let c_tx = commit(v_tx_2, r_tx); + let v_tilde_2 = encrypt_amount(v_tx_2, s_x, SIGMA_A); + let r_a_new_2 = derive_allow_r(DVK_I, sigma_a_new_2); + let c_a_new_2 = commit(v_a_new_2, r_a_new_2); + let a_tilde_new_2 = encrypt_allowance(v_a_new_2, DVK_I, sigma_a_new_2); + let r_e_2_pt = scalar_mul(r_e_2, H); + let s_a_r_x = ecdh(r_e_2, k_aud_r); + let m_r = sponge_squeeze_2(domain::AUDITOR_RECIPIENT, s_a_r_x, SIGMA_A); + let s_a_s_x = ecdh(r_e_2, k_aud_s); + let m_s = sponge_squeeze_2(domain::AUDITOR_SENDER, s_a_s_x, SIGMA_A); + + run_main( + SK_OP, + DVK_I, + V_A, + R_A, + v_tx_2, + r_e_2, + C_A_X, + C_A_Y, + SIGMA_A, + Y_OP_X, + Y_OP_Y, + PVK_RECIPIENT_X, + PVK_RECIPIENT_Y, + K_AUD_R_X, + K_AUD_R_Y, + K_AUD_S_X, + K_AUD_S_Y, + c_a_new_2.x, + c_a_new_2.y, + c_tx.x, + c_tx.y, + r_e_2_pt.x, + r_e_2_pt.y, + v_tilde_2, + a_tilde_new_2, + sigma_a_new_2, + v_tx_2 + m_r[0], + r_tx + m_r[1], + v_tx_2 + m_s[0], + v_a_new_2 + m_s[1], + ); +} + +#[test] +fn full_allowance_transfer() { + // Boundary: v_tx = v_a, so v_a_new = 0. C_a' commits to (0, r_a_new'), + // which is a valid commitment to zero. a_tilde' and a_tilde_aud_s both + // bind to 0 (plus mask) so the owner's auditor sees the post-transfer + // allowance reach zero. The delegation entry isn't deleted (that's + // revoke_operator's job, Section 7.9) -- it now escrows zero value. + let pvk_recipient = + EmbeddedCurvePoint { x: PVK_RECIPIENT_X, y: PVK_RECIPIENT_Y, is_infinite: false }; + let k_aud_r = EmbeddedCurvePoint { x: K_AUD_R_X, y: K_AUD_R_Y, is_infinite: false }; + let k_aud_s = EmbeddedCurvePoint { x: K_AUD_S_X, y: K_AUD_S_Y, is_infinite: false }; + let s_x = ecdh(R_E, pvk_recipient); + let r_tx = derive_tx_blind(s_x, SIGMA_A); + let c_tx = commit(V_A, r_tx); + let v_tilde = encrypt_amount(V_A, s_x, SIGMA_A); + let r_a_new = derive_allow_r(DVK_I, SIGMA_A_NEW); + let c_a_new = commit(0, r_a_new); + let a_tilde_new = encrypt_allowance(0, DVK_I, SIGMA_A_NEW); + let s_a_r_x = ecdh(R_E, k_aud_r); + let m_r = sponge_squeeze_2(domain::AUDITOR_RECIPIENT, s_a_r_x, SIGMA_A); + let s_a_s_x = ecdh(R_E, k_aud_s); + let m_s = sponge_squeeze_2(domain::AUDITOR_SENDER, s_a_s_x, SIGMA_A); + + run_main( + SK_OP, + DVK_I, + V_A, + R_A, + V_A, + R_E, + C_A_X, + C_A_Y, + SIGMA_A, + Y_OP_X, + Y_OP_Y, + PVK_RECIPIENT_X, + PVK_RECIPIENT_Y, + K_AUD_R_X, + K_AUD_R_Y, + K_AUD_S_X, + K_AUD_S_Y, + c_a_new.x, + c_a_new.y, + c_tx.x, + c_tx.y, + R_E_X, + R_E_Y, + v_tilde, + a_tilde_new, + SIGMA_A_NEW, + V_A + m_r[0], + r_tx + m_r[1], + V_A + m_s[0], + 0 + m_s[1], + ); +} + +#[test(should_fail)] +fn rejects_wrong_sk_op() { + // O1 fires: scalar_mul(SK_OP + 1, H) != Y_op. + run_main( + SK_OP + 1, + DVK_I, + V_A, + R_A, + V_TX, + R_E, + C_A_X, + C_A_Y, + SIGMA_A, + Y_OP_X, + Y_OP_Y, + PVK_RECIPIENT_X, + PVK_RECIPIENT_Y, + K_AUD_R_X, + K_AUD_R_Y, + K_AUD_S_X, + K_AUD_S_Y, + C_A_NEW_X, + C_A_NEW_Y, + C_TX_X, + C_TX_Y, + R_E_X, + R_E_Y, + V_TILDE, + A_TILDE_NEW, + SIGMA_A_NEW, + V_TILDE_AUD_R, + R_TILDE_AUD_R, + V_TILDE_AUD_S, + A_TILDE_AUD_S, + ); +} + +#[test(should_fail)] +fn rejects_wrong_dvk_against_wrapper() { + // Wrapper-binding regression. The OperatorTransfer circuit does NOT carry + // a vk derivation -- the operator never sees the owner's sk -- so a naive + // reading might worry that an operator could reuse a proof's witness + // across wrappers. The defense lives in O3. + // + // Mechanism (Section 7.8 final paragraph): + // * SetOperator derived dvk_i from a wrapper-specific vk: S2/S5 chain + // vk = Poseidon2(VK, sk_owner, wrap), then dvk_i = Poseidon2(DVK, vk, + // op_i). A different `wrap` produces a different vk, and therefore a + // different dvk_i. + // * That dvk_i then determined r_a (S6: r_a = Poseidon2(ALLOW_R, dvk_i, + // sigma_a)) and thereby C_a (S7). + // * On the OperatorTransfer side, C_a and sigma_a are PUBLIC inputs. + // O3 forces the prover's witness dvk_i to satisfy r_a == + // Poseidon2(ALLOW_R, dvk_i, sigma_a), and O2 then forces commit(v_a, + // r_a) == C_a. The combination pins dvk_i: only the dvk_i that + // SetOperator produced under that wrapper's vk admits an opening of + // C_a. + // + // This test exercises the failure mode. We keep every public input + // canonical (so C_a, sigma_a, Y_op come from the original wrapper) but + // hand the prover a `dvk_i` from a DIFFERENT wrapper -- i.e., one that + // would have been produced if vk had been derived under wrap_bad rather + // than wrap. With a different dvk_i, derive_allow_r(dvk_i_bad, SIGMA_A) + // yields a different r_a, so the prover-supplied R_A (also derived from + // dvk_i_bad and supplied to keep O3 internally consistent) no longer + // opens C_a under commit(V_A, R_A) -- O2 fails. Alternatively if we + // leave R_A canonical, O3 fails directly. The path here patches the + // private witness `dvk_i` only and keeps the canonical R_A; that + // triggers O3 immediately (r_a_derived != R_A). + // + // Why O3 is the load-bearing constraint: O3 is the *only* in-circuit + // tie between the wrapper-specific dvk_i (chosen at SetOperator) and + // the rest of the OperatorTransfer witness. Without it the operator + // could substitute any dvk_i, satisfying O2 with a re-randomization of + // r_a, and verify a proof against a C_a from a different wrapper. + let dvk_i_bad: Field = DVK_I + 1; + run_main( + SK_OP, + dvk_i_bad, + V_A, + R_A, + V_TX, + R_E, + C_A_X, + C_A_Y, + SIGMA_A, + Y_OP_X, + Y_OP_Y, + PVK_RECIPIENT_X, + PVK_RECIPIENT_Y, + K_AUD_R_X, + K_AUD_R_Y, + K_AUD_S_X, + K_AUD_S_Y, + C_A_NEW_X, + C_A_NEW_Y, + C_TX_X, + C_TX_Y, + R_E_X, + R_E_Y, + V_TILDE, + A_TILDE_NEW, + SIGMA_A_NEW, + V_TILDE_AUD_R, + R_TILDE_AUD_R, + V_TILDE_AUD_S, + A_TILDE_AUD_S, + ); +} + +#[test(should_fail)] +fn rejects_wrong_allowance_opening() { + // O2 fires: commit(V_A + 1, R_A) != C_a. v_a_new cascades into O11 / O12 + // / O_a8; recompute c_a_new, a_tilde_new, and a_tilde_aud_s against the + // tampered v_a_new so only O2 fires. + let v_a_bad: Field = V_A + 1; + let v_a_new_bad: Field = v_a_bad - V_TX; + let k_aud_s = EmbeddedCurvePoint { x: K_AUD_S_X, y: K_AUD_S_Y, is_infinite: false }; + let r_a_new = derive_allow_r(DVK_I, SIGMA_A_NEW); + let c_a_new_bad = commit(v_a_new_bad, r_a_new); + let a_tilde_new_bad = encrypt_allowance(v_a_new_bad, DVK_I, SIGMA_A_NEW); + let s_a_s_x = ecdh(R_E, k_aud_s); + let m_s = sponge_squeeze_2(domain::AUDITOR_SENDER, s_a_s_x, SIGMA_A); + + run_main( + SK_OP, + DVK_I, + v_a_bad, + R_A, + V_TX, + R_E, + C_A_X, + C_A_Y, + SIGMA_A, + Y_OP_X, + Y_OP_Y, + PVK_RECIPIENT_X, + PVK_RECIPIENT_Y, + K_AUD_R_X, + K_AUD_R_Y, + K_AUD_S_X, + K_AUD_S_Y, + c_a_new_bad.x, + c_a_new_bad.y, + C_TX_X, + C_TX_Y, + R_E_X, + R_E_Y, + V_TILDE, + a_tilde_new_bad, + SIGMA_A_NEW, + V_TILDE_AUD_R, + R_TILDE_AUD_R, + V_TILDE_AUD_S, + v_a_new_bad + m_s[1], + ); +} + +#[test(should_fail)] +fn rejects_under_funded_transfer() { + // v_tx > v_a: v_a - v_tx underflows in the field to a value >= 2^128. + // O4's `v_a_new` range check fires. Every downstream public is + // recomputed against v_tx_too_large so the single firing constraint is + // O4's v_a_new range check (the v_a and v_tx individual sub-checks pass: + // V_A is in range and v_tx_too_large = V_A + 1 = 1001 is also in range). + let v_tx_too_large: Field = V_A + 1; + let pvk_recipient = + EmbeddedCurvePoint { x: PVK_RECIPIENT_X, y: PVK_RECIPIENT_Y, is_infinite: false }; + let k_aud_r = EmbeddedCurvePoint { x: K_AUD_R_X, y: K_AUD_R_Y, is_infinite: false }; + let k_aud_s = EmbeddedCurvePoint { x: K_AUD_S_X, y: K_AUD_S_Y, is_infinite: false }; + let s_x = ecdh(R_E, pvk_recipient); + let r_tx = derive_tx_blind(s_x, SIGMA_A); + let c_tx = commit(v_tx_too_large, r_tx); + let v_tilde = encrypt_amount(v_tx_too_large, s_x, SIGMA_A); + let r_a_new = derive_allow_r(DVK_I, SIGMA_A_NEW); + let c_a_new_invalid = commit(V_A - v_tx_too_large, r_a_new); + let a_tilde_new_invalid = encrypt_allowance(V_A - v_tx_too_large, DVK_I, SIGMA_A_NEW); + let s_a_r_x = ecdh(R_E, k_aud_r); + let m_r = sponge_squeeze_2(domain::AUDITOR_RECIPIENT, s_a_r_x, SIGMA_A); + let s_a_s_x = ecdh(R_E, k_aud_s); + let m_s = sponge_squeeze_2(domain::AUDITOR_SENDER, s_a_s_x, SIGMA_A); + + run_main( + SK_OP, + DVK_I, + V_A, + R_A, + v_tx_too_large, + R_E, + C_A_X, + C_A_Y, + SIGMA_A, + Y_OP_X, + Y_OP_Y, + PVK_RECIPIENT_X, + PVK_RECIPIENT_Y, + K_AUD_R_X, + K_AUD_R_Y, + K_AUD_S_X, + K_AUD_S_Y, + c_a_new_invalid.x, + c_a_new_invalid.y, + c_tx.x, + c_tx.y, + R_E_X, + R_E_Y, + v_tilde, + a_tilde_new_invalid, + SIGMA_A_NEW, + v_tx_too_large + m_r[0], + r_tx + m_r[1], + v_tx_too_large + m_s[0], + (V_A - v_tx_too_large) + m_s[1], + ); +} + +#[test(should_fail)] +fn rejects_v_a_out_of_range() { + // 2^127 is exactly the boundary O4 rules out (Section 2.6 / 3.4 -- SEP-41 + // i128 non-negative range is [0, 2^127), so 2^127 is the smallest value + // that must fail). With v_a_huge - V_TX = 2^127 - 100 still in [0, 2^127), + // the v_a_new range check passes; only the `v_a.assert_max_bit_size<127>()` + // sub-check of O4 fires. C_a, c_a_new, and a_tilde_aud_s are all + // recomputed against v_a_huge / v_a_new_huge so O2 / O11 / O12 / O_a8 + // cannot coincidentally fire. R_A is independent of v_a so it stays + // canonical. + let v_a_huge: Field = 0x80000000000000000000000000000000; + let v_a_new_huge: Field = v_a_huge - V_TX; + let k_aud_s = EmbeddedCurvePoint { x: K_AUD_S_X, y: K_AUD_S_Y, is_infinite: false }; + let c_a = commit(v_a_huge, R_A); + let r_a_new = derive_allow_r(DVK_I, SIGMA_A_NEW); + let c_a_new = commit(v_a_new_huge, r_a_new); + let a_tilde_new = encrypt_allowance(v_a_new_huge, DVK_I, SIGMA_A_NEW); + let s_a_s_x = ecdh(R_E, k_aud_s); + let m_s = sponge_squeeze_2(domain::AUDITOR_SENDER, s_a_s_x, SIGMA_A); + + run_main( + SK_OP, + DVK_I, + v_a_huge, + R_A, + V_TX, + R_E, + c_a.x, + c_a.y, + SIGMA_A, + Y_OP_X, + Y_OP_Y, + PVK_RECIPIENT_X, + PVK_RECIPIENT_Y, + K_AUD_R_X, + K_AUD_R_Y, + K_AUD_S_X, + K_AUD_S_Y, + c_a_new.x, + c_a_new.y, + C_TX_X, + C_TX_Y, + R_E_X, + R_E_Y, + V_TILDE, + a_tilde_new, + SIGMA_A_NEW, + V_TILDE_AUD_R, + R_TILDE_AUD_R, + V_TILDE_AUD_S, + v_a_new_huge + m_s[1], + ); +} + +#[test(should_fail)] +fn rejects_v_tx_out_of_range() { + // v_tx = 2^127: O4's range check on v_tx fires. Every downstream public + // that depends on v_tx (C_tx, v_tilde, v_tilde_aud_r, v_tilde_aud_s) and + // on v_a_new (c_a_new, a_tilde_new, a_tilde_aud_s) is recomputed so the + // only constraints that fail are O4's range sub-checks. + let v_tx_huge: Field = 0x80000000000000000000000000000000; + let v_a_new_huge: Field = V_A - v_tx_huge; + let pvk_recipient = + EmbeddedCurvePoint { x: PVK_RECIPIENT_X, y: PVK_RECIPIENT_Y, is_infinite: false }; + let k_aud_r = EmbeddedCurvePoint { x: K_AUD_R_X, y: K_AUD_R_Y, is_infinite: false }; + let k_aud_s = EmbeddedCurvePoint { x: K_AUD_S_X, y: K_AUD_S_Y, is_infinite: false }; + let s_x = ecdh(R_E, pvk_recipient); + let r_tx = derive_tx_blind(s_x, SIGMA_A); + let c_tx = commit(v_tx_huge, r_tx); + let v_tilde = encrypt_amount(v_tx_huge, s_x, SIGMA_A); + let r_a_new = derive_allow_r(DVK_I, SIGMA_A_NEW); + let c_a_new = commit(v_a_new_huge, r_a_new); + let a_tilde_new = encrypt_allowance(v_a_new_huge, DVK_I, SIGMA_A_NEW); + let s_a_r_x = ecdh(R_E, k_aud_r); + let m_r = sponge_squeeze_2(domain::AUDITOR_RECIPIENT, s_a_r_x, SIGMA_A); + let s_a_s_x = ecdh(R_E, k_aud_s); + let m_s = sponge_squeeze_2(domain::AUDITOR_SENDER, s_a_s_x, SIGMA_A); + + run_main( + SK_OP, + DVK_I, + V_A, + R_A, + v_tx_huge, + R_E, + C_A_X, + C_A_Y, + SIGMA_A, + Y_OP_X, + Y_OP_Y, + PVK_RECIPIENT_X, + PVK_RECIPIENT_Y, + K_AUD_R_X, + K_AUD_R_Y, + K_AUD_S_X, + K_AUD_S_Y, + c_a_new.x, + c_a_new.y, + c_tx.x, + c_tx.y, + R_E_X, + R_E_Y, + v_tilde, + a_tilde_new, + SIGMA_A_NEW, + v_tx_huge + m_r[0], + r_tx + m_r[1], + v_tx_huge + m_s[0], + v_a_new_huge + m_s[1], + ); +} + +#[test(should_fail)] +fn rejects_wrong_recipient_pvk() { + // PVK_recipient replaced by Y_op (a valid on-curve point that isn't the + // registered recipient's PVK). S = r_e * PVK_recipient changes, so r_tx + // and v_tilde drift and O8 fails (C_tx no longer matches commit(v_tx, + // r_tx_new)). + run_main( + SK_OP, + DVK_I, + V_A, + R_A, + V_TX, + R_E, + C_A_X, + C_A_Y, + SIGMA_A, + Y_OP_X, + Y_OP_Y, + Y_OP_X, + Y_OP_Y, + K_AUD_R_X, + K_AUD_R_Y, + K_AUD_S_X, + K_AUD_S_Y, + C_A_NEW_X, + C_A_NEW_Y, + C_TX_X, + C_TX_Y, + R_E_X, + R_E_Y, + V_TILDE, + A_TILDE_NEW, + SIGMA_A_NEW, + V_TILDE_AUD_R, + R_TILDE_AUD_R, + V_TILDE_AUD_S, + A_TILDE_AUD_S, + ); +} + +#[test(should_fail)] +fn rejects_off_curve_pvk_recipient() { + // (1, 2) is off-curve: 4 != 1 - 17. The on-curve check on PVK_recipient + // fires before O5 ever consumes it. + run_main( + SK_OP, + DVK_I, + V_A, + R_A, + V_TX, + R_E, + C_A_X, + C_A_Y, + SIGMA_A, + Y_OP_X, + Y_OP_Y, + 1, + 2, + K_AUD_R_X, + K_AUD_R_Y, + K_AUD_S_X, + K_AUD_S_Y, + C_A_NEW_X, + C_A_NEW_Y, + C_TX_X, + C_TX_Y, + R_E_X, + R_E_Y, + V_TILDE, + A_TILDE_NEW, + SIGMA_A_NEW, + V_TILDE_AUD_R, + R_TILDE_AUD_R, + V_TILDE_AUD_S, + A_TILDE_AUD_S, + ); +} + +#[test(should_fail)] +fn rejects_poisoned_c_tx() { + // Anti-poisoning (Section 5.4): a prover that commits C_tx with arbitrary + // blinding -- here r_tx + 1 -- desynchronizes the recipient's accumulated + // blinding. O7 + O8 together close this attack: r_tx is uniquely + // determined by Poseidon2(delta_tx_blind, S.x, sigma_a), so any C_tx not + // built from that exact r_tx fails O8. + let pvk_recipient = + EmbeddedCurvePoint { x: PVK_RECIPIENT_X, y: PVK_RECIPIENT_Y, is_infinite: false }; + let s_x = ecdh(R_E, pvk_recipient); + let r_tx_poisoned = derive_tx_blind(s_x, SIGMA_A) + 1; + let c_tx_poisoned = commit(V_TX, r_tx_poisoned); + + run_main( + SK_OP, + DVK_I, + V_A, + R_A, + V_TX, + R_E, + C_A_X, + C_A_Y, + SIGMA_A, + Y_OP_X, + Y_OP_Y, + PVK_RECIPIENT_X, + PVK_RECIPIENT_Y, + K_AUD_R_X, + K_AUD_R_Y, + K_AUD_S_X, + K_AUD_S_Y, + C_A_NEW_X, + C_A_NEW_Y, + c_tx_poisoned.x, + c_tx_poisoned.y, + R_E_X, + R_E_Y, + V_TILDE, + A_TILDE_NEW, + SIGMA_A_NEW, + V_TILDE_AUD_R, + R_TILDE_AUD_R, + V_TILDE_AUD_S, + A_TILDE_AUD_S, + ); +} + +#[test(should_fail)] +fn rejects_tampered_v_tilde() { + // O9 fires: v_tilde + 1 no longer matches v_tx + Poseidon2(TX_AMOUNT, ...). + run_main( + SK_OP, + DVK_I, + V_A, + R_A, + V_TX, + R_E, + C_A_X, + C_A_Y, + SIGMA_A, + Y_OP_X, + Y_OP_Y, + PVK_RECIPIENT_X, + PVK_RECIPIENT_Y, + K_AUD_R_X, + K_AUD_R_Y, + K_AUD_S_X, + K_AUD_S_Y, + C_A_NEW_X, + C_A_NEW_Y, + C_TX_X, + C_TX_Y, + R_E_X, + R_E_Y, + V_TILDE + 1, + A_TILDE_NEW, + SIGMA_A_NEW, + V_TILDE_AUD_R, + R_TILDE_AUD_R, + V_TILDE_AUD_S, + A_TILDE_AUD_S, + ); +} + +#[test(should_fail)] +fn rejects_tampered_a_tilde_new() { + // O12 fires. + run_main( + SK_OP, + DVK_I, + V_A, + R_A, + V_TX, + R_E, + C_A_X, + C_A_Y, + SIGMA_A, + Y_OP_X, + Y_OP_Y, + PVK_RECIPIENT_X, + PVK_RECIPIENT_Y, + K_AUD_R_X, + K_AUD_R_Y, + K_AUD_S_X, + K_AUD_S_Y, + C_A_NEW_X, + C_A_NEW_Y, + C_TX_X, + C_TX_Y, + R_E_X, + R_E_Y, + V_TILDE, + A_TILDE_NEW + 1, + SIGMA_A_NEW, + V_TILDE_AUD_R, + R_TILDE_AUD_R, + V_TILDE_AUD_S, + A_TILDE_AUD_S, + ); +} + +#[test(should_fail)] +fn rejects_tampered_c_a_new() { + // O11 fires. + run_main( + SK_OP, + DVK_I, + V_A, + R_A, + V_TX, + R_E, + C_A_X, + C_A_Y, + SIGMA_A, + Y_OP_X, + Y_OP_Y, + PVK_RECIPIENT_X, + PVK_RECIPIENT_Y, + K_AUD_R_X, + K_AUD_R_Y, + K_AUD_S_X, + K_AUD_S_Y, + C_A_NEW_X + 1, + C_A_NEW_Y, + C_TX_X, + C_TX_Y, + R_E_X, + R_E_Y, + V_TILDE, + A_TILDE_NEW, + SIGMA_A_NEW, + V_TILDE_AUD_R, + R_TILDE_AUD_R, + V_TILDE_AUD_S, + A_TILDE_AUD_S, + ); +} + +#[test(should_fail)] +fn rejects_r_e_zero() { + // O13 fires: r_e = 0 would force R_e = O (identity) and collapse every + // ECDH in this transfer (S, S_{a,r}, S_{a,s} all become O), making every + // mask a knowable constant function of sigma_a. Every r_e-dependent + // public (R_e = (0, 0) identity-encoding, C_tx / v_tilde derived against + // s_x = 0, and both auditor channels' ciphertexts derived against + // s_a_{r,s}.x = 0) is recomputed so the `assert(r_e != 0)` check is the + // only constraint that can catch this -- removing O13 would let the + // proof verify. + let pvk_recipient = + EmbeddedCurvePoint { x: PVK_RECIPIENT_X, y: PVK_RECIPIENT_Y, is_infinite: false }; + let k_aud_r = EmbeddedCurvePoint { x: K_AUD_R_X, y: K_AUD_R_Y, is_infinite: false }; + let k_aud_s = EmbeddedCurvePoint { x: K_AUD_S_X, y: K_AUD_S_Y, is_infinite: false }; + let s_x_zero = ecdh(0, pvk_recipient); + let r_tx_zero = derive_tx_blind(s_x_zero, SIGMA_A); + let c_tx_zero = commit(V_TX, r_tx_zero); + let v_tilde_zero = encrypt_amount(V_TX, s_x_zero, SIGMA_A); + let s_a_r_x_zero = ecdh(0, k_aud_r); + let m_r_zero = sponge_squeeze_2(domain::AUDITOR_RECIPIENT, s_a_r_x_zero, SIGMA_A); + let s_a_s_x_zero = ecdh(0, k_aud_s); + let m_s_zero = sponge_squeeze_2(domain::AUDITOR_SENDER, s_a_s_x_zero, SIGMA_A); + + run_main( + SK_OP, + DVK_I, + V_A, + R_A, + V_TX, + 0, + C_A_X, + C_A_Y, + SIGMA_A, + Y_OP_X, + Y_OP_Y, + PVK_RECIPIENT_X, + PVK_RECIPIENT_Y, + K_AUD_R_X, + K_AUD_R_Y, + K_AUD_S_X, + K_AUD_S_Y, + C_A_NEW_X, + C_A_NEW_Y, + c_tx_zero.x, + c_tx_zero.y, + 0, + 0, + v_tilde_zero, + A_TILDE_NEW, + SIGMA_A_NEW, + V_TX + m_r_zero[0], + r_tx_zero + m_r_zero[1], + V_TX + m_s_zero[0], + V_A_NEW + m_s_zero[1], + ); +} + +#[test(should_fail)] +fn rejects_wrong_r_e() { + // r_e mismatches the public R_e: O6 fails because scalar_mul(r_e+1, H) + // no longer equals the canonical (R_E_X, R_E_Y). R_e itself is left + // canonical (so O6 catches the mismatch) but every other r_e-dependent + // public (C_tx, v_tilde, both auditor channels) is recomputed against + // r_e+1 so O8 / O9 / O_a3 / O_a4 / O_a7 / O_a8 cannot coincidentally + // fire. + let r_e_bad: Field = R_E + 1; + let pvk_recipient = + EmbeddedCurvePoint { x: PVK_RECIPIENT_X, y: PVK_RECIPIENT_Y, is_infinite: false }; + let k_aud_r = EmbeddedCurvePoint { x: K_AUD_R_X, y: K_AUD_R_Y, is_infinite: false }; + let k_aud_s = EmbeddedCurvePoint { x: K_AUD_S_X, y: K_AUD_S_Y, is_infinite: false }; + let s_x_bad = ecdh(r_e_bad, pvk_recipient); + let r_tx_bad = derive_tx_blind(s_x_bad, SIGMA_A); + let c_tx_bad = commit(V_TX, r_tx_bad); + let v_tilde_bad = encrypt_amount(V_TX, s_x_bad, SIGMA_A); + let s_a_r_x_bad = ecdh(r_e_bad, k_aud_r); + let m_r_bad = sponge_squeeze_2(domain::AUDITOR_RECIPIENT, s_a_r_x_bad, SIGMA_A); + let s_a_s_x_bad = ecdh(r_e_bad, k_aud_s); + let m_s_bad = sponge_squeeze_2(domain::AUDITOR_SENDER, s_a_s_x_bad, SIGMA_A); + + run_main( + SK_OP, + DVK_I, + V_A, + R_A, + V_TX, + r_e_bad, + C_A_X, + C_A_Y, + SIGMA_A, + Y_OP_X, + Y_OP_Y, + PVK_RECIPIENT_X, + PVK_RECIPIENT_Y, + K_AUD_R_X, + K_AUD_R_Y, + K_AUD_S_X, + K_AUD_S_Y, + C_A_NEW_X, + C_A_NEW_Y, + c_tx_bad.x, + c_tx_bad.y, + R_E_X, + R_E_Y, + v_tilde_bad, + A_TILDE_NEW, + SIGMA_A_NEW, + V_TX + m_r_bad[0], + r_tx_bad + m_r_bad[1], + V_TX + m_s_bad[0], + V_A_NEW + m_s_bad[1], + ); +} + +#[test(should_fail)] +fn rejects_off_curve_k_aud_r() { + // (1, 2) is off-curve. The on-curve check on K_aud_r fires before O_a1 + // ever consumes it. + run_main( + SK_OP, + DVK_I, + V_A, + R_A, + V_TX, + R_E, + C_A_X, + C_A_Y, + SIGMA_A, + Y_OP_X, + Y_OP_Y, + PVK_RECIPIENT_X, + PVK_RECIPIENT_Y, + 1, + 2, + K_AUD_S_X, + K_AUD_S_Y, + C_A_NEW_X, + C_A_NEW_Y, + C_TX_X, + C_TX_Y, + R_E_X, + R_E_Y, + V_TILDE, + A_TILDE_NEW, + SIGMA_A_NEW, + V_TILDE_AUD_R, + R_TILDE_AUD_R, + V_TILDE_AUD_S, + A_TILDE_AUD_S, + ); +} + +#[test(should_fail)] +fn rejects_identity_k_aud_r() { + // K_aud_r = identity collapses ECDH (S_{a,r} = r_e * O = O), so the + // on-curve check explicitly rejects the identity for keys. + run_main( + SK_OP, + DVK_I, + V_A, + R_A, + V_TX, + R_E, + C_A_X, + C_A_Y, + SIGMA_A, + Y_OP_X, + Y_OP_Y, + PVK_RECIPIENT_X, + PVK_RECIPIENT_Y, + 0, + 0, + K_AUD_S_X, + K_AUD_S_Y, + C_A_NEW_X, + C_A_NEW_Y, + C_TX_X, + C_TX_Y, + R_E_X, + R_E_Y, + V_TILDE, + A_TILDE_NEW, + SIGMA_A_NEW, + V_TILDE_AUD_R, + R_TILDE_AUD_R, + V_TILDE_AUD_S, + A_TILDE_AUD_S, + ); +} + +#[test(should_fail)] +fn rejects_off_curve_k_aud_s() { + run_main( + SK_OP, + DVK_I, + V_A, + R_A, + V_TX, + R_E, + C_A_X, + C_A_Y, + SIGMA_A, + Y_OP_X, + Y_OP_Y, + PVK_RECIPIENT_X, + PVK_RECIPIENT_Y, + K_AUD_R_X, + K_AUD_R_Y, + 1, + 2, + C_A_NEW_X, + C_A_NEW_Y, + C_TX_X, + C_TX_Y, + R_E_X, + R_E_Y, + V_TILDE, + A_TILDE_NEW, + SIGMA_A_NEW, + V_TILDE_AUD_R, + R_TILDE_AUD_R, + V_TILDE_AUD_S, + A_TILDE_AUD_S, + ); +} + +#[test(should_fail)] +fn rejects_identity_k_aud_s() { + run_main( + SK_OP, + DVK_I, + V_A, + R_A, + V_TX, + R_E, + C_A_X, + C_A_Y, + SIGMA_A, + Y_OP_X, + Y_OP_Y, + PVK_RECIPIENT_X, + PVK_RECIPIENT_Y, + K_AUD_R_X, + K_AUD_R_Y, + 0, + 0, + C_A_NEW_X, + C_A_NEW_Y, + C_TX_X, + C_TX_Y, + R_E_X, + R_E_Y, + V_TILDE, + A_TILDE_NEW, + SIGMA_A_NEW, + V_TILDE_AUD_R, + R_TILDE_AUD_R, + V_TILDE_AUD_S, + A_TILDE_AUD_S, + ); +} + +#[test(should_fail)] +fn rejects_tampered_v_tilde_aud_r() { + // O_a3 fires. + run_main( + SK_OP, + DVK_I, + V_A, + R_A, + V_TX, + R_E, + C_A_X, + C_A_Y, + SIGMA_A, + Y_OP_X, + Y_OP_Y, + PVK_RECIPIENT_X, + PVK_RECIPIENT_Y, + K_AUD_R_X, + K_AUD_R_Y, + K_AUD_S_X, + K_AUD_S_Y, + C_A_NEW_X, + C_A_NEW_Y, + C_TX_X, + C_TX_Y, + R_E_X, + R_E_Y, + V_TILDE, + A_TILDE_NEW, + SIGMA_A_NEW, + V_TILDE_AUD_R + 1, + R_TILDE_AUD_R, + V_TILDE_AUD_S, + A_TILDE_AUD_S, + ); +} + +#[test(should_fail)] +fn rejects_tampered_r_tilde_aud_r() { + // O_a4 fires. + run_main( + SK_OP, + DVK_I, + V_A, + R_A, + V_TX, + R_E, + C_A_X, + C_A_Y, + SIGMA_A, + Y_OP_X, + Y_OP_Y, + PVK_RECIPIENT_X, + PVK_RECIPIENT_Y, + K_AUD_R_X, + K_AUD_R_Y, + K_AUD_S_X, + K_AUD_S_Y, + C_A_NEW_X, + C_A_NEW_Y, + C_TX_X, + C_TX_Y, + R_E_X, + R_E_Y, + V_TILDE, + A_TILDE_NEW, + SIGMA_A_NEW, + V_TILDE_AUD_R, + R_TILDE_AUD_R + 1, + V_TILDE_AUD_S, + A_TILDE_AUD_S, + ); +} + +#[test(should_fail)] +fn rejects_tampered_v_tilde_aud_s() { + // O_a7 fires. + run_main( + SK_OP, + DVK_I, + V_A, + R_A, + V_TX, + R_E, + C_A_X, + C_A_Y, + SIGMA_A, + Y_OP_X, + Y_OP_Y, + PVK_RECIPIENT_X, + PVK_RECIPIENT_Y, + K_AUD_R_X, + K_AUD_R_Y, + K_AUD_S_X, + K_AUD_S_Y, + C_A_NEW_X, + C_A_NEW_Y, + C_TX_X, + C_TX_Y, + R_E_X, + R_E_Y, + V_TILDE, + A_TILDE_NEW, + SIGMA_A_NEW, + V_TILDE_AUD_R, + R_TILDE_AUD_R, + V_TILDE_AUD_S + 1, + A_TILDE_AUD_S, + ); +} + +#[test(should_fail)] +fn rejects_tampered_a_tilde_aud_s() { + // O_a8 fires. + run_main( + SK_OP, + DVK_I, + V_A, + R_A, + V_TX, + R_E, + C_A_X, + C_A_Y, + SIGMA_A, + Y_OP_X, + Y_OP_Y, + PVK_RECIPIENT_X, + PVK_RECIPIENT_Y, + K_AUD_R_X, + K_AUD_R_Y, + K_AUD_S_X, + K_AUD_S_Y, + C_A_NEW_X, + C_A_NEW_Y, + C_TX_X, + C_TX_Y, + R_E_X, + R_E_Y, + V_TILDE, + A_TILDE_NEW, + SIGMA_A_NEW, + V_TILDE_AUD_R, + R_TILDE_AUD_R, + V_TILDE_AUD_S, + A_TILDE_AUD_S + 1, + ); +} + +#[test(should_fail)] +fn rejects_wrong_k_aud_r() { + // K_aud_r replaced by a valid but different on-curve key (Y_op). Shared + // secret changes, recipient-channel masks change, O_a3 fails. + run_main( + SK_OP, + DVK_I, + V_A, + R_A, + V_TX, + R_E, + C_A_X, + C_A_Y, + SIGMA_A, + Y_OP_X, + Y_OP_Y, + PVK_RECIPIENT_X, + PVK_RECIPIENT_Y, + Y_OP_X, + Y_OP_Y, + K_AUD_S_X, + K_AUD_S_Y, + C_A_NEW_X, + C_A_NEW_Y, + C_TX_X, + C_TX_Y, + R_E_X, + R_E_Y, + V_TILDE, + A_TILDE_NEW, + SIGMA_A_NEW, + V_TILDE_AUD_R, + R_TILDE_AUD_R, + V_TILDE_AUD_S, + A_TILDE_AUD_S, + ); +} + +#[test(should_fail)] +fn rejects_wrong_k_aud_s() { + // K_aud_s replaced by a valid but different on-curve key (Y_op). Shared + // secret changes, owner-channel masks change, O_a7 fails. + run_main( + SK_OP, + DVK_I, + V_A, + R_A, + V_TX, + R_E, + C_A_X, + C_A_Y, + SIGMA_A, + Y_OP_X, + Y_OP_Y, + PVK_RECIPIENT_X, + PVK_RECIPIENT_Y, + K_AUD_R_X, + K_AUD_R_Y, + Y_OP_X, + Y_OP_Y, + C_A_NEW_X, + C_A_NEW_Y, + C_TX_X, + C_TX_Y, + R_E_X, + R_E_Y, + V_TILDE, + A_TILDE_NEW, + SIGMA_A_NEW, + V_TILDE_AUD_R, + R_TILDE_AUD_R, + V_TILDE_AUD_S, + A_TILDE_AUD_S, + ); +} From cfb1e12fe9e3aef7de4a43b08b0361ff0ef09fbb Mon Sep 17 00:00:00 2001 From: brozorec <9572072+brozorec@users.noreply.github.com> Date: Wed, 27 May 2026 11:23:02 +0200 Subject: [PATCH 12/16] feat(confidential): publish operator-transfer verification key 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 --- packages/tokens/src/confidential/circuits/scripts/extract_vks.sh | 1 + .../src/confidential/circuits/vks/operator_transfer.vk.json | 1 + 2 files changed, 2 insertions(+) create mode 100644 packages/tokens/src/confidential/circuits/vks/operator_transfer.vk.json diff --git a/packages/tokens/src/confidential/circuits/scripts/extract_vks.sh b/packages/tokens/src/confidential/circuits/scripts/extract_vks.sh index f9a5fca9e..fea6866ff 100755 --- a/packages/tokens/src/confidential/circuits/scripts/extract_vks.sh +++ b/packages/tokens/src/confidential/circuits/scripts/extract_vks.sh @@ -22,6 +22,7 @@ set -euo pipefail cd "$(dirname "$0")/.." CIRCUITS=( + "operator_transfer" "register" "withdraw" ) diff --git a/packages/tokens/src/confidential/circuits/vks/operator_transfer.vk.json b/packages/tokens/src/confidential/circuits/vks/operator_transfer.vk.json new file mode 100644 index 000000000..465f65aeb --- /dev/null +++ b/packages/tokens/src/confidential/circuits/vks/operator_transfer.vk.json @@ -0,0 +1 @@ +["0x0000000000000000000000000000000000000000000000000000000000008000","0x0000000000000000000000000000000000000000000000000000000000000028","0x0000000000000000000000000000000000000000000000000000000000000001","0x0000000000000000000000000000000000000000000000000000000000000018","0x0000000000000000000000000000006000afa08abfe7aa7f968697dfe228b036","0x00000000000000000000000000000000002ca36b4afe95c199b2a6685a92db33","0x0000000000000000000000000000000320935e2f28bf769df11a14db17376bfd","0x00000000000000000000000000000000002b3f8ea4c775be9e2ee523055d9ad8","0x000000000000000000000000000000854b28d8d2c71b3634b744414fca185df9","0x000000000000000000000000000000000002823a3bf5322f5557e99e10f1127e","0x000000000000000000000000000000d36ed5360551ef5f0978f5d3abd8b62a53","0x000000000000000000000000000000000026fc2eacfdddbe01373b088e73443a","0x00000000000000000000000000000088306bc4689e0b8fe250c25437e57692b0","0x000000000000000000000000000000000020429c751d7ccb837c99d4c46db9aa","0x0000000000000000000000000000004138b4a2859a6ee0f86212546bfa5a0beb","0x0000000000000000000000000000000000292bab64c7528cee6118dcef0239d4","0x000000000000000000000000000000443c807f3ad9eb6bd1b643a80d165cc077","0x00000000000000000000000000000000001c08a8fad078fc4e4ba3df6155f038","0x000000000000000000000000000000c604459995a06cc819d645ddba6f35d112","0x000000000000000000000000000000000029035fe9d977655c1c356907b2b091","0x000000000000000000000000000000e5bca0eaea1007dd32d37dd3fd38ea631b","0x00000000000000000000000000000000000de187ad385f834da7dc2f6efa3281","0x0000000000000000000000000000009128946800e08ab19b10c06ee3f7bb95fb","0x000000000000000000000000000000000022e8c0c8d3b1604c2a84b5fafecf6c","0x000000000000000000000000000000582a4c9bf781a3852ded9134a9d382de09","0x00000000000000000000000000000000000e1db518c51dd5a790952dc43c535e","0x0000000000000000000000000000009822d6ab747b7394c0bbb64dd302d0fa0b","0x00000000000000000000000000000000002cdd6d30bad8f2875bba349813b203","0x0000000000000000000000000000005738f222d113ad96d990e7d4a004ec2e57","0x000000000000000000000000000000000014e35bf3517c8beabf7ec116bd4c00","0x000000000000000000000000000000d89185b10a6c32362672867293fa6e87a6","0x00000000000000000000000000000000002cbca48853bc69fafe318ba9e1f9cc","0x00000000000000000000000000000033039cc41a55955127fd7bc892962ea8c7","0x00000000000000000000000000000000002dfe905b3e2d6d1d8217c2ff6cd6d4","0x00000000000000000000000000000075bc684506ebf05e35935da2774fd4e5e7","0x000000000000000000000000000000000008a9a2b23c34d8f3fbc1648b816351","0x0000000000000000000000000000001b69b1f99b8363d20252bb6b009fb3d7cc","0x0000000000000000000000000000000000220351383686f263064852f10751ea","0x000000000000000000000000000000463574f70920415f428cf523d40dc35e93","0x000000000000000000000000000000000012ad9197d03697766a98de24f77365","0x0000000000000000000000000000002adbdaf296abeb97607607fa45b99f41a2","0x000000000000000000000000000000000006caddd92a99e4702404f805155379","0x000000000000000000000000000000ba9a2d5d9fbfe151d8bc67ed55fb1f7ff2","0x0000000000000000000000000000000000259594fd50f8c1435a2eed1df76f5d","0x000000000000000000000000000000f3b23eccd9427c94fb5cbebe8d866cd778","0x000000000000000000000000000000000008a2b18955c779693cf0837ca1226c","0x000000000000000000000000000000b2937388964bc3a63fe359b15cad47481d","0x00000000000000000000000000000000002786ef69eed6d0022ef73979044ba3","0x000000000000000000000000000000939a7e368cedd5bf3c29e006369df2cc87","0x00000000000000000000000000000000001abf142490bfafec1a85082789f94f","0x0000000000000000000000000000007e869ef3a5da8819090c949306fa189f8f","0x0000000000000000000000000000000000083a789a8138b415a24e7343537560","0x000000000000000000000000000000cefac383eac2687725cdcfef62b0cc84dd","0x00000000000000000000000000000000001297bb2e1f2db1e3f61ee7b42f1f31","0x0000000000000000000000000000004d29f075c898c4625db4537ed7309f1244","0x000000000000000000000000000000000022a5a91375af5f73b612239f620c03","0x000000000000000000000000000000974724a1ca411a070583424fb95e99b8a3","0x0000000000000000000000000000000000002ee741304cb544808aa13987e2ac","0x000000000000000000000000000000451a5e05496018b5dde361de16bcc0065d","0x00000000000000000000000000000000000f2d5c46da580eb694929590c311d0","0x000000000000000000000000000000ce647a6d734a872af83ba8fbbde6993023","0x0000000000000000000000000000000000296394b32d292f97720d84b45cbb7a","0x00000000000000000000000000000081f52cefd7f6aba906e61b16ece26b1e50","0x0000000000000000000000000000000000225a10753fdde52a72926cac71dbfc","0x000000000000000000000000000000345b18f2cd544f07af40f551a7c2101c9d","0x00000000000000000000000000000000001f0a819ebe371911f80507b3e6b0b0","0x000000000000000000000000000000ca1d9e9cea934bf69022cda9ba3ae9b99a","0x00000000000000000000000000000000002e8e8b5cdd13a0d5de1418a110f648","0x0000000000000000000000000000003bd2d25bd9eb79b8c34780ed69cd204d0b","0x000000000000000000000000000000000023d4cbab015bd91bbd697772fe442f","0x000000000000000000000000000000eb38fb2c107c605c019d2982e206d3263a","0x00000000000000000000000000000000002bb2720b816b2ed876b849b0c1c114","0x0000000000000000000000000000008d695540e7da4bbf1663ff0b9312d1a0ba","0x000000000000000000000000000000000014811a798e6dc9f27ea3a1972d546f","0x0000000000000000000000000000003b41a97aca330a77624e411d325a2b5d33","0x0000000000000000000000000000000000033d6f6cb02d26347fea56d021d19b","0x0000000000000000000000000000007804650f7457393a89b7ca095e19f4377e","0x00000000000000000000000000000000000e5796b0f7aeb40e3ca6418aea0ee6","0x000000000000000000000000000000abec380a26d38ce65cb66441a6bfee4a82","0x00000000000000000000000000000000000f8a7a22469a819644d9752d33ea6a","0x000000000000000000000000000000c67b5353e3e700df86864f844b3877d0e0","0x0000000000000000000000000000000000184e98ee0a9d88cf1a71cc9d4c9ae3","0x00000000000000000000000000000062f8be694721a883d04939611bd13f5e8e","0x00000000000000000000000000000000001d0c07aca55566e398993a611efc22","0x000000000000000000000000000000489bc85eb60d891d6796dc59728f0bfbfa","0x000000000000000000000000000000000025eddc5c092aa77314a05f9ee6eda9","0x000000000000000000000000000000bc2e4048990d47af5a98c1eced27570293","0x000000000000000000000000000000000026a0245faff4f37863b7f18c5f7aa1","0x000000000000000000000000000000ffad1d95a8ee7ccd2287a489375de526f6","0x00000000000000000000000000000000002549709cd2124b19800eed4f003e52","0x00000000000000000000000000000043c4b44d03e114c03c1689f77c5789643c","0x00000000000000000000000000000000000621edcb5a9eab5ae5675b4da9d9a2","0x000000000000000000000000000000d0492104db91551b307d94f2ab08cc79f4","0x000000000000000000000000000000000025f2f754a0726238075d9def0babb9","0x000000000000000000000000000000cb74279f288c14712784e2fa6490e4536b","0x00000000000000000000000000000000001cb622d0b3b58b69ee8edce2674300","0x000000000000000000000000000000e2295164532fcce82a3c013e597c594ef8","0x00000000000000000000000000000000001189d45faed739fc4e16a3dd3f9b7b","0x00000000000000000000000000000019a9d0d71575acbc5f5960590bd2f40a84","0x00000000000000000000000000000000000c0d0b7c8a283361906d364db189df","0x000000000000000000000000000000ffec2c04e68f5bbfb7a031a8dfe73f8bc6","0x00000000000000000000000000000000000a3ebe2336d65cff7e94ebb4a6f2df","0x0000000000000000000000000000000e2e091047811681c2716df0d71df042c8","0x00000000000000000000000000000000001ced77a29b750aec5c8fa1e3c63876","0x0000000000000000000000000000000000000000000000000000000000000001","0x0000000000000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000000000000000000000000000000000000002","0x0000000000000000000000000000000000000000000000000000000000000000","0x000000000000000000000000000000102dd903844972572d5d1c7bb350dd732a","0x00000000000000000000000000000000002e70ec42b25ff4b04de40733b17c5c","0x00000000000000000000000000000030d192a365e278540ff83197f93b508175","0x000000000000000000000000000000000002d5a8f55ad55c32962bfc4e0e6013"] \ No newline at end of file From 77902dabaf28f3a35f1ab1682d942fc27c72c2cc Mon Sep 17 00:00:00 2001 From: brozorec <9572072+brozorec@users.noreply.github.com> Date: Wed, 27 May 2026 14:48:42 +0200 Subject: [PATCH 13/16] chore: add baseline --- .../src/confidential/circuits/Nargo.toml | 2 +- .../circuits/constraints.baseline | 23 ++++--------------- .../circuits/scripts/extract_vks.sh | 2 +- 3 files changed, 6 insertions(+), 21 deletions(-) diff --git a/packages/tokens/src/confidential/circuits/Nargo.toml b/packages/tokens/src/confidential/circuits/Nargo.toml index 0d0fa25a6..e7ce76751 100644 --- a/packages/tokens/src/confidential/circuits/Nargo.toml +++ b/packages/tokens/src/confidential/circuits/Nargo.toml @@ -1,9 +1,9 @@ [workspace] members = [ "lib", - "operator_transfer", "register", "withdraw", + "operator_transfer", "gadgets/assert_on_curve", "gadgets/commit", "gadgets/ecdh", diff --git a/packages/tokens/src/confidential/circuits/constraints.baseline b/packages/tokens/src/confidential/circuits/constraints.baseline index 37eb2232c..a6b250384 100644 --- a/packages/tokens/src/confidential/circuits/constraints.baseline +++ b/packages/tokens/src/confidential/circuits/constraints.baseline @@ -1,23 +1,8 @@ -# Noir constraint baseline (DO NOT EDIT BY HAND) -# -# Normalized output of `nargo info` for the confidential token Noir crate. -# Regenerate with: -# -# cd packages/tokens/src/confidential/circuits -# LC_ALL=C nargo info | grep '^|' | LC_ALL=C sort > constraints.baseline -# -# LC_ALL=C is mandatory -- it makes `sort` use byte order, which is the -# only ordering that's stable across macOS (en_US locale) and the Ubuntu -# CI runner (C locale). -# -# CI diffs the current normalized `nargo info` against this file and -# fails on any change. Bumping constraint counts is expected when the -# lib changes -- regenerate this file in the same PR and reviewers -# will see the delta side-by-side. -# -# Toolchain: nargo 1.0.0-beta.11 -# | Package | Function | Expression Width | ACIR Opcodes | Brillig Opcodes | +| circuit_operator_transfer | decompose_hint | N/A | N/A | 30 | +| circuit_operator_transfer | directive_invert | N/A | N/A | 9 | +| circuit_operator_transfer | lte_hint | N/A | N/A | 33 | +| circuit_operator_transfer | main | Bounded { width: 4 } | 131 | 72 | | circuit_register | decompose_hint | N/A | N/A | 30 | | circuit_register | directive_invert | N/A | N/A | 9 | | circuit_register | lte_hint | N/A | N/A | 33 | diff --git a/packages/tokens/src/confidential/circuits/scripts/extract_vks.sh b/packages/tokens/src/confidential/circuits/scripts/extract_vks.sh index fea6866ff..4ff68b20c 100755 --- a/packages/tokens/src/confidential/circuits/scripts/extract_vks.sh +++ b/packages/tokens/src/confidential/circuits/scripts/extract_vks.sh @@ -22,9 +22,9 @@ set -euo pipefail cd "$(dirname "$0")/.." CIRCUITS=( - "operator_transfer" "register" "withdraw" + "operator_transfer" ) OUT_DIR="vks" From f73f251f5913adec4816826799e4410674bbb1eb Mon Sep 17 00:00:00 2001 From: brozorec <9572072+brozorec@users.noreply.github.com> Date: Wed, 27 May 2026 14:59:25 +0200 Subject: [PATCH 14/16] fix(confidential): align operator-transfer PVK doctrine with transfer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../circuits/operator_transfer/src/main.nr | 23 +++++------ .../circuits/operator_transfer/src/tests.nr | 38 ------------------- .../circuits/vks/operator_transfer.vk.json | 2 +- 3 files changed, 11 insertions(+), 52 deletions(-) diff --git a/packages/tokens/src/confidential/circuits/operator_transfer/src/main.nr b/packages/tokens/src/confidential/circuits/operator_transfer/src/main.nr index ef565b599..3f504366a 100644 --- a/packages/tokens/src/confidential/circuits/operator_transfer/src/main.nr +++ b/packages/tokens/src/confidential/circuits/operator_transfer/src/main.nr @@ -118,15 +118,14 @@ mod tests; // Y_op, C_a, C_a', C_tx, R_e, S, S_{a,r}, and S_{a,s} are bound to in-circuit // multi_scalar_mul outputs (O1, O2, O11, O8, O6, O5, O_a1, O_a5) and are // therefore on-curve by construction -- no explicit check needed. -// PVK_recipient, K_aud_r, and K_aud_s are public-input keys consumed by ECDH -// multiplications (Section 10.8 path (3)); the verifier doesn't check curve -// membership and an off-curve key would break the soundness of O5 / O_a1 / -// O_a5. This file explicitly validates all three on-curve AND non-identity -// before the corresponding ECDH consumes them. (PVK_recipient is path (2) of -// Section 10.8 -- proof-constrained at the recipient's registration and -// trusted on read from `to.viewing_public_key` -- but the explicit check -// keeps the operator-side circuit defensive even if the recipient slot is -// ever loaded outside the wrapper.) +// PVK_recipient was constrained on-curve when the recipient registered (R3, +// Section 7.2) and is loaded by the wrapper from trusted storage, so it is +// path (2) of Section 10.8 and the circuit consumes it without re-checking. +// K_aud_r and K_aud_s are public-input keys with no proof constraint at +// insertion (path (3)) -- the verifier doesn't check curve membership and +// an off-curve key would break the soundness of O_a1 / O_a5. This file +// explicitly validates both on-curve AND non-identity before the +// corresponding ECDH consumes them. // // Public inputs (24 fields, in design-doc canonical order) // -------------------------------------------------------- @@ -250,12 +249,10 @@ fn main( assert(r_e_derived.x == r_e_x); assert(r_e_derived.y == r_e_y); - // O5 -- PVK_recipient validated on-curve + non-identity before consumption - // (Section 10.8 path (3) wrt this circuit; defensive even though the - // recipient's PVK was proof-constrained at registration, R3). + // O5 -- PVK_recipient is path (2) of Section 10.8 (proof-constrained at + // the recipient's registration via R3, trusted on read). let pvk_recipient = EmbeddedCurvePoint { x: pvk_recipient_x, y: pvk_recipient_y, is_infinite: false }; - assert_on_curve_non_identity(pvk_recipient); let s_x = ecdh(r_e, pvk_recipient); // O7 (anti-poisoning, Section 5.4) diff --git a/packages/tokens/src/confidential/circuits/operator_transfer/src/tests.nr b/packages/tokens/src/confidential/circuits/operator_transfer/src/tests.nr index be5112271..b0a68a0b3 100644 --- a/packages/tokens/src/confidential/circuits/operator_transfer/src/tests.nr +++ b/packages/tokens/src/confidential/circuits/operator_transfer/src/tests.nr @@ -803,44 +803,6 @@ fn rejects_wrong_recipient_pvk() { ); } -#[test(should_fail)] -fn rejects_off_curve_pvk_recipient() { - // (1, 2) is off-curve: 4 != 1 - 17. The on-curve check on PVK_recipient - // fires before O5 ever consumes it. - run_main( - SK_OP, - DVK_I, - V_A, - R_A, - V_TX, - R_E, - C_A_X, - C_A_Y, - SIGMA_A, - Y_OP_X, - Y_OP_Y, - 1, - 2, - K_AUD_R_X, - K_AUD_R_Y, - K_AUD_S_X, - K_AUD_S_Y, - C_A_NEW_X, - C_A_NEW_Y, - C_TX_X, - C_TX_Y, - R_E_X, - R_E_Y, - V_TILDE, - A_TILDE_NEW, - SIGMA_A_NEW, - V_TILDE_AUD_R, - R_TILDE_AUD_R, - V_TILDE_AUD_S, - A_TILDE_AUD_S, - ); -} - #[test(should_fail)] fn rejects_poisoned_c_tx() { // Anti-poisoning (Section 5.4): a prover that commits C_tx with arbitrary diff --git a/packages/tokens/src/confidential/circuits/vks/operator_transfer.vk.json b/packages/tokens/src/confidential/circuits/vks/operator_transfer.vk.json index 465f65aeb..bd9f3e017 100644 --- a/packages/tokens/src/confidential/circuits/vks/operator_transfer.vk.json +++ b/packages/tokens/src/confidential/circuits/vks/operator_transfer.vk.json @@ -1 +1 @@ -["0x0000000000000000000000000000000000000000000000000000000000008000","0x0000000000000000000000000000000000000000000000000000000000000028","0x0000000000000000000000000000000000000000000000000000000000000001","0x0000000000000000000000000000000000000000000000000000000000000018","0x0000000000000000000000000000006000afa08abfe7aa7f968697dfe228b036","0x00000000000000000000000000000000002ca36b4afe95c199b2a6685a92db33","0x0000000000000000000000000000000320935e2f28bf769df11a14db17376bfd","0x00000000000000000000000000000000002b3f8ea4c775be9e2ee523055d9ad8","0x000000000000000000000000000000854b28d8d2c71b3634b744414fca185df9","0x000000000000000000000000000000000002823a3bf5322f5557e99e10f1127e","0x000000000000000000000000000000d36ed5360551ef5f0978f5d3abd8b62a53","0x000000000000000000000000000000000026fc2eacfdddbe01373b088e73443a","0x00000000000000000000000000000088306bc4689e0b8fe250c25437e57692b0","0x000000000000000000000000000000000020429c751d7ccb837c99d4c46db9aa","0x0000000000000000000000000000004138b4a2859a6ee0f86212546bfa5a0beb","0x0000000000000000000000000000000000292bab64c7528cee6118dcef0239d4","0x000000000000000000000000000000443c807f3ad9eb6bd1b643a80d165cc077","0x00000000000000000000000000000000001c08a8fad078fc4e4ba3df6155f038","0x000000000000000000000000000000c604459995a06cc819d645ddba6f35d112","0x000000000000000000000000000000000029035fe9d977655c1c356907b2b091","0x000000000000000000000000000000e5bca0eaea1007dd32d37dd3fd38ea631b","0x00000000000000000000000000000000000de187ad385f834da7dc2f6efa3281","0x0000000000000000000000000000009128946800e08ab19b10c06ee3f7bb95fb","0x000000000000000000000000000000000022e8c0c8d3b1604c2a84b5fafecf6c","0x000000000000000000000000000000582a4c9bf781a3852ded9134a9d382de09","0x00000000000000000000000000000000000e1db518c51dd5a790952dc43c535e","0x0000000000000000000000000000009822d6ab747b7394c0bbb64dd302d0fa0b","0x00000000000000000000000000000000002cdd6d30bad8f2875bba349813b203","0x0000000000000000000000000000005738f222d113ad96d990e7d4a004ec2e57","0x000000000000000000000000000000000014e35bf3517c8beabf7ec116bd4c00","0x000000000000000000000000000000d89185b10a6c32362672867293fa6e87a6","0x00000000000000000000000000000000002cbca48853bc69fafe318ba9e1f9cc","0x00000000000000000000000000000033039cc41a55955127fd7bc892962ea8c7","0x00000000000000000000000000000000002dfe905b3e2d6d1d8217c2ff6cd6d4","0x00000000000000000000000000000075bc684506ebf05e35935da2774fd4e5e7","0x000000000000000000000000000000000008a9a2b23c34d8f3fbc1648b816351","0x0000000000000000000000000000001b69b1f99b8363d20252bb6b009fb3d7cc","0x0000000000000000000000000000000000220351383686f263064852f10751ea","0x000000000000000000000000000000463574f70920415f428cf523d40dc35e93","0x000000000000000000000000000000000012ad9197d03697766a98de24f77365","0x0000000000000000000000000000002adbdaf296abeb97607607fa45b99f41a2","0x000000000000000000000000000000000006caddd92a99e4702404f805155379","0x000000000000000000000000000000ba9a2d5d9fbfe151d8bc67ed55fb1f7ff2","0x0000000000000000000000000000000000259594fd50f8c1435a2eed1df76f5d","0x000000000000000000000000000000f3b23eccd9427c94fb5cbebe8d866cd778","0x000000000000000000000000000000000008a2b18955c779693cf0837ca1226c","0x000000000000000000000000000000b2937388964bc3a63fe359b15cad47481d","0x00000000000000000000000000000000002786ef69eed6d0022ef73979044ba3","0x000000000000000000000000000000939a7e368cedd5bf3c29e006369df2cc87","0x00000000000000000000000000000000001abf142490bfafec1a85082789f94f","0x0000000000000000000000000000007e869ef3a5da8819090c949306fa189f8f","0x0000000000000000000000000000000000083a789a8138b415a24e7343537560","0x000000000000000000000000000000cefac383eac2687725cdcfef62b0cc84dd","0x00000000000000000000000000000000001297bb2e1f2db1e3f61ee7b42f1f31","0x0000000000000000000000000000004d29f075c898c4625db4537ed7309f1244","0x000000000000000000000000000000000022a5a91375af5f73b612239f620c03","0x000000000000000000000000000000974724a1ca411a070583424fb95e99b8a3","0x0000000000000000000000000000000000002ee741304cb544808aa13987e2ac","0x000000000000000000000000000000451a5e05496018b5dde361de16bcc0065d","0x00000000000000000000000000000000000f2d5c46da580eb694929590c311d0","0x000000000000000000000000000000ce647a6d734a872af83ba8fbbde6993023","0x0000000000000000000000000000000000296394b32d292f97720d84b45cbb7a","0x00000000000000000000000000000081f52cefd7f6aba906e61b16ece26b1e50","0x0000000000000000000000000000000000225a10753fdde52a72926cac71dbfc","0x000000000000000000000000000000345b18f2cd544f07af40f551a7c2101c9d","0x00000000000000000000000000000000001f0a819ebe371911f80507b3e6b0b0","0x000000000000000000000000000000ca1d9e9cea934bf69022cda9ba3ae9b99a","0x00000000000000000000000000000000002e8e8b5cdd13a0d5de1418a110f648","0x0000000000000000000000000000003bd2d25bd9eb79b8c34780ed69cd204d0b","0x000000000000000000000000000000000023d4cbab015bd91bbd697772fe442f","0x000000000000000000000000000000eb38fb2c107c605c019d2982e206d3263a","0x00000000000000000000000000000000002bb2720b816b2ed876b849b0c1c114","0x0000000000000000000000000000008d695540e7da4bbf1663ff0b9312d1a0ba","0x000000000000000000000000000000000014811a798e6dc9f27ea3a1972d546f","0x0000000000000000000000000000003b41a97aca330a77624e411d325a2b5d33","0x0000000000000000000000000000000000033d6f6cb02d26347fea56d021d19b","0x0000000000000000000000000000007804650f7457393a89b7ca095e19f4377e","0x00000000000000000000000000000000000e5796b0f7aeb40e3ca6418aea0ee6","0x000000000000000000000000000000abec380a26d38ce65cb66441a6bfee4a82","0x00000000000000000000000000000000000f8a7a22469a819644d9752d33ea6a","0x000000000000000000000000000000c67b5353e3e700df86864f844b3877d0e0","0x0000000000000000000000000000000000184e98ee0a9d88cf1a71cc9d4c9ae3","0x00000000000000000000000000000062f8be694721a883d04939611bd13f5e8e","0x00000000000000000000000000000000001d0c07aca55566e398993a611efc22","0x000000000000000000000000000000489bc85eb60d891d6796dc59728f0bfbfa","0x000000000000000000000000000000000025eddc5c092aa77314a05f9ee6eda9","0x000000000000000000000000000000bc2e4048990d47af5a98c1eced27570293","0x000000000000000000000000000000000026a0245faff4f37863b7f18c5f7aa1","0x000000000000000000000000000000ffad1d95a8ee7ccd2287a489375de526f6","0x00000000000000000000000000000000002549709cd2124b19800eed4f003e52","0x00000000000000000000000000000043c4b44d03e114c03c1689f77c5789643c","0x00000000000000000000000000000000000621edcb5a9eab5ae5675b4da9d9a2","0x000000000000000000000000000000d0492104db91551b307d94f2ab08cc79f4","0x000000000000000000000000000000000025f2f754a0726238075d9def0babb9","0x000000000000000000000000000000cb74279f288c14712784e2fa6490e4536b","0x00000000000000000000000000000000001cb622d0b3b58b69ee8edce2674300","0x000000000000000000000000000000e2295164532fcce82a3c013e597c594ef8","0x00000000000000000000000000000000001189d45faed739fc4e16a3dd3f9b7b","0x00000000000000000000000000000019a9d0d71575acbc5f5960590bd2f40a84","0x00000000000000000000000000000000000c0d0b7c8a283361906d364db189df","0x000000000000000000000000000000ffec2c04e68f5bbfb7a031a8dfe73f8bc6","0x00000000000000000000000000000000000a3ebe2336d65cff7e94ebb4a6f2df","0x0000000000000000000000000000000e2e091047811681c2716df0d71df042c8","0x00000000000000000000000000000000001ced77a29b750aec5c8fa1e3c63876","0x0000000000000000000000000000000000000000000000000000000000000001","0x0000000000000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000000000000000000000000000000000000002","0x0000000000000000000000000000000000000000000000000000000000000000","0x000000000000000000000000000000102dd903844972572d5d1c7bb350dd732a","0x00000000000000000000000000000000002e70ec42b25ff4b04de40733b17c5c","0x00000000000000000000000000000030d192a365e278540ff83197f93b508175","0x000000000000000000000000000000000002d5a8f55ad55c32962bfc4e0e6013"] \ No newline at end of file +["0x0000000000000000000000000000000000000000000000000000000000008000","0x0000000000000000000000000000000000000000000000000000000000000028","0x0000000000000000000000000000000000000000000000000000000000000001","0x0000000000000000000000000000000000000000000000000000000000000018","0x0000000000000000000000000000001740946cd15a5a8e68e9cdba3a3b3631ff","0x00000000000000000000000000000000000a8a2e0ce5243c97a17d17bdcc084e","0x0000000000000000000000000000009d68f6375be6c69c390f7311f1831123d2","0x000000000000000000000000000000000024f955a28ebb53da614fc1082ba785","0x00000000000000000000000000000015473537d766618d231ccd70a4bb3c55ea","0x00000000000000000000000000000000000882a860f48dee7a99901bcf378bfc","0x00000000000000000000000000000069733038ac69351fff4622ad001afb54d9","0x00000000000000000000000000000000002a08a1af1bc8cfe120b692b4f307b4","0x0000000000000000000000000000004be950251f2a49e94b7d44eb9e3afe90ac","0x000000000000000000000000000000000002a1f848816ced4fa8fb6aa16a32e7","0x00000000000000000000000000000063c51e193000e35a3024e03dabea872f0a","0x00000000000000000000000000000000001cd9470b1fde4d481171ee6dbac11c","0x0000000000000000000000000000005c20dd2c8d2a4be33333a03bb10a4d0af3","0x000000000000000000000000000000000013304730bd8d37fbb61d13439f17fe","0x0000000000000000000000000000008ccb6ab6f78f10ecd8176b188d304db523","0x000000000000000000000000000000000000d0533c04fb8258b94beb3f9a3f6e","0x00000000000000000000000000000083969ed0678e296f71d160ee1c8f01ca0b","0x0000000000000000000000000000000000057e4afcddafcd271800d46769adb6","0x000000000000000000000000000000aef8a4f4f60aca19b6bc03cb0ac0bbe8df","0x00000000000000000000000000000000002cfac23b7403a58513dae78a9d83fd","0x0000000000000000000000000000004813a08680fbf7afcb0969d2d4b0ee41bb","0x0000000000000000000000000000000000296dd9dc6e728b1f8e412fa88aeec1","0x0000000000000000000000000000001a2a2d41528717c08b68b5d0dc5608bf22","0x000000000000000000000000000000000019201c7715e9a08b621f2c2732535d","0x0000000000000000000000000000005738f222d113ad96d990e7d4a004ec2e57","0x000000000000000000000000000000000014e35bf3517c8beabf7ec116bd4c00","0x000000000000000000000000000000d89185b10a6c32362672867293fa6e87a6","0x00000000000000000000000000000000002cbca48853bc69fafe318ba9e1f9cc","0x0000000000000000000000000000001b3b5deab496cf20a41d61b1f67844fb4d","0x00000000000000000000000000000000002dc22a576e2591c83b447533977f8a","0x00000000000000000000000000000057341529f79d03dd62e749baa54cfe7e1d","0x00000000000000000000000000000000000957060117dc105b7a9e76f028f3fd","0x00000000000000000000000000000039a3affe717e7edef72aef947f2bd3279e","0x00000000000000000000000000000000001196ad96eb0d57152706d91a19eca0","0x00000000000000000000000000000066a5915576615a912c06e4a7665ada33ba","0x0000000000000000000000000000000000165be61904cfbe4f39ab8c4f5b87bf","0x00000000000000000000000000000083dbc408fba901681dd66a399fe270e462","0x000000000000000000000000000000000013d328801d5fa690c5a97c6ae2ee32","0x0000000000000000000000000000002e4d83f3c7704fdca18d6205a59bde77aa","0x000000000000000000000000000000000007a5147133806547ef310cb1d74c67","0x00000000000000000000000000000019298cfd7bab2e6cdb0c5272676631ed46","0x000000000000000000000000000000000005c89812673c4f6d7e13f5b2ea8705","0x00000000000000000000000000000055dcd4c4f6d3153df55ead758a3cda2f63","0x00000000000000000000000000000000002d5a7fee9ee96e65838db2cbe08ee7","0x000000000000000000000000000000355105877e82c678a908bb0f829463550c","0x0000000000000000000000000000000000167de6477f45878724ec65fa2aaf4d","0x000000000000000000000000000000cc696ef98d12871a50d59ea8f7db3eb381","0x00000000000000000000000000000000001b44b43157a6c9a02e77366b94c608","0x0000000000000000000000000000005df55d604821679fb946ca971b3574e25b","0x00000000000000000000000000000000000e3f2a35435acb6f44b38cdf797e3b","0x00000000000000000000000000000076c63823bf2b28b9bc3e31c789659034ff","0x00000000000000000000000000000000002dd8cfc98661e2ca5dc139faa3f5fc","0x0000000000000000000000000000009e80bae2bf071fc10a30495c28fa43a826","0x000000000000000000000000000000000016f1de60cc10aa0fcf76b79da63c13","0x000000000000000000000000000000fae24eb9b039cb600d7aa70071b1ad9983","0x00000000000000000000000000000000001473361fa0206007cdab582d1bf570","0x00000000000000000000000000000098a160a6a13c00ea6bcd8da606854a1b6e","0x00000000000000000000000000000000001ca29f611f7081c4e6ae51430e4a2c","0x000000000000000000000000000000cd0ed27c8cf9c25d9eaa25ccd10504d447","0x000000000000000000000000000000000014ef98057d5d44cfd2762f6b789346","0x0000000000000000000000000000001435fb33069031db948c5a6bf3157ad522","0x000000000000000000000000000000000015dc5540cddd57cd08909fa7c98a5e","0x000000000000000000000000000000c57ee3359b56c395eec0da59d8f0943a82","0x00000000000000000000000000000000001a3ac1d5b9ae8c8b3285ed8c9cfdbc","0x0000000000000000000000000000007a9eedab32e3d3030312d643e39bc638ae","0x000000000000000000000000000000000011d59670e93ad1b3e56c58ca15ad14","0x000000000000000000000000000000727fd02e57e7323cb5f02e843f05b00cc8","0x00000000000000000000000000000000001baf71908e86bd4d4708587368ce85","0x0000000000000000000000000000004cfdb7315ea7e185b17968799af464a59c","0x00000000000000000000000000000000002d47ea987bd729be91d27080c9e18f","0x000000000000000000000000000000abb1c0ee2bc9a3142ed970616d1a34f7cc","0x000000000000000000000000000000000010f5cd9027554dbb157935d5e3e3f9","0x000000000000000000000000000000a1535129a6c4099ffe5c3719c030fc7c64","0x00000000000000000000000000000000000f12573381e092b0b09dc8511ca7af","0x0000000000000000000000000000000b535f968fa0fd1a6cdca2c52bf16828ef","0x000000000000000000000000000000000008d89bd765a82f92d849fe2a9a201c","0x000000000000000000000000000000db41862b210b40f05ddd8296d5921a54dd","0x000000000000000000000000000000000013d55e08ffe3f4bb52dd6aad972cf2","0x0000000000000000000000000000005ecac81268cdb4eb64be71dab62572f552","0x000000000000000000000000000000000023ed1703441f83031171267eb20992","0x0000000000000000000000000000002538c71288a68e358860fc46837102acdb","0x0000000000000000000000000000000000275ae862567a0772b5de8c02b74779","0x000000000000000000000000000000eaf91a4c67ef1d49e186092585099af555","0x0000000000000000000000000000000000177f1fd70b92470bf8c254e8fafb5e","0x000000000000000000000000000000ffad1d95a8ee7ccd2287a489375de526f6","0x00000000000000000000000000000000002549709cd2124b19800eed4f003e52","0x00000000000000000000000000000043c4b44d03e114c03c1689f77c5789643c","0x00000000000000000000000000000000000621edcb5a9eab5ae5675b4da9d9a2","0x000000000000000000000000000000d0492104db91551b307d94f2ab08cc79f4","0x000000000000000000000000000000000025f2f754a0726238075d9def0babb9","0x000000000000000000000000000000cb74279f288c14712784e2fa6490e4536b","0x00000000000000000000000000000000001cb622d0b3b58b69ee8edce2674300","0x000000000000000000000000000000e2295164532fcce82a3c013e597c594ef8","0x00000000000000000000000000000000001189d45faed739fc4e16a3dd3f9b7b","0x00000000000000000000000000000019a9d0d71575acbc5f5960590bd2f40a84","0x00000000000000000000000000000000000c0d0b7c8a283361906d364db189df","0x000000000000000000000000000000ffec2c04e68f5bbfb7a031a8dfe73f8bc6","0x00000000000000000000000000000000000a3ebe2336d65cff7e94ebb4a6f2df","0x0000000000000000000000000000000e2e091047811681c2716df0d71df042c8","0x00000000000000000000000000000000001ced77a29b750aec5c8fa1e3c63876","0x0000000000000000000000000000000000000000000000000000000000000001","0x0000000000000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000000000000000000000000000000000000002","0x0000000000000000000000000000000000000000000000000000000000000000","0x000000000000000000000000000000e8d7b27b93a5ad0e59b01a3ab0b2cdfc61","0x00000000000000000000000000000000000959fb1c1d6920eb32cd0cf4c90a9b","0x000000000000000000000000000000f8acce29af6f26449d3ca91bc1d65963f4","0x000000000000000000000000000000000010832525acffd17f238b374a6208bf"] \ No newline at end of file From 8314a44d163072bf20e679278347a999dca306ca Mon Sep 17 00:00:00 2001 From: brozorec <9572072+brozorec@users.noreply.github.com> Date: Wed, 27 May 2026 15:13:00 +0200 Subject: [PATCH 15/16] chore(confidential): restore constraints.baseline header 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. --- .../circuits/constraints.baseline | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/packages/tokens/src/confidential/circuits/constraints.baseline b/packages/tokens/src/confidential/circuits/constraints.baseline index a6b250384..c40a3bafe 100644 --- a/packages/tokens/src/confidential/circuits/constraints.baseline +++ b/packages/tokens/src/confidential/circuits/constraints.baseline @@ -1,3 +1,22 @@ +# Noir constraint baseline (DO NOT EDIT BY HAND) +# +# Normalized output of `nargo info` for the confidential token Noir crate. +# Regenerate with: +# +# cd packages/tokens/src/confidential/circuits +# LC_ALL=C nargo info | grep '^|' | LC_ALL=C sort > constraints.baseline +# +# LC_ALL=C is mandatory -- it makes `sort` use byte order, which is the +# only ordering that's stable across macOS (en_US locale) and the Ubuntu +# CI runner (C locale). +# +# CI diffs the current normalized `nargo info` against this file and +# fails on any change. Bumping constraint counts is expected when the +# lib changes -- regenerate this file in the same PR and reviewers +# will see the delta side-by-side. +# +# Toolchain: nargo 1.0.0-beta.11 +# | Package | Function | Expression Width | ACIR Opcodes | Brillig Opcodes | | circuit_operator_transfer | decompose_hint | N/A | N/A | 30 | | circuit_operator_transfer | directive_invert | N/A | N/A | 9 | From b92ce4c370c6d90f4f5e15166da923514b5c4580 Mon Sep 17 00:00:00 2001 From: brozorec <9572072+brozorec@users.noreply.github.com> Date: Wed, 27 May 2026 15:25:59 +0200 Subject: [PATCH 16/16] feat: add sigmas inequality constraint --- .../circuits/operator_transfer/src/main.nr | 7 +++ .../circuits/operator_transfer/src/tests.nr | 57 ++++++++++++++++++- .../circuits/vks/operator_transfer.vk.json | 2 +- 3 files changed, 64 insertions(+), 2 deletions(-) diff --git a/packages/tokens/src/confidential/circuits/operator_transfer/src/main.nr b/packages/tokens/src/confidential/circuits/operator_transfer/src/main.nr index 3f504366a..9d22ec5cb 100644 --- a/packages/tokens/src/confidential/circuits/operator_transfer/src/main.nr +++ b/packages/tokens/src/confidential/circuits/operator_transfer/src/main.nr @@ -45,6 +45,10 @@ mod tests; // sigma_a) Encrypted amount // (emitted). // Allowance state (new) +// -- sigma_a' != sigma_a Salt rotation +// (implementation +// hardening; not in +// Section 7.8). // O10 r_a' = Poseidon2(delta_allow_r, dvk_i, // sigma_a') New allowance // randomness. @@ -267,6 +271,9 @@ fn main( let v_tilde_derived = encrypt_amount(v_tx, s_x, sigma_a); assert(v_tilde_derived == v_tilde); + // Salt rotation (implementation hardening). + assert(sigma_a_new != sigma_a); + // O10 let r_a_new = derive_allow_r(dvk_i, sigma_a_new); diff --git a/packages/tokens/src/confidential/circuits/operator_transfer/src/tests.nr b/packages/tokens/src/confidential/circuits/operator_transfer/src/tests.nr index b0a68a0b3..b31b5d887 100644 --- a/packages/tokens/src/confidential/circuits/operator_transfer/src/tests.nr +++ b/packages/tokens/src/confidential/circuits/operator_transfer/src/tests.nr @@ -594,7 +594,7 @@ fn rejects_wrong_allowance_opening() { } #[test(should_fail)] -fn rejects_under_funded_transfer() { +fn rejects_v_a_new_out_of_range() { // v_tx > v_a: v_a - v_tx underflows in the field to a value >= 2^128. // O4's `v_a_new` range check fires. Every downstream public is // recomputed against v_tx_too_large so the single firing constraint is @@ -887,6 +887,14 @@ fn rejects_tampered_v_tilde() { ); } +// O10 (`r_a' = Poseidon2(delta_allow_r, dvk_i, sigma_a')`) is exercised +// transitively by the two tests below: a tampered `c_a_new` is detected by +// O11 (which consumes `r_a_new`), and a tampered `a_tilde_new` is detected +// by O12 (which consumes `dvk_i` and `sigma_a'` directly). `dvk_i` is the +// single shared witness between O3 and O10, so isolating O10 with its own +// dedicated negative test would require a second `dvk_i` witness that +// doesn't exist in the circuit. + #[test(should_fail)] fn rejects_tampered_a_tilde_new() { // O12 fires. @@ -961,6 +969,53 @@ fn rejects_tampered_c_a_new() { ); } +#[test(should_fail)] +fn rejects_sigma_a_new_equal_to_sigma_a() { + // Salt-rotation hardening: the circuit rejects `sigma_a' == sigma_a` + // even though Section 7.8 does not require it. With `sigma_a' = sigma_a`: + // r_a_new = derive_allow_r(dvk_i, sigma_a_new) = r_a + // c_a_new = (V_A - V_TX) * G + r_a * H + // a_tilde_new = encrypt_allowance(V_A - V_TX, dvk_i, sigma_a_new) + // All downstream publics are recomputed consistently so the only + // firing constraint is `assert(sigma_a_new != sigma_a)`. + let r_a_new = derive_allow_r(DVK_I, SIGMA_A); + let c_a_new = commit(V_A_NEW, r_a_new); + let a_tilde_new = encrypt_allowance(V_A_NEW, DVK_I, SIGMA_A); + + run_main( + SK_OP, + DVK_I, + V_A, + R_A, + V_TX, + R_E, + C_A_X, + C_A_Y, + SIGMA_A, + Y_OP_X, + Y_OP_Y, + PVK_RECIPIENT_X, + PVK_RECIPIENT_Y, + K_AUD_R_X, + K_AUD_R_Y, + K_AUD_S_X, + K_AUD_S_Y, + c_a_new.x, + c_a_new.y, + C_TX_X, + C_TX_Y, + R_E_X, + R_E_Y, + V_TILDE, + a_tilde_new, + SIGMA_A, + V_TILDE_AUD_R, + R_TILDE_AUD_R, + V_TILDE_AUD_S, + A_TILDE_AUD_S, + ); +} + #[test(should_fail)] fn rejects_r_e_zero() { // O13 fires: r_e = 0 would force R_e = O (identity) and collapse every diff --git a/packages/tokens/src/confidential/circuits/vks/operator_transfer.vk.json b/packages/tokens/src/confidential/circuits/vks/operator_transfer.vk.json index bd9f3e017..2ecd3479f 100644 --- a/packages/tokens/src/confidential/circuits/vks/operator_transfer.vk.json +++ b/packages/tokens/src/confidential/circuits/vks/operator_transfer.vk.json @@ -1 +1 @@ -["0x0000000000000000000000000000000000000000000000000000000000008000","0x0000000000000000000000000000000000000000000000000000000000000028","0x0000000000000000000000000000000000000000000000000000000000000001","0x0000000000000000000000000000000000000000000000000000000000000018","0x0000000000000000000000000000001740946cd15a5a8e68e9cdba3a3b3631ff","0x00000000000000000000000000000000000a8a2e0ce5243c97a17d17bdcc084e","0x0000000000000000000000000000009d68f6375be6c69c390f7311f1831123d2","0x000000000000000000000000000000000024f955a28ebb53da614fc1082ba785","0x00000000000000000000000000000015473537d766618d231ccd70a4bb3c55ea","0x00000000000000000000000000000000000882a860f48dee7a99901bcf378bfc","0x00000000000000000000000000000069733038ac69351fff4622ad001afb54d9","0x00000000000000000000000000000000002a08a1af1bc8cfe120b692b4f307b4","0x0000000000000000000000000000004be950251f2a49e94b7d44eb9e3afe90ac","0x000000000000000000000000000000000002a1f848816ced4fa8fb6aa16a32e7","0x00000000000000000000000000000063c51e193000e35a3024e03dabea872f0a","0x00000000000000000000000000000000001cd9470b1fde4d481171ee6dbac11c","0x0000000000000000000000000000005c20dd2c8d2a4be33333a03bb10a4d0af3","0x000000000000000000000000000000000013304730bd8d37fbb61d13439f17fe","0x0000000000000000000000000000008ccb6ab6f78f10ecd8176b188d304db523","0x000000000000000000000000000000000000d0533c04fb8258b94beb3f9a3f6e","0x00000000000000000000000000000083969ed0678e296f71d160ee1c8f01ca0b","0x0000000000000000000000000000000000057e4afcddafcd271800d46769adb6","0x000000000000000000000000000000aef8a4f4f60aca19b6bc03cb0ac0bbe8df","0x00000000000000000000000000000000002cfac23b7403a58513dae78a9d83fd","0x0000000000000000000000000000004813a08680fbf7afcb0969d2d4b0ee41bb","0x0000000000000000000000000000000000296dd9dc6e728b1f8e412fa88aeec1","0x0000000000000000000000000000001a2a2d41528717c08b68b5d0dc5608bf22","0x000000000000000000000000000000000019201c7715e9a08b621f2c2732535d","0x0000000000000000000000000000005738f222d113ad96d990e7d4a004ec2e57","0x000000000000000000000000000000000014e35bf3517c8beabf7ec116bd4c00","0x000000000000000000000000000000d89185b10a6c32362672867293fa6e87a6","0x00000000000000000000000000000000002cbca48853bc69fafe318ba9e1f9cc","0x0000000000000000000000000000001b3b5deab496cf20a41d61b1f67844fb4d","0x00000000000000000000000000000000002dc22a576e2591c83b447533977f8a","0x00000000000000000000000000000057341529f79d03dd62e749baa54cfe7e1d","0x00000000000000000000000000000000000957060117dc105b7a9e76f028f3fd","0x00000000000000000000000000000039a3affe717e7edef72aef947f2bd3279e","0x00000000000000000000000000000000001196ad96eb0d57152706d91a19eca0","0x00000000000000000000000000000066a5915576615a912c06e4a7665ada33ba","0x0000000000000000000000000000000000165be61904cfbe4f39ab8c4f5b87bf","0x00000000000000000000000000000083dbc408fba901681dd66a399fe270e462","0x000000000000000000000000000000000013d328801d5fa690c5a97c6ae2ee32","0x0000000000000000000000000000002e4d83f3c7704fdca18d6205a59bde77aa","0x000000000000000000000000000000000007a5147133806547ef310cb1d74c67","0x00000000000000000000000000000019298cfd7bab2e6cdb0c5272676631ed46","0x000000000000000000000000000000000005c89812673c4f6d7e13f5b2ea8705","0x00000000000000000000000000000055dcd4c4f6d3153df55ead758a3cda2f63","0x00000000000000000000000000000000002d5a7fee9ee96e65838db2cbe08ee7","0x000000000000000000000000000000355105877e82c678a908bb0f829463550c","0x0000000000000000000000000000000000167de6477f45878724ec65fa2aaf4d","0x000000000000000000000000000000cc696ef98d12871a50d59ea8f7db3eb381","0x00000000000000000000000000000000001b44b43157a6c9a02e77366b94c608","0x0000000000000000000000000000005df55d604821679fb946ca971b3574e25b","0x00000000000000000000000000000000000e3f2a35435acb6f44b38cdf797e3b","0x00000000000000000000000000000076c63823bf2b28b9bc3e31c789659034ff","0x00000000000000000000000000000000002dd8cfc98661e2ca5dc139faa3f5fc","0x0000000000000000000000000000009e80bae2bf071fc10a30495c28fa43a826","0x000000000000000000000000000000000016f1de60cc10aa0fcf76b79da63c13","0x000000000000000000000000000000fae24eb9b039cb600d7aa70071b1ad9983","0x00000000000000000000000000000000001473361fa0206007cdab582d1bf570","0x00000000000000000000000000000098a160a6a13c00ea6bcd8da606854a1b6e","0x00000000000000000000000000000000001ca29f611f7081c4e6ae51430e4a2c","0x000000000000000000000000000000cd0ed27c8cf9c25d9eaa25ccd10504d447","0x000000000000000000000000000000000014ef98057d5d44cfd2762f6b789346","0x0000000000000000000000000000001435fb33069031db948c5a6bf3157ad522","0x000000000000000000000000000000000015dc5540cddd57cd08909fa7c98a5e","0x000000000000000000000000000000c57ee3359b56c395eec0da59d8f0943a82","0x00000000000000000000000000000000001a3ac1d5b9ae8c8b3285ed8c9cfdbc","0x0000000000000000000000000000007a9eedab32e3d3030312d643e39bc638ae","0x000000000000000000000000000000000011d59670e93ad1b3e56c58ca15ad14","0x000000000000000000000000000000727fd02e57e7323cb5f02e843f05b00cc8","0x00000000000000000000000000000000001baf71908e86bd4d4708587368ce85","0x0000000000000000000000000000004cfdb7315ea7e185b17968799af464a59c","0x00000000000000000000000000000000002d47ea987bd729be91d27080c9e18f","0x000000000000000000000000000000abb1c0ee2bc9a3142ed970616d1a34f7cc","0x000000000000000000000000000000000010f5cd9027554dbb157935d5e3e3f9","0x000000000000000000000000000000a1535129a6c4099ffe5c3719c030fc7c64","0x00000000000000000000000000000000000f12573381e092b0b09dc8511ca7af","0x0000000000000000000000000000000b535f968fa0fd1a6cdca2c52bf16828ef","0x000000000000000000000000000000000008d89bd765a82f92d849fe2a9a201c","0x000000000000000000000000000000db41862b210b40f05ddd8296d5921a54dd","0x000000000000000000000000000000000013d55e08ffe3f4bb52dd6aad972cf2","0x0000000000000000000000000000005ecac81268cdb4eb64be71dab62572f552","0x000000000000000000000000000000000023ed1703441f83031171267eb20992","0x0000000000000000000000000000002538c71288a68e358860fc46837102acdb","0x0000000000000000000000000000000000275ae862567a0772b5de8c02b74779","0x000000000000000000000000000000eaf91a4c67ef1d49e186092585099af555","0x0000000000000000000000000000000000177f1fd70b92470bf8c254e8fafb5e","0x000000000000000000000000000000ffad1d95a8ee7ccd2287a489375de526f6","0x00000000000000000000000000000000002549709cd2124b19800eed4f003e52","0x00000000000000000000000000000043c4b44d03e114c03c1689f77c5789643c","0x00000000000000000000000000000000000621edcb5a9eab5ae5675b4da9d9a2","0x000000000000000000000000000000d0492104db91551b307d94f2ab08cc79f4","0x000000000000000000000000000000000025f2f754a0726238075d9def0babb9","0x000000000000000000000000000000cb74279f288c14712784e2fa6490e4536b","0x00000000000000000000000000000000001cb622d0b3b58b69ee8edce2674300","0x000000000000000000000000000000e2295164532fcce82a3c013e597c594ef8","0x00000000000000000000000000000000001189d45faed739fc4e16a3dd3f9b7b","0x00000000000000000000000000000019a9d0d71575acbc5f5960590bd2f40a84","0x00000000000000000000000000000000000c0d0b7c8a283361906d364db189df","0x000000000000000000000000000000ffec2c04e68f5bbfb7a031a8dfe73f8bc6","0x00000000000000000000000000000000000a3ebe2336d65cff7e94ebb4a6f2df","0x0000000000000000000000000000000e2e091047811681c2716df0d71df042c8","0x00000000000000000000000000000000001ced77a29b750aec5c8fa1e3c63876","0x0000000000000000000000000000000000000000000000000000000000000001","0x0000000000000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000000000000000000000000000000000000002","0x0000000000000000000000000000000000000000000000000000000000000000","0x000000000000000000000000000000e8d7b27b93a5ad0e59b01a3ab0b2cdfc61","0x00000000000000000000000000000000000959fb1c1d6920eb32cd0cf4c90a9b","0x000000000000000000000000000000f8acce29af6f26449d3ca91bc1d65963f4","0x000000000000000000000000000000000010832525acffd17f238b374a6208bf"] \ No newline at end of file +["0x0000000000000000000000000000000000000000000000000000000000008000","0x0000000000000000000000000000000000000000000000000000000000000028","0x0000000000000000000000000000000000000000000000000000000000000001","0x0000000000000000000000000000000000000000000000000000000000000018","0x000000000000000000000000000000728e2976c8a3b381c14c127381048a53ff","0x000000000000000000000000000000000016ce552e1b1a624054e3562b94183d","0x000000000000000000000000000000e60fa7e0f78838ff3d4d02f3afc8b29b9f","0x00000000000000000000000000000000002a0ed00b1a3bcb6075c1d60b0dc89f","0x000000000000000000000000000000659e02009282967f681cda2b0fef4e7e2c","0x00000000000000000000000000000000001ece57a1fdae239761aec60d4d2f25","0x00000000000000000000000000000076c40b6a84b2c331f536d595e672f9257a","0x000000000000000000000000000000000001e0d2d659ad103167f468dd991b77","0x000000000000000000000000000000ce7150dcf2d204eb2d8bbdd353f98aff4a","0x000000000000000000000000000000000018ecded80ed6856d7427a721de3b50","0x0000000000000000000000000000006656fefd05d8031f49135b54b6548f9cd6","0x000000000000000000000000000000000028914c2b4b33d2944993748e964c01","0x000000000000000000000000000000942f0f950562f961c12e5a30a23bf798b2","0x0000000000000000000000000000000000094403e315f3b732308e52d309ecd9","0x000000000000000000000000000000d6f7af0b27604cb87c56eaf34784253cc0","0x000000000000000000000000000000000009eaa1afb41f23289e04fd115cbf65","0x000000000000000000000000000000538bbe78c8ae32a9eeeeb53b4af1087639","0x00000000000000000000000000000000000d080f5cb5cb6e3b57215eb11bc602","0x000000000000000000000000000000186f1aa3fd90bc0890e000547ee558c3b1","0x00000000000000000000000000000000001ffd00e007210aac461fa94c8cde62","0x0000000000000000000000000000005dc11a8c8ab837620d133283d42e0c9771","0x00000000000000000000000000000000000be66f79027be91fc01c120a2e3fac","0x00000000000000000000000000000090ccf0fb57cd89fd7152d35fa10b932cd0","0x000000000000000000000000000000000027d9132527f9a009f6045a53a7079f","0x0000000000000000000000000000005738f222d113ad96d990e7d4a004ec2e57","0x000000000000000000000000000000000014e35bf3517c8beabf7ec116bd4c00","0x000000000000000000000000000000d89185b10a6c32362672867293fa6e87a6","0x00000000000000000000000000000000002cbca48853bc69fafe318ba9e1f9cc","0x0000000000000000000000000000006531231bd6ff91d4b3f4d895d0ce4044a3","0x000000000000000000000000000000000027eaaa4dbfab8179a7e1e099f95c94","0x000000000000000000000000000000cbc798523c917716b3815eda28365b7d15","0x00000000000000000000000000000000000e3a4eab74843d295759ac11ff6bf6","0x00000000000000000000000000000017f3b066aeb378654695d217da8c414c93","0x000000000000000000000000000000000001dc76e9fff2ad3aed6f791dd04bf6","0x0000000000000000000000000000006d2a9c6ef25bf999851bd956d4e71050f2","0x00000000000000000000000000000000001be3c330356f2e79309b2e4636b91c","0x0000000000000000000000000000004a7356b403e142436d7b02f899d35c4c81","0x000000000000000000000000000000000001f092fee03c63fca04f6a02215b35","0x00000000000000000000000000000099c97cf0c563a958e6a0fc32df458ee3da","0x000000000000000000000000000000000011f8f0df3984dd840a2faae3f0c8c2","0x000000000000000000000000000000319ee83108dcfa5796295cfd2f763450f2","0x00000000000000000000000000000000002aa088c410185f5bec00a222d97751","0x000000000000000000000000000000e7ec92b10a8e4322bb5fe40b9cbee9407e","0x00000000000000000000000000000000000722c453c7738d57c7d56a5a72e632","0x00000000000000000000000000000035c52301b11020576c2f70eb174d1aa68d","0x0000000000000000000000000000000000092a840276c46fca44dc58bdf735f9","0x00000000000000000000000000000058a18c8d96166b5de22d19e3edf588437f","0x00000000000000000000000000000000000f6d03e84cdddf9b43214b174f2864","0x0000000000000000000000000000004cc33c276d6010148da999e4902610f75c","0x00000000000000000000000000000000000be4f70ff1fb89a03fa0d9d3c928f2","0x00000000000000000000000000000039f52413eb11726ddde84ac343dd3a91de","0x00000000000000000000000000000000000653c620b5282b1f55e0f082c70716","0x00000000000000000000000000000011d63e320df0558e2736b5f0f15dc835b5","0x00000000000000000000000000000000002250ff33088fbe5c802a11960c11c4","0x000000000000000000000000000000f6a4932bdcbd9cb3f78c242740c58b2954","0x00000000000000000000000000000000001b3378dc0c3bfeb11955970a80a197","0x000000000000000000000000000000087959d56d861d383165f77bef23f34519","0x00000000000000000000000000000000000743a84f758794dc44ab96c9409e07","0x000000000000000000000000000000f2584327702442f77815e5986d216dad21","0x0000000000000000000000000000000000281d55540b2c88cf0e7849375ceae4","0x000000000000000000000000000000f02adfdfffa9d184e8c82e977857ef338b","0x0000000000000000000000000000000000121b6fe0db969b709cf5a331479817","0x00000000000000000000000000000037f198caddfdf78cd773ada269cf5e2a2b","0x000000000000000000000000000000000006f862b3b3a1d2affcb3d7d56c4cea","0x00000000000000000000000000000053b2ea393f429cddf8c1e6ad55fe9057b9","0x00000000000000000000000000000000002e44685af86d29cf672f8443f37e26","0x0000000000000000000000000000002d15b6ca8c2a2901da27d5fd817e5d782c","0x00000000000000000000000000000000001331e12fe784170750affdfed585f6","0x000000000000000000000000000000b2dadba131fd2a171969ce57ac9227323b","0x00000000000000000000000000000000002fa6720d2f031c4b6311fe4331039f","0x000000000000000000000000000000087b458b361a365fe55cfe6d88d328ac53","0x0000000000000000000000000000000000055bd5fb6bb736c2002f3adc4efe39","0x000000000000000000000000000000c61a5f0c8d1a1b10df2a0463f3d1acd107","0x0000000000000000000000000000000000098b842d4862030b2c5c64348ec2d1","0x000000000000000000000000000000a09158db7a1f1b4f5f96db1dabd1cf57be","0x00000000000000000000000000000000001ffce9ce4cf64b235eda36bd320d6b","0x000000000000000000000000000000eb177619e4c76a4c13cc4a7f90510dc65e","0x000000000000000000000000000000000002e5e60f1d28e5468d28c53cd7883d","0x000000000000000000000000000000e0a74ae2ea732e40422855c781c069a9d3","0x0000000000000000000000000000000000294ace59ee79eed594830a9ca62ae8","0x000000000000000000000000000000b2b59b433c9dc526c93c3fbb0c45b5266e","0x000000000000000000000000000000000029ed83d3cd468c72f080fb3dfd9294","0x00000000000000000000000000000048e33de80839b8f732e8d11f520eb3b39c","0x000000000000000000000000000000000000e2778436f01eeb9e8e6713aaf148","0x000000000000000000000000000000ffad1d95a8ee7ccd2287a489375de526f6","0x00000000000000000000000000000000002549709cd2124b19800eed4f003e52","0x00000000000000000000000000000043c4b44d03e114c03c1689f77c5789643c","0x00000000000000000000000000000000000621edcb5a9eab5ae5675b4da9d9a2","0x000000000000000000000000000000d0492104db91551b307d94f2ab08cc79f4","0x000000000000000000000000000000000025f2f754a0726238075d9def0babb9","0x000000000000000000000000000000cb74279f288c14712784e2fa6490e4536b","0x00000000000000000000000000000000001cb622d0b3b58b69ee8edce2674300","0x000000000000000000000000000000e2295164532fcce82a3c013e597c594ef8","0x00000000000000000000000000000000001189d45faed739fc4e16a3dd3f9b7b","0x00000000000000000000000000000019a9d0d71575acbc5f5960590bd2f40a84","0x00000000000000000000000000000000000c0d0b7c8a283361906d364db189df","0x000000000000000000000000000000ffec2c04e68f5bbfb7a031a8dfe73f8bc6","0x00000000000000000000000000000000000a3ebe2336d65cff7e94ebb4a6f2df","0x0000000000000000000000000000000e2e091047811681c2716df0d71df042c8","0x00000000000000000000000000000000001ced77a29b750aec5c8fa1e3c63876","0x0000000000000000000000000000000000000000000000000000000000000001","0x0000000000000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000000000000000000000000000000000000002","0x0000000000000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000005fc9c6f0c0aa2bb71dee6940ba2b444829","0x000000000000000000000000000000000008e1584de58abb84df88eb2afd8eb0","0x0000000000000000000000000000000950590267c551136f6421674fe0e75b1b","0x0000000000000000000000000000000000090a0a0c36773bf2b7368171949f40"] \ No newline at end of file